codesearch-mcp 0.2.0-alpha → 0.2.1-alpha

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.
@@ -6,6 +6,7 @@ import * as schema from '../db/schema';
6
6
  type Schema = typeof schema;
7
7
  export type DrizzleDb = NodePgDatabase<Schema>;
8
8
  export type DrizzleTx = NodePgTransaction<Schema, ExtractTablesWithRelations<Schema>>;
9
+ export declare const CURRENT_SYMBOL_INDEX_VERSION = 2;
9
10
  export declare class DatabaseService implements IDatabase {
10
11
  private config;
11
12
  private pool;
@@ -31,8 +32,20 @@ export declare class DatabaseService implements IDatabase {
31
32
  deleteFiles(project: string, filePaths: string[]): Promise<void>;
32
33
  listIndexedProjects(): Promise<string[]>;
33
34
  deleteProject(name: string): Promise<void>;
35
+ listProjectsWithIndexRows(): Promise<string[]>;
36
+ /**
37
+ * Among `projectNames` (or every project with rows when null), those with at
38
+ * least one row in `code_chunk`/`code_symbol` and a `symbol_index_version`
39
+ * below `CURRENT_SYMBOL_INDEX_VERSION` (a missing row counts as version 1).
40
+ * A missing `symbol_index_version` table (un-migrated DB, `rag-mcp`/`rag-web`
41
+ * bootstrapped with `initDb`) counts as "no marker rows" — every project with
42
+ * rows is then outdated. Any other database error propagates.
43
+ */
44
+ listOutdatedProjects(projectNames: string[] | null): Promise<string[]>;
45
+ upsertSymbolIndexVersion(project: string): Promise<void>;
34
46
  /** Guard against SQL injection — the dimension is interpolated into DDL. */
35
47
  private assertDim;
48
+ private assertMinPostgresVersion;
36
49
  private ensureDatabaseExists;
37
50
  }
38
51
  export {};
@@ -33,12 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DatabaseService = void 0;
36
+ exports.DatabaseService = exports.CURRENT_SYMBOL_INDEX_VERSION = void 0;
37
37
  const node_postgres_1 = require("drizzle-orm/node-postgres");
38
38
  const pg_1 = require("pg");
39
39
  const drizzle_orm_1 = require("drizzle-orm");
40
40
  const schema = __importStar(require("../db/schema"));
41
41
  const db_1 = require("../db");
42
+ const MIN_PG_VERSION_NUM = 150000;
43
+ exports.CURRENT_SYMBOL_INDEX_VERSION = 2;
42
44
  class DatabaseService {
43
45
  config;
44
46
  pool;
@@ -68,6 +70,12 @@ class DatabaseService {
68
70
  async initDbWithSchema() {
69
71
  await this.ensureDatabaseExists();
70
72
  await this.initDb();
73
+ // Read as the very first statement on the application database, before any
74
+ // CREATE TABLE / CREATE INDEX / ALTER TABLE / DROP — `NULLS NOT DISTINCT`
75
+ // (used below) requires PostgreSQL 15+, and the DROP/CREATE pair must never
76
+ // run against an older server (it would fail after the DROP and leave
77
+ // code_symbol without a unique index).
78
+ await this.assertMinPostgresVersion();
71
79
  // code_chunk table — created at the legacy default width (1024) so search
72
80
  // tools always have a table. IndexerService recreates it at the resolved
73
81
  // width before indexing when the active model's dimension differs.
@@ -130,7 +138,23 @@ class DatabaseService {
130
138
  }
131
139
  await this.query(`CREATE INDEX IF NOT EXISTS idx_symbol_name ON code_symbol (name, project)`);
132
140
  await this.query(`CREATE INDEX IF NOT EXISTS idx_symbol_file ON code_symbol (project, "filePath")`);
133
- await this.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_symbol_unique ON code_symbol (project, "filePath", name, kind)`);
141
+ await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS container TEXT`);
142
+ await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS "isStatic" BOOLEAN NOT NULL DEFAULT false`);
143
+ // DDL is transactional in PostgreSQL — either both statements apply or
144
+ // neither does, so a failure here never leaves code_symbol without a
145
+ // unique index (the old idx_symbol_unique is dropped only once the new
146
+ // one is confirmed to exist).
147
+ await this.transaction(async (tx) => {
148
+ await tx.execute('DROP INDEX IF EXISTS idx_symbol_unique');
149
+ await tx.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_symbol_unique_v2 ' +
150
+ 'ON code_symbol (project, "filePath", container, name, kind, line) NULLS NOT DISTINCT');
151
+ });
152
+ await this.query(`
153
+ CREATE TABLE IF NOT EXISTS symbol_index_version (
154
+ project TEXT PRIMARY KEY,
155
+ version INT NOT NULL
156
+ )
157
+ `);
134
158
  // code_call table
135
159
  await this.query(`
136
160
  CREATE TABLE IF NOT EXISTS code_call (
@@ -244,6 +268,50 @@ class DatabaseService {
244
268
  await tx.delete(db_1.codeDependency).where((0, drizzle_orm_1.eq)(db_1.codeDependency.project, name));
245
269
  });
246
270
  }
271
+ async listProjectsWithIndexRows() {
272
+ const rows = await this.query(`SELECT DISTINCT name FROM (
273
+ SELECT "serviceName" AS name FROM code_chunk
274
+ UNION
275
+ SELECT project AS name FROM code_symbol
276
+ ) t`);
277
+ return rows.map(r => r.name);
278
+ }
279
+ /**
280
+ * Among `projectNames` (or every project with rows when null), those with at
281
+ * least one row in `code_chunk`/`code_symbol` and a `symbol_index_version`
282
+ * below `CURRENT_SYMBOL_INDEX_VERSION` (a missing row counts as version 1).
283
+ * A missing `symbol_index_version` table (un-migrated DB, `rag-mcp`/`rag-web`
284
+ * bootstrapped with `initDb`) counts as "no marker rows" — every project with
285
+ * rows is then outdated. Any other database error propagates.
286
+ */
287
+ async listOutdatedProjects(projectNames) {
288
+ const rowsUnion = `
289
+ SELECT "serviceName" AS name FROM code_chunk
290
+ UNION
291
+ SELECT project AS name FROM code_symbol
292
+ `;
293
+ const filterSql = projectNames ? 'AND t.name = ANY($1)' : '';
294
+ const params = projectNames ? [projectNames] : [];
295
+ try {
296
+ const rows = await this.query(`SELECT DISTINCT t.name FROM (${rowsUnion}) t
297
+ WHERE NOT EXISTS (
298
+ SELECT 1 FROM symbol_index_version v
299
+ WHERE v.project = t.name AND v.version >= ${exports.CURRENT_SYMBOL_INDEX_VERSION}
300
+ )
301
+ ${filterSql}`, params);
302
+ return rows.map(r => r.name);
303
+ }
304
+ catch (err) {
305
+ if (!/relation "symbol_index_version" does not exist/i.test(err.message))
306
+ throw err;
307
+ const rows = await this.query(`SELECT DISTINCT t.name FROM (${rowsUnion}) t ${projectNames ? 'WHERE t.name = ANY($1)' : ''}`, params);
308
+ return rows.map(r => r.name);
309
+ }
310
+ }
311
+ async upsertSymbolIndexVersion(project) {
312
+ await this.query(`INSERT INTO symbol_index_version (project, version) VALUES ($1, $2)
313
+ ON CONFLICT (project) DO UPDATE SET version = EXCLUDED.version`, [project, exports.CURRENT_SYMBOL_INDEX_VERSION]);
314
+ }
247
315
  /** Guard against SQL injection — the dimension is interpolated into DDL. */
248
316
  assertDim(dimensions) {
249
317
  if (!Number.isInteger(dimensions) || dimensions <= 0 || dimensions > 2000) {
@@ -251,6 +319,20 @@ class DatabaseService {
251
319
  }
252
320
  return dimensions;
253
321
  }
322
+ async assertMinPostgresVersion() {
323
+ let versionNum;
324
+ try {
325
+ const rows = await this.query(`SELECT current_setting('server_version_num') AS v`);
326
+ versionNum = Number(rows[0]?.v);
327
+ }
328
+ catch (err) {
329
+ throw new Error(`Failed to read PostgreSQL server_version_num (requires PostgreSQL 15+): ${err.message}`);
330
+ }
331
+ if (!Number.isFinite(versionNum) || versionNum < MIN_PG_VERSION_NUM) {
332
+ throw new Error(`PostgreSQL 15+ is required (server_version_num=${versionNum}) — ` +
333
+ `the symbol index's unique index relies on NULLS NOT DISTINCT, added in PostgreSQL 15.`);
334
+ }
335
+ }
254
336
  async ensureDatabaseExists() {
255
337
  const { host, port, username, password, database } = this.config.db;
256
338
  const client = new pg_1.Client({ host, port, user: username, password, database: 'postgres' });
@@ -35,18 +35,6 @@ export declare class IndexerService {
35
35
  prepareEmbeddingSchema(): Promise<IndexMeta>;
36
36
  private reconcileIndex;
37
37
  private countChunks;
38
- backfillSymbols(signal?: AbortSignal): Promise<{
39
- total: number;
40
- processed: number;
41
- skipped: number;
42
- failed: number;
43
- }>;
44
- backfillCalls(signal?: AbortSignal): Promise<{
45
- total: number;
46
- processed: number;
47
- skipped: number;
48
- failed: number;
49
- }>;
50
38
  private writeDepEdges;
51
39
  scanAndIndex(dir: string, serviceName: string, force?: boolean, signal?: AbortSignal, preCollectedFiles?: string[], opts?: {
52
40
  rootTsConfigPath?: string;
@@ -55,8 +43,6 @@ export declare class IndexerService {
55
43
  removed: string[];
56
44
  failed: string[];
57
45
  }>;
58
- private static inferRole;
59
- private getEnrichedPrompt;
60
46
  private static hashContent;
61
47
  private ingestFile;
62
48
  }
@@ -40,6 +40,8 @@ const crypto_1 = require("crypto");
40
40
  const cli_progress_1 = require("cli-progress");
41
41
  const drizzle_orm_1 = require("drizzle-orm");
42
42
  const owner_path_1 = require("../owner-path");
43
+ const enriched_prompt_1 = require("../enriched-prompt");
44
+ const symbol_rows_1 = require("../symbol-rows");
43
45
  const db_1 = require("../db");
44
46
  const file_utils_1 = require("../file-utils");
45
47
  const LoggerService_1 = require("./LoggerService");
@@ -124,97 +126,6 @@ class IndexerService {
124
126
  throw err;
125
127
  }
126
128
  }
127
- async backfillSymbols(signal) {
128
- const t0 = Date.now();
129
- const rows = await this.db.query(`SELECT DISTINCT "serviceName", "filePath" FROM code_chunk ORDER BY "serviceName", "filePath"`);
130
- this.logger.debug('IndexerService: backfillSymbols start', { total: rows.length });
131
- let processed = 0, skipped = 0, failed = 0;
132
- for (const { serviceName, filePath } of rows) {
133
- if (signal?.aborted)
134
- break;
135
- const absPath = path.join(this.config.workspaceRoot, filePath);
136
- const projectRoot = path.join(this.config.workspaceRoot, serviceName);
137
- if (!fs.existsSync(absPath)) {
138
- skipped++;
139
- continue;
140
- }
141
- try {
142
- const { symbols: rawSymbols } = await this.ast.analyzeFile(absPath, serviceName, projectRoot);
143
- const symbols = [...new Map(rawSymbols.map(s => ({ ...s, filePath })).map(s => [`${s.name}:${s.kind}`, s])).values()].map(({ project, filePath: fp, name, kind, signature, line, col }) => ({ project, filePath: fp, name, kind, signature, line, col }));
144
- await this.db.transaction(async (tx) => {
145
- await tx.delete(db_1.codeSymbol).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.codeSymbol.project, serviceName), (0, drizzle_orm_1.eq)(db_1.codeSymbol.filePath, filePath)));
146
- if (symbols.length > 0) {
147
- await tx.insert(db_1.codeSymbol).values(symbols);
148
- }
149
- });
150
- processed++;
151
- }
152
- catch (err) {
153
- const reason = err.message;
154
- console.error(` Failed: ${filePath} — ${reason}`);
155
- this.logger.error('IndexerService: symbol backfill failed', { file: filePath, reason });
156
- failed++;
157
- }
158
- const count = processed + failed;
159
- if (count > 0 && count % 50 === 0) {
160
- const msg = ` ${count}/${rows.length} files...`;
161
- process.stdout.isTTY ? process.stdout.write(msg + '\r') : console.log(msg);
162
- }
163
- }
164
- this.logger.debug('IndexerService: backfillSymbols done', { total: rows.length, processed, skipped, failed, ms: Date.now() - t0 });
165
- return { total: rows.length, processed, skipped, failed };
166
- }
167
- async backfillCalls(signal) {
168
- const t0 = Date.now();
169
- const rows = await this.db.query(`SELECT DISTINCT "serviceName", "filePath" FROM code_chunk ORDER BY "serviceName", "filePath"`);
170
- this.logger.debug('IndexerService: backfillCalls start', { total: rows.length });
171
- let processed = 0, skipped = 0, failed = 0;
172
- for (const { serviceName, filePath } of rows) {
173
- if (signal?.aborted)
174
- break;
175
- const absPath = path.join(this.config.workspaceRoot, filePath);
176
- const projectRoot = path.join(this.config.workspaceRoot, serviceName);
177
- if (!fs.existsSync(absPath)) {
178
- skipped++;
179
- continue;
180
- }
181
- try {
182
- const { calls: rawCalls } = await this.ast.analyzeFile(absPath, serviceName, projectRoot);
183
- const calls = rawCalls
184
- .filter(c => c.calleeFile.startsWith(this.config.workspaceRoot))
185
- .map(c => ({
186
- project: serviceName,
187
- fromFile: filePath,
188
- fromSymbol: c.fromSymbol,
189
- calleeName: c.calleeName,
190
- calleeFile: path.relative(this.config.workspaceRoot, c.calleeFile),
191
- calleeSymbol: c.calleeSymbol,
192
- line: c.line,
193
- col: c.col,
194
- }));
195
- await this.db.transaction(async (tx) => {
196
- await tx.delete(db_1.codeCall).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.codeCall.project, serviceName), (0, drizzle_orm_1.eq)(db_1.codeCall.fromFile, filePath)));
197
- if (calls.length > 0) {
198
- await tx.insert(db_1.codeCall).values(calls);
199
- }
200
- });
201
- processed++;
202
- }
203
- catch (err) {
204
- const reason = err.message;
205
- console.error(` Failed: ${filePath} — ${reason}`);
206
- this.logger.error('IndexerService: call backfill failed', { file: filePath, reason });
207
- failed++;
208
- }
209
- const count = processed + failed;
210
- if (count > 0 && count % 50 === 0) {
211
- const msg = ` ${count}/${rows.length} files...`;
212
- process.stdout.isTTY ? process.stdout.write(msg + '\r') : console.log(msg);
213
- }
214
- }
215
- this.logger.debug('IndexerService: backfillCalls done', { total: rows.length, processed, skipped, failed, ms: Date.now() - t0 });
216
- return { total: rows.length, processed, skipped, failed };
217
- }
218
129
  async writeDepEdges(serviceName, relFilePath, edges, tx) {
219
130
  const run = async (t) => {
220
131
  await t.delete(db_1.codeDependency).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.codeDependency.project, serviceName), (0, drizzle_orm_1.eq)(db_1.codeDependency.fromFile, relFilePath)));
