codesearch-mcp 0.0.15-alpha → 0.0.16-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 CHANGED
@@ -3,6 +3,23 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
+ ## [0.0.16-alpha] - 2026-05-02
7
+
8
+ ### Added
9
+
10
+ - **Hybrid incremental indexing (mtime + SHA-256)** — `rag-index` now uses a two-step skip check per file: fast `mtime` comparison first (no file read), then SHA-256 content hash if `mtime` changed. Files with identical content but reset timestamps (after `git clone`, DB transfer, or `touch`) are skipped without re-embedding. `lastModified` is refreshed in a single batch SQL `UPDATE` after each project scan. `--reindex` clears both fields.
11
+ - `contentHash varchar(64)` column added to `code_chunk`; applied automatically via `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` on startup — no manual migration needed.
12
+
13
+ ### Changed
14
+
15
+ - **Migrated from TypeORM to Drizzle ORM**
16
+ - **Batch AST parsing (`analyzeProject`)** — `IndexerService` now parses all project files in a single `MorphProject` instance instead of one per file, reducing ts-morph overhead on large projects
17
+ - **Enriched embedding context** — chunks now include `[Symbols: class Foo, function bar]` tag when the chunk overlaps known symbol definitions, improving semantic search quality for partial/truncated chunks. **Requires full re-index** (`rag-index /path --reindex`) — existing embeddings were built without symbol context and will produce degraded similarity scores against new chunks if mixed.
18
+ - **`endLine` added to `SymbolEntry`** — enables accurate chunk-to-symbol overlap detection used by enriched prompts
19
+ - **`analyzeProject` error isolation** — parse failures are caught per-file; one broken file no longer aborts the entire project scan
20
+ - **Memory cleanup in `analyzeProject`** — source files are explicitly removed from `MorphProject` after parsing to release memory
21
+ - **Logging improvements** — `AstParser: analyzeProject done` debug log reports total files, parsed count, and elapsed ms; per-file `depsMs` renamed to `astMs` (now ~0 since AST work is done upfront)
22
+
6
23
  ## [0.0.15-alpha] - 2026-05-02
7
24
 
8
25
  ### Changed
package/README.md CHANGED
@@ -6,6 +6,8 @@ Ask Claude things like *"where is the payment retry logic?"* or *"show me how au
6
6
 
7
7
  > **Alpha** — works well locally, API may change before 1.0.
8
8
 
9
+ See [CHANGELOG.md](./CHANGELOG.md) for version history.
10
+
9
11
  ## How it works
10
12
 
