@cleocode/core 2026.4.37 → 2026.4.38

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 (61) hide show
  1. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  2. package/dist/hooks/handlers/task-hooks.js +11 -0
  3. package/dist/hooks/handlers/task-hooks.js.map +1 -1
  4. package/dist/index.js +644 -33
  5. package/dist/index.js.map +4 -4
  6. package/dist/internal.d.ts +3 -1
  7. package/dist/internal.d.ts.map +1 -1
  8. package/dist/internal.js +3 -1
  9. package/dist/internal.js.map +1 -1
  10. package/dist/memory/decisions.d.ts.map +1 -1
  11. package/dist/memory/decisions.js +18 -0
  12. package/dist/memory/decisions.js.map +1 -1
  13. package/dist/memory/engine-compat.d.ts +17 -0
  14. package/dist/memory/engine-compat.d.ts.map +1 -1
  15. package/dist/memory/engine-compat.js +36 -0
  16. package/dist/memory/engine-compat.js.map +1 -1
  17. package/dist/memory/graph-memory-bridge.d.ts +158 -0
  18. package/dist/memory/graph-memory-bridge.d.ts.map +1 -0
  19. package/dist/memory/graph-memory-bridge.js +519 -0
  20. package/dist/memory/graph-memory-bridge.js.map +1 -0
  21. package/dist/memory/index.d.ts +1 -0
  22. package/dist/memory/index.d.ts.map +1 -1
  23. package/dist/memory/index.js +2 -0
  24. package/dist/memory/index.js.map +1 -1
  25. package/dist/memory/learnings.d.ts.map +1 -1
  26. package/dist/memory/learnings.js +18 -0
  27. package/dist/memory/learnings.js.map +1 -1
  28. package/dist/memory/llm-extraction.js.map +1 -1
  29. package/dist/memory/patterns.d.ts.map +1 -1
  30. package/dist/memory/patterns.js +18 -0
  31. package/dist/memory/patterns.js.map +1 -1
  32. package/dist/memory/quality-feedback.d.ts +129 -0
  33. package/dist/memory/quality-feedback.d.ts.map +1 -0
  34. package/dist/memory/quality-feedback.js +449 -0
  35. package/dist/memory/quality-feedback.js.map +1 -0
  36. package/dist/memory/sleep-consolidation.d.ts +98 -0
  37. package/dist/memory/sleep-consolidation.d.ts.map +1 -0
  38. package/dist/memory/sleep-consolidation.js +706 -0
  39. package/dist/memory/sleep-consolidation.js.map +1 -0
  40. package/dist/memory/temporal-supersession.d.ts +155 -0
  41. package/dist/memory/temporal-supersession.d.ts.map +1 -0
  42. package/dist/memory/temporal-supersession.js +406 -0
  43. package/dist/memory/temporal-supersession.js.map +1 -0
  44. package/package.json +6 -6
  45. package/src/hooks/handlers/task-hooks.ts +11 -0
  46. package/src/internal.ts +12 -0
  47. package/src/memory/__tests__/graph-memory-bridge.test.ts +357 -0
  48. package/src/memory/__tests__/llm-extraction.test.ts +17 -0
  49. package/src/memory/__tests__/quality-feedback.test.ts +418 -0
  50. package/src/memory/__tests__/sleep-consolidation.test.ts +790 -0
  51. package/src/memory/__tests__/temporal-supersession.test.ts +534 -0
  52. package/src/memory/decisions.ts +24 -0
  53. package/src/memory/engine-compat.ts +37 -0
  54. package/src/memory/graph-memory-bridge.ts +751 -0
  55. package/src/memory/index.ts +2 -0
  56. package/src/memory/learnings.ts +24 -0
  57. package/src/memory/patterns.ts +24 -0
  58. package/src/memory/quality-feedback.ts +640 -0
  59. package/src/memory/sleep-consolidation.ts +932 -0
  60. package/src/memory/temporal-supersession.ts +568 -0
  61. package/src/store/__tests__/performance-safety.test.ts +4 -4
