@kage-core/kage-graph-mcp 1.1.34 → 1.1.36

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
+ viewerPage: "overview",
17
+ viewerSection: "overview",
18
+ viewerAction: null,
19
+ graphActionFilter: "",
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,10 +129,20 @@
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"),
125
142
  relationFilter: document.getElementById("relationFilter"),
143
+ showUntrusted: document.getElementById("showUntrusted"),
144
+ showUncovered: document.getElementById("showUncovered"),
145
+ showMemoryCode: document.getElementById("showMemoryCode"),
126
146
  scopeFilter: document.getElementById("scopeFilter"),
127
147
  maxNodes: document.getElementById("maxNodes"),
128
148
  showDependencies: document.getElementById("showDependencies"),
@@ -143,17 +163,75 @@
143
163
  entityList: document.getElementById("entityList"),
144
164
  edgeList: document.getElementById("edgeList"),
145
165
  metricsSummary: document.getElementById("metricsSummary"),
166
+ graphInsightStatus: document.getElementById("graphInsightStatus"),
167
+ graphInsights: document.getElementById("graphInsights"),
146
168
  entityCount: document.getElementById("entityCount"),
147
169
  edgeCount: document.getElementById("edgeCount"),
148
170
  reviewCount: document.getElementById("reviewCount"),
171
+ dashboardStats: document.getElementById("dashboardStats"),
172
+ dashboardCharts: document.getElementById("dashboardCharts"),
173
+ memoryStatus: document.getElementById("memoryStatus"),
174
+ memoryStats: document.getElementById("memoryStats"),
175
+ memoryOverview: document.getElementById("memoryOverview"),
176
+ memorySearch: document.getElementById("memorySearch"),
177
+ memoryFilter: document.getElementById("memoryFilter"),
178
+ memoryList: document.getElementById("memoryList"),
179
+ ownersStatus: document.getElementById("ownersStatus"),
180
+ ownersSummary: document.getElementById("ownersSummary"),
181
+ ownersList: document.getElementById("ownersList"),
182
+ reviewOverview: document.getElementById("reviewOverview"),
149
183
  reviewList: document.getElementById("reviewList"),
184
+ proofOverview: document.getElementById("proofOverview"),
150
185
  proofStatus: document.getElementById("proofStatus"),
151
186
  proofList: document.getElementById("proofList"),
152
187
  intelligenceStatus: document.getElementById("intelligenceStatus"),
153
- intelligenceList: document.getElementById("intelligenceList")
188
+ intelligenceList: document.getElementById("intelligenceList"),
189
+ debugOverview: document.getElementById("debugOverview"),
190
+ pageEyebrow: document.getElementById("pageEyebrow"),
191
+ pageTitle: document.getElementById("pageTitle"),
192
+ viewerPageLinks: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-viewer-page]")) : [],
193
+ };
194
+
195
+ var PAGE_META = {
196
+ overview: {
197
+ eyebrow: "kage://overview",
198
+ title: "Repo dashboard",
199
+ summary: "What is safe to change next, what needs attention, and what is ready to hand off."
200
+ },
201
+ graph: {
202
+ eyebrow: "kage://graph",
203
+ title: "Dependency graph",
204
+ summary: "Search a file or symbol, then follow connected memory, routes, and tests before editing."
205
+ },
206
+ memory: {
207
+ eyebrow: "kage://memory",
208
+ title: "Memory library",
209
+ summary: "Find repo lore by file, feature, bug, command, or decision. Pick a packet to see linked code."
210
+ },
211
+ intel: {
212
+ eyebrow: "kage://risks",
213
+ title: "Risks",
214
+ summary: "Files, owners, and modules to inspect before changes. Each card links into the graph."
215
+ },
216
+ review: {
217
+ eyebrow: "kage://review",
218
+ title: "Review & handoff",
219
+ summary: "Blockers that must clear before another agent or teammate picks up this branch."
220
+ },
221
+ owners: {
222
+ eyebrow: "kage://owners",
223
+ title: "Owners & reviewers",
224
+ summary: "Local git ownership signal. Use it for reviewer routing and bus-factor checks."
225
+ },
226
+ data: {
227
+ eyebrow: "kage://artifacts",
228
+ title: "Artifacts & diagnostics",
229
+ summary: "Raw nodes, relations, and indexing health. Use only when graph or recall looks wrong."
230
+ }
154
231
  };
155
232
 
156
233
  var MEMORY_CODE_RELATIONS = new Set(["explains_symbol", "informs_symbol", "fixes_symbol", "applies_to_route", "verified_by_test", "affects_code_path"]);
234
+ var MEMORY_PACKET_TYPES = new Set(["memory", "command", "repo_map", "runbook", "bug_fix", "decision", "rationale", "convention", "workflow", "gotcha", "reference", "policy", "issue_context", "code_explanation", "negative_result", "constraint"]);
157
235
  var INSPECTOR_CONNECTION_LIMIT = 8;
158
236
  var PATH_BRIDGE_EDGE_LIMIT_PER_PATH = 8;
159
237
  var PATH_BRIDGE_EDGE_LIMIT_TOTAL = 160;
@@ -161,21 +239,40 @@
161
239
  var VISIBLE_EDGE_MIN = 160;
162
240
  var VISIBLE_EDGE_MAX = 560;
163
241
 
242
+ state.viewerPage = initialViewerPage();
243
+ applyViewerPage(state.viewerPage);
244
+ els.viewerPageLinks.forEach(function (link) {
245
+ link.addEventListener("click", function (event) {
246
+ var page = link.getAttribute("data-viewer-page") || "overview";
247
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return;
248
+ event.preventDefault();
249
+ navigateViewerPage(page);
250
+ });
251
+ });
164
252
  els.graphFile.addEventListener("change", handleFile);
165
253
  els.searchInput.addEventListener("input", scheduleRender);
166
- els.viewMode.addEventListener("change", render);
254
+ els.findPath.addEventListener("click", findDependencyPath);
255
+ els.clearPath.addEventListener("click", clearDependencyPath);
256
+ els.pathFromInput.addEventListener("keydown", function (event) { if (event.key === "Enter") findDependencyPath(); });
257
+ els.pathToInput.addEventListener("keydown", function (event) { if (event.key === "Enter") findDependencyPath(); });
258
+ els.viewMode.addEventListener("change", function () { clearGraphActionFilter(); render(); });
167
259
  els.renderMode.addEventListener("change", function () {
168
260
  state.lastVisibleSignature = "";
169
261
  render();
170
262
  });
171
- els.typeFilter.addEventListener("change", render);
172
- els.relationFilter.addEventListener("change", render);
263
+ els.typeFilter.addEventListener("change", function () { clearGraphActionFilter(); render(); });
264
+ els.relationFilter.addEventListener("change", function () { clearGraphActionFilter(); render(); });
173
265
  els.scopeFilter.addEventListener("change", render);
174
266
  els.maxNodes.addEventListener("change", render);
175
267
  els.showDependencies.addEventListener("change", render);
268
+ if (els.showUntrusted) els.showUntrusted.addEventListener("click", function () { applyGraphActionFilter("untrusted"); });
269
+ if (els.showUncovered) els.showUncovered.addEventListener("click", function () { applyGraphActionFilter("uncovered"); });
270
+ if (els.showMemoryCode) els.showMemoryCode.addEventListener("click", function () { applyGraphActionFilter("memory-code"); });
176
271
  els.zoomOut.addEventListener("click", function () { zoomGraph(0.82); });
177
272
  els.zoomIn.addEventListener("click", function () { zoomGraph(1.22); });
178
273
  els.fitView.addEventListener("click", fitActiveGraph);
274
+ if (els.memorySearch) els.memorySearch.addEventListener("input", renderMemoryLibrary);
275
+ if (els.memoryFilter) els.memoryFilter.addEventListener("change", renderMemoryLibrary);
179
276
  els.canvas.addEventListener("mousedown", startCanvasPointer);
180
277
  els.canvas.addEventListener("mousemove", moveCanvasPointer);
181
278
  els.canvas.addEventListener("mouseup", endCanvasPointer);
@@ -196,7 +293,9 @@
196
293
  window.addEventListener("resize", function () {
197
294
  resizeActiveGraph();
198
295
  });
199
- els.resetView.addEventListener("click", function () {
296
+ els.resetView.addEventListener("click", resetGraphView);
297
+
298
+ function resetGraphView() {
200
299
  els.searchInput.value = "";
201
300
  els.viewMode.value = "combined";
202
301
  els.renderMode.value = "2d";
@@ -206,11 +305,173 @@
206
305
  els.maxNodes.value = "90";
207
306
  els.showDependencies.checked = false;
208
307
  state.selected = null;
308
+ state.graphActionFilter = "";
309
+ clearDependencyPath(false);
209
310
  state.lastVisibleSignature = "";
210
311
  render();
211
- });
312
+ }
212
313
  loadFromUrlParams();
213
314
 
315
+ function initialViewerPage() {
316
+ var fileName = "";
317
+ try {
318
+ fileName = String(window.location.pathname || "").split("/").pop() || "index.html";
319
+ } catch (_error) {
320
+ fileName = "index.html";
321
+ }
322
+ var pageByFile = {
323
+ "index.html": "overview",
324
+ "": "overview",
325
+ "graph.html": "graph",
326
+ "memory.html": "memory",
327
+ "owners.html": "owners",
328
+ "intel.html": "intel",
329
+ "review.html": "review",
330
+ "data.html": "data"
331
+ };
332
+ return pageByFile[fileName] || "overview";
333
+ }
334
+
335
+ function applyViewerPage(page, updateLinks) {
336
+ var normalized = normalizeViewerPage(page);
337
+ state.viewerPage = normalized;
338
+ if (normalized === "overview") {
339
+ setViewerSection("overview");
340
+ } else if (normalized === "graph") {
341
+ setViewerSection("graph");
342
+ } else {
343
+ setViewerSection("graph", pageToAction(normalized));
344
+ }
345
+ state.viewerPage = normalized;
346
+ applyPageHeader(normalized);
347
+ if (updateLinks !== false) syncViewerPageLinks();
348
+ syncViewerPageClass();
349
+ }
350
+
351
+ function applyPageHeader(page) {
352
+ var meta = PAGE_META[page] || PAGE_META.overview;
353
+ if (els.pageEyebrow) els.pageEyebrow.textContent = meta.eyebrow;
354
+ if (els.pageTitle) els.pageTitle.textContent = meta.title;
355
+ if (els.graphSummary && !state.graph) els.graphSummary.textContent = meta.summary;
356
+ try {
357
+ if (typeof document !== "undefined" && document.title !== undefined) {
358
+ document.title = "Kage " + meta.title.toLowerCase() + " viewer";
359
+ }
360
+ } catch (_error) {
361
+ // ignore
362
+ }
363
+ }
364
+
365
+ function normalizeViewerPage(page) {
366
+ var normalized = String(page || "overview").toLowerCase();
367
+ if (normalized === "intelligence") normalized = "intel";
368
+ if (["overview", "graph", "memory", "owners", "intel", "review", "data"].indexOf(normalized) === -1) return "overview";
369
+ return normalized;
370
+ }
371
+
372
+ function pageToAction(page) {
373
+ if (page === "intel") return "intelligence";
374
+ if (page === "owners") return "intelligence";
375
+ if (page === "memory") return "memory";
376
+ if (page === "review") return "review";
377
+ if (page === "data") return "data";
378
+ return null;
379
+ }
380
+
381
+ function pageFromSection(section, action) {
382
+ if (section === "overview") return "overview";
383
+ if (action === "intelligence") return "intel";
384
+ if (action === "memory") return "memory";
385
+ if (action === "review") return "review";
386
+ if (action === "data") return "data";
387
+ return "graph";
388
+ }
389
+
390
+ function viewerPageHref(page) {
391
+ var fileByPage = {
392
+ overview: "./",
393
+ graph: "./graph.html",
394
+ memory: "./memory.html",
395
+ owners: "./owners.html",
396
+ intel: "./intel.html",
397
+ review: "./review.html",
398
+ data: "./data.html"
399
+ };
400
+ var search = "";
401
+ try {
402
+ search = window.location.search || "";
403
+ } catch (_error) {
404
+ search = "";
405
+ }
406
+ return (fileByPage[normalizeViewerPage(page)] || "./") + search;
407
+ }
408
+
409
+ function navigateViewerPage(page) {
410
+ window.location.href = viewerPageHref(page);
411
+ }
412
+
413
+ function showViewerPageInPlace(page) {
414
+ applyViewerPage(page);
415
+ try {
416
+ window.history.pushState({}, "", viewerPageHref(page));
417
+ } catch (_error) {
418
+ // Static/file viewers can ignore history failures; visual state is enough.
419
+ }
420
+ }
421
+
422
+ function syncViewerPageLinks() {
423
+ els.viewerPageLinks.forEach(function (link) {
424
+ var page = normalizeViewerPage(link.getAttribute("data-viewer-page"));
425
+ link.setAttribute("href", viewerPageHref(page));
426
+ var active = page === state.viewerPage;
427
+ link.classList.toggle("active", active);
428
+ if (active) link.setAttribute("aria-current", "page");
429
+ else link.removeAttribute("aria-current");
430
+ });
431
+ }
432
+
433
+ function syncViewerPageClass() {
434
+ if (!document.body || !document.body.classList) return;
435
+ document.body.classList.remove(
436
+ "viewer-page-overview",
437
+ "viewer-page-graph",
438
+ "viewer-page-memory",
439
+ "viewer-page-owners",
440
+ "viewer-page-intel",
441
+ "viewer-page-review",
442
+ "viewer-page-data"
443
+ );
444
+ document.body.classList.add("viewer-page-" + normalizeViewerPage(state.viewerPage));
445
+ }
446
+
447
+ function setViewerSection(section, action) {
448
+ state.viewerSection = section === "graph" ? "graph" : "overview";
449
+ state.viewerAction = action || null;
450
+ state.viewerPage = pageFromSection(state.viewerSection, state.viewerAction);
451
+ if (state.viewerSection === "overview") closeWorkspace();
452
+ if (document.body && document.body.classList) {
453
+ document.body.classList.remove("viewer-section-overview", "viewer-section-graph");
454
+ document.body.classList.add("viewer-section-" + state.viewerSection);
455
+ }
456
+ syncViewerPageLinks();
457
+ syncViewerPageClass();
458
+ if (state.viewerSection === "graph") resizeActiveGraph();
459
+ }
460
+
461
+ function closeWorkspace() {
462
+ if (document.body && document.body.classList) {
463
+ document.body.classList.remove("viewer-workspace-open");
464
+ }
465
+ }
466
+
467
+ function selectEntity(id, openInspector) {
468
+ state.selected = { kind: "entity", id: id };
469
+ }
470
+
471
+ function selectEdge(id, openInspector) {
472
+ state.selected = { kind: "edge", id: id };
473
+ }
474
+
214
475
  function handleFile(event) {
215
476
  var files = Array.from(event.target.files || []);
216
477
  if (!files.length) return;
@@ -224,7 +485,8 @@
224
485
  state.entityById = new Map();
225
486
  state.episodesById = new Map();
226
487
  els.emptyState.classList.add("hidden");
227
- els.graphSummary.textContent = "Metrics loaded.";
488
+ var pageMeta = PAGE_META[state.viewerPage] || PAGE_META.overview;
489
+ els.graphSummary.textContent = state.viewerPage === "graph" ? "Metrics loaded." : pageMeta.summary;
228
490
  renderMetrics();
229
491
  return;
230
492
  }
