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 runtimePath = resolve(root, '.cache/freecad/reference-desktop.json') const reportPath = resolve(root, 'config/freecad-property-direction-inventory.json') const fail = (message) => { throw new Error(`FreeCAD PropertyDirection inventory check failed: ${message}`) } const [runtimeContent, reportContent] = await Promise.all([readFile(runtimePath), readFile(reportPath)]) const runtime = JSON.parse(runtimeContent) const report = JSON.parse(reportContent) if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyDirection' || 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.recordCount !== 2 || !Array.isArray(report.records) || report.records.length !== 2) fail('record count is not exactly two') if (report.provenance?.runtime?.path !== '.cache/freecad/reference-desktop.json' || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale') const storage = report.storage if (storage?.inheritedFrom !== 'App::PropertyVector' || storage.xmlElement !== 'PropertyVector' || storage.attributes?.join(',') !== 'valueX,valueY,valueZ' || storage.scalarEncoding !== 'decimal-double' || storage.externalResource !== false) fail('native storage contract is incomplete') const expected = { 'Part::Mirroring.Normal': { group: 'Plane', dependencyNames: 'Source,Base,MirrorPlane', consumer: 'gp_Ax2(gp_Pnt(Base), gp_Dir(Normal))' }, 'Part::ProjectOnSurface.Direction': { group: 'Projection', dependencyNames: 'SupportFace,Projection', consumer: 'gp_Dir(Direction)' }, } for (const [identity, contract] of Object.entries(expected)) { const [objectTypeId, propertyName] = identity.split(/\.(?=[^.]+$)/) const record = report.records.find((candidate) => candidate.objectTypeId === objectTypeId && candidate.propertyName === propertyName) if (!record || record.objectAvailable !== true || record.probeStatus !== 'available' || record.group !== contract.group || record.status?.length !== 0 || record.defaultDisplayValue !== 'Vector (0.0, 0.0, 1.0)' || JSON.stringify(record.defaultValue) !== '[0,0,1]') fail(`native inventory changed for ${identity}`) const model = record.valueModel if (model?.kind !== 'three-component-direction-vector' || model.representation !== 'Base::Vector3d' || model.normalizationAtPropertyBoundary !== 'none' || model.unit !== 'Length' || model.writable !== true) fail(`value model changed for ${identity}`) if (record.inputs?.accepted?.join(',') !== 'Base.Vector,tuple-of-three-float-or-integer-components' || record.inputs.listAccepted !== false || record.inputs.componentPaths?.join(',') !== 'x,y,z' || record.editor !== 'Gui::PropertyEditor::PropertyDirectionItem') fail(`setter or editor contract changed for ${identity}`) if (record.dependencies?.map(({ propertyName: name }) => name).join(',') !== contract.dependencyNames || record.applicability?.requiredObjectTypeId !== objectTypeId || record.applicability.source !== 'Document.supportedTypes runtime inventory' || record.applicability.propertyWriteRequiresShape !== false || record.applicability.hostExecutionRequiresShapeInputs !== true || record.applicability.nonZeroRequiredByConsumer !== true || record.applicability.consumer !== contract.consumer) fail(`dependencies or applicability changed for ${identity}`) } const mirror = report.records.find(({ objectTypeId }) => objectTypeId === 'Part::Mirroring') const mirrorPlane = mirror?.dependencies?.find(({ propertyName }) => propertyName === 'MirrorPlane') if (mirrorPlane?.role !== 'optional-plane-override' || mirrorPlane.requiredForExecution !== false || mirrorPlane.overrides?.join(',') !== 'Base,Normal' || mirrorPlane.overriddenStatus !== 'ReadOnly') fail('Mirroring plane override contract is incomplete') const projection = report.records.find(({ objectTypeId }) => objectTypeId === 'Part::ProjectOnSurface') if (projection?.dependencies?.some(({ requiredForExecution }) => requiredForExecution !== true)) fail('ProjectOnSurface dependencies are not locked') const nativeMatches = [] for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyDirection') nativeMatches.push({ objectTypeId: objectType.typeId, propertyName: property.name, group: property.group, status: property.status, defaultValue: property.default }) if (nativeMatches.length !== 2) fail('locked runtime oracle no longer contains two PropertyDirection records') for (const locked of report.provenance?.sources ?? []) { const content = await readFile(resolve(root, locked?.path ?? '')) if (content.length !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`source provenance is stale for ${locked.path}`) } const [propertyHeader, propertySource, editorHeader, editorSource, mirrorSource, projectionSource] = await Promise.all([ readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/PropertyGeo.h'), 'utf8'), readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/PropertyGeo.cpp'), 'utf8'), readFile(resolve(root, '.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.h'), 'utf8'), readFile(resolve(root, '.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp'), 'utf8'), readFile(resolve(root, '.cache/freecad/FreeCAD/src/Mod/Part/App/FeatureMirroring.cpp'), 'utf8'), readFile(resolve(root, '.cache/freecad/FreeCAD/src/Mod/Part/App/FeatureProjectOnSurface.cpp'), 'utf8'), ]) if (!propertyHeader.includes('class AppExport PropertyDirection: public PropertyVector') || !propertyHeader.includes('return Base::Unit::Length;') || !propertyHeader.includes('Gui::PropertyEditor::PropertyDirectionItem')) fail('locked PropertyDirection declaration changed') if (!propertySource.includes('TYPESYSTEM_SOURCE(App::PropertyDirection, App::PropertyVector)') || !propertySource.includes('PyObject_TypeCheck(value, &(Base::VectorPy::Type))') || !propertySource.includes('PyTuple_Check(value) && PyTuple_Size(value) == 3') || !propertySource.includes('PyFloat_Check(item)') || !propertySource.includes('PyLong_Check(item)') || !propertySource.includes(' valueModel.writable).length, defaultValue: report.records[0].defaultValue, storage: report.storage, classification: report.classification }, null, 2))