@kage-core/kage-graph-mcp 1.1.33 → 1.1.35

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 CHANGED
@@ -13,6 +13,16 @@
13
13
  visibleEntityIds: new Set(),
14
14
  visibleEdgeIds: new Set(),
15
15
  selected: null,
16
+ viewerSection: "overview",
17
+ viewerAction: null,
18
+ workspaceTab: "controls",
19
+ workspaceOpen: false,
20
+ pathHighlight: {
21
+ nodes: new Set(),
22
+ edges: new Set(),
23
+ direction: "",
24
+ steps: []
25
+ },
16
26
  metrics: null,
17
27
  inbox: null,
18
28
  reports: {
@@ -119,6 +129,13 @@
119
129
  graphSubhead: document.getElementById("graphSubhead"),
120
130
  selectionStatus: document.getElementById("selectionStatus"),
121
131
  searchInput: document.getElementById("searchInput"),
132
+ pathFromInput: document.getElementById("pathFromInput"),
133
+ pathToInput: document.getElementById("pathToInput"),
134
+ pathNodeOptions: document.getElementById("pathNodeOptions"),
135
+ findPath: document.getElementById("findPath"),
136
+ clearPath: document.getElementById("clearPath"),
137
+ pathStatus: document.getElementById("pathStatus"),
138
+ pathResult: document.getElementById("pathResult"),
122
139
  viewMode: document.getElementById("viewMode"),
123
140
  renderMode: document.getElementById("renderMode"),
124
141
  typeFilter: document.getElementById("typeFilter"),
@@ -146,11 +163,21 @@
146
163
  entityCount: document.getElementById("entityCount"),
147
164
  edgeCount: document.getElementById("edgeCount"),
148
165
  reviewCount: document.getElementById("reviewCount"),
166
+ dashboardStats: document.getElementById("dashboardStats"),
149
167
  reviewList: document.getElementById("reviewList"),
150
168
  proofStatus: document.getElementById("proofStatus"),
151
169
  proofList: document.getElementById("proofList"),
152
170
  intelligenceStatus: document.getElementById("intelligenceStatus"),
153
- intelligenceList: document.getElementById("intelligenceList")
171
+ intelligenceList: document.getElementById("intelligenceList"),
172
+ viewerSectionButtons: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-viewer-section]")) : [],
173
+ dashboardActionButtons: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-dashboard-action]")) : [],
174
+ workspaceTabs: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-workspace-tab]")) : [],
175
+ quickViewButtons: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-quick-view]")) : [],
176
+ quickRenderButtons: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-quick-render]")) : [],
177
+ quickSearch: document.getElementById("quickSearch"),
178
+ quickPath: document.getElementById("quickPath"),
179
+ quickInspector: document.getElementById("quickInspector"),
180
+ closeWorkspace: document.getElementById("closeWorkspace")
154
181
  };
155
182
 
156
183
  var MEMORY_CODE_RELATIONS = new Set(["explains_symbol", "informs_symbol", "fixes_symbol", "applies_to_route", "verified_by_test", "affects_code_path"]);
@@ -161,8 +188,69 @@
161
188
  var VISIBLE_EDGE_MIN = 160;
162
189
  var VISIBLE_EDGE_MAX = 560;
163
190
 
191
+ setViewerSection(state.viewerSection);
192
+ setWorkspaceTab(state.workspaceTab, false);
193
+ els.viewerSectionButtons.forEach(function (button) {
194
+ button.addEventListener("click", function () {
195
+ setViewerSection(button.getAttribute("data-viewer-section"));
196
+ });
197
+ });
198
+ els.dashboardActionButtons.forEach(function (button) {
199
+ button.addEventListener("click", function () {
200
+ openDashboardAction(button.getAttribute("data-dashboard-action"));
201
+ });
202
+ });
203
+ els.workspaceTabs.forEach(function (button) {
204
+ button.addEventListener("click", function () {
205
+ setWorkspaceTab(button.getAttribute("data-workspace-tab"), true);
206
+ });
207
+ });
208
+ els.quickViewButtons.forEach(function (button) {
209
+ button.addEventListener("click", function () {
210
+ setViewerSection("graph");
211
+ els.viewMode.value = button.getAttribute("data-quick-view") || "combined";
212
+ state.lastVisibleSignature = "";
213
+ syncQuickControls();
214
+ render();
215
+ });
216
+ });
217
+ els.quickRenderButtons.forEach(function (button) {
218
+ button.addEventListener("click", function () {
219
+ setViewerSection("graph");
220
+ els.renderMode.value = button.getAttribute("data-quick-render") || "2d";
221
+ state.lastVisibleSignature = "";
222
+ syncQuickControls();
223
+ render();
224
+ });
225
+ });
226
+ if (els.quickSearch) {
227
+ els.quickSearch.addEventListener("click", function () {
228
+ setViewerSection("graph");
229
+ setWorkspaceTab("controls", true);
230
+ els.searchInput.focus();
231
+ });
232
+ }
233
+ if (els.quickPath) {
234
+ els.quickPath.addEventListener("click", function () {
235
+ setViewerSection("graph");
236
+ setWorkspaceTab("controls", true);
237
+ els.pathFromInput.focus();
238
+ });
239
+ }
240
+ if (els.quickInspector) {
241
+ els.quickInspector.addEventListener("click", function () {
242
+ setViewerSection("graph");
243
+ setWorkspaceTab("inspector", true);
244
+ });
245
+ }
246
+ if (els.closeWorkspace) els.closeWorkspace.addEventListener("click", closeWorkspace);
247
+ syncQuickControls();
164
248
  els.graphFile.addEventListener("change", handleFile);
165
249
  els.searchInput.addEventListener("input", scheduleRender);
