@jafreck/lore 0.3.7 → 0.3.8

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 (168) hide show
  1. package/README.md +23 -122
  2. package/dist/cli.d.ts +2 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +63 -29
  5. package/dist/cli.js.map +1 -1
  6. package/dist/db/read-only.d.ts +22 -43
  7. package/dist/db/read-only.d.ts.map +1 -1
  8. package/dist/db/read-only.js +80 -156
  9. package/dist/db/read-only.js.map +1 -1
  10. package/dist/db/schema.d.ts +9 -0
  11. package/dist/db/schema.d.ts.map +1 -1
  12. package/dist/db/schema.js +164 -85
  13. package/dist/db/schema.js.map +1 -1
  14. package/dist/discovery/poller.d.ts +6 -16
  15. package/dist/discovery/poller.d.ts.map +1 -1
  16. package/dist/discovery/poller.js +21 -52
  17. package/dist/discovery/poller.js.map +1 -1
  18. package/dist/discovery/scip-flush.d.ts +44 -0
  19. package/dist/discovery/scip-flush.d.ts.map +1 -0
  20. package/dist/discovery/scip-flush.js +67 -0
  21. package/dist/discovery/scip-flush.js.map +1 -0
  22. package/dist/discovery/watcher.d.ts +6 -16
  23. package/dist/discovery/watcher.d.ts.map +1 -1
  24. package/dist/discovery/watcher.js +22 -54
  25. package/dist/discovery/watcher.js.map +1 -1
  26. package/dist/docs/docs.d.ts +0 -2
  27. package/dist/docs/docs.d.ts.map +1 -1
  28. package/dist/docs/docs.js +0 -27
  29. package/dist/docs/docs.js.map +1 -1
  30. package/dist/index.d.ts +5 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/indexer/index.d.ts +19 -10
  35. package/dist/indexer/index.d.ts.map +1 -1
  36. package/dist/indexer/index.js +99 -24
  37. package/dist/indexer/index.js.map +1 -1
  38. package/dist/indexer/pipeline.d.ts +25 -14
  39. package/dist/indexer/pipeline.d.ts.map +1 -1
  40. package/dist/indexer/pipeline.js +8 -7
  41. package/dist/indexer/pipeline.js.map +1 -1
  42. package/dist/indexer/stages/docs-index.d.ts +1 -3
  43. package/dist/indexer/stages/docs-index.d.ts.map +1 -1
  44. package/dist/indexer/stages/docs-index.js +1 -25
  45. package/dist/indexer/stages/docs-index.js.map +1 -1
  46. package/dist/indexer/stages/index.d.ts +4 -2
  47. package/dist/indexer/stages/index.d.ts.map +1 -1
  48. package/dist/indexer/stages/index.js +3 -2
  49. package/dist/indexer/stages/index.js.map +1 -1
  50. package/dist/indexer/stages/lsp-enrichment.d.ts.map +1 -1
  51. package/dist/indexer/stages/lsp-enrichment.js +20 -1
  52. package/dist/indexer/stages/lsp-enrichment.js.map +1 -1
  53. package/dist/indexer/stages/overlay-cleanup.d.ts +31 -0
  54. package/dist/indexer/stages/overlay-cleanup.d.ts.map +1 -0
  55. package/dist/indexer/stages/overlay-cleanup.js +64 -0
  56. package/dist/indexer/stages/overlay-cleanup.js.map +1 -0
  57. package/dist/indexer/stages/reverse-deps.d.ts +13 -0
  58. package/dist/indexer/stages/reverse-deps.d.ts.map +1 -0
  59. package/dist/indexer/stages/reverse-deps.js +81 -0
  60. package/dist/indexer/stages/reverse-deps.js.map +1 -0
  61. package/dist/indexer/stages/{scip-source.d.ts → scip-indexer.d.ts} +38 -9
  62. package/dist/indexer/stages/scip-indexer.d.ts.map +1 -0
  63. package/dist/indexer/stages/{scip-source.js → scip-indexer.js} +350 -91
  64. package/dist/indexer/stages/scip-indexer.js.map +1 -0
  65. package/dist/indexer/stages/source-index.d.ts +3 -3
  66. package/dist/indexer/stages/source-index.d.ts.map +1 -1
  67. package/dist/indexer/stages/source-index.js +116 -69
  68. package/dist/indexer/stages/source-index.js.map +1 -1
  69. package/dist/lsp/enrichment.d.ts.map +1 -1
  70. package/dist/lsp/enrichment.js +1 -18
  71. package/dist/lsp/enrichment.js.map +1 -1
  72. package/dist/parsing/extractors/go.d.ts.map +1 -1
  73. package/dist/parsing/extractors/go.js +0 -38
  74. package/dist/parsing/extractors/go.js.map +1 -1
  75. package/dist/parsing/extractors/javascript.d.ts.map +1 -1
  76. package/dist/parsing/extractors/javascript.js +0 -47
  77. package/dist/parsing/extractors/javascript.js.map +1 -1
  78. package/dist/parsing/extractors/python.d.ts.map +1 -1
  79. package/dist/parsing/extractors/python.js +0 -22
  80. package/dist/parsing/extractors/python.js.map +1 -1
  81. package/dist/parsing/extractors/types.d.ts +0 -16
  82. package/dist/parsing/extractors/types.d.ts.map +1 -1
  83. package/dist/parsing/extractors/types.js +1 -1
  84. package/dist/parsing/extractors/types.js.map +1 -1
  85. package/dist/parsing/extractors/typescript.js +2 -2
  86. package/dist/parsing/extractors/typescript.js.map +1 -1
  87. package/dist/resolution/call-graph.d.ts +3 -1
  88. package/dist/resolution/call-graph.d.ts.map +1 -1
  89. package/dist/resolution/call-graph.js +15 -13
  90. package/dist/resolution/call-graph.js.map +1 -1
  91. package/dist/resolution/resolution-method.d.ts +1 -1
  92. package/dist/resolution/resolution-method.d.ts.map +1 -1
  93. package/dist/resolution/resolution-method.js +2 -0
  94. package/dist/resolution/resolution-method.js.map +1 -1
  95. package/dist/runtime.d.ts +0 -2
  96. package/dist/runtime.d.ts.map +1 -1
  97. package/dist/runtime.js.map +1 -1
  98. package/dist/scip/compdb.d.ts +38 -0
  99. package/dist/scip/compdb.d.ts.map +1 -0
  100. package/dist/scip/compdb.js +138 -0
  101. package/dist/scip/compdb.js.map +1 -0
  102. package/dist/scip/enrichment.d.ts +6 -1
  103. package/dist/scip/enrichment.d.ts.map +1 -1
  104. package/dist/scip/enrichment.js +59 -2
  105. package/dist/scip/enrichment.js.map +1 -1
  106. package/dist/scip/installer.d.ts +59 -0
  107. package/dist/scip/installer.d.ts.map +1 -0
  108. package/dist/scip/installer.js +302 -0
  109. package/dist/scip/installer.js.map +1 -0
  110. package/dist/scip/registry.d.ts +1 -1
  111. package/dist/scip/registry.d.ts.map +1 -1
  112. package/dist/scip/registry.js +33 -5
  113. package/dist/scip/registry.js.map +1 -1
  114. package/dist/server/server.d.ts +2 -6
  115. package/dist/server/server.d.ts.map +1 -1
  116. package/dist/server/server.js +6 -56
  117. package/dist/server/server.js.map +1 -1
  118. package/dist/server/tool-registry.d.ts +2 -2
  119. package/dist/server/tool-registry.d.ts.map +1 -1
  120. package/dist/server/tool-registry.js +41 -20
  121. package/dist/server/tool-registry.js.map +1 -1
  122. package/dist/server/tools/cohesion.d.ts +46 -0
  123. package/dist/server/tools/cohesion.d.ts.map +1 -0
  124. package/dist/server/tools/cohesion.js +100 -0
  125. package/dist/server/tools/cohesion.js.map +1 -0
  126. package/dist/server/tools/dependents.d.ts +139 -0
  127. package/dist/server/tools/dependents.d.ts.map +1 -0
  128. package/dist/server/tools/dependents.js +354 -0
  129. package/dist/server/tools/dependents.js.map +1 -0
  130. package/dist/server/tools/diff.d.ts +73 -0
  131. package/dist/server/tools/diff.d.ts.map +1 -0
  132. package/dist/server/tools/diff.js +157 -0
  133. package/dist/server/tools/diff.js.map +1 -0
  134. package/dist/server/tools/structure.d.ts +77 -0
  135. package/dist/server/tools/structure.d.ts.map +1 -0
  136. package/dist/server/tools/structure.js +309 -0
  137. package/dist/server/tools/structure.js.map +1 -0
  138. package/dist/server/tools/test-map.d.ts +7 -0
  139. package/dist/server/tools/test-map.d.ts.map +1 -1
  140. package/dist/server/tools/test-map.js +18 -1
  141. package/dist/server/tools/test-map.js.map +1 -1
  142. package/dist/server/tools/trace.d.ts +84 -0
  143. package/dist/server/tools/trace.d.ts.map +1 -0
  144. package/dist/server/tools/trace.js +317 -0
  145. package/dist/server/tools/trace.js.map +1 -0
  146. package/dist/testing/coverage.d.ts +11 -0
  147. package/dist/testing/coverage.d.ts.map +1 -1
  148. package/dist/testing/coverage.js +34 -2
  149. package/dist/testing/coverage.js.map +1 -1
  150. package/dist/testing/test-mapper.d.ts +1 -1
  151. package/dist/testing/test-mapper.d.ts.map +1 -1
  152. package/dist/testing/test-mapper.js +1 -0
  153. package/dist/testing/test-mapper.js.map +1 -1
  154. package/package.json +3 -1
  155. package/dist/indexer/stages/scip-enrichment.d.ts +0 -43
  156. package/dist/indexer/stages/scip-enrichment.d.ts.map +0 -1
  157. package/dist/indexer/stages/scip-enrichment.js +0 -172
  158. package/dist/indexer/stages/scip-enrichment.js.map +0 -1
  159. package/dist/indexer/stages/scip-source.d.ts.map +0 -1
  160. package/dist/indexer/stages/scip-source.js.map +0 -1
  161. package/dist/server/tools/notes.d.ts +0 -165
  162. package/dist/server/tools/notes.d.ts.map +0 -1
  163. package/dist/server/tools/notes.js +0 -214
  164. package/dist/server/tools/notes.js.map +0 -1
  165. package/dist/server/tools/routes.d.ts +0 -38
  166. package/dist/server/tools/routes.d.ts.map +0 -1
  167. package/dist/server/tools/routes.js +0 -38
  168. package/dist/server/tools/routes.js.map +0 -1
