@event4u/agent-config 17.0.0 → 17.2.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 (164) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +157 -468
  5. package/README.md +4 -3
  6. package/dist/agent-src/commands/project/analyze.md +9 -6
  7. package/dist/agent-src/commands/project/map.md +58 -0
  8. package/dist/agent-src/commands/project.md +5 -3
  9. package/dist/agent-src/contexts/execution/host-capability-manifest.md +5 -3
  10. package/dist/agent-src/skills/code-intelligence/SKILL.md +6 -0
  11. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  12. package/dist/agent-src/templates/roadmaps.md +5 -1
  13. package/dist/cli/registry.js +1 -1
  14. package/dist/cli-delegate/{chunk-IROAQ77A.js → chunk-3PK3VZHB.js} +107 -9
  15. package/dist/cli-delegate/{chunk-2SFOOFJJ.js → chunk-BZJ4Y3CI.js} +1 -1
  16. package/dist/cli-delegate/{chunk-CAL76ADI.js → chunk-CMZEKSNN.js} +52 -10
  17. package/dist/cli-delegate/{chunk-ESUNQ5XR.js → chunk-E3CFULNX.js} +22 -4
  18. package/dist/cli-delegate/{chunk-GOMILC3O.js → chunk-QBHEI5N4.js} +4 -3
  19. package/dist/cli-delegate/{chunk-SIOMDAKL.js → chunk-VYZGJLCA.js} +29 -39
  20. package/dist/cli-delegate/chunk-W5JXMF2T.js +77 -0
  21. package/dist/cli-delegate/{chunk-4SHNERBL.js → chunk-XIAPY44N.js} +1 -1
  22. package/dist/cli-delegate/{chunk-JHMMWNJ2.js → chunk-YHAPONWV.js} +1 -1
  23. package/dist/cli-delegate/cmd_conformance.js +32 -47
  24. package/dist/cli-delegate/cmd_doctor.js +4 -4
  25. package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
  26. package/dist/cli-delegate/cmd_explain.js +1 -1
  27. package/dist/cli-delegate/cmd_export.js +6 -6
  28. package/dist/cli-delegate/cmd_fleet.js +3 -3
  29. package/dist/cli-delegate/cmd_preflight.js +3 -3
  30. package/dist/cli-delegate/cmd_prune.js +6 -6
  31. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  32. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  33. package/dist/cli-delegate/cmd_sync.js +6 -6
  34. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  35. package/dist/cli-delegate/cmd_update.js +9 -13
  36. package/dist/cli-delegate/cmd_validate.js +6 -6
  37. package/dist/discovery/deprecation-report.md +1 -1
  38. package/dist/discovery/discovery-manifest.json +41 -15
  39. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  40. package/dist/discovery/discovery-manifest.summary.md +3 -3
  41. package/dist/discovery/dormancy-report.md +1 -1
  42. package/dist/discovery/orphan-report.md +1 -1
  43. package/dist/discovery/packs.json +6 -5
  44. package/dist/discovery/trust-report.md +2 -2
  45. package/dist/discovery/workspaces.json +5 -4
  46. package/dist/hooks/dispatch.js +163 -148
  47. package/dist/hooks/dispatch.sha256 +1 -1
  48. package/dist/hooks/instructions_observer.chunk.cjs +301 -0
  49. package/dist/hooks/instructions_observer.chunk.cjs.sha256 +1 -0
  50. package/dist/install/claudeRuleRewrite.js +10 -3
  51. package/dist/install/conflict.js +24 -2
  52. package/dist/install/conflictTracker.js +14 -3
  53. package/dist/install/install.mjs +222 -117
  54. package/dist/install/installThinLayer.js +1 -0
  55. package/dist/install/loggedWrites.js +142 -0
  56. package/dist/install/paths.js +12 -2
  57. package/dist/install/preserve.js +25 -4
  58. package/dist/install/txlog.js +10 -6
  59. package/dist/mcp/registry-manifest.json +3 -3
  60. package/dist/mcp/server.json +2 -2
  61. package/dist/mcp/server.mjs +33595 -22916
  62. package/dist/scripts/_lib/egress_rules.js +4 -1
  63. package/dist/scripts/_lib/identifier_redaction.js +35 -2
  64. package/dist/scripts/ai_council/seat_policy.js +7 -1
  65. package/dist/server/schemas/settings.js +1 -1
  66. package/package.json +4 -1
  67. package/src/config/ci-local-parity.yml +1 -1
  68. package/src/config/continuity-surface.json +7 -0
  69. package/src/config/evaluator-budgets.json +4 -4
  70. package/src/config/gate-coverage.yml +27 -7
  71. package/src/config/gate-violation-baselines.json +9 -3
  72. package/src/config/gitignore-block.txt +6 -0
  73. package/src/config/hook-bundle-budget.json +19 -4
  74. package/src/config/hook-token-budget.json +1 -1
  75. package/src/config/packed-binary-manifest.json +27 -0
  76. package/src/config/production-entries.json +18 -0
  77. package/src/config/release-gate-locality.yml +0 -7
  78. package/src/config/skill-body-budget.json +7 -1
  79. package/src/install/claudeRuleRewrite.ts +10 -1
  80. package/src/install/conflict.ts +24 -1
  81. package/src/install/conflictTracker.ts +18 -2
  82. package/src/install/installThinLayer.ts +6 -0
  83. package/src/install/loggedWrites.ts +148 -0
  84. package/src/install/paths.ts +11 -2
  85. package/src/install/preserve.ts +40 -4
  86. package/src/install/txlog.ts +10 -6
  87. package/src/scripts/_cli/cmd_conformance.ts +3 -2
  88. package/src/scripts/_cli/cmd_update.ts +15 -17
  89. package/src/scripts/_lib/bench_ab_complexity.ts +14 -2
  90. package/src/scripts/_lib/collector_record.ts +1 -0
  91. package/src/scripts/_lib/council_settings_block.ts +9 -2
  92. package/src/scripts/_lib/egress_rules.ts +4 -1
  93. package/src/scripts/_lib/host_capability.ts +10 -5
  94. package/src/scripts/_lib/identifier_redaction.ts +38 -2
  95. package/src/scripts/_lib/price_freshness.ts +112 -0
  96. package/src/scripts/_lib/production_reach.ts +194 -0
  97. package/src/scripts/_lib/quota_parked.ts +17 -28
  98. package/src/scripts/_lib/release_findings_ingest.ts +13 -335
  99. package/src/scripts/_lib/runtime_journal.ts +7 -7
  100. package/src/scripts/_lib/tier_downshift_verdict.ts +81 -35
  101. package/src/scripts/ai_council/chairman.ts +5 -2
  102. package/src/scripts/ai_council/clients.ts +4 -3
  103. package/src/scripts/ai_council/free_hand_shadow.ts +24 -12
  104. package/src/scripts/ai_council/free_route_catalog.ts +142 -5
  105. package/src/scripts/ai_council/free_route_client.ts +4 -0
  106. package/src/scripts/ai_council/hand_payload.ts +115 -7
  107. package/src/scripts/ai_council/orchestrator.ts +7 -10
  108. package/src/scripts/ai_council/route_chooser.ts +1 -1
  109. package/src/scripts/ai_council/route_member.ts +66 -4
  110. package/src/scripts/ai_council/seat_policy.ts +8 -1
  111. package/src/scripts/ai_council/self_hosted_probe.ts +327 -0
  112. package/src/scripts/ai_council/status_surface.ts +2 -0
  113. package/src/scripts/ask_transport.ts +137 -15
  114. package/src/scripts/check_finding_dispositions.ts +25 -71
  115. package/src/scripts/check_release_pr_shape.ts +3 -3
  116. package/src/scripts/check_step_production_reach.ts +257 -0
  117. package/src/scripts/code_graph/build.ts +210 -10
  118. package/src/scripts/code_graph/cli.ts +168 -46
  119. package/src/scripts/code_graph/detect.ts +5 -4
  120. package/src/scripts/code_graph/docs_layer.ts +57 -0
  121. package/src/scripts/code_graph/extract.ts +58 -13
  122. package/src/scripts/code_graph/extract_more.ts +314 -0
  123. package/src/scripts/code_graph/laravel_overlay.ts +454 -0
  124. package/src/scripts/code_graph/map.ts +846 -0
  125. package/src/scripts/code_graph/map_model.ts +977 -0
  126. package/src/scripts/code_graph/map_verify.ts +140 -0
  127. package/src/scripts/code_graph/map_viewer/viewer.css +278 -0
  128. package/src/scripts/code_graph/map_viewer/viewer.js +756 -0
  129. package/src/scripts/code_graph/scip_reader.ts +201 -0
  130. package/src/scripts/code_graph/sqlite_store.ts +8 -1
  131. package/src/scripts/code_graph/types.ts +81 -15
  132. package/src/scripts/code_graph/validate.ts +16 -26
  133. package/src/scripts/code_graph/verbs.ts +7 -0
  134. package/src/scripts/council_cli.ts +10 -8
  135. package/src/scripts/hook_chunks.mjs +7 -0
  136. package/src/scripts/hook_manifest.json +1 -1
  137. package/src/scripts/hook_manifest.yaml +23 -0
  138. package/src/scripts/hooks/dispatch_hook.ts +4 -4
  139. package/src/scripts/hooks/instructions_observer_hook.ts +25 -6
  140. package/src/scripts/hooks/lazy_concern.ts +4 -0
  141. package/src/scripts/hooks/payload_stub.ts +27 -0
  142. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +29 -70
  143. package/src/scripts/hooks/subagent_ledger_hook.ts +80 -0
  144. package/src/scripts/install-hooks.sh +5 -0
  145. package/src/scripts/install.ts +2 -2
  146. package/src/scripts/lint_agents_layout.ts +2 -0
  147. package/src/scripts/lint_decision_classes.ts +34 -0
  148. package/src/scripts/lint_hook_manifest.ts +25 -0
  149. package/src/scripts/lint_roadmap_blockers.ts +52 -3
  150. package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -6
  151. package/src/scripts/mcp_server/graph_tools.ts +73 -1
  152. package/src/scripts/mcp_server/tools.ts +19 -36
  153. package/src/scripts/release.ts +5 -9
  154. package/src/scripts/release_drill.ts +11 -165
  155. package/src/scripts/release_publication.ts +0 -9
  156. package/src/scripts/report_production_reach.ts +35 -0
  157. package/src/scripts/worker_reuse_shadow_report.ts +399 -1
  158. package/src/server/schemas/settings.ts +1 -1
  159. package/src/vendor/grammars/README.md +10 -5
  160. package/src/vendor/grammars/tree-sitter-go.wasm +0 -0
  161. package/src/vendor/grammars/tree-sitter-java.wasm +0 -0
  162. package/src/vendor/grammars/tree-sitter-python.wasm +0 -0
  163. package/dist/cli-delegate/chunk-A4QJ6ENT.js +0 -50
  164. /package/dist/cli-delegate/{chunk-E6XNBVE4.js → chunk-KR5ZIFPO.js} +0 -0
