gitnexus 1.6.12 → 1.6.13-rc.2

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 (48) hide show
  1. package/README.md +28 -31
  2. package/dist/cli/analyze.js +7 -24
  3. package/dist/cli/doctor.js +14 -9
  4. package/dist/core/lbug/extension-load-error.d.ts +8 -2
  5. package/dist/core/lbug/extension-load-error.js +23 -16
  6. package/dist/core/lbug/extension-loader.d.ts +21 -6
  7. package/dist/core/lbug/extension-loader.js +72 -17
  8. package/dist/core/lbug/lbug-adapter.d.ts +6 -25
  9. package/dist/core/lbug/lbug-adapter.js +36 -47
  10. package/dist/core/lbug/native-check.d.ts +15 -1
  11. package/dist/core/lbug/native-check.js +45 -22
  12. package/dist/core/lbug/pool-adapter.js +8 -1
  13. package/dist/core/lbug/sidecar-recovery.d.ts +32 -1
  14. package/dist/core/lbug/sidecar-recovery.js +63 -1
  15. package/dist/core/lbug/vendored-extension-path.d.ts +26 -0
  16. package/dist/core/lbug/vendored-extension-path.js +98 -0
  17. package/dist/core/lbug/wal-checkpoint-driver.d.ts +5 -2
  18. package/dist/core/lbug/wal-checkpoint-driver.js +7 -3
  19. package/dist/core/run-analyze.d.ts +4 -2
  20. package/dist/core/run-analyze.js +205 -68
  21. package/dist/core/search/fts-crash-marker.d.ts +59 -0
  22. package/dist/core/search/fts-crash-marker.js +55 -0
  23. package/dist/core/search/fts-indexes.js +2 -2
  24. package/dist/core/search/fts-policy.d.ts +11 -1
  25. package/dist/core/search/fts-policy.js +47 -0
  26. package/dist/core/tree-sitter/vendored-grammars.d.ts +2 -10
  27. package/dist/core/tree-sitter/vendored-grammars.js +2 -11
  28. package/dist/core/vendor-root.d.ts +8 -0
  29. package/dist/core/vendor-root.js +10 -0
  30. package/dist/mcp/server.js +10 -3
  31. package/dist/server/api.js +21 -14
  32. package/dist/storage/repo-meta.d.ts +31 -2
  33. package/package.json +2 -2
  34. package/scripts/assert-publish-fts-coverage.cjs +246 -0
  35. package/scripts/cross-platform-shard.ts +7 -2
  36. package/scripts/cross-platform-tests.ts +15 -0
  37. package/scripts/ensure-fts.ts +13 -11
  38. package/vendor/lbug-fts/LICENSE +27 -0
  39. package/vendor/lbug-fts/manifest.json +21 -0
  40. package/vendor/lbug-fts/prebuilds/SHA256SUMS +5 -0
  41. package/vendor/lbug-fts/prebuilds/darwin-arm64/libfts.lbug_extension +0 -0
  42. package/vendor/lbug-fts/prebuilds/darwin-x64/libfts.lbug_extension +0 -0
  43. package/vendor/lbug-fts/prebuilds/linux-arm64/libfts.lbug_extension +0 -0
  44. package/vendor/lbug-fts/prebuilds/linux-x64/libfts.lbug_extension +0 -0
  45. package/vendor/lbug-fts/prebuilds/win32-x64/libfts.lbug_extension +0 -0
  46. package/web/assets/{agent-DeT_Hy8W.js → agent-DiLVv9Xg.js} +8 -8
  47. package/web/assets/{index-K4KOdfVv.js → index-coee0RPm.js} +3 -3
  48. package/web/index.html +1 -1
package/README.md CHANGED
@@ -518,11 +518,11 @@ truthy, when the install is not an npm global/local install (npx cache, dev
518
518
  checkout, Docker image — the Docker CLI image sets the opt-out itself), or
519
519
  when opted out:
520
520
 
521
- | Variable | Effect |
522
- | --- | --- |
523
- | `GITNEXUS_NO_UPDATE_NOTIFIER` | Truthy (`1`, `true`, …) disables the update check on every surface. |
524
- | `NO_UPDATE_NOTIFIER` | Cross-tool convention; honored the same way. |
525
- | `npm_config_registry` | The check reads the `latest` dist-tag from this registry instead of `https://registry.npmjs.org`. Credentials are never sent, and registries that require authentication are not supported (the check silently skips). |
521
+ | Variable | Effect |
522
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
523
+ | `GITNEXUS_NO_UPDATE_NOTIFIER` | Truthy (`1`, `true`, …) disables the update check on every surface. |
524
+ | `NO_UPDATE_NOTIFIER` | Cross-tool convention; honored the same way. |
525
+ | `npm_config_registry` | The check reads the `latest` dist-tag from this registry instead of `https://registry.npmjs.org`. Credentials are never sent, and registries that require authentication are not supported (the check silently skips). |
526
526
 
527
527
  Eval harnesses running a global install can set `GITNEXUS_NO_UPDATE_NOTIFIER`
528
528
  for a quiet registry.
@@ -614,20 +614,17 @@ runtime dependencies Windows does not ship by default:
614
614
 
615
615
  1. **Microsoft Visual C++ 2015-2022 Redistributable (x64)** —
616
616
  <https://aka.ms/vs/17/release/vc_redist.x64.exe>
617
- 2. **OpenSSL 3** — `libssl-3-x64.dll` and `libcrypto-3-x64.dll`, resolvable on `PATH`
617
+ 2. **OpenSSL 3** — install it as a system runtime so `libssl-3-x64.dll` and
618
+ `libcrypto-3-x64.dll` resolve without borrowing them from another application.
618
619
 
619
- The redistributable alone is **not** sufficient. If Git for Windows is installed you already have
620
- the OpenSSL DLLs — run `gitnexus` from **Git Bash**, or prepend the directory to `PATH` in the
621
- shell you use:
620
+ The redistributable alone is **not** sufficient. Do not prepend a third-party
621
+ application directory (including Git for Windows) to `PATH` to pick up those DLLs.
622
622
 
