Files
Web_FreeCAD_Bitbybit/scripts/check-freecad-property-direction-inventory.mjs
wangdequan d11566403d
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: close ordered pairs and property codec batches
2026-08-17 04:58:46 -04:00

60 lines
8.0 KiB
JavaScript

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('<PropertyVector') || !propertySource.includes('valueX=') || !propertySource.includes('valueY=') || !propertySource.includes('valueZ=')) fail('locked PropertyDirection setter/storage inheritance changed')
if (!editorHeader.includes('class GuiExport PropertyDirectionItem: public PropertyVectorDistanceItem') || !editorSource.includes('PROPERTYITEM_SOURCE(Gui::PropertyEditor::PropertyDirectionItem)')) fail('locked PropertyDirection editor changed')
if (!mirrorSource.includes('ADD_PROPERTY_TYPE(Normal, (Base::Vector3d(0, 0, 1))') || !mirrorSource.includes('Base::Vector3d norm = Normal.getValue()') || !mirrorSource.includes('gp_Ax2 ax2(gp_Pnt(base.x, base.y, base.z), gp_Dir(norm.x, norm.y, norm.z))') || !mirrorSource.includes('Normal.setStatus(App::Property::ReadOnly, true)')) fail('locked Mirroring consumer contract changed')
if (!projectionSource.includes('ADD_PROPERTY_TYPE(\n Direction,') || !projectionSource.includes('const auto& vec = Direction.getValue()') || !projectionSource.includes('gp_Dir dir(vec.x, vec.y, vec.z)') || !projectionSource.includes('No support face specified') || !projectionSource.includes('getProjectionShapes()')) fail('locked ProjectOnSurface consumer contract changed')
console.log(JSON.stringify({ status: 'freecad-property-direction-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, writableRecords: report.records.filter(({ valueModel }) => valueModel.writable).length, defaultValue: report.records[0].defaultValue, storage: report.storage, classification: report.classification }, null, 2))