feat: classify recovered naming drift
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
49
scripts/check-freecad-recovered-naming-classification.mjs
Normal file
49
scripts/check-freecad-recovered-naming-classification.mjs
Normal 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))
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
107
scripts/freecad-recovered-naming-classification.mjs
Normal file
107
scripts/freecad-recovered-naming-classification.mjs
Normal 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.`)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
103
scripts/run-freecad-recovered-naming-classification.mjs
Normal file
103
scripts/run-freecad-recovered-naming-classification.mjs
Normal 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))
|
||||
Reference in New Issue
Block a user