gitnexus 1.6.4-rc.93 → 1.6.4-rc.94

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.
@@ -90,7 +90,7 @@ export async function augment(pattern, cwd) {
90
90
  await initLbug(repoId, repo.lbugPath);
91
91
  }
92
92
  // Step 1: BM25 search (fast, no embeddings)
93
- const bm25Results = await searchFTSFromLbug(pattern, 10, repoId);
93
+ const { results: bm25Results } = await searchFTSFromLbug(pattern, 10, repoId);
94
94
  if (bm25Results.length === 0)
95
95
  return '';
96
96
  // Step 2: Map BM25 file results to symbols
@@ -10,6 +10,11 @@ export interface BM25SearchResult {
10
10
  rank: number;
11
11
  nodeIds?: string[];
12
12
  }
13
+ export interface FTSSearchResponse {
14
+ results: BM25SearchResult[];
15
+ /** True when at least one FTS index query succeeded (index exists). */
16
+ ftsAvailable: boolean;
17
+ }
13
18
  /**
14
19
  * Search using LadybugDB's built-in FTS (always fresh, reads from disk)
15
20
  *
@@ -21,4 +26,4 @@ export interface BM25SearchResult {
21
26
  * @param repoId - If provided, queries will be routed via the MCP connection pool
22
27
  * @returns Ranked search results from FTS indexes
23
28
  */
24
- export declare const searchFTSFromLbug: (query: string, limit?: number, repoId?: string) => Promise<BM25SearchResult[]>;
29
+ export declare const searchFTSFromLbug: (query: string, limit?: number, repoId?: string) => Promise<FTSSearchResponse>;
@@ -8,7 +8,8 @@ import { queryFTS } from '../lbug/lbug-adapter.js';
8
8
  import { FTS_INDEXES } from './fts-schema.js';
9
9
  /**
10
10
  * Execute a single FTS query via a custom executor (for MCP connection pool).
11
- * Returns the same shape as core queryFTS (from LadybugDB adapter).
11
+ * Returns `null` when the query fails (e.g. FTS index does not exist) so the
12
+ * caller can distinguish "zero matches" from "index missing".
12
13
  */
13
14
  async function queryFTSViaExecutor(executor, tableName, indexName, query, limit) {
14
15
  // Escape single quotes and backslashes to prevent Cypher injection
@@ -32,7 +33,7 @@ async function queryFTSViaExecutor(executor, tableName, indexName, query, limit)
32
33
  });
33
34
  }
34
35
  catch {
35
- return [];
36
+ return null;
36
37
  }
37
38
  }
38
39
  /**
@@ -48,6 +49,7 @@ async function queryFTSViaExecutor(executor, tableName, indexName, query, limit)
48
49
  */
49
50
  export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
50
51
  const resultsByIndex = [];
52
+ let queriesSucceeded = 0;
51
53
  if (repoId) {
52
54
  // Use MCP connection pool via dynamic import
53
55
  // IMPORTANT: FTS queries run sequentially to avoid connection contention.
@@ -56,15 +58,27 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
56
58
  const { executeQuery } = poolMod;
57
59
  const executor = (cypher) => executeQuery(repoId, cypher);
58
60
  for (const { table, indexName } of FTS_INDEXES) {
59
- resultsByIndex.push(await queryFTSViaExecutor(executor, table, indexName, query, limit));
61
+ const result = await queryFTSViaExecutor(executor, table, indexName, query, limit);
62
+ if (result !== null) {
63
+ queriesSucceeded++;
64
+ resultsByIndex.push(result);
65
+ }
60
66
  }
61
67
  }
62
68
  else {
63
69
  // Use core lbug adapter (CLI / pipeline context) — also sequential for safety.
64
70
  for (const { table, indexName } of FTS_INDEXES) {
65
- resultsByIndex.push(await queryFTS(table, indexName, query, limit, false).catch(() => []));
71
+ try {
72
+ const result = await queryFTS(table, indexName, query, limit, false);
73
+ queriesSucceeded++;
74
+ resultsByIndex.push(result);
75
+ }
76
+ catch {
77
+ // FTS index may not exist — count as failed
78
+ }
66
79
  }
67
80
  }
