@kage-core/kage-graph-mcp 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +171 -137
- package/dist/daemon.js +78 -55
- package/dist/index.js +24 -182
- package/dist/kernel.js +1236 -297
- package/dist/structural-worker.js +17 -13
- package/package.json +4 -2
- package/viewer/console.js +783 -0
- package/viewer/index.html +338 -281
- package/viewer/app.js +0 -6782
- package/viewer/data.html +0 -296
- package/viewer/graph.html +0 -296
- package/viewer/intel.html +0 -296
- package/viewer/memory.html +0 -367
- package/viewer/owners.html +0 -296
- package/viewer/review.html +0 -307
- package/viewer/styles.css +0 -2878
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
var params = new URLSearchParams(location.search);
|
|
4
|
+
function src(name, fb) { return params.get(name) || fb; }
|
|
5
|
+
var root = "./data/kage/reports/";
|
|
6
|
+
var paths = {
|
|
7
|
+
lifecycle: src("lifecycle", root + "lifecycle.json"),
|
|
8
|
+
trust: src("trust", root + "trust.json"),
|
|
9
|
+
suppressed: src("suppressed", root + "suppressed.json"),
|
|
10
|
+
metrics: src("metrics", "./data/kage/metrics.json"),
|
|
11
|
+
activity: src("activity", root + "activity.json"),
|
|
12
|
+
value: src("value", root + "value.json"),
|
|
13
|
+
};
|
|
14
|
+
var state = { items: [], filter: "all", q: "", metrics: null, graphReady: false, showAll: false };
|
|
15
|
+
|
|
16
|
+
// Theme-aware colors: the canvas graph and donut can't use CSS variables directly,
|
|
17
|
+
// so resolve them once (and again when the OS light/dark preference flips).
|
|
18
|
+
var THEME = {};
|
|
19
|
+
function cssVar(name, fallback) {
|
|
20
|
+
var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
21
|
+
return v || fallback;
|
|
22
|
+
}
|
|
23
|
+
function refreshTheme() {
|
|
24
|
+
THEME = {
|
|
25
|
+
gain: cssVar("--gain", "#0c7a4d"),
|
|
26
|
+
warn: cssVar("--warn", "#9a6b08"),
|
|
27
|
+
code: cssVar("--code", "#155e9c"),
|
|
28
|
+
memory: cssVar("--memory", "#6d49b8"),
|
|
29
|
+
ink: cssVar("--ink", "#1c1e1a"),
|
|
30
|
+
muted: cssVar("--muted", "#5d635b"),
|
|
31
|
+
faint: cssVar("--faint", "#8b9088"),
|
|
32
|
+
paper: cssVar("--paper", "#fffdf9"),
|
|
33
|
+
line: cssVar("--line-strong", "#c9c4b4"),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
refreshTheme();
|
|
37
|
+
if (window.matchMedia) {
|
|
38
|
+
var scheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
39
|
+
if (scheme.addEventListener) scheme.addEventListener("change", function () { refreshTheme(); renderInsights(state.metrics, state.items); });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getJSON(p) { return fetch(p).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); }
|
|
43
|
+
function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }
|
|
44
|
+
function fmt(n) {
|
|
45
|
+
if (n == null || isNaN(n)) return "—";
|
|
46
|
+
if (n >= 1e6) return (n / 1e6).toFixed(n >= 1e7 ? 0 : 1).replace(/\.0$/, "") + "M";
|
|
47
|
+
if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1).replace(/\.0$/, "") + "K";
|
|
48
|
+
return String(n);
|
|
49
|
+
}
|
|
50
|
+
function base(p) { return String(p).split("/").pop(); }
|
|
51
|
+
// The daemon serves report params as absolute paths (.../<repo>/.agent_memory/...),
|
|
52
|
+
// while project_dir is often "." (reports generated with `--project .`). Pull the
|
|
53
|
+
// real repo name from the path; fall back to project_dir basename, then a default.
|
|
54
|
+
function resolveRepoName(metrics, lifecycle) {
|
|
55
|
+
var cands = [paths.lifecycle, paths.trust, paths.metrics, paths.suppressed];
|
|
56
|
+
for (var i = 0; i < cands.length; i++) {
|
|
57
|
+
var m = /\/([^/]+)\/\.agent_memory\//.exec(cands[i] || "");
|
|
58
|
+
if (m && m[1]) return m[1];
|
|
59
|
+
}
|
|
60
|
+
if (metrics && metrics.repo) return metrics.repo;
|
|
61
|
+
var pd = ((metrics && metrics.project_dir) || (lifecycle && lifecycle.project_dir) || "").replace(/\/+$/, "");
|
|
62
|
+
var bn = pd.split("/").pop();
|
|
63
|
+
if (bn && bn !== "." && bn !== "..") return bn;
|
|
64
|
+
return "repository";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- nav ----
|
|
68
|
+
var META = {
|
|
69
|
+
gains: ["kage://gains", "What Kage saved you", "Tokens, dollars, and bad memories caught — receipts, not vibes."],
|
|
70
|
+
overview: ["kage://trust", "Memory trust", "Whether this repo's agent memory can be trusted — at a glance."],
|
|
71
|
+
graph: ["kage://memory-map", "Memory ↔ code map", "Each packet anchored to the files it's grounded in. Hover a node to inspect."],
|
|
72
|
+
memory: ["kage://memory", "Memory", "Every packet Kage has stored, with health and grounding."],
|
|
73
|
+
activity: ["kage://activity", "Activity", "What agents actually recalled and captured here, over time."],
|
|
74
|
+
insights: ["kage://insights", "Insights", "Health, composition, and what Kage has mapped in this repo."],
|
|
75
|
+
};
|
|
76
|
+
function show(name) {
|
|
77
|
+
var btns = document.querySelectorAll("#nav button");
|
|
78
|
+
for (var i = 0; i < btns.length; i++) btns[i].setAttribute("aria-current", btns[i].dataset.section === name ? "true" : "false");
|
|
79
|
+
var secs = document.querySelectorAll(".section");
|
|
80
|
+
for (var j = 0; j < secs.length; j++) secs[j].classList.toggle("active", secs[j].id === "section-" + name);
|
|
81
|
+
var m = META[name] || META.overview;
|
|
82
|
+
document.getElementById("eyebrow").textContent = m[0];
|
|
83
|
+
document.getElementById("title").textContent = m[1];
|
|
84
|
+
document.getElementById("subtitle").textContent = m[2];
|
|
85
|
+
if (name === "graph" && !state.graphReady) { state.graphReady = true; setTimeout(initGraph, 30); }
|
|
86
|
+
if (location.hash !== "#" + name) history.replaceState(null, "", "#" + name);
|
|
87
|
+
}
|
|
88
|
+
document.getElementById("nav").addEventListener("click", function (e) {
|
|
89
|
+
var b = e.target.closest("button[data-section]"); if (b) show(b.dataset.section);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ---- load ----
|
|
93
|
+
Promise.all([getJSON(paths.trust), getJSON(paths.suppressed), getJSON(paths.lifecycle), getJSON(paths.metrics), getJSON(paths.activity), getJSON(paths.value)])
|
|
94
|
+
.then(function (r) { render(r[0], r[1], r[2], r[3], r[4], r[5]); })
|
|
95
|
+
.catch(function () { render(null, null, null, null, null, null); });
|
|
96
|
+
|
|
97
|
+
function render(trust, suppressed, lifecycle, metrics, activity, value) {
|
|
98
|
+
state.items = (lifecycle && lifecycle.items) || [];
|
|
99
|
+
state.metrics = metrics || {};
|
|
100
|
+
state.activity = activity || {};
|
|
101
|
+
document.getElementById("repo").textContent = resolveRepoName(metrics, lifecycle);
|
|
102
|
+
renderGains(value);
|
|
103
|
+
renderHero(trust);
|
|
104
|
+
renderTiles(metrics, state.items);
|
|
105
|
+
renderAttention(state.items, suppressed);
|
|
106
|
+
renderChips(); renderList();
|
|
107
|
+
renderInsights(metrics, state.items);
|
|
108
|
+
renderActivity(activity);
|
|
109
|
+
var start = (location.hash || "").replace("#", "");
|
|
110
|
+
show(META[start] ? start : "gains");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---- gains (value ledger receipts) ----
|
|
114
|
+
// value.json is the raw ledger written by recall: { totals, events[] }. Windows are
|
|
115
|
+
// recomputed here with the same rules as `kage gains` (today = local midnight,
|
|
116
|
+
// 7d = rolling, all-time = totals so trimmed events never lose history).
|
|
117
|
+
var DOLLARS_PER_MILLION_TOKENS = 15;
|
|
118
|
+
function dollars(tokens) { return (tokens / 1e6) * DOLLARS_PER_MILLION_TOKENS; }
|
|
119
|
+
function fmtDollars(tokens) {
|
|
120
|
+
var d = dollars(tokens);
|
|
121
|
+
return "$" + (d >= 100 ? Math.round(d) : d.toFixed(2));
|
|
122
|
+
}
|
|
123
|
+
function summarizeWindow(events, cutoff) {
|
|
124
|
+
var w = { tokens_saved: 0, stale_withheld: 0, recalls: 0, caller_answers: 0 };
|
|
125
|
+
events.forEach(function (e) {
|
|
126
|
+
var at = Date.parse(e.at);
|
|
127
|
+
if (!at || at < cutoff) return;
|
|
128
|
+
if (e.kind === "recall_served") { w.recalls += 1; w.tokens_saved += Math.max(0, e.tokens_saved || 0); }
|
|
129
|
+
else if (e.kind === "stale_withheld") w.stale_withheld += 1;
|
|
130
|
+
else if (e.kind === "caller_answered") w.caller_answers += 1;
|
|
131
|
+
});
|
|
132
|
+
return w;
|
|
133
|
+
}
|
|
134
|
+
function renderGains(value) {
|
|
135
|
+
var hero = document.getElementById("gainsHero");
|
|
136
|
+
var tiles = document.getElementById("gainsTiles");
|
|
137
|
+
var timeline = document.getElementById("gainsTimeline");
|
|
138
|
+
if (!hero) return;
|
|
139
|
+
var events = (value && value.events) || [];
|
|
140
|
+
var totals = (value && value.totals) || null;
|
|
141
|
+
var midnight = new Date(); midnight.setHours(0, 0, 0, 0);
|
|
142
|
+
var today = summarizeWindow(events, midnight.getTime());
|
|
143
|
+
var week = summarizeWindow(events, Date.now() - 7 * 86400000);
|
|
144
|
+
var all = totals || summarizeWindow(events, 0);
|
|
145
|
+
|
|
146
|
+
hero.textContent = "";
|
|
147
|
+
var head = el("div", "r-head");
|
|
148
|
+
head.appendChild(el("span", null, "Kage · savings receipt"));
|
|
149
|
+
head.appendChild(el("span", null, new Date().toISOString().slice(0, 10)));
|
|
150
|
+
hero.appendChild(head);
|
|
151
|
+
var hh = el("div", "r-hero");
|
|
152
|
+
var big = el("div", "big");
|
|
153
|
+
big.appendChild(el("b", null, all.tokens_saved ? "0" : "—"));
|
|
154
|
+
big.appendChild(el("span", "unit", "tokens saved, all time"));
|
|
155
|
+
if (all.tokens_saved) big.appendChild(el("span", "dollars", "≈ " + fmtDollars(all.tokens_saved)));
|
|
156
|
+
hh.appendChild(big);
|
|
157
|
+
hh.appendChild(el("p", "sub", all.tokens_saved
|
|
158
|
+
? "Context your agents did not have to re-read from source, because Kage served grounded memory instead."
|
|
159
|
+
: "No savings recorded yet. Run kage recall or kage_context against this repo and the receipt fills in."));
|
|
160
|
+
hero.appendChild(hh);
|
|
161
|
+
var wins = el("div", "r-windows");
|
|
162
|
+
[["Today", today], ["Last 7 days", week], ["All time", all]].forEach(function (p) {
|
|
163
|
+
var w = el("div", "r-win");
|
|
164
|
+
w.appendChild(el("div", "k", p[0]));
|
|
165
|
+
w.appendChild(el("div", "v", fmt(p[1].tokens_saved) + " tok"));
|
|
166
|
+
w.appendChild(el("div", "d", "≈ " + fmtDollars(p[1].tokens_saved)));
|
|
167
|
+
wins.appendChild(w);
|
|
168
|
+
});
|
|
169
|
+
hero.appendChild(wins);
|
|
170
|
+
var lines = el("div", "r-lines");
|
|
171
|
+
[
|
|
172
|
+
["Recalls served from memory", fmt(all.recalls), "gain"],
|
|
173
|
+
["Stale memories caught & withheld", fmt(all.stale_withheld), all.stale_withheld ? "warn" : ""],
|
|
174
|
+
["Caller questions answered from the graph", fmt(all.caller_answers), ""],
|
|
175
|
+
].forEach(function (li) {
|
|
176
|
+
var row = el("div", "r-line");
|
|
177
|
+
row.appendChild(el("span", null, li[0]));
|
|
178
|
+
row.appendChild(el("span", "dots"));
|
|
179
|
+
row.appendChild(el("b", li[2], li[1]));
|
|
180
|
+
lines.appendChild(row);
|
|
181
|
+
});
|
|
182
|
+
hero.appendChild(lines);
|
|
183
|
+
hero.appendChild(el("div", "r-foot", "estimated at $" + DOLLARS_PER_MILLION_TOKENS + " per 1M input tokens · ledger: .agent_memory/reports/value.json · verify: kage gains"));
|
|
184
|
+
if (all.tokens_saved) countUp(big.querySelector("b"), all.tokens_saved, 900);
|
|
185
|
+
|
|
186
|
+
if (tiles) {
|
|
187
|
+
tiles.textContent = "";
|
|
188
|
+
[
|
|
189
|
+
{ k: "Saved (7 days)", v: fmt(week.tokens_saved), s: "≈ " + fmtDollars(week.tokens_saved) + " of context not re-read", cls: "green" },
|
|
190
|
+
{ k: "Recalls served (7d)", v: fmt(week.recalls), s: fmt(all.recalls) + " all-time", cls: "green" },
|
|
191
|
+
{ k: "Stale caught", v: fmt(all.stale_withheld), s: all.stale_withheld ? "withheld before they misled an agent" : "nothing withheld yet", cls: all.stale_withheld ? "warn" : "" },
|
|
192
|
+
{ k: "Graph answers", v: fmt(all.caller_answers), s: "caller questions answered from the code graph", cls: "code" },
|
|
193
|
+
].forEach(function (d) { var x = el("div", "tile"); x.appendChild(el("div", "k", d.k)); x.appendChild(el("div", "v " + (d.cls || ""), d.v)); x.appendChild(el("div", "s", d.s)); tiles.appendChild(x); });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (timeline) {
|
|
197
|
+
timeline.textContent = ""; timeline.className = "vfeed";
|
|
198
|
+
if (!events.length) { timeline.appendChild(el("div", "empty", "No value events yet. Each recall, withheld stale memory, and graph answer lands here with a timestamp.")); return; }
|
|
199
|
+
var V_ICON = { recall_served: "✓", stale_withheld: "⊘", caller_answered: "◆" };
|
|
200
|
+
events.slice(-30).reverse().forEach(function (e) {
|
|
201
|
+
var row = el("div", "vev " + (e.kind || ""));
|
|
202
|
+
row.appendChild(el("span", "vi", V_ICON[e.kind] || "•"));
|
|
203
|
+
var mid = el("div", "vt");
|
|
204
|
+
if (e.kind === "recall_served") {
|
|
205
|
+
mid.appendChild(document.createTextNode("Recall served — saved "));
|
|
206
|
+
mid.appendChild(el("b", null, "~" + fmt(Math.max(0, e.tokens_saved || 0)) + " tokens"));
|
|
207
|
+
mid.appendChild(document.createTextNode(" vs reading source"));
|
|
208
|
+
} else if (e.kind === "stale_withheld") {
|
|
209
|
+
mid.appendChild(document.createTextNode("Stale memory withheld"));
|
|
210
|
+
if (e.packet_title) { mid.appendChild(document.createTextNode(" — ")); mid.appendChild(el("b", null, e.packet_title)); }
|
|
211
|
+
} else {
|
|
212
|
+
mid.appendChild(document.createTextNode("Caller question answered from the code graph"));
|
|
213
|
+
}
|
|
214
|
+
row.appendChild(mid);
|
|
215
|
+
row.appendChild(el("span", "when", relTime(e.at)));
|
|
216
|
+
timeline.appendChild(row);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---- activity feed (real recorded recalls + captures) ----
|
|
222
|
+
function relTime(iso) {
|
|
223
|
+
var t = Date.parse(iso); if (!t) return "";
|
|
224
|
+
var s = (Date.now() - t) / 1000;
|
|
225
|
+
if (s < 60) return "just now";
|
|
226
|
+
if (s < 3600) return Math.floor(s / 60) + "m ago";
|
|
227
|
+
if (s < 86400) return Math.floor(s / 3600) + "h ago";
|
|
228
|
+
if (s < 86400 * 30) return Math.floor(s / 86400) + "d ago";
|
|
229
|
+
return String(iso).slice(0, 10);
|
|
230
|
+
}
|
|
231
|
+
var EV_ICON = { recall: "↺", capture: "+", supersede: "⇄", deprecate: "✕", update: "✎", promote: "▲", feedback: "✦", other: "•" };
|
|
232
|
+
function renderActivity(activity) {
|
|
233
|
+
var t = (activity && activity.totals) || { events: 0, recalls: 0, captures: 0, recalls_7d: 0 };
|
|
234
|
+
var tiles = document.getElementById("activityTiles");
|
|
235
|
+
if (tiles) {
|
|
236
|
+
tiles.textContent = "";
|
|
237
|
+
[
|
|
238
|
+
{ k: "Recalls (7 days)", v: fmt(t.recalls_7d), s: "agent memory pulls", cls: "green" },
|
|
239
|
+
{ k: "Total recalls", v: fmt(t.recalls), s: "all time, this machine", cls: "green" },
|
|
240
|
+
{ k: "Captures", v: fmt(t.captures), s: "memories written", cls: "memory" },
|
|
241
|
+
{ k: "Events", v: fmt(t.events), s: "in the activity log", cls: "" },
|
|
242
|
+
].forEach(function (d) { var x = el("div", "tile"); x.appendChild(el("div", "k", d.k)); x.appendChild(el("div", "v " + (d.cls || ""), d.v)); x.appendChild(el("div", "s", d.s)); tiles.appendChild(x); });
|
|
243
|
+
}
|
|
244
|
+
// recalls-per-day bars
|
|
245
|
+
var daily = (activity && activity.daily) || [];
|
|
246
|
+
var db = document.getElementById("activityDaily");
|
|
247
|
+
if (db) {
|
|
248
|
+
db.textContent = "";
|
|
249
|
+
if (!daily.length) { db.appendChild(el("div", "empty", "No recalls recorded yet. Run kage recall / kage_context and they'll appear here.")); }
|
|
250
|
+
var dmax = daily.reduce(function (a, b) { return Math.max(a, b.recalls); }, 1);
|
|
251
|
+
daily.forEach(function (d) {
|
|
252
|
+
var col = el("div", "col");
|
|
253
|
+
col.appendChild(el("span", "v", String(d.recalls)));
|
|
254
|
+
var bar = el("span", "bar" + (d.recalls ? "" : " empty")); col.appendChild(bar);
|
|
255
|
+
col.appendChild(el("span", "d", d.day.slice(5)));
|
|
256
|
+
db.appendChild(col);
|
|
257
|
+
setTimeout(function () { bar.style.height = Math.max(3, d.recalls / dmax * 96) + "px"; }, 60);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
// feed
|
|
261
|
+
var feed = document.getElementById("activityFeed");
|
|
262
|
+
if (feed) {
|
|
263
|
+
feed.textContent = ""; feed.className = "feed";
|
|
264
|
+
var events = (activity && activity.events) || [];
|
|
265
|
+
if (!events.length) { feed.appendChild(el("div", "empty", "Nothing recorded yet. As agents recall and capture memory, it streams in here.")); return; }
|
|
266
|
+
events.forEach(function (e) {
|
|
267
|
+
var row = el("div", "ev " + (e.kind || "other"));
|
|
268
|
+
row.appendChild(el("span", "ei", EV_ICON[e.kind] || "•"));
|
|
269
|
+
var mid = el("div");
|
|
270
|
+
mid.appendChild(el("span", "et", e.title || e.kind));
|
|
271
|
+
mid.appendChild(document.createTextNode(" "));
|
|
272
|
+
mid.appendChild(el("span", "ek", e.kind === "recall" ? "recall" : (e.detail || e.kind)));
|
|
273
|
+
row.appendChild(mid);
|
|
274
|
+
row.appendChild(el("span", "when", relTime(e.at)));
|
|
275
|
+
feed.appendChild(row);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---- hero ----
|
|
281
|
+
function renderHero(trust) {
|
|
282
|
+
var hero = document.getElementById("hero");
|
|
283
|
+
var score = trust && typeof trust.trust_score === "number" ? trust.trust_score : null;
|
|
284
|
+
var m = (trust && trust.metrics) || {};
|
|
285
|
+
var status = score == null ? "idle" : (score >= 90 ? "ok" : score >= 70 ? "warn" : "alert");
|
|
286
|
+
hero.setAttribute("data-status", status);
|
|
287
|
+
var bars = [
|
|
288
|
+
["Hallucinated citations rejected", m.hallucinated_citation_rejection_rate],
|
|
289
|
+
["Stale memory excluded from recall", m.stale_memory_exclusion_rate],
|
|
290
|
+
["Live memory grounded to code", m.live_grounding_rate],
|
|
291
|
+
];
|
|
292
|
+
var verdict = score == null ? "Run kage benchmark --trust to score this repo's memory." :
|
|
293
|
+
status === "ok" ? "Agents recall only memory that is grounded in current code." :
|
|
294
|
+
"Some memory needs review before agents should trust it.";
|
|
295
|
+
var left = el("div");
|
|
296
|
+
left.appendChild(el("span", "eyebrow", "Memory Trust"));
|
|
297
|
+
var s = el("div", "score"); s.appendChild(el("b", null, score == null ? "—" : "0")); s.appendChild(el("span", null, "/100"));
|
|
298
|
+
left.appendChild(s); left.appendChild(el("p", "verdict", verdict));
|
|
299
|
+
var right = el("div", "bars");
|
|
300
|
+
bars.forEach(function (b) {
|
|
301
|
+
var has = !(b[1] == null || isNaN(b[1])); var v = has ? Math.max(0, Math.min(100, b[1])) : 0;
|
|
302
|
+
var bar = el("div", "bar");
|
|
303
|
+
bar.appendChild(el("span", "lbl", b[0])); bar.appendChild(el("b", "val", has ? v + "%" : "—"));
|
|
304
|
+
var t = el("span", "track"), f = el("i"); t.appendChild(f); bar.appendChild(t);
|
|
305
|
+
right.appendChild(bar); setTimeout(function () { f.style.width = v + "%"; }, 60);
|
|
306
|
+
});
|
|
307
|
+
hero.textContent = ""; hero.appendChild(left); hero.appendChild(right);
|
|
308
|
+
if (score != null) countUp(s.querySelector("b"), score, 900);
|
|
309
|
+
}
|
|
310
|
+
function countUp(node, to, dur) {
|
|
311
|
+
var start = performance.now();
|
|
312
|
+
function step(now) {
|
|
313
|
+
var t = Math.min(1, (now - start) / dur); var e = 1 - Math.pow(1 - t, 3);
|
|
314
|
+
node.textContent = Math.round(e * to); if (t < 1) requestAnimationFrame(step);
|
|
315
|
+
}
|
|
316
|
+
requestAnimationFrame(step);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---- overview tiles ----
|
|
320
|
+
function counts(items) {
|
|
321
|
+
var c = {}; items.forEach(function (i) { c[i.health] = (c[i.health] || 0) + 1; }); return c;
|
|
322
|
+
}
|
|
323
|
+
function renderTiles(metrics, items) {
|
|
324
|
+
var cg = (metrics && metrics.code_graph) || {};
|
|
325
|
+
var act = (state.activity && state.activity.totals) || {};
|
|
326
|
+
var c = counts(items);
|
|
327
|
+
var review = (c.stale || 0) + (c.disputed || 0) + (c.ungrounded || 0);
|
|
328
|
+
var r7 = act.recalls_7d || 0;
|
|
329
|
+
var data = [
|
|
330
|
+
{ k: "Memory packets", v: fmt(items.length), s: (c.hot || 0) + " hot · " + (c.healthy || 0) + " healthy", cls: "memory" },
|
|
331
|
+
{ k: "Needs review", v: fmt(review), s: review ? "stale or ungrounded" : "all current", cls: review ? "warn" : "green" },
|
|
332
|
+
{ k: "Files mapped", v: fmt(cg.files), s: fmt(cg.symbols) + " symbols indexed", cls: "code" },
|
|
333
|
+
{ k: "Recalls (7 days)", v: fmt(r7), s: r7 ? fmt(act.recalls || 0) + " all-time" : "no recalls yet", cls: r7 ? "green" : "" },
|
|
334
|
+
];
|
|
335
|
+
var box = document.getElementById("tiles"); box.textContent = "";
|
|
336
|
+
data.forEach(function (d) {
|
|
337
|
+
var t = el("div", "tile"); t.appendChild(el("div", "k", d.k));
|
|
338
|
+
t.appendChild(el("div", "v " + (d.cls || ""), d.v)); t.appendChild(el("div", "s", d.s)); box.appendChild(t);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ---- attention ----
|
|
343
|
+
function renderAttention(items, suppressed) {
|
|
344
|
+
var att = [];
|
|
345
|
+
items.forEach(function (i) {
|
|
346
|
+
if (["stale", "disputed", "ungrounded"].indexOf(i.health) !== -1 || i.severity === "blocker")
|
|
347
|
+
att.push({ title: i.title, why: i.reason || (i.stale_reasons && i.stale_reasons[0]) || i.recommended_action, tag: i.health });
|
|
348
|
+
});
|
|
349
|
+
((suppressed && suppressed.items) || []).forEach(function (s) {
|
|
350
|
+
if (!att.some(function (a) { return a.title === s.title; })) att.push({ title: s.title, why: s.reason, tag: "withheld" });
|
|
351
|
+
});
|
|
352
|
+
var mount = document.getElementById("attentionMount"); mount.textContent = "";
|
|
353
|
+
if (!att.length) {
|
|
354
|
+
var ok = el("div", "panel"); ok.appendChild(el("h2", null, "Nothing needs your attention"));
|
|
355
|
+
ok.appendChild(el("div", "empty", "No stale, disputed, or withheld memory. Agents can trust everything in here right now.")); mount.appendChild(ok); return;
|
|
356
|
+
}
|
|
357
|
+
var card = el("div", "alert-card");
|
|
358
|
+
var ah = el("div", "ah"); ah.appendChild(el("h2", null, "Needs your attention")); ah.appendChild(el("span", "c", att.length + " to review")); card.appendChild(ah);
|
|
359
|
+
att.slice(0, 12).forEach(function (a) {
|
|
360
|
+
var row = el("div", "att"); row.appendChild(el("span", "t", a.title)); row.appendChild(el("span", "tag", a.tag));
|
|
361
|
+
row.appendChild(el("span", "why", a.why || "needs review")); card.appendChild(row);
|
|
362
|
+
});
|
|
363
|
+
mount.appendChild(card);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ---- memory list ----
|
|
367
|
+
function renderChips() {
|
|
368
|
+
var c = counts(state.items); c.all = state.items.length;
|
|
369
|
+
var order = ["all", "hot", "healthy", "cold", "stale", "disputed", "ungrounded"];
|
|
370
|
+
var box = document.getElementById("chips"); box.textContent = "";
|
|
371
|
+
order.forEach(function (k) {
|
|
372
|
+
if (k !== "all" && !c[k]) return;
|
|
373
|
+
var b = el("button", "chip"); b.setAttribute("aria-pressed", state.filter === k ? "true" : "false");
|
|
374
|
+
b.innerHTML = (k === "all" ? "All" : k[0].toUpperCase() + k.slice(1)) + " <b>" + (c[k] || 0) + "</b>";
|
|
375
|
+
b.onclick = function () { state.filter = k; state.showAll = false; renderChips(); renderList(); };
|
|
376
|
+
box.appendChild(b);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
var LIST_CAP = 50;
|
|
380
|
+
var HEALTH_LABEL = { hot: "Hot", healthy: "Healthy", cold: "Cold", stale: "Stale", disputed: "Disputed", ungrounded: "Ungrounded", generated: "Generated" };
|
|
381
|
+
function fileSummary(paths) {
|
|
382
|
+
var seen = {}, bases = [];
|
|
383
|
+
(paths || []).forEach(function (p) { var b = base(p); if (!seen[b]) { seen[b] = 1; bases.push(b); } });
|
|
384
|
+
if (!bases.length) return null;
|
|
385
|
+
if (bases.length === 1) return bases[0];
|
|
386
|
+
return bases[0] + " +" + (bases.length - 1);
|
|
387
|
+
}
|
|
388
|
+
function memoryRow(i) {
|
|
389
|
+
var row = el("div", "row"); row.appendChild(el("span", "dot " + (i.health || "")));
|
|
390
|
+
var body = el("div"); body.appendChild(el("div", "title", i.title));
|
|
391
|
+
var meta = el("div", "meta"); meta.appendChild(el("span", "type", i.type || "memory"));
|
|
392
|
+
var fs = fileSummary(i.paths); if (fs) meta.appendChild(el("span", "paths", fs));
|
|
393
|
+
body.appendChild(meta); row.appendChild(body);
|
|
394
|
+
var right = el("div", "right");
|
|
395
|
+
right.appendChild(el("span", "pill " + (i.health || ""), HEALTH_LABEL[i.health] || i.health || "memory"));
|
|
396
|
+
if (i.total_uses) right.appendChild(el("span", "uses", "used " + i.total_uses + "×"));
|
|
397
|
+
row.appendChild(right);
|
|
398
|
+
row.onclick = function () { openDetail(i); };
|
|
399
|
+
return row;
|
|
400
|
+
}
|
|
401
|
+
// packet detail drawer — read a memory's full content, grounding, and status.
|
|
402
|
+
var drawer = document.getElementById("detail"), drawerBg = document.getElementById("detailBackdrop");
|
|
403
|
+
function closeDetail() { if (drawer) drawer.classList.remove("open"); if (drawerBg) drawerBg.classList.remove("open"); }
|
|
404
|
+
function openDetail(it) {
|
|
405
|
+
if (!drawer) return;
|
|
406
|
+
var h = "";
|
|
407
|
+
h += '<button class="dr-x" id="drClose" aria-label="Close">×</button>';
|
|
408
|
+
h += '<div class="dr-tags"><span class="type">' + escapeHtml(it.type || "memory") + '</span>';
|
|
409
|
+
h += '<span class="pill ' + (it.health || "") + '">' + escapeHtml(HEALTH_LABEL[it.health] || it.health || "") + '</span>';
|
|
410
|
+
if (it.status && it.status !== "approved") h += '<span class="pill">' + escapeHtml(it.status) + '</span>';
|
|
411
|
+
h += "</div>";
|
|
412
|
+
h += '<h3 class="dr-title">' + escapeHtml(it.title) + "</h3>";
|
|
413
|
+
if (it.summary) h += '<p class="dr-summary">' + escapeHtml(it.summary) + "</p>";
|
|
414
|
+
if (it.body) h += '<div class="dr-body">' + escapeHtml(it.body) + "</div>";
|
|
415
|
+
var files = (it.paths || []).filter(Boolean);
|
|
416
|
+
if (files.length) h += '<div class="dr-sec"><div class="dr-h">Grounded in</div>' + files.map(function (f) { return '<div class="dr-file">' + escapeHtml(f) + "</div>"; }).join("") + "</div>";
|
|
417
|
+
if (it.tags && it.tags.length) h += '<div class="dr-sec"><div class="dr-h">Tags</div><div class="dr-tagrow">' + it.tags.map(function (t) { return '<span class="tg">' + escapeHtml(t) + "</span>"; }).join("") + "</div></div>";
|
|
418
|
+
var meta = '<div class="dr-row">Used <b>' + (it.total_uses || 0) + "×</b>" + (it.uses_30d ? " (" + it.uses_30d + " in last 30 days)" : "") + "</div>";
|
|
419
|
+
if (it.last_accessed_at) meta += '<div class="dr-row">Last recalled <b>' + escapeHtml(String(it.last_accessed_at).slice(0, 10)) + "</b></div>";
|
|
420
|
+
if (it.stale_reasons && it.stale_reasons.length) meta += '<div class="dr-row warn">' + escapeHtml(it.stale_reasons[0]) + "</div>";
|
|
421
|
+
if (it.action) meta += '<div class="dr-row">' + escapeHtml(it.action) + "</div>";
|
|
422
|
+
h += '<div class="dr-sec"><div class="dr-h">Status</div>' + meta + "</div>";
|
|
423
|
+
drawer.innerHTML = h; drawer.scrollTop = 0; drawer.classList.add("open"); if (drawerBg) drawerBg.classList.add("open");
|
|
424
|
+
var x = document.getElementById("drClose"); if (x) x.onclick = closeDetail;
|
|
425
|
+
}
|
|
426
|
+
if (drawerBg) drawerBg.onclick = closeDetail;
|
|
427
|
+
document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeDetail(); });
|
|
428
|
+
function renderList() {
|
|
429
|
+
var list = document.getElementById("list"); list.textContent = "";
|
|
430
|
+
var q = state.q.trim().toLowerCase();
|
|
431
|
+
var rows = state.items.filter(function (i) {
|
|
432
|
+
if (state.filter !== "all" && i.health !== state.filter) return false;
|
|
433
|
+
if (!q) return true;
|
|
434
|
+
return (i.title || "").toLowerCase().indexOf(q) !== -1 || (i.type || "").toLowerCase().indexOf(q) !== -1 ||
|
|
435
|
+
(i.paths || []).join(" ").toLowerCase().indexOf(q) !== -1;
|
|
436
|
+
});
|
|
437
|
+
var rank = { hot: 0, healthy: 1, stale: 2, disputed: 2, ungrounded: 2, cold: 3, generated: 4 };
|
|
438
|
+
rows.sort(function (a, b) {
|
|
439
|
+
var ra = rank[a.health]; if (ra == null) ra = 5; var rb = rank[b.health]; if (rb == null) rb = 5;
|
|
440
|
+
return ra - rb || (b.total_uses || 0) - (a.total_uses || 0);
|
|
441
|
+
});
|
|
442
|
+
var countEl = document.getElementById("memcount");
|
|
443
|
+
if (countEl) countEl.textContent = rows.length === state.items.length ? rows.length + " packets" : rows.length + " of " + state.items.length + " packets";
|
|
444
|
+
if (!rows.length) { list.appendChild(el("div", "empty", "No memory matches.")); return; }
|
|
445
|
+
|
|
446
|
+
var grouped = state.filter === "all" && !q;
|
|
447
|
+
var cap = state.showAll ? Infinity : LIST_CAP, shown = 0;
|
|
448
|
+
if (grouped) {
|
|
449
|
+
var buckets = [["Needs review", ["stale", "disputed", "ungrounded"]], ["Hot", ["hot"]], ["Healthy", ["healthy"]], ["Cold", ["cold"]], ["Other", ["generated"]]];
|
|
450
|
+
buckets.forEach(function (bk) {
|
|
451
|
+
var brows = rows.filter(function (r) { return bk[1].indexOf(r.health) !== -1; });
|
|
452
|
+
if (!brows.length || shown >= cap) return;
|
|
453
|
+
var head = el("div", "grouphead");
|
|
454
|
+
head.appendChild(el("span", "gl", bk[0])); head.appendChild(el("span", "gc", String(brows.length))); head.appendChild(el("span", "gline"));
|
|
455
|
+
list.appendChild(head);
|
|
456
|
+
brows.forEach(function (r) { if (shown < cap) { list.appendChild(memoryRow(r)); shown++; } });
|
|
457
|
+
});
|
|
458
|
+
} else {
|
|
459
|
+
rows.forEach(function (r) { if (shown < cap) { list.appendChild(memoryRow(r)); shown++; } });
|
|
460
|
+
}
|
|
461
|
+
if (!state.showAll && rows.length > shown) {
|
|
462
|
+
var more = el("button", "showmore", "Show all " + rows.length + " packets");
|
|
463
|
+
more.onclick = function () { state.showAll = true; renderList(); };
|
|
464
|
+
list.appendChild(more);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
var searchEl = document.getElementById("search");
|
|
468
|
+
if (searchEl) searchEl.addEventListener("input", function (e) { state.q = e.target.value; state.showAll = false; renderList(); });
|
|
469
|
+
|
|
470
|
+
// ---- insights ----
|
|
471
|
+
function renderInsights(metrics, items) {
|
|
472
|
+
var cg = (metrics && metrics.code_graph) || {}, mg = (metrics && metrics.memory_graph) || {}, sv = (metrics && metrics.savings) || {};
|
|
473
|
+
var c = counts(items);
|
|
474
|
+
// Health donut = trustworthiness, not recall frequency. "Cold" (not yet recalled)
|
|
475
|
+
// is grounded & current, so it counts as healthy — coloring it grey made a perfectly
|
|
476
|
+
// trustworthy store look dead. Segments: grounded & current vs needs review.
|
|
477
|
+
var needsReview = (c.stale || 0) + (c.disputed || 0) + (c.ungrounded || 0);
|
|
478
|
+
var generated = c.generated || 0;
|
|
479
|
+
var groundedCurrent = items.length - needsReview - generated;
|
|
480
|
+
var seg = [
|
|
481
|
+
{ k: "Grounded & current", v: groundedCurrent, col: THEME.gain },
|
|
482
|
+
{ k: "Needs review", v: needsReview, col: THEME.warn },
|
|
483
|
+
{ k: "Generated", v: generated, col: THEME.code },
|
|
484
|
+
].filter(function (s) { return s.v > 0; });
|
|
485
|
+
var total = items.length || 1;
|
|
486
|
+
var stops = [], acc = 0;
|
|
487
|
+
seg.forEach(function (s) { var from = acc / total * 360, to = (acc + s.v) / total * 360; stops.push(s.col + " " + from + "deg " + to + "deg"); acc += s.v; });
|
|
488
|
+
var donut = document.getElementById("donut");
|
|
489
|
+
donut.style.background = "conic-gradient(" + (stops.join(", ") || THEME.gain + " 0deg 360deg") + ")";
|
|
490
|
+
var pct = Math.round(groundedCurrent / total * 100);
|
|
491
|
+
// The big % is the health readout, so color it by how healthy it is — a high
|
|
492
|
+
// grounded share is green, not a warning. (Amber/red only when it's actually low.)
|
|
493
|
+
var pctClass = pct >= 85 ? "" : pct >= 60 ? "warn" : "danger";
|
|
494
|
+
var dc = document.getElementById("donutCenter"); dc.textContent = "";
|
|
495
|
+
dc.appendChild(el("b", pctClass, pct + "%"));
|
|
496
|
+
var leg = document.getElementById("healthLegend"); leg.textContent = "";
|
|
497
|
+
seg.forEach(function (s) {
|
|
498
|
+
var li = el("div", "li"); var i = el("i"); i.style.background = s.col; li.appendChild(i);
|
|
499
|
+
li.appendChild(document.createTextNode(s.k)); li.appendChild(el("b", null, s.v + " · " + Math.round(s.v / total * 100) + "%")); leg.appendChild(li);
|
|
500
|
+
});
|
|
501
|
+
var cap = document.getElementById("healthCap");
|
|
502
|
+
if (cap) {
|
|
503
|
+
var recalled = items.filter(function (i) { return (i.total_uses || 0) > 0; }).length;
|
|
504
|
+
cap.innerHTML = "<b>" + total + "</b> packets · <b>" + recalled + "</b> recalled by agents so far" +
|
|
505
|
+
(needsReview ? " · <b>" + needsReview + "</b> need review" : " · all current");
|
|
506
|
+
}
|
|
507
|
+
// type bars
|
|
508
|
+
var types = {}; items.forEach(function (i) { var t = i.type || "memory"; types[t] = (types[t] || 0) + 1; });
|
|
509
|
+
var arr = Object.keys(types).map(function (k) { return [k, types[k]]; }).sort(function (a, b) { return b[1] - a[1]; }).slice(0, 7);
|
|
510
|
+
var max = arr.length ? arr[0][1] : 1;
|
|
511
|
+
var tb = document.getElementById("typeBars"); tb.textContent = "";
|
|
512
|
+
arr.forEach(function (p) {
|
|
513
|
+
var row = el("div", "hbar"); row.appendChild(el("span", "n", p[0]));
|
|
514
|
+
var t = el("span", "t"), f = el("i"); t.appendChild(f); row.appendChild(t); row.appendChild(el("span", "c", String(p[1])));
|
|
515
|
+
tb.appendChild(row); setTimeout(function () { f.style.width = (p[1] / max * 100) + "%"; }, 80);
|
|
516
|
+
});
|
|
517
|
+
// most-grounded files: which code carries the most institutional memory
|
|
518
|
+
var fileCounts = {};
|
|
519
|
+
items.forEach(function (i) { (i.paths || []).forEach(function (p) { fileCounts[p] = (fileCounts[p] || 0) + 1; }); });
|
|
520
|
+
var gf = Object.keys(fileCounts).map(function (k) { return [k, fileCounts[k]]; }).sort(function (a, b) { return b[1] - a[1]; }).slice(0, 8);
|
|
521
|
+
var gmax = gf.length ? gf[0][1] : 1;
|
|
522
|
+
var gfb = document.getElementById("groundedFiles"); if (gfb) {
|
|
523
|
+
gfb.textContent = "";
|
|
524
|
+
if (!gf.length) { gfb.appendChild(el("div", "empty", "No memory is grounded to code yet.")); }
|
|
525
|
+
var baseSeen = {}; gf.forEach(function (p) { var b = base(p[0]); baseSeen[b] = (baseSeen[b] || 0) + 1; });
|
|
526
|
+
gf.forEach(function (p) {
|
|
527
|
+
var b = base(p[0]), label = baseSeen[b] > 1 ? p[0].split("/").slice(-2).join("/") : b;
|
|
528
|
+
var row = el("div", "hbar code"); var n = el("span", "n", label); n.title = p[0]; row.appendChild(n);
|
|
529
|
+
var t = el("span", "t"), f = el("i"); t.appendChild(f); row.appendChild(t); row.appendChild(el("span", "c", String(p[1])));
|
|
530
|
+
gfb.appendChild(row); setTimeout(function () { f.style.width = (p[1] / gmax * 100) + "%"; }, 80);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
// code tiles
|
|
534
|
+
var langs = Object.keys(cg.languages || {}).length;
|
|
535
|
+
var ct = [
|
|
536
|
+
["Files", fmt(cg.files)], ["Symbols", fmt(cg.symbols)], ["Routes", fmt(cg.routes)], ["Tests", fmt(cg.tests)],
|
|
537
|
+
["Imports", fmt(cg.imports)], ["Call edges", fmt(cg.calls)], ["Index coverage", (cg.indexer_coverage_percent != null ? cg.indexer_coverage_percent + "%" : "—")], ["Languages", fmt(langs)],
|
|
538
|
+
];
|
|
539
|
+
var ctb = document.getElementById("codeTiles"); ctb.textContent = "";
|
|
540
|
+
ct.forEach(function (d) { var t = el("div", "tile"); t.appendChild(el("div", "k", d[0])); t.appendChild(el("div", "v", d[1])); ctb.appendChild(t); });
|
|
541
|
+
// facts
|
|
542
|
+
var hot = items.filter(function (i) { return i.health === "hot"; }).sort(function (a, b) { return (b.total_uses || 0) - (a.total_uses || 0); })[0];
|
|
543
|
+
var cold = c.cold || 0;
|
|
544
|
+
var facts = [];
|
|
545
|
+
facts.push(["💾", "Every recall pulls grounded memory instead of re-reading source — saving about <b>" + fmt(sv.estimated_tokens_saved_per_recall) + " tokens</b> each time."]);
|
|
546
|
+
if (mg.evidence_coverage_percent != null) facts.push(["🔗", "<b>" + mg.evidence_coverage_percent + "%</b> of " + fmt(mg.edges) + " memory edges are backed by evidence — nothing in the graph is a guess."]);
|
|
547
|
+
if (hot) facts.push(["🔥", "Most-recalled memory: <b>" + escapeHtml(hot.title) + "</b>" + (hot.total_uses ? " (used " + hot.total_uses + "×)." : ".")]);
|
|
548
|
+
if (cold) facts.push(["🧊", "<b>" + cold + "</b> packets haven't been recalled recently — run <code>kage refresh</code> to keep them grounded, or let them age out."]);
|
|
549
|
+
if (mg.average_quality_score != null) facts.push(["⭐", "Average packet quality score is <b>" + mg.average_quality_score + "/100</b> across the repo."]);
|
|
550
|
+
var fb = document.getElementById("facts"); fb.textContent = "";
|
|
551
|
+
facts.forEach(function (f) { var row = el("div", "fact"); row.appendChild(el("span", "em", f[0])); var s = el("span"); s.innerHTML = f[1]; row.appendChild(s); fb.appendChild(row); });
|
|
552
|
+
}
|
|
553
|
+
function escapeHtml(s) { var d = el("div"); d.textContent = s == null ? "" : s; return d.innerHTML; }
|
|
554
|
+
|
|
555
|
+
// ---- memory <-> code graph (interactive canvas) ----
|
|
556
|
+
var G = { nodes: [], edges: [], hover: -1, focus: -1, raf: 0, alpha: 1, filter: "all", view: { s: 1, tx: 0, ty: 0 }, drag: null, pan: null, tween: null };
|
|
557
|
+
// Canvas needs concrete colors; turn a resolved theme hex into rgba at a given alpha.
|
|
558
|
+
function rgba(hex, a) {
|
|
559
|
+
var h = String(hex).replace("#", "");
|
|
560
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
561
|
+
var n = parseInt(h, 16);
|
|
562
|
+
if (isNaN(n)) return hex;
|
|
563
|
+
return "rgba(" + ((n >> 16) & 255) + "," + ((n >> 8) & 255) + "," + (n & 255) + "," + a + ")";
|
|
564
|
+
}
|
|
565
|
+
function seeded(n) { var x = Math.sin(n * 999.137) * 43758.5453; return x - Math.floor(x); }
|
|
566
|
+
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
|
|
567
|
+
function nodeR(nd) { return Math.min(13, 4.5 + nd.deg * 1.1); }
|
|
568
|
+
function buildGraph() {
|
|
569
|
+
var items = state.items.filter(function (i) { return i.paths && i.paths.length; });
|
|
570
|
+
var rank = { hot: 0, healthy: 1, stale: 2, disputed: 2, ungrounded: 2, cold: 3 };
|
|
571
|
+
items.sort(function (a, b) { var ra = rank[a.health] == null ? 4 : rank[a.health], rb = rank[b.health] == null ? 4 : rank[b.health]; return ra - rb || (b.total_uses || 0) - (a.total_uses || 0); });
|
|
572
|
+
items = items.slice(0, 60);
|
|
573
|
+
var nodes = [], edges = [], fileIdx = {};
|
|
574
|
+
items.forEach(function (it, mi) {
|
|
575
|
+
var review = ["stale", "disputed", "ungrounded"].indexOf(it.health) !== -1;
|
|
576
|
+
var label = (it.title || "memory"); if (label.length > 26) label = label.slice(0, 25) + "…";
|
|
577
|
+
nodes.push({ id: "m" + mi, label: label, kind: "memory", review: review, health: it.health, deg: 0, tip: it.title, sub: it.type || "memory", uses: it.total_uses || 0, files: (it.paths || []).map(base) });
|
|
578
|
+
var mIndex = nodes.length - 1;
|
|
579
|
+
it.paths.slice(0, 3).forEach(function (p) {
|
|
580
|
+
var key = "f:" + p, fi = fileIdx[key];
|
|
581
|
+
if (fi == null) { nodes.push({ id: key, label: base(p), kind: "file", deg: 0, tip: p, sub: "code file" }); fi = nodes.length - 1; fileIdx[key] = fi; }
|
|
582
|
+
edges.push([mIndex, fi]); nodes[mIndex].deg++; nodes[fi].deg++;
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
G.nodes = nodes; G.edges = edges; G.alpha = 1;
|
|
586
|
+
}
|
|
587
|
+
function initGraph() {
|
|
588
|
+
var canvas = document.getElementById("graph"); if (!canvas) return;
|
|
589
|
+
buildGraph();
|
|
590
|
+
var ctx = canvas.getContext("2d"), dpr = window.devicePixelRatio || 1, W = 0, H = 0;
|
|
591
|
+
var tip = document.getElementById("gtip");
|
|
592
|
+
function resize() { W = canvas.clientWidth; H = canvas.clientHeight; canvas.width = W * dpr; canvas.height = H * dpr; }
|
|
593
|
+
resize();
|
|
594
|
+
G.nodes.forEach(function (n, i) { n.x = W / 2 + (seeded(i + 1) - 0.5) * W * 0.7; n.y = H / 2 + (seeded(i + 7) - 0.5) * H * 0.7; n.vx = 0; n.vy = 0; n.fixed = false; });
|
|
595
|
+
|
|
596
|
+
function fitView() {
|
|
597
|
+
if (!G.nodes.length) return;
|
|
598
|
+
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
599
|
+
G.nodes.forEach(function (n) { minX = Math.min(minX, n.x); minY = Math.min(minY, n.y); maxX = Math.max(maxX, n.x); maxY = Math.max(maxY, n.y); });
|
|
600
|
+
var pad = 60, gw = (maxX - minX) || 1, gh = (maxY - minY) || 1;
|
|
601
|
+
var s = clamp(Math.min((W - 2 * pad) / gw, (H - 2 * pad) / gh), 0.35, 1.8);
|
|
602
|
+
G.view.s = s; G.view.tx = (W - (minX + maxX) * s) / 2; G.view.ty = (H - (minY + maxY) * s) / 2;
|
|
603
|
+
}
|
|
604
|
+
function s2w(sx, sy) { return { x: (sx - G.view.tx) / G.view.s, y: (sy - G.view.ty) / G.view.s }; }
|
|
605
|
+
function neighbors(idx) { var s = {}; if (idx < 0) return null; s[idx] = 1; G.edges.forEach(function (e) { if (e[0] === idx) s[e[1]] = 1; if (e[1] === idx) s[e[0]] = 1; }); return s; }
|
|
606
|
+
function categorySet() {
|
|
607
|
+
if (G.filter === "all") return null;
|
|
608
|
+
// Seed = the matching memory nodes only. Expand to the files they touch, but
|
|
609
|
+
// gate on the immutable seed — never pull in other memories that merely share a
|
|
610
|
+
// hub file (that cascade made "Needs review" highlight ~everything).
|
|
611
|
+
var seed = {};
|
|
612
|
+
G.nodes.forEach(function (n, i) { if (n.kind === "memory" && (G.filter === "review" ? n.review : n.health === G.filter)) seed[i] = 1; });
|
|
613
|
+
var set = {};
|
|
614
|
+
Object.keys(seed).forEach(function (k) { set[k] = 1; });
|
|
615
|
+
G.edges.forEach(function (e) { if (seed[e[0]]) set[e[1]] = 1; if (seed[e[1]]) set[e[0]] = 1; });
|
|
616
|
+
return set;
|
|
617
|
+
}
|
|
618
|
+
function color(nd) { return nd.kind === "file" ? THEME.code : (nd.review ? THEME.warn : THEME.memory); }
|
|
619
|
+
function bodyColor(nd) { return rgba(THEME.paper, 0.94); }
|
|
620
|
+
var DIAMOND = { decision: 1, bug_fix: 1, test: 1, gotcha: 1 };
|
|
621
|
+
function shapePath(x, y, r, nd) {
|
|
622
|
+
if (nd.kind === "file") { roundRect(x - r * 1.3, y - r * 0.78, r * 2.6, r * 1.56, 3); return; }
|
|
623
|
+
if (DIAMOND[nd.sub]) { ctx.beginPath(); ctx.moveTo(x, y - r); ctx.lineTo(x + r, y); ctx.lineTo(x, y + r); ctx.lineTo(x - r, y); ctx.closePath(); return; }
|
|
624
|
+
ctx.beginPath(); ctx.arc(x, y, r, 0, 6.2832);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function tick() {
|
|
628
|
+
var n = G.nodes, e = G.edges, a = G.alpha;
|
|
629
|
+
if (G.tween) {
|
|
630
|
+
var tw = G.tween, v = G.view;
|
|
631
|
+
v.s += (tw.s - v.s) * 0.2; v.tx += (tw.tx - v.tx) * 0.2; v.ty += (tw.ty - v.ty) * 0.2;
|
|
632
|
+
if (Math.abs(tw.s - v.s) < 0.01 && Math.abs(tw.tx - v.tx) < 0.6 && Math.abs(tw.ty - v.ty) < 0.6) { v.s = tw.s; v.tx = tw.tx; v.ty = tw.ty; G.tween = null; }
|
|
633
|
+
}
|
|
634
|
+
if (a > 0.03) {
|
|
635
|
+
for (var i = 0; i < n.length; i++) for (var j = i + 1; j < n.length; j++) {
|
|
636
|
+
var dx = n[i].x - n[j].x, dy = n[i].y - n[j].y, d2 = dx * dx + dy * dy + 0.01, d = Math.sqrt(d2);
|
|
637
|
+
var rep = 2900 / d2, fx = dx / d * rep, fy = dy / d * rep;
|
|
638
|
+
n[i].vx += fx; n[i].vy += fy; n[j].vx -= fx; n[j].vy -= fy;
|
|
639
|
+
}
|
|
640
|
+
for (var k = 0; k < e.length; k++) {
|
|
641
|
+
var s = n[e[k][0]], t = n[e[k][1]], dx2 = t.x - s.x, dy2 = t.y - s.y, dd = Math.sqrt(dx2 * dx2 + dy2 * dy2) + 0.01;
|
|
642
|
+
var f = (dd - 94) * 0.013, ux = dx2 / dd * f, uy = dy2 / dd * f;
|
|
643
|
+
s.vx += ux; s.vy += uy; t.vx -= ux; t.vy -= uy;
|
|
644
|
+
}
|
|
645
|
+
var cx = W / 2, cy = H / 2;
|
|
646
|
+
for (var m = 0; m < n.length; m++) {
|
|
647
|
+
if (n[m].fixed) { n[m].vx = n[m].vy = 0; continue; }
|
|
648
|
+
n[m].vx += (cx - n[m].x) * 0.0016; n[m].vy += (cy - n[m].y) * 0.0016;
|
|
649
|
+
n[m].vx *= 0.86; n[m].vy *= 0.86;
|
|
650
|
+
n[m].x += n[m].vx * a; n[m].y += n[m].vy * a;
|
|
651
|
+
}
|
|
652
|
+
G.alpha *= 0.992;
|
|
653
|
+
}
|
|
654
|
+
draw();
|
|
655
|
+
G.raf = requestAnimationFrame(tick);
|
|
656
|
+
}
|
|
657
|
+
function draw() {
|
|
658
|
+
var n = G.nodes, v = G.view;
|
|
659
|
+
var hl = G.hover >= 0 ? neighbors(G.hover) : (G.focus >= 0 ? neighbors(G.focus) : null);
|
|
660
|
+
var emph = hl || categorySet(), active = !!emph;
|
|
661
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, W, H);
|
|
662
|
+
ctx.setTransform(dpr * v.s, 0, 0, dpr * v.s, dpr * v.tx, dpr * v.ty);
|
|
663
|
+
ctx.lineWidth = 1 / v.s;
|
|
664
|
+
G.edges.forEach(function (e) {
|
|
665
|
+
var on = active && emph[e[0]] && emph[e[1]];
|
|
666
|
+
ctx.strokeStyle = on ? rgba(THEME.code, 0.7) : (active ? rgba(THEME.faint, 0.08) : rgba(THEME.faint, 0.25));
|
|
667
|
+
ctx.beginPath(); ctx.moveTo(n[e[0]].x, n[e[0]].y); ctx.lineTo(n[e[1]].x, n[e[1]].y); ctx.stroke();
|
|
668
|
+
});
|
|
669
|
+
n.forEach(function (nd, i) {
|
|
670
|
+
var r = nodeR(nd), dim = active && !emph[i], col = color(nd), strong = i === G.hover || i === G.focus;
|
|
671
|
+
ctx.save();
|
|
672
|
+
ctx.globalAlpha = dim ? 0.14 : 1;
|
|
673
|
+
// glowing rim over a dark body
|
|
674
|
+
if (!dim) { ctx.shadowColor = col; ctx.shadowBlur = (strong ? 16 : (nd.kind === "memory" ? 9 : 10)); }
|
|
675
|
+
shapePath(nd.x, nd.y, r, nd); ctx.fillStyle = bodyColor(nd); ctx.fill();
|
|
676
|
+
ctx.shadowBlur = 0;
|
|
677
|
+
ctx.lineWidth = (strong ? 2.4 : 1.3) / v.s; ctx.strokeStyle = col; ctx.stroke();
|
|
678
|
+
// inner dot for memory nodes
|
|
679
|
+
if (nd.kind === "memory") { ctx.globalAlpha = (dim ? 0.14 : 1) * 0.9; ctx.fillStyle = col; ctx.beginPath(); ctx.arc(nd.x, nd.y, Math.max(2.2, r * 0.2), 0, 6.2832); ctx.fill(); }
|
|
680
|
+
ctx.restore();
|
|
681
|
+
// halo on hover/focus
|
|
682
|
+
if (!dim && strong) { ctx.save(); shapePath(nd.x, nd.y, r + 5, nd); ctx.strokeStyle = i === G.focus ? THEME.ink : col; ctx.lineWidth = 1.8 / v.s; ctx.shadowColor = col; ctx.shadowBlur = 9; ctx.stroke(); ctx.restore(); }
|
|
683
|
+
});
|
|
684
|
+
// labels in screen space (mono pill, centered below) so they stay crisp at any zoom
|
|
685
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
686
|
+
n.forEach(function (nd, i) {
|
|
687
|
+
if (active && !emph[i]) return;
|
|
688
|
+
var strong = i === G.hover || i === G.focus;
|
|
689
|
+
// Labels are detail-on-demand only: shown for the hovered/focused node and its
|
|
690
|
+
// highlighted neighbourhood. No always-on labels — they cluttered the graph.
|
|
691
|
+
if (!active) return;
|
|
692
|
+
var sx = nd.x * v.s + v.tx, sy = nd.y * v.s + v.ty, r = nodeR(nd) * v.s;
|
|
693
|
+
if (sx < -60 || sx > W + 60 || sy < -20 || sy > H + 20) return;
|
|
694
|
+
ctx.font = (strong ? "700 " : "600 ") + "11px ui-monospace, Menlo, monospace";
|
|
695
|
+
var w = ctx.measureText(nd.label).width, pw = w + 14, ph = 18, lx = sx - pw / 2, ly = sy + r + 7;
|
|
696
|
+
ctx.globalAlpha = 0.94; ctx.fillStyle = rgba(THEME.paper, 0.94); roundRect(lx, ly, pw, ph, 4); ctx.fill();
|
|
697
|
+
ctx.lineWidth = 1; ctx.strokeStyle = strong ? rgba(THEME.gain, 0.6) : rgba(THEME.line, 0.8); ctx.stroke();
|
|
698
|
+
ctx.globalAlpha = 1; ctx.fillStyle = strong ? THEME.ink : THEME.muted; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
699
|
+
ctx.fillText(nd.label, sx, ly + ph / 2);
|
|
700
|
+
});
|
|
701
|
+
ctx.textAlign = "left"; ctx.textBaseline = "alphabetic"; ctx.globalAlpha = 1;
|
|
702
|
+
}
|
|
703
|
+
function roundRect(x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); }
|
|
704
|
+
function pick(p) {
|
|
705
|
+
var best = -1, bd = Infinity;
|
|
706
|
+
for (var i = 0; i < G.nodes.length; i++) {
|
|
707
|
+
var nd = G.nodes[i], sx = nd.x * G.view.s + G.view.tx, sy = nd.y * G.view.s + G.view.ty;
|
|
708
|
+
var dx = sx - p.x, dy = sy - p.y, d = dx * dx + dy * dy, tol = nodeR(nd) * G.view.s + 8;
|
|
709
|
+
if (d < tol * tol && d < bd) { bd = d; best = i; }
|
|
710
|
+
}
|
|
711
|
+
return best;
|
|
712
|
+
}
|
|
713
|
+
function xy(ev) { var r = canvas.getBoundingClientRect(); return { x: ev.clientX - r.left, y: ev.clientY - r.top }; }
|
|
714
|
+
function showTip(nd, p) {
|
|
715
|
+
tip.style.display = "block"; tip.style.left = Math.min(W - 250, p.x + 14) + "px"; tip.style.top = (p.y + 14) + "px";
|
|
716
|
+
tip.innerHTML = "<b>" + escapeHtml(nd.tip) + "</b><div class='p'>" + escapeHtml(nd.sub) + " · " + nd.deg + " link" + (nd.deg === 1 ? "" : "s") + "</div>";
|
|
717
|
+
}
|
|
718
|
+
function zoomAt(p, factor) {
|
|
719
|
+
G.tween = null;
|
|
720
|
+
var ns = clamp(G.view.s * factor, 0.3, 4), w = s2w(p.x, p.y);
|
|
721
|
+
G.view.s = ns; G.view.tx = p.x - w.x * ns; G.view.ty = p.y - w.y * ns;
|
|
722
|
+
}
|
|
723
|
+
var detail = document.getElementById("gdetail");
|
|
724
|
+
function updateDetail() {
|
|
725
|
+
if (!detail) return;
|
|
726
|
+
if (G.focus < 0) { detail.style.display = "none"; return; }
|
|
727
|
+
var nd = G.nodes[G.focus], html;
|
|
728
|
+
if (nd.kind === "file") {
|
|
729
|
+
html = "<div class='gd-k k-file'>code file</div><b class='gd-t'>" + escapeHtml(nd.tip) + "</b>" +
|
|
730
|
+
"<div class='gd-row'>cited by <b>" + nd.deg + "</b> memor" + (nd.deg === 1 ? "y" : "ies") + "</div>";
|
|
731
|
+
} else {
|
|
732
|
+
html = "<div class='gd-k k-memory'>" + escapeHtml(nd.sub) + "</div><b class='gd-t'>" + escapeHtml(nd.tip) + "</b>" +
|
|
733
|
+
"<div class='gd-row'>health <b class='" + (nd.health || "") + "'>" + (nd.health || "—") + "</b>" + (nd.uses ? " · used " + nd.uses + "×" : "") + " · " + nd.deg + " file" + (nd.deg === 1 ? "" : "s") + "</div>" +
|
|
734
|
+
(nd.files && nd.files.length ? "<div class='gd-files'>" + nd.files.map(escapeHtml).join(" · ") + "</div>" : "");
|
|
735
|
+
}
|
|
736
|
+
detail.innerHTML = "<button class='gd-x' id='gdClose'>×</button>" + html; detail.style.display = "block";
|
|
737
|
+
var x = document.getElementById("gdClose"); if (x) x.onclick = function () { G.focus = -1; updateDetail(); };
|
|
738
|
+
}
|
|
739
|
+
function focusNode(idx) { G.focus = idx; updateDetail(); }
|
|
740
|
+
|
|
741
|
+
canvas.addEventListener("mousedown", function (ev) {
|
|
742
|
+
G.tween = null;
|
|
743
|
+
var p = xy(ev), idx = pick(p);
|
|
744
|
+
if (idx >= 0) { G.drag = { idx: idx, moved: false }; G.nodes[idx].fixed = true; canvas.style.cursor = "grabbing"; }
|
|
745
|
+
else { G.pan = { x: p.x, y: p.y, tx: G.view.tx, ty: G.view.ty, moved: false }; canvas.style.cursor = "grabbing"; }
|
|
746
|
+
});
|
|
747
|
+
canvas.addEventListener("dblclick", function (ev) {
|
|
748
|
+
var p = xy(ev), idx = pick(p); if (idx < 0) return;
|
|
749
|
+
var nd = G.nodes[idx], ns = clamp(Math.max(G.view.s, 1.6), 0.3, 4);
|
|
750
|
+
G.tween = { s: ns, tx: W / 2 - nd.x * ns, ty: H / 2 - nd.y * ns }; focusNode(idx);
|
|
751
|
+
});
|
|
752
|
+
window.addEventListener("mousemove", function (ev) {
|
|
753
|
+
if (G.drag) { var p = xy(ev), w = s2w(p.x, p.y), nd = G.nodes[G.drag.idx]; nd.x = w.x; nd.y = w.y; nd.vx = nd.vy = 0; G.drag.moved = true; G.hover = G.drag.idx; G.alpha = Math.max(G.alpha, 0.3); showTip(nd, p); return; }
|
|
754
|
+
if (G.pan) { var q = xy(ev); G.view.tx = G.pan.tx + (q.x - G.pan.x); G.view.ty = G.pan.ty + (q.y - G.pan.y); G.pan.moved = true; tip.style.display = "none"; return; }
|
|
755
|
+
if (!canvas.matches(":hover")) return;
|
|
756
|
+
var pp = xy(ev), id = pick(pp); G.hover = id; canvas.style.cursor = id >= 0 ? "pointer" : "grab";
|
|
757
|
+
if (id >= 0) showTip(G.nodes[id], pp); else tip.style.display = "none";
|
|
758
|
+
});
|
|
759
|
+
window.addEventListener("mouseup", function () {
|
|
760
|
+
if (G.drag) { if (!G.drag.moved) { var i = G.drag.idx; focusNode(G.focus === i ? -1 : i); G.nodes[i].fixed = false; } G.drag = null; }
|
|
761
|
+
else if (G.pan) { if (!G.pan.moved) focusNode(-1); G.pan = null; }
|
|
762
|
+
canvas.style.cursor = "grab";
|
|
763
|
+
});
|
|
764
|
+
canvas.addEventListener("mouseleave", function () { if (!G.drag && !G.pan) { G.hover = -1; tip.style.display = "none"; } });
|
|
765
|
+
canvas.addEventListener("wheel", function (ev) { ev.preventDefault(); zoomAt(xy(ev), ev.deltaY < 0 ? 1.12 : 0.892); }, { passive: false });
|
|
766
|
+
|
|
767
|
+
var zin = document.getElementById("zoomIn"), zout = document.getElementById("zoomOut"), zr = document.getElementById("resetView");
|
|
768
|
+
if (zin) zin.onclick = function () { zoomAt({ x: W / 2, y: H / 2 }, 1.25); };
|
|
769
|
+
if (zout) zout.onclick = function () { zoomAt({ x: W / 2, y: H / 2 }, 0.8); };
|
|
770
|
+
if (zr) zr.onclick = function () { G.tween = null; focusNode(-1); G.nodes.forEach(function (n) { n.fixed = false; }); G.alpha = 0.5; fitView(); };
|
|
771
|
+
var fbar = document.getElementById("gfilters");
|
|
772
|
+
if (fbar) fbar.addEventListener("click", function (ev) {
|
|
773
|
+
var b = ev.target.closest("button[data-gfilter]"); if (!b) return;
|
|
774
|
+
G.filter = b.dataset.gfilter;
|
|
775
|
+
fbar.querySelectorAll("button").forEach(function (x) { x.setAttribute("aria-pressed", x === b ? "true" : "false"); });
|
|
776
|
+
});
|
|
777
|
+
window.addEventListener("resize", function () { if (document.getElementById("section-graph").classList.contains("active")) { resize(); fitView(); } });
|
|
778
|
+
|
|
779
|
+
canvas.style.cursor = "grab";
|
|
780
|
+
fitView();
|
|
781
|
+
cancelAnimationFrame(G.raf); tick();
|
|
782
|
+
}
|
|
783
|
+
})();
|