feat: complete production naming and reference lifecycle gates
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled

This commit is contained in:
2026-08-14 10:10:30 -04:00
parent f97eae4153
commit f64b78865c
75 changed files with 57290 additions and 15675 deletions

View File

@@ -69,12 +69,12 @@ const report = {
productionPublication: false,
productionManifestGenerated: false,
boundary: {
freecadNamingBuildStatus: 'contract-only',
exTsn02: 'in_progress',
freecadNamingBuildStatus: 'production-linked',
exTsn02: 'completed',
systemExact: false,
workerLinked: false,
availability: 'unavailable',
callbacks: [],
workerLinked: true,
availability: 'available',
callbacks: ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'],
},
}
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)

View File

@@ -0,0 +1,36 @@
import { createHash } from 'node:crypto'
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/chrome-partdesign-reference-lifecycle-verification.json'), 'utf8'))
const evidencePaths = [
'public/chrome-partdesign-reference-lifecycle-harness.html',
'src/facade/externalPartDesignLinks.ts',
'src/facade/mockFacade.ts',
'src/facade/geometryRuntime.ts',
'src/facade/geometryWorker.ts',
'node_modules/@bitbybit-dev/occt/bitbybit-dev-occt/bitbybit-dev-occt.a4a6ec2a.wasm',
]
const fail = (message) => { throw new Error(`Chrome PartDesign reference lifecycle: ${message}`) }
const same = (actual, expected) => JSON.stringify(actual) === JSON.stringify(expected)
const linked = (snapshot) => snapshot?.source?.status === 'open' && snapshot.properties?.XLink?.objectId === 'SourceBox' && snapshot.properties.XLinkSub?.subElements?.[0] === 'Face1' && snapshot.properties.XLinkList?.length === 2 && snapshot.properties.XLinkSubList?.length === 2 && snapshot.binder?.cacheState === 'linked' && snapshot.binder.support?.length === 1 && same(Object.values(snapshot.propertyStatus ?? {}), [['21'], ['21'], ['21'], ['21']])
const unresolved = (snapshot, status) => snapshot?.source?.status === status && snapshot.properties?.XLink === null && snapshot.properties.XLinkSub === null && snapshot.properties.XLinkList?.length === 0 && snapshot.properties.XLinkSubList?.length === 0 && snapshot.binder?.cacheState === 'cached' && snapshot.binder.support?.length === 0 && snapshot.binder.area === 30
const initialDimensions = (snapshot) => snapshot?.dimensions?.xLinkLength === 4 && snapshot.dimensions.xLinkSubWidth === 5 && same(snapshot.dimensions.xLinkListHeights, [6, 2]) && same(snapshot.dimensions.xLinkSubListAreas, [30, 14, 14]) && snapshot.binder?.area === 30
const editedDimensions = (snapshot) => snapshot?.dimensions?.xLinkLength === 8 && snapshot.dimensions.xLinkSubWidth === 9 && same(snapshot.dimensions.xLinkListHeights, [6, 4]) && same(snapshot.dimensions.xLinkSubListAreas, [54, 28, 28]) && snapshot.binder?.area === 54
if (report.schemaVersion !== 1 || report.browserId !== 'chrome' || report.status !== 'pass' || report.crossOriginIsolated !== true || !/HeadlessChrome\/150\./.test(report.userAgent ?? '')) fail('evidence is not passing, isolated, or from the locked Chrome baseline.')
if (!Array.isArray(report.evidenceInputs) || !same(report.evidenceInputs.map(({ path }) => path), evidencePaths)) fail('evidence input inventory is incomplete.')
for (const artifact of report.evidenceInputs) {
const content = await readFile(resolve(root, artifact.path))
const sha256 = createHash('sha256').update(content).digest('hex')
if (artifact.bytes !== content.byteLength || artifact.sha256 !== sha256) fail(`stale implementation or artifact evidence for ${artifact.path}.`)
}
if (!linked(report.initial) || !initialDimensions(report.initial) || report.initial.history?.undo !== 0) fail('initial external reference graph is invalid.')
if (!unresolved(report.closed, 'closed') || !linked(report.relinkedAfterClose) || !initialDimensions(report.relinkedAfterClose)) fail('source close/relink behavior is invalid.')
if (!unresolved(report.missing, 'missing') || !unresolved(report.reopenedMissing, 'missing') || !linked(report.relinkedAfterMissing) || !initialDimensions(report.relinkedAfterMissing)) fail('OPFS missing-source reopen/relink behavior is invalid.')
if (!linked(report.edited) || !editedDimensions(report.edited) || report.edited.history?.undo !== 1 || !linked(report.undone) || !initialDimensions(report.undone) || report.undone.history?.redo !== 1 || !linked(report.redone) || !editedDimensions(report.redone)) fail('external source edit Undo/Redo behavior is invalid.')
if (!linked(report.reopened) || !editedDimensions(report.reopened) || !linked(report.reopenedUndo) || !initialDimensions(report.reopenedUndo) || !linked(report.reopenedRedo) || !editedDimensions(report.reopenedRedo)) fail('OPFS round-trip did not preserve external link history.')
if (report.released?.shapeCount !== 0 || report.released?.kernelReferenceCount !== 0 || report.released?.opfsRemoved !== true) fail('Chrome lifecycle leaked geometry or OPFS state.')
console.log(JSON.stringify({ status: 'chrome-partdesign-reference-lifecycle-pass', browserId: report.browserId, propertyKinds: ['XLink', 'XLinkSub', 'XLinkList', 'XLinkSubList'], sourceCloseRelink: true, missingSourceRoundtrip: true, editUndoRedo: true, opfsHistoryRoundtrip: true, released: { shapeCount: report.released.shapeCount, kernelReferenceCount: report.released.kernelReferenceCount } }, null, 2))

View File

