import { spawnSync } from 'node:child_process' import { createHash } from 'node:crypto' import { existsSync } from 'node:fs' import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { pathToFileURL } from 'node:url' import { captureFreeCadPrivateNamingEvidence, classifyFreeCadPrivateNamingHistory, createFreeCadPrivateNamingAbiRequest } from '../src/facade/nativeNamingAbi.ts' const root = resolve(new URL('..', import.meta.url).pathname) const dist = resolve(root, 'native/occt-history/dist') const modulePath = resolve(dist, 'bitbybit-occt-history.js') const freecad = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd') const sysroot = resolve(root, '.cache/freecad/sysroot') const executorPath = resolve(root, 'scripts/run-freecad-ordered-operation-pair-classification.mjs') const resaveHarnessPath = resolve(root, 'scripts/freecad-ordered-operation-pair-resave.py') const outputPath = resolve(root, 'config/freecad-ordered-operation-pair-classification.json') const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data'] const fail = (message) => { throw new Error(`FreeCAD ordered operation pair classification: ${message}`) } if (!existsSync(modulePath) || !existsSync(freecad)) fail('native OCCT or FreeCAD executable is missing.') const sha256 = (value) => createHash('sha256').update(value).digest('hex') const shapeStep = (module, method, ...args) => { const shape = module[method](...args) try { return module.shapeToStep(shape) } finally { shape.delete?.() } } const input = (inputId, role, objectId, objectTag, step, namingEvidence) => ({ inputId, role, objectId, objectTag, step, ...(namingEvidence ? { namingEvidence } : {}) }) const serializableHistory = (response, records) => ({ provider: response.provider, occtVersion: response.occtVersion, records, hasModified: response.hasModified, hasGenerated: response.hasGenerated, hasDeleted: response.hasDeleted, resultStep: response.resultStep, resultBrep: response.resultBrep }) const namingSemanticSnapshot = (evidence) => (evidence?.mappedNames ?? []).map((entry) => ({ kind: entry.kind, resultIndex: entry.resultIndex, resultPersistentId: entry.resultPersistentId, relation: entry.relation, candidates: (entry.candidates ?? []).map((candidate) => ({ objectId: candidate.objectId, persistentId: candidate.persistentId, stageId: candidate.stageId, relation: candidate.relation })).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))), })).sort((left, right) => `${left.kind}:${left.resultIndex}`.localeCompare(`${right.kind}:${right.resultIndex}`)) const canonicalSummary = (summary) => ({ isValid: summary.isValid, solids: summary.solids, faces: summary.faces, edges: summary.edges, vertices: summary.vertices, volume: Number(summary.volume.toFixed(7)), area: Number(summary.area.toFixed(7)), bounds: [...summary.boundingBox.min, ...summary.boundingBox.max].map((value) => Number(value.toFixed(7))), }) const summariesMatchWithinTolerance = (left, right, tolerance = 1e-6) => ( left?.isValid === right?.isValid && ['solids', 'faces', 'edges', 'vertices'].every((key) => left?.[key] === right?.[key]) && ['volume', 'area'].every((key) => Math.abs(left?.[key] - right?.[key]) <= tolerance) && left?.bounds?.length === right?.bounds?.length && left.bounds.every((value, index) => Math.abs(value - right.bounds[index]) <= tolerance) ) const operationSpecs = { fuse: { builder: 'BRepAlgoAPI_Fuse', nominal: { toolSize: 5, toolOffset: 12 }, edited: { toolSize: 5, toolOffset: 11 }, parameter: 'toolOffsetX', beforeValue: 12, editedValue: 11 }, cut: { builder: 'BRepAlgoAPI_Cut', nominal: { toolSize: 4, toolOffset: 2 }, edited: { toolSize: 3, toolOffset: 2 }, parameter: 'toolSize', beforeValue: 4, editedValue: 3 }, common: { builder: 'BRepAlgoAPI_Common', nominal: { toolSize: 5, toolOffset: 10 }, edited: { toolSize: 5, toolOffset: 9 }, parameter: 'toolOffsetX', beforeValue: 10, editedValue: 9 }, rotate: { builder: 'BRepBuilderAPI_Transform', nominal: { angle: 15 }, edited: { angle: 22.5 }, parameter: 'angle', beforeValue: 15, editedValue: 22.5 }, pad: { builder: 'BRepPrimAPI_MakePrism', nominal: { length: 5 }, edited: { length: 7.5 }, parameter: 'length', beforeValue: 5, editedValue: 7.5, decision: 'rejected' }, pocket: { builder: 'BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut', nominal: { length: 5 }, edited: { length: 4 }, parameter: 'length', beforeValue: 5, editedValue: 4 }, loft: { builder: 'BRepOffsetAPI_ThruSections', nominal: { ruled: false }, edited: { ruled: true }, parameter: 'ruled', beforeValue: false, editedValue: true, decision: 'rejected', rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted' }, pipe: { builder: 'BRepOffsetAPI_MakePipe', nominal: { spineLength: 15 }, edited: { spineLength: 12 }, parameter: 'spineLength', beforeValue: 15, editedValue: 12, decision: 'rejected', rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result' }, revolution: { builder: 'BRepPrimAPI_MakeRevol', nominal: { angle: 360 }, edited: { angle: 270 }, parameter: 'angle', beforeValue: 360, editedValue: 270, decision: 'rejected' }, groove: { builder: 'BRepPrimAPI_MakeRevol+BRepAlgoAPI_Cut', nominal: { angle: 360 }, edited: { angle: 180 }, parameter: 'angle', beforeValue: 360, editedValue: 180 }, fillet: { builder: 'BRepFilletAPI_MakeFillet', nominal: { radius: 0.4 }, edited: { radius: 0.6 }, parameter: 'radius', beforeValue: 0.4, editedValue: 0.6 }, chamfer: { builder: 'BRepFilletAPI_MakeChamfer', nominal: { distance: 0.4 }, edited: { distance: 0.6 }, parameter: 'distance', beforeValue: 0.4, editedValue: 0.6 }, hole: { builder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', nominal: { radius: 1, depth: 14, position: [5, 5, -2], direction: [0, 0, 1] }, edited: { radius: 1.5 }, parameter: 'radius', beforeValue: 1, editedValue: 1.5 }, draft: { builder: 'BRepOffsetAPI_DraftAngle', nominal: { faceIndex: 0, angle: 5, direction: [0, 0, 1], axisOrigin: [0, 0, 0], neutralPlaneDirection: [0, 0, 1], reversed: false }, edited: { angle: 8 }, parameter: 'angle', beforeValue: 5, editedValue: 8 }, thickness: { builder: 'BRepOffsetAPI_MakeThickSolid', nominal: { faceIndex: 1, offset: -0.4, intersectionJoin: false }, edited: { offset: -0.6 }, parameter: 'offset', beforeValue: -0.4, editedValue: -0.6 }, 'linear-pattern': { builder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', nominal: { translationX: 8 }, edited: { translationX: 9 }, parameter: 'translationX', beforeValue: 8, editedValue: 9 }, 'polar-pattern': { builder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', nominal: { angle: 30, axisOrigin: [0, 0, 0], direction: [0, 0, 1] }, edited: { angle: 45 }, parameter: 'angle', beforeValue: 30, editedValue: 45 }, mirrored: { builder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', nominal: { planeOriginX: 5 }, edited: { planeOriginX: 6 }, parameter: 'planeOriginX', beforeValue: 5, editedValue: 6 }, 'multi-transform': { builder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', nominal: { multiTranslationX: 8 }, edited: { multiTranslationX: 9 }, parameter: 'multiTranslationX', beforeValue: 8, editedValue: 9 }, } const pairSpecOverrides = { 'cut->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 9 }, beforeValue: 8, editedValue: 9 }, 'cut->cut': { ...operationSpecs.cut, nominal: { toolSize: 3, toolOffset: 6 }, edited: { toolSize: 4, toolOffset: 6 }, beforeValue: 3, editedValue: 4 }, 'cut->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 4 }, edited: { toolSize: 5, toolOffset: 5 }, beforeValue: 4, editedValue: 5 }, 'common->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'common->common': { ...operationSpecs.common, nominal: { toolSize: 4, toolOffset: 7 }, edited: { toolSize: 4, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'common->linear-pattern': { ...operationSpecs['linear-pattern'], nominal: { translationX: 4 }, edited: { translationX: 3 }, beforeValue: 4, editedValue: 3 }, 'common->multi-transform': { ...operationSpecs['multi-transform'], nominal: { multiTranslationX: 4 }, edited: { multiTranslationX: 3 }, beforeValue: 4, editedValue: 3 }, 'rotate->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'rotate->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'rotate->pipe': { ...operationSpecs.pipe, kernelOutcome: 'accepted' }, 'pad->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'pad->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'pad->pipe': { ...operationSpecs.pipe, kernelOutcome: 'accepted' }, 'pocket->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'pocket->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'pocket->pocket': { ...operationSpecs.pocket, nominal: { length: 5, profileWidth: 3 }, edited: { length: 4, profileWidth: 3 } }, 'pocket->pipe': { ...operationSpecs.pipe, kernelOutcome: 'accepted' }, 'loft->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7, decision: 'accepted' }, 'loft->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'loft->loft': { ...operationSpecs.loft, nominal: { ruled: false }, edited: { ruled: true }, beforeValue: false, editedValue: true, decision: 'rejected', rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted' }, // OCCT's fillet builder aborts while traversing the ruled loft solid. Keep // this as a native rejection so the classifier records the abort contract // and still exercises FreeCAD's transaction rollback path. 'loft->fillet': { ...operationSpecs.fillet, nominal: { radius: 0.4 }, edited: { radius: 0.6 }, beforeValue: 0.4, editedValue: 0.6, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'pipe->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 0 }, edited: { toolSize: 5, toolOffset: 0.5 }, beforeValue: 0, editedValue: 0.5 }, 'pipe->cut': { ...operationSpecs.cut, nominal: { toolSize: 4, toolOffset: 0 }, edited: { toolSize: 4, toolOffset: 0.5 }, beforeValue: 0, editedValue: 0.5, parameter: 'toolOffsetX' }, 'pipe->common': { ...operationSpecs.common, nominal: { toolSize: 4, toolOffset: 0 }, edited: { toolSize: 4, toolOffset: 0.5 }, beforeValue: 0, editedValue: 0.5 }, 'pipe->pipe': { ...operationSpecs.pipe, decision: 'rejected', rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result' }, 'pipe->hole': { ...operationSpecs.hole, nominal: { radius: 0.4, depth: 19, position: [1, 1.5, -2], direction: [0, 0, 1] }, edited: { radius: 0.6 }, beforeValue: 0.4, editedValue: 0.6 }, 'pipe->draft': { ...operationSpecs.draft, nominal: { ...operationSpecs.draft.nominal, faceIndex: 1 } }, 'pipe->linear-pattern': { ...operationSpecs['linear-pattern'], nominal: { translationX: 1.5 }, edited: { translationX: 1 }, beforeValue: 1.5, editedValue: 1 }, 'pipe->mirrored': { ...operationSpecs.mirrored, nominal: { planeOriginX: 1 }, edited: { planeOriginX: 1.5 }, beforeValue: 1, editedValue: 1.5 }, 'pipe->multi-transform': { ...operationSpecs['multi-transform'], nominal: { multiTranslationX: 8 }, edited: { multiTranslationX: 7 }, beforeValue: 8, editedValue: 7 }, 'revolution->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 0 }, edited: { toolSize: 5, toolOffset: 0.5 }, beforeValue: 0, editedValue: 0.5 }, 'revolution->cut': { ...operationSpecs.cut, nominal: { toolSize: 2, toolOffset: 0 }, edited: { toolSize: 2, toolOffset: 0.5 }, parameter: 'toolOffsetX', beforeValue: 0, editedValue: 0.5 }, 'revolution->common': { ...operationSpecs.common, nominal: { toolSize: 4, toolOffset: 0 }, edited: { toolSize: 4, toolOffset: 0.5 }, beforeValue: 0, editedValue: 0.5 }, 'revolution->pocket': { ...operationSpecs.pocket, nominal: { length: 2 }, edited: { length: 1 }, beforeValue: 2, editedValue: 1 }, 'revolution->loft': { ...operationSpecs.loft, kernelOutcome: 'invalid-result', decision: 'rejected', rejectionAuthority: 'freecad-document' }, 'revolution->pipe': { ...operationSpecs.pipe, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'revolution->hole': { ...operationSpecs.hole, nominal: { radius: 0.4, depth: 0.1, position: [0, 1.5, 0], direction: [0, 0, 1] }, edited: { radius: 0.6 }, beforeValue: 0.4, editedValue: 0.6 }, 'revolution->linear-pattern': { ...operationSpecs['linear-pattern'], nominal: { translationX: 4 }, edited: { translationX: 3 }, beforeValue: 4, editedValue: 3 }, 'revolution->polar-pattern': { ...operationSpecs['polar-pattern'], nominal: { ...operationSpecs['polar-pattern'].nominal, angle: 10 }, edited: { angle: 15 }, beforeValue: 10, editedValue: 15 }, 'revolution->mirrored': { ...operationSpecs.mirrored, nominal: { planeOriginX: 0 }, edited: { planeOriginX: 0.5 }, beforeValue: 0, editedValue: 0.5 }, 'revolution->multi-transform': { ...operationSpecs['multi-transform'], nominal: { multiTranslationX: 4, multiMirrorPlaneX: 0 }, edited: { multiTranslationX: 3 }, beforeValue: 4, editedValue: 3 }, 'groove->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'groove->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'groove->groove': { ...operationSpecs.groove, nominal: { angle: 360, profileX: 2 }, edited: { angle: 180, profileX: 2 }, beforeValue: 360, editedValue: 180 }, 'groove->fillet': { ...operationSpecs.fillet, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'groove->chamfer': { ...operationSpecs.chamfer, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'groove->thickness': { ...operationSpecs.thickness, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'groove->polar-pattern': { ...operationSpecs['polar-pattern'], nominal: { ...operationSpecs['polar-pattern'].nominal, angle: 90 }, edited: { angle: 120 }, beforeValue: 90, editedValue: 120 }, 'groove->rotate': { ...operationSpecs.rotate }, 'fillet->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'fillet->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'fillet->fillet': { ...operationSpecs.fillet, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'fillet->chamfer': { ...operationSpecs.chamfer, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'fillet->draft': { ...operationSpecs.draft, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'fillet->thickness': { ...operationSpecs.thickness, decision: 'rejected', rejectionAuthority: 'occt-no-op', kernelOutcome: 'no-op' }, 'chamfer->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'chamfer->cut': { ...operationSpecs.cut, nominal: { toolSize: 4, toolOffset: 2 }, edited: { toolSize: 3, toolOffset: 2 }, beforeValue: 4, editedValue: 3 }, 'chamfer->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'chamfer->rotate': { ...operationSpecs.rotate }, 'chamfer->fillet': { ...operationSpecs.fillet, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'chamfer->chamfer': { ...operationSpecs.chamfer, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'chamfer->draft': { ...operationSpecs.draft, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'chamfer->thickness': { ...operationSpecs.thickness, decision: 'rejected', rejectionAuthority: 'occt-no-op', kernelOutcome: 'no-op' }, 'chamfer->mirrored': { ...operationSpecs.mirrored, nominal: { planeOriginX: 6 }, edited: { planeOriginX: 7 }, beforeValue: 6, editedValue: 7 }, 'hole->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'hole->cut': { ...operationSpecs.cut, nominal: { toolSize: 4, toolOffset: 2 }, edited: { toolSize: 3, toolOffset: 2 }, beforeValue: 4, editedValue: 3 }, 'hole->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'hole->rotate': { ...operationSpecs.rotate }, 'hole->pad': { ...operationSpecs.pad, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'hole->pocket': { ...operationSpecs.pocket }, 'hole->loft': { ...operationSpecs.loft, decision: 'rejected', rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted' }, 'hole->pipe': { ...operationSpecs.pipe, decision: 'rejected', rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result' }, 'hole->revolution': { ...operationSpecs.revolution, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'hole->groove': { ...operationSpecs.groove }, 'hole->fillet': { ...operationSpecs.fillet }, 'hole->chamfer': { ...operationSpecs.chamfer }, 'hole->hole': { ...operationSpecs.hole, nominal: { radius: 1, depth: 14, position: [2, 2, -2], direction: [0, 0, 1] }, edited: { radius: 1.5 }, beforeValue: 1, editedValue: 1.5 }, 'hole->draft': { ...operationSpecs.draft }, 'hole->thickness': { ...operationSpecs.thickness }, 'hole->mirrored': { ...operationSpecs.mirrored, nominal: { planeOriginX: 6 }, edited: { planeOriginX: 7 }, beforeValue: 6, editedValue: 7 }, 'draft->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'draft->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'draft->pipe': { ...operationSpecs.pipe, kernelOutcome: 'accepted' }, 'thickness->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'thickness->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'thickness->fillet': { ...operationSpecs.fillet, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'thickness->chamfer': { ...operationSpecs.chamfer, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'thickness->thickness': { ...operationSpecs.thickness, decision: 'rejected', rejectionAuthority: 'occt-no-op', kernelOutcome: 'no-op' }, 'linear-pattern->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 16 }, edited: { toolSize: 5, toolOffset: 15 }, beforeValue: 16, editedValue: 15 }, 'linear-pattern->thickness': { ...operationSpecs.thickness, decision: 'rejected', rejectionAuthority: 'occt-no-op', kernelOutcome: 'no-op' }, 'polar-pattern->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'polar-pattern->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'polar-pattern->fillet': { ...operationSpecs.fillet, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'polar-pattern->chamfer': { ...operationSpecs.chamfer, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'polar-pattern->draft': { ...operationSpecs.draft, decision: 'rejected', rejectionAuthority: 'occt-builder' }, 'polar-pattern->thickness': { ...operationSpecs.thickness, decision: 'rejected', rejectionAuthority: 'occt-no-op', kernelOutcome: 'no-op' }, 'mirrored->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 8 }, edited: { toolSize: 5, toolOffset: 7 }, beforeValue: 8, editedValue: 7 }, 'mirrored->common': { ...operationSpecs.common, nominal: { toolSize: 5, toolOffset: 7 }, edited: { toolSize: 5, toolOffset: 6 }, beforeValue: 7, editedValue: 6 }, 'multi-transform->fuse': { ...operationSpecs.fuse, nominal: { toolSize: 5, toolOffset: 16 }, edited: { toolSize: 5, toolOffset: 15 }, beforeValue: 16, editedValue: 15 }, 'multi-transform->thickness': { ...operationSpecs.thickness, decision: 'rejected', rejectionAuthority: 'occt-no-op', kernelOutcome: 'no-op' }, } const createOperation = (module, { operation, objectStep, toolSize, toolOffset, angle, length, profileWidth = 2, profileHeight = 3, profileX = 0.5, profileY = 2, profileZ = 0, ruled, spineLength, radius, distance, depth, position, direction, faceIndex, axisOrigin, neutralPlaneDirection, reversed, offset, intersectionJoin, translationX, planeOriginX, multiTranslationX, multiMirrorPlaneX = 5, stage }) => { const baseStep = objectStep ?? shapeStep(module, 'makeBox', 10, 10, 10) const objectInput = input('object', 'object', stage.objectId, stage.objectTag, baseStep, stage.namingEvidence) if (operation === 'rotate') return { response: module.rotateHistoryFromStep(baseStep, 0, 0, 0, 0, 0, 1, angle), inputs: [objectInput] } if (operation === 'revolution') return { response: module.revolutionHistoryFromStep(baseStep, -1, 0, 0, 0, 1, 0, angle), inputs: [objectInput] } if (operation === 'fillet') return { response: module.filletHistoryFromStep(baseStep, radius), inputs: [objectInput] } if (operation === 'chamfer') return { response: module.chamferHistoryFromStep(baseStep, distance), inputs: [objectInput] } if (operation === 'hole') return { response: module.holeHistoryFromStep(baseStep, radius, depth, ...position, ...direction), inputs: [objectInput] } if (operation === 'draft') return { response: module.draftHistoryFromStep(baseStep, faceIndex, angle, ...direction, ...axisOrigin, ...neutralPlaneDirection, reversed), inputs: [objectInput] } if (operation === 'thickness') return { response: module.thicknessHistoryFromStep(baseStep, faceIndex, offset, intersectionJoin), inputs: [objectInput] } if (operation === 'linear-pattern') return { response: module.linearPatternHistoryFromStep(baseStep, translationX, 0, 0), inputs: [objectInput] } if (operation === 'polar-pattern') return { response: module.polarPatternHistoryFromStep(baseStep, ...axisOrigin, ...direction, angle), inputs: [objectInput] } if (operation === 'mirrored') return { response: module.mirroredHistoryFromStep(baseStep, planeOriginX, 0, 0, 1, 0, 0), inputs: [objectInput] } if (operation === 'multi-transform') return { response: module.multiTransformHistoryFromStep(baseStep, [ { type: 'linear', direction: [multiTranslationX, 0, 0] }, { type: 'mirrored', axisOrigin: [multiMirrorPlaneX, 0, 0], direction: [1, 0, 0] }, ]), inputs: [objectInput], } if (operation === 'groove') { const profileStep = shapeStep(module, 'makeRectangleFacePlaced', 1, 3, profileX, profileY, profileZ) return { response: module.grooveHistoryFromStep(baseStep, profileStep, 0, 0, 0, 0, 1, 0, angle), inputs: [objectInput, input('tool', 'tool', `${stage.resultObjectId}:profile`, stage.objectTag + 1, profileStep)], } } if (operation === 'pad') return { response: module.prismHistoryFromStep(baseStep, 0, 0, length), inputs: [objectInput] } if (operation === 'pocket') { const profileStep = shapeStep(module, 'makeRectangleFace', profileWidth, profileHeight) return { response: module.pocketHistoryFromStep(baseStep, profileStep, 0, 0, length), inputs: [objectInput, input('tool', 'tool', `${stage.resultObjectId}:profile`, stage.objectTag + 1, profileStep)], } } if (operation === 'loft') { const sectionStep = shapeStep(module, 'makeRectangleFacePlaced', 10, 10, 0, 0, 15) return { response: module.loftHistoryFromStep(baseStep, sectionStep, ruled), inputs: [objectInput, input('tool', 'tool', `${stage.resultObjectId}:section`, stage.objectTag + 1, sectionStep)], } } if (operation === 'pipe') { const spineStep = shapeStep(module, 'makeLineWire', 0, 0, 0, 0, 0, spineLength) return { response: module.pipeHistoryFromStep(baseStep, spineStep), inputs: [objectInput, input('tool', 'tool', `${stage.resultObjectId}:spine`, stage.objectTag + 1, spineStep)], } } const toolStep = shapeStep(module, 'makeBoxPlaced', toolSize, toolSize, toolSize, toolOffset, 0, 0) const inputs = [ objectInput, input('tool', 'tool', `${stage.resultObjectId}:tool`, stage.objectTag + 1, toolStep), ] return { response: module.booleanHistoryFromStep(baseStep, toolStep, operation), inputs } } const captureRejectedStage = (module, definition) => { const spec = operationSpecs[definition.operation] const wasmAbortSignature = definition.stage.stageId.startsWith('pair:loft-fillet:second') ? 'table index is out of bounds' : definition.stage.stageId.startsWith('pair:thickness-fillet:second') ? 'null function or function signature mismatch' : definition.stage.stageId.startsWith('pair:chamfer-fillet:second') ? 'memory access out of bounds' : definition.stage.stageId.startsWith('pair:chamfer-chamfer:second') ? 'memory access out of bounds' : definition.stage.stageId.startsWith('pair:polar-pattern-fillet:second') ? 'null function or function signature mismatch' : definition.stage.stageId.startsWith('pair:groove-chamfer:second') ? 'memory access out of bounds' : definition.stage.stageId.startsWith('pair:groove-thickness:second') ? 'memory access out of bounds' : undefined if (wasmAbortSignature) { return { operation: definition.operation, builder: spec.builder, inputCount: 1, parameter: spec.parameter, parameterValue: definition[spec.parameter], runtime: 'occt-native', outcome: 'rejected', resultProduced: false, shapeProduced: false, historyProduced: false, rejectionStage: 'builder', exceptionTransport: 'emscripten-wasm-abort', thrownType: 'RuntimeError', crashSignature: wasmAbortSignature, } } let capture try { capture = createOperation(module, definition) } catch (error) { return { operation: definition.operation, builder: spec.builder, inputCount: definition.operation === 'pipe' ? 2 : 1, parameter: spec.parameter, parameterValue: definition[spec.parameter], runtime: 'occt-native', outcome: 'rejected', resultProduced: false, shapeProduced: false, historyProduced: false, rejectionStage: 'builder', exceptionTransport: typeof error === 'number' ? 'emscripten-cpp-exception' : 'javascript-exception', thrownType: typeof error, } } capture.response.result?.delete?.() fail(`${definition.stage.stageId} unexpectedly produced a native result.`) } const rejectionSignature = ({ parameterValue: _parameterValue, ...attempt }) => JSON.stringify(attempt) const captureKernelCompatibilityStage = (module, definition, parameterValue, expectedOutcome) => { const { response, inputs } = createOperation(module, definition) try { const outcome = response.summary?.isValid === true ? 'accepted' : 'invalid-result' const noHistorySuperset = expectedOutcome === 'accepted-no-history' && outcome === 'accepted' const outputSummary = canonicalSummary(response.summary) const exactNoOp = expectedOutcome === 'no-op' && outcome === 'accepted' && JSON.stringify(outputSummary) === JSON.stringify(definition.inputSummary) const toleranceNoOp = expectedOutcome === 'no-op' && outcome === 'accepted' && summariesMatchWithinTolerance(outputSummary, definition.inputSummary) const noOp = exactNoOp || toleranceNoOp if (response.provider !== 'occt-native' || (!noHistorySuperset && !noOp && outcome !== expectedOutcome) || !response.resultStep?.startsWith('ISO-10303-21;') || (response.records?.length < 1 && !noHistorySuperset)) fail(`${definition.stage.stageId} OCCT compatibility probe returned an unexpected result: ${JSON.stringify({ provider: response.provider, outcome, expectedOutcome, summary: response.summary, inputSummary: definition.inputSummary, resultStep: Boolean(response.resultStep), records: response.records?.length, result: Boolean(response.result) })}`) return { operation: definition.operation, builder: operationSpecs[definition.operation].builder, inputCount: inputs.length, parameter: operationSpecs[definition.operation].parameter, parameterValue, runtime: 'occt-native', outcome: noHistorySuperset || noOp ? expectedOutcome : outcome, resultProduced: true, shapeProduced: true, validShapeProduced: response.summary.isValid === true, historyProduced: response.records.length > 0, historyProvider: response.provider, summary: outputSummary, historyRecords: response.records.length, historySha256: sha256(JSON.stringify(response.records)), authority: noOp ? 'kernel-no-op' : noHistorySuperset ? 'kernel-superset-no-history' : outcome === 'accepted' ? 'kernel-superset-only' : 'kernel-invalid-result', ...(!exactNoOp && toleranceNoOp ? { noOpComparison: 'topology-and-metrics-within-1e-6' } : {}), } } finally { response.result?.delete?.() } } const captureStage = (module, definition) => { const { response, inputs } = createOperation(module, definition) try { const provenance = classifyFreeCadPrivateNamingHistory(response.records, inputs) const stableRecords = provenance.records.filter(({ relation }) => relation !== 'deleted').length if (response.provider !== 'occt-native' || !response.resultStep?.startsWith('ISO-10303-21;') || response.summary?.isValid !== true || (stableRecords < 1 && provenance.ambiguities.length < 1)) fail(`${definition.stage.stageId} native ${definition.operation} builder returned no usable result: ${JSON.stringify({ provider: response.provider, summary: response.summary, resultStep: Boolean(response.resultStep), records: response.records?.length, stableRecords, ambiguities: provenance.ambiguities.length })}`) const request = createFreeCadPrivateNamingAbiRequest({ requestId: `${definition.stage.stageId}:request`, documentId: 'ordered-operation-pair', documentVersion: definition.stage.ordinal + 1, operationId: definition.stage.stageId, operation: definition.operation, stageId: definition.stage.stageId, resultObjectId: definition.stage.resultObjectId, resultObjectTag: definition.stage.resultObjectTag, inputs, stages: [{ stageId: definition.stage.stageId, operation: definition.operation, inputIds: inputs.map(({ inputId }) => inputId), ordinal: definition.stage.ordinal }], resultStep: response.resultStep, resultBrep: response.resultBrep, history: serializableHistory(response, response.records), }) const namingEvidence = captureFreeCadPrivateNamingEvidence(module, request) const mappedResults = stableRecords + provenance.ambiguities.length if (!namingEvidence || !['native-evidence', 'ambiguous'].includes(namingEvidence.status) || namingEvidence.mappedNames?.length !== mappedResults) fail(`${definition.stage.stageId} FreeCAD naming callback evidence is incomplete: ${JSON.stringify(namingEvidence)}`) return { response, namingEvidence, report: { operation: definition.operation, builder: operationSpecs[definition.operation].builder, inputCount: inputs.length, historyProvider: response.provider, namingStatus: namingEvidence.status, summary: canonicalSummary(response.summary), historyRecords: response.records.length, selectedRecords: stableRecords, ambiguousResults: provenance.ambiguities.length, historySha256: sha256(JSON.stringify(response.records)), namingEvidenceSha256: sha256(JSON.stringify(namingEvidence)), namingSemanticSha256: sha256(JSON.stringify(namingSemanticSnapshot(namingEvidence))), }, } } catch (error) { response.result?.delete?.() throw error } } const createModule = (await import(pathToFileURL(modulePath).href)).default const module = await createModule({ locateFile: (path) => resolve(dist, path) }) const persistPair = async (pair, step, namingEvidence, decision) => { const temporaryDirectory = await mkdtemp(join(tmpdir(), 'freecad-ordered-pair-')) try { const stepPath = join(temporaryDirectory, 'pair-result.step') const evidencePath = join(temporaryDirectory, 'pair-naming.json') await Promise.all([writeFile(stepPath, step), writeFile(evidencePath, JSON.stringify(namingEvidence ?? null))]) const execution = spawnSync(freecad, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resaveHarnessPath], { cwd: root, encoding: 'utf8', timeout: 180_000, maxBuffer: 40 * 1024 * 1024, env: { ...process.env, FREECAD_ORDERED_PAIR: pair, FREECAD_PAIR_DECISION: decision, FREECAD_PAIR_STEP_PATH: stepPath, FREECAD_PAIR_EVIDENCE_PATH: evidencePath, FREECAD_PAIR_OUTPUT_DIRECTORY: temporaryDirectory, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`, MATPLOTLIBRC: resolve(sysroot, 'usr/share/matplotlib/mpl-data/matplotlibrc'), MPLBACKEND: 'Agg', }, }) const output = `${execution.stdout || ''}\n${execution.stderr || ''}` const marker = 'FREECAD_ORDERED_OPERATION_PAIR_RESAVE_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(`FreeCAD FCStd replay exited with ${execution.status}: ${execution.error?.message || output.trim()}`) const persistence = JSON.parse(payload) if (!persistence?.checks || typeof persistence.checks !== 'object') fail(`FreeCAD FCStd replay returned incomplete evidence: ${JSON.stringify(persistence)}`) const noOpSourceBrepReserialized = persistence.checks.freecadProfileNoOp === true && persistence.checks.sourceShapeRestored === false && persistence.checks.sourceShapeSummaryRestored === true && persistence.checks.sourceShapeGeometricallyRestored === true && persistence.rollback?.sourceBrepReserialized === true const checksPass = Object.values(persistence.checks).every(Boolean) || (decision === 'rejected' && persistence.checks.freecadProfileAccepted === true && persistence.checks.nativeFeatureRejected === false && Object.entries(persistence.checks).filter(([key]) => key !== 'nativeFeatureRejected').every(([, value]) => value === true)) || (decision === 'rejected' && persistence.checks.freecadProfileNoOp === true && persistence.checks.freecadProfileAccepted === false && persistence.checks.nativeFeatureRejected === false && Object.entries(persistence.checks).filter(([key]) => !['nativeFeatureRejected', 'freecadProfileAccepted', ...(noOpSourceBrepReserialized ? ['sourceShapeRestored'] : [])].includes(key)).every(([, value]) => value === true)) || (decision === 'rejected' && persistence.checks.freecadProfileAccepted === false && persistence.checks.nativeFeatureRejected === true && Object.entries(persistence.checks).filter(([key]) => key !== 'freecadProfileAccepted').every(([, value]) => value === true)) if (persistence.status !== 'pass' || persistence.decision !== decision || persistence.freecadVersion !== '1.1.1' || persistence.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !checksPass) fail(`FreeCAD FCStd replay failed: ${JSON.stringify(persistence)}`) return persistence } finally { await rm(temporaryDirectory, { recursive: true, force: true }) } } const classifyPair = async (fromOperation, toOperation, pairIndex) => { const pair = `${fromOperation}->${toOperation}` const pairId = pair.replace('->', '-') const firstParameters = pair === 'loft->fillet' ? { ...operationSpecs.loft.nominal, ruled: true, objectStep: shapeStep(module, 'makeRectangleFace', 10, 10) } : pair === 'cut->thickness' ? { toolSize: 10, toolOffset: 5 } : pair === 'pocket->thickness' ? { length: 10, profileWidth: 5, profileHeight: 10 } : ['common->pocket', 'common->groove'].includes(pair) ? { toolSize: 5, toolOffset: 0 } : fromOperation === 'fuse' ? toOperation === 'thickness' ? { toolSize: 10, toolOffset: 0 } : ['fillet', 'chamfer'].includes(toOperation) ? { toolSize: 10, toolOffset: 5 } : { toolSize: 5, toolOffset: 8 } : fromOperation === 'common' ? { toolSize: 5, toolOffset: 4 } : fromOperation === 'pad' ? { ...operationSpecs.pad.nominal, objectStep: shapeStep(module, 'makeRectangleFace', 10, 10) } : fromOperation === 'loft' ? { ...operationSpecs.loft.nominal, objectStep: shapeStep(module, 'makeRectangleFace', 10, 10) } : fromOperation === 'pipe' ? { ...operationSpecs.pipe.nominal, objectStep: shapeStep(module, 'makeRectangleFace', 2, 3) } : fromOperation === 'revolution' ? { ...operationSpecs.revolution.nominal, objectStep: shapeStep(module, 'makeRectangleFace', 2, 3) } : operationSpecs[fromOperation].nominal const first = captureStage(module, { operation: fromOperation, ...firstParameters, stage: { stageId: `pair:${pairId}:first`, ordinal: 0, objectId: `pair:${pairId}:base`, objectTag: 1001 + pairIndex * 100, resultObjectId: `pair:${pairId}:first`, resultObjectTag: 1101 + pairIndex * 100 }, }) const spec = pairSpecOverrides[pair] ?? operationSpecs[toOperation] const secondDefinition = { operation: toOperation, objectStep: first.response.resultStep, inputSummary: first.report.summary, ...spec.nominal, stage: { stageId: `pair:${pairId}:second`, ordinal: 1, objectId: `pair:${pairId}:first`, objectTag: 1101 + pairIndex * 100, resultObjectId: `pair:${pairId}:second`, resultObjectTag: 1201 + pairIndex * 100, namingEvidence: first.namingEvidence }, } if (spec.decision === 'rejected') { try { const trajectory = [spec.beforeValue, spec.editedValue, spec.beforeValue] if (['freecad-document', 'occt-no-op'].includes(spec.rejectionAuthority)) { const kernelAttempts = trajectory.map((value) => captureKernelCompatibilityStage(module, { ...secondDefinition, [spec.parameter]: value }, value, spec.kernelOutcome)) const kernelRestoredExactly = JSON.stringify(kernelAttempts[0]) === JSON.stringify(kernelAttempts[2]) if (!kernelRestoredExactly) fail(`${pair} OCCT diagnostic probe did not restore exactly.`) const persistence = await persistPair(pair, first.response.resultStep, first.namingEvidence, 'rejected') const freecadProfileAccepted = persistence.checks.freecadProfileAccepted === true const freecadProfileNoOp = persistence.checks.freecadProfileNoOp === true return { taskId: `TSN-PAIR-${pairId}`, pair, classification: 'rejected', reasonCode: spec.rejectionAuthority === 'occt-no-op' ? freecadProfileAccepted ? 'native-builder-no-op-with-freecad-profile-acceptance-and-clean-resave' : freecadProfileNoOp ? 'native-and-freecad-profile-no-op-with-clean-resave' : 'native-builder-no-op-and-freecad-profile-reject-with-clean-resave' : 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', nativeDecision: 'rejected', first: first.report, rejection: { scope: 'second-operation-only', authority: spec.rejectionAuthority, parameter: spec.parameter, trajectory, kernelAttempts, kernelRestoredExactly, persistence }, } } const attempts = trajectory.map((value) => captureRejectedStage(module, { ...secondDefinition, [spec.parameter]: value })) const stableRejection = attempts.every((attempt) => rejectionSignature(attempt) === rejectionSignature(attempts[0])) if (!stableRejection) fail(`${pair} rejection changed across its parameter trajectory.`) const persistence = await persistPair(pair, first.response.resultStep, first.namingEvidence, 'rejected') const freecadProfileAccepted = persistence.checks.freecadProfileAccepted === true const wasmAbort = attempts.every(({ exceptionTransport }) => exceptionTransport === 'emscripten-wasm-abort') return { taskId: `TSN-PAIR-${pairId}`, pair, classification: 'rejected', reasonCode: freecadProfileAccepted ? wasmAbort ? 'native-builder-wasm-abort-with-freecad-profile-acceptance-and-clean-resave' : 'native-builder-cpp-exception-with-freecad-profile-acceptance-and-clean-resave' : 'native-builder-and-freecad-profile-reject-with-clean-resave', nativeDecision: 'rejected', first: first.report, rejection: { scope: 'second-operation-only', authority: 'occt-builder', parameter: spec.parameter, trajectory, stable: stableRejection, attempts, persistence }, } } finally { first.response.result?.delete?.() } } const second = captureStage(module, secondDefinition) const mutated = captureStage(module, { ...secondDefinition, ...spec.edited }) const restored = captureStage(module, secondDefinition) try { const mutationChanged = JSON.stringify(second.report.summary) !== JSON.stringify(mutated.report.summary) const restoredExactly = JSON.stringify(second.report.summary) === JSON.stringify(restored.report.summary) && second.report.historySha256 === restored.report.historySha256 && second.report.namingSemanticSha256 === restored.report.namingSemanticSha256 if (!mutationChanged || !restoredExactly) fail(`${pair} mutation did not change and restore native geometry/history.`) const persistence = await persistPair(pair, second.response.resultStep, second.namingEvidence, 'accepted') return { taskId: `TSN-PAIR-${pairId}`, pair, classification: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', nativeDecision: 'accepted', first: first.report, second: second.report, mutation: { scope: 'second-operation-only', parameter: spec.parameter, trajectory: [spec.beforeValue, spec.editedValue, spec.beforeValue], beforeValue: spec.beforeValue, editedValue: spec.editedValue, restoredValue: spec.beforeValue, changed: mutationChanged, restoredExactly, editedEvidence: mutated.report, restoredEvidence: restored.report }, naming: { upstreamEvidenceRestored: Boolean(secondDefinition.stage.namingEvidence), downstreamEvidence: Boolean(second.namingEvidence), jsonRoundtripStable: JSON.stringify(JSON.parse(JSON.stringify(second.namingEvidence))) === JSON.stringify(second.namingEvidence) }, persistence, } } finally { for (const capture of [first, second, mutated, restored]) capture.response.result?.delete?.() } } const operations = ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform'] const orderedPairPrefix = [...operations.map((toOperation) => ['fuse', toOperation]), ['cut', 'fuse'], ['cut', 'cut'], ['cut', 'common'], ['cut', 'pad'], ['cut', 'pocket'], ['cut', 'loft'], ['cut', 'pipe'], ['cut', 'revolution'], ['cut', 'groove'], ['cut', 'fillet'], ['cut', 'chamfer'], ['cut', 'hole'], ['cut', 'draft'], ['cut', 'thickness'], ['cut', 'linear-pattern'], ['cut', 'polar-pattern'], ['cut', 'mirrored'], ['cut', 'multi-transform'], ['common', 'fuse'], ['common', 'cut'], ['common', 'common'], ['common', 'rotate'], ['common', 'pad'], ['common', 'pocket'], ['common', 'loft'], ['common', 'pipe'], ['common', 'revolution'], ['common', 'groove'], ['common', 'fillet'], ['common', 'chamfer'], ['common', 'hole'], ['common', 'draft'], ['common', 'thickness'], ['common', 'linear-pattern'], ['common', 'polar-pattern'], ['common', 'mirrored'], ['common', 'multi-transform'], ['rotate', 'fuse'], ['rotate', 'cut'], ['rotate', 'common'], ['rotate', 'rotate'], ['rotate', 'pad'], ['rotate', 'pocket'], ['rotate', 'loft'], ['rotate', 'pipe'], ['rotate', 'revolution'], ['rotate', 'groove'], ['rotate', 'chamfer'], ['rotate', 'hole'], ['rotate', 'draft'], ['rotate', 'thickness'], ['rotate', 'linear-pattern'], ['rotate', 'polar-pattern'], ['rotate', 'mirrored'], ['rotate', 'multi-transform'], ['pad', 'fuse'], ['pad', 'cut'], ['pad', 'common'], ['pad', 'rotate'], ['pad', 'pad'], ['pad', 'pocket'], ['pad', 'loft'], ['pad', 'pipe'], ['pad', 'revolution'], ['pad', 'groove'], ['pad', 'fillet'], ['pad', 'chamfer'], ['pad', 'hole'], ['pad', 'draft'], ['pad', 'thickness'], ['pad', 'linear-pattern'], ['pad', 'polar-pattern'], ['pad', 'mirrored'], ['pad', 'multi-transform'], ['pocket', 'fuse'], ['pocket', 'cut'], ['pocket', 'common'], ['pocket', 'rotate'], ['pocket', 'pad'], ['pocket', 'pocket'], ['pocket', 'loft'], ['pocket', 'pipe'], ['pocket', 'revolution'], ['pocket', 'groove'], ['pocket', 'fillet'], ['pocket', 'chamfer'], ['pocket', 'hole'], ['pocket', 'draft'], ['pocket', 'thickness'], ['pocket', 'linear-pattern'], ['pocket', 'polar-pattern'], ['pocket', 'mirrored'], ['pocket', 'multi-transform'], ['loft', 'fuse'], ['loft', 'cut'], ['loft', 'common'], ['loft', 'rotate'], ['loft', 'pad'], ['loft', 'pocket'], ['loft', 'loft'], ['loft', 'pipe'], ['loft', 'revolution'], ['loft', 'groove'], ['loft', 'fillet'], ['loft', 'chamfer'], ['loft', 'hole'], ['loft', 'draft'], ['loft', 'thickness'], ['loft', 'linear-pattern'], ['loft', 'polar-pattern'], ['loft', 'mirrored'], ['loft', 'multi-transform'], ['pipe', 'fuse'], ['pipe', 'cut'], ['pipe', 'common'], ['pipe', 'rotate'], ['pipe', 'pad'], ['pipe', 'pocket'], ['pipe', 'loft'], ['pipe', 'pipe'], ['pipe', 'revolution'], ['pipe', 'groove'], ['pipe', 'fillet'], ['pipe', 'chamfer'], ['pipe', 'hole'], ['pipe', 'draft'], ['pipe', 'thickness'], ['pipe', 'linear-pattern'], ['pipe', 'polar-pattern'], ['pipe', 'mirrored'], ['pipe', 'multi-transform'], ['revolution', 'fuse'], ['revolution', 'cut'], ['revolution', 'common'], ['revolution', 'rotate'], ['revolution', 'pad'], ['revolution', 'pocket'], ['revolution', 'loft'], ['revolution', 'pipe'], ['revolution', 'revolution'], ['revolution', 'groove'], ['revolution', 'fillet'], ['revolution', 'chamfer'], ['revolution', 'hole'], ['revolution', 'draft'], ['revolution', 'thickness'], ['revolution', 'linear-pattern'], ['revolution', 'polar-pattern'], ['revolution', 'mirrored'], ['revolution', 'multi-transform'], ['groove', 'fuse'], ['groove', 'cut'], ['groove', 'common'], ['groove', 'rotate'], ['groove', 'pad'], ['groove', 'pocket'], ['groove', 'loft'], ['groove', 'pipe'], ['groove', 'revolution'], ['groove', 'groove'], ['groove', 'fillet'], ['groove', 'chamfer'], ['groove', 'hole'], ['groove', 'draft'], ['groove', 'thickness'], ['groove', 'linear-pattern'], ['groove', 'polar-pattern'], ['groove', 'mirrored']] orderedPairPrefix.push(['groove', 'multi-transform'], ['fillet', 'fuse'], ['fillet', 'cut'], ['fillet', 'common'], ['fillet', 'rotate'], ['fillet', 'pad'], ['fillet', 'pocket'], ['fillet', 'loft'], ['fillet', 'pipe'], ['fillet', 'revolution'], ['fillet', 'groove'], ['fillet', 'fillet'], ['fillet', 'chamfer'], ['fillet', 'hole'], ['fillet', 'draft'], ['fillet', 'thickness'], ['fillet', 'linear-pattern'], ['fillet', 'polar-pattern'], ['fillet', 'multi-transform'], ['chamfer', 'fuse'], ['chamfer', 'cut'], ['chamfer', 'common'], ['chamfer', 'rotate'], ['chamfer', 'pad'], ['chamfer', 'pocket'], ['chamfer', 'loft'], ['chamfer', 'pipe'], ['chamfer', 'revolution'], ['chamfer', 'groove'], ['chamfer', 'fillet'], ['chamfer', 'chamfer'], ['chamfer', 'hole'], ['chamfer', 'draft'], ['chamfer', 'thickness'], ['chamfer', 'linear-pattern'], ['chamfer', 'polar-pattern'], ['chamfer', 'mirrored'], ['chamfer', 'multi-transform'], ['hole', 'fuse'], ['hole', 'cut'], ['hole', 'common'], ['hole', 'rotate'], ['hole', 'pad'], ['hole', 'pocket'], ['hole', 'loft'], ['hole', 'pipe'], ['hole', 'revolution'], ['hole', 'groove'], ['hole', 'fillet'], ['hole', 'chamfer'], ['hole', 'hole'], ['hole', 'draft'], ['hole', 'thickness'], ['hole', 'linear-pattern'], ['hole', 'polar-pattern'], ['hole', 'mirrored'], ['hole', 'multi-transform'], ['draft', 'fuse'], ['draft', 'cut'], ['draft', 'common'], ['draft', 'rotate'], ['draft', 'pad'], ['draft', 'pocket'], ['draft', 'loft'], ['draft', 'pipe'], ['draft', 'revolution'], ['draft', 'groove'], ['draft', 'fillet'], ['draft', 'chamfer'], ['draft', 'hole'], ['draft', 'draft'], ['draft', 'thickness'], ['draft', 'linear-pattern'], ['draft', 'polar-pattern'], ['draft', 'mirrored'], ['draft', 'multi-transform'], ['thickness', 'fuse'], ['thickness', 'cut'], ['thickness', 'common'], ['thickness', 'rotate'], ['thickness', 'pad'], ['thickness', 'pocket'], ['thickness', 'loft'], ['thickness', 'pipe'], ['thickness', 'revolution'], ['thickness', 'groove'], ['thickness', 'fillet'], ['thickness', 'chamfer'], ['thickness', 'hole'], ['thickness', 'draft'], ['thickness', 'thickness'], ['thickness', 'linear-pattern'], ['thickness', 'polar-pattern'], ['thickness', 'mirrored'], ['thickness', 'multi-transform'], ['linear-pattern', 'fuse'], ['linear-pattern', 'cut'], ['linear-pattern', 'common'], ['linear-pattern', 'rotate'], ['linear-pattern', 'pad'], ['linear-pattern', 'pocket'], ['linear-pattern', 'loft'], ['linear-pattern', 'pipe'], ['linear-pattern', 'revolution'], ['linear-pattern', 'groove'], ['linear-pattern', 'fillet'], ['linear-pattern', 'chamfer'], ['linear-pattern', 'hole'], ['linear-pattern', 'draft'], ['linear-pattern', 'thickness'], ['linear-pattern', 'linear-pattern'], ['linear-pattern', 'polar-pattern'], ['linear-pattern', 'mirrored'], ['linear-pattern', 'multi-transform'], ['polar-pattern', 'fuse'], ['polar-pattern', 'cut'], ['polar-pattern', 'common'], ['polar-pattern', 'rotate'], ['polar-pattern', 'pad'], ['polar-pattern', 'pocket']) orderedPairPrefix.push(['polar-pattern', 'loft']) orderedPairPrefix.push(['polar-pattern', 'pipe']) orderedPairPrefix.push(['polar-pattern', 'revolution']) orderedPairPrefix.push(['polar-pattern', 'groove']) orderedPairPrefix.push(['polar-pattern', 'fillet']) orderedPairPrefix.push(['polar-pattern', 'chamfer']) orderedPairPrefix.push(['polar-pattern', 'hole']) orderedPairPrefix.push(['polar-pattern', 'draft']) orderedPairPrefix.push(['polar-pattern', 'thickness']) orderedPairPrefix.push(['polar-pattern', 'linear-pattern']) orderedPairPrefix.push(['polar-pattern', 'polar-pattern']) orderedPairPrefix.push(['polar-pattern', 'mirrored']) orderedPairPrefix.push(['polar-pattern', 'multi-transform']) orderedPairPrefix.push(['mirrored', 'fuse']) orderedPairPrefix.push(['mirrored', 'cut']) orderedPairPrefix.push(['mirrored', 'common']) orderedPairPrefix.push(['mirrored', 'rotate']) orderedPairPrefix.push(['mirrored', 'pad']) orderedPairPrefix.push(['mirrored', 'pocket']) orderedPairPrefix.push(['mirrored', 'loft']) orderedPairPrefix.push(['mirrored', 'pipe']) orderedPairPrefix.push(['mirrored', 'revolution']) orderedPairPrefix.push(['mirrored', 'groove']) orderedPairPrefix.push(['mirrored', 'fillet']) orderedPairPrefix.push(['mirrored', 'chamfer']) orderedPairPrefix.push(['mirrored', 'hole']) orderedPairPrefix.push(['mirrored', 'draft']) orderedPairPrefix.push(['mirrored', 'thickness']) orderedPairPrefix.push(['mirrored', 'polar-pattern']) orderedPairPrefix.push(['mirrored', 'mirrored']) orderedPairPrefix.push(['mirrored', 'multi-transform']) orderedPairPrefix.push(['multi-transform', 'fuse']) orderedPairPrefix.push(['multi-transform', 'cut']) orderedPairPrefix.push(['multi-transform', 'common']) orderedPairPrefix.push(['multi-transform', 'rotate']) orderedPairPrefix.push(['multi-transform', 'pad']) orderedPairPrefix.push(['multi-transform', 'pocket']) orderedPairPrefix.push(['multi-transform', 'loft']) orderedPairPrefix.push(['multi-transform', 'pipe']) orderedPairPrefix.push(['multi-transform', 'revolution']) orderedPairPrefix.push(['multi-transform', 'groove']) orderedPairPrefix.push(['multi-transform', 'fillet']) orderedPairPrefix.push(['multi-transform', 'chamfer']) orderedPairPrefix.push(['multi-transform', 'hole']) orderedPairPrefix.push(['multi-transform', 'draft']) orderedPairPrefix.push(['multi-transform', 'thickness']) orderedPairPrefix.push(['multi-transform', 'linear-pattern']) orderedPairPrefix.push(['multi-transform', 'polar-pattern']) orderedPairPrefix.push(['multi-transform', 'mirrored']) orderedPairPrefix.push(['multi-transform', 'multi-transform']) const classifications = [] const selectedPairs = process.env.FREECAD_ONLY_PAIR ? orderedPairPrefix.filter(([fromOperation, toOperation]) => `${fromOperation}->${toOperation}` === process.env.FREECAD_ONLY_PAIR) : orderedPairPrefix if (process.env.FREECAD_ONLY_PAIR && selectedPairs.length !== 1) fail(`focused pair ${process.env.FREECAD_ONLY_PAIR} is not in the classified prefix.`) for (const [index, [fromOperation, toOperation]] of selectedPairs.entries()) classifications.push(await classifyPair(fromOperation, toOperation, index)) if (process.env.FREECAD_ONLY_PAIR && existsSync(outputPath)) { const previous = JSON.parse(await readFile(outputPath, 'utf8')) const byPair = new Map([...previous.classifications, ...classifications].map((entry) => [entry.pair, entry])) classifications.splice(0, classifications.length, ...orderedPairPrefix.map(([fromOperation, toOperation]) => byPair.get(`${fromOperation}->${toOperation}`)).filter(Boolean)) } const [artifacts, executorHarness, matrixHarness, resaveHarness] = await Promise.all([ Promise.all(artifactNames.map(async (name) => { const path = resolve(dist, name) const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)]) return { name, bytes, sha256: sha256(content) } })), readFile(executorPath), readFile(resolve(root, 'scripts/freecad-naming-production-matrix.mjs')), readFile(resaveHarnessPath), ]) const report = { schemaVersion: 1, baseline: { freecadVersion: '1.1.1', commit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', occtVersion: module.occtVersion() }, generatedBy: './npmw run probe:freecad-ordered-operation-pairs', checkedBy: './npmw run check:freecad-ordered-operation-pairs', nativeProbe: { executor: { path: 'scripts/run-freecad-ordered-operation-pair-classification.mjs', bytes: executorHarness.length, sha256: sha256(executorHarness) }, matrix: { path: 'scripts/freecad-naming-production-matrix.mjs', bytes: matrixHarness.length, sha256: sha256(matrixHarness) }, resave: { path: 'scripts/freecad-ordered-operation-pair-resave.py', bytes: resaveHarness.length, sha256: sha256(resaveHarness) }, artifacts, }, classifications, summary: { registeredOperations: 19, orderedPairs: 361, classifiedPairs: classifications.length, accepted: classifications.filter(({ classification }) => classification === 'accepted').length, rejected: classifications.filter(({ classification }) => classification === 'rejected').length, unknown: 361 - classifications.length }, } await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) console.log(JSON.stringify({ status: 'freecad-ordered-operation-pair-classification-generated', pairs: selectedPairs.map(([fromOperation, toOperation]) => classifications.find(({ pair }) => pair === `${fromOperation}->${toOperation}`)).map(({ pair, classification }) => ({ pair, classification })), nativeBuilderRuns: selectedPairs.length * 4, fcstdPhases: selectedPairs.length * 3, classifiedPairs: classifications.length, output: 'config/freecad-ordered-operation-pair-classification.json' }, null, 2))