import { spawnSync } from 'node:child_process' import { createHash } from 'node:crypto' import { mkdir, readFile, writeFile } from 'node:fs/promises' import { readFileSync } from 'node:fs' import { dirname, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { decodeFcstdPathProperty, inspectFcstdArchive, rewriteFcstdMetadataArchive, rewriteFcstdPathProperty } from '../src/facade/fcstd' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const executable = resolve(root, '.cache/freecad/install-desktop/bin/FreeCAD') const probe = resolve(root, 'scripts/freecad-cam-path-oracle.py') const sysroot = resolve(root, '.cache/freecad/sysroot') const outputDirectory = resolve(root, '.cache/freecad/cam-path-oracle') const sourcePath = resolve(outputDirectory, 'native-path-source.FCStd') const mutatedPath = resolve(outputDirectory, 'native-path-mutated.FCStd') const preservedPath = resolve(outputDirectory, 'web-preserved.FCStd') const webEditedPath = resolve(outputDirectory, 'web-edited-path.FCStd') const webEditedPythonPath = resolve(outputDirectory, 'web-edited-python-path.FCStd') const reportPath = resolve(root, 'config/freecad-cam-path-oracle.json') const marker = 'FREECAD_CAM_PATH_RESULT=' let probeCounter = 0 const fail = (message: string): never => { throw new Error(`FreeCAD CAM Path oracle: ${message}`) } const sha256 = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('hex') const semanticSha256 = (value: unknown) => createHash('sha256').update(JSON.stringify(value)).digest('hex') await mkdir(outputDirectory, { recursive: true }) const runtimeEnv = { ...process.env, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`, MATPLOTLIBRC: resolve(sysroot, 'usr/share/matplotlib/mpl-data/matplotlibrc'), MPLBACKEND: 'Agg', } const runProbe = (extraEnv: NodeJS.ProcessEnv) => { const resultFile = resolve(outputDirectory, `native-probe-result-${probeCounter++}.json`) const execution = spawnSync('xvfb-run', ['-a', executable, '--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), probe], { cwd: root, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, timeout: 120_000, env: { ...runtimeEnv, ...extraEnv, FREECAD_CAM_PATH_RESULT_FILE: resultFile }, }) const output = `${execution.stdout ?? ''}\n${execution.stderr ?? ''}` const line = output.split(/\r?\n/).find((candidate) => candidate.startsWith(marker)) const resultFileAvailable = (() => { try { return readFileSync(resultFile, 'utf8') } catch { return '' } })() const qtTeardownSignal = execution.status !== 0 && /SIGSEGV|Segmentation fault|Shiboken::BindingManager/.test(output) && Boolean(line || resultFileAvailable) if (execution.error || (execution.status !== 0 && !qtTeardownSignal) || (!line && !resultFileAvailable)) fail(`native probe exited with ${execution.status}: ${execution.error?.message ?? output.trim()}`) const result = resultFileAvailable ? JSON.parse(resultFileAvailable) as Record : JSON.parse(line!.slice(marker.length)) as Record if (qtTeardownSignal && result.nativeRuntime) result.nativeRuntime.qtTaskPanelTeardown = 'isolated-pyside-wrapper-release-after-complete-marker' return result } const native = runProbe({ FREECAD_CAM_PATH_SOURCE: sourcePath, FREECAD_CAM_PATH_MUTATED: mutatedPath, }) if (native.freecadVersion !== '1.1.1' || native.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('native runtime does not match the locked FreeCAD 1.1.1 oracle') if (native.nativeRuntime?.guiUp !== true || native.qtDynamicOracle?.activeWorkbench !== 'CAMWorkbench') fail('CAM Qt workbench did not activate in the GUI oracle') if (native.profileAlgorithm?.pathPropertyType !== 'Path::PropertyPath' || native.profileAlgorithm?.cuttingCommandCount !== 20 || native.profileAlgorithm?.commandCount < 20) fail('native Profile operation did not reproduce the locked algorithm result') if (native.profileAlgorithm?.variants?.length !== 2 || native.profileAlgorithm.variants.some((variant: any) => variant.commandCount !== 32 || variant.cuttingCommandCount !== 20 || !/^[0-9a-f]{64}$/.test(variant.commandSha256))) fail('native Profile compensation variants are incomplete') const expectedHelixDirections: Record = { 'inside-conventional': ['CW', 'G2'], 'outside-climb': ['CW', 'G2'], 'inside-climb': ['CCW', 'G3'], 'outside-conventional': ['CCW', 'G3'], } if (native.helixAlgorithm?.fixture !== 'Mod/CAM/CAMTests/test_holes00.fcstd' || native.helixAlgorithm?.pathPropertyType !== 'Path::PropertyPath' || native.helixAlgorithm?.toolDiameterMm !== 0.9 || native.helixAlgorithm?.baseSubElementCount !== 9 || native.helixAlgorithm?.scenarioCount !== 4) fail('native Helix fixture contract is incomplete') if (native.helixAlgorithm.scenarios.some((scenario: any) => { const expected = expectedHelixDirections[scenario.id] return !expected || scenario.direction !== expected[0] || scenario.expectedDirection !== expected[0] || scenario.expectedArc !== expected[1] || JSON.stringify(scenario.arcNames) !== JSON.stringify([expected[1]]) || scenario.arcCommandCount !== 1260 || scenario.commandCount < 1400 || !/^[0-9a-f]{64}$/.test(scenario.commandSha256) })) fail('native Helix direction or command evidence differs from the locked oracle') const incompleteActions = native.qtDynamicOracle?.requiredCommands?.filter((command: any) => command.emptySelection?.registered !== true || command.selectedModel?.registered !== true || command.selectedJob?.registered !== true || command.selectedOperation?.registered !== true || command.emptyDocument?.registered !== true || command.selectedModel?.actionCount < 1) ?? [] if (incompleteActions.length > 0) fail(`required CAM QAction registration evidence is incomplete: ${JSON.stringify(incompleteActions)}`) const original = new Uint8Array(await readFile(sourcePath)) const inspection = inspectFcstdArchive(original) const nativePath = inspection.objects.find((object) => object.name === 'NativePath') ?? fail('NativePath is missing from Web FCStd inspection') const pythonPath = inspection.objects.find((object) => object.name === 'PythonPath') ?? fail('PythonPath is missing from Web FCStd inspection') const pathProperties = [nativePath, pythonPath].map((object) => object.properties.find((property) => property.name === 'Path')) if (nativePath.typeId !== 'Path::Feature' || pythonPath.typeId !== 'Path::FeaturePython') fail('Web inspection did not preserve Path object TypeIds') if (pathProperties.some((property) => property?.typeId !== 'Path::PropertyPath')) fail('Web inspection did not preserve Path::PropertyPath metadata') if (!inspection.proxyDocument.readOnly || inspection.compatibility.blockedObjects < 1) fail('FeaturePython archive must remain a read-only proxy at the current Web codec boundary') const preserved = rewriteFcstdMetadataArchive(original, inspection.proxyDocument) if (sha256(original) !== sha256(preserved) || !Buffer.from(original).equals(Buffer.from(preserved))) fail('read-only Web rewrite was not byte-for-byte transparent') await writeFile(preservedPath, preserved) const reopened = runProbe({ FREECAD_CAM_PATH_VERIFY_ONLY: '1', FREECAD_CAM_PATH_VERIFY_ARCHIVE: preservedPath, }) if (reopened.verified !== true || reopened.objects?.some((object: any) => object.commandCount !== 5 || object.pathPropertyType !== 'Path::PropertyPath')) fail('FreeCAD could not reopen the Web-preserved Path archive') const decodedPath = decodeFcstdPathProperty(original, 'NativePath') if (decodedPath.commands.length !== 5 || decodedPath.resourcePath !== 'NativePath.nc') fail('Web Path::PropertyPath codec did not decode the native command resource') const edited = rewriteFcstdPathProperty(original, { objectName: 'NativePath', value: { ...decodedPath, commands: [...decodedPath.commands, { name: 'M3', parameters: { S: 12000 } }] }, }) const editedDecoded = decodeFcstdPathProperty(edited, 'NativePath') if (editedDecoded.commands.length !== 6 || editedDecoded.commands.at(-1)?.name !== 'M3' || editedDecoded.commands.at(-1)?.parameters.S !== 12000) fail('Web Path::PropertyPath codec did not persist the edited command') await writeFile(webEditedPath, edited) const editedReopened = runProbe({ FREECAD_CAM_PATH_VERIFY_ONLY: '1', FREECAD_CAM_PATH_VERIFY_ARCHIVE: webEditedPath, FREECAD_CAM_PATH_EXPECT_NATIVE_COUNT: '6', FREECAD_CAM_PATH_EXPECT_PYTHON_COUNT: '5', FREECAD_CAM_PATH_EXPECT_NATIVE_LAST: 'M3', }) if (editedReopened.verified !== true) fail('FreeCAD could not reopen the Web-edited native Path command resource') const decodedPythonPath = decodeFcstdPathProperty(original, 'PythonPath', 'Path', { allowFeaturePython: true }) if (decodedPythonPath.commands.length !== 5 || decodedPythonPath.resourcePath !== 'PythonPath.nc') fail('Web FeaturePython Path::PropertyPath opt-in codec did not decode the native command resource') const editedPython = rewriteFcstdPathProperty(original, { objectName: 'PythonPath', propertyName: 'Path', allowFeaturePython: true, value: { ...decodedPythonPath, commands: [...decodedPythonPath.commands, { name: 'M2', parameters: {} }] }, }) const editedPythonDecoded = decodeFcstdPathProperty(editedPython, 'PythonPath', 'Path', { allowFeaturePython: true }) if (editedPythonDecoded.commands.length !== 6 || editedPythonDecoded.commands.at(-1)?.name !== 'M2') fail('Web FeaturePython Path::PropertyPath codec did not persist the edited command') await writeFile(webEditedPythonPath, editedPython) const editedPythonReopened = runProbe({ FREECAD_CAM_PATH_VERIFY_ONLY: '1', FREECAD_CAM_PATH_VERIFY_ARCHIVE: webEditedPythonPath, FREECAD_CAM_PATH_EXPECT_NATIVE_COUNT: '5', FREECAD_CAM_PATH_EXPECT_PYTHON_COUNT: '6', FREECAD_CAM_PATH_EXPECT_PYTHON_LAST: 'M2', }) if (editedPythonReopened.verified !== true) fail('FreeCAD could not reopen the Web-edited FeaturePython Path command resource') const mutated = new Uint8Array(await readFile(mutatedPath)) inspectFcstdArchive(mutated) native.fcstdRoundTrip.source.semanticSha256 = semanticSha256(native.fcstdRoundTrip.sourceReopened) native.fcstdRoundTrip.mutated.semanticSha256 = semanticSha256(native.fcstdRoundTrip.mutatedReopened) reopened.archive.semanticSha256 = semanticSha256(reopened.objects) delete native.fcstdRoundTrip.source.sha256 delete native.fcstdRoundTrip.mutated.sha256 delete reopened.archive.sha256 delete native.fcstdRoundTrip.source.byteLength delete native.fcstdRoundTrip.mutated.byteLength delete reopened.archive.byteLength const relativePath = (path: string) => relative(root, path).replaceAll('\\', '/') for (const archive of [native.fcstdRoundTrip.source, native.fcstdRoundTrip.mutated, reopened.archive]) archive.path = relativePath(archive.path) native.nativeRuntime.pathModule = native.nativeRuntime.pathModule.startsWith(root) ? relativePath(native.nativeRuntime.pathModule) : native.nativeRuntime.pathModule const report = { schemaVersion: 1, baselineId: 'freecad-1.1.1', sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', generatedBy: './npmw run probe:freecad-cam-path', checkedBy: './npmw run check:freecad-cam-path', runtime: native.nativeRuntime, profileAlgorithm: native.profileAlgorithm, helixAlgorithm: native.helixAlgorithm, qtDynamicOracle: native.qtDynamicOracle, fcstdRoundTrip: { ...native.fcstdRoundTrip, webInspection: { objectTypes: [nativePath.typeId, pythonPath.typeId], pathPropertyTypes: pathProperties.map((property) => property?.typeId), compatibility: inspection.compatibility, proxyReadOnly: inspection.proxyDocument.readOnly, }, webTransparentPreservation: { archive: reopened.archive, byteExact: true, nativeReopenVerified: reopened.verified, objects: reopened.objects, }, webEditablePathProperty: { objectName: 'NativePath', resourcePath: editedDecoded.resourcePath, sourceCommandCount: decodedPath.commands.length, editedCommandCount: editedDecoded.commands.length, lastCommand: editedDecoded.commands.at(-1), nativeReopenVerified: editedReopened.verified, featurePythonExecutionBlocked: true, }, webEditableFeaturePythonPathProperty: { objectName: 'PythonPath', resourcePath: editedPythonDecoded.resourcePath, sourceCommandCount: decodedPythonPath.commands.length, editedCommandCount: editedPythonDecoded.commands.length, lastCommand: editedPythonDecoded.commands.at(-1), nativeReopenVerified: editedPythonReopened.verified, scriptExecution: 'blocked', optInRequired: true, }, }, claims: { nativeProfilePathAlgorithm: 'verified', nativeHelixPathAlgorithm: 'verified-four-direction-cases', qtCamWorkbenchDynamicOracle: 'verified', nativePathFeatureFcstdSaveOpenMutateSaveOpen: 'verified', nativeWebNativePathFcstdTransparentRoundTrip: 'verified', webEditablePathPropertyCodec: 'verified-native-path-feature', webEditableFeaturePythonPathResourceCodec: 'verified-safe-resource-only-opt-in', }, boundary: { exactEvidence: 'FreeCAD 1.1.1 executes compensated/uncompensated Profile and four Helix direction cases, exposes live Qt CAM actions, and reopens both Path object types after native and byte-transparent Web round-trips.', remainingGap: 'FeaturePython Path::PropertyPath resource editing is now an explicit safe opt-in that never executes Python; FeaturePython operation metadata, recompute behavior and algorithms beyond the locked Profile/Helix scenarios remain unclaimed.', exactParityClaim: false, }, status: 'verified-with-native-path-editable-subset', } await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`) console.log(JSON.stringify({ status: report.status, profileCommands: report.profileAlgorithm.commandCount, cuttingCommands: report.profileAlgorithm.cuttingCommandCount, helixScenarios: report.helixAlgorithm.scenarioCount, camCommands: report.qtDynamicOracle.camCommandCount, qtVersion: report.qtDynamicOracle.qtVersion, fcstdObjects: report.fcstdRoundTrip.webInspection.objectTypes, byteExact: report.fcstdRoundTrip.webTransparentPreservation.byteExact, report: relativePath(reportPath), }, null, 2))