#!/usr/bin/env node import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { createRequire } from "node:module"; import { pathToFileURL } from "node:url"; const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); const build = path.resolve(process.argv[2] || path.join(root, "freecad-source/build-web/configure/bin")); const manifestPath = path.join(root, "docs/p6/python-api-manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); const records = manifest.records.filter((row) => row.module === "Material" && row.tier === "tier-1-readonly").map(({ id, name, family }) => ({ apiId: id, name, family })); if (records.length !== 37) throw new Error(`Material tier-1 manifest expected 37 records, got ${records.length}`); globalThis.__dirname = `${build}${path.sep}`; globalThis.require = createRequire(import.meta.url); const workerPath = path.join(build, "FreeCADWebMaterialKernelProbe.worker.cjs"); const generatedWorkerPath = path.join(build, "FreeCADWebMaterialKernelProbe.worker.js"); if (!fs.existsSync(workerPath) && fs.existsSync(generatedWorkerPath)) fs.copyFileSync(generatedWorkerPath, workerPath); const loader = await import(pathToFileURL(path.join(build, "FreeCADWebMaterialKernelProbe.js"))); const kernel = await loader.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebMaterialKernelProbe.wasm")), locateFile: (file) => file.endsWith(".worker.js") ? workerPath : path.join(build, file) }); const invoke = (value) => JSON.parse(String(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify(value)]))); const request = { cmd: "material.pythonFamilyBatch", args: { tier: "tier-1-readonly", records } }; const success = invoke(request); const repeated = invoke(request); const invalid = invoke({ cmd: "material.pythonFamilyBatch", args: { tier: "tier-1-readonly", records: [{ ...records[0], apiId: "py:BIM:class:Array2D" }] } }); const malformed = invoke({ cmd: "material.pythonFamilyBatch", args: { tier: "tier-unsupported", records } }); if (success.ok !== true || success.module !== "Material" || success.count !== 37 || success.records?.length !== 37 || success.resourcesReleased !== true) throw new Error("Material family dispatch did not return a complete result"); if (success.records.some((record) => !["exposed", "restricted", "nonportable", "not_implemented"].includes(record.status) || record.native !== true || record.resourcesReleased !== true)) throw new Error("Material family dispatch returned a non-terminal record"); if (JSON.stringify(repeated) !== JSON.stringify(success)) throw new Error("Material family replay changed the native result"); if (invalid.ok !== true || invalid.records?.[0]?.status !== "not_implemented" || invalid.records?.[0]?.code !== "unmatched_source_record") throw new Error("module-scoped mismatch was not rejected"); if (malformed.ok !== false || malformed.code !== "invalid_request") throw new Error("invalid tier was not rejected"); const hash = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); const resultById = new Map(success.records.map((record) => [record.apiId, record])); for (const row of manifest.records) { if (row.module !== "Material" || row.tier !== "tier-1-readonly") continue; const native = resultById.get(row.id); if (!native) throw new Error(`native result missing ${row.id}`); row.disposition = native.status; row.dispositionEvidence = ["docs/p6/material-tier1-readonly-family.json"]; row.dispositionReason = native.code === "gui_dependency_unavailable" ? "requires GUI/PySide/desktop capability outside the GUI-free Web runtime" : native.code === "external_material_unavailable" ? "requires the optional external Material manager, disabled in this Web build" : native.code === "native_python_symbol" ? "passing native Python class registration and construction evidence from FreeCAD WebAssembly" : "native adapter is registered; method-level parameter/return fixture remains outside this batch"; } fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); const report = { schema: "freecad-web-p6-material-tier1-readonly-family/v1", taskId: "FW-P6-FPA-F1-MATERIAL", module: "Material", tier: "tier-1-readonly", status: "pass_with_boundaries", sourceRevision: success.records.find((record) => record.sourceRevision)?.sourceRevision || "2778e3f9c215da48bf0cfd562478d45d624aaa47", route: { worker: "web/src/material.worker.ts", command: "material.pythonFamilyBatch", native: "freecad-source/src/WebAssembly/FreeCADCoreProbe/MaterialKernelProbe.cpp:fc_web_dispatch_json" }, input: request, inputSha256: hash(request), result: success, resultSha256: hash(success), replay: { equal: true, resultSha256: hash(repeated) }, invalidRecord: { result: invalid, resultSha256: hash(invalid) }, malformedRequest: { result: malformed, resultSha256: hash(malformed) }, dispositionCounts: success.records.reduce((counts, record) => { counts[record.status] = (counts[record.status] || 0) + 1; return counts; }, {}), resources: { resourcesReleased: success.resourcesReleased, documentMutation: false }, boundaries: [ "Every selected record is returned by compiled FreeCAD WebAssembly through fc_web_dispatch_json with a terminal disposition.", "Native classes are constructed through the Materials Python extension; method-level fixtures remain explicit not_implemented until argument fixtures are added.", "GUI/PySide and optional external Material manager APIs remain restricted; no JavaScript API result or cross-module name match is used.", ], }; fs.writeFileSync(path.join(root, "docs/p6/material-tier1-readonly-family.json"), `${JSON.stringify(report, null, 2)}\n`); fs.writeFileSync(path.join(root, "docs/freecad-p6-material-tier1-readonly-family.log"), `${JSON.stringify({ status: report.status, count: records.length, dispositionCounts: report.dispositionCounts, resultSha256: report.resultSha256 })}\n`); console.log(JSON.stringify({ status: report.status, count: records.length, dispositionCounts: report.dispositionCounts, resultSha256: report.resultSha256 })); process.exit(0);