@jafreck/lore 0.2.3 → 0.2.5

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 (150) hide show
  1. package/README.md +22 -13
  2. package/dist/cli.js +130 -14
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/indexer/call-graph.d.ts +24 -7
  9. package/dist/indexer/call-graph.d.ts.map +1 -1
  10. package/dist/indexer/call-graph.js +154 -22
  11. package/dist/indexer/call-graph.js.map +1 -1
  12. package/dist/indexer/config-parser.js +6 -1
  13. package/dist/indexer/config-parser.js.map +1 -1
  14. package/dist/indexer/coverage.js +3 -1
  15. package/dist/indexer/coverage.js.map +1 -1
  16. package/dist/indexer/db.d.ts.map +1 -1
  17. package/dist/indexer/db.js +43 -0
  18. package/dist/indexer/db.js.map +1 -1
  19. package/dist/indexer/docs.d.ts.map +1 -1
  20. package/dist/indexer/docs.js +4 -0
  21. package/dist/indexer/docs.js.map +1 -1
  22. package/dist/indexer/embedder.d.ts.map +1 -1
  23. package/dist/indexer/embedder.js +5 -0
  24. package/dist/indexer/embedder.js.map +1 -1
  25. package/dist/indexer/extractors/bash.d.ts.map +1 -1
  26. package/dist/indexer/extractors/bash.js +29 -1
  27. package/dist/indexer/extractors/bash.js.map +1 -1
  28. package/dist/indexer/extractors/c.d.ts +5 -2
  29. package/dist/indexer/extractors/c.d.ts.map +1 -1
  30. package/dist/indexer/extractors/c.js +277 -8
  31. package/dist/indexer/extractors/c.js.map +1 -1
  32. package/dist/indexer/extractors/cpp.d.ts +5 -2
  33. package/dist/indexer/extractors/cpp.d.ts.map +1 -1
  34. package/dist/indexer/extractors/cpp.js +349 -3
  35. package/dist/indexer/extractors/cpp.js.map +1 -1
  36. package/dist/indexer/extractors/csharp.d.ts.map +1 -1
  37. package/dist/indexer/extractors/csharp.js +166 -1
  38. package/dist/indexer/extractors/csharp.js.map +1 -1
  39. package/dist/indexer/extractors/dart.d.ts.map +1 -1
  40. package/dist/indexer/extractors/dart.js +134 -1
  41. package/dist/indexer/extractors/dart.js.map +1 -1
  42. package/dist/indexer/extractors/elixir.d.ts.map +1 -1
  43. package/dist/indexer/extractors/elixir.js +45 -0
  44. package/dist/indexer/extractors/elixir.js.map +1 -1
  45. package/dist/indexer/extractors/elm.d.ts.map +1 -1
  46. package/dist/indexer/extractors/elm.js +33 -0
  47. package/dist/indexer/extractors/elm.js.map +1 -1
  48. package/dist/indexer/extractors/go.d.ts.map +1 -1
  49. package/dist/indexer/extractors/go.js +206 -1
  50. package/dist/indexer/extractors/go.js.map +1 -1
  51. package/dist/indexer/extractors/haskell.d.ts.map +1 -1
  52. package/dist/indexer/extractors/haskell.js +29 -0
  53. package/dist/indexer/extractors/haskell.js.map +1 -1
  54. package/dist/indexer/extractors/java.d.ts.map +1 -1
  55. package/dist/indexer/extractors/java.js +170 -1
  56. package/dist/indexer/extractors/java.js.map +1 -1
  57. package/dist/indexer/extractors/javascript.d.ts.map +1 -1
  58. package/dist/indexer/extractors/javascript.js +24 -2
  59. package/dist/indexer/extractors/javascript.js.map +1 -1
  60. package/dist/indexer/extractors/julia.d.ts.map +1 -1
  61. package/dist/indexer/extractors/julia.js +23 -1
  62. package/dist/indexer/extractors/julia.js.map +1 -1
  63. package/dist/indexer/extractors/kotlin.d.ts.map +1 -1
  64. package/dist/indexer/extractors/kotlin.js +122 -1
  65. package/dist/indexer/extractors/kotlin.js.map +1 -1
  66. package/dist/indexer/extractors/lua.d.ts.map +1 -1
  67. package/dist/indexer/extractors/lua.js +19 -1
  68. package/dist/indexer/extractors/lua.js.map +1 -1
  69. package/dist/indexer/extractors/objc.d.ts.map +1 -1
  70. package/dist/indexer/extractors/objc.js +171 -1
  71. package/dist/indexer/extractors/objc.js.map +1 -1
  72. package/dist/indexer/extractors/ocaml.d.ts.map +1 -1
  73. package/dist/indexer/extractors/ocaml.js +29 -0
  74. package/dist/indexer/extractors/ocaml.js.map +1 -1
  75. package/dist/indexer/extractors/php.d.ts.map +1 -1
  76. package/dist/indexer/extractors/php.js +133 -1
  77. package/dist/indexer/extractors/php.js.map +1 -1
  78. package/dist/indexer/extractors/python.d.ts.map +1 -1
  79. package/dist/indexer/extractors/python.js +24 -3
  80. package/dist/indexer/extractors/python.js.map +1 -1
  81. package/dist/indexer/extractors/ruby.d.ts.map +1 -1
  82. package/dist/indexer/extractors/ruby.js +23 -1
  83. package/dist/indexer/extractors/ruby.js.map +1 -1
  84. package/dist/indexer/extractors/rust.d.ts.map +1 -1
  85. package/dist/indexer/extractors/rust.js +139 -2
  86. package/dist/indexer/extractors/rust.js.map +1 -1
  87. package/dist/indexer/extractors/scala.d.ts.map +1 -1
  88. package/dist/indexer/extractors/scala.js +24 -1
  89. package/dist/indexer/extractors/scala.js.map +1 -1
  90. package/dist/indexer/extractors/swift.d.ts.map +1 -1
  91. package/dist/indexer/extractors/swift.js +129 -1
  92. package/dist/indexer/extractors/swift.js.map +1 -1
  93. package/dist/indexer/extractors/types.d.ts +78 -2
  94. package/dist/indexer/extractors/types.d.ts.map +1 -1
  95. package/dist/indexer/extractors/types.js +167 -8
  96. package/dist/indexer/extractors/types.js.map +1 -1
  97. package/dist/indexer/extractors/typescript.d.ts.map +1 -1
  98. package/dist/indexer/extractors/typescript.js +217 -1
  99. package/dist/indexer/extractors/typescript.js.map +1 -1
  100. package/dist/indexer/extractors/zig.d.ts.map +1 -1
  101. package/dist/indexer/extractors/zig.js +30 -0
  102. package/dist/indexer/extractors/zig.js.map +1 -1
  103. package/dist/indexer/git-history.d.ts.map +1 -1
  104. package/dist/indexer/git-history.js +4 -8
  105. package/dist/indexer/git-history.js.map +1 -1
  106. package/dist/indexer/git-hooks.js +1 -1
  107. package/dist/indexer/git-hooks.js.map +1 -1
  108. package/dist/indexer/index.d.ts +24 -1
  109. package/dist/indexer/index.d.ts.map +1 -1
  110. package/dist/indexer/index.js +245 -66
  111. package/dist/indexer/index.js.map +1 -1
  112. package/dist/indexer/lsp/client.d.ts.map +1 -1
  113. package/dist/indexer/lsp/client.js +40 -15
  114. package/dist/indexer/lsp/client.js.map +1 -1
  115. package/dist/indexer/lsp/config.d.ts.map +1 -1
  116. package/dist/indexer/lsp/config.js +18 -4
  117. package/dist/indexer/lsp/config.js.map +1 -1
  118. package/dist/indexer/poller.d.ts +8 -0
  119. package/dist/indexer/poller.d.ts.map +1 -1
  120. package/dist/indexer/poller.js +3 -1
  121. package/dist/indexer/poller.js.map +1 -1
  122. package/dist/indexer/resolver.d.ts.map +1 -1
  123. package/dist/indexer/resolver.js +8 -4
  124. package/dist/indexer/resolver.js.map +1 -1
  125. package/dist/indexer/watcher.d.ts +8 -0
  126. package/dist/indexer/watcher.d.ts.map +1 -1
  127. package/dist/indexer/watcher.js +18 -1
  128. package/dist/indexer/watcher.js.map +1 -1
  129. package/dist/logger.d.ts +104 -0
  130. package/dist/logger.d.ts.map +1 -0
  131. package/dist/logger.js +170 -0
  132. package/dist/logger.js.map +1 -0
  133. package/dist/lore-server/db.d.ts.map +1 -1
  134. package/dist/lore-server/db.js +28 -24
  135. package/dist/lore-server/db.js.map +1 -1
  136. package/dist/lore-server/server.d.ts +3 -0
  137. package/dist/lore-server/server.d.ts.map +1 -1
  138. package/dist/lore-server/server.js +62 -102
  139. package/dist/lore-server/server.js.map +1 -1
  140. package/dist/lore-server/tools/graph.d.ts +4 -3
  141. package/dist/lore-server/tools/graph.d.ts.map +1 -1
  142. package/dist/lore-server/tools/graph.js +48 -10
  143. package/dist/lore-server/tools/graph.js.map +1 -1
  144. package/dist/lore-server/tools/notes.d.ts.map +1 -1
  145. package/dist/lore-server/tools/notes.js +6 -2
  146. package/dist/lore-server/tools/notes.js.map +1 -1
  147. package/dist/lore-server/tools/search.d.ts.map +1 -1
  148. package/dist/lore-server/tools/search.js +46 -14
  149. package/dist/lore-server/tools/search.js.map +1 -1
  150. package/package.json +2 -2
