@adhisang/minecraft-modding-mcp 4.2.1 → 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 +69 -0
- package/README.md +5 -4
- package/dist/access-transformer-parser.d.ts +2 -1
- package/dist/access-transformer-parser.js +10 -1
- package/dist/access-widener-parser.d.ts +1 -0
- package/dist/access-widener-parser.js +6 -3
- package/dist/build-suggested-call.js +31 -10
- package/dist/cache-registry.d.ts +8 -0
- package/dist/cache-registry.js +50 -32
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/decompiler/vineflower.js +8 -2
- package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
- package/dist/entry-tools/analyze-mod-service.js +2 -1
- package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
- package/dist/entry-tools/analyze-symbol-service.js +139 -34
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-members-service.js +6 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-source-service.js +5 -6
- package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
- package/dist/entry-tools/batch-mappings-service.js +4 -5
- package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
- package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
- package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
- package/dist/entry-tools/compare-minecraft-service.js +1 -1
- package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +4 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -2
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +3 -2
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
- package/dist/entry-tools/inspect-minecraft/internal.js +32 -14
- package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/response-contract.d.ts +3 -1
- package/dist/entry-tools/response-contract.js +3 -2
- package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
- package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
- package/dist/entry-tools/validate-project-service.d.ts +42 -36
- package/dist/entry-tools/validate-project-service.js +3 -1
- package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
- package/dist/entry-tools/verify-mixin-target-service.js +120 -29
- package/dist/error-mapping.d.ts +51 -0
- package/dist/error-mapping.js +157 -2
- package/dist/index.js +147 -76
- 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 +24 -0
- package/dist/mapping/loaders/tiny-maven.js +46 -20
- package/dist/mapping/lookup.d.ts +13 -2
- package/dist/mapping/lookup.js +60 -2
- package/dist/mapping/types.d.ts +6 -0
- package/dist/mapping-service.d.ts +26 -2
- package/dist/mapping-service.js +212 -55
- package/dist/mcp-helpers.d.ts +1 -0
- package/dist/mcp-helpers.js +25 -3
- package/dist/minecraft-explorer-service.js +24 -11
- package/dist/mixin/access-validators.js +33 -1
- package/dist/mixin/annotation-validators.js +73 -9
- package/dist/mixin/parsed-validator.js +3 -2
- package/dist/mixin-parser.js +119 -7
- package/dist/mod-decompile-service.d.ts +2 -0
- package/dist/mod-decompile-service.js +21 -3
- package/dist/mod-remap-service.d.ts +20 -1
- package/dist/mod-remap-service.js +101 -45
- package/dist/mojang-tiny-mapping-service.js +5 -2
- package/dist/nbt/java-nbt-codec.js +46 -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/observability.d.ts +6 -0
- package/dist/observability.js +10 -0
- package/dist/page-cursor.d.ts +17 -0
- package/dist/page-cursor.js +0 -0
- package/dist/path-converter.js +10 -3
- package/dist/registry-service.d.ts +2 -0
- package/dist/registry-service.js +41 -2
- package/dist/repo-downloader.js +4 -3
- package/dist/resources.js +45 -9
- package/dist/response-utils.d.ts +29 -4
- package/dist/response-utils.js +95 -5
- package/dist/source/access-validate.js +12 -2
- package/dist/source/artifact-resolver.js +2 -2
- package/dist/source/class-source/members-builder.d.ts +44 -1
- package/dist/source/class-source/members-builder.js +51 -4
- package/dist/source/class-source/snippet-builder.d.ts +13 -0
- package/dist/source/class-source/snippet-builder.js +54 -4
- package/dist/source/class-source-helpers.d.ts +4 -4
- package/dist/source/class-source-helpers.js +77 -20
- package/dist/source/class-source.d.ts +19 -0
- package/dist/source/class-source.js +144 -40
- package/dist/source/file-access.js +3 -2
- package/dist/source/indexer.js +29 -9
- package/dist/source/lifecycle/mapping-helpers.js +28 -3
- package/dist/source/lifecycle/runtime-check.js +20 -6
- package/dist/source/search.d.ts +1 -1
- package/dist/source/search.js +117 -30
- package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
- package/dist/source/validate-mixin.d.ts +2 -1
- package/dist/source/validate-mixin.js +45 -4
- package/dist/source/workspace-target.js +17 -10
- package/dist/source-jar-reader.d.ts +54 -1
- package/dist/source-jar-reader.js +121 -10
- package/dist/source-resolver.js +2 -2
- package/dist/source-service.d.ts +28 -7
- 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 +15 -0
- package/dist/storage/files-repo.js +67 -37
- package/dist/storage/symbols-repo.d.ts +15 -0
- package/dist/storage/symbols-repo.js +53 -19
- package/dist/symbols/symbol-extractor.js +35 -6
- package/dist/text-truncate.d.ts +7 -0
- package/dist/text-truncate.js +21 -0
- package/dist/tiny-remapper-resolver.js +10 -0
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +12 -1
- package/dist/tool-guidance.d.ts +6 -2
- package/dist/tool-guidance.js +63 -13
- package/dist/tool-schemas.d.ts +561 -496
- package/dist/tool-schemas.js +175 -139
- package/dist/types.d.ts +1 -0
- package/dist/version-diff-service.js +5 -0
- package/dist/version-service.d.ts +3 -0
- package/dist/version-service.js +18 -2
- package/dist/warning-details.d.ts +30 -0
- package/dist/warning-details.js +80 -0
- package/dist/workspace-mapping-service.js +9 -0
- package/docs/README-ja.md +2 -2
- package/docs/examples.md +26 -2
- package/docs/tool-reference.md +22 -17
- package/package.json +1 -1
|
@@ -3,6 +3,15 @@ import { createError, ERROR_CODES } from "./errors.js";
|
|
|
3
3
|
import { log } from "./logger.js";
|
|
4
4
|
import { isSecureJarEntryPath } from "./path-resolver.js";
|
|
5
5
|
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
6
|
+
// Test-only instrumentation: counts real jar opens at the single openZipFile
|
|
7
|
+
// chokepoint so perf tests can assert N-opens-per-call collapses to 1.
|
|
8
|
+
let zipOpenCount = 0;
|
|
9
|
+
export function __getZipOpenCount() {
|
|
10
|
+
return zipOpenCount;
|
|
11
|
+
}
|
|
12
|
+
export function __resetZipOpenCount() {
|
|
13
|
+
zipOpenCount = 0;
|
|
14
|
+
}
|
|
6
15
|
function toErrorMessage(value) {
|
|
7
16
|
if (value instanceof Error) {
|
|
8
17
|
return value.message;
|
|
@@ -23,7 +32,7 @@ export function hasJavaSourceExtension(entryPath) {
|
|
|
23
32
|
}
|
|
24
33
|
return true;
|
|
25
34
|
}
|
|
26
|
-
function openZipFile(jarPath) {
|
|
35
|
+
export function openZipFile(jarPath) {
|
|
27
36
|
return new Promise((resolve, reject) => {
|
|
28
37
|
yauzl.open(jarPath, {
|
|
29
38
|
lazyEntries: true,
|
|
@@ -33,6 +42,7 @@ function openZipFile(jarPath) {
|
|
|
33
42
|
reject(new Error(`Failed to read jar "${jarPath}": ${toErrorMessage(error)}`));
|
|
34
43
|
return;
|
|
35
44
|
}
|
|
45
|
+
zipOpenCount += 1;
|
|
36
46
|
resolve(zipFile);
|
|
37
47
|
});
|
|
38
48
|
});
|
|
@@ -193,6 +203,66 @@ export async function readJarEntryAsBuffer(jarPath, entryPath) {
|
|
|
193
203
|
}
|
|
194
204
|
});
|
|
195
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Opens a jar ONCE, drains its central directory into a name->entry index, and
|
|
208
|
+
* serves repeated entry reads via O(1) lookup + openReadStream. Use this when a
|
|
209
|
+
* single logical operation reads many entries from the same jar (e.g. walking a
|
|
210
|
+
* class hierarchy) instead of calling {@link readJarEntryAsBuffer} per entry,
|
|
211
|
+
* which re-opens and re-scans the jar each time. Callers MUST close() the reader
|
|
212
|
+
* (in a finally) to release the file descriptor. getEntryBuffer throws the same
|
|
213
|
+
* INVALID_INPUT (unsafe path) / SOURCE_NOT_FOUND (missing entry) errors as
|
|
214
|
+
* readJarEntryAsBuffer; duplicate entry names resolve to the first occurrence.
|
|
215
|
+
*/
|
|
216
|
+
export async function createJarEntryReader(jarPath) {
|
|
217
|
+
const zipFile = await openZipFile(jarPath);
|
|
218
|
+
let closed = false;
|
|
219
|
+
const index = new Map();
|
|
220
|
+
try {
|
|
221
|
+
while (true) {
|
|
222
|
+
const entry = await readNextEntry(zipFile);
|
|
223
|
+
if (!entry) {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
if (!isSecureJarEntryPath(entry.fileName)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (!index.has(entry.fileName)) {
|
|
230
|
+
index.set(entry.fileName, entry);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
zipFile.close();
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
async getEntryBuffer(entryPath) {
|
|
240
|
+
const normalizedTargetPath = entryPath.replaceAll("\\", "/");
|
|
241
|
+
if (!isSecureJarEntryPath(normalizedTargetPath)) {
|
|
242
|
+
throw createError({
|
|
243
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
244
|
+
message: `Entry path "${normalizedTargetPath}" is not allowed.`,
|
|
245
|
+
details: { jarPath, entryPath: normalizedTargetPath }
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const entry = index.get(normalizedTargetPath);
|
|
249
|
+
if (!entry) {
|
|
250
|
+
throw createError({
|
|
251
|
+
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
252
|
+
message: `Entry "${normalizedTargetPath}" was not found in "${jarPath}".`,
|
|
253
|
+
details: { jarPath, entryPath: normalizedTargetPath }
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return readEntryStream(zipFile, entry, jarPath);
|
|
257
|
+
},
|
|
258
|
+
close() {
|
|
259
|
+
if (!closed) {
|
|
260
|
+
closed = true;
|
|
261
|
+
zipFile.close();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
196
266
|
export async function collectMatchedJarEntriesAsUtf8(jarPath, predicate, options = {}) {
|
|
197
267
|
return withZipFile(jarPath, async (zipFile) => {
|
|
198
268
|
const entries = [];
|
|
@@ -226,6 +296,48 @@ export async function collectMatchedJarEntriesAsUtf8(jarPath, predicate, options
|
|
|
226
296
|
}
|
|
227
297
|
});
|
|
228
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Raw-buffer counterpart of {@link collectMatchedJarEntriesAsUtf8}: opens the jar
|
|
301
|
+
* ONCE and returns the matched entries as raw Buffers (no UTF-8 decode), so binary
|
|
302
|
+
* inputs like `.class` bytes survive intact. Used to avoid re-opening the jar per
|
|
303
|
+
* sampled entry. Mirrors the sibling's maxBytes/maxEntries/continueOnError semantics
|
|
304
|
+
* and its finally-close, but takes an injectable open seam for open-count assertions.
|
|
305
|
+
*/
|
|
306
|
+
export async function collectMatchedJarEntriesAsBuffers(jarPath, predicate, options = {}, deps = {}) {
|
|
307
|
+
const open = deps.openZipFile ?? openZipFile;
|
|
308
|
+
const zipFile = await open(jarPath);
|
|
309
|
+
try {
|
|
310
|
+
const entries = [];
|
|
311
|
+
const maxEntries = options.maxEntries == null ? undefined : Math.max(1, Math.trunc(options.maxEntries));
|
|
312
|
+
while (true) {
|
|
313
|
+
const entry = await readNextEntry(zipFile);
|
|
314
|
+
if (!entry) {
|
|
315
|
+
return entries;
|
|
316
|
+
}
|
|
317
|
+
if (!isSecureJarEntryPath(entry.fileName)) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!predicate(entry.fileName)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const contentBuffer = await readEntryStream(zipFile, entry, jarPath, options.maxBytes);
|
|
325
|
+
entries.push({ filePath: entry.fileName, content: contentBuffer });
|
|
326
|
+
if (maxEntries != null && entries.length >= maxEntries) {
|
|
327
|
+
return entries;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (!options.continueOnError) {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
zipFile.close();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
229
341
|
export async function* iterateJavaEntriesAsUtf8(jarPath, maxBytes) {
|
|
230
342
|
const zipFile = await openZipFile(jarPath);
|
|
231
343
|
try {
|
|
@@ -285,7 +397,7 @@ function countMatches(input, pattern) {
|
|
|
285
397
|
}
|
|
286
398
|
return count;
|
|
287
399
|
}
|
|
288
|
-
export async function detectFabricLikeInputNamespace(inputJar) {
|
|
400
|
+
export async function detectFabricLikeInputNamespace(inputJar, deps = {}) {
|
|
289
401
|
const warnings = [];
|
|
290
402
|
const classEntries = (await listJarEntries(inputJar))
|
|
291
403
|
.filter((entry) => entry.endsWith(".class"))
|
|
@@ -297,16 +409,15 @@ export async function detectFabricLikeInputNamespace(inputJar) {
|
|
|
297
409
|
warnings
|
|
298
410
|
};
|
|
299
411
|
}
|
|
412
|
+
// Read all sampled class entries in a SINGLE jar open instead of re-opening the
|
|
413
|
+
// jar once per entry. The sample set and latin1 decode are unchanged, so scores —
|
|
414
|
+
// and therefore the detected namespace — are identical.
|
|
415
|
+
const sampleSet = new Set(classEntries);
|
|
416
|
+
const matched = await collectMatchedJarEntriesAsBuffers(inputJar, (name) => sampleSet.has(name), { continueOnError: true }, deps);
|
|
300
417
|
let mojangScore = 0;
|
|
301
418
|
let intermediaryScore = 0;
|
|
302
|
-
for (const
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
text = (await readJarEntryAsBuffer(inputJar, entry)).toString("latin1");
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
419
|
+
for (const { content } of matched) {
|
|
420
|
+
const text = content.toString("latin1");
|
|
310
421
|
mojangScore += countMatches(text, /net\/minecraft\/(?:advancements|client|commands|core|data|gametest|nbt|network|recipe|resources|server|sounds|stats|tags|util|world)\//g) * 3;
|
|
311
422
|
intermediaryScore += countMatches(text, /net\/minecraft\/class_\d+/g) * 3;
|
|
312
423
|
intermediaryScore += countMatches(text, /\b(?:method|field)_\d+\b/g);
|
package/dist/source-resolver.js
CHANGED
|
@@ -260,7 +260,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
260
260
|
? false
|
|
261
261
|
: download.statusCode !== 404 && isTransientFailure(download.statusCode);
|
|
262
262
|
sawRemoteRepoFailure = sawRemoteRepoFailure || transient;
|
|
263
|
-
if (hasNextAttempt && transient) {
|
|
263
|
+
if (hasNextAttempt && (transient || download.ok)) {
|
|
264
264
|
options.onRepoFailover?.({
|
|
265
265
|
stage: "source",
|
|
266
266
|
repoUrl: sourceUrl,
|
|
@@ -320,7 +320,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
320
320
|
? false
|
|
321
321
|
: downloaded.statusCode !== 404 && isTransientFailure(downloaded.statusCode);
|
|
322
322
|
sawRemoteRepoFailure = sawRemoteRepoFailure || transient;
|
|
323
|
-
if (hasNextAttempt && transient) {
|
|
323
|
+
if (hasNextAttempt && (transient || downloaded.ok)) {
|
|
324
324
|
options.onRepoFailover?.({
|
|
325
325
|
stage: "binary",
|
|
326
326
|
repoUrl: binaryUrl,
|
package/dist/source-service.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as indexer from "./source/indexer.js";
|
|
|
11
11
|
import * as workspaceTarget from "./source/workspace-target.js";
|
|
12
12
|
import * as validateMixinModule from "./source/validate-mixin.js";
|
|
13
13
|
import * as artifactResolver from "./source/artifact-resolver.js";
|
|
14
|
+
import type { WireMembersBlock } from "./source/class-source/members-builder.js";
|
|
14
15
|
import { type StageEmitter } from "./stage-emitter.js";
|
|
15
16
|
import { WorkspaceMappingService, type WorkspaceCompileMappingOutput } from "./workspace-mapping-service.js";
|
|
16
17
|
import { type WorkspaceContextCache } from "./workspace-context-cache.js";
|
|
@@ -30,7 +31,8 @@ export type ResolveArtifactInput = {
|
|
|
30
31
|
scope?: ArtifactScope;
|
|
31
32
|
preferProjectVersion?: boolean;
|
|
32
33
|
strictVersion?: boolean;
|
|
33
|
-
|
|
34
|
+
/** When true, compute the optional `sampleEntries` listing (detail=full / include=samples). */
|
|
35
|
+
includeSampleEntries?: boolean;
|
|
34
36
|
};
|
|
35
37
|
export type ResolveArtifactOutput = {
|
|
36
38
|
artifactId: string;
|
|
@@ -112,6 +114,8 @@ export type SearchClassSourceInput = {
|
|
|
112
114
|
export type SearchClassSourceOutput = {
|
|
113
115
|
hits: SearchSourceHit[];
|
|
114
116
|
nextCursor?: string;
|
|
117
|
+
/** True when a provided cursor was malformed or belonged to a different query and was ignored (results restart from page one). */
|
|
118
|
+
cursorIgnored?: boolean;
|
|
115
119
|
mappingApplied: SourceMapping;
|
|
116
120
|
returnedNamespace: SourceMapping;
|
|
117
121
|
artifactContents: ArtifactContentsSummary;
|
|
@@ -205,6 +209,10 @@ export type GetClassSourceOutput = {
|
|
|
205
209
|
};
|
|
206
210
|
truncated: boolean;
|
|
207
211
|
charsTruncated?: boolean;
|
|
212
|
+
/** True when startLine begins past EOF; the returned range is empty (not clamped). */
|
|
213
|
+
outOfRange?: boolean;
|
|
214
|
+
/** First not-yet-returned line when truncated; pass back as startLine to continue. */
|
|
215
|
+
nextStartLine?: number;
|
|
208
216
|
origin: ResolvedSourceArtifact["origin"];
|
|
209
217
|
artifactId: string;
|
|
210
218
|
requestedMapping: SourceMapping;
|
|
@@ -213,6 +221,11 @@ export type GetClassSourceOutput = {
|
|
|
213
221
|
provenance: ArtifactProvenance;
|
|
214
222
|
qualityFlags: string[];
|
|
215
223
|
artifactContents: ArtifactContentsSummary;
|
|
224
|
+
/** Replayable get-class-source call that reads from nextStartLine (truncated, non-file output only). */
|
|
225
|
+
suggestedCall?: {
|
|
226
|
+
tool: string;
|
|
227
|
+
params: Record<string, unknown>;
|
|
228
|
+
};
|
|
216
229
|
outputFile?: string;
|
|
217
230
|
warnings: string[];
|
|
218
231
|
};
|
|
@@ -245,11 +258,14 @@ export type GetClassMembersInput = {
|
|
|
245
258
|
includeInherited?: boolean;
|
|
246
259
|
memberPattern?: string;
|
|
247
260
|
maxMembers?: number;
|
|
261
|
+
cursor?: string;
|
|
248
262
|
projectPath?: string;
|
|
249
263
|
gradleUserHome?: string;
|
|
250
264
|
scope?: ArtifactScope;
|
|
251
265
|
preferProjectVersion?: boolean;
|
|
252
266
|
strictVersion?: boolean;
|
|
267
|
+
/** When true, keep jvmDescriptor on FIELD members (always kept on methods/constructors). */
|
|
268
|
+
includeDescriptors?: boolean;
|
|
253
269
|
};
|
|
254
270
|
export type DecompiledMember = {
|
|
255
271
|
name: string;
|
|
@@ -265,11 +281,7 @@ export type DecompiledFallback = {
|
|
|
265
281
|
export type GetClassMembersStatus = "ok" | "members_unavailable" | "partial";
|
|
266
282
|
export type GetClassMembersOutput = {
|
|
267
283
|
className: string;
|
|
268
|
-
members:
|
|
269
|
-
constructors: SignatureMember[];
|
|
270
|
-
fields: SignatureMember[];
|
|
271
|
-
methods: SignatureMember[];
|
|
272
|
-
};
|
|
284
|
+
members: WireMembersBlock;
|
|
273
285
|
counts: {
|
|
274
286
|
constructors: number;
|
|
275
287
|
fields: number;
|
|
@@ -277,6 +289,10 @@ export type GetClassMembersOutput = {
|
|
|
277
289
|
total: number;
|
|
278
290
|
};
|
|
279
291
|
truncated: boolean;
|
|
292
|
+
/** Continuation cursor when more members remain; pass back as cursor to fetch the next page. */
|
|
293
|
+
nextCursor?: string;
|
|
294
|
+
/** True when a provided cursor was malformed or belonged to a different query and was ignored. */
|
|
295
|
+
cursorIgnored?: boolean;
|
|
280
296
|
context: ExplorerResponseContext;
|
|
281
297
|
origin: ResolvedSourceArtifact["origin"];
|
|
282
298
|
artifactId: string;
|
|
@@ -420,7 +436,12 @@ export type ValidateMixinInput = {
|
|
|
420
436
|
path: string;
|
|
421
437
|
};
|
|
422
438
|
sourceRoots?: string[];
|
|
423
|
-
|
|
439
|
+
/**
|
|
440
|
+
* Optional when input.mode='project', or preferProjectVersion=true with a
|
|
441
|
+
* projectPath set: the dispatcher detects the Minecraft version from
|
|
442
|
+
* gradle.properties before any version-dependent stage runs.
|
|
443
|
+
*/
|
|
444
|
+
version?: string;
|
|
424
445
|
mapping?: SourceMapping;
|
|
425
446
|
sourcePriority?: MappingSourcePriority;
|
|
426
447
|
scope?: ArtifactScope;
|
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,
|
|
@@ -47,10 +47,14 @@ export declare class FilesRepo {
|
|
|
47
47
|
private readonly listStmt;
|
|
48
48
|
private readonly listRowsStmt;
|
|
49
49
|
private readonly searchPathStmt;
|
|
50
|
+
private readonly searchContentLikePathsStmt;
|
|
50
51
|
private readonly searchFtsStmt;
|
|
51
52
|
private readonly getByPathsStmtCache;
|
|
52
53
|
private readonly classLookupStmtCache;
|
|
54
|
+
private readonly dynamicStmtCache;
|
|
55
|
+
private static readonly DYNAMIC_STMT_CACHE_MAX;
|
|
53
56
|
constructor(db: SqliteDatabase);
|
|
57
|
+
private prepareCached;
|
|
54
58
|
clearFilesForArtifact(artifactId: string): void;
|
|
55
59
|
insertFilesForArtifact(artifactId: string, files: IndexedFile[]): void;
|
|
56
60
|
replaceFilesForArtifact(artifactId: string, files: IndexedFile[]): void;
|
|
@@ -58,6 +62,17 @@ export declare class FilesRepo {
|
|
|
58
62
|
getFileContent(artifactId: string, filePath: string): FileRow | undefined;
|
|
59
63
|
listFiles(artifactId: string, options: ListFilesOptions): PagedResult<string>;
|
|
60
64
|
listFileRows(artifactId: string, options: ListFileRowsOptions): PagedResult<FileRow>;
|
|
65
|
+
/**
|
|
66
|
+
* Narrow the literal/contains text-scan candidate set without hydrating content:
|
|
67
|
+
* returns the file_paths whose content matches `content LIKE %needle%` (ASCII
|
|
68
|
+
* case-insensitive). The result is a SUPERSET of the JS contains/exact match set
|
|
69
|
+
* for an ASCII needle, so callers MUST still run the exact JS post-verify. Only
|
|
70
|
+
* file_path is selected — content stays in the DB until getFileContentsByPaths.
|
|
71
|
+
*/
|
|
72
|
+
searchContentLikeCandidatePaths(artifactId: string, needle: string, limit: number): {
|
|
73
|
+
filePaths: string[];
|
|
74
|
+
scannedRows: number;
|
|
75
|
+
};
|
|
61
76
|
getFileContentsByPaths(artifactId: string, filePaths: string[]): FileRow[];
|
|
62
77
|
searchFileCandidates(artifactId: string, options: SearchFilesOptions): PagedResult<SearchFileCandidateResult> & {
|
|
63
78
|
scannedRows: number;
|