@ctxo/cli 0.0.1 → 0.7.0-alpha.0

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.
@@ -0,0 +1,787 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Ctxo Dependency Graph</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+ :root {
10
+ --bg: #0f0f1a; --bg2: #1a1a2e; --border: #2a2a4e; --input-bg: #0f0f1a; --input-border: #3a3a5e;
11
+ --text: #e0e0e0; --text2: #ccc; --text3: #aaa; --text4: #555;
12
+ --hover: #2a2a4e; --name: #fff; --graph-bg: #0f0f1a;
13
+ --node-font: #ffffff; --node-highlight: #ffffff;
14
+ --scrollbar: #2a2a4e;
15
+ }
16
+ [data-theme="light"] {
17
+ --bg: #f5f6f8; --bg2: #ffffff; --border: #dde0e6; --input-bg: #f0f1f4; --input-border: #ccd0d8;
18
+ --text: #2d3038; --text2: #4a4f5c; --text3: #6b7080; --text4: #9ca0ac;
19
+ --hover: #ebedf2; --name: #1a1d24; --graph-bg: #f5f6f8;
20
+ --node-font: #1a1d24; --node-highlight: #ffffff;
21
+ --scrollbar: #ccd0d8;
22
+ }
23
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--text); height: 100vh; overflow: hidden; display: flex; transition: background 0.2s, color 0.2s; }
24
+
25
+ /* Graph fills all available space */
26
+ #graph-wrap { flex: 1; position: relative; overflow: hidden; }
27
+ #graph { width: 100%; height: 100%; background: var(--graph-bg); transition: background 0.2s; }
28
+
29
+ /* Sidebar - right side */
30
+ #sidebar { width: 280px; background: var(--bg2); border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; transition: background 0.2s, border-color 0.2s; }
31
+
32
+ /* Theme toggle */
33
+ #theme-bar { padding: 8px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
34
+ #theme-bar span { font-size: 12px; font-weight: 600; color: var(--text); }
35
+ .theme-btn { background: var(--input-bg); border: 1px solid var(--input-border); border-radius: 6px; width: 30px; height: 30px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; color: var(--text3); transition: all 0.15s; }
36
+ .theme-btn:hover { border-color: #4E79A7; color: var(--text); }
37
+
38
+ /* Search */
39
+ #search-wrap { padding: 12px; border-bottom: 1px solid var(--border); }
40
+ #searchInput { width: 100%; padding: 7px 10px; background: var(--input-bg); border: 1px solid var(--input-border); border-radius: 6px; color: var(--text); font-size: 13px; outline: none; transition: border-color 0.15s; }
41
+ #searchInput:focus { border-color: #4E79A7; }
42
+ #searchInput::placeholder { color: var(--text4); }
43
+ #searchResults { max-height: 140px; overflow-y: auto; margin-top: 4px; }
44
+ .search-item { padding: 5px 8px; font-size: 11px; cursor: pointer; border-radius: 4px; border-left: 3px solid transparent; display: flex; align-items: center; gap: 6px; }
45
+ .search-item:hover { background: var(--hover); }
46
+ .search-item .s-name { color: var(--text); font-weight: 500; }
47
+ .search-item .s-file { color: var(--text4); font-size: 10px; margin-left: auto; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
48
+
49
+ /* Topbar (filters + stats, below search) */
50
+ #topbar { padding: 8px 12px; border-bottom: 1px solid var(--border); }
51
+ #filters-row { display: flex; gap: 12px; flex-wrap: wrap; }
52
+ #stats { font-size: 10px; color: var(--text4); margin-top: 4px; }
53
+
54
+ /* Node info panel - fills all remaining sidebar space */
55
+ #info-panel { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
56
+ .panel-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text3); margin-bottom: 8px; }
57
+ .info-header { padding: 10px 12px 0; }
58
+ .info-name { font-size: 14px; font-weight: 600; color: var(--name); margin-bottom: 2px; word-break: break-all; }
59
+ .info-file { font-size: 10px; color: var(--text4); font-family: monospace; margin-bottom: 6px; }
60
+
61
+ /* Tabs */
62
+ .info-tabs { display: flex; border-bottom: 1px solid var(--border); padding: 0 12px; gap: 0; }
63
+ .info-tab { padding: 6px 10px; font-size: 10px; font-weight: 500; color: var(--text4); cursor: pointer; border-bottom: 2px solid transparent; background: none; border-top: none; border-left: none; border-right: none; transition: all 0.15s; text-transform: uppercase; letter-spacing: 0.03em; }
64
+ .info-tab:hover { color: var(--text2); }
65
+ .info-tab.active { color: #4E79A7; border-bottom-color: #4E79A7; }
66
+ .info-tab .tab-count { font-size: 9px; background: var(--hover); padding: 0 4px; border-radius: 8px; margin-left: 3px; }
67
+
68
+ /* Tab panels */
69
+ .info-tab-panel { display: none; padding: 10px 12px; overflow-y: auto; flex: 1; min-height: 0; }
70
+ .info-tab-panel.active { display: block; flex: 1; }
71
+
72
+ .info-row { font-size: 11px; color: var(--text3); padding: 2px 0; display: flex; justify-content: space-between; }
73
+ .info-row span:last-child { color: var(--text); font-weight: 500; }
74
+ .info-dead { display: inline-block; font-size: 9px; padding: 2px 6px; border-radius: 3px; font-weight: 600; text-transform: uppercase; background: rgba(225,87,89,0.2); color: #E15759; margin-top: 4px; }
75
+ .info-blast { margin-top: 8px; padding: 6px 8px; background: rgba(242,142,43,0.1); border: 1px solid rgba(242,142,43,0.3); border-radius: 6px; font-size: 11px; color: #F28E2B; }
76
+ .neighbor-item { padding: 4px 6px; font-size: 11px; cursor: pointer; border-radius: 4px; border-left: 3px solid transparent; color: var(--text2); }
77
+ .neighbor-item:hover { background: var(--hover); }
78
+ .info-empty { color: var(--text4); font-size: 11px; padding: 8px 0; }
79
+
80
+ /* (Layers & Edges legend moved to graph overlay chips) */
81
+
82
+ /* Git history */
83
+ .git-section { margin-top: 8px; }
84
+ .git-commit { padding: 4px 6px; font-size: 10px; border-radius: 4px; margin-bottom: 2px; display: flex; gap: 6px; align-items: flex-start; }
85
+ .git-commit.normal { background: rgba(78,121,167,0.06); border-left: 3px solid #4E79A7; }
86
+ .git-commit.anti { background: rgba(225,87,89,0.08); border-left: 3px solid #E15759; }
87
+ .git-date { color: var(--text4); min-width: 62px; flex-shrink: 0; }
88
+ .git-msg { color: var(--text2); flex: 1; word-break: break-word; }
89
+ .git-hash { color: var(--text4); font-family: monospace; font-size: 9px; flex-shrink: 0; }
90
+
91
+ /* Anti-pattern indicator */
92
+ .anti-warn { display: inline-block; font-size: 9px; padding: 2px 6px; border-radius: 3px; font-weight: 600; text-transform: uppercase; background: rgba(225,87,89,0.15); color: #E15759; margin-top: 4px; margin-right: 4px; }
93
+
94
+ /* Complexity badge */
95
+ .complexity-badge { display: inline-block; font-size: 9px; padding: 2px 6px; border-radius: 3px; font-weight: 600; margin-top: 4px; }
96
+ .complexity-low { background: rgba(89,161,79,0.2); color: #59A14F; }
97
+ .complexity-med { background: rgba(237,201,72,0.2); color: #EDC948; }
98
+ .complexity-high { background: rgba(225,87,89,0.2); color: #E15759; }
99
+
100
+ /* Filter toggles */
101
+ .filter-toggle { display: flex; align-items: center; gap: 5px; font-size: 10px; color: var(--text3); cursor: pointer; }
102
+ .filter-toggle input[type=checkbox] { accent-color: #4E79A7; width: 12px; height: 12px; }
103
+ .tip-icon { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; border-radius: 50%; background: var(--border); color: var(--text4); font-size: 9px; font-weight: 600; cursor: help; flex-shrink: 0; transition: all 0.15s; font-style: normal; }
104
+ .tip-icon:hover { background: #4E79A7; color: #fff; }
105
+
106
+ /* Graph overlay - inline horizontal panels */
107
+ #graph-overlay { position: absolute; top: 10px; left: 10px; z-index: 5; display: flex; gap: 6px; align-items: flex-start; }
108
+ .legend-panel { background: var(--bg2); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; transition: all 0.2s; }
109
+ .legend-header { display: flex; align-items: center; gap: 5px; padding: 6px 10px; font-size: 11px; font-weight: 500; color: var(--text2); cursor: pointer; user-select: none; white-space: nowrap; }
110
+ .legend-header:hover { color: var(--text); }
111
+ .legend-header .arrow { font-size: 8px; transition: transform 0.2s; }
112
+ .legend-header .arrow.open { transform: rotate(180deg); }
113
+ .legend-body { display: flex; gap: 2px; padding: 0 8px 6px; flex-wrap: wrap; }
114
+ .legend-body.collapsed { display: none; }
115
+ .chip-item { display: flex; align-items: center; gap: 4px; font-size: 10px; color: var(--text2); padding: 3px 8px; cursor: pointer; border-radius: 12px; transition: opacity 0.15s; background: rgba(255,255,255,0.04); white-space: nowrap; }
116
+ .chip-item:hover { background: var(--hover); }
117
+ .chip-item.dimmed { opacity: 0.3; text-decoration: line-through; }
118
+ .chip-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
119
+ .chip-line { width: 14px; height: 0; border-top: 2px solid; flex-shrink: 0; }
120
+ .chip-line.dashed { border-top-style: dashed; }
121
+ .chip-count { color: var(--text4); font-size: 9px; margin-left: 1px; }
122
+
123
+ /* (stats moved into #topbar) */
124
+
125
+ /* Banner */
126
+ .banner { padding: 6px 12px; background: rgba(78,121,167,0.1); border-bottom: 1px solid var(--border); font-size: 11px; color: #4E79A7; display: none; }
127
+ .banner.visible { display: block; }
128
+
129
+ ::-webkit-scrollbar { width: 5px; }
130
+ ::-webkit-scrollbar-track { background: transparent; }
131
+ ::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 3px; }
132
+ </style>
133
+ </head>
134
+ <body>
135
+
136
+ <div id="graph-wrap">
137
+ <div id="graph"></div>
138
+ <div id="graph-overlay">
139
+ <div class="legend-panel" id="layerPanel">
140
+ <div class="legend-header" id="layerHeader">
141
+ Layers <span class="arrow open">&#9660;</span>
142
+ </div>
143
+ <div class="legend-body" id="layerBody"></div>
144
+ </div>
145
+ <div class="legend-panel" id="edgePanel">
146
+ <div class="legend-header" id="edgeHeader">
147
+ Edges <span class="arrow open">&#9660;</span>
148
+ </div>
149
+ <div class="legend-body" id="edgeBody"></div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ <div id="sidebar">
154
+ <div id="theme-bar">
155
+ <span>Ctxo Graph</span>
156
+ <button class="theme-btn" id="themeToggle" title="Toggle light/dark">&#9790;</button>
157
+ </div>
158
+ <div class="banner" id="banner"></div>
159
+ <div id="search-wrap">
160
+ <input id="searchInput" type="text" placeholder="Search symbols..." />
161
+ <div id="searchResults"></div>
162
+ </div>
163
+ <div id="topbar">
164
+ <div id="filters-row">
165
+ <label class="filter-toggle" id="antiPatternToggle">
166
+ <input type="checkbox" />
167
+ Anti-patterns
168
+ <span class="tip-icon" title="Show only symbols in files with reverted commits (revert/rollback history)">?</span>
169
+ </label>
170
+ <label class="filter-toggle" id="deadCodeToggle">
171
+ <input type="checkbox" />
172
+ Dead code
173
+ <span class="tip-icon" title="Show only unreachable symbols with zero importers or cascading dead dependencies">?</span>
174
+ </label>
175
+ </div>
176
+ <div id="label-row" style="display:flex;align-items:center;gap:6px;margin-top:4px">
177
+ <span style="font-size:10px;color:var(--text4)">Labels</span>
178
+ <input type="range" id="labelSlider" min="0" max="100" value="15" style="flex:1;accent-color:#4E79A7;height:14px" />
179
+ <span id="labelVal" style="font-size:10px;color:var(--text3);min-width:28px;text-align:right">15%</span>
180
+ <span class="tip-icon" title="Minimum PageRank % to show node labels. 0% = all labels, higher = only important symbols">?</span>
181
+ </div>
182
+ <div id="stats"></div>
183
+ </div>
184
+ <div id="info-panel">
185
+ <div class="info-header">
186
+ <div class="info-name" id="infoName">Click a node to inspect</div>
187
+ <div class="info-file" id="infoFile"></div>
188
+ </div>
189
+ <div class="info-tabs" id="infoTabs" style="display:none">
190
+ <button class="info-tab active" data-tab="tab-info">Info</button>
191
+ <button class="info-tab" data-tab="tab-history">History</button>
192
+ <button class="info-tab" data-tab="tab-deps">Deps</button>
193
+ </div>
194
+ <div class="info-tab-panel active" id="tab-info"></div>
195
+ <div class="info-tab-panel" id="tab-history"></div>
196
+ <div class="info-tab-panel" id="tab-deps"></div>
197
+ </div>
198
+ </div>
199
+
200
+ <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
201
+ <script>
202
+
203
+ var CTXO_DATA = /*__CTXO_DATA__*/null;
204
+
205
+ // Tableau 10 inspired palette mapped to architectural layers
206
+ var LAYER_COLORS = {
207
+ Domain: '#4E79A7', // steel blue
208
+ Adapter: '#F28E2B', // orange
209
+ Test: '#59A14F', // green
210
+ Configuration: '#EDC948', // gold
211
+ Composition: '#B07AA1', // purple
212
+ Infrastructure:'#E15759', // coral red
213
+ Unknown: '#BAB0AC' // grey
214
+ };
215
+
216
+ var EDGE_COLORS = {
217
+ imports: { color: '#4E79A7', opacity: 0.6 },
218
+ calls: { color: '#59A14F', opacity: 0.7 },
219
+ extends: { color: '#B07AA1', opacity: 0.8 },
220
+ implements: { color: '#F28E2B', opacity: 0.8 },
221
+ uses: { color: '#BAB0AC', opacity: 0.35 }
222
+ };
223
+
224
+ // DOM helper
225
+ function el(tag, attrs, children) {
226
+ var e = document.createElement(tag);
227
+ if (attrs) {
228
+ for (var k in attrs) {
229
+ if (k === 'textContent') e.textContent = attrs[k];
230
+ else if (k === 'className') e.className = attrs[k];
231
+ else if (k === 'style' && typeof attrs[k] === 'object') Object.assign(e.style, attrs[k]);
232
+ else e.setAttribute(k, attrs[k]);
233
+ }
234
+ }
235
+ if (children) children.forEach(function(c) {
236
+ if (typeof c === 'string') e.appendChild(document.createTextNode(c));
237
+ else if (c) e.appendChild(c);
238
+ });
239
+ return e;
240
+ }
241
+
242
+ var network = null;
243
+ var nodesDataset = null;
244
+ var edgesDataset = null;
245
+ var allNodes = {}; // id -> node data
246
+ var allEdges = []; // edge data array
247
+ var hiddenLayers = new Set();
248
+ var maxPR = 0.001;
249
+ var labelThreshold = 0.15;
250
+
251
+ function init() {
252
+ if (!CTXO_DATA) {
253
+ document.getElementById('graph').appendChild(
254
+ el('div', { style: { display:'flex', alignItems:'center', justifyContent:'center', height:'100%', color:'#555' }, textContent: 'No data. Run: ctxo visualize' })
255
+ );
256
+ return;
257
+ }
258
+
259
+ var data = CTXO_DATA;
260
+
261
+ // Banner
262
+ if (data.shownSymbols < data.totalSymbols) {
263
+ var banner = document.getElementById('banner');
264
+ banner.textContent = 'Showing top ' + data.shownSymbols + ' of ' + data.totalSymbols + ' symbols by PageRank. Use --max-nodes to adjust.';
265
+ banner.classList.add('visible');
266
+ }
267
+
268
+ // Compute max PageRank for sizing
269
+ maxPR = 0.001;
270
+ data.nodes.forEach(function(n) { if (n.pageRank > maxPR) maxPR = n.pageRank; });
271
+
272
+ // Build vis.js nodes
273
+ var visNodes = [];
274
+ data.nodes.forEach(function(n) {
275
+ var color = LAYER_COLORS[n.layer] || '#BAB0AC';
276
+ var size = 10 + 30 * (n.pageRank / maxPR);
277
+ var prNorm = n.pageRank / maxPR;
278
+ var showLabel = prNorm >= labelThreshold;
279
+
280
+ var nodeObj = {
281
+ id: n.id,
282
+ label: showLabel ? n.name : '',
283
+ title: n.name + '\n' + n.file + '\n' + n.kind + ' | ' + n.layer + '\nPageRank: ' + n.pageRank.toFixed(6) + '\nIn: ' + n.inDegree + ' Out: ' + n.outDegree + (n.isDead ? '\n[DEAD CODE]' : ''),
284
+ size: size,
285
+ color: {
286
+ background: color,
287
+ border: color,
288
+ highlight: { background: '#ffffff', border: color },
289
+ hover: { background: '#ffffff', border: color }
290
+ },
291
+ borderWidth: n.isDead ? 3 : 1.5,
292
+ borderWidthSelected: 3,
293
+ font: { color: '#ffffff', size: showLabel ? 12 : 0 },
294
+ // Store metadata
295
+ _kind: n.kind,
296
+ _file: n.file,
297
+ _startLine: n.startLine,
298
+ _layer: n.layer,
299
+ _pageRank: n.pageRank,
300
+ _isDead: n.isDead,
301
+ _deadConfidence: n.deadConfidence,
302
+ _deadReason: n.deadReason,
303
+ _cyclomatic: n.cyclomatic,
304
+ _hasAntiPattern: n.hasAntiPattern,
305
+ _inDegree: n.inDegree,
306
+ _outDegree: n.outDegree,
307
+ _name: n.name,
308
+ _prNorm: prNorm
309
+ };
310
+
311
+ if (n.isDead) {
312
+ nodeObj.shapeProperties = { borderDashes: [6, 3] };
313
+ nodeObj.color.border = '#E15759';
314
+ nodeObj.opacity = 0.5;
315
+ } else if (n.hasAntiPattern) {
316
+ nodeObj.color.border = '#F28E2B';
317
+ nodeObj.borderWidth = 2.5;
318
+ }
319
+
320
+ allNodes[n.id] = nodeObj;
321
+ visNodes.push(nodeObj);
322
+ });
323
+
324
+ // Build vis.js edges
325
+ data.edges.forEach(function(e, i) {
326
+ var ec = EDGE_COLORS[e.kind] || EDGE_COLORS.uses;
327
+ var isWeak = (e.kind === 'uses' || e.kind === 'imports');
328
+ var edgeObj = {
329
+ id: 'e' + i,
330
+ from: e.source,
331
+ to: e.target,
332
+ arrows: { to: { enabled: true, scaleFactor: 0.5 } },
333
+ color: { color: ec.color, opacity: ec.opacity, highlight: ec.color, hover: ec.color },
334
+ width: isWeak ? 1 : 2,
335
+ dashes: e.kind === 'uses',
336
+ smooth: { type: 'continuous', roundness: 0.2 },
337
+ selectionWidth: 3,
338
+ _kind: e.kind
339
+ };
340
+ allEdges.push(edgeObj);
341
+ });
342
+
343
+ nodesDataset = new vis.DataSet(visNodes);
344
+ edgesDataset = new vis.DataSet(allEdges);
345
+
346
+ // Initialize network
347
+ network = new vis.Network(
348
+ document.getElementById('graph'),
349
+ { nodes: nodesDataset, edges: edgesDataset },
350
+ {
351
+ nodes: { shape: 'dot', borderWidth: 1.5 },
352
+ edges: { smooth: { type: 'continuous', roundness: 0.2 } },
353
+ physics: {
354
+ solver: 'forceAtlas2Based',
355
+ forceAtlas2Based: {
356
+ gravitationalConstant: -60,
357
+ centralGravity: 0.005,
358
+ springLength: 120,
359
+ springConstant: 0.08,
360
+ damping: 0.4,
361
+ avoidOverlap: 0.8
362
+ },
363
+ stabilization: { iterations: 200, fit: true }
364
+ },
365
+ interaction: {
366
+ hover: true,
367
+ tooltipDelay: 100,
368
+ hideEdgesOnDrag: true,
369
+ navigationButtons: false,
370
+ keyboard: false
371
+ },
372
+ layout: { improvedLayout: true }
373
+ }
374
+ );
375
+
376
+ // Disable physics after stabilization for crisp feel
377
+ network.once('stabilizationIterationsDone', function() {
378
+ network.setOptions({ physics: { enabled: false } });
379
+ });
380
+
381
+ // Click handler - show node info + blast radius
382
+ network.on('click', function(params) {
383
+ if (params.nodes.length > 0) {
384
+ var nodeId = params.nodes[0];
385
+ showNodeInfo(nodeId);
386
+ } else {
387
+ clearNodeInfo();
388
+ }
389
+ });
390
+
391
+ // Hover handler
392
+ network.on('hoverNode', function(params) {
393
+ document.getElementById('graph').style.cursor = 'pointer';
394
+ });
395
+ network.on('blurNode', function() {
396
+ document.getElementById('graph').style.cursor = 'grab';
397
+ });
398
+
399
+ // Build UI
400
+ buildLayerLegend(data);
401
+ buildEdgeLegend();
402
+ setupSearch(data);
403
+ updateStats();
404
+
405
+ // Theme toggle
406
+ document.getElementById('themeToggle').addEventListener('click', function() {
407
+ var current = document.documentElement.getAttribute('data-theme');
408
+ var isLight = current === 'light';
409
+ var next = isLight ? null : 'light';
410
+ if (next) document.documentElement.setAttribute('data-theme', next);
411
+ else document.documentElement.removeAttribute('data-theme');
412
+ this.textContent = next ? '\u2600' : '\u263E';
413
+
414
+ // Update vis.js background + node font colors
415
+ var fontColor = next ? '#1a1d24' : '#ffffff';
416
+ var bgColor = next ? '#f5f6f8' : '#0f0f1a';
417
+ var updates = [];
418
+ for (var id in allNodes) {
419
+ allNodes[id].font = { color: fontColor, size: allNodes[id].font.size };
420
+ updates.push({ id: id, font: allNodes[id].font });
421
+ }
422
+ nodesDataset.update(updates);
423
+ network.setOptions({ nodes: { font: { color: fontColor } } });
424
+ document.getElementById('graph').style.background = bgColor;
425
+ });
426
+
427
+ // Anti-pattern filter
428
+ document.getElementById('antiPatternToggle').querySelector('input').addEventListener('change', function() {
429
+ applyNodeFilters();
430
+ });
431
+
432
+ // Dead code filter
433
+ document.getElementById('deadCodeToggle').querySelector('input').addEventListener('change', function() {
434
+ applyNodeFilters();
435
+ });
436
+
437
+ // Label threshold slider
438
+ document.getElementById('labelSlider').addEventListener('input', function() {
439
+ labelThreshold = parseInt(this.value) / 100;
440
+ document.getElementById('labelVal').textContent = this.value + '%';
441
+ updateLabels();
442
+ });
443
+ }
444
+
445
+ function showNodeInfo(nodeId) {
446
+ var n = allNodes[nodeId];
447
+ if (!n) return;
448
+
449
+ // Header
450
+ document.getElementById('infoName').textContent = n._name;
451
+ document.getElementById('infoFile').textContent = n._file + ':' + n._startLine + ' | ' + n._kind;
452
+
453
+ // Show tabs
454
+ document.getElementById('infoTabs').style.display = '';
455
+
456
+ // Wire tab switching
457
+ var tabs = document.querySelectorAll('.info-tab');
458
+ tabs.forEach(function(tab) {
459
+ tab.onclick = function() {
460
+ tabs.forEach(function(t) { t.classList.remove('active'); });
461
+ tab.classList.add('active');
462
+ document.querySelectorAll('.info-tab-panel').forEach(function(p) { p.classList.remove('active'); });
463
+ document.getElementById(tab.dataset.tab).classList.add('active');
464
+ };
465
+ });
466
+
467
+ // ── TAB 1: Info ──
468
+ var infoPanel = document.getElementById('tab-info');
469
+ while (infoPanel.firstChild) infoPanel.removeChild(infoPanel.firstChild);
470
+
471
+ var rows = [
472
+ ['Layer', n._layer],
473
+ ['PageRank', n._pageRank.toFixed(6)],
474
+ ['In-degree', String(n._inDegree)],
475
+ ['Out-degree', String(n._outDegree)]
476
+ ];
477
+ rows.forEach(function(r) {
478
+ infoPanel.appendChild(el('div', { className: 'info-row' }, [
479
+ el('span', { textContent: r[0] }),
480
+ el('span', { textContent: r[1] })
481
+ ]));
482
+ });
483
+
484
+ // Badges
485
+ var badgesDiv = el('div', { style: { marginTop: '6px', display: 'flex', flexWrap: 'wrap', gap: '4px' } });
486
+ if (n._cyclomatic != null) {
487
+ var cClass = n._cyclomatic <= 5 ? 'complexity-low' : n._cyclomatic <= 15 ? 'complexity-med' : 'complexity-high';
488
+ badgesDiv.appendChild(el('span', { className: 'complexity-badge ' + cClass, textContent: 'complexity: ' + n._cyclomatic }));
489
+ }
490
+ if (n._isDead) {
491
+ badgesDiv.appendChild(el('span', { className: 'info-dead', textContent: 'DEAD' + (n._deadConfidence != null ? ' (' + (n._deadConfidence * 100) + '%)' : '') }));
492
+ }
493
+ if (n._hasAntiPattern) {
494
+ badgesDiv.appendChild(el('span', { className: 'anti-warn', textContent: 'HAS REVERTS' }));
495
+ }
496
+ if (badgesDiv.childNodes.length > 0) infoPanel.appendChild(badgesDiv);
497
+
498
+ if (n._isDead && n._deadReason) {
499
+ infoPanel.appendChild(el('div', { style: { fontSize: '10px', color: '#E15759', marginTop: '4px', fontStyle: 'italic' }, textContent: n._deadReason }));
500
+ }
501
+
502
+ // Blast radius
503
+ var visited = {};
504
+ visited[nodeId] = true;
505
+ var queue = [nodeId];
506
+ while (queue.length > 0) {
507
+ var current = queue.shift();
508
+ allEdges.forEach(function(e) {
509
+ if (e.to === current && !visited[e.from]) {
510
+ visited[e.from] = true;
511
+ queue.push(e.from);
512
+ }
513
+ });
514
+ }
515
+ var blastCount = Object.keys(visited).length - 1;
516
+ if (blastCount > 0) {
517
+ infoPanel.appendChild(el('div', { className: 'info-blast', textContent: blastCount + ' symbols impacted (blast radius)' }));
518
+ }
519
+
520
+ // ── TAB 2: History ──
521
+ var histPanel = document.getElementById('tab-history');
522
+ while (histPanel.firstChild) histPanel.removeChild(histPanel.firstChild);
523
+
524
+ var fileInfo = null;
525
+ if (CTXO_DATA.files) {
526
+ for (var i = 0; i < CTXO_DATA.files.length; i++) {
527
+ if (CTXO_DATA.files[i].file === n._file) { fileInfo = CTXO_DATA.files[i]; break; }
528
+ }
529
+ }
530
+
531
+ var histCount = 0;
532
+ if (fileInfo) {
533
+ if (fileInfo.antiPatterns.length > 0) {
534
+ histPanel.appendChild(el('div', { className: 'panel-title', textContent: 'ANTI-PATTERNS (' + fileInfo.antiPatterns.length + ')' }));
535
+ fileInfo.antiPatterns.forEach(function(ap) {
536
+ histPanel.appendChild(el('div', { className: 'git-commit anti' }, [
537
+ el('span', { className: 'git-date', textContent: ap.date }),
538
+ el('span', { className: 'git-msg', textContent: ap.message }),
539
+ el('span', { className: 'git-hash', textContent: ap.hash.substring(0, 7) })
540
+ ]));
541
+ });
542
+ histCount += fileInfo.antiPatterns.length;
543
+ }
544
+ if (fileInfo.intent.length > 0) {
545
+ histPanel.appendChild(el('div', { className: 'panel-title', style: { marginTop: fileInfo.antiPatterns.length > 0 ? '10px' : '0' }, textContent: 'COMMITS (' + fileInfo.intent.length + ')' }));
546
+ fileInfo.intent.forEach(function(ci) {
547
+ histPanel.appendChild(el('div', { className: 'git-commit normal' }, [
548
+ el('span', { className: 'git-date', textContent: ci.date }),
549
+ el('span', { className: 'git-msg', textContent: ci.message }),
550
+ el('span', { className: 'git-hash', textContent: ci.hash.substring(0, 7) })
551
+ ]));
552
+ });
553
+ histCount += fileInfo.intent.length;
554
+ }
555
+ }
556
+ if (histCount === 0) {
557
+ histPanel.appendChild(el('div', { className: 'info-empty', textContent: 'No git history available' }));
558
+ }
559
+
560
+ // Update history tab count
561
+ var histTab = document.querySelector('[data-tab="tab-history"]');
562
+ histTab.textContent = 'History';
563
+ if (histCount > 0) {
564
+ histTab.appendChild(el('span', { className: 'tab-count', textContent: String(histCount) }));
565
+ }
566
+
567
+ // ── TAB 3: Deps ──
568
+ var depsPanel = document.getElementById('tab-deps');
569
+ while (depsPanel.firstChild) depsPanel.removeChild(depsPanel.firstChild);
570
+
571
+ var neighborIds = network.getConnectedNodes(nodeId);
572
+ if (neighborIds.length > 0) {
573
+ // Split into dependents (reverse) and dependencies (forward)
574
+ var dependents = [];
575
+ var dependencies = [];
576
+ allEdges.forEach(function(e) {
577
+ if (e.to === nodeId && allNodes[e.from]) dependents.push({ id: e.from, kind: e._kind });
578
+ if (e.from === nodeId && allNodes[e.to]) dependencies.push({ id: e.to, kind: e._kind });
579
+ });
580
+
581
+ if (dependencies.length > 0) {
582
+ depsPanel.appendChild(el('div', { className: 'panel-title', textContent: 'DEPENDS ON (' + dependencies.length + ')' }));
583
+ dependencies.forEach(function(dep) {
584
+ var nn = allNodes[dep.id];
585
+ var color = LAYER_COLORS[nn._layer] || '#BAB0AC';
586
+ var item = el('div', { className: 'neighbor-item', style: { borderLeftColor: color } }, [
587
+ el('span', { textContent: nn._name }),
588
+ el('span', { style: { marginLeft: 'auto', fontSize: '9px', color: 'var(--text4)' }, textContent: dep.kind })
589
+ ]);
590
+ item.onclick = function() {
591
+ network.focus(dep.id, { scale: 1.4, animation: true });
592
+ network.selectNodes([dep.id]);
593
+ showNodeInfo(dep.id);
594
+ };
595
+ depsPanel.appendChild(item);
596
+ });
597
+ }
598
+
599
+ if (dependents.length > 0) {
600
+ depsPanel.appendChild(el('div', { className: 'panel-title', style: { marginTop: dependencies.length > 0 ? '10px' : '0' }, textContent: 'USED BY (' + dependents.length + ')' }));
601
+ dependents.forEach(function(dep) {
602
+ var nn = allNodes[dep.id];
603
+ var color = LAYER_COLORS[nn._layer] || '#BAB0AC';
604
+ var item = el('div', { className: 'neighbor-item', style: { borderLeftColor: color } }, [
605
+ el('span', { textContent: nn._name }),
606
+ el('span', { style: { marginLeft: 'auto', fontSize: '9px', color: 'var(--text4)' }, textContent: dep.kind })
607
+ ]);
608
+ item.onclick = function() {
609
+ network.focus(dep.id, { scale: 1.4, animation: true });
610
+ network.selectNodes([dep.id]);
611
+ showNodeInfo(dep.id);
612
+ };
613
+ depsPanel.appendChild(item);
614
+ });
615
+ }
616
+ } else {
617
+ depsPanel.appendChild(el('div', { className: 'info-empty', textContent: 'No dependencies' }));
618
+ }
619
+
620
+ // Update deps tab count
621
+ var depsTab = document.querySelector('[data-tab="tab-deps"]');
622
+ depsTab.textContent = 'Deps';
623
+ if (neighborIds.length > 0) {
624
+ depsTab.appendChild(el('span', { className: 'tab-count', textContent: String(neighborIds.length) }));
625
+ }
626
+
627
+ // Reset to Info tab
628
+ tabs.forEach(function(t) { t.classList.remove('active'); });
629
+ document.querySelector('[data-tab="tab-info"]').classList.add('active');
630
+ document.querySelectorAll('.info-tab-panel').forEach(function(p) { p.classList.remove('active'); });
631
+ document.getElementById('tab-info').classList.add('active');
632
+ }
633
+
634
+ function clearNodeInfo() {
635
+ document.getElementById('infoName').textContent = 'Click a node to inspect';
636
+ document.getElementById('infoFile').textContent = '';
637
+ document.getElementById('infoTabs').style.display = 'none';
638
+ ['tab-info', 'tab-history', 'tab-deps'].forEach(function(id) {
639
+ var p = document.getElementById(id);
640
+ while (p.firstChild) p.removeChild(p.firstChild);
641
+ });
642
+ }
643
+
644
+ function buildLayerLegend(data) {
645
+ var body = document.getElementById('layerBody');
646
+ var header = document.getElementById('layerHeader');
647
+
648
+ var layerCounts = {};
649
+ data.nodes.forEach(function(n) {
650
+ layerCounts[n.layer] = (layerCounts[n.layer] || 0) + 1;
651
+ });
652
+
653
+ Object.keys(LAYER_COLORS).forEach(function(layer) {
654
+ var count = layerCounts[layer];
655
+ if (!count) return;
656
+ var color = LAYER_COLORS[layer];
657
+ var item = el('div', { className: 'chip-item' }, [
658
+ el('span', { className: 'chip-dot', style: { background: color } }),
659
+ el('span', { textContent: layer }),
660
+ el('span', { className: 'chip-count', textContent: String(count) })
661
+ ]);
662
+ item.onclick = function() {
663
+ if (hiddenLayers.has(layer)) {
664
+ hiddenLayers.delete(layer);
665
+ item.classList.remove('dimmed');
666
+ } else {
667
+ hiddenLayers.add(layer);
668
+ item.classList.add('dimmed');
669
+ }
670
+ applyLayerFilter();
671
+ };
672
+ body.appendChild(item);
673
+ });
674
+
675
+ header.onclick = function() {
676
+ body.classList.toggle('collapsed');
677
+ header.querySelector('.arrow').classList.toggle('open');
678
+ };
679
+ }
680
+
681
+ function buildEdgeLegend() {
682
+ var body = document.getElementById('edgeBody');
683
+ var header = document.getElementById('edgeHeader');
684
+
685
+ var edgeKinds = [
686
+ { name: 'imports', dashed: false },
687
+ { name: 'calls', dashed: false },
688
+ { name: 'extends', dashed: false },
689
+ { name: 'implements', dashed: false },
690
+ { name: 'uses', dashed: true }
691
+ ];
692
+ edgeKinds.forEach(function(ek) {
693
+ var ec = EDGE_COLORS[ek.name];
694
+ var lineClass = 'chip-line' + (ek.dashed ? ' dashed' : '');
695
+ body.appendChild(el('div', { className: 'chip-item' }, [
696
+ el('span', { className: lineClass, style: { borderColor: ec.color } }),
697
+ el('span', { textContent: ek.name })
698
+ ]));
699
+ });
700
+
701
+ header.onclick = function() {
702
+ body.classList.toggle('collapsed');
703
+ header.querySelector('.arrow').classList.toggle('open');
704
+ };
705
+ }
706
+
707
+ function applyLayerFilter() {
708
+ applyNodeFilters();
709
+ }
710
+
711
+ function applyNodeFilters() {
712
+ var antiOnly = document.getElementById('antiPatternToggle').querySelector('input').checked;
713
+ var deadOnly = document.getElementById('deadCodeToggle').querySelector('input').checked;
714
+ var updates = [];
715
+ for (var id in allNodes) {
716
+ var n = allNodes[id];
717
+ var hidden = false;
718
+ if (hiddenLayers.has(n._layer)) hidden = true;
719
+ if (antiOnly && !n._hasAntiPattern) hidden = true;
720
+ if (deadOnly && !n._isDead) hidden = true;
721
+ updates.push({ id: id, hidden: hidden });
722
+ }
723
+ nodesDataset.update(updates);
724
+ updateStats();
725
+ }
726
+
727
+ function updateLabels() {
728
+ var isLight = document.documentElement.getAttribute('data-theme') === 'light';
729
+ var fontColor = isLight ? '#1a1d24' : '#ffffff';
730
+ var updates = [];
731
+ for (var id in allNodes) {
732
+ var n = allNodes[id];
733
+ var show = n._prNorm >= labelThreshold;
734
+ n.label = show ? n._name : '';
735
+ n.font = { color: fontColor, size: show ? 12 : 0 };
736
+ updates.push({ id: id, label: n.label, font: n.font });
737
+ }
738
+ nodesDataset.update(updates);
739
+ }
740
+
741
+ function setupSearch(data) {
742
+ var input = document.getElementById('searchInput');
743
+ var resultsEl = document.getElementById('searchResults');
744
+ var timeout;
745
+
746
+ input.addEventListener('input', function() {
747
+ clearTimeout(timeout);
748
+ var val = this.value;
749
+ timeout = setTimeout(function() {
750
+ while (resultsEl.firstChild) resultsEl.removeChild(resultsEl.firstChild);
751
+ if (!val.trim()) return;
752
+ var q = val.toLowerCase();
753
+ var matches = data.nodes.filter(function(n) {
754
+ return n.name.toLowerCase().indexOf(q) !== -1 || n.file.toLowerCase().indexOf(q) !== -1;
755
+ }).slice(0, 15);
756
+
757
+ matches.forEach(function(n) {
758
+ var color = LAYER_COLORS[n.layer] || '#BAB0AC';
759
+ var item = el('div', { className: 'search-item', style: { borderLeftColor: color } }, [
760
+ el('span', { className: 's-name', textContent: n.name }),
761
+ el('span', { className: 's-file', textContent: n.file })
762
+ ]);
763
+ item.onclick = function() {
764
+ network.focus(n.id, { scale: 1.5, animation: true });
765
+ network.selectNodes([n.id]);
766
+ showNodeInfo(n.id);
767
+ };
768
+ resultsEl.appendChild(item);
769
+ });
770
+ }, 150);
771
+ });
772
+ }
773
+
774
+ function updateStats() {
775
+ var visibleCount = 0;
776
+ for (var id in allNodes) {
777
+ if (!allNodes[id].hidden) visibleCount++;
778
+ }
779
+ var layerCount = Object.keys(LAYER_COLORS).filter(function(l) { return !hiddenLayers.has(l); }).length;
780
+ document.getElementById('stats').textContent =
781
+ visibleCount + ' nodes - ' + allEdges.length + ' edges - ' + layerCount + ' layers';
782
+ }
783
+
784
+ init();
785
+ </script>
786
+ </body>
787
+ </html>