From 0728eaadea78598c03cedbda0022bc2aeec0bf30 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 8 Apr 2026 11:38:26 -0700 Subject: [PATCH] Allow XML-block prose form in SKILL.md DON'T parser Recognize "DO NOT" / "DO" lines (with optional colon) inside and blocks, and make skillGuideline substring matching case-insensitive so the validator handles the new XML-structured SKILL.md without rejecting the refactored prose. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/build.js | 12 ++++++---- scripts/lib/utils.js | 57 ++++++++++++++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/scripts/build.js b/scripts/build.js index 7bd683a7f..6c8980f6e 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -122,10 +122,14 @@ function validateAntipatternRules(rootDir) { const antipatterns = new Function(`return [${apMatch[1]}]`)(); const { antipatterns: skillSections } = readPatterns(rootDir); - // Build section -> joined-DON'T-text lookup for substring matching + // 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'); + sectionText[section.name] = section.items.join('\n').toLowerCase(); } let errors = 0; @@ -143,8 +147,8 @@ function validateAntipatternRules(rootDir) { 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`); + 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; } diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js index 360ac96d7..3ea386c32 100644 --- a/scripts/lib/utils.js +++ b/scripts/lib/utils.js @@ -215,7 +215,11 @@ export function writeFile(filePath, content) { /** * Extract patterns from frontend-design SKILL.md - * Parses **DO**: and **DON'T**: lines, grouped by section headings + * Parses DO/DON'T lines grouped by section headings. + * Recognizes both formats: + * - Markdown bullet form: `**DO**: …` / `**DON'T**: …` + * - XML-block prose form: `DO …` / `DO NOT …` (used inside + * , , , ) * Returns { patterns: [...], antipatterns: [...] } */ export function readPatterns(rootDir) { @@ -232,6 +236,17 @@ export function readPatterns(rootDir) { const antipatternsMap = {}; // category -> items[] let currentSection = null; + const pushPattern = (item) => { + if (!currentSection) return; + if (!patternsMap[currentSection]) patternsMap[currentSection] = []; + patternsMap[currentSection].push(item); + }; + const pushAntipattern = (item) => { + if (!currentSection) return; + if (!antipatternsMap[currentSection]) antipatternsMap[currentSection] = []; + antipatternsMap[currentSection].push(item); + }; + for (const line of lines) { const trimmed = line.trim(); @@ -245,23 +260,35 @@ export function readPatterns(rootDir) { continue; } - // Parse **DO**: lines - if (trimmed.startsWith('**DO**:') && currentSection) { - const item = trimmed.slice(7).trim(); - if (!patternsMap[currentSection]) { - patternsMap[currentSection] = []; - } - patternsMap[currentSection].push(item); + // Markdown bullet form (legacy): **DO**: ... and **DON'T**: ... + if (trimmed.startsWith('**DO**:')) { + pushPattern(trimmed.slice(7).trim()); + continue; + } + if (trimmed.startsWith("**DON'T**:")) { + pushAntipattern(trimmed.slice(10).trim()); continue; } - // Parse **DON'T**: lines - if (trimmed.startsWith("**DON'T**:") && currentSection) { - const item = trimmed.slice(10).trim(); - if (!antipatternsMap[currentSection]) { - antipatternsMap[currentSection] = []; - } - antipatternsMap[currentSection].push(item); + // XML-block prose form (current). Both space and colon variants: + // "DO NOT use ..." / "DO NOT: Use ..." + // "DO use ..." / "DO: Use ..." + // IMPORTANT: check `DO NOT` BEFORE `DO` so the prefix doesn't get + // gobbled by the wrong matcher. + if (trimmed.startsWith('DO NOT: ')) { + pushAntipattern(trimmed.slice('DO NOT: '.length).trim()); + continue; + } + if (trimmed.startsWith('DO NOT ')) { + pushAntipattern(trimmed.slice('DO NOT '.length).trim()); + continue; + } + if (trimmed.startsWith('DO: ')) { + pushPattern(trimmed.slice('DO: '.length).trim()); + continue; + } + if (trimmed.startsWith('DO ')) { + pushPattern(trimmed.slice('DO '.length).trim()); continue; } }