250
+ els.findPath.addEventListener("click", findDependencyPath);
251
+ els.clearPath.addEventListener("click", clearDependencyPath);
252
+ els.pathFromInput.addEventListener("keydown", function (event) { if (event.key === "Enter") findDependencyPath(); });
253
+ els.pathToInput.addEventListener("keydown", function (event) { if (event.key === "Enter") findDependencyPath(); });
166
254
  els.viewMode.addEventListener("change", render);
167
255
  els.renderMode.addEventListener("change", function () {
168
256
  state.lastVisibleSignature = "";
@@ -206,11 +294,110 @@
206
294
  els.maxNodes.value = "90";
207
295
  els.showDependencies.checked = false;
208
296
  state.selected = null;
297
+ setWorkspaceTab("controls", true);
298
+ clearDependencyPath(false);
209
299
  state.lastVisibleSignature = "";
210
300
  render();
211
301
  });
212
302
  loadFromUrlParams();
213
303
 
304
+ function setWorkspaceTab(tab, open) {
305
+ var allowed = new Set(["controls", "inspector", "intelligence", "review", "tables"]);
306
+ state.workspaceTab = allowed.has(tab) ? tab : "controls";
307
+ if (open !== false) state.workspaceOpen = true;
308
+ if (document.body && document.body.classList) {
309
+ document.body.classList.remove(
310
+ "viewer-tab-controls",
311
+ "viewer-tab-inspector",
312
+ "viewer-tab-intelligence",
313
+ "viewer-tab-review",
314
+ "viewer-tab-tables"
315
+ );
316
+ document.body.classList.add("viewer-tab-" + state.workspaceTab);
317
+ document.body.classList.toggle("viewer-workspace-open", state.workspaceOpen);
318
+ }
319
+ els.workspaceTabs.forEach(function (button) {
320
+ var active = button.getAttribute("data-workspace-tab") === state.workspaceTab;
321
+ button.classList.toggle("active", active);
322
+ button.setAttribute("aria-selected", active ? "true" : "false");
323
+ });
324
+ }
325
+
326
+ function setViewerSection(section, action) {
327
+ state.viewerSection = section === "graph" ? "graph" : "overview";
328
+ state.viewerAction = action || null;
329
+ if (state.viewerSection === "overview") closeWorkspace();
330
+ if (document.body && document.body.classList) {
331
+ document.body.classList.remove("viewer-section-overview", "viewer-section-graph");
332
+ document.body.classList.add("viewer-section-" + state.viewerSection);
333
+ }
334
+ syncSectionControls();
335
+ if (state.viewerSection === "graph") resizeActiveGraph();
336
+ }
337
+
338
+ function openDashboardAction(action) {
339
+ var normalized = String(action || "").toLowerCase();
340
+ var tabByAction = {
341
+ memory: "review",
342
+ intelligence: "intelligence",
343
+ review: "review",
344
+ data: "tables"
345
+ };
346
+ setViewerSection("graph", normalized);
347
+ setWorkspaceTab(tabByAction[normalized] || "controls", true);
348
+ }
349
+
350
+ function syncSectionControls() {
351
+ els.viewerSectionButtons.forEach(function (button) {
352
+ var section = button.getAttribute("data-viewer-section");
353
+ var active = state.viewerSection === section && !state.viewerAction;
354
+ if (button.classList && button.classList.contains("viewer-section")) {
355
+ button.classList.toggle("active", active);
356
+ button.setAttribute("aria-selected", active ? "true" : "false");
357
+ }
358
+ });
359
+ els.dashboardActionButtons.forEach(function (button) {
360
+ var action = button.getAttribute("data-dashboard-action");
361
+ var active = state.viewerSection === "graph" && state.viewerAction === action;
362
+ if (button.classList && button.classList.contains("viewer-section")) {
363
+ button.classList.toggle("active", active);
364
+ button.setAttribute("aria-selected", active ? "true" : "false");
365
+ }
366
+ });
367
+ }
368
+
369
+ function closeWorkspace() {
370
+ state.workspaceOpen = false;
371
+ if (document.body && document.body.classList) {
372
+ document.body.classList.remove("viewer-workspace-open");
373
+ }
374
+ }
375
+
376
+ function selectEntity(id, openInspector) {
377
+ state.selected = { kind: "entity", id: id };
378
+ setViewerSection("graph");
379
+ if (openInspector) setWorkspaceTab("inspector", true);
380
+ }
381
+
382
+ function selectEdge(id, openInspector) {
383
+ state.selected = { kind: "edge", id: id };
384
+ setViewerSection("graph");
385
+ if (openInspector) setWorkspaceTab("inspector", true);
386
+ }
387
+
388
+ function syncQuickControls() {
389
+ els.quickViewButtons.forEach(function (button) {
390
+ var active = button.getAttribute("data-quick-view") === els.viewMode.value;
391
+ button.classList.toggle("active", active);
392
+ button.setAttribute("aria-pressed", active ? "true" : "false");
393
+ });
394
+ els.quickRenderButtons.forEach(function (button) {
395
+ var active = button.getAttribute("data-quick-render") === els.renderMode.value;
396
+ button.classList.toggle("active", active);
397
+ button.setAttribute("aria-pressed", active ? "true" : "false");
398
+ });
399
+ }
400
+
214
401
  function handleFile(event) {
215
402
  var files = Array.from(event.target.files || []);
216
403
  if (!files.length) return;
@@ -258,9 +445,12 @@
258
445
  return [episode.id, episode];
259
446
  }));
260
447
  state.selected = null;
448
+ closeWorkspace();
449
+ clearDependencyPath(false);
261
450
  state.lastVisibleSignature = "";
262
451
 
263
452
  populateFilters();
453
+ populatePathOptions();
264
454
  els.emptyState.classList.add("hidden");