11
13
  1. `rag-index` scans your projects, chunks source files, embeds them with [bge-m3](https://huggingface.co/BAAI/bge-m3) via Ollama, and stores vectors in PostgreSQL
@@ -1,3 +1,2 @@
1
- export { CodeChunk } from './entities/code-chunk.entity';
2
- export { CodeDependency } from './entities/code-dependency.entity';
3
- export { CodeSymbol } from './entities/code-symbol.entity';
1
+ export { codeChunk, codeDependency, codeSymbol } from './schema';
2
+ export type { CodeChunkInsert } from './schema';
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CodeSymbol = exports.CodeDependency = exports.CodeChunk = void 0;
4
- var code_chunk_entity_1 = require("./entities/code-chunk.entity");
5
- Object.defineProperty(exports, "CodeChunk", { enumerable: true, get: function () { return code_chunk_entity_1.CodeChunk; } });
6
- var code_dependency_entity_1 = require("./entities/code-dependency.entity");
7
- Object.defineProperty(exports, "CodeDependency", { enumerable: true, get: function () { return code_dependency_entity_1.CodeDependency; } });
8
- var code_symbol_entity_1 = require("./entities/code-symbol.entity");
9
- Object.defineProperty(exports, "CodeSymbol", { enumerable: true, get: function () { return code_symbol_entity_1.CodeSymbol; } });
3
+ exports.codeSymbol = exports.codeDependency = exports.codeChunk = void 0;
4
+ var schema_1 = require("./schema");
5
+ Object.defineProperty(exports, "codeChunk", { enumerable: true, get: function () { return schema_1.codeChunk; } });
6
+ Object.defineProperty(exports, "codeDependency", { enumerable: true, get: function () { return schema_1.codeDependency; } });
7
+ Object.defineProperty(exports, "codeSymbol", { enumerable: true, get: function () { return schema_1.codeSymbol; } });
@@ -0,0 +1,417 @@
1
+ export declare const codeChunk: import("drizzle-orm/pg-core").PgTableWithColumns<{
2
+ name: "code_chunk";
3
+ schema: undefined;
4
+ columns: {
5
+ id: import("drizzle-orm/pg-core").PgColumn<{
6
+ name: "id";
7
+ tableName: "code_chunk";
8
+ dataType: "string";
9
+ columnType: "PgUUID";
10
+ data: string;
11
+ driverParam: string;
12
+ notNull: true;
13
+ hasDefault: true;
14
+ isPrimaryKey: true;
15
+ isAutoincrement: false;
16
+ hasRuntimeDefault: false;
17
+ enumValues: undefined;
18
+ baseColumn: never;
19
+ identity: undefined;
20
+ generated: undefined;
21
+ }, {}, {}>;
22
+ serviceName: import("drizzle-orm/pg-core").PgColumn<{
23
+ name: "serviceName";
24
+ tableName: "code_chunk";
25
+ dataType: "string";
26
+ columnType: "PgText";
27
+ data: string;
28
+ driverParam: string;
29
+ notNull: true;
30
+ hasDefault: false;
31
+ isPrimaryKey: false;
32
+ isAutoincrement: false;
33
+ hasRuntimeDefault: false;
34
+ enumValues: [string, ...string[]];
35
+ baseColumn: never;
36
+ identity: undefined;
37
+ generated: undefined;
38
+ }, {}, {}>;
39
+ filePath: import("drizzle-orm/pg-core").PgColumn<{
40
+ name: "filePath";
41
+ tableName: "code_chunk";
42
+ dataType: "string";
43
+ columnType: "PgText";
44
+ data: string;
45
+ driverParam: string;
46
+ notNull: true;
47
+ hasDefault: false;
48
+ isPrimaryKey: false;
49
+ isAutoincrement: false;
50
+ hasRuntimeDefault: false;
51
+ enumValues: [string, ...string[]];
52
+ baseColumn: never;
53
+ identity: undefined;
54
+ generated: undefined;
55
+ }, {}, {}>;
56
+ startLine: import("drizzle-orm/pg-core").PgColumn<{
57
+ name: "startLine";
58
+ tableName: "code_chunk";
59
+ dataType: "number";
60
+ columnType: "PgInteger";
61
+ data: number;
62
+ driverParam: string | number;
63
+ notNull: false;
64
+ hasDefault: false;
65
+ isPrimaryKey: false;
66
+ isAutoincrement: false;
67
+ hasRuntimeDefault: false;
68
+ enumValues: undefined;
69
+ baseColumn: never;
70
+ identity: undefined;
71
+ generated: undefined;
72
+ }, {}, {}>;
73
+ content: import("drizzle-orm/pg-core").PgColumn<{
74
+ name: "content";
75
+ tableName: "code_chunk";
76
+ dataType: "string";
77
+ columnType: "PgText";
78
+ data: string;
79
+ driverParam: string;
80
+ notNull: true;
81
+ hasDefault: false;
82
+ isPrimaryKey: false;
83
+ isAutoincrement: false;
84
+ hasRuntimeDefault: false;
85
+ enumValues: [string, ...string[]];
86
+ baseColumn: never;
87
+ identity: undefined;
88
+ generated: undefined;
89
+ }, {}, {}>;
90
+ embedding: import("drizzle-orm/pg-core").PgColumn<{
91
+ name: "embedding";
92
+ tableName: "code_chunk";
93
+ dataType: "custom";
94
+ columnType: "PgCustomColumn";
95
+ data: string;
96
+ driverParam: unknown;
97
+ notNull: true;
98
+ hasDefault: false;
99
+ isPrimaryKey: false;
100
+ isAutoincrement: false;
101
+ hasRuntimeDefault: false;
102
+ enumValues: undefined;
103
+ baseColumn: never;
104
+ identity: undefined;
105
+ generated: undefined;
106
+ }, {}, {
107
+ pgColumnBuilderBrand: "PgCustomColumnBuilderBrand";
108
+ }>;
109
+ lastModified: import("drizzle-orm/pg-core").PgColumn<{
110
+ name: "lastModified";
111
+ tableName: "code_chunk";
112
+ dataType: "number";
113
+ columnType: "PgDoublePrecision";
114
+ data: number;
115
+ driverParam: string | number;
116
+ notNull: false;
117
+ hasDefault: false;
118
+ isPrimaryKey: false;
119
+ isAutoincrement: false;
120
+ hasRuntimeDefault: false;
121
+ enumValues: undefined;
122
+ baseColumn: never;
123
+ identity: undefined;
124
+ generated: undefined;
125
+ }, {}, {}>;
126
+ contentHash: import("drizzle-orm/pg-core").PgColumn<{
127
+ name: "contentHash";
128
+ tableName: "code_chunk";
129
+ dataType: "string";
130
+ columnType: "PgVarchar";
131
+ data: string;
132
+ driverParam: string;
133
+ notNull: false;
134
+ hasDefault: false;
135
+ isPrimaryKey: false;
136
+ isAutoincrement: false;
137
+ hasRuntimeDefault: false;
138
+ enumValues: [string, ...string[]];
139
+ baseColumn: never;
140
+ identity: undefined;
141
+ generated: undefined;
142
+ }, {}, {
143
+ length: 64;
144
+ }>;
145
+ };
146
+ dialect: "pg";
147
+ }>;
148
+ export declare const codeDependency: import("drizzle-orm/pg-core").PgTableWithColumns<{
149
+ name: "code_dependency";
150
+ schema: undefined;
151
+ columns: {
152
+ id: import("drizzle-orm/pg-core").PgColumn<{
153
+ name: "id";
154
+ tableName: "code_dependency";
155
+ dataType: "string";
156
+ columnType: "PgUUID";
157
+ data: string;
158
+ driverParam: string;
159
+ notNull: true;
160
+ hasDefault: true;
161
+ isPrimaryKey: true;
162
+ isAutoincrement: false;
163
+ hasRuntimeDefault: false;
164
+ enumValues: undefined;
165
+ baseColumn: never;
166
+ identity: undefined;
167
+ generated: undefined;
168
+ }, {}, {}>;
169
+ project: import("drizzle-orm/pg-core").PgColumn<{
170
+ name: "project";
171
+ tableName: "code_dependency";
172
+ dataType: "string";
173
+ columnType: "PgText";
174
+ data: string;
175
+ driverParam: string;
176
+ notNull: true;
177
+ hasDefault: false;
178
+ isPrimaryKey: false;
179
+ isAutoincrement: false;
180
+ hasRuntimeDefault: false;
181
+ enumValues: [string, ...string[]];
182
+ baseColumn: never;
183
+ identity: undefined;
184
+ generated: undefined;
185
+ }, {}, {}>;
186
+ fromFile: import("drizzle-orm/pg-core").PgColumn<{
187
+ name: "fromFile";
188
+ tableName: "code_dependency";
189
+ dataType: "string";
190
+ columnType: "PgText";
191
+ data: string;
192
+ driverParam: string;
193
+ notNull: true;
194
+ hasDefault: false;
195
+ isPrimaryKey: false;
196
+ isAutoincrement: false;
197
+ hasRuntimeDefault: false;
198
+ enumValues: [string, ...string[]];
199
+ baseColumn: never;
200
+ identity: undefined;
201
+ generated: undefined;
202
+ }, {}, {}>;
203
+ fromSymbol: import("drizzle-orm/pg-core").PgColumn<{
204
+ name: "fromSymbol";
205
+ tableName: "code_dependency";
206
+ dataType: "string";
207
+ columnType: "PgText";
208
+ data: string;
209
+ driverParam: string;
210
+ notNull: false;
211
+ hasDefault: false;
212
+ isPrimaryKey: false;
213
+ isAutoincrement: false;
214
+ hasRuntimeDefault: false;
215
+ enumValues: [string, ...string[]];
216
+ baseColumn: never;
217
+ identity: undefined;
218
+ generated: undefined;
219
+ }, {}, {}>;
220
+ toFile: import("drizzle-orm/pg-core").PgColumn<{
221
+ name: "toFile";
222
+ tableName: "code_dependency";
223
+ dataType: "string";
224
+ columnType: "PgText";
225
+ data: string;
226
+ driverParam: string;
227
+ notNull: true;
228
+ hasDefault: false;
229
+ isPrimaryKey: false;
230
+ isAutoincrement: false;
231
+ hasRuntimeDefault: false;
232
+ enumValues: [string, ...string[]];
233
+ baseColumn: never;
234
+ identity: undefined;
235
+ generated: undefined;
236
+ }, {}, {}>;
237
+ toSymbol: import("drizzle-orm/pg-core").PgColumn<{
238
+ name: "toSymbol";
239
+ tableName: "code_dependency";
240
+ dataType: "string";
241
+ columnType: "PgText";
242
+ data: string;
243
+ driverParam: string;
244
+ notNull: false;
245
+ hasDefault: false;
246
+ isPrimaryKey: false;
247
+ isAutoincrement: false;
248
+ hasRuntimeDefault: false;
249
+ enumValues: [string, ...string[]];
250
+ baseColumn: never;
251
+ identity: undefined;
252
+ generated: undefined;
253
+ }, {}, {}>;
254
+ depType: import("drizzle-orm/pg-core").PgColumn<{
255
+ name: "depType";
256
+ tableName: "code_dependency";
257
+ dataType: "string";
258
+ columnType: "PgText";
259
+ data: string;
260
+ driverParam: string;
261
+ notNull: true;
262
+ hasDefault: false;
263
+ isPrimaryKey: false;
264
+ isAutoincrement: false;
265
+ hasRuntimeDefault: false;
266
+ enumValues: [string, ...string[]];
267
+ baseColumn: never;
268
+ identity: undefined;
269
+ generated: undefined;
270
+ }, {}, {}>;
271
+ };
272
+ dialect: "pg";
273
+ }>;
274
+ export declare const codeSymbol: import("drizzle-orm/pg-core").PgTableWithColumns<{
275
+ name: "code_symbol";
276
+ schema: undefined;
277
+ columns: {
278
+ id: import("drizzle-orm/pg-core").PgColumn<{
279
+ name: "id";
280
+ tableName: "code_symbol";
281
+ dataType: "string";
282
+ columnType: "PgUUID";
283
+ data: string;
284
+ driverParam: string;
285
+ notNull: true;
286
+ hasDefault: true;
287
+ isPrimaryKey: true;
288
+ isAutoincrement: false;
289
+ hasRuntimeDefault: false;
290
+ enumValues: undefined;
291
+ baseColumn: never;
292
+ identity: undefined;
293
+ generated: undefined;
294
+ }, {}, {}>;
295
+ project: import("drizzle-orm/pg-core").PgColumn<{
296
+ name: "project";
297
+ tableName: "code_symbol";
298
+ dataType: "string";
299
+ columnType: "PgText";
300
+ data: string;
301
+ driverParam: string;
302
+ notNull: true;
303
+ hasDefault: false;
304
+ isPrimaryKey: false;
305
+ isAutoincrement: false;
306
+ hasRuntimeDefault: false;
307
+ enumValues: [string, ...string[]];
308
+ baseColumn: never;
309
+ identity: undefined;
310
+ generated: undefined;
311
+ }, {}, {}>;
312
+ filePath: import("drizzle-orm/pg-core").PgColumn<{
313
+ name: "filePath";
314
+ tableName: "code_symbol";
315
+ dataType: "string";
316
+ columnType: "PgText";
317
+ data: string;
318
+ driverParam: string;
319
+ notNull: true;
320
+ hasDefault: false;
321
+ isPrimaryKey: false;
322
+ isAutoincrement: false;
323
+ hasRuntimeDefault: false;
324
+ enumValues: [string, ...string[]];
325
+ baseColumn: never;
326
+ identity: undefined;
327
+ generated: undefined;
328
+ }, {}, {}>;
329
+ name: import("drizzle-orm/pg-core").PgColumn<{
330
+ name: "name";
331
+ tableName: "code_symbol";
332
+ dataType: "string";
333
+ columnType: "PgText";
334
+ data: string;
335
+ driverParam: string;
336
+ notNull: true;
337
+ hasDefault: false;
338
+ isPrimaryKey: false;
339
+ isAutoincrement: false;
340
+ hasRuntimeDefault: false;
341
+ enumValues: [string, ...string[]];
342
+ baseColumn: never;
343
+ identity: undefined;
344
+ generated: undefined;
345
+ }, {}, {}>;
346
+ kind: import("drizzle-orm/pg-core").PgColumn<{
347
+ name: "kind";
348
+ tableName: "code_symbol";
349
+ dataType: "string";
350
+ columnType: "PgText";
351
+ data: string;
352
+ driverParam: string;
353
+ notNull: true;
354
+ hasDefault: false;
355
+ isPrimaryKey: false;
356
+ isAutoincrement: false;
357
+ hasRuntimeDefault: false;
358
+ enumValues: [string, ...string[]];
359
+ baseColumn: never;
360
+ identity: undefined;
361
+ generated: undefined;
362
+ }, {}, {}>;
363
+ signature: import("drizzle-orm/pg-core").PgColumn<{
364
+ name: "signature";
365
+ tableName: "code_symbol";
366
+ dataType: "string";
367
+ columnType: "PgText";
368
+ data: string;
369
+ driverParam: string;
370
+ notNull: false;
371
+ hasDefault: false;
372
+ isPrimaryKey: false;
373
+ isAutoincrement: false;
374
+ hasRuntimeDefault: false;
375
+ enumValues: [string, ...string[]];
376
+ baseColumn: never;
377
+ identity: undefined;
378
+ generated: undefined;
379
+ }, {}, {}>;
380
+ line: import("drizzle-orm/pg-core").PgColumn<{
381
+ name: "line";
382
+ tableName: "code_symbol";
383
+ dataType: "number";
384
+ columnType: "PgInteger";
385
+ data: number;
386
+ driverParam: string | number;
387
+ notNull: false;
388
+ hasDefault: false;
389
+ isPrimaryKey: false;
390
+ isAutoincrement: false;
391
+ hasRuntimeDefault: false;
392
+ enumValues: undefined;
393
+ baseColumn: never;
394
+ identity: undefined;
395
+ generated: undefined;
396
+ }, {}, {}>;
397
+ col: import("drizzle-orm/pg-core").PgColumn<{
398
+ name: "col";
399
+ tableName: "code_symbol";
400
+ dataType: "number";
401
+ columnType: "PgInteger";
402
+ data: number;
403
+ driverParam: string | number;
404
+ notNull: false;
405
+ hasDefault: false;
406
+ isPrimaryKey: false;
407
+ isAutoincrement: false;
408
+ hasRuntimeDefault: false;
409
+ enumValues: undefined;
410
+ baseColumn: never;
411
+ identity: undefined;
412
+ generated: undefined;
413
+ }, {}, {}>;
414
+ };
415
+ dialect: "pg";
416
+ }>;
417
+ export type CodeChunkInsert = typeof codeChunk.$inferInsert;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.codeSymbol = exports.codeDependency = exports.codeChunk = void 0;
4
+ const pg_core_1 = require("drizzle-orm/pg-core");
5
+ const vector = (0, pg_core_1.customType)({
6
+ dataType(config) { return `vector(${config?.dimensions ?? 1024})`; },
7
+ });
8
+ exports.codeChunk = (0, pg_core_1.pgTable)('code_chunk', {
9
+ id: (0, pg_core_1.uuid)('id').primaryKey().defaultRandom(),
10
+ serviceName: (0, pg_core_1.text)('serviceName').notNull(),
11
+ filePath: (0, pg_core_1.text)('filePath').notNull(),
12
+ startLine: (0, pg_core_1.integer)('startLine'),
13
+ content: (0, pg_core_1.text)('content').notNull(),
14
+ embedding: vector('embedding', { dimensions: 1024 }).notNull(),
15
+ lastModified: (0, pg_core_1.doublePrecision)('lastModified'),
16
+ contentHash: (0, pg_core_1.varchar)('contentHash', { length: 64 }),
17
+ });
18
+ exports.codeDependency = (0, pg_core_1.pgTable)('code_dependency', {
19
+ id: (0, pg_core_1.uuid)('id').primaryKey().defaultRandom(),
20
+ project: (0, pg_core_1.text)('project').notNull(),
21
+ fromFile: (0, pg_core_1.text)('fromFile').notNull(),
22
+ fromSymbol: (0, pg_core_1.text)('fromSymbol'),
23
+ toFile: (0, pg_core_1.text)('toFile').notNull(),
24
+ toSymbol: (0, pg_core_1.text)('toSymbol'),
25
+ depType: (0, pg_core_1.text)('depType').notNull(),
26
+ });
27
+ exports.codeSymbol = (0, pg_core_1.pgTable)('code_symbol', {
28
+ id: (0, pg_core_1.uuid)('id').primaryKey().defaultRandom(),
29
+ project: (0, pg_core_1.text)('project').notNull(),
30
+ filePath: (0, pg_core_1.text)('filePath').notNull(),
31
+ name: (0, pg_core_1.text)('name').notNull(),
32
+ kind: (0, pg_core_1.text)('kind').notNull(),
33
+ signature: (0, pg_core_1.text)('signature'),
34
+ line: (0, pg_core_1.integer)('line'),
35
+ col: (0, pg_core_1.integer)('col'),
36
+ });
@@ -13,6 +13,7 @@ export interface SymbolEntry {
13
13
  kind: 'function' | 'class' | 'interface' | 'type' | 'variable' | 'method';
14
14
  signature: string | null;
15
15
  line: number | null;
16
+ endLine: number | null;
16
17
  col: number | null;
17
18
  }
18
19
  export interface FileAnalysis {
@@ -20,5 +21,10 @@ export interface FileAnalysis {
20
21
  symbols: SymbolEntry[];
21
22
  }
22
23
  export interface IAstParser {
24
+ /**
25
+ * @deprecated Use analyzeProject instead for project-wide context.
26
+ */
23
27
  analyzeFile(filePath: string, serviceName: string, projectRoot?: string): Promise<FileAnalysis>;
28
+ analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string): Promise<Map<string, FileAnalysis>>;
29
+ clearCache(projectRoot?: string): void;
24
30
  }
@@ -4,8 +4,11 @@ export declare class AstParserService implements IAstParser {
4
4
  private logger;
5
5
  private morphProjects;
6
6
  constructor(logger?: LoggerService);
7
+ analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string): Promise<Map<string, FileAnalysis>>;
8
+ clearCache(projectRoot?: string): void;
7
9
  analyzeFile(filePath: string, serviceName: string, projectRoot?: string): Promise<FileAnalysis>;
8
10
  extractImportEdges(filePath: string, serviceName: string, projectRoot?: string): Promise<DepEdge[]>;
11
+ private createMorphProject;
9
12
  private getMorphProject;
10
13
  private extractEdges;
11
14
  private resolveFromSymbol;
@@ -51,6 +51,55 @@ class AstParserService {
51
51
  this.logger = logger;
52
52
  }
53
53
  // ── public interface ────────────────────────────────────────────────────────
54
+ async analyzeProject(filePaths, serviceName, projectRoot = process.cwd()) {
55
+ const t0 = Date.now();
56
+ const analysisMap = new Map();
57
+ const morphProject = this.createMorphProject(projectRoot);
58
+ for (const filePath of filePaths) {
59
+ if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
60
+ analysisMap.set(filePath, { edges: [], symbols: [] });
61
+ continue;
62
+ }
63
+ try {
64
+ morphProject.addSourceFileAtPath(filePath);
65
+ }
66
+ catch (err) {
67
+ this.logger.warn('AstParser: failed to add source file', { filePath, reason: err.message });
68
+ analysisMap.set(filePath, { edges: [], symbols: [] });
69
+ }
70
+ }
71
+ for (const sourceFile of morphProject.getSourceFiles()) {
72
+ const filePath = sourceFile.getFilePath();
73
+ try {
74
+ const edges = this.extractEdges(sourceFile, filePath, serviceName);
75
+ const symbols = this.extractSymbols(sourceFile, filePath, serviceName);
76
+ analysisMap.set(filePath, { edges, symbols });
77
+ }
78
+ catch (err) {
79
+ this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
80
+ analysisMap.set(filePath, { edges: [], symbols: [] });
81
+ }
82
+ }
83
+ // Release source file memory before returning
84
+ for (const sourceFile of morphProject.getSourceFiles()) {
85
+ morphProject.removeSourceFile(sourceFile);
86
+ }
87
+ this.logger.debug('AstParser: analyzeProject done', {
88
+ projectRoot, total: filePaths.length, parsed: analysisMap.size, ms: Date.now() - t0,
89
+ });
90
+ return analysisMap;
91
+ }
92
+ clearCache(projectRoot) {
93
+ if (projectRoot) {
94
+ const project = this.morphProjects.get(projectRoot);
95
+ if (project) {
96
+ this.morphProjects.delete(projectRoot);
97
+ }
98
+ }
99
+ else {
100
+ this.morphProjects.clear();
101
+ }
102
+ }
54
103
  async analyzeFile(filePath, serviceName, projectRoot = path.dirname(filePath)) {
55
104
  if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
56
105
  return { edges: [], symbols: [] };
@@ -77,20 +126,7 @@ class AstParserService {
77
126
  return edges;
78
127
  }
79
128
  // ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
80
- getMorphProject(projectRoot) {
81
- if (this.morphProjects.has(projectRoot)) {
82
- // Move to end = most recently used
83
- const cached = this.morphProjects.get(projectRoot);
84
- this.morphProjects.delete(projectRoot);
85
- this.morphProjects.set(projectRoot, cached);
86
- return cached;
87
- }
88
- if (this.morphProjects.size >= MORPH_CACHE_MAX) {
89
- // Evict least recently used (first entry in insertion-order Map)
90
- const lruKey = this.morphProjects.keys().next().value;
91
- this.morphProjects.delete(lruKey);
92
- this.logger.debug('AstParser: evicted LRU project', { lruKey });
93
- }
129
+ createMorphProject(projectRoot) {
94
130
  const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
95
131
  const hasTsconfig = fs.existsSync(tsconfigPath);
96
132
  this.logger.debug('AstParser: tsconfig lookup', { projectRoot, found: hasTsconfig });
@@ -117,6 +153,23 @@ class AstParserService {
117
153
  });
118
154
  project = new ts_morph_1.Project(FALLBACK_OPTS);
119
155
  }
156
+ return project;
157
+ }
158
+ getMorphProject(projectRoot) {
159
+ if (this.morphProjects.has(projectRoot)) {
160
+ // Move to end = most recently used
161
+ const cached = this.morphProjects.get(projectRoot);
162
+ this.morphProjects.delete(projectRoot);
163
+ this.morphProjects.set(projectRoot, cached);
164
+ return cached;
165
+ }
166
+ if (this.morphProjects.size >= MORPH_CACHE_MAX) {
167
+ // Evict least recently used (first entry in insertion-order Map)
168
+ const lruKey = this.morphProjects.keys().next().value;
169
+ this.morphProjects.delete(lruKey);
170
+ this.logger.debug('AstParser: evicted LRU project', { lruKey });
171
+ }
172
+ const project = this.createMorphProject(projectRoot);
120
173
  this.morphProjects.set(projectRoot, project);
121
174
  return project;
122
175
  }
