scai 0.1.163 → 0.1.164

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 (2) hide show
  1. package/dist/db/fileIndex.js +128 -126
  2. package/package.json +1 -1
@@ -100,54 +100,143 @@ export function queryFiles(safeQuery, limit = 10) {
100
100
  // - Uses LLM aggressively
101
101
  // - Optimizes for precision
102
102
  // --------------------------------------------------
103
- export async function semanticSearchFiles(originalQuery, query, topK = 5) {
103
+ export async function semanticSearchFiles(originalQuery, _query, // ignored now – LLM owns query construction
104
+ topK = 5) {
104
105
  const db = getDbForRepo();
105
- const seen = new Map();
106
- // -----------------------------
107
- // Primary FTS
108
- // -----------------------------
109
- const safeQuery = sanitizeQueryForFts(query);
106
+ // --------------------------------------------------
107
+ // 1. LLM → primary FTS query (always)
108
+ // --------------------------------------------------
109
+ const primaryFtsQuery = await generatePrimaryFtsQuery(originalQuery);
110
+ logInputOutput("semanticSearchFiles LLM primary query", "output", {
111
+ originalQuery,
112
+ ftsQuery: primaryFtsQuery,
113
+ });
114
+ // --------------------------------------------------
115
+ // 2. Run primary FTS once
116
+ // --------------------------------------------------
110
117
  const primaryResults = db
111
118
  .prepare(sqlTemplates.searchFilesTemplate)
112
- .all(safeQuery, RELATED_FILES_LIMIT);
113
- logInputOutput("semanticSearchFiles primary FTS", "input", {
114
- safeQuery,
115
- count: primaryResults.length,
116
- });
117
- if (primaryResults.length === 0) {
118
- // No FTS at all → blind expansion
119
- return semanticFallbackSearch(originalQuery, topK);
119
+ .all(primaryFtsQuery, RELATED_FILES_LIMIT);
120
+ if (primaryResults.length > 0) {
121
+ return rankAndMap(new Map(primaryResults.map(r => [r.id, r])), topK);
120
122
  }
121
- // -----------------------------
122
- // Relevance check (KEY STEP)
123
- // -----------------------------
124
- const relevance = await checkFtsRelevanceWithModel(originalQuery, primaryResults);
125
- logInputOutput("semanticSearchFiles relevance check", "output", relevance);
126
- // -----------------------------
127
- // If relevant → trust FTS
128
- // -----------------------------
129
- if (relevance.relevant) {
130
- primaryResults.forEach(r => seen.set(r.id, r));
131
- return rankAndMap(seen, topK);
132
- }
133
- // -----------------------------
134
- // Not relevant → model-guided expansion
135
- // -----------------------------
136
- for (const term of relevance.suggestedTerms) {
137
- const safeTerm = sanitizeQueryForFts(term);
123
+ // --------------------------------------------------
124
+ // 3. Fallback: LLM → 2–3 subqueries (ONLY if zero results)
125
+ // --------------------------------------------------
126
+ const subQueries = await generateFallbackFtsQueries(originalQuery, primaryFtsQuery);
127
+ logInputOutput("semanticSearchFiles LLM fallback queries", "output", {
128
+ originalQuery,
129
+ primaryFtsQuery,
130
+ subQueries,
131
+ });
132
+ // --------------------------------------------------
133
+ // 4. Execute fallback queries sequentially
134
+ // --------------------------------------------------
135
+ for (const subQuery of subQueries) {
138
136
  const rows = db
139
137
  .prepare(sqlTemplates.searchFilesTemplate)
140
- .all(safeTerm, RELATED_FILES_LIMIT);
141
- for (const row of rows) {
142
- if (!seen.has(row.id))
143
- seen.set(row.id, row);
138
+ .all(subQuery, RELATED_FILES_LIMIT);
139
+ if (rows.length > 0) {
140
+ return rankAndMap(new Map(rows.map(r => [r.id, r])), topK);
141
+ }
142
+ }
143
+ // --------------------------------------------------
144
+ // 5. Hard stop
145
+ // --------------------------------------------------
146
+ return [];
147
+ }
148
+ async function generatePrimaryFtsQuery(userQuery) {
149
+ const prompt = `
150
+ You are generating a SQLite FTS query for searching a source code repository.
151
+
152
+ Input (natural language):
153
+ "${userQuery}"
154
+
155
+ Task:
156
+ - Produce ONE concise FTS query
157
+ - Focus on filenames, symbols, module names, domain nouns
158
+ - Prefer literal identifiers likely to exist in code
159
+ - NO sentences
160
+ - NO stopwords
161
+ - NO explanations
162
+ - NO wildcards unless absolutely necessary
163
+ - Use OR between terms
164
+ - **MAX 10 terms only** — be selective and concise
165
+
166
+ Output JSON ONLY:
167
+ {
168
+ "ftsQuery": "term1 OR term2 OR term3"
169
+ }
170
+ `.trim();
171
+ try {
172
+ const response = await generate({ content: prompt, query: "" });
173
+ const cleaned = await cleanupModule.run({
174
+ query: userQuery,
175
+ content: response.data,
176
+ });
177
+ if (cleaned.data &&
178
+ typeof cleaned.data === "object" &&
179
+ "ftsQuery" in cleaned.data &&
180
+ typeof cleaned.data.ftsQuery === "string") {
181
+ return cleaned.data.ftsQuery;
144
182
  }
145
183
  }
146
- // Safety net: never return empty if we had FTS
147
- if (seen.size === 0) {
148
- primaryResults.forEach(r => seen.set(r.id, r));
184
+ catch (err) {
185
+ log(`⚠️ [semanticSearchFiles] Failed to generate primary FTS query: ${String(err)}`);
149
186
  }
150
- return rankAndMap(seen, topK);
187
+ // Absolute safety fallback — never explode
188
+ return sanitizeQueryForFts(userQuery);
189
+ }
190
+ async function generateFallbackFtsQueries(userQuery, failedQuery) {
191
+ const prompt = `
192
+ You are generating fallback SQLite FTS queries for a source code search.
193
+
194
+ Original user query:
195
+ "${userQuery}"
196
+
197
+ Primary FTS query returned ZERO results:
198
+ "${failedQuery}"
199
+
200
+ Task:
201
+ - Generate 2–3 independent FTS queries (MAX 3)
202
+ - Each query should be concise: no more than 10 OR-joined search terms
203
+ - Focus on filenames, symbols, module names
204
+ - Avoid natural-language sentences
205
+ - Avoid recursion or refinement loops
206
+ - Use OR between terms
207
+
208
+ Output JSON ONLY:
209
+ {
210
+ "subQueries": [
211
+ "query1",
212
+ "query2",
213
+ "query3"
214
+ ]
215
+ }
216
+ `.trim();
217
+ try {
218
+ const response = await generate({ content: prompt, query: "" });
219
+ const cleaned = await cleanupModule.run({
220
+ query: userQuery,
221
+ content: response.data,
222
+ });
223
+ if (cleaned.data &&
224
+ typeof cleaned.data === "object" &&
225
+ Array.isArray(cleaned.data.subQueries)) {
226
+ return cleaned.data.subQueries
227
+ .filter((q) => typeof q === "string")
228
+ .slice(0, 3) // cap to 3 queries
229
+ .map((q) => q
230
+ .split(' OR ')
231
+ .map(term => sanitizeQueryForFts(term)) // sanitize each term individually
232
+ .slice(0, 10) // cap terms per query
233
+ .join(' OR '));
234
+ }
235
+ }
236
+ catch (err) {
237
+ log(`⚠️ [semanticSearchFiles] Failed to generate fallback queries: ${String(err)}`);
238
+ }
239
+ return [];
151
240
  }
152
241
  // --------------------------------------------------
153
242
  // PLANNER SEARCH (fileSearchModule, discovery)
@@ -209,71 +298,6 @@ function rankAndMap(seen, topK) {
209
298
  bm25Score: r.bm25Score,
210
299
  }));
211
300
  }
212
- /**
213
- * Ask the model whether the initial FTS hits are relevant to the query.
214
- * If not, suggest new search terms for another FTS pass.
215
- */
216
- async function checkFtsRelevanceWithModel(query, ftsResults) {
217
- if (ftsResults.length === 0) {
218
- return { relevant: false, suggestedTerms: [] };
219
- }
220
- const prompt = `
221
- You are assisting a code search system that uses full-text search (FTS)
222
- over source code.
223
-
224
- Query (natural language):
225
- "${query}"
226
-
227
- Initial FTS results (filenames and summaries):
228
- ${JSON.stringify(ftsResults)}
229
-
230
- Task:
231
- 1. Decide whether these results are relevant to the query.
232
- 2. If they are NOT relevant, suggest alternative search terms.
233
-
234
- IMPORTANT RULES FOR SUGGESTED TERMS:
235
- - Terms MUST be likely to appear literally in source code.
236
- - Prefer: filenames, module names, function names, variables, symbols, config keys.
237
- - Use short identifiers (1–3 words max).
238
- - Avoid natural-language phrases or explanations.
239
- - Avoid conceptual or architectural descriptions.
240
- - Examples of GOOD terms: "api", "router", "frontend", "backend", "client", "server", "routes", "config.ts"
241
- - Examples of BAD terms: "frontend backend separation", "code architecture", "business logic"
242
-
243
- Output format:
244
- - If relevant:
245
- { "relevant": true, "suggestedTerms": [] }
246
- - If not relevant:
247
- { "relevant": false, "suggestedTerms": ["term1", "term2", "term3"] }
248
-
249
- Return ONLY valid JSON.
250
- `.trim();
251
- try {
252
- const response = await generate({ content: prompt, query: "" });
253
- const cleaned = await cleanupModule.run({
254
- query,
255
- content: response.data,
256
- });
257
- const data = cleaned.data;
258
- // Type guard: ensure it's an object with correct properties
259
- if (data &&
260
- typeof data === "object" &&
261
- "relevant" in data &&
262
- "suggestedTerms" in data &&
263
- typeof data.relevant === "boolean" &&
264
- Array.isArray(data.suggestedTerms)) {
265
- const relevant = data.relevant;
266
- const suggestedTerms = data.suggestedTerms.filter((t) => typeof t === "string");
267
- return { relevant, suggestedTerms };
268
- }
269
- return { relevant: false, suggestedTerms: [] };
270
- }
271
- catch (err) {
272
- log(`⚠️ [searchFiles] Failed to check FTS relevance: ${String(err)}`);
273
- return { relevant: false, suggestedTerms: [] };
274
- }
275
- }
276
- ;
277
301
  async function expandQueryWithModel(query) {
278
302
  const prompt = `
279
303
  You are assisting a code search system.
@@ -305,25 +329,3 @@ Question:
305
329
  return [];
306
330
  }
307
331
  }
308
- async function semanticFallbackSearch(query, topK) {
309
- const db = getDbForRepo();
310
- const seen = new Map();
311
- const llmTerms = await expandQueryWithModel(query);
312
- logInputOutput("semanticSearchFiles blind expansion", "output", {
313
- query,
314
- suggestedTerms: llmTerms,
315
- });
316
- for (const term of llmTerms) {
317
- const safeTerm = sanitizeQueryForFts(term);
318
- const rows = db
319
- .prepare(sqlTemplates.searchFilesTemplate)
320
- .all(safeTerm, RELATED_FILES_LIMIT);
321
- for (const row of rows) {
322
- if (!seen.has(row.id))
323
- seen.set(row.id, row);
324
- }
325
- }
326
- if (seen.size === 0)
327
- return [];
328
- return rankAndMap(seen, topK);
329
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.163",
3
+ "version": "0.1.164",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"