@adhisang/minecraft-modding-mcp 5.0.0 → 6.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/access-widener-parser.d.ts +1 -0
  3. package/dist/access-widener-parser.js +6 -3
  4. package/dist/cache-registry.js +35 -31
  5. package/dist/decompiler/vineflower.js +8 -2
  6. package/dist/entry-tools/analyze-mod-service.js +2 -1
  7. package/dist/entry-tools/analyze-symbol-service.js +18 -9
  8. package/dist/entry-tools/compare-minecraft-service.js +1 -1
  9. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -1
  10. package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -1
  11. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +2 -1
  12. package/dist/entry-tools/inspect-minecraft/internal.js +2 -0
  13. package/dist/entry-tools/response-contract.d.ts +2 -0
  14. package/dist/entry-tools/response-contract.js +2 -2
  15. package/dist/entry-tools/validate-project-service.d.ts +4 -4
  16. package/dist/entry-tools/verify-mixin-target-service.js +7 -7
  17. package/dist/index.js +20 -16
  18. package/dist/java-process.js +1 -1
  19. package/dist/json-rpc-framing.d.ts +4 -0
  20. package/dist/json-rpc-framing.js +23 -1
  21. package/dist/mapping/internal-types.d.ts +17 -0
  22. package/dist/mapping/loaders/tiny-maven.js +46 -20
  23. package/dist/mapping/lookup.d.ts +12 -1
  24. package/dist/mapping/lookup.js +53 -1
  25. package/dist/mapping-service.js +36 -21
  26. package/dist/mixin/annotation-validators.js +21 -3
  27. package/dist/mixin/parsed-validator.js +1 -0
  28. package/dist/mixin-parser.js +71 -3
  29. package/dist/mod-decompile-service.d.ts +2 -0
  30. package/dist/mod-decompile-service.js +19 -2
  31. package/dist/mod-remap-service.js +6 -5
  32. package/dist/mojang-tiny-mapping-service.js +5 -2
  33. package/dist/nbt/java-nbt-codec.js +40 -16
  34. package/dist/nbt/json-patch.js +51 -12
  35. package/dist/nbt/typed-json.d.ts +1 -0
  36. package/dist/nbt/typed-json.js +12 -2
  37. package/dist/nbt/types.d.ts +2 -2
  38. package/dist/path-converter.js +10 -3
  39. package/dist/registry-service.d.ts +2 -0
  40. package/dist/registry-service.js +16 -1
  41. package/dist/repo-downloader.js +4 -3
  42. package/dist/source/artifact-resolver.js +1 -1
  43. package/dist/source/class-source/snippet-builder.js +6 -0
  44. package/dist/source/class-source-helpers.d.ts +4 -4
  45. package/dist/source/class-source-helpers.js +12 -11
  46. package/dist/source/class-source.d.ts +19 -0
  47. package/dist/source/class-source.js +48 -21
  48. package/dist/source/file-access.js +3 -2
  49. package/dist/source/indexer.js +24 -7
  50. package/dist/source/lifecycle/mapping-helpers.js +28 -3
  51. package/dist/source/lifecycle/runtime-check.js +20 -6
  52. package/dist/source/search.js +23 -8
  53. package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
  54. package/dist/source/workspace-target.js +2 -1
  55. package/dist/source-resolver.js +2 -2
  56. package/dist/source-service.js +9 -1
  57. package/dist/stdio-supervisor.d.ts +2 -2
  58. package/dist/stdio-supervisor.js +29 -9
  59. package/dist/storage/db.js +24 -1
  60. package/dist/storage/files-repo.d.ts +3 -0
  61. package/dist/storage/files-repo.js +43 -37
  62. package/dist/storage/symbols-repo.d.ts +9 -0
  63. package/dist/storage/symbols-repo.js +47 -19
  64. package/dist/symbols/symbol-extractor.js +1 -1
  65. package/dist/tool-guidance.d.ts +2 -1
  66. package/dist/tool-guidance.js +26 -1
  67. package/dist/tool-schemas.d.ts +4 -4
  68. package/dist/tool-schemas.js +91 -91
  69. package/dist/warning-details.d.ts +12 -0
  70. package/dist/warning-details.js +17 -0
  71. package/dist/workspace-mapping-service.js +9 -0
  72. package/package.json +1 -1
@@ -141,7 +141,15 @@ export class SourceService {
141
141
  }
