gitnexus 1.6.4-rc.2 → 1.6.4-rc.21

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 (243) hide show
  1. package/README.md +35 -0
  2. package/dist/_shared/index.d.ts +1 -1
  3. package/dist/_shared/index.d.ts.map +1 -1
  4. package/dist/_shared/index.js +1 -1
  5. package/dist/_shared/index.js.map +1 -1
  6. package/dist/_shared/scope-resolution/finalize-algorithm.d.ts +22 -14
  7. package/dist/_shared/scope-resolution/finalize-algorithm.d.ts.map +1 -1
  8. package/dist/_shared/scope-resolution/finalize-algorithm.js +298 -37
  9. package/dist/_shared/scope-resolution/finalize-algorithm.js.map +1 -1
  10. package/dist/_shared/scope-resolution/scope-tree.d.ts +23 -1
  11. package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -1
  12. package/dist/_shared/scope-resolution/scope-tree.js +36 -2
  13. package/dist/_shared/scope-resolution/scope-tree.js.map +1 -1
  14. package/dist/_shared/scope-resolution/types.d.ts +47 -3
  15. package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
  16. package/dist/_shared/scope-resolution/types.js +10 -2
  17. package/dist/_shared/scope-resolution/types.js.map +1 -1
  18. package/dist/cli/analyze.d.ts +6 -0
  19. package/dist/cli/analyze.js +35 -0
  20. package/dist/cli/doctor.d.ts +1 -0
  21. package/dist/cli/doctor.js +31 -0
  22. package/dist/cli/index.js +13 -0
  23. package/dist/cli/setup.js +2 -2
  24. package/dist/core/embeddings/config.d.ts +2 -0
  25. package/dist/core/embeddings/config.js +36 -0
  26. package/dist/core/embeddings/embedder.js +11 -6
  27. package/dist/core/embeddings/embedding-pipeline.d.ts +7 -1
  28. package/dist/core/embeddings/embedding-pipeline.js +93 -29
  29. package/dist/core/embeddings/exact-search.d.ts +15 -0
  30. package/dist/core/embeddings/exact-search.js +27 -0
  31. package/dist/core/embeddings/types.d.ts +4 -0
  32. package/dist/core/embeddings/types.js +2 -0
  33. package/dist/core/group/config-parser.js +2 -0
  34. package/dist/core/group/matching.d.ts +3 -3
  35. package/dist/core/group/matching.js +46 -6
  36. package/dist/core/group/storage.js +2 -0
  37. package/dist/core/group/sync.js +1 -1
  38. package/dist/core/group/types.d.ts +18 -0
  39. package/dist/core/ingestion/call-processor.d.ts +3 -3
  40. package/dist/core/ingestion/call-processor.js +58 -65
  41. package/dist/core/ingestion/constants.d.ts +4 -3
  42. package/dist/core/ingestion/constants.js +8 -3
  43. package/dist/core/ingestion/finalize-orchestrator.js +6 -3
  44. package/dist/core/ingestion/heritage-processor.js +2 -2
  45. package/dist/core/ingestion/import-processor.js +1 -1
  46. package/dist/core/ingestion/language-provider.d.ts +8 -0
  47. package/dist/core/ingestion/languages/csharp/captures.js +4 -1
  48. package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +14 -13
  49. package/dist/core/ingestion/languages/csharp/namespace-siblings.js +62 -50
  50. package/dist/core/ingestion/languages/python/captures.js +9 -1
  51. package/dist/core/ingestion/languages/python/index.d.ts +1 -1
  52. package/dist/core/ingestion/languages/python/index.js +1 -1
  53. package/dist/core/ingestion/languages/python/simple-hooks.d.ts +3 -1
  54. package/dist/core/ingestion/languages/python/simple-hooks.js +8 -0
  55. package/dist/core/ingestion/languages/python.js +28 -1
  56. package/dist/core/ingestion/languages/swift.js +14 -0
  57. package/dist/core/ingestion/languages/typescript/arity-metadata.d.ts +59 -0
  58. package/dist/core/ingestion/languages/typescript/arity-metadata.js +103 -0
  59. package/dist/core/ingestion/languages/typescript/arity.d.ts +37 -0
  60. package/dist/core/ingestion/languages/typescript/arity.js +54 -0
  61. package/dist/core/ingestion/languages/typescript/cache-stats.d.ts +17 -0
  62. package/dist/core/ingestion/languages/typescript/cache-stats.js +28 -0
  63. package/dist/core/ingestion/languages/typescript/captures.d.ts +28 -0
  64. package/dist/core/ingestion/languages/typescript/captures.js +451 -0
  65. package/dist/core/ingestion/languages/typescript/import-decomposer.d.ts +49 -0
  66. package/dist/core/ingestion/languages/typescript/import-decomposer.js +371 -0
  67. package/dist/core/ingestion/languages/typescript/import-target.d.ts +50 -0
  68. package/dist/core/ingestion/languages/typescript/import-target.js +61 -0
  69. package/dist/core/ingestion/languages/typescript/index.d.ts +94 -0
  70. package/dist/core/ingestion/languages/typescript/index.js +94 -0
  71. package/dist/core/ingestion/languages/typescript/interpret.d.ts +35 -0
  72. package/dist/core/ingestion/languages/typescript/interpret.js +317 -0
  73. package/dist/core/ingestion/languages/typescript/merge-bindings.d.ts +62 -0
  74. package/dist/core/ingestion/languages/typescript/merge-bindings.js +158 -0
  75. package/dist/core/ingestion/languages/typescript/query.d.ts +77 -0
  76. package/dist/core/ingestion/languages/typescript/query.js +778 -0
  77. package/dist/core/ingestion/languages/typescript/receiver-binding.d.ts +59 -0
  78. package/dist/core/ingestion/languages/typescript/receiver-binding.js +171 -0
  79. package/dist/core/ingestion/languages/typescript/scope-resolver.d.ts +16 -0
  80. package/dist/core/ingestion/languages/typescript/scope-resolver.js +113 -0
  81. package/dist/core/ingestion/languages/typescript/simple-hooks.d.ts +71 -0
  82. package/dist/core/ingestion/languages/typescript/simple-hooks.js +131 -0
  83. package/dist/core/ingestion/languages/typescript.js +19 -0
  84. package/dist/core/ingestion/method-extractors/configs/swift.js +3 -4
  85. package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +14 -1
  86. package/dist/core/ingestion/parsing-processor.js +20 -9
  87. package/dist/core/ingestion/pipeline-phases/processes.js +9 -4
  88. package/dist/core/ingestion/pipeline-phases/tools.d.ts +1 -0
  89. package/dist/core/ingestion/pipeline-phases/tools.js +10 -4
  90. package/dist/core/ingestion/registry-primary-flag.d.ts +3 -1
  91. package/dist/core/ingestion/registry-primary-flag.js +4 -1
  92. package/dist/core/ingestion/scope-extractor-bridge.d.ts +5 -2
  93. package/dist/core/ingestion/scope-extractor-bridge.js +7 -2
  94. package/dist/core/ingestion/scope-extractor.js +19 -18
  95. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +73 -11
  96. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +48 -10
  97. package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +283 -14
  98. package/dist/core/ingestion/scope-resolution/passes/imported-return-types.d.ts +23 -2
  99. package/dist/core/ingestion/scope-resolution/passes/imported-return-types.js +109 -37
  100. package/dist/core/ingestion/scope-resolution/passes/mro.js +3 -1
  101. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +13 -5
  102. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +11 -2
  103. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
  104. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +8 -0
  105. package/dist/core/ingestion/scope-resolution/pipeline/run.js +21 -5
  106. package/dist/core/ingestion/scope-resolution/pipeline/validate-bindings-immutability.d.ts +39 -0
  107. package/dist/core/ingestion/scope-resolution/pipeline/validate-bindings-immutability.js +65 -0
  108. package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +54 -11
  109. package/dist/core/ingestion/scope-resolution/scope/walkers.js +105 -30
  110. package/dist/core/ingestion/type-extractors/swift.js +7 -4
  111. package/dist/core/ingestion/utils/ast-helpers.d.ts +2 -0
  112. package/dist/core/ingestion/utils/ast-helpers.js +12 -0
  113. package/dist/core/ingestion/utils/env.d.ts +10 -0
  114. package/dist/core/ingestion/utils/env.js +14 -0
  115. package/dist/core/ingestion/workers/parse-worker.d.ts +1 -0
  116. package/dist/core/ingestion/workers/parse-worker.js +15 -9
  117. package/dist/core/ingestion/workers/worker-pool.d.ts +11 -4
  118. package/dist/core/ingestion/workers/worker-pool.js +244 -48
  119. package/dist/core/lbug/extension-loader.d.ts +86 -0
  120. package/dist/core/lbug/extension-loader.js +184 -0
  121. package/dist/core/lbug/lbug-adapter.d.ts +18 -17
  122. package/dist/core/lbug/lbug-adapter.js +45 -73
  123. package/dist/core/lbug/pool-adapter.js +10 -28
  124. package/dist/core/platform/capabilities.d.ts +24 -0
  125. package/dist/core/platform/capabilities.js +54 -0
  126. package/dist/core/run-analyze.js +36 -9
  127. package/dist/core/search/bm25-index.d.ts +0 -17
  128. package/dist/core/search/bm25-index.js +10 -118
  129. package/dist/core/search/fts-indexes.d.ts +1 -0
  130. package/dist/core/search/fts-indexes.js +7 -0
  131. package/dist/core/search/fts-schema.d.ts +6 -0
  132. package/dist/core/search/fts-schema.js +7 -0
  133. package/dist/mcp/core/embedder.js +11 -4
  134. package/dist/mcp/local/local-backend.js +50 -15
  135. package/dist/server/api.d.ts +5 -0
  136. package/dist/server/api.js +113 -0
  137. package/hooks/claude/gitnexus-hook.cjs +11 -1
  138. package/package.json +6 -5
  139. package/scripts/build-tree-sitter-dart.cjs +42 -0
  140. package/scripts/build-tree-sitter-proto.cjs +1 -1
  141. package/scripts/build.js +22 -2
  142. package/scripts/install-duckdb-extension.mjs +37 -0
  143. package/vendor/tree-sitter-dart/README.md +18 -0
  144. package/vendor/tree-sitter-dart/binding.gyp +31 -0
  145. package/vendor/tree-sitter-dart/bindings/node/binding.cc +20 -0
  146. package/vendor/tree-sitter-dart/bindings/node/index.d.ts +28 -0
  147. package/vendor/tree-sitter-dart/bindings/node/index.js +7 -0
  148. package/vendor/tree-sitter-dart/grammar.js +2895 -0
  149. package/vendor/tree-sitter-dart/package.json +18 -0
  150. package/vendor/tree-sitter-dart/queries/highlights.scm +246 -0
  151. package/vendor/tree-sitter-dart/queries/tags.scm +92 -0
  152. package/vendor/tree-sitter-dart/queries/test.scm +1 -0
  153. package/vendor/tree-sitter-dart/src/grammar.json +12459 -0
  154. package/vendor/tree-sitter-dart/src/node-types.json +15055 -0
  155. package/vendor/tree-sitter-dart/src/parser.c +196127 -0
  156. package/vendor/tree-sitter-dart/src/scanner.c +130 -0
  157. package/vendor/tree-sitter-dart/src/tree_sitter/alloc.h +54 -0
  158. package/vendor/tree-sitter-dart/src/tree_sitter/array.h +290 -0
  159. package/vendor/tree-sitter-dart/src/tree_sitter/parser.h +265 -0
  160. package/vendor/tree-sitter-swift/LICENSE +21 -0
  161. package/vendor/tree-sitter-swift/README.md +139 -0
  162. package/vendor/tree-sitter-swift/bindings/node/index.d.ts +28 -0
  163. package/vendor/tree-sitter-swift/bindings/node/index.js +7 -0
  164. package/vendor/tree-sitter-swift/package.json +28 -0
  165. package/vendor/tree-sitter-swift/prebuilds/darwin-arm64/tree-sitter-swift.node +0 -0
  166. package/vendor/tree-sitter-swift/prebuilds/darwin-x64/tree-sitter-swift.node +0 -0
  167. package/vendor/tree-sitter-swift/prebuilds/linux-arm64/tree-sitter-swift.node +0 -0
  168. package/vendor/tree-sitter-swift/prebuilds/linux-x64/tree-sitter-swift.node +0 -0
  169. package/vendor/tree-sitter-swift/prebuilds/win32-arm64/tree-sitter-swift.node +0 -0
  170. package/vendor/tree-sitter-swift/prebuilds/win32-x64/tree-sitter-swift.node +0 -0
  171. package/vendor/tree-sitter-swift/src/node-types.json +30694 -0
  172. package/web/assets/agent-DaprsFSX.js +597 -0
  173. package/web/assets/architecture-YZFGNWBL-S5CXDPWN-DEdGaPg2.js +1 -0
  174. package/web/assets/architectureDiagram-EMZXCZ2Q-Domyk_gO.js +36 -0
  175. package/web/assets/blockDiagram-IGV67L2C-B_2kD7tM.js +132 -0
  176. package/web/assets/c4Diagram-DFAF54RM-BhJJW8Gg.js +10 -0
  177. package/web/assets/chunk-3GS5O3IE-jlWIjPsl.js +231 -0
  178. package/web/assets/chunk-3YCYZ6SJ-Blq_IzZs.js +1 -0
  179. package/web/assets/chunk-6NTNNK5N-DyPc58pp.js +1 -0
  180. package/web/assets/chunk-7RZVMHOQ-BdIU-RGO.js +321 -0
  181. package/web/assets/chunk-A34GCYZU-BI2i_LdU.js +1 -0
  182. package/web/assets/chunk-AEOMTBSW-D7qjBMHW.js +1 -0
  183. package/web/assets/chunk-CilyBKbf.js +1 -0
  184. package/web/assets/chunk-DJ7UZH7F-i11ywiBl.js +1 -0
  185. package/web/assets/chunk-DKKBVRCY-1SffGI1N.js +4 -0
  186. package/web/assets/chunk-DU5LTGQ6-DaPeiwD5.js +1 -0
  187. package/web/assets/chunk-FXACKDTF-uhhi2PC2.js +159 -0
  188. package/web/assets/chunk-H3VCZNTA-IchcISDt.js +1 -0
  189. package/web/assets/chunk-HN6EAY2L-D7ZFMNrB.js +1 -0
  190. package/web/assets/chunk-KSICW3F5-C2tZmXwv.js +15 -0
  191. package/web/assets/chunk-O5ABG6QK-Bt-Km84H.js +1 -0
  192. package/web/assets/chunk-PK6DOVAG-ChlWY0BQ.js +206 -0
  193. package/web/assets/chunk-RNJOYNJ4-B724K7cW.js +1 -0
  194. package/web/assets/chunk-RWUO3TPN-DYn1XriD.js +1 -0
  195. package/web/assets/chunk-TBF5ZNIQ-DKtDz6ae.js +1 -0
  196. package/web/assets/chunk-TU3PZOEN-DE5Qhc0N.js +1 -0
  197. package/web/assets/chunk-TYMNRAUI-g1h33cq-.js +1 -0
  198. package/web/assets/chunk-VELTKBKT-C9dVN39o.js +1 -0
  199. package/web/assets/chunk-W7ZLLLMY-Du-Hb9yb.js +1 -0
  200. package/web/assets/chunk-WSB5WSVC-B123clsZ.js +1 -0
  201. package/web/assets/chunk-XGPFEOL4-BR7Eue38.js +1 -0
  202. package/web/assets/classDiagram-PPOCWD7C-BglfKSs_.js +1 -0
  203. package/web/assets/classDiagram-v2-23LJLIIU-BSzTM28O.js +1 -0
  204. package/web/assets/context-builder-CqQNhRj1.js +15 -0
  205. package/web/assets/cose-bilkent-PNC4W37J-DCfErU-A.js +1 -0
  206. package/web/assets/dagre-E77IOHMT-tDRRhDoN.js +4 -0
  207. package/web/assets/diagram-H7BISOXX-CUVHlmAh.js +43 -0
  208. package/web/assets/diagram-JC5VWROH-BoyOxulB.js +24 -0
  209. package/web/assets/diagram-LXUTUG65-osr9hb7N.js +10 -0
  210. package/web/assets/diagram-WEHSV5V5-d8nUqS39.js +24 -0
  211. package/web/assets/erDiagram-GCSMX5X6-b-IwOhPS.js +85 -0
  212. package/web/assets/flowDiagram-OTCZ4VVT-Ott2Q0AP.js +162 -0
  213. package/web/assets/ganttDiagram-MUNLMDZQ-BYtgN_5s.js +292 -0
  214. package/web/assets/gitGraph-7Q5UKJZL-54BCDZD5-CFyBIGZq.js +1 -0
  215. package/web/assets/gitGraphDiagram-3HKGZ4G3-CsVD2gn4.js +106 -0
  216. package/web/assets/index-BleGLU8S.css +2 -0
  217. package/web/assets/index-C_xK08EW.js +885 -0
  218. package/web/assets/info-OMHHGYJF-BF2H5H6G-yjAxKEzh.js +1 -0
  219. package/web/assets/infoDiagram-MN7RKWGX-DXK0Unn5.js +2 -0
  220. package/web/assets/ishikawaDiagram-YMYX4NHK-CXsnC2FA.js +70 -0
  221. package/web/assets/journeyDiagram-SO5T7YLQ-BzZ07B-X.js +139 -0
  222. package/web/assets/kanban-definition-LJHFXRCJ-C6_EpAd9.js +89 -0
  223. package/web/assets/katex-GD7MH7QM-CJiOjBBJ.js +261 -0
  224. package/web/assets/mindmap-definition-2EUWGEK5-CCYGWZ1m.js +96 -0
  225. package/web/assets/packet-4T2RLAQJ-EV4IVRXR-B8k4E3IT.js +1 -0
  226. package/web/assets/pie-ZZUOXDRM-N23DN5KN-DdvfY118.js +1 -0
  227. package/web/assets/pieDiagram-3IATQBI2-RyvRlQb4.js +30 -0
  228. package/web/assets/quadrantDiagram-E256RVCF-Bfb6sxCx.js +7 -0
  229. package/web/assets/radar-PYXPWWZC-P6TP7ZYP-1EEDC_yU.js +1 -0
  230. package/web/assets/requirementDiagram-M5DCFWZL-DjvHDyvN.js +84 -0
  231. package/web/assets/sankeyDiagram-L3NBLAOT-CBCbbl8s.js +10 -0
  232. package/web/assets/sequenceDiagram-ZOUHS735-BscU8TUR.js +157 -0
  233. package/web/assets/stateDiagram-MLPALWAM-CJusEK2D.js +1 -0
  234. package/web/assets/stateDiagram-v2-B5LQ5ZB2-DImJ3PXD.js +1 -0
  235. package/web/assets/timeline-definition-5SPVSISX-DigPA1X8.js +120 -0
  236. package/web/assets/treeView-SZITEDCU-5DXDK3XO-CzPDt3aG.js +1 -0
  237. package/web/assets/treemap-W4RFUUIX-WYLRDWKO-B9Iqiorr.js +1 -0
  238. package/web/assets/vennDiagram-IE5QUKF5-C91UkZIf.js +34 -0
  239. package/web/assets/wardley-RL74JXVD-BCRCBASE-x42Qw7hp.js +1 -0
  240. package/web/assets/wardleyDiagram-XU3VSMPF-DloBhI0U.js +20 -0
  241. package/web/assets/xychartDiagram-ZHJ5623Y-BGWJvgwI.js +7 -0
  242. package/web/index.html +21 -0
  243. package/scripts/patch-tree-sitter-swift.cjs +0 -78
