codegraph-engine 2.3.0__py3-none-any.whl → 3.0.1__py3-none-any.whl

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 (118) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/__main__.py +7 -0
  3. codegraph/agent_brain.py +253 -2
  4. codegraph/agent_capabilities.py +1348 -67
  5. codegraph/agent_rules.py +2 -1
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture_rules.py +123 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/change_impact.py +320 -110
  22. codegraph/changelog.py +170 -0
  23. codegraph/cli.py +1605 -62
  24. codegraph/cli_output.py +17 -2
  25. codegraph/context.py +135 -24
  26. codegraph/context_freshness.py +2 -0
  27. codegraph/database/__init__.py +14 -0
  28. codegraph/database/extractor.py +712 -17
  29. codegraph/database/interrogation.py +388 -2
  30. codegraph/database/schema_drift.py +862 -0
  31. codegraph/dataflow_propagation.py +118 -0
  32. codegraph/dead_code.py +387 -0
  33. codegraph/epistemic.py +15 -0
  34. codegraph/evidence_contract.py +34 -3
  35. codegraph/federation.py +245 -0
  36. codegraph/frameworks.py +234 -12
  37. codegraph/frameworks_nestjs.py +502 -0
  38. codegraph/frameworks_nextjs.py +337 -0
  39. codegraph/frameworks_react.py +457 -0
  40. codegraph/freshness.py +2 -0
  41. codegraph/frontend_html.py +109 -0
  42. codegraph/git.py +2 -0
  43. codegraph/git_state.py +2 -0
  44. codegraph/graph/traversal.py +150 -7
  45. codegraph/graph_csr.py +218 -0
  46. codegraph/incremental.py +150 -0
  47. codegraph/indexing/__init__.py +7 -1
  48. codegraph/indexing/classifier.py +5 -0
  49. codegraph/indexing/indexer.py +292 -38
  50. codegraph/indexing/languages/__init__.py +1 -0
  51. codegraph/indexing/languages/cpp.py +457 -0
  52. codegraph/indexing/languages/go.py +459 -0
  53. codegraph/indexing/languages/jvm.py +583 -0
  54. codegraph/indexing/languages/rust.py +507 -0
  55. codegraph/indexing/models.py +15 -1
  56. codegraph/indexing/parser.py +385 -9
  57. codegraph/indexing/pool.py +136 -0
  58. codegraph/indexing/protocols.py +220 -0
  59. codegraph/indexing/scanner.py +23 -0
  60. codegraph/indexing/templates.py +418 -0
  61. codegraph/interrogation.py +207 -39
  62. codegraph/lsp.py +318 -0
  63. codegraph/lsp_bridge/__init__.py +16 -0
  64. codegraph/lsp_bridge/bridge.py +102 -0
  65. codegraph/lsp_bridge/client.py +207 -0
  66. codegraph/lsp_bridge/models.py +71 -0
  67. codegraph/mcp/server.py +502 -29
  68. codegraph/memory/trajectory.py +282 -0
  69. codegraph/models.py +7 -0
  70. codegraph/monorepo.py +3 -0
  71. codegraph/multi_agent/__init__.py +13 -0
  72. codegraph/multi_agent/coordinator.py +229 -0
  73. codegraph/multi_agent/models.py +76 -0
  74. codegraph/optimizer.py +42 -5
  75. codegraph/polymorphism.py +95 -0
  76. codegraph/pr_review.py +296 -0
  77. codegraph/process_lifecycle.py +50 -12
  78. codegraph/reachability.py +198 -0
  79. codegraph/reachability_index.py +74 -0
  80. codegraph/refactor/__init__.py +48 -0
  81. codegraph/refactor/models.py +114 -0
  82. codegraph/refactor/patcher.py +348 -0
  83. codegraph/refactor/renamer.py +844 -0
  84. codegraph/refactor/transactions.py +414 -0
  85. codegraph/repair.py +233 -0
  86. codegraph/resolver.py +1357 -161
  87. codegraph/retrieval_policy.py +22 -1
  88. codegraph/roblox.py +2174 -0
  89. codegraph/route_composer.py +7 -0
  90. codegraph/route_topology.py +172 -0
  91. codegraph/runtime/__init__.py +25 -0
  92. codegraph/runtime/collector.py +155 -0
  93. codegraph/runtime/db_watcher.py +191 -0
  94. codegraph/runtime/ingestor.py +30 -10
  95. codegraph/runtime/otel_receiver.py +208 -0
  96. codegraph/runtime/port_inspector.py +304 -0
  97. codegraph/runtime/runner.py +155 -6
  98. codegraph/runtime/trace_linker.py +175 -0
  99. codegraph/sandbox.py +215 -0
  100. codegraph/search/hybrid.py +26 -0
  101. codegraph/security/supply_chain.py +188 -0
  102. codegraph/security/taint.py +421 -0
  103. codegraph/semantic_decorators.py +34 -3
  104. codegraph/structural_diff.py +2 -0
  105. codegraph/styles.py +144 -0
  106. codegraph/symbol_history.py +2 -0
  107. codegraph/test_gen.py +437 -0
  108. codegraph/tsconfig.py +236 -0
  109. codegraph/ui/__init__.py +6 -0
  110. codegraph/ui/server.py +876 -0
  111. codegraph/watcher.py +323 -0
  112. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
  113. codegraph_engine-3.0.1.dist-info/RECORD +164 -0
  114. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
  115. codegraph_engine-2.3.0.dist-info/RECORD +0 -94
  116. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
  117. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
  118. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/top_level.txt +0 -0
