@adhisang/minecraft-modding-mcp 7.0.0-rc.3 → 7.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 (60) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +3 -2
  3. package/dist/cache-registry.d.ts +16 -0
  4. package/dist/cache-registry.js +78 -10
  5. package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
  6. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
  7. package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
  8. package/dist/entry-tools/batch-class-members-service.js +20 -6
  9. package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
  10. package/dist/entry-tools/batch-class-source-service.js +10 -0
  11. package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
  12. package/dist/entry-tools/compare-minecraft-service.js +65 -4
  13. package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
  14. package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
  15. package/dist/entry-tools/manage-cache-service.d.ts +2 -2
  16. package/dist/entry-tools/validate-project/cases/project-summary.js +71 -12
  17. package/dist/entry-tools/validate-project-service.d.ts +2 -2
  18. package/dist/index.js +37 -15
  19. package/dist/json-rpc-framing.d.ts +20 -0
  20. package/dist/json-rpc-framing.js +80 -7
  21. package/dist/mapping/loaders/tiny-maven.d.ts +9 -0
  22. package/dist/mapping/loaders/tiny-maven.js +10 -2
  23. package/dist/repo-downloader.js +13 -2
  24. package/dist/source/artifact-resolver.d.ts +14 -0
  25. package/dist/source/artifact-resolver.js +106 -12
  26. package/dist/source/class-source/members-builder.d.ts +7 -0
  27. package/dist/source/class-source/members-builder.js +4 -1
  28. package/dist/source/class-source.d.ts +9 -2
  29. package/dist/source/class-source.js +229 -26
  30. package/dist/source/indexer.js +69 -1
  31. package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
  32. package/dist/source/lifecycle/mapping-helpers.js +29 -3
  33. package/dist/source/lifecycle/runtime-check.d.ts +25 -0
  34. package/dist/source/lifecycle/runtime-check.js +68 -39
  35. package/dist/source/symbol-resolver.js +88 -0
  36. package/dist/source-jar-reader.d.ts +33 -0
  37. package/dist/source-jar-reader.js +58 -0
  38. package/dist/source-resolver.d.ts +7 -0
  39. package/dist/source-resolver.js +20 -5
  40. package/dist/source-service.d.ts +5 -0
  41. package/dist/source-service.js +7 -0
  42. package/dist/stdio-supervisor.js +193 -34
  43. package/dist/storage/db.d.ts +62 -2
  44. package/dist/storage/db.js +186 -21
  45. package/dist/storage/sqlite.d.ts +31 -1
  46. package/dist/storage/sqlite.js +125 -16
  47. package/dist/tool-guidance.js +4 -1
  48. package/dist/tool-schemas.d.ts +64 -52
  49. package/dist/tool-schemas.js +9 -7
  50. package/dist/types.d.ts +9 -0
  51. package/dist/v1-parity-schemas.js +36 -2
  52. package/dist/version-diff-service.d.ts +23 -0
  53. package/dist/version-diff-service.js +101 -0
  54. package/dist/version-service.d.ts +14 -0
  55. package/dist/version-service.js +45 -3
  56. package/dist/workspace-mapping-service.d.ts +8 -0
  57. package/dist/workspace-mapping-service.js +35 -7
  58. package/docs/README-ja.md +2 -0
  59. package/docs/tool-reference.md +55 -11
  60. package/package.json +1 -1
@@ -3,12 +3,14 @@ import { isAbsolute, resolve as resolvePath } from "node:path";
3
3
  import { buildSuggestedCall } from "../build-suggested-call.js";
4
4
  import { ERROR_CODES, createError, isAppError } from "../errors.js";
5
5
  import * as artifactResolver from "./artifact-resolver.js";
6
+ import { isUnobfuscatedIdentityPair } from "./lifecycle/mapping-helpers.js";
6
7
  import * as classSourceHelpers from "./class-source-helpers.js";
7
8
  import { buildClassSourceSnippet } from "./class-source/snippet-builder.js";
8
9
  import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire, projectMembersByLevel } from "./class-source/members-builder.js";
9
10
  import { collectDidYouMeanCandidates } from "./did-you-mean.js";
10
11
  import { matchesMemberPattern } from "./member-pattern.js";
11
12
  import { findNestedJarClasses, resolveUniqueNestedJarForClass } from "./nested-jars.js";
13
+ import { extractSymbolsFromSource } from "../symbols/symbol-extractor.js";
12
14
  import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
