@kage-core/kage-graph-mcp 1.0.0 → 1.1.0
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 +294 -0
- package/dist/cli.js +726 -0
- package/dist/daemon.js +230 -0
- package/dist/index.js +659 -21
- package/dist/kernel.js +4177 -0
- package/dist/registry/index.js +373 -0
- package/package.json +17 -8
- package/viewer/app.js +1000 -0
- package/viewer/index.html +136 -0
- package/viewer/styles.css +530 -0
- package/index.ts +0 -254
- package/tsconfig.json +0 -14
package/viewer/app.js
ADDED
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var state = {
|
|
5
|
+
graph: null,
|
|
6
|
+
entities: [],
|
|
7
|
+
edges: [],
|
|
8
|
+
episodesById: new Map(),
|
|
9
|
+
entityById: new Map(),
|
|
10
|
+
positions: new Map(),
|
|
11
|
+
visibleEntityIds: new Set(),
|
|
12
|
+
visibleEdgeIds: new Set(),
|
|
13
|
+
selected: null,
|
|
14
|
+
metrics: null,
|
|
15
|
+
pendingPackets: [],
|
|
16
|
+
reviewText: "",
|
|
17
|
+
viewBox: { x: 0, y: 0, width: 1000, height: 660 },
|
|
18
|
+
pan: null
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
var palette = {
|
|
22
|
+
repo: "#41ff8f",
|
|
23
|
+
memory: "#b88cff",
|
|
24
|
+
path: "#ff6b6b",
|
|
25
|
+
tag: "#ffd166",
|
|
26
|
+
package: "#6ad7ff",
|
|
27
|
+
command: "#9be7c0",
|
|
28
|
+
memory_type: "#41ff8f",
|
|
29
|
+
file: "#6ad7ff",
|
|
30
|
+
symbol: "#b88cff",
|
|
31
|
+
route: "#ff8fab",
|
|
32
|
+
test: "#ffd166",
|
|
33
|
+
external: "#93a4a0",
|
|
34
|
+
script: "#6ad7ff",
|
|
35
|
+
default: "#9be7c0"
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
var els = {
|
|
39
|
+
graphFile: document.getElementById("graphFile"),
|
|
40
|
+
graphSummary: document.getElementById("graphSummary"),
|
|
41
|
+
statusStrip: document.getElementById("statusStrip"),
|
|
42
|
+
autoLoadStatus: document.getElementById("autoLoadStatus"),
|
|
43
|
+
workspaceMode: document.getElementById("workspaceMode"),
|
|
44
|
+
graphSubhead: document.getElementById("graphSubhead"),
|
|
45
|
+
selectionStatus: document.getElementById("selectionStatus"),
|
|
46
|
+
searchInput: document.getElementById("searchInput"),
|
|
47
|
+
viewMode: document.getElementById("viewMode"),
|
|
48
|
+
typeFilter: document.getElementById("typeFilter"),
|
|
49
|
+
relationFilter: document.getElementById("relationFilter"),
|
|
50
|
+
resetView: document.getElementById("resetView"),
|
|
51
|
+
zoomOut: document.getElementById("zoomOut"),
|
|
52
|
+
zoomIn: document.getElementById("zoomIn"),
|
|
53
|
+
fitView: document.getElementById("fitView"),
|
|
54
|
+
svg: document.getElementById("graphSvg"),
|
|
55
|
+
nodeLayer: document.getElementById("nodeLayer"),
|
|
56
|
+
edgeLayer: document.getElementById("edgeLayer"),
|
|
57
|
+
emptyState: document.getElementById("emptyState"),
|
|
58
|
+
selectionDetails: document.getElementById("selectionDetails"),
|
|
59
|
+
entityList: document.getElementById("entityList"),
|
|
60
|
+
edgeList: document.getElementById("edgeList"),
|
|
61
|
+
metricsSummary: document.getElementById("metricsSummary"),
|
|
62
|
+
entityCount: document.getElementById("entityCount"),
|
|
63
|
+
edgeCount: document.getElementById("edgeCount"),
|
|
64
|
+
reviewCount: document.getElementById("reviewCount"),
|
|
65
|
+
reviewList: document.getElementById("reviewList"),
|
|
66
|
+
proofStatus: document.getElementById("proofStatus"),
|
|
67
|
+
proofList: document.getElementById("proofList")
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
els.graphFile.addEventListener("change", handleFile);
|
|
71
|
+
els.searchInput.addEventListener("input", render);
|
|
72
|
+
els.viewMode.addEventListener("change", render);
|
|
73
|
+
els.typeFilter.addEventListener("change", render);
|
|
74
|
+
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(); });
|
|
78
|
+
els.svg.addEventListener("mousedown", startPan);
|
|
79
|
+
els.svg.addEventListener("click", handleSvgClick);
|
|
80
|
+
els.svg.addEventListener("wheel", handleWheelZoom, { passive: false });
|
|
81
|
+
window.addEventListener("mousemove", continuePan);
|
|
82
|
+
window.addEventListener("mouseup", endPan);
|
|
83
|
+
els.resetView.addEventListener("click", function () {
|
|
84
|
+
els.searchInput.value = "";
|
|
85
|
+
els.viewMode.value = "combined";
|
|
86
|
+
els.typeFilter.value = "";
|
|
87
|
+
els.relationFilter.value = "";
|
|
88
|
+
state.selected = null;
|
|
89
|
+
fitView();
|
|
90
|
+
render();
|
|
91
|
+
});
|
|
92
|
+
loadFromUrlParams();
|
|
93
|
+
|
|
94
|
+
function handleFile(event) {
|
|
95
|
+
var files = Array.from(event.target.files || []);
|
|
96
|
+
if (!files.length) return;
|
|
97
|
+
Promise.all(files.map(readJsonFile)).then(function (items) {
|
|
98
|
+
state.metrics = items.map(function (item) { return item.graph; }).find(isMetricsGraph) || null;
|
|
99
|
+
var graphItems = items.filter(function (item) { return !isMetricsGraph(item.graph); });
|
|
100
|
+
if (!graphItems.length && state.metrics) {
|
|
101
|
+
state.graph = { entities: [], edges: [], episodes: [] };
|
|
102
|
+
state.entities = [];
|
|
103
|
+
state.edges = [];
|
|
104
|
+
state.entityById = new Map();
|
|
105
|
+
state.episodesById = new Map();
|
|
106
|
+
els.emptyState.classList.add("hidden");
|
|
107
|
+
els.graphSummary.textContent = "Metrics loaded.";
|
|
108
|
+
renderMetrics();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
var merged = mergeNormalizedGraphs(graphItems.map(function (item) { return normalizeGraph(item.graph); }));
|
|
112
|
+
loadNormalizedGraph(merged, graphItems.map(function (item) { return item.fileName; }).join(", "));
|
|
113
|
+
}).catch(function (error) {
|
|
114
|
+
showError("Could not load JSON: " + error.message);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function loadGraph(graph, fileName) {
|
|
119
|
+
loadNormalizedGraph(normalizeGraph(graph), fileName);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function loadNormalizedGraph(normalized, fileName) {
|
|
123
|
+
var entities = normalized.entities;
|
|
124
|
+
var edges = normalized.edges;
|
|
125
|
+
var episodes = normalized.episodes;
|
|
126
|
+
|
|
127
|
+
state.graph = normalized;
|
|
128
|
+
state.entities = entities;
|
|
129
|
+
state.edges = edges;
|
|
130
|
+
state.entityById = new Map(entities.map(function (entity) {
|
|
131
|
+
return [entity.id, entity];
|
|
132
|
+
}));
|
|
133
|
+
state.episodesById = new Map(episodes.map(function (episode) {
|
|
134
|
+
return [episode.id, episode];
|
|
135
|
+
}));
|
|
136
|
+
state.selected = null;
|
|
137
|
+
|
|
138
|
+
populateFilters();
|
|
139
|
+
layoutGraph();
|
|
140
|
+
fitView();
|
|
141
|
+
els.emptyState.classList.add("hidden");
|
|
142
|
+
els.graphSummary.textContent = fileName + " loaded: " + entities.length + " nodes, " + edges.length + " relations.";
|
|
143
|
+
render();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function loadFromUrlParams() {
|
|
147
|
+
var params = new URLSearchParams(window.location.search);
|
|
148
|
+
var graphPaths = []
|
|
149
|
+
.concat(params.getAll("graph"))
|
|
150
|
+
.concat(params.getAll("code"))
|
|
151
|
+
.flatMap(function (value) { return String(value || "").split(","); })
|
|
152
|
+
.map(function (value) { return value.trim(); })
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
var metricsPath = params.get("metrics");
|
|
155
|
+
var reviewPath = params.get("review");
|
|
156
|
+
var pendingPath = params.get("pending");
|
|
157
|
+
var inferredRoot = inferMemoryRoot(graphPaths[0] || "");
|
|
158
|
+
if (!reviewPath && inferredRoot) reviewPath = inferredRoot + "/review/memory-review.md";
|
|
159
|
+
if (!pendingPath && inferredRoot) pendingPath = inferredRoot + "/pending";
|
|
160
|
+
var jobs = [];
|
|
161
|
+
if (metricsPath) jobs.push(fetchJson(metricsPath).then(function (metrics) { state.metrics = metrics; }));
|
|
162
|
+
if (reviewPath) jobs.push(fetchText(reviewPath).then(function (text) { state.reviewText = text; }).catch(function () { state.reviewText = ""; }));
|
|
163
|
+
if (pendingPath) jobs.push(loadPending(pendingPath).then(function (packets) { state.pendingPackets = packets; }));
|
|
164
|
+
if (!graphPaths.length && !jobs.length) {
|
|
165
|
+
setAutoLoad("manual mode", false);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
setAutoLoad("loading project graph", false);
|
|
169
|
+
Promise.all(graphPaths.map(function (path) {
|
|
170
|
+
return fetch(path).then(function (response) {
|
|
171
|
+
if (!response.ok) throw new Error(response.status + " " + path);
|
|
172
|
+
return response.json().then(function (graph) { return { fileName: path.split("/").pop() || path, graph: graph }; });
|
|
173
|
+
});
|
|
174
|
+
}).concat(jobs)).then(function (items) {
|
|
175
|
+
var graphItems = items.filter(Boolean);
|
|
176
|
+
if (!graphItems.length) {
|
|
177
|
+
loadNormalizedGraph({ entities: [], edges: [], episodes: [] }, "project metrics");
|
|
178
|
+
setAutoLoad("project console loaded", true);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
var merged = mergeNormalizedGraphs(graphItems.map(function (item) { return normalizeGraph(item.graph); }));
|
|
182
|
+
loadNormalizedGraph(merged, graphItems.map(function (item) { return item.fileName; }).join(", ") || "metrics");
|
|
183
|
+
setAutoLoad("project console loaded", true);
|
|
184
|
+
}).catch(function (error) {
|
|
185
|
+
setAutoLoad("auto-load failed", false);
|
|
186
|
+
showError("Could not auto-load graph: " + error.message);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function inferMemoryRoot(path) {
|
|
191
|
+
var marker = "/.agent_memory/";
|
|
192
|
+
var index = path.indexOf(marker);
|
|
193
|
+
if (index === -1) return "";
|
|
194
|
+
return path.slice(0, index + marker.length - 1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function fetchJson(path) {
|
|
198
|
+
return fetch(path).then(function (response) {
|
|
199
|
+
if (!response.ok) throw new Error(response.status + " " + path);
|
|
200
|
+
return response.json();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function fetchText(path) {
|
|
205
|
+
return fetch(path).then(function (response) {
|
|
206
|
+
if (!response.ok) throw new Error(response.status + " " + path);
|
|
207
|
+
return response.text();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function loadPending(path) {
|
|
212
|
+
return fetchJson(path).then(function (listing) {
|
|
213
|
+
var files = listing && Array.isArray(listing.files) ? listing.files : [];
|
|
214
|
+
return Promise.all(files.map(function (file) { return fetchJson(file.path); }));
|
|
215
|
+
}).catch(function () { return []; });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function setAutoLoad(text, ok) {
|
|
219
|
+
if (!els.autoLoadStatus) return;
|
|
220
|
+
els.autoLoadStatus.textContent = "auto-load: " + text;
|
|
221
|
+
els.autoLoadStatus.className = "autoload-status " + (ok ? "ok" : "");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readJsonFile(file) {
|
|
225
|
+
return new Promise(function (resolve, reject) {
|
|
226
|
+
var reader = new FileReader();
|
|
227
|
+
reader.onload = function () {
|
|
228
|
+
try {
|
|
229
|
+
resolve({ fileName: file.name, graph: JSON.parse(String(reader.result || "{}")) });
|
|
230
|
+
} catch (error) {
|
|
231
|
+
reject(error);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
reader.onerror = function () { reject(new Error("Could not read " + file.name + ".")); };
|
|
235
|
+
reader.readAsText(file);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isMetricsGraph(graph) {
|
|
240
|
+
return graph && graph.code_graph && graph.memory_graph && graph.harness;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function mergeNormalizedGraphs(graphs) {
|
|
244
|
+
var entities = new Map();
|
|
245
|
+
var edges = new Map();
|
|
246
|
+
var episodes = new Map();
|
|
247
|
+
graphs.forEach(function (graph) {
|
|
248
|
+
graph.entities.forEach(function (entity) { entities.set(entity.id, entity); });
|
|
249
|
+
graph.edges.forEach(function (edge) { edges.set(edge.id, edge); });
|
|
250
|
+
graph.episodes.forEach(function (episode) { episodes.set(episode.id, episode); });
|
|
251
|
+
});
|
|
252
|
+
return { entities: Array.from(entities.values()), edges: Array.from(edges.values()), episodes: Array.from(episodes.values()) };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function normalizeGraph(graph) {
|
|
256
|
+
if (Array.isArray(graph.entities) && Array.isArray(graph.edges)) {
|
|
257
|
+
return {
|
|
258
|
+
entities: graph.entities.map(function (entity) { return Object.assign({ graph_kind: "memory" }, entity); }),
|
|
259
|
+
edges: graph.edges.map(function (edge) { return Object.assign({ graph_kind: "memory" }, edge); }),
|
|
260
|
+
episodes: Array.isArray(graph.episodes) ? graph.episodes : []
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (Array.isArray(graph.files) || Array.isArray(graph.symbols)) {
|
|
265
|
+
return normalizeCodeGraph(graph);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { entities: [], edges: [], episodes: [] };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function normalizeCodeGraph(graph) {
|
|
272
|
+
var entities = [];
|
|
273
|
+
var edges = [];
|
|
274
|
+
var seen = new Set();
|
|
275
|
+
var addEntity = function (entity) {
|
|
276
|
+
if (seen.has(entity.id)) return;
|
|
277
|
+
seen.add(entity.id);
|
|
278
|
+
entities.push(entity);
|
|
279
|
+
};
|
|
280
|
+
var addEdge = function (from, to, relation, fact, source) {
|
|
281
|
+
if (!from || !to) return;
|
|
282
|
+
edges.push({
|
|
283
|
+
id: relation + ":" + from + ":" + to + ":" + edges.length,
|
|
284
|
+
from: from,
|
|
285
|
+
to: to,
|
|
286
|
+
relation: relation,
|
|
287
|
+
fact: fact,
|
|
288
|
+
confidence: 1,
|
|
289
|
+
evidence: [],
|
|
290
|
+
commit: graph.repo_state && graph.repo_state.head,
|
|
291
|
+
source: source || "code_graph",
|
|
292
|
+
graph_kind: "code"
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
(graph.files || []).forEach(function (file) {
|
|
297
|
+
addEntity({
|
|
298
|
+
id: "file:" + file.path,
|
|
299
|
+
type: "file",
|
|
300
|
+
graph_kind: "code",
|
|
301
|
+
name: file.path,
|
|
302
|
+
summary: file.kind + " file, " + file.language + ", " + file.line_count + " lines",
|
|
303
|
+
aliases: [file.hash],
|
|
304
|
+
evidence: []
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
(graph.symbols || []).forEach(function (symbol) {
|
|
309
|
+
addEntity({
|
|
310
|
+
id: symbol.id,
|
|
311
|
+
type: symbol.kind === "test" ? "test" : "symbol",
|
|
312
|
+
graph_kind: "code",
|
|
313
|
+
name: symbol.name,
|
|
314
|
+
summary: symbol.kind + " in " + symbol.path + ":" + symbol.line + (symbol.signature ? "\n" + symbol.signature : ""),
|
|
315
|
+
aliases: [symbol.path],
|
|
316
|
+
evidence: []
|
|
317
|
+
});
|
|
318
|
+
addEdge("file:" + symbol.path, symbol.id, "defines_symbol", symbol.path + " defines " + symbol.kind + " " + symbol.name + ".", "symbols");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
(graph.imports || []).forEach(function (item) {
|
|
322
|
+
if (item.to_path) addEdge("file:" + item.from_path, "file:" + item.to_path, "imports", item.from_path + " imports " + item.specifier + ".", "imports");
|
|
323
|
+
else {
|
|
324
|
+
var externalId = "external:" + item.specifier;
|
|
325
|
+
addEntity({ id: externalId, type: "external", graph_kind: "code", name: item.specifier, summary: "External import", aliases: [], evidence: [] });
|
|
326
|
+
addEdge("file:" + item.from_path, externalId, "imports_external", item.from_path + " imports " + item.specifier + ".", "imports");
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
(graph.calls || []).forEach(function (call) {
|
|
331
|
+
addEdge(call.from_symbol || "file:" + call.path, call.to_symbol, "calls", call.path + ":" + call.line + " calls target symbol.", "calls");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
(graph.routes || []).forEach(function (route) {
|
|
335
|
+
addEntity({
|
|
336
|
+
id: route.id,
|
|
337
|
+
type: "route",
|
|
338
|
+
graph_kind: "code",
|
|
339
|
+
name: route.method + " " + route.path,
|
|
340
|
+
summary: route.framework + " route in " + route.file_path + ":" + route.line,
|
|
341
|
+
aliases: [route.file_path],
|
|
342
|
+
evidence: []
|
|
343
|
+
});
|
|
344
|
+
addEdge("file:" + route.file_path, route.id, "defines_route", route.file_path + " defines " + route.method + " " + route.path + ".", "routes");
|
|
345
|
+
if (route.handler_symbol) addEdge(route.id, route.handler_symbol, "handled_by", route.method + " " + route.path + " is handled by a symbol.", "routes");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
(graph.tests || []).forEach(function (test) {
|
|
349
|
+
addEdge(test.test_symbol, test.covers_symbol ? symbolEntityId(graph, test.covers_symbol, test.covers_path) : "file:" + test.covers_path, "covers", test.title + " covers " + (test.covers_symbol || test.covers_path || "unknown") + ".", "tests");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
(graph.packages || []).forEach(function (pkg) {
|
|
353
|
+
var id = pkg.kind + ":" + pkg.name;
|
|
354
|
+
addEntity({ id: id, type: pkg.kind === "script" ? "script" : "external", graph_kind: "code", name: pkg.name, summary: pkg.kind + ": " + pkg.version, aliases: [], evidence: [] });
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return { entities: entities, edges: edges, episodes: [] };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function symbolEntityId(graph, name, path) {
|
|
361
|
+
var match = (graph.symbols || []).find(function (symbol) {
|
|
362
|
+
return symbol.name === name && (!path || symbol.path === path);
|
|
363
|
+
});
|
|
364
|
+
return match ? match.id : null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function populateFilters() {
|
|
368
|
+
replaceOptions(els.typeFilter, "All types", unique(state.entities.map(function (entity) {
|
|
369
|
+
return entity.type || "unknown";
|
|
370
|
+
})));
|
|
371
|
+
replaceOptions(els.relationFilter, "All relations", unique(state.edges.map(function (edge) {
|
|
372
|
+
return edge.relation || "related";
|
|
373
|
+
})));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function replaceOptions(select, label, values) {
|
|
377
|
+
select.textContent = "";
|
|
378
|
+
select.appendChild(new Option(label, ""));
|
|
379
|
+
values.sort().forEach(function (value) {
|
|
380
|
+
select.appendChild(new Option(value, value));
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function layoutGraph() {
|
|
385
|
+
state.positions = new Map();
|
|
386
|
+
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); } }
|
|
392
|
+
];
|
|
393
|
+
lanes.forEach(function (lane) {
|
|
394
|
+
var bucket = state.entities.filter(function (entity) {
|
|
395
|
+
return lane.match(entity);
|
|
396
|
+
}).sort(function (a, b) {
|
|
397
|
+
return degreeOf(b.id) - degreeOf(a.id) || displayName(a).localeCompare(displayName(b));
|
|
398
|
+
});
|
|
399
|
+
if (!bucket.length) return;
|
|
400
|
+
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;
|
|
405
|
+
state.positions.set(entity.id, {
|
|
406
|
+
x: lane.x + xOffset,
|
|
407
|
+
y: lane.y + row * lane.step + yJitter
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function render() {
|
|
414
|
+
if (!state.graph) return;
|
|
415
|
+
|
|
416
|
+
var query = normalize(els.searchInput.value);
|
|
417
|
+
var mode = els.viewMode.value;
|
|
418
|
+
var type = els.typeFilter.value;
|
|
419
|
+
var relation = els.relationFilter.value;
|
|
420
|
+
var matchedEntityIds = new Set();
|
|
421
|
+
var matchedEdgeIds = new Set();
|
|
422
|
+
|
|
423
|
+
state.entities.forEach(function (entity) {
|
|
424
|
+
if (mode !== "combined" && entity.graph_kind !== mode) return;
|
|
425
|
+
var passesType = !type || entity.type === type;
|
|
426
|
+
var passesSearch = !query || searchableText(entity).indexOf(query) !== -1;
|
|
427
|
+
if (passesType && passesSearch) matchedEntityIds.add(entity.id);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
state.edges.forEach(function (edge) {
|
|
431
|
+
if (mode !== "combined" && edge.graph_kind !== mode) return;
|
|
432
|
+
var fromMatched = matchedEntityIds.has(edge.from);
|
|
433
|
+
var toMatched = matchedEntityIds.has(edge.to);
|
|
434
|
+
var edgeMatchesSearch = !query || searchableText(edge).indexOf(query) !== -1;
|
|
435
|
+
var passesRelation = !relation || edge.relation === relation;
|
|
436
|
+
if (passesRelation && (edgeMatchesSearch || fromMatched || toMatched)) {
|
|
437
|
+
matchedEdgeIds.add(edge.id);
|
|
438
|
+
if (!type) {
|
|
439
|
+
if (state.entityById.has(edge.from)) matchedEntityIds.add(edge.from);
|
|
440
|
+
if (state.entityById.has(edge.to)) matchedEntityIds.add(edge.to);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
if (!query && !type && !relation) {
|
|
446
|
+
matchedEntityIds = new Set(state.entities.filter(function (entity) { return mode === "combined" || entity.graph_kind === mode; }).map(function (entity) { return entity.id; }));
|
|
447
|
+
matchedEdgeIds = new Set(state.edges.filter(function (edge) { return mode === "combined" || edge.graph_kind === mode; }).map(function (edge) { return edge.id; }));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
state.visibleEntityIds = matchedEntityIds;
|
|
451
|
+
state.visibleEdgeIds = matchedEdgeIds;
|
|
452
|
+
|
|
453
|
+
renderSvg();
|
|
454
|
+
renderLists();
|
|
455
|
+
renderDetails();
|
|
456
|
+
renderMetrics();
|
|
457
|
+
renderReviewQueue();
|
|
458
|
+
renderProof();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function renderSvg() {
|
|
462
|
+
els.edgeLayer.textContent = "";
|
|
463
|
+
els.nodeLayer.textContent = "";
|
|
464
|
+
els.svg.setAttribute("viewBox", [state.viewBox.x, state.viewBox.y, state.viewBox.width, state.viewBox.height].join(" "));
|
|
465
|
+
|
|
466
|
+
var selectedEntityId = state.selected && state.selected.kind === "entity" ? state.selected.id : null;
|
|
467
|
+
var selectedEdgeId = state.selected && state.selected.kind === "edge" ? state.selected.id : null;
|
|
468
|
+
var connectedIds = connectedEntityIds(selectedEntityId, selectedEdgeId);
|
|
469
|
+
|
|
470
|
+
state.edges.forEach(function (edge) {
|
|
471
|
+
var from = state.positions.get(edge.from);
|
|
472
|
+
var to = state.positions.get(edge.to);
|
|
473
|
+
if (!from || !to) return;
|
|
474
|
+
|
|
475
|
+
var group = svgEl("g");
|
|
476
|
+
var visible = state.visibleEdgeIds.has(edge.id);
|
|
477
|
+
var connected = selectedEdgeId === edge.id || connectedIds.edges.has(edge.id);
|
|
478
|
+
var line = svgEl("path", {
|
|
479
|
+
d: edgePath(from, to),
|
|
480
|
+
class: classNames("edge-line", "review-" + reviewStatus(edge).replace(/\s+/g, "-"), !visible && "filtered", connected && "connected", selectedEdgeId === edge.id && "selected")
|
|
481
|
+
});
|
|
482
|
+
var hit = svgEl("path", {
|
|
483
|
+
d: edgePath(from, to),
|
|
484
|
+
class: "edge-hit"
|
|
485
|
+
});
|
|
486
|
+
hit.addEventListener("click", function () {
|
|
487
|
+
state.selected = { kind: "edge", id: edge.id };
|
|
488
|
+
render();
|
|
489
|
+
});
|
|
490
|
+
hit.addEventListener("mousedown", function (event) {
|
|
491
|
+
event.stopPropagation();
|
|
492
|
+
});
|
|
493
|
+
group.appendChild(line);
|
|
494
|
+
group.appendChild(hit);
|
|
495
|
+
els.edgeLayer.appendChild(group);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
state.entities.forEach(function (entity) {
|
|
499
|
+
var pos = state.positions.get(entity.id);
|
|
500
|
+
if (!pos) return;
|
|
501
|
+
|
|
502
|
+
var visible = state.visibleEntityIds.has(entity.id);
|
|
503
|
+
var selected = selectedEntityId === entity.id;
|
|
504
|
+
var connected = connectedIds.entities.has(entity.id);
|
|
505
|
+
var group = svgEl("g", {
|
|
506
|
+
class: classNames("node", "graph-" + (entity.graph_kind || "unknown"), !visible && "filtered", selected && "selected", connected && "connected"),
|
|
507
|
+
transform: "translate(" + pos.x + " " + pos.y + ")"
|
|
508
|
+
});
|
|
509
|
+
var dims = nodeDimensions(entity);
|
|
510
|
+
var rect = svgEl("rect", {
|
|
511
|
+
x: -dims.width / 2,
|
|
512
|
+
y: -dims.height / 2,
|
|
513
|
+
width: dims.width,
|
|
514
|
+
height: dims.height,
|
|
515
|
+
class: "node-body"
|
|
516
|
+
});
|
|
517
|
+
var port = svgEl("circle", {
|
|
518
|
+
cx: -dims.width / 2 + 10,
|
|
519
|
+
cy: 0,
|
|
520
|
+
r: selected ? 4 : 3,
|
|
521
|
+
class: "node-port",
|
|
522
|
+
fill: palette[entity.type] || palette.default
|
|
523
|
+
});
|
|
524
|
+
var titleText = svgEl("text", {
|
|
525
|
+
x: -dims.width / 2 + 22,
|
|
526
|
+
y: -4,
|
|
527
|
+
class: "node-title"
|
|
528
|
+
});
|
|
529
|
+
titleText.textContent = shortName(displayName(entity), dims.labelMax);
|
|
530
|
+
var typeText = svgEl("text", {
|
|
531
|
+
x: -dims.width / 2 + 22,
|
|
532
|
+
y: 13,
|
|
533
|
+
class: "node-type"
|
|
534
|
+
});
|
|
535
|
+
typeText.textContent = nodeKindLabel(entity);
|
|
536
|
+
var title = svgEl("title");
|
|
537
|
+
title.textContent = displayName(entity) + "\n" + (entity.summary || "");
|
|
538
|
+
group.addEventListener("click", function () {
|
|
539
|
+
state.selected = { kind: "entity", id: entity.id };
|
|
540
|
+
render();
|
|
541
|
+
});
|
|
542
|
+
group.addEventListener("mousedown", function (event) {
|
|
543
|
+
event.stopPropagation();
|
|
544
|
+
});
|
|
545
|
+
group.appendChild(title);
|
|
546
|
+
group.appendChild(rect);
|
|
547
|
+
group.appendChild(port);
|
|
548
|
+
group.appendChild(titleText);
|
|
549
|
+
group.appendChild(typeText);
|
|
550
|
+
els.nodeLayer.appendChild(group);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function renderLists() {
|
|
555
|
+
var visibleEntities = state.entities.filter(function (entity) {
|
|
556
|
+
return state.visibleEntityIds.has(entity.id);
|
|
557
|
+
});
|
|
558
|
+
var visibleEdges = state.edges.filter(function (edge) {
|
|
559
|
+
return state.visibleEdgeIds.has(edge.id);
|
|
560
|
+
}).sort(function (a, b) {
|
|
561
|
+
return reviewRank(a) - reviewRank(b);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
els.entityCount.textContent = String(visibleEntities.length);
|
|
565
|
+
els.edgeCount.textContent = String(visibleEdges.length);
|
|
566
|
+
els.entityList.textContent = "";
|
|
567
|
+
els.edgeList.textContent = "";
|
|
568
|
+
|
|
569
|
+
visibleEntities.forEach(function (entity) {
|
|
570
|
+
var button = document.createElement("button");
|
|
571
|
+
button.type = "button";
|
|
572
|
+
button.className = classNames("list-item", state.selected && state.selected.kind === "entity" && state.selected.id === entity.id && "selected");
|
|
573
|
+
button.innerHTML = "<span class=\"item-title\"></span><span class=\"item-meta\"></span>";
|
|
574
|
+
button.querySelector(".item-title").textContent = displayName(entity);
|
|
575
|
+
button.querySelector(".item-meta").textContent = (entity.type || "unknown") + " | " + entity.id;
|
|
576
|
+
button.addEventListener("click", function () {
|
|
577
|
+
state.selected = { kind: "entity", id: entity.id };
|
|
578
|
+
render();
|
|
579
|
+
});
|
|
580
|
+
els.entityList.appendChild(button);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
visibleEdges.forEach(function (edge) {
|
|
584
|
+
var button = document.createElement("button");
|
|
585
|
+
button.type = "button";
|
|
586
|
+
button.className = classNames("list-item", state.selected && state.selected.kind === "edge" && state.selected.id === edge.id && "selected");
|
|
587
|
+
button.innerHTML = "<span class=\"item-title\"></span><span class=\"item-meta\"></span>";
|
|
588
|
+
button.querySelector(".item-title").textContent = edge.relation || "related";
|
|
589
|
+
button.querySelector(".item-meta").textContent = displayName(state.entityById.get(edge.from)) + " -> " + displayName(state.entityById.get(edge.to)) + " | " + reviewStatus(edge);
|
|
590
|
+
button.addEventListener("click", function () {
|
|
591
|
+
state.selected = { kind: "edge", id: edge.id };
|
|
592
|
+
render();
|
|
593
|
+
});
|
|
594
|
+
els.edgeList.appendChild(button);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function renderDetails() {
|
|
599
|
+
if (!state.selected) {
|
|
600
|
+
els.selectionDetails.className = "details-empty";
|
|
601
|
+
els.selectionDetails.textContent = "Select an entity or edge.";
|
|
602
|
+
els.selectionStatus.textContent = "No selection";
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
var item = state.selected.kind === "entity"
|
|
607
|
+
? state.entityById.get(state.selected.id)
|
|
608
|
+
: state.edges.find(function (edge) { return edge.id === state.selected.id; });
|
|
609
|
+
|
|
610
|
+
if (!item) {
|
|
611
|
+
els.selectionDetails.textContent = "Selection no longer exists.";
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
els.selectionDetails.className = "";
|
|
616
|
+
els.selectionDetails.textContent = "";
|
|
617
|
+
var title = document.createElement("div");
|
|
618
|
+
title.className = "detail-title";
|
|
619
|
+
title.textContent = state.selected.kind === "entity" ? displayName(item) : (item.relation || "related");
|
|
620
|
+
var kind = document.createElement("div");
|
|
621
|
+
kind.className = "detail-kind";
|
|
622
|
+
kind.textContent = state.selected.kind === "entity" ? (item.type || "unknown") : "edge";
|
|
623
|
+
var rows = document.createElement("dl");
|
|
624
|
+
rows.appendChild(detailRow("ID", item.id));
|
|
625
|
+
els.selectionStatus.textContent = state.selected.kind === "entity" ? "Node" : reviewStatus(item);
|
|
626
|
+
|
|
627
|
+
if (state.selected.kind === "entity") {
|
|
628
|
+
rows.appendChild(detailRow("Summary", item.summary || ""));
|
|
629
|
+
rows.appendChild(detailRow("Graph", item.graph_kind || ""));
|
|
630
|
+
rows.appendChild(detailRow("Aliases", Array.isArray(item.aliases) ? item.aliases.join(", ") : ""));
|
|
631
|
+
rows.appendChild(detailRow("Evidence", formatEvidence(item.evidence)));
|
|
632
|
+
rows.appendChild(detailRow("First seen", item.first_seen_at || ""));
|
|
633
|
+
rows.appendChild(detailRow("Last seen", item.last_seen_at || ""));
|
|
634
|
+
} else {
|
|
635
|
+
rows.appendChild(detailRow("From", displayName(state.entityById.get(item.from)) + " (" + item.from + ")"));
|
|
636
|
+
rows.appendChild(detailRow("To", displayName(state.entityById.get(item.to)) + " (" + item.to + ")"));
|
|
637
|
+
rows.appendChild(detailRow("Fact", item.fact || ""));
|
|
638
|
+
rows.appendChild(detailRow("Graph", item.graph_kind || ""));
|
|
639
|
+
rows.appendChild(detailRow("Review", reviewStatus(item)));
|
|
640
|
+
rows.appendChild(detailRow("Confidence", item.confidence == null ? "" : String(item.confidence)));
|
|
641
|
+
rows.appendChild(detailRow("Evidence", formatEvidence(item.evidence)));
|
|
642
|
+
rows.appendChild(detailRow("Valid from", item.valid_from || ""));
|
|
643
|
+
rows.appendChild(detailRow("Invalidated at", item.invalidated_at || ""));
|
|
644
|
+
rows.appendChild(detailRow("Commit", item.commit || ""));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
els.selectionDetails.appendChild(title);
|
|
648
|
+
els.selectionDetails.appendChild(kind);
|
|
649
|
+
els.selectionDetails.appendChild(rows);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function detailRow(label, value) {
|
|
653
|
+
var wrapper = document.createElement("div");
|
|
654
|
+
var term = document.createElement("dt");
|
|
655
|
+
var description = document.createElement("dd");
|
|
656
|
+
wrapper.className = "detail-row";
|
|
657
|
+
term.textContent = label;
|
|
658
|
+
description.textContent = value || "n/a";
|
|
659
|
+
wrapper.appendChild(term);
|
|
660
|
+
wrapper.appendChild(description);
|
|
661
|
+
return wrapper;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function formatEvidence(evidence) {
|
|
665
|
+
if (!Array.isArray(evidence) || evidence.length === 0) return "";
|
|
666
|
+
return evidence.map(function (id) {
|
|
667
|
+
var episode = state.episodesById.get(id);
|
|
668
|
+
return episode && episode.summary ? id + ": " + episode.summary : id;
|
|
669
|
+
}).join("\n");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function reviewStatus(item) {
|
|
673
|
+
if (item.invalidated_at) return "invalidated";
|
|
674
|
+
if (item.confidence != null && item.confidence < 0.75) return "low confidence";
|
|
675
|
+
if (Array.isArray(item.evidence) && item.evidence.length === 0) return "missing evidence";
|
|
676
|
+
return "ok";
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function reviewRank(item) {
|
|
680
|
+
var status = reviewStatus(item);
|
|
681
|
+
if (status === "invalidated") return 0;
|
|
682
|
+
if (status === "missing evidence") return 1;
|
|
683
|
+
if (status === "low confidence") return 2;
|
|
684
|
+
return 3;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function renderMetrics() {
|
|
688
|
+
if (!els.metricsSummary) return;
|
|
689
|
+
var visibleEdges = state.edges.filter(function (edge) { return state.visibleEdgeIds.has(edge.id); });
|
|
690
|
+
var visibleEntities = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
|
|
691
|
+
var evidenceEdges = visibleEdges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length > 0; }).length;
|
|
692
|
+
var official = state.metrics;
|
|
693
|
+
var metrics = official ? [
|
|
694
|
+
["Readiness", official.harness.readiness_score + "/100"],
|
|
695
|
+
["Tokens Saved", official.savings ? official.savings.estimated_tokens_saved_per_recall : "n/a"],
|
|
696
|
+
["Code Files", official.code_graph.files],
|
|
697
|
+
["Memory Edges", official.memory_graph.edges],
|
|
698
|
+
["Quality", official.memory_graph.average_quality_score + "/100"],
|
|
699
|
+
["Evidence", official.memory_graph.evidence_coverage_percent + "%"]
|
|
700
|
+
] : [
|
|
701
|
+
["Nodes", visibleEntities.length + "/" + state.entities.length],
|
|
702
|
+
["Relations", visibleEdges.length + "/" + state.edges.length],
|
|
703
|
+
["Memory Nodes", visibleEntities.filter(function (entity) { return entity.graph_kind === "memory"; }).length],
|
|
704
|
+
["Code Nodes", visibleEntities.filter(function (entity) { return entity.graph_kind === "code"; }).length],
|
|
705
|
+
["Evidence", visibleEdges.length ? Math.round(evidenceEdges / visibleEdges.length * 100) + "%" : "n/a"],
|
|
706
|
+
["Review Flags", visibleEdges.filter(function (edge) { return reviewStatus(edge) !== "ok"; }).length]
|
|
707
|
+
];
|
|
708
|
+
els.metricsSummary.textContent = "";
|
|
709
|
+
metrics.forEach(function (metric) {
|
|
710
|
+
var div = document.createElement("div");
|
|
711
|
+
div.className = "metric";
|
|
712
|
+
div.innerHTML = "<strong></strong><span></span>";
|
|
713
|
+
div.querySelector("strong").textContent = metric[1];
|
|
714
|
+
div.querySelector("span").textContent = metric[0];
|
|
715
|
+
els.metricsSummary.appendChild(div);
|
|
716
|
+
});
|
|
717
|
+
renderStatusStrip(visibleEntities, visibleEdges, official);
|
|
718
|
+
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.";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function renderReviewQueue() {
|
|
723
|
+
if (!els.reviewList) return;
|
|
724
|
+
var packets = state.pendingPackets || [];
|
|
725
|
+
els.reviewCount.textContent = String(packets.length);
|
|
726
|
+
els.reviewList.textContent = "";
|
|
727
|
+
if (!packets.length && !state.reviewText) {
|
|
728
|
+
els.reviewList.className = "review-list details-empty";
|
|
729
|
+
els.reviewList.textContent = "No pending packets loaded. Launch with `kage viewer --project <repo>` to load review context automatically.";
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
els.reviewList.className = "review-list";
|
|
733
|
+
packets.forEach(function (packet) {
|
|
734
|
+
var item = document.createElement("div");
|
|
735
|
+
item.className = "review-item";
|
|
736
|
+
var quality = packet.quality || {};
|
|
737
|
+
item.innerHTML = [
|
|
738
|
+
"<div class=\"review-title\"></div>",
|
|
739
|
+
"<div class=\"review-meta\"></div>",
|
|
740
|
+
"<div class=\"review-summary\"></div>",
|
|
741
|
+
"<div class=\"review-risks\"></div>"
|
|
742
|
+
].join("");
|
|
743
|
+
item.querySelector(".review-title").textContent = packet.title || packet.id;
|
|
744
|
+
item.querySelector(".review-meta").textContent = [packet.type, packet.status, "score " + (quality.score == null ? "n/a" : quality.score + "/100")].filter(Boolean).join(" | ");
|
|
745
|
+
item.querySelector(".review-summary").textContent = packet.summary || "";
|
|
746
|
+
item.querySelector(".review-risks").textContent = Array.isArray(quality.risks) && quality.risks.length ? "risks: " + quality.risks.join(", ") : "risks: none";
|
|
747
|
+
els.reviewList.appendChild(item);
|
|
748
|
+
});
|
|
749
|
+
if (state.reviewText) {
|
|
750
|
+
var artifact = document.createElement("details");
|
|
751
|
+
artifact.className = "review-artifact";
|
|
752
|
+
artifact.innerHTML = "<summary>Review artifact markdown</summary><pre></pre>";
|
|
753
|
+
artifact.querySelector("pre").textContent = state.reviewText.slice(0, 12000);
|
|
754
|
+
els.reviewList.appendChild(artifact);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function renderProof() {
|
|
759
|
+
if (!els.proofList) return;
|
|
760
|
+
var metrics = state.metrics;
|
|
761
|
+
els.proofList.textContent = "";
|
|
762
|
+
if (!metrics) {
|
|
763
|
+
els.proofStatus.textContent = "not loaded";
|
|
764
|
+
els.proofList.className = "proof-list details-empty";
|
|
765
|
+
els.proofList.textContent = "Metrics not loaded. Run `kage metrics --project <repo> --json > .agent_memory/metrics.json` or launch with `kage viewer`.";
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
els.proofStatus.textContent = "loaded";
|
|
769
|
+
els.proofList.className = "proof-list";
|
|
770
|
+
var rows = [
|
|
771
|
+
["Readiness", metrics.harness && metrics.harness.readiness_score != null ? metrics.harness.readiness_score + "/100" : "n/a"],
|
|
772
|
+
["Useful memory", metrics.quality ? metrics.quality.useful_memory_ratio_percent + "%" : "n/a"],
|
|
773
|
+
["Evidence", metrics.memory_graph ? metrics.memory_graph.evidence_coverage_percent + "%" : "n/a"],
|
|
774
|
+
["Pending review", metrics.memory_graph ? String(metrics.memory_graph.pending_packets) : "n/a"],
|
|
775
|
+
["Recall hit rate", metrics.pain ? metrics.pain.recall_hit_rate_percent + "%" : "n/a"],
|
|
776
|
+
["Tokens saved", metrics.pain ? String(metrics.pain.estimated_tokens_saved) : metrics.savings ? String(metrics.savings.estimated_tokens_saved_per_recall) : "n/a"]
|
|
777
|
+
];
|
|
778
|
+
rows.forEach(function (row) {
|
|
779
|
+
var item = document.createElement("div");
|
|
780
|
+
item.className = "proof-item";
|
|
781
|
+
item.innerHTML = "<strong></strong><span></span>";
|
|
782
|
+
item.querySelector("strong").textContent = row[1];
|
|
783
|
+
item.querySelector("span").textContent = row[0];
|
|
784
|
+
els.proofList.appendChild(item);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function renderStatusStrip(visibleEntities, visibleEdges, official) {
|
|
789
|
+
if (!els.statusStrip) return;
|
|
790
|
+
var memoryCount = visibleEntities.filter(function (entity) { return entity.graph_kind === "memory"; }).length;
|
|
791
|
+
var codeCount = visibleEntities.filter(function (entity) { return entity.graph_kind === "code"; }).length;
|
|
792
|
+
var reviewFlags = visibleEdges.filter(function (edge) { return reviewStatus(edge) !== "ok"; }).length;
|
|
793
|
+
var pills = official ? [
|
|
794
|
+
["Readiness", official.harness.readiness_score + "/100", ""],
|
|
795
|
+
["Pending", official.memory_graph ? String(official.memory_graph.pending_packets) : "n/a", official.memory_graph && official.memory_graph.pending_packets ? "warn" : ""],
|
|
796
|
+
["Tokens saved", official.savings ? String(official.savings.estimated_tokens_saved_per_recall) : "n/a", "warn"],
|
|
797
|
+
["Quality", official.memory_graph.average_quality_score + "/100", "memory"],
|
|
798
|
+
["Parser coverage", official.code_graph.indexer_coverage_percent + "%", "code"]
|
|
799
|
+
] : [
|
|
800
|
+
["Memory", String(memoryCount), "memory"],
|
|
801
|
+
["Code", String(codeCount), "code"],
|
|
802
|
+
["Relations", String(visibleEdges.length), ""],
|
|
803
|
+
["Review flags", String(reviewFlags), reviewFlags ? "warn" : ""]
|
|
804
|
+
];
|
|
805
|
+
els.statusStrip.textContent = "";
|
|
806
|
+
pills.forEach(function (pill) {
|
|
807
|
+
var span = document.createElement("span");
|
|
808
|
+
span.className = classNames("status-pill", pill[2]);
|
|
809
|
+
span.innerHTML = "<strong></strong><span></span>";
|
|
810
|
+
span.querySelector("strong").textContent = pill[1];
|
|
811
|
+
span.querySelector("span").textContent = pill[0];
|
|
812
|
+
els.statusStrip.appendChild(span);
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function connectedEntityIds(entityId, edgeId) {
|
|
817
|
+
var entities = new Set();
|
|
818
|
+
var edges = new Set();
|
|
819
|
+
|
|
820
|
+
state.edges.forEach(function (edge) {
|
|
821
|
+
if (entityId && (edge.from === entityId || edge.to === entityId)) {
|
|
822
|
+
edges.add(edge.id);
|
|
823
|
+
entities.add(edge.from);
|
|
824
|
+
entities.add(edge.to);
|
|
825
|
+
}
|
|
826
|
+
if (edgeId && edge.id === edgeId) {
|
|
827
|
+
entities.add(edge.from);
|
|
828
|
+
entities.add(edge.to);
|
|
829
|
+
edges.add(edge.id);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
return { entities: entities, edges: edges };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function degreeOf(id) {
|
|
837
|
+
return state.edges.reduce(function (sum, edge) {
|
|
838
|
+
return sum + (edge.from === id || edge.to === id ? 1 : 0);
|
|
839
|
+
}, 0);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function fitView() {
|
|
843
|
+
var visible = state.entities.filter(function (entity) {
|
|
844
|
+
return state.visibleEntityIds.size === 0 || state.visibleEntityIds.has(entity.id);
|
|
845
|
+
});
|
|
846
|
+
var points = visible.map(function (entity) { return state.positions.get(entity.id); }).filter(Boolean);
|
|
847
|
+
if (!points.length) {
|
|
848
|
+
state.viewBox = { x: 0, y: 0, width: 1000, height: 660 };
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
var xs = points.map(function (point) { return point.x; });
|
|
852
|
+
var ys = points.map(function (point) { return point.y; });
|
|
853
|
+
var minX = Math.min.apply(null, xs) - 130;
|
|
854
|
+
var maxX = Math.max.apply(null, xs) + 150;
|
|
855
|
+
var minY = Math.min.apply(null, ys) - 82;
|
|
856
|
+
var maxY = Math.max.apply(null, ys) + 82;
|
|
857
|
+
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))
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function zoomView(factor) {
|
|
866
|
+
var box = state.viewBox;
|
|
867
|
+
var nextWidth = clamp(box.width * factor, 260, 1400);
|
|
868
|
+
var nextHeight = clamp(box.height * factor, 210, 950);
|
|
869
|
+
state.viewBox = {
|
|
870
|
+
x: box.x + (box.width - nextWidth) / 2,
|
|
871
|
+
y: box.y + (box.height - nextHeight) / 2,
|
|
872
|
+
width: nextWidth,
|
|
873
|
+
height: nextHeight
|
|
874
|
+
};
|
|
875
|
+
renderSvg();
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function startPan(event) {
|
|
879
|
+
if (event.button !== 0) return;
|
|
880
|
+
state.pan = {
|
|
881
|
+
x: event.clientX,
|
|
882
|
+
y: event.clientY,
|
|
883
|
+
viewBox: Object.assign({}, state.viewBox),
|
|
884
|
+
moved: false
|
|
885
|
+
};
|
|
886
|
+
els.svg.classList.add("dragging");
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function continuePan(event) {
|
|
890
|
+
if (!state.pan) return;
|
|
891
|
+
var rect = els.svg.getBoundingClientRect();
|
|
892
|
+
var dx = (event.clientX - state.pan.x) / Math.max(rect.width, 1) * state.pan.viewBox.width;
|
|
893
|
+
var dy = (event.clientY - state.pan.y) / Math.max(rect.height, 1) * state.pan.viewBox.height;
|
|
894
|
+
if (Math.abs(dx) > 1 || Math.abs(dy) > 1) state.pan.moved = true;
|
|
895
|
+
state.viewBox = {
|
|
896
|
+
x: state.pan.viewBox.x - dx,
|
|
897
|
+
y: state.pan.viewBox.y - dy,
|
|
898
|
+
width: state.pan.viewBox.width,
|
|
899
|
+
height: state.pan.viewBox.height
|
|
900
|
+
};
|
|
901
|
+
renderSvg();
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function endPan() {
|
|
905
|
+
if (!state.pan) return;
|
|
906
|
+
window.setTimeout(function () {
|
|
907
|
+
state.pan = null;
|
|
908
|
+
}, 0);
|
|
909
|
+
els.svg.classList.remove("dragging");
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function handleSvgClick(event) {
|
|
913
|
+
if (state.pan && state.pan.moved) return;
|
|
914
|
+
if (event.target !== els.svg) return;
|
|
915
|
+
state.selected = null;
|
|
916
|
+
render();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function handleWheelZoom(event) {
|
|
920
|
+
event.preventDefault();
|
|
921
|
+
var rect = els.svg.getBoundingClientRect();
|
|
922
|
+
var pointX = state.viewBox.x + (event.clientX - rect.left) / Math.max(rect.width, 1) * state.viewBox.width;
|
|
923
|
+
var pointY = state.viewBox.y + (event.clientY - rect.top) / Math.max(rect.height, 1) * state.viewBox.height;
|
|
924
|
+
var factor = event.deltaY > 0 ? 1.12 : 0.88;
|
|
925
|
+
var nextWidth = clamp(state.viewBox.width * factor, 260, 1400);
|
|
926
|
+
var nextHeight = clamp(state.viewBox.height * factor, 210, 950);
|
|
927
|
+
var rx = (pointX - state.viewBox.x) / state.viewBox.width;
|
|
928
|
+
var ry = (pointY - state.viewBox.y) / state.viewBox.height;
|
|
929
|
+
state.viewBox = {
|
|
930
|
+
x: pointX - nextWidth * rx,
|
|
931
|
+
y: pointY - nextHeight * ry,
|
|
932
|
+
width: nextWidth,
|
|
933
|
+
height: nextHeight
|
|
934
|
+
};
|
|
935
|
+
renderSvg();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function displayName(entity) {
|
|
939
|
+
if (!entity) return "Unknown";
|
|
940
|
+
return entity.name || entity.title || entity.path || entity.id || "Unknown";
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function edgePath(from, to) {
|
|
944
|
+
var dx = Math.max(60, Math.abs(to.x - from.x) * 0.48);
|
|
945
|
+
var x1 = from.x + (to.x >= from.x ? 72 : -72);
|
|
946
|
+
var x2 = to.x + (to.x >= from.x ? -72 : 72);
|
|
947
|
+
return "M " + x1 + " " + from.y + " C " + (x1 + (to.x >= from.x ? dx : -dx)) + " " + from.y + ", " + (x2 - (to.x >= from.x ? dx : -dx)) + " " + to.y + ", " + x2 + " " + to.y;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function nodeDimensions(entity) {
|
|
951
|
+
var name = displayName(entity);
|
|
952
|
+
var width = clamp(116 + Math.min(name.length, 26) * 4.5, 142, entity.graph_kind === "memory" ? 210 : 190);
|
|
953
|
+
return {
|
|
954
|
+
width: width,
|
|
955
|
+
height: 42,
|
|
956
|
+
labelMax: width > 180 ? 28 : 22
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function nodeKindLabel(entity) {
|
|
961
|
+
return (entity.graph_kind || "graph") + " / " + (entity.type || "node");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function searchableText(value) {
|
|
965
|
+
return normalize(JSON.stringify(value || {}));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function shortName(value, max) {
|
|
969
|
+
var text = String(value || "");
|
|
970
|
+
return text.length > max ? text.slice(0, Math.max(1, max - 1)) + "..." : text;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function normalize(value) {
|
|
974
|
+
return String(value || "").toLowerCase();
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function unique(values) {
|
|
978
|
+
return Array.from(new Set(values.filter(Boolean)));
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function clamp(value, min, max) {
|
|
982
|
+
return Math.max(min, Math.min(max, value));
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function classNames() {
|
|
986
|
+
return Array.prototype.slice.call(arguments).filter(Boolean).join(" ");
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function svgEl(name, attrs) {
|
|
990
|
+
var element = document.createElementNS("http://www.w3.org/2000/svg", name);
|
|
991
|
+
Object.keys(attrs || {}).forEach(function (key) {
|
|
992
|
+
element.setAttribute(key, attrs[key]);
|
|
993
|
+
});
|
|
994
|
+
return element;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function showError(message) {
|
|
998
|
+
els.graphSummary.textContent = message;
|
|
999
|
+
}
|
|
1000
|
+
})();
|