@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,276 @@
1
+ /**
2
+ * Decision cross-link module for CLEO BRAIN.
3
+ *
4
+ * Extracts file paths and symbol names referenced in a decision's text and
5
+ * rationale, then creates `affects` edges from the decision graph node to
6
+ * matching `file` / `symbol` nodes in brain_page_nodes. This implements
7
+ * the cross-substrate edge described in
8
+ * docs/plans/brain-synaptic-visualization-research.md §3.2.
9
+ *
10
+ * All database operations are best-effort — they never throw or block the
11
+ * caller. Nodes for referenced files / symbols are upserted on demand so
12
+ * the graph remains consistent even when the target has not yet been
13
+ * independently indexed.
14
+ *
15
+ * @task T626
16
+ * @epic T626
17
+ */
18
+
19
+ import { addGraphEdge, upsertGraphNode } from './graph-auto-populate.js';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Extraction
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** A reference extracted from a decision or rationale string. */
26
+ export interface ExtractedRef {
27
+ /** Raw text that matched. */
28
+ raw: string;
29
+ /** Resolved graph node ID: 'file:<path>' or 'symbol:<name>'. */
30
+ nodeId: string;
31
+ /** Discriminated node type. */
32
+ nodeType: 'file' | 'symbol';
33
+ /** Human-readable label for the graph node. */
34
+ label: string;
35
+ }
36
+
37
+ /**
38
+ * Regex patterns used to locate file paths and symbol names inside text.
39
+ *
40
+ * File-path pattern — matches:
41
+ * - Relative paths: `src/store/brain-schema.ts`
42
+ * - Absolute paths: `/mnt/projects/cleocode/packages/core/src/…`
43
+ * - Extension-gated: only `.ts`, `.tsx`, `.js`, `.jsx`, `.rs`, `.json`
44
+ *
45
+ * Symbol pattern — matches:
46
+ * - PascalCase class/interface names: `BrainPageNodes`
47
+ * - camelCase function names at word boundaries: `upsertGraphNode`
48
+ * - snake_case identifiers: `brain_page_edges`
49
+ *
50
+ * Overlapping matches are deduplicated by nodeId before edge creation.
51
+ */
52
+
53
+ const FILE_PATH_RE =
54
+ /(?:^|[\s`"'([\]{,])((\/[\w.\-/]+|[\w.-]+(?:\/[\w.-]+)+)\.(ts|tsx|js|jsx|rs|json))(?=$|[\s`"')[\]{,])/gm;
55
+
56
+ const SYMBOL_RE =
57
+ /(?<![`"'/\w.])(?:[A-Z][a-zA-Z0-9]{2,}|[a-z][a-zA-Z0-9]*(?:[A-Z][a-zA-Z0-9]*)+|[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*){2,})(?![`"'/\w])/g;
58
+
59
+ /**
60
+ * Extract file-path and symbol references from free-form text.
61
+ *
62
+ * Symbols shorter than 4 characters or matching common English stop-words
63
+ * are filtered to reduce noise.
64
+ *
65
+ * @param text - Decision text and/or rationale to scan.
66
+ * @returns Deduplicated array of extracted references.
67
+ */
68
+ export function extractReferencedSymbols(text: string): ExtractedRef[] {
69
+ const seen = new Set<string>();
70
+ const refs: ExtractedRef[] = [];
71
+
72
+ // --- File paths ---
73
+ for (const match of text.matchAll(FILE_PATH_RE)) {
74
+ const raw = match[1];
75
+ if (!raw) continue;
76
+ const nodeId = `file:${raw}`;
77
+ if (seen.has(nodeId)) continue;
78
+ seen.add(nodeId);
79
+ refs.push({ raw, nodeId, nodeType: 'file', label: raw });
80
+ }
81
+
82
+ // --- Symbol names ---
83
+ for (const match of text.matchAll(SYMBOL_RE)) {
84
+ const raw = match[0];
85
+ if (!raw || raw.length < 4) continue;
86
+ if (SYMBOL_STOP_WORDS.has(raw.toLowerCase())) continue;
87
+ const nodeId = `symbol:${raw}`;
88
+ if (seen.has(nodeId)) continue;
89
+ seen.add(nodeId);
90
+ refs.push({ raw, nodeId, nodeType: 'symbol', label: raw });
91
+ }
92
+
93
+ return refs;
94
+ }
95
+
96
+ /**
97
+ * Common English / technical words that look like camelCase or PascalCase
98
+ * symbols but carry no meaningful code reference. Filtered out to keep the
99
+ * extracted reference set signal-rich.
100
+ */
101
+ const SYMBOL_STOP_WORDS = new Set([
102
+ 'this',
103
+ 'that',
104
+ 'with',
105
+ 'from',
106
+ 'into',
107
+ 'when',
108
+ 'then',
109
+ 'also',
110
+ 'both',
111
+ 'each',
112
+ 'such',
113
+ 'over',
114
+ 'after',
115
+ 'before',
116
+ 'always',
117
+ 'never',
118
+ 'should',
119
+ 'must',
120
+ 'will',
121
+ 'would',
122
+ 'could',
123
+ 'have',
124
+ 'been',
125
+ 'there',
126
+ 'their',
127
+ 'they',
128
+ 'them',
129
+ 'these',
130
+ 'those',
131
+ 'some',
132
+ 'only',
133
+ 'just',
134
+ 'more',
135
+ 'most',
136
+ 'many',
137
+ 'much',
138
+ 'well',
139
+ 'very',
140
+ 'here',
141
+ 'where',
142
+ 'which',
143
+ 'what',
144
+ 'why',
145
+ 'how',
146
+ 'the',
147
+ 'and',
148
+ 'but',
149
+ 'for',
150
+ 'not',
151
+ 'are',
152
+ 'was',
153
+ 'were',
154
+ 'has',
155
+ 'had',
156
+ 'its',
157
+ 'the',
158
+ 'data',
159
+ 'true',
160
+ 'false',
161
+ 'null',
162
+ 'none',
163
+ 'type',
164
+ 'test',
165
+ 'spec',
166
+ 'todo',
167
+ 'fixme',
168
+ 'note',
169
+ 'example',
170
+ 'index',
171
+ 'config',
172
+ 'error',
173
+ 'value',
174
+ 'input',
175
+ 'output',
176
+ 'result',
177
+ 'return',
178
+ 'default',
179
+ 'source',
180
+ 'target',
181
+ 'import',
182
+ 'export',
183
+ 'class',
184
+ 'interface',
185
+ 'function',
186
+ 'const',
187
+ 'async',
188
+ 'await',
189
+ ]);
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Edge creation
193
+ // ---------------------------------------------------------------------------
194
+
195
+ /**
196
+ * Create `affects` edges from a decision graph node to every referenced
197
+ * file / symbol node.
198
+ *
199
+ * For each reference:
200
+ * 1. Upsert the target node (file or symbol) so the graph stays consistent.
201
+ * 2. Insert an `applies_to` edge from `decision:<id>` to the target node.
202
+ *
203
+ * All writes are best-effort via {@link upsertGraphNode} and
204
+ * {@link addGraphEdge} — failures are swallowed internally.
205
+ *
206
+ * @param projectRoot - Absolute path to the project root directory.
207
+ * @param decisionId - The decision ID (e.g. `D001`).
208
+ * @param refs - Extracted references returned by {@link extractReferencedSymbols}.
209
+ */
210
+ export async function linkDecisionToTargets(
211
+ projectRoot: string,
212
+ decisionId: string,
213
+ refs: ExtractedRef[],
214
+ ): Promise<void> {
215
+ const fromId = `decision:${decisionId}`;
216
+
217
+ const writes = refs.map(async (ref) => {
218
+ // Upsert the target node so the edge has a valid destination even if the
219
+ // file / symbol has not been independently indexed yet.
220
+ await upsertGraphNode(
221
+ projectRoot,
222
+ ref.nodeId,
223
+ ref.nodeType,
224
+ ref.label,
225
+ 0.5, // placeholder quality until nexus indexes it
226
+ ref.raw,
227
+ );
228
+
229
+ await addGraphEdge(
230
+ projectRoot,
231
+ fromId,
232
+ ref.nodeId,
233
+ 'applies_to',
234
+ 1.0,
235
+ 'auto:decision-cross-link',
236
+ );
237
+ });
238
+
239
+ // Fire all writes concurrently — individual failures are swallowed inside
240
+ // upsertGraphNode / addGraphEdge.
241
+ await Promise.allSettled(writes);
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Convenience facade
246
+ // ---------------------------------------------------------------------------
247
+
248
+ /**
249
+ * Extract file/symbol references from a decision and create `applies_to`
250
+ * edges in the brain graph. Combines {@link extractReferencedSymbols} and
251
+ * {@link linkDecisionToTargets} in one call.
252
+ *
253
+ * This is the function wired into {@link storeDecision} after a new decision
254
+ * is saved. It is always fire-and-forget: the caller should NOT await it
255
+ * when used inside the decision write path.
256
+ *
257
+ * @param projectRoot - Absolute path to the project root directory.
258
+ * @param decisionId - The saved decision ID (e.g. `D001`).
259
+ * @param decisionText - Full decision text.
260
+ * @param rationale - Full rationale text.
261
+ */
262
+ export async function autoCrossLinkDecision(
263
+ projectRoot: string,
264
+ decisionId: string,
265
+ decisionText: string,
266
+ rationale: string,
267
+ ): Promise<void> {
268
+ try {
269
+ const combined = `${decisionText} ${rationale}`;
270
+ const refs = extractReferencedSymbols(combined);
271
+ if (refs.length === 0) return;
272
+ await linkDecisionToTargets(projectRoot, decisionId, refs);
273
+ } catch {
274
+ /* best-effort — never surface errors to caller */
275
+ }
276
+ }
@@ -13,6 +13,7 @@ import { getBrainAccessor } from '../store/brain-accessor.js';
13
13
  import type { BrainDecisionRow, NewBrainDecisionRow } from '../store/brain-schema.js';
14
14
  import { taskExistsInTasksDb } from '../store/cross-db-cleanup.js';
15
15
  import { getDb } from '../store/sqlite.js';
16
+ import { autoCrossLinkDecision } from './decision-cross-link.js';
16
17
  import { addGraphEdge, upsertGraphNode } from './graph-auto-populate.js';
17
18
  import { computeDecisionQuality } from './quality-scoring.js';
18
19
  import { detectSupersession, supersedeMemory } from './temporal-supersession.js';
@@ -229,6 +230,12 @@ export async function storeDecision(
229
230
  'auto:store-decision',
230
231
  );
231
232
  }
233
+
234
+ // Cross-link decision → referenced file/symbol nodes (T626 phase 1).
235
+ // Fire-and-forget — autoCrossLinkDecision swallows its own errors.
236
+ autoCrossLinkDecision(projectRoot, saved.id, saved.decision, saved.rationale).catch(() => {
237
+ /* best-effort */
238
+ });
232
239
  } catch {
233
240
  /* Graph population is best-effort — never block the primary return */
234
241
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Canonical edge-type constants for `brain_page_edges`.
3
+ *
4
+ * All code that writes or queries `brain_page_edges.edge_type` MUST use
5
+ * these constants instead of raw string literals to prevent enum drift.
6
+ *
7
+ * The values here are a subset of `BRAIN_EDGE_TYPES` (brain-schema.ts).
8
+ * They are duplicated as constants so callers do not need to import the
9
+ * schema module (which carries Drizzle + SQLite dependencies).
10
+ *
11
+ * @epic T626
12
+ */
13
+ export const EDGE_TYPES = {
14
+ // Plasticity (Hebbian / STDP co-retrieval)
15
+ CO_RETRIEVED: 'co_retrieved',
16
+ // Temporal supersession
17
+ SUPERSEDES: 'supersedes',
18
+ // Task / decision / pattern → target context
19
+ APPLIES_TO: 'applies_to',
20
+ // Provenance
21
+ DERIVED_FROM: 'derived_from',
22
+ // Observation → symbol/file impact
23
+ AFFECTS: 'affects',
24
+ // Observation → symbol name mention
25
+ MENTIONS: 'mentions',
26
+ // Observation → symbol/file structural link
27
+ DOCUMENTS: 'documents',
28
+ } as const;
29
+
30
+ /** Discriminated union of the canonical edge type constant values. */
31
+ export type EdgeType = (typeof EDGE_TYPES)[keyof typeof EDGE_TYPES];
@@ -1501,6 +1501,8 @@ export * from './brain-retrieval.js';
1501
1501
  export * from './brain-search.js';
1502
1502
  // === BRAIN Memory modules (brain.db backed) ===
1503
1503
  export * from './decisions.js';
1504
+ // === T626-M1: Canonical edge-type constants ===
1505
+ export * from './edge-types.js';
1504
1506
  // === T549 Wave 2: Extraction Gate ===
1505
1507
  export * from './extraction-gate.js';
1506
1508
  export * from './learnings.js';
@@ -563,6 +563,8 @@ export const BRAIN_EDGE_TYPES = [
563
563
  // Graph bridging (memory ↔ code)
564
564
  'references', // observation → references → symbol
565
565
  'modified_by', // file → modified_by → session
566
+ // Plasticity (Hebbian + STDP co-retrieval)
567
+ 'co_retrieved', // A → co_retrieved → B (Hebbian: frequently retrieved together)
566
568
  ] as const;
567
569
 
568
570
  /** Discriminated union of all supported brain graph edge types. */
@@ -713,6 +715,52 @@ export const brainRetrievalLog = sqliteTable(
713
715
  ],
714
716
  );
715
717
 
718
+ // ============================================================================
719
+ // PLASTICITY EVENTS — STDP weight-change audit log (T626 phase 5)
720
+ // ============================================================================
721
+
722
+ /**
723
+ * Records every STDP weight-change event applied to a brain_page_edges row.
724
+ *
725
+ * Each row captures the causal pair (source_node, target_node), the signed
726
+ * delta applied to the edge weight, whether it was a potentiation or
727
+ * depression event, and which session and timestamp triggered it.
728
+ *
729
+ * @task T626
730
+ * @epic T626
731
+ */
732
+ export const brainPlasticityEvents = sqliteTable(
733
+ 'brain_plasticity_events',
734
+ {
735
+ id: integer('id').primaryKey({ autoIncrement: true }),
736
+ /** from_id of the affected brain_page_edges row. */
737
+ sourceNode: text('source_node').notNull(),
738
+ /** to_id of the affected brain_page_edges row. */
739
+ targetNode: text('target_node').notNull(),
740
+ /**
741
+ * Signed weight delta applied to the edge.
742
+ * Positive = potentiation (LTP), negative = depression (LTD).
743
+ */
744
+ deltaW: real('delta_w').notNull(),
745
+ /**
746
+ * STDP event kind: `ltp` (Long-Term Potentiation) or `ltd` (Long-Term
747
+ * Depression).
748
+ */
749
+ kind: text('kind', { enum: ['ltp', 'ltd'] }).notNull(),
750
+ /** ISO 8601 timestamp when this event was applied. */
751
+ timestamp: text('timestamp').notNull().default(sql`(datetime('now'))`),
752
+ /** Session ID that triggered the STDP pass, if available. */
753
+ sessionId: text('session_id'),
754
+ },
755
+ (table) => [
756
+ index('idx_plasticity_source').on(table.sourceNode),
757
+ index('idx_plasticity_target').on(table.targetNode),
758
+ index('idx_plasticity_timestamp').on(table.timestamp),
759
+ index('idx_plasticity_session').on(table.sessionId),
760
+ index('idx_plasticity_kind').on(table.kind),
761
+ ],
762
+ );
763
+
716
764
  // === TYPE EXPORTS ===
717
765
 
718
766
  export type BrainRetrievalLogRow = typeof brainRetrievalLog.$inferSelect;
@@ -733,4 +781,6 @@ export type BrainPageEdgeRow = typeof brainPageEdges.$inferSelect;
733
781
  export type NewBrainPageEdgeRow = typeof brainPageEdges.$inferInsert;
734
782
  export type BrainStickyNoteRow = typeof brainStickyNotes.$inferSelect;
735
783
  export type NewBrainStickyNoteRow = typeof brainStickyNotes.$inferInsert;
784
+ export type BrainPlasticityEventRow = typeof brainPlasticityEvents.$inferSelect;
785
+ export type NewBrainPlasticityEventRow = typeof brainPlasticityEvents.$inferInsert;
736
786
  // BrainNodeType and BrainEdgeType are declared alongside their enum arrays above.
@@ -158,6 +158,23 @@ function runBrainMigrations(
158
158
  // all migrations as applied without actually running them (Scenario 2 race).
159
159
  // ensureColumns is idempotent — no-op if the column already exists.
160
160
  ensureColumns(nativeDb, 'brain_observations', [{ name: 'agent', ddl: 'text' }], 'brain');
161
+
162
+ // T626-M1: Normalize co_retrieved edge type — idempotent safety-net UPDATE.
163
+ // The shipped Hebbian strengthener emitted edge_type = 'relates_to' instead of
164
+ // 'co_retrieved'. Relabel only rows from the consolidation provenance so no
165
+ // semantic edges are affected. The Drizzle migration file does the same UPDATE;
166
+ // this guard handles installs where the journal reconciler already marked
167
+ // the migration applied before the SQL ran.
168
+ if (tableExists(nativeDb, 'brain_page_edges')) {
169
+ nativeDb
170
+ .prepare(
171
+ `UPDATE brain_page_edges
172
+ SET edge_type = 'co_retrieved'
173
+ WHERE edge_type = 'relates_to'
174
+ AND provenance LIKE 'consolidation:%'`,
175
+ )
176
+ .run();
177
+ }
161
178
  }
162
179
 
163
180
  /**