81
+ const ftsAvailable = queriesSucceeded > 0;
68
82
  // Collect all node scores per filePath to track which nodes actually matched
69
83
  const fileNodeScores = new Map();
70
84
  const addResults = (results) => {
@@ -92,10 +106,13 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
92
106
  const sorted = Array.from(merged.values())
93
107
  .sort((a, b) => b.score - a.score)
94
108
  .slice(0, limit);
95
- return sorted.map((r, index) => ({
96
- filePath: r.filePath,
97
- score: r.score,
98
- rank: index + 1,
99
- nodeIds: r.nodeIds,
100
- }));
109
+ return {
110
+ results: sorted.map((r, index) => ({
111
+ filePath: r.filePath,
112
+ score: r.score,
113
+ rank: index + 1,
114
+ nodeIds: r.nodeIds,
115
+ })),
116
+ ftsAvailable,
117
+ };
101
118
  };
@@ -32,9 +32,10 @@ export interface HybridSearchResult {
32
32
  */
33
33
  export declare const mergeWithRRF: (bm25Results: BM25SearchResult[], semanticResults: SemanticSearchResult[], limit?: number) => HybridSearchResult[];
34
34
  /**
35
- * Check if hybrid search is available
36
- * LadybugDB FTS is always available once the database is initialized.
37
- * Semantic search is optional - hybrid works with just FTS if embeddings aren't ready.
35
+ * Check if hybrid search is available.
36
+ * FTS indexes may be missing on read-only MCP connections (see #1403);
37
+ * callers should inspect `ftsAvailable` from searchFTSFromLbug for
38
+ * per-query availability. This helper is a coarse gate only.
38
39
  */
39
40
  export declare const isHybridSearchReady: () => boolean;
40
41
  /**
@@ -79,12 +79,13 @@ export const mergeWithRRF = (bm25Results, semanticResults, limit = 10) => {
79
79
  return sorted;
80
80
  };
81
81
  /**
82
- * Check if hybrid search is available
83
- * LadybugDB FTS is always available once the database is initialized.
84
- * Semantic search is optional - hybrid works with just FTS if embeddings aren't ready.
82
+ * Check if hybrid search is available.
83
+ * FTS indexes may be missing on read-only MCP connections (see #1403);
84
+ * callers should inspect `ftsAvailable` from searchFTSFromLbug for
85
+ * per-query availability. This helper is a coarse gate only.
85
86
  */
86
87
  export const isHybridSearchReady = () => {
87
- return true; // FTS is always available via LadybugDB when DB is open
88
+ return true; // FTS is attempted on every query; ftsAvailable signals actual availability
88
89
  };
89
90
  /**
90
91
  * Format hybrid results for LLM consumption
@@ -112,7 +113,7 @@ export const formatHybridResults = (results) => {
112
113
  */
113
114
  export const hybridSearch = async (query, limit, executeQuery, semanticSearch) => {
114
115
  // Use LadybugDB FTS for always-fresh BM25 results
115
- const bm25Results = await searchFTSFromLbug(query, limit);
116
+ const { results: bm25Results } = await searchFTSFromLbug(query, limit);
116
117
  const semanticResults = await semanticSearch(executeQuery, query, limit);
117
118
  return mergeWithRRF(bm25Results, semanticResults, limit);
118
119
  };
@@ -818,7 +818,7 @@ export class LocalBackend {
818
818
  definitions: definitions.slice(0, 20), // cap standalone definitions
819
819
  timing,
820
820
  ...(!ftsUsed && {
821
- warning: 'FTS extension unavailable - keyword search degraded. Run: gitnexus analyze --force to rebuild indexes.',
821
+ warning: 'FTS indexes missing keyword search degraded. Run: gitnexus analyze --force to rebuild indexes.',
822
822
  }),
823
823
  };
824
824
  }
