@cleocode/core 2026.4.37 → 2026.4.39
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/hooks/handlers/task-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/task-hooks.js +11 -0
- package/dist/hooks/handlers/task-hooks.js.map +1 -1
- package/dist/index.js +1048 -33
- package/dist/index.js.map +4 -4
- package/dist/internal.d.ts +3 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -1
- package/dist/internal.js.map +1 -1
- package/dist/memory/brain-lifecycle.d.ts +2 -0
- package/dist/memory/brain-lifecycle.d.ts.map +1 -1
- package/dist/memory/decisions.d.ts.map +1 -1
- package/dist/memory/decisions.js +18 -0
- package/dist/memory/decisions.js.map +1 -1
- package/dist/memory/engine-compat.d.ts +17 -0
- package/dist/memory/engine-compat.d.ts.map +1 -1
- package/dist/memory/engine-compat.js +36 -0
- package/dist/memory/engine-compat.js.map +1 -1
- package/dist/memory/graph-memory-bridge.d.ts +158 -0
- package/dist/memory/graph-memory-bridge.d.ts.map +1 -0
- package/dist/memory/graph-memory-bridge.js +519 -0
- package/dist/memory/graph-memory-bridge.js.map +1 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +2 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/learnings.d.ts.map +1 -1
- package/dist/memory/learnings.js +18 -0
- package/dist/memory/learnings.js.map +1 -1
- package/dist/memory/llm-extraction.js.map +1 -1
- package/dist/memory/patterns.d.ts.map +1 -1
- package/dist/memory/patterns.js +18 -0
- package/dist/memory/patterns.js.map +1 -1
- package/dist/memory/quality-feedback.d.ts +129 -0
- package/dist/memory/quality-feedback.d.ts.map +1 -0
- package/dist/memory/quality-feedback.js +449 -0
- package/dist/memory/quality-feedback.js.map +1 -0
- package/dist/memory/sleep-consolidation.d.ts +98 -0
- package/dist/memory/sleep-consolidation.d.ts.map +1 -0
- package/dist/memory/sleep-consolidation.js +706 -0
- package/dist/memory/sleep-consolidation.js.map +1 -0
- package/dist/memory/temporal-supersession.d.ts +155 -0
- package/dist/memory/temporal-supersession.d.ts.map +1 -0
- package/dist/memory/temporal-supersession.js +406 -0
- package/dist/memory/temporal-supersession.js.map +1 -0
- package/dist/tasks/complete.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/hooks/handlers/task-hooks.ts +11 -0
- package/src/internal.ts +12 -0
- package/src/memory/__tests__/graph-memory-bridge.test.ts +357 -0
- package/src/memory/__tests__/llm-extraction.test.ts +17 -0
- package/src/memory/__tests__/quality-feedback.test.ts +418 -0
- package/src/memory/__tests__/sleep-consolidation.test.ts +790 -0
- package/src/memory/__tests__/temporal-supersession.test.ts +534 -0
- package/src/memory/brain-lifecycle.ts +13 -0
- package/src/memory/decisions.ts +24 -0
- package/src/memory/engine-compat.ts +37 -0
- package/src/memory/graph-memory-bridge.ts +751 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/learnings.ts +24 -0
- package/src/memory/patterns.ts +24 -0
- package/src/memory/quality-feedback.ts +640 -0
- package/src/memory/sleep-consolidation.ts +932 -0
- package/src/memory/temporal-supersession.ts +568 -0
- package/src/store/__tests__/performance-safety.test.ts +4 -4
- package/src/tasks/complete.ts +20 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Memory Bridge — connects brain.db memory nodes to nexus.db code nodes.
|
|
3
|
+
*
|
|
4
|
+
* Scans brain observations, decisions, patterns, and learnings for entity
|
|
5
|
+
* references (file paths, function names, symbol names) and matches them
|
|
6
|
+
* against nexus_nodes in the global nexus.db. Matching pairs are linked via
|
|
7
|
+
* `code_reference` edges written to brain_page_edges in brain.db.
|
|
8
|
+
*
|
|
9
|
+
* Design constraints:
|
|
10
|
+
* - brain.db is read-write (edges written here).
|
|
11
|
+
* - nexus.db is READ-ONLY from this module — never mutated.
|
|
12
|
+
* - All operations are BEST-EFFORT; failures never surface to callers.
|
|
13
|
+
* - Cross-DB join is handled in-process: read nexus nodes, then write brain edges.
|
|
14
|
+
* - Entity matching: exact match on filePath or symbol name; fuzzy match on
|
|
15
|
+
* symbol name (case-insensitive substring, minimum 4 chars).
|
|
16
|
+
*
|
|
17
|
+
* @task graph-memory-bridge
|
|
18
|
+
* @epic T523
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { BrainNodeType } from '../store/brain-schema.js';
|
|
22
|
+
import { brainPageEdges, brainPageNodes } from '../store/brain-schema.js';
|
|
23
|
+
import { getBrainDb, getBrainNativeDb } from '../store/brain-sqlite.js';
|
|
24
|
+
import { getNexusDb, getNexusNativeDb } from '../store/nexus-sqlite.js';
|
|
25
|
+
import { typedAll } from '../store/typed-query.js';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Public result types
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** A single code-reference link created or found by the bridge. */
|
|
32
|
+
export interface CodeReferenceLink {
|
|
33
|
+
/** Brain memory node ID (format: '<type>:<source-id>'). */
|
|
34
|
+
brainNodeId: string;
|
|
35
|
+
/** Nexus node ID (format: '<filePath>::<name>' or '<filePath>'). */
|
|
36
|
+
nexusNodeId: string;
|
|
37
|
+
/** Human-readable nexus node label. */
|
|
38
|
+
nexusLabel: string;
|
|
39
|
+
/** Match strategy used: 'exact-file', 'exact-symbol', or 'fuzzy-symbol'. */
|
|
40
|
+
matchStrategy: 'exact-file' | 'exact-symbol' | 'fuzzy-symbol';
|
|
41
|
+
/** Edge weight (exact matches = 1.0, fuzzy = 0.6). */
|
|
42
|
+
weight: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Summary result from autoLinkMemories. */
|
|
46
|
+
export interface AutoLinkResult {
|
|
47
|
+
/** Total brain entries scanned for entity references. */
|
|
48
|
+
scanned: number;
|
|
49
|
+
/** Number of new code_reference edges created. */
|
|
50
|
+
linked: number;
|
|
51
|
+
/** Number of links that already existed (skipped). */
|
|
52
|
+
alreadyLinked: number;
|
|
53
|
+
/** Individual links created in this run. */
|
|
54
|
+
links: CodeReferenceLink[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Result from queryMemoriesForCode. */
|
|
58
|
+
export interface MemoriesForCodeResult {
|
|
59
|
+
/** The nexus node ID that was queried. */
|
|
60
|
+
nexusNodeId: string;
|
|
61
|
+
/** Brain memory nodes reachable from this code node. */
|
|
62
|
+
memories: Array<{
|
|
63
|
+
nodeId: string;
|
|
64
|
+
nodeType: string;
|
|
65
|
+
label: string;
|
|
66
|
+
qualityScore: number;
|
|
67
|
+
edgeWeight: number;
|
|
68
|
+
matchStrategy: string;
|
|
69
|
+
}>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Result from queryCodeForMemory. */
|
|
73
|
+
export interface CodeForMemoryResult {
|
|
74
|
+
/** The brain memory node ID that was queried. */
|
|
75
|
+
brainNodeId: string;
|
|
76
|
+
/** Nexus code nodes reachable from this memory node. */
|
|
77
|
+
codeNodes: Array<{
|
|
78
|
+
nexusNodeId: string;
|
|
79
|
+
label: string;
|
|
80
|
+
filePath: string | null;
|
|
81
|
+
kind: string;
|
|
82
|
+
edgeWeight: number;
|
|
83
|
+
matchStrategy: string;
|
|
84
|
+
}>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Internal raw row types
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
interface RawBrainNode {
|
|
92
|
+
id: string;
|
|
93
|
+
node_type: string;
|
|
94
|
+
label: string;
|
|
95
|
+
quality_score: number;
|
|
96
|
+
metadata_json: string | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface RawNexusNode {
|
|
100
|
+
id: string;
|
|
101
|
+
label: string;
|
|
102
|
+
name: string | null;
|
|
103
|
+
file_path: string | null;
|
|
104
|
+
kind: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Internal helpers
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Regex patterns to extract entity references from text.
|
|
113
|
+
*
|
|
114
|
+
* Matches:
|
|
115
|
+
* - File paths: relative paths ending in known source extensions
|
|
116
|
+
* - Function/symbol names: camelCase, PascalCase, snake_case identifiers (≥4 chars)
|
|
117
|
+
*/
|
|
118
|
+
const FILE_PATH_PATTERN =
|
|
119
|
+
/(?:^|\s|['"`(])([a-zA-Z0-9_\-./]+\.(?:ts|tsx|js|jsx|rs|go|py|mjs|cjs))(?:$|\s|['"`)])/g;
|
|
120
|
+
|
|
121
|
+
const SYMBOL_PATTERN =
|
|
122
|
+
/\b([a-zA-Z_][a-zA-Z0-9_]*(?:[A-Z][a-zA-Z0-9_]*)+|[a-zA-Z_]{4,}[a-zA-Z0-9_]*)\b/g;
|
|
123
|
+
|
|
124
|
+
/** Common stop-words to skip in symbol extraction (short-circuits false positives). */
|
|
125
|
+
const SYMBOL_STOP_WORDS = new Set([
|
|
126
|
+
'true',
|
|
127
|
+
'false',
|
|
128
|
+
'null',
|
|
129
|
+
'undefined',
|
|
130
|
+
'const',
|
|
131
|
+
'async',
|
|
132
|
+
'await',
|
|
133
|
+
'return',
|
|
134
|
+
'export',
|
|
135
|
+
'import',
|
|
136
|
+
'from',
|
|
137
|
+
'type',
|
|
138
|
+
'interface',
|
|
139
|
+
'function',
|
|
140
|
+
'class',
|
|
141
|
+
'this',
|
|
142
|
+
'super',
|
|
143
|
+
'extends',
|
|
144
|
+
'implements',
|
|
145
|
+
'with',
|
|
146
|
+
'that',
|
|
147
|
+
'then',
|
|
148
|
+
'when',
|
|
149
|
+
'have',
|
|
150
|
+
'been',
|
|
151
|
+
'will',
|
|
152
|
+
'should',
|
|
153
|
+
'could',
|
|
154
|
+
'would',
|
|
155
|
+
'error',
|
|
156
|
+
'result',
|
|
157
|
+
'value',
|
|
158
|
+
'data',
|
|
159
|
+
'info',
|
|
160
|
+
'note',
|
|
161
|
+
'todo',
|
|
162
|
+
'done',
|
|
163
|
+
'fail',
|
|
164
|
+
'pass',
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Extract file paths from text.
|
|
169
|
+
*/
|
|
170
|
+
function extractFilePaths(text: string): string[] {
|
|
171
|
+
const paths = new Set<string>();
|
|
172
|
+
for (const m of text.matchAll(FILE_PATH_PATTERN)) {
|
|
173
|
+
const p = m[1];
|
|
174
|
+
if (p) paths.add(p);
|
|
175
|
+
}
|
|
176
|
+
return Array.from(paths);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extract potential symbol names from text (camelCase / PascalCase / snake_case ≥ 4 chars).
|
|
181
|
+
*/
|
|
182
|
+
function extractSymbolCandidates(text: string): string[] {
|
|
183
|
+
const syms = new Set<string>();
|
|
184
|
+
for (const m of text.matchAll(SYMBOL_PATTERN)) {
|
|
185
|
+
const s = m[1];
|
|
186
|
+
if (s && s.length >= 4 && !SYMBOL_STOP_WORDS.has(s.toLowerCase())) {
|
|
187
|
+
syms.add(s);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return Array.from(syms);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Build a plain-text corpus from a brain_page_nodes metadata_json blob.
|
|
195
|
+
* Returns empty string if metadata is absent or malformed.
|
|
196
|
+
*/
|
|
197
|
+
function metadataText(metaJson: string | null): string {
|
|
198
|
+
if (!metaJson) return '';
|
|
199
|
+
try {
|
|
200
|
+
const obj = JSON.parse(metaJson) as Record<string, unknown>;
|
|
201
|
+
return Object.values(obj)
|
|
202
|
+
.filter((v) => typeof v === 'string')
|
|
203
|
+
.join(' ');
|
|
204
|
+
} catch {
|
|
205
|
+
return '';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// linkMemoryToCode — manual single-link creation
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a `code_reference` edge from a brain memory node to a nexus code node.
|
|
215
|
+
*
|
|
216
|
+
* Writes to brain_page_edges (brain.db). The nexus node must already exist in
|
|
217
|
+
* nexus.db but is never mutated. The brain node is upserted as a stub if it
|
|
218
|
+
* does not yet exist in brain_page_nodes.
|
|
219
|
+
*
|
|
220
|
+
* This function is idempotent — calling it multiple times with the same
|
|
221
|
+
* (memoryId, codeSymbol) pair is safe (the composite PK prevents duplicates).
|
|
222
|
+
*
|
|
223
|
+
* @param projectRoot - Absolute path to project root (locates brain.db)
|
|
224
|
+
* @param memoryId - Brain memory node ID (format: '<type>:<source-id>')
|
|
225
|
+
* @param codeSymbol - Nexus node ID (format: '<filePath>::<name>' or '<filePath>')
|
|
226
|
+
* @returns True if the edge was created or already existed; false on error
|
|
227
|
+
*/
|
|
228
|
+
export async function linkMemoryToCode(
|
|
229
|
+
projectRoot: string,
|
|
230
|
+
memoryId: string,
|
|
231
|
+
codeSymbol: string,
|
|
232
|
+
): Promise<boolean> {
|
|
233
|
+
try {
|
|
234
|
+
const brainDb = await getBrainDb(projectRoot);
|
|
235
|
+
|
|
236
|
+
// Ensure nexus.db is initialized so we can verify the target node exists
|
|
237
|
+
await getNexusDb();
|
|
238
|
+
const nexusNative = getNexusNativeDb();
|
|
239
|
+
|
|
240
|
+
if (!nexusNative) return false;
|
|
241
|
+
|
|
242
|
+
// Verify the nexus node exists (read-only check)
|
|
243
|
+
const nexusNode = nexusNative
|
|
244
|
+
.prepare('SELECT id, label, file_path, kind FROM nexus_nodes WHERE id = ? LIMIT 1')
|
|
245
|
+
.get(codeSymbol) as
|
|
246
|
+
| { id: string; label: string; file_path: string | null; kind: string }
|
|
247
|
+
| undefined;
|
|
248
|
+
|
|
249
|
+
if (!nexusNode) return false;
|
|
250
|
+
|
|
251
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
252
|
+
|
|
253
|
+
// Upsert the memory node stub in brain_page_nodes so the edge FK is satisfied
|
|
254
|
+
// (the real node may already exist from graph-auto-populate; onConflictDoUpdate
|
|
255
|
+
// only refreshes lastActivityAt if the node already exists).
|
|
256
|
+
const idParts = memoryId.split(':');
|
|
257
|
+
const nodeType = (idParts[0] as BrainNodeType) ?? 'observation';
|
|
258
|
+
|
|
259
|
+
await brainDb
|
|
260
|
+
.insert(brainPageNodes)
|
|
261
|
+
.values({
|
|
262
|
+
id: memoryId,
|
|
263
|
+
nodeType,
|
|
264
|
+
label: memoryId,
|
|
265
|
+
qualityScore: 0.5,
|
|
266
|
+
contentHash: null,
|
|
267
|
+
lastActivityAt: now,
|
|
268
|
+
createdAt: now,
|
|
269
|
+
updatedAt: now,
|
|
270
|
+
})
|
|
271
|
+
.onConflictDoUpdate({
|
|
272
|
+
target: brainPageNodes.id,
|
|
273
|
+
set: { lastActivityAt: now, updatedAt: now },
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Write the code_reference edge
|
|
277
|
+
// Cast required: drizzle's enum type may not yet include 'code_reference'
|
|
278
|
+
// in the compiled .d.ts (the schema source was updated but not yet built).
|
|
279
|
+
await brainDb
|
|
280
|
+
.insert(brainPageEdges)
|
|
281
|
+
.values({
|
|
282
|
+
fromId: memoryId,
|
|
283
|
+
toId: codeSymbol,
|
|
284
|
+
edgeType: 'code_reference' as import('../store/brain-schema.js').BrainEdgeType,
|
|
285
|
+
weight: 1.0,
|
|
286
|
+
provenance: 'manual',
|
|
287
|
+
createdAt: now,
|
|
288
|
+
})
|
|
289
|
+
.onConflictDoNothing();
|
|
290
|
+
|
|
291
|
+
return true;
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.warn('[graph-memory-bridge] linkMemoryToCode failed:', err);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// autoLinkMemories — scan brain nodes and link to nexus matches
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Scan brain memory nodes for entity references and match them against nexus.
|
|
304
|
+
*
|
|
305
|
+
* For each brain node, extracts:
|
|
306
|
+
* - File path references → matched against nexus_nodes.file_path (exact)
|
|
307
|
+
* - Symbol name references → matched against nexus_nodes.name (exact, then fuzzy)
|
|
308
|
+
*
|
|
309
|
+
* Matching edges are written to brain_page_edges with edgeType='code_reference'.
|
|
310
|
+
* This function is idempotent — existing edges are skipped.
|
|
311
|
+
*
|
|
312
|
+
* Should be called from runConsolidation() as a best-effort step. All errors
|
|
313
|
+
* are caught and logged; never throws.
|
|
314
|
+
*
|
|
315
|
+
* @param projectRoot - Absolute path to project root (locates brain.db)
|
|
316
|
+
* @returns Summary of scanned entries and created links
|
|
317
|
+
*/
|
|
318
|
+
export async function autoLinkMemories(projectRoot: string): Promise<AutoLinkResult> {
|
|
319
|
+
const result: AutoLinkResult = { scanned: 0, linked: 0, alreadyLinked: 0, links: [] };
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
await getBrainDb(projectRoot);
|
|
323
|
+
const brainNative = getBrainNativeDb();
|
|
324
|
+
|
|
325
|
+
await getNexusDb();
|
|
326
|
+
const nexusNative = getNexusNativeDb();
|
|
327
|
+
|
|
328
|
+
if (!brainNative || !nexusNative) return result;
|
|
329
|
+
|
|
330
|
+
// Load all brain page nodes that are memory entity types
|
|
331
|
+
const brainNodes = typedAll<RawBrainNode>(
|
|
332
|
+
brainNative.prepare(`
|
|
333
|
+
SELECT id, node_type, label, quality_score, metadata_json
|
|
334
|
+
FROM brain_page_nodes
|
|
335
|
+
WHERE node_type IN ('observation', 'decision', 'pattern', 'learning')
|
|
336
|
+
AND quality_score >= 0.3
|
|
337
|
+
ORDER BY quality_score DESC
|
|
338
|
+
LIMIT 500
|
|
339
|
+
`),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
result.scanned = brainNodes.length;
|
|
343
|
+
|
|
344
|
+
if (brainNodes.length === 0) return result;
|
|
345
|
+
|
|
346
|
+
// Load nexus nodes into memory (indexed by name and filePath for fast lookup).
|
|
347
|
+
// We load only essential columns to keep memory usage low.
|
|
348
|
+
const nexusNodes = typedAll<RawNexusNode>(
|
|
349
|
+
nexusNative.prepare(`
|
|
350
|
+
SELECT id, label, name, file_path, kind
|
|
351
|
+
FROM nexus_nodes
|
|
352
|
+
WHERE kind NOT IN ('community', 'process', 'folder')
|
|
353
|
+
LIMIT 20000
|
|
354
|
+
`),
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
if (nexusNodes.length === 0) return result;
|
|
358
|
+
|
|
359
|
+
// Build lookup indexes
|
|
360
|
+
const byFilePath = new Map<string, RawNexusNode[]>();
|
|
361
|
+
const byNameExact = new Map<string, RawNexusNode[]>();
|
|
362
|
+
const byNameLower = new Map<string, RawNexusNode[]>();
|
|
363
|
+
|
|
364
|
+
for (const node of nexusNodes) {
|
|
365
|
+
if (node.file_path) {
|
|
366
|
+
const fp = node.file_path.toLowerCase();
|
|
367
|
+
const existing = byFilePath.get(fp) ?? [];
|
|
368
|
+
existing.push(node);
|
|
369
|
+
byFilePath.set(fp, existing);
|
|
370
|
+
}
|
|
371
|
+
if (node.name) {
|
|
372
|
+
// Exact (case-sensitive)
|
|
373
|
+
const exact = byNameExact.get(node.name) ?? [];
|
|
374
|
+
exact.push(node);
|
|
375
|
+
byNameExact.set(node.name, exact);
|
|
376
|
+
// Lowercase for fuzzy
|
|
377
|
+
const lower = node.name.toLowerCase();
|
|
378
|
+
const fuzzy = byNameLower.get(lower) ?? [];
|
|
379
|
+
fuzzy.push(node);
|
|
380
|
+
byNameLower.set(lower, fuzzy);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
385
|
+
|
|
386
|
+
// Load existing code_reference edges to avoid duplicates
|
|
387
|
+
const existingEdges = new Set<string>();
|
|
388
|
+
const rawEdges = typedAll<{ from_id: string; to_id: string }>(
|
|
389
|
+
brainNative.prepare(`
|
|
390
|
+
SELECT from_id, to_id FROM brain_page_edges WHERE edge_type = 'code_reference'
|
|
391
|
+
`),
|
|
392
|
+
);
|
|
393
|
+
for (const e of rawEdges) {
|
|
394
|
+
existingEdges.add(`${e.from_id}|${e.to_id}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Process each brain node
|
|
398
|
+
for (const brainNode of brainNodes) {
|
|
399
|
+
const corpus = `${brainNode.label} ${metadataText(brainNode.metadata_json)}`;
|
|
400
|
+
|
|
401
|
+
const filePaths = extractFilePaths(corpus);
|
|
402
|
+
const symbolCandidates = extractSymbolCandidates(corpus);
|
|
403
|
+
|
|
404
|
+
const candidates: Array<{
|
|
405
|
+
nexusNode: RawNexusNode;
|
|
406
|
+
strategy: CodeReferenceLink['matchStrategy'];
|
|
407
|
+
weight: number;
|
|
408
|
+
}> = [];
|
|
409
|
+
|
|
410
|
+
// 1. Exact file path matches
|
|
411
|
+
for (const fp of filePaths) {
|
|
412
|
+
const matches = byFilePath.get(fp.toLowerCase());
|
|
413
|
+
if (matches) {
|
|
414
|
+
for (const n of matches) {
|
|
415
|
+
candidates.push({ nexusNode: n, strategy: 'exact-file', weight: 1.0 });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 2. Exact symbol name matches
|
|
421
|
+
for (const sym of symbolCandidates) {
|
|
422
|
+
const exactMatches = byNameExact.get(sym);
|
|
423
|
+
if (exactMatches) {
|
|
424
|
+
for (const n of exactMatches) {
|
|
425
|
+
candidates.push({ nexusNode: n, strategy: 'exact-symbol', weight: 1.0 });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 3. Fuzzy (case-insensitive) symbol matches — only for symbols not already exact-matched
|
|
431
|
+
const exactSymSet = new Set(
|
|
432
|
+
symbolCandidates.flatMap((s) => byNameExact.get(s) ?? []).map((n) => n.id),
|
|
433
|
+
);
|
|
434
|
+
for (const sym of symbolCandidates) {
|
|
435
|
+
if (sym.length < 5) continue; // skip very short symbols for fuzzy
|
|
436
|
+
const lower = sym.toLowerCase();
|
|
437
|
+
const fuzzyMatches = byNameLower.get(lower);
|
|
438
|
+
if (fuzzyMatches) {
|
|
439
|
+
for (const n of fuzzyMatches) {
|
|
440
|
+
if (!exactSymSet.has(n.id)) {
|
|
441
|
+
candidates.push({ nexusNode: n, strategy: 'fuzzy-symbol', weight: 0.6 });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Deduplicate candidates (keep highest weight per nexus node)
|
|
448
|
+
const bestByNexusId = new Map<string, (typeof candidates)[number]>();
|
|
449
|
+
for (const c of candidates) {
|
|
450
|
+
const existing = bestByNexusId.get(c.nexusNode.id);
|
|
451
|
+
if (!existing || c.weight > existing.weight) {
|
|
452
|
+
bestByNexusId.set(c.nexusNode.id, c);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Write edges (cap at 10 per brain node to avoid noise)
|
|
457
|
+
const sortedCandidates = Array.from(bestByNexusId.values())
|
|
458
|
+
.sort((a, b) => b.weight - a.weight)
|
|
459
|
+
.slice(0, 10);
|
|
460
|
+
|
|
461
|
+
for (const { nexusNode, strategy, weight } of sortedCandidates) {
|
|
462
|
+
const edgeKey = `${brainNode.id}|${nexusNode.id}`;
|
|
463
|
+
|
|
464
|
+
if (existingEdges.has(edgeKey)) {
|
|
465
|
+
result.alreadyLinked++;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Upsert brain node stub (idempotent — real node may already exist)
|
|
470
|
+
brainNative
|
|
471
|
+
.prepare(`
|
|
472
|
+
INSERT OR IGNORE INTO brain_page_nodes
|
|
473
|
+
(id, node_type, label, quality_score, content_hash, metadata_json, last_activity_at, created_at, updated_at)
|
|
474
|
+
VALUES (?, ?, ?, ?, NULL, NULL, ?, ?, ?)
|
|
475
|
+
`)
|
|
476
|
+
.run(
|
|
477
|
+
brainNode.id,
|
|
478
|
+
brainNode.node_type,
|
|
479
|
+
brainNode.label,
|
|
480
|
+
brainNode.quality_score,
|
|
481
|
+
now,
|
|
482
|
+
now,
|
|
483
|
+
now,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// Write edge
|
|
487
|
+
try {
|
|
488
|
+
brainNative
|
|
489
|
+
.prepare(`
|
|
490
|
+
INSERT OR IGNORE INTO brain_page_edges
|
|
491
|
+
(from_id, to_id, edge_type, weight, provenance, created_at)
|
|
492
|
+
VALUES (?, ?, 'code_reference', ?, ?, ?)
|
|
493
|
+
`)
|
|
494
|
+
.run(brainNode.id, nexusNode.id, weight, `auto:${strategy}`, now);
|
|
495
|
+
|
|
496
|
+
existingEdges.add(edgeKey);
|
|
497
|
+
result.linked++;
|
|
498
|
+
result.links.push({
|
|
499
|
+
brainNodeId: brainNode.id,
|
|
500
|
+
nexusNodeId: nexusNode.id,
|
|
501
|
+
nexusLabel: nexusNode.label,
|
|
502
|
+
matchStrategy: strategy,
|
|
503
|
+
weight,
|
|
504
|
+
});
|
|
505
|
+
} catch (edgeErr) {
|
|
506
|
+
console.warn('[graph-memory-bridge] edge insert failed:', edgeErr);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} catch (err) {
|
|
511
|
+
console.warn('[graph-memory-bridge] autoLinkMemories failed:', err);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// queryMemoriesForCode — find memories related to a code symbol
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Given a code symbol (nexus node ID), find related brain memory nodes.
|
|
523
|
+
*
|
|
524
|
+
* Traverses `code_reference` edges in brain_page_edges where the target is
|
|
525
|
+
* the given nexus node ID. Returns the brain memory nodes with edge metadata.
|
|
526
|
+
*
|
|
527
|
+
* @param projectRoot - Absolute path to project root (locates brain.db)
|
|
528
|
+
* @param symbol - Nexus node ID (format: '<filePath>::<name>' or '<filePath>')
|
|
529
|
+
* @returns Memory nodes that reference the given code symbol
|
|
530
|
+
*/
|
|
531
|
+
export async function queryMemoriesForCode(
|
|
532
|
+
projectRoot: string,
|
|
533
|
+
symbol: string,
|
|
534
|
+
): Promise<MemoriesForCodeResult> {
|
|
535
|
+
const result: MemoriesForCodeResult = { nexusNodeId: symbol, memories: [] };
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
await getBrainDb(projectRoot);
|
|
539
|
+
const brainNative = getBrainNativeDb();
|
|
540
|
+
|
|
541
|
+
if (!brainNative) return result;
|
|
542
|
+
|
|
543
|
+
interface RawRow {
|
|
544
|
+
id: string;
|
|
545
|
+
node_type: string;
|
|
546
|
+
label: string;
|
|
547
|
+
quality_score: number;
|
|
548
|
+
weight: number;
|
|
549
|
+
provenance: string | null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const rows = typedAll<RawRow>(
|
|
553
|
+
brainNative.prepare(`
|
|
554
|
+
SELECT n.id, n.node_type, n.label, n.quality_score,
|
|
555
|
+
e.weight, e.provenance
|
|
556
|
+
FROM brain_page_edges e
|
|
557
|
+
JOIN brain_page_nodes n ON n.id = e.from_id
|
|
558
|
+
WHERE e.to_id = ?
|
|
559
|
+
AND e.edge_type = 'code_reference'
|
|
560
|
+
ORDER BY e.weight DESC, n.quality_score DESC
|
|
561
|
+
LIMIT 50
|
|
562
|
+
`),
|
|
563
|
+
symbol,
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
result.memories = rows.map((r) => ({
|
|
567
|
+
nodeId: r.id,
|
|
568
|
+
nodeType: r.node_type,
|
|
569
|
+
label: r.label,
|
|
570
|
+
qualityScore: r.quality_score,
|
|
571
|
+
edgeWeight: r.weight,
|
|
572
|
+
matchStrategy: r.provenance?.replace('auto:', '') ?? 'manual',
|
|
573
|
+
}));
|
|
574
|
+
} catch (err) {
|
|
575
|
+
console.warn('[graph-memory-bridge] queryMemoriesForCode failed:', err);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
// queryCodeForMemory — find code nodes related to a memory entry
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Given a brain memory node ID, find related nexus code nodes.
|
|
587
|
+
*
|
|
588
|
+
* Traverses `code_reference` edges in brain_page_edges from the given memory
|
|
589
|
+
* node ID, then fetches the corresponding nexus node metadata.
|
|
590
|
+
*
|
|
591
|
+
* @param projectRoot - Absolute path to project root (locates brain.db)
|
|
592
|
+
* @param memoryId - Brain memory node ID (format: '<type>:<source-id>')
|
|
593
|
+
* @returns Nexus code nodes referenced by the given memory entry
|
|
594
|
+
*/
|
|
595
|
+
export async function queryCodeForMemory(
|
|
596
|
+
projectRoot: string,
|
|
597
|
+
memoryId: string,
|
|
598
|
+
): Promise<CodeForMemoryResult> {
|
|
599
|
+
const result: CodeForMemoryResult = { brainNodeId: memoryId, codeNodes: [] };
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
await getBrainDb(projectRoot);
|
|
603
|
+
const brainNative = getBrainNativeDb();
|
|
604
|
+
|
|
605
|
+
await getNexusDb();
|
|
606
|
+
const nexusNative = getNexusNativeDb();
|
|
607
|
+
|
|
608
|
+
if (!brainNative || !nexusNative) return result;
|
|
609
|
+
|
|
610
|
+
// Get all code_reference edge targets from brain.db
|
|
611
|
+
interface RawBrainEdgeRow {
|
|
612
|
+
to_id: string;
|
|
613
|
+
weight: number;
|
|
614
|
+
provenance: string | null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const brainEdges = typedAll<RawBrainEdgeRow>(
|
|
618
|
+
brainNative.prepare(`
|
|
619
|
+
SELECT to_id, weight, provenance
|
|
620
|
+
FROM brain_page_edges
|
|
621
|
+
WHERE from_id = ?
|
|
622
|
+
AND edge_type = 'code_reference'
|
|
623
|
+
ORDER BY weight DESC
|
|
624
|
+
LIMIT 50
|
|
625
|
+
`),
|
|
626
|
+
memoryId,
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
if (brainEdges.length === 0) return result;
|
|
630
|
+
|
|
631
|
+
// Fetch nexus node metadata for each target (read-only)
|
|
632
|
+
for (const edge of brainEdges) {
|
|
633
|
+
const nexusNode = nexusNative
|
|
634
|
+
.prepare('SELECT id, label, file_path, kind FROM nexus_nodes WHERE id = ? LIMIT 1')
|
|
635
|
+
.get(edge.to_id) as
|
|
636
|
+
| { id: string; label: string; file_path: string | null; kind: string }
|
|
637
|
+
| undefined;
|
|
638
|
+
|
|
639
|
+
if (nexusNode) {
|
|
640
|
+
result.codeNodes.push({
|
|
641
|
+
nexusNodeId: nexusNode.id,
|
|
642
|
+
label: nexusNode.label,
|
|
643
|
+
filePath: nexusNode.file_path,
|
|
644
|
+
kind: nexusNode.kind,
|
|
645
|
+
edgeWeight: edge.weight,
|
|
646
|
+
matchStrategy: edge.provenance?.replace('auto:', '') ?? 'manual',
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
} catch (err) {
|
|
651
|
+
console.warn('[graph-memory-bridge] queryCodeForMemory failed:', err);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return result;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
// listCodeLinks — show all code↔memory connections
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
660
|
+
|
|
661
|
+
/** A single code-memory link for display. */
|
|
662
|
+
export interface CodeLinkEntry {
|
|
663
|
+
/** Brain memory node ID. */
|
|
664
|
+
brainNodeId: string;
|
|
665
|
+
/** Brain node type. */
|
|
666
|
+
brainNodeType: string;
|
|
667
|
+
/** Brain node label. */
|
|
668
|
+
brainNodeLabel: string;
|
|
669
|
+
/** Nexus code node ID. */
|
|
670
|
+
nexusNodeId: string;
|
|
671
|
+
/** Nexus node label. */
|
|
672
|
+
nexusNodeLabel: string;
|
|
673
|
+
/** File path in the nexus node (relative to project root). */
|
|
674
|
+
filePath: string | null;
|
|
675
|
+
/** Code kind (function, class, file, etc.). */
|
|
676
|
+
kind: string;
|
|
677
|
+
/** Edge weight. */
|
|
678
|
+
weight: number;
|
|
679
|
+
/** When the edge was created. */
|
|
680
|
+
createdAt: string;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Return all `code_reference` edges from brain.db enriched with nexus metadata.
|
|
685
|
+
*
|
|
686
|
+
* Used by `cleo memory code-links` CLI command.
|
|
687
|
+
*
|
|
688
|
+
* @param projectRoot - Absolute path to project root (locates brain.db)
|
|
689
|
+
* @param limit - Maximum number of entries to return (default 100)
|
|
690
|
+
* @returns Array of code link entries sorted by weight descending
|
|
691
|
+
*/
|
|
692
|
+
export async function listCodeLinks(projectRoot: string, limit = 100): Promise<CodeLinkEntry[]> {
|
|
693
|
+
const entries: CodeLinkEntry[] = [];
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
await getBrainDb(projectRoot);
|
|
697
|
+
const brainNative = getBrainNativeDb();
|
|
698
|
+
|
|
699
|
+
await getNexusDb();
|
|
700
|
+
const nexusNative = getNexusNativeDb();
|
|
701
|
+
|
|
702
|
+
if (!brainNative || !nexusNative) return entries;
|
|
703
|
+
|
|
704
|
+
// Fetch all code_reference edges with brain node metadata
|
|
705
|
+
interface RawRow {
|
|
706
|
+
from_id: string;
|
|
707
|
+
to_id: string;
|
|
708
|
+
weight: number;
|
|
709
|
+
created_at: string;
|
|
710
|
+
node_type: string;
|
|
711
|
+
label: string;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const rows = typedAll<RawRow>(
|
|
715
|
+
brainNative.prepare(`
|
|
716
|
+
SELECT e.from_id, e.to_id, e.weight, e.created_at,
|
|
717
|
+
n.node_type, n.label
|
|
718
|
+
FROM brain_page_edges e
|
|
719
|
+
JOIN brain_page_nodes n ON n.id = e.from_id
|
|
720
|
+
WHERE e.edge_type = 'code_reference'
|
|
721
|
+
ORDER BY e.weight DESC, e.created_at DESC
|
|
722
|
+
LIMIT ?
|
|
723
|
+
`),
|
|
724
|
+
limit,
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
for (const row of rows) {
|
|
728
|
+
const nexusNode = nexusNative
|
|
729
|
+
.prepare('SELECT id, label, file_path, kind FROM nexus_nodes WHERE id = ? LIMIT 1')
|
|
730
|
+
.get(row.to_id) as
|
|
731
|
+
| { id: string; label: string; file_path: string | null; kind: string }
|
|
732
|
+
| undefined;
|
|
733
|
+
|
|
734
|
+
entries.push({
|
|
735
|
+
brainNodeId: row.from_id,
|
|
736
|
+
brainNodeType: row.node_type,
|
|
737
|
+
brainNodeLabel: row.label,
|
|
738
|
+
nexusNodeId: row.to_id,
|
|
739
|
+
nexusNodeLabel: nexusNode?.label ?? row.to_id,
|
|
740
|
+
filePath: nexusNode?.file_path ?? null,
|
|
741
|
+
kind: nexusNode?.kind ?? 'unknown',
|
|
742
|
+
weight: row.weight,
|
|
743
|
+
createdAt: row.created_at,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
} catch (err) {
|
|
747
|
+
console.warn('[graph-memory-bridge] listCodeLinks failed:', err);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return entries;
|
|
751
|
+
}
|