@@ -1,19 +1,25 @@
1
1
  /**
2
- * @module indexer/stages/scip-source
2
+ * @module indexer/stages/scip-indexer
3
3
  *
4
4
  * Pipeline stage: for SCIP-covered languages, populate `files`, `symbols`,
5
5
  * `symbol_refs`, `type_refs`, `symbol_relationships`, and `file_imports`
6
6
  * **directly from the SCIP index** — bypassing tree-sitter entirely.
7
7
  *
8
- * This is the SCIP-primary architecture. SCIP is the source of truth for
9
- * both the symbol table and the call graph. For each SCIP document:
8
+ * This is the single-pass SCIP architecture. SCIP is the source of truth
9
+ * for the symbol table, the call graph, **and** enrichment metadata (type
10
+ * signatures, definition locations). All data is written in one pass.
11
+ *
12
+ * For each SCIP document:
10
13
  *
11
14
  * 1. **Symbols**: Definition occurrences → `symbols` rows; kinds inferred
12
15
  * from SCIP descriptor suffixes; spans from `enclosing_range`.
16
+ * Enrichment columns (`resolved_type_signature`, `resolved_return_type`,
17
+ * `definition_uri`, `definition_path`) are populated inline.
13
18
  * 2. **Refs**: Non-definition, non-local reference occurrences →
14
19
  * `symbol_refs` rows with both `caller_id` and `callee_id` resolved
15
20
  * using containment (which symbol's span encloses this ref?) and
16
21
  * definition lookup (where is the referenced SCIP symbol defined?).
22
+ * Enrichment columns are populated inline from the same SCIP data.
17
23
  *
18
24
  * SCIP refs are inserted **pre-resolved** with `resolution_method =
19
25
  * 'scip_definition'`. The downstream resolution stage only processes
@@ -23,20 +29,32 @@
23
29
  *
24
30
  * Same tables as `SourceIndexStage`: `files`, `symbols`, `symbols_fts`,
25
31
  * `symbol_refs`, `type_refs`, `symbol_relationships`, `file_imports`.
32
+ * Additionally populates enrichment columns (type signatures, definition
33
+ * locations) inline during the same pass.
26
34
  *
27
35
  * ## Pipeline ordering
28
36
  *
29
37
  * This stage runs **before** `SourceIndexStage`. It stores which
30
- * languages and files it handled in `context.scipSourcedLanguages` and
31
- * `context.scipSourcedFiles` so the tree-sitter path can skip them.
38
+ * languages and files it handled in `context.scipSourcedLanguages`,
39
+ * `context.scipSourcedFiles`, and `context.scipCoveredLanguages` so
40
+ * `SourceIndexStage` can skip full extraction (but still compute
41
+ * tree-sitter metrics) and `LspEnrichmentStage` knows not to re-enrich
42
+ * these languages.
32
43
  */
