gitnexus 1.6.5-rc.22 → 1.6.5-rc.23

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.
@@ -46,5 +46,8 @@ export declare const formatHybridResults: (results: HybridSearchResult[]) => str
46
46
  * Execute BM25 + semantic search and merge with RRF.
47
47
  * Uses LadybugDB FTS for always-fresh BM25 results (no cached data).
48
48
  * The semanticSearch function is injected to keep this module environment-agnostic.
49
+ *
50
+ * When FTS is unavailable (e.g. read-only MCP connection, missing indexes),
51
+ * falls back to semantic-only results instead of crashing (#1489).
49
52
  */
50
53
  export declare const hybridSearch: (query: string, limit: number, executeQuery: (cypher: string) => Promise<any[]>, semanticSearch: (executeQuery: (cypher: string) => Promise<any[]>, query: string, k?: number) => Promise<SemanticSearchResult[]>) => Promise<HybridSearchResult[]>;
@@ -23,9 +23,14 @@ const RRF_K = 60;
23
23
  */
24
24
  export const mergeWithRRF = (bm25Results, semanticResults, limit = 10) => {
25
25
  const merged = new Map();
26
+ // Guard against undefined/null inputs (#1489) — when FTS is unavailable
27
+ // in the MCP process, bm25Results can arrive as undefined and the
28
+ // for-loop would throw "bm25Results is not iterable".
29
+ const safeBm25 = bm25Results ?? [];
30
+ const safeSemantic = semanticResults ?? [];
26
31
  // Process BM25 results
27
- for (let i = 0; i < bm25Results.length; i++) {
28
- const r = bm25Results[i];
32
+ for (let i = 0; i < safeBm25.length; i++) {
33
+ const r = safeBm25[i];
29
34
  const rrfScore = 1 / (RRF_K + i + 1); // i+1 because rank starts at 1
30
35
  merged.set(r.filePath, {
31
36
  filePath: r.filePath,
@@ -36,8 +41,8 @@ export const mergeWithRRF = (bm25Results, semanticResults, limit = 10) => {
36
41
  });
37
42
  }
38
43
  // Process semantic results and merge
39
- for (let i = 0; i < semanticResults.length; i++) {
40
- const r = semanticResults[i];
44
+ for (let i = 0; i < safeSemantic.length; i++) {
45
+ const r = safeSemantic[i];
41
46
  const rrfScore = 1 / (RRF_K + i + 1);
42
47
  const existing = merged.get(r.filePath);
43
48
  if (existing) {
@@ -110,10 +115,22 @@ export const formatHybridResults = (results) => {
110
115
  * Execute BM25 + semantic search and merge with RRF.
111
116
  * Uses LadybugDB FTS for always-fresh BM25 results (no cached data).
112
117
  * The semanticSearch function is injected to keep this module environment-agnostic.
118
+ *
119
+ * When FTS is unavailable (e.g. read-only MCP connection, missing indexes),
120
+ * falls back to semantic-only results instead of crashing (#1489).
113
121
  */
114
122
  export const hybridSearch = async (query, limit, executeQuery, semanticSearch) => {
115
- // Use LadybugDB FTS for always-fresh BM25 results
116
- const { results: bm25Results } = await searchFTSFromLbug(query, limit);
123
+ // Use LadybugDB FTS for always-fresh BM25 results.
124
+ // If FTS fails (e.g. extension not loaded in MCP process), fall back to
125
+ // semantic-only search instead of crashing with "bm25Results is not iterable".
126
+ let bm25Results = [];
127
+ try {
128
+ const ftsResponse = await searchFTSFromLbug(query, limit);
129
+ bm25Results = ftsResponse?.results ?? [];
130
+ }
131
+ catch {
132
+ // FTS unavailable — continue with semantic-only search
133
+ }
117
134
  const semanticResults = await semanticSearch(executeQuery, query, limit);
118
135
  return mergeWithRRF(bm25Results, semanticResults, limit);
119
136
  };
@@ -634,8 +634,10 @@ export class LocalBackend {
634
634
  timer.time('bm25', this.bm25Search(repo, searchQuery, searchLimit)),
635
635
  timer.time('vector', this.semanticSearch(repo, searchQuery, searchLimit)),
636
636
  ]);
637
- const bm25Results = bm25SearchResult.results;
638
- const ftsUsed = bm25SearchResult.ftsUsed;
637
+ // Guard against undefined results (#1489) — when FTS is entirely
638
+ // unavailable the search helper may return an unexpected shape.
639
+ const bm25Results = bm25SearchResult?.results ?? [];
640
+ const ftsUsed = bm25SearchResult?.ftsUsed ?? false;
639
641
  // Merge via reciprocal rank fusion
640
642
  timer.start('merge');
641
643
  const scoreMap = new Map();
@@ -651,8 +653,9 @@ export class LocalBackend {
651
653
  scoreMap.set(key, { score: rrfScore, data: result });
652
654
  }
653
655
  }
654
- for (let i = 0; i < semanticResults.length; i++) {
655
- const result = semanticResults[i];
656
+ const safeSemanticResults = semanticResults ?? [];
657
+ for (let i = 0; i < safeSemanticResults.length; i++) {
658
+ const result = safeSemanticResults[i];
656
659
  const key = result.nodeId || result.filePath;
657
660
  const rrfScore = 1 / (60 + i);
658
661
  const existing = scoreMap.get(key);
@@ -826,7 +829,15 @@ export class LocalBackend {
826
829
  * BM25 keyword search helper - uses LadybugDB FTS for always-fresh results
827
830
  */
828
831
  async bm25Search(repo, query, limit) {
829
- const { searchFTSFromLbug } = await import('../../core/search/bm25-index.js');
832
+ let searchFTSFromLbug;
833
+ try {
834
+ ({ searchFTSFromLbug } = await import('../../core/search/bm25-index.js'));
835
+ }
836
+ catch (err) {
837
+ // Module import can fail in sandboxed MCP contexts (#1489)
838
+ logger.warn({ err: err?.message }, 'GitNexus: bm25-index.js import failed — falling back to semantic-only');
839
+ return { results: [], ftsUsed: false };
840
+ }
830
841
  let ftsResponse;
831
842
  try {
832
843
  ftsResponse = await searchFTSFromLbug(query, limit, repo.id);
@@ -835,8 +846,10 @@ export class LocalBackend {
835
846
  logger.error({ err: err.message }, 'GitNexus: BM25/FTS search failed (FTS indexes may not exist) -');
836
847
  return { results: [], ftsUsed: false };
837
848
  }
838
- const bm25Results = ftsResponse.results;
839
- const ftsUsed = ftsResponse.ftsAvailable;
849
+ // Guard against unexpected response shape (#1489) — ftsResponse.results
850
+ // could be undefined when the FTS extension is unavailable in the MCP process.
851
+ const bm25Results = ftsResponse?.results ?? [];
852
+ const ftsUsed = ftsResponse?.ftsAvailable ?? false;
840
853
  const results = [];
841
854
  for (const bm25Result of bm25Results) {
842
855
  const fullPath = bm25Result.filePath;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.5-rc.22",
3
+ "version": "1.6.5-rc.23",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",