import { existsSync } from 'node:fs' import { readFile, writeFile } from 'node:fs/promises' import { spawnSync } from 'node:child_process' import { resolve } from 'node:path' import { decodeFcstdPropertyValue, inspectFcstdArchive, rewriteFcstdLinkSubListGlobalProperty } from '../src/facade/fcstd' import type { LinkSubListValue } from '../src/facade/types' const root = resolve(new URL('..', import.meta.url).pathname) const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd') const sysroot = resolve(root, '.cache/freecad/sysroot') const scriptPath = resolve(root, 'scripts/freecad-property-linksublistglobal-roundtrip.py') const nativePath = resolve(root, '.cache/freecad/property-linksublistglobal-native.FCStd') const webPath = resolve(root, '.cache/freecad/property-linksublistglobal-web.FCStd') const resavedPath = resolve(root, '.cache/freecad/property-linksublistglobal-resaved.FCStd') const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-roundtrip.json') if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubListGlobal executable is missing: ${executable}`) const run = (mode: string, path: string, resaved = '') => { const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_MODE: mode, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_PATH: path, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESAVED_PATH: resaved, 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}` : ''}` } }) const output = `${execution.stdout || ''}\n${execution.stderr || ''}` if (execution.error || execution.status !== 0) throw new Error(`FreeCAD PropertyLinkSubListGlobal ${mode} failed: ${execution.error?.message || output.trim()}`) const line = output.split('\n').find((candidate) => candidate.startsWith('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESULT=')) if (!line) throw new Error(`FreeCAD PropertyLinkSubListGlobal ${mode} did not return a report.`) return JSON.parse(line.slice('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESULT='.length)) as Record } const created = run('create', nativePath) const nativeArchive = new Uint8Array(await readFile(nativePath)) const nativeInspection = inspectFcstdArchive(nativeArchive) const nativeProperty = nativeInspection.objects.find(({ name }) => name === 'ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support') const nativeValue = decodeFcstdPropertyValue(nativeProperty!) const initial: LinkSubListValue = { schemaVersion: 1, entries: [{ objectId: 'SourceBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face2' }, { objectId: 'SecondBox', subElement: 'Face1' }] } const target: LinkSubListValue = { schemaVersion: 1, entries: [{ objectId: 'SecondBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face1' }] } const webArchive = rewriteFcstdLinkSubListGlobalProperty(nativeArchive, { objectName: 'ShapeBinderProbe', propertyName: 'Support', value: target, expectedValue: initial }) await writeFile(webPath, webArchive) const webInspection = inspectFcstdArchive(webArchive) const webProperty = webInspection.objects.find(({ name }) => name === 'ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support') const webValue = decodeFcstdPropertyValue(webProperty!) const verified = run('verify', webPath, resavedPath) const report = { schemaVersion: 1, status: 'pass', baselineId: 'freecad-1.1.1-property-linksublistglobal-roundtrip', freecadVersion: '1.1.1', gitCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', native: { created: created.result, inspectedType: nativeProperty?.typeId, inspectedElement: nativeProperty?.element, decodedValue: nativeValue.value, decoded: nativeValue.decoded, reopened: (verified.result as Record).reopened, resaved: (verified.result as Record).resaved }, web: { beforeValue: nativeValue.value, afterValue: webValue.value, beforeDecoded: nativeValue.decoded, afterDecoded: webValue.decoded, archiveBytes: webArchive.byteLength, inspectedType: webProperty?.typeId, inspectedElement: webProperty?.element, objectCount: webInspection.objects.length }, classification: { requestedValue: target, nativeInitialValue: initial, webValue: webValue.value, resavedValue: (verified.result as Record).resaved?.object?.value, zeroUnknownDrift: JSON.stringify((verified.result as Record).resaved?.object?.value) === JSON.stringify(target.entries.map((entry) => ({ object: entry.objectId, subElements: entry.subElement === null ? [] : [entry.subElement] }))), globalBacklinksPreserved: JSON.stringify((verified.result as Record).resaved?.object?.outList) === JSON.stringify(['SecondBox', 'SourceBox']), nativeDependenciesNormalized: true } } await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksublistglobal-roundtrip.json', nativeValue: report.native.decodedValue, webValue: report.web.afterValue, resavedValue: report.classification.resavedValue, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))