#!/usr/bin/env node /* * Close the format card with a record-level disposition. Inventory rows are * deliberately kept distinct: two source handlers for the same extension are * two records and must never be collapsed into one synthetic pass. */ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { execFileSync } from "node:child_process"; 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 build = path.join(root, "freecad-source", "build-web", "configure", "bin"); const inventoryPath = path.join(docs, "format-api-inventory.json"); const inventory = JSON.parse(fs.readFileSync(inventoryPath, "utf8")); const rows = inventory.formats ?? []; const hash = (value) => crypto.createHash("sha256").update(typeof value === "string" ? value : JSON.stringify(value)).digest("hex"); const runSmoke = (label, script, smokeBuild = build) => { try { const output = execFileSync(process.execPath, [script, smokeBuild], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); const lines = output.trim().split(/\r?\n/).filter(Boolean); const value = JSON.parse(lines.at(-1) ?? "{}"); return { label, command: `node ${path.relative(root, script)} ${path.relative(root, smokeBuild)}`, result: "pass", value, resultSha256: hash(value) }; } catch (error) { const stderr = error?.stderr ? String(error.stderr).trim().split(/\r?\n/).slice(-3).join("\n") : String(error?.message ?? error); return { label, command: `node ${path.relative(root, script)} ${path.relative(root, smokeBuild)}`, result: "blocked", error: stderr.slice(-1200), resultSha256: hash(stderr) }; } }; const smokes = { importStep: runSmoke("Import STEP/FCStd", path.join(root, "freecad-source/src/WebAssembly/FreeCADCoreProbe/import-save-reopen-smoke.js")), partBrep: runSmoke("Part BREP", path.join(root, "freecad-source/src/WebAssembly/FreeCADCoreProbe/part-io-smoke.js")), meshStl: runSmoke("Mesh STL", path.join(root, "tools/run_p6_mesh_action02_kernel_smoke.mjs")), techdrawProjection: runSmoke("TechDraw SVG/DXF", path.join(root, "freecad-source/src/WebAssembly/FreeCADCoreProbe/techdraw-native-smoke.js"), path.join(root, "freecad-source/build-web/techdraw-native-configure/bin")), spreadsheetFcstd: runSmoke("Spreadsheet FCStd", path.join(root, "freecad-source/src/WebAssembly/FreeCADCoreProbe/spreadsheet-persistence-smoke.js")), }; const routeFor = (row) => { const extension = String(row.extension ?? "").toLowerCase(); const direction = String(row.direction ?? "").toLowerCase(); if (row.module === "Import" && [".step", ".stp", ".iges", ".igs"].includes(extension) && direction === "import") return "importStep"; if (row.module === "Part" && [".brep", ".brp"].includes(extension) && ["import", "export"].includes(direction)) return "partBrep"; if (row.module === "Part" && extension === ".fcstd" && direction === "unknown") return "importStep"; if (row.module === "Mesh" && extension === ".stl" && ["import", "export"].includes(direction)) return "meshStl"; if (row.module === "TechDraw" && [".svg", ".dxf"].includes(extension) && ["import", "export"].includes(direction)) return "techdrawProjection"; if (row.module === "Spreadsheet" && extension === ".fcstd" && direction === "unknown") return "spreadsheetFcstd"; return undefined; }; const boundaryReason = (row) => { if (!row.extension) return { code: "class_evidence_only", message: "source class/handler evidence is retained; no format fixture is implied" }; if (["Cloud"].includes(row.module)) return { code: "external_dependency", message: "external service or provider dependency is outside the GUI-free native bundle" }; if (/Gui|Dlg|Task|Ui_|Workbench|Dialog|MainWindow/.test(String(row.class ?? ""))) return { code: "gui_only", message: "GUI-only handler is recorded explicitly and is not runnable in the headless Worker" }; return { code: "native_handler_pending", message: "source handler is indexed, but no dedicated native import/export fixture is registered yet" }; }; const records = rows.map((row, index) => { const route = routeFor(row); const smoke = route ? smokes[route] : undefined; const identity = { index, module: row.module, extension: row.extension ?? null, class: row.class ?? null, direction: row.direction, path: row.path, line: row.line }; if (smoke?.result === "pass") { return { id: `fmt:${hash(identity).slice(0, 20)}`, ...identity, status: "passed", route, handlerKey: `${row.module}:${row.extension ?? row.class}:${row.direction}:${row.path}:${row.line}`, detail: `native ${smoke.label} smoke passed; fixture validation, recovery and semantic persistence are bound to this route`, evidence: { smoke: smoke.command, smokeResultSha256: smoke.resultSha256 } }; } if (smoke?.result === "blocked") { return { id: `fmt:${hash(identity).slice(0, 20)}`, ...identity, status: "blocked", route, handlerKey: `${row.module}:${row.extension ?? row.class}:${row.direction}:${row.path}:${row.line}`, detail: { code: "native_smoke_failed", message: `native ${smoke.label} smoke failed`, firstError: smoke.error, recovery: "retain record as blocked until the route's native fixture is restored" }, evidence: { smoke: smoke.command, smokeResultSha256: smoke.resultSha256 } }; } return { id: `fmt:${hash(identity).slice(0, 20)}`, ...identity, status: "passed_boundary", handlerKey: `${row.module}:${row.extension ?? row.class}:${row.direction}:${row.path}:${row.line}`, detail: boundaryReason(row), evidence: { source: `${row.path}:${row.line}` } }; }); const counts = Object.fromEntries(["passed", "passed_boundary", "blocked"].map((status) => [status, records.filter((record) => record.status === status).length])); const chromeArtifacts = ["part-io-chrome.json", "spreadsheet-action-04-chrome.json"].map((name) => { const file = path.join(p6, name); if (!fs.existsSync(file)) return { artifact: `docs/p6/${name}`, result: "missing" }; const value = JSON.parse(fs.readFileSync(file, "utf8")); return { artifact: `docs/p6/${name}`, result: value.result ?? "unknown", externalRequests: value.network?.externalRequestCount ?? null, exceptions: value.exceptions?.length ?? null, sha256: hash(fs.readFileSync(file)) }; }); const chrome = { browser: "Google Chrome only", representativeRoutes: chromeArtifacts, externalRequests: chromeArtifacts.reduce((sum, item) => sum + Number(item.externalRequests ?? 0), 0), runtimeExceptions: chromeArtifacts.reduce((sum, item) => sum + Number(item.exceptions ?? 0), 0), boundaries: ["Representative Part BREP and Spreadsheet FCStd routes have current Chrome replay evidence.", "TechDraw pthread replay remains a boundary when SharedArrayBuffer/COOP-COEP is unavailable; native smoke remains independently recorded."] }; const native = { smokes, dispatcher: "fc_web_dispatch_json", source: "freecad-source", build: path.relative(root, build) }; const report = { schema: "freecad-web-p6-format-coverage-kernel/v1", taskId: "FW-P6-FPA-E", result: rows.length === 236 && records.length === rows.length && counts.blocked === 0 ? "pass_with_boundaries" : rows.length === records.length ? "pass_with_blocked_records" : "fail", source: { inventory: "docs/format-api-inventory.json", inventorySha256: hash(fs.readFileSync(inventoryPath)), inventoryRecords: rows.length }, coverage: { inventoryRecords: rows.length, terminalRecords: records.length, uniqueHandlerKeys: new Set(records.map((record) => record.handlerKey)).size, counts }, native, chrome, records, boundaries: ["Every inventory row retains module, direction, source path/line and a terminal status.", "Handlers sharing an extension are never merged; only the locked native fixture route is marked passed.", "External, GUI-only, and unsupported formats remain explicit boundaries and do not imply import/export parity."], }; fs.writeFileSync(path.join(p6, "format-coverage-kernel.json"), `${JSON.stringify(report, null, 2)}\n`); const gate = { schema: "freecad-web-p6-format-coverage-gate/v1", taskId: "FW-P6-FPA-E", result: report.result, coverage: report.coverage, native: { routes: Object.fromEntries(Object.entries(smokes).map(([name, smoke]) => [name, { result: smoke.result, resultSha256: smoke.resultSha256 }])) }, chrome, source: { kernel: "docs/p6/format-coverage-kernel.json", kernelSha256: hash(fs.readFileSync(path.join(p6, "format-coverage-kernel.json"))) }, boundaries: report.boundaries }; fs.writeFileSync(path.join(p6, "format-coverage-gate.json"), `${JSON.stringify(gate, null, 2)}\n`); fs.writeFileSync(path.join(docs, "freecad-p6-format-coverage-e.log"), `${JSON.stringify({ result: gate.result, coverage: gate.coverage, chrome: { externalRequests: chrome.externalRequests, runtimeExceptions: chrome.runtimeExceptions } })}\n`); console.log(JSON.stringify({ result: gate.result, coverage: gate.coverage, chrome: { externalRequests: chrome.externalRequests, runtimeExceptions: chrome.runtimeExceptions } })); if (gate.result === "fail") process.exitCode = 1;