33
44
  import * as fs from 'node:fs';
34
45
  import * as crypto from 'node:crypto';
46
+ import { tmpdir } from 'node:os';
35
47
  import { resolve } from 'node:path';
48
+ import { pathToFileURL } from 'node:url';
36
49
  import { fromBinary } from '@bufbuild/protobuf';
37
50
  import { IndexSchema, SymbolRole, } from '../../scip/scip_pb.js';
38
51
  import { normalizeTypeName } from '../../resolution/call-graph.js';
39
52
  import { SCIP_SUPPORTED_LANGUAGES, resolveScipIndexerRegistry } from '../../scip/registry.js';
53
+ import { getLogger } from '../../logger.js';
54
+ import { extractReturnType } from '../../scip/index-reader.js';
55
+ import { getSpecsForLanguage, installScipIndexer } from '../../scip/installer.js';
56
+ import { ensureCompilationDatabase } from '../../scip/compdb.js';
57
+ import { EXT_TO_LANG } from '../../discovery/walker.js';
40
58
  import { readFileSync, existsSync } from 'node:fs';
41
59
  import { join } from 'node:path';
42
60
  import { execFile } from 'node:child_process';
@@ -89,6 +107,10 @@ function inferKindFromScipSymbol(scipSymbol, docHint) {
89
107
  return 'constant';
90
108
  if (docHint.includes('(property)'))
91
109
  return 'property';
110
+ // scip-clang uses term descriptors for C/C++ functions:
111
+ // ` $ funcName(hexhash).` — the (hash) indicates a function, not a variable.
112
+ if (/\([0-9a-f]{8,}\)\.$/.test(scipSymbol))
113
+ return 'function';
92
114
  return 'variable';
93
115
  }
94
116
  // Meta: ends with :