@@ -258,11 +520,17 @@
258
520
  return [episode.id, episode];
259
521
  }));
260
522
  state.selected = null;
523
+ closeWorkspace();
524
+ clearDependencyPath(false);
261
525
  state.lastVisibleSignature = "";
262
526
 
263
527
  populateFilters();
528
+ populatePathOptions();
264
529
  els.emptyState.classList.add("hidden");
265
- els.graphSummary.textContent = fileName + " loaded: " + entities.length + " nodes, " + edges.length + " relations.";
530
+ var meta = PAGE_META[state.viewerPage] || PAGE_META.overview;
531
+ els.graphSummary.textContent = state.viewerPage === "graph"
532
+ ? fileName + " loaded: " + entities.length + " nodes, " + edges.length + " relations."
533
+ : meta.summary;
266
534
  render();
267
535
  }
268
536
 
@@ -277,6 +545,8 @@
277
545
 
278
546
  function loadFromUrlParams() {
279
547
  var params = new URLSearchParams(window.location.search);
548
+ var requestedSection = String(params.get("section") || "").toLowerCase();
549
+ if (requestedSection === "graph" || requestedSection === "overview") setViewerSection(requestedSection);
280
550
  applyRequestedView(params.get("view") || params.get("mode"));
281
551
  applyRequestedRenderMode(params.get("render") || params.get("graphMode"));
282
552
  var memoryGraphPaths = splitParamValues(params.getAll("graph"));
@@ -836,6 +1106,188 @@
836
1106
  });
837
1107
  }
838
1108
 
1109
+ function populatePathOptions() {
1110
+ if (!els.pathNodeOptions) return;
1111
+ var seen = new Set();
1112
+ els.pathNodeOptions.textContent = "";
1113
+ pathCandidateEntities().slice(0, 500).forEach(function (entity) {
1114
+ [entity.path, displayName(entity), entity.id].filter(Boolean).forEach(function (value) {
1115
+ var text = String(value);
1116
+ if (seen.has(text)) return;
1117
+ seen.add(text);
1118
+ var option = document.createElement("option");
1119
+ option.value = text;
1120
+ els.pathNodeOptions.appendChild(option);
1121
+ });
1122
+ });
1123
+ }
1124
+
1125
+ function pathCandidateEntities() {
1126
+ return state.entities
1127
+ .filter(function (entity) {
1128
+ return entity.graph_kind === "code" && ["file", "symbol", "route", "test", "script"].indexOf(entity.type) !== -1;
1129
+ })
1130
+ .sort(function (a, b) {
1131
+ return entityImportance(b) - entityImportance(a) || displayName(a).localeCompare(displayName(b));
1132
+ });
1133
+ }
1134
+
1135
+ function resolvePathEntity(value) {
1136
+ var query = String(value || "").trim();
1137
+ if (!query) return null;
1138
+ var lower = query.toLowerCase();
1139
+ var candidates = pathCandidateEntities();
1140
+ var exact = candidates.filter(function (entity) {
1141
+ return String(entity.id || "").toLowerCase() === lower ||
1142
+ String(entity.path || "").toLowerCase() === lower ||
1143
+ displayName(entity).toLowerCase() === lower;
1144
+ });
1145
+ if (exact.length) return exact[0];
1146
+ var partial = candidates.filter(function (entity) {
1147
+ return String(entity.path || "").toLowerCase().indexOf(lower) !== -1 ||
1148
+ displayName(entity).toLowerCase().indexOf(lower) !== -1 ||
1149
+ String(entity.id || "").toLowerCase().indexOf(lower) !== -1;
1150
+ });
1151
+ return partial[0] || null;
1152
+ }
1153
+
1154
+ function codePathEdges() {
1155
+ var relations = new Set(["imports", "imports_external", "defines_symbol", "calls", "covers", "defines_route", "handled_by"]);
1156
+ return state.edges.filter(function (edge) {
1157
+ if (!relations.has(edge.relation)) return false;
1158
+ var from = state.entityById.get(edge.from);
1159
+ var to = state.entityById.get(edge.to);
1160
+ return from && to && from.graph_kind === "code" && to.graph_kind === "code";
1161
+ });
1162
+ }
1163
+
1164
+ function shortestCodePath(fromId, toId, undirected) {
1165
+ var adjacency = new Map();
1166
+ codePathEdges().forEach(function (edge) {
1167
+ if (!adjacency.has(edge.from)) adjacency.set(edge.from, []);
1168
+ adjacency.get(edge.from).push({ to: edge.to, edge: edge });
1169
+ if (undirected) {
1170
+ if (!adjacency.has(edge.to)) adjacency.set(edge.to, []);
1171
+ adjacency.get(edge.to).push({ to: edge.from, edge: edge });
1172
+ }
1173
+ });
1174
+ var queue = [fromId];
1175
+ var seen = new Set([fromId]);
1176
+ var previous = new Map();
1177
+ while (queue.length) {
1178
+ var current = queue.shift();
1179
+ if (current === toId) break;
1180
+ (adjacency.get(current) || []).forEach(function (step) {
1181
+ if (seen.has(step.to)) return;
1182
+ seen.add(step.to);
1183
+ previous.set(step.to, { from: current, edge: step.edge });
1184
+ queue.push(step.to);
1185
+ });
1186
+ }
1187
+ if (!seen.has(toId)) return null;
1188
+ var nodes = [toId];
1189
+ var edges = [];
1190
+ var cursor = toId;
1191
+ while (cursor !== fromId) {
1192
+ var prev = previous.get(cursor);
1193
+ if (!prev) return null;
1194
+ edges.unshift(prev.edge.id);
1195
+ cursor = prev.from;
1196
+ nodes.unshift(cursor);
1197
+ }
1198
+ return { nodes: nodes, edges: edges };
1199
+ }
1200
+
1201
+ function findDependencyPath() {
1202
+ var from = resolvePathEntity(els.pathFromInput.value);
1203
+ var to = resolvePathEntity(els.pathToInput.value);
1204
+ if (!from || !to) {
1205
+ setPathStatus("Could not resolve both endpoints. Try a file path or exact symbol name.", "warn");
1206
+ return;
1207
+ }
1208
+ if (from.id === to.id) {
1209
+ setPathStatus("Pick two different code nodes.", "warn");
1210
+ return;
1211
+ }
1212
+ var direction = "forward";
1213
+ var path = shortestCodePath(from.id, to.id, false);
1214
+ if (!path) {
1215
+ var reverse = shortestCodePath(to.id, from.id, false);
1216
+ if (reverse) {
1217
+ direction = "reverse";
1218
+ path = {
1219
+ nodes: reverse.nodes.slice().reverse(),
1220
+ edges: reverse.edges.slice().reverse()
1221
+ };
1222
+ }
1223
+ }
1224
+ if (!path) {
1225
+ direction = "undirected";
1226
+ path = shortestCodePath(from.id, to.id, true);
1227
+ }
1228
+ if (!path) {
1229
+ state.pathHighlight = { nodes: new Set(), edges: new Set(), direction: "", steps: [] };
1230
+ els.pathResult.textContent = "";
1231
+ els.pathResult.className = "path-result";
1232
+ setPathStatus("No code dependency path found between those nodes.", "warn");
1233
+ render();
1234
+ return;
1235
+ }
1236
+ state.pathHighlight = {
1237
+ nodes: new Set(path.nodes),
1238
+ edges: new Set(path.edges),
1239
+ direction: direction,
1240
+ steps: path.nodes
1241
+ };
1242
+ setPathStatus(path.nodes.length + " nodes, " + path.edges.length + " edge(s), " + direction + " path.", "ok");
1243
+ renderPathResult(path.nodes, path.edges);
1244
+ state.lastVisibleSignature = "";
1245
+ render();
1246
+ }
1247
+
1248
+ function setPathStatus(text, status) {
1249
+ els.pathStatus.textContent = text;
1250
+ els.pathStatus.className = "path-status" + (status ? " " + status : "");
1251
+ }
1252
+
1253
+ function clearDependencyPath(renderNow) {
1254
+ state.pathHighlight = { nodes: new Set(), edges: new Set(), direction: "", steps: [] };
1255
+ if (els.pathFromInput) els.pathFromInput.value = "";
1256
+ if (els.pathToInput) els.pathToInput.value = "";
1257
+ if (els.pathResult) {
1258
+ els.pathResult.textContent = "";
1259
+ els.pathResult.className = "path-result";
1260
+ }
1261
+ if (els.pathStatus) setPathStatus("Pick two code nodes to trace a dependency path.", "");
1262
+ if (renderNow !== false) {
1263
+ state.lastVisibleSignature = "";
1264
+ render();
1265
+ }
1266
+ }
1267
+
1268
+ function renderPathResult(nodes, edges) {
1269
+ els.pathResult.textContent = "";
1270
+ nodes.forEach(function (id, index) {
1271
+ var entity = state.entityById.get(id);
1272
+ var edge = index > 0 ? state.edgeById.get(edges[index - 1]) : null;
1273
+ var button = document.createElement("button");
1274
+ button.type = "button";
1275
+ button.className = "path-step";
1276
+ button.innerHTML = "<strong></strong><span></span>";
1277
+ button.querySelector("strong").textContent = entity ? displayName(entity) : id;
1278
+ button.querySelector("span").textContent = [
1279
+ index === 0 ? "start" : edge ? edge.relation : "path",
1280
+ entity && entity.path ? entity.path : id
1281
+ ].filter(Boolean).join(" | ");
1282
+ button.addEventListener("click", function () {
1283
+ selectEntity(id, true);
1284
+ render();
1285
+ });
1286
+ els.pathResult.appendChild(button);
1287
+ });
1288
+ els.pathResult.className = "path-result visible";
1289
+ }
1290
+
839
1291
  function layoutGraph(visibleIds) {
840
1292
  state.positions = new Map();
841
1293
  var candidates = state.entities.filter(function (entity) {
@@ -908,6 +1360,10 @@
908
1360
  matchedEdgeIds = new Set(state.edges.filter(function (edge) { return mode === "combined" || edge.graph_kind === mode; }).map(function (edge) { return edge.id; }));
909
1361
  }
910
1362
 
1363
+ var actionFiltered = applyMatchedGraphActionFilter(matchedEntityIds, matchedEdgeIds);
1364
+ matchedEntityIds = actionFiltered.entities;
1365
+ matchedEdgeIds = actionFiltered.edges;
1366
+
911
1367
  var visible = refineVisibleGraph(matchedEntityIds, matchedEdgeIds, {
912
1368
  query: query,
913
1369
  type: type,
@@ -926,12 +1382,7 @@
926
1382
  state.lastVisibleSignature = nextSignature;
927
1383
 
928
1384
  renderActiveGraph(graphChanged);
929
- renderLists();
930
- renderDetails();
931
- renderMetrics();
932
- renderReviewQueue();
933
- renderProof();
934
- renderIntelligence();
1385
+ renderPagePanels();
935
1386
  }
936
1387
 
937
1388
  function scheduleRender() {
@@ -942,6 +1393,88 @@
942
1393
  });
943
1394
  }
944
1395
 
1396
+ function renderPagePanels() {
1397
+ if (state.viewerPage === "graph") {
1398
+ renderDetails();
1399
+ renderMetrics();
1400
+ return;
1401
+ }
1402
+ if (state.viewerPage === "memory") {
1403
+ renderDetails();
1404
+ renderMemoryLibrary();
1405
+ return;
1406
+ }
1407
+ if (state.viewerPage === "owners") {
1408
+ renderOwners();
1409
+ return;
1410
+ }
1411
+ if (state.viewerPage === "intel") {
1412
+ renderIntelligence();
1413
+ return;
1414
+ }
1415
+ if (state.viewerPage === "review") {
1416
+ renderReviewQueue();
1417
+ renderProof();
1418
+ return;
1419
+ }
1420
+ if (state.viewerPage === "data") {
1421
+ renderDetails();
1422
+ renderArtifactDiagnostics(state.entities, state.edges);
1423
+ renderLists();
1424
+ return;
1425
+ }
1426
+ renderDashboard();
1427
+ }
1428
+
1429
+ function applyGraphActionFilter(filter) {
1430
+ state.graphActionFilter = state.graphActionFilter === filter ? "" : filter;
1431
+ if (filter === "memory-code") {
1432
+ els.viewMode.value = "combined";
1433
+ els.relationFilter.value = "__memory_code__";
1434
+ } else if (state.graphActionFilter) {
1435
+ els.viewMode.value = "combined";
1436
+ els.relationFilter.value = "";
1437
+ }
1438
+ state.lastVisibleSignature = "";
1439
+ render();
1440
+ }
1441
+
1442
+ function clearGraphActionFilter() {
1443
+ state.graphActionFilter = "";
1444
+ }
1445
+
1446
+ function applyMatchedGraphActionFilter(entityIds, edgeIds) {
1447
+ if (!state.graphActionFilter) return { entities: entityIds, edges: edgeIds };
1448
+ if (state.graphActionFilter === "memory-code") {
1449
+ var memoryCodeEdges = state.edges.filter(isMemoryCodeEdge);
1450
+ return entitiesForEdges(memoryCodeEdges);
1451
+ }
1452
+ if (state.graphActionFilter === "untrusted") {
1453
+ var flagged = state.edges.filter(function (edge) { return reviewStatus(edge) !== "ok"; });
1454
+ return entitiesForEdges(flagged);
1455
+ }
1456
+ if (state.graphActionFilter === "uncovered") {
1457
+ var covered = memoryLinkedCodeKeys();
1458
+ var uncovered = state.entities.filter(function (entity) {
1459
+ return entity.graph_kind === "code" && entity.type === "file" && !covered.has(codeCoverageKey(entity));
1460
+ });
1461
+ var entities = new Set(uncovered.map(function (entity) { return entity.id; }));
1462
+ return { entities: entities, edges: edgesWithVisibleEndpoints(new Set(state.edges.map(function (edge) { return edge.id; })), entities) };
1463
+ }
1464
+ return { entities: entityIds, edges: edgeIds };
1465
+ }
1466
+
1467
+ function entitiesForEdges(edges) {
1468
+ var entities = new Set();
1469
+ var edgeIds = new Set();
1470
+ edges.forEach(function (edge) {
1471
+ edgeIds.add(edge.id);
1472
+ if (state.entityById.has(edge.from)) entities.add(edge.from);
1473
+ if (state.entityById.has(edge.to)) entities.add(edge.to);
1474
+ });
1475
+ return { entities: entities, edges: edgeIds };
1476
+ }
1477
+
945
1478
  function refineVisibleGraph(entityIds, edgeIds, options) {
946
1479
  var entities = new Set(entityIds);
947
1480
  var edges = new Set(edgeIds);
@@ -981,7 +1514,16 @@
981
1514
  }
982
1515
  }
983
1516
 
984
- return { entities: entities, edges: capVisibleEdges(edgesWithVisibleEndpoints(edges, entities), entities, options) };
1517
+ if (state.pathHighlight && state.pathHighlight.nodes.size) {
1518
+ state.pathHighlight.nodes.forEach(function (id) { entities.add(id); });
1519
+ state.pathHighlight.edges.forEach(function (id) { edges.add(id); });
1520
+ }
1521
+
1522
+ var cappedEdges = capVisibleEdges(edgesWithVisibleEndpoints(edges, entities), entities, options);
1523
+ if (state.pathHighlight && state.pathHighlight.edges.size) {
1524
+ state.pathHighlight.edges.forEach(function (id) { if (edges.has(id)) cappedEdges.add(id); });
1525
+ }
1526
+ return { entities: entities, edges: cappedEdges };
985
1527
  }
