mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
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:
co-authored by
Claude Opus 4.6
parent
83330a66cb
commit
e1032b7285
@@ -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
@@ -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!');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user