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.
- package/CHANGELOG.md +16 -0
- package/README.md +5 -2
- package/dist/app.js +2 -17
- package/dist/src/commands/IndexCommand.js +27 -7
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/schema.d.ts +75 -0
- package/dist/src/db/schema.js +10 -1
- package/dist/src/enriched-prompt.d.ts +8 -0
- package/dist/src/enriched-prompt.js +87 -0
- package/dist/src/interfaces/IAstParser.d.ts +18 -2
- package/dist/src/interfaces/IDatabase.d.ts +14 -0
- package/dist/src/interfaces/ISearcher.d.ts +3 -0
- package/dist/src/services/AstParserService.d.ts +39 -7
- package/dist/src/services/AstParserService.js +504 -104
- package/dist/src/services/DatabaseService.d.ts +13 -0
- package/dist/src/services/DatabaseService.js +84 -2
- package/dist/src/services/IndexerService.d.ts +0 -14
- package/dist/src/services/IndexerService.js +29 -151
- package/dist/src/services/McpServer.js +3 -3
- package/dist/src/services/SearcherService.d.ts +24 -3
- package/dist/src/services/SearcherService.js +149 -58
- package/dist/src/symbol-name.d.ts +44 -0
- package/dist/src/symbol-name.js +68 -0
- package/dist/src/symbol-rows.d.ts +12 -0
- package/dist/src/symbol-rows.js +38 -0
- package/dist/src/tools/FindSymbolTool.js +30 -7
- package/dist/src/tools/GetCallersTool.js +20 -5
- package/docs/architecture.svg +103 -0
- package/package.json +1 -2
- package/dist/src/commands/BackfillCommand.d.ts +0 -7
- package/dist/src/commands/BackfillCommand.js +0 -37
|
@@ -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(`
|
|
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
|
-
|
|
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
|
-
//
|
|
461
|
-
|
|
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
|
-
//
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
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 =>
|
|
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 (
|
|
506
|
-
await tx.insert(db_1.codeSymbol).values(
|
|
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
|
|
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 \`
|
|
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
|
-
*
|
|
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
|
}
|