@@ -231,7 +142,16 @@ class IndexerService {
231
142
  const startTime = Date.now();
232
143
  this.logger.debug('IndexerService: scanAndIndex start', { serviceName, dir, force });
233
144
  if (force) {
234
- await this.db.query(`UPDATE code_chunk SET "lastModified" = NULL, "contentHash" = NULL WHERE "serviceName" = $1`, [serviceName]);
145
+ // Reset incremental state and clear the project's symbol/call rows in one
146
+ // transaction: a file whose re-ingestion later fails must hold no rows at
147
+ // all, never rows in the previous (pre-member-extraction) format.
148
+ await this.db.transaction(async (tx) => {
149
+ await tx.update(db_1.codeChunk)
150
+ .set({ lastModified: null, contentHash: null })
151
+ .where((0, drizzle_orm_1.eq)(db_1.codeChunk.serviceName, serviceName));
152
+ await tx.delete(db_1.codeSymbol).where((0, drizzle_orm_1.eq)(db_1.codeSymbol.project, serviceName));
153
+ await tx.delete(db_1.codeCall).where((0, drizzle_orm_1.eq)(db_1.codeCall.project, serviceName));
154
+ });
235
155
  }
236
156
  const files = preCollectedFiles ?? (0, file_utils_1.collectFiles)(dir, (0, file_utils_1.loadIgnore)(dir), dir);
237
157
  this.logger.debug('IndexerService: scanAndIndex files collected', { serviceName, count: files.length, force });
@@ -334,6 +254,12 @@ class IndexerService {
334
254
  }
335
255
  }
