kotadb 2.2.0-next.20260205005118 → 2.2.0

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.
@@ -1,17 +1,14 @@
1
1
  /**
2
2
  * Database query layer for indexed data
3
- *
3
+ *
4
4
  * Local-only implementation using SQLite for all operations.
5
- *
5
+ *
6
6
  * @module @api/queries
7
7
  */
8
8
 
9
9
  import { randomUUID } from "node:crypto";
10
10
  import type { Reference } from "@indexer/reference-extractor";
11
- import type {
12
- Symbol as ExtractedSymbol,
13
- SymbolKind,
14
- } from "@indexer/symbol-extractor";
11
+ import type { Symbol as ExtractedSymbol, SymbolKind } from "@indexer/symbol-extractor";
15
12
  import { createLogger } from "@logging/logger.js";
16
13
  import type { IndexRequest, IndexedFile } from "@shared/types";
17
14
  import { detectLanguage } from "@shared/language-utils";
@@ -21,310 +18,253 @@ import { parseTsConfig, type PathMappings } from "@indexer/path-resolver.js";
21
18
 
22
19
  const logger = createLogger({ module: "api-queries" });
23
20
 
21
+
24
22
  /**
25
23
  * Normalize file path to consistent format for database storage.
26
- *
24
+ *
27
25
  * Rules:
28
26
  * - No leading slashes
29
27
  * - Forward slashes only (replace backslashes)
30
28
  * - No ./ prefix
31
29
  * - Consistent relative-to-repo-root format
32
- *
30
+ *
33
31
  * @param filePath - Absolute or relative file path
34
32
  * @returns Normalized relative path
35
33
  */
36
34
  function normalizePath(filePath: string): string {
37
- let normalized = filePath;
38
-
39
- // Replace backslashes with forward slashes
40
- normalized = normalized.replace(/\\/g, "/");
41
-
42
- // Remove leading slash if present
43
- if (normalized.startsWith("/")) {
44
- normalized = normalized.slice(1);
45
- }
46
-
47
- // Remove ./ prefix
48
- if (normalized.startsWith("./")) {
49
- normalized = normalized.slice(2);
50
- }
51
-
52
- return normalized;
35
+ let normalized = filePath;
36
+
37
+ // Replace backslashes with forward slashes
38
+ normalized = normalized.replace(/\\/g, '/');
39
+
40
+ // Remove leading slash if present
41
+ if (normalized.startsWith('/')) {
42
+ normalized = normalized.slice(1);
43
+ }
44
+
45
+ // Remove ./ prefix
46
+ if (normalized.startsWith('./')) {
47
+ normalized = normalized.slice(2);
48
+ }
49
+
50
+ return normalized;
53
51
  }
54
52
 
55
53
  export interface SearchOptions {
56
- repositoryId?: string;
57
- projectId?: string;
58
- limit?: number;
54
+ repositoryId?: string;
55
+ projectId?: string;
56
+ limit?: number;
59
57
  }
60
58
 
61
- /**
62
- * Represents a single matching line with surrounding context
63
- */
64
- export interface SnippetMatch {
65
- line: number; // 1-indexed line number
66
- content: string; // The matching line
67
- context_before: string[]; // N lines before
68
- context_after: string[]; // N lines after
69
- }
70
-
71
- /**
72
- * Extract line-based snippets from file content showing matches with context.
73
- *
74
- * Algorithm:
75
- * 1. Split content into lines
76
- * 2. Find all lines matching query (case-insensitive substring match)
77
- * 3. For each match, extract contextLines before/after
78
- * 4. Return array of SnippetMatch objects (one per matching line)
79
- *
80
- * NOTE: Does NOT merge overlapping contexts - each match gets separate snippet.
81
- *
82
- * @param content - Full file content
83
- * @param query - Search query term
84
- * @param contextLines - Lines of context before/after (default: 3, max: 10)
85
- * @returns Array of snippet matches with line numbers and context
86
- */
87
- export function extractLineSnippets(
88
- content: string,
89
- query: string,
90
- contextLines: number,
91
- ): SnippetMatch[] {
92
- if (!content || !query) return [];
93
-
94
- const lines = content.split("\n");
95
- const lowerQuery = query.toLowerCase();
96
- const matches: SnippetMatch[] = [];
97
-
98
- lines.forEach((line, index) => {
99
- if (line.toLowerCase().includes(lowerQuery)) {
100
- const lineNumber = index + 1; // 1-indexed
101
- const start = Math.max(0, index - contextLines);
102
- const end = Math.min(lines.length, index + contextLines + 1);
103
-
104
- matches.push({
105
- line: lineNumber,
106
- content: line,
107
- context_before: lines.slice(start, index),
108
- context_after: lines.slice(index + 1, end),
109
- });
110
- }
111
- });
112
-
113
- return matches;
114
- }
115
59
  // ============================================================================
116
60
  // Internal implementations that accept a database parameter
117
61
  // These are used by both the new API and backward-compatible aliases
118
62
  // ============================================================================
119
63
 
