@zenera/neo 1.1.11 → 1.1.12

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.
Files changed (60) hide show
  1. package/README.md +3 -3
  2. package/dist/agent.d.ts +8 -7
  3. package/dist/agent.js +15 -8
  4. package/dist/architecture.d.ts +7 -5
  5. package/dist/architecture.js +20 -12
  6. package/dist/index.d.ts +12 -2
  7. package/dist/index.js +12 -2
  8. package/dist/inspect/client.d.ts +1 -1
  9. package/dist/inspect/client.js +42 -26
  10. package/dist/kernel.d.ts +1 -1
  11. package/dist/kernel.js +40 -23
  12. package/dist/memory/files.d.ts +24 -0
  13. package/dist/memory/files.js +85 -0
  14. package/dist/memory/graph.d.ts +56 -0
  15. package/dist/memory/graph.js +159 -0
  16. package/dist/memory/index.d.ts +93 -0
  17. package/dist/memory/index.js +357 -0
  18. package/dist/memory/instructions.d.ts +15 -0
  19. package/dist/memory/instructions.js +103 -0
  20. package/dist/memory/page.d.ts +8 -0
  21. package/dist/memory/page.js +797 -0
  22. package/dist/memory/recall.d.ts +35 -0
  23. package/dist/memory/recall.js +161 -0
  24. package/dist/memory/render.d.ts +10 -0
  25. package/dist/memory/render.js +228 -0
  26. package/dist/memory/report.d.ts +57 -0
  27. package/dist/memory/report.js +113 -0
  28. package/dist/memory/store.d.ts +49 -0
  29. package/dist/memory/store.js +226 -0
  30. package/dist/memory/tools.d.ts +11 -0
  31. package/dist/memory/tools.js +310 -0
  32. package/dist/memory/types.d.ts +192 -0
  33. package/dist/memory/types.js +105 -0
  34. package/dist/memory/vectors.d.ts +29 -0
  35. package/dist/memory/vectors.js +152 -0
  36. package/dist/project/config.d.ts +49 -1
  37. package/dist/project/config.js +63 -0
  38. package/dist/project/index.d.ts +2 -2
  39. package/dist/project/index.js +1 -1
  40. package/dist/project/load.d.ts +16 -2
  41. package/dist/project/load.js +67 -1
  42. package/dist/runner.d.ts +4 -2
  43. package/dist/runner.js +29 -30
  44. package/dist/services.d.ts +23 -6
  45. package/dist/services.js +25 -15
  46. package/dist/tools/workspace.js +195 -17
  47. package/dist/trajectory.d.ts +14 -12
  48. package/dist/types.d.ts +5 -5
  49. package/dist/types.js +3 -3
  50. package/package.json +2 -5
  51. package/dist/memory-stores/factory.d.ts +0 -15
  52. package/dist/memory-stores/factory.js +0 -29
  53. package/dist/memory-stores/file.d.ts +0 -37
  54. package/dist/memory-stores/file.js +0 -174
  55. package/dist/memory-stores/in-memory.d.ts +0 -17
  56. package/dist/memory-stores/in-memory.js +0 -80
  57. package/dist/memory-stores/index.d.ts +0 -4
  58. package/dist/memory-stores/index.js +0 -5
  59. package/dist/memory.d.ts +0 -115
  60. package/dist/memory.js +0 -213
