@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
|
@@ -25,6 +25,22 @@ function looksLikeClassSegment(name) {
|
|
|
25
25
|
const trimmed = name.trim();
|
|
26
26
|
return /^[A-Z_$]/.test(trimmed);
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Collapse N per-member remap failures into a single warning line. Preserves the
|
|
30
|
+
* original "Could not remap" / "Remap failed for" prefix so downstream classifiers
|
|
31
|
+
* (e.g. REMAP_WARNING_RE in validate-mixin, warningDetails coding) still recognise it,
|
|
32
|
+
* while bounding the per-member flood to one line plus a short sample of names.
|
|
33
|
+
*/
|
|
34
|
+
function pushAggregatedRemapWarning(warnings, prefix, names, kind, sourceMapping, targetMapping) {
|
|
35
|
+
if (names.length === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const unique = [...new Set(names)];
|
|
39
|
+
const sample = unique.slice(0, 3).join(", ");
|
|
40
|
+
const more = unique.length > 3 ? `, +${unique.length - 3} more` : "";
|
|
41
|
+
const plural = names.length === 1 ? "" : "s";
|
|
42
|
+
warnings.push(`${prefix} ${names.length} ${kind}${plural} from ${sourceMapping} to ${targetMapping} (${sample}${more}).`);
|
|
43
|
+
}
|
|
28
44
|
export function rejectLifecycleClassLikeInput(svc, input) {
|
|
29
45
|
void svc;
|
|
30
46
|
if (!looksLikeClassSegment(input.methodName)) {
|
|
@@ -234,6 +250,13 @@ export async function remapSignatureMembers(svc, members, kind, version, sourceM
|
|
|
234
250
|
"resolveMethodMappingExact" in svc.mappingService &&
|
|
235
251
|
typeof svc.mappingService.resolveMethodMappingExact === "function";
|
|
236
252
|
const memberEntries = [...memberKeyToRemapped.entries()];
|
|
253
|
+
// Collect per-member remap failures and emit a SINGLE aggregated warning each
|
|
254
|
+
// after the loop instead of one line per member. On namespace mismatches a large
|
|
255
|
+
// class can fail to remap every one of its ~150 members, which previously flooded
|
|
256
|
+
// both warnings[] and the 1:1 warningDetails[] with hundreds of near-identical
|
|
257
|
+
// lines (the dominant token sink for these responses).
|
|
258
|
+
const unremappedNames = [];
|
|
259
|
+
const remapErrorNames = [];
|
|
237
260
|
await Promise.all(memberEntries.map(async ([key, _sourceName]) => {
|
|
238
261
|
const [ownerFqn, name, descriptor] = key.split("\0");
|
|
239
262
|
try {
|
|
@@ -299,20 +322,22 @@ export async function remapSignatureMembers(svc, members, kind, version, sourceM
|
|
|
299
322
|
}
|
|
300
323
|
}
|
|
301
324
|
else {
|
|
302
|
-
|
|
325
|
+
unremappedNames.push(name);
|
|
303
326
|
failedNames.add(name);
|
|
304
327
|
}
|
|
305
328
|
}
|
|
306
329
|
else {
|
|
307
|
-
|
|
330
|
+
unremappedNames.push(name);
|
|
308
331
|
failedNames.add(name);
|
|
309
332
|
}
|
|
310
333
|
}
|
|
311
334
|
catch {
|
|
312
|
-
|
|
335
|
+
remapErrorNames.push(name);
|
|
313
336
|
failedNames.add(name);
|
|
314
337
|
}
|
|
315
338
|
}));
|
|
339
|
+
pushAggregatedRemapWarning(warnings, "Could not remap", unremappedNames, kind, sourceMapping, targetMapping);
|
|
340
|
+
pushAggregatedRemapWarning(warnings, "Remap failed for", remapErrorNames, kind, sourceMapping, targetMapping);
|
|
316
341
|
const isField = kind === "field";
|
|
317
342
|
return {
|
|
318
343
|
members: members.map((member) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ERROR_CODES, isAppError } from "../../errors.js";
|
|
1
2
|
export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbackBase) {
|
|
2
3
|
const version = input.version.trim();
|
|
3
4
|
const name = input.name.trim();
|
|
@@ -50,16 +51,25 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
50
51
|
signature = await svc.explorerService.getSignature({
|
|
51
52
|
fqn: targetClass,
|
|
52
53
|
jarPath,
|
|
53
|
-
access: "all"
|
|
54
|
+
access: "all",
|
|
55
|
+
// Method existence is asked relative to the owner type, so inherited methods count:
|
|
56
|
+
// `level.getGameTime()` exists on Level even though it is declared on a supertype.
|
|
57
|
+
// Scoped to methods only: fields already resolved correctly without an inheritance
|
|
58
|
+
// walk, and including inherited fields would let a shadowed field name match twice
|
|
59
|
+
// and flip a previously-resolved field to "ambiguous".
|
|
60
|
+
includeInherited: input.kind === "method"
|
|
54
61
|
});
|
|
55
62
|
}
|
|
56
|
-
catch {
|
|
63
|
+
catch (error) {
|
|
64
|
+
const classMissing = isAppError(error) && error.code === ERROR_CODES.CLASS_NOT_FOUND;
|
|
57
65
|
return {
|
|
58
66
|
...fallbackBase,
|
|
59
67
|
querySymbol,
|
|
60
68
|
warnings: [
|
|
61
69
|
...fallbackBase.warnings,
|
|
62
|
-
|
|
70
|
+
classMissing
|
|
71
|
+
? `Class "${targetClass}" was not found in the Minecraft ${version} runtime jar; it does not exist (or is not in this jar).`
|
|
72
|
+
: `Version ${version} is unobfuscated; runtime bytecode lookup could not load class "${targetClass}".`
|
|
63
73
|
]
|
|
64
74
|
};
|
|
65
75
|
}
|
|
@@ -114,9 +124,13 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
114
124
|
});
|
|
115
125
|
}
|
|
116
126
|
const methodCandidates = signature.methods.filter((method) => method.name === name);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
const signatureMode = input.signatureMode ?? "name-only";
|
|
128
|
+
if (signatureMode === "name-only") {
|
|
129
|
+
// Existence semantics: any overload with this name means the method exists. Multiple
|
|
130
|
+
// overloads are not "ambiguous" for a name-only existence check — only zero matches
|
|
131
|
+
// is not_found. (Use signatureMode: "exact" with a descriptor to pin one overload.)
|
|
132
|
+
if (methodCandidates.length === 0) {
|
|
133
|
+
return buildUnresolved("not_found");
|
|
120
134
|
}
|
|
121
135
|
return buildResolved({
|
|
122
136
|
kind: "method",
|
package/dist/source/search.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare function searchClassSource(svc: SourceService, input: SearchClass
|
|
|
44
44
|
export declare function searchSymbolIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
45
45
|
export declare function searchTextIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
46
46
|
export declare function searchPathIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
47
|
-
export declare function searchTextIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
47
|
+
export declare function searchTextIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void, onWarning?: (warning: string) => void): void;
|
|
48
48
|
export declare function searchPathIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
49
49
|
export declare function findSymbolHits(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined): IndexedSymbolHit[];
|
|
50
50
|
export declare function indexedCandidateLimit(svc: SourceService): number;
|
package/dist/source/search.js
CHANGED
|
@@ -65,11 +65,13 @@ export function buildGlobRegex(pattern) {
|
|
|
65
65
|
while (i < pattern.length) {
|
|
66
66
|
const ch = pattern[i];
|
|
67
67
|
if (ch === "*" && pattern[i + 1] === "*") {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
if (pattern[i + 2] === "/") {
|
|
69
|
+
result += "(?:.*/)?";
|
|
70
|
+
i += 3;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
result += ".*";
|
|
74
|
+
i += 2;
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
else if (ch === "*") {
|
|
@@ -182,6 +184,11 @@ export function findContentMatchIndex(content, query, match, pattern) {
|
|
|
182
184
|
}
|
|
183
185
|
const normalizedContent = toLower(content);
|
|
184
186
|
const normalizedQuery = toLower(query);
|
|
187
|
+
// Both "contains" and "prefix" map to a substring search here. For free-text
|
|
188
|
+
// content, `prefix` is intentionally treated as "contains": file bodies are
|
|
189
|
+
// searched grep-style and the default match for intent=text resolves to
|
|
190
|
+
// "prefix" (see normalizeMatch), so anchoring at the start of the file would
|
|
191
|
+
// yield almost no hits. Symbol/path prefix anchoring lives in findMatchIndex.
|
|
185
192
|
return normalizedContent.indexOf(normalizedQuery);
|
|
186
193
|
}
|
|
187
194
|
export function scoreSymbolMatch(match, index, symbolKind) {
|
|
@@ -296,6 +303,9 @@ export async function searchClassSource(svc, input) {
|
|
|
296
303
|
searchWarnings.push(`queryNamespace=${input.queryNamespace}: translation failed (${caughtError instanceof Error ? caughtError.message : String(caughtError)}); running literal search instead.`);
|
|
297
304
|
}
|
|
298
305
|
}
|
|
306
|
+
else if (intent === "symbol") {
|
|
307
|
+
searchWarnings.push(`queryNamespace=${input.queryNamespace} requires a fully-qualified class name for translation; running literal search in ${artifactMapping} instead. Resolve "${originalQuery}" to an FQCN with find-class or find-mapping first.`);
|
|
308
|
+
}
|
|
299
309
|
else if (intent === "text" || intent === "path") {
|
|
300
310
|
searchWarnings.push(`queryNamespace=${input.queryNamespace} has no effect when intent="${intent}" — ${intent} search is a literal match against the artifact's ${artifactMapping} index. Use intent="symbol" for namespace translation.`);
|
|
301
311
|
}
|
|
@@ -336,6 +346,10 @@ export async function searchClassSource(svc, input) {
|
|
|
336
346
|
});
|
|
337
347
|
const decodedCursor = decodeSearchCursor(input.cursor);
|
|
338
348
|
const cursor = decodedCursor?.contextKey === cursorContext ? decodedCursor : undefined;
|
|
349
|
+
// A provided cursor that did not decode, or whose context key does not match
|
|
350
|
+
// this query, is silently dropped and the scan restarts from page one. Flag
|
|
351
|
+
// it so callers do not assume they are continuing a previous page.
|
|
352
|
+
const cursorIgnored = input.cursor != null && cursor == null;
|
|
339
353
|
const accumulator = createSearchHitAccumulator(limit, cursor);
|
|
340
354
|
const indexedSearchEnabled = svc.config.indexedSearchEnabled !== false;
|
|
341
355
|
if (match === "regex") {
|
|
@@ -345,13 +359,16 @@ export async function searchClassSource(svc, input) {
|
|
|
345
359
|
const recordHit = (hit) => {
|
|
346
360
|
accumulator.add(hit);
|
|
347
361
|
};
|
|
362
|
+
const recordWarning = (warning) => {
|
|
363
|
+
searchWarnings.push(warning);
|
|
364
|
+
};
|
|
348
365
|
const tokenOnlyTextIntent = intent === "text" && queryMode === "token";
|
|
349
366
|
if (intent === "symbol") {
|
|
350
367
|
searchSymbolIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
351
368
|
}
|
|
352
369
|
else if (queryMode === "literal" && intent === "text") {
|
|
353
370
|
svc.metrics.recordSearchFallback();
|
|
354
|
-
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
371
|
+
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit, recordWarning);
|
|
355
372
|
}
|
|
356
373
|
else if (!indexedSearchEnabled) {
|
|
357
374
|
svc.metrics.recordIndexedDisabled();
|
|
@@ -361,7 +378,7 @@ export async function searchClassSource(svc, input) {
|
|
|
361
378
|
searchPathIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
362
379
|
}
|
|
363
380
|
else {
|
|
364
|
-
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
381
|
+
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit, recordWarning);
|
|
365
382
|
}
|
|
366
383
|
}
|
|
367
384
|
}
|
|
@@ -388,7 +405,7 @@ export async function searchClassSource(svc, input) {
|
|
|
388
405
|
searchPathIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
389
406
|
}
|
|
390
407
|
else {
|
|
391
|
-
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
408
|
+
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit, recordWarning);
|
|
392
409
|
}
|
|
393
410
|
}
|
|
394
411
|
}
|
|
@@ -400,7 +417,7 @@ export async function searchClassSource(svc, input) {
|
|
|
400
417
|
searchPathIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
401
418
|
}
|
|
402
419
|
else {
|
|
403
|
-
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
420
|
+
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit, recordWarning);
|
|
404
421
|
}
|
|
405
422
|
}
|
|
406
423
|
}
|
|
@@ -415,6 +432,7 @@ export async function searchClassSource(svc, input) {
|
|
|
415
432
|
return {
|
|
416
433
|
hits: page,
|
|
417
434
|
nextCursor,
|
|
435
|
+
...(cursorIgnored ? { cursorIgnored: true } : {}),
|
|
418
436
|
mappingApplied: artifact.mappingApplied ?? "obfuscated",
|
|
419
437
|
returnedNamespace: artifact.mappingApplied ?? "obfuscated",
|
|
420
438
|
artifactContents: svc.buildArtifactContentsSummary({
|
|
@@ -534,39 +552,98 @@ export function searchPathIntentIndexed(svc, artifactId, query, match, scope, on
|
|
|
534
552
|
});
|
|
535
553
|
}
|
|
536
554
|
}
|
|
537
|
-
|
|
538
|
-
|
|
555
|
+
const ASCII_RE = /^[\x00-\x7F]*$/;
|
|
556
|
+
/** True when every code unit is ASCII, so SQLite LIKE folds case identically to JS. */
|
|
557
|
+
function isAsciiNeedle(query) {
|
|
558
|
+
return ASCII_RE.test(query);
|
|
559
|
+
}
|
|
560
|
+
function searchScanBudgetWarning(byteBudget) {
|
|
561
|
+
return `search scan budget of ${byteBudget} bytes reached; results may be incomplete. Narrow the query or scope (packagePrefix / fileGlob) to scan fewer files.`;
|
|
562
|
+
}
|
|
563
|
+
export function searchTextIntent(svc, artifactId, query, match, scope, regexPattern, onHit, onWarning) {
|
|
539
564
|
const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
565
|
+
const byteBudget = Math.max(1, svc.config.searchScanMaxBytes ?? Number.MAX_SAFE_INTEGER);
|
|
566
|
+
let scannedBytes = 0;
|
|
567
|
+
let truncated = false;
|
|
568
|
+
const passesScope = (filePath) => checkPackagePrefix(filePath, scope?.packagePrefix) && (!glob || glob.test(filePath));
|
|
569
|
+
const emitIfMatch = (filePath, content) => {
|
|
570
|
+
const contentIndex = match === "regex"
|
|
571
|
+
? matchRegexIndex(content, regexPattern)
|
|
572
|
+
: findContentMatchIndex(content, query, match);
|
|
573
|
+
if (contentIndex < 0) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
onHit({
|
|
577
|
+
filePath,
|
|
578
|
+
score: scoreTextMatch(match, contentIndex),
|
|
579
|
+
matchedIn: "content",
|
|
580
|
+
reasonCodes: ["content_match", `text_${match}`]
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
// FAST PATH: an ASCII, non-regex needle narrows candidates via a content LIKE
|
|
584
|
+
// prefilter (a proven superset of the JS match), so only matching files are
|
|
585
|
+
// hydrated instead of scanning the whole corpus. The unchanged JS post-verify in
|
|
586
|
+
// emitIfMatch removes any over-included rows.
|
|
587
|
+
if (match !== "regex" && isAsciiNeedle(query)) {
|
|
588
|
+
const candidateLimit = indexedCandidateLimitForMatch(svc, match);
|
|
589
|
+
const { filePaths, scannedRows } = svc.filesRepo.searchContentLikeCandidatePaths(artifactId, query, candidateLimit);
|
|
590
|
+
svc.metrics.recordSearchDbRoundtrip();
|
|
591
|
+
svc.metrics.recordSearchRowsScanned(scannedRows);
|
|
592
|
+
// The prefilter caps candidates at candidateLimit ordered by file_path. Below the
|
|
593
|
+
// cap the candidate set is exhaustive, so the JS post-verify yields exactly the old
|
|
594
|
+
// full-scan hits. AT the cap more matches may exist beyond the path-ordered cut, and
|
|
595
|
+
// dropping them would change the scored top-K page and totalApprox/cursor — so fall
|
|
596
|
+
// through to the exhaustive paged scan below to preserve identical results.
|
|
597
|
+
if (scannedRows < candidateLimit) {
|
|
598
|
+
svc.metrics.recordSearchLikePrefilter();
|
|
599
|
+
const scopedPaths = filePaths.filter(passesScope);
|
|
600
|
+
const rows = svc.filesRepo.getFileContentsByPaths(artifactId, scopedPaths);
|
|
601
|
+
svc.metrics.recordSearchDbRoundtrip();
|
|
602
|
+
svc.metrics.recordSearchRowsScanned(rows.length);
|
|
603
|
+
for (const row of rows) {
|
|
604
|
+
if (scannedBytes >= byteBudget) {
|
|
605
|
+
truncated = true;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
scannedBytes += row.contentBytes;
|
|
609
|
+
emitIfMatch(row.filePath, row.content);
|
|
610
|
+
}
|
|
611
|
+
if (truncated) {
|
|
612
|
+
svc.metrics.recordSearchScanTruncated();
|
|
613
|
+
onWarning?.(searchScanBudgetWarning(byteBudget));
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
// Cap overflow: fall through to the exhaustive scan (rare; only common-token queries).
|
|
618
|
+
}
|
|
619
|
+
// SLOW PATH: regex (cannot be pushed to SQL) or non-ASCII needles (SQLite LIKE
|
|
620
|
+
// would fold case differently than JS) scan the corpus page by page.
|
|
621
|
+
const pageSize = Math.max(1, svc.config.searchScanPageSize ?? 250);
|
|
540
622
|
let cursor = undefined;
|
|
541
|
-
while (true) {
|
|
623
|
+
outer: while (true) {
|
|
542
624
|
const page = svc.filesRepo.listFileRows(artifactId, { limit: pageSize, cursor });
|
|
543
625
|
svc.metrics.recordSearchDbRoundtrip();
|
|
544
626
|
svc.metrics.recordSearchRowsScanned(page.items.length);
|
|
545
627
|
for (const row of page.items) {
|
|
546
|
-
if (!
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
if (glob && !glob.test(row.filePath)) {
|
|
628
|
+
if (!passesScope(row.filePath)) {
|
|
550
629
|
continue;
|
|
551
630
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (contentIndex < 0) {
|
|
556
|
-
continue;
|
|
631
|
+
if (scannedBytes >= byteBudget) {
|
|
632
|
+
truncated = true;
|
|
633
|
+
break outer;
|
|
557
634
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
score: scoreTextMatch(match, contentIndex),
|
|
561
|
-
matchedIn: "content",
|
|
562
|
-
reasonCodes: ["content_match", `text_${match}`]
|
|
563
|
-
});
|
|
635
|
+
scannedBytes += row.contentBytes;
|
|
636
|
+
emitIfMatch(row.filePath, row.content);
|
|
564
637
|
}
|
|
565
638
|
if (!page.nextCursor) {
|
|
566
639
|
break;
|
|
567
640
|
}
|
|
568
641
|
cursor = page.nextCursor;
|
|
569
642
|
}
|
|
643
|
+
if (truncated) {
|
|
644
|
+
svc.metrics.recordSearchScanTruncated();
|
|
645
|
+
onWarning?.(searchScanBudgetWarning(byteBudget));
|
|
646
|
+
}
|
|
570
647
|
}
|
|
571
648
|
export function searchPathIntent(svc, artifactId, query, match, scope, regexPattern, onHit) {
|
|
572
649
|
const pageSize = Math.max(1, svc.config.searchScanPageSize ?? 250);
|
|
@@ -604,7 +681,9 @@ export function searchPathIntent(svc, artifactId, query, match, scope, regexPatt
|
|
|
604
681
|
}
|
|
605
682
|
export function findSymbolHits(svc, artifactId, query, match, scope, regexPattern) {
|
|
606
683
|
if (match !== "regex") {
|
|
607
|
-
const filePathLike = scope?.fileGlob
|
|
684
|
+
const filePathLike = scope?.fileGlob && !scope.fileGlob.includes("**")
|
|
685
|
+
? globToSqlLike(normalizePathStyle(scope.fileGlob))
|
|
686
|
+
: undefined;
|
|
608
687
|
const scoped = svc.symbolsRepo.findScopedSymbols({
|
|
609
688
|
artifactId,
|
|
610
689
|
query,
|
|
@@ -616,8 +695,12 @@ export function findSymbolHits(svc, artifactId, query, match, scope, regexPatter
|
|
|
616
695
|
});
|
|
617
696
|
svc.metrics.recordSearchDbRoundtrip();
|
|
618
697
|
svc.metrics.recordSearchRowsScanned(scoped.items.length);
|
|
698
|
+
const globFilter = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
619
699
|
const result = [];
|
|
620
700
|
for (const symbol of scoped.items) {
|
|
701
|
+
if (globFilter && !globFilter.test(symbol.filePath)) {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
621
704
|
if (!isSymbolKind(symbol.symbolKind)) {
|
|
622
705
|
continue;
|
|
623
706
|
}
|
|
@@ -633,12 +716,15 @@ export function findSymbolHits(svc, artifactId, query, match, scope, regexPatter
|
|
|
633
716
|
}
|
|
634
717
|
return result;
|
|
635
718
|
}
|
|
636
|
-
|
|
719
|
+
// Stream rows instead of materializing every symbol: regex scans on large
|
|
720
|
+
// artifacts only keep matching rows in memory this way.
|
|
721
|
+
const candidates = svc.symbolsRepo.iterateSymbolsForArtifact(artifactId, scope?.symbolKind);
|
|
637
722
|
svc.metrics.recordSearchDbRoundtrip();
|
|
638
|
-
|
|
723
|
+
let rowsScanned = 0;
|
|
639
724
|
const result = [];
|
|
640
725
|
const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
641
726
|
for (const symbol of candidates) {
|
|
727
|
+
rowsScanned += 1;
|
|
642
728
|
if (!checkPackagePrefix(symbol.filePath, scope?.packagePrefix)) {
|
|
643
729
|
continue;
|
|
644
730
|
}
|
|
@@ -660,6 +746,7 @@ export function findSymbolHits(svc, artifactId, query, match, scope, regexPatter
|
|
|
660
746
|
matchIndex: index
|
|
661
747
|
});
|
|
662
748
|
}
|
|
749
|
+
svc.metrics.recordSearchRowsScanned(rowsScanned);
|
|
663
750
|
return result;
|
|
664
751
|
}
|
|
665
752
|
export function indexedCandidateLimit(svc) {
|
|
@@ -40,10 +40,22 @@ export async function runResolveStage(svc, ctx) {
|
|
|
40
40
|
scope: ctx.input.scope,
|
|
41
41
|
preferProjectVersion: false
|
|
42
42
|
});
|
|
43
|
-
ctx.jarPath = ctx.resolvedArtifact.binaryJarPath ?? (await svc.versionService.resolveVersionJar(ctx.version)).jarPath;
|
|
44
43
|
ctx.warnings.push(...ctx.resolvedArtifact.warnings);
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
if (ctx.resolvedArtifact.binaryJarPath) {
|
|
45
|
+
ctx.jarPath = ctx.resolvedArtifact.binaryJarPath;
|
|
46
|
+
ctx.mappingApplied = ctx.resolvedArtifact.mappingApplied;
|
|
47
|
+
ctx.signatureLookupMapping = ctx.resolvedArtifact.mappingApplied;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
ctx.jarPath = (await svc.versionService.resolveVersionJar(ctx.version)).jarPath;
|
|
51
|
+
ctx.signatureLookupMapping = "obfuscated";
|
|
52
|
+
ctx.scopeFallback = {
|
|
53
|
+
requested: ctx.input.scope,
|
|
54
|
+
applied: "vanilla",
|
|
55
|
+
reason: "Resolved artifact had no binary jar (sources only); falling back to vanilla client jar."
|
|
56
|
+
};
|
|
57
|
+
ctx.warnings.push(`Scope "${ctx.input.scope}" resolved without a binary jar; falling back to vanilla client jar.`);
|
|
58
|
+
}
|
|
47
59
|
if (ctx.resolvedArtifact.version) {
|
|
48
60
|
ctx.version = ctx.resolvedArtifact.version;
|
|
49
61
|
}
|
|
@@ -59,6 +71,14 @@ export async function runResolveStage(svc, ctx) {
|
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
73
|
else {
|
|
74
|
+
if (ctx.input.scope && ctx.input.scope !== "vanilla") {
|
|
75
|
+
ctx.scopeFallback = {
|
|
76
|
+
requested: ctx.input.scope,
|
|
77
|
+
applied: "vanilla",
|
|
78
|
+
reason: "projectPath required for merged/loader scope; falling back to vanilla client jar."
|
|
79
|
+
};
|
|
80
|
+
ctx.warnings.push(`Scope "${ctx.input.scope}" requires projectPath; falling back to vanilla client jar.`);
|
|
81
|
+
}
|
|
62
82
|
ctx.jarPath = (await svc.versionService.resolveVersionJar(ctx.version)).jarPath;
|
|
63
83
|
}
|
|
64
84
|
if (ctx.jarPath.includes("-sources.jar")) {
|
|
@@ -4,7 +4,8 @@ import type { ValidateMixinInput, ValidateMixinOptions, ValidateMixinOutput } fr
|
|
|
4
4
|
import { type StageEmitter } from "../stage-emitter.js";
|
|
5
5
|
import type { MappingSourcePriority } from "../types.js";
|
|
6
6
|
import type { FindMappingOutput as MappingFindMappingOutput } from "../mapping-service.js";
|
|
7
|
-
export type ValidateMixinSingleInput = Omit<ValidateMixinInput, "input"> & {
|
|
7
|
+
export type ValidateMixinSingleInput = Omit<ValidateMixinInput, "input" | "version"> & {
|
|
8
|
+
version: string;
|
|
8
9
|
source?: string;
|
|
9
10
|
sourcePath?: string;
|
|
10
11
|
batchCaches?: {
|
|
@@ -159,7 +159,43 @@ export async function validateMixin(svc, input, options = {}) {
|
|
|
159
159
|
throw annotateValidateMixinError(err, "input-validation");
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the Minecraft version for a validate-mixin request. Uses the explicit
|
|
164
|
+
* `version` when present; otherwise detects it from gradle.properties for
|
|
165
|
+
* project mode or when preferProjectVersion is set with a projectPath. Throws an
|
|
166
|
+
* actionable INVALID_INPUT error when no version can be determined.
|
|
167
|
+
*/
|
|
168
|
+
async function resolveRequestedMinecraftVersion(svc, input) {
|
|
169
|
+
const explicit = input.version?.trim();
|
|
170
|
+
if (explicit) {
|
|
171
|
+
return explicit;
|
|
172
|
+
}
|
|
173
|
+
const projectPathCandidate = input.projectPath ?? (input.input.mode === "project" ? input.input.path : undefined);
|
|
174
|
+
if (projectPathCandidate) {
|
|
175
|
+
const normalizedProjectPath = resolveMixinInputPath(projectPathCandidate, "path");
|
|
176
|
+
const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
|
|
177
|
+
if (detected?.trim()) {
|
|
178
|
+
return detected.trim();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
throw createError({
|
|
182
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
183
|
+
message: "Could not detect a Minecraft version from the project. Pass version explicitly, or ensure gradle.properties declares minecraft_version.",
|
|
184
|
+
details: {
|
|
185
|
+
failedStage: "input-validation",
|
|
186
|
+
nextAction: 'Pass version (e.g. "1.21.10"), or set preferProjectVersion=true with a projectPath whose gradle.properties contains minecraft_version.'
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async function runValidateMixinDispatcher(svc, rawInput, options = {}) {
|
|
191
|
+
const requestedVersion = await resolveRequestedMinecraftVersion(svc, rawInput);
|
|
192
|
+
// Service-level default so direct/internal/entry-tool callers (not just the MCP
|
|
193
|
+
// wire boundary, where Zod applies it) get the lean summary-first report shape.
|
|
194
|
+
const input = {
|
|
195
|
+
...rawInput,
|
|
196
|
+
version: requestedVersion,
|
|
197
|
+
reportMode: rawInput.reportMode ?? "summary-first"
|
|
198
|
+
};
|
|
163
199
|
const { input: sourceInput, ...sharedInput } = input;
|
|
164
200
|
const mode = sourceInput.mode;
|
|
165
201
|
const stageEmitter = options.stageEmitter ?? NOOP_STAGE_EMITTER;
|
|
@@ -495,7 +531,9 @@ async function finalizeValidateMixinPipeline(svc, ctx) {
|
|
|
495
531
|
result.aggregatedWarnings = undefined;
|
|
496
532
|
result.toolHealth = undefined;
|
|
497
533
|
result.confidenceBreakdown = undefined;
|
|
498
|
-
|
|
534
|
+
// resolutionTrace only exists when explain=true; keep it then so explain still
|
|
535
|
+
// delivers the per-issue trace even under the compact report shape.
|
|
536
|
+
if (result.provenance && input.explain !== true) {
|
|
499
537
|
result.provenance.resolutionTrace = undefined;
|
|
500
538
|
}
|
|
501
539
|
}
|
|
@@ -676,6 +714,9 @@ function applyValidateMixinOutputCompaction(output, input) {
|
|
|
676
714
|
? true
|
|
677
715
|
: warningCandidates.every((entry) => sameStringArray(entry, warningCandidates[0]));
|
|
678
716
|
if (input.reportMode === "summary-first") {
|
|
717
|
+
// explain=true keeps the per-result heavy detail (resolvedMembers/toolHealth)
|
|
718
|
+
// even under summary-first, so the diagnostic opt-in still works.
|
|
719
|
+
const keepHeavyDetail = input.explain === true;
|
|
679
720
|
nextOutput = {
|
|
680
721
|
...nextOutput,
|
|
681
722
|
results: nextOutput.results.map((entry) => (entry.result
|
|
@@ -686,8 +727,8 @@ function applyValidateMixinOutputCompaction(output, input) {
|
|
|
686
727
|
warnings: canHoistWarnings ? [] : entry.result.warnings,
|
|
687
728
|
structuredWarnings: undefined,
|
|
688
729
|
aggregatedWarnings: undefined,
|
|
689
|
-
resolvedMembers: undefined,
|
|
690
|
-
toolHealth: undefined,
|
|
730
|
+
resolvedMembers: keepHeavyDetail ? entry.result.resolvedMembers : undefined,
|
|
731
|
+
toolHealth: keepHeavyDetail ? entry.result.toolHealth : undefined,
|
|
691
732
|
confidenceBreakdown: undefined,
|
|
692
733
|
provenance: canHoistProvenance ? undefined : entry.result.provenance
|
|
693
734
|
}
|
|
@@ -37,6 +37,7 @@ export async function loadOrDetectWorkspaceContext(svc, projectPath) {
|
|
|
37
37
|
value: loaderResult.loader
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
+
const latest = svc.workspaceContextCache.read(projectPath);
|
|
40
41
|
const ctx = {
|
|
41
42
|
projectPath,
|
|
42
43
|
minecraftVersion,
|
|
@@ -44,7 +45,7 @@ export async function loadOrDetectWorkspaceContext(svc, projectPath) {
|
|
|
44
45
|
loader: loaderResult?.resolved ? loaderResult.loader : undefined,
|
|
45
46
|
detectedAt: Date.now(),
|
|
46
47
|
evidence,
|
|
47
|
-
dependencyVersions: cached?.dependencyVersions ?? new Map(),
|
|
48
|
+
dependencyVersions: latest?.dependencyVersions ?? cached?.dependencyVersions ?? new Map(),
|
|
48
49
|
partial: false
|
|
49
50
|
};
|
|
50
51
|
svc.workspaceContextCache.write(ctx);
|
|
@@ -256,15 +257,21 @@ export async function synthesizeDependencyTarget(svc, input, dep) {
|
|
|
256
257
|
nextAction,
|
|
257
258
|
...buildSuggestedCall({
|
|
258
259
|
tool: "resolve-artifact",
|
|
259
|
-
params:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
260
|
+
params: undefined,
|
|
261
|
+
examples: [
|
|
262
|
+
{
|
|
263
|
+
params: {
|
|
264
|
+
target: {
|
|
265
|
+
kind: "dependency",
|
|
266
|
+
group,
|
|
267
|
+
name,
|
|
268
|
+
version: "<your-version>"
|
|
269
|
+
},
|
|
270
|
+
projectPath
|
|
271
|
+
},
|
|
272
|
+
reason: "Replace <your-version> with the dependency version, or declare it in gradle.properties."
|
|
273
|
+
}
|
|
274
|
+
]
|
|
268
275
|
})
|
|
269
276
|
}
|
|
270
277
|
});
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
export declare function __getZipOpenCount(): number;
|
|
3
|
+
export declare function __resetZipOpenCount(): void;
|
|
4
|
+
interface ZipEntry {
|
|
5
|
+
fileName: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ZipFile {
|
|
8
|
+
readEntry(): void;
|
|
9
|
+
close(): void;
|
|
10
|
+
once(event: "entry", listener: (entry: ZipEntry) => void): this;
|
|
11
|
+
once(event: "end", listener: () => void): this;
|
|
12
|
+
once(event: "error", listener: (error: Error) => void): this;
|
|
13
|
+
removeListener(event: "entry", listener: (entry: ZipEntry) => void): this;
|
|
14
|
+
removeListener(event: "end", listener: () => void): this;
|
|
15
|
+
removeListener(event: "error", listener: (error: Error) => void): this;
|
|
16
|
+
openReadStream(entry: ZipEntry, callback: (error: Error | null, stream: Readable | null) => void): void;
|
|
17
|
+
}
|
|
1
18
|
export interface JavaEntryText {
|
|
2
19
|
filePath: string;
|
|
3
20
|
content: string;
|
|
@@ -12,6 +29,7 @@ export interface CollectMatchedJarEntriesOptions {
|
|
|
12
29
|
continueOnError?: boolean;
|
|
13
30
|
}
|
|
14
31
|
export declare function hasJavaSourceExtension(entryPath: string): boolean;
|
|
32
|
+
export declare function openZipFile(jarPath: string): Promise<ZipFile>;
|
|
15
33
|
export declare class EntryTooLargeError extends Error {
|
|
16
34
|
constructor(entryPath: string, jarPath: string, maxBytes: number);
|
|
17
35
|
}
|
|
@@ -20,10 +38,45 @@ export declare function listJavaEntries(jarPath: string): Promise<string[]>;
|
|
|
20
38
|
export declare function hasAnyJarEntry(jarPath: string, predicate: (entryPath: string) => boolean): Promise<boolean>;
|
|
21
39
|
export declare function readJarEntryAsUtf8(jarPath: string, entryPath: string): Promise<string>;
|
|
22
40
|
export declare function readJarEntryAsBuffer(jarPath: string, entryPath: string): Promise<Buffer>;
|
|
41
|
+
export interface JarEntryReader {
|
|
42
|
+
getEntryBuffer(entryPath: string): Promise<Buffer>;
|
|
43
|
+
close(): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Opens a jar ONCE, drains its central directory into a name->entry index, and
|
|
47
|
+
* serves repeated entry reads via O(1) lookup + openReadStream. Use this when a
|
|
48
|
+
* single logical operation reads many entries from the same jar (e.g. walking a
|
|
49
|
+
* class hierarchy) instead of calling {@link readJarEntryAsBuffer} per entry,
|
|
50
|
+
* which re-opens and re-scans the jar each time. Callers MUST close() the reader
|
|
51
|
+
* (in a finally) to release the file descriptor. getEntryBuffer throws the same
|
|
52
|
+
* INVALID_INPUT (unsafe path) / SOURCE_NOT_FOUND (missing entry) errors as
|
|
53
|
+
* readJarEntryAsBuffer; duplicate entry names resolve to the first occurrence.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createJarEntryReader(jarPath: string): Promise<JarEntryReader>;
|
|
23
56
|
export declare function collectMatchedJarEntriesAsUtf8(jarPath: string, predicate: (entryPath: string) => boolean, options?: CollectMatchedJarEntriesOptions): Promise<JarEntryText[]>;
|
|
57
|
+
export interface JarEntryBuffer {
|
|
58
|
+
filePath: string;
|
|
59
|
+
content: Buffer;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Seam for injecting the jar-open primitive so callers/tests can count opens.
|
|
63
|
+
* Defaults to the module-level {@link openZipFile}.
|
|
64
|
+
*/
|
|
65
|
+
export interface JarReaderOpenDeps {
|
|
66
|
+
openZipFile?: (jarPath: string) => Promise<ZipFile>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Raw-buffer counterpart of {@link collectMatchedJarEntriesAsUtf8}: opens the jar
|
|
70
|
+
* ONCE and returns the matched entries as raw Buffers (no UTF-8 decode), so binary
|
|
71
|
+
* inputs like `.class` bytes survive intact. Used to avoid re-opening the jar per
|
|
72
|
+
* sampled entry. Mirrors the sibling's maxBytes/maxEntries/continueOnError semantics
|
|
73
|
+
* and its finally-close, but takes an injectable open seam for open-count assertions.
|
|
74
|
+
*/
|
|
75
|
+
export declare function collectMatchedJarEntriesAsBuffers(jarPath: string, predicate: (entryPath: string) => boolean, options?: CollectMatchedJarEntriesOptions, deps?: JarReaderOpenDeps): Promise<JarEntryBuffer[]>;
|
|
24
76
|
export declare function iterateJavaEntriesAsUtf8(jarPath: string, maxBytes?: number): AsyncGenerator<JavaEntryText>;
|
|
25
77
|
export declare function readAllJavaEntriesAsUtf8(jarPath: string, maxBytes?: number): Promise<JavaEntryText[]>;
|
|
26
|
-
export declare function detectFabricLikeInputNamespace(inputJar: string): Promise<{
|
|
78
|
+
export declare function detectFabricLikeInputNamespace(inputJar: string, deps?: JarReaderOpenDeps): Promise<{
|
|
27
79
|
fromNamespace: "intermediary" | "mojang";
|
|
28
80
|
warnings: string[];
|
|
29
81
|
}>;
|
|
82
|
+
export {};
|