93 lines
6.5 KiB
JavaScript
93 lines
6.5 KiB
JavaScript
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::PropertyAcceleration', 'App::PropertyAngle', 'App::PropertyArea', 'App::PropertyBool', 'App::PropertyBoolList', 'App::PropertyColor', 'App::PropertyColorList', 'App::PropertyDistance', 'App::PropertyEnumeration',
|
|
'App::PropertyFloat', 'App::PropertyFloatConstraint', 'App::PropertyFloatList', 'App::PropertyInteger', 'App::PropertyIntegerConstraint',
|
|
'App::PropertyIntegerList', 'App::PropertyLength', 'App::PropertyLink', 'App::PropertyLinkHidden', 'App::PropertyLinkList',
|
|
'App::PropertyLinkSub', 'App::PropertyLinkSubList', 'App::PropertyPlacement', 'App::PropertyString',
|
|
'App::PropertyPrecision', 'App::PropertyQuantityConstraint', 'App::PropertyStringList', 'App::PropertyVector', 'App::PropertyDirection', 'App::PropertyFile', 'App::PropertyFileIncluded',
|
|
])
|
|
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 facadeBehaviorStatusNames = new Set([
|
|
'Hidden', 'Immutable', 'LockDynamic', 'NoModify', 'Ordered', 'Output', 'PartialTrigger',
|
|
'PropHidden', 'PropNoPersist', 'PropNoRecompute', 'PropOutput', 'PropReadOnly', 'PropTransient',
|
|
'ReadOnly', 'Transient',
|
|
])
|
|
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 preservedOnlyStatusNames = observedStatuses.filter((status) => !facadeBehaviorStatusNames.has(status))
|
|
const preservedOnlyStatusRecordCount = properties.filter((property) => property.status.some((status) => preservedOnlyStatusNames.includes(status))).length
|
|
const unavailableObjects = runtime.types.filter(({ available }) => !available).map(({ typeId, error }) => ({ typeId, error }))
|
|
const harnessContent = await readFile(harnessPath)
|
|
const opaqueSupport = supportSummary['opaque-fcstd-proxy']
|
|
const exactPromotionReady = opaqueSupport.typeCount === 0 && opaqueSupport.recordCount === 0
|
|
const exactBlocker = exactPromotionReady ? null : `${opaqueSupport.typeCount} runtime property types and ${opaqueSupport.recordCount} records remain opaque-only; complete native document and property semantics`
|
|
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: observedStatuses,
|
|
behaviorNative: [...facadeBehaviorStatusNames].sort(),
|
|
preservedOnly: preservedOnlyStatusNames,
|
|
preservedOnlyRecordCount: preservedOnlyStatusRecordCount,
|
|
proxyOnly: [],
|
|
proxyOnlyRecordCount: 0,
|
|
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,
|
|
exactBlocker,
|
|
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, exactBlocker }, null, 2))
|