import { createHash } from 'node:crypto' import { readFile, writeFile } 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 sourcePaths = [ '.cache/freecad/FreeCAD/src/App/PropertyFile.h', '.cache/freecad/FreeCAD/src/App/PropertyFile.cpp', '.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.h', '.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp', '.cache/freecad/FreeCAD/src/App/DocumentObjectFileIncluded.cpp', '.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewSection.cpp', '.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawGeomHatch.cpp', '.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawHatch.cpp', '.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawSVGTemplate.cpp', '.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawTileWeld.cpp', '.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewImage.cpp', ] const outputPath = resolve(root, 'config/freecad-property-fileincluded-inventory.json') const [runtimeContent, ...sourceContents] = await Promise.all([ readFile(resolve(root, runtimePath)), ...sourcePaths.map((path) => readFile(resolve(root, path))), ]) const runtime = JSON.parse(runtimeContent) const fail = (message) => { throw new Error(`FreeCAD PropertyFileIncluded inventory generation failed: ${message}`) } if (runtime.schemaVersion !== 1 || runtime.freecadVersion !== '1.1.1' || runtime.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline') const dependencySources = new Map([ ['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'], ]) const normalizeStatus = (status) => status.map((entry) => entry === 27 ? 'PropOutput' : String(entry)) const records = [] for (const objectType of runtime.runtimeObjects?.types ?? []) { for (const property of objectType.properties ?? []) { if (property.typeId !== 'App::PropertyFileIncluded') continue const key = `${objectType.typeId}.${property.name}` const sourceProperty = dependencySources.get(key) records.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, defaultKind: property.default === '' ? 'empty' : 'document-transient-copy', valueModel: { kind: 'included-file-resource', representation: 'read-only document-transient file backed by embedded FCStd bytes', writableByStatus: property.status.length === 0, pathIsRuntimeOnly: true, bytesAreAuthoritative: true, }, inputs: { accepted: ['utf8-string-path', 'bytes-path', 'open-io-file', '[source-path,archive-name]', '{filename:string,filter?:string}'], sourceFileMustExist: true, emptyStringBehavior: 'accepted-no-op; does not clear an existing value', sameTransientPathBehavior: 'rejected', }, dependency: sourceProperty ? { sourceProperty, relation: 'host-onChanged-copies-source-bytes-into-included-property', } : null, editor: 'Gui::PropertyEditor::PropertyTransientFileItem', applicability: { requiredObjectTypeId: objectType.typeId, propertyWriteRequiresShape: false, source: 'Document.supportedTypes runtime inventory', }, }) } } records.sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`)) if (records.length !== 20) fail(`expected 20 native App::PropertyFileIncluded records, found ${records.length}`) if (new Set(records.map(({ objectTypeId }) => objectTypeId)).size !== 15) fail('expected 15 native host object types') if (records.filter(({ valueModel }) => valueModel.writableByStatus).length !== 11 || records.filter(({ dependency }) => dependency !== null).length !== 14) fail('status or dependency partition changed') const report = { schemaVersion: 1, status: 'pass', baseline: { freecadVersion: runtime.freecadVersion, commit: runtime.gitCommit }, propertyType: 'App::PropertyFileIncluded', recordCount: records.length, objectTypeCount: new Set(records.map(({ objectTypeId }) => objectTypeId)).size, records, setterSemantics: { externalSource: 'copy bytes into the document transient directory', writableTransientSource: 'rename into the selected transient destination', replacement: 'delete the previous transient copy unless undo owns it', destinationPermissions: 'read-only', archiveNameCollision: 'append a positive integer before the extension', undoRedo: 'Copy/Paste move or copy transient files while preserving per-property ownership', }, storage: { xmlElement: 'FileIncluded', archiveAttribute: 'file', archiveResource: 'separate FCStd ZIP entry containing byte-identical file data', forceXmlAttribute: 'data', forceXmlResource: 'binary data embedded in the XML stream', emptyEncoding: '', restoredValue: 'document transient path; not a portable semantic path', browserBoundary: 'project resource identity plus bytes; never expose or persist an ambient host path', }, provenance: { runtime: { path: runtimePath, bytes: runtimeContent.length, sha256: createHash('sha256').update(runtimeContent).digest('hex') }, sources: sourcePaths.map((path, index) => ({ path, bytes: sourceContents[index].length, sha256: createHash('sha256').update(sourceContents[index]).digest('hex') })), }, classification: 'opaque-fcstd-proxy', nextPhases: ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'], } await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) console.log(JSON.stringify({ status: 'freecad-property-fileincluded-inventory-generated', output: 'config/freecad-property-fileincluded-inventory.json', propertyType: report.propertyType, recordCount: report.recordCount, objectTypeCount: report.objectTypeCount, writableByStatus: records.filter(({ valueModel }) => valueModel.writableByStatus).length, derivedRecords: records.filter(({ dependency }) => dependency !== null).length }, null, 2))