ast-mcp-server 0.6.0 → 0.7.1

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +47 -15
  3. package/SECURITY.md +31 -0
  4. package/dist/cli.js +0 -0
  5. package/dist/index.js +129 -10
  6. package/dist/server.js +21 -2
  7. package/dist/services/agent-setup.js +36 -117
  8. package/dist/services/agent-targets.js +122 -0
  9. package/dist/services/context-builder.js +52 -24
  10. package/dist/services/diagnostics.js +10 -5
  11. package/dist/services/file-fingerprints.js +104 -0
  12. package/dist/services/file-snapshot.js +8 -1
  13. package/dist/services/impact.js +334 -0
  14. package/dist/services/operations.js +343 -254
  15. package/dist/services/outline.js +94 -21
  16. package/dist/services/pagination.js +10 -4
  17. package/dist/services/project-operation-scheduler.js +475 -0
  18. package/dist/services/project-status.js +134 -52
  19. package/dist/services/project-watcher.js +230 -0
  20. package/dist/services/project.js +646 -66
  21. package/dist/services/public-errors.js +255 -0
  22. package/dist/services/references.js +7 -1
  23. package/dist/services/relationships.js +1198 -0
  24. package/dist/services/request-context.js +42 -0
  25. package/dist/services/runtime-activity.js +65 -0
  26. package/dist/services/runtime-logger.js +50 -0
  27. package/dist/services/runtime-policy.js +47 -0
  28. package/dist/services/setup-wizard.js +4 -7
  29. package/dist/services/shutdown.js +93 -0
  30. package/dist/services/symbol-index-limits.js +5 -0
  31. package/dist/services/symbol-index-order.js +13 -0
  32. package/dist/services/symbol-index-policy.js +91 -0
  33. package/dist/services/symbol-index-sqlite.js +850 -0
  34. package/dist/services/symbol-index.js +447 -0
  35. package/dist/services/symbols.js +403 -79
  36. package/dist/services/test-candidates.js +209 -0
  37. package/dist/services/workspace.js +18 -7
  38. package/dist/tools/apply_operation.js +8 -4
  39. package/dist/tools/explore.js +8 -6
  40. package/dist/tools/find_references.js +12 -13
  41. package/dist/tools/get_diagnostics.js +10 -7
  42. package/dist/tools/get_file.js +23 -11
  43. package/dist/tools/get_impact.js +134 -0
  44. package/dist/tools/get_operation_preview.js +7 -4
  45. package/dist/tools/get_outline.js +8 -6
  46. package/dist/tools/get_project_status.js +77 -8
  47. package/dist/tools/get_symbol_source.js +8 -6
  48. package/dist/tools/list_files.js +11 -6
  49. package/dist/tools/rename_symbol.js +6 -4
  50. package/dist/tools/replace_symbol_body.js +6 -4
  51. package/dist/tools/result.js +27 -17
  52. package/dist/tools/scaffold_class.js +6 -4
  53. package/dist/tools/search_symbols.js +15 -14
  54. package/docs/support.md +80 -0
  55. package/package.json +16 -5
  56. package/skills/structural-code-editing/SKILL.md +37 -10
