@orangepro/orangepro-mcp 0.1.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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/local/agentWorkflow.js +81 -0
  4. package/dist/local/aiGraph/links.js +635 -0
  5. package/dist/local/analyze/analyzer.js +2129 -0
  6. package/dist/local/analyze/behaviorContracts.js +169 -0
  7. package/dist/local/analyze/boilerplate.js +42 -0
  8. package/dist/local/analyze/callGraph.js +458 -0
  9. package/dist/local/analyze/classify.js +219 -0
  10. package/dist/local/analyze/clustering.js +357 -0
  11. package/dist/local/analyze/confirm.js +2422 -0
  12. package/dist/local/analyze/coverage.js +518 -0
  13. package/dist/local/analyze/coverageArtifacts.js +607 -0
  14. package/dist/local/analyze/frameworks.js +115 -0
  15. package/dist/local/analyze/linkage/conventions.js +160 -0
  16. package/dist/local/analyze/parseCache.js +164 -0
  17. package/dist/local/analyze/selfAssert.js +53 -0
  18. package/dist/local/analyze/symbols.js +430 -0
  19. package/dist/local/analyze/testLayer.js +135 -0
  20. package/dist/local/analyze/treeSitter/engine.js +1253 -0
  21. package/dist/local/analyze/treeSitter/languages.js +101 -0
  22. package/dist/local/autoProve.js +620 -0
  23. package/dist/local/cli.js +1468 -0
  24. package/dist/local/cliArgs.js +112 -0
  25. package/dist/local/corpusScope.js +162 -0
  26. package/dist/local/enrich/csv.js +348 -0
  27. package/dist/local/enrich/index.js +43 -0
  28. package/dist/local/enrich/markdown.js +193 -0
  29. package/dist/local/explain/explain.js +91 -0
  30. package/dist/local/exportCli.js +26 -0
  31. package/dist/local/flows/flowWalker.js +215 -0
  32. package/dist/local/flows/llmFlowDiscovery.js +567 -0
  33. package/dist/local/freshness/changed.js +280 -0
  34. package/dist/local/freshness/manifest.js +35 -0
  35. package/dist/local/freshness/status.js +30 -0
  36. package/dist/local/gaps/gaps.js +114 -0
  37. package/dist/local/generate/buckets.js +73 -0
  38. package/dist/local/generate/compareJudge.js +124 -0
  39. package/dist/local/generate/compareReport.js +538 -0
  40. package/dist/local/generate/compareScore.js +105 -0
  41. package/dist/local/generate/deriveImports.js +91 -0
  42. package/dist/local/generate/generator.js +2586 -0
  43. package/dist/local/generate/prompt.js +144 -0
  44. package/dist/local/generate/promptV5.js +438 -0
  45. package/dist/local/generate/providers.js +400 -0
  46. package/dist/local/generate/runHints.js +304 -0
  47. package/dist/local/graph/citations.js +73 -0
  48. package/dist/local/graph/confirmable.js +72 -0
  49. package/dist/local/graph/factories.js +210 -0
  50. package/dist/local/graph/ontology.js +18 -0
  51. package/dist/local/interactive.js +53 -0
  52. package/dist/local/jobs/jobStore.js +80 -0
  53. package/dist/local/jobs/notify.js +29 -0
  54. package/dist/local/jobs/runner.js +75 -0
  55. package/dist/local/ledger.js +117 -0
  56. package/dist/local/localConfig.js +112 -0
  57. package/dist/local/mcp.js +548 -0
  58. package/dist/local/operations.js +1749 -0
  59. package/dist/local/pack/coverageReport.js +192 -0
  60. package/dist/local/pack/exporter.js +195 -0
  61. package/dist/local/pack/schema.js +128 -0
  62. package/dist/local/pack/summary.js +127 -0
  63. package/dist/local/pack/validate.js +25 -0
  64. package/dist/local/proofRunnability.js +366 -0
  65. package/dist/local/recipe/dbSqljs.js +255 -0
  66. package/dist/local/reprove/paths.js +13 -0
  67. package/dist/local/reprove/scoped.js +136 -0
  68. package/dist/local/resolve/barrelWalker.js +178 -0
  69. package/dist/local/resolve/exportIndex.js +270 -0
  70. package/dist/local/resolve/importGraph.js +347 -0
  71. package/dist/local/resolve/resolver.js +122 -0
  72. package/dist/local/resolve/resolverCache.js +117 -0
  73. package/dist/local/rtm.js +413 -0
  74. package/dist/local/score/coverage.js +99 -0
  75. package/dist/local/score/doctor.js +67 -0
  76. package/dist/local/score/risk.js +362 -0
  77. package/dist/local/score/score.js +182 -0
  78. package/dist/local/types.js +1 -0
  79. package/dist/local/util/hash.js +16 -0
  80. package/dist/local/util/ids.js +16 -0
  81. package/dist/local/util/progress.js +8 -0
  82. package/dist/local/util/redact.js +39 -0
  83. package/dist/local/util/time.js +1 -0
  84. package/dist/local/util/walk.js +174 -0
  85. package/dist/local/viz/behaviorReportData.js +367 -0
  86. package/dist/local/viz/behaviorReportHtml.js +664 -0
  87. package/dist/local/viz/d3.bundle.js +3 -0
  88. package/dist/local/viz/html.js +1152 -0
  89. package/dist/local/viz/payload.js +525 -0
  90. package/dist/local/workspace.js +99 -0
  91. package/docs/agent-workflow.md +167 -0
  92. package/docs/agents/claude-code.md +43 -0
  93. package/docs/agents/codex.md +52 -0
  94. package/docs/agents/cursor.md +39 -0
  95. package/docs/agents/opencode.md +43 -0
  96. package/docs/agents/vscode.md +34 -0
  97. package/docs/local-proof-kit.md +269 -0
  98. package/package.json +92 -0
  99. package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
  100. package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
  101. package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
  102. package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
  103. package/scripts/spikes/failure-summary.mjs +29 -0
