#!/usr/bin/env node /** * SKILL-REGISTRY 自动校验脚本 * 对比 SKILL-REGISTRY.md 声明 vs 磁盘实际状态 * 用法: node scripts/validate-registry.js */ const fs = require('fs'); const path = require('path'); const ROOT = path.join(__dirname, '..'); const issues = []; let passed = 0; function check(name, condition, detail) { if (condition) { passed++; } else { issues.push({ severity: detail.startsWith('[CRITICAL]') ? 'CRITICAL' : 'WARNING', name, detail }); } } // 1. 钩子磁盘文件数 const hooksDir = path.join(ROOT, 'hooks'); const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js')); const registry = fs.readFileSync(path.join(ROOT, 'SKILL-REGISTRY.md'), 'utf8'); const hookCountMatch = registry.match(/磁盘文件\s*(\d+)\s*个/); if (hookCountMatch) { const declared = parseInt(hookCountMatch[1]); check('hooks-disk-count', declared === hookFiles.length, `[WARNING] REGISTRY 声明 ${declared} 个钩子文件,实际 ${hookFiles.length} 个`); } else { check('hooks-disk-count', false, '[WARNING] REGISTRY 中未找到钩子磁盘文件数声明'); } // 2. 注册钩子文件存在性 const settings = JSON.parse(fs.readFileSync(path.join(ROOT, 'settings.json'), 'utf8')); const allHookPaths = []; for (const stage of Object.values(settings.hooks || {})) { for (const entry of (Array.isArray(stage) ? stage : [])) { for (const hook of (entry.hooks || [])) { const m = (hook.command || '').match(/hooks\/([^\s"]+\.js)/); if (m) allHookPaths.push(m[1]); } } } for (const hp of allHookPaths) { const exists = fs.existsSync(path.join(hooksDir, hp)); check(`hook-exists:${hp}`, exists, `[CRITICAL] 注册钩子文件不存在: hooks/${hp}`); } // 3. 技能目录数 const skillsDir = path.join(ROOT, 'skills'); const skillDirs = fs.readdirSync(skillsDir).filter(f => { return !f.startsWith('_') && !f.startsWith('.') && fs.statSync(path.join(skillsDir, f)).isDirectory(); }); const stats = JSON.parse(fs.readFileSync(path.join(ROOT, 'stats-compiled.json'), 'utf8')); check('skills-count', skillDirs.length === stats.summary.skills, `[WARNING] 技能目录 ${skillDirs.length} vs stats 声明 ${stats.summary.skills}`); // 4. Agent 文件数 const agentsDir = path.join(ROOT, 'agents'); const agentFiles = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md')); check('agents-count', agentFiles.length === stats.summary.agents, `[WARNING] Agent 文件 ${agentFiles.length} vs stats 声明 ${stats.summary.agents}`); // 5. 版本一致性 const claudeMd = fs.readFileSync(path.join(ROOT, 'CLAUDE.md'), 'utf8'); const claudeVersion = (claudeMd.match(/Smart Assistant[^v]*(v[\d.]+)/) || [])[1] || 'unknown'; const registryVersion = (registry.match(/v[\d.]+/) || [])[0] || 'unknown'; const statsVersion = stats.version || 'unknown'; check('version-consistency', claudeVersion === registryVersion && registryVersion === statsVersion, `[WARNING] 版本不一致: CLAUDE.md=${claudeVersion} REGISTRY=${registryVersion} stats=${statsVersion}`); // 6. 每个技能都有 SKILL.md for (const sd of skillDirs) { const skillMd = path.join(skillsDir, sd, 'SKILL.md'); check(`skill-md:${sd}`, fs.existsSync(skillMd), `[WARNING] 技能 ${sd} 缺少 SKILL.md`); } // 输出 console.log(`\n=== SKILL-REGISTRY 校验报告 ===`); console.log(`通过: ${passed} 问题: ${issues.length}\n`); if (issues.length === 0) { console.log('✓ 全部通过,无偏差'); process.exit(0); } else { for (const i of issues) { console.log(`[${i.severity}] ${i.name}: ${i.detail}`); } process.exit(issues.some(i => i.severity === 'CRITICAL') ? 2 : 1); }