@event4u/agent-config 17.1.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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +70 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/project/analyze.md +9 -6
- package/dist/agent-src/commands/project/map.md +58 -0
- package/dist/agent-src/commands/project.md +5 -3
- package/dist/agent-src/skills/code-intelligence/SKILL.md +6 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +23 -39
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -13
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +65 -74
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/instructions_observer.chunk.cjs +301 -0
- package/dist/hooks/instructions_observer.chunk.cjs.sha256 +1 -0
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +33058 -22930
- package/package.json +4 -1
- package/src/config/continuity-surface.json +7 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -0
- package/src/config/hook-bundle-budget.json +17 -2
- package/src/config/hook-token-budget.json +1 -1
- package/src/config/packed-binary-manifest.json +27 -0
- package/src/config/skill-body-budget.json +7 -1
- package/src/scripts/_lib/bench_ab_complexity.ts +14 -2
- package/src/scripts/_lib/collector_record.ts +1 -0
- package/src/scripts/_lib/runtime_journal.ts +7 -7
- package/src/scripts/code_graph/build.ts +210 -10
- package/src/scripts/code_graph/cli.ts +168 -46
- package/src/scripts/code_graph/detect.ts +5 -4
- package/src/scripts/code_graph/docs_layer.ts +57 -0
- package/src/scripts/code_graph/extract.ts +58 -13
- package/src/scripts/code_graph/extract_more.ts +314 -0
- package/src/scripts/code_graph/laravel_overlay.ts +454 -0
- package/src/scripts/code_graph/map.ts +846 -0
- package/src/scripts/code_graph/map_model.ts +977 -0
- package/src/scripts/code_graph/map_verify.ts +140 -0
- package/src/scripts/code_graph/map_viewer/viewer.css +278 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +756 -0
- package/src/scripts/code_graph/scip_reader.ts +201 -0
- package/src/scripts/code_graph/sqlite_store.ts +8 -1
- package/src/scripts/code_graph/types.ts +81 -15
- package/src/scripts/code_graph/validate.ts +16 -26
- package/src/scripts/code_graph/verbs.ts +7 -0
- package/src/scripts/hook_chunks.mjs +7 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +20 -0
- package/src/scripts/hooks/dispatch_hook.ts +2 -2
- package/src/scripts/hooks/instructions_observer_hook.ts +25 -6
- package/src/scripts/hooks/lazy_concern.ts +4 -0
- package/src/scripts/install-hooks.sh +5 -0
- package/src/scripts/lint_agents_layout.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +1 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -6
- package/src/scripts/mcp_server/graph_tools.ts +73 -1
- package/src/scripts/mcp_server/tools.ts +19 -36
- package/src/vendor/grammars/README.md +10 -5
- package/src/vendor/grammars/tree-sitter-go.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-java.wasm +0 -0
- 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
|
+
})();
|