#!/usr/bin/env node import fs from 'node:fs/promises'; import path from 'node:path'; import process from 'node:process'; const DEFAULT_EXPECTED_MACHINE_COUNT = 28; async function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { printHelp(); return; } const projectRoot = await findProjectRoot(process.cwd()); process.chdir(projectRoot); const { machines, dossiers, blueprints, animations, catalogueHelpers, contract, } = await loadCatalogueModules(projectRoot); if (typeof contract.createCoreMachineCatalogueReport !== 'function') { throw new Error('coreMachineContract.ts did not export createCoreMachineCatalogueReport().'); } if (typeof contract.formatCatalogueContractReport !== 'function') { throw new Error('coreMachineContract.ts did not export formatCatalogueContractReport().'); } const report = contract.createCoreMachineCatalogueReport({ machines, dossiers, blueprints, animations, catalogueHelpers, expectedMachineCount: options.expected, }); if (options.json) { console.log(JSON.stringify(report, null, 2)); } else { console.log(contract.formatCatalogueContractReport(report, { includeMachineSummaries: true, maxIssues: options.maxIssues, })); } const warningFailures = options.strict ? report.warnings.length : 0; if (report.errors.length > 0 || warningFailures > 0) { const strictSuffix = options.strict && report.warnings.length > 0 ? ` and ${report.warnings.length} warning(s) because --strict is enabled` : ''; console.error( `\nCore machine contract audit failed with ${report.errors.length} error(s)${strictSuffix}.`, ); process.exitCode = 1; } } async function loadCatalogueModules(projectRoot) { let vite; try { vite = await import('vite'); } catch (error) { throw new Error( [ 'Unable to import Vite, which is used to load the TypeScript catalogue modules for this audit.', 'Install project dependencies first (npm install, pnpm install, or yarn install) and rerun the script.', error instanceof Error ? `Original error: ${error.message}` : '', ].filter(Boolean).join('\n'), ); } const server = await vite.createServer({ root: projectRoot, appType: 'custom', logLevel: 'error', server: { middlewareMode: true, }, }); try { const [ machines, dossiers, blueprints, animations, catalogueHelpers, contract, ] = await Promise.all([ server.ssrLoadModule('/src/modules/machines/coreMachines.ts'), server.ssrLoadModule('/src/modules/machines/dossiers/index.ts'), server.ssrLoadModule('/src/modules/machines/procedural/index.ts'), server.ssrLoadModule('/src/animations/coreMachineAnimations.ts'), optionalSsrLoad(server, '/src/modules/machines/catalogue/catalogueHelpers.ts'), server.ssrLoadModule('/src/modules/machines/quality/coreMachineContract.ts'), ]); return { machines, dossiers, blueprints, animations, catalogueHelpers, contract, }; } finally { await server.close(); } } async function optionalSsrLoad(server, moduleId) { try { return await server.ssrLoadModule(moduleId); } catch { return undefined; } } function parseArgs(argv) { const options = { expected: DEFAULT_EXPECTED_MACHINE_COUNT, json: false, strict: false, help: false, verbose: false, maxIssues: 200, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--help' || arg === '-h') { options.help = true; continue; } if (arg === '--json') { options.json = true; continue; } if (arg === '--strict' || arg === '--warnings-as-errors') { options.strict = true; continue; } if (arg === '--verbose') { options.verbose = true; continue; } if (arg === '--expected') { const value = argv[index + 1]; index += 1; options.expected = parsePositiveInteger(value, '--expected'); continue; } if (arg.startsWith('--expected=')) { options.expected = parsePositiveInteger(arg.slice('--expected='.length), '--expected'); continue; } if (arg === '--max-issues') { const value = argv[index + 1]; index += 1; options.maxIssues = parsePositiveInteger(value, '--max-issues'); continue; } if (arg.startsWith('--max-issues=')) { options.maxIssues = parsePositiveInteger(arg.slice('--max-issues='.length), '--max-issues'); continue; } throw new Error(`Unknown argument: ${arg}`); } return options; } function parsePositiveInteger(value, flagName) { const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) { throw new Error(`${flagName} must be a positive integer. Received: ${value}`); } return parsed; } async function findProjectRoot(startDirectory) { let current = path.resolve(startDirectory); while (true) { if (await pathExists(path.join(current, 'package.json'))) { return current; } const parent = path.dirname(current); if (parent === current) { throw new Error(`Unable to find package.json while walking up from ${startDirectory}.`); } current = parent; } } async function pathExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } function printHelp() { console.log(`Core machine catalogue contract audit Usage: node scripts/audit-core-machine-contract.mjs [options] Options: --expected Expected number of Phase 1 machines. Default: ${DEFAULT_EXPECTED_MACHINE_COUNT} --json Print the full report as JSON. --strict Treat warnings as failures. --warnings-as-errors Alias for --strict. --max-issues Maximum formatted issues to print in text mode. Default: 200 --verbose Print stack traces for startup errors. --help, -h Show this help text. The audit loads the TypeScript catalogue through Vite SSR, then verifies that the machine registry, dossiers, procedural blueprints, and animation modules remain in lockstep and contain the educational/rendering metadata required for the Phase 1 catalogue.`); } main().catch((error) => { const verbose = process.argv.includes('--verbose'); const message = error instanceof Error ? error.message : String(error); console.error(`\nCore machine contract audit failed to start:\n${message}`); if (verbose && error instanceof Error && error.stack) { console.error(`\n${error.stack}`); } process.exitCode = 2; });