@kage-core/kage-graph-mcp 1.1.35 → 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,10 +13,10 @@
13
13
  visibleEntityIds: new Set(),
14
14
  visibleEdgeIds: new Set(),
15
15
  selected: null,
16
+ viewerPage: "overview",
16
17
  viewerSection: "overview",
17
18
  viewerAction: null,
18
- workspaceTab: "controls",
19
- workspaceOpen: false,
19
+ graphActionFilter: "",
20
20
  pathHighlight: {
21
21
  nodes: new Set(),
22
22
  edges: new Set(),
@@ -140,6 +140,9 @@
140
140
  renderMode: document.getElementById("renderMode"),
141
141
  typeFilter: document.getElementById("typeFilter"),
142
142
  relationFilter: document.getElementById("relationFilter"),
143
+ showUntrusted: document.getElementById("showUntrusted"),
144
+ showUncovered: document.getElementById("showUncovered"),
145
+ showMemoryCode: document.getElementById("showMemoryCode"),
143
146
  scopeFilter: document.getElementById("scopeFilter"),
144
147
  maxNodes: document.getElementById("maxNodes"),
145
148
  showDependencies: document.getElementById("showDependencies"),
@@ -160,27 +163,75 @@
160
163
  entityList: document.getElementById("entityList"),
161
164
  edgeList: document.getElementById("edgeList"),
162
165
  metricsSummary: document.getElementById("metricsSummary"),
166
+ graphInsightStatus: document.getElementById("graphInsightStatus"),
167
+ graphInsights: document.getElementById("graphInsights"),
163
168
  entityCount: document.getElementById("entityCount"),
164
169
  edgeCount: document.getElementById("edgeCount"),
165
170
  reviewCount: document.getElementById("reviewCount"),
166
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"),
167
183
  reviewList: document.getElementById("reviewList"),
184
+ proofOverview: document.getElementById("proofOverview"),
168
185
  proofStatus: document.getElementById("proofStatus"),
169
186
  proofList: document.getElementById("proofList"),
170
187
  intelligenceStatus: document.getElementById("intelligenceStatus"),
171
188
  intelligenceList: document.getElementById("intelligenceList"),
172
- viewerSectionButtons: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-viewer-section]")) : [],
173
- dashboardActionButtons: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-dashboard-action]")) : [],
174
- workspaceTabs: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-workspace-tab]")) : [],
175
- quickViewButtons: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-quick-view]")) : [],
176
- quickRenderButtons: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-quick-render]")) : [],
177
- quickSearch: document.getElementById("quickSearch"),
178
- quickPath: document.getElementById("quickPath"),
179
- quickInspector: document.getElementById("quickInspector"),
180
- closeWorkspace: document.getElementById("closeWorkspace")
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
+ }
181
231
  };
182
232
 
183
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"]);
184
235
  var INSPECTOR_CONNECTION_LIMIT = 8;
185
236
  var PATH_BRIDGE_EDGE_LIMIT_PER_PATH = 8;
186
237
  var PATH_BRIDGE_EDGE_LIMIT_TOTAL = 160;
@@ -188,82 +239,40 @@
188
239
  var VISIBLE_EDGE_MIN = 160;
189
240
  var VISIBLE_EDGE_MAX = 560;
190
241
 
191
- setViewerSection(state.viewerSection);
192
- setWorkspaceTab(state.workspaceTab, false);
193
- els.viewerSectionButtons.forEach(function (button) {
194
- button.addEventListener("click", function () {
195
- setViewerSection(button.getAttribute("data-viewer-section"));
196
- });
197
- });
198
- els.dashboardActionButtons.forEach(function (button) {
199
- button.addEventListener("click", function () {
200
- openDashboardAction(button.getAttribute("data-dashboard-action"));
201
- });
202
- });
203
- els.workspaceTabs.forEach(function (button) {
204
- button.addEventListener("click", function () {
205
- setWorkspaceTab(button.getAttribute("data-workspace-tab"), true);
206
- });
207
- });
208
- els.quickViewButtons.forEach(function (button) {
209
- button.addEventListener("click", function () {
210
- setViewerSection("graph");
211
- els.viewMode.value = button.getAttribute("data-quick-view") || "combined";
212
- state.lastVisibleSignature = "";
213
- syncQuickControls();
214
- render();
215
- });
216
- });
217
- els.quickRenderButtons.forEach(function (button) {
218
- button.addEventListener("click", function () {
219
- setViewerSection("graph");
220
- els.renderMode.value = button.getAttribute("data-quick-render") || "2d";
221
- state.lastVisibleSignature = "";
222
- syncQuickControls();
223
- render();
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);
224
250
  });
225
251
  });
226
- if (els.quickSearch) {
227
- els.quickSearch.addEventListener("click", function () {
228
- setViewerSection("graph");
229
- setWorkspaceTab("controls", true);
230
- els.searchInput.focus();
231
- });
232
- }
233
- if (els.quickPath) {
234
- els.quickPath.addEventListener("click", function () {
235
- setViewerSection("graph");
236
- setWorkspaceTab("controls", true);
237
- els.pathFromInput.focus();
238
- });
239
- }
240
- if (els.quickInspector) {
241
- els.quickInspector.addEventListener("click", function () {
242
- setViewerSection("graph");
243
- setWorkspaceTab("inspector", true);
244
- });
245
- }
246
- if (els.closeWorkspace) els.closeWorkspace.addEventListener("click", closeWorkspace);
247
- syncQuickControls();
248
252
  els.graphFile.addEventListener("change", handleFile);
