@adhisang/minecraft-modding-mcp 5.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/access-widener-parser.d.ts +1 -0
  3. package/dist/access-widener-parser.js +6 -3
  4. package/dist/cache-registry.js +35 -31
  5. package/dist/config.js +1 -1
  6. package/dist/decompiler/vineflower.js +8 -2
  7. package/dist/entry-tools/analyze-mod-service.js +2 -1
  8. package/dist/entry-tools/analyze-symbol-service.js +18 -9
  9. package/dist/entry-tools/batch-class-members-service.d.ts +2 -0
  10. package/dist/entry-tools/batch-class-members-service.js +1 -0
  11. package/dist/entry-tools/compare-minecraft-service.js +1 -1
  12. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -1
  13. package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -1
  14. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +2 -1
  15. package/dist/entry-tools/inspect-minecraft/internal.js +2 -0
  16. package/dist/entry-tools/response-contract.d.ts +2 -0
  17. package/dist/entry-tools/response-contract.js +2 -2
  18. package/dist/entry-tools/validate-project/cases/project-summary.js +30 -12
  19. package/dist/entry-tools/validate-project-service.d.ts +4 -4
  20. package/dist/entry-tools/verify-mixin-target-service.js +7 -7
  21. package/dist/index.js +21 -16
  22. package/dist/java-process.js +1 -1
  23. package/dist/json-rpc-framing.d.ts +4 -0
  24. package/dist/json-rpc-framing.js +23 -1
  25. package/dist/mapping/internal-types.d.ts +17 -0
  26. package/dist/mapping/loaders/tiny-maven.js +46 -20
  27. package/dist/mapping/lookup.d.ts +12 -1
  28. package/dist/mapping/lookup.js +53 -1
  29. package/dist/mapping-service.js +37 -22
  30. package/dist/minecraft-explorer-service.js +2 -1
  31. package/dist/mixin/annotation-validators.js +21 -3
  32. package/dist/mixin/parsed-validator.js +1 -0
  33. package/dist/mixin-parser.js +71 -3
  34. package/dist/mod-decompile-service.d.ts +2 -0
  35. package/dist/mod-decompile-service.js +19 -2
  36. package/dist/mod-remap-service.js +6 -5
  37. package/dist/mojang-tiny-mapping-service.js +5 -2
  38. package/dist/nbt/java-nbt-codec.js +40 -16
  39. package/dist/nbt/json-patch.js +51 -12
  40. package/dist/nbt/typed-json.d.ts +1 -0
  41. package/dist/nbt/typed-json.js +12 -2
  42. package/dist/nbt/types.d.ts +2 -2
  43. package/dist/path-converter.js +10 -3
  44. package/dist/registry-service.d.ts +2 -0
  45. package/dist/registry-service.js +16 -1
  46. package/dist/repo-downloader.js +4 -3
  47. package/dist/source/artifact-resolver.js +1 -1
  48. package/dist/source/class-source/members-builder.d.ts +26 -0
  49. package/dist/source/class-source/members-builder.js +31 -4
  50. package/dist/source/class-source/snippet-builder.js +6 -0
  51. package/dist/source/class-source-helpers.d.ts +4 -4
  52. package/dist/source/class-source-helpers.js +12 -11
  53. package/dist/source/class-source.d.ts +19 -0
  54. package/dist/source/class-source.js +87 -26
  55. package/dist/source/file-access.js +3 -2
  56. package/dist/source/indexer.js +24 -7
  57. package/dist/source/lifecycle/mapping-helpers.js +28 -3
  58. package/dist/source/lifecycle/runtime-check.js +20 -6
  59. package/dist/source/member-pattern.d.ts +16 -0
  60. package/dist/source/member-pattern.js +27 -0
  61. package/dist/source/search.js +23 -8
  62. package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
  63. package/dist/source/workspace-target.js +2 -1
  64. package/dist/source-resolver.js +2 -2
  65. package/dist/source-service.d.ts +8 -4
  66. package/dist/source-service.js +9 -1
  67. package/dist/stdio-supervisor.d.ts +2 -2
  68. package/dist/stdio-supervisor.js +29 -9
  69. package/dist/storage/db.js +24 -1
  70. package/dist/storage/files-repo.d.ts +3 -0
  71. package/dist/storage/files-repo.js +43 -37
  72. package/dist/storage/symbols-repo.d.ts +9 -0
  73. package/dist/storage/symbols-repo.js +47 -19
  74. package/dist/symbols/symbol-extractor.js +1 -1
  75. package/dist/tool-guidance.d.ts +2 -1
  76. package/dist/tool-guidance.js +27 -2
  77. package/dist/tool-schemas.d.ts +15 -4
  78. package/dist/tool-schemas.js +102 -97
  79. package/dist/warning-details.d.ts +12 -0
  80. package/dist/warning-details.js +17 -0
  81. package/dist/workspace-mapping-service.js +9 -0
  82. package/docs/tool-reference.md +23 -0
  83. package/package.json +1 -1
