universal-ast-mapper 2.0.0 → 2.0.2

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 (75) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +261 -12
  3. package/dist/ai-refactor.js +185 -0
  4. package/dist/ai-testgen.js +105 -0
  5. package/dist/analysis.js +134 -0
  6. package/dist/arch-rules.js +82 -0
  7. package/dist/callgraph.js +467 -0
  8. package/dist/check.js +112 -0
  9. package/dist/cli.js +2284 -0
  10. package/dist/complexity.js +98 -0
  11. package/dist/config.js +53 -0
  12. package/dist/contextpack.js +79 -0
  13. package/dist/coupling.js +35 -0
  14. package/dist/covmerge.js +176 -0
  15. package/dist/crosslang.js +425 -0
  16. package/dist/dashboard.js +259 -0
  17. package/dist/diagram.js +264 -0
  18. package/dist/diskcache.js +97 -0
  19. package/dist/docgen.js +156 -0
  20. package/dist/embeddings.js +136 -0
  21. package/dist/explain.js +123 -0
  22. package/dist/explorer.js +123 -0
  23. package/dist/extractors/c.js +204 -0
  24. package/dist/extractors/common.js +56 -0
  25. package/dist/extractors/cpp.js +272 -0
  26. package/dist/extractors/csharp.js +209 -0
  27. package/dist/extractors/go.js +212 -0
  28. package/dist/extractors/java.js +152 -0
  29. package/dist/extractors/kotlin.js +159 -0
  30. package/dist/extractors/php.js +208 -0
  31. package/dist/extractors/python.js +153 -0
  32. package/dist/extractors/ruby.js +146 -0
  33. package/dist/extractors/rust.js +249 -0
  34. package/dist/extractors/swift.js +192 -0
  35. package/dist/extractors/typescript.js +577 -0
  36. package/dist/fix.js +92 -0
  37. package/dist/gitdiff.js +178 -0
  38. package/dist/graph-analysis.js +279 -0
  39. package/dist/graph.js +165 -0
  40. package/dist/history.js +36 -0
  41. package/dist/html.js +658 -0
  42. package/dist/incremental.js +122 -0
  43. package/dist/index.js +1945 -0
  44. package/dist/indexstore.js +105 -0
  45. package/dist/layers.js +36 -0
  46. package/dist/lsp.js +238 -0
  47. package/dist/modulecoupling.js +0 -0
  48. package/dist/parser.js +84 -0
  49. package/dist/patch.js +199 -0
  50. package/dist/plugins.js +88 -0
  51. package/dist/pool.js +114 -0
  52. package/dist/prompts.js +67 -0
  53. package/dist/registry.js +87 -0
  54. package/dist/report.js +441 -0
  55. package/dist/resolver.js +222 -0
  56. package/dist/roots.js +47 -0
  57. package/dist/search.js +68 -0
  58. package/dist/security.js +178 -0
  59. package/dist/semantic.js +365 -0
  60. package/dist/serve.js +328 -0
  61. package/dist/sfc.js +27 -0
  62. package/dist/similar.js +98 -0
  63. package/dist/skeleton.js +132 -0
  64. package/dist/smells.js +285 -0
  65. package/dist/sourcemap.js +60 -0
  66. package/dist/testgen.js +280 -0
  67. package/dist/testmap.js +167 -0
  68. package/dist/tsconfig.js +212 -0
  69. package/dist/typeflow.js +124 -0
  70. package/dist/types.js +5 -0
  71. package/dist/unused-params.js +127 -0
  72. package/dist/webapp.js +646 -0
  73. package/dist/worker.js +27 -0
  74. package/dist/workspace.js +330 -0
  75. package/package.json +2 -1
