#!/usr/bin/env node 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 docs = path.join(root, "docs"); const p6 = path.join(docs, "p6"); const read = (file) => JSON.parse(fs.readFileSync(path.join(p6, file), "utf8")); const hash = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(p6, file))).digest("hex"); const chrome = read("document-contract-chrome.json"); const cases = Object.fromEntries(chrome.cases.map((item) => [item.id, item])); const required = ["document-ready", "document-action", "document-duplicate", "document-expected-error", "document-cancel", "document-reset", "document-repeat"]; const resultState = cases["document-action"]?.state?.response?.result; const repeatState = cases["document-repeat"]?.state?.response?.result; const contracts = Object.fromEntries(required.map((id) => [id, { status: cases[id]?.status ?? "missing", stage: cases[id]?.state?.stage ?? "missing", responseKind: cases[id]?.state?.response?.kind ?? null, error: cases[id]?.state?.response?.error?.code ?? null, progressEvents: cases[id]?.state?.progress?.length ?? 0, }])); const result = chrome.result === "pass" && required.every((id) => cases[id]?.status === "pass") && cases["document-action"]?.state?.response?.kind === "result" && cases["document-action"]?.state?.progress?.length === 5 && cases["document-duplicate"]?.state?.response?.error?.code === "duplicate_request" && cases["document-cancel"]?.state?.response?.error?.code === "cancelled" && resultState?.semanticRoundTrip === true && repeatState?.semanticRoundTrip === true && resultState?.recovery?.invalidError === "Length of box too small" && resultState?.recovery?.hasError === 0 && resultState?.opened?.objectType === "Part::Box" && resultState?.opened?.dimensions?.join(",") === "2,3,4" && resultState?.transaction?.undoResult === 1 && resultState?.transaction?.redoResult === 1 && resultState?.resourcesReleased === true && chrome.cases.flatMap((item) => item.externalRequests ?? []).length === 0 && chrome.cases.flatMap((item) => item.exceptions ?? []).length === 0; const report = { schema: "freecad-web-p6-document-contract-gate/v1", taskId: "FW-P6-FPA-C", result: result ? "pass" : "fail", contracts, semantic: { action: resultState ?? null, repeat: repeatState ?? null, compares: ["objectType", "dimensions", "volume", "recovery", "transaction"] }, chrome: { artifact: "docs/p6/document-contract-chrome.json", sha256: hash("document-contract-chrome.json"), screenshot: chrome.evidence?.screenshot ?? null, screenshotSha256: chrome.evidence?.screenshotSha256 ?? null, browser: chrome.chrome?.version ?? null, externalRequests: chrome.cases.flatMap((item) => item.externalRequests ?? []).length, exceptions: chrome.cases.flatMap((item) => item.exceptions ?? []).length }, boundaries: [ "C proves one native Part::Box document semantic round-trip and transaction/recovery contract; it does not close the 403 object inventory.", "FCStd acceptance compares semantic fields, not archive bytes.", "Cross-module links and larger object graphs remain explicit D/C follow-up work.", ], }; fs.writeFileSync(path.join(p6, "document-contract-gate.json"), `${JSON.stringify(report, null, 2)}\n`); fs.writeFileSync(path.join(docs, "freecad-p6-document-contract-gate.log"), `${JSON.stringify({ result: report.result, cases: required.length, semanticRoundTrip: resultState?.semanticRoundTrip ?? false, externalRequests: report.chrome.externalRequests, exceptions: report.chrome.exceptions })}\n`); console.log(JSON.stringify({ result: report.result, cases: required.length, semanticRoundTrip: resultState?.semanticRoundTrip ?? false, externalRequests: report.chrome.externalRequests, exceptions: report.chrome.exceptions })); if (!result) process.exitCode = 1;