import { createHash } from 'node:crypto' import { isDeepStrictEqual } from 'node:util' import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' const root = resolve(new URL('..', import.meta.url).pathname) const runtimePath = '.cache/freecad/reference-desktop.json' const reportPath = 'config/freecad-property-fileincluded-inventory.json' const fail = (message) => { throw new Error(`FreeCAD PropertyFileIncluded inventory check failed: ${message}`) } const [runtimeContent, reportContent] = await Promise.all([ readFile(resolve(root, runtimePath)), readFile(resolve(root, reportPath)), ]) const runtime = JSON.parse(runtimeContent) const report = JSON.parse(reportContent) if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyFileIncluded' || report.classification !== 'opaque-fcstd-proxy' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('report boundary is invalid') if (report.recordCount !== 20 || report.objectTypeCount !== 15 || report.records?.length !== 20) fail('runtime record or host count changed') if (report.provenance?.runtime?.path !== runtimePath || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale') 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 normalizeStatus = (status) => status.map((entry) => entry === 27 ? 'PropOutput' : String(entry)) const nativeMatches = [] for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) { if (property.typeId !== 'App::PropertyFileIncluded') continue nativeMatches.push({ objectTypeId: objectType.typeId, objectAvailable: objectType.available === true, probeStatus: objectType.probeStatus, propertyName: property.name, group: property.group, status: property.status, normalizedStatus: normalizeStatus(property.status), defaultRaw: property.default, }) } const identity = ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, status, normalizedStatus, defaultRaw }) => ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, status, normalizedStatus, defaultRaw }) const sortRecords = (records) => records.map(identity).sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`)) if (nativeMatches.length !== 20 || !isDeepStrictEqual(sortRecords(nativeMatches), sortRecords(report.records))) fail('report diverges from the locked runtime oracle') const expectedKeys = [ 'App::DocumentObjectFileIncluded.File', 'App::VRMLObject.VrmlFile', 'Image::ImagePlane.ImageFile', 'Robot::RobotObject.RobotKinematicFile', 'Robot::RobotObject.RobotVrmlFile', 'Sketcher::SketchObjectSF.SketchFlatFile', 'TechDraw::DrawComplexSection.PatIncluded', 'TechDraw::DrawComplexSection.SvgIncluded', 'TechDraw::DrawComplexSectionPython.PatIncluded', 'TechDraw::DrawComplexSectionPython.SvgIncluded', 'TechDraw::DrawGeomHatch.PatIncluded', 'TechDraw::DrawHatch.SvgIncluded', 'TechDraw::DrawSVGTemplate.PageResult', 'TechDraw::DrawTileWeld.SymbolIncluded', 'TechDraw::DrawTileWeldPython.SymbolIncluded', 'TechDraw::DrawViewImage.ImageIncluded', 'TechDraw::DrawViewSection.PatIncluded', 'TechDraw::DrawViewSection.SvgIncluded', 'TechDraw::DrawViewSectionPython.PatIncluded', 'TechDraw::DrawViewSectionPython.SvgIncluded', ] if (report.records.map(({ objectTypeId, propertyName }) => `${objectTypeId}.${propertyName}`).join('|') !== expectedKeys.join('|')) fail('host/property inventory changed') if (report.records.some((record) => record.objectAvailable !== true || record.probeStatus !== 'available' || record.valueModel?.kind !== 'included-file-resource' || record.valueModel.representation !== 'read-only document-transient file backed by embedded FCStd bytes' || record.valueModel.pathIsRuntimeOnly !== true || record.valueModel.bytesAreAuthoritative !== true || record.inputs?.sourceFileMustExist !== true || record.inputs.emptyStringBehavior !== 'accepted-no-op; does not clear an existing value' || record.inputs.sameTransientPathBehavior !== 'rejected' || record.editor !== 'Gui::PropertyEditor::PropertyTransientFileItem' || record.applicability?.requiredObjectTypeId !== record.objectTypeId || record.applicability.propertyWriteRequiresShape !== false)) fail('per-record value, input, editor or applicability contract is incomplete') if (report.records.filter(({ valueModel }) => valueModel.writableByStatus).length !== 11 || report.records.filter(({ status }) => isDeepStrictEqual(status, ['ReadOnly'])).length !== 8 || report.records.filter(({ status }) => isDeepStrictEqual(status, [27])).length !== 1 || report.records.filter(({ defaultKind }) => defaultKind === 'empty').length !== 8 || report.records.filter(({ defaultKind }) => defaultKind === 'document-transient-copy').length !== 12) fail('status/default partition changed') const dependencyPairs = report.records.filter(({ dependency }) => dependency !== null).map(({ objectTypeId, propertyName, dependency }) => `${objectTypeId}.${propertyName}<-${dependency.sourceProperty}`) const expectedDependencyPairs = [ 'TechDraw::DrawComplexSection.PatIncluded<-FileGeomPattern', 'TechDraw::DrawComplexSection.SvgIncluded<-FileHatchPattern', 'TechDraw::DrawComplexSectionPython.PatIncluded<-FileGeomPattern', 'TechDraw::DrawComplexSectionPython.SvgIncluded<-FileHatchPattern', 'TechDraw::DrawGeomHatch.PatIncluded<-FilePattern', 'TechDraw::DrawHatch.SvgIncluded<-HatchPattern', 'TechDraw::DrawSVGTemplate.PageResult<-Template', 'TechDraw::DrawTileWeld.SymbolIncluded<-SymbolFile', 'TechDraw::DrawTileWeldPython.SymbolIncluded<-SymbolFile', 'TechDraw::DrawViewImage.ImageIncluded<-ImageFile', 'TechDraw::DrawViewSection.PatIncluded<-FileGeomPattern', 'TechDraw::DrawViewSection.SvgIncluded<-FileHatchPattern', 'TechDraw::DrawViewSectionPython.PatIncluded<-FileGeomPattern', 'TechDraw::DrawViewSectionPython.SvgIncluded<-FileHatchPattern', ] if (!isDeepStrictEqual(dependencyPairs, expectedDependencyPairs) || report.records.filter(({ dependency }) => dependency === null).length !== 6 || report.records.some(({ dependency }) => dependency && dependency.relation !== 'host-onChanged-copies-source-bytes-into-included-property')) fail('derived-source dependency inventory changed') const setter = report.setterSemantics if (setter?.externalSource !== 'copy bytes into the document transient directory' || setter.writableTransientSource !== 'rename into the selected transient destination' || setter.replacement !== 'delete the previous transient copy unless undo owns it' || setter.destinationPermissions !== 'read-only' || setter.archiveNameCollision !== 'append a positive integer before the extension' || setter.undoRedo !== 'Copy/Paste move or copy transient files while preserving per-property ownership') fail('setter and ownership semantics are incomplete') const storage = report.storage if (storage?.xmlElement !== 'FileIncluded' || storage.archiveAttribute !== 'file' || storage.archiveResource !== 'separate FCStd ZIP entry containing byte-identical file data' || storage.forceXmlAttribute !== 'data' || storage.forceXmlResource !== 'binary data embedded in the XML stream' || storage.emptyEncoding !== '' || storage.restoredValue !== 'document transient path; not a portable semantic path' || storage.browserBoundary !== 'project resource identity plus bytes; never expose or persist an ambient host path') fail('FCStd or browser storage boundary is incomplete') const sources = new Map(await Promise.all((report.provenance?.sources ?? []).map(async ({ path }) => [path, await readFile(resolve(root, path), 'utf8')]))) const propertyHeader = sources.get('.cache/freecad/FreeCAD/src/App/PropertyFile.h') ?? '' const propertySource = sources.get('.cache/freecad/FreeCAD/src/App/PropertyFile.cpp') ?? '' const editorHeader = sources.get('.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.h') ?? '' const editorSource = sources.get('.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp') ?? '' if (!propertyHeader.includes('class AppExport PropertyFileIncluded: public Property') || !propertyHeader.includes('return "Gui::PropertyEditor::PropertyTransientFileItem"') || !propertyHeader.includes("it's not allowed to write the file") || !propertySource.includes('TYPESYSTEM_SOURCE(App::PropertyFileIncluded, App::Property)') || !propertySource.includes('Not possible to set the same file!') || !propertySource.includes('str << "File " << file.filePath() << " does not exist."') || !propertySource.includes('PyTuple_Check(value)') || !propertySource.includes('PyDict_Check(value)') || !propertySource.includes('writer.isForceXML()') || !propertySource.includes(' valueModel.writableByStatus).length, readOnlyRecords: 8, outputRecords: 1, directRecords: 6, derivedRecords: 14, storage: report.storage, classification: report.classification }, null, 2))