265
455
  els.graphSummary.textContent = fileName + " loaded: " + entities.length + " nodes, " + edges.length + " relations.";
266
456
  render();
@@ -277,6 +467,8 @@
277
467
 
278
468
  function loadFromUrlParams() {
279
469
  var params = new URLSearchParams(window.location.search);
470
+ var requestedSection = String(params.get("section") || "").toLowerCase();
471
+ if (requestedSection === "graph" || requestedSection === "overview") setViewerSection(requestedSection);
280
472
  applyRequestedView(params.get("view") || params.get("mode"));
281
473
  applyRequestedRenderMode(params.get("render") || params.get("graphMode"));
282
474
  var memoryGraphPaths = splitParamValues(params.getAll("graph"));
@@ -836,6 +1028,188 @@
836
1028
  });
837
1029
  }
838
1030
 
1031
+ function populatePathOptions() {
1032
+ if (!els.pathNodeOptions) return;
1033
+ var seen = new Set();
1034
+ els.pathNodeOptions.textContent = "";
1035
+ pathCandidateEntities().slice(0, 500).forEach(function (entity) {
1036
+ [entity.path, displayName(entity), entity.id].filter(Boolean).forEach(function (value) {
1037
+ var text = String(value);
1038
+ if (seen.has(text)) return;
1039
+ seen.add(text);
1040
+ var option = document.createElement("option");
1041
+ option.value = text;
1042
+ els.pathNodeOptions.appendChild(option);
1043
+ });
1044
+ });
1045
+ }
1046
+
1047
+ function pathCandidateEntities() {
1048
+ return state.entities
1049
+ .filter(function (entity) {
1050
+ return entity.graph_kind === "code" && ["file", "symbol", "route", "test", "script"].indexOf(entity.type) !== -1;
1051
+ })
1052
+ .sort(function (a, b) {
1053
+ return entityImportance(b) - entityImportance(a) || displayName(a).localeCompare(displayName(b));
1054
+ });
1055
+ }
1056
+
1057
+ function resolvePathEntity(value) {
1058
+ var query = String(value || "").trim();
1059
+ if (!query) return null;
1060
+ var lower = query.toLowerCase();
1061
+ var candidates = pathCandidateEntities();
1062
+ var exact = candidates.filter(function (entity) {
1063
+ return String(entity.id || "").toLowerCase() === lower ||
1064
+ String(entity.path || "").toLowerCase() === lower ||
1065
+ displayName(entity).toLowerCase() === lower;
1066
+ });
1067
+ if (exact.length) return exact[0];
1068
+ var partial = candidates.filter(function (entity) {
1069
+ return String(entity.path || "").toLowerCase().indexOf(lower) !== -1 ||
1070
+ displayName(entity).toLowerCase().indexOf(lower) !== -1 ||
1071
+ String(entity.id || "").toLowerCase().indexOf(lower) !== -1;
1072
+ });
1073
+ return partial[0] || null;
1074
+ }
1075
+
1076
+ function codePathEdges() {
1077
+ var relations = new Set(["imports", "imports_external", "defines_symbol", "calls", "covers", "defines_route", "handled_by"]);
1078
+ return state.edges.filter(function (edge) {
1079
+ if (!relations.has(edge.relation)) return false;
1080
+ var from = state.entityById.get(edge.from);
1081
+ var to = state.entityById.get(edge.to);
1082
+ return from && to && from.graph_kind === "code" && to.graph_kind === "code";
1083
+ });
1084
+ }
1085
+
1086
+ function shortestCodePath(fromId, toId, undirected) {
1087
+ var adjacency = new Map();
1088
+ codePathEdges().forEach(function (edge) {
1089
+ if (!adjacency.has(edge.from)) adjacency.set(edge.from, []);
1090
+ adjacency.get(edge.from).push({ to: edge.to, edge: edge });
1091
+ if (undirected) {
1092
+ if (!adjacency.has(edge.to)) adjacency.set(edge.to, []);
1093
+ adjacency.get(edge.to).push({ to: edge.from, edge: edge });
1094
+ }
1095
+ });
1096
+ var queue = [fromId];
1097
+ var seen = new Set([fromId]);
1098
+ var previous = new Map();
1099
+ while (queue.length) {
1100
+ var current = queue.shift();
1101
+ if (current === toId) break;
1102
+ (adjacency.get(current) || []).forEach(function (step) {
1103
+ if (seen.has(step.to)) return;
1104
+ seen.add(step.to);
1105
+ previous.set(step.to, { from: current, edge: step.edge });
1106
+ queue.push(step.to);
1107
+ });
1108
+ }
1109
+ if (!seen.has(toId)) return null;
1110
+ var nodes = [toId];
1111
+ var edges = [];
1112
+ var cursor = toId;
1113
+ while (cursor !== fromId) {
1114
+ var prev = previous.get(cursor);
1115
+ if (!prev) return null;
1116
+ edges.unshift(prev.edge.id);
1117
+ cursor = prev.from;
1118
+ nodes.unshift(cursor);
1119
+ }
1120
+ return { nodes: nodes, edges: edges };
1121
+ }
1122
+
1123
+ function findDependencyPath() {
1124
+ var from = resolvePathEntity(els.pathFromInput.value);
1125
+ var to = resolvePathEntity(els.pathToInput.value);
1126
+ if (!from || !to) {
1127
+ setPathStatus("Could not resolve both endpoints. Try a file path or exact symbol name.", "warn");
1128
+ return;
1129
+ }
1130
+ if (from.id === to.id) {
1131
+ setPathStatus("Pick two different code nodes.", "warn");
1132
+ return;
1133
+ }
1134
+ var direction = "forward";
1135
+ var path = shortestCodePath(from.id, to.id, false);
1136
+ if (!path) {
1137
+ var reverse = shortestCodePath(to.id, from.id, false);
1138
+ if (reverse) {
1139
+ direction = "reverse";
1140
+ path = {
1141
+ nodes: reverse.nodes.slice().reverse(),
1142
+ edges: reverse.edges.slice().reverse()
1143
+ };
1144
+ }
1145
+ }
1146
+ if (!path) {
1147
+ direction = "undirected";
1148
+ path = shortestCodePath(from.id, to.id, true);
1149
+ }
1150
+ if (!path) {
1151
+ state.pathHighlight = { nodes: new Set(), edges: new Set(), direction: "", steps: [] };
1152
+ els.pathResult.textContent = "";
1153
+ els.pathResult.className = "path-result";
1154
+ setPathStatus("No code dependency path found between those nodes.", "warn");
1155
+ render();
1156
+ return;
1157
+ }
1158
+ state.pathHighlight = {
1159
+ nodes: new Set(path.nodes),
1160
+ edges: new Set(path.edges),
1161
+ direction: direction,
1162
+ steps: path.nodes
1163
+ };
1164
+ setPathStatus(path.nodes.length + " nodes, " + path.edges.length + " edge(s), " + direction + " path.", "ok");
1165
+ renderPathResult(path.nodes, path.edges);
1166
+ state.lastVisibleSignature = "";
1167
+ render();
1168
+ }
1169
+
1170
+ function setPathStatus(text, status) {
1171
+ els.pathStatus.textContent = text;
1172
+ els.pathStatus.className = "path-status" + (status ? " " + status : "");
1173
+ }
1174
+
1175
+ function clearDependencyPath(renderNow) {
1176
+ state.pathHighlight = { nodes: new Set(), edges: new Set(), direction: "", steps: [] };
1177
+ if (els.pathFromInput) els.pathFromInput.value = "";
1178
+ if (els.pathToInput) els.pathToInput.value = "";
1179
+ if (els.pathResult) {
1180
+ els.pathResult.textContent = "";
1181
+ els.pathResult.className = "path-result";
1182
+ }
1183
+ if (els.pathStatus) setPathStatus("Pick two code nodes to trace a dependency path.", "");
1184
+ if (renderNow !== false) {
1185
+ state.lastVisibleSignature = "";
1186
+ render();
1187
+ }
1188
+ }
1189
+
1190
+ function renderPathResult(nodes, edges) {
1191
+ els.pathResult.textContent = "";
1192
+ nodes.forEach(function (id, index) {
1193
+ var entity = state.entityById.get(id);
1194
+ var edge = index > 0 ? state.edgeById.get(edges[index - 1]) : null;
1195
+ var button = document.createElement("button");
1196
+ button.type = "button";
1197
+ button.className = "path-step";
1198
+ button.innerHTML = "<strong></strong><span></span>";
1199
+ button.querySelector("strong").textContent = entity ? displayName(entity) : id;
1200
+ button.querySelector("span").textContent = [
1201
+ index === 0 ? "start" : edge ? edge.relation : "path",
1202
+ entity && entity.path ? entity.path : id
1203
+ ].filter(Boolean).join(" | ");
1204
+ button.addEventListener("click", function () {
1205
+ selectEntity(id, true);
1206
+ render();
1207
+ });
1208
+ els.pathResult.appendChild(button);
1209
+ });
1210
+ els.pathResult.className = "path-result visible";
1211
+ }
1212
+
839
1213
  function layoutGraph(visibleIds) {
840
1214
  state.positions = new Map();
841
1215
  var candidates = state.entities.filter(function (entity) {
@@ -872,6 +1246,7 @@
872
1246
 
873
1247
  function render() {
874
1248
  if (!state.graph) return;
1249
+ syncQuickControls();
875
1250
 
876
1251
  var query = parseSearchQuery(els.searchInput.value);
877
1252
  state.renderQuery = query;
@@ -981,7 +1356,16 @@
981
1356
  }
982
1357
  }
983
1358
 
984
- return { entities: entities, edges: capVisibleEdges(edgesWithVisibleEndpoints(edges, entities), entities, options) };
1359
+ if (state.pathHighlight && state.pathHighlight.nodes.size) {
1360
+ state.pathHighlight.nodes.forEach(function (id) { entities.add(id); });
1361
+ state.pathHighlight.edges.forEach(function (id) { edges.add(id); });
1362
+ }
1363
+
1364
+ var cappedEdges = capVisibleEdges(edgesWithVisibleEndpoints(edges, entities), entities, options);
1365
+ if (state.pathHighlight && state.pathHighlight.edges.size) {
1366
+ state.pathHighlight.edges.forEach(function (id) { if (edges.has(id)) cappedEdges.add(id); });
1367
+ }
1368
+ return { entities: entities, edges: cappedEdges };
985
1369
  }
986
1370
 
987
1371
  function capVisibleEdges(edgeIds, entityIds, options) {
@@ -1484,9 +1868,10 @@
1484
1868
  var to = nodeMap.get(edge.to);
1485
1869
  if (!from || !to) return;
1486
1870
  var connected = focusId && (edge.from === focusId || edge.to === focusId);
1871
+ var pathEdge = state.pathHighlight && state.pathHighlight.edges.has(edge.id);
1487
1872
  var matches = matchesSearchQuery(edge, query) || matchesSearchQuery(from.entity, query) || matchesSearchQuery(to.entity, query);
1488
- var alpha = !matches ? 0.035 : focusId ? (connected ? 0.62 : 0.055) : (dense ? 0.13 : 0.22);
1489
- var color = hexToRgb(edgeThemeColor(edge, from.entity, to.entity));
1873
+ var alpha = pathEdge ? 0.92 : !matches ? 0.035 : focusId ? (connected ? 0.62 : 0.055) : (dense ? 0.13 : 0.22);
1874
+ var color = hexToRgb(pathEdge ? graphPalette.bridge : edgeThemeColor(edge, from.entity, to.entity));
1490
1875
  var dx = to.x - from.x;
1491
1876
  var dy = to.y - from.y;
1492
1877
  var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
@@ -1497,10 +1882,10 @@
1497
1882
  ctx.moveTo(from.x, from.y);
1498
1883
  ctx.quadraticCurveTo(cx, cy, to.x, to.y);
1499
1884
  ctx.strokeStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + alpha + ")";
1500
- ctx.lineWidth = connected ? 2.2 : 1;
1885
+ ctx.lineWidth = pathEdge ? 3 : connected ? 2.2 : 1;
1501
1886
  ctx.stroke();
1502
- if (connected || (!dense && state.sim.zoom > 1.25)) drawArrow(ctx, from, to, cx, cy, color, alpha);
1503
- if (connected && state.sim.zoom > 0.62) drawEdgeLabel(ctx, edge, cx, cy);
1887
+ if (pathEdge || connected || (!dense && state.sim.zoom > 1.25)) drawArrow(ctx, from, to, cx, cy, color, alpha);
1888
+ if ((pathEdge || connected) && state.sim.zoom > 0.62) drawEdgeLabel(ctx, edge, cx, cy);
1504
1889
  });