package/dist/webapp.js ADDED
@@ -0,0 +1,646 @@
1
+ /** Generate the self-contained SPA HTML served by `ast-map serve`. */
2
+ export function webAppHtml(port) {
3
+ return `<!DOCTYPE html>
4
+ <html lang="en">
5
+ <head>
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <title>AST Map — Live Dashboard</title>
9
+ <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
10
+ <style>
11
+ :root {
12
+ --bg: #0f1117; --surface: #1a1d27; --border: #2d3142;
13
+ --text: #e2e8f0; --muted: #94a3b8; --accent: #7c3aed;
14
+ --green: #22c55e; --yellow: #eab308; --red: #ef4444; --blue: #3b82f6;
15
+ }
16
+ * { box-sizing: border-box; margin: 0; padding: 0; }
17
+ body { background: var(--bg); color: var(--text); font-family: system-ui, sans-serif; display: flex; height: 100vh; overflow: hidden; }
18
+ #sidebar { width: 220px; background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; padding: 16px 0; flex-shrink: 0; }
19
+ .logo { padding: 0 16px 16px; font-size: 14px; font-weight: 700; color: var(--accent); letter-spacing: 1px; border-bottom: 1px solid var(--border); }
20
+ .logo span { color: var(--muted); font-weight: 400; }
21
+ .nav-item { padding: 10px 16px; cursor: pointer; font-size: 13px; color: var(--muted); transition: all .15s; border-left: 3px solid transparent; }
22
+ .nav-item:hover { color: var(--text); background: rgba(124,58,237,.1); }
23
+ .nav-item.active { color: var(--text); border-left-color: var(--accent); background: rgba(124,58,237,.15); }
24
+ .nav-section { padding: 12px 16px 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-top: 8px; }
25
+ #main { flex: 1; overflow-y: auto; padding: 24px; }
26
+ .page { display: none; } .page.active { display: block; }
27
+ h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
28
+ .subtitle { color: var(--muted); font-size: 13px; margin-bottom: 20px; }
29
+ .grid { display: grid; gap: 16px; }
30
+ .grid-4 { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
31
+ .grid-2 { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); }
32
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
33
+ .stat-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 6px; }
34
+ .stat-value { font-size: 28px; font-weight: 700; line-height: 1; }
35
+ .stat-sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
36
+ .score-ring { width: 80px; height: 80px; margin: 0 auto 8px; }
37
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
38
+ .badge-green { background: rgba(34,197,94,.15); color: var(--green); }
39
+ .badge-yellow { background: rgba(234,179,8,.15); color: var(--yellow); }
40
+ .badge-red { background: rgba(239,68,68,.15); color: var(--red); }
41
+ .badge-blue { background: rgba(59,130,246,.15); color: var(--blue); }
42
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
43
+ th { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--border); color: var(--muted); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; }
44
+ td { padding: 8px 12px; border-bottom: 1px solid rgba(45,49,66,.5); }
45
+ tr:hover td { background: rgba(124,58,237,.05); }
46
+ .search { width: 100%; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; color: var(--text); font-size: 13px; margin-bottom: 16px; outline: none; }
47
+ .search:focus { border-color: var(--accent); }
48
+ #graph-canvas { width: 100%; height: calc(100vh - 140px); background: var(--surface); border: 1px solid var(--border); border-radius: 8px; }
49
+ .tooltip { position: fixed; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; font-size: 12px; pointer-events: none; z-index: 9999; max-width: 260px; display: none; }
50
+ .sparkline { display: inline-block; width: 80px; height: 24px; vertical-align: middle; }
51
+ .refresh-btn { margin-left: auto; padding: 6px 14px; background: var(--accent); border: none; border-radius: 6px; color: #fff; font-size: 12px; cursor: pointer; }
52
+ .refresh-btn:hover { opacity: .85; }
53
+ .header-row { display: flex; align-items: center; margin-bottom: 16px; }
54
+ .pill { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 11px; background: var(--border); color: var(--muted); margin-left: 6px; }
55
+ .error-box { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.3); border-radius: 6px; padding: 12px 16px; color: var(--red); font-size: 13px; margin-top: 8px; }
56
+ .loading { color: var(--muted); font-size: 13px; padding: 32px; text-align: center; }
57
+ .timeline { height: 120px; width: 100%; }
58
+ .run-layout { display: grid; grid-template-columns: 260px 1fr; gap: 16px; height: calc(100vh - 120px); }
59
+ .cmd-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow-y: auto; padding: 10px; }
60
+ .cmd-group-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); padding: 10px 6px 4px; }
61
+ .cmd-btn { width: 100%; text-align: left; padding: 8px 10px; border: none; border-radius: 6px; background: transparent; color: var(--text); font-size: 13px; cursor: pointer; display: block; margin-bottom: 1px; }
62
+ .cmd-btn:hover { background: rgba(124,58,237,.15); color: var(--accent); }
63
+ .cmd-form { padding: 4px 10px 8px; display: none; }
64
+ .cmd-form.open { display: block; }
65
+ .cmd-input { width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 5px 8px; color: var(--text); font-size: 12px; margin-bottom: 4px; outline: none; }
66
+ .cmd-input:focus { border-color: var(--accent); }
67
+ .cmd-run-btn { padding: 5px 14px; background: var(--accent); border: none; border-radius: 4px; color: #fff; font-size: 12px; cursor: pointer; }
68
+ .result-panel { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
69
+ .tab-bar { display: flex; border-bottom: 1px solid var(--border); overflow-x: auto; flex-shrink: 0; background: var(--bg); min-height: 36px; }
70
+ .tab { display: flex; align-items: center; gap: 6px; padding: 8px 14px; font-size: 12px; cursor: pointer; border-right: 1px solid var(--border); white-space: nowrap; color: var(--muted); user-select: none; }
71
+ .tab.active { background: var(--surface); color: var(--text); border-bottom: 2px solid var(--accent); margin-bottom: -1px; }
72
+ .tab-close { opacity: .5; line-height: 1; }
73
+ .tab-close:hover { opacity: 1; }
74
+ .tab-content { flex: 1; overflow-y: auto; padding: 16px; }
75
+ .tab-pane { display: none; }
76
+ .tab-pane.active { display: block; }
77
+ .result-pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 12px; font-family: monospace; font-size: 12px; white-space: pre-wrap; overflow-x: auto; max-height: 500px; overflow-y: auto; }
78
+ .empty-tabs { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 13px; }
79
+ .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .6s linear infinite; vertical-align: middle; margin-right: 6px; }
80
+ @keyframes spin { to { transform: rotate(360deg); } }
81
+ </style>
82
+ </head>
83
+ <body>
84
+ <div id="sidebar">
85
+ <div class="logo">AST Map <span>v2</span></div>
86
+ <div class="nav-section">Overview</div>
87
+ <div class="nav-item active" data-page="overview">📊 Dashboard</div>
88
+ <div class="nav-item" data-page="timeline">📈 History</div>
89
+ <div class="nav-section">Analysis</div>
90
+ <div class="nav-item" data-page="files">📁 Files</div>
91
+ <div class="nav-item" data-page="symbols">🔷 Symbols</div>
92
+ <div class="nav-item" data-page="deps">🕸️ Dependency Graph</div>
93
+ <div class="nav-section">Issues</div>
94
+ <div class="nav-item" data-page="smells">🤢 Code Smells</div>
95
+ <div class="nav-item" data-page="security">🔒 Security</div>
96
+ <div class="nav-item" data-page="dead">💀 Dead Code</div>
97
+ <div class="nav-section">Commands</div>
98
+ <div class="nav-item" data-page="run">⚡ Run Commands</div>
99
+ </div>
100
+
101
+ <div id="main">
102
+ <!-- OVERVIEW -->
103
+ <div class="page active" id="page-overview">
104
+ <div class="header-row"><h1>Dashboard</h1><button class="refresh-btn" onclick="loadAll()">↺ Refresh</button></div>
105
+ <div class="subtitle" id="root-label">Loading…</div>
106
+ <div class="grid grid-4" id="stat-cards" style="margin-bottom:20px"></div>
107
+ <div class="grid grid-2">
108
+ <div class="card"><div class="stat-label">Top imported symbols</div><div id="top-syms"></div></div>
109
+ <div class="card"><div class="stat-label">Recent issues</div><div id="recent-issues"></div></div>
110
+ </div>
111
+ </div>
112
+
113
+ <!-- HISTORY -->
114
+ <div class="page" id="page-timeline">
115
+ <h1>Health Score History</h1>
116
+ <div class="subtitle">Score trend over time</div>
117
+ <div class="card" style="margin-bottom:16px"><svg class="timeline" id="timeline-svg"></svg></div>
118
+ <div class="card"><table><thead><tr><th>Date</th><th>Score</th><th>Grade</th><th>Files</th><th>Dead</th><th>Cycles</th></tr></thead><tbody id="history-table"></tbody></table></div>
119
+ </div>
120
+
121
+ <!-- FILES -->
122
+ <div class="page" id="page-files">
123
+ <h1>Files</h1>
124
+ <input class="search" id="file-search" placeholder="Filter files…" oninput="filterFiles()">
125
+ <div class="card"><table><thead><tr><th>File</th><th>Lang</th><th>Symbols</th><th>Lines</th></tr></thead><tbody id="file-table"></tbody></table></div>
126
+ </div>
127
+
128
+ <!-- SYMBOLS -->
129
+ <div class="page" id="page-symbols">
130
+ <h1>Symbols</h1>
131
+ <input class="search" id="sym-search" placeholder="Search symbols…" oninput="filterSymbols()">
132
+ <div class="card"><table><thead><tr><th>Symbol</th><th>Kind</th><th>File</th><th>Exported</th></tr></thead><tbody id="sym-table"></tbody></table></div>
133
+ </div>
134
+
135
+ <!-- DEPS GRAPH -->
136
+ <div class="page" id="page-deps">
137
+ <h1>Dependency Graph</h1>
138
+ <div class="subtitle">File-level import relationships — drag to explore</div>
139
+ <svg id="graph-canvas"></svg>
140
+ </div>
141
+
142
+ <!-- SMELLS -->
143
+ <div class="page" id="page-smells">
144
+ <h1>Code Smells</h1>
145
+ <div id="smells-content"></div>
146
+ </div>
147
+
148
+ <!-- SECURITY -->
149
+ <div class="page" id="page-security">
150
+ <h1>Security Issues</h1>
151
+ <div id="security-content"></div>
152
+ </div>
153
+
154
+ <!-- DEAD CODE -->
155
+ <div class="page" id="page-dead">
156
+ <h1>Dead Exports</h1>
157
+ <div class="subtitle">Exported symbols with no known importers inside the scanned directory</div>
158
+ <div class="card"><table><thead><tr><th>Symbol</th><th>Kind</th><th>File</th><th>Confidence</th></tr></thead><tbody id="dead-table"></tbody></table></div>
159
+ </div>
160
+
161
+ <!-- RUN COMMANDS -->
162
+ <div class="page" id="page-run">
163
+ <div class="header-row"><h1>Run Commands</h1></div>
164
+ <div class="subtitle">Interactive analysis — click a command to run it instantly</div>
165
+ <div class="run-layout">
166
+ <div class="cmd-panel" id="cmd-panel"></div>
167
+ <div class="result-panel">
168
+ <div class="tab-bar" id="tab-bar"></div>
169
+ <div class="tab-content" id="tab-content">
170
+ <div class="empty-tabs" id="empty-tabs">↑ Pick a command to run</div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+
177
+ <div class="tooltip" id="tooltip"></div>
178
+
179
+ <script>
180
+ const API = 'http://localhost:${port}/api';
181
+ let state = { report: null, graph: null, dead: [], history: [], skeletons: [], smells: [], security: [] };
182
+
183
+ // ─── Navigation ───────────────────────────────────────────────────────────────
184
+ document.querySelectorAll('.nav-item').forEach(el => {
185
+ el.addEventListener('click', () => {
186
+ document.querySelectorAll('.nav-item').forEach(e => e.classList.remove('active'));
187
+ document.querySelectorAll('.page').forEach(e => e.classList.remove('active'));
188
+ el.classList.add('active');
189
+ const page = document.getElementById('page-' + el.dataset.page);
190
+ if (page) { page.classList.add('active'); renderPage(el.dataset.page); }
191
+ });
192
+ });
193
+
194
+ // ─── Data loading ─────────────────────────────────────────────────────────────
195
+ async function fetchJson(path) {
196
+ const r = await fetch(API + path);
197
+ if (!r.ok) throw new Error(r.statusText);
198
+ return r.json();
199
+ }
200
+
201
+ async function loadAll() {
202
+ try {
203
+ const [report, graph, dead, history, skeletons, smells, security] = await Promise.all([
204
+ fetchJson('/report'), fetchJson('/graph'), fetchJson('/dead'),
205
+ fetchJson('/history'), fetchJson('/skeletons'), fetchJson('/smells'), fetchJson('/security'),
206
+ ]);
207
+ state = { report, graph, dead, history, skeletons, smells, security };
208
+ renderPage(document.querySelector('.nav-item.active')?.dataset.page ?? 'overview');
209
+ } catch(e) {
210
+ document.getElementById('root-label').textContent = 'Error: ' + e.message;
211
+ }
212
+ }
213
+
214
+ // ─── Renderers ────────────────────────────────────────────────────────────────
215
+ function renderPage(name) {
216
+ if (name === 'overview') renderOverview();
217
+ else if (name === 'timeline') renderTimeline();
218
+ else if (name === 'files') renderFiles();
219
+ else if (name === 'symbols') renderSymbols();
220
+ else if (name === 'deps') renderGraph();
221
+ else if (name === 'smells') renderSmells();
222
+ else if (name === 'security') renderSecurity();
223
+ else if (name === 'dead') renderDead();
224
+ else if (name === 'run') renderRun();
225
+ }
226
+
227
+ function grade(s) { return s >= 90 ? 'A' : s >= 80 ? 'B' : s >= 70 ? 'C' : s >= 60 ? 'D' : 'F'; }
228
+ function gradeClass(s) { return s >= 80 ? 'badge-green' : s >= 60 ? 'badge-yellow' : 'badge-red'; }
229
+
230
+ function renderOverview() {
231
+ const r = state.report;
232
+ if (!r) return;
233
+ document.getElementById('root-label').textContent = r.directory ?? '.';
234
+ const score = r.score ?? 0;
235
+ document.getElementById('stat-cards').innerHTML = [
236
+ { label: 'Health Score', value: score, sub: 'Grade ' + (r.grade ?? grade(score)), cls: gradeClass(score) },
237
+ { label: 'Files', value: r.files ?? 0, sub: '' },
238
+ { label: 'Symbols', value: r.symbols ?? 0, sub: '' },
239
+ { label: 'Dead Exports', value: r.deadExports ?? state.dead.length, sub: '', cls: (r.deadExports || state.dead.length) > 0 ? 'badge-yellow' : 'badge-green' },
240
+ { label: 'Circular Deps', value: r.cyclicGroups ?? 0, sub: '', cls: (r.cyclicGroups ?? 0) > 0 ? 'badge-red' : 'badge-green' },
241
+ { label: 'Max Complexity', value: r.maxComplexity ?? 0, sub: '', cls: (r.maxComplexity ?? 0) > 20 ? 'badge-red' : (r.maxComplexity ?? 0) > 10 ? 'badge-yellow' : 'badge-green' },
242
+ { label: 'Smells', value: state.smells.length, sub: '', cls: state.smells.length > 0 ? 'badge-yellow' : 'badge-green' },
243
+ { label: 'Security', value: state.security.length, sub: '', cls: state.security.length > 0 ? 'badge-red' : 'badge-green' },
244
+ ].map(s => \`<div class="card"><div class="stat-label">\${s.label}</div><div class="stat-value"><span class="badge \${s.cls || ''}">\${s.value}</span></div><div class="stat-sub">\${s.sub}</div></div>\`).join('');
245
+
246
+ const topNodes = (state.graph?.nodes ?? []).filter(n => n.nodeType === 'symbol').sort((a, b) => (b.inDegree ?? 0) - (a.inDegree ?? 0)).slice(0, 8);
247
+ document.getElementById('top-syms').innerHTML = topNodes.map(n =>
248
+ \`<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border)"><span>\${n.id?.split('::').pop() ?? n.id}</span><span class="badge badge-blue">\${n.inDegree ?? 0}</span></div>\`
249
+ ).join('') || '<div style="color:var(--muted);font-size:12px">No data</div>';
250
+
251
+ const recent = [...state.smells.slice(0, 3).map(s => ({ type: '🤢', msg: (s.symbol ?? s.smell), file: s.file })),
252
+ ...state.security.slice(0, 3).map(s => ({ type: '🔒', msg: s.rule, file: s.file }))];
253
+ document.getElementById('recent-issues').innerHTML = recent.map(i =>
254
+ \`<div style="display:flex;gap:8px;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border)"><span>\${i.type}</span><div><div>\${i.msg}</div><div style="color:var(--muted)">\${i.file}</div></div></div>\`
255
+ ).join('') || '<div style="color:var(--green);font-size:12px">No issues 🎉</div>';
256
+ }
257
+
258
+ function renderTimeline() {
259
+ const hist = state.history;
260
+ const tbody = document.getElementById('history-table');
261
+ tbody.innerHTML = [...hist].reverse().map(h =>
262
+ \`<tr><td>\${h.date}</td><td><span class="badge \${gradeClass(h.score)}">\${h.score}</span></td><td>\${h.grade}</td><td>\${h.files}</td><td>\${h.dead}</td><td>\${h.cycles}</td></tr>\`
263
+ ).join('');
264
+
265
+ if (hist.length < 2) return;
266
+ const svg = document.getElementById('timeline-svg');
267
+ const W = svg.clientWidth || 600, H = 120, M = { t: 10, r: 20, b: 30, l: 40 };
268
+ const iW = W - M.l - M.r, iH = H - M.t - M.b;
269
+ const xs = d3.scalePoint().domain(hist.map(h => h.date)).range([0, iW]);
270
+ const ys = d3.scaleLinear().domain([0, 100]).range([iH, 0]);
271
+ const line = d3.line().x(h => xs(h.date) ?? 0).y(h => ys(h.score));
272
+ svg.innerHTML = \`<g transform="translate(\${M.l},\${M.t})">
273
+ <g transform="translate(0,\${iH})">\${d3.axisBottom(xs).ticks(5)(d3.select(document.createElementNS('http://www.w3.org/2000/svg','g'))?.node?.() ?? document.createElementNS('http://www.w3.org/2000/svg','g'))?.outerHTML ?? ''}</g>
274
+ <path d="\${line(hist)}" fill="none" stroke="#7c3aed" stroke-width="2"/>
275
+ \${hist.map(h => \`<circle cx="\${xs(h.date)}" cy="\${ys(h.score)}" r="4" fill="#7c3aed"/>\`).join('')}
276
+ </g>\`;
277
+ }
278
+
279
+ let allFiles = [];
280
+ function renderFiles() {
281
+ allFiles = state.skeletons;
282
+ filterFiles();
283
+ }
284
+ function filterFiles() {
285
+ const q = document.getElementById('file-search')?.value?.toLowerCase() ?? '';
286
+ const rows = allFiles.filter(s => !q || s.file.toLowerCase().includes(q));
287
+ document.getElementById('file-table').innerHTML = rows.map(s =>
288
+ \`<tr><td style="font-family:monospace">\${s.file}</td><td><span class="pill">\${s.language}</span></td><td>\${s.symbolCount ?? s.symbols?.length ?? 0}</td><td>\${s.lineCount ?? '?'}</td></tr>\`
289
+ ).join('');
290
+ }
291
+
292
+ let allSymbols = [];
293
+ function renderSymbols() {
294
+ allSymbols = state.skeletons.flatMap(s => flattenSyms(s.symbols, s.file));
295
+ filterSymbols();
296
+ }
297
+ function flattenSyms(syms, file, out = []) {
298
+ for (const s of syms) { out.push({ ...s, file }); flattenSyms(s.children ?? [], file, out); }
299
+ return out;
300
+ }
301
+ function filterSymbols() {
302
+ const q = document.getElementById('sym-search')?.value?.toLowerCase() ?? '';
303
+ const rows = allSymbols.filter(s => !q || s.name.toLowerCase().includes(q) || s.kind.includes(q));
304
+ document.getElementById('sym-table').innerHTML = rows.slice(0, 200).map(s =>
305
+ \`<tr><td><b>\${esc(s.name)}</b></td><td><span class="pill">\${s.kind}</span></td><td style="font-family:monospace;font-size:11px">\${esc(s.file)}</td><td>\${s.exported ? '✓' : ''}</td></tr>\`
306
+ ).join('');
307
+ }
308
+
309
+ function renderGraph() {
310
+ const g = state.graph;
311
+ if (!g) return;
312
+ const svg = d3.select('#graph-canvas');
313
+ svg.selectAll('*').remove();
314
+ const W = document.getElementById('graph-canvas').clientWidth || 800;
315
+ const H = document.getElementById('graph-canvas').clientHeight || 500;
316
+ const fileNodes = g.nodes.filter(n => n.nodeType === 'file').slice(0, 60);
317
+ const nodeIds = new Set(fileNodes.map(n => n.id));
318
+ const links = g.edges.filter(e => e.edgeType === 'imports' && nodeIds.has(e.from) && nodeIds.has(e.to))
319
+ .map(e => ({ source: e.from, target: e.to }));
320
+
321
+ const sim = d3.forceSimulation(fileNodes)
322
+ .force('link', d3.forceLink(links).id(d => d.id).distance(80))
323
+ .force('charge', d3.forceManyBody().strength(-120))
324
+ .force('center', d3.forceCenter(W / 2, H / 2));
325
+
326
+ const link = svg.append('g').selectAll('line').data(links).join('line')
327
+ .attr('stroke', '#2d3142').attr('stroke-width', 1);
328
+ const node = svg.append('g').selectAll('circle').data(fileNodes).join('circle')
329
+ .attr('r', 6).attr('fill', '#7c3aed').attr('cursor', 'pointer')
330
+ .call(d3.drag().on('start', e => { if (!e.active) sim.alphaTarget(.3).restart(); e.subject.fx = e.subject.x; e.subject.fy = e.subject.y; })
331
+ .on('drag', e => { e.subject.fx = e.x; e.subject.fy = e.y; })
332
+ .on('end', e => { if (!e.active) sim.alphaTarget(0); e.subject.fx = null; e.subject.fy = null; }))
333
+ .on('mouseover', (ev, d) => { const t = document.getElementById('tooltip'); t.style.display='block'; t.style.left=ev.clientX+12+'px'; t.style.top=ev.clientY+'px'; t.textContent=d.id; })
334
+ .on('mouseout', () => { document.getElementById('tooltip').style.display='none'; });
335
+
336
+ sim.on('tick', () => {
337
+ link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
338
+ .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
339
+ node.attr('cx', d => d.x).attr('cy', d => d.y);
340
+ });
341
+ }
342
+
343
+ function renderSmells() {
344
+ const s = state.smells;
345
+ document.getElementById('smells-content').innerHTML = s.length === 0
346
+ ? '<div class="card" style="color:var(--green)">No smells detected 🎉</div>'
347
+ : \`<div class="card"><table><thead><tr><th>Smell</th><th>Symbol</th><th>File</th><th>Line</th><th>Severity</th></tr></thead><tbody>\${s.map(i =>
348
+ \`<tr><td><span class="badge badge-yellow">\${esc(i.smell)}</span></td><td>\${esc(i.symbol??'')}</td><td style="font-size:11px">\${esc(i.file)}</td><td>\${i.line??''}</td><td>\${i.severity}</td></tr>\`
349
+ ).join('')}</tbody></table></div>\`;
350
+ }
351
+
352
+ function renderSecurity() {
353
+ const s = state.security;
354
+ document.getElementById('security-content').innerHTML = s.length === 0
355
+ ? '<div class="card" style="color:var(--green)">No security issues detected 🎉</div>'
356
+ : \`<div class="card"><table><thead><tr><th>Rule</th><th>Severity</th><th>File</th><th>Line</th><th>Message</th></tr></thead><tbody>\${s.map(i =>
357
+ \`<tr><td><span class="badge \${i.severity==='critical'||i.severity==='high'?'badge-red':'badge-yellow'}">\${esc(i.rule)}</span></td><td>\${esc(i.severity)}</td><td style="font-size:11px">\${esc(i.file)}</td><td>\${i.line}</td><td style="font-size:11px">\${esc(i.message)}</td></tr>\`
358
+ ).join('')}</tbody></table></div>\`;
359
+ }
360
+
361
+ function renderDead() {
362
+ const d = state.dead;
363
+ document.getElementById('dead-table').innerHTML = d.map(i =>
364
+ \`<tr><td><b>\${esc(i.symbol)}</b></td><td><span class="pill">\${esc(i.kind)}</span></td><td style="font-family:monospace;font-size:11px">\${esc(i.file)}</td><td><span class="badge \${i.confidence==='high'?'badge-red':'badge-yellow'}">\${esc(i.confidence)}</span></td></tr>\`
365
+ ).join('');
366
+ }
367
+
368
+ function esc(s) { return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
369
+
370
+ // ─── Run Commands ─────────────────────────────────────────────────────────────
371
+ var CMDS = [
372
+ { group: 'Analysis', items: [
373
+ { id: 'dead', label: '💀 Dead Exports', desc: 'Find unused exported symbols' },
374
+ { id: 'cycles', label: '🔄 Circular Deps', desc: 'Circular dependency chains' },
375
+ { id: 'duplicates', label: '♊ Duplicates', desc: 'Symbols defined more than once' },
376
+ { id: 'similar', label: '🔍 Similar Code', desc: 'Structurally similar functions' },
377
+ { id: 'complexity', label: '📊 Complexity', desc: 'Cyclomatic complexity per file' },
378
+ { id: 'top', label: '🏆 Top Symbols', desc: 'Most imported symbols' }
379
+ ]},
380
+ { group: 'Quality', items: [
381
+ { id: 'smells', label: '🤢 Code Smells', desc: 'Detect anti-patterns' },
382
+ { id: 'security', label: '🔒 Security Scan', desc: 'Security vulnerabilities' },
383
+ { id: 'arch', label: '🏛️ Arch Rules', desc: 'Architecture rule violations' }
384
+ ]},
385
+ { group: 'Search & Explore', items: [
386
+ { id: 'find', label: '🔎 Find Symbol', desc: 'Search symbols by name', fields: [
387
+ { name: 'query', ph: 'Symbol name…', req: true },
388
+ { name: 'kind', ph: 'Kind filter (fn, class…)', req: false }
389
+ ]},
390
+ { id: 'impact', label: '💥 Change Impact', desc: 'Blast radius of changing a symbol', fields: [
391
+ { name: 'symbol', ph: 'file.ts::SymbolName', req: true }
392
+ ]},
393
+ { id: 'fileDeps', label: '📦 File Deps', desc: 'What a file imports and who imports it', fields: [
394
+ { name: 'file', ph: 'src/foo.ts', req: true }
395
+ ]},
396
+ { id: 'explain', label: '💡 Explain Symbol', desc: 'Full structural context of a symbol', fields: [
397
+ { name: 'file', ph: 'src/foo.ts', req: true },
398
+ { name: 'symbol', ph: 'SymbolName', req: true }
399
+ ]}
400
+ ]},
401
+ { group: 'Generate', items: [
402
+ { id: 'diagram', label: '🕸️ Diagram', desc: 'Mermaid diagram of the codebase', fields: [
403
+ { name: 'type', ph: 'deps | class | modules', req: false }
404
+ ]},
405
+ { id: 'doc', label: '📝 Docs', desc: 'Generate Markdown documentation' }
406
+ ]}
407
+ ];
408
+
409
+ var _tabCount = 0;
410
+ var _runInit = false;
411
+
412
+ function renderRun() {
413
+ if (_runInit) return;
414
+ _runInit = true;
415
+ var panel = document.getElementById('cmd-panel');
416
+ var html = '';
417
+ CMDS.forEach(function(g) {
418
+ html += '<div class="cmd-group-label">' + g.group + '</div>';
419
+ g.items.forEach(function(cmd) {
420
+ html += '<button class="cmd-btn" title="' + esc(cmd.desc) + '" onclick="toggleForm(\'' + cmd.id + '\')">' + cmd.label + '</button>';
421
+ if (cmd.fields) {
422
+ html += '<div class="cmd-form" id="form-' + cmd.id + '">';
423
+ cmd.fields.forEach(function(f) {
424
+ html += '<input class="cmd-input" id="inp-' + cmd.id + '-' + f.name + '" placeholder="' + esc(f.ph) + '" onkeydown="if(event.key===\'Enter\')runCmd(\'' + cmd.id + '\')" />';
425
+ });
426
+ html += '<button class="cmd-run-btn" onclick="runCmd(\'' + cmd.id + '\')">Run ▶</button>';
427
+ html += '</div>';
428
+ }
429
+ });
430
+ });
431
+ panel.innerHTML = html;
432
+ }
433
+
434
+ function findCmd(id) {
435
+ for (var i = 0; i < CMDS.length; i++) {
436
+ for (var j = 0; j < CMDS[i].items.length; j++) {
437
+ if (CMDS[i].items[j].id === id) return CMDS[i].items[j];
438
+ }
439
+ }
440
+ return null;
441
+ }
442
+
443
+ function toggleForm(id) {
444
+ var cmd = findCmd(id);
445
+ if (!cmd || !cmd.fields) { runCmd(id); return; }
446
+ var form = document.getElementById('form-' + id);
447
+ if (form) form.classList.toggle('open');
448
+ }
449
+
450
+ async function runCmd(id) {
451
+ var cmd = findCmd(id);
452
+ if (!cmd) return;
453
+ var args = {};
454
+ if (cmd.fields) {
455
+ for (var i = 0; i < cmd.fields.length; i++) {
456
+ var f = cmd.fields[i];
457
+ var inp = document.getElementById('inp-' + id + '-' + f.name);
458
+ var v = inp ? inp.value.trim() : '';
459
+ if (f.req && !v) { alert(f.name + ' is required'); return; }
460
+ if (v) args[f.name] = v;
461
+ }
462
+ }
463
+ var firstArg = args.query || args.symbol || args.file || args.type || '';
464
+ var label = cmd.label + (firstArg ? ': ' + firstArg : '');
465
+ var tabId = ++_tabCount;
466
+ addTab(tabId, label, '<div class="loading"><span class="spinner"></span> Running…</div>');
467
+ try {
468
+ var r = await fetch('http://localhost:${port}/api/run', {
469
+ method: 'POST',
470
+ headers: { 'Content-Type': 'application/json' },
471
+ body: JSON.stringify({ cmd: id, args: args })
472
+ });
473
+ var json = await r.json();
474
+ if (!r.ok) throw new Error(json.error || r.statusText);
475
+ setTabContent(tabId, renderResult(id, json.data));
476
+ } catch(e) {
477
+ setTabContent(tabId, '<div class="error-box">' + esc(e.message) + '</div>');
478
+ }
479
+ }
480
+
481
+ function addTab(id, label, content) {
482
+ var emptyEl = document.getElementById('empty-tabs');
483
+ if (emptyEl) emptyEl.remove();
484
+ var bar = document.getElementById('tab-bar');
485
+ var tc = document.getElementById('tab-content');
486
+ bar.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
487
+ tc.querySelectorAll('.tab-pane').forEach(function(t) { t.classList.remove('active'); });
488
+ var tab = document.createElement('div');
489
+ tab.className = 'tab active';
490
+ tab.dataset.tab = id;
491
+ tab.innerHTML = '<span>' + esc(label) + '</span><span class="tab-close" onclick="closeTab(' + id + ',event)">✕</span>';
492
+ tab.addEventListener('click', function(e) { if (!e.target.classList.contains('tab-close')) switchTab(id); });
493
+ bar.appendChild(tab);
494
+ var pane = document.createElement('div');
495
+ pane.className = 'tab-pane active';
496
+ pane.id = 'pane-' + id;
497
+ pane.innerHTML = content;
498
+ tc.appendChild(pane);
499
+ }
500
+
501
+ function setTabContent(id, html) {
502
+ var pane = document.getElementById('pane-' + id);
503
+ if (pane) pane.innerHTML = html;
504
+ }
505
+
506
+ function switchTab(id) {
507
+ document.querySelectorAll('.tab').forEach(function(t) { t.classList.toggle('active', t.dataset.tab == id); });
508
+ document.querySelectorAll('.tab-pane').forEach(function(t) { t.classList.toggle('active', t.id === 'pane-' + id); });
509
+ }
510
+
511
+ function closeTab(id, e) {
512
+ e.stopPropagation();
513
+ var tab = document.querySelector('.tab[data-tab="' + id + '"]');
514
+ var pane = document.getElementById('pane-' + id);
515
+ var wasActive = tab && tab.classList.contains('active');
516
+ if (tab) tab.remove();
517
+ if (pane) pane.remove();
518
+ if (wasActive) {
519
+ var remaining = document.querySelectorAll('.tab');
520
+ if (remaining.length > 0) {
521
+ switchTab(remaining[remaining.length - 1].dataset.tab);
522
+ } else {
523
+ document.getElementById('tab-content').innerHTML = '<div class="empty-tabs" id="empty-tabs">↑ Pick a command to run</div>';
524
+ }
525
+ }
526
+ }
527
+
528
+ function renderResult(cmd, data) {
529
+ if (data == null) return '<div style="color:var(--muted)">No results</div>';
530
+ if (cmd === 'dead') {
531
+ if (!data.length) return '<div style="color:var(--green)">No dead exports 🎉</div>';
532
+ return renderTable(['Symbol','Kind','File','Confidence'], data, function(d) {
533
+ return [d.symbol, '<span class="pill">'+esc(d.kind)+'</span>', d.file, '<span class="badge '+(d.confidence==='high'?'badge-red':'badge-yellow')+'">'+d.confidence+'</span>'];
534
+ });
535
+ }
536
+ if (cmd === 'cycles') {
537
+ if (!data.length) return '<div style="color:var(--green)">No cycles 🎉</div>';
538
+ return data.map(function(c) { return '<div class="card" style="margin-bottom:8px"><b>Cycle:</b> <span style="font-family:monospace;font-size:12px">'+esc((c.cycle||[]).join(' → '))+'</span></div>'; }).join('');
539
+ }
540
+ if (cmd === 'duplicates') {
541
+ if (!data.length) return '<div style="color:var(--green)">No duplicates 🎉</div>';
542
+ return renderTable(['Symbol','Kind','Files'], data, function(d) {
543
+ return [d.name, '<span class="pill">'+esc(d.kind)+'</span>', (d.locations||[]).map(function(l){return l.file;}).join(', ')];
544
+ });
545
+ }
546
+ if (cmd === 'similar') {
547
+ if (!data.length) return '<div style="color:var(--green)">No similar groups 🎉</div>';
548
+ return data.map(function(g) {
549
+ var members = (g.members||[]).map(function(m) { return '<span style="font-family:monospace;font-size:11px">'+esc(m.name)+' <span style="color:var(--muted)">('+esc(m.file)+')</span></span>'; }).join(', ');
550
+ return '<div class="card" style="margin-bottom:8px"><b>'+esc(g.kind||'similar')+'</b> — '+members+'</div>';
551
+ }).join('');
552
+ }
553
+ if (cmd === 'complexity') {
554
+ return renderTable(['File','Functions','Max CC','Avg CC','Rating'], data, function(d) {
555
+ var r = d.rating||'';
556
+ var cls = (r==='high'||r==='very-high')?'badge-red':r==='moderate'?'badge-yellow':'badge-green';
557
+ return [d.file, (d.functions||[]).length, d.maxComplexity||0, Math.round(d.avgComplexity||0), '<span class="badge '+cls+'">'+r+'</span>'];
558
+ });
559
+ }
560
+ if (cmd === 'top') {
561
+ return renderTable(['Symbol','Kind','File','Imports'], data, function(d) {
562
+ return [d.symbol||d.id, '<span class="pill">'+esc(d.kind||'')+'</span>', d.file||'', '<span class="badge badge-blue">'+(d.inDegree||d.importCount||0)+'</span>'];
563
+ });
564
+ }
565
+ if (cmd === 'smells') {
566
+ if (!data.length) return '<div style="color:var(--green)">No smells 🎉</div>';
567
+ return renderTable(['Smell','Symbol','File','Line','Sev'], data, function(d) {
568
+ return ['<span class="badge badge-yellow">'+esc(d.smell)+'</span>', d.symbol||'', d.file, d.line||'', d.severity];
569
+ });
570
+ }
571
+ if (cmd === 'security') {
572
+ if (!data.length) return '<div style="color:var(--green)">No issues 🎉</div>';
573
+ return renderTable(['Rule','Sev','File','Line','Message'], data, function(d) {
574
+ var cls = (d.severity==='critical'||d.severity==='high')?'badge-red':'badge-yellow';
575
+ return ['<span class="badge '+cls+'">'+esc(d.rule)+'</span>', d.severity, d.file, d.line, d.message];
576
+ });
577
+ }
578
+ if (cmd === 'arch') {
579
+ if (!data.length) return '<div style="color:var(--green)">No violations 🎉</div>';
580
+ return renderTable(['Rule','From','To','Severity'], data, function(d) {
581
+ return [d.rule||d.description||'', d.from||'', d.to||'', '<span class="badge '+(d.severity==='error'?'badge-red':'badge-yellow')+'">'+d.severity+'</span>'];
582
+ });
583
+ }
584
+ if (cmd === 'find') {
585
+ if (!data.length) return '<div style="color:var(--muted)">No symbols found</div>';
586
+ return renderTable(['Symbol','Kind','File','Line','Exported'], data, function(d) {
587
+ return [d.name, '<span class="pill">'+esc(d.kind)+'</span>', d.file, d.line||'', d.exported?'✓':''];
588
+ });
589
+ }
590
+ if (cmd === 'impact') {
591
+ if (!data) return '<div style="color:var(--muted)">Symbol not found in graph</div>';
592
+ var header = '<div class="card" style="margin-bottom:8px"><b>Direct:</b> '+(data.direct||[]).length+' &nbsp; <b>Transitive:</b> '+(data.transitive||[]).length+' &nbsp; <b>Total files:</b> '+data.totalFiles+'</div>';
593
+ var allNodes = (data.direct||[]).map(function(n){return Object.assign({},n,{rel:'direct'});}).concat((data.transitive||[]).map(function(n){return Object.assign({},n,{rel:'transitive'});}));
594
+ return header + renderTable(['File','Symbol','Relation'], allNodes, function(d) {
595
+ return [d.file, d.symbol||'', '<span class="badge '+(d.rel==='direct'?'badge-blue':'badge-yellow')+'">'+d.rel+'</span>'];
596
+ });
597
+ }
598
+ if (cmd === 'fileDeps') {
599
+ if (!data) return '<div style="color:var(--muted)">File not found in graph</div>';
600
+ var imH = (data.imports||[]).map(function(f) { return '<div style="font-family:monospace;font-size:12px;padding:2px 0">'+esc(f.file)+'</div>'; }).join('');
601
+ var ibH = (data.importedBy||[]).map(function(f) { return '<div style="font-family:monospace;font-size:12px;padding:2px 0">'+esc(f.file)+'</div>'; }).join('');
602
+ return '<div class="card" style="margin-bottom:8px"><b>Imports:</b> '+(data.imports||[]).length+' &nbsp; <b>Imported by:</b> '+(data.importedBy||[]).length+'</div>'
603
+ + '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">'
604
+ + '<div><div class="stat-label" style="margin-bottom:6px">Imports</div>'+imH+'</div>'
605
+ + '<div><div class="stat-label" style="margin-bottom:6px">Imported by</div>'+ibH+'</div></div>';
606
+ }
607
+ if (cmd === 'explain') {
608
+ if (!data) return '<div style="color:var(--muted)">Symbol not found</div>';
609
+ var detail = JSON.stringify({ signature: data.signature, params: data.params, returnType: data.returnType, exported: data.exported, calls: (data.calls||[]).length, calledBy: (data.calledBy||[]).length }, null, 2);
610
+ return '<div class="card" style="margin-bottom:8px"><b>'+esc(data.symbol||'')+'</b> <span class="pill">'+esc(data.kind||'')+'</span> in <code style="font-size:11px">'+esc(data.file||'')+'</code></div>'
611
+ + (data.docstring ? '<div class="card" style="margin-bottom:8px;font-size:12px">'+esc(data.docstring)+'</div>' : '')
612
+ + '<div class="result-pre">'+esc(detail)+'</div>';
613
+ }
614
+ if (cmd === 'diagram') {
615
+ return '<div class="card" style="margin-bottom:8px"><b>'+esc(data.title||'Diagram')+'</b> — '+data.nodeCount+' nodes, '+data.edgeCount+' edges</div>'
616
+ + '<div class="result-pre">'+esc(data.mermaid||'')+'</div>';
617
+ }
618
+ if (cmd === 'doc') {
619
+ return '<div class="card" style="margin-bottom:8px"><b>Generated docs</b> — '+data.files+' files, '+data.symbols+' symbols</div>'
620
+ + '<div class="result-pre">'+esc(data.markdown||'')+'</div>';
621
+ }
622
+ return '<div class="result-pre">'+esc(JSON.stringify(data, null, 2))+'</div>';
623
+ }
624
+
625
+ function renderTable(headers, rows, mapper) {
626
+ if (!rows || !rows.length) return '<div style="color:var(--muted);font-size:13px;padding:12px">No results</div>';
627
+ var h = headers.map(function(x) { return '<th>'+x+'</th>'; }).join('');
628
+ var r = rows.slice(0, 200).map(function(d) {
629
+ return '<tr>' + mapper(d).map(function(c) { return '<td>'+(c==null?'':c)+'</td>'; }).join('') + '</tr>';
630
+ }).join('');
631
+ return '<div class="card"><table><thead><tr>'+h+'</tr></thead><tbody>'+r+'</tbody></table></div>';
632
+ }
633
+
634
+ // ─── Bootstrap ────────────────────────────────────────────────────────────────
635
+ loadAll();
636
+
637
+ // ─── Live reload via SSE ──────────────────────────────────────────────────────
638
+ (function connectSSE() {
639
+ const es = new EventSource('http://localhost:${port}/events');
640
+ es.addEventListener('change', () => loadAll());
641
+ es.addEventListener('error', () => { es.close(); setTimeout(connectSSE, 3000); });
642
+ })();
643
+ </script>
644
+ </body>
645
+ </html>`;
646
+ }