fix: tighten frontmatter parsing and add SKILL-07 body content check
- Require \n---\n (not just \n---) for closing frontmatter delimiter in both parseFrontmatter and parseFrontmatterMultiline, with fallback for files ending in \n--- - Add SKILL-07: SKILL.md must have non-empty body content after frontmatter (L2 instructions are required) - Update rule count to 14 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ac5cb552c0
commit
7a214cc7d8
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Deterministic Skill Validator
|
* Deterministic Skill Validator
|
||||||
*
|
*
|
||||||
* Validates 13 deterministic rules across all skill directories.
|
* Validates 14 deterministic rules across all skill directories.
|
||||||
* Acts as a fast first-pass complement to the inference-based skill validator.
|
* Acts as a fast first-pass complement to the inference-based skill validator.
|
||||||
*
|
*
|
||||||
* What it checks:
|
* What it checks:
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
* - SKILL-04: name format (lowercase, hyphens, no forbidden substrings)
|
* - SKILL-04: name format (lowercase, hyphens, no forbidden substrings)
|
||||||
* - SKILL-05: name matches directory basename
|
* - SKILL-05: name matches directory basename
|
||||||
* - SKILL-06: description quality (length, "Use when"/"Use if")
|
* - SKILL-06: description quality (length, "Use when"/"Use if")
|
||||||
|
* - SKILL-07: SKILL.md has body content after frontmatter
|
||||||
* - WF-01: workflow.md frontmatter has no name
|
* - WF-01: workflow.md frontmatter has no name
|
||||||
* - WF-02: workflow.md frontmatter has no description
|
* - WF-02: workflow.md frontmatter has no description
|
||||||
* - PATH-02: no installed_path variable
|
* - PATH-02: no installed_path variable
|
||||||
|
|
@ -68,8 +69,15 @@ function parseFrontmatter(content) {
|
||||||
const trimmed = content.trimStart();
|
const trimmed = content.trimStart();
|
||||||
if (!trimmed.startsWith('---')) return null;
|
if (!trimmed.startsWith('---')) return null;
|
||||||
|
|
||||||
const endIndex = trimmed.indexOf('\n---', 3);
|
let endIndex = trimmed.indexOf('\n---\n', 3);
|
||||||
if (endIndex === -1) return null;
|
if (endIndex === -1) {
|
||||||
|
// Handle file ending with \n---
|
||||||
|
if (trimmed.endsWith('\n---')) {
|
||||||
|
endIndex = trimmed.length - 4;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fmBlock = trimmed.slice(3, endIndex).trim();
|
const fmBlock = trimmed.slice(3, endIndex).trim();
|
||||||
if (fmBlock === '') return {};
|
if (fmBlock === '') return {};
|
||||||
|
|
@ -100,8 +108,15 @@ function parseFrontmatterMultiline(content) {
|
||||||
const trimmed = content.trimStart();
|
const trimmed = content.trimStart();
|
||||||
if (!trimmed.startsWith('---')) return null;
|
if (!trimmed.startsWith('---')) return null;
|
||||||
|
|
||||||
const endIndex = trimmed.indexOf('\n---', 3);
|
let endIndex = trimmed.indexOf('\n---\n', 3);
|
||||||
if (endIndex === -1) return null;
|
if (endIndex === -1) {
|
||||||
|
// Handle file ending with \n---
|
||||||
|
if (trimmed.endsWith('\n---')) {
|
||||||
|
endIndex = trimmed.length - 4;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fmBlock = trimmed.slice(3, endIndex).trim();
|
const fmBlock = trimmed.slice(3, endIndex).trim();
|
||||||
if (fmBlock === '') return {};
|
if (fmBlock === '') return {};
|
||||||
|
|
@ -245,7 +260,7 @@ function validateSkill(skillDir) {
|
||||||
detail: 'SKILL.md not found in skill directory.',
|
detail: 'SKILL.md not found in skill directory.',
|
||||||
fix: 'Create SKILL.md as the skill entrypoint.',
|
fix: 'Create SKILL.md as the skill entrypoint.',
|
||||||
});
|
});
|
||||||
// Cannot check SKILL-02 through SKILL-06 without SKILL.md
|
// Cannot check SKILL-02 through SKILL-07 without SKILL.md
|
||||||
return findings;
|
return findings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,6 +377,33 @@ function validateSkill(skillDir) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SKILL-07: SKILL.md must have body content after frontmatter ---
|
||||||
|
{
|
||||||
|
const trimmed = skillContent.trimStart();
|
||||||
|
let bodyStart = -1;
|
||||||
|
if (trimmed.startsWith('---')) {
|
||||||
|
let endIdx = trimmed.indexOf('\n---\n', 3);
|
||||||
|
if (endIdx !== -1) {
|
||||||
|
bodyStart = endIdx + 4;
|
||||||
|
} else if (trimmed.endsWith('\n---')) {
|
||||||
|
bodyStart = trimmed.length; // no body at all
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bodyStart = 0; // no frontmatter, entire file is body
|
||||||
|
}
|
||||||
|
const body = bodyStart >= 0 ? trimmed.slice(bodyStart).trim() : '';
|
||||||
|
if (body === '') {
|
||||||
|
findings.push({
|
||||||
|
rule: 'SKILL-07',
|
||||||
|
title: 'SKILL.md Must Have Body Content',
|
||||||
|
severity: 'HIGH',
|
||||||
|
file: 'SKILL.md',
|
||||||
|
detail: 'SKILL.md has no content after frontmatter. L2 instructions are required.',
|
||||||
|
fix: 'Add markdown body with skill instructions after the closing ---.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- WF-01 / WF-02: workflow.md must NOT have name/description ---
|
// --- WF-01 / WF-02: workflow.md must NOT have name/description ---
|
||||||
if (fs.existsSync(workflowMdPath)) {
|
if (fs.existsSync(workflowMdPath)) {
|
||||||
const wfContent = safeReadFile(workflowMdPath, findings, 'workflow.md');
|
const wfContent = safeReadFile(workflowMdPath, findings, 'workflow.md');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue