#!/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 }));