@@ -5,10 +5,38 @@ import { ERROR_CODES, createError, isAppError } from "../errors.js";
5
5
  import * as artifactResolver from "./artifact-resolver.js";
6
6
  import * as classSourceHelpers from "./class-source-helpers.js";
7
7
  import { buildClassSourceSnippet } from "./class-source/snippet-builder.js";
8
- import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire } from "./class-source/members-builder.js";
8
+ import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire, projectMembersByLevel } from "./class-source/members-builder.js";
9
+ import { matchesMemberPattern } from "./member-pattern.js";
9
10
  import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
10
11
  import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
12
+ import { isUnobfuscatedVersion } from "../version-service.js";
11
13
  const MEMBERS_STATUS_LEGACY = process.env.MEMBERS_STATUS_LEGACY === "1";
14
+ /**
15
+ * Unobfuscated Minecraft versions (26.1+) ship their runtime and decompiled source
16
+ * in deobfuscated (mojang) names directly — there is no real obfuscated namespace to
17
+ * map to. An artifact whose stored namespace label is "obfuscated" is therefore a
18
+ * mislabel for a mojang request: the bytes already carry mojang names.
19
+ *
20
+ * Without this reconciliation, requestedMapping="mojang" !== mappingApplied="obfuscated"
21
+ * triggers a cascade of doomed work: resolveClassNameForLookup attempts a mojang->obfuscated
22
+ * class remap, remapSignatureMembers tries to remap every member obfuscated->mojang and drops
23
+ * them all (counts.total===0), which forces a spurious decompiledFallback, disables
24
+ * memberPattern, and floods the response with one "Could not remap ..." warning per member.
25
+ *
26
+ * Collapsing mappingApplied to the requested mojang namespace makes the whole pipeline an
27
+ * identity no-op: members are returned from bytecode with correct mojang names, memberPattern
28
+ * applies, and the per-member warning flood disappears. This is the single highest-impact
29
+ * token-efficiency fix for unobfuscated versions.
30
+ */
31
+ export function reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied) {
32
+ if (version &&
33
+ requestedMapping === "mojang" &&
34
+ mappingApplied === "obfuscated" &&
35
+ isUnobfuscatedVersion(version)) {
36
+ return "mojang";
37
+ }
38
+ return mappingApplied;
39
+ }
12
40
  function normalizeStrictPositiveInt(value, field) {
13
41
  if (value == null) {
14
42
  return undefined;
@@ -35,15 +63,6 @@ function normalizeMemberAccess(access) {
35
63
  details: { access }
36
64
  });
37
65
  }
38
- function buildResolveArtifactParams(target, extra = {}) {
39
- return {
40
- target: {
41
- kind: target.kind,
42
- value: target.value
43
- },
44
- ...extra
45
- };
46
- }
47
66
  function looksLikeDeobfuscatedClassName(value) {
48
67
  const trimmed = value.trim();
49
68
  if (!trimmed) {
@@ -221,13 +240,15 @@ export function buildDecompiledFallback(svc, artifactId, lookupClassName, member
221
240
  if (!memberPattern) {
222
241
  return list;
223
242
  }
224
- const lower = memberPattern.toLowerCase();
225
- return list.filter((entry) => entry.name.toLowerCase().includes(lower));
243
+ return list.filter((entry) => matchesMemberPattern(entry.name, memberPattern));
226
244
  };
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,31 @@ 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
281
+ };
282
+ }
283
+ /**
284
+ * Apply a member projection to the decompiled fallback so it honors the same
285
+ * `projection` contract as the bytecode members block. The fallback only carries
286
+ * names + source line + kind (no signatures), so any non-"full" level reduces to
287
+ * the member name (the array it lives in already conveys the kind). Keeps the
288
+ * full shape for the default "full" level.
289
+ */
290
+ function projectDecompiledFallback(fallback, level) {
291
+ if (level === "full") {
292
+ return fallback;
293
+ }
294
+ const reduce = (member) => ({ name: member.name });
295
+ return {
296
+ constructors: fallback.constructors.map(reduce),
297
+ fields: fallback.fields.map(reduce),
298
+ methods: fallback.methods.map(reduce),
299
+ origin: fallback.origin
259
300
  };
260
301
  }
261
302
  // The class-like symbol kinds findClass returns. MUST stay in sync with the JS-side
