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, } }