@@ -223,7 +276,6 @@ class AstParserService {
223
276
  // ── private: symbol extraction ─────────────────────────────────────────────
224
277
  extractSymbols(sourceFile, filePath, serviceName) {
225
278
  const symbols = [];
226
- // Find the first line matching the keyword — skips leading decorators.
227
279
  const sig = (text, keyword) => {
228
280
  const found = text.split('\n').find(l => keyword.test(l));
229
281
  return found?.trim() ?? text.split('\n')[0].trim();
@@ -239,7 +291,11 @@ class AstParserService {
239
291
  continue;
240
292
  const nameNode = cls.getNameNode();
241
293
  const { line, col } = nameNode ? getPos(nameNode) : { line: cls.getStartLineNumber(), col: 1 };
242
- symbols.push({ project: serviceName, filePath, name, kind: 'class', signature: sig(cls.getText(), /\bclass\s/), line, col });
294
+ symbols.push({
295
+ project: serviceName, filePath, name, kind: 'class',
296
+ signature: sig(cls.getText(), /\bclass\s/),
297
+ line, endLine: cls.getEndLineNumber(), col,
298
+ });
243
299
  }
244
300
  for (const fn of sourceFile.getFunctions()) {
245
301
  const name = fn.getName();
@@ -247,20 +303,36 @@ class AstParserService {
247
303
  continue;
248
304
  const nameNode = fn.getNameNode();
249
305
  const { line, col } = nameNode ? getPos(nameNode) : { line: fn.getStartLineNumber(), col: 1 };
250
- symbols.push({ project: serviceName, filePath, name, kind: 'function', signature: sig(fn.getText(), /\bfunction\s/), line, col });
306
+ symbols.push({
307
+ project: serviceName, filePath, name, kind: 'function',
308
+ signature: sig(fn.getText(), /\bfunction\s/),
309
+ line, endLine: fn.getEndLineNumber(), col,
310
+ });
251
311
  }
252
312
  for (const iface of sourceFile.getInterfaces()) {
253
313
  const { line, col } = getPos(iface.getNameNode());
254
- symbols.push({ project: serviceName, filePath, name: iface.getName(), kind: 'interface', signature: sig(iface.getText(), /\binterface\s/), line, col });
314
+ symbols.push({
315
+ project: serviceName, filePath, name: iface.getName(), kind: 'interface',
316
+ signature: sig(iface.getText(), /\binterface\s/),
317
+ line, endLine: iface.getEndLineNumber(), col,
318
+ });
255
319
  }
256
320
  for (const ta of sourceFile.getTypeAliases()) {
257
321
  const { line, col } = getPos(ta.getNameNode());
258
- symbols.push({ project: serviceName, filePath, name: ta.getName(), kind: 'type', signature: sig(ta.getText(), /\btype\s/), line, col });
322
+ symbols.push({
323
+ project: serviceName, filePath, name: ta.getName(), kind: 'type',
324
+ signature: sig(ta.getText(), /\btype\s/),
325
+ line, endLine: ta.getEndLineNumber(), col,
326
+ });
259
327
  }
260
328
  for (const stmt of sourceFile.getVariableStatements()) {
261
329
  for (const decl of stmt.getDeclarations()) {
262
330
  const { line, col } = getPos(decl.getNameNode());
263
- symbols.push({ project: serviceName, filePath, name: decl.getName(), kind: 'variable', signature: sig(stmt.getText(), /\b(const|let|var)\s/), line, col });
331
+ symbols.push({
332
+ project: serviceName, filePath, name: decl.getName(), kind: 'variable',
333
+ signature: sig(stmt.getText(), /\b(const|let|var)\s/),
334
+ line, endLine: stmt.getEndLineNumber(), col,
335
+ });
264
336
  }
265
337
  }
266
338
  return symbols;
@@ -1,14 +1,21 @@
1
- import { EntityManager, EntityTarget, ObjectLiteral, Repository } from 'typeorm';
1
+ import type { NodePgDatabase, NodePgTransaction } from 'drizzle-orm/node-postgres';
2
+ import type { ExtractTablesWithRelations } from 'drizzle-orm/relations';
2
3
  import type { Config } from '../config';
4
+ import * as schema from '../db/schema';
5
+ type Schema = typeof schema;
6
+ export type DrizzleDb = NodePgDatabase<Schema>;
7
+ export type DrizzleTx = NodePgTransaction<Schema, ExtractTablesWithRelations<Schema>>;
3
8
  export declare class DatabaseService {
4
9
  private config;
5
- private dataSource;
10
+ private pool;
11
+ private _db;
6
12
  constructor(config: Config);
13
+ get orm(): DrizzleDb;
7
14
  query<T = any>(sql: string, params?: any[]): Promise<T>;
8
- transaction<T>(fn: (manager: EntityManager) => Promise<T>): Promise<T>;
9
- getRepository<T extends ObjectLiteral>(entity: EntityTarget<T>): Repository<T>;
15
+ transaction<T>(fn: (tx: DrizzleTx) => Promise<T>): Promise<T>;
10
16
  destroy(): Promise<void>;
11
17
  initDb(): Promise<void>;
12
18
  initDbWithSchema(): Promise<void>;
13
19
  private ensureDatabaseExists;
14
20
  }
21
+ export {};
@@ -1,105 +1,146 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.DatabaseService = void 0;
4
- const typeorm_1 = require("typeorm");
37
+ const node_postgres_1 = require("drizzle-orm/node-postgres");
5
38
  const pg_1 = require("pg");
6
- const db_1 = require("../db");
39
+ const schema = __importStar(require("../db/schema"));
7
40
  class DatabaseService {
8
41
  config;
9
- dataSource;
42
+ pool;
43
+ _db;
10
44
  constructor(config) {
11
45
  this.config = config;
12
- this.dataSource = new typeorm_1.DataSource({
13
- type: 'postgres',
14
- ...config.db,
15
- entities: [db_1.CodeChunk, db_1.CodeDependency, db_1.CodeSymbol],
16
- synchronize: false,
17
- });
18
46
  }
19
- query(sql, params) {
20
- return this.dataSource.query(sql, params);
47
+ get orm() { return this._db; }
48
+ async query(sql, params) {
49
+ const result = await this.pool.query(sql, params);
50
+ return result.rows;
21
51
  }
22
52
  transaction(fn) {
23
- return this.dataSource.transaction(fn);
24
- }
25
- getRepository(entity) {
26
- return this.dataSource.getRepository(entity);
53
+ return this._db.transaction(fn);
27
54
  }
28
55
  destroy() {
29
- if (!this.dataSource.isInitialized)
56
+ if (!this.pool)
30
57
  return Promise.resolve();
31
- return this.dataSource.destroy();
58
+ return this.pool.end();
32
59
  }
33
60
  async initDb() {
34
- await this.dataSource.initialize();
61
+ const { host, port, username, password, database } = this.config.db;
62
+ this.pool = new pg_1.Pool({ host, port, user: username, password, database });
63
+ this._db = (0, node_postgres_1.drizzle)(this.pool, { schema });
35
64
  }
36
65
  async initDbWithSchema() {
37
66
  await this.ensureDatabaseExists();
38
- await this.dataSource.initialize();
67
+ await this.initDb();
39
68
  // code_chunk table
40
- const [{ exists }] = await this.dataSource.query(`
69
+ const [{ exists }] = await this.query(`
41
70
  SELECT EXISTS (
42
71
  SELECT 1 FROM information_schema.tables
43
72
  WHERE table_schema = 'public' AND table_name = 'code_chunk'
44
73
  )
45
74
  `);
46
75
  if (!exists) {
47
- await this.dataSource.synchronize();
48
- await this.dataSource.query(`
76
+ await this.query(`
77
+ CREATE TABLE code_chunk (
78
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
79
+ "serviceName" TEXT NOT NULL,
80
+ "filePath" TEXT NOT NULL,
81
+ "startLine" INT,
82
+ content TEXT NOT NULL,
83
+ embedding vector(1024) NOT NULL,
84
+ "lastModified" DOUBLE PRECISION,
85
+ "contentHash" VARCHAR(64)
86
+ )
87
+ `);
88
+ await this.query(`
49
89
  CREATE INDEX idx_code_chunk_embedding
50
90
  ON code_chunk USING hnsw (embedding vector_cosine_ops)
51
91
  `);
52
92
  }
53
- await this.dataSource.query(`CREATE INDEX IF NOT EXISTS idx_code_chunk_service_file ON code_chunk ("serviceName", "filePath")`);
93
+ await this.query(`CREATE INDEX IF NOT EXISTS idx_code_chunk_service_file ON code_chunk ("serviceName", "filePath")`);
94
+ await this.query(`ALTER TABLE code_chunk ADD COLUMN IF NOT EXISTS "contentHash" varchar(64) NULL`);
54
95
  // code_dependency table
55
- const [{ existsDep }] = await this.dataSource.query(`
96
+ const [{ existsDep }] = await this.query(`
56
97
  SELECT EXISTS (
57
98
  SELECT 1 FROM information_schema.tables
58
99
  WHERE table_schema = 'public' AND table_name = 'code_dependency'
59
100
  ) AS "existsDep"
60
101
  `);
61
102
  if (!existsDep) {
62
- await this.dataSource.query(`
103
+ await this.query(`
63
104
  CREATE TABLE code_dependency (
64
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
65
- project TEXT NOT NULL,
66
- "fromFile" TEXT NOT NULL,
105
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
106
+ project TEXT NOT NULL,
107
+ "fromFile" TEXT NOT NULL,
67
108
  "fromSymbol" TEXT,
68
- "toFile" TEXT NOT NULL,
69
- "toSymbol" TEXT,
70
- "depType" TEXT NOT NULL
109
+ "toFile" TEXT NOT NULL,
110
+ "toSymbol" TEXT,
111
+ "depType" TEXT NOT NULL
71
112
  )
72
113
  `);
73
114
  }
74
- await this.dataSource.query(`CREATE INDEX IF NOT EXISTS idx_dep_project_from ON code_dependency (project, "fromFile")`);
75
- await this.dataSource.query(`CREATE INDEX IF NOT EXISTS idx_dep_project_to ON code_dependency (project, "toFile")`);
76
- await this.dataSource.query(`DROP INDEX IF EXISTS idx_dep_from`);
77
- await this.dataSource.query(`DROP INDEX IF EXISTS idx_dep_to`);
78
- await this.dataSource.query(`DROP INDEX IF EXISTS idx_dep_project`);
115
+ await this.query(`CREATE INDEX IF NOT EXISTS idx_dep_project_from ON code_dependency (project, "fromFile")`);
116
+ await this.query(`CREATE INDEX IF NOT EXISTS idx_dep_project_to ON code_dependency (project, "toFile")`);
117
+ await this.query(`DROP INDEX IF EXISTS idx_dep_from`);
118
+ await this.query(`DROP INDEX IF EXISTS idx_dep_to`);
119
+ await this.query(`DROP INDEX IF EXISTS idx_dep_project`);
79
120
  // code_symbol table
80
- const [{ existsSym }] = await this.dataSource.query(`
121
+ const [{ existsSym }] = await this.query(`
81
122
  SELECT EXISTS (
82
123
  SELECT 1 FROM information_schema.tables
83
124
  WHERE table_schema = 'public' AND table_name = 'code_symbol'
84
125
  ) AS "existsSym"
85
126
  `);
86
127
  if (!existsSym) {
87
- await this.dataSource.query(`
128
+ await this.query(`
88
129
  CREATE TABLE code_symbol (
89
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
90
- project TEXT NOT NULL,
130
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
131
+ project TEXT NOT NULL,
91
132
  "filePath" TEXT NOT NULL,
92
- name TEXT NOT NULL,
93
- kind TEXT NOT NULL,
94
- signature TEXT,
95
- line INT,
96
- col INT
133
+ name TEXT NOT NULL,
134
+ kind TEXT NOT NULL,
135
+ signature TEXT,
136
+ line INT,
137
+ col INT
97
138
  )
98
139
  `);
99
140
  }
100
- await this.dataSource.query(`CREATE INDEX IF NOT EXISTS idx_symbol_name ON code_symbol (name, project)`);
101
- await this.dataSource.query(`CREATE INDEX IF NOT EXISTS idx_symbol_file ON code_symbol (project, "filePath")`);
102
- await this.dataSource.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_symbol_unique ON code_symbol (project, "filePath", name, kind)`);
141
+ await this.query(`CREATE INDEX IF NOT EXISTS idx_symbol_name ON code_symbol (name, project)`);
142
+ await this.query(`CREATE INDEX IF NOT EXISTS idx_symbol_file ON code_symbol (project, "filePath")`);
143
+ await this.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_symbol_unique ON code_symbol (project, "filePath", name, kind)`);
103
144
  }
104
145
  async ensureDatabaseExists() {
105
146
  const { host, port, username, password, database } = this.config.db;
@@ -34,6 +34,8 @@ export declare class IndexerService {
34
34
  private writeDepEdges;
35
35
  scanAndIndex(dir: string, serviceName: string, force?: boolean, signal?: AbortSignal, preCollectedFiles?: string[]): Promise<IndexStats>;
36
36
  private getSplitter;
37
+ private getEnrichedPrompt;
37
38
  private embedText;
39
+ private static hashContent;
38
40
  private ingestFile;
39
41
  }
@@ -36,8 +36,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.IndexerService = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const crypto_1 = require("crypto");
39
40
  const textsplitters_1 = require("@langchain/textsplitters");
40
41
  const cli_progress_1 = require("cli-progress");
42
+ const drizzle_orm_1 = require("drizzle-orm");
41
43
  const db_1 = require("../db");
42
44
  const file_utils_1 = require("../file-utils");
43
45
  const LoggerService_1 = require("./LoggerService");
@@ -80,11 +82,11 @@ class IndexerService {
80
82
  }
81
83
  try {
82
84
  const { symbols: rawSymbols } = await this.ast.analyzeFile(absPath, serviceName, projectRoot);
83
- const symbols = [...new Map(rawSymbols.map(s => ({ ...s, filePath })).map(s => [`${s.name}:${s.kind}`, s])).values()];
84
- await this.db.transaction(async (manager) => {
85
- await manager.delete(db_1.CodeSymbol, { project: serviceName, filePath });
85
+ 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 }));
86
+ await this.db.transaction(async (tx) => {
87
+ 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)));
86
88
  if (symbols.length > 0) {
87
- await manager.createQueryBuilder().insert().into(db_1.CodeSymbol).values(symbols).execute();
89
+ await tx.insert(db_1.codeSymbol).values(symbols);
88
90
  }
89
91
  });
90
92
  processed++;
@@ -109,30 +111,29 @@ class IndexerService {
109
111
  .map(d => ({ name: d.name, path: path.join(rootDir, d.name) }))
110
112
  .filter(p => fs.existsSync(path.join(p.path, 'package.json')));
111
113
  }
112
- async writeDepEdges(serviceName, relFilePath, edges, manager) {
113
- const run = async (mgr) => {
114
- await mgr.delete(db_1.CodeDependency, { project: serviceName, fromFile: relFilePath });
114
+ async writeDepEdges(serviceName, relFilePath, edges, tx) {
115
+ const run = async (t) => {
116
+ 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)));
115
117
  if (edges.length > 0) {
116
- await mgr.createQueryBuilder().insert().into(db_1.CodeDependency).values(edges).execute();
118
+ await t.insert(db_1.codeDependency).values(edges);
117
119
  }
118
120
  };
119
- if (manager)
120
- await run(manager);
121
+ if (tx)
122
+ await run(tx);
121
123
  else
122
124
  await this.db.transaction(run);
123
125
  }
124
126
  async scanAndIndex(dir, serviceName, force = false, signal, preCollectedFiles) {
125
127
  const startTime = Date.now();
126
- const repo = this.db.getRepository(db_1.CodeChunk);
127
128
  if (force) {
128
- await this.db.query(`UPDATE code_chunk SET "lastModified" = NULL WHERE "serviceName" = $1`, [serviceName]);
129
+ await this.db.query(`UPDATE code_chunk SET "lastModified" = NULL, "contentHash" = NULL WHERE "serviceName" = $1`, [serviceName]);
129
130
  }
130
131
  const files = preCollectedFiles ?? (0, file_utils_1.collectFiles)(dir, (0, file_utils_1.loadIgnore)(dir), dir);
131
132
  const collectedRelPaths = new Set(files.map(f => path.relative(this.config.workspaceRoot, f)));
132
133
  const dbPaths = await this.db.query(`SELECT DISTINCT "filePath" FROM code_chunk WHERE "serviceName" = $1`, [serviceName]);
133
134
  for (const { filePath } of dbPaths) {
134
135
  if (!collectedRelPaths.has(filePath)) {
135
- await repo.delete({ serviceName, filePath });
136
+ await this.db.transaction(tx => tx.delete(db_1.codeChunk).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.codeChunk.serviceName, serviceName), (0, drizzle_orm_1.eq)(db_1.codeChunk.filePath, filePath))));
136
137
  await this.db.query(`DELETE FROM code_dependency WHERE project = $1 AND "fromFile" = $2`, [serviceName, filePath]);
137
138
  await this.db.query(`DELETE FROM code_dependency WHERE project = $1 AND "toFile" = $2`, [serviceName, filePath]);
138
139
  console.log(` Removed stale: ${path.basename(filePath)}`);
@@ -171,6 +172,8 @@ class IndexerService {
171
172
  const stats = {
172
173
  serviceName, indexed: 0, skipped: 0, failed: 0, chunks: 0, elapsedSec: 0, warnings,
173
174
  };
175
+ const mtimeRefreshBatch = [];
176
+ const analysisMap = await this.ast.analyzeProject(files, serviceName, dir);
174
177
  for (const filePath of files) {
175
178
  if (signal?.aborted) {
176
179
  bar.stop();
@@ -180,7 +183,14 @@ class IndexerService {
180
183
  }
181
184
  bar.update({ filename: path.relative(dir, filePath) });
182
185
  const mtimeMs = fs.statSync(filePath).mtimeMs;
183
- const result = await this.ingestFile(filePath, serviceName, mtimeMs, dir);
186
+ const analysis = analysisMap.get(filePath);
187
+ if (analysis === undefined && /\.(ts|tsx|js|jsx)$/.test(filePath) && !/\.d\.ts$/.test(filePath)) {
188
+ this.logger.warn('IndexerService: no pre-computed analysis for TS file', { file: filePath });
189
+ }
190
+ const result = await this.ingestFile(filePath, serviceName, mtimeMs, dir, analysis);
191
+ if (result.pendingMtimeRefresh) {
192
+ mtimeRefreshBatch.push({ relFilePath: result.pendingMtimeRefresh, mtimeMs });
193
+ }
184
194
  if (result.status === 'indexed')
185
195
  stats.indexed++;
186
196
  else if (result.status === 'skipped')
@@ -192,29 +202,71 @@ class IndexerService {
192
202
  }
193
203
  bar.stop();
194
204
  process.stdout.write('\x1B[J');
205
+ if (mtimeRefreshBatch.length > 0) {
206
+ try {
207
+ await this.db.query(`UPDATE code_chunk SET "lastModified" = t.mtime
208
+ FROM unnest($1::text[], $2::float8[]) AS t(fp, mtime)
209
+ WHERE "serviceName" = $3 AND "filePath" = t.fp`, [
210
+ mtimeRefreshBatch.map(r => r.relFilePath),
211
+ mtimeRefreshBatch.map(r => r.mtimeMs),
212
+ serviceName,
213
+ ]);
214
+ }
215
+ catch (err) {
216
+ this.logger.error('IndexerService: mtime refresh batch failed', { reason: err.message });
217
+ }
218
+ }
219
+ this.ast.clearCache(dir);
195
220
  stats.elapsedSec = Math.round((Date.now() - startTime) / 100) / 10;
196
221
  return stats;
197
222
  }
198
223
  getSplitter(filePath) {
199
224
  return SCHEMA_EXTENSIONS.test(filePath) ? this.schemaSplitter : this.jsSplitter;
200
225
  }
226
+ getEnrichedPrompt(chunk, analysis, serviceName, relPath) {
227
+ const { pageContent, metadata } = chunk;
228
+ const header = `Project: ${serviceName} | Path: ${relPath}`;
229
+ if (!analysis || analysis.symbols.length === 0) {
230
+ return `${header}\n${pageContent}`;
231
+ }
232
+ const chunkStartLine = metadata?.loc?.lines?.from ?? 1;
233
+ const chunkEndLine = metadata?.loc?.lines?.to ?? chunkStartLine;
234
+ const relevantSymbols = analysis.symbols.filter(s => s.line != null && s.endLine != null && s.line <= chunkEndLine && s.endLine >= chunkStartLine);
235
+ if (relevantSymbols.length === 0) {
236
+ return `${header}\n${pageContent}`;
237
+ }
238
+ const symbolsTag = `[Symbols: ${relevantSymbols.map(s => `${s.kind} ${s.name}`).join(', ')}]`;
239
+ return `${header}\n${symbolsTag}\n${pageContent}`;
240
+ }
201
241
  async embedText(prompt) {
202
242
  return this.embedder.embed(prompt);
203
243
  }
204
- async ingestFile(filePath, serviceName, mtimeMs, projectRoot) {
244
+ static hashContent(content) {
245
+ return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
246
+ }
247
+ async ingestFile(filePath, serviceName, mtimeMs, projectRoot, analysis) {
205
248
  try {
206
- const repo = this.db.getRepository(db_1.CodeChunk);
207
249
  const relFilePath = path.relative(this.config.workspaceRoot, filePath);
208
- const existing = await repo.findOne({
209
- select: ['id', 'lastModified'],
210
- where: { serviceName, filePath: relFilePath },
211
- });
250
+ const [existing] = await this.db.orm
251
+ .select({ id: db_1.codeChunk.id, lastModified: db_1.codeChunk.lastModified, contentHash: db_1.codeChunk.contentHash })
252
+ .from(db_1.codeChunk)
253
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.codeChunk.serviceName, serviceName), (0, drizzle_orm_1.eq)(db_1.codeChunk.filePath, relFilePath)))
254
+ .limit(1);
255
+ // Fast path: mtime unchanged — skip without reading file
212
256
  if (existing?.lastModified === mtimeMs) {
213
257
  return { status: 'skipped', chunks: 0 };
214
258
  }
215
259
  const t0 = Date.now();
216
- const { edges: rawEdges, symbols: rawSymbols } = await this.ast.analyzeFile(filePath, serviceName, projectRoot);
217
- // normalize edges to workspace-relative paths
260
+ const code = fs.readFileSync(filePath, 'utf-8');
261
+ const hash = IndexerService.hashContent(code);
262
+ // Content unchanged (mtime drifted — DB transfer, git clone, touch)
263
+ if (existing !== undefined && existing.contentHash !== null && existing.contentHash === hash) {
264
+ return { status: 'skipped', chunks: 0, pendingMtimeRefresh: relFilePath };
265
+ }
266
+ const tAst = Date.now();
267
+ const { edges: rawEdges, symbols: rawSymbols } = analysis ?? { edges: [], symbols: [] };
268
+ // astMs is near-zero — actual AST time is spent upfront in analyzeProject()
269
+ // Normalize edges to workspace-relative paths
218
270
  const edges = rawEdges
219
271
  .filter(e => e.toFile.startsWith(this.config.workspaceRoot))
220
272
  .map(e => ({
@@ -222,10 +274,9 @@ class IndexerService {
222
274
  fromFile: path.relative(this.config.workspaceRoot, e.fromFile),
223
275
  toFile: path.relative(this.config.workspaceRoot, e.toFile),
224
276
  }));
225
- // normalize symbols: replace absolute filePath with workspace-relative
226
- const symbols = [...new Map(rawSymbols.map(s => ({ ...s, filePath: relFilePath })).map(s => [`${s.name}:${s.kind}`, s])).values()];
227
- const depsMs = Date.now() - t0;
228
- const code = fs.readFileSync(filePath, 'utf-8');
277
+ // Normalize symbols: replace absolute filePath with workspace-relative, strip endLine (not stored)
278
+ 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 }));
279
+ const astMs = Date.now() - tAst;
229
280
  const chunks = await this.getSplitter(filePath).createDocuments([code]);
230
281
  const relPath = path.relative(projectRoot, filePath);
231
282
  const t2 = Date.now();
@@ -234,43 +285,36 @@ class IndexerService {
234
285
  const batch = chunks.slice(i, i + this.config.indexer.batchSize);
235
286
  const embedded = await Promise.all(batch.map(async (chunk) => {
236
287
  const startLine = (chunk.metadata?.loc?.lines?.from ?? 1);
237
- const prompt = `Project: ${serviceName} | Path: ${relPath}\n${chunk.pageContent}`;
288
+ const prompt = this.getEnrichedPrompt(chunk, analysis, serviceName, relPath);
238
289
  const embedding = await this.embedText(prompt);
239
- return repo.create({
290
+ return {
240
291
  serviceName, filePath: relFilePath, startLine,
241
292
  content: chunk.pageContent,
242
293
  embedding: `[${embedding}]`,
243
294
  lastModified: mtimeMs,
244
- });
295
+ contentHash: hash,
296
+ };
245
297
  }));
246
298
  records.push(...embedded);
247
299
  }
248
300
  const embedMs = Date.now() - t2;
249
301
  const t3 = Date.now();
250
- await this.db.transaction(async (manager) => {
251
- await manager.delete(db_1.CodeChunk, { serviceName, filePath: relFilePath });
302
+ await this.db.transaction(async (tx) => {
303
+ await tx.delete(db_1.codeChunk).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.codeChunk.serviceName, serviceName), (0, drizzle_orm_1.eq)(db_1.codeChunk.filePath, relFilePath)));
252
304
  for (let i = 0; i < records.length; i += this.config.indexer.batchSize) {
253
- await manager.createQueryBuilder()
254
- .insert()
255
- .into(db_1.CodeChunk)
256
- .values(records.slice(i, i + this.config.indexer.batchSize))
257
- .execute();
305
+ await tx.insert(db_1.codeChunk).values(records.slice(i, i + this.config.indexer.batchSize));
258
306
  }
259
- await this.writeDepEdges(serviceName, relFilePath, edges, manager);
260
- await manager.delete(db_1.CodeSymbol, { project: serviceName, filePath: relFilePath });
307
+ await this.writeDepEdges(serviceName, relFilePath, edges, tx);
308
+ 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)));
261
309
  if (symbols.length > 0) {
262
- await manager.createQueryBuilder()
263
- .insert()
264
- .into(db_1.CodeSymbol)
265
- .values(symbols)
266
- .execute();
310
+ await tx.insert(db_1.codeSymbol).values(symbols);
267
311
  }
