@event4u/agent-config 17.1.0 → 17.2.1

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 (73) 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 +92 -0
  5. package/README.md +2 -2
  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/skills/code-intelligence/SKILL.md +6 -0
  10. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  11. package/dist/cli/registry.js +1 -1
  12. package/dist/cli-delegate/cmd_conformance.js +23 -39
  13. package/dist/discovery/deprecation-report.md +1 -1
  14. package/dist/discovery/discovery-manifest.json +39 -13
  15. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  16. package/dist/discovery/discovery-manifest.summary.md +3 -3
  17. package/dist/discovery/dormancy-report.md +1 -1
  18. package/dist/discovery/orphan-report.md +1 -1
  19. package/dist/discovery/packs.json +6 -5
  20. package/dist/discovery/trust-report.md +2 -2
  21. package/dist/discovery/workspaces.json +5 -4
  22. package/dist/hooks/dispatch.js +65 -74
  23. package/dist/hooks/dispatch.sha256 +1 -1
  24. package/dist/hooks/instructions_observer.chunk.cjs +301 -0
  25. package/dist/hooks/instructions_observer.chunk.cjs.sha256 +1 -0
  26. package/dist/mcp/registry-manifest.json +2 -2
  27. package/dist/mcp/server.json +2 -2
  28. package/dist/mcp/server.mjs +33058 -22930
  29. package/package.json +4 -1
  30. package/src/config/continuity-surface.json +7 -0
  31. package/src/config/evaluator-budgets.json +4 -4
  32. package/src/config/gate-violation-baselines.json +3 -3
  33. package/src/config/gitignore-block.txt +6 -0
  34. package/src/config/hook-bundle-budget.json +17 -2
  35. package/src/config/hook-token-budget.json +1 -1
  36. package/src/config/packed-binary-manifest.json +27 -0
  37. package/src/config/skill-body-budget.json +7 -1
  38. package/src/scripts/_lib/bench_ab_complexity.ts +14 -2
  39. package/src/scripts/_lib/collector_record.ts +1 -0
  40. package/src/scripts/_lib/runtime_journal.ts +7 -7
  41. package/src/scripts/code_graph/build.ts +210 -10
  42. package/src/scripts/code_graph/cli.ts +168 -46
  43. package/src/scripts/code_graph/detect.ts +5 -4
  44. package/src/scripts/code_graph/docs_layer.ts +57 -0
  45. package/src/scripts/code_graph/extract.ts +58 -13
  46. package/src/scripts/code_graph/extract_more.ts +314 -0
  47. package/src/scripts/code_graph/laravel_overlay.ts +454 -0
  48. package/src/scripts/code_graph/map.ts +846 -0
  49. package/src/scripts/code_graph/map_model.ts +977 -0
  50. package/src/scripts/code_graph/map_verify.ts +140 -0
  51. package/src/scripts/code_graph/map_viewer/viewer.css +278 -0
  52. package/src/scripts/code_graph/map_viewer/viewer.js +756 -0
  53. package/src/scripts/code_graph/scip_reader.ts +201 -0
  54. package/src/scripts/code_graph/sqlite_store.ts +8 -1
  55. package/src/scripts/code_graph/types.ts +81 -15
  56. package/src/scripts/code_graph/validate.ts +16 -26
  57. package/src/scripts/code_graph/verbs.ts +7 -0
  58. package/src/scripts/hook_chunks.mjs +7 -0
  59. package/src/scripts/hook_manifest.json +1 -1
  60. package/src/scripts/hook_manifest.yaml +20 -0
  61. package/src/scripts/hooks/dispatch_hook.ts +2 -2
  62. package/src/scripts/hooks/instructions_observer_hook.ts +25 -6
  63. package/src/scripts/hooks/lazy_concern.ts +4 -0
  64. package/src/scripts/install-hooks.sh +5 -0
  65. package/src/scripts/lint_agents_layout.ts +2 -0
  66. package/src/scripts/lint_hook_manifest.ts +1 -0
  67. package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -6
  68. package/src/scripts/mcp_server/graph_tools.ts +73 -1
  69. package/src/scripts/mcp_server/tools.ts +19 -36
  70. package/src/vendor/grammars/README.md +10 -5
  71. package/src/vendor/grammars/tree-sitter-go.wasm +0 -0
  72. package/src/vendor/grammars/tree-sitter-java.wasm +0 -0
  73. package/src/vendor/grammars/tree-sitter-python.wasm +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
+ })();