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

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

View File

@@ -17,7 +17,7 @@ import { runTopologyMutationReplay } from '../src/facade/topologyReplay'
import { captureNativeTopologyHistory, captureNativeTopologyHistoryStages, captureSignatureTopologyHistory, composeNativeTopologyHistoryLineage } from '../src/facade/topologyHistory'
import { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from '../src/facade/nativeHistoryProvider'
import { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, nativeNamingAbiCapabilities, type NativeOcctHistoryProvider } from '../src/facade/nativeHistoryProtocol'
import { probeFreeCadPrivateNamingAbi } from '../src/facade/nativeNamingAbi'
import { captureFreeCadPrivateNamingEvidence, classifyFreeCadPrivateNamingHistory, createFreeCadPrivateNamingAbiRequest, probeFreeCadPrivateNamingAbi, selectFreeCadPrivateNamingHistoryRecords } from '../src/facade/nativeNamingAbi'
import { NativeOcctHistoryWorkerProvider } from '../src/facade/nativeHistoryWorkerClient'
import { assessResourceQuota, planResourceSweep } from '../src/facade/resourcePolicy'
import { applySketchAutoConstraints, cloneSketch, createSketch, deleteSketchGeometry, dragSketchPoint, editBsplineGeometry, editSketchBspline, extendSketchLine, FREECAD_SKETCHER_CONSTRAINT_TYPES, FREECAD_SKETCHER_GEOMETRY_TYPES, FREECAD_SKETCHER_INTERNAL_ALIGNMENT_TYPES, replaySketchEditorEvents, setSketchConstruction, SketchEditorInteractionSession, sketchGeometrySignature, solveSketch, splitSketchLine, suggestSketchAutoConstraints, trimSketchLine, validateSketchGeometry, type SketchGeometry } from '../src/facade/sketcher'
@@ -705,6 +705,79 @@ test('native naming ABI capabilities forbid synthetic FreeCAD tokens unless a pr
assert.deepEqual(nativeNamingAbiCapabilities({ ...base, naming: { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', elementMap2: 'opaque-preserved', tokenGeneration: 'native-only' } }), { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', elementMap2: 'opaque-preserved', tokenGeneration: 'native-only' })
})
test('FreeCAD naming ABI excludes native history sources without an external transport input', () => {
const records = [
{ relation: 'modified' as const, source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 },
{ relation: 'generated' as const, source: 'tool', kind: 'face', sourceIndex: 0, resultIndex: 1 },
{ relation: 'modified' as const, source: 'base-input', kind: 'edge', sourceIndex: 0, resultIndex: 2 },
{ relation: 'generated' as const, source: 'object', sourceId: 'native-cylinder', kind: 'face', sourceIndex: 0, resultIndex: 3 },
]
const baseInput = { inputId: 'base-input', objectId: 'base', role: 'object', step: 'ISO-10303-21; base', objectTag: 17 }
assert.deepEqual(selectFreeCadPrivateNamingHistoryRecords(records, [baseInput]), [records[0], records[2]])
assert.deepEqual(selectFreeCadPrivateNamingHistoryRecords(records, [baseInput, { inputId: 'tool-input', role: 'tool' }]), [records[0], records[1], records[2]])
const request = createFreeCadPrivateNamingAbiRequest({
requestId: 'hole-naming', documentId: 'doc', documentVersion: 1, operationId: 'hole', operation: 'hole', stageId: 'hole:stage', resultObjectId: 'hole:result', resultObjectTag: 23,
inputs: [baseInput], stages: [{ stageId: 'hole:stage', operation: 'hole', inputIds: ['base-input'], ordinal: 0 }],
history: { provider: 'occt-native', occtVersion: '8.0.0', records, hasModified: true, hasGenerated: true, hasDeleted: false },
})
assert.deepEqual(request.history.records.map(({ source, resultIndex }) => ({ source, resultIndex })), [{ source: 'object', resultIndex: 0 }, { source: 'base-input', resultIndex: 2 }])
})
test('FreeCAD naming ABI preserves non-unique native provenance instead of selecting the first source', () => {
const inputs = [
{ inputId: 'base', objectId: 'base-object', role: 'object', stageId: 'base-stage', step: 'ISO-10303-21; base', objectTag: 17 },
{ inputId: 'tool', objectId: 'tool-object', role: 'tool', stageId: 'tool-stage', step: 'ISO-10303-21; tool', objectTag: 19 },
]
const records = [
{ relation: 'modified' as const, source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 },
{ relation: 'modified' as const, source: 'tool', kind: 'face', sourceIndex: 0, resultIndex: 0 },
{ relation: 'generated' as const, source: 'object', kind: 'edge', sourceIndex: 1, resultIndexes: [1, 1] },
{ relation: 'generated' as const, source: 'native-profile', sourceId: 'native-profile', kind: 'edge', sourceIndex: 0, resultIndex: 2 },
]
const classification = classifyFreeCadPrivateNamingHistory(records, inputs)
assert.deepEqual(classification.records.map(({ kind, resultIndex }) => ({ kind, resultIndex })), [{ kind: 'edge', resultIndex: 1 }])
assert.equal(classification.externalRecordCount, 3)
assert.equal(classification.internalRecordCount, 1)
assert.equal(classification.duplicateRecordCount, 1)
assert.equal(classification.relationConflictCount, 0)
assert.deepEqual(classification.ambiguities, [{
resultKind: 'face', resultIndex: 0, candidates: [
{ inputId: 'base', objectId: 'base-object', persistentId: 'Face1', stageId: 'base-stage', relation: 'modified', sourceKind: 'face', sourceIndex: 0 },
{ inputId: 'tool', objectId: 'tool-object', persistentId: 'Face1', stageId: 'tool-stage', relation: 'modified', sourceKind: 'face', sourceIndex: 0 },
],
}])
const request = createFreeCadPrivateNamingAbiRequest({
requestId: 'ambiguous', documentId: 'doc', documentVersion: 1, operationId: 'common', operation: 'common', stageId: 'common:stage', resultObjectId: 'common:result', resultObjectTag: 23,
inputs, stages: [{ stageId: 'common:stage', operation: 'common', inputIds: ['base', 'tool'], ordinal: 0 }],
history: { provider: 'occt-native', occtVersion: '8.0.0', records: records.slice(0, 2), hasModified: true, hasGenerated: false, hasDeleted: false },
})
let called = false
const module = {
freecadNamingAbiVersion: () => 1,
freecadNamingCapabilitiesJson: () => JSON.stringify({ schemaVersion: 1, freecadVersion: '1.1.1', sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', mappedNameRef: true, stringHasher: true, elementMap2: true, operations: ['common'] }),
freecadNamingEvidenceJson: () => { called = true; return '{}' },
}
const evidence = captureFreeCadPrivateNamingEvidence(module, request)
assert.equal(called, false)
assert.equal(evidence?.status, 'ambiguous')
assert.deepEqual(evidence?.mappedNames?.[0].candidates, [
{ objectId: 'base-object', persistentId: 'Face1', stageId: 'base-stage' },
{ objectId: 'tool-object', persistentId: 'Face1', stageId: 'tool-stage' },
])
const downstream = classifyFreeCadPrivateNamingHistory(
[{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }],
[{ inputId: 'object', objectId: 'common:result', role: 'object', stageId: 'common:stage', step: 'ISO-10303-21; common', namingEvidence: evidence }],
)
assert.equal(downstream.records.length, 0)
assert.deepEqual(downstream.ambiguities[0].candidates.map(({ objectId, persistentId }) => ({ objectId, persistentId })), [
{ objectId: 'base-object', persistentId: 'Face1' },
{ objectId: 'tool-object', persistentId: 'Face1' },
])
})
test('versioned FreeCAD private naming ABI is capability-gated and attaches validated native evidence', async () => {
const elementMap2 = parseElementMap2([
'BeginElementMap v1', '1 PostfixCount 1', 'Edge', 'MapCount 1', 'ElementMap 1 1 1', 'Edge',
@@ -737,7 +810,13 @@ test('versioned FreeCAD private naming ABI is capability-gated and attaches vali
assert.equal(capturedRequest?.resultObjectTag, 23)
assert.equal((capturedRequest?.inputs as Array<{ objectTag?: number; namingEvidence?: { stageId: string } }>)[0].objectTag, 17)
assert.equal((capturedRequest?.inputs as Array<{ objectId?: string }>)[0].objectId, 'base')
assert.equal((capturedRequest?.inputs as Array<{ namingEvidence?: { stageId: string } }>)[0].namingEvidence?.stageId, 'source:stage:0')
assert.deepEqual((capturedRequest?.inputs as Array<{ namingEvidence?: { stageId: string; status: string; mappedNames?: unknown[] } }>)[0].namingEvidence, {
schemaVersion: 1,
stageId: 'source:stage:0',
resultObjectId: 'source',
status: 'final-shape-only',
reason: 'Native provider returned final Shape without MappedNameRef/StringHasher evidence.',
})
assert.notEqual((capturedRequest?.inputs as Array<{ namingEvidence?: unknown }>)[0].namingEvidence, inputEvidence)
await assert.rejects(() => provider.capture({ protocolVersion: 1, requestId: 'naming-invalid-tag', documentId: 'doc', documentVersion: 2, operationId: 'cut-native', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool', resultObjectTag: 0 }, new AbortController().signal), /positive safe integer/)
@@ -4974,6 +5053,11 @@ test('FCStd LinkSub uses native multi-subelement XML and round-trips without JSO
assert.match(writtenXml, /<LinkSub value="pad" count="2"><Sub value="Edge1"\/><Sub value="Edge7"\/><\/LinkSub>/)
const roundTripped = inspectFcstdArchive(zipSync({ 'Document.xml': written })).objects.find((object) => object.name === 'fillet')?.properties[0]
assert.deepEqual(roundTripped?.subElements, ['Edge1', 'Edge7'])
const edgeRef: TopoRefValue = { schemaVersion: 1, objectId: 'pad', kind: 'edge', persistentId: 'Edge1', topologyVersion: 2, generation: 1, status: 'stable' }
const withRef = (ref: TopoRefValue) => serializeFcstdMetadataArchive({ ...document, objects: [document.objects[0], { ...document.objects[1], properties: [{ ...document.objects[1].properties[0], value: { schemaVersion: 1, objectId: 'pad', subElements: [ref] } }] }] })
assert.match(new TextDecoder().decode(unzipSync(withRef(edgeRef))['Document.xml']), /<Sub value="Edge1"\/>/)
assert.throws(() => withRef({ ...edgeRef, status: 'ambiguous', candidates: ['Edge1', 'Edge2'] }), /stable topology status/)
assert.throws(() => withRef({ ...edgeRef, persistentId: 'web-edge-id' }), /lossless native EdgeN/)
assert.throws(() => inspectFcstdArchive(zipSync({ 'Document.xml': strToU8(xml.replace('count="2"', 'count="3"')) })), /LinkSub count/)
})
@@ -4990,6 +5074,9 @@ test('FCStd LinkSubList uses native obj/sub children and preserves structured en
const summary = inspectFcstdArchive(serializeFcstdMetadataArchive(document)).objects.find((object) => object.name === 'holder')?.properties[0]
assert.deepEqual(summary?.linkSubs, [{ objectId: 'pad', subElement: 'Face1' }, { objectId: 'pad', subElement: 'Edge3' }, { objectId: 'body', subElement: '' }])
assert.deepEqual(decodeFcstdPropertyValue(summary!), { value: { schemaVersion: 1, entries: [{ objectId: 'pad', subElement: 'Face1' }, { objectId: 'pad', subElement: 'Edge3' }, { objectId: 'body', subElement: '' }] }, decoded: true })
const faceRef: TopoRefValue = { schemaVersion: 1, objectId: 'pad', kind: 'face', persistentId: 'Face1', topologyVersion: 2, generation: 1, status: 'ambiguous', candidates: ['Face1', 'Face2'] }
const ambiguousDocument = { ...document, objects: [{ ...document.objects[0], properties: [{ ...document.objects[0].properties[0], value: { schemaVersion: 1, entries: [{ objectId: 'pad', subElement: faceRef }] } }] }, ...document.objects.slice(1)] }
assert.throws(() => serializeFcstdMetadataArchive(ambiguousDocument), /stable topology status/)
const malformed = '<Document><Objects><Object name="holder" type="Part::Feature"/></Objects><ObjectData><Object name="holder"><Properties><Property name="References" type="App::PropertyLinkSubList"><LinkSubList count="2"><Link obj="pad" sub="Face1"/></LinkSubList></Property></Properties></Object></ObjectData></Document>'
assert.throws(() => inspectFcstdArchive(zipSync({ 'Document.xml': strToU8(malformed) })), /LinkSubList count/)
})