142
142
  async checkSymbolExists(input) {
143
143
  const result = await this.mappingService.checkSymbolExists(input);
144
- if (result.status !== "mapping_unavailable" ||
144
+ // On unobfuscated versions the mapping graph is empty, so the mapping service can
145
+ // only ever report symbols as missing. We then validate against runtime bytecode.
146
+ // Classes/fields surface as `mapping_unavailable` when the graph yields nothing, but
147
+ // a method query with a non-empty graph returns `not_found` (empty member lookup) —
148
+ // which previously skipped the runtime fallback and produced false negatives for
149
+ // methods that genuinely exist (B1). Treat `not_found` methods as fallback-eligible.
150
+ const fallbackEligible = result.status === "mapping_unavailable" ||
151
+ (result.status === "not_found" && input.kind === "method");
152
+ if (!fallbackEligible ||
145
153
  !isUnobfuscatedVersion(input.version) ||
146
154
  (input.sourceMapping !== "mojang" && input.sourceMapping !== "obfuscated")) {
147
155
  return result;
@@ -90,8 +90,8 @@ export declare class StdioSupervisor {
90
90
  private readonly handleWorkerData;
91
91
  private readonly handleWorkerStdinError;
92
92
  private readonly handleWorkerStderr;
93
- private readonly handleWorkerProcessError;
94
- private readonly handleWorkerExit;
93
+ private handleWorkerProcessError;
94
+ private handleWorkerExit;
95
95
  private handleWorkerMessage;
96
96
  private applyStageUpdate;
97
97
  private handleWorkerReady;
@@ -408,6 +408,13 @@ export class StdioSupervisor {
408
408
  if (this.shuttingDown) {
409
409
  return;
410
410
  }
411
+ const stale = this.child;
412
+ if (stale) {
413
+ this.detachChild();
414
+ if (stale.exitCode === null) {
415
+ stale.kill("SIGTERM");
416
+ }
417
+ }
411
418
  const child = spawn(process.execPath, [...process.execArgv, this.entryFile], {
412
419
  env: {
413
420
  ...process.env,
@@ -423,8 +430,8 @@ export class StdioSupervisor {
423
430
  child.stdout.on("data", this.handleWorkerData);
424
431
  child.stderr.on("data", this.handleWorkerStderr);
425
432
  child.stdin.on("error", this.handleWorkerStdinError);
426
- child.once("error", this.handleWorkerProcessError);
427
- child.once("exit", this.handleWorkerExit);
433
+ child.once("error", (error) => this.handleWorkerProcessError(child, error));
434
+ child.once("exit", (code, signal) => this.handleWorkerExit(child, code, signal));
428
435
  log("info", "supervisor.worker_spawn", { pid: child.pid });
429
436
  }
430
437
  handleWorkerData = (chunk) => {
@@ -455,10 +462,23 @@ export class StdioSupervisor {
455
462
  process.stderr.write(`${line}\n`);
456
463
  }
457
464
  };
458
- handleWorkerProcessError = (error) => {
465
+ handleWorkerProcessError(child, error) {
459
466
  log("error", "supervisor.worker_process_error", { message: error.message });
460
- };
461
- handleWorkerExit = (code, signal) => {
467
+ if (child !== this.child) {
468
+ return;
469
+ }
470
+ if (child.exitCode === null && child.signalCode === null) {
471
+ child.kill("SIGTERM");
472
+ }
473
+ this.handleWorkerExit(child, null, null);
474
+ }
475
+ handleWorkerExit(child, code, signal) {
476
+ if (child !== this.child) {
477
+ if (child.exitCode === null && child.signalCode === null) {
478
+ child.kill("SIGTERM");
479
+ }
480
+ return;
481
+ }
462
482
  const childPid = this.child?.pid;
463
483
  this.detachChild();
464
484
  if (this.shuttingDown) {
@@ -472,7 +492,7 @@ export class StdioSupervisor {
472
492
  });
473
493
  this.failPendingRequestsOnWorkerExit({ code, signal });
474
494
  this.scheduleRestart();
475
- };
495
+ }
476
496
  handleWorkerMessage(message) {
477
497
  debugSupervisor("worker_message", {
478
498
  hasMethod: "method" in message,
@@ -580,7 +600,7 @@ export class StdioSupervisor {
580
600
  }
581
601
  }
582
602
  failPendingRequestsOnWorkerExit(exit) {
583
- const preservedInitializeKey = this.initializeRequest && !this.clientInitialized
603
+ const preservedInitializeKey = this.initializeRequest
584
604
  ? requestKey(this.initializeRequest.id)
585
605
  : undefined;
586
606
  const now = performance.now();
@@ -635,8 +655,8 @@ export class StdioSupervisor {
635
655
  child.stdout.off("data", this.handleWorkerData);
636
656
  child.stderr.off("data", this.handleWorkerStderr);
637
657
  child.stdin.off("error", this.handleWorkerStdinError);
638
- child.off("error", this.handleWorkerProcessError);
639
- child.off("exit", this.handleWorkerExit);
658
+ child.removeAllListeners("error");
659
+ child.removeAllListeners("exit");
640
660
  this.child = undefined;
641
661
  this.childReady = false;
642
662
  }
@@ -14,7 +14,7 @@ function runIntegrityCheck(db) {
14
14
  throw createError({
15
15
  code: ERROR_CODES.DB_FAILURE,
16
16
  message: "SQLite integrity check failed.",
17
- details: { integrityCheck: result }
17
+ details: { reason: "integrity_check_failed", integrityCheck: result }
18
18
  });
19
19
  }
20
20
  }
@@ -47,6 +47,22 @@ function isSchemaVersionMismatchError(error) {
47
47
  return (error.details?.reason === "schema_version_unsupported" ||
48
48
  error.details?.reason === "schema_version_invalid");
49
49
  }
50
+ const SQLITE_CORRUPT_ERRCODE = 11;
51
+ const SQLITE_NOTADB_ERRCODE = 26;
52
+ function isCorruptionError(error) {
53
+ if (isAppError(error)) {
54
+ return (error.code === ERROR_CODES.DB_FAILURE && error.details?.reason === "integrity_check_failed");
55
+ }
56
+ const sqliteError = error;
57
+ if (sqliteError?.code === "SQLITE_CORRUPT" || sqliteError?.code === "SQLITE_NOTADB") {
58
+ return true;
59
+ }
60
+ if (typeof sqliteError?.errcode !== "number") {
61
+ return false;
62
+ }
63
+ const primaryErrcode = sqliteError.errcode & 0xff;
64
+ return primaryErrcode === SQLITE_CORRUPT_ERRCODE || primaryErrcode === SQLITE_NOTADB_ERRCODE;
65
+ }
50
66
  function buildDefaultLogger() {
51
67
  return {
52
68
  warn: (message, details) => {
@@ -104,6 +120,13 @@ export function openDatabase(config, logger = buildDefaultLogger()) {
104
120
  throw caughtError;
105
121
  }
106
122
  if (!isMissingPath(config.sqlitePath)) {
123
+ if (!isCorruptionError(caughtError)) {
124
+ logger.error("SQLite initialization failed", {
125
+ path: config.sqlitePath,
126
+ reason: errorMessage
127
+ });
128
+ throw caughtError;
129
+ }
107
130
  const backupPath = backupCorruptedDb(config.sqlitePath);
108
131
  logger.warn("SQLite database integrity check failed. Recreated database after backup", {
109
132
  sqlitePath: config.sqlitePath,
@@ -51,7 +51,10 @@ export declare class FilesRepo {
51
51
  private readonly searchFtsStmt;
52
52
  private readonly getByPathsStmtCache;
53
53
  private readonly classLookupStmtCache;
54
+ private readonly dynamicStmtCache;
55
+ private static readonly DYNAMIC_STMT_CACHE_MAX;
54
56
  constructor(db: SqliteDatabase);
57
+ private prepareCached;
55
58
  clearFilesForArtifact(artifactId: string): void;
56
59
  insertFilesForArtifact(artifactId: string, files: IndexedFile[]): void;
57
60
  replaceFilesForArtifact(artifactId: string, files: IndexedFile[]): void;
@@ -67,18 +67,21 @@ function parseSearchCursor(cursor) {
67
67
  });
68
68
  }
69
69
  }
70
- function nextCursorFromRows(rows) {
71
- if (rows.length === 0) {
70
+ function nextCursorFromRows(rows, limit) {
71
+ if (rows.length === 0 || rows.length < limit) {
72
72
  return undefined;
73
73
  }
74
74
  const last = rows[rows.length - 1];
75
75
  return buildCursor(last.file_path);
76
76
  }
77
+ function compareFilePaths(left, right) {
78
+ return left < right ? -1 : left > right ? 1 : 0;
79
+ }
77
80
  function compareSearchOrdering(left, right) {
78
81
  if (right.score !== left.score) {
79
82
  return right.score - left.score;
80
83
  }
81
- return left.filePath.localeCompare(right.filePath);
84
+ return compareFilePaths(left.filePath, right.filePath);
82
85
  }
83
86
  function isAfterSearchCursor(hit, cursor) {
84
87
  if (hit.score < cursor.score) {
@@ -87,7 +90,7 @@ function isAfterSearchCursor(hit, cursor) {
87
90
  if (hit.score > cursor.score) {
88
91
  return false;
89
92
  }
90
- return hit.filePath.localeCompare(cursor.filePath) > 0;
93
+ return compareFilePaths(hit.filePath, cursor.filePath) > 0;
91
94
  }
92
95
  function buildPreview(content, query) {
93
96
  const normalizedQuery = query.toLowerCase();
@@ -137,6 +140,10 @@ export class FilesRepo {
137
140
  searchFtsStmt;
138
141
  getByPathsStmtCache = new Map();
139
142
  classLookupStmtCache = new Map();
143
+ // Fixed-SQL statements built outside the constructor; cached (LRU by SQL text)
144
+ // so hot paths do not re-prepare per call.
145
+ dynamicStmtCache = new Map();
146
+ static DYNAMIC_STMT_CACHE_MAX = 64;
140
147
  constructor(db) {
141
148
  this.db = db;
142
149
  this.deleteStmt = this.db.prepare(`
@@ -155,14 +162,14 @@ export class FilesRepo {
155
162
  this.listStmt = this.db.prepare(`
156
163
  SELECT artifact_id, file_path
157
164
  FROM files
158
- WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ? || '%')
165
+ WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ? ESCAPE '\\')
159
166
  ORDER BY file_path ASC
160
167
  LIMIT ?
161
168
  `);
162
169
  this.listRowsStmt = this.db.prepare(`
163
170
  SELECT artifact_id, file_path, content, content_bytes, content_hash
164
171
  FROM files
165
- WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ? || '%')
172
+ WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ? ESCAPE '\\')
166
173
  ORDER BY file_path ASC
167
174
  LIMIT ?
168
175
  `);
@@ -188,6 +195,23 @@ export class FilesRepo {
188
195
  LIMIT ?
189
196
  `);
190
197
  }
198
+ prepareCached(sql) {
199
+ const cached = this.dynamicStmtCache.get(sql);
200
+ if (cached) {
201
+ // Refresh recency so hot statements survive eviction.
202
+ this.dynamicStmtCache.delete(sql);
203
+ this.dynamicStmtCache.set(sql, cached);
204
+ return cached;
205
+ }
206
+ const stmt = this.db.prepare(sql);
207
+ this.dynamicStmtCache.set(sql, stmt);
208
+ if (this.dynamicStmtCache.size > FilesRepo.DYNAMIC_STMT_CACHE_MAX) {
209
+ const oldest = this.dynamicStmtCache.keys().next().value;
210
+ if (oldest !== undefined)
211
+ this.dynamicStmtCache.delete(oldest);
212
+ }
213
+ return stmt;
214
+ }
191
215
  clearFilesForArtifact(artifactId) {
192
216
  this.deleteStmt.run([artifactId]);
193
217
  }
@@ -231,15 +255,15 @@ export class FilesRepo {
231
255
  }
232
256
  listFiles(artifactId, options) {
233
257
  const cursor = parseCursor(options.cursor);
234
- const rows = this.listStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix ?? "", Math.max(1, options.limit));
258
+ const rows = this.listStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix != null ? `${escapeLikeNeedle(options.prefix)}%` : "", Math.max(1, options.limit));
235
259
  return {
236
260
  items: rows.map((row) => row.file_path),
237
- nextCursor: nextCursorFromRows(rows)
261
+ nextCursor: nextCursorFromRows(rows, Math.max(1, options.limit))
238
262
  };
239
263
  }
240
264
  listFileRows(artifactId, options) {
241
265
  const cursor = parseCursor(options.cursor);
242
- const rows = this.listRowsStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix ?? "", Math.max(1, options.limit));
266
+ const rows = this.listRowsStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix != null ? `${escapeLikeNeedle(options.prefix)}%` : "", Math.max(1, options.limit));
243
267
  return {
244
268
  items: rows.map((row) => ({
245
269
  artifactId: row.artifact_id,
@@ -248,7 +272,7 @@ export class FilesRepo {
248
272
  contentBytes: row.content_bytes,
249
273
  contentHash: row.content_hash
250
274
  })),
251
- nextCursor: nextCursorFromRows(rows)
275
+ nextCursor: nextCursorFromRows(rows, Math.max(1, options.limit))
252
276
  };
253
277
  }
254
278
  /**
@@ -288,7 +312,7 @@ export class FilesRepo {
288
312
  return { items: [], nextCursor: undefined, scannedRows: 0, dbRoundtrips: 0 };
289
313
  }
290
314
  const cursor = parseSearchCursor(options.cursor);
291
- const likeQuery = `%${normalized}%`;
315
+ const likeQuery = `%${escapeLikeNeedle(normalized)}%`;
292
316
  const ftsQuery = buildIndexedMatchQuery(normalized, options.match);
293
317
  const mode = options.mode ?? "mixed";
294
318
  // Cursor-adaptive fetch limit: when no cursor, use a generous limit;
@@ -298,28 +322,11 @@ export class FilesRepo {
298
322
  const fetchLimit = baseFetchLimit;
299
323
  // When cursor score is below all possible bands, skip both queries
300
324
  const cursorExhausted = cursor != null && cursor.score < 100;
301
- // Skip path query entirely when cursor is within the content-only tier
302
- const cursorPastPath = cursor != null && cursor.score < 120;
303
- const includePath = mode !== "text" && !cursorExhausted && !cursorPastPath;
325
+ const includePath = mode !== "text" && !cursorExhausted;
304
326
  const includeContent = mode !== "path" && !cursorExhausted;
305
- // Path query: push cursor into SQL when cursor.score == 120 (within path tier)
306
- let pathRows;
307
- if (includePath && cursor && cursor.score === 120) {
308
- // Cursor is within the path tier — only fetch paths after cursor.filePath
309
- pathRows = this.db.prepare(`
310
- SELECT file_path
311
- FROM files
312
- WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\' AND file_path > ?
313
- ORDER BY file_path ASC
314
- LIMIT ?
315
- `).all(artifactId, likeQuery, cursor.filePath, fetchLimit);
316
- }
317
- else if (includePath) {
318
- pathRows = this.searchPathStmt.all(artifactId, likeQuery, fetchLimit);
319
- }
320
- else {
321
- pathRows = [];
322
- }
327
+ const pathRows = includePath
328
+ ? this.searchPathStmt.all(artifactId, likeQuery, fetchLimit)
329
+ : [];
323
330
  const merged = pathRows.map((row) => ({
324
331
  filePath: row.file_path,
325
332
  score: 120,
@@ -417,7 +424,7 @@ export class FilesRepo {
417
424
  return 0;
418
425
  }
419
426
  try {
420
- const row = this.db.prepare(`SELECT COUNT(*) AS cnt FROM files_fts WHERE artifact_id = ? AND files_fts MATCH ?`).get(artifactId, ftsQuery);
427
+ const row = this.prepareCached(`SELECT COUNT(*) AS cnt FROM files_fts WHERE artifact_id = ? AND files_fts MATCH ?`).get(artifactId, ftsQuery);
421
428
  return row?.cnt ?? 0;
422
429
  }
423
430
  catch {
@@ -433,8 +440,8 @@ export class FilesRepo {
433
440
  if (!normalized) {
434
441
  return 0;
435
442
  }
436
- const likeQuery = `%${normalized}%`;
437
- const row = this.db.prepare(`SELECT COUNT(*) AS cnt FROM files WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\'`).get(artifactId, likeQuery);
443
+ const likeQuery = `%${escapeLikeNeedle(normalized)}%`;
444
+ const row = this.prepareCached(`SELECT COUNT(*) AS cnt FROM files WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\'`).get(artifactId, likeQuery);
438
445
  return row?.cnt ?? 0;
439
446
  }
440
447
  findFirstFilePathByName(artifactId, fileName) {
@@ -442,8 +449,7 @@ export class FilesRepo {
442
449
  if (!normalized) {
443
450
  return undefined;
444
451
  }
445
- const row = this.db
446
- .prepare(`
452
+ const row = this.prepareCached(`
447
453
  SELECT file_path
448
454
  FROM files
449
455
  WHERE artifact_id = ?
@@ -46,12 +46,21 @@ export declare class SymbolsRepo {
46
46
  private readonly listByArtifactStmt;
47
47
  private readonly listByArtifactKindStmt;
48
48
  private readonly listByFileStmt;
49
+ private readonly dynamicStmtCache;
50
+ private static readonly DYNAMIC_STMT_CACHE_MAX;
49
51
  constructor(db: SqliteDatabase);
52
+ private prepareCached;
50
53
  clearSymbolsForArtifact(artifactId: string): void;
51
54
  insertSymbolsForArtifact(artifactId: string, symbols: IndexedSymbol[]): void;
52
55
  replaceSymbolsForArtifact(artifactId: string, symbols: IndexedSymbol[]): void;
53
56
  findSymbols(options: FindSymbolsOptions): PagedResult<SymbolRow>;
54
57
  listSymbolsForArtifact(artifactId: string, symbolKind?: string): SymbolRow[];
58
+ /**
59
+ * Streaming variant of listSymbolsForArtifact: rows are mapped lazily so callers
60
+ * that filter (e.g. regex symbol search) never materialize the full symbol set.
61
+ * Do not run other statements on this connection while consuming the iterator.
62
+ */
63
+ iterateSymbolsForArtifact(artifactId: string, symbolKind?: string): IterableIterator<SymbolRow>;
55
64
  listSymbolsForFile(artifactId: string, filePath: string): SymbolRow[];
56
65
  listSymbolsForFiles(artifactId: string, filePaths: string[], symbolKind?: string): Map<string, SymbolRow[]>;
57
66
  findBySymbolNames(artifactId: string, symbolNames: string[]): SymbolRow[];
@@ -60,6 +60,10 @@ export class SymbolsRepo {
60
60
  listByArtifactStmt;
61
61
  listByArtifactKindStmt;
62
62
  listByFileStmt;
63
+ // Dynamic WHERE-clause builders produce a small bounded set of SQL strings;
64
+ // cache their prepared statements (LRU by SQL text) instead of re-preparing per call.
65
+ dynamicStmtCache = new Map();
66
+ static DYNAMIC_STMT_CACHE_MAX = 64;
63
67
  constructor(db) {
64
68
  this.db = db;
65
69
  this.deleteStmt = this.db.prepare(`
@@ -106,6 +110,23 @@ export class SymbolsRepo {
106
110
  ORDER BY line ASC, symbol_name ASC
107
111
  `);
108
112
  }
113
+ prepareCached(sql) {
114
+ const cached = this.dynamicStmtCache.get(sql);
115
+ if (cached) {
116
+ // Refresh recency so hot statements survive eviction.
117
+ this.dynamicStmtCache.delete(sql);
118
+ this.dynamicStmtCache.set(sql, cached);
119
+ return cached;
120
+ }
121
+ const stmt = this.db.prepare(sql);
122
+ this.dynamicStmtCache.set(sql, stmt);
123
+ if (this.dynamicStmtCache.size > SymbolsRepo.DYNAMIC_STMT_CACHE_MAX) {
124
+ const oldest = this.dynamicStmtCache.keys().next().value;
125
+ if (oldest !== undefined)
126
+ this.dynamicStmtCache.delete(oldest);
127
+ }
128
+ return stmt;
129
+ }
109
130
  clearSymbolsForArtifact(artifactId) {
110
131
  this.deleteStmt.run([artifactId]);
111
132
  }
@@ -150,7 +171,7 @@ export class SymbolsRepo {
150
171
  LIMIT ?
151
172
  `;
152
173
  const nameParam = options.exact ? symbolName : `${symbolName}%`;
153
- rows = this.db.prepare(sql).all(options.artifactId, options.symbolKind ?? "", options.symbolKind ?? "", nameParam, cursor.symbolName, cursor.symbolName, cursor.filePath, cursor.symbolName, cursor.filePath, cursor.line, Math.max(1, fetchLimit));
174
+ rows = this.prepareCached(sql).all(options.artifactId, options.symbolKind ?? "", options.symbolKind ?? "", nameParam, cursor.symbolName, cursor.symbolName, cursor.filePath, cursor.symbolName, cursor.filePath, cursor.line, Math.max(1, fetchLimit));
154
175
  }
155
176
  else {
156
177
  const queryRows = options.exact ? this.searchExactStmt : this.searchStmt;
@@ -173,16 +194,26 @@ export class SymbolsRepo {
173
194
  };
174
195
  }
175
196
  listSymbolsForArtifact(artifactId, symbolKind) {
197
+ return [...this.iterateSymbolsForArtifact(artifactId, symbolKind)];
198
+ }
199
+ /**
200
+ * Streaming variant of listSymbolsForArtifact: rows are mapped lazily so callers
201
+ * that filter (e.g. regex symbol search) never materialize the full symbol set.
202
+ * Do not run other statements on this connection while consuming the iterator.
203
+ */
204
+ *iterateSymbolsForArtifact(artifactId, symbolKind) {
176
205
  const rows = (symbolKind
177
- ? this.listByArtifactKindStmt.all(artifactId, symbolKind)
178
- : this.listByArtifactStmt.all(artifactId));
179
- return rows.map((row) => toSymbolRow(artifactId, {
180
- filePath: row.file_path,
181
- symbolKind: row.symbol_kind,
182
- symbolName: row.symbol_name,
183
- qualifiedName: row.qualified_name ?? undefined,
184
- line: row.line
185
- }));
206
+ ? this.listByArtifactKindStmt.iterate(artifactId, symbolKind)
207
+ : this.listByArtifactStmt.iterate(artifactId));
208
+ for (const row of rows) {
209
+ yield toSymbolRow(artifactId, {
210
+ filePath: row.file_path,
211
+ symbolKind: row.symbol_kind,
212
+ symbolName: row.symbol_name,
213
+ qualifiedName: row.qualified_name ?? undefined,
214
+ line: row.line
215
+ });
216
+ }
186
217
  }
187
218
  listSymbolsForFile(artifactId, filePath) {
188
219
  const rows = this.listByFileStmt.all(artifactId, filePath);
@@ -209,7 +240,7 @@ export class SymbolsRepo {
209
240
  ORDER BY file_path ASC, line ASC, symbol_name ASC
210
241
  `;
211
242
  const params = symbolKind ? [artifactId, ...uniqueFilePaths, symbolKind] : [artifactId, ...uniqueFilePaths];
212
- const rows = this.db.prepare(sql).all(...params);
243
+ const rows = this.prepareCached(sql).all(...params);
213
244
  const byFile = new Map();
214
245
  for (const row of rows) {
215
246
  const bucket = byFile.get(row.file_path) ?? [];
@@ -230,8 +261,7 @@ export class SymbolsRepo {
230
261
  return [];
231
262
  }
232
263
  const placeholders = unique.map(() => "?").join(", ");
233
- const rows = this.db
234
- .prepare(`
264
+ const rows = this.prepareCached(`
235
265
  SELECT file_path, symbol_kind, symbol_name, qualified_name, line
236
266
  FROM symbols
237
267
  WHERE artifact_id = ?
@@ -306,7 +336,7 @@ export class SymbolsRepo {
306
336
  ORDER BY symbol_name ASC, file_path ASC, line ASC
307
337
  LIMIT ?
308
338
  `;
309
- const rows = this.db.prepare(sql).all(...params);
339
+ const rows = this.prepareCached(sql).all(...params);
310
340
  const hasMore = rows.length > rawLimit;
311
341
  const page = hasMore ? rows.slice(0, rawLimit) : rows;
312
342
  const lastRow = page.length > 0 ? page[page.length - 1] : undefined;
@@ -360,12 +390,11 @@ export class SymbolsRepo {
360
390
  WHERE artifact_id = ?
361
391
  ${where.length > 0 ? `AND ${where.join("\n AND ")}` : ""}
362
392
  `;
363
- const row = this.db.prepare(sql).get(...params);
393
+ const row = this.prepareCached(sql).get(...params);
364
394
  return row?.cnt ?? 0;
365
395
  }
366
396
  listDistinctFilePathsByKind(artifactId, symbolKind) {
367
- const rows = this.db
368
- .prepare(`
397
+ const rows = this.prepareCached(`
369
398
  SELECT DISTINCT file_path
370
399
  FROM symbols
371
400
  WHERE artifact_id = ? AND symbol_kind = ?
@@ -380,8 +409,7 @@ export class SymbolsRepo {
380
409
  if (!normalizedClassName || !normalizedSimpleName) {
381
410
  return undefined;
382
411
  }
383
- const row = this.db
384
- .prepare(`
412
+ const row = this.prepareCached(`
385
413
  SELECT file_path
386
414
  FROM symbols
387
415
  WHERE artifact_id = ?
@@ -1,7 +1,7 @@
1
1
  // A type token may carry spaced generics (Map<String, Integer>), arrays (int[][]),
2
2
  // wildcards (List<? extends Foo>), and FQNs (java.util.Map). It must not contain "=".
3
3
  const TYPE_TOKEN = "[\\w.$][\\w.$<>\\[\\],?\\s]*";
4
- const MODIFIER = "(?:public|private|protected|abstract|final|static|native|synchronized|default|strictfp|transient|volatile)";
4
+ const MODIFIER = "(?:public|private|protected|abstract|final|static|native|synchronized|default|strictfp|transient|volatile|sealed|non-sealed)";
5
5
  const MODIFIER_OR_ANNOTATION_RUN = `(?:(?:@[\\w.]+|${MODIFIER})\\s+)*`;
6
6
  const TYPE_PARAMS = "(?:<[^>]+>\\s*)?";
7
7
  const THROWS_CLAUSE = "(?:\\s+throws\\s+[\\w.$,\\s]+)?";
@@ -6,7 +6,8 @@ export type ToolMeta = {
6
6
  requestId: string;
7
7
  tool: string;
8
8
  durationMs: number;
9
- warnings: string[];
9
+ /** Omitted entirely when there are no warnings (token efficiency). */
10
+ warnings?: string[];
10
11
  /** Structured companion to `warnings`: one classified entry per warning string, referencing its text via `warnings[detail.index]`. */
11
12
  warningDetails?: WarningDetail[];
12
13
  detailApplied?: "summary" | "standard" | "full";
@@ -317,7 +317,22 @@ export function buildValidateMixinSuggestedParams(normalizedInput) {
317
317
  version
318
318
  };
319
319
  }
320
- const path = asNonEmptyString(inputRecord?.path) ??
320
+ if (inputRecord?.mode === "project") {
321
+ const projectInputPath = asNonEmptyString(inputRecord.path);
322
+ if (projectInputPath) {
323
+ return {
324
+ ...shared,
325
+ input: {
326
+ mode: "project",
327
+ path: projectInputPath
328
+ },
329
+ version
330
+ };
331
+ }
332
+ }
333
+ const path = (inputRecord?.mode === "path" || inputRecord?.mode === undefined
334
+ ? asNonEmptyString(inputRecord?.path)
335
+ : undefined) ??
321
336
  asNonEmptyString(record.sourcePath);
322
337
  if (path) {
323
338
  return {
@@ -600,6 +615,16 @@ export function buildValidateProjectSuggestedParams(normalizedInput) {
600
615
  return result;
601
616
  }
602
617
  const inputRecord = asObjectRecord(subjectRecord?.input) ?? asObjectRecord(record.input);
618
+ if (task === "access-transformer") {
619
+ result.subject = {
620
+ kind: "access-transformer",
621
+ input: inputRecord ?? {
622
+ mode: "inline",
623
+ content: "<access transformer contents>"
624
+ }
625
+ };
626
+ return result;
627
+ }
603
628
  result.subject = {
604
629
  kind: "access-widener",
605
630
  input: inputRecord ?? {
@@ -211,11 +211,11 @@ export declare const sourceLookupTargetSchema: z.ZodDiscriminatedUnion<"kind", [
211
211
  kind: "artifact";
212
212
  artifactId: string;
213
213
  }>]>;
214
- export declare const RESOLVE_ARTIFACT_TARGET_DESCRIPTION = "Object with kind. Examples: {\"kind\":\"version\",\"value\":\"1.21.10\"}, {\"kind\":\"workspace\"} (uses projectPath), or {\"kind\":\"dependency\",\"group\":\"dev.architectury\",\"name\":\"architectury\"}. Must be an object, not a string.";
215
- export declare const SOURCE_LOOKUP_TARGET_DESCRIPTION = "Object with kind (same shape as resolve-artifact). Examples: {\"kind\":\"version\",\"value\":\"1.21.10\"}, {\"kind\":\"workspace\"} (uses projectPath), {\"kind\":\"dependency\",\"group\":\"...\",\"name\":\"...\"}, or {\"kind\":\"artifact\",\"artifactId\":\"...\"} to reuse an already-resolved artifact. Must be an object, not a string.";
216
- export declare const SOURCE_SCOPE_DESCRIPTION = "vanilla = Mojang client jar only; merged = source-oriented merged runtime discovery; loader = loader/runtime artifact discovery when the workspace exposes transformed runtime jars.";
214
+ export declare const RESOLVE_ARTIFACT_TARGET_DESCRIPTION = "Object, not string. e.g. {\"kind\":\"version\",\"value\":\"1.21.10\"}, {\"kind\":\"workspace\"}, {\"kind\":\"dependency\",\"group\":\"g\",\"name\":\"n\"}.";
215
+ export declare const SOURCE_LOOKUP_TARGET_DESCRIPTION = "Same shape as resolve-artifact target, plus {\"kind\":\"artifact\",\"artifactId\":\"...\"} to reuse a resolved artifact. Object, not string.";
216
+ export declare const SOURCE_SCOPE_DESCRIPTION = "vanilla = Mojang client jar only; merged = merged runtime discovery; loader = loader-transformed runtime jars.";
217
217
  export declare const SIGNATURE_MODE_DESCRIPTION = "exact: descriptor required for kind=method; name-only (default): match by owner+name only.";
218
- export declare const NAME_MODE_DESCRIPTION = "auto (default): accept a fully-qualified name, or a dotless name where the tool allows it; fqcn: require a fully-qualified name.";
218
+ export declare const NAME_MODE_DESCRIPTION = "auto (default): FQCN, or dotless name where allowed; fqcn: require FQCN.";
219
219
  export declare const listVersionsShape: {
220
220
  includeSnapshots: z.ZodDefault<z.ZodBoolean>;
221
221
  limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;