const fs = require('fs'); const path = require('path'); const VALID_NODE_TYPES = new Set([ 'file','function','class','module','concept','config','document', 'service','table','endpoint','pipeline','schema','resource','domain','flow','step' ]); const VALID_EDGE_TYPES = new Set([ 'imports','exports','contains','inherits','implements','calls','subscribes', 'publishes','middleware','reads_from','writes_to','transforms','validates', 'depends_on','tested_by','configures','related','similar_to','deploys','serves', 'migrates','documents','provisions','routes','defines_schema','triggers', 'contains_flow','flow_step','cross_domain' ]); const VALID_COMPLEXITY = new Set(['simple','moderate','complex']); const VALID_DIRECTION = new Set(['forward','backward','bidirectional']); const FILE_LEVEL_TYPES = new Set(['file','config','document','service','pipeline','table','schema','resource','endpoint']); const graphPath = process.argv[2]; const outPath = process.argv[3]; const issues = []; const warnings = []; function addIssue(msg) { issues.push(msg); } function addWarning(msg) { warnings.push(msg); } try { const raw = fs.readFileSync(graphPath, 'utf8'); const graph = JSON.parse(raw); const nodes = graph.nodes || []; const edges = graph.edges || []; const layers = graph.layers || []; const tour = graph.tour || []; // Build lookup sets const nodeIds = new Set(nodes.map(n => n.id)); const nodeIdsInLayers = new Set(); const nodeIdsInTour = new Set(); // Stats const nodeTypeCounts = {}; const edgeTypeCounts = {}; for (const n of nodes) { nodeTypeCounts[n.type] = (nodeTypeCounts[n.type] || 0) + 1; } for (const e of edges) { edgeTypeCounts[e.type] = (edgeTypeCounts[e.type] || 0) + 1; } // ========== Check 1: Schema Validation ========== // Nodes for (const n of nodes) { if (!n.id || typeof n.id !== 'string' || n.id.trim() === '') addIssue(`Node missing/empty id: ${JSON.stringify(n)}`); if (!n.type || !VALID_NODE_TYPES.has(n.type)) addIssue(`Node "${n.id}" has invalid type "${n.type}"`); if (!n.name || typeof n.name !== 'string' || n.name.trim() === '') addIssue(`Node "${n.id}" missing/empty name`); if (!n.summary || typeof n.summary !== 'string' || n.summary.trim() === '') addIssue(`Node "${n.id}" missing/empty summary`); if (n.summary && n.name && n.summary.trim() === n.name.trim()) addWarning(`Node "${n.id}" summary equals name`); if (!Array.isArray(n.tags) || n.tags.length === 0) addIssue(`Node "${n.id}" missing/empty tags`); else { for (const t of n.tags) { if (typeof t !== 'string') addIssue(`Node "${n.id}" has non-string tag`); } } if (!n.complexity || !VALID_COMPLEXITY.has(n.complexity)) addIssue(`Node "${n.id}" has invalid complexity "${n.complexity}"`); // ID prefix check (Check 9) if (n.id && n.type) { const prefix = n.id.split(':')[0]; if (prefix !== n.type) addWarning(`Node "${n.id}" type "${n.type}" does not match ID prefix "${prefix}"`); } } // Edges for (let i = 0; i < edges.length; i++) { const e = edges[i]; if (!e.source || typeof e.source !== 'string') addIssue(`Edge[${i}] missing/empty source`); if (!e.target || typeof e.target !== 'string') addIssue(`Edge[${i}] missing/empty target`); if (!e.type || !VALID_EDGE_TYPES.has(e.type)) addIssue(`Edge[${i}] (${e.source} -> ${e.target}) has invalid type "${e.type}"`); if (!e.direction || !VALID_DIRECTION.has(e.direction)) addIssue(`Edge[${i}] (${e.source} -> ${e.target}) has invalid direction "${e.direction}"`); if (typeof e.weight !== 'number' || e.weight < 0 || e.weight > 1) addIssue(`Edge[${i}] (${e.source} -> ${e.target}) has invalid weight "${e.weight}"`); } // ========== Check 2: Referential Integrity ========== for (let i = 0; i < edges.length; i++) { const e = edges[i]; if (e.source && !nodeIds.has(e.source)) addIssue(`Edge[${i}] source "${e.source}" references non-existent node`); if (e.target && !nodeIds.has(e.target)) addIssue(`Edge[${i}] target "${e.target}" references non-existent node`); } for (const layer of layers) { if (Array.isArray(layer.nodeIds)) { for (const nid of layer.nodeIds) { nodeIdsInLayers.add(nid); if (!nodeIds.has(nid)) addIssue(`Layer "${layer.id}" nodeIds references non-existent node "${nid}"`); } } } for (const step of tour) { if (Array.isArray(step.nodeIds)) { for (const nid of step.nodeIds) { nodeIdsInTour.add(nid); if (!nodeIds.has(nid)) addIssue(`Tour step ${step.order} references non-existent node "${nid}"`); } } } // ========== Check 3: Completeness ========== if (nodes.length < 1) addIssue('No nodes in graph'); if (edges.length < 1) addIssue('No edges in graph'); if (layers.length < 1) addIssue('No layers in graph'); if (tour.length < 1) addIssue('No tour steps in graph'); // ========== Check 4: Layer Coverage ========== const fileLevelNodes = nodes.filter(n => FILE_LEVEL_TYPES.has(n.type)); const fileLevelNodeIds = new Set(fileLevelNodes.map(n => n.id)); const coveredInLayers = new Set(); for (const layer of layers) { if (!Array.isArray(layer.nodeIds) || layer.nodeIds.length === 0) { addIssue(`Layer "${layer.id}" has empty nodeIds`); } for (const nid of (layer.nodeIds || [])) { if (fileLevelNodeIds.has(nid)) { if (coveredInLayers.has(nid)) { addIssue(`File-level node "${nid}" appears in multiple layers`); } else { coveredInLayers.add(nid); } } } } for (const fnid of fileLevelNodeIds) { if (!coveredInLayers.has(fnid)) { addIssue(`File-level node "${fnid}" is not in any layer`); } } // ========== Check 5: Uniqueness ========== const seenIds = new Set(); for (const n of nodes) { if (seenIds.has(n.id)) addIssue(`Duplicate node ID: "${n.id}"`); seenIds.add(n.id); } // ========== Check 6: Tour Validation ========== if (tour.length < 5 || tour.length > 15) { addWarning(`Tour has ${tour.length} steps (expected 5-15)`); } for (let i = 0; i < tour.length; i++) { const step = tour[i]; if (step.order !== i + 1) addWarning(`Tour step index ${i} has order ${step.order}, expected ${i + 1}`); if (!Array.isArray(step.nodeIds) || step.nodeIds.length === 0) addWarning(`Tour step ${step.order} has no nodeIds`); } // ========== Check 7: Quality Checks ========== // Build edge connectivity set const connectedNodes = new Set(); for (const e of edges) { connectedNodes.add(e.source); connectedNodes.add(e.target); } for (const n of nodes) { if (!connectedNodes.has(n.id)) addWarning(`Orphan node: "${n.id}" has no edges`); } for (let i = 0; i < edges.length; i++) { const e = edges[i]; if (e.source === e.target) addWarning(`Self-referencing edge[${i}]: "${e.source}"`); } // ========== Check 8: Non-Code Node Quality ========== // Build edge index by source and target const edgesBySource = {}; const edgesByTarget = {}; for (const e of edges) { if (!edgesBySource[e.source]) edgesBySource[e.source] = []; edgesBySource[e.source].push(e); if (!edgesByTarget[e.target]) edgesByTarget[e.target] = []; edgesByTarget[e.target].push(e); } function nodeHasEdgeType(nodeId, edgeType) { const outEdges = edgesBySource[nodeId] || []; const inEdges = edgesByTarget[nodeId] || []; return outEdges.some(e => e.type === edgeType) || inEdges.some(e => e.type === edgeType); } for (const n of nodes) { const allEdges = [...(edgesBySource[n.id] || []), ...(edgesByTarget[n.id] || [])]; if (n.type === 'document' && !nodeHasEdgeType(n.id, 'documents')) { addWarning(`Document node "${n.id}" has no "documents" edge`); } if (n.type === 'service' && !(nodeHasEdgeType(n.id, 'deploys') || nodeHasEdgeType(n.id, 'depends_on'))) { addWarning(`Service node "${n.id}" has no "deploys" or "depends_on" edge`); } if (n.type === 'pipeline' && !nodeHasEdgeType(n.id, 'triggers')) { addWarning(`Pipeline node "${n.id}" has no "triggers" edge`); } if (n.type === 'table' && !(nodeHasEdgeType(n.id, 'migrates') || nodeHasEdgeType(n.id, 'defines_schema'))) { addWarning(`Table node "${n.id}" has no "migrates" or "defines_schema" edge`); } if (n.type === 'schema' && !nodeHasEdgeType(n.id, 'defines_schema')) { addWarning(`Schema node "${n.id}" has no "defines_schema" edge`); } } // ========== Output ========== const result = { scriptCompleted: true, issues, warnings, stats: { totalNodes: nodes.length, totalEdges: edges.length, totalLayers: layers.length, tourSteps: tour.length, nodeTypes: nodeTypeCounts, edgeTypes: edgeTypeCounts } }; fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8'); console.log(JSON.stringify(result, null, 2)); process.exit(0); } catch (err) { console.error('Script crash:', err.message); process.exit(1); }