Add icon-tile-stack rule and cross-validate engine against skill

A new icon-tile-stack detection (the canonical AI feature-card with a
small rounded-square icon container above a heading), backed by a
two-column TDD fixture, plus a single-source-of-truth design that ties
the engine to the impeccable skill so they can no longer drift silently.

Detection
- New icon-tile-stack rule (slop): heading's previousElementSibling is
  a 32–128px rounded-square element with a non-transparent background
  or border, contains an svg/icon-i child, and sits above (not next to)
  the heading. Excludes round avatars, wide thumbnails, side-by-side
  layouts, tiny icons, and hero images.
- Two-column fixture convention: a single icon-tile-stack.html with a
  flag column (4 cases) and pass column (6 cases), with snippet-text
  matching used by the fixture test.

Single source of truth
- Each ANTIPATTERNS entry can now declare skillSection + skillGuideline.
  18 of 25 rules carry these fields; the build's new
  validateAntipatternRules() in scripts/build.js fails if any declared
  skillGuideline isn't found verbatim in the right SKILL.md section.
- scripts/build-extension.js now includes the description field in
  extension/detector/antipatterns.json (it was previously dropped).
- The existing count validator was promoted from warn to error so
  command count drift fails the build the same way detection drift does.

Impeccable skill DON'Ts
- Added 4 new top-level DON'Ts that target real default AI behavior:
  single-font, flat-type-hierarchy, all-caps-body, line-length.
- Cut 7 new DON'Ts I had drafted (tight-leading, tiny-text, wide-tracking,
  justified-text, low-contrast, cramped-padding, skipped-heading) because
  they teach things every model already knows from CSS/a11y basics. The
  detector still catches all of them.

Stale count cleanup
- 22 commands → 21 across 17 references in HTML, README, NOTICE, AGENTS,
  plugin.json, marketplace.json (left over from the validate skill removal).
- Dropped the hand-coded "212 design guidelines" marketing copy on the
  homepage, which never mapped to any real count.

Sub-agent
- New private .claude/agents/anti-patterns.md captures the full TDD
  recipe, schema, plug-in points, jsdom constraints, and pre-commit
  checklist so future sessions can add rules end-to-end without
  re-investigating the wiring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-06 22:58:13 -07:00