986
1528
 
987
1529
  function capVisibleEdges(edgeIds, entityIds, options) {
@@ -1444,8 +1986,8 @@
1444
1986
  ctx.fillStyle = graphPalette.background;
1445
1987
  ctx.fillRect(0, 0, width, height);
1446
1988
  var gradient = ctx.createRadialGradient(width * 0.52, height * 0.44, 40, width * 0.52, height * 0.44, Math.max(width, height) * 0.72);
1447
- gradient.addColorStop(0, "rgba(65,255,143,0.080)");
1448
- gradient.addColorStop(0.48, "rgba(65,255,143,0.018)");
1989
+ gradient.addColorStop(0, "rgba(65,255,143,0.145)");
1990
+ gradient.addColorStop(0.48, "rgba(65,255,143,0.030)");
1449
1991
  gradient.addColorStop(1, "rgba(2,5,3,0)");
1450
1992
  ctx.fillStyle = gradient;
1451
1993
  ctx.fillRect(0, 0, width, height);
@@ -1484,9 +2026,10 @@
1484
2026
  var to = nodeMap.get(edge.to);
1485
2027
  if (!from || !to) return;
1486
2028
  var connected = focusId && (edge.from === focusId || edge.to === focusId);
2029
+ var pathEdge = state.pathHighlight && state.pathHighlight.edges.has(edge.id);
1487
2030
  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));
2031
+ var alpha = pathEdge ? 0.92 : !matches ? 0.035 : focusId ? (connected ? 0.62 : 0.055) : (dense ? 0.13 : 0.22);
2032
+ var color = hexToRgb(pathEdge ? graphPalette.bridge : edgeThemeColor(edge, from.entity, to.entity));
1490
2033
  var dx = to.x - from.x;
1491
2034
  var dy = to.y - from.y;
1492
2035
  var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
@@ -1497,10 +2040,10 @@
1497
2040
  ctx.moveTo(from.x, from.y);
1498
2041
  ctx.quadraticCurveTo(cx, cy, to.x, to.y);
1499
2042
  ctx.strokeStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + alpha + ")";
1500
- ctx.lineWidth = connected ? 2.2 : 1;
2043
+ ctx.lineWidth = pathEdge ? 3 : connected ? 2.2 : 1;
1501
2044
  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);
2045
+ if (pathEdge || connected || (!dense && state.sim.zoom > 1.25)) drawArrow(ctx, from, to, cx, cy, color, alpha);
2046
+ if ((pathEdge || connected) && state.sim.zoom > 0.62) drawEdgeLabel(ctx, edge, cx, cy);
1504
2047
  });
1505
2048
  }
1506
2049
 
@@ -1514,14 +2057,15 @@
1514
2057
  var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
1515
2058
  var hovered = state.sim.hoverNode && state.sim.hoverNode.id === node.id;
1516
2059
  var connected = focusId && (node.id === focusId || (focusNeighbors && focusNeighbors.has(node.id)));
2060
+ var pathNode = state.pathHighlight && state.pathHighlight.nodes.has(node.id);
1517
2061
  var matches = matchesSearchQuery(entity, query);
1518
- var alpha = !matches ? 0.12 : focusId && !connected ? 0.20 : 1;
2062
+ var alpha = pathNode ? 1 : !matches ? 0.12 : focusId && !connected ? 0.20 : 1;
1519
2063
  var color = nodeThemeColor(entity);
1520
2064
  ctx.save();
1521
2065
  ctx.globalAlpha = alpha;
1522
- if (selected || hovered) {
1523
- ctx.shadowColor = color;
1524
- ctx.shadowBlur = selected ? 14 : 10;
2066
+ if (selected || hovered || pathNode || entity.graph_kind === "memory") {
2067
+ ctx.shadowColor = pathNode ? graphPalette.bridge : color;
2068
+ ctx.shadowBlur = selected ? 16 : pathNode ? 14 : entity.graph_kind === "memory" ? 9 : 10;
1525
2069
  }
1526
2070
  drawNodeShape(ctx, node.x, node.y, node.r, entity);
1527
2071
  ctx.fillStyle = nodeFillColor(entity);
@@ -1538,18 +2082,18 @@
1538
2082
  }
1539
2083
  ctx.restore();
1540
2084
 
1541
- if (selected || hovered) {
2085
+ if (selected || hovered || pathNode) {
1542
2086
  ctx.save();
1543
2087
  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;
2088
+ ctx.strokeStyle = pathNode ? graphPalette.bridge : color;
2089
+ ctx.lineWidth = selected ? 2.6 : pathNode ? 2.2 : 1.8;
2090
+ ctx.shadowColor = pathNode ? graphPalette.bridge : color;
1547
2091
  ctx.shadowBlur = 8;
1548
2092
  ctx.stroke();
1549
2093
  ctx.restore();
1550
2094
  }
1551
2095
 
1552
- var shouldLabel = matches && (selected || hovered || (query.active && matches) || (!dense && state.sim.zoom > 0.75) || (dense && state.sim.zoom > 1.55 && node.r > 13));
2096
+ 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
2097
  if (shouldLabel) drawNodeLabel(ctx, node, selected || hovered);
1554
2098
  });
1555
2099
  }
@@ -1681,7 +2225,7 @@
1681
2225
  class: "edge-hit"
1682
2226
  });
1683
2227
  hit.addEventListener("click", function () {
1684
- state.selected = { kind: "edge", id: edge.id };
2228
+ selectEdge(edge.id, true);
1685
2229
  render();
1686
2230
  });
