@kage-core/kage-graph-mcp 1.1.34 → 1.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -129
- package/package.json +1 -1
- package/viewer/app.js +1507 -114
- package/viewer/data.html +303 -0
- package/viewer/graph.html +303 -0
- package/viewer/index.html +250 -123
- package/viewer/intel.html +303 -0
- package/viewer/memory.html +303 -0
- package/viewer/owners.html +303 -0
- package/viewer/review.html +303 -0
- package/viewer/styles.css +1304 -109
package/viewer/app.js
CHANGED
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
visibleEntityIds: new Set(),
|
|
14
14
|
visibleEdgeIds: new Set(),
|
|
15
15
|
selected: null,
|
|
16
|
+
viewerPage: "overview",
|
|
17
|
+
viewerSection: "overview",
|
|
18
|
+
viewerAction: null,
|
|
19
|
+
graphActionFilter: "",
|
|
20
|
+
pathHighlight: {
|
|
21
|
+
nodes: new Set(),
|
|
22
|
+
edges: new Set(),
|
|
23
|
+
direction: "",
|
|
24
|
+
steps: []
|
|
25
|
+
},
|
|
16
26
|
metrics: null,
|
|
17
27
|
inbox: null,
|
|
18
28
|
reports: {
|
|
@@ -119,10 +129,20 @@
|
|
|
119
129
|
graphSubhead: document.getElementById("graphSubhead"),
|
|
120
130
|
selectionStatus: document.getElementById("selectionStatus"),
|
|
121
131
|
searchInput: document.getElementById("searchInput"),
|
|
132
|
+
pathFromInput: document.getElementById("pathFromInput"),
|
|
133
|
+
pathToInput: document.getElementById("pathToInput"),
|
|
134
|
+
pathNodeOptions: document.getElementById("pathNodeOptions"),
|
|
135
|
+
findPath: document.getElementById("findPath"),
|
|
136
|
+
clearPath: document.getElementById("clearPath"),
|
|
137
|
+
pathStatus: document.getElementById("pathStatus"),
|
|
138
|
+
pathResult: document.getElementById("pathResult"),
|
|
122
139
|
viewMode: document.getElementById("viewMode"),
|
|
123
140
|
renderMode: document.getElementById("renderMode"),
|
|
124
141
|
typeFilter: document.getElementById("typeFilter"),
|
|
125
142
|
relationFilter: document.getElementById("relationFilter"),
|
|
143
|
+
showUntrusted: document.getElementById("showUntrusted"),
|
|
144
|
+
showUncovered: document.getElementById("showUncovered"),
|
|
145
|
+
showMemoryCode: document.getElementById("showMemoryCode"),
|
|
126
146
|
scopeFilter: document.getElementById("scopeFilter"),
|
|
127
147
|
maxNodes: document.getElementById("maxNodes"),
|
|
128
148
|
showDependencies: document.getElementById("showDependencies"),
|
|
@@ -143,17 +163,75 @@
|
|
|
143
163
|
entityList: document.getElementById("entityList"),
|
|
144
164
|
edgeList: document.getElementById("edgeList"),
|
|
145
165
|
metricsSummary: document.getElementById("metricsSummary"),
|
|
166
|
+
graphInsightStatus: document.getElementById("graphInsightStatus"),
|
|
167
|
+
graphInsights: document.getElementById("graphInsights"),
|
|
146
168
|
entityCount: document.getElementById("entityCount"),
|
|
147
169
|
edgeCount: document.getElementById("edgeCount"),
|
|
148
170
|
reviewCount: document.getElementById("reviewCount"),
|
|
171
|
+
dashboardStats: document.getElementById("dashboardStats"),
|
|
172
|
+
dashboardCharts: document.getElementById("dashboardCharts"),
|
|
173
|
+
memoryStatus: document.getElementById("memoryStatus"),
|
|
174
|
+
memoryStats: document.getElementById("memoryStats"),
|
|
175
|
+
memoryOverview: document.getElementById("memoryOverview"),
|
|
176
|
+
memorySearch: document.getElementById("memorySearch"),
|
|
177
|
+
memoryFilter: document.getElementById("memoryFilter"),
|
|
178
|
+
memoryList: document.getElementById("memoryList"),
|
|
179
|
+
ownersStatus: document.getElementById("ownersStatus"),
|
|
180
|
+
ownersSummary: document.getElementById("ownersSummary"),
|
|
181
|
+
ownersList: document.getElementById("ownersList"),
|
|
182
|
+
reviewOverview: document.getElementById("reviewOverview"),
|
|
149
183
|
reviewList: document.getElementById("reviewList"),
|
|
184
|
+
proofOverview: document.getElementById("proofOverview"),
|
|
150
185
|
proofStatus: document.getElementById("proofStatus"),
|
|
151
186
|
proofList: document.getElementById("proofList"),
|
|
152
187
|
intelligenceStatus: document.getElementById("intelligenceStatus"),
|
|
153
|
-
intelligenceList: document.getElementById("intelligenceList")
|
|
188
|
+
intelligenceList: document.getElementById("intelligenceList"),
|
|
189
|
+
debugOverview: document.getElementById("debugOverview"),
|
|
190
|
+
pageEyebrow: document.getElementById("pageEyebrow"),
|
|
191
|
+
pageTitle: document.getElementById("pageTitle"),
|
|
192
|
+
viewerPageLinks: typeof document.querySelectorAll === "function" ? Array.from(document.querySelectorAll("[data-viewer-page]")) : [],
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
var PAGE_META = {
|
|
196
|
+
overview: {
|
|
197
|
+
eyebrow: "kage://overview",
|
|
198
|
+
title: "Repo dashboard",
|
|
199
|
+
summary: "What is safe to change next, what needs attention, and what is ready to hand off."
|
|
200
|
+
},
|
|
201
|
+
graph: {
|
|
202
|
+
eyebrow: "kage://graph",
|
|
203
|
+
title: "Dependency graph",
|
|
204
|
+
summary: "Search a file or symbol, then follow connected memory, routes, and tests before editing."
|
|
205
|
+
},
|
|
206
|
+
memory: {
|
|
207
|
+
eyebrow: "kage://memory",
|
|
208
|
+
title: "Memory library",
|
|
209
|
+
summary: "Find repo lore by file, feature, bug, command, or decision. Pick a packet to see linked code."
|
|
210
|
+
},
|
|
211
|
+
intel: {
|
|
212
|
+
eyebrow: "kage://risks",
|
|
213
|
+
title: "Risks",
|
|
214
|
+
summary: "Files, owners, and modules to inspect before changes. Each card links into the graph."
|
|
215
|
+
},
|
|
216
|
+
review: {
|
|
217
|
+
eyebrow: "kage://review",
|
|
218
|
+
title: "Review & handoff",
|
|
219
|
+
summary: "Blockers that must clear before another agent or teammate picks up this branch."
|
|
220
|
+
},
|
|
221
|
+
owners: {
|
|
222
|
+
eyebrow: "kage://owners",
|
|
223
|
+
title: "Owners & reviewers",
|
|
224
|
+
summary: "Local git ownership signal. Use it for reviewer routing and bus-factor checks."
|
|
225
|
+
},
|
|
226
|
+
data: {
|
|
227
|
+
eyebrow: "kage://artifacts",
|
|
228
|
+
title: "Artifacts & diagnostics",
|
|
229
|
+
summary: "Raw nodes, relations, and indexing health. Use only when graph or recall looks wrong."
|
|
230
|
+
}
|
|
154
231
|
};
|
|
155
232
|
|
|
156
233
|
var MEMORY_CODE_RELATIONS = new Set(["explains_symbol", "informs_symbol", "fixes_symbol", "applies_to_route", "verified_by_test", "affects_code_path"]);
|
|
234
|
+
var MEMORY_PACKET_TYPES = new Set(["memory", "command", "repo_map", "runbook", "bug_fix", "decision", "rationale", "convention", "workflow", "gotcha", "reference", "policy", "issue_context", "code_explanation", "negative_result", "constraint"]);
|
|
157
235
|
var INSPECTOR_CONNECTION_LIMIT = 8;
|
|
158
236
|
var PATH_BRIDGE_EDGE_LIMIT_PER_PATH = 8;
|
|
159
237
|
var PATH_BRIDGE_EDGE_LIMIT_TOTAL = 160;
|
|
@@ -161,21 +239,40 @@
|
|
|
161
239
|
var VISIBLE_EDGE_MIN = 160;
|
|
162
240
|
var VISIBLE_EDGE_MAX = 560;
|
|
163
241
|
|
|
242
|
+
state.viewerPage = initialViewerPage();
|
|
243
|
+
applyViewerPage(state.viewerPage);
|
|
244
|
+
els.viewerPageLinks.forEach(function (link) {
|
|
245
|
+
link.addEventListener("click", function (event) {
|
|
246
|
+
var page = link.getAttribute("data-viewer-page") || "overview";
|
|
247
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return;
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
navigateViewerPage(page);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
164
252
|
els.graphFile.addEventListener("change", handleFile);
|
|
165
253
|
els.searchInput.addEventListener("input", scheduleRender);
|
|
166
|
-
els.
|
|
254
|
+
els.findPath.addEventListener("click", findDependencyPath);
|
|
255
|
+
els.clearPath.addEventListener("click", clearDependencyPath);
|
|
256
|
+
els.pathFromInput.addEventListener("keydown", function (event) { if (event.key === "Enter") findDependencyPath(); });
|
|
257
|
+
els.pathToInput.addEventListener("keydown", function (event) { if (event.key === "Enter") findDependencyPath(); });
|
|
258
|
+
els.viewMode.addEventListener("change", function () { clearGraphActionFilter(); render(); });
|
|
167
259
|
els.renderMode.addEventListener("change", function () {
|
|
168
260
|
state.lastVisibleSignature = "";
|
|
169
261
|
render();
|
|
170
262
|
});
|
|
171
|
-
els.typeFilter.addEventListener("change", render);
|
|
172
|
-
els.relationFilter.addEventListener("change", render);
|
|
263
|
+
els.typeFilter.addEventListener("change", function () { clearGraphActionFilter(); render(); });
|
|
264
|
+
els.relationFilter.addEventListener("change", function () { clearGraphActionFilter(); render(); });
|
|
173
265
|
els.scopeFilter.addEventListener("change", render);
|
|
174
266
|
els.maxNodes.addEventListener("change", render);
|
|
175
267
|
els.showDependencies.addEventListener("change", render);
|
|
268
|
+
if (els.showUntrusted) els.showUntrusted.addEventListener("click", function () { applyGraphActionFilter("untrusted"); });
|
|
269
|
+
if (els.showUncovered) els.showUncovered.addEventListener("click", function () { applyGraphActionFilter("uncovered"); });
|
|
270
|
+
if (els.showMemoryCode) els.showMemoryCode.addEventListener("click", function () { applyGraphActionFilter("memory-code"); });
|
|
176
271
|
els.zoomOut.addEventListener("click", function () { zoomGraph(0.82); });
|
|
177
272
|
els.zoomIn.addEventListener("click", function () { zoomGraph(1.22); });
|
|
178
273
|
els.fitView.addEventListener("click", fitActiveGraph);
|
|
274
|
+
if (els.memorySearch) els.memorySearch.addEventListener("input", renderMemoryLibrary);
|
|
275
|
+
if (els.memoryFilter) els.memoryFilter.addEventListener("change", renderMemoryLibrary);
|
|
179
276
|
els.canvas.addEventListener("mousedown", startCanvasPointer);
|
|
180
277
|
els.canvas.addEventListener("mousemove", moveCanvasPointer);
|
|
181
278
|
els.canvas.addEventListener("mouseup", endCanvasPointer);
|
|
@@ -196,7 +293,9 @@
|
|
|
196
293
|
window.addEventListener("resize", function () {
|
|
197
294
|
resizeActiveGraph();
|
|
198
295
|
});
|
|
199
|
-
els.resetView.addEventListener("click",
|
|
296
|
+
els.resetView.addEventListener("click", resetGraphView);
|
|
297
|
+
|
|
298
|
+
function resetGraphView() {
|
|
200
299
|
els.searchInput.value = "";
|
|
201
300
|
els.viewMode.value = "combined";
|
|
202
301
|
els.renderMode.value = "2d";
|
|
@@ -206,11 +305,173 @@
|
|
|
206
305
|
els.maxNodes.value = "90";
|
|
207
306
|
els.showDependencies.checked = false;
|
|
208
307
|
state.selected = null;
|
|
308
|
+
state.graphActionFilter = "";
|
|
309
|
+
clearDependencyPath(false);
|
|
209
310
|
state.lastVisibleSignature = "";
|
|
210
311
|
render();
|
|
211
|
-
}
|
|
312
|
+
}
|
|
212
313
|
loadFromUrlParams();
|
|
213
314
|
|
|
315
|
+
function initialViewerPage() {
|
|
316
|
+
var fileName = "";
|
|
317
|
+
try {
|
|
318
|
+
fileName = String(window.location.pathname || "").split("/").pop() || "index.html";
|
|
319
|
+
} catch (_error) {
|
|
320
|
+
fileName = "index.html";
|
|
321
|
+
}
|
|
322
|
+
var pageByFile = {
|
|
323
|
+
"index.html": "overview",
|
|
324
|
+
"": "overview",
|
|
325
|
+
"graph.html": "graph",
|
|
326
|
+
"memory.html": "memory",
|
|
327
|
+
"owners.html": "owners",
|
|
328
|
+
"intel.html": "intel",
|
|
329
|
+
"review.html": "review",
|
|
330
|
+
"data.html": "data"
|
|
331
|
+
};
|
|
332
|
+
return pageByFile[fileName] || "overview";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function applyViewerPage(page, updateLinks) {
|
|
336
|
+
var normalized = normalizeViewerPage(page);
|
|
337
|
+
state.viewerPage = normalized;
|
|
338
|
+
if (normalized === "overview") {
|
|
339
|
+
setViewerSection("overview");
|
|
340
|
+
} else if (normalized === "graph") {
|
|
341
|
+
setViewerSection("graph");
|
|
342
|
+
} else {
|
|
343
|
+
setViewerSection("graph", pageToAction(normalized));
|
|
344
|
+
}
|
|
345
|
+
state.viewerPage = normalized;
|
|
346
|
+
applyPageHeader(normalized);
|
|
347
|
+
if (updateLinks !== false) syncViewerPageLinks();
|
|
348
|
+
syncViewerPageClass();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function applyPageHeader(page) {
|
|
352
|
+
var meta = PAGE_META[page] || PAGE_META.overview;
|
|
353
|
+
if (els.pageEyebrow) els.pageEyebrow.textContent = meta.eyebrow;
|
|
354
|
+
if (els.pageTitle) els.pageTitle.textContent = meta.title;
|
|
355
|
+
if (els.graphSummary && !state.graph) els.graphSummary.textContent = meta.summary;
|
|
356
|
+
try {
|
|
357
|
+
if (typeof document !== "undefined" && document.title !== undefined) {
|
|
358
|
+
document.title = "Kage " + meta.title.toLowerCase() + " viewer";
|
|
359
|
+
}
|
|
360
|
+
} catch (_error) {
|
|
361
|
+
// ignore
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function normalizeViewerPage(page) {
|
|
366
|
+
var normalized = String(page || "overview").toLowerCase();
|
|
367
|
+
if (normalized === "intelligence") normalized = "intel";
|
|
368
|
+
if (["overview", "graph", "memory", "owners", "intel", "review", "data"].indexOf(normalized) === -1) return "overview";
|
|
369
|
+
return normalized;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function pageToAction(page) {
|
|
373
|
+
if (page === "intel") return "intelligence";
|
|
374
|
+
if (page === "owners") return "intelligence";
|
|
375
|
+
if (page === "memory") return "memory";
|
|
376
|
+
if (page === "review") return "review";
|
|
377
|
+
if (page === "data") return "data";
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function pageFromSection(section, action) {
|
|
382
|
+
if (section === "overview") return "overview";
|
|
383
|
+
if (action === "intelligence") return "intel";
|
|
384
|
+
if (action === "memory") return "memory";
|
|
385
|
+
if (action === "review") return "review";
|
|
386
|
+
if (action === "data") return "data";
|
|
387
|
+
return "graph";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function viewerPageHref(page) {
|
|
391
|
+
var fileByPage = {
|
|
392
|
+
overview: "./",
|
|
393
|
+
graph: "./graph.html",
|
|
394
|
+
memory: "./memory.html",
|
|
395
|
+
owners: "./owners.html",
|
|
396
|
+
intel: "./intel.html",
|
|
397
|
+
review: "./review.html",
|
|
398
|
+
data: "./data.html"
|
|
399
|
+
};
|
|
400
|
+
var search = "";
|
|
401
|
+
try {
|
|
402
|
+
search = window.location.search || "";
|
|
403
|
+
} catch (_error) {
|
|
404
|
+
search = "";
|
|
405
|
+
}
|
|
406
|
+
return (fileByPage[normalizeViewerPage(page)] || "./") + search;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function navigateViewerPage(page) {
|
|
410
|
+
window.location.href = viewerPageHref(page);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function showViewerPageInPlace(page) {
|
|
414
|
+
applyViewerPage(page);
|
|
415
|
+
try {
|
|
416
|
+
window.history.pushState({}, "", viewerPageHref(page));
|
|
417
|
+
} catch (_error) {
|
|
418
|
+
// Static/file viewers can ignore history failures; visual state is enough.
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function syncViewerPageLinks() {
|
|
423
|
+
els.viewerPageLinks.forEach(function (link) {
|
|
424
|
+
var page = normalizeViewerPage(link.getAttribute("data-viewer-page"));
|
|
425
|
+
link.setAttribute("href", viewerPageHref(page));
|
|
426
|
+
var active = page === state.viewerPage;
|
|
427
|
+
link.classList.toggle("active", active);
|
|
428
|
+
if (active) link.setAttribute("aria-current", "page");
|
|
429
|
+
else link.removeAttribute("aria-current");
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function syncViewerPageClass() {
|
|
434
|
+
if (!document.body || !document.body.classList) return;
|
|
435
|
+
document.body.classList.remove(
|
|
436
|
+
"viewer-page-overview",
|
|
437
|
+
"viewer-page-graph",
|
|
438
|
+
"viewer-page-memory",
|
|
439
|
+
"viewer-page-owners",
|
|
440
|
+
"viewer-page-intel",
|
|
441
|
+
"viewer-page-review",
|
|
442
|
+
"viewer-page-data"
|
|
443
|
+
);
|
|
444
|
+
document.body.classList.add("viewer-page-" + normalizeViewerPage(state.viewerPage));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function setViewerSection(section, action) {
|
|
448
|
+
state.viewerSection = section === "graph" ? "graph" : "overview";
|
|
449
|
+
state.viewerAction = action || null;
|
|
450
|
+
state.viewerPage = pageFromSection(state.viewerSection, state.viewerAction);
|
|
451
|
+
if (state.viewerSection === "overview") closeWorkspace();
|
|
452
|
+
if (document.body && document.body.classList) {
|
|
453
|
+
document.body.classList.remove("viewer-section-overview", "viewer-section-graph");
|
|
454
|
+
document.body.classList.add("viewer-section-" + state.viewerSection);
|
|
455
|
+
}
|
|
456
|
+
syncViewerPageLinks();
|
|
457
|
+
syncViewerPageClass();
|
|
458
|
+
if (state.viewerSection === "graph") resizeActiveGraph();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function closeWorkspace() {
|
|
462
|
+
if (document.body && document.body.classList) {
|
|
463
|
+
document.body.classList.remove("viewer-workspace-open");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function selectEntity(id, openInspector) {
|
|
468
|
+
state.selected = { kind: "entity", id: id };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function selectEdge(id, openInspector) {
|
|
472
|
+
state.selected = { kind: "edge", id: id };
|
|
473
|
+
}
|
|
474
|
+
|
|
214
475
|
function handleFile(event) {
|
|
215
476
|
var files = Array.from(event.target.files || []);
|
|
216
477
|
if (!files.length) return;
|
|
@@ -224,7 +485,8 @@
|
|
|
224
485
|
state.entityById = new Map();
|
|
225
486
|
state.episodesById = new Map();
|
|
226
487
|
els.emptyState.classList.add("hidden");
|
|
227
|
-
|
|
488
|
+
var pageMeta = PAGE_META[state.viewerPage] || PAGE_META.overview;
|
|
489
|
+
els.graphSummary.textContent = state.viewerPage === "graph" ? "Metrics loaded." : pageMeta.summary;
|
|
228
490
|
renderMetrics();
|
|
229
491
|
return;
|
|
230
492
|
}
|
|
@@ -258,11 +520,17 @@
|
|
|
258
520
|
return [episode.id, episode];
|
|
259
521
|
}));
|
|
260
522
|
state.selected = null;
|
|
523
|
+
closeWorkspace();
|
|
524
|
+
clearDependencyPath(false);
|
|
261
525
|
state.lastVisibleSignature = "";
|
|
262
526
|
|
|
263
527
|
populateFilters();
|
|
528
|
+
populatePathOptions();
|
|
264
529
|
els.emptyState.classList.add("hidden");
|
|
265
|
-
|
|
530
|
+
var meta = PAGE_META[state.viewerPage] || PAGE_META.overview;
|
|
531
|
+
els.graphSummary.textContent = state.viewerPage === "graph"
|
|
532
|
+
? fileName + " loaded: " + entities.length + " nodes, " + edges.length + " relations."
|
|
533
|
+
: meta.summary;
|
|
266
534
|
render();
|
|
267
535
|
}
|
|
268
536
|
|
|
@@ -277,6 +545,8 @@
|
|
|
277
545
|
|
|
278
546
|
function loadFromUrlParams() {
|
|
279
547
|
var params = new URLSearchParams(window.location.search);
|
|
548
|
+
var requestedSection = String(params.get("section") || "").toLowerCase();
|
|
549
|
+
if (requestedSection === "graph" || requestedSection === "overview") setViewerSection(requestedSection);
|
|
280
550
|
applyRequestedView(params.get("view") || params.get("mode"));
|
|
281
551
|
applyRequestedRenderMode(params.get("render") || params.get("graphMode"));
|
|
282
552
|
var memoryGraphPaths = splitParamValues(params.getAll("graph"));
|
|
@@ -836,6 +1106,188 @@
|
|
|
836
1106
|
});
|
|
837
1107
|
}
|
|
838
1108
|
|
|
1109
|
+
function populatePathOptions() {
|
|
1110
|
+
if (!els.pathNodeOptions) return;
|
|
1111
|
+
var seen = new Set();
|
|
1112
|
+
els.pathNodeOptions.textContent = "";
|
|
1113
|
+
pathCandidateEntities().slice(0, 500).forEach(function (entity) {
|
|
1114
|
+
[entity.path, displayName(entity), entity.id].filter(Boolean).forEach(function (value) {
|
|
1115
|
+
var text = String(value);
|
|
1116
|
+
if (seen.has(text)) return;
|
|
1117
|
+
seen.add(text);
|
|
1118
|
+
var option = document.createElement("option");
|
|
1119
|
+
option.value = text;
|
|
1120
|
+
els.pathNodeOptions.appendChild(option);
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function pathCandidateEntities() {
|
|
1126
|
+
return state.entities
|
|
1127
|
+
.filter(function (entity) {
|
|
1128
|
+
return entity.graph_kind === "code" && ["file", "symbol", "route", "test", "script"].indexOf(entity.type) !== -1;
|
|
1129
|
+
})
|
|
1130
|
+
.sort(function (a, b) {
|
|
1131
|
+
return entityImportance(b) - entityImportance(a) || displayName(a).localeCompare(displayName(b));
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function resolvePathEntity(value) {
|
|
1136
|
+
var query = String(value || "").trim();
|
|
1137
|
+
if (!query) return null;
|
|
1138
|
+
var lower = query.toLowerCase();
|
|
1139
|
+
var candidates = pathCandidateEntities();
|
|
1140
|
+
var exact = candidates.filter(function (entity) {
|
|
1141
|
+
return String(entity.id || "").toLowerCase() === lower ||
|
|
1142
|
+
String(entity.path || "").toLowerCase() === lower ||
|
|
1143
|
+
displayName(entity).toLowerCase() === lower;
|
|
1144
|
+
});
|
|
1145
|
+
if (exact.length) return exact[0];
|
|
1146
|
+
var partial = candidates.filter(function (entity) {
|
|
1147
|
+
return String(entity.path || "").toLowerCase().indexOf(lower) !== -1 ||
|
|
1148
|
+
displayName(entity).toLowerCase().indexOf(lower) !== -1 ||
|
|
1149
|
+
String(entity.id || "").toLowerCase().indexOf(lower) !== -1;
|
|
1150
|
+
});
|
|
1151
|
+
return partial[0] || null;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function codePathEdges() {
|
|
1155
|
+
var relations = new Set(["imports", "imports_external", "defines_symbol", "calls", "covers", "defines_route", "handled_by"]);
|
|
1156
|
+
return state.edges.filter(function (edge) {
|
|
1157
|
+
if (!relations.has(edge.relation)) return false;
|
|
1158
|
+
var from = state.entityById.get(edge.from);
|
|
1159
|
+
var to = state.entityById.get(edge.to);
|
|
1160
|
+
return from && to && from.graph_kind === "code" && to.graph_kind === "code";
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function shortestCodePath(fromId, toId, undirected) {
|
|
1165
|
+
var adjacency = new Map();
|
|
1166
|
+
codePathEdges().forEach(function (edge) {
|
|
1167
|
+
if (!adjacency.has(edge.from)) adjacency.set(edge.from, []);
|
|
1168
|
+
adjacency.get(edge.from).push({ to: edge.to, edge: edge });
|
|
1169
|
+
if (undirected) {
|
|
1170
|
+
if (!adjacency.has(edge.to)) adjacency.set(edge.to, []);
|
|
1171
|
+
adjacency.get(edge.to).push({ to: edge.from, edge: edge });
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
var queue = [fromId];
|
|
1175
|
+
var seen = new Set([fromId]);
|
|
1176
|
+
var previous = new Map();
|
|
1177
|
+
while (queue.length) {
|
|
1178
|
+
var current = queue.shift();
|
|
1179
|
+
if (current === toId) break;
|
|
1180
|
+
(adjacency.get(current) || []).forEach(function (step) {
|
|
1181
|
+
if (seen.has(step.to)) return;
|
|
1182
|
+
seen.add(step.to);
|
|
1183
|
+
previous.set(step.to, { from: current, edge: step.edge });
|
|
1184
|
+
queue.push(step.to);
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
if (!seen.has(toId)) return null;
|
|
1188
|
+
var nodes = [toId];
|
|
1189
|
+
var edges = [];
|
|
1190
|
+
var cursor = toId;
|
|
1191
|
+
while (cursor !== fromId) {
|
|
1192
|
+
var prev = previous.get(cursor);
|
|
1193
|
+
if (!prev) return null;
|
|
1194
|
+
edges.unshift(prev.edge.id);
|
|
1195
|
+
cursor = prev.from;
|
|
1196
|
+
nodes.unshift(cursor);
|
|
1197
|
+
}
|
|
1198
|
+
return { nodes: nodes, edges: edges };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function findDependencyPath() {
|
|
1202
|
+
var from = resolvePathEntity(els.pathFromInput.value);
|
|
1203
|
+
var to = resolvePathEntity(els.pathToInput.value);
|
|
1204
|
+
if (!from || !to) {
|
|
1205
|
+
setPathStatus("Could not resolve both endpoints. Try a file path or exact symbol name.", "warn");
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (from.id === to.id) {
|
|
1209
|
+
setPathStatus("Pick two different code nodes.", "warn");
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
var direction = "forward";
|
|
1213
|
+
var path = shortestCodePath(from.id, to.id, false);
|
|
1214
|
+
if (!path) {
|
|
1215
|
+
var reverse = shortestCodePath(to.id, from.id, false);
|
|
1216
|
+
if (reverse) {
|
|
1217
|
+
direction = "reverse";
|
|
1218
|
+
path = {
|
|
1219
|
+
nodes: reverse.nodes.slice().reverse(),
|
|
1220
|
+
edges: reverse.edges.slice().reverse()
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (!path) {
|
|
1225
|
+
direction = "undirected";
|
|
1226
|
+
path = shortestCodePath(from.id, to.id, true);
|
|
1227
|
+
}
|
|
1228
|
+
if (!path) {
|
|
1229
|
+
state.pathHighlight = { nodes: new Set(), edges: new Set(), direction: "", steps: [] };
|
|
1230
|
+
els.pathResult.textContent = "";
|
|
1231
|
+
els.pathResult.className = "path-result";
|
|
1232
|
+
setPathStatus("No code dependency path found between those nodes.", "warn");
|
|
1233
|
+
render();
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
state.pathHighlight = {
|
|
1237
|
+
nodes: new Set(path.nodes),
|
|
1238
|
+
edges: new Set(path.edges),
|
|
1239
|
+
direction: direction,
|
|
1240
|
+
steps: path.nodes
|
|
1241
|
+
};
|
|
1242
|
+
setPathStatus(path.nodes.length + " nodes, " + path.edges.length + " edge(s), " + direction + " path.", "ok");
|
|
1243
|
+
renderPathResult(path.nodes, path.edges);
|
|
1244
|
+
state.lastVisibleSignature = "";
|
|
1245
|
+
render();
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function setPathStatus(text, status) {
|
|
1249
|
+
els.pathStatus.textContent = text;
|
|
1250
|
+
els.pathStatus.className = "path-status" + (status ? " " + status : "");
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function clearDependencyPath(renderNow) {
|
|
1254
|
+
state.pathHighlight = { nodes: new Set(), edges: new Set(), direction: "", steps: [] };
|
|
1255
|
+
if (els.pathFromInput) els.pathFromInput.value = "";
|
|
1256
|
+
if (els.pathToInput) els.pathToInput.value = "";
|
|
1257
|
+
if (els.pathResult) {
|
|
1258
|
+
els.pathResult.textContent = "";
|
|
1259
|
+
els.pathResult.className = "path-result";
|
|
1260
|
+
}
|
|
1261
|
+
if (els.pathStatus) setPathStatus("Pick two code nodes to trace a dependency path.", "");
|
|
1262
|
+
if (renderNow !== false) {
|
|
1263
|
+
state.lastVisibleSignature = "";
|
|
1264
|
+
render();
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function renderPathResult(nodes, edges) {
|
|
1269
|
+
els.pathResult.textContent = "";
|
|
1270
|
+
nodes.forEach(function (id, index) {
|
|
1271
|
+
var entity = state.entityById.get(id);
|
|
1272
|
+
var edge = index > 0 ? state.edgeById.get(edges[index - 1]) : null;
|
|
1273
|
+
var button = document.createElement("button");
|
|
1274
|
+
button.type = "button";
|
|
1275
|
+
button.className = "path-step";
|
|
1276
|
+
button.innerHTML = "<strong></strong><span></span>";
|
|
1277
|
+
button.querySelector("strong").textContent = entity ? displayName(entity) : id;
|
|
1278
|
+
button.querySelector("span").textContent = [
|
|
1279
|
+
index === 0 ? "start" : edge ? edge.relation : "path",
|
|
1280
|
+
entity && entity.path ? entity.path : id
|
|
1281
|
+
].filter(Boolean).join(" | ");
|
|
1282
|
+
button.addEventListener("click", function () {
|
|
1283
|
+
selectEntity(id, true);
|
|
1284
|
+
render();
|
|
1285
|
+
});
|
|
1286
|
+
els.pathResult.appendChild(button);
|
|
1287
|
+
});
|
|
1288
|
+
els.pathResult.className = "path-result visible";
|
|
1289
|
+
}
|
|
1290
|
+
|
|
839
1291
|
function layoutGraph(visibleIds) {
|
|
840
1292
|
state.positions = new Map();
|
|
841
1293
|
var candidates = state.entities.filter(function (entity) {
|
|
@@ -908,6 +1360,10 @@
|
|
|
908
1360
|
matchedEdgeIds = new Set(state.edges.filter(function (edge) { return mode === "combined" || edge.graph_kind === mode; }).map(function (edge) { return edge.id; }));
|
|
909
1361
|
}
|
|
910
1362
|
|
|
1363
|
+
var actionFiltered = applyMatchedGraphActionFilter(matchedEntityIds, matchedEdgeIds);
|
|
1364
|
+
matchedEntityIds = actionFiltered.entities;
|
|
1365
|
+
matchedEdgeIds = actionFiltered.edges;
|
|
1366
|
+
|
|
911
1367
|
var visible = refineVisibleGraph(matchedEntityIds, matchedEdgeIds, {
|
|
912
1368
|
query: query,
|
|
913
1369
|
type: type,
|
|
@@ -926,12 +1382,7 @@
|
|
|
926
1382
|
state.lastVisibleSignature = nextSignature;
|
|
927
1383
|
|
|
928
1384
|
renderActiveGraph(graphChanged);
|
|
929
|
-
|
|
930
|
-
renderDetails();
|
|
931
|
-
renderMetrics();
|
|
932
|
-
renderReviewQueue();
|
|
933
|
-
renderProof();
|
|
934
|
-
renderIntelligence();
|
|
1385
|
+
renderPagePanels();
|
|
935
1386
|
}
|
|
936
1387
|
|
|
937
1388
|
function scheduleRender() {
|
|
@@ -942,6 +1393,88 @@
|
|
|
942
1393
|
});
|
|
943
1394
|
}
|
|
944
1395
|
|
|
1396
|
+
function renderPagePanels() {
|
|
1397
|
+
if (state.viewerPage === "graph") {
|
|
1398
|
+
renderDetails();
|
|
1399
|
+
renderMetrics();
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
if (state.viewerPage === "memory") {
|
|
1403
|
+
renderDetails();
|
|
1404
|
+
renderMemoryLibrary();
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (state.viewerPage === "owners") {
|
|
1408
|
+
renderOwners();
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (state.viewerPage === "intel") {
|
|
1412
|
+
renderIntelligence();
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (state.viewerPage === "review") {
|
|
1416
|
+
renderReviewQueue();
|
|
1417
|
+
renderProof();
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (state.viewerPage === "data") {
|
|
1421
|
+
renderDetails();
|
|
1422
|
+
renderArtifactDiagnostics(state.entities, state.edges);
|
|
1423
|
+
renderLists();
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
renderDashboard();
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function applyGraphActionFilter(filter) {
|
|
1430
|
+
state.graphActionFilter = state.graphActionFilter === filter ? "" : filter;
|
|
1431
|
+
if (filter === "memory-code") {
|
|
1432
|
+
els.viewMode.value = "combined";
|
|
1433
|
+
els.relationFilter.value = "__memory_code__";
|
|
1434
|
+
} else if (state.graphActionFilter) {
|
|
1435
|
+
els.viewMode.value = "combined";
|
|
1436
|
+
els.relationFilter.value = "";
|
|
1437
|
+
}
|
|
1438
|
+
state.lastVisibleSignature = "";
|
|
1439
|
+
render();
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function clearGraphActionFilter() {
|
|
1443
|
+
state.graphActionFilter = "";
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function applyMatchedGraphActionFilter(entityIds, edgeIds) {
|
|
1447
|
+
if (!state.graphActionFilter) return { entities: entityIds, edges: edgeIds };
|
|
1448
|
+
if (state.graphActionFilter === "memory-code") {
|
|
1449
|
+
var memoryCodeEdges = state.edges.filter(isMemoryCodeEdge);
|
|
1450
|
+
return entitiesForEdges(memoryCodeEdges);
|
|
1451
|
+
}
|
|
1452
|
+
if (state.graphActionFilter === "untrusted") {
|
|
1453
|
+
var flagged = state.edges.filter(function (edge) { return reviewStatus(edge) !== "ok"; });
|
|
1454
|
+
return entitiesForEdges(flagged);
|
|
1455
|
+
}
|
|
1456
|
+
if (state.graphActionFilter === "uncovered") {
|
|
1457
|
+
var covered = memoryLinkedCodeKeys();
|
|
1458
|
+
var uncovered = state.entities.filter(function (entity) {
|
|
1459
|
+
return entity.graph_kind === "code" && entity.type === "file" && !covered.has(codeCoverageKey(entity));
|
|
1460
|
+
});
|
|
1461
|
+
var entities = new Set(uncovered.map(function (entity) { return entity.id; }));
|
|
1462
|
+
return { entities: entities, edges: edgesWithVisibleEndpoints(new Set(state.edges.map(function (edge) { return edge.id; })), entities) };
|
|
1463
|
+
}
|
|
1464
|
+
return { entities: entityIds, edges: edgeIds };
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function entitiesForEdges(edges) {
|
|
1468
|
+
var entities = new Set();
|
|
1469
|
+
var edgeIds = new Set();
|
|
1470
|
+
edges.forEach(function (edge) {
|
|
1471
|
+
edgeIds.add(edge.id);
|
|
1472
|
+
if (state.entityById.has(edge.from)) entities.add(edge.from);
|
|
1473
|
+
if (state.entityById.has(edge.to)) entities.add(edge.to);
|
|
1474
|
+
});
|
|
1475
|
+
return { entities: entities, edges: edgeIds };
|
|
1476
|
+
}
|
|
1477
|
+
|
|
945
1478
|
function refineVisibleGraph(entityIds, edgeIds, options) {
|
|
946
1479
|
var entities = new Set(entityIds);
|
|
947
1480
|
var edges = new Set(edgeIds);
|
|
@@ -981,7 +1514,16 @@
|
|
|
981
1514
|
}
|
|
982
1515
|
}
|
|
983
1516
|
|
|
984
|
-
|
|
1517
|
+
if (state.pathHighlight && state.pathHighlight.nodes.size) {
|
|
1518
|
+
state.pathHighlight.nodes.forEach(function (id) { entities.add(id); });
|
|
1519
|
+
state.pathHighlight.edges.forEach(function (id) { edges.add(id); });
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
var cappedEdges = capVisibleEdges(edgesWithVisibleEndpoints(edges, entities), entities, options);
|
|
1523
|
+
if (state.pathHighlight && state.pathHighlight.edges.size) {
|
|
1524
|
+
state.pathHighlight.edges.forEach(function (id) { if (edges.has(id)) cappedEdges.add(id); });
|
|
1525
|
+
}
|
|
1526
|
+
return { entities: entities, edges: cappedEdges };
|
|
985
1527
|
}
|
|
986
1528
|
|
|
987
1529
|
function capVisibleEdges(edgeIds, entityIds, options) {
|
|
@@ -1444,8 +1986,8 @@
|
|
|
1444
1986
|
ctx.fillStyle = graphPalette.background;
|
|
1445
1987
|
ctx.fillRect(0, 0, width, height);
|
|
1446
1988
|
var gradient = ctx.createRadialGradient(width * 0.52, height * 0.44, 40, width * 0.52, height * 0.44, Math.max(width, height) * 0.72);
|
|
1447
|
-
gradient.addColorStop(0, "rgba(65,255,143,0.
|
|
1448
|
-
gradient.addColorStop(0.48, "rgba(65,255,143,0.
|
|
1989
|
+
gradient.addColorStop(0, "rgba(65,255,143,0.145)");
|
|
1990
|
+
gradient.addColorStop(0.48, "rgba(65,255,143,0.030)");
|
|
1449
1991
|
gradient.addColorStop(1, "rgba(2,5,3,0)");
|
|
1450
1992
|
ctx.fillStyle = gradient;
|
|
1451
1993
|
ctx.fillRect(0, 0, width, height);
|
|
@@ -1484,9 +2026,10 @@
|
|
|
1484
2026
|
var to = nodeMap.get(edge.to);
|
|
1485
2027
|
if (!from || !to) return;
|
|
1486
2028
|
var connected = focusId && (edge.from === focusId || edge.to === focusId);
|
|
2029
|
+
var pathEdge = state.pathHighlight && state.pathHighlight.edges.has(edge.id);
|
|
1487
2030
|
var matches = matchesSearchQuery(edge, query) || matchesSearchQuery(from.entity, query) || matchesSearchQuery(to.entity, query);
|
|
1488
|
-
var alpha = !matches ? 0.035 : focusId ? (connected ? 0.62 : 0.055) : (dense ? 0.13 : 0.22);
|
|
1489
|
-
var color = hexToRgb(edgeThemeColor(edge, from.entity, to.entity));
|
|
2031
|
+
var alpha = pathEdge ? 0.92 : !matches ? 0.035 : focusId ? (connected ? 0.62 : 0.055) : (dense ? 0.13 : 0.22);
|
|
2032
|
+
var color = hexToRgb(pathEdge ? graphPalette.bridge : edgeThemeColor(edge, from.entity, to.entity));
|
|
1490
2033
|
var dx = to.x - from.x;
|
|
1491
2034
|
var dy = to.y - from.y;
|
|
1492
2035
|
var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
|
|
@@ -1497,10 +2040,10 @@
|
|
|
1497
2040
|
ctx.moveTo(from.x, from.y);
|
|
1498
2041
|
ctx.quadraticCurveTo(cx, cy, to.x, to.y);
|
|
1499
2042
|
ctx.strokeStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + alpha + ")";
|
|
1500
|
-
ctx.lineWidth = connected ? 2.2 : 1;
|
|
2043
|
+
ctx.lineWidth = pathEdge ? 3 : connected ? 2.2 : 1;
|
|
1501
2044
|
ctx.stroke();
|
|
1502
|
-
if (connected || (!dense && state.sim.zoom > 1.25)) drawArrow(ctx, from, to, cx, cy, color, alpha);
|
|
1503
|
-
if (connected && state.sim.zoom > 0.62) drawEdgeLabel(ctx, edge, cx, cy);
|
|
2045
|
+
if (pathEdge || connected || (!dense && state.sim.zoom > 1.25)) drawArrow(ctx, from, to, cx, cy, color, alpha);
|
|
2046
|
+
if ((pathEdge || connected) && state.sim.zoom > 0.62) drawEdgeLabel(ctx, edge, cx, cy);
|
|
1504
2047
|
});
|
|
1505
2048
|
}
|
|
1506
2049
|
|
|
@@ -1514,14 +2057,15 @@
|
|
|
1514
2057
|
var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
|
|
1515
2058
|
var hovered = state.sim.hoverNode && state.sim.hoverNode.id === node.id;
|
|
1516
2059
|
var connected = focusId && (node.id === focusId || (focusNeighbors && focusNeighbors.has(node.id)));
|
|
2060
|
+
var pathNode = state.pathHighlight && state.pathHighlight.nodes.has(node.id);
|
|
1517
2061
|
var matches = matchesSearchQuery(entity, query);
|
|
1518
|
-
var alpha = !matches ? 0.12 : focusId && !connected ? 0.20 : 1;
|
|
2062
|
+
var alpha = pathNode ? 1 : !matches ? 0.12 : focusId && !connected ? 0.20 : 1;
|
|
1519
2063
|
var color = nodeThemeColor(entity);
|
|
1520
2064
|
ctx.save();
|
|
1521
2065
|
ctx.globalAlpha = alpha;
|
|
1522
|
-
if (selected || hovered) {
|
|
1523
|
-
ctx.shadowColor = color;
|
|
1524
|
-
ctx.shadowBlur = selected ? 14 : 10;
|
|
2066
|
+
if (selected || hovered || pathNode || entity.graph_kind === "memory") {
|
|
2067
|
+
ctx.shadowColor = pathNode ? graphPalette.bridge : color;
|
|
2068
|
+
ctx.shadowBlur = selected ? 16 : pathNode ? 14 : entity.graph_kind === "memory" ? 9 : 10;
|
|
1525
2069
|
}
|
|
1526
2070
|
drawNodeShape(ctx, node.x, node.y, node.r, entity);
|
|
1527
2071
|
ctx.fillStyle = nodeFillColor(entity);
|
|
@@ -1538,18 +2082,18 @@
|
|
|
1538
2082
|
}
|
|
1539
2083
|
ctx.restore();
|
|
1540
2084
|
|
|
1541
|
-
if (selected || hovered) {
|
|
2085
|
+
if (selected || hovered || pathNode) {
|
|
1542
2086
|
ctx.save();
|
|
1543
2087
|
drawNodeShape(ctx, node.x, node.y, node.r + 4, entity);
|
|
1544
|
-
ctx.strokeStyle = color;
|
|
1545
|
-
ctx.lineWidth = selected ? 2.6 : 1.8;
|
|
1546
|
-
ctx.shadowColor = color;
|
|
2088
|
+
ctx.strokeStyle = pathNode ? graphPalette.bridge : color;
|
|
2089
|
+
ctx.lineWidth = selected ? 2.6 : pathNode ? 2.2 : 1.8;
|
|
2090
|
+
ctx.shadowColor = pathNode ? graphPalette.bridge : color;
|
|
1547
2091
|
ctx.shadowBlur = 8;
|
|
1548
2092
|
ctx.stroke();
|
|
1549
2093
|
ctx.restore();
|
|
1550
2094
|
}
|
|
1551
2095
|
|
|
1552
|
-
var shouldLabel = matches && (selected || hovered || (query.active && matches) || (!dense && state.sim.zoom > 0.75) || (dense && state.sim.zoom > 1.55 && node.r > 13));
|
|
2096
|
+
var shouldLabel = pathNode || (matches && (selected || hovered || (query.active && matches) || (!dense && state.sim.zoom > 0.75) || (dense && state.sim.zoom > 1.55 && node.r > 13)));
|
|
1553
2097
|
if (shouldLabel) drawNodeLabel(ctx, node, selected || hovered);
|
|
1554
2098
|
});
|
|
1555
2099
|
}
|
|
@@ -1681,7 +2225,7 @@
|
|
|
1681
2225
|
class: "edge-hit"
|
|
1682
2226
|
});
|
|
1683
2227
|
hit.addEventListener("click", function () {
|
|
1684
|
-
|
|
2228
|
+
selectEdge(edge.id, true);
|
|
1685
2229
|
render();
|
|
1686
2230
|
});
|
|
1687
2231
|
hit.addEventListener("mousedown", function (event) {
|
|
@@ -1733,7 +2277,7 @@
|
|
|
1733
2277
|
var title = svgEl("title");
|
|
1734
2278
|
title.textContent = displayName(entity) + "\n" + (entity.summary || "");
|
|
1735
2279
|
group.addEventListener("click", function () {
|
|
1736
|
-
|
|
2280
|
+
selectEntity(entity.id, true);
|
|
1737
2281
|
render();
|
|
1738
2282
|
});
|
|
1739
2283
|
group.addEventListener("mousedown", function (event) {
|
|
@@ -1764,12 +2308,16 @@
|
|
|
1764
2308
|
}
|
|
1765
2309
|
|
|
1766
2310
|
function renderLists() {
|
|
1767
|
-
var visibleEntities = state.
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
2311
|
+
var visibleEntities = state.viewerPage === "data"
|
|
2312
|
+
? state.entities.slice()
|
|
2313
|
+
: state.entities.filter(function (entity) {
|
|
2314
|
+
return state.visibleEntityIds.has(entity.id);
|
|
2315
|
+
});
|
|
2316
|
+
var visibleEdges = (state.viewerPage === "data"
|
|
2317
|
+
? state.edges.slice()
|
|
2318
|
+
: state.edges.filter(function (edge) {
|
|
2319
|
+
return state.visibleEdgeIds.has(edge.id);
|
|
2320
|
+
})).sort(function (a, b) {
|
|
1773
2321
|
return reviewRank(a) - reviewRank(b);
|
|
1774
2322
|
});
|
|
1775
2323
|
|
|
@@ -1778,7 +2326,11 @@
|
|
|
1778
2326
|
els.entityList.textContent = "";
|
|
1779
2327
|
els.edgeList.textContent = "";
|
|
1780
2328
|
|
|
1781
|
-
|
|
2329
|
+
var rowLimit = state.viewerPage === "data" ? 40 : 80;
|
|
2330
|
+
var entityRows = visibleEntities.slice(0, rowLimit);
|
|
2331
|
+
var edgeRows = visibleEdges.slice(0, rowLimit);
|
|
2332
|
+
|
|
2333
|
+
entityRows.forEach(function (entity) {
|
|
1782
2334
|
var button = document.createElement("button");
|
|
1783
2335
|
button.type = "button";
|
|
1784
2336
|
button.className = classNames("list-item", state.selected && state.selected.kind === "entity" && state.selected.id === entity.id && "selected");
|
|
@@ -1786,13 +2338,16 @@
|
|
|
1786
2338
|
button.querySelector(".item-title").textContent = displayName(entity);
|
|
1787
2339
|
button.querySelector(".item-meta").textContent = (entity.type || "unknown") + " | " + entity.id;
|
|
1788
2340
|
button.addEventListener("click", function () {
|
|
1789
|
-
|
|
2341
|
+
selectEntity(entity.id, true);
|
|
1790
2342
|
render();
|
|
1791
2343
|
});
|
|
1792
2344
|
els.entityList.appendChild(button);
|
|
1793
2345
|
});
|
|
2346
|
+
if (visibleEntities.length > entityRows.length) {
|
|
2347
|
+
appendListNote(els.entityList, "Showing " + entityRows.length + " of " + visibleEntities.length + ". Use search to narrow.");
|
|
2348
|
+
}
|
|
1794
2349
|
|
|
1795
|
-
|
|
2350
|
+
edgeRows.forEach(function (edge) {
|
|
1796
2351
|
var button = document.createElement("button");
|
|
1797
2352
|
button.type = "button";
|
|
1798
2353
|
button.className = classNames("list-item", state.selected && state.selected.kind === "edge" && state.selected.id === edge.id && "selected");
|
|
@@ -1800,18 +2355,30 @@
|
|
|
1800
2355
|
button.querySelector(".item-title").textContent = edge.relation || "related";
|
|
1801
2356
|
button.querySelector(".item-meta").textContent = displayName(state.entityById.get(edge.from)) + " -> " + displayName(state.entityById.get(edge.to)) + " | " + reviewStatus(edge);
|
|
1802
2357
|
button.addEventListener("click", function () {
|
|
1803
|
-
|
|
2358
|
+
selectEdge(edge.id, true);
|
|
1804
2359
|
render();
|
|
1805
2360
|
});
|
|
1806
2361
|
els.edgeList.appendChild(button);
|
|
1807
2362
|
});
|
|
2363
|
+
if (visibleEdges.length > edgeRows.length) {
|
|
2364
|
+
appendListNote(els.edgeList, "Showing " + edgeRows.length + " of " + visibleEdges.length + ". Filter by relation or select a node first.");
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function appendListNote(parent, text) {
|
|
2369
|
+
var note = document.createElement("div");
|
|
2370
|
+
note.className = "list-note";
|
|
2371
|
+
note.textContent = text;
|
|
2372
|
+
parent.appendChild(note);
|
|
1808
2373
|
}
|
|
1809
2374
|
|
|
1810
2375
|
function renderDetails() {
|
|
1811
2376
|
if (!state.selected) {
|
|
2377
|
+
setSelectionBodyState(null);
|
|
1812
2378
|
els.selectionDetails.className = "details-empty";
|
|
1813
|
-
els.selectionDetails.textContent = "Select
|
|
2379
|
+
els.selectionDetails.textContent = "Select a node or relation to see what it means, why it exists, and which connected memory or code to inspect next.";
|
|
1814
2380
|
els.selectionStatus.textContent = "No selection";
|
|
2381
|
+
if (!state.pathHighlight.steps.length) setPathStatus("Select a code node, then trace to another code node when you need impact proof.", "");
|
|
1815
2382
|
return;
|
|
1816
2383
|
}
|
|
1817
2384
|
|
|
@@ -1820,10 +2387,12 @@
|
|
|
1820
2387
|
: state.edges.find(function (edge) { return edge.id === state.selected.id; });
|
|
1821
2388
|
|
|
1822
2389
|
if (!item) {
|
|
2390
|
+
setSelectionBodyState(null);
|
|
1823
2391
|
els.selectionDetails.textContent = "Selection no longer exists.";
|
|
1824
2392
|
return;
|
|
1825
2393
|
}
|
|
1826
2394
|
|
|
2395
|
+
setSelectionBodyState(state.selected.kind === "entity" ? item : null);
|
|
1827
2396
|
els.selectionDetails.className = "";
|
|
1828
2397
|
els.selectionDetails.textContent = "";
|
|
1829
2398
|
var title = document.createElement("div");
|
|
@@ -1860,6 +2429,32 @@
|
|
|
1860
2429
|
els.selectionDetails.appendChild(kind);
|
|
1861
2430
|
els.selectionDetails.appendChild(rows);
|
|
1862
2431
|
renderInspectorConnections(item);
|
|
2432
|
+
if (state.selected.kind === "entity" && item.graph_kind === "code") prefillPathFromSelection(true);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
function setSelectionBodyState(entity) {
|
|
2436
|
+
if (!document.body || !document.body.classList) return;
|
|
2437
|
+
document.body.classList.toggle("has-selection", Boolean(state.selected));
|
|
2438
|
+
document.body.classList.toggle("has-code-selection", Boolean(entity && entity.graph_kind === "code"));
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
function prefillPathFromSelection(silent) {
|
|
2442
|
+
if (!state.selected || state.selected.kind !== "entity") {
|
|
2443
|
+
if (!silent) setPathStatus("Select a code node first. Path tracing is for files, symbols, routes, tests, and scripts.", "warn");
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
var entity = state.entityById.get(state.selected.id);
|
|
2447
|
+
if (!entity || entity.graph_kind !== "code" || ["file", "symbol", "route", "test", "script"].indexOf(entity.type) === -1) {
|
|
2448
|
+
if (!silent) setPathStatus("Select a code node first. Memory nodes are shown through memory-code links, not code path tracing.", "warn");
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
if (!els.pathFromInput.value || silent) {
|
|
2452
|
+
els.pathFromInput.value = entity.path || displayName(entity) || entity.id;
|
|
2453
|
+
}
|
|
2454
|
+
if (!silent) {
|
|
2455
|
+
els.pathToInput.focus();
|
|
2456
|
+
setPathStatus("Selected " + displayName(entity) + ". Pick a target test, route, file, or symbol to trace impact.", "ok");
|
|
2457
|
+
}
|
|
1863
2458
|
}
|
|
1864
2459
|
|
|
1865
2460
|
function entityDetailRows(entity) {
|
|
@@ -1987,6 +2582,23 @@
|
|
|
1987
2582
|
return Boolean(edge && (edge.memory_code_link || isMemoryCodeRelation(edge.relation)));
|
|
1988
2583
|
}
|
|
1989
2584
|
|
|
2585
|
+
function codeCoverageKey(entity) {
|
|
2586
|
+
if (!entity) return "";
|
|
2587
|
+
return String(entity.path || entity.id || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
function memoryLinkedCodeKeys() {
|
|
2591
|
+
var keys = new Set();
|
|
2592
|
+
state.edges.filter(isMemoryCodeEdge).forEach(function (edge) {
|
|
2593
|
+
[state.entityById.get(edge.from), state.entityById.get(edge.to)].forEach(function (entity) {
|
|
2594
|
+
if (!entity || entity.graph_kind !== "code") return;
|
|
2595
|
+
var key = codeCoverageKey(entity);
|
|
2596
|
+
if (key) keys.add(key);
|
|
2597
|
+
});
|
|
2598
|
+
});
|
|
2599
|
+
return keys;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
1990
2602
|
function connectionImportance(link) {
|
|
1991
2603
|
var relation = String(link.edge.relation || "");
|
|
1992
2604
|
var score = entityImportance(link.other);
|
|
@@ -2029,7 +2641,8 @@
|
|
|
2029
2641
|
button.querySelector(".detail-link-meta").textContent = item.meta;
|
|
2030
2642
|
button.querySelector(".detail-link-body").textContent = item.body;
|
|
2031
2643
|
button.addEventListener("click", function () {
|
|
2032
|
-
|
|
2644
|
+
if (item.entity) selectEntity(item.entity.id, true);
|
|
2645
|
+
else selectEdge(item.edge.id, true);
|
|
2033
2646
|
render();
|
|
2034
2647
|
});
|
|
2035
2648
|
list.appendChild(button);
|
|
@@ -2088,13 +2701,15 @@
|
|
|
2088
2701
|
}).length;
|
|
2089
2702
|
var evidenceEdges = visibleEdges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length > 0; }).length;
|
|
2090
2703
|
var official = state.metrics;
|
|
2704
|
+
var memoryCodeLinks = state.edges.filter(isMemoryCodeEdge).length;
|
|
2705
|
+
var pendingReview = official && official.memory_graph ? Number(firstNumber(official.memory_graph.pending_packets, 0)) : 0;
|
|
2091
2706
|
var metrics = official ? [
|
|
2092
|
-
["
|
|
2093
|
-
["
|
|
2094
|
-
["
|
|
2095
|
-
["
|
|
2096
|
-
["
|
|
2097
|
-
["
|
|
2707
|
+
["Validation", official.harness && official.harness.validation_ok ? "Clean" : "Check"],
|
|
2708
|
+
["Review queue", pendingReview ? pendingReview + " pending" : "Clear"],
|
|
2709
|
+
["Reusable memory", official.memory_graph ? official.memory_graph.approved_packets + " packets" : "n/a"],
|
|
2710
|
+
["Code indexed", official.structural_index ? official.structural_index.files + " files" : official.code_graph.files + " files"],
|
|
2711
|
+
["Parser coverage", official.code_graph.indexer_coverage_percent + "%"],
|
|
2712
|
+
["Memory-code links", memoryCodeLinks]
|
|
2098
2713
|
] : [
|
|
2099
2714
|
["Nodes", visibleEntities.length + "/" + state.entities.length],
|
|
2100
2715
|
["Relations", visibleEdges.length + "/" + state.edges.length],
|
|
@@ -2116,6 +2731,625 @@
|
|
|
2116
2731
|
els.workspaceMode.textContent = (els.viewMode.value || "combined").replace(/^./, function (letter) { return letter.toUpperCase(); });
|
|
2117
2732
|
els.graphSubhead.textContent = visibleEntities.length + " visible nodes and " + visibleEdges.length + " visible relations" +
|
|
2118
2733
|
(hiddenDependencies && !els.showDependencies.checked ? " (" + hiddenDependencies + " dependency/noise nodes hidden)." : ".");
|
|
2734
|
+
renderGraphInsights(visibleEntities, visibleEdges, hiddenDependencies);
|
|
2735
|
+
updateGraphActionButtons();
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
function renderGraphInsights(visibleEntities, visibleEdges, hiddenDependencies) {
|
|
2739
|
+
if (!els.graphInsights) return;
|
|
2740
|
+
els.graphInsights.textContent = "";
|
|
2741
|
+
var allMemoryCode = state.edges.filter(isMemoryCodeEdge);
|
|
2742
|
+
var visibleMemoryCode = visibleEdges.filter(isMemoryCodeEdge);
|
|
2743
|
+
var reviewFlags = visibleEdges.filter(function (edge) { return reviewStatus(edge) !== "ok"; });
|
|
2744
|
+
var visibleCodeFiles = visibleEntities.filter(function (entity) { return entity.graph_kind === "code" && entity.type === "file"; });
|
|
2745
|
+
var coveredKeys = memoryLinkedCodeKeys();
|
|
2746
|
+
var uncoveredCodeFiles = visibleCodeFiles.filter(function (entity) { return !coveredKeys.has(codeCoverageKey(entity)); });
|
|
2747
|
+
var evidenceEdges = visibleEdges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length; }).length;
|
|
2748
|
+
var evidencePercent = visibleEdges.length ? Math.round(evidenceEdges / visibleEdges.length * 100) : 0;
|
|
2749
|
+
var coveragePercent = visibleCodeFiles.length ? Math.round((visibleCodeFiles.length - uncoveredCodeFiles.length) / visibleCodeFiles.length * 100) : 0;
|
|
2750
|
+
var queryActive = parseSearchQuery(els.searchInput.value).active;
|
|
2751
|
+
var hasActiveFilters = queryActive || state.graphActionFilter || els.viewMode.value !== "combined" || els.typeFilter.value || els.relationFilter.value || els.showDependencies.checked;
|
|
2752
|
+
if (!visibleEntities.length || hasActiveFilters) {
|
|
2753
|
+
var recovery = document.createElement("article");
|
|
2754
|
+
recovery.className = classNames("metric-card graph-action-card graph-recovery-card", !visibleEntities.length && "metric-card-warn");
|
|
2755
|
+
recovery.innerHTML = [
|
|
2756
|
+
"<div class=\"metric-card-head\"><span></span><strong></strong></div>",
|
|
2757
|
+
"<p></p>",
|
|
2758
|
+
"<button type=\"button\">Clear search/filter</button>",
|
|
2759
|
+
"<em></em>"
|
|
2760
|
+
].join("");
|
|
2761
|
+
recovery.querySelector(".metric-card-head span").textContent = !visibleEntities.length ? "No graph results" : "Active graph filter";
|
|
2762
|
+
recovery.querySelector(".metric-card-head strong").textContent = !visibleEntities.length ? "0 visible" : "filtered";
|
|
2763
|
+
recovery.querySelector("p").textContent = !visibleEntities.length
|
|
2764
|
+
? "The current search or filter hides every node. Clear it to recover the graph."
|
|
2765
|
+
: "A search, relation, or journey filter is active.";
|
|
2766
|
+
recovery.querySelector("button").addEventListener("click", resetGraphView);
|
|
2767
|
+
recovery.querySelector("em").textContent = queryActive ? "Search: " + els.searchInput.value : (state.graphActionFilter || "custom filter");
|
|
2768
|
+
els.graphInsights.appendChild(recovery);
|
|
2769
|
+
}
|
|
2770
|
+
var cards = [
|
|
2771
|
+
graphActionCard("Memory coverage", coveragePercent + "%", uncoveredCodeFiles.length
|
|
2772
|
+
? uncoveredCodeFiles.length + " visible code file(s) have no linked repo memory."
|
|
2773
|
+
: "Visible code files have linked repo memory.",
|
|
2774
|
+
"Show uncovered code", "uncovered", uncoveredCodeFiles.length ? "warn" : "ok"),
|
|
2775
|
+
graphActionCard("Untrusted edges", reviewFlags.length ? reviewFlags.length + " flagged" : "clear", reviewFlags.length
|
|
2776
|
+
? "Low-confidence, missing-evidence, or invalidated relations are visible."
|
|
2777
|
+
: "Visible relations are evidence-backed enough for inspection.",
|
|
2778
|
+
"Filter to untrusted", "untrusted", reviewFlags.length ? "warn" : "ok", [
|
|
2779
|
+
{ label: "Low confidence", value: reviewFlags.filter(function (edge) { return reviewStatus(edge) === "low confidence"; }).length, score: Math.min(100, reviewFlags.length * 20), status: reviewFlags.length ? "warn" : "ok" },
|
|
2780
|
+
{ label: "Missing evidence", value: reviewFlags.filter(function (edge) { return reviewStatus(edge) === "missing evidence"; }).length, score: Math.min(100, reviewFlags.length * 20), status: reviewFlags.length ? "warn" : "ok" },
|
|
2781
|
+
{ label: "Invalidated", value: reviewFlags.filter(function (edge) { return reviewStatus(edge) === "invalidated"; }).length, score: Math.min(100, reviewFlags.length * 20), status: reviewFlags.length ? "danger" : "ok" }
|
|
2782
|
+
]),
|
|
2783
|
+
graphActionCard("Evidence in view", evidencePercent + "%", evidenceEdges + " of " + visibleEdges.length + " visible relation(s) carry evidence.",
|
|
2784
|
+
"Show memory-code links", "memory-code", evidencePercent >= 80 ? "ok" : "warn"),
|
|
2785
|
+
graphActionCard("Trace impact", visibleMemoryCode.length + " links", "Select a code node, then trace to a test, route, or symbol from the Inspector.",
|
|
2786
|
+
"Use selected node", "path", visibleMemoryCode.length ? "ok" : "warn")
|
|
2787
|
+
];
|
|
2788
|
+
cards.forEach(function (card) { els.graphInsights.appendChild(card); });
|
|
2789
|
+
if (els.graphInsightStatus) els.graphInsightStatus.textContent = state.graphActionFilter || (hiddenDependencies ? hiddenDependencies + " external hidden" : "ready");
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
function graphActionCard(title, value, detail, actionLabel, action, status, rows) {
|
|
2793
|
+
var card = document.createElement("article");
|
|
2794
|
+
card.className = classNames("metric-card graph-action-card", status && "metric-card-" + status, state.graphActionFilter === action && "active");
|
|
2795
|
+
card.innerHTML = [
|
|
2796
|
+
"<div class=\"metric-card-head\"><span></span><strong></strong></div>",
|
|
2797
|
+
"<p></p>",
|
|
2798
|
+
"<div class=\"metric-bars\"></div>",
|
|
2799
|
+
"<button type=\"button\"></button>",
|
|
2800
|
+
"<em></em>"
|
|
2801
|
+
].join("");
|
|
2802
|
+
card.querySelector(".metric-card-head span").textContent = title;
|
|
2803
|
+
card.querySelector(".metric-card-head strong").textContent = value;
|
|
2804
|
+
card.querySelector("p").textContent = detail;
|
|
2805
|
+
var bars = card.querySelector(".metric-bars");
|
|
2806
|
+
if (Array.isArray(rows) && rows.length) {
|
|
2807
|
+
rows.forEach(function (row) {
|
|
2808
|
+
var item = document.createElement("div");
|
|
2809
|
+
item.className = classNames("metric-bar", row.status && "metric-bar-" + row.status);
|
|
2810
|
+
item.innerHTML = "<span></span><strong></strong><i></i>";
|
|
2811
|
+
item.querySelector("span").textContent = row.label;
|
|
2812
|
+
item.querySelector("strong").textContent = formatDashboardValue(row.value);
|
|
2813
|
+
item.querySelector("i").style.width = clamp(Number(row.score || 0), row.value ? 8 : 0, 100) + "%";
|
|
2814
|
+
bars.appendChild(item);
|
|
2815
|
+
});
|
|
2816
|
+
} else {
|
|
2817
|
+
bars.remove();
|
|
2818
|
+
}
|
|
2819
|
+
var button = card.querySelector("button");
|
|
2820
|
+
button.textContent = actionLabel;
|
|
2821
|
+
button.addEventListener("click", function () {
|
|
2822
|
+
if (action === "path") {
|
|
2823
|
+
prefillPathFromSelection();
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
applyGraphActionFilter(action);
|
|
2827
|
+
});
|
|
2828
|
+
card.querySelector("em").textContent = state.graphActionFilter === action ? "Active filter" : "";
|
|
2829
|
+
return card;
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function updateGraphActionButtons() {
|
|
2833
|
+
[
|
|
2834
|
+
[els.showUntrusted, "untrusted"],
|
|
2835
|
+
[els.showUncovered, "uncovered"],
|
|
2836
|
+
[els.showMemoryCode, "memory-code"]
|
|
2837
|
+
].forEach(function (entry) {
|
|
2838
|
+
if (!entry[0]) return;
|
|
2839
|
+
entry[0].classList.toggle("active", state.graphActionFilter === entry[1]);
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
function renderArtifactDiagnostics(visibleEntities, visibleEdges) {
|
|
2844
|
+
if (!els.debugOverview) return;
|
|
2845
|
+
els.debugOverview.textContent = "";
|
|
2846
|
+
var episodes = state.episodesById ? state.episodesById.size : 0;
|
|
2847
|
+
var evidenceEdges = state.edges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length; }).length;
|
|
2848
|
+
var evidencePercent = state.edges.length ? Math.round(evidenceEdges / state.edges.length * 100) : 0;
|
|
2849
|
+
var memoryCodeEdges = state.edges.filter(isMemoryCodeEdge).length;
|
|
2850
|
+
var reviewFlags = state.edges.filter(function (edge) { return reviewStatus(edge) !== "ok"; }).length;
|
|
2851
|
+
[
|
|
2852
|
+
metricBars("Artifact shape", state.entities.length + " nodes", [
|
|
2853
|
+
{ label: "Visible nodes", value: visibleEntities.length, score: state.entities.length ? visibleEntities.length / state.entities.length * 100 : 0, status: "ok" },
|
|
2854
|
+
{ label: "Relations", value: state.edges.length, score: 100, status: "ok" },
|
|
2855
|
+
{ label: "Episodes", value: episodes, score: episodes ? 100 : 0, status: episodes ? "ok" : "warn" }
|
|
2856
|
+
], "Use this when graph generation seems incomplete.", "ok"),
|
|
2857
|
+
metricDonut("Evidence", evidencePercent, evidenceEdges + " of " + state.edges.length + " relation(s) have evidence", "Low evidence means recall may explain less than expected.", evidencePercent >= 80 ? "ok" : "warn"),
|
|
2858
|
+
metricBars("Link diagnostics", memoryCodeEdges + " memory-code", [
|
|
2859
|
+
{ label: "Memory-code", value: memoryCodeEdges, score: Math.min(100, memoryCodeEdges / Math.max(1, state.edges.length) * 100), status: memoryCodeEdges ? "ok" : "warn" },
|
|
2860
|
+
{ label: "Review flags", value: reviewFlags, score: Math.min(100, reviewFlags * 12), status: reviewFlags ? "warn" : "ok" },
|
|
2861
|
+
{ label: "Visible edges", value: visibleEdges.length, score: state.edges.length ? visibleEdges.length / state.edges.length * 100 : 0, status: "ok" }
|
|
2862
|
+
], "Use raw rows below to inspect exact IDs, relations, and evidence.", reviewFlags ? "warn" : "ok")
|
|
2863
|
+
].forEach(function (card) { els.debugOverview.appendChild(card); });
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
function renderDashboard() {
|
|
2867
|
+
if (!els.dashboardStats) return;
|
|
2868
|
+
var metrics = state.metrics || {};
|
|
2869
|
+
var memoryGraph = metrics.memory_graph || {};
|
|
2870
|
+
var codeGraph = metrics.code_graph || {};
|
|
2871
|
+
var structural = metrics.structural_index || {};
|
|
2872
|
+
var savings = metrics.savings || {};
|
|
2873
|
+
var pain = metrics.pain || {};
|
|
2874
|
+
var memoryNodes = state.entities.filter(function (entity) { return entity.graph_kind === "memory"; }).length;
|
|
2875
|
+
var codeNodes = state.entities.filter(function (entity) { return entity.graph_kind === "code"; }).length;
|
|
2876
|
+
var memoryCodeEdges = state.edges.filter(isMemoryCodeEdge);
|
|
2877
|
+
var reports = state.reports || {};
|
|
2878
|
+
var reportCount = Object.keys(reports).filter(function (key) { return reports[key]; }).length;
|
|
2879
|
+
var risk = reports.risk || {};
|
|
2880
|
+
var riskTargets = Array.isArray(risk.targets) ? risk.targets : Object.keys(risk.targets || {});
|
|
2881
|
+
var inboxCounts = state.inbox && state.inbox.counts ? state.inbox.counts : {};
|
|
2882
|
+
var pendingReview = Number(firstNumber(inboxCounts.pending, memoryGraph.pending_packets, (state.pendingPackets || []).length, 0));
|
|
2883
|
+
var staleFlags = Number(firstNumber(inboxCounts.stale, 0));
|
|
2884
|
+
var duplicateFlags = Number(firstNumber(inboxCounts.duplicates, memoryGraph.duplicate_candidate_pairs, 0));
|
|
2885
|
+
var missingContext = Number(firstNumber(inboxCounts.missing_context, 0));
|
|
2886
|
+
var ownerSilos = Array.isArray(risk.ownership_silos) ? risk.ownership_silos.length : 0;
|
|
2887
|
+
var hotspots = Array.isArray(risk.global_hotspots) ? risk.global_hotspots.length : 0;
|
|
2888
|
+
var readiness = dashboardReadiness(metrics, pendingReview, staleFlags, duplicateFlags, missingContext);
|
|
2889
|
+
var memoryCoverage = dashboardMemoryCoverage(reports, memoryCodeEdges, memoryGraph, memoryNodes);
|
|
2890
|
+
var riskHealth = riskTargets.length || hotspots ? (riskTargets.length + hotspots) + " signals" : "No flags";
|
|
2891
|
+
var statRows = [
|
|
2892
|
+
["Handoff", readiness.label, readiness.detail, readiness.status],
|
|
2893
|
+
["Memory", memoryCoverage.label, memoryCoverage.detail, memoryCoverage.status],
|
|
2894
|
+
["Risk", riskHealth, riskTargets.length + " targets, " + ownerSilos + " ownership silos", riskTargets.length || ownerSilos || hotspots ? "warn" : "ok"],
|
|
2895
|
+
["Code map", firstNumber(codeGraph.files, structural.files, countEntitiesByType("file")) + " files", firstNumber(codeGraph.symbols, structural.symbols, codeNodes) + " symbols indexed", "code"]
|
|
2896
|
+
];
|
|
2897
|
+
els.dashboardStats.textContent = "";
|
|
2898
|
+
statRows.forEach(function (row) {
|
|
2899
|
+
var item = document.createElement("div");
|
|
2900
|
+
item.className = classNames("dashboard-stat", row[3] && "dashboard-stat-" + row[3]);
|
|
2901
|
+
item.innerHTML = "<span></span><strong></strong><em></em>";
|
|
2902
|
+
item.querySelector("strong").textContent = formatDashboardValue(row[1]);
|
|
2903
|
+
item.querySelector("span").textContent = row[0];
|
|
2904
|
+
item.querySelector("em").textContent = row[2] || "";
|
|
2905
|
+
els.dashboardStats.appendChild(item);
|
|
2906
|
+
});
|
|
2907
|
+
|
|
2908
|
+
setDashboardRows("dashboardMemory", [
|
|
2909
|
+
["Reusable", firstNumber(memoryGraph.approved_packets, memoryNodes) + " packets"],
|
|
2910
|
+
["Linked", memoryCodeEdges.length + " code links"],
|
|
2911
|
+
["Review", pendingReview ? pendingReview + " pending" : "clear"]
|
|
2912
|
+
]);
|
|
2913
|
+
setDashboardRows("dashboardGraph", [
|
|
2914
|
+
["Files", firstNumber(codeGraph.files, structural.files, countEntitiesByType("file"))],
|
|
2915
|
+
["Symbols", firstNumber(codeGraph.symbols, structural.symbols, countEntitiesByType("symbol"))],
|
|
2916
|
+
["Coverage", codeGraph.indexer_coverage_percent != null ? codeGraph.indexer_coverage_percent + "%" : "not loaded"]
|
|
2917
|
+
]);
|
|
2918
|
+
setDashboardRows("dashboardIntel", [
|
|
2919
|
+
["Risk targets", riskTargets.length || "none"],
|
|
2920
|
+
["Ownership silos", ownerSilos || "none"],
|
|
2921
|
+
["Decision coverage", reports.decisions && reports.decisions.coverage_percent != null ? reports.decisions.coverage_percent + "%" : "not loaded"]
|
|
2922
|
+
]);
|
|
2923
|
+
setDashboardRows("dashboardReview", [
|
|
2924
|
+
["Handoff", readiness.label],
|
|
2925
|
+
["Pending", pendingReview || "none"],
|
|
2926
|
+
["Stale / duplicate", staleFlags + " / " + duplicateFlags],
|
|
2927
|
+
["Missing context", missingContext || "none"]
|
|
2928
|
+
]);
|
|
2929
|
+
renderDashboardCharts({
|
|
2930
|
+
metrics: metrics,
|
|
2931
|
+
reports: reports,
|
|
2932
|
+
memoryGraph: memoryGraph,
|
|
2933
|
+
codeGraph: codeGraph,
|
|
2934
|
+
structural: structural,
|
|
2935
|
+
memoryCodeEdges: memoryCodeEdges,
|
|
2936
|
+
memoryNodes: memoryNodes,
|
|
2937
|
+
pendingReview: pendingReview,
|
|
2938
|
+
staleFlags: staleFlags,
|
|
2939
|
+
duplicateFlags: duplicateFlags,
|
|
2940
|
+
missingContext: missingContext,
|
|
2941
|
+
riskTargets: riskTargets,
|
|
2942
|
+
ownerSilos: ownerSilos,
|
|
2943
|
+
hotspots: hotspots
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
function renderDashboardCharts(data) {
|
|
2948
|
+
if (!els.dashboardCharts) return;
|
|
2949
|
+
var approvedPackets = Number(firstNumber(data.memoryGraph.approved_packets, data.memoryNodes, 0));
|
|
2950
|
+
var linkedPacketIds = new Set();
|
|
2951
|
+
data.memoryCodeEdges.forEach(function (edge) {
|
|
2952
|
+
var from = state.entityById.get(edge.from);
|
|
2953
|
+
var to = state.entityById.get(edge.to);
|
|
2954
|
+
if (from && isMemoryPacketEntity(from)) linkedPacketIds.add(from.id);
|
|
2955
|
+
if (to && isMemoryPacketEntity(to)) linkedPacketIds.add(to.id);
|
|
2956
|
+
});
|
|
2957
|
+
var memoryGrounding = approvedPackets ? Math.round(linkedPacketIds.size / approvedPackets * 100) : 0;
|
|
2958
|
+
var sourceCoverage = Number(firstNumber(data.codeGraph.indexer_coverage_percent, 0));
|
|
2959
|
+
var blockers = data.pendingReview + data.staleFlags + data.duplicateFlags + data.missingContext;
|
|
2960
|
+
var riskSignals = data.riskTargets.length + data.ownerSilos + data.hotspots;
|
|
2961
|
+
els.dashboardCharts.textContent = "";
|
|
2962
|
+
[
|
|
2963
|
+
metricDonut("Memory grounding", memoryGrounding, linkedPacketIds.size + " of " + approvedPackets + " packets linked to code", "Open Memory and fix Needs paths first.", memoryGrounding >= 70 ? "ok" : "warn"),
|
|
2964
|
+
metricDonut("Source map", sourceCoverage, firstNumber(data.codeGraph.files, data.structural.files, 0) + " files indexed for graph recall", "If this drops, refresh indexing before relying on graph answers.", sourceCoverage >= 90 ? "ok" : "warn"),
|
|
2965
|
+
metricBars("Handoff blockers", blockers ? blockers + " open" : "clear", [
|
|
2966
|
+
{ label: "Pending", value: data.pendingReview, score: Math.min(100, data.pendingReview * 24), status: data.pendingReview ? "warn" : "ok" },
|
|
2967
|
+
{ label: "Stale", value: data.staleFlags, score: Math.min(100, data.staleFlags * 24), status: data.staleFlags ? "warn" : "ok" },
|
|
2968
|
+
{ label: "Duplicate", value: data.duplicateFlags, score: Math.min(100, data.duplicateFlags * 24), status: data.duplicateFlags ? "warn" : "ok" },
|
|
2969
|
+
{ label: "Missing context", value: data.missingContext, score: Math.min(100, data.missingContext * 18), status: data.missingContext ? "warn" : "ok" }
|
|
2970
|
+
], blockers ? "Resolve Review before handing work to another agent." : "Memory is clean for handoff.", blockers ? "warn" : "ok"),
|
|
2971
|
+
metricBars("Change risk", riskSignals ? riskSignals + " signals" : "none", [
|
|
2972
|
+
{ label: "Targets", value: data.riskTargets.length, score: Math.min(100, data.riskTargets.length * 18), status: data.riskTargets.length ? "warn" : "ok" },
|
|
2973
|
+
{ label: "Silos", value: data.ownerSilos, score: Math.min(100, data.ownerSilos * 18), status: data.ownerSilos ? "warn" : "ok" },
|
|
2974
|
+
{ label: "Hotspots", value: data.hotspots, score: Math.min(100, data.hotspots * 18), status: data.hotspots ? "danger" : "ok" }
|
|
2975
|
+
], riskSignals ? "Open Intel or Owners before editing risky files." : "No loaded risk flags.", riskSignals ? "warn" : "ok")
|
|
2976
|
+
].forEach(function (card) { els.dashboardCharts.appendChild(card); });
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
function metricDonut(title, percent, detail, action, status) {
|
|
2980
|
+
var card = document.createElement("article");
|
|
2981
|
+
var value = clamp(Number(percent || 0), 0, 100);
|
|
2982
|
+
card.className = classNames("metric-card", status && "metric-card-" + status);
|
|
2983
|
+
card.innerHTML = [
|
|
2984
|
+
"<div class=\"metric-card-head\"><span></span><strong></strong></div>",
|
|
2985
|
+
"<div class=\"metric-visual\"><div class=\"metric-donut\"><span></span></div><p></p></div>",
|
|
2986
|
+
"<em></em>"
|
|
2987
|
+
].join("");
|
|
2988
|
+
card.querySelector(".metric-card-head span").textContent = title;
|
|
2989
|
+
card.querySelector(".metric-card-head strong").textContent = value + "%";
|
|
2990
|
+
card.querySelector(".metric-donut").style.setProperty("--value", value);
|
|
2991
|
+
card.querySelector(".metric-donut span").textContent = value + "%";
|
|
2992
|
+
card.querySelector("p").textContent = detail || "";
|
|
2993
|
+
card.querySelector("em").textContent = action || "";
|
|
2994
|
+
return card;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
function metricBars(title, value, rows, action, status) {
|
|
2998
|
+
var card = document.createElement("article");
|
|
2999
|
+
card.className = classNames("metric-card", status && "metric-card-" + status);
|
|
3000
|
+
card.innerHTML = [
|
|
3001
|
+
"<div class=\"metric-card-head\"><span></span><strong></strong></div>",
|
|
3002
|
+
"<div class=\"metric-bars\"></div>",
|
|
3003
|
+
"<em></em>"
|
|
3004
|
+
].join("");
|
|
3005
|
+
card.querySelector(".metric-card-head span").textContent = title;
|
|
3006
|
+
card.querySelector(".metric-card-head strong").textContent = formatDashboardValue(value);
|
|
3007
|
+
var list = card.querySelector(".metric-bars");
|
|
3008
|
+
rows.forEach(function (row) {
|
|
3009
|
+
var item = document.createElement("div");
|
|
3010
|
+
item.className = classNames("metric-bar", row.status && "metric-bar-" + row.status);
|
|
3011
|
+
item.innerHTML = "<span></span><strong></strong><i></i>";
|
|
3012
|
+
item.querySelector("span").textContent = row.label;
|
|
3013
|
+
item.querySelector("strong").textContent = formatDashboardValue(row.value);
|
|
3014
|
+
item.querySelector("i").style.width = clamp(Number(row.score || 0), row.value ? 8 : 0, 100) + "%";
|
|
3015
|
+
list.appendChild(item);
|
|
3016
|
+
});
|
|
3017
|
+
card.querySelector("em").textContent = action || "";
|
|
3018
|
+
return card;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
function dashboardReadiness(metrics, pendingReview, staleFlags, duplicateFlags, missingContext) {
|
|
3022
|
+
if (pendingReview || staleFlags || duplicateFlags || missingContext) {
|
|
3023
|
+
return { label: "Needs review", detail: pendingReview + " pending, " + staleFlags + " stale, " + duplicateFlags + " duplicate, " + missingContext + " missing context", status: "warn" };
|
|
3024
|
+
}
|
|
3025
|
+
if (metrics && metrics.harness && metrics.harness.validation_ok) {
|
|
3026
|
+
return { label: "Ready", detail: "Memory and graph checks are clean", status: "ok" };
|
|
3027
|
+
}
|
|
3028
|
+
return { label: "Unknown", detail: "Run kage refresh or open local viewer with metrics", status: "warn" };
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
function dashboardMemoryCoverage(reports, memoryCodeEdges, memoryGraph, memoryNodes) {
|
|
3032
|
+
var coverage = reports.decisions && reports.decisions.coverage_percent;
|
|
3033
|
+
if (coverage != null) {
|
|
3034
|
+
return {
|
|
3035
|
+
label: coverage + "%",
|
|
3036
|
+
detail: "Decision memory coverage for important code paths",
|
|
3037
|
+
status: Number(coverage) >= 70 ? "ok" : "warn"
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
var packets = Number(firstNumber(memoryGraph.approved_packets, memoryNodes, 0));
|
|
3041
|
+
if (!packets) return { label: "No memory", detail: "Agents will rediscover repo context", status: "warn" };
|
|
3042
|
+
return {
|
|
3043
|
+
label: memoryCodeEdges.length + " links",
|
|
3044
|
+
detail: "Memory packets connected back to code",
|
|
3045
|
+
status: memoryCodeEdges.length ? "ok" : "warn"
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
function setDashboardRows(cardId, rows) {
|
|
3050
|
+
var card = document.getElementById(cardId);
|
|
3051
|
+
if (!card) return;
|
|
3052
|
+
var list = card.querySelector("ul");
|
|
3053
|
+
if (!list) return;
|
|
3054
|
+
list.textContent = "";
|
|
3055
|
+
rows.forEach(function (row) {
|
|
3056
|
+
var item = document.createElement("li");
|
|
3057
|
+
item.innerHTML = "<strong></strong><span></span>";
|
|
3058
|
+
item.querySelector("strong").textContent = row[0];
|
|
3059
|
+
item.querySelector("span").textContent = formatDashboardValue(row[1]);
|
|
3060
|
+
list.appendChild(item);
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
function firstNumber() {
|
|
3065
|
+
for (var index = 0; index < arguments.length; index += 1) {
|
|
3066
|
+
var value = arguments[index];
|
|
3067
|
+
if (value !== null && value !== undefined && value !== "") return value;
|
|
3068
|
+
}
|
|
3069
|
+
return "n/a";
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
function countEntitiesByType(type) {
|
|
3073
|
+
return state.entities.filter(function (entity) { return entity.type === type; }).length;
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
function formatDashboardValue(value) {
|
|
3077
|
+
if (typeof value === "number" && Number.isFinite(value)) return value.toLocaleString();
|
|
3078
|
+
return String(value == null ? "n/a" : value);
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
function escapeHtml(value) {
|
|
3082
|
+
return String(value == null ? "" : value).replace(/[&<>"']/g, function (char) {
|
|
3083
|
+
return { "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[char] || char;
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
function renderMemoryLibrary() {
|
|
3088
|
+
if (!els.memoryList) return;
|
|
3089
|
+
var memoryEntities = state.entities.filter(function (entity) {
|
|
3090
|
+
return isMemoryPacketEntity(entity);
|
|
3091
|
+
}).sort(function (a, b) {
|
|
3092
|
+
return entityImportance(b) - entityImportance(a) || displayName(a).localeCompare(displayName(b));
|
|
3093
|
+
});
|
|
3094
|
+
var memoryLinkCounts = new Map();
|
|
3095
|
+
state.edges.forEach(function (edge) {
|
|
3096
|
+
if (!isMemoryCodeEdge(edge)) return;
|
|
3097
|
+
var fromEntity = state.entityById.get(edge.from);
|
|
3098
|
+
var toEntity = state.entityById.get(edge.to);
|
|
3099
|
+
if (fromEntity && fromEntity.graph_kind === "memory" && toEntity && toEntity.graph_kind === "code") {
|
|
3100
|
+
memoryLinkCounts.set(edge.from, (memoryLinkCounts.get(edge.from) || 0) + 1);
|
|
3101
|
+
}
|
|
3102
|
+
if (toEntity && toEntity.graph_kind === "memory" && fromEntity && fromEntity.graph_kind === "code") {
|
|
3103
|
+
memoryLinkCounts.set(edge.to, (memoryLinkCounts.get(edge.to) || 0) + 1);
|
|
3104
|
+
}
|
|
3105
|
+
});
|
|
3106
|
+
var linkedCount = memoryEntities.filter(function (entity) { return (memoryLinkCounts.get(entity.id) || 0) > 0; }).length;
|
|
3107
|
+
var query = parseSearchQuery(els.memorySearch ? els.memorySearch.value : "");
|
|
3108
|
+
var filter = els.memoryFilter ? els.memoryFilter.value : "all";
|
|
3109
|
+
var filtered = memoryEntities.filter(function (entity) {
|
|
3110
|
+
var linkCount = memoryLinkCounts.get(entity.id) || 0;
|
|
3111
|
+
if (filter === "linked" && !linkCount) return false;
|
|
3112
|
+
if (filter === "needs-paths" && linkCount) return false;
|
|
3113
|
+
if (["decision", "runbook", "bug_fix"].indexOf(filter) !== -1 && !memoryMatchesKind(entity, filter)) return false;
|
|
3114
|
+
return matchesSearchQuery(entity, query);
|
|
3115
|
+
});
|
|
3116
|
+
els.memoryStatus.textContent = filtered.length + " shown";
|
|
3117
|
+
if (els.memoryStats) {
|
|
3118
|
+
els.memoryStats.innerHTML = [
|
|
3119
|
+
memoryStat("Reusable", memoryEntities.length),
|
|
3120
|
+
memoryStat("Code-linked", linkedCount),
|
|
3121
|
+
memoryStat("Needs paths", memoryEntities.length - linkedCount)
|
|
3122
|
+
].join("");
|
|
3123
|
+
}
|
|
3124
|
+
if (els.memoryOverview) renderMemoryOverview(memoryEntities, linkedCount);
|
|
3125
|
+
els.memoryList.textContent = "";
|
|
3126
|
+
if (!memoryEntities.length) {
|
|
3127
|
+
els.memoryList.className = "memory-list details-empty";
|
|
3128
|
+
els.memoryList.textContent = "No memory packets loaded. Launch with `kage viewer --project <repo>` to load repo memory.";
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
if (!filtered.length) {
|
|
3132
|
+
els.memoryList.className = "memory-list details-empty";
|
|
3133
|
+
els.memoryList.textContent = "No matching memory. Clear search or switch the filter.";
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
filtered.sort(function (a, b) {
|
|
3137
|
+
return (memoryLinkCounts.get(b.id) || 0) - (memoryLinkCounts.get(a.id) || 0) ||
|
|
3138
|
+
entityImportance(b) - entityImportance(a) ||
|
|
3139
|
+
displayName(a).localeCompare(displayName(b));
|
|
3140
|
+
});
|
|
3141
|
+
els.memoryList.className = "memory-list";
|
|
3142
|
+
filtered.slice(0, 60).forEach(function (entity) {
|
|
3143
|
+
var links = memoryCodeLinksForEntity(entity.id);
|
|
3144
|
+
var firstCodeTarget = primaryCodeTargetForMemory(entity.id, links);
|
|
3145
|
+
var item = document.createElement("button");
|
|
3146
|
+
item.type = "button";
|
|
3147
|
+
var selected = state.selected && state.selected.kind === "entity" && state.selected.id === entity.id;
|
|
3148
|
+
item.className = classNames("memory-row", selected && "selected");
|
|
3149
|
+
item.setAttribute("aria-selected", selected ? "true" : "false");
|
|
3150
|
+
item.innerHTML = [
|
|
3151
|
+
"<span class=\"memory-row-main\"><strong></strong><em></em></span>",
|
|
3152
|
+
"<span class=\"memory-row-meta\"></span>",
|
|
3153
|
+
"<span class=\"memory-row-target\"></span>"
|
|
3154
|
+
].join("");
|
|
3155
|
+
item.querySelector("strong").textContent = displayName(entity);
|
|
3156
|
+
item.querySelector("em").textContent = entity.type || "memory";
|
|
3157
|
+
item.querySelector(".memory-row-meta").textContent = trimIntelText(entity.summary || entity.description || entity.path || "No summary", 150);
|
|
3158
|
+
item.querySelector(".memory-row-target").textContent = links.length
|
|
3159
|
+
? links.length + " code link" + (links.length === 1 ? "" : "s") + (firstCodeTarget ? " | " + trimIntelText(codeTargetLabel(firstCodeTarget), 64) : "")
|
|
3160
|
+
: "needs code paths";
|
|
3161
|
+
item.addEventListener("click", function () {
|
|
3162
|
+
selectEntity(entity.id, true);
|
|
3163
|
+
render();
|
|
3164
|
+
});
|
|
3165
|
+
els.memoryList.appendChild(item);
|
|
3166
|
+
});
|
|
3167
|
+
if (filtered.length > 60) appendListNote(els.memoryList, "Showing 60 of " + filtered.length + ". Search a path or topic to narrow.");
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
function memoryStat(label, value) {
|
|
3171
|
+
return "<div><strong>" + escapeHtml(String(value)) + "</strong><span>" + escapeHtml(label) + "</span></div>";
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
function renderMemoryOverview(memoryEntities, linkedCount) {
|
|
3175
|
+
els.memoryOverview.textContent = "";
|
|
3176
|
+
var total = memoryEntities.length;
|
|
3177
|
+
var linkedPercent = total ? Math.round(linkedCount / total * 100) : 0;
|
|
3178
|
+
var typeCounts = new Map();
|
|
3179
|
+
memoryEntities.forEach(function (entity) {
|
|
3180
|
+
typeCounts.set(entity.type || "memory", (typeCounts.get(entity.type || "memory") || 0) + 1);
|
|
3181
|
+
});
|
|
3182
|
+
var topTypes = Array.from(typeCounts.entries()).sort(function (a, b) { return b[1] - a[1] || a[0].localeCompare(b[0]); }).slice(0, 4);
|
|
3183
|
+
var maxType = Math.max(1, topTypes.reduce(function (max, row) { return Math.max(max, row[1]); }, 0));
|
|
3184
|
+
els.memoryOverview.appendChild(metricDonut(
|
|
3185
|
+
"Code grounding",
|
|
3186
|
+
linkedPercent,
|
|
3187
|
+
linkedCount + " packet(s) are connected to files, symbols, routes, or tests",
|
|
3188
|
+
linkedPercent >= 70 ? "Use linked memory before edits." : "Filter Needs paths and add concrete code references.",
|
|
3189
|
+
linkedPercent >= 70 ? "ok" : "warn"
|
|
3190
|
+
));
|
|
3191
|
+
els.memoryOverview.appendChild(metricBars("Memory mix", total + " packets", topTypes.map(function (row) {
|
|
3192
|
+
return {
|
|
3193
|
+
label: row[0],
|
|
3194
|
+
value: row[1],
|
|
3195
|
+
score: row[1] / maxType * 100,
|
|
3196
|
+
status: row[0] === "memory" ? "" : "ok"
|
|
3197
|
+
};
|
|
3198
|
+
}), "A healthy repo has decisions, bug fixes, runbooks, gotchas, and code explanations.", "ok"));
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
function memoryCodeLinksForEntity(entityId) {
|
|
3202
|
+
return state.edges.filter(function (edge) {
|
|
3203
|
+
if ((edge.from !== entityId && edge.to !== entityId) || !isMemoryCodeEdge(edge)) return false;
|
|
3204
|
+
var other = state.entityById.get(edge.from === entityId ? edge.to : edge.from);
|
|
3205
|
+
return Boolean(other && other.graph_kind === "code");
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
function primaryCodeTargetForMemory(entityId, links) {
|
|
3210
|
+
return links.map(function (edge) {
|
|
3211
|
+
return state.entityById.get(edge.from === entityId ? edge.to : edge.from);
|
|
3212
|
+
}).filter(Boolean).sort(function (a, b) {
|
|
3213
|
+
return codeTargetScore(b) - codeTargetScore(a) || codeTargetLabel(a).localeCompare(codeTargetLabel(b));
|
|
3214
|
+
})[0] || null;
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
function codeTargetScore(entity) {
|
|
3218
|
+
var score = 0;
|
|
3219
|
+
if (!entity) return score;
|
|
3220
|
+
if (entity.type === "file") score += 80;
|
|
3221
|
+
if (entity.type === "route" || entity.type === "test") score += 60;
|
|
3222
|
+
if (entity.type === "symbol") score += 45;
|
|
3223
|
+
if (entity.path) score += 35;
|
|
3224
|
+
if (entity.line) score += 8;
|
|
3225
|
+
if (String(entity.path || "").indexOf(".agent_memory/") === 0) score -= 100;
|
|
3226
|
+
return score;
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
function codeTargetLabel(entity) {
|
|
3230
|
+
if (!entity) return "";
|
|
3231
|
+
if (entity.type === "file" && entity.path) return entity.path;
|
|
3232
|
+
if (entity.type === "route") {
|
|
3233
|
+
return (entity.method && entity.route_path ? entity.method + " " + entity.route_path : displayName(entity)) +
|
|
3234
|
+
(entity.path ? " in " + entity.path : "");
|
|
3235
|
+
}
|
|
3236
|
+
if ((entity.type === "symbol" || entity.type === "test") && entity.path) {
|
|
3237
|
+
return displayName(entity) + " in " + entity.path;
|
|
3238
|
+
}
|
|
3239
|
+
return entity.path || displayName(entity);
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
function memoryMatchesKind(entity, kind) {
|
|
3243
|
+
if (!entity) return false;
|
|
3244
|
+
if (entity.type === kind) return true;
|
|
3245
|
+
var normalizedKind = String(kind || "").replace(/_/g, " ");
|
|
3246
|
+
var text = [
|
|
3247
|
+
entity.type,
|
|
3248
|
+
entity.name,
|
|
3249
|
+
entity.summary,
|
|
3250
|
+
entity.description,
|
|
3251
|
+
entity.path
|
|
3252
|
+
].join(" ").toLowerCase();
|
|
3253
|
+
return text.indexOf(kind) !== -1 || text.indexOf(normalizedKind) !== -1;
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
function isMemoryPacketEntity(entity) {
|
|
3257
|
+
return Boolean(entity && entity.graph_kind === "memory" && MEMORY_PACKET_TYPES.has(entity.type));
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
function renderOwners() {
|
|
3261
|
+
if (!els.ownersList) return;
|
|
3262
|
+
var contributors = state.reports && state.reports.contributors;
|
|
3263
|
+
var risk = state.reports && state.reports.risk;
|
|
3264
|
+
var profiles = contributors && Array.isArray(contributors.contributors) ? contributors.contributors : [];
|
|
3265
|
+
var silos = risk && Array.isArray(risk.ownership_silos) ? risk.ownership_silos : [];
|
|
3266
|
+
els.ownersStatus.textContent = profiles.length ? profiles.length + " contributors" : "not loaded";
|
|
3267
|
+
els.ownersList.textContent = "";
|
|
3268
|
+
if (els.ownersSummary) renderOwnersSummary(profiles, silos);
|
|
3269
|
+
if (!profiles.length && !silos.length) {
|
|
3270
|
+
els.ownersList.className = "owners-list details-empty";
|
|
3271
|
+
els.ownersList.textContent = "No owner report loaded. Launch with `kage viewer --project <repo>` to load contributor and ownership reports.";
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
els.ownersList.className = "owners-list";
|
|
3275
|
+
profiles.slice(0, 24).forEach(function (profile) {
|
|
3276
|
+
var item = document.createElement("article");
|
|
3277
|
+
item.className = "owner-card";
|
|
3278
|
+
item.innerHTML = [
|
|
3279
|
+
"<div class=\"owner-head\"><strong></strong><span></span></div>",
|
|
3280
|
+
"<div class=\"owner-stats\"></div>",
|
|
3281
|
+
"<p></p>"
|
|
3282
|
+
].join("");
|
|
3283
|
+
item.querySelector("strong").textContent = shortContributor(profile.contributor);
|
|
3284
|
+
item.querySelector(".owner-head span").textContent = (profile.primary_owned_files || 0) + " owned files";
|
|
3285
|
+
item.querySelector(".owner-stats").textContent = [
|
|
3286
|
+
(profile.commits_total || 0) + " commits",
|
|
3287
|
+
(profile.commits_90d || 0) + " in 90d",
|
|
3288
|
+
(profile.silo_files && profile.silo_files.length ? profile.silo_files.length + " silo files" : "no silo flags")
|
|
3289
|
+
].join(" | ");
|
|
3290
|
+
item.querySelector("p").textContent = Array.isArray(profile.top_modules) && profile.top_modules.length
|
|
3291
|
+
? "Modules: " + profile.top_modules.slice(0, 4).join(", ")
|
|
3292
|
+
: "Local git ownership signal.";
|
|
3293
|
+
els.ownersList.appendChild(item);
|
|
3294
|
+
});
|
|
3295
|
+
if (silos.length) {
|
|
3296
|
+
var siloSection = document.createElement("section");
|
|
3297
|
+
siloSection.className = "owner-silos";
|
|
3298
|
+
siloSection.innerHTML = "<h3>Ownership Silos</h3>";
|
|
3299
|
+
silos.slice(0, 16).forEach(function (silo) {
|
|
3300
|
+
var row = document.createElement("button");
|
|
3301
|
+
row.type = "button";
|
|
3302
|
+
row.className = "owner-silo-row";
|
|
3303
|
+
row.innerHTML = "<strong></strong><span></span>";
|
|
3304
|
+
row.querySelector("strong").textContent = silo.file_path || "file";
|
|
3305
|
+
row.querySelector("span").textContent = [
|
|
3306
|
+
shortContributor(silo.primary_owner || "unknown"),
|
|
3307
|
+
silo.primary_owner_pct != null ? Math.round(Number(silo.primary_owner_pct || 0) * 100) + "% ownership" : "",
|
|
3308
|
+
(silo.commit_count_total || 0) + " commits"
|
|
3309
|
+
].filter(Boolean).join(" | ");
|
|
3310
|
+
row.addEventListener("click", function () {
|
|
3311
|
+
focusGraphPath(silo.file_path);
|
|
3312
|
+
});
|
|
3313
|
+
siloSection.appendChild(row);
|
|
3314
|
+
});
|
|
3315
|
+
els.ownersList.appendChild(siloSection);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
function renderOwnersSummary(profiles, silos) {
|
|
3320
|
+
els.ownersSummary.textContent = "";
|
|
3321
|
+
if (!profiles.length && !silos.length) return;
|
|
3322
|
+
var totalOwned = profiles.reduce(function (sum, profile) { return sum + Number(profile.primary_owned_files || 0); }, 0);
|
|
3323
|
+
var topOwned = profiles.reduce(function (max, profile) { return Math.max(max, Number(profile.primary_owned_files || 0)); }, 0);
|
|
3324
|
+
var topOwnerShare = totalOwned ? Math.round(topOwned / totalOwned * 100) : 0;
|
|
3325
|
+
var commits90 = profiles.reduce(function (sum, profile) { return sum + Number(profile.commits_90d || 0); }, 0);
|
|
3326
|
+
var maxOwned = Math.max(1, topOwned);
|
|
3327
|
+
els.ownersSummary.appendChild(metricDonut(
|
|
3328
|
+
"Backup coverage",
|
|
3329
|
+
Math.max(0, 100 - topOwnerShare),
|
|
3330
|
+
silos.length ? silos.length + " single-owner file(s) need backup reviewers" : "No loaded ownership silos",
|
|
3331
|
+
silos.length ? "Click silo rows to inspect the file in Graph." : "Keep ownership spread visible before large changes.",
|
|
3332
|
+
silos.length ? "warn" : "ok"
|
|
3333
|
+
));
|
|
3334
|
+
els.ownersSummary.appendChild(metricBars("Owner concentration", profiles.length + " profiles", profiles.slice(0, 4).map(function (profile) {
|
|
3335
|
+
var owned = Number(profile.primary_owned_files || 0);
|
|
3336
|
+
return {
|
|
3337
|
+
label: shortContributor(profile.contributor),
|
|
3338
|
+
value: owned + " files",
|
|
3339
|
+
score: owned / maxOwned * 100,
|
|
3340
|
+
status: owned && Array.isArray(profile.silo_files) && profile.silo_files.length ? "warn" : "ok"
|
|
3341
|
+
};
|
|
3342
|
+
}), "Use this for reviewer routing and bus-factor checks.", silos.length ? "warn" : "ok"));
|
|
3343
|
+
els.ownersSummary.appendChild(metricBars("Recent activity", commits90 + " commits", profiles.slice(0, 4).map(function (profile) {
|
|
3344
|
+
var commits = Number(profile.commits_90d || 0);
|
|
3345
|
+
var maxCommits = Math.max(1, profiles.reduce(function (max, item) { return Math.max(max, Number(item.commits_90d || 0)); }, 0));
|
|
3346
|
+
return {
|
|
3347
|
+
label: shortContributor(profile.contributor),
|
|
3348
|
+
value: commits,
|
|
3349
|
+
score: commits / maxCommits * 100,
|
|
3350
|
+
status: commits ? "ok" : ""
|
|
3351
|
+
};
|
|
3352
|
+
}), "Prefer recent editors for fast review context.", "ok"));
|
|
2119
3353
|
}
|
|
2120
3354
|
|
|
2121
3355
|
function renderReviewQueue() {
|
|
@@ -2123,8 +3357,11 @@
|
|
|
2123
3357
|
var packets = state.pendingPackets || [];
|
|
2124
3358
|
var inbox = state.inbox;
|
|
2125
3359
|
var inboxItems = inbox && Array.isArray(inbox.items) ? inbox.items : [];
|
|
2126
|
-
|
|
3360
|
+
var counts = inbox && inbox.counts ? inbox.counts : {};
|
|
3361
|
+
var openCount = reviewOpenCount(counts, packets, inboxItems);
|
|
3362
|
+
els.reviewCount.textContent = String(openCount);
|
|
2127
3363
|
els.reviewList.textContent = "";
|
|
3364
|
+
if (els.reviewOverview) renderReviewOverview(inbox, packets, inboxItems);
|
|
2128
3365
|
if (!packets.length && !inboxItems.length && !state.reviewText) {
|
|
2129
3366
|
els.reviewList.className = "review-list details-empty";
|
|
2130
3367
|
els.reviewList.textContent = "No pending packets loaded. Launch with `kage viewer --project <repo>` to load review context automatically.";
|
|
@@ -2134,7 +3371,6 @@
|
|
|
2134
3371
|
if (inbox) {
|
|
2135
3372
|
var summary = document.createElement("div");
|
|
2136
3373
|
summary.className = "review-item";
|
|
2137
|
-
var counts = inbox.counts || {};
|
|
2138
3374
|
summary.innerHTML = [
|
|
2139
3375
|
"<div class=\"review-title\"></div>",
|
|
2140
3376
|
"<div class=\"review-meta\"></div>",
|
|
@@ -2151,10 +3387,10 @@
|
|
|
2151
3387
|
summary.querySelector(".review-summary").textContent = Array.isArray(inbox.recommendations) && inbox.recommendations.length
|
|
2152
3388
|
? inbox.recommendations.slice(0, 2).join(" ")
|
|
2153
3389
|
: "No inbox recommendations.";
|
|
2154
|
-
summary.querySelector(".review-risks").textContent =
|
|
3390
|
+
summary.querySelector(".review-risks").textContent = openCount ? "Resolve inbox items before merge" : "Ready for handoff";
|
|
2155
3391
|
els.reviewList.appendChild(summary);
|
|
2156
3392
|
}
|
|
2157
|
-
inboxItems.slice(0,
|
|
3393
|
+
inboxItems.slice(0, 8).forEach(function (entry) {
|
|
2158
3394
|
var item = document.createElement("div");
|
|
2159
3395
|
item.className = "review-item";
|
|
2160
3396
|
item.innerHTML = [
|
|
@@ -2166,7 +3402,9 @@
|
|
|
2166
3402
|
item.querySelector(".review-title").textContent = entry.title || entry.summary || entry.kind;
|
|
2167
3403
|
item.querySelector(".review-meta").textContent = [entry.kind, entry.severity, entry.type, entry.status].filter(Boolean).join(" | ");
|
|
2168
3404
|
item.querySelector(".review-summary").textContent = entry.action || entry.summary || "";
|
|
2169
|
-
item.querySelector(".review-risks").textContent = Array.isArray(entry.reasons) && entry.reasons.length
|
|
3405
|
+
item.querySelector(".review-risks").textContent = Array.isArray(entry.reasons) && entry.reasons.length
|
|
3406
|
+
? trimIntelText(entry.reasons[0], 86)
|
|
3407
|
+
: "Review before handoff";
|
|
2170
3408
|
els.reviewList.appendChild(item);
|
|
2171
3409
|
});
|
|
2172
3410
|
packets.forEach(function (packet) {
|
|
@@ -2182,7 +3420,9 @@
|
|
|
2182
3420
|
item.querySelector(".review-title").textContent = packet.title || packet.id;
|
|
2183
3421
|
item.querySelector(".review-meta").textContent = [packet.type, packet.status, "score " + (quality.score == null ? "n/a" : quality.score + "/100")].filter(Boolean).join(" | ");
|
|
2184
3422
|
item.querySelector(".review-summary").textContent = packet.summary || "";
|
|
2185
|
-
item.querySelector(".review-risks").textContent = Array.isArray(quality.risks) && quality.risks.length
|
|
3423
|
+
item.querySelector(".review-risks").textContent = Array.isArray(quality.risks) && quality.risks.length
|
|
3424
|
+
? "Resolve " + quality.risks.join(", ")
|
|
3425
|
+
: "Evidence looks clean";
|
|
2186
3426
|
els.reviewList.appendChild(item);
|
|
2187
3427
|
});
|
|
2188
3428
|
if (state.reviewText) {
|
|
@@ -2194,25 +3434,58 @@
|
|
|
2194
3434
|
}
|
|
2195
3435
|
}
|
|
2196
3436
|
|
|
3437
|
+
function renderReviewOverview(inbox, packets, inboxItems) {
|
|
3438
|
+
els.reviewOverview.textContent = "";
|
|
3439
|
+
var counts = inbox && inbox.counts ? inbox.counts : {};
|
|
3440
|
+
var pending = Number(firstNumber(counts.pending, packets.length, 0));
|
|
3441
|
+
var stale = Number(firstNumber(counts.stale, 0));
|
|
3442
|
+
var duplicates = Number(firstNumber(counts.duplicates, 0));
|
|
3443
|
+
var missingContext = Number(firstNumber(counts.missing_context, 0));
|
|
3444
|
+
var blockers = reviewOpenCount(counts, packets, inboxItems);
|
|
3445
|
+
els.reviewOverview.appendChild(metricDonut(
|
|
3446
|
+
"Handoff readiness",
|
|
3447
|
+
blockers ? 0 : 100,
|
|
3448
|
+
blockers ? blockers + " review blocker(s) need attention" : "No pending, stale, duplicate, or missing-context memory",
|
|
3449
|
+
blockers ? "Resolve these before trusting branch memory." : "Ready to hand work to another agent or teammate.",
|
|
3450
|
+
blockers ? "warn" : "ok"
|
|
3451
|
+
));
|
|
3452
|
+
els.reviewOverview.appendChild(metricBars("Inbox breakdown", blockers ? blockers + " open" : "clear", [
|
|
3453
|
+
{ label: "Pending", value: pending, score: Math.min(100, pending * 24), status: pending ? "warn" : "ok" },
|
|
3454
|
+
{ label: "Stale", value: stale, score: Math.min(100, stale * 24), status: stale ? "warn" : "ok" },
|
|
3455
|
+
{ label: "Duplicates", value: duplicates, score: Math.min(100, duplicates * 24), status: duplicates ? "warn" : "ok" },
|
|
3456
|
+
{ label: "Missing context", value: missingContext, score: Math.min(100, missingContext * 24), status: missingContext ? "warn" : "ok" }
|
|
3457
|
+
], "These are the only review metrics that should block merge or handoff.", blockers ? "warn" : "ok"));
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
function reviewOpenCount(counts, packets, inboxItems) {
|
|
3461
|
+
var pending = Number(firstNumber(counts && counts.pending, packets && packets.length, 0));
|
|
3462
|
+
var stale = Number(firstNumber(counts && counts.stale, 0));
|
|
3463
|
+
var duplicates = Number(firstNumber(counts && counts.duplicates, 0));
|
|
3464
|
+
var missingContext = Number(firstNumber(counts && counts.missing_context, 0));
|
|
3465
|
+
var counted = pending + stale + duplicates + missingContext;
|
|
3466
|
+
if (counted) return counted;
|
|
3467
|
+
return Array.isArray(inboxItems) ? inboxItems.length : 0;
|
|
3468
|
+
}
|
|
3469
|
+
|
|
2197
3470
|
function renderProof() {
|
|
2198
3471
|
if (!els.proofList) return;
|
|
2199
3472
|
var metrics = state.metrics;
|
|
2200
3473
|
els.proofList.textContent = "";
|
|
2201
3474
|
if (!metrics) {
|
|
2202
3475
|
els.proofStatus.textContent = "not loaded";
|
|
3476
|
+
if (els.proofOverview) els.proofOverview.textContent = "";
|
|
2203
3477
|
els.proofList.className = "proof-list details-empty";
|
|
2204
3478
|
els.proofList.textContent = "Metrics not loaded. Run `kage metrics --project <repo> --json > .agent_memory/metrics.json` or launch with `kage viewer`.";
|
|
2205
3479
|
return;
|
|
2206
3480
|
}
|
|
2207
3481
|
els.proofStatus.textContent = "loaded";
|
|
2208
3482
|
els.proofList.className = "proof-list";
|
|
3483
|
+
if (els.proofOverview) renderProofOverview(metrics, state.reports || {});
|
|
2209
3484
|
var rows = [
|
|
2210
|
-
["
|
|
2211
|
-
["Useful memory", metrics.quality ? metrics.quality.useful_memory_ratio_percent + "%" : "n/a"],
|
|
3485
|
+
["Validation", metrics.harness && metrics.harness.validation_ok ? "clean" : "check"],
|
|
2212
3486
|
["Evidence", metrics.memory_graph ? metrics.memory_graph.evidence_coverage_percent + "%" : "n/a"],
|
|
2213
3487
|
["Pending review", metrics.memory_graph ? String(metrics.memory_graph.pending_packets) : "n/a"],
|
|
2214
|
-
["Recall
|
|
2215
|
-
["Tokens saved", metrics.pain ? String(metrics.pain.estimated_tokens_saved) : metrics.savings ? String(metrics.savings.estimated_tokens_saved_per_recall) : "n/a"]
|
|
3488
|
+
["Recall savings", metrics.pain ? String(metrics.pain.estimated_tokens_saved) : metrics.savings ? String(metrics.savings.estimated_tokens_saved_per_recall) : "n/a"]
|
|
2216
3489
|
];
|
|
2217
3490
|
rows.forEach(function (row) {
|
|
2218
3491
|
var item = document.createElement("div");
|
|
@@ -2224,6 +3497,29 @@
|
|
|
2224
3497
|
});
|
|
2225
3498
|
}
|
|
2226
3499
|
|
|
3500
|
+
function renderProofOverview(metrics, reports) {
|
|
3501
|
+
els.proofOverview.textContent = "";
|
|
3502
|
+
var quality = reports.quality || {};
|
|
3503
|
+
var benchmark = reports.benchmark || {};
|
|
3504
|
+
var gates = Array.isArray(benchmark.gates) ? benchmark.gates : [];
|
|
3505
|
+
var passingGates = gates.filter(function (gate) { return gate.pass; }).length;
|
|
3506
|
+
var gatePercent = gates.length ? Math.round(passingGates / gates.length * 100) : (benchmark.ok ? 100 : 0);
|
|
3507
|
+
var evidence = Number(firstNumber(metrics.memory_graph && metrics.memory_graph.evidence_coverage_percent, quality.evidence_coverage_percent, 0));
|
|
3508
|
+
var pathGrounding = Number(firstNumber(quality.path_grounding_coverage_percent, 0));
|
|
3509
|
+
els.proofOverview.appendChild(metricDonut(
|
|
3510
|
+
"Trust gate",
|
|
3511
|
+
gatePercent,
|
|
3512
|
+
gates.length ? passingGates + " of " + gates.length + " benchmark gates passing" : "Benchmark report not loaded",
|
|
3513
|
+
gatePercent >= 100 ? "Keep this green before publishing or handing off." : "Fix failing proof gates before release.",
|
|
3514
|
+
gatePercent >= 100 ? "ok" : "warn"
|
|
3515
|
+
));
|
|
3516
|
+
els.proofOverview.appendChild(metricBars("Memory quality", evidence + "% evidence", [
|
|
3517
|
+
{ label: "Evidence", value: evidence + "%", score: evidence, status: evidence >= 80 ? "ok" : "warn" },
|
|
3518
|
+
{ label: "Path grounded", value: pathGrounding ? pathGrounding + "%" : "n/a", score: pathGrounding || 0, status: pathGrounding >= 80 ? "ok" : "warn" },
|
|
3519
|
+
{ label: "Useful", value: quality.useful_memory_ratio_percent != null ? quality.useful_memory_ratio_percent + "%" : "n/a", score: Number(quality.useful_memory_ratio_percent || 0), status: Number(quality.useful_memory_ratio_percent || 0) >= 70 ? "ok" : "warn" }
|
|
3520
|
+
], "Trust memory only when it is evidence-backed and path-grounded.", evidence >= 80 ? "ok" : "warn"));
|
|
3521
|
+
}
|
|
3522
|
+
|
|
2227
3523
|
function renderIntelligence() {
|
|
2228
3524
|
if (!els.intelligenceList) return;
|
|
2229
3525
|
var reports = state.reports || {};
|
|
@@ -2236,36 +3532,40 @@
|
|
|
2236
3532
|
return;
|
|
2237
3533
|
}
|
|
2238
3534
|
els.intelligenceList.className = "intelligence-list";
|
|
2239
|
-
cards.forEach(function (card) {
|
|
3535
|
+
normalizeIntelCards(cards).slice(0, 6).forEach(function (card) {
|
|
2240
3536
|
var item = document.createElement("article");
|
|
2241
3537
|
item.className = "intel-card";
|
|
2242
3538
|
item.innerHTML = [
|
|
2243
|
-
"<h3></h3>",
|
|
2244
|
-
"<div class=\"intel-
|
|
2245
|
-
"<
|
|
3539
|
+
"<div class=\"intel-card-head\"><div><h3></h3><span></span></div><strong></strong></div>",
|
|
3540
|
+
"<div class=\"intel-metric-label\"></div>",
|
|
3541
|
+
"<p class=\"intel-highlight\"></p>",
|
|
3542
|
+
"<p class=\"intel-action\"><b>Action:</b> <span></span></p>",
|
|
2246
3543
|
"<ul></ul>"
|
|
2247
3544
|
].join("");
|
|
2248
3545
|
item.querySelector("h3").textContent = card.title;
|
|
2249
|
-
item.querySelector(".intel-
|
|
2250
|
-
item.querySelector(".intel-
|
|
3546
|
+
item.querySelector(".intel-card-head span").textContent = card.kicker;
|
|
3547
|
+
item.querySelector(".intel-card-head strong").textContent = card.metric || "n/a";
|
|
3548
|
+
item.querySelector(".intel-metric-label").textContent = card.metricLabel || "signal";
|
|
3549
|
+
item.querySelector(".intel-highlight").textContent = card.highlight || card.summary || "";
|
|
3550
|
+
item.querySelector(".intel-action span").textContent = card.action || "Review this signal before changing related code.";
|
|
2251
3551
|
var list = item.querySelector("ul");
|
|
2252
|
-
card.rows.slice(0,
|
|
3552
|
+
card.rows.slice(0, 3).forEach(function (row) {
|
|
2253
3553
|
var li = document.createElement("li");
|
|
2254
3554
|
li.innerHTML = "<strong></strong> <span></span>";
|
|
2255
3555
|
li.querySelector("strong").textContent = row[0];
|
|
2256
|
-
li.querySelector("span").textContent = row[1];
|
|
3556
|
+
li.querySelector("span").textContent = trimIntelText(row[1], 92);
|
|
2257
3557
|
list.appendChild(li);
|
|
2258
3558
|
});
|
|
2259
3559
|
els.intelligenceList.appendChild(item);
|
|
2260
3560
|
});
|
|
2261
|
-
var sections = buildIntelligenceSections(reports);
|
|
3561
|
+
var sections = rankIntelligenceSections(buildIntelligenceSections(reports)).slice(0, 4);
|
|
2262
3562
|
if (sections.length) {
|
|
2263
|
-
var
|
|
2264
|
-
|
|
3563
|
+
var deepGrid = document.createElement("div");
|
|
3564
|
+
deepGrid.className = "intel-deep-grid";
|
|
2265
3565
|
sections.forEach(function (section) {
|
|
2266
|
-
|
|
3566
|
+
deepGrid.appendChild(renderIntelligenceSection(section));
|
|
2267
3567
|
});
|
|
2268
|
-
els.intelligenceList.appendChild(
|
|
3568
|
+
els.intelligenceList.appendChild(deepGrid);
|
|
2269
3569
|
}
|
|
2270
3570
|
}
|
|
2271
3571
|
|
|
@@ -2288,32 +3588,46 @@
|
|
|
2288
3588
|
var list = document.createElement("div");
|
|
2289
3589
|
list.className = "intel-section-list";
|
|
2290
3590
|
section.rows.slice(0, section.limit || 8).forEach(function (row) {
|
|
2291
|
-
var
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
3591
|
+
var rowEl = document.createElement(row.path ? "button" : "div");
|
|
3592
|
+
if (row.path) rowEl.type = "button";
|
|
3593
|
+
rowEl.className = classNames("intel-row", row.status && "intel-row-" + safeCssName(row.status), row.path && "clickable");
|
|
3594
|
+
rowEl.innerHTML = [
|
|
2295
3595
|
"<span class=\"intel-row-main\"><strong></strong><em></em></span>",
|
|
2296
3596
|
"<span class=\"intel-row-meta\"></span>",
|
|
2297
3597
|
"<span class=\"intel-row-bar\"><i></i></span>"
|
|
2298
3598
|
].join("");
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
3599
|
+
rowEl.querySelector("strong").textContent = row.label || "";
|
|
3600
|
+
rowEl.querySelector("em").textContent = row.value || "";
|
|
3601
|
+
rowEl.querySelector(".intel-row-meta").textContent = row.meta || "";
|
|
3602
|
+
rowEl.querySelector(".intel-row-bar i").style.width = clamp(Number(row.score || 0), 4, 100) + "%";
|
|
2303
3603
|
if (row.path) {
|
|
2304
|
-
|
|
2305
|
-
|
|
3604
|
+
rowEl.title = "Focus " + row.path + " in the graph";
|
|
3605
|
+
rowEl.setAttribute("aria-label", "Focus " + row.path + " in Graph");
|
|
3606
|
+
rowEl.addEventListener("click", function () {
|
|
2306
3607
|
focusGraphPath(row.path);
|
|
2307
3608
|
});
|
|
2308
|
-
} else {
|
|
2309
|
-
button.disabled = true;
|
|
2310
3609
|
}
|
|
2311
|
-
list.appendChild(
|
|
3610
|
+
list.appendChild(rowEl);
|
|
2312
3611
|
});
|
|
2313
3612
|
panel.appendChild(list);
|
|
2314
3613
|
return panel;
|
|
2315
3614
|
}
|
|
2316
3615
|
|
|
3616
|
+
function rankIntelligenceSections(sections) {
|
|
3617
|
+
return sections.slice().sort(function (a, b) {
|
|
3618
|
+
return intelligenceSectionPriority(a) - intelligenceSectionPriority(b);
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
function intelligenceSectionPriority(section) {
|
|
3623
|
+
var title = String(section && section.title || "").toLowerCase();
|
|
3624
|
+
if (title.indexOf("blast") !== -1 || title.indexOf("risk") !== -1) return 0;
|
|
3625
|
+
if (title.indexOf("onboarding") !== -1 || title.indexOf("decision") !== -1) return 1;
|
|
3626
|
+
if (title.indexOf("module") !== -1 || title.indexOf("health") !== -1) return 2;
|
|
3627
|
+
if (title.indexOf("owner") !== -1 || title.indexOf("contributor") !== -1) return 3;
|
|
3628
|
+
return 4;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
2317
3631
|
function buildIntelligenceSections(reports) {
|
|
2318
3632
|
var sections = [];
|
|
2319
3633
|
var contributors = reports.contributors;
|
|
@@ -2353,7 +3667,7 @@
|
|
|
2353
3667
|
title: "Ownership Map",
|
|
2354
3668
|
kicker: "who owns what",
|
|
2355
3669
|
stat: silos.length + " silos",
|
|
2356
|
-
summary: "
|
|
3670
|
+
summary: "Action: assign backup reviewers for silo files before risky changes.",
|
|
2357
3671
|
rows: rows,
|
|
2358
3672
|
limit: 10,
|
|
2359
3673
|
});
|
|
@@ -2368,7 +3682,7 @@
|
|
|
2368
3682
|
title: "Module Health Map",
|
|
2369
3683
|
kicker: "churn / tests / ownership",
|
|
2370
3684
|
stat: modules.length + " modules",
|
|
2371
|
-
summary: "
|
|
3685
|
+
summary: "Action: start cleanup and test planning from the lowest-score modules.",
|
|
2372
3686
|
rows: modules.slice(0, 8).map(function (item) {
|
|
2373
3687
|
return {
|
|
2374
3688
|
label: item.module,
|
|
@@ -2387,7 +3701,7 @@
|
|
|
2387
3701
|
title: "Onboarding Targets",
|
|
2388
3702
|
kicker: "missing repo lore",
|
|
2389
3703
|
stat: (decisions.coverage_percent != null ? decisions.coverage_percent + "%" : "n/a"),
|
|
2390
|
-
summary: "
|
|
3704
|
+
summary: "Action: capture why-memory for these files before the next agent works there.",
|
|
2391
3705
|
rows: gaps.slice(0, 8).map(function (gap) {
|
|
2392
3706
|
var score = Math.min(100, Number(gap.dependents || 0) * 18 + Number(gap.churn_90d || 0) * 6 + 12);
|
|
2393
3707
|
return {
|
|
@@ -2413,7 +3727,7 @@
|
|
|
2413
3727
|
title: "Architecture Communities",
|
|
2414
3728
|
kicker: "module clusters",
|
|
2415
3729
|
stat: communities.length + " clusters",
|
|
2416
|
-
summary: "
|
|
3730
|
+
summary: "Action: use clusters to understand the graph before changing architecture.",
|
|
2417
3731
|
rows: communities.slice(0, 8).map(function (community) {
|
|
2418
3732
|
var files = community.files || [];
|
|
2419
3733
|
var entrypoints = community.entrypoints || [];
|
|
@@ -2435,7 +3749,7 @@
|
|
|
2435
3749
|
title: "Execution Flows",
|
|
2436
3750
|
kicker: "entrypoint traces",
|
|
2437
3751
|
stat: insights.entry_flows.length + " flows",
|
|
2438
|
-
summary: "
|
|
3752
|
+
summary: "Action: inspect entry files first when debugging runtime behavior.",
|
|
2439
3753
|
rows: insights.entry_flows.slice(0, 8).map(function (flow) {
|
|
2440
3754
|
var path = flow.path || [];
|
|
2441
3755
|
return {
|
|
@@ -2494,7 +3808,7 @@
|
|
|
2494
3808
|
title: "Workspace Map",
|
|
2495
3809
|
kicker: "deps / contracts / co-changes",
|
|
2496
3810
|
stat: workspaceRows.length + " links",
|
|
2497
|
-
summary: "
|
|
3811
|
+
summary: "Action: check linked repos before changing shared packages or contracts.",
|
|
2498
3812
|
rows: workspaceRows,
|
|
2499
3813
|
limit: 14,
|
|
2500
3814
|
});
|
|
@@ -2528,7 +3842,7 @@
|
|
|
2528
3842
|
title: "Blast Radius",
|
|
2529
3843
|
kicker: "change impact",
|
|
2530
3844
|
stat: riskRows.length + " signals",
|
|
2531
|
-
summary: "
|
|
3845
|
+
summary: "Action: review tests, owners, and dependents before editing these targets.",
|
|
2532
3846
|
rows: riskRows,
|
|
2533
3847
|
limit: 10,
|
|
2534
3848
|
});
|
|
@@ -2550,11 +3864,13 @@
|
|
|
2550
3864
|
});
|
|
2551
3865
|
if (!found) {
|
|
2552
3866
|
els.searchInput.value = normalized;
|
|
3867
|
+
showViewerPageInPlace("graph");
|
|
2553
3868
|
scheduleRender();
|
|
2554
3869
|
return;
|
|
2555
3870
|
}
|
|
2556
|
-
|
|
3871
|
+
selectEntity(found.id, true);
|
|
2557
3872
|
els.searchInput.value = normalized;
|
|
3873
|
+
showViewerPageInPlace("graph");
|
|
2558
3874
|
scheduleRender();
|
|
2559
3875
|
}
|
|
2560
3876
|
|
|
@@ -2716,18 +4032,86 @@
|
|
|
2716
4032
|
return cards;
|
|
2717
4033
|
}
|
|
2718
4034
|
|
|
4035
|
+
function normalizeIntelCards(cards) {
|
|
4036
|
+
return cards.map(function (card) {
|
|
4037
|
+
var normalized = Object.assign({}, card);
|
|
4038
|
+
var row = function (label) {
|
|
4039
|
+
var found = (card.rows || []).find(function (item) { return item[0] === label; });
|
|
4040
|
+
return found ? found[1] : null;
|
|
4041
|
+
};
|
|
4042
|
+
if (card.title === "Memory-Code Bridge") {
|
|
4043
|
+
normalized.metric = row("Links") || "0";
|
|
4044
|
+
normalized.metricLabel = "memory-code links";
|
|
4045
|
+
normalized.highlight = "Shows whether saved repo knowledge is tied to actual files, symbols, routes, and tests.";
|
|
4046
|
+
normalized.action = "If this is low, capture memory with concrete paths so agents can recall it during edits.";
|
|
4047
|
+
} else if (card.title === "Change Risk") {
|
|
4048
|
+
var siloText = row("Silos");
|
|
4049
|
+
var siloMatch = siloText && String(siloText).match(/\d+/);
|
|
4050
|
+
normalized.metric = siloMatch ? siloMatch[0] + " silos" : ((card.rows || []).length + " signals");
|
|
4051
|
+
normalized.metricLabel = "risk signals";
|
|
4052
|
+
normalized.highlight = "Flags files with blast radius, test gaps, or ownership concentration.";
|
|
4053
|
+
normalized.action = "Use these rows to pick tests and reviewers before touching risky files.";
|
|
4054
|
+
} else if (card.title === "Contributors") {
|
|
4055
|
+
normalized.metric = (card.rows || []).length + " profiles";
|
|
4056
|
+
normalized.metricLabel = "review routing";
|
|
4057
|
+
normalized.highlight = "Shows who recently touched or owns parts of the repo.";
|
|
4058
|
+
normalized.action = "Use this to find backup reviewers and avoid single-person knowledge bottlenecks.";
|
|
4059
|
+
} else if (card.title === "Decision Memory") {
|
|
4060
|
+
normalized.metric = row("Coverage") || "n/a";
|
|
4061
|
+
normalized.metricLabel = "why-memory coverage";
|
|
4062
|
+
normalized.highlight = "Shows whether important code paths have captured rationale and gotchas.";
|
|
4063
|
+
normalized.action = "Add memory for coverage gaps before future agents rediscover the same context.";
|
|
4064
|
+
} else if (card.title === "Module Health") {
|
|
4065
|
+
normalized.metric = (card.rows || []).length + " modules";
|
|
4066
|
+
normalized.metricLabel = "lowest scores first";
|
|
4067
|
+
normalized.highlight = "Ranks modules by churn, tests, ownership, and graph signals.";
|
|
4068
|
+
normalized.action = "Start with low-score modules when planning cleanup or refactors.";
|
|
4069
|
+
} else if (card.title === "Graph Insights") {
|
|
4070
|
+
normalized.metric = row("Cycles") || row("Communities") || "n/a";
|
|
4071
|
+
normalized.metricLabel = row("Cycles") != null ? "dependency cycles" : "architecture clusters";
|
|
4072
|
+
normalized.highlight = "Explains dense graph structure through central files, cycles, and communities.";
|
|
4073
|
+
normalized.action = "Inspect central files and cycles before making architectural changes.";
|
|
4074
|
+
} else if (card.title === "Workspace") {
|
|
4075
|
+
normalized.metric = row("Repos") || "n/a";
|
|
4076
|
+
normalized.metricLabel = "connected repos";
|
|
4077
|
+
normalized.highlight = "Shows package, route, topic, and co-change links across local repos.";
|
|
4078
|
+
normalized.action = "Check these links before changing shared contracts or packages.";
|
|
4079
|
+
} else if (card.title === "Memory Quality") {
|
|
4080
|
+
normalized.metric = row("Evidence") || row("Useful") || "n/a";
|
|
4081
|
+
normalized.metricLabel = "trust signal";
|
|
4082
|
+
normalized.highlight = "Shows whether memory is evidence-backed, grounded, and reviewable.";
|
|
4083
|
+
normalized.action = "Review pending or weak memory before relying on it for agent handoff.";
|
|
4084
|
+
} else if (card.title === "Benchmark") {
|
|
4085
|
+
normalized.metric = (card.rows || []).filter(function (item) { return String(item[1] || "").indexOf("pass") !== -1; }).length + " pass";
|
|
4086
|
+
normalized.metricLabel = "local proof checks";
|
|
4087
|
+
normalized.highlight = "Shows whether repo memory and graph behavior pass local quality checks.";
|
|
4088
|
+
normalized.action = "Use failed checks as release blockers or cleanup targets.";
|
|
4089
|
+
}
|
|
4090
|
+
return normalized;
|
|
4091
|
+
});
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
function trimIntelText(value, limit) {
|
|
4095
|
+
var text = String(value == null ? "" : value);
|
|
4096
|
+
var max = Number(limit || 90);
|
|
4097
|
+
if (text.length <= max) return text;
|
|
4098
|
+
return text.slice(0, Math.max(0, max - 1)).replace(/\s+\S*$/, "") + "...";
|
|
4099
|
+
}
|
|
4100
|
+
|
|
2719
4101
|
function renderStatusStrip(visibleEntities, visibleEdges, official) {
|
|
2720
4102
|
if (!els.statusStrip) return;
|
|
2721
4103
|
var memoryCount = visibleEntities.filter(function (entity) { return entity.graph_kind === "memory"; }).length;
|
|
2722
4104
|
var codeCount = visibleEntities.filter(function (entity) { return entity.graph_kind === "code"; }).length;
|
|
2723
4105
|
var reviewFlags = visibleEdges.filter(function (edge) { return reviewStatus(edge) !== "ok"; }).length;
|
|
4106
|
+
var memoryCodeLinks = state.edges.filter(isMemoryCodeEdge).length;
|
|
4107
|
+
var pendingReview = official && official.memory_graph ? Number(firstNumber(official.memory_graph.pending_packets, 0)) : 0;
|
|
2724
4108
|
var pills = official ? [
|
|
2725
|
-
["
|
|
2726
|
-
["
|
|
2727
|
-
["
|
|
2728
|
-
["
|
|
2729
|
-
["
|
|
2730
|
-
["Memory
|
|
4109
|
+
["Status", official.harness && official.harness.validation_ok ? "Clean" : "Check", official.harness && official.harness.validation_ok ? "memory" : "warn"],
|
|
4110
|
+
["Review", pendingReview ? pendingReview + " pending" : "Clear", pendingReview ? "warn" : "memory"],
|
|
4111
|
+
["Source map", official.structural_index ? official.structural_index.files + " files" : official.code_graph.files + " files", "code"],
|
|
4112
|
+
["Symbols", String(official.code_graph.symbols), "code"],
|
|
4113
|
+
["Coverage", official.code_graph.indexer_coverage_percent + "%", "code"],
|
|
4114
|
+
["Memory links", String(memoryCodeLinks), memoryCodeLinks ? "memory" : "warn"]
|
|
2731
4115
|
] : [
|
|
2732
4116
|
["Memory", String(memoryCount), "memory"],
|
|
2733
4117
|
["Code", String(codeCount), "code"],
|
|
@@ -2847,11 +4231,12 @@
|
|
|
2847
4231
|
|
|
2848
4232
|
function endCanvasPointer() {
|
|
2849
4233
|
if (state.sim.dragNode) {
|
|
2850
|
-
state.
|
|
4234
|
+
selectEntity(state.sim.dragNode.id, true);
|
|
2851
4235
|
state.sim.dragNode = null;
|
|
2852
4236
|
render();
|
|
2853
4237
|
} else if (state.sim.panning && !state.sim.panning.moved) {
|
|
2854
4238
|
state.selected = null;
|
|
4239
|
+
closeWorkspace();
|
|
2855
4240
|
render();
|
|
2856
4241
|
}
|
|
2857
4242
|
state.sim.panning = null;
|
|
@@ -2888,7 +4273,7 @@
|
|
|
2888
4273
|
var world = canvasToWorld(event);
|
|
2889
4274
|
var node = findCanvasNode(world.x, world.y);
|
|
2890
4275
|
if (!node) return;
|
|
2891
|
-
|
|
4276
|
+
selectEntity(node.id, true);
|
|
2892
4277
|
render();
|
|
2893
4278
|
}
|
|
2894
4279
|
|
|
@@ -2979,10 +4364,15 @@
|
|
|
2979
4364
|
if (state.three.THREE) return Promise.resolve(state.three.THREE);
|
|
2980
4365
|
if (state.three.loading) return state.three.loading;
|
|
2981
4366
|
state.three.failed = false;
|
|
2982
|
-
|
|
2983
|
-
.
|
|
2984
|
-
|
|
2985
|
-
|
|
4367
|
+
var threeSources = [
|
|
4368
|
+
"./vendor/three/build/three.module.min.js",
|
|
4369
|
+
"../vendor/three/build/three.module.min.js",
|
|
4370
|
+
"/vendor/three/build/three.module.min.js",
|
|
4371
|
+
"https://unpkg.com/three@0.184.0/build/three.module.min.js"
|
|
4372
|
+
];
|
|
4373
|
+
state.three.loading = threeSources.reduce(function (chain, source) {
|
|
4374
|
+
return chain.catch(function () { return import(source); });
|
|
4375
|
+
}, Promise.reject())
|
|
2986
4376
|
.then(function (mod) {
|
|
2987
4377
|
state.three.THREE = mod;
|
|
2988
4378
|
return mod;
|
|
@@ -2999,7 +4389,7 @@
|
|
|
2999
4389
|
ensureThree().then(function () {
|
|
3000
4390
|
if (activeRenderMode() !== "3d") return;
|
|
3001
4391
|
setupThreeScene();
|
|
3002
|
-
rebuildThreeScene();
|
|
4392
|
+
if (graphChanged || !state.three.nodeById.size) rebuildThreeScene();
|
|
3003
4393
|
if (graphChanged || state.three.distance <= 0) fitThreeGraph();
|
|
3004
4394
|
startThreeGraph();
|
|
3005
4395
|
renderThreeFrame();
|
|
@@ -3062,8 +4452,9 @@
|
|
|
3062
4452
|
state.sim.nodes.forEach(function (node, index) {
|
|
3063
4453
|
var entity = node.entity;
|
|
3064
4454
|
var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
|
|
4455
|
+
var pathNode = state.pathHighlight && state.pathHighlight.nodes.has(node.id);
|
|
3065
4456
|
var material = new THREE.SpriteMaterial({
|
|
3066
|
-
map: threeNodeTexture(entity, selected),
|
|
4457
|
+
map: threeNodeTexture(entity, selected || pathNode),
|
|
3067
4458
|
transparent: true,
|
|
3068
4459
|
opacity: isDependencyEntity(entity) ? 0.70 : 1,
|
|
3069
4460
|
depthWrite: false
|
|
@@ -3071,7 +4462,7 @@
|
|
|
3071
4462
|
var mesh = new THREE.Sprite(material);
|
|
3072
4463
|
var position = threePosition(node, index);
|
|
3073
4464
|
mesh.position.set(position.x, position.y, position.z);
|
|
3074
|
-
var size = threeNodeSize(node, selected);
|
|
4465
|
+
var size = threeNodeSize(node, selected || pathNode);
|
|
3075
4466
|
mesh.scale.set(size, size, 1);
|
|
3076
4467
|
mesh.userData.node = node;
|
|
3077
4468
|
state.three.nodeGroup.add(mesh);
|
|
@@ -3085,11 +4476,12 @@
|
|
|
3085
4476
|
var fromEntity = from.node.entity;
|
|
3086
4477
|
var toEntity = to.node.entity;
|
|
3087
4478
|
var connected = state.selected && state.selected.kind === "entity" && (state.selected.id === edge.from || state.selected.id === edge.to);
|
|
4479
|
+
var pathEdge = state.pathHighlight && state.pathHighlight.edges.has(edge.id);
|
|
3088
4480
|
var geometry = new THREE.BufferGeometry().setFromPoints([from.mesh.position, to.mesh.position]);
|
|
3089
4481
|
var material = new THREE.LineBasicMaterial({
|
|
3090
|
-
color: new THREE.Color(edgeThemeColor(edge, fromEntity, toEntity)),
|
|
4482
|
+
color: new THREE.Color(pathEdge ? graphPalette.bridge : edgeThemeColor(edge, fromEntity, toEntity)),
|
|
3091
4483
|
transparent: true,
|
|
3092
|
-
opacity: threeEdgeOpacity(edge, fromEntity, toEntity, connected),
|
|
4484
|
+
opacity: pathEdge ? 0.82 : threeEdgeOpacity(edge, fromEntity, toEntity, connected),
|
|
3093
4485
|
depthWrite: false,
|
|
3094
4486
|
depthTest: false
|
|
3095
4487
|
});
|
|
@@ -3426,7 +4818,7 @@
|
|
|
3426
4818
|
: null;
|
|
3427
4819
|
state.three.drag = null;
|
|
3428
4820
|
if (picked) {
|
|
3429
|
-
|
|
4821
|
+
selectEntity(picked.id, true);
|
|
3430
4822
|
render();
|
|
3431
4823
|
}
|
|
3432
4824
|
}
|
|
@@ -3447,7 +4839,7 @@
|
|
|
3447
4839
|
function handleThreeDoubleClick(event) {
|
|
3448
4840
|
var picked = pickThreeNode(event);
|
|
3449
4841
|
if (!picked) return;
|
|
3450
|
-
|
|
4842
|
+
selectEntity(picked.id, true);
|
|
3451
4843
|
render();
|
|
3452
4844
|
}
|
|
3453
4845
|
|
|
@@ -3632,6 +5024,7 @@
|
|
|
3632
5024
|
if (state.pan && state.pan.moved) return;
|
|
3633
5025
|
if (event.target !== els.svg) return;
|
|
3634
5026
|
state.selected = null;
|
|
5027
|
+
closeWorkspace();
|
|
3635
5028
|
render();
|
|
3636
5029
|
}
|
|
3637
5030
|
|