polycodegraph 0.1.0__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 (67) hide show
  1. codegraph/__init__.py +10 -0
  2. codegraph/analysis/__init__.py +30 -0
  3. codegraph/analysis/_common.py +125 -0
  4. codegraph/analysis/blast_radius.py +63 -0
  5. codegraph/analysis/cycles.py +79 -0
  6. codegraph/analysis/dataflow.py +861 -0
  7. codegraph/analysis/dead_code.py +165 -0
  8. codegraph/analysis/hotspots.py +68 -0
  9. codegraph/analysis/infrastructure.py +439 -0
  10. codegraph/analysis/metrics.py +52 -0
  11. codegraph/analysis/report.py +222 -0
  12. codegraph/analysis/roles.py +323 -0
  13. codegraph/analysis/untested.py +79 -0
  14. codegraph/cli.py +1506 -0
  15. codegraph/config.py +64 -0
  16. codegraph/embed/__init__.py +35 -0
  17. codegraph/embed/chunker.py +120 -0
  18. codegraph/embed/embedder.py +113 -0
  19. codegraph/embed/query.py +181 -0
  20. codegraph/embed/store.py +360 -0
  21. codegraph/graph/__init__.py +0 -0
  22. codegraph/graph/builder.py +212 -0
  23. codegraph/graph/schema.py +69 -0
  24. codegraph/graph/store_networkx.py +55 -0
  25. codegraph/graph/store_sqlite.py +249 -0
  26. codegraph/mcp_server/__init__.py +6 -0
  27. codegraph/mcp_server/server.py +933 -0
  28. codegraph/parsers/__init__.py +0 -0
  29. codegraph/parsers/base.py +70 -0
  30. codegraph/parsers/go.py +570 -0
  31. codegraph/parsers/python.py +1707 -0
  32. codegraph/parsers/typescript.py +1397 -0
  33. codegraph/py.typed +0 -0
  34. codegraph/resolve/__init__.py +4 -0
  35. codegraph/resolve/calls.py +480 -0
  36. codegraph/review/__init__.py +31 -0
  37. codegraph/review/baseline.py +32 -0
  38. codegraph/review/differ.py +211 -0
  39. codegraph/review/hook.py +70 -0
  40. codegraph/review/risk.py +219 -0
  41. codegraph/review/rules.py +342 -0
  42. codegraph/viz/__init__.py +17 -0
  43. codegraph/viz/_style.py +45 -0
  44. codegraph/viz/dashboard.py +740 -0
  45. codegraph/viz/diagrams.py +370 -0
  46. codegraph/viz/explore.py +453 -0
  47. codegraph/viz/hld.py +683 -0
  48. codegraph/viz/html.py +115 -0
  49. codegraph/viz/mermaid.py +111 -0
  50. codegraph/viz/svg.py +77 -0
  51. codegraph/web/__init__.py +4 -0
  52. codegraph/web/server.py +165 -0
  53. codegraph/web/static/app.css +664 -0
  54. codegraph/web/static/app.js +919 -0
  55. codegraph/web/static/index.html +112 -0
  56. codegraph/web/static/views/architecture.js +1671 -0
  57. codegraph/web/static/views/graph3d.css +564 -0
  58. codegraph/web/static/views/graph3d.js +999 -0
  59. codegraph/web/static/views/graph3d_transform.js +984 -0
  60. codegraph/workspace/__init__.py +34 -0
  61. codegraph/workspace/config.py +110 -0
  62. codegraph/workspace/operations.py +294 -0
  63. polycodegraph-0.1.0.dist-info/METADATA +687 -0
  64. polycodegraph-0.1.0.dist-info/RECORD +67 -0
  65. polycodegraph-0.1.0.dist-info/WHEEL +4 -0
  66. polycodegraph-0.1.0.dist-info/entry_points.txt +2 -0
  67. polycodegraph-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,740 @@