1505
1890
  }
1506
1891
 
@@ -1514,14 +1899,15 @@
1514
1899
  var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
1515
1900
  var hovered = state.sim.hoverNode && state.sim.hoverNode.id === node.id;
1516
1901
  var connected = focusId && (node.id === focusId || (focusNeighbors && focusNeighbors.has(node.id)));
1902
+ var pathNode = state.pathHighlight && state.pathHighlight.nodes.has(node.id);
1517
1903
  var matches = matchesSearchQuery(entity, query);
1518
- var alpha = !matches ? 0.12 : focusId && !connected ? 0.20 : 1;
1904
+ var alpha = pathNode ? 1 : !matches ? 0.12 : focusId && !connected ? 0.20 : 1;
1519
1905
  var color = nodeThemeColor(entity);
1520
1906
  ctx.save();
1521
1907
  ctx.globalAlpha = alpha;
1522
- if (selected || hovered) {
1523
- ctx.shadowColor = color;
1524
- ctx.shadowBlur = selected ? 14 : 10;
1908
+ if (selected || hovered || pathNode) {
1909
+ ctx.shadowColor = pathNode ? graphPalette.bridge : color;
1910
+ ctx.shadowBlur = selected ? 14 : pathNode ? 12 : 10;
1525
1911
  }
1526
1912
  drawNodeShape(ctx, node.x, node.y, node.r, entity);
1527
1913
  ctx.fillStyle = nodeFillColor(entity);
@@ -1538,18 +1924,18 @@
1538
1924
  }
