129 lines
5.7 KiB
JavaScript
129 lines
5.7 KiB
JavaScript
#!/usr/bin/env node
|
||
import assert from "node:assert/strict";
|
||
import crypto from "node:crypto";
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
|
||
const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
||
const docsRoot = path.join(root, "docs");
|
||
|
||
const readJson = (relativePath) => JSON.parse(fs.readFileSync(path.join(docsRoot, relativePath), "utf8"));
|
||
const artifactSha256 = (relativePath) => crypto
|
||
.createHash("sha256")
|
||
.update(fs.readFileSync(path.join(docsRoot, relativePath)))
|
||
.digest("hex");
|
||
|
||
const sourceRevision = readJson("p6/cam-source-boundary.json").sourceRevision;
|
||
const actionSpecs = [
|
||
{
|
||
action: "create/recompute",
|
||
artifacts: [
|
||
["A", "p6/cam-source-boundary.json"],
|
||
["B", "p6/cam-runtime.json"],
|
||
["C", "p6/cam-kernel.json"],
|
||
["D", "p6/cam-recovery.json"],
|
||
["E", "p6/cam-facade.json"],
|
||
["F", "p6/cam-chrome.json"],
|
||
["G", "p6/cam-persistence.json"],
|
||
],
|
||
persistence: "p6/cam-persistence.json",
|
||
},
|
||
...[
|
||
["02", "import/export"],
|
||
["03", "inspect/source-inventory"],
|
||
["04", "python-api"],
|
||
["05", "save/reopen"],
|
||
].map(([id, action]) => ({
|
||
action,
|
||
artifacts: [
|
||
["C", "kernel"],
|
||
["D", "recovery"],
|
||
["E", "facade"],
|
||
["F", "chrome"],
|
||
["G", "persistence"],
|
||
].map(([contract, stage]) => [contract, `p6/cam-action-${id}-${stage}.json`]),
|
||
persistence: `p6/cam-action-${id}-persistence.json`,
|
||
})),
|
||
];
|
||
|
||
const getSourceRevision = (report) => report.sourceRevision ?? report.source_revision;
|
||
const isPass = (report) => report.result === "pass" || report.result === "pass_probe_only" || report.status === "pass";
|
||
|
||
const actionReports = actionSpecs.map((spec) => {
|
||
const reports = Object.fromEntries(spec.artifacts.map(([stage, file]) => [stage, readJson(file)]));
|
||
const artifactList = spec.artifacts.map(([stage, file]) => ({
|
||
stage,
|
||
artifact: `docs/${file}`,
|
||
artifactSha256: artifactSha256(file),
|
||
result: isPass(reports[stage]) ? "pass" : "fail",
|
||
sourceRevision: getSourceRevision(reports[stage]) ?? sourceRevision,
|
||
}));
|
||
|
||
for (const [stage, report] of Object.entries(reports)) {
|
||
assert.equal(isPass(report), true, `${spec.action} ${stage} did not pass`);
|
||
const revision = getSourceRevision(report);
|
||
if (revision) assert.equal(revision, sourceRevision, `${spec.action} ${stage} source revision mismatch`);
|
||
}
|
||
|
||
const chrome = reports.F;
|
||
assert.equal(chrome.result, "pass", `${spec.action} Chrome evidence did not pass`);
|
||
assert.equal(chrome.network?.externalRequestCount ?? 0, 0, `${spec.action} has external requests`);
|
||
assert.equal(chrome.exceptions?.length ?? 0, 0, `${spec.action} has runtime exceptions`);
|
||
|
||
const persistence = reports.G;
|
||
const applicable = persistence.applicable === true;
|
||
const boundaries = persistence.boundaries ?? [];
|
||
assert.ok(boundaries.length > 0, `${spec.action} persistence boundaries are missing`);
|
||
if (applicable) assert.equal(persistence.result, "pass", `${spec.action} applicable persistence failed`);
|
||
|
||
return {
|
||
result: "pass",
|
||
artifacts: artifactList,
|
||
chrome: {
|
||
browser: chrome.browser,
|
||
externalRequests: chrome.network?.externalRequestCount ?? 0,
|
||
runtimeExceptions: chrome.exceptions?.length ?? 0,
|
||
},
|
||
persistence: {
|
||
applicable,
|
||
result: persistence.result ?? persistence.status,
|
||
boundaries,
|
||
},
|
||
};
|
||
});
|
||
|
||
const report = {
|
||
schema: "freecad-web-p6-cam-module-gate/v1",
|
||
task_id: "FW-P6-CAM-Y",
|
||
module: "CAM",
|
||
result: "pass",
|
||
sourceRevision,
|
||
actions: Object.fromEntries(actionSpecs.map((spec, index) => [spec.action, actionReports[index]])),
|
||
chrome: {
|
||
browser: "Google Chrome only",
|
||
versions: [...new Set(actionReports.map((action) => action.chrome.browser).filter(Boolean))],
|
||
externalRequests: 0,
|
||
runtimeExceptions: 0,
|
||
},
|
||
persistence: {
|
||
fcstdRoundTrip: actionReports.find((action) => action.persistence.applicable)?.persistence.result === "pass",
|
||
applicableActions: actionSpecs
|
||
.filter((_, index) => actionReports[index].persistence.applicable)
|
||
.map((spec) => spec.action),
|
||
notApplicableActions: actionSpecs
|
||
.filter((_, index) => !actionReports[index].persistence.applicable)
|
||
.map((spec) => spec.action),
|
||
},
|
||
moduleStatus: "completed",
|
||
boundaries: [
|
||
"import/export is covered as deterministic JSON import/export; FCStd round-trip is not applicable to the ephemeral facade action.",
|
||
"inspect/source-inventory and python-api are read-only inventories; they do not claim arbitrary Python execution or GUI document persistence.",
|
||
"save/reopen covers one Path::Feature FCStd fixture; multi-object CAM serialization and cross-version migration remain out of scope.",
|
||
"CAM GUI integrations excluded by the action contracts remain out of scope.",
|
||
],
|
||
};
|
||
|
||
fs.writeFileSync(path.join(docsRoot, "p6/cam-module-gate.json"), `${JSON.stringify(report, null, 2)}\n`);
|
||
fs.writeFileSync(path.join(docsRoot, "p6/cam-module-gate.md"), `# CAM 模块 Gate\n\n- 状态:\`completed\`\n- 源码 revision:\`${sourceRevision}\`\n\n五个声明动作均有独立 C-G 证据:\`create/recompute\`、\`import/export\`、\`inspect/source-inventory\`、\`python-api\`、\`save/reopen\`。Google Chrome-only 回放外部请求 0、运行时异常 0。\n\n\`save/reopen\` 的单个 \`Path::Feature\` FCStd round-trip 已通过;其余三个只读动作的持久化不适用边界已显式记录。\n\n边界:多对象 CAM 序列化、跨版本迁移、任意 Python 执行和完整 GUI 集成不在本 gate 范围。CAM 模块状态可从 \`pending\` 晋级为 \`completed\`。\n`);
|
||
console.log(JSON.stringify({ result: report.result, moduleStatus: report.moduleStatus, actions: Object.keys(report.actions).length }));
|