@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.
- package/CHANGELOG.md +36 -0
- package/dist/access-widener-parser.d.ts +1 -0
- package/dist/access-widener-parser.js +6 -3
- package/dist/cache-registry.js +35 -31
- package/dist/decompiler/vineflower.js +8 -2
- package/dist/entry-tools/analyze-mod-service.js +2 -1
- package/dist/entry-tools/analyze-symbol-service.js +18 -9
- package/dist/entry-tools/compare-minecraft-service.js +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +2 -1
- package/dist/entry-tools/inspect-minecraft/internal.js +2 -0
- package/dist/entry-tools/response-contract.d.ts +2 -0
- package/dist/entry-tools/response-contract.js +2 -2
- package/dist/entry-tools/validate-project-service.d.ts +4 -4
- package/dist/entry-tools/verify-mixin-target-service.js +7 -7
- package/dist/index.js +20 -16
- package/dist/java-process.js +1 -1
- package/dist/json-rpc-framing.d.ts +4 -0
- package/dist/json-rpc-framing.js +23 -1
- package/dist/mapping/internal-types.d.ts +17 -0
- package/dist/mapping/loaders/tiny-maven.js +46 -20
- package/dist/mapping/lookup.d.ts +12 -1
- package/dist/mapping/lookup.js +53 -1
- package/dist/mapping-service.js +36 -21
- package/dist/mixin/annotation-validators.js +21 -3
- package/dist/mixin/parsed-validator.js +1 -0
- package/dist/mixin-parser.js +71 -3
- package/dist/mod-decompile-service.d.ts +2 -0
- package/dist/mod-decompile-service.js +19 -2
- package/dist/mod-remap-service.js +6 -5
- package/dist/mojang-tiny-mapping-service.js +5 -2
- package/dist/nbt/java-nbt-codec.js +40 -16
- package/dist/nbt/json-patch.js +51 -12
- package/dist/nbt/typed-json.d.ts +1 -0
- package/dist/nbt/typed-json.js +12 -2
- package/dist/nbt/types.d.ts +2 -2
- package/dist/path-converter.js +10 -3
- package/dist/registry-service.d.ts +2 -0
- package/dist/registry-service.js +16 -1
- package/dist/repo-downloader.js +4 -3
- package/dist/source/artifact-resolver.js +1 -1
- package/dist/source/class-source/snippet-builder.js +6 -0
- package/dist/source/class-source-helpers.d.ts +4 -4
- package/dist/source/class-source-helpers.js +12 -11
- package/dist/source/class-source.d.ts +19 -0
- package/dist/source/class-source.js +48 -21
- package/dist/source/file-access.js +3 -2
- package/dist/source/indexer.js +24 -7
- package/dist/source/lifecycle/mapping-helpers.js +28 -3
- package/dist/source/lifecycle/runtime-check.js +20 -6
- package/dist/source/search.js +23 -8
- package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
- package/dist/source/workspace-target.js +2 -1
- package/dist/source-resolver.js +2 -2
- package/dist/source-service.js +9 -1
- package/dist/stdio-supervisor.d.ts +2 -2
- package/dist/stdio-supervisor.js +29 -9
- package/dist/storage/db.js +24 -1
- package/dist/storage/files-repo.d.ts +3 -0
- package/dist/storage/files-repo.js +43 -37
- package/dist/storage/symbols-repo.d.ts +9 -0
- package/dist/storage/symbols-repo.js +47 -19
- package/dist/symbols/symbol-extractor.js +1 -1
- package/dist/tool-guidance.d.ts +2 -1
- package/dist/tool-guidance.js +26 -1
- package/dist/tool-schemas.d.ts +4 -4
- package/dist/tool-schemas.js +91 -91
- package/dist/warning-details.d.ts +12 -0
- package/dist/warning-details.js +17 -0
- package/dist/workspace-mapping-service.js +9 -0
- package/package.json +1 -1
package/dist/source-service.js
CHANGED
|
@@ -141,7 +141,15 @@ export class SourceService {
|
|
|
141
141
|
}
|
|
142
142
|
async checkSymbolExists(input) {
|
|
143
143
|
const result = await this.mappingService.checkSymbolExists(input);
|
|
144
|
-
|
|
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
|
|
94
|
-
private
|
|
93
|
+
private handleWorkerProcessError;
|
|
94
|
+
private handleWorkerExit;
|
|
95
95
|
private handleWorkerMessage;
|
|
96
96
|
private applyStageUpdate;
|
|
97
97
|
private handleWorkerReady;
|
package/dist/stdio-supervisor.js
CHANGED
|
@@ -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
|
|
465
|
+
handleWorkerProcessError(child, error) {
|
|
459
466
|
log("error", "supervisor.worker_process_error", { message: error.message });
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
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.
|
|
639
|
-
child.
|
|
658
|
+
child.removeAllListeners("error");
|
|
659
|
+
child.removeAllListeners("exit");
|
|
640
660
|
this.child = undefined;
|
|
641
661
|
this.childReady = false;
|
|
642
662
|
}
|
package/dist/storage/db.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
178
|
-
: this.listByArtifactStmt.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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]+)?";
|
package/dist/tool-guidance.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export type ToolMeta = {
|
|
|
6
6
|
requestId: string;
|
|
7
7
|
tool: string;
|
|
8
8
|
durationMs: number;
|
|
9
|
-
warnings
|
|
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";
|
package/dist/tool-guidance.js
CHANGED
|
@@ -317,7 +317,22 @@ export function buildValidateMixinSuggestedParams(normalizedInput) {
|
|
|
317
317
|
version
|
|
318
318
|
};
|
|
319
319
|
}
|
|
320
|
-
|
|
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 ?? {
|
package/dist/tool-schemas.d.ts
CHANGED
|
@@ -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
|
|
215
|
-
export declare const SOURCE_LOOKUP_TARGET_DESCRIPTION = "
|
|
216
|
-
export declare const SOURCE_SCOPE_DESCRIPTION = "vanilla = Mojang client jar only; merged =
|
|
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):
|
|
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>>;
|