gitnexus 1.6.3-rc.8 → 1.6.3

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 (285) hide show
  1. package/README.md +21 -5
  2. package/dist/_shared/graph/types.d.ts +16 -0
  3. package/dist/_shared/graph/types.d.ts.map +1 -1
  4. package/dist/_shared/index.d.ts +20 -2
  5. package/dist/_shared/index.d.ts.map +1 -1
  6. package/dist/_shared/index.js +11 -0
  7. package/dist/_shared/index.js.map +1 -1
  8. package/dist/_shared/scope-resolution/def-index.js +2 -2
  9. package/dist/_shared/scope-resolution/def-index.js.map +1 -1
  10. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +8 -0
  11. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -1
  12. package/dist/_shared/scope-resolution/method-dispatch-index.js +2 -2
  13. package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -1
  14. package/dist/_shared/scope-resolution/module-scope-index.d.ts +8 -0
  15. package/dist/_shared/scope-resolution/module-scope-index.d.ts.map +1 -1
  16. package/dist/_shared/scope-resolution/module-scope-index.js +10 -2
  17. package/dist/_shared/scope-resolution/module-scope-index.js.map +1 -1
  18. package/dist/_shared/scope-resolution/parsed-file.d.ts +76 -0
  19. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -0
  20. package/dist/_shared/scope-resolution/parsed-file.js +54 -0
  21. package/dist/_shared/scope-resolution/parsed-file.js.map +1 -0
  22. package/dist/_shared/scope-resolution/position-index.d.ts +12 -0
  23. package/dist/_shared/scope-resolution/position-index.d.ts.map +1 -1
  24. package/dist/_shared/scope-resolution/position-index.js +2 -2
  25. package/dist/_shared/scope-resolution/position-index.js.map +1 -1
  26. package/dist/_shared/scope-resolution/qualified-name-index.js +2 -2
  27. package/dist/_shared/scope-resolution/qualified-name-index.js.map +1 -1
  28. package/dist/_shared/scope-resolution/reference-site.d.ts +75 -0
  29. package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -0
  30. package/dist/_shared/scope-resolution/reference-site.js +24 -0
  31. package/dist/_shared/scope-resolution/reference-site.js.map +1 -0
  32. package/dist/_shared/scope-resolution/registries/class-registry.d.ts +27 -0
  33. package/dist/_shared/scope-resolution/registries/class-registry.d.ts.map +1 -0
  34. package/dist/_shared/scope-resolution/registries/class-registry.js +30 -0
  35. package/dist/_shared/scope-resolution/registries/class-registry.js.map +1 -0
  36. package/dist/_shared/scope-resolution/registries/context.d.ts +69 -0
  37. package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -0
  38. package/dist/_shared/scope-resolution/registries/context.js +44 -0
  39. package/dist/_shared/scope-resolution/registries/context.js.map +1 -0
  40. package/dist/_shared/scope-resolution/registries/evidence.d.ts +56 -0
  41. package/dist/_shared/scope-resolution/registries/evidence.d.ts.map +1 -0
  42. package/dist/_shared/scope-resolution/registries/evidence.js +150 -0
  43. package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -0
  44. package/dist/_shared/scope-resolution/registries/field-registry.d.ts +26 -0
  45. package/dist/_shared/scope-resolution/registries/field-registry.d.ts.map +1 -0
  46. package/dist/_shared/scope-resolution/registries/field-registry.js +31 -0
  47. package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -0
  48. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +81 -0
  49. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -0
  50. package/dist/_shared/scope-resolution/registries/lookup-core.js +332 -0
  51. package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -0
  52. package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts +33 -0
  53. package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts.map +1 -0
  54. package/dist/_shared/scope-resolution/registries/lookup-qualified.js +56 -0
  55. package/dist/_shared/scope-resolution/registries/lookup-qualified.js.map +1 -0
  56. package/dist/_shared/scope-resolution/registries/method-registry.d.ts +36 -0
  57. package/dist/_shared/scope-resolution/registries/method-registry.d.ts.map +1 -0
  58. package/dist/_shared/scope-resolution/registries/method-registry.js +32 -0
  59. package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -0
  60. package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts +43 -0
  61. package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts.map +1 -0
  62. package/dist/_shared/scope-resolution/registries/tie-breaks.js +60 -0
  63. package/dist/_shared/scope-resolution/registries/tie-breaks.js.map +1 -0
  64. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts +1 -10
  65. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -1
  66. package/dist/_shared/scope-resolution/resolve-type-ref.js +6 -0
  67. package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -1
  68. package/dist/_shared/scope-resolution/scope-tree.d.ts +4 -4
  69. package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -1
  70. package/dist/_shared/scope-resolution/scope-tree.js +3 -2
  71. package/dist/_shared/scope-resolution/scope-tree.js.map +1 -1
  72. package/dist/_shared/scope-resolution/shadow/aggregate.d.ts +6 -2
  73. package/dist/_shared/scope-resolution/shadow/aggregate.d.ts.map +1 -1
  74. package/dist/_shared/scope-resolution/shadow/aggregate.js +5 -0
  75. package/dist/_shared/scope-resolution/shadow/aggregate.js.map +1 -1
  76. package/dist/_shared/scope-resolution/types.d.ts +11 -0
  77. package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
  78. package/dist/cli/ai-context.js +35 -4
  79. package/dist/cli/analyze.d.ts +27 -0
  80. package/dist/cli/analyze.js +31 -1
  81. package/dist/cli/clean.js +19 -1
  82. package/dist/cli/group.js +73 -0
  83. package/dist/cli/index-repo.js +8 -1
  84. package/dist/cli/index.js +26 -1
  85. package/dist/cli/list.js +11 -1
  86. package/dist/cli/remove.d.ts +30 -0
  87. package/dist/cli/remove.js +99 -0
  88. package/dist/cli/setup.js +185 -57
  89. package/dist/cli/tool.d.ts +5 -0
  90. package/dist/cli/tool.js +42 -0
  91. package/dist/config/ignore-service.d.ts +9 -0
  92. package/dist/config/ignore-service.js +80 -13
  93. package/dist/core/embedding-mode.d.ts +30 -0
  94. package/dist/core/embedding-mode.js +30 -0
  95. package/dist/core/embeddings/ast-utils.js +22 -22
  96. package/dist/core/embeddings/chunker.js +30 -25
  97. package/dist/core/embeddings/embedding-pipeline.d.ts +6 -0
  98. package/dist/core/embeddings/embedding-pipeline.js +15 -6
  99. package/dist/core/embeddings/text-generator.d.ts +1 -1
  100. package/dist/core/embeddings/text-generator.js +33 -24
  101. package/dist/core/embeddings/types.d.ts +43 -1
  102. package/dist/core/embeddings/types.js +101 -29
  103. package/dist/core/git-staleness.d.ts +18 -0
  104. package/dist/core/git-staleness.js +108 -0
  105. package/dist/core/graph/graph.js +115 -20
  106. package/dist/core/graph/types.d.ts +12 -1
  107. package/dist/core/group/config-parser.d.ts +4 -0
  108. package/dist/core/group/config-parser.js +18 -1
  109. package/dist/core/group/cross-impact.d.ts +41 -0
  110. package/dist/core/group/cross-impact.js +441 -0
  111. package/dist/core/group/extractors/http-patterns/php.js +126 -18
  112. package/dist/core/group/group-path-utils.d.ts +17 -0
  113. package/dist/core/group/group-path-utils.js +40 -0
  114. package/dist/core/group/resolve-at-member.d.ts +10 -0
  115. package/dist/core/group/resolve-at-member.js +31 -0
  116. package/dist/core/group/service.d.ts +9 -0
  117. package/dist/core/group/service.js +259 -25
  118. package/dist/core/group/types.d.ts +30 -0
  119. package/dist/core/ingestion/ast-cache.d.ts +16 -1
  120. package/dist/core/ingestion/ast-cache.js +14 -2
  121. package/dist/core/ingestion/call-processor.js +9 -0
  122. package/dist/core/ingestion/emit-references.d.ts +88 -0
  123. package/dist/core/ingestion/emit-references.js +229 -0
  124. package/dist/core/ingestion/filesystem-walker.js +6 -4
  125. package/dist/core/ingestion/finalize-orchestrator.d.ts +63 -0
  126. package/dist/core/ingestion/finalize-orchestrator.js +139 -0
  127. package/dist/core/ingestion/framework-detection.js +6 -2
  128. package/dist/core/ingestion/import-processor.js +4 -0
  129. package/dist/core/ingestion/import-resolvers/python.js +9 -6
  130. package/dist/core/ingestion/import-target-adapter.d.ts +73 -0
  131. package/dist/core/ingestion/import-target-adapter.js +95 -0
  132. package/dist/core/ingestion/language-provider.d.ts +36 -33
  133. package/dist/core/ingestion/languages/csharp/accessor-unwrap.d.ts +21 -0
  134. package/dist/core/ingestion/languages/csharp/accessor-unwrap.js +56 -0
  135. package/dist/core/ingestion/languages/csharp/arity-metadata.d.ts +26 -0
  136. package/dist/core/ingestion/languages/csharp/arity-metadata.js +46 -0
  137. package/dist/core/ingestion/languages/csharp/arity.d.ts +23 -0
  138. package/dist/core/ingestion/languages/csharp/arity.js +37 -0
  139. package/dist/core/ingestion/languages/csharp/cache-stats.d.ts +15 -0
  140. package/dist/core/ingestion/languages/csharp/cache-stats.js +26 -0
  141. package/dist/core/ingestion/languages/csharp/captures.d.ts +19 -0
  142. package/dist/core/ingestion/languages/csharp/captures.js +249 -0
  143. package/dist/core/ingestion/languages/csharp/import-decomposer.d.ts +19 -0
  144. package/dist/core/ingestion/languages/csharp/import-decomposer.js +93 -0
  145. package/dist/core/ingestion/languages/csharp/import-target.d.ts +25 -0
  146. package/dist/core/ingestion/languages/csharp/import-target.js +123 -0
  147. package/dist/core/ingestion/languages/csharp/index.d.ts +82 -0
  148. package/dist/core/ingestion/languages/csharp/index.js +82 -0
  149. package/dist/core/ingestion/languages/csharp/interpret.d.ts +15 -0
  150. package/dist/core/ingestion/languages/csharp/interpret.js +132 -0
  151. package/dist/core/ingestion/languages/csharp/merge-bindings.d.ts +27 -0
  152. package/dist/core/ingestion/languages/csharp/merge-bindings.js +55 -0
  153. package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +50 -0
  154. package/dist/core/ingestion/languages/csharp/namespace-siblings.js +374 -0
  155. package/dist/core/ingestion/languages/csharp/query.d.ts +35 -0
  156. package/dist/core/ingestion/languages/csharp/query.js +515 -0
  157. package/dist/core/ingestion/languages/csharp/receiver-binding.d.ts +31 -0
  158. package/dist/core/ingestion/languages/csharp/receiver-binding.js +135 -0
  159. package/dist/core/ingestion/languages/csharp/scope-resolver.d.ts +10 -0
  160. package/dist/core/ingestion/languages/csharp/scope-resolver.js +63 -0
  161. package/dist/core/ingestion/languages/csharp/simple-hooks.d.ts +53 -0
  162. package/dist/core/ingestion/languages/csharp/simple-hooks.js +76 -0
  163. package/dist/core/ingestion/languages/csharp.js +14 -0
  164. package/dist/core/ingestion/languages/python/arity-metadata.d.ts +24 -0
  165. package/dist/core/ingestion/languages/python/arity-metadata.js +45 -0
  166. package/dist/core/ingestion/languages/python/arity.d.ts +22 -0
  167. package/dist/core/ingestion/languages/python/arity.js +38 -0
  168. package/dist/core/ingestion/languages/python/cache-stats.d.ts +17 -0
  169. package/dist/core/ingestion/languages/python/cache-stats.js +28 -0
  170. package/dist/core/ingestion/languages/python/captures.d.ts +19 -0
  171. package/dist/core/ingestion/languages/python/captures.js +106 -0
  172. package/dist/core/ingestion/languages/python/import-decomposer.d.ts +15 -0
  173. package/dist/core/ingestion/languages/python/import-decomposer.js +112 -0
  174. package/dist/core/ingestion/languages/python/import-target.d.ts +21 -0
  175. package/dist/core/ingestion/languages/python/import-target.js +99 -0
  176. package/dist/core/ingestion/languages/python/index.d.ts +80 -0
  177. package/dist/core/ingestion/languages/python/index.js +80 -0
  178. package/dist/core/ingestion/languages/python/interpret.d.ts +15 -0
  179. package/dist/core/ingestion/languages/python/interpret.js +191 -0
  180. package/dist/core/ingestion/languages/python/merge-bindings.d.ts +16 -0
  181. package/dist/core/ingestion/languages/python/merge-bindings.js +44 -0
  182. package/dist/core/ingestion/languages/python/query.d.ts +9 -0
  183. package/dist/core/ingestion/languages/python/query.js +267 -0
  184. package/dist/core/ingestion/languages/python/receiver-binding.d.ts +21 -0
  185. package/dist/core/ingestion/languages/python/receiver-binding.js +116 -0
  186. package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -0
  187. package/dist/core/ingestion/languages/python/scope-resolver.js +53 -0
  188. package/dist/core/ingestion/languages/python/simple-hooks.d.ts +23 -0
  189. package/dist/core/ingestion/languages/python/simple-hooks.js +35 -0
  190. package/dist/core/ingestion/languages/python.js +14 -0
  191. package/dist/core/ingestion/model/method-registry.d.ts +9 -0
  192. package/dist/core/ingestion/model/method-registry.js +4 -0
  193. package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +59 -0
  194. package/dist/core/ingestion/model/scope-resolution-indexes.js +42 -0
  195. package/dist/core/ingestion/model/semantic-model.d.ts +64 -0
  196. package/dist/core/ingestion/model/semantic-model.js +55 -0
  197. package/dist/core/ingestion/mro-processor.js +38 -22
  198. package/dist/core/ingestion/parsing-processor.d.ts +18 -1
  199. package/dist/core/ingestion/parsing-processor.js +45 -11
  200. package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
  201. package/dist/core/ingestion/pipeline-phases/index.js +1 -0
  202. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +10 -0
  203. package/dist/core/ingestion/pipeline-phases/parse-impl.js +17 -2
  204. package/dist/core/ingestion/pipeline-phases/parse.d.ts +18 -0
  205. package/dist/core/ingestion/pipeline.js +2 -1
  206. package/dist/core/ingestion/registry-primary-flag.d.ts +86 -0
  207. package/dist/core/ingestion/registry-primary-flag.js +111 -0
  208. package/dist/core/ingestion/resolve-references.d.ts +63 -0
  209. package/dist/core/ingestion/resolve-references.js +175 -0
  210. package/dist/core/ingestion/scope-extractor-bridge.d.ts +32 -0
  211. package/dist/core/ingestion/scope-extractor-bridge.js +44 -0
  212. package/dist/core/ingestion/scope-extractor.d.ts +86 -0
  213. package/dist/core/ingestion/scope-extractor.js +758 -0
  214. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +372 -0
  215. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +212 -0
  216. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +43 -0
  217. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +79 -0
  218. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +57 -0
  219. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +112 -0
  220. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +17 -0
  221. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +46 -0
  222. package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +19 -0
  223. package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +30 -0
  224. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +37 -0
  225. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +113 -0
  226. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +38 -0
  227. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +73 -0
  228. package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +42 -0
  229. package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +198 -0
  230. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +27 -0
  231. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +131 -0
  232. package/dist/core/ingestion/scope-resolution/passes/imported-return-types.d.ts +48 -0
  233. package/dist/core/ingestion/scope-resolution/passes/imported-return-types.js +130 -0
  234. package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +42 -0
  235. package/dist/core/ingestion/scope-resolution/passes/mro.js +99 -0
  236. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +26 -0
  237. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +61 -0
  238. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +46 -0
  239. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +327 -0
  240. package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +47 -0
  241. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +130 -0
  242. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +68 -0
  243. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +125 -0
  244. package/dist/core/ingestion/scope-resolution/pipeline/registry.d.ts +17 -0
  245. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +21 -0
  246. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +66 -0
  247. package/dist/core/ingestion/scope-resolution/pipeline/run.js +157 -0
  248. package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +36 -0
  249. package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +52 -0
  250. package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +127 -0
  251. package/dist/core/ingestion/scope-resolution/scope/walkers.js +349 -0
  252. package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +52 -0
  253. package/dist/core/ingestion/scope-resolution/workspace-index.js +61 -0
  254. package/dist/core/ingestion/shadow-harness.d.ts +113 -0
  255. package/dist/core/ingestion/shadow-harness.js +148 -0
  256. package/dist/core/ingestion/utils/ast-helpers.d.ts +19 -1
  257. package/dist/core/ingestion/utils/ast-helpers.js +70 -0
  258. package/dist/core/ingestion/utils/max-file-size.d.ts +20 -0
  259. package/dist/core/ingestion/utils/max-file-size.js +52 -0
  260. package/dist/core/ingestion/workers/parse-worker.d.ts +9 -0
  261. package/dist/core/ingestion/workers/parse-worker.js +57 -21
  262. package/dist/core/lbug/lbug-adapter.d.ts +22 -2
  263. package/dist/core/lbug/lbug-adapter.js +58 -14
  264. package/dist/core/lbug/pool-adapter.d.ts +17 -0
  265. package/dist/core/lbug/pool-adapter.js +24 -14
  266. package/dist/core/run-analyze.d.ts +32 -0
  267. package/dist/core/run-analyze.js +74 -19
  268. package/dist/core/search/bm25-index.d.ts +18 -0
  269. package/dist/core/search/bm25-index.js +125 -12
  270. package/dist/core/tree-sitter/parser-loader.js +6 -1
  271. package/dist/mcp/local/local-backend.d.ts +67 -3
  272. package/dist/mcp/local/local-backend.js +296 -34
  273. package/dist/mcp/resources.d.ts +31 -0
  274. package/dist/mcp/resources.js +100 -17
  275. package/dist/mcp/tools.d.ts +4 -1
  276. package/dist/mcp/tools.js +75 -54
  277. package/dist/server/api.js +6 -2
  278. package/dist/storage/git.d.ts +49 -0
  279. package/dist/storage/git.js +111 -0
  280. package/dist/storage/repo-manager.d.ts +246 -1
  281. package/dist/storage/repo-manager.js +391 -9
  282. package/package.json +7 -6
  283. package/scripts/bench-scope-resolution.ts +134 -0
  284. package/scripts/ci-list-migrated-languages.ts +24 -0
  285. package/skills/gitnexus-cli.md +1 -0
