@kage-core/kage-graph-mcp 1.1.0 → 1.1.2

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
@@ -15,26 +15,57 @@
15
15
  pendingPackets: [],
16
16
  reviewText: "",
17
17
  viewBox: { x: 0, y: 0, width: 1000, height: 660 },
18
+ lastVisibleSignature: "",
19
+ sim: {
20
+ nodes: [],
21
+ edges: [],
22
+ nodeById: new Map(),
23
+ panX: 0,
24
+ panY: 0,
25
+ zoom: 1,
26
+ dragNode: null,
27
+ panning: null,
28
+ hoverNode: null,
29
+ raf: null,
30
+ running: false,
31
+ lastSignature: ""
32
+ },
18
33
  pan: null
19
34
  };
20
35
 
21
36
  var palette = {
22
37
  repo: "#41ff8f",
23
- memory: "#b88cff",
38
+ memory: "#41ff8f",
24
39
  path: "#ff6b6b",
25
40
  tag: "#ffd166",
26
41
  package: "#6ad7ff",
27
42
  command: "#9be7c0",
28
43
  memory_type: "#41ff8f",
29
44
  file: "#6ad7ff",
30
- symbol: "#b88cff",
31
- route: "#ff8fab",
45
+ symbol: "#9be7c0",
46
+ route: "#6ad7ff",
32
47
  test: "#ffd166",
33
- external: "#93a4a0",
48
+ external: "#62776b",
34
49
  script: "#6ad7ff",
35
50
  default: "#9be7c0"
36
51
  };
37
52
 
53
+ var graphPalette = {
54
+ background: "#020503",
55
+ grid: "rgba(65,255,143,0.040)",
56
+ gridStrong: "rgba(65,255,143,0.070)",
57
+ text: "#d7f9df",
58
+ muted: "#6ea77d",
59
+ memory: "#41ff8f",
60
+ code: "#6ad7ff",
61
+ amber: "#ffd166",
62
+ danger: "#ff6b6b",
63
+ dependency: "#62776b",
64
+ body: "rgba(4,12,8,0.88)",
65
+ bodyCode: "rgba(5,16,18,0.88)",
66
+ bodyMemory: "rgba(5,18,10,0.90)"
67
+ };
68
+
38
69
  var els = {
39
70
  graphFile: document.getElementById("graphFile"),
40
71
  graphSummary: document.getElementById("graphSummary"),
@@ -47,10 +78,15 @@
47
78
  viewMode: document.getElementById("viewMode"),
48
79
  typeFilter: document.getElementById("typeFilter"),
49
80
  relationFilter: document.getElementById("relationFilter"),
81
+ scopeFilter: document.getElementById("scopeFilter"),
82
+ maxNodes: document.getElementById("maxNodes"),
83
+ showDependencies: document.getElementById("showDependencies"),
50
84
  resetView: document.getElementById("resetView"),
51
85
  zoomOut: document.getElementById("zoomOut"),
52
86
  zoomIn: document.getElementById("zoomIn"),
53
87
  fitView: document.getElementById("fitView"),
88
+ canvas: document.getElementById("graphCanvas"),
89
+ tooltip: document.getElementById("graphTooltip"),
54
90
  svg: document.getElementById("graphSvg"),
55
91
  nodeLayer: document.getElementById("nodeLayer"),
56
92
  edgeLayer: document.getElementById("edgeLayer"),
@@ -72,21 +108,38 @@
72
108
  els.viewMode.addEventListener("change", render);
73
109
  els.typeFilter.addEventListener("change", render);
74
110
  els.relationFilter.addEventListener("change", render);
75
- els.zoomOut.addEventListener("click", function () { zoomView(1.18); });
76
- els.zoomIn.addEventListener("click", function () { zoomView(0.84); });
77
- els.fitView.addEventListener("click", function () { fitView(); renderSvg(); });
111
+ els.scopeFilter.addEventListener("change", render);
112
+ els.maxNodes.addEventListener("change", render);
113
+ els.showDependencies.addEventListener("change", render);
114
+ els.zoomOut.addEventListener("click", function () { zoomCanvas(0.82); });
115
+ els.zoomIn.addEventListener("click", function () { zoomCanvas(1.22); });
116
+ els.fitView.addEventListener("click", function () { fitCanvas(); drawCanvasGraph(); });
117
+ els.canvas.addEventListener("mousedown", startCanvasPointer);
118
+ els.canvas.addEventListener("mousemove", moveCanvasPointer);
119
+ els.canvas.addEventListener("mouseup", endCanvasPointer);
120
+ els.canvas.addEventListener("mouseleave", leaveCanvasPointer);
121
+ els.canvas.addEventListener("wheel", handleCanvasWheel, { passive: false });
122
+ els.canvas.addEventListener("dblclick", handleCanvasDoubleClick);
78
123
  els.svg.addEventListener("mousedown", startPan);
79
124
  els.svg.addEventListener("click", handleSvgClick);
80
125
  els.svg.addEventListener("wheel", handleWheelZoom, { passive: false });
81
126
  window.addEventListener("mousemove", continuePan);
82
127
  window.addEventListener("mouseup", endPan);
128
+ window.addEventListener("resize", function () {
129
+ resizeCanvas();
130
+ fitCanvas();
131
+ drawCanvasGraph();
132
+ });
83
133
  els.resetView.addEventListener("click", function () {
84
134
  els.searchInput.value = "";
85
135
  els.viewMode.value = "combined";
86
136
  els.typeFilter.value = "";
87
137
  els.relationFilter.value = "";
138
+ els.scopeFilter.value = "signal";
139
+ els.maxNodes.value = "90";
140
+ els.showDependencies.checked = false;
88
141
  state.selected = null;
89
- fitView();
142
+ state.lastVisibleSignature = "";
90
143
  render();
91
144
  });
92
145
  loadFromUrlParams();
@@ -134,10 +187,9 @@
134
187
  return [episode.id, episode];
135
188
  }));
136
189
  state.selected = null;
190
+ state.lastVisibleSignature = "";
137
191
 
138
192
  populateFilters();
139
- layoutGraph();
140
- fitView();
141
193
  els.emptyState.classList.add("hidden");
142
194
  els.graphSummary.textContent = fileName + " loaded: " + entities.length + " nodes, " + edges.length + " relations.";
143
195
  render();
@@ -381,27 +433,32 @@
381
433
  });
382
434
  }
383
435
 
