Allow XML-block prose form in SKILL.md DON'T parser

Recognize "DO NOT" / "DO" lines (with optional colon) inside <rules>
and <absolute_bans> 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) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-08 11:38:26 -07:00
co-authored by Claude Opus 4.6
parent 282987ad7b
commit 0728eaadea
2 changed files with 50 additions and 19 deletions
+8 -4
View File
@@ -122,10 +122,14 @@ function validateAntipatternRules(rootDir) {
const antipatterns = new Function(`return [${apMatch[1]}]`)(); const antipatterns = new Function(`return [${apMatch[1]}]`)();
const { antipatterns: skillSections } = readPatterns(rootDir); 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 = {}; const sectionText = {};
for (const section of skillSections) { for (const section of skillSections) {
sectionText[section.name] = section.items.join('\n'); sectionText[section.name] = section.items.join('\n').toLowerCase();
} }
let errors = 0; let errors = 0;
@@ -143,8 +147,8 @@ function validateAntipatternRules(rootDir) {
errors++; errors++;
continue; continue;
} }
if (!text.includes(rule.skillGuideline)) { 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`); 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++; errors++;
continue; continue;
} }
+42 -15
View File
@@ -215,7 +215,11 @@ export function writeFile(filePath, content) {
/** /**
* Extract patterns from frontend-design SKILL.md * 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
* <typography_rules>, <color_rules>, <spatial_rules>, <absolute_bans>)
* Returns { patterns: [...], antipatterns: [...] } * Returns { patterns: [...], antipatterns: [...] }
*/ */
export function readPatterns(rootDir) { export function readPatterns(rootDir) {
@@ -232,6 +236,17 @@ export function readPatterns(rootDir) {
const antipatternsMap = {}; // category -> items[] const antipatternsMap = {}; // category -> items[]
let currentSection = null; 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) { for (const line of lines) {
const trimmed = line.trim(); const trimmed = line.trim();
@@ -245,23 +260,35 @@ export function readPatterns(rootDir) {
continue; continue;
} }
// Parse **DO**: lines // Markdown bullet form (legacy): **DO**: ... and **DON'T**: ...
if (trimmed.startsWith('**DO**:') && currentSection) { if (trimmed.startsWith('**DO**:')) {
const item = trimmed.slice(7).trim(); pushPattern(trimmed.slice(7).trim());
if (!patternsMap[currentSection]) { continue;
patternsMap[currentSection] = []; }
} if (trimmed.startsWith("**DON'T**:")) {
patternsMap[currentSection].push(item); pushAntipattern(trimmed.slice(10).trim());
continue; continue;
} }
// Parse **DON'T**: lines // XML-block prose form (current). Both space and colon variants:
if (trimmed.startsWith("**DON'T**:") && currentSection) { // "DO NOT use ..." / "DO NOT: Use ..."
const item = trimmed.slice(10).trim(); // "DO use ..." / "DO: Use ..."
if (!antipatternsMap[currentSection]) { // IMPORTANT: check `DO NOT` BEFORE `DO` so the prefix doesn't get
antipatternsMap[currentSection] = []; // gobbled by the wrong matcher.
} if (trimmed.startsWith('DO NOT: ')) {
antipatternsMap[currentSection].push(item); 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; continue;
} }
} }