test: align js validator use-section rules

This commit is contained in:
sck_0
2026-02-02 21:37:05 +01:00
parent 2070a91ef7
commit 3d6c75d37f
2 changed files with 177 additions and 138 deletions

View File

@@ -0,0 +1,16 @@
const assert = require('assert');
const { hasUseSection } = require('../validate-skills');
const samples = [
['## When to Use', true],
['## Use this skill when', true],
['## When to Use This Skill', true],
['## Overview', false],
];
for (const [heading, expected] of samples) {
const content = `\n${heading}\n- item\n`;
assert.strictEqual(hasUseSection(content), expected, heading);
}
console.log('ok');

View File

@@ -32,12 +32,24 @@ const MAX_SKILL_LINES = 500;
const ALLOWED_FIELDS = new Set([ const ALLOWED_FIELDS = new Set([
'name', 'name',
'description', 'description',
'risk',
'source',
'license', 'license',
'compatibility', 'compatibility',
'metadata', 'metadata',
'allowed-tools', 'allowed-tools',
]); ]);
const USE_SECTION_PATTERNS = [
/^##\s+When\s+to\s+Use/im,
/^##\s+Use\s+this\s+skill\s+when/im,
/^##\s+When\s+to\s+Use\s+This\s+Skill/im,
];
function hasUseSection(content) {
return USE_SECTION_PATTERNS.some(pattern => pattern.test(content));
}
function isPlainObject(value) { function isPlainObject(value) {
return value && typeof value === 'object' && !Array.isArray(value); return value && typeof value === 'object' && !Array.isArray(value);
} }
@@ -99,14 +111,15 @@ function addStrictSectionErrors(label, missing, baselineSet) {
} }
} }
const skillIds = listSkillIds(SKILLS_DIR); function run() {
const baseline = loadBaseline(); const skillIds = listSkillIds(SKILLS_DIR);
const baselineUse = new Set(baseline.useSection || []); const baseline = loadBaseline();
const baselineDoNotUse = new Set(baseline.doNotUseSection || []); const baselineUse = new Set(baseline.useSection || []);
const baselineInstructions = new Set(baseline.instructionsSection || []); const baselineDoNotUse = new Set(baseline.doNotUseSection || []);
const baselineLongFile = new Set(baseline.longFile || []); const baselineInstructions = new Set(baseline.instructionsSection || []);
const baselineLongFile = new Set(baseline.longFile || []);
for (const skillId of skillIds) { for (const skillId of skillIds) {
const skillPath = path.join(SKILLS_DIR, skillId, 'SKILL.md'); const skillPath = path.join(SKILLS_DIR, skillId, 'SKILL.md');
if (!fs.existsSync(skillPath)) { if (!fs.existsSync(skillPath)) {
@@ -202,7 +215,7 @@ for (const skillId of skillIds) {
longFiles.push(skillId); longFiles.push(skillId);
} }
if (!content.includes('## Use this skill when')) { if (!hasUseSection(content)) {
missingUseSection.push(skillId); missingUseSection.push(skillId);
} }
@@ -213,34 +226,34 @@ for (const skillId of skillIds) {
if (!content.includes('## Instructions')) { if (!content.includes('## Instructions')) {
missingInstructionsSection.push(skillId); missingInstructionsSection.push(skillId);
} }
} }
if (missingUseSection.length) { if (missingUseSection.length) {
addWarning(`Missing "Use this skill when" section: ${missingUseSection.length} skills (examples: ${missingUseSection.slice(0, 5).join(', ')})`); addWarning(`Missing "Use this skill when" section: ${missingUseSection.length} skills (examples: ${missingUseSection.slice(0, 5).join(', ')})`);
} }
if (missingDoNotUseSection.length) { if (missingDoNotUseSection.length) {
addWarning(`Missing "Do not use" section: ${missingDoNotUseSection.length} skills (examples: ${missingDoNotUseSection.slice(0, 5).join(', ')})`); addWarning(`Missing "Do not use" section: ${missingDoNotUseSection.length} skills (examples: ${missingDoNotUseSection.slice(0, 5).join(', ')})`);
} }
if (missingInstructionsSection.length) { if (missingInstructionsSection.length) {
addWarning(`Missing "Instructions" section: ${missingInstructionsSection.length} skills (examples: ${missingInstructionsSection.slice(0, 5).join(', ')})`); addWarning(`Missing "Instructions" section: ${missingInstructionsSection.length} skills (examples: ${missingInstructionsSection.slice(0, 5).join(', ')})`);
} }
if (longFiles.length) { if (longFiles.length) {
addWarning(`SKILL.md over ${MAX_SKILL_LINES} lines: ${longFiles.length} skills (examples: ${longFiles.slice(0, 5).join(', ')})`); addWarning(`SKILL.md over ${MAX_SKILL_LINES} lines: ${longFiles.length} skills (examples: ${longFiles.slice(0, 5).join(', ')})`);
} }
if (unknownFieldSkills.length) { if (unknownFieldSkills.length) {
addWarning(`Unknown frontmatter fields detected: ${unknownFieldSkills.length} skills (examples: ${unknownFieldSkills.slice(0, 5).join(', ')})`); addWarning(`Unknown frontmatter fields detected: ${unknownFieldSkills.length} skills (examples: ${unknownFieldSkills.slice(0, 5).join(', ')})`);
} }
addStrictSectionErrors('Use this skill when', missingUseSection, baselineUse); addStrictSectionErrors('Use this skill when', missingUseSection, baselineUse);
addStrictSectionErrors('Do not use', missingDoNotUseSection, baselineDoNotUse); addStrictSectionErrors('Do not use', missingDoNotUseSection, baselineDoNotUse);
addStrictSectionErrors('Instructions', missingInstructionsSection, baselineInstructions); addStrictSectionErrors('Instructions', missingInstructionsSection, baselineInstructions);
addStrictSectionErrors(`SKILL.md line count <= ${MAX_SKILL_LINES}`, longFiles, baselineLongFile); addStrictSectionErrors(`SKILL.md line count <= ${MAX_SKILL_LINES}`, longFiles, baselineLongFile);
if (writeBaseline) { if (writeBaseline) {
const baselineData = { const baselineData = {
generatedAt: new Date().toISOString(), generatedAt: new Date().toISOString(),
useSection: [...missingUseSection].sort(), useSection: [...missingUseSection].sort(),
@@ -250,21 +263,31 @@ if (writeBaseline) {
}; };
fs.writeFileSync(BASELINE_PATH, JSON.stringify(baselineData, null, 2)); fs.writeFileSync(BASELINE_PATH, JSON.stringify(baselineData, null, 2));
console.log(`Baseline written to ${BASELINE_PATH}`); console.log(`Baseline written to ${BASELINE_PATH}`);
} }
if (warnings.length) { if (warnings.length) {
console.warn('Warnings:'); console.warn('Warnings:');
for (const warning of warnings) { for (const warning of warnings) {
console.warn(`- ${warning}`); console.warn(`- ${warning}`);
} }
} }
if (errors.length) { if (errors.length) {
console.error('\nErrors:'); console.error('\nErrors:');
for (const error of errors) { for (const error of errors) {
console.error(`- ${error}`); console.error(`- ${error}`);
} }
process.exit(1); process.exit(1);
}
console.log(`Validation passed for ${skillIds.length} skills.`);
} }
console.log(`Validation passed for ${skillIds.length} skills.`); if (require.main === module) {
run();
}
module.exports = {
hasUseSection,
run,
};