feat: classify recovered naming drift
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

This commit is contained in:
2026-08-15 01:01:54 -04:00
parent 5bbd7b9d4f
commit ea3706bd3d
21 changed files with 18736 additions and 15778 deletions

View File

@@ -19,13 +19,74 @@ if (success.after?.acceptClicked !== true || success.taskPanel?.closed?.activeDi
const pad = success.after?.pad
if (success.after?.bodyTip !== 'Pad' || success.after.bodyGroup?.join(',') !== 'Sketch,Pad' || pad?.typeId !== 'PartDesign::Pad' || pad.length !== 10 || pad.shapeValid !== true || pad.solidCount !== 1 || pad.faceCount !== 6 || pad.edgeCount !== 12 || pad.vertexCount !== 8 || pad.volume !== 120 || pad.profile !== 'Sketch' || pad.state?.join(',') !== 'Up-to-date') fail('native Pad result semantics are incomplete.')
if ((!Number.isInteger(success.after.undoCount) || success.after.undoCount < 1) && (!Array.isArray(success.after.undoNames) || success.after.undoNames.length < 1)) fail('native Pad transaction evidence is missing.')
if (JSON.stringify(report.remainingStates) !== JSON.stringify(['disabled', 'failure', 'cancel', 'recovery']) || report.exactPromotionReady !== false) fail('remaining workflow boundary is incorrect.')
const disabled = report.workflows?.disabled
if (disabled?.workflowId !== report.family.id || disabled.commandId !== report.family.primaryCommand || disabled.state !== 'disabled' || disabled.success !== true) fail('disabled workflow identity is invalid.')
if (disabled.before?.activeWorkbench !== 'PartDesignWorkbench' || disabled.before.commandRegistered !== true || disabled.before.commandActive !== false || disabled.before.actionCount < 1 || disabled.before.actionEnabled !== false) fail('native disabled command state is incomplete.')
const checkpoints = [
disabled.before?.sideEffects,
disabled.activationAttempts?.actionTrigger?.after,
disabled.activationAttempts?.runCommand?.after,
disabled.after?.sideEffects,
]
for (const checkpoint of checkpoints) {
if (!checkpoint || checkpoint.documentCount !== 0 || checkpoint.documents?.length !== 0 || checkpoint.activeDocument !== '' || checkpoint.activeGuiDocument !== false || checkpoint.selection?.length !== 0) fail('disabled Pad created or activated document state.')
if (checkpoint.taskPanel?.activeDialog !== false || checkpoint.taskPanel.inEdit !== '') fail('disabled Pad created a Task or edit state.')
if (checkpoint.transaction?.active !== false || checkpoint.transaction.name !== '' || checkpoint.transaction.id !== null || checkpoint.transaction.documents?.length !== 0) fail('disabled Pad created a transaction scope.')
}
if (disabled.activationAttempts.actionTrigger.requested !== true || disabled.activationAttempts.actionTrigger.triggeredSignalCount !== 0 || disabled.activationAttempts.actionTrigger.sideEffectsUnchanged !== true) fail('disabled QAction activation was not blocked cleanly.')
if (disabled.activationAttempts.runCommand.requested !== true || disabled.activationAttempts.runCommand.sideEffectsUnchanged !== true || disabled.after.commandActive !== false || disabled.after.actionEnabled !== false) fail('disabled command-manager activation was not blocked cleanly.')
const failure = report.workflows?.failure
if (failure?.workflowId !== report.family.id || failure.commandId !== report.family.primaryCommand || failure.state !== 'failure' || failure.success !== true) fail('failure workflow identity is invalid.')
if (failure.before?.activeWorkbench !== 'PartDesignWorkbench' || failure.before.commandRegistered !== true || failure.before.commandActive !== true || failure.before.actionCount < 1 || typeof failure.before.actionEnabled !== 'boolean' || failure.before.baseline?.selection?.join(',') !== 'Sketch' || failure.before.baseline.sketchGeometryCount !== 2 || failure.before.baseline.bodyTip !== '') fail('native failure preconditions are incomplete.')
if (failure.taskPanel?.opened?.activeDialog !== true || failure.taskPanel.opened.inEdit !== 'Pad' || failure.taskPanel.preview?.exists !== true || failure.taskPanel.preview.typeId !== 'PartDesign::Pad' || failure.taskPanel.preview.profile !== 'Sketch' || failure.taskPanel.preview.shapeNull !== true || !failure.taskPanel.preview.state?.includes('Invalid') || !failure.taskPanel.preview.statusString) fail('invalid Pad preview evidence is incomplete.')
const warning = failure.diagnostic?.warnings?.[0]
if (failure.diagnostic?.requested !== true || failure.diagnostic.warnings.length !== 1 || warning.title !== 'Input error' || warning.text !== failure.taskPanel.preview.statusString) fail('native Pad failure diagnostic is incomplete.')
if (failure.taskPanel.afterAttempt?.taskPanel?.activeDialog !== true || failure.taskPanel.afterAttempt.taskPanel.inEdit !== 'Pad' || failure.taskPanel.afterAttempt.padExists !== true || failure.taskPanel.afterAttempt.padShapeNull !== true || failure.taskPanel.afterAttempt.transaction?.active !== true) fail('failed Pad Task was not retained for correction.')
if (failure.after?.rejectRequested !== true || failure.after.taskPanel?.activeDialog !== false || failure.after.taskPanel.inEdit !== '' || failure.after.padExists !== false || JSON.stringify(failure.after.objectNames) !== JSON.stringify(failure.before.baseline.objectNames) || failure.after.bodyTip !== failure.before.baseline.bodyTip || failure.after.sketchGeometryCount !== failure.before.baseline.sketchGeometryCount || failure.after.sketchShapeValid !== failure.before.baseline.sketchShapeValid || failure.after.transaction?.active !== false) fail('failed Pad cleanup did not restore the last valid document state.')
const cancel = report.workflows?.cancel
if (cancel?.workflowId !== report.family.id || cancel.commandId !== report.family.primaryCommand || cancel.state !== 'cancel' || cancel.success !== true || cancel.nativeSelectionOutcome !== 'selected-profile-consumed' || cancel.nativeFocusOutcome !== 'task-view-after-cancel') fail('cancel workflow identity is invalid.')
if (cancel.before?.activeWorkbench !== 'PartDesignWorkbench' || cancel.before.commandRegistered !== true || cancel.before.commandActive !== true || cancel.before.actionCount < 1 || typeof cancel.before.actionEnabled !== 'boolean' || cancel.before.baseline?.selection?.join(',') !== 'Sketch' || cancel.before.baseline.activeBody !== 'Body' || cancel.before.baseline.focus?.className !== 'Gui::View3DInventorViewer' || cancel.before.baseline.focus.objectName !== '') fail('native cancel preconditions are incomplete.')
if (cancel.taskPanel?.opened?.activeDialog !== true || cancel.taskPanel.opened.inEdit !== 'Pad' || cancel.taskPanel.preview?.exists !== true || cancel.taskPanel.preview.typeId !== 'PartDesign::Pad' || cancel.taskPanel.preview.length !== 17.5 || cancel.taskPanel.preview.shapeValid !== true || cancel.taskPanel.preview.solidCount !== 1 || cancel.taskPanel.preview.volume !== 210 || cancel.taskPanel.preview.bodyTip !== 'Pad' || cancel.taskPanel.preview.activeBody !== 'Body' || cancel.taskPanel.preview.selection?.length !== 0 || cancel.taskPanel.preview.transaction?.active !== true) fail('native Pad cancel preview evidence is incomplete.')
if (cancel.after?.rejectRequested !== true || cancel.after.taskPanel?.activeDialog !== false || cancel.after.taskPanel.inEdit !== '' || cancel.after.padExists !== false || JSON.stringify(cancel.after.objectNames) !== JSON.stringify(cancel.before.baseline.objectNames) || cancel.after.bodyTip !== cancel.before.baseline.bodyTip || cancel.after.activeBody !== cancel.before.baseline.activeBody || cancel.after.selection?.length !== 0 || cancel.after.focus?.className !== 'Gui::TaskView::TaskView' || cancel.after.focus.objectName !== 'Tasks' || cancel.after.sketchGeometryCount !== cancel.before.baseline.sketchGeometryCount || cancel.after.sketchShapeValid !== cancel.before.baseline.sketchShapeValid || cancel.after.sketchVisible !== true || cancel.after.transaction?.active !== false) fail('cancelled Pad did not restore the native document and active Body or expose the native TaskView focus outcome.')
const recovery = report.workflows?.recovery
if (recovery?.workflowId !== report.family.id || recovery.commandId !== report.family.primaryCommand || recovery.state !== 'recovery' || recovery.success !== true) fail('recovery workflow identity is invalid.')
if (recovery.before?.sketchGeometryCount !== 2 || recovery.before.padExists !== false || recovery.before.bodyTip !== '' || recovery.before.transaction?.active !== false) fail('native recovery open-wire baseline is incomplete.')
const recoveryWarning = recovery.initialFailure?.diagnostic?.warnings?.[0]
if (recovery.initialFailure?.taskPanel?.activeDialog !== true || recovery.initialFailure.taskPanel.inEdit !== 'Pad' || recovery.initialFailure.padShapeNull !== true || !recovery.initialFailure.padState?.includes('Invalid') || recoveryWarning?.title !== 'Input error' || recoveryWarning.text !== 'Wire is not closed.' || recovery.initialFailure.afterAttempt?.activeDialog !== true || recovery.initialFailure.afterAttempt.inEdit !== 'Pad') fail('native recovery failure phase is incomplete.')
if (recovery.repairedPreview?.taskPanel?.activeDialog !== true || recovery.repairedPreview.taskPanel.inEdit !== 'Pad' || recovery.repairedPreview.lengthInputUpdated !== true || recovery.repairedPreview.sketchGeometryCount !== 4 || recovery.repairedPreview.padLength !== 12 || recovery.repairedPreview.padShapeValid !== true || recovery.repairedPreview.padSolidCount !== 1 || recovery.repairedPreview.padVolume !== 144 || recovery.repairedPreview.transaction?.active !== true) fail('native repaired Pad preview is incomplete.')
const assertRecoveredPad = (value, phase) => {
if (!value || value.padExists !== true || value.bodyTip !== 'Pad' || value.bodyGroup?.join(',') !== 'Sketch,Pad' || value.sketchGeometryCount !== 4 || value.padTypeId !== 'PartDesign::Pad' || value.padLength !== 12 || value.padProfile !== 'Sketch' || value.padShapeValid !== true || value.padSolidCount !== 1 || value.padFaceCount !== 6 || value.padEdgeCount !== 12 || value.padVertexCount !== 8 || value.padVolume !== 144 || value.padState?.join(',') !== 'Up-to-date' || value.transaction?.active !== false) fail(`native recovered Pad ${phase} state is incomplete.`)
}
if (recovery.committed?.acceptRequested !== true || recovery.committed.taskPanel?.activeDialog !== false || recovery.committed.taskPanel.inEdit !== '') fail('repaired Pad Task did not commit cleanly.')
assertRecoveredPad(recovery.committed, 'commit')
if (recovery.undo?.padExists !== false || recovery.undo.sketchGeometryCount !== 2 || recovery.undo.bodyTip !== '' || recovery.undo.redoCount < 1 || recovery.undo.transaction?.active !== false) fail('native recovery Undo did not restore the failed-workflow baseline.')
assertRecoveredPad(recovery.redo, 'redo')
if (recovery.redo.undoCount < 1) fail('native recovery Redo did not restore an undoable Pad.')
const persistencePhases = {
saved: 'saved',
reopened: 'reopened',
resavedReopened: 'resaved-reopened',
}
if (Object.keys(recovery.persistence ?? {}).sort().join(',') !== Object.keys(persistencePhases).sort().join(',')) fail('native recovery persistence phases are incomplete.')
for (const [phase, value] of Object.entries(recovery.persistence)) {
assertRecoveredPad(value, phase)
if (value.phase !== persistencePhases[phase]) fail(`native recovery ${phase} phase identity is incomplete.`)
if (!value.fileName?.endsWith('/partdesign-pad-recovery.FCStd')) fail(`native recovery ${phase} file provenance is incomplete.`)
}
if (JSON.stringify(report.remainingStates) !== JSON.stringify([]) || report.exactPromotionReady !== false) fail('remaining workflow boundary is incorrect.')
console.log(JSON.stringify({
status: 'freecad-gui-workflow-pass',
family: report.family.id,
command: report.family.primaryCommand,
capturedStates: Object.keys(report.workflows),
remainingStates: report.remainingStates,
disabledNoSideEffects: true,
failureDiagnostic: warning.text,
failureRolledBack: true,
cancelPreviewVolume: cancel.taskPanel.preview.volume,
cancelRolledBack: true,
recoveryVolume: recovery.persistence.resavedReopened.padVolume,
recoveryResaved: true,
bodyTip: success.after.bodyTip,
volume: pad.volume,
undoNames: success.after.undoNames,

View File

@@ -41,9 +41,11 @@ for (const stateName of stateNames) {
const expectedTaskIds = Array.from({ length: 7 }, (_, index) => `ORA-GUI-WF-${String(index).padStart(3, '0')}`)
if (!Array.isArray(workflow.progress) || workflow.progress.map((entry) => entry.taskId).join(',') !== expectedTaskIds.join(',')) fail('workflow progress must contain ordered ORA-GUI-WF-000 through 006 tasks.')
const statuses = workflow.progress.map((entry) => entry.status)
if (statuses.filter((status) => status === 'in_progress').length !== 1 || statuses.some((status) => !['completed', 'in_progress', 'pending'].includes(status))) fail('workflow progress must have exactly one in-progress task.')
const inProgressCount = statuses.filter((status) => status === 'in_progress').length
if (inProgressCount > 1 || statuses.some((status) => !['completed', 'in_progress', 'pending'].includes(status))) fail('workflow progress has invalid statuses.')
const inProgressIndex = statuses.indexOf('in_progress')
if (statuses.slice(0, inProgressIndex).some((status) => status !== 'completed') || statuses.slice(inProgressIndex + 1).some((status) => status !== 'pending')) fail('workflow progress is not a contiguous serial queue.')
if (inProgressCount === 0 && statuses.some((status) => status !== 'completed')) fail('workflow progress without an in-progress task must be fully completed.')
if (inProgressCount === 1 && (statuses.slice(0, inProgressIndex).some((status) => status !== 'completed') || statuses.slice(inProgressIndex + 1).some((status) => status !== 'pending'))) fail('workflow progress is not a contiguous serial queue.')
for (const [index, entry] of workflow.progress.entries()) {
if (!Array.isArray(entry.evidence)) fail(`${entry.taskId} evidence must be an array.`)
if (index < inProgressIndex && entry.evidence.length === 0) fail(`${entry.taskId} is completed without evidence.`)
@@ -55,6 +57,6 @@ console.log(JSON.stringify({
primaryCommand: family.primaryCommand,
primaryExactTask: family.primaryExactTask,
completed: statuses.filter((status) => status === 'completed').length,
inProgress: workflow.progress[inProgressIndex].taskId,
inProgress: inProgressIndex >= 0 ? workflow.progress[inProgressIndex].taskId : null,
pending: statuses.filter((status) => status === 'pending').length,
}, null, 2))

View File

@@ -0,0 +1,49 @@
import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
import { buildRecoveredNamingSnapshot, recoveredNamingDecisions, same, validateRecoveredNamingSnapshot } from './freecad-recovered-naming-classification.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const fail = (message) => { throw new Error(`FreeCAD recovered naming classification: ${message}`) }
const [report, oracle, resave] = await Promise.all([
load('config/freecad-recovered-naming-classification.json'),
load('config/freecad-composite-history-elementmap-oracle.json'),
load('config/freecad-composite-history-resave-verification.json'),
])
if (report.schemaVersion !== 1 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.baseline.oracleId !== oracle.baselineId) fail('baseline is invalid.')
const harnessPath = resolve(root, report.nativeProbe?.path ?? '')
const [harnessContent, harnessBytes] = await Promise.all([readFile(harnessPath), stat(harnessPath).then(({ size }) => size)])
if (report.nativeProbe.path !== 'scripts/freecad-composite-history-elementmap-oracle.py' || report.nativeProbe.bytes !== harnessBytes || report.nativeProbe.sha256 !== createHash('sha256').update(harnessContent).digest('hex') || report.nativeProbe.independentRuns < 2) fail('native replay provenance is missing or stale.')
if (!Array.isArray(report.classifications) || report.classifications.length !== recoveredNamingDecisions.length) fail('classification report does not contain the complete serial decision prefix.')
let classifiedStages = 0
for (const [index, entry] of report.classifications.entries()) {
const decision = recoveredNamingDecisions[index]
if (!same({ taskId: entry.taskId, caseId: entry.caseId, classification: entry.classification, reasonCode: entry.reasonCode, rationale: entry.rationale }, decision) || entry.replayStable !== true || entry.implementationDefect !== (decision.classification === 'implementation_defect')) fail(`${decision.taskId} classification contract is invalid.`)
const fixture = oracle.cases.find(({ id }) => id === entry.caseId)
const resaveFixture = resave.cases.find(({ id }) => id === entry.caseId)
if (!fixture || !resaveFixture) fail(`${entry.caseId} is absent from the locked native corpus.`)
const expected = buildRecoveredNamingSnapshot(fixture, resaveFixture)
validateRecoveredNamingSnapshot(expected)
validateRecoveredNamingSnapshot(entry.reference)
validateRecoveredNamingSnapshot(entry.replay)
if (!same(entry.reference, expected) || !same(entry.replay, expected)) fail(`${entry.caseId} classification evidence is stale or the independent replay differs.`)
classifiedStages += expected.driftOrdinals.length
}
const countClassification = (classification) => recoveredNamingDecisions.filter((entry) => entry.classification === classification).length
const expectedSummary = { classifiedCases: recoveredNamingDecisions.length, classifiedStages, stableSemantics: countClassification('stable_semantics'), allowedEvolution: countClassification('allowed_evolution'), implementationDefects: countClassification('implementation_defect'), unknown: 0 }
if (!same(report.summary, expectedSummary)) fail('classification summary is inconsistent.')
const driftCases = oracle.cases.filter((candidate) => candidate.mutation.metrics.namingRestorationDriftStages > 0)
const totalDriftStages = driftCases.reduce((sum, candidate) => sum + candidate.mutation.metrics.namingRestorationDriftStages, 0)
console.log(JSON.stringify({
status: 'freecad-recovered-naming-classification-pass',
completedTasks: report.classifications.map(({ taskId }) => taskId),
cases: report.classifications.map(({ caseId, classification }) => ({ caseId, classification })),
independentRuns: report.nativeProbe.independentRuns,
classifiedCases: report.summary.classifiedCases,
classifiedStages: report.summary.classifiedStages,
remainingCases: driftCases.length - report.summary.classifiedCases,
remainingStages: totalDriftStages - report.summary.classifiedStages,
}, null, 2))

View File

@@ -6,13 +6,14 @@ import { validateCompositeMutationEvidence } from './freecad-composite-mutation-
const root = resolve(new URL('..', import.meta.url).pathname)
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const fail = (message) => { throw new Error(`FreeCAD TSN stage correlation: ${message}`) }
const [desktop, production, productionRegistry, composite, resave, mutations] = await Promise.all([
const [desktop, production, productionRegistry, composite, resave, mutations, recoveredNaming] = await Promise.all([
load('config/freecad-tsn-stage-correlation-oracle.json'),
load('config/freecad-isomorphic-provenance-verification.json'),
load('config/chrome-freecad-naming-production-verification.json'),
load('config/freecad-composite-history-elementmap-oracle.json'),
load('config/freecad-composite-history-resave-verification.json'),
load('config/freecad-parameter-mutation-report.json'),
load('config/freecad-recovered-naming-classification.json'),
])
const operations = ['cut', 'rotate', 'fillet', 'mirrored', 'linear-pattern']
if (desktop.schemaVersion !== 1 || desktop.baselineId !== 'freecad-1.1.1-tsn-stage-correlation' || desktop.freecadVersion !== '1.1.1' || desktop.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || desktop.status !== 'pass') fail('desktop oracle is not locked to FreeCAD 1.1.1.')
@@ -33,6 +34,18 @@ if (desktop.nativeDesktopResaveCovered !== true || desktop.summary?.mutationCase
for (const [index, mutation] of desktop.mutations.entries()) if (mutation.operation !== operations[index] || mutation.status !== 'pass' || mutation.parameterChanged !== true || mutation.downstreamShapeChanged !== true || mutation.restoredExactly !== true) fail(`desktop ${operations[index]} mutation did not edit, recompute and restore the chain.`)
if (composite.summary?.cases !== 30 || composite.summary?.passed !== 30 || resave.summary?.nativeDesktopResaveCases !== 30 || resave.summary?.roundtripNameDrift !== 0 || resave.summary?.resaveNameDrift !== 0) fail('the locked 30-case native desktop resave corpus is incomplete.')
const compositeMutation = validateCompositeMutationEvidence(composite)
const driftCases = composite.cases.filter((fixture) => fixture.mutation.metrics.namingRestorationDriftStages > 0)
const driftById = new Map(driftCases.map((fixture) => [fixture.id, fixture]))
const recoveredNamingClassifications = recoveredNaming.classifications || []
if (recoveredNaming.baseline?.freecadVersion !== desktop.freecadVersion || recoveredNaming.baseline?.commit !== desktop.gitCommit || recoveredNamingClassifications.length !== new Set(recoveredNamingClassifications.map(({ caseId }) => caseId)).size) fail('recovered naming classifications have invalid baseline or duplicate cases.')
let classifiedNamingDriftStages = 0
for (const entry of recoveredNamingClassifications) {
const fixture = driftById.get(entry.caseId)
if (!fixture || !['stable_semantics', 'allowed_evolution', 'implementation_defect'].includes(entry.classification) || entry.replayStable !== true) fail(`recovered naming classification ${entry.caseId} is invalid.`)
classifiedNamingDriftStages += fixture.mutation.metrics.namingRestorationDriftStages
}
const unclassifiedNamingDriftCases = compositeMutation.mutationNamingRestoreDriftCases - recoveredNamingClassifications.length
const unclassifiedNamingDriftStages = compositeMutation.mutationNamingRestoreDriftStages - classifiedNamingDriftStages
const resaveById = new Map((resave.cases || []).map((fixture) => [fixture.id, fixture]))
let compositeCaseReconciliations = 0
if (resaveById.size !== compositeMutation.mutationCases || resave.cases?.length !== compositeMutation.mutationCases) fail('the 30-case resave report is not joined one-to-one with mutation evidence.')
@@ -70,7 +83,7 @@ const blockers = [
`Native desktop builder stages missing from the sequential chain: ${proxyStageOperations.join(', ')}`,
`Native parameter-mutation family missing for production operation: ${missingNativeMutationOperations.join(', ')}`,
`${desktop.summary.mutationRestoreTopologyDriftCases}/5 edit/restore cases changed ${desktop.summary.mutationRestoreTopologyDriftStages} downstream semantic topology digests without wrong binding`,
`${compositeMutation.mutationNamingRestoreDriftCases}/30 native case mutations changed ${compositeMutation.mutationNamingRestoreDriftStages} recovered naming-stage digests while all 219 geometry stages restored`,
`${unclassifiedNamingDriftCases}/${compositeMutation.mutationNamingRestoreDriftCases} native naming-drift cases and ${unclassifiedNamingDriftStages}/${compositeMutation.mutationNamingRestoreDriftStages} recovered naming stages remain unclassified; ${recoveredNamingClassifications.length} case is classified from an independent native replay`,
`Only ${coveredTransitions.length}/${orderedOperationPairs} ordered production operation pairs are classified and replayed; ${orderedOperationPairs - coveredTransitions.length} remain unclassified for type compatibility`,
]
console.log(JSON.stringify({
@@ -80,7 +93,7 @@ console.log(JSON.stringify({
desktopStages: desktop.summary.stages,
nativeBuilderStages: desktop.summary.nativeBuilderStages,
proxyStageOperations,
compositeMutations: { cases: compositeMutation.mutationCases, passed: compositeMutation.mutationPassed, caseReconciliations: compositeCaseReconciliations, categoryCases: compositeMutation.categoryCases, stageRecords: compositeMutation.mutationStageRecords, phaseStageRecords: compositeMutation.mutationPhaseStageRecords, finalPropagationFailures: compositeMutation.mutationFinalPropagationFailures, geometryRestoreFailures: compositeMutation.mutationStageRestoreFailures, namingRestoreDriftCases: compositeMutation.mutationNamingRestoreDriftCases, namingRestoreDriftStages: compositeMutation.mutationNamingRestoreDriftStages },
compositeMutations: { cases: compositeMutation.mutationCases, passed: compositeMutation.mutationPassed, caseReconciliations: compositeCaseReconciliations, categoryCases: compositeMutation.categoryCases, stageRecords: compositeMutation.mutationStageRecords, phaseStageRecords: compositeMutation.mutationPhaseStageRecords, finalPropagationFailures: compositeMutation.mutationFinalPropagationFailures, geometryRestoreFailures: compositeMutation.mutationStageRestoreFailures, namingRestoreDriftCases: compositeMutation.mutationNamingRestoreDriftCases, namingRestoreDriftStages: compositeMutation.mutationNamingRestoreDriftStages, classifiedNamingDriftCases: recoveredNamingClassifications.length, classifiedNamingDriftStages, unclassifiedNamingDriftCases, unclassifiedNamingDriftStages },
mutations: { cases: desktop.summary.mutationCases, passed: desktop.summary.mutationPassed, nativeCorrelated: nativeMutationCorrelation.filter(({ complete }) => complete).length, missingNativeMutationOperations, restoreTopologyDriftCases: desktop.summary.mutationRestoreTopologyDriftCases, restoreTopologyDriftStages: desktop.summary.mutationRestoreTopologyDriftStages },
transitions: { registeredOperations: registeredOperations.length, orderedOperationPairs, covered: coveredTransitions.length, unclassified: orderedOperationPairs - coveredTransitions.length, coveredTransitions },
nativeDesktopResaveCases: resave.summary.nativeDesktopResaveCases + 1,

View File

@@ -6,10 +6,11 @@ import FreeCAD as App
import FreeCADGui as Gui
import Part
import Sketcher
from PySide import QtWidgets
from PySide import QtCore, QtWidgets
MARKER = "FREECAD_GUI_WORKFLOW_RESULT="
REQUESTED_STATE = os.environ.get("FREECAD_GUI_WORKFLOW_STATE", "success")
def progress(phase):
@@ -165,7 +166,693 @@ def counter(document, name):
return None
def active_transaction_state():
transaction = App.getActiveTransaction()
return {
"active": transaction is not None,
"name": str(transaction[0]) if transaction else "",
"id": int(transaction[1]) if transaction else None,
"documents": [
{
"name": str(document.Name),
"hasPendingTransaction": bool(document.HasPendingTransaction),
"undoCount": counter(document, "UndoCount"),
"redoCount": counter(document, "RedoCount"),
"undoNames": available_transactions(document, "getAvailableUndoNames"),
"redoNames": available_transactions(document, "getAvailableRedoNames"),
}
for document in App.listDocuments().values()
],
}
def disabled_side_effect_state(phase):
app_document = App.ActiveDocument
gui_document = Gui.activeDocument()
return {
"phase": phase,
"documents": sorted(str(name) for name in App.listDocuments()),
"documentCount": len(App.listDocuments()),
"activeDocument": str(app_document.Name) if app_document else "",
"activeGuiDocument": gui_document is not None,
"selection": selection_names(),
"taskPanel": {
"activeDialog": bool(Gui.Control.activeDialog()),
"inEdit": active_edit_name(),
},
"transaction": active_transaction_state(),
}
def focus_state():
focus = QtWidgets.QApplication.focusWidget()
return None if focus is None else {
"className": str(focus.metaObject().className()),
"objectName": str(focus.objectName()),
}
def active_body_name():
active_body = Gui.activeView().getActiveObject("pdbody") if Gui.activeDocument() else None
return str(getattr(active_body, "Name", "")) if active_body else ""
def run_disabled_workflow():
progress("activate-workbench:start")
Gui.activateWorkbench("PartDesignWorkbench")
flush_gui()
progress("activate-workbench:done")
Gui.Selection.clearSelection()
flush_gui()
command = Gui.Command.get("PartDesign_Pad")
actions = list(command.getAction()) if command else []
before_side_effects = disabled_side_effect_state("before")
before = {
"activeWorkbench": str(Gui.activeWorkbench().name()),
"commandRegistered": command is not None,
"commandActive": bool(command.isActive()) if command else False,
"actionCount": len(actions),
"actionEnabled": bool(actions[0].isEnabled()) if actions else None,
"sideEffects": before_side_effects,
}
action_signal_count = [0]
if actions:
actions[0].triggered.connect(lambda _checked=False: action_signal_count.__setitem__(0, action_signal_count[0] + 1))
progress("disabled-action-trigger:start")
actions[0].trigger()
flush_gui()
progress("disabled-action-trigger:done")
after_action_trigger = disabled_side_effect_state("after-action-trigger")
progress("disabled-run-command:start")
Gui.runCommand("PartDesign_Pad", 0)
flush_gui()
progress("disabled-run-command:done")
after_run_command = disabled_side_effect_state("after-run-command")
after = {
"commandActive": bool(command.isActive()) if command else False,
"actionEnabled": bool(actions[0].isEnabled()) if actions else None,
"sideEffects": after_run_command,
}
def stable_side_effects(value):
return {key: item for key, item in value.items() if key != "phase"}
unchanged_after_action = stable_side_effects(after_action_trigger) == stable_side_effects(before_side_effects)
unchanged_after_run_command = stable_side_effects(after_run_command) == stable_side_effects(before_side_effects)
version = App.Version()
result = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1",
"freecadVersion": ".".join(str(value) for value in version[:3]),
"gitCommit": str(version[7]) if len(version) > 7 else "",
"workflowId": "partdesign-pad-task",
"commandId": "PartDesign_Pad",
"state": "disabled",
"before": before,
"activationAttempts": {
"actionTrigger": {
"requested": bool(actions),
"triggeredSignalCount": action_signal_count[0],
"sideEffectsUnchanged": unchanged_after_action,
"after": after_action_trigger,
},
"runCommand": {
"requested": True,
"sideEffectsUnchanged": unchanged_after_run_command,
"after": after_run_command,
},
},
"after": after,
"success": bool(
command is not None
and actions
and not before["commandActive"]
and before["actionEnabled"] is False
and action_signal_count[0] == 0
and unchanged_after_action
and unchanged_after_run_command
and after_run_command["documentCount"] == 0
and not after_run_command["taskPanel"]["activeDialog"]
and not after_run_command["taskPanel"]["inEdit"]
and not after_run_command["transaction"]["active"]
and not after_run_command["transaction"]["documents"]
),
}
progress("result:done")
emit(result, 0 if result["success"] else 2)
def accept_task_capturing_warning():
captured = []
polls = [0]
def capture_warning():
modal = QtWidgets.QApplication.activeModalWidget()
if isinstance(modal, QtWidgets.QMessageBox):
captured.append({
"title": str(modal.windowTitle()),
"text": str(modal.text()),
"informativeText": str(modal.informativeText()),
"detailedText": str(modal.detailedText()),
"standardButtons": int(modal.standardButtons()),
})
button = modal.button(QtWidgets.QMessageBox.Ok)
if button:
button.click()
else:
modal.accept()
return
polls[0] += 1
if polls[0] < 200:
QtCore.QTimer.singleShot(5, capture_warning)
task = Gui.Control.activeTaskDialog()
if task is None:
return {"requested": False, "warnings": captured, "polls": polls[0]}
QtCore.QTimer.singleShot(0, capture_warning)
task.accept()
flush_gui()
return {"requested": True, "warnings": captured, "polls": polls[0]}
def run_failure_workflow():
progress("activate-workbench:start")
Gui.activateWorkbench("PartDesignWorkbench")
flush_gui()
progress("activate-workbench:done")
document = App.newDocument("PadGuiFailure")
document.UndoMode = 1
body = document.addObject("PartDesign::Body", "Body")
sketch = body.newObject("Sketcher::SketchObject", "Sketch")
points = [App.Vector(0, 0, 0), App.Vector(4, 0, 0), App.Vector(4, 3, 0)]
for index in range(2):
sketch.addGeometry(Part.LineSegment(points[index], points[index + 1]), False)
document.recompute()
Gui.activeView().setActiveObject("pdbody", body)
Gui.Selection.clearSelection()
Gui.Selection.addSelection(document.Name, sketch.Name)
flush_gui()
baseline = {
"objectNames": [str(obj.Name) for obj in document.Objects],
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
"selection": selection_names(),
"sketchGeometryCount": int(sketch.GeometryCount),
"sketchShapeValid": bool(sketch.Shape.isValid()),
"sketchState": [str(value) for value in sketch.State],
"transaction": active_transaction_state(),
}
command = Gui.Command.get("PartDesign_Pad")
actions = list(command.getAction()) if command else []
before = {
"activeWorkbench": str(Gui.activeWorkbench().name()),
"commandRegistered": command is not None,
"commandActive": bool(command.isActive()) if command else False,
"actionCount": len(actions),
"actionEnabled": bool(actions[0].isEnabled()) if actions else None,
"baseline": baseline,
}
progress("failure-run-command:start")
Gui.runCommand("PartDesign_Pad", 0)
flush_gui()
progress("failure-run-command:done")
pad = document.getObject("Pad")
opened = task_panel_state("failure-opened")
preview = {
"exists": pad is not None,
"typeId": str(pad.TypeId) if pad else "",
"profile": profile_name(pad) if pad else "",
"shapeNull": bool(pad.Shape.isNull()) if pad else None,
"shapeValid": bool(pad.Shape.isValid()) if pad and not pad.Shape.isNull() else False if pad else None,
"state": [str(value) for value in pad.State] if pad else [],
"statusString": str(pad.getStatusString()) if pad else "",
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
"transaction": active_transaction_state(),
}
progress("failure-accept:start")
diagnostic = accept_task_capturing_warning()
progress("failure-accept:done")
pad = document.getObject("Pad")
after_attempt = {
"taskPanel": task_panel_state("failure-after-accept"),
"objectNames": [str(obj.Name) for obj in document.Objects],
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
"padExists": pad is not None,
"padShapeNull": bool(pad.Shape.isNull()) if pad else None,
"padState": [str(value) for value in pad.State] if pad else [],
"padStatusString": str(pad.getStatusString()) if pad else "",
"transaction": active_transaction_state(),
}
progress("failure-cleanup:start")
task = Gui.Control.activeTaskDialog()
cleanup_requested = task is not None
if task:
task.reject()
flush_gui()
document.recompute()
flush_gui()
progress("failure-cleanup:done")
after_cleanup = {
"rejectRequested": cleanup_requested,
"taskPanel": task_panel_state("failure-cleaned"),
"objectNames": [str(obj.Name) for obj in document.Objects],
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
"padExists": document.getObject("Pad") is not None,
"selection": selection_names(),
"sketchGeometryCount": int(sketch.GeometryCount),
"sketchShapeValid": bool(sketch.Shape.isValid()),
"sketchState": [str(value) for value in sketch.State],
"sketchVisible": bool(sketch.Visibility),
"transaction": active_transaction_state(),
}
warning = diagnostic["warnings"][0] if diagnostic["warnings"] else None
version = App.Version()
result = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1",
"freecadVersion": ".".join(str(value) for value in version[:3]),
"gitCommit": str(version[7]) if len(version) > 7 else "",
"workflowId": "partdesign-pad-task",
"commandId": "PartDesign_Pad",
"state": "failure",
"before": before,
"taskPanel": {"opened": opened, "preview": preview, "afterAttempt": after_attempt},
"diagnostic": diagnostic,
"after": after_cleanup,
"success": bool(
before["commandActive"]
and opened["activeDialog"]
and opened["inEdit"] == "Pad"
and preview["exists"]
and preview["shapeNull"]
and "Invalid" in preview["state"]
and diagnostic["requested"]
and warning
and warning["title"] == "Input error"
and warning["text"] == preview["statusString"]
and after_attempt["taskPanel"]["activeDialog"]
and after_attempt["taskPanel"]["inEdit"] == "Pad"
and after_attempt["padExists"]
and after_attempt["padShapeNull"]
and after_cleanup["rejectRequested"]
and not after_cleanup["taskPanel"]["activeDialog"]
and not after_cleanup["taskPanel"]["inEdit"]
and not after_cleanup["padExists"]
and after_cleanup["objectNames"] == baseline["objectNames"]
and after_cleanup["bodyTip"] == baseline["bodyTip"]
and after_cleanup["sketchGeometryCount"] == baseline["sketchGeometryCount"]
and after_cleanup["sketchShapeValid"] == baseline["sketchShapeValid"]
and not after_cleanup["transaction"]["active"]
),
}
progress("result:done")
App.closeDocument(document.Name)
emit(result, 0 if result["success"] else 2)
def run_cancel_workflow():
progress("activate-workbench:start")
Gui.activateWorkbench("PartDesignWorkbench")
flush_gui()
progress("activate-workbench:done")
document = App.newDocument("PadGuiCancel")
document.UndoMode = 1
body = document.addObject("PartDesign::Body", "Body")
sketch = body.newObject("Sketcher::SketchObject", "Sketch")
points = [
App.Vector(0, 0, 0),
App.Vector(4, 0, 0),
App.Vector(4, 3, 0),
App.Vector(0, 3, 0),
]
for index in range(4):
sketch.addGeometry(Part.LineSegment(points[index], points[(index + 1) % 4]), False)
document.recompute()
Gui.activeView().setActiveObject("pdbody", body)
Gui.Selection.clearSelection()
Gui.Selection.addSelection(document.Name, sketch.Name)
viewport_widgets = [
widget
for widget in Gui.getMainWindow().findChildren(QtWidgets.QWidget)
if str(widget.metaObject().className()) == "Gui::View3DInventorViewer"
]
if viewport_widgets:
viewport_widgets[0].setFocus(QtCore.Qt.OtherFocusReason)
flush_gui()
baseline = {
"objectNames": [str(obj.Name) for obj in document.Objects],
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
"activeBody": active_body_name(),
"selection": selection_names(),
"focus": focus_state(),
"sketchGeometryCount": int(sketch.GeometryCount),
"sketchShapeValid": bool(sketch.Shape.isValid()),
"transaction": active_transaction_state(),
}
command = Gui.Command.get("PartDesign_Pad")
actions = list(command.getAction()) if command else []
before = {
"activeWorkbench": str(Gui.activeWorkbench().name()),
"commandRegistered": command is not None,
"commandActive": bool(command.isActive()) if command else False,
"actionCount": len(actions),
"actionEnabled": bool(actions[0].isEnabled()) if actions else None,
"baseline": baseline,
}
progress("cancel-run-command:start")
Gui.runCommand("PartDesign_Pad", 0)
flush_gui()
progress("cancel-run-command:done")
opened = task_panel_state("cancel-opened")
pad = document.getObject("Pad")
if pad:
pad.Length = 17.5
document.recompute()
flush_gui()
preview = {
"exists": pad is not None,
"typeId": str(pad.TypeId) if pad else "",
"length": round(float(pad.Length.Value), 9) if pad else None,
"shapeValid": bool(pad.Shape.isValid()) if pad else None,
"solidCount": len(pad.Shape.Solids) if pad else None,
"volume": round(float(pad.Shape.Volume), 9) if pad else None,
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
"activeBody": active_body_name(),
"selection": selection_names(),
"focus": focus_state(),
"transaction": active_transaction_state(),
}
progress("cancel-reject:start")
task = Gui.Control.activeTaskDialog()
reject_requested = task is not None
if task:
task.reject()
flush_gui()
document.recompute()
flush_gui()
progress("cancel-reject:done")
after = {
"rejectRequested": reject_requested,
"taskPanel": task_panel_state("cancel-closed"),
"objectNames": [str(obj.Name) for obj in document.Objects],
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
"activeBody": active_body_name(),
"padExists": document.getObject("Pad") is not None,
"selection": selection_names(),
"focus": focus_state(),
"sketchGeometryCount": int(sketch.GeometryCount),
"sketchShapeValid": bool(sketch.Shape.isValid()),
"sketchVisible": bool(sketch.Visibility),
"transaction": active_transaction_state(),
}
version = App.Version()
result = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1",
"freecadVersion": ".".join(str(value) for value in version[:3]),
"gitCommit": str(version[7]) if len(version) > 7 else "",
"workflowId": "partdesign-pad-task",
"commandId": "PartDesign_Pad",
"state": "cancel",
"before": before,
"taskPanel": {"opened": opened, "preview": preview, "closed": after["taskPanel"]},
"after": after,
"nativeSelectionOutcome": "selected-profile-consumed",
"nativeFocusOutcome": "task-view-after-cancel",
"success": bool(
before["commandActive"]
and baseline["selection"] == ["Sketch"]
and baseline["activeBody"] == "Body"
and baseline["focus"] == {"className": "Gui::View3DInventorViewer", "objectName": ""}
and opened["activeDialog"]
and opened["inEdit"] == "Pad"
and preview["exists"]
and preview["typeId"] == "PartDesign::Pad"
and preview["length"] == 17.5
and preview["shapeValid"]
and preview["solidCount"] == 1
and abs(preview["volume"] - 210.0) < 1e-7
and preview["selection"] == []
and preview["activeBody"] == baseline["activeBody"]
and preview["transaction"]["active"]
and after["rejectRequested"]
and not after["taskPanel"]["activeDialog"]
and not after["taskPanel"]["inEdit"]
and not after["padExists"]
and after["objectNames"] == baseline["objectNames"]
and after["bodyTip"] == baseline["bodyTip"]
and after["activeBody"] == baseline["activeBody"]
and after["selection"] == []
and after["focus"] == {"className": "Gui::TaskView::TaskView", "objectName": "Tasks"}
and after["sketchGeometryCount"] == baseline["sketchGeometryCount"]
and after["sketchShapeValid"] == baseline["sketchShapeValid"]
and after["sketchVisible"]
and not after["transaction"]["active"]
),
}
progress("result:done")
App.closeDocument(document.Name)
emit(result, 0 if result["success"] else 2)
def recovery_document_state(document, phase):
body = document.getObject("Body")
sketch = document.getObject("Sketch")
pad = document.getObject("Pad")
return {
"phase": phase,
"objectNames": [str(obj.Name) for obj in document.Objects],
"bodyGroup": [str(obj.Name) for obj in body.Group] if body else [],
"bodyTip": str(getattr(body.Tip, "Name", "")) if body and body.Tip else "",
"sketchGeometryCount": int(sketch.GeometryCount) if sketch else None,
"sketchShapeValid": bool(sketch.Shape.isValid()) if sketch else None,
"padExists": pad is not None,
"padTypeId": str(pad.TypeId) if pad else "",
"padLength": round(float(pad.Length.Value), 9) if pad else None,
"padProfile": profile_name(pad) if pad else "",
"padShapeValid": bool(pad.Shape.isValid()) if pad and not pad.Shape.isNull() else False if pad else None,
"padSolidCount": len(pad.Shape.Solids) if pad and not pad.Shape.isNull() else 0 if pad else None,
"padFaceCount": len(pad.Shape.Faces) if pad and not pad.Shape.isNull() else 0 if pad else None,
"padEdgeCount": len(pad.Shape.Edges) if pad and not pad.Shape.isNull() else 0 if pad else None,
"padVertexCount": len(pad.Shape.Vertexes) if pad and not pad.Shape.isNull() else 0 if pad else None,
"padVolume": round(float(pad.Shape.Volume), 9) if pad and not pad.Shape.isNull() else 0 if pad else None,
"padState": [str(value) for value in pad.State] if pad else [],
"undoCount": counter(document, "UndoCount"),
"redoCount": counter(document, "RedoCount"),
"undoNames": available_transactions(document, "getAvailableUndoNames"),
"redoNames": available_transactions(document, "getAvailableRedoNames"),
"transaction": active_transaction_state(),
}
def run_recovery_workflow():
recovery_file = os.environ.get("FREECAD_GUI_WORKFLOW_RECOVERY_FILE", "")
if not recovery_file:
raise RuntimeError("FREECAD_GUI_WORKFLOW_RECOVERY_FILE is required")
progress("activate-workbench:start")
Gui.activateWorkbench("PartDesignWorkbench")
flush_gui()
progress("activate-workbench:done")
document = App.newDocument("PadGuiRecovery")
document.UndoMode = 1
body = document.addObject("PartDesign::Body", "Body")
sketch = body.newObject("Sketcher::SketchObject", "Sketch")
points = [
App.Vector(0, 0, 0),
App.Vector(4, 0, 0),
App.Vector(4, 3, 0),
App.Vector(0, 3, 0),
]
for index in range(2):
sketch.addGeometry(Part.LineSegment(points[index], points[index + 1]), False)
document.recompute()
Gui.activeView().setActiveObject("pdbody", body)
Gui.Selection.clearSelection()
Gui.Selection.addSelection(document.Name, sketch.Name)
flush_gui()
baseline = recovery_document_state(document, "open-wire-baseline")
progress("recovery-run-command:start")
Gui.runCommand("PartDesign_Pad", 0)
flush_gui()
progress("recovery-run-command:done")
initial_pad = document.getObject("Pad")
initial_failure = {
"taskPanel": task_panel_state("recovery-invalid-opened"),
"padShapeNull": bool(initial_pad.Shape.isNull()) if initial_pad else None,
"padState": [str(value) for value in initial_pad.State] if initial_pad else [],
"padStatusString": str(initial_pad.getStatusString()) if initial_pad else "",
"diagnostic": accept_task_capturing_warning(),
}
initial_failure["afterAttempt"] = task_panel_state("recovery-invalid-after-accept")
progress("recovery-repair:start")
sketch.addGeometry(Part.LineSegment(points[2], points[3]), False)
sketch.addGeometry(Part.LineSegment(points[3], points[0]), False)
document.recompute()
pad = document.getObject("Pad")
# FreeCAD exposes Gui::PrefQuantitySpinBox to PySide as a generic QWidget;
# rawValue is its double Qt property, while value expects Base::Quantity.
length_edit = Gui.getMainWindow().findChild(QtWidgets.QWidget, "lengthEdit")
if length_edit:
length_edit.setProperty("rawValue", 12.0)
flush_gui()
elif pad:
pad.Length = 12.0
document.recompute()
flush_gui()
repaired_preview = recovery_document_state(document, "repaired-preview")
repaired_preview["lengthInputUpdated"] = bool(
length_edit is not None and abs(float(length_edit.property("rawValue")) - 12.0) < 1e-9
)
repaired_preview["taskPanel"] = task_panel_state("recovery-repaired")
progress("recovery-repair:done")
progress("recovery-accept:start")
task = Gui.Control.activeTaskDialog()
accept_requested = task is not None
if task:
task.accept()
flush_gui()
document.recompute()
flush_gui()
progress("recovery-accept:done")
committed = recovery_document_state(document, "committed")
committed["acceptRequested"] = accept_requested
committed["taskPanel"] = task_panel_state("recovery-committed")
progress("recovery-undo:start")
document.undo()
document.recompute()
flush_gui()
undo = recovery_document_state(document, "undo")
progress("recovery-undo:done")
progress("recovery-redo:start")
document.redo()
document.recompute()
flush_gui()
redo = recovery_document_state(document, "redo")
progress("recovery-redo:done")
progress("recovery-save:start")
document.saveAs(recovery_file)
flush_gui()
saved = recovery_document_state(document, "saved")
saved["fileName"] = str(document.FileName)
document_name = document.Name
App.closeDocument(document_name)
reopened_document = App.openDocument(recovery_file)
reopened_document.recompute()
flush_gui()
reopened = recovery_document_state(reopened_document, "reopened")
reopened["fileName"] = str(reopened_document.FileName)
reopened_document.save()
App.closeDocument(reopened_document.Name)
resaved_document = App.openDocument(recovery_file)
resaved_document.recompute()
flush_gui()
resaved = recovery_document_state(resaved_document, "resaved-reopened")
resaved["fileName"] = str(resaved_document.FileName)
progress("recovery-save:done")
warning = initial_failure["diagnostic"]["warnings"][0] if initial_failure["diagnostic"]["warnings"] else None
def valid_pad_state(value):
return bool(
value["padExists"]
and value["bodyTip"] == "Pad"
and value["bodyGroup"] == ["Sketch", "Pad"]
and value["sketchGeometryCount"] == 4
and value["padTypeId"] == "PartDesign::Pad"
and value["padLength"] == 12
and value["padProfile"] == "Sketch"
and value["padShapeValid"]
and value["padSolidCount"] == 1
and value["padFaceCount"] == 6
and value["padEdgeCount"] == 12
and value["padVertexCount"] == 8
and abs(value["padVolume"] - 144.0) < 1e-7
and value["padState"] == ["Up-to-date"]
and not value["transaction"]["active"]
)
version = App.Version()
result = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1",
"freecadVersion": ".".join(str(value) for value in version[:3]),
"gitCommit": str(version[7]) if len(version) > 7 else "",
"workflowId": "partdesign-pad-task",
"commandId": "PartDesign_Pad",
"state": "recovery",
"before": baseline,
"initialFailure": initial_failure,
"repairedPreview": repaired_preview,
"committed": committed,
"undo": undo,
"redo": redo,
"persistence": {"saved": saved, "reopened": reopened, "resavedReopened": resaved},
"success": bool(
baseline["sketchGeometryCount"] == 2
and not baseline["padExists"]
and initial_failure["taskPanel"]["activeDialog"]
and initial_failure["taskPanel"]["inEdit"] == "Pad"
and initial_failure["padShapeNull"]
and "Invalid" in initial_failure["padState"]
and warning
and warning["title"] == "Input error"
and warning["text"] == "Wire is not closed."
and initial_failure["afterAttempt"]["activeDialog"]
and initial_failure["afterAttempt"]["inEdit"] == "Pad"
and repaired_preview["taskPanel"]["activeDialog"]
and repaired_preview["taskPanel"]["inEdit"] == "Pad"
and repaired_preview["lengthInputUpdated"]
and repaired_preview["padShapeValid"]
and repaired_preview["padVolume"] == 144
and committed["acceptRequested"]
and not committed["taskPanel"]["activeDialog"]
and not committed["taskPanel"]["inEdit"]
and valid_pad_state(committed)
and not undo["padExists"]
and undo["sketchGeometryCount"] == 2
and undo["bodyTip"] == ""
and undo["redoCount"] >= 1
and valid_pad_state(redo)
and redo["undoCount"] >= 1
and valid_pad_state(saved)
and saved["fileName"] == recovery_file
and valid_pad_state(reopened)
and reopened["fileName"] == recovery_file
and valid_pad_state(resaved)
and resaved["fileName"] == recovery_file
),
}
progress("result:done")
App.closeDocument(resaved_document.Name)
emit(result, 0 if result["success"] else 2)
try:
if REQUESTED_STATE == "disabled":
run_disabled_workflow()
if REQUESTED_STATE == "failure":
run_failure_workflow()
if REQUESTED_STATE == "cancel":
run_cancel_workflow()
if REQUESTED_STATE == "recovery":
run_recovery_workflow()
if REQUESTED_STATE != "success":
raise RuntimeError("Unsupported workflow state: " + REQUESTED_STATE)
progress("activate-workbench:start")
Gui.activateWorkbench("PartDesignWorkbench")
flush_gui()
@@ -264,7 +951,7 @@ try:
"gitCommit": str(version[7]) if len(version) > 7 else "",
"workflowId": "partdesign-pad-task",
"commandId": "PartDesign_Pad",
"state": "success",
"state": REQUESTED_STATE,
"before": before,
"taskPanel": {
"opened": opened,
@@ -294,7 +981,7 @@ except Exception as error:
"baselineId": "freecad-1.1.1",
"workflowId": "partdesign-pad-task",
"commandId": "PartDesign_Pad",
"state": "success",
"state": REQUESTED_STATE,
"success": False,
"error": {"type": type(error).__name__, "message": str(error)},
}, 2)

View File

@@ -0,0 +1,107 @@
const sha256 = (value) => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value)
const canonical = (value) => Array.isArray(value)
? value.map(canonical)
: value && typeof value === 'object'
? Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonical(entry)]))
: value
export const recoveredNamingDecisions = [
{
taskId: 'TSN-DRIFT-000',
caseId: 'partdesign-plain',
classification: 'allowed_evolution',
reasonCode: 'native-history-stabilizes-after-edit-restore',
rationale: 'FreeCAD changes the Body and Pad naming history after the first Length edit/restore while restoring geometry exactly; the evolved names repeat deterministically and remain stable through save, reopen, and resave.',
},
{
taskId: 'TSN-DRIFT-001',
caseId: 'partdesign-midplane',
classification: 'allowed_evolution',
reasonCode: 'native-history-stabilizes-after-edit-restore',
rationale: 'FreeCAD applies the same deterministic history evolution to a Midplane Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
},
]
export const same = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right))
const namingFingerprint = (record) => ({
relationDigest: record.relationDigest,
semanticNameDigest: record.semanticNameDigest,
})
const phaseFingerprint = (record) => ({
geometryDigest: record.shape.geometryDigest,
relationDigest: record.relationDigest,
semanticNameDigest: record.semanticNameDigest,
})
export const buildRecoveredNamingSnapshot = (fixture, resaveFixture) => {
const driftOrdinals = fixture?.mutation?.metrics?.namingRestorationDriftOrdinals
if (!Array.isArray(driftOrdinals) || driftOrdinals.length === 0) throw new Error(`${fixture?.id ?? 'unknown case'} has no recovered naming drift to classify.`)
if (!resaveFixture || !['initial', 'reopened', 'resaved'].every((phase) => Array.isArray(resaveFixture.stageCorrelations?.[phase]))) throw new Error(`${fixture.id} lacks native save/reopen/resave evidence.`)
const stages = driftOrdinals.map((ordinal) => {
const nominal = fixture.stages?.[ordinal]
const before = fixture.mutation.phases?.before?.[ordinal]
const edited = fixture.mutation.phases?.edited?.[ordinal]
const restored = fixture.mutation.phases?.restored?.[ordinal]
if (![nominal, before, edited, restored].every(Boolean)) throw new Error(`${fixture.id}/${ordinal} lacks a complete mutation phase.`)
const persistence = Object.fromEntries(['initial', 'reopened', 'resaved'].map((phase) => {
const record = resaveFixture.stageCorrelations[phase][ordinal]
if (!record) throw new Error(`${fixture.id}/${ordinal} lacks ${phase} persistence evidence.`)
return [phase, namingFingerprint(record)]
}))
return {
ordinal,
name: nominal.name,
typeId: nominal.typeId,
nominal: phaseFingerprint(nominal),
before: phaseFingerprint(before),
edited: phaseFingerprint(edited),
restored: phaseFingerprint(restored),
persistence,
}
})
return {
caseId: fixture.id,
contract: {
targetObject: fixture.mutation.contract.targetObject,
targetTypeId: fixture.mutation.contract.targetTypeId,
propertyPath: fixture.mutation.contract.propertyPath,
propertyType: fixture.mutation.contract.propertyType,
finalObject: fixture.mutation.contract.finalObject,
},
values: fixture.mutation.values,
driftOrdinals,
stages,
checks: {
propertyRestored: fixture.mutation.metrics.propertyRestored === true,
geometryRestored: stages.every(({ before, restored }) => before.geometryDigest === restored.geometryDigest),
namingEvolved: stages.every(({ before, restored }) => !same({ relationDigest: before.relationDigest, semanticNameDigest: before.semanticNameDigest }, { relationDigest: restored.relationDigest, semanticNameDigest: restored.semanticNameDigest })),
restoredMatchesNominal: stages.every(({ nominal, restored }) => same(nominal, restored)),
persistenceStable: resaveFixture.roundtripNameDrift === 0
&& resaveFixture.resaveNameDrift === 0
&& resaveFixture.nativeDesktopResaveCovered === true
&& stages.every(({ restored, persistence }) => ['initial', 'reopened', 'resaved'].every((phase) => same({ relationDigest: restored.relationDigest, semanticNameDigest: restored.semanticNameDigest }, persistence[phase]))),
},
}
}
export const validateRecoveredNamingSnapshot = (snapshot) => {
if (!snapshot || typeof snapshot.caseId !== 'string' || !snapshot.caseId) throw new Error('classification snapshot has no case identity.')
if (!Array.isArray(snapshot.driftOrdinals) || snapshot.driftOrdinals.length === 0 || snapshot.stages?.length !== snapshot.driftOrdinals.length) throw new Error(`${snapshot.caseId} has an invalid drift-stage set.`)
if (!same(snapshot.stages.map(({ ordinal }) => ordinal), snapshot.driftOrdinals)) throw new Error(`${snapshot.caseId} drift-stage identities are inconsistent.`)
for (const stage of snapshot.stages) {
if (!Number.isInteger(stage.ordinal) || !stage.name || !stage.typeId) throw new Error(`${snapshot.caseId} has an invalid stage identity.`)
for (const phase of ['nominal', 'before', 'edited', 'restored']) {
const value = stage[phase]
if (!sha256(value?.geometryDigest) || !sha256(value?.relationDigest) || !sha256(value?.semanticNameDigest)) throw new Error(`${snapshot.caseId}/${stage.name}/${phase} has an invalid fingerprint.`)
}
for (const phase of ['initial', 'reopened', 'resaved']) {
const value = stage.persistence?.[phase]
if (!sha256(value?.relationDigest) || !sha256(value?.semanticNameDigest)) throw new Error(`${snapshot.caseId}/${stage.name}/${phase} has invalid persistence evidence.`)
}
}
if (!Object.values(snapshot.checks ?? {}).every((value) => value === true)) throw new Error(`${snapshot.caseId} does not satisfy the allowed-evolution evidence contract.`)
}

View File

@@ -4,12 +4,13 @@ import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const outputPath = resolve(root, 'config/freecad-active-work-queue.json')
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const [gui, workflowPlan, composite, correlation, production] = await Promise.all([
const [gui, workflowPlan, composite, correlation, production, recoveredNaming] = await Promise.all([
load('.cache/freecad/reference-desktop-gui-commands.json'),
load('config/freecad-gui-workflow-plan.json'),
load('config/freecad-composite-history-elementmap-oracle.json'),
load('config/freecad-tsn-stage-correlation-oracle.json'),
load('config/chrome-freecad-naming-production-verification.json'),
load('config/freecad-recovered-naming-classification.json'),
])
const guiShardTasks = gui.guiCommands.mergedShards.map((shard, index) => ({
@@ -39,15 +40,25 @@ const workflowTasks = workflowTitles.map((title, index) => ({
...(workflowProgress.get(`ORA-GUI-WF-${String(index).padStart(3, '0')}`).evidence.length > 0 ? { evidence: workflowProgress.get(`ORA-GUI-WF-${String(index).padStart(3, '0')}`).evidence } : {}),
exit: index === 0 ? 'one command family and its exact task are recorded' : 'one native workflow fixture and its focused assertion pass',
}))
const workflowClosed = workflowTasks.every((task) => task.status === 'completed')
const driftCases = composite.cases.filter((fixture) => fixture.mutation.metrics.namingRestorationDriftStages > 0)
const classifications = new Map(recoveredNaming.classifications.map((entry) => [entry.caseId, entry]))
if (classifications.size !== recoveredNaming.classifications.length) throw new Error('Recovered naming classifications contain duplicate cases.')
for (const entry of recoveredNaming.classifications) {
if (!driftCases.some(({ id }) => id === entry.caseId) || !['stable_semantics', 'allowed_evolution', 'implementation_defect'].includes(entry.classification)) throw new Error(`Recovered naming classification ${entry.caseId} is invalid.`)
}
const classifiedPrefix = driftCases.findIndex(({ id }) => !classifications.has(id))
const completedDriftCases = classifiedPrefix < 0 ? driftCases.length : classifiedPrefix
if (driftCases.slice(completedDriftCases).some(({ id }) => classifications.has(id))) throw new Error('Recovered naming classifications must form a contiguous serial prefix.')
const driftTasks = driftCases.map((fixture, index) => ({
id: `TSN-DRIFT-${String(index).padStart(3, '0')}`,
title: `Classify recovered naming evolution for ${fixture.id}`,
status: 'pending',
status: index < completedDriftCases ? 'completed' : workflowClosed && index === completedDriftCases ? 'in_progress' : 'pending',
dependencies: index === 0 ? ['ORA-GUI-WF-006'] : [`TSN-DRIFT-${String(index - 1).padStart(3, '0')}`],
caseId: fixture.id,
driftStages: fixture.mutation.metrics.namingRestorationDriftStages,
driftOrdinals: fixture.mutation.metrics.namingRestorationDriftOrdinals,
...(classifications.has(fixture.id) ? { classification: classifications.get(fixture.id).classification, evidence: ['config/freecad-recovered-naming-classification.json', classifications.get(fixture.id).reasonCode] } : {}),
exit: 'the case is classified as stable semantics, allowed evolution, or an implementation defect',
}))
const productionDriftTasks = correlation.mutations
@@ -86,8 +97,8 @@ const closureTasks = [
]
const milestones = [
{ id: 'ORA-GUI-BASELINE', exactTask: 'EX-ORA-01', status: 'completed', tasks: [...setupTasks, ...guiShardTasks, ...closureTasks] },
{ id: 'ORA-GUI-WORKFLOWS', exactTask: 'EX-ORA-01', status: 'in_progress', tasks: workflowTasks },
{ id: 'TSN-RECOVERY-DRIFT', exactTask: 'EX-TSN-04', status: 'pending', tasks: [...driftTasks, ...productionDriftTasks] },
{ id: 'ORA-GUI-WORKFLOWS', exactTask: 'EX-ORA-01', status: workflowClosed ? 'completed' : 'in_progress', tasks: workflowTasks },
{ id: 'TSN-RECOVERY-DRIFT', exactTask: 'EX-TSN-04', status: workflowClosed ? 'in_progress' : 'pending', tasks: [...driftTasks, ...productionDriftTasks] },
{ id: 'TSN-ORDERED-PAIRS', exactTask: 'EX-TSN-04', status: 'pending', tasks: transitionTasks },
]
const allTasks = milestones.flatMap((milestone) => milestone.tasks)
@@ -97,7 +108,7 @@ if (!nextTask) throw new Error('FreeCAD active work queue requires one in-progre
const queue = {
schemaVersion: 1,
baseline: { freecadVersion: '1.1.1', commit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' },
updatedAt: '2026-08-14',
updatedAt: '2026-08-15',
generatedBy: 'scripts/generate-freecad-active-work-queue.mjs',
policy: {
maxInProgress: 1,

View File

@@ -1,5 +1,5 @@
import { spawnSync } from 'node:child_process'
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
@@ -8,63 +8,81 @@ const executable = resolve(root, '.cache/freecad/install-desktop/bin/FreeCAD')
const probe = resolve(root, 'scripts/freecad-gui-workflow-oracle.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const outputDirectory = resolve(root, '.cache/freecad/gui-workflow-oracle')
const resultFile = resolve(outputDirectory, 'partdesign-pad-success-result.json')
const reportPath = resolve(root, 'config/freecad-gui-workflow-oracle.json')
const configDirectory = resolve(outputDirectory, 'config')
const userConfig = resolve(configDirectory, 'user.cfg')
const systemConfig = resolve(configDirectory, 'system.cfg')
const marker = 'FREECAD_GUI_WORKFLOW_RESULT='
const timeoutMs = Number(process.env.FREECAD_GUI_WORKFLOW_TIMEOUT_MS || 120_000)
const fail = (message) => { throw new Error(`FreeCAD GUI workflow oracle: ${message}`) }
await mkdir(outputDirectory, { recursive: true })
mkdirSync(configDirectory, { recursive: true })
const emptyConfig = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n<FCParameters><FCParamGroup Name="Root"/></FCParameters>\n'
writeFileSync(userConfig, emptyConfig)
writeFileSync(systemConfig, emptyConfig)
const runtimeEnv = {
...process.env,
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',
FREECAD_GUI_WORKFLOW_RESULT_FILE: resultFile,
const runProbe = (state) => {
const resultFile = resolve(outputDirectory, `partdesign-pad-${state}-result.json`)
const recoveryFile = resolve(outputDirectory, 'partdesign-pad-recovery.FCStd')
const configDirectory = resolve(outputDirectory, `config-${state}`)
const userConfig = resolve(configDirectory, 'user.cfg')
const systemConfig = resolve(configDirectory, 'system.cfg')
mkdirSync(configDirectory, { recursive: true })
writeFileSync(userConfig, emptyConfig)
writeFileSync(systemConfig, emptyConfig)
rmSync(resultFile, { force: true })
if (state === 'recovery') rmSync(recoveryFile, { force: true })
const runtimeEnv = {
...process.env,
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',
FREECAD_GUI_WORKFLOW_STATE: state,
FREECAD_GUI_WORKFLOW_RESULT_FILE: resultFile,
FREECAD_GUI_WORKFLOW_RECOVERY_FILE: recoveryFile,
}
const execution = spawnSync('xvfb-run', ['-a', executable, '--user-cfg', userConfig, '--system-cfg', systemConfig, '--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), probe], {
cwd: root,
encoding: 'utf8',
maxBuffer: 32 * 1024 * 1024,
timeout: timeoutMs,
env: runtimeEnv,
})
const output = `${execution.stdout ?? ''}\n${execution.stderr ?? ''}`
const markerLine = output.split(/\r?\n/).find((line) => line.startsWith(marker))
const resultBytes = (() => { try { return readFileSync(resultFile, 'utf8') } catch { return '' } })()
if (execution.error || (execution.status !== 0 && !resultBytes && !markerLine)) fail(`${state} native probe exited with ${execution.status}: ${[execution.error?.message, output.trim()].filter(Boolean).join('\n')}`)
const result = JSON.parse(resultBytes || markerLine.slice(marker.length))
if (result.freecadVersion !== '1.1.1' || result.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail(`${state} native baseline mismatch: ${JSON.stringify(result)}`)
if (result.workflowId !== 'partdesign-pad-task' || result.commandId !== 'PartDesign_Pad' || result.state !== state || result.success !== true) fail(`Pad ${state} workflow failed: ${JSON.stringify(result)}`)
return result
}
const execution = spawnSync('xvfb-run', ['-a', executable, '--user-cfg', userConfig, '--system-cfg', systemConfig, '--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), probe], {
cwd: root,
encoding: 'utf8',
maxBuffer: 32 * 1024 * 1024,
timeout: timeoutMs,
env: runtimeEnv,
})
const output = `${execution.stdout ?? ''}\n${execution.stderr ?? ''}`
const markerLine = output.split(/\r?\n/).find((line) => line.startsWith(marker))
const resultBytes = (() => { try { return readFileSync(resultFile, 'utf8') } catch { return '' } })()
if (execution.error || (execution.status !== 0 && !resultBytes && !markerLine)) fail(`native probe exited with ${execution.status}: ${[execution.error?.message, output.trim()].filter(Boolean).join('\n')}`)
const result = JSON.parse(resultBytes || markerLine.slice(marker.length))
if (result.freecadVersion !== '1.1.1' || result.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail(`native baseline mismatch: ${JSON.stringify(result)}`)
if (result.workflowId !== 'partdesign-pad-task' || result.commandId !== 'PartDesign_Pad' || result.state !== 'success' || result.success !== true) fail(`Pad success workflow failed: ${JSON.stringify(result)}`)
const success = runProbe('success')
const disabled = runProbe('disabled')
const failure = runProbe('failure')
const cancel = runProbe('cancel')
const recovery = runProbe('recovery')
const report = {
schemaVersion: 1,
baseline: {
freecadVersion: result.freecadVersion,
commit: result.gitCommit,
freecadVersion: success.freecadVersion,
commit: success.gitCommit,
profile: 'desktop-xvfb-isolated-config',
},
generatedBy: './npmw run probe:freecad-gui-workflow',
checkedBy: './npmw run check:freecad-gui-workflow',
family: {
id: result.workflowId,
primaryCommand: result.commandId,
id: success.workflowId,
primaryCommand: success.commandId,
exactTask: 'EX-UI-03',
source: '.cache/freecad/FreeCAD/src/Mod/PartDesign/Gui/Command.cpp',
},
workflows: {
success: result,
success,
disabled,
failure,
cancel,
recovery,
},
remainingStates: ['disabled', 'failure', 'cancel', 'recovery'],
remainingStates: [],
exactPromotionReady: false,
}
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
@@ -72,10 +90,19 @@ console.log(JSON.stringify({
status: 'freecad-gui-workflow-generated',
family: report.family.id,
command: report.family.primaryCommand,
state: result.state,
bodyTip: result.after.bodyTip,
volume: result.after.pad.volume,
taskOpened: result.taskPanel.opened.activeDialog,
taskClosed: !result.taskPanel.closed.activeDialog,
capturedStates: Object.keys(report.workflows),
bodyTip: success.after.bodyTip,
volume: success.after.pad.volume,
taskOpened: success.taskPanel.opened.activeDialog,
taskClosed: !success.taskPanel.closed.activeDialog,
disabledWithoutDocument: disabled.after.sideEffects.documentCount === 0,
disabledWithoutTask: !disabled.after.sideEffects.taskPanel.activeDialog,
disabledWithoutTransaction: !disabled.after.sideEffects.transaction.active,
failureDiagnostic: failure.diagnostic.warnings[0].text,
failureRolledBack: !failure.after.padExists && !failure.after.transaction.active,
cancelPreviewVolume: cancel.taskPanel.preview.volume,
cancelRolledBack: !cancel.after.padExists && !cancel.after.transaction.active,
recoveryVolume: recovery.persistence.resavedReopened.padVolume,
recoveryResaved: recovery.persistence.resavedReopened.padExists,
report: 'config/freecad-gui-workflow-oracle.json',
}, null, 2))

View File

@@ -0,0 +1,103 @@
import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { existsSync } from 'node:fs'
import { readFile, stat, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { buildRecoveredNamingSnapshot, recoveredNamingDecisions, same, validateRecoveredNamingSnapshot } from './freecad-recovered-naming-classification.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
const pythonPath = resolve(root, '.cache/freecad/sysroot/usr/lib/python3/dist-packages')
const harnessPath = resolve(root, 'scripts/freecad-composite-history-elementmap-oracle.py')
const oraclePath = resolve(root, 'config/freecad-composite-history-elementmap-oracle.json')
const resavePath = resolve(root, 'config/freecad-composite-history-resave-verification.json')
const outputPath = resolve(root, 'config/freecad-recovered-naming-classification.json')
const fail = (message) => { throw new Error(`FreeCAD recovered naming classification: ${message}`) }
if (!existsSync(executable)) fail(`native executable is missing: ${executable}`)
const [reference, referenceResave, harnessContent, harnessBytes] = await Promise.all([
readFile(oraclePath, 'utf8').then(JSON.parse),
readFile(resavePath, 'utf8').then(JSON.parse),
readFile(harnessPath),
stat(harnessPath).then(({ size }) => size),
])
const execution = spawnSync(executable, ['--python-path', pythonPath, harnessPath], {
cwd: root,
encoding: 'utf8',
timeout: 180_000,
maxBuffer: 80 * 1024 * 1024,
env: {
...process.env,
PYTHONPATH: `${pythonPath}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
LD_LIBRARY_PATH: `${resolve(root, '.cache/freecad/sysroot/usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
MATPLOTLIBRC: resolve(root, '.cache/freecad/sysroot/etc/matplotlibrc'),
MPLBACKEND: 'Agg',
},
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_COMPOSITE_HISTORY_ELEMENTMAP_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(`native replay exited with ${execution.status}: ${execution.error?.message || output.trim()}`)
const replay = JSON.parse(payload)
if (reference.freecadVersion !== '1.1.1' || reference.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || replay.freecadVersion !== reference.freecadVersion || replay.gitCommit !== reference.gitCommit || replay.status !== 'pass') fail('native baseline or replay status is invalid.')
const classifications = recoveredNamingDecisions.map((decision) => {
const referenceFixture = reference.cases.find(({ id }) => id === decision.caseId)
const referenceResaveFixture = referenceResave.cases.find(({ id }) => id === decision.caseId)
const replayFixture = replay.cases.find(({ id }) => id === decision.caseId)
if (![referenceFixture, referenceResaveFixture, replayFixture].every(Boolean)) fail(`${decision.caseId} is missing from reference or replay evidence.`)
const referenceSnapshot = buildRecoveredNamingSnapshot(referenceFixture, referenceResaveFixture)
const replaySnapshot = buildRecoveredNamingSnapshot(replayFixture, {
stageCorrelations: replayFixture.stageCorrelations,
roundtripNameDrift: replayFixture.roundtripNameDrift,
resaveNameDrift: replayFixture.resaveNameDrift,
nativeDesktopResaveCovered: replayFixture.nativeDesktopResaveCovered,
})
validateRecoveredNamingSnapshot(referenceSnapshot)
validateRecoveredNamingSnapshot(replaySnapshot)
if (!same(referenceSnapshot, replaySnapshot)) fail(`${decision.caseId} fingerprints differ between the locked reference and independent native replay.`)
return {
...decision,
reference: referenceSnapshot,
replay: replaySnapshot,
replayStable: true,
implementationDefect: decision.classification === 'implementation_defect',
}
})
const countClassification = (classification) => classifications.filter((entry) => entry.classification === classification).length
const classifiedStages = classifications.reduce((sum, entry) => sum + entry.reference.driftOrdinals.length, 0)
const report = {
schemaVersion: 1,
baseline: {
freecadVersion: reference.freecadVersion,
commit: reference.gitCommit,
oracleId: reference.baselineId,
},
generatedBy: './npmw run probe:freecad-recovered-naming-classification',
checkedBy: './npmw run check:freecad-recovered-naming-classification',
nativeProbe: {
path: 'scripts/freecad-composite-history-elementmap-oracle.py',
bytes: harnessBytes,
sha256: createHash('sha256').update(harnessContent).digest('hex'),
independentRuns: 2,
},
classifications,
summary: {
classifiedCases: classifications.length,
classifiedStages,
stableSemantics: countClassification('stable_semantics'),
allowedEvolution: countClassification('allowed_evolution'),
implementationDefects: countClassification('implementation_defect'),
unknown: 0,
},
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({
status: 'freecad-recovered-naming-classification-generated',
cases: report.classifications.map(({ taskId, caseId, classification, reference: snapshot }) => ({ taskId, caseId, classification, driftOrdinals: snapshot.driftOrdinals })),
independentRuns: report.nativeProbe.independentRuns,
output: 'config/freecad-recovered-naming-classification.json',
}, null, 2))