@adhisang/minecraft-modding-mcp 6.3.0 → 7.0.0-rc.1

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 (101) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +13 -3
  3. package/dist/cache-policy.d.ts +71 -0
  4. package/dist/cache-policy.js +83 -0
  5. package/dist/cache-registry.js +45 -8
  6. package/dist/cli.js +74 -3
  7. package/dist/compat-stdio-transport.d.ts +1 -1
  8. package/dist/compat-stdio-transport.js +13 -1
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +8 -2
  11. package/dist/decompiler/vineflower.d.ts +1 -0
  12. package/dist/decompiler/vineflower.js +8 -5
  13. package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
  14. package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
  15. package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
  16. package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
  17. package/dist/entry-tools/entry-tool-schema.js +4 -1
  18. package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
  19. package/dist/entry-tools/inspect-minecraft/internal.js +50 -13
  20. package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
  21. package/dist/entry-tools/manage-cache-service.d.ts +81 -91
  22. package/dist/entry-tools/validate-project/cases/mixin.js +26 -6
  23. package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
  24. package/dist/entry-tools/validate-project-service.d.ts +164 -592
  25. package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
  26. package/dist/entry-tools/verify-mixin-target-service.js +19 -1
  27. package/dist/era-classifier.d.ts +161 -0
  28. package/dist/era-classifier.js +292 -0
  29. package/dist/error-mapping.d.ts +76 -0
  30. package/dist/error-mapping.js +116 -8
  31. package/dist/index.d.ts +42 -4
  32. package/dist/index.js +636 -473
  33. package/dist/java-process.d.ts +2 -0
  34. package/dist/java-process.js +22 -2
  35. package/dist/json-rpc-framing.d.ts +77 -1
  36. package/dist/json-rpc-framing.js +249 -13
  37. package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
  38. package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
  39. package/dist/mapping/loaders/tiny-loom.js +45 -33
  40. package/dist/mapping/loaders/tiny-maven.js +6 -11
  41. package/dist/mapping/parsers/tiny.d.ts +57 -0
  42. package/dist/mapping/parsers/tiny.js +99 -22
  43. package/dist/mapping-service.d.ts +19 -0
  44. package/dist/mapping-service.js +93 -9
  45. package/dist/maven-resolver.d.ts +18 -0
  46. package/dist/maven-resolver.js +20 -0
  47. package/dist/mcp-helpers.d.ts +19 -2
  48. package/dist/mcp-helpers.js +58 -9
  49. package/dist/minecraft-explorer-service.d.ts +1 -1
  50. package/dist/mixin/types.d.ts +8 -0
  51. package/dist/mod-analyzer.js +7 -7
  52. package/dist/mod-decompile-service.js +1 -0
  53. package/dist/nbt/java-nbt-codec.js +12 -2
  54. package/dist/nbt/json-patch.js +14 -3
  55. package/dist/nbt/pipeline.js +40 -3
  56. package/dist/nbt/typed-json.js +26 -1
  57. package/dist/registration-adapter.d.ts +32 -0
  58. package/dist/registration-adapter.js +52 -0
  59. package/dist/repo-downloader.d.ts +165 -0
  60. package/dist/repo-downloader.js +568 -10
  61. package/dist/request-context.d.ts +7 -0
  62. package/dist/request-context.js +9 -0
  63. package/dist/resources.d.ts +1 -1
  64. package/dist/resources.js +25 -19
  65. package/dist/server-identity.d.ts +27 -0
  66. package/dist/server-identity.js +26 -0
  67. package/dist/source/access-validate.js +53 -0
  68. package/dist/source/artifact-resolver.d.ts +81 -2
  69. package/dist/source/artifact-resolver.js +227 -15
  70. package/dist/source/class-source.d.ts +36 -0
  71. package/dist/source/class-source.js +222 -38
  72. package/dist/source/did-you-mean.d.ts +12 -1
  73. package/dist/source/did-you-mean.js +6 -2
  74. package/dist/source/file-access.js +150 -46
  75. package/dist/source/indexer.js +1 -0
  76. package/dist/source/shared-utils.d.ts +21 -0
  77. package/dist/source/shared-utils.js +23 -0
  78. package/dist/source-resolver.js +224 -57
  79. package/dist/source-service.d.ts +12 -1
  80. package/dist/stdio-supervisor.d.ts +357 -2
  81. package/dist/stdio-supervisor.js +1031 -80
  82. package/dist/storage/db.d.ts +2 -1
  83. package/dist/storage/db.js +15 -8
  84. package/dist/synthetic-decorator.d.ts +24 -0
  85. package/dist/synthetic-decorator.js +48 -0
  86. package/dist/tool-guidance.d.ts +17 -1
  87. package/dist/tool-guidance.js +323 -18
  88. package/dist/tool-schema-registry.d.ts +2 -0
  89. package/dist/tool-schema-registry.js +4 -0
  90. package/dist/tool-schemas.d.ts +2212 -3919
  91. package/dist/tool-schemas.js +33 -7
  92. package/dist/types.d.ts +35 -0
  93. package/dist/v1-parity-schemas.d.ts +7 -0
  94. package/dist/v1-parity-schemas.js +5584 -0
  95. package/dist/version-diff-service.d.ts +33 -0
  96. package/dist/version-diff-service.js +148 -3
  97. package/dist/version-service.js +36 -14
  98. package/dist/warning-details.js +18 -1
  99. package/docs/README-ja.md +5 -3
  100. package/docs/tool-reference.md +196 -19
  101. package/package.json +13 -8