1539
1925
  ctx.restore();
1540
1926
 
1541
- if (selected || hovered) {
1927
+ if (selected || hovered || pathNode) {
1542
1928
  ctx.save();
1543
1929
  drawNodeShape(ctx, node.x, node.y, node.r + 4, entity);
1544
- ctx.strokeStyle = color;
1545
- ctx.lineWidth = selected ? 2.6 : 1.8;
1546
- ctx.shadowColor = color;
1930
+ ctx.strokeStyle = pathNode ? graphPalette.bridge : color;
1931
+ ctx.lineWidth = selected ? 2.6 : pathNode ? 2.2 : 1.8;
1932
+ ctx.shadowColor = pathNode ? graphPalette.bridge : color;
1547
1933
  ctx.shadowBlur = 8;
1548
1934
  ctx.stroke();
1549
1935
  ctx.restore();
1550
1936
  }
1551
1937
 
1552
- var shouldLabel = matches && (selected || hovered || (query.active && matches) || (!dense && state.sim.zoom > 0.75) || (dense && state.sim.zoom > 1.55 && node.r > 13));
1938
+ var shouldLabel = pathNode || (matches && (selected || hovered || (query.active && matches) || (!dense && state.sim.zoom > 0.75) || (dense && state.sim.zoom > 1.55 && node.r > 13)));
1553
1939
  if (shouldLabel) drawNodeLabel(ctx, node, selected || hovered);
1554
1940
  });
1555
1941
  }
@@ -1681,7 +2067,7 @@
1681
2067
  class: "edge-hit"
1682
2068
  });
1683
2069
  hit.addEventListener("click", function () {
1684
- state.selected = { kind: "edge", id: edge.id };
2070
+ selectEdge(edge.id, true);
1685
2071
  render();
1686
2072
  });