@@ -0,0 +1,184 @@
1
+ import { spawn } from 'child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ const DEFAULT_EXTENSION_INSTALL_TIMEOUT_MS = 15_000;
4
+ const EXTENSION_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
5
+ const alreadyAvailable = (message) => message.includes('already loaded') ||
6
+ message.includes('already installed') ||
7
+ message.includes('already exists');
8
+ const resolvePolicyFromEnv = () => {
9
+ const raw = process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
10
+ if (raw === 'load-only' || raw === 'never' || raw === 'auto')
11
+ return raw;
12
+ return 'auto';
13
+ };
14
+ export const getExtensionInstallTimeoutMs = () => {
15
+ const raw = process.env.GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS;
16
+ const parsed = raw ? Number(raw) : NaN;
17
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_EXTENSION_INSTALL_TIMEOUT_MS;
18
+ };
19
+ export const getExtensionInstallChildProcessArgs = (extensionName) => {
20
+ const childScript = new URL('../../../scripts/install-duckdb-extension.mjs', import.meta.url);
21
+ return [fileURLToPath(childScript), extensionName];
22
+ };
23
+ /**
24
+ * Run `INSTALL <extension>` in a short-lived child Node process so the parent
25
+ * event loop is never blocked by DuckDB's synchronous network call.
26
+ *
27
+ * The child opens its own scratch LadybugDB, executes the install, and exits.
28
+ * If the child exceeds `timeoutMs` the parent kills it with SIGKILL and
29
+ * resolves with `timedOut: true`.
30
+ */
31
+ export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutMs = getExtensionInstallTimeoutMs()) => {
32
+ if (!EXTENSION_NAME_PATTERN.test(extensionName)) {
33
+ throw new Error(`Invalid DuckDB extension name: ${extensionName}`);
34
+ }
35
+ return await new Promise((resolve) => {
36
+ const child = spawn(process.execPath, getExtensionInstallChildProcessArgs(extensionName), {
37
+ env: {
38
+ ...process.env,
39
+ GITNEXUS_LBUG_EXTENSION_NAME: extensionName,
40
+ },
41
+ stdio: ['ignore', 'ignore', 'pipe'],
42
+ windowsHide: true,
43
+ });
44
+ let stderr = '';
45
+ child.stderr?.setEncoding('utf8');
46
+ child.stderr?.on('data', (chunk) => {
47
+ stderr = (stderr + chunk).slice(-4000);
48
+ });
49
+ let settled = false;
50
+ const timer = setTimeout(() => {
51
+ if (settled)
52
+ return;
53
+ settled = true;
54
+ child.kill('SIGKILL');
55
+ resolve({
56
+ success: false,
57
+ timedOut: true,
58
+ message: `INSTALL ${extensionName} timed out after ${timeoutMs}ms`,
59
+ });
60
+ }, timeoutMs);
61
+ child.on('error', (err) => {
62
+ if (settled)
63
+ return;
64
+ settled = true;
65
+ clearTimeout(timer);
66
+ resolve({ success: false, timedOut: false, message: err.message });
67
+ });
68
+ child.on('exit', (code, signal) => {
69
+ if (settled)
70
+ return;
71
+ settled = true;
72
+ clearTimeout(timer);
73
+ resolve({
74
+ success: code === 0,
75
+ timedOut: false,
76
+ message: code === 0
77
+ ? `INSTALL ${extensionName} completed`
78
+ : `INSTALL ${extensionName} failed with ${signal ?? `exit code ${code}`}${stderr ? `: ${stderr.trim()}` : ''}`,
79
+ });
80
+ });
81
+ });
82
+ };
83
+ /**
84
+ * Centralized lifecycle manager for optional LadybugDB extensions.
85
+ *
86
+ * Always tries `LOAD EXTENSION <name>` first — it is per-connection,
87
+ * idempotent, and never touches the network. If `LOAD` fails and the active
88
+ * policy permits, the manager runs a single bounded out-of-process `INSTALL`
89
+ * attempt per process and retries `LOAD`. Capability outcomes are cached so
90
+ * unavailable extensions degrade search features without ever blocking
91
+ * subsequent analyze or query calls.
92
+ *
93
+ * Policy precedence (most specific wins):
94
+ * per-call `opts.policy` → constructor `options.policy` → env → `auto`
95
+ */
96
+ export class ExtensionManager {
97
+ options;
98
+ capabilities = new Map();
99
+ installAttempted = new Map();
100
+ warnedKeys = new Set();
101
+ constructor(options = {}) {
102
+ this.options = options;
103
+ }
104
+ /** Reset cached capability and install state. Test-only. */
105
+ reset() {
106
+ this.capabilities.clear();
107
+ this.installAttempted.clear();
108
+ this.warnedKeys.clear();
109
+ }
110
+ /** Snapshot of currently-known optional extension capabilities. */
111
+ getCapabilities() {
112
+ return Array.from(this.capabilities.values());
113
+ }
114
+ /**
115
+ * Ensure an optional extension is loaded on the supplied connection.
116
+ *
117
+ * Returns `true` when the extension is usable on `query`, `false` when it
118
+ * is unavailable. Never throws on install failure — analyze and query
119
+ * paths are expected to degrade gracefully.
120
+ */
121
+ async ensure(query, name, label, opts = {}) {
122
+ if (!EXTENSION_NAME_PATTERN.test(name)) {
123
+ throw new Error(`Invalid DuckDB extension name: ${name}`);
124
+ }
125
+ const policy = opts.policy ?? this.options.policy ?? resolvePolicyFromEnv();
126
+ const timeoutMs = opts.installTimeoutMs ?? this.options.installTimeoutMs ?? getExtensionInstallTimeoutMs();
127
+ const warn = this.options.warn ?? console.warn;
128
+ if (policy === 'never') {
129
+ this.markUnavailable(name, label, 'extension install policy is "never"', warn);
130
+ return false;
131
+ }
132
+ if (await this.tryLoad(query, name)) {
133
+ this.markLoaded(name);
134
+ return true;
135
+ }
136
+ if (policy === 'load-only') {
137
+ this.markUnavailable(name, label, 'load-only policy: extension not pre-installed', warn);
138
+ return false;
139
+ }
140
+ let install = this.installAttempted.get(name);
141
+ if (!install) {
142
+ const installFn = this.options.installExtension ?? installDuckDbExtensionOutOfProcess;
143
+ install = await installFn(name, timeoutMs);
144
+ this.installAttempted.set(name, install);
145
+ }
146
+ if (!install.success) {
147
+ this.markUnavailable(name, label, install.message, warn);
148
+ return false;
149
+ }
150
+ if (await this.tryLoad(query, name)) {
151
+ this.markLoaded(name);
152
+ return true;
153
+ }
154
+ this.markUnavailable(name, label, `LOAD ${name} failed after successful INSTALL`, warn);
155
+ return false;
156
+ }
157
+ async tryLoad(query, name) {
158
+ try {
159
+ await query(`LOAD EXTENSION ${name}`);
160
+ return true;
161
+ }
162
+ catch (err) {
163
+ const msg = err instanceof Error ? err.message : String(err);
164
+ return alreadyAvailable(msg);
165
+ }
166
+ }
167
+ markLoaded(name) {
168
+ this.capabilities.set(name, { name, loaded: true });
169
+ }
170
+ markUnavailable(name, label, reason, warn) {
171
+ this.capabilities.set(name, { name, loaded: false, reason });
172
+ const key = `${name}:${reason}`;
173
+ if (this.warnedKeys.has(key))
174
+ return;
175
+ this.warnedKeys.add(key);
176
+ warn(`GitNexus: ${label} extension unavailable; continuing without ${label} features. ${reason}`);
177
+ }
178
+ }
179
+ /** Process-wide singleton shared by core and pool adapters. */
180
+ export const extensionManager = new ExtensionManager();
181
+ /** Snapshot of which optional DuckDB extensions are loaded in this process. */
182
+ export const getExtensionCapabilities = () => extensionManager.getCapabilities();
183
+ /** Test-only: clear the singleton's cached capability and install state. */
184
+ export const resetExtensionState = () => extensionManager.reset();
@@ -1,6 +1,7 @@
1
1
  import lbug from '@ladybugdb/core';