@@ -0,0 +1,36 @@
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const oracle = JSON.parse(await readFile(resolve(root, 'config/freecad-attachment-combination-oracle.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD attachment combination oracle: ${message}`) }
const same = (actual, expected) => JSON.stringify(actual) === JSON.stringify(expected)
const near = (actual, expected) => typeof actual === 'number' && Math.abs(actual - expected) <= oracle.tolerance
const valid = (state, mode) => state?.mapMode === mode && state.finitePlacement === true && same(state.state, ['Up-to-date']) && state.status === 'Valid' && state.positionBySupport === true && state.suggestionMessage === 'OK' && state.suggestedModes?.includes(mode)
if (oracle.schemaVersion !== 1 || oracle.baselineId !== 'freecad-1.1.1-attachment-combination-oracle' || oracle.freecadVersion !== '1.1.1' || oracle.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || oracle.status !== 'pass' || oracle.tolerance !== 1e-7) fail('baseline is invalid.')
const expectedEngineCounts = { plane: 85, line: 44, point: 22, sketch: 85 }
if (oracle.caseCount !== 236 || Object.entries(expectedEngineCounts).some(([engine, count]) => oracle.engineCaseCounts?.[engine] !== count) || Object.keys(oracle.engineCaseCounts ?? {}).length !== 4 || oracle.cases?.length !== oracle.caseCount) fail('the implemented engine/reference combination census is incomplete.')
if (!same(oracle.translation, [1.25, -0.75, 2])) fail('source mutation vector changed.')
const identities = new Set()
for (const entry of oracle.cases) {
const identity = `${entry.engine}\0${entry.mode}\0${entry.combinationIndex}`
if (identities.has(identity)) fail(`duplicate case ${entry.id}.`)
identities.add(identity)
if (!Array.isArray(entry.referenceCombination) || entry.referenceCombination.length === 0 || entry.referenceCombination.some((value) => typeof value !== 'string' || !value)) fail(`${entry.id} has no executable reference combination.`)
if (!valid(entry.initial, entry.mode)) fail(`${entry.id} initial execution is invalid: ${entry.initial?.status}`)
if (!valid(entry.mutated, entry.mode)) fail(`${entry.id} source mutation is invalid: ${entry.mutated?.status}`)
if (!valid(entry.roundtrip, entry.mode)) fail(`${entry.id} FCStd round-trip is invalid: ${entry.roundtrip?.status}`)
if (!same(entry.initial.support, entry.mutated.support) || !same(entry.mutated.support, entry.roundtrip.support)) fail(`${entry.id} support references drifted.`)
for (let index = 0; index < 3; index += 1) {
const initial = entry.initial.placement.position[index]
const mutated = entry.mutated.placement.position[index]
if (!near(mutated - initial, oracle.translation[index])) fail(`${entry.id} did not follow the source translation on axis ${index}.`)
if (!near(entry.roundtrip.placement.position[index], mutated)) fail(`${entry.id} placement drifted after FCStd reopen on axis ${index}.`)
}
for (let index = 0; index < 3; index += 1) if (!near(entry.roundtrip.placement.axis[index], entry.mutated.placement.axis[index])) fail(`${entry.id} rotation axis drifted after FCStd reopen.`)
if (!near(entry.roundtrip.placement.angleDegrees, entry.mutated.placement.angleDegrees)) fail(`${entry.id} rotation angle drifted after FCStd reopen.`)
}
console.log(JSON.stringify({ status: 'freecad-attachment-combination-oracle-pass', baselineId: oracle.baselineId, cases: oracle.caseCount, engines: oracle.engineCaseCounts, allImplementedModes: true, allReferenceCombinations: true, sourceMutation: oracle.translation, fcstdRoundtrip: true }, null, 2))

View File

@@ -6,8 +6,11 @@ import { migrateStringHasherSchema, parseStringHasherTable, validateStringHasher
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-composite-history-elementmap-oracle.json'), 'utf8'))
const resaveReport = JSON.parse(await readFile(resolve(root, 'config/freecad-composite-history-resave-verification.json'), 'utf8'))
if (report.schemaVersion !== 1 || report.baselineId !== 'freecad-1.1.1-composite-history-elementmap2' || report.freecadVersion !== '1.1.1' || report.status !== 'pass' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('FreeCAD composite history ElementMap2 oracle baseline is invalid.')
if (report.summary?.cases !== 30 || report.summary?.passed !== 30 || report.summary?.failed !== 0 || !Array.isArray(report.cases) || report.cases.length !== 30) throw new Error('FreeCAD composite oracle must contain exactly 30 passing cases.')
if (resaveReport.schemaVersion !== 1 || resaveReport.baselineId !== report.baselineId || resaveReport.freecadVersion !== report.freecadVersion || resaveReport.gitCommit !== report.gitCommit || resaveReport.status !== 'pass' || resaveReport.summary?.cases !== 30 || resaveReport.summary?.passed !== 30 || resaveReport.summary?.failed !== 0 || resaveReport.summary?.roundtripNameDrift !== 0 || resaveReport.summary?.resaveNameDrift !== 0 || resaveReport.summary?.nativeDesktopResaveCases !== 30 || !Array.isArray(resaveReport.cases) || resaveReport.cases.length !== 30) throw new Error('FreeCAD composite resave oracle must contain exactly 30 passing save/reopen/resave cases.')
for (const fixture of resaveReport.cases) if (fixture.roundtripNameDrift !== 0 || fixture.resaveNameDrift !== 0 || fixture.nativeDesktopResaveCovered !== true) throw new Error(`FreeCAD composite fixture ${fixture.id} changed mapped names after native FreeCAD resave.`)
let parsedResources = 0
let parsedStringHasherResources = 0
let namingEvidenceStages = 0
@@ -74,4 +77,4 @@ for (const fixture of report.cases) {
if (parsedResources === 0) throw new Error('FreeCAD composite oracle did not capture any ElementMap2 resources.')
if (nativeIndexedNameStages !== namingEvidenceStages) throw new Error(`FreeCAD composite oracle must retain direct indexed-name evidence for every stage, found ${nativeIndexedNameStages}/${namingEvidenceStages}.`)
if (namingEvidenceMissing !== 0 || namingEvidenceBoundaryViolations !== 0) throw new Error(`FreeCAD naming evidence boundary is incomplete: missing=${namingEvidenceMissing}, violations=${namingEvidenceBoundaryViolations}.`)
console.log(JSON.stringify({ status: 'freecad-composite-history-elementmap-pass', cases: report.summary.cases, parsedResources, parsedStringHasherResources, namingEvidenceStages, nativeEvidenceValidatedStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: nativeIndexedNameStages - nativeMappedNameStages, privateTokenEvidenceCompleteStages, namingEvidenceMissing, namingEvidenceBoundaryViolations, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages }, null, 2))
console.log(JSON.stringify({ status: 'freecad-composite-history-elementmap-pass', cases: report.summary.cases, parsedResources, parsedStringHasherResources, namingEvidenceStages, nativeEvidenceValidatedStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: nativeIndexedNameStages - nativeMappedNameStages, privateTokenEvidenceCompleteStages, namingEvidenceMissing, namingEvidenceBoundaryViolations, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages, nativeDesktopResaveCases: resaveReport.summary.nativeDesktopResaveCases, resaveNameDrift: resaveReport.summary.resaveNameDrift }, null, 2))

View File

@@ -235,7 +235,7 @@ if (nativeEvidenceValidatedStages !== nativeEvidenceStages) globalBlockers.push(
if (nativeIndexedNameStages !== nativeEvidenceStages) globalBlockers.push(`${nativeEvidenceStages - nativeIndexedNameStages} native-evidence stages have no direct FreeCAD indexed-name identity`)
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')
globalBlockers.push('Production private naming ABI is available and non-unique isomorphic provenance is fail-closed; exhaustive cross-feature mutation, FCStd round-trip closure and complete native document/property semantics remain open')
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')

View File

@@ -0,0 +1,33 @@
import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-isomorphic-provenance-verification.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD isomorphic provenance check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.runtime !== 'node' || report.implementation !== 'freecad-linked' || report.productionWorkerLinked !== true || report.systemExact !== false) fail('report boundary is invalid')
const symmetric = report.cases?.symmetricCommon
if (symmetric?.status !== 'ambiguous' || symmetric.stable !== 0 || symmetric.ambiguous < 1 || symmetric.candidates < symmetric.ambiguous * 2 || symmetric.persistedCandidateSets !== true) fail('symmetric Common evidence is incomplete')
const mixed = report.cases?.mixedCut
if (mixed?.status !== 'ambiguous' || mixed.stable < 1 || mixed.ambiguous < 1 || mixed.elementMaps < 1) fail('mixed Cut evidence is incomplete')
const downstream = report.cases?.downstreamRotate
if (downstream?.status !== 'ambiguous' || downstream.ambiguous < 1 || !downstream.candidateObjects?.includes('cut-object') || !downstream.candidateObjects?.includes('cut-tool')) fail('cross-feature candidate propagation is incomplete')
const longChain = report.cases?.longChain
if (JSON.stringify(longChain?.operations) !== '["cut","rotate","fillet","mirrored","linear-pattern"]' || longChain.stages?.length !== 5 || longChain.persistedFinalEvidence !== true || longChain.referencedCandidateObjectsRetained !== true || longChain.stringHasherMonotonic !== true) fail('long-chain naming evidence is incomplete')
for (const [index, stage] of longChain.stages.entries()) {
if (stage.status !== 'ambiguous' || stage.stable < 1 || stage.ambiguous < 1 || stage.candidates < stage.ambiguous * 2 || (index > 0 && stage.persistedInputEvidence !== true)) fail(`long-chain stage ${String(stage.operation)} did not preserve stable and ambiguous provenance`)
if (index > 0 && (stage.referencedCandidateObjectsRetained !== true || stage.elementMaps < 1 || stage.referencedInputCandidateObjects?.some((objectId) => !stage.candidateObjects?.includes(objectId)))) fail(`long-chain stage ${String(stage.operation)} lost referenced provenance or ElementMap2 evidence`)
if (index > 1 && stage.stringHasherEntries < longChain.stages[index - 1].stringHasherEntries) fail(`long-chain stage ${String(stage.operation)} regressed StringHasher state`)
}
if (report.fcstdAmbiguityRejected !== true) fail('FCStd fail-closed evidence is absent')
if (JSON.stringify(report.fcstdStableLinkRoundtrip?.subElements) !== '["Face1"]' || report.fcstdStableLinkRoundtrip.inspectRewritePreserved !== true || report.fcstdStableLinkRoundtrip.nativeDesktopResaveCovered !== false) fail('FCStd stable LinkSub persistence boundary is incomplete')
for (const artifact of report.artifacts ?? []) {
const path = resolve(root, 'native/occt-history/dist', artifact.name)
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
if (bytes !== artifact.bytes || createHash('sha256').update(content).digest('hex') !== artifact.sha256) fail(`report is stale for ${artifact.name}`)
}
if (report.artifacts?.length !== 3) fail('JS/WASM/DATA artifact lock is incomplete')
const harnessPath = resolve(root, report.harness?.path ?? '')
const [harnessContent, harnessBytes] = await Promise.all([readFile(harnessPath), stat(harnessPath).then(({ size }) => size)])
if (report.harness?.path !== 'scripts/run-freecad-isomorphic-provenance.ts' || harnessBytes !== report.harness.bytes || createHash('sha256').update(harnessContent).digest('hex') !== report.harness.sha256) fail('report is stale for its executable harness')
console.log(JSON.stringify({ status: 'freecad-isomorphic-provenance-pass', symmetricAmbiguities: symmetric.ambiguous, downstreamAmbiguities: downstream.ambiguous, longChainStages: longChain.stages.length, fcstdAmbiguityRejected: true, fcstdStableLinkRoundtrip: true, systemExact: false }, null, 2))

View File

@@ -15,8 +15,8 @@ for (const task of plan.orderedTasks) {
for (const dependency of task.dependencies ?? []) if (!taskById.has(dependency)) fail(`${task.id} depends on unknown task ${dependency}.`)
for (const evidence of task.evidence) if (!packageJson.scripts?.[evidence]) fail(`${task.id} references missing npm script ${evidence}.`)
}
if (taskById.get('SDK-01').status !== 'completed' || taskById.get('SDK-02A').status !== 'completed' || taskById.get('SDK-02B').status !== 'completed' || taskById.get('SDK-04').status !== 'completed' || taskById.get('SDK-03').status !== 'completed' || taskById.get('SDK-05').status !== 'completed' || taskById.get('PAR-01').status !== 'in_progress') fail('current SDK/parameter task status is stale.')
if (taskById.get('SDK-01').status !== 'completed' || taskById.get('SDK-02A').status !== 'completed' || taskById.get('SDK-02B').status !== 'completed' || taskById.get('SDK-04').status !== 'completed' || taskById.get('SDK-03').status !== 'completed' || taskById.get('SDK-05').status !== 'completed' || taskById.get('PAR-01').status !== 'completed') fail('current SDK/parameter task status is stale.')
if (JSON.stringify(taskById.get('SDK-03').dependencies) !== JSON.stringify(['SDK-02B', 'SDK-04'])) fail('SDK-03 dependencies must retain both the archive set and isolated bridge.')
if (taskById.get('SDK-04').dependencies?.length !== 1 || taskById.get('SDK-04').dependencies[0] !== 'SDK-02B') fail('SDK-04 must start directly from the completed archive set.')
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false || plan.boundary?.productionWorker?.availability !== 'unavailable' || plan.boundary?.productionWorker?.callbacks?.length !== 0) fail('production boundary changed before the candidate Worker gate.')
if (plan.boundary?.freecadNamingBuildStatus !== 'production-linked' || plan.boundary?.exTsn02 !== 'completed' || plan.boundary?.systemExact !== false || plan.boundary?.productionWorker?.availability !== 'available' || plan.boundary?.productionWorker?.callbacks?.length !== 3) fail('production boundary does not identify the callback-complete linked Worker.')
console.log(JSON.stringify({ status: 'freecad-naming-next-tasks-pass', tasks: plan.orderedTasks.length, completed: plan.orderedTasks.filter(({ status }) => status === 'completed').length, inProgress: plan.orderedTasks.filter(({ status }) => status === 'in_progress').map(({ id }) => id), boundary: plan.boundary }, null, 2))

View File

@@ -0,0 +1,54 @@
import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
import { FREECAD_NAMING_CALLBACKS, FREECAD_NAMING_OPERATIONS } from './freecad-naming-production-matrix.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const fail = (message) => { throw new Error(`FreeCAD production naming verification failed: ${message}`) }
const checkReport = async (reportName, runtime, artifactRoot) => {
const report = JSON.parse(await readFile(resolve(root, 'config', reportName), 'utf8'))
if (report.status !== 'pass' || report.runtime !== runtime || report.implementation !== 'freecad-linked' || report.productionWorkerLinked !== true || report.systemExact !== false) fail(`${reportName} has an invalid production boundary.`)
if (JSON.stringify(report.callbacks) !== JSON.stringify(FREECAD_NAMING_CALLBACKS)) fail(`${reportName} callback list is incomplete.`)
if (JSON.stringify(report.operations) !== JSON.stringify(FREECAD_NAMING_OPERATIONS) || report.cases?.length !== FREECAD_NAMING_OPERATIONS.length) fail(`${reportName} operation matrix is incomplete.`)
if (report.cases.some((entry, index) => {
const native = entry.selectedRecords > 0
return entry.operation !== FREECAD_NAMING_OPERATIONS[index]
|| entry.status !== 'pass'
|| entry.provenanceResults < 1
|| entry.provenanceResults !== entry.selectedRecords + entry.ambiguousResults
|| entry.externallySourcedRecords < 1
|| entry.internalRecordsExcluded < 0
|| entry.externallySourcedRecords + entry.internalRecordsExcluded !== entry.historyRecords
|| entry.duplicateRecordsCollapsed < 0
|| entry.modifiedRelationPrecedence < 0
|| entry.callbackExecuted !== native
|| entry.mappedNames !== entry.selectedRecords
|| (native && (entry.stringHasherEntries < 0 || entry.elementMaps < 1 || !['native-evidence', 'mixed-native-and-ambiguous'].includes(entry.namingStatus)))
|| (!native && (entry.ambiguousResults < 1 || entry.stringHasherEntries !== 0 || entry.elementMaps !== 0 || entry.namingStatus !== 'ambiguous'))
})) fail(`${reportName} contains an invalid operation result.`)
const callbackCases = report.cases.filter(({ callbackExecuted }) => callbackExecuted).length
const ambiguityOnlyCases = report.cases.filter(({ namingStatus }) => namingStatus === 'ambiguous').length
if (report.callbackExecutedCases !== callbackCases || report.ambiguityOnlyCases !== ambiguityOnlyCases || callbackCases < 1 || callbackCases + ambiguityOnlyCases !== FREECAD_NAMING_OPERATIONS.length) fail(`${reportName} has invalid callback/ambiguity partitioning.`)
if (report.isomorphicCorpus?.status !== 'pass' || report.isomorphicCorpus.fixture !== 'coincident-equal-boxes' || report.isomorphicCorpus.uniqueResults !== 0 || report.isomorphicCorpus.ambiguousResults < 1 || report.isomorphicCorpus.arrayOrderFallbackUsed !== false || report.isomorphicCorpus.candidateSets?.some(({ candidates }) => candidates.length < 2)) fail(`${reportName} is missing its symmetric native-provenance corpus.`)
if (report.invalidHistoryRejected !== true || report.chainedStage?.mappedNames < 1 || report.chainedStage?.stringHasherEntries < 2) fail(`${reportName} is missing chained or fail-closed evidence.`)
for (const artifact of report.artifacts ?? []) {
const path = resolve(root, artifactRoot, artifact.name)
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
if (bytes !== artifact.bytes || createHash('sha256').update(content).digest('hex') !== artifact.sha256) fail(`${reportName} is stale for ${artifact.name}.`)
}
if (report.artifacts?.length !== 3) fail(`${reportName} must lock JS, WASM and DATA.`)
const harnessPath = resolve(root, report.harness?.path ?? '')
const [harnessContent, harnessBytes] = await Promise.all([readFile(harnessPath), stat(harnessPath).then(({ size }) => size)])
if (report.harness?.path !== 'scripts/freecad-naming-production-matrix.mjs' || harnessBytes !== report.harness.bytes || createHash('sha256').update(harnessContent).digest('hex') !== report.harness.sha256) fail(`${reportName} is stale for the shared production harness.`)
return { runtime, cases: report.cases.length, generatedAt: report.generatedAt }
}
const requestedRuntime = process.argv.find((argument) => argument.startsWith('--runtime='))?.slice('--runtime='.length) ?? 'all'
if (!['all', 'node', 'chrome'].includes(requestedRuntime)) fail(`unsupported runtime ${requestedRuntime}.`)
const specs = [
['freecad-naming-production-verification.json', 'node', 'native/occt-history/dist'],
['chrome-freecad-naming-production-verification.json', 'chrome', 'public/native/occt-history'],
].filter(([, runtime]) => requestedRuntime === 'all' || runtime === requestedRuntime)
const reports = []
for (const [reportName, runtime, artifactRoot] of specs) reports.push(await checkReport(reportName, runtime, artifactRoot))
console.log(JSON.stringify({ status: 'freecad-production-naming-pass', implementation: 'freecad-linked', callbacks: FREECAD_NAMING_CALLBACKS, operations: FREECAD_NAMING_OPERATIONS.length, reports, systemExact: false }, null, 2))

View File

@@ -22,7 +22,7 @@ const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK readiness:
if (plan.schemaVersion !== 1 || plan.scope !== 'pre-production-sdk-readiness') fail('unsupported plan schema or scope.')
if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to the required FreeCAD/Emscripten wasm target.')
if (plan.productionPublication !== false) fail('candidate SDK readiness cannot publish a production Worker.')
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false) fail('plan boundary must remain contract-only/in_progress/non-exact.')
if (plan.boundary?.freecadNamingBuildStatus !== 'production-linked' || plan.boundary?.exTsn02 !== 'completed' || plan.boundary?.systemExact !== false) fail('plan boundary must identify the completed production linkage while retaining non-exact system status.')
if (JSON.stringify(plan.libraries?.map(({ name }) => name)) !== JSON.stringify(REQUIRED_FREECAD_NAMING_LIBRARIES)) fail('library list or order does not match the production SDK contract.')
if (!REQUIRED_FREECAD_NAMING_CALLBACKS.every((callback) => plan.namingBridge?.requiredExports?.includes(callback))) fail('naming bridge plan does not require all production callbacks.')
if (plan.compileOptions?.cxxStandard !== 'c++20' || plan.compileOptions?.pthread !== true || JSON.stringify(plan.compileOptions?.definitions) !== JSON.stringify(REQUIRED_FREECAD_NAMING_DEFINITIONS)) fail('compile options must lock C++20, pthread and the FreeCAD wasm compatibility definitions.')

View File

@@ -19,7 +19,7 @@ const required = (value, name) => {
const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK: ${message}`) }
if (!sdkRoot) {
console.log(JSON.stringify({ status: 'sdk-not-configured', availability: 'unavailable', systemExact: false, reason: 'FREECAD_WASM_SDK_DIR is not configured; the shipped Worker remains OCCT-only.' }, null, 2))
console.log(JSON.stringify({ status: 'sdk-not-configured', availability: 'unavailable', systemExact: false, reason: 'FREECAD_WASM_SDK_DIR is not configured; the already-published production Worker must be checked through its artifact and production reports.' }, null, 2))
process.exit(0)
}
@@ -32,7 +32,7 @@ if (required(manifest.freecadVersion, 'freecadVersion') !== '1.1.1') fail('freec
if (required(manifest.sourceCommit, 'sourceCommit') !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('sourceCommit is not the locked FreeCAD commit.')
if (required(manifest.emscriptenVersion, 'emscriptenVersion') !== '3.1.69') fail('emscriptenVersion must be 3.1.69.')
if (required(manifest.qtTarget, 'qtTarget') !== 'wasm32-emscripten' || required(manifest.pythonTarget, 'pythonTarget') !== 'wasm32-emscripten') fail('Qt and Python must both be wasm32-emscripten targets.')
if (manifest.productionPublication !== false || manifest.boundary?.freecadNamingBuildStatus !== 'contract-only' || manifest.boundary?.exTsn02 !== 'in_progress' || manifest.boundary?.systemExact !== false) fail('manifest must preserve the candidate-only contract-only/in_progress/non-exact boundary.')
if (manifest.productionPublication !== false || manifest.boundary?.freecadNamingBuildStatus !== 'production-linked' || manifest.boundary?.exTsn02 !== 'completed' || manifest.boundary?.systemExact !== false) fail('manifest must remain a non-publishing SDK manifest while reflecting the completed production linkage and non-exact system boundary.')
const includeDirs = Array.isArray(manifest.includeDirs) ? manifest.includeDirs : fail('includeDirs must be an array.')
const resolvedIncludeDirs = includeDirs.map((includeDir) => isAbsolute(includeDir) ? includeDir : resolve(sdkRoot, includeDir))
const libraries = Array.isArray(manifest.libraries) ? manifest.libraries : fail('libraries must be an array.')

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 transport, builder naming transport and isomorphic-source boundaries.')
if (!Array.isArray(blockers) || blockers.length !== 2 || blockers.some((blocker) => blocker.includes('lacks the FreeCAD-linked') || blocker.includes('browser builder naming evidence transport') || blocker.includes('non-unique isomorphic'))) fail('exact blocker list must remove the closed production linkage and isomorphic provenance boundaries and retain two broader exactness 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.`)

View File

@@ -0,0 +1,19 @@
import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-native-property-semantics.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD native property semantics check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.exactPromotionReady !== false || report.exactBlocker !== 'complete native document and property semantics') fail('report boundary is invalid')
if (report.runtime?.registeredObjectTypes !== 352 || report.runtime.instantiableObjectTypes !== 348 || report.runtime.unavailableObjectTypes !== 4 || report.runtime.propertyRecords !== 5510 || report.runtime.propertyTypes !== 85 || report.runtime.unavailableObjects?.length !== 4) fail('runtime inventory counts changed')
const support = report.supportSummary
if (support?.['native-editable-codec']?.typeCount !== 18 || support['native-editable-codec'].recordCount !== 4135 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 62 || support['opaque-fcstd-proxy'].recordCount !== 692) fail('property support partition is stale')
if (report.types?.length !== 85 || new Set(report.types.map(({ typeId }) => typeId)).size !== 85 || report.types.reduce((total, entry) => total + entry.recordCount, 0) !== 5510) fail('per-TypeId inventory is invalid')
if (report.propertyStatus?.unknownNumericBits?.length !== 0 || report.propertyStatus.proxyOnlyRecordCount !== 2639 || report.propertyStatus.facadeNative?.join(',') !== 'Hidden,ReadOnly' || report.propertyStatus.proxyOnly?.length !== 13) fail('property status coverage is stale')
for (const locked of [report.source, report.harness]) {
const path = resolve(root, locked?.path ?? '')
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
if (bytes !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`report is stale for ${locked.path}`)
}
console.log(JSON.stringify({ status: 'freecad-native-property-semantics-pass', propertyTypes: 85, propertyRecords: 5510, nativeCodecRecords: 4818, opaqueProxyRecords: 692, proxyOnlyStatusRecords: 2639, exactPromotionReady: false }, null, 2))

View File

@@ -3,81 +3,71 @@ import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const load = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8'))
const [abi, matrix, plan, readiness, sdkPlan, nextTasks, buildScript, smokeTest] = await Promise.all([
const [abi, matrix, plan, readiness, sdkPlan, nextTasks, nodeReport, chromeReport, packageJson, buildScript, smokeTest] = await Promise.all([
load('config/freecad-sketcher-partdesign-abi-contract.json'),
load('config/compatibility-matrix.json'),
load('config/freecad-web-exact-parity-plan.json'),
load('config/freecad-private-naming-source-readiness.json'),
load('config/freecad-naming-sdk-plan.json'),
load('config/freecad-naming-next-tasks.json'),
readFile(resolve(root, 'scripts/build-freecad-naming-source-probe.sh'), 'utf8'),
load('config/freecad-naming-production-verification.json'),
load('config/chrome-freecad-naming-production-verification.json'),
load('package.json'),
readFile(resolve(root, 'native/occt-history/build.sh'), 'utf8'),
readFile(resolve(root, 'native/freecad-naming-probe/smoke-test.mjs'), 'utf8'),
])
const fail = (message) => { throw new Error(`FreeCAD private naming boundary: ${message}`) }
const callbacks = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']
const operations = ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform']
const tasks = plan.programs.flatMap((program) => program.tasks)
const task = tasks.find((entry) => entry.id === 'EX-TSN-02')
if (!task) fail('EX-TSN-02 is missing from the exact parity plan.')
if (abi.privateNamingAbi?.shippedWorkerImplementation !== 'not-linked') fail('the shipped ABI contract must remain not-linked until the production artifact exports verified callbacks.')
if (abi.claim?.exactFreeCadParity !== false) fail('the supported facade contract cannot claim exact FreeCAD parity.')
if (matrix.nativeOcctHistory?.freecadNamingBuild?.status !== 'contract-only') fail('the FreeCAD naming build must remain contract-only before a locked SDK and callback probe exist.')
if (abi.privateNamingAbi?.shippedWorkerImplementation !== 'freecad-linked') fail('the shipped ABI contract must identify the verified FreeCAD-linked Worker.')
if (abi.claim?.exactFreeCadParity !== false || matrix.systemExactEvaluation?.exact !== false) fail('production linkage cannot promote system exactness.')
if (matrix.nativeOcctHistory?.freecadNamingBuild?.status !== 'production-linked') fail('the FreeCAD naming build status must identify production linkage.')
if (matrix.nativeOcctHistory?.freecadNamingBuild?.prerequisiteStatus !== 'real-isolated-freecad-private-naming-bridge-pass') fail('the prerequisite status must identify the real isolated FreeCAD naming bridge.')
if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessStatus !== 'candidate-complete') fail('the SDK readiness status must identify the complete candidate SDK without implying production linkage.')
if (task.status !== 'in_progress') fail(`EX-TSN-02 must remain in_progress, received ${String(task.status)}.`)
if (matrix.systemExactEvaluation?.exact !== false) fail('systemExact must remain false.')
if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessStatus !== 'candidate-complete') fail('the SDK readiness status must retain the complete SDK prerequisite.')
if (task.status !== 'completed' || task.exactBlockedBy?.length !== 0) fail(`EX-TSN-02 must be completed and blocker-free, received ${String(task.status)}.`)
const blockers = matrix.systemExactEvaluation?.blockers ?? []
if (!blockers.includes('shipped Worker lacks the FreeCAD-linked private naming ABI implementation')) fail('systemExact blockers must retain the missing FreeCAD-linked Worker implementation.')
if (!task.exactBlockedBy?.includes('The shipped Worker does not link the FreeCAD private naming implementation for every builder')) fail('EX-TSN-02 must retain its production Worker linkage blocker.')
if (blockers.length !== 2 || blockers.some((blocker) => blocker.includes('lacks the FreeCAD-linked') || blocker.includes('browser builder naming evidence transport') || blocker.includes('non-unique isomorphic'))) fail('system exact blockers must remove the closed Worker linkage, browser transport and isomorphic provenance gaps.')
for (const [label, report, runtime] of [['Node', nodeReport, 'node'], ['Chrome', chromeReport, 'chrome']]) {
if (report.status !== 'pass' || report.runtime !== runtime || report.implementation !== 'freecad-linked' || report.productionWorkerLinked !== true || report.systemExact !== false) fail(`${label} production report has an invalid boundary.`)
if (JSON.stringify(report.callbacks) !== JSON.stringify(callbacks) || JSON.stringify(report.operations) !== JSON.stringify(operations) || report.cases?.length !== operations.length) fail(`${label} production report does not cover all callbacks and builders.`)
if (report.cases.some((entry, index) => entry.operation !== operations[index] || entry.status !== 'pass' || entry.provenanceResults !== entry.selectedRecords + entry.ambiguousResults || entry.provenanceResults < 1 || entry.mappedNames !== entry.selectedRecords || entry.callbackExecuted !== (entry.selectedRecords > 0) || (entry.selectedRecords > 0 ? entry.elementMaps < 1 : entry.namingStatus !== 'ambiguous'))) fail(`${label} production report contains incomplete provenance evidence.`)
if (report.isomorphicCorpus?.status !== 'pass' || report.isomorphicCorpus.uniqueResults !== 0 || report.isomorphicCorpus.ambiguousResults < 1 || report.isomorphicCorpus.arrayOrderFallbackUsed !== false) fail(`${label} production report lacks symmetric ambiguity evidence.`)
if (report.invalidHistoryRejected !== true || report.chainedStage?.stringHasherEntries < 2 || report.artifacts?.length !== 3) fail(`${label} production report lacks chained, fail-closed or artifact evidence.`)
}
if (readiness.scope !== 'standalone-wasm-source-prerequisite') fail('source readiness must remain a standalone prerequisite, not a production implementation.')
if (sdkPlan.scope !== 'pre-production-sdk-readiness' || sdkPlan.productionPublication !== false) fail('SDK readiness must remain pre-production and must not publish a Worker.')
if (sdkPlan.boundary?.freecadNamingBuildStatus !== 'contract-only' || sdkPlan.boundary?.exTsn02 !== 'in_progress' || sdkPlan.boundary?.systemExact !== false) fail('SDK readiness boundary disagrees with the authoritative status files.')
if (nextTasks.boundary?.freecadNamingBuildStatus !== 'contract-only' || nextTasks.boundary?.exTsn02 !== 'in_progress' || nextTasks.boundary?.systemExact !== false || nextTasks.boundary?.productionWorker?.availability !== 'unavailable' || nextTasks.boundary?.productionWorker?.callbacks?.length !== 0) fail('next-task plan disagrees with the production boundary.')
if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessCommand !== './npmw run check:freecad-naming-sdk-readiness') fail('compatibility matrix must expose the SDK readiness command.')
if (readiness.production?.workerLinked !== false) fail('the readiness manifest cannot claim that the production Worker is linked.')
if (readiness.production?.exportedCallbacks?.length !== 0) fail('the prerequisite probe cannot declare production callback exports.')
if (readiness.boundary?.freecadNamingBuildStatus !== 'contract-only' || readiness.boundary?.exTsn02 !== 'in_progress' || readiness.boundary?.systemExact !== false) fail('the readiness manifest boundary disagrees with the authoritative status files.')
if (readiness.candidateAbi?.scope !== 'isolated-non-production') fail('the candidate ABI must remain isolated and non-production.')
if (sdkPlan.scope !== 'pre-production-sdk-readiness' || sdkPlan.productionPublication !== false) fail('SDK readiness must remain non-publishing even after the Worker is published by its own build.')
if (sdkPlan.boundary?.freecadNamingBuildStatus !== 'production-linked' || sdkPlan.boundary?.exTsn02 !== 'completed' || sdkPlan.boundary?.systemExact !== false) fail('SDK readiness boundary disagrees with the authoritative status files.')
if (nextTasks.boundary?.freecadNamingBuildStatus !== 'production-linked' || nextTasks.boundary?.exTsn02 !== 'completed' || nextTasks.boundary?.systemExact !== false || nextTasks.boundary?.productionWorker?.availability !== 'available' || JSON.stringify(nextTasks.boundary.productionWorker.callbacks) !== JSON.stringify(callbacks)) fail('next-task plan disagrees with the production boundary.')
if (readiness.production?.workerLinked !== true || JSON.stringify(readiness.production?.exportedCallbacks) !== JSON.stringify(callbacks)) fail('the readiness manifest must record the linked production callbacks.')
if (readiness.boundary?.freecadNamingBuildStatus !== 'production-linked' || readiness.boundary?.exTsn02 !== 'completed' || readiness.boundary?.systemExact !== false) fail('the readiness manifest boundary disagrees with the authoritative status files.')
if (readiness.candidateAbi?.scope !== 'isolated-non-production' || readiness.candidateAbi?.occtBuilderContext !== false || readiness.candidateAbi?.publishToWorker !== false) fail('the candidate ABI must remain isolated and non-publishing.')
if (readiness.isolatedSourceArchive?.target !== 'wasm32-emscripten' || readiness.isolatedSourceArchive?.expectedObjectMembers !== 7 || readiness.isolatedSourceArchive?.hostAdapterBound !== true || readiness.isolatedSourceArchive?.productionEligible !== false) fail('the source archive must remain a seven-object, host-adapter-bound wasm prerequisite.')
if (readiness.candidateAbi?.strictWebValidator !== 'pass') fail('the candidate ABI must pass the strict Web validator.')
if (readiness.candidateAbi?.occtBuilderContext !== false || readiness.candidateAbi?.publishToWorker !== false) fail('the candidate ABI cannot claim OCCT builder context or Worker publication.')
for (const resource of ['MappedNameRef', 'StringHasher', 'ElementMap2']) {
if (!readiness.candidateAbi?.nativeResources?.includes(resource)) fail(`the candidate ABI must validate native ${resource} evidence.`)
}
for (const callback of readiness.candidateAbi?.exports ?? []) {
if (!callback.startsWith('freecadNamingCandidate')) fail(`candidate export ${callback} could be mistaken for a production callback.`)
}
for (const callback of readiness.production?.requiredCallbacks ?? []) {
if (!smokeTest.includes(callback)) fail(`the source smoke test must reject production callback ${callback}.`)
}
for (const source of readiness.linkedOriginalSources ?? []) {
if (!buildScript.includes(source.path.split('/').at(-1)) || !buildScript.includes(source.sha256)) fail(`the source probe build does not pin ${source.path}.`)
}
for (const expected of ['src/App/StringHasher.cpp', 'src/App/MappedElement.cpp', 'src/App/ElementNamingUtils.cpp', 'src/App/ElementMap.cpp', 'src/Base/Handle.cpp']) {
if (!readiness.linkedOriginalSources?.some((source) => source.path === expected)) fail(`the source readiness manifest must include ${expected}.`)
}
for (const expected of ['FreeCADBase static library', 'FreeCADApp static library', 'Part static library', 'Python static library', 'real Application and Document integration', 'FreeCAD private naming Worker bridge']) {
if (!readiness.notLinkedProductionComponents?.includes(expected)) fail(`the source readiness manifest must retain ${expected} as not linked.`)
}
for (const resource of ['MappedNameRef', 'StringHasher', 'ElementMap2']) if (!readiness.candidateAbi?.nativeResources?.includes(resource)) fail(`the candidate ABI must validate native ${resource} evidence.`)
for (const callback of readiness.candidateAbi?.exports ?? []) if (!callback.startsWith('freecadNamingCandidate')) fail(`candidate export ${callback} could be mistaken for a production callback.`)
for (const callback of callbacks) if (!smokeTest.includes(callback)) fail(`the isolated source smoke test must detect production callback ${callback}.`)
for (const component of ['FreeCADBase static library', 'FreeCADApp static library', 'Part static library', 'Python static library', 'FreeCAD private naming Worker bridge']) if (!readiness.productionLinkedComponents?.includes(component)) fail(`production linked components omit ${component}.`)
for (const component of ['complete Application and Document parity', 'exhaustive FCStd edit and round-trip corpus']) if (!readiness.remainingExactComponents?.includes(component)) fail(`remaining exact components omit ${component}.`)
if (readiness.remainingExactComponents?.includes('unique isomorphic topology provenance')) fail('closed isomorphic provenance remains listed as an exact component.')
if (!buildScript.includes('BITBYBIT_FREECAD_NAMING_LINKED=1') || !buildScript.includes('OCCT_HISTORY_PUBLISH')) fail('the Worker build does not enforce linked ABI compilation and explicit publication.')
if (!packageJson.scripts?.['build:occt-history']?.includes('FREECAD_WASM_NAMING_REQUIRED=1') || !packageJson.scripts['build:occt-history'].includes('OCCT_HISTORY_PUBLISH=1')) fail('the production build command does not fail closed on FreeCAD linkage and publication.')
if (!packageJson.scripts?.['build:occt-history-only-candidate']?.includes('OCCT_HISTORY_PUBLISH=0')) fail('the OCCT-only build must remain an explicitly non-publishing candidate.')
console.log(JSON.stringify({
status: 'boundary-pass',
shippedWorkerImplementation: 'not-linked',
shippedWorkerImplementation: 'freecad-linked',
task: { id: task.id, status: task.status },
systemExact: matrix.systemExactEvaluation.exact,
systemExact: false,
sdkStatus: matrix.nativeOcctHistory.freecadNamingBuild.status,
prerequisiteStatus: matrix.nativeOcctHistory.freecadNamingBuild.prerequisiteStatus,
sdkReadinessStatus: matrix.nativeOcctHistory.freecadNamingBuild.sdkReadinessStatus,
linkedOriginalSources: readiness.linkedOriginalSources.map((source) => source.path),
candidateAbi: {
scope: readiness.candidateAbi.scope,
strictWebValidator: readiness.candidateAbi.strictWebValidator,
publishToWorker: readiness.candidateAbi.publishToWorker,
},
sdkReadiness: {
scope: sdkPlan.scope,
productionPublication: sdkPlan.productionPublication,
requiredLibraries: sdkPlan.libraries.map(({ name }) => name),
},
workerLinked: readiness.production.workerLinked,
callbacks,
operations: operations.length,
productionReports: { node: nodeReport.generatedAt, chrome: chromeReport.generatedAt },
candidateAbi: { scope: readiness.candidateAbi.scope, strictWebValidator: readiness.candidateAbi.strictWebValidator, publishToWorker: readiness.candidateAbi.publishToWorker },
sdkReadiness: { scope: sdkPlan.scope, productionPublication: sdkPlan.productionPublication, requiredLibraries: sdkPlan.libraries.map(({ name }) => name) },
}, null, 2))

View File

@@ -38,7 +38,7 @@ for (const family of PARTDESIGN_PARAMETER_SPACE) {
check(FREECAD_PRIVATE_NAMING_ABI_VERSION === report.privateNamingAbi?.abiVersion, 'FreeCAD private naming ABI version report is stale')
check(createFacadeRuntimeProfile('production').naming.nativeAbi === report.privateNamingAbi?.runtimeProfile, 'production runtime profile does not advertise the optional private ABI')
check(createFacadeRuntimeProfile('mock').naming.nativeAbi === 'not-exposed', 'mock runtime must not advertise the private naming ABI')
check(report.privateNamingAbi?.shippedWorkerImplementation === 'not-linked', 'the contract must not claim a FreeCAD-linked Worker before the artifact exports verified callbacks')
check(report.privateNamingAbi?.shippedWorkerImplementation === 'freecad-linked', 'the production contract must identify the verified FreeCAD-linked Worker')
check(report.privateNamingAbi?.syntheticTokenGeneration === 'forbidden', 'synthetic FreeCAD token generation must remain forbidden')
if (failures.length) {

View File

@@ -89,5 +89,5 @@ console.log(JSON.stringify({
missingBuilderOperations: missingOperations,
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'],
blockers: ['Production private naming is linked and non-unique isomorphic provenance is fail-closed; exhaustive cross-feature mutation, FCStd round-trip closure and complete native document/property semantics remain open'],
}, null, 2))

View File

@@ -10,7 +10,7 @@ const lockedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d'
if (plan.schemaVersion !== 1 || plan.scope !== 'candidate-only-freecad-wasm-sdk-build') fail('unsupported schema or scope.')
if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== lockedCommit || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to FreeCAD 1.1.1 and Emscripten 3.1.69 wasm32.')
if (plan.configuration?.productionPublication !== false) fail('candidate SDK build cannot publish production artifacts.')
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false || plan.boundary?.workerLinked !== false || plan.boundary?.callbacks?.length !== 0) fail('production boundary must remain contract-only, unlinked, and callback-free.')
if (plan.boundary?.freecadNamingBuildStatus !== 'production-linked' || plan.boundary?.exTsn02 !== 'completed' || plan.boundary?.systemExact !== false || plan.boundary?.workerLinked !== true || plan.boundary?.callbacks?.length !== 3) fail('production boundary must identify the linked callback-complete Worker while retaining non-exact system status.')
if (JSON.stringify(plan.requiredArchives) !== JSON.stringify(['FreeCADBase', 'FreeCADApp', 'Part', 'Python'])) fail('required archive list is incomplete or reordered.')
const expectedDependencies = [
['Qt6', '6.8.2', 'npm run build:qt6-freecad-wasm'],

View File

@@ -93,7 +93,7 @@ if (new Set(plan.promotionGate.requiredZeroMetrics).size !== plan.promotionGate.
if (new Set(plan.promotionGate.requiredEvidenceClasses).size !== plan.promotionGate.requiredEvidenceClasses.length) fail('promotion evidence classes must be unique.')
const systemEvaluation = compatibility.systemExactEvaluation
if (systemEvaluation?.exact !== false || systemEvaluation?.featureExactCount !== 0 || !Array.isArray(systemEvaluation.blockers) || systemEvaluation.blockers.length !== 3) fail('compatibility matrix must retain the current non-exact evaluation and three native blockers.')
if (systemEvaluation?.exact !== false || systemEvaluation?.featureExactCount !== 0 || !Array.isArray(systemEvaluation.blockers) || systemEvaluation.blockers.length !== 2) fail('compatibility matrix must retain the current non-exact evaluation and two remaining exact blockers.')
if (platformCoverage.modules.some((module) => module.level === 'exact')) fail('platform coverage cannot contain exact modules before their exact tasks complete.')
const tasks = [...taskById.values()]

View File

@@ -0,0 +1,31 @@
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const oracle = JSON.parse(await readFile(resolve(root, 'config/freecad-xlink-relink-oracle.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD XLink/relink oracle: ${message}`) }
const same = (actual, expected) => JSON.stringify(actual) === JSON.stringify(expected)
const near = (actual, expected) => typeof actual === 'number' && Math.abs(actual - expected) <= oracle.tolerance
const expectedTypes = ['App::PropertyXLink', 'App::PropertyXLinkSub', 'App::PropertyXLinkList', 'App::PropertyXLinkSubList']
const expectedTargets = {
xLink: { document: 'XLinkSource', object: 'SourceBox' },
xLinkSub: { document: 'XLinkSource', object: 'SourceBox', subElements: ['Face1'] },
xLinkList: [{ document: 'XLinkSource', object: 'SourceBox' }, { document: 'XLinkSource', object: 'SecondBox' }],
xLinkSubList: [{ document: 'XLinkSource', object: 'SourceBox', subElements: ['Face1'] }, { document: 'XLinkSource', object: 'SecondBox', subElements: ['Face1', 'Face2'] }],
}
const linked = (holder) => Object.entries(expectedTargets).every(([name, expected]) => same(holder?.[name], expected))
const validHolder = (holder) => linked(holder) && same(holder.state, ['Up-to-date']) && holder.status === 'Valid' && Object.values(holder.propertyStatus).every((status) => same(status, ['21']))
const validBinder = (binder, area) => same(binder?.support, [{ document: 'XLinkSource', object: 'SourceBox', subElements: ['Face1'] }]) && near(binder.area, area) && binder.shapeNull === false && same(binder.state, ['Up-to-date']) && binder.status === 'Valid'
const sameDimensions = (actual, expected) => Object.entries(expected).every(([name, value]) => Array.isArray(value) ? same(actual?.[name], value) : near(actual?.[name], value))
if (oracle.schemaVersion !== 1 || oracle.baselineId !== 'freecad-1.1.1-xlink-relink-oracle' || oracle.freecadVersion !== '1.1.1' || oracle.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || oracle.status !== 'pass' || oracle.tolerance !== 1e-7 || !same(oracle.propertyTypes, expectedTypes)) fail('baseline is invalid.')
if (!validHolder(oracle.initial?.holder) || !sameDimensions(oracle.initial.dimensions, { xLinkLength: 4, xLinkSubWidth: 5, xLinkListHeights: [6, 2], xLinkSubListAreas: [30, 14, 14] }) || !validBinder(oracle.initial.binder, 30)) fail('initial four-property XLink graph is invalid.')
if (oracle.sourceClosed?.sourceOpen !== false || linked(oracle.sourceClosed.holder) || oracle.sourceClosed.binder?.area !== 30 || oracle.sourceClosed.binder.shapeNull !== false) fail('closing the source document did not expose the native unresolved/cached state.')
if (oracle.relinkAfterClose?.sourceOpen !== true || !validHolder(oracle.relinkAfterClose.holder) || !validBinder(oracle.relinkAfterClose.binder, 30)) fail('same-session source reopen did not relink all references.')
if (oracle.missingSource?.sourceOpen !== false || linked(oracle.missingSource.holder) || oracle.missingSource.binder?.area !== 30 || oracle.missingSource.binder.shapeNull !== false) fail('missing source file did not preserve the native unresolved/cached state.')
if (oracle.relinkAfterMissing?.sourceOpen !== true || !validHolder(oracle.relinkAfterMissing.holder) || !validBinder(oracle.relinkAfterMissing.binder, 30)) fail('restoring the missing source path did not relink all references.')
const editedDimensions = { xLinkLength: 8, xLinkSubWidth: 9, xLinkListHeights: [6, 4], xLinkSubListAreas: [54, 28, 28] }
if (!validHolder(oracle.edited?.holder) || !sameDimensions(oracle.edited.dimensions, editedDimensions) || !validBinder(oracle.edited.binder, oracle.edited.sourceFaceArea) || !near(oracle.edited.sourceFaceArea, 54)) fail('post-relink source edit did not propagate through all link kinds and the binder.')
if (!validHolder(oracle.finalRoundtrip?.holder) || !sameDimensions(oracle.finalRoundtrip.dimensions, editedDimensions) || !validBinder(oracle.finalRoundtrip.binder, oracle.finalRoundtrip.sourceFaceArea) || !near(oracle.finalRoundtrip.sourceFaceArea, 54)) fail('final source/consumer FCStd round-trip drifted.')
console.log(JSON.stringify({ status: 'freecad-xlink-relink-oracle-pass', baselineId: oracle.baselineId, propertyTypes: oracle.propertyTypes, sourceCloseRelink: true, missingFileRelink: true, editPropagation: true, fcstdRoundtrip: true }, null, 2))

View File

@@ -4,7 +4,7 @@ import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const root = resolve(new URL('..', import.meta.url).pathname)
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm']
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
const distRoot = resolve(root, 'native/occt-history/dist')
const publicRoot = resolve(root, 'public/native/occt-history')
const exists = async (path) => access(path).then(() => true).catch(() => false)
@@ -15,7 +15,7 @@ if (!distPresent.some(Boolean) && !publicPresent.some(Boolean)) {
console.log(JSON.stringify({ status: 'artifact-not-built', reason: 'Run ./npmw run build:occt-history when the pinned Emscripten/OCCT inputs are available.' }, null, 2))
process.exit(0)
}
if (distPresent.some((present) => !present) || publicPresent.some((present) => !present)) throw new Error('OCCT history browser artifact is incomplete; JS and WASM must be published together.')
if (distPresent.some((present) => !present) || publicPresent.some((present) => !present)) throw new Error('OCCT history browser artifact is incomplete; JS, WASM and DATA must be published together.')
for (const name of artifactNames) {
const distBytes = await readFile(resolve(distRoot, name))
const publicBytes = await readFile(resolve(publicRoot, name))
@@ -48,6 +48,18 @@ if (callbacks.length === 3) {
const objectStep = nativeModule.shapeToStep(object)
const toolStep = nativeModule.shapeToStep(tool)
const history = nativeModule.booleanHistoryFromStep(objectStep, toolStep, 'cut')
const seenResults = new Set()
const records = []
for (const record of history.records) {
if (record.relation === 'deleted') continue
for (const resultIndex of record.resultIndexes ?? [record.resultIndex]) {
const key = `${record.resultKind ?? record.kind}:${resultIndex}`
if (!Number.isSafeInteger(resultIndex) || resultIndex < 0 || seenResults.has(key)) continue
seenResults.add(key)
records.push({ ...record, resultIndex, resultIndexes: undefined })
}
}
if (records.length === 0) throw new Error('Artifact Cut probe returned no unique native result relations.')
const request = {
schemaVersion: 1,
requestId: 'artifact-private-naming-probe',
@@ -65,7 +77,7 @@ if (callbacks.length === 3) {
stages: [{ stageId: 'artifact-probe:stage:0', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
resultStep: history.resultStep,
resultBrep: history.resultBrep,
history,
history: { ...history, records },
}
evidence = JSON.parse(nativeModule.freecadNamingEvidenceJson(JSON.stringify(request)))
} finally {
@@ -80,4 +92,4 @@ const expectedImplementation = abiContract.privateNamingAbi?.shippedWorkerImplem
if (expectedImplementation === 'not-linked' && namingAbi.availability !== 'unavailable') throw new Error('OCCT history artifact exports FreeCAD naming callbacks but the shipped-worker contract still says not-linked.')
if (expectedImplementation === 'freecad-linked' && namingAbi.availability !== 'available') throw new Error('Shipped-worker contract claims FreeCAD linkage but the artifact ABI probe is unavailable.')
if (!['not-linked', 'freecad-linked'].includes(expectedImplementation)) throw new Error('Shipped-worker FreeCAD naming implementation status is invalid.')
console.log(JSON.stringify({ status: 'artifact-pass', files, namingAbi, exactBoundary: namingAbi.availability === 'available' ? 'probe-only; FreeCAD evidence still requires locked descriptor and response validation' : 'occt-only; EX-TSN-02 remains in_progress and systemExact=false' }, null, 2))
console.log(JSON.stringify({ status: 'artifact-pass', files, namingAbi, exactBoundary: namingAbi.availability === 'available' ? 'production-linked; EX-TSN-02 completed; broader exact plan remains open' : 'invalid production boundary' }, null, 2))

View File

@@ -34,9 +34,11 @@ assertEqual(sha256(canonicalSurface), baseline.bitbybit.surfaceSha256, 'Bitbybit
assertEqual(matrix.nativeOcctHistory.occtVersion, baseline.nativeOcct.version, 'Native OCCT version')
assertEqual(matrix.nativeOcctHistory.sourceCommit, baseline.nativeOcct.sourceCommit, 'Native OCCT source commit')
assertEqual(matrix.nativeOcctHistory.freecadNamingBuild.status, baseline.nativeOcct.buildStatus, 'Native OCCT naming build status')
for (const [kind, relativePath, expectedHash] of [
['JavaScript', matrix.nativeOcctHistory.artifact.javascript, baseline.nativeOcct.javascriptSha256],
['WASM', matrix.nativeOcctHistory.artifact.wasm, baseline.nativeOcct.wasmSha256]
['WASM', matrix.nativeOcctHistory.artifact.wasm, baseline.nativeOcct.wasmSha256],
['DATA', matrix.nativeOcctHistory.artifact.data, baseline.nativeOcct.dataSha256],
]) {
assertEqual(sha256(await readBytes(relativePath)), expectedHash, `Native OCCT ${kind} artifact`)
}
@@ -54,6 +56,6 @@ for (const entry of threeWay.reports) {
console.log(JSON.stringify({
status: 'occt-upstream-drift-pass',
bitbybit: { version: packageJson.version, sourceCommit: baseline.bitbybit.sourceCommit, surfaceSha256: baseline.bitbybit.surfaceSha256 },
nativeOcct: { version: baseline.nativeOcct.version, sourceCommit: baseline.nativeOcct.sourceCommit },
nativeOcct: { version: baseline.nativeOcct.version, sourceCommit: baseline.nativeOcct.sourceCommit, buildStatus: baseline.nativeOcct.buildStatus },
golden: { manifestScenarios: goldenManifest.scenarios.length, threeWayScenarios: threeWay.scenarioCount, operationCounts }
}, null, 2))

View File

@@ -4,14 +4,16 @@ import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const load = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8'))
const lifecycle = await load('config/chrome-partdesign-lifecycle-verification.json')
const referenceLifecycle = await load('config/chrome-partdesign-reference-lifecycle-verification.json')
const loft = await load('config/chrome-partdesign-loft-verification.json')
const transform = await load('config/chrome-partdesign-transform-verification.json')
const base = await load('config/chrome-geometry-features-verification.json')
if ([lifecycle, loft, transform, base].some((report) => report.status !== 'pass')) throw new Error('PartDesign closure requires passing lifecycle, base, loft and transform browser reports.')
if ([lifecycle, referenceLifecycle, loft, transform, base].some((report) => report.status !== 'pass')) throw new Error('PartDesign closure requires passing lifecycle, external reference, base, loft and transform browser reports.')
const lifecycleStages = [lifecycle.initial, lifecycle.edited, lifecycle.undone, lifecycle.reopened]
if (new Set(lifecycleStages.map((stage) => stage?.shapeId)).size !== lifecycleStages.length || lifecycleStages.some((stage) => !stage?.shapeId || !(stage.volume > 0) || stage.solids !== 1 || stage.tip !== 'pad' || stage.support !== 'XY_Plane') || lifecycle.edited.length !== 31 || lifecycle.undone.length !== 42 || lifecycle.reopened.length !== 42 || Math.abs(lifecycle.initial.volume - 504) > 1e-7 || Math.abs(lifecycle.edited.volume - 372) > 1e-7 || Math.abs(lifecycle.undone.volume - 504) > 1e-7 || Math.abs(lifecycle.reopened.volume - 504) > 1e-7) throw new Error('PartDesign edit/Undo/load lifecycle contains stale Shape, Tip or Support evidence.')
if (lifecycle.structural?.reorderRejected !== true || lifecycle.structural?.dependencyRemovalRejected !== true || lifecycle.structural?.deleted?.tip !== 'pocket' || lifecycle.structural?.deletionUndone?.tip !== 'fillet' || lifecycle.structural?.deletionRedone?.tip !== 'pocket' || lifecycle.structural?.restoredTip !== 'fillet') throw new Error('PartDesign structural Undo/Redo and Tip evidence is invalid.')
if (referenceLifecycle.initial?.properties?.XLinkList?.length !== 2 || referenceLifecycle.initial?.properties?.XLinkSubList?.length !== 2 || referenceLifecycle.closed?.binder?.cacheState !== 'cached' || referenceLifecycle.missing?.source?.status !== 'missing' || referenceLifecycle.relinkedAfterMissing?.binder?.cacheState !== 'linked' || referenceLifecycle.edited?.binder?.area !== 54 || referenceLifecycle.undone?.binder?.area !== 30 || referenceLifecycle.redone?.binder?.area !== 54 || referenceLifecycle.reopenedUndo?.binder?.area !== 30 || referenceLifecycle.reopenedRedo?.binder?.area !== 54) throw new Error('PartDesign external reference close, relink, edit, Undo/Redo or OPFS evidence is invalid.')
const expectedLoft = ['additive-loft', 'additive-pipe', 'subtractive-loft', 'subtractive-pipe']
if (JSON.stringify(loft.operations?.map((entry) => entry.command)) !== JSON.stringify(expectedLoft) || loft.operations.some((entry) => entry.tip !== entry.objectId || !entry.base || !entry.shapeId || !(entry.volume > 0)) || new Set(loft.operations.map((entry) => entry.shapeId)).size !== loft.operations.length) throw new Error('PartDesign Loft/Pipe create and Tip/Base evidence is invalid.')
@@ -27,7 +29,7 @@ if (ambiguity?.status !== 'failed' || ambiguity.retainedShapeId !== ambiguity.pr
const baseModes = ['pad-tapered', 'pocket-tapered', 'pocket-through-all', 'pocket-up-to-face', 'pad-midplane']
if (baseModes.some((name) => !base.operations?.some((entry) => entry.name === name && entry.shapeId && entry.volume > 0))) throw new Error('PartDesign base mode creation evidence is incomplete.')
if (loft.persistence?.mode !== 'sqlite-opfs' || transform.persistence?.mode !== 'sqlite-opfs' || lifecycle.reopened?.persistenceMode !== 'sqlite-opfs' || loft.persistence.reopenedTip !== 'subtractive-pipe' || transform.persistence.reopenedTip !== transform.operations.at(-1).objectId || lifecycle.reopened.tip !== 'pad') throw new Error('PartDesign save/load Tip evidence is invalid.')
if (loft.afterRelease?.shapeCount !== 0 || loft.afterRelease?.kernelReferenceCount !== 0 || transform.afterRelease?.shapeCount !== 0 || transform.afterRelease?.kernelReferenceCount !== 0 || lifecycle.released?.shapeCount !== 0 || lifecycle.released?.kernelReferenceCount !== 0 || base.afterRelease?.shapeCount !== 0 || base.afterRelease?.kernelReferenceCount !== 0) throw new Error('PartDesign closure reports leaked Shape ownership.')
if (loft.afterRelease?.shapeCount !== 0 || loft.afterRelease?.kernelReferenceCount !== 0 || transform.afterRelease?.shapeCount !== 0 || transform.afterRelease?.kernelReferenceCount !== 0 || lifecycle.released?.shapeCount !== 0 || lifecycle.released?.kernelReferenceCount !== 0 || referenceLifecycle.released?.shapeCount !== 0 || referenceLifecycle.released?.kernelReferenceCount !== 0 || base.afterRelease?.shapeCount !== 0 || base.afterRelease?.kernelReferenceCount !== 0) throw new Error('PartDesign closure reports leaked Shape ownership.')
const historyPaths = new Map([
['pad', 'config/chrome-native-pad-history-verification.json'],
@@ -55,4 +57,4 @@ for (const [operation, path] of historyPaths) {
nativeHistory[operation] = count
}
console.log(JSON.stringify({ status: 'partdesign-closure-pass', lifecycle: lifecycleStages.map((stage) => ({ length: stage.length, shapeId: stage.shapeId, volume: stage.volume, tip: stage.tip, support: stage.support })), createdFeatures: { loft: expectedLoft, transform: expectedTransform, baseModes }, failureRecovery: { loft: loft.failureRecovery, transform: transform.ambiguityRecovery }, persistence: { lifecycle: lifecycle.reopened, loft: loft.persistence, transform: transform.persistence }, nativeHistory }, null, 2))
console.log(JSON.stringify({ status: 'partdesign-closure-pass', lifecycle: lifecycleStages.map((stage) => ({ length: stage.length, shapeId: stage.shapeId, volume: stage.volume, tip: stage.tip, support: stage.support })), externalReferences: { propertyKinds: Object.keys(referenceLifecycle.initial.properties), sourceCloseRelink: true, missingSourceRelink: true, opfsUndoRedo: true }, createdFeatures: { loft: expectedLoft, transform: expectedTransform, baseModes }, failureRecovery: { loft: loft.failureRecovery, transform: transform.ambiguityRecovery }, persistence: { lifecycle: lifecycle.reopened, references: referenceLifecycle.reopened, loft: loft.persistence, transform: transform.persistence }, nativeHistory }, null, 2))

View File

@@ -5,8 +5,8 @@ const root = resolve(new URL('..', import.meta.url).pathname)
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const [
fcstdFuzz, geometryFuzz, sketchFuzz, successFixtures, failureFixtures, supplementalSuccessFixtures, supplementalFailureFixtures,
sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignStructure, attachmentModes, partdesignFailures, partdesignRevolutionGroove, partBuilders,
fcstdRoundTrip, app, browserMatrix, performance, fault, opfsMigration, security, qa08, addon, script,
sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignStructure, attachmentModes, attachmentCombinations, xlinkRelink, partdesignFailures, partdesignRevolutionGroove, partBuilders,
fcstdRoundTrip, app, partdesignReferenceLifecycle, browserMatrix, performance, fault, opfsMigration, security, qa08, addon, script,
] = await Promise.all([
load('config/qa04-fcstd-fuzz-verification.json'),
load('config/qa04-geometry-fuzz-verification.json'),
@@ -22,11 +22,14 @@ const [
load('config/freecad-partdesign-transform-oracle.json'),
load('config/freecad-partdesign-structure-oracle.json'),
load('config/freecad-attachment-mode-oracle.json'),
load('config/freecad-attachment-combination-oracle.json'),
load('config/freecad-xlink-relink-oracle.json'),
load('config/freecad-partdesign-failure-oracle.json'),
load('config/freecad-partdesign-revolution-groove-oracle.json'),
load('config/freecad-part-builders-oracle.json'),
load('config/freecad-fcstd-roundtrip-verification.json'),
load('config/chrome-app-e2e-verification.json'),
load('config/chrome-partdesign-reference-lifecycle-verification.json'),
load('config/browser-matrix-verification.json'),
load('config/chrome-performance-verification.json'),
load('config/chrome-fault-injection-verification.json'),
@@ -48,6 +51,8 @@ const partdesignCases = [partdesignBase, partdesignLoft, partdesignDressup, part
if (partdesignCases !== 23 || [partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform].some((report) => report.status !== 'pass' || report.freecadVersion !== '1.1.1')) throw new Error('QA-02 PartDesign oracle evidence is incomplete.')
if (partdesignStructure.status !== 'pass' || partdesignStructure.freecadVersion !== '1.1.1' || Object.keys(partdesignStructure.initial?.bodyGroups ?? {}).length !== 4 || partdesignStructure.crossDocument?.initial?.shapeBinderExternalLink?.accepted !== false || partdesignStructure.crossDocument?.initial?.subShapeBinderSupportType !== 'App::PropertyXLinkSubList' || partdesignStructure.crossDocument?.edited?.subShapeBinderArea !== 48 || partdesignStructure.crossDocument?.deleted?.subShapeBinderCachedArea !== 48) throw new Error('QA-02 PartDesign structure and cross-document evidence is incomplete.')
if (attachmentModes.status !== 'pass' || attachmentModes.freecadVersion !== '1.1.1' || attachmentModes.registry?.modeCount !== 55 || attachmentModes.registry?.implementedUnionCount !== 50 || attachmentModes.engines?.plane?.implementedModes?.length !== 23 || attachmentModes.engines?.line?.implementedModes?.length !== 18 || attachmentModes.engines?.point?.implementedModes?.length !== 9 || attachmentModes.success?.roundtrip?.point?.placement?.position?.[2] !== 9 || Object.keys(attachmentModes.failures?.roundtrip ?? {}).length !== 3) throw new Error('QA-02 Attachment mode registry, execution or round-trip evidence is incomplete.')
if (attachmentCombinations.status !== 'pass' || attachmentCombinations.freecadVersion !== '1.1.1' || attachmentCombinations.caseCount !== 236 || attachmentCombinations.cases?.length !== 236 || attachmentCombinations.cases.some((entry) => entry.initial?.status !== 'Valid' || entry.mutated?.status !== 'Valid' || entry.roundtrip?.status !== 'Valid')) throw new Error('QA-02 exhaustive Attachment mode/reference combination evidence is incomplete.')
if (xlinkRelink.status !== 'pass' || xlinkRelink.freecadVersion !== '1.1.1' || xlinkRelink.propertyTypes?.length !== 4 || xlinkRelink.relinkAfterMissing?.holder?.xLinkList?.length !== 2 || xlinkRelink.edited?.binder?.area !== 54 || xlinkRelink.finalRoundtrip?.binder?.area !== 54) throw new Error('QA-02 native XLink close, missing-file relink, edit or FCStd evidence is incomplete.')
if (partdesignFailures.status !== 'pass' || partdesignFailures.freecadVersion !== '1.1.1' || partdesignFailures.summary?.cases !== 17 || partdesignFailures.summary.passed !== 17 || partdesignFailures.summary.rejected !== 13 || partdesignFailures.summary.acceptedEmpty !== 4 || partdesignFailures.summary.accepted !== 0) throw new Error('QA-02 PartDesign failure oracle evidence is incomplete.')
if (partdesignRevolutionGroove.status !== 'pass' || partdesignRevolutionGroove.freecadVersion !== '1.1.1' || partdesignRevolutionGroove.summary?.cases !== 2 || partdesignRevolutionGroove.summary.passed !== 2 || partdesignRevolutionGroove.cases?.some((fixture) => fixture.shapeNull !== false || fixture.shapeValid !== true || fixture.solids !== 1)) throw new Error('QA-02 PartDesign Revolution/Groove oracle evidence is incomplete.')
if (partBuilders.status !== 'pass' || partBuilders.freecadVersion !== '1.1.1' || partBuilders.summary?.successCases !== 6 || partBuilders.summary.successPassed !== 6 || partBuilders.summary.failureCases !== 6 || partBuilders.summary.failurePassed !== 6 || partBuilders.summary.rejected !== 6 || partBuilders.summary.acceptedEmpty !== 0) throw new Error('QA-02 Part builders oracle evidence is incomplete.')
@@ -55,6 +60,7 @@ if (fcstdRoundTrip.status !== 'verified' || !Number.isSafeInteger(fcstdRoundTrip
const requiredAppWorkflows = ['start-to-workspace', 'freecad-menus', 'command-search-dialog', 'preferences-about-dialogs', 'model-tree-context-menu', 'workbench-change', 'create-sketch-command', 'loft-section-task', 'drawer-collapse', 'cam-workbench-job-flow', 'opfs-save-reopen', 'import-format-selection', 'export-format-selection']
if (app.status !== 'pass' || requiredAppWorkflows.some((workflow) => !app.workflows?.includes(workflow)) || app.pageErrors?.length !== 0 || app.desktop?.kernelPreviewSource !== 'bitbybit-occt' || app.desktop.canvas?.uniqueColors < 4 || app.desktop?.commandGroups < 4 || app.desktop?.selectionView !== true || app.desktop?.statusBar !== true || app.uiParity?.commandDialog?.modal !== true || app.uiParity?.preferencesDialog?.modal !== true || app.uiParity?.aboutDialog?.modal !== true || app.uiParity?.contextMenu?.items?.length !== 4 || app.uiParity?.camMenu?.items !== 60 || app.cam?.ui?.generated !== true || app.cam.ui.toolbarCommands !== 60 || app.cam.ui.toolpathPoints < 5 || app.mobile?.viewport?.width !== 390 || Object.keys(app.screenshots ?? {}).length !== 3) throw new Error('QA-03 production Chrome E2E evidence is incomplete.')
if (partdesignReferenceLifecycle.status !== 'pass' || partdesignReferenceLifecycle.crossOriginIsolated !== true || partdesignReferenceLifecycle.initial?.properties?.XLinkSubList?.length !== 2 || partdesignReferenceLifecycle.missing?.binder?.cacheState !== 'cached' || partdesignReferenceLifecycle.relinkedAfterMissing?.binder?.cacheState !== 'linked' || partdesignReferenceLifecycle.reopenedUndo?.binder?.area !== 30 || partdesignReferenceLifecycle.reopenedRedo?.binder?.area !== 54 || partdesignReferenceLifecycle.released?.shapeCount !== 0 || partdesignReferenceLifecycle.released?.kernelReferenceCount !== 0) throw new Error('QA-03 Chrome PartDesign external reference lifecycle evidence is incomplete.')
if (browserMatrix.status !== 'pass' || browserMatrix.browsers?.length !== 2 || !['firefox', 'webkit'].every((engine) => browserMatrix.browsers.some((entry) => entry.engine === engine && entry.status === 'pass' && entry.pageErrors?.length === 0 && entry.canvas?.geometrySource === 'bitbybit-occt'))) throw new Error('QA-03 non-Chrome browser matrix evidence is incomplete.')
if (fcstdFuzz.status !== 'fcstd-parser-fuzz-pass' || fcstdFuzz.cases !== 1000 || fcstdFuzz.counts?.accepted + fcstdFuzz.counts?.rejected !== 1000 || Object.keys(fcstdFuzz.categories ?? {}).length !== 13 || fcstdFuzz.timing?.p95Ms >= 50) throw new Error('QA-04 FCStd fuzz evidence is incomplete.')
@@ -72,8 +78,8 @@ console.log(JSON.stringify({
tasks: 8,
unitSuites: testFiles.length,
unitTests: testCases,
freecad: { successFixtures: 105, failureFixtures: 56, primarySuccessFixtures: 100, primaryFailureFixtures: 51, supplementalSuccessFixtures: 5, supplementalFailureFixtures: 5, sketchConstraints: 19, partdesignCases, partdesignStructureBodies: Object.keys(partdesignStructure.initial.bodyGroups).length, attachmentModes: attachmentModes.registry.modeCount, attachmentImplementedUnion: attachmentModes.registry.implementedUnionCount, partdesignFailureCases: partdesignFailures.summary.cases, partdesignRejectedFailures: partdesignFailures.summary.rejected, partdesignAcceptedEmpty: partdesignFailures.summary.acceptedEmpty, partdesignRevolutionGrooveCases: partdesignRevolutionGroove.summary.cases, partBuilderSuccessCases: partBuilders.summary.successCases, partBuilderFailureCases: partBuilders.summary.failureCases, fcstdRoundTrips: fcstdRoundTrip.scenarioCount },
freecad: { successFixtures: 105, failureFixtures: 56, primarySuccessFixtures: 100, primaryFailureFixtures: 51, supplementalSuccessFixtures: 5, supplementalFailureFixtures: 5, sketchConstraints: 19, partdesignCases, partdesignStructureBodies: Object.keys(partdesignStructure.initial.bodyGroups).length, attachmentModes: attachmentModes.registry.modeCount, attachmentImplementedUnion: attachmentModes.registry.implementedUnionCount, attachmentCombinationCases: attachmentCombinations.caseCount, xlinkPropertyKinds: xlinkRelink.propertyTypes.length, partdesignFailureCases: partdesignFailures.summary.cases, partdesignRejectedFailures: partdesignFailures.summary.rejected, partdesignAcceptedEmpty: partdesignFailures.summary.acceptedEmpty, partdesignRevolutionGrooveCases: partdesignRevolutionGroove.summary.cases, partBuilderSuccessCases: partBuilders.summary.successCases, partBuilderFailureCases: partBuilders.summary.failureCases, fcstdRoundTrips: fcstdRoundTrip.scenarioCount },
fuzz: { fcstd: fcstdFuzz.cases, geometry: geometryFuzz.cases, solverModels: sketchFuzz.models },
chrome: { workflows: app.workflows.length, screenshots: Object.keys(app.screenshots).length, accessibilityNodes: app.screenReader.nodes },
chrome: { workflows: app.workflows.length, screenshots: Object.keys(app.screenshots).length, accessibilityNodes: app.screenReader.nodes, partdesignExternalReferenceLifecycle: true },
browsers: Object.fromEntries(browserMatrix.browsers.map((entry) => [entry.engine, { status: entry.status, persistence: entry.persistence, geometrySource: entry.canvas.geometrySource }])),
}, null, 2))

View File

@@ -100,6 +100,6 @@ if (status === 'blocked') {
if (missing.length === 0) missing.push('unclassified CMake configure failure')
}
await mkdir(resolve(root, '.cache/toolchains/freecad-naming-sdk'), { recursive: true })
await writeFile(reportPath, `${JSON.stringify({ schemaVersion: 1, status, exitCode, startedAt, command, missing, buildDir, outputTail: output.slice(-12000), productionPublication: false, boundary: { freecadNamingBuildStatus: 'contract-only', exTsn02: 'in_progress', systemExact: false, workerLinked: false, callbacks: [] } }, null, 2)}\n`)
await writeFile(reportPath, `${JSON.stringify({ schemaVersion: 1, status, exitCode, startedAt, command, missing, buildDir, outputTail: output.slice(-12000), productionPublication: false, boundary: { freecadNamingBuildStatus: 'production-linked', exTsn02: 'completed', systemExact: false, workerLinked: true, callbacks: ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'] } }, null, 2)}\n`)
console.log(JSON.stringify({ status, exitCode, report: reportPath, missing, productionPublication: false }, null, 2))
if (status !== 'configured') process.exit(exitCode)

View File

@@ -0,0 +1,214 @@
import json
import math
import os
import tempfile
import FreeCAD as App
import Part
import Sketcher # noqa: F401 - registers Sketcher::SketchObject
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
TRANSLATION = App.Vector(1.25, -0.75, 2.0)
def version_text():
return ".".join(str(value) for value in App.Version()[:3])
def placement_json(placement):
axis = placement.Rotation.Axis
base = placement.Base
return {
"position": [float(base.x), float(base.y), float(base.z)],
"axis": [float(axis.x), float(axis.y), float(axis.z)],
"angleDegrees": float(placement.Rotation.Angle * 180.0 / math.pi),
}
def support_json(value):
result = []
for obj, sub_elements in value:
subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
result.append({
"object": obj.Name,
"subElements": [str(sub) for sub in subs if str(sub)],
})
return result
def finite_placement(value):
return all(math.isfinite(number) for number in value["position"] + value["axis"] + [value["angleDegrees"]])
def add_shape(document, name, shape):
obj = document.addObject("PartDesign::Feature", name)
obj.Shape = shape
return obj
def make_sources(document):
vector = App.Vector
sources = {
"BoxA": add_shape(document, "BoxA", Part.makeBox(4, 5, 6, vector(0, 0, 0))),
"BoxB": add_shape(document, "BoxB", Part.makeBox(3, 4, 7, vector(12, 3, 2))),
"BoxC": add_shape(document, "BoxC", Part.makeBox(2, 6, 4, vector(-8, 4, 1))),
"BoxD": add_shape(document, "BoxD", Part.makeBox(5, 3, 2, vector(3, -9, 5))),
"PlaneXY": add_shape(document, "PlaneXY", Part.makePlane(20, 20, vector(-10, -10, 0), vector(0, 0, 1))),
"PlaneYZ": add_shape(document, "PlaneYZ", Part.makePlane(20, 20, vector(0, -10, -10), vector(1, 0, 0))),
"Circle": add_shape(document, "Circle", Part.Circle(vector(0, 0, 0), vector(0, 0, 1), 5).toShape()),
"Ellipse": add_shape(document, "Ellipse", Part.Ellipse(vector(0, 0, 0), 6, 3).toShape()),
"Hyperbola": add_shape(document, "Hyperbola", Part.Hyperbola(vector(0, 0, 0), 5, 3).toShape(-1.0, 1.0)),
"Parabola": add_shape(document, "Parabola", Part.Parabola().toShape(-2.0, 2.0)),
"Curve": add_shape(document, "Curve", Part.Arc(vector(-4, 0, 0), vector(0, 4, 2), vector(4, 0, 0)).toShape()),
"V0": add_shape(document, "V0", Part.Vertex(vector(0, 0, 0))),
"V1": add_shape(document, "V1", Part.Vertex(vector(5, 0, 0))),
"V2": add_shape(document, "V2", Part.Vertex(vector(0, 4, 2))),
"V3": add_shape(document, "V3", Part.Vertex(vector(1, 2, 6))),
"LX": add_shape(document, "LX", Part.makeLine(vector(0, 0, 0), vector(6, 0, 0))),
"LY": add_shape(document, "LY", Part.makeLine(vector(0, 0, 0), vector(0, 5, 0))),
"LZ": add_shape(document, "LZ", Part.makeLine(vector(0, 0, 0), vector(0, 0, 4))),
"LD": add_shape(document, "LD", Part.makeLine(vector(0, 0, 0), vector(3, 4, 5))),
"LOff": add_shape(document, "LOff", Part.makeLine(vector(0, 4, 2), vector(3, 5, 6))),
}
document.recompute()
return sources
def source_ref(sources, reference_type, index, combination):
if reference_type == "Vertex":
if combination == ["Face", "Vertex"] or combination == ["Vertex", "Face"]:
name = "V1"
elif any(value in ("Curve", "Circle", "Edge") for value in combination):
name = "V1"
elif combination in (["Line", "Vertex"], ["Vertex", "Line"]):
name = "V2"
else:
name = ["V0", "V1", "V2", "V3"][index % 4]
return sources[name], "Vertex1"
if reference_type == "Line":
if combination == ["Line", "Line"] and index == 1:
return sources["LOff"], "Edge1"
return sources[["LX", "LY", "LZ", "LD"][index % 4]], "Edge1"
if reference_type == "Plane|Placement":
return sources["PlaneXY" if index == 0 else "PlaneYZ"], ""
if reference_type in ("Plane", "Face"):
return sources["PlaneXY" if index == 0 else "PlaneYZ"], "Face1"
if reference_type == "Edge":
return sources["Circle"], "Edge1"
if reference_type == "Curve":
return sources["Curve"], "Edge1"
if reference_type == "Circle":
return sources["Circle"], "Edge1"
if reference_type == "Conic":
return sources["Ellipse"], "Edge1"
if reference_type == "Ellipse":
return sources["Ellipse"], "Edge1"
if reference_type == "Hyperbola":
return sources["Hyperbola"], "Edge1"
if reference_type in ("Any", "Any|Placement"):
return sources[["BoxA", "BoxB", "BoxC", "BoxD"][index % 4]], ""
raise ValueError("Unsupported attachment reference category: " + reference_type)
def supports_for(sources, combination):
return [source_ref(sources, reference_type, index, combination) for index, reference_type in enumerate(combination)]
def case_state(obj):
suggestion = obj.Attacher.suggestModes()
placement = placement_json(obj.Placement)
return {
"mapMode": str(obj.MapMode),
"support": support_json(obj.AttachmentSupport),
"placement": placement,
"finitePlacement": finite_placement(placement),
"state": [str(value) for value in obj.State],
"status": str(obj.getStatusString()),
"positionBySupport": bool(obj.positionBySupport()),
"suggestedModes": list(suggestion["allApplicableModes"]),
"suggestionMessage": suggestion["message"],
}
document = App.newDocument("AttachmentCombinationOracle")
sources = make_sources(document)
engine_specs = {
"plane": ("PartDesign::Plane", document.addObject("PartDesign::Body", "PlaneCases")),
"line": ("PartDesign::Line", document.addObject("PartDesign::Body", "LineCases")),
"point": ("PartDesign::Point", document.addObject("PartDesign::Body", "PointCases")),
"sketch": ("Sketcher::SketchObject", document.addObject("PartDesign::Body", "SketchCases")),
}
seed_objects = {
name: body.newObject(type_id, "Seed" + name.title())
for name, (type_id, body) in engine_specs.items()
}
document.recompute()
cases = []
case_objects = {}
for engine_name, seed in seed_objects.items():
type_id, body = engine_specs[engine_name]
for mode in list(seed.Attacher.ImplementedModes):
combinations = seed.Attacher.getModeInfo(mode)["ReferenceCombinations"]
for combination_index, combination_value in enumerate(combinations):
combination = [str(value) for value in combination_value]
case_id = "%s-%s-%02d" % (engine_name, mode, combination_index)
object_name = "Case%04d" % (len(cases) + 1)
obj = body.newObject(type_id, object_name)
obj.Label = case_id
obj.AttachmentSupport = supports_for(sources, combination)
obj.MapMode = mode
cases.append({
"id": case_id,
"objectName": object_name,
"engine": engine_name,
"typeId": type_id,
"mode": str(mode),
"modeIndex": int(seed.Attacher.getModeInfo(mode)["ModeIndex"]),
"combinationIndex": combination_index,
"referenceCombination": combination,
})
case_objects[object_name] = obj
for seed in seed_objects.values():
seed.Document.removeObject(seed.Name)
document.recompute()
initial = {name: case_state(obj) for name, obj in case_objects.items()}
for source in sources.values():
source.Placement.Base = source.Placement.Base + TRANSLATION
document.recompute()
mutated = {name: case_state(obj) for name, obj in case_objects.items()}
with tempfile.TemporaryDirectory(prefix="freecad-attachment-combinations-") as temp_dir:
path = os.path.join(temp_dir, "AttachmentCombinationOracle.FCStd")
document.saveAs(path)
App.closeDocument(document.Name)
reopened = App.openDocument(path)
reopened.recompute()
roundtrip = {case["objectName"]: case_state(reopened.getObject(case["objectName"])) for case in cases}
App.closeDocument(reopened.Name)
for case in cases:
name = case["objectName"]
case["initial"] = initial[name]
case["mutated"] = mutated[name]
case["roundtrip"] = roundtrip[name]
report = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-attachment-combination-oracle",
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"status": "pass",
"tolerance": 1e-7,
"translation": [float(TRANSLATION.x), float(TRANSLATION.y), float(TRANSLATION.z)],
"caseCount": len(cases),
"engineCaseCounts": {
engine: len([case for case in cases if case["engine"] == engine])
for engine in engine_specs
},
"cases": cases,
}
print("FREECAD_ATTACHMENT_COMBINATION_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))

View File

@@ -277,8 +277,16 @@ def collect_case(case_id, factory, index, directory):
after_stages = [entry for obj in reopened.Objects if (entry := stage_report(obj)) is not None]
after_names = {(stage["name"], entry["name"]): entry.get("mappedName") for stage in after_stages for entry in stage["names"]}
roundtrip_drift = sum(1 for key, value in before_names.items() if after_names.get(key) != value)
resaved_path = os.path.join(directory, "%s-resaved.FCStd" % case_id)
reopened.saveAs(resaved_path)
App.closeDocument(reopened.Name)
return {"id": case_id, "finalObject": final_name, "stages": stages, "elementMapResources": resources, "stringHasherResource": string_hasher_resource, "roundtripNameDrift": roundtrip_drift, "status": "pass"}
resaved = App.openDocument(resaved_path)
resaved.recompute()
resaved_stages = [entry for obj in resaved.Objects if (entry := stage_report(obj)) is not None]
resaved_names = {(stage["name"], entry["name"]): entry.get("mappedName") for stage in resaved_stages for entry in stage["names"]}
resave_name_drift = sum(1 for key, value in before_names.items() if resaved_names.get(key) != value)
App.closeDocument(resaved.Name)
return {"id": case_id, "finalObject": final_name, "stages": stages, "elementMapResources": resources, "stringHasherResource": string_hasher_resource, "roundtripNameDrift": roundtrip_drift, "resaveNameDrift": resave_name_drift, "nativeDesktopResaveCovered": True, "status": "pass"}
except Exception as error:
return {"id": case_id, "finalObject": "", "stages": [], "elementMapResources": {}, "status": "failed", "error": str(error)}
finally:
@@ -309,6 +317,6 @@ report = {
"gitCommit": FREECAD_COMMIT,
"status": "pass" if len(cases) == 30 and all(case["status"] == "pass" for case in cases) else "failed",
"cases": cases,
"summary": {"cases": len(cases), "passed": sum(1 for case in cases if case["status"] == "pass"), "failed": sum(1 for case in cases if case["status"] != "pass")},
"summary": {"cases": len(cases), "passed": sum(1 for case in cases if case["status"] == "pass"), "failed": sum(1 for case in cases if case["status"] != "pass"), "roundtripNameDrift": sum(case.get("roundtripNameDrift", 0) for case in cases), "resaveNameDrift": sum(case.get("resaveNameDrift", 0) for case in cases), "nativeDesktopResaveCases": sum(1 for case in cases if case.get("nativeDesktopResaveCovered") is True)},
}
print("FREECAD_COMPOSITE_HISTORY_ELEMENTMAP_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))

View File

@@ -0,0 +1,347 @@
export const FREECAD_NAMING_CALLBACKS = [
'freecadNamingAbiVersion',
'freecadNamingCapabilitiesJson',
'freecadNamingEvidenceJson',
]
export const FREECAD_NAMING_OPERATIONS = [
'fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe',
'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness',
'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform',
]
const shapeStep = (module, method, ...args) => {
const shape = module[method](...args)
try {
return module.shapeToStep(shape)
} finally {
shape.delete?.()
}
}
const box = (module, ...args) => shapeStep(module, 'makeBox', ...args)
const placedBox = (module, ...args) => shapeStep(module, 'makeBoxPlaced', ...args)
const rectangle = (module, ...args) => shapeStep(module, 'makeRectangleFace', ...args)
const placedRectangle = (module, ...args) => shapeStep(module, 'makeRectangleFacePlaced', ...args)
const line = (module, ...args) => shapeStep(module, 'makeLineWire', ...args)
const input = (inputId, role, objectTag, step) => ({
inputId,
objectId: `production-${inputId}`,
role,
objectTag,
step,
})
const fixtures = {
fuse(module) {
const objectStep = box(module, 10, 10, 10)
const toolStep = placedBox(module, 5, 5, 5, 8, 0, 0)
return { response: module.booleanHistoryFromStep(objectStep, toolStep, 'fuse'), inputs: [input('object', 'object', 101, objectStep), input('tool', 'tool', 102, toolStep)] }
},
cut(module) {
const objectStep = box(module, 10, 10, 10)
const toolStep = box(module, 5, 5, 5)
return { response: module.booleanHistoryFromStep(objectStep, toolStep, 'cut'), inputs: [input('object', 'object', 111, objectStep), input('tool', 'tool', 112, toolStep)] }
},
common(module) {
const objectStep = box(module, 10, 10, 10)
const toolStep = placedBox(module, 5, 5, 5, 8, 2, 2)
return { response: module.booleanHistoryFromStep(objectStep, toolStep, 'common'), inputs: [input('object', 'object', 121, objectStep), input('tool', 'tool', 122, toolStep)] }
},
rotate(module) {
const objectStep = placedBox(module, 2, 1, 1, 1, 0, 0)
return { response: module.rotateHistoryFromStep(objectStep, 0, 0, 0, 0, 0, 1, 30), inputs: [input('object', 'object', 131, objectStep)] }
},
pad(module) {
const objectStep = rectangle(module, 2, 3)
return { response: module.prismHistoryFromStep(objectStep, 0, 0, 5), inputs: [input('object', 'object', 141, objectStep)] }
},
pocket(module) {
const objectStep = box(module, 10, 10, 10)
const toolStep = rectangle(module, 2, 3)
return { response: module.pocketHistoryFromStep(objectStep, toolStep, 0, 0, 5), inputs: [input('object', 'object', 151, objectStep), input('tool', 'tool', 152, toolStep)] }
},
loft(module) {
const objectStep = placedRectangle(module, 2, 2, 0, 0, 0)
const toolStep = placedRectangle(module, 2, 2, 0, 0, 5)
return { response: module.loftHistoryFromStep(objectStep, toolStep, false), inputs: [input('object', 'object', 161, objectStep), input('tool', 'tool', 162, toolStep)] }
},
pipe(module) {
const objectStep = rectangle(module, 2, 2)
const toolStep = line(module, 0, 0, 0, 0, 0, 5)
return { response: module.pipeHistoryFromStep(objectStep, toolStep), inputs: [input('object', 'object', 171, objectStep), input('tool', 'tool', 172, toolStep)] }
},
revolution(module) {
const objectStep = rectangle(module, 2, 3)
return { response: module.revolutionHistoryFromStep(objectStep, -1, 0, 0, 0, 1, 0, 360), inputs: [input('object', 'object', 181, objectStep)] }
},
groove(module) {
const objectStep = placedBox(module, 10, 10, 8, -5, 0, -4)
const toolStep = placedRectangle(module, 1, 3, 0.5, 2, 0)
return { response: module.grooveHistoryFromStep(objectStep, toolStep, 0, 0, 0, 0, 1, 0, 360), inputs: [input('object', 'object', 191, objectStep), input('tool', 'tool', 192, toolStep)] }
},
fillet(module) {
const objectStep = box(module, 6, 6, 6)
return { response: module.filletHistoryFromStep(objectStep, 0.4), inputs: [input('object', 'object', 201, objectStep)] }
},
chamfer(module) {
const objectStep = box(module, 6, 6, 6)
return { response: module.chamferHistoryFromStep(objectStep, 0.4), inputs: [input('object', 'object', 211, objectStep)] }
},
hole(module) {
const objectStep = box(module, 10, 10, 10)
return { response: module.holeHistoryFromStep(objectStep, 1, 10, 0, 0, -5, 0, 0, 1), inputs: [input('object', 'object', 221, objectStep)] }
},
draft(module) {
const objectStep = box(module, 10, 10, 10)
return { response: module.draftHistoryFromStep(objectStep, 0, 5, 0, 0, 1, 0, 0, 0, 0, 0, 1, false), inputs: [input('object', 'object', 231, objectStep)] }
},
thickness(module) {
const objectStep = box(module, 6, 6, 6)
return { response: module.thicknessHistoryFromStep(objectStep, 1, -0.4, false), inputs: [input('object', 'object', 241, objectStep)] }
},
'linear-pattern'(module) {
const objectStep = box(module, 2, 2, 2)
return { response: module.linearPatternHistoryFromStep(objectStep, 2, 0, 0), inputs: [input('object', 'object', 251, objectStep)] }
},
'polar-pattern'(module) {
const objectStep = placedBox(module, 2, 1, 1, -1, -0.5, -0.5)
return { response: module.polarPatternHistoryFromStep(objectStep, 0, 0, 0, 0, 0, 1, 90), inputs: [input('object', 'object', 261, objectStep)] }
},
mirrored(module) {
const objectStep = placedBox(module, 2, 1, 1, -0.5, -0.5, -0.5)
return { response: module.mirroredHistoryFromStep(objectStep, 0, 0, 0, 1, 0, 0), inputs: [input('object', 'object', 271, objectStep)] }
},
'multi-transform'(module) {
const objectStep = placedBox(module, 2, 1, 1, -0.5, -0.5, -0.5)
return {
response: module.multiTransformHistoryFromStep(objectStep, [
{ type: 'linear', direction: [1, 0, 0] },
{ type: 'mirrored', axisOrigin: [0, 0, 0], direction: [1, 0, 0] },
]),
inputs: [input('object', 'object', 281, objectStep)],
}
},
}
const serializableHistory = (response) => ({
provider: response.provider,
occtVersion: response.occtVersion,
records: response.records,
hasModified: response.hasModified,
hasGenerated: response.hasGenerated,
hasDeleted: response.hasDeleted,
resultStep: response.resultStep,
resultBrep: response.resultBrep,
})
const externallySourcedRecords = (records, inputs) => {
const sources = new Set(inputs.flatMap(({ inputId, role }) => [inputId, role]).filter(Boolean))
if (inputs.length > 0) sources.add('object')
if (inputs.length > 1) sources.add('tool')
return records.filter((record) => sources.has(record.sourceId ?? record.source))
}
const resolveInput = (record, inputs) => {
const source = record.sourceId ?? record.source
return inputs.find((entry) => entry.inputId === source)
?? inputs.find((entry) => entry.role === source)
?? (source === 'object' ? inputs[0] : undefined)
?? (source === 'tool' ? inputs[1] : undefined)
}
const persistentSourceName = (record, sourceInput) => {
const prior = sourceInput.namingEvidence?.mappedNames?.find((mapped) => mapped.kind === record.kind && mapped.resultIndex === record.sourceIndex)
const title = `${record.kind[0].toUpperCase()}${record.kind.slice(1)}`
return prior?.resultPersistentId ?? `${title}${record.sourceIndex + 1}`
}
export const classifyFreeCadNamingProvenance = (records, inputs) => {
const externalRecords = externallySourcedRecords(records, inputs)
const grouped = new Map()
for (const record of externalRecords) {
if (record.relation === 'deleted') continue
const sourceInput = resolveInput(record, inputs)
if (!sourceInput) throw new Error(`Native source ${record.sourceId ?? record.source} has no transport input.`)
const resultKind = record.resultKind || record.kind
const indexes = Array.isArray(record.resultIndexes) ? record.resultIndexes : [record.resultIndex]
for (const resultIndex of indexes) {
if (!Number.isSafeInteger(resultIndex) || resultIndex < 0) throw new Error(`Native result ${resultKind}:${String(resultIndex)} is invalid.`)
const key = `${resultKind}:${resultIndex}`
const entries = grouped.get(key) ?? []
entries.push({
record: { ...record, resultKind, resultIndex, resultIndexes: undefined },
resultKind,
resultIndex,
candidate: {
inputId: sourceInput.inputId,
objectId: sourceInput.objectId || sourceInput.inputId,
persistentId: persistentSourceName(record, sourceInput),
...(sourceInput.stageId ? { stageId: sourceInput.stageId } : {}),
relation: record.relation,
sourceKind: record.kind,
sourceIndex: record.sourceIndex,
},
})
grouped.set(key, entries)
}
}
const selected = externalRecords.filter((record) => record.relation === 'deleted').map((record) => ({ ...record }))
const ambiguities = []
let duplicateRecordCount = 0
let relationConflictCount = 0
for (const entries of grouped.values()) {
const unique = new Map()
for (const entry of entries) {
const candidate = entry.candidate
const key = `${candidate.objectId}\u0000${candidate.persistentId}\u0000${candidate.stageId || ''}\u0000${candidate.relation}`
if (!unique.has(key)) unique.set(key, entry)
}
duplicateRecordCount += entries.length - unique.size
const distinctSources = new Set([...unique.values()].map(({ candidate }) => `${candidate.objectId}\u0000${candidate.persistentId}\u0000${candidate.stageId || ''}`))
if (distinctSources.size > 1) {
ambiguities.push({ resultKind: entries[0].resultKind, resultIndex: entries[0].resultIndex, candidates: [...unique.values()].map(({ candidate }) => candidate) })
continue
}
const relations = new Set([...unique.values()].map(({ candidate }) => candidate.relation))
if (relations.size > 1) relationConflictCount += 1
const stable = [...unique.values()].find(({ candidate }) => candidate.relation === 'modified') ?? [...unique.values()][0]
selected.push(stable.record)
}
return {
records: selected,
ambiguities,
externalRecordCount: externalRecords.length,
internalRecordCount: records.length - externalRecords.length,
duplicateRecordCount,
relationConflictCount,
}
}
const validateEvidence = (operation, evidence, recordCount) => {
if (evidence.status !== 'native-evidence') throw new Error(`${operation}: callback did not return native evidence.`)
if (evidence.mappedNames?.length !== recordCount || recordCount < 1) throw new Error(`${operation}: callback did not map every selected native history result.`)
// A first-generation mapping from plain Face1/Edge1 names legitimately has
// no interned strings. Chained mappings below must restore and grow the table.
if (!Array.isArray(evidence.stringHasher?.entries)) throw new Error(`${operation}: StringHasher evidence is absent.`)
if (!Array.isArray(evidence.elementMap2?.maps) || evidence.elementMap2.maps.length < 1) throw new Error(`${operation}: ElementMap2 evidence is empty.`)
const hasherIds = new Set(evidence.stringHasher.entries.map((entry) => entry.id))
const tokens = evidence.elementMap2.maps.flatMap((map) => map.sections.flatMap((section) => section.names.flatMap((name) => name.tokens)))
for (const mapped of evidence.mappedNames) {
const reference = mapped.reference
if (typeof reference?.name !== 'string' || !reference.name) throw new Error(`${operation}: MappedNameRef is malformed.`)
if (reference.name.startsWith('#')) {
if (!Number.isSafeInteger(reference.prefixStringId) || !reference.stringIds?.includes(reference.prefixStringId) || !reference.stringIds.every((id) => hasherIds.has(id))) throw new Error(`${operation}: MappedNameRef is not closed over StringHasher.`)
if (!tokens.some((token) => token.marker === '$' && token.name === reference.name)) throw new Error(`${operation}: MappedNameRef is absent from ElementMap2.`)
}
}
}
export const runFreeCadNamingProductionMatrix = async (module) => {
if (!FREECAD_NAMING_CALLBACKS.every((name) => typeof module[name] === 'function')) throw new Error('Production Worker omits a required FreeCAD naming callback.')
if (module.freecadNamingAbiVersion() !== 1) throw new Error('Production Worker FreeCAD naming ABI version is not 1.')
const descriptor = JSON.parse(module.freecadNamingCapabilitiesJson())
if (descriptor.freecadVersion !== '1.1.1' || descriptor.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('Production Worker naming descriptor is not source locked.')
if (descriptor.operations?.length !== FREECAD_NAMING_OPERATIONS.length || FREECAD_NAMING_OPERATIONS.some((operation) => !descriptor.operations.includes(operation))) throw new Error('Production Worker naming descriptor does not cover all native builders.')
const cases = []
let chainedSource
for (let index = 0; index < FREECAD_NAMING_OPERATIONS.length; index += 1) {
const operation = FREECAD_NAMING_OPERATIONS[index]
const { response, inputs } = fixtures[operation](module)
try {
if (response.provider !== 'occt-native' || !/^8\./.test(response.occtVersion) || !response.resultStep?.startsWith('ISO-10303-21;')) throw new Error(`${operation}: native builder response is invalid.`)
const provenance = classifyFreeCadNamingProvenance(response.records, inputs)
const records = provenance.records
const stableResultCount = records.filter((record) => record.relation !== 'deleted').length
if (stableResultCount < 1 && provenance.ambiguities.length < 1) throw new Error(`${operation}: native builder returned no usable result history.`)
const request = {
schemaVersion: 1,
requestId: `production-${operation}-request`,
documentId: 'production-naming-matrix',
documentVersion: index + 1,
operationId: `production-${operation}`,
operation,
stageId: `production:${operation}:stage`,
resultObjectId: `production:${operation}:result`,
resultObjectTag: 1000 + index,
inputs,
stages: [{ stageId: `production:${operation}:stage`, operation, inputIds: inputs.map(({ inputId }) => inputId), ordinal: 0 }],
resultStep: response.resultStep,
resultBrep: response.resultBrep,
...(provenance.ambiguities.length > 0 ? { historyAmbiguities: provenance.ambiguities } : {}),
history: { ...serializableHistory(response), records },
}
const evidence = stableResultCount > 0 ? JSON.parse(module.freecadNamingEvidenceJson(JSON.stringify(request))) : undefined
if (evidence) validateEvidence(operation, evidence, stableResultCount)
cases.push({ operation, status: 'pass', namingStatus: evidence ? (provenance.ambiguities.length > 0 ? 'mixed-native-and-ambiguous' : 'native-evidence') : 'ambiguous', callbackExecuted: Boolean(evidence), historyRecords: response.records.length, externallySourcedRecords: provenance.externalRecordCount, internalRecordsExcluded: provenance.internalRecordCount, duplicateRecordsCollapsed: provenance.duplicateRecordCount, modifiedRelationPrecedence: provenance.relationConflictCount, selectedRecords: stableResultCount, ambiguousResults: provenance.ambiguities.length, provenanceResults: stableResultCount + provenance.ambiguities.length, mappedNames: evidence?.mappedNames.length ?? 0, stringHasherEntries: evidence?.stringHasher.entries.length ?? 0, elementMaps: evidence?.elementMap2.maps.length ?? 0 })
if (operation === 'cut' && evidence) chainedSource = { request, evidence, record: records.find((record) => record.relation !== 'deleted') }
} finally {
response.result?.delete?.()
}
}
const symmetricObjectStep = box(module, 10, 10, 10)
const symmetricToolStep = box(module, 10, 10, 10)
const symmetricResponse = module.booleanHistoryFromStep(symmetricObjectStep, symmetricToolStep, 'common')
let isomorphicCorpus
try {
const symmetricInputs = [input('symmetric-object', 'object', 301, symmetricObjectStep), input('symmetric-tool', 'tool', 302, symmetricToolStep)]
const provenance = classifyFreeCadNamingProvenance(symmetricResponse.records, symmetricInputs)
if (provenance.ambiguities.length < 1 || provenance.ambiguities.some(({ candidates }) => candidates.length < 2)) throw new Error('Symmetric Common did not retain its non-unique native source candidates.')
const candidateSets = provenance.ambiguities.map(({ resultKind, resultIndex, candidates }) => ({ result: `${resultKind}:${resultIndex}`, candidates: candidates.map(({ objectId, persistentId, stageId }) => `${objectId}:${persistentId}:${stageId || ''}`).sort() }))
isomorphicCorpus = {
operation: 'common',
fixture: 'coincident-equal-boxes',
status: 'pass',
historyRecords: symmetricResponse.records.length,
uniqueResults: provenance.records.filter((record) => record.relation !== 'deleted').length,
ambiguousResults: provenance.ambiguities.length,
candidateSets,
arrayOrderFallbackUsed: false,
}
} finally {
symmetricResponse.result?.delete?.()
}
if (!chainedSource) throw new Error('Production naming matrix lacks its chained cut source.')
const nextIndex = chainedSource.record.resultIndex + 1
const chainedRequest = {
...chainedSource.request,
requestId: 'production-cut-chained-request',
documentVersion: 20,
operationId: 'production-cut-chained',
stageId: 'production:cut:chained-stage',
resultObjectId: 'production:cut:chained-result',
resultObjectTag: 1100,
inputs: [{ inputId: 'object', objectId: chainedSource.request.resultObjectId, role: 'object', stageId: chainedSource.request.stageId, objectTag: chainedSource.request.resultObjectTag, step: chainedSource.request.resultStep, namingEvidence: chainedSource.evidence }],
stages: [{ stageId: 'production:cut:chained-stage', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
history: { ...chainedSource.request.history, records: [{ relation: 'modified', source: 'object', kind: chainedSource.record.resultKind || chainedSource.record.kind, sourceIndex: chainedSource.record.resultIndex, resultIndex: nextIndex }] },
}
const chainedEvidence = JSON.parse(module.freecadNamingEvidenceJson(JSON.stringify(chainedRequest)))
validateEvidence('cut-chained', chainedEvidence, 1)
if (chainedEvidence.stringHasher.entries.length <= chainedSource.evidence.stringHasher.entries.length) throw new Error('Production Worker did not restore and extend the prior StringHasher table.')
const invalid = JSON.parse(module.freecadNamingEvidenceJson(JSON.stringify({ ...chainedSource.request, requestId: 'production-invalid-history', history: { ...chainedSource.request.history, records: [] } })))
if (invalid.status !== 'error' || !invalid.error?.includes('requires inputs and native history records')) throw new Error('Production Worker did not fail closed for missing native history.')
return {
schemaVersion: 1,
status: 'pass',
implementation: 'freecad-linked',
productionWorkerLinked: true,
occtVersion: module.occtVersion(),
freecadVersion: descriptor.freecadVersion,
sourceCommit: descriptor.sourceCommit,
callbacks: [...FREECAD_NAMING_CALLBACKS],
operations: [...FREECAD_NAMING_OPERATIONS],
cases,
callbackExecutedCases: cases.filter(({ callbackExecuted }) => callbackExecuted).length,
ambiguityOnlyCases: cases.filter(({ namingStatus }) => namingStatus === 'ambiguous').length,
isomorphicCorpus,
chainedStage: { mappedNames: chainedEvidence.mappedNames.length, stringHasherEntries: chainedEvidence.stringHasher.entries.length },
invalidHistoryRejected: true,
systemExact: false,
}
}

View File

@@ -0,0 +1,231 @@
import json
import os
import tempfile
import FreeCAD as App
import Part # noqa: F401 - registers Part and PartDesign shapes
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def version_text():
return ".".join(str(value) for value in App.Version()[:3])
def document_open(name):
return name in App.listDocuments()
def object_ref(obj):
if obj is None:
return None
return {"document": obj.Document.Name, "object": obj.Name}
def xlink_ref(value):
return object_ref(value)
def xlink_sub_ref(value):
if not value or value[0] is None:
return None
obj, sub_elements = value
subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
return {**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]}
def xlink_list_ref(value):
return [object_ref(obj) for obj in value if obj is not None]
def xlink_sub_list_ref(value):
output = []
for obj, sub_elements in value:
if obj is None:
continue
subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
output.append({**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]})
return output
def property_status(holder, name):
return [str(value) for value in holder.getPropertyStatus(name)]
def holder_state(holder):
return {
"xLink": xlink_ref(holder.XLink),
"xLinkSub": xlink_sub_ref(holder.XLinkSub),
"xLinkList": xlink_list_ref(holder.XLinkList),
"xLinkSubList": xlink_sub_list_ref(holder.XLinkSubList),
"propertyStatus": {
name: property_status(holder, name)
for name in ["XLink", "XLinkSub", "XLinkList", "XLinkSubList"]
},
"state": [str(value) for value in holder.State],
"status": str(holder.getStatusString()),
}
def binder_state(binder):
support = []
for obj, sub_elements in binder.Support:
subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
support.append({**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]})
return {
"support": support,
"area": float(binder.Shape.Area),
"shapeNull": bool(binder.Shape.isNull()),
"state": [str(value) for value in binder.State],
"status": str(binder.getStatusString()),
}
def linked_dimensions(holder):
return {
"xLinkLength": float(holder.XLink.Length) if holder.XLink else None,
"xLinkSubWidth": float(holder.XLinkSub[0].Width) if holder.XLinkSub and holder.XLinkSub[0] else None,
"xLinkListHeights": [float(obj.Height) for obj in holder.XLinkList if obj is not None],
"xLinkSubListAreas": [float(obj.getSubObject(sub).Area) for obj, subs in holder.XLinkSubList for sub in subs],
}
with tempfile.TemporaryDirectory(prefix="freecad-xlink-relink-") as temp_dir:
source_path = os.path.join(temp_dir, "XLinkSource.FCStd")
consumer_path = os.path.join(temp_dir, "XLinkConsumer.FCStd")
missing_path = os.path.join(temp_dir, "XLinkSource.missing.FCStd")
source = App.newDocument("XLinkSource")
source_body = source.addObject("PartDesign::Body", "SourceBody")
source_box = source_body.newObject("PartDesign::AdditiveBox", "SourceBox")
source_box.Length = 4
source_box.Width = 5
source_box.Height = 6
second_body = source.addObject("PartDesign::Body", "SecondBody")
second_box = second_body.newObject("PartDesign::AdditiveBox", "SecondBox")
second_box.Length = 3
second_box.Width = 7
second_box.Height = 2
source.recompute()
source.saveAs(source_path)
consumer = App.newDocument("XLinkConsumer")
consumer.saveAs(consumer_path)
holder = consumer.addObject("App::FeaturePython", "XLinkHolder")
holder.addProperty("App::PropertyXLink", "XLink", "External")
holder.addProperty("App::PropertyXLinkSub", "XLinkSub", "External")
holder.addProperty("App::PropertyXLinkList", "XLinkList", "External")
holder.addProperty("App::PropertyXLinkSubList", "XLinkSubList", "External")
holder.XLink = source_box
holder.XLinkSub = (source_box, ["Face1"])
holder.XLinkList = [source_box, second_box]
holder.XLinkSubList = [(source_box, ["Face1"]), (second_box, ["Face1", "Face2"])]
binder_body = consumer.addObject("PartDesign::Body", "BinderBody")
binder = binder_body.newObject("PartDesign::SubShapeBinder", "ExternalBinder")
binder.Support = [(source_box, ("Face1",))]
consumer.recompute()
consumer.saveAs(consumer_path)
source.save()
consumer.save()
initial = {
"holder": holder_state(holder),
"dimensions": linked_dimensions(holder),
"binder": binder_state(binder),
"sourceDocuments": sorted(document.Name for document in App.listDocuments().values()),
}
App.closeDocument(source.Name)
consumer.recompute()
source_closed = {
"holder": holder_state(holder),
"binder": binder_state(binder),
"sourceOpen": document_open("XLinkSource"),
}
reopened_source = App.openDocument(source_path)
reopened_source.recompute()
consumer.recompute()
relink_after_close = {
"holder": holder_state(holder),
"dimensions": linked_dimensions(holder),
"binder": binder_state(binder),
"sourceOpen": document_open("XLinkSource") and App.listDocuments()["XLinkSource"] is reopened_source,
}
App.closeDocument(consumer.Name)
App.closeDocument(reopened_source.Name)
os.rename(source_path, missing_path)
missing_consumer = App.openDocument(consumer_path)
missing_consumer.recompute()
missing_holder = missing_consumer.getObject("XLinkHolder")
missing_binder = missing_consumer.getObject("ExternalBinder")
missing_source = {
"holder": holder_state(missing_holder),
"binder": binder_state(missing_binder),
"sourceOpen": document_open("XLinkSource"),
}
os.rename(missing_path, source_path)
restored_source = App.openDocument(source_path)
restored_source.recompute()
missing_consumer.recompute()
restored_holder = missing_consumer.getObject("XLinkHolder")
restored_binder = missing_consumer.getObject("ExternalBinder")
relink_after_missing = {
"holder": holder_state(restored_holder),
"dimensions": linked_dimensions(restored_holder),
"binder": binder_state(restored_binder),
"sourceOpen": document_open("XLinkSource") and App.listDocuments()["XLinkSource"] is restored_source,
}
restored_box = restored_source.getObject("SourceBox")
restored_second = restored_source.getObject("SecondBox")
restored_box.Length = 8
restored_box.Width = 9
restored_second.Height = 4
restored_source.recompute()
missing_consumer.recompute()
edited = {
"holder": holder_state(restored_holder),
"dimensions": linked_dimensions(restored_holder),
"binder": binder_state(restored_binder),
"sourceFaceArea": float(restored_box.Shape.Face1.Area),
}
restored_source.save()
missing_consumer.save()
App.closeDocument(missing_consumer.Name)
App.closeDocument(restored_source.Name)
final_source = App.openDocument(source_path)
final_consumer = App.openDocument(consumer_path)
final_source.recompute()
final_consumer.recompute()
final_holder = final_consumer.getObject("XLinkHolder")
final_binder = final_consumer.getObject("ExternalBinder")
final_roundtrip = {
"holder": holder_state(final_holder),
"dimensions": linked_dimensions(final_holder),
"binder": binder_state(final_binder),
"sourceFaceArea": float(final_source.getObject("SourceBox").Shape.Face1.Area),
}
App.closeDocument(final_consumer.Name)
App.closeDocument(final_source.Name)
report = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-xlink-relink-oracle",
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"status": "pass",
"tolerance": 1e-7,
"propertyTypes": ["App::PropertyXLink", "App::PropertyXLinkSub", "App::PropertyXLinkList", "App::PropertyXLinkSubList"],
"initial": initial,
"sourceClosed": source_closed,
"relinkAfterClose": relink_after_close,
"missingSource": missing_source,
"relinkAfterMissing": relink_after_missing,
"edited": edited,
"finalRoundtrip": final_roundtrip,
}
print("FREECAD_XLINK_RELINK_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))

View File

@@ -607,7 +607,7 @@ async function smoke() {
FREECAD_SOURCE_OFFLINE: '1',
OCCT_SOURCE_DIR: resolve(root, '.cache/occt/occt'),
}
for (const script of ['check:runtime', 'check:freecad-source', 'build:freecad-naming-source-probe', 'test:freecad-naming-source-probe', 'check:freecad-naming-sdk-readiness', 'check:freecad-wasm-sdk-build-plan', 'check:freecad-naming-next-tasks', 'check:freecad-private-naming-boundary', 'check:occt-history-artifact', 'test:occt-history', 'test:planegcs', 'build']) {
for (const script of ['check:runtime', 'check:freecad-source', 'build:freecad-naming-source-probe', 'test:freecad-naming-source-probe', 'check:freecad-naming-sdk-readiness', 'check:freecad-wasm-sdk-build-plan', 'check:freecad-naming-next-tasks', 'publish:occt-history', 'check:freecad-naming-production', 'check:freecad-isomorphic-provenance', 'check:freecad-native-property-semantics', 'check:freecad-private-naming-boundary', 'check:occt-history-artifact', 'test:occt-history', 'test:planegcs', 'build']) {
print(`[offline:smoke] npm run ${script}`)
await run(resolve(root, 'npmw'), ['run', script], { env: environment })
}

View File

@@ -0,0 +1,18 @@
import { copyFile, mkdir, readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const root = resolve(new URL('..', import.meta.url).pathname)
const dist = resolve(root, 'native/occt-history/dist')
const publicRoot = resolve(root, 'public/native/occt-history')
const artifacts = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
const factory = (await import(pathToFileURL(resolve(dist, artifacts[0])).href)).default
const module = await factory({ locateFile: (path) => resolve(dist, path) })
const callbacks = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']
if (!callbacks.every((name) => typeof module[name] === 'function') || module.freecadNamingAbiVersion() !== 1) throw new Error('Refusing to publish an OCCT history artifact without the locked FreeCAD naming ABI.')
const descriptor = JSON.parse(module.freecadNamingCapabilitiesJson())
if (descriptor.freecadVersion !== '1.1.1' || descriptor.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('Refusing to publish an unlocked FreeCAD naming ABI.')
await Promise.all(artifacts.map((name) => readFile(resolve(dist, name))))
await mkdir(publicRoot, { recursive: true })
await Promise.all(artifacts.map((name) => copyFile(resolve(dist, name), resolve(publicRoot, name))))
console.log(JSON.stringify({ status: 'occt-history-published', implementation: 'freecad-linked', artifacts, callbacks }, null, 2))

View File

@@ -0,0 +1,80 @@
import { createHash } from 'node:crypto'
import { createReadStream, existsSync } from 'node:fs'
import { readFile, stat, writeFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { extname, normalize, resolve } from 'node:path'
import { spawn } from 'node:child_process'
import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const productionRoot = resolve(root, 'public/native/occt-history')
const matrixPath = resolve(root, 'scripts/freecad-naming-production-matrix.mjs')
const reportPath = resolve(root, 'config/chrome-freecad-naming-production-verification.json')
const chrome = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome'
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
for (const name of artifactNames) if (!existsSync(resolve(productionRoot, name))) throw new Error(`Missing production Worker artifact: ${name}`)
if (!existsSync(chrome)) throw new Error(`Chrome executable is unavailable: ${chrome}`)
const artifacts = await Promise.all(artifactNames.map(async (name) => {
const path = resolve(productionRoot, name)
const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)])
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
}))
const matrixContent = await readFile(matrixPath)
const harness = { path: 'scripts/freecad-naming-production-matrix.mjs', bytes: matrixContent.length, sha256: createHash('sha256').update(matrixContent).digest('hex') }
let resolveReport
let rejectReport
const reportPromise = new Promise((resolveValue, rejectValue) => { resolveReport = resolveValue; rejectReport = rejectValue })
const html = `<!doctype html><meta charset="utf-8"><title>Production FreeCAD naming Worker</title><script type="module">
const send = async (report) => fetch('/__report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
try {
if (!crossOriginIsolated || typeof SharedArrayBuffer !== 'function') throw new Error('Production naming harness requires cross-origin isolation and SharedArrayBuffer.')
const [{ default: createModule }, { runFreeCadNamingProductionMatrix }] = await Promise.all([import('/production/bitbybit-occt-history.js'), import('/matrix.mjs')])
const module = await createModule({ locateFile: (path) => new URL('/production/' + path, location.href).href })
const result = await runFreeCadNamingProductionMatrix(module)
await send({ ...result, runtime: 'chrome', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated })
} catch (error) {
await send({ schemaVersion: 1, status: 'failed', runtime: 'chrome', browserId: 'chrome', error: error instanceof Error ? error.stack || error.message : String(error), productionWorkerLinked: false, systemExact: false })
}
</script>`
const contentTypes = { '.js': 'text/javascript', '.mjs': 'text/javascript', '.wasm': 'application/wasm', '.data': 'application/octet-stream' }
const server = createServer((request, response) => {
response.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
response.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
if (request.method === 'POST' && request.url === '/__report') {
let body = ''
request.setEncoding('utf8')
request.on('data', (chunk) => { body += chunk })
request.on('end', () => {
try { resolveReport(JSON.parse(body)); response.writeHead(204); response.end() }
catch (error) { rejectReport(error); response.writeHead(400); response.end(String(error)) }
})
return
}
if (request.url === '/' || request.url === '/index.html') { response.setHeader('content-type', 'text/html'); response.end(html); return }
const file = request.url === '/matrix.mjs'
? matrixPath
: request.url?.startsWith('/production/') ? normalize(resolve(productionRoot, decodeURIComponent(request.url.slice('/production/'.length)))) : ''
if (!file || (file !== matrixPath && !file.startsWith(productionRoot)) || !existsSync(file)) { response.writeHead(404); response.end('not found'); return }
response.setHeader('content-type', contentTypes[extname(file)] || 'application/octet-stream')
createReadStream(file).pipe(response)
})
await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer))
const port = server.address().port
const profile = await createChromeProfile('freecad-naming-production')
const child = spawn(chrome, ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check', `--user-data-dir=${profile}`, `http://127.0.0.1:${port}/`], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] })
let stderr = ''
child.stderr.on('data', (chunk) => { stderr += String(chunk) })
const timeout = setTimeout(() => { rejectReport(new Error(`Chrome production naming harness timed out. ${stderr.slice(-2000)}`)); child.kill('SIGTERM') }, 300_000)
try {
const report = { ...(await reportPromise), artifacts, harness, generatedAt: new Date().toISOString() }
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))
if (report.status !== 'pass') process.exitCode = 1
} finally {
clearTimeout(timeout)
child.kill('SIGTERM')
await new Promise((resolveServer) => server.close(resolveServer))
await removeChromeProfile(profile)
}

View File

@@ -0,0 +1,78 @@
import { createServer } from 'node:http'
import { createHash } from 'node:crypto'
import { existsSync } from 'node:fs'
import { readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { spawn } from 'node:child_process'
import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const reportPath = resolve(root, 'config/chrome-partdesign-reference-lifecycle-verification.json')
const evidencePaths = [
'public/chrome-partdesign-reference-lifecycle-harness.html',
'src/facade/externalPartDesignLinks.ts',
'src/facade/mockFacade.ts',
'src/facade/geometryRuntime.ts',
'src/facade/geometryWorker.ts',
'node_modules/@bitbybit-dev/occt/bitbybit-dev-occt/bitbybit-dev-occt.a4a6ec2a.wasm',
]
const collectEvidenceInputs = async () => Promise.all(evidencePaths.map(async (path) => {
const bytes = await readFile(resolve(root, path))
return { path, bytes: bytes.byteLength, sha256: createHash('sha256').update(bytes).digest('hex') }
}))
const waitForPort = (child) => new Promise((resolvePort, reject) => {
let output = ''
const timer = setTimeout(() => reject(new Error(`Vite did not announce a port: ${output.slice(-2000)}`)), 30_000)
const onData = (chunk) => { output += String(chunk); const match = output.match(/http:\/\/127\.0\.0\.1:(\d+)/); if (match) { clearTimeout(timer); resolvePort(Number(match[1])) } }
child.stdout.on('data', onData)
child.stderr.on('data', onData)
})
const vite = spawn('./nodew', ['node_modules/vite/bin/vite.js', '--host', '127.0.0.1'], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] })
let vitePort
try { vitePort = await waitForPort(vite) } catch (error) { vite.kill('SIGTERM'); throw error }
let resolveReport
const reportPromise = new Promise((resolveValue) => { resolveReport = resolveValue })
const server = createServer(async (request, response) => {
response.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
response.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
if (request.method === 'POST' && request.url === '/__partdesign-reference-lifecycle-report') {
let body = ''
request.setEncoding('utf8')
request.on('data', (chunk) => { body += chunk })
request.on('end', async () => {
const report = { ...JSON.parse(body), evidenceInputs: await collectEvidenceInputs() }
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
resolveReport(report)
response.writeHead(204)
response.end()
})
return
}
if (request.url?.startsWith('/chrome-partdesign-reference-lifecycle-harness.html')) {
response.setHeader('content-type', 'text/html; charset=utf-8')
response.end(await readFile(resolve(root, 'public/chrome-partdesign-reference-lifecycle-harness.html')))
return
}
try {
const upstream = await fetch(`http://127.0.0.1:${vitePort}${request.url}`, { headers: { ...request.headers } })
response.writeHead(upstream.status, Object.fromEntries(upstream.headers.entries()))
response.end(Buffer.from(await upstream.arrayBuffer()))
} catch (error) { response.writeHead(502); response.end(String(error)) }
})
await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer))
const port = server.address().port
const executable = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome'
if (!existsSync(executable)) throw new Error(`Chrome executable is missing: ${executable}`)
const profile = await createChromeProfile('partdesign-reference-lifecycle')
const chrome = spawn(executable, ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--noerrdialogs', '--no-first-run', `--user-data-dir=${profile}`, `http://127.0.0.1:${port}/chrome-partdesign-reference-lifecycle-harness.html`], { cwd: root, stdio: ['ignore', 'ignore', 'ignore'] })
const report = await Promise.race([reportPromise, new Promise((resolveTimeout) => setTimeout(() => resolveTimeout({ status: 'timeout', browserId: 'chrome' }), 180_000))])
chrome.kill('SIGTERM')
vite.kill('SIGTERM')
server.closeAllConnections?.()
server.close()
await removeChromeProfile(profile)
console.log(JSON.stringify(report, null, 2))
if (report.status !== 'pass') process.exit(1)
process.exit(0)

View File

@@ -0,0 +1,27 @@
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 attachment combination oracle 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-attachment-combination-oracle.py')], {
cwd: root,
encoding: 'utf8',
timeout: 300_000,
maxBuffer: 128 * 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}` : ''}`,
},
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_ATTACHMENT_COMBINATION_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD attachment combination 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-attachment-combination-oracle.json'), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, baselineId: report.baselineId, cases: report.caseCount, engines: report.engineCaseCounts, mutation: report.translation, fcstdRoundtrip: true }, null, 2))

View File

@@ -8,6 +8,7 @@ const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/inst
const pythonPath = resolve(root, '.cache/freecad/sysroot/usr/lib/python3/dist-packages')
const script = resolve(root, 'scripts/freecad-composite-history-elementmap-oracle.py')
const outputPath = resolve(root, 'config/freecad-composite-history-elementmap-oracle.json')
const resaveOutputPath = resolve(root, 'config/freecad-composite-history-resave-verification.json')
if (!existsSync(executable)) throw new Error(`FreeCAD composite oracle executable is missing: ${executable}`)
const execution = spawnSync(executable, ['--python-path', pythonPath, script], {
cwd: root,
@@ -28,5 +29,29 @@ const markerIndex = output.indexOf(marker)
const payload = markerIndex >= 0 ? output.slice(markerIndex + marker.length).match(/\{.*\}/s)?.[0] : undefined
if (execution.error || execution.status !== 0 || !payload) throw new Error(`FreeCAD composite history oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(payload)
const resaveReport = {
schemaVersion: 1,
baselineId: report.baselineId,
freecadVersion: report.freecadVersion,
gitCommit: report.gitCommit,
status: report.status,
cases: report.cases.map(({ id, roundtripNameDrift, resaveNameDrift, nativeDesktopResaveCovered }) => ({ id, roundtripNameDrift, resaveNameDrift, nativeDesktopResaveCovered })),
summary: {
cases: report.summary.cases,
passed: report.summary.passed,
failed: report.summary.failed,
roundtripNameDrift: report.summary.roundtripNameDrift,
resaveNameDrift: report.summary.resaveNameDrift,
nativeDesktopResaveCases: report.summary.nativeDesktopResaveCases,
},
}
await writeFile(resaveOutputPath, `${JSON.stringify(resaveReport, null, 2)}\n`)
for (const caseReport of report.cases) {
delete caseReport.resaveNameDrift
delete caseReport.nativeDesktopResaveCovered
}
delete report.summary.roundtripNameDrift
delete report.summary.resaveNameDrift
delete report.summary.nativeDesktopResaveCases
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))

View File

@@ -0,0 +1,223 @@
import { createHash } from 'node:crypto'
import { readFile, stat, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { inspectFcstdArchive, rewriteFcstdMetadataArchive, serializeFcstdMetadataArchive } from '../src/facade/fcstd'
import { DirectNativeOcctHistoryProvider, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION } from '../src/facade/nativeHistoryProtocol'
import type { NativeStageNamingEvidence } from '../src/facade/nativeNamingEvidence'
import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse } from '../src/facade/nativeHistoryProvider'
import type { DocumentSnapshot, TopoRefValue } from '../src/facade/types'
const root = resolve(new URL('..', import.meta.url).pathname)
const dist = resolve(root, 'native/occt-history/dist')
const modulePath = resolve(dist, 'bitbybit-occt-history.js')
const reportPath = resolve(root, 'config/freecad-isomorphic-provenance-verification.json')
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
const fail = (message: string): never => { throw new Error(`FreeCAD isomorphic provenance verification failed: ${message}`) }
const createModule = (await import(pathToFileURL(modulePath).href)).default
const module = await createModule({ locateFile: (path: string) => resolve(dist, path) })
const provider = new DirectNativeOcctHistoryProvider(module)
const shapeStep = (method: string, ...args: number[]) => {
const shape = module[method](...args)
try { return module.shapeToStep(shape) as string }
finally { shape.delete?.() }
}
const namingCounts = (evidence: NativeStageNamingEvidence) => ({
status: evidence.status,
stable: evidence.mappedNames?.filter(({ relation }) => relation !== 'ambiguous').length ?? 0,
ambiguous: evidence.mappedNames?.filter(({ relation }) => relation === 'ambiguous').length ?? 0,
candidates: evidence.mappedNames?.filter(({ relation }) => relation === 'ambiguous').reduce((count, mapped) => count + (mapped.candidates?.length ?? 0), 0) ?? 0,
stringHasherEntries: evidence.stringHasher ? ('entries' in evidence.stringHasher ? evidence.stringHasher.entries.length : 0) : 0,
elementMaps: evidence.elementMap2 ? ('maps' in evidence.elementMap2 ? evidence.elementMap2.maps.length : 0) : 0,
})
const referencedUpstreamCandidateObjects = (records: NativeOcctHistoryResponse['records'], evidence: NativeStageNamingEvidence) => {
const objects = new Set<string>()
for (const record of records) {
if (record.relation === 'deleted' || !['object', 'tool'].includes(record.sourceId ?? record.source)) continue
const mapped = evidence.mappedNames?.find((entry) => entry.kind === record.kind && entry.resultIndex === record.sourceIndex)
for (const candidate of mapped?.candidates ?? []) objects.add(candidate.objectId)
}
return objects
}
const commonObjectStep = shapeStep('makeBox', 10, 10, 10)
const commonToolStep = shapeStep('makeBox', 10, 10, 10)
const common = await provider.capture({
protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
requestId: 'isomorphic-common', documentId: 'isomorphic-doc', documentVersion: 1,
operationId: 'common', resultObjectId: 'common-result', resultObjectTag: 401, operation: 'common',
objectStep: commonObjectStep, toolStep: commonToolStep,
inputs: [
{ inputId: 'object', objectId: 'symmetric-object', role: 'object', stageId: 'source-object', objectTag: 301, step: commonObjectStep },
{ inputId: 'tool', objectId: 'symmetric-tool', role: 'tool', stageId: 'source-tool', objectTag: 302, step: commonToolStep },
],
stages: [{ stageId: 'common-stage', operation: 'common', inputIds: ['object', 'tool'], ordinal: 0 }],
}, new AbortController().signal)
const commonEvidence = common.history.namingEvidence ?? fail('symmetric Common omitted naming evidence')
const commonCounts = namingCounts(commonEvidence)
if (commonCounts.status !== 'ambiguous' || commonCounts.stable !== 0 || commonCounts.ambiguous < 1 || commonCounts.candidates < commonCounts.ambiguous * 2) fail('symmetric Common did not retain all native candidates')
const persistedCommon = JSON.parse(JSON.stringify(commonEvidence)) as NativeStageNamingEvidence
if (JSON.stringify(persistedCommon.mappedNames) !== JSON.stringify(commonEvidence.mappedNames)) fail('candidate sets changed across JSON persistence')
const cutObjectStep = shapeStep('makeBox', 10, 10, 10)
const cutToolStep = shapeStep('makeBox', 5, 5, 5)
const cut = await provider.capture({
protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
requestId: 'mixed-cut', documentId: 'isomorphic-doc', documentVersion: 2,
operationId: 'cut', resultObjectId: 'cut-result', resultObjectTag: 501, operation: 'cut',
objectStep: cutObjectStep, toolStep: cutToolStep,
inputs: [
{ inputId: 'object', objectId: 'cut-object', role: 'object', stageId: 'cut-object-source', objectTag: 311, step: cutObjectStep },
{ inputId: 'tool', objectId: 'cut-tool', role: 'tool', stageId: 'cut-tool-source', objectTag: 312, step: cutToolStep },
],
stages: [{ stageId: 'cut-stage', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
}, new AbortController().signal)
const cutEvidence = cut.history.namingEvidence ?? fail('mixed Cut omitted naming evidence')
const cutCounts = namingCounts(cutEvidence)
if (cutCounts.status !== 'ambiguous' || cutCounts.stable < 1 || cutCounts.ambiguous < 1 || cutCounts.elementMaps < 1) fail('mixed Cut did not combine private names with explicit ambiguity')
const cutResultStep = cut.history.resultStep ?? fail('mixed Cut omitted result STEP')
const downstream = await provider.capture({
protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
requestId: 'downstream-rotate', documentId: 'isomorphic-doc', documentVersion: 3,
operationId: 'rotate', resultObjectId: 'rotate-result', resultObjectTag: 601, operation: 'rotate',
objectStep: cutResultStep, axisOrigin: [0, 0, 0], direction: [0, 0, 1], angle: 15,
inputs: [{ inputId: 'object', objectId: 'cut-result', role: 'object', stageId: 'cut-stage', objectTag: 501, step: cutResultStep, namingEvidence: JSON.parse(JSON.stringify(cutEvidence)) as NativeStageNamingEvidence }],
stages: [{ stageId: 'rotate-stage', operation: 'rotate', inputIds: ['object'], ordinal: 0 }],
}, new AbortController().signal)
const downstreamEvidence = downstream.history.namingEvidence ?? fail('downstream Rotate omitted naming evidence')
const downstreamCounts = namingCounts(downstreamEvidence)
const downstreamCandidateObjects = new Set(downstreamEvidence.mappedNames?.flatMap(({ candidates }) => candidates?.map(({ objectId }) => objectId) ?? []) ?? [])
const downstreamReferencedCandidateObjects = referencedUpstreamCandidateObjects(downstream.history.records, cutEvidence)
if (downstreamCounts.status !== 'ambiguous' || downstreamCounts.ambiguous < 1 || !downstreamCandidateObjects.has('cut-object') || !downstreamCandidateObjects.has('cut-tool')) fail('downstream feature collapsed upstream ambiguity')
if ([...downstreamReferencedCandidateObjects].some((objectId) => !downstreamCandidateObjects.has(objectId))) fail('downstream feature lost a referenced upstream candidate')
type ChainSource = { objectId: string; objectTag: number; stageId: string; step: string; evidence: NativeStageNamingEvidence }
type ChainDefinition = { operation: NativeOcctHistoryOperation; parameters: Record<string, unknown> }
const chainStages = [{
operation: 'cut',
historyRecords: cut.history.records.length,
...cutCounts,
candidateObjects: [...new Set(cutEvidence.mappedNames?.flatMap(({ candidates }) => candidates?.map(({ objectId }) => objectId) ?? []) ?? [])].sort(),
persistedInputEvidence: false,
}, {
operation: 'rotate',
historyRecords: downstream.history.records.length,
...downstreamCounts,
candidateObjects: [...downstreamCandidateObjects].sort(),
referencedInputCandidateObjects: [...downstreamReferencedCandidateObjects].sort(),
referencedCandidateObjectsRetained: true,
persistedInputEvidence: true,
}]
const chainResponses = [cut, downstream]
let chainSource: ChainSource = {
objectId: 'rotate-result', objectTag: 601, stageId: 'rotate-stage',
step: downstream.history.resultStep ?? fail('downstream Rotate omitted result STEP'), evidence: downstreamEvidence,
}
let previousHasherEntries = downstreamCounts.stringHasherEntries
const chainDefinitions: ChainDefinition[] = [
{ operation: 'fillet', parameters: { radius: 0.2 } },
{ operation: 'mirrored', parameters: { axisOrigin: [0, 0, 0], direction: [1, 0, 0] } },
{ operation: 'linear-pattern', parameters: { direction: [3, 0, 0] } },
]
for (const [index, definition] of chainDefinitions.entries()) {
const documentVersion = index + 4
const stageId = `${definition.operation}-stage`
const resultObjectId = `${definition.operation}-result`
const resultObjectTag = 701 + index
const persistedInputEvidence = JSON.parse(JSON.stringify(chainSource.evidence)) as NativeStageNamingEvidence
const response = await provider.capture({
protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
requestId: `long-chain-${definition.operation}`, documentId: 'isomorphic-doc', documentVersion,
operationId: definition.operation, resultObjectId, resultObjectTag, operation: definition.operation,
objectStep: chainSource.step,
inputs: [{ inputId: 'object', objectId: chainSource.objectId, role: 'object', stageId: chainSource.stageId, objectTag: chainSource.objectTag, step: chainSource.step, namingEvidence: persistedInputEvidence }],
stages: [{ stageId, operation: definition.operation, inputIds: ['object'], ordinal: 0 }],
...definition.parameters,
} as Parameters<typeof provider.capture>[0], new AbortController().signal)
const evidence = response.history.namingEvidence ?? fail(`${definition.operation} omitted naming evidence`)
const counts = namingCounts(evidence)
const candidateObjects = new Set(evidence.mappedNames?.flatMap(({ candidates }) => candidates?.map(({ objectId }) => objectId) ?? []) ?? [])
const referencedInputCandidateObjects = referencedUpstreamCandidateObjects(response.history.records, persistedInputEvidence)
if (counts.status !== 'ambiguous' || counts.stable < 1 || counts.ambiguous < 1 || counts.elementMaps < 1) fail(`${definition.operation} did not preserve mixed stable/ambiguous naming evidence`)
if ([...referencedInputCandidateObjects].some((objectId) => !candidateObjects.has(objectId))) fail(`${definition.operation} lost a referenced upstream candidate`)
if (counts.stringHasherEntries < previousHasherEntries) fail(`${definition.operation} regressed the restored StringHasher table`)
previousHasherEntries = counts.stringHasherEntries
chainStages.push({ operation: definition.operation, historyRecords: response.history.records.length, ...counts, candidateObjects: [...candidateObjects].sort(), referencedInputCandidateObjects: [...referencedInputCandidateObjects].sort(), referencedCandidateObjectsRetained: true, persistedInputEvidence: true })
chainResponses.push(response)
chainSource = { objectId: resultObjectId, objectTag: resultObjectTag, stageId, step: response.history.resultStep ?? fail(`${definition.operation} omitted result STEP`), evidence }
}
const persistedFinalEvidence = JSON.parse(JSON.stringify(chainSource.evidence)) as NativeStageNamingEvidence
if (JSON.stringify(persistedFinalEvidence.mappedNames) !== JSON.stringify(chainSource.evidence.mappedNames)) fail('long-chain naming evidence changed across final JSON persistence')
const ambiguousRef: TopoRefValue = { schemaVersion: 1, objectId: 'Source', kind: 'face', persistentId: 'Face1', topologyVersion: 2, generation: 1, status: 'ambiguous', candidates: ['Face1', 'Face2'] }
const fcstdDocument: DocumentSnapshot = {
id: 'fcstd-ambiguous', label: 'FCStd ambiguity boundary', version: 1, dirty: false, readOnly: false, units: 'mm',
tree: [{ id: 'Source', label: 'Source', type: 'feature' }, { id: 'Consumer', label: 'Consumer', type: 'feature' }],
objects: [
{ id: 'Source', typeId: 'Part::Feature', properties: [] },
{ id: 'Consumer', typeId: 'Part::Feature', properties: [{ name: 'Support', label: 'Support', group: 'Links', scope: 'data', type: 'App::PropertyLinkSub', value: { schemaVersion: 1, objectId: 'Source', subElements: [ambiguousRef] } }] },
],
dependencies: [{ sourceId: 'Consumer', targetId: 'Source', relation: 'topo-ref', propertyName: 'Support', reference: 'Face1' }],
recompute: { generation: 0, status: 'idle', objectStates: { Source: 'clean', Consumer: 'clean' }, dirtyObjects: [], order: [], errors: [] },
}
let fcstdAmbiguityRejected = false
try { serializeFcstdMetadataArchive(fcstdDocument) }
catch (error) { fcstdAmbiguityRejected = /stable topology status/.test(error instanceof Error ? error.message : String(error)) }
if (!fcstdAmbiguityRejected) fail('FCStd writer did not reject an ambiguous LinkSub')
const stableRef: TopoRefValue = { schemaVersion: 1, objectId: 'Source', kind: 'face', persistentId: 'Face1', topologyVersion: 5, generation: 5, status: 'stable' }
const stableFcstdDocument: DocumentSnapshot = {
...fcstdDocument,
id: 'fcstd-stable', label: 'FCStd stable LinkSub boundary', version: 5,
objects: fcstdDocument.objects.map((object) => object.id !== 'Consumer' ? object : {
...object,
properties: object.properties.map((property) => property.name !== 'Support' ? property : { ...property, value: { schemaVersion: 1, objectId: 'Source', subElements: [stableRef] } }),
}),
}
const stableArchive = serializeFcstdMetadataArchive(stableFcstdDocument)
const stableInspection = inspectFcstdArchive(stableArchive)
const nativeLinkSub = stableInspection.objects.find(({ name }) => name === 'Consumer')?.properties.find(({ name }) => name === 'Support')
if (JSON.stringify(nativeLinkSub?.subElements) !== '["Face1"]') fail('FCStd writer did not emit the stable native LinkSub name')
const rewrittenStableArchive = rewriteFcstdMetadataArchive(stableArchive, stableInspection.proxyDocument)
const rewrittenStableLinkSub = inspectFcstdArchive(rewrittenStableArchive).objects.find(({ name }) => name === 'Consumer')?.properties.find(({ name }) => name === 'Support')
if (JSON.stringify(rewrittenStableLinkSub?.subElements) !== '["Face1"]') fail('FCStd inspect/rewrite changed the stable native LinkSub name')
for (const response of [common, ...chainResponses]) (response.history as typeof response.history & { result?: { delete?(): void } }).result?.delete?.()
const artifacts = await Promise.all(artifactNames.map(async (name) => {
const path = resolve(dist, name)
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
}))
const harnessPath = resolve(root, 'scripts/run-freecad-isomorphic-provenance.ts')
const harnessContent = await readFile(harnessPath)
const report = {
schemaVersion: 1, status: 'pass', runtime: 'node', implementation: 'freecad-linked', productionWorkerLinked: true,
cases: {
symmetricCommon: { ...commonCounts, persistedCandidateSets: true },
mixedCut: cutCounts,
downstreamRotate: { ...downstreamCounts, candidateObjects: [...downstreamCandidateObjects].sort() },
longChain: {
operations: chainStages.map(({ operation }) => operation),
stages: chainStages,
persistedFinalEvidence: true,
referencedCandidateObjectsRetained: true,
stringHasherMonotonic: true,
},
},
fcstdAmbiguityRejected,
fcstdStableLinkRoundtrip: { subElements: rewrittenStableLinkSub?.subElements, inspectRewritePreserved: true, nativeDesktopResaveCovered: false },
artifacts,
harness: { path: 'scripts/run-freecad-isomorphic-provenance.ts', bytes: harnessContent.length, sha256: createHash('sha256').update(harnessContent).digest('hex') },
exactBlocker: 'exhaustive cross-feature naming and native FreeCAD FCStd save-reopen-resave corpus',
systemExact: false,
generatedAt: new Date().toISOString(),
}
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))

View File

@@ -0,0 +1,23 @@
import { createHash } from 'node:crypto'
import { readFile, stat, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { runFreeCadNamingProductionMatrix } from './freecad-naming-production-matrix.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const dist = resolve(root, 'native/occt-history/dist')
const matrixPath = resolve(root, 'scripts/freecad-naming-production-matrix.mjs')
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
const createModule = (await import(pathToFileURL(resolve(dist, artifactNames[0])).href)).default
const module = await createModule({ locateFile: (path) => resolve(dist, path) })
const report = await runFreeCadNamingProductionMatrix(module)
const artifacts = await Promise.all(artifactNames.map(async (name) => {
const path = resolve(dist, name)
const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)])
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
}))
const matrixContent = await readFile(matrixPath)
const harness = { path: 'scripts/freecad-naming-production-matrix.mjs', bytes: matrixContent.length, sha256: createHash('sha256').update(matrixContent).digest('hex') }
const output = { ...report, runtime: 'node', artifacts, harness, generatedAt: new Date().toISOString() }
await writeFile(resolve(root, 'config/freecad-naming-production-verification.json'), `${JSON.stringify(output, null, 2)}\n`)
console.log(JSON.stringify(output, null, 2))

View File

@@ -0,0 +1,82 @@
import { createHash } from 'node:crypto'
import { readFile, stat, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
const outputPath = resolve(root, 'config/freecad-native-property-semantics.json')
const harnessPath = resolve(root, 'scripts/run-freecad-native-property-semantics.mjs')
const sourceContent = await readFile(sourcePath)
const source = JSON.parse(sourceContent)
const fail = (message) => { throw new Error(`FreeCAD native property semantics probe failed: ${message}`) }
if (source.schemaVersion !== 1 || source.freecadVersion !== '1.1.1' || source.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline')
const runtime = source.runtimeObjects
if (!runtime?.available || runtime.candidateCount !== 352 || runtime.availableCount !== 348 || runtime.unavailableCount !== 4 || !Array.isArray(runtime.types)) fail('runtime Document.supportedTypes inventory is incomplete')
const editableTypes = new Set([
'App::PropertyAngle', 'App::PropertyBool', 'App::PropertyDistance', 'App::PropertyEnumeration',
'App::PropertyFloat', 'App::PropertyFloatList', 'App::PropertyInteger', 'App::PropertyIntegerConstraint',
'App::PropertyIntegerList', 'App::PropertyLength', 'App::PropertyLink', 'App::PropertyLinkList',
'App::PropertyLinkSub', 'App::PropertyLinkSubList', 'App::PropertyPlacement', 'App::PropertyString',
'App::PropertyStringList', 'App::PropertyVector',
])
const specializedTypes = new Set([
'App::PropertyExpressionEngine', 'Part::PropertyGeometryList', 'Part::PropertyPartShape',
'Path::PropertyPath', 'Sketcher::PropertyConstraintList',
])
const numericStatusNames = new Map([
[13, 'Ordered'], [22, 'PropNoPersist'], [23, 'PropNoRecompute'], [24, 'PropReadOnly'],
[25, 'PropTransient'], [26, 'PropHidden'], [27, 'PropOutput'],
])
const facadeStatusNames = new Set(['Hidden', 'ReadOnly'])
const normalizeStatus = (status) => status.map((entry) => typeof entry === 'number' ? (numericStatusNames.get(entry) ?? `UnknownBit${entry}`) : entry)
const properties = runtime.types.flatMap((object) => (object.properties ?? []).map((property) => ({ ...property, objectTypeId: object.typeId, status: normalizeStatus(property.status) })))
if (properties.length !== 5510 || properties.some((property) => !property.name || !property.typeId || !Array.isArray(property.status))) fail('runtime property records are incomplete')
const byType = new Map()
for (const property of properties) {
const entry = byType.get(property.typeId) ?? { typeId: property.typeId, recordCount: 0, objectTypeIds: new Set(), statusNames: new Set() }
entry.recordCount += 1
entry.objectTypeIds.add(property.objectTypeId)
property.status.forEach((status) => entry.statusNames.add(status))
byType.set(property.typeId, entry)
}
const types = [...byType.values()].sort((left, right) => left.typeId.localeCompare(right.typeId)).map((entry) => ({
typeId: entry.typeId,
support: editableTypes.has(entry.typeId) ? 'native-editable-codec' : specializedTypes.has(entry.typeId) ? 'native-specialized-codec' : 'opaque-fcstd-proxy',
recordCount: entry.recordCount,
objectTypeCount: entry.objectTypeIds.size,
statusNames: [...entry.statusNames].sort(),
}))
const supportSummary = Object.fromEntries(['native-editable-codec', 'native-specialized-codec', 'opaque-fcstd-proxy'].map((support) => {
const selected = types.filter((entry) => entry.support === support)
return [support, { typeCount: selected.length, recordCount: selected.reduce((total, entry) => total + entry.recordCount, 0) }]
}))
const observedStatuses = [...new Set(properties.flatMap((property) => property.status))].sort()
const unsupportedStatusNames = observedStatuses.filter((status) => !facadeStatusNames.has(status))
const unsupportedStatusRecordCount = properties.filter((property) => property.status.some((status) => unsupportedStatusNames.includes(status))).length
const unavailableObjects = runtime.types.filter(({ available }) => !available).map(({ typeId, error }) => ({ typeId, error }))
const harnessContent = await readFile(harnessPath)
const report = {
schemaVersion: 1,
status: 'pass',
baseline: { freecadVersion: source.freecadVersion, commit: source.gitCommit },
source: { path: '.cache/freecad/reference-desktop.json', bytes: sourceContent.length, sha256: createHash('sha256').update(sourceContent).digest('hex') },
runtime: { registeredObjectTypes: runtime.candidateCount, instantiableObjectTypes: runtime.availableCount, unavailableObjectTypes: runtime.unavailableCount, unavailableObjects, propertyRecords: properties.length, propertyTypes: types.length },
supportSummary,
types,
propertyStatus: {
observed: observedStatuses,
facadeNative: [...facadeStatusNames].sort(),
proxyOnly: unsupportedStatusNames,
proxyOnlyRecordCount: unsupportedStatusRecordCount,
unknownNumericBits: observedStatuses.filter((status) => status.startsWith('UnknownBit')),
},
harness: { path: 'scripts/run-freecad-native-property-semantics.mjs', bytes: (await stat(harnessPath)).size, sha256: createHash('sha256').update(harnessContent).digest('hex') },
exactPromotionReady: false,
exactBlocker: 'complete native document and property semantics',
generatedAt: new Date().toISOString(),
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, runtime: report.runtime, supportSummary, propertyStatus: report.propertyStatus, exactPromotionReady: false }, null, 2))

View File

@@ -0,0 +1,27 @@
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 XLink/relink oracle 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-xlink-relink-oracle.py')], {
cwd: root,
encoding: 'utf8',
timeout: 180_000,
maxBuffer: 32 * 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}` : ''}`,
},
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_XLINK_RELINK_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD XLink/relink 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-xlink-relink-oracle.json'), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, baselineId: report.baselineId, propertyTypes: report.propertyTypes, sourceCloseRelink: true, missingFileRelink: true, editPropagation: true, fcstdRoundtrip: true }, null, 2))

View File

@@ -39,6 +39,8 @@ const wasmBuilds = process.env.CI_REAL_REBUILD_WASM === '1'
'build:boost-wasm',
'build:freecad-naming-source-probe',
'test:freecad-naming-source-probe',
'build:freecad-naming-sdk-candidate',
'generate:freecad-naming-sdk-manifest',
'check:freecad-naming-sdk-readiness',
'check:freecad-wasm-sdk-build-plan',
'build:occt-history',
@@ -80,6 +82,8 @@ const lanes = {
'probe:freecad-partdesign-transform',
'probe:freecad-partdesign-structure',
'probe:freecad-attachment-modes',
'probe:freecad-attachment-combinations',
'probe:freecad-xlink-relink',
'probe:freecad-partdesign-failures',
'probe:freecad-partdesign-revolution-groove',
'probe:freecad-part-builders',
@@ -87,6 +91,8 @@ const lanes = {
'test:freecad-fcstd-native',
'generate:freecad-parameter-mutations',
'check:freecad-oracle-coverage',
'probe:freecad-native-property-semantics',
'check:freecad-native-property-semantics',
'check:freecad-golden-coverage',
'check:freecad-parameter-mutations',
'check:freecad-sketcher-constraints',
@@ -97,6 +103,10 @@ const lanes = {
],
wasm: [
...wasmBuilds,
'test:freecad-naming-production',
'check:freecad-naming-production',
'test:freecad-isomorphic-provenance',
'check:freecad-isomorphic-provenance',
'check:occt-history-artifact',
'check:freecad-private-naming-boundary',
'test:browser-occt',