1
+ """Single-page tabbed dashboard combining diagrams + node-link views."""
2
+ from __future__ import annotations
3
+
4
+ from collections import Counter
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import networkx as nx
9
+
10
+ from codegraph.analysis import (
11
+ compute_metrics,
12
+ find_cycles,
13
+ find_dead_code,
14
+ find_hotspots,
15
+ find_untested,
16
+ )
17
+ from codegraph.analysis.infrastructure import detect_infrastructure
18
+ from codegraph.viz._style import kind_str
19
+ from codegraph.viz.diagrams import (
20
+ build_matrix,
21
+ build_sankey,
22
+ build_treemap,
23
+ pick_flow_entry_points,
24
+ render_flow_diagram,
25
+ to_json,
26
+ )
27
+ from codegraph.viz.hld import build_hld
28
+
29
+
30
+ def _attach_handler_dataflow(
31
+ architecture: dict[str, Any], graph: nx.MultiDiGraph
32
+ ) -> None:
33
+ """Attach per-handler dataflow hops (with arg_flow) to architecture payload.
34
+
35
+ The architecture view's Learn Mode modal expects each handler to carry a
36
+ ``dataflow`` field containing the v0.3 hop chain. ``detect_infrastructure``
37
+ only emits the structural fields, so we enrich here from
38
+ :func:`shape_hops_for_handler`.
39
+ """
40
+ from codegraph.analysis.dataflow import shape_hops_for_handler
41
+
42
+ for h in architecture.get("handlers") or []:
43
+ qn = h.get("qualname") or ""
44
+ method = h.get("method") or ""
45
+ path = h.get("path") or ""
46
+ if not qn:
47
+ continue
48
+ try:
49
+ h["dataflow"] = shape_hops_for_handler(
50
+ graph, qn, method=method, path=path
51
+ )
52
+ except Exception:
53
+ continue
54
+
55
+
56
+ def _hotspot_scores_by_file(graph: nx.MultiDiGraph) -> dict[str, int]:
57
+ scores: dict[str, int] = {}
58
+ for h in find_hotspots(graph, limit=10_000):
59
+ scores[h.file] = max(scores.get(h.file, 0), h.score)
60
+ return scores
61
+
62
+
63
+ def _strip_noise(graph: nx.MultiDiGraph) -> nx.MultiDiGraph:
64
+ drop: list[str] = []
65
+ for nid, attrs in graph.nodes(data=True):
66
+ if isinstance(nid, str) and nid.startswith("unresolved::"):
67
+ drop.append(nid)
68
+ continue
69
+ kind = kind_str(attrs.get("kind"))
70
+ if kind == "FILE":
71
+ drop.append(nid)
72
+ continue
73
+ # External / language-stub MODULE nodes have no file path and no
74
+ # symbols inside them; they only clutter sankey + treemap.
75
+ if kind == "MODULE" and not attrs.get("file"):
76
+ drop.append(nid)
77
+ if not drop:
78
+ return graph
79
+ g = graph.copy()
80
+ g.remove_nodes_from(drop)
81
+ return g
82
+
83
+
84
+ def _flows_payload(graph: nx.MultiDiGraph, limit: int = 8) -> list[dict[str, Any]]:
85
+ out: list[dict[str, Any]] = []
86
+ for entry in pick_flow_entry_points(graph, limit=limit):
87
+ diagram = render_flow_diagram(graph, entry["id"])
88
+ if not diagram:
89
+ continue
90
+ out.append(
91
+ {
92
+ "qualname": entry["qualname"],
93
+ "file": entry["file"],
94
+ "reason": entry["reason"],
95
+ "mermaid": diagram,
96
+ }
97
+ )
98
+ return out
99
+
100
+
101
+ def _file_stats(graph: nx.MultiDiGraph) -> list[dict[str, Any]]:
102
+ counts: Counter[str] = Counter()
103
+ languages: dict[str, str] = {}
104
+ for _nid, attrs in graph.nodes(data=True):
105
+ f = attrs.get("file")
106
+ if not isinstance(f, str) or not f:
107
+ continue
108
+ counts[f] += 1
109
+ if attrs.get("language"):
110
+ languages[f] = str(attrs["language"])
111
+ return [
112
+ {"file": f, "symbols": c, "language": languages.get(f, "")}
113
+ for f, c in counts.most_common(80)
114
+ ]
115
+
116
+
117
+ def build_dashboard_payload(
118
+ graph: nx.MultiDiGraph,
119
+ *,
120
+ matrix_top_n: int = 36,
121
+ sankey_links: int = 50,
122
+ flow_count: int = 8,
123
+ ) -> dict[str, Any]:
124
+ """Compute the full data payload (no HTML). Pure: no I/O."""
125
+ cleaned = _strip_noise(graph)
126
+ metrics = compute_metrics(cleaned)
127
+ cycles = find_cycles(cleaned)
128
+ dead = find_dead_code(cleaned)
129
+ untested = find_untested(cleaned)
130
+ hotspots = find_hotspots(cleaned, limit=15)
131
+
132
+ matrix = build_matrix(cleaned, top_n=matrix_top_n)
133
+ sankey = build_sankey(cleaned, max_links=sankey_links)
134
+ treemap = build_treemap(cleaned, hotspot_scores=_hotspot_scores_by_file(cleaned))
135
+ flows = _flows_payload(cleaned, limit=flow_count)
136
+ files = _file_stats(cleaned)
137
+ hld = build_hld(cleaned)
138
+ architecture = detect_infrastructure(graph)
139
+ _attach_handler_dataflow(architecture, graph)
140
+
141
+ return {
142
+ "metrics": {
143
+ "nodes": metrics.total_nodes,
144
+ "edges": metrics.total_edges,
145
+ "unresolved": metrics.unresolved_edges,
146
+ "by_kind": metrics.nodes_by_kind,
147
+ "by_edge": metrics.edges_by_kind,
148
+ "languages": metrics.languages,
149
+ },
150
+ "issues": {
151
+ "cycles": cycles.total,
152
+ "dead": len(dead),
153
+ "untested": len(untested),
154
+ },
155
+ "hotspots": [
156
+ {
157
+ "qualname": h.qualname,
158
+ "file": h.file,
159
+ "fan_in": h.fan_in,
160
+ "fan_out": h.fan_out,
161
+ "loc": h.loc,
162
+ "score": h.score,
163
+ }
164
+ for h in hotspots
165
+ ],
166
+ "matrix": {
167
+ "modules": matrix.modules,
168
+ "counts": matrix.counts,
169
+ "max": matrix.max_count,
170
+ },
171
+ "sankey": sankey,
172
+ "treemap": treemap,
173
+ "flows": flows,
174
+ "files": files,
175
+ "hld": {
176
+ "layers": hld.layers,
177
+ "components": hld.components,
178
+ "edges": hld.edges,
179
+ "modules": hld.modules,
180
+ "metrics": hld.metrics,
181
+ "mermaid_layered": hld.mermaid_layered,
182
+ "mermaid_context": hld.mermaid_context,
183
+ },
184
+ "architecture": architecture,
185
+ }
186
+
187
+
188
+ # pragma: codegraph-public-api
189
+ def render_dashboard(
190
+ graph: nx.MultiDiGraph,
191
+ out_path: Path,
192
+ *,
193
+ matrix_top_n: int = 36,
194
+ sankey_links: int = 50,
195
+ flow_count: int = 8,
196
+ ) -> Path:
197
+ """Render the (legacy) single-page dashboard to ``out_path``.
198
+
199
+ The new web UI in ``codegraph/web/`` is the preferred path; this
200
+ function is retained for offline ``codegraph explore`` output.
201
+ """
202
+ payload = build_dashboard_payload(
203
+ graph,
204
+ matrix_top_n=matrix_top_n,
205
+ sankey_links=sankey_links,
206
+ flow_count=flow_count,
207
+ )
208
+ out_path.parent.mkdir(parents=True, exist_ok=True)
209
+ out_path.write_text(_HTML_TEMPLATE.replace("__DATA__", to_json(payload)),
210
+ encoding="utf-8")
211
+ return out_path
212
+
213
+
214
+ _HTML_TEMPLATE = r"""<!DOCTYPE html>
215
+ <html lang="en"><head><meta charset="utf-8">
216
+ <title>codegraph dashboard</title>
217
+ <script src="https://d3js.org/d3.v7.min.js"></script>
218
+ <script src="https://cdn.jsdelivr.net/npm/d3-sankey@0.12.3/dist/d3-sankey.min.js"></script>
219
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js"></script>
220
+ <style>
221
+ :root { color-scheme: dark; --bg: #0b1220; --panel: #131c2e; --border: #243049;
222
+ --muted: #94a3b8; --fg: #e2e8f0; --accent: #818cf8; --accent2: #22d3ee;
223
+ --hot: #f43f5e; --warm: #f59e0b; --cool: #38bdf8; }
224
+ * { box-sizing: border-box; }
225
+ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,
226
+ sans-serif; background: var(--bg); color: var(--fg); }
227
+ header { display: flex; align-items: center; justify-content: space-between;
228
+ padding: 18px 28px; border-bottom: 1px solid var(--border);
229
+ background: linear-gradient(180deg, #0d1426, #0b1220); position: sticky;
230
+ top: 0; z-index: 10; }
231
+ h1 { margin: 0; font-size: 18px; font-weight: 600; letter-spacing: 0.01em; }
232
+ h1 small { color: var(--muted); font-weight: 400; margin-left: 10px; font-size: 13px; }
233
+ nav.tabs { display: flex; gap: 4px; flex-wrap: wrap; }
234
+ nav.tabs button { background: transparent; color: var(--muted); border: 1px solid
235
+ transparent; border-radius: 6px; padding: 7px 12px; cursor: pointer;
236
+ font-size: 13px; font-weight: 500; }
237
+ nav.tabs button:hover { color: var(--fg); background: var(--panel); }
238
+ nav.tabs button.active { color: var(--fg); background: var(--panel);
239
+ border-color: var(--border); box-shadow: inset 0 -2px 0 var(--accent); }
240
+ main { padding: 28px; max-width: 1500px; margin: 0 auto; }
241
+ .panel { display: none; }
242
+ .panel.active { display: block; animation: fade .25s ease; }
243
+ @keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; } }
244
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
245
+ gap: 12px; margin-bottom: 28px; }
246
+ .card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
247
+ padding: 16px 18px; }
248
+ .card .num { font-size: 28px; font-weight: 600; }
249
+ .card .num.hot { color: var(--hot); }
250
+ .card .num.warm { color: var(--warm); }
251
+ .card .num.cool { color: var(--cool); }
252
+ .card .lbl { color: var(--muted); font-size: 11px; text-transform: uppercase;
253
+ letter-spacing: 0.1em; margin-top: 6px; }
254
+ .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
255
+ .grid3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
256
+ gap: 18px; }
257
+ .section { background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
258
+ padding: 18px 22px; }
259
+ .section h2 { font-size: 13px; color: var(--muted); text-transform: uppercase;
260
+ letter-spacing: 0.1em; margin: 0 0 14px; font-weight: 600; }
261
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
262
+ th, td { text-align: left; padding: 7px 8px; border-bottom: 1px solid var(--border); }
263
+ th { color: var(--muted); font-weight: 500; font-size: 11px; text-transform: uppercase;
264
+ letter-spacing: 0.08em; }
265
+ td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
266
+ .muted { color: var(--muted); }
267
+ code { background: #0b1220; padding: 1px 6px; border-radius: 4px; font-size: 12px;
268
+ border: 1px solid var(--border); }
269
+ .matrix-wrap { overflow: auto; max-height: 80vh; border: 1px solid var(--border);
270
+ border-radius: 8px; background: #0b1220; }
271
+ table.matrix { border-collapse: separate; border-spacing: 0; font-size: 11px; }
272
+ table.matrix th, table.matrix td { border: none; padding: 0; }
273
+ table.matrix .corner { position: sticky; top: 0; left: 0; z-index: 4; background: var(--panel); }
274
+ table.matrix thead th { position: sticky; top: 0; z-index: 3; background: var(--panel);
275
+ padding: 6px 4px; min-width: 22px; text-align: center; transform: rotate(-45deg)
276
+ translateY(8px); transform-origin: bottom left; height: 100px; vertical-align: bottom;
277
+ font-weight: 500; color: var(--muted); white-space: nowrap; }
278
+ table.matrix tbody th { position: sticky; left: 0; z-index: 2; background: var(--panel);
279
+ padding: 4px 10px; text-align: right; color: var(--muted); white-space: nowrap;
280
+ font-weight: 500; max-width: 280px; overflow: hidden; text-overflow: ellipsis; }
281
+ table.matrix td.cell { width: 22px; height: 22px; text-align: center; color: #fff;
282
+ cursor: default; }
283
+ table.matrix td.cell:hover { outline: 2px solid var(--accent); }
284
+ .legend { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--muted);
285
+ margin-top: 12px; }
286
+ .legend .gradient { width: 200px; height: 12px; border-radius: 6px;
287
+ background: linear-gradient(90deg, #1e2a45, #6366f1, #f43f5e); }
288
+ #sankey, #treemap { width: 100%; height: 700px; background: #0b1220; border-radius: 8px;
289
+ border: 1px solid var(--border); }
290
+ .flows-list { display: grid; grid-template-columns: 280px 1fr; gap: 18px; min-height: 600px; }
291
+ .flows-nav { background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
292
+ padding: 12px; overflow-y: auto; max-height: 75vh; }
293
+ .flow-item { padding: 10px 12px; border-radius: 6px; cursor: pointer;
294
+ border: 1px solid transparent; margin-bottom: 4px; }
295
+ .flow-item:hover { background: #1a2540; }
296
+ .flow-item.active { background: #1a2540; border-color: var(--accent); }
297
+ .flow-item .qn { font-size: 13px; font-weight: 500; color: var(--fg);
298
+ word-break: break-all; }
299
+ .flow-item .meta { font-size: 11px; color: var(--muted); margin-top: 3px; }
300
+ .flow-canvas { background: #0b1220; border: 1px solid var(--border); border-radius: 10px;
301
+ padding: 20px; overflow: auto; min-height: 600px; display: flex;
302
+ align-items: center; justify-content: center; }
303
+ .flow-canvas .mermaid { color: var(--fg); }
304
+ .empty { color: var(--muted); font-size: 13px; padding: 60px; text-align: center; }
305
+ input.search { width: 100%; padding: 8px 10px; background: #0b1220; color: var(--fg);
306
+ border: 1px solid var(--border); border-radius: 6px; font-size: 13px;
307
+ margin-bottom: 10px; }
308
+ .tooltip { position: fixed; background: #1e293b; border: 1px solid var(--border);
309
+ padding: 8px 10px; border-radius: 6px; font-size: 12px; pointer-events: none;
310
+ opacity: 0; transition: opacity .12s; z-index: 100; max-width: 320px; }
311
+ .tooltip.show { opacity: 1; }
312
+ .iframe-views { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
313
+ gap: 14px; }
314
+ .iframe-views a { display: block; background: var(--panel); border: 1px solid var(--border);
315
+ border-radius: 10px; padding: 16px 18px; color: var(--fg); text-decoration: none; }
316
+ .iframe-views a:hover { border-color: var(--accent); }
317
+ .iframe-views .t { font-weight: 600; font-size: 14px; }
318
+ .iframe-views .d { color: var(--muted); font-size: 12px; margin-top: 4px; }
319
+ /* ---- HLD ---- */
320
+ .hld-grid { display: grid; grid-template-columns: 1fr 320px; gap: 18px;
321
+ align-items: start; }
322
+ .hld-mini-cards { display: grid; grid-template-columns: repeat(4, 1fr);
323
+ gap: 10px; margin-bottom: 16px; }
324
+ .hld-card { background: var(--panel); border: 1px solid var(--border);
325
+ border-radius: 10px; padding: 14px 16px; }
326
+ .hld-card .num { font-size: 22px; font-weight: 600; }
327
+ .hld-card .lbl { color: var(--muted); font-size: 11px; text-transform: uppercase;
328
+ letter-spacing: 0.08em; margin-top: 4px; }
329
+ .hld-canvas { background: #0f172a; border: 1px solid var(--border);
330
+ border-radius: 12px; padding: 22px; overflow: auto; }
331
+ .hld-canvas h3 { margin: 0 0 14px; font-size: 12px; color: var(--muted);
332
+ text-transform: uppercase; letter-spacing: 0.1em; }
333
+ .hld-canvas + .hld-canvas { margin-top: 18px; }
334
+ .hld-canvas .mermaid { display: flex; justify-content: center; }
335
+ .hld-canvas .mermaid svg { max-width: 100%; height: auto; }
336
+ .hld-side { background: var(--panel); border: 1px solid var(--border);
337
+ border-radius: 10px; padding: 16px; position: sticky; top: 90px;
338
+ max-height: calc(100vh - 110px); overflow-y: auto; }
339
+ .hld-side h3 { margin: 0 0 10px; font-size: 12px; color: var(--muted);
340
+ text-transform: uppercase; letter-spacing: 0.1em; }
341
+ .layer-row { display: flex; align-items: center; gap: 10px; padding: 8px 6px;
342
+ border-radius: 6px; }
343
+ .layer-row:hover { background: #1a2540; }
344
+ .layer-swatch { width: 14px; height: 14px; border-radius: 3px; flex: none; }
345
+ .layer-row .meta { font-size: 11px; color: var(--muted); }
346
+ .legend-edges { font-size: 12px; margin-top: 14px; }
347
+ .legend-edges .row { display: flex; justify-content: space-between;
348
+ padding: 4px 0; border-bottom: 1px solid var(--border); }
349
+ .legend-edges .row:last-child { border-bottom: none; }
350
+ .legend-edges b { font-weight: 500; color: var(--fg); }
351
+ .help-card { background: linear-gradient(135deg,#1a2540,#0f172a);
352
+ border: 1px solid var(--accent); border-radius: 10px; padding: 16px 18px;
353
+ margin-bottom: 18px; font-size: 13px; color: var(--fg); }
354
+ .help-card b { color: var(--accent); }
355
+ @media (max-width: 1100px) {
356
+ .hld-grid { grid-template-columns: 1fr; }
357
+ .hld-side { position: static; max-height: none; }
358
+ }
359
+ </style></head><body>
360
+ <header>
361
+ <h1>codegraph dashboard <small>multi-view code intelligence</small></h1>
362
+ <nav class="tabs" id="tabs"></nav>
363
+ </header>
364
+ <div class="tooltip" id="tt"></div>
365
+ <main>
366
+ <section class="panel active" id="p-overview"></section>
367
+ <section class="panel" id="p-hld"></section>
368
+ <section class="panel" id="p-architecture"></section>
369
+ <section class="panel" id="p-flows"></section>
370
+ <section class="panel" id="p-matrix"></section>
371
+ <section class="panel" id="p-sankey"></section>
372
+ <section class="panel" id="p-treemap"></section>
373
+ <section class="panel" id="p-files"></section>
374
+ </main>
375
+ <script>
376
+ const DATA = __DATA__;
377
+ const TABS = [
378
+ {id: "overview", label: "Overview"},
379
+ {id: "hld", label: "HLD"},
380
+ {id: "architecture", label: "Architecture"},
381
+ {id: "flows", label: "Flows"},
382
+ {id: "matrix", label: "Matrix"},
383
+ {id: "sankey", label: "Sankey"},
384
+ {id: "treemap", label: "Treemap"},
385
+ {id: "files", label: "Files"},
386
+ ];
387
+
388
+ // ---- tabs ----
389
+ const tabsEl = document.getElementById("tabs");
390
+ TABS.forEach(t => {
391
+ const b = document.createElement("button");
392
+ b.textContent = t.label;
393
+ b.dataset.tab = t.id;
394
+ b.onclick = () => activate(t.id);
395
+ tabsEl.appendChild(b);
396
+ });
397
+ function activate(id) {
398
+ document.querySelectorAll("nav.tabs button").forEach(b =>
399
+ b.classList.toggle("active", b.dataset.tab === id));
400
+ document.querySelectorAll(".panel").forEach(p =>
401
+ p.classList.toggle("active", p.id === "p-" + id));
402
+ if (id === "sankey") drawSankey();
403
+ if (id === "treemap") drawTreemap();
404
+ if (id === "flows") ensureFlow();
405
+ if (id === "hld") ensureHld();
406
+ }
407
+ activate("overview");
408
+
409
+ // ---- tooltip ----
410
+ const tt = document.getElementById("tt");
411
+ function showTip(html, x, y) { tt.innerHTML = html; tt.style.left = (x+12)+"px";
412
+ tt.style.top = (y+12)+"px"; tt.classList.add("show"); }
413
+ function hideTip() { tt.classList.remove("show"); }
414
+
415
+ // ---- overview ----
416
+ function renderOverview() {
417
+ const m = DATA.metrics, iss = DATA.issues;
418
+ const card = (n, l, cls) => `<div class="card"><div class="num ${cls||""}">${n}</div>`
419
+ + `<div class="lbl">${l}</div></div>`;
420
+ const rows = (obj) => Object.entries(obj).sort().map(([k, v]) =>
421
+ `<tr><td>${k}</td><td class="num">${v}</td></tr>`).join("");
422
+ const hotspots = DATA.hotspots.map(h =>
423
+ `<tr><td><code>${h.qualname}</code></td>`
424
+ + `<td class="muted">${h.file}</td>`
425
+ + `<td class="num">${h.fan_in}</td><td class="num">${h.fan_out}</td>`
426
+ + `<td class="num">${h.loc}</td><td class="num">${h.score}</td></tr>`).join("");
427
+ document.getElementById("p-overview").innerHTML = `
428
+ <div class="help-card">
429
+ <b>Where to start?</b> Open the <b>HLD</b> tab for a clean layered
430
+ architecture diagram. Use <b>Flows</b> to follow specific call chains.
431
+ The <b>Matrix</b> shows who calls whom; the <b>Sankey</b> shows the
432
+ heaviest flows. Cards below summarise the whole repo.
433
+ </div>
434
+ <div class="cards">
435
+ ${card(m.nodes, "Nodes")}
436
+ ${card(m.edges, "Edges")}
437
+ ${card(m.unresolved, "Unresolved", m.unresolved ? "warm" : "")}
438
+ ${card(iss.cycles, "Cycles", iss.cycles ? "hot" : "")}
439
+ ${card(iss.dead, "Dead-code candidates", iss.dead ? "warm" : "")}
440
+ ${card(iss.untested, "Untested fns", iss.untested ? "warm" : "")}
441
+ </div>
442
+ <div class="grid3">
443
+ <div class="section"><h2>Nodes by kind</h2><table>${rows(m.by_kind)}</table></div>
444
+ <div class="section"><h2>Edges by kind</h2><table>${rows(m.by_edge)}</table></div>
445
+ <div class="section"><h2>Languages</h2><table>${rows(m.languages)}</table></div>
446
+ </div>
447
+ <div class="section" style="margin-top:18px"><h2>Top hotspots</h2>
448
+ <table><tr><th>Symbol</th><th>File</th><th class="num">Fan-in</th>
449
+ <th class="num">Fan-out</th><th class="num">LOC</th><th class="num">Score</th></tr>
450
+ ${hotspots}</table></div>`;
451
+ }
452
+ renderOverview();
453
+
454
+ // ---- HLD (hand-rolled, lazy-rendered) ----
455
+ let hldBuilt = false;
456
+ function ensureHld() {
457
+ if (hldBuilt) return;
458
+ hldBuilt = true;
459
+ const hld = DATA.hld;
460
+ if (!hld) {
461
+ document.getElementById("p-hld").innerHTML =
462
+ '<div class="empty">No HLD payload — rebuild the dashboard.</div>';
463
+ return;
464
+ }
465
+ const m = hld.metrics;
466
+ const card = (n, l) => `<div class="hld-card"><div class="num">${n}</div>`
467
+ + `<div class="lbl">${l}</div></div>`;
468
+
469
+ const layerSide = hld.layers.filter(L => (hld.components[L.id] || []).length)
470
+ .map(L => {
471
+ const comps = hld.components[L.id] || [];
472
+ return `<div class="layer-row">
473
+ <div class="layer-swatch" style="background:${L.color}"></div>
474
+ <div><div><b>${L.title}</b></div>
475
+ <div class="meta">${comps.length} module${comps.length===1?"":"s"} - ${escapeHtml(L.subtitle)}</div></div>
476
+ </div>`;
477
+ }).join("");
478
+
479
+ const edgeRows = hld.edges.slice(0, 20).map(e => {
480
+ const sl = hld.layers.find(L => L.id === e.source) || {title: e.source};
481
+ const tl = hld.layers.find(L => L.id === e.target) || {title: e.target};
482
+ return `<div class="row"><span>${sl.title} <span class="muted">--></span> ${tl.title} `
483
+ + `<span class="muted">(${e.kind.toLowerCase()})</span></span><b>${e.weight}</b></div>`;
484
+ }).join("");
485
+
486
+ document.getElementById("p-hld").innerHTML = `
487
+ <div class="help-card">
488
+ <b>How to read this page.</b> Top diagram = system context (who uses what).
489
+ Below = layered architecture: each colored band is a layer, each box is a
490
+ Python module, arrow labels show how many calls/imports cross that
491
+ boundary. Thicker arrows = heavier traffic. Use Cmd/Ctrl + scroll to zoom.
492
+ </div>
493
+ <div class="hld-mini-cards">
494
+ ${card(m.layers, "Layers")}
495
+ ${card(m.components, "Modules")}
496
+ ${card(m.cross_layer_edges, "Cross-layer edges")}
497
+ ${card(m.total_cross_layer_calls, "Cross-layer calls")}
498
+ </div>
499
+ <div class="hld-grid">
500
+ <div>
501
+ <div class="hld-canvas">
502
+ <h3>System context</h3>
503
+ <pre class="mermaid" id="hld-context">${escapeHtml(hld.mermaid_context)}</pre>
504
+ </div>
505
+ <div class="hld-canvas">
506
+ <h3>Layered architecture (live data)</h3>
507
+ <pre class="mermaid" id="hld-layered">${escapeHtml(hld.mermaid_layered)}</pre>
508
+ </div>
509
+ </div>
510
+ <aside class="hld-side">
511
+ <h3>Layers</h3>
512
+ ${layerSide}
513
+ <h3 style="margin-top:18px">Top cross-layer flows</h3>
514
+ <div class="legend-edges">${edgeRows || '<div class="muted">none</div>'}</div>
515
+ </aside>
516
+ </div>`;
517
+ mermaid.run({nodes: document.querySelectorAll("#p-hld .mermaid")});
518
+ }
519
+
520
+
521
+ // ---- architecture (links to pyvis pages) ----
522
+ function renderArchitecture() {
523
+ document.getElementById("p-architecture").innerHTML = `
524
+ <div class="section">
525
+ <h2>Interactive node-link explorers</h2>
526
+ <p class="muted" style="margin:0 0 14px;font-size:13px">
527
+ Force-directed views with in-page search and filtering.</p>
528
+ <div class="iframe-views">
529
+ <a href="architecture.html"><div class="t">Architecture (modules)</div>
530
+ <div class="d">One node per file, edges aggregated by kind.</div></a>
531
+ <a href="callgraph.html"><div class="t">Call graph</div>
532
+ <div class="d">Functions and methods only, sized by fan-in.</div></a>
533
+ <a href="inheritance.html"><div class="t">Inheritance</div>
534
+ <div class="d">Classes with INHERITS / IMPLEMENTS edges.</div></a>
535
+ </div>
536
+ </div>`;
537
+ }
538
+ renderArchitecture();
539
+
540
+ // ---- matrix ----
541
+ function renderMatrix() {
542
+ const m = DATA.matrix, max = m.max || 1;
543
+ const colour = v => {
544
+ if (!v) return "transparent";
545
+ const t = v / max;
546
+ const r = Math.round(30 + t * (244-30));
547
+ const g = Math.round(42 + t * (63-42));
548
+ const b = Math.round(69 + t * (94-69));
549
+ return `rgb(${r},${g},${b})`;
550
+ };
551
+ let html = '<div class="section"><h2>Module-to-module call matrix '
552
+ + '(rows = caller, cols = callee)</h2>';
553
+ if (!m.modules.length) {
554
+ html += '<div class="empty">No cross-module CALLS recorded.</div></div>';
555
+ document.getElementById("p-matrix").innerHTML = html;
556
+ return;
557
+ }
558
+ html += '<div class="matrix-wrap"><table class="matrix"><thead><tr>'
559
+ + '<th class="corner"></th>';
560
+ m.modules.forEach(mod => {
561
+ html += `<th title="${mod.qualname}">${mod.name}</th>`;
562
+ });
563
+ html += "</tr></thead><tbody>";
564
+ m.modules.forEach((row, i) => {
565
+ html += `<tr><th title="${row.qualname}">${row.qualname}</th>`;
566
+ m.counts[i].forEach((v, j) => {
567
+ const tip = v ? `${row.name} -> ${m.modules[j].name}: ${v} call(s)` : "";
568
+ html += `<td class="cell" data-tip="${tip}" style="background:${colour(v)}">`
569
+ + `${v || ""}</td>`;
570
+ });
571
+ html += "</tr>";
572
+ });
573
+ html += "</tbody></table></div>";
574
+ html += '<div class="legend"><span>0</span><div class="gradient"></div>'
575
+ + `<span>${max}</span></div></div>`;
576
+ const el = document.getElementById("p-matrix");
577
+ el.innerHTML = html;
578
+ el.querySelectorAll("td.cell").forEach(cell => {
579
+ cell.addEventListener("mousemove", e => {
580
+ const t = e.target.dataset.tip; if (t) showTip(t, e.clientX, e.clientY);
581
+ });
582
+ cell.addEventListener("mouseleave", hideTip);
583
+ });
584
+ }
585
+ renderMatrix();
586
+
587
+ // ---- sankey ----
588
+ let sankeyDrawn = false;
589
+ function drawSankey() {
590
+ if (sankeyDrawn) return;
591
+ sankeyDrawn = true;
592
+ const data = DATA.sankey;
593
+ const host = document.getElementById("p-sankey");
594
+ host.innerHTML = '<div class="section"><h2>Top inter-module call flows '
595
+ + '(width = number of calls)</h2>'
596
+ + (data.links.length
597
+ ? '<svg id="sankey"></svg>'
598
+ : '<div class="empty">No cross-module call flows yet.</div>')
599
+ + '</div>';
600
+ if (!data.links.length) return;
601
+ const svg = d3.select("#sankey");
602
+ const {width, height} = svg.node().getBoundingClientRect();
603
+ const sankey = d3.sankey().nodeWidth(14).nodePadding(8)
604
+ .extent([[1, 1], [width - 1, height - 5]]);
605
+ const graph = sankey({
606
+ nodes: data.nodes.map(d => Object.assign({}, d)),
607
+ links: data.links.map(d => Object.assign({}, d)),
608
+ });
609
+ const colour = d3.scaleOrdinal(d3.schemeTableau10);
610
+ svg.append("g").selectAll("rect").data(graph.nodes).join("rect")
611
+ .attr("x", d => d.x0).attr("y", d => d.y0)
612
+ .attr("height", d => d.y1 - d.y0).attr("width", d => d.x1 - d.x0)
613
+ .attr("fill", d => colour(d.package || d.name))
614
+ .on("mousemove", (e, d) => showTip(`<b>${d.qualname}</b><br>`
615
+ + `value: ${Math.round(d.value)}`, e.clientX, e.clientY))
616
+ .on("mouseleave", hideTip);
617
+ svg.append("g").attr("fill", "none").selectAll("path").data(graph.links).join("path")
618
+ .attr("d", d3.sankeyLinkHorizontal())
619
+ .attr("stroke", d => colour(d.source.package || d.source.name))
620
+ .attr("stroke-width", d => Math.max(1, d.width)).attr("stroke-opacity", 0.45)
621
+ .on("mousemove", (e, d) => showTip(
622
+ `${d.source.qualname} -> ${d.target.qualname}<br>${d.value} call(s)`,
623
+ e.clientX, e.clientY))
624
+ .on("mouseleave", hideTip);
625
+ svg.append("g").style("font-size", "11px").style("fill", "#cbd5e1")
626
+ .selectAll("text").data(graph.nodes).join("text")
627
+ .attr("x", d => d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)
628
+ .attr("y", d => (d.y1 + d.y0) / 2).attr("dy", "0.35em")
629
+ .attr("text-anchor", d => d.x0 < width / 2 ? "start" : "end")
630
+ .text(d => d.name);
631
+ }
632
+
633
+ // ---- treemap ----
634
+ let treemapDrawn = false;
635
+ function drawTreemap() {
636
+ if (treemapDrawn) return;
637
+ treemapDrawn = true;
638
+ const host = document.getElementById("p-treemap");
639
+ host.innerHTML = '<div class="section"><h2>Codebase footprint '
640
+ + '(area = LOC, color = hotspot score)</h2><svg id="treemap"></svg></div>';
641
+ const root = d3.hierarchy(DATA.treemap)
642
+ .sum(d => d.value || 0)
643
+ .sort((a, b) => b.value - a.value);
644
+ const svg = d3.select("#treemap");
645
+ const {width, height} = svg.node().getBoundingClientRect();
646
+ d3.treemap().size([width, height]).paddingInner(2).paddingTop(18).round(true)(root);
647
+ const maxScore = d3.max(root.leaves(), d => d.data.score) || 1;
648
+ const colour = d3.scaleSequential([0, maxScore], d3.interpolateInferno);
649
+
650
+ const pkg = svg.append("g").selectAll("g").data(root.descendants().filter(d => d.depth === 1))
651
+ .join("g").attr("transform", d => `translate(${d.x0},${d.y0})`);
652
+ pkg.append("rect").attr("width", d => d.x1 - d.x0).attr("height", d => d.y1 - d.y0)
653
+ .attr("fill", "#1e293b").attr("stroke", "#334155");
654
+ pkg.append("text").attr("x", 6).attr("y", 12).attr("fill", "#cbd5e1")
655
+ .style("font-size", "11px").style("font-weight", "600").text(d => d.data.name);
656
+
657
+ const leaf = svg.append("g").selectAll("g").data(root.leaves())
658
+ .join("g").attr("transform", d => `translate(${d.x0},${d.y0})`);
659
+ leaf.append("rect").attr("width", d => Math.max(0, d.x1 - d.x0))
660
+ .attr("height", d => Math.max(0, d.y1 - d.y0))
661
+ .attr("fill", d => d.data.score ? colour(d.data.score) : "#334155")
662
+ .attr("stroke", "#0b1220").attr("stroke-width", 0.5)
663
+ .on("mousemove", (e, d) => showTip(
664
+ `<b>${d.data.name}</b><br>${d.data.file}<br>LOC: ${d.data.value}`
665
+ + `<br>symbols: ${d.data.symbols}<br>hotspot score: ${d.data.score}`,
666
+ e.clientX, e.clientY))
667
+ .on("mouseleave", hideTip);
668
+ leaf.append("text").attr("x", 4).attr("y", 12).attr("fill", "#fff")
669
+ .style("font-size", "10px").style("pointer-events", "none")
670
+ .text(d => (d.x1 - d.x0 > 60 && d.y1 - d.y0 > 18) ? d.data.name.split(".").pop() : "");
671
+ }
672
+
673
+ // ---- flows ----
674
+ mermaid.initialize({startOnLoad: false, theme: "dark",
675
+ themeVariables: {fontSize: "13px", primaryColor: "#1e293b",
676
+ primaryTextColor: "#e2e8f0", lineColor: "#475569"}});
677
+ let activeFlow = -1;
678
+ function ensureFlow() {
679
+ if (DATA.flows.length === 0 && document.getElementById("p-flows").innerHTML) return;
680
+ const host = document.getElementById("p-flows");
681
+ if (host.dataset.built) return;
682
+ host.dataset.built = "1";
683
+ if (!DATA.flows.length) {
684
+ host.innerHTML = '<div class="empty">No call chains found yet. '
685
+ + 'Run <code>codegraph build</code> on a real codebase.</div>';
686
+ return;
687
+ }
688
+ let html = '<div class="flows-list"><div class="flows-nav">'
689
+ + '<input class="search" id="flow-search" placeholder="Filter entry points...">';
690
+ DATA.flows.forEach((f, i) => {
691
+ html += `<div class="flow-item" data-i="${i}">`
692
+ + `<div class="qn">${escapeHtml(f.qualname)}</div>`
693
+ + `<div class="meta">${escapeHtml(f.reason)} - ${escapeHtml(f.file)}</div></div>`;
694
+ });
695
+ html += '</div><div class="flow-canvas" id="flow-canvas">'
696
+ + '<div class="muted">Pick an entry point on the left.</div></div></div>';
697
+ host.innerHTML = html;
698
+ host.querySelectorAll(".flow-item").forEach(el => {
699
+ el.onclick = () => selectFlow(parseInt(el.dataset.i, 10));
700
+ });
701
+ document.getElementById("flow-search").addEventListener("input", e => {
702
+ const q = e.target.value.toLowerCase();
703
+ host.querySelectorAll(".flow-item").forEach(el => {
704
+ el.style.display = el.textContent.toLowerCase().includes(q) ? "" : "none";
705
+ });
706
+ });
707
+ selectFlow(0);
708
+ }
709
+ function selectFlow(i) {
710
+ if (i === activeFlow) return;
711
+ activeFlow = i;
712
+ document.querySelectorAll("#p-flows .flow-item").forEach(el =>
713
+ el.classList.toggle("active", parseInt(el.dataset.i, 10) === i));
714
+ const flow = DATA.flows[i];
715
+ const canvas = document.getElementById("flow-canvas");
716
+ canvas.innerHTML = `<pre class="mermaid">${escapeHtml(flow.mermaid)}</pre>`;
717
+ mermaid.run({nodes: canvas.querySelectorAll(".mermaid")});
718
+ }
719
+ function escapeHtml(s) { return String(s).replace(/[&<>"]/g, c =>
720
+ ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c])); }
721
+
722
+ // ---- files ----
723
+ function renderFiles() {
724
+ const rows = DATA.files.map(f => {
725
+ const slug = f.file.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_|_$/g, "") || "file";
726
+ return `<tr><td><a href="files/${slug}.html" style="color:var(--accent)">`
727
+ + `<code>${escapeHtml(f.file)}</code></a></td>`
728
+ + `<td class="muted">${f.language}</td>`
729
+ + `<td class="num">${f.symbols}</td></tr>`;
730
+ }).join("");
731
+ document.getElementById("p-files").innerHTML = `
732
+ <div class="section"><h2>Files</h2><table>
733
+ <tr><th>Path</th><th>Language</th><th class="num">Symbols</th></tr>
734
+ ${rows}</table></div>`;
735
+ }
736
+ renderFiles();
737
+ </script></body></html>"""
738
+
739
+
740
+ __all__ = ["build_dashboard_payload", "render_dashboard"]