@@ -444,7 +485,7 @@ export async function getClassSource(svc, input) {
444
485
  const artifact = svc.getArtifact(artifactId);
445
486
  artifactId = artifact.artifactId;
446
487
  origin = artifact.origin;
447
- requestedMapping = artifact.requestedMapping ?? requestedMapping;
488
+ requestedMapping = input.mapping != null ? requestedMapping : (artifact.requestedMapping ?? requestedMapping);
448
489
  mappingApplied = artifact.mappingApplied ?? requestedMapping;
449
490
  provenance = artifact.provenance;
450
491
  qualityFlags = artifact.qualityFlags;
@@ -461,6 +502,7 @@ export async function getClassSource(svc, input) {
461
502
  preferProjectVersion: input.preferProjectVersion,
462
503
  warnings
463
504
  });
505
+ mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
464
506
  let activeArtifactId = artifactId;
465
507
  let activeOrigin = origin;
466
508
  let activeProvenance = provenance;
@@ -624,11 +666,25 @@ export async function getClassSource(svc, input) {
624
666
  mode,
625
667
  startLine: nextStartLine,
626
668
  ...(input.endLine != null ? { endLine: input.endLine } : {}),
627
- ...(input.maxLines != null ? { maxLines: input.maxLines } : {}),
669
+ ...(maxLines != null ? { maxLines } : {}),
628
670
  ...(input.maxChars != null ? { maxChars: input.maxChars } : {})
629
671
  }
630
672
  })
631
673
  : undefined;
674
+ // Decompiled source text can use different method/accessor names than the jar
675
+ // the workspace actually compiles against (e.g. `getGameRenderState()` in the
676
+ // decompiled source vs `gameRenderState()` in the runtime jar). Flag it so
677
+ // callers verify names against get-class-members (bytecode-derived) before
678
+ // copying signatures out of this source.
679
+ if (activeOrigin === "decompiled") {
680
+ activeQualityFlags = dedupeQualityFlags([
681
+ ...activeQualityFlags,
682
+ "decompiled-source-signatures-unverified"
683
+ ]);
684
+ warnings.push("Source is decompiled: method/accessor names may differ from the jar the workspace "
685
+ + "compiles against. Confirm signatures with get-class-members (bytecode-derived) "
686
+ + "before copying names from this source.");
687
+ }
632
688
  return {
633
689
  className,
634
690
  mode,
@@ -746,6 +802,7 @@ export async function getClassMembers(svc, input) {
746
802
  preferProjectVersion: input.preferProjectVersion,
747
803
  warnings
748
804
  });
805
+ mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
749
806
  if (requestedMapping !== "obfuscated" && !version) {
750
807
  throw createError({
751
808
  code: ERROR_CODES.MAPPING_NOT_APPLIED,
@@ -753,10 +810,7 @@ export async function getClassMembers(svc, input) {
753
810
  details: {
754
811
  mapping: requestedMapping,
755
812
  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
- })
813
+ ...buildSuggestedCall({ tool: "list-versions", params: {} })
760
814
  }
761
815
  });
762
816
  }
