import { pathToFileURL } from 'node:url' import { createHash } from 'node:crypto' import { mkdir, readFile, stat, writeFile } from 'node:fs/promises' import { dirname } from 'node:path' import { resolve } from 'node:path' import { captureFreeCadPrivateNamingEvidence, createFreeCadPrivateNamingAbiRequest, probeFreeCadPrivateNamingAbi, type NativeFreeCadNamingAbiModule, } from '../../src/facade/nativeNamingAbi' type CandidateModule = NativeFreeCadNamingAbiModule & { makeBox(x: number, y: number, z: number): unknown shapeToStep(shape: unknown): string booleanHistoryFromStep(objectStep: string, toolStep: string, operation: 'cut'): { provider: 'occt-native' occtVersion: string hasModified: boolean hasGenerated: boolean hasDeleted: boolean resultStep: string records: Array<{ relation: 'modified' | 'generated' | 'deleted' source: string kind: 'face' | 'edge' | 'vertex' resultKind?: 'face' | 'edge' | 'vertex' sourceIndex: number resultIndex?: number resultIndexes?: number[] }> } } const dist = resolve(process.env.FREECAD_NAMING_WORKER_DIST ?? '.cache/candidates/freecad-naming-worker') const moduleUrl = pathToFileURL(resolve(dist, 'bitbybit-occt-history.js')).href const createCandidate = (await import(moduleUrl)).default as (options: { locateFile(path: string): string }) => Promise const candidate = await createCandidate({ locateFile: (path) => resolve(dist, path) }) const probe = probeFreeCadPrivateNamingAbi(candidate) if (probe.availability !== 'available') throw new Error(`Candidate Worker naming ABI probe failed: ${probe.reason}`) const object = candidate.makeBox(10, 10, 10) const tool = candidate.makeBox(5, 5, 5) const objectStep = candidate.shapeToStep(object) const toolStep = candidate.shapeToStep(tool) const history = candidate.booleanHistoryFromStep(objectStep, toolStep, 'cut') const record = history.records.find((entry) => entry.relation !== 'deleted' && Number.isSafeInteger(entry.sourceIndex) && (Number.isSafeInteger(entry.resultIndex) || entry.resultIndexes?.some(Number.isSafeInteger))) if (!record) throw new Error('Candidate Worker OCCT cut returned no usable native history record.') const resultIndex = Number.isSafeInteger(record.resultIndex) ? record.resultIndex! : record.resultIndexes!.find(Number.isSafeInteger)! const selectedRecord = { ...record, resultIndex, resultIndexes: undefined } const evidence = captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({ requestId: 'candidate-worker-cut-1', documentId: 'candidate-worker-document', documentVersion: 1, operationId: 'candidate-worker-cut', operation: 'cut', stageId: 'candidate-worker:stage:1', resultObjectId: 'candidate-worker:result:1', resultObjectTag: 100, inputs: [ { inputId: 'object', objectId: 'candidate-object', role: 'object', objectTag: 42, step: objectStep }, { inputId: 'tool', objectId: 'candidate-tool', role: 'tool', objectTag: 43, step: toolStep }, ], stages: [{ stageId: 'candidate-worker:stage:1', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }], resultStep: history.resultStep, history: { ...history, records: [selectedRecord] }, }), probe) if (!evidence?.elementMap2 || !evidence.stringHasher || evidence.mappedNames?.length !== 1) throw new Error('Candidate Worker returned incomplete FreeCAD naming evidence for OCCT cut history.') const secondEvidence = captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({ requestId: 'candidate-worker-cut-2', documentId: 'candidate-worker-document', documentVersion: 2, operationId: 'candidate-worker-cut-2', operation: 'cut', stageId: 'candidate-worker:stage:2', resultObjectId: 'candidate-worker:result:2', resultObjectTag: 101, inputs: [{ inputId: 'object', objectId: 'candidate-worker:result:1', role: 'object', stageId: 'candidate-worker:stage:1', objectTag: 100, step: history.resultStep, namingEvidence: evidence }], stages: [{ stageId: 'candidate-worker:stage:2', operation: 'cut', inputIds: ['object'], ordinal: 0 }], resultStep: history.resultStep, history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind ?? record.kind, sourceIndex: resultIndex, resultIndex: resultIndex + 1 }] }, }), probe) const thirdEvidence = secondEvidence && captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({ requestId: 'candidate-worker-cut-3', documentId: 'candidate-worker-document', documentVersion: 3, operationId: 'candidate-worker-cut-3', operation: 'cut', stageId: 'candidate-worker:stage:3', resultObjectId: 'candidate-worker:result:3', resultObjectTag: 102, inputs: [{ inputId: 'object', objectId: 'candidate-worker:result:2', role: 'object', stageId: 'candidate-worker:stage:2', objectTag: 101, step: history.resultStep, namingEvidence: secondEvidence }], stages: [{ stageId: 'candidate-worker:stage:3', operation: 'cut', inputIds: ['object'], ordinal: 0 }], resultStep: history.resultStep, history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind ?? record.kind, sourceIndex: resultIndex + 1, resultIndex: resultIndex + 2 }] }, }), probe) if (!secondEvidence?.stringHasher || !thirdEvidence?.stringHasher || secondEvidence.stringHasher.entries.length < 1 || thirdEvidence.stringHasher.entries.length <= secondEvidence.stringHasher.entries.length) throw new Error('Candidate Worker did not restore non-empty StringHasher evidence across three stages.') let rejected = false try { captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({ requestId: 'candidate-worker-invalid', documentId: 'candidate-worker-document', documentVersion: 2, operationId: 'candidate-worker-invalid', operation: 'cut', stageId: 'candidate-worker:invalid', resultObjectId: 'candidate-worker:invalid-result', resultObjectTag: 101, inputs: [{ inputId: 'object', objectId: 'candidate-object', role: 'object', objectTag: 42, step: objectStep }], stages: [{ stageId: 'candidate-worker:invalid', operation: 'cut', inputIds: ['object'], ordinal: 0 }], history: { ...history, records: [] }, }), probe) } catch (error) { if (!(error instanceof Error) || !error.message.includes('requires inputs and native history records')) throw error rejected = true } if (!rejected) throw new Error('Candidate Worker accepted incomplete native history.') const result = { status: 'freecad-naming-candidate-worker-pass', occtVersion: history.occtVersion, callbacks: ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'], namingEvidence: { mappedNames: evidence.mappedNames.length, stringHasherEntries: evidence.stringHasher.entries.length, elementMaps: evidence.elementMap2.maps.length, }, chainedStage: { mappedNames: secondEvidence.mappedNames.length, stringHasherEntries: secondEvidence.stringHasher.entries.length }, thirdStage: { mappedNames: thirdEvidence.mappedNames.length, stringHasherEntries: thirdEvidence.stringHasher.entries.length }, invalidHistoryRejected: true, candidateOnly: true, productionPublication: false, productionWorkerLinked: false, } const reportPath = process.env.FREECAD_NAMING_WORKER_REPORT if (reportPath) { const artifacts = await Promise.all(['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data'].map(async (name) => { const path = resolve(dist, name) const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)]) return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') } })) const absoluteReportPath = resolve(reportPath) await mkdir(dirname(absoluteReportPath), { recursive: true }) await writeFile(absoluteReportPath, `${JSON.stringify({ schemaVersion: 1, ...result, artifacts }, null, 2)}\n`) } console.log(JSON.stringify(result, null, 2))