@adhisang/minecraft-modding-mcp 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/access-widener-parser.d.ts +1 -0
  3. package/dist/access-widener-parser.js +6 -3
  4. package/dist/cache-registry.js +35 -31
  5. package/dist/decompiler/vineflower.js +8 -2
  6. package/dist/entry-tools/analyze-mod-service.js +2 -1
  7. package/dist/entry-tools/analyze-symbol-service.js +18 -9
  8. package/dist/entry-tools/compare-minecraft-service.js +1 -1
  9. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -1
  10. package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -1
  11. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +2 -1
  12. package/dist/entry-tools/inspect-minecraft/internal.js +2 -0
  13. package/dist/entry-tools/response-contract.d.ts +2 -0
  14. package/dist/entry-tools/response-contract.js +2 -2
  15. package/dist/entry-tools/validate-project-service.d.ts +4 -4
  16. package/dist/entry-tools/verify-mixin-target-service.js +7 -7
  17. package/dist/index.js +20 -16
  18. package/dist/java-process.js +1 -1
  19. package/dist/json-rpc-framing.d.ts +4 -0
  20. package/dist/json-rpc-framing.js +23 -1
  21. package/dist/mapping/internal-types.d.ts +17 -0
  22. package/dist/mapping/loaders/tiny-maven.js +46 -20
  23. package/dist/mapping/lookup.d.ts +12 -1
  24. package/dist/mapping/lookup.js +53 -1
  25. package/dist/mapping-service.js +36 -21
  26. package/dist/mixin/annotation-validators.js +21 -3
  27. package/dist/mixin/parsed-validator.js +1 -0
  28. package/dist/mixin-parser.js +71 -3
  29. package/dist/mod-decompile-service.d.ts +2 -0
  30. package/dist/mod-decompile-service.js +19 -2
  31. package/dist/mod-remap-service.js +6 -5
  32. package/dist/mojang-tiny-mapping-service.js +5 -2
  33. package/dist/nbt/java-nbt-codec.js +40 -16
  34. package/dist/nbt/json-patch.js +51 -12
  35. package/dist/nbt/typed-json.d.ts +1 -0
  36. package/dist/nbt/typed-json.js +12 -2
  37. package/dist/nbt/types.d.ts +2 -2
  38. package/dist/path-converter.js +10 -3
  39. package/dist/registry-service.d.ts +2 -0
  40. package/dist/registry-service.js +16 -1
  41. package/dist/repo-downloader.js +4 -3
  42. package/dist/source/artifact-resolver.js +1 -1
  43. package/dist/source/class-source/snippet-builder.js +6 -0
  44. package/dist/source/class-source-helpers.d.ts +4 -4
  45. package/dist/source/class-source-helpers.js +12 -11
  46. package/dist/source/class-source.d.ts +19 -0
  47. package/dist/source/class-source.js +48 -21
  48. package/dist/source/file-access.js +3 -2
  49. package/dist/source/indexer.js +24 -7
  50. package/dist/source/lifecycle/mapping-helpers.js +28 -3
  51. package/dist/source/lifecycle/runtime-check.js +20 -6
  52. package/dist/source/search.js +23 -8
  53. package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
  54. package/dist/source/workspace-target.js +2 -1
  55. package/dist/source-resolver.js +2 -2
  56. package/dist/source-service.js +9 -1
  57. package/dist/stdio-supervisor.d.ts +2 -2
  58. package/dist/stdio-supervisor.js +29 -9
  59. package/dist/storage/db.js +24 -1
  60. package/dist/storage/files-repo.d.ts +3 -0
  61. package/dist/storage/files-repo.js +43 -37
  62. package/dist/storage/symbols-repo.d.ts +9 -0
  63. package/dist/storage/symbols-repo.js +47 -19
  64. package/dist/symbols/symbol-extractor.js +1 -1
  65. package/dist/tool-guidance.d.ts +2 -1
  66. package/dist/tool-guidance.js +26 -1
  67. package/dist/tool-schemas.d.ts +4 -4
  68. package/dist/tool-schemas.js +91 -91
  69. package/dist/warning-details.d.ts +12 -0
  70. package/dist/warning-details.js +17 -0
  71. package/dist/workspace-mapping-service.js +9 -0
  72. package/package.json +1 -1
