@kage-core/kage-graph-mcp 1.1.20 → 1.1.21

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
@@ -111,8 +111,13 @@
111
111
  proofList: document.getElementById("proofList")
112
112
  };
113
113
 
114
- var MEMORY_CODE_RELATIONS = new Set(["explains_symbol", "informs_symbol", "fixes_symbol", "applies_to_route", "verified_by_test", "affects_path"]);
114
+ var MEMORY_CODE_RELATIONS = new Set(["explains_symbol", "informs_symbol", "fixes_symbol", "applies_to_route", "verified_by_test", "affects_code_path"]);
115
115
  var INSPECTOR_CONNECTION_LIMIT = 8;
116
+ var PATH_BRIDGE_EDGE_LIMIT_PER_PATH = 8;
117
+ var PATH_BRIDGE_EDGE_LIMIT_TOTAL = 160;
118
+ var VISIBLE_EDGE_MULTIPLIER = 4;
119
+ var VISIBLE_EDGE_MIN = 160;
120
+ var VISIBLE_EDGE_MAX = 560;
116
121
 
117
122
  els.graphFile.addEventListener("change", handleFile);
118
123
  els.searchInput.addEventListener("input", scheduleRender);
@@ -221,12 +226,13 @@
221
226
 
222
227
  function loadFromUrlParams() {
223
228
  var params = new URLSearchParams(window.location.search);
224
- var graphPaths = []
225
- .concat(params.getAll("graph"))
226
- .concat(params.getAll("code"))
227
- .flatMap(function (value) { return String(value || "").split(","); })
228
- .map(function (value) { return value.trim(); })
229
- .filter(Boolean);
229
+ applyRequestedView(params.get("view") || params.get("mode"));
230
+ var memoryGraphPaths = splitParamValues(params.getAll("graph"));
231
+ var codeGraphPaths = splitParamValues(params.getAll("code"));
232
+ var graphPaths = memoryGraphPaths.concat(codeGraphPaths);
233
+ var graphLabels = new Map();
234
+ memoryGraphPaths.forEach(function (path) { graphLabels.set(path, "memory knowledge graph"); });
235
+ codeGraphPaths.forEach(function (path) { graphLabels.set(path, "structural code graph"); });
230
236
  var metricsPath = params.get("metrics");
231
237
  var inboxPath = params.get("inbox");
232
238
  var reviewPath = params.get("review");
@@ -246,10 +252,7 @@
246
252
  }
247
253
  setAutoLoad("loading project graph", false);