@@ -0,0 +1,534 @@
1
+ /**
2
+ * Tests for temporal-supersession.ts — audit-trail supersession for CLEO BRAIN.
3
+ *
4
+ * Covers:
5
+ * - supersedeMemory: marks old as invalid, creates supersedes edge
6
+ * - detectSupersession: finds high-overlap candidates on fresh entries
7
+ * - getSupersessionChain: traverses supersedes edges newest→oldest
8
+ * - isLatest: checks invalid_at IS NULL
9
+ */
10
+
11
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
12
+ import { tmpdir } from 'node:os';
13
+ import { join } from 'node:path';
14
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
15
+
16
+ let tempDir: string;
17
+ let cleoDir: string;
18
+
19
+ describe('temporal-supersession', () => {
20
+ beforeEach(async () => {
21
+ tempDir = await mkdtemp(join(tmpdir(), 'cleo-supersession-'));
22
+ cleoDir = join(tempDir, '.cleo');
23
+ await mkdir(cleoDir, { recursive: true });
24
+ process.env['CLEO_DIR'] = cleoDir;
25
+ });
26
+
27
+ afterEach(async () => {
28
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
29
+ closeBrainDb();
30
+ delete process.env['CLEO_DIR'];
31
+ await rm(tempDir, { recursive: true, force: true });
32
+ });
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // supersedeMemory
36
+ // ---------------------------------------------------------------------------
37
+
38
+ describe('supersedeMemory', () => {
39
+ it('marks the old entry as invalid (sets invalid_at)', async () => {
40
+ const { storeDecision } = await import('../decisions.js');
41
+ const { supersedeMemory, isLatest } = await import('../temporal-supersession.js');
42
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
43
+ closeBrainDb();
44
+
45
+ const old = await storeDecision(tempDir, {
46
+ type: 'technical',
47
+ decision: 'Use SQLite for storage',
48
+ rationale: 'Simple and reliable',
49
+ confidence: 'high',
50
+ });
51
+ closeBrainDb();
52
+
53
+ const replacement = await storeDecision(tempDir, {
54
+ type: 'technical',
55
+ decision: 'Use PostgreSQL for storage',
56
+ rationale: 'Better concurrency',
57
+ confidence: 'high',
58
+ });
59
+ closeBrainDb();
60
+
61
+ // Both should be latest before supersession
62
+ expect(await isLatest(tempDir, old.id)).toBe(true);
63
+ expect(await isLatest(tempDir, replacement.id)).toBe(true);
64
+ closeBrainDb();
65
+
66
+ const result = await supersedeMemory(
67
+ tempDir,
68
+ old.id,
69
+ replacement.id,
70
+ 'PostgreSQL chosen for production scalability',
71
+ );
72
+ closeBrainDb();
73
+
74
+ expect(result.success).toBe(true);
75
+ expect(result.oldId).toBe(old.id);
76
+ expect(result.newId).toBe(replacement.id);
77
+ expect(result.edgeType).toBe('supersedes');
78
+
79
+ // Old entry is now invalid; replacement remains valid
80
+ expect(await isLatest(tempDir, old.id)).toBe(false);
81
+ closeBrainDb();
82
+ expect(await isLatest(tempDir, replacement.id)).toBe(true);
83
+ });
84
+
85
+ it('creates a supersedes graph edge from new to old', async () => {
86
+ const { storeDecision } = await import('../decisions.js');
87
+ const { supersedeMemory } = await import('../temporal-supersession.js');
88
+ const { getBrainDb, getBrainNativeDb, closeBrainDb } = await import(
89
+ '../../store/brain-sqlite.js'
90
+ );
91
+ closeBrainDb();
92
+
93
+ const old = await storeDecision(tempDir, {
94
+ type: 'architecture',
95
+ decision: 'Monorepo layout',
96
+ rationale: 'Shared tooling',
97
+ confidence: 'medium',
98
+ });
99
+ closeBrainDb();
100
+
101
+ const replacement = await storeDecision(tempDir, {
102
+ type: 'architecture',
103
+ decision: 'Polyrepo layout',
104
+ rationale: 'Independent deployments',
105
+ confidence: 'high',
106
+ });
107
+ closeBrainDb();
108
+
109
+ await supersedeMemory(tempDir, old.id, replacement.id, 'scale requirements changed');
110
+ closeBrainDb();
111
+
112
+ // Verify edge in brain_page_edges
113
+ await getBrainDb(tempDir);
114
+ const nativeDb = getBrainNativeDb();
115
+ expect(nativeDb).not.toBeNull();
116
+
117
+ const edge = nativeDb!
118
+ .prepare(
119
+ `SELECT from_id, to_id, edge_type, provenance
120
+ FROM brain_page_edges
121
+ WHERE edge_type = 'supersedes'
122
+ AND to_id = ?`,
123
+ )
124
+ .get(`decision:${old.id}`) as
125
+ | {
126
+ from_id: string;
127
+ to_id: string;
128
+ edge_type: string;
129
+ provenance: string;
130
+ }
131
+ | undefined;
132
+
133
+ expect(edge).toBeDefined();
134
+ expect(edge!.from_id).toBe(`decision:${replacement.id}`);
135
+ expect(edge!.to_id).toBe(`decision:${old.id}`);
136
+ expect(edge!.edge_type).toBe('supersedes');
137
+ expect(edge!.provenance).toBe('scale requirements changed');
138
+ });
139
+
140
+ it('stores the reason as edge provenance (up to 500 chars)', async () => {
141
+ const { storeLearning } = await import('../learnings.js');
142
+ const { supersedeMemory } = await import('../temporal-supersession.js');
143
+ const { getBrainDb, getBrainNativeDb, closeBrainDb } = await import(
144
+ '../../store/brain-sqlite.js'
145
+ );
146
+ closeBrainDb();
147
+
148
+ const old = await storeLearning(tempDir, {
149
+ insight: 'Always use transactions for multi-step writes',
150
+ source: 'manual',
151
+ confidence: 0.9,
152
+ });
153
+ closeBrainDb();
154
+
155
+ const replacement = await storeLearning(tempDir, {
156
+ insight: 'Prefer batch inserts over individual transactions for bulk writes',
157
+ source: 'manual',
158
+ confidence: 0.95,
159
+ });
160
+ closeBrainDb();
161
+
162
+ const reason = 'Performance testing revealed batch insert is 10x faster';
163
+ await supersedeMemory(tempDir, old.id, replacement.id, reason);
164
+ closeBrainDb();
165
+
166
+ await getBrainDb(tempDir);
167
+ const nativeDb = getBrainNativeDb();
168
+ const edge = nativeDb!
169
+ .prepare(
170
+ `SELECT provenance FROM brain_page_edges
171
+ WHERE edge_type = 'supersedes' AND to_id = ?`,
172
+ )
173
+ .get(`learning:${old.id}`) as { provenance: string } | undefined;
174
+
175
+ expect(edge?.provenance).toBe(reason);
176
+ });
177
+
178
+ it('does NOT delete the old entry (audit trail preserved)', async () => {
179
+ const { storeLearning } = await import('../learnings.js');
180
+ const { supersedeMemory } = await import('../temporal-supersession.js');
181
+ const { getBrainDb, getBrainNativeDb, closeBrainDb } = await import(
182
+ '../../store/brain-sqlite.js'
183
+ );
184
+ closeBrainDb();
185
+
186
+ const old = await storeLearning(tempDir, {
187
+ insight: 'Use camelCase for TypeScript variables',
188
+ source: 'manual',
189
+ confidence: 0.8,
190
+ });
191
+ closeBrainDb();
192
+
193
+ const replacement = await storeLearning(tempDir, {
194
+ insight: 'Follow the project ESLint config for all naming conventions',
195
+ source: 'manual',
196
+ confidence: 0.95,
197
+ });
198
+ closeBrainDb();
199
+
200
+ await supersedeMemory(tempDir, old.id, replacement.id, 'style guide updated');
201
+ closeBrainDb();
202
+
203
+ // Old entry must still exist in the table
204
+ await getBrainDb(tempDir);
205
+ const nativeDb = getBrainNativeDb();
206
+ const row = nativeDb!
207
+ .prepare(`SELECT id, insight, invalid_at FROM brain_learnings WHERE id = ?`)
208
+ .get(old.id) as { id: string; insight: string; invalid_at: string | null } | undefined;
209
+
210
+ expect(row).toBeDefined();
211
+ expect(row!.id).toBe(old.id);
212
+ expect(row!.insight).toBe('Use camelCase for TypeScript variables');
213
+ expect(row!.invalid_at).not.toBeNull(); // marked invalid but not deleted
214
+ });
215
+
216
+ it('throws when oldId equals newId', async () => {
217
+ const { supersedeMemory } = await import('../temporal-supersession.js');
218
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
219
+ closeBrainDb();
220
+
221
+ await expect(supersedeMemory(tempDir, 'D001', 'D001', 'self')).rejects.toThrow(
222
+ 'must be different',
223
+ );
224
+ });
225
+
226
+ it('throws when reason is empty', async () => {
227
+ const { supersedeMemory } = await import('../temporal-supersession.js');
228
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
229
+ closeBrainDb();
230
+
231
+ await expect(supersedeMemory(tempDir, 'D001', 'D002', '')).rejects.toThrow(
232
+ 'reason is required',
233
+ );
234
+ });
235
+
236
+ it('throws when oldId is not found', async () => {
237
+ const { storeDecision } = await import('../decisions.js');
238
+ const { supersedeMemory } = await import('../temporal-supersession.js');
239
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
240
+ closeBrainDb();
241
+
242
+ const real = await storeDecision(tempDir, {
243
+ type: 'technical',
244
+ decision: 'Use Redis for caching',
245
+ rationale: 'Low latency',
246
+ confidence: 'high',
247
+ });
248
+ closeBrainDb();
249
+
250
+ await expect(supersedeMemory(tempDir, 'NONEXISTENT', real.id, 'reason')).rejects.toThrow(
251
+ 'Entry not found',
252
+ );
253
+ });
254
+ });
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // detectSupersession
258
+ // ---------------------------------------------------------------------------
259
+
260
+ describe('detectSupersession', () => {
261
+ it('returns empty array when no similar entries exist', async () => {
262
+ const { detectSupersession } = await import('../temporal-supersession.js');
263
+ const { storeDecision } = await import('../decisions.js');
264
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
265
+ closeBrainDb();
266
+
267
+ const d = await storeDecision(tempDir, {
268
+ type: 'technical',
269
+ decision: 'Use Redis for caching',
270
+ rationale: 'Low latency reads',
271
+ confidence: 'high',
272
+ });
273
+ closeBrainDb();
274
+
275
+ // Provide a completely different text so no overlap threshold is met
276
+ const candidates = await detectSupersession(tempDir, {
277
+ id: d.id,
278
+ text: 'Completely unrelated topic about database migrations',
279
+ createdAt: new Date().toISOString().replace('T', ' ').slice(0, 19),
280
+ });
281
+ closeBrainDb();
282
+
283
+ expect(candidates).toEqual([]);
284
+ });
285
+
286
+ it('detects high-overlap entries as candidates', async () => {
287
+ const { storeLearning } = await import('../learnings.js');
288
+ const { detectSupersession } = await import('../temporal-supersession.js');
289
+ const { getBrainDb, getBrainNativeDb, closeBrainDb } = await import(
290
+ '../../store/brain-sqlite.js'
291
+ );
292
+ closeBrainDb();
293
+
294
+ // Store an older learning first with a back-dated timestamp
295
+ await getBrainDb(tempDir);
296
+ const nativeDb = getBrainNativeDb();
297
+ const pastDate = '2025-01-01 00:00:00';
298
+ nativeDb!
299
+ .prepare(
300
+ `INSERT INTO brain_learnings (id, insight, source, confidence, applicable_types_json, memory_tier, memory_type, source_confidence, verified, valid_at, created_at)
301
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
302
+ )
303
+ .run(
304
+ 'L-OLDIE001',
305
+ 'pnpm workspace filters using --filter flag for targeted builds',
306
+ 'manual',
307
+ 0.8,
308
+ '[]',
309
+ 'medium',
310
+ 'semantic',
311
+ 'owner',
312
+ 1,
313
+ pastDate,
314
+ pastDate,
315
+ );
316
+ closeBrainDb();
317
+
318
+ // Store a new learning with highly overlapping text
319
+ const newEntry = await storeLearning(tempDir, {
320
+ insight:
321
+ 'pnpm workspace filters using --filter flag for targeted builds with recursive mode',
322
+ source: 'manual',
323
+ confidence: 0.9,
324
+ });
325
+ closeBrainDb();
326
+
327
+ const nowish = new Date().toISOString().replace('T', ' ').slice(0, 19);
328
+ const candidates = await detectSupersession(tempDir, {
329
+ id: newEntry.id,
330
+ text: 'pnpm workspace filters using --filter flag for targeted builds with recursive mode',
331
+ createdAt: nowish,
332
+ });
333
+ closeBrainDb();
334
+
335
+ // L-OLDIE001 should appear as a candidate (high keyword overlap)
336
+ const found = candidates.find((c) => c.existingId === 'L-OLDIE001');
337
+ expect(found).toBeDefined();
338
+ expect(found!.similarity).toBeGreaterThanOrEqual(0.8);
339
+ });
340
+
341
+ it('returns empty array on DB unavailability (best-effort)', async () => {
342
+ const { detectSupersession } = await import('../temporal-supersession.js');
343
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
344
+ closeBrainDb();
345
+
346
+ // Use a non-existent project root so brain.db cannot be opened
347
+ const result = await detectSupersession('/tmp/nonexistent-project-xyz-cleo', {
348
+ id: 'D001',
349
+ text: 'some text',
350
+ createdAt: '2026-01-01 00:00:00',
351
+ });
352
+
353
+ // Best-effort: should return empty, not throw
354
+ expect(result).toEqual([]);
355
+ });
356
+ });
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // getSupersessionChain
360
+ // ---------------------------------------------------------------------------
361
+
362
+ describe('getSupersessionChain', () => {
363
+ it('returns a single-entry chain for the latest version (no predecessors)', async () => {
364
+ const { storeDecision } = await import('../decisions.js');
365
+ const { getSupersessionChain } = await import('../temporal-supersession.js');
366
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
367
+ closeBrainDb();
368
+
369
+ const d = await storeDecision(tempDir, {
370
+ type: 'technical',
371
+ decision: 'Use Vitest for testing',
372
+ rationale: 'Fast and compatible with ESM',
373
+ confidence: 'high',
374
+ });
375
+ closeBrainDb();
376
+
377
+ const chain = await getSupersessionChain(tempDir, d.id);
378
+ closeBrainDb();
379
+
380
+ // The entry has no supersedes edges yet — chain contains just itself
381
+ expect(chain.entryId).toBe(d.id);
382
+ expect(chain.chain).toHaveLength(1);
383
+ expect(chain.chain[0]!.entryId).toBe(d.id);
384
+ expect(chain.chain[0]!.isLatest).toBe(true);
385
+ expect(chain.chain[0]!.supersededReason).toBeNull();
386
+ });
387
+
388
+ it('returns ordered chain [newest → oldest] across two versions', async () => {
389
+ const { storeDecision } = await import('../decisions.js');
390
+ const { supersedeMemory, getSupersessionChain } = await import('../temporal-supersession.js');
391
+ const { getBrainDb, getBrainNativeDb, closeBrainDb } = await import(
392
+ '../../store/brain-sqlite.js'
393
+ );
394
+ closeBrainDb();
395
+
396
+ // v1
397
+ const v1 = await storeDecision(tempDir, {
398
+ type: 'architecture',
399
+ decision: 'Deploy to Heroku',
400
+ rationale: 'Simple PaaS',
401
+ confidence: 'medium',
402
+ });
403
+ closeBrainDb();
404
+
405
+ // v2
406
+ const v2 = await storeDecision(tempDir, {
407
+ type: 'architecture',
408
+ decision: 'Deploy to AWS ECS',
409
+ rationale: 'Better control and cost at scale',
410
+ confidence: 'high',
411
+ });
412
+ closeBrainDb();
413
+
414
+ // Insert graph nodes directly via raw SQL so the chain traversal can find them.
415
+ // This avoids the need to mock the read-only isAutoCaptureEnabled export.
416
+ await getBrainDb(tempDir);
417
+ const nativeDb2 = getBrainNativeDb()!;
418
+ const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
419
+ nativeDb2
420
+ .prepare(
421
+ `INSERT OR IGNORE INTO brain_page_nodes (id, node_type, label, quality_score, created_at, last_activity_at)
422
+ VALUES (?, 'decision', ?, ?, ?, ?)`,
423
+ )
424
+ .run(`decision:${v1.id}`, v1.decision, 0.7, now, now);
425
+ nativeDb2
426
+ .prepare(
427
+ `INSERT OR IGNORE INTO brain_page_nodes (id, node_type, label, quality_score, created_at, last_activity_at)
428
+ VALUES (?, 'decision', ?, ?, ?, ?)`,
429
+ )
430
+ .run(`decision:${v2.id}`, v2.decision, 0.9, now, now);
431
+ closeBrainDb();
432
+
433
+ await supersedeMemory(tempDir, v1.id, v2.id, 'moved to AWS for production');
434
+ closeBrainDb();
435
+
436
+ // Trace chain starting from v2 (the newest)
437
+ const chain = await getSupersessionChain(tempDir, v2.id);
438
+ closeBrainDb();
439
+
440
+ expect(chain.entryId).toBe(v2.id);
441
+ // v2 is newest (chain[0]) → v1 is the superseded older version (chain[1])
442
+ expect(chain.chain).toHaveLength(2);
443
+ expect(chain.chain[0]!.entryId).toBe(v2.id);
444
+ expect(chain.chain[0]!.isLatest).toBe(true);
445
+ expect(chain.chain[1]!.entryId).toBe(v1.id);
446
+ expect(chain.chain[1]!.isLatest).toBe(false);
447
+ expect(chain.chain[1]!.supersededReason).toBe('moved to AWS for production');
448
+ });
449
+
450
+ it('returns empty chain for unknown entryId', async () => {
451
+ const { getSupersessionChain } = await import('../temporal-supersession.js');
452
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
453
+ closeBrainDb();
454
+
455
+ // Initialise the DB (needed to call getBrainDb internally)
456
+ const { getBrainDb } = await import('../../store/brain-sqlite.js');
457
+ await getBrainDb(tempDir);
458
+ closeBrainDb();
459
+
460
+ const chain = await getSupersessionChain(tempDir, 'NONEXISTENT-ID');
461
+ closeBrainDb();
462
+
463
+ expect(chain.entryId).toBe('NONEXISTENT-ID');
464
+ expect(chain.chain).toEqual([]);
465
+ });
466
+ });
467
+
468
+ // ---------------------------------------------------------------------------
469
+ // isLatest
470
+ // ---------------------------------------------------------------------------
471
+
472
+ describe('isLatest', () => {
473
+ it('returns true for a freshly-stored entry (invalid_at IS NULL)', async () => {
474
+ const { storeDecision } = await import('../decisions.js');
475
+ const { isLatest } = await import('../temporal-supersession.js');
476
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
477
+ closeBrainDb();
478
+
479
+ const d = await storeDecision(tempDir, {
480
+ type: 'technical',
481
+ decision: 'Use TypeScript strict mode',
482
+ rationale: 'Catches bugs early',
483
+ confidence: 'high',
484
+ });
485
+ closeBrainDb();
486
+
487
+ expect(await isLatest(tempDir, d.id)).toBe(true);
488
+ });
489
+
490
+ it('returns false after the entry is superseded', async () => {
491
+ const { storeDecision } = await import('../decisions.js');
492
+ const { supersedeMemory, isLatest } = await import('../temporal-supersession.js');
493
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
494
+ closeBrainDb();
495
+
496
+ const old = await storeDecision(tempDir, {
497
+ type: 'technical',
498
+ decision: 'Use Jest for testing',
499
+ rationale: 'Wide ecosystem',
500
+ confidence: 'medium',
501
+ });
502
+ closeBrainDb();
503
+
504
+ const replacement = await storeDecision(tempDir, {
505
+ type: 'technical',
506
+ decision: 'Use Vitest for testing',
507
+ rationale: 'Faster and ESM-native',
508
+ confidence: 'high',
509
+ });
510
+ closeBrainDb();
511
+
512
+ expect(await isLatest(tempDir, old.id)).toBe(true);
513
+ closeBrainDb();
514
+
515
+ await supersedeMemory(tempDir, old.id, replacement.id, 'Vitest is faster');
516
+ closeBrainDb();
517
+
518
+ expect(await isLatest(tempDir, old.id)).toBe(false);
519
+ closeBrainDb();
520
+ expect(await isLatest(tempDir, replacement.id)).toBe(true);
521
+ });
522
+
523
+ it('returns false for an unknown entryId', async () => {
524
+ const { isLatest } = await import('../temporal-supersession.js');
525
+ const { getBrainDb, closeBrainDb } = await import('../../store/brain-sqlite.js');
526
+ closeBrainDb();
527
+
528
+ await getBrainDb(tempDir);
529
+ closeBrainDb();
530
+
531
+ expect(await isLatest(tempDir, 'NONEXISTENT-ID')).toBe(false);
532
+ });
533
+ });
534
+ });
@@ -15,6 +15,7 @@ import { taskExistsInTasksDb } from '../store/cross-db-cleanup.js';
15
15
  import { getDb } from '../store/sqlite.js';
16
16
  import { addGraphEdge, upsertGraphNode } from './graph-auto-populate.js';
17
17
  import { computeDecisionQuality } from './quality-scoring.js';
18
+ import { detectSupersession, supersedeMemory } from './temporal-supersession.js';
18
19
 
19
20
  /** Parameters for storing a new decision. */
20
21
  export interface StoreDecisionParams {
@@ -232,6 +233,29 @@ export async function storeDecision(
232
233
  /* Graph population is best-effort — never block the primary return */
233
234
  }
234
235
 
236
+ // Detect supersession: check if this new decision supersedes any existing ones.
237
+ // Fire-and-forget — never block the primary return.
238
+ detectSupersession(projectRoot, {
239
+ id: saved.id,
240
+ text: saved.decision + ' ' + saved.rationale,
241
+ createdAt: saved.createdAt ?? new Date().toISOString().replace('T', ' ').slice(0, 19),
242
+ })
243
+ .then((candidates) => {
244
+ for (const candidate of candidates) {
245
+ supersedeMemory(
246
+ projectRoot,
247
+ candidate.existingId,
248
+ saved.id,
249
+ 'auto:decision-supersedes — high overlap detected at store time',
250
+ ).catch(() => {
251
+ /* best-effort */
252
+ });
253
+ }
254
+ })
255
+ .catch(() => {
256
+ /* best-effort */
257
+ });
258
+
235
259
  return saved;
236
260
  }
237
261
 
@@ -1851,3 +1851,40 @@ export async function memoryGraphRemove(
1851
1851
  };
1852
1852
  }
1853
1853
  }
1854
+
1855
+ // ============================================================================
1856
+ // Quality Feedback Report (T555)
1857
+ // ============================================================================
1858
+
1859
+ /**
1860
+ * Return the BRAIN memory quality dashboard report.
1861
+ *
1862
+ * Aggregates retrieval log, usage log, and all four typed tables to produce
1863
+ * a MemoryQualityReport with tier distribution, top/never-retrieved entries,
1864
+ * quality score distribution, and noise ratio.
1865
+ *
1866
+ * @param projectRoot - Optional project root path; defaults to resolved root
1867
+ * @returns EngineResult containing a MemoryQualityReport object
1868
+ *
1869
+ * @example
1870
+ * ```typescript
1871
+ * const result = await memoryQualityReport('/project');
1872
+ * if (result.success) console.log(result.data.noiseRatio);
1873
+ * ```
1874
+ */
1875
+ export async function memoryQualityReport(projectRoot?: string): Promise<EngineResult> {
1876
+ try {
1877
+ const root = resolveRoot(projectRoot);
1878
+ const { getMemoryQualityReport } = await import('./quality-feedback.js');
1879
+ const report = await getMemoryQualityReport(root);
1880
+ return { success: true, data: report };
1881
+ } catch (error) {
1882
+ return {
1883
+ success: false,
1884
+ error: {
1885
+ code: 'E_QUALITY_REPORT',
1886
+ message: error instanceof Error ? error.message : String(error),
1887
+ },
1888
+ };
1889
+ }
1890
+ }