@@ -19,7 +19,7 @@ import { inferSeededDocNoteKey, buildDocNoteScope } from './docs.js';
19
19
  import { ingestGitHistory } from './git-history.js';
20
20
  import { ParserPool } from './parser.js';
21
21
  import { ImportResolver } from './resolver.js';
22
- import { buildCallGraph } from './call-graph.js';
22
+ import { resolveSymbolEdges, normalizeTypeName } from './call-graph.js';
23
23
  import { isPublicDeclarationSurfaceSymbol, } from './extractors/types.js';
24
24
  import { CExtractor } from './extractors/c.js';
25
25
  import { RustExtractor } from './extractors/rust.js';
@@ -49,6 +49,7 @@ import { DEFAULT_EMBEDDING_MODEL, buildStructuralEmbeddingText } from './embedde
49
49
  import { ingestCoverageReport } from './coverage.js';
50
50
  import { refreshTestMappings } from './test-mapper.js';
51
51
  import { LspEnrichmentCoordinator } from './lsp/enrichment.js';
52
+ import { getLogger } from '../logger.js';
52
53
  // ─── Extractor registry ───────────────────────────────────────────────────────
53
54
  const EXTRACTORS = {
54
55
  c: new CExtractor(),
@@ -127,13 +128,17 @@ export class IndexBuilder {
127
128
  * the database.
128
129
  */
129
130
  async build() {
131
+ const log = getLogger();
132
+ const buildStart = performance.now();
130
133
  const db = openDb(this.dbPath);
131
134
  const branch = this.resolveBranch();
132
135
  const lspCoordinator = this.createLspEnrichmentCoordinator();
136
+ log.indexing('build started', { dbPath: this.dbPath, branch, rootDir: this.walkerConfig.rootDir });
133
137
  try {
134
138
  this.saveDocsAutoNotesSetting(db);
135
139
  const files = await walkFiles(this.walkerConfig);
136
140
  const docs = await walkDocumentationFiles(this.walkerConfig);
141
+ log.indexing('walk complete', { fileCount: files.length, docCount: docs.length });
137
142
  if (lspCoordinator) {
138
143
  const languages = new Set(files.map((file) => file.language));
139
144
  if (this.indexDependencies)
@@ -141,6 +146,9 @@ export class IndexBuilder {
141
146
  await lspCoordinator.start(languages);
142
147
  }
143
148
  const resumeAt = this.loadBuildCheckpoint(db, branch, files.length);
149
+ if (resumeAt > 0) {
150
+ log.indexing('resuming from checkpoint', { resumeAt, totalFiles: files.length });
151
+ }
144
152
  db.transaction(() => {
145
153
  for (let i = resumeAt; i < files.length; i++) {
146
154
  const file = files[i];
@@ -158,24 +166,64 @@ export class IndexBuilder {
158
166
  this.removeStaleDocumentation(db, branch, seenDocPaths);
159
167
  })();
160
168
  this.saveBuildCheckpoint(db, branch, files.length, files.length);
169
+ log.indexing('files processed, resolving imports');
161
170
  this.resolveImports(db, branch);
162
171
  await this.indexDependencyDeclarations(db, lspCoordinator);
163
- await this.enrichProjectSymbolsAndCallRefs(db, branch, files, lspCoordinator);
172
+ await this.enrichProjectRefs(db, branch, files, lspCoordinator);
164
173
  refreshTestMappings(db, branch);
165
- buildCallGraph(db);
174
+ resolveSymbolEdges(db);
166
175
  this.saveLastKnownHead(db);
167
176
  if (this.embedder) {
177
+ log.indexing('embedding started', { model: this.embeddingModel });
168
178
  await this.embedder.init();
169
179
  await this.embedStructural(db);
170
180
  await this.embedDocumentation(db);
181
+ log.indexing('embedding complete');
171
182
  }
172
183
  if (this.history) {
184
+ log.indexing('git history ingestion started');
173
185
  const historyOptions = typeof this.history === 'object' ? this.history : undefined;
174
186
  await ingestGitHistory(db, this.walkerConfig.rootDir, historyOptions);
175
187
  if (this.embedder) {
176
188
  await this.embedCommitMessages(db);
177
189
  }
190
+ log.indexing('git history ingestion complete');
191
+ }
192
+ // Gather final DB stats for the build summary
193
+ let totalSymbols = 0;
194
+ try {
195
+ totalSymbols = db.prepare('SELECT COUNT(*) AS cnt FROM symbols').get().cnt;
196
+ }
197
+ catch { /* table may not exist */ }
198
+ let totalEdges = 0;
199
+ try {
200
+ totalEdges = db.prepare('SELECT COUNT(*) AS cnt FROM symbol_refs').get().cnt;
201
+ }
202
+ catch { /* table may not exist */ }
203
+ let totalDocs = 0;
204
+ try {
205
+ totalDocs = db.prepare('SELECT COUNT(*) AS cnt FROM docs').get().cnt;
178
206
  }
207
+ catch { /* table may not exist */ }
208
+ let commitCount;
209
+ try {
210
+ commitCount = db.prepare('SELECT COUNT(*) AS cnt FROM commits').get().cnt;
211
+ }
212
+ catch { /* commits table may not exist */ }
213
+ const dbSizeBytes = fs.existsSync(this.dbPath) ? fs.statSync(this.dbPath).size : undefined;
214
+ const indexDurationMs = Math.round(performance.now() - buildStart);
215
+ log.startup('indexing complete', {
216
+ dbPath: this.dbPath,
217
+ dbSizeBytes,
218
+ embeddingModel: this.embeddingModel,
219
+ embeddingReady: !!this.embedder,
220
+ totalFiles: files.length,
221
+ totalSymbols,
222
+ totalDocs,
223
+ totalEdges,
224
+ commitCount,
225
+ indexDurationMs,
226
+ });
179
227
  }
180
228
  finally {
181
229
  if (lspCoordinator) {
@@ -195,6 +243,12 @@ export class IndexBuilder {
195
243
  const branch = this.resolveBranch();
196
244
  const lspCoordinator = this.createLspEnrichmentCoordinator();
197
245
  const enrichedFiles = [];
246
+ /** Symbol IDs whose embeddings should be removed (from deleted/re-processed files). */
247
+ const staleSymbolIds = [];
248
+ /** Paths of changed source files — used to look up new file IDs for scoped embedding. */
249
+ const changedSourcePaths = [];
250
+ /** Paths of changed doc files — used to look up new doc IDs for scoped embedding. */
251
+ const changedDocPaths = [];
198
252
  try {
199
253
  this.saveDocsAutoNotesSetting(db);
200
254
  const docs = await walkDocumentationFiles(this.walkerConfig);
@@ -218,6 +272,10 @@ export class IndexBuilder {
218
272
  if (!fs.existsSync(filePath)) {
219
273
  const row = db.prepare('SELECT id FROM files WHERE path = ? AND branch = ?').get(filePath, branch);
220
274
  if (row) {
275
+ // Collect symbol IDs for embedding cleanup before cascade-delete removes them.
276
+ const symRows = db.prepare('SELECT id FROM symbols WHERE file_id = ?').all(row.id);
277
+ for (const s of symRows)
278
+ staleSymbolIds.push(s.id);
221
279
  // Null out any resolved_id references pointing to this file
222
280
  db.prepare('UPDATE file_imports SET resolved_id = NULL WHERE resolved_id = ?').run(row.id);
223
281
  db.prepare('DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_id = ?)').run(row.id);
@@ -229,10 +287,18 @@ export class IndexBuilder {
229
287
  const language = detectLanguageForPath(filePath, this.walkerConfig);
230
288
  if (language) {
231
289
  enrichedFiles.push({ path: filePath, language });
290
+ changedSourcePaths.push(filePath);
232
291
  // Null out resolved_id references pointing to this file before deletion
233
292
  const existingRow = db.prepare('SELECT id FROM files WHERE path = ? AND branch = ?').get(filePath, branch);
234
293
  if (existingRow) {
294
+ // Collect symbol IDs for embedding cleanup before cascade-delete removes them.
295
+ const symRows = db.prepare('SELECT id FROM symbols WHERE file_id = ?').all(existingRow.id);
296
+ for (const s of symRows)
297
+ staleSymbolIds.push(s.id);
235
298
  db.prepare('UPDATE file_imports SET resolved_id = NULL WHERE resolved_id = ?').run(existingRow.id);
299
+ db.prepare('UPDATE symbol_refs SET callee_id = NULL WHERE callee_id IN (SELECT id FROM symbols WHERE file_id = ?)').run(existingRow.id);
300
+ db.prepare('UPDATE type_refs SET type_id = NULL WHERE type_id IN (SELECT id FROM symbols WHERE file_id = ?)').run(existingRow.id);
301
+ db.prepare('UPDATE symbol_relationships SET target_symbol_id = NULL WHERE target_symbol_id IN (SELECT id FROM symbols WHERE file_id = ?)').run(existingRow.id);
236
302
  db.prepare('DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_id = ?)').run(existingRow.id);
237
303
  }
238
304
  // Delete existing rows for this file (cascade handles symbols/imports)
@@ -243,6 +309,7 @@ export class IndexBuilder {
243
309
  if (changedDoc) {
244
310
  this.processDocumentationFile(db, changedDoc, branch);
245
311
  this.upsertSeededDocumentationNote(db, changedDoc, branch);
312
+ changedDocPaths.push(filePath);
246
313
  }
247
314
  else {
248
315
  this.deleteDocumentationByPath(db, filePath, branch);
@@ -251,7 +318,7 @@ export class IndexBuilder {
251
318
  })();
252
319
  this.resolveImports(db, branch);
253
320
  await this.indexDependencyDeclarations(db, lspCoordinator);
254
- await this.enrichProjectSymbolsAndCallRefs(db, branch, enrichedFiles, lspCoordinator);
321
+ await this.enrichProjectRefs(db, branch, enrichedFiles, lspCoordinator);
255
322
  refreshTestMappings(db, branch);
256
323
  if (this.history) {
257
324
  const historyOptions = typeof this.history === 'object' ? this.history : undefined;
@@ -259,13 +326,29 @@ export class IndexBuilder {
259
326
  }
260
327
  if (this.embedder) {
261
328
  await this.embedder.init();
262
- await this.embedStructural(db);
263
- await this.embedDocumentation(db);
329
+ // Clean up orphaned symbol embeddings for symbols that were deleted/replaced.
330
+ this.deleteSymbolEmbeddings(db, staleSymbolIds);
331
+ // Resolve the new file IDs for the changed source files.
332
+ const changedFileIds = [];
333
+ for (const p of changedSourcePaths) {
334
+ const row = db.prepare('SELECT id FROM files WHERE path = ? AND branch = ?').get(p, branch);
335
+ if (row)
336
+ changedFileIds.push(row.id);
337
+ }
338
+ // Resolve the new doc IDs for the changed documentation files.
339
+ const changedDocIds = [];
340
+ for (const p of changedDocPaths) {
341
+ const row = db.prepare('SELECT id FROM docs WHERE path = ? AND branch = ?').get(p, branch);
342
+ if (row)
343
+ changedDocIds.push(row.id);
344
+ }
345
+ await this.embedStructural(db, changedFileIds);
346
+ await this.embedDocumentation(db, changedDocIds);
264
347
  if (this.history) {
265
348
  await this.embedCommitMessages(db);
266
349
  }
267
350
  }
268
- buildCallGraph(db);
351
+ resolveSymbolEdges(db);
269
352
  this.saveLastKnownHead(db);
270
353
  }
271
354
  finally {
@@ -342,10 +425,18 @@ export class IndexBuilder {
342
425
  fileId = existing.id;
343
426
  // Remove stale symbols / imports / external deps (also clean up FTS5 index)
344
427
  db.prepare(`DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_id = ?)`).run(fileId);
428
+ db.prepare('DELETE FROM symbol_relationships WHERE file_id = ?').run(fileId);
429
+ db.prepare('DELETE FROM type_refs WHERE file_id = ?').run(fileId);
430
+ // NULL out cross-file FK references that point to symbols in this file
431
+ db.prepare('UPDATE symbol_refs SET callee_id = NULL WHERE callee_id IN (SELECT id FROM symbols WHERE file_id = ?)').run(fileId);
432
+ db.prepare('UPDATE type_refs SET type_id = NULL WHERE type_id IN (SELECT id FROM symbols WHERE file_id = ?)').run(fileId);
433
+ db.prepare('UPDATE symbol_relationships SET target_symbol_id = NULL WHERE target_symbol_id IN (SELECT id FROM symbols WHERE file_id = ?)').run(fileId);
345
434
  db.prepare('DELETE FROM symbols WHERE file_id = ?').run(fileId);
346
435
  db.prepare('DELETE FROM file_imports WHERE file_id = ?').run(fileId);
347
436
  db.prepare('DELETE FROM external_deps WHERE file_id = ?').run(fileId);
348
437
  db.prepare('DELETE FROM api_routes WHERE file_id = ?').run(fileId);
438
+ // Delete stale annotations so cascade-independent re-index doesn't accumulate duplicates.
439
+ db.prepare('DELETE FROM annotations WHERE file_id = ?').run(fileId);
349
440
  }
350
441
  else {
351
442
  const info = db
@@ -388,14 +479,28 @@ export class IndexBuilder {
388
479
  insertImport.run(fileId, imp.source);
389
480
  }
390
481
  // Insert call refs (callee_id resolved in call-graph phase)
391
- const insertCallRef = db.prepare(`INSERT INTO symbol_refs (caller_id, callee_name, call_line)
392
- VALUES (?, ?, ?)`);
482
+ const insertCallRef = db.prepare(`INSERT INTO symbol_refs (caller_id, callee_name, call_line, call_character, call_kind)
483
+ VALUES (?, ?, ?, ?, ?)`);
393
484
  for (const ref of result.callRefs) {
394
485
  const callerId = symbolIdMap.get(ref.callerSymbol);
395
486
  if (callerId !== undefined) {
396
- insertCallRef.run(callerId, ref.calleeRaw, ref.line);
487
+ insertCallRef.run(callerId, ref.calleeRaw, ref.line, ref.character ?? null, ref.callKind ?? 'direct');
397
488
  }
398
489
  }
490
+ // Insert relationships (target_symbol_id resolved in resolveSymbolEdges phase)
491
+ const insertRelationship = db.prepare(`INSERT INTO symbol_relationships (file_id, source_symbol_id, target_symbol_name, relationship_type, line, character)
492
+ VALUES (?, ?, ?, ?, ?, ?)`);
493
+ for (const rel of result.relationships) {
494
+ const sourceId = symbolIdMap.get(rel.fromSymbol) ?? null;
495
+ insertRelationship.run(fileId, sourceId, rel.toSymbol, rel.kind, rel.line, rel.character ?? null);
496
+ }
497
+ // Insert type refs (type_id resolved in resolveSymbolEdges phase)
498
+ const insertTypeRef = db.prepare(`INSERT INTO type_refs (file_id, symbol_id, type_name, type_name_bare, ref_kind, ref_line, ref_character)
499
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
500
+ for (const ref of result.typeRefs) {
501
+ const symId = symbolIdMap.get(ref.enclosingSymbol) ?? null;
502
+ insertTypeRef.run(fileId, symId, ref.typeRaw, normalizeTypeName(ref.typeRaw), ref.refKind, ref.line, ref.character ?? null);
503
+ }
399
504
  }
400
505
  processDocumentationFile(db, doc, branch) {
401
506
  const existing = db.prepare('SELECT id, content_hash FROM docs WHERE path = ? AND branch = ?').get(doc.path, branch);
@@ -486,6 +591,18 @@ export class IndexBuilder {
486
591
  return;
487
592
  db.prepare(`DELETE FROM doc_section_embeddings WHERE rowid IN (${sectionIds.map(() => '?').join(', ')})`).run(...sectionIds);
488
593
  }
594
+ /**
595
+ * Remove orphaned rows from the `symbol_embeddings` vec0 table for symbols
596
+ * that have been deleted (e.g. file re-processed or removed).
597
+ */
598
+ deleteSymbolEmbeddings(db, symbolIds) {
599
+ if (symbolIds.length === 0)
600
+ return;
601
+ const hasEmbeddingsTable = db.prepare("SELECT 1 AS present FROM sqlite_master WHERE type IN ('table', 'virtual table') AND name = 'symbol_embeddings'").get();
602
+ if (!hasEmbeddingsTable)
603
+ return;
604
+ db.prepare(`DELETE FROM symbol_embeddings WHERE rowid IN (${symbolIds.map(() => '?').join(', ')})`).run(...symbolIds);
605
+ }
489
606
  /**
490
607
  * Second pass: resolve raw_import strings to file IDs in the
491
608
  * `file_imports.resolved_id` column. Also populates `external_deps` for
@@ -582,7 +699,7 @@ export class IndexBuilder {
582
699
  }
583
700
  return new LspEnrichmentCoordinator(this.lspSettings, this.walkerConfig.rootDir);
584
701
  }
585
- async enrichProjectSymbolsAndCallRefs(db, branch, files, lspCoordinator) {
702
+ async enrichProjectRefs(db, branch, files, lspCoordinator) {
586
703
  if (!lspCoordinator || files.length === 0)
587
704
  return;
588
705
  const selectSymbols = db.prepare(`SELECT s.id, s.name, s.signature, s.start_line
@@ -590,11 +707,21 @@ export class IndexBuilder {
590
707
  JOIN files f ON f.id = s.file_id
591
708
  WHERE f.path = ? AND f.branch = ?
592
709
  ORDER BY s.id`);
593
- const selectCallRefs = db.prepare(`SELECT sr.id, sr.call_line
710
+ const selectCallRefs = db.prepare(`SELECT sr.id, sr.call_line, sr.call_character
594
711
  FROM symbol_refs sr
595
712
  JOIN symbols s ON s.id = sr.caller_id
596
713
  JOIN files f ON f.id = s.file_id
597
714
  WHERE f.path = ? AND f.branch = ?
715
+ ORDER BY sr.id`);
716
+ const selectTypeRefs = db.prepare(`SELECT tr.id, tr.ref_line, tr.ref_character
717
+ FROM type_refs tr
718
+ JOIN files f ON f.id = tr.file_id
719
+ WHERE f.path = ? AND f.branch = ?
720
+ ORDER BY tr.id`);
721
+ const selectRelationships = db.prepare(`SELECT sr.id, sr.line, sr.character
722
+ FROM symbol_relationships sr
723
+ JOIN files f ON f.id = sr.file_id
724
+ WHERE f.path = ? AND f.branch = ? AND sr.line IS NOT NULL
598
725
  ORDER BY sr.id`);
599
726
  const updateSymbol = db.prepare(`UPDATE symbols
600
727
  SET resolved_type_signature = ?, resolved_return_type = ?, definition_uri = ?, definition_path = ?
@@ -602,6 +729,12 @@ export class IndexBuilder {
602
729
  const updateSymbolFts = db.prepare('UPDATE symbols_fts SET signature = ? WHERE rowid = ?');
603
730
  const updateCallRef = db.prepare(`UPDATE symbol_refs
604
731
  SET resolved_type_signature = ?, resolved_return_type = ?, definition_uri = ?, definition_path = ?
732
+ WHERE id = ?`);
733
+ const updateTypeRef = db.prepare(`UPDATE type_refs
734
+ SET resolved_type_signature = ?, definition_uri = ?, definition_path = ?
735
+ WHERE id = ?`);
736
+ const updateRelationship = db.prepare(`UPDATE symbol_relationships
737
+ SET definition_uri = ?, definition_path = ?
605
738
  WHERE id = ?`);
606
739
  for (const file of files) {
607
740
  if (!file || !fs.existsSync(file.path))
@@ -613,46 +746,55 @@ export class IndexBuilder {
613
746
  catch {
614
747
  continue;
615
748
  }
749
+ const tagged = [];
616
750
  const symbols = selectSymbols.all(file.path, branch);
617
- if (symbols.length > 0) {
618
- const symbolMetadata = await lspCoordinator.enrich({
619
- filePath: file.path,
620
- language: file.language,
621
- source,
622
- targets: symbols.map((symbol) => ({ line: symbol.start_line, character: 0 })),
623
- });
624
- for (let i = 0; i < symbols.length; i++) {
625
- const symbol = symbols[i];
626
- if (!symbol)
627
- continue;
628
- const metadata = symbolMetadata[i];
629
- if (!metadata)
630
- continue;
631
- updateSymbol.run(metadata.resolvedTypeSignature, metadata.resolvedReturnType, metadata.definitionUri, metadata.definitionPath, symbol.id);
632
- updateSymbolFts.run(buildStructuralEmbeddingText({
633
- name: symbol.name,
634
- signature: symbol.signature,
635
- resolvedTypeSignature: metadata.resolvedTypeSignature,
636
- resolvedReturnType: metadata.resolvedReturnType,
637
- }), symbol.id);
638
- }
751
+ for (const s of symbols) {
752
+ tagged.push({ table: 'symbol', rowId: s.id, line: s.start_line, character: 0, name: s.name, signature: s.signature });
639
753
  }
640
754
  const callRefs = selectCallRefs.all(file.path, branch);
641
- if (callRefs.length > 0) {
642
- const callRefMetadata = await lspCoordinator.enrich({
643
- filePath: file.path,
644
- language: file.language,
645
- source,
646
- targets: callRefs.map((callRef) => ({ line: callRef.call_line, character: 0 })),
647
- });
648
- for (let i = 0; i < callRefs.length; i++) {
649
- const callRef = callRefs[i];
650
- if (!callRef)
651
- continue;
652
- const metadata = callRefMetadata[i];
653
- if (!metadata)
654
- continue;
655
- updateCallRef.run(metadata.resolvedTypeSignature, metadata.resolvedReturnType, metadata.definitionUri, metadata.definitionPath, callRef.id);
755
+ for (const cr of callRefs) {
756
+ tagged.push({ table: 'callRef', rowId: cr.id, line: cr.call_line, character: cr.call_character ?? 0 });
757
+ }
758
+ const typeRefs = selectTypeRefs.all(file.path, branch);
759
+ for (const tr of typeRefs) {
760
+ tagged.push({ table: 'typeRef', rowId: tr.id, line: tr.ref_line, character: tr.ref_character ?? 0 });
761
+ }
762
+ const relationships = selectRelationships.all(file.path, branch);
763
+ for (const r of relationships) {
764
+ tagged.push({ table: 'relationship', rowId: r.id, line: r.line, character: r.character ?? 0 });
765
+ }
766
+ if (tagged.length === 0)
767
+ continue;
768
+ const metadata = await lspCoordinator.enrich({
769
+ filePath: file.path,
770
+ language: file.language,
771
+ source,
772
+ targets: tagged.map(t => ({ line: t.line, character: t.character })),
773
+ });
774
+ for (let i = 0; i < tagged.length; i++) {
775
+ const tag = tagged[i];
776
+ const m = metadata[i];
777
+ if (!m)
778
+ continue;
779
+ switch (tag.table) {
780
+ case 'symbol':
781
+ updateSymbol.run(m.resolvedTypeSignature, m.resolvedReturnType, m.definitionUri, m.definitionPath, tag.rowId);
782
+ updateSymbolFts.run(buildStructuralEmbeddingText({
783
+ name: tag.name,
784
+ signature: tag.signature ?? null,
785
+ resolvedTypeSignature: m.resolvedTypeSignature,
786
+ resolvedReturnType: m.resolvedReturnType,
787
+ }), tag.rowId);
788
+ break;
789
+ case 'callRef':
790
+ updateCallRef.run(m.resolvedTypeSignature, m.resolvedReturnType, m.definitionUri, m.definitionPath, tag.rowId);
791
+ break;
792
+ case 'typeRef':
793
+ updateTypeRef.run(m.resolvedTypeSignature, m.definitionUri, m.definitionPath, tag.rowId);
794
+ break;
795
+ case 'relationship':
796
+ updateRelationship.run(m.definitionUri, m.definitionPath, tag.rowId);
797
+ break;
656
798
  }
657
799
  }
658
800
  }
@@ -785,20 +927,31 @@ export class IndexBuilder {
785
927
  *
786
928
  * Also stores the embedding model name and dims in `lore_meta` and
787
929
  * creates the vec0 tables if they don't exist yet.
930
+ *
931
+ * @param fileIds When provided, only embed symbols belonging to these file
932
+ * IDs (incremental mode). When omitted, embeds all symbols
933
+ * (full-build mode).
788
934
  */
789
- async embedStructural(db) {
935
+ async embedStructural(db, fileIds) {
790
936
  const embedder = this.embedder;
791
937
  setLoreMeta(db, 'embedding_model', embedder.modelName);
792
938
  setLoreMeta(db, 'embedding_dims', String(embedder.dims));
793
939
  createVec0Tables(db, embedder.dims);
794
- // Fetch all symbols that have structural text to embed.
795
- const symbols = db
796
- .prepare(`SELECT id, name, signature, resolved_type_signature, resolved_return_type
797
- FROM symbols
798
- WHERE signature IS NOT NULL
799
- OR resolved_type_signature IS NOT NULL
800
- OR resolved_return_type IS NOT NULL`)
801
- .all();
940
+ // Build the query scoped to specific files when doing an incremental update.
941
+ const baseQuery = `SELECT id, name, signature, resolved_type_signature, resolved_return_type
942
+ FROM symbols
943
+ WHERE (signature IS NOT NULL
944
+ OR resolved_type_signature IS NOT NULL
945
+ OR resolved_return_type IS NOT NULL)`;
946
+ let symbols;
947
+ if (fileIds && fileIds.length > 0) {
948
+ symbols = db
949
+ .prepare(`${baseQuery} AND file_id IN (${fileIds.map(() => '?').join(', ')})`)
950
+ .all(...fileIds);
951
+ }
952
+ else {
953
+ symbols = db.prepare(baseQuery).all();
954
+ }
802
955
  const insertEmbed = db.prepare('INSERT OR REPLACE INTO symbol_embeddings(rowid, embedding) VALUES (CAST(? AS INTEGER), json(?))');
803
956
  for (let i = 0; i < symbols.length; i += EMBED_BATCH_SIZE) {
804
957
  const batch = symbols.slice(i, i + EMBED_BATCH_SIZE);
@@ -818,16 +971,33 @@ export class IndexBuilder {
818
971
  })();
819
972
  }
820
973
  }
821
- async embedDocumentation(db) {
974
+ /**
975
+ * Embed documentation sections in batches and persist results to
976
+ * the `doc_section_embeddings` vec0 virtual table.
977
+ *
978
+ * @param docIds When provided, only embed sections belonging to these
979
+ * doc IDs (incremental mode). When omitted, embeds all
980
+ * sections (full-build mode).
981
+ */
982
+ async embedDocumentation(db, docIds) {
822
983
  const embedder = this.embedder;
823
984
  db.exec(`
824
985
  CREATE VIRTUAL TABLE IF NOT EXISTS doc_section_embeddings USING vec0(
825
986
  embedding FLOAT[${embedder.dims}]
826
987
  );
827
988
  `);
828
- const sections = db.prepare(`SELECT id, title, content
829
- FROM doc_sections
830
- ORDER BY id`).all();
989
+ let sections;
990
+ if (docIds && docIds.length > 0) {
991
+ sections = db.prepare(`SELECT id, title, content
992
+ FROM doc_sections
993
+ WHERE doc_id IN (${docIds.map(() => '?').join(', ')})
994
+ ORDER BY id`).all(...docIds);
995
+ }
996
+ else {
997
+ sections = db.prepare(`SELECT id, title, content
998
+ FROM doc_sections
999
+ ORDER BY id`).all();
1000
+ }
831
1001
  if (sections.length === 0)
832
1002
  return;
833
1003
  const insertEmbed = db.prepare('INSERT OR REPLACE INTO doc_section_embeddings(rowid, embedding) VALUES (CAST(? AS INTEGER), json(?))');
@@ -845,12 +1015,21 @@ export class IndexBuilder {
845
1015
  })();
846
1016
  }
847
1017
  }
1018
+ /**
1019
+ * Embed commit messages that haven't been embedded yet.
1020
+ *
1021
+ * Uses a `LEFT JOIN` against `commit_embeddings` to skip commits whose
1022
+ * embeddings already exist, so only newly-ingested commits are processed.
1023
+ */
848
1024
  async embedCommitMessages(db) {
849
1025
  const embedder = this.embedder;
850
- const commits = db.prepare(`SELECT rowid, message
851
- FROM commits
852
- WHERE length(trim(message)) > 0
853
- ORDER BY rowid`).all();
1026
+ // Only embed commits that don't already have an embedding row.
1027
+ const commits = db.prepare(`SELECT c.rowid, c.message
1028
+ FROM commits c
1029
+ LEFT JOIN commit_embeddings ce ON ce.rowid = c.rowid
1030
+ WHERE length(trim(c.message)) > 0
1031
+ AND ce.rowid IS NULL
1032
+ ORDER BY c.rowid`).all();
854
1033
  if (commits.length === 0)
855
1034
  return;
856
1035
  const insertEmbed = db.prepare('INSERT OR REPLACE INTO commit_embeddings(rowid, embedding) VALUES (CAST(? AS INTEGER), json(?))');