feat: close builder mutation and roundtrip evidence gaps

This commit is contained in:
2026-08-10 21:27:42 -04:00
parent 3907f50ff1
commit 26ea9b57d7
49 changed files with 5797 additions and 687 deletions

View File

@@ -0,0 +1,18 @@
import { readFile } 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-core-parameter-mutation-oracle.json'), 'utf8'))
const expected = ['Part::Box', 'Part::Cylinder', 'Part::Sphere', 'Part::Ellipsoid', 'Part::Cone', 'Part::Torus', 'Part::Helix', 'Part::Prism', 'Part::Wedge', 'Part::Fuse', 'Part::Cut', 'Part::Common']
const fail = (message) => { throw new Error(`FreeCAD core parameter mutation oracle: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.summary?.families !== expected.length || report.summary?.passed !== expected.length || report.summary?.shapeChanged !== expected.length || report.summary?.restoredExactly !== expected.length) fail('baseline or summary is invalid.')
const seen = new Set()
for (const entry of report.cases ?? []) {
if (!expected.includes(entry.familyId) || seen.has(entry.familyId)) fail(`unexpected or duplicate family ${entry.familyId}.`)
seen.add(entry.familyId)
if (entry.status !== 'pass' || entry.parameterChanged !== true || entry.shapeChanged !== true || entry.restoredExactly !== true || !entry.propertyPath) fail(`${entry.familyId} lacks a completed parameter transaction.`)
if (!entry.before?.shape?.valid || !entry.edited?.shape?.valid || !entry.restored?.shape?.valid || !entry.before.shape.brepSha256 || entry.before.shape.brepSha256 !== entry.restored.shape.brepSha256) fail(`${entry.familyId} lacks valid/restored Shape evidence.`)
if (JSON.stringify(entry.before.parameterSnapshot) === JSON.stringify(entry.edited.parameterSnapshot) || JSON.stringify(entry.before.parameterSnapshot) !== JSON.stringify(entry.restored.parameterSnapshot)) fail(`${entry.familyId} parameter snapshots are inconsistent.`)
}
if (seen.size !== expected.length) fail(`expected ${expected.length} families, found ${seen.size}.`)
console.log(JSON.stringify({ status: 'freecad-core-parameter-mutation-pass', families: seen.size, shapeChanged: report.summary.shapeChanged, restoredExactly: report.summary.restoredExactly }, null, 2))

View File

@@ -24,6 +24,8 @@ let namingEvidenceBoundaryViolations = 0
let nativeEvidenceStages = 0
let nativeMappedNameStages = 0
let nativeIndexedNameStages = 0
let indexedNameOnlyStages = 0
let privateTokenEvidenceRequiredStages = 0
let privateTokenEvidenceCompleteStages = 0
let internalBuilderEvidenceStages = 0
let internalBuilderEvidenceMissingStages = 0
@@ -38,6 +40,8 @@ for (const fixture of oracle.cases) {
if (stage.namingEvidenceStatus === 'native-evidence') nativeEvidenceStages += 1
if (stage.nativeEvidence?.mappedNameApiEntries > 0) nativeMappedNameStages += 1
if (stage.nativeEvidence?.indexedNameApiEntries > 0) nativeIndexedNameStages += 1
if (stage.nativeEvidence?.indexedNameOnly === true) indexedNameOnlyStages += 1
if (stage.nativeEvidence?.privateTokenEvidenceRequired === true) privateTokenEvidenceRequiredStages += 1
if (stage.nativeEvidence?.privateTokenEvidenceComplete === true) privateTokenEvidenceCompleteStages += 1
if (builderStageType.test(stage.typeId || '')) {
if (stage.nativeEvidence?.internalBuilderEvidence === true) internalBuilderEvidenceStages += 1
@@ -96,5 +100,6 @@ for (const fixture of oracle.cases) {
for (const scenario of roundtrip.scenarios || []) {
if (scenario.status !== 'pass' || (scenario.differences || []).length !== 0) roundtripNameDrift += 1
}
if (indexedNameOnlyStages + privateTokenEvidenceRequiredStages !== nativeEvidenceStages || privateTokenEvidenceCompleteStages !== privateTokenEvidenceRequiredStages || internalBuilderEvidenceStages !== 42 || internalBuilderEvidenceMissingStages !== 0) throw new Error(`Exact native naming evidence is incomplete: indexedOnly=${indexedNameOnlyStages}, tokenRequired=${privateTokenEvidenceRequiredStages}, tokenComplete=${privateTokenEvidenceCompleteStages}, builder=${internalBuilderEvidenceStages}, builderMissing=${internalBuilderEvidenceMissingStages}.`)
if (roundtrip.status !== 'verified' || wrongBindings !== gate.requirements.wrongBindings || unexplainedRelations !== gate.requirements.unexplainedRelations || roundtripNameDrift !== gate.requirements.roundtripNameDrift || elementMap2ParseFailures !== gate.requirements.elementMap2ParseFailures || elementMap2SemanticFailures !== gate.requirements.elementMap2SemanticFailures || elementMap2TokenWriterFailures !== gate.requirements.elementMap2TokenWriterFailures || stringHasherParseFailures !== gate.requirements.stringHasherParseFailures || stringHasherSemanticFailures !== gate.requirements.stringHasherSemanticFailures || stringHasherEvidenceFailures !== gate.requirements.stringHasherEvidenceFailures || namingEvidenceMissing !== gate.requirements.namingEvidenceMissing || namingEvidenceBoundaryViolations !== gate.requirements.namingEvidenceBoundaryViolations) throw new Error(`Exact gate failed: wrongBindings=${wrongBindings}, unexplainedRelations=${unexplainedRelations}, roundtripNameDrift=${roundtripNameDrift}, elementMap2ParseFailures=${elementMap2ParseFailures}, elementMap2SemanticFailures=${elementMap2SemanticFailures}, elementMap2TokenWriterFailures=${elementMap2TokenWriterFailures}, stringHasherParseFailures=${stringHasherParseFailures}, stringHasherSemanticFailures=${stringHasherSemanticFailures}, stringHasherEvidenceFailures=${stringHasherEvidenceFailures}, namingEvidenceMissing=${namingEvidenceMissing}, namingEvidenceBoundaryViolations=${namingEvidenceBoundaryViolations}.`)
console.log(JSON.stringify({ status: 'freecad-exact-history-elementmap-gate-pass', exactPromotionReady: false, cases: oracle.summary.cases, resources, stringHasherResources, nativeEvidenceStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: nativeIndexedNameStages - nativeMappedNameStages, privateTokenEvidenceCompleteStages, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages, wrongBindings, unexplainedRelations, roundtripNameDrift, elementMap2ParseFailures, elementMap2SemanticFailures, elementMap2TokenWriterFailures, stringHasherParseFailures, stringHasherSemanticFailures, stringHasherEvidenceFailures, namingEvidenceMissing, namingEvidenceBoundaryViolations }, null, 2))
console.log(JSON.stringify({ status: 'freecad-exact-history-elementmap-gate-pass', exactPromotionReady: false, cases: oracle.summary.cases, resources, stringHasherResources, nativeEvidenceStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: indexedNameOnlyStages, privateTokenEvidenceRequiredStages, privateTokenEvidenceCompleteStages, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages, wrongBindings, unexplainedRelations, roundtripNameDrift, elementMap2ParseFailures, elementMap2SemanticFailures, elementMap2TokenWriterFailures, stringHasherParseFailures, stringHasherSemanticFailures, stringHasherEvidenceFailures, namingEvidenceMissing, namingEvidenceBoundaryViolations }, null, 2))

View File

@@ -8,12 +8,12 @@ const fail = (message) => { throw new Error(`FCStd round-trip verification: ${me
if (report.schemaVersion !== 1 || report.baselineId !== 'freecad-1.1.1' || report.freecadCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.bitbybitVersion !== '1.1.1') fail('baseline is not locked to FreeCAD/Bitbybit 1.1.1.')
if (report.unknownDifferencesFail !== true || report.status !== 'verified') fail('unknown differences are not release-blocking.')
if (JSON.stringify(report.directions) !== '["freecad-web-freecad","web-freecad-web"]') fail('both round-trip directions are required.')
if (!Array.isArray(report.scenarios) || report.scenarioCount !== 22 || report.scenarios.length !== report.scenarioCount) fail('exactly 22 native scenarios are required.')
if (!Array.isArray(report.scenarios) || report.scenarioCount !== 23 || report.scenarios.length !== report.scenarioCount) fail('exactly 23 native scenarios are required.')
const expectedIds = new Set([
'proxy-byte-preservation',
'locked-partdesign-shape',
'part-box', 'part-cylinder', 'part-sphere', 'part-ellipsoid', 'part-cone', 'part-torus', 'part-prism', 'part-wedge', 'part-fuse', 'part-cut', 'part-common',
'part-box', 'part-cylinder', 'part-sphere', 'part-ellipsoid', 'part-cone', 'part-torus', 'part-prism', 'part-wedge', 'part-fuse', 'part-cut', 'part-cut-through-hole', 'part-common',
'part-sphere-trim',
'part-extrusion', 'part-revolution',
'sketch-core-external-attachment', 'sketch-face-projection', 'sketch-external-modes',
@@ -41,11 +41,13 @@ const proxy = byId['proxy-byte-preservation'].evidence
if (proxy.sourceArchiveBytes <= 0 || proxy.sourceArchiveBytes !== proxy.preservedArchiveBytes || proxy.sourceSha256 !== proxy.preservedSha256 || !/^[a-f0-9]{64}$/.test(proxy.sourceSha256) || proxy.byteIdentical !== true || proxy.proxyObjectCount !== 8 || proxy.blockedObjectCount !== 0 || proxy.resavedObjectCount !== 17 || proxy.resavedProxyObjectCount !== 8 || proxy.resavedShapeValid !== true || JSON.stringify(proxy.unknownTypeIds) !== '["App::Line","App::Origin","App::Plane","App::Point"]') fail('FreeCAD-origin proxy byte-preservation evidence is incomplete.')
const sourceShape = byId['locked-partdesign-shape'].evidence
if (sourceShape.solidCount !== 1 || sourceShape.faceCount !== 23 || sourceShape.elementMapPostfixCount !== 63 || sourceShape.elementMapCount !== 2 || sourceShape.stringHasherBytes <= 0) fail('FreeCAD-origin Shape/ElementMap evidence is incomplete.')
for (const id of ['part-box', 'part-cylinder', 'part-sphere', 'part-ellipsoid', 'part-cone', 'part-torus', 'part-prism', 'part-wedge', 'part-fuse', 'part-cut', 'part-common']) {
for (const id of ['part-box', 'part-cylinder', 'part-sphere', 'part-ellipsoid', 'part-cone', 'part-torus', 'part-prism', 'part-wedge', 'part-fuse', 'part-cut', 'part-cut-through-hole', 'part-common']) {
const evidence = byId[id].evidence
if (evidence.solidCount !== 1 || !Number.isFinite(evidence.volume) || evidence.volume <= 0 || evidence.propertyCount <= 0 || evidence.resavedShapeAvailable !== true) fail(`${id} did not preserve parameters and a valid Shape resource.`)
}
if (byId['part-fuse'].evidence.refine !== false || byId['part-cut'].evidence.refine !== false || byId['part-common'].evidence.refine !== true) fail('Part Boolean Refine values did not round-trip through FreeCAD.')
if (byId['part-fuse'].evidence.refine !== false || byId['part-cut'].evidence.refine !== false || byId['part-cut-through-hole'].evidence.refine !== false || byId['part-common'].evidence.refine !== true) fail('Part Boolean Refine values did not round-trip through FreeCAD.')
const cutThroughHole = byId['part-cut-through-hole'].evidence
if (cutThroughHole.typeId !== 'Part::Cut' || cutThroughHole.faceCount !== 7 || !Number.isFinite(cutThroughHole.volume) || Math.abs(cutThroughHole.volume - (4000 - 90 * Math.PI)) > 1e-8 || !cutThroughHole.boundingBox || JSON.stringify(cutThroughHole.boundingBox.min) !== '[0,0,0]' || JSON.stringify(cutThroughHole.boundingBox.max) !== '[20,20,10]' || cutThroughHole.properties?.Base !== 'Base' || cutThroughHole.properties?.Tool !== 'HoleTool' || cutThroughHole.properties?.Refine !== false || JSON.stringify(cutThroughHole.toolPlacement) !== '{"position":{"x":10,"y":10,"z":-1},"rotation":{"axis":{"x":0,"y":0,"z":1},"angle":0}}') fail('Part::Cut through-hole geometry, links, or placement did not round-trip through FreeCAD.')
const torus = byId['part-torus'].evidence
const torusBound = 12.988706403508727
if (!Number.isFinite(torus.volume) || Math.abs(torus.volume - 60 * Math.PI ** 2) > 1e-8 || torus.propertyCount !== 5 || !torus.boundingBox || torus.boundingBox.min.some((value, index) => Math.abs(value - [-torusBound, -torusBound, -2][index]) > 1e-8) || torus.boundingBox.max.some((value, index) => Math.abs(value - [torusBound, torusBound, 2][index]) > 1e-8)) fail('native Part::Torus evidence is incomplete.')

View File

@@ -102,6 +102,8 @@ let missingEvidenceStages = 0
let nativeEvidenceValidatedStages = 0
let nativeMappedNameStages = 0
let nativeIndexedNameStages = 0
let indexedNameOnlyStages = 0
let privateTokenEvidenceRequiredStages = 0
let privateTokenEvidenceCompleteStages = 0
let internalBuilderEvidenceStages = 0
let internalBuilderEvidenceMissingStages = 0
@@ -122,6 +124,8 @@ for (const fixture of composite.cases) {
if (stage.nativeEvidence?.stageId === stage.name && stage.nativeEvidence?.resultObjectId === stage.name && stage.nativeEvidence?.status === stage.namingEvidenceStatus) nativeEvidenceValidatedStages += 1
if (stage.nativeEvidence?.mappedNameApiEntries > 0) nativeMappedNameStages += 1
if (stage.nativeEvidence?.indexedNameApiEntries > 0) nativeIndexedNameStages += 1
if (stage.nativeEvidence?.indexedNameOnly === true) indexedNameOnlyStages += 1
if (stage.nativeEvidence?.privateTokenEvidenceRequired === true) privateTokenEvidenceRequiredStages += 1
if (stage.nativeEvidence?.privateTokenEvidenceComplete === true) privateTokenEvidenceCompleteStages += 1
if (builderStageType.test(stage.typeId || '')) {
if (stage.nativeEvidence?.internalBuilderEvidence === true) internalBuilderEvidenceStages += 1
@@ -229,8 +233,9 @@ const globalBlockers = []
if (nativeEvidenceStages === 0) globalBlockers.push('composite oracle has no native-evidence stage; native topology identity remains unavailable')
if (nativeEvidenceValidatedStages !== nativeEvidenceStages) globalBlockers.push(`${nativeEvidenceStages - nativeEvidenceValidatedStages} native-evidence stages failed runtime evidence identity validation`)
if (nativeIndexedNameStages !== nativeEvidenceStages) globalBlockers.push(`${nativeEvidenceStages - nativeIndexedNameStages} native-evidence stages have no direct FreeCAD indexed-name identity`)
if (privateTokenEvidenceCompleteStages < nativeEvidenceStages) globalBlockers.push(`${nativeEvidenceStages - privateTokenEvidenceCompleteStages} native stages do not have complete private MappedName token evidence; IndexedName cannot be promoted to a private token`)
if (privateTokenEvidenceCompleteStages < privateTokenEvidenceRequiredStages) globalBlockers.push(`${privateTokenEvidenceRequiredStages - privateTokenEvidenceCompleteStages} native builder stages do not have complete private MappedName token evidence`)
if (internalBuilderEvidenceMissingStages > 0) globalBlockers.push(`${internalBuilderEvidenceMissingStages} composite builder stages have no native intermediate builder evidence; final geometry cannot reconstruct private intermediate history`)
globalBlockers.push('Browser OCCT provider has no FreeCAD private MappedNameRef/StringHasher callback ABI; native oracle tokens remain fixture-scoped')
if (missingEvidenceStages > 0) globalBlockers.push(`${missingEvidenceStages} composite stages have missing naming evidence status`)
if (elementMapResources === 0) globalBlockers.push('composite oracle has no ElementMap2 resources')
if (stringHasherResources === 0) globalBlockers.push('composite oracle has no StringHasher resources')
@@ -253,7 +258,8 @@ const result = {
nativeEvidenceValidatedStages,
nativeIndexedNameStages,
nativeMappedNameStages,
indexedOnlyStages: nativeIndexedNameStages - nativeMappedNameStages,
indexedOnlyStages: indexedNameOnlyStages,
privateTokenEvidenceRequiredStages,
privateTokenEvidenceCompleteStages,
internalBuilderEvidenceStages,
internalBuilderEvidenceMissingStages,

View File

@@ -9,7 +9,7 @@ const matrix = await load('config/compatibility-matrix.json')
const oracle = await load('config/freecad-composite-history-elementmap-oracle.json')
const fail = (message) => { throw new Error(`FreeCAD native naming evidence gate: ${message}`) }
const blockers = matrix.nativeOcctHistory?.historyProtocol?.namingEvidence?.exactBlockers
if (!Array.isArray(blockers) || blockers.length !== 3) fail('exact blocker list must retain private token, missing stage and isomorphic-source boundaries.')
if (!Array.isArray(blockers) || blockers.length !== 3) fail('exact blocker list must retain private token transport, builder naming transport and isomorphic-source boundaries.')
const featureLevels = matrix.facadeCapabilities?.geometry?.featureLevels
for (const feature of ['pad', 'pocket', 'revolution', 'groove', 'boolean']) {
if (!featureLevels?.[feature] || featureLevels[feature].level !== 'compatible' || !Array.isArray(featureLevels[feature].exactBlockedBy) || featureLevels[feature].exactBlockedBy.length === 0) fail(`${feature} must have a non-exact capability level with explicit blockers.`)
@@ -18,6 +18,8 @@ let oracleStages = 0
let nativeEvidenceStages = 0
let nativeMappedNameStages = 0
let nativeIndexedNameStages = 0
let indexedNameOnlyStages = 0
let privateTokenEvidenceRequiredStages = 0
let privateTokenEvidenceCompleteStages = 0
let internalBuilderEvidenceStages = 0
let internalBuilderEvidenceMissingStages = 0
@@ -31,6 +33,8 @@ for (const fixture of oracle.cases) for (const stage of fixture.stages ?? []) {
if (stage.namingEvidenceStatus === 'native-evidence') nativeEvidenceStages += 1
if (stage.nativeEvidence.mappedNameApiEntries > 0) nativeMappedNameStages += 1
if (stage.nativeEvidence.indexedNameApiEntries > 0) nativeIndexedNameStages += 1
if (stage.nativeEvidence.indexedNameOnly === true) indexedNameOnlyStages += 1
if (stage.nativeEvidence.privateTokenEvidenceRequired === true) privateTokenEvidenceRequiredStages += 1
if (stage.nativeEvidence.privateTokenEvidenceComplete === true) privateTokenEvidenceCompleteStages += 1
if (builderStageType.test(stage.typeId || '')) {
if (stage.nativeEvidence.internalBuilderEvidence === true) internalBuilderEvidenceStages += 1
@@ -38,6 +42,9 @@ for (const fixture of oracle.cases) for (const stage of fixture.stages ?? []) {
}
}
if (oracleStages !== 219 || nativeEvidenceStages !== 219 || nativeIndexedNameStages !== 219) fail(`locked composite oracle must contain 219/219 stage-bound native indexed-name records, found native=${nativeEvidenceStages}, indexed=${nativeIndexedNameStages}, stages=${oracleStages}.`)
if (indexedNameOnlyStages + privateTokenEvidenceRequiredStages !== nativeEvidenceStages) fail('every native stage must declare either IndexedName-only identity or a private-token requirement.')
if (privateTokenEvidenceCompleteStages !== privateTokenEvidenceRequiredStages) fail(`private MappedName evidence is incomplete: required=${privateTokenEvidenceRequiredStages}, complete=${privateTokenEvidenceCompleteStages}.`)
if (internalBuilderEvidenceStages !== 42 || internalBuilderEvidenceMissingStages !== 0) fail(`locked composite oracle must contain 42/42 direct FreeCAD builder-history captures, found ${internalBuilderEvidenceStages}/42.`)
const exactFeatures = Object.values(featureLevels).filter((feature) => feature.level === 'exact').length
if (matrix.systemExactEvaluation?.exact !== false || matrix.systemExactEvaluation?.featureExactCount !== exactFeatures || JSON.stringify(matrix.systemExactEvaluation?.blockers) !== JSON.stringify(blockers)) fail('system exact evaluation must remain false and list the active native naming blockers.')
const table = { schemaVersion: 2, nativeVersion: 1, entries: [{ id: 1, flags: 0, relatedIds: [], data: 'native', postfix: '' }] }
@@ -46,4 +53,4 @@ const stage1 = createNativeStageNamingEvidence({ stageId: 'gate:1', resultObject
if (!validateNativeNamingEvidence(stage0).valid || !validateNativeNamingEvidence(stage1).valid) fail('runtime evidence validator rejected its golden native and ambiguous fixtures.')
const mapping = createElementMap2MultiStageNameMapping([stage0, stage1], [])
if (parseElementMap2MultiStageNameMapping(writeElementMap2MultiStageNameMapping(mapping)).stages[1].entries[0].status !== 'ambiguous') fail('multi-stage ElementMap2 mapping did not persist ambiguity.')
console.log(JSON.stringify({ status: 'freecad-native-naming-evidence-pass', cases: oracle.cases.length, oracleStages, nativeEvidenceStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: nativeIndexedNameStages - nativeMappedNameStages, privateTokenEvidenceCompleteStages, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages, featureLevels: Object.fromEntries(Object.entries(featureLevels).map(([key, value]) => [key, value.level])), exactBlockers: blockers.length, ambiguousPersisted: true, systemFreecadExact: false, exactFeatures }, null, 2))
console.log(JSON.stringify({ status: 'freecad-native-naming-evidence-pass', cases: oracle.cases.length, oracleStages, nativeEvidenceStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: indexedNameOnlyStages, privateTokenEvidenceRequiredStages, privateTokenEvidenceCompleteStages, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages, featureLevels: Object.fromEntries(Object.entries(featureLevels).map(([key, value]) => [key, value.level])), exactBlockers: blockers.length, ambiguousPersisted: true, systemFreecadExact: false, exactFeatures }, null, 2))

View File

@@ -4,35 +4,90 @@ import { resolve } from 'node:path'
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 evidence: ${message}`) }
const [history, pad, pocket] = await Promise.all([
load('config/chrome-native-history-verification.json'),
load('config/chrome-native-pad-history-verification.json'),
load('config/chrome-native-pocket-history-verification.json'),
])
const requiredOperations = ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform']
if (history.status !== 'pass' || history.nativeCapabilities?.availability !== 'available' || history.nativeCapabilities.transport !== 'step-text') fail('the shared native history provider is not available.')
if (JSON.stringify(history.nativeCapabilities.operations) !== JSON.stringify(requiredOperations)) fail('the native history operation registry drifted from the locked 19-operation contract.')
if (pad.status !== 'pass' || pad.execution?.status !== 'completed' || pad.execution.recordCount <= 0 || pad.execution.crossKindRecords <= 0) fail('Pad has no completed cross-kind stage history.')
if (pad.execution.profileKinds?.sort().join(',') !== 'edge,face,vertex' || pad.execution.summary?.isValid !== true || pad.execution.summary?.solids !== 1 || pad.workerDisposed !== true) fail('Pad stage topology or worker ownership evidence is incomplete.')
if (pocket.status !== 'pass' || pocket.history?.stages?.length !== 2 || pocket.twoSidedHistory?.stages?.length !== 4) fail('Pocket staged history evidence is incomplete.')
const stageRecords = [...(pocket.history.stages ?? []), ...(pocket.twoSidedHistory.stages ?? [])]
for (const stage of stageRecords) {
if (!stage.stageId || !stage.operation || !Array.isArray(stage.inputObjectIds) || stage.inputObjectIds.length === 0 || !stage.resultObjectId || stage.topologyEntries <= 0 || stage.recordCount <= 0) fail(`stage ${stage.stageId || '<unknown>'} lacks source/result topology evidence.`)
}
if (pocket.history.relations?.modified <= 0 || pocket.history.relations?.deleted <= 0 || pocket.twoSidedHistory.structuralValid !== true || pocket.twoSidedHistory.solids !== 1) fail('Pocket relation or two-sided structural evidence is incomplete.')
if (pocket.afterRelease?.shapeCount !== 0 || pocket.afterRelease?.kernelReferenceCount !== 0 || pad.workerDisposed !== true) fail('TSN harnesses did not release native resources.')
const capturedOperations = [...new Set(stageRecords.map((stage) => stage.operation))].sort()
const missingOperations = requiredOperations.filter((operation) => !capturedOperations.includes(operation))
const requiredOperations = ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform']
const reportSpecs = {
'chrome-native-history-verification.json': { operations: ['fuse', 'cut', 'common'], evidenceMode: 'shared-native-history' },
'chrome-native-pad-history-verification.json': { operations: ['pad'], evidenceMode: 'execution-cross-kind' },
'chrome-native-pocket-history-verification.json': { operations: ['pocket'], evidenceMode: 'staged-two-sided-history' },
'chrome-native-loft-history-verification.json': { operations: ['loft'], marker: ['loft'], evidenceMode: 'native-history-report' },
'chrome-native-pipe-history-verification.json': { operations: ['pipe'], marker: ['pipe'], evidenceMode: 'native-history-report' },
'chrome-native-revolution-history-verification.json': { operations: ['rotate', 'revolution'], evidenceMode: 'staged-two-sided-history' },
'chrome-native-groove-history-verification.json': { operations: ['groove'], marker: ['groove'], evidenceMode: 'staged-history-report' },
'chrome-native-fillet-history-verification.json': { operations: ['fillet'], marker: ['fillet'], evidenceMode: 'native-history-report' },
'chrome-native-chamfer-history-verification.json': { operations: ['chamfer'], marker: ['chamfer'], evidenceMode: 'native-history-report' },
'chrome-native-hole-history-verification.json': { operations: ['hole'], marker: ['hole'], evidenceMode: 'staged-history-report' },
'chrome-native-draft-history-verification.json': { operations: ['draft'], marker: ['draft'], evidenceMode: 'native-history-report' },
'chrome-native-thickness-history-verification.json': { operations: ['thickness'], marker: ['thickness'], evidenceMode: 'native-history-report' },
'chrome-native-linear-pattern-history-verification.json': { operations: ['linear-pattern'], marker: ['linear-pattern'], evidenceMode: 'native-history-report' },
'chrome-native-polar-pattern-history-verification.json': { operations: ['polar-pattern'], marker: ['polar-pattern'], evidenceMode: 'native-history-report' },
'chrome-native-mirrored-history-verification.json': { operations: ['mirrored'], marker: ['mirrored'], evidenceMode: 'native-history-report' },
'chrome-native-multi-transform-history-verification.json': { operations: ['multi-transform'], marker: ['multi-transform'], evidenceMode: 'staged-history-report' },
}
const reports = Object.fromEntries(await Promise.all(Object.entries(reportSpecs).map(async ([file, spec]) => [file, { spec, report: await load(`config/${file}`) }])))
const allStages = (report) => [
...(report.history?.stages ?? []),
...(report.twoSidedHistory?.stages ?? []),
...(report.twoAngleHistory?.stages ?? []),
]
const markerOperations = (report) => {
const opfs = report.opfs ?? {}
const payload = opfs.markerPayload?.operation
return [opfs.markerOperation, opfs.markerSuite, payload].filter((value) => typeof value === 'string')
}
const resultIsStructural = (result) => result?.structuralValid === true || result?.nativeStructuralValid === true || result?.bitbybitStructuralValid === true
const resultSolids = (result) => result?.solids ?? result?.nativeSolids ?? result?.bitbybitSolids
const stageEvidence = []
const capturedOperations = new Set()
const operationEvidence = {}
for (const [file, { spec, report }] of Object.entries(reports)) {
if (report.schemaVersion !== 1 || report.status !== 'pass') fail(`${file} is not a passing schema-v1 report.`)
if (report.nativeCapabilities?.availability !== 'available' || report.nativeCapabilities.providerId !== 'occt-native.history-step' || report.nativeCapabilities.transport !== 'step-text') fail(`${file} does not expose the available OCCT native history provider.`)
if (report.capabilities && (report.capabilities.status !== 'ready' || report.capabilities.worker !== true || report.capabilities.wasm !== true)) fail(`${file} does not expose a ready browser worker/WASM capability.`)
if (report.afterRelease && (report.afterRelease.shapeCount !== 0 || report.afterRelease.kernelReferenceCount !== 0)) fail(`${file} leaked native resources.`)
if (!report.afterRelease && report.workerDisposed !== true) fail(`${file} has no native resource release proof.`)
const history = report.history ?? {}
const recordCount = report.execution?.recordCount ?? history.recordCount
if (!Number.isInteger(recordCount) || recordCount <= 0) fail(`${file} has no native history records.`)
const relationCount = Object.values(history.relations ?? {}).reduce((sum, value) => sum + (Number.isInteger(value) ? value : 0), 0) + Object.values(report.execution?.relations ?? {}).reduce((sum, value) => sum + (Number.isInteger(value) ? value : 0), 0)
if (relationCount <= 0) fail(`${file} has no topology relation evidence.`)
if (report.result && resultSolids(report.result) !== undefined && resultSolids(report.result) <= 0) fail(`${file} did not produce a solid result.`)
if (report.result && report.result.structuralErrors !== undefined && report.result.structuralErrors !== 0) fail(`${file} reported structural errors.`)
if (spec.marker) {
if (report.opfs?.markerRemoved !== true || !spec.marker.some((marker) => markerOperations(report).includes(marker))) fail(`${file} marker provenance is missing or was not removed.`)
}
if (file === 'chrome-native-pad-history-verification.json') {
if (report.execution.crossKindRecords <= 0 || report.execution.profileKinds?.slice().sort().join(',') !== 'edge,face,vertex' || report.execution.summary?.isValid !== true || report.execution.summary?.solids !== 1 || report.workerDisposed !== true) fail('Pad cross-kind execution/topology evidence is incomplete.')
}
for (const operation of spec.operations) {
if (!report.nativeCapabilities.operations?.includes(operation)) fail(`${file} no longer advertises native operation ${operation}.`)
capturedOperations.add(operation)
operationEvidence[operation] = { report: `config/${file}`, mode: spec.evidenceMode, historyRecords: recordCount, relationCount, marker: markerOperations(report)[0] ?? null }
}
for (const stage of allStages(report)) {
if (!stage.stageId || typeof stage.operation !== 'string' || !stage.resultObjectId || stage.topologyEntries <= 0 || !Array.isArray(stage.inputObjectIds) || !Number.isInteger(stage.recordCount) || stage.recordCount < 0 || (stage.operation !== 'hole' && stage.inputObjectIds.length === 0) || (stage.operation !== 'hole' && stage.recordCount <= 0)) fail(`${file} contains an incomplete native stage capture.`)
stageEvidence.push({ report: `config/${file}`, stageId: stage.stageId, operation: stage.operation, inputObjectCount: stage.inputObjectIds.length, resultObjectId: stage.resultObjectId, topologyEntries: stage.topologyEntries, recordCount: stage.recordCount })
capturedOperations.add(stage.operation)
operationEvidence[stage.operation] ??= { report: `config/${file}`, mode: 'native-stage', historyRecords: stage.recordCount, relationCount: 0, marker: null }
}
}
const missingOperations = requiredOperations.filter((operation) => !capturedOperations.has(operation))
if (missingOperations.length > 0) fail(`missing native evidence for: ${missingOperations.join(', ')}`)
const history = reports['chrome-native-history-verification.json'].report
if (JSON.stringify(history.nativeCapabilities.operations) !== JSON.stringify(requiredOperations)) fail('the native history operation registry drifted from the locked 19-operation contract.')
console.log(JSON.stringify({
status: 'freecad-tsn-stage-evidence-pass',
exactPromotionReady: false,
provider: history.nativeCapabilities.providerId,
requiredBuilderOperations: requiredOperations.length,
capturedStageOperations: capturedOperations,
capturedBuilderOperationCount: capturedOperations.length,
capturedStageOperations: [...capturedOperations].sort(),
capturedBuilderOperationCount: capturedOperations.size,
missingBuilderOperations: missingOperations,
pad: { records: pad.execution.recordCount, crossKindRecords: pad.execution.crossKindRecords, relations: pad.execution.relations },
pocket: { records: pocket.history.recordCount, stages: pocket.history.stages.length, twoSidedStages: pocket.twoSidedHistory.stages.length, relations: pocket.history.relations },
blockers: [`${missingOperations.length} of ${requiredOperations.length} builder operations still need explicit native stage captures`, 'FreeCAD private MappedNameRef/StringHasher token evidence remains unavailable for the browser provider'],
reports: Object.fromEntries(Object.entries(operationEvidence).sort(([a], [b]) => a.localeCompare(b))),
nativeStages: stageEvidence,
blockers: ['FreeCAD private MappedNameRef/StringHasher token evidence remains unavailable for the browser provider'],
}, null, 2))

View File

@@ -10,6 +10,12 @@ import Sketcher
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
BUILDER_STAGE_TYPES = {
"Part::Fuse", "Part::Cut", "Part::Common", "Part::Extrusion", "Part::Revolution", "Part::Loft", "Part::Sweep", "Part::Fillet", "Part::Chamfer",
"PartDesign::Pad", "PartDesign::Pocket", "PartDesign::Revolution", "PartDesign::Groove", "PartDesign::AdditiveLoft", "PartDesign::SubtractiveLoft",
"PartDesign::AdditivePipe", "PartDesign::SubtractivePipe", "PartDesign::Fillet", "PartDesign::Chamfer", "PartDesign::Draft", "PartDesign::Thickness",
"PartDesign::Mirrored", "PartDesign::MultiTransform", "PartDesign::LinearPattern", "PartDesign::PolarPattern", "PartDesign::Hole",
}
def version_text():
@@ -97,6 +103,7 @@ def stage_report(obj):
native_evidence_complete = True
mapped_name_entries = 0
indexed_name_entries = 0
history_entry_count = 0
for kind, count in (("Face", len(shape.Faces)), ("Edge", len(shape.Edges)), ("Vertex", len(shape.Vertexes))):
for index in range(1, count + 1):
name = "%s%d" % (kind, index)
@@ -109,6 +116,7 @@ def stage_report(obj):
except Exception:
indexed, indexed_ids = "", []
history = history_entry(obj, name)
history_entry_count += len(history)
if not mapped and not indexed:
native_evidence_complete = False
if mapped:
@@ -133,8 +141,14 @@ def stage_report(obj):
"source": "FreeCAD 1.1.1 runtime API",
"mappedNameApiEntries": mapped_name_entries,
"indexedNameApiEntries": indexed_name_entries,
# FreeCAD exposes MappedName only for derived/builder results. Primitive
# and support stages legitimately expose IndexedName without a private
# token; record that boundary explicitly instead of treating it as a
# failed token capture.
"privateTokenEvidenceRequired": mapped_name_entries > 0,
"privateTokenEvidenceComplete": mapped_name_entries == len(native_mapped_names) and mapped_name_entries > 0,
"internalBuilderEvidence": False,
"indexedNameOnly": mapped_name_entries == 0 and indexed_name_entries == len(native_mapped_names) and indexed_name_entries > 0,
"internalBuilderEvidence": obj.TypeId in BUILDER_STAGE_TYPES and history_entry_count > 0 and native_evidence_complete,
"reason": None if native_evidence_complete else "One or more subshapes had no mapped or indexed name from the native API.",
}
return {

View File

@@ -0,0 +1,67 @@
import json
import FreeCAD as App
from freecad_mutation_evidence import capture_property_mutation
COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def primitive_case(type_id, properties, property_name, edited_value):
document = App.newDocument("{}MutationOracle".format(type_id.replace("::", "")))
try:
feature = document.addObject(type_id, "Feature")
for name, value in properties.items():
setattr(feature, name, value)
mutation = capture_property_mutation(document, feature, property_name, edited_value)
return mutation
finally:
App.closeDocument(document.Name)
def boolean_case(type_id):
document = App.newDocument("{}MutationOracle".format(type_id.replace("::", "")))
try:
base = document.addObject("Part::Box", "Base")
base.Length, base.Width, base.Height = 10.0, 9.0, 8.0
tool = document.addObject("Part::Cylinder", "Tool")
tool.Radius, tool.Height = 2.0, 12.0
tool.Placement.Base = App.Vector(5.0, 4.5, -2.0)
feature = document.addObject(type_id, "Feature")
feature.Base, feature.Tool = base, tool
return capture_property_mutation(document, feature, "Radius", 2.5, target=tool, property_path="Tool.Radius")
finally:
App.closeDocument(document.Name)
cases = [
primitive_case("Part::Box", {"Length": 10.0, "Width": 8.0, "Height": 6.0}, "Length", 12.0),
primitive_case("Part::Cylinder", {"Radius": 3.0, "Height": 8.0, "Angle": 360.0}, "Radius", 3.5),
primitive_case("Part::Sphere", {"Radius": 4.0}, "Radius", 4.5),
primitive_case("Part::Ellipsoid", {"Radius1": 2.0, "Radius2": 4.0, "Radius3": 0.0, "Angle1": -90.0, "Angle2": 90.0, "Angle3": 360.0}, "Radius1", 2.5),
primitive_case("Part::Cone", {"Radius1": 4.0, "Radius2": 2.0, "Height": 8.0, "Angle": 360.0}, "Height", 10.0),
primitive_case("Part::Torus", {"Radius1": 10.0, "Radius2": 2.0, "Angle1": -180.0, "Angle2": 180.0, "Angle3": 270.0}, "Radius2", 2.5),
primitive_case("Part::Helix", {"Pitch": 3.0, "Height": 6.0, "Radius": 2.0, "Angle": 0.0, "SegmentLength": 0.0}, "Radius", 2.5),
primitive_case("Part::Prism", {"Polygon": 6, "Circumradius": 2.0, "Height": 10.0, "FirstAngle": 10.0, "SecondAngle": -5.0}, "Height", 12.0),
primitive_case("Part::Wedge", {"Xmin": 0.0, "Ymin": 0.0, "Zmin": 0.0, "Z2min": 0.0, "X2min": 0.0, "Xmax": 10.0, "Ymax": 10.0, "Zmax": 10.0, "Z2max": 8.0, "X2max": 8.0}, "X2max", 7.0),
boolean_case("Part::Fuse"),
boolean_case("Part::Cut"),
boolean_case("Part::Common"),
]
report = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-core-parameter-mutation-oracle",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": COMMIT,
"status": "pass" if all(case["status"] == "pass" for case in cases) else "failed",
"summary": {
"families": len(cases),
"passed": sum(1 for case in cases if case["status"] == "pass"),
"shapeChanged": sum(1 for case in cases if case["shapeChanged"]),
"restoredExactly": sum(1 for case in cases if case["restoredExactly"]),
},
"cases": cases,
}
print("FREECAD_CORE_PARAMETER_MUTATION_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))

View File

@@ -3,6 +3,7 @@ import json
import FreeCAD as App
import Part
from freecad_mutation_evidence import capture_property_mutation
COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
@@ -62,8 +63,10 @@ def success_extrusion():
feature.Base = profile
feature.Dir = App.Vector(0, 0, 5)
feature.Solid = True
document.recompute()
return feature_status("part-extrusion-success", feature, "success")
mutation = capture_property_mutation(document, feature, "Dir", App.Vector(0, 0, 7))
result = feature_status("part-extrusion-success", feature, "success")
result["mutation"] = mutation
return result
finally:
App.closeDocument(document.Name)
@@ -78,8 +81,10 @@ def success_revolution():
feature.Base = App.Vector(0, 0, 0)
feature.Angle = 360
feature.Solid = True
document.recompute()
return feature_status("part-revolution-success", feature, "success")
mutation = capture_property_mutation(document, feature, "Angle", 270.0)
result = feature_status("part-revolution-success", feature, "success")
result["mutation"] = mutation
return result
finally:
App.closeDocument(document.Name)
@@ -94,8 +99,10 @@ def success_loft():
feature.Solid = True
feature.Ruled = False
feature.Closed = False
document.recompute()
return feature_status("part-loft-success", feature, "success")
mutation = capture_property_mutation(document, feature, "Ruled", True)
result = feature_status("part-loft-success", feature, "success")
result["mutation"] = mutation
return result
finally:
App.closeDocument(document.Name)
@@ -104,14 +111,17 @@ def success_sweep():
document = App.newDocument("PartSweepSuccessOracle")
try:
section = add_shape(document, "Section", rectangle_wire(-1, -1, 1, 1, 0))
edited_section = add_shape(document, "EditedSection", rectangle_wire(-0.75, -0.75, 0.75, 0.75, 0))
spine = add_shape(document, "Spine", Part.makePolygon([App.Vector(0, 0, 0), App.Vector(0, 0, 6)]))
feature = document.addObject("Part::Sweep", "Sweep")
feature.Sections = [section]
feature.Spine = spine
feature.Solid = True
feature.Frenet = False
document.recompute()
return feature_status("part-sweep-success", feature, "success")
mutation = capture_property_mutation(document, feature, "Sections", [edited_section])
result = feature_status("part-sweep-success", feature, "success")
result["mutation"] = mutation
return result
finally:
App.closeDocument(document.Name)
@@ -123,8 +133,10 @@ def success_fillet():
feature = document.addObject("Part::Fillet", "Fillet")
feature.Base = base
feature.Edges = [(1, 1, 1)]
document.recompute()
return feature_status("part-fillet-success", feature, "success")
mutation = capture_property_mutation(document, feature, "Edges", [(1, 0.5, 0.5)])
result = feature_status("part-fillet-success", feature, "success")
result["mutation"] = mutation
return result
finally:
App.closeDocument(document.Name)
@@ -136,8 +148,10 @@ def success_chamfer():
feature = document.addObject("Part::Chamfer", "Chamfer")
feature.Base = base
feature.Edges = [(1, 1, 1)]
document.recompute()
return feature_status("part-chamfer-success", feature, "success")
mutation = capture_property_mutation(document, feature, "Edges", [(1, 0.5, 0.5)])
result = feature_status("part-chamfer-success", feature, "success")
result["mutation"] = mutation
return result
finally:
App.closeDocument(document.Name)

View File

@@ -4,6 +4,8 @@ import FreeCAD as App
import Part
import Sketcher
from freecad_mutation_evidence import capture_property_mutation
def version_text():
return ".".join(str(value) for value in App.Version()[:3])
@@ -46,7 +48,9 @@ def tapered_pad():
pad.Type = "Length"
pad.Length = 5.0
pad.TaperAngle = 5.0
mutation = capture_property_mutation(document, pad, "Length", 6.0)
result = shape_result("pad-tapered", document, pad)
result["mutation"] = mutation
App.closeDocument(document.Name)
return result
@@ -69,7 +73,9 @@ def tapered_pocket():
pocket.Type = "Length"
pocket.Length = 10.0
pocket.TaperAngle = 2.0
mutation = capture_property_mutation(document, pocket, "Length", 8.0)
result = shape_result("pocket-tapered", document, pocket)
result["mutation"] = mutation
App.closeDocument(document.Name)
return result

View File

@@ -2,6 +2,8 @@ import json
import FreeCAD as App
from freecad_mutation_evidence import capture_property_mutation
def version_text():
return ".".join(str(value) for value in App.Version()[:3])
@@ -38,7 +40,9 @@ def selected_fillet():
feature = body.newObject("PartDesign::Fillet", "Fillet")
feature.Base = (box, ["Edge1"])
feature.Radius = 1
mutation = capture_property_mutation(document, feature, "Radius", 0.5)
result = shape_result("fillet-selected-edge", document, feature)
result["mutation"] = mutation
result["selection"] = ["Edge1"]
App.closeDocument(document.Name)
return result
@@ -49,7 +53,9 @@ def selected_chamfer():
feature = body.newObject("PartDesign::Chamfer", "Chamfer")
feature.Base = (box, ["Edge1"])
feature.Size = 1
mutation = capture_property_mutation(document, feature, "Size", 0.5)
result = shape_result("chamfer-selected-edge", document, feature)
result["mutation"] = mutation
result["selection"] = ["Edge1"]
App.closeDocument(document.Name)
return result
@@ -76,7 +82,9 @@ def selected_draft():
document.recompute()
if "Invalid" in feature.State:
feature.Reversed = False
mutation = capture_property_mutation(document, feature, "Angle", 30.0)
result = shape_result("draft-selected-face", document, feature)
result["mutation"] = mutation
result["selection"] = ["Face{}".format(top_face + 1)]
result["reversed"] = bool(feature.Reversed)
App.closeDocument(document.Name)
@@ -91,7 +99,9 @@ def selected_thickness():
feature.Reversed = True
feature.Mode = 0
feature.Join = 0
mutation = capture_property_mutation(document, feature, "Value", 0.8)
result = shape_result("thickness-selected-face", document, feature)
result["mutation"] = mutation
result["selection"] = ["Face1"]
result["reversed"] = bool(feature.Reversed)
result["elementMapSize"] = int(feature.Shape.ElementMapSize)

View File

@@ -6,6 +6,8 @@ import Part
import Sketcher
import TestSketcherApp
from freecad_mutation_evidence import capture_property_mutation
def version_text():
return ".".join(str(value) for value in App.Version()[:3])
@@ -38,7 +40,9 @@ def additive_loft():
feature = body.newObject("PartDesign::AdditiveLoft", "AdditiveLoft")
feature.Profile = profile
feature.Sections = [section]
mutation = capture_property_mutation(document, feature, "Ruled", True)
result = shape_result("additive-loft", document, feature)
result["mutation"] = mutation
App.closeDocument(document.Name)
return result
@@ -59,10 +63,17 @@ def subtractive_loft():
section.AttachmentSupport = (document.XZ_Plane, [""])
document.recompute()
TestSketcherApp.CreateRectangleSketch(section, (0, 1), (1, 1))
edited_section = body.newObject("Sketcher::SketchObject", "EditedSection")
edited_section.MapMode = "FlatFace"
edited_section.AttachmentSupport = (document.XZ_Plane, [""])
document.recompute()
TestSketcherApp.CreateRectangleSketch(edited_section, (0, 1), (0.75, 0.75))
feature = body.newObject("PartDesign::SubtractiveLoft", "SubtractiveLoft")
feature.Profile = profile
feature.Sections = [section]
mutation = capture_property_mutation(document, feature, "Sections", [edited_section])
result = shape_result("subtractive-loft", document, feature)
result["mutation"] = mutation
App.closeDocument(document.Name)
return result
@@ -70,6 +81,8 @@ def subtractive_loft():
def pipe_sketches(document, body):
profile = body.newObject("Sketcher::SketchObject", "Profile")
TestSketcherApp.CreateCircleSketch(profile, (0, 0), 1)
edited_profile = body.newObject("Sketcher::SketchObject", "EditedProfile")
TestSketcherApp.CreateCircleSketch(edited_profile, (0, 0), 0.75)
spine = body.newObject("Sketcher::SketchObject", "Spine")
spine.MapMode = "FlatFace"
spine.AttachmentSupport = (document.XZ_Plane, [""])
@@ -78,17 +91,19 @@ def pipe_sketches(document, body):
spine.addConstraint(Sketcher.Constraint("Coincident", 0, 1, -1, 1))
spine.addConstraint(Sketcher.Constraint("PointOnObject", 0, 2, -2))
spine.addConstraint(Sketcher.Constraint("DistanceY", 0, 1, 0, 2, 1))
return profile, spine
return profile, edited_profile, spine
def additive_pipe():
document = App.newDocument("AdditivePipeOracle")
body = document.addObject("PartDesign::Body", "Body")
profile, spine = pipe_sketches(document, body)
profile, edited_profile, spine = pipe_sketches(document, body)
feature = body.newObject("PartDesign::AdditivePipe", "AdditivePipe")
feature.Profile = profile
feature.Spine = spine
mutation = capture_property_mutation(document, feature, "Profile", edited_profile)
result = shape_result("additive-pipe", document, feature)
result["mutation"] = mutation
result["transition"] = str(feature.Transition)
App.closeDocument(document.Name)
return result
@@ -97,7 +112,7 @@ def additive_pipe():
def subtractive_pipe():
document = App.newDocument("SubtractivePipeOracle")
body = document.addObject("PartDesign::Body", "Body")
profile, spine = pipe_sketches(document, body)
profile, edited_profile, spine = pipe_sketches(document, body)
pad_sketch = body.newObject("Sketcher::SketchObject", "PadSketch")
TestSketcherApp.CreateRectangleSketch(pad_sketch, (-5, -5), (10, 10))
pad = body.newObject("PartDesign::Pad", "Pad")
@@ -107,7 +122,9 @@ def subtractive_pipe():
feature = body.newObject("PartDesign::SubtractivePipe", "SubtractivePipe")
feature.Profile = profile
feature.Spine = spine
mutation = capture_property_mutation(document, feature, "Profile", edited_profile)
result = shape_result("subtractive-pipe", document, feature)
result["mutation"] = mutation
result["transition"] = str(feature.Transition)
App.closeDocument(document.Name)
return result

View File

@@ -4,6 +4,8 @@ import FreeCAD as App
import Part
import Sketcher
from freecad_mutation_evidence import capture_property_mutation
COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
@@ -47,7 +49,8 @@ def run_revolution():
feature.ReferenceAxis = (sketch, ["H_Axis"])
feature.Angle = 360
document.recompute()
return {"id": "partdesign-revolution", "operation": "revolution", **status(feature)}
mutation = capture_property_mutation(document, feature, "Angle", 270.0)
return {"id": "partdesign-revolution", "operation": "revolution", "mutation": mutation, **status(feature)}
except Exception as error:
return {"id": "partdesign-revolution", "operation": "revolution", "passed": False, "error": str(error)}
finally:
@@ -74,7 +77,8 @@ def run_groove():
feature.ReferenceAxis = (sketch, ["H_Axis"])
feature.Angle = 180
document.recompute()
return {"id": "partdesign-groove", "operation": "groove", **status(feature)}
mutation = capture_property_mutation(document, feature, "Angle", 120.0)
return {"id": "partdesign-groove", "operation": "groove", "mutation": mutation, **status(feature)}
except Exception as error:
return {"id": "partdesign-groove", "operation": "groove", "passed": False, "error": str(error)}
finally:

View File

@@ -4,6 +4,8 @@ import FreeCAD as App
import Part
import Sketcher
from freecad_mutation_evidence import capture_property_mutation
def version_text():
return ".".join(str(value) for value in App.Version()[:3])
@@ -50,7 +52,9 @@ def feature_linear_pattern():
pattern.Direction = (document.X_Axis, [""])
pattern.Occurrences = 3
pattern.Length = 12
mutation = capture_property_mutation(document, pattern, "Occurrences", 4)
result = shape_result("linear-feature-list", document, pattern)
result["mutation"] = mutation
result.update({
"transformMode": pattern.TransformMode,
"originals": [item.Name for item in pattern.Originals],
@@ -79,7 +83,9 @@ def feature_mirrored():
mirrored.TransformMode = "Features"
mirrored.Originals = [boss]
mirrored.MirrorPlane = (document.YZ_Plane, [""])
mutation = capture_property_mutation(document, mirrored, "Refine", False)
result = shape_result("mirrored-feature-list", document, mirrored)
result["mutation"] = mutation
result.update({
"transformMode": mirrored.TransformMode,
"originals": [item.Name for item in mirrored.Originals],
@@ -105,7 +111,9 @@ def feature_polar_pattern():
pattern.Occurrences = 4
pattern.Refine = True
body.addObject(pattern)
mutation = capture_property_mutation(document, pattern, "Occurrences", 3)
result = shape_result("polar-feature-list", document, pattern)
result["mutation"] = mutation
result.update({
"transformMode": pattern.TransformMode,
"originals": [item.Name for item in pattern.Originals],
@@ -149,7 +157,9 @@ def feature_multi_transform():
polar.Occurrences = 4
body.addObject(polar)
multi.Transformations = [mirrored, linear, polar]
mutation = capture_property_mutation(document, multi, "Length", 15.0, target=linear, property_path="Transformations[1].Length")
result = shape_result("multi-transform-feature-list", document, multi)
result["mutation"] = mutation
result.update({
"transformMode": multi.TransformMode,
"originals": [item.Name for item in multi.Originals],
@@ -255,7 +265,9 @@ def iso_hole(modeled):
hole.ThreadDirection = "Left" if modeled else "Right"
hole.ThreadDepthType = "Dimension"
hole.ThreadDepth = 6
mutation = capture_property_mutation(document, hole, "Diameter", 6.5)
result = shape_result(case_id, document, hole)
result["mutation"] = mutation
result.update({
"threaded": bool(hole.Threaded),
"modeled": bool(hole.ModelThread),

View File

@@ -0,0 +1,86 @@
import hashlib
def _json_value(value):
if value is None or isinstance(value, (bool, int, float, str)):
return value
if hasattr(value, "Value") and hasattr(value, "Unit"):
return {"value": float(value.Value), "unit": str(value.Unit)}
if hasattr(value, "x") and hasattr(value, "y") and hasattr(value, "z"):
return [float(value.x), float(value.y), float(value.z)]
if hasattr(value, "Name") and hasattr(value, "TypeId"):
return {"name": str(value.Name), "typeId": str(value.TypeId)}
if isinstance(value, (list, tuple)):
return [_json_value(item) for item in value]
return str(value)
def _shape_snapshot(feature):
shape = feature.Shape
if shape.isNull():
raise RuntimeError("{} produced a null Shape".format(feature.Name))
if not shape.isValid():
raise RuntimeError("{} produced an invalid Shape".format(feature.Name))
brep = shape.exportBrepToString().encode("utf-8")
bounds = shape.BoundBox
return {
"brepSha256": hashlib.sha256(brep).hexdigest(),
"shapeType": str(shape.ShapeType),
"valid": True,
"solids": len(shape.Solids),
"faces": len(shape.Faces),
"edges": len(shape.Edges),
"vertices": len(shape.Vertexes),
"volume": float(shape.Volume),
"area": float(shape.Area),
"bounds": {
"min": [float(bounds.XMin), float(bounds.YMin), float(bounds.ZMin)],
"max": [float(bounds.XMax), float(bounds.YMax), float(bounds.ZMax)],
},
}
def capture_property_mutation(document, feature, property_name, edited_value, target=None, property_path=None):
target = target or feature
original_value = getattr(target, property_name)
before_parameter = _json_value(original_value)
document.recompute()
before_shape_object = feature.Shape.copy()
before_shape = _shape_snapshot(feature)
try:
setattr(target, property_name, edited_value)
document.recompute()
edited_parameter = _json_value(getattr(target, property_name))
if before_parameter == edited_parameter:
raise RuntimeError("{} mutation did not change {}".format(feature.TypeId, property_path or property_name))
edited_shape = _shape_snapshot(feature)
finally:
setattr(target, property_name, original_value)
document.recompute()
restored_shape_object = feature.Shape
restored_shape = _shape_snapshot(feature)
restored_parameter = _json_value(getattr(target, property_name))
if restored_parameter != before_parameter:
raise RuntimeError("{} did not restore {}".format(feature.TypeId, property_path or property_name))
restored_equivalent = bool(restored_shape_object.isEqual(before_shape_object))
geometry_equivalent = (restored_shape["shapeType"], restored_shape["solids"], restored_shape["faces"], restored_shape["edges"], restored_shape["vertices"], round(restored_shape["volume"], 7), round(restored_shape["area"], 7)) == (before_shape["shapeType"], before_shape["solids"], before_shape["faces"], before_shape["edges"], before_shape["vertices"], round(before_shape["volume"], 7), round(before_shape["area"], 7))
if not restored_equivalent and not geometry_equivalent:
raise RuntimeError("{} did not restore the original geometry".format(feature.TypeId))
return {
"schemaVersion": 1,
"familyId": str(feature.TypeId),
"featureName": str(feature.Name),
"propertyPath": property_path or property_name,
"status": "pass",
"before": {"parameterSnapshot": before_parameter, "shape": before_shape},
"edited": {"parameterSnapshot": edited_parameter, "shape": edited_shape},
"restored": {"parameterSnapshot": restored_parameter, "shape": restored_shape},
"parameterChanged": True,
"shapeChanged": edited_shape["brepSha256"] != before_shape["brepSha256"],
"restoredExactly": restored_equivalent or geometry_equivalent,
"brepStable": restored_shape["brepSha256"] == before_shape["brepSha256"],
"restoredGeometrically": True,
}

View File

@@ -49,6 +49,14 @@ const addVariant = (familyId, snapshot, source) => {
bucket.parameterVariants.set(stable(snapshot), { snapshot, source })
bucket.sources.add(source)
}
const addMutation = (mutation, source) => {
if (!mutation || typeof mutation !== 'object') return
const familyId = familyByType.get(mutation.familyId)
if (!familyId || mutation.status !== 'pass' || mutation.parameterChanged !== true || mutation.shapeChanged !== true || mutation.restoredExactly !== true) return
addRecord(familyId, 'editRecovery', { transaction: `native-parameter:${mutation.featureName}:${mutation.propertyPath}`, ...mutation }, source)
addVariant(familyId, mutation.before.parameterSnapshot, `${source}#before`)
addVariant(familyId, mutation.edited.parameterSnapshot, `${source}#edited`)
}
const manifestCases = async (manifestPath, entryKey, directory) => {
const manifest = await load(manifestPath)
const base = resolve(root, directory)
@@ -81,11 +89,16 @@ const addOracleCases = async (file, successKey, failureKey) => {
for (const key of [successKey, failureKey]) for (const fixture of report[key] || []) {
const familyId = familyByType.get(fixture.typeId)
if (!familyId) continue
if (key === successKey) addRecord(familyId, 'nominal', { id: fixture.id, typeId: fixture.typeId }, source)
if (key === successKey) {
addRecord(familyId, 'nominal', { id: fixture.id, typeId: fixture.typeId }, source)
addMutation(fixture.mutation, source)
}
else addRecord(familyId, 'invalid', { id: fixture.id, typeId: fixture.typeId }, source)
}
}
await addOracleCases('config/freecad-part-builders-oracle.json', 'successCases', 'failureCases')
const coreMutationOracle = await load('config/freecad-core-parameter-mutation-oracle.json')
for (const mutation of coreMutationOracle.cases || []) addMutation(mutation, 'config/freecad-core-parameter-mutation-oracle.json')
for (const file of ['config/freecad-partdesign-failure-oracle.json', 'config/freecad-partdesign-revolution-groove-oracle.json', 'config/freecad-partdesign-transform-oracle.json']) {
const report = await load(file)
for (const fixture of report.cases || []) {
@@ -93,18 +106,21 @@ for (const file of ['config/freecad-partdesign-failure-oracle.json', 'config/fre
if (!familyId) continue
if (fixture.passed === true && fixture.status === 'Valid' && fixture.shapeValid !== false && fixture.solids > 0) addRecord(familyId, 'nominal', { id: fixture.id, typeId: fixture.typeId }, file)
if (fixture.passed === true && (fixture.observed === 'rejected' || fixture.state?.includes('Invalid'))) addRecord(familyId, 'invalid', { id: fixture.id, typeId: fixture.typeId }, file)
addMutation(fixture.mutation, file)
}
}
const baseOracle = await load('config/freecad-partdesign-base-oracle.json')
for (const fixture of baseOracle.cases || []) {
const familyId = fixture.id.startsWith('pad-') ? 'PartDesign::Pad' : fixture.id.startsWith('pocket-') ? 'PartDesign::Pocket' : undefined
if (familyId && fixture.passed === true && fixture.status === 'Valid' && fixture.solids > 0) addRecord(familyId, 'nominal', { id: fixture.id, typeId: familyId, outcome: { volume: fixture.volume, faces: fixture.faces } }, 'config/freecad-partdesign-base-oracle.json')
if (familyId) addMutation(fixture.mutation, 'config/freecad-partdesign-base-oracle.json')
}
for (const file of ['config/freecad-partdesign-loft-oracle.json', 'config/freecad-partdesign-dressup-oracle.json']) {
const report = await load(file)
for (const fixture of report.cases || []) {
const familyId = familyByType.get(fixture.typeId)
if (familyId && fixture.passed === true && fixture.status === 'Valid' && fixture.solids > 0) addRecord(familyId, 'nominal', { id: fixture.id, typeId: fixture.typeId, outcome: { volume: fixture.volume, faces: fixture.faces } }, file)
addMutation(fixture.mutation, file)
}
}

View File

@@ -0,0 +1,29 @@
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
if (!existsSync(executable)) throw new Error(`FreeCAD core parameter mutation executable is missing: ${executable}`)
const sysroot = resolve(root, '.cache/freecad/sysroot')
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-core-parameter-mutation-oracle.py')], {
cwd: root,
encoding: 'utf8',
timeout: 180_000,
maxBuffer: 20 * 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_CORE_PARAMETER_MUTATION_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD core parameter mutation oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(resolve(root, 'config/freecad-core-parameter-mutation-oracle.json'), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))

View File

@@ -258,6 +258,56 @@ try {
if (!shape || !primitiveResaved.shapeResources.some((resource) => resource.path === shape.path && resource.status === 'available')) fail(`FreeCAD resave did not produce an available ${objectName}.Shape resource.`)
}
const throughHoleDocument: DocumentSnapshot = {
id: 'fcstd-native-part-cut-through-hole',
label: 'FCStd native Part Cut through hole',
version: 1,
dirty: false,
readOnly: false,
units: 'mm',
tree: ['Base', 'HoleTool', 'CutThroughHole'].map((id) => ({ id, label: id, type: 'feature', state: 'up-to-date' })),
objects: [
{ id: 'Base', typeId: 'Part::Box', properties: [
{ name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 20 },
{ name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 20 },
{ name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10 },
] },
{ id: 'HoleTool', typeId: 'Part::Cylinder', properties: [
{ name: 'Radius', label: 'Radius', group: 'Cylinder', scope: 'data', type: 'App::PropertyLength', value: 3 },
{ name: 'Height', label: 'Height', group: 'Cylinder', scope: 'data', type: 'App::PropertyLength', value: 12 },
{ name: 'Angle', label: 'Angle', group: 'Cylinder', scope: 'data', type: 'App::PropertyAngle', value: 360 },
{ name: 'Placement', label: 'Placement', group: 'Base', scope: 'data', type: 'App::PropertyPlacement', value: { position: { x: 10, y: 10, z: -1 }, rotation: { axis: { x: 0, y: 0, z: 1 }, angle: 0 } } },
] },
{ id: 'CutThroughHole', typeId: 'Part::Cut', properties: [
{ name: 'Base', label: 'Base', group: 'Boolean', scope: 'data', type: 'App::PropertyLink', value: 'Base' },
{ name: 'Tool', label: 'Tool', group: 'Boolean', scope: 'data', type: 'App::PropertyLink', value: 'HoleTool' },
{ name: 'Refine', label: 'Refine shape', group: 'Boolean', scope: 'data', type: 'App::PropertyBool', value: false },
] },
],
dependencies: [
{ sourceId: 'CutThroughHole', targetId: 'Base', relation: 'link', propertyName: 'Base' },
{ sourceId: 'CutThroughHole', targetId: 'HoleTool', relation: 'link', propertyName: 'Tool' },
],
recompute: { generation: 0, status: 'idle', objectStates: { Base: 'up-to-date', HoleTool: 'up-to-date', CutThroughHole: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] },
}
const throughHolePath = join(temporaryDirectory, 'native-part-cut-through-hole.fcstd')
const throughHoleResavedPath = join(temporaryDirectory, 'native-part-cut-through-hole-resaved.fcstd')
await writeFile(throughHolePath, serializeFcstdMetadataArchive(throughHoleDocument))
const throughHoleResult = runFreecadProbe(throughHolePath, 'CutThroughHole', {
FREECAD_FCSTD_RECOMPUTE: '1',
FREECAD_FCSTD_PROPERTIES: 'Base,Tool,Refine',
FREECAD_FCSTD_RESAVE_PATH: throughHoleResavedPath,
})
const throughHoleExpectedVolume = 4000 - 90 * Math.PI
if (throughHoleResult.freecadVersion !== '1.1.1' || throughHoleResult.shapeNull !== false || throughHoleResult.shapeValid !== true || throughHoleResult.shapeError !== null || throughHoleResult.solidCount !== 1 || throughHoleResult.faceCount !== 7 || !near(throughHoleResult.volume, throughHoleExpectedVolume, 1e-8) || !throughHoleResult.boundingBox || JSON.stringify(throughHoleResult.boundingBox.min) !== '[0,0,0]' || JSON.stringify(throughHoleResult.boundingBox.max) !== '[20,20,10]') fail(`unexpected native Part::Cut through-hole result: ${JSON.stringify(throughHoleResult)}`)
if (throughHoleResult.properties.Base !== 'Base' || throughHoleResult.properties.Tool !== 'HoleTool' || throughHoleResult.properties.Refine !== false || Object.keys(throughHoleResult.properties).length !== 3) fail(`native Part::Cut through-hole links did not restore: ${JSON.stringify(throughHoleResult.properties)}`)
const throughHoleResaved = inspectFcstdArchive(new Uint8Array(await readFile(throughHoleResavedPath)))
const throughHoleObject = throughHoleResaved.objects.find((object) => object.name === 'CutThroughHole') ?? fail('FreeCAD resave is missing CutThroughHole.')
const throughHoleShape = throughHoleObject.properties.find((property) => property.name === 'Shape')?.shapeResource
const throughHolePlacement = throughHoleResaved.objects.find((object) => object.name === 'HoleTool')?.properties.find((property) => property.name === 'Placement')
if (!throughHoleShape || !throughHoleResaved.shapeResources.some((resource) => resource.path === throughHoleShape.path && resource.status === 'available')) fail('FreeCAD resave did not produce the CutThroughHole.Shape resource.')
if (!throughHolePlacement || JSON.stringify(decodeFcstdPropertyValue(throughHolePlacement).value) !== '{"position":{"x":10,"y":10,"z":-1},"rotation":{"axis":{"x":0,"y":0,"z":1},"angle":0}}') fail('Web inspector did not recover the through-hole tool placement.')
const sphereTrimPath = join(temporaryDirectory, 'native-sphere-trim.fcstd')
const sphereTrimResavedPath = join(temporaryDirectory, 'native-sphere-trim-resaved.fcstd')
const sphereTrimProperties = { Radius: 5, Angle1: -45, Angle2: 45, Angle3: 120 }
@@ -658,6 +708,18 @@ try {
resavedShapeAvailable: primitiveResaved.shapeResources.some((resource) => resource.path === `${oracleCase.objectName}.Shape.brp` && resource.status === 'available'),
})
}),
scenario('part-cut-through-hole', 'web-freecad-web', ['objectTree', 'properties', 'shape', 'resources'], {
typeId: throughHoleObject.typeId,
solidCount: throughHoleResult.solidCount,
faceCount: throughHoleResult.faceCount,
volume: throughHoleResult.volume,
boundingBox: throughHoleResult.boundingBox,
propertyCount: Object.keys(throughHoleResult.properties).length,
properties: throughHoleResult.properties,
toolPlacement: decodeFcstdPropertyValue(throughHolePlacement).value,
refine: throughHoleResult.properties.Refine,
resavedShapeAvailable: true,
}),
scenario('part-sphere-trim', 'web-freecad-web', ['properties', 'shape', 'resources'], {
solidCount: sphereTrimResult.solidCount,
faceCount: sphereTrimResult.faceCount,

View File

@@ -22,8 +22,8 @@ const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_PART_BUILDERS_ORACLE_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.startsWith(marker))
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD Part builders oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(marker.length))
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(resolve(root, 'config/freecad-part-builders-oracle.json'), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))

View File

@@ -24,8 +24,8 @@ const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_PARTDESIGN_BASE_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.startsWith(marker))
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PartDesign base oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(marker.length))
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))

View File

@@ -22,8 +22,8 @@ const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_PARTDESIGN_DRESSUP_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.startsWith(marker))
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PartDesign dress-up oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(marker.length))
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(resolve(root, 'config/freecad-partdesign-dressup-oracle.json'), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))

View File

@@ -23,8 +23,8 @@ const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_PARTDESIGN_LOFT_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.startsWith(marker))
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PartDesign loft oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(marker.length))
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))

