| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- 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);
- }
|