@@ -110,6 +110,17 @@ let conn = null;
110
110
  let currentDbPath = null;
111
111
  let ftsLoaded = false;
112
112
  let vectorExtensionLoaded = false;
113
+ /**
114
+ * In-process cache of FTS indexes that have been ensured against the current
115
+ * connection. Prevents repeated `CALL CREATE_FTS_INDEX` round-trips inside a
116
+ * single CLI/MCP session — the first call to `ensureFTSIndex` for a given
117
+ * `(tableName, indexName)` pays the LadybugDB cost (~440 ms even when the
118
+ * index already exists on disk), subsequent calls are a Set lookup. Cleared
119
+ * by `closeLbug` so a re-init starts fresh.
120
+ *
121
+ * Key format: `${tableName}:${indexName}`.
122
+ */
123
+ const ensuredFTSIndexes = new Set();
113
124
  /**
114
125
  * Check if an error indicates a missing column or table (schema-level problem)
115
126
  * rather than a transient/connection error. Used for legacy DB fallback logic.
@@ -935,6 +946,7 @@ export const closeLbug = async () => {
935
946
  currentDbPath = null;
936
947
  ftsLoaded = false;
937
948
  vectorExtensionLoaded = false;
949
+ ensuredFTSIndexes.clear();
938
950
  };
939
951
  export const isLbugReady = () => conn !== null && db !== null;
940
952
  /**
@@ -1014,36 +1026,50 @@ export const getEmbeddingTableName = () => EMBEDDING_TABLE_NAME;
1014
1026
  // ============================================================================
1015
1027
  /**
1016
1028
  * Load the FTS extension (required before using FTS functions).
1017
- * Safe to call multiple times — tracks loaded state via module-level ftsLoaded.
1029
+ *
1030
+ * Safe to call multiple times — when invoked without arguments, tracks loaded
1031
+ * state via module-level `ftsLoaded`. When invoked with an explicit
1032
+ * connection, loads on that connection and returns whether the load
1033
+ * succeeded — letting callers (e.g. the pool adapter) track their own state.
1034
+ *
1035
+ * Tries `LOAD EXTENSION fts` first so previously-cached installs skip the
1036
+ * network entirely; falls back to `INSTALL` + `LOAD` only when the extension
1037
+ * hasn't been cached yet.
1018
1038
  */
