@kage-core/kage-graph-mcp 1.0.0 → 1.1.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.
package/viewer/app.js ADDED
@@ -0,0 +1,1727 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ var state = {
5
+ graph: null,
6
+ entities: [],
7
+ edges: [],
8
+ episodesById: new Map(),
9
+ entityById: new Map(),
10
+ positions: new Map(),
11
+ visibleEntityIds: new Set(),
12
+ visibleEdgeIds: new Set(),
13
+ selected: null,
14
+ metrics: null,
15
+ pendingPackets: [],
16
+ reviewText: "",
17
+ viewBox: { x: 0, y: 0, width: 1000, height: 660 },
18
+ lastVisibleSignature: "",
19
+ sim: {
20
+ nodes: [],
21
+ edges: [],
22
+ nodeById: new Map(),
23
+ panX: 0,
24
+ panY: 0,
25
+ zoom: 1,
26
+ dragNode: null,
27
+ panning: null,
28
+ hoverNode: null,
29
+ raf: null,
30
+ running: false,
31
+ lastSignature: ""
32
+ },
33
+ pan: null
34
+ };
35
+
36
+ var palette = {
37
+ repo: "#41ff8f",
38
+ memory: "#b88cff",
39
+ path: "#ff6b6b",
40
+ tag: "#ffd166",
41
+ package: "#6ad7ff",
42
+ command: "#9be7c0",
43
+ memory_type: "#41ff8f",
44
+ file: "#6ad7ff",
45
+ symbol: "#b88cff",
46
+ route: "#ff8fab",
47
+ test: "#ffd166",
48
+ external: "#93a4a0",
49
+ script: "#6ad7ff",
50
+ default: "#9be7c0"
51
+ };
52
+
53
+ var els = {
54
+ graphFile: document.getElementById("graphFile"),
55
+ graphSummary: document.getElementById("graphSummary"),
56
+ statusStrip: document.getElementById("statusStrip"),
57
+ autoLoadStatus: document.getElementById("autoLoadStatus"),
58
+ workspaceMode: document.getElementById("workspaceMode"),
59
+ graphSubhead: document.getElementById("graphSubhead"),
60
+ selectionStatus: document.getElementById("selectionStatus"),
61
+ searchInput: document.getElementById("searchInput"),
62
+ viewMode: document.getElementById("viewMode"),
63
+ typeFilter: document.getElementById("typeFilter"),
64
+ relationFilter: document.getElementById("relationFilter"),
65
+ scopeFilter: document.getElementById("scopeFilter"),
66
+ maxNodes: document.getElementById("maxNodes"),
67
+ showDependencies: document.getElementById("showDependencies"),
68
+ resetView: document.getElementById("resetView"),
69
+ zoomOut: document.getElementById("zoomOut"),
70
+ zoomIn: document.getElementById("zoomIn"),
71
+ fitView: document.getElementById("fitView"),
72
+ canvas: document.getElementById("graphCanvas"),
73
+ tooltip: document.getElementById("graphTooltip"),
74
+ svg: document.getElementById("graphSvg"),
75
+ nodeLayer: document.getElementById("nodeLayer"),
76
+ edgeLayer: document.getElementById("edgeLayer"),
77
+ emptyState: document.getElementById("emptyState"),
78
+ selectionDetails: document.getElementById("selectionDetails"),
79
+ entityList: document.getElementById("entityList"),
80
+ edgeList: document.getElementById("edgeList"),
81
+ metricsSummary: document.getElementById("metricsSummary"),
82
+ entityCount: document.getElementById("entityCount"),
83
+ edgeCount: document.getElementById("edgeCount"),
84
+ reviewCount: document.getElementById("reviewCount"),
85
+ reviewList: document.getElementById("reviewList"),
86
+ proofStatus: document.getElementById("proofStatus"),
87
+ proofList: document.getElementById("proofList")
88
+ };
89
+
90
+ els.graphFile.addEventListener("change", handleFile);
91
+ els.searchInput.addEventListener("input", render);
92
+ els.viewMode.addEventListener("change", render);
93
+ els.typeFilter.addEventListener("change", render);
94
+ els.relationFilter.addEventListener("change", render);
95
+ els.scopeFilter.addEventListener("change", render);
96
+ els.maxNodes.addEventListener("change", render);
97
+ els.showDependencies.addEventListener("change", render);
98
+ els.zoomOut.addEventListener("click", function () { zoomCanvas(0.82); });
99
+ els.zoomIn.addEventListener("click", function () { zoomCanvas(1.22); });
100
+ els.fitView.addEventListener("click", function () { fitCanvas(); drawCanvasGraph(); });
101
+ els.canvas.addEventListener("mousedown", startCanvasPointer);
102
+ els.canvas.addEventListener("mousemove", moveCanvasPointer);
103
+ els.canvas.addEventListener("mouseup", endCanvasPointer);
104
+ els.canvas.addEventListener("mouseleave", leaveCanvasPointer);
105
+ els.canvas.addEventListener("wheel", handleCanvasWheel, { passive: false });
106
+ els.canvas.addEventListener("dblclick", handleCanvasDoubleClick);
107
+ els.svg.addEventListener("mousedown", startPan);
108
+ els.svg.addEventListener("click", handleSvgClick);
109
+ els.svg.addEventListener("wheel", handleWheelZoom, { passive: false });
110
+ window.addEventListener("mousemove", continuePan);
111
+ window.addEventListener("mouseup", endPan);
112
+ window.addEventListener("resize", function () {
113
+ resizeCanvas();
114
+ fitCanvas();
115
+ drawCanvasGraph();
116
+ });
117
+ els.resetView.addEventListener("click", function () {
118
+ els.searchInput.value = "";
119
+ els.viewMode.value = "combined";
120
+ els.typeFilter.value = "";
121
+ els.relationFilter.value = "";
122
+ els.scopeFilter.value = "signal";
123
+ els.maxNodes.value = "90";
124
+ els.showDependencies.checked = false;
125
+ state.selected = null;
126
+ state.lastVisibleSignature = "";
127
+ render();
128
+ });
129
+ loadFromUrlParams();
130
+
131
+ function handleFile(event) {
132
+ var files = Array.from(event.target.files || []);
133
+ if (!files.length) return;
134
+ Promise.all(files.map(readJsonFile)).then(function (items) {
135
+ state.metrics = items.map(function (item) { return item.graph; }).find(isMetricsGraph) || null;
136
+ var graphItems = items.filter(function (item) { return !isMetricsGraph(item.graph); });
137
+ if (!graphItems.length && state.metrics) {
138
+ state.graph = { entities: [], edges: [], episodes: [] };
139
+ state.entities = [];
140
+ state.edges = [];
141
+ state.entityById = new Map();
142
+ state.episodesById = new Map();
143
+ els.emptyState.classList.add("hidden");
144
+ els.graphSummary.textContent = "Metrics loaded.";
145
+ renderMetrics();
146
+ return;
147
+ }
148
+ var merged = mergeNormalizedGraphs(graphItems.map(function (item) { return normalizeGraph(item.graph); }));
149
+ loadNormalizedGraph(merged, graphItems.map(function (item) { return item.fileName; }).join(", "));
150
+ }).catch(function (error) {
151
+ showError("Could not load JSON: " + error.message);
152
+ });
153
+ }
154
+
155
+ function loadGraph(graph, fileName) {
156
+ loadNormalizedGraph(normalizeGraph(graph), fileName);
157
+ }
158
+
159
+ function loadNormalizedGraph(normalized, fileName) {
160
+ var entities = normalized.entities;
161
+ var edges = normalized.edges;
162
+ var episodes = normalized.episodes;
163
+
164
+ state.graph = normalized;
165
+ state.entities = entities;
166
+ state.edges = edges;
167
+ state.entityById = new Map(entities.map(function (entity) {
168
+ return [entity.id, entity];
169
+ }));
170
+ state.episodesById = new Map(episodes.map(function (episode) {
171
+ return [episode.id, episode];
172
+ }));
173
+ state.selected = null;
174
+ state.lastVisibleSignature = "";
175
+
176
+ populateFilters();
177
+ els.emptyState.classList.add("hidden");
178
+ els.graphSummary.textContent = fileName + " loaded: " + entities.length + " nodes, " + edges.length + " relations.";
179
+ render();
180
+ }
181
+
182
+ function loadFromUrlParams() {
183
+ var params = new URLSearchParams(window.location.search);
184
+ var graphPaths = []
185
+ .concat(params.getAll("graph"))
186
+ .concat(params.getAll("code"))
187
+ .flatMap(function (value) { return String(value || "").split(","); })
188
+ .map(function (value) { return value.trim(); })
189
+ .filter(Boolean);
190
+ var metricsPath = params.get("metrics");
191
+ var reviewPath = params.get("review");
192
+ var pendingPath = params.get("pending");
193
+ var inferredRoot = inferMemoryRoot(graphPaths[0] || "");
194
+ if (!reviewPath && inferredRoot) reviewPath = inferredRoot + "/review/memory-review.md";
195
+ if (!pendingPath && inferredRoot) pendingPath = inferredRoot + "/pending";
196
+ var jobs = [];
197
+ if (metricsPath) jobs.push(fetchJson(metricsPath).then(function (metrics) { state.metrics = metrics; }));
198
+ if (reviewPath) jobs.push(fetchText(reviewPath).then(function (text) { state.reviewText = text; }).catch(function () { state.reviewText = ""; }));
199
+ if (pendingPath) jobs.push(loadPending(pendingPath).then(function (packets) { state.pendingPackets = packets; }));
200
+ if (!graphPaths.length && !jobs.length) {
201
+ setAutoLoad("manual mode", false);
202
+ return;
203
+ }
204
+ setAutoLoad("loading project graph", false);
205
+ Promise.all(graphPaths.map(function (path) {
206
+ return fetch(path).then(function (response) {
207
+ if (!response.ok) throw new Error(response.status + " " + path);
208
+ return response.json().then(function (graph) { return { fileName: path.split("/").pop() || path, graph: graph }; });
209
+ });
210
+ }).concat(jobs)).then(function (items) {
211
+ var graphItems = items.filter(Boolean);
212
+ if (!graphItems.length) {
213
+ loadNormalizedGraph({ entities: [], edges: [], episodes: [] }, "project metrics");
214
+ setAutoLoad("project console loaded", true);
215
+ return;
216
+ }
217
+ var merged = mergeNormalizedGraphs(graphItems.map(function (item) { return normalizeGraph(item.graph); }));
218
+ loadNormalizedGraph(merged, graphItems.map(function (item) { return item.fileName; }).join(", ") || "metrics");
219
+ setAutoLoad("project console loaded", true);
220
+ }).catch(function (error) {
221
+ setAutoLoad("auto-load failed", false);
222
+ showError("Could not auto-load graph: " + error.message);
223
+ });
224
+ }
225
+
226
+ function inferMemoryRoot(path) {
227
+ var marker = "/.agent_memory/";
228
+ var index = path.indexOf(marker);
229
+ if (index === -1) return "";
230
+ return path.slice(0, index + marker.length - 1);
231
+ }
232
+
233
+ function fetchJson(path) {
234
+ return fetch(path).then(function (response) {
235
+ if (!response.ok) throw new Error(response.status + " " + path);
236
+ return response.json();
237
+ });
238
+ }
239
+
240
+ function fetchText(path) {
241
+ return fetch(path).then(function (response) {
242
+ if (!response.ok) throw new Error(response.status + " " + path);
243
+ return response.text();
244
+ });
245
+ }
246
+
247
+ function loadPending(path) {
248
+ return fetchJson(path).then(function (listing) {
249
+ var files = listing && Array.isArray(listing.files) ? listing.files : [];
250
+ return Promise.all(files.map(function (file) { return fetchJson(file.path); }));
251
+ }).catch(function () { return []; });
252
+ }
253
+
254
+ function setAutoLoad(text, ok) {
255
+ if (!els.autoLoadStatus) return;
256
+ els.autoLoadStatus.textContent = "auto-load: " + text;
257
+ els.autoLoadStatus.className = "autoload-status " + (ok ? "ok" : "");
258
+ }
259
+
260
+ function readJsonFile(file) {
261
+ return new Promise(function (resolve, reject) {
262
+ var reader = new FileReader();
263
+ reader.onload = function () {
264
+ try {
265
+ resolve({ fileName: file.name, graph: JSON.parse(String(reader.result || "{}")) });
266
+ } catch (error) {
267
+ reject(error);
268
+ }
269
+ };
270
+ reader.onerror = function () { reject(new Error("Could not read " + file.name + ".")); };
271
+ reader.readAsText(file);
272
+ });
273
+ }
274
+
275
+ function isMetricsGraph(graph) {
276
+ return graph && graph.code_graph && graph.memory_graph && graph.harness;
277
+ }
278
+
279
+ function mergeNormalizedGraphs(graphs) {
280
+ var entities = new Map();
281
+ var edges = new Map();
282
+ var episodes = new Map();
283
+ graphs.forEach(function (graph) {
284
+ graph.entities.forEach(function (entity) { entities.set(entity.id, entity); });
285
+ graph.edges.forEach(function (edge) { edges.set(edge.id, edge); });
286
+ graph.episodes.forEach(function (episode) { episodes.set(episode.id, episode); });
287
+ });
288
+ return { entities: Array.from(entities.values()), edges: Array.from(edges.values()), episodes: Array.from(episodes.values()) };
289
+ }
290
+
291
+ function normalizeGraph(graph) {
292
+ if (Array.isArray(graph.entities) && Array.isArray(graph.edges)) {
293
+ return {
294
+ entities: graph.entities.map(function (entity) { return Object.assign({ graph_kind: "memory" }, entity); }),
295
+ edges: graph.edges.map(function (edge) { return Object.assign({ graph_kind: "memory" }, edge); }),
296
+ episodes: Array.isArray(graph.episodes) ? graph.episodes : []
297
+ };
298
+ }
299
+
300
+ if (Array.isArray(graph.files) || Array.isArray(graph.symbols)) {
301
+ return normalizeCodeGraph(graph);
302
+ }
303
+
304
+ return { entities: [], edges: [], episodes: [] };
305
+ }
306
+
307
+ function normalizeCodeGraph(graph) {
308
+ var entities = [];
309
+ var edges = [];
310
+ var seen = new Set();
311
+ var addEntity = function (entity) {
312
+ if (seen.has(entity.id)) return;
313
+ seen.add(entity.id);
314
+ entities.push(entity);
315
+ };
316
+ var addEdge = function (from, to, relation, fact, source) {
317
+ if (!from || !to) return;
318
+ edges.push({
319
+ id: relation + ":" + from + ":" + to + ":" + edges.length,
320
+ from: from,
321
+ to: to,
322
+ relation: relation,
323
+ fact: fact,
324
+ confidence: 1,
325
+ evidence: [],
326
+ commit: graph.repo_state && graph.repo_state.head,
327
+ source: source || "code_graph",
328
+ graph_kind: "code"
329
+ });
330
+ };
331
+
332
+ (graph.files || []).forEach(function (file) {
333
+ addEntity({
334
+ id: "file:" + file.path,
335
+ type: "file",
336
+ graph_kind: "code",
337
+ name: file.path,
338
+ summary: file.kind + " file, " + file.language + ", " + file.line_count + " lines",
339
+ aliases: [file.hash],
340
+ evidence: []
341
+ });
342
+ });
343
+
344
+ (graph.symbols || []).forEach(function (symbol) {
345
+ addEntity({
346
+ id: symbol.id,
347
+ type: symbol.kind === "test" ? "test" : "symbol",
348
+ graph_kind: "code",
349
+ name: symbol.name,
350
+ summary: symbol.kind + " in " + symbol.path + ":" + symbol.line + (symbol.signature ? "\n" + symbol.signature : ""),
351
+ aliases: [symbol.path],
352
+ evidence: []
353
+ });
354
+ addEdge("file:" + symbol.path, symbol.id, "defines_symbol", symbol.path + " defines " + symbol.kind + " " + symbol.name + ".", "symbols");
355
+ });
356
+
357
+ (graph.imports || []).forEach(function (item) {
358
+ if (item.to_path) addEdge("file:" + item.from_path, "file:" + item.to_path, "imports", item.from_path + " imports " + item.specifier + ".", "imports");
359
+ else {
360
+ var externalId = "external:" + item.specifier;
361
+ addEntity({ id: externalId, type: "external", graph_kind: "code", name: item.specifier, summary: "External import", aliases: [], evidence: [] });
362
+ addEdge("file:" + item.from_path, externalId, "imports_external", item.from_path + " imports " + item.specifier + ".", "imports");
363
+ }
364
+ });
365
+
366
+ (graph.calls || []).forEach(function (call) {
367
+ addEdge(call.from_symbol || "file:" + call.path, call.to_symbol, "calls", call.path + ":" + call.line + " calls target symbol.", "calls");
368
+ });
369
+
370
+ (graph.routes || []).forEach(function (route) {
371
+ addEntity({
372
+ id: route.id,
373
+ type: "route",
374
+ graph_kind: "code",
375
+ name: route.method + " " + route.path,
376
+ summary: route.framework + " route in " + route.file_path + ":" + route.line,
377
+ aliases: [route.file_path],
378
+ evidence: []
379
+ });
380
+ addEdge("file:" + route.file_path, route.id, "defines_route", route.file_path + " defines " + route.method + " " + route.path + ".", "routes");
381
+ if (route.handler_symbol) addEdge(route.id, route.handler_symbol, "handled_by", route.method + " " + route.path + " is handled by a symbol.", "routes");
382
+ });
383
+
384
+ (graph.tests || []).forEach(function (test) {
385
+ addEdge(test.test_symbol, test.covers_symbol ? symbolEntityId(graph, test.covers_symbol, test.covers_path) : "file:" + test.covers_path, "covers", test.title + " covers " + (test.covers_symbol || test.covers_path || "unknown") + ".", "tests");
386
+ });
387
+
388
+ (graph.packages || []).forEach(function (pkg) {
389
+ var id = pkg.kind + ":" + pkg.name;
390
+ addEntity({ id: id, type: pkg.kind === "script" ? "script" : "external", graph_kind: "code", name: pkg.name, summary: pkg.kind + ": " + pkg.version, aliases: [], evidence: [] });
391
+ });
392
+
393
+ return { entities: entities, edges: edges, episodes: [] };
394
+ }
395
+
396
+ function symbolEntityId(graph, name, path) {
397
+ var match = (graph.symbols || []).find(function (symbol) {
398
+ return symbol.name === name && (!path || symbol.path === path);
399
+ });
400
+ return match ? match.id : null;
401
+ }
402
+
403
+ function populateFilters() {
404
+ replaceOptions(els.typeFilter, "All types", unique(state.entities.map(function (entity) {
405
+ return entity.type || "unknown";
406
+ })));
407
+ replaceOptions(els.relationFilter, "All relations", unique(state.edges.map(function (edge) {
408
+ return edge.relation || "related";
409
+ })));
410
+ }
411
+
412
+ function replaceOptions(select, label, values) {
413
+ select.textContent = "";
414
+ select.appendChild(new Option(label, ""));
415
+ values.sort().forEach(function (value) {
416
+ select.appendChild(new Option(value, value));
417
+ });
418
+ }
419
+
420
+ function layoutGraph(visibleIds) {
421
+ state.positions = new Map();
422
+ var candidates = state.entities.filter(function (entity) {
423
+ return !visibleIds || visibleIds.has(entity.id);
424
+ });
425
+ var lanes = [
426
+ { name: "memory", x: 170, y: 96, step: 82, columns: 10, match: function (entity) { return entity.graph_kind === "memory"; } },
427
+ { name: "files", x: 470, y: 86, step: 78, columns: 10, match: function (entity) { return entity.graph_kind === "code" && entity.type === "file"; } },
428
+ { name: "flow", x: 760, y: 96, step: 76, columns: 10, match: function (entity) { return entity.graph_kind === "code" && ["symbol", "route", "test"].indexOf(entity.type) !== -1; } },
429
+ { name: "external", x: 1040, y: 122, step: 78, columns: 8, match: function (entity) { return isDependencyEntity(entity) || (entity.graph_kind === "code" && ["external", "script"].indexOf(entity.type) !== -1); } },
430
+ { name: "other", x: 760, y: 560, step: 78, columns: 8, match: function (entity) { return ["memory", "code"].indexOf(entity.graph_kind) === -1 || (entity.graph_kind === "code" && ["file", "symbol", "route", "test", "external", "script"].indexOf(entity.type) === -1); } }
431
+ ];
432
+ var placed = new Set();
433
+ lanes.forEach(function (lane) {
434
+ var bucket = candidates.filter(function (entity) {
435
+ return !placed.has(entity.id) && lane.match(entity);
436
+ }).sort(function (a, b) {
437
+ return entityImportance(b) - entityImportance(a) || displayName(a).localeCompare(displayName(b));
438
+ });
439
+ if (!bucket.length) return;
440
+ bucket.forEach(function (entity, index) {
441
+ placed.add(entity.id);
442
+ var column = Math.floor(index / lane.columns);
443
+ var row = index % lane.columns;
444
+ var xOffset = column * 228;
445
+ var yJitter = column % 2 ? 26 : 0;
446
+ state.positions.set(entity.id, {
447
+ x: lane.x + xOffset,
448
+ y: lane.y + row * lane.step + yJitter
449
+ });
450
+ });
451
+ });
452
+ }
453
+
454
+ function render() {
455
+ if (!state.graph) return;
456
+
457
+ var query = normalize(els.searchInput.value);
458
+ var mode = els.viewMode.value;
459
+ var type = els.typeFilter.value;
460
+ var relation = els.relationFilter.value;
461
+ var matchedEntityIds = new Set();
462
+ var matchedEdgeIds = new Set();
463
+
464
+ state.entities.forEach(function (entity) {
465
+ if (mode !== "combined" && entity.graph_kind !== mode) return;
466
+ var passesType = !type || entity.type === type;
467
+ var passesSearch = !query || searchableText(entity).indexOf(query) !== -1;
468
+ if (passesType && passesSearch) matchedEntityIds.add(entity.id);
469
+ });
470
+
471
+ state.edges.forEach(function (edge) {
472
+ if (mode !== "combined" && edge.graph_kind !== mode) return;
473
+ var fromMatched = matchedEntityIds.has(edge.from);
474
+ var toMatched = matchedEntityIds.has(edge.to);
475
+ var edgeMatchesSearch = !query || searchableText(edge).indexOf(query) !== -1;
476
+ var passesRelation = !relation || edge.relation === relation;
477
+ if (passesRelation && (edgeMatchesSearch || fromMatched || toMatched)) {
478
+ matchedEdgeIds.add(edge.id);
479
+ if (!type) {
480
+ if (state.entityById.has(edge.from)) matchedEntityIds.add(edge.from);
481
+ if (state.entityById.has(edge.to)) matchedEntityIds.add(edge.to);
482
+ }
483
+ }
484
+ });
485
+
486
+ if (!query && !type && !relation) {
487
+ matchedEntityIds = new Set(state.entities.filter(function (entity) { return mode === "combined" || entity.graph_kind === mode; }).map(function (entity) { return entity.id; }));
488
+ matchedEdgeIds = new Set(state.edges.filter(function (edge) { return mode === "combined" || edge.graph_kind === mode; }).map(function (edge) { return edge.id; }));
489
+ }
490
+
491
+ var visible = refineVisibleGraph(matchedEntityIds, matchedEdgeIds, {
492
+ query: query,
493
+ type: type,
494
+ relation: relation,
495
+ scope: els.scopeFilter.value,
496
+ maxNodes: Number(els.maxNodes.value || 90),
497
+ showDependencies: els.showDependencies.checked
498
+ });
499
+
500
+ state.visibleEntityIds = visible.entities;
501
+ state.visibleEdgeIds = visible.edges;
502
+ layoutGraph(state.visibleEntityIds);
503
+ var nextSignature = visibleSignature(state.visibleEntityIds, state.visibleEdgeIds);
504
+ var graphChanged = nextSignature !== state.lastVisibleSignature;
505
+ state.lastVisibleSignature = nextSignature;
506
+
507
+ renderCanvasGraph(graphChanged);
508
+ renderLists();
509
+ renderDetails();
510
+ renderMetrics();
511
+ renderReviewQueue();
512
+ renderProof();
513
+ }
514
+
515
+ function refineVisibleGraph(entityIds, edgeIds, options) {
516
+ var entities = new Set(entityIds);
517
+ var edges = new Set(edgeIds);
518
+
519
+ if (!options.showDependencies) {
520
+ Array.from(entities).forEach(function (id) {
521
+ var entity = state.entityById.get(id);
522
+ if (!entity) return;
523
+ if (isDependencyEntity(entity) && !(options.query && searchableText(entity).indexOf(options.query) !== -1)) {
524
+ entities.delete(id);
525
+ }
526
+ });
527
+ edges = edgesWithVisibleEndpoints(edges, entities);
528
+ }
529
+
530
+ if (options.scope === "focus" && state.selected) {
531
+ var focused = focusSelection(entities, edges, state.selected);
532
+ entities = focused.entities;
533
+ edges = focused.edges;
534
+ } else if (options.scope === "signal" && entities.size > options.maxNodes) {
535
+ var ranked = Array.from(entities).sort(function (a, b) {
536
+ return entityImportance(state.entityById.get(b)) - entityImportance(state.entityById.get(a)) ||
537
+ displayName(state.entityById.get(a)).localeCompare(displayName(state.entityById.get(b)));
538
+ });
539
+ entities = new Set(ranked.slice(0, options.maxNodes));
540
+ if (state.selected && state.selected.kind === "entity") entities.add(state.selected.id);
541
+ edges = edgesWithVisibleEndpoints(edges, entities);
542
+ }
543
+
544
+ if (state.selected && state.selected.kind === "edge" && edges.has(state.selected.id)) {
545
+ var selectedEdge = state.edges.find(function (edge) { return edge.id === state.selected.id; });
546
+ if (selectedEdge) {
547
+ entities.add(selectedEdge.from);
548
+ entities.add(selectedEdge.to);
549
+ }
550
+ }
551
+
552
+ return { entities: entities, edges: edgesWithVisibleEndpoints(edges, entities) };
553
+ }
554
+
555
+ function focusSelection(entityIds, edgeIds, selection) {
556
+ var entities = new Set();
557
+ var edges = new Set();
558
+ if (selection.kind === "entity") {
559
+ entities.add(selection.id);
560
+ state.edges.forEach(function (edge) {
561
+ if (!edgeIds.has(edge.id)) return;
562
+ if (edge.from === selection.id || edge.to === selection.id) {
563
+ edges.add(edge.id);
564
+ entities.add(edge.from);
565
+ entities.add(edge.to);
566
+ }
567
+ });
568
+ } else {
569
+ var selectedEdge = state.edges.find(function (edge) { return edge.id === selection.id; });
570
+ if (selectedEdge && edgeIds.has(selectedEdge.id)) {
571
+ edges.add(selectedEdge.id);
572
+ entities.add(selectedEdge.from);
573
+ entities.add(selectedEdge.to);
574
+ }
575
+ }
576
+ entityIds.forEach(function (id) {
577
+ if (entities.has(id)) return;
578
+ var entity = state.entityById.get(id);
579
+ if (entity && entity.graph_kind === "memory" && entities.size < 16) entities.add(id);
580
+ });
581
+ return { entities: entities.size ? entities : entityIds, edges: edges.size ? edges : edgeIds };
582
+ }
583
+
584
+ function edgesWithVisibleEndpoints(edgeIds, entityIds) {
585
+ return new Set(Array.from(edgeIds).filter(function (id) {
586
+ var edge = state.edges.find(function (candidate) { return candidate.id === id; });
587
+ return edge && entityIds.has(edge.from) && entityIds.has(edge.to);
588
+ }));
589
+ }
590
+
591
+ function renderCanvasGraph(graphChanged) {
592
+ resizeCanvas();
593
+ syncSimulationGraph(graphChanged);
594
+ if (graphChanged) fitCanvas();
595
+ startSimulation();
596
+ drawCanvasGraph();
597
+ }
598
+
599
+ function resizeCanvas() {
600
+ var canvas = els.canvas;
601
+ var ctx = canvas.getContext("2d");
602
+ var rect = canvas.parentElement.getBoundingClientRect();
603
+ var dpr = Math.max(1, window.devicePixelRatio || 1);
604
+ var width = Math.max(320, rect.width);
605
+ var height = Math.max(360, rect.height);
606
+ if (canvas.width !== Math.round(width * dpr) || canvas.height !== Math.round(height * dpr)) {
607
+ canvas.width = Math.round(width * dpr);
608
+ canvas.height = Math.round(height * dpr);
609
+ canvas.style.width = width + "px";
610
+ canvas.style.height = height + "px";
611
+ }
612
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
613
+ }
614
+
615
+ function syncSimulationGraph(forceReset) {
616
+ var existing = state.sim.nodeById;
617
+ var visibleEntities = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
618
+ var nextNodes = visibleEntities.map(function (entity, index) {
619
+ var current = existing.get(entity.id);
620
+ var lanePos = state.positions.get(entity.id);
621
+ var seed = seededPosition(index, visibleEntities.length);
622
+ var degree = degreeOf(entity.id);
623
+ if (!current || forceReset) {
624
+ current = {
625
+ id: entity.id,
626
+ entity: entity,
627
+ x: lanePos ? lanePos.x : seed.x,
628
+ y: lanePos ? lanePos.y : seed.y,
629
+ vx: 0,
630
+ vy: 0,
631
+ r: clamp(9 + degree * 1.7, 10, entity.graph_kind === "memory" ? 25 : 22)
632
+ };
633
+ }
634
+ current.entity = entity;
635
+ current.r = clamp(9 + degree * 1.7, 10, entity.graph_kind === "memory" ? 25 : 22);
636
+ return current;
637
+ });
638
+ state.sim.nodes = nextNodes;
639
+ state.sim.nodeById = new Map(nextNodes.map(function (node) { return [node.id, node]; }));
640
+ state.sim.edges = state.edges.filter(function (edge) {
641
+ return state.visibleEdgeIds.has(edge.id) && state.sim.nodeById.has(edge.from) && state.sim.nodeById.has(edge.to);
642
+ });
643
+ }
644
+
645
+ function seededPosition(index, total) {
646
+ var angle = (Math.PI * 2 * index) / Math.max(1, total);
647
+ var radius = 220 + Math.min(180, total * 2);
648
+ return {
649
+ x: Math.cos(angle) * radius + 520,
650
+ y: Math.sin(angle) * radius + 340
651
+ };
652
+ }
653
+
654
+ function startSimulation() {
655
+ if (state.sim.running) return;
656
+ state.sim.running = true;
657
+ state.sim.raf = window.requestAnimationFrame(simulationTick);
658
+ }
659
+
660
+ function simulationTick() {
661
+ if (!state.sim.running) return;
662
+ stepSimulation();
663
+ drawCanvasGraph();
664
+ state.sim.raf = window.requestAnimationFrame(simulationTick);
665
+ }
666
+
667
+ function stepSimulation() {
668
+ var nodes = state.sim.nodes;
669
+ if (!nodes.length) return;
670
+ var nodeMap = state.sim.nodeById;
671
+ var count = nodes.length;
672
+ var repulsion = count > 120 ? 3600 : count > 70 ? 2500 : 1700;
673
+ var attraction = count > 100 ? 0.006 : 0.010;
674
+ var centerGravity = count > 100 ? 0.006 : 0.011;
675
+ var laneGravity = 0.012;
676
+
677
+ nodes.forEach(function (node, index) {
678
+ if (state.sim.dragNode === node) return;
679
+ var fx = 0;
680
+ var fy = 0;
681
+ for (var j = 0; j < nodes.length; j++) {
682
+ if (index === j) continue;
683
+ var other = nodes[j];
684
+ var dx = node.x - other.x;
685
+ var dy = node.y - other.y;
686
+ var dist = Math.max(18, Math.sqrt(dx * dx + dy * dy));
687
+ var force = repulsion / (dist * dist);
688
+ fx += (dx / dist) * force;
689
+ fy += (dy / dist) * force;
690
+ }
691
+ var lane = state.positions.get(node.id);
692
+ if (lane) {
693
+ fx += (lane.x - node.x) * laneGravity;
694
+ fy += (lane.y - node.y) * laneGravity;
695
+ }
696
+ fx += (620 - node.x) * centerGravity * 0.15;
697
+ fy += (360 - node.y) * centerGravity * 0.15;
698
+ node.vx = (node.vx + fx) * 0.82;
699
+ node.vy = (node.vy + fy) * 0.82;
700
+ });
701
+
702
+ state.sim.edges.forEach(function (edge) {
703
+ var from = nodeMap.get(edge.from);
704
+ var to = nodeMap.get(edge.to);
705
+ if (!from || !to) return;
706
+ var dx = to.x - from.x;
707
+ var dy = to.y - from.y;
708
+ var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
709
+ var target = edge.graph_kind === "memory" ? 145 : 120;
710
+ var force = (dist - target) * attraction * (edge.confidence == null ? 1 : clamp(edge.confidence, 0.35, 1.2));
711
+ var fx = (dx / dist) * force;
712
+ var fy = (dy / dist) * force;
713
+ if (state.sim.dragNode !== from) {
714
+ from.vx += fx;
715
+ from.vy += fy;
716
+ }
717
+ if (state.sim.dragNode !== to) {
718
+ to.vx -= fx;
719
+ to.vy -= fy;
720
+ }
721
+ });
722
+
723
+ nodes.forEach(function (node) {
724
+ if (state.sim.dragNode === node) return;
725
+ node.x += clamp(node.vx, -8, 8);
726
+ node.y += clamp(node.vy, -8, 8);
727
+ });
728
+ }
729
+
730
+ function drawCanvasGraph() {
731
+ var canvas = els.canvas;
732
+ var ctx = canvas.getContext("2d");
733
+ var width = canvas.width / Math.max(1, window.devicePixelRatio || 1);
734
+ var height = canvas.height / Math.max(1, window.devicePixelRatio || 1);
735
+ ctx.clearRect(0, 0, width, height);
736
+ drawCanvasGrid(ctx, width, height);
737
+ ctx.save();
738
+ ctx.translate(state.sim.panX, state.sim.panY);
739
+ ctx.scale(state.sim.zoom, state.sim.zoom);
740
+ drawCanvasEdges(ctx);
741
+ drawCanvasNodes(ctx);
742
+ ctx.restore();
743
+
744
+ if (!state.sim.nodes.length) {
745
+ ctx.fillStyle = "#6ea77d";
746
+ ctx.font = "13px ui-monospace, Menlo, monospace";
747
+ ctx.textAlign = "center";
748
+ ctx.fillText("No graph data visible.", width / 2, height / 2);
749
+ }
750
+ }
751
+
752
+ function drawCanvasGrid(ctx, width, height) {
753
+ ctx.save();
754
+ ctx.fillStyle = "#020503";
755
+ ctx.fillRect(0, 0, width, height);
756
+ ctx.strokeStyle = "rgba(65,255,143,0.045)";
757
+ ctx.lineWidth = 1;
758
+ var grid = 28;
759
+ for (var x = 0; x < width; x += grid) {
760
+ ctx.beginPath();
761
+ ctx.moveTo(x, 0);
762
+ ctx.lineTo(x, height);
763
+ ctx.stroke();
764
+ }
765
+ for (var y = 0; y < height; y += grid) {
766
+ ctx.beginPath();
767
+ ctx.moveTo(0, y);
768
+ ctx.lineTo(width, y);
769
+ ctx.stroke();
770
+ }
771
+ ctx.restore();
772
+ }
773
+
774
+ function drawCanvasEdges(ctx) {
775
+ var nodeMap = state.sim.nodeById;
776
+ var focusId = focusedCanvasNodeId();
777
+ var query = normalize(els.searchInput.value);
778
+ var dense = state.sim.nodes.length > 55;
779
+ state.sim.edges.forEach(function (edge) {
780
+ var from = nodeMap.get(edge.from);
781
+ var to = nodeMap.get(edge.to);
782
+ if (!from || !to) return;
783
+ var connected = focusId && (edge.from === focusId || edge.to === focusId);
784
+ var matches = !query || searchableText(edge).indexOf(query) !== -1 || searchableText(from.entity).indexOf(query) !== -1 || searchableText(to.entity).indexOf(query) !== -1;
785
+ var alpha = !matches ? 0.04 : focusId ? (connected ? 0.78 : 0.07) : (dense ? 0.18 : 0.34);
786
+ var color = hexToRgb(palette[from.entity.type] || palette[from.entity.graph_kind] || palette.default);
787
+ var dx = to.x - from.x;
788
+ var dy = to.y - from.y;
789
+ var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
790
+ var offset = dense ? 10 : 16;
791
+ var cx = (from.x + to.x) / 2 + (-dy / dist) * offset;
792
+ var cy = (from.y + to.y) / 2 + (dx / dist) * offset;
793
+ ctx.beginPath();
794
+ ctx.moveTo(from.x, from.y);
795
+ ctx.quadraticCurveTo(cx, cy, to.x, to.y);
796
+ ctx.strokeStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + alpha + ")";
797
+ ctx.lineWidth = connected ? 2.8 : 1.2;
798
+ ctx.stroke();
799
+ if (connected || (!dense && state.sim.zoom > 1.25)) drawArrow(ctx, from, to, cx, cy, color, alpha);
800
+ if (connected && state.sim.zoom > 0.62) drawEdgeLabel(ctx, edge, cx, cy);
801
+ });
802
+ }
803
+
804
+ function drawCanvasNodes(ctx) {
805
+ var focusId = focusedCanvasNodeId();
806
+ var query = normalize(els.searchInput.value);
807
+ var dense = state.sim.nodes.length > 55;
808
+ state.sim.nodes.forEach(function (node) {
809
+ var entity = node.entity;
810
+ var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
811
+ var hovered = state.sim.hoverNode && state.sim.hoverNode.id === node.id;
812
+ var connected = focusId && (node.id === focusId || state.sim.edges.some(function (edge) {
813
+ return (edge.from === focusId && edge.to === node.id) || (edge.to === focusId && edge.from === node.id);
814
+ }));
815
+ var matches = !query || searchableText(entity).indexOf(query) !== -1;
816
+ var alpha = !matches ? 0.12 : focusId && !connected ? 0.20 : 1;
817
+ var color = palette[entity.type] || palette[entity.graph_kind] || palette.default;
818
+ ctx.save();
819
+ ctx.globalAlpha = alpha;
820
+ if (selected || hovered || (!focusId && !dense)) {
821
+ ctx.shadowColor = color;
822
+ ctx.shadowBlur = selected ? 20 : hovered ? 16 : 5;
823
+ }
824
+ drawNodeShape(ctx, node.x, node.y, node.r, entity);
825
+ var grad = ctx.createRadialGradient(node.x - node.r * 0.3, node.y - node.r * 0.3, 1, node.x, node.y, node.r * 1.35);
826
+ grad.addColorStop(0, brighten(color, 54));
827
+ grad.addColorStop(1, color);
828
+ ctx.fillStyle = grad;
829
+ ctx.fill();
830
+ ctx.restore();
831
+
832
+ if (selected || hovered) {
833
+ ctx.save();
834
+ drawNodeShape(ctx, node.x, node.y, node.r + 4, entity);
835
+ ctx.strokeStyle = color;
836
+ ctx.lineWidth = selected ? 3 : 2;
837
+ ctx.shadowColor = color;
838
+ ctx.shadowBlur = 12;
839
+ ctx.stroke();
840
+ ctx.restore();
841
+ }
842
+
843
+ var shouldLabel = matches && (selected || hovered || (query && matches) || (!dense && state.sim.zoom > 0.75) || (dense && state.sim.zoom > 1.55 && node.r > 13));
844
+ if (shouldLabel) drawNodeLabel(ctx, node, selected || hovered);
845
+ });
846
+ }
847
+
848
+ function drawArrow(ctx, from, to, cx, cy, color, alpha) {
849
+ var angle = Math.atan2(to.y - cy, to.x - cx);
850
+ var tipX = to.x - to.r * Math.cos(angle);
851
+ var tipY = to.y - to.r * Math.sin(angle);
852
+ ctx.beginPath();
853
+ ctx.moveTo(tipX, tipY);
854
+ ctx.lineTo(tipX - 8 * Math.cos(angle - 0.35), tipY - 8 * Math.sin(angle - 0.35));
855
+ ctx.lineTo(tipX - 8 * Math.cos(angle + 0.35), tipY - 8 * Math.sin(angle + 0.35));
856
+ ctx.closePath();
857
+ ctx.fillStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + Math.min(0.85, alpha + 0.10) + ")";
858
+ ctx.fill();
859
+ }
860
+
861
+ function drawEdgeLabel(ctx, edge, x, y) {
862
+ var inv = 1 / state.sim.zoom;
863
+ ctx.save();
864
+ ctx.font = "700 " + (10 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
865
+ ctx.textAlign = "center";
866
+ ctx.fillStyle = "rgba(215,249,223,0.82)";
867
+ ctx.fillText(shortName(edge.relation || "related", 22), x, y - 5 * inv);
868
+ ctx.restore();
869
+ }
870
+
871
+ function drawNodeLabel(ctx, node, strong) {
872
+ var inv = 1 / state.sim.zoom;
873
+ var label = shortName(displayName(node.entity), strong ? 30 : 20);
874
+ ctx.save();
875
+ ctx.font = (strong ? "800 " : "700 ") + (12 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
876
+ var width = ctx.measureText(label).width + 16 * inv;
877
+ var height = 20 * inv;
878
+ var x = node.x - width / 2;
879
+ var y = node.y + node.r + 8 * inv;
880
+ ctx.fillStyle = "rgba(3,6,4,0.88)";
881
+ roundedRect(ctx, x, y, width, height, 4 * inv);
882
+ ctx.fill();
883
+ ctx.strokeStyle = strong ? "rgba(65,255,143,0.55)" : "rgba(65,255,143,0.18)";
884
+ ctx.stroke();
885
+ ctx.fillStyle = strong ? "#d7f9df" : "#9be7c0";
886
+ ctx.textAlign = "center";
887
+ ctx.fillText(label, node.x, y + 14 * inv);
888
+ ctx.restore();
889
+ }
890
+
891
+ function drawNodeShape(ctx, x, y, r, entity) {
892
+ var type = entity.type || "";
893
+ if (type === "file" || type === "repo" || type === "command" || type === "script") {
894
+ roundedRect(ctx, x - r * 1.25, y - r * 0.75, r * 2.5, r * 1.5, 4);
895
+ return;
896
+ }
897
+ if (type === "decision" || type === "bug_fix" || type === "test") {
898
+ ctx.beginPath();
899
+ ctx.moveTo(x, y - r);
900
+ ctx.lineTo(x + r, y);
901
+ ctx.lineTo(x, y + r);
902
+ ctx.lineTo(x - r, y);
903
+ ctx.closePath();
904
+ return;
905
+ }
906
+ if (type === "route" || type === "external" || isDependencyEntity(entity)) {
907
+ ctx.beginPath();
908
+ for (var i = 0; i < 6; i++) {
909
+ var angle = Math.PI / 3 * i - Math.PI / 2;
910
+ var px = x + r * Math.cos(angle);
911
+ var py = y + r * Math.sin(angle);
912
+ if (i === 0) ctx.moveTo(px, py);
913
+ else ctx.lineTo(px, py);
914
+ }
915
+ ctx.closePath();
916
+ return;
917
+ }
918
+ ctx.beginPath();
919
+ ctx.arc(x, y, r, 0, Math.PI * 2);
920
+ }
921
+
922
+ function renderSvg() {
923
+ els.edgeLayer.textContent = "";
924
+ els.nodeLayer.textContent = "";
925
+ els.svg.setAttribute("viewBox", [state.viewBox.x, state.viewBox.y, state.viewBox.width, state.viewBox.height].join(" "));
926
+
927
+ var selectedEntityId = state.selected && state.selected.kind === "entity" ? state.selected.id : null;
928
+ var selectedEdgeId = state.selected && state.selected.kind === "edge" ? state.selected.id : null;
929
+ var connectedIds = connectedEntityIds(selectedEntityId, selectedEdgeId);
930
+
931
+ renderLaneLabels();
932
+
933
+ state.edges.forEach(function (edge) {
934
+ if (!state.visibleEdgeIds.has(edge.id)) return;
935
+ var from = state.positions.get(edge.from);
936
+ var to = state.positions.get(edge.to);
937
+ if (!from || !to) return;
938
+
939
+ var group = svgEl("g");
940
+ var connected = selectedEdgeId === edge.id || connectedIds.edges.has(edge.id);
941
+ var line = svgEl("path", {
942
+ d: edgePath(from, to),
943
+ class: classNames("edge-line", "review-" + reviewStatus(edge).replace(/\s+/g, "-"), connected && "connected", selectedEdgeId === edge.id && "selected")
944
+ });
945
+ var hit = svgEl("path", {
946
+ d: edgePath(from, to),
947
+ class: "edge-hit"
948
+ });
949
+ hit.addEventListener("click", function () {
950
+ state.selected = { kind: "edge", id: edge.id };
951
+ render();
952
+ });
953
+ hit.addEventListener("mousedown", function (event) {
954
+ event.stopPropagation();
955
+ });
956
+ group.appendChild(line);
957
+ group.appendChild(hit);
958
+ els.edgeLayer.appendChild(group);
959
+ });
960
+
961
+ state.entities.forEach(function (entity) {
962
+ if (!state.visibleEntityIds.has(entity.id)) return;
963
+ var pos = state.positions.get(entity.id);
964
+ if (!pos) return;
965
+
966
+ var selected = selectedEntityId === entity.id;
967
+ var connected = connectedIds.entities.has(entity.id);
968
+ var group = svgEl("g", {
969
+ class: classNames("node", "graph-" + (entity.graph_kind || "unknown"), "type-" + safeCssName(entity.type || "unknown"), isDependencyEntity(entity) && "dependency-node", selected && "selected", connected && "connected"),
970
+ transform: "translate(" + pos.x + " " + pos.y + ")"
971
+ });
972
+ var dims = nodeDimensions(entity);
973
+ var rect = svgEl("rect", {
974
+ x: -dims.width / 2,
975
+ y: -dims.height / 2,
976
+ width: dims.width,
977
+ height: dims.height,
978
+ class: "node-body"
979
+ });
980
+ var port = svgEl("circle", {
981
+ cx: -dims.width / 2 + 10,
982
+ cy: 0,
983
+ r: selected ? 4 : 3,
984
+ class: "node-port",
985
+ fill: palette[entity.type] || palette.default
986
+ });
987
+ var titleText = svgEl("text", {
988
+ x: -dims.width / 2 + 22,
989
+ y: -4,
990
+ class: "node-title"
991
+ });
992
+ titleText.textContent = shortName(displayName(entity), dims.labelMax);
993
+ var typeText = svgEl("text", {
994
+ x: -dims.width / 2 + 22,
995
+ y: 13,
996
+ class: "node-type"
997
+ });
998
+ typeText.textContent = nodeKindLabel(entity);
999
+ var title = svgEl("title");
1000
+ title.textContent = displayName(entity) + "\n" + (entity.summary || "");
1001
+ group.addEventListener("click", function () {
1002
+ state.selected = { kind: "entity", id: entity.id };
1003
+ render();
1004
+ });
1005
+ group.addEventListener("mousedown", function (event) {
1006
+ event.stopPropagation();
1007
+ });
1008
+ group.appendChild(title);
1009
+ group.appendChild(rect);
1010
+ group.appendChild(port);
1011
+ group.appendChild(titleText);
1012
+ group.appendChild(typeText);
1013
+ els.nodeLayer.appendChild(group);
1014
+ });
1015
+ }
1016
+
1017
+ function renderLaneLabels() {
1018
+ var visible = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
1019
+ [
1020
+ ["MEMORY", 92, visible.some(function (entity) { return entity.graph_kind === "memory"; })],
1021
+ ["FILES", 392, visible.some(function (entity) { return entity.graph_kind === "code" && entity.type === "file"; })],
1022
+ ["FLOW", 682, visible.some(function (entity) { return entity.graph_kind === "code" && ["symbol", "route", "test"].indexOf(entity.type) !== -1; })],
1023
+ ["DEPS", 962, visible.some(function (entity) { return isDependencyEntity(entity); })]
1024
+ ].forEach(function (lane) {
1025
+ if (!lane[2]) return;
1026
+ var label = svgEl("text", { x: lane[1], y: 34, class: "lane-label" });
1027
+ label.textContent = lane[0];
1028
+ els.edgeLayer.appendChild(label);
1029
+ });
1030
+ }
1031
+
1032
+ function renderLists() {
1033
+ var visibleEntities = state.entities.filter(function (entity) {
1034
+ return state.visibleEntityIds.has(entity.id);
1035
+ });
1036
+ var visibleEdges = state.edges.filter(function (edge) {
1037
+ return state.visibleEdgeIds.has(edge.id);
1038
+ }).sort(function (a, b) {
1039
+ return reviewRank(a) - reviewRank(b);
1040
+ });
1041
+
1042
+ els.entityCount.textContent = String(visibleEntities.length);
1043
+ els.edgeCount.textContent = String(visibleEdges.length);
1044
+ els.entityList.textContent = "";
1045
+ els.edgeList.textContent = "";
1046
+
1047
+ visibleEntities.forEach(function (entity) {
1048
+ var button = document.createElement("button");
1049
+ button.type = "button";
1050
+ button.className = classNames("list-item", state.selected && state.selected.kind === "entity" && state.selected.id === entity.id && "selected");
1051
+ button.innerHTML = "<span class=\"item-title\"></span><span class=\"item-meta\"></span>";
1052
+ button.querySelector(".item-title").textContent = displayName(entity);
1053
+ button.querySelector(".item-meta").textContent = (entity.type || "unknown") + " | " + entity.id;
1054
+ button.addEventListener("click", function () {
1055
+ state.selected = { kind: "entity", id: entity.id };
1056
+ render();
1057
+ });
1058
+ els.entityList.appendChild(button);
1059
+ });
1060
+
1061
+ visibleEdges.forEach(function (edge) {
1062
+ var button = document.createElement("button");
1063
+ button.type = "button";
1064
+ button.className = classNames("list-item", state.selected && state.selected.kind === "edge" && state.selected.id === edge.id && "selected");
1065
+ button.innerHTML = "<span class=\"item-title\"></span><span class=\"item-meta\"></span>";
1066
+ button.querySelector(".item-title").textContent = edge.relation || "related";
1067
+ button.querySelector(".item-meta").textContent = displayName(state.entityById.get(edge.from)) + " -> " + displayName(state.entityById.get(edge.to)) + " | " + reviewStatus(edge);
1068
+ button.addEventListener("click", function () {
1069
+ state.selected = { kind: "edge", id: edge.id };
1070
+ render();
1071
+ });
1072
+ els.edgeList.appendChild(button);
1073
+ });
1074
+ }
1075
+
1076
+ function renderDetails() {
1077
+ if (!state.selected) {
1078
+ els.selectionDetails.className = "details-empty";
1079
+ els.selectionDetails.textContent = "Select an entity or edge.";
1080
+ els.selectionStatus.textContent = "No selection";
1081
+ return;
1082
+ }
1083
+
1084
+ var item = state.selected.kind === "entity"
1085
+ ? state.entityById.get(state.selected.id)
1086
+ : state.edges.find(function (edge) { return edge.id === state.selected.id; });
1087
+
1088
+ if (!item) {
1089
+ els.selectionDetails.textContent = "Selection no longer exists.";
1090
+ return;
1091
+ }
1092
+
1093
+ els.selectionDetails.className = "";
1094
+ els.selectionDetails.textContent = "";
1095
+ var title = document.createElement("div");
1096
+ title.className = "detail-title";
1097
+ title.textContent = state.selected.kind === "entity" ? displayName(item) : (item.relation || "related");
1098
+ var kind = document.createElement("div");
1099
+ kind.className = "detail-kind";
1100
+ kind.textContent = state.selected.kind === "entity" ? (item.type || "unknown") : "edge";
1101
+ var rows = document.createElement("dl");
1102
+ rows.appendChild(detailRow("ID", item.id));
1103
+ els.selectionStatus.textContent = state.selected.kind === "entity" ? "Node" : reviewStatus(item);
1104
+
1105
+ if (state.selected.kind === "entity") {
1106
+ rows.appendChild(detailRow("Summary", item.summary || ""));
1107
+ rows.appendChild(detailRow("Graph", item.graph_kind || ""));
1108
+ rows.appendChild(detailRow("Aliases", Array.isArray(item.aliases) ? item.aliases.join(", ") : ""));
1109
+ rows.appendChild(detailRow("Evidence", formatEvidence(item.evidence)));
1110
+ rows.appendChild(detailRow("First seen", item.first_seen_at || ""));
1111
+ rows.appendChild(detailRow("Last seen", item.last_seen_at || ""));
1112
+ } else {
1113
+ rows.appendChild(detailRow("From", displayName(state.entityById.get(item.from)) + " (" + item.from + ")"));
1114
+ rows.appendChild(detailRow("To", displayName(state.entityById.get(item.to)) + " (" + item.to + ")"));
1115
+ rows.appendChild(detailRow("Fact", item.fact || ""));
1116
+ rows.appendChild(detailRow("Graph", item.graph_kind || ""));
1117
+ rows.appendChild(detailRow("Review", reviewStatus(item)));
1118
+ rows.appendChild(detailRow("Confidence", item.confidence == null ? "" : String(item.confidence)));
1119
+ rows.appendChild(detailRow("Evidence", formatEvidence(item.evidence)));
1120
+ rows.appendChild(detailRow("Valid from", item.valid_from || ""));
1121
+ rows.appendChild(detailRow("Invalidated at", item.invalidated_at || ""));
1122
+ rows.appendChild(detailRow("Commit", item.commit || ""));
1123
+ }
1124
+
1125
+ els.selectionDetails.appendChild(title);
1126
+ els.selectionDetails.appendChild(kind);
1127
+ els.selectionDetails.appendChild(rows);
1128
+ }
1129
+
1130
+ function detailRow(label, value) {
1131
+ var wrapper = document.createElement("div");
1132
+ var term = document.createElement("dt");
1133
+ var description = document.createElement("dd");
1134
+ wrapper.className = "detail-row";
1135
+ term.textContent = label;
1136
+ description.textContent = value || "n/a";
1137
+ wrapper.appendChild(term);
1138
+ wrapper.appendChild(description);
1139
+ return wrapper;
1140
+ }
1141
+
1142
+ function formatEvidence(evidence) {
1143
+ if (!Array.isArray(evidence) || evidence.length === 0) return "";
1144
+ return evidence.map(function (id) {
1145
+ var episode = state.episodesById.get(id);
1146
+ return episode && episode.summary ? id + ": " + episode.summary : id;
1147
+ }).join("\n");
1148
+ }
1149
+
1150
+ function reviewStatus(item) {
1151
+ if (item.invalidated_at) return "invalidated";
1152
+ if (item.confidence != null && item.confidence < 0.75) return "low confidence";
1153
+ if (Array.isArray(item.evidence) && item.evidence.length === 0) return "missing evidence";
1154
+ return "ok";
1155
+ }
1156
+
1157
+ function reviewRank(item) {
1158
+ var status = reviewStatus(item);
1159
+ if (status === "invalidated") return 0;
1160
+ if (status === "missing evidence") return 1;
1161
+ if (status === "low confidence") return 2;
1162
+ return 3;
1163
+ }
1164
+
1165
+ function renderMetrics() {
1166
+ if (!els.metricsSummary) return;
1167
+ var visibleEdges = state.edges.filter(function (edge) { return state.visibleEdgeIds.has(edge.id); });
1168
+ var visibleEntities = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
1169
+ var hiddenDependencies = state.entities.filter(function (entity) {
1170
+ return isDependencyEntity(entity) && !state.visibleEntityIds.has(entity.id);
1171
+ }).length;
1172
+ var evidenceEdges = visibleEdges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length > 0; }).length;
1173
+ var official = state.metrics;
1174
+ var metrics = official ? [
1175
+ ["Readiness", official.harness.readiness_score + "/100"],
1176
+ ["Tokens Saved", official.savings ? official.savings.estimated_tokens_saved_per_recall : "n/a"],
1177
+ ["Code Files", official.code_graph.files],
1178
+ ["Memory Edges", official.memory_graph.edges],
1179
+ ["Quality", official.memory_graph.average_quality_score + "/100"],
1180
+ ["Evidence", official.memory_graph.evidence_coverage_percent + "%"]
1181
+ ] : [
1182
+ ["Nodes", visibleEntities.length + "/" + state.entities.length],
1183
+ ["Relations", visibleEdges.length + "/" + state.edges.length],
1184
+ ["Memory Nodes", visibleEntities.filter(function (entity) { return entity.graph_kind === "memory"; }).length],
1185
+ ["Code Nodes", visibleEntities.filter(function (entity) { return entity.graph_kind === "code"; }).length],
1186
+ ["Evidence", visibleEdges.length ? Math.round(evidenceEdges / visibleEdges.length * 100) + "%" : "n/a"],
1187
+ ["Review Flags", visibleEdges.filter(function (edge) { return reviewStatus(edge) !== "ok"; }).length]
1188
+ ];
1189
+ els.metricsSummary.textContent = "";
1190
+ metrics.forEach(function (metric) {
1191
+ var div = document.createElement("div");
1192
+ div.className = "metric";
1193
+ div.innerHTML = "<strong></strong><span></span>";
1194
+ div.querySelector("strong").textContent = metric[1];
1195
+ div.querySelector("span").textContent = metric[0];
1196
+ els.metricsSummary.appendChild(div);
1197
+ });
1198
+ renderStatusStrip(visibleEntities, visibleEdges, official);
1199
+ els.workspaceMode.textContent = (els.viewMode.value || "combined").replace(/^./, function (letter) { return letter.toUpperCase(); });
1200
+ els.graphSubhead.textContent = visibleEntities.length + " visible nodes and " + visibleEdges.length + " visible relations" +
1201
+ (hiddenDependencies && !els.showDependencies.checked ? " (" + hiddenDependencies + " dependency/noise nodes hidden)." : ".");
1202
+ }
1203
+
1204
+ function renderReviewQueue() {
1205
+ if (!els.reviewList) return;
1206
+ var packets = state.pendingPackets || [];
1207
+ els.reviewCount.textContent = String(packets.length);
1208
+ els.reviewList.textContent = "";
1209
+ if (!packets.length && !state.reviewText) {
1210
+ els.reviewList.className = "review-list details-empty";
1211
+ els.reviewList.textContent = "No pending packets loaded. Launch with `kage viewer --project <repo>` to load review context automatically.";
1212
+ return;
1213
+ }
1214
+ els.reviewList.className = "review-list";
1215
+ packets.forEach(function (packet) {
1216
+ var item = document.createElement("div");
1217
+ item.className = "review-item";
1218
+ var quality = packet.quality || {};
1219
+ item.innerHTML = [
1220
+ "<div class=\"review-title\"></div>",
1221
+ "<div class=\"review-meta\"></div>",
1222
+ "<div class=\"review-summary\"></div>",
1223
+ "<div class=\"review-risks\"></div>"
1224
+ ].join("");
1225
+ item.querySelector(".review-title").textContent = packet.title || packet.id;
1226
+ item.querySelector(".review-meta").textContent = [packet.type, packet.status, "score " + (quality.score == null ? "n/a" : quality.score + "/100")].filter(Boolean).join(" | ");
1227
+ item.querySelector(".review-summary").textContent = packet.summary || "";
1228
+ item.querySelector(".review-risks").textContent = Array.isArray(quality.risks) && quality.risks.length ? "risks: " + quality.risks.join(", ") : "risks: none";
1229
+ els.reviewList.appendChild(item);
1230
+ });
1231
+ if (state.reviewText) {
1232
+ var artifact = document.createElement("details");
1233
+ artifact.className = "review-artifact";
1234
+ artifact.innerHTML = "<summary>Review artifact markdown</summary><pre></pre>";
1235
+ artifact.querySelector("pre").textContent = state.reviewText.slice(0, 12000);
1236
+ els.reviewList.appendChild(artifact);
1237
+ }
1238
+ }
1239
+
1240
+ function renderProof() {
1241
+ if (!els.proofList) return;
1242
+ var metrics = state.metrics;
1243
+ els.proofList.textContent = "";
1244
+ if (!metrics) {
1245
+ els.proofStatus.textContent = "not loaded";
1246
+ els.proofList.className = "proof-list details-empty";
1247
+ els.proofList.textContent = "Metrics not loaded. Run `kage metrics --project <repo> --json > .agent_memory/metrics.json` or launch with `kage viewer`.";
1248
+ return;
1249
+ }
1250
+ els.proofStatus.textContent = "loaded";
1251
+ els.proofList.className = "proof-list";
1252
+ var rows = [
1253
+ ["Readiness", metrics.harness && metrics.harness.readiness_score != null ? metrics.harness.readiness_score + "/100" : "n/a"],
1254
+ ["Useful memory", metrics.quality ? metrics.quality.useful_memory_ratio_percent + "%" : "n/a"],
1255
+ ["Evidence", metrics.memory_graph ? metrics.memory_graph.evidence_coverage_percent + "%" : "n/a"],
1256
+ ["Pending review", metrics.memory_graph ? String(metrics.memory_graph.pending_packets) : "n/a"],
1257
+ ["Recall hit rate", metrics.pain ? metrics.pain.recall_hit_rate_percent + "%" : "n/a"],
1258
+ ["Tokens saved", metrics.pain ? String(metrics.pain.estimated_tokens_saved) : metrics.savings ? String(metrics.savings.estimated_tokens_saved_per_recall) : "n/a"]
1259
+ ];
1260
+ rows.forEach(function (row) {
1261
+ var item = document.createElement("div");
1262
+ item.className = "proof-item";
1263
+ item.innerHTML = "<strong></strong><span></span>";
1264
+ item.querySelector("strong").textContent = row[1];
1265
+ item.querySelector("span").textContent = row[0];
1266
+ els.proofList.appendChild(item);
1267
+ });
1268
+ }
1269
+
1270
+ function renderStatusStrip(visibleEntities, visibleEdges, official) {
1271
+ if (!els.statusStrip) return;
1272
+ var memoryCount = visibleEntities.filter(function (entity) { return entity.graph_kind === "memory"; }).length;
1273
+ var codeCount = visibleEntities.filter(function (entity) { return entity.graph_kind === "code"; }).length;
1274
+ var reviewFlags = visibleEdges.filter(function (edge) { return reviewStatus(edge) !== "ok"; }).length;
1275
+ var pills = official ? [
1276
+ ["Readiness", official.harness.readiness_score + "/100", ""],
1277
+ ["Pending", official.memory_graph ? String(official.memory_graph.pending_packets) : "n/a", official.memory_graph && official.memory_graph.pending_packets ? "warn" : ""],
1278
+ ["Tokens saved", official.savings ? String(official.savings.estimated_tokens_saved_per_recall) : "n/a", "warn"],
1279
+ ["Quality", official.memory_graph.average_quality_score + "/100", "memory"],
1280
+ ["Parser coverage", official.code_graph.indexer_coverage_percent + "%", "code"]
1281
+ ] : [
1282
+ ["Memory", String(memoryCount), "memory"],
1283
+ ["Code", String(codeCount), "code"],
1284
+ ["Relations", String(visibleEdges.length), ""],
1285
+ ["Review flags", String(reviewFlags), reviewFlags ? "warn" : ""]
1286
+ ];
1287
+ els.statusStrip.textContent = "";
1288
+ pills.forEach(function (pill) {
1289
+ var span = document.createElement("span");
1290
+ span.className = classNames("status-pill", pill[2]);
1291
+ span.innerHTML = "<strong></strong><span></span>";
1292
+ span.querySelector("strong").textContent = pill[1];
1293
+ span.querySelector("span").textContent = pill[0];
1294
+ els.statusStrip.appendChild(span);
1295
+ });
1296
+ }
1297
+
1298
+ function connectedEntityIds(entityId, edgeId) {
1299
+ var entities = new Set();
1300
+ var edges = new Set();
1301
+
1302
+ state.edges.forEach(function (edge) {
1303
+ if (entityId && (edge.from === entityId || edge.to === entityId)) {
1304
+ edges.add(edge.id);
1305
+ entities.add(edge.from);
1306
+ entities.add(edge.to);
1307
+ }
1308
+ if (edgeId && edge.id === edgeId) {
1309
+ entities.add(edge.from);
1310
+ entities.add(edge.to);
1311
+ edges.add(edge.id);
1312
+ }
1313
+ });
1314
+
1315
+ return { entities: entities, edges: edges };
1316
+ }
1317
+
1318
+ function degreeOf(id) {
1319
+ return state.edges.reduce(function (sum, edge) {
1320
+ return sum + (edge.from === id || edge.to === id ? 1 : 0);
1321
+ }, 0);
1322
+ }
1323
+
1324
+ function entityImportance(entity) {
1325
+ if (!entity) return -1000;
1326
+ var score = degreeOf(entity.id) * 8;
1327
+ if (entity.graph_kind === "memory") score += 80;
1328
+ if (entity.graph_kind === "code" && entity.type === "file") score += 42;
1329
+ if (["route", "test"].indexOf(entity.type) !== -1) score += 46;
1330
+ if (entity.type === "symbol") score += 34;
1331
+ if (entity.type === "script") score += 24;
1332
+ if (["runbook", "bug_fix", "decision", "workflow", "gotcha", "convention"].indexOf(entity.type) !== -1) score += 40;
1333
+ if (isDependencyEntity(entity)) score -= 90;
1334
+ if (isGeneratedEntity(entity)) score -= 48;
1335
+ return score;
1336
+ }
1337
+
1338
+ function isDependencyEntity(entity) {
1339
+ var text = normalize([entity.id, entity.name, entity.path, entity.summary].filter(Boolean).join(" "));
1340
+ return entity.type === "external" ||
1341
+ text.indexOf("node_modules/") !== -1 ||
1342
+ text.indexOf("/node_modules") !== -1 ||
1343
+ text.indexOf("package-lock.json") !== -1 ||
1344
+ text.indexOf("pnpm-lock.yaml") !== -1 ||
1345
+ text.indexOf("yarn.lock") !== -1 ||
1346
+ text.indexOf("bun.lockb") !== -1 ||
1347
+ text.indexOf("dist/") !== -1 ||
1348
+ text.indexOf("build/") !== -1;
1349
+ }
1350
+
1351
+ function isGeneratedEntity(entity) {
1352
+ var text = normalize([entity.id, entity.name, entity.path, entity.summary].filter(Boolean).join(" "));
1353
+ return text.indexOf(".agent_memory/indexes/") !== -1 ||
1354
+ text.indexOf(".agent_memory/code_graph/") !== -1 ||
1355
+ text.indexOf(".agent_memory/graph/") !== -1 ||
1356
+ text.indexOf(".min.js") !== -1 ||
1357
+ text.indexOf("coverage/") !== -1;
1358
+ }
1359
+
1360
+ function startCanvasPointer(event) {
1361
+ if (event.button !== 0) return;
1362
+ var world = canvasToWorld(event);
1363
+ var node = findCanvasNode(world.x, world.y);
1364
+ if (node) {
1365
+ state.sim.dragNode = node;
1366
+ state.sim.panning = null;
1367
+ } else {
1368
+ state.sim.panning = {
1369
+ x: event.clientX,
1370
+ y: event.clientY,
1371
+ panX: state.sim.panX,
1372
+ panY: state.sim.panY,
1373
+ moved: false
1374
+ };
1375
+ }
1376
+ }
1377
+
1378
+ function moveCanvasPointer(event) {
1379
+ var world = canvasToWorld(event);
1380
+ if (state.sim.dragNode) {
1381
+ state.sim.dragNode.x = world.x;
1382
+ state.sim.dragNode.y = world.y;
1383
+ state.sim.dragNode.vx = 0;
1384
+ state.sim.dragNode.vy = 0;
1385
+ } else if (state.sim.panning) {
1386
+ var dx = event.clientX - state.sim.panning.x;
1387
+ var dy = event.clientY - state.sim.panning.y;
1388
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) state.sim.panning.moved = true;
1389
+ state.sim.panX = state.sim.panning.panX + dx;
1390
+ state.sim.panY = state.sim.panning.panY + dy;
1391
+ }
1392
+ state.sim.hoverNode = findCanvasNode(world.x, world.y);
1393
+ updateCanvasTooltip(event);
1394
+ drawCanvasGraph();
1395
+ }
1396
+
1397
+ function endCanvasPointer() {
1398
+ if (state.sim.dragNode) {
1399
+ state.selected = { kind: "entity", id: state.sim.dragNode.id };
1400
+ state.sim.dragNode = null;
1401
+ render();
1402
+ } else if (state.sim.panning && !state.sim.panning.moved) {
1403
+ state.selected = null;
1404
+ render();
1405
+ }
1406
+ state.sim.panning = null;
1407
+ }
1408
+
1409
+ function leaveCanvasPointer() {
1410
+ state.sim.hoverNode = null;
1411
+ state.sim.dragNode = null;
1412
+ state.sim.panning = null;
1413
+ if (els.tooltip) els.tooltip.classList.remove("visible");
1414
+ drawCanvasGraph();
1415
+ }
1416
+
1417
+ function handleCanvasWheel(event) {
1418
+ event.preventDefault();
1419
+ var rect = els.canvas.getBoundingClientRect();
1420
+ var before = canvasToWorld(event);
1421
+ var factor = event.deltaY > 0 ? 0.88 : 1.14;
1422
+ state.sim.zoom = clamp(state.sim.zoom * factor, 0.14, 4.5);
1423
+ state.sim.panX = event.clientX - rect.left - before.x * state.sim.zoom;
1424
+ state.sim.panY = event.clientY - rect.top - before.y * state.sim.zoom;
1425
+ drawCanvasGraph();
1426
+ }
1427
+
1428
+ function handleCanvasDoubleClick(event) {
1429
+ var world = canvasToWorld(event);
1430
+ var node = findCanvasNode(world.x, world.y);
1431
+ if (!node) return;
1432
+ state.selected = { kind: "entity", id: node.id };
1433
+ els.scopeFilter.value = "focus";
1434
+ render();
1435
+ }
1436
+
1437
+ function canvasToWorld(event) {
1438
+ var rect = els.canvas.getBoundingClientRect();
1439
+ return {
1440
+ x: (event.clientX - rect.left - state.sim.panX) / state.sim.zoom,
1441
+ y: (event.clientY - rect.top - state.sim.panY) / state.sim.zoom
1442
+ };
1443
+ }
1444
+
1445
+ function findCanvasNode(x, y) {
1446
+ for (var i = state.sim.nodes.length - 1; i >= 0; i--) {
1447
+ var node = state.sim.nodes[i];
1448
+ var dx = node.x - x;
1449
+ var dy = node.y - y;
1450
+ if (dx * dx + dy * dy <= Math.pow(node.r + 7, 2)) return node;
1451
+ }
1452
+ return null;
1453
+ }
1454
+
1455
+ function updateCanvasTooltip(event) {
1456
+ if (!els.tooltip) return;
1457
+ var node = state.sim.hoverNode;
1458
+ if (!node || state.sim.dragNode || state.sim.panning) {
1459
+ els.tooltip.classList.remove("visible");
1460
+ return;
1461
+ }
1462
+ var entity = node.entity;
1463
+ var relationCount = state.sim.edges.filter(function (edge) { return edge.from === node.id || edge.to === node.id; }).length;
1464
+ var color = palette[entity.type] || palette[entity.graph_kind] || palette.default;
1465
+ els.tooltip.innerHTML = [
1466
+ "<div class=\"tt-name\"></div>",
1467
+ "<div class=\"tt-type\"></div>",
1468
+ "<div class=\"tt-summary\"></div>",
1469
+ "<div class=\"tt-conns\"></div>"
1470
+ ].join("");
1471
+ els.tooltip.querySelector(".tt-name").textContent = displayName(entity);
1472
+ els.tooltip.querySelector(".tt-type").textContent = nodeKindLabel(entity);
1473
+ els.tooltip.querySelector(".tt-type").style.color = color;
1474
+ els.tooltip.querySelector(".tt-summary").textContent = shortName(entity.summary || entity.id, 150);
1475
+ els.tooltip.querySelector(".tt-conns").textContent = relationCount + " relation" + (relationCount === 1 ? "" : "s");
1476
+ var rect = els.canvas.getBoundingClientRect();
1477
+ els.tooltip.style.left = (event.clientX - rect.left + 14) + "px";
1478
+ els.tooltip.style.top = (event.clientY - rect.top + 14) + "px";
1479
+ els.tooltip.classList.add("visible");
1480
+ }
1481
+
1482
+ function zoomCanvas(factor) {
1483
+ resizeCanvas();
1484
+ var rect = els.canvas.getBoundingClientRect();
1485
+ var cx = rect.width / 2;
1486
+ var cy = rect.height / 2;
1487
+ var before = {
1488
+ x: (cx - state.sim.panX) / state.sim.zoom,
1489
+ y: (cy - state.sim.panY) / state.sim.zoom
1490
+ };
1491
+ state.sim.zoom = clamp(state.sim.zoom * factor, 0.14, 4.5);
1492
+ state.sim.panX = cx - before.x * state.sim.zoom;
1493
+ state.sim.panY = cy - before.y * state.sim.zoom;
1494
+ drawCanvasGraph();
1495
+ }
1496
+
1497
+ function fitCanvas() {
1498
+ resizeCanvas();
1499
+ if (!state.sim.nodes.length) {
1500
+ state.sim.panX = 0;
1501
+ state.sim.panY = 0;
1502
+ state.sim.zoom = 1;
1503
+ return;
1504
+ }
1505
+ var xs = state.sim.nodes.map(function (node) { return node.x; });
1506
+ var ys = state.sim.nodes.map(function (node) { return node.y; });
1507
+ var minX = Math.min.apply(null, xs) - 90;
1508
+ var maxX = Math.max.apply(null, xs) + 90;
1509
+ var minY = Math.min.apply(null, ys) - 90;
1510
+ var maxY = Math.max.apply(null, ys) + 90;
1511
+ var width = els.canvas.width / Math.max(1, window.devicePixelRatio || 1);
1512
+ var height = els.canvas.height / Math.max(1, window.devicePixelRatio || 1);
1513
+ var graphWidth = Math.max(1, maxX - minX);
1514
+ var graphHeight = Math.max(1, maxY - minY);
1515
+ state.sim.zoom = clamp(Math.min(width / graphWidth, height / graphHeight), 0.22, 1.45);
1516
+ state.sim.panX = width / 2 - ((minX + maxX) / 2) * state.sim.zoom;
1517
+ state.sim.panY = height / 2 - ((minY + maxY) / 2) * state.sim.zoom;
1518
+ }
1519
+
1520
+ function focusedCanvasNodeId() {
1521
+ if (state.sim.hoverNode) return state.sim.hoverNode.id;
1522
+ if (state.selected && state.selected.kind === "entity") return state.selected.id;
1523
+ if (state.selected && state.selected.kind === "edge") {
1524
+ var selectedEdge = state.edges.find(function (edge) { return edge.id === state.selected.id; });
1525
+ return selectedEdge ? selectedEdge.from : null;
1526
+ }
1527
+ return null;
1528
+ }
1529
+
1530
+ function fitView() {
1531
+ var visible = state.entities.filter(function (entity) {
1532
+ return state.visibleEntityIds.size === 0 || state.visibleEntityIds.has(entity.id);
1533
+ });
1534
+ var points = visible.map(function (entity) { return state.positions.get(entity.id); }).filter(Boolean);
1535
+ if (!points.length) {
1536
+ state.viewBox = { x: 0, y: 0, width: 1000, height: 660 };
1537
+ return;
1538
+ }
1539
+ var xs = points.map(function (point) { return point.x; });
1540
+ var ys = points.map(function (point) { return point.y; });
1541
+ var minX = Math.min.apply(null, xs) - 130;
1542
+ var maxX = Math.max.apply(null, xs) + 150;
1543
+ var minY = Math.min.apply(null, ys) - 82;
1544
+ var maxY = Math.max.apply(null, ys) + 82;
1545
+ state.viewBox = {
1546
+ x: minX,
1547
+ y: minY,
1548
+ width: Math.max(620, maxX - minX),
1549
+ height: Math.max(400, maxY - minY)
1550
+ };
1551
+ }
1552
+
1553
+ function zoomView(factor) {
1554
+ var box = state.viewBox;
1555
+ var nextWidth = clamp(box.width * factor, 260, 1400);
1556
+ var nextHeight = clamp(box.height * factor, 210, 950);
1557
+ state.viewBox = {
1558
+ x: box.x + (box.width - nextWidth) / 2,
1559
+ y: box.y + (box.height - nextHeight) / 2,
1560
+ width: nextWidth,
1561
+ height: nextHeight
1562
+ };
1563
+ renderSvg();
1564
+ }
1565
+
1566
+ function startPan(event) {
1567
+ if (event.button !== 0) return;
1568
+ state.pan = {
1569
+ x: event.clientX,
1570
+ y: event.clientY,
1571
+ viewBox: Object.assign({}, state.viewBox),
1572
+ moved: false
1573
+ };
1574
+ els.svg.classList.add("dragging");
1575
+ }
1576
+
1577
+ function continuePan(event) {
1578
+ if (!state.pan) return;
1579
+ var rect = els.svg.getBoundingClientRect();
1580
+ var dx = (event.clientX - state.pan.x) / Math.max(rect.width, 1) * state.pan.viewBox.width;
1581
+ var dy = (event.clientY - state.pan.y) / Math.max(rect.height, 1) * state.pan.viewBox.height;
1582
+ if (Math.abs(dx) > 1 || Math.abs(dy) > 1) state.pan.moved = true;
1583
+ state.viewBox = {
1584
+ x: state.pan.viewBox.x - dx,
1585
+ y: state.pan.viewBox.y - dy,
1586
+ width: state.pan.viewBox.width,
1587
+ height: state.pan.viewBox.height
1588
+ };
1589
+ renderSvg();
1590
+ }
1591
+
1592
+ function endPan() {
1593
+ if (!state.pan) return;
1594
+ window.setTimeout(function () {
1595
+ state.pan = null;
1596
+ }, 0);
1597
+ els.svg.classList.remove("dragging");
1598
+ }
1599
+
1600
+ function handleSvgClick(event) {
1601
+ if (state.pan && state.pan.moved) return;
1602
+ if (event.target !== els.svg) return;
1603
+ state.selected = null;
1604
+ render();
1605
+ }
1606
+
1607
+ function handleWheelZoom(event) {
1608
+ event.preventDefault();
1609
+ var rect = els.svg.getBoundingClientRect();
1610
+ var pointX = state.viewBox.x + (event.clientX - rect.left) / Math.max(rect.width, 1) * state.viewBox.width;
1611
+ var pointY = state.viewBox.y + (event.clientY - rect.top) / Math.max(rect.height, 1) * state.viewBox.height;
1612
+ var factor = event.deltaY > 0 ? 1.12 : 0.88;
1613
+ var nextWidth = clamp(state.viewBox.width * factor, 260, 1400);
1614
+ var nextHeight = clamp(state.viewBox.height * factor, 210, 950);
1615
+ var rx = (pointX - state.viewBox.x) / state.viewBox.width;
1616
+ var ry = (pointY - state.viewBox.y) / state.viewBox.height;
1617
+ state.viewBox = {
1618
+ x: pointX - nextWidth * rx,
1619
+ y: pointY - nextHeight * ry,
1620
+ width: nextWidth,
1621
+ height: nextHeight
1622
+ };
1623
+ renderSvg();
1624
+ }
1625
+
1626
+ function displayName(entity) {
1627
+ if (!entity) return "Unknown";
1628
+ return entity.name || entity.title || entity.path || entity.id || "Unknown";
1629
+ }
1630
+
1631
+ function edgePath(from, to) {
1632
+ var dx = Math.max(60, Math.abs(to.x - from.x) * 0.48);
1633
+ var x1 = from.x + (to.x >= from.x ? 72 : -72);
1634
+ var x2 = to.x + (to.x >= from.x ? -72 : 72);
1635
+ return "M " + x1 + " " + from.y + " C " + (x1 + (to.x >= from.x ? dx : -dx)) + " " + from.y + ", " + (x2 - (to.x >= from.x ? dx : -dx)) + " " + to.y + ", " + x2 + " " + to.y;
1636
+ }
1637
+
1638
+ function nodeDimensions(entity) {
1639
+ var name = displayName(entity);
1640
+ var width = clamp(116 + Math.min(name.length, 26) * 4.5, 142, entity.graph_kind === "memory" ? 210 : 190);
1641
+ return {
1642
+ width: width,
1643
+ height: 42,
1644
+ labelMax: width > 180 ? 28 : 22
1645
+ };
1646
+ }
1647
+
1648
+ function nodeKindLabel(entity) {
1649
+ return (entity.graph_kind || "graph") + " / " + (entity.type || "node");
1650
+ }
1651
+
1652
+ function searchableText(value) {
1653
+ return normalize(JSON.stringify(value || {}));
1654
+ }
1655
+
1656
+ function shortName(value, max) {
1657
+ var text = String(value || "");
1658
+ return text.length > max ? text.slice(0, Math.max(1, max - 1)) + "..." : text;
1659
+ }
1660
+
1661
+ function normalize(value) {
1662
+ return String(value || "").toLowerCase();
1663
+ }
1664
+
1665
+ function unique(values) {
1666
+ return Array.from(new Set(values.filter(Boolean)));
1667
+ }
1668
+
1669
+ function clamp(value, min, max) {
1670
+ return Math.max(min, Math.min(max, value));
1671
+ }
1672
+
1673
+ function hexToRgb(value) {
1674
+ var hex = String(value || "#9be7c0").replace("#", "");
1675
+ if (hex.length === 3) hex = hex.split("").map(function (part) { return part + part; }).join("");
1676
+ var parsed = parseInt(hex, 16);
1677
+ return {
1678
+ r: (parsed >> 16) & 255,
1679
+ g: (parsed >> 8) & 255,
1680
+ b: parsed & 255
1681
+ };
1682
+ }
1683
+
1684
+ function brighten(value, amount) {
1685
+ var rgb = hexToRgb(value);
1686
+ return "rgb(" + Math.min(255, rgb.r + amount) + "," + Math.min(255, rgb.g + amount) + "," + Math.min(255, rgb.b + amount) + ")";
1687
+ }
1688
+
1689
+ function roundedRect(ctx, x, y, width, height, radius) {
1690
+ var r = Math.min(radius, Math.abs(width) / 2, Math.abs(height) / 2);
1691
+ ctx.beginPath();
1692
+ ctx.moveTo(x + r, y);
1693
+ ctx.lineTo(x + width - r, y);
1694
+ ctx.quadraticCurveTo(x + width, y, x + width, y + r);
1695
+ ctx.lineTo(x + width, y + height - r);
1696
+ ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
1697
+ ctx.lineTo(x + r, y + height);
1698
+ ctx.quadraticCurveTo(x, y + height, x, y + height - r);
1699
+ ctx.lineTo(x, y + r);
1700
+ ctx.quadraticCurveTo(x, y, x + r, y);
1701
+ ctx.closePath();
1702
+ }
1703
+
1704
+ function classNames() {
1705
+ return Array.prototype.slice.call(arguments).filter(Boolean).join(" ");
1706
+ }
1707
+
1708
+ function visibleSignature(entityIds, edgeIds) {
1709
+ return Array.from(entityIds).sort().join("|") + "::" + Array.from(edgeIds).sort().join("|");
1710
+ }
1711
+
1712
+ function safeCssName(value) {
1713
+ return String(value || "unknown").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
1714
+ }
1715
+
1716
+ function svgEl(name, attrs) {
1717
+ var element = document.createElementNS("http://www.w3.org/2000/svg", name);
1718
+ Object.keys(attrs || {}).forEach(function (key) {
1719
+ element.setAttribute(key, attrs[key]);
1720
+ });
1721
+ return element;
1722
+ }
1723
+
1724
+ function showError(message) {
1725
+ els.graphSummary.textContent = message;
1726
+ }
1727
+ })();