Files
FreeCADWasmDev/tools/run_p6_reverseengineering_python_family_gate.mjs
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

81 lines
6.6 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 docs = path.join(root, "docs/p6");
const manifestPath = path.join(root, "docs/p6/python-api-manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const selected = manifest.records.filter((row) => row.module === "ReverseEngineering" && row.tier === "tier-1-readonly");
if (selected.length !== 1) throw new Error(`ReverseEngineering tier-1 manifest expected 1 record, got ${selected.length}`);
const records = selected.map(({ id, name, family }) => ({ apiId: id, name, family }));
const request = { cmd: "reverseengineering.pythonFamilyBatch", args: { tier: "tier-1-readonly", records } };
globalThis.__dirname = `${build}${path.sep}`;
globalThis.require = createRequire(import.meta.url);
process.chdir(path.dirname(build));
const loader = await import(pathToFileURL(path.join(build, "FreeCADWebReverseEngineeringRuntimeProbe.js")));
const kernel = await loader.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebReverseEngineeringRuntimeProbe.wasm")), locateFile: (file) => path.join(build, file) });
const invoke = (value) => JSON.parse(String(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify(value)])));
const success = invoke(request);
const repeated = invoke(request);
const invalidRecord = invoke({ cmd: request.cmd, args: { tier: request.args.tier, records: [{ ...records[0], apiId: "py:BIM:class:ReverseEngineeringWorkbench" }] } });
const malformed = invoke({ cmd: request.cmd, args: { tier: "tier-2-pure", records } });
const malformedRepeat = invoke({ cmd: request.cmd, args: { tier: "tier-2-pure", records } });
const unsupported = invoke({ cmd: "reverseengineering.unknown", args: {} });
if (success.ok !== true || success.schema !== "freecad-web-reverseengineering/python-family-batch/v1" || success.module !== "ReverseEngineering" || success.tier !== "tier-1-readonly" || success.source !== "freecad-source" || success.count !== 1 || success.records?.length !== 1 || success.resourcesReleased !== true) throw new Error("ReverseEngineering family dispatch did not return a complete result");
const nativeRecord = success.records[0];
if (nativeRecord.status !== "restricted" || nativeRecord.code !== "gui_dependency_unavailable" || nativeRecord.native !== true || nativeRecord.resourcesReleased !== true || nativeRecord.sourceEntry !== "src/Mod/ReverseEngineering/InitGui.py") throw new Error("ReverseEngineeringWorkbench did not receive the expected GUI restriction");
if (JSON.stringify(repeated) !== JSON.stringify(success)) throw new Error("ReverseEngineering family replay changed the native result");
if (invalidRecord.ok !== true || invalidRecord.records?.[0]?.status !== "not_implemented" || invalidRecord.records?.[0]?.code !== "unmatched_source_record") throw new Error("module-scoped mismatch was not rejected");
if (malformed.ok !== false || malformed.code !== "invalid_request" || JSON.stringify(malformedRepeat) !== JSON.stringify(malformed)) throw new Error("invalid tier replay was not rejected deterministically");
if (unsupported.ok !== false || unsupported.code !== "unsupported_command") throw new Error("unsupported command was not rejected");
const hash = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
const evidenceHash = hash(nativeRecord);
const adapterManifestPath = path.join(docs, "reverseengineering-tier1-readonly-adapter-manifest.json");
for (const row of manifest.records) {
if (row.module !== "ReverseEngineering" || row.tier !== "tier-1-readonly") continue;
row.disposition = nativeRecord.status;
row.dispositionEvidence = ["docs/p6/reverseengineering-tier1-readonly-family.json"];
row.dispositionEvidenceHash = evidenceHash;
row.dispositionReason = "ReverseEngineeringWorkbench is registered from InitGui.py and requires FreeCADGui/desktop workbench capability unavailable in the GUI-free Web kernel";
}
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
const adapterManifest = JSON.parse(fs.readFileSync(adapterManifestPath, "utf8"));
adapterManifest.records = adapterManifest.records.map((row) => ({ ...row, disposition: nativeRecord.status, adapterStatus: "restricted_gui_dependency", evidenceHash }));
fs.writeFileSync(adapterManifestPath, `${JSON.stringify(adapterManifest, null, 2)}\n`);
const report = {
schema: "freecad-web-p6-reverseengineering-tier1-readonly-family/v1",
taskId: "FW-P6-FPA-F1-REVERSEENGINEERING",
module: "ReverseEngineering",
tier: "tier-1-readonly",
status: "pass",
dispatcher: "fc_web_dispatch_json",
route: { worker: "web/src/reverseengineering-python.worker.ts", command: request.cmd, native: "freecad-source/src/WebAssembly/FreeCADCoreProbe/ReverseEngineeringRuntimeProbe.cpp:fc_web_dispatch_json" },
sourceRevision: nativeRecord.sourceRevision,
input: request,
inputSha256: hash(request),
result: success,
resultSha256: hash(success),
replay: { equal: true, resultSha256: hash(repeated), resourcesReleased: success.resourcesReleased },
invalidRecord: { result: invalidRecord, resultSha256: hash(invalidRecord) },
malformedRequest: { first: malformed, repeated: malformedRepeat, resultSha256: hash(malformed), stable: true },
unsupportedCommand: { result: unsupported, resultSha256: hash(unsupported) },
records: [{ apiId: nativeRecord.apiId, status: nativeRecord.status, code: nativeRecord.code, evidenceHash }],
boundaries: [
"The selected class is a GUI workbench registration from InitGui.py; native FreeCAD marks it restricted rather than exposing a GUI object in the Web kernel.",
"Exact module, API name, and family matching prevents cross-module name promotion; mismatched records are terminal not_implemented.",
"No DocumentObject is created or mutated by this readonly dispatch; native resources are released on success and replay.",
],
};
fs.writeFileSync(path.join(docs, "reverseengineering-tier1-readonly-family.json"), `${JSON.stringify(report, null, 2)}\n`);
fs.writeFileSync(path.join(root, "docs/freecad-p6-reverseengineering-tier1-readonly-family.log"), `${JSON.stringify({ status: report.status, count: selected.length, disposition: nativeRecord.status, evidenceHash, resultSha256: report.resultSha256 })}\n`);
console.log(JSON.stringify({ status: report.status, count: selected.length, disposition: nativeRecord.status, evidenceHash, resultSha256: report.resultSha256 }));