1019
- export const loadFTSExtension = async () => {
1020
- if (ftsLoaded)
1021
- return;
1022
- if (!conn) {
1039
+ export const loadFTSExtension = async (targetConn) => {
1040
+ const useModuleState = targetConn === undefined;
1041
+ if (useModuleState && ftsLoaded)
1042
+ return true;
1043
+ const c = targetConn ?? conn;
1044
+ if (!c) {
1023
1045
  throw new Error('LadybugDB not initialized. Call initLbug first.');
1024
1046
  }
1047
+ const markLoaded = () => {
1048
+ if (useModuleState)
1049
+ ftsLoaded = true;
1050
+ return true;
1051
+ };
1025
1052
  try {
1026
1053
  // Try loading locally first (no network required)
1027
- await conn.query('LOAD EXTENSION fts');
1028
- ftsLoaded = true;
1054
+ await c.query('LOAD EXTENSION fts');
1055
+ return markLoaded();
1029
1056
  }
1030
1057
  catch {
1031
1058
  // Fall back to install + load (requires network)
1032
1059
  try {
1033
- await conn.query('INSTALL fts');
1034
- await conn.query('LOAD EXTENSION fts');
1035
- ftsLoaded = true;
1060
+ await c.query('INSTALL fts');
1061
+ await c.query('LOAD EXTENSION fts');
1062
+ return markLoaded();
1036
1063
  }
1037
1064
  catch (err) {
1038
1065
  const msg = err?.message || '';
1039
1066
  if (msg.includes('already loaded') ||
1040
1067
  msg.includes('already installed') ||
1041
1068
  msg.includes('already exists')) {
1042
- ftsLoaded = true;
1043
- }
1044
- else {
1045
- console.error('GitNexus: FTS extension load failed:', msg);
1069
+ return markLoaded();
1046
1070
  }
1071
+ console.error('GitNexus: FTS extension load failed:', msg);
1072
+ return false;
1047
1073
  }
1048
1074
  }
1049
1075
  };
@@ -1097,6 +1123,24 @@ export const createFTSIndex = async (tableName, indexName, properties, stemmer =
1097
1123
  }
1098
1124
  }
1099
1125
  };
