ua-inline-validate.cjs 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const graphPath = process.argv[2];
  4. const outputPath = process.argv[3];
  5. try {
  6. const graph = JSON.parse(fs.readFileSync(graphPath, 'utf8'));
  7. const issues = [], warnings = [];
  8. if (!Array.isArray(graph.nodes)) { issues.push('graph.nodes is missing or not an array'); graph.nodes = []; }
  9. if (!Array.isArray(graph.edges)) { issues.push('graph.edges is missing or not an array'); graph.edges = []; }
  10. const nodeIds = new Set();
  11. const seen = new Map();
  12. graph.nodes.forEach((n, i) => {
  13. if (!n.id) { issues.push(`Node[${i}] missing id`); return; }
  14. if (!n.type) issues.push(`Node[${i}] '${n.id}' missing type`);
  15. if (!n.name) issues.push(`Node[${i}] '${n.id}' missing name`);
  16. if (!n.summary) issues.push(`Node[${i}] '${n.id}' missing summary`);
  17. if (!n.tags || !n.tags.length) issues.push(`Node[${i}] '${n.id}' missing tags`);
  18. if (seen.has(n.id)) issues.push(`Duplicate node ID '${n.id}' at indices ${seen.get(n.id)} and ${i}`);
  19. else seen.set(n.id, i);
  20. nodeIds.add(n.id);
  21. });
  22. graph.edges.forEach((e, i) => {
  23. if (!nodeIds.has(e.source)) issues.push(`Edge[${i}] source '${e.source}' not found`);
  24. if (!nodeIds.has(e.target)) issues.push(`Edge[${i}] target '${e.target}' not found`);
  25. });
  26. const fileLevelTypes = new Set(['file', 'config', 'document', 'service', 'pipeline', 'table', 'schema', 'resource', 'endpoint']);
  27. const fileNodes = graph.nodes.filter(n => fileLevelTypes.has(n.type)).map(n => n.id);
  28. const assigned = new Map();
  29. if (!Array.isArray(graph.layers)) { if (graph.layers) warnings.push('graph.layers is not an array'); graph.layers = []; }
  30. if (!Array.isArray(graph.tour)) { if (graph.tour) warnings.push('graph.tour is not an array'); graph.tour = []; }
  31. graph.layers.forEach(layer => {
  32. (layer.nodeIds || []).forEach(id => {
  33. if (!nodeIds.has(id)) issues.push(`Layer '${layer.id}' refs missing node '${id}'`);
  34. if (assigned.has(id)) issues.push(`Node '${id}' appears in multiple layers`);
  35. assigned.set(id, layer.id);
  36. });
  37. });
  38. fileNodes.forEach(id => {
  39. if (!assigned.has(id)) issues.push(`File node '${id}' not in any layer`);
  40. });
  41. graph.tour.forEach((step, i) => {
  42. (step.nodeIds || []).forEach(id => {
  43. if (!nodeIds.has(id)) issues.push(`Tour step[${i}] refs missing node '${id}'`);
  44. });
  45. });
  46. const withEdges = new Set([
  47. ...graph.edges.map(e => e.source),
  48. ...graph.edges.map(e => e.target)
  49. ]);
  50. graph.nodes.forEach(n => {
  51. if (!withEdges.has(n.id)) warnings.push(`Node '${n.id}' has no edges (orphan)`);
  52. });
  53. const stats = {
  54. totalNodes: graph.nodes.length,
  55. totalEdges: graph.edges.length,
  56. totalLayers: graph.layers.length,
  57. tourSteps: graph.tour.length,
  58. nodeTypes: graph.nodes.reduce((a, n) => { a[n.type] = (a[n.type]||0)+1; return a; }, {}),
  59. edgeTypes: graph.edges.reduce((a, e) => { a[e.type] = (a[e.type]||0)+1; return a; }, {})
  60. };
  61. fs.writeFileSync(outputPath, JSON.stringify({ issues, warnings, stats }, null, 2));
  62. process.exit(0);
  63. } catch (err) { process.stderr.write(err.message + '\n'); process.exit(1); }