120
64
  function saveIndexedFilesInternal(
121
- db: KotaDatabase,
122
- files: IndexedFile[],
123
- repositoryId: string,
65
+ db: KotaDatabase,
66
+ files: IndexedFile[],
67
+ repositoryId: string,
124
68
  ): number {
125
- if (files.length === 0) {
126
- return 0;
127
- }
69
+ if (files.length === 0) {
70
+ return 0;
71
+ }
128
72
 
129
- let count = 0;
73
+ let count = 0;
130
74
 
131
- db.transaction(() => {
132
- const stmt = db.prepare(`
75
+ db.transaction(() => {
76
+ const stmt = db.prepare(`
133
77
  INSERT OR REPLACE INTO indexed_files (
134
78
  id, repository_id, path, content, language,
135
79
  size_bytes, content_hash, indexed_at, metadata
136
80
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
137
81
  `);
138
82
 
139
- for (const file of files) {
140
- const id = randomUUID();
141
- const language = detectLanguage(file.path);
142
- const sizeBytes = new TextEncoder().encode(file.content).length;
143
- const indexedAt = file.indexedAt
144
- ? file.indexedAt.toISOString()
145
- : new Date().toISOString();
146
- const metadata = JSON.stringify({
147
- dependencies: file.dependencies || [],
148
- });
149
-
150
- stmt.run([
151
- id,
152
- repositoryId,
153
- file.path,
154
- file.content,
155
- language,
156
- sizeBytes,
157
- null, // content_hash
158
- indexedAt,
159
- metadata,
160
- ]);
161
- count++;
162
- }
163
- });
164
-
165
- logger.info("Saved indexed files to SQLite", { count, repositoryId });
166
- return count;
83
+ for (const file of files) {
84
+ const id = randomUUID();
85
+ const language = detectLanguage(file.path);
86
+ const sizeBytes = new TextEncoder().encode(file.content).length;
87
+ const indexedAt = file.indexedAt ? file.indexedAt.toISOString() : new Date().toISOString();
88
+ const metadata = JSON.stringify({ dependencies: file.dependencies || [] });
89
+
90
+ stmt.run([
91
+ id,
92
+ repositoryId,
93
+ file.path,
94
+ file.content,
95
+ language,
96
+ sizeBytes,
97
+ null, // content_hash
98
+ indexedAt,
99
+ metadata
100
+ ]);
101
+ count++;
102
+ }
103
+ });
104
+
105
+ logger.info("Saved indexed files to SQLite", { count, repositoryId });
106
+ return count;
167
107
  }
168
108
 
169
109
  function storeSymbolsInternal(
170
- db: KotaDatabase,
171
- symbols: ExtractedSymbol[],
172
- fileId: string,
110
+ db: KotaDatabase,
111
+ symbols: ExtractedSymbol[],
112
+ fileId: string,
173
113
  ): number {
174
- if (symbols.length === 0) {
175
- return 0;
176
- }
114
+ if (symbols.length === 0) {
115
+ return 0;
116
+ }
177
117
 
178
- // Get repository_id from the file
179
- const fileResult = db.queryOne<{ repository_id: string }>(
180
- "SELECT repository_id FROM indexed_files WHERE id = ?",
181
- [fileId],
182
- );
118
+ // Get repository_id from the file
119
+ const fileResult = db.queryOne<{ repository_id: string }>(
120
+ "SELECT repository_id FROM indexed_files WHERE id = ?",
121
+ [fileId]
122
+ );
183
123
 
184
- if (!fileResult) {
185
- throw new Error(`File not found: ${fileId}`);
186
- }
124
+ if (!fileResult) {
125
+ throw new Error(`File not found: ${fileId}`);
126
+ }
187
127
 
188
- const repositoryId = fileResult.repository_id;
189
- let count = 0;
128
+ const repositoryId = fileResult.repository_id;
129
+ let count = 0;
190
130
 
191
- db.transaction(() => {
192
- const stmt = db.prepare(`
131
+ db.transaction(() => {
132
+ const stmt = db.prepare(`
193
133
  INSERT OR REPLACE INTO indexed_symbols (
194
134
  id, file_id, repository_id, name, kind,
195
135
  line_start, line_end, signature, documentation, metadata
196
136
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
197
137
  `);
198
138
 
199
- for (const symbol of symbols) {
200
- const id = randomUUID();
201
- const metadata = JSON.stringify({
202
- column_start: symbol.columnStart,
203
- column_end: symbol.columnEnd,
204
- is_exported: symbol.isExported,
205
- is_async: symbol.isAsync,
206
- access_modifier: symbol.accessModifier,
207
- });
208
-
209
- stmt.run([
210
- id,
211
- fileId,
212
- repositoryId,
213
- symbol.name,
214
- symbol.kind,
215
- symbol.lineStart,
216
- symbol.lineEnd,
217
- symbol.signature || null,
218
- symbol.documentation || null,
219
- metadata,
220
- ]);
221
- count++;
222
- }
223
- });
224
-
225
- logger.info("Stored symbols to SQLite", { count, fileId });
226
- return count;
139
+ for (const symbol of symbols) {
140
+ const id = randomUUID();
141
+ const metadata = JSON.stringify({
142
+ column_start: symbol.columnStart,
143
+ column_end: symbol.columnEnd,
144
+ is_exported: symbol.isExported,
145
+ is_async: symbol.isAsync,
146
+ access_modifier: symbol.accessModifier,
147
+ });
148
+
149
+ stmt.run([
150
+ id,
151
+ fileId,
152
+ repositoryId,
153
+ symbol.name,
154
+ symbol.kind,
155
+ symbol.lineStart,
156
+ symbol.lineEnd,
157
+ symbol.signature || null,
158
+ symbol.documentation || null,
159
+ metadata
160
+ ]);
161
+ count++;
162
+ }
163
+ });
164
+
165
+ logger.info("Stored symbols to SQLite", { count, fileId });
166
+ return count;
227
167
  }
228
168
 
229
169
  function storeReferencesInternal(
230
- db: KotaDatabase,
231
- fileId: string,
232
- repositoryId: string,
233
- filePath: string,
234
- references: Reference[],
235
- allFiles: Array<{ path: string }>,
236
- pathMappings?: PathMappings | null,
170
+ db: KotaDatabase,
171
+ fileId: string,
172
+ repositoryId: string,
173
+ filePath: string,
174
+ references: Reference[],
175
+ allFiles: Array<{ path: string }>,
176
+ pathMappings?: PathMappings | null,
237
177
  ): number {
238
- if (references.length === 0) {
239
- return 0;
240
- }
178
+ if (references.length === 0) {
179
+ return 0;
180
+ }
241
181
 
242
- let count = 0;
182
+ let count = 0;
243
183
 
244
- db.transaction(() => {
245
- // First, delete existing references for this file
246
- db.run("DELETE FROM indexed_references WHERE file_id = ?", [fileId]);
184
+ db.transaction(() => {
185
+ // First, delete existing references for this file
186
+ db.run("DELETE FROM indexed_references WHERE file_id = ?", [fileId]);
247
187
 
248
- const stmt = db.prepare(`
188
+ const stmt = db.prepare(`
249
189
  INSERT INTO indexed_references (
250
190
  id, file_id, repository_id, symbol_name, target_symbol_id,
251
191
  target_file_path, line_number, column_number, reference_type, metadata
252
192
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
253
193
  `);
254
194
 
255
- for (const ref of references) {
256
- const id = randomUUID();
257
- const metadata = JSON.stringify({
258
- target_name: ref.targetName,
259
- column_number: ref.columnNumber,
260
- ...ref.metadata,
261
- });
262
-
263
- // Resolve target_file_path for import references
264
- let targetFilePath: string | null = null;
265
- if (ref.referenceType === "import" && ref.metadata?.importSource) {
266
- const resolved = resolveImport(
267
- ref.metadata.importSource,
268
- filePath,
269
- allFiles,
270
- pathMappings,
271
- );
272
-
273
- // Normalize path if resolved
274
- if (resolved) {
275
- targetFilePath = normalizePath(resolved);
276
- }
277
- }
278
-
279
- stmt.run([
280
- id,
281
- fileId,
282
- repositoryId,
283
- ref.targetName || "unknown",
284
- null, // target_symbol_id - deferred
285
- targetFilePath, // NOW RESOLVED for imports
286
- ref.lineNumber,
287
- ref.columnNumber || 0,
288
- ref.referenceType,
289
- metadata,
290
- ]);
291
- count++;
292
- }
293
- });
294
-
295
- logger.info("Stored references to SQLite", { count, fileId });
296
- return count;
195
+ for (const ref of references) {
196
+ const id = randomUUID();
197
+ const metadata = JSON.stringify({
198
+ target_name: ref.targetName,
199
+ column_number: ref.columnNumber,
200
+ ...ref.metadata,
201
+ });
202
+
203
+ // Resolve target_file_path for import references
204
+ let targetFilePath: string | null = null;
205
+ if (ref.referenceType === 'import' && ref.metadata?.importSource) {
206
+ const resolved = resolveImport(
207
+ ref.metadata.importSource,
208
+ filePath,
209
+ allFiles,
210
+ pathMappings
211
+ );
212
+
213
+ // Normalize path if resolved
214
+ if (resolved) {
215
+ targetFilePath = normalizePath(resolved);
216
+ }
217
+ }
218
+
219
+ stmt.run([
220
+ id,
221
+ fileId,
222
+ repositoryId,
223
+ ref.targetName || "unknown",
224
+ null, // target_symbol_id - deferred
225
+ targetFilePath, // NOW RESOLVED for imports
226
+ ref.lineNumber,
227
+ ref.columnNumber || 0,
228
+ ref.referenceType,
229
+ metadata
230
+ ]);
231
+ count++;
232
+ }
233
+ });
234
+
235
+ logger.info("Stored references to SQLite", { count, fileId });
236
+ return count;
297
237
  }
298
238
 
299
239
  /**
300
240
  * Escape a search term for use in SQLite FTS5 MATCH clause.
301
241
  * Wraps the entire term in double quotes for exact phrase matching.
302
242
  * Escapes internal double quotes by doubling them.
303
- *
243
+ *
304
244
  * This ensures that:
305
245
  * - Multi-word searches match adjacent words in order ("hello world")
306
246
  * - Hyphenated terms don't trigger FTS5 operator parsing ("mom-and-pop")
307
247
  * - FTS5 keywords (AND, OR, NOT) are treated as literals, not operators
308
- *
248
+ *
309
249
  * @param term - Raw search term from user input
310
250
  * @returns Escaped term safe for FTS5 MATCH clause
311
251
  */
312
252
  function escapeFts5Term(term: string): string {
313
- // Escape internal double quotes by doubling them
314
- const escaped = term.replace(/"/g, '""');
315
- // Wrap in double quotes for exact phrase matching
316
- return `"${escaped}"`;
253
+ // Escape internal double quotes by doubling them
254
+ const escaped = term.replace(/"/g, '""');
255
+ // Wrap in double quotes for exact phrase matching
256
+ return `"${escaped}"`;
317
257
  }
318
258
 
319
259
  function searchFilesInternal(
320
- db: KotaDatabase,
321
- term: string,
322
- repositoryId: string | undefined,
323
- limit: number,
260
+ db: KotaDatabase,
261
+ term: string,
262
+ repositoryId: string | undefined,
263
+ limit: number,
324
264
  ): IndexedFile[] {
325
- const hasRepoFilter = repositoryId !== undefined;
326
- const sql = hasRepoFilter
327
- ? `
265
+ const hasRepoFilter = repositoryId !== undefined;
266
+ const sql = hasRepoFilter
267
+ ? `
328
268
  SELECT
329
269
  f.id,
330
270
  f.repository_id,
@@ -340,7 +280,7 @@ function searchFilesInternal(
340
280
  ORDER BY bm25(indexed_files_fts)
341
281
  LIMIT ?
342
282
  `
343
- : `
283
+ : `
344
284
  SELECT
345
285
  f.id,
346
286
  f.repository_id,
@@ -356,40 +296,38 @@ function searchFilesInternal(
356
296
  LIMIT ?
357
297
  `;
358
298
 
359
- const escapedTerm = escapeFts5Term(term);
360
- const params = hasRepoFilter
361
- ? [escapedTerm, repositoryId, limit]
362
- : [escapedTerm, limit];
363
- const rows = db.query<{
364
- id: string;
365
- repository_id: string;
366
- path: string;
367
- content: string;
368
- metadata: string;
369
- indexed_at: string;
370
- }>(sql, params);
371
-
372
- return rows.map((row) => {
373
- const metadata = JSON.parse(row.metadata || "{}");
374
- return {
375
- id: row.id,
376
- projectRoot: row.repository_id,
377
- path: row.path,
378
- content: row.content,
379
- dependencies: metadata.dependencies || [],
380
- indexedAt: new Date(row.indexed_at),
381
- };
382
- });
299
+ const escapedTerm = escapeFts5Term(term);
300
+ const params = hasRepoFilter ? [escapedTerm, repositoryId, limit] : [escapedTerm, limit];
301
+ const rows = db.query<{
302
+ id: string;
303
+ repository_id: string;
304
+ path: string;
305
+ content: string;
306
+ metadata: string;
307
+ indexed_at: string;
308
+ }>(sql, params);
309
+
310
+ return rows.map((row) => {
311
+ const metadata = JSON.parse(row.metadata || '{}');
312
+ return {
313
+ id: row.id,
314
+ projectRoot: row.repository_id,
315
+ path: row.path,
316
+ content: row.content,
317
+ dependencies: metadata.dependencies || [],
318
+ indexedAt: new Date(row.indexed_at),
319
+ };
320
+ });
383
321
  }
384
322
 
385
323
  function listRecentFilesInternal(
386
- db: KotaDatabase,
387
- limit: number,
388
- repositoryId?: string,
324
+ db: KotaDatabase,
325
+ limit: number,
326
+ repositoryId?: string,
389
327
  ): IndexedFile[] {
390
- const hasRepoFilter = repositoryId !== undefined;
391
- const sql = hasRepoFilter
392
- ? `
328
+ const hasRepoFilter = repositoryId !== undefined;
329
+ const sql = hasRepoFilter
330
+ ? `
393
331
  SELECT
394
332
  id, repository_id, path, content, metadata, indexed_at
395
333
  FROM indexed_files
@@ -397,7 +335,7 @@ function listRecentFilesInternal(
397
335
  ORDER BY indexed_at DESC
398
336
  LIMIT ?
399
337
  `
400
- : `
338
+ : `
401
339
  SELECT
402
340
  id, repository_id, path, content, metadata, indexed_at
403
341
  FROM indexed_files
@@ -405,92 +343,94 @@ function listRecentFilesInternal(
405
343
  LIMIT ?
406
344
  `;
407
345
 
408
- const params = hasRepoFilter ? [repositoryId, limit] : [limit];
409
- const rows = db.query<{
410
- id: string;
411
- repository_id: string;
412
- path: string;
413
- content: string;
414
- metadata: string;
415
- indexed_at: string;
416
- }>(sql, params);
417
-
418
- return rows.map((row) => {
419
- const metadata = JSON.parse(row.metadata || "{}");
420
- return {
421
- id: row.id,
422
- projectRoot: row.repository_id,
423
- path: row.path,
424
- content: row.content,
425
- dependencies: metadata.dependencies || [],
426
- indexedAt: new Date(row.indexed_at),
427
- };
428
- });
346
+ const params = hasRepoFilter ? [repositoryId, limit] : [limit];
347
+ const rows = db.query<{
348
+ id: string;
349
+ repository_id: string;
350
+ path: string;
351
+ content: string;
352
+ metadata: string;
353
+ indexed_at: string;
354
+ }>(sql, params);
355
+
356
+ return rows.map((row) => {
357
+ const metadata = JSON.parse(row.metadata || '{}');
358
+ return {
359
+ id: row.id,
360
+ projectRoot: row.repository_id,
361
+ path: row.path,
362
+ content: row.content,
363
+ dependencies: metadata.dependencies || [],
364
+ indexedAt: new Date(row.indexed_at),
365
+ };
366
+ });
429
367
  }
430
368
 
431
369
  function resolveFilePathInternal(
432
- db: KotaDatabase,
433
- filePath: string,
434
- repositoryId: string,
370
+ db: KotaDatabase,
371
+ filePath: string,
372
+ repositoryId: string,
435
373
  ): string | null {
436
- const sql = `
374
+ const sql = `
437
375
  SELECT id
438
376
  FROM indexed_files
439
377
  WHERE repository_id = ? AND path = ?
440
378
  LIMIT 1
441
379
  `;
442
380
 
443
- const result = db.queryOne<{ id: string }>(sql, [repositoryId, filePath]);
444
- return result?.id || null;
381
+ const result = db.queryOne<{ id: string }>(sql, [repositoryId, filePath]);
382
+ return result?.id || null;
445
383
  }
446
384
 
447
385
  function ensureRepositoryInternal(
448
- db: KotaDatabase,
449
- fullName: string,
450
- gitUrl?: string,
451
- defaultBranch?: string,
386
+ db: KotaDatabase,
387
+ fullName: string,
388
+ gitUrl?: string,
389
+ defaultBranch?: string,
452
390
  ): string {
453
- // Check if repository already exists
454
- const existing = db.queryOne<{ id: string }>(
455
- "SELECT id FROM repositories WHERE full_name = ?",
456
- [fullName],
457
- );
458
-
459
- if (existing) {
460
- logger.debug("Repository already exists in SQLite", {
461
- fullName,
462
- id: existing.id,
463
- });
464
- return existing.id;
465
- }
466
-
467
- // Create new repository
468
- const id = randomUUID();
469
- const name = fullName.split("/").pop() || fullName;
470
- const now = new Date().toISOString();
471
-
472
- db.run(
473
- `
391
+ // Check if repository already exists
392
+ const existing = db.queryOne<{ id: string }>(
393
+ "SELECT id FROM repositories WHERE full_name = ?",
394
+ [fullName]
395
+ );
396
+
397
+ if (existing) {
398
+ logger.debug("Repository already exists in SQLite", { fullName, id: existing.id });
399
+ return existing.id;
400
+ }
401
+
402
+ // Create new repository
403
+ const id = randomUUID();
404
+ const name = fullName.split("/").pop() || fullName;
405
+ const now = new Date().toISOString();
406
+
407
+ db.run(`
474
408
  INSERT INTO repositories (id, name, full_name, git_url, default_branch, created_at, updated_at)
475
409
  VALUES (?, ?, ?, ?, ?, ?, ?)
476
- `,
477
- [id, name, fullName, gitUrl || null, defaultBranch || "main", now, now],
478
- );
479
-
480
- logger.info("Created repository in SQLite", { fullName, id });
481
- return id;
410
+ `, [
411
+ id,
412
+ name,
413
+ fullName,
414
+ gitUrl || null,
415
+ defaultBranch || "main",
416
+ now,
417
+ now
418
+ ]);
419
+
420
+ logger.info("Created repository in SQLite", { fullName, id });
421
+ return id;
482
422
  }
483
423
 
484
424
  function updateRepositoryLastIndexedInternal(
485
- db: KotaDatabase,
486
- repositoryId: string,
425
+ db: KotaDatabase,
426
+ repositoryId: string,
487
427
  ): void {
488
- const now = new Date().toISOString();
489
- db.run(
490
- "UPDATE repositories SET last_indexed_at = ?, updated_at = ? WHERE id = ?",
491
- [now, now, repositoryId],
492
- );
493
- logger.debug("Updated repository last_indexed_at", { repositoryId });
428
+ const now = new Date().toISOString();
429
+ db.run(
430
+ "UPDATE repositories SET last_indexed_at = ?, updated_at = ? WHERE id = ?",
431
+ [now, now, repositoryId]
432
+ );
433
+ logger.debug("Updated repository last_indexed_at", { repositoryId });
494
434
  }
495
435
 
496
436
  // ============================================================================
@@ -499,16 +439,16 @@ function updateRepositoryLastIndexedInternal(
499
439
 
500
440
  /**
501
441
  * Save indexed files to SQLite database
502
- *
442
+ *
503
443
  * @param files - Array of indexed files
504
444
  * @param repositoryId - Repository UUID
505
445
  * @returns Number of files saved
506
446
  */
507
447
  export function saveIndexedFiles(
508
- files: IndexedFile[],
509
- repositoryId: string,
448
+ files: IndexedFile[],
449
+ repositoryId: string,
510
450
  ): number {
511
- return saveIndexedFilesInternal(getGlobalDatabase(), files, repositoryId);
451
+ return saveIndexedFilesInternal(getGlobalDatabase(), files, repositoryId);
512
452
  }
513
453
 
514
454
  /**
@@ -519,10 +459,10 @@ export function saveIndexedFiles(
519
459
  * @returns Number of symbols stored
520
460
  */
521
461
  export function storeSymbols(
522
- symbols: ExtractedSymbol[],
523
- fileId: string,
462
+ symbols: ExtractedSymbol[],
463
+ fileId: string,
524
464
  ): number {
525
- return storeSymbolsInternal(getGlobalDatabase(), symbols, fileId);
465
+ return storeSymbolsInternal(getGlobalDatabase(), symbols, fileId);
526
466
  }
527
467
 
528
468
  /**
@@ -533,33 +473,33 @@ export function storeSymbols(
533
473
  * @returns Number of references stored
534
474
  */
535
475
  export function storeReferences(
536
- fileId: string,
537
- filePath: string,
538
- references: Reference[],
539
- allFiles: Array<{ path: string }>,
540
- pathMappings?: PathMappings | null,
476
+ fileId: string,
477
+ filePath: string,
478
+ references: Reference[],
479
+ allFiles: Array<{ path: string }>,
480
+ pathMappings?: PathMappings | null
541
481
  ): number {
542
- const db = getGlobalDatabase();
543
-
544
- // Get repository_id from file
545
- const result = db.queryOne<{ repository_id: string }>(
546
- "SELECT repository_id FROM indexed_files WHERE id = ?",
547
- [fileId],
548
- );
549
-
550
- if (!result) {
551
- throw new Error(`File not found: ${fileId}`);
552
- }
553
-
554
- return storeReferencesInternal(
555
- db,
556
- fileId,
557
- result.repository_id,
558
- filePath,
559
- references,
560
- allFiles,
561
- pathMappings,
562
- );
482
+ const db = getGlobalDatabase();
483
+
484
+ // Get repository_id from file
485
+ const result = db.queryOne<{ repository_id: string }>(
486
+ "SELECT repository_id FROM indexed_files WHERE id = ?",
487
+ [fileId]
488
+ );
489
+
490
+ if (!result) {
491
+ throw new Error(`File not found: ${fileId}`);
492
+ }
493
+
494
+ return storeReferencesInternal(
495
+ db,
496
+ fileId,
497
+ result.repository_id,
498
+ filePath,
499
+ references,
500
+ allFiles,
501
+ pathMappings
502
+ );
563
503
  }
564
504
 
565
505
  /**
@@ -570,16 +510,11 @@ export function storeReferences(
570
510
  * @returns Array of matching indexed files
571
511
  */
572
512
  export function searchFiles(
573
- term: string,
574
- options: SearchOptions = {},
513
+ term: string,
514
+ options: SearchOptions = {},
575
515
  ): IndexedFile[] {
576
- const limit = Math.min(Math.max(options.limit ?? 20, 1), 100);
577
- return searchFilesInternal(
578
- getGlobalDatabase(),
579
- term,
580
- options.repositoryId,
581
- limit,
582
- );
516
+ const limit = Math.min(Math.max(options.limit ?? 20, 1), 100);
517
+ return searchFilesInternal(getGlobalDatabase(), term, options.repositoryId, limit);
583
518
  }
584
519
 
585
520
  /**
@@ -589,10 +524,10 @@ export function searchFiles(
589
524
  * @returns Array of recently indexed files
590
525
  */
591
526
  export function listRecentFiles(
592
- limit: number,
593
- repositoryId?: string,
527
+ limit: number,
528
+ repositoryId?: string,
594
529
  ): IndexedFile[] {
595
- return listRecentFilesInternal(getGlobalDatabase(), limit, repositoryId);
530
+ return listRecentFilesInternal(getGlobalDatabase(), limit, repositoryId);
596
531
  }
597
532
 
598
533
  /**
@@ -603,16 +538,16 @@ export function listRecentFiles(
603
538
  * @returns File UUID or null if not found
604
539
  */
605
540
  export function resolveFilePath(
606
- filePath: string,
607
- repositoryId: string,
541
+ filePath: string,
542
+ repositoryId: string,
608
543
  ): string | null {
609
- return resolveFilePathInternal(getGlobalDatabase(), filePath, repositoryId);
544
+ return resolveFilePathInternal(getGlobalDatabase(), filePath, repositoryId);
610
545
  }
611
546
 
612
547
  export interface DependencyResult {
613
- direct: string[];
614
- indirect: Record<string, string[]>;
615
- cycles: string[][];
548
+ direct: string[];
549
+ indirect: Record<string, string[]>;
550
+ cycles: string[][];
616
551
  }
617
552
 
618
553
  /**
@@ -635,27 +570,27 @@ export interface DependencyResult {
635
570
  * @returns Dependency result with direct/indirect relationships and cycles
636
571
  */
637
572
  export function queryDependents(
638
- fileId: string,
639
- depth: number,
640
- includeTests: boolean,
641
- referenceTypes: string[] = ["import", "re_export", "export_all"],
573
+ fileId: string,
574
+ depth: number,
575
+ includeTests: boolean,
576
+ referenceTypes: string[] = ["import", "re_export", "export_all"],
642
577
  ): DependencyResult {
643
- const db = getGlobalDatabase();
644
-
645
- // Get repository_id and path for target file
646
- const fileRecord = db.queryOne<{ repository_id: string; path: string }>(
647
- "SELECT repository_id, path FROM indexed_files WHERE id = ?",
648
- [fileId],
649
- );
650
-
651
- if (!fileRecord) {
652
- throw new Error(`File not found: ${fileId}`);
653
- }
654
-
655
- // Build IN clause placeholders for reference types
656
- const refTypePlaceholders = referenceTypes.map(() => "?").join(", ");
657
-
658
- const sql = `
578
+ const db = getGlobalDatabase();
579
+
580
+ // Get repository_id and path for target file
581
+ const fileRecord = db.queryOne<{ repository_id: string; path: string }>(
582
+ "SELECT repository_id, path FROM indexed_files WHERE id = ?",
583
+ [fileId]
584
+ );
585
+
586
+ if (!fileRecord) {
587
+ throw new Error(`File not found: ${fileId}`);
588
+ }
589
+
590
+ // Build IN clause placeholders for reference types
591
+ const refTypePlaceholders = referenceTypes.map(() => "?").join(", ");
592
+
593
+ const sql = `
659
594
  WITH RECURSIVE
660
595
  dependents AS (
661
596
  SELECT
@@ -710,27 +645,22 @@ export function queryDependents(
710
645
  FROM cycles
711
646
  ORDER BY depth ASC, file_path ASC
712
647
  `;
713
-
714
- // Build params array: [refTypes..., repoId, path, refTypes..., repoId, depth, refTypes..., repoId, depth]
715
- const results = db.query<{
716
- file_path: string | null;
717
- depth: number | null;
718
- cycle_path: string | null;
719
- }>(sql, [
720
- ...referenceTypes,
721
- fileRecord.repository_id,
722
- fileRecord.path,
723
- ...referenceTypes,
724
- fileRecord.repository_id,
725
- depth,
726
- ...referenceTypes,
727
- fileRecord.repository_id,
728
- depth,
729
- ]);
730
-
731
- return processDepthResults(results, includeTests);
648
+
649
+ // Build params array: [refTypes..., repoId, path, refTypes..., repoId, depth, refTypes..., repoId, depth]
650
+ const results = db.query<{
651
+ file_path: string | null;
652
+ depth: number | null;
653
+ cycle_path: string | null;
654
+ }>(sql, [
655
+ ...referenceTypes, fileRecord.repository_id, fileRecord.path,
656
+ ...referenceTypes, fileRecord.repository_id, depth,
657
+ ...referenceTypes, fileRecord.repository_id, depth
658
+ ]);
659
+
660
+ return processDepthResults(results, includeTests);
732
661
  }
733
662
 
663
+
734
664
  /**
735
665
  * Query files that the given file depends on (forward lookup).
736
666
  *
@@ -742,26 +672,26 @@ export function queryDependents(
742
672
  * @returns Dependency result with direct/indirect relationships and cycles
743
673
  */
744
674
  export function queryDependencies(
745
- fileId: string,
746
- depth: number,
747
- referenceTypes: string[] = ["import", "re_export", "export_all"],
675
+ fileId: string,
676
+ depth: number,
677
+ referenceTypes: string[] = ["import", "re_export", "export_all"],
748
678
  ): DependencyResult {
749
- const db = getGlobalDatabase();
750
-
751
- // Get repository_id for source file
752
- const fileRecord = db.queryOne<{ repository_id: string }>(
753
- "SELECT repository_id FROM indexed_files WHERE id = ?",
754
- [fileId],
755
- );
756
-
757
- if (!fileRecord) {
758
- throw new Error(`File not found: ${fileId}`);
759
- }
760
-
761
- // Build IN clause placeholders for reference types
762
- const refTypePlaceholders = referenceTypes.map(() => "?").join(", ");
763
-
764
- const sql = `
679
+ const db = getGlobalDatabase();
680
+
681
+ // Get repository_id for source file
682
+ const fileRecord = db.queryOne<{ repository_id: string }>(
683
+ "SELECT repository_id FROM indexed_files WHERE id = ?",
684
+ [fileId]
685
+ );
686
+
687
+ if (!fileRecord) {
688
+ throw new Error(`File not found: ${fileId}`);
689
+ }
690
+
691
+ // Build IN clause placeholders for reference types
692
+ const refTypePlaceholders = referenceTypes.map(() => "?").join(", ");
693
+
694
+ const sql = `
765
695
  WITH RECURSIVE
766
696
  dependencies AS (
767
697
  SELECT
@@ -814,343 +744,326 @@ export function queryDependencies(
814
744
  FROM cycles
815
745
  ORDER BY depth ASC, file_path ASC
816
746
  `;
817
-
818
- // Build params array: [refTypes..., repoId, fileId, refTypes..., repoId, depth, refTypes..., repoId, depth]
819
- const results = db.query<{
820
- file_path: string | null;
821
- depth: number | null;
822
- cycle_path: string | null;
823
- }>(sql, [
824
- ...referenceTypes,
825
- fileRecord.repository_id,
826
- fileId,
827
- ...referenceTypes,
828
- fileRecord.repository_id,
829
- depth,
830
- ...referenceTypes,
831
- fileRecord.repository_id,
832
- depth,
833
- ]);
834
-
835
- return processDepthResults(results, true); // Always include tests for dependencies
747
+
748
+ // Build params array: [refTypes..., repoId, fileId, refTypes..., repoId, depth, refTypes..., repoId, depth]
749
+ const results = db.query<{
750
+ file_path: string | null;
751
+ depth: number | null;
752
+ cycle_path: string | null;
753
+ }>(sql, [
754
+ ...referenceTypes, fileRecord.repository_id, fileId,
755
+ ...referenceTypes, fileRecord.repository_id, depth,
756
+ ...referenceTypes, fileRecord.repository_id, depth
757
+ ]);
758
+
759
+ return processDepthResults(results, true); // Always include tests for dependencies
836
760
  }
837
761
 
762
+
838
763
  function processDepthResults(
839
- results: Array<{
840
- file_path: string | null;
841
- depth: number | null;
842
- cycle_path: string | null;
843
- }>,
844
- includeTests: boolean,
764
+ results: Array<{
765
+ file_path: string | null;
766
+ depth: number | null;
767
+ cycle_path: string | null;
768
+ }>,
769
+ includeTests: boolean
845
770
  ): DependencyResult {
846
- const direct: string[] = [];
847
- const indirect: Record<string, string[]> = {};
848
- const cycles: string[][] = [];
849
- const seenCycles = new Set<string>();
850
-
851
- for (const result of results) {
852
- // Handle cycle detection
853
- if (result.cycle_path) {
854
- const cycleKey = result.cycle_path;
855
- if (!seenCycles.has(cycleKey)) {
856
- seenCycles.add(cycleKey);
857
- const cyclePaths = result.cycle_path
858
- .split("|")
859
- .filter((path) => path.length > 0);
860
-
861
- if (cyclePaths.length > 1) {
862
- cycles.push(cyclePaths);
863
- }
864
- }
865
- continue; // Don't add cycles to direct/indirect
866
- }
867
-
868
- // Skip if file_path is null (cycle-only rows)
869
- if (!result.file_path || result.depth === null) {
870
- continue;
871
- }
872
-
873
- // Filter test files if requested
874
- if (
875
- !includeTests &&
876
- (result.file_path.includes("test") || result.file_path.includes("spec"))
877
- ) {
878
- continue;
879
- }
880
-
881
- // Categorize by depth
882
- if (result.depth === 1) {
883
- if (!direct.includes(result.file_path)) {
884
- direct.push(result.file_path);
885
- }
886
- } else {
887
- const key = `depth_${result.depth}`;
888
- if (!indirect[key]) {
889
- indirect[key] = [];
890
- }
891
- if (!indirect[key].includes(result.file_path)) {
892
- indirect[key].push(result.file_path);
893
- }
894
- }
895
- }
896
-
897
- return { direct, indirect, cycles };
771
+ const direct: string[] = [];
772
+ const indirect: Record<string, string[]> = {};
773
+ const cycles: string[][] = [];
774
+ const seenCycles = new Set<string>();
775
+
776
+ for (const result of results) {
777
+ // Handle cycle detection
778
+ if (result.cycle_path) {
779
+ const cycleKey = result.cycle_path;
780
+ if (!seenCycles.has(cycleKey)) {
781
+ seenCycles.add(cycleKey);
782
+ const cyclePaths = result.cycle_path
783
+ .split('|')
784
+ .filter(path => path.length > 0);
785
+
786
+ if (cyclePaths.length > 1) {
787
+ cycles.push(cyclePaths);
788
+ }
789
+ }
790
+ continue; // Don't add cycles to direct/indirect
791
+ }
792
+
793
+ // Skip if file_path is null (cycle-only rows)
794
+ if (!result.file_path || result.depth === null) {
795
+ continue;
796
+ }
797
+
798
+ // Filter test files if requested
799
+ if (!includeTests && (result.file_path.includes("test") || result.file_path.includes("spec"))) {
800
+ continue;
801
+ }
802
+
803
+ // Categorize by depth
804
+ if (result.depth === 1) {
805
+ if (!direct.includes(result.file_path)) {
806
+ direct.push(result.file_path);
807
+ }
808
+ } else {
809
+ const key = `depth_${result.depth}`;
810
+ if (!indirect[key]) {
811
+ indirect[key] = [];
812
+ }
813
+ if (!indirect[key].includes(result.file_path)) {
814
+ indirect[key].push(result.file_path);
815
+ }
816
+ }
817
+ }
818
+
819
+ return { direct, indirect, cycles };
898
820
  }
899
821
 
822
+
900
823
  /**
901
824
  * Query dependents (reverse lookup): files/symbols that depend on the target
902
- *
825
+ *
903
826
  * @internal - Use queryDependents() for the wrapped version
904
827
  */
905
828
 
906
829
  /**
907
830
  * Ensure repository exists in SQLite, create if not.
908
- *
831
+ *
909
832
  * @param fullName - Repository full name (owner/repo format)
910
833
  * @param gitUrl - Git URL for the repository (optional)
911
834
  * @param defaultBranch - Default branch name (optional, defaults to 'main')
912
835
  * @returns Repository UUID
913
836
  */
914
837
  export function ensureRepository(
915
- fullName: string,
916
- gitUrl?: string,
917
- defaultBranch?: string,
838
+ fullName: string,
839
+ gitUrl?: string,
840
+ defaultBranch?: string,
918
841
  ): string {
919
- return ensureRepositoryInternal(
920
- getGlobalDatabase(),
921
- fullName,
922
- gitUrl,
923
- defaultBranch,
924
- );
842
+ return ensureRepositoryInternal(getGlobalDatabase(), fullName, gitUrl, defaultBranch);
925
843
  }
926
844
 
927
845
  /**
928
846
  * Update repository last_indexed_at timestamp.
929
- *
847
+ *
930
848
  * @param repositoryId - Repository UUID
931
849
  */
932
- export function updateRepositoryLastIndexed(repositoryId: string): void {
933
- updateRepositoryLastIndexedInternal(getGlobalDatabase(), repositoryId);
850
+ export function updateRepositoryLastIndexed(
851
+ repositoryId: string,
852
+ ): void {
853
+ updateRepositoryLastIndexedInternal(getGlobalDatabase(), repositoryId);
934
854
  }
935
855
 
936
856
  /**
937
857
  * Run indexing workflow for local mode (synchronous, no queue).
938
- *
858
+ *
939
859
  * @param request - Index request with repository details
940
860
  * @returns Indexing result with stats
941
861
  */
942
- export async function runIndexingWorkflow(request: IndexRequest): Promise<{
943
- repositoryId: string;
944
- filesIndexed: number;
945
- symbolsExtracted: number;
946
- referencesExtracted: number;
862
+ export async function runIndexingWorkflow(
863
+ request: IndexRequest,
864
+ ): Promise<{
865
+ repositoryId: string;
866
+ filesIndexed: number;
867
+ symbolsExtracted: number;
868
+ referencesExtracted: number;
947
869
  }> {
948
- const { existsSync } = await import("node:fs");
949
- const { resolve } = await import("node:path");
950
- const { prepareRepository } = await import("@indexer/repos");
951
- const { discoverSources, parseSourceFile } = await import("@indexer/parsers");
952
- const { parseFileWithRecovery, isSupportedForAST } =
953
- await import("@indexer/ast-parser");
954
- const { extractSymbols } = await import("@indexer/symbol-extractor");
955
- const { extractReferences } = await import("@indexer/reference-extractor");
956
- const { parseTsConfig } = await import("@indexer/path-resolver");
957
-
958
- const db = getGlobalDatabase();
959
-
960
- let localPath: string;
961
- let fullName = request.repository;
962
-
963
- if (request.localPath) {
964
- localPath = resolve(request.localPath);
965
-
966
- const workspaceRoot = resolve(process.cwd());
967
- if (!localPath.startsWith(workspaceRoot)) {
968
- throw new Error(`localPath must be within workspace: ${workspaceRoot}`);
969
- }
970
- if (!fullName.includes("/")) {
971
- fullName = `local/${fullName}`;
972
- }
973
- } else {
974
- const repo = await prepareRepository(request);
975
- localPath = repo.localPath;
976
- }
977
-
978
- if (!existsSync(localPath)) {
979
- throw new Error(`Repository path does not exist: ${localPath}`);
980
- }
981
-
982
- const gitUrl = request.localPath
983
- ? localPath
984
- : `https://github.com/${fullName}.git`;
985
- const repositoryId = ensureRepository(fullName, gitUrl, request.ref);
986
-
987
- logger.info("Starting local indexing workflow", {
988
- repositoryId,
989
- fullName,
990
- localPath,
991
- });
992
-
993
- // Parse tsconfig.json for path alias resolution
994
- const pathMappings = parseTsConfig(localPath);
995
- if (pathMappings) {
996
- logger.info("Loaded path mappings from tsconfig.json", {
997
- aliasCount: Object.keys(pathMappings.paths).length,
998
- baseUrl: pathMappings.baseUrl,
999
- });
1000
- }
1001
-
1002
- const sources = await discoverSources(localPath);
1003
- const records = (
1004
- await Promise.all(
1005
- sources.map((source) => parseSourceFile(source, localPath)),
1006
- )
1007
- ).filter((entry): entry is NonNullable<typeof entry> => entry !== null);
1008
-
1009
- const filesIndexed = saveIndexedFiles(records, repositoryId);
1010
-
1011
- // Query ALL indexed files for complete resolution (fixes order dependency bug)
1012
- const allIndexedFiles = db
1013
- .query<{
1014
- id: string;
1015
- path: string;
1016
- }>(`SELECT id, path FROM indexed_files WHERE repository_id = ?`, [repositoryId])
1017
- .map((row) => ({
1018
- id: row.id,
1019
- path: row.path,
1020
- repository_id: repositoryId,
1021
- }));
1022
-
1023
- logger.debug("Queried all indexed files for path alias resolution", {
1024
- count: allIndexedFiles.length,
1025
- repositoryId,
1026
- });
1027
-
1028
- let totalSymbols = 0;
1029
- let totalReferences = 0;
1030
-
1031
- // First pass: Store symbols and collect references (but don't resolve imports yet)
1032
- interface FileWithReferences {
1033
- fileId: string;
1034
- filePath: string;
1035
- references: Reference[];
1036
- }
1037
- const filesWithReferences: FileWithReferences[] = [];
1038
-
1039
- for (const file of records) {
1040
- if (!isSupportedForAST(file.path)) continue;
1041
-
1042
- const parseResult = parseFileWithRecovery(file.path, file.content);
1043
- if (!parseResult.ast) continue;
1044
-
1045
- const symbols = extractSymbols(parseResult.ast!, file.path);
1046
- const references = extractReferences(parseResult.ast!, file.path);
1047
-
1048
- const fileRecord = db.queryOne<{ id: string }>(
1049
- "SELECT id FROM indexed_files WHERE repository_id = ? AND path = ?",
1050
- [repositoryId, file.path],
1051
- );
1052
-
1053
- if (!fileRecord) {
1054
- logger.warn("Could not find file record after indexing", {
1055
- filePath: file.path,
1056
- repositoryId,
1057
- });
1058
- continue;
1059
- }
1060
-
1061
- // Store symbols immediately
1062
- const symbolCount = storeSymbols(symbols, fileRecord.id);
1063
- totalSymbols += symbolCount;
1064
-
1065
- // Collect references for later processing
1066
- filesWithReferences.push({
1067
- fileId: fileRecord.id,
1068
- filePath: file.path,
1069
- references,
1070
- });
1071
- }
1072
-
1073
- // Second pass: Store references with complete file list for proper path alias resolution
1074
- for (const fileWithRefs of filesWithReferences) {
1075
- const referenceCount = storeReferences(
1076
- fileWithRefs.fileId,
1077
- fileWithRefs.filePath,
1078
- fileWithRefs.references,
1079
- allIndexedFiles, // Use complete file list instead of incremental array
1080
- pathMappings,
1081
- );
1082
- totalReferences += referenceCount;
1083
- }
1084
-
1085
- // Build symbol metadata for backward compatibility
1086
- const allSymbolsWithFileId: Array<{
1087
- id: string;
1088
- file_id: string;
1089
- name: string;
1090
- kind: SymbolKind;
1091
- lineStart: number;
1092
- lineEnd: number;
1093
- columnStart: number;
1094
- columnEnd: number;
1095
- signature: string | null;
1096
- documentation: string | null;
1097
- isExported: boolean;
1098
- }> = [];
1099
- const allReferencesWithFileId: Array<Reference & { file_id: string }> = [];
1100
-
1101
- for (const fileWithRefs of filesWithReferences) {
1102
- const storedSymbols = db.query<{
1103
- id: string;
1104
- file_id: string;
1105
- name: string;
1106
- kind: SymbolKind;
1107
- line_start: number;
1108
- line_end: number;
1109
- signature: string | null;
1110
- documentation: string | null;
1111
- metadata: string;
1112
- }>(
1113
- "SELECT id, file_id, name, kind, line_start, line_end, signature, documentation, metadata FROM indexed_symbols WHERE file_id = ?",
1114
- [fileWithRefs.fileId],
1115
- );
1116
-
1117
- for (const s of storedSymbols) {
1118
- const metadata = JSON.parse(s.metadata || "{}");
1119
- allSymbolsWithFileId.push({
1120
- id: s.id,
1121
- file_id: s.file_id,
1122
- name: s.name,
1123
- kind: s.kind as SymbolKind,
1124
- lineStart: s.line_start,
1125
- lineEnd: s.line_end,
1126
- columnStart: metadata.column_start || 0,
1127
- columnEnd: metadata.column_end || 0,
1128
- signature: s.signature || null,
1129
- documentation: s.documentation || null,
1130
- isExported: metadata.is_exported || false,
1131
- });
1132
- }
1133
-
1134
- for (const ref of fileWithRefs.references) {
1135
- allReferencesWithFileId.push({ ...ref, file_id: fileWithRefs.fileId });
1136
- }
1137
- }
1138
-
1139
- updateRepositoryLastIndexed(repositoryId);
1140
-
1141
- logger.info("Local indexing workflow completed", {
1142
- repositoryId,
1143
- filesIndexed,
1144
- symbolsExtracted: totalSymbols,
1145
- referencesExtracted: totalReferences,
1146
- });
1147
-
1148
- return {
1149
- repositoryId,
1150
- filesIndexed,
1151
- symbolsExtracted: totalSymbols,
1152
- referencesExtracted: totalReferences,
1153
- };
870
+ const { existsSync } = await import("node:fs");
871
+ const { resolve } = await import("node:path");
872
+ const { prepareRepository } = await import("@indexer/repos");
873
+ const { discoverSources, parseSourceFile } = await import("@indexer/parsers");
874
+ const { parseFileWithRecovery, isSupportedForAST } = await import("@indexer/ast-parser");
875
+ const { extractSymbols } = await import("@indexer/symbol-extractor");
876
+ const { extractReferences } = await import("@indexer/reference-extractor");
877
+ const { parseTsConfig } = await import("@indexer/path-resolver");
878
+
879
+ const db = getGlobalDatabase();
880
+
881
+ let localPath: string;
882
+ let fullName = request.repository;
883
+
884
+ if (request.localPath) {
885
+ localPath = resolve(request.localPath);
886
+
887
+ const workspaceRoot = resolve(process.cwd());
888
+ if (!localPath.startsWith(workspaceRoot)) {
889
+ throw new Error(`localPath must be within workspace: ${workspaceRoot}`);
890
+ }
891
+ if (!fullName.includes("/")) {
892
+ fullName = `local/${fullName}`;
893
+ }
894
+ } else {
895
+ const repo = await prepareRepository(request);
896
+ localPath = repo.localPath;
897
+ }
898
+
899
+ if (!existsSync(localPath)) {
900
+ throw new Error(`Repository path does not exist: ${localPath}`);
901
+ }
902
+
903
+ const gitUrl = request.localPath ? localPath : `https://github.com/${fullName}.git`;
904
+ const repositoryId = ensureRepository(fullName, gitUrl, request.ref);
905
+
906
+ logger.info("Starting local indexing workflow", {
907
+ repositoryId,
908
+ fullName,
909
+ localPath,
910
+ });
911
+
912
+ // Parse tsconfig.json for path alias resolution
913
+ const pathMappings = parseTsConfig(localPath);
914
+ if (pathMappings) {
915
+ logger.info("Loaded path mappings from tsconfig.json", {
916
+ aliasCount: Object.keys(pathMappings.paths).length,
917
+ baseUrl: pathMappings.baseUrl,
918
+ });
919
+ }
920
+
921
+ const sources = await discoverSources(localPath);
922
+ const records = (
923
+ await Promise.all(sources.map((source) => parseSourceFile(source, localPath)))
924
+ ).filter((entry): entry is NonNullable<typeof entry> => entry !== null);
925
+
926
+ const filesIndexed = saveIndexedFiles(records, repositoryId);
927
+
928
+ // Query ALL indexed files for complete resolution (fixes order dependency bug)
929
+ const allIndexedFiles = db
930
+ .query<{ id: string; path: string }>(
931
+ `SELECT id, path FROM indexed_files WHERE repository_id = ?`,
932
+ [repositoryId]
933
+ )
934
+ .map(row => ({ id: row.id, path: row.path, repository_id: repositoryId }));
935
+
936
+ logger.debug("Queried all indexed files for path alias resolution", {
937
+ count: allIndexedFiles.length,
938
+ repositoryId,
939
+ });
940
+
941
+ let totalSymbols = 0;
942
+ let totalReferences = 0;
943
+
944
+ // First pass: Store symbols and collect references (but don't resolve imports yet)
945
+ interface FileWithReferences {
946
+ fileId: string;
947
+ filePath: string;
948
+ references: Reference[];
949
+ }
950
+ const filesWithReferences: FileWithReferences[] = [];
951
+
952
+ for (const file of records) {
953
+ if (!isSupportedForAST(file.path)) continue;
954
+
955
+ const parseResult = parseFileWithRecovery(file.path, file.content);
956
+ if (!parseResult.ast) continue;
957
+
958
+ const symbols = extractSymbols(parseResult.ast!, file.path);
959
+ const references = extractReferences(parseResult.ast!, file.path);
960
+
961
+ const fileRecord = db.queryOne<{ id: string }>(
962
+ "SELECT id FROM indexed_files WHERE repository_id = ? AND path = ?",
963
+ [repositoryId, file.path]
964
+ );
965
+
966
+ if (!fileRecord) {
967
+ logger.warn("Could not find file record after indexing", {
968
+ filePath: file.path,
969
+ repositoryId,
970
+ });
971
+ continue;
972
+ }
973
+
974
+ // Store symbols immediately
975
+ const symbolCount = storeSymbols(symbols, fileRecord.id);
976
+ totalSymbols += symbolCount;
977
+
978
+ // Collect references for later processing
979
+ filesWithReferences.push({
980
+ fileId: fileRecord.id,
981
+ filePath: file.path,
982
+ references,
983
+ });
984
+ }
985
+
986
+ // Second pass: Store references with complete file list for proper path alias resolution
987
+ for (const fileWithRefs of filesWithReferences) {
988
+ const referenceCount = storeReferences(
989
+ fileWithRefs.fileId,
990
+ fileWithRefs.filePath,
991
+ fileWithRefs.references,
992
+ allIndexedFiles, // Use complete file list instead of incremental array
993
+ pathMappings
994
+ );
995
+ totalReferences += referenceCount;
996
+ }
997
+
998
+ // Build symbol metadata for backward compatibility
999
+ const allSymbolsWithFileId: Array<{
1000
+ id: string;
1001
+ file_id: string;
1002
+ name: string;
1003
+ kind: SymbolKind;
1004
+ lineStart: number;
1005
+ lineEnd: number;
1006
+ columnStart: number;
1007
+ columnEnd: number;
1008
+ signature: string | null;
1009
+ documentation: string | null;
1010
+ isExported: boolean;
1011
+ }> = [];
1012
+ const allReferencesWithFileId: Array<Reference & { file_id: string }> = [];
1013
+
1014
+ for (const fileWithRefs of filesWithReferences) {
1015
+ const storedSymbols = db.query<{
1016
+ id: string;
1017
+ file_id: string;
1018
+ name: string;
1019
+ kind: SymbolKind;
1020
+ line_start: number;
1021
+ line_end: number;
1022
+ signature: string | null;
1023
+ documentation: string | null;
1024
+ metadata: string;
1025
+ }>(
1026
+ "SELECT id, file_id, name, kind, line_start, line_end, signature, documentation, metadata FROM indexed_symbols WHERE file_id = ?",
1027
+ [fileWithRefs.fileId]
1028
+ );
1029
+
1030
+ for (const s of storedSymbols) {
1031
+ const metadata = JSON.parse(s.metadata || "{}");
1032
+ allSymbolsWithFileId.push({
1033
+ id: s.id,
1034
+ file_id: s.file_id,
1035
+ name: s.name,
1036
+ kind: s.kind as SymbolKind,
1037
+ lineStart: s.line_start,
1038
+ lineEnd: s.line_end,
1039
+ columnStart: metadata.column_start || 0,
1040
+ columnEnd: metadata.column_end || 0,
1041
+ signature: s.signature || null,
1042
+ documentation: s.documentation || null,
1043
+ isExported: metadata.is_exported || false,
1044
+ });
1045
+ }
1046
+
1047
+ for (const ref of fileWithRefs.references) {
1048
+ allReferencesWithFileId.push({ ...ref, file_id: fileWithRefs.fileId });
1049
+ }
1050
+ }
1051
+
1052
+ updateRepositoryLastIndexed(repositoryId);
1053
+
1054
+ logger.info("Local indexing workflow completed", {
1055
+ repositoryId,
1056
+ filesIndexed,
1057
+ symbolsExtracted: totalSymbols,
1058
+ referencesExtracted: totalReferences,
1059
+ });
1060
+
1061
+ return {
1062
+ repositoryId,
1063
+ filesIndexed,
1064
+ symbolsExtracted: totalSymbols,
1065
+ referencesExtracted: totalReferences,
1066
+ };
1154
1067
  }
1155
1068
 
1156
1069
  // ============================================================================
@@ -1159,181 +1072,174 @@ export async function runIndexingWorkflow(request: IndexRequest): Promise<{
1159
1072
 
1160
1073
  /**
1161
1074
  * Check if a repository has been indexed (has files in indexed_files table).
1162
- *
1075
+ *
1163
1076
  * @param repositoryId - Repository UUID or full_name
1164
1077
  * @returns true if the repository has indexed files, false otherwise
1165
1078
  */
1166
1079
  function isRepositoryIndexedInternal(
1167
- db: KotaDatabase,
1168
- repositoryId: string,
1080
+ db: KotaDatabase,
1081
+ repositoryId: string,
1169
1082
  ): boolean {
1170
- // First try to match by ID, then by full_name
1171
- const result = db.queryOne<{ count: number }>(
1172
- `SELECT COUNT(*) as count FROM indexed_files
1083
+ // First try to match by ID, then by full_name
1084
+ const result = db.queryOne<{ count: number }>(
1085
+ `SELECT COUNT(*) as count FROM indexed_files
1173
1086
  WHERE repository_id = ?
1174
1087
  OR repository_id IN (SELECT id FROM repositories WHERE full_name = ?)`,
1175
- [repositoryId, repositoryId],
1176
- );
1177
- return (result?.count ?? 0) > 0;
1088
+ [repositoryId, repositoryId]
1089
+ );
1090
+ return (result?.count ?? 0) > 0;
1178
1091
  }
1179
1092
 
1180
1093
  /**
1181
1094
  * Check if a repository has been indexed.
1182
- *
1095
+ *
1183
1096
  * @param repositoryId - Repository UUID or full_name
1184
1097
  * @returns true if the repository has indexed files, false otherwise
1185
1098
  */
1186
1099
  export function isRepositoryIndexed(repositoryId: string): boolean {
1187
- return isRepositoryIndexedInternal(getGlobalDatabase(), repositoryId);
1100
+ return isRepositoryIndexedInternal(getGlobalDatabase(), repositoryId);
1188
1101
  }
1189
1102
 
1190
1103
  /**
1191
1104
  * Delete a single file from the index by path.
1192
1105
  * Cascading deletes will remove associated symbols and references.
1193
- *
1106
+ *
1194
1107
  * @param repositoryId - Repository UUID
1195
1108
  * @param filePath - Relative file path to delete
1196
1109
  * @returns true if file was deleted, false if not found
1197
1110
  */
1198
1111
  function deleteFileByPathInternal(
1199
- db: KotaDatabase,
1200
- repositoryId: string,
1201
- filePath: string,
1112
+ db: KotaDatabase,
1113
+ repositoryId: string,
1114
+ filePath: string,
1202
1115
  ): boolean {
1203
- const normalizedPath = normalizePath(filePath);
1204
-
1205
- // The indexed_files table has ON DELETE CASCADE for:
1206
- // - indexed_symbols (via file_id FK)
1207
- // - indexed_references (via file_id FK)
1208
- // FTS5 triggers handle indexed_files_fts cleanup automatically
1209
-
1210
- const result = db.queryOne<{ id: string }>(
1211
- `SELECT id FROM indexed_files WHERE repository_id = ? AND path = ?`,
1212
- [repositoryId, normalizedPath],
1213
- );
1214
-
1215
- if (!result) {
1216
- logger.debug("File not found for deletion", {
1217
- repositoryId,
1218
- filePath: normalizedPath,
1219
- });
1220
- return false;
1221
- }
1222
-
1223
- db.run(`DELETE FROM indexed_files WHERE id = ?`, [result.id]);
1224
-
1225
- logger.info("Deleted file from index", {
1226
- repositoryId,
1227
- filePath: normalizedPath,
1228
- fileId: result.id,
1229
- });
1230
- return true;
1116
+ const normalizedPath = normalizePath(filePath);
1117
+
1118
+ // The indexed_files table has ON DELETE CASCADE for:
1119
+ // - indexed_symbols (via file_id FK)
1120
+ // - indexed_references (via file_id FK)
1121
+ // FTS5 triggers handle indexed_files_fts cleanup automatically
1122
+
1123
+ const result = db.queryOne<{ id: string }>(
1124
+ `SELECT id FROM indexed_files WHERE repository_id = ? AND path = ?`,
1125
+ [repositoryId, normalizedPath]
1126
+ );
1127
+
1128
+ if (!result) {
1129
+ logger.debug("File not found for deletion", { repositoryId, filePath: normalizedPath });
1130
+ return false;
1131
+ }
1132
+
1133
+ db.run(
1134
+ `DELETE FROM indexed_files WHERE id = ?`,
1135
+ [result.id]
1136
+ );
1137
+
1138
+ logger.info("Deleted file from index", { repositoryId, filePath: normalizedPath, fileId: result.id });
1139
+ return true;
1231
1140
  }
1232
1141
 
1233
1142
  /**
1234
1143
  * Delete a single file from the index by path.
1235
- *
1144
+ *
1236
1145
  * @param repositoryId - Repository UUID
1237
1146
  * @param filePath - Relative file path to delete
1238
1147
  * @returns true if file was deleted, false if not found
1239
1148
  */
1240
1149
  export function deleteFileByPath(
1241
- repositoryId: string,
1242
- filePath: string,
1150
+ repositoryId: string,
1151
+ filePath: string,
1243
1152
  ): boolean {
1244
- return deleteFileByPathInternal(getGlobalDatabase(), repositoryId, filePath);
1153
+ return deleteFileByPathInternal(getGlobalDatabase(), repositoryId, filePath);
1245
1154
  }
1246
1155
 
1247
1156
  /**
1248
1157
  * Delete multiple files from the index by paths.
1249
1158
  * Uses a transaction for atomic operation.
1250
1159
  * Cascading deletes will remove associated symbols and references.
1251
- *
1160
+ *
1252
1161
  * @param repositoryId - Repository UUID
1253
1162
  * @param filePaths - Array of relative file paths to delete
1254
1163
  * @returns Object with deleted count and list of deleted paths
1255
1164
  */
1256
1165
  function deleteFilesByPathsInternal(
1257
- db: KotaDatabase,
1258
- repositoryId: string,
1259
- filePaths: string[],
1166
+ db: KotaDatabase,
1167
+ repositoryId: string,
1168
+ filePaths: string[],
1260
1169
  ): { deletedCount: number; deletedPaths: string[] } {
1261
- if (filePaths.length === 0) {
1262
- return { deletedCount: 0, deletedPaths: [] };
1263
- }
1264
-
1265
- const normalizedPaths = filePaths.map(normalizePath);
1266
- const deletedPaths: string[] = [];
1267
-
1268
- db.transaction(() => {
1269
- for (const normalizedPath of normalizedPaths) {
1270
- const result = db.queryOne<{ id: string }>(
1271
- `SELECT id FROM indexed_files WHERE repository_id = ? AND path = ?`,
1272
- [repositoryId, normalizedPath],
1273
- );
1274
-
1275
- if (result) {
1276
- db.run(`DELETE FROM indexed_files WHERE id = ?`, [result.id]);
1277
- deletedPaths.push(normalizedPath);
1278
- }
1279
- }
1280
- });
1281
-
1282
- logger.info("Deleted files from index", {
1283
- repositoryId,
1284
- requestedCount: filePaths.length,
1285
- deletedCount: deletedPaths.length,
1286
- });
1287
-
1288
- return { deletedCount: deletedPaths.length, deletedPaths };
1170
+ if (filePaths.length === 0) {
1171
+ return { deletedCount: 0, deletedPaths: [] };
1172
+ }
1173
+
1174
+ const normalizedPaths = filePaths.map(normalizePath);
1175
+ const deletedPaths: string[] = [];
1176
+
1177
+ db.transaction(() => {
1178
+ for (const normalizedPath of normalizedPaths) {
1179
+ const result = db.queryOne<{ id: string }>(
1180
+ `SELECT id FROM indexed_files WHERE repository_id = ? AND path = ?`,
1181
+ [repositoryId, normalizedPath]
1182
+ );
1183
+
1184
+ if (result) {
1185
+ db.run(`DELETE FROM indexed_files WHERE id = ?`, [result.id]);
1186
+ deletedPaths.push(normalizedPath);
1187
+ }
1188
+ }
1189
+ });
1190
+
1191
+ logger.info("Deleted files from index", {
1192
+ repositoryId,
1193
+ requestedCount: filePaths.length,
1194
+ deletedCount: deletedPaths.length
1195
+ });
1196
+
1197
+ return { deletedCount: deletedPaths.length, deletedPaths };
1289
1198
  }
1290
1199
 
1291
1200
  /**
1292
1201
  * Delete multiple files from the index by paths.
1293
- *
1202
+ *
1294
1203
  * @param repositoryId - Repository UUID
1295
1204
  * @param filePaths - Array of relative file paths to delete
1296
1205
  * @returns Object with deleted count and list of deleted paths
1297
1206
  */
1298
1207
  export function deleteFilesByPaths(
1299
- repositoryId: string,
1300
- filePaths: string[],
1208
+ repositoryId: string,
1209
+ filePaths: string[],
1301
1210
  ): { deletedCount: number; deletedPaths: string[] } {
1302
- return deleteFilesByPathsInternal(
1303
- getGlobalDatabase(),
1304
- repositoryId,
1305
- filePaths,
1306
- );
1211
+ return deleteFilesByPathsInternal(getGlobalDatabase(), repositoryId, filePaths);
1307
1212
  }
1308
1213
 
1309
1214
  /**
1310
1215
  * Get repository ID from full_name.
1311
1216
  * Useful for auto-indexing when you have the path but need the UUID.
1312
- *
1217
+ *
1313
1218
  * @param fullName - Repository full name (e.g., "owner/repo" or "local/path")
1314
1219
  * @returns Repository UUID or null if not found
1315
1220
  */
1316
1221
  function getRepositoryIdByNameInternal(
1317
- db: KotaDatabase,
1318
- fullName: string,
1222
+ db: KotaDatabase,
1223
+ fullName: string,
1319
1224
  ): string | null {
1320
- const result = db.queryOne<{ id: string }>(
1321
- `SELECT id FROM repositories WHERE full_name = ?`,
1322
- [fullName],
1323
- );
1324
- return result?.id ?? null;
1225
+ const result = db.queryOne<{ id: string }>(
1226
+ `SELECT id FROM repositories WHERE full_name = ?`,
1227
+ [fullName]
1228
+ );
1229
+ return result?.id ?? null;
1325
1230
  }
1326
1231
 
1327
1232
  /**
1328
1233
  * Get repository ID from full_name.
1329
- *
1234
+ *
1330
1235
  * @param fullName - Repository full name
1331
1236
  * @returns Repository UUID or null if not found
1332
1237
  */
1333
1238
  export function getRepositoryIdByName(fullName: string): string | null {
1334
- return getRepositoryIdByNameInternal(getGlobalDatabase(), fullName);
1239
+ return getRepositoryIdByNameInternal(getGlobalDatabase(), fullName);
1335
1240
  }
1336
1241
 
1242
+
1337
1243
  // ============================================================================
1338
1244
  // Backward-compatible aliases that accept db parameter
1339
1245
  // These use the passed database (for tests) rather than the global one
@@ -1343,109 +1249,101 @@ export function getRepositoryIdByName(fullName: string): string | null {
1343
1249
  * @deprecated Use saveIndexedFiles() directly
1344
1250
  */
1345
1251
  export function saveIndexedFilesLocal(
1346
- db: KotaDatabase,
1347
- files: IndexedFile[],
1348
- repositoryId: string,
1252
+ db: KotaDatabase,
1253
+ files: IndexedFile[],
1254
+ repositoryId: string
1349
1255
  ): number {
1350
- return saveIndexedFilesInternal(db, files, repositoryId);
1256
+ return saveIndexedFilesInternal(db, files, repositoryId);
1351
1257
  }
1352
1258
 
1353
1259
  /**
1354
1260
  * @deprecated Use storeSymbols() directly
1355
1261
  */
1356
1262
  export function storeSymbolsLocal(
1357
- db: KotaDatabase,
1358
- symbols: ExtractedSymbol[],
1359
- fileId: string,
1263
+ db: KotaDatabase,
1264
+ symbols: ExtractedSymbol[],
1265
+ fileId: string
1360
1266
  ): number {
1361
- return storeSymbolsInternal(db, symbols, fileId);
1267
+ return storeSymbolsInternal(db, symbols, fileId);
1362
1268
  }
1363
1269
 
1364
1270
  /**
1365
1271
  * @deprecated Use storeReferences() directly
1366
1272
  */
1367
1273
  export function storeReferencesLocal(
1368
- db: KotaDatabase,
1369
- fileId: string,
1370
- filePath: string,
1371
- references: Reference[],
1372
- allFiles: Array<{ path: string }>,
1373
- pathMappings?: PathMappings | null,
1274
+ db: KotaDatabase,
1275
+ fileId: string,
1276
+ filePath: string,
1277
+ references: Reference[],
1278
+ allFiles: Array<{ path: string }>,
1279
+ pathMappings?: PathMappings | null
1374
1280
  ): number {
1375
- const repositoryId = db.queryOne<{ repository_id: string }>(
1376
- "SELECT repository_id FROM indexed_files WHERE id = ?",
1377
- [fileId],
1378
- )?.repository_id;
1379
-
1380
- if (!repositoryId) {
1381
- throw new Error(`File not found: ${fileId}`);
1382
- }
1383
-
1384
- return storeReferencesInternal(
1385
- db,
1386
- fileId,
1387
- repositoryId,
1388
- filePath,
1389
- references,
1390
- allFiles,
1391
- pathMappings,
1392
- );
1281
+ const repositoryId = db.queryOne<{ repository_id: string }>(
1282
+ "SELECT repository_id FROM indexed_files WHERE id = ?",
1283
+ [fileId]
1284
+ )?.repository_id;
1285
+
1286
+ if (!repositoryId) {
1287
+ throw new Error(`File not found: ${fileId}`);
1288
+ }
1289
+
1290
+ return storeReferencesInternal(db, fileId, repositoryId, filePath, references, allFiles, pathMappings);
1393
1291
  }
1394
1292
 
1395
1293
  /**
1396
1294
  * @deprecated Use searchFiles() directly
1397
1295
  */
1398
1296
  export function searchFilesLocal(
1399
- db: KotaDatabase,
1400
- term: string,
1401
- repositoryId: string | undefined,
1402
- limit: number,
1297
+ db: KotaDatabase,
1298
+ term: string,
1299
+ repositoryId: string | undefined,
1300
+ limit: number
1403
1301
  ): IndexedFile[] {
1404
- return searchFilesInternal(db, term, repositoryId, limit);
1302
+ return searchFilesInternal(db, term, repositoryId, limit);
1405
1303
  }
1406
1304
 
1407
1305
  /**
1408
1306
  * @deprecated Use listRecentFiles() directly
1409
1307
  */
1410
1308
  export function listRecentFilesLocal(
1411
- db: KotaDatabase,
1412
- limit: number,
1413
- repositoryId?: string,
1309
+ db: KotaDatabase,
1310
+ limit: number,
1311
+ repositoryId?: string,
1414
1312
  ): IndexedFile[] {
1415
- return listRecentFilesInternal(db, limit, repositoryId);
1313
+ return listRecentFilesInternal(db, limit, repositoryId);
1416
1314
  }
1417
1315
 
1418
1316
  /**
1419
1317
  * @deprecated Use resolveFilePath() directly
1420
1318
  */
1421
1319
  export function resolveFilePathLocal(
1422
- db: KotaDatabase,
1423
- filePath: string,
1424
- repositoryId: string,
1320
+ db: KotaDatabase,
1321
+ filePath: string,
1322
+ repositoryId: string
1425
1323
  ): string | null {
1426
- return resolveFilePathInternal(db, filePath, repositoryId);
1324
+ return resolveFilePathInternal(db, filePath, repositoryId);
1427
1325
  }
1428
1326
 
1429
1327
  /**
1430
1328
  * @deprecated Use ensureRepository() directly
1431
1329
  */
1432
1330
  export function ensureRepositoryLocal(
1433
- db: KotaDatabase,
1434
- fullName: string,
1435
- gitUrl?: string,
1436
- defaultBranch?: string,
1331
+ db: KotaDatabase,
1332
+ fullName: string,
1333
+ gitUrl?: string,
1334
+ defaultBranch?: string
1437
1335
  ): string {
1438
- return ensureRepositoryInternal(db, fullName, gitUrl, defaultBranch);
1336
+ return ensureRepositoryInternal(db, fullName, gitUrl, defaultBranch);
1439
1337
  }
1440
1338
 
1441
1339
  /**
1442
1340
  * @deprecated Use updateRepositoryLastIndexed() directly
1443
1341
  */
1444
1342
  export function updateRepositoryLastIndexedLocal(
1445
- db: KotaDatabase,
1446
- repositoryId: string,
1343
+ db: KotaDatabase,
1344
+ repositoryId: string
1447
1345
  ): void {
1448
- return updateRepositoryLastIndexedInternal(db, repositoryId);
1346
+ return updateRepositoryLastIndexedInternal(db, repositoryId);
1449
1347
  }
1450
1348
 
1451
1349
  /**
@@ -1453,10 +1351,10 @@ export function updateRepositoryLastIndexedLocal(
1453
1351
  * Version that accepts db parameter for testing.
1454
1352
  */
1455
1353
  export function isRepositoryIndexedLocal(
1456
- db: KotaDatabase,
1457
- repositoryId: string,
1354
+ db: KotaDatabase,
1355
+ repositoryId: string
1458
1356
  ): boolean {
1459
- return isRepositoryIndexedInternal(db, repositoryId);
1357
+ return isRepositoryIndexedInternal(db, repositoryId);
1460
1358
  }
1461
1359
 
1462
1360
  /**
@@ -1464,11 +1362,11 @@ export function isRepositoryIndexedLocal(
1464
1362
  * Version that accepts db parameter for testing.
1465
1363
  */
1466
1364
  export function deleteFileByPathLocal(
1467
- db: KotaDatabase,
1468
- repositoryId: string,
1469
- filePath: string,
1365
+ db: KotaDatabase,
1366
+ repositoryId: string,
1367
+ filePath: string
1470
1368
  ): boolean {
1471
- return deleteFileByPathInternal(db, repositoryId, filePath);
1369
+ return deleteFileByPathInternal(db, repositoryId, filePath);
1472
1370
  }
1473
1371
 
1474
1372
  /**
@@ -1476,11 +1374,11 @@ export function deleteFileByPathLocal(
1476
1374
  * Version that accepts db parameter for testing.
1477
1375
  */
1478
1376
  export function deleteFilesByPathsLocal(
1479
- db: KotaDatabase,
1480
- repositoryId: string,
1481
- filePaths: string[],
1377
+ db: KotaDatabase,
1378
+ repositoryId: string,
1379
+ filePaths: string[]
1482
1380
  ): { deletedCount: number; deletedPaths: string[] } {
1483
- return deleteFilesByPathsInternal(db, repositoryId, filePaths);
1381
+ return deleteFilesByPathsInternal(db, repositoryId, filePaths);
1484
1382
  }
1485
1383
 
1486
1384
  /**
@@ -1488,101 +1386,26 @@ export function deleteFilesByPathsLocal(
1488
1386
  * Version that accepts db parameter for testing.
1489
1387
  */
1490
1388
  export function getRepositoryIdByNameLocal(
1491
- db: KotaDatabase,
1492
- fullName: string,
1389
+ db: KotaDatabase,
1390
+ fullName: string
1493
1391
  ): string | null {
1494
- return getRepositoryIdByNameInternal(db, fullName);
1392
+ return getRepositoryIdByNameInternal(db, fullName);
1495
1393
  }
1496
1394
 
1497
1395
  // Add alias for runIndexingWorkflowLocal
1498
1396
  export const runIndexingWorkflowLocal = runIndexingWorkflow;
1499
1397
 
1398
+
1500
1399
  /**
1501
1400
  * Create default organization for a new user.
1502
- *
1401
+ *
1503
1402
  * @deprecated This function is not available in local-only mode.
1504
1403
  * Organizations are a cloud-only feature.
1505
1404
  */
1506
1405
  export async function createDefaultOrganization(
1507
- _client: unknown,
1508
- _userId: string,
1509
- _userEmail?: string,
1406
+ _client: unknown,
1407
+ _userId: string,
1408
+ _userEmail?: string,
1510
1409
  ): Promise<string> {
1511
- throw new Error(
1512
- "createDefaultOrganization() is not available in local-only mode - organizations are a cloud-only feature",
1513
- );
1514
- }
1515
-
1516
- /**
1517
- * Get index statistics for startup context display.
1518
- * Queries counts of indexed files, symbols, references, and memory entries.
1519
- *
1520
- * @param db - Database instance (for testability)
1521
- * @returns Statistics object with counts by type
1522
- */
1523
- function getIndexStatisticsInternal(db: KotaDatabase): {
1524
- files: number;
1525
- symbols: number;
1526
- references: number;
1527
- decisions: number;
1528
- patterns: number;
1529
- failures: number;
1530
- repositories: number;
1531
- } {
1532
- const stats = {
1533
- files: 0,
1534
- symbols: 0,
1535
- references: 0,
1536
- decisions: 0,
1537
- patterns: 0,
1538
- failures: 0,
1539
- repositories: 0,
1540
- };
1541
-
1542
- // Helper function to safely query count with fallback for missing tables
1543
- const safeCount = (tableName: string): number => {
1544
- try {
1545
- const result = db.queryOne<{ count: number }>(
1546
- `SELECT COUNT(*) as count FROM ${tableName}`,
1547
- );
1548
- return result?.count || 0;
1549
- } catch (error) {
1550
- // Table doesn't exist yet (e.g., memory layer tables)
1551
- return 0;
1552
- }
1553
- };
1554
-
1555
- // Count indexed files
1556
- stats.files = safeCount("indexed_files");
1557
-
1558
- // Count indexed symbols
1559
- stats.symbols = safeCount("indexed_symbols");
1560
-
1561
- // Count references
1562
- stats.references = safeCount("indexed_references");
1563
-
1564
- // Count decisions (may not exist in all installations)
1565
- stats.decisions = safeCount("kota_decisions");
1566
-
1567
- // Count patterns (may not exist in all installations)
1568
- stats.patterns = safeCount("kota_patterns");
1569
-
1570
- // Count failures (may not exist in all installations)
1571
- stats.failures = safeCount("kota_failures");
1572
-
1573
- // Count repositories
1574
- stats.repositories = safeCount("repositories");
1575
-
1576
- return stats;
1577
- }
1578
-
1579
- /**
1580
- * Get index statistics for startup context display (public API).
1581
- *
1582
- * @returns Statistics object with counts by type
1583
- */
1584
- export function getIndexStatistics(): ReturnType<
1585
- typeof getIndexStatisticsInternal
1586
- > {
1587
- return getIndexStatisticsInternal(getGlobalDatabase());
1410
+ throw new Error('createDefaultOrganization() is not available in local-only mode - organizations are a cloud-only feature');
1588
1411
  }