1126
+ /**
1127
+ * Lazy-create an FTS index, caching the fact in-process.
1128
+ *
1129
+ * Used by `queryFTS` so that `analyze` doesn't pay the ~440 ms × 5 fixed
1130
+ * LadybugDB cost up-front (it dominates analyze on small repos). Instead,
1131
+ * the cost is moved to the first `query`/`context` call in a session,
1132
+ * where it's amortised across many lookups.
1133
+ *
1134
+ * Safe to call repeatedly — the in-process Set guarantees only the first
1135
+ * call hits LadybugDB. `closeLbug` clears the cache so re-init starts fresh.
1136
+ */
1137
+ export const ensureFTSIndex = async (tableName, indexName, properties, stemmer = 'porter') => {
1138
+ const key = `${tableName}:${indexName}`;
1139
+ if (ensuredFTSIndexes.has(key))
1140
+ return;
1141
+ await createFTSIndex(tableName, indexName, properties, stemmer);
1142
+ ensuredFTSIndexes.add(key);
1143
+ };
1100
1144
  /**
1101
1145
  * Query a full-text search index
1102
1146
  * @param tableName - The node table name
@@ -15,6 +15,22 @@
15
15
  * from the same Database is the officially supported concurrency pattern.
16
16
  */
17
17
  import lbug from '@ladybugdb/core';
18
+ /**
19
+ * Listeners notified when a pool entry is torn down (LRU eviction, idle
20
+ * timeout, explicit close). Used by upper layers (e.g. the BM25 search
21
+ * module) to invalidate per-repo caches that must not outlive the pool
22
+ * entry that produced them.
23
+ *
24
+ * Listeners run synchronously inside `closeOne` after the pool entry has
25
+ * been removed; throwing listeners are isolated so one bad listener does
26
+ * not prevent others from firing or break teardown.
27
+ */
28
+ type PoolCloseListener = (repoId: string) => void;
29
+ /**
30
+ * Subscribe to pool-close events. Returns a disposer that removes the
31
+ * listener (handy for tests).
32
+ */
33
+ export declare function addPoolCloseListener(listener: PoolCloseListener): () => void;
18
34
  /** Saved real stdout/stderr write — used to silence native module output without race conditions */
19
35
  export declare const realStdoutWrite: any;
20
36
  export declare const realStderrWrite: any;
@@ -74,3 +90,4 @@ export declare const isLbugReady: (repoId: string) => boolean;
74
90
  export declare const CYPHER_WRITE_RE: RegExp;
75
91
  /** Check if a Cypher query contains write operations */
76
92
  export declare function isWriteQuery(query: string): boolean;
93
+ export {};
@@ -16,7 +16,19 @@
16
16
  */
17
17
  import fs from 'fs/promises';
18
18
  import lbug from '@ladybugdb/core';
19
+ import { loadFTSExtension } from './lbug-adapter.js';
19
20
  const pool = new Map();
21
+ const poolCloseListeners = new Set();
22
+ /**
23
+ * Subscribe to pool-close events. Returns a disposer that removes the
24
+ * listener (handy for tests).
25
+ */
26
+ export function addPoolCloseListener(listener) {
27
+ poolCloseListeners.add(listener);
28
+ return () => {
29
+ poolCloseListeners.delete(listener);
30
+ };
31
+ }
20
32
  const dbCache = new Map();
