91 lines
4.2 KiB
JavaScript
91 lines
4.2 KiB
JavaScript
// 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 [];
|
||
}
|