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 reportPath = resolve(root, process.env.FREECAD_GUI_COMMAND_REPORT || '.cache/freecad/reference-desktop-gui-commands.json') const outputPath = resolve(root, process.env.FREECAD_GUI_COMMAND_INVENTORY || 'config/freecad-gui-command-inventory.json') const sourceInventory = JSON.parse(await readFile(resolve(root, 'config/freecad-source-inventory.json'), 'utf8')) const reportBytes = await readFile(reportPath) const report = JSON.parse(reportBytes) if (report.baselineId !== 'freecad-1.1.1' || report.freecadVersion !== '1.1.1' || report.guiUp !== true) throw new Error('A verified desktop FreeCAD GUI report is required.') if (report.probeScope !== 'gui-commands') throw new Error('The dedicated gui-commands oracle report is required.') if (!report.guiCommands?.available || !Array.isArray(report.guiCommands.commands)) throw new Error('Desktop report does not contain GUI command observations.') const sourceCommandModules = new Map() for (const module of sourceInventory.modules) for (const commandId of module.commandIds) { const modules = sourceCommandModules.get(commandId) || [] modules.push(module.name) sourceCommandModules.set(commandId, modules) } const normalizeState = (state) => typeof state === 'boolean' ? state : state && typeof state === 'object' ? 'error' : 'no-action' const unique = (values) => [...new Set(values)].sort() const commands = report.guiCommands.commands.map((command) => { const observations = command.observations || [] const noDocumentStates = unique(observations.map((entry) => normalizeState(entry.noDocument))) const documentNoSelectionStates = unique(observations.map((entry) => normalizeState(entry.documentNoSelection))) const selectedStates = unique(observations.map((entry) => normalizeState(entry.selectedPartBox))) const documentSensitive = observations.some((entry) => typeof entry.noDocument === 'boolean' && typeof entry.documentNoSelection === 'boolean' && entry.noDocument !== entry.documentNoSelection) const selectionSensitive = observations.some((entry) => typeof entry.documentNoSelection === 'boolean' && typeof entry.selectedPartBox === 'boolean' && entry.documentNoSelection !== entry.selectedPartBox) return { id: command.id, sourceModules: unique(sourceCommandModules.get(command.id) || ['GuiCore']), workbenches: unique(command.registeredWorkbenches || observations.map((entry) => entry.workbench)), cases: { noDocument: { states: noDocumentStates, observed: true }, documentNoSelection: { states: documentNoSelectionStates, observed: true }, selectedPartBox: { states: selectedStates, observed: true }, }, documentSensitive, selectionSensitive, status: 'runtime-probed', } }).sort((a, b) => a.id.localeCompare(b.id)) const sourceOnlyCommands = [...sourceCommandModules.entries()] .filter(([id]) => !commands.some((command) => command.id === id)) .map(([id, modules]) => ({ id, sourceModules: unique(modules), reason: modules.some((module) => ['Cloud', 'JtReader', 'Sandbox', 'TemplatePyMod'].includes(module)) ? 'module-not-built' : 'source-registration-not-loaded', })) .sort((a, b) => a.id.localeCompare(b.id)) const inventory = { schemaVersion: 1, baseline: { freecadVersion: '1.1.1', commit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' }, generatedBy: 'scripts/generate-freecad-gui-command-inventory.mjs', sourceInventory: 'config/freecad-source-inventory.json', contextBoundaries: 'config/freecad-gui-command-context-boundaries.json', runtimeEvidence: { report: '.cache/freecad/reference-desktop-gui-commands.json', reportSha256: createHash('sha256').update(reportBytes).digest('hex'), workbenchCount: report.guiCommands.workbenches.length, stateProbe: report.guiCommands.stateProbe, stateCases: report.guiCommands.stateCases, directIsActiveBoundary: report.guiCommands.directIsActiveBoundary, }, commandCount: commands.length, commands, sourceOnlyCommands, } await writeFile(outputPath, `${JSON.stringify(inventory, null, 2)}\n`, 'utf8') console.log(JSON.stringify({ status: 'gui-command-inventory-generated', output: outputPath, commandCount: commands.length, sourceOnlyCount: sourceOnlyCommands.length }, null, 2))