Files

346 lines
14 KiB
JavaScript
Executable File
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.

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const { check: checkRuntimeConformance } = require('./check_trace_runtime_conformance');
const ALLOWED_STATUSES = new Set([
'planned-first-pass',
'implemented-first-pass',
'needs-baseline-sampling',
'deferred-not-mounted',
'native-capability-gap',
'live-discovered',
'missed-from-trace',
]);
const PRIORITIES_REQUIRING_DECISION = new Set(['P0', 'P1']);
function parseArgs(argv) {
const args = {
caseDir: '',
inventory: '',
matrix: '',
contract: '',
nodeAudit: '',
requireRuntimeClosure: false,
requireStageAudit: false,
json: false,
markdown: false,
help: false,
};
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--case-dir' || a === '--dir' || a === '-d') args.caseDir = argv[++i] || '';
else if (a === '--inventory') args.inventory = argv[++i] || '';
else if (a === '--matrix') args.matrix = argv[++i] || '';
else if (a === '--contract') args.contract = argv[++i] || '';
else if (a === '--node-audit') args.nodeAudit = argv[++i] || '';
else if (a === '--require-runtime-closure') args.requireRuntimeClosure = true;
else if (a === '--require-stage-audit') args.requireStageAudit = true;
else if (a === '--json') args.json = true;
else if (a === '--markdown') args.markdown = true;
else if (a === '--help' || a === '-h') args.help = true;
else throw new Error(`未知参数:${a}`);
}
if (!args.json && !args.markdown) args.markdown = true;
return args;
}
function usage() {
return `用法:
node scripts/check_trace_api_coverage.js --case-dir case --markdown
node scripts/check_trace_api_coverage.js --case-dir case --require-stage-audit --require-runtime-closure --json
node scripts/check_trace_api_coverage.js --inventory case/notes/trace-api-inventory.json --matrix case/notes/env-coverage-matrix.md --markdown
说明:检查 Trace API inventory、env coverage matrix、实现文件和 Trace-runtime 行为闭环,以及后续阶段报告中计划外新增 WebAPI 是否说明原因。`;
}
function exists(p) {
try { return fs.existsSync(p); } catch { return false; }
}
function readText(p) {
return fs.readFileSync(p, 'utf8').replace(/^\uFEFF/, '');
}
function readJson(p) {
return JSON.parse(readText(p));
}
function rel(root, file) {
return (path.relative(root, file) || '.').replace(/\\/g, '/');
}
function normalizePriority(value) {
const text = String(value || '').trim().toUpperCase();
if (/^P?[0-2]$/.test(text)) return text.startsWith('P') ? text : `P${text}`;
return text || 'UNKNOWN';
}
function normalizeStatus(value) {
return String(value || '').trim();
}
function normalizeInventory(raw) {
if (Array.isArray(raw)) return raw;
if (Array.isArray(raw.apis)) return raw.apis;
if (raw.inventory && Array.isArray(raw.inventory.apis)) return raw.inventory.apis;
if (raw.apiInventory && Array.isArray(raw.apiInventory)) return raw.apiInventory;
return [];
}
function implementationPath(caseDir, file) {
if (!file) return '';
return path.isAbsolute(file) ? file : path.resolve(caseDir, file);
}
function listMarkdown(dir) {
if (!exists(dir)) return [];
const out = [];
for (const name of fs.readdirSync(dir)) {
const file = path.join(dir, name);
try {
if (fs.statSync(file).isFile() && name.toLowerCase().endsWith('.md')) out.push(file);
} catch {}
}
return out.sort();
}
function hasTraceEvidence(caseDir) {
const candidates = [
path.join(caseDir, 'ruyi-trace'),
path.join(caseDir, 'notes', 'ruyitrace-summary.md'),
path.join(caseDir, 'tmp', 'env-trace.jsonl'),
path.join(caseDir, 'tmp', 'missing-env.json'),
];
return candidates.some(exists);
}
function auditMatrixText(text) {
const required = ['Trace', 'baselineId', 'P0', 'P1', '首轮', '采样', '暂不挂载', '计划外'];
return required.filter(key => !text.includes(key));
}
function auditStageReports(caseDir) {
const stageDir = path.join(caseDir, '阶段报告');
const files = listMarkdown(stageDir);
const findings = [];
const reasonPattern = /trace-not-covered|dynamic-resource-new-branch|baseline-mismatch|trace-truncated|native-gap|missed-from-trace|Trace 未覆盖|动态资源新分支|baseline|截断|能力缺口|矩阵遗漏|流程缺陷/;
for (const file of files) {
const text = readText(file);
const mentionsOldHeading = text.includes('本阶段新增 / 修改的 WebAPI');
const hasPlanned = text.includes('Trace 计划内') || text.includes('计划内首轮');
const hasUnplanned = text.includes('计划外新增');
if (mentionsOldHeading && !hasPlanned && !hasUnplanned) {
findings.push({
file: rel(caseDir, file),
problem: '阶段报告仍使用笼统的“本阶段新增 / 修改的 WebAPI”,未拆分 Trace 计划内与计划外新增。',
});
}
if (hasUnplanned) {
const section = text.slice(text.indexOf('计划外新增'));
if (!reasonPattern.test(section)) {
findings.push({
file: rel(caseDir, file),
problem: '计划外新增 WebAPI 缺少原因枚举或证据说明。',
});
}
}
}
return findings;
}
function check(args) {
const caseDir = args.caseDir ? path.resolve(args.caseDir) : process.cwd();
const notesDir = path.join(caseDir, 'notes');
const inventoryPath = path.resolve(args.inventory || path.join(notesDir, 'trace-api-inventory.json'));
const matrixPath = path.resolve(args.matrix || path.join(notesDir, 'env-coverage-matrix.md'));
const contractPath = path.resolve(args.contract || path.join(notesDir, 'trace-runtime-contract.json'));
const nodeAuditPath = path.resolve(args.nodeAudit || path.join(caseDir, 'tmp', 'node-trace-runtime-audit.json'));
const problems = [];
const warnings = [];
const traceDetected = hasTraceEvidence(caseDir);
if (traceDetected && !exists(inventoryPath)) problems.push(`存在 Trace 证据,但缺少 ${rel(caseDir, inventoryPath)}`);
if (traceDetected && !exists(matrixPath)) problems.push(`存在 Trace 证据,但缺少 ${rel(caseDir, matrixPath)}`);
let apis = [];
let inventoryMeta = {};
if (exists(inventoryPath)) {
try {
const raw = readJson(inventoryPath);
inventoryMeta = {
schemaVersion: raw.schemaVersion || '',
source: raw.source || '',
baselineId: raw.baselineId || '',
};
apis = normalizeInventory(raw);
if (!apis.length) problems.push('trace-api-inventory.json 没有可识别的 apis 数组');
} catch (err) {
problems.push(`trace-api-inventory.json 无法解析为 JSON:${err.message}`);
}
}
const apiResults = [];
for (const item of apis) {
const api = item.api || item.path || item.name || '(unknown)';
const priority = normalizePriority(item.priority || item.level);
const status = normalizeStatus(item.implementationStatus || item.status || item.coverageStatus);
const reason = String(item.reason || item.decisionReason || '').trim();
const implementationFile = String(item.implementationFile || item.file || '').trim();
const samplingRequired = Boolean(item.samplingRequired || status === 'needs-baseline-sampling');
const resolvedImplementationFile = implementationPath(caseDir, implementationFile);
const result = { api, priority, status, reason, implementationFile, resolvedImplementationFile, samplingRequired, clean: true, problems: [] };
if (!status || status === 'unplanned') {
result.problems.push('缺少实现状态或状态为 unplanned');
} else if (!ALLOWED_STATUSES.has(status)) {
result.problems.push(`未知状态:${status}`);
}
if (PRIORITIES_REQUIRING_DECISION.has(priority) && status === 'planned-first-pass' && !implementationFile) {
result.problems.push('P0/P1 首轮实现项缺少 implementationFile');
}
if (status === 'implemented-first-pass' && (!implementationFile || !exists(resolvedImplementationFile))) {
result.problems.push(`implemented-first-pass 对应实现文件不存在:${implementationFile || '未记录'}`);
}
if (args.requireRuntimeClosure && PRIORITIES_REQUIRING_DECISION.has(priority) && status === 'planned-first-pass') {
result.problems.push('进入 runtime closure 阶段后 P0/P1 不得仍为 planned-first-pass');
}
if (['needs-baseline-sampling', 'deferred-not-mounted', 'native-capability-gap', 'missed-from-trace', 'live-discovered'].includes(status) && !reason) {
result.problems.push(`${status} 必须写明 reason`);
}
if (status === 'missed-from-trace') {
result.problems.push('Trace 已命中但矩阵遗漏,必须作为流程缺陷修复后再继续');
}
if (samplingRequired && status !== 'needs-baseline-sampling' && !reason) {
result.problems.push('samplingRequired=true 时必须说明采样策略或阻塞原因');
}
if (result.problems.length) {
result.clean = false;
for (const p of result.problems) problems.push(`${api}:${p}`);
}
apiResults.push(result);
}
let matrixMissing = [];
if (exists(matrixPath)) {
const text = readText(matrixPath);
matrixMissing = auditMatrixText(text);
if (matrixMissing.length) problems.push(`env-coverage-matrix.md 缺少关键内容:${matrixMissing.join('、')}`);
}
const stageFindings = args.requireStageAudit ? auditStageReports(caseDir) : [];
for (const finding of stageFindings) problems.push(`${finding.file}:${finding.problem}`);
let runtimeConformance = null;
if (args.requireRuntimeClosure) {
runtimeConformance = checkRuntimeConformance({
caseDir,
contract: contractPath,
nodeAudit: nodeAuditPath,
strictP2: false,
});
for (const problem of runtimeConformance.problems) problems.push(`Trace-runtime:${problem}`);
const inventoryApis = new Set(apiResults.map(item => item.api));
if (exists(contractPath)) {
try {
const contractRaw = readJson(contractPath);
const contractItems = normalizeInventory({ apis: contractRaw.contracts || [] });
const missingFromInventory = [...new Set(contractItems.map(item => item.api || item.path || item.name).filter(Boolean))]
.filter(api => !inventoryApis.has(api));
if (missingFromInventory.length) {
problems.push(`trace-api-inventory.json 缺少 runtime contract 中的 API:${missingFromInventory.slice(0, 50).join('、')}`);
}
} catch (err) {
problems.push(`无法核对 runtime contract 与 inventory:${err.message}`);
}
}
}
return {
caseDir,
traceDetected,
inventoryPath,
matrixPath,
contractPath,
nodeAuditPath,
inventoryMeta,
clean: problems.length === 0,
problems,
warnings,
apiCount: apiResults.length,
apiResults,
matrixMissing,
stageFindings,
runtimeConformance,
};
}
function renderMarkdown(result) {
const lines = [
'# Trace API 覆盖矩阵检查结果',
'',
`case 目录:${result.caseDir}`,
`检测到 Trace 证据:${result.traceDetected ? '是' : '否'}`,
`inventory:${result.inventoryPath}`,
`matrix:${result.matrixPath}`,
`runtime contract:${result.contractPath}`,
`Node runtime audit:${result.nodeAuditPath}`,
`是否通过:${result.clean ? '是' : '否'}`,
'',
'## Inventory 摘要',
`- schemaVersion:${result.inventoryMeta.schemaVersion || '未记录'}`,
`- source:${result.inventoryMeta.source || '未记录'}`,
`- baselineId:${result.inventoryMeta.baselineId || '未记录'}`,
`- API 数量:${result.apiCount}`,
];
if (result.apiResults.length) {
lines.push('', '## API 状态');
for (const item of result.apiResults.slice(0, 80)) {
lines.push(`- ${item.clean ? '通过' : '失败'} ${item.priority} ${item.api}:${item.status || '未记录'}${item.implementationFile ? `;文件 ${item.implementationFile}` : ''}`);
for (const problem of item.problems) lines.push(` - ${problem}`);
}
if (result.apiResults.length > 80) lines.push(`- 其余 ${result.apiResults.length - 80} 条略。`);
}
if (result.matrixMissing.length) {
lines.push('', '## Matrix 缺失项');
for (const item of result.matrixMissing) lines.push(`- ${item}`);
}
if (result.stageFindings.length) {
lines.push('', '## 阶段报告问题');
for (const item of result.stageFindings) lines.push(`- ${item.file}:${item.problem}`);
}
if (result.runtimeConformance) {
lines.push('', '## Trace-runtime 闭环');
lines.push(`- 是否通过:${result.runtimeConformance.clean ? '是' : '否'}`);
lines.push(`- 契约项:${result.runtimeConformance.summary.contractCount ?? 0}`);
lines.push(`- 阻断差异:${result.runtimeConformance.summary.blockingMismatches ?? 0}`);
}
if (result.problems.length) {
lines.push('', '## 问题');
for (const problem of result.problems) lines.push(`- ${problem}`);
}
if (!result.problems.length) lines.push('', '## 结论', '- Trace API 覆盖矩阵检查通过。');
return `${lines.join('\n')}\n`;
}
try {
const args = parseArgs(process.argv);
if (args.help) {
console.log(usage());
process.exit(0);
}
const result = check(args);
if (args.json) console.log(JSON.stringify(result, null, 2));
if (args.markdown) process.stdout.write(renderMarkdown(result));
process.exit(result.clean ? 0 : 1);
} catch (err) {
console.error(err.message || String(err));
console.error(usage());
process.exit(1);
}