import { mkdir, readFile, writeFile } from 'node:fs/promises' import { resolve } from 'node:path' import { spawnSync } from 'node:child_process' import { loadGoldenManifest } from './freecad-golden-contract.mjs' const root = resolve(new URL('..', import.meta.url).pathname) const scenarioDir = resolve(root, 'fixtures/freecad-golden/scenarios') const failureDir = resolve(root, 'fixtures/freecad-golden/failures') const workDir = resolve(root, '.cache/freecad/generated-fixtures') const command = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-native/bin/FreeCADCmd') const driver = resolve(root, 'scripts/freecad-golden-driver.py') await mkdir(scenarioDir, { recursive: true }) await mkdir(failureDir, { recursive: true }) await mkdir(workDir, { recursive: true }) const manifest = await loadGoldenManifest(resolve(root, 'fixtures/freecad-golden/manifest.json')) const existing = manifest.scenarios.map(({ scenario }) => scenario) const generated = [] const add = (id, category, description, operation) => generated.push({ schemaVersion: 1, id, category, description, operation, tolerance: { linear: 1e-6, scalar: 1e-6 } }) for (let index = 1; index <= 20; index += 1) { add(`part-box-${String(index).padStart(3, '0')}`, 'Part', `Parametric box fixture ${index}`, { type: 'box', length: 2 + (index % 9), width: 3 + ((index * 2) % 11), height: 4 + ((index * 3) % 13), placement: { translation: [index % 5, (index * 2) % 7, index % 3] }, }) } for (let index = 1; index <= 20; index += 1) { add(`part-cylinder-${String(index).padStart(3, '0')}`, 'Part', `Parametric cylinder fixture ${index}`, { type: 'cylinder', radius: 1 + (index % 8) / 2, height: 5 + (index * 3) % 17, angle: 360, placement: { translation: [-(index % 4), (index * 2) % 5, index % 3] }, }) } for (let index = 1; index <= 15; index += 1) { add(`part-sphere-${String(index).padStart(3, '0')}`, 'Part', `Parametric sphere fixture ${index}`, { type: 'sphere', radius: 1 + (index % 9) / 3, placement: { translation: [index % 6, -(index % 5), (index * 2) % 4] }, }) } for (let index = 1; index <= 15; index += 1) { add(`part-cone-${String(index).padStart(3, '0')}`, 'Part', `Parametric cone fixture ${index}`, { type: 'cone', radius1: 2 + (index % 6), radius2: 0.5 + (index % 4) / 2, height: 4 + (index % 15), angle: 360, placement: { translation: [index % 4, index % 6, -(index % 2)] }, }) } for (let index = 1; index <= 25; index += 1) { const kind = index % 3 === 0 ? 'common' : index % 3 === 1 ? 'cut' : 'fuse' const size = 8 + (index % 5) const base = { type: 'box', length: size, width: size - 1, height: size + 1, placement: { translation: [0, 0, index % 2] } } const tool = kind === 'cut' ? { type: 'cylinder', radius: 1.5 + (index % 3), height: size + 1, angle: 360, placement: { translation: [size / 2, (size - 1) / 2, index % 2] } } : { type: 'box', length: 4 + (index % 4), width: 4 + ((index + 1) % 4), height: 4 + ((index + 2) % 4), placement: { translation: [2, 2, index % 2] } } add(`part-${kind}-${String(index).padStart(3, '0')}`, 'Part', `${kind} Boolean fixture ${index}`, { type: kind, base, tool }) } const expectedFromOracle = (scenario) => { const tempFile = resolve(workDir, `${scenario.id}.json`) return writeFile(tempFile, `${JSON.stringify(scenario, null, 2)}\n`).then(() => { const execution = spawnSync(command, [driver], { cwd: root, encoding: 'utf8', timeout: 120000, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, FREECAD_GOLDEN_SCENARIO: tempFile }, }) const output = `${execution.stdout || ''}\n${execution.stderr || ''}` const line = output.split(/\r?\n/).find((entry) => entry.startsWith('FREECAD_GOLDEN_RESULT=')) if (execution.error || execution.status !== 0 || !line) throw new Error(`${scenario.id} oracle failure: ${execution.error?.message || output.trim()}`) const actual = JSON.parse(line.slice('FREECAD_GOLDEN_RESULT='.length)) if (!['Solid', 'Compound'].includes(actual.shapeType) || actual.isNull !== false || actual.isValid !== true || actual.solids < 1) throw new Error(`${scenario.id} produced an invalid success shape.`) return { ...scenario, expected: { shapeType: actual.shapeType, isNull: actual.isNull, isValid: actual.isValid, solids: actual.solids, faces: actual.faces, edges: actual.edges, vertices: actual.vertices, volume: actual.volume, area: actual.area, boundingBox: actual.boundingBox, }, } }) } const allSuccess = [...existing] for (const scenario of generated) { const completed = await expectedFromOracle(scenario) allSuccess.push(completed) await writeFile(resolve(scenarioDir, `${completed.id}.json`), `${JSON.stringify(completed, null, 2)}\n`) } const successEntries = allSuccess.map((scenario) => ({ id: scenario.id, file: `scenarios/${scenario.id}.json` })) await writeFile(resolve(root, 'fixtures/freecad-golden/manifest.json'), `${JSON.stringify({ schemaVersion: 1, baselineId: 'freecad-1.1.1', driver: 'scripts/freecad-golden-driver.py', scenarios: successEntries }, null, 2)}\n`) const failures = [] const addFailure = (id, description, operation, expectedError) => failures.push({ schemaVersion: 1, id, category: 'Part', description, operation, expectedError }) for (let index = 1; index <= 12; index += 1) addFailure(`invalid-box-${String(index).padStart(3, '0')}`, 'Box dimension rejection', { type: 'box', length: index % 2 ? 0 : -index, width: 2 + index, height: 3 }, 'positive') for (let index = 1; index <= 10; index += 1) addFailure(`invalid-cylinder-${String(index).padStart(3, '0')}`, 'Cylinder parameter rejection', { type: 'cylinder', radius: index % 2 ? 0 : -index, height: index % 3 ? 5 : 0, angle: index % 4 ? 360 : 361 }, 'parameters') for (let index = 1; index <= 8; index += 1) addFailure(`invalid-sphere-${String(index).padStart(3, '0')}`, 'Sphere radius rejection', { type: 'sphere', radius: index % 2 ? 0 : -index }, 'positive') for (let index = 1; index <= 8; index += 1) addFailure(`invalid-cone-${String(index).padStart(3, '0')}`, 'Cone parameter rejection', { type: 'cone', radius1: index % 2 ? -1 : 0, radius2: index % 2 ? 0 : -1, height: index % 3 ? 5 : 0, angle: index % 4 ? 360 : 361 }, 'parameters') for (let index = 1; index <= 6; index += 1) addFailure(`invalid-boolean-${String(index).padStart(3, '0')}`, 'Boolean operand rejection', { type: index % 2 ? 'cut' : 'fuse', base: { type: 'box', length: 1, width: 1, height: 1 }, ...(index % 3 ? { tool: { type: 'unknown' } } : {}) }, index % 3 ? 'Unsupported' : 'operation') addFailure('invalid-common-001', 'Common operand rejection', { type: 'common', base: { type: 'box', length: 1, width: 1, height: 1 } }, 'operation') for (let index = 1; index <= 6; index += 1) addFailure(`invalid-operation-${String(index).padStart(3, '0')}`, 'Unknown operation rejection', { type: `unsupported-${index}` }, 'Unsupported') for (const failure of failures) await writeFile(resolve(failureDir, `${failure.id}.json`), `${JSON.stringify(failure, null, 2)}\n`) await writeFile(resolve(failureDir, 'manifest.json'), `${JSON.stringify({ schemaVersion: 1, baselineId: 'freecad-1.1.1', driver: 'scripts/freecad-golden-driver.py', failures: failures.map((failure) => ({ id: failure.id, file: `${failure.id}.json` })) }, null, 2)}\n`) console.log(JSON.stringify({ status: 'freecad-golden-fixtures-generated', successCount: allSuccess.length, generatedSuccessCount: generated.length, failureCount: failures.length }, null, 2))