@cleocode/core 2026.4.37 → 2026.4.38

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 (61) 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 +644 -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/decisions.d.ts.map +1 -1
  11. package/dist/memory/decisions.js +18 -0
  12. package/dist/memory/decisions.js.map +1 -1
  13. package/dist/memory/engine-compat.d.ts +17 -0
  14. package/dist/memory/engine-compat.d.ts.map +1 -1
  15. package/dist/memory/engine-compat.js +36 -0
  16. package/dist/memory/engine-compat.js.map +1 -1
  17. package/dist/memory/graph-memory-bridge.d.ts +158 -0
  18. package/dist/memory/graph-memory-bridge.d.ts.map +1 -0
  19. package/dist/memory/graph-memory-bridge.js +519 -0
  20. package/dist/memory/graph-memory-bridge.js.map +1 -0
  21. package/dist/memory/index.d.ts +1 -0
  22. package/dist/memory/index.d.ts.map +1 -1
  23. package/dist/memory/index.js +2 -0
  24. package/dist/memory/index.js.map +1 -1
  25. package/dist/memory/learnings.d.ts.map +1 -1
  26. package/dist/memory/learnings.js +18 -0
  27. package/dist/memory/learnings.js.map +1 -1
  28. package/dist/memory/llm-extraction.js.map +1 -1
  29. package/dist/memory/patterns.d.ts.map +1 -1
  30. package/dist/memory/patterns.js +18 -0
  31. package/dist/memory/patterns.js.map +1 -1
  32. package/dist/memory/quality-feedback.d.ts +129 -0
  33. package/dist/memory/quality-feedback.d.ts.map +1 -0
  34. package/dist/memory/quality-feedback.js +449 -0
  35. package/dist/memory/quality-feedback.js.map +1 -0
  36. package/dist/memory/sleep-consolidation.d.ts +98 -0
  37. package/dist/memory/sleep-consolidation.d.ts.map +1 -0
  38. package/dist/memory/sleep-consolidation.js +706 -0
  39. package/dist/memory/sleep-consolidation.js.map +1 -0
  40. package/dist/memory/temporal-supersession.d.ts +155 -0
  41. package/dist/memory/temporal-supersession.d.ts.map +1 -0
  42. package/dist/memory/temporal-supersession.js +406 -0
  43. package/dist/memory/temporal-supersession.js.map +1 -0
  44. package/package.json +6 -6
  45. package/src/hooks/handlers/task-hooks.ts +11 -0
  46. package/src/internal.ts +12 -0
  47. package/src/memory/__tests__/graph-memory-bridge.test.ts +357 -0
  48. package/src/memory/__tests__/llm-extraction.test.ts +17 -0
  49. package/src/memory/__tests__/quality-feedback.test.ts +418 -0
  50. package/src/memory/__tests__/sleep-consolidation.test.ts +790 -0
  51. package/src/memory/__tests__/temporal-supersession.test.ts +534 -0
  52. package/src/memory/decisions.ts +24 -0
  53. package/src/memory/engine-compat.ts +37 -0
  54. package/src/memory/graph-memory-bridge.ts +751 -0
  55. package/src/memory/index.ts +2 -0
  56. package/src/memory/learnings.ts +24 -0
  57. package/src/memory/patterns.ts +24 -0
  58. package/src/memory/quality-feedback.ts +640 -0
  59. package/src/memory/sleep-consolidation.ts +932 -0
  60. package/src/memory/temporal-supersession.ts +568 -0
  61. package/src/store/__tests__/performance-safety.test.ts +4 -4