336
256
  this.ast.clearCache(dir);
257
+ // The marker is written exactly when the scan reaches here without having
258
+ // been interrupted — never on the early-return abort path above, and never
259
+ // when anything in this method throws (the error propagates instead).
260
+ if (!signal?.aborted) {
261
+ await this.db.upsertSymbolIndexVersion(serviceName);
262
+ }
337
263
  stats.elapsedSec = Math.round((Date.now() - startTime) / 100) / 10;
338
264
  this.logger.debug('IndexerService: project indexed', {
339
265
  project: serviceName,
@@ -366,51 +292,6 @@ class IndexerService {
366
292
  }
367
293
  return { removed, failed };
368
294
  }
369
- static inferRole(relPath) {
370
- const p = relPath.toLowerCase();
371
- if (p.includes('/services/') || p.endsWith('.service.ts'))
372
- return 'Service';
373
- if (p.includes('/controllers/') || p.endsWith('.controller.ts'))
374
- return 'Controller';
375
- if (p.includes('/entities/') || p.endsWith('.entity.ts'))
376
- return 'Entity';
377
- if (p.includes('/guards/') || p.endsWith('.guard.ts'))
378
- return 'Guard';
379
- if (p.includes('/commands/'))
380
- return 'Command';
381
- if (p.includes('/interfaces/'))
382
- return 'Interface';
383
- if (p.includes('/middleware/'))
384
- return 'Middleware';
385
- if (p.includes('/interceptors/'))
386
- return 'Interceptor';
387
- if (p.endsWith('.test.ts') || p.endsWith('.spec.ts') || p.includes('/tests/'))
388
- return 'Test';
389
- if (p.includes('/utils/') || p.includes('/helpers/') || p.endsWith('-utils.ts'))
390
- return 'Utility';
391
- return null;
392
- }
393
- getEnrichedPrompt(piece, relPath, serviceName, _meta, edges, symbols) {
394
- const chunkStart = piece.startLine;
395
- const chunkEnd = piece.endLine;
396
- const lines = [];
397
- lines.push(`Project: ${serviceName} | File: ${relPath}`);
398
- const role = IndexerService.inferRole(relPath);
399
- if (role)
400
- lines.push(`Role: ${role}`);
401
- const overlapping = symbols.filter(s => s.line != null &&
402
- s.line <= chunkEnd &&
403
- (s.endLine ?? s.line) >= chunkStart);
404
- if (overlapping.length > 0) {
405
- lines.push(`Symbols: ${overlapping.map(s => `${s.kind} ${s.name}`).join(', ')}`);
406
- }
407
- if (edges.length > 0) {
408
- const depNames = [...new Set(edges.map(e => path.basename(e.toFile, path.extname(e.toFile)).split('.')[0]))];
409
- lines.push(`Dependencies: ${depNames.join(', ')}`);
410
- }
411
- lines.push(`Code: ${piece.content}`);
412
- return lines.join('\n');
413
- }
414
295
  static hashContent(content) {
415
296
  return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
416
297
  }
@@ -434,7 +315,7 @@ class IndexerService {
434
315
  return { status: 'skipped', chunks: 0, pendingMtimeRefresh: relFilePath };
435
316
  }
436
317
  const tAst = Date.now();
437
- const { edges: rawEdges, symbols: rawSymbols, calls: rawCalls, ownerRanges: rawOwnerRanges = [] } = analysis ?? { edges: [], symbols: [], calls: [] };
318
+ const { edges: rawEdges, symbols: rawSymbols, members: rawMembers = [], calls: rawCalls, ownerRanges: rawOwnerRanges = [], } = analysis ?? { edges: [], symbols: [], calls: [] };
438
319
  // astMs is near-zero — actual AST time is spent upfront in analyzeProject()
439
320
  // Normalize edges to workspace-relative paths
440
321
  const edges = rawEdges
@@ -457,8 +338,9 @@ class IndexerService {
457
338
  line: c.line,
458
339
  col: c.col,
459
340
  }));
