@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
|
@@ -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
|
+
}
|
package/src/memory/decisions.ts
CHANGED
|
@@ -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];
|
package/src/memory/index.ts
CHANGED
|
@@ -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
|
/**
|