@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.
- package/dist/index.js +488 -11
- package/dist/index.js.map +3 -3
- package/dist/internal.js +497 -13
- package/dist/internal.js.map +3 -3
- package/dist/memory/graph-queries.d.ts.map +1 -1
- package/dist/store/agent-registry-accessor.d.ts +1 -1
- package/dist/store/brain-accessor.d.ts +57 -1
- package/dist/store/brain-accessor.d.ts.map +1 -1
- package/dist/store/brain-schema.d.ts +738 -0
- package/dist/store/brain-schema.d.ts.map +1 -1
- package/dist/store/brain-sqlite.d.ts.map +1 -1
- package/dist/store/nexus-schema.d.ts +1 -1
- package/dist/system/health.d.ts.map +1 -1
- package/dist/validation/doctor/checks.d.ts +7 -0
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/migrations/drizzle-brain/20260416000001_t673-retrieval-log-plasticity-columns/migration.sql +57 -0
- package/migrations/drizzle-brain/20260416000002_t673-plasticity-events-expand/migration.sql +44 -0
- package/migrations/drizzle-brain/20260416000003_t673-page-edges-plasticity-columns/migration.sql +44 -0
- package/migrations/drizzle-brain/20260416000004_t673-new-plasticity-tables/migration.sql +73 -0
- package/package.json +8 -8
- package/src/memory/__tests__/brain-retrieval-m1.test.ts +250 -0
- package/src/memory/__tests__/brain-schema-m2-m3.test.ts +418 -0
- package/src/memory/__tests__/brain-schema-m4.test.ts +494 -0
- package/src/memory/brain-retrieval.ts +1 -1
- package/src/memory/graph-queries.ts +14 -0
- package/src/store/agent-registry-accessor.ts +1 -1
- package/src/store/brain-accessor.ts +120 -0
- package/src/store/brain-schema.ts +373 -1
- package/src/store/brain-sqlite.ts +123 -0
- package/src/system/health.ts +4 -1
- package/src/validation/doctor/checks.ts +107 -0
- package/src/validation/protocols/protocols-markdown/research.md +1 -1
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for STDP M4 plasticity auxiliary tables.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the three new tables from T673-M4 are created correctly
|
|
5
|
+
* and behave as specified in docs/specs/stdp-wire-up-spec.md §2.1.4–§2.1.6.
|
|
6
|
+
*
|
|
7
|
+
* Tests use a real SQLite database (no mocks). Each test gets an isolated
|
|
8
|
+
* temp directory so there are no cross-test state leaks.
|
|
9
|
+
*
|
|
10
|
+
* Tables under test:
|
|
11
|
+
* - brain_weight_history (T697 — owner Q4 mandate)
|
|
12
|
+
* - brain_modulators (T699 — R-STDP reward signal event log)
|
|
13
|
+
* - brain_consolidation_events (T701 — pipeline run audit log)
|
|
14
|
+
*
|
|
15
|
+
* @task T697
|
|
16
|
+
* @task T699
|
|
17
|
+
* @task T701
|
|
18
|
+
* @epic T673
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
25
|
+
|
|
26
|
+
let tempDir: string;
|
|
27
|
+
|
|
28
|
+
describe('Brain Schema M4 — plasticity aux tables (real SQLite, no mocks)', () => {
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
tempDir = await mkdtemp(join(tmpdir(), 'cleo-brain-m4-'));
|
|
31
|
+
process.env['CLEO_DIR'] = join(tempDir, '.cleo');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
const { closeBrainDb } = await import('../../store/brain-sqlite.js');
|
|
36
|
+
closeBrainDb();
|
|
37
|
+
delete process.env['CLEO_DIR'];
|
|
38
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// =========================================================================
|
|
42
|
+
// brain_weight_history (T697)
|
|
43
|
+
// =========================================================================
|
|
44
|
+
|
|
45
|
+
describe('brain_weight_history', () => {
|
|
46
|
+
it('T697-1: table exists after DB initialisation', async () => {
|
|
47
|
+
const { getBrainDb } = await import('../../store/brain-sqlite.js');
|
|
48
|
+
const { closeBrainDb } = await import('../../store/brain-sqlite.js');
|
|
49
|
+
closeBrainDb();
|
|
50
|
+
await getBrainDb(tempDir);
|
|
51
|
+
|
|
52
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
53
|
+
const nativeDb = getBrainNativeDb();
|
|
54
|
+
const row = nativeDb
|
|
55
|
+
?.prepare(
|
|
56
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name='brain_weight_history'`,
|
|
57
|
+
)
|
|
58
|
+
.get() as { name: string } | undefined;
|
|
59
|
+
expect(row?.name).toBe('brain_weight_history');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('T697-2: insert and select back all required columns', async () => {
|
|
63
|
+
const { insertWeightHistoryRow } = await import('../../store/brain-accessor.js');
|
|
64
|
+
|
|
65
|
+
const inserted = await insertWeightHistoryRow(tempDir, {
|
|
66
|
+
edgeFromId: 'observation:obs-A',
|
|
67
|
+
edgeToId: 'observation:obs-B',
|
|
68
|
+
edgeType: 'co_retrieved',
|
|
69
|
+
weightBefore: 0.5,
|
|
70
|
+
weightAfter: 0.55,
|
|
71
|
+
deltaWeight: 0.05,
|
|
72
|
+
eventKind: 'ltp',
|
|
73
|
+
sourcePlasticityEventId: null,
|
|
74
|
+
retrievalLogId: null,
|
|
75
|
+
rewardSignal: null,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(inserted.id).toBeGreaterThan(0);
|
|
79
|
+
expect(inserted.edgeFromId).toBe('observation:obs-A');
|
|
80
|
+
expect(inserted.edgeToId).toBe('observation:obs-B');
|
|
81
|
+
expect(inserted.edgeType).toBe('co_retrieved');
|
|
82
|
+
expect(inserted.weightBefore).toBe(0.5);
|
|
83
|
+
expect(inserted.weightAfter).toBe(0.55);
|
|
84
|
+
expect(inserted.deltaWeight).toBe(0.05);
|
|
85
|
+
expect(inserted.eventKind).toBe('ltp');
|
|
86
|
+
expect(inserted.changedAt).toBeTruthy();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('T697-3: default changedAt is populated by SQLite', async () => {
|
|
90
|
+
const { insertWeightHistoryRow } = await import('../../store/brain-accessor.js');
|
|
91
|
+
|
|
92
|
+
const inserted = await insertWeightHistoryRow(tempDir, {
|
|
93
|
+
edgeFromId: 'observation:A',
|
|
94
|
+
edgeToId: 'observation:B',
|
|
95
|
+
edgeType: 'co_retrieved',
|
|
96
|
+
weightBefore: null,
|
|
97
|
+
weightAfter: 0.075,
|
|
98
|
+
deltaWeight: 0.075,
|
|
99
|
+
eventKind: 'hebbian',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// changedAt should look like a datetime string, not null/undefined
|
|
103
|
+
expect(typeof inserted.changedAt).toBe('string');
|
|
104
|
+
expect(inserted.changedAt.length).toBeGreaterThan(10);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('T697-4: weightBefore is nullable (new edge INSERT path)', async () => {
|
|
108
|
+
const { insertWeightHistoryRow } = await import('../../store/brain-accessor.js');
|
|
109
|
+
|
|
110
|
+
const inserted = await insertWeightHistoryRow(tempDir, {
|
|
111
|
+
edgeFromId: 'observation:novel-A',
|
|
112
|
+
edgeToId: 'observation:novel-B',
|
|
113
|
+
edgeType: 'co_retrieved',
|
|
114
|
+
weightBefore: null,
|
|
115
|
+
weightAfter: 0.075,
|
|
116
|
+
deltaWeight: 0.075,
|
|
117
|
+
eventKind: 'ltp',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(inserted.weightBefore).toBeNull();
|
|
121
|
+
expect(inserted.weightAfter).toBe(0.075);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('T697-5: all six event_kind values are accepted', async () => {
|
|
125
|
+
const { insertWeightHistoryRow } = await import('../../store/brain-accessor.js');
|
|
126
|
+
const kinds = ['ltp', 'ltd', 'hebbian', 'decay', 'prune', 'external'] as const;
|
|
127
|
+
|
|
128
|
+
for (const kind of kinds) {
|
|
129
|
+
const row = await insertWeightHistoryRow(tempDir, {
|
|
130
|
+
edgeFromId: `obs:from-${kind}`,
|
|
131
|
+
edgeToId: `obs:to-${kind}`,
|
|
132
|
+
edgeType: 'co_retrieved',
|
|
133
|
+
weightBefore: 0.5,
|
|
134
|
+
weightAfter: 0.45,
|
|
135
|
+
deltaWeight: -0.05,
|
|
136
|
+
eventKind: kind,
|
|
137
|
+
});
|
|
138
|
+
expect(row.eventKind).toBe(kind);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('T697-6: reward_signal and FK columns are nullable', async () => {
|
|
143
|
+
const { insertWeightHistoryRow } = await import('../../store/brain-accessor.js');
|
|
144
|
+
|
|
145
|
+
const inserted = await insertWeightHistoryRow(tempDir, {
|
|
146
|
+
edgeFromId: 'obs:X',
|
|
147
|
+
edgeToId: 'obs:Y',
|
|
148
|
+
edgeType: 'co_retrieved',
|
|
149
|
+
weightBefore: 0.2,
|
|
150
|
+
weightAfter: 0.3,
|
|
151
|
+
deltaWeight: 0.1,
|
|
152
|
+
eventKind: 'ltp',
|
|
153
|
+
sourcePlasticityEventId: 42,
|
|
154
|
+
retrievalLogId: 7,
|
|
155
|
+
rewardSignal: 1.0,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(inserted.sourcePlasticityEventId).toBe(42);
|
|
159
|
+
expect(inserted.retrievalLogId).toBe(7);
|
|
160
|
+
expect(inserted.rewardSignal).toBe(1.0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('T697-7: all six expected indexes exist', async () => {
|
|
164
|
+
const { getBrainDb } = await import('../../store/brain-sqlite.js');
|
|
165
|
+
const { closeBrainDb } = await import('../../store/brain-sqlite.js');
|
|
166
|
+
closeBrainDb();
|
|
167
|
+
await getBrainDb(tempDir);
|
|
168
|
+
|
|
169
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
170
|
+
const nativeDb = getBrainNativeDb();
|
|
171
|
+
const indexes = nativeDb
|
|
172
|
+
?.prepare(
|
|
173
|
+
`SELECT name FROM sqlite_master
|
|
174
|
+
WHERE type='index' AND tbl_name='brain_weight_history'
|
|
175
|
+
ORDER BY name`,
|
|
176
|
+
)
|
|
177
|
+
.all() as Array<{ name: string }>;
|
|
178
|
+
|
|
179
|
+
const indexNames = indexes.map((i) => i.name);
|
|
180
|
+
expect(indexNames).toContain('idx_weight_history_edge');
|
|
181
|
+
expect(indexNames).toContain('idx_weight_history_from');
|
|
182
|
+
expect(indexNames).toContain('idx_weight_history_to');
|
|
183
|
+
expect(indexNames).toContain('idx_weight_history_changed_at');
|
|
184
|
+
expect(indexNames).toContain('idx_weight_history_event_kind');
|
|
185
|
+
expect(indexNames).toContain('idx_weight_history_plasticity_event');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// =========================================================================
|
|
190
|
+
// brain_modulators (T699)
|
|
191
|
+
// =========================================================================
|
|
192
|
+
|
|
193
|
+
describe('brain_modulators', () => {
|
|
194
|
+
it('T699-1: table exists after DB initialisation', async () => {
|
|
195
|
+
const { getBrainDb } = await import('../../store/brain-sqlite.js');
|
|
196
|
+
const { closeBrainDb } = await import('../../store/brain-sqlite.js');
|
|
197
|
+
closeBrainDb();
|
|
198
|
+
await getBrainDb(tempDir);
|
|
199
|
+
|
|
200
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
201
|
+
const nativeDb = getBrainNativeDb();
|
|
202
|
+
const row = nativeDb
|
|
203
|
+
?.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='brain_modulators'`)
|
|
204
|
+
.get() as { name: string } | undefined;
|
|
205
|
+
expect(row?.name).toBe('brain_modulators');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('T699-2: insert and select back all required columns', async () => {
|
|
209
|
+
const { insertModulatorRow } = await import('../../store/brain-accessor.js');
|
|
210
|
+
|
|
211
|
+
const inserted = await insertModulatorRow(tempDir, {
|
|
212
|
+
modulatorType: 'task_completed',
|
|
213
|
+
valence: 0.5,
|
|
214
|
+
magnitude: 1.0,
|
|
215
|
+
sourceEventId: 'T697',
|
|
216
|
+
sessionId: 'ses_test_modulator',
|
|
217
|
+
description: 'Task T697 completed (unverified)',
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(inserted.id).toBeGreaterThan(0);
|
|
221
|
+
expect(inserted.modulatorType).toBe('task_completed');
|
|
222
|
+
expect(inserted.valence).toBe(0.5);
|
|
223
|
+
expect(inserted.magnitude).toBe(1.0);
|
|
224
|
+
expect(inserted.sourceEventId).toBe('T697');
|
|
225
|
+
expect(inserted.sessionId).toBe('ses_test_modulator');
|
|
226
|
+
expect(inserted.description).toBe('Task T697 completed (unverified)');
|
|
227
|
+
expect(inserted.createdAt).toBeTruthy();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('T699-3: magnitude defaults to 1.0 when not provided', async () => {
|
|
231
|
+
const { insertModulatorRow } = await import('../../store/brain-accessor.js');
|
|
232
|
+
|
|
233
|
+
const inserted = await insertModulatorRow(tempDir, {
|
|
234
|
+
modulatorType: 'task_cancelled',
|
|
235
|
+
valence: -0.5,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(inserted.magnitude).toBe(1.0);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('T699-4: valence accepts full [-1.0, +1.0] range', async () => {
|
|
242
|
+
const { insertModulatorRow } = await import('../../store/brain-accessor.js');
|
|
243
|
+
const valences = [-1.0, -0.5, 0.0, 0.5, 1.0];
|
|
244
|
+
|
|
245
|
+
for (const valence of valences) {
|
|
246
|
+
const row = await insertModulatorRow(tempDir, {
|
|
247
|
+
modulatorType: 'external',
|
|
248
|
+
valence,
|
|
249
|
+
});
|
|
250
|
+
expect(row.valence).toBe(valence);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('T699-5: optional fields are nullable', async () => {
|
|
255
|
+
const { insertModulatorRow } = await import('../../store/brain-accessor.js');
|
|
256
|
+
|
|
257
|
+
const inserted = await insertModulatorRow(tempDir, {
|
|
258
|
+
modulatorType: 'session_success',
|
|
259
|
+
valence: 0.3,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(inserted.sourceEventId).toBeNull();
|
|
263
|
+
expect(inserted.sessionId).toBeNull();
|
|
264
|
+
expect(inserted.description).toBeNull();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('T699-6: all five expected indexes exist', async () => {
|
|
268
|
+
const { getBrainDb } = await import('../../store/brain-sqlite.js');
|
|
269
|
+
const { closeBrainDb } = await import('../../store/brain-sqlite.js');
|
|
270
|
+
closeBrainDb();
|
|
271
|
+
await getBrainDb(tempDir);
|
|
272
|
+
|
|
273
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
274
|
+
const nativeDb = getBrainNativeDb();
|
|
275
|
+
const indexes = nativeDb
|
|
276
|
+
?.prepare(
|
|
277
|
+
`SELECT name FROM sqlite_master
|
|
278
|
+
WHERE type='index' AND tbl_name='brain_modulators'
|
|
279
|
+
ORDER BY name`,
|
|
280
|
+
)
|
|
281
|
+
.all() as Array<{ name: string }>;
|
|
282
|
+
|
|
283
|
+
const indexNames = indexes.map((i) => i.name);
|
|
284
|
+
expect(indexNames).toContain('idx_modulators_type');
|
|
285
|
+
expect(indexNames).toContain('idx_modulators_session');
|
|
286
|
+
expect(indexNames).toContain('idx_modulators_created_at');
|
|
287
|
+
expect(indexNames).toContain('idx_modulators_source_event');
|
|
288
|
+
expect(indexNames).toContain('idx_modulators_valence');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// =========================================================================
|
|
293
|
+
// brain_consolidation_events (T701)
|
|
294
|
+
// =========================================================================
|
|
295
|
+
|
|
296
|
+
describe('brain_consolidation_events', () => {
|
|
297
|
+
it('T701-1: table exists after DB initialisation', async () => {
|
|
298
|
+
const { getBrainDb } = await import('../../store/brain-sqlite.js');
|
|
299
|
+
const { closeBrainDb } = await import('../../store/brain-sqlite.js');
|
|
300
|
+
closeBrainDb();
|
|
301
|
+
await getBrainDb(tempDir);
|
|
302
|
+
|
|
303
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
304
|
+
const nativeDb = getBrainNativeDb();
|
|
305
|
+
const row = nativeDb
|
|
306
|
+
?.prepare(
|
|
307
|
+
`SELECT name FROM sqlite_master
|
|
308
|
+
WHERE type='table' AND name='brain_consolidation_events'`,
|
|
309
|
+
)
|
|
310
|
+
.get() as { name: string } | undefined;
|
|
311
|
+
expect(row?.name).toBe('brain_consolidation_events');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('T701-2: logConsolidationStart inserts a row with correct trigger', async () => {
|
|
315
|
+
const { logConsolidationStart } = await import('../../store/brain-accessor.js');
|
|
316
|
+
|
|
317
|
+
const id = await logConsolidationStart(tempDir, 'session_end', 'ses_test_T701');
|
|
318
|
+
|
|
319
|
+
expect(typeof id).toBe('number');
|
|
320
|
+
expect(id).toBeGreaterThan(0);
|
|
321
|
+
|
|
322
|
+
// Verify the row exists in the DB
|
|
323
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
324
|
+
const nativeDb = getBrainNativeDb();
|
|
325
|
+
const row = nativeDb
|
|
326
|
+
?.prepare('SELECT * FROM brain_consolidation_events WHERE id = ?')
|
|
327
|
+
.get(id) as Record<string, unknown> | undefined;
|
|
328
|
+
|
|
329
|
+
expect(row).toBeDefined();
|
|
330
|
+
expect(row?.['trigger']).toBe('session_end');
|
|
331
|
+
expect(row?.['session_id']).toBe('ses_test_T701');
|
|
332
|
+
expect(row?.['step_results_json']).toBe('{}');
|
|
333
|
+
expect(row?.['succeeded']).toBe(1);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('T701-3: logConsolidationComplete updates the row with final results', async () => {
|
|
337
|
+
const { logConsolidationStart, logConsolidationComplete } = await import(
|
|
338
|
+
'../../store/brain-accessor.js'
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const id = await logConsolidationStart(tempDir, 'manual', 'ses_test_complete');
|
|
342
|
+
const stats = {
|
|
343
|
+
step6_hebbian: { count: 12, durationMs: 45 },
|
|
344
|
+
step9a_reward: { count: 3, durationMs: 120 },
|
|
345
|
+
step9b_stdp: { count: 8, durationMs: 250 },
|
|
346
|
+
};
|
|
347
|
+
const updated = await logConsolidationComplete(tempDir, id, stats, 415, true);
|
|
348
|
+
|
|
349
|
+
expect(updated.id).toBe(id);
|
|
350
|
+
expect(updated.durationMs).toBe(415);
|
|
351
|
+
expect(updated.succeeded).toBe(true);
|
|
352
|
+
expect(JSON.parse(updated.stepResultsJson)).toMatchObject(stats);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('T701-4: succeeded defaults to true', async () => {
|
|
356
|
+
const { logConsolidationStart } = await import('../../store/brain-accessor.js');
|
|
357
|
+
|
|
358
|
+
const id = await logConsolidationStart(tempDir, 'scheduled', undefined);
|
|
359
|
+
|
|
360
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
361
|
+
const nativeDb = getBrainNativeDb();
|
|
362
|
+
const row = nativeDb
|
|
363
|
+
?.prepare('SELECT succeeded FROM brain_consolidation_events WHERE id = ?')
|
|
364
|
+
.get(id) as { succeeded: number } | undefined;
|
|
365
|
+
|
|
366
|
+
// SQLite stores boolean as 1 (true)
|
|
367
|
+
expect(row?.succeeded).toBe(1);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('T701-5: session_id and duration_ms are nullable', async () => {
|
|
371
|
+
const { logConsolidationStart } = await import('../../store/brain-accessor.js');
|
|
372
|
+
|
|
373
|
+
const id = await logConsolidationStart(tempDir, 'manual', undefined);
|
|
374
|
+
|
|
375
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
376
|
+
const nativeDb = getBrainNativeDb();
|
|
377
|
+
const row = nativeDb
|
|
378
|
+
?.prepare('SELECT session_id, duration_ms FROM brain_consolidation_events WHERE id = ?')
|
|
379
|
+
.get(id) as { session_id: null; duration_ms: null } | undefined;
|
|
380
|
+
|
|
381
|
+
expect(row?.session_id).toBeNull();
|
|
382
|
+
expect(row?.duration_ms).toBeNull();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('T701-6: all four trigger values are accepted', async () => {
|
|
386
|
+
const { logConsolidationStart } = await import('../../store/brain-accessor.js');
|
|
387
|
+
const triggers = ['session_end', 'maintenance', 'scheduled', 'manual'] as const;
|
|
388
|
+
|
|
389
|
+
for (const trigger of triggers) {
|
|
390
|
+
const id = await logConsolidationStart(tempDir, trigger, undefined);
|
|
391
|
+
expect(id).toBeGreaterThan(0);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
395
|
+
const nativeDb = getBrainNativeDb();
|
|
396
|
+
const count = (
|
|
397
|
+
nativeDb?.prepare('SELECT COUNT(*) as cnt FROM brain_consolidation_events').get() as {
|
|
398
|
+
cnt: number;
|
|
399
|
+
}
|
|
400
|
+
).cnt;
|
|
401
|
+
expect(count).toBe(4);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('T701-7: all three expected indexes exist', async () => {
|
|
405
|
+
const { getBrainDb } = await import('../../store/brain-sqlite.js');
|
|
406
|
+
const { closeBrainDb } = await import('../../store/brain-sqlite.js');
|
|
407
|
+
closeBrainDb();
|
|
408
|
+
await getBrainDb(tempDir);
|
|
409
|
+
|
|
410
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
411
|
+
const nativeDb = getBrainNativeDb();
|
|
412
|
+
const indexes = nativeDb
|
|
413
|
+
?.prepare(
|
|
414
|
+
`SELECT name FROM sqlite_master
|
|
415
|
+
WHERE type='index' AND tbl_name='brain_consolidation_events'
|
|
416
|
+
ORDER BY name`,
|
|
417
|
+
)
|
|
418
|
+
.all() as Array<{ name: string }>;
|
|
419
|
+
|
|
420
|
+
const indexNames = indexes.map((i) => i.name);
|
|
421
|
+
expect(indexNames).toContain('idx_consolidation_events_started_at');
|
|
422
|
+
expect(indexNames).toContain('idx_consolidation_events_trigger');
|
|
423
|
+
expect(indexNames).toContain('idx_consolidation_events_session');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('T701-8: failed consolidation records succeeded=false', async () => {
|
|
427
|
+
const { logConsolidationStart, logConsolidationComplete } = await import(
|
|
428
|
+
'../../store/brain-accessor.js'
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const id = await logConsolidationStart(tempDir, 'session_end', 'ses_fail_test');
|
|
432
|
+
const updated = await logConsolidationComplete(
|
|
433
|
+
tempDir,
|
|
434
|
+
id,
|
|
435
|
+
{ error: 'step9b_stdp threw' },
|
|
436
|
+
50,
|
|
437
|
+
false,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
expect(updated.succeeded).toBe(false);
|
|
441
|
+
expect(JSON.parse(updated.stepResultsJson)).toMatchObject({ error: 'step9b_stdp threw' });
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// =========================================================================
|
|
446
|
+
// Cross-table: idempotency and isolation
|
|
447
|
+
// =========================================================================
|
|
448
|
+
|
|
449
|
+
describe('Cross-table isolation', () => {
|
|
450
|
+
it('all three tables are independent — inserts to one do not affect others', async () => {
|
|
451
|
+
const { insertWeightHistoryRow, insertModulatorRow, logConsolidationStart } = await import(
|
|
452
|
+
'../../store/brain-accessor.js'
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
await insertWeightHistoryRow(tempDir, {
|
|
456
|
+
edgeFromId: 'obs:A',
|
|
457
|
+
edgeToId: 'obs:B',
|
|
458
|
+
edgeType: 'co_retrieved',
|
|
459
|
+
weightBefore: null,
|
|
460
|
+
weightAfter: 0.05,
|
|
461
|
+
deltaWeight: 0.05,
|
|
462
|
+
eventKind: 'ltp',
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
await insertModulatorRow(tempDir, {
|
|
466
|
+
modulatorType: 'task_verified',
|
|
467
|
+
valence: 1.0,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
await logConsolidationStart(tempDir, 'session_end', 'ses_isolation_test');
|
|
471
|
+
|
|
472
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
473
|
+
const nativeDb = getBrainNativeDb();
|
|
474
|
+
|
|
475
|
+
const whCount = (
|
|
476
|
+
nativeDb?.prepare('SELECT COUNT(*) as cnt FROM brain_weight_history').get() as {
|
|
477
|
+
cnt: number;
|
|
478
|
+
}
|
|
479
|
+
).cnt;
|
|
480
|
+
const modCount = (
|
|
481
|
+
nativeDb?.prepare('SELECT COUNT(*) as cnt FROM brain_modulators').get() as { cnt: number }
|
|
482
|
+
).cnt;
|
|
483
|
+
const ceCount = (
|
|
484
|
+
nativeDb?.prepare('SELECT COUNT(*) as cnt FROM brain_consolidation_events').get() as {
|
|
485
|
+
cnt: number;
|
|
486
|
+
}
|
|
487
|
+
).cnt;
|
|
488
|
+
|
|
489
|
+
expect(whCount).toBe(1);
|
|
490
|
+
expect(modCount).toBe(1);
|
|
491
|
+
expect(ceCount).toBe(1);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
});
|
|
@@ -84,6 +84,13 @@ interface RawEdge {
|
|
|
84
84
|
weight: number;
|
|
85
85
|
provenance: string | null;
|
|
86
86
|
created_at: string;
|
|
87
|
+
// T673-M3 plasticity columns — may be absent from partial SELECTs
|
|
88
|
+
last_reinforced_at?: string | null;
|
|
89
|
+
reinforcement_count?: number;
|
|
90
|
+
plasticity_class?: string;
|
|
91
|
+
last_depressed_at?: string | null;
|
|
92
|
+
depression_count?: number;
|
|
93
|
+
stability_score?: number | null;
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
/** Map a snake_case raw row to a camelCase BrainPageNodeRow. */
|
|
@@ -110,6 +117,13 @@ function mapEdge(raw: RawEdge): BrainPageEdgeRow {
|
|
|
110
117
|
weight: raw.weight,
|
|
111
118
|
provenance: raw.provenance,
|
|
112
119
|
createdAt: raw.created_at,
|
|
120
|
+
// T673-M3 plasticity columns (default to neutral values if absent from SELECT)
|
|
121
|
+
lastReinforcedAt: raw.last_reinforced_at ?? null,
|
|
122
|
+
reinforcementCount: raw.reinforcement_count ?? 0,
|
|
123
|
+
plasticityClass: (raw.plasticity_class ?? 'static') as BrainPageEdgeRow['plasticityClass'],
|
|
124
|
+
lastDepressedAt: raw.last_depressed_at ?? null,
|
|
125
|
+
depressionCount: raw.depression_count ?? 0,
|
|
126
|
+
stabilityScore: raw.stability_score ?? null,
|
|
113
127
|
};
|
|
114
128
|
}
|
|
115
129
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* project conduit.db — project_agent_refs (openConduitDb)
|
|
16
16
|
* Join performed in Node (SQLite cannot cross-file-handle JOIN).
|
|
17
17
|
*
|
|
18
|
-
* @see .cleo/
|
|
18
|
+
* @see .cleo/rcasd/T310/specification/T310-specification.md §3.5
|
|
19
19
|
* @see .cleo/adrs/ADR-037-conduit-signaldock-separation.md
|
|
20
20
|
* @task T355
|
|
21
21
|
* @epic T310
|
|
@@ -13,14 +13,19 @@ import type { SQL } from 'drizzle-orm';
|
|
|
13
13
|
import { and, asc, desc, eq, gte, or } from 'drizzle-orm';
|
|
14
14
|
import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite';
|
|
15
15
|
import type {
|
|
16
|
+
BrainConsolidationEventRow,
|
|
16
17
|
BrainDecisionRow,
|
|
17
18
|
BrainLearningRow,
|
|
18
19
|
BrainMemoryLinkRow,
|
|
20
|
+
BrainModulatorInsert,
|
|
21
|
+
BrainModulatorRow,
|
|
19
22
|
BrainObservationRow,
|
|
20
23
|
BrainPageEdgeRow,
|
|
21
24
|
BrainPageNodeRow,
|
|
22
25
|
BrainPatternRow,
|
|
23
26
|
BrainStickyNoteRow,
|
|
27
|
+
BrainWeightHistoryInsert,
|
|
28
|
+
BrainWeightHistoryRow,
|
|
24
29
|
NewBrainDecisionRow,
|
|
25
30
|
NewBrainLearningRow,
|
|
26
31
|
NewBrainMemoryLinkRow,
|
|
@@ -620,3 +625,118 @@ export async function getBrainAccessor(cwd?: string): Promise<BrainDataAccessor>
|
|
|
620
625
|
const db = await getBrainDb(cwd);
|
|
621
626
|
return new BrainDataAccessor(db);
|
|
622
627
|
}
|
|
628
|
+
|
|
629
|
+
// ============================================================================
|
|
630
|
+
// M4 PLASTICITY AUX TABLE ACCESSORS (T673-M4)
|
|
631
|
+
// Minimal writers — Wave 1+ workers wire the full call paths.
|
|
632
|
+
// ============================================================================
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Insert one row into brain_weight_history.
|
|
636
|
+
* Called by writeWeightHistory() in brain-stdp.ts for each LTP, LTD, Hebbian,
|
|
637
|
+
* or prune event that crosses the 1e-6 negligibility threshold.
|
|
638
|
+
*
|
|
639
|
+
* @param cwd - Project root (locates brain.db). Defaults to process.cwd().
|
|
640
|
+
* @param input - Row data. `changedAt` defaults to SQLite datetime('now').
|
|
641
|
+
* @returns The inserted row with its generated id.
|
|
642
|
+
*
|
|
643
|
+
* @task T697
|
|
644
|
+
* @epic T673
|
|
645
|
+
*/
|
|
646
|
+
export async function insertWeightHistoryRow(
|
|
647
|
+
cwd: string | undefined,
|
|
648
|
+
input: BrainWeightHistoryInsert,
|
|
649
|
+
): Promise<BrainWeightHistoryRow> {
|
|
650
|
+
const db = await getBrainDb(cwd);
|
|
651
|
+
const result = await db.insert(brainSchema.brainWeightHistory).values(input).returning();
|
|
652
|
+
return result[0]!;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Insert one row into brain_modulators.
|
|
657
|
+
* Called by backfillRewardSignals() for each task outcome it processes.
|
|
658
|
+
* Both this INSERT and the retrieval_log UPDATE MUST run in the same logical
|
|
659
|
+
* pass but in separate transactions (no ATTACH) — see spec §4.3.
|
|
660
|
+
*
|
|
661
|
+
* @param cwd - Project root (locates brain.db). Defaults to process.cwd().
|
|
662
|
+
* @param input - Row data. `createdAt` defaults to SQLite datetime('now').
|
|
663
|
+
* @returns The inserted row with its generated id.
|
|
664
|
+
*
|
|
665
|
+
* @task T699
|
|
666
|
+
* @epic T673
|
|
667
|
+
*/
|
|
668
|
+
export async function insertModulatorRow(
|
|
669
|
+
cwd: string | undefined,
|
|
670
|
+
input: BrainModulatorInsert,
|
|
671
|
+
): Promise<BrainModulatorRow> {
|
|
672
|
+
const db = await getBrainDb(cwd);
|
|
673
|
+
const result = await db.insert(brainSchema.brainModulators).values(input).returning();
|
|
674
|
+
return result[0]!;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Open a consolidation event row in brain_consolidation_events.
|
|
679
|
+
* Call this at the START of runConsolidation before any steps execute.
|
|
680
|
+
* Returns the new row id — pass it to logConsolidationComplete() when done.
|
|
681
|
+
*
|
|
682
|
+
* @param cwd - Project root (locates brain.db).
|
|
683
|
+
* @param trigger - What initiated this consolidation run.
|
|
684
|
+
* @param sessionId - Active session ID, if any.
|
|
685
|
+
* @returns The id of the newly inserted row.
|
|
686
|
+
*
|
|
687
|
+
* @task T701
|
|
688
|
+
* @epic T673
|
|
689
|
+
*/
|
|
690
|
+
export async function logConsolidationStart(
|
|
691
|
+
cwd: string | undefined,
|
|
692
|
+
trigger: string,
|
|
693
|
+
sessionId?: string,
|
|
694
|
+
): Promise<number> {
|
|
695
|
+
const db = await getBrainDb(cwd);
|
|
696
|
+
const result = await db
|
|
697
|
+
.insert(brainSchema.brainConsolidationEvents)
|
|
698
|
+
.values({
|
|
699
|
+
trigger,
|
|
700
|
+
sessionId: sessionId ?? null,
|
|
701
|
+
// stepResultsJson is required NOT NULL — use empty object as placeholder
|
|
702
|
+
// until logConsolidationComplete updates it with final step results.
|
|
703
|
+
stepResultsJson: '{}',
|
|
704
|
+
succeeded: true,
|
|
705
|
+
})
|
|
706
|
+
.returning({ id: brainSchema.brainConsolidationEvents.id });
|
|
707
|
+
return result[0]!.id;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Complete a consolidation event row by updating it with final results.
|
|
712
|
+
* Call this at the END of runConsolidation after all steps complete.
|
|
713
|
+
*
|
|
714
|
+
* @param cwd - Project root (locates brain.db).
|
|
715
|
+
* @param id - Row id returned by logConsolidationStart.
|
|
716
|
+
* @param stats - JSON-serializable step results object.
|
|
717
|
+
* @param durationMs - Total wall-clock duration in milliseconds.
|
|
718
|
+
* @param succeeded - Whether the run completed without error.
|
|
719
|
+
* @returns The updated row.
|
|
720
|
+
*
|
|
721
|
+
* @task T701
|
|
722
|
+
* @epic T673
|
|
723
|
+
*/
|
|
724
|
+
export async function logConsolidationComplete(
|
|
725
|
+
cwd: string | undefined,
|
|
726
|
+
id: number,
|
|
727
|
+
stats: Record<string, unknown>,
|
|
728
|
+
durationMs: number,
|
|
729
|
+
succeeded = true,
|
|
730
|
+
): Promise<BrainConsolidationEventRow> {
|
|
731
|
+
const db = await getBrainDb(cwd);
|
|
732
|
+
const result = await db
|
|
733
|
+
.update(brainSchema.brainConsolidationEvents)
|
|
734
|
+
.set({
|
|
735
|
+
stepResultsJson: JSON.stringify(stats),
|
|
736
|
+
durationMs,
|
|
737
|
+
succeeded,
|
|
738
|
+
})
|
|
739
|
+
.where(eq(brainSchema.brainConsolidationEvents.id, id))
|
|
740
|
+
.returning();
|
|
741
|
+
return result[0]!;
|
|
742
|
+
}
|