@adhisang/minecraft-modding-mcp 5.0.0 → 6.1.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 (83) hide show
  1. package/CHANGELOG.md +55 -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/config.js +1 -1
  6. package/dist/decompiler/vineflower.js +8 -2
  7. package/dist/entry-tools/analyze-mod-service.js +2 -1
  8. package/dist/entry-tools/analyze-symbol-service.js +18 -9
  9. package/dist/entry-tools/batch-class-members-service.d.ts +2 -0
  10. package/dist/entry-tools/batch-class-members-service.js +1 -0
  11. package/dist/entry-tools/compare-minecraft-service.js +1 -1
  12. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -1
  13. package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -1
  14. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +2 -1
  15. package/dist/entry-tools/inspect-minecraft/internal.js +2 -0
  16. package/dist/entry-tools/response-contract.d.ts +2 -0
  17. package/dist/entry-tools/response-contract.js +2 -2
  18. package/dist/entry-tools/validate-project/cases/project-summary.js +30 -12
  19. package/dist/entry-tools/validate-project-service.d.ts +4 -4
  20. package/dist/entry-tools/verify-mixin-target-service.js +7 -7
  21. package/dist/index.js +21 -16
  22. package/dist/java-process.js +1 -1
  23. package/dist/json-rpc-framing.d.ts +4 -0
  24. package/dist/json-rpc-framing.js +23 -1
  25. package/dist/mapping/internal-types.d.ts +17 -0
  26. package/dist/mapping/loaders/tiny-maven.js +46 -20
  27. package/dist/mapping/lookup.d.ts +12 -1
  28. package/dist/mapping/lookup.js +53 -1
  29. package/dist/mapping-service.js +37 -22
  30. package/dist/minecraft-explorer-service.js +2 -1
  31. package/dist/mixin/annotation-validators.js +21 -3
  32. package/dist/mixin/parsed-validator.js +1 -0
  33. package/dist/mixin-parser.js +71 -3
  34. package/dist/mod-decompile-service.d.ts +2 -0
  35. package/dist/mod-decompile-service.js +19 -2
  36. package/dist/mod-remap-service.js +6 -5
  37. package/dist/mojang-tiny-mapping-service.js +5 -2
  38. package/dist/nbt/java-nbt-codec.js +40 -16
  39. package/dist/nbt/json-patch.js +51 -12
  40. package/dist/nbt/typed-json.d.ts +1 -0
  41. package/dist/nbt/typed-json.js +12 -2
  42. package/dist/nbt/types.d.ts +2 -2
  43. package/dist/path-converter.js +10 -3
  44. package/dist/registry-service.d.ts +2 -0
  45. package/dist/registry-service.js +16 -1
  46. package/dist/repo-downloader.js +4 -3
  47. package/dist/source/artifact-resolver.js +1 -1
  48. package/dist/source/class-source/members-builder.d.ts +26 -0
  49. package/dist/source/class-source/members-builder.js +31 -4
  50. package/dist/source/class-source/snippet-builder.js +6 -0
  51. package/dist/source/class-source-helpers.d.ts +4 -4
  52. package/dist/source/class-source-helpers.js +12 -11
  53. package/dist/source/class-source.d.ts +19 -0
  54. package/dist/source/class-source.js +87 -26
  55. package/dist/source/file-access.js +3 -2
  56. package/dist/source/indexer.js +24 -7
  57. package/dist/source/lifecycle/mapping-helpers.js +28 -3
  58. package/dist/source/lifecycle/runtime-check.js +20 -6
  59. package/dist/source/member-pattern.d.ts +16 -0
  60. package/dist/source/member-pattern.js +27 -0
  61. package/dist/source/search.js +23 -8
  62. package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
  63. package/dist/source/workspace-target.js +2 -1
  64. package/dist/source-resolver.js +2 -2
  65. package/dist/source-service.d.ts +8 -4
  66. package/dist/source-service.js +9 -1
  67. package/dist/stdio-supervisor.d.ts +2 -2
  68. package/dist/stdio-supervisor.js +29 -9
  69. package/dist/storage/db.js +24 -1
  70. package/dist/storage/files-repo.d.ts +3 -0
  71. package/dist/storage/files-repo.js +43 -37
  72. package/dist/storage/symbols-repo.d.ts +9 -0
  73. package/dist/storage/symbols-repo.js +47 -19
  74. package/dist/symbols/symbol-extractor.js +1 -1
  75. package/dist/tool-guidance.d.ts +2 -1
  76. package/dist/tool-guidance.js +27 -2
  77. package/dist/tool-schemas.d.ts +15 -4
  78. package/dist/tool-schemas.js +102 -97
  79. package/dist/warning-details.d.ts +12 -0
  80. package/dist/warning-details.js +17 -0
  81. package/dist/workspace-mapping-service.js +9 -0
  82. package/docs/tool-reference.md +23 -0
  83. package/package.json +1 -1
