78 lines
6.1 KiB
JavaScript
78 lines
6.1 KiB
JavaScript
#!/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);
|