iter(g249): doctor 增加命令面与门禁链自洽自检

This commit is contained in:
HIsheR
2026-10-26 22:05:39 +08:00
parent 38731955dc
commit 2c348704a0
6 changed files with 46 additions and 7 deletions
+36 -1
View File
@@ -1675,6 +1675,17 @@ function cmdLintIterations() {
}
/*** 环境自检:新 Agent 接手前一条命令确认 node / 依赖 / git 钩子 / .ua 元数据就绪,任一失败退出码 1 ***/
/*** 作用:解析本文件的命令面——dispatch 的 case 集合 vs usage 串里列的命令,两者必须相互覆盖
* 新增命令时只加 case 不改 usage(或反之)会让 Agent 查不到入口:usage 是 Agent 唯一能看到的命令清单。 ***/
function commandSurface() {
const self = fs.readFileSync(fileURLToPath(import.meta.url), 'utf8');
const cases = new Set([...self.matchAll(/^ {2}case '([^']+)':/gm)].map((m) => m[1]));
const usage = /console\.log\('usage: ua <(.+)>'\)/.exec(self); // 贪婪到行尾括号:usage 里本身含 <kw> 这类尖括号
// 先剔掉 [--json|--baseline] 这类选项组再按 | 切:否则选项里的竖线会被当成命令分隔符
const listed = usage ? usage[1].replace(/\[[^\]]*\]/g, '').split('|').map((part) => part.trim().split(/\s+/)[0]).filter(Boolean) : [];
return { cases, listed };
}
function cmdDoctor() {
const checks = []; // 逐项结果:名称 + 是否通过 + 可选失败详情
const nodeMajor = Number(process.versions.node.split('.')[0]);
@@ -1701,6 +1712,30 @@ function cmdDoctor() {
const badLines = fs.readFileSync(logFile, 'utf8').split(/\r?\n/).filter((l) => l.trim()).filter((l) => { try { JSON.parse(l); return false; } catch { return true; } });
checks.push({ name: '.ua/iterations.log 每行合法 JSON', pass: !badLines.length, detail: badLines.length ? `${badLines.length} 行非法(首行:${badLines[0].slice(0, 40)}…)` : '' });
} else checks.push({ name: '.ua/iterations.log 存在', pass: false, detail: '尚未生成,跑一轮迭代或 ua index' });
// 命令面自检:usage ↔ dispatch ↔ CLI 清单三方一致,防止新命令只落一处
const { cases, listed } = commandSurface();
const missingInUsage = [...cases].filter((c) => !listed.includes(c));
const missingInCode = listed.filter((c) => !cases.has(c));
checks.push({
name: 'usage 串 ↔ dispatch 一致',
pass: !missingInUsage.length && !missingInCode.length,
detail: [missingInUsage.length ? `usage 缺 ${missingInUsage.join('/')}` : '', missingInCode.length ? `实现缺 ${missingInCode.join('/')}` : ''].filter(Boolean).join(';'),
});
// 门禁链自检:package.json 的 check 必须跑齐所有门禁步骤(新增门禁忘了接进链等于没加)
const needSteps = ['build', 'demo', 'style', 'typecheck', 'smoke', 'index', 'verify', 'lint-iterations', 'audit:visual', 'audit:contrast'];
const chain = String(JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).scripts?.check ?? '');
const lacking = needSteps.filter((step) => !chain.includes(`npm run ${step}`));
checks.push({ name: 'check 链覆盖全部门禁步骤', pass: !lacking.length, detail: lacking.length ? `缺 ${lacking.join(' / ')}` : '' });
// 清单自检:ua.manifest.json 里写进 api.exports 的命令必须真的存在(文档承诺 ≠ 实现)
const cliManifestPath = path.join(ROOT, 'packages', 'tools', 'cli', 'ua.manifest.json');
const declared = fs.existsSync(cliManifestPath)
? (JSON.parse(fs.readFileSync(cliManifestPath, 'utf8')).api?.exports ?? [])
.filter((line) => /^ua \S/.test(line))
.map((line) => line.replace(/^ua\s+/, '').split(/[\s:]/)[0]) // 说明用全角冒号接在后面,按空白与冒号一起切
: [];
const ghost = declared.filter((name) => !cases.has(name));
checks.push({ name: 'CLI 清单声明的命令都有实现', pass: !ghost.length, detail: ghost.length ? `清单有实现无:${ghost.join('/')}` : '' });
for (const c of checks) console.log(`${c.pass ? 'pass' : 'FAIL'} ${c.name}${c.detail ? ':' + c.detail : ''}`);
const fails = checks.filter((c) => !c.pass).length;
if (fails) { console.error(`doctor: ${fails} 项未通过,先修复再跑 npm run check`); process.exit(1); }
@@ -1767,6 +1802,6 @@ switch (cmd) {
cmdMcp();
break;
default:
console.log('usage: ua <index|list|search <kw>|info <name>|docs|tokens [--json]|style|sync-counts|verify|doctor|commit-lint|lint-iterations|audit:visual [--json|--baseline]|audit:contrast [--json|--list]|changelog|mcp>');
console.log('usage: ua <index|list|search <kw>|info <name> [--json|--md]|recipes [编号] [--json]|docs|tokens [--json]|style|sync-counts|verify|doctor|commit-lint|lint-iterations|audit:visual [--json|--baseline]|audit:contrast [--json|--list]|changelog|mcp>');
process.exit(cmd ? 1 : 0);
}