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.
- package/README.md +80 -20
- package/dist/api/generate.d.ts.map +1 -1
- package/dist/api/generate.js +52 -35
- package/dist/api/generate.js.map +1 -1
- package/dist/api/run.d.ts.map +1 -1
- package/dist/api/run.js +5 -3
- package/dist/api/run.js.map +1 -1
- package/dist/api/types.d.ts +4 -4
- package/dist/api/types.d.ts.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +29 -14
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +5 -3
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
- package/dist/core/analyzer/artifact-generator.js +11 -3
- package/dist/core/analyzer/artifact-generator.js.map +1 -1
- package/dist/core/analyzer/call-graph.d.ts +2 -2
- package/dist/core/analyzer/call-graph.d.ts.map +1 -1
- package/dist/core/analyzer/call-graph.js +137 -6
- package/dist/core/analyzer/call-graph.js.map +1 -1
- package/dist/core/analyzer/dependency-graph.d.ts +19 -0
- package/dist/core/analyzer/dependency-graph.d.ts.map +1 -1
- package/dist/core/analyzer/dependency-graph.js +76 -0
- package/dist/core/analyzer/dependency-graph.js.map +1 -1
- package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -1
- package/dist/core/analyzer/duplicate-detector.js +7 -1
- package/dist/core/analyzer/duplicate-detector.js.map +1 -1
- package/dist/core/analyzer/signature-extractor.d.ts.map +1 -1
- package/dist/core/analyzer/signature-extractor.js +59 -0
- package/dist/core/analyzer/signature-extractor.js.map +1 -1
- package/dist/core/generator/openspec-format-generator.d.ts +17 -2
- package/dist/core/generator/openspec-format-generator.d.ts.map +1 -1
- package/dist/core/generator/openspec-format-generator.js +111 -10
- package/dist/core/generator/openspec-format-generator.js.map +1 -1
- package/dist/core/generator/rag-manifest-generator.d.ts +37 -0
- package/dist/core/generator/rag-manifest-generator.d.ts.map +1 -0
- package/dist/core/generator/rag-manifest-generator.js +134 -0
- package/dist/core/generator/rag-manifest-generator.js.map +1 -0
- package/dist/core/services/llm-service.d.ts +23 -1
- package/dist/core/services/llm-service.d.ts.map +1 -1
- package/dist/core/services/llm-service.js +94 -2
- package/dist/core/services/llm-service.js.map +1 -1
- package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/orient.js +108 -1
- package/dist/core/services/mcp-handlers/orient.js.map +1 -1
- package/dist/core/services/mcp-watcher.js +1 -1
- package/dist/core/services/mcp-watcher.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/viewer/InteractiveGraphViewer.jsx +6 -2
- package/src/viewer/components/ClassGraph.jsx +97 -14
- package/src/viewer/components/FlatGraph.jsx +3 -3
- 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
|
-
|
|
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',
|
|
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
|
-
//
|
|
55
|
-
nodes
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
442
|
-
[classKey, edgeCount],
|
|
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
|
-
{
|
|
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
|
|
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);
|