Files
Web_FreeCAD_Bitbybit/scripts/freecad-naming-production-matrix.mjs
wangdequan f64b78865c
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
feat: complete production naming and reference lifecycle gates
2026-08-14 10:10:30 -04:00

348 lines
19 KiB
JavaScript

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