@@ -54,7 +54,7 @@ export function hasExactVersionToken(path, version) {
54
54
  // Avoid prefix false-positives like "1.21.1" matching "1.21.10".
55
55
  const cached = VERSION_TOKEN_REGEX_CACHE.get(normalizedVersion);
56
56
  const pattern = cached
57
- ?? rememberCachedRegex(VERSION_TOKEN_REGEX_CACHE, normalizedVersion, new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}([^0-9a-z]|$)`, "i"));
57
+ ?? rememberCachedRegex(VERSION_TOKEN_REGEX_CACHE, normalizedVersion, new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}(?![0-9a-z]|\\.[0-9])`, "i"));
58
58
  return pattern.test(normalizedPath);
59
59
  }
60
60
  function inferMergedRuntimeNamespaceHint(path) {
@@ -53,6 +53,12 @@ export function buildClassSourceSnippet(input) {
53
53
  sourceText = sliceToMaxCharsSafe(sourceText, input.maxChars);
54
54
  charsTruncated = true;
55
55
  truncated = true;
56
+ if (input.mode !== "metadata") {
57
+ const survivingNewlines = (sourceText.match(/\n/g) ?? []).length;
58
+ returnedEnd = sourceText.endsWith("\n")
59
+ ? returnedStart + survivingNewlines - 1
60
+ : returnedStart + survivingNewlines;
61
+ }
56
62
  }
57
63
  let nextStartLine;
58
64
  // Metadata mode returns a synthesized outline, not a line window into the
@@ -9,16 +9,16 @@ export declare function extractDecompiledMembers(className: string, filePath: st
9
9
  fields: DecompiledMember[];
10
10
  methods: DecompiledMember[];
11
11
  };
12
- export declare function computeLineBraceDepths(lines: string[]): number[];
12
+ export declare function computeLineBraceDepths(lines: string[], strippedLines?: string[]): number[];
13
13
  export declare function computeBraceRange(lines: string[], symbols: Array<{
14
14
  symbolKind: string;
15
15
  symbolName: string;
16
16
  line: number;
17
- }>, simpleName: string): {
17
+ }>, simpleName: string, strippedLines?: string[]): {
18
18
  declarationLine: number;
19
19
  endLine: number;
20
20
  } | undefined;
21
- export declare function scanBraceRange(lines: string[], declarationLine: number): {
21
+ export declare function scanBraceRange(lines: string[], declarationLine: number, strippedLines?: string[]): {
22
22
  declarationLine: number;
23
23
  endLine: number;
24
24
  };
@@ -28,7 +28,7 @@ export declare function computeNestedTypeRanges(lines: string[], symbols: Array<
28
28
  }>, outerBody: {
29
29
  declarationLine: number;
30
30
  endLine: number;
31
- }): Array<{
31
+ }, strippedLines?: string[]): Array<{
32
32
  declarationLine: number;
33
33
  endLine: number;
34
34
  }>;
@@ -30,13 +30,14 @@ export function extractDecompiledMembers(className, filePath, content) {
30
30
  const symbols = extractSymbolsFromSource(filePath, content);
31
31
  const simpleName = className.split(/[.$]/).at(-1) ?? className;
32
32
  const lines = content.split(/\r?\n/);
33
- const body = computeBraceRange(lines, symbols, simpleName);
33
+ const stripped = stripBraceNoise(lines);
34
+ const body = computeBraceRange(lines, symbols, simpleName, stripped);
34
35
  if (!body) {
35
36
  return { constructors: [], fields: [], methods: [] };
36
37
  }
37
- const depths = computeLineBraceDepths(lines);
38
+ const depths = computeLineBraceDepths(lines, stripped);
38
39
  const baseDepth = depths[body.declarationLine - 1] ?? 0;
39
- const nestedRanges = computeNestedTypeRanges(lines, symbols, body);
40
+ const nestedRanges = computeNestedTypeRanges(lines, symbols, body, stripped);
40
41
  const constructors = [];
41
42
  const fields = [];
42
43
  const methods = [];
@@ -126,8 +127,8 @@ function stripBraceNoise(lines) {
126
127
  }
127
128
  return out;
128
129
  }