@@ -10,7 +10,7 @@ import { collectDidYouMeanCandidates } from "./did-you-mean.js";
10
10
  import { matchesMemberPattern } from "./member-pattern.js";
11
11
  import { findNestedJarClasses, resolveUniqueNestedJarForClass } from "./nested-jars.js";
12
12
  import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
13
- import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
13
+ import { dedupeQualityFlags, inheritArtifactMapping, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
14
14
  import { isUnobfuscatedVersion } from "../version-service.js";
15
15
  const MEMBERS_STATUS_LEGACY = process.env.MEMBERS_STATUS_LEGACY === "1";
16
16
  /**
@@ -82,13 +82,35 @@ function obfuscatedNamespaceHint(className) {
82
82
  function hasPartialNetMinecraftCoverage(qualityFlags) {
83
83
  return qualityFlags.includes("partial-source-no-net-minecraft");
84
84
  }
85
+ /**
86
+ * Whether "this artifact is indexed in obfuscated names, ask for mapping=mojang"
87
+ * is TRUE for this artifact, from the pieces of it the caller sees.
88
+ *
89
+ * Two artifact kinds report `mappingApplied: "obfuscated"` without being an
90
+ * obfuscated Minecraft index, and the hint is simply false for them:
91
+ * - a native dependency artifact, where the resolver SUBSTITUTES "obfuscated"
92
+ * after the mapping pipeline declines (its class names are already the
93
+ * library's own, so no mapping exists to ask for), and
94
+ * - a shell jar, which holds no classes at all — its content lives in the
95
+ * nested jars, and no mapping choice can change that.
96
+ *
97
+ * `looksLikeDeobfuscatedClassName` matches any capitalized simple name, so
98
+ * without these exclusions an ordinary library class such as "GameTest"
99
+ * qualifies and the caller is told to remap a jar that was never obfuscated.
100
+ */
101
+ function isObfuscatedNamespaceHintTrue(input) {
102
+ return (input.mappingApplied === "obfuscated" &&
103
+ !input.nativeDependency &&
104
+ !input.qualityFlags.includes("shell-jar") &&
105
+ looksLikeDeobfuscatedClassName(input.className));
106
+ }
85
107
  function shouldSuggestObfuscatedMapping(artifact, className) {
86
- const nativeDependency = artifact.provenance?.dependencyResolution != null;
87
- const shellArtifact = artifact.qualityFlags.includes("shell-jar");
88
- return (artifact.mappingApplied === "obfuscated" &&
89
- !nativeDependency &&
90
- !shellArtifact &&
91
- looksLikeDeobfuscatedClassName(className));
108
+ return isObfuscatedNamespaceHintTrue({
109
+ mappingApplied: artifact.mappingApplied,
110
+ qualityFlags: artifact.qualityFlags,
111
+ nativeDependency: artifact.provenance?.dependencyResolution != null,
112
+ className
113
+ });
92
114
  }
93
115
  function classNameToClassPath(className) {
94
116
  const normalized = normalizePathStyle(className.trim()).replace(/\//g, ".");
@@ -175,10 +197,26 @@ export function buildFallbackProvenance(svc, input) {
175
197
  transformChain
176
198
  };
177
199
  }
200
+ /**
201
+ * Near-miss candidates from the requested artifact's index followed by any the
202
+ * lookup's final artifact contributes, deduplicated by FQN so a class present in
203
+ * both is reported once, under the requested artifact.
204
+ */
205
+ function unionDidYouMeanCandidates(svc, requestedArtifactId, endedOnArtifactId, className) {
206
+ const requested = collectDidYouMeanCandidates(svc, requestedArtifactId, className);
207
+ if (endedOnArtifactId === requestedArtifactId) {
208
+ return requested;
209
+ }
210
+ const seen = new Set(requested.map((candidate) => candidate.className));
211
+ const fromFallback = collectDidYouMeanCandidates(svc, endedOnArtifactId, className, endedOnArtifactId).filter((candidate) => !seen.has(candidate.className));
212
+ return [...requested, ...fromFallback];
213
+ }
178
214
  export function buildClassSourceNotFoundError(svc, input) {
179
215
  const simpleName = input.className.split(/[.$]/).at(-1) ?? input.className;
216
+ const requestedArtifactId = input.requestedArtifactId ?? input.artifactId;
180
217
  const details = {
181
- artifactId: input.artifactId,
218
+ artifactId: requestedArtifactId,
219
+ ...(input.artifactId !== requestedArtifactId ? { fallbackArtifactId: input.artifactId } : {}),
182
220
  className: input.className,
183
221
  mapping: input.mappingApplied,
184
222
  qualityFlags: input.qualityFlags,
@@ -191,12 +229,18 @@ export function buildClassSourceNotFoundError(svc, input) {
191
229
  ...(input.nestedJars && input.nestedJars.length > 0 ? { nestedJars: input.nestedJars } : {}),
192
230
  // Candidates are hints from the symbol index, never assertions that the
193
231
  // class exists at the suggested location; empty when nothing usable.
194
- didYouMean: collectDidYouMeanCandidates(svc, input.artifactId, input.className)
232
+ //
233
+ // Both indexes are consulted, requested artifact first. Collecting from the
234
+ // requested artifact ALONE is empty by construction in the scenario the
235
+ // partial-source fallback exists to serve: that artifact is the one without
236
+ // net.minecraft symbols, which is why the fallback fired and indexed the other
237
+ // jar. Candidates from the artifact the caller did not name carry its id.
238
+ didYouMean: unionDidYouMeanCandidates(svc, requestedArtifactId, input.artifactId, input.className)
195
239
  };
196
240
  let nextAction = `Use find-class to resolve the correct fully-qualified name for "${simpleName}".`;
197
241
  let suggestionSpec = {
198
242
  tool: "find-class",
199
- params: { className: simpleName, artifactId: input.artifactId }
243
+ params: { className: simpleName, artifactId: requestedArtifactId }
200
244
  };
201
245
  if (input.targetKind === "version" && input.scope && input.scope !== "merged" && !input.projectPath) {
202
246
  nextAction +=
@@ -225,11 +269,32 @@ export function buildClassSourceNotFoundError(svc, input) {
225
269
  else {
226
270
  suggestionSpec = {
227
271
  tool: "find-class",
228
- params: { className: simpleName, artifactId: input.artifactId }
272
+ params: { className: simpleName, artifactId: requestedArtifactId }
229
273
  };
230
274
  }
231
275
  }
232
- if (input.mappingApplied === "obfuscated" && looksLikeDeobfuscatedClassName(input.className)) {
276
+ // Two gates, because the hint fails in two independent ways.
277
+ //
278
+ // `isObfuscatedNamespaceHintTrue` decides whether the claim is even true of
279
+ // this artifact — a dependency jar or a shell jar reports "obfuscated"
280
+ // without being an obfuscated index, and used to be told to remap itself.
281
+ //
282
+ // The second gate is about the ASK, not the claim: the hint ends in
283
+ // `mapping="mojang"`, so firing it at a caller who already sent a
284
+ // non-obfuscated mapping asks for the argument they just supplied and sends
285
+ // them round the same call again. The generic backstop in
286
+ // `dropSatisfiedParameterAsks` cannot rescue this one — it matches an
287
+ // imperative "Provide/Pass mapping", and the sentence is concatenated into
288
+ // the same `nextAction` string as the find-class guidance, which is published
289
+ // as a single hint that no mid-string excision can repair.
290
+ const callerAskedForNonObfuscatedMapping = input.callerSuppliedMapping != null && input.callerSuppliedMapping !== "obfuscated";
291
+ if (!callerAskedForNonObfuscatedMapping &&
292
+ isObfuscatedNamespaceHintTrue({
293
+ mappingApplied: input.mappingApplied,
294
+ qualityFlags: input.qualityFlags,
295
+ nativeDependency: input.nativeDependency === true,
296
+ className: input.className
297
+ })) {
233
298
  nextAction += ` ${obfuscatedNamespaceHint(input.className)}`;
234
299
  }
235
300
  details.nextAction = nextAction;
@@ -403,13 +468,14 @@ export function findClass(svc, input) {
403
468
  const filteredMatches = partialVanillaLookup && matches.every((match) => !match.qualifiedName.startsWith("net.minecraft.") && !match.qualifiedName.startsWith("com.mojang."))
404
469
  ? []
405
470
  : matches;
406
- if (filteredMatches.length === 0 && partialVanillaLookup) {
407
- warnings.push(`Artifact source coverage is partial and excludes net.minecraft; returning non-vanilla matches for "${className}" would be misleading. Use get-class-source/get-class-members for binary fallback or get-class-api-matrix for mapped API inspection.`);
408
- }
409
- if (filteredMatches.length === 0 && shouldSuggestObfuscatedMapping(artifact, className)) {
410
- warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
411
- }
412
- return { matches: filteredMatches, total: filteredMatches.length, warnings };
471
+ return finishFindClass(svc, {
472
+ artifact,
473
+ artifactId,
474
+ className,
475
+ matches: filteredMatches,
476
+ partialVanillaLookup,
477
+ warnings
478
+ });
413
479
  }
414
480
  const result = svc.symbolsRepo.findScopedSymbols({
415
481
  artifactId,
@@ -418,32 +484,82 @@ export function findClass(svc, input) {
418
484
  symbolKinds: TYPE_SYMBOL_KINDS,
419
485
  limit: limit * 5
420
486
  });
421
- const matches = [];
487
+ // The extractor stores ONE qualifiedName per FILE (the top-level type), so a
488
+ // nested type's row carries its OUTER type's FQN. Reporting that verbatim
489
+ // returned a qualifiedName that did not contain the searched token at all —
490
+ // feeding match[0] into get-class-source then fetched the wrong class.
491
+ const candidates = [];
422
492
  for (const row of result.items) {
423
- if (matches.length >= limit)
424
- break;
425
493
  const isTypeSymbol = row.symbolKind === "class" || row.symbolKind === "interface" ||
426
494
  row.symbolKind === "enum" || row.symbolKind === "record";
427
495
  if (!isTypeSymbol)
428
496
  continue;
429
- matches.push({
430
- qualifiedName: row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", "."),
497
+ const enclosingQualifiedName = row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", ".");
498
+ const enclosingSimpleName = enclosingQualifiedName.split(".").at(-1) ?? enclosingQualifiedName;
499
+ const nested = enclosingSimpleName !== row.symbolName;
500
+ candidates.push({
501
+ qualifiedName: nested ? `${enclosingQualifiedName}.${row.symbolName}` : enclosingQualifiedName,
431
502
  filePath: row.filePath,
432
503
  line: row.line,
433
- symbolKind: row.symbolKind
504
+ symbolKind: row.symbolKind,
505
+ ...(nested ? { nested: true, enclosingClass: enclosingQualifiedName } : {})
434
506
  });
435
507
  }
508
+ // A top-level type named exactly like the query is what the caller almost
509
+ // always means; a nested type that merely shares the simple name comes after.
510
+ candidates.sort((left, right) => {
511
+ const leftNested = left.nested === true ? 1 : 0;
512
+ const rightNested = right.nested === true ? 1 : 0;
513
+ if (leftNested !== rightNested)
514
+ return leftNested - rightNested;
515
+ return left.qualifiedName.localeCompare(right.qualifiedName);
516
+ });
517
+ const matches = candidates.slice(0, limit);
436
518
  const partialVanillaLookup = hasPartialNetMinecraftCoverage(artifact.qualityFlags) && looksLikeDeobfuscatedClassName(className);
437
519
  const filteredMatches = partialVanillaLookup && matches.every((match) => !match.qualifiedName.startsWith("net.minecraft.") && !match.qualifiedName.startsWith("com.mojang."))
438
520
  ? []
439
521
  : matches;
440
- if (filteredMatches.length === 0 && partialVanillaLookup) {
522
+ return finishFindClass(svc, {
523
+ artifact,
524
+ artifactId,
525
+ className,
526
+ matches: filteredMatches,
527
+ partialVanillaLookup,
528
+ warnings
529
+ });
530
+ }
531
+ /**
532
+ * Shared tail for both findClass branches: coverage/namespace warnings plus a
533
+ * machine-usable recovery route.
534
+ *
535
+ * An empty result on an artifact whose index excludes net.minecraft is
536
+ * internally consistent but externally contradictory — get-class-source answers
537
+ * the SAME class from the binary fallback. The caller therefore gets both the
538
+ * reason and the exact call that works.
539
+ */
540
+ function finishFindClass(svc, input) {
541
+ const { artifact, artifactId, className, matches, partialVanillaLookup, warnings } = input;
542
+ let suggestedCall;
543
+ if (matches.length === 0 && partialVanillaLookup) {
441
544
  warnings.push(`Artifact source coverage is partial and excludes net.minecraft; returning non-vanilla matches for "${className}" would be misleading. Use get-class-source/get-class-members for binary fallback or get-class-api-matrix for mapped API inspection.`);
545
+ suggestedCall = buildSuggestedCall({
546
+ tool: "get-class-source",
547
+ params: {
548
+ className,
549
+ target: { kind: "artifact", artifactId },
550
+ mode: "metadata"
551
+ }
552
+ }).suggestedCall;
442
553
  }
443
- if (filteredMatches.length === 0 && shouldSuggestObfuscatedMapping(artifact, className)) {
554
+ if (matches.length === 0 && shouldSuggestObfuscatedMapping(artifact, className)) {
444
555
  warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
445
556
  }
446
- return { matches: filteredMatches, total: filteredMatches.length, warnings };
557
+ return {
558
+ matches,
559
+ total: matches.length,
560
+ warnings,
561
+ ...(suggestedCall ? { suggestedCall } : {})
562
+ };
447
563
  }
448
564
  export async function findClassIncludingNested(svc, input) {
449
565
  const indexed = findClass(svc, input);
@@ -507,6 +623,9 @@ export async function getClassSource(svc, input) {
507
623
  }
508
624
  let artifactId = normalizedArtifactId;
509
625
  let origin = "local-jar";
626
+ // Derivation, tracked separately from origin: whether the indexed text was
627
+ // produced by decompiling bytecode. Only the persisted flag answers that.
628
+ let isDecompiled = false;
510
629
  let warnings = [];
511
630
  let requestedMapping = normalizeMapping(input.mapping);
512
631
  let mappingApplied = requestedMapping;
@@ -536,6 +655,7 @@ export async function getClassSource(svc, input) {
536
655
  });
537
656
  artifactId = resolved.artifactId;
538
657
  origin = resolved.origin;
658
+ isDecompiled = resolved.isDecompiled;
539
659
  warnings = [...resolved.warnings];
540
660
  requestedMapping = resolved.requestedMapping;
541
661
  mappingApplied = resolved.mappingApplied;
@@ -550,7 +670,8 @@ export async function getClassSource(svc, input) {
550
670
  const artifact = svc.getArtifact(artifactId);
551
671
  artifactId = artifact.artifactId;
552
672
  origin = artifact.origin;
553
- requestedMapping = input.mapping != null ? requestedMapping : (artifact.requestedMapping ?? requestedMapping);
673
+ isDecompiled = artifact.isDecompiled;
674
+ requestedMapping = inheritArtifactMapping(input.mapping, artifact);
554
675
  mappingApplied = artifact.mappingApplied ?? requestedMapping;
555
676
  provenance = artifact.provenance;
556
677
  qualityFlags = artifact.qualityFlags;
@@ -570,6 +691,7 @@ export async function getClassSource(svc, input) {
570
691
  mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
571
692
  let activeArtifactId = artifactId;
572
693
  let activeOrigin = origin;
694
+ let activeIsDecompiled = isDecompiled;
573
695
  let activeProvenance = provenance;
574
696
  let activeQualityFlags = [...qualityFlags];
575
697
  let activeMappingApplied = mappingApplied;
@@ -595,13 +717,18 @@ export async function getClassSource(svc, input) {
595
717
  requestedMapping,
596
718
  mappingApplied,
597
719
  provenance: activeProvenance,
598
- qualityFlags: activeQualityFlags
720
+ qualityFlags: activeQualityFlags,
721
+ // A caller who declined decompilation must not pay for one here: the
722
+ // fallback used to hardcode allowDecompile:true and silently ran a full
723
+ // Vineflower pass on the binary jar.
724
+ allowDecompile: input.allowDecompile
599
725
  });
600
726
  if (!fallbackResolved || fallbackResolved.artifactId === activeArtifactId) {
601
727
  return false;
602
728
  }
603
729
  activeArtifactId = fallbackResolved.artifactId;
604
730
  activeOrigin = fallbackResolved.origin;
731
+ activeIsDecompiled = fallbackResolved.isDecompiled;
605
732
  activeMappingApplied = fallbackResolved.mappingApplied ?? activeMappingApplied;
606
733
  activeProvenance = fallbackResolved.provenance ?? activeProvenance;
607
734
  activeQualityFlags = dedupeQualityFlags([...(fallbackResolved.qualityFlags ?? []), "binary-fallback"]);
@@ -644,6 +771,9 @@ export async function getClassSource(svc, input) {
644
771
  });
645
772
  activeArtifactId = redirectResolved.artifactId;
646
773
  activeOrigin = redirectResolved.origin;
774
+ // The INNER jar's derivation: the shell that bundled it holds no source of
775
+ // its own, so its flag says nothing about the text being returned here.
776
+ activeIsDecompiled = redirectResolved.isDecompiled;
647
777
  activeMappingApplied = redirectResolved.mappingApplied ?? activeMappingApplied;
648
778
  activeProvenance = redirectResolved.provenance
649
779
  ? {
@@ -689,11 +819,17 @@ export async function getClassSource(svc, input) {
689
819
  if (!filePath) {
690
820
  throw buildClassSourceNotFoundError(svc, {
691
821
  artifactId: activeArtifactId,
822
+ requestedArtifactId: artifactId,
692
823
  className,
693
824
  lookupClassName: activeLookupClassName,
694
- mappingApplied: activeMappingApplied,
825
+ // The REQUESTED artifact's namespace and quality, matching the artifactId this
826
+ // error reports. The nested-jar redirect replaces the active values with the
827
+ // inner jar's, which would otherwise describe an artifact the caller never named.
828
+ mappingApplied,
695
829
  requestedMapping,
696
- qualityFlags: activeQualityFlags,
830
+ qualityFlags,
831
+ callerSuppliedMapping: input.mapping,
832
+ nativeDependency: provenance?.dependencyResolution != null,
697
833
  attemptedBinaryFallback,
698
834
  targetKind: input.target?.kind,
699
835
  targetValue: input.target && "value" in input.target ? input.target.value : undefined,
@@ -728,11 +864,17 @@ export async function getClassSource(svc, input) {
728
864
  if (!row) {
729
865
  throw buildClassSourceNotFoundError(svc, {
730
866
  artifactId: activeArtifactId,
867
+ requestedArtifactId: artifactId,
731
868
  className,
732
869
  lookupClassName: activeLookupClassName,
733
- mappingApplied: activeMappingApplied,
870
+ // The REQUESTED artifact's namespace and quality, matching the artifactId this
871
+ // error reports. The nested-jar redirect replaces the active values with the
872
+ // inner jar's, which would otherwise describe an artifact the caller never named.
873
+ mappingApplied,
734
874
  requestedMapping,
735
- qualityFlags: activeQualityFlags,
875
+ qualityFlags,
876
+ callerSuppliedMapping: input.mapping,
877
+ nativeDependency: provenance?.dependencyResolution != null,
736
878
  attemptedBinaryFallback,
737
879
  filePath,
738
880
  targetKind: input.target?.kind,
@@ -800,7 +942,16 @@ export async function getClassSource(svc, input) {
800
942
  // decompiled source vs `gameRenderState()` in the runtime jar). Flag it so
801
943
  // callers verify names against get-class-members (bytecode-derived) before
802
944
  // copying signatures out of this source.
803
- if (activeOrigin === "decompiled") {
945
+ //
946
+ // The gate is the persisted `isDecompiled` flag, never `origin === "decompiled"`:
947
+ // origin records WHERE the bytes came from, derivation records WHETHER the text
948
+ // was decompiled, and the two disagree. A Jar-in-Jar shell keeps the origin the
949
+ // resolver chose while ingest clears its derivation flag (src/source/indexer.ts),
950
+ // so an origin-keyed gate warned about decompiled signatures for a jar that holds
951
+ // no source at all. file-access, search, artifact-resolver and the mapping
952
+ // pipeline all read the boolean; do not "restore consistency" by keying this
953
+ // back on the origin.
954
+ if (activeIsDecompiled) {
804
955
  activeQualityFlags = dedupeQualityFlags([
805
956
  ...activeQualityFlags,
806
957
  "decompiled-source-signatures-unverified"
@@ -832,7 +983,7 @@ export async function getClassSource(svc, input) {
832
983
  artifactContents: svc.buildArtifactContentsSummary({
833
984
  origin: activeOrigin,
834
985
  sourceJarPath: activeSourceJarPath,
835
- isDecompiled: activeOrigin === "decompiled",
986
+ isDecompiled: activeIsDecompiled,
836
987
  qualityFlags: activeQualityFlags
837
988
  }),
838
989
  ...(continuation?.suggestedCall ? { suggestedCall: continuation.suggestedCall } : {}),
@@ -865,6 +1016,8 @@ export async function getClassMembers(svc, input) {
865
1016
  }
866
1017
  let artifactId = normalizedArtifactId;
867
1018
  let origin = "local-jar";
1019
+ // See getClassSource: derivation is the persisted flag, not the origin.
1020
+ let isDecompiled = false;
868
1021
  let warnings = [];
869
1022
  let mappingApplied = requestedMapping;
870
1023
  let provenance;
@@ -896,6 +1049,7 @@ export async function getClassMembers(svc, input) {
896
1049
  });
897
1050
  artifactId = resolved.artifactId;
898
1051
  origin = resolved.origin;
1052
+ isDecompiled = resolved.isDecompiled;
899
1053
  warnings.push(...resolved.warnings);
900
1054
  requestedMapping = resolved.requestedMapping;
901
1055
  mappingApplied = resolved.mappingApplied;
@@ -910,6 +1064,8 @@ export async function getClassMembers(svc, input) {
910
1064
  const artifact = svc.getArtifact(artifactId);
911
1065
  artifactId = artifact.artifactId;
912
1066
  origin = artifact.origin;
1067
+ isDecompiled = artifact.isDecompiled;
1068
+ requestedMapping = inheritArtifactMapping(input.mapping, artifact);
913
1069
  mappingApplied = artifact.mappingApplied ?? requestedMapping;
914
1070
  provenance = artifact.provenance;
915
1071
  qualityFlags = artifact.qualityFlags;
@@ -939,12 +1095,27 @@ export async function getClassMembers(svc, input) {
939
1095
  });
940
1096
  }
941
1097
  if (!binaryJarPath) {
1098
+ // The override below applies only when the TOOL picked this artifact
1099
+ // without the caller expressing an opinion on it: a version/coordinate
1100
+ // target, or falling through the workspace/dependency shapes. A caller who
1101
+ // passed `artifactId` directly, or `target: { kind: "jar", ... }`, named
1102
+ // the exact artifact/jar themselves - having no binary companion is then
1103
+ // exactly the input they can change.
1104
+ const artifactWasCallerNamed = Boolean(normalizedArtifactId) || input.target?.kind === "jar";
942
1105
  throw createError({
943
1106
  code: ERROR_CODES.CONTEXT_UNRESOLVED,
944
1107
  message: `Class members require a binary jar, but artifact "${artifactId}" has no binaryJarPath.`,
945
1108
  details: {
946
1109
  artifactId,
947
1110
  className,
1111
+ // `ERR_CONTEXT_UNRESOLVED` classifies as `code_issue` by code, which is
1112
+ // right for the sibling case (a caller naming a version no artifact
1113
+ // carries) and wrong when the artifact was resolved by the TOOL, since
1114
+ // whether it carries a binary jar is not something that request could
1115
+ // express. Published as caller-fixable it invites an endless retry of
1116
+ // an input that was never at fault, so this site overrides the default
1117
+ // for the tool-resolved case only.
1118
+ issueOrigin: artifactWasCallerNamed ? undefined : "tool_issue",
948
1119
  nextAction: "Resolve with target: { kind: \"jar\" | \"version\", value: ... } or use an artifact that has a binary jar."
949
1120
  }
950
1121
  });
@@ -1037,6 +1208,8 @@ export async function getClassMembers(svc, input) {
1037
1208
  mappingApplied,
1038
1209
  requestedMapping,
1039
1210
  qualityFlags,
1211
+ callerSuppliedMapping: input.mapping,
1212
+ nativeDependency: provenance?.dependencyResolution != null,
1040
1213
  attemptedBinaryFallback: true,
1041
1214
  targetKind: input.target?.kind,
1042
1215
  targetValue: input.target && "value" in input.target ? input.target.value : undefined,
@@ -1179,7 +1352,18 @@ export async function getClassMembers(svc, input) {
1179
1352
  truncated,
1180
1353
  ...(nextCursor ? { nextCursor } : {}),
1181
1354
  ...(memberCursorIgnored ? { cursorIgnored: true } : {}),
1182
- context: signatureContext,
1355
+ context: {
1356
+ ...signatureContext,
1357
+ // The jar-derived context describes the BYTECODE the reader opened. The
1358
+ // members handed back have already been remapped into requestedMapping,
1359
+ // so echoing the jar namespace here contradicted `returnedNamespace` in
1360
+ // the same payload. Report what the response actually contains, and fill
1361
+ // the version the resolver established when the jar path yielded none.
1362
+ ...(signatureContext.minecraftVersion === "unknown" && version
1363
+ ? { minecraftVersion: version }
1364
+ : {}),
1365
+ mappingNamespace: requestedMapping
1366
+ },
1183
1367
  origin,
1184
1368
  artifactId,
1185
1369
  requestedMapping,
@@ -1190,7 +1374,7 @@ export async function getClassMembers(svc, input) {
1190
1374
  artifactContents: svc.buildArtifactContentsSummary({
1191
1375
  origin,
1192
1376
  sourceJarPath,
1193
- isDecompiled: origin === "decompiled",
1377
+ isDecompiled,
1194
1378
  qualityFlags: fallbackQualityFlags
1195
1379
  }),
1196
1380
  ...(decompiledFallback ? { decompiledFallback } : {}),
@@ -2,6 +2,13 @@ import type { SourceService } from "../source-service.js";
2
2
  export interface DidYouMeanCandidate {
3
3
  className: string;
4
4
  matchReason: string;
5
+ /**
6
+ * Set only when the candidate was found in an artifact OTHER than the one the caller
7
+ * named — today, the internal binary fallback or nested-jar redirect the lookup ended
8
+ * on. An unattributed candidate always means "in the artifact you asked about", so the
9
+ * field marks the exception rather than restating the common case on every entry.
10
+ */
11
+ artifactId?: string;
5
12
  }
6
13
  /**
7
14
  * Collects ranked near-miss class candidates from the artifact's symbol
@@ -10,5 +17,9 @@ export interface DidYouMeanCandidate {
10
17
  * edit-distance suggestions. Candidates are hints, never assertions that the
11
18
  * class exists at the suggested location. Returns an empty array when the
12
19
  * index has nothing usable.
20
+ *
21
+ * `attributeTo` stamps every produced candidate with that artifact id; pass it when
22
+ * collecting from an artifact the caller did not name, and leave it unset for the
23
+ * requested artifact.
13
24
  */
14
- export declare function collectDidYouMeanCandidates(svc: SourceService, artifactId: string, className: string): DidYouMeanCandidate[];
25
+ export declare function collectDidYouMeanCandidates(svc: SourceService, artifactId: string, className: string, attributeTo?: string): DidYouMeanCandidate[];
@@ -15,8 +15,12 @@ function fqnOfRow(row) {
15
15
  * edit-distance suggestions. Candidates are hints, never assertions that the
16
16
  * class exists at the suggested location. Returns an empty array when the
17
17
  * index has nothing usable.
18
+ *
19
+ * `attributeTo` stamps every produced candidate with that artifact id; pass it when
20
+ * collecting from an artifact the caller did not name, and leave it unset for the
21
+ * requested artifact.
18
22
  */
19
- export function collectDidYouMeanCandidates(svc, artifactId, className) {
23
+ export function collectDidYouMeanCandidates(svc, artifactId, className, attributeTo) {
20
24
  try {
21
25
  const simpleName = className.split(/[.$]/).at(-1) ?? className;
22
26
  if (!simpleName) {
@@ -29,7 +33,7 @@ export function collectDidYouMeanCandidates(svc, artifactId, className) {
29
33
  return;
30
34
  }
31
35
  seen.add(fqn);
32
- out.push({ className: fqn, matchReason });
36
+ out.push({ className: fqn, matchReason, ...(attributeTo ? { artifactId: attributeTo } : {}) });
33
37
  };
34
38
  const exact = svc.symbolsRepo.findScopedSymbols({
35
39
  artifactId,