268
312
  });
269
313
  const dbMs = Date.now() - t3;
270
314
  if (chunks.length > 0) {
271
315
  this.logger.debug('IndexerService: file indexed', {
272
316
  file: relFilePath, chunks: chunks.length,
273
- depsMs, embedMs, dbMs, totalMs: Date.now() - t0,
317
+ astMs, embedMs, dbMs, totalMs: Date.now() - t0,
274
318
  });
275
319
  }
276
320
  return { status: 'indexed', chunks: chunks.length };
@@ -63,6 +63,8 @@ Pick one workflow based on what you need:
63
63
  | **Flow audit** | "how does X connect to Y", "audit this flow", "trace the path" | search_codebase → **get_dependencies + get_dependents** → Grep |
64
64
  | **Impact** | "what would break if I change X", "what uses Z" | get_dependents → search_codebase for context |
65
65
 
66
+ > **No results?** Never conclude the code doesn't exist after a single attempt. Retry with \`threshold\` raised by 0.05–0.10 (e.g. 0.45 → 0.50 → 0.55) before giving up. The code may exist under a different name or domain term.
67
+
66
68
  ## Tools
67
69
 
68
70
  - **list_projects** — call this first to see what projects are indexed and their exact names.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codesearch-mcp",
3
- "version": "0.0.15-alpha",
3
+ "version": "0.0.16-alpha",
4
4
  "description": "Local semantic codebase search via MCP — indexes projects into pgvector, exposes search to Claude",
5
5
  "license": "MIT",
6
6
  "main": "dist/app.js",
@@ -34,14 +34,13 @@
34
34
  "@modelcontextprotocol/sdk": "^1.0.0",
35
35
  "cli-progress": "^3.12.0",
36
36
  "dotenv": "^17.4.2",
37
+ "drizzle-orm": "^0.45.2",
37
38
  "ignore": "^7.0.5",
38
39
  "ollama": "^0.5.0",
39
40
  "pg": "^8.13.0",
40
41
  "pino": "^10.3.1",
41
42
  "pino-roll": "^4.0.0",
42
- "reflect-metadata": "^0.2.2",
43
43
  "ts-morph": "^28.0.0",
44
- "typeorm": "^0.3.20",
45
44
  "zod": "^4.3.6"
46
45
  },
47
46
  "devDependencies": {