@@ -0,0 +1,756 @@
1
+ /*
2
+ * Offline viewer for agents/map/index.html.
3
+ *
4
+ * Reads the map model from the inline `#map-data` JSON block and draws it on a
5
+ * canvas: areas → an area's files → a file's symbols in the side panel. The
6
+ * layout is precomputed by the build, so nothing here simulates physics.
7
+ *
8
+ * Every string from the model reaches the page through textContent, never
9
+ * innerHTML: file paths and symbol names come from the scanned repository.
10
+ */
11
+ (function () {
12
+ 'use strict';
13
+
14
+ var data = JSON.parse(document.getElementById('map-data').textContent);
15
+ var model = data.model;
16
+ var files = model.files;
17
+ var areas = model.areas;
18
+
19
+ var STR = {
20
+ en: {
21
+ map: 'Map',
22
+ areas: 'Areas',
23
+ groups: 'Not in an area',
24
+ lensAreas: 'Areas',
25
+ lensFolders: 'Folders',
26
+ lensArtefacts: 'Artefacts',
27
+ artefactHint: 'Skills, rules, commands and ADRs, related by the fields they declare (routes_to, supersedes, ADR references). Most referenced first.',
28
+ folderHint: 'Each file carries the colour of its area: a folder of many colours is one whose layout and coupling disagree.',
29
+ search: 'Search files and areas',
30
+ files: function (n) { return n === 1 ? '1 file' : n + ' files'; },
31
+ role: 'Role',
32
+ area: 'Area',
33
+ usedBy: 'Used by',
34
+ uses: 'Uses',
35
+ symbols: 'Symbols',
36
+ tests: 'Covered by tests',
37
+ externals: 'External dependencies',
38
+ requestPaths: 'Request paths',
39
+ change: function (rev, c, i) { return 'This change (' + rev + '): ' + c + ' changed, ' + i + ' linked into a change'; },
40
+ reachedBy: 'Reached by routes',
41
+ outcome: 'Session outcome (local, decayed)',
42
+ drift: 'Since the last map',
43
+ prs: 'Open PRs sharing an area',
44
+ guesses: 'Guessed links',
45
+ state: 'Graph state',
46
+ cohesion: 'Cohesion',
47
+ testShare: 'Test share',
48
+ mismatch: 'Outside the hub folder',
49
+ hub: 'Hub',
50
+ more: function (n) { return '+ ' + n + ' more'; },
51
+ infra: 'Infrastructure',
52
+ guessToggle: 'Guesses',
53
+ testsToggle: 'Tests',
54
+ theme: 'Theme',
55
+ lang: 'Deutsch',
56
+ zoomIn: 'Zoom in',
57
+ zoomOut: 'Zoom out',
58
+ reset: 'Fit',
59
+ none: 'none',
60
+ hint: 'Click an area to open it. Click a file to inspect it. Esc goes back.',
61
+ roles: {
62
+ code: 'code', test: 'test', entry: 'entry point', view: 'view', migration: 'migration',
63
+ translation: 'translation', config: 'config', doc: 'documentation', infra: 'infrastructure', isolated: 'isolated'
64
+ }
65
+ },
66
+ de: {
67
+ map: 'Karte',
68
+ areas: 'Bereiche',
69
+ groups: 'Ohne Bereich',
70
+ lensAreas: 'Bereiche',
71
+ lensFolders: 'Ordner',
72
+ lensArtefacts: 'Artefakte',
73
+ artefactHint: 'Skills, Rules, Commands und ADRs, verbunden über ihre deklarierten Felder (routes_to, supersedes, ADR-Verweise). Meistreferenzierte zuerst.',
74
+ folderHint: 'Jede Datei trägt die Farbe ihres Bereichs: Ein bunter Ordner ist einer, dessen Ablage und Kopplung auseinanderlaufen.',
75
+ search: 'Dateien und Bereiche suchen',
76
+ files: function (n) { return n === 1 ? '1 Datei' : n + ' Dateien'; },
77
+ role: 'Rolle',
78
+ area: 'Bereich',
79
+ usedBy: 'Verwendet von',
80
+ uses: 'Verwendet',
81
+ symbols: 'Symbole',
82
+ tests: 'Abgedeckt durch Tests',
83
+ externals: 'Externe Abhängigkeiten',
84
+ requestPaths: 'Request-Pfade',
85
+ change: function (rev, c, i) { return 'Diese Änderung (' + rev + '): ' + c + ' geändert, ' + i + ' hängen daran'; },
86
+ reachedBy: 'Erreicht über Routen',
87
+ outcome: 'Sitzungsergebnis (lokal, abklingend)',
88
+ drift: 'Seit der letzten Karte',
89
+ prs: 'Offene PRs im selben Bereich',
90
+ guesses: 'Geratene Verbindungen',
91
+ state: 'Graph-Stand',
92
+ cohesion: 'Zusammenhalt',
93
+ testShare: 'Testanteil',
94
+ mismatch: 'Außerhalb des Hub-Ordners',
95
+ hub: 'Hub',
96
+ more: function (n) { return '+ ' + n + ' weitere'; },
97
+ infra: 'Infrastruktur',
98
+ guessToggle: 'Vermutungen',
99
+ testsToggle: 'Tests',
100
+ theme: 'Design',
101
+ lang: 'English',
102
+ zoomIn: 'Vergrößern',
103
+ zoomOut: 'Verkleinern',
104
+ reset: 'Einpassen',
105
+ none: 'keine',
106
+ hint: 'Bereich anklicken zum Öffnen, Datei anklicken zum Ansehen. Esc geht zurück.',
107
+ roles: {
108
+ code: 'Code', test: 'Test', entry: 'Einstiegspunkt', view: 'View', migration: 'Migration',
109
+ translation: 'Übersetzung', config: 'Konfiguration', doc: 'Dokumentation', infra: 'Infrastruktur', isolated: 'isoliert'
110
+ }
111
+ }
112
+ };
113
+ var lang = (navigator.language || 'en').toLowerCase().indexOf('de') === 0 ? 'de' : 'en';
114
+ function t(key) { return STR[lang][key]; }
115
+
116
+ // Adjacency, built once.
117
+ var inOf = files.map(function () { return []; });
118
+ var outOf = files.map(function () { return []; });
119
+ model.edges.forEach(function (e) {
120
+ outOf[e[0]].push([e[1], e[2]]);
121
+ inOf[e[1]].push([e[0], e[2]]);
122
+ });
123
+ var byWeight = function (a, b) { return b[1] - a[1] || a[0] - b[0]; };
124
+ inOf.forEach(function (l) { l.sort(byWeight); });
125
+ outOf.forEach(function (l) { l.sort(byWeight); });
126
+
127
+ var changedSet = Object.create(null);
128
+ var impactedSet = Object.create(null);
129
+ var touchedArea = Object.create(null);
130
+ if (model.diff) {
131
+ model.diff.changed.forEach(function (i) { changedSet[i] = true; });
132
+ model.diff.impacted.forEach(function (i) { impactedSet[i] = true; });
133
+ model.diff.areas.forEach(function (a) { touchedArea[a[0]] = true; });
134
+ }
135
+
136
+ var outcomeOf = Object.create(null);
137
+ (model.outcomes || []).forEach(function (o) { outcomeOf[o[0]] = o[1]; });
138
+
139
+ function hue(i) { return Math.round((i * 137.508) % 360); }
140
+ function areaColor(i, alpha) {
141
+ return 'hsla(' + hue(i) + ', 62%, ' + (document.documentElement.dataset.resolvedTheme === 'dark' ? '62%' : '46%') + ', ' + (alpha == null ? 1 : alpha) + ')';
142
+ }
143
+ function cssVar(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
144
+ function base(p) { var i = p.lastIndexOf('/'); return i < 0 ? p : p.slice(i + 1); }
145
+ function areaLabel(a) {
146
+ return a.kind === 'small-parts' ? '… ' + base(a.name.replace(/^small parts · /, '')) : base(a.name);
147
+ }
148
+ function dir(p) { var i = p.lastIndexOf('/'); return i < 0 ? '.' : p.slice(0, i); }
149
+
150
+ // DOM helpers — text only.
151
+ function el(tag, attrs, text) {
152
+ var n = document.createElement(tag);
153
+ if (attrs) Object.keys(attrs).forEach(function (k) { n.setAttribute(k, attrs[k]); });
154
+ if (text != null) n.textContent = String(text);
155
+ return n;
156
+ }
157
+ function clear(n) { while (n.firstChild) n.removeChild(n.firstChild); }
158
+ function swatch(i) {
159
+ var s = el('span', { class: 'swatch' });
160
+ s.style.background = areaColor(i);
161
+ return s;
162
+ }
163
+
164
+ var canvas = document.getElementById('stage');
165
+ var ctx = canvas.getContext('2d');
166
+ var panel = document.getElementById('panel');
167
+ var crumbs = document.getElementById('crumbs');
168
+ var searchInput = document.getElementById('search');
169
+ var results = document.getElementById('results');
170
+
171
+ var state = {
172
+ level: 'overview', // overview | area
173
+ area: -1,
174
+ file: -1,
175
+ showInfra: true,
176
+ showGuesses: false,
177
+ showTests: true,
178
+ lens: 'areas', // areas | folders
179
+ view: { scale: 1, x: 0, y: 0 }
180
+ };
181
+ var drawn = []; // {kind, idx, x, y, r} in screen space, for hit tests
182
+ window.__mapLabelBoxes = { boxes: [], width: 0, height: 0 };
183
+
184
+ function resize() {
185
+ var r = canvas.getBoundingClientRect();
186
+ var dpr = window.devicePixelRatio || 1;
187
+ canvas.width = Math.max(1, Math.round(r.width * dpr));
188
+ canvas.height = Math.max(1, Math.round(r.height * dpr));
189
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
190
+ draw();
191
+ }
192
+
193
+ function size() {
194
+ var r = canvas.getBoundingClientRect();
195
+ return { w: r.width, h: r.height };
196
+ }
197
+
198
+ function toScreen(x, y) {
199
+ var s = size();
200
+ var pad = 48;
201
+ var k = (Math.min(s.w, s.h) / 2 - pad) * state.view.scale;
202
+ return [s.w / 2 + (x * k) + state.view.x, s.h / 2 + (y * k) + state.view.y];
203
+ }
204
+
205
+ // Visible nodes for the current level.
206
+ function currentNodes() {
207
+ var out = [];
208
+ if (state.level === 'overview') {
209
+ var max = 1;
210
+ areas.forEach(function (a) { max = Math.max(max, a.files.length); });
211
+ areas.forEach(function (a, i) {
212
+ out.push({ kind: 'area', idx: i, x: a.x, y: a.y, r: 5 + 26 * Math.sqrt(a.files.length / max), label: areaLabel(a), weight: a.files.length });
213
+ });
214
+ return out;
215
+ }
216
+ var a = areas[state.area];
217
+ var maxIn = 1;
218
+ a.local.forEach(function (l) { maxIn = Math.max(maxIn, files[l[0]].inFiles); });
219
+ a.local.forEach(function (l) {
220
+ var f = files[l[0]];
221
+ if (!state.showTests && f.role === 'test') return;
222
+ if (!state.showInfra && f.role === 'infra') return;
223
+ var outside = f.area !== state.area;
224
+ out.push({ kind: 'file', idx: l[0], x: l[1], y: l[2], r: outside ? 4 : 4 + 10 * Math.sqrt(f.inFiles / maxIn), label: base(f.path), weight: f.inFiles + (outside ? -1000 : 0), outside: outside });
225
+ });
226
+ return out;
227
+ }
228
+
229
+ function draw() {
230
+ var s = size();
231
+ ctx.clearRect(0, 0, s.w, s.h);
232
+ var nodes = currentNodes();
233
+ var pos = Object.create(null);
234
+ nodes.forEach(function (n) {
235
+ var p = toScreen(n.x, n.y);
236
+ n.sx = p[0];
237
+ n.sy = p[1];
238
+ pos[n.kind + n.idx] = n;
239
+ });
240
+
241
+ // Edges first.
242
+ var edge = cssVar('--edge');
243
+ var strong = cssVar('--edge-strong');
244
+ ctx.lineCap = 'round';
245
+ if (state.level === 'overview') {
246
+ var maxW = 1;
247
+ model.areaEdges.forEach(function (e) { maxW = Math.max(maxW, e[2]); });
248
+ model.areaEdges.forEach(function (e) {
249
+ var a = pos['area' + e[0]];
250
+ var b = pos['area' + e[1]];
251
+ if (!a || !b) return;
252
+ var hot = state.area === e[0] || state.area === e[1];
253
+ ctx.strokeStyle = hot ? strong : edge;
254
+ ctx.lineWidth = 0.5 + 4 * Math.sqrt(e[2] / maxW);
255
+ ctx.beginPath();
256
+ ctx.moveTo(a.sx, a.sy);
257
+ ctx.lineTo(b.sx, b.sy);
258
+ ctx.stroke();
259
+ });
260
+ } else {
261
+ var drawEdges = function (list, dashed) {
262
+ ctx.setLineDash(dashed ? [4, 4] : []);
263
+ list.forEach(function (e) {
264
+ var a = pos['file' + e[0]];
265
+ var b = pos['file' + e[1]];
266
+ if (!a || !b) return;
267
+ var hot = state.file === e[0] || state.file === e[1];
268
+ ctx.strokeStyle = hot ? strong : edge;
269
+ ctx.lineWidth = hot ? 1.8 : 0.8;
270
+ ctx.beginPath();
271
+ ctx.moveTo(a.sx, a.sy);
272
+ ctx.lineTo(b.sx, b.sy);
273
+ ctx.stroke();
274
+ });
275
+ ctx.setLineDash([]);
276
+ };
277
+ drawEdges(model.edges, false);
278
+ if (state.showGuesses) drawEdges(model.guessEdges, true);
279
+ }
280
+
281
+ // Nodes.
282
+ var grey = cssVar('--grey-node');
283
+ var dimOthers = state.file >= 0 && state.level === 'area';
284
+ var linked = Object.create(null);
285
+ if (dimOthers) {
286
+ linked[state.file] = true;
287
+ inOf[state.file].forEach(function (x) { linked[x[0]] = true; });
288
+ outOf[state.file].forEach(function (x) { linked[x[0]] = true; });
289
+ }
290
+ drawn = [];
291
+ nodes.forEach(function (n) {
292
+ var r = n.r;
293
+ var dim = dimOthers && !linked[n.idx];
294
+ if (dim) r = Math.max(2.5, r * 0.6);
295
+ var color;
296
+ if (n.kind === 'area') color = areaColor(n.idx, state.area >= 0 && state.area !== n.idx ? 0.55 : 1);
297
+ else if (n.outside) color = grey;
298
+ else {
299
+ var f = files[n.idx];
300
+ color = f.role === 'test' ? areaColor(state.area, 0.45) : f.role === 'infra' ? grey : areaColor(state.area, 1);
301
+ }
302
+ ctx.globalAlpha = dim ? 0.35 : 1;
303
+ ctx.fillStyle = color;
304
+ ctx.beginPath();
305
+ ctx.arc(n.sx, n.sy, r, 0, Math.PI * 2);
306
+ ctx.fill();
307
+ if ((n.kind === 'file' && n.idx === state.file) || (n.kind === 'area' && n.idx === state.area)) {
308
+ ctx.lineWidth = 2.5;
309
+ ctx.strokeStyle = cssVar('--accent');
310
+ ctx.stroke();
311
+ } else if ((n.kind === 'area' && touchedArea[n.idx]) || (n.kind === 'file' && changedSet[n.idx])) {
312
+ ctx.lineWidth = 2.5;
313
+ ctx.strokeStyle = cssVar('--change');
314
+ ctx.stroke();
315
+ } else if (n.kind === 'file' && outcomeOf[n.idx] !== undefined) {
316
+ ctx.lineWidth = 2.5;
317
+ ctx.strokeStyle = outcomeOf[n.idx] > 0 ? cssVar('--good') : cssVar('--contested');
318
+ ctx.stroke();
319
+ } else if (n.kind === 'file' && impactedSet[n.idx]) {
320
+ ctx.setLineDash([3, 2]);
321
+ ctx.lineWidth = 1.5;
322
+ ctx.strokeStyle = cssVar('--change');
323
+ ctx.stroke();
324
+ ctx.setLineDash([]);
325
+ }
326
+ ctx.globalAlpha = 1;
327
+ drawn.push({ kind: n.kind, idx: n.idx, x: n.sx, y: n.sy, r: Math.max(r, 8) });
328
+ });
329
+
330
+ drawLabels(nodes, s);
331
+ }
332
+
333
+ // Labels: biggest first, placed only where they fit, flipped near the right
334
+ // edge, never outside the canvas, never on top of another label.
335
+ function drawLabels(nodes, s) {
336
+ ctx.font = '12px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
337
+ ctx.textBaseline = 'middle';
338
+ var narrow = s.w < 600;
339
+ var order = nodes.slice().sort(function (a, b) { return b.weight - a.weight || a.idx - b.idx; });
340
+ var limit = state.level === 'overview' ? (narrow ? 8 : 60) : (narrow ? 12 : 80);
341
+ var placed = [];
342
+ var text = cssVar('--text');
343
+ var bg = cssVar('--bg');
344
+ var count = 0;
345
+ for (var i = 0; i < order.length && count < limit; i++) {
346
+ var n = order[i];
347
+ var label = n.label.length > 34 ? n.label.slice(0, 33) + '…' : n.label;
348
+ var w = ctx.measureText(label).width;
349
+ var h = 14;
350
+ var x = n.sx + n.r + 4;
351
+ var y = n.sy - h / 2;
352
+ if (x + w > s.w - 2) x = n.sx - n.r - 4 - w;
353
+ if (x < 2 || y < 2 || y + h > s.h - 2 || x + w > s.w - 2) continue;
354
+ var box = { x: x - 2, y: y, w: w + 4, h: h };
355
+ var hit = placed.some(function (p) { return box.x < p.x + p.w && p.x < box.x + box.w && box.y < p.y + p.h && p.y < box.y + box.h; });
356
+ if (hit) continue;
357
+ placed.push(box);
358
+ ctx.globalAlpha = 0.8;
359
+ ctx.fillStyle = bg;
360
+ ctx.fillRect(box.x, box.y, box.w, box.h);
361
+ ctx.globalAlpha = 1;
362
+ ctx.fillStyle = text;
363
+ ctx.fillText(label, x, n.sy);
364
+ count += 1;
365
+ }
366
+ window.__mapLabelBoxes = { boxes: placed, width: s.w, height: s.h };
367
+ }
368
+
369
+ function hitTest(x, y) {
370
+ var best = null;
371
+ var bestD = Infinity;
372
+ drawn.forEach(function (d) {
373
+ var dx = d.x - x;
374
+ var dy = d.y - y;
375
+ var dist = Math.sqrt(dx * dx + dy * dy);
376
+ if (dist <= d.r && dist < bestD) { best = d; bestD = dist; }
377
+ });
378
+ return best;
379
+ }
380
+
381
+ // Panel rendering.
382
+ function fileButton(i) {
383
+ var b = el('button', { type: 'button' });
384
+ b.appendChild(el('span', { class: 'fname' }, base(files[i].path)));
385
+ b.appendChild(el('span', { class: 'fdir' }, dir(files[i].path)));
386
+ b.addEventListener('click', function () { openFile(i); });
387
+ return el('li').appendChild(b).parentNode;
388
+ }
389
+ function fileList(title, list, cap) {
390
+ panel.appendChild(el('h3', null, title + ' (' + list.length + ')'));
391
+ if (!list.length) { panel.appendChild(el('div', { class: 'more' }, t('none'))); return; }
392
+ var ul = el('ul', { class: 'plist' });
393
+ list.slice(0, cap).forEach(function (i) { ul.appendChild(fileButton(i)); });
394
+ panel.appendChild(ul);
395
+ if (list.length > cap) panel.appendChild(el('div', { class: 'more' }, t('more')(list.length - cap)));
396
+ }
397
+ // route → handler → views · models → tables, one line per route.
398
+ function requestPathList(title, list) {
399
+ if (!list.length) return;
400
+ panel.appendChild(el('h3', null, title + ' (' + list.length + ')'));
401
+ var ul = el('ul', { class: 'plist' });
402
+ list.slice(0, 40).forEach(function (r) {
403
+ var li = el('li');
404
+ li.appendChild(el('code', null, r.route));
405
+ var parts = [];
406
+ if (r.handler >= 0) parts.push(base(files[r.handler].path) + (r.handlerSymbol ? '::' + r.handlerSymbol : ''));
407
+ r.views.forEach(function (v) { parts.push(base(files[v].path)); });
408
+ r.models.forEach(function (v) { parts.push(base(files[v].path)); });
409
+ r.tables.forEach(function (tb) { parts.push('⊞ ' + tb); });
410
+ li.appendChild(el('span', { class: 'fdir' }, parts.length ? '→ ' + parts.join(' → ') : '→ ' + t('none')));
411
+ ul.appendChild(li);
412
+ });
413
+ panel.appendChild(ul);
414
+ if (list.length > 40) panel.appendChild(el('div', { class: 'more' }, t('more')(list.length - 40)));
415
+ }
416
+ function kv(pairs) {
417
+ var dl = el('dl', { class: 'kv' });
418
+ pairs.forEach(function (p) { dl.appendChild(el('dt', null, p[0])); dl.appendChild(el('dd', null, p[1])); });
419
+ panel.appendChild(dl);
420
+ }
421
+
422
+ function renderPanel() {
423
+ clear(panel);
424
+ if (state.file >= 0) {
425
+ var f = files[state.file];
426
+ panel.appendChild(el('h2', null, base(f.path)));
427
+ panel.appendChild(el('div', { class: 'meta' }, dir(f.path)));
428
+ kv([
429
+ [t('area'), f.area >= 0 ? areas[f.area].name : '—'],
430
+ [t('role'), STR[lang].roles[f.role]],
431
+ [t('guesses'), String(f.guesses)],
432
+ [t('outcome'), outcomeOf[state.file] === undefined ? '—' : String(outcomeOf[state.file])],
433
+ [t('state'), data.meta.state]
434
+ ]);
435
+ fileList(t('usedBy'), inOf[state.file].map(function (x) { return x[0]; }), 30);
436
+ fileList(t('uses'), outOf[state.file].map(function (x) { return x[0]; }), 30);
437
+ fileList(t('tests'), f.tests, 20);
438
+ requestPathList(t('requestPaths'), (model.analyses.requestPaths || []).filter(function (r) { return r.file === state.file; }));
439
+ requestPathList(t('reachedBy'), (model.analyses.requestPaths || []).filter(function (r) { return r.handler === state.file; }));
440
+ panel.appendChild(el('h3', null, t('symbols') + ' (' + (f.symbols.length + f.symbolsOmitted) + ')'));
441
+ var ul = el('ul', { class: 'plist' });
442
+ f.symbols.forEach(function (s) {
443
+ var li = el('li');
444
+ li.appendChild(el('code', null, s[0]));
445
+ li.appendChild(el('span', { class: 'meta' }, ' ' + s[1] + ' · ' + s[2]));
446
+ ul.appendChild(li);
447
+ });
448
+ panel.appendChild(ul);
449
+ if (f.symbolsOmitted) panel.appendChild(el('div', { class: 'more' }, t('more')(f.symbolsOmitted)));
450
+ if (f.externals.length) {
451
+ panel.appendChild(el('h3', null, t('externals') + ' (' + f.externals.length + ')'));
452
+ var ex = el('ul', { class: 'plist' });
453
+ f.externals.slice(0, 30).forEach(function (x) { ex.appendChild(el('li', null, x)); });
454
+ panel.appendChild(ex);
455
+ }
456
+ return;
457
+ }
458
+ if (state.area >= 0) {
459
+ var a = areas[state.area];
460
+ var h = el('h2');
461
+ h.appendChild(swatch(state.area));
462
+ h.appendChild(document.createTextNode(a.name));
463
+ panel.appendChild(h);
464
+ panel.appendChild(el('div', { class: 'meta' }, t('files')(a.files.length)));
465
+ kv([
466
+ [t('hub'), files[a.hub].path],
467
+ [t('cohesion'), Math.round(a.cohesion * 100) + ' %'],
468
+ [t('testShare'), Math.round(a.testShare * 100) + ' %'],
469
+ [t('mismatch'), Math.round(a.folderMismatch * 100) + ' %']
470
+ ]);
471
+ fileList(t('files')(a.files.length), a.files.slice().sort(function (x, y) { return files[y].inFiles - files[x].inFiles || x - y; }), 60);
472
+ return;
473
+ }
474
+ if (state.lens === 'folders') { renderFolders(); return; }
475
+ if (state.lens === 'artefacts') { renderArtefacts(); return; }
476
+ panel.appendChild(el('h2', null, data.meta.title));
477
+ panel.appendChild(el('div', { class: 'meta' }, data.meta.summary));
478
+ panel.appendChild(el('p', { class: 'meta' }, t('hint')));
479
+ if (model.drift) {
480
+ panel.appendChild(el('h3', null, t('drift')));
481
+ var dr = el('ul', { class: 'plist' });
482
+ model.drift.changed.slice(0, 10).forEach(function (c) { dr.appendChild(el('li', { class: 'fdir' }, areas[c[0]].name + ' +' + c[1] + ' / −' + c[2])); });
483
+ model.drift.appeared.slice(0, 10).forEach(function (a) { dr.appendChild(el('li', { class: 'fdir' }, '+ ' + areas[a].name)); });
484
+ model.drift.split.slice(0, 10).forEach(function (s) { dr.appendChild(el('li', { class: 'fdir' }, s[0] + ' → ' + s[1].length)); });
485
+ model.drift.merged.slice(0, 10).forEach(function (m) { dr.appendChild(el('li', { class: 'fdir' }, m[1].join(' + ') + ' → ' + areas[m[0]].name)); });
486
+ model.drift.vanished.slice(0, 10).forEach(function (v) { dr.appendChild(el('li', { class: 'fdir' }, '− ' + v)); });
487
+ panel.appendChild(dr);
488
+ }
489
+ if (model.prOverlap && model.prOverlap.conflicts.length) {
490
+ panel.appendChild(el('h3', null, t('prs')));
491
+ var pl = el('ul', { class: 'plist' });
492
+ model.prOverlap.conflicts.slice(0, 15).forEach(function (c) {
493
+ pl.appendChild(el('li', { class: 'fdir' }, '#' + c[0] + ' · #' + c[1] + ' → ' + c[2].map(function (a) { return areas[a].name; }).join(', ')));
494
+ });
495
+ panel.appendChild(pl);
496
+ }
497
+ if (model.diff) {
498
+ panel.appendChild(el('h3', null, t('change')(model.diff.rev, model.diff.changed.length, model.diff.impacted.length)));
499
+ var dl = el('ul', { class: 'plist' });
500
+ model.diff.areas.slice(0, 20).forEach(function (a) {
501
+ var b = el('button', { type: 'button' });
502
+ b.appendChild(swatch(a[0]));
503
+ b.appendChild(document.createTextNode(areas[a[0]].name + ' · ' + a[1] + ' / ' + a[2]));
504
+ b.addEventListener('click', function () { openArea(a[0]); });
505
+ var li = el('li');
506
+ li.appendChild(b);
507
+ dl.appendChild(li);
508
+ });
509
+ panel.appendChild(dl);
510
+ }
511
+ panel.appendChild(el('h3', null, t('areas') + ' (' + areas.length + ')'));
512
+ var list = el('ul', { class: 'plist' });
513
+ areas.forEach(function (a, i) {
514
+ var b = el('button', { type: 'button' });
515
+ b.appendChild(swatch(i));
516
+ b.appendChild(document.createTextNode(a.name + ' · ' + a.files.length));
517
+ b.addEventListener('click', function () { openArea(i); });
518
+ var li = el('li');
519
+ li.appendChild(b);
520
+ list.appendChild(li);
521
+ });
522
+ panel.appendChild(list);
523
+ panel.appendChild(el('h3', null, t('groups')));
524
+ var groups = Object.create(null);
525
+ files.forEach(function (f, i) { if (f.area < 0) (groups[f.role] = groups[f.role] || []).push(i); });
526
+ Object.keys(groups).sort().forEach(function (role) {
527
+ fileList(STR[lang].roles[role], groups[role], 15);
528
+ });
529
+ }
530
+
531
+ function renderFolders() {
532
+ panel.appendChild(el('h2', null, t('lensFolders')));
533
+ panel.appendChild(el('p', { class: 'meta' }, t('folderHint')));
534
+ var byDir = Object.create(null);
535
+ files.forEach(function (f, i) { (byDir[dir(f.path)] = byDir[dir(f.path)] || []).push(i); });
536
+ Object.keys(byDir).sort().forEach(function (d) {
537
+ var list = byDir[d];
538
+ var colours = {};
539
+ list.forEach(function (i) { if (files[i].area >= 0) colours[files[i].area] = true; });
540
+ var details = el('details');
541
+ var summary = el('summary', null, d + ' · ' + list.length);
542
+ Object.keys(colours).slice(0, 12).forEach(function (a) { summary.appendChild(swatch(Number(a))); });
543
+ details.appendChild(summary);
544
+ var ul = el('ul', { class: 'plist' });
545
+ list.forEach(function (i) {
546
+ var b = el('button', { type: 'button' });
547
+ if (files[i].area >= 0) b.appendChild(swatch(files[i].area));
548
+ b.appendChild(document.createTextNode(base(files[i].path)));
549
+ b.addEventListener('click', function () { state.lens = 'areas'; syncLens(); openFile(i); });
550
+ var li = el('li');
551
+ li.appendChild(b);
552
+ ul.appendChild(li);
553
+ });
554
+ details.appendChild(ul);
555
+ panel.appendChild(details);
556
+ });
557
+ }
558
+
559
+ function renderArtefacts() {
560
+ var a = model.artefacts;
561
+ panel.appendChild(el('h2', null, t('lensArtefacts')));
562
+ panel.appendChild(el('p', { class: 'meta' }, t('artefactHint')));
563
+ var inbound = a.nodes.map(function () { return []; });
564
+ var outbound = a.nodes.map(function () { return []; });
565
+ a.edges.forEach(function (e) { inbound[e[1]].push(e); outbound[e[0]].push(e); });
566
+ var order = a.nodes.map(function (_, i) { return i; }).filter(function (i) { return inbound[i].length + outbound[i].length > 0; });
567
+ order.sort(function (x, y) { return inbound[y].length - inbound[x].length || (a.nodes[x][0] < a.nodes[y][0] ? -1 : 1); });
568
+ order.slice(0, 150).forEach(function (i) {
569
+ var n = a.nodes[i];
570
+ var details = el('details');
571
+ details.appendChild(el('summary', null, n[0] + (n[2] ? ' · ' + n[2] : '') + ' · ' + inbound[i].length + ' ←'));
572
+ var ul = el('ul', { class: 'plist' });
573
+ if (n[1] >= 0) {
574
+ var open = el('button', { type: 'button' }, files[n[1]].path);
575
+ open.addEventListener('click', function () { state.lens = 'areas'; syncLens(); openFile(n[1]); });
576
+ var li0 = el('li'); li0.appendChild(open); ul.appendChild(li0);
577
+ }
578
+ outbound[i].concat(inbound[i]).slice(0, 30).forEach(function (e) {
579
+ var out = e[0] === i;
580
+ ul.appendChild(el('li', { class: 'fdir' }, (out ? '→ ' + e[2] + ' ' : '← ' + e[2] + ' ') + a.nodes[out ? e[1] : e[0]][0]));
581
+ });
582
+ details.appendChild(ul);
583
+ panel.appendChild(details);
584
+ });
585
+ if (order.length > 150) panel.appendChild(el('div', { class: 'more' }, t('more')(order.length - 150)));
586
+ }
587
+
588
+ function syncLens() {
589
+ document.getElementById('lens-areas').setAttribute('aria-pressed', String(state.lens === 'areas'));
590
+ document.getElementById('lens-folders').setAttribute('aria-pressed', String(state.lens === 'folders'));
591
+ var la = document.getElementById('lens-artefacts');
592
+ la.hidden = !model.artefacts;
593
+ la.setAttribute('aria-pressed', String(state.lens === 'artefacts'));
594
+ }
595
+
596
+ function renderCrumbs() {
597
+ clear(crumbs);
598
+ var home = el('button', { type: 'button' }, t('map'));
599
+ home.addEventListener('click', goHome);
600
+ crumbs.appendChild(home);
601
+ if (state.area >= 0) {
602
+ crumbs.appendChild(el('span', null, '›'));
603
+ var ab = el('button', { type: 'button', title: areas[state.area].name }, areaLabel(areas[state.area]));
604
+ ab.addEventListener('click', function () { openArea(state.area); });
605
+ crumbs.appendChild(ab);
606
+ }
607
+ if (state.file >= 0) {
608
+ crumbs.appendChild(el('span', null, '›'));
609
+ crumbs.appendChild(el('button', { type: 'button', title: files[state.file].path, disabled: 'disabled' }, base(files[state.file].path)));
610
+ }
611
+ }
612
+
613
+ function refresh() {
614
+ renderCrumbs();
615
+ renderPanel();
616
+ draw();
617
+ }
618
+ function resetView() { state.view = { scale: 1, x: 0, y: 0 }; }
619
+ function goHome() { state.level = 'overview'; state.area = -1; state.file = -1; resetView(); refresh(); }
620
+ function openArea(i) { state.level = 'area'; state.area = i; state.file = -1; resetView(); refresh(); }
621
+ function openFile(i) {
622
+ var f = files[i];
623
+ if (f.area >= 0 && (state.level !== 'area' || state.area !== f.area)) {
624
+ state.level = 'area';
625
+ state.area = f.area;
626
+ resetView();
627
+ }
628
+ state.file = i;
629
+ refresh();
630
+ }
631
+
632
+ // Input.
633
+ var drag = null;
634
+ canvas.addEventListener('pointerdown', function (e) {
635
+ drag = { x: e.clientX, y: e.clientY, vx: state.view.x, vy: state.view.y, moved: false };
636
+ canvas.setPointerCapture(e.pointerId);
637
+ });
638
+ canvas.addEventListener('pointermove', function (e) {
639
+ if (!drag) return;
640
+ var dx = e.clientX - drag.x;
641
+ var dy = e.clientY - drag.y;
642
+ if (Math.abs(dx) + Math.abs(dy) > 3) drag.moved = true;
643
+ if (drag.moved) { state.view.x = drag.vx + dx; state.view.y = drag.vy + dy; draw(); }
644
+ });
645
+ canvas.addEventListener('pointerup', function (e) {
646
+ var wasDrag = drag && drag.moved;
647
+ drag = null;
648
+ if (wasDrag) return;
649
+ var r = canvas.getBoundingClientRect();
650
+ var hit = hitTest(e.clientX - r.left, e.clientY - r.top);
651
+ if (!hit) return;
652
+ if (hit.kind === 'area') openArea(hit.idx);
653
+ else openFile(hit.idx);
654
+ });
655
+ canvas.addEventListener('wheel', function (e) {
656
+ e.preventDefault();
657
+ zoom(e.deltaY < 0 ? 1.15 : 1 / 1.15);
658
+ }, { passive: false });
659
+ function zoom(f) {
660
+ state.view.scale = Math.min(12, Math.max(0.3, state.view.scale * f));
661
+ draw();
662
+ }
663
+ document.addEventListener('keydown', function (e) {
664
+ if (e.key === 'Escape') {
665
+ if (document.activeElement === searchInput && searchInput.value) { searchInput.value = ''; clear(results); return; }
666
+ if (state.file >= 0) { state.file = -1; refresh(); } else if (state.area >= 0) goHome();
667
+ }
668
+ });
669
+
670
+ searchInput.addEventListener('input', function () {
671
+ clear(results);
672
+ var q = searchInput.value.trim().toLowerCase();
673
+ if (q.length < 2) return;
674
+ var hits = 0;
675
+ areas.forEach(function (a, i) {
676
+ if (hits >= 30 || a.name.toLowerCase().indexOf(q) < 0) return;
677
+ var b = el('button', { type: 'button' }, '◎ ' + a.name);
678
+ b.addEventListener('click', function () { clear(results); openArea(i); });
679
+ var li = el('li'); li.appendChild(b); results.appendChild(li);
680
+ hits += 1;
681
+ });
682
+ files.forEach(function (f, i) {
683
+ if (hits >= 30 || f.path.toLowerCase().indexOf(q) < 0) return;
684
+ var b = el('button', { type: 'button' }, f.path);
685
+ b.addEventListener('click', function () { clear(results); openFile(i); });
686
+ var li = el('li'); li.appendChild(b); results.appendChild(li);
687
+ hits += 1;
688
+ });
689
+ });
690
+
691
+ function toggle(id, key) {
692
+ var b = document.getElementById(id);
693
+ b.setAttribute('aria-pressed', String(state[key]));
694
+ b.addEventListener('click', function () {
695
+ state[key] = !state[key];
696
+ b.setAttribute('aria-pressed', String(state[key]));
697
+ draw();
698
+ });
699
+ }
700
+ document.getElementById('lens-areas').addEventListener('click', function () { state.lens = 'areas'; syncLens(); refresh(); });
701
+ document.getElementById('lens-folders').addEventListener('click', function () {
702
+ state.lens = 'folders'; state.file = -1; state.area = -1; state.level = 'overview'; syncLens(); refresh();
703
+ });
704
+ document.getElementById('lens-artefacts').addEventListener('click', function () {
705
+ state.lens = 'artefacts'; state.file = -1; state.area = -1; state.level = 'overview'; syncLens(); refresh();
706
+ });
707
+ toggle('t-infra', 'showInfra');
708
+ toggle('t-guess', 'showGuesses');
709
+ toggle('t-tests', 'showTests');
710
+ document.getElementById('z-in').addEventListener('click', function () { zoom(1.25); });
711
+ document.getElementById('z-out').addEventListener('click', function () { zoom(1 / 1.25); });
712
+ document.getElementById('z-fit').addEventListener('click', function () { resetView(); draw(); });
713
+
714
+ var themes = ['system', 'light', 'dark'];
715
+ var theme = 'system';
716
+ function applyTheme() {
717
+ var root = document.documentElement;
718
+ if (theme === 'system') root.removeAttribute('data-theme');
719
+ else root.setAttribute('data-theme', theme);
720
+ var dark = theme === 'dark' || (theme === 'system' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
721
+ root.dataset.resolvedTheme = dark ? 'dark' : 'light';
722
+ document.getElementById('theme').textContent = t('theme') + ': ' + theme;
723
+ }
724
+ document.getElementById('theme').addEventListener('click', function () {
725
+ theme = themes[(themes.indexOf(theme) + 1) % themes.length];
726
+ applyTheme();
727
+ draw();
728
+ });
729
+ document.getElementById('lang').addEventListener('click', function () {
730
+ lang = lang === 'de' ? 'en' : 'de';
731
+ applyStrings();
732
+ refresh();
733
+ });
734
+ function applyStrings() {
735
+ document.documentElement.lang = lang;
736
+ searchInput.setAttribute('placeholder', t('search'));
737
+ searchInput.setAttribute('aria-label', t('search'));
738
+ document.getElementById('lens-areas').textContent = t('lensAreas');
739
+ document.getElementById('lens-folders').textContent = t('lensFolders');
740
+ document.getElementById('lens-artefacts').textContent = t('lensArtefacts');
741
+ document.getElementById('t-infra').textContent = t('infra');
742
+ document.getElementById('t-guess').textContent = t('guessToggle');
743
+ document.getElementById('t-tests').textContent = t('testsToggle');
744
+ document.getElementById('z-in').setAttribute('aria-label', t('zoomIn'));
745
+ document.getElementById('z-out').setAttribute('aria-label', t('zoomOut'));
746
+ document.getElementById('z-fit').textContent = t('reset');
747
+ document.getElementById('lang').textContent = t('lang');
748
+ applyTheme();
749
+ }
750
+
751
+ applyStrings();
752
+ syncLens();
753
+ window.addEventListener('resize', resize);
754
+ resize();
755
+ refresh();
756
+ })();