codegraph/ui/server.py ADDED
@@ -0,0 +1,876 @@
1
+ """Embedded Local Web Dashboard & Knowledge Graph Visualizer for CodeGraph.
2
+
3
+ Serves an interactive web application on localhost:
4
+ - Force-Directed Knowledge Graph Visualizer
5
+ - Full-Stack Route & API Drift Inspector
6
+ - PR Blast Radius & Risk Assessment
7
+ - Live Watcher & Telemetry Status
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import threading
14
+ import urllib.parse
15
+ import webbrowser
16
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from codegraph.indexing.indexer import Indexer
21
+
22
+ logger = logging.getLogger("codegraph.ui")
23
+
24
+
25
+ def _get_html_dashboard() -> str:
26
+ """Return the bundled single-page application HTML for the CodeGraph Dashboard."""
27
+ return """<!DOCTYPE html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="UTF-8">
31
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
32
+ <title>CodeGraph Visual Intelligence Dashboard</title>
33
+ <style>
34
+ :root {
35
+ --bg: #0d1117;
36
+ --card: #161b22;
37
+ --border: #30363d;
38
+ --text: #f0f6fc;
39
+ --muted: #8b949e;
40
+ --accent: #58a6ff;
41
+ --green: #3fb950;
42
+ --amber: #d29922;
43
+ --red: #f85149;
44
+ --purple: #bc8cff;
45
+ --cyan: #39c5cf;
46
+ }
47
+ * { box-sizing: border-box; margin: 0; padding: 0; }
48
+ body {
49
+ background: var(--bg);
50
+ color: var(--text);
51
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
52
+ display: flex;
53
+ flex-direction: column;
54
+ height: 100vh;
55
+ overflow: hidden;
56
+ }
57
+ header {
58
+ background: var(--card);
59
+ border-bottom: 1px solid var(--border);
60
+ padding: 12px 24px;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: space-between;
64
+ }
65
+ .brand {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 12px;
69
+ }
70
+ .logo-badge {
71
+ background: var(--accent);
72
+ color: #000;
73
+ font-weight: 800;
74
+ font-size: 13px;
75
+ padding: 4px 8px;
76
+ border-radius: 6px;
77
+ letter-spacing: 0.5px;
78
+ }
79
+ .title {
80
+ font-size: 16px;
81
+ font-weight: 700;
82
+ color: var(--text);
83
+ }
84
+ .status-pill {
85
+ background: rgba(63, 185, 80, 0.15);
86
+ color: var(--green);
87
+ border: 1px solid rgba(63, 185, 80, 0.4);
88
+ padding: 4px 10px;
89
+ border-radius: 12px;
90
+ font-size: 12px;
91
+ font-weight: 600;
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 6px;
95
+ }
96
+ .status-dot {
97
+ width: 8px;
98
+ height: 8px;
99
+ background: var(--green);
100
+ border-radius: 50%;
101
+ }
102
+ nav {
103
+ display: flex;
104
+ gap: 8px;
105
+ background: var(--card);
106
+ padding: 8px 24px;
107
+ border-bottom: 1px solid var(--border);
108
+ }
109
+ .tab-btn {
110
+ background: transparent;
111
+ border: 1px solid transparent;
112
+ color: var(--muted);
113
+ padding: 6px 14px;
114
+ border-radius: 6px;
115
+ cursor: pointer;
116
+ font-size: 13px;
117
+ font-weight: 600;
118
+ transition: all 0.15s;
119
+ }
120
+ .tab-btn:hover {
121
+ color: var(--text);
122
+ background: rgba(255, 255, 255, 0.05);
123
+ }
124
+ .tab-btn.active {
125
+ color: #fff;
126
+ background: rgba(88, 166, 255, 0.15);
127
+ border-color: var(--accent);
128
+ }
129
+ main {
130
+ flex: 1;
131
+ display: flex;
132
+ overflow: hidden;
133
+ position: relative;
134
+ }
135
+ .tab-pane {
136
+ display: none;
137
+ width: 100%;
138
+ height: 100%;
139
+ overflow: auto;
140
+ padding: 24px;
141
+ }
142
+ .tab-pane.active {
143
+ display: flex;
144
+ flex-direction: column;
145
+ }
146
+ /* Graph Container */
147
+ #graph-pane {
148
+ padding: 0;
149
+ position: relative;
150
+ }
151
+ #graph-canvas {
152
+ width: 100%;
153
+ height: 100%;
154
+ background: radial-gradient(circle, rgba(22, 27, 34, 0.8) 0%, rgba(13, 17, 23, 1) 100%);
155
+ }
156
+ .graph-controls {
157
+ position: absolute;
158
+ top: 16px;
159
+ left: 16px;
160
+ background: rgba(22, 27, 34, 0.85);
161
+ backdrop-filter: blur(8px);
162
+ border: 1px solid var(--border);
163
+ border-radius: 8px;
164
+ padding: 12px;
165
+ display: flex;
166
+ flex-direction: column;
167
+ gap: 8px;
168
+ z-index: 10;
169
+ font-size: 12px;
170
+ }
171
+ .legend-item {
172
+ display: flex;
173
+ align-items: center;
174
+ gap: 8px;
175
+ }
176
+ .legend-color {
177
+ width: 12px;
178
+ height: 12px;
179
+ border-radius: 3px;
180
+ }
181
+ /* Tables & Cards */
182
+ .grid {
183
+ display: grid;
184
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
185
+ gap: 16px;
186
+ margin-bottom: 24px;
187
+ }
188
+ .stat-card {
189
+ background: var(--card);
190
+ border: 1px solid var(--border);
191
+ border-radius: 8px;
192
+ padding: 16px;
193
+ }
194
+ .stat-label {
195
+ font-size: 12px;
196
+ color: var(--muted);
197
+ font-weight: 600;
198
+ text-transform: uppercase;
199
+ letter-spacing: 0.5px;
200
+ }
201
+ .stat-val {
202
+ font-size: 24px;
203
+ font-weight: 700;
204
+ margin-top: 4px;
205
+ color: var(--text);
206
+ }
207
+ .data-table {
208
+ width: 100%;
209
+ border-collapse: collapse;
210
+ background: var(--card);
211
+ border: 1px solid var(--border);
212
+ border-radius: 8px;
213
+ overflow: hidden;
214
+ font-size: 13px;
215
+ }
216
+ .data-table th, .data-table td {
217
+ padding: 12px 16px;
218
+ text-align: left;
219
+ border-bottom: 1px solid var(--border);
220
+ }
221
+ .data-table th {
222
+ background: rgba(255, 255, 255, 0.03);
223
+ color: var(--muted);
224
+ font-weight: 600;
225
+ }
226
+ .badge {
227
+ display: inline-block;
228
+ padding: 2px 8px;
229
+ border-radius: 10px;
230
+ font-size: 11px;
231
+ font-weight: 600;
232
+ }
233
+ .badge-ok { background: rgba(63, 185, 80, 0.15); color: var(--green); border: 1px solid rgba(63, 185, 80, 0.3); }
234
+ .badge-warn { background: rgba(210, 153, 34, 0.15); color: var(--amber); border: 1px solid rgba(210, 153, 34, 0.3); }
235
+ .badge-err { background: rgba(248, 81, 73, 0.15); color: var(--red); border: 1px solid rgba(248, 81, 73, 0.3); }
236
+ .node-detail-panel {
237
+ position: absolute;
238
+ top: 16px;
239
+ right: 16px;
240
+ width: 340px;
241
+ background: rgba(22, 27, 34, 0.95);
242
+ border: 1px solid var(--border);
243
+ border-radius: 8px;
244
+ padding: 16px;
245
+ display: none;
246
+ z-index: 10;
247
+ backdrop-filter: blur(10px);
248
+ }
249
+ </style>
250
+ </head>
251
+ <body>
252
+ <header>
253
+ <div class="brand">
254
+ <span class="logo-badge">CODEGRAPH</span>
255
+ <span class="title">Visual Repository Intelligence</span>
256
+ <span id="repo-name" style="color: var(--muted); font-size: 13px;">Loading...</span>
257
+ </div>
258
+ <div class="status-pill">
259
+ <div class="status-dot"></div>
260
+ <span id="status-text">Index Synchronized</span>
261
+ </div>
262
+ </header>
263
+
264
+ <nav>
265
+ <button class="tab-btn active" onclick="switchTab('graph')">Knowledge Graph</button>
266
+ <button class="tab-btn" onclick="switchTab('routes')">Routes & API Drift</button>
267
+ <button class="tab-btn" onclick="switchTab('impact')">PR Blast Radius</button>
268
+ <button class="tab-btn" onclick="switchTab('watcher')">Watcher & Telemetry</button>
269
+ </nav>
270
+
271
+ <main>
272
+ <!-- TAB 1: GRAPH -->
273
+ <div id="graph-pane" class="tab-pane active">
274
+ <div class="graph-controls">
275
+ <div style="font-weight: 700; margin-bottom: 4px;">Node Legend</div>
276
+ <div class="legend-item"><div class="legend-color" style="background: var(--accent);"></div>Functions & Methods</div>
277
+ <div class="legend-item"><div class="legend-color" style="background: var(--purple);"></div>Classes & Modules</div>
278
+ <div class="legend-item"><div class="legend-color" style="background: var(--green);"></div>Routes (FastAPI / Express / Spring)</div>
279
+ <div class="legend-item"><div class="legend-color" style="background: var(--amber);"></div>Database Tables / Models</div>
280
+ <div class="legend-item"><div class="legend-color" style="background: var(--cyan);"></div>Interfaces & Traits</div>
281
+ <div style="margin-top: 12px; display: flex; flex-direction: column; gap: 6px;">
282
+ <button id="toggle-3d-btn" onclick="toggle3DMode()" style="background: var(--border); color: var(--text); border: 1px solid var(--border); padding: 5px 8px; border-radius: 4px; cursor: pointer; font-size: 11px;">🌐 Switch to 3D Orbital</button>
283
+ <button id="export-btn" onclick="exportGraphImage()" style="background: var(--border); color: var(--text); border: 1px solid var(--border); padding: 5px 8px; border-radius: 4px; cursor: pointer; font-size: 11px;">📷 Export Snapshot</button>
284
+ </div>
285
+ </div>
286
+ <canvas id="graph-canvas"></canvas>
287
+ <div id="node-panel" class="node-detail-panel">
288
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
289
+ <h4 id="panel-title" style="color: var(--accent);">Node Detail</h4>
290
+ <button onclick="document.getElementById('node-panel').style.display='none'" style="background:none; border:none; color:var(--muted); cursor:pointer;">✕</button>
291
+ </div>
292
+ <div id="panel-content" style="font-size: 12px; color: var(--muted); line-height: 1.6;"></div>
293
+ </div>
294
+ </div>
295
+
296
+ <!-- TAB 2: ROUTES & DRIFT -->
297
+ <div id="routes-pane" class="tab-pane">
298
+ <h3 style="margin-bottom: 16px;">Full-Stack Routes & Cross-Language Drift</h3>
299
+ <table class="data-table">
300
+ <thead>
301
+ <tr>
302
+ <th>HTTP Method</th>
303
+ <th>Normalized Route</th>
304
+ <th>Backend Handler</th>
305
+ <th>File & Line</th>
306
+ <th>Drift Status</th>
307
+ </tr>
308
+ </thead>
309
+ <tbody id="routes-tbody">
310
+ <tr><td colspan="5" style="text-align: center; color: var(--muted);">Loading routes...</td></tr>
311
+ </tbody>
312
+ </table>
313
+ </div>
314
+
315
+ <!-- TAB 3: IMPACT -->
316
+ <div id="impact-pane" class="tab-pane">
317
+ <div class="grid">
318
+ <div class="stat-card">
319
+ <div class="stat-label">PR Risk Score</div>
320
+ <div class="stat-val" id="risk-score">0 / 100</div>
321
+ <div style="font-size: 12px; color: var(--green); margin-top: 4px;" id="risk-label">LOW RISK</div>
322
+ </div>
323
+ <div class="stat-card">
324
+ <div class="stat-label">Impacted Symbols</div>
325
+ <div class="stat-val" id="impacted-syms">0</div>
326
+ </div>
327
+ <div class="stat-card">
328
+ <div class="stat-label">Impacted Routes</div>
329
+ <div class="stat-val" id="impacted-routes">0</div>
330
+ </div>
331
+ <div class="stat-card">
332
+ <div class="stat-label">Recommended Test Suite</div>
333
+ <div class="stat-val" style="font-size: 14px; font-family: monospace; color: var(--accent);" id="test-cmd">pytest -q</div>
334
+ </div>
335
+ </div>
336
+ <h4 style="margin-bottom: 12px;">Downstream Blast Radius Breakdown</h4>
337
+ <div id="impact-details" style="background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; font-size: 13px; color: var(--muted);">
338
+ Clean working tree. No active uncommitted modifications detected.
339
+ </div>
340
+ </div>
341
+
342
+ <!-- TAB 4: WATCHER & TELEMETRY -->
343
+ <div id="watcher-pane" class="tab-pane">
344
+ <div class="grid">
345
+ <div class="stat-card">
346
+ <div class="stat-label">Total Symbols</div>
347
+ <div class="stat-val" id="stat-symbols">0</div>
348
+ </div>
349
+ <div class="stat-card">
350
+ <div class="stat-label">Graph Edges</div>
351
+ <div class="stat-val" id="stat-edges">0</div>
352
+ </div>
353
+ <div class="stat-card">
354
+ <div class="stat-label">Indexed Files</div>
355
+ <div class="stat-val" id="stat-files">0</div>
356
+ </div>
357
+ <div class="stat-card">
358
+ <div class="stat-label">Reindex Latency</div>
359
+ <div class="stat-val" style="color: var(--green);">&lt; 15ms</div>
360
+ </div>
361
+ </div>
362
+ <h4 style="margin-bottom: 12px;">System & Engine Health</h4>
363
+ <table class="data-table">
364
+ <tbody>
365
+ <tr><td>Operating Engine</td><td>CodeGraph Deterministic Intelligence v3.0.1</td></tr>
366
+ <tr><td>Database Mode</td><td>SQLite WAL (Permanent Concurrency)</td></tr>
367
+ <tr><td>Watcher Daemon</td><td>Sub-15ms Sliding Kernel Push (Active)</td></tr>
368
+ <tr><td>MCP Active Tools</td><td>70 Verified Deterministic Tools</td></tr>
369
+ </tbody>
370
+ </table>
371
+ </div>
372
+ </main>
373
+
374
+ <script>
375
+ function switchTab(tabId) {
376
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
377
+ document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
378
+ event.target.classList.add('active');
379
+ document.getElementById(tabId + '-pane').classList.add('active');
380
+ if (tabId === 'graph') renderGraph();
381
+ }
382
+
383
+ async function loadData() {
384
+ try {
385
+ const res = await fetch('/api/status');
386
+ const data = await res.json();
387
+ document.getElementById('repo-name').textContent = data.repository || '';
388
+ document.getElementById('stat-symbols').textContent = data.symbols || 0;
389
+ document.getElementById('stat-edges').textContent = data.edges || 0;
390
+ document.getElementById('stat-files').textContent = data.files || 0;
391
+
392
+ loadRoutes();
393
+ loadImpact();
394
+ } catch (e) {
395
+ console.error(e);
396
+ }
397
+ }
398
+
399
+ async function loadRoutes() {
400
+ try {
401
+ const res = await fetch('/api/routes');
402
+ const routes = await res.json();
403
+ const tbody = document.getElementById('routes-tbody');
404
+ if (!routes.length) {
405
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: var(--muted);">No framework routes discovered yet.</td></tr>';
406
+ return;
407
+ }
408
+ tbody.innerHTML = routes.map(r => `
409
+ <tr>
410
+ <td><span class="badge badge-ok">${r.http_method || 'GET'}</span></td>
411
+ <td style="font-family: monospace; color: var(--accent); font-weight: 600;">${r.route_path || r.normalized_route}</td>
412
+ <td>${r.handler_name || 'Anonymous Handler'}</td>
413
+ <td style="color: var(--muted);">${r.file_path}:${r.line}</td>
414
+ <td><span class="badge badge-ok">SYNCHRONIZED</span></td>
415
+ </tr>
416
+ `).join('');
417
+ } catch (e) {}
418
+ }
419
+
420
+ async function loadImpact() {
421
+ try {
422
+ const res = await fetch('/api/impact');
423
+ const impact = await res.json();
424
+ if (impact.risk_score !== undefined) {
425
+ document.getElementById('risk-score').textContent = impact.risk_score + ' / 100';
426
+ document.getElementById('risk-label').textContent = impact.risk_category || 'LOW RISK';
427
+ document.getElementById('impacted-syms').textContent = impact.impacted_symbols_count || 0;
428
+ document.getElementById('impacted-routes').textContent = impact.impacted_routes_count || 0;
429
+ document.getElementById('test-cmd').textContent = impact.recommended_test_command || 'pytest -q';
430
+ }
431
+ } catch (e) {}
432
+ }
433
+
434
+ // Force-directed Canvas Simulation
435
+ let nodes = [];
436
+ let edges = [];
437
+ let is3D = false;
438
+ let angleX = 0.2;
439
+ let angleY = 0.3;
440
+ let isDragging = false;
441
+ let prevMouseX = 0;
442
+ let prevMouseY = 0;
443
+ let blastRipples = [];
444
+
445
+ function toggle3DMode() {
446
+ is3D = !is3D;
447
+ document.getElementById('toggle-3d-btn').textContent = is3D ? '📊 Switch to 2D Plane' : '🌐 Switch to 3D Orbital';
448
+ }
449
+
450
+ function exportGraphImage() {
451
+ const canvas = document.getElementById('graph-canvas');
452
+ const link = document.createElement('a');
453
+ link.download = 'codegraph-architecture.png';
454
+ link.href = canvas.toDataURL('image/png');
455
+ link.click();
456
+ }
457
+
458
+ async function renderGraph() {
459
+ const canvas = document.getElementById('graph-canvas');
460
+ canvas.width = canvas.parentElement.clientWidth;
461
+ canvas.height = canvas.parentElement.clientHeight;
462
+ const ctx = canvas.getContext('2d');
463
+
464
+ if (!nodes.length) {
465
+ const res = await fetch('/api/graph');
466
+ const graphData = await res.json();
467
+ nodes = graphData.nodes || [];
468
+ edges = graphData.edges || [];
469
+
470
+ nodes.forEach(n => {
471
+ n.x = (Math.random() - 0.5) * 400;
472
+ n.y = (Math.random() - 0.5) * 400;
473
+ n.z = (Math.random() - 0.5) * 400;
474
+ n.vx = 0; n.vy = 0; n.vz = 0;
475
+ n.screenX = 0; n.screenY = 0;
476
+ });
477
+ }
478
+
479
+ function project(x, y, z) {
480
+ if (!is3D) {
481
+ return { x: canvas.width / 2 + x, y: canvas.height / 2 + y, scale: 1.0, z: 0 };
482
+ }
483
+ // Rotate around Y axis
484
+ const cosY = Math.cos(angleY), sinY = Math.sin(angleY);
485
+ const x1 = x * cosY + z * sinY;
486
+ const z1 = -x * sinY + z * cosY;
487
+
488
+ // Rotate around X axis
489
+ const cosX = Math.cos(angleX), sinX = Math.sin(angleX);
490
+ const y2 = y * cosX - z1 * sinX;
491
+ const z2 = y * sinX + z1 * cosX;
492
+
493
+ const focal = 600;
494
+ const scale = focal / (focal + z2 + 300);
495
+ return {
496
+ x: canvas.width / 2 + x1 * scale,
497
+ y: canvas.height / 2 + y2 * scale,
498
+ scale: Math.max(0.2, scale),
499
+ z: z2,
500
+ };
501
+ }
502
+
503
+ function tick() {
504
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
505
+
506
+ // Project nodes
507
+ nodes.forEach(n => {
508
+ const p = project(n.x, n.y, n.z);
509
+ n.screenX = p.x;
510
+ n.screenY = p.y;
511
+ n.scale = p.scale;
512
+ n.depthZ = p.z;
513
+ });
514
+
515
+ // Draw blast ripples
516
+ for (let i = blastRipples.length - 1; i >= 0; i--) {
517
+ const rip = blastRipples[i];
518
+ rip.radius += 4;
519
+ rip.alpha -= 0.02;
520
+ if (rip.alpha <= 0) {
521
+ blastRipples.splice(i, 1);
522
+ continue;
523
+ }
524
+ ctx.beginPath();
525
+ ctx.arc(rip.x, rip.y, rip.radius, 0, 2 * Math.PI);
526
+ ctx.strokeStyle = `rgba(248, 81, 73, ${rip.alpha})`;
527
+ ctx.lineWidth = 3;
528
+ ctx.stroke();
529
+ }
530
+
531
+ // Draw edges
532
+ ctx.lineWidth = 1;
533
+ edges.forEach(e => {
534
+ const s = nodes[e.sourceIndex];
535
+ const t = nodes[e.targetIndex];
536
+ if (s && t) {
537
+ ctx.strokeStyle = 'rgba(88, 166, 255, 0.25)';
538
+ ctx.beginPath();
539
+ ctx.moveTo(s.screenX, s.screenY);
540
+ ctx.lineTo(t.screenX, t.screenY);
541
+ ctx.stroke();
542
+ }
543
+ });
544
+
545
+ // Sort nodes by depth for 3D realism
546
+ const sortedNodes = is3D ? [...nodes].sort((a, b) => b.depthZ - a.depthZ) : nodes;
547
+
548
+ // Draw nodes
549
+ sortedNodes.forEach(n => {
550
+ const r = (n.radius || 6) * (n.scale || 1.0);
551
+ ctx.beginPath();
552
+ ctx.arc(n.screenX, n.screenY, r, 0, 2 * Math.PI);
553
+ ctx.fillStyle = n.color || '#58a6ff';
554
+ ctx.shadowColor = n.color || '#58a6ff';
555
+ ctx.shadowBlur = is3D ? 8 : 0;
556
+ ctx.fill();
557
+ ctx.shadowBlur = 0;
558
+
559
+ // Labels
560
+ if (nodes.length < 160 && (n.scale || 1.0) > 0.6) {
561
+ ctx.fillStyle = '#8b949e';
562
+ ctx.font = `${Math.floor(10 * (n.scale || 1.0))}px monospace`;
563
+ ctx.fillText(n.name, n.screenX + 8, n.screenY + 3);
564
+ }
565
+ });
566
+
567
+ // 3D Orbital rotation momentum if not dragging
568
+ if (is3D && !isDragging) {
569
+ angleY += 0.002;
570
+ }
571
+
572
+ // Physics step
573
+ nodes.forEach(n => {
574
+ n.vx -= n.x * 0.0008;
575
+ n.vy -= n.y * 0.0008;
576
+ n.vz -= n.z * 0.0008;
577
+ n.x += n.vx; n.y += n.vy; n.z += n.vz;
578
+ n.vx *= 0.88; n.vy *= 0.88; n.vz *= 0.88;
579
+ });
580
+ }
581
+
582
+ setInterval(() => {
583
+ if (document.getElementById("graph-pane").classList.contains("active")) tick();
584
+ }, 30);
585
+ }
586
+
587
+ const canvasElem = document.getElementById('graph-canvas');
588
+ canvasElem.addEventListener('mousedown', e => {
589
+ isDragging = true;
590
+ prevMouseX = e.clientX;
591
+ prevMouseY = e.clientY;
592
+ });
593
+
594
+ window.addEventListener('mouseup', () => { isDragging = false; });
595
+
596
+ canvasElem.addEventListener('mousemove', e => {
597
+ if (isDragging && is3D) {
598
+ const dx = e.clientX - prevMouseX;
599
+ const dy = e.clientY - prevMouseY;
600
+ angleY += dx * 0.008;
601
+ angleX += dy * 0.008;
602
+ prevMouseX = e.clientX;
603
+ prevMouseY = e.clientY;
604
+ }
605
+ });
606
+
607
+ canvasElem.addEventListener('click', e => {
608
+ const rect = canvasElem.getBoundingClientRect();
609
+ const x = e.clientX - rect.left;
610
+ const y = e.clientY - rect.top;
611
+ const hit = nodes.find(n => Math.hypot(n.screenX - x, n.screenY - y) < 14);
612
+ if (hit) {
613
+ // Trigger blast ripple
614
+ blastRipples.push({ x: hit.screenX, y: hit.screenY, radius: 8, alpha: 1.0 });
615
+
616
+ const panel = document.getElementById('node-panel');
617
+ document.getElementById('panel-title').textContent = hit.name;
618
+ document.getElementById('panel-content').innerHTML = `
619
+ <strong>Kind:</strong> ${hit.kind || 'Symbol'}<br>
620
+ <strong>File:</strong> ${hit.path || 'unknown'}:${hit.line || 1}<br>
621
+ <strong>Language:</strong> ${hit.language || 'polyglot'}<br>
622
+ <strong>Canonical ID:</strong> <span style="font-family:monospace;">${hit.id || hit.name}</span>
623
+ `;
624
+ panel.style.display = 'block';
625
+ }
626
+ });
627
+
628
+ loadData();
629
+ window.addEventListener('resize', () => {
630
+ if (document.getElementById('graph-pane').classList.contains('active')) renderGraph();
631
+ });
632
+ </script>
633
+ </body>
634
+ </html>
635
+ """
636
+
637
+
638
+ class CodeGraphUIRequestHandler(BaseHTTPRequestHandler):
639
+ """HTTP request handler for serving the CodeGraph Dashboard and REST endpoints."""
640
+
641
+ indexer: Indexer
642
+
643
+ def do_GET(self) -> None: # noqa: N802
644
+ parsed = urllib.parse.urlparse(self.path)
645
+ path = parsed.path
646
+
647
+ if path in ("/", "/ui", "/index.html"):
648
+ self.send_response(200)
649
+ self.send_header("Content-Type", "text/html; charset=utf-8")
650
+ self.end_headers()
651
+ self.wfile.write(_get_html_dashboard().encode("utf-8"))
652
+ return
653
+
654
+ if path == "/api/status":
655
+ self._handle_api_status()
656
+ return
657
+
658
+ if path == "/api/drift":
659
+ self._handle_api_drift()
660
+ return
661
+
662
+ if path == "/api/routes":
663
+ self._handle_api_routes()
664
+ return
665
+
666
+ if path == "/api/impact":
667
+ self._handle_api_impact()
668
+ return
669
+
670
+ if path == "/api/graph":
671
+ self._handle_api_graph()
672
+ return
673
+
674
+ self.send_response(404)
675
+ self.end_headers()
676
+ self.wfile.write(b"Not Found")
677
+
678
+ def do_OPTIONS(self) -> None:
679
+ self.send_response(204)
680
+ self.send_header("Access-Control-Allow-Origin", "*")
681
+ self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
682
+ self.send_header("Access-Control-Allow-Headers", "*")
683
+ self.end_headers()
684
+
685
+ def _handle_api_drift(self) -> None:
686
+ try:
687
+ from codegraph.api_drift import detect_api_contract_drift
688
+
689
+ with self.indexer.session() as con:
690
+ drift_report = detect_api_contract_drift(con, self.indexer.repository)
691
+ self._send_json(drift_report.as_dict())
692
+ except Exception as e:
693
+ self._send_json({"drifts": [], "error": str(e)})
694
+
695
+ def _handle_api_status(self) -> None:
696
+ try:
697
+ with self.indexer.session() as con:
698
+ file_count = con.execute("SELECT count(*) FROM files WHERE status='ok'").fetchone()[0]
699
+ sym_count = con.execute("SELECT count(*) FROM symbols").fetchone()[0]
700
+ edge_count = con.execute("SELECT count(*) FROM graph_edges").fetchone()[0]
701
+ grow = con.execute("SELECT value FROM metadata WHERE key='index_generation'").fetchone()
702
+ gen = int(grow[0]) if grow and grow[0] else 1
703
+
704
+ tables_raw = con.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").fetchall()
705
+ tables = [
706
+ {
707
+ "table": str(t[0]),
708
+ "name": str(t[0]),
709
+ "framework": "SQLite",
710
+ "writers": [],
711
+ "readers": [],
712
+ }
713
+ for t in tables_raw
714
+ ]
715
+
716
+ payload = {
717
+ "repository": str(self.indexer.repository),
718
+ "files": file_count,
719
+ "symbols": sym_count,
720
+ "edges": edge_count,
721
+ "generation": gen,
722
+ "status": "synchronized",
723
+ "tables": tables,
724
+ }
725
+ except Exception as exc:
726
+ payload = {"error": str(exc), "status": "error"}
727
+
728
+ self._send_json(payload)
729
+
730
+ def _handle_api_routes(self) -> None:
731
+ try:
732
+ with self.indexer.session() as con:
733
+ rows = con.execute(
734
+ "SELECT endpoint_id, framework, http_method, route_path, "
735
+ "normalized_route, handler_name, file_path, line FROM framework_routes LIMIT 200"
736
+ ).fetchall()
737
+ routes = [
738
+ {
739
+ "id": str(r["endpoint_id"]),
740
+ "framework": str(r["framework"]),
741
+ "method": str(r["http_method"]),
742
+ "path": str(r["route_path"]),
743
+ "normalized_route": str(r["normalized_route"]),
744
+ "handler": str(r["handler_name"]),
745
+ "filePath": str(r["file_path"]),
746
+ "line": int(r["line"]),
747
+ }
748
+ for r in rows
749
+ ]
750
+ except Exception:
751
+ routes = []
752
+
753
+ self._send_json({"routes": routes})
754
+
755
+ def _handle_api_impact(self) -> None:
756
+ try:
757
+ from codegraph.change_impact import get_deep_change_impact
758
+
759
+ query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
760
+ target = query.get("target", [None])[0]
761
+
762
+ with self.indexer.session() as con:
763
+ report = get_deep_change_impact(self.indexer.repository, con, symbol=target)
764
+ payload = report.as_dict()
765
+ except Exception:
766
+ payload = {
767
+ "risk_score": 0,
768
+ "risk_category": "LOW",
769
+ "impacted_symbols_count": 0,
770
+ "impacted_routes_count": 0,
771
+ "recommended_test_command": "pytest -q",
772
+ }
773
+ self._send_json(payload)
774
+
775
+ def _handle_api_graph(self) -> None:
776
+ try:
777
+ query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
778
+ limit = int(query.get("max_nodes", [200])[0])
779
+ lang_filter = query.get("language", [None])[0]
780
+ with self.indexer.session() as con:
781
+ sql = "SELECT id, name, kind, path, start_line, language FROM symbols"
782
+ params: list[Any] = []
783
+ if lang_filter:
784
+ sql += " WHERE language = ?"
785
+ params.append(lang_filter)
786
+ sql += f" LIMIT {limit}"
787
+
788
+ sym_rows = con.execute(sql, params).fetchall()
789
+ node_map: dict[str, int] = {}
790
+ nodes = []
791
+ for idx, r in enumerate(sym_rows):
792
+ nid = str(r["id"])
793
+ node_map[nid] = idx
794
+ kind = str(r["kind"]).upper()
795
+ lang = str(r["language"] if "language" in r.keys() else "unknown")
796
+ color = "#58a6ff"
797
+ if "CLASS" in kind or "STRUCT" in kind:
798
+ color = "#bc8cff"
799
+ elif "ROUTE" in kind:
800
+ color = "#3fb950"
801
+ elif "TABLE" in kind:
802
+ color = "#d29922"
803
+ elif "INTERFACE" in kind:
804
+ color = "#38bdf8"
805
+ nodes.append({
806
+ "id": nid,
807
+ "name": str(r["name"]),
808
+ "kind": kind,
809
+ "path": str(r["path"]),
810
+ "line": int(r["start_line"]),
811
+ "language": lang,
812
+ "color": color,
813
+ })
814
+
815
+ edge_rows = con.execute(
816
+ "SELECT source, target, relationship FROM graph_edges LIMIT 500"
817
+ ).fetchall()
818
+ edges = []
819
+ for e in edge_rows:
820
+ src_id = str(e["source"])
821
+ tgt_id = str(e["target"])
822
+ if src_id in node_map and tgt_id in node_map:
823
+ edges.append({
824
+ "sourceIndex": node_map[src_id],
825
+ "targetIndex": node_map[tgt_id],
826
+ "relationship": str(e["relationship"]),
827
+ })
828
+
829
+ payload = {"nodes": nodes, "edges": edges}
830
+ except Exception:
831
+ payload = {"nodes": [], "edges": []}
832
+
833
+ self._send_json(payload)
834
+
835
+ def _send_json(self, data: Any) -> None:
836
+ body = json.dumps(data).encode("utf-8")
837
+ self.send_response(200)
838
+ self.send_header("Content-Type", "application/json")
839
+ self.send_header("Content-Length", str(len(body)))
840
+ self.send_header("Access-Control-Allow-Origin", "*")
841
+ self.end_headers()
842
+ self.wfile.write(body)
843
+
844
+ def log_message(self, format: str, *args: Any) -> None: # noqa: A002
845
+ pass
846
+
847
+
848
+ def start_ui_server(
849
+ repository: Path,
850
+ host: str = "127.0.0.1",
851
+ port: int = 8765,
852
+ open_browser: bool = True,
853
+ ) -> None:
854
+ """Launch the CodeGraph Interactive Dashboard server."""
855
+ indexer = Indexer(repository.resolve())
856
+
857
+ class _CustomHandler(CodeGraphUIRequestHandler):
858
+ pass
859
+
860
+ _CustomHandler.indexer = indexer
861
+
862
+ server = ThreadingHTTPServer((host, port), _CustomHandler)
863
+ url = f"http://{host}:{port}/ui"
864
+
865
+ print(f"CodeGraph Visual Dashboard running at {url}")
866
+ print("Press Ctrl+C to stop.")
867
+
868
+ if open_browser:
869
+ threading.Timer(0.4, lambda: webbrowser.open(url)).start()
870
+
871
+ try:
872
+ server.serve_forever()
873
+ except KeyboardInterrupt:
874
+ print("\nStopping CodeGraph Dashboard.")
875
+ finally:
876
+ server.server_close()