#!/usr/bin/env node const fs = require('fs'); const graphPath = process.argv[2]; const outputPath = process.argv[3]; try { const graph = JSON.parse(fs.readFileSync(graphPath, 'utf8')); const issues = [], warnings = []; if (!Array.isArray(graph.nodes)) { issues.push('graph.nodes is missing or not an array'); graph.nodes = []; } if (!Array.isArray(graph.edges)) { issues.push('graph.edges is missing or not an array'); graph.edges = []; } const nodeIds = new Set(); const seen = new Map(); graph.nodes.forEach((n, i) => { if (!n.id) { issues.push(`Node[${i}] missing id`); return; } if (!n.type) issues.push(`Node[${i}] '${n.id}' missing type`); if (!n.name) issues.push(`Node[${i}] '${n.id}' missing name`); if (!n.summary) issues.push(`Node[${i}] '${n.id}' missing summary`); if (!n.tags || !n.tags.length) issues.push(`Node[${i}] '${n.id}' missing tags`); if (seen.has(n.id)) issues.push(`Duplicate node ID '${n.id}' at indices ${seen.get(n.id)} and ${i}`); else seen.set(n.id, i); nodeIds.add(n.id); }); graph.edges.forEach((e, i) => { if (!nodeIds.has(e.source)) issues.push(`Edge[${i}] source '${e.source}' not found`); if (!nodeIds.has(e.target)) issues.push(`Edge[${i}] target '${e.target}' not found`); }); const fileLevelTypes = new Set(['file', 'config', 'document', 'service', 'pipeline', 'table', 'schema', 'resource', 'endpoint']); const fileNodes = graph.nodes.filter(n => fileLevelTypes.has(n.type)).map(n => n.id); const assigned = new Map(); if (!Array.isArray(graph.layers)) { if (graph.layers) warnings.push('graph.layers is not an array'); graph.layers = []; } if (!Array.isArray(graph.tour)) { if (graph.tour) warnings.push('graph.tour is not an array'); graph.tour = []; } graph.layers.forEach(layer => { (layer.nodeIds || []).forEach(id => { if (!nodeIds.has(id)) issues.push(`Layer '${layer.id}' refs missing node '${id}'`); if (assigned.has(id)) issues.push(`Node '${id}' appears in multiple layers`); assigned.set(id, layer.id); }); }); fileNodes.forEach(id => { if (!assigned.has(id)) issues.push(`File node '${id}' not in any layer`); }); graph.tour.forEach((step, i) => { (step.nodeIds || []).forEach(id => { if (!nodeIds.has(id)) issues.push(`Tour step[${i}] refs missing node '${id}'`); }); }); const withEdges = new Set([ ...graph.edges.map(e => e.source), ...graph.edges.map(e => e.target) ]); graph.nodes.forEach(n => { if (!withEdges.has(n.id)) warnings.push(`Node '${n.id}' has no edges (orphan)`); }); const stats = { totalNodes: graph.nodes.length, totalEdges: graph.edges.length, totalLayers: graph.layers.length, tourSteps: graph.tour.length, nodeTypes: graph.nodes.reduce((a, n) => { a[n.type] = (a[n.type]||0)+1; return a; }, {}), edgeTypes: graph.edges.reduce((a, e) => { a[e.type] = (a[e.type]||0)+1; return a; }, {}) }; fs.writeFileSync(outputPath, JSON.stringify({ issues, warnings, stats }, null, 2)); process.exit(0); } catch (err) { process.stderr.write(err.message + '\n'); process.exit(1); }