@ghostlygawd/codeweb 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +30 -0
- package/CHANGELOG.md +480 -0
- package/LICENSE +21 -0
- package/README.md +573 -0
- package/agents/codeweb-dissector.md +56 -0
- package/agents/codeweb-domain-mapper.md +63 -0
- package/commands/codeweb.md +57 -0
- package/hooks/hooks.json +47 -0
- package/hooks/post-edit-diff.mjs +83 -0
- package/hooks/pre-edit-impact.mjs +94 -0
- package/hooks/session-brief.mjs +53 -0
- package/package.json +64 -0
- package/scripts/annotate.mjs +46 -0
- package/scripts/bench-ts-engine.mjs +59 -0
- package/scripts/bench.mjs +133 -0
- package/scripts/break-cycles.mjs +0 -0
- package/scripts/brief.mjs +28 -0
- package/scripts/build-report.mjs +182 -0
- package/scripts/campaign.mjs +61 -0
- package/scripts/check-consistency.mjs +45 -0
- package/scripts/ci-gate.mjs +86 -0
- package/scripts/cluster3.mjs +112 -0
- package/scripts/codemod.mjs +130 -0
- package/scripts/context-pack.mjs +118 -0
- package/scripts/deadcode.mjs +96 -0
- package/scripts/diff.mjs +0 -0
- package/scripts/explain.mjs +87 -0
- package/scripts/extract-symbols.mjs +1307 -0
- package/scripts/find-similar.mjs +86 -0
- package/scripts/find.mjs +50 -0
- package/scripts/fitness.mjs +90 -0
- package/scripts/grammars/PROVENANCE.md +36 -0
- package/scripts/grammars/tree-sitter-c-sharp.wasm +0 -0
- package/scripts/grammars/tree-sitter-go.wasm +0 -0
- package/scripts/grammars/tree-sitter-java.wasm +0 -0
- package/scripts/grammars/tree-sitter-python.wasm +0 -0
- package/scripts/grammars/tree-sitter-rust.wasm +0 -0
- package/scripts/grammars/tree-sitter-typescript.wasm +0 -0
- package/scripts/hotspots.mjs +53 -0
- package/scripts/lib/annotations.mjs +51 -0
- package/scripts/lib/bench-core.mjs +178 -0
- package/scripts/lib/brief-core.mjs +92 -0
- package/scripts/lib/campaign.mjs +73 -0
- package/scripts/lib/claims-check.mjs +44 -0
- package/scripts/lib/cli.mjs +140 -0
- package/scripts/lib/complexity.mjs +68 -0
- package/scripts/lib/dup-check.mjs +51 -0
- package/scripts/lib/find-core.mjs +85 -0
- package/scripts/lib/gate-md.mjs +50 -0
- package/scripts/lib/graph-ops.mjs +319 -0
- package/scripts/lib/hotspots.mjs +34 -0
- package/scripts/lib/query-core.mjs +85 -0
- package/scripts/lib/reading-order.mjs +53 -0
- package/scripts/lib/reliance.mjs +143 -0
- package/scripts/lib/risk.mjs +18 -0
- package/scripts/lib/shingles.mjs +39 -0
- package/scripts/lib/skeleton.mjs +41 -0
- package/scripts/lib/stats.mjs +92 -0
- package/scripts/lib/ts-engine.mjs +605 -0
- package/scripts/mcp-server.mjs +370 -0
- package/scripts/optimize.mjs +152 -0
- package/scripts/overlap.mjs +380 -0
- package/scripts/placement.mjs +93 -0
- package/scripts/query.mjs +94 -0
- package/scripts/reading-order.mjs +36 -0
- package/scripts/refresh.mjs +73 -0
- package/scripts/release-utils.mjs +158 -0
- package/scripts/release.mjs +62 -0
- package/scripts/report-template.html +730 -0
- package/scripts/review.mjs +112 -0
- package/scripts/risk.mjs +77 -0
- package/scripts/run.mjs +96 -0
- package/scripts/screenshot.mjs +104 -0
- package/scripts/simulate-edit.mjs +70 -0
- package/scripts/stats.mjs +31 -0
- package/scripts/trend.mjs +147 -0
- package/scripts/version-sync.mjs +19 -0
- package/skills/codebase-anatomy/SKILL.md +174 -0
- package/skills/codebase-anatomy/references/engine-detection.md +49 -0
- package/skills/codebase-anatomy/references/graph-schema.md +120 -0
- package/skills/codebase-anatomy/references/overlap-heuristics.md +52 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>codeweb — system map</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{
|
|
9
|
+
/* Crafted Dark — warm ink + electric chartreuse; mirrors the site tokens + brand.json.
|
|
10
|
+
The ACCENT is brand/UI only (selection, focus, links) — never a data series.
|
|
11
|
+
Data colors are the validated 8-slot categorical set (fixed order, CVD-checked against
|
|
12
|
+
this surface: worst adjacent ΔE 8.4 protan / 19.3 normal, all ≥3:1) + reserved status
|
|
13
|
+
colors for severity. No web fonts; system stacks only. */
|
|
14
|
+
--bg:#100E14; --panel:#1A1820; --panel2:#232029; --line:#322E3A;
|
|
15
|
+
--fg:#ECECEE; --muted:#9C99A6; --accent:#C6F24E; --accentFg:#0A090D;
|
|
16
|
+
--ring:rgba(255,255,255,.10);
|
|
17
|
+
/* categorical slots (fixed order — assigned to areas by size rank, 9+ fold to neutral) */
|
|
18
|
+
--s1:#3987e5; --s2:#008300; --s3:#d55181; --s4:#c98500;
|
|
19
|
+
--s5:#199e70; --s6:#d95926; --s7:#9085e9; --s8:#e66767; --sX:#7d7a87;
|
|
20
|
+
/* status (reserved for severity — never a series) */
|
|
21
|
+
--stWarn:#fab219; --stSerious:#ec835a; --stCritical:#d03b3b; --stGood:#0ca30c;
|
|
22
|
+
}
|
|
23
|
+
*{box-sizing:border-box}
|
|
24
|
+
html,body{margin:0;height:100%;background:var(--bg);color:var(--fg);
|
|
25
|
+
font:13px/1.5 ui-sans-serif,system-ui,"Segoe UI",Roboto,Arial,sans-serif}
|
|
26
|
+
#top{height:48px;display:flex;align-items:center;gap:6px;padding:0 14px;
|
|
27
|
+
border-bottom:1px solid var(--line);background:var(--panel)}
|
|
28
|
+
#top b{font-size:15px;letter-spacing:.5px;margin-right:8px}
|
|
29
|
+
#top .meta{color:var(--muted);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:46vw}
|
|
30
|
+
#top .spacer{flex:1}
|
|
31
|
+
.tab{background:transparent;color:var(--muted);border:1px solid transparent;border-radius:7px;
|
|
32
|
+
padding:6px 12px;cursor:pointer;font-size:13px}
|
|
33
|
+
.tab:hover{color:var(--fg)}
|
|
34
|
+
.tab.on{color:var(--fg);background:var(--panel2);border-color:var(--line)}
|
|
35
|
+
#wrap{display:flex;height:calc(100% - 48px)}
|
|
36
|
+
#main{flex:1;position:relative;min-width:0;overflow:hidden}
|
|
37
|
+
.view{position:absolute;inset:0;overflow:auto;padding:16px 18px;display:none}
|
|
38
|
+
.view.on{display:block}
|
|
39
|
+
#view-graph.on,#view-treemap.on{padding:0}
|
|
40
|
+
#detail{width:340px;border-left:1px solid var(--line);background:var(--panel);overflow:auto;padding:14px}
|
|
41
|
+
#detail h3{margin:0 0 4px;font-size:14px;word-break:break-all}
|
|
42
|
+
#detail .kv{color:var(--muted);font-size:12px;margin:3px 0}
|
|
43
|
+
#detail .sec{margin-top:14px;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted)}
|
|
44
|
+
#detail .lst div{padding:3px 0;border-bottom:1px solid var(--line);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
45
|
+
#detail .lst div:hover{color:var(--accent)}
|
|
46
|
+
.empty{color:var(--muted);padding:24px;text-align:center}
|
|
47
|
+
code{background:var(--panel2);padding:1px 5px;border-radius:4px;font-size:11px}
|
|
48
|
+
/* findings */
|
|
49
|
+
.fsec{margin-bottom:26px}
|
|
50
|
+
.fhead{display:flex;align-items:baseline;gap:10px;margin-bottom:8px}
|
|
51
|
+
.fhead h2{margin:0;font-size:15px}
|
|
52
|
+
.fhead .sub{color:var(--muted);font-size:12px}
|
|
53
|
+
table.f{width:100%;max-width:980px;border-collapse:collapse;font-size:13px}
|
|
54
|
+
table.f td{padding:6px 8px;border-bottom:1px solid var(--line);cursor:pointer}
|
|
55
|
+
table.f td:first-child{width:44px}
|
|
56
|
+
table.f tr:hover td{background:var(--panel2)}
|
|
57
|
+
.badge{display:inline-block;min-width:22px;text-align:center;padding:1px 7px;border-radius:11px;font-size:11px;font-weight:700;color:var(--accentFg);font-variant-numeric:tabular-nums}
|
|
58
|
+
.b-hi{background:var(--stCritical);color:#fff} .b-med{background:var(--stSerious)} .b-low{background:var(--stWarn)}
|
|
59
|
+
.b-acc{background:var(--panel2);color:var(--fg);box-shadow:inset 0 0 0 1px var(--ring)}
|
|
60
|
+
.mono{font-family:ui-monospace,Consolas,monospace;font-size:12px}
|
|
61
|
+
.dim{color:var(--muted)}
|
|
62
|
+
.pill{font-size:10px;color:var(--muted);border:1px solid var(--line);border-radius:10px;padding:0 6px;margin-left:4px}
|
|
63
|
+
/* matrix */
|
|
64
|
+
table.m{border-collapse:separate;border-spacing:2px;font-size:11px}
|
|
65
|
+
table.m th{padding:4px 8px;color:var(--muted);font-weight:600;text-align:right;white-space:nowrap}
|
|
66
|
+
table.m th.rot{height:104px;vertical-align:bottom;text-align:center}
|
|
67
|
+
table.m th.rot span{writing-mode:vertical-rl;transform:rotate(180deg);white-space:nowrap}
|
|
68
|
+
table.m td{width:36px;height:36px;text-align:center;border-radius:4px;cursor:pointer;color:var(--fg);font-weight:600;font-variant-numeric:tabular-nums;background:var(--panel)}
|
|
69
|
+
table.m td:hover{box-shadow:inset 0 0 0 2px var(--accent)}
|
|
70
|
+
table.m td.diag{box-shadow:inset 0 0 0 1px var(--ring)}
|
|
71
|
+
table.m td.zero{color:var(--line);background:var(--panel)!important;cursor:default;box-shadow:none}
|
|
72
|
+
.legend{color:var(--muted);font-size:12px;margin:12px 0;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
73
|
+
.chip{display:inline-block;width:11px;height:11px;border-radius:3px;vertical-align:-1px;box-shadow:inset 0 0 0 1px var(--ring)}
|
|
74
|
+
/* treemap */
|
|
75
|
+
#tm{position:absolute;inset:0;padding:6px}
|
|
76
|
+
.tmbox{position:absolute;overflow:hidden;border:2px solid var(--bg);border-radius:4px;cursor:pointer;font-size:10px}
|
|
77
|
+
.tmbox:hover{outline:2px solid var(--accent);z-index:5}
|
|
78
|
+
.tmdom{border:1px solid var(--line);border-radius:6px;background:var(--panel);cursor:default}
|
|
79
|
+
.tmlabel{position:absolute;top:1px;left:4px;right:3px;color:#ECECEE;font-size:10px;text-shadow:0 1px 2px rgba(0,0,0,.8);pointer-events:none;white-space:nowrap;overflow:hidden}
|
|
80
|
+
.tmdomlabel{position:absolute;font-size:11px;font-weight:600;color:var(--fg);pointer-events:none;white-space:nowrap;overflow:hidden;display:flex;align-items:center;gap:5px}
|
|
81
|
+
#tmLegend{position:absolute;right:14px;bottom:12px;background:var(--panel);border:1px solid var(--line);border-radius:7px;padding:6px 10px;font-size:11px;color:var(--muted);display:flex;align-items:center;gap:7px;z-index:6}
|
|
82
|
+
#tmLegend .bar{width:90px;height:8px;border-radius:4px;background:linear-gradient(90deg,#292331,#e66767)}
|
|
83
|
+
/* graph */
|
|
84
|
+
#cv{display:block;background:radial-gradient(circle at 50% 38%,#16131d,#100E14);touch-action:none}
|
|
85
|
+
#gsearch{position:absolute;top:10px;left:10px;background:var(--panel);border:1px solid var(--line);color:var(--fg);border-radius:6px;padding:6px 9px;width:220px}
|
|
86
|
+
#gsearch:focus{outline:none;border-color:var(--accent)}
|
|
87
|
+
#gctrl{position:absolute;top:10px;right:10px;display:flex;gap:6px}
|
|
88
|
+
#gctrl button{background:var(--panel);color:var(--fg);border:1px solid var(--line);border-radius:6px;padding:5px 9px;cursor:pointer;font-size:12px}
|
|
89
|
+
#gctrl button:hover{border-color:var(--accent)}
|
|
90
|
+
#gLegend{position:absolute;left:12px;bottom:12px;background:var(--panel);border:1px solid var(--line);border-radius:7px;padding:6px 10px;font-size:11px;color:var(--muted);z-index:6}
|
|
91
|
+
/* header brand */
|
|
92
|
+
#logo{width:20px;height:20px;flex:none;margin-right:2px}
|
|
93
|
+
/* mobile: the desktop app layout (fixed 340px sidebar, viewport-locked) becomes a vertical stack */
|
|
94
|
+
@media (max-width: 760px){
|
|
95
|
+
html,body{height:auto;min-height:100%}
|
|
96
|
+
body{overflow-x:hidden}
|
|
97
|
+
#top{height:auto;min-height:48px;flex-wrap:wrap;padding:8px 12px;gap:6px;position:sticky;top:0;z-index:20}
|
|
98
|
+
#top b{margin-right:4px}
|
|
99
|
+
#top .meta{display:none}
|
|
100
|
+
#top .spacer{display:none}
|
|
101
|
+
.tab{padding:6px 10px;font-size:12px}
|
|
102
|
+
#wrap{flex-direction:column;height:auto}
|
|
103
|
+
#main{flex:none;height:64vh;min-height:340px}
|
|
104
|
+
#detail{width:auto;border-left:0;border-top:1px solid var(--line);max-height:none}
|
|
105
|
+
.view{padding:12px 14px}
|
|
106
|
+
#view-graph.on,#view-treemap.on,#view-matrix.on{padding:0}
|
|
107
|
+
#view-matrix.on{padding:12px;overflow:auto}
|
|
108
|
+
table.m{font-size:10px}
|
|
109
|
+
table.m td{width:24px;height:24px}
|
|
110
|
+
#gsearch{width:150px;font-size:12px}
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
113
|
+
</head>
|
|
114
|
+
<body>
|
|
115
|
+
<div id="top">
|
|
116
|
+
<svg id="logo" viewBox="0 0 24 24" aria-hidden="true"><g fill="none" stroke="#C6F24E" stroke-width="1.6"><path d="M6 8 L18 6 M6 8 L12 18 M18 6 L12 18" opacity=".55"/></g><circle cx="6" cy="8" r="3" fill="#C6F24E"/><circle cx="18" cy="6" r="2.4" fill="#9085e9"/><circle cx="12" cy="18" r="2.4" fill="#199e70"/></svg>
|
|
117
|
+
<b>codeweb</b>
|
|
118
|
+
<span class="meta" id="metaTarget"></span>
|
|
119
|
+
<span class="meta" id="metaStats"></span>
|
|
120
|
+
<span class="spacer"></span>
|
|
121
|
+
<button id="roleFilter" title="Hide tests, fixtures, examples and generated code from every view" aria-pressed="true" hidden></button>
|
|
122
|
+
<div role="tablist" aria-label="Report views" style="display:contents">
|
|
123
|
+
<button class="tab on" role="tab" aria-selected="true" data-view="findings">Findings</button>
|
|
124
|
+
<button class="tab" role="tab" aria-selected="false" data-view="matrix">Matrix</button>
|
|
125
|
+
<button class="tab" role="tab" aria-selected="false" data-view="treemap">Treemap</button>
|
|
126
|
+
<button class="tab" role="tab" aria-selected="false" data-view="graph">Graph</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div id="wrap">
|
|
130
|
+
<div id="main">
|
|
131
|
+
<div class="view on" id="view-findings"></div>
|
|
132
|
+
<div class="view" id="view-matrix"></div>
|
|
133
|
+
<div class="view" id="view-treemap"><div id="tm"></div><div id="tmLegend"><span>duplication</span><span class="bar"></span><span>0 → 100%</span></div></div>
|
|
134
|
+
<div class="view" id="view-graph">
|
|
135
|
+
<canvas id="cv" role="img" aria-label="Force-directed dependency graph"></canvas>
|
|
136
|
+
<input id="gsearch" placeholder="search symbols… (Enter jumps)" autocomplete="off" aria-label="Search symbols; press Enter to jump to the next match, Escape to clear">
|
|
137
|
+
<span id="gcount" class="meta" style="position:absolute;top:16px;left:245px" aria-live="polite"></span>
|
|
138
|
+
<div id="gctrl"><button id="gFit">Fit</button><button id="gToggle"></button></div>
|
|
139
|
+
<div id="gLegend">● area — size = symbols, click to expand it · ○ symbol — size = loc · scroll zooms</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
<div id="detail" aria-live="polite"><div class="empty">Pick an item to inspect it.</div></div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<script id="graph-data" type="application/json">__GRAPH_DATA__</script>
|
|
146
|
+
|
|
147
|
+
<script>
|
|
148
|
+
(function () {
|
|
149
|
+
"use strict";
|
|
150
|
+
var G;
|
|
151
|
+
try { G = JSON.parse(document.getElementById('graph-data').textContent); }
|
|
152
|
+
catch (e) { document.body.innerHTML = '<p style="color:#f88;padding:24px">Failed to parse graph data: ' + e.message + '</p>'; return; }
|
|
153
|
+
G.nodes = G.nodes || []; G.edges = G.edges || []; G.domains = G.domains || []; G.overlaps = G.overlaps || []; G.meta = G.meta || {};
|
|
154
|
+
G.nodes.forEach(function (n) { if (!n.domain) n.domain = 'unassigned'; });
|
|
155
|
+
|
|
156
|
+
// ---------- code roles ----------
|
|
157
|
+
// Product code vs supporting code (tests / fixtures / examples / benches / generated). The
|
|
158
|
+
// extractor stamps node.role; older graphs fall back to a path heuristic here. Rankings and
|
|
159
|
+
// views default to PRODUCT scope — on a monorepo the map otherwise drowns in playground noise.
|
|
160
|
+
function deriveRole(file) {
|
|
161
|
+
var f = file || '';
|
|
162
|
+
if (/(^|\/)(tests?|__tests__)\//.test(f) || /(\.test\.|\.spec\.|_test\.)/.test(f)) return 'test';
|
|
163
|
+
if (/(^|\/)(fixtures?|__fixtures__|__mocks__|mocks?)\//.test(f)) return 'fixture';
|
|
164
|
+
if (/(^|\/)(examples?|samples?|demos?|playgrounds?|playground|sandbox|e2e)\//.test(f)) return 'example';
|
|
165
|
+
if (/(^|\/)(benchmarks?|bench|perf)\//.test(f)) return 'bench';
|
|
166
|
+
if (/\.(min|bundle)\.[cm]?js$/.test(f) || /(^|\/)(generated|__generated__)\//.test(f)) return 'generated';
|
|
167
|
+
return 'product';
|
|
168
|
+
}
|
|
169
|
+
G.nodes.forEach(function (n) { if (!n.role) n.role = deriveRole(n.file); });
|
|
170
|
+
var nonProduct = G.nodes.filter(function (n) { return n.role !== 'product'; }).length;
|
|
171
|
+
var productOnly = nonProduct > 0; // default: product scope whenever there is anything to hide
|
|
172
|
+
|
|
173
|
+
// ---------- indexes (full graph — the detail panel always shows the whole truth) ----------
|
|
174
|
+
var nodeById = new Map(G.nodes.map(function (n) { return [n.id, n]; }));
|
|
175
|
+
var outAdj = new Map(), inAdj = new Map(), inDeg = new Map(), outDeg = new Map();
|
|
176
|
+
G.nodes.forEach(function (n) { outAdj.set(n.id, []); inAdj.set(n.id, []); inDeg.set(n.id, 0); outDeg.set(n.id, 0); });
|
|
177
|
+
G.edges.forEach(function (e) {
|
|
178
|
+
if (!nodeById.has(e.from) || !nodeById.has(e.to)) return;
|
|
179
|
+
outAdj.get(e.from).push(e.to); inAdj.get(e.to).push(e.from);
|
|
180
|
+
outDeg.set(e.from, outDeg.get(e.from) + 1); inDeg.set(e.to, inDeg.get(e.to) + 1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// The validated categorical set (fixed order — the ORDER is the CVD-safety mechanism).
|
|
184
|
+
// Areas get slots by size rank; a 9th+ area folds to neutral (labels still carry identity).
|
|
185
|
+
var domColor = {};
|
|
186
|
+
var DOMC = ['#3987e5', '#008300', '#d55181', '#c98500', '#199e70', '#d95926', '#9085e9', '#e66767'];
|
|
187
|
+
var NEUTRAL = '#7d7a87';
|
|
188
|
+
function colorOf(nm) { return domColor[nm] || NEUTRAL; }
|
|
189
|
+
// darker step of the same hue — node rings, treemap header chips
|
|
190
|
+
function shade(hex, f) {
|
|
191
|
+
var n = parseInt(hex.slice(1), 16), r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
|
|
192
|
+
return 'rgb(' + Math.round(r * f) + ',' + Math.round(g * f) + ',' + Math.round(b * f) + ')';
|
|
193
|
+
}
|
|
194
|
+
var domSummary = {}; G.domains.forEach(function (d) { domSummary[d.name] = d; });
|
|
195
|
+
|
|
196
|
+
// ---------- active scope + derived metrics (recomputed when the role filter toggles) ----------
|
|
197
|
+
var AN = [], AE = [], activeIds = null, domainNames = [], duplicates = [], patterns = [], dupNodeIds = new Set(), hotspots = [], dead = [];
|
|
198
|
+
var ENTRY = /^(main|index|cli|run|start|default|setup|init|activate|handler|middleware|module)$/i;
|
|
199
|
+
function domCount(nm) { return AN.filter(function (n) { return n.domain === nm; }).length; }
|
|
200
|
+
function computeDerived() {
|
|
201
|
+
AN = productOnly ? G.nodes.filter(function (n) { return n.role === 'product'; }) : G.nodes;
|
|
202
|
+
activeIds = new Set(AN.map(function (n) { return n.id; }));
|
|
203
|
+
AE = G.edges.filter(function (e) { return activeIds.has(e.from) && activeIds.has(e.to); });
|
|
204
|
+
|
|
205
|
+
domainNames = Array.from(new Set(AN.map(function (n) { return n.domain; })));
|
|
206
|
+
domainNames = domainNames.filter(function (nm) { return domCount(nm) > 0; }).sort(function (a, b) { return domCount(b) - domCount(a); });
|
|
207
|
+
domainNames.forEach(function (nm, i) { if (!domColor[nm]) domColor[nm] = DOMC[i] || NEUTRAL; });
|
|
208
|
+
|
|
209
|
+
// duplicates: same symbol name defined in >=2 files — split off INTERFACE PATTERNS (many
|
|
210
|
+
// same-named implementations that nothing in-repo calls: framework hooks/contracts like a
|
|
211
|
+
// bundler plugin's resolveId(). "Merge these" is wrong advice for those.)
|
|
212
|
+
var byName = new Map();
|
|
213
|
+
AN.forEach(function (n) {
|
|
214
|
+
if (!['function', 'method', 'class'].includes(n.kind)) return;
|
|
215
|
+
if (!byName.has(n.label)) byName.set(n.label, []); byName.get(n.label).push(n);
|
|
216
|
+
});
|
|
217
|
+
duplicates = []; patterns = [];
|
|
218
|
+
byName.forEach(function (arr, name) {
|
|
219
|
+
var files = new Set(arr.map(function (n) { return n.file; }));
|
|
220
|
+
if (files.size >= 2 && name.length > 3) {
|
|
221
|
+
var doms = new Set(arr.map(function (n) { return n.domain; }));
|
|
222
|
+
var uncalled = arr.filter(function (n) { return inDeg.get(n.id) === 0; }).length;
|
|
223
|
+
var g = { name: name, files: files.size, count: arr.length, nodes: arr, domains: Array.from(doms) };
|
|
224
|
+
if (files.size >= 4 && uncalled / arr.length >= 0.75) patterns.push(g); else duplicates.push(g);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
duplicates.sort(function (a, b) { return b.files - a.files; });
|
|
228
|
+
patterns.sort(function (a, b) { return b.files - a.files; });
|
|
229
|
+
dupNodeIds = new Set(); duplicates.forEach(function (d) { d.nodes.forEach(function (n) { dupNodeIds.add(n.id); }); });
|
|
230
|
+
|
|
231
|
+
hotspots = AN.slice().filter(function (n) { return inDeg.get(n.id) > 0; })
|
|
232
|
+
.sort(function (a, b) { return inDeg.get(b.id) - inDeg.get(a.id); }).slice(0, 30);
|
|
233
|
+
|
|
234
|
+
dead = AN.filter(function (n) {
|
|
235
|
+
return inDeg.get(n.id) === 0 && ['function', 'method'].includes(n.kind) && !ENTRY.test(n.label) && (n.loc || 0) >= 3;
|
|
236
|
+
}).sort(function (a, b) { return (b.loc || 0) - (a.loc || 0); });
|
|
237
|
+
}
|
|
238
|
+
computeDerived();
|
|
239
|
+
|
|
240
|
+
// ---------- meta bar + role filter ----------
|
|
241
|
+
function renderMeta() {
|
|
242
|
+
document.getElementById('metaTarget').textContent = (G.meta.target || '') + (G.meta.mode ? ' · ' + G.meta.mode : '');
|
|
243
|
+
var el = document.getElementById('metaStats');
|
|
244
|
+
el.textContent = [
|
|
245
|
+
AN.length + ' symbols' + (productOnly && nonProduct ? ' (+' + nonProduct + ' hidden)' : ''),
|
|
246
|
+
AE.length + ' edges', domainNames.length + ' areas', duplicates.length + ' dup-groups'
|
|
247
|
+
].filter(Boolean).join(' · ');
|
|
248
|
+
el.title = G.meta.engine ? 'extraction engine: ' + G.meta.engine : ''; // internals live in the tooltip, not the masthead
|
|
249
|
+
}
|
|
250
|
+
renderMeta();
|
|
251
|
+
var roleBtn = document.getElementById('roleFilter');
|
|
252
|
+
if (nonProduct > 0) {
|
|
253
|
+
roleBtn.hidden = false;
|
|
254
|
+
var syncRoleBtn = function () {
|
|
255
|
+
roleBtn.textContent = productOnly ? 'Product only ✓' : 'All code';
|
|
256
|
+
roleBtn.setAttribute('aria-pressed', String(productOnly));
|
|
257
|
+
roleBtn.className = 'tab' + (productOnly ? ' on' : '');
|
|
258
|
+
};
|
|
259
|
+
syncRoleBtn();
|
|
260
|
+
roleBtn.onclick = function () {
|
|
261
|
+
productOnly = !productOnly; syncRoleBtn();
|
|
262
|
+
computeDerived(); renderMeta();
|
|
263
|
+
rendered = {}; // every view re-renders against the new scope
|
|
264
|
+
var cur = document.querySelector('.tab[role="tab"].on').getAttribute('data-view');
|
|
265
|
+
if (gInit) { gLayout(); syncToggle(); }
|
|
266
|
+
activate(cur);
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------- detail panel ----------
|
|
271
|
+
var detail = document.getElementById('detail');
|
|
272
|
+
detail.addEventListener('click', function (ev) {
|
|
273
|
+
var a = ev.target.closest('[data-set-root]');
|
|
274
|
+
if (!a) return;
|
|
275
|
+
ev.preventDefault();
|
|
276
|
+
var v = prompt('Absolute project root for editor links (stored only in this browser):', editorRoot());
|
|
277
|
+
if (v == null) return;
|
|
278
|
+
try { localStorage.setItem('cwEditorRoot', v.trim().replace(/\/+$/, '')); } catch (e) { /* private mode */ }
|
|
279
|
+
if (lastNodeId) showNode(lastNodeId);
|
|
280
|
+
});
|
|
281
|
+
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|
282
|
+
// First paint is an OVERVIEW, not an empty panel — the 340px column always earns its width.
|
|
283
|
+
// Spec J: the report is where a human decides to act — close the loop with a real editor link.
|
|
284
|
+
// The shipped HTML never embeds the absolute source path (standing privacy invariant: reports
|
|
285
|
+
// are shareable), so the VIEWER supplies their project root once — stored in localStorage,
|
|
286
|
+
// client-side only — and every node gets a vscode://file/<root>/<file>:<line> link. Without a
|
|
287
|
+
// root: a copyable file:line plus the one-click affordance to set it. Nothing silently missing.
|
|
288
|
+
var lastNodeId = null;
|
|
289
|
+
function editorRoot() { try { return localStorage.getItem('cwEditorRoot') || ''; } catch (e) { return ''; } }
|
|
290
|
+
function editorLink(n) {
|
|
291
|
+
if (!n || !n.file) return '';
|
|
292
|
+
var line = n.line || 1, root = editorRoot();
|
|
293
|
+
if (root) {
|
|
294
|
+
var abs = root.replace(/\/+$/, '') + '/' + n.file;
|
|
295
|
+
return '<div class="kv"><a href="vscode://file/' + esc(abs) + ':' + line + '" style="color:var(--accent)">Open in editor</a> · <a href="#" data-set-root style="color:var(--muted)">root ✎</a></div>';
|
|
296
|
+
}
|
|
297
|
+
return '<div class="kv">editor: <code>' + esc(n.file + ':' + line) + '</code> · <a href="#" data-set-root style="color:var(--muted)">set root for editor links</a></div>';
|
|
298
|
+
}
|
|
299
|
+
function detailEmpty() {
|
|
300
|
+
var topDoms = domainNames.slice(0, 5).map(function (nm) {
|
|
301
|
+
return '<div class="kv"><span class="chip" style="background:' + colorOf(nm) + '"></span> ' + esc(nm) + ' <span class="dim">· ' + domCount(nm) + ' symbols</span></div>';
|
|
302
|
+
}).join('');
|
|
303
|
+
detail.innerHTML = '<h3>' + esc(G.meta.target || 'this repo') + '</h3>' +
|
|
304
|
+
'<div class="kv">' + AN.length + ' symbols · ' + AE.length + ' edges · ' + domainNames.length + ' areas</div>' +
|
|
305
|
+
'<div class="sec">largest areas</div>' + topDoms +
|
|
306
|
+
'<div class="sec">worth a look</div>' +
|
|
307
|
+
'<div class="kv">' + duplicates.length + ' duplication group(s) · ' + hotspots.length + ' hotspot(s) · ' + dead.length + ' dead-code candidate(s)</div>' +
|
|
308
|
+
'<div class="sec">how to read this</div>' +
|
|
309
|
+
'<div class="kv">Click any row, cell, or node to inspect it here. In the graph, click an area bubble to expand it; search + Enter jumps to a symbol. <code>#s=<id></code> deep-links.</div>';
|
|
310
|
+
}
|
|
311
|
+
function nodeRow(id) { var t = nodeById.get(id); return '<div data-node="' + esc(id) + '">' + esc(t ? (t.label || id) : id) + ' <span class="dim mono">' + esc(t ? t.file : '') + '</span></div>'; }
|
|
312
|
+
function bindNodeRows(scope) {
|
|
313
|
+
(scope || detail).querySelectorAll('[data-node]').forEach(function (el) {
|
|
314
|
+
var go = function () { showNode(el.getAttribute('data-node')); };
|
|
315
|
+
el.onclick = go;
|
|
316
|
+
el.setAttribute('tabindex', '0');
|
|
317
|
+
el.onkeydown = function (ev) { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); go(); } };
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function showNode(id) {
|
|
321
|
+
var n = nodeById.get(id); if (!n) return;
|
|
322
|
+
lastNodeId = id;
|
|
323
|
+
try { history.replaceState(null, '', '#s=' + encodeURIComponent(id)); } catch (e) { /* file:// quirk */ }
|
|
324
|
+
var outs = outAdj.get(id) || [], ins = inAdj.get(id) || [];
|
|
325
|
+
detail.innerHTML = '<h3>' + esc(n.label) + '</h3>' +
|
|
326
|
+
'<div class="kv">' + esc(n.kind || '') + (n.role && n.role !== 'product' ? ' · ' + esc(n.role) : '') + (n.exports ? ' · exported' : '') + (dupNodeIds.has(id) ? ' · <span style="color:var(--hi)">duplicated</span>' : '') + '</div>' +
|
|
327
|
+
'<div class="kv"><span style="display:inline-block;width:10px;height:10px;border-radius:3px;background:' + colorOf(n.domain) + '"></span> ' + esc(n.domain) + '</div>' +
|
|
328
|
+
(n.file ? '<div class="kv"><code>' + esc(n.file) + (n.line ? ':' + n.line : '') + '</code>' + (n.loc ? ' · ' + n.loc + ' loc' : '') + '</div>' : '') +
|
|
329
|
+
editorLink(n) +
|
|
330
|
+
(n.summary ? '<div class="kv">' + esc(n.summary) + '</div>' : '') +
|
|
331
|
+
'<div class="sec">used by (' + ins.length + ')</div><div class="lst" id="d_in"></div>' +
|
|
332
|
+
'<div class="sec">calls (' + outs.length + ')</div><div class="lst" id="d_out"></div>';
|
|
333
|
+
document.getElementById('d_in').innerHTML = ins.slice(0, 60).map(nodeRow).join('') || '<div class="empty">— (nothing calls this)</div>';
|
|
334
|
+
document.getElementById('d_out').innerHTML = outs.slice(0, 60).map(nodeRow).join('') || '<div class="empty">—</div>';
|
|
335
|
+
bindNodeRows();
|
|
336
|
+
}
|
|
337
|
+
function showDup(d) {
|
|
338
|
+
detail.innerHTML = '<h3>' + esc(d.name) + '()</h3>' +
|
|
339
|
+
'<div class="kv">defined in <b>' + d.files + ' files</b> · ' + d.domains.length + ' area(s)</div>' +
|
|
340
|
+
'<div class="kv">' + esc(d.domains.join(', ')) + '</div>' +
|
|
341
|
+
'<div class="sec">every definition</div><div class="lst" id="d_dup"></div>';
|
|
342
|
+
document.getElementById('d_dup').innerHTML = d.nodes.map(function (n) { return nodeRow(n.id); }).join('');
|
|
343
|
+
bindNodeRows();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---------- FINDINGS ----------
|
|
347
|
+
function sev(files) { return files >= 6 ? 'hi' : files >= 4 ? 'med' : 'low'; }
|
|
348
|
+
function section(title, sub, n, body) {
|
|
349
|
+
return '<div class="fsec"><div class="fhead"><h2>' + esc(title) + '</h2><span class="sub">' + esc(sub) + '</span><span class="pill">' + n + '</span></div>' + body + '</div>';
|
|
350
|
+
}
|
|
351
|
+
function renderFindings() {
|
|
352
|
+
var v = document.getElementById('view-findings');
|
|
353
|
+
var dupRows = duplicates.slice(0, 40).map(function (d, i) {
|
|
354
|
+
return '<tr data-dup="' + i + '"><td><span class="badge b-' + sev(d.files) + '">' + d.files + '</span></td>' +
|
|
355
|
+
'<td class="mono">' + esc(d.name) + '()</td>' +
|
|
356
|
+
'<td class="dim">' + esc(d.domains.slice(0, 4).join(', ')) + (d.domains.length > 4 ? '…' : '') + '</td>' +
|
|
357
|
+
'<td class="dim">defined in ' + d.files + ' files</td></tr>';
|
|
358
|
+
}).join('');
|
|
359
|
+
var hotRows = hotspots.map(function (n) {
|
|
360
|
+
return '<tr data-node="' + esc(n.id) + '"><td><span class="badge b-acc">' + inDeg.get(n.id) + '</span></td>' +
|
|
361
|
+
'<td class="mono">' + esc(n.label) + '</td><td class="dim mono">' + esc(n.file) + '</td>' +
|
|
362
|
+
'<td class="dim">used by ' + inDeg.get(n.id) + '</td></tr>';
|
|
363
|
+
}).join('');
|
|
364
|
+
var deadRows = dead.slice(0, 40).map(function (n) {
|
|
365
|
+
return '<tr data-node="' + esc(n.id) + '"><td class="mono">' + esc(n.label) + '</td>' +
|
|
366
|
+
'<td class="dim mono">' + esc(n.file) + (n.line ? ':' + n.line : '') + '</td>' +
|
|
367
|
+
'<td class="dim">' + (n.loc || 0) + ' loc' + (n.exports ? ' · exported' : '') + '</td></tr>';
|
|
368
|
+
}).join('');
|
|
369
|
+
var patRows = patterns.slice(0, 20).map(function (d, i) {
|
|
370
|
+
return '<tr data-pat="' + i + '"><td><span class="badge b-low">' + d.files + '</span></td>' +
|
|
371
|
+
'<td class="mono">' + esc(d.name) + '()</td>' +
|
|
372
|
+
'<td class="dim">' + esc(d.domains.slice(0, 4).join(', ')) + (d.domains.length > 4 ? '…' : '') + '</td>' +
|
|
373
|
+
'<td class="dim">' + d.files + ' implementations, none called in-repo</td></tr>';
|
|
374
|
+
}).join('');
|
|
375
|
+
v.innerHTML =
|
|
376
|
+
section('Duplication', 'merge these — same function defined in many files', duplicates.length,
|
|
377
|
+
dupRows ? '<table class="f">' + dupRows + '</table>' : '<div class="empty">No duplication found 🎉</div>') +
|
|
378
|
+
(patterns.length ? section('Interface patterns', 'same-name implementations of a framework contract (nothing in-repo calls them) — NOT copy-paste; don’t merge', patterns.length,
|
|
379
|
+
'<table class="f">' + patRows + '</table>') : '') +
|
|
380
|
+
section('Hotspots', 'most depended-on code — change with care', hotspots.length,
|
|
381
|
+
hotRows ? '<table class="f">' + hotRows + '</table>' : '<div class="empty">—</div>') +
|
|
382
|
+
section('Likely dead code', 'nothing in this graph calls these', dead.length,
|
|
383
|
+
deadRows ? '<table class="f">' + deadRows + '</table>' : '<div class="empty">—</div>');
|
|
384
|
+
v.querySelectorAll('tr[data-dup]').forEach(function (tr) { tr.onclick = function () { showDup(duplicates[+tr.getAttribute('data-dup')]); }; });
|
|
385
|
+
v.querySelectorAll('tr[data-pat]').forEach(function (tr) { tr.onclick = function () { showDup(patterns[+tr.getAttribute('data-pat')]); }; });
|
|
386
|
+
v.querySelectorAll('tr[data-dup],tr[data-pat],tr[data-node]').forEach(function (tr) {
|
|
387
|
+
tr.setAttribute('tabindex', '0');
|
|
388
|
+
tr.onkeydown = function (ev) { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); tr.onclick(); } };
|
|
389
|
+
});
|
|
390
|
+
bindNodeRows(v);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---------- MATRIX ----------
|
|
394
|
+
function renderMatrix() {
|
|
395
|
+
var v = document.getElementById('view-matrix');
|
|
396
|
+
var idx = {}; domainNames.forEach(function (d, i) { idx[d] = i; });
|
|
397
|
+
var M = domainNames.map(function () { return domainNames.map(function () { return 0; }); });
|
|
398
|
+
AE.forEach(function (e) {
|
|
399
|
+
var a = nodeById.get(e.from), b = nodeById.get(e.to); if (!a || !b) return;
|
|
400
|
+
if (idx[a.domain] == null || idx[b.domain] == null) return;
|
|
401
|
+
M[idx[a.domain]][idx[b.domain]] += (e.weight || 1);
|
|
402
|
+
});
|
|
403
|
+
var max = 1; M.forEach(function (r, i) { r.forEach(function (val, j) { if (i !== j && val > max) max = val; }); });
|
|
404
|
+
var head = '<th></th>' + domainNames.map(function (d) { return '<th class="rot"><span>' + esc(d) + '</span></th>'; }).join('');
|
|
405
|
+
var rows = domainNames.map(function (da, i) {
|
|
406
|
+
var cells = domainNames.map(function (db, j) {
|
|
407
|
+
var val = M[i][j];
|
|
408
|
+
if (val === 0) return '<td class="zero' + (i === j ? ' diag' : '') + '">·</td>';
|
|
409
|
+
var t = Math.min(1, Math.log(val + 1) / Math.log(max + 1));
|
|
410
|
+
var bg = i === j ? 'rgba(25,158,112,' + (0.28 + t * 0.55) + ')' : 'rgba(57,135,229,' + (0.2 + t * 0.72) + ')';
|
|
411
|
+
return '<td class="' + (i === j ? 'diag' : '') + '" style="background:' + bg + '" data-a="' + i + '" data-b="' + j + '">' + val + '</td>';
|
|
412
|
+
}).join('');
|
|
413
|
+
return '<tr><th>' + esc(da) + '</th>' + cells + '</tr>';
|
|
414
|
+
}).join('');
|
|
415
|
+
v.innerHTML = '<div class="fhead"><h2>Area coupling</h2><span class="sub">rows call columns</span></div>' +
|
|
416
|
+
'<table class="m"><tr>' + head + '</tr>' + rows + '</table>' +
|
|
417
|
+
'<div class="legend"><span class="chip" style="background:rgba(57,135,229,.8)"></span> cross-area calls — tangling' +
|
|
418
|
+
'<span class="chip" style="background:rgba(25,158,112,.8)"></span> diagonal — internal cohesion' +
|
|
419
|
+
'<span>· big off-diagonal numbers = merge candidates or missing interface · click a cell for its edges</span></div>';
|
|
420
|
+
v.querySelectorAll('td[data-a]').forEach(function (td) {
|
|
421
|
+
td.onclick = function () { showCoupling(domainNames[+td.getAttribute('data-a')], domainNames[+td.getAttribute('data-b')]); };
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function showCoupling(a, b) {
|
|
425
|
+
var es = G.edges.filter(function (e) { var x = nodeById.get(e.from), y = nodeById.get(e.to); return x && y && x.domain === a && y.domain === b; });
|
|
426
|
+
detail.innerHTML = '<h3>' + esc(a) + ' → ' + esc(b) + '</h3><div class="kv">' + es.length + ' call edge(s)</div>' +
|
|
427
|
+
'<div class="sec">edges</div><div class="lst" id="d_e"></div>';
|
|
428
|
+
document.getElementById('d_e').innerHTML = es.slice(0, 80).map(function (e) {
|
|
429
|
+
return '<div data-node="' + esc(e.from) + '">' + esc(nodeById.get(e.from).label) + ' <span class="dim">→ ' + esc(nodeById.get(e.to).label) + '</span></div>';
|
|
430
|
+
}).join('') || '<div class="empty">—</div>';
|
|
431
|
+
bindNodeRows();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ---------- TREEMAP ----------
|
|
435
|
+
function bisect(items, x, y, w, h, out) {
|
|
436
|
+
if (!items.length) return;
|
|
437
|
+
if (items.length === 1) { out.push(Object.assign({}, items[0], { x: x, y: y, w: w, h: h })); return; }
|
|
438
|
+
var total = items.reduce(function (s, i) { return s + i.value; }, 0), acc = 0, k = 0;
|
|
439
|
+
for (; k < items.length - 1; k++) { if (acc + items[k].value > total / 2) break; acc += items[k].value; }
|
|
440
|
+
// always leave >=1 item on each side: a dominant last item (or all-zero values) would otherwise
|
|
441
|
+
// put every item back into `a` and recurse forever -> RangeError on large/degenerate graphs.
|
|
442
|
+
var cut = Math.min(k + 1, items.length - 1);
|
|
443
|
+
var a = items.slice(0, cut), b = items.slice(cut);
|
|
444
|
+
var frac = total > 0 ? a.reduce(function (s, i) { return s + i.value; }, 0) / total : cut / items.length;
|
|
445
|
+
if (w >= h) { bisect(a, x, y, w * frac, h, out); bisect(b, x + w * frac, y, w * (1 - frac), h, out); }
|
|
446
|
+
else { bisect(a, x, y, w, h * frac, out); bisect(b, x, y + h * frac, w, h * (1 - frac), out); }
|
|
447
|
+
}
|
|
448
|
+
// Sequential single-hue ramp (dark slate -> risk red), lightness monotonic — density is a
|
|
449
|
+
// MAGNITUDE, so lightness carries it (CVD-safe by construction; the old green->red pair
|
|
450
|
+
// measured deutan ΔE 2.2 — invisible to red-green-deficient viewers).
|
|
451
|
+
function dupColor(density) {
|
|
452
|
+
var t = Math.max(0, Math.min(1, density));
|
|
453
|
+
var r = Math.round(41 + (230 - 41) * t), g = Math.round(35 + (103 - 35) * t), b = Math.round(49 + (103 - 49) * t);
|
|
454
|
+
return 'rgb(' + r + ',' + g + ',' + b + ')';
|
|
455
|
+
}
|
|
456
|
+
function renderTreemap() {
|
|
457
|
+
var tm = document.getElementById('tm'); tm.innerHTML = '';
|
|
458
|
+
var Wd = tm.clientWidth || 800, Hd = tm.clientHeight || 600;
|
|
459
|
+
var doms = domainNames.map(function (d) {
|
|
460
|
+
var ns = AN.filter(function (n) { return n.domain === d; });
|
|
461
|
+
return { name: d, value: ns.reduce(function (s, n) { return s + (n.loc || 1); }, 0), nodes: ns };
|
|
462
|
+
}).filter(function (d) { return d.value > 0; }).sort(function (a, b) { return b.value - a.value; });
|
|
463
|
+
var drects = []; bisect(doms.map(function (d) { return { value: d.value, ref: d }; }), 0, 0, Wd, Hd, drects);
|
|
464
|
+
drects.forEach(function (dr) {
|
|
465
|
+
var d = dr.ref;
|
|
466
|
+
var border = document.createElement('div');
|
|
467
|
+
border.className = 'tmbox tmdom'; border.style.left = dr.x + 'px'; border.style.top = dr.y + 'px';
|
|
468
|
+
border.style.width = dr.w + 'px'; border.style.height = dr.h + 'px';
|
|
469
|
+
tm.appendChild(border);
|
|
470
|
+
var fmap = new Map();
|
|
471
|
+
d.nodes.forEach(function (n) { if (!fmap.has(n.file)) fmap.set(n.file, []); fmap.get(n.file).push(n); });
|
|
472
|
+
var files = Array.from(fmap.entries()).map(function (kv) {
|
|
473
|
+
var loc = kv[1].reduce(function (s, n) { return s + (n.loc || 1); }, 0);
|
|
474
|
+
var dups = kv[1].filter(function (n) { return dupNodeIds.has(n.id); }).length;
|
|
475
|
+
return { value: loc, file: kv[0], nodes: kv[1], density: kv[1].length ? dups / kv[1].length : 0 };
|
|
476
|
+
}).sort(function (a, b) { return b.value - a.value; });
|
|
477
|
+
var pad = 18, frects = [];
|
|
478
|
+
bisect(files.map(function (f) { return { value: f.value, ref: f }; }), dr.x + 2, dr.y + pad, Math.max(1, dr.w - 4), Math.max(1, dr.h - pad - 2), frects);
|
|
479
|
+
frects.forEach(function (fr) {
|
|
480
|
+
var f = fr.ref, box = document.createElement('div');
|
|
481
|
+
box.className = 'tmbox'; box.style.left = fr.x + 'px'; box.style.top = fr.y + 'px';
|
|
482
|
+
box.style.width = fr.w + 'px'; box.style.height = fr.h + 'px';
|
|
483
|
+
box.style.background = dupColor(f.density);
|
|
484
|
+
box.title = f.file + ' (' + f.value + ' loc, ' + Math.round(f.density * 100) + '% duplicated)';
|
|
485
|
+
if (fr.w > 46 && fr.h > 14) { var lab = document.createElement('div'); lab.className = 'tmlabel'; lab.textContent = f.file.split('/').pop(); box.appendChild(lab); }
|
|
486
|
+
box.onclick = function () { showFile(f); };
|
|
487
|
+
tm.appendChild(box);
|
|
488
|
+
});
|
|
489
|
+
var dl = document.createElement('div'); dl.className = 'tmdomlabel'; dl.style.left = (dr.x + 6) + 'px'; dl.style.top = (dr.y + 2) + 'px'; dl.style.maxWidth = Math.max(20, dr.w - 10) + 'px';
|
|
490
|
+
dl.innerHTML = '<span class="chip" style="background:' + colorOf(d.name) + '"></span>' + esc(d.name) + ' <span class="dim">· ' + d.nodes.length + '</span>'; tm.appendChild(dl);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
function showFile(f) {
|
|
494
|
+
detail.innerHTML = '<h3 class="mono">' + esc(f.file) + '</h3>' +
|
|
495
|
+
'<div class="kv">' + f.nodes.length + ' symbols · ' + f.value + ' loc · ' + Math.round(f.density * 100) + '% duplicated</div>' +
|
|
496
|
+
'<div class="sec">symbols</div><div class="lst" id="d_f"></div>';
|
|
497
|
+
document.getElementById('d_f').innerHTML = f.nodes.map(function (n) {
|
|
498
|
+
return '<div data-node="' + esc(n.id) + '">' + (dupNodeIds.has(n.id) ? '⚠ ' : '') + esc(n.label) + ' <span class="dim">' + (n.loc || 0) + ' loc</span></div>';
|
|
499
|
+
}).join('');
|
|
500
|
+
bindNodeRows();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ---------- GRAPH ----------
|
|
504
|
+
// Focus + context: every area starts as a bubble; clicking one expands ITS symbols in place
|
|
505
|
+
// while the rest stay collapsed — the all-at-once hairball is structurally impossible unless
|
|
506
|
+
// explicitly asked for ("Expand all"). Layout is deterministic (seeded spiral, no random), so
|
|
507
|
+
// the same repo always draws the same map. Positions survive re-layout for continuity.
|
|
508
|
+
var gInit = false, expanded = new Set(), W = null, sim = null, selectedNode = null, searchTerm = '', lastLit = null;
|
|
509
|
+
var lastPos = new Map(); // id -> {x,y} carried across layouts
|
|
510
|
+
var cv = document.getElementById('cv'), ctx = cv.getContext('2d'), DPR = window.devicePixelRatio || 1, cam = { x: 0, y: 0, k: 1 };
|
|
511
|
+
var UIFONT = 'system-ui,-apple-system,"Segoe UI",Roboto,Arial,sans-serif'; // canvas needs a REAL stack (bare ui-sans-serif falls back to serif)
|
|
512
|
+
function allExpanded() { return domainNames.every(function (d) { return expanded.has(d); }); }
|
|
513
|
+
function gResize() { var r = cv.parentElement.getBoundingClientRect(); cv.width = r.width * DPR; cv.height = r.height * DPR; cv.style.width = r.width + 'px'; cv.style.height = r.height + 'px'; }
|
|
514
|
+
function mapEnd(n) { return expanded.has(n.domain) ? n.id : 'd:' + n.domain; }
|
|
515
|
+
function buildW() {
|
|
516
|
+
var wn = [];
|
|
517
|
+
domainNames.forEach(function (nm) {
|
|
518
|
+
if (expanded.has(nm)) {
|
|
519
|
+
AN.forEach(function (n) { if (n.domain === nm) wn.push({ id: n.id, label: n.label, domain: nm, sym: true, r: Math.max(3.5, Math.min(16, 3 + Math.sqrt(n.loc || 6))) }); });
|
|
520
|
+
} else {
|
|
521
|
+
var c = domCount(nm);
|
|
522
|
+
wn.push({ id: 'd:' + nm, label: nm, domain: nm, bubble: true, count: c, r: Math.max(14, Math.min(56, 9 + Math.sqrt(c) * 4.6)) });
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
var agg = new Map();
|
|
526
|
+
AE.forEach(function (e) {
|
|
527
|
+
var a = nodeById.get(e.from), b = nodeById.get(e.to); if (!a || !b) return;
|
|
528
|
+
var f = mapEnd(a), t = mapEnd(b); if (f === t) return;
|
|
529
|
+
var k = f + '␟' + t;
|
|
530
|
+
var cur = agg.get(k); if (cur) cur.weight++; else agg.set(k, { from: f, to: t, weight: 1, cross: a.domain !== b.domain });
|
|
531
|
+
});
|
|
532
|
+
var we = Array.from(agg.values());
|
|
533
|
+
return { nodes: wn, edges: we, idx: new Map(wn.map(function (n) { return [n.id, n]; })) };
|
|
534
|
+
}
|
|
535
|
+
function gLayout() {
|
|
536
|
+
if (W) W.nodes.forEach(function (nd) { lastPos.set(nd.id, { x: nd.x, y: nd.y }); });
|
|
537
|
+
W = buildW();
|
|
538
|
+
var n = W.nodes.length, R = Math.max(140, Math.sqrt(n) * 60);
|
|
539
|
+
W.nodes.forEach(function (nd, i) {
|
|
540
|
+
var prev = lastPos.get(nd.id) || lastPos.get('d:' + nd.domain); // new symbols hatch from their area's bubble
|
|
541
|
+
if (prev) { nd.x = prev.x + (nd.sym ? Math.cos(i * 2.39996) * 14 : 0); nd.y = prev.y + (nd.sym ? Math.sin(i * 2.39996) * 14 : 0); }
|
|
542
|
+
else { var a = i * 2.39996, rad = R * Math.sqrt(i / Math.max(1, n)); nd.x = Math.cos(a) * rad; nd.y = Math.sin(a) * rad; }
|
|
543
|
+
nd.vx = 0; nd.vy = 0;
|
|
544
|
+
});
|
|
545
|
+
sim = { alpha: 1, running: false }; gFit(); gStart();
|
|
546
|
+
}
|
|
547
|
+
var REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
548
|
+
function gStart() {
|
|
549
|
+
if (!sim) return;
|
|
550
|
+
if (REDUCED) { // settle synchronously — no animation for motion-sensitive users
|
|
551
|
+
sim.alpha = 1;
|
|
552
|
+
for (var it = 0; it < 260 && sim.alpha > 0.02; it++) { gStep(); sim.alpha *= 0.985; }
|
|
553
|
+
gFit(); gDraw(); sim.running = false; return;
|
|
554
|
+
}
|
|
555
|
+
sim.alpha = Math.max(sim.alpha, 0.7); if (!sim.running) { sim.running = true; requestAnimationFrame(gTick); }
|
|
556
|
+
}
|
|
557
|
+
function gTick() { if (!sim || !document.getElementById('view-graph').classList.contains('on')) { if (sim) sim.running = false; return; } gStep(); gDraw(); sim.alpha *= 0.985; if (sim.alpha > 0.02) requestAnimationFrame(gTick); else sim.running = false; }
|
|
558
|
+
function gStep() {
|
|
559
|
+
var ns = W.nodes, es = W.edges, a = sim.alpha, i, j;
|
|
560
|
+
for (i = 0; i < ns.length; i++) { ns[i].fx = 0; ns[i].fy = 0; }
|
|
561
|
+
for (i = 0; i < ns.length; i++) {
|
|
562
|
+
var A = ns[i];
|
|
563
|
+
for (j = i + 1; j < ns.length; j++) {
|
|
564
|
+
var B = ns[j], dx = A.x - B.x, dy = A.y - B.y, d2 = dx * dx + dy * dy || 0.01, d = Math.sqrt(d2);
|
|
565
|
+
var repK = 900 + (A.r + B.r) * 55; // bigger bodies push harder — bubbles keep their space
|
|
566
|
+
var f = repK / d2;
|
|
567
|
+
A.fx += dx / d * f; A.fy += dy / d * f; B.fx -= dx / d * f; B.fy -= dy / d * f;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (i = 0; i < es.length; i++) {
|
|
571
|
+
var e = es[i], P = W.idx.get(e.from), Q = W.idx.get(e.to); if (!P || !Q) continue;
|
|
572
|
+
var L = P.bubble && Q.bubble ? 180 : (P.bubble || Q.bubble) ? 120 : 52;
|
|
573
|
+
var ex = Q.x - P.x, ey = Q.y - P.y, ed = Math.sqrt(ex * ex + ey * ey) || 0.01, df = (ed - L) / ed * 0.03;
|
|
574
|
+
P.fx += ex * df; P.fy += ey * df; Q.fx -= ex * df; Q.fy -= ey * df;
|
|
575
|
+
}
|
|
576
|
+
for (i = 0; i < ns.length; i++) { var nd = ns[i]; nd.fx += -nd.x * 0.015; nd.fy += -nd.y * 0.015; nd.vx = (nd.vx + nd.fx * a) * 0.86; nd.vy = (nd.vy + nd.fy * a) * 0.86; nd.x += nd.vx; nd.y += nd.vy; }
|
|
577
|
+
}
|
|
578
|
+
function gFit() { var ns = W.nodes; if (!ns.length) return; var a = 1e9, b = 1e9, c = -1e9, d = -1e9; ns.forEach(function (n) { a = Math.min(a, n.x); b = Math.min(b, n.y); c = Math.max(c, n.x); d = Math.max(d, n.y); }); var w = cv.width / DPR, h = cv.height / DPR; cam.k = Math.max(0.05, Math.min((w - 70) / Math.max(1, c - a), (h - 70) / Math.max(1, d - b), 2.5)); cam.x = w / 2 - (a + c) / 2 * cam.k; cam.y = h / 2 - (b + d) / 2 * cam.k; }
|
|
579
|
+
function X(x) { return x * cam.k + cam.x; } function Y(y) { return y * cam.k + cam.y; }
|
|
580
|
+
function gDraw() {
|
|
581
|
+
ctx.setTransform(DPR, 0, 0, DPR, 0, 0); ctx.clearRect(0, 0, cv.width, cv.height);
|
|
582
|
+
// highlight set: search matches, or the selected node + its neighbors (neighbors inside a
|
|
583
|
+
// collapsed bubble light the bubble — the connection is never invisible)
|
|
584
|
+
var hl = null, hlDom = null;
|
|
585
|
+
if (searchTerm) {
|
|
586
|
+
var t = searchTerm.toLowerCase();
|
|
587
|
+
hl = new Set(); hlDom = new Set();
|
|
588
|
+
AN.forEach(function (n) { if (n.label.toLowerCase().indexOf(t) >= 0) { hl.add(n.id); hlDom.add(n.domain); } });
|
|
589
|
+
} else if (selectedNode) {
|
|
590
|
+
hl = new Set([selectedNode]); hlDom = new Set();
|
|
591
|
+
var lightUp = function (id) { var n = nodeById.get(id); if (!n) return; hl.add(mapEnd(n)); hlDom.add(n.domain); };
|
|
592
|
+
(outAdj.get(selectedNode) || []).forEach(lightUp); (inAdj.get(selectedNode) || []).forEach(lightUp);
|
|
593
|
+
}
|
|
594
|
+
var lit = function (nd) { return !hl || hl.has(nd.id) || (nd.bubble && hlDom && hlDom.has(nd.domain)); };
|
|
595
|
+
lastLit = hl ? new Set(W.nodes.filter(lit).map(function (n) { return n.id; })) : null;
|
|
596
|
+
// edges: quadratic curves, weight -> alpha/width; cross-area edges wear the tangle color
|
|
597
|
+
W.edges.forEach(function (e) {
|
|
598
|
+
var A = W.idx.get(e.from), B = W.idx.get(e.to); if (!A || !B) return;
|
|
599
|
+
var on = !hl || (lit(A) && lit(B));
|
|
600
|
+
var wgt = Math.min(1, Math.log(e.weight + 1) / 3.4);
|
|
601
|
+
// between two collapsed bubbles EVERY edge is cross-area — orange would be noise there;
|
|
602
|
+
// the tangle color only means something once symbols are on screen
|
|
603
|
+
var tangle = e.cross && !(A.bubble && B.bubble);
|
|
604
|
+
ctx.strokeStyle = !on ? 'rgba(125,122,135,0.05)'
|
|
605
|
+
: tangle ? 'rgba(236,131,90,' + (0.25 + wgt * 0.4) + ')'
|
|
606
|
+
: 'rgba(156,153,166,' + (0.14 + wgt * 0.34) + ')';
|
|
607
|
+
ctx.lineWidth = A.bubble && B.bubble ? Math.max(0.8, Math.min(6, Math.log(e.weight + 1) * 1.4)) : 1;
|
|
608
|
+
var x1 = X(A.x), y1 = Y(A.y), x2 = X(B.x), y2 = Y(B.y);
|
|
609
|
+
var mx = (x1 + x2) / 2, my = (y1 + y2) / 2, dx = x2 - x1, dy = y2 - y1;
|
|
610
|
+
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.quadraticCurveTo(mx - dy * 0.09, my + dx * 0.09, x2, y2); ctx.stroke();
|
|
611
|
+
});
|
|
612
|
+
// nodes: fill + darker ring of the same hue; selection wears the brand accent
|
|
613
|
+
W.nodes.forEach(function (nd) {
|
|
614
|
+
var vis = lit(nd);
|
|
615
|
+
var c = colorOf(nd.domain);
|
|
616
|
+
ctx.globalAlpha = vis ? 1 : 0.14;
|
|
617
|
+
ctx.beginPath(); ctx.arc(X(nd.x), Y(nd.y), nd.r, 0, 6.2832);
|
|
618
|
+
ctx.fillStyle = c; ctx.fill();
|
|
619
|
+
ctx.lineWidth = nd.bubble ? 2 : 1.4; ctx.strokeStyle = shade(c, 0.5); ctx.stroke();
|
|
620
|
+
if (selectedNode === nd.id) { ctx.lineWidth = 2.5; ctx.strokeStyle = '#C6F24E'; ctx.beginPath(); ctx.arc(X(nd.x), Y(nd.y), nd.r + 2.5, 0, 6.2832); ctx.stroke(); }
|
|
621
|
+
var showLabel = vis && (nd.bubble || nd.r > 7.5 || cam.k > 1.7 || (hl && hl.has(nd.id) && hl.size < 40));
|
|
622
|
+
if (showLabel) {
|
|
623
|
+
var lx = X(nd.x) + nd.r + 4, ly = Y(nd.y) + 3.5;
|
|
624
|
+
ctx.font = (nd.bubble ? '600 12px ' : '500 10.5px ') + UIFONT;
|
|
625
|
+
ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(16,14,20,0.85)'; ctx.lineJoin = 'round';
|
|
626
|
+
var txt = nd.bubble ? nd.label + ' · ' + nd.count : nd.label;
|
|
627
|
+
ctx.strokeText(txt, lx, ly);
|
|
628
|
+
ctx.fillStyle = nd.bubble ? '#ECECEE' : '#D9D7E0'; ctx.fillText(txt, lx, ly);
|
|
629
|
+
}
|
|
630
|
+
ctx.globalAlpha = 1;
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
function gPick(mx, my) { var best = null, bd = 1e9; W.nodes.forEach(function (nd) { var dx = X(nd.x) - mx, dy = Y(nd.y) - my, d = Math.sqrt(dx * dx + dy * dy); if (d < nd.r + 4 && d < bd) { bd = d; best = nd; } }); return best; }
|
|
634
|
+
var gdrag = null, gmoved = false;
|
|
635
|
+
cv.addEventListener('pointerdown', function (ev) { var r = cv.getBoundingClientRect(); gmoved = false; gdrag = { mx: ev.clientX - r.left, my: ev.clientY - r.top, cx: cam.x, cy: cam.y }; try { cv.setPointerCapture(ev.pointerId); } catch (e) {} });
|
|
636
|
+
window.addEventListener('pointermove', function (ev) { if (!gdrag) return; var r = cv.getBoundingClientRect(), mx = ev.clientX - r.left, my = ev.clientY - r.top; if (Math.abs(mx - gdrag.mx) + Math.abs(my - gdrag.my) > 3) gmoved = true; cam.x = gdrag.cx + (mx - gdrag.mx); cam.y = gdrag.cy + (my - gdrag.my); gDraw(); });
|
|
637
|
+
window.addEventListener('pointerup', function (ev) { if (gdrag && !gmoved && document.getElementById('view-graph').classList.contains('on')) { var r = cv.getBoundingClientRect(), nd = gPick(ev.clientX - r.left, ev.clientY - r.top); if (nd) { if (nd.bubble) { expanded.add(nd.domain); gLayout(); syncToggle(); } else { selectedNode = nd.id; showNode(nd.id); gDraw(); } } else { selectedNode = null; gDraw(); } } gdrag = null; });
|
|
638
|
+
cv.addEventListener('wheel', function (ev) { ev.preventDefault(); var r = cv.getBoundingClientRect(), mx = ev.clientX - r.left, my = ev.clientY - r.top, f = ev.deltaY < 0 ? 1.12 : 0.89, nk = Math.max(0.05, Math.min(6, cam.k * f)); cam.x = mx - (mx - cam.x) * (nk / cam.k); cam.y = my - (my - cam.y) * (nk / cam.k); cam.k = nk; gDraw(); }, { passive: false });
|
|
639
|
+
document.getElementById('gFit').onclick = function () { gFit(); gDraw(); };
|
|
640
|
+
var gToggle = document.getElementById('gToggle');
|
|
641
|
+
function syncToggle() { gToggle.textContent = allExpanded() ? 'Collapse to areas' : (expanded.size ? 'Collapse (' + expanded.size + ' open)' : 'Expand all symbols'); }
|
|
642
|
+
gToggle.onclick = function () { if (expanded.size) expanded = new Set(); else domainNames.forEach(function (d) { expanded.add(d); }); selectedNode = null; gLayout(); syncToggle(); };
|
|
643
|
+
// Search NAVIGATES: live typing dims non-matches; Enter cycles matches (centering + selecting
|
|
644
|
+
// each), Escape clears. The count chip narrates n/N for screen readers too.
|
|
645
|
+
var gsearchEl = document.getElementById('gsearch'), gcountEl = document.getElementById('gcount');
|
|
646
|
+
var searchHits = [], searchIx = -1;
|
|
647
|
+
function centerOn(nd) { var w = cv.width / DPR, h = cv.height / DPR; cam.k = Math.max(cam.k, 0.9); cam.x = w / 2 - nd.x * cam.k; cam.y = h / 2 - nd.y * cam.k; }
|
|
648
|
+
function refreshHits() {
|
|
649
|
+
// search SYMBOLS (the active scope), not whatever bubbles the current view draws — a domain
|
|
650
|
+
// view must still find normalizePath and expand to it on Enter.
|
|
651
|
+
var t = searchTerm.toLowerCase();
|
|
652
|
+
searchHits = t ? AN.filter(function (n) { return n.label.toLowerCase().indexOf(t) >= 0; }) : [];
|
|
653
|
+
searchIx = -1;
|
|
654
|
+
gcountEl.textContent = searchTerm ? (searchHits.length ? searchHits.length + ' match(es) — Enter jumps' : 'no matches') : '';
|
|
655
|
+
}
|
|
656
|
+
gsearchEl.addEventListener('input', function (e) { searchTerm = e.target.value.trim(); refreshHits(); gDraw(); });
|
|
657
|
+
gsearchEl.addEventListener('keydown', function (e) {
|
|
658
|
+
if (e.key === 'Escape') { gsearchEl.value = ''; searchTerm = ''; refreshHits(); selectedNode = null; gDraw(); return; }
|
|
659
|
+
if (e.key !== 'Enter' || !searchHits.length) return;
|
|
660
|
+
e.preventDefault();
|
|
661
|
+
searchIx = (searchIx + 1) % searchHits.length;
|
|
662
|
+
var hit = searchHits[searchIx];
|
|
663
|
+
if (!expanded.has(hit.domain)) { expanded.add(hit.domain); gLayout(); syncToggle(); } // reveal, don't refuse
|
|
664
|
+
var nd = W.idx.get(hit.id);
|
|
665
|
+
if (nd) { centerOn(nd); selectedNode = hit.id; showNode(hit.id); }
|
|
666
|
+
gcountEl.textContent = (searchIx + 1) + '/' + searchHits.length + ' ' + hit.label;
|
|
667
|
+
gDraw();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// ---------- tab routing ----------
|
|
671
|
+
var rendered = {};
|
|
672
|
+
function activate(view) {
|
|
673
|
+
document.querySelectorAll('.tab[role="tab"]').forEach(function (t) { var on = t.getAttribute('data-view') === view; t.classList.toggle('on', on); t.setAttribute('aria-selected', String(on)); });
|
|
674
|
+
document.querySelectorAll('.view').forEach(function (v) { v.classList.toggle('on', v.id === 'view-' + view); });
|
|
675
|
+
if (view === 'findings' && !rendered.findings) { renderFindings(); rendered.findings = true; }
|
|
676
|
+
if (view === 'matrix' && !rendered.matrix) { renderMatrix(); rendered.matrix = true; }
|
|
677
|
+
if (view === 'treemap') { renderTreemap(); }
|
|
678
|
+
if (view === 'graph') { gResize(); if (!gInit) { gInit = true; syncToggle(); gLayout(); } else gDraw(); }
|
|
679
|
+
}
|
|
680
|
+
document.querySelectorAll('.tab[role="tab"]').forEach(function (t) { t.onclick = function () { activate(t.getAttribute('data-view')); }; });
|
|
681
|
+
window.addEventListener('resize', function () { if (document.getElementById('view-treemap').classList.contains('on')) renderTreemap(); if (document.getElementById('view-graph').classList.contains('on')) { gResize(); gDraw(); } });
|
|
682
|
+
|
|
683
|
+
// boot
|
|
684
|
+
renderFindings(); rendered.findings = true; detailEmpty();
|
|
685
|
+
|
|
686
|
+
// staging hook for scripts/screenshot.mjs — drives the report into a DEMONSTRATIVE state
|
|
687
|
+
// (top hotspot selected, its area expanded, neighbors lit). Not part of the UI.
|
|
688
|
+
window.__codewebStage = {
|
|
689
|
+
topHotspot: function () {
|
|
690
|
+
var top = hotspots[0]; if (!top) return null;
|
|
691
|
+
activate('graph'); gResize(); if (!gInit) { gInit = true; syncToggle(); gLayout(); }
|
|
692
|
+
if (!expanded.has(top.domain)) { expanded.add(top.domain); gLayout(); syncToggle(); }
|
|
693
|
+
var nd = W.idx.get(top.id);
|
|
694
|
+
if (nd) { selectedNode = top.id; cam.k = Math.max(cam.k, 1.2); centerOn(nd); showNode(top.id); gDraw(); }
|
|
695
|
+
return top.id;
|
|
696
|
+
},
|
|
697
|
+
// screen-space bounding box of the drawn graph (CSS px) — lets the screenshot pipeline crop
|
|
698
|
+
// to content instead of shipping empty canvas
|
|
699
|
+
graphBBox: function () {
|
|
700
|
+
if (!W || !W.nodes.length) return null;
|
|
701
|
+
var a = 1e9, b = 1e9, c = -1e9, d = -1e9, counted = 0;
|
|
702
|
+
W.nodes.forEach(function (n) {
|
|
703
|
+
if (lastLit && !lastLit.has(n.id)) return; // with a selection/search, crop to what's LIT
|
|
704
|
+
counted++;
|
|
705
|
+
a = Math.min(a, X(n.x) - n.r); b = Math.min(b, Y(n.y) - n.r);
|
|
706
|
+
c = Math.max(c, X(n.x) + n.r + 130); d = Math.max(d, Y(n.y) + n.r); // +130: room for the widest label
|
|
707
|
+
});
|
|
708
|
+
return counted ? { x: a, y: b, w: c - a, h: d - b } : null;
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// deep link: #s=<node id> selects the symbol on load (shareable findings)
|
|
713
|
+
(function () {
|
|
714
|
+
var m = /^#s=(.+)$/.exec(location.hash || '');
|
|
715
|
+
if (!m) return;
|
|
716
|
+
var id = decodeURIComponent(m[1]);
|
|
717
|
+
if (!nodeById.has(id)) return;
|
|
718
|
+
activate('graph');
|
|
719
|
+
// after the first layout pass, expand JUST the target's area, then center + select
|
|
720
|
+
setTimeout(function () {
|
|
721
|
+
var tn = nodeById.get(id);
|
|
722
|
+
if (tn && !expanded.has(tn.domain)) { expanded.add(tn.domain); gLayout(); syncToggle(); }
|
|
723
|
+
var nd = W && W.idx.get(id);
|
|
724
|
+
if (nd) { selectedNode = id; centerOn(nd); showNode(id); gDraw(); }
|
|
725
|
+
}, REDUCED ? 50 : 900);
|
|
726
|
+
})();
|
|
727
|
+
})();
|
|
728
|
+
</script>
|
|
729
|
+
</body>
|
|
730
|
+
</html>
|