44 lines
4.9 KiB
JavaScript
44 lines
4.9 KiB
JavaScript
#!/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;
|