Files
FreeCADWasmDev/tools/run_p6_cloud_gate.mjs
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

147 lines
7.0 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";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const docsRoot = path.join(root, "docs");
const outputPath = path.join(docsRoot, "p6", "cloud-gate.json");
const markdownPath = path.join(docsRoot, "p6", "cloud-gate.md");
const logPath = path.join(docsRoot, "freecad-p6-cloud-x-gate.log");
const ledger = JSON.parse(fs.readFileSync(path.join(docsRoot, "current", "task-state.json"), "utf8"));
const revision = "b5a086fda59b0325fe0996147e6e9b344414f9bc";
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
const isHash = (value) => typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
const contracts = [
["A", "FW-P6-CLD-A", "cloud-source-boundary.json", "cloud/FW-P6-CLD-A.md"],
["B", "FW-P6-CLD-B", "cloud-runtime.json", "cloud/FW-P6-CLD-B.md"],
["C", "FW-P6-CLD-C", "cloud-kernel.json", "cloud/FW-P6-CLD-C.md"],
["D", "FW-P6-CLD-D", "cloud-recovery.json", "cloud/FW-P6-CLD-D.md"],
["E", "FW-P6-CLD-E", "cloud-facade.json", "cloud/FW-P6-CLD-E.md"],
["F", "FW-P6-CLD-F", "cloud-chrome.json", "cloud/FW-P6-CLD-F.md"],
["G", "FW-P6-CLD-G", "cloud-persistence.json", "cloud/FW-P6-CLD-G.md"],
];
function sourceRevision(evidence) {
return evidence.source_revision ?? evidence.sourceRevision;
}
function inputHash(evidence) {
return evidence.input_sha256
?? evidence.fixture?.input_sha256
?? evidence.invalid_input?.input_sha256
?? (evidence.inputs ? sha256(JSON.stringify(evidence.inputs)) : null);
}
function resultHash(evidence, evidenceBytes) {
return evidence.result_sha256
?? evidence.result?.result_sha256
?? evidence.evidence?.resultHash
?? sha256(evidenceBytes);
}
const checks = contracts.map(([step, taskId, evidenceName, cardName]) => {
const evidencePath = path.join(docsRoot, "p6", evidenceName);
const cardPath = path.join(docsRoot, "tasks", "p6", cardName);
const evidenceBytes = fs.readFileSync(evidencePath);
const evidence = JSON.parse(evidenceBytes);
const card = fs.readFileSync(cardPath, "utf8");
const conclusion = evidence.status ?? evidence.result;
const inputSha256 = inputHash(evidence);
const resultSha256 = resultHash(evidence, evidenceBytes);
const commandsPresent = (Array.isArray(evidence.commands) && evidence.commands.length > 0) || card.includes("复现");
const artifactsPresent = (evidence.artifacts && Object.keys(evidence.artifacts).length > 0) || card.includes("产物");
const pass = ledger.tasks[taskId] === "completed"
&& evidence.task_id === taskId
&& sourceRevision(evidence) === revision
&& (conclusion === "pass" || conclusion === "pass_probe_only")
&& isHash(inputSha256)
&& isHash(resultSha256)
&& commandsPresent
&& artifactsPresent;
return {
step,
task_id: taskId,
pass,
conclusion,
source_revision: sourceRevision(evidence),
input_sha256: inputSha256,
result_sha256: resultSha256,
evidence: `docs/p6/${evidenceName}`,
evidence_sha256: sha256(evidenceBytes),
task_card: `docs/tasks/p6/${cardName}`,
commands_present: commandsPresent,
artifacts_present: artifactsPresent,
};
});
const chrome = JSON.parse(fs.readFileSync(path.join(docsRoot, "p6", "cloud-chrome.json"), "utf8"));
const chromeChecks = {
result: chrome.result,
version: chrome.chrome?.version,
mode: chrome.mode,
viewport: chrome.viewport,
external_requests: chrome.network?.externalRequestCount,
console_errors: chrome.console?.filter((entry) => entry.type === "error" || entry.type === "browser-error").length,
exceptions: chrome.exceptions?.length,
non_chrome_browsers_attempted: chrome.policy?.nonChromeBrowsersAttempted,
screenshot_sha256: chrome.evidence?.screenshotSha256,
};
const chromePass = chromeChecks.result === "pass"
&& chromeChecks.version?.startsWith("Google Chrome ")
&& chromeChecks.mode === "headless-new"
&& chromeChecks.viewport?.width === 1440
&& chromeChecks.viewport?.height === 900
&& chromeChecks.external_requests === 0
&& chromeChecks.console_errors === 0
&& chromeChecks.exceptions === 0
&& chromeChecks.non_chrome_browsers_attempted === false
&& isHash(chromeChecks.screenshot_sha256);
const remainingActions = ["FW-P6-CLD-ACT-02", "FW-P6-CLD-ACT-03", "FW-P6-CLD-ACT-04"].map((taskId) => ({
task_id: taskId,
status: ledger.tasks[taskId],
}));
const pass = checks.every((check) => check.pass)
&& chromePass
&& remainingActions.every((task) => task.status === "planned")
&& ledger.tasks["FW-P6-CLD"] === "in_progress";
assert.equal(pass, true, JSON.stringify({ checks, chromeChecks, remainingActions }));
const gateInput = { evidence: checks.map((check) => check.evidence_sha256), chrome: chromeChecks.screenshot_sha256 };
const gateResult = { checks: checks.map((check) => check.pass), chromePass, remainingActions };
const report = {
schema: "freecad-web-p6-cloud-gate/v1",
task_id: "FW-P6-CLD-X",
module: "Cloud",
gate: "first-slice",
status: "pass",
source_revision: revision,
input_sha256: sha256(JSON.stringify(gateInput)),
result_sha256: sha256(JSON.stringify(gateResult)),
contracts: checks,
chrome: { pass: chromePass, ...chromeChecks },
module_status_after_gate: "in_progress",
remaining_actions: remainingActions,
commands: ["node FreeCAD/tools/run_p6_cloud_gate.mjs", "python3 FreeCAD/tools/validate_web_context.py"],
artifacts: { markdown: "docs/p6/cloud-gate.md", log: "docs/freecad-p6-cloud-x-gate.log" },
boundaries: [
"This gate closes only the first Cloud configure/recompute slice.",
"Cloud inspect/source-inventory, Python API, and remote transfer action packets remain pending.",
"No full-module, release, legal, performance, or non-Chrome compatibility claim is made."
],
generated_at: new Date().toISOString(),
};
const rows = checks.map((check) => `| ${check.step} | \`${check.task_id}\` | ${check.conclusion} | \`${check.evidence_sha256}\` |`).join("\n");
const markdown = `# Cloud first-slice gate\n\n- Result: \`pass\`\n- Source revision: \`${revision}\`\n- Closed scope: Cloud configure/recompute first slice only\n- Module status: \`in_progress\`\n\n| Step | Task | Conclusion | Evidence SHA-256 |\n|---|---|---|---|\n${rows}\n\nChrome \`${chromeChecks.version}\` passed in \`${chromeChecks.mode}\` at \`1440x900\`; external requests, console errors, and exceptions were all zero.\n\nRemaining action packets: \`FW-P6-CLD-ACT-02\`, \`FW-P6-CLD-ACT-03\`, and \`FW-P6-CLD-ACT-04\` are still \`planned\`. This gate does not mark the complete Cloud module as finished.\n`;
fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`);
fs.writeFileSync(markdownPath, markdown);
fs.writeFileSync(logPath, `contracts=${checks.length}\nchromePass=${chromePass}\nremainingActions=${remainingActions.length}\nmoduleStatus=${ledger.tasks["FW-P6-CLD"]}\nresult=pass\n`);
console.log(JSON.stringify({ result: report.status, contracts: checks.length, chrome: chromePass, remainingActions: remainingActions.length }));