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