import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { Worker } from "node:worker_threads"; import { createRequire } from "node:module"; import { pathToFileURL } from "node:url"; const root = "/home/mes123456/FreeCADWasmDev/working/FreeCAD"; const build = path.resolve(process.argv[2] || path.join(root, "freecad-source/build-web/configure/bin")); const output = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-part-method-")); execFileSync(path.join(root, "web/node_modules/.bin/tsc"), ["--target", "ES2022", "--module", "ESNext", "--moduleResolution", "Bundler", "--strict", "--skipLibCheck", "--outDir", output, "--declaration", "false", "--noEmit", "false", path.join(root, "web/src/facade-envelope.ts"), path.join(root, "web/src/part-python-method-facade.ts")], { cwd: root, stdio: "inherit" }); const facadePath = pathToFileURL(path.join(output, "part-python-method-facade.js")).href; const envelopePath = pathToFileURL(path.join(output, "facade-envelope.js")).href; const request = { outerRadius: 5, innerRadius: 2, height: 10 }; const create = (id, args = request) => ({ schema: "freecad-web-facade/v1", version: 1, kind: "request", requestId: id, command: "part.pythonMethod", documentId: "part-method-document", payload: { schema: "freecad-web-part/python-method/v1", module: "Part", tier: "tier-2-pure", apiId: "py:Part:function:makeTube", name: "makeTube", family: "pure-function", args } }); const addTubeRequest = { name: "TubeNative" }; const createAdd = (id, args = addTubeRequest) => ({ schema: "freecad-web-facade/v1", version: 1, kind: "request", requestId: id, command: "part.pythonMethod", documentId: "part-method-document", payload: { schema: "freecad-web-part/python-method/v1", module: "Part", tier: "tier-2-pure", apiId: "py:Part:function:addTube", name: "addTube", family: "pure-function", args } }); const dimensionRequest = { shapeTypes: ["Solid", "Solid"] }; const createDimension = (id, args = dimensionRequest) => ({ schema: "freecad-web-facade/v1", version: 1, kind: "request", requestId: id, command: "part.pythonMethod", documentId: "part-method-document", payload: { schema: "freecad-web-part/python-method/v1", module: "Part", tier: "tier-2-pure", apiId: "py:Part:function:dimensionOfShapes", name: "dimensionOfShapes", family: "geometry-value", args } }); const roundRequest = { vector: [1.2345, -2.3456, 3.4567], decimals: 2 }; const createRound = (id, args = roundRequest) => ({ schema: "freecad-web-facade/v1", version: 1, kind: "request", requestId: id, command: "part.pythonMethod", documentId: "part-method-document", payload: { schema: "freecad-web-part/python-method/v1", module: "Part", tier: "tier-2-pure", apiId: "py:Part:function:roundVector", name: "roundVector", family: "geometry-value", args } }); const placementRequest = { base1: [0, 0, 0], base2: [0, 0, 0], rotation1: [0, 0, 0, 1], rotation2: [0, 0, 0, 1] }; const createPlacement = (id, args = placementRequest) => ({ schema: "freecad-web-facade/v1", version: 1, kind: "request", requestId: id, command: "part.pythonMethod", documentId: "part-method-document", payload: { schema: "freecad-web-part/python-method/v1", module: "Part", tier: "tier-2-pure", apiId: "py:Part:function:PlacementsFuzzyCompare", name: "PlacementsFuzzyCompare", family: "pure-function", args } }); const worker = new Worker(path.join(root, "tools/part-python-method-facade-smoke-worker.mjs"), { workerData: { buildDirectory: build, facadePath, envelopePath } }); const next = () => new Promise((resolve, reject) => { const onMessage = (value) => { worker.off("error", onError); resolve(value); }; const onError = (error) => { worker.off("message", onMessage); reject(error); }; worker.once("message", onMessage); worker.once("error", onError); }); const ready = await next(); worker.postMessage(createDimension("part-dimension-success")); const dimensionSuccess = await next(); worker.postMessage(createDimension("part-dimension-repeat")); const dimensionRepeat = await next(); worker.postMessage(createDimension("part-dimension-invalid", { shapeTypes: ["Solid", "Unknown"] })); const dimensionInvalid = await next(); worker.postMessage(createDimension("part-dimension-mixed", { shapeTypes: ["Solid", "Edge"] })); const dimensionMixed = await next(); worker.postMessage(createDimension("part-dimension-cancel")); worker.postMessage({ schema: "freecad-web-facade/v1", version: 1, kind: "cancel", requestId: "part-dimension-cancel-control", cancelOf: "part-dimension-cancel", documentId: "part-method-document" }); const dimensionCancelled = await next(); worker.postMessage(createAdd("part-add-success")); const addSuccess = await next(); worker.postMessage(createAdd("part-add-repeat")); const addRepeat = await next(); worker.postMessage(createAdd("part-add-invalid", { name: "bad-name" })); const addInvalid = await next(); worker.postMessage(createAdd("part-add-cancel")); worker.postMessage({ schema: "freecad-web-facade/v1", version: 1, kind: "cancel", requestId: "part-add-cancel-control", cancelOf: "part-add-cancel", documentId: "part-method-document" }); const addCancelled = await next(); worker.postMessage(create("part-method-success")); const success = await next(); worker.postMessage(create("part-method-repeat")); const repeat = await next(); worker.postMessage(create("part-method-invalid", { outerRadius: 2, innerRadius: 2, height: 10 })); const invalid = await next(); worker.postMessage(create("part-method-cancel")); worker.postMessage({ schema: "freecad-web-facade/v1", version: 1, kind: "cancel", requestId: "part-method-cancel-control", cancelOf: "part-method-cancel", documentId: "part-method-document" }); const cancelled = await next(); worker.postMessage(createRound("part-round-success")); const roundSuccess = await next(); worker.postMessage(createRound("part-round-repeat")); const roundRepeat = await next(); worker.postMessage(createRound("part-round-invalid", { vector: [1, 2, 3], decimals: 13 })); const roundInvalid = await next(); worker.postMessage(createRound("part-round-cancel")); worker.postMessage({ schema: "freecad-web-facade/v1", version: 1, kind: "cancel", requestId: "part-round-cancel-control", cancelOf: "part-round-cancel", documentId: "part-method-document" }); const roundCancelled = await next(); worker.postMessage(createPlacement("part-placement-success")); const placementSuccess = await next(); worker.postMessage(createPlacement("part-placement-repeat")); const placementRepeat = await next(); worker.postMessage(createPlacement("part-placement-invalid", { ...placementRequest, base1: [0, 0] })); const placementInvalid = await next(); worker.postMessage(createPlacement("part-placement-cancel")); worker.postMessage({ schema: "freecad-web-facade/v1", version: 1, kind: "cancel", requestId: "part-placement-cancel-control", cancelOf: "part-placement-cancel", documentId: "part-method-document" }); const placementCancelled = await next(); await worker.terminate(); globalThis.__dirname = `${build}${path.sep}`; globalThis.require = createRequire(import.meta.url); const module = await import(pathToFileURL(path.join(build, "FreeCADWebPartBoxProbe.js"))); const kernel = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebPartBoxProbe.wasm")), print: () => {}, printErr: () => {} }); const invoke = (value) => JSON.parse(String(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify(value)]))); const dimensionDirect = invoke({ cmd: "part.pythonMethod", args: createDimension("dimension-direct").payload }); const addDirect = invoke({ cmd: "part.pythonMethod", args: createAdd("add-direct").payload }); const crossModule = invoke({ cmd: "part.pythonMethod", args: { ...create("direct").payload, module: "BIM" } }); const roundDirect = invoke({ cmd: "part.pythonMethod", args: createRound("round-direct").payload }); const placementDirect = invoke({ cmd: "part.pythonMethod", args: createPlacement("placement-direct").payload }); const unsupported = invoke({ cmd: "part.unknownMethod", args: {} }); const diagnostics = { ready, dimensionSuccess, dimensionRepeat, dimensionInvalid, dimensionMixed, dimensionCancelled, addSuccess, addRepeat, addInvalid, addCancelled, success, repeat, invalid, cancelled, roundSuccess, roundRepeat, roundInvalid, roundCancelled, placementSuccess, placementRepeat, placementInvalid, placementCancelled, dimensionDirect, addDirect, crossModule, roundDirect, placementDirect, unsupported }; if (ready.kind !== "ready" || dimensionSuccess.kind !== "result" || dimensionSuccess.result.status !== "exposed" || dimensionSuccess.result.result.dimension !== 3 || dimensionSuccess.result.result.shapeCount !== 2 || dimensionRepeat.kind !== "result" || JSON.stringify(dimensionSuccess.result) !== JSON.stringify(dimensionRepeat.result) || dimensionInvalid.kind !== "error" || dimensionInvalid.error.code !== "invalid_arguments" || dimensionMixed.kind !== "error" || dimensionMixed.error.code !== "source_error" || dimensionCancelled.kind !== "error" || dimensionCancelled.error.code !== "cancelled" || addSuccess.kind !== "result" || addSuccess.result.status !== "exposed" || addSuccess.result.result.objectType !== "Part::FeaturePython" || addSuccess.result.result.name !== "TubeNative" || addSuccess.result.result.solid !== true || Math.abs(addSuccess.result.result.volume - 659.7344572538565) > 1e-7 || addRepeat.kind !== "result" || JSON.stringify(addSuccess.result) !== JSON.stringify(addRepeat.result) || addInvalid.kind !== "error" || addInvalid.error.code !== "invalid_arguments" || addCancelled.kind !== "error" || addCancelled.error.code !== "cancelled" || success.kind !== "result" || success.result.status !== "exposed" || success.result.result.solid !== true || Math.abs(success.result.result.volume - 659.7344572538565) > 1e-7 || repeat.kind !== "result" || JSON.stringify(success.result) !== JSON.stringify(repeat.result) || invalid.kind !== "error" || invalid.error.code !== "invalid_arguments" || cancelled.kind !== "error" || cancelled.error.code !== "cancelled" || roundSuccess.kind !== "result" || roundSuccess.result.status !== "exposed" || roundSuccess.result.result.value !== "[1.23, -2.35, 3.46]" || roundRepeat.kind !== "result" || JSON.stringify(roundSuccess.result) !== JSON.stringify(roundRepeat.result) || roundInvalid.kind !== "error" || roundInvalid.error.code !== "invalid_arguments" || roundCancelled.kind !== "error" || roundCancelled.error.code !== "cancelled" || placementSuccess.kind !== "result" || placementSuccess.result.status !== "exposed" || placementSuccess.result.result.equal !== true || placementRepeat.kind !== "result" || JSON.stringify(placementSuccess.result) !== JSON.stringify(placementRepeat.result) || placementInvalid.kind !== "error" || placementInvalid.error.code !== "invalid_request" || placementCancelled.kind !== "error" || placementCancelled.error.code !== "cancelled" || dimensionDirect.ok !== true || dimensionDirect.result.dimension !== 3 || addDirect.ok !== true || addDirect.result.objectType !== "Part::FeaturePython" || crossModule.ok !== false || crossModule.code !== "unmatched_source_record" || roundDirect.ok !== true || roundDirect.result.value !== "[1.23, -2.35, 3.46]" || placementDirect.ok !== true || placementDirect.result.equal !== true || unsupported.ok !== false || unsupported.code !== "unsupported_command") { console.error(JSON.stringify(diagnostics)); throw new Error("Part Python method gate assertion failed"); } const manifestPath = path.join(root, "docs/p6/python-api-manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); for (const apiId of ["py:Part:function:addTube", "py:Part:function:dimensionOfShapes", "py:Part:function:makeTube", "py:Part:function:roundVector", "py:Part:function:PlacementsFuzzyCompare"]) { const row = manifest.records.find((item) => item.id === apiId); if (!row) throw new Error(`${apiId} is missing from the source manifest`); row.disposition = "exposed"; row.dispositionEvidence = ["docs/p6/part-python-method-runtime.json"]; row.dispositionReason = `${apiId.endsWith("addTube") ? "typed Part.addTube" : apiId.endsWith("dimensionOfShapes") ? "typed Part.dimensionOfShapes" : apiId.endsWith("makeTube") ? "typed Part.makeTube" : "typed Part.roundVector"} native method replay through fc_web_dispatch_json`; } fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); const hash = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); const fileHash = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); const report = { schema: "freecad-web-p6-part-python-method-runtime/v1", taskId: "FW-P6-FPA-Y", module: "Part", tier: "tier-2-pure", apiId: "py:Part:function:addTube", status: "pass", methods: [{ apiId: "py:Part:function:addTube", sourceEntry: "src/Mod/Part/BasicShapes/Shapes.py", request: createAdd("part-add-success"), requestSha256: hash(createAdd("part-add-success")), resultSha256: hash(addSuccess), trace: { success: addSuccess, repeat: addRepeat, invalid: addInvalid, cancelled: addCancelled, direct: addDirect }, replay: { equal: true }, resources: { resourcesReleased: addSuccess.result.resourcesReleased === true, documentMutation: true } }, { apiId: "py:Part:function:dimensionOfShapes", sourceEntry: "src/Mod/Part/BOPTools/ShapeMerge.py", request: createDimension("part-dimension-success"), requestSha256: hash(createDimension("part-dimension-success")), resultSha256: hash(dimensionSuccess), trace: { success: dimensionSuccess, repeat: dimensionRepeat, invalid: dimensionInvalid, mixed: dimensionMixed, cancelled: dimensionCancelled, direct: dimensionDirect }, replay: { equal: true }, resources: { resourcesReleased: dimensionSuccess.result.resourcesReleased === true, documentMutation: false } }, { apiId: "py:Part:function:makeTube", sourceEntry: "src/Mod/Part/BasicShapes/Shapes.py", request: create("part-method-success"), requestSha256: hash(create("part-method-success")), resultSha256: hash(success), trace: { success, repeat, invalid, cancelled, crossModule, unsupported }, replay: { equal: true }, resources: { resourcesReleased: success.result.resourcesReleased === true, documentMutation: false } }, { apiId: "py:Part:function:roundVector", sourceEntry: "src/Mod/Part/BasicShapes/ShapeContent.py", request: createRound("part-round-success"), requestSha256: hash(createRound("part-round-success")), resultSha256: hash(roundSuccess), trace: { success: roundSuccess, repeat: roundRepeat, invalid: roundInvalid, cancelled: roundCancelled, direct: roundDirect }, replay: { equal: true }, resources: { resourcesReleased: roundSuccess.result.resourcesReleased === true, documentMutation: false } }, { apiId: "py:Part:function:PlacementsFuzzyCompare", sourceEntry: "src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py", request: createPlacement("part-placement-success"), requestSha256: hash(createPlacement("part-placement-success")), resultSha256: hash(placementSuccess), trace: { success: placementSuccess, repeat: placementRepeat, invalid: placementInvalid, cancelled: placementCancelled, direct: placementDirect }, replay: { equal: true }, resources: { resourcesReleased: placementSuccess.result.resourcesReleased === true, documentMutation: false } }], route: { command: "part.pythonMethod", worker: "web/src/part.worker.ts", facade: "web/src/part-python-method-facade.ts", native: "freecad-source/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp:fc_web_dispatch_json" }, request: createDimension("part-dimension-success"), requestSha256: hash(createDimension("part-dimension-success")), trace: { ready, dimensionSuccess, dimensionRepeat, dimensionInvalid, dimensionMixed, dimensionCancelled, addSuccess, addRepeat, addInvalid, addCancelled, success, repeat, invalid, cancelled, roundSuccess, roundRepeat, roundInvalid, roundCancelled, placementSuccess, placementRepeat, placementInvalid, placementCancelled, dimensionDirect, addDirect, crossModule, roundDirect, placementDirect, unsupported }, resultSha256: hash(dimensionSuccess), replay: { equal: true }, resources: { resourcesReleased: dimensionSuccess.result.resourcesReleased === true, documentMutation: false }, artifacts: { wasm: { path: "web/public/kernel/FreeCADWebPartBoxProbe.wasm", sha256: fileHash(path.join(root, "web/public/kernel/FreeCADWebPartBoxProbe.wasm")) }, loader: { path: "web/public/kernel/FreeCADWebPartBoxProbe.js", sha256: fileHash(path.join(root, "web/public/kernel/FreeCADWebPartBoxProbe.js")) }, source: { path: "freecad-source/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp", officialPython: ["freecad-source/src/Mod/Part/BasicShapes/Shapes.py", "freecad-source/src/Mod/Part/BOPTools/ShapeMerge.py", "freecad-source/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py"] } }, boundaries: ["Only the manifest-backed Part.addTube, Part.dimensionOfShapes, Part.makeTube, Part.roundVector and Part.PlacementsFuzzyCompare signatures are exposed; other Part APIs remain disposition-gated.", "Part.addTube executes the official BasicShapes.Shapes source inside native Python and closes its temporary document before returning.", "Part.dimensionOfShapes executes the official BOPTools.ShapeMerge source against native Part Shape fixtures; mixed dimensions remain a source_error boundary.", "The native result is produced by FreeCAD source execution; no JavaScript geometry, placement comparison, or rounding substitute is used."] }; fs.writeFileSync(path.join(root, "docs/p6/part-python-method-runtime.json"), `${JSON.stringify(report, null, 2)}\n`); fs.writeFileSync(path.join(root, "docs/freecad-p6-part-python-method-gate.log"), `${JSON.stringify({ status: report.status, apiId: report.apiId, resultSha256: report.resultSha256 })}\n`); console.log(JSON.stringify({ status: report.status, apiId: report.apiId, volume: success.result.result.volume, resultSha256: report.resultSha256 }));