mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 14:16:28 +03:00
feat(skill): register split, color strategy, and pre-design intake
Splits the skill into two register references (editorial, product), replaces category-based theme selection with a forced physical-scene inference, and introduces a four-step color strategy axis (Restrained / Committed / Full palette / Drenched) with editorial permission for the bold three. Adds a seed mode to /impeccable document for pre-implementation projects, updates /impeccable teach Step 5 to offer the seed path, and grows /impeccable shape with Design Direction + Scope intake (fidelity, breadth, interactivity, time). Extends live-mode variant distinctness to forbid three variants sharing theme and dominant hue. Also drops the anti-pattern validator coupling, consolidates a11y into audit.md, and updates CLAUDE.md with the register architecture. 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
81f880d030
commit
4daabe5232
+5
-67
@@ -33,8 +33,10 @@ function generateCounts(rootDir, skills, buildDir) {
|
||||
const impeccableSkill = skills.find(s => s.name === 'impeccable');
|
||||
let commandCount;
|
||||
if (impeccableSkill) {
|
||||
// Count lines in the router table that have a | `command` | pattern
|
||||
const routerMatches = impeccableSkill.body.match(/^\| `\w+` \|/gm);
|
||||
// Count lines in the command table that start with | `...` | — tolerant
|
||||
// of argument hints inside the backticks (e.g. `craft [feature]`) and of
|
||||
// multi-word commands (e.g. `pin <command>`).
|
||||
const routerMatches = impeccableSkill.body.match(/^\| `[^`]+` \|/gm);
|
||||
commandCount = routerMatches ? routerMatches.length : 0;
|
||||
} else {
|
||||
// Fallback: count user-invocable skills
|
||||
@@ -125,67 +127,6 @@ function validateSkillFrontmatter(skills) {
|
||||
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.
|
||||
// Lowercased for case-insensitive matching: my XML refactor uses sentence-
|
||||
// case "DO NOT nest cards" while the rules' skillGuideline strings are
|
||||
// sentence-cased "Nest cards inside cards" (a fragment from the original
|
||||
// markdown bullet "**DON'T**: Nest cards inside cards.").
|
||||
const sectionText = {};
|
||||
for (const section of skillSections) {
|
||||
sectionText[section.name] = section.items.join('\n').toLowerCase();
|
||||
}
|
||||
|
||||
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.toLowerCase())) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan user-facing copy for em dashes (— or —).
|
||||
* Em dashes in project copy are a known anti-pattern here; flag them loudly.
|
||||
@@ -719,16 +660,13 @@ async function build() {
|
||||
// Generate authoritative counts and validate references
|
||||
const countErrors = generateCounts(ROOT_DIR, skills, buildDir);
|
||||
|
||||
// Cross-validate engine rules against impeccable SKILL.md DON'Ts
|
||||
const validationErrors = validateAntipatternRules(ROOT_DIR);
|
||||
|
||||
// Verify every hand-authored HTML page carries the shared site header
|
||||
const headerErrors = validateSiteHeader(ROOT_DIR);
|
||||
|
||||
// Scan user-facing copy for em dashes
|
||||
const emDashErrors = validateNoEmDashes(ROOT_DIR);
|
||||
|
||||
if (countErrors > 0 || validationErrors > 0 || headerErrors > 0 || emDashErrors > 0) {
|
||||
if (countErrors > 0 || headerErrors > 0 || emDashErrors > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
+11
-7
@@ -214,16 +214,20 @@ export function writeFile(filePath, content) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract patterns from frontend-design SKILL.md
|
||||
* Parses DO/DON'T lines grouped by section headings.
|
||||
* Recognizes both formats:
|
||||
* Extract DO/DON'T patterns from a skill markdown file, grouped by section
|
||||
* (h3 `### ` headings). Recognizes both formats:
|
||||
* - Markdown bullet form: `**DO**: …` / `**DON'T**: …`
|
||||
* - XML-block prose form: `DO …` / `DO NOT …` (used inside
|
||||
* <typography_rules>, <color_rules>, <spatial_rules>, <absolute_bans>)
|
||||
* - Prose form: `DO …` / `DO NOT …`
|
||||
*
|
||||
* Defaults to the main impeccable SKILL.md but accepts any relative path so
|
||||
* rules in `src/detect-antipatterns.mjs` can anchor to register-specific
|
||||
* reference files (e.g. `reference/editorial.md`) via an optional `skillFile`
|
||||
* field. Callers that don't pass `relativePath` get the legacy behavior.
|
||||
*
|
||||
* Returns { patterns: [...], antipatterns: [...] }
|
||||
*/
|
||||
export function readPatterns(rootDir) {
|
||||
const skillPath = path.join(rootDir, 'source/skills/impeccable/SKILL.md');
|
||||
export function readPatterns(rootDir, relativePath = 'source/skills/impeccable/SKILL.md') {
|
||||
const skillPath = path.join(rootDir, relativePath);
|
||||
|
||||
if (!fs.existsSync(skillPath)) {
|
||||
return { patterns: [], antipatterns: [] };
|
||||
|
||||
Reference in New Issue
Block a user