248
254
  Promise.all(graphPaths.map(function (path) {
249
- return fetch(path).then(function (response) {
250
- if (!response.ok) throw new Error(response.status + " " + path);
251
- return response.json().then(function (graph) { return { fileName: path.split("/").pop() || path, graph: graph }; });
252
- });
255
+ return loadGraphPath(path).then(function (graph) { return { fileName: graphLabels.get(path) || path.split("/").pop() || path, graph: graph }; });
253
256
  }).concat(jobs)).then(function (items) {
254
257
  var graphItems = items.filter(Boolean);
255
258
  if (!graphItems.length) {
@@ -270,7 +273,7 @@
270
273
  setAutoLoad("loading hosted repo graph", false);
271
274
  Promise.all([
272
275
  fetchJson("./data/kage/graph.json"),
273
- fetchJson("./data/kage/code_graph/graph.json"),
276
+ loadGraphPath("./data/kage/code_graph/graph.json"),
274
277
  fetchJson("./data/kage/metrics.json").catch(function () { return null; }),
275
278
  fetchJson("./data/kage/inbox.json").catch(function () { return null; })
276
279
  ]).then(function (items) {
@@ -313,6 +316,58 @@
313
316
  });
314
317
  }
315
318
 
319
+ function loadGraphPath(path) {
320
+ return fetchJson(path).then(function (graph) {
321
+ return hydrateCompactCodeGraph(graph, path);
322
+ });
323
+ }
324
+
325
+ function resolveGraphRef(basePath, ref) {
326
+ if (!ref) return ref;
327
+ try {
328
+ return new URL(ref, new URL(basePath, window.location.href)).href;
329
+ } catch (_error) {
330
+ var base = String(basePath || "");
331
+ return base.replace(/\/[^/]*$/, "/" + ref);
332
+ }
333
+ }
334
+
335
+ function hydrateCompactCodeGraph(graph, basePath) {
336
+ if (!graph || graph.compact !== true || !graph.refs) return Promise.resolve(graph);
337
+ if (graph.refs.entities && graph.refs.edges) {
338
+ return Promise.all([
339
+ fetchJson(resolveGraphRef(basePath, graph.refs.entities)),
340
+ fetchJson(resolveGraphRef(basePath, graph.refs.edges)),
341
+ graph.refs.episodes ? fetchJson(resolveGraphRef(basePath, graph.refs.episodes)) : Promise.resolve([])
342
+ ]).then(function (items) {
343
+ return Object.assign({}, graph, {
344
+ compact: false,
345
+ entities: items[0],
346
+ edges: items[1],
347
+ episodes: items[2]
348
+ });
349
+ });
350
+ }
351
+ return Promise.all([
352
+ fetchJson(resolveGraphRef(basePath, graph.refs.files)),
353
+ fetchJson(resolveGraphRef(basePath, graph.refs.symbols)),
354
+ fetchJson(resolveGraphRef(basePath, graph.refs.imports))
355
+ ]).then(function (items) {
356
+ var fileOverrides = new Map(graph.file_parser_overrides || []);
357
+ var symbolOverrides = new Map(graph.symbol_parser_overrides || []);
358
+ return Object.assign({}, graph, {
359
+ compact: false,
360
+ files: (items[0] || []).map(function (file) {
361
+ return fileOverrides.has(file.path) ? Object.assign({}, file, { parser: fileOverrides.get(file.path) }) : file;
362
+ }),
363
+ symbols: (items[1] || []).map(function (symbol) {
364
+ return symbolOverrides.has(symbol.id) ? Object.assign({}, symbol, { parser: symbolOverrides.get(symbol.id) }) : symbol;
365
+ }).concat(graph.extra_symbols || []),
366
+ imports: (items[2] || []).concat(graph.extra_imports || [])
367
+ });
368
+ });
369
+ }
370
+
316
371
  function fetchText(path) {
317
372
  return fetch(path).then(function (response) {
318
373
  if (!response.ok) throw new Error(response.status + " " + path);
@@ -379,7 +434,8 @@
379
434
  graph.edges.forEach(function (edge) {
380
435
  var from = remappedIds.get(edge.from) || edge.from;
381
436
  var to = remappedIds.get(edge.to) || edge.to;
382
- var memoryCodeLink = Boolean(edge.memory_code_link || isMemoryCodeRelation(edge.relation));
437
+ var crossesBoundary = crossesMemoryCodeBoundary(entities.get(from), entities.get(to));
438
+ var memoryCodeLink = Boolean(crossesBoundary && (edge.memory_code_link || isMemoryCodeRelation(edge.relation) || edge.relation === "affects_path"));
383
439
  var id = edge.id + ":" + from + ":" + to;
384
440
  edges.set(id, Object.assign({}, edge, {
385
441
  id: id,
@@ -390,18 +446,131 @@
390
446
  });
391
447
  graph.episodes.forEach(function (episode) { episodes.set(episode.id, episode); });
392
448
  });
449
+ addPathPrefixBridgeEdges(entities, edges);
393
450
  return { entities: Array.from(entities.values()), edges: Array.from(edges.values()), episodes: Array.from(episodes.values()) };
394
451
  }
395
452
 
453
+ function crossesMemoryCodeBoundary(from, to) {
454
+ if (!from || !to) return false;
455
+ return (from.graph_kind === "memory" && to.graph_kind === "code") ||
456
+ (from.graph_kind === "code" && to.graph_kind === "memory");
457
+ }
458
+
459
+ function addPathPrefixBridgeEdges(entities, edges) {
460
+ var codeFiles = Array.from(entities.values()).filter(function (entity) {
461
+ return entity.graph_kind === "code" && entity.type === "file" && entity.path;
462
+ });
463
+ if (!codeFiles.length) return;
464
+
465
+ var codeDegree = new Map();
466
+ edges.forEach(function (edge) {
467
+ var from = entities.get(edge.from);
468
+ var to = entities.get(edge.to);
469
+ if (from && from.graph_kind === "code") codeDegree.set(from.id, (codeDegree.get(from.id) || 0) + 1);
470
+ if (to && to.graph_kind === "code") codeDegree.set(to.id, (codeDegree.get(to.id) || 0) + 1);
471
+ });
472
+
473
+ var total = 0;
474
+ Array.from(edges.values()).filter(function (edge) {
475
+ return edge.relation === "affects_path";
476
+ }).forEach(function (edge) {
477
+ if (total >= PATH_BRIDGE_EDGE_LIMIT_TOTAL) return;
478
+ var memoryEntity = entities.get(edge.from);
479
+ var pathEntity = entities.get(edge.to);
480
+ if (!memoryEntity || !pathEntity || memoryEntity.graph_kind !== "memory") return;
481
+ if (pathEntity.graph_kind === "code") return;
482
+
483
+ var memoryPath = entityPathValue(pathEntity);
484
+ var matches = codeFiles.filter(function (file) {
485
+ return fileMatchesMemoryPath(file.path, memoryPath);
486
+ }).sort(function (a, b) {
487
+ return pathBridgeFileScore(b, memoryPath, codeDegree) - pathBridgeFileScore(a, memoryPath, codeDegree) ||
488
+ String(a.path || "").localeCompare(String(b.path || ""));
489
+ }).slice(0, PATH_BRIDGE_EDGE_LIMIT_PER_PATH);
490
+
491
+ matches.forEach(function (file) {
492
+ if (total >= PATH_BRIDGE_EDGE_LIMIT_TOTAL) return;
493
+ var id = "path_bridge:" + edge.id + ":" + file.id;
494
+ if (edges.has(id)) return;
495
+ edges.set(id, {
496
+ id: id,
497
+ from: edge.from,
498
+ to: file.id,
499
+ relation: "affects_code_path",
500
+ fact: (memoryEntity.name || "Memory") + " applies to code under " + (memoryPath || "repo root") + ": " + file.path + ".",
501
+ confidence: Math.min(Number(edge.confidence || 0.7), 0.75),
502
+ evidence: edge.evidence || [],
503
+ commit: edge.commit,
504
+ source: "viewer_path_bridge",
505
+ graph_kind: "memory",
506
+ memory_code_link: true
507
+ });
508
+ total += 1;
509
+ });
510
+ });
511
+ }
512
+
513
+ function entityPathValue(entity) {
514
+ var candidates = [entity.path, entity.name].concat(entity.aliases || [], entity.id || []);
515
+ for (var index = 0; index < candidates.length; index += 1) {
516
+ var normalized = normalizeRepoPath(candidates[index]);
517
+ if (normalized || normalized === "") return normalized;
518
+ }
519
+ return null;
520
+ }
521
+
522
+ function normalizeRepoPath(value) {
523
+ if (value === null || value === undefined) return null;
524
+ var text = String(value).trim();
525
+ if (!text) return null;
526
+ text = text.replace(/^path:/, "").replace(/^file:/, "").replace(/\\/g, "/");
527
+ text = text.replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/$/, "");
528
+ if ([".", "/", "root"].indexOf(text.toLowerCase()) !== -1) return "";
529
+ return text;
530
+ }
531
+
532
+ function fileMatchesMemoryPath(filePath, memoryPath) {
533
+ var file = normalizeRepoPath(filePath);
534
+ if (file === null || memoryPath === null) return false;
535
+ if (memoryPath === "") return true;
536
+ return file === memoryPath || file.indexOf(memoryPath + "/") === 0;
537
+ }
538
+
539
+ function pathBridgeFileScore(file, memoryPath, codeDegree) {
540
+ var path = String(file.path || "");
541
+ var lower = path.toLowerCase();
542
+ var score = (codeDegree.get(file.id) || 0) * 6;
543
+ if (normalizeRepoPath(path) === memoryPath) score += 90;
544
+ if (/^(readme|package|agents|claude)\./i.test(path.split("/").pop() || "")) score += 28;
545
+ if (["source", "test"].indexOf(file.kind) !== -1) score += 14;
546
+ if (lower.indexOf(".agent_memory/") !== -1 || lower.indexOf("node_modules/") !== -1 || lower.indexOf("dist/") !== -1 || lower.indexOf("build/") !== -1) score -= 80;
547
+ score -= path.split("/").length * 2;
548
+ score -= Math.min(20, path.length / 60);
549
+ return score;
550
+ }
551
+
396
552
  function canonicalEntityId(entity, aliasToCodeId) {
397
553
  if (!entity) return "";
398
554
  if (entity.graph_kind === "memory" && ["symbol", "test", "route", "file", "path"].indexOf(entity.type) !== -1) {
399
- var alias = [entity.id, entity.name].concat(entity.aliases || []).find(function (value) { return aliasToCodeId.has(value); });
555
+ var candidates = [entity.id, entity.name].concat(entity.aliases || []).filter(function (value) {
556
+ return canonicalAliasMatchesEntityType(value, entity.type);
557
+ });
558
+ var alias = candidates.find(function (value) { return aliasToCodeId.has(value); });
400
559
  if (alias) return aliasToCodeId.get(alias);
401
560
  }
402
561
  return aliasToCodeId.get(entity.id) || entity.id;
403
562
  }
404
563
 
564
+ function canonicalAliasMatchesEntityType(value, type) {
565
+ if (!value) return false;
566
+ var text = String(value);
567
+ if (type === "file" || type === "path") return true;
568
+ if (type === "route") return text.indexOf("route:") === 0;
569
+ if (type === "test") return text.indexOf("symbol:") === 0 || text.indexOf("test:") === 0;
570
+ if (type === "symbol") return text.indexOf("symbol:") === 0;
571
+ return false;
572
+ }
573
+
405
574
  function mergeViewerEntity(existing, next) {
406
575
  if (!existing) return next;
407
576
  var graphKinds = unique([existing.graph_kind, next.graph_kind].concat(existing.graph_kinds || []).concat(next.graph_kinds || []));
@@ -463,6 +632,13 @@
463
632
  name: file.path,
464
633
  summary: file.kind + " file, " + file.language + ", " + file.line_count + " lines",
465
634
  aliases: [file.hash, file.path],
635
+ path: file.path,
636
+ language: file.language,
637
+ parser: file.parser,
638
+ kind: file.kind,
639
+ size_bytes: file.size_bytes,
640
+ line_count: file.line_count,
641
+ hash: file.hash,
466
642
  evidence: []
467
643
  });
468
644
  });
@@ -474,7 +650,15 @@
474
650
  graph_kind: "code",
475
651
  name: symbol.name,
476
652
  summary: symbol.kind + " in " + symbol.path + ":" + symbol.line + (symbol.signature ? "\n" + symbol.signature : ""),
477
- aliases: [symbol.path],
653
+ aliases: [],
654
+ path: symbol.path,
655
+ language: symbol.language,
656
+ parser: symbol.parser,
657
+ kind: symbol.kind,
658
+ exported: Boolean(symbol.export),
659
+ line: symbol.line,
660
+ end_line: symbol.end_line,
661
+ signature: symbol.signature,
478
662
  evidence: []
479
663
  });
