@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/README.md +25 -15
- package/dist/cli.js +64 -7
- package/dist/daemon.js +53 -1
- package/dist/index.js +26 -8
- package/dist/kernel.js +367 -51
- package/package.json +11 -2
- package/viewer/app.js +897 -39
- package/viewer/index.html +28 -3
- package/viewer/styles.css +102 -4
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: "#
|
|
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: "#
|
|
31
|
-
route: "#
|
|
45
|
+
symbol: "#9be7c0",
|
|
46
|
+
route: "#6ad7ff",
|
|
32
47
|
test: "#ffd166",
|
|
33
|
-
external: "#
|
|
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.
|
|
76
|
-
els.
|
|
77
|
-
els.
|
|
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
|
-
|
|
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:
|
|
388
|
-
{ name: "files", x:
|
|
389
|
-
{ name: "flow", x:
|
|
390
|
-
{ name: "external", x:
|
|
391
|
-
{ name: "other", x:
|
|
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 =
|
|
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
|
|
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
|
-
|
|
402
|
-
var
|
|
403
|
-
var
|
|
404
|
-
var
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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, "-"),
|
|
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"),
|
|
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:
|
|
859
|
-
y:
|
|
860
|
-
width: Math.max(
|
|
861
|
-
height: Math.max(
|
|
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) {
|