feat: extend FreeCAD parity queue and pair diagnostics
This commit is contained in:
@@ -50,12 +50,38 @@ const operationSpecs = {
|
||||
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' },
|
||||
}
|
||||
|
||||
const createOperation = (module, { operation, objectStep, toolSize, toolOffset, angle, stage }) => {
|
||||
const createOperation = (module, { operation, objectStep, toolSize, toolOffset, angle, length, ruled, spineLength, 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 === '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,
|
||||
@@ -64,6 +90,61 @@ const createOperation = (module, { operation, objectStep, toolSize, toolOffset,
|
||||
return { response: module.booleanHistoryFromStep(baseStep, toolStep, operation), inputs }
|
||||
}
|
||||
|
||||
const captureRejectedStage = (module, definition) => {
|
||||
let capture
|
||||
try {
|
||||
capture = createOperation(module, definition)
|
||||
} catch (error) {
|
||||
return {
|
||||
operation: definition.operation,
|
||||
builder: operationSpecs[definition.operation].builder,
|
||||
inputCount: 1,
|
||||
parameter: operationSpecs[definition.operation].parameter,
|
||||
parameterValue: definition.length,
|
||||
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 {
|
||||
@@ -114,12 +195,12 @@ const captureStage = (module, definition) => {
|
||||
|
||||
const createModule = (await import(pathToFileURL(modulePath).href)).default
|
||||
const module = await createModule({ locateFile: (path) => resolve(dist, path) })
|
||||
const persistAcceptedPair = async (pair, second) => {
|
||||
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, second.response.resultStep), writeFile(evidencePath, JSON.stringify(second.namingEvidence))])
|
||||
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',
|
||||
@@ -128,6 +209,7 @@ const persistAcceptedPair = async (pair, second) => {
|
||||
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,
|
||||
@@ -143,7 +225,7 @@ const persistAcceptedPair = async (pair, second) => {
|
||||
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.freecadVersion !== '1.1.1' || persistence.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !Object.values(persistence.checks).every(Boolean)) fail(`FreeCAD FCStd replay failed: ${JSON.stringify(persistence)}`)
|
||||
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 })
|
||||
@@ -164,6 +246,41 @@ const classifyFusePair = async (toOperation, pairIndex) => {
|
||||
...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-fuse-${toOperation}`,
|
||||
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((length) => captureRejectedStage(module, { ...secondDefinition, length }))
|
||||
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-fuse-${toOperation}`,
|
||||
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)
|
||||
@@ -171,7 +288,7 @@ const classifyFusePair = async (toOperation, pairIndex) => {
|
||||
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 persistAcceptedPair(pair, second)
|
||||
const persistence = await persistPair(pair, second.response.resultStep, second.namingEvidence, 'accepted')
|
||||
return {
|
||||
taskId: `TSN-PAIR-fuse-${toOperation}`,
|
||||
pair,
|
||||
@@ -190,7 +307,7 @@ const classifyFusePair = async (toOperation, pairIndex) => {
|
||||
}
|
||||
|
||||
const classifications = []
|
||||
for (const [index, operation] of ['fuse', 'cut', 'common', 'rotate'].entries()) classifications.push(await classifyFusePair(operation, index))
|
||||
for (const [index, operation] of ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe'].entries()) classifications.push(await classifyFusePair(operation, index))
|
||||
|
||||
const [artifacts, executorHarness, matrixHarness, resaveHarness] = await Promise.all([
|
||||
Promise.all(artifactNames.map(async (name) => {
|
||||
|
||||
Reference in New Issue
Block a user