2
2
  import { KnowledgeGraph } from '../graph/types.js';
3
3
  import type { CachedEmbedding } from '../embeddings/types.js';
4
+ import { type ExtensionEnsureOptions } from './extension-loader.js';
4
5
  /** Factory for creating WriteStreams — injectable for testing. */
5
6
  export type WriteStreamFactory = (filePath: string) => import('fs').WriteStream;
6
7
  /** Result of splitting the relationship CSV into per-label-pair files. */
@@ -122,23 +123,24 @@ export declare const deleteNodesForFile: (filePath: string, dbPath?: string) =>
122
123
  }>;
123
124
  export declare const getEmbeddingTableName: () => string;
124
125
  /**
125
- * Load the FTS extension (required before using FTS functions).
126
+ * Load the FTS extension on the supplied connection (or the singleton
127
+ * writable connection when none is given).
126
128
  *
127
- * Safe to call multiple times when invoked without arguments, tracks loaded
128
- * state via module-level `ftsLoaded`. When invoked with an explicit
129
- * connection, loads on that connection and returns whether the load
130
- * succeeded letting callers (e.g. the pool adapter) track their own state.
131
- *
132
- * Tries `LOAD EXTENSION fts` first so previously-cached installs skip the
133
- * network entirely; falls back to `INSTALL` + `LOAD` only when the extension
134
- * hasn't been cached yet.
129
+ * Delegates to the shared `ExtensionManager` so install policy (auto /
130
+ * load-only / never), out-of-process bounded INSTALL, and capability
131
+ * caching are owned in one place. The module-level `ftsLoaded` flag is
132
+ * kept purely as a per-call short-circuit on the singleton writable
133
+ * connection so repeated callers (e.g. createFTSIndex) avoid an extra
134
+ * `LOAD` round-trip per invocation. Pool adapter callers pass
135
+ * `{ policy: 'load-only' }` so query paths never block on a network install.
135
136
  */