384
- function layoutGraph() {
436
+ function layoutGraph(visibleIds) {
385
437
  state.positions = new Map();
438
+ var candidates = state.entities.filter(function (entity) {
439
+ return !visibleIds || visibleIds.has(entity.id);
440
+ });
386
441
  var lanes = [
387
- { name: "memory", x: 190, y: 86, step: 62, match: function (entity) { return entity.graph_kind === "memory"; } },
388
- { name: "files", x: 480, y: 78, step: 60, match: function (entity) { return entity.graph_kind === "code" && entity.type === "file"; } },
389
- { name: "flow", x: 700, y: 88, step: 58, match: function (entity) { return entity.graph_kind === "code" && ["symbol", "route", "test"].indexOf(entity.type) !== -1; } },
390
- { name: "external", x: 895, y: 110, step: 64, match: function (entity) { return entity.graph_kind === "code" && ["external", "script"].indexOf(entity.type) !== -1; } },
391
- { name: "other", x: 700, y: 570, step: 58, match: function (entity) { return ["memory", "code"].indexOf(entity.graph_kind) === -1 || (entity.graph_kind === "code" && ["file", "symbol", "route", "test", "external", "script"].indexOf(entity.type) === -1); } }
442
+ { name: "memory", x: 170, y: 96, step: 82, columns: 10, match: function (entity) { return entity.graph_kind === "memory"; } },
443
+ { name: "files", x: 470, y: 86, step: 78, columns: 10, match: function (entity) { return entity.graph_kind === "code" && entity.type === "file"; } },
444
+ { name: "flow", x: 760, y: 96, step: 76, columns: 10, match: function (entity) { return entity.graph_kind === "code" && ["symbol", "route", "test"].indexOf(entity.type) !== -1; } },
445
+ { name: "external", x: 1040, y: 122, step: 78, columns: 8, match: function (entity) { return isDependencyEntity(entity) || (entity.graph_kind === "code" && ["external", "script"].indexOf(entity.type) !== -1); } },
446
+ { name: "other", x: 760, y: 560, step: 78, columns: 8, match: function (entity) { return ["memory", "code"].indexOf(entity.graph_kind) === -1 || (entity.graph_kind === "code" && ["file", "symbol", "route", "test", "external", "script"].indexOf(entity.type) === -1); } }
392
447
  ];
448
+ var placed = new Set();
393
449
  lanes.forEach(function (lane) {
394
- var bucket = state.entities.filter(function (entity) {
395
- return lane.match(entity);
450
+ var bucket = candidates.filter(function (entity) {
451
+ return !placed.has(entity.id) && lane.match(entity);
396
452
  }).sort(function (a, b) {
397
- return degreeOf(b.id) - degreeOf(a.id) || displayName(a).localeCompare(displayName(b));
453
+ return entityImportance(b) - entityImportance(a) || displayName(a).localeCompare(displayName(b));
398
454
  });
399
455
  if (!bucket.length) return;
400
456
  bucket.forEach(function (entity, index) {
401
- var column = Math.floor(index / 8);
402
- var row = index % 8;
403
- var xOffset = column * 168;
404
- var yJitter = column % 2 ? 18 : 0;
457
+ placed.add(entity.id);
458
+ var column = Math.floor(index / lane.columns);
459
+ var row = index % lane.columns;
460
+ var xOffset = column * 228;
461
+ var yJitter = column % 2 ? 26 : 0;
405
462
  state.positions.set(entity.id, {
406
463
  x: lane.x + xOffset,
407
464
  y: lane.y + row * lane.step + yJitter
@@ -413,7 +470,7 @@
413
470
  function render() {
414
471
  if (!state.graph) return;
415
472
 
416
- var query = normalize(els.searchInput.value);
473
+ var query = parseSearchQuery(els.searchInput.value);
417
474
  var mode = els.viewMode.value;
418
475
  var type = els.typeFilter.value;
419
476
  var relation = els.relationFilter.value;
@@ -423,7 +480,7 @@
423
480
  state.entities.forEach(function (entity) {
424
481
  if (mode !== "combined" && entity.graph_kind !== mode) return;
425
482
  var passesType = !type || entity.type === type;
426
- var passesSearch = !query || searchableText(entity).indexOf(query) !== -1;
483
+ var passesSearch = matchesSearchQuery(entity, query);
427
484
  if (passesType && passesSearch) matchedEntityIds.add(entity.id);
428
485
  });
429
486
 
@@ -431,7 +488,7 @@
431
488
  if (mode !== "combined" && edge.graph_kind !== mode) return;
432
489
  var fromMatched = matchedEntityIds.has(edge.from);
433
490
  var toMatched = matchedEntityIds.has(edge.to);
434
- var edgeMatchesSearch = !query || searchableText(edge).indexOf(query) !== -1;
491
+ var edgeMatchesSearch = matchesSearchQuery(edge, query);
435
492
  var passesRelation = !relation || edge.relation === relation;
436
493
  if (passesRelation && (edgeMatchesSearch || fromMatched || toMatched)) {
437
494
  matchedEdgeIds.add(edge.id);
@@ -442,15 +499,28 @@
442
499
  }
443
500
  });
444
501
 
445
- if (!query && !type && !relation) {
502
+ if (!query.active && !type && !relation) {
446
503
  matchedEntityIds = new Set(state.entities.filter(function (entity) { return mode === "combined" || entity.graph_kind === mode; }).map(function (entity) { return entity.id; }));
447
504
  matchedEdgeIds = new Set(state.edges.filter(function (edge) { return mode === "combined" || edge.graph_kind === mode; }).map(function (edge) { return edge.id; }));
448
505
  }
449
506
 
450
- state.visibleEntityIds = matchedEntityIds;
451
- state.visibleEdgeIds = matchedEdgeIds;
507
+ var visible = refineVisibleGraph(matchedEntityIds, matchedEdgeIds, {
508
+ query: query,
509
+ type: type,
510
+ relation: relation,
511
+ scope: els.scopeFilter.value,
512
+ maxNodes: Number(els.maxNodes.value || 90),
513
+ showDependencies: els.showDependencies.checked
514
+ });
452
515
 
453
- renderSvg();
516
+ state.visibleEntityIds = visible.entities;
517
+ state.visibleEdgeIds = visible.edges;
518
+ layoutGraph(state.visibleEntityIds);
519
+ var nextSignature = visibleSignature(state.visibleEntityIds, state.visibleEdgeIds);
520
+ var graphChanged = nextSignature !== state.lastVisibleSignature;
521
+ state.lastVisibleSignature = nextSignature;
522
+
523
+ renderCanvasGraph(graphChanged);
454
524
  renderLists();
455
525
  renderDetails();
456
526
  renderMetrics();
@@ -458,6 +528,457 @@
458
528
  renderProof();
459
529
  }
460
530
 
531
+ function refineVisibleGraph(entityIds, edgeIds, options) {
532
+ var entities = new Set(entityIds);
533
+ var edges = new Set(edgeIds);
534
+
535
+ if (!options.showDependencies) {
536
+ Array.from(entities).forEach(function (id) {
537
+ var entity = state.entityById.get(id);
538
+ if (!entity) return;
539
+ if (isDependencyEntity(entity) && !matchesSearchQuery(entity, options.query)) {
540
+ entities.delete(id);
541
+ }
542
+ });
543
+ edges = edgesWithVisibleEndpoints(edges, entities);
544
+ }
545
+
546
+ if (options.scope === "focus" && state.selected) {
547
+ var focused = focusSelection(entities, edges, state.selected);
548
+ entities = focused.entities;
549
+ edges = focused.edges;
550
+ } else if (options.scope === "signal" && entities.size > options.maxNodes) {
551
+ var ranked = Array.from(entities).sort(function (a, b) {
552
+ return entityImportance(state.entityById.get(b)) - entityImportance(state.entityById.get(a)) ||
553
+ displayName(state.entityById.get(a)).localeCompare(displayName(state.entityById.get(b)));
554
+ });
555
+ entities = new Set(ranked.slice(0, options.maxNodes));
556
+ if (state.selected && state.selected.kind === "entity") entities.add(state.selected.id);
557
+ edges = edgesWithVisibleEndpoints(edges, entities);
558
+ }
559
+
560
+ if (state.selected && state.selected.kind === "edge" && edges.has(state.selected.id)) {
561
+ var selectedEdge = state.edges.find(function (edge) { return edge.id === state.selected.id; });
562
+ if (selectedEdge) {
563
+ entities.add(selectedEdge.from);
564
+ entities.add(selectedEdge.to);
565
+ }
566
+ }
567
+
568
+ return { entities: entities, edges: edgesWithVisibleEndpoints(edges, entities) };
569
+ }
570
+
571
+ function focusSelection(entityIds, edgeIds, selection) {
572
+ var entities = new Set();
573
+ var edges = new Set();
574
+ if (selection.kind === "entity") {
575
+ entities.add(selection.id);
576
+ state.edges.forEach(function (edge) {
577
+ if (!edgeIds.has(edge.id)) return;
578
+ if (edge.from === selection.id || edge.to === selection.id) {
579
+ edges.add(edge.id);
580
+ entities.add(edge.from);
581
+ entities.add(edge.to);
582
+ }
583
+ });
584
+ } else {
585
+ var selectedEdge = state.edges.find(function (edge) { return edge.id === selection.id; });
586
+ if (selectedEdge && edgeIds.has(selectedEdge.id)) {
587
+ edges.add(selectedEdge.id);
588
+ entities.add(selectedEdge.from);
589
+ entities.add(selectedEdge.to);
590
+ }
591
+ }
592
+ entityIds.forEach(function (id) {
593
+ if (entities.has(id)) return;
594
+ var entity = state.entityById.get(id);
595
+ if (entity && entity.graph_kind === "memory" && entities.size < 16) entities.add(id);
596
+ });
597
+ return { entities: entities.size ? entities : entityIds, edges: edges.size ? edges : edgeIds };
598
+ }
599
+
600
+ function edgesWithVisibleEndpoints(edgeIds, entityIds) {
601
+ return new Set(Array.from(edgeIds).filter(function (id) {
602
+ var edge = state.edges.find(function (candidate) { return candidate.id === id; });
603
+ return edge && entityIds.has(edge.from) && entityIds.has(edge.to);
604
+ }));
605
+ }
606
+
607
+ function renderCanvasGraph(graphChanged) {
608
+ resizeCanvas();
609
+ syncSimulationGraph(graphChanged);
610
+ if (graphChanged) fitCanvas();
611
+ startSimulation();
612
+ drawCanvasGraph();
613
+ }
614
+
615
+ function resizeCanvas() {
616
+ var canvas = els.canvas;
617
+ var ctx = canvas.getContext("2d");
618
+ var rect = canvas.parentElement.getBoundingClientRect();
619
+ var dpr = Math.max(1, window.devicePixelRatio || 1);
620
+ var width = Math.max(320, rect.width);
621
+ var height = Math.max(360, rect.height);
622
+ if (canvas.width !== Math.round(width * dpr) || canvas.height !== Math.round(height * dpr)) {
623
+ canvas.width = Math.round(width * dpr);
624
+ canvas.height = Math.round(height * dpr);
625
+ canvas.style.width = width + "px";
626
+ canvas.style.height = height + "px";
627
+ }
628
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
629
+ }
630
+
631
+ function syncSimulationGraph(forceReset) {
632
+ var existing = state.sim.nodeById;
633
+ var visibleEntities = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
634
+ var nextNodes = visibleEntities.map(function (entity, index) {
635
+ var current = existing.get(entity.id);
636
+ var lanePos = state.positions.get(entity.id);
637
+ var seed = seededPosition(index, visibleEntities.length);
638
+ var degree = degreeOf(entity.id);
639
+ if (!current || forceReset) {
640
+ current = {
641
+ id: entity.id,
642
+ entity: entity,
643
+ x: lanePos ? lanePos.x : seed.x,
644
+ y: lanePos ? lanePos.y : seed.y,
645
+ vx: 0,
646
+ vy: 0,
647
+ r: clamp(9 + degree * 1.7, 10, entity.graph_kind === "memory" ? 25 : 22)
648
+ };
649
+ }
650
+ current.entity = entity;
651
+ current.r = clamp(9 + degree * 1.7, 10, entity.graph_kind === "memory" ? 25 : 22);
652
+ return current;
653
+ });
654
+ state.sim.nodes = nextNodes;
655
+ state.sim.nodeById = new Map(nextNodes.map(function (node) { return [node.id, node]; }));
656
+ state.sim.edges = state.edges.filter(function (edge) {
657
+ return state.visibleEdgeIds.has(edge.id) && state.sim.nodeById.has(edge.from) && state.sim.nodeById.has(edge.to);
658
+ });
659
+ }
660
+
661
+ function seededPosition(index, total) {
662
+ var angle = (Math.PI * 2 * index) / Math.max(1, total);
663
+ var radius = 220 + Math.min(180, total * 2);
664
+ return {
665
+ x: Math.cos(angle) * radius + 520,
666
+ y: Math.sin(angle) * radius + 340
667
+ };
668
+ }
669
+
670
+ function startSimulation() {
671
+ if (state.sim.running) return;
672
+ state.sim.running = true;
673
+ state.sim.raf = window.requestAnimationFrame(simulationTick);
674
+ }
675
+
676
+ function simulationTick() {
677
+ if (!state.sim.running) return;
678
+ stepSimulation();
679
+ drawCanvasGraph();
680
+ state.sim.raf = window.requestAnimationFrame(simulationTick);
681
+ }
682
+
683
+ function stepSimulation() {
684
+ var nodes = state.sim.nodes;
685
+ if (!nodes.length) return;
686
+ var nodeMap = state.sim.nodeById;
687
+ var count = nodes.length;
688
+ var repulsion = count > 120 ? 3600 : count > 70 ? 2500 : 1700;
689
+ var attraction = count > 100 ? 0.006 : 0.010;
690
+ var centerGravity = count > 100 ? 0.006 : 0.011;
691
+ var laneGravity = 0.012;
692
+
693
+ nodes.forEach(function (node, index) {
694
+ if (state.sim.dragNode === node) return;
695
+ var fx = 0;
696
+ var fy = 0;
697
+ for (var j = 0; j < nodes.length; j++) {
698
+ if (index === j) continue;
699
+ var other = nodes[j];
700
+ var dx = node.x - other.x;
701
+ var dy = node.y - other.y;
702
+ var dist = Math.max(18, Math.sqrt(dx * dx + dy * dy));
703
+ var force = repulsion / (dist * dist);
704
+ fx += (dx / dist) * force;
705
+ fy += (dy / dist) * force;
706
+ }
707
+ var lane = state.positions.get(node.id);
708
+ if (lane) {
709
+ fx += (lane.x - node.x) * laneGravity;
710
+ fy += (lane.y - node.y) * laneGravity;
711
+ }
712
+ fx += (620 - node.x) * centerGravity * 0.15;
713
+ fy += (360 - node.y) * centerGravity * 0.15;
714
+ node.vx = (node.vx + fx) * 0.82;
715
+ node.vy = (node.vy + fy) * 0.82;
716
+ });
717
+
718
+ state.sim.edges.forEach(function (edge) {
719
+ var from = nodeMap.get(edge.from);
720
+ var to = nodeMap.get(edge.to);
721
+ if (!from || !to) return;
722
+ var dx = to.x - from.x;
723
+ var dy = to.y - from.y;
724
+ var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
725
+ var target = edge.graph_kind === "memory" ? 145 : 120;
726
+ var force = (dist - target) * attraction * (edge.confidence == null ? 1 : clamp(edge.confidence, 0.35, 1.2));
727
+ var fx = (dx / dist) * force;
728
+ var fy = (dy / dist) * force;
729
+ if (state.sim.dragNode !== from) {
730
+ from.vx += fx;
731
+ from.vy += fy;
732
+ }
733
+ if (state.sim.dragNode !== to) {
734
+ to.vx -= fx;
735
+ to.vy -= fy;
736
+ }
737
+ });
738
+
739
+ nodes.forEach(function (node) {
740
+ if (state.sim.dragNode === node) return;
741
+ node.x += clamp(node.vx, -8, 8);
742
+ node.y += clamp(node.vy, -8, 8);
743
+ });
744
+ }
745
+
746
+ function drawCanvasGraph() {
747
+ var canvas = els.canvas;
748
+ var ctx = canvas.getContext("2d");
749
+ var width = canvas.width / Math.max(1, window.devicePixelRatio || 1);
750
+ var height = canvas.height / Math.max(1, window.devicePixelRatio || 1);
751
+ ctx.clearRect(0, 0, width, height);
752
+ drawCanvasGrid(ctx, width, height);
753
+ ctx.save();
754
+ ctx.translate(state.sim.panX, state.sim.panY);
755
+ ctx.scale(state.sim.zoom, state.sim.zoom);
756
+ drawCanvasEdges(ctx);
757
+ drawCanvasNodes(ctx);
758
+ ctx.restore();
759
+
760
+ if (!state.sim.nodes.length) {
761
+ ctx.fillStyle = "#6ea77d";
762
+ ctx.font = "13px ui-monospace, Menlo, monospace";
763
+ ctx.textAlign = "center";
764
+ ctx.fillText("No graph data visible.", width / 2, height / 2);
765
+ }
766
+ }
767
+
768
+ function drawCanvasGrid(ctx, width, height) {
769
+ ctx.save();
770
+ ctx.fillStyle = graphPalette.background;
771
+ ctx.fillRect(0, 0, width, height);
772
+ var gradient = ctx.createRadialGradient(width * 0.52, height * 0.44, 40, width * 0.52, height * 0.44, Math.max(width, height) * 0.72);
773
+ gradient.addColorStop(0, "rgba(65,255,143,0.080)");
774
+ gradient.addColorStop(0.48, "rgba(65,255,143,0.018)");
775
+ gradient.addColorStop(1, "rgba(2,5,3,0)");
776
+ ctx.fillStyle = gradient;
777
+ ctx.fillRect(0, 0, width, height);
778
+ ctx.strokeStyle = graphPalette.grid;
779
+ ctx.lineWidth = 1;
780
+ var grid = 28;
781
+ for (var x = 0; x < width; x += grid) {
782
+ ctx.beginPath();
783
+ ctx.moveTo(x, 0);
784
+ ctx.lineTo(x, height);
785
+ ctx.stroke();
786
+ }
787
+ for (var y = 0; y < height; y += grid) {
788
+ ctx.beginPath();
789
+ ctx.moveTo(0, y);
790
+ ctx.lineTo(width, y);
791
+ ctx.stroke();
792
+ }
793
+ ctx.strokeStyle = graphPalette.gridStrong;
794
+ ctx.beginPath();
795
+ ctx.moveTo(0, height / 2);
796
+ ctx.lineTo(width, height / 2);
797
+ ctx.moveTo(width / 2, 0);
798
+ ctx.lineTo(width / 2, height);
799
+ ctx.stroke();
800
+ ctx.restore();
801
+ }
802
+
803
+ function drawCanvasEdges(ctx) {
804
+ var nodeMap = state.sim.nodeById;
805
+ var focusId = focusedCanvasNodeId();
806
+ var query = parseSearchQuery(els.searchInput.value);
807
+ var dense = state.sim.nodes.length > 55;
808
+ state.sim.edges.forEach(function (edge) {
809
+ var from = nodeMap.get(edge.from);
810
+ var to = nodeMap.get(edge.to);
811
+ if (!from || !to) return;
812
+ var connected = focusId && (edge.from === focusId || edge.to === focusId);
813
+ var matches = matchesSearchQuery(edge, query) || matchesSearchQuery(from.entity, query) || matchesSearchQuery(to.entity, query);
814
+ var alpha = !matches ? 0.035 : focusId ? (connected ? 0.62 : 0.055) : (dense ? 0.13 : 0.22);
815
+ var color = hexToRgb(edgeThemeColor(edge, from.entity, to.entity));
816
+ var dx = to.x - from.x;
817
+ var dy = to.y - from.y;
818
+ var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
819
+ var offset = dense ? 10 : 16;
820
+ var cx = (from.x + to.x) / 2 + (-dy / dist) * offset;
821
+ var cy = (from.y + to.y) / 2 + (dx / dist) * offset;
822
+ ctx.beginPath();
823
+ ctx.moveTo(from.x, from.y);
824
+ ctx.quadraticCurveTo(cx, cy, to.x, to.y);
825
+ ctx.strokeStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + alpha + ")";
826
+ ctx.lineWidth = connected ? 2.2 : 1;
827
+ ctx.stroke();
828
+ if (connected || (!dense && state.sim.zoom > 1.25)) drawArrow(ctx, from, to, cx, cy, color, alpha);
829
+ if (connected && state.sim.zoom > 0.62) drawEdgeLabel(ctx, edge, cx, cy);
830
+ });
831
+ }
832
+
833
+ function drawCanvasNodes(ctx) {
834
+ var focusId = focusedCanvasNodeId();
835
+ var query = parseSearchQuery(els.searchInput.value);
836
+ var dense = state.sim.nodes.length > 55;
837
+ state.sim.nodes.forEach(function (node) {
838
+ var entity = node.entity;
839
+ var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
840
+ var hovered = state.sim.hoverNode && state.sim.hoverNode.id === node.id;
841
+ var connected = focusId && (node.id === focusId || state.sim.edges.some(function (edge) {
842
+ return (edge.from === focusId && edge.to === node.id) || (edge.to === focusId && edge.from === node.id);
843
+ }));
844
+ var matches = matchesSearchQuery(entity, query);
845
+ var alpha = !matches ? 0.12 : focusId && !connected ? 0.20 : 1;
846
+ var color = nodeThemeColor(entity);
847
+ ctx.save();
848
+ ctx.globalAlpha = alpha;
849
+ if (selected || hovered) {
850
+ ctx.shadowColor = color;
851
+ ctx.shadowBlur = selected ? 14 : 10;
852
+ }
853
+ drawNodeShape(ctx, node.x, node.y, node.r, entity);
854
+ ctx.fillStyle = nodeFillColor(entity);
855
+ ctx.fill();
856
+ ctx.strokeStyle = color;
857
+ ctx.lineWidth = selected || hovered ? 2.2 : 1.2;
858
+ ctx.stroke();
859
+ if (entity.graph_kind === "memory") {
860
+ ctx.fillStyle = color;
861
+ ctx.globalAlpha = alpha * 0.85;
862
+ ctx.beginPath();
863
+ ctx.arc(node.x, node.y, Math.max(2.4, node.r * 0.18), 0, Math.PI * 2);
864
+ ctx.fill();
865
+ }
866
+ ctx.restore();
867
+
868
+ if (selected || hovered) {
869
+ ctx.save();
870
+ drawNodeShape(ctx, node.x, node.y, node.r + 4, entity);
871
+ ctx.strokeStyle = color;
872
+ ctx.lineWidth = selected ? 2.6 : 1.8;
873
+ ctx.shadowColor = color;
874
+ ctx.shadowBlur = 8;
875
+ ctx.stroke();
876
+ ctx.restore();
877
+ }
878
+
879
+ var shouldLabel = matches && (selected || hovered || (query.active && matches) || (!dense && state.sim.zoom > 0.75) || (dense && state.sim.zoom > 1.55 && node.r > 13));
880
+ if (shouldLabel) drawNodeLabel(ctx, node, selected || hovered);
881
+ });
882
+ }
883
+
884
+ function drawArrow(ctx, from, to, cx, cy, color, alpha) {
885
+ var angle = Math.atan2(to.y - cy, to.x - cx);
886
+ var tipX = to.x - to.r * Math.cos(angle);
887
+ var tipY = to.y - to.r * Math.sin(angle);
888
+ ctx.beginPath();
889
+ ctx.moveTo(tipX, tipY);
890
+ ctx.lineTo(tipX - 6 * Math.cos(angle - 0.32), tipY - 6 * Math.sin(angle - 0.32));
891
+ ctx.lineTo(tipX - 6 * Math.cos(angle + 0.32), tipY - 6 * Math.sin(angle + 0.32));
892
+ ctx.closePath();
893
+ ctx.fillStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + Math.min(0.85, alpha + 0.10) + ")";
894
+ ctx.fill();
895
+ }
896
+
897
+ function drawEdgeLabel(ctx, edge, x, y) {
898
+ var inv = 1 / state.sim.zoom;
899
+ ctx.save();
900
+ ctx.font = "700 " + (9 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
901
+ ctx.textAlign = "center";
902
+ ctx.fillStyle = "rgba(155,231,192,0.72)";
903
+ ctx.fillText(shortName(edge.relation || "related", 22), x, y - 5 * inv);
904
+ ctx.restore();
905
+ }
906
+
907
+ function drawNodeLabel(ctx, node, strong) {
908
+ var inv = 1 / state.sim.zoom;
909
+ var label = shortName(displayName(node.entity), strong ? 30 : 20);
910
+ ctx.save();
911
+ ctx.font = (strong ? "800 " : "700 ") + (11 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
912
+ var width = ctx.measureText(label).width + 16 * inv;
913
+ var height = 20 * inv;
914
+ var x = node.x - width / 2;
915
+ var y = node.y + node.r + 8 * inv;
916
+ ctx.fillStyle = "rgba(2,5,3,0.92)";
917
+ roundedRect(ctx, x, y, width, height, 4 * inv);
918
+ ctx.fill();
919
+ ctx.strokeStyle = strong ? "rgba(65,255,143,0.45)" : "rgba(65,255,143,0.14)";
920
+ ctx.stroke();
921
+ ctx.fillStyle = strong ? graphPalette.text : graphPalette.muted;
922
+ ctx.textAlign = "center";
923
+ ctx.fillText(label, node.x, y + 14 * inv);
924
+ ctx.restore();
925
+ }
926
+
927
+ function drawNodeShape(ctx, x, y, r, entity) {
928
+ var type = entity.type || "";
929
+ if (type === "file" || type === "repo" || type === "command" || type === "script") {
930
+ roundedRect(ctx, x - r * 1.25, y - r * 0.75, r * 2.5, r * 1.5, 4);
931
+ return;
932
+ }
933
+ if (type === "decision" || type === "bug_fix" || type === "test") {
934
+ ctx.beginPath();
935
+ ctx.moveTo(x, y - r);
936
+ ctx.lineTo(x + r, y);
937
+ ctx.lineTo(x, y + r);
938
+ ctx.lineTo(x - r, y);
939
+ ctx.closePath();
940
+ return;
941
+ }
942
+ if (type === "route" || type === "external" || isDependencyEntity(entity)) {
943
+ ctx.beginPath();
944
+ for (var i = 0; i < 6; i++) {
945
+ var angle = Math.PI / 3 * i - Math.PI / 2;
946
+ var px = x + r * Math.cos(angle);
947
+ var py = y + r * Math.sin(angle);
948
+ if (i === 0) ctx.moveTo(px, py);
949
+ else ctx.lineTo(px, py);
950
+ }
951
+ ctx.closePath();
952
+ return;
953
+ }
954
+ ctx.beginPath();
955
+ ctx.arc(x, y, r, 0, Math.PI * 2);
956
+ }
957
+
958
+ function nodeThemeColor(entity) {
959
+ if (isDependencyEntity(entity) || entity.type === "external") return graphPalette.dependency;
960
+ if (entity.type === "test" || entity.type === "tag") return graphPalette.amber;
961
+ if (entity.type === "bug_fix" || entity.type === "path") return graphPalette.danger;
962
+ if (entity.graph_kind === "memory" || ["memory", "repo", "memory_type", "decision", "runbook", "workflow", "convention", "gotcha", "reference", "policy"].indexOf(entity.type) !== -1) return graphPalette.memory;
963
+ if (entity.graph_kind === "code" || ["file", "symbol", "route", "script", "command", "package"].indexOf(entity.type) !== -1) return graphPalette.code;
964
+ return graphPalette.muted;
965
+ }
966
+
967
+ function nodeFillColor(entity) {
968
+ if (isDependencyEntity(entity) || entity.type === "external") return "rgba(7,13,10,0.88)";
969
+ if (entity.graph_kind === "memory" || entity.type === "memory") return graphPalette.bodyMemory;
970
+ if (entity.graph_kind === "code") return graphPalette.bodyCode;
971
+ return graphPalette.body;
972
+ }
973
+
974
+ function edgeThemeColor(edge, fromEntity, toEntity) {
975
+ if (edge.relation && /test|covers/i.test(edge.relation)) return graphPalette.amber;
976
+ if (edge.relation && /invalid|risk|missing|bug/i.test(edge.relation)) return graphPalette.danger;
977
+ if (isDependencyEntity(fromEntity) || isDependencyEntity(toEntity)) return graphPalette.dependency;
978
+ if (fromEntity.graph_kind === "memory" || toEntity.graph_kind === "memory") return graphPalette.memory;
979
+ return graphPalette.code;
980
+ }
981
+
461
982
  function renderSvg() {
462
983
  els.edgeLayer.textContent = "";
463
984
  els.nodeLayer.textContent = "";
@@ -467,17 +988,19 @@
467
988
  var selectedEdgeId = state.selected && state.selected.kind === "edge" ? state.selected.id : null;
468
989
  var connectedIds = connectedEntityIds(selectedEntityId, selectedEdgeId);
469
990
 
991
+ renderLaneLabels();
992
+
470
993
  state.edges.forEach(function (edge) {
994
+ if (!state.visibleEdgeIds.has(edge.id)) return;
471
995
  var from = state.positions.get(edge.from);
472
996
  var to = state.positions.get(edge.to);
473
997
  if (!from || !to) return;
474
998
 
475
999
  var group = svgEl("g");
476
- var visible = state.visibleEdgeIds.has(edge.id);
477
1000
  var connected = selectedEdgeId === edge.id || connectedIds.edges.has(edge.id);
478
1001
  var line = svgEl("path", {
479
1002
  d: edgePath(from, to),
480
- class: classNames("edge-line", "review-" + reviewStatus(edge).replace(/\s+/g, "-"), !visible && "filtered", connected && "connected", selectedEdgeId === edge.id && "selected")
1003
+ class: classNames("edge-line", "review-" + reviewStatus(edge).replace(/\s+/g, "-"), connected && "connected", selectedEdgeId === edge.id && "selected")
481
1004
  });
482
1005
  var hit = svgEl("path", {
483
1006
  d: edgePath(from, to),
@@ -496,14 +1019,14 @@
496
1019
  });
497
1020
 
498
1021
  state.entities.forEach(function (entity) {
1022
+ if (!state.visibleEntityIds.has(entity.id)) return;
499
1023
  var pos = state.positions.get(entity.id);
500
1024
  if (!pos) return;
501
1025
 
502
- var visible = state.visibleEntityIds.has(entity.id);
503
1026
  var selected = selectedEntityId === entity.id;
504
1027
  var connected = connectedIds.entities.has(entity.id);
505
1028
  var group = svgEl("g", {
506
- class: classNames("node", "graph-" + (entity.graph_kind || "unknown"), !visible && "filtered", selected && "selected", connected && "connected"),
1029
+ class: classNames("node", "graph-" + (entity.graph_kind || "unknown"), "type-" + safeCssName(entity.type || "unknown"), isDependencyEntity(entity) && "dependency-node", selected && "selected", connected && "connected"),
507
1030
  transform: "translate(" + pos.x + " " + pos.y + ")"
508
1031
  });
509
1032
  var dims = nodeDimensions(entity);
@@ -551,6 +1074,21 @@
551
1074
  });
552
1075
  }
553
1076
 
1077
+ function renderLaneLabels() {
1078
+ var visible = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
1079
+ [
1080
+ ["MEMORY", 92, visible.some(function (entity) { return entity.graph_kind === "memory"; })],
1081
+ ["FILES", 392, visible.some(function (entity) { return entity.graph_kind === "code" && entity.type === "file"; })],
1082
+ ["FLOW", 682, visible.some(function (entity) { return entity.graph_kind === "code" && ["symbol", "route", "test"].indexOf(entity.type) !== -1; })],
1083
+ ["DEPS", 962, visible.some(function (entity) { return isDependencyEntity(entity); })]
1084
+ ].forEach(function (lane) {
1085
+ if (!lane[2]) return;
1086
+ var label = svgEl("text", { x: lane[1], y: 34, class: "lane-label" });
1087
+ label.textContent = lane[0];
1088
+ els.edgeLayer.appendChild(label);
1089
+ });
1090
+ }
1091
+
554
1092
  function renderLists() {
555
1093
  var visibleEntities = state.entities.filter(function (entity) {
556
1094
  return state.visibleEntityIds.has(entity.id);
@@ -688,6 +1226,9 @@
688
1226
  if (!els.metricsSummary) return;
689
1227
  var visibleEdges = state.edges.filter(function (edge) { return state.visibleEdgeIds.has(edge.id); });
690
1228
  var visibleEntities = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
1229
+ var hiddenDependencies = state.entities.filter(function (entity) {
1230
+ return isDependencyEntity(entity) && !state.visibleEntityIds.has(entity.id);
1231
+ }).length;
691
1232
  var evidenceEdges = visibleEdges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length > 0; }).length;
692
1233
  var official = state.metrics;
693
1234
  var metrics = official ? [
@@ -716,7 +1257,8 @@
716
1257
  });
717
1258
  renderStatusStrip(visibleEntities, visibleEdges, official);
718
1259
  els.workspaceMode.textContent = (els.viewMode.value || "combined").replace(/^./, function (letter) { return letter.toUpperCase(); });
719
- els.graphSubhead.textContent = visibleEntities.length + " visible nodes and " + visibleEdges.length + " visible relations.";
1260
+ els.graphSubhead.textContent = visibleEntities.length + " visible nodes and " + visibleEdges.length + " visible relations" +
1261
+ (hiddenDependencies && !els.showDependencies.checked ? " (" + hiddenDependencies + " dependency/noise nodes hidden)." : ".");
720
1262
  }
721
1263
 
722
1264
  function renderReviewQueue() {
@@ -839,6 +1381,212 @@
839
1381
  }, 0);
840
1382
  }
841
1383
 
1384
+ function entityImportance(entity) {
1385
+ if (!entity) return -1000;
1386
+ var score = degreeOf(entity.id) * 8;
1387
+ if (entity.graph_kind === "memory") score += 80;
1388
+ if (entity.graph_kind === "code" && entity.type === "file") score += 42;
1389
+ if (["route", "test"].indexOf(entity.type) !== -1) score += 46;
1390
+ if (entity.type === "symbol") score += 34;
1391
+ if (entity.type === "script") score += 24;
1392
+ if (["runbook", "bug_fix", "decision", "workflow", "gotcha", "convention"].indexOf(entity.type) !== -1) score += 40;
1393
+ if (isDependencyEntity(entity)) score -= 90;
1394
+ if (isGeneratedEntity(entity)) score -= 48;
1395
+ return score;
1396
+ }
1397
+
1398
+ function isDependencyEntity(entity) {
1399
+ var text = normalize([entity.id, entity.name, entity.path, entity.summary].filter(Boolean).join(" "));
1400
+ return entity.type === "external" ||
1401
+ text.indexOf("node_modules/") !== -1 ||
1402
+ text.indexOf("/node_modules") !== -1 ||
1403
+ text.indexOf("package-lock.json") !== -1 ||
1404
+ text.indexOf("pnpm-lock.yaml") !== -1 ||
1405
+ text.indexOf("yarn.lock") !== -1 ||
1406
+ text.indexOf("bun.lockb") !== -1 ||
1407
+ text.indexOf("dist/") !== -1 ||
1408
+ text.indexOf("build/") !== -1;
1409
+ }
1410
+
1411
+ function isGeneratedEntity(entity) {
1412
+ var text = normalize([entity.id, entity.name, entity.path, entity.summary].filter(Boolean).join(" "));
1413
+ return text.indexOf(".agent_memory/indexes/") !== -1 ||
1414
+ text.indexOf(".agent_memory/code_graph/") !== -1 ||
1415
+ text.indexOf(".agent_memory/graph/") !== -1 ||
1416
+ text.indexOf(".min.js") !== -1 ||
1417
+ text.indexOf("coverage/") !== -1;
1418
+ }
1419
+
1420
+ function startCanvasPointer(event) {
1421
+ if (event.button !== 0) return;
1422
+ var world = canvasToWorld(event);
1423
+ var node = findCanvasNode(world.x, world.y);
1424
+ if (node) {
1425
+ state.sim.dragNode = node;
1426
+ state.sim.panning = null;
1427
+ } else {
1428
+ state.sim.panning = {
1429
+ x: event.clientX,
1430
+ y: event.clientY,
1431
+ panX: state.sim.panX,
1432
+ panY: state.sim.panY,
1433
+ moved: false
1434
+ };
1435
+ }
1436
+ }
1437
+
1438
+ function moveCanvasPointer(event) {
1439
+ var world = canvasToWorld(event);
1440
+ if (state.sim.dragNode) {
1441
+ state.sim.dragNode.x = world.x;
1442
+ state.sim.dragNode.y = world.y;
1443
+ state.sim.dragNode.vx = 0;
1444
+ state.sim.dragNode.vy = 0;
1445
+ } else if (state.sim.panning) {
1446
+ var dx = event.clientX - state.sim.panning.x;
1447
+ var dy = event.clientY - state.sim.panning.y;
1448
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) state.sim.panning.moved = true;
1449
+ state.sim.panX = state.sim.panning.panX + dx;
1450
+ state.sim.panY = state.sim.panning.panY + dy;
1451
+ }
1452
+ state.sim.hoverNode = findCanvasNode(world.x, world.y);
1453
+ updateCanvasTooltip(event);
1454
+ drawCanvasGraph();
1455
+ }
1456
+
1457
+ function endCanvasPointer() {
1458
+ if (state.sim.dragNode) {
1459
+ state.selected = { kind: "entity", id: state.sim.dragNode.id };
1460
+ state.sim.dragNode = null;
1461
+ render();
1462
+ } else if (state.sim.panning && !state.sim.panning.moved) {
1463
+ state.selected = null;
1464
+ render();
1465
+ }
1466
+ state.sim.panning = null;
1467
+ }
1468
+
1469
+ function leaveCanvasPointer() {
1470
+ state.sim.hoverNode = null;
1471
+ state.sim.dragNode = null;
1472
+ state.sim.panning = null;
1473
+ if (els.tooltip) els.tooltip.classList.remove("visible");
1474
+ drawCanvasGraph();
1475
+ }
1476
+
1477
+ function handleCanvasWheel(event) {
1478
+ event.preventDefault();
1479
+ var rect = els.canvas.getBoundingClientRect();
1480
+ var before = canvasToWorld(event);
1481
+ var factor = event.deltaY > 0 ? 0.88 : 1.14;
1482
+ state.sim.zoom = clamp(state.sim.zoom * factor, 0.14, 4.5);
1483
+ state.sim.panX = event.clientX - rect.left - before.x * state.sim.zoom;
1484
+ state.sim.panY = event.clientY - rect.top - before.y * state.sim.zoom;
1485
+ drawCanvasGraph();
1486
+ }
1487
+
1488
+ function handleCanvasDoubleClick(event) {
1489
+ var world = canvasToWorld(event);
1490
+ var node = findCanvasNode(world.x, world.y);
1491
+ if (!node) return;
1492
+ state.selected = { kind: "entity", id: node.id };
1493
+ els.scopeFilter.value = "focus";
1494
+ render();
1495
+ }
1496
+
1497
+ function canvasToWorld(event) {
1498
+ var rect = els.canvas.getBoundingClientRect();
1499
+ return {
1500
+ x: (event.clientX - rect.left - state.sim.panX) / state.sim.zoom,
1501
+ y: (event.clientY - rect.top - state.sim.panY) / state.sim.zoom
1502
+ };
1503
+ }
1504
+
1505
+ function findCanvasNode(x, y) {
1506
+ for (var i = state.sim.nodes.length - 1; i >= 0; i--) {
1507
+ var node = state.sim.nodes[i];
1508
+ var dx = node.x - x;
1509
+ var dy = node.y - y;
1510
+ if (dx * dx + dy * dy <= Math.pow(node.r + 7, 2)) return node;
1511
+ }
1512
+ return null;
1513
+ }
1514
+
1515
+ function updateCanvasTooltip(event) {
1516
+ if (!els.tooltip) return;
1517
+ var node = state.sim.hoverNode;
1518
+ if (!node || state.sim.dragNode || state.sim.panning) {
1519
+ els.tooltip.classList.remove("visible");
1520
+ return;
1521
+ }
1522
+ var entity = node.entity;
1523
+ var relationCount = state.sim.edges.filter(function (edge) { return edge.from === node.id || edge.to === node.id; }).length;
1524
+ var color = nodeThemeColor(entity);
1525
+ els.tooltip.innerHTML = [
1526
+ "<div class=\"tt-name\"></div>",
1527
+ "<div class=\"tt-type\"></div>",
1528
+ "<div class=\"tt-summary\"></div>",
1529
+ "<div class=\"tt-conns\"></div>"
1530
+ ].join("");
1531
+ els.tooltip.querySelector(".tt-name").textContent = displayName(entity);
1532
+ els.tooltip.querySelector(".tt-type").textContent = nodeKindLabel(entity);
1533
+ els.tooltip.querySelector(".tt-type").style.color = color;
1534
+ els.tooltip.querySelector(".tt-summary").textContent = shortName(entity.summary || entity.id, 150);
1535
+ els.tooltip.querySelector(".tt-conns").textContent = relationCount + " relation" + (relationCount === 1 ? "" : "s");
1536
+ var rect = els.canvas.getBoundingClientRect();
1537
+ els.tooltip.style.left = (event.clientX - rect.left + 14) + "px";
1538
+ els.tooltip.style.top = (event.clientY - rect.top + 14) + "px";
1539
+ els.tooltip.classList.add("visible");
1540
+ }
1541
+
1542
+ function zoomCanvas(factor) {
1543
+ resizeCanvas();
1544
+ var rect = els.canvas.getBoundingClientRect();
1545
+ var cx = rect.width / 2;
1546
+ var cy = rect.height / 2;
1547
+ var before = {
1548
+ x: (cx - state.sim.panX) / state.sim.zoom,
1549
+ y: (cy - state.sim.panY) / state.sim.zoom
1550
+ };
1551
+ state.sim.zoom = clamp(state.sim.zoom * factor, 0.14, 4.5);
1552
+ state.sim.panX = cx - before.x * state.sim.zoom;
1553
+ state.sim.panY = cy - before.y * state.sim.zoom;
1554
+ drawCanvasGraph();
1555
+ }
1556
+
1557
+ function fitCanvas() {
1558
+ resizeCanvas();
1559
+ if (!state.sim.nodes.length) {
1560
+ state.sim.panX = 0;
1561
+ state.sim.panY = 0;
1562
+ state.sim.zoom = 1;
1563
+ return;
1564
+ }
1565
+ var xs = state.sim.nodes.map(function (node) { return node.x; });
1566
+ var ys = state.sim.nodes.map(function (node) { return node.y; });
1567
+ var minX = Math.min.apply(null, xs) - 90;
1568
+ var maxX = Math.max.apply(null, xs) + 90;
1569
+ var minY = Math.min.apply(null, ys) - 90;
1570
+ var maxY = Math.max.apply(null, ys) + 90;
1571
+ var width = els.canvas.width / Math.max(1, window.devicePixelRatio || 1);
1572
+ var height = els.canvas.height / Math.max(1, window.devicePixelRatio || 1);
1573
+ var graphWidth = Math.max(1, maxX - minX);
1574
+ var graphHeight = Math.max(1, maxY - minY);
1575
+ state.sim.zoom = clamp(Math.min(width / graphWidth, height / graphHeight), 0.22, 1.45);
1576
+ state.sim.panX = width / 2 - ((minX + maxX) / 2) * state.sim.zoom;
1577
+ state.sim.panY = height / 2 - ((minY + maxY) / 2) * state.sim.zoom;
1578
+ }
1579
+
1580
+ function focusedCanvasNodeId() {
1581
+ if (state.sim.hoverNode) return state.sim.hoverNode.id;
1582
+ if (state.selected && state.selected.kind === "entity") return state.selected.id;
1583
+ if (state.selected && state.selected.kind === "edge") {
1584
+ var selectedEdge = state.edges.find(function (edge) { return edge.id === state.selected.id; });
1585
+ return selectedEdge ? selectedEdge.from : null;
1586
+ }
1587
+ return null;
1588
+ }
1589
+
842
1590
  function fitView() {
843
1591
  var visible = state.entities.filter(function (entity) {
844
1592
  return state.visibleEntityIds.size === 0 || state.visibleEntityIds.has(entity.id);
@@ -855,10 +1603,10 @@
855
1603
  var minY = Math.min.apply(null, ys) - 82;
856
1604
  var maxY = Math.max.apply(null, ys) + 82;
857
1605
  state.viewBox = {
858
- x: clamp(minX, -160, 1000),
859
- y: clamp(minY, -120, 660),
860
- width: Math.max(520, Math.min(1120, maxX - minX)),
861
- height: Math.max(360, Math.min(700, maxY - minY))
1606
+ x: minX,
1607
+ y: minY,
1608
+ width: Math.max(620, maxX - minX),
1609
+ height: Math.max(400, maxY - minY)
862
1610
  };
863
1611
  }
864
1612
 
@@ -965,6 +1713,77 @@
965
1713
  return normalize(JSON.stringify(value || {}));
966
1714
  }
967
1715
 
1716
+ function parseSearchQuery(value) {
1717
+ var raw = normalize(value);
1718
+ var tokens = raw
1719
+ .replace(/[^a-z0-9_./:-]+/g, " ")
1720
+ .split(/\s+/)
1721
+ .map(searchStem)
1722
+ .filter(Boolean)
1723
+ .filter(function (token) { return !SEARCH_STOP_WORDS.has(token); });
1724
+ if ((tokens.indexOf("run") !== -1 || tokens.indexOf("runn") !== -1) && tokens.indexOf("test") !== -1) tokens.push("runtest");
1725
+ var groups = tokens.map(function (token) {
1726
+ return unique([token].concat(SEARCH_SYNONYMS[token] || []).map(searchStem).filter(Boolean));
1727
+ });
1728
+ return {
1729
+ active: raw.trim().length > 0,
1730
+ raw: raw,
1731
+ tokens: unique(groups.reduce(function (all, group) { return all.concat(group); }, [])),
1732
+ groups: groups
1733
+ };
1734
+ }
1735
+
1736
+ var SEARCH_STOP_WORDS = new Set([
1737
+ "a", "an", "and", "are", "about", "can", "do", "does", "for", "from", "how", "i", "in", "is", "it", "me", "of", "on", "or", "please", "show", "that", "the", "there", "this", "to", "what", "when", "where", "which", "who", "why", "with"
1738
+ ]);
1739
+
1740
+ var SEARCH_SYNONYMS = {
1741
+ memory: ["packet", "runbook", "decision", "workflow", "gotcha", "reference"],
1742
+ test: ["tests", "testing", "vitest", "jest", "pytest", "spec"],
1743
+ run: ["running", "command", "script", "npm", "pnpm", "yarn"],
1744
+ runn: ["run", "running", "command", "script", "npm", "pnpm", "yarn"],
1745
+ runtest: ["run", "test", "runbook", "command"],
1746
+ start: ["serve", "dev", "launch"],
1747
+ build: ["compile", "tsc"],
1748
+ bug: ["fix", "gotcha", "error"],
1749
+ route: ["endpoint", "api"],
1750
+ file: ["path"],
1751
+ dependency: ["package", "external", "deps"]
1752
+ };
1753
+
1754
+ function matchesSearchQuery(value, query) {
1755
+ if (!query || !query.active) return true;
1756
+ var text = searchableText(value);
1757
+ if (query.raw && text.indexOf(query.raw) !== -1) return true;
1758
+ if (!query.tokens.length) return true;
1759
+ var textTokens = new Set(text
1760
+ .replace(/[^a-z0-9_./:-]+/g, " ")
1761
+ .split(/\s+/)
1762
+ .map(searchStem)
1763
+ .filter(Boolean));
1764
+ var requiredGroups = (query.groups || []).filter(function (group) {
1765
+ return group.some(function (token) { return SEARCH_SOFT_TOKENS.has(token) ? text.indexOf(token) !== -1 || textTokens.has(token) : true; });
1766
+ });
1767
+ if (!requiredGroups.length) requiredGroups = query.groups || [query.tokens];
1768
+ return requiredGroups.every(function (group) {
1769
+ return group.some(function (token) {
1770
+ if (text.indexOf(token) !== -1) return true;
1771
+ return textTokens.has(token);
1772
+ });
1773
+ });
1774
+ }
1775
+
1776
+ var SEARCH_SOFT_TOKENS = new Set(["memory", "packet", "about"]);
1777
+
1778
+ function searchStem(value) {
1779
+ var token = String(value || "").toLowerCase();
1780
+ if (token.length > 5 && token.endsWith("ing")) token = token.slice(0, -3);
1781
+ if (token.length > 4 && token.endsWith("ies")) token = token.slice(0, -3) + "y";
1782
+ if (token.length > 4 && token.endsWith("es")) token = token.slice(0, -2);
1783
+ if (token.length > 3 && token.endsWith("s")) token = token.slice(0, -1);
1784
+ return token;
1785
+ }
1786
+
968
1787
  function shortName(value, max) {
969
1788
  var text = String(value || "");
970
1789
  return text.length > max ? text.slice(0, Math.max(1, max - 1)) + "..." : text;
@@ -982,10 +1801,49 @@
982
1801
  return Math.max(min, Math.min(max, value));
983
1802
  }
984
1803
 
1804
+ function hexToRgb(value) {
1805
+ var hex = String(value || "#9be7c0").replace("#", "");
1806
+ if (hex.length === 3) hex = hex.split("").map(function (part) { return part + part; }).join("");
1807
+ var parsed = parseInt(hex, 16);
1808
+ return {
1809
+ r: (parsed >> 16) & 255,
1810
+ g: (parsed >> 8) & 255,
1811
+ b: parsed & 255
1812
+ };
1813
+ }
1814
+
1815
+ function brighten(value, amount) {
1816
+ var rgb = hexToRgb(value);
1817
+ return "rgb(" + Math.min(255, rgb.r + amount) + "," + Math.min(255, rgb.g + amount) + "," + Math.min(255, rgb.b + amount) + ")";
1818
+ }
1819
+
1820
+ function roundedRect(ctx, x, y, width, height, radius) {
1821
+ var r = Math.min(radius, Math.abs(width) / 2, Math.abs(height) / 2);
1822
+ ctx.beginPath();
1823
+ ctx.moveTo(x + r, y);
1824
+ ctx.lineTo(x + width - r, y);
1825
+ ctx.quadraticCurveTo(x + width, y, x + width, y + r);
1826
+ ctx.lineTo(x + width, y + height - r);
1827
+ ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
1828
+ ctx.lineTo(x + r, y + height);
1829
+ ctx.quadraticCurveTo(x, y + height, x, y + height - r);
1830
+ ctx.lineTo(x, y + r);
1831
+ ctx.quadraticCurveTo(x, y, x + r, y);
1832
+ ctx.closePath();
1833
+ }
1834
+
985
1835
  function classNames() {
986
1836
  return Array.prototype.slice.call(arguments).filter(Boolean).join(" ");
987
1837
  }
988
1838
 
1839
+ function visibleSignature(entityIds, edgeIds) {
1840
+ return Array.from(entityIds).sort().join("|") + "::" + Array.from(edgeIds).sort().join("|");
1841
+ }
1842
+
1843
+ function safeCssName(value) {
1844
+ return String(value || "unknown").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
1845
+ }
1846
+
989
1847
  function svgEl(name, attrs) {
990
1848
  var element = document.createElementNS("http://www.w3.org/2000/svg", name);
991
1849
  Object.keys(attrs || {}).forEach(function (key) {