@@ -0,0 +1,850 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { createSymbolIndexFileEntry, createSymbolIndexRefreshPlan, createSymbolIndexSymbol, MAX_SYMBOL_INDEX_FILE_ENTRIES, MAX_SYMBOL_INDEX_QUERY_CANDIDATES, MAX_SYMBOL_INDEX_ROW_PAYLOAD_BYTES, MAX_SYMBOL_INDEX_SCANNED_SYMBOLS, MAX_SYMBOL_INDEX_TOTAL_PAYLOAD_BYTES, SYMBOL_INDEX_SCHEMA_VERSION, SymbolIndexScanLimitError, symbolIndexMatchRank, symbolIndexQueryOffset, symbolIndexRankWindow, symbolProjectionJsonByteLength, } from "./symbol-index.js";
5
+ const SQLITE_HEADER = Buffer.from("SQLite format 3\u0000", "utf8");
6
+ const DEFAULT_BUSY_TIMEOUT_MS = 1_000;
7
+ const SQLITE_READ_PAGE_SIZE = 32;
8
+ const EXPECTED_TABLE_COLUMNS = {
9
+ metadata: [
10
+ { name: "key", type: "TEXT", notnull: 0, dflt_value: null, pk: 1 },
11
+ { name: "value", type: "TEXT", notnull: 1, dflt_value: null, pk: 0 },
12
+ ],
13
+ symbol_index: [
14
+ { name: "project_id", type: "TEXT", notnull: 1, dflt_value: null, pk: 1 },
15
+ { name: "config_id", type: "TEXT", notnull: 1, dflt_value: "''", pk: 2 },
16
+ { name: "file_path", type: "TEXT", notnull: 1, dflt_value: null, pk: 3 },
17
+ { name: "content_hash", type: "TEXT", notnull: 1, dflt_value: null, pk: 0 },
18
+ { name: "config_digest", type: "TEXT", notnull: 1, dflt_value: null, pk: 0 },
19
+ { name: "symbols_json", type: "TEXT", notnull: 1, dflt_value: null, pk: 0 },
20
+ { name: "symbols_digest", type: "TEXT", notnull: 1, dflt_value: null, pk: 0 },
21
+ { name: "last_indexed_at", type: "TEXT", notnull: 1, dflt_value: null, pk: 0 },
22
+ { name: "schema_version", type: "INTEGER", notnull: 1, dflt_value: null, pk: 0 },
23
+ ],
24
+ };
25
+ const LEGACY_SYMBOL_INDEX_COLUMNS = EXPECTED_TABLE_COLUMNS.symbol_index.filter((column) => column.name !== "symbols_digest");
26
+ const SYMBOL_INDEX_TABLE_SQL = `
27
+ CREATE TABLE IF NOT EXISTS symbol_index (
28
+ project_id TEXT NOT NULL,
29
+ config_id TEXT NOT NULL DEFAULT '',
30
+ file_path TEXT NOT NULL,
31
+ content_hash TEXT NOT NULL,
32
+ config_digest TEXT NOT NULL,
33
+ symbols_json TEXT NOT NULL,
34
+ symbols_digest TEXT NOT NULL,
35
+ last_indexed_at TEXT NOT NULL,
36
+ schema_version INTEGER NOT NULL,
37
+ PRIMARY KEY (project_id, config_id, file_path)
38
+ );
39
+ `;
40
+ export class SymbolIndexStorageError extends Error {
41
+ code;
42
+ cause;
43
+ constructor(code, message, cause) {
44
+ super(message);
45
+ this.name = "SymbolIndexStorageError";
46
+ this.code = code;
47
+ this.cause = cause;
48
+ }
49
+ }
50
+ function storageErrorCode(error, fallback) {
51
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
52
+ if (message.includes("busy") || message.includes("locked") || message.includes("timeout")) {
53
+ return "contention";
54
+ }
55
+ if (message.includes("malformed") ||
56
+ message.includes("not a database") ||
57
+ message.includes("database disk image")) {
58
+ return "corrupt_storage";
59
+ }
60
+ return fallback;
61
+ }
62
+ function wrapStorageError(error, fallback, message) {
63
+ if (error instanceof SymbolIndexStorageError)
64
+ return error;
65
+ return new SymbolIndexStorageError(storageErrorCode(error, fallback), message, error);
66
+ }
67
+ function assertSafeDatabasePath(filePath) {
68
+ if (!path.isAbsolute(filePath)) {
69
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache path must be absolute.");
70
+ }
71
+ const normalized = path.normalize(filePath);
72
+ if (normalized !== filePath || normalized.split(path.sep).includes("..")) {
73
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache path is not canonical.");
74
+ }
75
+ return normalized;
76
+ }
77
+ async function ensureCanonicalDirectory(directoryPath) {
78
+ const parsed = path.parse(directoryPath);
79
+ const segments = path.relative(parsed.root, directoryPath).split(path.sep).filter(Boolean);
80
+ let current = parsed.root;
81
+ for (const segment of segments) {
82
+ current = path.join(current, segment);
83
+ let stats;
84
+ try {
85
+ stats = await lstat(current);
86
+ }
87
+ catch (error) {
88
+ if (error.code !== "ENOENT") {
89
+ throw wrapStorageError(error, "write_failed", "SQLite cache directory could not be inspected.");
90
+ }
91
+ try {
92
+ await mkdir(current);
93
+ }
94
+ catch (mkdirError) {
95
+ if (mkdirError.code !== "EEXIST") {
96
+ throw wrapStorageError(mkdirError, "write_failed", "SQLite cache directory could not be created.");
97
+ }
98
+ }
99
+ stats = await lstat(current);
100
+ }
101
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
102
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache directory contains a non-canonical ancestor.");
103
+ }
104
+ }
105
+ if ((await realpath(directoryPath)) !== directoryPath) {
106
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache directory resolves outside its canonical path.");
107
+ }
108
+ }
109
+ async function assertDatabaseHeader(filePath) {
110
+ let handle;
111
+ try {
112
+ handle = await open(filePath, "r");
113
+ const bytes = Buffer.alloc(SQLITE_HEADER.length);
114
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
115
+ if (bytesRead !== SQLITE_HEADER.length || !bytes.equals(SQLITE_HEADER)) {
116
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache header is invalid.");
117
+ }
118
+ }
119
+ catch (error) {
120
+ if (error instanceof SymbolIndexStorageError)
121
+ throw error;
122
+ if (error?.code === "ENOENT")
123
+ return;
124
+ throw wrapStorageError(error, "read_failed", "SQLite cache header could not be read.");
125
+ }
126
+ finally {
127
+ await handle?.close();
128
+ }
129
+ }
130
+ async function assertCanonicalDatabaseTarget(filePath) {
131
+ try {
132
+ const stats = await lstat(filePath);
133
+ if (stats.isSymbolicLink() || !stats.isFile() || (await realpath(filePath)) !== filePath) {
134
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache file must be a canonical regular file.");
135
+ }
136
+ }
137
+ catch (error) {
138
+ if (error?.code === "ENOENT")
139
+ return;
140
+ if (error instanceof SymbolIndexStorageError)
141
+ throw error;
142
+ throw wrapStorageError(error, "read_failed", "SQLite cache target could not be inspected.");
143
+ }
144
+ }
145
+ async function loadDatabaseSyncConstructor() {
146
+ try {
147
+ const sqlite = await import("node:sqlite");
148
+ return sqlite.DatabaseSync;
149
+ }
150
+ catch (error) {
151
+ throw new SymbolIndexStorageError("capability_unavailable", "The current Node runtime does not expose node:sqlite.", error);
152
+ }
153
+ }
154
+ function projectMatches(left, right) {
155
+ return left.project_id === right.project_id && left.config_id === right.config_id;
156
+ }
157
+ function configKey(configId) {
158
+ return configId ?? "";
159
+ }
160
+ function projectionDigest(symbolsJson) {
161
+ return createHash("sha256").update(symbolsJson).digest("hex");
162
+ }
163
+ function countJsonArrayItemsWithinLimit(value, limit) {
164
+ let depth = 0;
165
+ let count = 0;
166
+ let inString = false;
167
+ let escaped = false;
168
+ let expectingValue = true;
169
+ for (let index = 0; index < value.length; index += 1) {
170
+ const character = value[index];
171
+ if (inString) {
172
+ if (escaped)
173
+ escaped = false;
174
+ else if (character === "\\")
175
+ escaped = true;
176
+ else if (character === '"')
177
+ inString = false;
178
+ continue;
179
+ }
180
+ if (character === '"') {
181
+ if (depth === 1 && expectingValue) {
182
+ count += 1;
183
+ if (count > limit)
184
+ return count;
185
+ expectingValue = false;
186
+ }
187
+ inString = true;
188
+ continue;
189
+ }
190
+ if (depth === 0) {
191
+ if (/\s/u.test(character))
192
+ continue;
193
+ if (character !== "[")
194
+ return 0;
195
+ depth = 1;
196
+ continue;
197
+ }
198
+ if (depth === 1 && expectingValue && !/\s/u.test(character) && character !== "]") {
199
+ count += 1;
200
+ if (count > limit)
201
+ return count;
202
+ expectingValue = false;
203
+ }
204
+ if (character === "[" || character === "{")
205
+ depth += 1;
206
+ else if (character === "]" || character === "}") {
207
+ if (depth === 1)
208
+ return count;
209
+ depth -= 1;
210
+ }
211
+ else if (character === "," && depth === 1) {
212
+ expectingValue = true;
213
+ }
214
+ }
215
+ return count;
216
+ }
217
+ function toEntry(row, remainingSymbols = MAX_SYMBOL_INDEX_SCANNED_SYMBOLS, symbolLimitError = () => new SymbolIndexStorageError("corrupt_storage", "SQLite cache row symbol limit was exceeded.")) {
218
+ if (typeof row.project_id !== "string" ||
219
+ typeof row.config_id !== "string" ||
220
+ typeof row.file_path !== "string" ||
221
+ typeof row.content_hash !== "string" ||
222
+ typeof row.config_digest !== "string" ||
223
+ typeof row.symbols_json !== "string" ||
224
+ typeof row.symbols_digest !== "string" ||
225
+ typeof row.last_indexed_at !== "string" ||
226
+ typeof row.schema_version !== "number") {
227
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache row shape is invalid.");
228
+ }
229
+ if (row.schema_version !== SYMBOL_INDEX_SCHEMA_VERSION) {
230
+ throw new SymbolIndexStorageError("unsupported_schema", "SQLite cache row schema is unsupported.");
231
+ }
232
+ if (Buffer.byteLength(row.symbols_json, "utf8") > MAX_SYMBOL_INDEX_ROW_PAYLOAD_BYTES ||
233
+ projectionDigest(row.symbols_json) !== row.symbols_digest) {
234
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache symbol projection checksum is invalid.");
235
+ }
236
+ if (countJsonArrayItemsWithinLimit(row.symbols_json, remainingSymbols) > remainingSymbols) {
237
+ throw symbolLimitError();
238
+ }
239
+ let symbols;
240
+ try {
241
+ symbols = JSON.parse(row.symbols_json);
242
+ }
243
+ catch (error) {
244
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache symbol payload is invalid.", error);
245
+ }
246
+ try {
247
+ return createSymbolIndexFileEntry({
248
+ project: {
249
+ project_id: row.project_id,
250
+ config_id: row.config_id === "" ? null : row.config_id,
251
+ },
252
+ file_path: row.file_path,
253
+ content_hash: row.content_hash,
254
+ config_digest: row.config_digest,
255
+ symbols: symbols,
256
+ last_indexed_at: row.last_indexed_at,
257
+ });
258
+ }
259
+ catch (error) {
260
+ throw wrapStorageError(error, "corrupt_storage", "SQLite cache row validation failed.");
261
+ }
262
+ }
263
+ function assertEntrySetWithinBudget(entries, failure) {
264
+ if (entries.length > MAX_SYMBOL_INDEX_FILE_ENTRIES) {
265
+ throw new SymbolIndexStorageError(failure, "SQLite cache file-entry limit was exceeded.");
266
+ }
267
+ let payloadBytes = 0;
268
+ let symbols = 0;
269
+ for (const entry of entries) {
270
+ const entryPayloadBytes = symbolProjectionJsonByteLength(entry.symbols);
271
+ if (entryPayloadBytes > MAX_SYMBOL_INDEX_ROW_PAYLOAD_BYTES) {
272
+ throw new SymbolIndexStorageError(failure, "SQLite cache row payload limit was exceeded.");
273
+ }
274
+ payloadBytes += entryPayloadBytes;
275
+ symbols += entry.symbols.length;
276
+ if (payloadBytes > MAX_SYMBOL_INDEX_TOTAL_PAYLOAD_BYTES ||
277
+ symbols > MAX_SYMBOL_INDEX_SCANNED_SYMBOLS) {
278
+ throw new SymbolIndexStorageError(failure, "SQLite cache lifecycle budget was exceeded.");
279
+ }
280
+ }
281
+ }
282
+ export async function openSQLiteSymbolIndexStore(filePath, options = {}) {
283
+ const safePath = assertSafeDatabasePath(filePath);
284
+ if (options.DatabaseSync === null) {
285
+ throw new SymbolIndexStorageError("capability_unavailable", "SQLite capability was disabled by the failure-injection hook.");
286
+ }
287
+ const DatabaseSync = options.DatabaseSync ?? (await loadDatabaseSyncConstructor());
288
+ const databaseDirectory = path.dirname(safePath);
289
+ try {
290
+ await ensureCanonicalDirectory(databaseDirectory);
291
+ }
292
+ catch (error) {
293
+ throw wrapStorageError(error, "write_failed", "SQLite cache directory creation failed.");
294
+ }
295
+ await assertCanonicalDatabaseTarget(safePath);
296
+ await assertDatabaseHeader(safePath);
297
+ try {
298
+ return new SQLiteSymbolIndexStore(safePath, DatabaseSync, options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS);
299
+ }
300
+ catch (error) {
301
+ throw wrapStorageError(error, "read_failed", "SQLite cache could not be opened.");
302
+ }
303
+ }
304
+ export async function quarantineSQLiteSymbolIndexFile(filePath) {
305
+ const safePath = assertSafeDatabasePath(filePath);
306
+ const quarantinePath = `${safePath}.corrupt-${process.pid}-${Date.now()}`;
307
+ try {
308
+ await rename(safePath, quarantinePath);
309
+ }
310
+ catch (error) {
311
+ if (error?.code === "ENOENT")
312
+ return null;
313
+ throw wrapStorageError(error, "write_failed", "SQLite cache quarantine failed.");
314
+ }
315
+ await Promise.all([
316
+ rm(`${safePath}-wal`, { force: true }),
317
+ rm(`${safePath}-shm`, { force: true }),
318
+ ]);
319
+ return quarantinePath;
320
+ }
321
+ export class SQLiteSymbolIndexStore {
322
+ filePath;
323
+ busyTimeoutMs;
324
+ migrationWasPerformed = false;
325
+ db;
326
+ constructor(filePath, DatabaseSync, busyTimeoutMs = DEFAULT_BUSY_TIMEOUT_MS) {
327
+ this.filePath = assertSafeDatabasePath(filePath);
328
+ this.busyTimeoutMs =
329
+ Number.isInteger(busyTimeoutMs) && busyTimeoutMs > 0
330
+ ? busyTimeoutMs
331
+ : DEFAULT_BUSY_TIMEOUT_MS;
332
+ this.db = new DatabaseSync(this.filePath);
333
+ try {
334
+ this.initialize();
335
+ }
336
+ catch (error) {
337
+ this.db.close();
338
+ this.db = null;
339
+ throw error;
340
+ }
341
+ }
342
+ get migrationPerformed() {
343
+ return this.migrationWasPerformed;
344
+ }
345
+ requireDatabase() {
346
+ if (!this.db)
347
+ throw new SymbolIndexStorageError("closed", "SQLite cache is closed.");
348
+ return this.db;
349
+ }
350
+ initialize() {
351
+ const db = this.requireDatabase();
352
+ try {
353
+ db.exec(`
354
+ PRAGMA busy_timeout = ${this.busyTimeoutMs};
355
+ PRAGMA journal_mode = WAL;
356
+ `);
357
+ let hasProjectionDigest = false;
358
+ try {
359
+ db.exec("BEGIN IMMEDIATE");
360
+ db.exec(`
361
+ CREATE TABLE IF NOT EXISTS metadata (
362
+ key TEXT PRIMARY KEY,
363
+ value TEXT NOT NULL
364
+ );
365
+ `);
366
+ db.exec(SYMBOL_INDEX_TABLE_SQL);
367
+ hasProjectionDigest = this.assertSchemaShape();
368
+ db.prepare("INSERT OR IGNORE INTO metadata (key, value) VALUES ('schema_version', ?)").run(String(SYMBOL_INDEX_SCHEMA_VERSION));
369
+ db.exec("COMMIT");
370
+ }
371
+ catch (error) {
372
+ try {
373
+ db.exec("ROLLBACK");
374
+ }
375
+ catch {
376
+ // Preserve the schema-creation failure.
377
+ }
378
+ throw wrapStorageError(error, "write_failed", "SQLite cache schema creation failed.");
379
+ }
380
+ this.migrationWasPerformed = this.migrateRowsIfNeeded(hasProjectionDigest);
381
+ this.readRows(null, SYMBOL_INDEX_SCHEMA_VERSION);
382
+ const integrity = db.prepare("PRAGMA integrity_check").get();
383
+ if (integrity?.integrity_check !== "ok") {
384
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache integrity check failed.");
385
+ }
386
+ }
387
+ catch (error) {
388
+ throw wrapStorageError(error, "read_failed", "SQLite cache initialization failed.");
389
+ }
390
+ }
391
+ assertSchemaShape() {
392
+ const db = this.requireDatabase();
393
+ const metadataColumns = db.prepare("PRAGMA table_info(metadata)").all();
394
+ const symbolColumns = db.prepare("PRAGMA table_info(symbol_index)").all();
395
+ const sameColumns = (actual, expected) => actual.length === expected.length &&
396
+ actual.every((column, index) => {
397
+ const specification = expected[index];
398
+ return (column.cid === index &&
399
+ column.name === specification.name &&
400
+ column.type === specification.type &&
401
+ column.notnull === specification.notnull &&
402
+ column.dflt_value === specification.dflt_value &&
403
+ column.pk === specification.pk);
404
+ });
405
+ if (!sameColumns(metadataColumns, EXPECTED_TABLE_COLUMNS.metadata)) {
406
+ throw new SymbolIndexStorageError("unsupported_schema", "SQLite cache table schema is unsupported.");
407
+ }
408
+ if (sameColumns(symbolColumns, EXPECTED_TABLE_COLUMNS.symbol_index))
409
+ return true;
410
+ if (sameColumns(symbolColumns, LEGACY_SYMBOL_INDEX_COLUMNS))
411
+ return false;
412
+ throw new SymbolIndexStorageError("unsupported_schema", "SQLite cache table schema is unsupported.");
413
+ }
414
+ *readSqlitePages(sql, parameters = []) {
415
+ const statement = this.requireDatabase().prepare(`${sql}\nLIMIT ? OFFSET ?`);
416
+ let offset = 0;
417
+ while (offset <= MAX_SYMBOL_INDEX_FILE_ENTRIES) {
418
+ const pageLimit = Math.min(SQLITE_READ_PAGE_SIZE, MAX_SYMBOL_INDEX_FILE_ENTRIES + 1 - offset);
419
+ const page = statement.all(...parameters, pageLimit, offset);
420
+ for (const row of page)
421
+ yield row;
422
+ offset += page.length;
423
+ if (page.length < pageLimit)
424
+ return;
425
+ }
426
+ }
427
+ migrateRowsIfNeeded(hasProjectionDigest) {
428
+ const db = this.requireDatabase();
429
+ try {
430
+ db.exec("BEGIN IMMEDIATE");
431
+ const metadata = db
432
+ .prepare("SELECT value FROM metadata WHERE key = 'schema_version'")
433
+ .get();
434
+ const metadataVersion = Number(metadata?.value);
435
+ if (!Number.isInteger(metadataVersion) ||
436
+ metadataVersion < 0 ||
437
+ metadataVersion > SYMBOL_INDEX_SCHEMA_VERSION ||
438
+ (metadataVersion === SYMBOL_INDEX_SCHEMA_VERSION && !hasProjectionDigest)) {
439
+ throw new SymbolIndexStorageError("unsupported_schema", "SQLite cache schema is unsupported.");
440
+ }
441
+ const rows = this.readSqlitePages(`SELECT project_id, config_id, file_path, content_hash, config_digest, symbols_json,
442
+ ${hasProjectionDigest ? "symbols_digest" : "NULL AS symbols_digest"},
443
+ last_indexed_at, schema_version
444
+ FROM symbol_index
445
+ ORDER BY project_id, config_id, file_path`);
446
+ const normalizedRows = [];
447
+ let payloadBytes = 0;
448
+ let decodedSymbols = 0;
449
+ let hasLegacyRows = false;
450
+ for (const row of rows) {
451
+ if (normalizedRows.length >= MAX_SYMBOL_INDEX_FILE_ENTRIES) {
452
+ throw new SymbolIndexStorageError("migration_failed", "SQLite cache migration file-entry limit was exceeded.");
453
+ }
454
+ if (typeof row.schema_version !== "number" ||
455
+ ![0, 1, SYMBOL_INDEX_SCHEMA_VERSION].includes(row.schema_version)) {
456
+ throw new SymbolIndexStorageError("unsupported_schema", "SQLite cache row schema is unsupported.");
457
+ }
458
+ if (typeof row.symbols_json !== "string") {
459
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache row shape is invalid.");
460
+ }
461
+ payloadBytes += Buffer.byteLength(row.symbols_json, "utf8");
462
+ if (payloadBytes > MAX_SYMBOL_INDEX_TOTAL_PAYLOAD_BYTES) {
463
+ throw new SymbolIndexStorageError("migration_failed", "SQLite cache migration payload budget was exceeded.");
464
+ }
465
+ const digest = projectionDigest(row.symbols_json);
466
+ const normalized = {
467
+ ...row,
468
+ symbols_digest: row.schema_version === SYMBOL_INDEX_SCHEMA_VERSION ? row.symbols_digest : digest,
469
+ schema_version: SYMBOL_INDEX_SCHEMA_VERSION,
470
+ };
471
+ const entry = toEntry(normalized, MAX_SYMBOL_INDEX_SCANNED_SYMBOLS - decodedSymbols, () => new SymbolIndexStorageError("migration_failed", "SQLite cache migration decoded-symbol budget was exceeded."));
472
+ decodedSymbols += entry.symbols.length;
473
+ if (decodedSymbols > MAX_SYMBOL_INDEX_SCANNED_SYMBOLS) {
474
+ throw new SymbolIndexStorageError("migration_failed", "SQLite cache migration decoded-symbol budget was exceeded.");
475
+ }
476
+ hasLegacyRows ||= row.schema_version !== SYMBOL_INDEX_SCHEMA_VERSION;
477
+ normalizedRows.push({ entry, digest });
478
+ }
479
+ assertEntrySetWithinBudget(normalizedRows.map(({ entry }) => entry), "migration_failed");
480
+ const needsMigration = metadataVersion !== SYMBOL_INDEX_SCHEMA_VERSION || !hasProjectionDigest || hasLegacyRows;
481
+ if (!needsMigration) {
482
+ db.exec("COMMIT");
483
+ return false;
484
+ }
485
+ if (!hasProjectionDigest) {
486
+ db.exec("ALTER TABLE symbol_index RENAME TO symbol_index_legacy");
487
+ db.exec(SYMBOL_INDEX_TABLE_SQL);
488
+ const insert = db.prepare(`INSERT INTO symbol_index
489
+ (project_id, config_id, file_path, content_hash, config_digest, symbols_json, symbols_digest, last_indexed_at, schema_version)
490
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
491
+ for (const { entry } of normalizedRows) {
492
+ const symbolsJson = JSON.stringify(entry.symbols);
493
+ insert.run(entry.project.project_id, configKey(entry.project.config_id), entry.file_path, entry.content_hash, entry.config_digest, symbolsJson, projectionDigest(symbolsJson), entry.last_indexed_at, SYMBOL_INDEX_SCHEMA_VERSION);
494
+ }
495
+ db.exec("DROP TABLE symbol_index_legacy");
496
+ }
497
+ else {
498
+ const update = db.prepare(`UPDATE symbol_index
499
+ SET symbols_digest = ?, schema_version = ?
500
+ WHERE project_id = ? AND config_id = ? AND file_path = ?`);
501
+ for (const { entry, digest } of normalizedRows) {
502
+ update.run(digest, SYMBOL_INDEX_SCHEMA_VERSION, entry.project.project_id, configKey(entry.project.config_id), entry.file_path);
503
+ }
504
+ }
505
+ db.prepare("UPDATE metadata SET value = ? WHERE key = 'schema_version'").run(String(SYMBOL_INDEX_SCHEMA_VERSION));
506
+ db.exec("COMMIT");
507
+ this.assertSchemaShape();
508
+ return true;
509
+ }
510
+ catch (error) {
511
+ try {
512
+ db.exec("ROLLBACK");
513
+ }
514
+ catch {
515
+ // The original failure is the useful classification.
516
+ }
517
+ throw wrapStorageError(error, "migration_failed", "SQLite cache migration failed.");
518
+ }
519
+ }
520
+ readRows(project, schemaVersion) {
521
+ const rows = project
522
+ ? this.readSqlitePages(`SELECT project_id, config_id, file_path, content_hash, config_digest, symbols_json,
523
+ symbols_digest, last_indexed_at, schema_version
524
+ FROM symbol_index
525
+ WHERE project_id = ? AND config_id = ? AND schema_version = ?
526
+ ORDER BY file_path`, [project.project_id, configKey(project.config_id), schemaVersion])
527
+ : this.readSqlitePages(`SELECT project_id, config_id, file_path, content_hash, config_digest, symbols_json,
528
+ symbols_digest, last_indexed_at, schema_version
529
+ FROM symbol_index
530
+ WHERE schema_version = ?
531
+ ORDER BY project_id, config_id, file_path`, [schemaVersion]);
532
+ const entries = [];
533
+ let totalPayloadBytes = 0;
534
+ let decodedSymbols = 0;
535
+ for (const row of rows) {
536
+ if (entries.length >= MAX_SYMBOL_INDEX_FILE_ENTRIES) {
537
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache file-entry limit was exceeded.");
538
+ }
539
+ if (typeof row.symbols_json !== "string") {
540
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache row shape is invalid.");
541
+ }
542
+ totalPayloadBytes += Buffer.byteLength(row.symbols_json, "utf8");
543
+ if (totalPayloadBytes > MAX_SYMBOL_INDEX_TOTAL_PAYLOAD_BYTES) {
544
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache payload budget was exceeded.");
545
+ }
546
+ const entry = toEntry(row, MAX_SYMBOL_INDEX_SCANNED_SYMBOLS - decodedSymbols, () => new SymbolIndexStorageError("corrupt_storage", "SQLite cache decoded-symbol budget was exceeded."));
547
+ decodedSymbols += entry.symbols.length;
548
+ if (decodedSymbols > MAX_SYMBOL_INDEX_SCANNED_SYMBOLS) {
549
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache decoded-symbol budget was exceeded.");
550
+ }
551
+ entries.push(entry);
552
+ }
553
+ return entries;
554
+ }
555
+ writeEntry(entry) {
556
+ const payloadBytes = symbolProjectionJsonByteLength(entry.symbols);
557
+ if (payloadBytes > MAX_SYMBOL_INDEX_ROW_PAYLOAD_BYTES) {
558
+ throw new SymbolIndexStorageError("write_failed", "SQLite cache row payload limit was exceeded.");
559
+ }
560
+ const symbolsJson = JSON.stringify(entry.symbols);
561
+ if (Buffer.byteLength(symbolsJson, "utf8") !== payloadBytes) {
562
+ throw new SymbolIndexStorageError("write_failed", "SQLite cache row payload accounting was inconsistent.");
563
+ }
564
+ this.requireDatabase()
565
+ .prepare(`INSERT INTO symbol_index
566
+ (project_id, config_id, file_path, content_hash, config_digest, symbols_json, symbols_digest, last_indexed_at, schema_version)
567
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
568
+ ON CONFLICT(project_id, config_id, file_path) DO UPDATE SET
569
+ content_hash = excluded.content_hash,
570
+ config_digest = excluded.config_digest,
571
+ symbols_json = excluded.symbols_json,
572
+ symbols_digest = excluded.symbols_digest,
573
+ last_indexed_at = excluded.last_indexed_at,
574
+ schema_version = excluded.schema_version`)
575
+ .run(entry.project.project_id, configKey(entry.project.config_id), entry.file_path, entry.content_hash, entry.config_digest, symbolsJson, projectionDigest(symbolsJson), entry.last_indexed_at, entry.index_schema_version);
576
+ }
577
+ transaction(operation, failure) {
578
+ const db = this.requireDatabase();
579
+ try {
580
+ db.exec("BEGIN IMMEDIATE");
581
+ operation();
582
+ db.exec("COMMIT");
583
+ }
584
+ catch (error) {
585
+ try {
586
+ db.exec("ROLLBACK");
587
+ }
588
+ catch {
589
+ // Preserve the original database failure.
590
+ }
591
+ throw wrapStorageError(error, failure, "SQLite cache transaction failed.");
592
+ }
593
+ }
594
+ async load(project, schemaVersion) {
595
+ if (schemaVersion !== SYMBOL_INDEX_SCHEMA_VERSION)
596
+ return [];
597
+ try {
598
+ return this.readRows(project, schemaVersion).filter((entry) => projectMatches(entry.project, project));
599
+ }
600
+ catch (error) {
601
+ throw wrapStorageError(error, "read_failed", "SQLite cache load failed.");
602
+ }
603
+ }
604
+ async upsert(entry) {
605
+ if (entry.index_schema_version !== SYMBOL_INDEX_SCHEMA_VERSION) {
606
+ throw new Error("Unsupported symbol index schema version.");
607
+ }
608
+ let normalized;
609
+ try {
610
+ normalized = createSymbolIndexFileEntry(entry);
611
+ }
612
+ catch (error) {
613
+ throw wrapStorageError(error, "write_failed", "SQLite cache entry validation failed.");
614
+ }
615
+ const finalEntries = new Map((await this.load(normalized.project, SYMBOL_INDEX_SCHEMA_VERSION)).map((current) => [
616
+ current.file_path,
617
+ current,
618
+ ]));
619
+ finalEntries.set(normalized.file_path, normalized);
620
+ assertEntrySetWithinBudget([...finalEntries.values()], "write_failed");
621
+ this.transaction(() => this.writeEntry(normalized), "write_failed");
622
+ }
623
+ async remove(project, filePath) {
624
+ this.transaction(() => this.requireDatabase()
625
+ .prepare("DELETE FROM symbol_index WHERE project_id = ? AND config_id = ? AND file_path = ?")
626
+ .run(project.project_id, configKey(project.config_id), filePath), "write_failed");
627
+ }
628
+ async countSymbols(query) {
629
+ try {
630
+ const normalizedQuery = query.query.toLowerCase();
631
+ const normalizedFileFilter = query.filters?.file_path?.toLowerCase();
632
+ const sqliteFileFilter = normalizedFileFilter && /^[\x20-\x7e]*$/.test(normalizedFileFilter)
633
+ ? normalizedFileFilter
634
+ : undefined;
635
+ const kindSet = query.filters?.kinds ? new Set(query.filters.kinds) : undefined;
636
+ const queryArguments = [
637
+ query.project.project_id,
638
+ configKey(query.project.config_id),
639
+ SYMBOL_INDEX_SCHEMA_VERSION,
640
+ ];
641
+ if (sqliteFileFilter)
642
+ queryArguments.push(sqliteFileFilter);
643
+ const rows = this.readSqlitePages(`SELECT project_id, config_id, file_path, content_hash, config_digest, symbols_json,
644
+ symbols_digest, last_indexed_at, schema_version
645
+ FROM symbol_index
646
+ WHERE project_id = ? AND config_id = ? AND schema_version = ?
647
+ ${sqliteFileFilter ? "AND (instr(lower(file_path), ?) > 0 OR file_path GLOB '*[^ -~]*')" : ""}
648
+ ORDER BY file_path`, queryArguments);
649
+ let scannedRows = 0;
650
+ let scannedPayloadBytes = 0;
651
+ let scannedSymbols = 0;
652
+ let count = 0;
653
+ for (const row of rows) {
654
+ scannedRows += 1;
655
+ if (scannedRows > MAX_SYMBOL_INDEX_FILE_ENTRIES) {
656
+ throw new SymbolIndexScanLimitError();
657
+ }
658
+ if (typeof row.symbols_json !== "string") {
659
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache row shape is invalid.");
660
+ }
661
+ scannedPayloadBytes += Buffer.byteLength(row.symbols_json, "utf8");
662
+ if (scannedPayloadBytes > MAX_SYMBOL_INDEX_TOTAL_PAYLOAD_BYTES) {
663
+ throw new SymbolIndexScanLimitError();
664
+ }
665
+ const entry = toEntry(row, MAX_SYMBOL_INDEX_SCANNED_SYMBOLS - scannedSymbols, () => new SymbolIndexScanLimitError());
666
+ if (normalizedFileFilter && !entry.file_path.toLowerCase().includes(normalizedFileFilter)) {
667
+ continue;
668
+ }
669
+ for (const symbol of entry.symbols) {
670
+ scannedSymbols += 1;
671
+ if (scannedSymbols > MAX_SYMBOL_INDEX_SCANNED_SYMBOLS) {
672
+ throw new SymbolIndexScanLimitError();
673
+ }
674
+ if (kindSet && !kindSet.has(symbol.kind))
675
+ continue;
676
+ if (symbol.name.toLowerCase().includes(normalizedQuery) ||
677
+ symbol.symbol_path.toLowerCase().includes(normalizedQuery) ||
678
+ symbol.selector.toLowerCase().includes(normalizedQuery)) {
679
+ count += 1;
680
+ }
681
+ }
682
+ }
683
+ return count;
684
+ }
685
+ catch (error) {
686
+ if (error instanceof SymbolIndexScanLimitError)
687
+ throw error;
688
+ throw wrapStorageError(error, "read_failed", "SQLite cache count failed.");
689
+ }
690
+ }
691
+ async querySymbols(query) {
692
+ if (!Number.isInteger(query.limit) ||
693
+ query.limit < 1 ||
694
+ query.limit > MAX_SYMBOL_INDEX_QUERY_CANDIDATES) {
695
+ throw new Error(`Symbol index query limit must be between 1 and ${MAX_SYMBOL_INDEX_QUERY_CANDIDATES}.`);
696
+ }
697
+ const offset = symbolIndexQueryOffset(query);
698
+ try {
699
+ const normalizedQuery = query.query.toLowerCase();
700
+ const normalizedFileFilter = query.filters?.file_path?.toLowerCase();
701
+ const sqliteFileFilter = normalizedFileFilter && /^[\x20-\x7e]*$/.test(normalizedFileFilter)
702
+ ? normalizedFileFilter
703
+ : undefined;
704
+ const kindSet = query.filters?.kinds ? new Set(query.filters.kinds) : undefined;
705
+ const queryArguments = [
706
+ query.project.project_id,
707
+ configKey(query.project.config_id),
708
+ SYMBOL_INDEX_SCHEMA_VERSION,
709
+ ];
710
+ if (sqliteFileFilter)
711
+ queryArguments.push(sqliteFileFilter);
712
+ const scanMatches = (visit) => {
713
+ const rows = this.readSqlitePages(`SELECT project_id, config_id, file_path, content_hash, config_digest, symbols_json,
714
+ symbols_digest, last_indexed_at, schema_version
715
+ FROM symbol_index
716
+ WHERE project_id = ? AND config_id = ? AND schema_version = ?
717
+ ${sqliteFileFilter ? "AND (instr(lower(file_path), ?) > 0 OR file_path GLOB '*[^ -~]*')" : ""}
718
+ ORDER BY file_path`, queryArguments);
719
+ let scannedRows = 0;
720
+ let scannedPayloadBytes = 0;
721
+ let scannedSymbols = 0;
722
+ for (const row of rows) {
723
+ scannedRows += 1;
724
+ if (scannedRows > MAX_SYMBOL_INDEX_FILE_ENTRIES) {
725
+ throw new SymbolIndexScanLimitError();
726
+ }
727
+ if (typeof row.symbols_json !== "string") {
728
+ throw new SymbolIndexStorageError("corrupt_storage", "SQLite cache row shape is invalid.");
729
+ }
730
+ scannedPayloadBytes += Buffer.byteLength(row.symbols_json, "utf8");
731
+ if (scannedPayloadBytes > MAX_SYMBOL_INDEX_TOTAL_PAYLOAD_BYTES) {
732
+ throw new SymbolIndexScanLimitError();
733
+ }
734
+ const entry = toEntry(row, MAX_SYMBOL_INDEX_SCANNED_SYMBOLS - scannedSymbols, () => new SymbolIndexScanLimitError());
735
+ if (normalizedFileFilter &&
736
+ !entry.file_path.toLowerCase().includes(normalizedFileFilter)) {
737
+ continue;
738
+ }
739
+ for (const [symbolPosition, symbol] of entry.symbols.entries()) {
740
+ scannedSymbols += 1;
741
+ if (scannedSymbols > MAX_SYMBOL_INDEX_SCANNED_SYMBOLS) {
742
+ throw new SymbolIndexScanLimitError();
743
+ }
744
+ if (kindSet && !kindSet.has(symbol.kind))
745
+ continue;
746
+ if (!symbol.name.toLowerCase().includes(normalizedQuery) &&
747
+ !symbol.symbol_path.toLowerCase().includes(normalizedQuery) &&
748
+ !symbol.selector.toLowerCase().includes(normalizedQuery)) {
749
+ continue;
750
+ }
751
+ visit({
752
+ ...createSymbolIndexSymbol(symbol),
753
+ project: {
754
+ project_id: entry.project.project_id,
755
+ config_id: entry.project.config_id,
756
+ },
757
+ file_path: entry.file_path,
758
+ content_hash: entry.content_hash,
759
+ config_digest: entry.config_digest,
760
+ index_schema_version: entry.index_schema_version,
761
+ symbol_position: symbolPosition,
762
+ });
763
+ }
764
+ }
765
+ };
766
+ const rankCounts = [0, 0, 0, 0, 0];
767
+ scanMatches((match) => {
768
+ rankCounts[symbolIndexMatchRank(query.query, match)] += 1;
769
+ });
770
+ const window = symbolIndexRankWindow(rankCounts, offset, query.limit);
771
+ const seenByRank = [0, 0, 0, 0, 0];
772
+ const selectedByRank = [[], [], [], [], []];
773
+ scanMatches((match) => {
774
+ const rank = symbolIndexMatchRank(query.query, match);
775
+ const position = seenByRank[rank];
776
+ seenByRank[rank] += 1;
777
+ if (position < window.skips[rank] || selectedByRank[rank].length >= window.takes[rank]) {
778
+ return;
779
+ }
780
+ selectedByRank[rank].push(match);
781
+ });
782
+ return selectedByRank.flat();
783
+ }
784
+ catch (error) {
785
+ if (error instanceof SymbolIndexScanLimitError)
786
+ throw error;
787
+ throw wrapStorageError(error, "read_failed", "SQLite cache query failed.");
788
+ }
789
+ }
790
+ async queryAllSymbols(query) {
791
+ return this.querySymbols({ ...query, limit: MAX_SYMBOL_INDEX_QUERY_CANDIDATES });
792
+ }
793
+ async clear(project) {
794
+ this.transaction(() => this.requireDatabase()
795
+ .prepare("DELETE FROM symbol_index WHERE project_id = ? AND config_id = ?")
796
+ .run(project.project_id, configKey(project.config_id)), "write_failed");
797
+ }
798
+ async flush() {
799
+ try {
800
+ const checkpoint = this.requireDatabase().prepare("PRAGMA wal_checkpoint(FULL)").get();
801
+ if (checkpoint?.busy !== 0) {
802
+ throw new SymbolIndexStorageError("contention", "SQLite cache flush was blocked.");
803
+ }
804
+ }
805
+ catch (error) {
806
+ throw wrapStorageError(error, "write_failed", "SQLite cache flush failed.");
807
+ }
808
+ }
809
+ async refresh(input) {
810
+ const existing = await this.load(input.project, SYMBOL_INDEX_SCHEMA_VERSION);
811
+ const plan = createSymbolIndexRefreshPlan(input, existing);
812
+ const finalEntries = new Map(existing.map((entry) => [entry.file_path, entry]));
813
+ for (const filePath of plan.removed_files)
814
+ finalEntries.delete(filePath);
815
+ for (const entry of plan.entries_to_rebuild)
816
+ finalEntries.set(entry.file_path, entry);
817
+ assertEntrySetWithinBudget([...finalEntries.values()], "write_failed");
818
+ this.transaction(() => {
819
+ for (const entry of plan.entries_to_rebuild)
820
+ this.writeEntry(entry);
821
+ for (const filePath of plan.removed_files) {
822
+ this.requireDatabase()
823
+ .prepare("DELETE FROM symbol_index WHERE project_id = ? AND config_id = ? AND file_path = ?")
824
+ .run(input.project.project_id, configKey(input.project.config_id), filePath);
825
+ }
826
+ }, "write_failed");
827
+ return {
828
+ rebuilt_files: plan.rebuilt_files,
829
+ reused_files: plan.reused_files,
830
+ removed_files: plan.removed_files,
831
+ };
832
+ }
833
+ async quarantine() {
834
+ const db = this.db;
835
+ this.close();
836
+ if (!db)
837
+ return;
838
+ await quarantineSQLiteSymbolIndexFile(this.filePath);
839
+ }
840
+ close() {
841
+ if (!this.db)
842
+ return;
843
+ try {
844
+ this.db.close();
845
+ }
846
+ finally {
847
+ this.db = null;
848
+ }
849
+ }
850
+ }