136
- export declare const loadFTSExtension: (targetConn?: lbug.Connection) => Promise<boolean>;
137
+ export declare const loadFTSExtension: (targetConn?: lbug.Connection, opts?: ExtensionEnsureOptions) => Promise<boolean>;
137
138
  /**
138
- * Load the VECTOR extension (required before using QUERY_VECTOR_INDEX).
139
- * Safe to call multiple times -- tracks loaded state via module-level vectorExtensionLoaded.
139
+ * Load the VECTOR extension on the supplied connection (or the singleton
140
+ * writable connection when none is given). Returns false when VECTOR is
141
+ * unavailable so semantic search can fall back to exact scan.
140
142
  */
141
- export declare const loadVectorExtension: () => Promise<void>;
143
+ export declare const loadVectorExtension: (targetConn?: lbug.Connection, opts?: ExtensionEnsureOptions) => Promise<boolean>;
142
144
  /**
143
145
  * Create a full-text search index on a table
144
146
  * @param tableName - The node table name (e.g., 'File', 'CodeSymbol')
@@ -150,10 +152,9 @@ export declare const createFTSIndex: (tableName: string, indexName: string, prop
150
152
  /**
151
153
  * Lazy-create an FTS index, caching the fact in-process.
152
154
  *
153
- * Used by `queryFTS` so that `analyze` doesn't pay the ~440 ms × 5 fixed
154
- * LadybugDB cost up-front (it dominates analyze on small repos). Instead,
155
- * the cost is moved to the first `query`/`context` call in a session,
156
- * where it's amortised across many lookups.
155
+ * Kept for writable maintenance paths that need to lazily materialize an
156
+ * index. Read-only query paths must not call this; production analysis owns
157
+ * creating the configured search indexes before the database is served.
157
158
  *
158
159
  * Safe to call repeatedly — the in-process Set guarantees only the first
159
160
  * call hits LadybugDB. `closeLbug` clears the cache so re-init starts fresh.
@@ -7,6 +7,8 @@ import path from 'path';
7
7
  import lbug from '@ladybugdb/core';
8
8
  import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, STALE_HASH_SENTINEL, } from './schema.js';
9
9
  import { streamAllCSVsToDisk } from './csv-generator.js';
10
+ import { extensionManager } from './extension-loader.js';
11
+ import { isVectorExtensionSupportedByPlatform } from '../platform/capabilities.js';
10
12
  /**
11
13
  * Split a relationship CSV into per-label-pair files on disk.
12
14
  *
@@ -112,11 +114,9 @@ let ftsLoaded = false;
112
114
  let vectorExtensionLoaded = false;
113
115
  /**
114
116
  * 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.
117
+ * writable connection. Prevents repeated `CALL CREATE_FTS_INDEX` round-trips
118
+ * for callers that explicitly opt into `ensureFTSIndex`. Cleared by
119
+ * `closeLbug` so a re-init starts fresh.
120
120
  *
121
121
  * Key format: `${tableName}:${indexName}`.
122
122
  */