@@ -0,0 +1,1152 @@
1
+ import { D3_SOURCE } from "./d3.bundle.js";
2
+ /**
3
+ * Render a self-contained, offline coverage-gap explorer ("Gap Zones").
4
+ *
5
+ * Three tabs:
6
+ * - Gap Zones — a D3 force layout where every code area is a hub in the centre
7
+ * lane and its sampled symbols are pulled into tier zones: covered (dynamic Proven +
8
+ * runtime) hug the centre, ASSOCIATED weak signals pull left, GAP (no test
9
+ * link) glow on the right. The right column IS the gap.
10
+ * - Overview — dynamic-Proven-share donut + a squarified treemap of areas (size = real
11
+ * code units, fill = tier mix).
12
+ * - Matrix — area × tier heat table + an area × language tier strip.
13
+ *
14
+ * No CDN, no network: D3 v7 is vendored inline and the gap-first DATA (the
15
+ * curated `payload.gap` view model) is injected as JSON. This is a LOCAL artifact
16
+ * (written to `.orangepro/graph.html`, never uploaded), so it intentionally
17
+ * includes real test/area names. It embeds NO raw source code, generated test
18
+ * bodies, prompts, secrets, or node `properties` beyond the curated gap view.
19
+ *
20
+ * TIERS ARE DISJOINT AND HONEST. "Proven" = dynamic targeted-proof ledger
21
+ * records only; static, runtime, associated, and AI-suggested signals NEVER move the proven %.
22
+ * The 4-tier breakdown (proven / runtime-covered / associated / no-link) is
23
+ * derived here straight from `payload.gap` — this renderer adds no scoring.
24
+ */
25
+ export function renderVizHtml(payload) {
26
+ const data = JSON.stringify(payload.gap).replace(/</g, "\\u003c");
27
+ // Inert, metadata-only provenance comment (generated-test bucket tally + counts).
28
+ // Carries no bodies/source — just trust-artifact aggregates for back-compat.
29
+ const meta = scriptSafe([
30
+ `confirmed=${payload.gap.stats.confirmed}/${payload.gap.stats.userflows}`,
31
+ `inferred=${payload.gap.stats.inferred}`,
32
+ `none=${payload.gap.stats.none}`,
33
+ `generated=${payload.meta.generated.count}`,
34
+ ...payload.meta.generated.byBucket.map((b) => `${b.bucket}=${b.count}`)
35
+ ].join(" ")).replace(/--/g, "—");
36
+ // Function replacements throughout: a STRING replacement interprets $-patterns
37
+ // ($&, $`, $', $$) inside the INSERTED text. D3's minified source contains
38
+ // literal $` sequences, and DATA / workspace names can contain $&/$'. A function
39
+ // replacement is inert: the returned string is inserted verbatim.
40
+ return HTML_TEMPLATE.replace(/__TITLE__/g, () => escapeHtml(payload.gap.workspace))
41
+ .replace("__META__", () => escapeHtml(meta))
42
+ .replace("__GENERATED__", () => escapeHtml(payload.meta.created_at || ""))
43
+ .replace("/*__D3__*/", () => scriptSafe(D3_SOURCE))
44
+ .replace('"__DATA__"', () => data)
45
+ .replace("/*__VIZ_LOGIC__*/", () => scriptSafe(VIZ_LOGIC));
46
+ }
47
+ function escapeHtml(s) {
48
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
49
+ }
50
+ /** Neutralize any `</script` so embedded JS can never close the host script tag. */
51
+ function scriptSafe(s) {
52
+ return s.replace(/<\/script/gi, "<\\/script").replace(/<!--/g, "<\\!--");
53
+ }
54
+ const VIZ_LOGIC = `
55
+ document.addEventListener('DOMContentLoaded', function() {
56
+ try { initViz(); }
57
+ catch(e) {
58
+ var lm = document.getElementById('loading-msg');
59
+ if (lm) { lm.style.display = 'block'; lm.textContent = 'Error loading visualization: ' + e.message; }
60
+ if (window.console) console.error(e);
61
+ }
62
+ });
63
+
64
+ function initViz() {
65
+ var D = DATA;
66
+ var $ = function(id){ return document.getElementById(id); };
67
+ function fmt(n){ return (Number(n) || 0).toLocaleString(); }
68
+ function esc(s){ return String(s == null ? '' : s).replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }
69
+
70
+ var lm = $('loading-msg'); if (lm) lm.style.display = 'none';
71
+ var appEl = $('app'); if (appEl) appEl.style.display = 'block';
72
+
73
+ // ---- derive the fields the view needs straight from payload.gap ----
74
+ var LT = (D.language_tiers || []).map(function(l){
75
+ return {
76
+ language: l.language,
77
+ total: Number(l.total) || 0,
78
+ proven: Number(l.proven) || 0,
79
+ runtime_covered: Number(l.runtime_covered) || 0,
80
+ associated: Number(l.associated) || 0,
81
+ unlinked: Number(l.unlinked) || 0,
82
+ proven_pct: Number(l.proven_pct) || 0,
83
+ dynamic_proof: (Number(l.proven) || 0) > 0
84
+ };
85
+ });
86
+ function lsum(f){ return LT.reduce(function(s,l){ return s + (Number(f(l)) || 0); }, 0); }
87
+ var TOTAL_PROVEN = lsum(function(l){ return l.proven; });
88
+ var TOTAL_RUNTIME = lsum(function(l){ return l.runtime_covered; });
89
+ var TOTAL_ASSOC = lsum(function(l){ return l.associated; });
90
+ var TOTAL_NONE = lsum(function(l){ return l.unlinked; });
91
+ var BEHAVIOR_TOTAL = (D.stats && Number(D.stats.behavior_total)) || (TOTAL_PROVEN + TOTAL_RUNTIME + TOTAL_ASSOC + TOTAL_NONE);
92
+ var PROVEN_PCT = (D.stats && Number(D.stats.behavior_confirmed_pct)) || 0;
93
+
94
+ // majority language per area (area_summary carries no language; code_behaviors do)
95
+ var areaLangCount = {};
96
+ (D.code_behaviors || []).forEach(function(b){
97
+ if (!b.area) return;
98
+ var m = areaLangCount[b.area] = areaLangCount[b.area] || {};
99
+ var L = b.language || '';
100
+ if (L) m[L] = (m[L] || 0) + 1;
101
+ });
102
+ function langOf(area){
103
+ var m = areaLangCount[area]; if (!m) return '';
104
+ var best = '', bn = -1;
105
+ for (var k in m){ if (m[k] > bn){ bn = m[k]; best = k; } }
106
+ return best;
107
+ }
108
+ // area rows, normalized to 4 disjoint tiers (confirmed=proven, runtime_covered,
109
+ // inferred=associated, none=no-link) + a derived language label.
110
+ var AREAS = (D.area_summary || []).map(function(a){
111
+ return {
112
+ area: a.area || 'core',
113
+ lang: langOf(a.area),
114
+ total: Number(a.total) || 0,
115
+ proven: Number(a.confirmed) || 0,
116
+ runtime: Number(a.runtime_covered) || 0,
117
+ associated: Number(a.inferred) || 0,
118
+ none: Number(a.none) || 0,
119
+ proven_pct: Number(a.confirmed_pct) || 0,
120
+ sample_gaps: Array.isArray(a.sample_gaps) ? a.sample_gaps : []
121
+ };
122
+ });
123
+ var areaByName = {};
124
+ AREAS.forEach(function(a){ areaByName[a.area] = a; });
125
+ function areaMatchesLang(a, langName){ return !langName || a.lang === langName; }
126
+
127
+ // ---- header / headline ----
128
+ var hp = $('hl-pct'); if (hp) hp.textContent = PROVEN_PCT.toFixed(2) + '%';
129
+ var hd = $('hl-denom'); if (hd) hd.textContent = fmt(BEHAVIOR_TOTAL);
130
+ var kP = $('kpi-proven'); if (kP) kP.textContent = fmt(TOTAL_PROVEN);
131
+ var kR = $('kpi-runtime'); if (kR) kR.textContent = fmt(TOTAL_RUNTIME);
132
+ var kA = $('kpi-assoc'); if (kA) kA.textContent = fmt(TOTAL_ASSOC);
133
+ var kN = $('kpi-none'); if (kN) kN.textContent = fmt(TOTAL_NONE);
134
+ var wsn = $('ws-name'); if (wsn) wsn.textContent = D.workspace || '';
135
+ var ftw = $('ft-ws'); if (ftw) ftw.textContent = D.workspace || '';
136
+ var ftd = $('ft-date'); if (ftd) ftd.textContent = (document.body.getAttribute('data-generated') || '').slice(0, 10);
137
+
138
+ // ---- partial-scan banner (a budget-stopped run must never read as complete) ----
139
+ if (D.partial_scan && $('partial-banner')) {
140
+ $('partial-banner').classList.add('show');
141
+ var ps = (D.partial_scan && typeof D.partial_scan === 'object') ? D.partial_scan : {};
142
+ var pf = (typeof D.files_not_analyzed === 'number') ? D.files_not_analyzed : (ps.files_not_analyzed || 0);
143
+ var bt = $('banner-text');
144
+ if (bt) bt.textContent = 'PARTIAL SCAN — ' + fmt(pf) + ' file(s) NOT analyzed'
145
+ + (ps.budget_ms ? ' (budget ' + Math.round(ps.budget_ms/1000) + 's)' : '')
146
+ + '. The coverage below is a FLOOR over a partial scan. Raise ORANGEPRO_MAX_ANALYZE_MS or scope with --base.';
147
+ }
148
+ // ---- hard-proof-limited banner (full proof pass exceeded the budget) ----
149
+ var proofLimited = D.proof_limited;
150
+ var proofBanner = $('proof-banner');
151
+ if (proofLimited && proofBanner) {
152
+ proofBanner.style.display = 'block';
153
+ if (proofLimited.scoped_by_risk) {
154
+ proofBanner.textContent = 'HARD PROOF SCOPED — this full scan had '
155
+ + fmt(proofLimited.skipped_files_budget)
156
+ + ' confirmable files, above the confirmer budget. OrangePro ran a risk-ranked subset ('
157
+ + fmt(proofLimited.scoped_by_risk.candidate_pairs)
158
+ + ' candidate pair(s)); Proven is still a conservative lower bound. Associated links are not proof. Use PR scope (--base <ref>) or raise ORANGEPRO_MAX_CONFIRM_FILES for the whole repo.';
159
+ } else {
160
+ proofBanner.textContent = 'HARD PROOF SKIPPED — this full scan exceeded the confirmer budget ('
161
+ + fmt(proofLimited.skipped_files_budget)
162
+ + ' files). Proven coverage is a conservative lower bound because that proof pass did not run. Associated links are still shown, but they are not proof. Use PR scope (--base <ref>) or raise ORANGEPRO_MAX_CONFIRM_FILES.';
163
+ }
164
+ }
165
+
166
+ // ===================================================================
167
+ // STATE + TABS
168
+ // ===================================================================
169
+ var state = { tiers: { proven:true, runtime:true, associated:true, none:true }, area:null, lang:'', search:'' };
170
+
171
+ var langSel = $('lang-filter');
172
+ if (langSel) LT.forEach(function(l){
173
+ var o = document.createElement('option'); o.value = l.language; o.textContent = l.language; langSel.appendChild(o);
174
+ });
175
+
176
+ var tabs = Array.prototype.slice.call(document.querySelectorAll('[role="tab"]'));
177
+ function selectTab(tab){
178
+ tabs.forEach(function(t){
179
+ var on = t === tab;
180
+ t.setAttribute('aria-selected', on ? 'true':'false');
181
+ t.tabIndex = on ? 0 : -1;
182
+ var pane = $(t.getAttribute('aria-controls'));
183
+ if (pane) pane.classList.toggle('active', on);
184
+ });
185
+ tab.focus();
186
+ if (tab.id === 'tab-zones') setTimeout(layoutForce, 0);
187
+ if (tab.id === 'tab-overview') drawOverview();
188
+ if (tab.id === 'tab-matrix') drawMatrix();
189
+ }
190
+ tabs.forEach(function(tab, i){
191
+ tab.addEventListener('click', function(){ selectTab(tab); });
192
+ tab.addEventListener('keydown', function(e){
193
+ if (e.key === 'ArrowRight'){ selectTab(tabs[(i+1)%tabs.length]); e.preventDefault(); }
194
+ else if (e.key === 'ArrowLeft'){ selectTab(tabs[(i-1+tabs.length)%tabs.length]); e.preventDefault(); }
195
+ else if (e.key === 'Home'){ selectTab(tabs[0]); e.preventDefault(); }
196
+ else if (e.key === 'End'){ selectTab(tabs[tabs.length-1]); e.preventDefault(); }
197
+ });
198
+ });
199
+
200
+ // ===================================================================
201
+ // TOOLTIP
202
+ // ===================================================================
203
+ var tip = $('tip');
204
+ function showTip(html, x, y){
205
+ if (!tip) return;
206
+ tip.innerHTML = html; tip.classList.add('show'); tip.setAttribute('aria-hidden','false');
207
+ var w = tip.offsetWidth, h = tip.offsetHeight;
208
+ var px = Math.min((x||0) + 14, window.innerWidth - w - 10);
209
+ var py = Math.min((y||0) + 14, window.innerHeight - h - 10);
210
+ if (py < 8) py = 8;
211
+ tip.style.left = px + 'px'; tip.style.top = py + 'px';
212
+ }
213
+ function hideTip(){ if (tip){ tip.classList.remove('show'); tip.setAttribute('aria-hidden','true'); } }
214
+
215
+ var TIER = {
216
+ proven: { color:'var(--proven)', label:'Dynamic Proven' },
217
+ runtime: { color:'var(--runtime)', label:'Runtime-covered' },
218
+ associated: { color:'var(--assoc)', label:'Associated' },
219
+ none: { color:'var(--none)', label:'No link' }
220
+ };
221
+
222
+ // ===================================================================
223
+ // DRILL PANEL (shared across tabs)
224
+ // ===================================================================
225
+ function glyphSVG(status){
226
+ if (status === 'associated')
227
+ return '<svg class="gly" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="5" fill="none" stroke="var(--assoc)" stroke-width="2.4"/></svg>';
228
+ if (status === 'proven' || status === 'runtime')
229
+ return '<svg class="gly" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="5.2" fill="' + (status === 'runtime' ? 'var(--runtime)' : 'var(--proven)') + '"/></svg>';
230
+ return '<svg class="gly" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="4.4" fill="none" stroke="var(--none)" stroke-width="2.2"/><circle cx="8" cy="8" r="7" fill="none" stroke="var(--none)" stroke-width="1" opacity=".55"/></svg>';
231
+ }
232
+ function openDrillForArea(areaName){
233
+ var a = areaByName[areaName]; if (!a) return;
234
+ $('drillEmpty').hidden = true; $('drillContent').hidden = false;
235
+ $('drillTitle').textContent = a.area;
236
+ $('drillSub').innerHTML = esc(a.lang) + ' &middot; <span class="num">' + fmt(a.total) + '</span> real-code units &middot; <span class="num">' + a.proven_pct.toFixed(2) + '%</span> proven';
237
+ $('dm-proven').textContent = fmt(a.proven);
238
+ $('dm-runtime').textContent = fmt(a.runtime);
239
+ $('dm-assoc').textContent = fmt(a.associated);
240
+ $('dm-none').textContent = fmt(a.none);
241
+ var list = $('gaplist'); list.innerHTML = '';
242
+ (a.sample_gaps || []).slice().sort(function(p,q){
243
+ var r = (p.status==='none'?0:1) - (q.status==='none'?0:1);
244
+ if (r) return r;
245
+ return (p.file||'').localeCompare(q.file||'') || (p.title||'').localeCompare(q.title||'');
246
+ }).forEach(function(g){
247
+ var row = document.createElement('div'); row.className = 'gaprow';
248
+ var col = g.status === 'associated' ? 'var(--assoc)' : 'var(--none)';
249
+ row.innerHTML =
250
+ '<div class="top">' + glyphSVG(g.status) +
251
+ '<span class="gt" title="' + esc(g.title) + '">' + esc(g.title) + '</span></div>' +
252
+ '<div class="gf" title="' + esc(g.file) + '">' + esc(g.file) + '</div>' +
253
+ '<div class="gtag" style="color:' + col + '">' + (g.status === 'associated' ? 'associated test link, not counted as proven' : 'no test link') + '</div>';
254
+ list.appendChild(row);
255
+ });
256
+ if (!(a.sample_gaps || []).length)
257
+ list.innerHTML = '<div style="color:var(--ink-3);font-size:12px;padding:10px 2px">No sampled uncovered symbols for this area.</div>';
258
+ }
259
+
260
+ // ===================================================================
261
+ // GAP ZONES — D3 force layout (covered centre / associated left / gap right)
262
+ // ===================================================================
263
+ var svg = $('forceSvg');
264
+ var VW = 1000, VH = 560;
265
+ var zonesLayer = $('zonesLayer'), linksLayer = $('linksLayer'), hubsLayer = $('hubsLayer'),
266
+ dotsLayer = $('dotsLayer'), viewport = $('viewport');
267
+ var SVGNS = 'http://www.w3.org/2000/svg';
268
+ var nodes = [], links = [], sim = null;
269
+ var CAP_PER_AREA = 40;
270
+ var totalSymbolsShown = 0;
271
+ var CENTER_X = VW * 0.5; // covered (proven + runtime) lane
272
+ var SIDE = 200; // horizontal pull: associated left, gap right
273
+
274
+ function buildModel(){
275
+ nodes = []; links = []; totalSymbolsShown = 0;
276
+ var areas = AREAS.slice();
277
+ var n = areas.length, topPad = 74, botPad = 50;
278
+ areas.forEach(function(a, i){
279
+ var t = n > 1 ? i/(n-1) : 0.5;
280
+ var rowY = topPad + t*(VH - topPad - botPad);
281
+ var size = Math.max(6, Math.min(12, 5 + Math.sqrt(a.total)/4.6));
282
+ var hub = { id:'hub:'+a.area, kind:'hub', area:a.area, lang:a.lang, x:CENTER_X, y:rowY,
283
+ fx:CENTER_X, fy:rowY, pinX:CENTER_X, pinY:rowY, r:size, data:a };
284
+ nodes.push(hub);
285
+ var tiers = [
286
+ { key:'proven', n:a.proven }, { key:'runtime', n:a.runtime },
287
+ { key:'associated', n:a.associated }, { key:'none', n:a.none }
288
+ ];
289
+ var areaTotal = a.total || 1;
290
+ var alloc = tiers.map(function(t2){
291
+ return { key:t2.key, n:t2.n, want: t2.n > 0 ? Math.max(1, Math.round(t2.n/areaTotal*CAP_PER_AREA)) : 0 };
292
+ });
293
+ var sum = alloc.reduce(function(s,x){ return s + x.want; }, 0);
294
+ while (sum > CAP_PER_AREA){
295
+ alloc.sort(function(p,q){ return q.want - p.want; });
296
+ for (var k=0; k<alloc.length && sum>CAP_PER_AREA; k++){ if (alloc[k].want > 1){ alloc[k].want--; sum--; } }
297
+ }
298
+ alloc.forEach(function(al){
299
+ for (var d=0; d<al.want; d++){
300
+ totalSymbolsShown++;
301
+ // covered (proven/runtime) hug centre; associated pull left; gap (none) push right.
302
+ var side = al.key === 'associated' ? -1 : al.key === 'none' ? 1 : 0;
303
+ var targetX = CENTER_X + side*SIDE;
304
+ var dot = { id:'dot:'+a.area+':'+al.key+':'+d, kind:'dot', area:a.area, lang:a.lang, tier:al.key,
305
+ side:side, targetX:targetX, hub:hub,
306
+ x: targetX + (Math.random()-0.5)*70, y: rowY + (Math.random()-0.5)*46,
307
+ r: (al.key === 'proven' || al.key === 'runtime') ? 4.4 + Math.random()*1.6 : 3.6 + Math.random()*2.2 };
308
+ links.push({ source:hub, target:dot, tier:al.key });
309
+ }
310
+ });
311
+ });
312
+ }
313
+
314
+ function dotMatchesFilters(dt){
315
+ if (dt.kind !== 'dot') return true;
316
+ if (!state.tiers[dt.tier]) return false;
317
+ if (state.area && dt.area !== state.area) return false;
318
+ if (state.lang && !areaMatchesLang(areaByName[dt.area] || {}, state.lang)) return false;
319
+ if (state.search && dt.area.toLowerCase().indexOf(state.search.toLowerCase()) === -1) return false;
320
+ return true;
321
+ }
322
+
323
+ function dragBehavior(node){
324
+ return d3.drag()
325
+ .on('start', function(e){ if (!e.active && sim) sim.alphaTarget(0.2).restart(); node.fx = node.x; node.fy = node.y; })
326
+ .on('drag', function(e){ node.fx = e.x; node.fy = e.y; })
327
+ .on('end', function(e){ if (!e.active && sim) sim.alphaTarget(0); if (node.kind === 'dot'){ node.fx = null; node.fy = null; } });
328
+ }
329
+
330
+ function drawGraph(){
331
+ if (!svg) return;
332
+ zonesLayer.innerHTML = ''; linksLayer.innerHTML = ''; hubsLayer.innerHTML = ''; dotsLayer.innerHTML = '';
333
+ // zone bands: associated (left) / covered (centre) / gap (right)
334
+ var zones = [
335
+ { x0:0, x1:0.375, tint:'rgba(245,158,11,.05)', op:'0.42', label:'ASSOCIATED', sub:'weak signal · not proof' },
336
+ { x0:0.375, x1:0.625, tint:'rgba(234,88,12,.05)', op:'0.42', label:'CODE · COVERED', sub:'proven + runtime' },
337
+ { x0:0.625, x1:1, tint:'rgba(244,63,94,.07)', op:'0.7', label:'GAP ZONE · NO TEST LINK', sub:'real code, no test signal' }
338
+ ];
339
+ zones.forEach(function(z){
340
+ var rect = document.createElementNS(SVGNS,'rect');
341
+ rect.setAttribute('x', (z.x0*VW)); rect.setAttribute('y', 0);
342
+ rect.setAttribute('width', (z.x1-z.x0)*VW); rect.setAttribute('height', VH);
343
+ rect.setAttribute('fill', z.tint); rect.setAttribute('opacity', z.op);
344
+ zonesLayer.appendChild(rect);
345
+ var lab = document.createElementNS(SVGNS,'text');
346
+ lab.setAttribute('class','zoneLabel'); lab.setAttribute('x', (z.x0+ (z.x1-z.x0)/2)*VW); lab.setAttribute('y', 26);
347
+ lab.setAttribute('text-anchor','middle'); lab.setAttribute('fill','var(--ink-3)');
348
+ lab.textContent = z.label; zonesLayer.appendChild(lab);
349
+ var sub = document.createElementNS(SVGNS,'text');
350
+ sub.setAttribute('class','zoneSub'); sub.setAttribute('x', (z.x0+ (z.x1-z.x0)/2)*VW); sub.setAttribute('y', 40);
351
+ sub.setAttribute('text-anchor','middle'); sub.setAttribute('fill','var(--ink-3)');
352
+ sub.textContent = z.sub; zonesLayer.appendChild(sub);
353
+ });
354
+
355
+ // links (hub -> symbol)
356
+ links.forEach(function(ln){
357
+ var el = document.createElementNS(SVGNS,'line');
358
+ el.setAttribute('stroke', TIER[ln.tier].color);
359
+ el.setAttribute('stroke-opacity', (ln.tier === 'proven' || ln.tier === 'runtime') ? 0.42 : 0.16);
360
+ el.setAttribute('stroke-width', 0.8);
361
+ ln._el = el; linksLayer.appendChild(el);
362
+ });
363
+
364
+ // hubs (one per area, centre column)
365
+ nodes.filter(function(n){ return n.kind === 'hub'; }).forEach(function(h){
366
+ var g = document.createElementNS(SVGNS,'g');
367
+ g.setAttribute('class','node-focus'); g.setAttribute('tabindex','0');
368
+ g.setAttribute('role','button'); g.setAttribute('aria-label', h.area + ' code area');
369
+ var c = document.createElementNS(SVGNS,'circle');
370
+ c.setAttribute('r', h.r); c.setAttribute('fill','var(--accent)');
371
+ c.setAttribute('stroke','var(--bg)'); c.setAttribute('stroke-width','1.5');
372
+ c.setAttribute('filter','url(#hubGlow)'); g.appendChild(c);
373
+ var lbl = document.createElementNS(SVGNS,'text');
374
+ lbl.setAttribute('class','hub-label'); lbl.setAttribute('text-anchor','middle');
375
+ lbl.setAttribute('dy', h.r + 12); lbl.textContent = h.area; g.appendChild(lbl);
376
+ h._g = g; hubsLayer.appendChild(g);
377
+ function hubTip(e){
378
+ var a = h.data;
379
+ showTip('<div class="tt">' + esc(h.area) + '</div>' +
380
+ '<div class="trow"><span>real-code units</span><span class="v">' + fmt(a.total) + '</span></div>' +
381
+ '<div class="trow"><span style="color:var(--proven)">Dynamic Proven</span><span class="v">' + fmt(a.proven) + '</span></div>' +
382
+ '<div class="trow"><span style="color:var(--runtime)">Runtime</span><span class="v">' + fmt(a.runtime) + '</span></div>' +
383
+ '<div class="trow"><span style="color:var(--assoc)">Associated</span><span class="v">' + fmt(a.associated) + '</span></div>' +
384
+ '<div class="trow"><span style="color:var(--none)">No link</span><span class="v">' + fmt(a.none) + '</span></div>',
385
+ (e.clientX||0),(e.clientY||0));
386
+ }
387
+ g.addEventListener('mouseenter', hubTip); g.addEventListener('mousemove', hubTip); g.addEventListener('mouseleave', hideTip);
388
+ g.addEventListener('click', function(){ setArea(h.area, true); });
389
+ g.addEventListener('keydown', function(e){ if (e.key==='Enter'||e.key===' '){ e.preventDefault(); setArea(h.area, true); } });
390
+ d3.select(g).call(dragBehavior(h));
391
+ });
392
+
393
+ // dots (sampled symbols)
394
+ nodes.filter(function(n){ return n.kind === 'dot'; }).forEach(function(dt){
395
+ var g = document.createElementNS(SVGNS,'g'); g.setAttribute('class','node-focus');
396
+ g.style.display = dotMatchesFilters(dt) ? '' : 'none';
397
+ var col = TIER[dt.tier].color;
398
+ if (dt.tier === 'proven' || dt.tier === 'runtime'){
399
+ var c = document.createElementNS(SVGNS,'circle');
400
+ c.setAttribute('r', dt.r); c.setAttribute('fill', col); c.setAttribute('fill-opacity','0.95');
401
+ g.appendChild(c);
402
+ } else if (dt.tier === 'associated'){
403
+ var c2 = document.createElementNS(SVGNS,'circle');
404
+ c2.setAttribute('r', dt.r); c2.setAttribute('fill','none'); c2.setAttribute('stroke', col); c2.setAttribute('stroke-width','1.8');
405
+ g.appendChild(c2);
406
+ } else {
407
+ var c3 = document.createElementNS(SVGNS,'circle');
408
+ c3.setAttribute('r', dt.r); c3.setAttribute('fill','none'); c3.setAttribute('stroke', col);
409
+ c3.setAttribute('stroke-width','1.9'); c3.setAttribute('filter','url(#redGlow)'); g.appendChild(c3);
410
+ var c3b = document.createElementNS(SVGNS,'circle');
411
+ c3b.setAttribute('r', dt.r+2.6); c3b.setAttribute('fill','none'); c3b.setAttribute('stroke', col);
412
+ c3b.setAttribute('stroke-width','0.8'); c3b.setAttribute('stroke-opacity','0.5'); g.appendChild(c3b);
413
+ }
414
+ dt._g = g; dotsLayer.appendChild(g);
415
+ function dotTip(e){
416
+ var label = (dt.tier === 'proven') ? 'Proven — dynamic targeted proof'
417
+ : (dt.tier === 'runtime') ? 'Runtime-covered — executed by a coverage report, not proof'
418
+ : (dt.tier === 'associated') ? 'Associated — name/path/import/structural matching only, not proof'
419
+ : 'No link — no static or runtime signal';
420
+ showTip('<div class="tt">' + esc(dt.area) + ' &middot; symbol</div>' +
421
+ '<div class="trow"><span>tier</span><span class="v" style="color:' + col + '">' + TIER[dt.tier].label + '</span></div>' +
422
+ '<div style="margin-top:6px;color:var(--ink-3);font-size:11px">' + label + '</div>',
423
+ (e.clientX||0),(e.clientY||0));
424
+ }
425
+ g.addEventListener('mouseenter', dotTip); g.addEventListener('mousemove', dotTip); g.addEventListener('mouseleave', hideTip);
426
+ g.addEventListener('click', function(){ setArea(dt.area, true); });
427
+ d3.select(g).call(dragBehavior(dt));
428
+ });
429
+ }
430
+
431
+ function ticked(){
432
+ links.forEach(function(ln){
433
+ if (!ln._el) return;
434
+ ln._el.setAttribute('x1', ln.source.x); ln._el.setAttribute('y1', ln.source.y);
435
+ ln._el.setAttribute('x2', ln.target.x); ln._el.setAttribute('y2', ln.target.y);
436
+ });
437
+ nodes.forEach(function(nd){ if (nd._g) nd._g.setAttribute('transform', 'translate(' + nd.x + ',' + nd.y + ')'); });
438
+ }
439
+ function startSim(){
440
+ if (!nodes.length) return;
441
+ sim = d3.forceSimulation(nodes)
442
+ .force('x', d3.forceX(function(d){ return d.kind === 'hub' ? d.pinX : d.targetX; }).strength(function(d){ return d.kind === 'hub' ? 1 : 0.2; }))
443
+ .force('y', d3.forceY(function(d){ return d.kind === 'hub' ? d.pinY : (d.hub ? d.hub.y : VH/2); }).strength(function(d){ return d.kind === 'hub' ? 1 : 0.12; }))
444
+ .force('collide', d3.forceCollide(function(d){ return (d.r || 4) + 1.4; }))
445
+ .force('link', d3.forceLink(links).id(function(d){ return d.id; }).distance(46).strength(0.02))
446
+ .on('tick', ticked);
447
+ for (var ti=0; ti<260; ti++) sim.tick();
448
+ ticked();
449
+ sim.alpha(0.4).restart();
450
+ }
451
+
452
+ // ---- zoom / pan (D3) — pinned to the viewBox so content never leaves view ----
453
+ if (svg) {
454
+ var ZB_EXTENT = [[0,0],[VW,VH]];
455
+ var ZB_CENTER = [VW/2, VH/2];
456
+ var zoomB = d3.zoom().scaleExtent([1,6]).extent(ZB_EXTENT).translateExtent(ZB_EXTENT)
457
+ .filter(function(event){ return event.type === 'wheel' ? true : !(event.target.closest && event.target.closest('.node-focus')); })
458
+ .on('zoom', function(event){ viewport.setAttribute('transform', event.transform.toString()); });
459
+ var svgSel = d3.select(svg).call(zoomB);
460
+ svgSel.on('dblclick.zoom', null);
461
+ if ($('zoom-in')) $('zoom-in').addEventListener('click', function(){ svgSel.transition().duration(200).call(zoomB.scaleBy, 1.25, ZB_CENTER); });
462
+ if ($('zoom-out')) $('zoom-out').addEventListener('click', function(){ svgSel.transition().duration(200).call(zoomB.scaleBy, 1/1.25, ZB_CENTER); });
463
+ if ($('zoom-reset')) $('zoom-reset').addEventListener('click', function(){ svgSel.transition().duration(250).call(zoomB.transform, d3.zoomIdentity); });
464
+ }
465
+
466
+ function layoutForce(){
467
+ if (!nodes.length){ buildModel(); drawGraph(); startSim(); }
468
+ else if (sim){ sim.alpha(0.5).restart(); }
469
+ updateSampleNote();
470
+ }
471
+ function updateSampleNote(){
472
+ var sn = $('sample-note');
473
+ if (sn) sn.textContent = 'showing ' + fmt(totalSymbolsShown) + ' of ' + fmt(BEHAVIOR_TOTAL) + ' symbols (capped ' + CAP_PER_AREA + '/area, proportional)';
474
+ }
475
+
476
+ // ===================================================================
477
+ // AREA SELECT (cross-tab highlight) + filters
478
+ // ===================================================================
479
+ function setArea(areaName, openDrill){
480
+ state.area = (state.area === areaName) ? null : areaName;
481
+ syncAreaButtons(); refreshDotVisibility();
482
+ if (state.area){ openDrillForArea(state.area); var sw = $('drillSw'); if (sw) sw.style.background = 'var(--accent)'; }
483
+ highlightMatrix(); highlightTreemap();
484
+ }
485
+ function syncAreaButtons(){
486
+ Array.prototype.forEach.call(document.querySelectorAll('.areabtn'), function(b){
487
+ b.setAttribute('aria-pressed', b.dataset.area === state.area ? 'true':'false');
488
+ });
489
+ }
490
+ (function(){
491
+ var wrap = $('areafilters'); if (!wrap) return;
492
+ AREAS.forEach(function(a){
493
+ var b = document.createElement('button'); b.className = 'areabtn'; b.dataset.area = a.area;
494
+ b.setAttribute('aria-pressed','false');
495
+ b.innerHTML = '<span class="sw"></span>' + esc(a.area) + '<span class="ct num">' + fmt(a.total) + '</span>';
496
+ b.addEventListener('click', function(){ setArea(a.area, true); });
497
+ wrap.appendChild(b);
498
+ });
499
+ })();
500
+ function refreshDotVisibility(){
501
+ nodes.forEach(function(nd){
502
+ if (nd.kind === 'dot' && nd._g) nd._g.style.display = dotMatchesFilters(nd) ? '' : 'none';
503
+ if (nd.kind === 'hub' && nd._g){
504
+ var keep = true;
505
+ if (state.area && nd.area !== state.area) keep = false;
506
+ if (state.search && nd.area.toLowerCase().indexOf(state.search.toLowerCase()) === -1) keep = false;
507
+ if (state.lang && !areaMatchesLang(nd.data, state.lang)) keep = false;
508
+ nd._g.style.opacity = keep ? '1' : '0.18';
509
+ }
510
+ });
511
+ var focused = !!(state.area || state.search || state.lang);
512
+ links.forEach(function(ln){
513
+ if (!ln._el) return;
514
+ var vis = dotMatchesFilters(ln.target);
515
+ ln._el.style.display = vis ? '' : 'none';
516
+ if (vis){
517
+ var base = (ln.tier === 'proven' || ln.tier === 'runtime') ? 0.42 : 0.16;
518
+ ln._el.setAttribute('stroke-opacity', focused ? Math.min(0.62, base + 0.24) : base);
519
+ }
520
+ });
521
+ }
522
+ Array.prototype.forEach.call(document.querySelectorAll('.chip[data-tier]'), function(chip){
523
+ chip.addEventListener('click', function(){
524
+ var key = chip.dataset.tier, on = chip.getAttribute('aria-pressed') === 'true';
525
+ chip.setAttribute('aria-pressed', on ? 'false':'true');
526
+ state.tiers[key] = !on;
527
+ refreshDotVisibility(); drawOverview(); drawMatrix();
528
+ });
529
+ });
530
+ var searchTimer = null;
531
+ if ($('search')) $('search').addEventListener('input', function(e){
532
+ state.search = e.target.value.trim();
533
+ clearTimeout(searchTimer);
534
+ searchTimer = setTimeout(function(){ refreshDotVisibility(); drawMatrix(); highlightTreemap(); }, 120);
535
+ });
536
+ if (langSel) langSel.addEventListener('change', function(e){ state.lang = e.target.value; refreshDotVisibility(); drawMatrix(); drawOverview(); });
537
+
538
+ // ===================================================================
539
+ // OVERVIEW TAB (donut + split bar + treemap)
540
+ // ===================================================================
541
+ function drawOverview(){
542
+ var r = 68, circ = 2*Math.PI*r;
543
+ var arc = Math.max(circ*PROVEN_PCT/100, 2.5);
544
+ if ($('donutArc')) $('donutArc').setAttribute('stroke-dasharray', arc.toFixed(1) + ' ' + circ.toFixed(1));
545
+ if ($('donutPct')) $('donutPct').textContent = PROVEN_PCT.toFixed(2) + '%';
546
+ var denom = (TOTAL_PROVEN + TOTAL_RUNTIME + TOTAL_ASSOC + TOTAL_NONE) || 1;
547
+ var track = $('splitTrack');
548
+ if (track){
549
+ track.innerHTML = '';
550
+ [ { key:'proven', n:TOTAL_PROVEN, col:'var(--proven)' },
551
+ { key:'runtime', n:TOTAL_RUNTIME, col:'var(--runtime)' },
552
+ { key:'associated', n:TOTAL_ASSOC, col:'var(--assoc)' },
553
+ { key:'none', n:TOTAL_NONE, col:'var(--none)' } ].forEach(function(s){
554
+ var w = s.n/denom*100, el = document.createElement('div'); el.className = 'seg';
555
+ el.style.width = w.toFixed(2) + '%';
556
+ el.style.background = state.tiers[s.key] ? s.col : 'var(--panel-3)';
557
+ el.style.opacity = state.tiers[s.key] ? '1' : '0.5';
558
+ el.style.color = (s.key==='associated' || s.key==='runtime') ? '#06210f' : '#06210f';
559
+ el.title = s.key + ': ' + fmt(s.n);
560
+ el.textContent = w >= 6 ? fmt(s.n) : '';
561
+ track.appendChild(el);
562
+ });
563
+ }
564
+ if ($('sb-proven')) $('sb-proven').textContent = fmt(TOTAL_PROVEN);
565
+ if ($('sb-runtime')) $('sb-runtime').textContent = fmt(TOTAL_RUNTIME);
566
+ if ($('sb-assoc')) $('sb-assoc').textContent = fmt(TOTAL_ASSOC);
567
+ if ($('sb-none')) $('sb-none').textContent = fmt(TOTAL_NONE);
568
+ drawTreemap();
569
+ }
570
+ function drawTreemap(){
571
+ var host = $('treemap'); if (!host) return;
572
+ host.innerHTML = '';
573
+ var W = host.clientWidth || 640, H = 360; host.style.height = H + 'px';
574
+ var areas = AREAS.filter(function(a){
575
+ if (state.lang && !areaMatchesLang(a, state.lang)) return false;
576
+ if (state.search && a.area.toLowerCase().indexOf(state.search.toLowerCase()) === -1) return false;
577
+ return a.total > 0;
578
+ }).slice().sort(function(p,q){ return q.total - p.total; });
579
+ if (!areas.length){ host.innerHTML = '<div class="empty" style="position:static"><div>No areas match the current filters.</div></div>'; return; }
580
+ var total = areas.reduce(function(s,a){ return s + a.total; }, 0) || 1;
581
+ function worst(row, len){
582
+ var max=-Infinity, min=Infinity, sum=0;
583
+ row.forEach(function(rr){ sum+=rr.val; max=Math.max(max,rr.val); min=Math.min(min,rr.val); });
584
+ var s2 = sum*sum, len2 = len*len;
585
+ return Math.max(len2*max/s2, s2/(len2*min));
586
+ }
587
+ function layoutRow(row, horizontal, ox, oy, ow, oh){
588
+ var sum = row.reduce(function(s,rr){ return s+rr.val; }, 0), off = 0;
589
+ row.forEach(function(rr){
590
+ var frac = rr.val/sum, tx,ty,tw,th;
591
+ if (horizontal){ tw=ow; th=oh*frac; tx=ox; ty=oy+off; off+=th; }
592
+ else { tw=ow*frac; th=oh; tx=ox+off; ty=oy; off+=tw; }
593
+ drawTile(rr.a, tx, ty, tw, th);
594
+ });
595
+ }
596
+ var areaScale = (W*H)/total;
597
+ var remaining = areas.map(function(a){ return { a:a, val:a.total*areaScale }; });
598
+ var cx=0, cy=0, cw=W, ch=H;
599
+ while (remaining.length){
600
+ var horizontal = cw >= ch ? false : true;
601
+ var row = [remaining[0]], shortest = horizontal ? cw : ch, i=1;
602
+ while (i < remaining.length){
603
+ var test = row.concat([remaining[i]]);
604
+ if (worst(row, shortest) >= worst(test, shortest)){ row = test; i++; } else break;
605
+ }
606
+ var rowVal = row.reduce(function(s,rr){ return s+rr.val; }, 0), thick = rowVal/shortest;
607
+ if (horizontal){ layoutRow(row, false, cx, cy, cw, thick); cy += thick; ch -= thick; }
608
+ else { layoutRow(row, true, cx, cy, thick, ch); cx += thick; cw -= thick; }
609
+ remaining = remaining.slice(row.length);
610
+ }
611
+ function drawTile(a, x, y, w, h){
612
+ var el = document.createElement('button'); el.className = 'tile';
613
+ el.style.left = (x+1)+'px'; el.style.top = (y+1)+'px';
614
+ el.style.width = Math.max(0,w-2)+'px'; el.style.height = Math.max(0,h-2)+'px';
615
+ var tot = a.total || 1, pNone = a.none/tot, pAssoc = a.associated/tot;
616
+ el.style.background = pNone > pAssoc
617
+ ? 'linear-gradient(135deg, rgba(244,63,94,' + (0.22+pNone*0.45).toFixed(2) + '), rgba(190,18,60,0.18))'
618
+ : 'linear-gradient(135deg, rgba(245,158,11,' + (0.22+pAssoc*0.4).toFixed(2) + '), rgba(180,83,9,0.18))';
619
+ el.setAttribute('aria-label', a.area + ', ' + fmt(a.total) + ' units, ' + a.proven_pct.toFixed(2) + '% proven');
620
+ var showText = (w > 54 && h > 34);
621
+ el.innerHTML = (showText ? '<div class="tn">' + esc(a.area) + '</div>' : '') +
622
+ (showText ? '<div><div class="tm">' +
623
+ '<span style="width:' + (a.proven/tot*100) + '%;background:var(--proven)"></span>' +
624
+ '<span style="width:' + (a.runtime/tot*100) + '%;background:var(--runtime)"></span>' +
625
+ '<span style="width:' + (a.associated/tot*100) + '%;background:var(--assoc)"></span>' +
626
+ '<span style="width:' + (a.none/tot*100) + '%;background:var(--none)"></span>' +
627
+ '</div><div class="tc">' + fmt(a.total) + ' &middot; ' + a.proven_pct.toFixed(1) + '%</div></div>' : '');
628
+ el.addEventListener('click', function(){ setArea(a.area, true); });
629
+ el.addEventListener('mouseenter', function(e){
630
+ showTip('<div class="tt">' + esc(a.area) + '</div>' +
631
+ '<div class="trow"><span>real-code units</span><span class="v">' + fmt(a.total) + '</span></div>' +
632
+ '<div class="trow"><span style="color:var(--proven)">Dynamic Proven</span><span class="v">' + fmt(a.proven) + '</span></div>' +
633
+ '<div class="trow"><span style="color:var(--runtime)">Runtime</span><span class="v">' + fmt(a.runtime) + '</span></div>' +
634
+ '<div class="trow"><span style="color:var(--assoc)">Associated</span><span class="v">' + fmt(a.associated) + '</span></div>' +
635
+ '<div class="trow"><span style="color:var(--none)">No link</span><span class="v">' + fmt(a.none) + '</span></div>',
636
+ e.clientX, e.clientY);
637
+ });
638
+ el.addEventListener('mouseleave', hideTip);
639
+ host.appendChild(el);
640
+ }
641
+ highlightTreemap();
642
+ }
643
+ function highlightTreemap(){
644
+ Array.prototype.forEach.call(document.querySelectorAll('.tile'), function(t){ t.style.outline = ''; });
645
+ if (state.area) Array.prototype.forEach.call(document.querySelectorAll('.tile'), function(t){
646
+ var al = t.getAttribute('aria-label');
647
+ if (al && al.indexOf(state.area + ',') === 0) t.style.outline = '2px solid var(--accent-ring)';
648
+ });
649
+ }
650
+
651
+ // ===================================================================
652
+ // MATRIX TAB (area × tier heat table + area × language strip)
653
+ // ===================================================================
654
+ function heatBg(share, col){ var a = 0.10 + share*0.55; return 'rgba(' + col + ',' + a.toFixed(2) + ')'; }
655
+ var RGB = { proven:'34,197,94', runtime:'88,166,255', associated:'245,158,11', none:'244,63,94' };
656
+ function drawMatrix(){
657
+ var body = $('mxbody'); if (!body) return;
658
+ body.innerHTML = '';
659
+ var areas = AREAS.filter(function(a){
660
+ if (state.lang && !areaMatchesLang(a, state.lang)) return false;
661
+ if (state.search && a.area.toLowerCase().indexOf(state.search.toLowerCase()) === -1) return false;
662
+ return true;
663
+ });
664
+ if (!areas.length){
665
+ var tr0 = document.createElement('tr');
666
+ tr0.innerHTML = '<td colspan="6" style="color:var(--ink-3);padding:18px;text-align:center">No areas match the current filters.</td>';
667
+ body.appendChild(tr0); drawLangStrip(); return;
668
+ }
669
+ areas.forEach(function(a){
670
+ var tr = document.createElement('tr'); tr.dataset.area = a.area;
671
+ var maxCell = Math.max(a.proven, a.runtime, a.associated, a.none) || 1;
672
+ function cell(key, n, col, glyph){
673
+ var dim = !state.tiers[key], share = n/maxCell;
674
+ return '<td><div class="mxcell" role="button" tabindex="0" data-area="' + esc(a.area) + '" aria-label="' + esc(a.area) + ' ' + key + ' ' + fmt(n) + '" ' +
675
+ 'style="background:' + (dim ? 'var(--panel-2)' : heatBg(share, col)) + ';opacity:' + (dim ? 0.4 : 1) + '">' + glyph + '<span>' + fmt(n) + '</span></div></td>';
676
+ }
677
+ var gP = '<svg class="gly" viewBox="0 0 16 16"><circle cx="8" cy="8" r="5" fill="var(--proven)"/></svg>';
678
+ var gR = '<svg class="gly" viewBox="0 0 16 16"><circle cx="8" cy="8" r="5" fill="var(--runtime)"/></svg>';
679
+ var gA = '<svg class="gly" viewBox="0 0 16 16"><circle cx="8" cy="8" r="5" fill="none" stroke="var(--assoc)" stroke-width="2.4"/></svg>';
680
+ var gN = '<svg class="gly" viewBox="0 0 16 16"><circle cx="8" cy="8" r="4.4" fill="none" stroke="var(--none)" stroke-width="2.2"/></svg>';
681
+ tr.innerHTML =
682
+ '<td class="rname">' + esc(a.area) + '<span class="lg">' + esc(a.lang) + '</span></td>' +
683
+ cell('proven', a.proven, RGB.proven, gP) +
684
+ cell('runtime', a.runtime, RGB.runtime, gR) +
685
+ cell('associated', a.associated, RGB.associated, gA) +
686
+ cell('none', a.none, RGB.none, gN) +
687
+ '<td class="pctcell" style="color:var(--proven)">' + a.proven_pct.toFixed(2) + '%</td>';
688
+ body.appendChild(tr);
689
+ });
690
+ Array.prototype.forEach.call(body.querySelectorAll('.mxcell'), function(c){
691
+ c.addEventListener('click', function(){ setArea(c.dataset.area, true); });
692
+ c.addEventListener('keydown', function(e){ if (e.key==='Enter'||e.key===' '){ e.preventDefault(); setArea(c.dataset.area, true); } });
693
+ });
694
+ highlightMatrix(); drawLangStrip();
695
+ }
696
+ function highlightMatrix(){
697
+ Array.prototype.forEach.call(document.querySelectorAll('#mxbody tr'), function(tr){
698
+ tr.style.background = (state.area && tr.dataset.area === state.area) ? 'var(--accent-soft)' : '';
699
+ });
700
+ }
701
+ function drawLangStrip(){
702
+ var host = $('langstrip'); if (!host) return;
703
+ host.innerHTML = '';
704
+ LT.forEach(function(l){
705
+ if (state.lang && l.language !== state.lang) return;
706
+ var denom = l.total || 1, row = document.createElement('div'); row.className = 'lsrow';
707
+ var hp = l.dynamic_proof ? '<span class="hp hard">dynamic proof</span>' : '<span class="hp assoc">assoc-only</span>';
708
+ row.innerHTML =
709
+ '<div class="ln">' + esc(l.language) + hp + '</div>' +
710
+ '<div class="lstrack" role="img" aria-label="' + esc(l.language) + ': ' + fmt(l.proven) + ' proven, ' + fmt(l.runtime_covered) + ' runtime, ' + fmt(l.associated) + ' associated, ' + fmt(l.unlinked) + ' no link">' +
711
+ '<span class="s" style="width:' + (l.proven/denom*100) + '%;background:var(--proven)" title="proven ' + fmt(l.proven) + '"></span>' +
712
+ '<span class="s" style="width:' + (l.runtime_covered/denom*100) + '%;background:var(--runtime)" title="runtime ' + fmt(l.runtime_covered) + '"></span>' +
713
+ '<span class="s" style="width:' + (l.associated/denom*100) + '%;background:var(--assoc)" title="associated ' + fmt(l.associated) + '"></span>' +
714
+ '<span class="s" style="width:' + (l.unlinked/denom*100) + '%;background:var(--none)" title="no link ' + fmt(l.unlinked) + '"></span>' +
715
+ '</div>' +
716
+ '<div class="lstotal">' + fmt(l.total) + '</div>';
717
+ host.appendChild(row);
718
+ });
719
+ if (!host.childNodes.length) host.innerHTML = '<div style="color:var(--ink-3);font-size:12px">No languages match the current filter.</div>';
720
+ }
721
+
722
+ // ===================================================================
723
+ // THEME TOGGLE
724
+ // ===================================================================
725
+ if ($('theme-toggle')) $('theme-toggle').addEventListener('click', function(){
726
+ var html = document.documentElement;
727
+ html.setAttribute('data-theme', html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
728
+ });
729
+
730
+ // ===================================================================
731
+ // INIT — Overview is the default tab; Gap Zones builds on first open.
732
+ // ===================================================================
733
+ buildModel(); drawGraph(); updateSampleNote(); startSim();
734
+ drawOverview(); drawMatrix();
735
+ if (location.hash){
736
+ var hashTab = document.querySelector('[role="tab"]#tab-' + location.hash.slice(1));
737
+ if (hashTab) selectTab(hashTab);
738
+ }
739
+ window.addEventListener('resize', function(){ if ($('pane-overview') && $('pane-overview').classList.contains('active')) drawTreemap(); });
740
+ }
741
+ `;
742
+ const HTML_TEMPLATE = `<!DOCTYPE html>
743
+ <html lang="en" data-theme="dark">
744
+ <head>
745
+ <meta charset="utf-8" />
746
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
747
+ <title>OrangePro · Gap Zones · __TITLE__</title>
748
+ <!-- orangepro: __META__ -->
749
+ <style>
750
+ :root{
751
+ --accent:#ea580c; --accent-700:#c2410c; --accent-soft:rgba(234,88,12,.16); --accent-line:rgba(234,88,12,.55); --accent-ring:#fb923c;
752
+ --proven:#22c55e; --proven-deep:#15803d; --proven-soft:rgba(34,197,94,.13);
753
+ --runtime:#58a6ff; --runtime-deep:#1f6feb; --runtime-soft:rgba(88,166,255,.14);
754
+ --assoc:#f59e0b; --assoc-deep:#b45309; --assoc-soft:rgba(245,158,11,.13);
755
+ --none:#f43f5e; --none-deep:#be123c; --none-soft:rgba(244,63,94,.14);
756
+ --nsc:#8b98ab; --nsc-soft:rgba(139,152,171,.14);
757
+ --bg:#0b0e13; --bg-2:#0f131a; --panel:#141a23; --panel-2:#1a212c; --panel-3:#202938;
758
+ --border:#242d3b; --border-strong:#33405380; --ink:#eef2f7; --ink-2:#b3bdcb; --ink-3:#8590a0; --grid:#1c2431;
759
+ --radius:12px; --radius-sm:8px; --radius-xs:6px; --shadow:0 6px 24px rgba(0,0,0,.42); --shadow-lg:0 22px 60px rgba(0,0,0,.55);
760
+ --mono:ui-monospace,"SF Mono",SFMono-Regular,"Cascadia Code","JetBrains Mono",Menlo,Consolas,monospace;
761
+ --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; --speed:200ms;
762
+ }
763
+ *{ box-sizing:border-box; } html,body{ margin:0; padding:0; }
764
+ body{
765
+ background:radial-gradient(1100px 460px at 82% -10%, rgba(234,88,12,.10) 0%, rgba(234,88,12,0) 62%), linear-gradient(180deg, var(--bg), var(--bg-2));
766
+ background-attachment:fixed; color:var(--ink); font-family:var(--sans); font-size:14px; line-height:1.45; -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
767
+ }
768
+ .num,.mono{ font-family:var(--mono); font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1; }
769
+ a{ color:var(--accent-ring); }
770
+ :focus-visible{ outline:3px solid var(--accent-ring); outline-offset:2px; border-radius:7px; }
771
+ [tabindex]:focus:not(:focus-visible){ outline:none; }
772
+ ::selection{ background:rgba(234,88,12,.35); }
773
+ button{ font-family:inherit; color:inherit; }
774
+ .wrap{ max-width:1480px; margin:0 auto; padding:16px 22px 48px; }
775
+ .sr-only{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); border:0; }
776
+
777
+ .topbar{ display:flex; align-items:center; gap:14px; padding:4px 2px 14px; flex-wrap:wrap; }
778
+ .brand{ display:flex; align-items:center; gap:11px; }
779
+ .logo{ width:34px; height:34px; border-radius:9px; background:linear-gradient(135deg,var(--accent),#fb923c); display:grid; place-items:center; box-shadow:0 4px 14px rgba(234,88,12,.4); }
780
+ .brand h1{ font-size:16px; margin:0; letter-spacing:-.01em; font-weight:680; }
781
+ .brand .sub{ font-size:12px; color:var(--ink-3); }
782
+ .ws{ margin-left:auto; display:flex; align-items:center; gap:8px; font-size:12.5px; color:var(--ink-2); background:var(--panel); border:1px solid var(--border); padding:7px 12px; border-radius:999px; box-shadow:var(--shadow); }
783
+ .ws .dot{ width:7px; height:7px; border-radius:50%; background:var(--proven); flex:none; box-shadow:0 0 0 3px var(--proven-soft); }
784
+ .ws b{ color:var(--ink); font-weight:600; }
785
+
786
+ .banner{ display:none; align-items:flex-start; gap:11px; background:rgba(234,88,12,.10); border:1px solid #6b3a13; border-left:4px solid var(--accent); color:#fcd9bb; padding:11px 14px; border-radius:var(--radius-sm); margin-bottom:14px; box-shadow:var(--shadow); }
787
+ .banner.show{ display:flex; }
788
+ .banner svg{ flex:none; margin-top:1px; } .banner b{ color:#fff; }
789
+ .proofbanner{ display:none; background:rgba(245,158,11,.10); border:1px solid #6b531a; border-left:4px solid var(--assoc); color:#fde6b8; padding:11px 14px; border-radius:var(--radius-sm); margin-bottom:14px; box-shadow:var(--shadow); font-weight:560; }
790
+
791
+ .headline{ display:flex; align-items:center; gap:18px; flex-wrap:wrap; background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); padding:12px 16px; margin-bottom:12px; }
792
+ .hl-num{ display:flex; align-items:baseline; gap:8px; }
793
+ .hl-num .big{ font-family:var(--mono); font-size:30px; font-weight:680; color:var(--proven); letter-spacing:-.02em; }
794
+ .hl-num .lbl{ font-size:12px; color:var(--ink-2); max-width:230px; line-height:1.35; }
795
+ .hl-frame{ font-size:12.5px; color:var(--ink-2); line-height:1.4; max-width:560px; border-left:1px solid var(--border); padding-left:16px; }
796
+ .hl-frame b{ color:var(--ink); font-weight:620; }
797
+ .hl-kpis{ margin-left:auto; display:flex; gap:10px; flex-wrap:wrap; }
798
+ .kpi{ background:var(--panel-2); border:1px solid var(--border); border-radius:var(--radius-sm); padding:8px 12px; min-width:92px; }
799
+ .kpi .k{ font-size:10.5px; text-transform:uppercase; letter-spacing:.06em; color:var(--ink-3); }
800
+ .kpi .v{ font-family:var(--mono); font-size:18px; font-weight:660; margin-top:2px; }
801
+ .kpi.proven .v{ color:var(--proven); } .kpi.runtime .v{ color:var(--runtime); } .kpi.assoc .v{ color:var(--assoc); } .kpi.none .v{ color:var(--none); }
802
+
803
+ .toolbar{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); padding:9px 12px; margin-bottom:12px; }
804
+ .search{ position:relative; display:flex; align-items:center; }
805
+ .search svg{ position:absolute; left:10px; color:var(--ink-3); pointer-events:none; }
806
+ .search input{ background:var(--panel-2); border:1px solid var(--border); color:var(--ink); border-radius:999px; padding:7px 12px 7px 32px; font-size:13px; width:240px; transition:border-color var(--speed); }
807
+ .search input::placeholder{ color:var(--ink-3); } .search input:focus{ border-color:var(--accent-line); }
808
+ .chips{ display:flex; gap:6px; align-items:center; }
809
+ .chips .lab{ font-size:11px; color:var(--ink-3); text-transform:uppercase; letter-spacing:.06em; margin-right:2px; }
810
+ .chip{ display:inline-flex; align-items:center; gap:6px; background:var(--panel-2); border:1px solid var(--border); color:var(--ink-2); border-radius:999px; padding:6px 11px; font-size:12.5px; cursor:pointer; transition:background var(--speed), border-color var(--speed), opacity var(--speed); }
811
+ .chip .gly{ width:13px; height:13px; flex:none; }
812
+ .chip[aria-pressed="false"]{ opacity:.42; }
813
+ .chip.proven[aria-pressed="true"]{ border-color:var(--proven-deep); background:var(--proven-soft); color:var(--proven); }
814
+ .chip.runtime[aria-pressed="true"]{ border-color:var(--runtime-deep); background:var(--runtime-soft); color:var(--runtime); }
815
+ .chip.assoc[aria-pressed="true"]{ border-color:var(--assoc-deep); background:var(--assoc-soft); color:var(--assoc); }
816
+ .chip.none[aria-pressed="true"]{ border-color:var(--none-deep); background:var(--none-soft); color:var(--none); }
817
+ .selsrc{ margin-left:auto; display:flex; align-items:center; gap:8px; }
818
+ .selsrc select{ background:var(--panel-2); border:1px solid var(--border); color:var(--ink); border-radius:var(--radius-sm); padding:7px 10px; font-size:13px; }
819
+ .iconbtn{ background:var(--panel-2); border:1px solid var(--border); color:var(--ink-2); border-radius:var(--radius-sm); padding:7px 9px; cursor:pointer; display:grid; place-items:center; transition:border-color var(--speed), color var(--speed); }
820
+ .iconbtn:hover{ border-color:var(--accent-line); color:var(--ink); }
821
+
822
+ .tabs{ display:flex; gap:4px; margin-bottom:12px; border-bottom:1px solid var(--border); }
823
+ .tab{ background:transparent; border:1px solid transparent; border-bottom:none; color:var(--ink-3); padding:9px 16px; font-size:13.5px; font-weight:560; cursor:pointer; border-radius:9px 9px 0 0; display:inline-flex; align-items:center; gap:8px; position:relative; top:1px; transition:color var(--speed), background var(--speed); }
824
+ .tab:hover{ color:var(--ink); }
825
+ .tab[aria-selected="true"]{ color:var(--ink); background:var(--panel); border-color:var(--border); }
826
+ .tab[aria-selected="true"]::after{ content:""; position:absolute; left:0; right:0; bottom:-1px; height:2px; background:var(--accent); }
827
+
828
+ .panel{ background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); }
829
+ .stagewrap{ display:grid; grid-template-columns:1fr 312px; gap:12px; align-items:stretch; }
830
+ @media (max-width:1100px){ .stagewrap{ grid-template-columns:1fr; } }
831
+ .tabpane{ display:none; } .tabpane.active{ display:block; }
832
+
833
+ .heroGrid{ display:grid; grid-template-columns:196px 1fr; gap:0; }
834
+ @media (max-width:760px){ .heroGrid{ grid-template-columns:1fr; } }
835
+ .rail{ border-right:1px solid var(--border); padding:14px; display:flex; flex-direction:column; gap:14px; }
836
+ @media (max-width:760px){ .rail{ border-right:none; border-bottom:1px solid var(--border); } }
837
+ .rail h3{ font-size:11px; text-transform:uppercase; letter-spacing:.07em; color:var(--ink-3); margin:0 0 8px; }
838
+ .legend-row{ display:flex; align-items:flex-start; gap:9px; margin-bottom:9px; font-size:12px; }
839
+ .legend-row .gly{ width:16px; height:16px; flex:none; margin-top:1px; }
840
+ .legend-row .lt{ font-weight:600; color:var(--ink); } .legend-row .ld{ color:var(--ink-3); font-size:11px; }
841
+ .areafilters{ display:flex; flex-direction:column; gap:4px; max-height:230px; overflow-y:auto; }
842
+ .areabtn{ text-align:left; background:transparent; border:1px solid transparent; color:var(--ink-2); border-radius:var(--radius-xs); padding:5px 8px; font-size:12px; cursor:pointer; display:flex; align-items:center; gap:7px; transition:background var(--speed), border-color var(--speed); }
843
+ .areabtn:hover{ background:var(--panel-2); }
844
+ .areabtn[aria-pressed="true"]{ background:var(--accent-soft); border-color:var(--accent-line); color:var(--ink); }
845
+ .areabtn .sw{ width:8px; height:8px; border-radius:2px; background:var(--accent); flex:none; }
846
+ .areabtn .ct{ margin-left:auto; font-family:var(--mono); font-size:10.5px; color:var(--ink-3); }
847
+ .explain{ font-size:11.5px; color:var(--ink-2); line-height:1.5; } .explain b{ color:var(--ink); }
848
+
849
+ .stage{ position:relative; }
850
+ #forceSvg{ display:block; width:100%; height:560px; touch-action:none; cursor:grab; background:radial-gradient(900px 380px at 84% 50%, rgba(244,63,94,.05), rgba(244,63,94,0) 60%); }
851
+ #forceSvg.grabbing{ cursor:grabbing; }
852
+ .zoneLabel{ font-size:11px; letter-spacing:.08em; text-transform:uppercase; font-weight:700; }
853
+ .zoneSub{ font-size:10px; }
854
+ .hub-label{ font-size:11px; font-weight:620; fill:var(--ink); paint-order:stroke; stroke:var(--bg); stroke-width:3px; }
855
+ .node-focus{ outline:none; cursor:pointer; }
856
+ .sample-note{ position:absolute; left:12px; bottom:10px; font-size:11px; color:var(--ink-2); background:rgba(11,14,19,.78); border:1px solid var(--border); border-radius:999px; padding:4px 11px; backdrop-filter:blur(3px); }
857
+ .zoomctl{ position:absolute; right:12px; bottom:10px; display:flex; gap:6px; }
858
+ .zoomctl .iconbtn svg{ width:15px; height:15px; }
859
+
860
+ .tip{ position:fixed; z-index:60; pointer-events:none; max-width:280px; background:var(--panel-2); border:1px solid var(--border-strong); border-radius:var(--radius-sm); box-shadow:var(--shadow-lg); padding:10px 12px; font-size:12px; color:var(--ink-2); opacity:0; transform:translateY(4px); transition:opacity 120ms, transform 120ms; }
861
+ .tip.show{ opacity:1; transform:translateY(0); }
862
+ .tip .tt{ font-weight:660; color:var(--ink); font-size:13px; margin-bottom:5px; display:flex; align-items:center; gap:7px; }
863
+ .tip .trow{ display:flex; justify-content:space-between; gap:16px; margin-top:2px; } .tip .trow .v{ font-family:var(--mono); }
864
+
865
+ .drill{ padding:14px; display:flex; flex-direction:column; min-height:560px; }
866
+ .drill .dh{ display:flex; align-items:center; gap:9px; margin-bottom:4px; }
867
+ .drill .dh .sw{ width:10px; height:10px; border-radius:3px; background:var(--accent); flex:none; }
868
+ .drill .dh h2{ font-size:14px; margin:0; }
869
+ .drill .dsub{ font-size:11.5px; color:var(--ink-3); margin-bottom:12px; }
870
+ .drill .dmini{ display:flex; gap:6px; margin-bottom:14px; flex-wrap:wrap; }
871
+ .dmini .seg{ flex:1; min-width:54px; border:1px solid var(--border); border-radius:var(--radius-xs); padding:6px 8px; background:var(--panel-2); }
872
+ .dmini .seg .k{ font-size:9.5px; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-3); }
873
+ .dmini .seg .v{ font-family:var(--mono); font-size:15px; font-weight:640; margin-top:1px; }
874
+ .dmini .seg.proven .v{ color:var(--proven); } .dmini .seg.runtime .v{ color:var(--runtime); } .dmini .seg.assoc .v{ color:var(--assoc); } .dmini .seg.none .v{ color:var(--none); }
875
+ .gaplist{ display:flex; flex-direction:column; gap:9px; overflow-y:auto; padding-right:2px; }
876
+ .gaprow{ display:block; width:100%; text-align:left; background:var(--panel-2); border:1px solid var(--border); border-radius:var(--radius-sm); padding:9px 10px; }
877
+ .gaprow:hover{ border-color:var(--border-strong); }
878
+ .gaprow .top{ display:flex; align-items:center; gap:8px; margin-bottom:7px; }
879
+ .gaprow .gly{ width:13px; height:13px; flex:none; }
880
+ .gaprow .gt{ font-size:12.5px; font-weight:600; color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
881
+ .gaprow .gf{ font-family:var(--mono); font-size:10.5px; color:var(--ink-3); margin-bottom:7px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
882
+ .gaprow .gtag{ font-size:9.5px; font-weight:700; text-transform:uppercase; letter-spacing:.05em; opacity:.92; }
883
+ .churn{ font-size:9.5px; font-weight:600; padding:1px 6px; border-radius:999px; text-transform:uppercase; letter-spacing:.04em; }
884
+ .churn.high{ background:var(--none-soft); color:var(--none); } .churn.med{ background:var(--assoc-soft); color:var(--assoc); } .churn.low{ background:var(--nsc-soft); color:var(--nsc); }
885
+ .empty{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; color:var(--ink-3); text-align:center; padding:40px 16px; flex:1; }
886
+ .empty svg{ opacity:.5; }
887
+
888
+ .ovgrid{ display:grid; grid-template-columns:280px 1fr; gap:12px; }
889
+ @media (max-width:900px){ .ovgrid{ grid-template-columns:1fr; } }
890
+ .card{ padding:16px; }
891
+ .card h3{ font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--ink-3); margin:0 0 12px; }
892
+ .donutwrap{ display:flex; flex-direction:column; align-items:center; gap:14px; }
893
+ .splitbar{ margin-top:6px; }
894
+ .splitbar .track{ display:flex; height:26px; border-radius:7px; overflow:hidden; border:1px solid var(--border); }
895
+ .splitbar .seg{ display:flex; align-items:center; justify-content:center; font-family:var(--mono); font-size:11px; font-weight:600; color:#0b0e13; min-width:2px; }
896
+ .splitbar .legend{ display:flex; justify-content:space-between; margin-top:8px; gap:8px; font-size:11px; flex-wrap:wrap; }
897
+ .splitbar .legend span{ display:inline-flex; align-items:center; gap:6px; color:var(--ink-2); }
898
+ .splitbar .legend .dotc{ width:9px; height:9px; border-radius:3px; flex:none; }
899
+ .treemap{ position:relative; width:100%; }
900
+ .tile{ position:absolute; border-radius:6px; overflow:hidden; cursor:pointer; border:1px solid rgba(0,0,0,.35); transition:transform var(--speed), filter var(--speed); display:flex; flex-direction:column; justify-content:space-between; padding:8px 9px; }
901
+ .tile:hover{ filter:brightness(1.12); transform:translateY(-1px); z-index:5; }
902
+ .tile .tn{ font-size:11.5px; font-weight:660; color:#fff; text-shadow:0 1px 3px rgba(0,0,0,.6); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
903
+ .tile .tm{ display:flex; height:5px; border-radius:3px; overflow:hidden; }
904
+ .tile .tc{ font-family:var(--mono); font-size:10px; color:rgba(255,255,255,.92); text-shadow:0 1px 2px rgba(0,0,0,.7); }
905
+
906
+ .mxtable{ width:100%; border-collapse:separate; border-spacing:0; font-size:12.5px; }
907
+ .mxtable caption{ text-align:left; font-size:12px; color:var(--ink-3); margin-bottom:8px; }
908
+ .mxtable th{ text-align:left; font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-3); font-weight:600; padding:7px 9px; }
909
+ .mxtable th.cnum{ text-align:center; }
910
+ .mxtable td{ padding:5px 7px; border-top:1px solid var(--border); }
911
+ .mxtable .rname{ font-size:12.5px; font-weight:560; }
912
+ .mxtable .rname .lg{ font-family:var(--mono); font-size:10px; color:var(--ink-3); margin-left:6px; }
913
+ .mxcell{ position:relative; height:30px; border-radius:6px; display:flex; align-items:center; justify-content:center; font-family:var(--mono); font-size:11.5px; font-weight:600; cursor:pointer; gap:5px; }
914
+ .mxcell .gly{ width:11px; height:11px; flex:none; }
915
+ .mxcell:hover{ outline:2px solid var(--accent-line); outline-offset:-2px; }
916
+ .pctcell{ font-family:var(--mono); font-weight:660; text-align:right; padding-right:12px !important; }
917
+ .langstrip{ margin-top:6px; }
918
+ .lsrow{ display:grid; grid-template-columns:170px 1fr 56px; gap:10px; align-items:center; margin-bottom:8px; }
919
+ .lsrow .ln{ font-size:12px; }
920
+ .lsrow .ln .hp{ font-size:9.5px; font-weight:700; padding:1px 5px; border-radius:4px; margin-left:6px; vertical-align:1px; }
921
+ .hp.hard{ background:var(--proven-soft); color:var(--proven); } .hp.assoc{ background:var(--nsc-soft); color:var(--nsc); }
922
+ .lstrack{ display:flex; height:18px; border-radius:5px; overflow:hidden; border:1px solid var(--border); }
923
+ .lstrack .s{ height:100%; }
924
+ .lstotal{ font-family:var(--mono); font-size:11px; color:var(--ink-2); text-align:right; }
925
+
926
+ .footer{ margin-top:18px; display:flex; align-items:center; gap:10px; flex-wrap:wrap; font-size:11.5px; color:var(--ink-3); padding:12px 4px 0; border-top:1px solid var(--border); }
927
+ .footer .lock{ display:inline-flex; align-items:center; gap:6px; }
928
+ .footer .sep{ width:3px; height:3px; border-radius:50%; background:var(--ink-3); }
929
+ .footer b{ color:var(--ink-2); }
930
+ .whatcounts{ font-size:11.5px; color:var(--ink-2); display:inline-flex; align-items:center; gap:7px; }
931
+ .whatcounts svg{ color:var(--accent-ring); flex:none; }
932
+ #loading-msg{ padding:40px; text-align:center; color:var(--ink-3); font-size:14px; }
933
+ html[data-theme="light"]{ --bg:#f4f5f7; --bg-2:#eceef1; --panel:#ffffff; --panel-2:#f6f8fa; --panel-3:#e7eaef; --border:#dde1e7; --border-strong:#c6cdd6; --ink:#161b22; --ink-2:#3d4756; --ink-3:#5e6979; --grid:#e6e9ee; --proven:#15803d; --runtime:#1f6feb; --assoc:#b45309; --none:#be123c; --proven-soft:rgba(21,128,61,.12); --runtime-soft:rgba(31,111,235,.12); --assoc-soft:rgba(180,83,9,.12); --none-soft:rgba(190,18,60,.12); --nsc-soft:rgba(94,107,125,.14); }
934
+ @media (prefers-reduced-motion: reduce){ *{ transition:none !important; animation:none !important; } }
935
+ </style>
936
+ </head>
937
+ <body data-generated="__GENERATED__">
938
+
939
+ <div id="loading-msg">Loading visualization…</div>
940
+
941
+ <div class="wrap" id="app" style="display:none">
942
+
943
+ <div class="topbar">
944
+ <div class="brand">
945
+ <div class="logo" aria-hidden="true">
946
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
947
+ <circle cx="12" cy="12" r="3.2"/><circle cx="5" cy="6" r="1.7"/><circle cx="19" cy="6" r="1.7"/><circle cx="19" cy="18" r="1.7"/>
948
+ <path d="M9.4 10.4 6.2 7.2M14.6 10.4l3-3M14.4 13.6l3 3"/>
949
+ </svg>
950
+ </div>
951
+ <div><h1>OrangePro · Gap Zones</h1><div class="sub">Offline coverage-gap explorer · metadata only</div></div>
952
+ </div>
953
+ <div class="ws" aria-label="Workspace status">
954
+ <span class="dot" aria-hidden="true"></span>
955
+ <span>workspace <b id="ws-name">__TITLE__</b></span>
956
+ </div>
957
+ </div>
958
+
959
+ <div class="banner" id="partial-banner" role="status">
960
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
961
+ <div><b>Partial scan.</b> <span id="banner-text"></span> Coverage below is incomplete — re-run a full scan for fresh numbers.</div>
962
+ </div>
963
+ <div class="proofbanner" id="proof-banner" role="status"></div>
964
+
965
+ <div class="headline">
966
+ <div class="hl-num">
967
+ <span class="big num" id="hl-pct">0%</span>
968
+ <span class="lbl">proven over <span class="num" id="hl-denom">0</span> real-code units</span>
969
+ </div>
970
+ <div class="hl-frame">
971
+ A small proven % is the point: a behavior is <b>proven</b> only when a dynamic targeted proof certificate closes it.
972
+ The rest is <b>runtime-covered</b>, a <b>weak signal</b>, or <b>no link</b> — not "untested," just not yet proven. The hero is the gap zone on the right.
973
+ </div>
974
+ <div class="hl-kpis">
975
+ <div class="kpi proven"><div class="k">Dynamic Proven</div><div class="v num" id="kpi-proven">0</div></div>
976
+ <div class="kpi runtime"><div class="k">Runtime</div><div class="v num" id="kpi-runtime">0</div></div>
977
+ <div class="kpi assoc"><div class="k">Associated</div><div class="v num" id="kpi-assoc">0</div></div>
978
+ <div class="kpi none"><div class="k">No&nbsp;link</div><div class="v num" id="kpi-none">0</div></div>
979
+ </div>
980
+ </div>
981
+
982
+ <div class="toolbar" role="toolbar" aria-label="Filters">
983
+ <div class="search">
984
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
985
+ <input id="search" type="search" placeholder="Filter areas…" aria-label="Filter areas by name" autocomplete="off" />
986
+ </div>
987
+ <div class="chips" role="group" aria-label="Tier filters">
988
+ <span class="lab">Tiers</span>
989
+ <button class="chip proven" data-tier="proven" aria-pressed="true"><svg class="gly" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="5" fill="var(--proven)"/></svg>Dynamic Proven</button>
990
+ <button class="chip runtime" data-tier="runtime" aria-pressed="true"><svg class="gly" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="5" fill="var(--runtime)"/></svg>Runtime</button>
991
+ <button class="chip assoc" data-tier="associated" aria-pressed="true"><svg class="gly" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="5" fill="none" stroke="var(--assoc)" stroke-width="2.2"/></svg>Associated</button>
992
+ <button class="chip none" data-tier="none" aria-pressed="true"><svg class="gly" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="5" fill="none" stroke="var(--none)" stroke-width="2.2"/></svg>No&nbsp;link</button>
993
+ </div>
994
+ <div class="selsrc">
995
+ <label class="sr-only" for="lang-filter">Language filter</label>
996
+ <select id="lang-filter" aria-label="Filter by language"><option value="">All languages</option></select>
997
+ <button class="iconbtn" id="theme-toggle" aria-label="Toggle light / dark theme" title="Toggle theme">
998
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"/></svg>
999
+ </button>
1000
+ </div>
1001
+ </div>
1002
+
1003
+ <div class="tabs" role="tablist" aria-label="Coverage views">
1004
+ <button class="tab" role="tab" id="tab-zones" aria-controls="pane-zones" aria-selected="false" tabindex="-1">
1005
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="6" cy="12" r="2.4"/><circle cx="18" cy="6" r="2.4"/><circle cx="18" cy="18" r="2.4"/><path d="M8.1 11l7.8-3.5M8.1 13l7.8 3.5"/></svg>
1006
+ Gap Zones</button>
1007
+ <button class="tab" role="tab" id="tab-overview" aria-controls="pane-overview" aria-selected="true" tabindex="0">
1008
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="8" height="8" rx="1.5"/><rect x="13" y="3" width="8" height="5" rx="1.5"/><rect x="13" y="10" width="8" height="11" rx="1.5"/><rect x="3" y="13" width="8" height="8" rx="1.5"/></svg>
1009
+ Overview</button>
1010
+ <button class="tab" role="tab" id="tab-matrix" aria-controls="pane-matrix" aria-selected="false" tabindex="-1">
1011
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M15 3v18M3 9h18M3 15h18"/></svg>
1012
+ Matrix</button>
1013
+ </div>
1014
+
1015
+ <div class="stagewrap">
1016
+ <div>
1017
+ <!-- TAB 1: GAP ZONES -->
1018
+ <div class="tabpane panel" id="pane-zones" role="tabpanel" aria-labelledby="tab-zones" tabindex="0">
1019
+ <div class="heroGrid">
1020
+ <div class="rail">
1021
+ <div>
1022
+ <h3>Tiers</h3>
1023
+ <div class="legend-row"><svg class="gly" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="5.2" fill="var(--proven)"/></svg><div><div class="lt">Dynamic Proven</div><div class="ld">disc · dynamic targeted proof</div></div></div>
1024
+ <div class="legend-row"><svg class="gly" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="5.2" fill="var(--runtime)"/></svg><div><div class="lt">Runtime-covered</div><div class="ld">disc · executed by coverage, not proof</div></div></div>
1025
+ <div class="legend-row"><svg class="gly" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="5" fill="none" stroke="var(--assoc)" stroke-width="2.4"/></svg><div><div class="lt">Associated</div><div class="ld">ring · weak signal, not proof</div></div></div>
1026
+ <div class="legend-row"><svg class="gly" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="4.4" fill="none" stroke="var(--none)" stroke-width="2.2"/><circle cx="8" cy="8" r="7" fill="none" stroke="var(--none)" stroke-width="1" opacity=".55"/></svg><div><div class="lt">No link</div><div class="ld">glowing ring · no test signal</div></div></div>
1027
+ </div>
1028
+ <div><h3>Code areas</h3><div class="areafilters" id="areafilters"></div></div>
1029
+ <div class="explain">
1030
+ <b>Tier definitions.</b> Proven = dynamic targeted mutation proof recorded in the local ledger, not static matching or LLM. Runtime-covered = executed by a repo coverage report. Associated signal = name/path/import/structural matching only, not semantic proof. No link = no direct static or runtime signal found. Broad e2e or integration coverage may still exist.
1031
+ </div>
1032
+ </div>
1033
+ <div class="stage" id="stage">
1034
+ <svg id="forceSvg" viewBox="0 0 1000 560" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Gap zones: code areas as hubs with sampled symbols placed in covered, associated, and no-link zones">
1035
+ <defs>
1036
+ <filter id="redGlow" x="-80%" y="-80%" width="260%" height="260%"><feGaussianBlur stdDeviation="3.4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
1037
+ <filter id="hubGlow" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="2.2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
1038
+ </defs>
1039
+ <g id="viewport"><g id="zonesLayer"></g><g id="linksLayer"></g><g id="hubsLayer"></g><g id="dotsLayer"></g></g>
1040
+ </svg>
1041
+ <div class="sample-note num" id="sample-note">showing 0 of 0 symbols</div>
1042
+ <div class="zoomctl">
1043
+ <button class="iconbtn" id="zoom-in" aria-label="Zoom in"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg></button>
1044
+ <button class="iconbtn" id="zoom-out" aria-label="Zoom out"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14"/></svg></button>
1045
+ <button class="iconbtn" id="zoom-reset" aria-label="Reset view"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 4v4h4"/></svg></button>
1046
+ </div>
1047
+ </div>
1048
+ </div>
1049
+ </div>
1050
+
1051
+ <!-- TAB 2: OVERVIEW -->
1052
+ <div class="tabpane active panel" id="pane-overview" role="tabpanel" aria-labelledby="tab-overview" tabindex="0">
1053
+ <div class="ovgrid">
1054
+ <div class="card">
1055
+ <h3>Proven share</h3>
1056
+ <div class="donutwrap">
1057
+ <svg width="180" height="180" viewBox="0 0 180 180" role="img" aria-label="Proven share of real-code denominator">
1058
+ <circle cx="90" cy="90" r="68" fill="none" stroke="var(--panel-3)" stroke-width="20"/>
1059
+ <circle id="donutArc" cx="90" cy="90" r="68" fill="none" stroke="var(--proven)" stroke-width="20" stroke-linecap="round" transform="rotate(-90 90 90)" stroke-dasharray="0 999"/>
1060
+ <text x="90" y="84" text-anchor="middle" class="num" font-size="30" font-weight="680" fill="var(--proven)" id="donutPct">0%</text>
1061
+ <text x="90" y="104" text-anchor="middle" font-size="11" fill="var(--ink-3)">proven</text>
1062
+ </svg>
1063
+ <div class="splitbar" style="width:100%">
1064
+ <div class="track" id="splitTrack" aria-hidden="true"></div>
1065
+ <div class="legend">
1066
+ <span><span class="dotc" style="background:var(--proven)"></span>Proven <b class="num" id="sb-proven">0</b></span>
1067
+ <span><span class="dotc" style="background:var(--runtime)"></span>Runtime <b class="num" id="sb-runtime">0</b></span>
1068
+ <span><span class="dotc" style="background:var(--assoc)"></span>Assoc <b class="num" id="sb-assoc">0</b></span>
1069
+ <span><span class="dotc" style="background:var(--none)"></span>No link <b class="num" id="sb-none">0</b></span>
1070
+ </div>
1071
+ </div>
1072
+ </div>
1073
+ </div>
1074
+ <div class="card">
1075
+ <h3>Gap heatmap — code areas (size = real-code units, fill = tier mix)</h3>
1076
+ <div class="treemap" id="treemap" role="group" aria-label="Treemap of code areas by coverage"></div>
1077
+ </div>
1078
+ </div>
1079
+ </div>
1080
+
1081
+ <!-- TAB 3: MATRIX -->
1082
+ <div class="tabpane panel" id="pane-matrix" role="tabpanel" aria-labelledby="tab-matrix" tabindex="0">
1083
+ <div class="card">
1084
+ <table class="mxtable" id="mxtable">
1085
+ <caption>Code behavior evidence by language — area × tier; cell shows count &amp; tier glyph; click to drill. Proven% over the area's real-code units.</caption>
1086
+ <thead><tr>
1087
+ <th scope="col">Code area</th>
1088
+ <th scope="col" class="cnum">Dynamic Proven</th>
1089
+ <th scope="col" class="cnum">Runtime</th>
1090
+ <th scope="col" class="cnum">Associated</th>
1091
+ <th scope="col" class="cnum">No&nbsp;link</th>
1092
+ <th scope="col" style="text-align:right">Proven&nbsp;%</th>
1093
+ </tr></thead>
1094
+ <tbody id="mxbody"></tbody>
1095
+ </table>
1096
+ </div>
1097
+ <div class="card" style="margin-top:12px">
1098
+ <h3>Area × language — tier split (test files excluded)</h3>
1099
+ <div class="langstrip" id="langstrip"></div>
1100
+ </div>
1101
+ </div>
1102
+ </div>
1103
+
1104
+ <!-- SHARED DRILL PANEL -->
1105
+ <div class="panel">
1106
+ <div class="drill" id="drill">
1107
+ <div class="empty" id="drillEmpty">
1108
+ <svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
1109
+ <div>Click a code-area hub, a dot, a treemap tile, or a matrix cell to see that area's real uncovered symbols.</div>
1110
+ </div>
1111
+ <div id="drillContent" hidden>
1112
+ <div class="dh"><span class="sw" id="drillSw"></span><h2 id="drillTitle">—</h2></div>
1113
+ <div class="dsub" id="drillSub">—</div>
1114
+ <div class="dmini">
1115
+ <div class="seg proven"><div class="k">Dynamic Proven</div><div class="v num" id="dm-proven">0</div></div>
1116
+ <div class="seg runtime"><div class="k">Runtime</div><div class="v num" id="dm-runtime">0</div></div>
1117
+ <div class="seg assoc"><div class="k">Assoc</div><div class="v num" id="dm-assoc">0</div></div>
1118
+ <div class="seg none"><div class="k">No link</div><div class="v num" id="dm-none">0</div></div>
1119
+ </div>
1120
+ <h3 style="font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-3);margin:0 0 9px">Uncovered symbols — no proven test (sampled, worst first)</h3>
1121
+ <div class="gaplist" id="gaplist"></div>
1122
+ </div>
1123
+ </div>
1124
+ </div>
1125
+ </div>
1126
+
1127
+ <div class="footer">
1128
+ <span class="whatcounts">
1129
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 16v-4M12 8h.01"/></svg>
1130
+ What counts: real code symbols only — <b>test files are excluded</b> from the denominator (they appear only as proof links).
1131
+ </span>
1132
+ <span class="sep" aria-hidden="true"></span>
1133
+ <span class="lock">
1134
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="10.5" width="16" height="10" rx="2"/><path d="M8 10.5V7a4 4 0 0 1 8 0v3.5"/></svg>
1135
+ Metadata only, generated locally
1136
+ </span>
1137
+ <span class="sep" aria-hidden="true"></span>
1138
+ <span>workspace <b id="ft-ws">__TITLE__</b></span>
1139
+ <span class="sep" aria-hidden="true"></span>
1140
+ <span>generated <b class="num" id="ft-date">—</b></span>
1141
+ </div>
1142
+ </div>
1143
+
1144
+ <div class="tip" id="tip" role="tooltip" aria-hidden="true"></div>
1145
+
1146
+ <script>/*__D3__*/</script>
1147
+ <script>
1148
+ const DATA="__DATA__";
1149
+ /*__VIZ_LOGIC__*/
1150
+ </script>
1151
+ </body>
1152
+ </html>`;