feat: complete production naming and reference lifecycle gates
This commit is contained in:
@@ -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`)
|
||||
|
||||
36
scripts/check-chrome-partdesign-reference-lifecycle.mjs
Normal file
36
scripts/check-chrome-partdesign-reference-lifecycle.mjs
Normal 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))
|
||||
36
scripts/check-freecad-attachment-combination-oracle.mjs
Normal file
36
scripts/check-freecad-attachment-combination-oracle.mjs
Normal 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))
|
||||
@@ -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))
|
||||
|
||||
@@ -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')
|
||||
|
||||
33
scripts/check-freecad-isomorphic-provenance.mjs
Normal file
33
scripts/check-freecad-isomorphic-provenance.mjs
Normal 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))
|
||||
@@ -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))
|
||||
|
||||
54
scripts/check-freecad-naming-production.mjs
Normal file
54
scripts/check-freecad-naming-production.mjs
Normal 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))
|
||||
@@ -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.')
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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.`)
|
||||
|
||||
19
scripts/check-freecad-native-property-semantics.mjs
Normal file
19
scripts/check-freecad-native-property-semantics.mjs
Normal 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))
|
||||
@@ -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))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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()]
|
||||
|
||||
31
scripts/check-freecad-xlink-relink-oracle.mjs
Normal file
31
scripts/check-freecad-xlink-relink-oracle.mjs
Normal 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))
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
214
scripts/freecad-attachment-combination-oracle.py
Normal file
214
scripts/freecad-attachment-combination-oracle.py
Normal 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=(",", ":")))
|
||||
@@ -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=(",", ":")))
|
||||
|
||||
347
scripts/freecad-naming-production-matrix.mjs
Normal file
347
scripts/freecad-naming-production-matrix.mjs
Normal 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,
|
||||
}
|
||||
}
|
||||
231
scripts/freecad-xlink-relink-oracle.py
Normal file
231
scripts/freecad-xlink-relink-oracle.py
Normal 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=(",", ":")))
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
18
scripts/publish-occt-history.mjs
Normal file
18
scripts/publish-occt-history.mjs
Normal 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))
|
||||
80
scripts/run-chrome-freecad-naming-production.mjs
Normal file
80
scripts/run-chrome-freecad-naming-production.mjs
Normal 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)
|
||||
}
|
||||
78
scripts/run-chrome-partdesign-reference-lifecycle.mjs
Normal file
78
scripts/run-chrome-partdesign-reference-lifecycle.mjs
Normal 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)
|
||||
27
scripts/run-freecad-attachment-combination-oracle.mjs
Normal file
27
scripts/run-freecad-attachment-combination-oracle.mjs
Normal 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))
|
||||
@@ -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))
|
||||
|
||||
223
scripts/run-freecad-isomorphic-provenance.ts
Normal file
223
scripts/run-freecad-isomorphic-provenance.ts
Normal 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))
|
||||
23
scripts/run-freecad-naming-production.mjs
Normal file
23
scripts/run-freecad-naming-production.mjs
Normal 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))
|
||||
82
scripts/run-freecad-native-property-semantics.mjs
Normal file
82
scripts/run-freecad-native-property-semantics.mjs
Normal 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))
|
||||
27
scripts/run-freecad-xlink-relink-oracle.mjs
Normal file
27
scripts/run-freecad-xlink-relink-oracle.mjs
Normal 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))
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user