@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.
Files changed (66) hide show
  1. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  2. package/dist/hooks/handlers/task-hooks.js +11 -0
  3. package/dist/hooks/handlers/task-hooks.js.map +1 -1
  4. package/dist/index.js +1048 -33
  5. package/dist/index.js.map +4 -4
  6. package/dist/internal.d.ts +3 -1
  7. package/dist/internal.d.ts.map +1 -1
  8. package/dist/internal.js +3 -1
  9. package/dist/internal.js.map +1 -1
  10. package/dist/memory/brain-lifecycle.d.ts +2 -0
  11. package/dist/memory/brain-lifecycle.d.ts.map +1 -1
  12. package/dist/memory/decisions.d.ts.map +1 -1
  13. package/dist/memory/decisions.js +18 -0
  14. package/dist/memory/decisions.js.map +1 -1
  15. package/dist/memory/engine-compat.d.ts +17 -0
  16. package/dist/memory/engine-compat.d.ts.map +1 -1
  17. package/dist/memory/engine-compat.js +36 -0
  18. package/dist/memory/engine-compat.js.map +1 -1
  19. package/dist/memory/graph-memory-bridge.d.ts +158 -0
  20. package/dist/memory/graph-memory-bridge.d.ts.map +1 -0
  21. package/dist/memory/graph-memory-bridge.js +519 -0
  22. package/dist/memory/graph-memory-bridge.js.map +1 -0
  23. package/dist/memory/index.d.ts +1 -0
  24. package/dist/memory/index.d.ts.map +1 -1
  25. package/dist/memory/index.js +2 -0
  26. package/dist/memory/index.js.map +1 -1
  27. package/dist/memory/learnings.d.ts.map +1 -1
  28. package/dist/memory/learnings.js +18 -0
  29. package/dist/memory/learnings.js.map +1 -1
  30. package/dist/memory/llm-extraction.js.map +1 -1
  31. package/dist/memory/patterns.d.ts.map +1 -1
  32. package/dist/memory/patterns.js +18 -0
  33. package/dist/memory/patterns.js.map +1 -1
  34. package/dist/memory/quality-feedback.d.ts +129 -0
  35. package/dist/memory/quality-feedback.d.ts.map +1 -0
  36. package/dist/memory/quality-feedback.js +449 -0
  37. package/dist/memory/quality-feedback.js.map +1 -0
  38. package/dist/memory/sleep-consolidation.d.ts +98 -0
  39. package/dist/memory/sleep-consolidation.d.ts.map +1 -0
  40. package/dist/memory/sleep-consolidation.js +706 -0
  41. package/dist/memory/sleep-consolidation.js.map +1 -0
  42. package/dist/memory/temporal-supersession.d.ts +155 -0
  43. package/dist/memory/temporal-supersession.d.ts.map +1 -0
  44. package/dist/memory/temporal-supersession.js +406 -0
  45. package/dist/memory/temporal-supersession.js.map +1 -0
  46. package/dist/tasks/complete.d.ts.map +1 -1
  47. package/package.json +8 -8
  48. package/src/hooks/handlers/task-hooks.ts +11 -0
  49. package/src/internal.ts +12 -0
  50. package/src/memory/__tests__/graph-memory-bridge.test.ts +357 -0
  51. package/src/memory/__tests__/llm-extraction.test.ts +17 -0
  52. package/src/memory/__tests__/quality-feedback.test.ts +418 -0
  53. package/src/memory/__tests__/sleep-consolidation.test.ts +790 -0
  54. package/src/memory/__tests__/temporal-supersession.test.ts +534 -0
  55. package/src/memory/brain-lifecycle.ts +13 -0
  56. package/src/memory/decisions.ts +24 -0
  57. package/src/memory/engine-compat.ts +37 -0
  58. package/src/memory/graph-memory-bridge.ts +751 -0
  59. package/src/memory/index.ts +2 -0
  60. package/src/memory/learnings.ts +24 -0
  61. package/src/memory/patterns.ts +24 -0
  62. package/src/memory/quality-feedback.ts +640 -0
  63. package/src/memory/sleep-consolidation.ts +932 -0
  64. package/src/memory/temporal-supersession.ts +568 -0
  65. package/src/store/__tests__/performance-safety.test.ts +4 -4
  66. 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
+ }