@cleocode/core 2026.4.49 → 2026.4.51

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 (35) hide show
  1. package/dist/index.js +445 -9
  2. package/dist/index.js.map +4 -4
  3. package/dist/internal.d.ts +2 -0
  4. package/dist/internal.d.ts.map +1 -1
  5. package/dist/internal.js +448 -9
  6. package/dist/internal.js.map +4 -4
  7. package/dist/memory/brain-lifecycle.d.ts +7 -0
  8. package/dist/memory/brain-lifecycle.d.ts.map +1 -1
  9. package/dist/memory/brain-stdp.d.ts +122 -0
  10. package/dist/memory/brain-stdp.d.ts.map +1 -0
  11. package/dist/memory/decision-cross-link.d.ts +70 -0
  12. package/dist/memory/decision-cross-link.d.ts.map +1 -0
  13. package/dist/memory/decisions.d.ts.map +1 -1
  14. package/dist/memory/edge-types.d.ts +24 -0
  15. package/dist/memory/edge-types.d.ts.map +1 -0
  16. package/dist/memory/index.d.ts +1 -0
  17. package/dist/memory/index.d.ts.map +1 -1
  18. package/dist/store/brain-schema.d.ts +134 -3
  19. package/dist/store/brain-schema.d.ts.map +1 -1
  20. package/dist/store/brain-sqlite.d.ts.map +1 -1
  21. package/dist/store/validation-schemas.d.ts +1 -0
  22. package/dist/store/validation-schemas.d.ts.map +1 -1
  23. package/migrations/drizzle-brain/20260415000001_t626-normalize-co-retrieved-edge-type/migration.sql +14 -0
  24. package/package.json +8 -8
  25. package/src/internal.ts +7 -0
  26. package/src/memory/__tests__/brain-stdp.test.ts +452 -0
  27. package/src/memory/__tests__/decision-cross-link.test.ts +240 -0
  28. package/src/memory/brain-lifecycle.ts +23 -4
  29. package/src/memory/brain-stdp.ts +448 -0
  30. package/src/memory/decision-cross-link.ts +276 -0
  31. package/src/memory/decisions.ts +7 -0
  32. package/src/memory/edge-types.ts +31 -0
  33. package/src/memory/index.ts +2 -0
  34. package/src/store/brain-schema.ts +50 -0
  35. package/src/store/brain-sqlite.ts +17 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/core",
3
- "version": "2026.4.49",
3
+ "version": "2026.4.51",
4
4
  "description": "CLEO core business logic kernel — tasks, sessions, memory, orchestration, lifecycle, with bundled SQLite store",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -63,13 +63,13 @@
63
63
  "write-file-atomic": "^7.0.1",
64
64
  "yaml": "^2.8.3",
65
65
  "zod": "^4.3.6",
66
- "@cleocode/adapters": "2026.4.49",
67
- "@cleocode/agents": "2026.4.49",
68
- "@cleocode/caamp": "2026.4.49",
69
- "@cleocode/contracts": "2026.4.49",
70
- "@cleocode/lafs": "2026.4.49",
71
- "@cleocode/nexus": "2026.4.49",
72
- "@cleocode/skills": "2026.4.49"
66
+ "@cleocode/adapters": "2026.4.51",
67
+ "@cleocode/agents": "2026.4.51",
68
+ "@cleocode/caamp": "2026.4.51",
69
+ "@cleocode/contracts": "2026.4.51",
70
+ "@cleocode/lafs": "2026.4.51",
71
+ "@cleocode/skills": "2026.4.51",
72
+ "@cleocode/nexus": "2026.4.51"
73
73
  },