480
664
  addEdge("file:" + symbol.path, symbol.id, "defines_symbol", symbol.path + " defines " + symbol.kind + " " + symbol.name + ".", "symbols");
@@ -500,7 +684,13 @@
500
684
  graph_kind: "code",
501
685
  name: route.method + " " + route.path,
502
686
  summary: route.framework + " route in " + route.file_path + ":" + route.line,
503
- aliases: [route.file_path],
687
+ aliases: [],
688
+ path: route.file_path,
689
+ method: route.method,
690
+ route_path: route.path,
691
+ framework: route.framework,
692
+ handler_symbol: route.handler_symbol,
693
+ line: route.line,
504
694
  evidence: []
505
695
  });
506
696
  addEdge("file:" + route.file_path, route.id, "defines_route", route.file_path + " defines " + route.method + " " + route.path + ".", "routes");
@@ -538,7 +728,7 @@
538
728
  return edge.relation || "related";
539
729
  })));
540
730
  if (state.edges.some(function (edge) { return edge.memory_code_link; })) {
541
- els.relationFilter.appendChild(new Option("Memory-Code links", "__memory_code__"));
731
+ els.relationFilter.appendChild(new Option("Memory <-> Code only", "__memory_code__"));
542
732
  }
543
733
  }
544
734
 