@@ -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 ?? {
@@ -647,7 +672,7 @@ export function buildInvalidInputGuidance(tool, normalizedInput) {
647
672
  }
648
673
  if (tool === "get-class-source" || tool === "get-class-members") {
649
674
  return gatedGuidance(tool, [
650
- `${tool}.target must be an object: {"kind":"version|jar|coordinate","value":"..."} or {"kind":"artifact","artifactId":"..."} (same shape as resolve-artifact).`,
675
+ `${tool}.target must be an object: {"kind":"version|jar|coordinate","value":"..."}, {"kind":"workspace"}, {"kind":"dependency","group":"...","name":"...","versionFromProject":true} (inspect a Fabric/loader dependency like vanilla), or {"kind":"artifact","artifactId":"..."} (same shape as resolve-artifact).`,
651
676
  "Bare string targets are not accepted; wrap the value under target.kind/target.value."
652
677
  ], buildSourceLookupSuggestedParams(tool, normalizedInput));
653
678
  }
@@ -211,11 +211,14 @@ 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\"}, or to inspect a loader/Fabric dependency like vanilla: {\"kind\":\"dependency\",\"group\":\"net.fabricmc.fabric-api\",\"name\":\"fabric-api\",\"versionFromProject\":true} (needs projectPath) or with an explicit \"version\".";
215
+ export declare const SOURCE_LOOKUP_TARGET_DESCRIPTION = "Same shape as resolve-artifact target (incl. {\"kind\":\"dependency\",...} to read a Fabric/loader dependency class like vanilla), 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
+ export declare const MEMBER_PATTERN_DESCRIPTION = "Case-insensitive substring filter on member names. Use \"|\" for OR alternatives, e.g. \"getStateForPlacement|canSurvive|setPlacedBy\" (each token is matched as a substring, not a regex).";
218
+ export declare const memberProjectionSchema: z.ZodEnum<["names", "signatures", "full"]>;
219
+ export declare const MEMBER_PROJECTION_DESCRIPTION = "Per-member field projection: \"names\" (member name only), \"signatures\" (name + javaSignature, no jvmDescriptor), or \"full\" (default; complete member shape). Use \"names\"/\"signatures\" to cut response tokens for existence/signature checks.";
217
220
  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.";
221
+ export declare const NAME_MODE_DESCRIPTION = "auto (default): FQCN, or dotless name where allowed; fqcn: require FQCN.";
219
222
  export declare const listVersionsShape: {
220
223
  includeSnapshots: z.ZodDefault<z.ZodBoolean>;
221
224
  limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
@@ -850,6 +853,7 @@ export declare const getClassMembersShape: {
850
853
  includeSynthetic: z.ZodDefault<z.ZodBoolean>;
851
854
  includeInherited: z.ZodDefault<z.ZodBoolean>;
852
855
  memberPattern: z.ZodOptional<z.ZodString>;
856
+ projection: z.ZodOptional<z.ZodEnum<["names", "signatures", "full"]>>;
853
857
  maxMembers: z.ZodOptional<z.ZodNumber>;
854
858
  cursor: z.ZodOptional<z.ZodString>;
855
859
  projectPath: z.ZodOptional<z.ZodString>;
@@ -938,6 +942,7 @@ export declare const getClassMembersSchema: z.ZodObject<{
938
942
  includeSynthetic: z.ZodDefault<z.ZodBoolean>;
939
943
  includeInherited: z.ZodDefault<z.ZodBoolean>;
940
944
  memberPattern: z.ZodOptional<z.ZodString>;
945
+ projection: z.ZodOptional<z.ZodEnum<["names", "signatures", "full"]>>;
941
946
  maxMembers: z.ZodOptional<z.ZodNumber>;
942
947
  cursor: z.ZodOptional<z.ZodString>;
943
948
  projectPath: z.ZodOptional<z.ZodString>;
@@ -992,6 +997,7 @@ export declare const getClassMembersSchema: z.ZodObject<{
992
997
  memberPattern?: string | undefined;
993
998
  include?: ("entries" | "workspace" | "warnings" | "source" | "registry" | "preview" | "files" | "samples" | "timings" | "issues" | "recovery" | "paths" | "members" | "provenance" | "artifact" | "candidates" | "classes" | "timeline" | "descriptors" | "diff" | "matrix" | "health" | "owners" | "cacheEntries")[] | undefined;
994
999
  gradleUserHome?: string | undefined;
1000
+ projection?: "full" | "names" | "signatures" | undefined;
995
1001
  }, {
996
1002
  className: string;
997
1003
  target: {
@@ -1034,6 +1040,7 @@ export declare const getClassMembersSchema: z.ZodObject<{
1034
1040
  include?: ("entries" | "workspace" | "warnings" | "source" | "registry" | "preview" | "files" | "samples" | "timings" | "issues" | "recovery" | "paths" | "members" | "provenance" | "artifact" | "candidates" | "classes" | "timeline" | "descriptors" | "diff" | "matrix" | "health" | "owners" | "cacheEntries")[] | undefined;
1035
1041
  gradleUserHome?: string | undefined;
1036
1042
  includeProvenance?: boolean | undefined;
1043
+ projection?: "full" | "names" | "signatures" | undefined;
1037
1044
  includeDescriptors?: boolean | undefined;
1038
1045
  }>;
1039
1046
  export declare const verifyMixinTargetMemberSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
@@ -1811,6 +1818,7 @@ export declare const batchClassMembersShape: {
1811
1818
  concurrency: z.ZodOptional<z.ZodNumber>;
1812
1819
  failFast: z.ZodOptional<z.ZodBoolean>;
1813
1820
  detail: z.ZodDefault<z.ZodEnum<["summary", "standard", "full"]>>;
1821
+ projection: z.ZodOptional<z.ZodEnum<["names", "signatures", "full"]>>;
1814
1822
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<["warnings", "provenance", "candidates", "members", "descriptors", "source", "files", "samples", "diff", "issues", "timeline", "matrix", "entries", "workspace", "health", "recovery", "paths", "owners", "preview", "cacheEntries", "timings", "artifact", "classes", "registry"]>, "many">>;
1815
1823
  entries: z.ZodArray<z.ZodObject<{
1816
1824
  className: z.ZodString;
@@ -1905,6 +1913,7 @@ export declare const batchClassMembersSchema: z.ZodObject<{
1905
1913
  concurrency: z.ZodOptional<z.ZodNumber>;
1906
1914
  failFast: z.ZodOptional<z.ZodBoolean>;
1907
1915
  detail: z.ZodDefault<z.ZodEnum<["summary", "standard", "full"]>>;
1916
+ projection: z.ZodOptional<z.ZodEnum<["names", "signatures", "full"]>>;
1908
1917
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<["warnings", "provenance", "candidates", "members", "descriptors", "source", "files", "samples", "diff", "issues", "timeline", "matrix", "entries", "workspace", "health", "recovery", "paths", "owners", "preview", "cacheEntries", "timings", "artifact", "classes", "registry"]>, "many">>;
1909
1918
  entries: z.ZodArray<z.ZodObject<{
1910
1919
  className: z.ZodString;
@@ -1967,6 +1976,7 @@ export declare const batchClassMembersSchema: z.ZodObject<{
1967
1976
  sourcePriority?: "loom-first" | "maven-first" | undefined;
1968
1977
  include?: ("entries" | "workspace" | "warnings" | "source" | "registry" | "preview" | "files" | "samples" | "timings" | "issues" | "recovery" | "paths" | "members" | "provenance" | "artifact" | "candidates" | "classes" | "timeline" | "descriptors" | "diff" | "matrix" | "health" | "owners" | "cacheEntries")[] | undefined;
1969
1978
  gradleUserHome?: string | undefined;
1979
+ projection?: "full" | "names" | "signatures" | undefined;
1970
1980
  concurrency?: number | undefined;
1971
1981
  failFast?: boolean | undefined;
1972
1982
  }, {
@@ -2008,6 +2018,7 @@ export declare const batchClassMembersSchema: z.ZodObject<{
2008
2018
  detail?: "summary" | "standard" | "full" | undefined;
2009
2019
  include?: ("entries" | "workspace" | "warnings" | "source" | "registry" | "preview" | "files" | "samples" | "timings" | "issues" | "recovery" | "paths" | "members" | "provenance" | "artifact" | "candidates" | "classes" | "timeline" | "descriptors" | "diff" | "matrix" | "health" | "owners" | "cacheEntries")[] | undefined;
2010
2020
  gradleUserHome?: string | undefined;
2021
+ projection?: "full" | "names" | "signatures" | undefined;
2011
2022
  concurrency?: number | undefined;
2012
2023
  failFast?: boolean | undefined;
2013
2024
  }>;