56 lines
2.6 KiB
JavaScript
56 lines
2.6 KiB
JavaScript
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
||
|
||
// 技能规模预算:SKILL.md 是**每次都会加载**的部分,references 是按需加载的。
|
||
// 所以真正需要设防的是 SKILL.md 膨胀成单体文档——references 再厚也只是"书架上多几本"。
|
||
const SKILLS = '.claude/skills';
|
||
const SKILL_MD_BUDGET = 240; // 现行最大 210(re-iot-proto);留一成余量,超出即提示拆分
|
||
const BRANCH_BUDGET = 600; // 单个 references 文件的上限(现行最大 506:windows-kernel.md)
|
||
|
||
function lines(p) {
|
||
return readFileSync(p, 'utf8').split('\n').length;
|
||
}
|
||
|
||
test(`SKILL.md 行数不超过 ${SKILL_MD_BUDGET}(超出应把细节下沉到 references)`, () => {
|
||
const over = [];
|
||
for (const d of readdirSync(SKILLS)) {
|
||
const p = `${SKILLS}/${d}/SKILL.md`;
|
||
if (!d.startsWith('re-') || !existsSync(p)) continue;
|
||
const n = lines(p);
|
||
if (n > SKILL_MD_BUDGET) over.push(`${d}(${n} 行)`);
|
||
}
|
||
assert.deepEqual(over, [], `SKILL.md 超预算——把机制细节移到 references/ 分支:\n${over.join('\n')}`);
|
||
});
|
||
|
||
test(`单个 references 分支不超过 ${BRANCH_BUDGET} 行`, () => {
|
||
const over = [];
|
||
for (const d of readdirSync(SKILLS)) {
|
||
const dir = `${SKILLS}/${d}/references`;
|
||
if (!d.startsWith('re-') || !existsSync(dir)) continue;
|
||
for (const f of readdirSync(dir)) {
|
||
if (!f.endsWith('.md')) continue;
|
||
const n = lines(`${dir}/${f}`);
|
||
if (n > BRANCH_BUDGET) over.push(`${d}/references/${f}(${n} 行)`);
|
||
}
|
||
}
|
||
assert.deepEqual(over, [], `分支超长——按主题再切一层:\n${over.join('\n')}`);
|
||
});
|
||
|
||
// 反向保护:预算是"拆分信号"而不是"越短越好"——SKILL.md 必须仍然自带工具准备与操作步骤,
|
||
// 否则就是被削成了空壳(把内容全推给 references 同样会让 agent 无从下手)。
|
||
test('SKILL.md 仍自带必备章节(防止为了满足预算而削壳)', () => {
|
||
const missing = [];
|
||
for (const d of readdirSync(SKILLS)) {
|
||
const p = `${SKILLS}/${d}/SKILL.md`;
|
||
if (!d.startsWith('re-') || !existsSync(p)) continue;
|
||
const text = readFileSync(p, 'utf8');
|
||
const type = /^type:\s*(\w+)/m.exec(text)?.[1] ?? 'atomic';
|
||
if (type !== 'atomic') continue; // 入口/网关另有一套章节规范
|
||
for (const sec of ['## 何时使用', '## 操作步骤']) {
|
||
if (!text.includes(sec)) missing.push(`${d}: ${sec}`);
|
||
}
|
||
}
|
||
assert.deepEqual(missing, [], `原子技能缺少必备章节:\n${missing.join('\n')}`);
|
||
});
|