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.
- package/package.json +1 -1
- package/src/api/queries.ts +812 -989
- package/src/db/migrations/004_memory_layer.sql +144 -16
- package/src/db/sqlite/sqlite-client.ts +1 -26
- package/src/db/sqlite-schema.sql +0 -48
- package/src/indexer/constants.ts +0 -1
- package/src/indexer/parsers.ts +1 -3
- package/src/mcp/server.ts +0 -2
- package/src/mcp/tools.ts +9 -230
- package/src/db/migrations/005_workflow_contexts.sql +0 -41
- package/src/db/migrations/006_add_migration_checksums.sql +0 -15
- package/src/db/sqlite/migration-runner.ts +0 -335
package/src/api/queries.ts
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
65
|
+
db: KotaDatabase,
|
|
66
|
+
files: IndexedFile[],
|
|
67
|
+
repositoryId: string,
|
|
124
68
|
): number {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
69
|
+
if (files.length === 0) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
128
72
|
|
|
129
|
-
|
|
73
|
+
let count = 0;
|
|
130
74
|
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
110
|
+
db: KotaDatabase,
|
|
111
|
+
symbols: ExtractedSymbol[],
|
|
112
|
+
fileId: string,
|
|
173
113
|
): number {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
114
|
+
if (symbols.length === 0) {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
177
117
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
124
|
+
if (!fileResult) {
|
|
125
|
+
throw new Error(`File not found: ${fileId}`);
|
|
126
|
+
}
|
|
187
127
|
|
|
188
|
-
|
|
189
|
-
|
|
128
|
+
const repositoryId = fileResult.repository_id;
|
|
129
|
+
let count = 0;
|
|
190
130
|
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
178
|
+
if (references.length === 0) {
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
241
181
|
|
|
242
|
-
|
|
182
|
+
let count = 0;
|
|
243
183
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
260
|
+
db: KotaDatabase,
|
|
261
|
+
term: string,
|
|
262
|
+
repositoryId: string | undefined,
|
|
263
|
+
limit: number,
|
|
324
264
|
): IndexedFile[] {
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
324
|
+
db: KotaDatabase,
|
|
325
|
+
limit: number,
|
|
326
|
+
repositoryId?: string,
|
|
389
327
|
): IndexedFile[] {
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
370
|
+
db: KotaDatabase,
|
|
371
|
+
filePath: string,
|
|
372
|
+
repositoryId: string,
|
|
435
373
|
): string | null {
|
|
436
|
-
|
|
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
|
-
|
|
444
|
-
|
|
381
|
+
const result = db.queryOne<{ id: string }>(sql, [repositoryId, filePath]);
|
|
382
|
+
return result?.id || null;
|
|
445
383
|
}
|
|
446
384
|
|
|
447
385
|
function ensureRepositoryInternal(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
386
|
+
db: KotaDatabase,
|
|
387
|
+
fullName: string,
|
|
388
|
+
gitUrl?: string,
|
|
389
|
+
defaultBranch?: string,
|
|
452
390
|
): string {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
486
|
-
|
|
425
|
+
db: KotaDatabase,
|
|
426
|
+
repositoryId: string,
|
|
487
427
|
): void {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
509
|
-
|
|
448
|
+
files: IndexedFile[],
|
|
449
|
+
repositoryId: string,
|
|
510
450
|
): number {
|
|
511
|
-
|
|
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
|
-
|
|
523
|
-
|
|
462
|
+
symbols: ExtractedSymbol[],
|
|
463
|
+
fileId: string,
|
|
524
464
|
): number {
|
|
525
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
476
|
+
fileId: string,
|
|
477
|
+
filePath: string,
|
|
478
|
+
references: Reference[],
|
|
479
|
+
allFiles: Array<{ path: string }>,
|
|
480
|
+
pathMappings?: PathMappings | null
|
|
541
481
|
): number {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
574
|
-
|
|
513
|
+
term: string,
|
|
514
|
+
options: SearchOptions = {},
|
|
575
515
|
): IndexedFile[] {
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
593
|
-
|
|
527
|
+
limit: number,
|
|
528
|
+
repositoryId?: string,
|
|
594
529
|
): IndexedFile[] {
|
|
595
|
-
|
|
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
|
-
|
|
607
|
-
|
|
541
|
+
filePath: string,
|
|
542
|
+
repositoryId: string,
|
|
608
543
|
): string | null {
|
|
609
|
-
|
|
544
|
+
return resolveFilePathInternal(getGlobalDatabase(), filePath, repositoryId);
|
|
610
545
|
}
|
|
611
546
|
|
|
612
547
|
export interface DependencyResult {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
573
|
+
fileId: string,
|
|
574
|
+
depth: number,
|
|
575
|
+
includeTests: boolean,
|
|
576
|
+
referenceTypes: string[] = ["import", "re_export", "export_all"],
|
|
642
577
|
): DependencyResult {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
675
|
+
fileId: string,
|
|
676
|
+
depth: number,
|
|
677
|
+
referenceTypes: string[] = ["import", "re_export", "export_all"],
|
|
748
678
|
): DependencyResult {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
838
|
+
fullName: string,
|
|
839
|
+
gitUrl?: string,
|
|
840
|
+
defaultBranch?: string,
|
|
918
841
|
): string {
|
|
919
|
-
|
|
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(
|
|
933
|
-
|
|
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(
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
1168
|
-
|
|
1080
|
+
db: KotaDatabase,
|
|
1081
|
+
repositoryId: string,
|
|
1169
1082
|
): boolean {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1112
|
+
db: KotaDatabase,
|
|
1113
|
+
repositoryId: string,
|
|
1114
|
+
filePath: string,
|
|
1202
1115
|
): boolean {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
1242
|
-
|
|
1150
|
+
repositoryId: string,
|
|
1151
|
+
filePath: string,
|
|
1243
1152
|
): boolean {
|
|
1244
|
-
|
|
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
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1166
|
+
db: KotaDatabase,
|
|
1167
|
+
repositoryId: string,
|
|
1168
|
+
filePaths: string[],
|
|
1260
1169
|
): { deletedCount: number; deletedPaths: string[] } {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
-
|
|
1300
|
-
|
|
1208
|
+
repositoryId: string,
|
|
1209
|
+
filePaths: string[],
|
|
1301
1210
|
): { deletedCount: number; deletedPaths: string[] } {
|
|
1302
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
1222
|
+
db: KotaDatabase,
|
|
1223
|
+
fullName: string,
|
|
1319
1224
|
): string | null {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1252
|
+
db: KotaDatabase,
|
|
1253
|
+
files: IndexedFile[],
|
|
1254
|
+
repositoryId: string
|
|
1349
1255
|
): number {
|
|
1350
|
-
|
|
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
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1263
|
+
db: KotaDatabase,
|
|
1264
|
+
symbols: ExtractedSymbol[],
|
|
1265
|
+
fileId: string
|
|
1360
1266
|
): number {
|
|
1361
|
-
|
|
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
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1297
|
+
db: KotaDatabase,
|
|
1298
|
+
term: string,
|
|
1299
|
+
repositoryId: string | undefined,
|
|
1300
|
+
limit: number
|
|
1403
1301
|
): IndexedFile[] {
|
|
1404
|
-
|
|
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
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1309
|
+
db: KotaDatabase,
|
|
1310
|
+
limit: number,
|
|
1311
|
+
repositoryId?: string,
|
|
1414
1312
|
): IndexedFile[] {
|
|
1415
|
-
|
|
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
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1320
|
+
db: KotaDatabase,
|
|
1321
|
+
filePath: string,
|
|
1322
|
+
repositoryId: string
|
|
1425
1323
|
): string | null {
|
|
1426
|
-
|
|
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
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1331
|
+
db: KotaDatabase,
|
|
1332
|
+
fullName: string,
|
|
1333
|
+
gitUrl?: string,
|
|
1334
|
+
defaultBranch?: string
|
|
1437
1335
|
): string {
|
|
1438
|
-
|
|
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
|
-
|
|
1446
|
-
|
|
1343
|
+
db: KotaDatabase,
|
|
1344
|
+
repositoryId: string
|
|
1447
1345
|
): void {
|
|
1448
|
-
|
|
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
|
-
|
|
1457
|
-
|
|
1354
|
+
db: KotaDatabase,
|
|
1355
|
+
repositoryId: string
|
|
1458
1356
|
): boolean {
|
|
1459
|
-
|
|
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
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1365
|
+
db: KotaDatabase,
|
|
1366
|
+
repositoryId: string,
|
|
1367
|
+
filePath: string
|
|
1470
1368
|
): boolean {
|
|
1471
|
-
|
|
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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1377
|
+
db: KotaDatabase,
|
|
1378
|
+
repositoryId: string,
|
|
1379
|
+
filePaths: string[]
|
|
1482
1380
|
): { deletedCount: number; deletedPaths: string[] } {
|
|
1483
|
-
|
|
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
|
-
|
|
1492
|
-
|
|
1389
|
+
db: KotaDatabase,
|
|
1390
|
+
fullName: string
|
|
1493
1391
|
): string | null {
|
|
1494
|
-
|
|
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
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1406
|
+
_client: unknown,
|
|
1407
|
+
_userId: string,
|
|
1408
|
+
_userEmail?: string,
|
|
1510
1409
|
): Promise<string> {
|
|
1511
|
-
|
|
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
|
}
|