13
15
  import { dedupeQualityFlags, inheritArtifactMapping, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
14
16
  import { isUnobfuscatedVersion } from "../version-service.js";
@@ -79,12 +81,33 @@ function looksLikeDeobfuscatedClassName(value) {
79
81
  function obfuscatedNamespaceHint(className) {
80
82
  return `Artifact is indexed in obfuscated runtime names. Deobfuscated names like "${className}" usually require mapping="mojang" or a find-mapping lookup to obfuscated names.`;
81
83
  }
84
+ /**
85
+ * The artifact's Minecraft version when its runtime ships unobfuscated (26.1+)
86
+ * names, else undefined. A native dependency's version is its own release number,
87
+ * not a Minecraft version, so it is never put to `isUnobfuscatedVersion` (the same
88
+ * rule as the `reconcileUnobfuscatedNamespace` call sites).
89
+ */
90
+ function unobfuscatedMinecraftVersion(input) {
91
+ if (input.nativeDependency || !input.version || !isUnobfuscatedVersion(input.version)) {
92
+ return undefined;
93
+ }
94
+ return input.version;
95
+ }
96
+ /**
97
+ * Said where the obfuscated namespace hint would otherwise have been: on 26.1+
98
+ * the as-shipped names are the Mojang names, so no mapping choice finds another class.
99
+ */
100
+ function unobfuscatedRuntimeNamesNote(version) {
101
+ return `Minecraft ${version} ships Mojang names at runtime, so retrying with a different mapping will not change the class names.`;
102
+ }
82
103
  function hasPartialNetMinecraftCoverage(qualityFlags) {
83
104
  return qualityFlags.includes("partial-source-no-net-minecraft");
84
105
  }
85
106
  /**
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.
107
+ * Whether a miss has the shape "this artifact is indexed in obfuscated names, ask
108
+ * for mapping=mojang" answers, from the pieces of the artifact the caller sees.
109
+ * The claim can still be false on Minecraft 26.1+; `isObfuscatedNamespaceHintTrue`
110
+ * adds that gate.
88
111
  *
89
112
  * Two artifact kinds report `mappingApplied: "obfuscated"` without being an
90
113
  * obfuscated Minecraft index, and the hint is simply false for them:
@@ -98,22 +121,41 @@ function hasPartialNetMinecraftCoverage(qualityFlags) {
98
121
  * without these exclusions an ordinary library class such as "GameTest"
99
122
  * qualifies and the caller is told to remap a jar that was never obfuscated.
100
123
  */
101
- function isObfuscatedNamespaceHintTrue(input) {
124
+ function isObfuscatedLabelMiss(input) {
102
125
  return (input.mappingApplied === "obfuscated" &&
103
126
  !input.nativeDependency &&
104
127
  !input.qualityFlags.includes("shell-jar") &&
105
128
  looksLikeDeobfuscatedClassName(input.className));
106
129
  }
107
- function shouldSuggestObfuscatedMapping(artifact, className) {
108
- return isObfuscatedNamespaceHintTrue({
130
+ /**
131
+ * `isObfuscatedLabelMiss` is the shape of the miss the hint answers; this adds
132
+ * the one fact that makes its claim false even then. A Minecraft 26.1+ artifact
133
+ * labelled "obfuscated" is indexed in the as-shipped names, which ARE Mojang
134
+ * names: telling the caller to ask for mapping="mojang" sends them round a
135
+ * retry that returns the same names.
136
+ */
137
+ function isObfuscatedNamespaceHintTrue(input) {
138
+ return (isObfuscatedLabelMiss(input) &&
139
+ unobfuscatedMinecraftVersion({
140
+ version: input.minecraftVersion,
141
+ nativeDependency: input.nativeDependency
142
+ }) === undefined);
143
+ }
144
+ function describeArtifactForNamespaceHint(artifact, className) {
145
+ return {
109
146
  mappingApplied: artifact.mappingApplied,
110
147
  qualityFlags: artifact.qualityFlags,
111
148
  nativeDependency: artifactResolver.isDependencyLikeArtifact({
112
149
  provenance: artifact.provenance,
113
150
  coordinate: artifact.coordinate
114
151
  }),
115
- className
116
- });
152
+ className,
153
+ minecraftVersion: artifactResolver.inferVersionFromContext({
154
+ version: artifact.version,
155
+ provenance: artifact.provenance,
156
+ coordinate: artifact.coordinate
157
+ })
158
+ };
117
159
  }
118
160
  function classNameToClassPath(className) {
119
161
  const normalized = normalizePathStyle(className.trim()).replace(/\//g, ".");
@@ -148,10 +190,31 @@ export function resolveClassFilePath(svc, artifactId, className) {
148
190
  const expectedPrefix = lastSlash < 0 ? "" : classPath.slice(0, lastSlash + 1);
149
191
  return svc.filesRepo.findBestClassLookupPath(artifactId, [...candidates], normalizedClassName, simpleName, expectedPrefix);
150
192
  }
193
+ /**
194
+ * The class-name translation behind `svc.resolveClassNameForLookup`, whose callers
195
+ * (the lifecycle tools) pass a Minecraft version, so `version` also keys the 26.1+
196
+ * identity shortcut.
197
+ */
151
198
  export async function resolveClassNameForLookup(svc, input) {
199
+ return resolveArtifactClassNameForLookup(svc, { ...input, minecraftVersion: input.version });
200
+ }
201
+ /**
202
+ * `resolveClassNameForLookup` for an artifact the caller named, whose `version`
203
+ * need not be a Minecraft version: a dependency artifact's is its own release
204
+ * number. `version` still drives the mapping lookup, exactly as before; only
205
+ * `minecraftVersion`, which must be a proven Minecraft version, may take the 26.1+
206
+ * identity shortcut.
207
+ */
208
+ async function resolveArtifactClassNameForLookup(svc, input) {
152
209
  if (input.sourceMapping === input.targetMapping) {
153
210
  return input.className;
154
211
  }
212
+ // On 26.1+ the as-shipped ("obfuscated") names ARE the Mojang names and the
213
+ // mapping graph between them is empty by design, so the lookup can only miss
214
+ // and warn about a translation that is the identity.
215
+ if (isUnobfuscatedIdentityPair(input.minecraftVersion, input.sourceMapping, input.targetMapping)) {
216
+ return input.className;
217
+ }
155
218
  if (!input.version) {
156
219
  input.warnings.push(`Could not map class "${input.className}" from ${input.sourceMapping} to ${input.targetMapping} for ${input.context} because version is unavailable.`);
157
220
  return input.className;
@@ -176,6 +239,38 @@ export async function resolveClassNameForLookup(svc, input) {
176
239
  }
177
240
  return input.className;
178
241
  }
242
+ /**
243
+ * The provenance as returned, carrying `unobfuscatedRuntime` whenever the artifact
244
+ * it describes is a Minecraft 26.1+ runtime. The resolver stamps the flag on every
245
+ * fresh resolve, but a row indexed before the flag existed is served warm and never
246
+ * rewritten, and a row with no provenance gets `buildFallbackProvenance`. Only the
247
+ * provenance's OWN recorded version counts - never a project-derived one - and a
248
+ * dependency-like provenance is excluded, so a library's release number is never
249
+ * read as a Minecraft version. A jar row that predates the in-jar proof records no
250
+ * version and stays unflagged until a resolve proves the jar again, which backfills
251
+ * the row (`backfillResolvedVersion` in the indexer).
252
+ */
253
+ function withUnobfuscatedRuntimeFlag(provenance) {
254
+ if (provenance.unobfuscatedRuntime === true ||
255
+ provenance.nestedJar ||
256
+ artifactResolver.isDependencyLikeArtifact({ provenance })) {
257
+ return provenance;
258
+ }
259
+ // A persisted or stubbed object can be partial; with nothing recorded there is
260
+ // nothing to derive the flag from.
261
+ const partial = provenance;
262
+ if (!partial.resolvedFrom || !partial.target) {
263
+ return provenance;
264
+ }
265
+ const version = unobfuscatedMinecraftVersion({
266
+ version: artifactResolver.inferVersionFromContext({
267
+ provenance,
268
+ coordinate: partial.resolvedFrom.coordinate
269
+ }),
270
+ nativeDependency: false
271
+ });
272
+ return version ? { ...provenance, unobfuscatedRuntime: true } : provenance;
273
+ }
179
274
  export function buildFallbackProvenance(svc, input) {
180
275
  const artifact = svc.getArtifact(input.artifactId);
181
276
  const fallbackTarget = artifact.version
@@ -240,7 +335,21 @@ export function buildClassSourceNotFoundError(svc, input) {
240
335
  // jar. Candidates from the artifact the caller did not name carry its id.
241
336
  didYouMean: unionDidYouMeanCandidates(svc, requestedArtifactId, input.artifactId, input.className)
242
337
  };
338
+ const unobfuscatedVersion = unobfuscatedMinecraftVersion({
339
+ version: input.version,
340
+ nativeDependency: input.nativeDependency === true
341
+ });
243
342
  let nextAction = `Use find-class to resolve the correct fully-qualified name for "${simpleName}".`;
343
+ // On 26.1+ there is no namespace to switch to, so the most useful thing to say
344
+ // first is the nearest class the index does hold.
345
+ const nearestCandidate = unobfuscatedVersion
346
+ ? details.didYouMean[0]
347
+ : undefined;
348
+ if (nearestCandidate) {
349
+ nextAction =
350
+ `Did you mean "${nearestCandidate.className}"? didYouMean lists near-miss classes from the index; ` +
351
+ `otherwise use find-class to resolve the correct fully-qualified name for "${simpleName}".`;
352
+ }
244
353
  let suggestionSpec = {
245
354
  tool: "find-class",
246
355
  params: { className: simpleName, artifactId: requestedArtifactId }
@@ -290,16 +399,26 @@ export function buildClassSourceNotFoundError(svc, input) {
290
399
  // imperative "Provide/Pass mapping", and the sentence is concatenated into
291
400
  // the same `nextAction` string as the find-class guidance, which is published
292
401
  // as a single hint that no mid-string excision can repair.
402
+ //
403
+ // A third gate lives inside `isObfuscatedNamespaceHintTrue`: on Minecraft 26.1+
404
+ // the claim is false, and the same miss gets a note that no mapping will help.
293
405
  const callerAskedForNonObfuscatedMapping = input.callerSuppliedMapping != null && input.callerSuppliedMapping !== "obfuscated";
294
- if (!callerAskedForNonObfuscatedMapping &&
295
- isObfuscatedNamespaceHintTrue({
296
- mappingApplied: input.mappingApplied,
297
- qualityFlags: input.qualityFlags,
298
- nativeDependency: input.nativeDependency === true,
299
- className: input.className
300
- })) {
406
+ const namespaceHintInput = {
407
+ mappingApplied: input.mappingApplied,
408
+ qualityFlags: input.qualityFlags,
409
+ nativeDependency: input.nativeDependency === true,
410
+ className: input.className,
411
+ minecraftVersion: input.version
412
+ };
413
+ if (!callerAskedForNonObfuscatedMapping && isObfuscatedNamespaceHintTrue(namespaceHintInput)) {
301
414
  nextAction += ` ${obfuscatedNamespaceHint(input.className)}`;
302
415
  }
416
+ else if (!callerAskedForNonObfuscatedMapping &&
417
+ unobfuscatedVersion &&
418
+ !hasPartialNetMinecraftCoverage(input.qualityFlags) &&
419
+ isObfuscatedLabelMiss(namespaceHintInput)) {
420
+ nextAction += ` ${unobfuscatedRuntimeNamesNote(unobfuscatedVersion)}`;
421
+ }
303
422
  details.nextAction = nextAction;
304
423
  Object.assign(details, buildSuggestedCall(suggestionSpec));
305
424
  // Split-source workspaces can omit client-only classes from merged indexes;
@@ -407,6 +526,50 @@ function projectDecompiledFallback(fallback, level) {
407
526
  // The class-like symbol kinds findClass returns. MUST stay in sync with the JS-side
408
527
  // isTypeSymbol checks below; pushed down to SQL so non-type rows are never fetched.
409
528
  const TYPE_SYMBOL_KINDS = ["class", "interface", "enum", "record"];
529
+ /**
530
+ * Reconstruct the full nesting chain between a file's top-level type and a
531
+ * type declared two or more levels deeper inside it. The extractor stores
532
+ * only ONE qualifiedName per FILE (the top-level type), so naively
533
+ * concatenating `<topLevelFQN>.<symbolName>` is only correct for exactly one
534
+ * level of nesting — it silently drops every intermediate enclosing type.
535
+ *
536
+ * Walks the file's own type declarations from the top-level type downward,
537
+ * using brace-range containment to find which declaration directly encloses
538
+ * `targetLine` at each level, collecting each intermediate simple name.
539
+ *
540
+ * Returns the intermediate simple names (outermost first, target excluded)
541
+ * or undefined when the chain cannot be reconstructed — e.g. the file's
542
+ * content is unavailable — in which case the caller falls back to the
543
+ * single-level formula.
544
+ */
545
+ function resolveNestedTypeChain(svc, artifactId, filePath, topSimpleName, targetLine) {
546
+ const fileRow = svc.filesRepo.getFileContent(artifactId, filePath);
547
+ if (!fileRow)
548
+ return undefined;
549
+ const lines = fileRow.content.split(/\r?\n/);
550
+ const fileSymbols = extractSymbolsFromSource(filePath, fileRow.content);
551
+ let currentBody = classSourceHelpers.computeBraceRange(lines, fileSymbols, topSimpleName);
552
+ if (!currentBody)
553
+ return undefined;
554
+ const chain = [];
555
+ // Bounded by the number of type symbols in the file; guards against ever
556
+ // looping on a malformed structure.
557
+ for (let step = 0; step <= fileSymbols.length; step += 1) {
558
+ const childRanges = classSourceHelpers.computeNestedTypeRanges(lines, fileSymbols, currentBody);
559
+ if (childRanges.some((range) => range.declarationLine === targetLine)) {
560
+ return chain;
561
+ }
562
+ const containingChild = childRanges.find((range) => range.declarationLine < targetLine && targetLine <= range.endLine);
563
+ if (!containingChild)
564
+ return undefined;
565
+ const childSymbol = fileSymbols.find((symbol) => symbol.line === containingChild.declarationLine);
566
+ if (!childSymbol)
567
+ return undefined;
568
+ chain.push(childSymbol.symbolName);
569
+ currentBody = containingChild;
570
+ }
571
+ return undefined;
572
+ }
410
573
  export function findClass(svc, input) {
411
574
  const className = input.className.trim();
412
575
  if (!className) {
@@ -500,8 +663,18 @@ export function findClass(svc, input) {
500
663
  const enclosingQualifiedName = row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", ".");
501
664
  const enclosingSimpleName = enclosingQualifiedName.split(".").at(-1) ?? enclosingQualifiedName;
502
665
  const nested = enclosingSimpleName !== row.symbolName;
666
+ let qualifiedName = nested ? `${enclosingQualifiedName}.${row.symbolName}` : enclosingQualifiedName;
667
+ if (nested) {
668
+ // The formula above assumes exactly one level of nesting. When the type
669
+ // is nested two or more levels deep, reconstruct the real chain so the
670
+ // intermediate enclosing type(s) are not silently dropped.
671
+ const intermediateChain = resolveNestedTypeChain(svc, artifactId, row.filePath, enclosingSimpleName, row.line);
672
+ if (intermediateChain && intermediateChain.length > 0) {
673
+ qualifiedName = `${enclosingQualifiedName}.${[...intermediateChain, row.symbolName].join(".")}`;
674
+ }
675
+ }
503
676
  candidates.push({
504
- qualifiedName: nested ? `${enclosingQualifiedName}.${row.symbolName}` : enclosingQualifiedName,
677
+ qualifiedName,
505
678
  filePath: row.filePath,
506
679
  line: row.line,
507
680
  symbolKind: row.symbolKind,
@@ -554,8 +727,23 @@ function finishFindClass(svc, input) {
554
727
  }
555
728
  }).suggestedCall;
556
729
  }
557
- if (matches.length === 0 && shouldSuggestObfuscatedMapping(artifact, className)) {
558
- warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
730
+ if (matches.length === 0) {
731
+ const hintInput = describeArtifactForNamespaceHint(artifact, className);
732
+ const unobfuscatedVersion = unobfuscatedMinecraftVersion({
733
+ version: hintInput.minecraftVersion,
734
+ nativeDependency: hintInput.nativeDependency
735
+ });
736
+ if (isObfuscatedNamespaceHintTrue(hintInput)) {
737
+ warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
738
+ }
739
+ else if (unobfuscatedVersion && isObfuscatedLabelMiss(hintInput)) {
740
+ const nearest = collectDidYouMeanCandidates(svc, artifactId, className)
741
+ .slice(0, 3)
742
+ .map((candidate) => `"${candidate.className}"`);
743
+ warnings.push(`No exact class symbol matched "${className}".` +
744
+ (nearest.length > 0 ? ` Did you mean ${nearest.join(", ")}?` : "") +
745
+ ` ${unobfuscatedRuntimeNamesNote(unobfuscatedVersion)}`);
746
+ }
559
747
  }
560
748
  return {
561
749
  matches,
@@ -702,6 +890,12 @@ export async function getClassSource(svc, input) {
702
890
  if (!artifactResolver.isDependencyLikeArtifact({ provenance, coordinate })) {
703
891
  mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
704
892
  }
893
+ // The same rule for the class-name lookup: its 26.1+ identity shortcut trusts only
894
+ // a Minecraft version, so a library's 26.x-shaped release number keeps the lookup
895
+ // (and its warnings) that any other library gets.
896
+ const minecraftVersion = artifactResolver.isDependencyLikeArtifact({ provenance, coordinate })
897
+ ? undefined
898
+ : version;
705
899
  let activeArtifactId = artifactId;
706
900
  let activeOrigin = origin;
707
901
  let activeIsDecompiled = isDecompiled;
@@ -802,9 +996,10 @@ export async function getClassSource(svc, input) {
802
996
  warnings.push(`Class "${className}" lives in nested jar "${match.entryName}" bundled by the shell jar; the lookup was redirected there automatically.`);
803
997
  return true;
804
998
  };
805
- let activeLookupClassName = await svc.resolveClassNameForLookup({
999
+ let activeLookupClassName = await resolveArtifactClassNameForLookup(svc, {
806
1000
  className,
807
1001
  version,
1002
+ minecraftVersion,
808
1003
  sourceMapping: requestedMapping,
809
1004
  targetMapping: activeMappingApplied,
810
1005
  sourcePriority: input.sourcePriority,
@@ -817,9 +1012,10 @@ export async function getClassSource(svc, input) {
817
1012
  filePath = resolveClassFilePath(svc, activeArtifactId, activeLookupClassName);
818
1013
  }
819
1014
  if (!filePath && (await tryBinaryFallback())) {
820
- activeLookupClassName = await svc.resolveClassNameForLookup({
1015
+ activeLookupClassName = await resolveArtifactClassNameForLookup(svc, {
821
1016
  className,
822
1017
  version,
1018
+ minecraftVersion,
823
1019
  sourceMapping: requestedMapping,
824
1020
  targetMapping: activeMappingApplied,
825
1021
  sourcePriority: input.sourcePriority,
@@ -861,9 +1057,10 @@ export async function getClassSource(svc, input) {
861
1057
  }
862
1058
  }
863
1059
  if (!row && (await tryBinaryFallback())) {
864
- activeLookupClassName = await svc.resolveClassNameForLookup({
1060
+ activeLookupClassName = await resolveArtifactClassNameForLookup(svc, {
865
1061
  className,
866
1062
  version,
1063
+ minecraftVersion,
867
1064
  sourceMapping: requestedMapping,
868
1065
  targetMapping: activeMappingApplied,
869
1066
  sourcePriority: input.sourcePriority,
@@ -922,13 +1119,13 @@ export async function getClassSource(svc, input) {
922
1119
  resolvedOutputFile = outputPath;
923
1120
  sourceText = `[Written to ${outputPath}]`;
924
1121
  }
925
- const normalizedProvenance = activeProvenance ??
1122
+ const normalizedProvenance = withUnobfuscatedRuntimeFlag(activeProvenance ??
926
1123
  buildFallbackProvenance(svc, {
927
1124
  artifactId: activeArtifactId,
928
1125
  origin: activeOrigin,
929
1126
  requestedMapping,
930
1127
  mappingApplied: activeMappingApplied
931
- });
1128
+ }));
932
1129
  const nextStartLine = snippet.nextStartLine;
933
1130
  // Continuation guidance: when output was truncated and was not redirected to
934
1131
  // a file, hand the caller the next line to read plus a replayable call that
@@ -1097,10 +1294,14 @@ export async function getClassMembers(svc, input) {
1097
1294
  });
1098
1295
  // Gated on the same predicate as the source path above, and for the same
1099
1296
  // reason: a dependency's `version` is its own coordinate version, so the
1100
- // unobfuscated-runtime relabel must not reach it.
1297
+ // unobfuscated-runtime relabel must not reach it - nor the 26.1+ identity
1298
+ // shortcut of the class and member lookups below.
1101
1299
  if (!artifactResolver.isDependencyLikeArtifact({ provenance, coordinate })) {
1102
1300
  mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
1103
1301
  }
1302
+ const minecraftVersion = artifactResolver.isDependencyLikeArtifact({ provenance, coordinate })
1303
+ ? undefined
1304
+ : version;
1104
1305
  if (requestedMapping !== "obfuscated" && !version) {
1105
1306
  throw createError({
1106
1307
  code: ERROR_CODES.MAPPING_NOT_APPLIED,
@@ -1167,9 +1368,10 @@ export async function getClassMembers(svc, input) {
1167
1368
  }
1168
1369
  });
1169
1370
  }
1170
- const lookupClassName = await svc.resolveClassNameForLookup({
1371
+ const lookupClassName = await resolveArtifactClassNameForLookup(svc, {
1171
1372
  className,
1172
1373
  version,
1374
+ minecraftVersion,
1173
1375
  sourceMapping: requestedMapping,
1174
1376
  targetMapping: mappingApplied,
1175
1377
  sourcePriority: input.sourcePriority,
@@ -1292,6 +1494,7 @@ export async function getClassMembers(svc, input) {
1292
1494
  signatureFields,
1293
1495
  signatureMethods,
1294
1496
  version,
1497
+ minecraftVersion,
1295
1498
  mappingApplied,
1296
1499
  requestedMapping,
1297
1500
  sourcePriority: input.sourcePriority,
@@ -1324,13 +1527,13 @@ export async function getClassMembers(svc, input) {
1324
1527
  const projectedMembers = projectMembersByLevel(projectMembersForWire({ constructors, fields, methods }, includeInherited, input.includeDescriptors ?? false), projection);
1325
1528
  const truncated = sliced.truncated;
1326
1529
  const nextCursor = sliced.nextOffset != null ? encodeOffsetCursor(sliced.nextOffset, memberCursorContext) : undefined;
1327
- const baseProvenance = provenance ??
1530
+ const baseProvenance = withUnobfuscatedRuntimeFlag(provenance ??
1328
1531
  buildFallbackProvenance(svc, {
1329
1532
  artifactId,
1330
1533
  origin,
1331
1534
  requestedMapping,
1332
1535
  mappingApplied
1333
- });
1536
+ }));
1334
1537
  const normalizedProvenance = nestedJarRedirect
1335
1538
  ? { ...baseProvenance, nestedJar: nestedJarRedirect }
1336
1539
  : baseProvenance;
@@ -415,8 +415,11 @@ export async function ingestIfNeeded(svc, resolved) {
415
415
  if (resolved.artifactAlias && existing.alias !== resolved.artifactAlias) {
416
416
  svc.artifactsRepo.setAlias(resolved.artifactId, resolved.artifactAlias);
417
417
  }
418
- svc.metrics.recordArtifactCacheHit();
419
418
  const touchedAt = new Date().toISOString();
419
+ if (!existing.version) {
420
+ backfillResolvedVersion(svc, resolved, touchedAt);
421
+ }
422
+ svc.metrics.recordArtifactCacheHit();
420
423
  svc.artifactsRepo.touchArtifact(resolved.artifactId, touchedAt);
421
424
  touchCacheMetrics(svc, resolved.artifactId, touchedAt);
422
425
  return;
@@ -444,6 +447,71 @@ export async function ingestIfNeeded(svc, resolved) {
444
447
  }
445
448
  }
446
449
  }
450
+ /**
451
+ * Writes the version a warm resolve derived onto a stored row that records none,
452
+ * with the two provenance fields that follow from it.
453
+ *
454
+ * The artifactId is a content hash, so a row indexed before its version could be
455
+ * derived - a Minecraft 26.1+ runtime jar indexed before the in-jar proof existed -
456
+ * is served warm by every later resolve and never rewritten. resolve-artifact then
457
+ * reports the version while the row lacks it, and every lookup by artifactId reads
458
+ * the row: get-class-members refuses mapping "mojang" for want of a version, and a
459
+ * miss still gets the legacy "use mapping=mojang" hint.
460
+ *
461
+ * Only gaps are filled. A stored version is never replaced, and the row keeps the
462
+ * label, chain and target it was indexed under. Only a version the fresh provenance
463
+ * records as resolved from is written: resolve-artifact records the version the
464
+ * target itself names (version target, coordinate or in-jar proof), which describes
465
+ * these bytes, while the binary fallback of get-class-source re-ingests with a
466
+ * caller version that may come from the project's gradle.properties, under the
467
+ * stored provenance, so it never qualifies. The row is re-read in the same
468
+ * synchronous turn as the write, so a concurrent writer since `existing` was read
469
+ * is not overwritten.
470
+ * `upsertArtifact` is the repo's only writer for these columns; on an existing row
471
+ * it updates in place and leaves `created_at` and the file index alone.
472
+ */
473
+ function backfillResolvedVersion(svc, resolved, timestamp) {
474
+ const version = resolved.version;
475
+ if (!version || resolved.provenance?.resolvedFrom?.version !== version) {
476
+ return;
477
+ }
478
+ // Only a proven Minecraft 26.1+ runtime qualifies. A library version recorded by a
479
+ // dependency resolve must never land on a row whose stored provenance does not mark
480
+ // it as a dependency, or later lookups would read that version as Minecraft's.
481
+ if (resolved.provenance?.unobfuscatedRuntime !== true) {
482
+ return;
483
+ }
484
+ const current = svc.artifactsRepo.getArtifact(resolved.artifactId);
485
+ if (!current || current.version) {
486
+ return;
487
+ }
488
+ svc.artifactsRepo.upsertArtifact({
489
+ artifactId: current.artifactId,
490
+ alias: current.alias,
491
+ origin: current.origin,
492
+ coordinate: current.coordinate,
493
+ version,
494
+ binaryJarPath: current.binaryJarPath,
495
+ sourceJarPath: current.sourceJarPath,
496
+ repoUrl: current.repoUrl,
497
+ requestedMapping: current.requestedMapping,
498
+ mappingApplied: current.mappingApplied,
499
+ provenance: current.provenance
500
+ ? {
501
+ ...current.provenance,
502
+ resolvedFrom: {
503
+ ...current.provenance.resolvedFrom,
504
+ version: current.provenance.resolvedFrom?.version ?? version
505
+ },
506
+ unobfuscatedRuntime: true
507
+ }
508
+ : undefined,
509
+ qualityFlags: current.qualityFlags,
510
+ artifactSignature: current.artifactSignature,
511
+ isDecompiled: current.isDecompiled,
512
+ timestamp
513
+ });
514
+ }
447
515
  async function rebuildMissingArtifactIndex(svc, resolved, reason) {
448
516
  svc.metrics.recordArtifactCacheMiss();
449
517
  svc.metrics.recordReindex();
@@ -1,6 +1,16 @@
1
1
  import type { SignatureMember } from "../../minecraft-explorer-service.js";
2
2
  import type { SourceService } from "../../source-service.js";
3
3
  import type { MappingSourcePriority, SourceMapping } from "../../types.js";
4
+ /**
5
+ * On unobfuscated versions (26.1+) the as-shipped ("obfuscated") names are the Mojang
6
+ * names, so obfuscated<->mojang is the identity. The mapping graph is empty there, so
7
+ * asking it would only report every member as unmapped.
8
+ *
9
+ * `minecraftVersion` must be a proven Minecraft version. A dependency
10
+ * artifact's version is its own release number (`annotations:26.0.2`), so callers
11
+ * serving one pass undefined and keep the mapping lookup it always had.
12
+ */
13
+ export declare function isUnobfuscatedIdentityPair(minecraftVersion: string | undefined, sourceMapping: SourceMapping, targetMapping: SourceMapping): boolean;
4
14
  export declare function normalizeMapping(mapping: SourceMapping | undefined): SourceMapping;
5
15
  export declare function rejectLifecycleClassLikeInput(svc: SourceService, input: {
6
16
  symbol: string;
@@ -16,7 +26,16 @@ export declare function resolveToObfuscatedMemberName(svc: SourceService, name:
16
26
  name: string;
17
27
  descriptor?: string;
18
28
  }>;
19
- export declare function remapSignatureMembers(svc: SourceService, members: SignatureMember[], kind: "field" | "method", version: string, sourceMapping: SourceMapping, targetMapping: SourceMapping, sourcePriority: MappingSourcePriority | undefined, warnings: string[], projectPath?: string, gradleUserHome?: string): Promise<{
29
+ export declare function remapSignatureMembers(svc: SourceService, members: SignatureMember[], kind: "field" | "method", version: string, sourceMapping: SourceMapping, targetMapping: SourceMapping, sourcePriority: MappingSourcePriority | undefined, warnings: string[], projectPath?: string, gradleUserHome?: string,
30
+ /**
31
+ * The Minecraft version the obfuscated<->mojang identity shortcut may trust.
32
+ * Defaults to `version`, which the lifecycle, validate-mixin and access-widener
33
+ * callers pass as a Minecraft version; get-class-members passes undefined for a
34
+ * dependency artifact, whose `version` is the library's own.
35
+ */
36
+ identity?: {
37
+ minecraftVersion: string | undefined;
38
+ }): Promise<{
20
39
  members: SignatureMember[];
21
40
  failedNames: Set<string>;
22
41
  }>;
@@ -1,6 +1,24 @@
1
1
  import { buildSuggestedCall } from "../../build-suggested-call.js";
2
2
  import { ERROR_CODES, createError } from "../../errors.js";
3
+ import { isUnobfuscatedVersion } from "../../version-service.js";
3
4
  import { rebuildJavaSignature, remapJvmDescriptor } from "../descriptor-utils.js";
5
+ /**
6
+ * On unobfuscated versions (26.1+) the as-shipped ("obfuscated") names are the Mojang
7
+ * names, so obfuscated<->mojang is the identity. The mapping graph is empty there, so
8
+ * asking it would only report every member as unmapped.
9
+ *
10
+ * `minecraftVersion` must be a proven Minecraft version. A dependency
11
+ * artifact's version is its own release number (`annotations:26.0.2`), so callers
12
+ * serving one pass undefined and keep the mapping lookup it always had.
13
+ */
14
+ export function isUnobfuscatedIdentityPair(minecraftVersion, sourceMapping, targetMapping) {
15
+ const pair = new Set([sourceMapping, targetMapping]);
16
+ return (pair.size === 2 &&
17
+ pair.has("obfuscated") &&
18
+ pair.has("mojang") &&
19
+ minecraftVersion !== undefined &&
20
+ isUnobfuscatedVersion(minecraftVersion));
21
+ }
4
22
  export function normalizeMapping(mapping) {
5
23
  if (mapping == null) {
6
24
  return "obfuscated";
@@ -87,7 +105,7 @@ export async function resolveToObfuscatedClassName(svc, className, version, mapp
87
105
  });
88
106
  }
89
107
  export async function resolveToObfuscatedMemberName(svc, name, ownerInSourceMapping, descriptor, kind, version, mapping, sourcePriority, warnings, gradleUserHome) {
90
- if (mapping === "obfuscated") {
108
+ if (mapping === "obfuscated" || isUnobfuscatedIdentityPair(version, mapping, "obfuscated")) {
91
109
  return {
92
110
  name,
93
111
  descriptor: kind === "method" ? descriptor : undefined
@@ -167,9 +185,17 @@ export async function resolveToObfuscatedMemberName(svc, name, ownerInSourceMapp
167
185
  descriptor: kind === "method" ? descriptor : undefined
168
186
  };
169
187
  }
170
- export async function remapSignatureMembers(svc, members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings, projectPath, gradleUserHome) {
188
+ export async function remapSignatureMembers(svc, members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings, projectPath, gradleUserHome,
189
+ /**
190
+ * The Minecraft version the obfuscated<->mojang identity shortcut may trust.
191
+ * Defaults to `version`, which the lifecycle, validate-mixin and access-widener
192
+ * callers pass as a Minecraft version; get-class-members passes undefined for a
193
+ * dependency artifact, whose `version` is the library's own.
194
+ */
195
+ identity = { minecraftVersion: version }) {
171
196
  const failedNames = new Set();
172
- if (sourceMapping === targetMapping) {
197
+ if (sourceMapping === targetMapping ||
198
+ isUnobfuscatedIdentityPair(identity.minecraftVersion, sourceMapping, targetMapping)) {
173
199
  return { members, failedNames };
174
200
  }
175
201
  const memberKeyToRemapped = new Map();
@@ -1,2 +1,27 @@
1
1
  import type { CheckSymbolExistsInput, CheckSymbolExistsOutput, SourceService } from "../../source-service.js";
2
+ /**
3
+ * What a runtime-bytecode existence check established.
4
+ *
5
+ * `verified` is true when the runtime jar answered: the class loaded and the member
6
+ * lookup ran to completion, or the class was confirmed missing (CLASS_NOT_FOUND). It
7
+ * is false when the check could not be performed - a short class name, a member query
8
+ * without an owner, or a class that failed to load for any other reason (unreadable
9
+ * jar, malformed class file) - and `result` is then the fallback base with the
10
+ * explanatory warning appended, so it still carries the caller's own status.
11
+ */
12
+ export interface UnobfuscatedRuntimeCheck {
13
+ verified: boolean;
14
+ result: CheckSymbolExistsOutput;
15
+ }
16
+ /**
17
+ * check-symbol-exists's runtime fallback: the result of `runUnobfuscatedRuntimeCheck`
18
+ * alone. Its callers keep the fallback base's status whenever the check could not be
19
+ * performed, so they need no `verified` flag.
20
+ */
2
21
  export declare function checkSymbolExistsInUnobfuscatedRuntime(svc: SourceService, input: CheckSymbolExistsInput, fallbackBase: CheckSymbolExistsOutput): Promise<CheckSymbolExistsOutput | undefined>;
22
+ /**
23
+ * Checks a symbol against the Minecraft runtime jar of an unobfuscated version, whose
24
+ * names are the Mojang names. Undefined when the version or name is empty or the
25
+ * runtime jar cannot be resolved.
26
+ */
27
+ export declare function runUnobfuscatedRuntimeCheck(svc: SourceService, input: CheckSymbolExistsInput, fallbackBase: CheckSymbolExistsOutput): Promise<UnobfuscatedRuntimeCheck | undefined>;