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.
- codegraph/__init__.py +10 -0
- codegraph/analysis/__init__.py +30 -0
- codegraph/analysis/_common.py +125 -0
- codegraph/analysis/blast_radius.py +63 -0
- codegraph/analysis/cycles.py +79 -0
- codegraph/analysis/dataflow.py +861 -0
- codegraph/analysis/dead_code.py +165 -0
- codegraph/analysis/hotspots.py +68 -0
- codegraph/analysis/infrastructure.py +439 -0
- codegraph/analysis/metrics.py +52 -0
- codegraph/analysis/report.py +222 -0
- codegraph/analysis/roles.py +323 -0
- codegraph/analysis/untested.py +79 -0
- codegraph/cli.py +1506 -0
- codegraph/config.py +64 -0
- codegraph/embed/__init__.py +35 -0
- codegraph/embed/chunker.py +120 -0
- codegraph/embed/embedder.py +113 -0
- codegraph/embed/query.py +181 -0
- codegraph/embed/store.py +360 -0
- codegraph/graph/__init__.py +0 -0
- codegraph/graph/builder.py +212 -0
- codegraph/graph/schema.py +69 -0
- codegraph/graph/store_networkx.py +55 -0
- codegraph/graph/store_sqlite.py +249 -0
- codegraph/mcp_server/__init__.py +6 -0
- codegraph/mcp_server/server.py +933 -0
- codegraph/parsers/__init__.py +0 -0
- codegraph/parsers/base.py +70 -0
- codegraph/parsers/go.py +570 -0
- codegraph/parsers/python.py +1707 -0
- codegraph/parsers/typescript.py +1397 -0
- codegraph/py.typed +0 -0
- codegraph/resolve/__init__.py +4 -0
- codegraph/resolve/calls.py +480 -0
- codegraph/review/__init__.py +31 -0
- codegraph/review/baseline.py +32 -0
- codegraph/review/differ.py +211 -0
- codegraph/review/hook.py +70 -0
- codegraph/review/risk.py +219 -0
- codegraph/review/rules.py +342 -0
- codegraph/viz/__init__.py +17 -0
- codegraph/viz/_style.py +45 -0
- codegraph/viz/dashboard.py +740 -0
- codegraph/viz/diagrams.py +370 -0
- codegraph/viz/explore.py +453 -0
- codegraph/viz/hld.py +683 -0
- codegraph/viz/html.py +115 -0
- codegraph/viz/mermaid.py +111 -0
- codegraph/viz/svg.py +77 -0
- codegraph/web/__init__.py +4 -0
- codegraph/web/server.py +165 -0
- codegraph/web/static/app.css +664 -0
- codegraph/web/static/app.js +919 -0
- codegraph/web/static/index.html +112 -0
- codegraph/web/static/views/architecture.js +1671 -0
- codegraph/web/static/views/graph3d.css +564 -0
- codegraph/web/static/views/graph3d.js +999 -0
- codegraph/web/static/views/graph3d_transform.js +984 -0
- codegraph/workspace/__init__.py +34 -0
- codegraph/workspace/config.py +110 -0
- codegraph/workspace/operations.py +294 -0
- polycodegraph-0.1.0.dist-info/METADATA +687 -0
- polycodegraph-0.1.0.dist-info/RECORD +67 -0
- polycodegraph-0.1.0.dist-info/WHEEL +4 -0
- polycodegraph-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
({"&":"&","<":"<",">":">",'"':"""}[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"]
|