21
33
  /** Max repos in the pool (LRU eviction) */
22
34
  const MAX_POOL_SIZE = 5;
@@ -119,6 +131,16 @@ function closeOne(repoId) {
119
131
  }
120
132
  }
121
133
  pool.delete(repoId);
134
+ // Notify listeners AFTER the pool entry is gone so any cache-invalidation
135
+ // they perform is consistent with `isLbugReady(repoId) === false`.
136
+ for (const listener of poolCloseListeners) {
137
+ try {
138
+ listener(repoId);
139
+ }
140
+ catch {
141
+ // Isolate listener failures — teardown must complete.
142
+ }
143
+ }
122
144
  }
123
145
  /**
124
146
  * Create a new Connection from a repo's Database.
@@ -263,13 +285,7 @@ async function doInitLbug(repoId, dbPath) {
263
285
  // Done BEFORE pool registration so no concurrent checkout can grab
264
286
  // the connection while the async FTS load is in progress.
265
287
  if (!shared.ftsLoaded) {
266
- try {
267
- await available[0].query('LOAD EXTENSION fts');
268
- shared.ftsLoaded = true;
269
- }
270
- catch {
271
- // Extension may not be installed — FTS queries will fail gracefully
272
- }
288
+ shared.ftsLoaded = await loadFTSExtension(available[0]);
273
289
  }
274
290
  // Load VECTOR extension once per shared Database for semantic search support.
275
291
  if (!shared.vectorLoaded) {
@@ -335,13 +351,7 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
335
351
  }
336
352
  // Load FTS extension if not already loaded on this Database
337
353
  if (!shared.ftsLoaded) {
338
- try {
339
- await available[0].query('LOAD EXTENSION fts');
340
- shared.ftsLoaded = true;
341
- }
342
- catch {
343
- // Extension may already be loaded or not installed
344
- }
354
+ shared.ftsLoaded = await loadFTSExtension(available[0]);
345
355
  }
346
356
  // Load VECTOR extension for semantic search support
347
357
  if (!shared.vectorLoaded) {
@@ -13,13 +13,43 @@ export interface AnalyzeCallbacks {
13
13
  onLog?: (message: string) => void;
14
14
  }
15
15
  export interface AnalyzeOptions {
16
+ /**
17
+ * Force a full re-index of the pipeline. Callers may OR this with
18
+ * other flags that imply re-analysis (e.g. `--skills`), so the value
19
+ * here is the PIPELINE-force signal, NOT the registry-collision
20
+ * bypass. See `allowDuplicateName` below.
21
+ */
16
22
  force?: boolean;
17
23
  embeddings?: boolean;
24
+ /**
25
+ * Explicitly drop any embeddings present in the existing index instead of
26
+ * preserving them. Only meaningful when `embeddings` is false/undefined:
27
+ * the default behavior in that case is to load the previously generated
28
+ * embeddings and re-insert them after the rebuild so a routine
29
+ * re-analyze does not silently wipe a long embedding pass (#issue: analyze
30
+ * silently wipes existing embeddings when run without --embeddings).
31
+ */
32
+ dropEmbeddings?: boolean;
18
33
  skipGit?: boolean;
19
34
  /** Skip AGENTS.md and CLAUDE.md gitnexus block updates. */
20
35
  skipAgentsMd?: boolean;
21
36
  /** Omit volatile symbol/relationship counts from AGENTS.md and CLAUDE.md. */
22
37
  noStats?: boolean;
38
+ /**
39
+ * User-provided alias for the registry `name` (#829). When set,
40
+ * forwarded to `registerRepo` so the indexed repo is stored under
41
+ * this alias instead of the path-derived basename.
42
+ */
43
+ registryName?: string;
44
+ /**
45
+ * Bypass the `RegistryNameCollisionError` guard and allow two paths
46
+ * to register under the same `name` (#829). Controlled by the
47
+ * dedicated `--allow-duplicate-name` CLI flag, intentionally
48
+ * independent from `--force` — users who hit the collision guard
49
+ * should be able to accept the duplicate without paying the cost
50
+ * of a pipeline re-index.
51
+ */
52
+ allowDuplicateName?: boolean;
23
53
  }
