#!/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 p6 = path.join(root, "docs/p6"); const artifact = path.join(p6, "view-interaction-chrome.json"); const chrome = JSON.parse(fs.readFileSync(artifact, "utf8")); const cases = Object.fromEntries((chrome.cases ?? []).map((item) => [item.id, item])); const required = ["view-ready", "view-selection", "view-section", "view-hidden", "view-desktop", "view-mobile"]; const fileHash = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); const pngInfo = (file) => { const bytes = fs.readFileSync(file); return { bytes: bytes.length, width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20), sha256: fileHash(file) }; }; const desktop = path.join(p6, "view-interaction-desktop.png"); const mobile = path.join(p6, "view-interaction-mobile.png"); const result = chrome.result === "pass" && required.every((id) => cases[id]?.status === "pass") && cases["view-ready"]?.state?.view?.objectCount === 1 && cases["view-ready"]?.state?.canvasPixels > 0 && cases["view-selection"]?.state?.view?.selected === true && cases["view-section"]?.state?.view?.section === true && cases["view-hidden"]?.state?.view?.visible === false && cases["view-desktop"]?.state?.view?.visible === true && cases["view-desktop"]?.state?.view?.camera?.position?.length === 3 && cases["view-mobile"]?.state?.controls?.includes("section-button") && fs.existsSync(desktop) && fs.existsSync(mobile) && pngInfo(desktop).bytes > 1000 && pngInfo(mobile).bytes > 1000 && (chrome.cases ?? []).flatMap((item) => item.externalRequests ?? []).length === 0 && (chrome.cases ?? []).flatMap((item) => item.exceptions ?? []).length === 0; const report = { schema: "freecad-web-p6-view-interaction-gate/v1", taskId: "FW-P6-FPA-H", result: result ? "pass" : "fail", cases: required.map((id) => ({ id, status: cases[id]?.status ?? "missing", stage: cases[id]?.state?.stage ?? null, view: cases[id]?.state?.view ?? null })), screenshots: { desktop: pngInfo(desktop), mobile: pngInfo(mobile) }, browser: { version: chrome.chrome?.version ?? null, mode: chrome.mode ?? null, viewport: chrome.viewport ?? null }, artifacts: { chrome: { path: "docs/p6/view-interaction-chrome.json", sha256: fileHash(artifact) }, source: { path: "web/src/main.ts", sha256: fileHash(path.join(root, "web/src/main.ts")) }, renderer: { path: "web/src/render-object-registry.ts", sha256: fileHash(path.join(root, "web/src/render-object-registry.ts")) } }, boundaries: [ "View state changes only Three.js projection and persisted view-provider state; geometry remains a native FreeCAD Worker mesh payload.", "The compact browser view uses one native STEP object as the representative Document actor; full multi-object projection remains governed by object/graph family cards.", "Section is an X-mid-plane clipping projection and is intentionally reversible; it does not mutate the native document geometry.", ], }; fs.writeFileSync(path.join(p6, "view-interaction-gate.json"), `${JSON.stringify(report, null, 2)}\n`); fs.writeFileSync(path.join(root, "docs/freecad-p6-view-interaction-gate.log"), `${JSON.stringify({ result: report.result, cases: required.length, desktop: report.screenshots.desktop, mobile: report.screenshots.mobile, externalRequests: 0, exceptions: 0 })}\n`); console.log(JSON.stringify({ result: report.result, cases: required.length, desktop: report.screenshots.desktop, mobile: report.screenshots.mobile })); if (!result) process.exitCode = 1;