@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
@@ -14,6 +14,7 @@ import { resolveTinyRemapperJar } from "../tiny-remapper-resolver.js";
14
14
  import { isUnobfuscatedVersion } from "../version-service.js";
15
15
  import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
16
16
  const VERSION_TOKEN_REGEX_CACHE = new Map();
17
+ const LOADER_VERSION_TOKEN_REGEX_CACHE = new Map();
17
18
  const MAX_HELPER_REGEX_CACHE = 128;
18
19
  function rememberCachedRegex(cache, key, regex) {
19
20
  if (cache.size >= MAX_HELPER_REGEX_CACHE) {
@@ -57,6 +58,97 @@ export function hasExactVersionToken(path, version) {
57
58
  ?? rememberCachedRegex(VERSION_TOKEN_REGEX_CACHE, normalizedVersion, new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}(?![0-9a-z]|\\.[0-9])`, "i"));
58
59
  return pattern.test(normalizedPath);
59
60
  }
61
+ /**
62
+ * True when a path carries the NeoForge/Forge loader version that corresponds
63
+ * 1:1 to `mcVersion`.
64
+ *
65
+ * ModDevGradle names every artifact after the LOADER version and never after
66
+ * Minecraft: MC 1.21.11 produces `build/moddev/artifacts/neoforge-21.11.38-beta-merged.jar`.
67
+ * A plain `hasExactVersionToken(path, "1.21.11")` therefore rejected every
68
+ * artifact of the canonical NeoForge workspace, which made
69
+ * validate-access-transformer unusable there.
70
+ *
71
+ * NeoForge derives its version from Minecraft as `<minor>.<patch>.<build>`, so
72
+ * `1.21.11` -> `21.11.<build>` and `1.21` -> `21.0.<build>`. The trailing dot
73
+ * before the build number keeps `21.1.` from matching `21.10.5`.
74
+ */
75
+ export function hasLoaderRuntimeVersionToken(path, mcVersion) {
76
+ const match = /^1\.(\d+)(?:\.(\d+))?$/.exec(mcVersion.trim());
77
+ if (!match) {
78
+ return false;
79
+ }
80
+ const minor = match[1];
81
+ const patch = match[2] ?? "0";
82
+ const normalizedPath = normalizePathStyle(path).toLowerCase();
83
+ const cacheKey = `loader:${minor}.${patch}`;
84
+ const pattern = LOADER_VERSION_TOKEN_REGEX_CACHE.get(cacheKey)
85
+ ?? rememberCachedRegex(LOADER_VERSION_TOKEN_REGEX_CACHE, cacheKey, new RegExp(`(^|[^0-9a-z.])${minor}\\.${patch}\\.\\d`, "i"));
86
+ return pattern.test(normalizedPath);
87
+ }
88
+ const RUNTIME_JAR_VERSION_REGEX = /(?:^|[^0-9.])(1\.\d+(?:\.\d+)?)(?![0-9.])/;
89
+ /**
90
+ * Minecraft version a runtime jar path carries, or undefined when the path
91
+ * names none. Loom lays its cache out as
92
+ * `<gradle>/caches/fabric-loom/<mcVersion>/...`, so the first `1.x[.y]` token
93
+ * of the path is the version the jar was built for.
94
+ */
95
+ export function inferRuntimeJarMinecraftVersion(path) {
96
+ return RUNTIME_JAR_VERSION_REGEX.exec(normalizePathStyle(path))?.[1];
97
+ }
98
+ /**
99
+ * Loader a runtime jar belongs to, read from its path.
100
+ *
101
+ * A Loom cache holds NeoForge-patched jars under a `/neoforge/` segment
102
+ * (`caches/fabric-loom/1.21.10/neoforge/21.10.50-beta/minecraft-merged-mojang-at-patched.jar`).
103
+ * Serving one of those to a Fabric workspace silently validated a Fabric access
104
+ * widener against NeoForge bytecode, so the loader has to travel with the jar.
105
+ */
106
+ export function inferRuntimeJarLoader(path) {
107
+ const lower = normalizePathStyle(path).toLowerCase();
108
+ if (lower.includes("neoforge") || lower.includes("neoform") || lower.includes("moddev")) {
109
+ return "neoforge";
110
+ }
111
+ if (/(^|[/\-_])forge([/\-_.]|$)/.test(lower) || lower.includes("forge_gradle") || lower.includes("srg")) {
112
+ return "forge";
113
+ }
114
+ if (lower.includes("fabric-loom") || lower.includes("loom-cache") || lower.includes("intermediary")) {
115
+ return "fabric";
116
+ }
117
+ return "unknown";
118
+ }
119
+ /**
120
+ * Fills in the truthful version/loader half of a runtime provenance record.
121
+ *
122
+ * `version` becomes the version the SERVED jar carries; the caller's original
123
+ * request is preserved under `requestedVersion` and flagged. A served loader
124
+ * that contradicts a KNOWN expected loader is flagged too — never silently
125
+ * dropped.
126
+ */
127
+ export function describeServedRuntimeJar(input) {
128
+ const notes = [];
129
+ const servedVersion = inferRuntimeJarMinecraftVersion(input.jarPath);
130
+ const versionApproximated = servedVersion !== undefined && servedVersion !== input.requestedVersion.trim();
131
+ const servedLoader = inferRuntimeJarLoader(input.jarPath);
132
+ const expectedLoader = input.expectedLoader;
133
+ const loaderMismatch = expectedLoader !== undefined &&
134
+ expectedLoader !== "unknown" &&
135
+ servedLoader !== "unknown" &&
136
+ servedLoader !== expectedLoader;
137
+ if (versionApproximated) {
138
+ notes.push(`Runtime jar is Minecraft ${servedVersion}, not the requested ${input.requestedVersion}; results are approximate.`);
139
+ }
140
+ if (loaderMismatch) {
141
+ notes.push(`Runtime jar is a ${servedLoader} artifact while the workspace is ${expectedLoader}; its bytecode differs from the ${expectedLoader} runtime.`);
142
+ }
143
+ return {
144
+ version: versionApproximated && servedVersion ? servedVersion : input.requestedVersion,
145
+ ...(versionApproximated ? { requestedVersion: input.requestedVersion, versionApproximated: true } : {}),
146
+ servedLoader,
147
+ ...(expectedLoader ? { expectedLoader } : {}),
148
+ ...(loaderMismatch ? { loaderMismatch: true } : {}),
149
+ notes
150
+ };
151
+ }
60
152
  function inferMergedRuntimeNamespaceHint(path) {
61
153
  const normalizedPath = normalizePathStyle(path).toLowerCase();
62
154
  if (normalizedPath.includes("merged-intermediary-v2") ||
@@ -407,6 +499,11 @@ export async function discoverAccessTransformerRuntimeCandidates(_svc, input) {
407
499
  const normalizedProjectPathLower = normalizedProjectPath
408
500
  ? normalizePathStyle(normalizedProjectPath).toLowerCase()
409
501
  : undefined;
502
+ // A jar inside the caller's own build directory belongs to the version that
503
+ // workspace declares, whatever its filename says.
504
+ const projectAnchorAllowed = normalizedProjectPathLower !== undefined &&
505
+ input.projectMinecraftVersion !== undefined &&
506
+ input.projectMinecraftVersion.trim() === input.version.trim();
410
507
  const searchRoots = buildLoaderRuntimeSearchRoots({
411
508
  projectPath: normalizedProjectPath,
412
509
  gradleUserHome: input.gradleUserHome
@@ -449,7 +546,17 @@ export async function discoverAccessTransformerRuntimeCandidates(_svc, input) {
449
546
  }
450
547
  seen.add(normalizedPath);
451
548
  const lower = normalizedPath.toLowerCase();
452
- if (!hasExactVersionToken(normalizedPath, input.version)) {
549
+ const insideProject = normalizedProjectPathLower !== undefined && lower.startsWith(normalizedProjectPathLower);
550
+ // Positive version evidence is REQUIRED; the three forms are ranked so a
551
+ // verbatim Minecraft version always beats an inferred one.
552
+ const versionEvidence = hasExactVersionToken(normalizedPath, input.version)
553
+ ? "exact-token"
554
+ : hasLoaderRuntimeVersionToken(normalizedPath, input.version)
555
+ ? "loader-token"
556
+ : projectAnchorAllowed && insideProject
557
+ ? "project-anchored"
558
+ : undefined;
559
+ if (!versionEvidence) {
453
560
  continue;
454
561
  }
455
562
  const looksMerged = lower.includes("merged");
@@ -457,6 +564,11 @@ export async function discoverAccessTransformerRuntimeCandidates(_svc, input) {
457
564
  const looksForge = lower.includes("forge");
458
565
  const looksNeoForge = lower.includes("neoforge") || lower.includes("moddev") || lower.includes("neoform");
459
566
  const looksPatchedRuntime = lower.includes("patched") || lower.includes("client-extra") || lower.includes("joined");
567
+ // client-extra / *-minecraft-resources jars carry resources ONLY, so they
568
+ // can never answer a class lookup. They stay selectable (a workspace that
569
+ // has nothing else still gets an answer) but must lose to any real
570
+ // classes jar.
571
+ const looksResourcesOnly = lower.includes("client-extra") || lower.includes("minecraft-resources");
460
572
  const appliedScope = looksMerged
461
573
  ? "merged"
462
574
  : "loader";
@@ -470,18 +582,21 @@ export async function discoverAccessTransformerRuntimeCandidates(_svc, input) {
470
582
  continue;
471
583
  }
472
584
  const score = 10_000 +
473
- (normalizedProjectPathLower && lower.startsWith(normalizedProjectPathLower) ? 4_000 : 0) +
585
+ (versionEvidence === "exact-token" ? 5_000 : versionEvidence === "loader-token" ? 3_500 : 3_000) +
586
+ (insideProject ? 4_000 : 0) +
474
587
  (looksPatchedRuntime ? 3_000 : 0) +
475
588
  (looksSrg ? 2_500 : 0) +
476
589
  (input.loader === "forge" && looksForge ? 1_500 : 0) +
477
590
  (input.loader === "neoforge" && looksNeoForge ? 1_500 : 0) +
478
591
  (input.requestedScope === appliedScope ? 1_000 : 0) +
479
- (looksMerged ? -500 : 0);
592
+ (looksMerged ? -500 : 0) +
593
+ (looksResourcesOnly ? -6_000 : 0);
480
594
  candidates.push({
481
595
  jarPath: normalizedPath,
482
596
  score,
483
597
  appliedScope,
484
- origin: "local-jar"
598
+ origin: "local-jar",
599
+ versionEvidence
485
600
  });
486
601
  }
487
602
  }
@@ -504,6 +619,9 @@ export async function resolveAccessWidenerRuntimeArtifact(svc, input) {
504
619
  const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
505
620
  version = detected ?? version;
506
621
  }
622
+ const expectedLoader = normalizedProjectPath
623
+ ? await detectWorkspaceRuntimeLoader(svc, normalizedProjectPath)
624
+ : undefined;
507
625
  const requestedScope = input.scope ?? (normalizedProjectPath ? "loader" : "vanilla");
508
626
  if (requestedScope === "vanilla") {
509
627
  const versionJar = await svc.versionService.resolveVersionJar(version);
@@ -574,9 +692,21 @@ export async function resolveAccessWidenerRuntimeArtifact(svc, input) {
574
692
  notes.push(...detection.warnings);
575
693
  }
576
694
  }
695
+ // Provenance must describe the jar that was SERVED, never the request.
696
+ const served = describeServedRuntimeJar({
697
+ jarPath: discovery.selected.jarPath,
698
+ requestedVersion: version,
699
+ expectedLoader
700
+ });
701
+ notes.push(...served.notes);
577
702
  return {
578
- version,
703
+ version: served.version,
579
704
  jarPath: discovery.selected.jarPath,
705
+ ...(served.requestedVersion ? { requestedVersion: served.requestedVersion } : {}),
706
+ ...(served.versionApproximated ? { versionApproximated: true } : {}),
707
+ servedLoader: served.servedLoader,
708
+ ...(served.expectedLoader ? { expectedLoader: served.expectedLoader } : {}),
709
+ ...(served.loaderMismatch ? { loaderMismatch: true } : {}),
580
710
  requestedScope,
581
711
  appliedScope,
582
712
  requestedMapping: input.awNamespace,
@@ -586,6 +716,28 @@ export async function resolveAccessWidenerRuntimeArtifact(svc, input) {
586
716
  scopeFallback
587
717
  };
588
718
  }
719
+ /**
720
+ * Loader a workspace declares, normalized onto {@link RuntimeLoader}. Quilt is
721
+ * Fabric-compatible for runtime-jar purposes; anything undetected stays
722
+ * "unknown" so no mismatch is ever asserted on a guess.
723
+ */
724
+ async function detectWorkspaceRuntimeLoader(svc, projectPath) {
725
+ const detection = await svc.workspaceMappingService.detectProjectLoader(projectPath);
726
+ if (!detection.resolved) {
727
+ return "unknown";
728
+ }
729
+ switch (detection.loader) {
730
+ case "fabric":
731
+ case "quilt":
732
+ return "fabric";
733
+ case "forge":
734
+ return "forge";
735
+ case "neoforge":
736
+ return "neoforge";
737
+ default:
738
+ return "unknown";
739
+ }
740
+ }
589
741
  export async function resolveAccessTransformerNamespace(svc, input) {
590
742
  const explicit = normalizeAccessTransformerNamespace(input.atNamespace);
591
743
  if (explicit) {
@@ -622,9 +774,15 @@ export async function resolveAccessTransformerNamespace(svc, input) {
622
774
  export async function resolveAccessTransformerRuntimeArtifact(svc, input) {
623
775
  const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
624
776
  let version = input.version;
625
- if (input.preferProjectVersion && normalizedProjectPath) {
626
- const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
627
- version = detected ?? version;
777
+ // The workspace's own declared version is read whenever a project is given:
778
+ // preferProjectVersion decides whether it OVERRIDES the requested version,
779
+ // but discovery needs it either way to anchor project-local artifacts whose
780
+ // filenames carry only a loader version.
781
+ const projectMinecraftVersion = normalizedProjectPath
782
+ ? await svc.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath)
783
+ : undefined;
784
+ if (input.preferProjectVersion && projectMinecraftVersion) {
785
+ version = projectMinecraftVersion;
628
786
  }
629
787
  const requestedScope = input.scope ?? (normalizedProjectPath ? "loader" : "vanilla");
630
788
  if (requestedScope === "vanilla") {
@@ -655,7 +813,8 @@ export async function resolveAccessTransformerRuntimeArtifact(svc, input) {
655
813
  gradleUserHome: input.gradleUserHome,
656
814
  requestedScope,
657
815
  atNamespace: input.atNamespace,
658
- loader
816
+ loader,
817
+ projectMinecraftVersion
659
818
  });
660
819
  if (!discovery.selected) {
661
820
  throw createError({
@@ -670,7 +829,13 @@ export async function resolveAccessTransformerRuntimeArtifact(svc, input) {
670
829
  candidateArtifacts: discovery.candidateArtifacts,
671
830
  loaderEvidence: loaderDetection.evidence,
672
831
  loaderWarnings: loaderDetection.warnings,
673
- nextAction: "Provide projectPath for a Forge/NeoForge workspace with generated runtime jars, or run the Gradle tasks that populate transformed runtime artifacts before retrying."
832
+ ...(projectMinecraftVersion ? { projectMinecraftVersion } : {}),
833
+ // An error must never ask for something the caller already sent.
834
+ nextAction: normalizedProjectPath
835
+ ? `Searched the workspace "${normalizedProjectPath}" and the Gradle caches but found no runtime jar for Minecraft ${version}${projectMinecraftVersion && projectMinecraftVersion !== version
836
+ ? ` (the workspace declares ${projectMinecraftVersion}; retry with version="${projectMinecraftVersion}" or preferProjectVersion=true)`
837
+ : ""}. Run the Gradle task that populates transformed runtime artifacts (NeoForge/ModDevGradle writes them under build/moddev/artifacts), then retry.`
838
+ : "Provide projectPath for a Forge/NeoForge workspace with generated runtime jars, or run the Gradle tasks that populate transformed runtime artifacts before retrying."
674
839
  }
675
840
  });
676
841
  }
@@ -690,15 +855,38 @@ export async function resolveAccessTransformerRuntimeArtifact(svc, input) {
690
855
  : "Resolved the closest transformed runtime artifact for validation."
691
856
  }
692
857
  : undefined;
858
+ const resolutionNotes = [
859
+ ...(scopeFallback ? [scopeFallback.reason] : []),
860
+ ...(selected.versionEvidence && selected.versionEvidence !== "exact-token"
861
+ ? [
862
+ selected.versionEvidence === "loader-token"
863
+ ? `Runtime jar matched Minecraft ${version} through its loader version token (ModDevGradle names artifacts after the loader, not Minecraft).`
864
+ : `Runtime jar matched Minecraft ${version} because it lives in the workspace build directory of a project declaring that version.`
865
+ ]
866
+ : [])
867
+ ];
868
+ const servedAt = describeServedRuntimeJar({
869
+ jarPath: selected.jarPath,
870
+ requestedVersion: version,
871
+ // An access transformer is a Forge/NeoForge artifact; the workspace loader
872
+ // refines that when it is known.
873
+ expectedLoader: loader === "forge" ? "forge" : loader === "neoforge" ? "neoforge" : undefined
874
+ });
875
+ resolutionNotes.push(...servedAt.notes);
693
876
  return {
694
- version,
877
+ version: servedAt.version,
695
878
  jarPath: selected.jarPath,
879
+ ...(servedAt.requestedVersion ? { requestedVersion: servedAt.requestedVersion } : {}),
880
+ ...(servedAt.versionApproximated ? { versionApproximated: true } : {}),
881
+ servedLoader: servedAt.servedLoader,
882
+ ...(servedAt.expectedLoader ? { expectedLoader: servedAt.expectedLoader } : {}),
883
+ ...(servedAt.loaderMismatch ? { loaderMismatch: true } : {}),
696
884
  requestedScope,
697
885
  appliedScope: selected.appliedScope,
698
886
  requestedMapping: input.atNamespace,
699
887
  mappingApplied,
700
888
  origin: selected.origin,
701
- resolutionNotes: scopeFallback ? [scopeFallback.reason] : undefined,
889
+ resolutionNotes: resolutionNotes.length > 0 ? resolutionNotes : undefined,
702
890
  scopeFallback
703
891
  };
704
892
  }
@@ -748,7 +936,7 @@ export async function resolveBinaryFallbackArtifact(svc, input) {
748
936
  return undefined;
749
937
  }
750
938
  try {
751
- const fallbackResolved = await resolveSourceTargetInternal({ kind: "jar", value: binaryJarPath }, { allowDecompile: true, preferBinaryOnly: true }, svc.config);
939
+ const fallbackResolved = await resolveSourceTargetInternal({ kind: "jar", value: binaryJarPath }, { allowDecompile: input.allowDecompile ?? true, preferBinaryOnly: true }, svc.config);
752
940
  fallbackResolved.version = fallbackResolved.version ?? input.version;
753
941
  fallbackResolved.coordinate = fallbackResolved.coordinate ?? input.coordinate;
754
942
  fallbackResolved.requestedMapping = input.requestedMapping;
@@ -844,8 +1032,19 @@ async function computeBinaryRemapGate(svc, input) {
844
1032
  warnings: baseline.warnings
845
1033
  };
846
1034
  }
1035
+ /**
1036
+ * Describe what an artifact's indexed contents are.
1037
+ *
1038
+ * `sourceKind` is a derivation question — was the text produced by decompiling
1039
+ * bytecode? — and only the persisted `isDecompiled` flag answers it. `origin`
1040
+ * records provenance (where the bytes came from) and is deliberately not read:
1041
+ * the two axes disagree in practice, most visibly for a Jar-in-Jar shell, whose
1042
+ * row keeps origin "decompiled" while ingest clears the derivation flag. It
1043
+ * stays in the input shape only because every caller already carries it
1044
+ * alongside the fields that are read.
1045
+ */
847
1046
  export function buildArtifactContentsSummary(_svc, input) {
848
- const sourceKind = input.isDecompiled || input.origin === "decompiled" || !normalizeOptionalString(input.sourceJarPath)
1047
+ const sourceKind = input.isDecompiled || !normalizeOptionalString(input.sourceJarPath)
849
1048
  ? "decompiled-binary"
850
1049
  : "source-jar";
851
1050
  const sourceCoverage = hasPartialNetMinecraftCoverage(input.qualityFlags) ? "partial" : "full";
@@ -1233,7 +1432,20 @@ export async function resolveArtifact(svc, input) {
1233
1432
  });
1234
1433
  }
1235
1434
  resolved.qualityFlags.push("version-approximated");
1236
- warnings.push(`Requested version "${value}" but resolved source jar does not contain exact version string: ${versionSourceDiscovery.selectedSourceJarPath}`);
1435
+ // Provenance must name the version that was SERVED. Echoing the request
1436
+ // here made a fallback indistinguishable from an exact hit, and left
1437
+ // downstream version context pointing at a version the jar is not.
1438
+ const servedVersion = inferRuntimeJarMinecraftVersion(versionSourceDiscovery.selectedSourceJarPath);
1439
+ provenance.versionApproximation = {
1440
+ requestedVersion: value,
1441
+ ...(servedVersion ? { servedVersion } : {}),
1442
+ sourceJarPath: versionSourceDiscovery.selectedSourceJarPath
1443
+ };
1444
+ if (servedVersion) {
1445
+ provenance.resolvedFrom.version = servedVersion;
1446
+ }
1447
+ warnings.push(`Requested version "${value}" but resolved source jar does not contain exact version string: ${versionSourceDiscovery.selectedSourceJarPath}` +
1448
+ (servedVersion ? ` (serving Minecraft ${servedVersion})` : ""));
1237
1449
  }
1238
1450
  }
1239
1451
  resolved.qualityFlags = dedupeQualityFlags(resolved.qualityFlags);
@@ -40,10 +40,46 @@ export declare function buildFallbackProvenance(svc: SourceService, input: {
40
40
  export declare function buildClassSourceNotFoundError(svc: SourceService, input: {
41
41
  className: string;
42
42
  lookupClassName: string;
43
+ /**
44
+ * Artifact the lookup ended on. TWO internal paths move it off the requested
45
+ * artifact: the binary fallback, and the nested-jar redirect that follows a
46
+ * shell jar's bundled inner jar.
47
+ */
43
48
  artifactId: string;
49
+ /**
50
+ * Artifact the caller actually asked about. Both internal redirects swap the
51
+ * active artifact mid-lookup, but the error must keep answering about the
52
+ * requested one: reporting the redirect target sends the caller to an artifact
53
+ * they never named. Defaults to `artifactId` for paths with no redirect.
54
+ */
55
+ requestedArtifactId?: string;
56
+ /**
57
+ * Mapping and quality flags OF THE REQUESTED ARTIFACT. `details.artifactId`
58
+ * names the requested artifact, so `details.mapping` (which reaches
59
+ * `error.context` through the allowlist) and `details.qualityFlags` have to
60
+ * describe that same artifact. The nested-jar redirect REPLACES the active
61
+ * values with the inner jar's, which would otherwise publish one artifact's
62
+ * identity beside another's namespace and quality — and point the
63
+ * `suggestedCall` at an index holding neither the class nor its siblings.
64
+ */
44
65
  mappingApplied: SourceMapping;
45
66
  requestedMapping: SourceMapping;
46
67
  qualityFlags: string[];
68
+ /**
69
+ * The mapping the CALLER wrote in the request, undefined when they omitted it.
70
+ * Distinct from `requestedMapping`, which falls back to the artifact's own
71
+ * namespace: only the caller-supplied value can tell whether the obfuscated
72
+ * namespace hint would be re-asking for an argument that is already present.
73
+ */
74
+ callerSuppliedMapping?: SourceMapping;
75
+ /**
76
+ * Whether the REQUESTED artifact is a native dependency, i.e. its provenance
77
+ * carries `dependencyResolution`. Such artifacts are handed
78
+ * `mappingApplied: "obfuscated"` by substitution rather than by being an
79
+ * obfuscated index, which the obfuscated namespace hint must not mistake for
80
+ * a missing mapping argument.
81
+ */
82
+ nativeDependency?: boolean;
47
83
  attemptedBinaryFallback: boolean;
48
84
  filePath?: string;
49
85
  targetKind?: string;