249
253
  els.searchInput.addEventListener("input", scheduleRender);
250
254
  els.findPath.addEventListener("click", findDependencyPath);
251
255
  els.clearPath.addEventListener("click", clearDependencyPath);
252
256
  els.pathFromInput.addEventListener("keydown", function (event) { if (event.key === "Enter") findDependencyPath(); });
253
257
  els.pathToInput.addEventListener("keydown", function (event) { if (event.key === "Enter") findDependencyPath(); });
254
- els.viewMode.addEventListener("change", render);
258
+ els.viewMode.addEventListener("change", function () { clearGraphActionFilter(); render(); });
255
259
  els.renderMode.addEventListener("change", function () {
256
260
  state.lastVisibleSignature = "";
257
261
  render();
258
262
  });
259
- els.typeFilter.addEventListener("change", render);
260
- els.relationFilter.addEventListener("change", render);
263
+ els.typeFilter.addEventListener("change", function () { clearGraphActionFilter(); render(); });
264
+ els.relationFilter.addEventListener("change", function () { clearGraphActionFilter(); render(); });
261
265
  els.scopeFilter.addEventListener("change", render);
262
266
  els.maxNodes.addEventListener("change", render);
263
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"); });
264
271
  els.zoomOut.addEventListener("click", function () { zoomGraph(0.82); });
265
272
  els.zoomIn.addEventListener("click", function () { zoomGraph(1.22); });
266
273
  els.fitView.addEventListener("click", fitActiveGraph);
274
+ if (els.memorySearch) els.memorySearch.addEventListener("input", renderMemoryLibrary);
275
+ if (els.memoryFilter) els.memoryFilter.addEventListener("change", renderMemoryLibrary);
267
276
  els.canvas.addEventListener("mousedown", startCanvasPointer);
268
277
  els.canvas.addEventListener("mousemove", moveCanvasPointer);
269
278
  els.canvas.addEventListener("mouseup", endCanvasPointer);
@@ -284,7 +293,9 @@
284
293
  window.addEventListener("resize", function () {
285
294
  resizeActiveGraph();
286
295
  });
287
- els.resetView.addEventListener("click", function () {
296
+ els.resetView.addEventListener("click", resetGraphView);
297
+
298
+ function resetGraphView() {
288
299
  els.searchInput.value = "";
289
300
  els.viewMode.value = "combined";
290
301
  els.renderMode.value = "2d";
@@ -294,80 +305,160 @@
294
305
  els.maxNodes.value = "90";
295
306
  els.showDependencies.checked = false;
296
307
  state.selected = null;
297
- setWorkspaceTab("controls", true);
308
+ state.graphActionFilter = "";
298
309
  clearDependencyPath(false);
299
310
  state.lastVisibleSignature = "";
300
311
  render();
301
- });
312
+ }
302
313
  loadFromUrlParams();
303
314
 
304
- function setWorkspaceTab(tab, open) {
305
- var allowed = new Set(["controls", "inspector", "intelligence", "review", "tables"]);
306
- state.workspaceTab = allowed.has(tab) ? tab : "controls";
307
- if (open !== false) state.workspaceOpen = true;
308
- if (document.body && document.body.classList) {
309
- document.body.classList.remove(
310
- "viewer-tab-controls",
311
- "viewer-tab-inspector",
312
- "viewer-tab-intelligence",
313
- "viewer-tab-review",
314
- "viewer-tab-tables"
315
- );
316
- document.body.classList.add("viewer-tab-" + state.workspaceTab);
317
- document.body.classList.toggle("viewer-workspace-open", state.workspaceOpen);
318
- }
319
- els.workspaceTabs.forEach(function (button) {
320
- var active = button.getAttribute("data-workspace-tab") === state.workspaceTab;
321
- button.classList.toggle("active", active);
322
- button.setAttribute("aria-selected", active ? "true" : "false");
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");
323
430
  });
324
431
  }
325
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
+
326
447
  function setViewerSection(section, action) {
327
448
  state.viewerSection = section === "graph" ? "graph" : "overview";
328
449
  state.viewerAction = action || null;
450
+ state.viewerPage = pageFromSection(state.viewerSection, state.viewerAction);
329
451
  if (state.viewerSection === "overview") closeWorkspace();
330
452
  if (document.body && document.body.classList) {
331
453
  document.body.classList.remove("viewer-section-overview", "viewer-section-graph");
332
454
  document.body.classList.add("viewer-section-" + state.viewerSection);
333
455
  }
334
- syncSectionControls();
456
+ syncViewerPageLinks();
457
+ syncViewerPageClass();
335
458
  if (state.viewerSection === "graph") resizeActiveGraph();
336
459
  }
337
460
 