1687
2073
  hit.addEventListener("mousedown", function (event) {
@@ -1733,7 +2119,7 @@
1733
2119
  var title = svgEl("title");
1734
2120
  title.textContent = displayName(entity) + "\n" + (entity.summary || "");
1735
2121
  group.addEventListener("click", function () {
1736
- state.selected = { kind: "entity", id: entity.id };
2122
+ selectEntity(entity.id, true);
1737
2123
  render();
1738
2124
  });
1739
2125
  group.addEventListener("mousedown", function (event) {
@@ -1786,7 +2172,7 @@
1786
2172
  button.querySelector(".item-title").textContent = displayName(entity);
1787
2173
  button.querySelector(".item-meta").textContent = (entity.type || "unknown") + " | " + entity.id;
1788
2174
  button.addEventListener("click", function () {
1789
- state.selected = { kind: "entity", id: entity.id };
2175
+ selectEntity(entity.id, true);
1790
2176
  render();
1791
2177
  });
1792
2178
  els.entityList.appendChild(button);
@@ -1800,7 +2186,7 @@
1800
2186
  button.querySelector(".item-title").textContent = edge.relation || "related";
1801
2187
  button.querySelector(".item-meta").textContent = displayName(state.entityById.get(edge.from)) + " -> " + displayName(state.entityById.get(edge.to)) + " | " + reviewStatus(edge);
1802
2188
  button.addEventListener("click", function () {
1803
- state.selected = { kind: "edge", id: edge.id };
2189
+ selectEdge(edge.id, true);
1804
2190
  render();
1805
2191
  });
1806
2192
  els.edgeList.appendChild(button);
@@ -2029,7 +2415,8 @@
2029
2415
  button.querySelector(".detail-link-meta").textContent = item.meta;
2030
2416
  button.querySelector(".detail-link-body").textContent = item.body;
2031
2417
  button.addEventListener("click", function () {
2032
- state.selected = item.entity ? { kind: "entity", id: item.entity.id } : { kind: "edge", id: item.edge.id };
2418
+ if (item.entity) selectEntity(item.entity.id, true);
2419
+ else selectEdge(item.edge.id, true);
2033
2420
  render();
2034
2421
  });
2035
2422
  list.appendChild(button);
@@ -2116,6 +2503,112 @@
2116
2503
  els.workspaceMode.textContent = (els.viewMode.value || "combined").replace(/^./, function (letter) { return letter.toUpperCase(); });
2117
2504
  els.graphSubhead.textContent = visibleEntities.length + " visible nodes and " + visibleEdges.length + " visible relations" +
2118
2505
  (hiddenDependencies && !els.showDependencies.checked ? " (" + hiddenDependencies + " dependency/noise nodes hidden)." : ".");
2506
+ renderDashboard();
2507
+ }
2508
+
2509
+ function renderDashboard() {
2510
+ if (!els.dashboardStats) return;
2511
+ var metrics = state.metrics || {};
2512
+ var memoryGraph = metrics.memory_graph || {};
2513
+ var codeGraph = metrics.code_graph || {};
2514
+ var structural = metrics.structural_index || {};
2515
+ var savings = metrics.savings || {};
2516
+ var pain = metrics.pain || {};
2517
+ var memoryNodes = state.entities.filter(function (entity) { return entity.graph_kind === "memory"; }).length;
2518
+ var codeNodes = state.entities.filter(function (entity) { return entity.graph_kind === "code"; }).length;
2519
+ var memoryCodeEdges = state.edges.filter(isMemoryCodeEdge);
2520
+ var reports = state.reports || {};
2521
+ var reportCount = Object.keys(reports).filter(function (key) { return reports[key]; }).length;
2522
+ var statRows = [
2523
+ ["Memory packets", firstNumber(memoryGraph.approved_packets, memoryNodes)],
2524
+ ["Code nodes", firstNumber(codeGraph.symbols, structural.symbols, codeNodes)],
2525
+ ["Files", firstNumber(codeGraph.files, structural.files, countEntitiesByType("file"))],
2526
+ ["Memory-code links", memoryCodeEdges.length],
2527
+ ["Parser coverage", codeGraph.indexer_coverage_percent != null ? codeGraph.indexer_coverage_percent + "%" : "n/a"],
2528
+ ["Tokens saved", firstNumber(savings.estimated_tokens_saved_per_recall, pain.estimated_tokens_saved, "n/a")]
2529
+ ];
2530
+ els.dashboardStats.textContent = "";
2531
+ statRows.forEach(function (row) {
2532
+ var item = document.createElement("div");
2533
+ item.className = "dashboard-stat";
2534
+ item.innerHTML = "<strong></strong><span></span>";
2535
+ item.querySelector("strong").textContent = formatDashboardValue(row[1]);
2536
+ item.querySelector("span").textContent = row[0];
2537
+ els.dashboardStats.appendChild(item);
2538
+ });
2539
+
2540
+ setDashboardRows("dashboardMemory", [
2541
+ ["Approved packets", firstNumber(memoryGraph.approved_packets, memoryNodes)],
2542
+ ["Pending review", firstNumber(memoryGraph.pending_packets, (state.pendingPackets || []).length)],
2543
+ ["Evidence coverage", memoryGraph.evidence_coverage_percent != null ? memoryGraph.evidence_coverage_percent + "%" : "n/a"],
2544
+ ["Code-linked memory", memoryCodeEdges.length]
2545
+ ]);
2546
+ setDashboardRows("dashboardGraph", [
2547
+ ["Files", firstNumber(codeGraph.files, structural.files, countEntitiesByType("file"))],
2548
+ ["Symbols", firstNumber(codeGraph.symbols, structural.symbols, countEntitiesByType("symbol"))],
2549
+ ["Relations", state.edges.length],
2550
+ ["Dependency/noise hidden", state.entities.filter(function (entity) { return isDependencyEntity(entity); }).length]
2551
+ ]);
2552
+ setDashboardRows("dashboardIntel", [
2553
+ ["Reports loaded", reportCount],
2554
+ ["Decision coverage", reports.decisions && reports.decisions.coverage_percent != null ? reports.decisions.coverage_percent + "%" : "n/a"],
2555
+ ["Modules scored", reports.moduleHealth && Array.isArray(reports.moduleHealth.modules) ? reports.moduleHealth.modules.length : "n/a"],
2556
+ ["Communities", reports.graphInsights && Array.isArray(reports.graphInsights.communities) ? reports.graphInsights.communities.length : "n/a"]
2557
+ ]);
2558
+ var risk = reports.risk || {};
2559
+ var riskTargets = Array.isArray(risk.targets) ? risk.targets : Object.keys(risk.targets || {});
2560
+ setDashboardRows("dashboardRisk", [
2561
+ ["Risk targets", riskTargets.length || "n/a"],
2562
+ ["Hotspots", Array.isArray(risk.global_hotspots) ? risk.global_hotspots.length : "n/a"],
2563
+ ["Ownership silos", Array.isArray(risk.ownership_silos) ? risk.ownership_silos.length : "n/a"],
2564
+ ["Cycles", reports.graphInsights && Array.isArray(reports.graphInsights.dependency_cycles) ? reports.graphInsights.dependency_cycles.length : "n/a"]
2565
+ ]);
2566
+ var inboxCounts = state.inbox && state.inbox.counts ? state.inbox.counts : {};
2567
+ setDashboardRows("dashboardReview", [
2568
+ ["Readiness", metrics.harness && metrics.harness.readiness_score != null ? metrics.harness.readiness_score + "/100" : "n/a"],
2569
+ ["Inbox pending", firstNumber(inboxCounts.pending, (state.pendingPackets || []).length)],
2570
+ ["Stale flags", firstNumber(inboxCounts.stale, 0)],
2571
+ ["Duplicate flags", firstNumber(inboxCounts.duplicates, 0)]
2572
+ ]);
2573
+ var workspace = reports.workspace || {};
2574
+ setDashboardRows("dashboardWorkspace", [
2575
+ ["Repos", Array.isArray(workspace.repos) ? workspace.repos.length : "n/a"],
2576
+ ["Package deps", Array.isArray(workspace.package_dependencies) ? workspace.package_dependencies.length : "n/a"],
2577
+ ["Route contracts", Array.isArray(workspace.route_contracts) ? workspace.route_contracts.length : "n/a"],
2578
+ ["Co-changes", Array.isArray(workspace.co_changes) ? workspace.co_changes.length : "n/a"]
2579
+ ]);
2580
+ }
2581
+
2582
+ function setDashboardRows(cardId, rows) {
2583
+ var card = document.getElementById(cardId);
2584
+ if (!card) return;
2585
+ var list = card.querySelector("ul");
2586
+ if (!list) return;
2587
+ list.textContent = "";
2588
+ rows.forEach(function (row) {
2589
+ var item = document.createElement("li");
2590
+ item.innerHTML = "<strong></strong><span></span>";
2591
+ item.querySelector("strong").textContent = row[0];
2592
+ item.querySelector("span").textContent = formatDashboardValue(row[1]);
2593
+ list.appendChild(item);
2594
+ });
2595
+ }
2596
+
2597
+ function firstNumber() {
2598
+ for (var index = 0; index < arguments.length; index += 1) {
2599
+ var value = arguments[index];
2600
+ if (value !== null && value !== undefined && value !== "") return value;
2601
+ }
2602
+ return "n/a";
2603
+ }
2604
+
2605
+ function countEntitiesByType(type) {
2606
+ return state.entities.filter(function (entity) { return entity.type === type; }).length;
2607
+ }
2608
+
2609
+ function formatDashboardValue(value) {
2610
+ if (typeof value === "number" && Number.isFinite(value)) return value.toLocaleString();
2611
+ return String(value == null ? "n/a" : value);
2119
2612
  }
2120
2613
 
2121
2614
  function renderReviewQueue() {
@@ -2321,6 +2814,7 @@
2321
2814
  var decisions = reports.decisions;
2322
2815
  var health = reports.moduleHealth;
2323
2816
  var insights = reports.graphInsights;
2817
+ var workspace = reports.workspace;
2324
2818
 
2325
2819
  if (contributors || risk) {
2326
2820
  var profiles = contributors && Array.isArray(contributors.contributors) ? contributors.contributors : [];
@@ -2449,6 +2943,57 @@
2449
2943
  });
2450
2944
  }
2451
2945
 
2946
+ if (workspace) {
2947
+ var deps = Array.isArray(workspace.package_dependencies) ? workspace.package_dependencies : [];
2948
+ var routeContracts = Array.isArray(workspace.route_contracts) ? workspace.route_contracts : [];
2949
+ var topicContracts = Array.isArray(workspace.topic_contracts) ? workspace.topic_contracts : [];
2950
+ var coChanges = Array.isArray(workspace.co_changes) ? workspace.co_changes : [];
2951
+ var workspaceRows = deps.slice(0, 6).map(function (dep) {
2952
+ return {
2953
+ label: dep.from + " -> " + dep.to,
2954
+ value: dep.package_name || "package",
2955
+ meta: "workspace package dependency",
2956
+ score: 72,
2957
+ status: "ok",
2958
+ };
2959
+ }).concat(routeContracts.slice(0, 6).map(function (contract) {
2960
+ return {
2961
+ label: contract.provider_repo + " -> " + contract.consumer_repo,
2962
+ value: [contract.method, contract.path].filter(Boolean).join(" "),
2963
+ meta: [contract.provider_file, contract.consumer_file].filter(Boolean).join(" -> "),
2964
+ score: contract.confidence === "high" ? 92 : 76,
2965
+ status: "ok",
2966
+ };
2967
+ })).concat(topicContracts.slice(0, 6).map(function (contract) {
2968
+ return {
2969
+ label: contract.producer_repo + " -> " + contract.consumer_repo,
2970
+ value: contract.topic,
2971
+ meta: [contract.producer_file, contract.consumer_file].filter(Boolean).join(" -> "),
2972
+ score: contract.confidence === "high" ? 88 : 72,
2973
+ status: "ok",
2974
+ };
2975
+ })).concat(coChanges.slice(0, 8).map(function (link) {
2976
+ var score = Math.min(100, Number(link.strength || 0) * 22 + Number(link.frequency || 0) * 12);
2977
+ return {
2978
+ label: link.source_repo + " <-> " + link.target_repo,
2979
+ value: (link.frequency || 0) + "x co-change",
2980
+ meta: [link.source_file, link.target_file].filter(Boolean).join(" <-> "),
2981
+ score: Math.max(20, score),
2982
+ status: "warn",
2983
+ };
2984
+ }));
2985
+ if (workspaceRows.length) {
2986
+ sections.push({
2987
+ title: "Workspace Map",
2988
+ kicker: "deps / contracts / co-changes",
2989
+ stat: workspaceRows.length + " links",
2990
+ summary: "Workspace links show how sibling repos relate through package dependencies, source-evidence contracts, topic/event links, and local git co-change history.",
2991
+ rows: workspaceRows,
2992
+ limit: 14,
2993
+ });
2994
+ }
2995
+ }
2996
+
2452
2997
  if (risk) {
2453
2998
  var targets = Array.isArray(risk.targets) ? risk.targets : Object.keys(risk.targets || {}).map(function (key) { return risk.targets[key]; });
2454
2999
  var hotspots = Array.isArray(risk.global_hotspots) ? risk.global_hotspots : [];
@@ -2501,7 +3046,7 @@
2501
3046
  scheduleRender();
2502
3047
  return;
2503
3048
  }
2504
- state.selected = { kind: "entity", id: found.id };
3049
+ selectEntity(found.id, true);
2505
3050
  els.searchInput.value = normalized;
2506
3051
  scheduleRender();
2507
3052
  }
@@ -2795,11 +3340,12 @@
2795
3340
 
2796
3341
  function endCanvasPointer() {
2797
3342
  if (state.sim.dragNode) {
2798
- state.selected = { kind: "entity", id: state.sim.dragNode.id };
3343
+ selectEntity(state.sim.dragNode.id, true);
2799
3344
  state.sim.dragNode = null;
2800
3345
  render();
2801
3346
  } else if (state.sim.panning && !state.sim.panning.moved) {
2802
3347
  state.selected = null;
3348
+ closeWorkspace();
2803
3349
  render();
2804
3350
  }
2805
3351
  state.sim.panning = null;
@@ -2836,7 +3382,7 @@
2836
3382
  var world = canvasToWorld(event);
2837
3383
  var node = findCanvasNode(world.x, world.y);
2838
3384
  if (!node) return;
2839
- state.selected = { kind: "entity", id: node.id };
3385
+ selectEntity(node.id, true);
2840
3386
  render();
2841
3387
  }
2842
3388
 
@@ -3010,8 +3556,9 @@
3010
3556
  state.sim.nodes.forEach(function (node, index) {
3011
3557
  var entity = node.entity;
3012
3558
  var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
3559
+ var pathNode = state.pathHighlight && state.pathHighlight.nodes.has(node.id);
3013
3560
  var material = new THREE.SpriteMaterial({
3014
- map: threeNodeTexture(entity, selected),
3561
+ map: threeNodeTexture(entity, selected || pathNode),
3015
3562
  transparent: true,
3016
3563
  opacity: isDependencyEntity(entity) ? 0.70 : 1,
3017
3564
  depthWrite: false
@@ -3019,7 +3566,7 @@
3019
3566
  var mesh = new THREE.Sprite(material);
3020
3567
  var position = threePosition(node, index);
3021
3568
  mesh.position.set(position.x, position.y, position.z);
3022
- var size = threeNodeSize(node, selected);
3569
+ var size = threeNodeSize(node, selected || pathNode);
3023
3570
  mesh.scale.set(size, size, 1);
3024
3571
  mesh.userData.node = node;
3025
3572
  state.three.nodeGroup.add(mesh);
@@ -3033,11 +3580,12 @@
3033
3580
  var fromEntity = from.node.entity;
3034
3581
  var toEntity = to.node.entity;
3035
3582
  var connected = state.selected && state.selected.kind === "entity" && (state.selected.id === edge.from || state.selected.id === edge.to);
3583
+ var pathEdge = state.pathHighlight && state.pathHighlight.edges.has(edge.id);
3036
3584
  var geometry = new THREE.BufferGeometry().setFromPoints([from.mesh.position, to.mesh.position]);
3037
3585
  var material = new THREE.LineBasicMaterial({
3038
- color: new THREE.Color(edgeThemeColor(edge, fromEntity, toEntity)),
3586
+ color: new THREE.Color(pathEdge ? graphPalette.bridge : edgeThemeColor(edge, fromEntity, toEntity)),
3039
3587
  transparent: true,
3040
- opacity: threeEdgeOpacity(edge, fromEntity, toEntity, connected),
3588
+ opacity: pathEdge ? 0.82 : threeEdgeOpacity(edge, fromEntity, toEntity, connected),
3041
3589
  depthWrite: false,
3042
3590
  depthTest: false
3043
3591
  });
@@ -3374,7 +3922,7 @@
3374
3922
  : null;
3375
3923
  state.three.drag = null;
3376
3924
  if (picked) {
3377
- state.selected = { kind: "entity", id: picked.id };
3925
+ selectEntity(picked.id, true);
3378
3926
  render();
3379
3927
  }
3380
3928
  }
@@ -3395,7 +3943,7 @@
3395
3943
  function handleThreeDoubleClick(event) {
3396
3944
  var picked = pickThreeNode(event);
3397
3945
  if (!picked) return;
3398
- state.selected = { kind: "entity", id: picked.id };
3946
+ selectEntity(picked.id, true);
3399
3947
  render();
3400
3948
  }
3401
3949
 
@@ -3580,6 +4128,7 @@
3580
4128
  if (state.pan && state.pan.moved) return;
3581
4129
  if (event.target !== els.svg) return;
3582
4130
  state.selected = null;
4131
+ closeWorkspace();
3583
4132
  render();
3584
4133
  }
3585
4134