1687
2231
  hit.addEventListener("mousedown", function (event) {
@@ -1733,7 +2277,7 @@
1733
2277
  var title = svgEl("title");
1734
2278
  title.textContent = displayName(entity) + "\n" + (entity.summary || "");
1735
2279
  group.addEventListener("click", function () {
1736
- state.selected = { kind: "entity", id: entity.id };
2280
+ selectEntity(entity.id, true);
1737
2281
  render();
1738
2282
  });
1739
2283
  group.addEventListener("mousedown", function (event) {
@@ -1764,12 +2308,16 @@
1764
2308
  }
1765
2309
 
1766
2310
  function renderLists() {
1767
- var visibleEntities = state.entities.filter(function (entity) {
1768
- return state.visibleEntityIds.has(entity.id);
1769
- });
1770
- var visibleEdges = state.edges.filter(function (edge) {
1771
- return state.visibleEdgeIds.has(edge.id);
1772
- }).sort(function (a, b) {
2311
+ var visibleEntities = state.viewerPage === "data"
2312
+ ? state.entities.slice()
2313
+ : state.entities.filter(function (entity) {
2314
+ return state.visibleEntityIds.has(entity.id);
2315
+ });
2316
+ var visibleEdges = (state.viewerPage === "data"
2317
+ ? state.edges.slice()
2318
+ : state.edges.filter(function (edge) {
2319
+ return state.visibleEdgeIds.has(edge.id);
2320
+ })).sort(function (a, b) {
1773
2321
  return reviewRank(a) - reviewRank(b);
1774
2322
  });
1775
2323
 
@@ -1778,7 +2326,11 @@
1778
2326
  els.entityList.textContent = "";
1779
2327
  els.edgeList.textContent = "";
1780
2328
 
1781
- visibleEntities.forEach(function (entity) {
2329
+ var rowLimit = state.viewerPage === "data" ? 40 : 80;
2330
+ var entityRows = visibleEntities.slice(0, rowLimit);
2331
+ var edgeRows = visibleEdges.slice(0, rowLimit);
2332
+
2333
+ entityRows.forEach(function (entity) {
1782
2334
  var button = document.createElement("button");
1783
2335
  button.type = "button";
1784
2336
  button.className = classNames("list-item", state.selected && state.selected.kind === "entity" && state.selected.id === entity.id && "selected");
@@ -1786,13 +2338,16 @@
1786
2338
  button.querySelector(".item-title").textContent = displayName(entity);
1787
2339
  button.querySelector(".item-meta").textContent = (entity.type || "unknown") + " | " + entity.id;
1788
2340
  button.addEventListener("click", function () {
1789
- state.selected = { kind: "entity", id: entity.id };
2341
+ selectEntity(entity.id, true);
1790
2342
  render();
1791
2343
  });
1792
2344
  els.entityList.appendChild(button);
1793
2345
  });
2346
+ if (visibleEntities.length > entityRows.length) {
2347
+ appendListNote(els.entityList, "Showing " + entityRows.length + " of " + visibleEntities.length + ". Use search to narrow.");
2348
+ }
1794
2349
 
1795
- visibleEdges.forEach(function (edge) {
2350
+ edgeRows.forEach(function (edge) {
1796
2351
  var button = document.createElement("button");
1797
2352
  button.type = "button";
1798
2353
  button.className = classNames("list-item", state.selected && state.selected.kind === "edge" && state.selected.id === edge.id && "selected");
@@ -1800,18 +2355,30 @@
1800
2355
  button.querySelector(".item-title").textContent = edge.relation || "related";
1801
2356
  button.querySelector(".item-meta").textContent = displayName(state.entityById.get(edge.from)) + " -> " + displayName(state.entityById.get(edge.to)) + " | " + reviewStatus(edge);
1802
2357
  button.addEventListener("click", function () {
1803
- state.selected = { kind: "edge", id: edge.id };
2358
+ selectEdge(edge.id, true);
1804
2359
  render();
1805
2360
  });
1806
2361
  els.edgeList.appendChild(button);
1807
2362
  });
2363
+ if (visibleEdges.length > edgeRows.length) {
2364
+ appendListNote(els.edgeList, "Showing " + edgeRows.length + " of " + visibleEdges.length + ". Filter by relation or select a node first.");
2365
+ }
2366
+ }
2367
+
2368
+ function appendListNote(parent, text) {
2369
+ var note = document.createElement("div");
2370
+ note.className = "list-note";
2371
+ note.textContent = text;
2372
+ parent.appendChild(note);
1808
2373
  }
1809
2374
 
1810
2375
  function renderDetails() {
1811
2376
  if (!state.selected) {
2377
+ setSelectionBodyState(null);
1812
2378
  els.selectionDetails.className = "details-empty";
1813
- els.selectionDetails.textContent = "Select an entity or edge.";
2379
+ els.selectionDetails.textContent = "Select a node or relation to see what it means, why it exists, and which connected memory or code to inspect next.";
1814
2380
  els.selectionStatus.textContent = "No selection";
2381
+ if (!state.pathHighlight.steps.length) setPathStatus("Select a code node, then trace to another code node when you need impact proof.", "");
1815
2382
  return;
1816
2383
  }
1817
2384
 
@@ -1820,10 +2387,12 @@
1820
2387
  : state.edges.find(function (edge) { return edge.id === state.selected.id; });
1821
2388
 
1822
2389
  if (!item) {
2390
+ setSelectionBodyState(null);
1823
2391
  els.selectionDetails.textContent = "Selection no longer exists.";
1824
2392
  return;
1825
2393
  }
1826
2394
 
2395
+ setSelectionBodyState(state.selected.kind === "entity" ? item : null);
1827
2396
  els.selectionDetails.className = "";
1828
2397
  els.selectionDetails.textContent = "";
1829
2398
  var title = document.createElement("div");
@@ -1860,6 +2429,32 @@
1860
2429
  els.selectionDetails.appendChild(kind);
1861
2430
  els.selectionDetails.appendChild(rows);
1862
2431
  renderInspectorConnections(item);
2432
+ if (state.selected.kind === "entity" && item.graph_kind === "code") prefillPathFromSelection(true);
2433
+ }
2434
+
2435
+ function setSelectionBodyState(entity) {
2436
+ if (!document.body || !document.body.classList) return;
2437
+ document.body.classList.toggle("has-selection", Boolean(state.selected));
2438
+ document.body.classList.toggle("has-code-selection", Boolean(entity && entity.graph_kind === "code"));
2439
+ }
2440
+
2441
+ function prefillPathFromSelection(silent) {
2442
+ if (!state.selected || state.selected.kind !== "entity") {
2443
+ if (!silent) setPathStatus("Select a code node first. Path tracing is for files, symbols, routes, tests, and scripts.", "warn");
2444
+ return;
2445
+ }
2446
+ var entity = state.entityById.get(state.selected.id);
2447
+ if (!entity || entity.graph_kind !== "code" || ["file", "symbol", "route", "test", "script"].indexOf(entity.type) === -1) {
2448
+ if (!silent) setPathStatus("Select a code node first. Memory nodes are shown through memory-code links, not code path tracing.", "warn");
2449
+ return;
2450
+ }
2451
+ if (!els.pathFromInput.value || silent) {
2452
+ els.pathFromInput.value = entity.path || displayName(entity) || entity.id;
2453
+ }
2454
+ if (!silent) {
2455
+ els.pathToInput.focus();
2456
+ setPathStatus("Selected " + displayName(entity) + ". Pick a target test, route, file, or symbol to trace impact.", "ok");
2457
+ }
1863
2458
  }
1864
2459
 
1865
2460
  function entityDetailRows(entity) {
@@ -1987,6 +2582,23 @@
1987
2582
  return Boolean(edge && (edge.memory_code_link || isMemoryCodeRelation(edge.relation)));
1988
2583
  }
1989
2584
 
2585
+ function codeCoverageKey(entity) {
2586
+ if (!entity) return "";
2587
+ return String(entity.path || entity.id || "").replace(/\\/g, "/").replace(/^\.\//, "");
2588
+ }
2589
+
2590
+ function memoryLinkedCodeKeys() {
2591
+ var keys = new Set();
2592
+ state.edges.filter(isMemoryCodeEdge).forEach(function (edge) {
2593
+ [state.entityById.get(edge.from), state.entityById.get(edge.to)].forEach(function (entity) {
2594
+ if (!entity || entity.graph_kind !== "code") return;
2595
+ var key = codeCoverageKey(entity);
2596
+ if (key) keys.add(key);
2597
+ });
2598
+ });
2599
+ return keys;
2600
+ }
2601
+
1990
2602
  function connectionImportance(link) {
1991
2603
  var relation = String(link.edge.relation || "");
1992
2604
  var score = entityImportance(link.other);
@@ -2029,7 +2641,8 @@
2029
2641
  button.querySelector(".detail-link-meta").textContent = item.meta;
2030
2642
  button.querySelector(".detail-link-body").textContent = item.body;
2031
2643
  button.addEventListener("click", function () {
2032
- state.selected = item.entity ? { kind: "entity", id: item.entity.id } : { kind: "edge", id: item.edge.id };
2644
+ if (item.entity) selectEntity(item.entity.id, true);
2645
+ else selectEdge(item.edge.id, true);
2033
2646
  render();
2034
2647
  });
2035
2648
  list.appendChild(button);
@@ -2088,13 +2701,15 @@
2088
2701
  }).length;
2089
2702
  var evidenceEdges = visibleEdges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length > 0; }).length;
2090
2703
  var official = state.metrics;
2704
+ var memoryCodeLinks = state.edges.filter(isMemoryCodeEdge).length;
2705
+ var pendingReview = official && official.memory_graph ? Number(firstNumber(official.memory_graph.pending_packets, 0)) : 0;
2091
2706
  var metrics = official ? [
2092
- ["Readiness", official.harness.readiness_score + "/100"],
2093
- ["Code Files", official.code_graph.files],
2094
- ["Structural Symbols", official.structural_index ? official.structural_index.symbols : official.code_graph.symbols],
2095
- ["Parser Coverage", official.code_graph.indexer_coverage_percent + "%"],
2096
- ["Cache Hits", official.structural_index ? official.structural_index.cache_hits : official.code_graph.cache_hits],
2097
- ["Tokens Saved", official.savings ? official.savings.estimated_tokens_saved_per_recall : "n/a"]
2707
+ ["Validation", official.harness && official.harness.validation_ok ? "Clean" : "Check"],
2708
+ ["Review queue", pendingReview ? pendingReview + " pending" : "Clear"],
2709
+ ["Reusable memory", official.memory_graph ? official.memory_graph.approved_packets + " packets" : "n/a"],
2710
+ ["Code indexed", official.structural_index ? official.structural_index.files + " files" : official.code_graph.files + " files"],
2711
+ ["Parser coverage", official.code_graph.indexer_coverage_percent + "%"],
2712
+ ["Memory-code links", memoryCodeLinks]
2098
2713
  ] : [
2099
2714
  ["Nodes", visibleEntities.length + "/" + state.entities.length],
2100
2715
  ["Relations", visibleEdges.length + "/" + state.edges.length],
@@ -2116,6 +2731,625 @@
2116
2731
  els.workspaceMode.textContent = (els.viewMode.value || "combined").replace(/^./, function (letter) { return letter.toUpperCase(); });
2117
2732
  els.graphSubhead.textContent = visibleEntities.length + " visible nodes and " + visibleEdges.length + " visible relations" +
2118
2733
  (hiddenDependencies && !els.showDependencies.checked ? " (" + hiddenDependencies + " dependency/noise nodes hidden)." : ".");
2734
+ renderGraphInsights(visibleEntities, visibleEdges, hiddenDependencies);
2735
+ updateGraphActionButtons();
2736
+ }
2737
+
2738
+ function renderGraphInsights(visibleEntities, visibleEdges, hiddenDependencies) {
2739
+ if (!els.graphInsights) return;
2740
+ els.graphInsights.textContent = "";
2741
+ var allMemoryCode = state.edges.filter(isMemoryCodeEdge);
2742
+ var visibleMemoryCode = visibleEdges.filter(isMemoryCodeEdge);
2743
+ var reviewFlags = visibleEdges.filter(function (edge) { return reviewStatus(edge) !== "ok"; });
2744
+ var visibleCodeFiles = visibleEntities.filter(function (entity) { return entity.graph_kind === "code" && entity.type === "file"; });
2745
+ var coveredKeys = memoryLinkedCodeKeys();
2746
+ var uncoveredCodeFiles = visibleCodeFiles.filter(function (entity) { return !coveredKeys.has(codeCoverageKey(entity)); });
2747
+ var evidenceEdges = visibleEdges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length; }).length;
2748
+ var evidencePercent = visibleEdges.length ? Math.round(evidenceEdges / visibleEdges.length * 100) : 0;
2749
+ var coveragePercent = visibleCodeFiles.length ? Math.round((visibleCodeFiles.length - uncoveredCodeFiles.length) / visibleCodeFiles.length * 100) : 0;
2750
+ var queryActive = parseSearchQuery(els.searchInput.value).active;
2751
+ var hasActiveFilters = queryActive || state.graphActionFilter || els.viewMode.value !== "combined" || els.typeFilter.value || els.relationFilter.value || els.showDependencies.checked;
2752
+ if (!visibleEntities.length || hasActiveFilters) {
2753
+ var recovery = document.createElement("article");
2754
+ recovery.className = classNames("metric-card graph-action-card graph-recovery-card", !visibleEntities.length && "metric-card-warn");
2755
+ recovery.innerHTML = [
2756
+ "<div class=\"metric-card-head\"><span></span><strong></strong></div>",
2757
+ "<p></p>",
2758
+ "<button type=\"button\">Clear search/filter</button>",
2759
+ "<em></em>"
2760
+ ].join("");
2761
+ recovery.querySelector(".metric-card-head span").textContent = !visibleEntities.length ? "No graph results" : "Active graph filter";
2762
+ recovery.querySelector(".metric-card-head strong").textContent = !visibleEntities.length ? "0 visible" : "filtered";
2763
+ recovery.querySelector("p").textContent = !visibleEntities.length
2764
+ ? "The current search or filter hides every node. Clear it to recover the graph."
2765
+ : "A search, relation, or journey filter is active.";
2766
+ recovery.querySelector("button").addEventListener("click", resetGraphView);
2767
+ recovery.querySelector("em").textContent = queryActive ? "Search: " + els.searchInput.value : (state.graphActionFilter || "custom filter");
2768
+ els.graphInsights.appendChild(recovery);
2769
+ }
2770
+ var cards = [
2771
+ graphActionCard("Memory coverage", coveragePercent + "%", uncoveredCodeFiles.length
2772
+ ? uncoveredCodeFiles.length + " visible code file(s) have no linked repo memory."
2773
+ : "Visible code files have linked repo memory.",
2774
+ "Show uncovered code", "uncovered", uncoveredCodeFiles.length ? "warn" : "ok"),
2775
+ graphActionCard("Untrusted edges", reviewFlags.length ? reviewFlags.length + " flagged" : "clear", reviewFlags.length
2776
+ ? "Low-confidence, missing-evidence, or invalidated relations are visible."
2777
+ : "Visible relations are evidence-backed enough for inspection.",
2778
+ "Filter to untrusted", "untrusted", reviewFlags.length ? "warn" : "ok", [
2779
+ { label: "Low confidence", value: reviewFlags.filter(function (edge) { return reviewStatus(edge) === "low confidence"; }).length, score: Math.min(100, reviewFlags.length * 20), status: reviewFlags.length ? "warn" : "ok" },
2780
+ { label: "Missing evidence", value: reviewFlags.filter(function (edge) { return reviewStatus(edge) === "missing evidence"; }).length, score: Math.min(100, reviewFlags.length * 20), status: reviewFlags.length ? "warn" : "ok" },
2781
+ { label: "Invalidated", value: reviewFlags.filter(function (edge) { return reviewStatus(edge) === "invalidated"; }).length, score: Math.min(100, reviewFlags.length * 20), status: reviewFlags.length ? "danger" : "ok" }
2782
+ ]),
2783
+ graphActionCard("Evidence in view", evidencePercent + "%", evidenceEdges + " of " + visibleEdges.length + " visible relation(s) carry evidence.",
2784
+ "Show memory-code links", "memory-code", evidencePercent >= 80 ? "ok" : "warn"),
2785
+ graphActionCard("Trace impact", visibleMemoryCode.length + " links", "Select a code node, then trace to a test, route, or symbol from the Inspector.",
2786
+ "Use selected node", "path", visibleMemoryCode.length ? "ok" : "warn")
2787
+ ];
2788
+ cards.forEach(function (card) { els.graphInsights.appendChild(card); });
2789
+ if (els.graphInsightStatus) els.graphInsightStatus.textContent = state.graphActionFilter || (hiddenDependencies ? hiddenDependencies + " external hidden" : "ready");
2790
+ }
2791
+
2792
+ function graphActionCard(title, value, detail, actionLabel, action, status, rows) {
2793
+ var card = document.createElement("article");
2794
+ card.className = classNames("metric-card graph-action-card", status && "metric-card-" + status, state.graphActionFilter === action && "active");
2795
+ card.innerHTML = [
2796
+ "<div class=\"metric-card-head\"><span></span><strong></strong></div>",
2797
+ "<p></p>",
2798
+ "<div class=\"metric-bars\"></div>",
2799
+ "<button type=\"button\"></button>",
2800
+ "<em></em>"
2801
+ ].join("");
2802
+ card.querySelector(".metric-card-head span").textContent = title;
2803
+ card.querySelector(".metric-card-head strong").textContent = value;
2804
+ card.querySelector("p").textContent = detail;
2805
+ var bars = card.querySelector(".metric-bars");
2806
+ if (Array.isArray(rows) && rows.length) {
2807
+ rows.forEach(function (row) {
2808
+ var item = document.createElement("div");
2809
+ item.className = classNames("metric-bar", row.status && "metric-bar-" + row.status);
2810
+ item.innerHTML = "<span></span><strong></strong><i></i>";
2811
+ item.querySelector("span").textContent = row.label;
2812
+ item.querySelector("strong").textContent = formatDashboardValue(row.value);
2813
+ item.querySelector("i").style.width = clamp(Number(row.score || 0), row.value ? 8 : 0, 100) + "%";
2814
+ bars.appendChild(item);
2815
+ });
2816
+ } else {
2817
+ bars.remove();
2818
+ }
2819
+ var button = card.querySelector("button");
2820
+ button.textContent = actionLabel;
2821
+ button.addEventListener("click", function () {
2822
+ if (action === "path") {
2823
+ prefillPathFromSelection();
2824
+ return;
2825
+ }
2826
+ applyGraphActionFilter(action);
2827
+ });
2828
+ card.querySelector("em").textContent = state.graphActionFilter === action ? "Active filter" : "";
2829
+ return card;
2830
+ }
2831
+
2832
+ function updateGraphActionButtons() {
2833
+ [
2834
+ [els.showUntrusted, "untrusted"],
2835
+ [els.showUncovered, "uncovered"],
2836
+ [els.showMemoryCode, "memory-code"]
2837
+ ].forEach(function (entry) {
2838
+ if (!entry[0]) return;
2839
+ entry[0].classList.toggle("active", state.graphActionFilter === entry[1]);
2840
+ });
2841
+ }
2842
+
2843
+ function renderArtifactDiagnostics(visibleEntities, visibleEdges) {
2844
+ if (!els.debugOverview) return;
2845
+ els.debugOverview.textContent = "";
2846
+ var episodes = state.episodesById ? state.episodesById.size : 0;
2847
+ var evidenceEdges = state.edges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length; }).length;
2848
+ var evidencePercent = state.edges.length ? Math.round(evidenceEdges / state.edges.length * 100) : 0;
2849
+ var memoryCodeEdges = state.edges.filter(isMemoryCodeEdge).length;
2850
+ var reviewFlags = state.edges.filter(function (edge) { return reviewStatus(edge) !== "ok"; }).length;
2851
+ [
2852
+ metricBars("Artifact shape", state.entities.length + " nodes", [
2853
+ { label: "Visible nodes", value: visibleEntities.length, score: state.entities.length ? visibleEntities.length / state.entities.length * 100 : 0, status: "ok" },
2854
+ { label: "Relations", value: state.edges.length, score: 100, status: "ok" },
2855
+ { label: "Episodes", value: episodes, score: episodes ? 100 : 0, status: episodes ? "ok" : "warn" }
2856
+ ], "Use this when graph generation seems incomplete.", "ok"),
2857
+ metricDonut("Evidence", evidencePercent, evidenceEdges + " of " + state.edges.length + " relation(s) have evidence", "Low evidence means recall may explain less than expected.", evidencePercent >= 80 ? "ok" : "warn"),
2858
+ metricBars("Link diagnostics", memoryCodeEdges + " memory-code", [
2859
+ { label: "Memory-code", value: memoryCodeEdges, score: Math.min(100, memoryCodeEdges / Math.max(1, state.edges.length) * 100), status: memoryCodeEdges ? "ok" : "warn" },
2860
+ { label: "Review flags", value: reviewFlags, score: Math.min(100, reviewFlags * 12), status: reviewFlags ? "warn" : "ok" },
2861
+ { label: "Visible edges", value: visibleEdges.length, score: state.edges.length ? visibleEdges.length / state.edges.length * 100 : 0, status: "ok" }
2862
+ ], "Use raw rows below to inspect exact IDs, relations, and evidence.", reviewFlags ? "warn" : "ok")
2863
+ ].forEach(function (card) { els.debugOverview.appendChild(card); });
2864
+ }
2865
+
2866
+ function renderDashboard() {
2867
+ if (!els.dashboardStats) return;
2868
+ var metrics = state.metrics || {};
2869
+ var memoryGraph = metrics.memory_graph || {};
2870
+ var codeGraph = metrics.code_graph || {};
2871
+ var structural = metrics.structural_index || {};
2872
+ var savings = metrics.savings || {};
2873
+ var pain = metrics.pain || {};
2874
+ var memoryNodes = state.entities.filter(function (entity) { return entity.graph_kind === "memory"; }).length;
2875
+ var codeNodes = state.entities.filter(function (entity) { return entity.graph_kind === "code"; }).length;
2876
+ var memoryCodeEdges = state.edges.filter(isMemoryCodeEdge);
2877
+ var reports = state.reports || {};
2878
+ var reportCount = Object.keys(reports).filter(function (key) { return reports[key]; }).length;
2879
+ var risk = reports.risk || {};
2880
+ var riskTargets = Array.isArray(risk.targets) ? risk.targets : Object.keys(risk.targets || {});
2881
+ var inboxCounts = state.inbox && state.inbox.counts ? state.inbox.counts : {};
2882
+ var pendingReview = Number(firstNumber(inboxCounts.pending, memoryGraph.pending_packets, (state.pendingPackets || []).length, 0));
2883
+ var staleFlags = Number(firstNumber(inboxCounts.stale, 0));
2884
+ var duplicateFlags = Number(firstNumber(inboxCounts.duplicates, memoryGraph.duplicate_candidate_pairs, 0));
2885
+ var missingContext = Number(firstNumber(inboxCounts.missing_context, 0));
2886
+ var ownerSilos = Array.isArray(risk.ownership_silos) ? risk.ownership_silos.length : 0;
2887
+ var hotspots = Array.isArray(risk.global_hotspots) ? risk.global_hotspots.length : 0;
2888
+ var readiness = dashboardReadiness(metrics, pendingReview, staleFlags, duplicateFlags, missingContext);
2889
+ var memoryCoverage = dashboardMemoryCoverage(reports, memoryCodeEdges, memoryGraph, memoryNodes);
2890
+ var riskHealth = riskTargets.length || hotspots ? (riskTargets.length + hotspots) + " signals" : "No flags";
2891
+ var statRows = [
2892
+ ["Handoff", readiness.label, readiness.detail, readiness.status],
2893
+ ["Memory", memoryCoverage.label, memoryCoverage.detail, memoryCoverage.status],
2894
+ ["Risk", riskHealth, riskTargets.length + " targets, " + ownerSilos + " ownership silos", riskTargets.length || ownerSilos || hotspots ? "warn" : "ok"],
2895
+ ["Code map", firstNumber(codeGraph.files, structural.files, countEntitiesByType("file")) + " files", firstNumber(codeGraph.symbols, structural.symbols, codeNodes) + " symbols indexed", "code"]
2896
+ ];
2897
+ els.dashboardStats.textContent = "";
2898
+ statRows.forEach(function (row) {
2899
+ var item = document.createElement("div");
2900
+ item.className = classNames("dashboard-stat", row[3] && "dashboard-stat-" + row[3]);
2901
+ item.innerHTML = "<span></span><strong></strong><em></em>";
2902
+ item.querySelector("strong").textContent = formatDashboardValue(row[1]);
2903
+ item.querySelector("span").textContent = row[0];
2904
+ item.querySelector("em").textContent = row[2] || "";
2905
+ els.dashboardStats.appendChild(item);
2906
+ });
2907
+
2908
+ setDashboardRows("dashboardMemory", [
2909
+ ["Reusable", firstNumber(memoryGraph.approved_packets, memoryNodes) + " packets"],
2910
+ ["Linked", memoryCodeEdges.length + " code links"],
2911
+ ["Review", pendingReview ? pendingReview + " pending" : "clear"]
2912
+ ]);
2913
+ setDashboardRows("dashboardGraph", [
2914
+ ["Files", firstNumber(codeGraph.files, structural.files, countEntitiesByType("file"))],
2915
+ ["Symbols", firstNumber(codeGraph.symbols, structural.symbols, countEntitiesByType("symbol"))],
2916
+ ["Coverage", codeGraph.indexer_coverage_percent != null ? codeGraph.indexer_coverage_percent + "%" : "not loaded"]
2917
+ ]);
2918
+ setDashboardRows("dashboardIntel", [
2919
+ ["Risk targets", riskTargets.length || "none"],
2920
+ ["Ownership silos", ownerSilos || "none"],
2921
+ ["Decision coverage", reports.decisions && reports.decisions.coverage_percent != null ? reports.decisions.coverage_percent + "%" : "not loaded"]
2922
+ ]);
2923
+ setDashboardRows("dashboardReview", [
2924
+ ["Handoff", readiness.label],
2925
+ ["Pending", pendingReview || "none"],
2926
+ ["Stale / duplicate", staleFlags + " / " + duplicateFlags],
2927
+ ["Missing context", missingContext || "none"]
2928
+ ]);
2929
+ renderDashboardCharts({
2930
+ metrics: metrics,
2931
+ reports: reports,
2932
+ memoryGraph: memoryGraph,
2933
+ codeGraph: codeGraph,
2934
+ structural: structural,
2935
+ memoryCodeEdges: memoryCodeEdges,
2936
+ memoryNodes: memoryNodes,
2937
+ pendingReview: pendingReview,
2938
+ staleFlags: staleFlags,
2939
+ duplicateFlags: duplicateFlags,
2940
+ missingContext: missingContext,
2941
+ riskTargets: riskTargets,
2942
+ ownerSilos: ownerSilos,
2943
+ hotspots: hotspots
2944
+ });
2945
+ }
2946
+
2947
+ function renderDashboardCharts(data) {
2948
+ if (!els.dashboardCharts) return;
2949
+ var approvedPackets = Number(firstNumber(data.memoryGraph.approved_packets, data.memoryNodes, 0));
2950
+ var linkedPacketIds = new Set();
2951
+ data.memoryCodeEdges.forEach(function (edge) {
2952
+ var from = state.entityById.get(edge.from);
2953
+ var to = state.entityById.get(edge.to);
2954
+ if (from && isMemoryPacketEntity(from)) linkedPacketIds.add(from.id);
2955
+ if (to && isMemoryPacketEntity(to)) linkedPacketIds.add(to.id);
2956
+ });
2957
+ var memoryGrounding = approvedPackets ? Math.round(linkedPacketIds.size / approvedPackets * 100) : 0;
2958
+ var sourceCoverage = Number(firstNumber(data.codeGraph.indexer_coverage_percent, 0));
2959
+ var blockers = data.pendingReview + data.staleFlags + data.duplicateFlags + data.missingContext;
2960
+ var riskSignals = data.riskTargets.length + data.ownerSilos + data.hotspots;
2961
+ els.dashboardCharts.textContent = "";
2962
+ [
2963
+ metricDonut("Memory grounding", memoryGrounding, linkedPacketIds.size + " of " + approvedPackets + " packets linked to code", "Open Memory and fix Needs paths first.", memoryGrounding >= 70 ? "ok" : "warn"),
2964
+ metricDonut("Source map", sourceCoverage, firstNumber(data.codeGraph.files, data.structural.files, 0) + " files indexed for graph recall", "If this drops, refresh indexing before relying on graph answers.", sourceCoverage >= 90 ? "ok" : "warn"),
2965
+ metricBars("Handoff blockers", blockers ? blockers + " open" : "clear", [
2966
+ { label: "Pending", value: data.pendingReview, score: Math.min(100, data.pendingReview * 24), status: data.pendingReview ? "warn" : "ok" },
2967
+ { label: "Stale", value: data.staleFlags, score: Math.min(100, data.staleFlags * 24), status: data.staleFlags ? "warn" : "ok" },
2968
+ { label: "Duplicate", value: data.duplicateFlags, score: Math.min(100, data.duplicateFlags * 24), status: data.duplicateFlags ? "warn" : "ok" },
2969
+ { label: "Missing context", value: data.missingContext, score: Math.min(100, data.missingContext * 18), status: data.missingContext ? "warn" : "ok" }
2970
+ ], blockers ? "Resolve Review before handing work to another agent." : "Memory is clean for handoff.", blockers ? "warn" : "ok"),
2971
+ metricBars("Change risk", riskSignals ? riskSignals + " signals" : "none", [
2972
+ { label: "Targets", value: data.riskTargets.length, score: Math.min(100, data.riskTargets.length * 18), status: data.riskTargets.length ? "warn" : "ok" },
2973
+ { label: "Silos", value: data.ownerSilos, score: Math.min(100, data.ownerSilos * 18), status: data.ownerSilos ? "warn" : "ok" },
2974
+ { label: "Hotspots", value: data.hotspots, score: Math.min(100, data.hotspots * 18), status: data.hotspots ? "danger" : "ok" }
2975
+ ], riskSignals ? "Open Intel or Owners before editing risky files." : "No loaded risk flags.", riskSignals ? "warn" : "ok")
2976
+ ].forEach(function (card) { els.dashboardCharts.appendChild(card); });
2977
+ }
2978
+
2979
+ function metricDonut(title, percent, detail, action, status) {
2980
+ var card = document.createElement("article");
2981
+ var value = clamp(Number(percent || 0), 0, 100);
2982
+ card.className = classNames("metric-card", status && "metric-card-" + status);
2983
+ card.innerHTML = [
2984
+ "<div class=\"metric-card-head\"><span></span><strong></strong></div>",
2985
+ "<div class=\"metric-visual\"><div class=\"metric-donut\"><span></span></div><p></p></div>",
2986
+ "<em></em>"
2987
+ ].join("");
2988
+ card.querySelector(".metric-card-head span").textContent = title;
2989
+ card.querySelector(".metric-card-head strong").textContent = value + "%";
2990
+ card.querySelector(".metric-donut").style.setProperty("--value", value);
2991
+ card.querySelector(".metric-donut span").textContent = value + "%";
2992
+ card.querySelector("p").textContent = detail || "";
2993
+ card.querySelector("em").textContent = action || "";
2994
+ return card;
2995
+ }
2996
+
2997
+ function metricBars(title, value, rows, action, status) {
2998
+ var card = document.createElement("article");
2999
+ card.className = classNames("metric-card", status && "metric-card-" + status);
3000
+ card.innerHTML = [
3001
+ "<div class=\"metric-card-head\"><span></span><strong></strong></div>",
3002
+ "<div class=\"metric-bars\"></div>",
3003
+ "<em></em>"
3004
+ ].join("");
3005
+ card.querySelector(".metric-card-head span").textContent = title;
3006
+ card.querySelector(".metric-card-head strong").textContent = formatDashboardValue(value);
3007
+ var list = card.querySelector(".metric-bars");
3008
+ rows.forEach(function (row) {
3009
+ var item = document.createElement("div");
3010
+ item.className = classNames("metric-bar", row.status && "metric-bar-" + row.status);
3011
+ item.innerHTML = "<span></span><strong></strong><i></i>";
3012
+ item.querySelector("span").textContent = row.label;
3013
+ item.querySelector("strong").textContent = formatDashboardValue(row.value);
3014
+ item.querySelector("i").style.width = clamp(Number(row.score || 0), row.value ? 8 : 0, 100) + "%";
3015
+ list.appendChild(item);
3016
+ });
3017
+ card.querySelector("em").textContent = action || "";
3018
+ return card;
3019
+ }
3020
+
3021
+ function dashboardReadiness(metrics, pendingReview, staleFlags, duplicateFlags, missingContext) {
3022
+ if (pendingReview || staleFlags || duplicateFlags || missingContext) {
3023
+ return { label: "Needs review", detail: pendingReview + " pending, " + staleFlags + " stale, " + duplicateFlags + " duplicate, " + missingContext + " missing context", status: "warn" };
3024
+ }
3025
+ if (metrics && metrics.harness && metrics.harness.validation_ok) {
3026
+ return { label: "Ready", detail: "Memory and graph checks are clean", status: "ok" };
3027
+ }
3028
+ return { label: "Unknown", detail: "Run kage refresh or open local viewer with metrics", status: "warn" };
3029
+ }
3030
+
3031
+ function dashboardMemoryCoverage(reports, memoryCodeEdges, memoryGraph, memoryNodes) {
3032
+ var coverage = reports.decisions && reports.decisions.coverage_percent;
3033
+ if (coverage != null) {
3034
+ return {
3035
+ label: coverage + "%",
3036
+ detail: "Decision memory coverage for important code paths",
3037
+ status: Number(coverage) >= 70 ? "ok" : "warn"
3038
+ };
3039
+ }
3040
+ var packets = Number(firstNumber(memoryGraph.approved_packets, memoryNodes, 0));
3041
+ if (!packets) return { label: "No memory", detail: "Agents will rediscover repo context", status: "warn" };
3042
+ return {
3043
+ label: memoryCodeEdges.length + " links",
3044
+ detail: "Memory packets connected back to code",
3045
+ status: memoryCodeEdges.length ? "ok" : "warn"
3046
+ };
3047
+ }
3048
+
3049
+ function setDashboardRows(cardId, rows) {
3050
+ var card = document.getElementById(cardId);
3051
+ if (!card) return;
3052
+ var list = card.querySelector("ul");
3053
+ if (!list) return;
3054
+ list.textContent = "";
3055
+ rows.forEach(function (row) {
3056
+ var item = document.createElement("li");
3057
+ item.innerHTML = "<strong></strong><span></span>";
3058
+ item.querySelector("strong").textContent = row[0];
3059
+ item.querySelector("span").textContent = formatDashboardValue(row[1]);
3060
+ list.appendChild(item);
3061
+ });
3062
+ }
3063
+
3064
+ function firstNumber() {
3065
+ for (var index = 0; index < arguments.length; index += 1) {
3066
+ var value = arguments[index];
3067
+ if (value !== null && value !== undefined && value !== "") return value;
3068
+ }
3069
+ return "n/a";
3070
+ }
3071
+
3072
+ function countEntitiesByType(type) {
3073
+ return state.entities.filter(function (entity) { return entity.type === type; }).length;
3074
+ }
3075
+
3076
+ function formatDashboardValue(value) {
3077
+ if (typeof value === "number" && Number.isFinite(value)) return value.toLocaleString();
3078
+ return String(value == null ? "n/a" : value);
3079
+ }
3080
+
3081
+ function escapeHtml(value) {
3082
+ return String(value == null ? "" : value).replace(/[&<>"']/g, function (char) {
3083
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[char] || char;
3084
+ });
3085
+ }
3086
+
3087
+ function renderMemoryLibrary() {
3088
+ if (!els.memoryList) return;
3089
+ var memoryEntities = state.entities.filter(function (entity) {
3090
+ return isMemoryPacketEntity(entity);
3091
+ }).sort(function (a, b) {
3092
+ return entityImportance(b) - entityImportance(a) || displayName(a).localeCompare(displayName(b));
3093
+ });
3094
+ var memoryLinkCounts = new Map();
3095
+ state.edges.forEach(function (edge) {
3096
+ if (!isMemoryCodeEdge(edge)) return;
3097
+ var fromEntity = state.entityById.get(edge.from);
3098
+ var toEntity = state.entityById.get(edge.to);
3099
+ if (fromEntity && fromEntity.graph_kind === "memory" && toEntity && toEntity.graph_kind === "code") {
3100
+ memoryLinkCounts.set(edge.from, (memoryLinkCounts.get(edge.from) || 0) + 1);
3101
+ }
3102
+ if (toEntity && toEntity.graph_kind === "memory" && fromEntity && fromEntity.graph_kind === "code") {
3103
+ memoryLinkCounts.set(edge.to, (memoryLinkCounts.get(edge.to) || 0) + 1);
3104
+ }
3105
+ });
3106
+ var linkedCount = memoryEntities.filter(function (entity) { return (memoryLinkCounts.get(entity.id) || 0) > 0; }).length;
3107
+ var query = parseSearchQuery(els.memorySearch ? els.memorySearch.value : "");
3108
+ var filter = els.memoryFilter ? els.memoryFilter.value : "all";
3109
+ var filtered = memoryEntities.filter(function (entity) {
3110
+ var linkCount = memoryLinkCounts.get(entity.id) || 0;
3111
+ if (filter === "linked" && !linkCount) return false;
3112
+ if (filter === "needs-paths" && linkCount) return false;
3113
+ if (["decision", "runbook", "bug_fix"].indexOf(filter) !== -1 && !memoryMatchesKind(entity, filter)) return false;
3114
+ return matchesSearchQuery(entity, query);
3115
+ });
3116
+ els.memoryStatus.textContent = filtered.length + " shown";
3117
+ if (els.memoryStats) {
3118
+ els.memoryStats.innerHTML = [
3119
+ memoryStat("Reusable", memoryEntities.length),
3120
+ memoryStat("Code-linked", linkedCount),
3121
+ memoryStat("Needs paths", memoryEntities.length - linkedCount)
3122
+ ].join("");
3123
+ }
3124
+ if (els.memoryOverview) renderMemoryOverview(memoryEntities, linkedCount);
3125
+ els.memoryList.textContent = "";
3126
+ if (!memoryEntities.length) {
3127
+ els.memoryList.className = "memory-list details-empty";
3128
+ els.memoryList.textContent = "No memory packets loaded. Launch with `kage viewer --project <repo>` to load repo memory.";
3129
+ return;
3130
+ }
3131
+ if (!filtered.length) {
3132
+ els.memoryList.className = "memory-list details-empty";
3133
+ els.memoryList.textContent = "No matching memory. Clear search or switch the filter.";
3134
+ return;
3135
+ }
3136
+ filtered.sort(function (a, b) {
3137
+ return (memoryLinkCounts.get(b.id) || 0) - (memoryLinkCounts.get(a.id) || 0) ||
3138
+ entityImportance(b) - entityImportance(a) ||
3139
+ displayName(a).localeCompare(displayName(b));
3140
+ });
3141
+ els.memoryList.className = "memory-list";
3142
+ filtered.slice(0, 60).forEach(function (entity) {
3143
+ var links = memoryCodeLinksForEntity(entity.id);
3144
+ var firstCodeTarget = primaryCodeTargetForMemory(entity.id, links);
3145
+ var item = document.createElement("button");
3146
+ item.type = "button";
3147
+ var selected = state.selected && state.selected.kind === "entity" && state.selected.id === entity.id;
3148
+ item.className = classNames("memory-row", selected && "selected");
3149
+ item.setAttribute("aria-selected", selected ? "true" : "false");
3150
+ item.innerHTML = [
3151
+ "<span class=\"memory-row-main\"><strong></strong><em></em></span>",
3152
+ "<span class=\"memory-row-meta\"></span>",
3153
+ "<span class=\"memory-row-target\"></span>"
3154
+ ].join("");
3155
+ item.querySelector("strong").textContent = displayName(entity);
3156
+ item.querySelector("em").textContent = entity.type || "memory";
3157
+ item.querySelector(".memory-row-meta").textContent = trimIntelText(entity.summary || entity.description || entity.path || "No summary", 150);
3158
+ item.querySelector(".memory-row-target").textContent = links.length
3159
+ ? links.length + " code link" + (links.length === 1 ? "" : "s") + (firstCodeTarget ? " | " + trimIntelText(codeTargetLabel(firstCodeTarget), 64) : "")
3160
+ : "needs code paths";
3161
+ item.addEventListener("click", function () {
3162
+ selectEntity(entity.id, true);
3163
+ render();
3164
+ });
3165
+ els.memoryList.appendChild(item);
3166
+ });
3167
+ if (filtered.length > 60) appendListNote(els.memoryList, "Showing 60 of " + filtered.length + ". Search a path or topic to narrow.");
3168
+ }
3169
+
3170
+ function memoryStat(label, value) {
3171
+ return "<div><strong>" + escapeHtml(String(value)) + "</strong><span>" + escapeHtml(label) + "</span></div>";
3172
+ }
3173
+
3174
+ function renderMemoryOverview(memoryEntities, linkedCount) {
3175
+ els.memoryOverview.textContent = "";
3176
+ var total = memoryEntities.length;
3177
+ var linkedPercent = total ? Math.round(linkedCount / total * 100) : 0;
3178
+ var typeCounts = new Map();
3179
+ memoryEntities.forEach(function (entity) {
3180
+ typeCounts.set(entity.type || "memory", (typeCounts.get(entity.type || "memory") || 0) + 1);
3181
+ });
3182
+ var topTypes = Array.from(typeCounts.entries()).sort(function (a, b) { return b[1] - a[1] || a[0].localeCompare(b[0]); }).slice(0, 4);
3183
+ var maxType = Math.max(1, topTypes.reduce(function (max, row) { return Math.max(max, row[1]); }, 0));
3184
+ els.memoryOverview.appendChild(metricDonut(
3185
+ "Code grounding",
3186
+ linkedPercent,
3187
+ linkedCount + " packet(s) are connected to files, symbols, routes, or tests",
3188
+ linkedPercent >= 70 ? "Use linked memory before edits." : "Filter Needs paths and add concrete code references.",
3189
+ linkedPercent >= 70 ? "ok" : "warn"
3190
+ ));
3191
+ els.memoryOverview.appendChild(metricBars("Memory mix", total + " packets", topTypes.map(function (row) {
3192
+ return {
3193
+ label: row[0],
3194
+ value: row[1],
3195
+ score: row[1] / maxType * 100,
3196
+ status: row[0] === "memory" ? "" : "ok"
3197
+ };
3198
+ }), "A healthy repo has decisions, bug fixes, runbooks, gotchas, and code explanations.", "ok"));
3199
+ }
3200
+
3201
+ function memoryCodeLinksForEntity(entityId) {
3202
+ return state.edges.filter(function (edge) {
3203
+ if ((edge.from !== entityId && edge.to !== entityId) || !isMemoryCodeEdge(edge)) return false;
3204
+ var other = state.entityById.get(edge.from === entityId ? edge.to : edge.from);
3205
+ return Boolean(other && other.graph_kind === "code");
3206
+ });
3207
+ }
3208
+
3209
+ function primaryCodeTargetForMemory(entityId, links) {
3210
+ return links.map(function (edge) {
3211
+ return state.entityById.get(edge.from === entityId ? edge.to : edge.from);
3212
+ }).filter(Boolean).sort(function (a, b) {
3213
+ return codeTargetScore(b) - codeTargetScore(a) || codeTargetLabel(a).localeCompare(codeTargetLabel(b));
3214
+ })[0] || null;
3215
+ }
3216
+
3217
+ function codeTargetScore(entity) {
3218
+ var score = 0;
3219
+ if (!entity) return score;
3220
+ if (entity.type === "file") score += 80;
3221
+ if (entity.type === "route" || entity.type === "test") score += 60;
3222
+ if (entity.type === "symbol") score += 45;
3223
+ if (entity.path) score += 35;
3224
+ if (entity.line) score += 8;
3225
+ if (String(entity.path || "").indexOf(".agent_memory/") === 0) score -= 100;
3226
+ return score;
3227
+ }
3228
+
3229
+ function codeTargetLabel(entity) {
3230
+ if (!entity) return "";
3231
+ if (entity.type === "file" && entity.path) return entity.path;
3232
+ if (entity.type === "route") {
3233
+ return (entity.method && entity.route_path ? entity.method + " " + entity.route_path : displayName(entity)) +
3234
+ (entity.path ? " in " + entity.path : "");
3235
+ }
3236
+ if ((entity.type === "symbol" || entity.type === "test") && entity.path) {
3237
+ return displayName(entity) + " in " + entity.path;
3238
+ }
3239
+ return entity.path || displayName(entity);
3240
+ }
3241
+
3242
+ function memoryMatchesKind(entity, kind) {
3243
+ if (!entity) return false;
3244
+ if (entity.type === kind) return true;
3245
+ var normalizedKind = String(kind || "").replace(/_/g, " ");
3246
+ var text = [
3247
+ entity.type,
3248
+ entity.name,
3249
+ entity.summary,
3250
+ entity.description,
3251
+ entity.path
3252
+ ].join(" ").toLowerCase();
3253
+ return text.indexOf(kind) !== -1 || text.indexOf(normalizedKind) !== -1;
3254
+ }
3255
+
3256
+ function isMemoryPacketEntity(entity) {
3257
+ return Boolean(entity && entity.graph_kind === "memory" && MEMORY_PACKET_TYPES.has(entity.type));
3258
+ }
3259
+
3260
+ function renderOwners() {
3261
+ if (!els.ownersList) return;
3262
+ var contributors = state.reports && state.reports.contributors;
3263
+ var risk = state.reports && state.reports.risk;
3264
+ var profiles = contributors && Array.isArray(contributors.contributors) ? contributors.contributors : [];
3265
+ var silos = risk && Array.isArray(risk.ownership_silos) ? risk.ownership_silos : [];
3266
+ els.ownersStatus.textContent = profiles.length ? profiles.length + " contributors" : "not loaded";
3267
+ els.ownersList.textContent = "";
3268
+ if (els.ownersSummary) renderOwnersSummary(profiles, silos);
3269
+ if (!profiles.length && !silos.length) {
3270
+ els.ownersList.className = "owners-list details-empty";
3271
+ els.ownersList.textContent = "No owner report loaded. Launch with `kage viewer --project <repo>` to load contributor and ownership reports.";
3272
+ return;
3273
+ }
3274
+ els.ownersList.className = "owners-list";
3275
+ profiles.slice(0, 24).forEach(function (profile) {
3276
+ var item = document.createElement("article");
3277
+ item.className = "owner-card";
3278
+ item.innerHTML = [
3279
+ "<div class=\"owner-head\"><strong></strong><span></span></div>",
3280
+ "<div class=\"owner-stats\"></div>",
3281
+ "<p></p>"
3282
+ ].join("");
3283
+ item.querySelector("strong").textContent = shortContributor(profile.contributor);
3284
+ item.querySelector(".owner-head span").textContent = (profile.primary_owned_files || 0) + " owned files";
3285
+ item.querySelector(".owner-stats").textContent = [
3286
+ (profile.commits_total || 0) + " commits",
3287
+ (profile.commits_90d || 0) + " in 90d",
3288
+ (profile.silo_files && profile.silo_files.length ? profile.silo_files.length + " silo files" : "no silo flags")
3289
+ ].join(" | ");
3290
+ item.querySelector("p").textContent = Array.isArray(profile.top_modules) && profile.top_modules.length
3291
+ ? "Modules: " + profile.top_modules.slice(0, 4).join(", ")
3292
+ : "Local git ownership signal.";
3293
+ els.ownersList.appendChild(item);
3294
+ });
3295
+ if (silos.length) {
3296
+ var siloSection = document.createElement("section");
3297
+ siloSection.className = "owner-silos";
3298
+ siloSection.innerHTML = "<h3>Ownership Silos</h3>";
3299
+ silos.slice(0, 16).forEach(function (silo) {
3300
+ var row = document.createElement("button");
3301
+ row.type = "button";
3302
+ row.className = "owner-silo-row";
3303
+ row.innerHTML = "<strong></strong><span></span>";
3304
+ row.querySelector("strong").textContent = silo.file_path || "file";
3305
+ row.querySelector("span").textContent = [
3306
+ shortContributor(silo.primary_owner || "unknown"),
3307
+ silo.primary_owner_pct != null ? Math.round(Number(silo.primary_owner_pct || 0) * 100) + "% ownership" : "",
3308
+ (silo.commit_count_total || 0) + " commits"
3309
+ ].filter(Boolean).join(" | ");
3310
+ row.addEventListener("click", function () {
3311
+ focusGraphPath(silo.file_path);
3312
+ });
3313
+ siloSection.appendChild(row);
3314
+ });
3315
+ els.ownersList.appendChild(siloSection);
3316
+ }
3317
+ }
3318
+
3319
+ function renderOwnersSummary(profiles, silos) {
3320
+ els.ownersSummary.textContent = "";
3321
+ if (!profiles.length && !silos.length) return;
3322
+ var totalOwned = profiles.reduce(function (sum, profile) { return sum + Number(profile.primary_owned_files || 0); }, 0);
3323
+ var topOwned = profiles.reduce(function (max, profile) { return Math.max(max, Number(profile.primary_owned_files || 0)); }, 0);
3324
+ var topOwnerShare = totalOwned ? Math.round(topOwned / totalOwned * 100) : 0;
3325
+ var commits90 = profiles.reduce(function (sum, profile) { return sum + Number(profile.commits_90d || 0); }, 0);
3326
+ var maxOwned = Math.max(1, topOwned);
3327
+ els.ownersSummary.appendChild(metricDonut(
3328
+ "Backup coverage",
3329
+ Math.max(0, 100 - topOwnerShare),
3330
+ silos.length ? silos.length + " single-owner file(s) need backup reviewers" : "No loaded ownership silos",
3331
+ silos.length ? "Click silo rows to inspect the file in Graph." : "Keep ownership spread visible before large changes.",
3332
+ silos.length ? "warn" : "ok"
3333
+ ));
3334
+ els.ownersSummary.appendChild(metricBars("Owner concentration", profiles.length + " profiles", profiles.slice(0, 4).map(function (profile) {
3335
+ var owned = Number(profile.primary_owned_files || 0);
3336
+ return {
3337
+ label: shortContributor(profile.contributor),
3338
+ value: owned + " files",
3339
+ score: owned / maxOwned * 100,
3340
+ status: owned && Array.isArray(profile.silo_files) && profile.silo_files.length ? "warn" : "ok"
3341
+ };
3342
+ }), "Use this for reviewer routing and bus-factor checks.", silos.length ? "warn" : "ok"));
3343
+ els.ownersSummary.appendChild(metricBars("Recent activity", commits90 + " commits", profiles.slice(0, 4).map(function (profile) {
3344
+ var commits = Number(profile.commits_90d || 0);
3345
+ var maxCommits = Math.max(1, profiles.reduce(function (max, item) { return Math.max(max, Number(item.commits_90d || 0)); }, 0));
3346
+ return {
3347
+ label: shortContributor(profile.contributor),
3348
+ value: commits,
3349
+ score: commits / maxCommits * 100,
3350
+ status: commits ? "ok" : ""
3351
+ };
3352
+ }), "Prefer recent editors for fast review context.", "ok"));
2119
3353
  }
2120
3354
 
2121
3355
  function renderReviewQueue() {
@@ -2123,8 +3357,11 @@
2123
3357
  var packets = state.pendingPackets || [];
2124
3358
  var inbox = state.inbox;
2125
3359
  var inboxItems = inbox && Array.isArray(inbox.items) ? inbox.items : [];
2126
- els.reviewCount.textContent = String(packets.length + inboxItems.length);
3360
+ var counts = inbox && inbox.counts ? inbox.counts : {};
3361
+ var openCount = reviewOpenCount(counts, packets, inboxItems);
3362
+ els.reviewCount.textContent = String(openCount);
2127
3363
  els.reviewList.textContent = "";
3364
+ if (els.reviewOverview) renderReviewOverview(inbox, packets, inboxItems);
2128
3365
  if (!packets.length && !inboxItems.length && !state.reviewText) {
2129
3366
  els.reviewList.className = "review-list details-empty";
2130
3367
  els.reviewList.textContent = "No pending packets loaded. Launch with `kage viewer --project <repo>` to load review context automatically.";
@@ -2134,7 +3371,6 @@
2134
3371
  if (inbox) {
2135
3372
  var summary = document.createElement("div");
2136
3373
  summary.className = "review-item";
2137
- var counts = inbox.counts || {};
2138
3374
  summary.innerHTML = [
2139
3375
  "<div class=\"review-title\"></div>",
2140
3376
  "<div class=\"review-meta\"></div>",
@@ -2151,10 +3387,10 @@
2151
3387
  summary.querySelector(".review-summary").textContent = Array.isArray(inbox.recommendations) && inbox.recommendations.length
2152
3388
  ? inbox.recommendations.slice(0, 2).join(" ")
2153
3389
  : "No inbox recommendations.";
2154
- summary.querySelector(".review-risks").textContent = inbox.ok ? "ready for handoff" : "requires review";
3390
+ summary.querySelector(".review-risks").textContent = openCount ? "Resolve inbox items before merge" : "Ready for handoff";
2155
3391
  els.reviewList.appendChild(summary);
2156
3392
  }
2157
- inboxItems.slice(0, 20).forEach(function (entry) {
3393
+ inboxItems.slice(0, 8).forEach(function (entry) {
2158
3394
  var item = document.createElement("div");
2159
3395
  item.className = "review-item";
2160
3396
  item.innerHTML = [
@@ -2166,7 +3402,9 @@
2166
3402
  item.querySelector(".review-title").textContent = entry.title || entry.summary || entry.kind;
2167
3403
  item.querySelector(".review-meta").textContent = [entry.kind, entry.severity, entry.type, entry.status].filter(Boolean).join(" | ");
2168
3404
  item.querySelector(".review-summary").textContent = entry.action || entry.summary || "";
2169
- item.querySelector(".review-risks").textContent = Array.isArray(entry.reasons) && entry.reasons.length ? "reasons: " + entry.reasons.slice(0, 3).join(", ") : "reasons: none";
3405
+ item.querySelector(".review-risks").textContent = Array.isArray(entry.reasons) && entry.reasons.length
3406
+ ? trimIntelText(entry.reasons[0], 86)
3407
+ : "Review before handoff";
2170
3408
  els.reviewList.appendChild(item);
2171
3409
  });
2172
3410
  packets.forEach(function (packet) {
@@ -2182,7 +3420,9 @@
2182
3420
  item.querySelector(".review-title").textContent = packet.title || packet.id;
2183
3421
  item.querySelector(".review-meta").textContent = [packet.type, packet.status, "score " + (quality.score == null ? "n/a" : quality.score + "/100")].filter(Boolean).join(" | ");
2184
3422
  item.querySelector(".review-summary").textContent = packet.summary || "";
2185
- item.querySelector(".review-risks").textContent = Array.isArray(quality.risks) && quality.risks.length ? "risks: " + quality.risks.join(", ") : "risks: none";
3423
+ item.querySelector(".review-risks").textContent = Array.isArray(quality.risks) && quality.risks.length
3424
+ ? "Resolve " + quality.risks.join(", ")
3425
+ : "Evidence looks clean";
2186
3426
  els.reviewList.appendChild(item);
2187
3427
  });
2188
3428
  if (state.reviewText) {
@@ -2194,25 +3434,58 @@
2194
3434
  }
2195
3435
  }
2196
3436
 
3437
+ function renderReviewOverview(inbox, packets, inboxItems) {
3438
+ els.reviewOverview.textContent = "";
3439
+ var counts = inbox && inbox.counts ? inbox.counts : {};
3440
+ var pending = Number(firstNumber(counts.pending, packets.length, 0));
3441
+ var stale = Number(firstNumber(counts.stale, 0));
3442
+ var duplicates = Number(firstNumber(counts.duplicates, 0));
3443
+ var missingContext = Number(firstNumber(counts.missing_context, 0));
3444
+ var blockers = reviewOpenCount(counts, packets, inboxItems);
3445
+ els.reviewOverview.appendChild(metricDonut(
3446
+ "Handoff readiness",
3447
+ blockers ? 0 : 100,
3448
+ blockers ? blockers + " review blocker(s) need attention" : "No pending, stale, duplicate, or missing-context memory",
3449
+ blockers ? "Resolve these before trusting branch memory." : "Ready to hand work to another agent or teammate.",
3450
+ blockers ? "warn" : "ok"
3451
+ ));
3452
+ els.reviewOverview.appendChild(metricBars("Inbox breakdown", blockers ? blockers + " open" : "clear", [
3453
+ { label: "Pending", value: pending, score: Math.min(100, pending * 24), status: pending ? "warn" : "ok" },
3454
+ { label: "Stale", value: stale, score: Math.min(100, stale * 24), status: stale ? "warn" : "ok" },
3455
+ { label: "Duplicates", value: duplicates, score: Math.min(100, duplicates * 24), status: duplicates ? "warn" : "ok" },
3456
+ { label: "Missing context", value: missingContext, score: Math.min(100, missingContext * 24), status: missingContext ? "warn" : "ok" }
3457
+ ], "These are the only review metrics that should block merge or handoff.", blockers ? "warn" : "ok"));
3458
+ }
3459
+
3460
+ function reviewOpenCount(counts, packets, inboxItems) {
3461
+ var pending = Number(firstNumber(counts && counts.pending, packets && packets.length, 0));
3462
+ var stale = Number(firstNumber(counts && counts.stale, 0));
3463
+ var duplicates = Number(firstNumber(counts && counts.duplicates, 0));
3464
+ var missingContext = Number(firstNumber(counts && counts.missing_context, 0));
3465
+ var counted = pending + stale + duplicates + missingContext;
3466
+ if (counted) return counted;
3467
+ return Array.isArray(inboxItems) ? inboxItems.length : 0;
3468
+ }
3469
+
2197
3470
  function renderProof() {
2198
3471
  if (!els.proofList) return;
2199
3472
  var metrics = state.metrics;
2200
3473
  els.proofList.textContent = "";
2201
3474
  if (!metrics) {
2202
3475
  els.proofStatus.textContent = "not loaded";
3476
+ if (els.proofOverview) els.proofOverview.textContent = "";
2203
3477
  els.proofList.className = "proof-list details-empty";
2204
3478
  els.proofList.textContent = "Metrics not loaded. Run `kage metrics --project <repo> --json > .agent_memory/metrics.json` or launch with `kage viewer`.";
2205
3479
  return;
2206
3480
  }
2207
3481
  els.proofStatus.textContent = "loaded";
2208
3482
  els.proofList.className = "proof-list";
3483
+ if (els.proofOverview) renderProofOverview(metrics, state.reports || {});
2209
3484
  var rows = [
2210
- ["Readiness", metrics.harness && metrics.harness.readiness_score != null ? metrics.harness.readiness_score + "/100" : "n/a"],
2211
- ["Useful memory", metrics.quality ? metrics.quality.useful_memory_ratio_percent + "%" : "n/a"],
3485
+ ["Validation", metrics.harness && metrics.harness.validation_ok ? "clean" : "check"],
2212
3486
  ["Evidence", metrics.memory_graph ? metrics.memory_graph.evidence_coverage_percent + "%" : "n/a"],
2213
3487
  ["Pending review", metrics.memory_graph ? String(metrics.memory_graph.pending_packets) : "n/a"],
2214
- ["Recall hit rate", metrics.pain ? metrics.pain.recall_hit_rate_percent + "%" : "n/a"],
2215
- ["Tokens saved", metrics.pain ? String(metrics.pain.estimated_tokens_saved) : metrics.savings ? String(metrics.savings.estimated_tokens_saved_per_recall) : "n/a"]
3488
+ ["Recall savings", metrics.pain ? String(metrics.pain.estimated_tokens_saved) : metrics.savings ? String(metrics.savings.estimated_tokens_saved_per_recall) : "n/a"]
2216
3489
  ];
2217
3490
  rows.forEach(function (row) {
2218
3491
  var item = document.createElement("div");
@@ -2224,6 +3497,29 @@
2224
3497
  });
2225
3498
  }
2226
3499
 
3500
+ function renderProofOverview(metrics, reports) {
3501
+ els.proofOverview.textContent = "";
3502
+ var quality = reports.quality || {};
3503
+ var benchmark = reports.benchmark || {};
3504
+ var gates = Array.isArray(benchmark.gates) ? benchmark.gates : [];
3505
+ var passingGates = gates.filter(function (gate) { return gate.pass; }).length;
3506
+ var gatePercent = gates.length ? Math.round(passingGates / gates.length * 100) : (benchmark.ok ? 100 : 0);
3507
+ var evidence = Number(firstNumber(metrics.memory_graph && metrics.memory_graph.evidence_coverage_percent, quality.evidence_coverage_percent, 0));
3508
+ var pathGrounding = Number(firstNumber(quality.path_grounding_coverage_percent, 0));
3509
+ els.proofOverview.appendChild(metricDonut(
3510
+ "Trust gate",
3511
+ gatePercent,
3512
+ gates.length ? passingGates + " of " + gates.length + " benchmark gates passing" : "Benchmark report not loaded",
3513
+ gatePercent >= 100 ? "Keep this green before publishing or handing off." : "Fix failing proof gates before release.",
3514
+ gatePercent >= 100 ? "ok" : "warn"
3515
+ ));
3516
+ els.proofOverview.appendChild(metricBars("Memory quality", evidence + "% evidence", [
3517
+ { label: "Evidence", value: evidence + "%", score: evidence, status: evidence >= 80 ? "ok" : "warn" },
3518
+ { label: "Path grounded", value: pathGrounding ? pathGrounding + "%" : "n/a", score: pathGrounding || 0, status: pathGrounding >= 80 ? "ok" : "warn" },
3519
+ { label: "Useful", value: quality.useful_memory_ratio_percent != null ? quality.useful_memory_ratio_percent + "%" : "n/a", score: Number(quality.useful_memory_ratio_percent || 0), status: Number(quality.useful_memory_ratio_percent || 0) >= 70 ? "ok" : "warn" }
3520
+ ], "Trust memory only when it is evidence-backed and path-grounded.", evidence >= 80 ? "ok" : "warn"));
3521
+ }
3522
+
2227
3523
  function renderIntelligence() {
2228
3524
  if (!els.intelligenceList) return;
2229
3525
  var reports = state.reports || {};
@@ -2236,36 +3532,40 @@
2236
3532
  return;
2237
3533
  }
2238
3534
  els.intelligenceList.className = "intelligence-list";
2239
- cards.forEach(function (card) {
3535
+ normalizeIntelCards(cards).slice(0, 6).forEach(function (card) {
2240
3536
  var item = document.createElement("article");
2241
3537
  item.className = "intel-card";
2242
3538
  item.innerHTML = [
2243
- "<h3></h3>",
2244
- "<div class=\"intel-kicker\"></div>",
2245
- "<div class=\"intel-summary\"></div>",
3539
+ "<div class=\"intel-card-head\"><div><h3></h3><span></span></div><strong></strong></div>",
3540
+ "<div class=\"intel-metric-label\"></div>",
3541
+ "<p class=\"intel-highlight\"></p>",
3542
+ "<p class=\"intel-action\"><b>Action:</b> <span></span></p>",
2246
3543
  "<ul></ul>"
2247
3544
  ].join("");
2248
3545
  item.querySelector("h3").textContent = card.title;
2249
- item.querySelector(".intel-kicker").textContent = card.kicker;
2250
- item.querySelector(".intel-summary").textContent = card.summary;
3546
+ item.querySelector(".intel-card-head span").textContent = card.kicker;
3547
+ item.querySelector(".intel-card-head strong").textContent = card.metric || "n/a";
3548
+ item.querySelector(".intel-metric-label").textContent = card.metricLabel || "signal";
3549
+ item.querySelector(".intel-highlight").textContent = card.highlight || card.summary || "";
3550
+ item.querySelector(".intel-action span").textContent = card.action || "Review this signal before changing related code.";
2251
3551
  var list = item.querySelector("ul");
2252
- card.rows.slice(0, 5).forEach(function (row) {
3552
+ card.rows.slice(0, 3).forEach(function (row) {
2253
3553
  var li = document.createElement("li");
2254
3554
  li.innerHTML = "<strong></strong> <span></span>";
2255
3555
  li.querySelector("strong").textContent = row[0];
2256
- li.querySelector("span").textContent = row[1];
3556
+ li.querySelector("span").textContent = trimIntelText(row[1], 92);
2257
3557
  list.appendChild(li);
2258
3558
  });
2259
3559
  els.intelligenceList.appendChild(item);
2260
3560
  });
2261
- var sections = buildIntelligenceSections(reports);
3561
+ var sections = rankIntelligenceSections(buildIntelligenceSections(reports)).slice(0, 4);
2262
3562
  if (sections.length) {
2263
- var grid = document.createElement("div");
2264
- grid.className = "intel-deep-grid";
3563
+ var deepGrid = document.createElement("div");
3564
+ deepGrid.className = "intel-deep-grid";
2265
3565
  sections.forEach(function (section) {
2266
- grid.appendChild(renderIntelligenceSection(section));
3566
+ deepGrid.appendChild(renderIntelligenceSection(section));
2267
3567
  });
2268
- els.intelligenceList.appendChild(grid);
3568
+ els.intelligenceList.appendChild(deepGrid);
2269
3569
  }
2270
3570
  }
2271
3571
 
@@ -2288,32 +3588,46 @@
2288
3588
  var list = document.createElement("div");
2289
3589
  list.className = "intel-section-list";
2290
3590
  section.rows.slice(0, section.limit || 8).forEach(function (row) {
2291
- var button = document.createElement("button");
2292
- button.type = "button";
2293
- button.className = classNames("intel-row", row.status && "intel-row-" + safeCssName(row.status), row.path && "clickable");
2294
- button.innerHTML = [
3591
+ var rowEl = document.createElement(row.path ? "button" : "div");
3592
+ if (row.path) rowEl.type = "button";
3593
+ rowEl.className = classNames("intel-row", row.status && "intel-row-" + safeCssName(row.status), row.path && "clickable");
3594
+ rowEl.innerHTML = [
2295
3595
  "<span class=\"intel-row-main\"><strong></strong><em></em></span>",
2296
3596
  "<span class=\"intel-row-meta\"></span>",
2297
3597
  "<span class=\"intel-row-bar\"><i></i></span>"
2298
3598
  ].join("");
2299
- button.querySelector("strong").textContent = row.label || "";
2300
- button.querySelector("em").textContent = row.value || "";
2301
- button.querySelector(".intel-row-meta").textContent = row.meta || "";
2302
- button.querySelector(".intel-row-bar i").style.width = clamp(Number(row.score || 0), 4, 100) + "%";
3599
+ rowEl.querySelector("strong").textContent = row.label || "";
3600
+ rowEl.querySelector("em").textContent = row.value || "";
3601
+ rowEl.querySelector(".intel-row-meta").textContent = row.meta || "";
3602
+ rowEl.querySelector(".intel-row-bar i").style.width = clamp(Number(row.score || 0), 4, 100) + "%";
2303
3603
  if (row.path) {
2304
- button.title = "Focus " + row.path + " in the graph";
2305
- button.addEventListener("click", function () {
3604
+ rowEl.title = "Focus " + row.path + " in the graph";
3605
+ rowEl.setAttribute("aria-label", "Focus " + row.path + " in Graph");
3606
+ rowEl.addEventListener("click", function () {
2306
3607
  focusGraphPath(row.path);
2307
3608
  });
2308
- } else {
2309
- button.disabled = true;
2310
3609
  }
2311
- list.appendChild(button);
3610
+ list.appendChild(rowEl);
2312
3611
  });
2313
3612
  panel.appendChild(list);
2314
3613
  return panel;
2315
3614
  }
2316
3615
 
3616
+ function rankIntelligenceSections(sections) {
3617
+ return sections.slice().sort(function (a, b) {
3618
+ return intelligenceSectionPriority(a) - intelligenceSectionPriority(b);
3619
+ });
3620
+ }
3621
+
3622
+ function intelligenceSectionPriority(section) {
3623
+ var title = String(section && section.title || "").toLowerCase();
3624
+ if (title.indexOf("blast") !== -1 || title.indexOf("risk") !== -1) return 0;
3625
+ if (title.indexOf("onboarding") !== -1 || title.indexOf("decision") !== -1) return 1;
3626
+ if (title.indexOf("module") !== -1 || title.indexOf("health") !== -1) return 2;
3627
+ if (title.indexOf("owner") !== -1 || title.indexOf("contributor") !== -1) return 3;
3628
+ return 4;
3629
+ }
3630
+
2317
3631
  function buildIntelligenceSections(reports) {
2318
3632
  var sections = [];
2319
3633
  var contributors = reports.contributors;
@@ -2353,7 +3667,7 @@
2353
3667
  title: "Ownership Map",
2354
3668
  kicker: "who owns what",
2355
3669
  stat: silos.length + " silos",
2356
- summary: "Repowise has a dedicated ownership page. Kage now surfaces the same reviewer-critical signal inside the memory viewer, tied back to selectable files.",
3670
+ summary: "Action: assign backup reviewers for silo files before risky changes.",
2357
3671
  rows: rows,
2358
3672
  limit: 10,
2359
3673
  });
@@ -2368,7 +3682,7 @@
2368
3682
  title: "Module Health Map",
2369
3683
  kicker: "churn / tests / ownership",
2370
3684
  stat: modules.length + " modules",
2371
- summary: "Lowest-scoring modules are shown first so the viewer points people toward the riskiest areas instead of only drawing nodes.",
3685
+ summary: "Action: start cleanup and test planning from the lowest-score modules.",
2372
3686
  rows: modules.slice(0, 8).map(function (item) {
2373
3687
  return {
2374
3688
  label: item.module,
@@ -2387,7 +3701,7 @@
2387
3701
  title: "Onboarding Targets",
2388
3702
  kicker: "missing repo lore",
2389
3703
  stat: (decisions.coverage_percent != null ? decisions.coverage_percent + "%" : "n/a"),
2390
- summary: "Files with centrality, churn, test gaps, or ownership but no linked why-memory. These are the places a future agent is most likely to rediscover context.",
3704
+ summary: "Action: capture why-memory for these files before the next agent works there.",
2391
3705
  rows: gaps.slice(0, 8).map(function (gap) {
2392
3706
  var score = Math.min(100, Number(gap.dependents || 0) * 18 + Number(gap.churn_90d || 0) * 6 + 12);
2393
3707
  return {
@@ -2413,7 +3727,7 @@
2413
3727
  title: "Architecture Communities",
2414
3728
  kicker: "module clusters",
2415
3729
  stat: communities.length + " clusters",
2416
- summary: "Repowise exposes architecture/community views. Kage can show the same high-level clusters next to the memory-code graph so users know what a dense graph means.",
3730
+ summary: "Action: use clusters to understand the graph before changing architecture.",
2417
3731
  rows: communities.slice(0, 8).map(function (community) {
2418
3732
  var files = community.files || [];
2419
3733
  var entrypoints = community.entrypoints || [];
@@ -2435,7 +3749,7 @@
2435
3749
  title: "Execution Flows",
2436
3750
  kicker: "entrypoint traces",
2437
3751
  stat: insights.entry_flows.length + " flows",
2438
- summary: "Short traces make the code graph explainable: where execution starts, what it crosses, and which file to inspect first.",
3752
+ summary: "Action: inspect entry files first when debugging runtime behavior.",
2439
3753
  rows: insights.entry_flows.slice(0, 8).map(function (flow) {
2440
3754
  var path = flow.path || [];
2441
3755
  return {
@@ -2494,7 +3808,7 @@
2494
3808
  title: "Workspace Map",
2495
3809
  kicker: "deps / contracts / co-changes",
2496
3810
  stat: workspaceRows.length + " links",
2497
- summary: "Workspace links show how sibling repos relate through package dependencies, source-evidence contracts, topic/event links, and local git co-change history.",
3811
+ summary: "Action: check linked repos before changing shared packages or contracts.",
2498
3812
  rows: workspaceRows,
2499
3813
  limit: 14,
2500
3814
  });
@@ -2528,7 +3842,7 @@
2528
3842
  title: "Blast Radius",
2529
3843
  kicker: "change impact",
2530
3844
  stat: riskRows.length + " signals",
2531
- summary: "Change risk is useful only if it is browsable. Rows focus the graph on affected files when the graph node exists.",
3845
+ summary: "Action: review tests, owners, and dependents before editing these targets.",
2532
3846
  rows: riskRows,
2533
3847
  limit: 10,
2534
3848
  });
@@ -2550,11 +3864,13 @@
2550
3864
  });
2551
3865
  if (!found) {
2552
3866
  els.searchInput.value = normalized;
3867
+ showViewerPageInPlace("graph");
2553
3868
  scheduleRender();
2554
3869
  return;
2555
3870
  }
2556
- state.selected = { kind: "entity", id: found.id };
3871
+ selectEntity(found.id, true);
2557
3872
  els.searchInput.value = normalized;
3873
+ showViewerPageInPlace("graph");
2558
3874
  scheduleRender();
2559
3875
  }
2560
3876
 
@@ -2716,18 +4032,86 @@
2716
4032
  return cards;
2717
4033
  }
2718
4034
 
4035
+ function normalizeIntelCards(cards) {
4036
+ return cards.map(function (card) {
4037
+ var normalized = Object.assign({}, card);
4038
+ var row = function (label) {
4039
+ var found = (card.rows || []).find(function (item) { return item[0] === label; });
4040
+ return found ? found[1] : null;
4041
+ };
4042
+ if (card.title === "Memory-Code Bridge") {
4043
+ normalized.metric = row("Links") || "0";
4044
+ normalized.metricLabel = "memory-code links";
4045
+ normalized.highlight = "Shows whether saved repo knowledge is tied to actual files, symbols, routes, and tests.";
4046
+ normalized.action = "If this is low, capture memory with concrete paths so agents can recall it during edits.";
4047
+ } else if (card.title === "Change Risk") {
4048
+ var siloText = row("Silos");
4049
+ var siloMatch = siloText && String(siloText).match(/\d+/);
4050
+ normalized.metric = siloMatch ? siloMatch[0] + " silos" : ((card.rows || []).length + " signals");
4051
+ normalized.metricLabel = "risk signals";
4052
+ normalized.highlight = "Flags files with blast radius, test gaps, or ownership concentration.";
4053
+ normalized.action = "Use these rows to pick tests and reviewers before touching risky files.";
4054
+ } else if (card.title === "Contributors") {
4055
+ normalized.metric = (card.rows || []).length + " profiles";
4056
+ normalized.metricLabel = "review routing";
4057
+ normalized.highlight = "Shows who recently touched or owns parts of the repo.";
4058
+ normalized.action = "Use this to find backup reviewers and avoid single-person knowledge bottlenecks.";
4059
+ } else if (card.title === "Decision Memory") {
4060
+ normalized.metric = row("Coverage") || "n/a";
4061
+ normalized.metricLabel = "why-memory coverage";
4062
+ normalized.highlight = "Shows whether important code paths have captured rationale and gotchas.";
4063
+ normalized.action = "Add memory for coverage gaps before future agents rediscover the same context.";
4064
+ } else if (card.title === "Module Health") {
4065
+ normalized.metric = (card.rows || []).length + " modules";
4066
+ normalized.metricLabel = "lowest scores first";
4067
+ normalized.highlight = "Ranks modules by churn, tests, ownership, and graph signals.";
4068
+ normalized.action = "Start with low-score modules when planning cleanup or refactors.";
4069
+ } else if (card.title === "Graph Insights") {
4070
+ normalized.metric = row("Cycles") || row("Communities") || "n/a";
4071
+ normalized.metricLabel = row("Cycles") != null ? "dependency cycles" : "architecture clusters";
4072
+ normalized.highlight = "Explains dense graph structure through central files, cycles, and communities.";
4073
+ normalized.action = "Inspect central files and cycles before making architectural changes.";
4074
+ } else if (card.title === "Workspace") {
4075
+ normalized.metric = row("Repos") || "n/a";
4076
+ normalized.metricLabel = "connected repos";
4077
+ normalized.highlight = "Shows package, route, topic, and co-change links across local repos.";
4078
+ normalized.action = "Check these links before changing shared contracts or packages.";
4079
+ } else if (card.title === "Memory Quality") {
4080
+ normalized.metric = row("Evidence") || row("Useful") || "n/a";
4081
+ normalized.metricLabel = "trust signal";
4082
+ normalized.highlight = "Shows whether memory is evidence-backed, grounded, and reviewable.";
4083
+ normalized.action = "Review pending or weak memory before relying on it for agent handoff.";
4084
+ } else if (card.title === "Benchmark") {
4085
+ normalized.metric = (card.rows || []).filter(function (item) { return String(item[1] || "").indexOf("pass") !== -1; }).length + " pass";
4086
+ normalized.metricLabel = "local proof checks";
4087
+ normalized.highlight = "Shows whether repo memory and graph behavior pass local quality checks.";
4088
+ normalized.action = "Use failed checks as release blockers or cleanup targets.";
4089
+ }
4090
+ return normalized;
4091
+ });
4092
+ }
4093
+
4094
+ function trimIntelText(value, limit) {
4095
+ var text = String(value == null ? "" : value);
4096
+ var max = Number(limit || 90);
4097
+ if (text.length <= max) return text;
4098
+ return text.slice(0, Math.max(0, max - 1)).replace(/\s+\S*$/, "") + "...";
4099
+ }
4100
+
2719
4101
  function renderStatusStrip(visibleEntities, visibleEdges, official) {
2720
4102
  if (!els.statusStrip) return;
2721
4103
  var memoryCount = visibleEntities.filter(function (entity) { return entity.graph_kind === "memory"; }).length;
2722
4104
  var codeCount = visibleEntities.filter(function (entity) { return entity.graph_kind === "code"; }).length;
2723
4105
  var reviewFlags = visibleEdges.filter(function (edge) { return reviewStatus(edge) !== "ok"; }).length;
4106
+ var memoryCodeLinks = state.edges.filter(isMemoryCodeEdge).length;
4107
+ var pendingReview = official && official.memory_graph ? Number(firstNumber(official.memory_graph.pending_packets, 0)) : 0;
2724
4108
  var pills = official ? [
2725
- ["Readiness", official.harness.readiness_score + "/100", ""],
2726
- ["Pending", official.memory_graph ? String(official.memory_graph.pending_packets) : "n/a", official.memory_graph && official.memory_graph.pending_packets ? "warn" : ""],
2727
- ["Structural", official.structural_index ? official.structural_index.files + " files" : official.code_graph.files + " files", "code"],
2728
- ["Code symbols", String(official.code_graph.symbols), "code"],
2729
- ["Parser coverage", official.code_graph.indexer_coverage_percent + "%", "code"],
2730
- ["Memory packets", official.memory_graph ? String(official.memory_graph.approved_packets) : "n/a", "memory"]
4109
+ ["Status", official.harness && official.harness.validation_ok ? "Clean" : "Check", official.harness && official.harness.validation_ok ? "memory" : "warn"],
4110
+ ["Review", pendingReview ? pendingReview + " pending" : "Clear", pendingReview ? "warn" : "memory"],
4111
+ ["Source map", official.structural_index ? official.structural_index.files + " files" : official.code_graph.files + " files", "code"],
4112
+ ["Symbols", String(official.code_graph.symbols), "code"],
4113
+ ["Coverage", official.code_graph.indexer_coverage_percent + "%", "code"],
4114
+ ["Memory links", String(memoryCodeLinks), memoryCodeLinks ? "memory" : "warn"]
2731
4115
  ] : [
2732
4116
  ["Memory", String(memoryCount), "memory"],
2733
4117
  ["Code", String(codeCount), "code"],
@@ -2847,11 +4231,12 @@
2847
4231
 
2848
4232
  function endCanvasPointer() {
2849
4233
  if (state.sim.dragNode) {
2850
- state.selected = { kind: "entity", id: state.sim.dragNode.id };
4234
+ selectEntity(state.sim.dragNode.id, true);
2851
4235
  state.sim.dragNode = null;
2852
4236
  render();
2853
4237
  } else if (state.sim.panning && !state.sim.panning.moved) {
2854
4238
  state.selected = null;
4239
+ closeWorkspace();
2855
4240
  render();
2856
4241
  }
2857
4242
  state.sim.panning = null;
@@ -2888,7 +4273,7 @@
2888
4273
  var world = canvasToWorld(event);
2889
4274
  var node = findCanvasNode(world.x, world.y);
2890
4275
  if (!node) return;
2891
- state.selected = { kind: "entity", id: node.id };
4276
+ selectEntity(node.id, true);
2892
4277
  render();
2893
4278
  }
2894
4279
 
@@ -2979,10 +4364,15 @@
2979
4364
  if (state.three.THREE) return Promise.resolve(state.three.THREE);
2980
4365
  if (state.three.loading) return state.three.loading;
2981
4366
  state.three.failed = false;
2982
- state.three.loading = import("/vendor/three/build/three.module.min.js")
2983
- .catch(function () {
2984
- return import("https://unpkg.com/three@0.184.0/build/three.module.min.js");
2985
- })
4367
+ var threeSources = [
4368
+ "./vendor/three/build/three.module.min.js",
4369
+ "../vendor/three/build/three.module.min.js",
4370
+ "/vendor/three/build/three.module.min.js",
4371
+ "https://unpkg.com/three@0.184.0/build/three.module.min.js"
4372
+ ];
4373
+ state.three.loading = threeSources.reduce(function (chain, source) {
4374
+ return chain.catch(function () { return import(source); });
4375
+ }, Promise.reject())
2986
4376
  .then(function (mod) {
2987
4377
  state.three.THREE = mod;
2988
4378
  return mod;
@@ -2999,7 +4389,7 @@
2999
4389
  ensureThree().then(function () {
3000
4390
  if (activeRenderMode() !== "3d") return;
3001
4391
  setupThreeScene();
3002
- rebuildThreeScene();
4392
+ if (graphChanged || !state.three.nodeById.size) rebuildThreeScene();
3003
4393
  if (graphChanged || state.three.distance <= 0) fitThreeGraph();
3004
4394
  startThreeGraph();
3005
4395
  renderThreeFrame();
@@ -3062,8 +4452,9 @@
3062
4452
  state.sim.nodes.forEach(function (node, index) {
3063
4453
  var entity = node.entity;
3064
4454
  var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
4455
+ var pathNode = state.pathHighlight && state.pathHighlight.nodes.has(node.id);
3065
4456
  var material = new THREE.SpriteMaterial({
3066
- map: threeNodeTexture(entity, selected),
4457
+ map: threeNodeTexture(entity, selected || pathNode),
3067
4458
  transparent: true,
3068
4459
  opacity: isDependencyEntity(entity) ? 0.70 : 1,
3069
4460
  depthWrite: false
@@ -3071,7 +4462,7 @@
3071
4462
  var mesh = new THREE.Sprite(material);
3072
4463
  var position = threePosition(node, index);
3073
4464
  mesh.position.set(position.x, position.y, position.z);
3074
- var size = threeNodeSize(node, selected);
4465
+ var size = threeNodeSize(node, selected || pathNode);
3075
4466
  mesh.scale.set(size, size, 1);
3076
4467
  mesh.userData.node = node;
3077
4468
  state.three.nodeGroup.add(mesh);
@@ -3085,11 +4476,12 @@
3085
4476
  var fromEntity = from.node.entity;
3086
4477
  var toEntity = to.node.entity;
3087
4478
  var connected = state.selected && state.selected.kind === "entity" && (state.selected.id === edge.from || state.selected.id === edge.to);
4479
+ var pathEdge = state.pathHighlight && state.pathHighlight.edges.has(edge.id);
3088
4480
  var geometry = new THREE.BufferGeometry().setFromPoints([from.mesh.position, to.mesh.position]);
3089
4481
  var material = new THREE.LineBasicMaterial({
3090
- color: new THREE.Color(edgeThemeColor(edge, fromEntity, toEntity)),
4482
+ color: new THREE.Color(pathEdge ? graphPalette.bridge : edgeThemeColor(edge, fromEntity, toEntity)),
3091
4483
  transparent: true,
3092
- opacity: threeEdgeOpacity(edge, fromEntity, toEntity, connected),
4484
+ opacity: pathEdge ? 0.82 : threeEdgeOpacity(edge, fromEntity, toEntity, connected),
3093
4485
  depthWrite: false,
3094
4486
  depthTest: false
3095
4487
  });
@@ -3426,7 +4818,7 @@
3426
4818
  : null;
3427
4819
  state.three.drag = null;
3428
4820
  if (picked) {
3429
- state.selected = { kind: "entity", id: picked.id };
4821
+ selectEntity(picked.id, true);
3430
4822
  render();
3431
4823
  }
3432
4824
  }
@@ -3447,7 +4839,7 @@
3447
4839
  function handleThreeDoubleClick(event) {
3448
4840
  var picked = pickThreeNode(event);
3449
4841
  if (!picked) return;
3450
- state.selected = { kind: "entity", id: picked.id };
4842
+ selectEntity(picked.id, true);
3451
4843
  render();
3452
4844
  }
3453
4845
 
@@ -3632,6 +5024,7 @@
3632
5024
  if (state.pan && state.pan.moved) return;
3633
5025
  if (event.target !== els.svg) return;
3634
5026
  state.selected = null;
5027
+ closeWorkspace();
3635
5028
  render();
3636
5029
  }
3637
5030