623
- ```powershell
624
- $env:PATH = "C:\Program Files\Git\mingw64\bin;$env:PATH"
625
- gitnexus analyze --repair-fts
626
- ```
627
-
628
- Without them the index is still built, but without search tables, so `query` returns empty keyword
629
- results until you re-run `gitnexus analyze --repair-fts` from a shell where the DLLs resolve
630
- ([#2669](https://github.com/abhigyanpatwari/GitNexus/issues/2669)).
623
+ Without both runtimes the index is still built, but without search tables, so
624
+ `query` returns empty keyword results until you install the prerequisites and
625
+ re-run `gitnexus analyze --repair-fts`
626
+ ([#2669](https://github.com/abhigyanpatwari/GitNexus/issues/2669),
627
+ [#3218](https://github.com/abhigyanpatwari/GitNexus/issues/3218)).
631
628
 
632
629
  ### Installation fails with native module errors
633
630
 
@@ -671,20 +668,20 @@ GitNexus uses optional DuckDB extensions for BM25 and vector search. The `gitnex
671
668
 
672
669
  Configure the behavior with these environment variables:
673
670
 
674
- | Variable | Values | Default | Effect |
675
- | -------------------------------------------- | ------------------------------ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
676
- | `GITNEXUS_LBUG_EXTENSION_INSTALL` | `auto`, `load-only`, `never` | `auto` | `auto` runs one bounded install if LOAD fails — a plain `INSTALL`, escalating to `FORCE INSTALL` only when the LOAD error shows the present extension file is broken. `load-only` only uses already-installed extensions (recommended for offline / firewalled environments). `never` skips optional extensions entirely. |
677
- | `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` | positive integer | `15000` | Wall-clock budget for the out-of-process extension-install child before it is killed. |
678
- | `GITNEXUS_FTS_STEMMER` | supported LadybugDB stemmer | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` when that better matches repository comments and identifiers. Re-run `gitnexus analyze --repair-fts` after changing it. |
679
- | `GITNEXUS_FTS_CJK_SEGMENTATION` | `none`, `bigram` | `none` | `bigram` inserts overlapping character-bigram boundaries into Chinese/Japanese Han-ideograph spans in `content`/`description` before FTS indexing, so LadybugDB's space-only tokenizer can see sub-phrase word boundaries. Scoped to CJK Unified Ideographs only — Japanese Hiragana/Katakana and Korean Hangul are not currently segmented. Unlike `GITNEXUS_FTS_STEMMER`, this rewrites stored text — enabling it on an already-indexed repo requires a full `gitnexus analyze --force`; neither `--repair-fts` nor a plain incremental `analyze` applies it to previously-indexed files. Set the same value wherever `analyze` and search-serving processes (CLI query, MCP server, web server) run. |
680
- | `GITNEXUS_STORAGE_PATH` | absolute, non-empty directory | unset (repo-local) | Complete external index directory. This preserves the existing configuration semantics and takes precedence over `GITNEXUS_STORAGE_ROOT` when both are set. |
681
- | `GITNEXUS_STORAGE_ROOT` | absolute, non-empty directory | unset (repo-local) | Absolute root directory for external indexes. GitNexus creates an isolated `<repo-basename>-<canonical-path-hash>/` slot beneath it for each repository, then registers the resolved slot so `status`, MCP, and `serve` can reopen it later. |
682
- | `GITNEXUS_CONTENT_RETENTION` | `full`, `symbol`, `none` | `full` | Source-text retention profile: `full` keeps file and symbol text, `symbol` keeps symbol snippets without full file content, and `none` keeps the structural graph without source body text. |
683
- | `GITNEXUS_STREAM_GRAPH_EMIT` | `0`, `1` | `1` (on) | **On by default** on a full rebuild (`--force`); incremental runs ignore it. Holds structural relationships (CALLS, IMPORTS, ACCESSES, CONTAINS, ...) as CSV-on-disk plus compact in-memory columns instead of as objects in three overlapping indexes, cutting peak in-memory graph heap by ~1.4x at no measurable CPU cost (measured A/B on a synthetic 400k-node / 1.08M-edge graph: 819 MB -> 584 MB, iteration at parity, scaling verified linear from 100k to 800k nodes, with every edge still visible through the graph interface; no end-to-end measurement on a real repository yet). Nothing is traded away — community detection, process extraction, PDG taint summaries and the local-symbol pruner all read a complete relationship set and behave identically. Set to `0` only to bisect a suspected streaming-related fault. |
684
- | `GITNEXUS_COMMUNITY_ENGINE` | `graphology`, `icebug`, `auto` | `graphology` | Community-detection engine used during analyze. `graphology` is the supported default. `icebug` and `auto` are **experimental** and currently behave identically: both try the optional `@ladybugmem/icebug` native Leiden over a CSR export and fall back to Graphology if it is not installed, cannot load, or lacks the deterministic thread/seed controls. Experimental engines partition differently, so community IDs are not comparable across engines. |
685
- | `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | integer `>= -1` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; `-1` keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. |
686
- | `GITNEXUS_LBUG_BUFFER_POOL_SIZE` | integer `>= 0` (bytes) | min(2 GiB, 80% RAM) | LadybugDB buffer-pool ceiling for every GitNexus database (analyze, MCP server, serve, group bridges). Bounded so a long-lived `gitnexus mcp` process or a large incremental `analyze` cannot grow toward LadybugDB's native 80%-of-RAM default and OOM the host (#2557). `0` restores that native unbounded default; invalid values warn and fall back to the default. During `analyze` the pool is right-sized to the graph and, on non-4 KiB-page hosts (Apple Silicon 16 KiB, Ascend/aarch64 64 KiB), scaled by the page-size granule ratio up to min(2 GiB × pageSize/4 KiB, 80% RAM) (#2631); this env var overrides all of that as an absolute value. |
687
- | `GITNEXUS_LBUG_MAX_DB_SIZE` | positive integer (bytes) | `17179869184` (16 GiB) | Upper bound for a single LadybugDB database file. This is an mmap/disk-address-space ceiling, not a memory limit — it does not constrain the buffer pool (use `GITNEXUS_LBUG_BUFFER_POOL_SIZE` for that). Raise it when indexing genuinely huge monorepos; invalid values silently fall back to the default. |
671
+ | Variable | Values | Default | Effect |
672
+ | -------------------------------------------- | ------------------------------ | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
673
+ | `GITNEXUS_LBUG_EXTENSION_INSTALL` | `auto`, `load-only`, `never` | `load-only` globally; `analyze` defaults to `auto` | The process-wide default is `load-only` so serve/MCP/query never install over the network. `gitnexus analyze` overrides to `auto` unless you set the env. FTS loads the packaged per-platform artifact first (macOS, Windows, and Linux), then a named `LOAD`, then `INSTALL` only under `auto`. `never` skips optional extensions entirely. |
674
+ | `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` | positive integer | `15000` | Wall-clock budget for the out-of-process extension-install child before it is killed. |
675
+ | `GITNEXUS_FTS_STEMMER` | supported LadybugDB stemmer | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` when that better matches repository comments and identifiers. Re-run `gitnexus analyze --repair-fts` after changing it. |
676
+ | `GITNEXUS_FTS_CJK_SEGMENTATION` | `none`, `bigram` | `none` | `bigram` inserts overlapping character-bigram boundaries into Chinese/Japanese Han-ideograph spans in `content`/`description` before FTS indexing, so LadybugDB's space-only tokenizer can see sub-phrase word boundaries. Scoped to CJK Unified Ideographs only — Japanese Hiragana/Katakana and Korean Hangul are not currently segmented. Unlike `GITNEXUS_FTS_STEMMER`, this rewrites stored text — enabling it on an already-indexed repo requires a full `gitnexus analyze --force`; neither `--repair-fts` nor a plain incremental `analyze` applies it to previously-indexed files. Set the same value wherever `analyze` and search-serving processes (CLI query, MCP server, web server) run. |
677
+ | `GITNEXUS_STORAGE_PATH` | absolute, non-empty directory | unset (repo-local) | Complete external index directory. This preserves the existing configuration semantics and takes precedence over `GITNEXUS_STORAGE_ROOT` when both are set. |
678
+ | `GITNEXUS_STORAGE_ROOT` | absolute, non-empty directory | unset (repo-local) | Absolute root directory for external indexes. GitNexus creates an isolated `<repo-basename>-<canonical-path-hash>/` slot beneath it for each repository, then registers the resolved slot so `status`, MCP, and `serve` can reopen it later. |
679
+ | `GITNEXUS_CONTENT_RETENTION` | `full`, `symbol`, `none` | `full` | Source-text retention profile: `full` keeps file and symbol text, `symbol` keeps symbol snippets without full file content, and `none` keeps the structural graph without source body text. |
680
+ | `GITNEXUS_STREAM_GRAPH_EMIT` | `0`, `1` | `1` (on) | **On by default** on a full rebuild (`--force`); incremental runs ignore it. Holds structural relationships (CALLS, IMPORTS, ACCESSES, CONTAINS, ...) as CSV-on-disk plus compact in-memory columns instead of as objects in three overlapping indexes, cutting peak in-memory graph heap by ~1.4x at no measurable CPU cost (measured A/B on a synthetic 400k-node / 1.08M-edge graph: 819 MB -> 584 MB, iteration at parity, scaling verified linear from 100k to 800k nodes, with every edge still visible through the graph interface; no end-to-end measurement on a real repository yet). Nothing is traded away — community detection, process extraction, PDG taint summaries and the local-symbol pruner all read a complete relationship set and behave identically. Set to `0` only to bisect a suspected streaming-related fault. |
681
+ | `GITNEXUS_COMMUNITY_ENGINE` | `graphology`, `icebug`, `auto` | `graphology` | Community-detection engine used during analyze. `graphology` is the supported default. `icebug` and `auto` are **experimental** and currently behave identically: both try the optional `@ladybugmem/icebug` native Leiden over a CSR export and fall back to Graphology if it is not installed, cannot load, or lacks the deterministic thread/seed controls. Experimental engines partition differently, so community IDs are not comparable across engines. |
682
+ | `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | integer `>= -1` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; `-1` keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. |
683
+ | `GITNEXUS_LBUG_BUFFER_POOL_SIZE` | integer `>= 0` (bytes) | min(2 GiB, 80% RAM) | LadybugDB buffer-pool ceiling for every GitNexus database (analyze, MCP server, serve, group bridges). Bounded so a long-lived `gitnexus mcp` process or a large incremental `analyze` cannot grow toward LadybugDB's native 80%-of-RAM default and OOM the host (#2557). `0` restores that native unbounded default; invalid values warn and fall back to the default. During `analyze` the pool is right-sized to the graph and, on non-4 KiB-page hosts (Apple Silicon 16 KiB, Ascend/aarch64 64 KiB), scaled by the page-size granule ratio up to min(2 GiB × pageSize/4 KiB, 80% RAM) (#2631); this env var overrides all of that as an absolute value. |
684
+ | `GITNEXUS_LBUG_MAX_DB_SIZE` | positive integer (bytes) | `17179869184` (16 GiB) | Upper bound for a single LadybugDB database file. This is an mmap/disk-address-space ceiling, not a memory limit — it does not constrain the buffer pool (use `GITNEXUS_LBUG_BUFFER_POOL_SIZE` for that). Raise it when indexing genuinely huge monorepos; invalid values silently fall back to the default. |
688
685
 
689
686
  ```bash
690
687
  # Offline/airgapped: never reach the network for extensions
@@ -12,7 +12,7 @@ import os from 'os';
12
12
  import { spawn } from 'child_process';
13
13
  import v8 from 'v8';
14
14
  import cliProgress from 'cli-progress';
15
- import { FTS_DISABLED_MESSAGE, isExplicitFtsDisablement } from '../core/search/fts-policy.js';
15
+ import { formatAnalyzeFtsSkipSummary } from '../core/search/fts-policy.js';
16
16
  import { isLbugReady, LbugWipeError } from '../core/lbug/lbug-adapter.js';
17
17
  import { boundedCheckpointBeforeExit } from '../core/lbug/shutdown-helpers.js';
18
18
  import { findUndeclaredRelationPairError } from '../core/lbug/rel-pair-routing.js';
@@ -1189,8 +1189,9 @@ const analyzeCommandImpl = async (inputPath, cliOptions, runnerIdentityAtBootstr
1189
1189
  console.error = origError;
1190
1190
  bar.stop();
1191
1191
  console.log(' Already up to date\n');
1192
- if (isExplicitFtsDisablement(result.ftsSkipReason))
1193
- console.log(` ${FTS_DISABLED_MESSAGE}\n`);
1192
+ if (result.ftsSkipped) {
1193
+ console.log(` ${formatAnalyzeFtsSkipSummary(result.ftsSkipReason)}\n`);
1194
+ }
1194
1195
  if (runOptions.registryName) {
1195
1196
  console.log(` Registry name: ${result.repoName}\n`);
1196
1197
  }
@@ -1320,27 +1321,9 @@ const analyzeCommandImpl = async (inputPath, cliOptions, runnerIdentityAtBootstr
1320
1321
  // progress-bar log() that fired mid-run has already scrolled away, so the
1321
1322
  // degraded-search state must also appear in the final summary (#1161).
1322
1323
  if (result.ftsSkipped) {
1323
- // #2658 review L2: a build/verify failure is NOT an extension-unavailable
1324
- // problem — sending the user to install the extension is the wrong remedy.
1325
- if (isExplicitFtsDisablement(result.ftsSkipReason)) {
1326
- console.log(`\n ${FTS_DISABLED_MESSAGE}`);
1327
- }
1328
- else if (result.ftsSkipReason === 'build-failed') {
1329
- console.log(`\n Warning: full-text/BM25 search is disabled — the search index build failed this run.\n` +
1330
- ` The FTS extension is available; rerun \`gitnexus analyze --repair-fts\`. If it persists,\n` +
1331
- ` check the disk for space or corruption. Run \`gitnexus doctor\` for details.`);
1332
- }
1333
- else {
1334
- console.log(
1335
- // NOT "then rerun" (#2841 §5.C): this run stamped `lastCommit`, so a
1336
- // plain rerun on an unchanged tree takes the up-to-date fast path and
1337
- // returns before Phase 3 could rebuild anything — the advice would be
1338
- // ineffective exactly when the user follows it. `--repair-fts` is the
1339
- // verb that rebuilds the search indexes without re-parsing the repo.
1340
- `\n Warning: full-text/BM25 search is disabled — the LadybugDB FTS extension was unavailable.\n` +
1341
- ` Install it once with network access (GITNEXUS_LBUG_EXTENSION_INSTALL=auto), then run\n` +
1342
- ` \`gitnexus analyze --repair-fts\` to build the search indexes. Run \`gitnexus doctor\` for details.`);
1343
- }
1324
+ // Total switch (#2658 L2 + native-abort/tuple-missing): a new skip
1325
+ // reason must not inherit the network-install remedy.
1326
+ console.log(`\n ${formatAnalyzeFtsSkipSummary(result.ftsSkipReason)}`);
1344
1327
  }
1345
1328
  try {
1346
1329
  await fs.access(getGlobalRegistryPath());
@@ -4,10 +4,11 @@ import { isHttpMode } from '../core/embeddings/http-client.js';
4
4
  import { getLocalEmbeddingRuntimeBlocker, localEmbeddingPrefixUnloadableMessage, localEmbeddingStackMissingMessage, } from '../core/embeddings/runtime-support.js';
5
5
  import { isPrefixRuntimeLoadable, resolveEmbeddingRuntime, } from '../core/embeddings/runtime-install.js';
6
6
  import { cudaRedirectDoctorStatus } from '../core/embeddings/onnxruntime-node-resolver.js';
7
- import { checkLbugNative, probeFtsExtensionLoad, probeVectorExtensionLoad, } from '../core/lbug/native-check.js';
7
+ import { checkLbugNative, ftsAvailabilityLabel, probeFtsExtensionLoad, probeVectorExtensionLoad, } from '../core/lbug/native-check.js';
8
8
  import { getEffectiveBufferPoolSize, getOsPageSize, isPageSizeAwareLadybug, } from '../core/lbug/lbug-config.js';
9
- import { diagnoseExtensionLoad } from '../core/lbug/extension-load-error.js';
10
- import { getExtensionInstallPolicy } from '../core/lbug/extension-loader.js';
9
+ import { diagnoseExtensionLoad, extractExtensionPath } from '../core/lbug/extension-load-error.js';
10
+ import { resolveFtsVersionPair } from '../core/lbug/vendored-extension-path.js';
11
+ import { getExtensionInstallPolicy, resolveAnalyzeInstallPolicy, } from '../core/lbug/extension-loader.js';
11
12
  import { updateEligibleInstallSync } from '../core/install-context.js';
12
13
  import { readValidatedUpdateCacheSync } from '../core/update-cache.js';
13
14
  import { t } from './i18n/index.js';
@@ -208,7 +209,7 @@ export const doctorCommand = async () => {
208
209
  const ftsProbe = nativeCheck.ok
209
210
  ? await probeFtsExtensionLoad()
210
211
  : { loaded: false, reason: 'LadybugDB native module (lbugjs.node) failed to load' };
211
- console.log(` ${label('doctor.labels.fullTextSearch', 18)}${ftsProbe.loaded ? 'available' : 'unavailable'}`);
212
+ console.log(` ${label('doctor.labels.fullTextSearch', 18)}${ftsAvailabilityLabel(ftsProbe)}`);
212
213
  if (!ftsProbe.loaded && ftsProbe.reason) {
213
214
  console.log(` ${padDisplayEnd('', 18)}${ftsProbe.reason}`);
214
215
  // Add an actionable remedy for recognized failure classes (#2374). The
@@ -216,7 +217,10 @@ export const doctorCommand = async () => {
216
217
  // ("specified module could not be found") is opaque, so name the fix (VC++
217
218
  // redist, then OpenSSL) instead of leaving the user to reinstall in vain.
218
219
  // `unknown`'s remedy is "run doctor", which would be circular here.
219
- const { kind, remedy } = diagnoseExtensionLoad(ftsProbe.reason);
220
+ // Policy `never` is not a load failure — skip structural diagnosis.
221
+ const { kind, remedy } = ftsProbe.suppressed
222
+ ? { kind: 'unknown', remedy: '' }
223
+ : diagnoseExtensionLoad(ftsProbe.reason, 'FTS', extractExtensionPath(ftsProbe.reason), resolveFtsVersionPair(extractExtensionPath(ftsProbe.reason)));
220
224
  if (kind !== 'unknown') {
221
225
  console.log(` ${padDisplayEnd('', 18)}${remedy}`);
222
226
  }
@@ -246,13 +250,14 @@ export const doctorCommand = async () => {
246
250
  // Surface the optional-extension install policy so offline users can see
247
251
  // whether analyze/query will reach the network (extension.ladybugdb.com).
248
252
  // Literal label (like the 'native' line) to avoid adding i18n keys.
249
- const installPolicy = getExtensionInstallPolicy();
250
- const policyHint = installPolicy === 'load-only'
253
+ const serveQueryPolicy = getExtensionInstallPolicy();
254
+ const analyzePolicy = resolveAnalyzeInstallPolicy();
255
+ const policyHint = (policy) => policy === 'load-only'
251
256
  ? ' (offline; load only, no network install)'
252
- : installPolicy === 'never'
257
+ : policy === 'never'
253
258
  ? ' (optional extensions disabled)'
254
259
  : ' (installs missing extensions over network)';
255
- console.log(` ${padDisplayEnd('Ext install:', 18)}${installPolicy}${policyHint}`);
260
+ console.log(` ${padDisplayEnd('Ext install:', 18)}serve/query=${serveQueryPolicy}${policyHint(serveQueryPolicy)}; analyze=${analyzePolicy}${policyHint(analyzePolicy)}`);
256
261
  console.log(` ${label('doctor.labels.exactScanLimit', 18)}${t('doctor.chunks', { count: capabilities.exactScanLimit })}`);
257
262
  if (capabilities.reason)
258
263
  console.log(` ${label('doctor.labels.note', 18)}${capabilities.reason}`);
@@ -1,4 +1,10 @@
1
- export type ExtensionLoadErrorKind = 'missing_file' | 'corrupt_file' | 'missing_dependency' | 'unknown';
1
+ export type ExtensionLoadErrorKind = 'missing_file' | 'corrupt_file' | 'missing_dependency' | 'version_skew' | 'unknown';
2
+ export interface ExtensionVersionPair {
3
+ expected?: string;
4
+ found?: string;
5
+ }
6
+ /** Kinds whose remedy must replace the generic network-install tail. */
7
+ export declare const usesClassifiedLoadRemedy: (kind: ExtensionLoadErrorKind) => boolean;
2
8
  export interface ExtensionLoadDiagnosis {
3
9
  readonly kind: ExtensionLoadErrorKind;
4
10
  /** Actionable, literal-English remedy suited to the class. */
@@ -63,5 +69,5 @@ export declare function inspectExtensionBinary(extensionPath: string | null | un
63
69
  * classifier (which still carries the language-independent hedged fallback). This
64
70
  * is the entry point every surface should call.
65
71
  */
66
- export declare function diagnoseExtensionLoad(reason: string | undefined | null, label?: string): ExtensionLoadDiagnosis;
72
+ export declare function diagnoseExtensionLoad(reason: string | undefined | null, label?: string, explicitPath?: string | null, versions?: ExtensionVersionPair): ExtensionLoadDiagnosis;
67
73
  export {};
@@ -24,6 +24,8 @@
24
24
  * lbug) and never throws — any read failure degrades to the string classifier.
25
25
  */
26
26
  import { closeSync, openSync, readSync } from 'node:fs';
27
+ /** Kinds whose remedy must replace the generic network-install tail. */
28
+ export const usesClassifiedLoadRemedy = (kind) => kind === 'missing_dependency' || kind === 'version_skew';
27
29
  /** LadybugDB says the extension file was never installed. INSTALL can heal it. */
28
30
  const MISSING_FILE_SIGNATURES = [
29
31
  /has not been installed/i,
@@ -92,6 +94,10 @@ const LOAD_FAILURE_WRAPPER = /failed to load library/i;
92
94
  // VECTOR through the same classifier, and FTS-specific advice (`--repair-fts`
93
95
  // repairs FTS indexes only) must not be dispensed for other extensions.
94
96
  const repairFtsHint = (label, lead) => label === 'FTS' ? ` (${lead}\`gitnexus analyze --repair-fts\`)` : '';
97
+ const VERSION_SKEW_HINT = 'This is a version mismatch, not a missing host runtime.';
98
+ const versionSkewRemedy = (label, expected, found) => `The ${label} extension version ${found} does not match the expected ${expected}. ` +
99
+ `Use a matching artifact${repairFtsHint(label, 'or ')} and run \`gitnexus doctor\`. ` +
100
+ VERSION_SKEW_HINT;
95
101
  const missingFileRemedy = (label) => `The ${label} extension is not installed. Re-run with network access and ` +
96
102
  `GITNEXUS_LBUG_EXTENSION_INSTALL=auto${repairFtsHint(label, 'or ')} to download it.`;
97
103
  const corruptFileRemedy = (label) => `The ${label} extension file is present but unreadable (corrupt, truncated, or built for another ` +
@@ -102,22 +108,18 @@ const corruptFileRemedy = (label) => `The ${label} extension file is present but
102
108
  // drift between them (#2383 F5).
103
109
  const VC_REDIST_INSTALL_HINT = 'the Microsoft Visual C++ 2015-2022 Redistributable (x64) from ' +
104
110
  'https://aka.ms/vs/17/release/vc_redist.x64.exe';
105
- // Git for Windows already ships the OpenSSL 3 DLLs in its mingw64 bin directory,
106
- // so the identical command that fails in PowerShell succeeds in Git Bash (#2669
107
- // reporter, who had the VC++ redist installed and still failed until that
108
- // directory was on PATH). Deliberately a fixed system path and never a
109
- // user-profile one: remedy text is NOT path-redacted (fts-indexes.ts redacts
110
- // only the reason), and fts-degraded-warning.test.ts asserts that no
111
- // `C:\Users\…` path ever reaches a user through this surface.
112
- const GIT_BASH_OPENSSL_HINT = ' If Git for Windows is installed you already have those DLLs: run the same command from Git Bash, ' +
113
- 'or prepend "C:\\Program Files\\Git\\mingw64\\bin" to PATH.';
111
+ // U7 arm B (OQ1 unanswered; KTD13 forbids shipping OpenSSL DLLs without a
112
+ // named CVE owner). Name the system runtimes. Do not tell anyone to borrow
113
+ // DLLs from Git for Windows or prepend a third-party application directory.
114
+ const WINDOWS_OPENSSL_RUNTIME_HINT = ' If the error persists, install OpenSSL 3 as a system runtime so ' +
115
+ 'libcrypto-3-x64.dll and libssl-3-x64.dll resolve without borrowing them ' +
116
+ 'from another application.';
114
117
  // MSVC-first per DuckDB's canonical answer for this exact error; OpenSSL second.
115
118
  const windowsMissingDependencyRemedy = (label) => `The ${label} extension is present but a required runtime library is missing (Windows error 126). ` +
116
119
  'Reinstalling the extension will NOT help. Install ' +
117
120
  VC_REDIST_INSTALL_HINT +
118
- '; if the error persists, the extension also needs OpenSSL 3 ' +
119
- '(libcrypto-3-x64.dll / libssl-3-x64.dll) on the DLL search path.' +
120
- GIT_BASH_OPENSSL_HINT;
121
+ '.' +
122
+ WINDOWS_OPENSSL_RUNTIME_HINT;
121
123
  const posixMissingDependencyRemedy = (label) => `The ${label} extension is present but a shared library it depends on could not be loaded (named in ` +
122
124
  'the error above). Reinstalling the extension will NOT help — install that library or add it to ' +
123
125
  'your loader search path.';
@@ -170,8 +172,7 @@ export function classifyExtensionLoadError(reason, label = 'FTS') {
170
172
  const structuralMissingDependencyRemedy = (label) => `The ${label} extension file is valid, so the failure is a missing or incompatible runtime dependency, ` +
171
173
  'not the extension itself — reinstalling will NOT help. On Windows, install ' +
172
174
  VC_REDIST_INSTALL_HINT +
173
- ' and ensure OpenSSL 3 is available; on Linux/macOS install the shared library named in the error above.' +
174
- GIT_BASH_OPENSSL_HINT;
175
+ ' and install OpenSSL 3 as a system runtime; on Linux/macOS install the shared library named in the error above.';
175
176
  /**
176
177
  * Pull the extension file path out of lbug's load error. lbug's wrapper is
177
178
  * English regardless of OS language — `Failed to load library: {path} which is
@@ -301,10 +302,10 @@ export function inspectExtensionBinary(extensionPath) {
301
302
  * classifier (which still carries the language-independent hedged fallback). This
302
303
  * is the entry point every surface should call.
303
304
  */
304
- export function diagnoseExtensionLoad(reason, label = 'FTS') {
305
+ export function diagnoseExtensionLoad(reason, label = 'FTS', explicitPath, versions) {
305
306
  const text = reason ?? '';
306
307
  const stringResult = classifyExtensionLoadError(text, label);
307
- const fileState = inspectExtensionBinary(extractExtensionPath(text));
308
+ const fileState = inspectExtensionBinary(explicitPath ?? extractExtensionPath(text));
308
309
  if (fileState === 'corrupt') {
309
310
  return { kind: 'corrupt_file', remedy: corruptFileRemedy(label) };
310
311
  }
@@ -319,6 +320,12 @@ export function diagnoseExtensionLoad(reason, label = 'FTS') {
319
320
  if (stringResult.kind === 'corrupt_file') {
320
321
  return stringResult;
321
322
  }
323
+ if (versions?.expected && versions.found && versions.expected !== versions.found) {
324
+ return {
325
+ kind: 'version_skew',
326
+ remedy: versionSkewRemedy(label, versions.expected, versions.found),
327
+ };
328
+ }
322
329
  // A structurally sound binary that still failed to load ⇒ a dependency/runtime
323
330
  // problem, decided WITHOUT the localized tail. Keep the string classifier's
324
331
  // sharper remedy when it recognized the specific case (e.g. English 126).
@@ -1,4 +1,10 @@
1
1
  import { type ExtensionLoadDiagnosis } from './extension-load-error.js';
2
+ export type ExtensionAttemptSource = 'vendored' | 'named' | 'install';
3
+ /** Structured load attempt — source and tuple labels only, never a path. */
4
+ export interface ExtensionLoadAttempt {
5
+ source: ExtensionAttemptSource;
6
+ tuple?: string;
7
+ }
2
8
  /**
3
9
  * Lifecycle policy for an optional DuckDB extension.
4
10
  *
@@ -27,6 +33,8 @@ export interface ExtensionCapability {
27
33
  * cached remedy instead of re-inspecting the extension file on every call (#2383 F3).
28
34
  */
29
35
  diagnosis?: ExtensionLoadDiagnosis;
36
+ /** What this ensure() tried, labels only (KTD7). */
37
+ attempts?: ExtensionLoadAttempt[];
30
38
  }
31
39
  /** Per-call overrides applied on top of `ExtensionManager` defaults. */
32
40
  export interface ExtensionEnsureOptions {
@@ -44,6 +52,10 @@ export interface ExtensionEnsureOptions {
44
52
  * degradation goes unreported.
45
53
  */
46
54
  quiet?: boolean;
55
+ /** Injected vendor tree for tests / e2e. Never an attacker-controlled env. */
56
+ vendorRoot?: string;
57
+ /** Injected Node platform tuple (`linux-x64`). Defaults to this process. */
58
+ platformTuple?: string;
47
59
  }
48
60
  export interface ExtensionManagerOptions {
49
61
  policy?: ExtensionInstallPolicy;
@@ -81,12 +93,13 @@ export declare const installDuckDbExtensionOutOfProcess: (extensionName: string,
81
93
  /**
82
94
  * Centralized lifecycle manager for optional LadybugDB extensions.
83
95
  *
84
- * Always tries `LOAD EXTENSION <name>` first — it is per-connection,
85
- * idempotent, and never touches the network. If `LOAD` fails and the active
86
- * policy permits, the manager runs a single bounded out-of-process `INSTALL`
87
- * attempt per process and retries `LOAD`. Capability outcomes are cached so
88
- * unavailable extensions degrade search features without ever blocking
89
- * subsequent analyze or query calls.
96
+ * Tries `LOAD` first — it is per-connection, idempotent, and never
97
+ * touches the network. For FTS, a packaged vendored path is path-LOADed
98
+ * before the named `LOAD EXTENSION fts`. If `LOAD` fails and the active
99
+ * policy permits, the manager runs a single bounded out-of-process
100
+ * `INSTALL` attempt per process and retries `LOAD`. Capability outcomes
101
+ * are cached so unavailable extensions degrade search features without
102
+ * ever blocking subsequent analyze or query calls.
90
103
  *
91
104
  * Policy precedence (most specific wins):
92
105
  * per-call `opts.policy` → constructor `options.policy` → env → `load-only`
@@ -118,6 +131,8 @@ export declare class ExtensionManager {
118
131
  * existed all along (#2374).
119
132
  */
120
133
  private tryLoad;
134
+ private tryLoadPath;
135
+ private composeReason;
121
136
  private markLoaded;
122
137
  private markUnavailable;
123
138
  }
@@ -1,7 +1,9 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { LBUG_MAX_DB_SIZE } from './lbug-config.js';
4
- import { diagnoseExtensionLoad } from './extension-load-error.js';
4
+ import { escapeCypherString } from './cypher-escape.js';
5
+ import { diagnoseExtensionLoad, extractExtensionPath, } from './extension-load-error.js';
6
+ import { defaultVendorRoot, isUnsupportedFtsTuple, nodePlatformTuple, resolveFtsVersionPair, resolveVendoredFtsPath, } from './vendored-extension-path.js';
5
7
  import { logger } from '../logger.js';
6
8
  const DEFAULT_EXTENSION_INSTALL_TIMEOUT_MS = 15_000;
7
9
  const EXTENSION_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
@@ -112,12 +114,13 @@ export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutM
112
114
  /**
113
115
  * Centralized lifecycle manager for optional LadybugDB extensions.
114
116
  *
115
- * Always tries `LOAD EXTENSION <name>` first — it is per-connection,
116
- * idempotent, and never touches the network. If `LOAD` fails and the active
117
- * policy permits, the manager runs a single bounded out-of-process `INSTALL`
118
- * attempt per process and retries `LOAD`. Capability outcomes are cached so
119
- * unavailable extensions degrade search features without ever blocking
120
- * subsequent analyze or query calls.
117
+ * Tries `LOAD` first — it is per-connection, idempotent, and never
118
+ * touches the network. For FTS, a packaged vendored path is path-LOADed
119
+ * before the named `LOAD EXTENSION fts`. If `LOAD` fails and the active
120
+ * policy permits, the manager runs a single bounded out-of-process
121
+ * `INSTALL` attempt per process and retries `LOAD`. Capability outcomes
122
+ * are cached so unavailable extensions degrade search features without
123
+ * ever blocking subsequent analyze or query calls.
121
124
  *
122
125
  * Policy precedence (most specific wins):
123
126
  * per-call `opts.policy` → constructor `options.policy` → env → `load-only`
@@ -155,19 +158,46 @@ export class ExtensionManager {
155
158
  const timeoutMs = opts.installTimeoutMs ?? this.options.installTimeoutMs ?? getExtensionInstallTimeoutMs();
156
159
  const warn = this.options.warn ?? ((msg) => logger.warn(msg));
157
160
  const quiet = opts.quiet === true;
161
+ const attempts = [];
162
+ let lastInspectPath = null;
163
+ const versionsFor = (inspectPath) => name === 'fts' ? resolveFtsVersionPair(inspectPath, opts.vendorRoot) : undefined;
158
164
  if (policy === 'never') {
159
- this.markUnavailable(name, label, 'extension install policy is "never"', warn, quiet);
165
+ this.markUnavailable(name, label, 'extension install policy is "never"', warn, quiet, attempts, null);
160
166
  return false;
161
167
  }
168
+ if (name === 'fts') {
169
+ const tuple = opts.platformTuple ?? nodePlatformTuple();
170
+ const vendorRoot = opts.vendorRoot ?? defaultVendorRoot();
171
+ const vendored = resolveVendoredFtsPath({ vendorRoot, tuple });
172
+ if (vendored) {
173
+ attempts.push({ source: 'vendored', tuple });
174
+ const vendoredError = await this.tryLoadPath(query, vendored);
175
+ if (vendoredError === null) {
176
+ this.markLoaded(name, attempts);
177
+ return true;
178
+ }
179
+ lastInspectPath = vendored;
180
+ }
181
+ else if (isUnsupportedFtsTuple(tuple, vendorRoot)) {
182
+ attempts.push({ source: 'vendored', tuple });
183
+ this.markUnavailable(name, label, this.composeReason(`no packaged FTS artifact for ${tuple}`, attempts), warn, quiet, attempts, null);
184
+ return false;
185
+ }
186
+ }
187
+ attempts.push({ source: 'named' });
162
188
  const loadError = await this.tryLoad(query, name);
163
189
  if (loadError === null) {
164
- this.markLoaded(name);
190
+ this.markLoaded(name, attempts);
165
191
  return true;
166
192
  }
193
+ const namedPath = extractExtensionPath(loadError);
194
+ if (namedPath)
195
+ lastInspectPath = namedPath;
167
196
  if (policy === 'load-only') {
168
- this.markUnavailable(name, label, `load-only policy (no install attempted); LOAD ${name} failed: ${loadError}`, warn, quiet);
197
+ this.markUnavailable(name, label, this.composeReason(`load-only policy (no install attempted); LOAD ${name} failed: ${loadError}`, attempts), warn, quiet, attempts, lastInspectPath, versionsFor(lastInspectPath));
169
198
  return false;
170
199
  }
200
+ attempts.push({ source: 'install' });
171
201
  let install = this.installAttempted.get(name);
172
202
  if (!install) {
173
203
  const installFn = this.options.installExtension ?? installDuckDbExtensionOutOfProcess;
@@ -177,15 +207,15 @@ export class ExtensionManager {
177
207
  this.installAttempted.set(name, install);
178
208
  }
179
209
  if (!install.success) {
180
- this.markUnavailable(name, label, `${install.message}; LOAD ${name} had failed: ${loadError}`, warn, quiet);
210
+ this.markUnavailable(name, label, this.composeReason(`${install.message}; LOAD ${name} had failed: ${loadError}`, attempts), warn, quiet, attempts, lastInspectPath, versionsFor(lastInspectPath));
181
211
  return false;
182
212
  }
183
213
  const retryError = await this.tryLoad(query, name);
184
214
  if (retryError === null) {
185
- this.markLoaded(name);
215
+ this.markLoaded(name, attempts);
186
216
  return true;
187
217
  }
188
- this.markUnavailable(name, label, `LOAD ${name} failed after successful INSTALL: ${retryError}`, warn, quiet);
218
+ this.markUnavailable(name, label, this.composeReason(`LOAD ${name} failed after successful INSTALL: ${retryError}`, attempts), warn, quiet, attempts, extractExtensionPath(retryError), versionsFor(extractExtensionPath(retryError)));
189
219
  return false;
190
220
  }
191
221
  /**
@@ -206,17 +236,42 @@ export class ExtensionManager {
206
236
  return alreadyAvailable(msg) ? null : oneLine(msg);
207
237
  }
208
238
  }
209
- markLoaded(name) {
210
- this.capabilities.set(name, { name, loaded: true });
239
+ async tryLoadPath(query, absPath) {
240
+ try {
241
+ await query(`LOAD EXTENSION '${escapeCypherString(absPath)}'`);
242
+ return null;
243
+ }
244
+ catch (err) {
245
+ const msg = err instanceof Error ? err.message : String(err);
246
+ return alreadyAvailable(msg) ? null : oneLine(msg);
247
+ }
248
+ }
249
+ composeReason(base, attempts) {
250
+ if (!attempts.some((attempt) => attempt.source === 'vendored'))
251
+ return base;
252
+ const trail = attempts
253
+ .map((attempt) => attempt.source === 'vendored' ? `vendored ${attempt.tuple}` : attempt.source)
254
+ .join(', ');
255
+ return `${base} (attempts: ${trail})`;
256
+ }
257
+ markLoaded(name, attempts = []) {
258
+ const record = attempts.some((attempt) => attempt.source === 'vendored') ? attempts : undefined;
259
+ this.capabilities.set(name, {
260
+ name,
261
+ loaded: true,
262
+ ...(record ? { attempts: record } : {}),
263
+ });
211
264
  }
212
- markUnavailable(name, label, reason, warn, quiet = false) {
265
+ markUnavailable(name, label, reason, warn, quiet = false, attempts = [], inspectPath = null, versions) {
213
266
  // Classify once here (the single load-failure sink, run per Database not per
214
267
  // request) so the hot per-request warning path does no file I/O (#2383 F3).
268
+ // Diagnose the LAST attempt's file, never a path scraped from concatenated text.
215
269
  this.capabilities.set(name, {
216
270
  name,
217
271
  loaded: false,
218
272
  reason,
219
- diagnosis: diagnoseExtensionLoad(reason, label),
273
+ attempts,
274
+ diagnosis: diagnoseExtensionLoad(reason, label, inspectPath, versions),
220
275
  });
221
276
  const message = `GitNexus: ${label} extension unavailable; continuing without ${label} features. ${reason}`;
222
277
  // A quiet probe must not register the dedup key: the owning caller may hit
@@ -1,6 +1,6 @@
1
1
  import lbug from '@ladybugdb/core';
2
2
  import { KnowledgeGraph } from '../graph/types.js';
3
- import type { ContentRetention } from '../../storage/repo-meta.js';
3
+ import { type ContentRetention } from '../../storage/repo-meta.js';
4
4
  import { NodeTableName } from './schema.js';
5
5
  import type { GraphEmitManifest } from './graph-emit-sink.js';
6
6
  import type { PdgEmitManifest } from './pdg-emit-sink.js';
@@ -22,10 +22,9 @@ export declare const getDatabase: () => lbug.Database | null;
22
22
  /**
23
23
  * Return true when the error message indicates a write was attempted against
24
24
  * a read-only LadybugDB connection. The MCP query pool opens DBs read-only,
25
- * so any path that calls a `CREATE_*` procedure there will surface this
26
- * (e.g. defensive `ensureFTSIndex` calls). Owners of the writable analyze
27
- * path should ignore this error — index creation is owned by `gitnexus
28
- * analyze` and either already happened or will happen on the next run.
25
+ * so any path that calls a `CREATE_*` procedure there will surface this.
26
+ * Index creation is owned by `gitnexus analyze` and either already happened
27
+ * or will happen on the next run.
29
28
  */
30
29
  export declare const isReadOnlyDbError: (err: unknown) => boolean;
31
30
  /**
@@ -640,8 +639,8 @@ export declare const loadFTSExtension: (targetConn?: lbug.Connection, opts?: Ext
640
639
  export declare const loadVectorExtension: (targetConn?: lbug.Connection, opts?: ExtensionEnsureOptions) => Promise<boolean>;
641
640
  /**
642
641
  * Default stemmer for FTS indexes. Single source so the analyze path
643
- * (`getSearchFTSStemmer`) and the read-only `createFTSIndex`/`ensureFTSIndex`
644
- * defaults can never silently diverge.
642
+ * (`getSearchFTSStemmer`) and `createFTSIndex` defaults can never silently
643
+ * diverge.
645
644
  */
646
645
  export declare const DEFAULT_FTS_STEMMER = "porter";
647
646
  /**
@@ -848,24 +847,6 @@ export declare const ensureEmbeddingRowDmlSafe: (indexRows?: IndexCatalogSnapsho
848
847
  export declare const ensureFtsRowDmlSafe: (indexRows?: IndexCatalogSnapshot, options?: {
849
848
  skipFts?: boolean;
850
849
  }) => Promise<boolean>;
851
- /**
852
- * Lazy-create an FTS index, caching the fact in-process.
853
- *
854
- * Kept for writable maintenance paths that need to lazily materialize an
855
- * index. Read-only query paths must not call this; production analysis owns
856
- * creating the configured search indexes before the database is served.
857
- *
858
- * Safe to call repeatedly — the in-process Set guarantees only the first
859
- * call hits LadybugDB. `closeLbug` clears the cache so re-init starts fresh.
860
- *
861
- * Defense in depth: if the active connection is read-only (e.g. the MCP
862
- * pool adapter), `CREATE_FTS_INDEX` will fail with "Cannot execute write
863
- * operations in a read-only database". Treat that as a no-op and cache
864
- * the key so callers don't loop on a path that can never succeed here —
865
- * the index is owned by `gitnexus analyze` (writable) and either already
866
- * exists or will be created on the next analyze.
867
- */
868
- export declare const ensureFTSIndex: (tableName: string, indexName: string, properties: string[], stemmer?: string) => Promise<void>;
869
850
  export type FtsQueryFailureClass = 'missing-index' | 'missing-table' | 'other';
870
851
  /**
871
852
  * Classify a `QUERY_FTS_INDEX` failure so a genuinely-missing index (normal —