338
- function openDashboardAction(action) {
339
- var normalized = String(action || "").toLowerCase();
340
- var tabByAction = {
341
- memory: "review",
342
- intelligence: "intelligence",
343
- review: "review",
344
- data: "tables"
345
- };
346
- setViewerSection("graph", normalized);
347
- setWorkspaceTab(tabByAction[normalized] || "controls", true);
348
- }
349
-
350
- function syncSectionControls() {
351
- els.viewerSectionButtons.forEach(function (button) {
352
- var section = button.getAttribute("data-viewer-section");
353
- var active = state.viewerSection === section && !state.viewerAction;
354
- if (button.classList && button.classList.contains("viewer-section")) {
355
- button.classList.toggle("active", active);
356
- button.setAttribute("aria-selected", active ? "true" : "false");
357
- }
358
- });
359
- els.dashboardActionButtons.forEach(function (button) {
360
- var action = button.getAttribute("data-dashboard-action");
361
- var active = state.viewerSection === "graph" && state.viewerAction === action;
362
- if (button.classList && button.classList.contains("viewer-section")) {
363
- button.classList.toggle("active", active);
364
- button.setAttribute("aria-selected", active ? "true" : "false");
365
- }
366
- });
367
- }
368
-
369
461
  function closeWorkspace() {
370
- state.workspaceOpen = false;
371
462
  if (document.body && document.body.classList) {
372
463
  document.body.classList.remove("viewer-workspace-open");
373
464
  }
@@ -375,27 +466,10 @@
375
466
 
376
467
  function selectEntity(id, openInspector) {
377
468
  state.selected = { kind: "entity", id: id };
378
- setViewerSection("graph");
379
- if (openInspector) setWorkspaceTab("inspector", true);
380
469
  }
381
470
 
382
471
  function selectEdge(id, openInspector) {
383
472
  state.selected = { kind: "edge", id: id };
384
- setViewerSection("graph");
385
- if (openInspector) setWorkspaceTab("inspector", true);
386
- }
387
-
388
- function syncQuickControls() {
389
- els.quickViewButtons.forEach(function (button) {
390
- var active = button.getAttribute("data-quick-view") === els.viewMode.value;
391
- button.classList.toggle("active", active);
392
- button.setAttribute("aria-pressed", active ? "true" : "false");
393
- });
394
- els.quickRenderButtons.forEach(function (button) {
395
- var active = button.getAttribute("data-quick-render") === els.renderMode.value;
396
- button.classList.toggle("active", active);
397
- button.setAttribute("aria-pressed", active ? "true" : "false");
398
- });
399
473
  }
400
474
 
401
475
  function handleFile(event) {
@@ -411,7 +485,8 @@
411
485
  state.entityById = new Map();
412
486
  state.episodesById = new Map();
413
487
  els.emptyState.classList.add("hidden");
414
- 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;
415
490
  renderMetrics();
416
491
  return;
417
492
  }
@@ -452,7 +527,10 @@
452
527
  populateFilters();
453
528
  populatePathOptions();
454
529
  els.emptyState.classList.add("hidden");
455
- 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;
456
534
  render();
457
535
  }
458
536
 
@@ -1246,7 +1324,6 @@
1246
1324
 
1247
1325
  function render() {
1248
1326
  if (!state.graph) return;
1249
- syncQuickControls();
1250
1327
 
1251
1328
  var query = parseSearchQuery(els.searchInput.value);
1252
1329
  state.renderQuery = query;
@@ -1283,6 +1360,10 @@
1283
1360
  matchedEdgeIds = new Set(state.edges.filter(function (edge) { return mode === "combined" || edge.graph_kind === mode; }).map(function (edge) { return edge.id; }));
1284
1361
  }
1285
1362
 
1363
+ var actionFiltered = applyMatchedGraphActionFilter(matchedEntityIds, matchedEdgeIds);
1364
+ matchedEntityIds = actionFiltered.entities;
1365
+ matchedEdgeIds = actionFiltered.edges;
1366
+
1286
1367
  var visible = refineVisibleGraph(matchedEntityIds, matchedEdgeIds, {
1287
1368
  query: query,
1288
1369
  type: type,
@@ -1301,12 +1382,7 @@
1301
1382
  state.lastVisibleSignature = nextSignature;
1302
1383
 
1303
1384
  renderActiveGraph(graphChanged);
1304
- renderLists();
1305
- renderDetails();
1306
- renderMetrics();
1307
- renderReviewQueue();
1308
- renderProof();
1309
- renderIntelligence();
1385
+ renderPagePanels();
1310
1386
  }
1311
1387
 
1312
1388
  function scheduleRender() {
@@ -1317,6 +1393,88 @@
1317
1393
  });
1318
1394
  }
1319
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
+
1320
1478
  function refineVisibleGraph(entityIds, edgeIds, options) {
1321
1479
  var entities = new Set(entityIds);
1322
1480
  var edges = new Set(edgeIds);
@@ -1828,8 +1986,8 @@
1828
1986
  ctx.fillStyle = graphPalette.background;
1829
1987
  ctx.fillRect(0, 0, width, height);
1830
1988
  var gradient = ctx.createRadialGradient(width * 0.52, height * 0.44, 40, width * 0.52, height * 0.44, Math.max(width, height) * 0.72);
1831
- gradient.addColorStop(0, "rgba(65,255,143,0.080)");
1832
- 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)");
1833
1991
  gradient.addColorStop(1, "rgba(2,5,3,0)");
1834
1992
  ctx.fillStyle = gradient;
1835
1993
  ctx.fillRect(0, 0, width, height);
@@ -1905,9 +2063,9 @@
1905
2063
  var color = nodeThemeColor(entity);
1906
2064
  ctx.save();