460
- // Normalize symbols: replace absolute filePath with workspace-relative, strip endLine (not stored)
461
- const symbols = [...new Map(rawSymbols.map(s => ({ ...s, filePath: relFilePath })).map(s => [`${s.name}:${s.kind}`, s])).values()].map(({ project, filePath: fp, name, kind, signature, line, col }) => ({ project, filePath: fp, name, kind, signature, line, col }));
341
+ // symbols ∪ members → code_symbol rows: overload sets collapse to their
342
+ // representative row, then a final dedup by (container, name, kind, line).
343
+ const symbolRows = (0, symbol_rows_1.buildSymbolRows)(rawSymbols, rawMembers).map(s => ({ ...s, filePath: relFilePath }));
462
344
  const astMs = Date.now() - tAst;
463
345
  const chunks = await this.chunker.chunk(code, {
464
346
  strategy: this.config.indexer.chunkStrategy,
@@ -466,20 +348,16 @@ class IndexerService {
466
348
  symbols: rawSymbols,
467
349
  });
468
350
  const relPath = path.relative(projectRoot, filePath);
469
- // Owner-path ranges: top-level symbols (container: null) + method ranges.
470
- // Deepest range containing a chunk's start line wins → Class.method.
471
- const ownerRanges = [
472
- ...rawSymbols
473
- .filter(s => s.line != null)
474
- .map(s => ({ name: s.name, container: null, line: s.line, endLine: s.endLine ?? s.line })),
475
- ...rawOwnerRanges,
476
- ];
351
+ // Ownership ranges (decorator-inclusive, symbols ∪ members minus
352
+ // method_signature) are built by AstParserService — the deepest range
353
+ // containing a chunk's start line wins.
354
+ const ownerRanges = rawOwnerRanges;
477
355
  const t2 = Date.now();
478
356
  const records = [];
479
357
  const embedBatchSize = this.config.embedding.batchSize;
480
358
  for (let i = 0; i < chunks.length; i += embedBatchSize) {
481
359
  const batch = chunks.slice(i, i + embedBatchSize);
482
- const prompts = batch.map(piece => this.getEnrichedPrompt(piece, relPath, serviceName, meta ?? 'missing', edges, rawSymbols));
360
+ const prompts = batch.map(piece => (0, enriched_prompt_1.getEnrichedPrompt)(piece, relPath, serviceName, edges, rawSymbols));
483
361
  const vectors = await this.embedder.embedBatch(prompts, 'document');
484
362
  for (let j = 0; j < batch.length; j++) {
485
363
  const piece = batch[j];
@@ -502,8 +380,8 @@ class IndexerService {
502
380
  }
503
381
  await this.writeDepEdges(serviceName, relFilePath, edges, tx);
504
382
  await tx.delete(db_1.codeSymbol).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.codeSymbol.project, serviceName), (0, drizzle_orm_1.eq)(db_1.codeSymbol.filePath, relFilePath)));
