ua-graph-validate.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. const fs = require('fs');
  2. const path = require('path');
  3. const VALID_NODE_TYPES = new Set([
  4. 'file','function','class','module','concept','config','document',
  5. 'service','table','endpoint','pipeline','schema','resource','domain','flow','step'
  6. ]);
  7. const VALID_EDGE_TYPES = new Set([
  8. 'imports','exports','contains','inherits','implements','calls','subscribes',
  9. 'publishes','middleware','reads_from','writes_to','transforms','validates',
  10. 'depends_on','tested_by','configures','related','similar_to','deploys','serves',
  11. 'migrates','documents','provisions','routes','defines_schema','triggers',
  12. 'contains_flow','flow_step','cross_domain'
  13. ]);
  14. const VALID_COMPLEXITY = new Set(['simple','moderate','complex']);
  15. const VALID_DIRECTION = new Set(['forward','backward','bidirectional']);
  16. const FILE_LEVEL_TYPES = new Set(['file','config','document','service','pipeline','table','schema','resource','endpoint']);
  17. const graphPath = process.argv[2];
  18. const outPath = process.argv[3];
  19. const issues = [];
  20. const warnings = [];
  21. function addIssue(msg) { issues.push(msg); }
  22. function addWarning(msg) { warnings.push(msg); }
  23. try {
  24. const raw = fs.readFileSync(graphPath, 'utf8');
  25. const graph = JSON.parse(raw);
  26. const nodes = graph.nodes || [];
  27. const edges = graph.edges || [];
  28. const layers = graph.layers || [];
  29. const tour = graph.tour || [];
  30. // Build lookup sets
  31. const nodeIds = new Set(nodes.map(n => n.id));
  32. const nodeIdsInLayers = new Set();
  33. const nodeIdsInTour = new Set();
  34. // Stats
  35. const nodeTypeCounts = {};
  36. const edgeTypeCounts = {};
  37. for (const n of nodes) { nodeTypeCounts[n.type] = (nodeTypeCounts[n.type] || 0) + 1; }
  38. for (const e of edges) { edgeTypeCounts[e.type] = (edgeTypeCounts[e.type] || 0) + 1; }
  39. // ========== Check 1: Schema Validation ==========
  40. // Nodes
  41. for (const n of nodes) {
  42. if (!n.id || typeof n.id !== 'string' || n.id.trim() === '') addIssue(`Node missing/empty id: ${JSON.stringify(n)}`);
  43. if (!n.type || !VALID_NODE_TYPES.has(n.type)) addIssue(`Node "${n.id}" has invalid type "${n.type}"`);
  44. if (!n.name || typeof n.name !== 'string' || n.name.trim() === '') addIssue(`Node "${n.id}" missing/empty name`);
  45. if (!n.summary || typeof n.summary !== 'string' || n.summary.trim() === '') addIssue(`Node "${n.id}" missing/empty summary`);
  46. if (n.summary && n.name && n.summary.trim() === n.name.trim()) addWarning(`Node "${n.id}" summary equals name`);
  47. if (!Array.isArray(n.tags) || n.tags.length === 0) addIssue(`Node "${n.id}" missing/empty tags`);
  48. else {
  49. for (const t of n.tags) {
  50. if (typeof t !== 'string') addIssue(`Node "${n.id}" has non-string tag`);
  51. }
  52. }
  53. if (!n.complexity || !VALID_COMPLEXITY.has(n.complexity)) addIssue(`Node "${n.id}" has invalid complexity "${n.complexity}"`);
  54. // ID prefix check (Check 9)
  55. if (n.id && n.type) {
  56. const prefix = n.id.split(':')[0];
  57. if (prefix !== n.type) addWarning(`Node "${n.id}" type "${n.type}" does not match ID prefix "${prefix}"`);
  58. }
  59. }
  60. // Edges
  61. for (let i = 0; i < edges.length; i++) {
  62. const e = edges[i];
  63. if (!e.source || typeof e.source !== 'string') addIssue(`Edge[${i}] missing/empty source`);
  64. if (!e.target || typeof e.target !== 'string') addIssue(`Edge[${i}] missing/empty target`);
  65. if (!e.type || !VALID_EDGE_TYPES.has(e.type)) addIssue(`Edge[${i}] (${e.source} -> ${e.target}) has invalid type "${e.type}"`);
  66. if (!e.direction || !VALID_DIRECTION.has(e.direction)) addIssue(`Edge[${i}] (${e.source} -> ${e.target}) has invalid direction "${e.direction}"`);
  67. if (typeof e.weight !== 'number' || e.weight < 0 || e.weight > 1) addIssue(`Edge[${i}] (${e.source} -> ${e.target}) has invalid weight "${e.weight}"`);
  68. }
  69. // ========== Check 2: Referential Integrity ==========
  70. for (let i = 0; i < edges.length; i++) {
  71. const e = edges[i];
  72. if (e.source && !nodeIds.has(e.source)) addIssue(`Edge[${i}] source "${e.source}" references non-existent node`);
  73. if (e.target && !nodeIds.has(e.target)) addIssue(`Edge[${i}] target "${e.target}" references non-existent node`);
  74. }
  75. for (const layer of layers) {
  76. if (Array.isArray(layer.nodeIds)) {
  77. for (const nid of layer.nodeIds) {
  78. nodeIdsInLayers.add(nid);
  79. if (!nodeIds.has(nid)) addIssue(`Layer "${layer.id}" nodeIds references non-existent node "${nid}"`);
  80. }
  81. }
  82. }
  83. for (const step of tour) {
  84. if (Array.isArray(step.nodeIds)) {
  85. for (const nid of step.nodeIds) {
  86. nodeIdsInTour.add(nid);
  87. if (!nodeIds.has(nid)) addIssue(`Tour step ${step.order} references non-existent node "${nid}"`);
  88. }
  89. }
  90. }
  91. // ========== Check 3: Completeness ==========
  92. if (nodes.length < 1) addIssue('No nodes in graph');
  93. if (edges.length < 1) addIssue('No edges in graph');
  94. if (layers.length < 1) addIssue('No layers in graph');
  95. if (tour.length < 1) addIssue('No tour steps in graph');
  96. // ========== Check 4: Layer Coverage ==========
  97. const fileLevelNodes = nodes.filter(n => FILE_LEVEL_TYPES.has(n.type));
  98. const fileLevelNodeIds = new Set(fileLevelNodes.map(n => n.id));
  99. const coveredInLayers = new Set();
  100. for (const layer of layers) {
  101. if (!Array.isArray(layer.nodeIds) || layer.nodeIds.length === 0) {
  102. addIssue(`Layer "${layer.id}" has empty nodeIds`);
  103. }
  104. for (const nid of (layer.nodeIds || [])) {
  105. if (fileLevelNodeIds.has(nid)) {
  106. if (coveredInLayers.has(nid)) {
  107. addIssue(`File-level node "${nid}" appears in multiple layers`);
  108. } else {
  109. coveredInLayers.add(nid);
  110. }
  111. }
  112. }
  113. }
  114. for (const fnid of fileLevelNodeIds) {
  115. if (!coveredInLayers.has(fnid)) {
  116. addIssue(`File-level node "${fnid}" is not in any layer`);
  117. }
  118. }
  119. // ========== Check 5: Uniqueness ==========
  120. const seenIds = new Set();
  121. for (const n of nodes) {
  122. if (seenIds.has(n.id)) addIssue(`Duplicate node ID: "${n.id}"`);
  123. seenIds.add(n.id);
  124. }
  125. // ========== Check 6: Tour Validation ==========
  126. if (tour.length < 5 || tour.length > 15) {
  127. addWarning(`Tour has ${tour.length} steps (expected 5-15)`);
  128. }
  129. for (let i = 0; i < tour.length; i++) {
  130. const step = tour[i];
  131. if (step.order !== i + 1) addWarning(`Tour step index ${i} has order ${step.order}, expected ${i + 1}`);
  132. if (!Array.isArray(step.nodeIds) || step.nodeIds.length === 0) addWarning(`Tour step ${step.order} has no nodeIds`);
  133. }
  134. // ========== Check 7: Quality Checks ==========
  135. // Build edge connectivity set
  136. const connectedNodes = new Set();
  137. for (const e of edges) {
  138. connectedNodes.add(e.source);
  139. connectedNodes.add(e.target);
  140. }
  141. for (const n of nodes) {
  142. if (!connectedNodes.has(n.id)) addWarning(`Orphan node: "${n.id}" has no edges`);
  143. }
  144. for (let i = 0; i < edges.length; i++) {
  145. const e = edges[i];
  146. if (e.source === e.target) addWarning(`Self-referencing edge[${i}]: "${e.source}"`);
  147. }
  148. // ========== Check 8: Non-Code Node Quality ==========
  149. // Build edge index by source and target
  150. const edgesBySource = {};
  151. const edgesByTarget = {};
  152. for (const e of edges) {
  153. if (!edgesBySource[e.source]) edgesBySource[e.source] = [];
  154. edgesBySource[e.source].push(e);
  155. if (!edgesByTarget[e.target]) edgesByTarget[e.target] = [];
  156. edgesByTarget[e.target].push(e);
  157. }
  158. function nodeHasEdgeType(nodeId, edgeType) {
  159. const outEdges = edgesBySource[nodeId] || [];
  160. const inEdges = edgesByTarget[nodeId] || [];
  161. return outEdges.some(e => e.type === edgeType) || inEdges.some(e => e.type === edgeType);
  162. }
  163. for (const n of nodes) {
  164. const allEdges = [...(edgesBySource[n.id] || []), ...(edgesByTarget[n.id] || [])];
  165. if (n.type === 'document' && !nodeHasEdgeType(n.id, 'documents')) {
  166. addWarning(`Document node "${n.id}" has no "documents" edge`);
  167. }
  168. if (n.type === 'service' && !(nodeHasEdgeType(n.id, 'deploys') || nodeHasEdgeType(n.id, 'depends_on'))) {
  169. addWarning(`Service node "${n.id}" has no "deploys" or "depends_on" edge`);
  170. }
  171. if (n.type === 'pipeline' && !nodeHasEdgeType(n.id, 'triggers')) {
  172. addWarning(`Pipeline node "${n.id}" has no "triggers" edge`);
  173. }
  174. if (n.type === 'table' && !(nodeHasEdgeType(n.id, 'migrates') || nodeHasEdgeType(n.id, 'defines_schema'))) {
  175. addWarning(`Table node "${n.id}" has no "migrates" or "defines_schema" edge`);
  176. }
  177. if (n.type === 'schema' && !nodeHasEdgeType(n.id, 'defines_schema')) {
  178. addWarning(`Schema node "${n.id}" has no "defines_schema" edge`);
  179. }
  180. }
  181. // ========== Output ==========
  182. const result = {
  183. scriptCompleted: true,
  184. issues,
  185. warnings,
  186. stats: {
  187. totalNodes: nodes.length,
  188. totalEdges: edges.length,
  189. totalLayers: layers.length,
  190. tourSteps: tour.length,
  191. nodeTypes: nodeTypeCounts,
  192. edgeTypes: edgeTypeCounts
  193. }
  194. };
  195. fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
  196. console.log(JSON.stringify(result, null, 2));
  197. process.exit(0);
  198. } catch (err) {
  199. console.error('Script crash:', err.message);
  200. process.exit(1);
  201. }