spec-gen-cli 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +80 -20
  2. package/dist/api/generate.d.ts.map +1 -1
  3. package/dist/api/generate.js +52 -35
  4. package/dist/api/generate.js.map +1 -1
  5. package/dist/api/run.d.ts.map +1 -1
  6. package/dist/api/run.js +5 -3
  7. package/dist/api/run.js.map +1 -1
  8. package/dist/api/types.d.ts +4 -4
  9. package/dist/api/types.d.ts.map +1 -1
  10. package/dist/cli/commands/generate.d.ts.map +1 -1
  11. package/dist/cli/commands/generate.js +29 -14
  12. package/dist/cli/commands/generate.js.map +1 -1
  13. package/dist/cli/commands/mcp.d.ts.map +1 -1
  14. package/dist/cli/commands/mcp.js +5 -3
  15. package/dist/cli/commands/mcp.js.map +1 -1
  16. package/dist/constants.d.ts +2 -0
  17. package/dist/constants.d.ts.map +1 -1
  18. package/dist/constants.js +2 -0
  19. package/dist/constants.js.map +1 -1
  20. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
  21. package/dist/core/analyzer/artifact-generator.js +11 -3
  22. package/dist/core/analyzer/artifact-generator.js.map +1 -1
  23. package/dist/core/analyzer/call-graph.d.ts +2 -2
  24. package/dist/core/analyzer/call-graph.d.ts.map +1 -1
  25. package/dist/core/analyzer/call-graph.js +137 -6
  26. package/dist/core/analyzer/call-graph.js.map +1 -1
  27. package/dist/core/analyzer/dependency-graph.d.ts +19 -0
  28. package/dist/core/analyzer/dependency-graph.d.ts.map +1 -1
  29. package/dist/core/analyzer/dependency-graph.js +76 -0
  30. package/dist/core/analyzer/dependency-graph.js.map +1 -1
  31. package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -1
  32. package/dist/core/analyzer/duplicate-detector.js +7 -1
  33. package/dist/core/analyzer/duplicate-detector.js.map +1 -1
  34. package/dist/core/analyzer/signature-extractor.d.ts.map +1 -1
  35. package/dist/core/analyzer/signature-extractor.js +59 -0
  36. package/dist/core/analyzer/signature-extractor.js.map +1 -1
  37. package/dist/core/generator/openspec-format-generator.d.ts +17 -2
  38. package/dist/core/generator/openspec-format-generator.d.ts.map +1 -1
  39. package/dist/core/generator/openspec-format-generator.js +111 -10
  40. package/dist/core/generator/openspec-format-generator.js.map +1 -1
  41. package/dist/core/generator/rag-manifest-generator.d.ts +37 -0
  42. package/dist/core/generator/rag-manifest-generator.d.ts.map +1 -0
  43. package/dist/core/generator/rag-manifest-generator.js +134 -0
  44. package/dist/core/generator/rag-manifest-generator.js.map +1 -0
  45. package/dist/core/services/llm-service.d.ts +23 -1
  46. package/dist/core/services/llm-service.d.ts.map +1 -1
  47. package/dist/core/services/llm-service.js +94 -2
  48. package/dist/core/services/llm-service.js.map +1 -1
  49. package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -1
  50. package/dist/core/services/mcp-handlers/orient.js +108 -1
  51. package/dist/core/services/mcp-handlers/orient.js.map +1 -1
  52. package/dist/core/services/mcp-watcher.js +1 -1
  53. package/dist/core/services/mcp-watcher.js.map +1 -1
  54. package/dist/types/index.d.ts +1 -1
  55. package/dist/types/index.d.ts.map +1 -1
  56. package/package.json +2 -1
  57. package/src/viewer/InteractiveGraphViewer.jsx +6 -2
  58. package/src/viewer/components/ClassGraph.jsx +97 -14
  59. package/src/viewer/components/FlatGraph.jsx +3 -3
  60. package/src/viewer/utils/graph-helpers.js +8 -0
