import { createHash } from 'node:crypto' import { readFile, stat } from 'node:fs/promises' import { resolve } from 'node:path' const root = resolve(new URL('..', import.meta.url).pathname) const report = JSON.parse(await readFile(resolve(root, 'config/freecad-native-property-semantics.json'), 'utf8')) const fail = (message) => { throw new Error(`FreeCAD native property semantics check failed: ${message}`) } if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.exactPromotionReady !== false || report.exactBlocker !== '46 runtime property types and 457 records remain opaque-only; complete native document and property semantics') fail('report boundary is invalid') if (report.runtime?.registeredObjectTypes !== 352 || report.runtime.instantiableObjectTypes !== 348 || report.runtime.unavailableObjectTypes !== 4 || report.runtime.propertyRecords !== 5510 || report.runtime.propertyTypes !== 85 || report.runtime.unavailableObjects?.length !== 4) fail('runtime inventory counts changed') const support = report.supportSummary if (support?.['native-editable-codec']?.typeCount !== 34 || support['native-editable-codec'].recordCount !== 4370 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 46 || support['opaque-fcstd-proxy'].recordCount !== 457) fail('property support partition is stale') if (report.types?.length !== 85 || new Set(report.types.map(({ typeId }) => typeId)).size !== 85 || report.types.reduce((total, entry) => total + entry.recordCount, 0) !== 5510) fail('per-TypeId inventory is invalid') const acceleration = report.types.find(({ typeId }) => typeId === 'App::PropertyAcceleration') if (acceleration?.support !== 'native-editable-codec' || acceleration.recordCount !== 1 || acceleration.objectTypeCount !== 1) fail('App::PropertyAcceleration promotion is missing') const area = report.types.find(({ typeId }) => typeId === 'App::PropertyArea') if (area?.support !== 'native-editable-codec' || area.recordCount !== 1 || area.objectTypeCount !== 1 || area.statusNames?.join(',') !== 'PropOutput,PropReadOnly') fail('App::PropertyArea promotion is missing') const boolList = report.types.find(({ typeId }) => typeId === 'App::PropertyBoolList') if (boolList?.support !== 'native-editable-codec' || boolList.recordCount !== 7 || boolList.objectTypeCount !== 7 || boolList.statusNames?.join(',') !== 'Hidden,Immutable,LockDynamic') fail('App::PropertyBoolList promotion is missing') const color = report.types.find(({ typeId }) => typeId === 'App::PropertyColor') if (color?.support !== 'native-editable-codec' || color.recordCount !== 8 || color.objectTypeCount !== 8 || color.statusNames?.length !== 0) fail('App::PropertyColor promotion is missing') const colorList = report.types.find(({ typeId }) => typeId === 'App::PropertyColorList') if (colorList?.support !== 'native-editable-codec' || colorList.recordCount !== 2 || colorList.objectTypeCount !== 2 || colorList.statusNames?.length !== 0) fail('App::PropertyColorList promotion is missing') const direction = report.types.find(({ typeId }) => typeId === 'App::PropertyDirection') if (direction?.support !== 'native-editable-codec' || direction.recordCount !== 2 || direction.objectTypeCount !== 2 || direction.statusNames?.length !== 0) fail('App::PropertyDirection promotion is missing') const file = report.types.find(({ typeId }) => typeId === 'App::PropertyFile') if (file?.support !== 'native-editable-codec' || file.recordCount !== 16 || file.objectTypeCount !== 12 || file.statusNames?.length !== 0) fail('App::PropertyFile promotion is missing') const fileIncluded = report.types.find(({ typeId }) => typeId === 'App::PropertyFileIncluded') if (fileIncluded?.support !== 'native-editable-codec' || fileIncluded.recordCount !== 20 || fileIncluded.objectTypeCount !== 15 || fileIncluded.statusNames?.join(',') !== 'PropOutput,ReadOnly') fail('App::PropertyFileIncluded promotion is missing') const font = report.types.find(({ typeId }) => typeId === 'App::PropertyFont') if (font?.support !== 'native-editable-codec' || font.recordCount !== 2 || font.objectTypeCount !== 2 || font.statusNames?.length !== 0) fail('App::PropertyFont promotion is missing') const force = report.types.find(({ typeId }) => typeId === 'App::PropertyForce') if (force?.support !== 'native-editable-codec' || force.recordCount !== 4 || force.objectTypeCount !== 2 || force.statusNames?.join(',') !== 'PropOutput') fail('App::PropertyForce promotion is missing') const heatFlux = report.types.find(({ typeId }) => typeId === 'App::PropertyHeatFlux') if (heatFlux?.support !== 'native-editable-codec' || heatFlux.recordCount !== 1 || heatFlux.objectTypeCount !== 1 || heatFlux.statusNames?.length !== 0) fail('App::PropertyHeatFlux promotion is missing') const integerSet = report.types.find(({ typeId }) => typeId === 'App::PropertyIntegerSet') if (integerSet?.support !== 'native-editable-codec' || integerSet.recordCount !== 2 || integerSet.objectTypeCount !== 2 || integerSet.statusNames?.length !== 0) fail('App::PropertyIntegerSet promotion is missing') if (report.propertyStatus?.unknownNumericBits?.length !== 0 || report.propertyStatus.proxyOnlyRecordCount !== 0 || report.propertyStatus.proxyOnly?.length !== 0 || report.propertyStatus.facadeNative?.join(',') !== report.propertyStatus.observed?.join(',')) fail('property status representation coverage is stale') if (report.propertyStatus.behaviorNative?.join(',') !== 'Hidden,Immutable,LockDynamic,NoModify,Ordered,Output,PartialTrigger,PropHidden,PropNoPersist,PropNoRecompute,PropOutput,PropReadOnly,PropTransient,ReadOnly,Transient' || report.propertyStatus.preservedOnly?.length !== 0 || report.propertyStatus.preservedOnlyRecordCount !== 0) fail('property status behavior boundary is stale') for (const locked of [report.source, report.harness]) { const path = resolve(root, locked?.path ?? '') const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)]) if (bytes !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`report is stale for ${locked.path}`) } console.log(JSON.stringify({ status: 'freecad-native-property-semantics-pass', propertyTypes: 85, propertyRecords: 5510, nativeCodecRecords: 5053, opaqueProxyRecords: 457, promotedTypes: [acceleration.typeId, area.typeId, boolList.typeId, color.typeId, colorList.typeId, direction.typeId, file.typeId, fileIncluded.typeId, font.typeId, force.typeId, heatFlux.typeId, integerSet.typeId], representedStatusRecords: 5510, preservedOnlyStatusRecords: 0, exactPromotionReady: false, exactBlocker: report.exactBlocker }, null, 2))