import { machineRegistry } from './registry'; import { MACHINE_CATEGORIES, MACHINE_DIFFICULTIES, isMachineCategory, isMachineDifficulty, isMachineId, } from './schema'; import type { MachineDefinition } from './schema'; export type ValidationSeverity = 'error' | 'warning'; export interface MachineRegistryValidationIssue { severity: ValidationSeverity; machineId?: string; field: string; message: string; } export interface MachineRegistryValidationResult { valid: boolean; errors: readonly MachineRegistryValidationIssue[]; warnings: readonly MachineRegistryValidationIssue[]; machineCount: number; partCount: number; } const issue = ( severity: ValidationSeverity, field: string, message: string, machineId?: string, ): MachineRegistryValidationIssue => ({ severity, field, message, machineId, }); const hasDuplicateValues = (values: readonly string[]): boolean => new Set(values).size !== values.length; export const validateMachineDefinition = ( machine: MachineDefinition, knownMachineIds: ReadonlySet = new Set(machineRegistry.map((entry) => entry.id)), ): readonly MachineRegistryValidationIssue[] => { const issues: MachineRegistryValidationIssue[] = []; const partIds = machine.parts.map((part) => part.id); const partIdSet = new Set(partIds); if (!isMachineId(machine.id)) { issues.push(issue('error', 'id', 'Machine id must be a non-empty string.', machine.id)); } if (!isMachineCategory(machine.category)) { issues.push( issue( 'error', 'category', `Category must be one of: ${MACHINE_CATEGORIES.join(', ')}.`, machine.id, ), ); } if (!isMachineDifficulty(machine.difficulty)) { issues.push( issue( 'error', 'difficulty', `Difficulty must be one of: ${MACHINE_DIFFICULTIES.join(', ')}.`, machine.id, ), ); } if (machine.complexity < 1 || machine.complexity > 10) { issues.push(issue('error', 'complexity', 'Complexity must be between 1 and 10.', machine.id)); } if (machine.parts.length < 3) { issues.push(issue('error', 'parts', 'Every machine must define at least three selectable parts.', machine.id)); } if (hasDuplicateValues(partIds)) { issues.push(issue('error', 'parts', 'Part ids must be unique within a machine.', machine.id)); } machine.parts.forEach((part) => { if (!part.name.trim()) { issues.push(issue('error', `parts.${part.id}.name`, 'Part name must be present.', machine.id)); } if (!part.description.trim()) { issues.push(issue('error', `parts.${part.id}.description`, 'Part description must be present.', machine.id)); } if (part.defaultOpacity < 0 || part.defaultOpacity > 1) { issues.push(issue('error', `parts.${part.id}.defaultOpacity`, 'Part opacity must be between 0 and 1.', machine.id)); } }); machine.animation.drivenParts.forEach((partId) => { if (!partIdSet.has(partId)) { issues.push( issue('error', 'animation.drivenParts', `Animation references unknown part "${partId}".`, machine.id), ); } }); machine.guidedTour.forEach((step) => { if (step.focusPartIds.length === 0) { issues.push(issue('warning', `guidedTour.${step.id}`, 'Guided tour step has no focused parts.', machine.id)); } step.focusPartIds.forEach((partId) => { if (!partIdSet.has(partId)) { issues.push( issue('error', `guidedTour.${step.id}.focusPartIds`, `Tour references unknown part "${partId}".`, machine.id), ); } }); }); machine.related.forEach((relatedId) => { if (relatedId === machine.id) { issues.push(issue('error', 'related', 'Machine cannot list itself as related.', machine.id)); } if (!knownMachineIds.has(relatedId)) { issues.push(issue('warning', 'related', `Related machine "${relatedId}" is not currently registered.`, machine.id)); } }); if (machine.facts.length < 4) { issues.push(issue('warning', 'facts', 'Machine should include at least four engineering facts.', machine.id)); } if (machine.asset.status === 'shipped' && !machine.asset.url) { issues.push(issue('error', 'asset.url', 'Shipped GLTF assets must include a URL.', machine.id)); } return issues; }; export const validateMachineRegistry = ( registry: readonly MachineDefinition[] = machineRegistry, ): MachineRegistryValidationResult => { const issues: MachineRegistryValidationIssue[] = []; const machineIds = registry.map((machine) => machine.id); const knownMachineIds = new Set(machineIds); if (hasDuplicateValues(machineIds)) { issues.push(issue('error', 'registry', 'Machine ids must be unique across the registry.')); } registry.forEach((machine) => { issues.push(...validateMachineDefinition(machine, knownMachineIds)); }); const errors = issues.filter((entry) => entry.severity === 'error'); const warnings = issues.filter((entry) => entry.severity === 'warning'); return { valid: errors.length === 0, errors, warnings, machineCount: registry.length, partCount: registry.reduce((total, machine) => total + machine.parts.length, 0), }; }; export const assertValidMachineRegistry = (registry: readonly MachineDefinition[] = machineRegistry): void => { const result = validateMachineRegistry(registry); if (!result.valid) { const message = result.errors .map((entry) => `${entry.machineId ? `${entry.machineId}: ` : ''}${entry.field} - ${entry.message}`) .join('\n'); throw new Error(`Mechanica machine registry validation failed:\n${message}`); } }; export const foundationRegistryValidation = validateMachineRegistry();