feat: close builder mutation and roundtrip evidence gaps
This commit is contained in:
18
scripts/check-freecad-core-parameter-mutation-oracle.mjs
Normal file
18
scripts/check-freecad-core-parameter-mutation-oracle.mjs
Normal 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))
|
||||
@@ -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))
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
67
scripts/freecad-core-parameter-mutation-oracle.py
Normal file
67
scripts/freecad-core-parameter-mutation-oracle.py
Normal 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=(",", ":")))
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
86
scripts/freecad_mutation_evidence.py
Normal file
86
scripts/freecad_mutation_evidence.py
Normal 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,
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
scripts/run-freecad-core-parameter-mutation-oracle.mjs
Normal file
29
scripts/run-freecad-core-parameter-mutation-oracle.mjs
Normal 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))
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user