74
74
  "engines": {
75
75
  "node": ">=24.0.0"
package/src/internal.ts CHANGED
@@ -206,6 +206,13 @@ export type {
206
206
  PopulateEmbeddingsResult,
207
207
  } from './memory/brain-retrieval.js';
208
208
  export { populateEmbeddings, retrieveWithBudget } from './memory/brain-retrieval.js';
209
+ // Memory — STDP plasticity (T626 phase 5)
210
+ export type {
211
+ PlasticityStatsSummary,
212
+ RecentPlasticityEvent,
213
+ StdpPlasticityResult,
214
+ } from './memory/brain-stdp.js';
215
+ export { applyStdpPlasticity, getPlasticityStats } from './memory/brain-stdp.js';
209
216
  export { migrateClaudeMem } from './memory/claude-mem-migration.js';
210
217
  // Memory — engine-compat
211
218
  export {
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Tests for STDP (Spike-Timing-Dependent Plasticity) in brain-stdp.ts.
3
+ *
4
+ * Verifies:
5
+ * - LTP (potentiation): A retrieved before B → edge A→B strengthened
6
+ * - LTD (depression): existing reverse edge B→A weakened when A fires first
7
+ * - Exponential decay: pairs farther apart in time get smaller Δw
8
+ * - Window cutoff: pairs beyond sessionWindowMs are ignored
9
+ * - Weight clamping: edges never exceed [0, 1]
10
+ * - New edge insertion: LTP creates an edge when none exists
11
+ * - LTD never creates edges: depression only weakens existing ones
12
+ * - Stats query: getPlasticityStats returns correct aggregates
13
+ *
14
+ * @task T626
15
+ * @epic T626
16
+ */
17
+
18
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
19
+
20
+ // ============================================================================
21
+ // Hoisted mock factories
22
+ // ============================================================================
23
+
24
+ const { mockGetBrainDb, mockGetBrainNativeDb } = vi.hoisted(() => ({
25
+ mockGetBrainDb: vi.fn().mockResolvedValue({}),
26
+ mockGetBrainNativeDb: vi.fn(),
27
+ }));
28
+
29
+ vi.mock('../../store/brain-sqlite.js', () => ({
30
+ getBrainDb: mockGetBrainDb,
31
+ getBrainNativeDb: mockGetBrainNativeDb,
32
+ }));
33
+
34
+ // ============================================================================
35
+ // Import module under test (after mocks)
36
+ // ============================================================================
37
+
38
+ import { applyStdpPlasticity, getPlasticityStats } from '../brain-stdp.js';
39
+
40
+ // ============================================================================
41
+ // Helpers
42
+ // ============================================================================
43
+
44
+ const PROJECT_ROOT = '/fake/project';
45
+
46
+ /** Build an ISO-like datetime string `msAgo` milliseconds before now. */
47
+ function msAgo(ms: number): string {
48
+ return new Date(Date.now() - ms).toISOString().replace('T', ' ').slice(0, 19);
49
+ }
50
+
51
+ type PrepStmt = {
52
+ run: ReturnType<typeof vi.fn>;
53
+ get: ReturnType<typeof vi.fn>;
54
+ all?: ReturnType<typeof vi.fn>;
55
+ };
56
+
57
+ /** Create a minimal prepared-statement stub. */
58
+ function makeStmt(runResult?: unknown, getResult?: unknown): PrepStmt {
59
+ return {
60
+ run: vi.fn().mockReturnValue(runResult ?? { changes: 0 }),
61
+ get: vi.fn().mockReturnValue(getResult),
62
+ };
63
+ }
64
+
65
+ // ============================================================================
66
+ // Tests: applyStdpPlasticity
67
+ // ============================================================================
68
+
69
+ describe('applyStdpPlasticity', () => {
70
+ let mockNativeDb: {
71
+ prepare: ReturnType<typeof vi.fn>;
72
+ stmts: Map<string, PrepStmt>;
73
+ };
74
+
75
+ /**
76
+ * Build a minimal mock nativeDb.
77
+ * `stmtOverrides` maps substring-of-SQL → statement stub.
78
+ */
79
+ function buildNativeDb(
80
+ logRows: Array<{
81
+ id: number;
82
+ entry_ids: string;
83
+ created_at: string;
84
+ retrieval_order: number | null;
85
+ delta_ms: number | null;
86
+ }>,
87
+ edgeMap: Map<string, number | undefined> = new Map(),
88
+ ): ReturnType<typeof buildNativeDb> {
89
+ const stmts = new Map<string, PrepStmt>();
90
+
91
+ const db = {
92
+ stmts,
93
+ prepare: vi.fn((sql: string) => {
94
+ // Guard: plasticity events table existence check
95
+ if (sql.includes('brain_plasticity_events') && sql.includes('SELECT 1')) {
96
+ return makeStmt(undefined, {});
97
+ }
98
+ // Guard: retrieval log existence check
99
+ if (sql.includes('brain_retrieval_log') && sql.includes('SELECT 1')) {
100
+ return makeStmt(undefined, {});
101
+ }
102
+ // Retrieval log query
103
+ if (sql.includes('FROM brain_retrieval_log') && sql.includes('ORDER BY')) {
104
+ const stmt = {
105
+ run: vi.fn(),
106
+ get: vi.fn(),
107
+ all: vi.fn().mockReturnValue(logRows),
108
+ };
109
+ return stmt;
110
+ }
111
+ // Get existing edge (SELECT weight FROM brain_page_edges)
112
+ if (sql.includes('SELECT weight') && sql.includes('edge_type')) {
113
+ return makeStmt(undefined, undefined); // always no existing edge by default
114
+ }
115
+ // Update edge weight
116
+ if (sql.includes('UPDATE brain_page_edges')) {
117
+ return makeStmt({ changes: 1 });
118
+ }
119
+ // Insert new edge
120
+ if (sql.includes('INSERT OR IGNORE INTO brain_page_edges')) {
121
+ return makeStmt({ changes: 1 });
122
+ }
123
+ // Log plasticity event
124
+ if (sql.includes('INSERT INTO brain_plasticity_events')) {
125
+ return makeStmt({ changes: 1 });
126
+ }
127
+ // Default
128
+ return makeStmt(undefined, undefined);
129
+ }),
130
+ };
131
+
132
+ // Replace the SELECT weight stub with one that uses the edgeMap
133
+ const originalPrepare = db.prepare;
134
+ db.prepare = vi.fn((sql: string) => {
135
+ if (sql.includes('SELECT weight') && sql.includes('edge_type')) {
136
+ return {
137
+ run: vi.fn(),
138
+ get: vi.fn((fromId: string, toId: string) => {
139
+ const key = `${fromId}|${toId}`;
140
+ const w = edgeMap.get(key);
141
+ return w !== undefined ? { weight: w } : undefined;
142
+ }),
143
+ };
144
+ }
145
+ return originalPrepare(sql);
146
+ });
147
+
148
+ return db;
149
+ }
150
+
151
+ beforeEach(() => {
152
+ mockGetBrainDb.mockResolvedValue({});
153
+ });
154
+
155
+ afterEach(() => {
156
+ vi.clearAllMocks();
157
+ });
158
+
159
+ it('returns zero counts when nativeDb is null', async () => {
160
+ mockGetBrainNativeDb.mockReturnValue(null);
161
+ const result = await applyStdpPlasticity(PROJECT_ROOT);
162
+ expect(result.ltpEvents).toBe(0);
163
+ expect(result.ltdEvents).toBe(0);
164
+ expect(result.edgesCreated).toBe(0);
165
+ expect(result.pairsExamined).toBe(0);
166
+ });
167
+
168
+ it('returns zero counts when retrieval_log table is absent', async () => {
169
+ const db = {
170
+ prepare: vi.fn().mockReturnValue({
171
+ run: vi.fn(),
172
+ get: vi.fn().mockImplementation(() => {
173
+ throw new Error('no such table');
174
+ }),
175
+ all: vi.fn().mockReturnValue([]),
176
+ }),
177
+ };
178
+ mockGetBrainNativeDb.mockReturnValue(db);
179
+ const result = await applyStdpPlasticity(PROJECT_ROOT);
180
+ expect(result.ltpEvents).toBe(0);
181
+ });
182
+
183
+ it('returns zero counts when no retrieval log rows exist', async () => {
184
+ const db = buildNativeDb([], new Map());
185
+ mockGetBrainNativeDb.mockReturnValue(db);
186
+ const result = await applyStdpPlasticity(PROJECT_ROOT);
187
+ expect(result.pairsExamined).toBe(0);
188
+ expect(result.ltpEvents).toBe(0);
189
+ });
190
+
191
+ it('applies LTP when A retrieved before B within window', async () => {
192
+ const window = 5 * 60 * 1000; // 5 min default
193
+ const now = Date.now();
194
+
195
+ // A retrieved 10 s ago, B retrieved 5 s ago → A before B by 5 s
196
+ const rows = [
197
+ {
198
+ id: 1,
199
+ entry_ids: JSON.stringify(['obs-A']),
200
+ created_at: new Date(now - 10_000).toISOString().replace('T', ' ').slice(0, 19),
201
+ retrieval_order: 0,
202
+ delta_ms: null,
203
+ },
204
+ {
205
+ id: 2,
206
+ entry_ids: JSON.stringify(['obs-B']),
207
+ created_at: new Date(now - 5_000).toISOString().replace('T', ' ').slice(0, 19),
208
+ retrieval_order: 1,
209
+ delta_ms: 5000,
210
+ },
211
+ ];
212
+
213
+ const db = buildNativeDb(rows, new Map()); // no existing edges
214
+ mockGetBrainNativeDb.mockReturnValue(db);
215
+
216
+ const result = await applyStdpPlasticity(PROJECT_ROOT, window);
217
+
218
+ expect(result.ltpEvents).toBeGreaterThanOrEqual(1);
219
+ expect(result.edgesCreated).toBeGreaterThanOrEqual(1);
220
+ expect(result.pairsExamined).toBeGreaterThanOrEqual(1);
221
+ });
222
+
223
+ it('applies LTD on reverse edge when reverse edge already exists', async () => {
224
+ const window = 5 * 60 * 1000;
225
+ const now = Date.now();
226
+
227
+ const rows = [
228
+ {
229
+ id: 1,
230
+ entry_ids: JSON.stringify(['obs-X']),
231
+ created_at: new Date(now - 10_000).toISOString().replace('T', ' ').slice(0, 19),
232
+ retrieval_order: 0,
233
+ delta_ms: null,
234
+ },
235
+ {
236
+ id: 2,
237
+ entry_ids: JSON.stringify(['obs-Y']),
238
+ created_at: new Date(now - 5_000).toISOString().replace('T', ' ').slice(0, 19),
239
+ retrieval_order: 1,
240
+ delta_ms: 5000,
241
+ },
242
+ ];
243
+
244
+ // Pre-seed reverse edge observation:obs-Y → observation:obs-X
245
+ const edgeMap = new Map<string, number>([['observation:obs-Y|observation:obs-X', 0.6]]);
246
+
247
+ const db = buildNativeDb(rows, edgeMap);
248
+ mockGetBrainNativeDb.mockReturnValue(db);
249
+
250
+ const result = await applyStdpPlasticity(PROJECT_ROOT, window);
251
+
252
+ expect(result.ltdEvents).toBeGreaterThanOrEqual(1);
253
+ });
254
+
255
+ it('skips pairs beyond sessionWindowMs', async () => {
256
+ const shortWindow = 1_000; // 1 second window
257
+ const now = Date.now();
258
+
259
+ // A and B retrieved 10 s apart — beyond 1 s window
260
+ const rows = [
261
+ {
262
+ id: 1,
263
+ entry_ids: JSON.stringify(['obs-P']),
264
+ created_at: new Date(now - 15_000).toISOString().replace('T', ' ').slice(0, 19),
265
+ retrieval_order: 0,
266
+ delta_ms: null,
267
+ },
268
+ {
269
+ id: 2,
270
+ entry_ids: JSON.stringify(['obs-Q']),
271
+ created_at: new Date(now - 5_000).toISOString().replace('T', ' ').slice(0, 19),
272
+ retrieval_order: 1,
273
+ delta_ms: 10_000,
274
+ },
275
+ ];
276
+
277
+ const db = buildNativeDb(rows, new Map());
278
+ mockGetBrainNativeDb.mockReturnValue(db);
279
+
280
+ const result = await applyStdpPlasticity(PROJECT_ROOT, shortWindow);
281
+
282
+ // Both rows are within cutoff (15 s < 1 h), but spike pair Δt = 10 s > 1 s window
283
+ expect(result.ltpEvents).toBe(0);
284
+ expect(result.ltdEvents).toBe(0);
285
+ });
286
+
287
+ it('does not apply LTD when no reverse edge exists', async () => {
288
+ const window = 5 * 60 * 1000;
289
+ const now = Date.now();
290
+
291
+ const rows = [
292
+ {
293
+ id: 1,
294
+ entry_ids: JSON.stringify(['obs-M']),
295
+ created_at: new Date(now - 10_000).toISOString().replace('T', ' ').slice(0, 19),
296
+ retrieval_order: 0,
297
+ delta_ms: null,
298
+ },
299
+ {
300
+ id: 2,
301
+ entry_ids: JSON.stringify(['obs-N']),
302
+ created_at: new Date(now - 5_000).toISOString().replace('T', ' ').slice(0, 19),
303
+ retrieval_order: 1,
304
+ delta_ms: 5000,
305
+ },
306
+ ];
307
+
308
+ // No existing edges at all
309
+ const db = buildNativeDb(rows, new Map());
310
+ mockGetBrainNativeDb.mockReturnValue(db);
311
+
312
+ const result = await applyStdpPlasticity(PROJECT_ROOT, window);
313
+
314
+ // LTP fires (new edge created), but LTD does NOT fire (no reverse edge)
315
+ expect(result.ltdEvents).toBe(0);
316
+ expect(result.ltpEvents).toBeGreaterThanOrEqual(1);
317
+ });
318
+
319
+ it('larger Δt produces smaller Δw (exponential decay)', async () => {
320
+ // We verify the formula directly without a real DB:
321
+ // Δw = A_PRE * exp(-Δt / TAU_PRE_MS)
322
+ // For Δt=1000 ms and Δt=10000 ms:
323
+ const A_PRE = 0.05;
324
+ const TAU = 20_000;
325
+ const dw1 = A_PRE * Math.exp(-1_000 / TAU);
326
+ const dw2 = A_PRE * Math.exp(-10_000 / TAU);
327
+ expect(dw1).toBeGreaterThan(dw2);
328
+ expect(dw2).toBeGreaterThan(0);
329
+ });
330
+
331
+ it('skips self-pairs (same entry ID)', async () => {
332
+ const window = 5 * 60 * 1000;
333
+ const now = Date.now();
334
+
335
+ // Two log rows each returning the same entry ID
336
+ const rows = [
337
+ {
338
+ id: 1,
339
+ entry_ids: JSON.stringify(['obs-SAME']),
340
+ created_at: new Date(now - 10_000).toISOString().replace('T', ' ').slice(0, 19),
341
+ retrieval_order: 0,
342
+ delta_ms: null,
343
+ },
344
+ {
345
+ id: 2,
346
+ entry_ids: JSON.stringify(['obs-SAME']),
347
+ created_at: new Date(now - 5_000).toISOString().replace('T', ' ').slice(0, 19),
348
+ retrieval_order: 1,
349
+ delta_ms: 5000,
350
+ },
351
+ ];
352
+
353
+ const db = buildNativeDb(rows, new Map());
354
+ mockGetBrainNativeDb.mockReturnValue(db);
355
+
356
+ const result = await applyStdpPlasticity(PROJECT_ROOT, window);
357
+
358
+ // pairsExamined may increment for the pair, but LTP must be 0 (self-pair skipped)
359
+ expect(result.ltpEvents).toBe(0);
360
+ });
361
+ });
362
+
363
+ // ============================================================================
364
+ // Tests: getPlasticityStats
365
+ // ============================================================================
366
+
367
+ describe('getPlasticityStats', () => {
368
+ afterEach(() => {
369
+ vi.clearAllMocks();
370
+ });
371
+
372
+ it('returns empty stats when nativeDb is null', async () => {
373
+ mockGetBrainDb.mockResolvedValue({});
374
+ mockGetBrainNativeDb.mockReturnValue(null);
375
+ const stats = await getPlasticityStats(PROJECT_ROOT);
376
+ expect(stats.totalEvents).toBe(0);
377
+ expect(stats.ltpCount).toBe(0);
378
+ expect(stats.ltdCount).toBe(0);
379
+ expect(stats.recentEvents).toHaveLength(0);
380
+ });
381
+
382
+ it('returns empty stats when plasticity_events table is absent', async () => {
383
+ mockGetBrainDb.mockResolvedValue({});
384
+ const db = {
385
+ prepare: vi.fn().mockReturnValue({
386
+ run: vi.fn(),
387
+ get: vi.fn().mockImplementation(() => {
388
+ throw new Error('no such table: brain_plasticity_events');
389
+ }),
390
+ all: vi.fn().mockReturnValue([]),
391
+ }),
392
+ };
393
+ mockGetBrainNativeDb.mockReturnValue(db);
394
+ const stats = await getPlasticityStats(PROJECT_ROOT);
395
+ expect(stats.totalEvents).toBe(0);
396
+ });
397
+
398
+ it('returns correct aggregates when events exist', async () => {
399
+ mockGetBrainDb.mockResolvedValue({});
400
+
401
+ const aggRow = {
402
+ total: 5,
403
+ ltp_count: 3,
404
+ ltd_count: 2,
405
+ net_delta_w: 0.12,
406
+ last_event_at: '2026-04-14 10:00:00',
407
+ };
408
+
409
+ const recentRows = [
410
+ {
411
+ id: 5,
412
+ source_node: 'observation:obs-A',
413
+ target_node: 'observation:obs-B',
414
+ delta_w: 0.04,
415
+ kind: 'ltp',
416
+ timestamp: '2026-04-14 10:00:00',
417
+ session_id: null,
418
+ },
419
+ ];
420
+
421
+ const db = {
422
+ prepare: vi.fn((sql: string) => {
423
+ if (sql.includes('SELECT 1') && sql.includes('brain_plasticity_events')) {
424
+ return { run: vi.fn(), get: vi.fn().mockReturnValue({}) };
425
+ }
426
+ if (sql.includes('COUNT(*)')) {
427
+ return { run: vi.fn(), get: vi.fn().mockReturnValue(aggRow) };
428
+ }
429
+ if (sql.includes('ORDER BY timestamp DESC')) {
430
+ return {
431
+ run: vi.fn(),
432
+ get: vi.fn(),
433
+ all: vi.fn().mockReturnValue(recentRows),
434
+ };
435
+ }
436
+ return { run: vi.fn(), get: vi.fn().mockReturnValue(undefined) };
437
+ }),
438
+ };
439
+ mockGetBrainNativeDb.mockReturnValue(db);
440
+
441
+ const stats = await getPlasticityStats(PROJECT_ROOT, 10);
442
+
443
+ expect(stats.totalEvents).toBe(5);
444
+ expect(stats.ltpCount).toBe(3);
445
+ expect(stats.ltdCount).toBe(2);
446
+ expect(stats.netDeltaW).toBeCloseTo(0.12);
447
+ expect(stats.lastEventAt).toBe('2026-04-14 10:00:00');
448
+ expect(stats.recentEvents).toHaveLength(1);
449
+ expect(stats.recentEvents[0]!.kind).toBe('ltp');
450
+ expect(stats.recentEvents[0]!.deltaW).toBeCloseTo(0.04);
451
+ });
452
+ });