feat: add candidate FreeCAD naming SDK and attachment oracles
Build and verify the candidate-only FreeCAD naming bridge and OCCT worker path, including three-stage StringHasher restoration. Add Datum, ShapeBinder, attachment-mode, and PartDesign structure oracles plus offline SDK build plans and CI boundary checks.
This commit is contained in:
185
native/freecad-naming-bridge/smoke-test.ts
Normal file
185
native/freecad-naming-bridge/smoke-test.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import createBridge from './dist/freecad-private-naming-bridge.js'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import {
|
||||
captureFreeCadPrivateNamingEvidence,
|
||||
createFreeCadPrivateNamingAbiRequest,
|
||||
probeFreeCadPrivateNamingAbi,
|
||||
type NativeFreeCadNamingAbiModule,
|
||||
} from '../../src/facade/nativeNamingAbi'
|
||||
|
||||
const distUrl = new URL('./dist/', import.meta.url)
|
||||
const bridge = await createBridge({ locateFile: (path: string) => fileURLToPath(new URL(path, distUrl)) }) as NativeFreeCadNamingAbiModule
|
||||
const probe = probeFreeCadPrivateNamingAbi(bridge)
|
||||
if (probe.availability !== 'available') throw new Error(`Isolated bridge ABI probe failed: ${probe.reason}`)
|
||||
|
||||
const request = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-1',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 1,
|
||||
operationId: 'isolated-cut-1',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:1',
|
||||
resultObjectId: 'isolated:result:1',
|
||||
resultObjectTag: 99,
|
||||
inputs: [
|
||||
{ inputId: 'object', objectId: 'source-object', role: 'object', step: 'ISO-10303-21; object', objectTag: 42 },
|
||||
{ inputId: 'tool', objectId: 'source-tool', role: 'tool', step: 'ISO-10303-21; tool', objectTag: 43 },
|
||||
],
|
||||
stages: [{ stageId: 'isolated:stage:1', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; result',
|
||||
history: {
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: false,
|
||||
records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }],
|
||||
},
|
||||
})
|
||||
const first = captureFreeCadPrivateNamingEvidence(bridge, request, probe)
|
||||
if (!first || first.status !== 'native-evidence' || first.mappedNames?.length !== 1 || !first.elementMap2 || !first.stringHasher) throw new Error('Isolated bridge returned incomplete first-stage evidence.')
|
||||
if (!first.mappedNames[0].reference.postfix?.includes(';:M;CUT;:H2a:7,F')) throw new Error(`Unexpected first-stage FreeCAD postfix: ${first.mappedNames[0].reference.postfix}`)
|
||||
|
||||
const chained = captureFreeCadPrivateNamingEvidence(bridge, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-2',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 2,
|
||||
operationId: 'isolated-cut-2',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:2',
|
||||
resultObjectId: 'isolated:result:2',
|
||||
resultObjectTag: 100,
|
||||
inputs: [{ inputId: 'object', objectId: 'isolated:result:1', role: 'object', stageId: 'isolated:stage:1', step: 'ISO-10303-21; first result', objectTag: 99, namingEvidence: first }],
|
||||
stages: [{ stageId: 'isolated:stage:2', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; second result',
|
||||
history: {
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: false,
|
||||
records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 1 }],
|
||||
},
|
||||
}), probe)
|
||||
if (!chained || chained.elementMap2?.maps.length !== 1) throw new Error('Isolated bridge did not preserve chained ElementMap2 evidence.')
|
||||
const chainedReference = chained.mappedNames?.[0].reference
|
||||
const prefixStringId = chainedReference?.prefixStringId
|
||||
const stringHasherIds = new Set(chained.stringHasher?.entries.map(({ id }) => id))
|
||||
const chainedTokens = chained.elementMap2.maps.flatMap((map) => map.sections.flatMap((section) => section.names.flatMap((name) => name.tokens)))
|
||||
if (!chainedReference?.name.startsWith('#') || !Number.isSafeInteger(prefixStringId) || !chainedReference.stringIds?.includes(prefixStringId!) || !stringHasherIds.has(prefixStringId!) || !chainedTokens.some((token) => token.marker === '$' && token.name === chainedReference.name)) throw new Error('Isolated bridge did not emit a closed native hashed MappedNameRef/StringHasher/ElementMap2 chain.')
|
||||
|
||||
const third = captureFreeCadPrivateNamingEvidence(bridge, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-3',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 3,
|
||||
operationId: 'isolated-cut-3',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:3',
|
||||
resultObjectId: 'isolated:result:3',
|
||||
resultObjectTag: 101,
|
||||
inputs: [{ inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained }],
|
||||
stages: [{ stageId: 'isolated:stage:3', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; third result',
|
||||
history: {
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: false,
|
||||
records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }],
|
||||
},
|
||||
}), probe)
|
||||
const thirdReference = third?.mappedNames?.[0]?.reference
|
||||
const thirdHasherIds = new Set(third?.stringHasher?.entries.map(({ id }) => id))
|
||||
if (!third || third.status !== 'native-evidence' || !thirdReference?.name.startsWith('#') || !thirdReference.stringIds?.every((id) => thirdHasherIds.has(id)) || (third.stringHasher?.entries.length ?? 0) <= (chained.stringHasher?.entries.length ?? 0)) throw new Error('Isolated bridge did not restore prior StringHasher evidence for a third naming stage.')
|
||||
|
||||
let tamperedHasherRejected = false
|
||||
try {
|
||||
const validRequest = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-tampered',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 3,
|
||||
operationId: 'isolated-cut-tampered',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:tampered',
|
||||
resultObjectId: 'isolated:result:tampered',
|
||||
resultObjectTag: 102,
|
||||
inputs: [{ inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained }],
|
||||
stages: [{ stageId: 'isolated:stage:tampered', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; tampered result',
|
||||
history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }] },
|
||||
})
|
||||
const tamperedRequest = structuredClone(validRequest)
|
||||
const tamperedEntry = tamperedRequest.inputs[0].namingEvidence?.stringHasher?.entries[0]
|
||||
if (!tamperedEntry) throw new Error('Chained evidence has no StringHasher entry to tamper.')
|
||||
tamperedEntry.id += 1
|
||||
const response = JSON.parse(bridge.freecadNamingEvidenceJson!(JSON.stringify(tamperedRequest))) as { status?: string; error?: string }
|
||||
if (response.status !== 'error' || !response.error?.includes('StringHasher IDs must be contiguous and ordered')) throw new Error(`Unexpected tampered StringHasher response: ${JSON.stringify(response)}`)
|
||||
tamperedHasherRejected = true
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
if (!tamperedHasherRejected) throw new Error('Isolated bridge accepted tampered prior StringHasher evidence.')
|
||||
|
||||
let inconsistentTablesRejected = false
|
||||
{
|
||||
const inconsistentRequest = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-inconsistent',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 3,
|
||||
operationId: 'isolated-cut-inconsistent',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:inconsistent',
|
||||
resultObjectId: 'isolated:result:inconsistent',
|
||||
resultObjectTag: 103,
|
||||
inputs: [
|
||||
{ inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained },
|
||||
{ inputId: 'tool', objectId: 'isolated:result:2-copy', role: 'tool', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result copy', objectTag: 104, namingEvidence: chained },
|
||||
],
|
||||
stages: [{ stageId: 'isolated:stage:inconsistent', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; inconsistent result',
|
||||
history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }] },
|
||||
})
|
||||
const secondTable = inconsistentRequest.inputs[1].namingEvidence?.stringHasher
|
||||
if (!secondTable?.entries[0]) throw new Error('Chained evidence has no StringHasher table to make inconsistent.')
|
||||
secondTable.entries[0].data += '-different'
|
||||
const response = JSON.parse(bridge.freecadNamingEvidenceJson!(JSON.stringify(inconsistentRequest))) as { status?: string; error?: string }
|
||||
if (response.status !== 'error' || !response.error?.includes('cannot merge inconsistent prior StringHasher tables')) throw new Error(`Unexpected inconsistent StringHasher response: ${JSON.stringify(response)}`)
|
||||
inconsistentTablesRejected = true
|
||||
}
|
||||
|
||||
let rejected = false
|
||||
try {
|
||||
captureFreeCadPrivateNamingEvidence(bridge, { ...request, history: { ...request.history, records: [] } }, probe)
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error) || !error.message.includes('requires inputs and native history records')) throw error
|
||||
rejected = true
|
||||
}
|
||||
if (!rejected) throw new Error('Isolated bridge accepted a request without native history records.')
|
||||
|
||||
const result = {
|
||||
status: 'freecad-private-naming-isolated-bridge-pass',
|
||||
descriptor: probe.descriptor,
|
||||
firstStage: { mappedNames: first.mappedNames.length, stringHasherEntries: first.stringHasher.entries.length },
|
||||
chainedStage: { mappedNames: chained.mappedNames.length, stringHasherEntries: chained.stringHasher.entries.length },
|
||||
thirdStage: { mappedNames: third.mappedNames.length, stringHasherEntries: third.stringHasher.entries.length },
|
||||
tamperedHasherRejected,
|
||||
inconsistentTablesRejected,
|
||||
productionPublication: false,
|
||||
productionWorkerLinked: false,
|
||||
}
|
||||
const reportPath = process.env.FREECAD_NAMING_BRIDGE_REPORT
|
||||
if (reportPath) {
|
||||
const artifacts = await Promise.all(['freecad-private-naming-bridge.js', 'freecad-private-naming-bridge.wasm', 'freecad-private-naming-bridge.data'].map(async (name) => {
|
||||
const path = fileURLToPath(new URL(name, distUrl))
|
||||
const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)])
|
||||
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
|
||||
}))
|
||||
const absoluteReportPath = resolve(reportPath)
|
||||
await mkdir(dirname(absoluteReportPath), { recursive: true })
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify({ schemaVersion: 1, ...result, artifacts }, null, 2)}\n`)
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
Reference in New Issue
Block a user