505
- if (symbols.length > 0) {
506
- await tx.insert(db_1.codeSymbol).values(symbols);
383
+ if (symbolRows.length > 0) {
384
+ await tx.insert(db_1.codeSymbol).values(symbolRows);
507
385
  }
508
386
  await tx.delete(db_1.codeCall).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.codeCall.project, serviceName), (0, drizzle_orm_1.eq)(db_1.codeCall.fromFile, relFilePath)));
509
387
  if (calls.length > 0) {
@@ -73,8 +73,8 @@ Pick one workflow based on what you need:
73
73
  - **read_file_range** — read a specific line range from any indexed file. Expects a **relative path** as returned by search_codebase.
74
74
  - **get_dependencies** — show what files a given file imports (direct or transitive). Use to understand what a file depends on.
75
75
  - **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers.
76
- - **find_symbol** — find where a symbol (function, class, type, interface, variable, method) is defined by exact name. Faster than semantic search when you know the name.
77
- - **get_callers** — find all call sites of a function or method by exact name. Uses the call graph index — faster and more precise than grep. Use after find_symbol to trace where a symbol is actually called.
76
+ - **find_symbol** — find where a symbol is defined by exact name: top-level declarations, class/object-literal members (methods, get/set accessors, constructors, function properties), interface and type-literal method signatures, and namespace-level declarations. Accepts a bare name (\`assertUsersInOrg\`), a qualified name (\`RepairsService.assertUsersInOrg\`, \`NS.Parser.parse\`) to disambiguate same-named members, a quoted name for string-literal members (\`Router.'kebab-name'\`), or a computed name (\`Stack.[Symbol.iterator]\`). Faster than semantic search when you know the name.
77
+ - **get_callers** — find all call sites of a function or method by exact name. Accepts a bare or qualified (\`Class.method\`) symbol, using the same suffix matching as \`find_symbol\`. Uses the call graph index — faster and more precise than grep. A call through an interface-typed receiver is recorded under the interface member (e.g. \`IEmbedder.embedBatch\`), not the implementing class. Use after find_symbol to trace where a symbol is actually called.
78
78
  - **list_files** — browse the directory tree of a project path. Use to understand project structure before reading files.
79
79
  - **read_file** — read a complete file by **absolute path** with line numbers. Truncates at max_lines with a hint to continue.
80
80
 
@@ -86,7 +86,7 @@ Pick one workflow based on what you need:
86
86
  3. If a result looks **truncated or incomplete**, call \`read_file_range\` with the returned \`file_path\` and \`start_line\`.
87
87
  4. For exploratory searches where results span chunk boundaries, pass \`include_neighbors: true\`.
88
88
  5. To immediately see a file's imports and callers without extra tool calls, pass \`include_graph_context: true\` to \`search_codebase\`. Use this when doing a quick structural survey; switch to \`get_dependencies\`/\`get_dependents\` for full transitive graphs.
89
- 6. Use \`find_symbol\` when you know the exact name — faster than semantic search. Use \`kind: "method"\` for class methods — grep matches lines where \`public\`, \`async\`, \`static\`, or \`override\` appears before the name. After locating a symbol, use \`get_callers\` to find all call sites — faster and more precise than grep (requires \`rag-index backfill --calls\` to have been run).
89
+ 6. Use \`find_symbol\` when you know the exact name — faster than semantic search. Use a qualified name (\`Class.method\`) to disambiguate same-named methods of different classes; a bare name searches every container. Hits marked \`(grep — not in index; may be stale or imprecise)\` come from a fallback text search, not the index — treat them as less reliable. After locating a symbol, use \`get_callers\` to find all call sites — faster and more precise than grep.
90
90
  7. Use \`list_files\` to explore project structure before diving into files. Use \`read_file\` to read a complete file by absolute path.
91
91
 
92
92
  ### Flow audit (mandatory steps — do not skip 3 and 4)
@@ -55,7 +55,7 @@ export interface GrepMatch {
55
55
  filePath: string;
56
56
  line: number;
57
57
  }
58
- export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'method' | 'all';
58
+ export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'method' | 'get_accessor' | 'set_accessor' | 'constructor' | 'function_property' | 'method_signature' | 'all';
59
59
  export interface CallerRow {
60
60
  fromFile: string;
61
61
  fromSymbol: string | null;
@@ -69,6 +69,10 @@ export interface SymbolHit {
69
69
  column: number;
70
70
  kind: Exclude<SymbolKind, 'all'>;
71
71
  signature: string;
72
+ /** `container.name`, or `name` when the container is null. */
73
+ qualified: string;
74
+ isStatic: boolean;
75
+ source: 'index' | 'grep';
72
76
  }
73
77
  export declare class SearcherService {
74
78
  private db;
@@ -130,6 +134,14 @@ export declare class SearcherService {
130
134
  getDependencies(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
131
135
  getDependents(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
132
136
  getCallers(symbol: string, project: string, calleeFile?: string | null): Promise<CallerRow[]>;
137
+ /**
138
+ * One-line warning when any project in `scope` (the given `project`, or
139
+ * every project with `code_chunk`/`code_symbol` rows) is outdated — shown
140
+ * first in `find_symbol`/`get_callers` output. Any failure to determine the
141
+ * scope or read markers only drops the notice (warn logged); the lookup
142
+ * itself is unaffected.
143
+ */
144
+ outdatedNotice(project: string | null): Promise<string | null>;
133
145
  grepSymbol(symbol: string, project: string, excludeFile: string): Promise<{
134
146
  matches: GrepMatch[];
135
147
  truncated: boolean;
@@ -137,6 +149,7 @@ export declare class SearcherService {
137
149
  findSymbol(name: string, project: string | null, kind: SymbolKind): Promise<{
138
150
  hits: SymbolHit[];
139
151
  truncated: boolean;
152
+ grepFailed: boolean;
140
153
  }>;
141
154
  private findSymbolFromIndex;
142
155
  private grepFindSymbol;
@@ -147,11 +160,19 @@ export declare class SearcherService {
147
160
  readFileAbsolute(absPath: string, maxLines: number): Promise<FileRangeResult>;
148
161
  readFileRange(filePath: string, startLine: number, endLine: number): Promise<FileRangeResult>;
149
162
  private walkDir;
163
+ /**
164
+ * ERE pattern for a grep-fallback kind, or null when that kind is never
165
+ * grepped (`function_property`/`method_signature` are index-only; a bare
166
+ * `constructor` search only makes sense when the name itself is `constructor`).
167
+ */
150
168
  private buildFindSymbolPattern;
169
+ /** 1-based index of the first whole-word occurrence of `name` in `line`, or -1. */
170
+ private static wholeWordIndex;
151
171
  private inferKind;
152
172
  /**
153
- * Async wrapper around grep to avoid blocking the event loop.
154
- * Returns stdout as a string, or null if grep returned no results or errored.
173
+ * Async wrapper around grep to avoid blocking the event loop. `ok: false`
174
+ * means grep could not be spawned or exited with status ≥ 2 (distinguishable
175
+ * from a clean "no matches", which is `ok: true, output: null`).
155
176
  */
156
177
  private spawnGrep;
157
178
  }