@@ -289,8 +289,9 @@ const doInitLbug = async (dbPath) => {
289
289
  }
290
290
  }
291
291
  }
292
- // Load VECTOR extension for semantic search support
293
- await loadVectorExtension();
292
+ // FTS powers baseline search, so initialize it with the core DB. VECTOR is
293
+ // only required for semantic embeddings and is probed lazily there.
294
+ await loadFTSExtension();
294
295
  currentDbPath = dbPath;
295
296
  return { db, conn };
296
297
  };
@@ -761,8 +762,9 @@ export const executeWithReusedStatement = async (cypher, paramsList) => {
761
762
  }
762
763
  }
763
764
  catch (e) {
764
- // Log the error and continue with next batch
765
- console.warn('Batch execution error:', e);
765
+ const msg = e instanceof Error ? e.message : String(e);
766
+ const queryPreview = cypher.replace(/\s+/g, ' ').slice(0, 120);
767
+ throw new Error(`Batch execution failed for rows ${i + 1}-${i + subBatch.length}: ${msg} (${queryPreview})`);
766
768
  }
767
769
  // Note: LadybugDB PreparedStatement doesn't require explicit close()
768
770
  }
@@ -1025,18 +1027,18 @@ export const getEmbeddingTableName = () => EMBEDDING_TABLE_NAME;
1025
1027
  // Full-Text Search (FTS) Functions