@@ -0,0 +1,158 @@
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
+ /** A single code-reference link created or found by the bridge. */
21
+ export interface CodeReferenceLink {
22
+ /** Brain memory node ID (format: '<type>:<source-id>'). */
23
+ brainNodeId: string;
24
+ /** Nexus node ID (format: '<filePath>::<name>' or '<filePath>'). */
25
+ nexusNodeId: string;
26
+ /** Human-readable nexus node label. */
27
+ nexusLabel: string;
28
+ /** Match strategy used: 'exact-file', 'exact-symbol', or 'fuzzy-symbol'. */
29
+ matchStrategy: 'exact-file' | 'exact-symbol' | 'fuzzy-symbol';
30
+ /** Edge weight (exact matches = 1.0, fuzzy = 0.6). */
31
+ weight: number;
32
+ }
33
+ /** Summary result from autoLinkMemories. */
34
+ export interface AutoLinkResult {
35
+ /** Total brain entries scanned for entity references. */
36
+ scanned: number;
37
+ /** Number of new code_reference edges created. */
38
+ linked: number;
39
+ /** Number of links that already existed (skipped). */
40
+ alreadyLinked: number;
41
+ /** Individual links created in this run. */
42
+ links: CodeReferenceLink[];
43
+ }
44
+ /** Result from queryMemoriesForCode. */
45
+ export interface MemoriesForCodeResult {
46
+ /** The nexus node ID that was queried. */
47
+ nexusNodeId: string;
48
+ /** Brain memory nodes reachable from this code node. */
49
+ memories: Array<{
50
+ nodeId: string;
51
+ nodeType: string;
52
+ label: string;
53
+ qualityScore: number;
54
+ edgeWeight: number;
55
+ matchStrategy: string;
56
+ }>;
57
+ }
58
+ /** Result from queryCodeForMemory. */
59
+ export interface CodeForMemoryResult {
60
+ /** The brain memory node ID that was queried. */
61
+ brainNodeId: string;
62
+ /** Nexus code nodes reachable from this memory node. */
63
+ codeNodes: Array<{
64
+ nexusNodeId: string;
65
+ label: string;
66
+ filePath: string | null;
67
+ kind: string;
68
+ edgeWeight: number;
69
+ matchStrategy: string;
70
+ }>;
71
+ }
72
+ /**
73
+ * Create a `code_reference` edge from a brain memory node to a nexus code node.
74
+ *
75
+ * Writes to brain_page_edges (brain.db). The nexus node must already exist in
76
+ * nexus.db but is never mutated. The brain node is upserted as a stub if it
77
+ * does not yet exist in brain_page_nodes.
78
+ *
79
+ * This function is idempotent — calling it multiple times with the same
80
+ * (memoryId, codeSymbol) pair is safe (the composite PK prevents duplicates).
81
+ *
82
+ * @param projectRoot - Absolute path to project root (locates brain.db)
83
+ * @param memoryId - Brain memory node ID (format: '<type>:<source-id>')
84
+ * @param codeSymbol - Nexus node ID (format: '<filePath>::<name>' or '<filePath>')
85
+ * @returns True if the edge was created or already existed; false on error
86
+ */
87
+ export declare function linkMemoryToCode(projectRoot: string, memoryId: string, codeSymbol: string): Promise<boolean>;
88
+ /**
89
+ * Scan brain memory nodes for entity references and match them against nexus.
90
+ *
91
+ * For each brain node, extracts:
92
+ * - File path references → matched against nexus_nodes.file_path (exact)
93
+ * - Symbol name references → matched against nexus_nodes.name (exact, then fuzzy)
94
+ *
95
+ * Matching edges are written to brain_page_edges with edgeType='code_reference'.
96
+ * This function is idempotent — existing edges are skipped.
97
+ *
98
+ * Should be called from runConsolidation() as a best-effort step. All errors
99
+ * are caught and logged; never throws.
100
+ *
101
+ * @param projectRoot - Absolute path to project root (locates brain.db)
102
+ * @returns Summary of scanned entries and created links
103
+ */
104
+ export declare function autoLinkMemories(projectRoot: string): Promise<AutoLinkResult>;
105
+ /**
106
+ * Given a code symbol (nexus node ID), find related brain memory nodes.
107
+ *
108
+ * Traverses `code_reference` edges in brain_page_edges where the target is
109
+ * the given nexus node ID. Returns the brain memory nodes with edge metadata.
110
+ *
111
+ * @param projectRoot - Absolute path to project root (locates brain.db)
112
+ * @param symbol - Nexus node ID (format: '<filePath>::<name>' or '<filePath>')
113
+ * @returns Memory nodes that reference the given code symbol
114
+ */
115
+ export declare function queryMemoriesForCode(projectRoot: string, symbol: string): Promise<MemoriesForCodeResult>;
116
+ /**
117
+ * Given a brain memory node ID, find related nexus code nodes.
118
+ *
119
+ * Traverses `code_reference` edges in brain_page_edges from the given memory
120
+ * node ID, then fetches the corresponding nexus node metadata.
121
+ *
122
+ * @param projectRoot - Absolute path to project root (locates brain.db)
123
+ * @param memoryId - Brain memory node ID (format: '<type>:<source-id>')
124
+ * @returns Nexus code nodes referenced by the given memory entry
125
+ */
126
+ export declare function queryCodeForMemory(projectRoot: string, memoryId: string): Promise<CodeForMemoryResult>;
127
+ /** A single code-memory link for display. */
128
+ export interface CodeLinkEntry {
129
+ /** Brain memory node ID. */
130
+ brainNodeId: string;
131
+ /** Brain node type. */
132
+ brainNodeType: string;
133
+ /** Brain node label. */
134
+ brainNodeLabel: string;
135
+ /** Nexus code node ID. */
136
+ nexusNodeId: string;
137
+ /** Nexus node label. */
138
+ nexusNodeLabel: string;
139
+ /** File path in the nexus node (relative to project root). */
140
+ filePath: string | null;
141
+ /** Code kind (function, class, file, etc.). */
142
+ kind: string;
143
+ /** Edge weight. */
144
+ weight: number;
145
+ /** When the edge was created. */
146
+ createdAt: string;
147
+ }
148
+ /**
149
+ * Return all `code_reference` edges from brain.db enriched with nexus metadata.
150
+ *
151
+ * Used by `cleo memory code-links` CLI command.
152
+ *
153
+ * @param projectRoot - Absolute path to project root (locates brain.db)
154
+ * @param limit - Maximum number of entries to return (default 100)
155
+ * @returns Array of code link entries sorted by weight descending
156
+ */
157
+ export declare function listCodeLinks(projectRoot: string, limit?: number): Promise<CodeLinkEntry[]>;
158
+ //# sourceMappingURL=graph-memory-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-memory-bridge.d.ts","sourceRoot":"","sources":["../../src/memory/graph-memory-bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,519 @@
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
+ import { brainPageEdges, brainPageNodes } from '../store/brain-schema.js';
21
+ import { getBrainDb, getBrainNativeDb } from '../store/brain-sqlite.js';
22
+ import { getNexusDb, getNexusNativeDb } from '../store/nexus-sqlite.js';
23
+ import { typedAll } from '../store/typed-query.js';
24
+ // ---------------------------------------------------------------------------
25
+ // Internal helpers
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * Regex patterns to extract entity references from text.
29
+ *
30
+ * Matches:
31
+ * - File paths: relative paths ending in known source extensions
32
+ * - Function/symbol names: camelCase, PascalCase, snake_case identifiers (≥4 chars)
33
+ */
34
+ const FILE_PATH_PATTERN = /(?:^|\s|['"`(])([a-zA-Z0-9_\-./]+\.(?:ts|tsx|js|jsx|rs|go|py|mjs|cjs))(?:$|\s|['"`)])/g;
35
+ const SYMBOL_PATTERN = /\b([a-zA-Z_][a-zA-Z0-9_]*(?:[A-Z][a-zA-Z0-9_]*)+|[a-zA-Z_]{4,}[a-zA-Z0-9_]*)\b/g;
36
+ /** Common stop-words to skip in symbol extraction (short-circuits false positives). */
37
+ const SYMBOL_STOP_WORDS = new Set([
38
+ 'true',
39
+ 'false',
40
+ 'null',
41
+ 'undefined',
42
+ 'const',
43
+ 'async',
44
+ 'await',
45
+ 'return',
46
+ 'export',
47
+ 'import',
48
+ 'from',
49
+ 'type',
50
+ 'interface',
51
+ 'function',
52
+ 'class',
53
+ 'this',
54
+ 'super',
55
+ 'extends',
56
+ 'implements',
57
+ 'with',
58
+ 'that',
59
+ 'then',
60
+ 'when',
61
+ 'have',
62
+ 'been',
63
+ 'will',
64
+ 'should',
65
+ 'could',
66
+ 'would',
67
+ 'error',
68
+ 'result',
69
+ 'value',
70
+ 'data',
71
+ 'info',
72
+ 'note',
73
+ 'todo',
74
+ 'done',
75
+ 'fail',
76
+ 'pass',
77
+ ]);
78
+ /**
79
+ * Extract file paths from text.
80
+ */
81
+ function extractFilePaths(text) {
82
+ const paths = new Set();
83
+ for (const m of text.matchAll(FILE_PATH_PATTERN)) {
84
+ const p = m[1];
85
+ if (p)
86
+ paths.add(p);
87
+ }
88
+ return Array.from(paths);
89
+ }
90
+ /**
91
+ * Extract potential symbol names from text (camelCase / PascalCase / snake_case ≥ 4 chars).
92
+ */
93
+ function extractSymbolCandidates(text) {
94
+ const syms = new Set();
95
+ for (const m of text.matchAll(SYMBOL_PATTERN)) {
96
+ const s = m[1];
97
+ if (s && s.length >= 4 && !SYMBOL_STOP_WORDS.has(s.toLowerCase())) {
98
+ syms.add(s);
99
+ }
100
+ }
101
+ return Array.from(syms);
102
+ }
103
+ /**
104
+ * Build a plain-text corpus from a brain_page_nodes metadata_json blob.
105
+ * Returns empty string if metadata is absent or malformed.
106
+ */
107
+ function metadataText(metaJson) {
108
+ if (!metaJson)
109
+ return '';
110
+ try {
111
+ const obj = JSON.parse(metaJson);
112
+ return Object.values(obj)
113
+ .filter((v) => typeof v === 'string')
114
+ .join(' ');
115
+ }
116
+ catch {
117
+ return '';
118
+ }
119
+ }
120
+ // ---------------------------------------------------------------------------
121
+ // linkMemoryToCode — manual single-link creation
122
+ // ---------------------------------------------------------------------------
123
+ /**
124
+ * Create a `code_reference` edge from a brain memory node to a nexus code node.
125
+ *
126
+ * Writes to brain_page_edges (brain.db). The nexus node must already exist in
127
+ * nexus.db but is never mutated. The brain node is upserted as a stub if it
128
+ * does not yet exist in brain_page_nodes.
129
+ *
130
+ * This function is idempotent — calling it multiple times with the same
131
+ * (memoryId, codeSymbol) pair is safe (the composite PK prevents duplicates).
132
+ *
133
+ * @param projectRoot - Absolute path to project root (locates brain.db)
134
+ * @param memoryId - Brain memory node ID (format: '<type>:<source-id>')
135
+ * @param codeSymbol - Nexus node ID (format: '<filePath>::<name>' or '<filePath>')
136
+ * @returns True if the edge was created or already existed; false on error
137
+ */
138
+ export async function linkMemoryToCode(projectRoot, memoryId, codeSymbol) {
139
+ try {
140
+ const brainDb = await getBrainDb(projectRoot);
141
+ // Ensure nexus.db is initialized so we can verify the target node exists
142
+ await getNexusDb();
143
+ const nexusNative = getNexusNativeDb();
144
+ if (!nexusNative)
145
+ return false;
146
+ // Verify the nexus node exists (read-only check)
147
+ const nexusNode = nexusNative
148
+ .prepare('SELECT id, label, file_path, kind FROM nexus_nodes WHERE id = ? LIMIT 1')
149
+ .get(codeSymbol);
150
+ if (!nexusNode)
151
+ return false;
152
+ const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
153
+ // Upsert the memory node stub in brain_page_nodes so the edge FK is satisfied
154
+ // (the real node may already exist from graph-auto-populate; onConflictDoUpdate
155
+ // only refreshes lastActivityAt if the node already exists).
156
+ const idParts = memoryId.split(':');
157
+ const nodeType = idParts[0] ?? 'observation';
158
+ await brainDb
159
+ .insert(brainPageNodes)
160
+ .values({
161
+ id: memoryId,
162
+ nodeType,
163
+ label: memoryId,
164
+ qualityScore: 0.5,
165
+ contentHash: null,
166
+ lastActivityAt: now,
167
+ createdAt: now,
168
+ updatedAt: now,
169
+ })
170
+ .onConflictDoUpdate({
171
+ target: brainPageNodes.id,
172
+ set: { lastActivityAt: now, updatedAt: now },
173
+ });
174
+ // Write the code_reference edge
175
+ // Cast required: drizzle's enum type may not yet include 'code_reference'
176
+ // in the compiled .d.ts (the schema source was updated but not yet built).
177
+ await brainDb
178
+ .insert(brainPageEdges)
179
+ .values({
180
+ fromId: memoryId,
181
+ toId: codeSymbol,
182
+ edgeType: 'code_reference',
183
+ weight: 1.0,
184
+ provenance: 'manual',
185
+ createdAt: now,
186
+ })
187
+ .onConflictDoNothing();
188
+ return true;
189
+ }
190
+ catch (err) {
191
+ console.warn('[graph-memory-bridge] linkMemoryToCode failed:', err);
192
+ return false;
193
+ }
194
+ }
195
+ // ---------------------------------------------------------------------------
196
+ // autoLinkMemories — scan brain nodes and link to nexus matches
197
+ // ---------------------------------------------------------------------------
198
+ /**
199
+ * Scan brain memory nodes for entity references and match them against nexus.
200
+ *
201
+ * For each brain node, extracts:
202
+ * - File path references → matched against nexus_nodes.file_path (exact)
203
+ * - Symbol name references → matched against nexus_nodes.name (exact, then fuzzy)
204
+ *
205
+ * Matching edges are written to brain_page_edges with edgeType='code_reference'.
206
+ * This function is idempotent — existing edges are skipped.
207
+ *
208
+ * Should be called from runConsolidation() as a best-effort step. All errors
209
+ * are caught and logged; never throws.
210
+ *
211
+ * @param projectRoot - Absolute path to project root (locates brain.db)
212
+ * @returns Summary of scanned entries and created links
213
+ */
214
+ export async function autoLinkMemories(projectRoot) {
215
+ const result = { scanned: 0, linked: 0, alreadyLinked: 0, links: [] };
216
+ try {
217
+ await getBrainDb(projectRoot);
218
+ const brainNative = getBrainNativeDb();
219
+ await getNexusDb();
220
+ const nexusNative = getNexusNativeDb();
221
+ if (!brainNative || !nexusNative)
222
+ return result;
223
+ // Load all brain page nodes that are memory entity types
224
+ const brainNodes = typedAll(brainNative.prepare(`
225
+ SELECT id, node_type, label, quality_score, metadata_json
226
+ FROM brain_page_nodes
227
+ WHERE node_type IN ('observation', 'decision', 'pattern', 'learning')
228
+ AND quality_score >= 0.3
229
+ ORDER BY quality_score DESC
230
+ LIMIT 500
231
+ `));
232
+ result.scanned = brainNodes.length;
233
+ if (brainNodes.length === 0)
234
+ return result;
235
+ // Load nexus nodes into memory (indexed by name and filePath for fast lookup).
236
+ // We load only essential columns to keep memory usage low.
237
+ const nexusNodes = typedAll(nexusNative.prepare(`
238
+ SELECT id, label, name, file_path, kind
239
+ FROM nexus_nodes
240
+ WHERE kind NOT IN ('community', 'process', 'folder')
241
+ LIMIT 20000
242
+ `));
243
+ if (nexusNodes.length === 0)
244
+ return result;
245
+ // Build lookup indexes
246
+ const byFilePath = new Map();
247
+ const byNameExact = new Map();
248
+ const byNameLower = new Map();
249
+ for (const node of nexusNodes) {
250
+ if (node.file_path) {
251
+ const fp = node.file_path.toLowerCase();
252
+ const existing = byFilePath.get(fp) ?? [];
253
+ existing.push(node);
254
+ byFilePath.set(fp, existing);
255
+ }
256
+ if (node.name) {
257
+ // Exact (case-sensitive)
258
+ const exact = byNameExact.get(node.name) ?? [];
259
+ exact.push(node);
260
+ byNameExact.set(node.name, exact);
261
+ // Lowercase for fuzzy
262
+ const lower = node.name.toLowerCase();
263
+ const fuzzy = byNameLower.get(lower) ?? [];
264
+ fuzzy.push(node);
265
+ byNameLower.set(lower, fuzzy);
266
+ }
267
+ }
268
+ const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
269
+ // Load existing code_reference edges to avoid duplicates
270
+ const existingEdges = new Set();
271
+ const rawEdges = typedAll(brainNative.prepare(`
272
+ SELECT from_id, to_id FROM brain_page_edges WHERE edge_type = 'code_reference'
273
+ `));
274
+ for (const e of rawEdges) {
275
+ existingEdges.add(`${e.from_id}|${e.to_id}`);
276
+ }
277
+ // Process each brain node
278
+ for (const brainNode of brainNodes) {
279
+ const corpus = `${brainNode.label} ${metadataText(brainNode.metadata_json)}`;
280
+ const filePaths = extractFilePaths(corpus);
281
+ const symbolCandidates = extractSymbolCandidates(corpus);
282
+ const candidates = [];
283
+ // 1. Exact file path matches
284
+ for (const fp of filePaths) {
285
+ const matches = byFilePath.get(fp.toLowerCase());
286
+ if (matches) {
287
+ for (const n of matches) {
288
+ candidates.push({ nexusNode: n, strategy: 'exact-file', weight: 1.0 });
289
+ }
290
+ }
291
+ }
292
+ // 2. Exact symbol name matches
293
+ for (const sym of symbolCandidates) {
294
+ const exactMatches = byNameExact.get(sym);
295
+ if (exactMatches) {
296
+ for (const n of exactMatches) {
297
+ candidates.push({ nexusNode: n, strategy: 'exact-symbol', weight: 1.0 });
298
+ }
299
+ }
300
+ }
301
+ // 3. Fuzzy (case-insensitive) symbol matches — only for symbols not already exact-matched
302
+ const exactSymSet = new Set(symbolCandidates.flatMap((s) => byNameExact.get(s) ?? []).map((n) => n.id));
303
+ for (const sym of symbolCandidates) {
304
+ if (sym.length < 5)
305
+ continue; // skip very short symbols for fuzzy
306
+ const lower = sym.toLowerCase();
307
+ const fuzzyMatches = byNameLower.get(lower);
308
+ if (fuzzyMatches) {
309
+ for (const n of fuzzyMatches) {
310
+ if (!exactSymSet.has(n.id)) {
311
+ candidates.push({ nexusNode: n, strategy: 'fuzzy-symbol', weight: 0.6 });
312
+ }
313
+ }
314
+ }
315
+ }
316
+ // Deduplicate candidates (keep highest weight per nexus node)
317
+ const bestByNexusId = new Map();
318
+ for (const c of candidates) {
319
+ const existing = bestByNexusId.get(c.nexusNode.id);
320
+ if (!existing || c.weight > existing.weight) {
321
+ bestByNexusId.set(c.nexusNode.id, c);
322
+ }
323
+ }
324
+ // Write edges (cap at 10 per brain node to avoid noise)
325
+ const sortedCandidates = Array.from(bestByNexusId.values())
326
+ .sort((a, b) => b.weight - a.weight)
327
+ .slice(0, 10);
328
+ for (const { nexusNode, strategy, weight } of sortedCandidates) {
329
+ const edgeKey = `${brainNode.id}|${nexusNode.id}`;
330
+ if (existingEdges.has(edgeKey)) {
331
+ result.alreadyLinked++;
332
+ continue;
333
+ }
334
+ // Upsert brain node stub (idempotent — real node may already exist)
335
+ brainNative
336
+ .prepare(`
337
+ INSERT OR IGNORE INTO brain_page_nodes
338
+ (id, node_type, label, quality_score, content_hash, metadata_json, last_activity_at, created_at, updated_at)
339
+ VALUES (?, ?, ?, ?, NULL, NULL, ?, ?, ?)
340
+ `)
341
+ .run(brainNode.id, brainNode.node_type, brainNode.label, brainNode.quality_score, now, now, now);
342
+ // Write edge
343
+ try {
344
+ brainNative
345
+ .prepare(`
346
+ INSERT OR IGNORE INTO brain_page_edges
347
+ (from_id, to_id, edge_type, weight, provenance, created_at)
348
+ VALUES (?, ?, 'code_reference', ?, ?, ?)
349
+ `)
350
+ .run(brainNode.id, nexusNode.id, weight, `auto:${strategy}`, now);
351
+ existingEdges.add(edgeKey);
352
+ result.linked++;
353
+ result.links.push({
354
+ brainNodeId: brainNode.id,
355
+ nexusNodeId: nexusNode.id,
356
+ nexusLabel: nexusNode.label,
357
+ matchStrategy: strategy,
358
+ weight,
359
+ });
360
+ }
361
+ catch (edgeErr) {
362
+ console.warn('[graph-memory-bridge] edge insert failed:', edgeErr);
363
+ }
364
+ }
365
+ }
366
+ }
367
+ catch (err) {
368
+ console.warn('[graph-memory-bridge] autoLinkMemories failed:', err);
369
+ }
370
+ return result;
371
+ }
372
+ // ---------------------------------------------------------------------------
373
+ // queryMemoriesForCode — find memories related to a code symbol
374
+ // ---------------------------------------------------------------------------
375
+ /**
376
+ * Given a code symbol (nexus node ID), find related brain memory nodes.
377
+ *
378
+ * Traverses `code_reference` edges in brain_page_edges where the target is
379
+ * the given nexus node ID. Returns the brain memory nodes with edge metadata.
380
+ *
381
+ * @param projectRoot - Absolute path to project root (locates brain.db)
382
+ * @param symbol - Nexus node ID (format: '<filePath>::<name>' or '<filePath>')
383
+ * @returns Memory nodes that reference the given code symbol
384
+ */
385
+ export async function queryMemoriesForCode(projectRoot, symbol) {
386
+ const result = { nexusNodeId: symbol, memories: [] };
387
+ try {
388
+ await getBrainDb(projectRoot);
389
+ const brainNative = getBrainNativeDb();
390
+ if (!brainNative)
391
+ return result;
392
+ const rows = typedAll(brainNative.prepare(`
393
+ SELECT n.id, n.node_type, n.label, n.quality_score,
394
+ e.weight, e.provenance
395
+ FROM brain_page_edges e
396
+ JOIN brain_page_nodes n ON n.id = e.from_id
397
+ WHERE e.to_id = ?
398
+ AND e.edge_type = 'code_reference'
399
+ ORDER BY e.weight DESC, n.quality_score DESC
400
+ LIMIT 50
401
+ `), symbol);
402
+ result.memories = rows.map((r) => ({
403
+ nodeId: r.id,
404
+ nodeType: r.node_type,
405
+ label: r.label,
406
+ qualityScore: r.quality_score,
407
+ edgeWeight: r.weight,
408
+ matchStrategy: r.provenance?.replace('auto:', '') ?? 'manual',
409
+ }));
410
+ }
411
+ catch (err) {
412
+ console.warn('[graph-memory-bridge] queryMemoriesForCode failed:', err);
413
+ }
414
+ return result;
415
+ }
416
+ // ---------------------------------------------------------------------------
417
+ // queryCodeForMemory — find code nodes related to a memory entry
418
+ // ---------------------------------------------------------------------------
419
+ /**
420
+ * Given a brain memory node ID, find related nexus code nodes.
421
+ *
422
+ * Traverses `code_reference` edges in brain_page_edges from the given memory
423
+ * node ID, then fetches the corresponding nexus node metadata.
424
+ *
425
+ * @param projectRoot - Absolute path to project root (locates brain.db)
426
+ * @param memoryId - Brain memory node ID (format: '<type>:<source-id>')
427
+ * @returns Nexus code nodes referenced by the given memory entry
428
+ */
429
+ export async function queryCodeForMemory(projectRoot, memoryId) {
430
+ const result = { brainNodeId: memoryId, codeNodes: [] };
431
+ try {
432
+ await getBrainDb(projectRoot);
433
+ const brainNative = getBrainNativeDb();
434
+ await getNexusDb();
435
+ const nexusNative = getNexusNativeDb();
436
+ if (!brainNative || !nexusNative)
437
+ return result;
438
+ const brainEdges = typedAll(brainNative.prepare(`
439
+ SELECT to_id, weight, provenance
440
+ FROM brain_page_edges
441
+ WHERE from_id = ?
442
+ AND edge_type = 'code_reference'
443
+ ORDER BY weight DESC
444
+ LIMIT 50
445
+ `), memoryId);
446
+ if (brainEdges.length === 0)
447
+ return result;
448
+ // Fetch nexus node metadata for each target (read-only)
449
+ for (const edge of brainEdges) {
450
+ const nexusNode = nexusNative
451
+ .prepare('SELECT id, label, file_path, kind FROM nexus_nodes WHERE id = ? LIMIT 1')
452
+ .get(edge.to_id);
453
+ if (nexusNode) {
454
+ result.codeNodes.push({
455
+ nexusNodeId: nexusNode.id,
456
+ label: nexusNode.label,
457
+ filePath: nexusNode.file_path,
458
+ kind: nexusNode.kind,
459
+ edgeWeight: edge.weight,
460
+ matchStrategy: edge.provenance?.replace('auto:', '') ?? 'manual',
461
+ });
462
+ }
463
+ }
464
+ }
465
+ catch (err) {
466
+ console.warn('[graph-memory-bridge] queryCodeForMemory failed:', err);
467
+ }
468
+ return result;
469
+ }
470
+ /**
471
+ * Return all `code_reference` edges from brain.db enriched with nexus metadata.
472
+ *
473
+ * Used by `cleo memory code-links` CLI command.
474
+ *
475
+ * @param projectRoot - Absolute path to project root (locates brain.db)
476
+ * @param limit - Maximum number of entries to return (default 100)
477
+ * @returns Array of code link entries sorted by weight descending
478
+ */
479
+ export async function listCodeLinks(projectRoot, limit = 100) {
480
+ const entries = [];
481
+ try {
482
+ await getBrainDb(projectRoot);
483
+ const brainNative = getBrainNativeDb();
484
+ await getNexusDb();
485
+ const nexusNative = getNexusNativeDb();
486
+ if (!brainNative || !nexusNative)
487
+ return entries;
488
+ const rows = typedAll(brainNative.prepare(`
489
+ SELECT e.from_id, e.to_id, e.weight, e.created_at,
490
+ n.node_type, n.label
491
+ FROM brain_page_edges e
492
+ JOIN brain_page_nodes n ON n.id = e.from_id
493
+ WHERE e.edge_type = 'code_reference'
494
+ ORDER BY e.weight DESC, e.created_at DESC
495
+ LIMIT ?
496
+ `), limit);
497
+ for (const row of rows) {
498
+ const nexusNode = nexusNative
499
+ .prepare('SELECT id, label, file_path, kind FROM nexus_nodes WHERE id = ? LIMIT 1')
500
+ .get(row.to_id);
501
+ entries.push({
502
+ brainNodeId: row.from_id,
503
+ brainNodeType: row.node_type,
504
+ brainNodeLabel: row.label,
505
+ nexusNodeId: row.to_id,
506
+ nexusNodeLabel: nexusNode?.label ?? row.to_id,
507
+ filePath: nexusNode?.file_path ?? null,
508
+ kind: nexusNode?.kind ?? 'unknown',
509
+ weight: row.weight,
510
+ createdAt: row.created_at,
511
+ });
512
+ }
513
+ }
514
+ catch (err) {
515
+ console.warn('[graph-memory-bridge] listCodeLinks failed:', err);
516
+ }
517
+ return entries;
518
+ }
519
+ //# sourceMappingURL=graph-memory-bridge.js.map