import { createHash } from 'node:crypto' import { execFileSync, spawnSync } from 'node:child_process' import { readFile, stat } from 'node:fs/promises' import { resolve } from 'node:path' const root = resolve(new URL('..', import.meta.url).pathname) const evidence = JSON.parse(await readFile(resolve(root, 'config/camotics-native-artifact.json'), 'utf8')) const sha256 = (value) => createHash('sha256').update(value).digest('hex') const run = (command, args, options = {}) => { const result = spawnSync(command, args, { encoding: 'utf8', ...options }) if (result.error || result.status !== 0) throw new Error(`${command} failed: ${result.error?.message || result.stderr || result.stdout}`) return result } if (evidence.schemaVersion !== 2 || evidence.status !== 'pass') throw new Error('CAMotics native evidence is not passing.') const camoticsPath = resolve(root, evidence.source.path) const camoticsRevision = execFileSync('git', ['-C', camoticsPath, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim() if (camoticsRevision !== evidence.source.revision) throw new Error(`CAMotics source revision drift: ${camoticsRevision}`) const cbangPath = resolve(root, evidence.cbang.path) const cbangRevision = execFileSync('git', ['-C', cbangPath, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim() if (cbangRevision !== evidence.cbang.revision) throw new Error(`C! source revision drift: ${cbangRevision}`) if (execFileSync('git', ['-C', cbangPath, 'status', '--porcelain'], { encoding: 'utf8' }).trim() !== '') throw new Error('Pinned C! checkout is dirty.') const cbangSConstruct = await readFile(resolve(root, 'CAMotics/cbang/SConstruct'), 'utf8') if (!cbangSConstruct.includes(`version = '${evidence.cbang.version}'`)) throw new Error('C! version does not match native evidence.') if (!evidence.build.options.includes('v8_compress_pointers=0') || evidence.build.withGui !== true || evidence.build.withTpl !== true || evidence.build.browserExecutable !== false || evidence.pipelineBoundary.workspaceNativeCliAvailable !== true) throw new Error('CAMotics native build boundary is invalid.') for (const [path, expected] of Object.entries(evidence.artifacts)) { const absolute = resolve(root, path) const artifact = await readFile(absolute) const metadata = await stat(absolute) if (!metadata.isFile() || (metadata.mode & 0o111) === 0) throw new Error(`CAMotics artifact is not executable: ${path}`) if (artifact.byteLength !== expected.bytes || sha256(artifact) !== expected.sha256) throw new Error(`CAMotics artifact size or digest mismatch: ${path}`) if (artifact.subarray(0, 4).toString('ascii') !== '\x7fELF') throw new Error(`CAMotics artifact is not an ELF executable: ${path}`) } for (const [path, expected] of Object.entries(evidence.cliArtifacts ?? {})) { const absolute = resolve(root, path) const artifact = await readFile(absolute) const metadata = await stat(absolute) if (!metadata.isFile() || (metadata.mode & 0o111) === 0 || artifact.byteLength !== expected.bytes || sha256(artifact) !== expected.sha256) throw new Error(`CAMotics CLI artifact size or digest mismatch: ${path}`) if (artifact.subarray(0, 4).toString('ascii') !== '\x7fELF') throw new Error(`CAMotics CLI artifact is not an ELF file: ${path}`) } const camoticsVersionProbe = run(resolve(root, 'CAMotics/camotics'), ['--version']) const tplVersionProbe = run(resolve(root, 'CAMotics/tplang'), ['--version']) const camoticsVersion = `${camoticsVersionProbe.stdout}${camoticsVersionProbe.stderr}`.trim() const tplVersion = `${tplVersionProbe.stdout}${tplVersionProbe.stderr}`.trim() if (camoticsVersion !== evidence.source.version || tplVersion !== evidence.source.version) throw new Error(`CAMotics version probe failed: ${camoticsVersion}/${tplVersion}`) const tplTests = run(resolve(root, 'CAMotics/tests/testHarness'), ['--no-color'], { cwd: resolve(root, 'CAMotics/tests/tplTests') }) const passed = Number(tplTests.stdout.match(/^Passed\s+(\d+)/m)?.[1] ?? -1) const failed = Number(tplTests.stdout.match(/^Failed\s+(\d+)/m)?.[1] ?? -1) if (passed !== evidence.tplTests.passed || failed !== evidence.tplTests.failed) throw new Error(`TPL test result drift: ${passed}/${failed}`) const tplExample = run(resolve(root, 'CAMotics/tplang'), [resolve(root, 'CAMotics/examples/box/box.tpl')]) const tplBytes = Buffer.from(tplExample.stdout) if (tplBytes.byteLength !== evidence.tplExample.bytes || sha256(tplBytes) !== evidence.tplExample.sha256) throw new Error('TPL example output drift.') const nativeLinks = run('ldd', [resolve(root, 'CAMotics/camotics')]).stdout for (const required of ['libQt5Widgets', 'libQt5WebSockets', 'libQt5Network', 'libQt5Core', 'libnode']) if (!nativeLinks.includes(required)) throw new Error(`CAMotics GUI is missing runtime dependency ${required}.`) if (evidence.guiSmoke.status !== 'pass' || evidence.guiSmoke.window.mapState !== 'IsViewable' || evidence.guiSmoke.window.width !== 1200 || evidence.guiSmoke.window.height !== 800 || evidence.guiSmoke.window.depth !== 24 || evidence.guiSmoke.processAliveBeforeTerminate !== true || evidence.guiSmoke.browserRuntime !== false) throw new Error('CAMotics GUI smoke boundary is invalid.') if (evidence.pipelineBoundary.workspaceNativeGuiAvailable !== true || evidence.pipelineBoundary.workspaceNativeTplAvailable !== true || evidence.pipelineBoundary.workspaceNativeCliAvailable !== true || evidence.pipelineBoundary.browserNativeGuiAvailable !== false || evidence.pipelineBoundary.browserNativeTplAvailable !== false || evidence.pipelineBoundary.canonicalGcodeParser !== 'linuxcnc-wasm' || evidence.pipelineBoundary.camoticsParsesCanonicalPipelineGcode !== false) throw new Error('CAMotics/LinuxCNC authority boundary is invalid.') for (const [path, smoke] of Object.entries(evidence.cliSmoke ?? {})) if (smoke.exitCode !== 0 || smoke.containsUsage !== true) throw new Error(`CAMotics CLI smoke failed: ${path}`) console.log(JSON.stringify({ status: 'camotics-native-pass', camoticsRevision, cbangRevision, versions: { camotics: camoticsVersion, tplang: tplVersion }, artifacts: evidence.artifacts, cliArtifacts: evidence.cliArtifacts, cliSmoke: evidence.cliSmoke, tplTests: { passed, failed }, tplExample: evidence.tplExample, guiSmoke: evidence.guiSmoke, pipelineBoundary: evidence.pipelineBoundary, }, null, 2))