@@ -868,7 +922,8 @@ export async function getClassMembers(svc, input) {
868
922
  // Slim the wire member shape: hoist a shared ownerFqn, drop accessFlags, omit
869
923
  // isSynthetic:false, and drop FIELD jvmDescriptor unless includeDescriptors.
870
924
  // Internal SignatureMember arrays above stay intact.
871
- const projectedMembers = projectMembersForWire({ constructors, fields, methods }, includeInherited, input.includeDescriptors ?? false);
925
+ const projection = input.projection ?? "full";
926
+ const projectedMembers = projectMembersByLevel(projectMembersForWire({ constructors, fields, methods }, includeInherited, input.includeDescriptors ?? false), projection);
872
927
  const truncated = sliced.truncated;
873
928
  const nextCursor = sliced.nextOffset != null ? encodeOffsetCursor(sliced.nextOffset, memberCursorContext) : undefined;
874
929
  const normalizedProvenance = provenance ??
@@ -886,7 +941,7 @@ export async function getClassMembers(svc, input) {
886
941
  const fallbackPattern = namespaceMismatch ? undefined : memberPattern;
887
942
  const sourceFallback = buildDecompiledFallback(svc, artifactId, lookupClassName, fallbackPattern, maxMembers);
888
943
  if (sourceFallback) {
889
- decompiledFallback = sourceFallback.fallback;
944
+ decompiledFallback = projectDecompiledFallback(sourceFallback.fallback, projection);
890
945
  decompiledMemberCounts = sourceFallback.counts;
891
946
  fallbackQualityFlags = dedupeQualityFlags([
892
947
  ...qualityFlags,
@@ -898,6 +953,12 @@ export async function getClassMembers(svc, input) {
898
953
  warnings.push("Bytecode member enumeration returned zero; populated decompiledFallback from decompiled source. "
899
954
  + "Descriptors and access modifiers are unavailable — use get-class-source for full details."
900
955
  + namespaceNote);
956
+ if (sourceFallback.truncated) {
957
+ const returnedTotal = decompiledFallback.constructors.length
958
+ + decompiledFallback.fields.length
959
+ + decompiledFallback.methods.length;
960
+ warnings.push(`Member list was truncated to ${returnedTotal} entries (from ${sourceFallback.counts.total}).`);
961
+ }
901
962
  if (namespaceMismatch && memberPattern) {
902
963
  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
964
  }
@@ -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." — a 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",
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Match a class-member name against a `memberPattern` filter.
3
+ *
4
+ * Matching is case-insensitive substring. A `|` splits the pattern into OR
5
+ * alternatives, so `"getStateForPlacement|canSurvive|setPlacedBy"` matches a
6
+ * name that contains ANY of those tokens. Before this, the pattern was matched
7
+ * as a single literal substring, so a piped pattern searched for a name
8
+ * containing a literal `|` and matched nothing — which made member listing
9
+ * return zero on large vanilla classes when callers used the natural OR syntax.
10
+ *
11
+ * Java member names never contain `|`, so treating `|` as OR never collides with
12
+ * a legitimate substring search. Empty or whitespace-only alternatives are
13
+ * dropped; a pattern with no usable alternative matches nothing (a non-empty
14
+ * filter that excludes everything), matching the prior empty-filter behavior.
15
+ */
16
+ export declare function matchesMemberPattern(name: string, pattern: string): boolean;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Match a class-member name against a `memberPattern` filter.
3
+ *
4
+ * Matching is case-insensitive substring. A `|` splits the pattern into OR
5
+ * alternatives, so `"getStateForPlacement|canSurvive|setPlacedBy"` matches a
6
+ * name that contains ANY of those tokens. Before this, the pattern was matched
7
+ * as a single literal substring, so a piped pattern searched for a name
8
+ * containing a literal `|` and matched nothing — which made member listing
9
+ * return zero on large vanilla classes when callers used the natural OR syntax.
10
+ *
11
+ * Java member names never contain `|`, so treating `|` as OR never collides with
12
+ * a legitimate substring search. Empty or whitespace-only alternatives are
13
+ * dropped; a pattern with no usable alternative matches nothing (a non-empty
14
+ * filter that excludes everything), matching the prior empty-filter behavior.
15
+ */
16
+ export function matchesMemberPattern(name, pattern) {
17
+ const alternatives = pattern
18
+ .split("|")
19
+ .map((part) => part.trim().toLowerCase())
20
+ .filter((part) => part.length > 0);
21
+ if (alternatives.length === 0) {
22
+ return false;
23
+ }
24
+ const lowerName = name.toLowerCase();
25
+ return alternatives.some((alt) => lowerName.includes(alt));
26
+ }
27
+ //# sourceMappingURL=member-pattern.js.map
@@ -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,
@@ -11,7 +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
+ import type { MemberProjection, ProjectedMembersBlock } from "./source/class-source/members-builder.js";
15
15
  import { type StageEmitter } from "./stage-emitter.js";
16
16
  import { WorkspaceMappingService, type WorkspaceCompileMappingOutput } from "./workspace-mapping-service.js";
17
17
  import { type WorkspaceContextCache } from "./workspace-context-cache.js";
@@ -266,11 +266,15 @@ export type GetClassMembersInput = {
266
266
  strictVersion?: boolean;
267
267
  /** When true, keep jvmDescriptor on FIELD members (always kept on methods/constructors). */
268
268
  includeDescriptors?: boolean;
269
+ /** Narrow the per-member fields returned: "names" | "signatures" | "full" (default "full"). */
270
+ projection?: MemberProjection;
269
271
  };
270
272
  export type DecompiledMember = {
271
273
  name: string;
272
- line: number;
273
- kind: "constructor" | "field" | "method";
274
+ /** Source line of the declaration. Omitted under a non-"full" projection. */
275
+ line?: number;
276
+ /** Member kind. Omitted under a non-"full" projection (the array conveys it). */
277
+ kind?: "constructor" | "field" | "method";
274
278
  };
275
279
  export type DecompiledFallback = {
276
280
  constructors: DecompiledMember[];
@@ -281,7 +285,7 @@ export type DecompiledFallback = {
281
285
  export type GetClassMembersStatus = "ok" | "members_unavailable" | "partial";
282
286
  export type GetClassMembersOutput = {
283
287
  className: string;
284
- members: WireMembersBlock;
288
+ members: ProjectedMembersBlock;
285
289
  counts: {
286
290
  constructors: number;
287
291
  fields: number;
@@ -141,7 +141,15 @@ export class SourceService {
141
141
  }
142
142
  async checkSymbolExists(input) {
143
143
  const result = await this.mappingService.checkSymbolExists(input);
144
- if (result.status !== "mapping_unavailable" ||
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. 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;