@@ -680,8 +870,8 @@
680
870
  displayName(state.entityById.get(a)).localeCompare(displayName(state.entityById.get(b)));
681
871
  });
682
872
  entities = options.mode === "combined"
683
- ? balancedSignalEntities(ranked, options.maxNodes)
684
- : new Set(ranked.slice(0, options.maxNodes));
873
+ ? balancedSignalEntities(ranked, edges, options.maxNodes)
874
+ : connectedSignalEntities(ranked, edges, options.maxNodes);
685
875
  if (state.selected && state.selected.kind === "entity") entities.add(state.selected.id);
686
876
  edges = edgesWithVisibleEndpoints(edges, entities);
687
877
  }
@@ -694,10 +884,78 @@
694
884
  }
695
885
  }
696
886
 
697
- return { entities: entities, edges: edgesWithVisibleEndpoints(edges, entities) };
887
+ return { entities: entities, edges: capVisibleEdges(edgesWithVisibleEndpoints(edges, entities), entities, options) };
888
+ }
889
+
890
+ function capVisibleEdges(edgeIds, entityIds, options) {
891
+ var maxEdges = clamp(Math.round((options.maxNodes || 90) * VISIBLE_EDGE_MULTIPLIER), VISIBLE_EDGE_MIN, VISIBLE_EDGE_MAX);
892
+ if (edgeIds.size <= maxEdges) return edgeIds;
893
+
894
+ var sorted = Array.from(edgeIds).sort(function (a, b) {
895
+ return edgeDisplayImportance(state.edgeById.get(b), entityIds) - edgeDisplayImportance(state.edgeById.get(a), entityIds) ||
896
+ String(a).localeCompare(String(b));
897
+ });
898
+ if (options.mode !== "combined" || options.relation || options.query.active) {
899
+ return new Set(sorted.slice(0, maxEdges));
900
+ }
901
+
902
+ var result = new Set();
903
+ var codeEdges = [];
904
+ var memoryCodeEdges = [];
905
+ var memoryEdges = [];
906
+ var otherEdges = [];
907
+ sorted.forEach(function (id) {
908
+ var edge = state.edgeById.get(id);
909
+ var from = edge && state.entityById.get(edge.from);
910
+ var to = edge && state.entityById.get(edge.to);
911
+ if (!edge || !from || !to) return;
912
+ if (isMemoryCodeEdge(edge)) memoryCodeEdges.push(id);
913
+ else if (from.graph_kind === "code" && to.graph_kind === "code") codeEdges.push(id);
914
+ else if (from.graph_kind === "memory" && to.graph_kind === "memory") memoryEdges.push(id);
915
+ else otherEdges.push(id);
916
+ });
917
+
918
+ var codeBudget = Math.round(maxEdges * 0.30);
919
+ var memoryCodeBudget = Math.round(maxEdges * 0.50);
920
+ var memoryBudget = Math.round(maxEdges * 0.14);
921
+ takeEdges(result, codeEdges, codeBudget);
922
+ takeEdges(result, memoryCodeEdges, memoryCodeBudget);
923
+ takeEdges(result, memoryEdges, memoryBudget);
924
+ takeEdges(result, otherEdges, maxEdges - result.size);
925
+ takeEdges(result, sorted, maxEdges - result.size);
926
+ return result;
927
+ }
928
+
929
+ function takeEdges(result, ids, count) {
930
+ var target = result.size + Math.max(0, count);
931
+ ids.forEach(function (id) {
932
+ if (result.size < target) result.add(id);
933
+ });
698
934
  }
699
935
 