@@ -352,7 +352,11 @@ export default function App({ graphUrl, mappingUrl = '/api/mapping', specUrl = '
352
352
  // Compute from clusters if not present in the JSON (for backward compatibility).
353
353
  const structuralClusters = graph?.structuralClusters ??
354
354
  (graph?.clusters?.filter(c => c.internalEdges > 0) ?? []);
355
- const displayClusters = structuralClusters;
355
+ // Fall back to all directory clusters when no structural ones exist (e.g. Swift/C++ projects
356
+ // where dep edges come from the call graph and may not yet be available).
357
+ const displayClusters = structuralClusters.length > 0
358
+ ? structuralClusters
359
+ : (graph?.clusters ?? []);
356
360
  const clusterNames = displayClusters.map((c) => c.name);
357
361
 
358
362
  // ── Upload screen ─────────────────────────────────────────────────────────
@@ -488,7 +492,7 @@ export default function App({ graphUrl, mappingUrl = '/api/mapping', specUrl = '
488
492
  {[
489
493
  ['nodes', stats.nodeCount],
490
494
  ['edges', stats.edgeCount],
491
- ['clusters', stats.structuralClusterCount ?? displayClusters.length],
495
+ ['clusters', displayClusters.length],
492
496
  ].map(([l, v]) => (
493
497
  <div
494
498
  key={l}
@@ -36,40 +36,84 @@ const LANG_CSS_VAR = {
36
36
  Go: 'var(--lc-cyan)',
37
37
  Rust: 'var(--lc-red)',
38
38
  Ruby: 'var(--lc-pink)',
39
+ Swift: 'var(--lc-orange)',
39
40
  };
40
41
 
42
+ const COMPONENT_COLORS = [
43
+ 'var(--lc-cyan)',
44
+ 'var(--lc-orange)',
45
+ 'var(--lc-green)',
46
+ 'var(--lc-pink)',
47
+ 'var(--lc-purple)',
48
+ 'var(--lc-yellow)',
49
+ 'var(--lc-red)',
50
+ '#7eb8f7',
51
+ '#f7c56a',
52
+ '#a0e8a0',
53
+ ];
54
+
41
55
  function langColor(lang) {
42
56
  return LANG_CSS_VAR[lang] ?? 'var(--ac-primary)';
43
57
  }
44
58
 
59
+ function componentColor(compIndex) {
60
+ return COMPONENT_COLORS[compIndex % COMPONENT_COLORS.length];
61
+ }
62
+
45
63
  // ============================================================================
46
64
  // FORCE LAYOUT
47
65
  // ============================================================================
48
66
 
67
+ // Find connected components (union-find)
68
+ function connectedComponents(nodes, edges) {
69
+ const parent = new Map(nodes.map(n => [n.id, n.id]));
70
+ function find(x) {
71
+ if (parent.get(x) !== x) parent.set(x, find(parent.get(x)));
72
+ return parent.get(x);
73
+ }
74
+ function union(a, b) { parent.set(find(a), find(b)); }
75
+ edges.forEach(e => { if (parent.has(e.source) && parent.has(e.target)) union(e.source, e.target); });
76
+ const groups = new Map();
77
+ nodes.forEach(n => {
78
+ const root = find(n.id);
79
+ if (!groups.has(root)) groups.set(root, []);
80
+ groups.get(root).push(n);
81
+ });
82
+ return [...groups.values()].sort((a, b) => b.length - a.length);
83
+ }
84
+
49
85
  function forceLayout(nodes, edges) {
50
86
  if (!nodes.length) return {};
51
87
  const pos = {};
52
88
  const angle0 = -Math.PI / 2;
53
89
 
54
- // Seed in a circle
55
- nodes.forEach((n, i) => {
56
- const a = angle0 + (i / nodes.length) * Math.PI * 2;
57
- pos[n.id] = {
58
- x: W / 2 + Math.cos(a) * W * 0.38,
59
- y: H / 2 + Math.sin(a) * H * 0.33,
60
- };
90
+ // Assign each component its own center so components don't overlap
91
+ const components = connectedComponents(nodes, edges);
92
+ const nComp = components.length;
93
+ components.forEach((comp, ci) => {
94
+ const a = angle0 + (ci / nComp) * Math.PI * 2;
95
+ const cx = nComp === 1 ? W / 2 : W / 2 + Math.cos(a) * W * 0.28;
96
+ const cy = nComp === 1 ? H / 2 : H / 2 + Math.sin(a) * H * 0.24;
97
+ const r = Math.min(60, 18 * Math.sqrt(comp.length));
98
+ comp.forEach((n, i) => {
99
+ const a2 = angle0 + (i / Math.max(comp.length, 1)) * Math.PI * 2;
100
+ pos[n.id] = { x: cx + Math.cos(a2) * r, y: cy + Math.sin(a2) * r };
101
+ });
61
102
  });
62
103
 
63
- const k = Math.sqrt((W * H) / Math.max(nodes.length, 1)) * 0.75;
104
+ const k = Math.sqrt((W * H) / Math.max(nodes.length, 1)) * 0.65;
64
105
 
65
106
  for (let iter = 0; iter < ITERS; iter++) {
66
107
  const disp = {};
67
108
  nodes.forEach((n) => { disp[n.id] = { x: 0, y: 0 }; });
68
109
 
69
- // Repulsion
110
+ // Repulsion (only within same component to avoid inter-component mixing)
111
+ const compOf = new Map();
112
+ components.forEach((comp, ci) => comp.forEach(n => compOf.set(n.id, ci)));
70
113
  for (let i = 0; i < nodes.length; i++) {
71
114
  for (let j = i + 1; j < nodes.length; j++) {
72
115
  const a = nodes[i], b = nodes[j];
116
+ if (compOf.get(a.id) !== compOf.get(b.id)) continue;
73
117
  const dx = pos[a.id].x - pos[b.id].x;
74
118
  const dy = pos[a.id].y - pos[b.id].y;
75
119
  const d = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
@@ -94,6 +138,22 @@ function forceLayout(nodes, edges) {
94
138
  disp[e.target].y += (dy / d) * f;
95
139
  });
96
140
 
141
+ // Gravity toward each component's assigned center
142
+ const compCenters = new Map();
143
+ components.forEach((comp, ci) => {
144
+ const a = angle0 + (ci / nComp) * Math.PI * 2;
145
+ compCenters.set(ci, {
146
+ cx: nComp === 1 ? W / 2 : W / 2 + Math.cos(a) * W * 0.28,
147
+ cy: nComp === 1 ? H / 2 : H / 2 + Math.sin(a) * H * 0.24,
148
+ });
149
+ });
150
+ nodes.forEach((n) => {
151
+ const ci = compOf.get(n.id);
152
+ const { cx, cy } = compCenters.get(ci);
153
+ disp[n.id].x += (cx - pos[n.id].x) * 0.06;
154
+ disp[n.id].y += (cy - pos[n.id].y) * 0.06;
155
+ });
156
+
97
157
  const temp = k * Math.max(0.01, 1 - iter / ITERS) * 0.7;
98
158
  nodes.forEach((n) => {
99
159
  const d = Math.sqrt(disp[n.id].x ** 2 + disp[n.id].y ** 2);
@@ -435,11 +495,31 @@ export function ClassGraph({ classData, onSelectClass, selectedClassId, focusedP
435
495
  ...classCallEdges,
436
496
  ], [inheritanceEdges, classCallEdges]);
437
497
 
438
- const classKey = allClasses.map(c => c.id).join('|');
498
+ // Only include nodes that participate in at least one edge — isolated nodes
499
+ // flood the canvas and hit the clamping boundary when there are many of them.
500
+ const { connectedClasses, isolatedCount } = useMemo(() => {
501
+ const connected = new Set();
502
+ for (const e of layoutEdges) { connected.add(e.source); connected.add(e.target); }
503
+ const filtered = allClasses.filter(c => connected.has(c.id));
504
+ // Fall back to all nodes if nothing has cross-class edges (avoids blank canvas)
505
+ const connectedClasses = filtered.length > 0 ? filtered : allClasses;
506
+ return { connectedClasses, isolatedCount: allClasses.length - connectedClasses.length };
507
+ }, [allClasses, layoutEdges]);
508
+
509
+ const classKey = connectedClasses.map(c => c.id).join('|');
439
510
  const edgeCount = layoutEdges.length;
511
+
512
+ const compIndexMap = useMemo(() => {
513
+ const map = new Map();
514
+ connectedComponents(connectedClasses, layoutEdges).forEach((comp, ci) => {
515
+ comp.forEach(n => map.set(n.id, ci));
516
+ });
517
+ return map;
518
+ }, [classKey, edgeCount]);
519
+
440
520
  const pos = useMemo(
441
- () => forceLayout(allClasses, layoutEdges),
442
- [classKey, edgeCount], // stable keys — intentionally omit allClasses/layoutEdges refs
521
+ () => forceLayout(connectedClasses, layoutEdges),
522
+ [classKey, edgeCount],
443
523
  );
444
524
 
445
525
  const showTip = useCallback((e, lines) => {
@@ -497,6 +577,7 @@ export function ClassGraph({ classData, onSelectClass, selectedClassId, focusedP
497
577
 
498
578
  const classCount = serverClasses.length;
499
579
  const moduleCount = moduleNodes.length;
580
+ const allLangsSame = new Set(allClasses.map(c => c.language)).size <= 1;
500
581
 
501
582
  return (
502
583
  <div style={{ position: 'relative', width: '100%', height: '100%' }}>
@@ -510,6 +591,7 @@ export function ClassGraph({ classData, onSelectClass, selectedClassId, focusedP
510
591
  <span>{moduleCount} modules</span>
511
592
  {inheritanceEdges.length > 0 && <span>{inheritanceEdges.length} inheritance</span>}
512
593
  <span>{classCallEdges.length} cross-module calls</span>
594
+ {isolatedCount > 0 && <span style={{ color: 'var(--tx-dim)' }}>{isolatedCount} isolated hidden</span>}
513
595
  <span style={{ color: 'var(--tx-dim)' }}>hover for details · click to expand</span>
514
596
  </div>
515
597
 
@@ -615,12 +697,13 @@ export function ClassGraph({ classData, onSelectClass, selectedClassId, focusedP
615
697
  })}
616
698
 
617
699
  {/* ── Nodes ─────────────────────────────────────────────────────── */}
618
- {allClasses.map(cls => {
700
+ {connectedClasses.map(cls => {
619
701
  const p = pos[cls.id];
620
702
  if (!p) return null;
621
703
  const isExpanded = expanded.has(cls.id);
622
704
  const methods = cls.methodIds.map(id => fnMap.get(id)).filter(Boolean);
623
- const color = langColor(cls.language);
705
+ const compIdx = compIndexMap.get(cls.id) ?? 0;
706
+ const color = allLangsSame ? componentColor(compIdx) : langColor(cls.language);
624
707
  const isFocused = focusedPathSet.has(cls.filePath);
625
708
  const hasFocus = focusedPathSet.size > 0;
626
709
  const isDimmed = hasFocus && !isFocused;
@@ -151,10 +151,10 @@ export function FlatGraph({
151
151
  y1={s.y + ny * nr}
152
152
  x2={t.x - nx * (nr + 5)}
153
153
  y2={t.y - ny * (nr + 5)}
154
- stroke={isSel ? 'var(--ac-primary)' : isAff ? '#f77c6a' : e.isType ? 'var(--ac-edge-type)' : 'var(--bd-edge)'}
154
+ stroke={isSel ? 'var(--ac-primary)' : isAff ? '#f77c6a' : e.isType ? 'var(--ac-edge-type)' : e.isCall ? 'var(--lc-cyan)' : 'var(--bd-edge)'}
155
155
  strokeWidth={isSel ? 1.5 : isAff ? 1.2 : 0.8}
156
- strokeOpacity={isDimEdge ? 0.08 : isSel ? 0.9 : isAff ? 0.7 : e.isType ? 0.35 : 0.55}
157
- strokeDasharray={e.isType ? '4 2' : undefined}
156
+ strokeOpacity={isDimEdge ? 0.08 : isSel ? 0.9 : isAff ? 0.7 : e.isType ? 0.35 : e.isCall ? 0.45 : 0.55}
157
+ strokeDasharray={e.isType ? '4 2' : e.isCall ? '6 3' : undefined}
158
158
  markerEnd={
159
159
  isSel
160
160
  ? 'url(#arr-sel)'
@@ -78,6 +78,7 @@ export function parseGraph(raw, palette = CLUSTER_PALETTE) {
78
78
  source: e.source,
79
79
  target: e.target,
80
80
  isType: e.isTypeOnly || false,
81
+ isCall: e.isCallEdge || false,
81
82
  importedNames: e.importedNames || [],
82
83
  }));
83
84
 
@@ -209,6 +210,13 @@ export function computeLayout(nodes, edges, W = 900, H = 540) {
209
210
  disp[e.target].y += (dy / d) * f;
210
211
  });
211
212
 
213
+ // Gravity toward center
214
+ const gravity = 0.04;
215
+ nodes.forEach((n) => {
216
+ disp[n.id].x += (W / 2 - pos[n.id].x) * gravity;
217
+ disp[n.id].y += (H / 2 - pos[n.id].y) * gravity;
218
+ });
219
+
212
220
  const temp = k * Math.max(0.05, 1 - iter / 80) * 0.5;
213
221
  nodes.forEach((n) => {
214
222
  const d = Math.sqrt(disp[n.id].x ** 2 + disp[n.id].y ** 2);