Merge branch 'codex/validator-alignment'
This commit is contained in:
@@ -21,6 +21,7 @@ The skill MUST have a section explicitly stating when to trigger it.
|
|||||||
|
|
||||||
- **Good**: "Use when the user asks to debug a React component."
|
- **Good**: "Use when the user asks to debug a React component."
|
||||||
- **Bad**: "This skill helps you with code."
|
- **Bad**: "This skill helps you with code."
|
||||||
|
Accepted headings: `## When to Use`, `## Use this skill when`, `## When to Use This Skill`.
|
||||||
|
|
||||||
### 3. Safety & Risk Classification
|
### 3. Safety & Risk Classification
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const SKILLS_DIR = path.join(ROOT, 'skills');
|
|||||||
const ALLOWED_FIELDS = new Set([
|
const ALLOWED_FIELDS = new Set([
|
||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
|
'risk',
|
||||||
|
'source',
|
||||||
'license',
|
'license',
|
||||||
'compatibility',
|
'compatibility',
|
||||||
'metadata',
|
'metadata',
|
||||||
|
|||||||
18
scripts/tests/test_validate_skills_headings.py
Normal file
18
scripts/tests/test_validate_skills_headings.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
from validate_skills import has_when_to_use_section
|
||||||
|
|
||||||
|
SAMPLES = [
|
||||||
|
("## When to Use", True),
|
||||||
|
("## Use this skill when", True),
|
||||||
|
("## When to Use This Skill", True),
|
||||||
|
("## Overview", False),
|
||||||
|
]
|
||||||
|
|
||||||
|
for heading, expected in SAMPLES:
|
||||||
|
content = f"\n{heading}\n- item\n"
|
||||||
|
assert has_when_to_use_section(content) is expected, heading
|
||||||
|
|
||||||
|
print("ok")
|
||||||
16
scripts/tests/validate_skills_headings.test.js
Normal file
16
scripts/tests/validate_skills_headings.test.js
Normal 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');
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ import re
|
|||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
WHEN_TO_USE_PATTERNS = [
|
||||||
|
re.compile(r"^##\s+When\s+to\s+Use", re.MULTILINE | re.IGNORECASE),
|
||||||
|
re.compile(r"^##\s+Use\s+this\s+skill\s+when", re.MULTILINE | re.IGNORECASE),
|
||||||
|
re.compile(r"^##\s+When\s+to\s+Use\s+This\s+Skill", re.MULTILINE | re.IGNORECASE),
|
||||||
|
]
|
||||||
|
|
||||||
|
def has_when_to_use_section(content):
|
||||||
|
return any(pattern.search(content) for pattern in WHEN_TO_USE_PATTERNS)
|
||||||
|
|
||||||
def parse_frontmatter(content):
|
def parse_frontmatter(content):
|
||||||
"""
|
"""
|
||||||
Simple frontmatter parser using regex to avoid external dependencies.
|
Simple frontmatter parser using regex to avoid external dependencies.
|
||||||
@@ -30,7 +39,6 @@ def validate_skills(skills_dir, strict_mode=False):
|
|||||||
|
|
||||||
# Pre-compiled regex
|
# Pre-compiled regex
|
||||||
security_disclaimer_pattern = re.compile(r"AUTHORIZED USE ONLY", re.IGNORECASE)
|
security_disclaimer_pattern = re.compile(r"AUTHORIZED USE ONLY", re.IGNORECASE)
|
||||||
trigger_section_pattern = re.compile(r"^##\s+When to Use", re.MULTILINE | re.IGNORECASE)
|
|
||||||
|
|
||||||
valid_risk_levels = ["none", "safe", "critical", "offensive"]
|
valid_risk_levels = ["none", "safe", "critical", "offensive"]
|
||||||
|
|
||||||
@@ -80,7 +88,7 @@ def validate_skills(skills_dir, strict_mode=False):
|
|||||||
else: warnings.append(msg)
|
else: warnings.append(msg)
|
||||||
|
|
||||||
# 3. Content Checks (Triggers)
|
# 3. Content Checks (Triggers)
|
||||||
if not trigger_section_pattern.search(content):
|
if not has_when_to_use_section(content):
|
||||||
msg = f"⚠️ {rel_path}: Missing '## When to Use' section"
|
msg = f"⚠️ {rel_path}: Missing '## When to Use' section"
|
||||||
if strict_mode: errors.append(msg.replace("⚠️", "❌"))
|
if strict_mode: errors.append(msg.replace("⚠️", "❌"))
|
||||||
else: warnings.append(msg)
|
else: warnings.append(msg)
|
||||||
|
|||||||
Reference in New Issue
Block a user