117 lines
6.4 KiB
JavaScript
117 lines
6.4 KiB
JavaScript
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))
|