kotadb 2.0.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/README.md +79 -0
- package/package.json +75 -0
- package/src/api/auto-reindex.ts +55 -0
- package/src/api/openapi/builder.ts +209 -0
- package/src/api/openapi/paths.ts +354 -0
- package/src/api/openapi/schemas.ts +608 -0
- package/src/api/queries.ts +1168 -0
- package/src/api/routes.ts +339 -0
- package/src/auth/middleware.ts +83 -0
- package/src/cli.ts +221 -0
- package/src/config/constants.ts +96 -0
- package/src/config/environment.ts +96 -0
- package/src/config/gitignore.ts +68 -0
- package/src/config/index.ts +20 -0
- package/src/config/project-root.ts +52 -0
- package/src/db/client.ts +72 -0
- package/src/db/sqlite/index.ts +35 -0
- package/src/db/sqlite/jsonl-exporter.ts +416 -0
- package/src/db/sqlite/jsonl-importer.ts +361 -0
- package/src/db/sqlite/sqlite-client.ts +536 -0
- package/src/index.ts +66 -0
- package/src/indexer/ast-parser.ts +146 -0
- package/src/indexer/ast-types.ts +54 -0
- package/src/indexer/circular-detector.ts +262 -0
- package/src/indexer/dependency-extractor.ts +352 -0
- package/src/indexer/extractors.ts +54 -0
- package/src/indexer/import-resolver.ts +167 -0
- package/src/indexer/parsers.ts +177 -0
- package/src/indexer/reference-extractor.ts +488 -0
- package/src/indexer/repos.ts +245 -0
- package/src/indexer/storage.ts +277 -0
- package/src/indexer/symbol-extractor.ts +660 -0
- package/src/instrument.ts +88 -0
- package/src/logging/context.ts +46 -0
- package/src/logging/logger.ts +193 -0
- package/src/logging/middleware.ts +107 -0
- package/src/mcp/github-integration.ts +293 -0
- package/src/mcp/headers.ts +101 -0
- package/src/mcp/impact-analysis.ts +495 -0
- package/src/mcp/jsonrpc.ts +141 -0
- package/src/mcp/lifecycle.ts +73 -0
- package/src/mcp/server.ts +202 -0
- package/src/mcp/session.ts +44 -0
- package/src/mcp/spec-validation.ts +491 -0
- package/src/mcp/tools.ts +889 -0
- package/src/sync/deletion-manifest.ts +210 -0
- package/src/sync/index.ts +16 -0
- package/src/sync/merge-driver.ts +172 -0
- package/src/sync/watcher.ts +221 -0
- package/src/types/rate-limit.ts +88 -0
- package/src/validation/common-schemas.ts +96 -0
- package/src/validation/schemas.ts +187 -0
|
@@ -0,0 +1,1168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database query layer for indexed data
|
|
3
|
+
*
|
|
4
|
+
* Local-only implementation using SQLite for all operations.
|
|
5
|
+
*
|
|
6
|
+
* @module @api/queries
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import type { Reference } from "@indexer/reference-extractor";
|
|
11
|
+
import type { Symbol as ExtractedSymbol, SymbolKind } from "@indexer/symbol-extractor";
|
|
12
|
+
import { createLogger } from "@logging/logger.js";
|
|
13
|
+
import type { IndexRequest, IndexedFile } from "@shared/types";
|
|
14
|
+
import { detectLanguage } from "@shared/language-utils";
|
|
15
|
+
import { getGlobalDatabase, type KotaDatabase } from "@db/sqlite/index.js";
|
|
16
|
+
|
|
17
|
+
const logger = createLogger({ module: "api-queries" });
|
|
18
|
+
|
|
19
|
+
export interface SearchOptions {
|
|
20
|
+
repositoryId?: string;
|
|
21
|
+
projectId?: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Internal implementations that accept a database parameter
|
|
27
|
+
// These are used by both the new API and backward-compatible aliases
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
function saveIndexedFilesInternal(
|
|
31
|
+
db: KotaDatabase,
|
|
32
|
+
files: IndexedFile[],
|
|
33
|
+
repositoryId: string,
|
|
34
|
+
): number {
|
|
35
|
+
if (files.length === 0) {
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let count = 0;
|
|
40
|
+
|
|
41
|
+
db.transaction(() => {
|
|
42
|
+
const stmt = db.prepare(`
|
|
43
|
+
INSERT OR REPLACE INTO indexed_files (
|
|
44
|
+
id, repository_id, path, content, language,
|
|
45
|
+
size_bytes, content_hash, indexed_at, metadata
|
|
46
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
47
|
+
`);
|
|
48
|
+
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
const id = randomUUID();
|
|
51
|
+
const language = detectLanguage(file.path);
|
|
52
|
+
const sizeBytes = new TextEncoder().encode(file.content).length;
|
|
53
|
+
const indexedAt = file.indexedAt ? file.indexedAt.toISOString() : new Date().toISOString();
|
|
54
|
+
const metadata = JSON.stringify({ dependencies: file.dependencies || [] });
|
|
55
|
+
|
|
56
|
+
stmt.run([
|
|
57
|
+
id,
|
|
58
|
+
repositoryId,
|
|
59
|
+
file.path,
|
|
60
|
+
file.content,
|
|
61
|
+
language,
|
|
62
|
+
sizeBytes,
|
|
63
|
+
null, // content_hash
|
|
64
|
+
indexedAt,
|
|
65
|
+
metadata
|
|
66
|
+
]);
|
|
67
|
+
count++;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
logger.info("Saved indexed files to SQLite", { count, repositoryId });
|
|
72
|
+
return count;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function storeSymbolsInternal(
|
|
76
|
+
db: KotaDatabase,
|
|
77
|
+
symbols: ExtractedSymbol[],
|
|
78
|
+
fileId: string,
|
|
79
|
+
): number {
|
|
80
|
+
if (symbols.length === 0) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Get repository_id from the file
|
|
85
|
+
const fileResult = db.queryOne<{ repository_id: string }>(
|
|
86
|
+
"SELECT repository_id FROM indexed_files WHERE id = ?",
|
|
87
|
+
[fileId]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (!fileResult) {
|
|
91
|
+
throw new Error(`File not found: ${fileId}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const repositoryId = fileResult.repository_id;
|
|
95
|
+
let count = 0;
|
|
96
|
+
|
|
97
|
+
db.transaction(() => {
|
|
98
|
+
const stmt = db.prepare(`
|
|
99
|
+
INSERT OR REPLACE INTO indexed_symbols (
|
|
100
|
+
id, file_id, repository_id, name, kind,
|
|
101
|
+
line_start, line_end, signature, documentation, metadata
|
|
102
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
for (const symbol of symbols) {
|
|
106
|
+
const id = randomUUID();
|
|
107
|
+
const metadata = JSON.stringify({
|
|
108
|
+
column_start: symbol.columnStart,
|
|
109
|
+
column_end: symbol.columnEnd,
|
|
110
|
+
is_exported: symbol.isExported,
|
|
111
|
+
is_async: symbol.isAsync,
|
|
112
|
+
access_modifier: symbol.accessModifier,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
stmt.run([
|
|
116
|
+
id,
|
|
117
|
+
fileId,
|
|
118
|
+
repositoryId,
|
|
119
|
+
symbol.name,
|
|
120
|
+
symbol.kind,
|
|
121
|
+
symbol.lineStart,
|
|
122
|
+
symbol.lineEnd,
|
|
123
|
+
symbol.signature || null,
|
|
124
|
+
symbol.documentation || null,
|
|
125
|
+
metadata
|
|
126
|
+
]);
|
|
127
|
+
count++;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
logger.info("Stored symbols to SQLite", { count, fileId });
|
|
132
|
+
return count;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function storeReferencesInternal(
|
|
136
|
+
db: KotaDatabase,
|
|
137
|
+
references: Reference[],
|
|
138
|
+
fileId: string,
|
|
139
|
+
): number {
|
|
140
|
+
if (references.length === 0) {
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get repository_id from the file
|
|
145
|
+
const fileResult = db.queryOne<{ repository_id: string }>(
|
|
146
|
+
"SELECT repository_id FROM indexed_files WHERE id = ?",
|
|
147
|
+
[fileId]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (!fileResult) {
|
|
151
|
+
throw new Error(`File not found: ${fileId}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const repositoryId = fileResult.repository_id;
|
|
155
|
+
let count = 0;
|
|
156
|
+
|
|
157
|
+
db.transaction(() => {
|
|
158
|
+
// First, delete existing references for this file
|
|
159
|
+
db.run("DELETE FROM indexed_references WHERE file_id = ?", [fileId]);
|
|
160
|
+
|
|
161
|
+
const stmt = db.prepare(`
|
|
162
|
+
INSERT INTO indexed_references (
|
|
163
|
+
id, file_id, repository_id, symbol_name, target_symbol_id,
|
|
164
|
+
target_file_path, line_number, column_number, reference_type, metadata
|
|
165
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
166
|
+
`);
|
|
167
|
+
|
|
168
|
+
for (const ref of references) {
|
|
169
|
+
const id = randomUUID();
|
|
170
|
+
const metadata = JSON.stringify({
|
|
171
|
+
target_name: ref.targetName,
|
|
172
|
+
column_number: ref.columnNumber,
|
|
173
|
+
...ref.metadata,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
stmt.run([
|
|
177
|
+
id,
|
|
178
|
+
fileId,
|
|
179
|
+
repositoryId,
|
|
180
|
+
ref.targetName || "unknown",
|
|
181
|
+
null, // target_symbol_id - deferred
|
|
182
|
+
null, // target_file_path - deferred
|
|
183
|
+
ref.lineNumber,
|
|
184
|
+
ref.columnNumber || 0,
|
|
185
|
+
ref.referenceType,
|
|
186
|
+
metadata
|
|
187
|
+
]);
|
|
188
|
+
count++;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
logger.info("Stored references to SQLite", { count, fileId });
|
|
193
|
+
return count;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function storeDependenciesInternal(
|
|
197
|
+
db: KotaDatabase,
|
|
198
|
+
dependencies: Array<{
|
|
199
|
+
repositoryId: string;
|
|
200
|
+
fromFileId: string | null;
|
|
201
|
+
toFileId: string | null;
|
|
202
|
+
fromSymbolId: string | null;
|
|
203
|
+
toSymbolId: string | null;
|
|
204
|
+
dependencyType: "file_import" | "symbol_usage";
|
|
205
|
+
metadata: Record<string, unknown>;
|
|
206
|
+
}>,
|
|
207
|
+
): number {
|
|
208
|
+
if (dependencies.length === 0) {
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let count = 0;
|
|
213
|
+
|
|
214
|
+
db.transaction(() => {
|
|
215
|
+
const stmt = db.prepare(`
|
|
216
|
+
INSERT INTO dependency_graph (
|
|
217
|
+
id, repository_id, from_file_id, to_file_id,
|
|
218
|
+
from_symbol_id, to_symbol_id, dependency_type, metadata
|
|
219
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
220
|
+
`);
|
|
221
|
+
|
|
222
|
+
for (const dep of dependencies) {
|
|
223
|
+
const id = randomUUID();
|
|
224
|
+
const metadata = JSON.stringify(dep.metadata);
|
|
225
|
+
|
|
226
|
+
stmt.run([
|
|
227
|
+
id,
|
|
228
|
+
dep.repositoryId,
|
|
229
|
+
dep.fromFileId,
|
|
230
|
+
dep.toFileId,
|
|
231
|
+
dep.fromSymbolId,
|
|
232
|
+
dep.toSymbolId,
|
|
233
|
+
dep.dependencyType,
|
|
234
|
+
metadata
|
|
235
|
+
]);
|
|
236
|
+
count++;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
logger.info("Stored dependencies to SQLite", { count });
|
|
241
|
+
return count;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Escape a search term for use in SQLite FTS5 MATCH clause.
|
|
246
|
+
* Wraps the entire term in double quotes for exact phrase matching.
|
|
247
|
+
* Escapes internal double quotes by doubling them.
|
|
248
|
+
*
|
|
249
|
+
* This ensures that:
|
|
250
|
+
* - Multi-word searches match adjacent words in order ("hello world")
|
|
251
|
+
* - Hyphenated terms don't trigger FTS5 operator parsing ("mom-and-pop")
|
|
252
|
+
* - FTS5 keywords (AND, OR, NOT) are treated as literals, not operators
|
|
253
|
+
*
|
|
254
|
+
* @param term - Raw search term from user input
|
|
255
|
+
* @returns Escaped term safe for FTS5 MATCH clause
|
|
256
|
+
*/
|
|
257
|
+
function escapeFts5Term(term: string): string {
|
|
258
|
+
// Escape internal double quotes by doubling them
|
|
259
|
+
const escaped = term.replace(/"/g, '""');
|
|
260
|
+
// Wrap in double quotes for exact phrase matching
|
|
261
|
+
return `"${escaped}"`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function searchFilesInternal(
|
|
265
|
+
db: KotaDatabase,
|
|
266
|
+
term: string,
|
|
267
|
+
repositoryId: string | undefined,
|
|
268
|
+
limit: number,
|
|
269
|
+
): IndexedFile[] {
|
|
270
|
+
const hasRepoFilter = repositoryId !== undefined;
|
|
271
|
+
const sql = hasRepoFilter
|
|
272
|
+
? `
|
|
273
|
+
SELECT
|
|
274
|
+
f.id,
|
|
275
|
+
f.repository_id,
|
|
276
|
+
f.path,
|
|
277
|
+
f.content,
|
|
278
|
+
f.metadata,
|
|
279
|
+
f.indexed_at,
|
|
280
|
+
snippet(indexed_files_fts, 1, '<mark>', '</mark>', '...', 32) AS snippet
|
|
281
|
+
FROM indexed_files_fts fts
|
|
282
|
+
JOIN indexed_files f ON fts.rowid = f.rowid
|
|
283
|
+
WHERE indexed_files_fts MATCH ?
|
|
284
|
+
AND f.repository_id = ?
|
|
285
|
+
ORDER BY bm25(indexed_files_fts)
|
|
286
|
+
LIMIT ?
|
|
287
|
+
`
|
|
288
|
+
: `
|
|
289
|
+
SELECT
|
|
290
|
+
f.id,
|
|
291
|
+
f.repository_id,
|
|
292
|
+
f.path,
|
|
293
|
+
f.content,
|
|
294
|
+
f.metadata,
|
|
295
|
+
f.indexed_at,
|
|
296
|
+
snippet(indexed_files_fts, 1, '<mark>', '</mark>', '...', 32) AS snippet
|
|
297
|
+
FROM indexed_files_fts fts
|
|
298
|
+
JOIN indexed_files f ON fts.rowid = f.rowid
|
|
299
|
+
WHERE indexed_files_fts MATCH ?
|
|
300
|
+
ORDER BY bm25(indexed_files_fts)
|
|
301
|
+
LIMIT ?
|
|
302
|
+
`;
|
|
303
|
+
|
|
304
|
+
const escapedTerm = escapeFts5Term(term);
|
|
305
|
+
const params = hasRepoFilter ? [escapedTerm, repositoryId, limit] : [escapedTerm, limit];
|
|
306
|
+
const rows = db.query<{
|
|
307
|
+
id: string;
|
|
308
|
+
repository_id: string;
|
|
309
|
+
path: string;
|
|
310
|
+
content: string;
|
|
311
|
+
metadata: string;
|
|
312
|
+
indexed_at: string;
|
|
313
|
+
}>(sql, params);
|
|
314
|
+
|
|
315
|
+
return rows.map((row) => {
|
|
316
|
+
const metadata = JSON.parse(row.metadata || '{}');
|
|
317
|
+
return {
|
|
318
|
+
id: row.id,
|
|
319
|
+
projectRoot: row.repository_id,
|
|
320
|
+
path: row.path,
|
|
321
|
+
content: row.content,
|
|
322
|
+
dependencies: metadata.dependencies || [],
|
|
323
|
+
indexedAt: new Date(row.indexed_at),
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function listRecentFilesInternal(
|
|
329
|
+
db: KotaDatabase,
|
|
330
|
+
limit: number,
|
|
331
|
+
repositoryId?: string,
|
|
332
|
+
): IndexedFile[] {
|
|
333
|
+
const hasRepoFilter = repositoryId !== undefined;
|
|
334
|
+
const sql = hasRepoFilter
|
|
335
|
+
? `
|
|
336
|
+
SELECT
|
|
337
|
+
id, repository_id, path, content, metadata, indexed_at
|
|
338
|
+
FROM indexed_files
|
|
339
|
+
WHERE repository_id = ?
|
|
340
|
+
ORDER BY indexed_at DESC
|
|
341
|
+
LIMIT ?
|
|
342
|
+
`
|
|
343
|
+
: `
|
|
344
|
+
SELECT
|
|
345
|
+
id, repository_id, path, content, metadata, indexed_at
|
|
346
|
+
FROM indexed_files
|
|
347
|
+
ORDER BY indexed_at DESC
|
|
348
|
+
LIMIT ?
|
|
349
|
+
`;
|
|
350
|
+
|
|
351
|
+
const params = hasRepoFilter ? [repositoryId, limit] : [limit];
|
|
352
|
+
const rows = db.query<{
|
|
353
|
+
id: string;
|
|
354
|
+
repository_id: string;
|
|
355
|
+
path: string;
|
|
356
|
+
content: string;
|
|
357
|
+
metadata: string;
|
|
358
|
+
indexed_at: string;
|
|
359
|
+
}>(sql, params);
|
|
360
|
+
|
|
361
|
+
return rows.map((row) => {
|
|
362
|
+
const metadata = JSON.parse(row.metadata || '{}');
|
|
363
|
+
return {
|
|
364
|
+
id: row.id,
|
|
365
|
+
projectRoot: row.repository_id,
|
|
366
|
+
path: row.path,
|
|
367
|
+
content: row.content,
|
|
368
|
+
dependencies: metadata.dependencies || [],
|
|
369
|
+
indexedAt: new Date(row.indexed_at),
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function resolveFilePathInternal(
|
|
375
|
+
db: KotaDatabase,
|
|
376
|
+
filePath: string,
|
|
377
|
+
repositoryId: string,
|
|
378
|
+
): string | null {
|
|
379
|
+
const sql = `
|
|
380
|
+
SELECT id
|
|
381
|
+
FROM indexed_files
|
|
382
|
+
WHERE repository_id = ? AND path = ?
|
|
383
|
+
LIMIT 1
|
|
384
|
+
`;
|
|
385
|
+
|
|
386
|
+
const result = db.queryOne<{ id: string }>(sql, [repositoryId, filePath]);
|
|
387
|
+
return result?.id || null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function ensureRepositoryInternal(
|
|
391
|
+
db: KotaDatabase,
|
|
392
|
+
fullName: string,
|
|
393
|
+
gitUrl?: string,
|
|
394
|
+
defaultBranch?: string,
|
|
395
|
+
): string {
|
|
396
|
+
// Check if repository already exists
|
|
397
|
+
const existing = db.queryOne<{ id: string }>(
|
|
398
|
+
"SELECT id FROM repositories WHERE full_name = ?",
|
|
399
|
+
[fullName]
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
if (existing) {
|
|
403
|
+
logger.debug("Repository already exists in SQLite", { fullName, id: existing.id });
|
|
404
|
+
return existing.id;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Create new repository
|
|
408
|
+
const id = randomUUID();
|
|
409
|
+
const name = fullName.split("/").pop() || fullName;
|
|
410
|
+
const now = new Date().toISOString();
|
|
411
|
+
|
|
412
|
+
db.run(`
|
|
413
|
+
INSERT INTO repositories (id, name, full_name, git_url, default_branch, created_at, updated_at)
|
|
414
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
415
|
+
`, [
|
|
416
|
+
id,
|
|
417
|
+
name,
|
|
418
|
+
fullName,
|
|
419
|
+
gitUrl || null,
|
|
420
|
+
defaultBranch || "main",
|
|
421
|
+
now,
|
|
422
|
+
now
|
|
423
|
+
]);
|
|
424
|
+
|
|
425
|
+
logger.info("Created repository in SQLite", { fullName, id });
|
|
426
|
+
return id;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function updateRepositoryLastIndexedInternal(
|
|
430
|
+
db: KotaDatabase,
|
|
431
|
+
repositoryId: string,
|
|
432
|
+
): void {
|
|
433
|
+
const now = new Date().toISOString();
|
|
434
|
+
db.run(
|
|
435
|
+
"UPDATE repositories SET last_indexed_at = ?, updated_at = ? WHERE id = ?",
|
|
436
|
+
[now, now, repositoryId]
|
|
437
|
+
);
|
|
438
|
+
logger.debug("Updated repository last_indexed_at", { repositoryId });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ============================================================================
|
|
442
|
+
// Public API - uses global database
|
|
443
|
+
// ============================================================================
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Save indexed files to SQLite database
|
|
447
|
+
*
|
|
448
|
+
* @param files - Array of indexed files
|
|
449
|
+
* @param repositoryId - Repository UUID
|
|
450
|
+
* @returns Number of files saved
|
|
451
|
+
*/
|
|
452
|
+
export function saveIndexedFiles(
|
|
453
|
+
files: IndexedFile[],
|
|
454
|
+
repositoryId: string,
|
|
455
|
+
): number {
|
|
456
|
+
return saveIndexedFilesInternal(getGlobalDatabase(), files, repositoryId);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Store symbols extracted from AST into SQLite database.
|
|
461
|
+
*
|
|
462
|
+
* @param symbols - Array of extracted symbols
|
|
463
|
+
* @param fileId - UUID of the indexed file
|
|
464
|
+
* @returns Number of symbols stored
|
|
465
|
+
*/
|
|
466
|
+
export function storeSymbols(
|
|
467
|
+
symbols: ExtractedSymbol[],
|
|
468
|
+
fileId: string,
|
|
469
|
+
): number {
|
|
470
|
+
return storeSymbolsInternal(getGlobalDatabase(), symbols, fileId);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Store references extracted from AST into SQLite database.
|
|
475
|
+
*
|
|
476
|
+
* @param references - Array of extracted references
|
|
477
|
+
* @param fileId - UUID of the source file
|
|
478
|
+
* @returns Number of references stored
|
|
479
|
+
*/
|
|
480
|
+
export function storeReferences(
|
|
481
|
+
references: Reference[],
|
|
482
|
+
fileId: string,
|
|
483
|
+
): number {
|
|
484
|
+
return storeReferencesInternal(getGlobalDatabase(), references, fileId);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Store dependency graph edges into SQLite database.
|
|
489
|
+
*
|
|
490
|
+
* @param dependencies - Array of dependency edges
|
|
491
|
+
* @returns Number of dependencies stored
|
|
492
|
+
*/
|
|
493
|
+
export function storeDependencies(
|
|
494
|
+
dependencies: Array<{
|
|
495
|
+
repositoryId: string;
|
|
496
|
+
fromFileId: string | null;
|
|
497
|
+
toFileId: string | null;
|
|
498
|
+
fromSymbolId: string | null;
|
|
499
|
+
toSymbolId: string | null;
|
|
500
|
+
dependencyType: "file_import" | "symbol_usage";
|
|
501
|
+
metadata: Record<string, unknown>;
|
|
502
|
+
}>,
|
|
503
|
+
): number {
|
|
504
|
+
return storeDependenciesInternal(getGlobalDatabase(), dependencies);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Search indexed files by content term using FTS5.
|
|
509
|
+
*
|
|
510
|
+
* @param term - Search term to match in file content
|
|
511
|
+
* @param options - Search options (repositoryId filter, limit)
|
|
512
|
+
* @returns Array of matching indexed files
|
|
513
|
+
*/
|
|
514
|
+
export function searchFiles(
|
|
515
|
+
term: string,
|
|
516
|
+
options: SearchOptions = {},
|
|
517
|
+
): IndexedFile[] {
|
|
518
|
+
const limit = Math.min(Math.max(options.limit ?? 20, 1), 100);
|
|
519
|
+
return searchFilesInternal(getGlobalDatabase(), term, options.repositoryId, limit);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* List recently indexed files.
|
|
524
|
+
*
|
|
525
|
+
* @param limit - Maximum number of files to return
|
|
526
|
+
* @returns Array of recently indexed files
|
|
527
|
+
*/
|
|
528
|
+
export function listRecentFiles(
|
|
529
|
+
limit: number,
|
|
530
|
+
repositoryId?: string,
|
|
531
|
+
): IndexedFile[] {
|
|
532
|
+
return listRecentFilesInternal(getGlobalDatabase(), limit, repositoryId);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Resolve file path to file UUID.
|
|
537
|
+
*
|
|
538
|
+
* @param filePath - Relative file path to resolve
|
|
539
|
+
* @param repositoryId - Repository UUID
|
|
540
|
+
* @returns File UUID or null if not found
|
|
541
|
+
*/
|
|
542
|
+
export function resolveFilePath(
|
|
543
|
+
filePath: string,
|
|
544
|
+
repositoryId: string,
|
|
545
|
+
): string | null {
|
|
546
|
+
return resolveFilePathInternal(getGlobalDatabase(), filePath, repositoryId);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export interface DependencyResult {
|
|
550
|
+
direct: string[];
|
|
551
|
+
indirect: Record<string, string[]>;
|
|
552
|
+
cycles: string[][];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Query files that depend on the given file (reverse lookup).
|
|
557
|
+
*
|
|
558
|
+
* @param fileId - Target file UUID
|
|
559
|
+
* @param depth - Recursion depth (1-5)
|
|
560
|
+
* @param includeTests - Whether to include test files
|
|
561
|
+
* @returns Dependency result with direct/indirect relationships and cycles
|
|
562
|
+
*/
|
|
563
|
+
export function queryDependents(
|
|
564
|
+
fileId: string,
|
|
565
|
+
depth: number,
|
|
566
|
+
includeTests: boolean,
|
|
567
|
+
): DependencyResult {
|
|
568
|
+
const db = getGlobalDatabase();
|
|
569
|
+
const fileRecord = db.queryOne<{ repository_id: string }>(
|
|
570
|
+
"SELECT repository_id FROM indexed_files WHERE id = ?",
|
|
571
|
+
[fileId]
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
if (!fileRecord) {
|
|
575
|
+
throw new Error(`File not found: ${fileId}`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const results = queryDependentsRaw(db, fileRecord.repository_id, fileId, null, depth);
|
|
579
|
+
return processDepthResults(results, includeTests);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Query files that the given file depends on (forward lookup).
|
|
584
|
+
*
|
|
585
|
+
* @param fileId - Source file UUID
|
|
586
|
+
* @param depth - Recursion depth (1-5)
|
|
587
|
+
* @returns Dependency result with direct/indirect relationships and cycles
|
|
588
|
+
*/
|
|
589
|
+
export function queryDependencies(
|
|
590
|
+
fileId: string,
|
|
591
|
+
depth: number,
|
|
592
|
+
): DependencyResult {
|
|
593
|
+
const db = getGlobalDatabase();
|
|
594
|
+
const fileRecord = db.queryOne<{ repository_id: string }>(
|
|
595
|
+
"SELECT repository_id FROM indexed_files WHERE id = ?",
|
|
596
|
+
[fileId]
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
if (!fileRecord) {
|
|
600
|
+
throw new Error(`File not found: ${fileId}`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const results = queryDependenciesRaw(db, fileRecord.repository_id, fileId, null, depth);
|
|
604
|
+
return processDepthResults(results, true);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function processDepthResults(
|
|
608
|
+
results: Array<{
|
|
609
|
+
file_path: string | null;
|
|
610
|
+
depth: number;
|
|
611
|
+
}>,
|
|
612
|
+
includeTests: boolean
|
|
613
|
+
): DependencyResult {
|
|
614
|
+
const direct: string[] = [];
|
|
615
|
+
const indirect: Record<string, string[]> = {};
|
|
616
|
+
const cycles: string[][] = [];
|
|
617
|
+
|
|
618
|
+
for (const result of results) {
|
|
619
|
+
if (!result.file_path) continue;
|
|
620
|
+
|
|
621
|
+
if (!includeTests && (result.file_path.includes("test") || result.file_path.includes("spec"))) {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (result.depth === 1) {
|
|
626
|
+
if (!direct.includes(result.file_path)) {
|
|
627
|
+
direct.push(result.file_path);
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
const key = `depth_${result.depth}`;
|
|
631
|
+
if (!indirect[key]) {
|
|
632
|
+
indirect[key] = [];
|
|
633
|
+
}
|
|
634
|
+
if (!indirect[key].includes(result.file_path)) {
|
|
635
|
+
indirect[key].push(result.file_path);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return { direct, indirect, cycles };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Query dependents (reverse lookup): files/symbols that depend on the target
|
|
645
|
+
*
|
|
646
|
+
* @internal - Use queryDependents() for the wrapped version
|
|
647
|
+
*/
|
|
648
|
+
export function queryDependentsRaw(
|
|
649
|
+
db: KotaDatabase,
|
|
650
|
+
repositoryId: string,
|
|
651
|
+
fileId: string | null,
|
|
652
|
+
symbolId: string | null = null,
|
|
653
|
+
depth: number = 5
|
|
654
|
+
): Array<{
|
|
655
|
+
file_id: string | null;
|
|
656
|
+
file_path: string | null;
|
|
657
|
+
symbol_id: string | null;
|
|
658
|
+
symbol_name: string | null;
|
|
659
|
+
dependency_type: string;
|
|
660
|
+
depth: number;
|
|
661
|
+
}> {
|
|
662
|
+
const targetCondition = symbolId
|
|
663
|
+
? 'AND dg.to_symbol_id = ?'
|
|
664
|
+
: 'AND dg.to_file_id = ?';
|
|
665
|
+
|
|
666
|
+
const recursiveJoinCondition = symbolId
|
|
667
|
+
? 'dg.to_symbol_id = d.from_symbol_id'
|
|
668
|
+
: 'dg.to_file_id = d.from_file_id';
|
|
669
|
+
|
|
670
|
+
const sql = `
|
|
671
|
+
WITH RECURSIVE dependents AS (
|
|
672
|
+
SELECT
|
|
673
|
+
dg.id,
|
|
674
|
+
dg.from_file_id,
|
|
675
|
+
dg.from_symbol_id,
|
|
676
|
+
dg.dependency_type,
|
|
677
|
+
1 AS depth,
|
|
678
|
+
'/' || dg.id || '/' AS path
|
|
679
|
+
FROM dependency_graph dg
|
|
680
|
+
WHERE dg.repository_id = ?
|
|
681
|
+
${targetCondition}
|
|
682
|
+
|
|
683
|
+
UNION ALL
|
|
684
|
+
|
|
685
|
+
SELECT
|
|
686
|
+
dg.id,
|
|
687
|
+
dg.from_file_id,
|
|
688
|
+
dg.from_symbol_id,
|
|
689
|
+
dg.dependency_type,
|
|
690
|
+
d.depth + 1,
|
|
691
|
+
d.path || dg.id || '/'
|
|
692
|
+
FROM dependency_graph dg
|
|
693
|
+
JOIN dependents d ON ${recursiveJoinCondition}
|
|
694
|
+
WHERE dg.repository_id = ?
|
|
695
|
+
AND d.depth < ?
|
|
696
|
+
AND INSTR(d.path, '/' || dg.id || '/') = 0
|
|
697
|
+
)
|
|
698
|
+
SELECT DISTINCT
|
|
699
|
+
d.from_file_id AS file_id,
|
|
700
|
+
f.path AS file_path,
|
|
701
|
+
d.from_symbol_id AS symbol_id,
|
|
702
|
+
s.name AS symbol_name,
|
|
703
|
+
d.dependency_type,
|
|
704
|
+
d.depth
|
|
705
|
+
FROM dependents d
|
|
706
|
+
LEFT JOIN indexed_files f ON d.from_file_id = f.id
|
|
707
|
+
LEFT JOIN indexed_symbols s ON d.from_symbol_id = s.id
|
|
708
|
+
ORDER BY d.depth ASC
|
|
709
|
+
`;
|
|
710
|
+
|
|
711
|
+
const targetParam = symbolId || fileId;
|
|
712
|
+
return db.query<{
|
|
713
|
+
file_id: string | null;
|
|
714
|
+
file_path: string | null;
|
|
715
|
+
symbol_id: string | null;
|
|
716
|
+
symbol_name: string | null;
|
|
717
|
+
dependency_type: string;
|
|
718
|
+
depth: number;
|
|
719
|
+
}>(sql, [repositoryId, targetParam, repositoryId, depth]);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Query dependencies (forward lookup): files/symbols that the source depends on
|
|
724
|
+
*
|
|
725
|
+
* @internal - Use queryDependencies() for the wrapped version
|
|
726
|
+
*/
|
|
727
|
+
export function queryDependenciesRaw(
|
|
728
|
+
db: KotaDatabase,
|
|
729
|
+
repositoryId: string,
|
|
730
|
+
fileId: string | null,
|
|
731
|
+
symbolId: string | null = null,
|
|
732
|
+
depth: number = 5
|
|
733
|
+
): Array<{
|
|
734
|
+
file_id: string | null;
|
|
735
|
+
file_path: string | null;
|
|
736
|
+
symbol_id: string | null;
|
|
737
|
+
symbol_name: string | null;
|
|
738
|
+
dependency_type: string;
|
|
739
|
+
depth: number;
|
|
740
|
+
}> {
|
|
741
|
+
const sourceCondition = symbolId
|
|
742
|
+
? 'AND dg.from_symbol_id = ?'
|
|
743
|
+
: 'AND dg.from_file_id = ?';
|
|
744
|
+
|
|
745
|
+
const recursiveJoinCondition = symbolId
|
|
746
|
+
? 'dg.from_symbol_id = d.to_symbol_id'
|
|
747
|
+
: 'dg.from_file_id = d.to_file_id';
|
|
748
|
+
|
|
749
|
+
const sql = `
|
|
750
|
+
WITH RECURSIVE dependencies AS (
|
|
751
|
+
SELECT
|
|
752
|
+
dg.id,
|
|
753
|
+
dg.to_file_id,
|
|
754
|
+
dg.to_symbol_id,
|
|
755
|
+
dg.dependency_type,
|
|
756
|
+
1 AS depth,
|
|
757
|
+
'/' || dg.id || '/' AS path
|
|
758
|
+
FROM dependency_graph dg
|
|
759
|
+
WHERE dg.repository_id = ?
|
|
760
|
+
${sourceCondition}
|
|
761
|
+
|
|
762
|
+
UNION ALL
|
|
763
|
+
|
|
764
|
+
SELECT
|
|
765
|
+
dg.id,
|
|
766
|
+
dg.to_file_id,
|
|
767
|
+
dg.to_symbol_id,
|
|
768
|
+
dg.dependency_type,
|
|
769
|
+
d.depth + 1,
|
|
770
|
+
d.path || dg.id || '/'
|
|
771
|
+
FROM dependency_graph dg
|
|
772
|
+
JOIN dependencies d ON ${recursiveJoinCondition}
|
|
773
|
+
WHERE dg.repository_id = ?
|
|
774
|
+
AND d.depth < ?
|
|
775
|
+
AND INSTR(d.path, '/' || dg.id || '/') = 0
|
|
776
|
+
)
|
|
777
|
+
SELECT DISTINCT
|
|
778
|
+
d.to_file_id AS file_id,
|
|
779
|
+
f.path AS file_path,
|
|
780
|
+
d.to_symbol_id AS symbol_id,
|
|
781
|
+
s.name AS symbol_name,
|
|
782
|
+
d.dependency_type,
|
|
783
|
+
d.depth
|
|
784
|
+
FROM dependencies d
|
|
785
|
+
LEFT JOIN indexed_files f ON d.to_file_id = f.id
|
|
786
|
+
LEFT JOIN indexed_symbols s ON d.to_symbol_id = s.id
|
|
787
|
+
ORDER BY d.depth ASC
|
|
788
|
+
`;
|
|
789
|
+
|
|
790
|
+
const sourceParam = symbolId || fileId;
|
|
791
|
+
return db.query<{
|
|
792
|
+
file_id: string | null;
|
|
793
|
+
file_path: string | null;
|
|
794
|
+
symbol_id: string | null;
|
|
795
|
+
symbol_name: string | null;
|
|
796
|
+
dependency_type: string;
|
|
797
|
+
depth: number;
|
|
798
|
+
}>(sql, [repositoryId, sourceParam, repositoryId, depth]);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Ensure repository exists in SQLite, create if not.
|
|
803
|
+
*
|
|
804
|
+
* @param fullName - Repository full name (owner/repo format)
|
|
805
|
+
* @param gitUrl - Git URL for the repository (optional)
|
|
806
|
+
* @param defaultBranch - Default branch name (optional, defaults to 'main')
|
|
807
|
+
* @returns Repository UUID
|
|
808
|
+
*/
|
|
809
|
+
export function ensureRepository(
|
|
810
|
+
fullName: string,
|
|
811
|
+
gitUrl?: string,
|
|
812
|
+
defaultBranch?: string,
|
|
813
|
+
): string {
|
|
814
|
+
return ensureRepositoryInternal(getGlobalDatabase(), fullName, gitUrl, defaultBranch);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Update repository last_indexed_at timestamp.
|
|
819
|
+
*
|
|
820
|
+
* @param repositoryId - Repository UUID
|
|
821
|
+
*/
|
|
822
|
+
export function updateRepositoryLastIndexed(
|
|
823
|
+
repositoryId: string,
|
|
824
|
+
): void {
|
|
825
|
+
updateRepositoryLastIndexedInternal(getGlobalDatabase(), repositoryId);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Run indexing workflow for local mode (synchronous, no queue).
|
|
830
|
+
*
|
|
831
|
+
* @param request - Index request with repository details
|
|
832
|
+
* @returns Indexing result with stats
|
|
833
|
+
*/
|
|
834
|
+
export async function runIndexingWorkflow(
|
|
835
|
+
request: IndexRequest,
|
|
836
|
+
): Promise<{
|
|
837
|
+
repositoryId: string;
|
|
838
|
+
filesIndexed: number;
|
|
839
|
+
symbolsExtracted: number;
|
|
840
|
+
referencesExtracted: number;
|
|
841
|
+
dependenciesExtracted: number;
|
|
842
|
+
}> {
|
|
843
|
+
const { existsSync } = await import("node:fs");
|
|
844
|
+
const { resolve } = await import("node:path");
|
|
845
|
+
const { prepareRepository } = await import("@indexer/repos");
|
|
846
|
+
const { discoverSources, parseSourceFile } = await import("@indexer/parsers");
|
|
847
|
+
const { parseFile, isSupportedForAST } = await import("@indexer/ast-parser");
|
|
848
|
+
const { extractSymbols } = await import("@indexer/symbol-extractor");
|
|
849
|
+
const { extractReferences } = await import("@indexer/reference-extractor");
|
|
850
|
+
const { extractDependencies } = await import("@indexer/dependency-extractor");
|
|
851
|
+
const { detectCircularDependencies } = await import("@indexer/circular-detector");
|
|
852
|
+
|
|
853
|
+
const db = getGlobalDatabase();
|
|
854
|
+
|
|
855
|
+
let localPath: string;
|
|
856
|
+
let fullName = request.repository;
|
|
857
|
+
|
|
858
|
+
if (request.localPath) {
|
|
859
|
+
localPath = resolve(request.localPath);
|
|
860
|
+
|
|
861
|
+
const workspaceRoot = resolve(process.cwd());
|
|
862
|
+
if (!localPath.startsWith(workspaceRoot)) {
|
|
863
|
+
throw new Error(`localPath must be within workspace: ${workspaceRoot}`);
|
|
864
|
+
}
|
|
865
|
+
if (!fullName.includes("/")) {
|
|
866
|
+
fullName = `local/${fullName}`;
|
|
867
|
+
}
|
|
868
|
+
} else {
|
|
869
|
+
const repo = await prepareRepository(request);
|
|
870
|
+
localPath = repo.localPath;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (!existsSync(localPath)) {
|
|
874
|
+
throw new Error(`Repository path does not exist: ${localPath}`);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const gitUrl = request.localPath ? localPath : `https://github.com/${fullName}.git`;
|
|
878
|
+
const repositoryId = ensureRepository(fullName, gitUrl, request.ref);
|
|
879
|
+
|
|
880
|
+
logger.info("Starting local indexing workflow", {
|
|
881
|
+
repositoryId,
|
|
882
|
+
fullName,
|
|
883
|
+
localPath,
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
const sources = await discoverSources(localPath);
|
|
887
|
+
const records = (
|
|
888
|
+
await Promise.all(sources.map((source) => parseSourceFile(source, localPath)))
|
|
889
|
+
).filter((entry): entry is NonNullable<typeof entry> => entry !== null);
|
|
890
|
+
|
|
891
|
+
const filesIndexed = saveIndexedFiles(records, repositoryId);
|
|
892
|
+
|
|
893
|
+
const allSymbolsWithFileId: Array<{
|
|
894
|
+
id: string;
|
|
895
|
+
file_id: string;
|
|
896
|
+
name: string;
|
|
897
|
+
kind: SymbolKind;
|
|
898
|
+
lineStart: number;
|
|
899
|
+
lineEnd: number;
|
|
900
|
+
columnStart: number;
|
|
901
|
+
columnEnd: number;
|
|
902
|
+
signature: string | null;
|
|
903
|
+
documentation: string | null;
|
|
904
|
+
isExported: boolean;
|
|
905
|
+
}> = [];
|
|
906
|
+
const allReferencesWithFileId: Array<Reference & { file_id: string }> = [];
|
|
907
|
+
const filesWithId: IndexedFile[] = [];
|
|
908
|
+
|
|
909
|
+
let totalSymbols = 0;
|
|
910
|
+
let totalReferences = 0;
|
|
911
|
+
|
|
912
|
+
for (const file of records) {
|
|
913
|
+
if (!isSupportedForAST(file.path)) continue;
|
|
914
|
+
|
|
915
|
+
const ast = parseFile(file.path, file.content);
|
|
916
|
+
if (!ast) continue;
|
|
917
|
+
|
|
918
|
+
const symbols = extractSymbols(ast, file.path);
|
|
919
|
+
const references = extractReferences(ast, file.path);
|
|
920
|
+
|
|
921
|
+
const fileRecord = db.queryOne<{ id: string }>(
|
|
922
|
+
"SELECT id FROM indexed_files WHERE repository_id = ? AND path = ?",
|
|
923
|
+
[repositoryId, file.path]
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
if (!fileRecord) {
|
|
927
|
+
logger.warn("Could not find file record after indexing", {
|
|
928
|
+
filePath: file.path,
|
|
929
|
+
repositoryId,
|
|
930
|
+
});
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
filesWithId.push({ ...file, id: fileRecord.id, repository_id: repositoryId });
|
|
935
|
+
|
|
936
|
+
const symbolCount = storeSymbols(symbols, fileRecord.id);
|
|
937
|
+
const referenceCount = storeReferences(references, fileRecord.id);
|
|
938
|
+
|
|
939
|
+
totalSymbols += symbolCount;
|
|
940
|
+
totalReferences += referenceCount;
|
|
941
|
+
|
|
942
|
+
const storedSymbols = db.query<{
|
|
943
|
+
id: string;
|
|
944
|
+
file_id: string;
|
|
945
|
+
name: string;
|
|
946
|
+
kind: SymbolKind;
|
|
947
|
+
line_start: number;
|
|
948
|
+
line_end: number;
|
|
949
|
+
signature: string | null;
|
|
950
|
+
documentation: string | null;
|
|
951
|
+
metadata: string;
|
|
952
|
+
}>(
|
|
953
|
+
"SELECT id, file_id, name, kind, line_start, line_end, signature, documentation, metadata FROM indexed_symbols WHERE file_id = ?",
|
|
954
|
+
[fileRecord.id]
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
for (const s of storedSymbols) {
|
|
958
|
+
const metadata = JSON.parse(s.metadata || "{}");
|
|
959
|
+
allSymbolsWithFileId.push({
|
|
960
|
+
id: s.id,
|
|
961
|
+
file_id: s.file_id,
|
|
962
|
+
name: s.name,
|
|
963
|
+
kind: s.kind as SymbolKind,
|
|
964
|
+
lineStart: s.line_start,
|
|
965
|
+
lineEnd: s.line_end,
|
|
966
|
+
columnStart: metadata.column_start || 0,
|
|
967
|
+
columnEnd: metadata.column_end || 0,
|
|
968
|
+
signature: s.signature || null,
|
|
969
|
+
documentation: s.documentation || null,
|
|
970
|
+
isExported: metadata.is_exported || false,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
for (const ref of references) {
|
|
975
|
+
allReferencesWithFileId.push({ ...ref, file_id: fileRecord.id });
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
logger.info("Extracting dependency graph", {
|
|
980
|
+
fileCount: filesWithId.length,
|
|
981
|
+
repositoryId,
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
const dependencies = extractDependencies(
|
|
985
|
+
filesWithId,
|
|
986
|
+
allSymbolsWithFileId,
|
|
987
|
+
allReferencesWithFileId,
|
|
988
|
+
repositoryId,
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
db.run("DELETE FROM dependency_graph WHERE repository_id = ?", [repositoryId]);
|
|
992
|
+
const dependencyCount = storeDependencies(dependencies);
|
|
993
|
+
|
|
994
|
+
const filePathById = new Map(filesWithId.map((f) => [f.id!, f.path]));
|
|
995
|
+
const symbolNameById = new Map(allSymbolsWithFileId.map((s) => [s.id, s.name]));
|
|
996
|
+
|
|
997
|
+
const circularChains = detectCircularDependencies(dependencies, filePathById, symbolNameById);
|
|
998
|
+
|
|
999
|
+
if (circularChains.length > 0) {
|
|
1000
|
+
logger.warn("Circular dependency chains detected", {
|
|
1001
|
+
chainCount: circularChains.length,
|
|
1002
|
+
repositoryId,
|
|
1003
|
+
chains: circularChains.map((c) => ({
|
|
1004
|
+
type: c.type,
|
|
1005
|
+
description: c.description,
|
|
1006
|
+
})),
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
updateRepositoryLastIndexed(repositoryId);
|
|
1011
|
+
|
|
1012
|
+
logger.info("Local indexing workflow completed", {
|
|
1013
|
+
repositoryId,
|
|
1014
|
+
filesIndexed,
|
|
1015
|
+
symbolsExtracted: totalSymbols,
|
|
1016
|
+
referencesExtracted: totalReferences,
|
|
1017
|
+
dependenciesExtracted: dependencyCount,
|
|
1018
|
+
circularDependencies: circularChains.length,
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
return {
|
|
1022
|
+
repositoryId,
|
|
1023
|
+
filesIndexed,
|
|
1024
|
+
symbolsExtracted: totalSymbols,
|
|
1025
|
+
referencesExtracted: totalReferences,
|
|
1026
|
+
dependenciesExtracted: dependencyCount,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ============================================================================
|
|
1031
|
+
// Backward-compatible aliases that accept db parameter
|
|
1032
|
+
// These use the passed database (for tests) rather than the global one
|
|
1033
|
+
// ============================================================================
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* @deprecated Use saveIndexedFiles() directly
|
|
1037
|
+
*/
|
|
1038
|
+
export function saveIndexedFilesLocal(
|
|
1039
|
+
db: KotaDatabase,
|
|
1040
|
+
files: IndexedFile[],
|
|
1041
|
+
repositoryId: string
|
|
1042
|
+
): number {
|
|
1043
|
+
return saveIndexedFilesInternal(db, files, repositoryId);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* @deprecated Use storeSymbols() directly
|
|
1048
|
+
*/
|
|
1049
|
+
export function storeSymbolsLocal(
|
|
1050
|
+
db: KotaDatabase,
|
|
1051
|
+
symbols: ExtractedSymbol[],
|
|
1052
|
+
fileId: string
|
|
1053
|
+
): number {
|
|
1054
|
+
return storeSymbolsInternal(db, symbols, fileId);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* @deprecated Use storeReferences() directly
|
|
1059
|
+
*/
|
|
1060
|
+
export function storeReferencesLocal(
|
|
1061
|
+
db: KotaDatabase,
|
|
1062
|
+
references: Reference[],
|
|
1063
|
+
fileId: string
|
|
1064
|
+
): number {
|
|
1065
|
+
return storeReferencesInternal(db, references, fileId);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* @deprecated Use searchFiles() directly
|
|
1070
|
+
*/
|
|
1071
|
+
export function searchFilesLocal(
|
|
1072
|
+
db: KotaDatabase,
|
|
1073
|
+
term: string,
|
|
1074
|
+
repositoryId: string | undefined,
|
|
1075
|
+
limit: number
|
|
1076
|
+
): IndexedFile[] {
|
|
1077
|
+
return searchFilesInternal(db, term, repositoryId, limit);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* @deprecated Use listRecentFiles() directly
|
|
1082
|
+
*/
|
|
1083
|
+
export function listRecentFilesLocal(
|
|
1084
|
+
db: KotaDatabase,
|
|
1085
|
+
limit: number,
|
|
1086
|
+
repositoryId?: string,
|
|
1087
|
+
): IndexedFile[] {
|
|
1088
|
+
return listRecentFilesInternal(db, limit, repositoryId);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* @deprecated Use resolveFilePath() directly
|
|
1093
|
+
*/
|
|
1094
|
+
export function resolveFilePathLocal(
|
|
1095
|
+
db: KotaDatabase,
|
|
1096
|
+
filePath: string,
|
|
1097
|
+
repositoryId: string
|
|
1098
|
+
): string | null {
|
|
1099
|
+
return resolveFilePathInternal(db, filePath, repositoryId);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* @deprecated Use storeDependencies() directly
|
|
1104
|
+
*/
|
|
1105
|
+
export function storeDependenciesLocal(
|
|
1106
|
+
db: KotaDatabase,
|
|
1107
|
+
dependencies: Array<{
|
|
1108
|
+
repositoryId: string;
|
|
1109
|
+
fromFileId: string | null;
|
|
1110
|
+
toFileId: string | null;
|
|
1111
|
+
fromSymbolId: string | null;
|
|
1112
|
+
toSymbolId: string | null;
|
|
1113
|
+
dependencyType: "file_import" | "symbol_usage";
|
|
1114
|
+
metadata: Record<string, unknown>;
|
|
1115
|
+
}>
|
|
1116
|
+
): number {
|
|
1117
|
+
return storeDependenciesInternal(db, dependencies);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* @deprecated Use queryDependentsRaw() directly
|
|
1122
|
+
*/
|
|
1123
|
+
export const queryDependentsLocal = queryDependentsRaw;
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* @deprecated Use queryDependenciesRaw() directly
|
|
1127
|
+
*/
|
|
1128
|
+
export const queryDependenciesLocal = queryDependenciesRaw;
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* @deprecated Use ensureRepository() directly
|
|
1132
|
+
*/
|
|
1133
|
+
export function ensureRepositoryLocal(
|
|
1134
|
+
db: KotaDatabase,
|
|
1135
|
+
fullName: string,
|
|
1136
|
+
gitUrl?: string,
|
|
1137
|
+
defaultBranch?: string
|
|
1138
|
+
): string {
|
|
1139
|
+
return ensureRepositoryInternal(db, fullName, gitUrl, defaultBranch);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* @deprecated Use updateRepositoryLastIndexed() directly
|
|
1144
|
+
*/
|
|
1145
|
+
export function updateRepositoryLastIndexedLocal(
|
|
1146
|
+
db: KotaDatabase,
|
|
1147
|
+
repositoryId: string
|
|
1148
|
+
): void {
|
|
1149
|
+
return updateRepositoryLastIndexedInternal(db, repositoryId);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Add alias for runIndexingWorkflowLocal
|
|
1153
|
+
export const runIndexingWorkflowLocal = runIndexingWorkflow;
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Create default organization for a new user.
|
|
1158
|
+
*
|
|
1159
|
+
* @deprecated This function is not available in local-only mode.
|
|
1160
|
+
* Organizations are a cloud-only feature.
|
|
1161
|
+
*/
|
|
1162
|
+
export async function createDefaultOrganization(
|
|
1163
|
+
_client: unknown,
|
|
1164
|
+
_userId: string,
|
|
1165
|
+
_userEmail?: string,
|
|
1166
|
+
): Promise<string> {
|
|
1167
|
+
throw new Error('createDefaultOrganization() is not available in local-only mode - organizations are a cloud-only feature');
|
|
1168
|
+
}
|