/** * @fileoverview Route Catalog Comparison for Parity Validation * Compares route constants between internal and external repositories * Part of CON-06: Cross-Repo Schema Parity & Integration Validation */ import fs from 'fs'; import path from 'path'; import { ROUTES } from '../routes/routes.js'; import { loadMultipleExternalRoutes, extractRouteConstants } from './loadExternalRoutes.js'; export interface RouteComparison { missing: string[]; extra: string[]; modified: Array<{ route: string; internal: string; external: string; }>; totalRoutes: number; parityScore: number; } /** * Compare internal routes with external routes from multiple repositories */ export async function compareRoutes(options: { frontend?: string; infra?: string; ml?: string; }): Promise { // Get internal route constants const internalRoutes = extractRouteConstants(ROUTES); console.log(`📊 Found ${internalRoutes.length} internal routes`); // Load external routes from specified repositories const repoPaths = [ options.frontend, options.infra, options.ml, ].filter(Boolean) as string[]; const externalRoutesMap = await loadMultipleExternalRoutes(repoPaths); if (Object.keys(externalRoutesMap).length === 0) { console.warn('⚠️ No external routes loaded for comparison'); return { missing: [], extra: [], modified: [], totalRoutes: internalRoutes.length, parityScore: 100, }; } // Combine all external routes for comparison const allExternalRoutes = new Set(); for (const externalRoutes of Object.values(externalRoutesMap)) { const constants = extractRouteConstants(externalRoutes); constants.forEach(route => allExternalRoutes.add(route)); } console.log(`📊 Found ${allExternalRoutes.size} external routes across ${Object.keys(externalRoutesMap).length} repositories`); // Find differences const missing = internalRoutes.filter(route => !allExternalRoutes.has(route)); const extra = Array.from(allExternalRoutes).filter(route => !internalRoutes.includes(route)); // For modified routes, we'd need to compare actual URL values // This is a simplified version - in practice, we'd need to compare the actual route strings const modified: Array<{route: string; internal: string; external: string}> = []; const totalRoutes = Math.max(internalRoutes.length, allExternalRoutes.size); const matchingRoutes = totalRoutes - missing.length - extra.length; const parityScore = totalRoutes > 0 ? (matchingRoutes / totalRoutes) * 100 : 100; return { missing, extra, modified, totalRoutes, parityScore, }; } /** * Save route comparison results to CSV for analysis */ export function saveRouteComparisonToCSV(comparison: RouteComparison, outputPath: string): void { // Ensure directory exists const dir = path.dirname(outputPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } let csvContent = 'Type,Route,Details\n'; // Missing routes comparison.missing.forEach(route => { csvContent += `MISSING,"${route}","Route exists in contracts but not in external repos"\n`; }); // Extra routes comparison.extra.forEach(route => { csvContent += `EXTRA,"${route}","Route exists in external repos but not in contracts"\n`; }); // Modified routes comparison.modified.forEach(mod => { csvContent += `MODIFIED,"${mod.route}","${mod.internal}" -> "${mod.external}"\n`; }); fs.writeFileSync(outputPath, csvContent); } /** * Generate route diff summary for reports */ export function generateRouteDiffSummary(comparison: RouteComparison): string { const lines = [ `# Route Catalog Parity Report`, ``, `## Summary`, `- **Total Internal Routes:** ${comparison.totalRoutes}`, `- **Missing Routes:** ${comparison.missing.length}`, `- **Extra Routes:** ${comparison.extra.length}`, `- **Modified Routes:** ${comparison.modified.length}`, `- **Parity Score:** ${comparison.parityScore.toFixed(1)}%`, ``, `## Missing Routes (in contracts but not external)`, ]; if (comparison.missing.length > 0) { comparison.missing.slice(0, 20).forEach(route => { lines.push(`- ${route}`); }); if (comparison.missing.length > 20) { lines.push(`- ... and ${comparison.missing.length - 20} more`); } } else { lines.push(`- None`); } lines.push(``); lines.push(`## Extra Routes (in external but not contracts)`); if (comparison.extra.length > 0) { comparison.extra.slice(0, 20).forEach(route => { lines.push(`- ${route}`); }); if (comparison.extra.length > 20) { lines.push(`- ... and ${comparison.extra.length - 20} more`); } } else { lines.push(`- None`); } return lines.join('\n'); }