@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.
- package/dist/index.js +445 -9
- package/dist/index.js.map +4 -4
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +448 -9
- package/dist/internal.js.map +4 -4
- package/dist/memory/brain-lifecycle.d.ts +7 -0
- package/dist/memory/brain-lifecycle.d.ts.map +1 -1
- package/dist/memory/brain-stdp.d.ts +122 -0
- package/dist/memory/brain-stdp.d.ts.map +1 -0
- package/dist/memory/decision-cross-link.d.ts +70 -0
- package/dist/memory/decision-cross-link.d.ts.map +1 -0
- package/dist/memory/decisions.d.ts.map +1 -1
- package/dist/memory/edge-types.d.ts +24 -0
- package/dist/memory/edge-types.d.ts.map +1 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/store/brain-schema.d.ts +134 -3
- package/dist/store/brain-schema.d.ts.map +1 -1
- package/dist/store/brain-sqlite.d.ts.map +1 -1
- package/dist/store/validation-schemas.d.ts +1 -0
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/migrations/drizzle-brain/20260415000001_t626-normalize-co-retrieved-edge-type/migration.sql +14 -0
- package/package.json +8 -8
- package/src/internal.ts +7 -0
- package/src/memory/__tests__/brain-stdp.test.ts +452 -0
- package/src/memory/__tests__/decision-cross-link.test.ts +240 -0
- package/src/memory/brain-lifecycle.ts +23 -4
- package/src/memory/brain-stdp.ts +448 -0
- package/src/memory/decision-cross-link.ts +276 -0
- package/src/memory/decisions.ts +7 -0
- package/src/memory/edge-types.ts +31 -0
- package/src/memory/index.ts +2 -0
- package/src/store/brain-schema.ts +50 -0
- 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.
|
|
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.
|
|
67
|
-
"@cleocode/agents": "2026.4.
|
|
68
|
-
"@cleocode/caamp": "2026.4.
|
|
69
|
-
"@cleocode/contracts": "2026.4.
|
|
70
|
-
"@cleocode/lafs": "2026.4.
|
|
71
|
-
"@cleocode/
|
|
72
|
-
"@cleocode/
|
|
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
|
+
});
|