@jhizzard/termdeck 0.10.2 → 0.10.4
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/package.json +4 -1
- package/packages/client/public/app.js +28 -4
- package/packages/client/public/graph.html +1 -0
- package/packages/client/public/graph.js +119 -23
- package/packages/client/public/style.css +69 -1
- package/packages/server/src/graph-routes.js +100 -0
- package/packages/server/src/setup/mnestra-migrations/012_project_tag_re_taxonomy.sql +397 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
|
|
5
5
|
"bin": {
|
|
6
6
|
"termdeck": "./packages/cli/src/index.js"
|
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
"ws": "^8.16.0",
|
|
43
43
|
"yaml": "^2.3.4"
|
|
44
44
|
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@anthropic-ai/sdk": "^0.39.0"
|
|
47
|
+
},
|
|
45
48
|
"keywords": [
|
|
46
49
|
"terminal",
|
|
47
50
|
"multiplexer",
|
|
@@ -1228,6 +1228,17 @@
|
|
|
1228
1228
|
state.focusedId = id;
|
|
1229
1229
|
}
|
|
1230
1230
|
|
|
1231
|
+
// Transfer xterm keyboard focus to the focused panel — without this,
|
|
1232
|
+
// the CSS class is the only thing that changed and keystrokes still
|
|
1233
|
+
// go to whichever element had DOM focus before (often the launcher
|
|
1234
|
+
// input, which submits a NEW terminal on Enter, or the previously
|
|
1235
|
+
// focused panel — leading to "easy to put wrong response into a
|
|
1236
|
+
// chat" reports). Mirrors the focus transfer in focusSessionById.
|
|
1237
|
+
const entry = state.sessions.get(id);
|
|
1238
|
+
if (entry && entry.terminal) {
|
|
1239
|
+
try { entry.terminal.focus(); } catch (err) { /* ignore */ }
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1231
1242
|
// Re-fit all visible terminals
|
|
1232
1243
|
requestAnimationFrame(() => fitAll());
|
|
1233
1244
|
}
|
|
@@ -3759,11 +3770,24 @@
|
|
|
3759
3770
|
|
|
3760
3771
|
// Keyboard shortcuts
|
|
3761
3772
|
document.addEventListener('keydown', (e) => {
|
|
3762
|
-
// Tour has priority: Esc exits, ArrowRight/Enter advances, ArrowLeft back
|
|
3773
|
+
// Tour has priority: Esc exits, ArrowRight/Enter advances, ArrowLeft back.
|
|
3774
|
+
// BUT: never swallow Enter/Arrow keys when the user is typing into a
|
|
3775
|
+
// terminal panel or any input/textarea — otherwise terminal Enter
|
|
3776
|
+
// (Claude Code / shell submit) gets eaten by the tour and the user
|
|
3777
|
+
// ends up advancing tour steps when they meant to send a message.
|
|
3778
|
+
// Brad's 2026-04-28 panel-UX report: "Hitting enter from full screen
|
|
3779
|
+
// goes to matrix again" matched this pathway when the v0.10.0 tour
|
|
3780
|
+
// re-fired post-upgrade.
|
|
3763
3781
|
if (tourState.active) {
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3782
|
+
const tgt = e.target;
|
|
3783
|
+
const tag = tgt?.tagName || '';
|
|
3784
|
+
const inEditable = tag === 'INPUT' || tag === 'TEXTAREA' || tgt?.isContentEditable;
|
|
3785
|
+
const inTerminal = tgt?.closest && tgt.closest('.term-panel');
|
|
3786
|
+
if (!inEditable && !inTerminal) {
|
|
3787
|
+
if (e.key === 'Escape') { e.preventDefault(); endTour(); return; }
|
|
3788
|
+
if (e.key === 'ArrowRight' || e.key === 'Enter') { e.preventDefault(); nextTourStep(); return; }
|
|
3789
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); prevTourStep(); return; }
|
|
3790
|
+
}
|
|
3767
3791
|
}
|
|
3768
3792
|
// Ctrl+Shift+N → new terminal
|
|
3769
3793
|
if (e.ctrlKey && e.shiftKey && e.key === 'N') {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
<div class="graph-empty" id="graphEmpty" hidden>
|
|
52
52
|
<h3 id="graphEmptyTitle">No memories yet</h3>
|
|
53
53
|
<p id="graphEmptyBody">This project has no <code>memory_items</code> rows. Run a Claude Code session in this project; the session-end hook will populate Mnestra and edges will be inferred on the next nightly cron.</p>
|
|
54
|
+
<button type="button" class="graph-empty-action" id="graphEmptyAllProjects" hidden>View All Projects</button>
|
|
54
55
|
</div>
|
|
55
56
|
|
|
56
57
|
<svg class="graph-svg" id="graphSvg" role="img" aria-label="Force-directed knowledge graph">
|
|
@@ -186,6 +186,51 @@
|
|
|
186
186
|
window.history.replaceState(null, '', url);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// -------- Stateful UI reset (Sprint 41 T3) ------------------------------
|
|
190
|
+
|
|
191
|
+
// Clear the SVG render and tear down the running simulation. Called at the
|
|
192
|
+
// start of every fetchGraph so a re-fetch from a different mode/project
|
|
193
|
+
// can't paint over a stale render.
|
|
194
|
+
function clearGraphSvg() {
|
|
195
|
+
if (state.sim) {
|
|
196
|
+
state.sim.stop();
|
|
197
|
+
state.sim = null;
|
|
198
|
+
}
|
|
199
|
+
const r = root();
|
|
200
|
+
if (r) {
|
|
201
|
+
while (r.firstChild) r.removeChild(r.firstChild);
|
|
202
|
+
}
|
|
203
|
+
state.nodeSel = null;
|
|
204
|
+
state.edgeSel = null;
|
|
205
|
+
state.labelSel = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Ephemeral toast in the top-right of the graph stage. Used to surface
|
|
209
|
+
// truncation warnings on the All Projects view. Auto-dismisses; calling
|
|
210
|
+
// again before dismissal replaces the message and resets the timer.
|
|
211
|
+
function showToast(msg, durationMs = 6000) {
|
|
212
|
+
const stageEl = stage();
|
|
213
|
+
if (!stageEl) return;
|
|
214
|
+
let el = document.getElementById('graphToast');
|
|
215
|
+
if (!el) {
|
|
216
|
+
el = document.createElement('div');
|
|
217
|
+
el.id = 'graphToast';
|
|
218
|
+
el.className = 'graph-toast';
|
|
219
|
+
stageEl.appendChild(el);
|
|
220
|
+
}
|
|
221
|
+
el.textContent = msg;
|
|
222
|
+
el.hidden = false;
|
|
223
|
+
// Force a reflow so the .show transition fires when toggling rapid-fire.
|
|
224
|
+
void el.offsetWidth;
|
|
225
|
+
el.classList.add('show');
|
|
226
|
+
if (showToast._timer) clearTimeout(showToast._timer);
|
|
227
|
+
showToast._timer = setTimeout(() => {
|
|
228
|
+
el.classList.remove('show');
|
|
229
|
+
// Hide after the transition completes so it can't catch clicks.
|
|
230
|
+
setTimeout(() => { el.hidden = true; }, 220);
|
|
231
|
+
}, durationMs);
|
|
232
|
+
}
|
|
233
|
+
|
|
189
234
|
// -------- API ------------------------------------------------------------
|
|
190
235
|
|
|
191
236
|
async function api(path) {
|
|
@@ -208,14 +253,32 @@
|
|
|
208
253
|
}
|
|
209
254
|
|
|
210
255
|
async function fetchGraph() {
|
|
256
|
+
// Sprint 41 T3 — reset all stateful UI before the new fetch starts so a
|
|
257
|
+
// re-fetch from a different mode/project starts from a clean slate. Fixes
|
|
258
|
+
// the three-way race where "Loading graph…" + "No memories yet" + a stale
|
|
259
|
+
// node render all paint over each other after a mode/project switch.
|
|
260
|
+
hideEmpty();
|
|
261
|
+
clearGraphSvg();
|
|
262
|
+
state.nodes = [];
|
|
263
|
+
state.edges = [];
|
|
211
264
|
setLoading('Loading graph…');
|
|
212
265
|
try {
|
|
213
266
|
let data;
|
|
214
|
-
if (state.mode === '
|
|
267
|
+
if (state.mode === 'project' && state.project === '__all__') {
|
|
268
|
+
data = await api('/api/graph/all');
|
|
269
|
+
if (data.enabled === false) return showDisabled(data);
|
|
270
|
+
state.nodes = data.nodes || [];
|
|
271
|
+
state.edges = data.edges || [];
|
|
272
|
+
if (data.truncated) {
|
|
273
|
+
showToast(
|
|
274
|
+
`Showing ${state.nodes.length} most-recent of ${data.totalAvailable} memories — narrow by project to see specific clusters.`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
} else if (state.mode === 'memory') {
|
|
215
278
|
data = await api(`/api/graph/memory/${encodeURIComponent(state.memoryId)}?depth=${state.depth}`);
|
|
216
279
|
if (data.enabled === false) return showDisabled(data);
|
|
217
|
-
state.nodes = data.nodes;
|
|
218
|
-
state.edges = data.edges;
|
|
280
|
+
state.nodes = data.nodes || [];
|
|
281
|
+
state.edges = data.edges || [];
|
|
219
282
|
// Use the root memory's project as the view's "current project" for
|
|
220
283
|
// the legend / drawer / fallback color when nodes span projects.
|
|
221
284
|
if (data.root && data.root.project) state.project = data.root.project;
|
|
@@ -224,8 +287,8 @@
|
|
|
224
287
|
state.project = name;
|
|
225
288
|
data = await api(`/api/graph/project/${encodeURIComponent(name)}`);
|
|
226
289
|
if (data.enabled === false) return showDisabled(data);
|
|
227
|
-
state.nodes = data.nodes;
|
|
228
|
-
state.edges = data.edges;
|
|
290
|
+
state.nodes = data.nodes || [];
|
|
291
|
+
state.edges = data.edges || [];
|
|
229
292
|
}
|
|
230
293
|
writeUrlState();
|
|
231
294
|
hideLoading();
|
|
@@ -238,6 +301,9 @@
|
|
|
238
301
|
renderGraph();
|
|
239
302
|
updateStats();
|
|
240
303
|
} catch (err) {
|
|
304
|
+
// Even on error, drop the empty-state overlay so the failure message
|
|
305
|
+
// shows alone instead of stacking on top of "No memories yet".
|
|
306
|
+
hideEmpty();
|
|
241
307
|
setLoading(`Failed: ${err.message}`);
|
|
242
308
|
}
|
|
243
309
|
}
|
|
@@ -256,14 +322,30 @@
|
|
|
256
322
|
}
|
|
257
323
|
function showEmpty() {
|
|
258
324
|
$('graphEmpty').hidden = false;
|
|
259
|
-
|
|
260
|
-
|
|
325
|
+
const allBtn = $('graphEmptyAllProjects');
|
|
326
|
+
if (state.mode === 'project' && state.project === '__all__') {
|
|
327
|
+
$('graphEmptyTitle').textContent = 'No memories yet';
|
|
328
|
+
$('graphEmptyBody').innerHTML =
|
|
329
|
+
'No <code>memory_items</code> rows in the database. Run a Claude Code session and the session-end hook will populate Mnestra; edges will be inferred on the next nightly cron.';
|
|
330
|
+
if (allBtn) allBtn.hidden = true;
|
|
331
|
+
} else if (state.mode === 'project') {
|
|
332
|
+
$('graphEmptyTitle').textContent = `No memories tagged "${state.project}"`;
|
|
333
|
+
$('graphEmptyBody').innerHTML =
|
|
334
|
+
'Your <code>memory_items</code> may be mis-tagged under a parent directory. ' +
|
|
335
|
+
'Try the All Projects view, or check the actual distribution with ' +
|
|
336
|
+
'<code>SELECT project, count(*) FROM memory_items GROUP BY project</code>.';
|
|
337
|
+
if (allBtn) allBtn.hidden = false;
|
|
261
338
|
} else {
|
|
262
339
|
$('graphEmptyTitle').textContent = 'No neighbors yet';
|
|
340
|
+
$('graphEmptyBody').textContent =
|
|
341
|
+
'This memory has no edges in memory_relationships. The next graph-inference cron run will infer them if any are warranted.';
|
|
342
|
+
if (allBtn) allBtn.hidden = true;
|
|
263
343
|
}
|
|
264
344
|
}
|
|
265
345
|
function hideEmpty() {
|
|
266
346
|
$('graphEmpty').hidden = true;
|
|
347
|
+
const allBtn = $('graphEmptyAllProjects');
|
|
348
|
+
if (allBtn) allBtn.hidden = true;
|
|
267
349
|
}
|
|
268
350
|
function showDisabled(data) {
|
|
269
351
|
hideLoading();
|
|
@@ -610,28 +692,31 @@
|
|
|
610
692
|
readUrlState();
|
|
611
693
|
await loadConfig();
|
|
612
694
|
|
|
613
|
-
// Project picker
|
|
695
|
+
// Project picker. The "All projects" option is always present so the user
|
|
696
|
+
// can recover from mis-tagged data (Sprint 41 T3); per-project options are
|
|
697
|
+
// appended after it from /api/config.
|
|
614
698
|
const sel = $('graphProject');
|
|
615
699
|
sel.innerHTML = '';
|
|
616
|
-
|
|
700
|
+
sel.disabled = false;
|
|
701
|
+
const allOpt = document.createElement('option');
|
|
702
|
+
allOpt.value = '__all__';
|
|
703
|
+
allOpt.textContent = 'All projects';
|
|
704
|
+
sel.appendChild(allOpt);
|
|
705
|
+
for (const p of state.projects) {
|
|
617
706
|
const opt = document.createElement('option');
|
|
618
|
-
opt.value =
|
|
619
|
-
opt.textContent =
|
|
707
|
+
opt.value = p;
|
|
708
|
+
opt.textContent = p;
|
|
620
709
|
sel.appendChild(opt);
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
// Pick state.project, or first project from config.
|
|
630
|
-
if (!state.project && state.mode !== 'memory') {
|
|
631
|
-
state.project = state.projects[0];
|
|
710
|
+
}
|
|
711
|
+
// Pick state.project, or fall back to the first configured project, or to
|
|
712
|
+
// __all__ when nothing is configured. Memory mode inherits its project
|
|
713
|
+
// from the root node and skips this resolution.
|
|
714
|
+
if (state.mode !== 'memory') {
|
|
715
|
+
if (!state.project) {
|
|
716
|
+
state.project = state.projects.length > 0 ? state.projects[0] : '__all__';
|
|
632
717
|
}
|
|
633
|
-
if (state.project) sel.value = state.project;
|
|
634
718
|
}
|
|
719
|
+
if (state.project) sel.value = state.project;
|
|
635
720
|
|
|
636
721
|
sel.addEventListener('change', () => {
|
|
637
722
|
state.mode = 'project';
|
|
@@ -640,6 +725,17 @@
|
|
|
640
725
|
fetchGraph();
|
|
641
726
|
});
|
|
642
727
|
|
|
728
|
+
const emptyAllBtn = $('graphEmptyAllProjects');
|
|
729
|
+
if (emptyAllBtn) {
|
|
730
|
+
emptyAllBtn.addEventListener('click', () => {
|
|
731
|
+
state.mode = 'project';
|
|
732
|
+
state.project = '__all__';
|
|
733
|
+
state.memoryId = null;
|
|
734
|
+
sel.value = '__all__';
|
|
735
|
+
fetchGraph();
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
643
739
|
$('graphSearch').addEventListener('input', (e) => applySearch(e.target.value));
|
|
644
740
|
$('graphReheat').addEventListener('click', () => {
|
|
645
741
|
if (state.sim) state.sim.alpha(0.6).restart();
|
|
@@ -297,7 +297,13 @@
|
|
|
297
297
|
/* ===== MAIN GRID ===== */
|
|
298
298
|
.grid-container {
|
|
299
299
|
flex: 1;
|
|
300
|
-
padding
|
|
300
|
+
/* Right padding of 38px reserves the 32px guide-rail collapsed strip
|
|
301
|
+
(position:fixed; right:0) plus 6px breathing. Without this the
|
|
302
|
+
rightmost panel's close (×) button sits underneath the guide
|
|
303
|
+
toggle and becomes unclickable. The expanded-guide state keeps
|
|
304
|
+
its own backdrop/shadow over panels — that's pre-existing
|
|
305
|
+
behavior and not affected by this padding. */
|
|
306
|
+
padding: 6px 38px 6px 6px;
|
|
301
307
|
overflow: hidden;
|
|
302
308
|
display: grid;
|
|
303
309
|
gap: 6px;
|
|
@@ -3439,6 +3445,15 @@
|
|
|
3439
3445
|
padding: 24px;
|
|
3440
3446
|
text-align: center;
|
|
3441
3447
|
}
|
|
3448
|
+
/* Sprint 41 T3 fixed the JS race but missed the CSS specificity gotcha:
|
|
3449
|
+
`.graph-loading { display: flex }` above wins over the UA default
|
|
3450
|
+
`[hidden] { display: none }` (class > attribute). Result: setting
|
|
3451
|
+
`el.hidden = true` from JS did nothing visually, and "Loading graph…"
|
|
3452
|
+
kept rendering behind "No memories yet". Explicit override below. */
|
|
3453
|
+
.graph-loading[hidden],
|
|
3454
|
+
.graph-empty[hidden] {
|
|
3455
|
+
display: none;
|
|
3456
|
+
}
|
|
3442
3457
|
.graph-loading-spinner {
|
|
3443
3458
|
width: 28px;
|
|
3444
3459
|
height: 28px;
|
|
@@ -3468,6 +3483,59 @@
|
|
|
3468
3483
|
border-radius: 3px;
|
|
3469
3484
|
font-size: 11px;
|
|
3470
3485
|
}
|
|
3486
|
+
/* Sprint 41 T3 — interactive button inside the .graph-empty overlay.
|
|
3487
|
+
The parent has pointer-events: none so taps on the SVG behind pass
|
|
3488
|
+
through; the button must opt back in for the click to register. */
|
|
3489
|
+
.graph-empty .graph-empty-action {
|
|
3490
|
+
pointer-events: auto;
|
|
3491
|
+
margin-top: 6px;
|
|
3492
|
+
background: var(--tg-surface);
|
|
3493
|
+
color: var(--tg-text);
|
|
3494
|
+
border: 1px solid var(--tg-border-active);
|
|
3495
|
+
border-radius: var(--tg-radius-sm);
|
|
3496
|
+
padding: 6px 14px;
|
|
3497
|
+
font-family: var(--tg-mono);
|
|
3498
|
+
font-size: 12px;
|
|
3499
|
+
cursor: pointer;
|
|
3500
|
+
transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
|
|
3501
|
+
}
|
|
3502
|
+
.graph-empty .graph-empty-action:hover {
|
|
3503
|
+
background: var(--tg-surface-hover);
|
|
3504
|
+
border-color: var(--tg-accent);
|
|
3505
|
+
color: var(--tg-text-bright);
|
|
3506
|
+
}
|
|
3507
|
+
.graph-empty .graph-empty-action:focus-visible {
|
|
3508
|
+
outline: 2px solid var(--tg-accent);
|
|
3509
|
+
outline-offset: 2px;
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
/* Sprint 41 T3 — ephemeral toast for graph-side notifications
|
|
3513
|
+
(truncation warnings, etc). Sits in the top-right of #graphStage. */
|
|
3514
|
+
.graph-toast {
|
|
3515
|
+
position: absolute;
|
|
3516
|
+
top: 14px;
|
|
3517
|
+
right: 14px;
|
|
3518
|
+
max-width: 360px;
|
|
3519
|
+
background: rgba(15, 17, 23, 0.95);
|
|
3520
|
+
color: var(--tg-text);
|
|
3521
|
+
border: 1px solid var(--tg-border-active);
|
|
3522
|
+
border-radius: var(--tg-radius-sm);
|
|
3523
|
+
padding: 10px 14px;
|
|
3524
|
+
font-family: var(--tg-mono);
|
|
3525
|
+
font-size: 12px;
|
|
3526
|
+
line-height: 1.4;
|
|
3527
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
|
3528
|
+
backdrop-filter: blur(6px);
|
|
3529
|
+
z-index: 60;
|
|
3530
|
+
opacity: 0;
|
|
3531
|
+
transform: translateY(-6px);
|
|
3532
|
+
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
3533
|
+
pointer-events: none;
|
|
3534
|
+
}
|
|
3535
|
+
.graph-toast.show {
|
|
3536
|
+
opacity: 1;
|
|
3537
|
+
transform: translateY(0);
|
|
3538
|
+
}
|
|
3471
3539
|
|
|
3472
3540
|
.graph-tooltip {
|
|
3473
3541
|
position: fixed;
|
|
@@ -26,6 +26,10 @@ const NODE_LABEL_LEN = 200;
|
|
|
26
26
|
const MAX_DEPTH = 4;
|
|
27
27
|
const DEFAULT_DEPTH = 2;
|
|
28
28
|
const MAX_NODES_PER_PROJECT = 2000;
|
|
29
|
+
// Sprint 41 T3 — All Projects view cap. Same ceiling as the per-project cap;
|
|
30
|
+
// the global view trades cluster-fidelity for breadth and warns the client via
|
|
31
|
+
// `truncated`/`totalAvailable` when the ceiling clips the corpus.
|
|
32
|
+
const MAX_NODES_GLOBAL = 2000;
|
|
29
33
|
|
|
30
34
|
function snippet(content, len = NODE_LABEL_LEN) {
|
|
31
35
|
if (!content) return '';
|
|
@@ -158,6 +162,60 @@ async function fetchProjectGraph(pool, projectName) {
|
|
|
158
162
|
return { nodes, edges };
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
async function fetchAllGraph(pool) {
|
|
166
|
+
// Sprint 41 T3 — backs the "All projects" picker option in /graph.html.
|
|
167
|
+
// Returns the most-recent MAX_NODES_GLOBAL active+non-archived memories plus
|
|
168
|
+
// every edge whose endpoints both land in the result set. `totalAvailable`
|
|
169
|
+
// and `truncated` let the client surface a toast when the corpus overflows
|
|
170
|
+
// the cap.
|
|
171
|
+
const totalSql = `
|
|
172
|
+
SELECT COUNT(*)::int AS c
|
|
173
|
+
FROM memory_items
|
|
174
|
+
WHERE is_active = TRUE AND archived = FALSE
|
|
175
|
+
`;
|
|
176
|
+
const totalRes = await pool.query(totalSql);
|
|
177
|
+
const totalAvailable = Number(totalRes.rows[0]?.c || 0);
|
|
178
|
+
|
|
179
|
+
const nodesSql = `
|
|
180
|
+
WITH all_nodes AS (
|
|
181
|
+
SELECT ${NODE_COLUMNS_SQL}
|
|
182
|
+
FROM memory_items
|
|
183
|
+
WHERE is_active = TRUE AND archived = FALSE
|
|
184
|
+
ORDER BY created_at DESC
|
|
185
|
+
LIMIT ${MAX_NODES_GLOBAL}
|
|
186
|
+
)
|
|
187
|
+
SELECT
|
|
188
|
+
n.*,
|
|
189
|
+
COALESCE((
|
|
190
|
+
SELECT COUNT(*)::int
|
|
191
|
+
FROM memory_relationships r
|
|
192
|
+
WHERE r.source_id = n.id OR r.target_id = n.id
|
|
193
|
+
), 0) AS degree
|
|
194
|
+
FROM all_nodes n
|
|
195
|
+
`;
|
|
196
|
+
const nodesRes = await pool.query(nodesSql);
|
|
197
|
+
const nodes = nodesRes.rows.map(rowToNode);
|
|
198
|
+
if (nodes.length === 0) {
|
|
199
|
+
return { nodes: [], edges: [], totalAvailable, truncated: false };
|
|
200
|
+
}
|
|
201
|
+
const ids = nodes.map((n) => n.id);
|
|
202
|
+
|
|
203
|
+
const edgesSql = `
|
|
204
|
+
SELECT ${EDGE_COLUMNS_BASE_SQL}, ${EDGE_COLUMNS_T2_SQL}
|
|
205
|
+
FROM memory_relationships
|
|
206
|
+
WHERE source_id = ANY($1::uuid[])
|
|
207
|
+
AND target_id = ANY($1::uuid[])
|
|
208
|
+
`;
|
|
209
|
+
const edgesRes = await pool.query(edgesSql, [ids]);
|
|
210
|
+
const edges = edgesRes.rows.map(rowToEdge);
|
|
211
|
+
return {
|
|
212
|
+
nodes,
|
|
213
|
+
edges,
|
|
214
|
+
totalAvailable,
|
|
215
|
+
truncated: totalAvailable > MAX_NODES_GLOBAL,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
161
219
|
async function fetchNeighborhood(pool, rootId, depth) {
|
|
162
220
|
// Inline recursive CTE so T4 ships independently of T1's
|
|
163
221
|
// expand_memory_neighborhood RPC. When that RPC lands, the CTE here can be
|
|
@@ -467,6 +525,46 @@ function createGraphRoutes({ app, getPool }) {
|
|
|
467
525
|
}
|
|
468
526
|
});
|
|
469
527
|
|
|
528
|
+
// Sprint 41 T3 — All Projects view. Sibling of /api/graph/project/:name with
|
|
529
|
+
// no project filter; cap is MAX_NODES_GLOBAL and the response carries
|
|
530
|
+
// `truncated`/`totalAvailable` so the client can surface a "showing N of M"
|
|
531
|
+
// toast when the corpus overflows.
|
|
532
|
+
app.get('/api/graph/all', async (req, res) => {
|
|
533
|
+
const pool = getPool();
|
|
534
|
+
if (!pool) {
|
|
535
|
+
return res.json(disabledPayload({
|
|
536
|
+
nodes: [],
|
|
537
|
+
edges: [],
|
|
538
|
+
totalAvailable: 0,
|
|
539
|
+
truncated: false,
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
const { nodes, edges, totalAvailable, truncated } = await fetchAllGraph(pool);
|
|
544
|
+
const byType = {};
|
|
545
|
+
for (const e of edges) {
|
|
546
|
+
byType[e.kind] = (byType[e.kind] || 0) + 1;
|
|
547
|
+
}
|
|
548
|
+
res.json({
|
|
549
|
+
enabled: true,
|
|
550
|
+
stats: {
|
|
551
|
+
nodes: nodes.length,
|
|
552
|
+
edges: edges.length,
|
|
553
|
+
byType,
|
|
554
|
+
truncated,
|
|
555
|
+
totalAvailable,
|
|
556
|
+
},
|
|
557
|
+
nodes,
|
|
558
|
+
edges,
|
|
559
|
+
totalAvailable,
|
|
560
|
+
truncated,
|
|
561
|
+
});
|
|
562
|
+
} catch (err) {
|
|
563
|
+
console.warn('[graph] /api/graph/all failed:', err.message);
|
|
564
|
+
res.status(500).json({ error: 'graph all query failed', detail: err.message });
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
470
568
|
app.get('/api/graph/memory/:id', async (req, res) => {
|
|
471
569
|
const id = req.params.id;
|
|
472
570
|
if (!UUID_RE.test(id)) {
|
|
@@ -540,6 +638,7 @@ module.exports = {
|
|
|
540
638
|
createGraphRoutes,
|
|
541
639
|
// Exported for tests + reuse:
|
|
542
640
|
fetchProjectGraph,
|
|
641
|
+
fetchAllGraph,
|
|
543
642
|
fetchNeighborhood,
|
|
544
643
|
fetchStats,
|
|
545
644
|
fetchInferenceStats,
|
|
@@ -550,6 +649,7 @@ module.exports = {
|
|
|
550
649
|
UUID_RE,
|
|
551
650
|
PROJECT_RE,
|
|
552
651
|
MAX_NODES_PER_PROJECT,
|
|
652
|
+
MAX_NODES_GLOBAL,
|
|
553
653
|
MAX_DEPTH,
|
|
554
654
|
DEFAULT_DEPTH,
|
|
555
655
|
};
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
-- Sprint 41 T2 — chopin-nashville re-taxonomy.
|
|
2
|
+
--
|
|
3
|
+
-- Why this exists:
|
|
4
|
+
-- Sprint 39 T3 migration 011_project_tag_backfill.sql moved 192 rows out of
|
|
5
|
+
-- the chopin-nashville bucket (1,139 → 947) using a conservative 5-bucket
|
|
6
|
+
-- keyword pass. 947 rows still remain mis-tagged. Sprint 41 widens the
|
|
7
|
+
-- keyword sets per the new project taxonomy (T1 owns the canonical doc at
|
|
8
|
+
-- docs/PROJECT-TAXONOMY.md) and adds three buckets that 011 did not have:
|
|
9
|
+
-- - chopin-in-bohemia (festival, distinct from the Nashville competition)
|
|
10
|
+
-- - chopin-scheduler (the SchedulingApp / "Maestro" project — single
|
|
11
|
+
-- project under two names per orchestrator
|
|
12
|
+
-- mid-inject clarification 2026-04-28 12:51 ET)
|
|
13
|
+
-- - claimguard (Gorgias-ticket-monitor / ClaimGuard-AI work)
|
|
14
|
+
--
|
|
15
|
+
-- The remaining residue after this migration is what Sprint 41 T4 hands to
|
|
16
|
+
-- the LLM-classification runner. Conservative wins: rows with no clear
|
|
17
|
+
-- keyword signal STAY chopin-nashville for T4 to handle.
|
|
18
|
+
--
|
|
19
|
+
-- What this migration does NOT do:
|
|
20
|
+
-- - Does NOT touch mnestra_session_memory / mnestra_project_memory / etc.
|
|
21
|
+
-- (legacy rag-events tables; different write path; Sprint 42+ cleanup).
|
|
22
|
+
-- - Does NOT consolidate duplicate tags like 'gorgias' vs
|
|
23
|
+
-- 'gorgias-ticket-monitor', 'pvb' vs 'PVB', or 'mnestra' vs 'engram'.
|
|
24
|
+
-- Visible in `SELECT project, count(*) FROM memory_items GROUP BY
|
|
25
|
+
-- project` but a separate cleanup pass.
|
|
26
|
+
-- - Does NOT re-tag rows whose only signal is the legitimate
|
|
27
|
+
-- chopin-nashville vocabulary (competition / performance / jury /
|
|
28
|
+
-- sponsor / applicant / repertoire / Acceptd / NICPC / laureate). Those
|
|
29
|
+
-- are the rows the chopin-nashville tag SHOULD claim.
|
|
30
|
+
-- - Does NOT touch source_session_id → transcript_path → cwd resolution.
|
|
31
|
+
-- The briefing scoped that as a possible additional signal, not
|
|
32
|
+
-- required. Keyword bucketing + T4's LLM pass hits the < 100 target.
|
|
33
|
+
--
|
|
34
|
+
-- Heuristic — content keyword bucketing:
|
|
35
|
+
-- The migration runs UPDATEs sequentially. Earlier buckets claim ambiguous
|
|
36
|
+
-- multi-project rows first; later buckets only see rows that no earlier
|
|
37
|
+
-- bucket has already re-tagged. Order is broadest-first (largest expected
|
|
38
|
+
-- bucket size first):
|
|
39
|
+
--
|
|
40
|
+
-- 1. termdeck — termdeck, mnestra, "4+1 sprint", xterm,
|
|
41
|
+
-- node-pty, flashback, memory_items,
|
|
42
|
+
-- memory_relationships
|
|
43
|
+
-- 2. rumen — rumen, rumen-tick, "insight synthesis"
|
|
44
|
+
-- 3. podium — podium
|
|
45
|
+
-- 4. chopin-in-bohemia — bohemia, "chopin in bohemia", "2026 festival"
|
|
46
|
+
-- 5. chopin-scheduler — scheduling, schedulingapp, \mMaestro\M
|
|
47
|
+
-- (Maestro is the working name; chopin-scheduler
|
|
48
|
+
-- is the canonical tag — alias confirmed 2026-04-28
|
|
49
|
+
-- by Joshua; case-sensitive word-boundary token
|
|
50
|
+
-- avoids matching unrelated "[maestro]" log
|
|
51
|
+
-- prefixes)
|
|
52
|
+
-- 6. pvb — PVB, petvetbid, "pet vet bid"
|
|
53
|
+
-- 7. claimguard — claimguard, gorgias-ticket-monitor,
|
|
54
|
+
-- "gorgias ticket monitor"
|
|
55
|
+
-- 8. dor — \mDOR\M, /DOR/, ~/Documents/DOR, dor.config,
|
|
56
|
+
-- "Rust LLM gateway", openclaw
|
|
57
|
+
-- (reused verbatim from 011's tightened
|
|
58
|
+
-- pattern; word-boundary uppercase rules out
|
|
59
|
+
-- "dormant", "vendored", "indoor", etc.)
|
|
60
|
+
--
|
|
61
|
+
-- Idempotence:
|
|
62
|
+
-- Every UPDATE is gated by `WHERE project = 'chopin-nashville'`. After the
|
|
63
|
+
-- first run, those rows have a different project tag, so re-running this
|
|
64
|
+
-- migration is a no-op (zero rows updated per bucket). RAISE NOTICE on a
|
|
65
|
+
-- re-run will print zeros, which is the expected idempotent signal.
|
|
66
|
+
--
|
|
67
|
+
-- Application:
|
|
68
|
+
-- THIS MIGRATION IS NOT EXECUTED BY THE LANE THAT WROTE IT. Orchestrator
|
|
69
|
+
-- reviews the RAISE NOTICE counts after applying. Apply via the bundled
|
|
70
|
+
-- migration runner at packages/server/src/setup/migration-runner.js (which
|
|
71
|
+
-- uses node-postgres client.query — psql metacommands like \gset are NOT
|
|
72
|
+
-- available, so the count probes use GET DIAGNOSTICS ROW_COUNT inside DO
|
|
73
|
+
-- blocks). Manual fallback:
|
|
74
|
+
-- `psql "$DATABASE_URL" -f 012_project_tag_re_taxonomy.sql`.
|
|
75
|
+
|
|
76
|
+
BEGIN;
|
|
77
|
+
|
|
78
|
+
-- ============================================================
|
|
79
|
+
-- AUDIT BEFORE
|
|
80
|
+
-- ============================================================
|
|
81
|
+
DO $$
|
|
82
|
+
DECLARE
|
|
83
|
+
before_chopin int;
|
|
84
|
+
before_termdeck int;
|
|
85
|
+
before_rumen int;
|
|
86
|
+
before_podium int;
|
|
87
|
+
before_bohemia int;
|
|
88
|
+
before_scheduler int;
|
|
89
|
+
before_pvb int;
|
|
90
|
+
before_claimguard int;
|
|
91
|
+
before_dor int;
|
|
92
|
+
BEGIN
|
|
93
|
+
SELECT count(*) INTO before_chopin FROM memory_items WHERE project = 'chopin-nashville';
|
|
94
|
+
SELECT count(*) INTO before_termdeck FROM memory_items WHERE project = 'termdeck';
|
|
95
|
+
SELECT count(*) INTO before_rumen FROM memory_items WHERE project = 'rumen';
|
|
96
|
+
SELECT count(*) INTO before_podium FROM memory_items WHERE project = 'podium';
|
|
97
|
+
SELECT count(*) INTO before_bohemia FROM memory_items WHERE project = 'chopin-in-bohemia';
|
|
98
|
+
SELECT count(*) INTO before_scheduler FROM memory_items WHERE project = 'chopin-scheduler';
|
|
99
|
+
SELECT count(*) INTO before_pvb FROM memory_items WHERE project = 'pvb';
|
|
100
|
+
SELECT count(*) INTO before_claimguard FROM memory_items WHERE project = 'claimguard';
|
|
101
|
+
SELECT count(*) INTO before_dor FROM memory_items WHERE project = 'dor';
|
|
102
|
+
RAISE NOTICE '[012-retaxonomy] BEFORE chopin-nashville=% termdeck=% rumen=% podium=% chopin-in-bohemia=% chopin-scheduler=% pvb=% claimguard=% dor=%',
|
|
103
|
+
before_chopin, before_termdeck, before_rumen, before_podium, before_bohemia, before_scheduler, before_pvb, before_claimguard, before_dor;
|
|
104
|
+
END $$;
|
|
105
|
+
|
|
106
|
+
-- ============================================================
|
|
107
|
+
-- BUCKET 1 — termdeck (broadest first; claims ambiguous multi-project rows)
|
|
108
|
+
--
|
|
109
|
+
-- Widened from 011's 3-keyword set [termdeck | mnestra | "4+1 sprint"] to
|
|
110
|
+
-- include TermDeck-internal vocabulary that almost never appears outside the
|
|
111
|
+
-- TermDeck stack (xterm, node-pty), the Flashback subsystem name, and the
|
|
112
|
+
-- memory_* table identifiers (which are spoken about in TermDeck/Mnestra
|
|
113
|
+
-- context overwhelmingly — graph-routes, mnestra-bridge, the migrations
|
|
114
|
+
-- themselves).
|
|
115
|
+
-- ============================================================
|
|
116
|
+
DO $$
|
|
117
|
+
DECLARE
|
|
118
|
+
rows_updated int;
|
|
119
|
+
BEGIN
|
|
120
|
+
UPDATE memory_items SET project = 'termdeck'
|
|
121
|
+
WHERE project = 'chopin-nashville'
|
|
122
|
+
AND (
|
|
123
|
+
content ILIKE '%termdeck%'
|
|
124
|
+
OR content ILIKE '%mnestra%'
|
|
125
|
+
OR content ILIKE '%4+1 sprint%'
|
|
126
|
+
OR content ILIKE '%xterm%'
|
|
127
|
+
OR content ILIKE '%node-pty%'
|
|
128
|
+
OR content ILIKE '%flashback%'
|
|
129
|
+
OR content ILIKE '%memory_items%'
|
|
130
|
+
OR content ILIKE '%memory_relationships%'
|
|
131
|
+
);
|
|
132
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
133
|
+
RAISE NOTICE '[012-retaxonomy] bucket 1 (termdeck): % rows re-tagged', rows_updated;
|
|
134
|
+
END $$;
|
|
135
|
+
|
|
136
|
+
-- ============================================================
|
|
137
|
+
-- BUCKET 2 — rumen
|
|
138
|
+
--
|
|
139
|
+
-- 011 used [rumen] alone. 012 widens to include rumen-tick (the Rumen
|
|
140
|
+
-- cron-tick subsystem) and "insight synthesis" (Rumen's product vocabulary).
|
|
141
|
+
-- ============================================================
|
|
142
|
+
DO $$
|
|
143
|
+
DECLARE
|
|
144
|
+
rows_updated int;
|
|
145
|
+
BEGIN
|
|
146
|
+
UPDATE memory_items SET project = 'rumen'
|
|
147
|
+
WHERE project = 'chopin-nashville'
|
|
148
|
+
AND (
|
|
149
|
+
content ILIKE '%rumen%'
|
|
150
|
+
OR content ILIKE '%rumen-tick%'
|
|
151
|
+
OR content ILIKE '%insight synthesis%'
|
|
152
|
+
);
|
|
153
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
154
|
+
RAISE NOTICE '[012-retaxonomy] bucket 2 (rumen): % rows re-tagged', rows_updated;
|
|
155
|
+
END $$;
|
|
156
|
+
|
|
157
|
+
-- ============================================================
|
|
158
|
+
-- BUCKET 3 — podium
|
|
159
|
+
--
|
|
160
|
+
-- Same single-keyword pattern as 011. Podium-specific vocabulary doesn't
|
|
161
|
+
-- have synonyms that justify widening. (The Chopin in Bohemia festival
|
|
162
|
+
-- mentions Podium often, but bucket 1's broadest-first ordering means
|
|
163
|
+
-- podium-AND-bohemia rows where podium is the dominant tag claim it here;
|
|
164
|
+
-- bohemia-only rows fall to bucket 4.)
|
|
165
|
+
-- ============================================================
|
|
166
|
+
DO $$
|
|
167
|
+
DECLARE
|
|
168
|
+
rows_updated int;
|
|
169
|
+
BEGIN
|
|
170
|
+
UPDATE memory_items SET project = 'podium'
|
|
171
|
+
WHERE project = 'chopin-nashville'
|
|
172
|
+
AND content ILIKE '%podium%';
|
|
173
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
174
|
+
RAISE NOTICE '[012-retaxonomy] bucket 3 (podium): % rows re-tagged', rows_updated;
|
|
175
|
+
END $$;
|
|
176
|
+
|
|
177
|
+
-- ============================================================
|
|
178
|
+
-- BUCKET 4 — chopin-in-bohemia (NEW in 012)
|
|
179
|
+
--
|
|
180
|
+
-- The 2026 festival is a distinct project from the Chopin Nashville
|
|
181
|
+
-- competition. Keywords: bohemia (substring; festival-specific), "chopin in
|
|
182
|
+
-- bohemia" (full phrase, near-zero false positives), "2026 festival" (date+
|
|
183
|
+
-- project disambiguator).
|
|
184
|
+
--
|
|
185
|
+
-- Note: rows that mention "Chopin Nashville" AND "Bohemia" together (rare —
|
|
186
|
+
-- maybe cross-project planning notes) will already have been claimed by
|
|
187
|
+
-- earlier buckets if they also mention TermDeck/Rumen/Podium tooling.
|
|
188
|
+
-- Otherwise they land here, which is the right call: the "current festival
|
|
189
|
+
-- being planned" is Bohemia 2026.
|
|
190
|
+
-- ============================================================
|
|
191
|
+
DO $$
|
|
192
|
+
DECLARE
|
|
193
|
+
rows_updated int;
|
|
194
|
+
BEGIN
|
|
195
|
+
UPDATE memory_items SET project = 'chopin-in-bohemia'
|
|
196
|
+
WHERE project = 'chopin-nashville'
|
|
197
|
+
AND (
|
|
198
|
+
content ILIKE '%bohemia%'
|
|
199
|
+
OR content ILIKE '%chopin in bohemia%'
|
|
200
|
+
OR content ILIKE '%2026 festival%'
|
|
201
|
+
);
|
|
202
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
203
|
+
RAISE NOTICE '[012-retaxonomy] bucket 4 (chopin-in-bohemia): % rows re-tagged', rows_updated;
|
|
204
|
+
END $$;
|
|
205
|
+
|
|
206
|
+
-- ============================================================
|
|
207
|
+
-- BUCKET 5 — chopin-scheduler (NEW in 012; absorbs Maestro alias)
|
|
208
|
+
--
|
|
209
|
+
-- Per orchestrator clarification 2026-04-28 12:51 ET: "Maestro" is the
|
|
210
|
+
-- working/branding name for the chopin-scheduler project. Same project,
|
|
211
|
+
-- two names. The on-disk path is SchedulingApp/, so the keywords cover both
|
|
212
|
+
-- the path-style identifier (scheduling, schedulingapp) and the branding
|
|
213
|
+
-- alias (\mMaestro\M — POSIX word-boundary, case-sensitive Capitalized
|
|
214
|
+
-- token).
|
|
215
|
+
--
|
|
216
|
+
-- The case-sensitive Maestro pattern matters: lowercase "maestro" can
|
|
217
|
+
-- appear in unrelated content (log prefixes like "[maestro]" if any tool
|
|
218
|
+
-- ever named itself that, generic music vocabulary). Capitalized Maestro
|
|
219
|
+
-- with word boundaries is much closer to "the project name" intent.
|
|
220
|
+
-- ============================================================
|
|
221
|
+
DO $$
|
|
222
|
+
DECLARE
|
|
223
|
+
rows_updated int;
|
|
224
|
+
BEGIN
|
|
225
|
+
UPDATE memory_items SET project = 'chopin-scheduler'
|
|
226
|
+
WHERE project = 'chopin-nashville'
|
|
227
|
+
AND (
|
|
228
|
+
content ILIKE '%scheduling%'
|
|
229
|
+
OR content ILIKE '%schedulingapp%'
|
|
230
|
+
OR content ~ '\mMaestro\M'
|
|
231
|
+
);
|
|
232
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
233
|
+
RAISE NOTICE '[012-retaxonomy] bucket 5 (chopin-scheduler): % rows re-tagged', rows_updated;
|
|
234
|
+
END $$;
|
|
235
|
+
|
|
236
|
+
-- ============================================================
|
|
237
|
+
-- BUCKET 6 — pvb (case-insensitive PVB / petvetbid markers)
|
|
238
|
+
--
|
|
239
|
+
-- Same pattern as 011 bucket 4. PVB is small in the chopin-nashville bucket
|
|
240
|
+
-- (Sprint 39 dry-run found 7 rows; live apply landed 3 because bucket 1
|
|
241
|
+
-- claimed mnestra-AND-PVB rows first). 012's earlier expansion of bucket 1
|
|
242
|
+
-- means this stays small or zero.
|
|
243
|
+
-- ============================================================
|
|
244
|
+
DO $$
|
|
245
|
+
DECLARE
|
|
246
|
+
rows_updated int;
|
|
247
|
+
BEGIN
|
|
248
|
+
UPDATE memory_items SET project = 'pvb'
|
|
249
|
+
WHERE project = 'chopin-nashville'
|
|
250
|
+
AND (
|
|
251
|
+
content ILIKE '%PVB%'
|
|
252
|
+
OR content ILIKE '%petvetbid%'
|
|
253
|
+
OR content ILIKE '%pet vet bid%'
|
|
254
|
+
);
|
|
255
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
256
|
+
RAISE NOTICE '[012-retaxonomy] bucket 6 (pvb): % rows re-tagged', rows_updated;
|
|
257
|
+
END $$;
|
|
258
|
+
|
|
259
|
+
-- ============================================================
|
|
260
|
+
-- BUCKET 7 — claimguard (NEW in 012)
|
|
261
|
+
--
|
|
262
|
+
-- ClaimGuard-AI is the active Unagi project (Joshua's roadmap shows it as
|
|
263
|
+
-- the next 1-2 sprints after Sprint 41 ships). On-disk path is
|
|
264
|
+
-- ~/Documents/Unagi/gorgias-ticket-monitor/. Keywords:
|
|
265
|
+
-- - claimguard (substring; product name, near-zero false positives)
|
|
266
|
+
-- - gorgias-ticket-monitor (the on-disk dir name; near-zero FP)
|
|
267
|
+
-- - "gorgias ticket monitor" (the spoken-form variant)
|
|
268
|
+
--
|
|
269
|
+
-- The bare "gorgias" keyword is intentionally NOT used here because the
|
|
270
|
+
-- pre-existing `gorgias` tag (468 rows) and `gorgias-ticket-monitor` tag
|
|
271
|
+
-- (207 rows) are separate categories — bare "gorgias" content could be
|
|
272
|
+
-- about Gorgias-the-helpdesk-product unrelated to ClaimGuard. The
|
|
273
|
+
-- compound-token discipline keeps the bucket precise.
|
|
274
|
+
-- ============================================================
|
|
275
|
+
DO $$
|
|
276
|
+
DECLARE
|
|
277
|
+
rows_updated int;
|
|
278
|
+
BEGIN
|
|
279
|
+
UPDATE memory_items SET project = 'claimguard'
|
|
280
|
+
WHERE project = 'chopin-nashville'
|
|
281
|
+
AND (
|
|
282
|
+
content ILIKE '%claimguard%'
|
|
283
|
+
OR content ILIKE '%gorgias-ticket-monitor%'
|
|
284
|
+
OR content ILIKE '%gorgias ticket monitor%'
|
|
285
|
+
);
|
|
286
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
287
|
+
RAISE NOTICE '[012-retaxonomy] bucket 7 (claimguard): % rows re-tagged', rows_updated;
|
|
288
|
+
END $$;
|
|
289
|
+
|
|
290
|
+
-- ============================================================
|
|
291
|
+
-- BUCKET 8 — dor (REUSED VERBATIM from 011's tightened pattern)
|
|
292
|
+
--
|
|
293
|
+
-- 011's audit found that the original briefing's `%dor%` ILIKE pattern
|
|
294
|
+
-- produced ~33% false positives (matched "dormant", "vendored", "indoor",
|
|
295
|
+
-- etc.). 011 tightened to:
|
|
296
|
+
-- - POSIX word boundary `\mDOR\M` — case-sensitive uppercase only
|
|
297
|
+
-- - path/identifier markers: /DOR/, ~/Documents/DOR, dor.config,
|
|
298
|
+
-- "Rust LLM gateway" (DOR's tagline)
|
|
299
|
+
-- - openclaw substring (OpenClaw is the slack-channel automation product
|
|
300
|
+
-- that lives next to DOR in Joshua's stack)
|
|
301
|
+
--
|
|
302
|
+
-- 012 reuses this verbatim. After 011 caught 3 dor rows live, residue is
|
|
303
|
+
-- expected to be near-zero — but the bucket stays in case any new rows
|
|
304
|
+
-- accumulated post-Sprint-39 carry the markers.
|
|
305
|
+
-- ============================================================
|
|
306
|
+
DO $$
|
|
307
|
+
DECLARE
|
|
308
|
+
rows_updated int;
|
|
309
|
+
BEGIN
|
|
310
|
+
UPDATE memory_items SET project = 'dor'
|
|
311
|
+
WHERE project = 'chopin-nashville'
|
|
312
|
+
AND (
|
|
313
|
+
content ~ '\mDOR\M'
|
|
314
|
+
OR content ILIKE '%/DOR/%'
|
|
315
|
+
OR content ILIKE '%~/Documents/DOR%'
|
|
316
|
+
OR content ILIKE '%dor.config%'
|
|
317
|
+
OR content ILIKE '%Rust LLM gateway%'
|
|
318
|
+
OR content ILIKE '%openclaw%'
|
|
319
|
+
);
|
|
320
|
+
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
|
321
|
+
RAISE NOTICE '[012-retaxonomy] bucket 8 (dor): % rows re-tagged', rows_updated;
|
|
322
|
+
END $$;
|
|
323
|
+
|
|
324
|
+
-- ============================================================
|
|
325
|
+
-- AUDIT AFTER
|
|
326
|
+
-- ============================================================
|
|
327
|
+
DO $$
|
|
328
|
+
DECLARE
|
|
329
|
+
after_chopin int;
|
|
330
|
+
after_termdeck int;
|
|
331
|
+
after_rumen int;
|
|
332
|
+
after_podium int;
|
|
333
|
+
after_bohemia int;
|
|
334
|
+
after_scheduler int;
|
|
335
|
+
after_pvb int;
|
|
336
|
+
after_claimguard int;
|
|
337
|
+
after_dor int;
|
|
338
|
+
BEGIN
|
|
339
|
+
SELECT count(*) INTO after_chopin FROM memory_items WHERE project = 'chopin-nashville';
|
|
340
|
+
SELECT count(*) INTO after_termdeck FROM memory_items WHERE project = 'termdeck';
|
|
341
|
+
SELECT count(*) INTO after_rumen FROM memory_items WHERE project = 'rumen';
|
|
342
|
+
SELECT count(*) INTO after_podium FROM memory_items WHERE project = 'podium';
|
|
343
|
+
SELECT count(*) INTO after_bohemia FROM memory_items WHERE project = 'chopin-in-bohemia';
|
|
344
|
+
SELECT count(*) INTO after_scheduler FROM memory_items WHERE project = 'chopin-scheduler';
|
|
345
|
+
SELECT count(*) INTO after_pvb FROM memory_items WHERE project = 'pvb';
|
|
346
|
+
SELECT count(*) INTO after_claimguard FROM memory_items WHERE project = 'claimguard';
|
|
347
|
+
SELECT count(*) INTO after_dor FROM memory_items WHERE project = 'dor';
|
|
348
|
+
RAISE NOTICE '[012-retaxonomy] AFTER chopin-nashville=% termdeck=% rumen=% podium=% chopin-in-bohemia=% chopin-scheduler=% pvb=% claimguard=% dor=%',
|
|
349
|
+
after_chopin, after_termdeck, after_rumen, after_podium, after_bohemia, after_scheduler, after_pvb, after_claimguard, after_dor;
|
|
350
|
+
RAISE NOTICE '[012-retaxonomy] Sprint 41 acceptance target: chopin-nashville drops 947 -> < 100 after T2+T4. T2 (this migration) handles deterministic keyword cases; T4 LLM-classifies the residue. If chopin-nashville count after this migration is still > 200, T4 has more rows to chew through; if < 100 already, T4 may have very little to do.';
|
|
351
|
+
END $$;
|
|
352
|
+
|
|
353
|
+
COMMIT;
|
|
354
|
+
|
|
355
|
+
-- ============================================================
|
|
356
|
+
-- POST-APPLY: optional verification queries (NOT part of the migration).
|
|
357
|
+
-- Run separately to confirm the new taxonomy holds and to spot-check
|
|
358
|
+
-- false-positive rates per bucket.
|
|
359
|
+
-- ============================================================
|
|
360
|
+
--
|
|
361
|
+
-- 1. Tag distribution after migration:
|
|
362
|
+
-- SELECT project, count(*) FROM memory_items
|
|
363
|
+
-- GROUP BY project ORDER BY count(*) DESC LIMIT 20;
|
|
364
|
+
--
|
|
365
|
+
-- 2. Confirm no chopin-nashville rows match obvious termdeck/rumen/podium
|
|
366
|
+
-- keywords (these should all return 0 if the migration succeeded):
|
|
367
|
+
-- SELECT count(*) FROM memory_items
|
|
368
|
+
-- WHERE project='chopin-nashville'
|
|
369
|
+
-- AND (content ILIKE '%termdeck%' OR content ILIKE '%rumen%'
|
|
370
|
+
-- OR content ILIKE '%podium%' OR content ILIKE '%bohemia%'
|
|
371
|
+
-- OR content ILIKE '%scheduling%' OR content ILIKE '%claimguard%');
|
|
372
|
+
-- -- Expected: 0
|
|
373
|
+
--
|
|
374
|
+
-- 3. Spot-check false-positive rate per bucket (replace 'termdeck' with
|
|
375
|
+
-- each new tag in turn):
|
|
376
|
+
-- SELECT id, left(content, 200) AS preview
|
|
377
|
+
-- FROM memory_items
|
|
378
|
+
-- WHERE project='termdeck' AND id IN (
|
|
379
|
+
-- SELECT id FROM memory_items
|
|
380
|
+
-- WHERE project='termdeck'
|
|
381
|
+
-- ORDER BY updated_at DESC LIMIT 10
|
|
382
|
+
-- );
|
|
383
|
+
--
|
|
384
|
+
-- 4. Confirm the legitimate-chopin-nashville signal is preserved (rows
|
|
385
|
+
-- matching competition/laureate/applicant/Acceptd/NICPC/Bohemia/
|
|
386
|
+
-- repertoire keywords should still be tagged chopin-nashville,
|
|
387
|
+
-- EXCEPT for those that ALSO matched a code-project keyword and got
|
|
388
|
+
-- legitimately re-tagged):
|
|
389
|
+
-- SELECT count(*) FROM memory_items
|
|
390
|
+
-- WHERE project='chopin-nashville'
|
|
391
|
+
-- AND (content ILIKE '%competition%' OR content ILIKE '%laureate%'
|
|
392
|
+
-- OR content ILIKE '%applicant%' OR content ILIKE '%Acceptd%'
|
|
393
|
+
-- OR content ILIKE '%NICPC%' OR content ILIKE '%repertoire%'
|
|
394
|
+
-- OR content ILIKE '%jury%');
|
|
395
|
+
-- -- Expected: most of the residue (~71+ rows from Sprint 39 baseline,
|
|
396
|
+
-- -- possibly higher as more legitimate competition content has
|
|
397
|
+
-- -- accumulated since 2026-04-27).
|