@@ -0,0 +1,797 @@
1
+ import { MERMAID_URL } from "../inspect/report.js";
2
+ /** Report → one standalone HTML document. */
3
+ export function renderMemoryHtml(report, opts = {}) {
4
+ return `<!doctype html>
5
+ <html lang="en">
6
+ <head>
7
+ <meta charset="utf-8">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
9
+ <title>${escapeHtml(report.title)}</title>
10
+ <style>${CSS}</style>
11
+ </head>
12
+ <body>
13
+ <header id="head"></header>
14
+ <main>
15
+ <aside id="side">
16
+ <div class="pad">
17
+ <input id="q" type="search" placeholder="search text, id, metadata…" autocomplete="off">
18
+ <div class="filters">
19
+ <select id="kind"><option value="">every kind</option></select>
20
+ <select id="aud"><option value="">every audience</option></select>
21
+ </div>
22
+ <div class="filters">
23
+ <label><input type="checkbox" id="files"> files only</label>
24
+ <label><input type="checkbox" id="stale"> superseded</label>
25
+ <label><input type="checkbox" id="orphans"> unlinked</label>
26
+ </div>
27
+ </div>
28
+ <ol id="list"></ol>
29
+ </aside>
30
+ <section id="middle">
31
+ <div class="gtools">
32
+ <button data-zoom="out" title="zoom out">−</button>
33
+ <button data-zoom="in" title="zoom in">+</button>
34
+ <button data-zoom="fit" title="fit to window">fit</button>
35
+ <button data-zoom="reset" title="actual size">1:1</button>
36
+ <select id="focus" title="how much of the graph to draw">
37
+ <option value="all">whole graph</option>
38
+ <option value="0">selection only</option>
39
+ <option value="1">1 hop from selection</option>
40
+ <option value="2">2 hops from selection</option>
41
+ <option value="3">3 hops from selection</option>
42
+ <option value="4">4 hops from selection</option>
43
+ <option value="5">5 hops from selection</option>
44
+ </select>
45
+ <span class="hint" id="gcount"></span>
46
+ <span class="hint lvl">100%</span>
47
+ </div>
48
+ <div class="viewport"><div class="canvas"><div class="hint pad">rendering…</div></div></div>
49
+ </section>
50
+ <section id="detail"><div class="hint pad">Pick a node.</div></section>
51
+ </main>
52
+ <script id="memory-data" type="application/json">${embedJson(report)}</script>
53
+ <script type="module">
54
+ const MERMAID_URL = ${embedJson(opts.mermaidUrl ?? MERMAID_URL)};
55
+ ${CLIENT}
56
+ </script>
57
+ </body>
58
+ </html>
59
+ `;
60
+ }
61
+ /**
62
+ * Node text and file contents are model output — untrusted by definition. Two
63
+ * rules keep them inert: they reach the document only as JSON inside a
64
+ * non-executable block, and the page only ever writes them through
65
+ * `textContent`. Escaping every `<`, `>` and `&` makes the first rule
66
+ * unbreakable: no byte sequence in a remembered file can close the script tag.
67
+ */
68
+ function embedJson(value) {
69
+ return JSON.stringify(value)
70
+ .replace(/</g, '\\u003c')
71
+ .replace(/>/g, '\\u003e')
72
+ .replace(/&/g, '\\u0026')
73
+ .replace(/\u2028/g, '\\u2028')
74
+ .replace(/\u2029/g, '\\u2029');
75
+ }
76
+ function escapeHtml(value) {
77
+ return value
78
+ .replace(/&/g, '&amp;')
79
+ .replace(/</g, '&lt;')
80
+ .replace(/>/g, '&gt;')
81
+ .replace(/"/g, '&quot;');
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // Styles
85
+ // ---------------------------------------------------------------------------
86
+ const CSS = `
87
+ :root {
88
+ --bg: #0f1115; --panel: #161922; --line: #262b36; --fg: #d7dbe3; --dim: #8a92a6;
89
+ --accent: #6ea8fe; --ok: #4ec9a0; --err: #f0776c; --warn: #e2b341;
90
+ --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
91
+ }
92
+ * { box-sizing: border-box; }
93
+ body { margin: 0; background: var(--bg); color: var(--fg);
94
+ font: 14px/1.5 system-ui, -apple-system, Segoe UI, sans-serif; }
95
+ header { padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--panel);
96
+ display: flex; flex-wrap: wrap; gap: 8px 20px; align-items: baseline; }
97
+ header h1 { font-size: 15px; margin: 0 12px 0 0; font-weight: 600; }
98
+ header .kv { color: var(--dim); font-size: 12px; }
99
+ header .kv b { color: var(--fg); font-weight: 600; }
100
+ /* The middle column is the point of the page, so it is the one that keeps its
101
+ space: minmax(0,...) stops a wide diagram forcing the grid open, and the
102
+ side panes give way first as the window narrows. */
103
+ main { display: grid; grid-template-columns: 300px minmax(0, 1fr) minmax(300px, 400px);
104
+ height: calc(100vh - 49px); }
105
+ @media (max-width: 1200px) { main { grid-template-columns: 240px minmax(0, 1fr) 300px; } }
106
+ @media (max-width: 900px) {
107
+ main { grid-template-columns: 200px minmax(0, 1fr); }
108
+ #detail { position: fixed; top: 49px; right: 0; bottom: 0; width: min(400px, 55vw);
109
+ box-shadow: -10px 0 30px rgba(0,0,0,.55); z-index: 5; }
110
+ }
111
+ aside { border-right: 1px solid var(--line); overflow: auto; background: var(--panel); }
112
+ .pad { padding: 10px; position: sticky; top: 0; background: var(--panel);
113
+ border-bottom: 1px solid var(--line); }
114
+ #q { width: 100%; padding: 6px 8px; background: var(--bg); color: var(--fg);
115
+ border: 1px solid var(--line); border-radius: 6px; }
116
+ .filters { display: flex; gap: 6px; margin-top: 8px; align-items: center; flex-wrap: wrap; }
117
+ .filters label { color: var(--dim); font-size: 12px; display: flex; gap: 4px; align-items: center; }
118
+ select { background: var(--bg); color: var(--fg); border: 1px solid var(--line);
119
+ border-radius: 6px; padding: 4px 6px; font-size: 12px; flex: 1; min-width: 0; }
120
+ #list { list-style: none; margin: 0; padding: 4px 0; }
121
+ #list li { padding: 7px 10px; border-left: 3px solid transparent; cursor: pointer; }
122
+ #list li:hover { background: #1c2130; }
123
+ #list li.sel { background: #1f2637; border-left-color: var(--accent); }
124
+ #list li.stale { opacity: .5; }
125
+ #list .row { display: flex; gap: 6px; align-items: baseline; }
126
+ #list .id { color: var(--dim); font: 11px var(--mono); margin-left: auto; }
127
+ #list .prev { font-size: 12px; color: var(--fg); white-space: nowrap;
128
+ overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
129
+ #middle { display: flex; flex-direction: column; overflow: hidden; }
130
+ #detail { border-left: 1px solid var(--line); overflow: auto; background: var(--panel); }
131
+ .gtools { display: flex; align-items: center; gap: 6px; padding: 6px 10px;
132
+ border-bottom: 1px solid var(--line); background: var(--panel); }
133
+ .gtools button { background: var(--bg); border: 1px solid var(--line); color: var(--fg);
134
+ border-radius: 6px; padding: 2px 10px; cursor: pointer; font: 12px var(--mono); }
135
+ .gtools button:hover { border-color: var(--accent); }
136
+ .gtools select { flex: 0 0 auto; margin-left: 8px; }
137
+ .gtools .lvl { margin-left: auto; font-family: var(--mono); }
138
+ /* The canvas is transformed, not scrolled: panning has to work past the edges
139
+ of the diagram, which overflow:auto would forbid. */
140
+ .viewport { flex: 1; overflow: hidden; position: relative; cursor: grab;
141
+ touch-action: none; user-select: none; -webkit-user-select: none; }
142
+ .viewport.drag { cursor: grabbing; }
143
+ .canvas { position: absolute; top: 0; left: 0; transform-origin: 0 0; }
144
+ .canvas svg { max-width: none; display: block; }
145
+ .canvas .node { cursor: pointer; }
146
+ /* Selection is a class rather than attributes written onto the shapes, so that
147
+ deselecting is possible: the previous pick has to lose the outline without a
148
+ redraw, and there is nothing to restore it to. */
149
+ .canvas .node.picked rect, .canvas .node.picked polygon,
150
+ .canvas .node.picked path, .canvas .node.picked circle {
151
+ stroke: var(--accent) !important; stroke-width: 3px !important; }
152
+ .hint { color: var(--dim); font-size: 12px; }
153
+ .hint.pad { padding: 16px; position: static; border: none; background: none; }
154
+ .kind { font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
155
+ padding: 1px 6px; border-radius: 4px; background: #232a38; color: var(--dim); }
156
+ .kind.task { background: #1e3357; color: #9dc1ff; }
157
+ .kind.plan { background: #3a2450; color: #d1a6ff; }
158
+ .kind.fact { background: #14392c; color: #8ee0c2; }
159
+ .kind.snippet { background: #1e3d33; color: #8ee0c2; }
160
+ .kind.file { background: #40331a; color: #f0dfb4; }
161
+ .kind.operation { background: #3d2a1b; color: #f2d9c2; }
162
+ .kind.preference { background: #3d1f2c; color: #f5b8cd; }
163
+ #detail .body { padding: 16px; }
164
+ .dt { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 0 0 10px; }
165
+ .idrow { display: flex; gap: 8px; align-items: center; margin: 0 0 14px; }
166
+ .idrow code { flex: 1; min-width: 0; font: 12px var(--mono); color: var(--fg);
167
+ background: var(--bg); border: 1px solid var(--line); border-radius: 6px;
168
+ padding: 4px 8px; word-break: break-all; }
169
+ .idrow button { flex: 0 0 auto; background: var(--bg); border: 1px solid var(--line);
170
+ color: var(--dim); border-radius: 6px; padding: 4px 9px; font: 11px var(--mono);
171
+ cursor: pointer; }
172
+ .idrow button:hover { border-color: var(--accent); color: var(--fg); }
173
+ /* Two columns, so the values line up and the eye can run down them; a wrapping
174
+ row of key-value pairs made every field look like part of the one before. */
175
+ dl.props { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 5px 14px;
176
+ margin: 0 0 14px; font: 12px var(--mono); }
177
+ dl.props dt { color: var(--dim); }
178
+ dl.props dd { margin: 0; word-break: break-word; }
179
+ section.blk { border: 1px solid var(--line); border-radius: 8px; margin-bottom: 12px;
180
+ background: var(--bg); overflow: hidden; }
181
+ section.blk > h3 { margin: 0; padding: 7px 12px; font-size: 12px; font-weight: 600;
182
+ color: var(--dim); border-bottom: 1px solid var(--line); letter-spacing: .03em; }
183
+ section.blk > h3 .tag { float: right; font-weight: 400; text-transform: none; }
184
+ pre { margin: 0; padding: 10px 12px; font: 12px/1.55 var(--mono); white-space: pre-wrap;
185
+ word-break: break-word; overflow-wrap: anywhere; }
186
+ img.media { display: block; max-width: 100%; margin: 10px 12px; border: 1px solid var(--line);
187
+ border-radius: 6px; background: #0d1017; }
188
+ .empty { padding: 10px 12px; color: var(--dim); font-size: 12px; font-style: italic; }
189
+ ul.links { list-style: none; margin: 0; padding: 0; }
190
+ ul.links li { display: grid; grid-template-columns: 96px auto minmax(0, 1fr); gap: 8px;
191
+ align-items: baseline; padding: 6px 12px; }
192
+ ul.links li + li { border-top: 1px solid var(--line); }
193
+ ul.links .rel { font: 11px var(--mono); color: var(--warn); white-space: nowrap; }
194
+ button.link { background: none; border: none; color: var(--accent); cursor: pointer;
195
+ font: 12px/1.4 system-ui, sans-serif; padding: 0; text-align: left;
196
+ overflow-wrap: anywhere; }
197
+ button.link:hover { text-decoration: underline; }
198
+ .badge { font: 11px var(--mono); color: var(--dim); }
199
+ .badge.warn { color: var(--warn); }
200
+ `;
201
+ // ---------------------------------------------------------------------------
202
+ // The client
203
+ //
204
+ // Plain ES2015 in a module, no build step and no dependencies but Mermaid.
205
+ // Written with string concatenation rather than template literals only because
206
+ // it lives inside one.
207
+ // ---------------------------------------------------------------------------
208
+ const CLIENT = String.raw `
209
+ const DATA = JSON.parse(document.getElementById('memory-data').textContent);
210
+ const NODES = DATA.nodes;
211
+ const EDGES = DATA.edges;
212
+ const BY_ID = new Map(NODES.map(function (n) { return [n.id, n]; }));
213
+
214
+ // Short synthetic keys, stable across redraws, because a Mermaid id has to
215
+ // survive being pasted into a DOM id and read back out of one.
216
+ const KEY = new Map();
217
+ const ID = new Map();
218
+ NODES.forEach(function (n, i) { KEY.set(n.id, 'n' + i); ID.set('n' + i, n.id); });
219
+
220
+ const ADJ = new Map();
221
+ function adj(id) {
222
+ if (!ADJ.has(id)) ADJ.set(id, []);
223
+ return ADJ.get(id);
224
+ }
225
+ EDGES.forEach(function (e) {
226
+ adj(e.source).push({ other: e.target, rel: e.relation, out: true });
227
+ adj(e.target).push({ other: e.source, rel: e.relation, out: false });
228
+ });
229
+
230
+ let sel = null;
231
+
232
+ // --- small builders -------------------------------------------------------
233
+
234
+ function el(tag, cls, text) {
235
+ const n = document.createElement(tag);
236
+ if (cls) n.className = cls;
237
+ if (text != null) n.textContent = text;
238
+ return n;
239
+ }
240
+
241
+ function short(id) { return id.length > 10 ? id.slice(0, 4) + '…' + id.slice(-4) : id; }
242
+
243
+ function clip(s, n) {
244
+ const one = String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
245
+ return one.length > n ? one.slice(0, n - 1) + '…' : one;
246
+ }
247
+
248
+ function bytes(n) {
249
+ if (n < 1024) return n + ' B';
250
+ if (n < 1048576) return (n / 1024).toFixed(1) + ' KiB';
251
+ return (n / 1048576).toFixed(1) + ' MiB';
252
+ }
253
+
254
+ /** Timestamps are read to answer "is this still current?", which an ISO string
255
+ * makes you do arithmetic for. The exact value stays, as the title. */
256
+ function ago(iso) {
257
+ const t = Date.parse(iso);
258
+ if (!t) return iso || '—';
259
+ const s = (Date.now() - t) / 1000;
260
+ if (s < 90) return 'just now';
261
+ if (s < 5400) return Math.round(s / 60) + ' min ago';
262
+ if (s < 172800) return Math.round(s / 3600) + ' h ago';
263
+ if (s < 5184000) return Math.round(s / 86400) + ' days ago';
264
+ return new Date(t).toISOString().slice(0, 10);
265
+ }
266
+
267
+ /**
268
+ * Text as at most 'max' lines of roughly 'width' characters. A diagram label is
269
+ * one long line otherwise, and Mermaid will not break it: every node ends up as
270
+ * wide as its sentence and the graph is unreadable at any zoom that fits.
271
+ */
272
+ function wrap(text, width, max) {
273
+ const words = String(text == null ? '' : text).replace(/\s+/g, ' ').trim().split(' ');
274
+ const out = [];
275
+ let cur = '';
276
+ let i = 0;
277
+ for (; i < words.length; i++) {
278
+ const w = words[i].length > width ? words[i].slice(0, width - 1) + '…' : words[i];
279
+ if (!w) continue;
280
+ if (!cur) cur = w;
281
+ else if (cur.length + 1 + w.length <= width) cur += ' ' + w;
282
+ else if (out.length + 1 < max) { out.push(cur); cur = w; }
283
+ else break;
284
+ }
285
+ if (cur) out.push(cur);
286
+ const last = out.length - 1;
287
+ if (i < words.length && last >= 0 && out[last].slice(-1) !== '…') out[last] += '…';
288
+ return out;
289
+ }
290
+
291
+ function block(title, tag) {
292
+ const s = el('section', 'blk');
293
+ const h = el('h3', null, title);
294
+ if (tag) h.appendChild(el('span', 'tag', tag));
295
+ s.appendChild(h);
296
+ return s;
297
+ }
298
+
299
+ function textBlock(title, body, tag) {
300
+ const s = block(title, tag);
301
+ if (body) s.appendChild(el('pre', null, body));
302
+ else s.appendChild(el('div', 'empty', 'empty'));
303
+ return s;
304
+ }
305
+
306
+ function prop(list, key, value, title) {
307
+ list.appendChild(el('dt', null, key));
308
+ const d = el('dd', null, value);
309
+ if (title) d.title = title;
310
+ list.appendChild(d);
311
+ }
312
+
313
+ // --- header ---------------------------------------------------------------
314
+
315
+ (function head() {
316
+ const h = document.getElementById('head');
317
+ h.appendChild(el('h1', null, DATA.title));
318
+ const bits = [
319
+ ['nodes', String(NODES.length)],
320
+ ['edges', String(EDGES.length)],
321
+ ];
322
+ if (DATA.embedding) {
323
+ bits.push(['embedding', DATA.embedding.model + ' · ' + DATA.embedding.dimensions + 'd']);
324
+ } else {
325
+ bits.push(['embedding', 'none — recall falls back to term overlap']);
326
+ }
327
+ if (DATA.vectors) bits.push(['vectors', DATA.vectors.rows + ' rows · ' + bytes(DATA.vectors.bytes)]);
328
+ bits.push(['dir', DATA.dir]);
329
+ bits.forEach(function (b) {
330
+ const s = el('span', 'kv');
331
+ s.appendChild(document.createTextNode(b[0] + ' '));
332
+ s.appendChild(el('b', null, b[1]));
333
+ h.appendChild(s);
334
+ });
335
+ // A vector row per node is the healthy state; anything else means a commit
336
+ // ran without an embedder and that memory cannot be found by meaning.
337
+ if (DATA.vectors && DATA.vectors.rows !== NODES.length) {
338
+ h.appendChild(el('span', 'badge warn',
339
+ '⚠ ' + (NODES.length - DATA.vectors.rows) + ' node(s) have no vector'));
340
+ }
341
+ })();
342
+
343
+ // --- filters --------------------------------------------------------------
344
+
345
+ const q = document.getElementById('q');
346
+ const kindSel = document.getElementById('kind');
347
+ const audSel = document.getElementById('aud');
348
+ const filesOnly = document.getElementById('files');
349
+ const staleOnly = document.getElementById('stale');
350
+ const orphansOnly = document.getElementById('orphans');
351
+ const focusSel = document.getElementById('focus');
352
+
353
+ DATA.kinds.forEach(function (k) { kindSel.appendChild(new Option(k, k)); });
354
+ DATA.audiences.forEach(function (a) { audSel.appendChild(new Option(a, a)); });
355
+
356
+ function matches(n) {
357
+ if (kindSel.value && n.kind !== kindSel.value) return false;
358
+ if (audSel.value && n.audience.indexOf(audSel.value) < 0) return false;
359
+ if (filesOnly.checked && !n.file) return false;
360
+ if (staleOnly.checked && !n.stale) return false;
361
+ if (orphansOnly.checked && n.degree !== 0) return false;
362
+ const term = q.value.trim().toLowerCase();
363
+ if (!term) return true;
364
+ return haystack(n).indexOf(term) >= 0;
365
+ }
366
+
367
+ function haystack(n) {
368
+ if (n._hay == null) {
369
+ n._hay = [n.id, n.kind, n.text, n.audience.join(' '),
370
+ n.file ? n.file.path : '', n.metadata ? JSON.stringify(n.metadata) : '']
371
+ .join(' ').toLowerCase();
372
+ }
373
+ return n._hay;
374
+ }
375
+
376
+ function visible() { return NODES.filter(matches); }
377
+
378
+ [q, kindSel, audSel, filesOnly, staleOnly, orphansOnly].forEach(function (c) {
379
+ c.addEventListener('input', refresh);
380
+ });
381
+ focusSel.addEventListener('input', drawGraph);
382
+
383
+ // --- the list -------------------------------------------------------------
384
+
385
+ function renderList() {
386
+ const list = document.getElementById('list');
387
+ const rows = visible();
388
+ list.replaceChildren();
389
+ if (!rows.length) {
390
+ list.appendChild(el('li', 'hint pad', NODES.length ? 'nothing matches' : 'this memory is empty'));
391
+ return;
392
+ }
393
+ rows.forEach(function (n) {
394
+ const li = el('li', n.stale ? 'stale' : null);
395
+ li.dataset.id = n.id;
396
+ if (n.id === sel) li.classList.add('sel');
397
+ const row = el('div', 'row');
398
+ row.appendChild(el('span', 'kind ' + n.kind, n.kind));
399
+ if (n.file) row.appendChild(el('span', 'badge', '⎘ ' + n.file.format));
400
+ row.appendChild(el('span', 'id', short(n.id)));
401
+ li.appendChild(row);
402
+ li.appendChild(el('div', 'prev', clip(n.text, 90)));
403
+ li.addEventListener('click', function () { select(n.id); });
404
+ list.appendChild(li);
405
+ });
406
+ }
407
+
408
+ // --- the detail pane ------------------------------------------------------
409
+
410
+ function renderDetail() {
411
+ const pane = document.getElementById('detail');
412
+ pane.replaceChildren();
413
+ const n = sel && BY_ID.get(sel);
414
+ if (!n) {
415
+ pane.appendChild(el('div', 'hint pad', 'Pick a node.'));
416
+ return;
417
+ }
418
+ const body = el('div', 'body');
419
+
420
+ const h = el('div', 'dt');
421
+ h.appendChild(el('span', 'kind ' + n.kind, n.kind));
422
+ if (n.file) h.appendChild(el('span', 'badge', '⎘ ' + n.file.format));
423
+ if (n.stale) h.appendChild(el('span', 'badge warn', 'superseded'));
424
+ body.appendChild(h);
425
+
426
+ // The id is the thing you carry to 'zen memory show', so it is whole, on its
427
+ // own line, and takeable in one click.
428
+ const idrow = el('div', 'idrow');
429
+ idrow.appendChild(el('code', null, n.id));
430
+ const copy = el('button', null, 'copy');
431
+ copy.addEventListener('click', function () {
432
+ navigator.clipboard.writeText(n.id).then(function () {
433
+ copy.textContent = 'copied';
434
+ setTimeout(function () { copy.textContent = 'copy'; }, 1200);
435
+ }, function () { copy.textContent = 'blocked'; });
436
+ });
437
+ idrow.appendChild(copy);
438
+ body.appendChild(idrow);
439
+
440
+ const props = el('dl', 'props');
441
+ prop(props, 'audience', n.audience.join(', ') || '—');
442
+ prop(props, 'recalled', n.useCount + '× · last ' + ago(n.lastUsedAt), n.lastUsedAt);
443
+ prop(props, 'created', ago(n.createdAt), n.createdAt);
444
+ if (n.updatedAt !== n.createdAt) prop(props, 'updated', ago(n.updatedAt), n.updatedAt);
445
+ prop(props, 'revision', String(n.revision));
446
+ body.appendChild(props);
447
+
448
+ body.appendChild(textBlock('Text', n.text));
449
+
450
+ if (n.metadata) {
451
+ body.appendChild(textBlock('Metadata', JSON.stringify(n.metadata, null, 2)));
452
+ }
453
+
454
+ if (n.file) {
455
+ const tag = n.file.path + ' · ' + bytes(n.file.bytes);
456
+ if (n.image) {
457
+ const s = block('File', tag);
458
+ const img = el('img', 'media');
459
+ img.src = n.image;
460
+ img.alt = n.file.path;
461
+ s.appendChild(img);
462
+ body.appendChild(s);
463
+ } else if (n.content != null) {
464
+ body.appendChild(textBlock('File', n.content, tag));
465
+ } else {
466
+ const s = block('File', tag);
467
+ s.appendChild(el('div', 'empty', EXCUSE[n.omitted] || 'not shown'));
468
+ body.appendChild(s);
469
+ }
470
+ body.appendChild(textBlock('Digest', n.file.sha256));
471
+ }
472
+
473
+ const links = adj(n.id);
474
+ const s = block('Links', String(links.length));
475
+ if (!links.length) {
476
+ s.appendChild(el('div', 'empty', 'nothing points here and it points nowhere'));
477
+ } else {
478
+ const ul = el('ul', 'links');
479
+ // Outgoing first: PRODUCED read forwards is the story of the node, and
480
+ // mixing the two directions made every row need reading twice.
481
+ links.slice().sort(function (a, b) { return (a.out ? 0 : 1) - (b.out ? 0 : 1); })
482
+ .forEach(function (l) {
483
+ const other = BY_ID.get(l.other);
484
+ const li = el('li');
485
+ li.appendChild(el('span', 'rel', (l.out ? '→ ' : '← ') + l.rel));
486
+ li.appendChild(el('span', 'kind ' + (other ? other.kind : ''), other ? other.kind : '?'));
487
+ const b = el('button', 'link', other ? clip(other.text, 70) || short(l.other) : short(l.other));
488
+ b.title = l.other;
489
+ b.addEventListener('click', function () { select(l.other); });
490
+ li.appendChild(b);
491
+ ul.appendChild(li);
492
+ });
493
+ s.appendChild(ul);
494
+ }
495
+ body.appendChild(s);
496
+ pane.appendChild(body);
497
+ }
498
+
499
+ const EXCUSE = {
500
+ 'too-big': 'too big to inline — read it from the memory directory',
501
+ binary: 'binary, so there is nothing to show',
502
+ missing: 'the graph records this file but it is not on disk',
503
+ unreadable: 'on disk but could not be read'
504
+ };
505
+
506
+ // --- selection ------------------------------------------------------------
507
+
508
+ function select(id, fromGraph) {
509
+ sel = id;
510
+ renderList();
511
+ renderDetail();
512
+ const li = document.querySelector('#list li[data-id="' + cssEscape(id) + '"]');
513
+ if (li) li.scrollIntoView({ block: 'nearest' });
514
+ if (focusSel.value !== 'all') drawGraph();
515
+ else markSelected();
516
+ if (!fromGraph) reveal(id);
517
+ }
518
+
519
+ function cssEscape(v) { return String(v).replace(/["\\]/g, '\\$&'); }
520
+
521
+ function markSelected() {
522
+ const key = sel && KEY.get(sel);
523
+ document.querySelectorAll('.canvas .node').forEach(function (g) {
524
+ g.classList.toggle('picked', keyOf(g) === key);
525
+ });
526
+ }
527
+
528
+ /** Scrolls a node into view in the diagram, when it is drawn at all. */
529
+ function reveal(id) {
530
+ const key = KEY.get(id);
531
+ const g = [].find.call(document.querySelectorAll('.canvas .node'), function (x) {
532
+ return keyOf(x) === key;
533
+ });
534
+ if (g) g.scrollIntoView({ block: 'nearest', inline: 'nearest' });
535
+ }
536
+
537
+ function refresh() {
538
+ renderList();
539
+ drawGraph();
540
+ }
541
+
542
+ // --- the diagram ----------------------------------------------------------
543
+
544
+ // Past this Mermaid produces something no one can read and takes a long time
545
+ // doing it. The filters and the hop focus are the way through, so the pane
546
+ // says so rather than hanging.
547
+ const MAX_GRAPH_NODES = 300;
548
+
549
+ const SHAPES = { file: ['[/', '/]'], plan: ['{{', '}}'], operation: ['([', '])'] };
550
+ const PAINTED = new Set(['task', 'plan', 'fact', 'snippet', 'file', 'operation', 'preference']);
551
+
552
+ // Labels are ids, closed-vocabulary kinds, and a clip of text stripped to a
553
+ // conservative character set: nothing a model wrote reaches the Mermaid parser
554
+ // with its punctuation intact, let alone the SVG. The ellipsis and the middle
555
+ // dot are in the set because the label is built out of them — stripping them
556
+ // turned '01M2…ERNE' into '01M2 ERNE', which reads as two words, not one id.
557
+ function safe(text) {
558
+ return String(text).replace(/[^\w .,:!?'·…\-\/>+=()%]/g, ' ').replace(/\s+/g, ' ').trim();
559
+ }
560
+
561
+ /**
562
+ * One node's label: what it is on the first line, what it says underneath.
563
+ * Run together on a single line there was no telling the id from the kind from
564
+ * the text, which is the whole complaint about reading these diagrams.
565
+ */
566
+ function label(id, n) {
567
+ const head = safe(n.kind + ' · ' + short(id)) + (n.file ? ' · ' + safe(n.file.format) : '');
568
+ return [head].concat(wrap(n.text, 28, 3).map(safe)).join('<br/>');
569
+ }
570
+
571
+ /** The nodes to draw: what the filters left, optionally narrowed to a neighbourhood. */
572
+ function scope() {
573
+ const shown = new Set(visible().map(function (n) { return n.id; }));
574
+ const hops = focusSel.value;
575
+ if (hops === 'all' || !sel) return shown;
576
+ const keep = new Set([sel]);
577
+ let edge = [sel];
578
+ for (let i = 0; i < Number(hops); i++) {
579
+ const next = [];
580
+ edge.forEach(function (id) {
581
+ adj(id).forEach(function (l) {
582
+ if (!keep.has(l.other)) { keep.add(l.other); next.push(l.other); }
583
+ });
584
+ });
585
+ edge = next;
586
+ }
587
+ return keep;
588
+ }
589
+
590
+ function diagram(ids) {
591
+ const lines = ['graph LR'];
592
+ lines.push(' classDef task fill:#1e3357,stroke:#3d5f9e,color:#cfe0ff;');
593
+ lines.push(' classDef plan fill:#3a2450,stroke:#6b4b90,color:#e6d3ff;');
594
+ lines.push(' classDef fact fill:#14392c,stroke:#2f7a5c,color:#c6f5e0;');
595
+ lines.push(' classDef snippet fill:#1e3d33,stroke:#3d7a66,color:#c8f0e2;');
596
+ lines.push(' classDef file fill:#40331a,stroke:#7a6535,color:#f0dfb4;');
597
+ lines.push(' classDef operation fill:#3d2a1b,stroke:#7a5535,color:#f2d9c2;');
598
+ lines.push(' classDef preference fill:#3d1f2c,stroke:#7a3b56,color:#f5b8cd;');
599
+ lines.push(' classDef stale opacity:0.45,stroke-dasharray:4 3;');
600
+ ids.forEach(function (id) {
601
+ const n = BY_ID.get(id);
602
+ if (!n) return;
603
+ const k = KEY.get(id);
604
+ const shape = SHAPES[n.kind] || ['(', ')'];
605
+ lines.push(' ' + k + shape[0] + '"' + label(id, n) + '"' + shape[1]);
606
+ // Kinds are configurable, so a project can have one this page has no
607
+ // colour for; naming a class that was never defined fails the render.
608
+ if (PAINTED.has(n.kind)) lines.push(' class ' + k + ' ' + n.kind + ';');
609
+ if (n.stale) lines.push(' class ' + k + ' stale;');
610
+ });
611
+ EDGES.forEach(function (e) {
612
+ if (!ids.has(e.source) || !ids.has(e.target)) return;
613
+ lines.push(' ' + KEY.get(e.source) + ' -->|' + e.relation + '| ' + KEY.get(e.target));
614
+ });
615
+ return lines.join('\n');
616
+ }
617
+
618
+ function keyOf(g) {
619
+ // Mermaid ids are '<renderId>-<ourKey>-<seq>'; only the key matters.
620
+ const m = /-(n\d+)-\d+$/.exec(g.id || '');
621
+ return m ? m[1] : null;
622
+ }
623
+
624
+ let mermaidLib = null;
625
+ function mermaid() {
626
+ if (!mermaidLib) {
627
+ mermaidLib = import(MERMAID_URL).then(function (m) {
628
+ m.default.initialize({
629
+ startOnLoad: false, theme: 'dark', securityLevel: 'strict',
630
+ maxEdges: 20000, maxTextSize: 5000000,
631
+ flowchart: { htmlLabels: false, nodeSpacing: 45, rankSpacing: 80, useMaxWidth: false }
632
+ });
633
+ return m.default;
634
+ });
635
+ }
636
+ return mermaidLib;
637
+ }
638
+
639
+ const canvas = document.querySelector('.canvas');
640
+ const gcount = document.getElementById('gcount');
641
+ let drawSeq = 0;
642
+
643
+ async function drawGraph() {
644
+ const ids = scope();
645
+ gcount.textContent = ids.size + ' of ' + NODES.length + ' shown';
646
+ if (!ids.size) {
647
+ canvas.replaceChildren(el('div', 'hint pad', 'nothing to draw'));
648
+ return;
649
+ }
650
+ if (ids.size > MAX_GRAPH_NODES) {
651
+ canvas.replaceChildren(el('div', 'hint pad',
652
+ ids.size + ' nodes is more than this pane can draw legibly. Narrow it with the'
653
+ + ' filters on the left, or pick a node and switch to a hop view above.'));
654
+ return;
655
+ }
656
+ const seq = ++drawSeq;
657
+ let lib;
658
+ try {
659
+ lib = await mermaid();
660
+ } catch (err) {
661
+ canvas.replaceChildren(el('div', 'hint pad',
662
+ 'Mermaid could not be loaded (offline?). The list and the detail pane still work.'));
663
+ return;
664
+ }
665
+ let rendered;
666
+ try {
667
+ rendered = await lib.render('memory-graph-' + seq, diagram(ids));
668
+ } catch (err) {
669
+ canvas.replaceChildren(el('pre', null, 'diagram failed: ' + err.message));
670
+ return;
671
+ }
672
+ if (seq !== drawSeq) return;
673
+ // Mermaid produced this markup from labels sanitized above; no node text and
674
+ // no file content reaches it.
675
+ canvas.innerHTML = rendered.svg;
676
+ const svg = canvas.querySelector('svg');
677
+ if (svg) {
678
+ const box = svg.viewBox && svg.viewBox.baseVal;
679
+ const w = (box && box.width) || svg.getBoundingClientRect().width;
680
+ const h = (box && box.height) || svg.getBoundingClientRect().height;
681
+ svg.removeAttribute('width');
682
+ svg.removeAttribute('height');
683
+ svg.style.maxWidth = 'none';
684
+ svg.style.width = w + 'px';
685
+ svg.style.height = h + 'px';
686
+ natural = { w: w, h: h };
687
+ }
688
+ canvas.querySelectorAll('.node').forEach(function (g) {
689
+ const key = keyOf(g);
690
+ const id = key && ID.get(key);
691
+ if (id) g.dataset.node = id;
692
+ });
693
+ markSelected();
694
+ // A redraw changes how much there is to see, so a view nobody has touched
695
+ // should follow it.
696
+ if (auto) fit();
697
+ }
698
+
699
+ // --- pan and zoom ---------------------------------------------------------
700
+
701
+ const viewport = document.querySelector('.viewport');
702
+ const level = document.querySelector('.lvl');
703
+ let scale = 1, tx = 0, ty = 0, natural = { w: 0, h: 0 }, moved = false;
704
+ // While true the view is the page's to choose; the first manual zoom or pan
705
+ // hands it to the operator and we stop moving it under them.
706
+ let auto = true;
707
+
708
+ function applyView() {
709
+ canvas.style.transform = 'translate(' + tx + 'px,' + ty + 'px) scale(' + scale + ')';
710
+ level.textContent = Math.round(scale * 100) + '%';
711
+ }
712
+
713
+ function zoomAt(factor, cx, cy) {
714
+ auto = false;
715
+ const next = Math.min(4, Math.max(0.05, scale * factor));
716
+ const k = next / scale;
717
+ tx = cx - k * (cx - tx);
718
+ ty = cy - k * (cy - ty);
719
+ scale = next;
720
+ applyView();
721
+ }
722
+
723
+ function fit() {
724
+ if (!natural.w) return;
725
+ const r = viewport.getBoundingClientRect();
726
+ // A pane can be narrower than the padding it wants; clamped, because a scale
727
+ // at or below zero renders the diagram as nothing at all.
728
+ scale = Math.max(0.05, Math.min((r.width - 24) / natural.w, (r.height - 24) / natural.h, 1));
729
+ tx = Math.max(0, (r.width - natural.w * scale) / 2);
730
+ ty = 12;
731
+ auto = true;
732
+ applyView();
733
+ }
734
+
735
+ // The pane is a grid column, so it resizes with the window and with nothing
736
+ // else; a scale measured against its old width would be wrong from then on.
737
+ if (window.ResizeObserver) {
738
+ new ResizeObserver(function () { if (auto) fit(); }).observe(viewport);
739
+ }
740
+
741
+ document.querySelector('.gtools').addEventListener('click', function (ev) {
742
+ const what = ev.target.dataset && ev.target.dataset.zoom;
743
+ if (!what) return;
744
+ const r = viewport.getBoundingClientRect();
745
+ if (what === 'in') zoomAt(1.25, r.width / 2, r.height / 2);
746
+ else if (what === 'out') zoomAt(0.8, r.width / 2, r.height / 2);
747
+ else if (what === 'fit') fit();
748
+ else { auto = false; scale = 1; tx = 0; ty = 0; applyView(); }
749
+ });
750
+
751
+ viewport.addEventListener('wheel', function (ev) {
752
+ ev.preventDefault();
753
+ const r = viewport.getBoundingClientRect();
754
+ zoomAt(ev.deltaY < 0 ? 1.1 : 0.9, ev.clientX - r.left, ev.clientY - r.top);
755
+ }, { passive: false });
756
+
757
+ viewport.addEventListener('dblclick', fit);
758
+
759
+ let dragging = false, sx = 0, sy = 0, downOn = null;
760
+ viewport.addEventListener('pointerdown', function (ev) {
761
+ // The node under the press is recorded here because capturing the pointer
762
+ // retargets the click that follows onto the viewport: a listener on the node
763
+ // itself never hears it, which is why picking one did nothing.
764
+ downOn = ev.target && ev.target.closest ? ev.target.closest('.node') : null;
765
+ dragging = true; moved = false; sx = ev.clientX - tx; sy = ev.clientY - ty;
766
+ viewport.classList.add('drag');
767
+ viewport.setPointerCapture(ev.pointerId);
768
+ });
769
+ viewport.addEventListener('pointermove', function (ev) {
770
+ if (!dragging) return;
771
+ const nx = ev.clientX - sx, ny = ev.clientY - sy;
772
+ if (Math.abs(nx - tx) + Math.abs(ny - ty) > 3) { moved = true; auto = false; }
773
+ tx = nx; ty = ny;
774
+ applyView();
775
+ });
776
+ function endDrag(ev) {
777
+ if (!dragging) return;
778
+ dragging = false;
779
+ viewport.classList.remove('drag');
780
+ if (viewport.hasPointerCapture(ev.pointerId)) viewport.releasePointerCapture(ev.pointerId);
781
+ // A press that did not turn into a drag is a pick.
782
+ const hit = downOn;
783
+ downOn = null;
784
+ if (!moved && hit && hit.dataset.node) select(hit.dataset.node, true);
785
+ // Let the click that ends a drag pass without selecting.
786
+ setTimeout(function () { moved = false; }, 0);
787
+ }
788
+ viewport.addEventListener('pointerup', endDrag);
789
+ viewport.addEventListener('pointercancel', endDrag);
790
+
791
+ // --- go -------------------------------------------------------------------
792
+
793
+ renderList();
794
+ renderDetail();
795
+ drawGraph().then(fit);
796
+ `;
797
+ //# sourceMappingURL=page.js.map