Files
reverse-skills-max/vendor-new/rev-skills/lib/capability-index.mjs

91 lines
4.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lib/capability-index.mjs — 能力层闭环:索引生成与完整性检查
//
// 能力层设计(re-analyze/references/capabilities.md):技能声明 capabilities → 路由按能力匹配。
// 本模块是「声明」与「消费」之间的那一环,产出能力 → 技能索引(路由/检索的查询表),
// 并提供两条完整性检查,防止能力层停留在"只写不读"的状态:
// 1) 注册表标签悬空——注册表里有、没有任何技能声明
// 2) 索引过期——声明变更后未重新生成(`node bin/capindex.mjs`)
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { parseFrontmatterDocument } from './frontmatter.mjs';
export const INDEX_REL = 're-analyze/references/capability-index.md';
const REGISTRY_REL = 're-analyze/references/capabilities.md';
const SKILL_PREFIX = 're-';
// 机器生成文件头——含生成命令与用途,避免被当作手写文档维护
const HEADER = `# 能力索引(机器生成,勿手改)
> 生成:\`node bin/capindex.mjs\`|校验:\`npm test\`(过期即失败)
> 用途:路由与检索按能力反查技能——能力层的查询表(声明侧见 capabilities.md)
`;
// 注册表标签 → 一行说明
export function collectRegistryTags(root) {
const p = join(root, REGISTRY_REL);
if (!existsSync(p)) return new Map();
const map = new Map();
for (const line of readFileSync(p, 'utf8').split('\n')) {
const m = line.match(/^\-\s+`([a-z0-9-]+)`\s+—\s+(.*)$/);
if (m) map.set(m[1], m[2].trim());
}
return map;
}
// 各技能声明的能力:tag → [技能名](非法写法在此跳过,由 validate 逐技能报错)
export function collectDeclaredCapabilities(root) {
const byTag = new Map();
for (const dir of readdirSync(root, { withFileTypes: true })) {
if (!dir.isDirectory() || !dir.name.startsWith(SKILL_PREFIX)) continue;
const p = join(root, dir.name, 'SKILL.md');
if (!existsSync(p)) continue;
let value;
try {
value = parseFrontmatterDocument(readFileSync(p, 'utf8')).fm.capabilities;
} catch {
continue;
}
if (value === undefined) continue;
const m = String(value).match(/^\[\s*([a-z0-9-]+(?:\s*,\s*[a-z0-9-]+)*)\s*\]$/);
if (!m) continue;
for (const tag of m[1].split(',').map(s => s.trim())) {
byTag.set(tag, [...(byTag.get(tag) ?? []), dir.name]);
}
}
return byTag;
}
// 渲染索引全文(生成与校验共用同一函数,保证比较的是同一事实)
export function renderCapabilityIndex(root) {
const declared = collectDeclaredCapabilities(root);
const registry = collectRegistryTags(root);
const tags = [...declared.keys()].sort();
const missing = [...registry.keys()].filter(t => !declared.has(t)).sort();
// 覆盖率看板:标签覆盖(注册表自洽)与技能覆盖(渐进标注进度)分开计
const declaredSkills = new Set([...declared.values()].flat());
const totalSkills = readdirSync(root, { withFileTypes: true })
.filter(d => d.isDirectory() && d.name.startsWith(SKILL_PREFIX) && existsSync(join(root, d.name, 'SKILL.md'))).length;
const out = [HEADER];
out.push(`## 已声明能力(标签 ${tags.length}/${registry.size}|技能 ${declaredSkills.size}/${totalSkills})\n`);
out.push('| 能力 | 说明 | 提供技能 |');
out.push('|---|---|---|');
for (const t of tags) {
out.push(`| \`${t}\` | ${registry.get(t) ?? '(不在注册表)'} | ${[...declared.get(t)].sort().join(', ')} |`);
}
out.push(`\n## 尚未被声明(${missing.length})\n`);
out.push(missing.length
? missing.map(t => `- ${t} — ${registry.get(t)}`).join('\n')
: '(无——注册表标签全部有技能声明)');
out.push('');
return out.join('\n');
}
// 索引缺失或与当前声明不一致 → 报错(提示重新生成)
export function checkCapabilityIndex(root) {
const p = join(root, INDEX_REL);
if (!existsSync(p)) return [`capability index missing: ${INDEX_REL}(运行 node bin/capindex.mjs)`];
if (readFileSync(p, 'utf8') !== renderCapabilityIndex(root)) {
return [`capability index out of date: ${INDEX_REL}(运行 node bin/capindex.mjs)`];
}
return [];
}