import * as engineDossierExports from './engines'; import * as gearboxAndDriveDossierExports from './gearboxesAndDrives'; import * as mechanismDossierExports from './mechanisms'; import * as pumpAndFluidDossierExports from './pumpsAndFluid'; import * as structuralOtherDossierExports from './structuralOther'; import type { MachineDossier } from './types'; export type DossierSourceName = | 'engines' | 'gearboxesAndDrives' | 'pumpsAndFluid' | 'mechanisms' | 'structuralOther'; interface DossierModule { readonly source: DossierSourceName; readonly exports: Record; } interface CollectedDossier { readonly dossier: MachineDossier; readonly source: DossierSourceName; } export interface CoreMachineDossierCoverageReport { readonly expectedIds: readonly string[]; readonly actualIds: readonly string[]; readonly missingIds: readonly string[]; readonly extraIds: readonly string[]; readonly duplicateIds: readonly string[]; readonly complete: boolean; } const DOSSIER_MODULES: readonly DossierModule[] = [ { source: 'engines', exports: engineDossierExports as unknown as Record, }, { source: 'gearboxesAndDrives', exports: gearboxAndDriveDossierExports as unknown as Record, }, { source: 'pumpsAndFluid', exports: pumpAndFluidDossierExports as unknown as Record, }, { source: 'mechanisms', exports: mechanismDossierExports as unknown as Record, }, { source: 'structuralOther', exports: structuralOtherDossierExports as unknown as Record, }, ]; const MACHINE_ID_KEYS = ['machineId', 'machine', 'id'] as const; const DOSSIER_SIGNAL_KEYS = [ 'title', 'name', 'summary', 'description', 'overview', 'category', 'difficulty', 'sections', 'facts', 'engineeringFacts', 'componentHierarchy', 'components', 'parts', 'partDetails', 'relatedMachines', 'guidedTour', 'tour', 'cameraPresets', 'assetReplacementPoints', 'replacementPoints', ] as const; function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; } function readStringProperty( record: Record, keys: readonly string[], ): string | undefined { for (const key of keys) { const value = record[key]; if (typeof value === 'string' && value.trim().length > 0) { return value; } } return undefined; } function countDossierSignals(record: Record): number { return DOSSIER_SIGNAL_KEYS.reduce((count, key) => count + (key in record ? 1 : 0), 0); } function looksLikeMachineDossier(value: unknown): value is MachineDossier { if (!isRecord(value)) { return false; } const machineId = readStringProperty(value, ['machineId', 'machine']); const id = readStringProperty(value, ['id']); const signalCount = countDossierSignals(value); if (machineId) { return signalCount >= 2; } if (!id) { return false; } const hasTitle = typeof value.title === 'string' || typeof value.name === 'string'; const hasMachineLevelSignal = 'category' in value || 'difficulty' in value || 'sections' in value || 'facts' in value || 'engineeringFacts' in value || 'componentHierarchy' in value || 'parts' in value || 'guidedTour' in value || 'tour' in value || 'cameraPresets' in value || 'relatedMachines' in value; return hasTitle && hasMachineLevelSignal && signalCount >= 3; } function readDossierMachineId(dossier: MachineDossier): string | undefined { if (!isRecord(dossier)) { return undefined; } return readStringProperty(dossier, MACHINE_ID_KEYS); } export function getDossierMachineId(dossier: MachineDossier): string { const machineId = readDossierMachineId(dossier); if (!machineId) { throw new Error( 'Machine dossier is missing a machineId/id string. Check the dossier factory output.', ); } return machineId; } function collectDossiersFromValue( value: unknown, source: DossierSourceName, output: CollectedDossier[], seen: Set, ): void { if (!isRecord(value)) { return; } if (seen.has(value)) { return; } seen.add(value); if (looksLikeMachineDossier(value)) { output.push({ dossier: value, source }); return; } if (Array.isArray(value)) { for (const child of value) { collectDossiersFromValue(child, source, output, seen); } return; } for (const child of Object.values(value)) { collectDossiersFromValue(child, source, output, seen); } } function collectAllDossiers(): readonly CollectedDossier[] { const seen = new Set(); const dossiers: CollectedDossier[] = []; for (const dossierModule of DOSSIER_MODULES) { collectDossiersFromValue(dossierModule.exports, dossierModule.source, dossiers, seen); } return dossiers; } const collectedDossiers = collectAllDossiers(); const mutableLookup: Record = {}; const mutableSources: Record = {}; const mutableDuplicateIds = new Set(); for (const collectedDossier of collectedDossiers) { const machineId = readDossierMachineId(collectedDossier.dossier); if (!machineId) { continue; } if (Object.prototype.hasOwnProperty.call(mutableLookup, machineId)) { if (mutableSources[machineId] !== collectedDossier.source) { mutableDuplicateIds.add(machineId); } continue; } mutableLookup[machineId] = collectedDossier.dossier; mutableSources[machineId] = collectedDossier.source; } export const coreMachineDossierLookup: Readonly> = Object.freeze(mutableLookup); export const coreMachineDossierSources: Readonly> = Object.freeze(mutableSources); export const duplicateCoreMachineDossierIds: readonly string[] = Object.freeze( [...mutableDuplicateIds].sort(), ); export const coreMachineDossierIds: readonly string[] = Object.freeze( Object.keys(coreMachineDossierLookup), ); export const coreMachineDossiers: readonly MachineDossier[] = Object.freeze( coreMachineDossierIds.map((machineId) => coreMachineDossierLookup[machineId]), ); export function hasCoreMachineDossier(machineId: string): boolean { return Object.prototype.hasOwnProperty.call(coreMachineDossierLookup, machineId); } export function getCoreMachineDossier(machineId: string): MachineDossier | undefined { return coreMachineDossierLookup[machineId]; } export function requireCoreMachineDossier(machineId: string): MachineDossier { const dossier = getCoreMachineDossier(machineId); if (!dossier) { const knownIds = coreMachineDossierIds.length > 0 ? coreMachineDossierIds.join(', ') : 'none'; throw new Error( `No learning dossier is registered for machine "${machineId}". Registered dossier IDs: ${knownIds}.`, ); } return dossier; } export function listCoreMachineDossiers(machineIds?: readonly string[]): readonly MachineDossier[] { if (!machineIds) { return coreMachineDossiers; } return machineIds.map((machineId) => requireCoreMachineDossier(machineId)); } export function auditCoreMachineDossierCoverage( expectedMachineIds: readonly string[], ): CoreMachineDossierCoverageReport { const expectedSet = new Set(expectedMachineIds); const actualSet = new Set(coreMachineDossierIds); const missingIds = expectedMachineIds.filter((machineId) => !actualSet.has(machineId)); const extraIds = coreMachineDossierIds.filter((machineId) => !expectedSet.has(machineId)); return { expectedIds: Object.freeze([...expectedMachineIds]), actualIds: coreMachineDossierIds, missingIds: Object.freeze(missingIds), extraIds: Object.freeze(extraIds), duplicateIds: duplicateCoreMachineDossierIds, complete: missingIds.length === 0 && extraIds.length === 0 && duplicateCoreMachineDossierIds.length === 0, }; } export function assertCoreMachineDossierCoverage( expectedMachineIds: readonly string[], ): CoreMachineDossierCoverageReport { const report = auditCoreMachineDossierCoverage(expectedMachineIds); if (!report.complete) { const problems = [ report.missingIds.length > 0 ? `missing: ${report.missingIds.join(', ')}` : '', report.extraIds.length > 0 ? `extra: ${report.extraIds.join(', ')}` : '', report.duplicateIds.length > 0 ? `duplicates: ${report.duplicateIds.join(', ')}` : '', ] .filter(Boolean) .join('; '); throw new Error(`Core machine dossier coverage is incomplete (${problems}).`); } return report; } export const machineDossiers = coreMachineDossiers; export const machineDossierById = coreMachineDossierLookup; export const getMachineDossier = getCoreMachineDossier; export const requireMachineDossier = requireCoreMachineDossier; export const hasMachineDossier = hasCoreMachineDossier; export const coreMachineDossierRegistry = Object.freeze({ ids: coreMachineDossierIds, all: coreMachineDossiers, byId: coreMachineDossierLookup, sources: coreMachineDossierSources, duplicateIds: duplicateCoreMachineDossierIds, has: hasCoreMachineDossier, get: getCoreMachineDossier, require: requireCoreMachineDossier, list: listCoreMachineDossiers, auditCoverage: auditCoreMachineDossierCoverage, assertCoverage: assertCoreMachineDossierCoverage, }); export default coreMachineDossierRegistry;