@@ -827,15 +827,16 @@ export class LocalBackend {
827
827
  */
828
828
  async bm25Search(repo, query, limit) {
829
829
  const { searchFTSFromLbug } = await import('../../core/search/bm25-index.js');
830
- let bm25Results;
830
+ let ftsResponse;
831
831
  try {
832
- bm25Results = await searchFTSFromLbug(query, limit, repo.id);
832
+ ftsResponse = await searchFTSFromLbug(query, limit, repo.id);
833
833
  }
834
834
  catch (err) {
835
835
  logger.error({ err: err.message }, 'GitNexus: BM25/FTS search failed (FTS indexes may not exist) -');
836
836
  return { results: [], ftsUsed: false };
837
837
  }
838
- const ftsUsed = bm25Results.length === 0 || bm25Results[0]?.ftsUsed !== false;
838
+ const bm25Results = ftsResponse.results;
839
+ const ftsUsed = ftsResponse.ftsAvailable;
839
840
  const results = [];
840
841
  for (const bm25Result of bm25Results) {
841
842
  const fullPath = bm25Result.filePath;
@@ -932,10 +932,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
932
932
  const enrich = req.body.enrich !== false; // default true
933
933
  const results = await withLbugDb(lbugPath, async () => {
934
934
  let searchResults;
935
+ let ftsAvailable;
935
936
  if (mode === 'semantic') {
936
937
  const { isEmbedderReady } = await import('../core/embeddings/embedder.js');
937
938
  if (!isEmbedderReady()) {
938
- return [];
939
+ return { searchResults: [], ftsAvailable: undefined };
939
940
  }
940
941
  const { semanticSearch: semSearch } = await import('../core/embeddings/embedding-pipeline.js');
941
942
  searchResults = await semSearch(executeQuery, query, limit);
@@ -948,8 +949,9 @@ export const createServer = async (port, host = '127.0.0.1') => {
948
949
  }));
949
950
  }
950
951
  else if (mode === 'bm25') {
951
- searchResults = await searchFTSFromLbug(query, limit);
952
- searchResults = searchResults.map((r, i) => ({
952
+ const ftsResponse = await searchFTSFromLbug(query, limit);
953
+ ftsAvailable = ftsResponse.ftsAvailable;
954
+ searchResults = ftsResponse.results.map((r, i) => ({
953
955
  ...r,
954
956
  rank: i + 1,
955
957
  sources: ['bm25'],
@@ -963,11 +965,13 @@ export const createServer = async (port, host = '127.0.0.1') => {
963
965
  searchResults = await hybridSearch(query, limit, executeQuery, semSearch);
964
966
  }
965
967
  else {
966
- searchResults = await searchFTSFromLbug(query, limit);
968
+ const ftsResponse = await searchFTSFromLbug(query, limit);
969
+ ftsAvailable = ftsResponse.ftsAvailable;
970
+ searchResults = ftsResponse.results;
967
971
  }
968
972
  }
969
973
  if (!enrich)
970
- return searchResults;
974
+ return { searchResults, ftsAvailable };
971
975
  // Server-side enrichment: add connections, cluster, processes per result
972
976
  // Uses parameterized queries to prevent Cypher injection via nodeId
973
977
  const validLabel = (label) => NODE_TABLES.includes(label);
@@ -1029,9 +1033,14 @@ export const createServer = async (port, host = '127.0.0.1') => {
1029
1033
  }
1030
1034
  return { ...r, ...enrichment };
1031
1035
  }));
1032
- return enriched;
1036
+ return { searchResults: enriched, ftsAvailable };
1033
1037
  });
1034
- res.json({ results });
1038
+ const response = { results: results.searchResults ?? results };
1039
+ if (results.ftsAvailable === false) {
1040
+ response.warning =
1041
+ 'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --force to rebuild indexes.';
1042
+ }
1043
+ res.json(response);
1035
1044
  }
1036
1045
  catch (err) {
1037
1046
  res.status(500).json({ error: err.message || 'Search failed' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.4-rc.93",
3
+ "version": "1.6.4-rc.94",
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",