81 lines
6.6 KiB
JavaScript
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 }));
|