Files
Web_FreeCAD_Bitbybit/scripts/run-freecad-ordered-operation-pair-classification.mjs
wangdequan 8ef796552d
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled
feat: classify rotate ordered pair row
2026-08-15 11:39:58 -04:00

400 lines
28 KiB
JavaScript

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 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' },
}
const createOperation = (module, { operation, objectStep, toolSize, toolOffset, angle, length, ruled, spineLength, radius, distance, depth, position, direction, faceIndex, axisOrigin, neutralPlaneDirection, reversed, offset, intersectionJoin, translationX, planeOriginX, multiTranslationX, 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: [5, 0, 0], direction: [1, 0, 0] },
]),
inputs: [objectInput],
}
if (operation === 'groove') {
const profileStep = shapeStep(module, 'makeRectangleFacePlaced', 1, 3, 0.5, 2, 0)
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', 2, 3)
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', 2, 3, 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]
let capture
try {
capture = createOperation(module, definition)
} catch (error) {
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: 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'
if (response.provider !== 'occt-native' || outcome !== expectedOutcome || !response.resultStep?.startsWith('ISO-10303-21;') || response.records?.length < 1) fail(`${definition.stage.stageId} OCCT compatibility probe returned an unexpected result.`)
return {
operation: definition.operation,
builder: operationSpecs[definition.operation].builder,
inputCount: inputs.length,
parameter: operationSpecs[definition.operation].parameter,
parameterValue,
runtime: 'occt-native',
outcome,
resultProduced: true,
shapeProduced: true,
validShapeProduced: response.summary.isValid === true,
historyProduced: true,
historyProvider: response.provider,
summary: canonicalSummary(response.summary),
historyRecords: response.records.length,
historySha256: sha256(JSON.stringify(response.records)),
authority: outcome === 'accepted' ? 'kernel-superset-only' : 'kernel-invalid-result',
}
} 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.`)
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.status !== 'pass' || persistence.decision !== decision || persistence.freecadVersion !== '1.1.1' || persistence.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !Object.values(persistence.checks).every(Boolean)) 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 === 'cut->thickness'
? { toolSize: 10, toolOffset: 5 }
: ['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 }
: 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,
...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 (spec.rejectionAuthority === 'freecad-document') {
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')
return {
taskId: `TSN-PAIR-${pairId}`,
pair,
classification: 'rejected',
reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave',
nativeDecision: 'rejected',
first: first.report,
rejection: { scope: 'second-operation-only', authority: 'freecad-document', 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')
return {
taskId: `TSN-PAIR-${pairId}`,
pair,
classification: 'rejected',
reasonCode: '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']]
const classifications = []
for (const [index, [fromOperation, toOperation]] of orderedPairPrefix.entries()) classifications.push(await classifyPair(fromOperation, toOperation, index))
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: classifications.map(({ pair, classification }) => ({ pair, classification })), nativeBuilderRuns: classifications.length * 4, fcstdPhases: classifications.length * 3, output: 'config/freecad-ordered-operation-pair-classification.json' }, null, 2))