24
54
  export interface AnalyzeResult {
25
55
  repoName: string;
@@ -36,6 +66,8 @@ export interface AnalyzeResult {
36
66
  /** The raw pipeline result — only populated when needed by callers (e.g. skill generation). */
37
67
  pipelineResult?: any;
38
68
  }
69
+ export { deriveEmbeddingMode } from './embedding-mode.js';
70
+ export type { EmbeddingMode } from './embedding-mode.js';
39
71
  export declare const PHASE_LABELS: Record<string, string>;
40
72
  /**
41
73
  * Run the full GitNexus analysis pipeline.
@@ -11,14 +11,18 @@
11
11
  import path from 'path';
12
12
  import fs from 'fs/promises';
13
13
  import { runPipelineFromRepo } from './ingestion/pipeline.js';
14
- import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, createFTSIndex, loadCachedEmbeddings, } from './lbug/lbug-adapter.js';
14
+ import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, loadCachedEmbeddings, } from './lbug/lbug-adapter.js';
15
15
  import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, cleanupOldKuzuFiles, } from '../storage/repo-manager.js';
16
- import { getCurrentCommit, hasGitDir } from '../storage/git.js';
16
+ import { getCurrentCommit, getRemoteUrl, hasGitDir, getInferredRepoName } from '../storage/git.js';
17
17
  import { generateAIContextFiles } from '../cli/ai-context.js';
18
18
  import { EMBEDDING_TABLE_NAME } from './lbug/schema.js';
19
19
  import { STALE_HASH_SENTINEL } from './lbug/schema.js';
20
20
  /** Threshold: auto-skip embeddings for repos with more nodes than this */
21
21
  const EMBEDDING_NODE_LIMIT = 50_000;
22
+ // Re-export the pure flag-derivation helper so external callers (and tests)
23
+ // keep importing from this module's stable surface.
24
+ export { deriveEmbeddingMode } from './embedding-mode.js';
25
+ import { deriveEmbeddingMode as _deriveEmbeddingMode } from './embedding-mode.js';
22
26
  export const PHASE_LABELS = {
23
27
  extracting: 'Scanning files',
24
28
  structure: 'Building structure',
@@ -65,7 +69,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
65
69
  // Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
66
70
  if (currentCommit !== '') {
67
71
  return {
68
- repoName: path.basename(repoPath),
72
+ repoName: options.registryName ?? getInferredRepoName(repoPath) ?? path.basename(repoPath),
69
73
  repoPath,
70
74
  stats: existingMeta.stats ?? {},
71
75
  alreadyUpToDate: true,
@@ -73,9 +77,39 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
73
77
  }
74
78
  }
75
79
  // ── Cache embeddings from existing index before rebuild ────────────
80
+ // Four modes:
81
+ // --embeddings -> load cache, restore, then generate any new ones
82
+ // --force (with existing
83
+ // embeddings) -> auto-imply --embeddings: load cache, restore,
84
+ // regenerate embeddings for new/changed nodes
85
+ // (a forced re-index of an embedded repo
86
+ // shouldn't quietly downgrade to "preserve only")
87
+ // (default) -> if existing index has embeddings, preserve them
88
+ // (load + restore, but do not generate); otherwise no-op
89
+ // --drop-embeddings -> skip cache load entirely; rebuild wipes embeddings
90
+ //
91
+ // The default-preserve branch is what makes a routine `analyze` (e.g. a
92
+ // post-commit hook) safe: a multi-minute embedding pass is no longer
93
+ // silently dropped just because the caller omitted `--embeddings`.
76
94
  let cachedEmbeddingNodeIds = new Set();
77
95
  let cachedEmbeddings = [];
78
- if (options.embeddings && existingMeta && !options.force) {
96
+ const existingEmbeddingCount = existingMeta?.stats?.embeddings ?? 0;
97
+ const { forceRegenerateEmbeddings, preserveExistingEmbeddings, shouldGenerateEmbeddings, shouldLoadCache, } = _deriveEmbeddingMode(options, existingEmbeddingCount);
98
+ if (options.dropEmbeddings && existingEmbeddingCount > 0) {
99
+ log(`Dropping ${existingEmbeddingCount} existing embeddings (--drop-embeddings). ` +
100
+ `Re-run with --embeddings to regenerate.`);
101
+ }
102
+ else if (forceRegenerateEmbeddings) {
103
+ log(`--force on a repo with ${existingEmbeddingCount} existing embeddings: ` +
104
+ `regenerating embeddings for new/changed nodes. ` +
105
+ `Pass --drop-embeddings to wipe them instead.`);
106
+ }
107
+ else if (preserveExistingEmbeddings) {
108
+ log(`Preserving ${existingEmbeddingCount} existing embeddings. ` +
109
+ `Pass --embeddings to also generate embeddings for new/changed nodes, ` +
110
+ `or --drop-embeddings to wipe them.`);
111
+ }
112
+ if (shouldLoadCache && existingMeta) {
79
113
  try {
80
114
  progress('embeddings', 0, 'Caching embeddings...');
81
115
  await initLbug(lbugPath);
@@ -84,7 +118,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
84
118
  cachedEmbeddings = cached.embeddings;
85
119
  await closeLbug();
86
120
  }
87
- catch {
121
+ catch (err) {
122
+ // Surface cache-load failures explicitly: silently swallowing here would
123
+ // re-introduce the original silent-data-loss symptom (embeddings end up
124
+ // at 0 in meta.json with no diagnostic) through a different door.
125
+ log(`Warning: could not load cached embeddings ` +
126
+ `(${err?.message ?? String(err)}). ` +
127
+ `Embeddings will not be preserved on this run.`);
128
+ cachedEmbeddingNodeIds = new Set();
129
+ cachedEmbeddings = [];
88
130
  try {
89
131
  await closeLbug();
90
132
  }
@@ -123,17 +165,12 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
123
165
  progress('lbug', pct, msg);
124
166
  });
125
167
  // ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
126
- progress('fts', 85, 'Creating search indexes...');
127
- try {
128
- await createFTSIndex('File', 'file_fts', ['name', 'content']);
129
- await createFTSIndex('Function', 'function_fts', ['name', 'content']);
130
- await createFTSIndex('Class', 'class_fts', ['name', 'content']);
131
- await createFTSIndex('Method', 'method_fts', ['name', 'content']);
132
- await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
133
- }
134
- catch {
135
- // Non-fatal — FTS is best-effort
136
- }
168
+ // FTS indexes are created lazily on first `query`/`context` call instead
169
+ // of eagerly here. On small repos / CI runners the LadybugDB
170
+ // CREATE_FTS_INDEX cost is ~440 ms × 5 (≈2 s) regardless of table size,
171
+ // which dominated `analyze` runtime and pushed Windows CI past its
172
+ // 30 s test budget. Lazy creation is implemented in
173
+ // `core/search/bm25-index.ts` via `ensureFTSIndex`.
137
174
  // ── Phase 3.5: Re-insert cached embeddings ────────────────────────
138
175
  if (cachedEmbeddings.length > 0) {
139
176
  const cachedDims = cachedEmbeddings[0].embedding.length;
@@ -162,7 +199,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
162
199
  // ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
163
200
  const stats = await getLbugStats();
164
201
  let embeddingSkipped = true;
165
- if (options.embeddings) {
202
+ if (shouldGenerateEmbeddings) {
166
203
  if (stats.nodes <= EMBEDDING_NODE_LIMIT) {
167
204
  embeddingSkipped = false;
168
205
  }
@@ -208,6 +245,13 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
208
245
  repoPath,
209
246
  lastCommit: currentCommit,
210
247
  indexedAt: new Date().toISOString(),
248
+ // Captured here (not at registration) so it travels with the
249
+ // on-disk meta.json — sibling-clone fingerprinting works for
250
+ // out-of-tree consumers (group-status, future tooling) without
251
+ // a second git shellout. `undefined` when the repo has no
252
+ // origin remote, which is fine: paths-only repos behave as
253
+ // before.
254
+ remoteUrl: hasGitDir(repoPath) ? getRemoteUrl(repoPath) : undefined,
211
255
  stats: {
212
256
  files: pipelineResult.totalFileCount,
213
257
  nodes: stats.nodes,
@@ -218,12 +262,23 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
218
262
  },
219
263
  };
220
264
  await saveMeta(storagePath, meta);
221
- await registerRepo(repoPath, meta);
265
+ // Forward the --name alias and the registry-collision bypass bit.
266
+ // `allowDuplicateName` is its own concern — independent from the
267
+ // pipeline `force` above. The CLI maps it from
268
+ // `--allow-duplicate-name` only; `--force` and `--skills` both
269
+ // trigger pipeline re-run but never bypass the registry guard.
270
+ // The returned name is the one actually written to the registry
271
+ // (after applying the precedence chain in registerRepo) — reuse it
272
+ // so AGENTS.md / skill files reference the same name MCP clients
273
+ // will look up (#979).
274
+ const projectName = await registerRepo(repoPath, meta, {
275
+ name: options.registryName,
276
+ allowDuplicateName: options.allowDuplicateName,
277
+ });
222
278
  // Only attempt to update .gitignore when a .git directory is present.
223
279
  if (hasGitDir(repoPath)) {
224
280
  await addToGitignore(repoPath);
225
281
  }
226
- const projectName = path.basename(repoPath);
227
282
  // ── Generate AI context files (best-effort) ───────────────────────
228
283
  let aggregatedClusterCount = 0;
229
284
  if (pipelineResult.communityResult?.communities) {
@@ -3,12 +3,30 @@
3
3
  *
4
4
  * Uses LadybugDB's built-in full-text search indexes for keyword-based search.
5
5
  * Always reads from the database (no cached state to drift).
6
+ *
7
+ * FTS indexes are created lazily on first query (via `ensureFTSIndex`) — see
8
+ * `lbug-adapter.ts` for the rationale. This keeps `analyze` fast (the
9
+ * ~440 ms × 5 LadybugDB CREATE_FTS_INDEX cost dominates pipeline time on
10
+ * small repos / CI runners) at the cost of paying that overhead on the
11
+ * first `query`/`context` call in a session.
6
12
  */
7
13
  export interface BM25SearchResult {
8
14
  filePath: string;
9
15
  score: number;
10
16
  rank: number;
17
+ nodeIds?: string[];
11
18
  }
19
+ /**
20
+ * Drop all ensured-FTS cache entries for a given repoId.
21
+ *
22
+ * Called from the pool-close listener so that a pool teardown / recreation
23
+ * forces the next `searchFTSFromLbug` call to re-issue `CREATE_FTS_INDEX`
24
+ * against the fresh connection rather than trust stale ensure-state from a
25
+ * previous pool lifetime.
26
+ *
27
+ * Exported for tests; the listener wiring is internal.
28
+ */
29
+ export declare function invalidateEnsuredFTSForRepo(repoId: string): void;
12
30
  /**
13
31
  * Search using LadybugDB's built-in FTS (always fresh, reads from disk)
14
32
  *
@@ -3,8 +3,96 @@
3
3
  *
4
4
  * Uses LadybugDB's built-in full-text search indexes for keyword-based search.
5
5
  * Always reads from the database (no cached state to drift).
6
+ *
7
+ * FTS indexes are created lazily on first query (via `ensureFTSIndex`) — see
8
+ * `lbug-adapter.ts` for the rationale. This keeps `analyze` fast (the
9
+ * ~440 ms × 5 LadybugDB CREATE_FTS_INDEX cost dominates pipeline time on
10
+ * small repos / CI runners) at the cost of paying that overhead on the
11
+ * first `query`/`context` call in a session.
12
+ */
13
+ import { queryFTS, ensureFTSIndex } from '../lbug/lbug-adapter.js';
14
+ /**
15
+ * FTS schema served by `searchFTSFromLbug`. Centralised so that both the
16
+ * CLI/pipeline path and the MCP pool path use identical (table, index,
17
+ * properties) tuples and the lazy-create logic stays in one place.
18
+ */
19
+ const FTS_INDEXES = [
20
+ { table: 'File', indexName: 'file_fts', properties: ['name', 'content'] },
21
+ { table: 'Function', indexName: 'function_fts', properties: ['name', 'content'] },
22
+ { table: 'Class', indexName: 'class_fts', properties: ['name', 'content'] },
23
+ { table: 'Method', indexName: 'method_fts', properties: ['name', 'content'] },
24
+ { table: 'Interface', indexName: 'interface_fts', properties: ['name', 'content'] },
25
+ ];
26
+ /**
27
+ * Per-process cache for the MCP pool path: tracks which `(repoId, table)`
28
+ * pairs have been ensured. The CLI/pipeline path gets its own cache inside
29
+ * `lbug-adapter.ts` keyed by table/index, scoped to the singleton connection.
30
+ *
31
+ * IMPORTANT: an entry is added ONLY when the index was confirmed to exist
32
+ * (CREATE_FTS_INDEX succeeded, or failed with `'already exists'`). Other
33
+ * failures (transient lock errors, missing extension, etc.) leave the key
34
+ * unset so the next query retries instead of silently caching the failure.
35
+ *
36
+ * Entries for a given repoId are invalidated when its pool is closed —
37
+ * see the `addPoolCloseListener` registration in `searchFTSFromLbug`.
38
+ */
39
+ const ensuredPoolFTS = new Set();
40
+ /**
41
+ * Drop all ensured-FTS cache entries for a given repoId.
42
+ *
43
+ * Called from the pool-close listener so that a pool teardown / recreation
44
+ * forces the next `searchFTSFromLbug` call to re-issue `CREATE_FTS_INDEX`
45
+ * against the fresh connection rather than trust stale ensure-state from a
46
+ * previous pool lifetime.
47
+ *
48
+ * Exported for tests; the listener wiring is internal.
49
+ */
50
+ export function invalidateEnsuredFTSForRepo(repoId) {
51
+ const prefix = `${repoId}:`;
52
+ for (const key of ensuredPoolFTS) {
53
+ if (key.startsWith(prefix))
54
+ ensuredPoolFTS.delete(key);
55
+ }
56
+ }
57
+ /**
58
+ * Tracks whether we've already wired the pool-close listener for this
59
+ * process. The pool adapter is dynamically imported, so registration
60
+ * happens lazily on the first MCP-pool-backed FTS query.
6
61
  */
7
- import { queryFTS } from '../lbug/lbug-adapter.js';
62
+ let poolCloseListenerRegistered = false;
63
+ function registerPoolCloseListenerOnce(addPoolCloseListener) {
64
+ if (poolCloseListenerRegistered)
65
+ return;
66
+ poolCloseListenerRegistered = true;
67
+ addPoolCloseListener((repoId) => invalidateEnsuredFTSForRepo(repoId));
68
+ }
69
+ async function ensureFTSIndexViaExecutor(executor, repoId, table, indexName, properties) {
70
+ const key = `${repoId}:${table}:${indexName}`;
71
+ if (ensuredPoolFTS.has(key))
72
+ return;
73
+ const propList = properties.map((p) => `'${p}'`).join(', ');
74
+ try {
75
+ await executor(`CALL CREATE_FTS_INDEX('${table}', '${indexName}', [${propList}], stemmer := 'porter')`);
76
+ // Index was created successfully — safe to cache.
77
+ ensuredPoolFTS.add(key);
78
+ }
79
+ catch (e) {
80
+ // 'already exists' is the happy path (index persists on disk between
81
+ // process invocations) — cache it. Anything else is treated as a
82
+ // transient failure: surface a one-time warning and leave the key
83
+ // unset so the NEXT query retries rather than silently using a
84
+ // cached failure (which previously disabled BM25 for the whole
85
+ // process for that repo).
86
+ const msg = String(e?.message ?? '');
87
+ if (msg.includes('already exists')) {
88
+ ensuredPoolFTS.add(key);
89
+ }
90
+ else {
91
+ console.warn(`[gitnexus] FTS index ensure failed for repo "${repoId}" table "${table}" ` +
92
+ `(index "${indexName}"): ${msg || e}. Will retry on next query.`);
93
+ }
94
+ }
95
+ }
8
96
  /**
9
97
  * Execute a single FTS query via a custom executor (for MCP connection pool).
10
98
  * Returns the same shape as core queryFTS (from LadybugDB adapter).
@@ -26,6 +114,7 @@ async function queryFTSViaExecutor(executor, tableName, indexName, query, limit)
26
114
  return {
27
115
  filePath: node.filePath || '',
28
116
  score: typeof score === 'number' ? score : parseFloat(score) || 0,
117
+ nodeId: node.nodeId || node.id || '',
29
118
  };
30
119
  });
31
120
  }
@@ -50,8 +139,19 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
50
139
  // Use MCP connection pool via dynamic import
51
140
  // IMPORTANT: FTS queries run sequentially to avoid connection contention.
52
141
  // The MCP pool supports multiple connections, but FTS is best run serially.
53
- const { executeQuery } = await import('../lbug/pool-adapter.js');
142
+ const poolMod = await import('../lbug/pool-adapter.js');
143
+ const { executeQuery, addPoolCloseListener } = poolMod;
144
+ // Register the pool-close listener lazily on first use so a teardown of
145
+ // the pool entry (LRU eviction, idle timeout, explicit close) drops the
146
+ // matching `ensuredPoolFTS` entries. Without this, stale ensure-state
147
+ // can outlive the pool that produced it.
148
+ registerPoolCloseListenerOnce(addPoolCloseListener);
54
149
  const executor = (cypher) => executeQuery(repoId, cypher);
150
+ // Lazy-create FTS indexes on first query for this repo (analyze no longer
151
+ // creates them up-front, so we ensure them here). Cached per-process.
152
+ for (const { table, indexName, properties } of FTS_INDEXES) {
153
+ await ensureFTSIndexViaExecutor(executor, repoId, table, indexName, properties);
154
+ }
55
155
  fileResults = await queryFTSViaExecutor(executor, 'File', 'file_fts', query, limit);
56
156
  functionResults = await queryFTSViaExecutor(executor, 'Function', 'function_fts', query, limit);
57
157
  classResults = await queryFTSViaExecutor(executor, 'Class', 'class_fts', query, limit);
@@ -59,24 +159,24 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
59
159
  interfaceResults = await queryFTSViaExecutor(executor, 'Interface', 'interface_fts', query, limit);
60
160
  }
61
161
  else {
62
- // Use core lbug adapter (CLI / pipeline context) — also sequential for safety
162
+ // Use core lbug adapter (CLI / pipeline context) — also sequential for safety.
163
+ // Lazy-create FTS indexes on first query (analyze no longer does it).
164
+ for (const { table, indexName, properties } of FTS_INDEXES) {
165
+ await ensureFTSIndex(table, indexName, [...properties]).catch(() => { });
166
+ }
63
167
  fileResults = await queryFTS('File', 'file_fts', query, limit, false).catch(() => []);
64
168
  functionResults = await queryFTS('Function', 'function_fts', query, limit, false).catch(() => []);
65
169
  classResults = await queryFTS('Class', 'class_fts', query, limit, false).catch(() => []);
66
170
  methodResults = await queryFTS('Method', 'method_fts', query, limit, false).catch(() => []);
67
171
  interfaceResults = await queryFTS('Interface', 'interface_fts', query, limit, false).catch(() => []);
68
172
  }
69
- // Merge results by filePath, summing scores for same file
70
- const merged = new Map();
173
+ // Collect all node scores per filePath to track which nodes actually matched
174
+ const fileNodeScores = new Map();
71
175
  const addResults = (results) => {
72
176
  for (const r of results) {
73
- const existing = merged.get(r.filePath);
74
- if (existing) {
75
- existing.score += r.score;
76
- }
77
- else {
78
- merged.set(r.filePath, { filePath: r.filePath, score: r.score });
79
- }
177
+ if (!fileNodeScores.has(r.filePath))
178
+ fileNodeScores.set(r.filePath, []);
179
+ fileNodeScores.get(r.filePath).push({ score: r.score, nodeId: r.nodeId });
80
180
  }
81
181
  };
82
182
  addResults(fileResults);
@@ -84,6 +184,18 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
84
184
  addResults(classResults);
85
185
  addResults(methodResults);
86
186
  addResults(interfaceResults);
187
+ // Sum the top-3 highest-scoring nodes per file and collect their nodeIds.
188
+ // Summing all nodes naively inflates scores for files with many mediocre
189
+ // matches (e.g. test files) over files with a single highly-relevant symbol.
190
+ const merged = new Map();
191
+ for (const [filePath, entries] of fileNodeScores) {
192
+ const top3 = [...entries].sort((a, b) => b.score - a.score).slice(0, 3);
193
+ merged.set(filePath, {
194
+ filePath,
195
+ score: top3.reduce((acc, e) => acc + e.score, 0),
196
+ nodeIds: top3.map((e) => e.nodeId).filter((id) => id),
197
+ });
198
+ }
87
199
  // Sort by score descending and add rank
88
200
  const sorted = Array.from(merged.values())
89
201
  .sort((a, b) => b.score - a.score)
@@ -92,5 +204,6 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
92
204
  filePath: r.filePath,
93
205
  score: r.score,
94
206
  rank: index + 1,
207
+ nodeIds: r.nodeIds,
95
208
  }));
96
209
  };
@@ -5,7 +5,12 @@ import Python from 'tree-sitter-python';
5
5
  import Java from 'tree-sitter-java';
6
6
  import C from 'tree-sitter-c';
7
7
  import CPP from 'tree-sitter-cpp';
8
- import CSharp from 'tree-sitter-c-sharp';
8
+ // Explicit subpath import: tree-sitter-c-sharp declares `type: "module"` with
9
+ // `main: "bindings/node"` (no extension) and no `exports` field, which triggers
10
+ // Node 22's DEP0151 deprecation warning on the bare-package import. Importing
11
+ // the built entrypoint directly bypasses the deprecated ESM main-field
12
+ // resolution. (#1013)
13
+ import CSharp from 'tree-sitter-c-sharp/bindings/node/index.js';
9
14
  import Go from 'tree-sitter-go';
10
15
  import Rust from 'tree-sitter-rust';
11
16
  import PHP from 'tree-sitter-php';