feat: advance TSN drift and ordered pair evidence
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 04:44:23 -04:00
parent ea3706bd3d
commit 83cbc80971
28 changed files with 27182 additions and 15757 deletions

View File

@@ -0,0 +1,42 @@
import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-ordered-operation-pair-classification.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD ordered operation pair classification: ${message}`) }
const sha256 = (value) => createHash('sha256').update(value).digest('hex')
if (report.schemaVersion !== 1 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !/^8\./.test(report.baseline.occtVersion)) fail('baseline is invalid.')
for (const harness of [report.nativeProbe?.executor, report.nativeProbe?.matrix, report.nativeProbe?.resave]) {
const path = resolve(root, harness?.path ?? '')
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
if (bytes !== harness.bytes || sha256(content) !== harness.sha256) fail(`${harness.path} provenance is stale.`)
}
for (const artifact of report.nativeProbe?.artifacts ?? []) {
const path = resolve(root, 'native/occt-history/dist', artifact.name)
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
if (bytes !== artifact.bytes || sha256(content) !== artifact.sha256) fail(`${artifact.name} provenance is stale.`)
}
const expectedPairs = [
{ pair: 'fuse->fuse', secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [12, 11, 12] },
{ pair: 'fuse->cut', secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
{ pair: 'fuse->common', secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [10, 9, 10] },
{ pair: 'fuse->rotate', secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
]
if (report.nativeProbe?.artifacts?.length !== 3 || report.classifications?.length !== expectedPairs.length) fail('native artifacts or classification prefix is incomplete.')
for (const [index, entry] of report.classifications.entries()) {
const expected = expectedPairs[index]
const secondOperation = expected.pair.split('->')[1]
if (entry.taskId !== `TSN-PAIR-${expected.pair.replace('->', '-')}` || entry.pair !== expected.pair || entry.classification !== 'accepted' || entry.nativeDecision !== 'accepted' || entry.first?.operation !== 'fuse' || entry.first.builder !== 'BRepAlgoAPI_Fuse' || entry.first.inputCount !== 2 || entry.second?.operation !== secondOperation || entry.second.builder !== expected.secondBuilder || entry.second.inputCount !== expected.secondInputCount || entry.first.historyProvider !== 'occt-native' || entry.second.historyProvider !== 'occt-native') fail(`${expected.pair} decision contract is invalid.`)
if (entry.first.summary?.isValid !== true || entry.second.summary?.isValid !== true || entry.first.historyRecords < 1 || entry.second.historyRecords < 1 || entry.mutation?.changed !== true || entry.mutation?.restoredExactly !== true) fail(`${expected.pair} builder or mutation evidence is incomplete.`)
if (entry.mutation.scope !== 'second-operation-only' || entry.mutation.parameter !== expected.mutationParameter || JSON.stringify(entry.mutation.trajectory) !== JSON.stringify(expected.mutationTrajectory) || entry.mutation.beforeValue !== expected.mutationTrajectory[0] || entry.mutation.editedValue !== expected.mutationTrajectory[1] || entry.mutation.restoredValue !== expected.mutationTrajectory[2]) fail(`${expected.pair} mutation trajectory is invalid.`)
if (entry.mutation.editedEvidence?.operation !== secondOperation || entry.mutation.restoredEvidence?.operation !== secondOperation || entry.mutation.editedEvidence?.builder !== expected.secondBuilder || entry.mutation.restoredEvidence?.builder !== expected.secondBuilder || entry.mutation.editedEvidence?.inputCount !== expected.secondInputCount || entry.mutation.restoredEvidence?.inputCount !== expected.secondInputCount || entry.mutation.editedEvidence?.historyProvider !== 'occt-native' || entry.mutation.restoredEvidence?.historyProvider !== 'occt-native') fail(`${expected.pair} mutation history provider is invalid.`)
if (entry.second.namingSemanticSha256 !== entry.mutation.restoredEvidence?.namingSemanticSha256) fail(`${expected.pair} semantic naming evidence did not restore to nominal.`)
if (entry.naming?.upstreamEvidenceRestored !== true || entry.naming?.downstreamEvidence !== true || entry.naming?.jsonRoundtripStable !== true) fail(`${expected.pair} naming evidence is incomplete.`)
if (entry.persistence?.status !== 'pass' || entry.persistence.pair !== entry.pair || entry.persistence.freecadVersion !== '1.1.1' || !Object.values(entry.persistence.checks ?? {}).every(Boolean)) fail(`${expected.pair} FCStd persistence evidence is incomplete.`)
const phases = entry.persistence.phases
if (JSON.stringify(phases?.initial) !== JSON.stringify(phases?.reopened) || JSON.stringify(phases?.initial) !== JSON.stringify(phases?.resaved) || phases.initial.namingEvidenceSha256 !== entry.second.namingEvidenceSha256) fail(`${expected.pair} FCStd phases changed Shape or naming evidence.`)
}
const expectedSummary = { registeredOperations: 19, orderedPairs: 361, classifiedPairs: 4, accepted: 4, rejected: 0, unknown: 357 }
if (JSON.stringify(report.summary) !== JSON.stringify(expectedSummary)) fail('summary is inconsistent.')
console.log(JSON.stringify({ status: 'freecad-ordered-operation-pair-classification-pass', completedTasks: report.classifications.map(({ taskId }) => taskId), pairs: report.classifications.map(({ pair, classification }) => ({ pair, classification })), nativeBuilderRuns: report.classifications.length * 4, fcstdPhases: report.classifications.length * 3, remainingPairs: report.summary.unknown }, null, 2))

View File

@@ -0,0 +1,43 @@
import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
import { buildProductionDriftSnapshot, productionDriftDecisions, same, validateProductionDriftSnapshot } from './freecad-production-drift-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 production drift classification: ${message}`) }
const [report, oracle] = await Promise.all([
load('config/freecad-production-drift-classification.json'),
load('config/freecad-tsn-stage-correlation-oracle.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-tsn-stage-correlation-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 !== productionDriftDecisions.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 = productionDriftDecisions[index]
if (!same({ taskId: entry.taskId, operation: entry.operation, 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 expected = buildProductionDriftSnapshot(oracle, entry.operation)
validateProductionDriftSnapshot(expected)
validateProductionDriftSnapshot(entry.reference)
validateProductionDriftSnapshot(entry.replay)
if (!same(entry.reference, expected) || !same(entry.replay, expected)) fail(`${entry.operation} classification evidence is stale or the independent replay differs.`)
classifiedStages += expected.driftOrdinals.length
}
const driftCases = oracle.driftReplays.length
const countClassification = (classification) => productionDriftDecisions.filter((entry) => entry.classification === classification).length
const expectedSummary = { driftCases, classifiedCases: productionDriftDecisions.length, classifiedStages, stableSemantics: countClassification('stable_semantics'), allowedEvolution: countClassification('allowed_evolution'), implementationDefects: countClassification('implementation_defect'), unknown: driftCases - productionDriftDecisions.length }
if (!same(report.summary, expectedSummary)) fail('classification summary is inconsistent.')
console.log(JSON.stringify({
status: 'freecad-production-drift-classification-pass',
completedTasks: report.classifications.map(({ taskId }) => taskId),
cases: report.classifications.map(({ operation, classification }) => ({ operation, classification })),
independentRuns: report.nativeProbe.independentRuns,
classifiedCases: report.summary.classifiedCases,
classifiedStages: report.summary.classifiedStages,
remainingCases: report.summary.unknown,
}, null, 2))

View File

@@ -6,7 +6,7 @@ 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, recoveredNaming] = await Promise.all([
const [desktop, production, productionRegistry, composite, resave, mutations, recoveredNaming, productionDriftClassification, orderedPairClassification] = 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'),
@@ -14,6 +14,8 @@ const [desktop, production, productionRegistry, composite, resave, mutations, re
load('config/freecad-composite-history-resave-verification.json'),
load('config/freecad-parameter-mutation-report.json'),
load('config/freecad-recovered-naming-classification.json'),
load('config/freecad-production-drift-classification.json'),
load('config/freecad-ordered-operation-pair-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.')
@@ -77,13 +79,28 @@ const proxyStageOperations = desktop.stages.filter(({ nativeBuilder }) => native
if (JSON.stringify(proxyStageOperations) !== '["rotate","mirrored","linear-pattern"]' || desktop.summary?.nativeBuilderStages !== 2 || desktop.summary?.proxyStages !== 3 || desktop.exactCorrelationReady !== false) fail('desktop native-builder/proxy boundary drifted.')
const registeredOperations = productionRegistry.operations || []
const coveredTransitions = operations.slice(1).map((operation, index) => `${operations[index]}->${operation}`)
const productionDriftMutations = desktop.mutations.filter(({ restoreTopologyDriftStages }) => restoreTopologyDriftStages > 0)
const productionDriftByOperation = new Map(productionDriftMutations.map((mutation) => [mutation.operation, mutation]))
const productionDriftClassifications = productionDriftClassification.classifications || []
if (productionDriftClassification.baseline?.freecadVersion !== desktop.freecadVersion || productionDriftClassification.baseline?.commit !== desktop.gitCommit || productionDriftClassifications.length !== new Set(productionDriftClassifications.map(({ operation }) => operation)).size) fail('production drift classifications have invalid baseline or duplicate operations.')
let classifiedProductionDriftStages = 0
for (const entry of productionDriftClassifications) {
const mutation = productionDriftByOperation.get(entry.operation)
if (!mutation || !['stable_semantics', 'allowed_evolution', 'implementation_defect'].includes(entry.classification) || entry.replayStable !== true) fail(`production drift classification ${entry.operation} is invalid.`)
classifiedProductionDriftStages += mutation.restoreTopologyDriftStages
}
const unclassifiedProductionDriftCases = productionDriftMutations.length - productionDriftClassifications.length
const unclassifiedProductionDriftStages = desktop.summary.mutationRestoreTopologyDriftStages - classifiedProductionDriftStages
const orderedPairClassifications = orderedPairClassification.classifications || []
if (orderedPairClassification.baseline?.freecadVersion !== desktop.freecadVersion || orderedPairClassification.baseline?.commit !== desktop.gitCommit || orderedPairClassifications.length !== new Set(orderedPairClassifications.map(({ pair }) => pair)).size) fail('ordered operation pair classifications have invalid baseline or duplicate pairs.')
for (const entry of orderedPairClassifications) if (!['accepted', 'rejected'].includes(entry.classification) || entry.nativeDecision !== entry.classification) fail(`ordered operation pair classification ${entry.pair} is invalid.`)
const coveredTransitions = [...new Set([...operations.slice(1).map((operation, index) => `${operations[index]}->${operation}`), ...orderedPairClassifications.map(({ pair }) => pair)])]
const orderedOperationPairs = registeredOperations.length * registeredOperations.length
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`,
`${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`,
`${unclassifiedProductionDriftCases}/${productionDriftMutations.length} production restore-topology cases and ${unclassifiedProductionDriftStages}/${desktop.summary.mutationRestoreTopologyDriftStages} downstream stages remain unclassified; ${productionDriftClassifications.length} ${productionDriftClassifications.length === 1 ? 'case is' : 'cases are'} classified from an independent native replay`,
`${unclassifiedNamingDriftCases}/${compositeMutation.mutationNamingRestoreDriftCases} native naming-drift cases and ${unclassifiedNamingDriftStages}/${compositeMutation.mutationNamingRestoreDriftStages} recovered naming stages remain unclassified; ${recoveredNamingClassifications.length} ${recoveredNamingClassifications.length === 1 ? 'case is' : 'cases are'} 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({
@@ -94,7 +111,7 @@ console.log(JSON.stringify({
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, 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 },
mutations: { cases: desktop.summary.mutationCases, passed: desktop.summary.mutationPassed, nativeCorrelated: nativeMutationCorrelation.filter(({ complete }) => complete).length, missingNativeMutationOperations, restoreTopologyDriftCases: desktop.summary.mutationRestoreTopologyDriftCases, restoreTopologyDriftStages: desktop.summary.mutationRestoreTopologyDriftStages, classifiedRestoreTopologyDriftCases: productionDriftClassifications.length, classifiedRestoreTopologyDriftStages: classifiedProductionDriftStages, unclassifiedRestoreTopologyDriftCases: unclassifiedProductionDriftCases, unclassifiedRestoreTopologyDriftStages: unclassifiedProductionDriftStages },
transitions: { registeredOperations: registeredOperations.length, orderedOperationPairs, covered: coveredTransitions.length, unclassified: orderedOperationPairs - coveredTransitions.length, coveredTransitions },
nativeDesktopResaveCases: resave.summary.nativeDesktopResaveCases + 1,
wrongBindings: desktop.summary.wrongBindings,

View File

@@ -87,7 +87,7 @@ def relation_from_mapped_name(mapped_name):
def normalize_mapped_name(mapped_name):
# StringHasher IDs are process-local. Preserve the token grammar while
# removing only the volatile hash-table slot assigned in this process.
return re.sub(r":H[0-9a-fA-F]+", ":H#", mapped_name or "")
return re.sub(r":H-?[0-9a-fA-F]+", ":H#", mapped_name or "")
def source_element_name(mapped_name):

View File

@@ -0,0 +1,113 @@
import hashlib
import json
import os
import FreeCAD as App
import Import
import Part
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def version_text():
return ".".join(str(value) for value in App.Version()[:3])
def shape_snapshot(feature):
shape = feature.Shape
if shape.isNull():
raise RuntimeError("ordered pair produced a null Shape")
bounds = shape.BoundBox
return {
"valid": bool(shape.isValid()),
"solids": len(shape.Solids),
"faces": len(shape.Faces),
"edges": len(shape.Edges),
"vertices": len(shape.Vertexes),
"volume": round(float(shape.Volume), 7),
"area": round(float(shape.Area), 7),
"bounds": [round(float(value), 7) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)],
}
def phase_snapshot(document):
feature = document.getObject("PairResult")
if feature is None:
raise RuntimeError("PairResult is absent after FCStd persistence")
evidence = str(feature.NamingEvidence)
return {
"pair": str(feature.OrderedPair),
"namingEvidenceBytes": len(evidence.encode("utf-8")),
"namingEvidenceSha256": hashlib.sha256(evidence.encode("utf-8")).hexdigest(),
"shape": shape_snapshot(feature),
}
def collect():
step_path = os.environ["FREECAD_PAIR_STEP_PATH"]
evidence_path = os.environ["FREECAD_PAIR_EVIDENCE_PATH"]
pair = os.environ["FREECAD_ORDERED_PAIR"]
output_directory = os.environ["FREECAD_PAIR_OUTPUT_DIRECTORY"]
with open(evidence_path, "r", encoding="utf-8") as evidence_file:
evidence = evidence_file.read()
document = App.newDocument("FreeCadOrderedPairResave")
try:
Import.insert(step_path, document.Name)
document.recompute()
source_shapes = [obj.Shape for obj in document.Objects if hasattr(obj, "Shape") and not obj.Shape.isNull()]
if not source_shapes:
raise RuntimeError("STEP import produced no Shape")
result = document.addObject("Part::Feature", "PairResult")
result.Shape = source_shapes[0].copy() if len(source_shapes) == 1 else Part.makeCompound([shape.copy() for shape in source_shapes])
result.addProperty("App::PropertyString", "OrderedPair", "Parity")
result.addProperty("App::PropertyString", "NamingEvidence", "Parity")
result.OrderedPair = pair
result.NamingEvidence = evidence
document.recompute()
initial = phase_snapshot(document)
initial_path = os.path.join(output_directory, "ordered-pair.FCStd")
resaved_path = os.path.join(output_directory, "ordered-pair-resaved.FCStd")
document.saveAs(initial_path)
App.closeDocument(document.Name)
reopened = App.openDocument(initial_path)
reopened.recompute()
reopened_snapshot = phase_snapshot(reopened)
reopened.saveAs(resaved_path)
App.closeDocument(reopened.Name)
resaved = App.openDocument(resaved_path)
resaved.recompute()
resaved_snapshot = phase_snapshot(resaved)
App.closeDocument(resaved.Name)
return {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-ordered-operation-pair-resave",
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"pair": pair,
"phases": {"initial": initial, "reopened": reopened_snapshot, "resaved": resaved_snapshot},
"checks": {
"shapeStable": initial["shape"] == reopened_snapshot["shape"] and initial["shape"] == resaved_snapshot["shape"],
"namingEvidenceStable": initial["namingEvidenceSha256"] == reopened_snapshot["namingEvidenceSha256"] and initial["namingEvidenceSha256"] == resaved_snapshot["namingEvidenceSha256"],
},
"status": "pass",
}
finally:
for name in list(App.listDocuments().keys()):
App.closeDocument(name)
try:
report = collect()
except Exception as error:
report = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-ordered-operation-pair-resave",
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"status": "failed",
"errorType": type(error).__name__,
"error": str(error),
}
print("FREECAD_ORDERED_OPERATION_PAIR_RESAVE_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))

View File

@@ -0,0 +1,102 @@
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 productionDriftDecisions = [
{
taskId: 'TSN-PROD-DRIFT-000',
operation: 'cut',
classification: 'allowed_evolution',
reasonCode: 'native-downstream-topology-stabilizes-after-edit-restore',
rationale: 'FreeCAD deterministically evolves the downstream Fillet, Mirrored, and LinearPattern semantic topology after a Pocket Length edit/restore while restoring their geometry; the evolved topology and mapped names remain stable through save, reopen, and resave.',
},
{
taskId: 'TSN-PROD-DRIFT-001',
operation: 'rotate',
classification: 'allowed_evolution',
reasonCode: 'native-downstream-topology-stabilizes-after-edit-restore',
rationale: 'FreeCAD deterministically evolves the downstream Mirrored and LinearPattern semantic topology after a Rotate Angle edit/restore while restoring their geometry; the evolved topology and mapped names remain stable through save, reopen, and resave.',
},
]
export const same = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right))
const stageFingerprint = (driftReplay, ordinal, phase) => {
const mutation = driftReplay.mutation
const persistence = driftReplay.persistence?.[phase]?.[ordinal]
if (!persistence) throw new Error(`${driftReplay.operation}/${ordinal} lacks ${phase} persistence evidence.`)
return {
geometrySignature: persistence.geometrySignature,
semanticTopologyDigest: persistence.semanticTopologyDigest,
mappedNames: persistence.mappedNames,
mappedNameDigest: persistence.mappedNameDigest,
}
}
export const buildProductionDriftSnapshot = (report, operation) => {
const driftReplay = report?.driftReplays?.find((entry) => entry.operation === operation)
if (!driftReplay) throw new Error(`${operation} has no isolated production drift replay.`)
const mutation = driftReplay.mutation
const ordinals = mutation.restoreTopologyDriftOrdinals
if (!Array.isArray(ordinals) || ordinals.length === 0) throw new Error(`${operation} has no recovered production topology drift.`)
const stages = ordinals.map((ordinal) => ({
ordinal,
operation: driftReplay.persistence.initial[ordinal].operation,
name: driftReplay.persistence.initial[ordinal].name,
before: {
geometrySignature: mutation.beforeGeometrySignatures[ordinal],
semanticTopologyDigest: mutation.beforeTopologyDigests[ordinal],
},
edited: {
geometrySignature: mutation.editedGeometrySignatures[ordinal],
semanticTopologyDigest: mutation.editedTopologyDigests[ordinal],
},
restored: {
geometrySignature: mutation.restoredGeometrySignatures[ordinal],
semanticTopologyDigest: mutation.restoredTopologyDigests[ordinal],
},
persistence: Object.fromEntries(['initial', 'reopened', 'resaved'].map((phase) => [phase, stageFingerprint(driftReplay, ordinal, phase)])),
}))
return {
operation,
contract: {
featureName: mutation.featureName,
typeId: mutation.typeId,
propertyPath: mutation.propertyPath,
},
values: {
before: mutation.beforeParameter,
edited: mutation.editedParameter,
restored: mutation.restoredParameter,
},
driftOrdinals: ordinals,
stages,
checks: {
parameterRestored: mutation.parameterChanged === true && same(mutation.beforeParameter, mutation.restoredParameter),
geometryRestored: mutation.restoredGeometrically === true && mutation.restoredExactly === true && same(mutation.beforeGeometrySignatures, mutation.restoredGeometrySignatures),
topologyEvolved: stages.every(({ before, restored }) => before.semanticTopologyDigest !== restored.semanticTopologyDigest),
restoredMatchesInitial: driftReplay.checks?.restoredMatchesInitial === true && stages.every(({ restored, persistence }) => same(restored.geometrySignature, persistence.initial.geometrySignature) && restored.semanticTopologyDigest === persistence.initial.semanticTopologyDigest),
persistenceStable: driftReplay.checks?.geometryStable === true && driftReplay.checks?.topologyStable === true && driftReplay.checks?.mappedNamesStable === true && stages.every(({ persistence }) => ['reopened', 'resaved'].every((phase) => same(persistence.initial, persistence[phase]))),
},
}
}
export const validateProductionDriftSnapshot = (snapshot) => {
if (!snapshot || typeof snapshot.operation !== 'string' || !snapshot.operation) throw new Error('production drift snapshot has no operation identity.')
if (!Array.isArray(snapshot.driftOrdinals) || snapshot.driftOrdinals.length === 0 || snapshot.stages?.length !== snapshot.driftOrdinals.length) throw new Error(`${snapshot.operation} has an invalid drift-stage set.`)
if (!same(snapshot.stages.map(({ ordinal }) => ordinal), snapshot.driftOrdinals)) throw new Error(`${snapshot.operation} drift-stage identities are inconsistent.`)
for (const stage of snapshot.stages) {
if (!Number.isInteger(stage.ordinal) || !stage.operation || !stage.name) throw new Error(`${snapshot.operation} has an invalid stage identity.`)
for (const phase of ['before', 'edited', 'restored']) {
if (!Array.isArray(stage[phase]?.geometrySignature) || !sha256(stage[phase]?.semanticTopologyDigest)) throw new Error(`${snapshot.operation}/${stage.name}/${phase} has an invalid fingerprint.`)
}
for (const phase of ['initial', 'reopened', 'resaved']) {
const value = stage.persistence?.[phase]
if (!Array.isArray(value?.geometrySignature) || !value?.mappedNames || typeof value.mappedNames !== 'object' || !sha256(value?.semanticTopologyDigest) || !sha256(value?.mappedNameDigest)) throw new Error(`${snapshot.operation}/${stage.name}/${phase} has invalid persistence evidence.`)
}
}
if (!Object.values(snapshot.checks ?? {}).every((value) => value === true)) throw new Error(`${snapshot.operation} does not satisfy the allowed-evolution evidence contract.`)
}

View File

@@ -20,6 +20,69 @@ export const recoveredNamingDecisions = [
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.',
},
{
taskId: 'TSN-DRIFT-002',
caseId: 'partdesign-reverse',
classification: 'allowed_evolution',
reasonCode: 'native-history-stabilizes-after-edit-restore',
rationale: 'FreeCAD applies the same deterministic history evolution to a Reversed Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
},
{
taskId: 'TSN-DRIFT-003',
caseId: 'partdesign-taper',
classification: 'allowed_evolution',
reasonCode: 'native-history-stabilizes-after-edit-restore',
rationale: 'FreeCAD applies the same deterministic history evolution to a Tapered Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
},
{
taskId: 'TSN-DRIFT-004',
caseId: 'partdesign-twoside',
classification: 'allowed_evolution',
reasonCode: 'native-history-stabilizes-after-edit-restore',
rationale: 'FreeCAD applies the same deterministic history evolution to a Two Lengths Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
},
{
taskId: 'TSN-DRIFT-005',
caseId: 'partdesign-pocket',
classification: 'allowed_evolution',
reasonCode: 'native-history-stabilizes-after-edit-restore',
rationale: 'FreeCAD applies deterministic history evolution to an upstream Pad Length edit/restore: Body, Pad, and downstream Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
},
{
taskId: 'TSN-DRIFT-006',
caseId: 'partdesign-pocket-through',
classification: 'allowed_evolution',
reasonCode: 'native-history-stabilizes-after-edit-restore',
rationale: 'FreeCAD applies deterministic history evolution through a Through All Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
},
{
taskId: 'TSN-DRIFT-007',
caseId: 'partdesign-pocket-midplane',
classification: 'allowed_evolution',
reasonCode: 'native-history-stabilizes-after-edit-restore',
rationale: 'FreeCAD applies deterministic history evolution through a Midplane Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
},
{
taskId: 'TSN-DRIFT-008',
caseId: 'partdesign-pocket-twoside',
classification: 'allowed_evolution',
reasonCode: 'native-history-stabilizes-after-edit-restore',
rationale: 'FreeCAD applies deterministic history evolution through a Two Lengths Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
},
{
taskId: 'TSN-DRIFT-009',
caseId: 'partdesign-pocket-taper',
classification: 'allowed_evolution',
reasonCode: 'native-history-stabilizes-after-edit-restore',
rationale: 'FreeCAD applies deterministic history evolution through a Tapered Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
},
{
taskId: 'TSN-DRIFT-010',
caseId: 'partdesign-pocket-up-to-face',
classification: 'allowed_evolution',
reasonCode: 'native-history-stabilizes-after-edit-restore',
rationale: 'FreeCAD applies deterministic history evolution through an Up To Face Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket 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))

View File

@@ -1,6 +1,7 @@
import hashlib
import json
import os
import re
import tempfile
import FreeCAD as App
@@ -56,7 +57,8 @@ def mapped_names(feature):
indexed, _ = shape.getElementIndexedName(name, True)
except Exception:
indexed = ""
result[name] = mapped or indexed or name
normalized = re.sub(r":H-?[0-9a-fA-F]+", ":H#", mapped or indexed or name)
result[name] = re.sub(r":H:-?[0-9a-fA-F]+(?=[,;])", ":H:#", normalized)
return result
@@ -192,14 +194,19 @@ def rebuild_derived(document, features):
def topology_snapshot(features):
return [{
"operation": operation,
"name": feature.Name,
"shape": shape_snapshot(feature),
"mappedNameCount": len(mapped_names(feature)),
"mappedNameDigest": mapped_name_digest(feature),
"semanticTopologyDigest": semantic_topology_digest(feature),
} for operation, feature in zip(OPERATIONS, features)]
result = []
for operation, feature in zip(OPERATIONS, features):
names = mapped_names(feature)
result.append({
"operation": operation,
"name": feature.Name,
"shape": shape_snapshot(feature),
"mappedNames": names,
"mappedNameCount": len(names),
"mappedNameDigest": hashlib.sha256(json.dumps(names, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest(),
"semanticTopologyDigest": semantic_topology_digest(feature),
})
return result
def parameter_value(value):
@@ -230,10 +237,15 @@ def capture_mutation(document, features, operation, feature, property_name, edit
restored_parameter = parameter_value(getattr(feature, property_name))
restored = topology_snapshot(features)
before_digests = [stage["semanticTopologyDigest"] for stage in before]
edited_digests = [stage["semanticTopologyDigest"] for stage in edited]
restored_digests = [stage["semanticTopologyDigest"] for stage in restored]
drift_ordinals = [index for index, (before_digest, restored_digest) in enumerate(zip(before_digests, restored_digests)) if before_digest != restored_digest]
before_geometry = [geometry_signature(stage) for stage in before]
edited_geometry = [geometry_signature(stage) for stage in edited]
restored_geometry = [geometry_signature(stage) for stage in restored]
before_final = before[-1]["shape"]["brepSha256"]
edited_final = edited[-1]["shape"]["brepSha256"]
restored_geometrically = [geometry_signature(stage) for stage in before] == [geometry_signature(stage) for stage in restored]
restored_geometrically = before_geometry == restored_geometry
restored_topology_exactly = before_digests == restored_digests
passed = before_parameter != edited_parameter and before_final != edited_final and before_parameter == restored_parameter and restored_geometrically
return {
@@ -249,14 +261,98 @@ def capture_mutation(document, features, operation, feature, property_name, edit
"restoredExactly": before_parameter == restored_parameter and restored_geometrically,
"restoredGeometrically": restored_geometrically,
"restoredTopologyExactly": restored_topology_exactly,
"restoreTopologyDriftStages": sum(1 for before_digest, restored_digest in zip(before_digests, restored_digests) if before_digest != restored_digest),
"restoreTopologyDriftStages": len(drift_ordinals),
"restoreTopologyDriftOrdinals": drift_ordinals,
"beforeGeometrySignatures": before_geometry,
"editedGeometrySignatures": edited_geometry,
"restoredGeometrySignatures": restored_geometry,
"beforeTopologyDigests": before_digests,
"editedTopologyDigests": [stage["semanticTopologyDigest"] for stage in edited],
"editedTopologyDigests": edited_digests,
"restoredTopologyDigests": restored_digests,
"status": "pass" if passed else "failed",
}
def mutation_specs(features):
pocket, rotated, fillet, mirrored, pattern = features
return [
("cut", pocket, "Length", 2.5),
("rotate", rotated, "Angle", 22.5),
("fillet", fillet, "Radius", 0.35),
("mirrored", mirrored, "PlaneNormal", App.Vector(0, 0, 1)),
("linear-pattern", pattern, "Occurrences", 3),
]
def persistence_projection(features):
return [{
"operation": stage["operation"],
"name": stage["name"],
"geometrySignature": geometry_signature(stage),
"semanticTopologyDigest": stage["semanticTopologyDigest"],
"mappedNames": stage["mappedNames"],
"mappedNameDigest": stage["mappedNameDigest"],
} for stage in topology_snapshot(features)]
def collect_drift_replay(target_index):
target_operation = OPERATIONS[target_index]
document = App.newDocument("TsnStageCorrelation{}Replay".format(target_operation.title()))
try:
features = create_chain(document)
rebuild_derived(document, features)
mutations = []
for operation, feature, property_name, edited_value in mutation_specs(features)[:target_index + 1]:
mutations.append(capture_mutation(document, features, operation, feature, property_name, edited_value))
target_mutation = mutations[-1]
feature_names = [feature.Name for feature in features]
with tempfile.TemporaryDirectory(prefix="freecad-tsn-{}-drift-".format(target_operation)) as directory:
initial_path = os.path.join(directory, "{}-restored.FCStd".format(target_operation))
resaved_path = os.path.join(directory, "{}-restored-resaved.FCStd".format(target_operation))
initial = persistence_projection(features)
document.saveAs(initial_path)
App.closeDocument(document.Name)
reopened = App.openDocument(initial_path)
reopened.recompute()
reopened_features = [reopened.getObject(name) for name in feature_names]
reopened_projection = persistence_projection(reopened_features)
reopened.saveAs(resaved_path)
App.closeDocument(reopened.Name)
resaved = App.openDocument(resaved_path)
resaved.recompute()
resaved_features = [resaved.getObject(name) for name in feature_names]
resaved_projection = persistence_projection(resaved_features)
App.closeDocument(resaved.Name)
initial_topology = [stage["semanticTopologyDigest"] for stage in initial]
reopened_topology = [stage["semanticTopologyDigest"] for stage in reopened_projection]
resaved_topology = [stage["semanticTopologyDigest"] for stage in resaved_projection]
initial_names = [stage["mappedNameDigest"] for stage in initial]
reopened_names = [stage["mappedNameDigest"] for stage in reopened_projection]
resaved_names = [stage["mappedNameDigest"] for stage in resaved_projection]
initial_geometry = [stage["geometrySignature"] for stage in initial]
reopened_geometry = [stage["geometrySignature"] for stage in reopened_projection]
resaved_geometry = [stage["geometrySignature"] for stage in resaved_projection]
return {
"operation": target_operation,
"mutation": target_mutation,
"persistence": {
"initial": initial,
"reopened": reopened_projection,
"resaved": resaved_projection,
},
"checks": {
"restoredMatchesInitial": target_mutation["restoredTopologyDigests"] == initial_topology,
"geometryStable": initial_geometry == reopened_geometry and initial_geometry == resaved_geometry,
"topologyStable": initial_topology == reopened_topology and initial_topology == resaved_topology,
"mappedNamesStable": initial_names == reopened_names and initial_names == resaved_names,
},
}
finally:
for name in list(App.listDocuments().keys()):
if name.startswith("TsnStageCorrelation{}Replay".format(target_operation.title())):
App.closeDocument(name)
def stage_report(operation, feature):
sources, relations = history_sources(feature)
names = mapped_names(feature)
@@ -279,14 +375,7 @@ def collect():
try:
features = create_chain(document)
rebuild_derived(document, features)
pocket, rotated, fillet, mirrored, pattern = features
mutations = [
capture_mutation(document, features, "cut", pocket, "Length", 2.5),
capture_mutation(document, features, "rotate", rotated, "Angle", 22.5),
capture_mutation(document, features, "fillet", fillet, "Radius", 0.35),
capture_mutation(document, features, "mirrored", mirrored, "PlaneNormal", App.Vector(0, 0, 1)),
capture_mutation(document, features, "linear-pattern", pattern, "Occurrences", 3),
]
mutations = [capture_mutation(document, features, operation, feature, property_name, edited_value) for operation, feature, property_name, edited_value in mutation_specs(features)]
stages = [stage_report(operation, feature) for operation, feature in zip(OPERATIONS, features)]
with tempfile.TemporaryDirectory(prefix="freecad-tsn-stage-correlation-") as directory:
initial_path = os.path.join(directory, "tsn-stage-correlation.FCStd")
@@ -312,6 +401,7 @@ def collect():
unexplained_relations += sum(1 for source in stage["historySources"] if source not in known_sources)
if stage["mappedNameCount"] <= 0:
wrong_bindings += 1
drift_replays = [collect_drift_replay(index) for index, mutation in enumerate(mutations) if mutation["restoreTopologyDriftStages"] > 0]
with open(__file__, "rb") as harness_file:
harness_content = harness_file.read()
return {
@@ -324,6 +414,7 @@ def collect():
"reopenedStages": reopened_stages,
"resavedStages": resaved_stages,
"mutations": mutations,
"driftReplays": drift_replays,
"summary": {
"stages": len(stages),
"nativeBuilderStages": sum(1 for stage in stages if stage["nativeBuilder"]),

View File

@@ -4,13 +4,15 @@ 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, recoveredNaming] = await Promise.all([
const [gui, workflowPlan, composite, correlation, production, recoveredNaming, productionDriftClassification, orderedPairClassification] = 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'),
load('config/freecad-production-drift-classification.json'),
load('config/freecad-ordered-operation-pair-classification.json'),
])
const guiShardTasks = gui.guiCommands.mergedShards.map((shard, index) => ({
@@ -61,26 +63,47 @@ const driftTasks = driftCases.map((fixture, index) => ({
...(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 recoveredNamingClosed = completedDriftCases === driftCases.length
const productionDriftClassifications = new Map(productionDriftClassification.classifications.map((entry) => [entry.operation, entry]))
if (productionDriftClassifications.size !== productionDriftClassification.classifications.length) throw new Error('Production drift classifications contain duplicate operations.')
const productionDriftMutations = correlation.mutations.filter((mutation) => mutation.restoreTopologyDriftStages > 0)
const productionClassifiedPrefix = productionDriftMutations.findIndex(({ operation }) => !productionDriftClassifications.has(operation))
const completedProductionDrifts = productionClassifiedPrefix < 0 ? productionDriftMutations.length : productionClassifiedPrefix
if (productionDriftMutations.slice(completedProductionDrifts).some(({ operation }) => productionDriftClassifications.has(operation))) throw new Error('Production drift classifications must form a contiguous serial prefix.')
for (const entry of productionDriftClassification.classifications) {
if (!productionDriftMutations.some(({ operation }) => operation === entry.operation) || !['stable_semantics', 'allowed_evolution', 'implementation_defect'].includes(entry.classification)) throw new Error(`Production drift classification ${entry.operation} is invalid.`)
}
const productionDriftTasks = correlation.mutations
.filter((mutation) => mutation.restoreTopologyDriftStages > 0)
.map((mutation, index) => ({
id: `TSN-PROD-DRIFT-${String(index).padStart(3, '0')}`,
title: `Classify production restore topology drift for ${mutation.operation}`,
status: 'pending',
status: index < completedProductionDrifts ? 'completed' : recoveredNamingClosed && index === completedProductionDrifts ? 'in_progress' : 'pending',
dependencies: [index === 0 ? driftTasks.at(-1).id : `TSN-PROD-DRIFT-${String(index - 1).padStart(3, '0')}`],
operation: mutation.operation,
driftStages: mutation.restoreTopologyDriftStages,
...(productionDriftClassifications.has(mutation.operation) ? { classification: productionDriftClassifications.get(mutation.operation).classification, evidence: ['config/freecad-production-drift-classification.json', productionDriftClassifications.get(mutation.operation).reasonCode] } : {}),
exit: 'every changed downstream stage has a native comparison and classification',
}))
const coveredTransitions = new Set(['cut->rotate', 'rotate->fillet', 'fillet->mirrored', 'mirrored->linear-pattern'])
const productionDriftClosed = completedProductionDrifts === productionDriftTasks.length
const orderedPairClassifications = new Map(orderedPairClassification.classifications.map((entry) => [entry.pair, entry]))
if (orderedPairClassifications.size !== orderedPairClassification.classifications.length) throw new Error('Ordered operation pair classifications contain duplicate pairs.')
for (const entry of orderedPairClassification.classifications) {
if (!['accepted', 'rejected'].includes(entry.classification) || entry.nativeDecision !== entry.classification) throw new Error(`Ordered operation pair classification ${entry.pair} is invalid.`)
}
const coveredTransitions = new Set(['cut->rotate', 'rotate->fillet', 'fillet->mirrored', 'mirrored->linear-pattern', ...orderedPairClassifications.keys()])
let nextTransitionSelected = false
const transitionTasks = production.operations.flatMap((from) => production.operations.map((to) => {
const pair = `${from}->${to}`
const inProgress = productionDriftClosed && !coveredTransitions.has(pair) && !nextTransitionSelected
if (inProgress) nextTransitionSelected = true
return {
id: `TSN-PAIR-${from}-${to}`,
title: `Classify ordered operation pair ${pair}`,
status: coveredTransitions.has(pair) ? 'completed' : 'pending',
status: coveredTransitions.has(pair) ? 'completed' : inProgress ? 'in_progress' : 'pending',
dependencies: coveredTransitions.has(pair) ? [] : [productionDriftTasks.at(-1).id],
pair,
...(orderedPairClassifications.has(pair) ? { classification: orderedPairClassifications.get(pair).classification, evidence: ['config/freecad-ordered-operation-pair-classification.json', orderedPairClassifications.get(pair).reasonCode] } : {}),
exit: 'native acceptance or rejection is recorded; accepted pairs include builder, mutation, naming, and resave evidence',
}
}))
@@ -98,8 +121,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: 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 },
{ id: 'TSN-RECOVERY-DRIFT', exactTask: 'EX-TSN-04', status: productionDriftClosed ? 'completed' : workflowClosed ? 'in_progress' : 'pending', tasks: [...driftTasks, ...productionDriftTasks] },
{ id: 'TSN-ORDERED-PAIRS', exactTask: 'EX-TSN-04', status: productionDriftClosed ? 'in_progress' : 'pending', tasks: transitionTasks },
]
const allTasks = milestones.flatMap((milestone) => milestone.tasks)
const count = (status) => allTasks.filter((task) => task.status === status).length

View File

@@ -0,0 +1,220 @@
import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { existsSync } from 'node:fs'
import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { captureFreeCadPrivateNamingEvidence, classifyFreeCadPrivateNamingHistory, createFreeCadPrivateNamingAbiRequest } from '../src/facade/nativeNamingAbi.ts'
const root = resolve(new URL('..', import.meta.url).pathname)
const dist = resolve(root, 'native/occt-history/dist')
const modulePath = resolve(dist, 'bitbybit-occt-history.js')
const freecad = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const executorPath = resolve(root, 'scripts/run-freecad-ordered-operation-pair-classification.mjs')
const resaveHarnessPath = resolve(root, 'scripts/freecad-ordered-operation-pair-resave.py')
const outputPath = resolve(root, 'config/freecad-ordered-operation-pair-classification.json')
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
const fail = (message) => { throw new Error(`FreeCAD ordered operation pair classification: ${message}`) }
if (!existsSync(modulePath) || !existsSync(freecad)) fail('native OCCT or FreeCAD executable is missing.')
const sha256 = (value) => createHash('sha256').update(value).digest('hex')
const shapeStep = (module, method, ...args) => {
const shape = module[method](...args)
try { return module.shapeToStep(shape) }
finally { shape.delete?.() }
}
const input = (inputId, role, objectId, objectTag, step, namingEvidence) => ({ inputId, role, objectId, objectTag, step, ...(namingEvidence ? { namingEvidence } : {}) })
const serializableHistory = (response, records) => ({ provider: response.provider, occtVersion: response.occtVersion, records, hasModified: response.hasModified, hasGenerated: response.hasGenerated, hasDeleted: response.hasDeleted, resultStep: response.resultStep, resultBrep: response.resultBrep })
const namingSemanticSnapshot = (evidence) => (evidence?.mappedNames ?? []).map((entry) => ({
kind: entry.kind,
resultIndex: entry.resultIndex,
resultPersistentId: entry.resultPersistentId,
relation: entry.relation,
candidates: (entry.candidates ?? []).map((candidate) => ({ objectId: candidate.objectId, persistentId: candidate.persistentId, stageId: candidate.stageId, relation: candidate.relation })).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))),
})).sort((left, right) => `${left.kind}:${left.resultIndex}`.localeCompare(`${right.kind}:${right.resultIndex}`))
const canonicalSummary = (summary) => ({
isValid: summary.isValid,
solids: summary.solids,
faces: summary.faces,
edges: summary.edges,
vertices: summary.vertices,
volume: Number(summary.volume.toFixed(7)),
area: Number(summary.area.toFixed(7)),
bounds: [...summary.boundingBox.min, ...summary.boundingBox.max].map((value) => Number(value.toFixed(7))),
})
const operationSpecs = {
fuse: { builder: 'BRepAlgoAPI_Fuse', nominal: { toolSize: 5, toolOffset: 12 }, edited: { toolSize: 5, toolOffset: 11 }, parameter: 'toolOffsetX', beforeValue: 12, editedValue: 11 },
cut: { builder: 'BRepAlgoAPI_Cut', nominal: { toolSize: 4, toolOffset: 2 }, edited: { toolSize: 3, toolOffset: 2 }, parameter: 'toolSize', beforeValue: 4, editedValue: 3 },
common: { builder: 'BRepAlgoAPI_Common', nominal: { toolSize: 5, toolOffset: 10 }, edited: { toolSize: 5, toolOffset: 9 }, parameter: 'toolOffsetX', beforeValue: 10, editedValue: 9 },
rotate: { builder: 'BRepBuilderAPI_Transform', nominal: { angle: 15 }, edited: { angle: 22.5 }, parameter: 'angle', beforeValue: 15, editedValue: 22.5 },
}
const createOperation = (module, { operation, objectStep, toolSize, toolOffset, angle, stage }) => {
const baseStep = objectStep ?? shapeStep(module, 'makeBox', 10, 10, 10)
const objectInput = input('object', 'object', stage.objectId, stage.objectTag, baseStep, stage.namingEvidence)
if (operation === 'rotate') return { response: module.rotateHistoryFromStep(baseStep, 0, 0, 0, 0, 0, 1, angle), inputs: [objectInput] }
const toolStep = shapeStep(module, 'makeBoxPlaced', toolSize, toolSize, toolSize, toolOffset, 0, 0)
const inputs = [
objectInput,
input('tool', 'tool', `${stage.resultObjectId}:tool`, stage.objectTag + 1, toolStep),
]
return { response: module.booleanHistoryFromStep(baseStep, toolStep, operation), inputs }
}
const captureStage = (module, definition) => {
const { response, inputs } = createOperation(module, definition)
try {
const provenance = classifyFreeCadPrivateNamingHistory(response.records, inputs)
const stableRecords = provenance.records.filter(({ relation }) => relation !== 'deleted').length
if (response.provider !== 'occt-native' || !response.resultStep?.startsWith('ISO-10303-21;') || response.summary?.isValid !== true || (stableRecords < 1 && provenance.ambiguities.length < 1)) fail(`${definition.stage.stageId} native ${definition.operation} builder returned no usable result.`)
const request = createFreeCadPrivateNamingAbiRequest({
requestId: `${definition.stage.stageId}:request`,
documentId: 'ordered-operation-pair',
documentVersion: definition.stage.ordinal + 1,
operationId: definition.stage.stageId,
operation: definition.operation,
stageId: definition.stage.stageId,
resultObjectId: definition.stage.resultObjectId,
resultObjectTag: definition.stage.resultObjectTag,
inputs,
stages: [{ stageId: definition.stage.stageId, operation: definition.operation, inputIds: inputs.map(({ inputId }) => inputId), ordinal: definition.stage.ordinal }],
resultStep: response.resultStep,
resultBrep: response.resultBrep,
history: serializableHistory(response, response.records),
})
const namingEvidence = captureFreeCadPrivateNamingEvidence(module, request)
const mappedResults = stableRecords + provenance.ambiguities.length
if (!namingEvidence || !['native-evidence', 'ambiguous'].includes(namingEvidence.status) || namingEvidence.mappedNames?.length !== mappedResults) fail(`${definition.stage.stageId} FreeCAD naming callback evidence is incomplete: ${JSON.stringify(namingEvidence)}`)
return {
response,
namingEvidence,
report: {
operation: definition.operation,
builder: operationSpecs[definition.operation].builder,
inputCount: inputs.length,
historyProvider: response.provider,
namingStatus: namingEvidence.status,
summary: canonicalSummary(response.summary),
historyRecords: response.records.length,
selectedRecords: stableRecords,
ambiguousResults: provenance.ambiguities.length,
historySha256: sha256(JSON.stringify(response.records)),
namingEvidenceSha256: sha256(JSON.stringify(namingEvidence)),
namingSemanticSha256: sha256(JSON.stringify(namingSemanticSnapshot(namingEvidence))),
},
}
} catch (error) {
response.result?.delete?.()
throw error
}
}
const createModule = (await import(pathToFileURL(modulePath).href)).default
const module = await createModule({ locateFile: (path) => resolve(dist, path) })
const persistAcceptedPair = async (pair, second) => {
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'freecad-ordered-pair-'))
try {
const stepPath = join(temporaryDirectory, 'pair-result.step')
const evidencePath = join(temporaryDirectory, 'pair-naming.json')
await Promise.all([writeFile(stepPath, second.response.resultStep), writeFile(evidencePath, JSON.stringify(second.namingEvidence))])
const execution = spawnSync(freecad, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resaveHarnessPath], {
cwd: root,
encoding: 'utf8',
timeout: 180_000,
maxBuffer: 40 * 1024 * 1024,
env: {
...process.env,
FREECAD_ORDERED_PAIR: pair,
FREECAD_PAIR_STEP_PATH: stepPath,
FREECAD_PAIR_EVIDENCE_PATH: evidencePath,
FREECAD_PAIR_OUTPUT_DIRECTORY: temporaryDirectory,
PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
MATPLOTLIBRC: resolve(sysroot, 'usr/share/matplotlib/mpl-data/matplotlibrc'),
MPLBACKEND: 'Agg',
},
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_ORDERED_OPERATION_PAIR_RESAVE_RESULT='
const markerIndex = output.indexOf(marker)
const payload = markerIndex >= 0 ? output.slice(markerIndex + marker.length).match(/\{.*\}/s)?.[0] : undefined
if (execution.error || execution.status !== 0 || !payload) fail(`FreeCAD FCStd replay exited with ${execution.status}: ${execution.error?.message || output.trim()}`)
const persistence = JSON.parse(payload)
if (persistence.status !== 'pass' || persistence.freecadVersion !== '1.1.1' || persistence.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !Object.values(persistence.checks).every(Boolean)) fail(`FreeCAD FCStd replay failed: ${JSON.stringify(persistence)}`)
return persistence
} finally {
await rm(temporaryDirectory, { recursive: true, force: true })
}
}
const classifyFusePair = async (toOperation, pairIndex) => {
const pair = `fuse->${toOperation}`
const pairId = pair.replace('->', '-')
const first = captureStage(module, {
operation: 'fuse', toolSize: 5, toolOffset: 8,
stage: { stageId: `pair:${pairId}:first`, ordinal: 0, objectId: `pair:${pairId}:base`, objectTag: 1001 + pairIndex * 100, resultObjectId: `pair:${pairId}:first`, resultObjectTag: 1101 + pairIndex * 100 },
})
const spec = operationSpecs[toOperation]
const secondDefinition = {
operation: toOperation,
objectStep: first.response.resultStep,
...spec.nominal,
stage: { stageId: `pair:${pairId}:second`, ordinal: 1, objectId: `pair:${pairId}:first`, objectTag: 1101 + pairIndex * 100, resultObjectId: `pair:${pairId}:second`, resultObjectTag: 1201 + pairIndex * 100, namingEvidence: first.namingEvidence },
}
const second = captureStage(module, secondDefinition)
const mutated = captureStage(module, { ...secondDefinition, ...spec.edited })
const restored = captureStage(module, secondDefinition)
try {
const mutationChanged = JSON.stringify(second.report.summary) !== JSON.stringify(mutated.report.summary)
const restoredExactly = JSON.stringify(second.report.summary) === JSON.stringify(restored.report.summary) && second.report.historySha256 === restored.report.historySha256 && second.report.namingSemanticSha256 === restored.report.namingSemanticSha256
if (!mutationChanged || !restoredExactly) fail(`${pair} mutation did not change and restore native geometry/history.`)
const persistence = await persistAcceptedPair(pair, second)
return {
taskId: `TSN-PAIR-fuse-${toOperation}`,
pair,
classification: 'accepted',
reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass',
nativeDecision: 'accepted',
first: first.report,
second: second.report,
mutation: { scope: 'second-operation-only', parameter: spec.parameter, trajectory: [spec.beforeValue, spec.editedValue, spec.beforeValue], beforeValue: spec.beforeValue, editedValue: spec.editedValue, restoredValue: spec.beforeValue, changed: mutationChanged, restoredExactly, editedEvidence: mutated.report, restoredEvidence: restored.report },
naming: { upstreamEvidenceRestored: Boolean(secondDefinition.stage.namingEvidence), downstreamEvidence: Boolean(second.namingEvidence), jsonRoundtripStable: JSON.stringify(JSON.parse(JSON.stringify(second.namingEvidence))) === JSON.stringify(second.namingEvidence) },
persistence,
}
} finally {
for (const capture of [first, second, mutated, restored]) capture.response.result?.delete?.()
}
}
const classifications = []
for (const [index, operation] of ['fuse', 'cut', 'common', 'rotate'].entries()) classifications.push(await classifyFusePair(operation, index))
const [artifacts, executorHarness, matrixHarness, resaveHarness] = await Promise.all([
Promise.all(artifactNames.map(async (name) => {
const path = resolve(dist, name)
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
return { name, bytes, sha256: sha256(content) }
})),
readFile(executorPath),
readFile(resolve(root, 'scripts/freecad-naming-production-matrix.mjs')),
readFile(resaveHarnessPath),
])
const report = {
schemaVersion: 1,
baseline: { freecadVersion: '1.1.1', commit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', occtVersion: module.occtVersion() },
generatedBy: './npmw run probe:freecad-ordered-operation-pairs',
checkedBy: './npmw run check:freecad-ordered-operation-pairs',
nativeProbe: {
executor: { path: 'scripts/run-freecad-ordered-operation-pair-classification.mjs', bytes: executorHarness.length, sha256: sha256(executorHarness) },
matrix: { path: 'scripts/freecad-naming-production-matrix.mjs', bytes: matrixHarness.length, sha256: sha256(matrixHarness) },
resave: { path: 'scripts/freecad-ordered-operation-pair-resave.py', bytes: resaveHarness.length, sha256: sha256(resaveHarness) },
artifacts,
},
classifications,
summary: { registeredOperations: 19, orderedPairs: 361, classifiedPairs: classifications.length, accepted: classifications.filter(({ classification }) => classification === 'accepted').length, rejected: classifications.filter(({ classification }) => classification === 'rejected').length, unknown: 361 - classifications.length },
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-ordered-operation-pair-classification-generated', pairs: classifications.map(({ pair, classification }) => ({ pair, classification })), nativeBuilderRuns: classifications.length * 4, fcstdPhases: classifications.length * 3, output: 'config/freecad-ordered-operation-pair-classification.json' }, null, 2))

View File

@@ -0,0 +1,105 @@
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 { buildProductionDriftSnapshot, productionDriftDecisions, same, validateProductionDriftSnapshot } from './freecad-production-drift-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 sysroot = resolve(root, '.cache/freecad/sysroot')
const harnessPath = resolve(root, 'scripts/freecad-tsn-stage-correlation-oracle.py')
const oraclePath = resolve(root, 'config/freecad-tsn-stage-correlation-oracle.json')
const outputPath = resolve(root, 'config/freecad-production-drift-classification.json')
const fail = (message) => { throw new Error(`FreeCAD production drift classification: ${message}`) }
const collectDifferences = (reference, replay, path = '$', differences = []) => {
if (same(reference, replay)) return differences
if (Array.isArray(reference) && Array.isArray(replay)) {
for (let index = 0; index < Math.max(reference.length, replay.length); index += 1) collectDifferences(reference[index], replay[index], `${path}[${index}]`, differences)
return differences
}
if (reference && replay && typeof reference === 'object' && typeof replay === 'object') {
for (const key of new Set([...Object.keys(reference), ...Object.keys(replay)])) collectDifferences(reference[key], replay[key], `${path}.${key}`, differences)
return differences
}
differences.push({ path, reference, replay })
return differences
}
if (!existsSync(executable)) fail(`native executable is missing: ${executable}`)
const [reference, harnessContent, harnessBytes] = await Promise.all([
readFile(oraclePath, 'utf8').then(JSON.parse),
readFile(harnessPath),
stat(harnessPath).then(({ size }) => size),
])
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), harnessPath], {
cwd: root,
encoding: 'utf8',
timeout: 180_000,
maxBuffer: 80 * 1024 * 1024,
env: {
...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',
},
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_TSN_STAGE_CORRELATION_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 = productionDriftDecisions.map((decision) => {
const referenceSnapshot = buildProductionDriftSnapshot(reference, decision.operation)
const replaySnapshot = buildProductionDriftSnapshot(replay, decision.operation)
validateProductionDriftSnapshot(referenceSnapshot)
validateProductionDriftSnapshot(replaySnapshot)
if (!same(referenceSnapshot, replaySnapshot)) fail(`${decision.operation} fingerprints differ between the locked reference and independent native replay: ${JSON.stringify(collectDifferences(referenceSnapshot, replaySnapshot))}`)
return {
...decision,
reference: referenceSnapshot,
replay: replaySnapshot,
replayStable: true,
implementationDefect: decision.classification === 'implementation_defect',
}
})
const driftCases = reference.driftReplays.length
const countClassification = (classification) => classifications.filter((entry) => entry.classification === classification).length
const report = {
schemaVersion: 1,
baseline: {
freecadVersion: reference.freecadVersion,
commit: reference.gitCommit,
oracleId: reference.baselineId,
},
generatedBy: './npmw run probe:freecad-production-drift-classification',
checkedBy: './npmw run check:freecad-production-drift-classification',
nativeProbe: {
path: 'scripts/freecad-tsn-stage-correlation-oracle.py',
bytes: harnessBytes,
sha256: createHash('sha256').update(harnessContent).digest('hex'),
independentRuns: 2,
},
classifications,
summary: {
driftCases,
classifiedCases: classifications.length,
classifiedStages: classifications.reduce((sum, entry) => sum + entry.reference.driftOrdinals.length, 0),
stableSemantics: countClassification('stable_semantics'),
allowedEvolution: countClassification('allowed_evolution'),
implementationDefects: countClassification('implementation_defect'),
unknown: driftCases - classifications.length,
},
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({
status: 'freecad-production-drift-classification-generated',
cases: report.classifications.map(({ taskId, operation, classification, reference: snapshot }) => ({ taskId, operation, classification, driftOrdinals: snapshot.driftOrdinals })),
independentRuns: report.nativeProbe.independentRuns,
output: 'config/freecad-production-drift-classification.json',
}, null, 2))

View File

@@ -13,6 +13,19 @@ const oraclePath = resolve(root, 'config/freecad-composite-history-elementmap-or
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}`) }
const collectDifferences = (reference, replay, path = '$', differences = []) => {
if (same(reference, replay)) return differences
if (Array.isArray(reference) && Array.isArray(replay)) {
for (let index = 0; index < Math.max(reference.length, replay.length); index += 1) collectDifferences(reference[index], replay[index], `${path}[${index}]`, differences)
return differences
}
if (reference && replay && typeof reference === 'object' && typeof replay === 'object') {
for (const key of new Set([...Object.keys(reference), ...Object.keys(replay)])) collectDifferences(reference[key], replay[key], `${path}.${key}`, differences)
return differences
}
differences.push({ path, reference, replay })
return differences
}
if (!existsSync(executable)) fail(`native executable is missing: ${executable}`)
const [reference, referenceResave, harnessContent, harnessBytes] = await Promise.all([
@@ -56,7 +69,7 @@ const classifications = recoveredNamingDecisions.map((decision) => {
})
validateRecoveredNamingSnapshot(referenceSnapshot)
validateRecoveredNamingSnapshot(replaySnapshot)
if (!same(referenceSnapshot, replaySnapshot)) fail(`${decision.caseId} fingerprints differ between the locked reference and independent native replay.`)
if (!same(referenceSnapshot, replaySnapshot)) fail(`${decision.caseId} fingerprints differ between the locked reference and independent native replay: ${JSON.stringify(collectDifferences(referenceSnapshot, replaySnapshot))}`)
return {
...decision,
reference: referenceSnapshot,