@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
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Tests for decision-cross-link module.
3
+ *
4
+ * Covers:
5
+ * - extractReferencedSymbols: file-path detection, symbol detection, dedup,
6
+ * stop-word filtering, short-name filtering.
7
+ * - linkDecisionToTargets: verifies edges are written to brain_page_edges
8
+ * when autoCapture is enabled.
9
+ * - autoCrossLinkDecision: end-to-end smoke test (never throws).
10
+ *
11
+ * @task T626
12
+ * @epic T626
13
+ */
14
+
15
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
16
+ import { tmpdir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ let tempDir: string;
25
+ let cleoDir: string;
26
+
27
+ beforeEach(async () => {
28
+ tempDir = await mkdtemp(join(tmpdir(), 'cleo-decision-cross-link-'));
29
+ cleoDir = join(tempDir, '.cleo');
30
+ await mkdir(cleoDir, { recursive: true });
31
+ process.env['CLEO_DIR'] = cleoDir;
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
+ vi.restoreAllMocks();
40
+ });
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // extractReferencedSymbols
44
+ // ---------------------------------------------------------------------------
45
+
46
+ describe('extractReferencedSymbols', () => {
47
+ it('extracts a relative TypeScript file path', async () => {
48
+ const { extractReferencedSymbols } = await import('../decision-cross-link.js');
49
+ const refs = extractReferencedSymbols('Changed src/store/brain-schema.ts to add new column');
50
+ const fileRefs = refs.filter((r) => r.nodeType === 'file');
51
+ expect(fileRefs.length).toBeGreaterThanOrEqual(1);
52
+ expect(fileRefs.some((r) => r.raw === 'src/store/brain-schema.ts')).toBe(true);
53
+ });
54
+
55
+ it('extracts an absolute TypeScript file path', async () => {
56
+ const { extractReferencedSymbols } = await import('../decision-cross-link.js');
57
+ const refs = extractReferencedSymbols(
58
+ 'See /mnt/projects/cleocode/packages/core/src/memory/decisions.ts for details',
59
+ );
60
+ const fileRefs = refs.filter((r) => r.nodeType === 'file');
61
+ expect(fileRefs.some((r) => r.raw.endsWith('decisions.ts'))).toBe(true);
62
+ });
63
+
64
+ it('extracts a PascalCase class reference', async () => {
65
+ const { extractReferencedSymbols } = await import('../decision-cross-link.js');
66
+ const refs = extractReferencedSymbols('BrainDataAccessor must expose the new query method');
67
+ const symRefs = refs.filter((r) => r.nodeType === 'symbol');
68
+ expect(symRefs.some((r) => r.raw === 'BrainDataAccessor')).toBe(true);
69
+ });
70
+
71
+ it('extracts a camelCase function reference', async () => {
72
+ const { extractReferencedSymbols } = await import('../decision-cross-link.js');
73
+ const refs = extractReferencedSymbols('Call upsertGraphNode before addGraphEdge');
74
+ const symRefs = refs.filter((r) => r.nodeType === 'symbol');
75
+ const names = symRefs.map((r) => r.raw);
76
+ expect(names).toContain('upsertGraphNode');
77
+ expect(names).toContain('addGraphEdge');
78
+ });
79
+
80
+ it('deduplicates identical references', async () => {
81
+ const { extractReferencedSymbols } = await import('../decision-cross-link.js');
82
+ const refs = extractReferencedSymbols(
83
+ 'upsertGraphNode is called twice: upsertGraphNode for nodes, upsertGraphNode for edges',
84
+ );
85
+ const symRefs = refs.filter((r) => r.raw === 'upsertGraphNode');
86
+ expect(symRefs).toHaveLength(1);
87
+ });
88
+
89
+ it('filters out stop-words', async () => {
90
+ const { extractReferencedSymbols } = await import('../decision-cross-link.js');
91
+ // "always", "should", "never" are in the stop-word list
92
+ const refs = extractReferencedSymbols('always should never with this that from into');
93
+ const symRefs = refs.filter((r) => r.nodeType === 'symbol');
94
+ const names = symRefs.map((r) => r.raw.toLowerCase());
95
+ expect(names).not.toContain('always');
96
+ expect(names).not.toContain('should');
97
+ expect(names).not.toContain('never');
98
+ });
99
+
100
+ it('filters out symbols shorter than 4 characters', async () => {
101
+ const { extractReferencedSymbols } = await import('../decision-cross-link.js');
102
+ const refs = extractReferencedSymbols('Use DB to store it');
103
+ const symRefs = refs.filter((r) => r.nodeType === 'symbol' && r.raw.length < 4);
104
+ expect(symRefs).toHaveLength(0);
105
+ });
106
+
107
+ it('returns empty array for plain-English text with no references', async () => {
108
+ const { extractReferencedSymbols } = await import('../decision-cross-link.js');
109
+ const refs = extractReferencedSymbols('We decided to use a relational database.');
110
+ // May have some false positives for symbols, but no file refs
111
+ const fileRefs = refs.filter((r) => r.nodeType === 'file');
112
+ expect(fileRefs).toHaveLength(0);
113
+ });
114
+
115
+ it('assigns correct nodeIds', async () => {
116
+ const { extractReferencedSymbols } = await import('../decision-cross-link.js');
117
+ const refs = extractReferencedSymbols(
118
+ 'Updated src/memory/decisions.ts and called storeDecision',
119
+ );
120
+ const fileRef = refs.find((r) => r.nodeType === 'file');
121
+ const symRef = refs.find((r) => r.nodeType === 'symbol' && r.raw === 'storeDecision');
122
+ expect(fileRef?.nodeId).toMatch(/^file:/);
123
+ expect(symRef?.nodeId).toBe('symbol:storeDecision');
124
+ });
125
+ });
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // linkDecisionToTargets — graph writes
129
+ // ---------------------------------------------------------------------------
130
+
131
+ describe('linkDecisionToTargets', () => {
132
+ it('creates applies_to edges in brain_page_edges when autoCapture is enabled', async () => {
133
+ // Stub shouldAutoPopulateGraph to return true by setting autoCapture config
134
+ // The easiest approach: write a minimal config.json so isAutoCaptureEnabled returns true
135
+ const { writeFile } = await import('node:fs/promises');
136
+ await writeFile(join(cleoDir, 'config.json'), JSON.stringify({ brain: { autoCapture: true } }));
137
+
138
+ const { closeBrainDb, getBrainDb } = await import('../../store/brain-sqlite.js');
139
+ closeBrainDb();
140
+
141
+ // Initialise DB
142
+ const db = await getBrainDb(tempDir);
143
+
144
+ const { extractReferencedSymbols, linkDecisionToTargets } = await import(
145
+ '../decision-cross-link.js'
146
+ );
147
+ const { brainPageEdges } = await import('../../store/brain-schema.js');
148
+
149
+ const refs = extractReferencedSymbols(
150
+ 'Refactored src/store/brain-schema.ts to add BrainPageNodes column',
151
+ );
152
+ expect(refs.length).toBeGreaterThan(0);
153
+
154
+ await linkDecisionToTargets(tempDir, 'D001', refs);
155
+
156
+ // Check that at least one applies_to edge was created
157
+ const edges = await db.select().from(brainPageEdges);
158
+ const affectsEdges = edges.filter(
159
+ (e) => e.fromId === 'decision:D001' && e.edgeType === 'applies_to',
160
+ );
161
+ expect(affectsEdges.length).toBeGreaterThanOrEqual(1);
162
+ });
163
+
164
+ it('is a no-op when refs array is empty', async () => {
165
+ const { writeFile } = await import('node:fs/promises');
166
+ await writeFile(join(cleoDir, 'config.json'), JSON.stringify({ brain: { autoCapture: true } }));
167
+
168
+ const { closeBrainDb, getBrainDb } = await import('../../store/brain-sqlite.js');
169
+ closeBrainDb();
170
+ const db = await getBrainDb(tempDir);
171
+ const { brainPageEdges } = await import('../../store/brain-schema.js');
172
+
173
+ const { linkDecisionToTargets } = await import('../decision-cross-link.js');
174
+ await linkDecisionToTargets(tempDir, 'D001', []);
175
+
176
+ const edges = await db.select().from(brainPageEdges);
177
+ expect(edges.filter((e) => e.fromId === 'decision:D001')).toHaveLength(0);
178
+ });
179
+
180
+ it('does not throw when autoCapture is disabled', async () => {
181
+ const { writeFile } = await import('node:fs/promises');
182
+ await writeFile(
183
+ join(cleoDir, 'config.json'),
184
+ JSON.stringify({ brain: { autoCapture: false } }),
185
+ );
186
+
187
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
188
+ closeBrainDb();
189
+
190
+ const { extractReferencedSymbols, linkDecisionToTargets } = await import(
191
+ '../decision-cross-link.js'
192
+ );
193
+ const refs = extractReferencedSymbols('Changed src/store/brain-schema.ts');
194
+ // Should not throw even when autoCapture is off
195
+ await expect(linkDecisionToTargets(tempDir, 'D001', refs)).resolves.toBeUndefined();
196
+ });
197
+ });
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // autoCrossLinkDecision — convenience facade
201
+ // ---------------------------------------------------------------------------
202
+
203
+ describe('autoCrossLinkDecision', () => {
204
+ it('never throws for any input', async () => {
205
+ const { autoCrossLinkDecision } = await import('../decision-cross-link.js');
206
+
207
+ // Should not throw even without a DB or config
208
+ await expect(
209
+ autoCrossLinkDecision(tempDir, 'D001', 'some decision text', 'some rationale'),
210
+ ).resolves.toBeUndefined();
211
+
212
+ // Empty strings
213
+ await expect(autoCrossLinkDecision(tempDir, 'D001', '', '')).resolves.toBeUndefined();
214
+ });
215
+
216
+ it('creates edges when decision text mentions a known file path', async () => {
217
+ const { writeFile } = await import('node:fs/promises');
218
+ await writeFile(join(cleoDir, 'config.json'), JSON.stringify({ brain: { autoCapture: true } }));
219
+
220
+ const { closeBrainDb, getBrainDb } = await import('../../store/brain-sqlite.js');
221
+ closeBrainDb();
222
+ const db = await getBrainDb(tempDir);
223
+ const { brainPageEdges } = await import('../../store/brain-schema.js');
224
+
225
+ const { autoCrossLinkDecision } = await import('../decision-cross-link.js');
226
+ await autoCrossLinkDecision(
227
+ tempDir,
228
+ 'D042',
229
+ 'Use packages/core/src/memory/decisions.ts as the write gate',
230
+ 'Centralises all decision writes in one module',
231
+ );
232
+
233
+ const edges = await db.select().from(brainPageEdges);
234
+ const decisionsEdges = edges.filter(
235
+ (e) =>
236
+ e.fromId === 'decision:D042' && e.edgeType === 'applies_to' && e.toId.startsWith('file:'),
237
+ );
238
+ expect(decisionsEdges.length).toBeGreaterThanOrEqual(1);
239
+ });
240
+ });
@@ -21,6 +21,7 @@ import { createHash } from 'node:crypto';
21
21
  import { getBrainAccessor } from '../store/brain-accessor.js';
22
22
  import { typedAll } from '../store/typed-query.js';
23
23
  import type { BrainConsolidationObservationRow } from './brain-row-types.js';
24
+ import { EDGE_TYPES } from './edge-types.js';
24
25
 
25
26
  /** Result from applying temporal decay. */
26
27
  export interface DecayResult {
@@ -592,6 +593,13 @@ export interface RunConsolidationResult {
592
593
  summariesGenerated: number;
593
594
  /** Code↔memory graph links created. */
594
595
  graphLinksCreated?: number;
596
+ /** STDP plasticity result from step 9. */
597
+ stdpPlasticity?: {
598
+ ltpEvents: number;
599
+ ltdEvents: number;
600
+ edgesCreated: number;
601
+ pairsExamined: number;
602
+ };
595
603
  }
596
604
 
597
605
  /**
@@ -695,6 +703,17 @@ export async function runConsolidation(projectRoot: string): Promise<RunConsolid
695
703
  console.warn('[consolidation] Step 8 graph memory bridge failed:', err);
696
704
  }
697
705
 
706
+ // Step 9: STDP timing-dependent plasticity (T626 phase 5)
707
+ // Refines co_retrieved edge weights using retrieval temporal order.
708
+ // Runs after Hebbian strengthening (step 6) so it builds on existing edges.
709
+ try {
710
+ const { applyStdpPlasticity } = await import('./brain-stdp.js');
711
+ const stdpResult = await applyStdpPlasticity(projectRoot);
712
+ result.stdpPlasticity = stdpResult;
713
+ } catch (err) {
714
+ console.warn('[consolidation] Step 9 STDP plasticity failed:', err);
715
+ }
716
+
698
717
  return result;
699
718
  }
700
719
 
@@ -979,9 +998,9 @@ async function strengthenCoRetrievedEdges(projectRoot: string): Promise<number>
979
998
  const updateStmt = nativeDb.prepare(`
980
999
  UPDATE brain_page_edges
981
1000
  SET weight = MIN(1.0, weight + 0.1)
982
- WHERE from_id = ? AND to_id = ? AND edge_type = 'relates_to'
1001
+ WHERE from_id = ? AND to_id = ? AND edge_type = ?
983
1002
  `);
984
- const updateResult = updateStmt.run(nodeFrom, nodeTo);
1003
+ const updateResult = updateStmt.run(nodeFrom, nodeTo, EDGE_TYPES.CO_RETRIEVED);
985
1004
  const changes = typeof updateResult.changes === 'number' ? updateResult.changes : 0;
986
1005
 
987
1006
  if (changes === 0) {
@@ -990,9 +1009,9 @@ async function strengthenCoRetrievedEdges(projectRoot: string): Promise<number>
990
1009
  .prepare(`
991
1010
  INSERT OR IGNORE INTO brain_page_edges
992
1011
  (from_id, to_id, edge_type, weight, provenance, created_at)
993
- VALUES (?, ?, 'relates_to', 0.3, 'consolidation:co-retrieval', ?)
1012
+ VALUES (?, ?, ?, 0.3, 'consolidation:co-retrieval', ?)
994
1013
  `)
995
- .run(nodeFrom, nodeTo, now);
1014
+ .run(nodeFrom, nodeTo, EDGE_TYPES.CO_RETRIEVED, now);
996
1015
  }
997
1016
  strengthened++;
998
1017
  } catch {