co-authored by Claude Opus 4.6
parent 83330a66cb
commit e1032b7285
27 changed files with 1026 additions and 30 deletions
+9 -2
View File
@@ -57,9 +57,16 @@ console.log(`Generated ${path.relative(ROOT, DETECTOR_OUTPUT)} (${(output.length
const rawSource = fs.readFileSync(SOURCE, 'utf-8');
const apMatch = rawSource.match(/const ANTIPATTERNS = \[([\s\S]*?)\n\];/);
if (apMatch) {
// Convert JS object literals to JSON
// Convert JS object literals to JSON. Include description so the
// devtools panel can show the full rule explanation in tooltips —
// previously this dropped description and the panel had nothing to display.
const antipatterns = new Function(`return [${apMatch[1]}]`)();
const apJson = antipatterns.map(({ id, name, category }) => ({ id, name, category: category || 'quality' }));
const apJson = antipatterns.map(({ id, name, category, description }) => ({
id,
name,
category: category || 'quality',
description: description || '',
}));
fs.writeFileSync(AP_OUTPUT, JSON.stringify(apJson, null, 2) + '\n');
console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${antipatterns.length} rules)`);
}
+72 -8
View File
@@ -64,7 +64,7 @@ function generateCounts(rootDir, skills, buildDir) {
'.claude-plugin/marketplace.json',
];
let warnings = 0;
let errors = 0;
for (const relPath of filesToCheck) {
const absPath = path.join(rootDir, relPath);
if (!fs.existsSync(absPath)) continue;
@@ -78,8 +78,8 @@ function generateCounts(rootDir, skills, buildDir) {
const num = parseInt(match[1]);
// Allow 1 (for "1 skill") and the correct count
if (num !== commandCount && num !== 1) {
console.warn(` ⚠️ ${relPath}: found "${match[0]}" but active command count is ${commandCount}`);
warnings++;
console.error(` ${relPath}: found "${match[0]}" but active command count is ${commandCount}`);
errors++;
}
}
@@ -88,17 +88,75 @@ function generateCounts(rootDir, skills, buildDir) {
for (const match of content.matchAll(detectPattern)) {
const num = parseInt(match[1]);
if (num !== detectionCount && num > 10) { // ignore small numbers like "3 patterns"
console.warn(` ⚠️ ${relPath}: found "${match[0]}" but detection count is ${detectionCount}`);
warnings++;
console.error(` ${relPath}: found "${match[0]}" but detection count is ${detectionCount}`);
errors++;
}
}
}
if (warnings > 0) {
console.warn(`\n⚠️ ${warnings} stale count reference(s) found. Update them to match source of truth.`);
if (errors > 0) {
console.error(`\n ${errors} stale count reference(s) found. Update them to match source of truth.`);
}
console.log(`✓ Generated counts: ${commandCount} commands, ${detectionCount} detection rules`);
return errors;
}
/**
* Cross-validate that every detection rule with a `skillGuideline` has a
* matching DON'T line in the right section of source/skills/impeccable/SKILL.md.
*
* This is the linchpin of the single-source-of-truth design: it catches drift
* between the engine's ANTIPATTERNS and the human-written DO/DON'T prose.
*
* Returns the number of validation errors. Build fails if > 0.
*/
function validateAntipatternRules(rootDir) {
const detectPath = path.join(rootDir, 'src/detect-antipatterns.mjs');
const src = fs.readFileSync(detectPath, 'utf-8');
const apMatch = src.match(/const ANTIPATTERNS = \[([\s\S]*?)\n\];/);
if (!apMatch) {
console.error(' ❌ Could not extract ANTIPATTERNS from detect-antipatterns.mjs');
return 1;
}
const antipatterns = new Function(`return [${apMatch[1]}]`)();
const { antipatterns: skillSections } = readPatterns(rootDir);
// Build section -> joined-DON'T-text lookup for substring matching
const sectionText = {};
for (const section of skillSections) {
sectionText[section.name] = section.items.join('\n');
}
let errors = 0;
let validated = 0;
for (const rule of antipatterns) {
if (!rule.skillGuideline) continue;
if (!rule.skillSection) {
console.error(` ❌ Rule '${rule.id}' declares skillGuideline but no skillSection`);
errors++;
continue;
}
const text = sectionText[rule.skillSection];
if (!text) {
console.error(` ❌ Rule '${rule.id}': skillSection '${rule.skillSection}' has no DON'T lines in source/skills/impeccable/SKILL.md`);
errors++;
continue;
}
if (!text.includes(rule.skillGuideline)) {
console.error(` ❌ Rule '${rule.id}': skillGuideline '${rule.skillGuideline}' not found in any **DON'T** of section '${rule.skillSection}' in source/skills/impeccable/SKILL.md`);
errors++;
continue;
}
validated++;
}
if (errors > 0) {
console.error(`\n${errors} anti-pattern rule(s) drift between src/detect-antipatterns.mjs and source/skills/impeccable/SKILL.md`);
} else {
console.log(`✓ Validated ${validated}/${antipatterns.length} anti-pattern rules against impeccable SKILL.md`);
}
return errors;
}
/**
@@ -441,7 +499,13 @@ async function build() {
// Generate authoritative counts and validate references
generateCounts(ROOT_DIR, skills, buildDir);
const countErrors = generateCounts(ROOT_DIR, skills, buildDir);
// Cross-validate engine rules against impeccable SKILL.md DON'Ts
const validationErrors = validateAntipatternRules(ROOT_DIR);
if (countErrors > 0 || validationErrors > 0) {
process.exit(1);
}
console.log('\n✨ Build complete!');
}