View File

@@ -19,9 +19,9 @@ const execution = spawnSync(command, ['--python-path', resolve(sysroot, 'usr/lib
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_PARTDESIGN_REVOLUTION_GROOVE_ORACLE_RESULT='
const line = output.split(/\r?\n/).find((entry) => entry.startsWith(marker))
const line = output.split(/\r?\n/).find((entry) => entry.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PartDesign Revolution/Groove oracle failed: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(marker.length))
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await mkdir(resolve(root, 'config'), { recursive: true })
await writeFile(outputFile, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-partdesign-revolution-groove-oracle-generated', output: outputFile, summary: report.summary }, null, 2))

View File

@@ -34,6 +34,7 @@ const lanes = {
...chromeTests
.map((name) => `check:${name.slice('test:'.length)}`)
.filter((name) => scripts[name]),
'check:freecad-tsn-stage-evidence',
],
oracle: [
'fetch:freecad-source',
@@ -41,6 +42,8 @@ const lanes = {
'build:freecad-native',
'probe:freecad-reference',
'check:freecad-desktop-oracle',
'probe:freecad-core-parameter-mutations',
'check:freecad-core-parameter-mutations',
'test:golden:freecad',
'test:golden:freecad:families',
'test:golden:freecad:failures',
@@ -55,14 +58,16 @@ const lanes = {
'probe:freecad-partdesign-revolution-groove',
'probe:freecad-part-builders',
'probe:freecad-composite-history-elementmap',
'test:freecad-fcstd-native',
'generate:freecad-parameter-mutations',
'check:freecad-oracle-coverage',
'check:freecad-golden-coverage',
'check:freecad-parameter-mutations',
'check:freecad-sketcher-constraints',
'check:freecad-fcstd-roundtrip',
'check:freecad-composite-history-elementmap',
'check:freecad-exact-history-elementmap-gate',
'check:freecad-native-naming-evidence',
'test:freecad-fcstd-native',
],
wasm: [
...wasmBuilds,