1026
1028
  // ============================================================================
1027
1029
  /**
1028
- * Load the FTS extension (required before using FTS functions).
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.
1030
+ * Load the FTS extension on the supplied connection (or the singleton
1031
+ * writable connection when none is given).
1034
1032
  *
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.
1033
+ * Delegates to the shared `ExtensionManager` so install policy (auto /
1034
+ * load-only / never), out-of-process bounded INSTALL, and capability
1035
+ * caching are owned in one place. The module-level `ftsLoaded` flag is
1036
+ * kept purely as a per-call short-circuit on the singleton writable
1037
+ * connection so repeated callers (e.g. createFTSIndex) avoid an extra
1038
+ * `LOAD` round-trip per invocation. Pool adapter callers pass
1039
+ * `{ policy: 'load-only' }` so query paths never block on a network install.
1038
1040
  */
1039
- export const loadFTSExtension = async (targetConn) => {
1041
+ export const loadFTSExtension = async (targetConn, opts = {}) => {
1040
1042
  const useModuleState = targetConn === undefined;
1041
1043
  if (useModuleState && ftsLoaded)
1042
1044
  return true;
@@ -1044,61 +1046,30 @@ export const loadFTSExtension = async (targetConn) => {
1044
1046
  if (!c) {
1045
1047
  throw new Error('LadybugDB not initialized. Call initLbug first.');
1046
1048
  }
1047
- const markLoaded = () => {
1048
- if (useModuleState)
1049
- ftsLoaded = true;
1050
- return true;
1051
- };
1052
- try {
1053
- // Try loading locally first (no network required)
1054
- await c.query('LOAD EXTENSION fts');
1055
- return markLoaded();
1056
- }
1057
- catch {
1058
- // Fall back to install + load (requires network)
1059
- try {
1060
- await c.query('INSTALL fts');
1061
- await c.query('LOAD EXTENSION fts');
1062
- return markLoaded();
1063
- }
1064
- catch (err) {
1065
- const msg = err?.message || '';
1066
- if (msg.includes('already loaded') ||
1067
- msg.includes('already installed') ||
1068
- msg.includes('already exists')) {
1069
- return markLoaded();
1070
- }
1071
- console.error('GitNexus: FTS extension load failed:', msg);
1072
- return false;
1073
- }
1074
- }
1049
+ const loaded = await extensionManager.ensure((sql) => c.query(sql), 'fts', 'FTS', opts);
1050
+ if (loaded && useModuleState)
1051
+ ftsLoaded = true;
1052
+ return loaded;
1075
1053
  };
1076
1054
  /**
1077
- * Load the VECTOR extension (required before using QUERY_VECTOR_INDEX).
1078
- * Safe to call multiple times -- tracks loaded state via module-level vectorExtensionLoaded.
1055
+ * Load the VECTOR extension on the supplied connection (or the singleton
1056
+ * writable connection when none is given). Returns false when VECTOR is
1057
+ * unavailable so semantic search can fall back to exact scan.
1079
1058
  */
1080
- export const loadVectorExtension = async () => {
1081
- if (vectorExtensionLoaded)
1082
- return;
1083
- if (!conn) {
1059
+ export const loadVectorExtension = async (targetConn, opts = {}) => {
1060
+ const useModuleState = targetConn === undefined;
1061
+ if (useModuleState && vectorExtensionLoaded)
1062
+ return true;
1063
+ if (!isVectorExtensionSupportedByPlatform())
1064
+ return false;
1065
+ const c = targetConn ?? conn;
1066
+ if (!c) {
1084
1067
  throw new Error('LadybugDB not initialized. Call initLbug first.');
1085
1068
  }
1086
- try {
1087
- await conn.query('INSTALL VECTOR');
1088
- await conn.query('LOAD EXTENSION VECTOR');
1069
+ const loaded = await extensionManager.ensure((sql) => c.query(sql), 'VECTOR', 'VECTOR', opts);
1070
+ if (loaded && useModuleState)
1089
1071
  vectorExtensionLoaded = true;
1090
- }
1091
- catch (err) {
1092
- const msg = err?.message || '';
1093
- if (msg.includes('already loaded') ||
1094
- msg.includes('already installed') ||
1095
- msg.includes('already exists')) {
1096
- vectorExtensionLoaded = true;
1097
- }
1098
- else {
1099
- console.error('GitNexus: VECTOR extension load failed:', msg);
1100
- }
1101
- }
1072
+ return loaded;
1102
1073
  };
1103
1074
  /**
1104
1075
  * Create a full-text search index on a table
@@ -1111,7 +1082,9 @@ export const createFTSIndex = async (tableName, indexName, properties, stemmer =
1111
1082
  if (!conn) {
1112
1083
  throw new Error('LadybugDB not initialized. Call initLbug first.');
1113
1084
  }
1114
- await loadFTSExtension();
1085
+ if (!(await loadFTSExtension())) {
1086
+ return;
1087
+ }
1115
1088
  const propList = properties.map((p) => `'${p}'`).join(', ');
1116
1089
  const query = `CALL CREATE_FTS_INDEX('${tableName}', '${indexName}', [${propList}], stemmer := '${stemmer}')`;
1117
1090
  try {
@@ -1126,10 +1099,9 @@ export const createFTSIndex = async (tableName, indexName, properties, stemmer =
1126
1099
  /**
1127
1100
  * Lazy-create an FTS index, caching the fact in-process.
1128
1101
  *
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.
1102
+ * Kept for writable maintenance paths that need to lazily materialize an
1103
+ * index. Read-only query paths must not call this; production analysis owns
1104
+ * creating the configured search indexes before the database is served.
1133
1105
  *
1134
1106
  * Safe to call repeatedly — the in-process Set guarantees only the first
1135
1107
  * call hits LadybugDB. `closeLbug` clears the cache so re-init starts fresh.
@@ -122,7 +122,6 @@ function closeOne(repoId) {
122
122
  // for the same dbPath reuse it instead of hitting a file lock.
123
123
  shared.refCount = 0;
124
124
  shared.ftsLoaded = false;
125
- shared.vectorLoaded = false;
126
125
  }
127
126
  else {
128
127
  shared.db.close().catch(() => { });
@@ -248,7 +247,7 @@ async function doInitLbug(repoId, dbPath) {
248
247
  false, // enableCompression (default)
249
248
  true);
250
249
  restoreStdout();
251
- shared = { db, refCount: 0, ftsLoaded: false, vectorLoaded: false };
250
+ shared = { db, refCount: 0, ftsLoaded: false };
252
251
  dbCache.set(dbPath, shared);
253
252
  break;
254
253
  }
@@ -284,19 +283,11 @@ async function doInitLbug(repoId, dbPath) {
284
283
  // Load FTS extension once per shared Database.
285
284
  // Done BEFORE pool registration so no concurrent checkout can grab
286
285
  // the connection while the async FTS load is in progress.
286
+ // policy: 'load-only' — the read pool must never trigger a network
287
+ // install; analyze owns extension installation. If LOAD fails, search
288
+ // features degrade gracefully and the user-facing query path proceeds.
287
289
  if (!shared.ftsLoaded) {
288
- shared.ftsLoaded = await loadFTSExtension(available[0]);
289
- }
290
- // Load VECTOR extension once per shared Database for semantic search support.
291
- if (!shared.vectorLoaded) {
292
- try {
293
- await available[0].query('INSTALL VECTOR');
294
- await available[0].query('LOAD EXTENSION VECTOR');
295
- shared.vectorLoaded = true;
296
- }
297
- catch {
298
- // VECTOR extension may not be available
299
- }
290
+ shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
300
291
  }
301
292
  // Register pool entry only after all connections are pre-warmed and FTS is
302
293
  // loaded. Concurrent executeQuery calls see either "not initialized"
@@ -335,7 +326,7 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
335
326
  // closeOne() respects the external flag and skips db.close().
336
327
  let shared = dbCache.get(dbPath);
337
328
  if (!shared) {
338
- shared = { db: existingDb, refCount: 0, ftsLoaded: false, vectorLoaded: false, external: true };
329
+ shared = { db: existingDb, refCount: 0, ftsLoaded: false, external: true };
339
330
  dbCache.set(dbPath, shared);
340
331
  }
341
332
  shared.refCount++;
@@ -349,20 +340,11 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
349
340
  finally {
350
341
  preWarmActive = false;
351
342
  }
352
- // Load FTS extension if not already loaded on this Database
343
+ // Load FTS extension if not already loaded on this Database.
344
+ // policy: 'load-only' — same contract as initLbug above; the read pool
345
+ // must not block on a network install during query execution.
353
346
  if (!shared.ftsLoaded) {
354
- shared.ftsLoaded = await loadFTSExtension(available[0]);
355
- }
356
- // Load VECTOR extension for semantic search support
357
- if (!shared.vectorLoaded) {
358
- try {
359
- await available[0].query('INSTALL VECTOR');
360
- await available[0].query('LOAD EXTENSION VECTOR');
361
- shared.vectorLoaded = true;
362
- }
363
- catch {
364
- // VECTOR extension may not be available
365
- }
347
+ shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
366
348
  }
367
349
  pool.set(repoId, {
368
350
  db: existingDb,
@@ -0,0 +1,24 @@
1
+ export type CapabilityStatus = 'available' | 'degraded' | 'unavailable';
2
+ export type SemanticSearchMode = 'vector-index' | 'exact-scan' | 'unavailable';
3
+ export interface RuntimeFingerprint {
4
+ platform: NodeJS.Platform;
5
+ arch: string;
6
+ node: string;
7
+ gitnexus: string;
8
+ ladybugdb?: string;
9
+ onnxruntime?: string;
10
+ }
11
+ export interface RuntimeCapabilities {
12
+ graph: CapabilityStatus;
13
+ fts: CapabilityStatus;
14
+ vector: CapabilityStatus;
15
+ semanticMode: SemanticSearchMode;
16
+ exactScanLimit: number;
17
+ reason?: string;
18
+ }
19
+ export declare const DEFAULT_EXACT_SCAN_LIMIT = 10000;
20
+ export declare const getExactScanLimit: () => number;
21
+ export declare const getRuntimeFingerprint: () => RuntimeFingerprint;
22
+ export declare const isVectorExtensionSupportedByPlatform: (platform?: NodeJS.Platform) => boolean;
23
+ export declare const getRuntimeCapabilities: () => RuntimeCapabilities;
24
+ export declare const defaultEmbeddingThreads: () => number;
@@ -0,0 +1,54 @@
1
+ import os from 'os';
2
+ import { createRequire } from 'module';
3
+ const require = createRequire(import.meta.url);
4
+ const packageVersion = (name) => {
5
+ try {
6
+ return require(`${name}/package.json`).version;
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ };
12
+ const gitnexusVersion = () => {
13
+ try {
14
+ return require('../../../package.json').version;
15
+ }
16
+ catch {
17
+ return 'unknown';
18
+ }
19
+ };
20
+ const parsePositiveInt = (value, fallback) => {
21
+ if (value === undefined)
22
+ return fallback;
23
+ const parsed = Number(value);
24
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
25
+ };
26
+ export const DEFAULT_EXACT_SCAN_LIMIT = 10_000;
27
+ export const getExactScanLimit = () => parsePositiveInt(process.env.GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT, DEFAULT_EXACT_SCAN_LIMIT);
28
+ export const getRuntimeFingerprint = () => ({
29
+ platform: process.platform,
30
+ arch: process.arch,
31
+ node: process.version,
32
+ gitnexus: gitnexusVersion(),
33
+ ladybugdb: packageVersion('@ladybugdb/core'),
34
+ onnxruntime: packageVersion('onnxruntime-node'),
35
+ });
36
+ export const isVectorExtensionSupportedByPlatform = (platform = process.platform) => platform !== 'win32';
37
+ export const getRuntimeCapabilities = () => {
38
+ const vector = isVectorExtensionSupportedByPlatform() ? 'available' : 'unavailable';
39
+ const exactScanLimit = getExactScanLimit();
40
+ return {
41
+ graph: 'available',
42
+ fts: 'available',
43
+ vector,
44
+ semanticMode: vector === 'available' ? 'vector-index' : 'exact-scan',
45
+ exactScanLimit,
46
+ reason: vector === 'unavailable'
47
+ ? 'LadybugDB VECTOR is disabled on this platform; semantic search uses exact scan when embeddings exist.'
48
+ : undefined,
49
+ };
50
+ };
51
+ export const defaultEmbeddingThreads = () => {
52
+ const available = typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length;
53
+ return Math.max(1, Math.min(4, Math.floor(available / 2) || 1));
54
+ };