1907
2065
  ctx.globalAlpha = alpha;
1908
- if (selected || hovered || pathNode) {
2066
+ if (selected || hovered || pathNode || entity.graph_kind === "memory") {
1909
2067
  ctx.shadowColor = pathNode ? graphPalette.bridge : color;
1910
- ctx.shadowBlur = selected ? 14 : pathNode ? 12 : 10;
2068
+ ctx.shadowBlur = selected ? 16 : pathNode ? 14 : entity.graph_kind === "memory" ? 9 : 10;
1911
2069
  }
1912
2070
  drawNodeShape(ctx, node.x, node.y, node.r, entity);
1913
2071
  ctx.fillStyle = nodeFillColor(entity);
@@ -2150,12 +2308,16 @@
2150
2308
  }
2151
2309
 
2152
2310
  function renderLists() {
2153
- var visibleEntities = state.entities.filter(function (entity) {
2154
- return state.visibleEntityIds.has(entity.id);
2155
- });
2156
- var visibleEdges = state.edges.filter(function (edge) {
2157
- return state.visibleEdgeIds.has(edge.id);
2158
- }).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) {
2159
2321
  return reviewRank(a) - reviewRank(b);
2160
2322
  });
2161
2323
 
@@ -2164,7 +2326,11 @@
2164
2326
  els.entityList.textContent = "";
2165
2327
  els.edgeList.textContent = "";
2166
2328
 
2167
- 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) {
2168
2334
  var button = document.createElement("button");
2169
2335
  button.type = "button";
2170
2336
  button.className = classNames("list-item", state.selected && state.selected.kind === "entity" && state.selected.id === entity.id && "selected");
@@ -2177,8 +2343,11 @@
2177
2343
  });
2178
2344
  els.entityList.appendChild(button);
2179
2345
  });
2346
+ if (visibleEntities.length > entityRows.length) {
2347
+ appendListNote(els.entityList, "Showing " + entityRows.length + " of " + visibleEntities.length + ". Use search to narrow.");
2348
+ }
2180
2349
 
2181
- visibleEdges.forEach(function (edge) {
2350
+ edgeRows.forEach(function (edge) {
2182
2351
  var button = document.createElement("button");
2183
2352
  button.type = "button";
2184
2353
  button.className = classNames("list-item", state.selected && state.selected.kind === "edge" && state.selected.id === edge.id && "selected");
@@ -2191,13 +2360,25 @@
2191
2360
  });
2192
2361
  els.edgeList.appendChild(button);
2193
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);
2194
2373
  }
2195
2374
 
2196
2375
  function renderDetails() {
2197
2376
  if (!state.selected) {
2377
+ setSelectionBodyState(null);
2198
2378
  els.selectionDetails.className = "details-empty";
2199
- 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.";
2200
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.", "");
2201
2382
  return;
2202
2383
  }
2203
2384
 
@@ -2206,10 +2387,12 @@
2206
2387
  : state.edges.find(function (edge) { return edge.id === state.selected.id; });
2207
2388
 
2208
2389
  if (!item) {
2390
+ setSelectionBodyState(null);
2209
2391
  els.selectionDetails.textContent = "Selection no longer exists.";
2210
2392
  return;
2211
2393
  }
2212
2394
 
2395
+ setSelectionBodyState(state.selected.kind === "entity" ? item : null);
2213
2396
  els.selectionDetails.className = "";
2214
2397
  els.selectionDetails.textContent = "";
2215
2398
  var title = document.createElement("div");
@@ -2246,6 +2429,32 @@
2246
2429
  els.selectionDetails.appendChild(kind);
2247
2430
  els.selectionDetails.appendChild(rows);
2248
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
+ }
2249
2458
  }
2250
2459
 
2251
2460
  function entityDetailRows(entity) {
@@ -2373,6 +2582,23 @@
2373
2582
  return Boolean(edge && (edge.memory_code_link || isMemoryCodeRelation(edge.relation)));
2374
2583
  }
2375
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
+
2376
2602
  function connectionImportance(link) {
2377
2603
  var relation = String(link.edge.relation || "");
2378
2604
  var score = entityImportance(link.other);
@@ -2475,13 +2701,15 @@
2475
2701
  }).length;
2476
2702
  var evidenceEdges = visibleEdges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length > 0; }).length;
2477
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;
2478
2706
  var metrics = official ? [
2479
- ["Readiness", official.harness.readiness_score + "/100"],
2480
- ["Code Files", official.code_graph.files],
2481
- ["Structural Symbols", official.structural_index ? official.structural_index.symbols : official.code_graph.symbols],
2482
- ["Parser Coverage", official.code_graph.indexer_coverage_percent + "%"],
2483
- ["Cache Hits", official.structural_index ? official.structural_index.cache_hits : official.code_graph.cache_hits],
2484
- ["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]
2485
2713
  ] : [
2486
2714
  ["Nodes", visibleEntities.length + "/" + state.entities.length],
2487
2715
  ["Relations", visibleEdges.length + "/" + state.edges.length],
@@ -2503,7 +2731,136 @@
2503
2731
  els.workspaceMode.textContent = (els.viewMode.value || "combined").replace(/^./, function (letter) { return letter.toUpperCase(); });
2504
2732
  els.graphSubhead.textContent = visibleEntities.length + " visible nodes and " + visibleEdges.length + " visible relations" +
2505
2733
  (hiddenDependencies && !els.showDependencies.checked ? " (" + hiddenDependencies + " dependency/noise nodes hidden)." : ".");
2506
- renderDashboard();
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); });
2507
2864
  }
