@cleocode/core 2026.4.58 → 2026.4.60

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 (32) hide show
  1. package/dist/index.js +488 -11
  2. package/dist/index.js.map +3 -3
  3. package/dist/internal.js +497 -13
  4. package/dist/internal.js.map +3 -3
  5. package/dist/memory/graph-queries.d.ts.map +1 -1
  6. package/dist/store/agent-registry-accessor.d.ts +1 -1
  7. package/dist/store/brain-accessor.d.ts +57 -1
  8. package/dist/store/brain-accessor.d.ts.map +1 -1
  9. package/dist/store/brain-schema.d.ts +738 -0
  10. package/dist/store/brain-schema.d.ts.map +1 -1
  11. package/dist/store/brain-sqlite.d.ts.map +1 -1
  12. package/dist/store/nexus-schema.d.ts +1 -1
  13. package/dist/system/health.d.ts.map +1 -1
  14. package/dist/validation/doctor/checks.d.ts +7 -0
  15. package/dist/validation/doctor/checks.d.ts.map +1 -1
  16. package/migrations/drizzle-brain/20260416000001_t673-retrieval-log-plasticity-columns/migration.sql +57 -0
  17. package/migrations/drizzle-brain/20260416000002_t673-plasticity-events-expand/migration.sql +44 -0
  18. package/migrations/drizzle-brain/20260416000003_t673-page-edges-plasticity-columns/migration.sql +44 -0
  19. package/migrations/drizzle-brain/20260416000004_t673-new-plasticity-tables/migration.sql +73 -0
  20. package/package.json +8 -8
  21. package/src/memory/__tests__/brain-retrieval-m1.test.ts +250 -0
  22. package/src/memory/__tests__/brain-schema-m2-m3.test.ts +418 -0
  23. package/src/memory/__tests__/brain-schema-m4.test.ts +494 -0
  24. package/src/memory/brain-retrieval.ts +1 -1
  25. package/src/memory/graph-queries.ts +14 -0
  26. package/src/store/agent-registry-accessor.ts +1 -1
  27. package/src/store/brain-accessor.ts +120 -0
  28. package/src/store/brain-schema.ts +373 -1
  29. package/src/store/brain-sqlite.ts +123 -0
  30. package/src/system/health.ts +4 -1
  31. package/src/validation/doctor/checks.ts +107 -0
  32. package/src/validation/protocols/protocols-markdown/research.md +1 -1
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Functional tests for STDP M2 (brain_plasticity_events expansion) and
3
+ * M3 (brain_page_edges plasticity columns) migrations.
4
+ *
5
+ * Uses a real in-memory SQLite database via getBrainDb() with a tmpdir
6
+ * CLEO_DIR. No vi.mock() on any brain or SQLite module.
7
+ *
8
+ * Test plan:
9
+ * M2-1: All 5 new columns exist in brain_plasticity_events after migration
10
+ * M2-2: INSERT with new columns succeeds; values round-trip correctly
11
+ * M2-3: INSERT without new columns also succeeds (defaults/nulls)
12
+ * M3-1: All 6 new columns exist in brain_page_edges after migration
13
+ * M3-2: INSERT with new plasticity columns succeeds; values round-trip
14
+ * M3-3: INSERT without new columns succeeds (defaults apply: 0, 'static')
15
+ * M3-4: plasticity_class enum values 'static', 'hebbian', 'stdp' are accepted
16
+ * M3-5: Seed UPDATE sets plasticity_class='hebbian' for co_retrieved edges
17
+ *
18
+ * @task T696 (M2)
19
+ * @task T706 (M3)
20
+ * @epic T627
21
+ */
22
+
23
+ import { mkdtemp, rm } from 'node:fs/promises';
24
+ import { tmpdir } from 'node:os';
25
+ import { join } from 'node:path';
26
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
27
+
28
+ vi.setConfig({ testTimeout: 30_000 });
29
+
30
+ // We do NOT mock brain-sqlite.js — this is a functional test.
31
+
32
+ let tempDir: string;
33
+
34
+ beforeEach(async () => {
35
+ tempDir = await mkdtemp(join(tmpdir(), 'cleo-brain-m2-m3-'));
36
+ const cleoDir = join(tempDir, '.cleo');
37
+ const { mkdirSync } = await import('node:fs');
38
+ mkdirSync(cleoDir, { recursive: true });
39
+ process.env['CLEO_DIR'] = cleoDir;
40
+ });
41
+
42
+ afterEach(async () => {
43
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
44
+ closeBrainDb();
45
+ delete process.env['CLEO_DIR'];
46
+ await rm(tempDir, { recursive: true, force: true });
47
+ // Reset module singleton so next test gets a fresh DB
48
+ const { resetBrainDbState } = await import('../../store/brain-sqlite.js');
49
+ resetBrainDbState();
50
+ });
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Helper: get PRAGMA table_info columns as a name→type map
54
+ // ---------------------------------------------------------------------------
55
+ async function getTableColumns(tableName: string): Promise<Map<string, string>> {
56
+ const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
57
+ const nativeDb = getBrainNativeDb();
58
+ if (!nativeDb) throw new Error('nativeDb is null');
59
+ type PragmaRow = { name: string; type: string };
60
+ const rows = nativeDb.prepare(`PRAGMA table_info(${tableName})`).all() as PragmaRow[];
61
+ const map = new Map<string, string>();
62
+ for (const row of rows) {
63
+ map.set(row.name, row.type.toUpperCase());
64
+ }
65
+ return map;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Helper: initialize brain DB (runs migrations, returns db handle)
70
+ // ---------------------------------------------------------------------------
71
+ async function openDb() {
72
+ const { getBrainDb } = await import('../../store/brain-sqlite.js');
73
+ return getBrainDb(tempDir);
74
+ }
75
+
76
+ // ===========================================================================
77
+ // M2: brain_plasticity_events — 5 new observability columns
78
+ // ===========================================================================
79
+
80
+ describe('M2 — brain_plasticity_events expansion', () => {
81
+ it('M2-1: all 5 new columns exist after migration', async () => {
82
+ await openDb();
83
+ const cols = await getTableColumns('brain_plasticity_events');
84
+
85
+ expect(cols.has('weight_before'), 'weight_before missing').toBe(true);
86
+ expect(cols.has('weight_after'), 'weight_after missing').toBe(true);
87
+ expect(cols.has('retrieval_log_id'), 'retrieval_log_id missing').toBe(true);
88
+ expect(cols.has('reward_signal'), 'reward_signal missing').toBe(true);
89
+ expect(cols.has('delta_t_ms'), 'delta_t_ms missing').toBe(true);
90
+
91
+ // Verify column types
92
+ expect(cols.get('weight_before')).toBe('REAL');
93
+ expect(cols.get('weight_after')).toBe('REAL');
94
+ expect(cols.get('retrieval_log_id')).toBe('INTEGER');
95
+ expect(cols.get('reward_signal')).toBe('REAL');
96
+ expect(cols.get('delta_t_ms')).toBe('INTEGER');
97
+ });
98
+
99
+ it('M2-2: INSERT with all new columns round-trips correctly', async () => {
100
+ await openDb();
101
+ const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
102
+ const nativeDb = getBrainNativeDb()!;
103
+
104
+ nativeDb
105
+ .prepare(
106
+ `INSERT INTO brain_plasticity_events
107
+ (source_node, target_node, delta_w, kind, session_id,
108
+ weight_before, weight_after, retrieval_log_id, reward_signal, delta_t_ms)
109
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
110
+ )
111
+ .run('obs:A', 'obs:B', 0.05, 'ltp', 'ses_test_001', 0.8, 0.85, 42, 1.0, 15000);
112
+
113
+ type EventRow = {
114
+ source_node: string;
115
+ target_node: string;
116
+ delta_w: number;
117
+ kind: string;
118
+ weight_before: number | null;
119
+ weight_after: number | null;
120
+ retrieval_log_id: number | null;
121
+ reward_signal: number | null;
122
+ delta_t_ms: number | null;
123
+ };
124
+
125
+ const row = nativeDb
126
+ .prepare(
127
+ `SELECT source_node, target_node, delta_w, kind,
128
+ weight_before, weight_after, retrieval_log_id, reward_signal, delta_t_ms
129
+ FROM brain_plasticity_events WHERE source_node = 'obs:A'`,
130
+ )
131
+ .get() as EventRow;
132
+
133
+ expect(row).not.toBeNull();
134
+ expect(row.source_node).toBe('obs:A');
135
+ expect(row.target_node).toBe('obs:B');
136
+ expect(row.delta_w).toBe(0.05);
137
+ expect(row.kind).toBe('ltp');
138
+ expect(row.weight_before).toBe(0.8);
139
+ expect(row.weight_after).toBe(0.85);
140
+ expect(row.retrieval_log_id).toBe(42);
141
+ expect(row.reward_signal).toBe(1.0);
142
+ expect(row.delta_t_ms).toBe(15000);
143
+ });
144
+
145
+ it('M2-3: INSERT without new columns succeeds (nulls for new cols)', async () => {
146
+ await openDb();
147
+ const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
148
+ const nativeDb = getBrainNativeDb()!;
149
+
150
+ // Old-style INSERT — no new columns
151
+ nativeDb
152
+ .prepare(
153
+ `INSERT INTO brain_plasticity_events
154
+ (source_node, target_node, delta_w, kind)
155
+ VALUES (?, ?, ?, ?)`,
156
+ )
157
+ .run('obs:C', 'obs:D', -0.06, 'ltd');
158
+
159
+ type EventRow = {
160
+ source_node: string;
161
+ weight_before: number | null;
162
+ weight_after: number | null;
163
+ retrieval_log_id: number | null;
164
+ reward_signal: number | null;
165
+ delta_t_ms: number | null;
166
+ };
167
+
168
+ const row = nativeDb
169
+ .prepare(
170
+ `SELECT source_node, weight_before, weight_after,
171
+ retrieval_log_id, reward_signal, delta_t_ms
172
+ FROM brain_plasticity_events WHERE source_node = 'obs:C'`,
173
+ )
174
+ .get() as EventRow;
175
+
176
+ expect(row).not.toBeNull();
177
+ // All new columns should be null (no default set)
178
+ expect(row.weight_before).toBeNull();
179
+ expect(row.weight_after).toBeNull();
180
+ expect(row.retrieval_log_id).toBeNull();
181
+ expect(row.reward_signal).toBeNull();
182
+ expect(row.delta_t_ms).toBeNull();
183
+ });
184
+
185
+ it('M2-4: indexes on new columns exist', async () => {
186
+ await openDb();
187
+ const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
188
+ const nativeDb = getBrainNativeDb()!;
189
+
190
+ type IndexRow = { name: string };
191
+ const indexes = nativeDb
192
+ .prepare(`PRAGMA index_list(brain_plasticity_events)`)
193
+ .all() as IndexRow[];
194
+ const names = indexes.map((r) => r.name);
195
+
196
+ expect(names).toContain('idx_plasticity_retrieval_log');
197
+ expect(names).toContain('idx_plasticity_reward');
198
+ });
199
+ });
200
+
201
+ // ===========================================================================
202
+ // M3: brain_page_edges — 6 new plasticity tracking columns
203
+ // ===========================================================================
204
+
205
+ describe('M3 — brain_page_edges plasticity columns', () => {
206
+ it('M3-1: all 6 new columns exist after migration', async () => {
207
+ await openDb();
208
+ const cols = await getTableColumns('brain_page_edges');
209
+
210
+ expect(cols.has('last_reinforced_at'), 'last_reinforced_at missing').toBe(true);
211
+ expect(cols.has('reinforcement_count'), 'reinforcement_count missing').toBe(true);
212
+ expect(cols.has('plasticity_class'), 'plasticity_class missing').toBe(true);
213
+ expect(cols.has('last_depressed_at'), 'last_depressed_at missing').toBe(true);
214
+ expect(cols.has('depression_count'), 'depression_count missing').toBe(true);
215
+ expect(cols.has('stability_score'), 'stability_score missing').toBe(true);
216
+
217
+ // Verify column types
218
+ expect(cols.get('last_reinforced_at')).toBe('TEXT');
219
+ expect(cols.get('reinforcement_count')).toBe('INTEGER');
220
+ expect(cols.get('plasticity_class')).toBe('TEXT');
221
+ expect(cols.get('last_depressed_at')).toBe('TEXT');
222
+ expect(cols.get('depression_count')).toBe('INTEGER');
223
+ expect(cols.get('stability_score')).toBe('REAL');
224
+ });
225
+
226
+ it('M3-2: INSERT with all new plasticity columns round-trips correctly', async () => {
227
+ await openDb();
228
+ const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
229
+ const nativeDb = getBrainNativeDb()!;
230
+
231
+ // First insert nodes (brain_page_edges requires from_id/to_id but NOT FK constrained)
232
+ const now = new Date().toISOString();
233
+ nativeDb
234
+ .prepare(
235
+ `INSERT INTO brain_page_edges
236
+ (from_id, to_id, edge_type, weight,
237
+ last_reinforced_at, reinforcement_count, plasticity_class,
238
+ last_depressed_at, depression_count, stability_score)
239
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
240
+ )
241
+ .run('obs:X', 'obs:Y', 'co_retrieved', 0.75, now, 5, 'stdp', null, 0, 0.62);
242
+
243
+ type EdgeRow = {
244
+ from_id: string;
245
+ to_id: string;
246
+ last_reinforced_at: string | null;
247
+ reinforcement_count: number;
248
+ plasticity_class: string;
249
+ last_depressed_at: string | null;
250
+ depression_count: number;
251
+ stability_score: number | null;
252
+ };
253
+
254
+ const row = nativeDb
255
+ .prepare(
256
+ `SELECT from_id, to_id, last_reinforced_at, reinforcement_count,
257
+ plasticity_class, last_depressed_at, depression_count, stability_score
258
+ FROM brain_page_edges WHERE from_id = 'obs:X' AND to_id = 'obs:Y'`,
259
+ )
260
+ .get() as EdgeRow;
261
+
262
+ expect(row).not.toBeNull();
263
+ expect(row.last_reinforced_at).toBe(now);
264
+ expect(row.reinforcement_count).toBe(5);
265
+ expect(row.plasticity_class).toBe('stdp');
266
+ expect(row.last_depressed_at).toBeNull();
267
+ expect(row.depression_count).toBe(0);
268
+ expect(row.stability_score).toBeCloseTo(0.62, 5);
269
+ });
270
+
271
+ it('M3-3: INSERT without new columns uses correct defaults (0, static)', async () => {
272
+ await openDb();
273
+ const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
274
+ const nativeDb = getBrainNativeDb()!;
275
+
276
+ // Old-style INSERT — no plasticity columns
277
+ nativeDb
278
+ .prepare(
279
+ `INSERT INTO brain_page_edges (from_id, to_id, edge_type, weight)
280
+ VALUES (?, ?, ?, ?)`,
281
+ )
282
+ .run('obs:M', 'obs:N', 'contains', 1.0);
283
+
284
+ type EdgeRow = {
285
+ reinforcement_count: number;
286
+ plasticity_class: string;
287
+ depression_count: number;
288
+ last_reinforced_at: string | null;
289
+ last_depressed_at: string | null;
290
+ stability_score: number | null;
291
+ };
292
+
293
+ const row = nativeDb
294
+ .prepare(
295
+ `SELECT reinforcement_count, plasticity_class, depression_count,
296
+ last_reinforced_at, last_depressed_at, stability_score
297
+ FROM brain_page_edges WHERE from_id = 'obs:M' AND to_id = 'obs:N'`,
298
+ )
299
+ .get() as EdgeRow;
300
+
301
+ expect(row).not.toBeNull();
302
+ expect(row.reinforcement_count).toBe(0);
303
+ expect(row.plasticity_class).toBe('static');
304
+ expect(row.depression_count).toBe(0);
305
+ expect(row.last_reinforced_at).toBeNull();
306
+ expect(row.last_depressed_at).toBeNull();
307
+ expect(row.stability_score).toBeNull();
308
+ });
309
+
310
+ it('M3-4: plasticity_class accepts all three valid enum values', async () => {
311
+ await openDb();
312
+ const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
313
+ const nativeDb = getBrainNativeDb()!;
314
+
315
+ const cases = [
316
+ ['obs:P1', 'obs:P2', 'static'],
317
+ ['obs:P3', 'obs:P4', 'hebbian'],
318
+ ['obs:P5', 'obs:P6', 'stdp'],
319
+ ] as const;
320
+
321
+ for (const [from, to, cls] of cases) {
322
+ expect(() =>
323
+ nativeDb
324
+ .prepare(
325
+ `INSERT INTO brain_page_edges (from_id, to_id, edge_type, weight, plasticity_class)
326
+ VALUES (?, ?, ?, ?, ?)`,
327
+ )
328
+ .run(from, to, 'co_retrieved', 0.5, cls),
329
+ ).not.toThrow();
330
+ }
331
+
332
+ type EdgeRow = { from_id: string; plasticity_class: string };
333
+ const rows = nativeDb
334
+ .prepare(
335
+ `SELECT from_id, plasticity_class FROM brain_page_edges
336
+ WHERE from_id IN ('obs:P1','obs:P3','obs:P5')
337
+ ORDER BY from_id`,
338
+ )
339
+ .all() as EdgeRow[];
340
+
341
+ expect(rows).toHaveLength(3);
342
+ expect(rows[0]!.plasticity_class).toBe('static');
343
+ expect(rows[1]!.plasticity_class).toBe('hebbian');
344
+ expect(rows[2]!.plasticity_class).toBe('stdp');
345
+ });
346
+
347
+ it('M3-5: co_retrieved edges are seeded as hebbian by migration', async () => {
348
+ // Insert a co_retrieved edge BEFORE opening the DB (so migration seeds it)
349
+ // We can't do this — migration runs at DB open. Instead, verify that
350
+ // inserting a co_retrieved edge and then re-opening picks up 'static' default,
351
+ // but the ensureColumns guard seeds existing ones.
352
+ //
353
+ // Since we can't pre-populate before migrations, this test verifies that
354
+ // after opening, a manually-inserted co_retrieved edge can be updated
355
+ // to 'hebbian' as the seed would — and that the column exists with correct default.
356
+ await openDb();
357
+ const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
358
+ const nativeDb = getBrainNativeDb()!;
359
+
360
+ // Insert co_retrieved edge (gets default 'static')
361
+ nativeDb
362
+ .prepare(
363
+ `INSERT INTO brain_page_edges (from_id, to_id, edge_type, weight)
364
+ VALUES (?, ?, ?, ?)`,
365
+ )
366
+ .run('obs:Q1', 'obs:Q2', 'co_retrieved', 0.3);
367
+
368
+ type EdgeRow = { plasticity_class: string };
369
+
370
+ // Verify default is 'static' on fresh insert
371
+ const before = nativeDb
372
+ .prepare(
373
+ `SELECT plasticity_class FROM brain_page_edges
374
+ WHERE from_id = 'obs:Q1' AND to_id = 'obs:Q2'`,
375
+ )
376
+ .get() as EdgeRow;
377
+ expect(before.plasticity_class).toBe('static');
378
+
379
+ // Apply seed logic (the ensureColumns guard does this for pre-existing rows)
380
+ nativeDb
381
+ .prepare(
382
+ `UPDATE brain_page_edges SET plasticity_class = 'hebbian'
383
+ WHERE edge_type = 'co_retrieved' AND plasticity_class = 'static'`,
384
+ )
385
+ .run();
386
+
387
+ const after = nativeDb
388
+ .prepare(
389
+ `SELECT plasticity_class FROM brain_page_edges
390
+ WHERE from_id = 'obs:Q1' AND to_id = 'obs:Q2'`,
391
+ )
392
+ .get() as EdgeRow;
393
+ expect(after.plasticity_class).toBe('hebbian');
394
+ });
395
+
396
+ it('M3-6: indexes on new plasticity columns exist', async () => {
397
+ await openDb();
398
+ const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
399
+ const nativeDb = getBrainNativeDb()!;
400
+
401
+ type IndexRow = { name: string };
402
+ const indexes = nativeDb.prepare(`PRAGMA index_list(brain_page_edges)`).all() as IndexRow[];
403
+ const names = indexes.map((r) => r.name);
404
+
405
+ expect(names).toContain('idx_brain_edges_last_reinforced');
406
+ expect(names).toContain('idx_brain_edges_plasticity_class');
407
+ expect(names).toContain('idx_brain_edges_stability');
408
+ });
409
+
410
+ it('M3-7: total column count is 12 (6 original + 6 new)', async () => {
411
+ await openDb();
412
+ const cols = await getTableColumns('brain_page_edges');
413
+ // Original 6: from_id, to_id, edge_type, weight, provenance, created_at
414
+ // New 6: last_reinforced_at, reinforcement_count, plasticity_class,
415
+ // last_depressed_at, depression_count, stability_score
416
+ expect(cols.size).toBe(12);
417
+ });
418
+ });