import { spawnSync } from 'node:child_process' import { createHash } from 'node:crypto' import { existsSync } from 'node:fs' import { readFile, stat, writeFile } from 'node:fs/promises' import { resolve } from 'node:path' import { buildRecoveredNamingSnapshot, recoveredNamingDecisions, same, validateRecoveredNamingSnapshot } from './freecad-recovered-naming-classification.mjs' const root = resolve(new URL('..', import.meta.url).pathname) const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd') const pythonPath = resolve(root, '.cache/freecad/sysroot/usr/lib/python3/dist-packages') const harnessPath = resolve(root, 'scripts/freecad-composite-history-elementmap-oracle.py') const oraclePath = resolve(root, 'config/freecad-composite-history-elementmap-oracle.json') const resavePath = resolve(root, 'config/freecad-composite-history-resave-verification.json') const outputPath = resolve(root, 'config/freecad-recovered-naming-classification.json') const fail = (message) => { throw new Error(`FreeCAD recovered naming classification: ${message}`) } const collectDifferences = (reference, replay, path = '$', differences = []) => { if (same(reference, replay)) return differences if (Array.isArray(reference) && Array.isArray(replay)) { for (let index = 0; index < Math.max(reference.length, replay.length); index += 1) collectDifferences(reference[index], replay[index], `${path}[${index}]`, differences) return differences } if (reference && replay && typeof reference === 'object' && typeof replay === 'object') { for (const key of new Set([...Object.keys(reference), ...Object.keys(replay)])) collectDifferences(reference[key], replay[key], `${path}.${key}`, differences) return differences } differences.push({ path, reference, replay }) return differences } if (!existsSync(executable)) fail(`native executable is missing: ${executable}`) const [reference, referenceResave, harnessContent, harnessBytes] = await Promise.all([ readFile(oraclePath, 'utf8').then(JSON.parse), readFile(resavePath, 'utf8').then(JSON.parse), readFile(harnessPath), stat(harnessPath).then(({ size }) => size), ]) const execution = spawnSync(executable, ['--python-path', pythonPath, harnessPath], { cwd: root, encoding: 'utf8', timeout: 180_000, maxBuffer: 80 * 1024 * 1024, env: { ...process.env, PYTHONPATH: `${pythonPath}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(root, '.cache/freecad/sysroot/usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`, MATPLOTLIBRC: resolve(root, '.cache/freecad/sysroot/etc/matplotlibrc'), MPLBACKEND: 'Agg', }, }) const output = `${execution.stdout || ''}\n${execution.stderr || ''}` const marker = 'FREECAD_COMPOSITE_HISTORY_ELEMENTMAP_RESULT=' const markerIndex = output.indexOf(marker) const payload = markerIndex >= 0 ? output.slice(markerIndex + marker.length).match(/\{.*\}/s)?.[0] : undefined if (execution.error || execution.status !== 0 || !payload) fail(`native replay exited with ${execution.status}: ${execution.error?.message || output.trim()}`) const replay = JSON.parse(payload) if (reference.freecadVersion !== '1.1.1' || reference.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || replay.freecadVersion !== reference.freecadVersion || replay.gitCommit !== reference.gitCommit || replay.status !== 'pass') fail('native baseline or replay status is invalid.') const classifications = recoveredNamingDecisions.map((decision) => { const referenceFixture = reference.cases.find(({ id }) => id === decision.caseId) const referenceResaveFixture = referenceResave.cases.find(({ id }) => id === decision.caseId) const replayFixture = replay.cases.find(({ id }) => id === decision.caseId) if (![referenceFixture, referenceResaveFixture, replayFixture].every(Boolean)) fail(`${decision.caseId} is missing from reference or replay evidence.`) const referenceSnapshot = buildRecoveredNamingSnapshot(referenceFixture, referenceResaveFixture) const replaySnapshot = buildRecoveredNamingSnapshot(replayFixture, { stageCorrelations: replayFixture.stageCorrelations, roundtripNameDrift: replayFixture.roundtripNameDrift, resaveNameDrift: replayFixture.resaveNameDrift, nativeDesktopResaveCovered: replayFixture.nativeDesktopResaveCovered, }) validateRecoveredNamingSnapshot(referenceSnapshot) validateRecoveredNamingSnapshot(replaySnapshot) if (!same(referenceSnapshot, replaySnapshot)) fail(`${decision.caseId} fingerprints differ between the locked reference and independent native replay: ${JSON.stringify(collectDifferences(referenceSnapshot, replaySnapshot))}`) return { ...decision, reference: referenceSnapshot, replay: replaySnapshot, replayStable: true, implementationDefect: decision.classification === 'implementation_defect', } }) const countClassification = (classification) => classifications.filter((entry) => entry.classification === classification).length const classifiedStages = classifications.reduce((sum, entry) => sum + entry.reference.driftOrdinals.length, 0) const report = { schemaVersion: 1, baseline: { freecadVersion: reference.freecadVersion, commit: reference.gitCommit, oracleId: reference.baselineId, }, generatedBy: './npmw run probe:freecad-recovered-naming-classification', checkedBy: './npmw run check:freecad-recovered-naming-classification', nativeProbe: { path: 'scripts/freecad-composite-history-elementmap-oracle.py', bytes: harnessBytes, sha256: createHash('sha256').update(harnessContent).digest('hex'), independentRuns: 2, }, classifications, summary: { classifiedCases: classifications.length, classifiedStages, stableSemantics: countClassification('stable_semantics'), allowedEvolution: countClassification('allowed_evolution'), implementationDefects: countClassification('implementation_defect'), unknown: 0, }, } await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) console.log(JSON.stringify({ status: 'freecad-recovered-naming-classification-generated', cases: report.classifications.map(({ taskId, caseId, classification, reference: snapshot }) => ({ taskId, caseId, classification, driftOrdinals: snapshot.driftOrdinals })), independentRuns: report.nativeProbe.independentRuns, output: 'config/freecad-recovered-naming-classification.json', }, null, 2))