2508
2865
 
2509
2866
  function renderDashboard() {
@@ -2519,64 +2876,174 @@
2519
2876
  var memoryCodeEdges = state.edges.filter(isMemoryCodeEdge);
2520
2877
  var reports = state.reports || {};
2521
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";
2522
2891
  var statRows = [
2523
- ["Memory packets", firstNumber(memoryGraph.approved_packets, memoryNodes)],
2524
- ["Code nodes", firstNumber(codeGraph.symbols, structural.symbols, codeNodes)],
2525
- ["Files", firstNumber(codeGraph.files, structural.files, countEntitiesByType("file"))],
2526
- ["Memory-code links", memoryCodeEdges.length],
2527
- ["Parser coverage", codeGraph.indexer_coverage_percent != null ? codeGraph.indexer_coverage_percent + "%" : "n/a"],
2528
- ["Tokens saved", firstNumber(savings.estimated_tokens_saved_per_recall, pain.estimated_tokens_saved, "n/a")]
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"]
2529
2896
  ];
2530
2897
  els.dashboardStats.textContent = "";
2531
2898
  statRows.forEach(function (row) {
2532
2899
  var item = document.createElement("div");
2533
- item.className = "dashboard-stat";
2534
- item.innerHTML = "<strong></strong><span></span>";
2900
+ item.className = classNames("dashboard-stat", row[3] && "dashboard-stat-" + row[3]);
2901
+ item.innerHTML = "<span></span><strong></strong><em></em>";
2535
2902
  item.querySelector("strong").textContent = formatDashboardValue(row[1]);
2536
2903
  item.querySelector("span").textContent = row[0];
2904
+ item.querySelector("em").textContent = row[2] || "";
2537
2905
  els.dashboardStats.appendChild(item);
2538
2906
  });
2539
2907
 
2540
2908
  setDashboardRows("dashboardMemory", [
2541
- ["Approved packets", firstNumber(memoryGraph.approved_packets, memoryNodes)],
2542
- ["Pending review", firstNumber(memoryGraph.pending_packets, (state.pendingPackets || []).length)],
2543
- ["Evidence coverage", memoryGraph.evidence_coverage_percent != null ? memoryGraph.evidence_coverage_percent + "%" : "n/a"],
2544
- ["Code-linked memory", memoryCodeEdges.length]
2909
+ ["Reusable", firstNumber(memoryGraph.approved_packets, memoryNodes) + " packets"],
2910
+ ["Linked", memoryCodeEdges.length + " code links"],
2911
+ ["Review", pendingReview ? pendingReview + " pending" : "clear"]
2545
2912
  ]);
2546
2913
  setDashboardRows("dashboardGraph", [
2547
2914
  ["Files", firstNumber(codeGraph.files, structural.files, countEntitiesByType("file"))],
2548
2915
  ["Symbols", firstNumber(codeGraph.symbols, structural.symbols, countEntitiesByType("symbol"))],
2549
- ["Relations", state.edges.length],
2550
- ["Dependency/noise hidden", state.entities.filter(function (entity) { return isDependencyEntity(entity); }).length]
2916
+ ["Coverage", codeGraph.indexer_coverage_percent != null ? codeGraph.indexer_coverage_percent + "%" : "not loaded"]
2551
2917
  ]);
2552
2918
  setDashboardRows("dashboardIntel", [
2553
- ["Reports loaded", reportCount],
2554
- ["Decision coverage", reports.decisions && reports.decisions.coverage_percent != null ? reports.decisions.coverage_percent + "%" : "n/a"],
2555
- ["Modules scored", reports.moduleHealth && Array.isArray(reports.moduleHealth.modules) ? reports.moduleHealth.modules.length : "n/a"],
2556
- ["Communities", reports.graphInsights && Array.isArray(reports.graphInsights.communities) ? reports.graphInsights.communities.length : "n/a"]
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"]
2557
2922
  ]);
2558
- var risk = reports.risk || {};
2559
- var riskTargets = Array.isArray(risk.targets) ? risk.targets : Object.keys(risk.targets || {});
2560
- setDashboardRows("dashboardRisk", [
2561
- ["Risk targets", riskTargets.length || "n/a"],
2562
- ["Hotspots", Array.isArray(risk.global_hotspots) ? risk.global_hotspots.length : "n/a"],
2563
- ["Ownership silos", Array.isArray(risk.ownership_silos) ? risk.ownership_silos.length : "n/a"],
2564
- ["Cycles", reports.graphInsights && Array.isArray(reports.graphInsights.dependency_cycles) ? reports.graphInsights.dependency_cycles.length : "n/a"]
2565
- ]);
2566
- var inboxCounts = state.inbox && state.inbox.counts ? state.inbox.counts : {};
2567
2923
  setDashboardRows("dashboardReview", [
2568
- ["Readiness", metrics.harness && metrics.harness.readiness_score != null ? metrics.harness.readiness_score + "/100" : "n/a"],
2569
- ["Inbox pending", firstNumber(inboxCounts.pending, (state.pendingPackets || []).length)],
2570
- ["Stale flags", firstNumber(inboxCounts.stale, 0)],
2571
- ["Duplicate flags", firstNumber(inboxCounts.duplicates, 0)]
2572
- ]);
2573
- var workspace = reports.workspace || {};
2574
- setDashboardRows("dashboardWorkspace", [
2575
- ["Repos", Array.isArray(workspace.repos) ? workspace.repos.length : "n/a"],
2576
- ["Package deps", Array.isArray(workspace.package_dependencies) ? workspace.package_dependencies.length : "n/a"],
2577
- ["Route contracts", Array.isArray(workspace.route_contracts) ? workspace.route_contracts.length : "n/a"],
2578
- ["Co-changes", Array.isArray(workspace.co_changes) ? workspace.co_changes.length : "n/a"]
2924
+ ["Handoff", readiness.label],
2925
+ ["Pending", pendingReview || "none"],
2926
+ ["Stale / duplicate", staleFlags + " / " + duplicateFlags],
2927
+ ["Missing context", missingContext || "none"]
2579
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
+ };
2580
3047
  }
2581
3048
 
2582
3049
  function setDashboardRows(cardId, rows) {
@@ -2611,13 +3078,290 @@
2611
3078
  return String(value == null ? "n/a" : value);
2612
3079
  }
2613
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"));
3353
+ }
3354
+
2614
3355
  function renderReviewQueue() {
2615
3356
  if (!els.reviewList) return;
2616
3357
  var packets = state.pendingPackets || [];
2617
3358
  var inbox = state.inbox;
2618
3359
  var inboxItems = inbox && Array.isArray(inbox.items) ? inbox.items : [];
2619
- 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);
2620
3363
  els.reviewList.textContent = "";
