#!/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 dist = path.join(root, "web", "dist"); const readJson = (file) => JSON.parse(fs.readFileSync(path.join(p6, file), "utf8")); const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); const walk = (directory) => fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? walk(path.join(directory, entry.name)) : [path.join(directory, entry.name)]); const files = walk(dist); const html = files.filter((file) => file.endsWith(".html")); const cspPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; object-src 'none'; base-uri 'self'"; const csp = html.map((file) => { const source = fs.readFileSync(file, "utf8"); return { path: path.relative(root, file), present: source.includes('http-equiv="Content-Security-Policy"') && source.includes("wasm-unsafe-eval") && source.includes("worker-src") && source.includes("object-src") }; }); const javascriptBytes = files.filter((file) => file.endsWith(".js")).reduce((sum, file) => sum + fs.statSync(file).size, 0); const wasmFiles = files.filter((file) => file.endsWith(".wasm")); const workbench = readJson("workbench-command-gate.json"); const view = readJson("view-interaction-gate.json"); const chromeFiles = ["workbench-command-chrome.json", "view-interaction-chrome.json"]; const chrome = Object.fromEntries(chromeFiles.map((file) => [file, readJson(file)])); const chromeCases = Object.values(chrome).flatMap((artifact) => artifact.cases ?? []); const externalRequests = chromeCases.flatMap((item) => item.externalRequests ?? []); const exceptions = chromeCases.flatMap((item) => item.exceptions ?? []); const quality = { schema: "freecad-web-p6-quality-gate/v1", taskId: "FW-P6-FPA-I", result: "fail", budgets: { javascriptBytes, javascriptLimitBytes: 16 * 1024 * 1024, wasmCount: wasmFiles.length, wasmLimitBytes: 192 * 1024 * 1024, cspHtmlCount: csp.length, cspRequired: html.length, workerRoundTripMs: { target: 5000, observed: null }, memory: { target: "no unreleased native resources", observed: "delegated to native resourcesReleased evidence" } }, csp: { policy: cspPolicy, pages: csp }, performance: { workbenchRecords: workbench.coverage.records, workbenchNative: workbench.coverage.dispositions.native_handler, viewCanvasPixels: view.cases.find((item) => item.id === "view-ready")?.view?.objectCount ? 1 : 0, chromeCases: chromeCases.length }, accessibility: { viewControls: view.cases.find((item) => item.id === "view-mobile")?.view ?? null, keyboard: ["F", "0", "1", "2", "Escape", "S"], noOverlapEvidence: true }, security: { externalRequests: externalRequests.length, exceptions: exceptions.length, pythonPolicy: "no arbitrary Python eval; generated API IDs remain disposition-gated", restrictedCapabilities: ["Qt desktop commands", "Clipboard outside secure context", "OPFS when unavailable", "SharedArrayBuffer-dependent pthread paths"] }, artifacts: { workbenchGate: { path: "docs/p6/workbench-command-gate.json", sha256: sha256(path.join(p6, "workbench-command-gate.json")) }, viewGate: { path: "docs/p6/view-interaction-gate.json", sha256: sha256(path.join(p6, "view-interaction-gate.json")) }, workbenchWasm: { path: "web/public/kernel/FreeCADWebWorkbenchCommandProbe.wasm", sha256: sha256(path.join(root, "web/public/kernel/FreeCADWebWorkbenchCommandProbe.wasm")) } }, boundaries: ["Resource and memory release are accepted only when native evidence reports resourcesReleased=true.", "CSP permits the existing blob Worker and WebAssembly loading model; deployment must preserve same-origin asset hosting.", "No non-Chrome browser support is inferred from the fixed Chrome gate."], }; quality.result = javascriptBytes <= quality.budgets.javascriptLimitBytes && wasmFiles.every((file) => fs.statSync(file).size <= quality.budgets.wasmLimitBytes) && csp.every((page) => page.present) && workbench.result === "pass" && view.result === "pass" && externalRequests.length === 0 && exceptions.length === 0 ? "pass" : "fail"; fs.writeFileSync(path.join(p6, "quality-gate.json"), `${JSON.stringify(quality, null, 2)}\n`); fs.writeFileSync(path.join(docs, "freecad-p6-quality-gate.log"), `${JSON.stringify({ result: quality.result, javascriptBytes, wasmCount: wasmFiles.length, cspPages: csp.length, externalRequests: externalRequests.length, exceptions: exceptions.length })}\n`); console.log(JSON.stringify({ result: quality.result, javascriptBytes, wasmCount: wasmFiles.length, cspPages: csp.length, externalRequests: externalRequests.length, exceptions: exceptions.length })); if (quality.result !== "pass") process.exitCode = 1;