@@ -110,12 +132,17 @@ function extractNameFromScipSymbol(scipSymbol) {
110
132
  let cleaned = scipSymbol.replace(/[.#/:]$/, '');
111
133
  // For methods, strip the disambiguator: `name(+1).` → `name`
112
134
  cleaned = cleaned.replace(/\(\+?\d*\)$/, '');
135
+ // scip-clang uses ` $ name(hash)` for C/C++ symbols — strip the hash.
136
+ // E.g., ` $ parse_analyze_fixedparams(39d222e79bbfb7c0)` → `parse_analyze_fixedparams`
137
+ cleaned = cleaned.replace(/\([0-9a-f]{8,}\)$/, '');
113
138
  // Get the last descriptor's name
114
139
  // Descriptors are separated by ., #, /, :, or ()
115
140
  const parts = cleaned.split(/[.#/:]/);
116
141
  let name = parts[parts.length - 1] || '';
117
142
  // Remove backtick escaping
118
143
  name = name.replace(/`/g, '');
144
+ // Strip leading ` $ ` prefix used by scip-clang
145
+ name = name.replace(/^\s*\$\s*/, '');
119
146
  // Handle parameter descriptors like `(paramName)`
120
147
  if (name.startsWith('(') && name.endsWith(')')) {
121
148
  name = name.slice(1, -1);
@@ -243,11 +270,14 @@ function inferTypeRefKind(sourceLines, refLine, refChar) {
243
270
  return 'other';
244
271
  }
245
272
  // ─── Stage implementation ────────────────────────────────────────────────────
246
- export class ScipSourceStage {
247
- name = 'scip-source';
273
+ export class ScipIndexerStage {
274
+ name = 'scip-indexer';
248
275
  async execute(context, mode) {
249
276
  if (!context.scip?.enabled)
250
277
  return;
278
+ // SCIP only runs during baseline builds — never during overlay updates.
279
+ if (context.layer === 'overlay')
280
+ return;
251
281
  const log = context.log;
252
282
  const rootDir = context.walkerConfig.rootDir;
253
283
  // In update mode, determine which SCIP-supported languages have changed
@@ -266,19 +296,27 @@ export class ScipSourceStage {
266
296
  }
267
297
  }
268
298
  if (staleLanguages.size === 0) {
269
- log.indexing('scip-source: no SCIP-supported languages in changed files, skipping');
299
+ log.indexing('scip-indexer: no SCIP-supported languages in changed files, skipping');
270
300
  return;
271
301
  }
272
- log.indexing('scip-source: stale languages', { languages: [...staleLanguages] });
302
+ log.indexing('scip-indexer: stale languages', { languages: [...staleLanguages] });
273
303
  }
274
- // Load SCIP index
275
- const indexBuffer = await this.loadScipIndex(context.scip, rootDir, staleLanguages);
276
- if (!indexBuffer) {
277
- log.indexing('scip-source: no SCIP index available');
304
+ // Load SCIP indexes (one per indexer that succeeds)
305
+ const indexBuffers = await this.loadScipIndexes(context.scip, rootDir, staleLanguages);
306
+ if (indexBuffers.length === 0) {
307
+ log.indexing('scip-indexer: no SCIP index available');
278
308
  return;
279
309
  }
280
- const scipIndex = fromBinary(IndexSchema, indexBuffer);
281
- log.indexing('scip-source: loaded index', {
310
+ // Parse all SCIP index buffers and combine their documents
311
+ const allDocuments = [];
312
+ const allExternalSymbols = [];
313
+ for (const buf of indexBuffers) {
314
+ const parsed = fromBinary(IndexSchema, buf);
315
+ allDocuments.push(...parsed.documents);
316
+ allExternalSymbols.push(...parsed.externalSymbols);
317
+ }
318
+ const scipIndex = { documents: allDocuments, externalSymbols: allExternalSymbols };
319
+ log.indexing('scip-indexer: loaded index', {
282
320
  documents: scipIndex.documents.length,
283
321
  externalSymbols: scipIndex.externalSymbols.length,
284
322
  });
@@ -294,7 +332,7 @@ export class ScipSourceStage {
294
332
  if (loreLang)
295
333
  coveredLanguages.add(loreLang);
296
334
  }
297
- log.indexing('scip-source: languages covered', { languages: [...coveredLanguages] });
335
+ log.indexing('scip-indexer: languages covered', { languages: [...coveredLanguages] });
298
336
  // Determine the project's SCIP symbol prefix so we can distinguish
299
337
  // internal symbols from external ones (stdlib, node_modules, etc.).
300
338
  // Internal symbols are those whose SCIP string starts with the same
@@ -350,17 +388,19 @@ export class ScipSourceStage {
350
388
  const db = context.db;
351
389
  const branch = context.branch;
352
390
  // Prepared statements
353
- const insertFile = db.prepare(`INSERT INTO files (path, branch, language, size_bytes, last_hash, source)
354
- VALUES (?, ?, ?, ?, ?, ?)`);
355
- const insertSymbol = db.prepare(`INSERT INTO symbols (file_id, name, kind, start_line, end_line, signature, doc_comment)
356
- VALUES (?, ?, ?, ?, ?, ?, ?)`);
357
- const insertCallRef = db.prepare(`INSERT INTO symbol_refs (caller_id, file_id, callee_id, callee_name, call_line, call_character, call_kind, resolution_method)
391
+ const insertFile = db.prepare(`INSERT INTO files (path, branch, language, size_bytes, last_hash, source, layer, generation)
358
392
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
359
- const insertTypeRef = db.prepare(`INSERT INTO type_refs (file_id, symbol_id, type_id, type_name, type_name_bare, ref_kind, ref_line, ref_character, resolution_method)
360
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
361
- const insertImport = db.prepare('INSERT INTO file_imports (file_id, raw_import) VALUES (?, ?)');
362
- const insertRelationship = db.prepare(`INSERT INTO symbol_relationships (file_id, source_symbol_id, target_symbol_name, relationship_type, line, character, resolution_method)
363
- VALUES (?, ?, ?, ?, ?, ?, ?)`);
393
+ const insertSymbol = db.prepare(`INSERT INTO symbols (file_id, name, kind, start_line, end_line, signature, doc_comment, resolved_type_signature, resolved_return_type, definition_uri, definition_path, layer, generation)
394
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
395
+ const insertCallRef = db.prepare(`INSERT INTO symbol_refs (caller_id, file_id, callee_id, callee_name, call_line, call_character, call_kind, resolution_method, resolved_type_signature, resolved_return_type, definition_uri, definition_path, definition_line, definition_character, layer, generation)
396
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
397
+ const insertTypeRef = db.prepare(`INSERT INTO type_refs (file_id, symbol_id, type_id, type_name, type_name_bare, ref_kind, ref_line, ref_character, resolution_method, resolved_type_signature, definition_uri, definition_path, definition_line, definition_character, layer, generation)
398
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
399
+ const insertImport = db.prepare('INSERT INTO file_imports (file_id, raw_import, resolved_id, layer, generation) VALUES (?, ?, ?, ?, ?)');
400
+ const insertRelationship = db.prepare(`INSERT INTO symbol_relationships (file_id, source_symbol_id, target_symbol_name, relationship_type, line, character, resolution_method, definition_uri, definition_path, definition_line, definition_character, layer, generation)
401
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
402
+ const layer = context.layer;
403
+ const generation = context.generation;
364
404
  // Global map: SCIP symbol string → Lore numeric symbol ID (across all files)
365
405
  const scipToLoreId = new Map();
366
406
  // Pass 1: Create files and symbols
@@ -395,7 +435,7 @@ export class ScipSourceStage {
395
435
  db.prepare('DELETE FROM files WHERE id = ?').run(existing.id);
396
436
  }
397
437
  // Insert file
398
- const fileInfo = insertFile.run(absPath, branch, loreLang, sizeBytes, hash, source);
438
+ const fileInfo = insertFile.run(absPath, branch, loreLang, sizeBytes, hash, source, layer, generation);
399
439
  const fileId = Number(fileInfo.lastInsertRowid);
400
440
  fileIdMap.set(absPath, fileId);
401
441
  coveredFiles.add(absPath);
@@ -452,13 +492,18 @@ export class ScipSourceStage {
452
492
  continue;
453
493
  const signature = extractSignatureFromDoc(firstDoc);
454
494
  const docComment = symInfo.documentation.slice(1).join('\n').trim() || null;
455
- const info = insertSymbol.run(fileId, name, kind, defLoc.startLine, defLoc.endLine, signature || null, docComment);
495
+ // Compute enrichment data inline (definition + type signature)
496
+ const resolvedTypeSig = signature || null;
497
+ const resolvedReturnType = extractReturnType(resolvedTypeSig);
498
+ const definitionUri = pathToFileURL(absPath).toString();
499
+ const info = insertSymbol.run(fileId, name, kind, defLoc.startLine, defLoc.endLine, signature || null, docComment, resolvedTypeSig, resolvedReturnType, definitionUri, absPath, layer, generation);
456
500
  const loreId = Number(info.lastInsertRowid);
457
501
  scipToLoreId.set(symInfo.symbol, loreId);
458
502
  }
459
503
  // Insert imports (from Import-role occurrences)
460
- // Prefer the actual import path from source; fall back to SCIP package
461
- const seenImports = new Set();
504
+ // Prefer the actual import path from source; fall back to SCIP package.
505
+ // Use symbolDefinitions to pre-resolve imports to target file IDs.
506
+ const seenImports = new Map(); // rawImport → resolved file ID
462
507
  for (const occ of doc.occurrences) {
463
508
  if ((occ.symbolRoles & SymbolRole.Import) !== 0 && occ.symbol) {
464
509
  const importLine = occ.range[0] ?? 0;
@@ -474,9 +519,23 @@ export class ScipSourceStage {
474
519
  const parts = occ.symbol.split(' ');
475
520
  rawImport = parts.length >= 4 ? parts[3] : occ.symbol;
476
521
  }
477
- if (rawImport && !seenImports.has(rawImport)) {
478
- seenImports.add(rawImport);
479
- insertImport.run(fileId, rawImport);
522
+ if (!rawImport)
523
+ continue;
524
+ // Resolve the import's target file via SCIP symbol → definition location
525
+ const defLoc = symbolDefinitions.get(occ.symbol);
526
+ const resolvedFileId = defLoc ? (fileIdMap.get(defLoc.filePath) ?? null) : null;
527
+ if (seenImports.has(rawImport)) {
528
+ // If we already inserted this import without a resolved_id,
529
+ // upgrade it now that we have one.
530
+ if (resolvedFileId && !seenImports.get(rawImport)) {
531
+ seenImports.set(rawImport, resolvedFileId);
532
+ db.prepare('UPDATE file_imports SET resolved_id = ? WHERE file_id = ? AND raw_import = ?')
533
+ .run(resolvedFileId, fileId, rawImport);
534
+ }
535
+ }
536
+ else {
537
+ seenImports.set(rawImport, resolvedFileId);
538
+ insertImport.run(fileId, rawImport, resolvedFileId, layer, generation);
480
539
  }
481
540
  }
482
541
  }
@@ -516,7 +575,10 @@ export class ScipSourceStage {
516
575
  // relationships for symbols whose definition is in another file or
517
576
  // external package, so defLoc may be undefined.
518
577
  const defLoc = symbolDefinitions.get(symInfo.symbol);
519
- insertRelationship.run(fileId, sourceId, targetName, relType, defLoc?.line ?? null, defLoc?.character ?? null, targetId ? 'scip_definition' : 'unresolved');
578
+ // Resolve the target's definition location for enrichment
579
+ const targetDef = symbolDefinitions.get(rel.symbol);
580
+ const relDefUri = targetDef ? pathToFileURL(targetDef.filePath).toString() : null;
581
+ insertRelationship.run(fileId, sourceId, targetName, relType, defLoc?.line ?? null, defLoc?.character ?? null, targetId ? 'scip_definition' : 'unresolved', relDefUri, targetDef?.filePath ?? null, targetDef?.line ?? null, targetDef?.character ?? null, layer, generation);
520
582
  // If we have both source and target IDs, update the resolved target
521
583
  if (targetId) {
522
584
  db.prepare('UPDATE symbol_relationships SET target_symbol_id = ? WHERE file_id = ? AND source_symbol_id = ? AND target_symbol_name = ? AND relationship_type = ?').run(targetId, fileId, sourceId, targetName, relType);
@@ -526,7 +588,7 @@ export class ScipSourceStage {
526
588
  }
527
589
  });
528
590
  processDocuments();
529
- log.indexing('scip-source: symbols inserted', {
591
+ log.indexing('scip-indexer: symbols inserted', {
530
592
  files: fileIdMap.size,
531
593
  symbols: scipToLoreId.size,
532
594
  });
@@ -655,21 +717,49 @@ export class ScipSourceStage {
655
717
  }
656
718
  if (refKind === 'type') {
657
719
  const typeRefKind = inferTypeRefKind(sourceLines, line, character);
658
- insertTypeRef.run(fileId, callerId, calleeId, calleeName, normalizeTypeName(calleeName), typeRefKind, line, character, method);
659
- typeRefsInserted++;
720
+ // Resolve enrichment metadata for the referenced type
721
+ const refDef = symbolDefinitions.get(occ.symbol);
722
+ const refInfo = symbolInfoMap.get(occ.symbol);
723
+ const refSig = refInfo ? extractSignatureFromDoc(refInfo.documentation[0] ?? '') || null : null;
724
+ const refDefUri = refDef ? pathToFileURL(refDef.filePath).toString() : null;
725
+ try {
726
+ insertTypeRef.run(fileId, callerId, calleeId ?? null, calleeName, normalizeTypeName(calleeName), typeRefKind, line, character, method, refSig, refDefUri, refDef?.filePath ?? null, refDef?.line ?? null, refDef?.character ?? null, layer, generation);
727
+ typeRefsInserted++;
728
+ }
729
+ catch {
730
+ // FK constraint failure — skip this ref
731
+ refsNoCaller++;
732
+ }
660
733
  }
661
734
  else {
662
735
  // refKind === 'call'
663
- insertCallRef.run(callerId, fileId, calleeId, calleeName, line, character, 'direct', method);
664
- refsInserted++;
665
- if (isExternal)
666
- refsExternal++;
736
+ // Skip if callee can't be resolved — FK constraint requires valid callee_id
737
+ if (!calleeId) {
738
+ refsNoCaller++;
739
+ continue;
740
+ }
741
+ // Resolve enrichment metadata for the callee
742
+ const refDef = symbolDefinitions.get(occ.symbol);
743
+ const refInfo = symbolInfoMap.get(occ.symbol);
744
+ const refSig = refInfo ? extractSignatureFromDoc(refInfo.documentation[0] ?? '') || null : null;
745
+ const refReturnType = extractReturnType(refSig);
746
+ const refDefUri = refDef ? pathToFileURL(refDef.filePath).toString() : null;
747
+ try {
748
+ insertCallRef.run(callerId, fileId, calleeId, calleeName, line, character, 'direct', method, refSig, refReturnType, refDefUri, refDef?.filePath ?? null, refDef?.line ?? null, refDef?.character ?? null, layer, generation);
749
+ refsInserted++;
750
+ if (isExternal)
751
+ refsExternal++;
752
+ }
753
+ catch {
754
+ // FK constraint failure — skip this ref
755
+ refsNoCaller++;
756
+ }
667
757
  }
668
758
  }
669
759
  }
670
760
  });
671
761
  processRefs();
672
- log.indexing('scip-source: refs inserted', {
762
+ log.indexing('scip-indexer: refs inserted', {
673
763
  callRefs: refsInserted,
674
764
  typeRefs: typeRefsInserted,
675
765
  external: refsExternal,
@@ -680,6 +770,9 @@ export class ScipSourceStage {
680
770
  // Communicate coverage to downstream stages
681
771
  context.scipSourcedLanguages = coveredLanguages;
682
772
  context.scipSourcedFiles = coveredFiles;
773
+ // Also set scipCoveredLanguages so that LspEnrichmentStage skips
774
+ // languages already fully enriched by this single SCIP pass.
775
+ context.scipCoveredLanguages = coveredLanguages;
683
776
  // Add SCIP-sourced files to context.files so later stages process them
684
777
  for (const doc of scipIndex.documents) {
685
778
  const absPath = resolve(rootDir, doc.relativePath);
@@ -693,62 +786,191 @@ export class ScipSourceStage {
693
786
  // No persistent resources to clean up
694
787
  }
695
788
  // ─── SCIP index loading ──────────────────────────────────────────────────
696
- async loadScipIndex(settings, rootDir, staleLanguages = null) {
789
+ async loadScipIndexes(settings, rootDir, staleLanguages = null) {
697
790
  // Try pre-computed index directory first
698
791
  if (settings.indexDir) {
792
+ const precomputed = [];
699
793
  // When staleLanguages is set, prefer per-language index files so
700
794
  // we only load the languages that actually need re-processing.
701
795
  if (staleLanguages) {
702
796
  for (const lang of staleLanguages) {
703
797
  const candidate = join(rootDir, settings.indexDir, `${lang}.scip`);
704
798
  if (existsSync(candidate)) {
705
- return readFileSync(candidate);
799
+ precomputed.push(readFileSync(candidate));
706
800
  }
707
801
  }
708
802
  }
709
- const candidates = [
710
- join(rootDir, settings.indexDir, 'index.scip'),
711
- // Language-specific index files
712
- ...['typescript', 'javascript', 'python', 'java', 'rust', 'c', 'cpp', 'csharp', 'ruby', 'php', 'go', 'dart'].map(lang => join(rootDir, settings.indexDir, `${lang}.scip`)),
713
- ];
714
- for (const candidate of candidates) {
715
- if (existsSync(candidate)) {
716
- return readFileSync(candidate);
803
+ if (precomputed.length === 0) {
804
+ const candidates = [
805
+ join(rootDir, settings.indexDir, 'index.scip'),
806
+ ...['typescript', 'javascript', 'python', 'java', 'rust', 'c', 'cpp', 'csharp', 'ruby', 'php', 'go', 'dart'].map(lang => join(rootDir, settings.indexDir, `${lang}.scip`)),
807
+ ];
808
+ for (const candidate of candidates) {
809
+ if (existsSync(candidate)) {
810
+ precomputed.push(readFileSync(candidate));
811
+ }
717
812
  }
718
813
  }
814
+ if (precomputed.length > 0)
815
+ return precomputed;
719
816
  }
720
817
  // Try running an indexer
721
- const resolvedIndexers = resolveScipIndexerRegistry(settings.indexers);
818
+ let resolvedIndexers = resolveScipIndexerRegistry(settings.indexers);
819
+ const log = getLogger();
820
+ // Check if any indexers are available; if not, try auto-installing
821
+ const requestedLanguages = staleLanguages ?? new Set(Object.keys(resolvedIndexers));
822
+ const hasAvailable = [...requestedLanguages].some((lang) => resolvedIndexers[lang]?.available);
823
+ if (!hasAvailable) {
824
+ const attempted = new Set();
825
+ for (const lang of requestedLanguages) {
826
+ for (const spec of getSpecsForLanguage(lang)) {
827
+ if (attempted.has(spec.command))
828
+ continue;
829
+ attempted.add(spec.command);
830
+ log.indexing(`scip-indexer: auto-installing ${spec.command} for ${lang}...`);
831
+ const result = await installScipIndexer(spec);
832
+ if (result.installed) {
833
+ log.indexing(`scip-indexer: installed ${spec.command} at ${result.path}`);
834
+ }
835
+ else {
836
+ log.indexing(`scip-indexer: could not install ${spec.command}: ${result.error ?? 'unknown'}`);
837
+ }
838
+ }
839
+ }
840
+ // Re-resolve after installation
841
+ resolvedIndexers = resolveScipIndexerRegistry(settings.indexers);
842
+ }
843
+ // Determine which SCIP-supported languages actually exist in the project
844
+ // so we don't waste time running irrelevant indexers (e.g., scip-go on a C project).
845
+ const projectLanguages = staleLanguages ?? detectProjectLanguages(resolve(rootDir));
846
+ // Run all available indexers and merge results — don't stop at the first success.
847
+ // Group by shared command to avoid running the same indexer twice (e.g., scip-java for java/scala/kotlin).
848
+ const commandsRun = new Set();
849
+ const indexBuffers = [];
722
850
  for (const [lang, indexer] of Object.entries(resolvedIndexers)) {
723
851
  if (!indexer.available)
724
852
  continue;
725
- // Per-language staleness: skip indexers for languages that haven't changed.
726
- if (staleLanguages && !staleLanguages.has(lang))
853
+ // Skip languages not present in the project
854
+ if (!projectLanguages.has(lang))
727
855
  continue;
856
+ // Don't run the same command twice (e.g., scip-clang for both c and cpp)
857
+ if (commandsRun.has(indexer.command))
858
+ continue;
859
+ commandsRun.add(indexer.command);
728
860
  try {
729
- const outputPath = join(rootDir, `.lore-scip-${lang}.scip`);
730
- const args = indexer.args.map(a => a.replace(/\{output\}/g, outputPath));
861
+ const outputPath = resolve(rootDir, `.lore-scip-${lang}.scip`);
862
+ let args = indexer.args.map(a => a.replace(/\{output\}/g, outputPath));
863
+ const cwd = resolve(rootDir);
864
+ // For C/C++: ensure a compile_commands.json exists and pass it to scip-clang
865
+ if ((lang === 'c' || lang === 'cpp') && args.some(a => a.includes('{compdb}'))) {
866
+ const compdb = await ensureCompilationDatabase(rootDir, settings.timeoutMs);
867
+ if (!compdb.path) {
868
+ log.indexing(`scip-indexer: no compile_commands.json for ${lang}, skipping`);
869
+ continue;
870
+ }
871
+ args = args.map(a => a.replace(/\{compdb\}/g, compdb.path));
872
+ }
873
+ // For TypeScript: generate a broad tsconfig so scip-typescript
874
+ // indexes ALL .ts files (including tests), not just those in the
875
+ // project's tsconfig "include" (which typically excludes tests).
876
+ let tempTsconfigPath = null;
877
+ if (lang === 'typescript') {
878
+ tempTsconfigPath = createLoreScipTsconfig(rootDir);
879
+ if (tempTsconfigPath) {
880
+ args.push(tempTsconfigPath);
881
+ }
882
+ }
883
+ // scip-clang needs a longer timeout for large C projects
884
+ const indexerTimeout = (lang === 'c' || lang === 'cpp')
885
+ ? Math.max(settings.timeoutMs, 600_000)
886
+ : settings.timeoutMs;
731
887
  const execFileAsync = promisify(execFile);
732
- await execFileAsync(indexer.command, args, {
733
- cwd: rootDir,
734
- timeout: settings.timeoutMs,
735
- });
888
+ const executablePath = indexer.resolvedPath ?? indexer.command;
889
+ try {
890
+ await execFileAsync(executablePath, args, {
891
+ cwd,
892
+ timeout: indexerTimeout,
893
+ });
894
+ }
895
+ finally {
896
+ if (tempTsconfigPath) {
897
+ try {
898
+ fs.unlinkSync(tempTsconfigPath);
899
+ }
900
+ catch { /* best effort */ }
901
+ }
902
+ }
736
903
  // Check for output
737
- for (const candidate of [outputPath, join(rootDir, 'index.scip')]) {
904
+ for (const candidate of [outputPath, resolve(rootDir, 'index.scip')]) {
738
905
  if (existsSync(candidate)) {
739
906
  const data = readFileSync(candidate);
740
907
  try {
741
908
  fs.unlinkSync(candidate);
742
909
  }
743
910
  catch { /* best effort */ }
744
- return data;
911
+ indexBuffers.push(data);
912
+ break;
745
913
  }
746
914
  }
747
915
  }
748
- catch {
916
+ catch (error) {
917
+ const msg = error instanceof Error ? error.message : String(error);
918
+ log.indexing(`scip-indexer: indexer failed for ${lang}: ${msg}`);
749
919
  continue;
750
920
  }
751
921
  }
922
+ return indexBuffers;
923
+ }
924
+ }
925
+ // ─── Temporary tsconfig for broad SCIP indexing ─────────────────────────────
926
+ /** Fields that only affect build output, not type-checking or SCIP indexing. */
927
+ const TSCONFIG_BUILD_ONLY_FIELDS = [
928
+ 'outDir', 'rootDir', 'declaration', 'declarationMap', 'declarationDir',
929
+ 'sourceMap', 'inlineSourceMap', 'inlineSources', 'composite',
930
+ 'tsBuildInfoFile', 'emitDeclarationOnly',
931
+ ];
932
+ /**
933
+ * Generate a temporary tsconfig that includes **all** `.ts`/`.tsx` files
934
+ * in the project, so `scip-typescript` indexes tests and other files
935
+ * excluded by the project's production tsconfig.
936
+ *
937
+ * The file is written to `os.tmpdir()` so the indexed repo is never mutated.
938
+ * Include/exclude globs use absolute paths rooted at `rootDir` so
939
+ * `scip-typescript` resolves source files correctly even though the
940
+ * tsconfig lives elsewhere.
941
+ *
942
+ * Strips build-only compiler options (`outDir`, `rootDir`, `declaration`,
943
+ * etc.) that would conflict with the broad `include` and are irrelevant
944
+ * for SCIP analysis. Preserves all type-checking options (`strict`,
945
+ * `paths`, `baseUrl`, etc.) so SCIP still resolves types correctly.
946
+ *
947
+ * Returns the path to the temp file, or `null` if no tsconfig exists.
948
+ */
949
+ export function createLoreScipTsconfig(rootDir) {
950
+ const log = getLogger();
951
+ const tsconfigPath = join(rootDir, 'tsconfig.json');
952
+ if (!existsSync(tsconfigPath))
953
+ return null;
954
+ try {
955
+ const raw = JSON.parse(readFileSync(tsconfigPath, 'utf8'));
956
+ const compilerOptions = { ...(raw.compilerOptions ?? {}) };
957
+ // Strip build-only fields
958
+ for (const field of TSCONFIG_BUILD_ONLY_FIELDS) {
959
+ delete compilerOptions[field];
960
+ }
961
+ // Use absolute paths so the tsconfig works from tmpdir
962
+ const absRoot = resolve(rootDir);
963
+ const loreTsconfig = {
964
+ compilerOptions,
965
+ include: [join(absRoot, '**/*.ts'), join(absRoot, '**/*.tsx')],
966
+ exclude: (raw.exclude ?? ['node_modules']).map((e) => join(absRoot, e)),
967
+ };
968
+ const outPath = join(tmpdir(), `lore-scip-${crypto.randomUUID()}.json`);
969
+ fs.writeFileSync(outPath, JSON.stringify(loreTsconfig, null, 2));
970
+ log.debug('scip', `generated broad tsconfig for SCIP: ${outPath}`);
971
+ return outPath;
972
+ }
973
+ catch {
752
974
  return null;
753
975
  }
754
976
  }
@@ -769,6 +991,10 @@ function classifyScipReference(scipSymbol) {
769
991
  // Method/function: ends with (). or (+N). (with disambiguator)
770
992
  if (/\(\+?\d*\)\.$/.test(scipSymbol))
771
993
  return 'call';
994
+ // scip-clang C/C++ functions: term descriptors ending with `(hexhash).`
995
+ // E.g., `$ parse_analyze_fixedparams(39d222e79bbfb7c0).`
996
+ if (/\([0-9a-f]{8,}\)\.$/.test(scipSymbol))
997
+ return 'call';
772
998
  // Type: ends with #
773
999
  if (scipSymbol.endsWith('#'))
774
1000
  return 'type';
@@ -871,32 +1097,65 @@ const SCIP_LANG_MAP = {
871
1097
  go: 'go',
872
1098
  dart: 'dart',
873
1099
  };
874
- const EXT_TO_LANG = {
875
- '.ts': 'typescript',
876
- '.tsx': 'typescript',
877
- '.js': 'javascript',
878
- '.jsx': 'javascript',
879
- '.mjs': 'javascript',
880
- '.cjs': 'javascript',
881
- '.py': 'python',
882
- '.java': 'java',
883
- '.scala': 'scala',
884
- '.sc': 'scala',
885
- '.kt': 'kotlin',
886
- '.kts': 'kotlin',
887
- '.rs': 'rust',
888
- '.c': 'c',
889
- '.h': 'c',
890
- '.cpp': 'cpp',
891
- '.cc': 'cpp',
892
- '.cxx': 'cpp',
893
- '.hpp': 'cpp',
894
- '.cs': 'csharp',
895
- '.rb': 'ruby',
896
- '.php': 'php',
897
- '.go': 'go',
898
- '.dart': 'dart',
899
- };
1100
+ /**
1101
+ * Quick scan of the project root to detect which SCIP-supported languages
1102
+ * are present. Checks for telltale file extensions and build files.
1103
+ * Only scans top-level + one directory deep to stay fast.
1104
+ */
1105
+ function detectProjectLanguages(rootDir) {
1106
+ const found = new Set();
1107
+ const langIndicators = {
1108
+ typescript: ['tsconfig.json', 'package.json'],
1109
+ python: ['setup.py', 'pyproject.toml', 'requirements.txt'],
1110
+ java: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
1111
+ rust: ['Cargo.toml'],
1112
+ c: ['Makefile', 'CMakeLists.txt', 'meson.build', 'configure', 'configure.ac'],
1113
+ cpp: ['CMakeLists.txt', 'meson.build'],
1114
+ csharp: ['.csproj', '.sln'],
1115
+ ruby: ['Gemfile'],
1116
+ go: ['go.mod'],
1117
+ php: ['composer.json'],
1118
+ dart: ['pubspec.yaml'],
1119
+ };
1120
+ // Check for language indicator files at the root
1121
+ for (const [lang, indicators] of Object.entries(langIndicators)) {
1122
+ for (const indicator of indicators) {
1123
+ if (existsSync(join(rootDir, indicator))) {
1124
+ found.add(lang);
1125
+ break;
1126
+ }
1127
+ }
1128
+ }
1129
+ // Quick extension scan: read first-level directory entries
1130
+ try {
1131
+ const entries = fs.readdirSync(rootDir, { withFileTypes: true });
1132
+ for (const entry of entries) {
1133
+ if (entry.isFile()) {
1134
+ const ext = entry.name.slice(entry.name.lastIndexOf('.')).toLowerCase();
1135
+ const lang = EXT_TO_LANG[ext];
1136
+ if (lang && SCIP_SUPPORTED_LANGUAGES.has(lang))
1137
+ found.add(lang);
1138
+ }
1139
+ else if (entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.')) {
1140
+ // One level deep
1141
+ try {
1142
+ const subEntries = fs.readdirSync(join(rootDir, entry.name), { withFileTypes: true });
1143
+ for (const sub of subEntries.slice(0, 50)) { // Limit to avoid scanning huge dirs
1144
+ if (sub.isFile()) {
1145
+ const ext = sub.name.slice(sub.name.lastIndexOf('.')).toLowerCase();
1146
+ const lang = EXT_TO_LANG[ext];
1147
+ if (lang && SCIP_SUPPORTED_LANGUAGES.has(lang))
1148
+ found.add(lang);
1149
+ }
1150
+ }
1151
+ }
1152
+ catch { /* ignore permission errors */ }
1153
+ }
1154
+ }
1155
+ }
1156
+ catch { /* ignore */ }
1157
+ return found;
1158
+ }
900
1159
  /**
901
1160
  * Determine the Lore language for a SCIP document.
902
1161
  *
@@ -921,4 +1180,4 @@ function inferLoreLanguage(scipLanguage, relativePath) {
921
1180
  // ─── Test-visible helpers ───────────────────────────────────────────────────
922
1181
  // Exported for unit testing only. Not part of the public API.
923
1182
  export { estimateSymbolEndLine as _estimateSymbolEndLine, inferTypeRefKind as _inferTypeRefKind, extractImportPathFromSource as _extractImportPathFromSource, inferKindFromScipSymbol as _inferKindFromScipSymbol, inferLoreLanguage as _inferLoreLanguage, classifyScipReference as _classifyScipReference, extractNameFromScipSymbol as _extractNameFromScipSymbol, };
924
- //# sourceMappingURL=scip-source.js.map
1183
+ //# sourceMappingURL=scip-indexer.js.map