3364
+ if (els.reviewOverview) renderReviewOverview(inbox, packets, inboxItems);
2621
3365
  if (!packets.length && !inboxItems.length && !state.reviewText) {
2622
3366
  els.reviewList.className = "review-list details-empty";
2623
3367
  els.reviewList.textContent = "No pending packets loaded. Launch with `kage viewer --project <repo>` to load review context automatically.";
@@ -2627,7 +3371,6 @@
2627
3371
  if (inbox) {
2628
3372
  var summary = document.createElement("div");
2629
3373
  summary.className = "review-item";
2630
- var counts = inbox.counts || {};
2631
3374
  summary.innerHTML = [
2632
3375
  "<div class=\"review-title\"></div>",
2633
3376
  "<div class=\"review-meta\"></div>",
@@ -2644,10 +3387,10 @@
2644
3387
  summary.querySelector(".review-summary").textContent = Array.isArray(inbox.recommendations) && inbox.recommendations.length
2645
3388
  ? inbox.recommendations.slice(0, 2).join(" ")
2646
3389
  : "No inbox recommendations.";
2647
- 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";
2648
3391
  els.reviewList.appendChild(summary);
2649
3392
  }
2650
- inboxItems.slice(0, 20).forEach(function (entry) {
3393
+ inboxItems.slice(0, 8).forEach(function (entry) {
2651
3394
  var item = document.createElement("div");
2652
3395
  item.className = "review-item";
2653
3396
  item.innerHTML = [
@@ -2659,7 +3402,9 @@
2659
3402
  item.querySelector(".review-title").textContent = entry.title || entry.summary || entry.kind;
2660
3403
  item.querySelector(".review-meta").textContent = [entry.kind, entry.severity, entry.type, entry.status].filter(Boolean).join(" | ");
2661
3404
  item.querySelector(".review-summary").textContent = entry.action || entry.summary || "";
2662
- 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";
2663
3408
  els.reviewList.appendChild(item);
2664
3409
  });
2665
3410
  packets.forEach(function (packet) {
@@ -2675,7 +3420,9 @@
2675
3420
  item.querySelector(".review-title").textContent = packet.title || packet.id;
2676
3421
  item.querySelector(".review-meta").textContent = [packet.type, packet.status, "score " + (quality.score == null ? "n/a" : quality.score + "/100")].filter(Boolean).join(" | ");
2677
3422
  item.querySelector(".review-summary").textContent = packet.summary || "";
2678
- 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";
2679
3426
  els.reviewList.appendChild(item);
2680
3427
  });
2681
3428
  if (state.reviewText) {
@@ -2687,25 +3434,58 @@
2687
3434
  }
2688
3435
  }
2689
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
+
2690
3470
  function renderProof() {
2691
3471
  if (!els.proofList) return;
2692
3472
  var metrics = state.metrics;
2693
3473
  els.proofList.textContent = "";
2694
3474
  if (!metrics) {
2695
3475
  els.proofStatus.textContent = "not loaded";
3476
+ if (els.proofOverview) els.proofOverview.textContent = "";
2696
3477
  els.proofList.className = "proof-list details-empty";
2697
3478
  els.proofList.textContent = "Metrics not loaded. Run `kage metrics --project <repo> --json > .agent_memory/metrics.json` or launch with `kage viewer`.";
2698
3479
  return;
2699
3480
  }
2700
3481
  els.proofStatus.textContent = "loaded";
2701
3482
  els.proofList.className = "proof-list";
3483
+ if (els.proofOverview) renderProofOverview(metrics, state.reports || {});
2702
3484
  var rows = [
2703
- ["Readiness", metrics.harness && metrics.harness.readiness_score != null ? metrics.harness.readiness_score + "/100" : "n/a"],
2704
- ["Useful memory", metrics.quality ? metrics.quality.useful_memory_ratio_percent + "%" : "n/a"],
3485
+ ["Validation", metrics.harness && metrics.harness.validation_ok ? "clean" : "check"],
2705
3486
  ["Evidence", metrics.memory_graph ? metrics.memory_graph.evidence_coverage_percent + "%" : "n/a"],
2706
3487
  ["Pending review", metrics.memory_graph ? String(metrics.memory_graph.pending_packets) : "n/a"],
2707
- ["Recall hit rate", metrics.pain ? metrics.pain.recall_hit_rate_percent + "%" : "n/a"],
2708
- ["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"]
2709
3489
  ];
2710
3490
  rows.forEach(function (row) {
2711
3491
  var item = document.createElement("div");
@@ -2717,6 +3497,29 @@
2717
3497
  });
2718
3498
  }
2719
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
+
2720
3523
  function renderIntelligence() {
2721
3524
  if (!els.intelligenceList) return;
2722
3525
  var reports = state.reports || {};
@@ -2729,36 +3532,40 @@
2729
3532
  return;
2730
3533
  }
2731
3534
  els.intelligenceList.className = "intelligence-list";
2732
- cards.forEach(function (card) {
3535
+ normalizeIntelCards(cards).slice(0, 6).forEach(function (card) {
2733
3536
  var item = document.createElement("article");
2734
3537
  item.className = "intel-card";
2735
3538
  item.innerHTML = [
2736
- "<h3></h3>",
2737
- "<div class=\"intel-kicker\"></div>",
2738
- "<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>",
2739
3543
  "<ul></ul>"
2740
3544
  ].join("");
2741
3545
  item.querySelector("h3").textContent = card.title;
2742
- item.querySelector(".intel-kicker").textContent = card.kicker;
2743
- 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.";
2744
3551
  var list = item.querySelector("ul");
2745
- card.rows.slice(0, 5).forEach(function (row) {
3552
+ card.rows.slice(0, 3).forEach(function (row) {
2746
3553
  var li = document.createElement("li");
2747
3554
  li.innerHTML = "<strong></strong> <span></span>";
2748
3555
  li.querySelector("strong").textContent = row[0];
2749
- li.querySelector("span").textContent = row[1];
3556
+ li.querySelector("span").textContent = trimIntelText(row[1], 92);
2750
3557
  list.appendChild(li);
2751
3558
  });
2752
3559
  els.intelligenceList.appendChild(item);
2753
3560
  });
2754
- var sections = buildIntelligenceSections(reports);
3561
+ var sections = rankIntelligenceSections(buildIntelligenceSections(reports)).slice(0, 4);
2755
3562
  if (sections.length) {
2756
- var grid = document.createElement("div");
2757
- grid.className = "intel-deep-grid";
3563
+ var deepGrid = document.createElement("div");
3564
+ deepGrid.className = "intel-deep-grid";
2758
3565
  sections.forEach(function (section) {
2759
- grid.appendChild(renderIntelligenceSection(section));
3566
+ deepGrid.appendChild(renderIntelligenceSection(section));
2760
3567
  });
2761
- els.intelligenceList.appendChild(grid);
3568
+ els.intelligenceList.appendChild(deepGrid);
2762
3569
  }
2763
3570
  }
2764
3571
 
@@ -2781,32 +3588,46 @@
2781
3588
  var list = document.createElement("div");
2782
3589
  list.className = "intel-section-list";
2783
3590
  section.rows.slice(0, section.limit || 8).forEach(function (row) {
2784
- var button = document.createElement("button");
2785
- button.type = "button";
2786
- button.className = classNames("intel-row", row.status && "intel-row-" + safeCssName(row.status), row.path && "clickable");
2787
- 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 = [
2788
3595
  "<span class=\"intel-row-main\"><strong></strong><em></em></span>",
2789
3596
  "<span class=\"intel-row-meta\"></span>",
2790
3597
  "<span class=\"intel-row-bar\"><i></i></span>"
2791
3598
  ].join("");
2792
- button.querySelector("strong").textContent = row.label || "";
2793
- button.querySelector("em").textContent = row.value || "";
2794
- button.querySelector(".intel-row-meta").textContent = row.meta || "";
2795
- 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) + "%";
2796
3603
  if (row.path) {
2797
- button.title = "Focus " + row.path + " in the graph";
2798
- 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 () {
2799
3607
  focusGraphPath(row.path);
2800
3608
  });
2801
- } else {
2802
- button.disabled = true;
2803
3609
  }
2804
- list.appendChild(button);
3610
+ list.appendChild(rowEl);
2805
3611
  });
2806
3612
  panel.appendChild(list);
2807
3613
  return panel;
2808
3614
  }
2809
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
+
2810
3631
  function buildIntelligenceSections(reports) {
2811
3632
  var sections = [];
2812
3633
  var contributors = reports.contributors;
@@ -2846,7 +3667,7 @@
2846
3667
  title: "Ownership Map",
2847
3668
  kicker: "who owns what",
2848
3669
  stat: silos.length + " silos",
2849
- 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.",
2850
3671
  rows: rows,
2851
3672
  limit: 10,
2852
3673
  });
@@ -2861,7 +3682,7 @@
2861
3682
  title: "Module Health Map",
2862
3683
  kicker: "churn / tests / ownership",
2863
3684
  stat: modules.length + " modules",
2864
- 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.",
2865
3686
  rows: modules.slice(0, 8).map(function (item) {
2866
3687
  return {
2867
3688
  label: item.module,
@@ -2880,7 +3701,7 @@
2880
3701
  title: "Onboarding Targets",
2881
3702
  kicker: "missing repo lore",
2882
3703
  stat: (decisions.coverage_percent != null ? decisions.coverage_percent + "%" : "n/a"),
2883
- 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.",
2884
3705
  rows: gaps.slice(0, 8).map(function (gap) {
2885
3706
  var score = Math.min(100, Number(gap.dependents || 0) * 18 + Number(gap.churn_90d || 0) * 6 + 12);
2886
3707
  return {
@@ -2906,7 +3727,7 @@
2906
3727
  title: "Architecture Communities",
2907
3728
  kicker: "module clusters",
2908
3729
  stat: communities.length + " clusters",
2909
- 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.",
2910
3731
  rows: communities.slice(0, 8).map(function (community) {
2911
3732
  var files = community.files || [];
2912
3733
  var entrypoints = community.entrypoints || [];
@@ -2928,7 +3749,7 @@
2928
3749
  title: "Execution Flows",
2929
3750
  kicker: "entrypoint traces",
2930
3751
  stat: insights.entry_flows.length + " flows",
2931
- 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.",
2932
3753
  rows: insights.entry_flows.slice(0, 8).map(function (flow) {
2933
3754
  var path = flow.path || [];
2934
3755
  return {
@@ -2987,7 +3808,7 @@
2987
3808
  title: "Workspace Map",
2988
3809
  kicker: "deps / contracts / co-changes",
2989
3810
  stat: workspaceRows.length + " links",
2990
- summary: "Workspace links show how sibling repos relate through package dependencies, source-evidence contracts, topic/event links, and local git co-change history.",
3811
+ summary: "Action: check linked repos before changing shared packages or contracts.",
2991
3812
  rows: workspaceRows,
2992
3813
  limit: 14,
2993
3814
  });
@@ -3021,7 +3842,7 @@
3021
3842
  title: "Blast Radius",
3022
3843
  kicker: "change impact",
3023
3844
  stat: riskRows.length + " signals",
3024
- 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.",
3025
3846
  rows: riskRows,
3026
3847
  limit: 10,
3027
3848
  });
@@ -3043,11 +3864,13 @@
3043
3864
  });
3044
3865
  if (!found) {
3045
3866
  els.searchInput.value = normalized;
3867
+ showViewerPageInPlace("graph");
3046
3868
  scheduleRender();
3047
3869
  return;
3048
3870
  }
3049
3871
  selectEntity(found.id, true);
3050
3872
  els.searchInput.value = normalized;
3873
+ showViewerPageInPlace("graph");
3051
3874
  scheduleRender();
3052
3875
  }
3053
3876
 
@@ -3209,18 +4032,86 @@
3209
4032
  return cards;
3210
4033
  }
3211
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
+
3212
4101
  function renderStatusStrip(visibleEntities, visibleEdges, official) {
3213
4102
  if (!els.statusStrip) return;
3214
4103
  var memoryCount = visibleEntities.filter(function (entity) { return entity.graph_kind === "memory"; }).length;
3215
4104
  var codeCount = visibleEntities.filter(function (entity) { return entity.graph_kind === "code"; }).length;
3216
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;
3217
4108
  var pills = official ? [
3218
- ["Readiness", official.harness.readiness_score + "/100", ""],
3219
- ["Pending", official.memory_graph ? String(official.memory_graph.pending_packets) : "n/a", official.memory_graph && official.memory_graph.pending_packets ? "warn" : ""],
3220
- ["Structural", official.structural_index ? official.structural_index.files + " files" : official.code_graph.files + " files", "code"],
3221
- ["Code symbols", String(official.code_graph.symbols), "code"],
3222
- ["Parser coverage", official.code_graph.indexer_coverage_percent + "%", "code"],
3223
- ["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"]
3224
4115
  ] : [
3225
4116
  ["Memory", String(memoryCount), "memory"],
3226
4117
  ["Code", String(codeCount), "code"],
@@ -3473,10 +4364,15 @@
3473
4364
  if (state.three.THREE) return Promise.resolve(state.three.THREE);
3474
4365
  if (state.three.loading) return state.three.loading;
3475
4366
  state.three.failed = false;
3476
- state.three.loading = import("/vendor/three/build/three.module.min.js")
3477
- .catch(function () {
3478
- return import("https://unpkg.com/three@0.184.0/build/three.module.min.js");
3479
- })
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())
3480
4376
  .then(function (mod) {
3481
4377
  state.three.THREE = mod;
3482
4378
  return mod;
@@ -3493,7 +4389,7 @@
3493
4389
  ensureThree().then(function () {
3494
4390
  if (activeRenderMode() !== "3d") return;
3495
4391
  setupThreeScene();
3496
- rebuildThreeScene();
4392
+ if (graphChanged || !state.three.nodeById.size) rebuildThreeScene();
3497
4393
  if (graphChanged || state.three.distance <= 0) fitThreeGraph();
3498
4394
  startThreeGraph();
3499
4395
  renderThreeFrame();