import { createHash } from 'node:crypto' import { readFile } 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 reportPath = resolve(root, 'config/freecad-property-integerset-inventory.json') const fail = (message) => { throw new Error(`FreeCAD PropertyIntegerSet inventory check failed: ${message}`) } const [sourceContent, reportContent] = await Promise.all([readFile(sourcePath), readFile(reportPath)]) const source = JSON.parse(sourceContent) const report = JSON.parse(reportContent) if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyIntegerSet' || report.classification !== 'opaque-fcstd-proxy') fail('report boundary is invalid') if (report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is not locked to FreeCAD 1.1.1') if (report.source?.path !== '.cache/freecad/reference-desktop.json' || report.source.bytes !== sourceContent.length || report.source.sha256 !== createHash('sha256').update(sourceContent).digest('hex')) fail('source provenance is stale') if (report.recordCount !== 2 || report.writableRecordCount !== 2 || report.records?.length !== 2) fail('record cardinality is invalid') const expected = [ ['Fem::FemSetElementNodesObject', 'Elements', 'Element indexes'], ['Fem::FemSetNodesObject', 'Nodes', 'Node indexes'], ] for (const [objectTypeId, propertyName, group] of expected) { const record = report.records.find((candidate) => candidate.objectTypeId === objectTypeId && candidate.propertyName === propertyName) if (!record || record.objectAvailable !== true || record.probeStatus !== 'available' || record.group !== group || JSON.stringify(record.status) !== '[]' || record.defaultRaw !== 'set()') fail(`${objectTypeId}.${propertyName} inventory changed`) const model = record.valueModel if (model?.kind !== 'integer-set' || model.elementType !== 'signed-integer' || model.cardinality !== 'variable-unique-unordered' || model.webRepresentation !== 'sorted-safe-integer-array' || model.writable !== true) fail(`${objectTypeId}.${propertyName} value model is invalid`) if (record.dependencies?.length !== 0 || record.applicability?.requiredObjectTypeId !== objectTypeId) fail(`${objectTypeId}.${propertyName} applicability is incomplete`) } const nativeMatches = [] for (const objectType of source.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyIntegerSet') nativeMatches.push({ objectTypeId: objectType.typeId, propertyName: property.name, group: property.group, status: property.status, defaultRaw: property.default }) const reportMatches = report.records.map(({ objectTypeId, propertyName, group, status, defaultRaw }) => ({ objectTypeId, propertyName, group, status, defaultRaw })) if (JSON.stringify(nativeMatches.sort((a, b) => `${a.objectTypeId}.${a.propertyName}`.localeCompare(`${b.objectTypeId}.${b.propertyName}`))) !== JSON.stringify(reportMatches)) fail('report does not match the locked runtime oracle') console.log(JSON.stringify({ status: 'freecad-property-integerset-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, writableRecordCount: report.writableRecordCount, objectTypes: expected.map(([typeId]) => typeId), classification: report.classification }, null, 2))