700
- function balancedSignalEntities(rankedIds, maxNodes) {
936
+ function edgeDisplayImportance(edge, entityIds) {
937
+ if (!edge) return -1000;
938
+ var from = state.entityById.get(edge.from);
939
+ var to = state.entityById.get(edge.to);
940
+ var score = entityImportance(from) + entityImportance(to);
941
+ var relation = String(edge.relation || "");
942
+ if (state.selected && state.selected.kind === "edge" && state.selected.id === edge.id) score += 10000;
943
+ if (state.selected && state.selected.kind === "entity" && (state.selected.id === edge.from || state.selected.id === edge.to)) score += 1600;
944
+ if (isMemoryCodeEdge(edge)) {
945
+ if (relation === "affects_code_path") score += 520;
946
+ else if (["fixes_symbol", "verified_by_test"].indexOf(relation) !== -1) score += 420;
947
+ else if (["explains_symbol", "informs_symbol", "applies_to_route"].indexOf(relation) !== -1) score += 300;
948
+ else score += 120;
949
+ } else if (["defines_symbol", "calls", "covers", "imports", "defines_route", "handled_by"].indexOf(relation) !== -1) {
950
+ score += 360;
951
+ } else if (["contains_memory", "has_type", "mentions_tag", "verified_by"].indexOf(relation) !== -1) {
952
+ score += 160;
953
+ }
954
+ if (entityIds && entityIds.has(edge.from) && entityIds.has(edge.to)) score += 30;
955
+ return score;
956
+ }
957
+
958
+ function balancedSignalEntities(rankedIds, edgeIds, maxNodes) {
701
959
  var result = new Set();
702
960
  var memoryIds = rankedIds.filter(function (id) {
703
961
  var entity = state.entityById.get(id);
@@ -712,10 +970,21 @@
712
970
  return entity && entity.graph_kind === "memory" && !memoryIds.includes(id);
713
971
  });
714
972
 
715
- var memoryBudget = clamp(Math.round(maxNodes * 0.34), 18, Math.min(44, maxNodes));
973
+ var memoryBudget = clamp(Math.round(maxNodes * 0.30), 14, Math.min(36, maxNodes));
716
974
  memoryIds.slice(0, memoryBudget).forEach(function (id) { result.add(id); });
717
975
  otherMemoryIds.slice(0, Math.max(0, Math.round(memoryBudget * 0.35))).forEach(function (id) { result.add(id); });
718
- codeIds.forEach(function (id) {
976
+
977
+ var bridgeBudget = Math.max(0, Math.round(maxNodes * 0.22));
978
+ memoryCodeConnectionsForIds(result).forEach(function (id) {
979
+ if (result.size < maxNodes && bridgeBudget > 0 && result.size < memoryBudget + bridgeBudget) result.add(id);
980
+ });
981
+
982
+ var codeBudget = Math.max(0, maxNodes - result.size);
983
+ connectedSignalEntities(codeIds, edgeIds, codeBudget).forEach(function (id) {
984
+ if (result.size < maxNodes) result.add(id);
985
+ });
986
+
987
+ memoryCodeConnectionsForIds(result).forEach(function (id) {
719
988
  if (result.size < maxNodes) result.add(id);
720
989
  });
721
990
  rankedIds.forEach(function (id) {
@@ -724,6 +993,65 @@
724
993
  return result;
725
994
  }
726
995
 
996
+ function memoryCodeConnectionsForIds(entityIds) {
997
+ var connected = new Map();
998
+ state.edges.forEach(function (edge) {
999
+ if (!isMemoryCodeEdge(edge)) return;
1000
+ if (entityIds.has(edge.from) && !entityIds.has(edge.to)) {
1001
+ connected.set(edge.to, Math.max(connected.get(edge.to) || 0, memoryCodePeerImportance(edge, edge.to)));
1002
+ }
1003
+ if (entityIds.has(edge.to) && !entityIds.has(edge.from)) {
1004
+ connected.set(edge.from, Math.max(connected.get(edge.from) || 0, memoryCodePeerImportance(edge, edge.from)));
1005
+ }
1006
+ });
1007
+ return Array.from(connected.keys()).sort(function (a, b) {
1008
+ return (connected.get(b) || 0) - (connected.get(a) || 0) ||
1009
+ displayName(state.entityById.get(a)).localeCompare(displayName(state.entityById.get(b)));
1010
+ });
1011
+ }
1012
+
1013
+ function memoryCodePeerImportance(edge, peerId) {
1014
+ var peer = state.entityById.get(peerId);
1015
+ var score = entityImportance(peer);
1016
+ var relation = String(edge.relation || "");
1017
+ if (relation === "affects_code_path") score += 900;
1018
+ if (["fixes_symbol", "verified_by_test"].indexOf(relation) !== -1) score += 520;
1019
+ if (["explains_symbol", "informs_symbol", "applies_to_route"].indexOf(relation) !== -1) score += 360;
1020
+ if (peer && peer.type === "file") score += 180;
1021
+ return score;
1022
+ }
1023
+
1024
+ function connectedSignalEntities(rankedIds, edgeIds, maxNodes) {
1025
+ var result = new Set();
1026
+ var candidates = new Set(rankedIds);
1027
+ var neighbors = new Map();
1028
+ edgeIds.forEach(function (edgeId) {
1029
+ var edge = state.edgeById.get(edgeId);
1030
+ if (!edge || !candidates.has(edge.from) || !candidates.has(edge.to)) return;
1031
+ if (!neighbors.has(edge.from)) neighbors.set(edge.from, []);
1032
+ if (!neighbors.has(edge.to)) neighbors.set(edge.to, []);
1033
+ neighbors.get(edge.from).push(edge.to);
1034
+ neighbors.get(edge.to).push(edge.from);
1035
+ });
1036
+
1037
+ rankedIds.forEach(function (id) {
1038
+ if (result.size >= maxNodes) return;
1039
+ result.add(id);
1040
+ (neighbors.get(id) || [])
1041
+ .sort(function (a, b) {
1042
+ return entityImportance(state.entityById.get(b)) - entityImportance(state.entityById.get(a)) ||
1043
+ displayName(state.entityById.get(a)).localeCompare(displayName(state.entityById.get(b)));
1044
+ })
1045
+ .forEach(function (peer) {
1046
+ if (result.size < maxNodes) result.add(peer);
1047
+ });
1048
+ });
1049
+ rankedIds.forEach(function (id) {
1050
+ if (result.size < maxNodes) result.add(id);
1051
+ });
1052
+ return result;
1053
+ }
1054
+
727
1055
  function focusSelection(entityIds, edgeIds, selection) {
728
1056
  var entities = new Set();
729
1057
  var edges = new Set();
@@ -1352,7 +1680,7 @@
1352
1680
  els.selectionStatus.textContent = state.selected.kind === "entity" ? "Node" : reviewStatus(item);
1353
1681
 
1354
1682
  if (state.selected.kind === "entity") {
1355
- rows.appendChild(detailRow("Summary", item.summary || ""));
1683
+ entityDetailRows(item).forEach(function (row) { rows.appendChild(row); });
1356
1684
  rows.appendChild(detailRow("Graph", item.graph_kind || ""));
1357
1685
  rows.appendChild(detailRow("Aliases", Array.isArray(item.aliases) ? item.aliases.join(", ") : ""));
1358
1686
  rows.appendChild(detailRow("Evidence", formatEvidence(item.evidence)));
@@ -1377,6 +1705,49 @@
1377
1705
  renderInspectorConnections(item);
1378
1706
  }
1379
1707
 
1708
+ function entityDetailRows(entity) {
1709
+ var rows = [detailRow("Summary", entitySummary(entity))];
1710
+ if (entity.graph_kind === "code") {
1711
+ if (entity.path) rows.push(detailRow("Path", entity.path));
1712
+ if (entity.language) rows.push(detailRow("Language", entity.language));
1713
+ if (entity.parser) rows.push(detailRow("Parser", entity.parser));
1714
+ if (entity.kind) rows.push(detailRow("Kind", entity.kind));
1715
+ if (entity.line) rows.push(detailRow("Line", String(entity.line)));
1716
+ if (entity.end_line) rows.push(detailRow("End line", String(entity.end_line)));
1717
+ if (entity.exported != null) rows.push(detailRow("Exported", entity.exported ? "yes" : "no"));
1718
+ if (entity.signature) rows.push(detailRow("Signature", entity.signature));
1719
+ if (entity.line_count) rows.push(detailRow("Lines", String(entity.line_count)));
1720
+ if (entity.size_bytes != null) rows.push(detailRow("Size", formatBytes(entity.size_bytes)));
1721
+ if (entity.hash) rows.push(detailRow("Hash", entity.hash));
1722
+ if (entity.method) rows.push(detailRow("Method", entity.method));
1723
+ if (entity.route_path) rows.push(detailRow("Route", entity.route_path));
1724
+ if (entity.framework) rows.push(detailRow("Framework", entity.framework));
1725
+ if (entity.handler_symbol) rows.push(detailRow("Handler", entity.handler_symbol));
1726
+ }
1727
+ return rows;
1728
+ }
1729
+
1730
+ function entitySummary(entity) {
1731
+ if (entity.summary) return entity.summary;
1732
+ if (entity.graph_kind === "code" && entity.type === "symbol") {
1733
+ return [entity.kind || "symbol", entity.path && "in " + entity.path, entity.line && "line " + entity.line].filter(Boolean).join(" ");
1734
+ }
1735
+ if (entity.graph_kind === "code" && entity.type === "file") {
1736
+ return [entity.kind || "file", entity.language, entity.line_count && entity.line_count + " lines"].filter(Boolean).join(", ");
1737
+ }
1738
+ if (entity.graph_kind === "code" && entity.type === "test") {
1739
+ return ["test", entity.path && "in " + entity.path, entity.line && "line " + entity.line].filter(Boolean).join(" ");
1740
+ }
1741
+ return displayName(entity);
1742
+ }
1743
+
1744
+ function formatBytes(value) {
1745
+ var bytes = Number(value || 0);
1746
+ if (bytes < 1024) return bytes + " B";
1747
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
1748
+ return (bytes / 1024 / 1024).toFixed(1) + " MB";
1749
+ }
1750
+
1380
1751
  function renderInspectorConnections(item) {
1381
1752
  if (state.selected.kind !== "entity") {
1382
1753
  if (item && isMemoryCodeEdge(item)) {
@@ -1387,6 +1758,14 @@
1387
1758
  return;
1388
1759
  }
1389
1760
 
1761
+ var direct = directConnections(item.id);
1762
+ if (direct.length) {
1763
+ var directRows = direct.slice(0, INSPECTOR_CONNECTION_LIMIT).map(function (link) {
1764
+ return connectionText(link.edge, item, link.other);
1765
+ });
1766
+ els.selectionDetails.appendChild(detailSection("Connected Relations", directRows, direct.length - directRows.length));
1767
+ }
1768
+
1390
1769
  var links = memoryCodeConnections(item.id);
1391
1770
  if (!links.length) return;
1392
1771
  var rows = links.slice(0, INSPECTOR_CONNECTION_LIMIT).map(function (link) {
@@ -1409,6 +1788,35 @@
1409
1788
  });
1410
1789
  }
1411
1790
 
1791
+ function directConnections(entityId) {
1792
+ return state.edges
1793
+ .filter(function (edge) { return edge.from === entityId || edge.to === entityId; })
1794
+ .map(function (edge) {
1795
+ var otherId = edge.from === entityId ? edge.to : edge.from;
1796
+ return { edge: edge, other: state.entityById.get(otherId) };
1797
+ })
1798
+ .filter(function (link) { return Boolean(link.other); })
1799
+ .sort(function (a, b) {
1800
+ return connectionImportance(b) - connectionImportance(a) ||
1801
+ displayName(a.other).localeCompare(displayName(b.other));
1802
+ });
1803
+ }
1804
+
1805
+ function splitParamValues(values) {
1806
+ return []
1807
+ .concat(values || [])
1808
+ .flatMap(function (value) { return String(value || "").split(","); })
1809
+ .map(function (value) { return value.trim(); })
1810
+ .filter(Boolean);
1811
+ }
1812
+
1813
+ function applyRequestedView(view) {
1814
+ if (!view) return;
1815
+ var normalized = String(view).toLowerCase();
1816
+ if (["combined", "memory", "code"].indexOf(normalized) === -1) return;
1817
+ els.viewMode.value = normalized;
1818
+ }
1819
+
1412
1820
  function isMemoryCodeEdge(edge) {
1413
1821
  return Boolean(edge && (edge.memory_code_link || isMemoryCodeRelation(edge.relation)));
1414
1822
  }
@@ -1513,11 +1921,11 @@
1513
1921
  var official = state.metrics;
1514
1922
  var metrics = official ? [
1515
1923
  ["Readiness", official.harness.readiness_score + "/100"],
1516
- ["Tokens Saved", official.savings ? official.savings.estimated_tokens_saved_per_recall : "n/a"],
1517
1924
  ["Code Files", official.code_graph.files],
1518
- ["Memory Edges", official.memory_graph.edges],
1519
- ["Quality", official.memory_graph.average_quality_score + "/100"],
1520
- ["Evidence", official.memory_graph.evidence_coverage_percent + "%"]
1925
+ ["Structural Symbols", official.structural_index ? official.structural_index.symbols : official.code_graph.symbols],
1926
+ ["Parser Coverage", official.code_graph.indexer_coverage_percent + "%"],
1927
+ ["Cache Hits", official.structural_index ? official.structural_index.cache_hits : official.code_graph.cache_hits],
1928
+ ["Tokens Saved", official.savings ? official.savings.estimated_tokens_saved_per_recall : "n/a"]
1521
1929
  ] : [
1522
1930
  ["Nodes", visibleEntities.length + "/" + state.entities.length],
1523
1931
  ["Relations", visibleEdges.length + "/" + state.edges.length],
@@ -1655,9 +2063,10 @@
1655
2063
  var pills = official ? [
1656
2064
  ["Readiness", official.harness.readiness_score + "/100", ""],
1657
2065
  ["Pending", official.memory_graph ? String(official.memory_graph.pending_packets) : "n/a", official.memory_graph && official.memory_graph.pending_packets ? "warn" : ""],
1658
- ["Tokens saved", official.savings ? String(official.savings.estimated_tokens_saved_per_recall) : "n/a", "warn"],
1659
- ["Quality", official.memory_graph.average_quality_score + "/100", "memory"],
1660
- ["Parser coverage", official.code_graph.indexer_coverage_percent + "%", "code"]
2066
+ ["Structural", official.structural_index ? official.structural_index.files + " files" : official.code_graph.files + " files", "code"],
2067
+ ["Code symbols", String(official.code_graph.symbols), "code"],
2068
+ ["Parser coverage", official.code_graph.indexer_coverage_percent + "%", "code"],
2069
+ ["Memory packets", official.memory_graph ? String(official.memory_graph.approved_packets) : "n/a", "memory"]
1661
2070
  ] : [
1662
2071
  ["Memory", String(memoryCount), "memory"],
1663
2072
  ["Code", String(codeCount), "code"],
@@ -2172,6 +2581,101 @@
2172
2581
  return String(value || "unknown").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
2173
2582
  }
2174
2583
 
2584
+ function testSignalVisibilityForGraph(graph, maxNodes) {
2585
+ var normalized = normalizeGraph(graph);
2586
+ return testVisibilityForNormalized(normalized, "code", maxNodes || 90);
2587
+ }
2588
+
2589
+ function testCombinedVisibilityForGraphs(graphs, maxNodes) {
2590
+ var normalized = mergeNormalizedGraphs(graphs.map(function (graph) { return normalizeGraph(graph); }));
2591
+ return testVisibilityForNormalized(normalized, "combined", maxNodes || 90);
2592
+ }
2593
+
2594
+ function testRelationVisibilityForGraphs(graphs, relation, maxNodes) {
2595
+ var normalized = mergeNormalizedGraphs(graphs.map(function (graph) { return normalizeGraph(graph); }));
2596
+ state.entities = normalized.entities;
2597
+ state.edges = normalized.edges;
2598
+ state.entityById = new Map(state.entities.map(function (entity) { return [entity.id, entity]; }));
2599
+ state.edgeById = new Map(state.edges.map(function (edge) { return [edge.id, edge]; }));
2600
+ state.degreeById = buildDegreeMap(state.edges);
2601
+ var edgeIds = new Set(state.edges.filter(function (edge) {
2602
+ return relation === "__memory_code__" ? isMemoryCodeEdge(edge) : edge.relation === relation;
2603
+ }).map(function (edge) { return edge.id; }));
2604
+ var entityIds = new Set();
2605
+ edgeIds.forEach(function (id) {
2606
+ var edge = state.edgeById.get(id);
2607
+ if (!edge) return;
2608
+ entityIds.add(edge.from);
2609
+ entityIds.add(edge.to);
2610
+ });
2611
+ var ranked = Array.from(entityIds).sort(function (a, b) {
2612
+ return entityImportance(state.entityById.get(b)) - entityImportance(state.entityById.get(a)) ||
2613
+ displayName(state.entityById.get(a)).localeCompare(displayName(state.entityById.get(b)));
2614
+ });
2615
+ var entities = connectedSignalEntities(ranked, edgeIds, maxNodes || 90);
2616
+ var edges = capVisibleEdges(edgesWithVisibleEndpoints(edgeIds, entities), entities, {
2617
+ maxNodes: maxNodes || 90,
2618
+ mode: "combined",
2619
+ relation: relation,
2620
+ query: { active: false }
2621
+ });
2622
+ return visibilityStats(entities, edges);
2623
+ }
2624
+
2625
+ function testVisibilityForNormalized(normalized, mode, maxNodes) {
2626
+ state.entities = normalized.entities;
2627
+ state.edges = normalized.edges;
2628
+ state.entityById = new Map(state.entities.map(function (entity) { return [entity.id, entity]; }));
2629
+ state.edgeById = new Map(state.edges.map(function (edge) { return [edge.id, edge]; }));
2630
+ state.degreeById = buildDegreeMap(state.edges);
2631
+ var ranked = state.entities
2632
+ .filter(function (entity) { return mode === "combined" || entity.graph_kind === mode; })
2633
+ .map(function (entity) { return entity.id; })
2634
+ .sort(function (a, b) {
2635
+ return entityImportance(state.entityById.get(b)) - entityImportance(state.entityById.get(a)) ||
2636
+ displayName(state.entityById.get(a)).localeCompare(displayName(state.entityById.get(b)));
2637
+ });
2638
+ var edgeIds = new Set(state.edges.filter(function (edge) { return mode === "combined" || edge.graph_kind === mode; }).map(function (edge) { return edge.id; }));
2639
+ var entities = mode === "combined"
2640
+ ? balancedSignalEntities(ranked, edgeIds, maxNodes)
2641
+ : connectedSignalEntities(ranked, edgeIds, maxNodes);
2642
+ var edges = capVisibleEdges(edgesWithVisibleEndpoints(edgeIds, entities), entities, {
2643
+ maxNodes: maxNodes,
2644
+ mode: mode,
2645
+ relation: "",
2646
+ query: { active: false }
2647
+ });
2648
+ return visibilityStats(entities, edges);
2649
+ }
2650
+
2651
+ function visibilityStats(entities, edges) {
2652
+ return {
2653
+ entities: entities.size,
2654
+ edges: edges.size,
2655
+ memoryCodeEdges: Array.from(edges).filter(function (id) {
2656
+ return isMemoryCodeEdge(state.edgeById.get(id));
2657
+ }).length,
2658
+ pathBridgeEdges: Array.from(edges).filter(function (id) {
2659
+ var edge = state.edgeById.get(id);
2660
+ return edge && edge.relation === "affects_code_path";
2661
+ }).length,
2662
+ codeEdges: Array.from(edges).filter(function (id) {
2663
+ var edge = state.edgeById.get(id);
2664
+ var from = edge && state.entityById.get(edge.from);
2665
+ var to = edge && state.entityById.get(edge.to);
2666
+ return from && to && from.graph_kind === "code" && to.graph_kind === "code";
2667
+ }).length,
2668
+ memory: Array.from(entities).filter(function (id) {
2669
+ var entity = state.entityById.get(id);
2670
+ return entity && entity.graph_kind === "memory";
2671
+ }).length,
2672
+ code: Array.from(entities).filter(function (id) {
2673
+ var entity = state.entityById.get(id);
2674
+ return entity && entity.graph_kind === "code";
2675
+ }).length
2676
+ };
2677
+ }
2678
+
2175
2679
  function svgEl(name, attrs) {
2176
2680
  var element = document.createElementNS("http://www.w3.org/2000/svg", name);
2177
2681
  Object.keys(attrs || {}).forEach(function (key) {
@@ -2184,6 +2688,11 @@
2184
2688
  globalThis.__KAGE_VIEWER_TEST__ = {
2185
2689
  normalizeGraph: normalizeGraph,
2186
2690
  normalizeCodeGraph: normalizeCodeGraph,
2691
+ hydrateCompactCodeGraph: hydrateCompactCodeGraph,
2692
+ connectedSignalEntities: connectedSignalEntities,
2693
+ testSignalVisibilityForGraph: testSignalVisibilityForGraph,
2694
+ testCombinedVisibilityForGraphs: testCombinedVisibilityForGraphs,
2695
+ testRelationVisibilityForGraphs: testRelationVisibilityForGraphs,
2187
2696
  mergeNormalizedGraphs: mergeNormalizedGraphs,
2188
2697
  isMemoryCodeRelation: isMemoryCodeRelation
2189
2698
  };