mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +03:00
skill: drop quality tiers, keep the real brand-craft guardrails
Codex's craft/brand pass introduced fast/ship/showpiece "quality bars" plus brand-specific build gates, asset ledgers, sub-agent review, and self-graded fallback labels. In practice those tiers became escape hatches rather than craft pressure: the final output should always be 10/10, and the real decision points are splashiness and maximalism, not quality. Removed: - All quality-bar / showpiece / fast / ship framing in shape.md and craft.md - Standalone Brand Direction (#4) and Asset Requirements (#10) sections in shape's brief; renumbered back to 1-10 - The Brand hard rules section in brand.md (folded its real prohibitions into the existing Imagery and Brand bans sections) - Brand-specific build-gate item, mock-fidelity bullet, production-bar bullet, present-step bullet in craft.md - Asset ledger ceremony in craft Step 4 - Review-only sub-agents and "self-reviewed fallback, not independently validated" machinery in craft.md and polish.md - The For brand surfaces, assess hard failures subsection in polish.md and the brand checklist row - tests/brand-showpiece-reference.test.mjs (and its package.json wiring) Kept (the real nuggets): - Asset-substitution prohibition: image-led briefs ship real/generated assets or canvas/SVG/WebGL, not generic CSS panels, cards, bullets, or copy - Repeated tiny uppercase tracked kicker labels as a brand ban - Detector/QA output is defect evidence only, never proof of quality - "What visual assets are real content here?" discovery question - Inspect each major section individually for brand and long-form work - repeated-section-kickers detection rule + fixture - CLI improvements (JSON to stdout, -json/-fast aliases, severity field) - critique.md: npx impeccable detect --json fix Harness output dirs refreshed via bun run build. Full test suite (186) passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
e587004ee4
commit
a8b032d362
@@ -257,6 +257,16 @@ const ANTIPATTERNS = [
|
||||
skillSection: 'Typography',
|
||||
skillGuideline: 'tiny uppercase tracked label above the hero headline',
|
||||
},
|
||||
{
|
||||
id: 'repeated-section-kickers',
|
||||
category: 'slop',
|
||||
severity: 'advisory',
|
||||
name: 'Repeated section kicker labels',
|
||||
description:
|
||||
'Repeating tiny uppercase tracked labels above section headings turns a brand page into AI editorial scaffolding. Replace them with stronger structure, artifacts, imagery, or a deliberate brand system.',
|
||||
skillSection: 'Typography',
|
||||
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
|
||||
},
|
||||
|
||||
// ── Quality: general design and accessibility issues ──
|
||||
{
|
||||
@@ -733,6 +743,15 @@ function checkHeroEyebrow(opts) {
|
||||
}];
|
||||
}
|
||||
|
||||
function checkRepeatedSectionKickers(opts) {
|
||||
const { candidates, minCount = 3 } = opts;
|
||||
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
|
||||
return candidates.map(candidate => ({
|
||||
id: 'repeated-section-kickers',
|
||||
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
|
||||
}));
|
||||
}
|
||||
|
||||
const LAYOUT_TRANSITION_PROPS = new Set([
|
||||
'width', 'height', 'padding', 'margin',
|
||||
'max-height', 'max-width', 'min-height', 'min-width',
|
||||
@@ -1231,6 +1250,107 @@ function checkElementHeroEyebrowDOM(el) {
|
||||
});
|
||||
}
|
||||
|
||||
const REPEATED_KICKER_SKIP_SELECTOR = [
|
||||
'nav',
|
||||
'form',
|
||||
'table',
|
||||
'thead',
|
||||
'tbody',
|
||||
'tfoot',
|
||||
'figure',
|
||||
'figcaption',
|
||||
'ol',
|
||||
'ul',
|
||||
'li',
|
||||
'[role="navigation"]',
|
||||
'[aria-label*="breadcrumb" i]',
|
||||
'[class*="breadcrumb" i]',
|
||||
'[data-impeccable-allow-kickers]',
|
||||
].join(',');
|
||||
|
||||
function cleanInlineText(el) {
|
||||
return [...el.childNodes]
|
||||
.filter(n => n.nodeType === 3)
|
||||
.map(n => n.textContent)
|
||||
.join(' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isRepeatedKickerCandidate(opts) {
|
||||
const {
|
||||
headingTag,
|
||||
headingText,
|
||||
headingFontSize,
|
||||
kickerTag,
|
||||
kickerText,
|
||||
kickerTextTransform,
|
||||
kickerFontSize,
|
||||
kickerLetterSpacing,
|
||||
} = opts;
|
||||
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
|
||||
if (!headingText || headingText.length < 3) return false;
|
||||
if (!(headingFontSize >= 20)) return false;
|
||||
if (!kickerTag || HEADING_TAGS.has(kickerTag)) return false;
|
||||
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
|
||||
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
|
||||
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
|
||||
|
||||
const isUppercased = kickerTextTransform === 'uppercase'
|
||||
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|
||||
if (!isUppercased) return false;
|
||||
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
|
||||
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
|
||||
if (!(kickerLetterSpacing >= minTrackedSpacing)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
|
||||
const candidates = [];
|
||||
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
|
||||
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
|
||||
const kicker = heading.previousElementSibling;
|
||||
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
|
||||
|
||||
const headingStyle = getStyle(heading);
|
||||
const kickerStyle = getStyle(kicker);
|
||||
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
|
||||
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
|
||||
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
|
||||
|
||||
if (!isRepeatedKickerCandidate({
|
||||
headingTag: heading.tagName.toLowerCase(),
|
||||
headingText,
|
||||
headingFontSize,
|
||||
kickerTag: kicker.tagName.toLowerCase(),
|
||||
kickerText,
|
||||
kickerTextTransform: kickerStyle.textTransform || '',
|
||||
kickerFontSize,
|
||||
kickerLetterSpacing,
|
||||
})) {
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
headingTag: heading.tagName.toLowerCase(),
|
||||
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
|
||||
kickerText: kickerText.slice(0, 40),
|
||||
});
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function checkRepeatedSectionKickersDOM() {
|
||||
const candidates = collectRepeatedSectionKickerCandidates(
|
||||
document,
|
||||
(el) => getComputedStyle(el),
|
||||
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
|
||||
);
|
||||
return checkRepeatedSectionKickers({ candidates });
|
||||
}
|
||||
|
||||
function checkElementMotionDOM(el) {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (SAFE_TAGS.has(tag)) return [];
|
||||
@@ -1658,6 +1778,15 @@ function checkElementHeroEyebrow(el, style, tag, window) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkRepeatedSectionKickersFromDoc(doc, win) {
|
||||
const candidates = collectRepeatedSectionKickerCandidates(
|
||||
doc,
|
||||
(el) => win.getComputedStyle(el),
|
||||
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
|
||||
);
|
||||
return checkRepeatedSectionKickers({ candidates });
|
||||
}
|
||||
|
||||
function checkElementMotion(tag, style) {
|
||||
return checkMotion({
|
||||
tag,
|
||||
@@ -2511,6 +2640,7 @@ if (IS_BROWSER) {
|
||||
return {
|
||||
type: f.type || f.id,
|
||||
category: ap ? ap.category : 'quality',
|
||||
severity: ap?.severity || 'warning',
|
||||
detail: f.detail || f.snippet,
|
||||
name: ap ? ap.name : (f.type || f.id),
|
||||
description: ap ? ap.description : '',
|
||||
@@ -2585,6 +2715,14 @@ if (IS_BROWSER) {
|
||||
allFindings.push({ el: document.body, findings: typoFindings });
|
||||
}
|
||||
|
||||
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
|
||||
.map(f => ({ type: f.id, detail: f.snippet }))
|
||||
.filter(f => _ruleOk(f.type));
|
||||
if (sectionKickerFindings.length > 0) {
|
||||
pageLevelFindings.push(...sectionKickerFindings);
|
||||
allFindings.push({ el: document.body, findings: sectionKickerFindings });
|
||||
}
|
||||
|
||||
const layoutFindings = checkLayout().filter(f => _ruleOk(f.type));
|
||||
for (const f of layoutFindings) {
|
||||
const el = f.el || document.body;
|
||||
|
||||
@@ -253,6 +253,16 @@ const ANTIPATTERNS = [
|
||||
skillSection: 'Typography',
|
||||
skillGuideline: 'tiny uppercase tracked label above the hero headline',
|
||||
},
|
||||
{
|
||||
id: 'repeated-section-kickers',
|
||||
category: 'slop',
|
||||
severity: 'advisory',
|
||||
name: 'Repeated section kicker labels',
|
||||
description:
|
||||
'Repeating tiny uppercase tracked labels above section headings turns a brand page into AI editorial scaffolding. Replace them with stronger structure, artifacts, imagery, or a deliberate brand system.',
|
||||
skillSection: 'Typography',
|
||||
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
|
||||
},
|
||||
|
||||
// ── Quality: general design and accessibility issues ──
|
||||
{
|
||||
@@ -729,6 +739,15 @@ function checkHeroEyebrow(opts) {
|
||||
}];
|
||||
}
|
||||
|
||||
function checkRepeatedSectionKickers(opts) {
|
||||
const { candidates, minCount = 3 } = opts;
|
||||
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
|
||||
return candidates.map(candidate => ({
|
||||
id: 'repeated-section-kickers',
|
||||
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
|
||||
}));
|
||||
}
|
||||
|
||||
const LAYOUT_TRANSITION_PROPS = new Set([
|
||||
'width', 'height', 'padding', 'margin',
|
||||
'max-height', 'max-width', 'min-height', 'min-width',
|
||||
@@ -1227,6 +1246,107 @@ function checkElementHeroEyebrowDOM(el) {
|
||||
});
|
||||
}
|
||||
|
||||
const REPEATED_KICKER_SKIP_SELECTOR = [
|
||||
'nav',
|
||||
'form',
|
||||
'table',
|
||||
'thead',
|
||||
'tbody',
|
||||
'tfoot',
|
||||
'figure',
|
||||
'figcaption',
|
||||
'ol',
|
||||
'ul',
|
||||
'li',
|
||||
'[role="navigation"]',
|
||||
'[aria-label*="breadcrumb" i]',
|
||||
'[class*="breadcrumb" i]',
|
||||
'[data-impeccable-allow-kickers]',
|
||||
].join(',');
|
||||
|
||||
function cleanInlineText(el) {
|
||||
return [...el.childNodes]
|
||||
.filter(n => n.nodeType === 3)
|
||||
.map(n => n.textContent)
|
||||
.join(' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isRepeatedKickerCandidate(opts) {
|
||||
const {
|
||||
headingTag,
|
||||
headingText,
|
||||
headingFontSize,
|
||||
kickerTag,
|
||||
kickerText,
|
||||
kickerTextTransform,
|
||||
kickerFontSize,
|
||||
kickerLetterSpacing,
|
||||
} = opts;
|
||||
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
|
||||
if (!headingText || headingText.length < 3) return false;
|
||||
if (!(headingFontSize >= 20)) return false;
|
||||
if (!kickerTag || HEADING_TAGS.has(kickerTag)) return false;
|
||||
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
|
||||
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
|
||||
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
|
||||
|
||||
const isUppercased = kickerTextTransform === 'uppercase'
|
||||
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|
||||
if (!isUppercased) return false;
|
||||
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
|
||||
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
|
||||
if (!(kickerLetterSpacing >= minTrackedSpacing)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
|
||||
const candidates = [];
|
||||
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
|
||||
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
|
||||
const kicker = heading.previousElementSibling;
|
||||
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
|
||||
|
||||
const headingStyle = getStyle(heading);
|
||||
const kickerStyle = getStyle(kicker);
|
||||
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
|
||||
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
|
||||
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
|
||||
|
||||
if (!isRepeatedKickerCandidate({
|
||||
headingTag: heading.tagName.toLowerCase(),
|
||||
headingText,
|
||||
headingFontSize,
|
||||
kickerTag: kicker.tagName.toLowerCase(),
|
||||
kickerText,
|
||||
kickerTextTransform: kickerStyle.textTransform || '',
|
||||
kickerFontSize,
|
||||
kickerLetterSpacing,
|
||||
})) {
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
headingTag: heading.tagName.toLowerCase(),
|
||||
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
|
||||
kickerText: kickerText.slice(0, 40),
|
||||
});
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function checkRepeatedSectionKickersDOM() {
|
||||
const candidates = collectRepeatedSectionKickerCandidates(
|
||||
document,
|
||||
(el) => getComputedStyle(el),
|
||||
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
|
||||
);
|
||||
return checkRepeatedSectionKickers({ candidates });
|
||||
}
|
||||
|
||||
function checkElementMotionDOM(el) {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (SAFE_TAGS.has(tag)) return [];
|
||||
@@ -1654,6 +1774,15 @@ function checkElementHeroEyebrow(el, style, tag, window) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkRepeatedSectionKickersFromDoc(doc, win) {
|
||||
const candidates = collectRepeatedSectionKickerCandidates(
|
||||
doc,
|
||||
(el) => win.getComputedStyle(el),
|
||||
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
|
||||
);
|
||||
return checkRepeatedSectionKickers({ candidates });
|
||||
}
|
||||
|
||||
function checkElementMotion(tag, style) {
|
||||
return checkMotion({
|
||||
tag,
|
||||
@@ -2507,6 +2636,7 @@ if (IS_BROWSER) {
|
||||
return {
|
||||
type: f.type || f.id,
|
||||
category: ap ? ap.category : 'quality',
|
||||
severity: ap?.severity || 'warning',
|
||||
detail: f.detail || f.snippet,
|
||||
name: ap ? ap.name : (f.type || f.id),
|
||||
description: ap ? ap.description : '',
|
||||
@@ -2581,6 +2711,14 @@ if (IS_BROWSER) {
|
||||
allFindings.push({ el: document.body, findings: typoFindings });
|
||||
}
|
||||
|
||||
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
|
||||
.map(f => ({ type: f.id, detail: f.snippet }))
|
||||
.filter(f => _ruleOk(f.type));
|
||||
if (sectionKickerFindings.length > 0) {
|
||||
pageLevelFindings.push(...sectionKickerFindings);
|
||||
allFindings.push({ el: document.body, findings: sectionKickerFindings });
|
||||
}
|
||||
|
||||
const layoutFindings = checkLayout().filter(f => _ruleOk(f.type));
|
||||
for (const f of layoutFindings) {
|
||||
const el = f.el || document.body;
|
||||
@@ -2719,7 +2857,7 @@ function getAP(id) {
|
||||
|
||||
function finding(id, filePath, snippet, line = 0) {
|
||||
const ap = getAP(id);
|
||||
return { antipattern: id, name: ap.name, description: ap.description, file: filePath, line, snippet };
|
||||
return { antipattern: id, name: ap.name, description: ap.description, severity: ap.severity || 'warning', file: filePath, line, snippet };
|
||||
}
|
||||
|
||||
/** Check if content looks like a full page (not a component/partial) */
|
||||
@@ -2985,6 +3123,9 @@ async function detectHtml(filePath) {
|
||||
for (const f of checkPageTypography(document, window)) {
|
||||
findings.push(finding(f.id, filePath, f.snippet));
|
||||
}
|
||||
for (const f of checkRepeatedSectionKickersFromDoc(document, window)) {
|
||||
findings.push(finding(f.id, filePath, f.snippet));
|
||||
}
|
||||
for (const f of checkPageLayout(document, window)) {
|
||||
findings.push(finding(f.id, filePath, f.snippet));
|
||||
}
|
||||
@@ -3653,7 +3794,11 @@ Examples:
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const args = process.argv.slice(2).map(arg => {
|
||||
if (arg === '-json') return '--json';
|
||||
if (arg === '-fast') return '--fast';
|
||||
return arg;
|
||||
});
|
||||
const jsonMode = args.includes('--json');
|
||||
const helpMode = args.includes('--help');
|
||||
const fastMode = args.includes('--fast');
|
||||
@@ -3762,7 +3907,8 @@ async function main() {
|
||||
}
|
||||
|
||||
if (allFindings.length > 0) {
|
||||
process.stderr.write(formatFindings(allFindings, jsonMode) + '\n');
|
||||
if (jsonMode) process.stdout.write(formatFindings(allFindings, true) + '\n');
|
||||
else process.stderr.write(formatFindings(allFindings, false) + '\n');
|
||||
process.exit(2);
|
||||
}
|
||||
if (jsonMode) process.stdout.write('[]\n');
|
||||
|
||||
Reference in New Issue
Block a user