129
- export function computeLineBraceDepths(lines) {
130
- const stripped = stripBraceNoise(lines);
130
+ export function computeLineBraceDepths(lines, strippedLines) {
131
+ const stripped = strippedLines ?? stripBraceNoise(lines);
131
132
  const depths = new Array(lines.length).fill(0);
132
133
  let depth = 0;
133
134
  for (let i = 0; i < stripped.length; i += 1) {
@@ -144,19 +145,19 @@ export function computeLineBraceDepths(lines) {
144
145
  }
145
146
  return depths;
146
147
  }
147
- export function computeBraceRange(lines, symbols, simpleName) {
148
+ export function computeBraceRange(lines, symbols, simpleName, strippedLines) {
148
149
  const classSymbol = symbols.find((symbol) => (symbol.symbolKind === "class" || symbol.symbolKind === "interface"
149
150
  || symbol.symbolKind === "enum" || symbol.symbolKind === "record")
150
151
  && symbol.symbolName === simpleName);
151
152
  if (!classSymbol) {
152
153
  return undefined;
153
154
  }
154
- return scanBraceRange(lines, classSymbol.line);
155
+ return scanBraceRange(lines, classSymbol.line, strippedLines);
155
156
  }
156
- export function scanBraceRange(lines, declarationLine) {
157
+ export function scanBraceRange(lines, declarationLine, strippedLines) {
157
158
  // Strip from the start of the file so multi-line block-comment state is correct
158
159
  // by the time we reach declarationLine; indices stay aligned with `lines`.
159
- const stripped = stripBraceNoise(lines);
160
+ const stripped = strippedLines ?? stripBraceNoise(lines);
160
161
  let depth = 0;
161
162
  let started = false;
162
163
  for (let i = declarationLine - 1; i < stripped.length; i += 1) {
@@ -175,7 +176,7 @@ export function scanBraceRange(lines, declarationLine) {
175
176
  }
176
177
  return { declarationLine, endLine: lines.length };
177
178
  }
178
- export function computeNestedTypeRanges(lines, symbols, outerBody) {
179
+ export function computeNestedTypeRanges(lines, symbols, outerBody, strippedLines) {
179
180
  const ranges = [];
180
181
  for (const candidate of symbols) {
181
182
  if (candidate.symbolKind !== "class" && candidate.symbolKind !== "interface"
@@ -188,7 +189,7 @@ export function computeNestedTypeRanges(lines, symbols, outerBody) {
188
189
  if (ranges.some((range) => candidate.line >= range.declarationLine && candidate.line <= range.endLine)) {
189
190
  continue;
190
191
  }
191
- const nestedRange = scanBraceRange(lines, candidate.line);
192
+ const nestedRange = scanBraceRange(lines, candidate.line, strippedLines);
192
193
  ranges.push(nestedRange);
193
194
  }
194
195
  return ranges;
@@ -2,6 +2,24 @@ import { type AppError } from "../errors.js";
2
2
  import type { SourceService } from "../source-service.js";
3
3
  import type { DecompiledFallback, FindClassInput, FindClassOutput, GetClassMembersInput, GetClassMembersOutput, GetClassSourceInput, GetClassSourceOutput } from "../source-service.js";
4
4
  import type { ArtifactProvenance, ArtifactScope, MappingSourcePriority, ResolvedSourceArtifact, SourceMapping } from "../types.js";
5
+ /**
6
+ * Unobfuscated Minecraft versions (26.1+) ship their runtime and decompiled source
7
+ * in deobfuscated (mojang) names directly — there is no real obfuscated namespace to
8
+ * map to. An artifact whose stored namespace label is "obfuscated" is therefore a
9
+ * mislabel for a mojang request: the bytes already carry mojang names.
10
+ *
11
+ * Without this reconciliation, requestedMapping="mojang" !== mappingApplied="obfuscated"
12
+ * triggers a cascade of doomed work: resolveClassNameForLookup attempts a mojang->obfuscated
13
+ * class remap, remapSignatureMembers tries to remap every member obfuscated->mojang and drops
14
+ * them all (counts.total===0), which forces a spurious decompiledFallback, disables
15
+ * memberPattern, and floods the response with one "Could not remap ..." warning per member.
16
+ *
17
+ * Collapsing mappingApplied to the requested mojang namespace makes the whole pipeline an
18
+ * identity no-op: members are returned from bytecode with correct mojang names, memberPattern
19
+ * applies, and the per-member warning flood disappears. This is the single highest-impact
20
+ * token-efficiency fix for unobfuscated versions.
21
+ */
22
+ export declare function reconcileUnobfuscatedNamespace(version: string | undefined, requestedMapping: SourceMapping, mappingApplied: SourceMapping): SourceMapping;
5
23
  export declare function resolveClassFilePath(svc: SourceService, artifactId: string, className: string): string | undefined;
6
24
  export declare function resolveClassNameForLookup(svc: SourceService, input: {
7
25
  className: string;
@@ -37,6 +55,7 @@ export declare function buildClassSourceNotFoundError(_svc: SourceService, input
37
55
  export declare function buildDecompiledFallback(svc: SourceService, artifactId: string, lookupClassName: string, memberPattern: string | undefined, maxMembers: number): {
38
56
  fallback: DecompiledFallback;
39
57
  counts: NonNullable<GetClassMembersOutput["decompiledMemberCounts"]>;
58
+ truncated: boolean;
40
59
  } | undefined;
41
60
  export declare function findClass(svc: SourceService, input: FindClassInput): FindClassOutput;
42
61
  export declare function getClassSource(svc: SourceService, input: GetClassSourceInput): Promise<GetClassSourceOutput>;
@@ -8,7 +8,34 @@ import { buildClassSourceSnippet } from "./class-source/snippet-builder.js";
8
8
  import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire } from "./class-source/members-builder.js";
9
9
  import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
10
10
  import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
11
+ import { isUnobfuscatedVersion } from "../version-service.js";
11
12
  const MEMBERS_STATUS_LEGACY = process.env.MEMBERS_STATUS_LEGACY === "1";
13
+ /**
14
+ * Unobfuscated Minecraft versions (26.1+) ship their runtime and decompiled source
15
+ * in deobfuscated (mojang) names directly — there is no real obfuscated namespace to
16
+ * map to. An artifact whose stored namespace label is "obfuscated" is therefore a
17
+ * mislabel for a mojang request: the bytes already carry mojang names.
18
+ *
19
+ * Without this reconciliation, requestedMapping="mojang" !== mappingApplied="obfuscated"
20
+ * triggers a cascade of doomed work: resolveClassNameForLookup attempts a mojang->obfuscated
21
+ * class remap, remapSignatureMembers tries to remap every member obfuscated->mojang and drops
22
+ * them all (counts.total===0), which forces a spurious decompiledFallback, disables
23
+ * memberPattern, and floods the response with one "Could not remap ..." warning per member.
24
+ *
25
+ * Collapsing mappingApplied to the requested mojang namespace makes the whole pipeline an
26
+ * identity no-op: members are returned from bytecode with correct mojang names, memberPattern
27
+ * applies, and the per-member warning flood disappears. This is the single highest-impact
28
+ * token-efficiency fix for unobfuscated versions.
29
+ */
30
+ export function reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied) {
31
+ if (version &&
32
+ requestedMapping === "mojang" &&
33
+ mappingApplied === "obfuscated" &&
34
+ isUnobfuscatedVersion(version)) {
35
+ return "mojang";
36
+ }
37
+ return mappingApplied;
38
+ }
12
39
  function normalizeStrictPositiveInt(value, field) {
13
40
  if (value == null) {
14
41
  return undefined;
@@ -35,15 +62,6 @@ function normalizeMemberAccess(access) {
35
62
  details: { access }
36
63
  });
37
64
  }
38
- function buildResolveArtifactParams(target, extra = {}) {
39
- return {
40
- target: {
41
- kind: target.kind,
42
- value: target.value
43
- },
44
- ...extra
45
- };
46
- }
47
65
  function looksLikeDeobfuscatedClassName(value) {
48
66
  const trimmed = value.trim();
49
67
  if (!trimmed) {
@@ -227,7 +245,10 @@ export function buildDecompiledFallback(svc, artifactId, lookupClassName, member
227
245
  let constructors = filterByPattern(extracted.constructors);
228
246
  let fields = filterByPattern(extracted.fields);
229
247
  let methods = filterByPattern(extracted.methods);
230
- const totalBefore = constructors.length + fields.length + methods.length;
248
+ const constructorsBefore = constructors.length;
249
+ const fieldsBefore = fields.length;
250
+ const methodsBefore = methods.length;
251
+ const totalBefore = constructorsBefore + fieldsBefore + methodsBefore;
231
252
  if (totalBefore === 0) {
232
253
  return undefined;
233
254
  }
@@ -251,11 +272,12 @@ export function buildDecompiledFallback(svc, artifactId, lookupClassName, member
251
272
  origin: "source-extracted"
252
273
  },
253
274
  counts: {
254
- constructors: constructors.length,
255
- fields: fields.length,
256
- methods: methods.length,
257
- total: constructors.length + fields.length + methods.length
258
- }
275
+ constructors: constructorsBefore,
276
+ fields: fieldsBefore,
277
+ methods: methodsBefore,
278
+ total: totalBefore
279
+ },
280
+ truncated: totalBefore > maxMembers
259
281
  };
260
282
  }
261
283
  // The class-like symbol kinds findClass returns. MUST stay in sync with the JS-side
@@ -444,7 +466,7 @@ export async function getClassSource(svc, input) {
444
466
  const artifact = svc.getArtifact(artifactId);
445
467
  artifactId = artifact.artifactId;
446
468
  origin = artifact.origin;
447
- requestedMapping = artifact.requestedMapping ?? requestedMapping;
469
+ requestedMapping = input.mapping != null ? requestedMapping : (artifact.requestedMapping ?? requestedMapping);
448
470
  mappingApplied = artifact.mappingApplied ?? requestedMapping;
449
471
  provenance = artifact.provenance;
450
472
  qualityFlags = artifact.qualityFlags;
@@ -461,6 +483,7 @@ export async function getClassSource(svc, input) {
461
483
  preferProjectVersion: input.preferProjectVersion,
462
484
  warnings
463
485
  });
486
+ mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
464
487
  let activeArtifactId = artifactId;
465
488
  let activeOrigin = origin;
466
489
  let activeProvenance = provenance;
@@ -624,7 +647,7 @@ export async function getClassSource(svc, input) {
624
647
  mode,
625
648
  startLine: nextStartLine,
626
649
  ...(input.endLine != null ? { endLine: input.endLine } : {}),
627
- ...(input.maxLines != null ? { maxLines: input.maxLines } : {}),
650
+ ...(maxLines != null ? { maxLines } : {}),
628
651
  ...(input.maxChars != null ? { maxChars: input.maxChars } : {})
629
652
  }
630
653
  })
@@ -746,6 +769,7 @@ export async function getClassMembers(svc, input) {
746
769
  preferProjectVersion: input.preferProjectVersion,
747
770
  warnings
748
771
  });
772
+ mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
749
773
  if (requestedMapping !== "obfuscated" && !version) {
750
774
  throw createError({
751
775
  code: ERROR_CODES.MAPPING_NOT_APPLIED,
@@ -753,10 +777,7 @@ export async function getClassMembers(svc, input) {
753
777
  details: {
754
778
  mapping: requestedMapping,
755
779
  nextAction: "Resolve with target: { kind: \"version\", value: ... } or specify a versioned coordinate.",
756
- ...buildSuggestedCall({
757
- tool: "resolve-artifact",
758
- params: buildResolveArtifactParams({ kind: "version", value: "latest" })
759
- })
780
+ ...buildSuggestedCall({ tool: "list-versions", params: {} })
760
781
  }
761
782
  });
762
783
  }
@@ -898,6 +919,12 @@ export async function getClassMembers(svc, input) {
898
919
  warnings.push("Bytecode member enumeration returned zero; populated decompiledFallback from decompiled source. "
899
920
  + "Descriptors and access modifiers are unavailable — use get-class-source for full details."
900
921
  + namespaceNote);
922
+ if (sourceFallback.truncated) {
923
+ const returnedTotal = decompiledFallback.constructors.length
924
+ + decompiledFallback.fields.length
925
+ + decompiledFallback.methods.length;
926
+ warnings.push(`Member list was truncated to ${returnedTotal} entries (from ${sourceFallback.counts.total}).`);
927
+ }
901
928
  if (namespaceMismatch && memberPattern) {
902
929
  warnings.push(`memberPattern="${memberPattern}" was not applied to decompiledFallback because the artifact namespace (${mappingApplied}) differs from the requested namespace (${requestedMapping}); filter the response client-side after mapping.`);
903
930
  }
@@ -71,12 +71,13 @@ export async function listArtifactFiles(svc, input) {
71
71
  const artifact = svc.getArtifact(input.artifactId);
72
72
  const limit = clampLimit(input.limit, 200, 2000);
73
73
  const warnings = [];
74
+ const prefix = input.prefix === undefined ? undefined : normalizePathStyle(input.prefix);
74
75
  const page = svc.filesRepo.listFiles(artifact.artifactId, {
75
76
  limit,
76
77
  cursor: input.cursor,
77
- prefix: input.prefix
78
+ prefix
78
79
  });
79
- const normalizedPrefix = normalizeOptionalString(input.prefix);
80
+ const normalizedPrefix = normalizeOptionalString(prefix);
80
81
  if (normalizedPrefix &&
81
82
  page.items.length === 0 &&
82
83
  (normalizedPrefix.startsWith("assets/") || normalizedPrefix.startsWith("data/"))) {
@@ -210,17 +210,20 @@ export async function buildRebuiltArtifactData(svc, resolved) {
210
210
  }
211
211
  catch (caughtError) {
212
212
  if (isAppError(caughtError) && caughtError.code === ERROR_CODES.DECOMPILER_FAILED) {
213
+ // Decompilation failed BEFORE the artifact was ever upserted, so there is no
214
+ // queryable artifact. Deliberately omit artifactId from the error: exposing the
215
+ // would-be id led callers to pass it to find-class/get-class-*, which then failed
216
+ // with "Artifact not found. Resolve context first." (B5 state inconsistency).
217
+ const priorDetails = { ...(caughtError.details ?? {}) };
218
+ delete priorDetails.artifactId;
213
219
  throw createError({
214
220
  code: ERROR_CODES.DECOMPILER_FAILED,
215
221
  message: caughtError.message,
216
222
  details: {
217
- ...(caughtError.details ?? {}),
218
- artifactId: resolved.artifactId,
223
+ ...priorDetails,
219
224
  binaryJarPath: resolved.binaryJarPath,
220
- producedJavaCount: typeof caughtError.details?.producedJavaCount === "number"
221
- ? caughtError.details.producedJavaCount
222
- : 0,
223
- nextAction: "Verify Java runtime and Vineflower availability, then retry. If available, prefer source-backed artifacts.",
225
+ producedJavaCount: typeof priorDetails.producedJavaCount === "number" ? priorDetails.producedJavaCount : 0,
226
+ nextAction: "Decompilation failed, so no artifact was created. Verify Java runtime and Vineflower availability, then retry; prefer source-backed artifacts when available.",
224
227
  recommendedCommand: "echo $MCP_VINEFLOWER_JAR_PATH"
225
228
  }
226
229
  });
@@ -336,6 +339,13 @@ export async function ingestIfNeeded(svc, resolved) {
336
339
  const inflight = svc.state.inflightArtifactIngests.get(resolved.artifactId);
337
340
  if (inflight) {
338
341
  await inflight;
342
+ const transformChain = resolved.provenance?.transformChain ?? [];
343
+ if (transformChain.includes("binary-remap:obf->mojang") && resolved.binaryJarPath) {
344
+ const reconciledBinaryJarPath = await maybeRemapBinaryForMojang(svc, resolved);
345
+ if (reconciledBinaryJarPath !== resolved.binaryJarPath) {
346
+ resolved.binaryJarPath = reconciledBinaryJarPath;
347
+ }
348
+ }
339
349
  return;
340
350
  }
341
351
  const ingestPromise = rebuildMissingArtifactIndex(svc, resolved, reason);
@@ -429,9 +439,16 @@ export async function maybeRemapBinaryForMojang(svc, resolved, deps = defaultBin
429
439
  if (inflight) {
430
440
  return inflight;
431
441
  }
442
+ // When a prior index-artifact persisted the remapped jar back into the
443
+ // artifacts row, binaryJarPath already equals remappedJarPath. Remapping that
444
+ // missing/corrupt path onto itself would fail permanently, so re-resolve the
445
+ // original obfuscated client jar to recover from out-of-band cache loss.
446
+ const inputJar = binaryJarPath === remappedJarPath
447
+ ? (await svc.versionService.resolveVersionJar(resolved.version)).jarPath
448
+ : binaryJarPath;
432
449
  const remapPromise = runBinaryRemapWithDeps(svc, {
433
450
  version: resolved.version,
434
- inputJar: binaryJarPath,
451
+ inputJar,
435
452
  remappedDir,
436
453
  remappedJarPath
437
454
  }, deps);
@@ -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
- warnings.push(`Could not remap ${kind} "${name}" from ${sourceMapping} to ${targetMapping}.`);
325
+ unremappedNames.push(name);
303
326
  failedNames.add(name);
304
327
  }
305
328
  }
306
329
  else {
307
- warnings.push(`Could not remap ${kind} "${name}" from ${sourceMapping} to ${targetMapping}.`);
330
+ unremappedNames.push(name);
308
331
  failedNames.add(name);
309
332
  }
310
333
  }
311
334
  catch {
312
- warnings.push(`Remap failed for ${kind} "${name}" from ${sourceMapping} to ${targetMapping}.`);
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
- `Version ${version} is unobfuscated; runtime bytecode lookup could not load class "${targetClass}".`
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
- if (input.signatureMode === "name-only") {
118
- if (methodCandidates.length !== 1) {
119
- return buildUnresolved(methodCandidates.length > 1 ? "ambiguous" : "not_found");
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",
@@ -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
- result += ".*";
69
- i += 2;
70
- if (pattern[i] === "/") {
71
- result += "(?:/)?";
72
- i += 1;
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 === "*") {
@@ -301,6 +303,9 @@ export async function searchClassSource(svc, input) {
301
303
  searchWarnings.push(`queryNamespace=${input.queryNamespace}: translation failed (${caughtError instanceof Error ? caughtError.message : String(caughtError)}); running literal search instead.`);
302
304
  }
303
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
+ }
304
309
  else if (intent === "text" || intent === "path") {
305
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.`);
306
311
  }
@@ -676,7 +681,9 @@ export function searchPathIntent(svc, artifactId, query, match, scope, regexPatt
676
681
  }
677
682
  export function findSymbolHits(svc, artifactId, query, match, scope, regexPattern) {
678
683
  if (match !== "regex") {
679
- const filePathLike = scope?.fileGlob ? globToSqlLike(normalizePathStyle(scope.fileGlob)) : undefined;
684
+ const filePathLike = scope?.fileGlob && !scope.fileGlob.includes("**")
685
+ ? globToSqlLike(normalizePathStyle(scope.fileGlob))
686
+ : undefined;
680
687
  const scoped = svc.symbolsRepo.findScopedSymbols({
681
688
  artifactId,
682
689
  query,
@@ -688,8 +695,12 @@ export function findSymbolHits(svc, artifactId, query, match, scope, regexPatter
688
695
  });
689
696
  svc.metrics.recordSearchDbRoundtrip();
690
697
  svc.metrics.recordSearchRowsScanned(scoped.items.length);
698
+ const globFilter = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
691
699
  const result = [];
692
700
  for (const symbol of scoped.items) {
701
+ if (globFilter && !globFilter.test(symbol.filePath)) {
702
+ continue;
703
+ }
693
704
  if (!isSymbolKind(symbol.symbolKind)) {
694
705
  continue;
695
706
  }
@@ -705,12 +716,15 @@ export function findSymbolHits(svc, artifactId, query, match, scope, regexPatter
705
716
  }
706
717
  return result;
707
718
  }
708
- const candidates = svc.symbolsRepo.listSymbolsForArtifact(artifactId, scope?.symbolKind);
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);
709
722
  svc.metrics.recordSearchDbRoundtrip();
710
- svc.metrics.recordSearchRowsScanned(candidates.length);
723
+ let rowsScanned = 0;
711
724
  const result = [];
712
725
  const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
713
726
  for (const symbol of candidates) {
727
+ rowsScanned += 1;
714
728
  if (!checkPackagePrefix(symbol.filePath, scope?.packagePrefix)) {
715
729
  continue;
716
730
  }
@@ -732,6 +746,7 @@ export function findSymbolHits(svc, artifactId, query, match, scope, regexPatter
732
746
  matchIndex: index
733
747
  });
734
748
  }
749
+ svc.metrics.recordSearchRowsScanned(rowsScanned);
735
750
  return result;
736
751
  }
737
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
- ctx.mappingApplied = ctx.resolvedArtifact.mappingApplied;
46
- ctx.signatureLookupMapping = ctx.resolvedArtifact.mappingApplied;
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")) {
@@ -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);
@@ -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,