@adhisang/minecraft-modding-mcp 3.1.1 → 3.2.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 (41) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +20 -8
  3. package/dist/access-transformer-parser.d.ts +17 -0
  4. package/dist/access-transformer-parser.js +97 -0
  5. package/dist/concurrency.d.ts +1 -0
  6. package/dist/concurrency.js +24 -0
  7. package/dist/decompiler/vineflower.js +22 -21
  8. package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
  9. package/dist/entry-tools/analyze-symbol-service.d.ts +20 -20
  10. package/dist/entry-tools/inspect-minecraft-service.d.ts +148 -148
  11. package/dist/entry-tools/validate-project-service.d.ts +153 -16
  12. package/dist/entry-tools/validate-project-service.js +360 -23
  13. package/dist/gradle-paths.d.ts +4 -0
  14. package/dist/gradle-paths.js +57 -0
  15. package/dist/index.js +65 -13
  16. package/dist/mapping-pipeline-service.d.ts +3 -1
  17. package/dist/mapping-pipeline-service.js +16 -1
  18. package/dist/mapping-service.d.ts +4 -0
  19. package/dist/mapping-service.js +155 -60
  20. package/dist/minecraft-explorer-service.d.ts +13 -0
  21. package/dist/minecraft-explorer-service.js +8 -4
  22. package/dist/mixin-validator.d.ts +33 -2
  23. package/dist/mixin-validator.js +197 -11
  24. package/dist/mod-analyzer.d.ts +1 -0
  25. package/dist/mod-analyzer.js +17 -1
  26. package/dist/mod-decompile-service.js +4 -4
  27. package/dist/mod-remap-service.js +1 -54
  28. package/dist/mod-search-service.d.ts +1 -0
  29. package/dist/mod-search-service.js +84 -51
  30. package/dist/response-utils.d.ts +35 -0
  31. package/dist/response-utils.js +113 -0
  32. package/dist/source-jar-reader.d.ts +16 -0
  33. package/dist/source-jar-reader.js +103 -1
  34. package/dist/source-resolver.js +9 -10
  35. package/dist/source-service.d.ts +22 -2
  36. package/dist/source-service.js +914 -105
  37. package/dist/tool-contract-manifest.js +8 -6
  38. package/dist/types.d.ts +17 -0
  39. package/dist/workspace-mapping-service.d.ts +13 -0
  40. package/dist/workspace-mapping-service.js +146 -14
  41. package/package.json +1 -1
@@ -1,23 +1,24 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
- import { readFile, writeFile } from "node:fs/promises";
4
- import { homedir } from "node:os";
3
+ import { access, readFile, writeFile } from "node:fs/promises";
5
4
  import { dirname, isAbsolute, resolve as resolvePath } from "node:path";
6
5
  import fastGlob from "fast-glob";
6
+ import { mapWithConcurrencyLimit } from "./concurrency.js";
7
7
  import { createError, ERROR_CODES, isAppError } from "./errors.js";
8
8
  import { loadConfig } from "./config.js";
9
9
  import { decompileBinaryJar } from "./decompiler/vineflower.js";
10
10
  import { resolveVineflowerJar } from "./vineflower-resolver.js";
11
11
  import { parseCoordinate } from "./maven-resolver.js";
12
- import { MinecraftExplorerService } from "./minecraft-explorer-service.js";
12
+ import { MinecraftExplorerService, modifierPrefix, parseFieldType, parseMethodDescriptor } from "./minecraft-explorer-service.js";
13
13
  import { parseMixinSource } from "./mixin-parser.js";
14
14
  import { parseAccessWidener } from "./access-widener-parser.js";
15
- import { validateParsedMixin, refreshMixinValidationOutcome, validateParsedAccessWidener } from "./mixin-validator.js";
15
+ import { parseAccessTransformer } from "./access-transformer-parser.js";
16
+ import { validateParsedMixin, refreshMixinValidationOutcome, validateParsedAccessWidener, validateParsedAccessTransformer } from "./mixin-validator.js";
16
17
  import { resolveSourceTarget as resolveSourceTargetInternal } from "./source-resolver.js";
17
18
  import { applyMappingPipeline } from "./mapping-pipeline-service.js";
18
19
  import { MappingService } from "./mapping-service.js";
19
20
  import { extractSymbolsFromSource } from "./symbols/symbol-extractor.js";
20
- import { iterateJavaEntriesAsUtf8, listJavaEntries } from "./source-jar-reader.js";
21
+ import { detectFabricLikeInputNamespace, iterateJavaEntriesAsUtf8, listJavaEntries } from "./source-jar-reader.js";
21
22
  import { openDatabase } from "./storage/db.js";
22
23
  import { ArtifactsRepo } from "./storage/artifacts-repo.js";
23
24
  import { FilesRepo } from "./storage/files-repo.js";
@@ -26,6 +27,7 @@ import { SymbolsRepo } from "./storage/symbols-repo.js";
26
27
  import { RuntimeMetrics } from "./observability.js";
27
28
  import { log } from "./logger.js";
28
29
  import { normalizePathForHost } from "./path-converter.js";
30
+ import { buildLoaderRuntimeSearchRoots, buildVersionSourceSearchRoots, normalizeOptionalProjectPath } from "./gradle-paths.js";
29
31
  import { createSearchHitAccumulator, decodeSearchCursor, encodeSearchCursor } from "./search-hit-accumulator.js";
30
32
  import { WorkspaceMappingService } from "./workspace-mapping-service.js";
31
33
  import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
@@ -178,6 +180,30 @@ function hasExactVersionToken(path, version) {
178
180
  ?? rememberCachedRegex(VERSION_TOKEN_REGEX_CACHE, normalizedVersion, new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}([^0-9a-z]|$)`, "i"));
179
181
  return pattern.test(normalizedPath);
180
182
  }
183
+ function inferMergedRuntimeNamespaceHint(path) {
184
+ const normalizedPath = normalizePathStyle(path).toLowerCase();
185
+ if (normalizedPath.includes("merged-intermediary-v2") ||
186
+ normalizedPath.includes("merged-intermediary")) {
187
+ return "intermediary";
188
+ }
189
+ if (normalizedPath.includes("minecraft-merged-mojang") ||
190
+ normalizedPath.includes("merged-mojang")) {
191
+ return "mojang";
192
+ }
193
+ if (normalizedPath.includes("merged-named")) {
194
+ return "named";
195
+ }
196
+ return undefined;
197
+ }
198
+ function runtimeJarNamespaceHintScore(hint) {
199
+ if (hint === "intermediary" || hint === "mojang") {
200
+ return 8_000;
201
+ }
202
+ if (hint === "named") {
203
+ return 1_000;
204
+ }
205
+ return 0;
206
+ }
181
207
  function looksLikeDeobfuscatedClassName(value) {
182
208
  const trimmed = value.trim();
183
209
  if (!trimmed) {
@@ -204,17 +230,6 @@ function buildResolveArtifactParams(target, extra = {}) {
204
230
  ...extra
205
231
  };
206
232
  }
207
- function normalizeOptionalProjectPath(projectPath) {
208
- if (!projectPath) {
209
- return undefined;
210
- }
211
- const trimmed = projectPath.trim();
212
- if (!trimmed) {
213
- return undefined;
214
- }
215
- const normalized = normalizePathForHost(trimmed, undefined, "projectPath");
216
- return isAbsolute(normalized) ? normalized : resolvePath(process.cwd(), normalized);
217
- }
218
233
  function looksLikeClassSegment(name) {
219
234
  const trimmed = name.trim();
220
235
  return /^[A-Z_$]/.test(trimmed);
@@ -227,29 +242,6 @@ function looksLikeJvmMethodDescriptor(descriptor) {
227
242
  const closing = trimmed.indexOf(")");
228
243
  return closing > 0 && closing < trimmed.length - 1;
229
244
  }
230
- function resolveGradleUserHomePath() {
231
- const configured = process.env.GRADLE_USER_HOME?.trim();
232
- if (!configured) {
233
- return resolvePath(homedir(), ".gradle");
234
- }
235
- const normalized = normalizePathForHost(configured, undefined, "GRADLE_USER_HOME");
236
- return isAbsolute(normalized) ? normalized : resolvePath(process.cwd(), normalized);
237
- }
238
- function buildVersionSourceSearchRoots(projectPath) {
239
- const roots = new Set();
240
- if (projectPath) {
241
- roots.add(resolvePath(projectPath, ".gradle", "loom-cache"));
242
- roots.add(resolvePath(projectPath, ".gradle-user", "caches", "fabric-loom"));
243
- roots.add(resolvePath(projectPath, ".gradle", "caches", "fabric-loom"));
244
- const projectParent = dirname(projectPath);
245
- roots.add(resolvePath(projectParent, ".gradle-user-home", "loom-cache"));
246
- roots.add(resolvePath(projectParent, ".gradle-user-home", "caches", "fabric-loom"));
247
- }
248
- const homeGradle = resolveGradleUserHomePath();
249
- roots.add(resolvePath(homeGradle, "loom-cache"));
250
- roots.add(resolvePath(homeGradle, "caches", "fabric-loom"));
251
- return [...roots];
252
- }
253
245
  function looksLikeMinecraftSourceArtifact(path, hasMinecraftNamespace) {
254
246
  if (hasMinecraftNamespace) {
255
247
  return true;
@@ -328,27 +320,6 @@ function normalizeOptionalString(value) {
328
320
  const trimmed = value.trim();
329
321
  return trimmed ? trimmed : undefined;
330
322
  }
331
- async function mapWithConcurrencyLimit(items, limit, mapper) {
332
- if (items.length === 0) {
333
- return [];
334
- }
335
- const results = new Array(items.length);
336
- let nextIndex = 0;
337
- const workerCount = Math.max(1, Math.min(Math.trunc(limit), items.length));
338
- await Promise.all(Array.from({ length: workerCount }, async () => {
339
- while (true) {
340
- // Safe in Node's single-threaded event loop because no await occurs between
341
- // reading and incrementing nextIndex inside this synchronous dispatch section.
342
- const currentIndex = nextIndex;
343
- nextIndex += 1;
344
- if (currentIndex >= items.length) {
345
- return;
346
- }
347
- results[currentIndex] = await mapper(items[currentIndex], currentIndex);
348
- }
349
- }));
350
- return results;
351
- }
352
323
  function normalizeStrictPositiveInt(value, field) {
353
324
  if (value == null) {
354
325
  return undefined;
@@ -386,6 +357,15 @@ const MIXIN_PROJECT_DISCOVERY_IGNORES = [
386
357
  "**/out/**",
387
358
  "**/node_modules/**"
388
359
  ];
360
+ async function pathExists(filePath) {
361
+ try {
362
+ await access(filePath);
363
+ return true;
364
+ }
365
+ catch {
366
+ return false;
367
+ }
368
+ }
389
369
  function normalizeMapping(mapping) {
390
370
  if (mapping == null) {
391
371
  return "obfuscated";
@@ -422,6 +402,19 @@ function normalizeAccessWidenerNamespace(namespace) {
422
402
  }
423
403
  return undefined;
424
404
  }
405
+ function normalizeAccessTransformerNamespace(namespace) {
406
+ const normalized = namespace?.trim().toLowerCase();
407
+ if (!normalized) {
408
+ return undefined;
409
+ }
410
+ if (normalized === "srg" || normalized === "mojang" || normalized === "obfuscated") {
411
+ return normalized;
412
+ }
413
+ return undefined;
414
+ }
415
+ function isSourceMappingNamespace(namespace) {
416
+ return namespace === "obfuscated" || namespace === "mojang" || namespace === "intermediary" || namespace === "yarn";
417
+ }
425
418
  function normalizeMemberAccess(access) {
426
419
  if (access == null) {
427
420
  return "public";
@@ -809,7 +802,7 @@ export class SourceService {
809
802
  searchedPaths.push(root);
810
803
  let discovered = [];
811
804
  try {
812
- discovered = fastGlob.sync("**/*sources.jar", {
805
+ discovered = await fastGlob.glob("**/*sources.jar", {
813
806
  cwd: root,
814
807
  absolute: true,
815
808
  onlyFiles: true
@@ -873,6 +866,371 @@ export class SourceService {
873
866
  selectedHasMinecraftNamespace: selected?.hasMinecraftNamespace
874
867
  };
875
868
  }
869
+ async discoverAccessWidenerRuntimeCandidates(input) {
870
+ const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
871
+ const normalizedProjectPathLower = normalizedProjectPath
872
+ ? normalizePathStyle(normalizedProjectPath).toLowerCase()
873
+ : undefined;
874
+ const searchRoots = buildVersionSourceSearchRoots(normalizedProjectPath);
875
+ const searchedPaths = [];
876
+ const candidates = [];
877
+ const seen = new Set();
878
+ for (const root of searchRoots) {
879
+ searchedPaths.push(root);
880
+ let discovered = [];
881
+ try {
882
+ discovered = await fastGlob.glob(["**/*minecraft*.jar", "**/*merged*.jar"], {
883
+ cwd: root,
884
+ absolute: true,
885
+ onlyFiles: true,
886
+ ignore: ["**/*sources.jar", "**/node_modules/**", "**/.git/**", "**/build/**", "**/out/**"]
887
+ });
888
+ }
889
+ catch {
890
+ continue;
891
+ }
892
+ for (const candidatePath of discovered) {
893
+ const normalizedPath = normalizePathStyle(candidatePath);
894
+ if (seen.has(normalizedPath)) {
895
+ continue;
896
+ }
897
+ seen.add(normalizedPath);
898
+ const lower = normalizedPath.toLowerCase();
899
+ if (!lower.includes("minecraft")) {
900
+ continue;
901
+ }
902
+ const exactVersionMatch = hasExactVersionToken(normalizedPath, input.version);
903
+ const looksMerged = lower.includes("minecraft-merged") || lower.includes("/merged/") || lower.includes("-merged");
904
+ const namespaceHint = inferMergedRuntimeNamespaceHint(normalizedPath);
905
+ const appliedScope = looksMerged
906
+ ? "merged"
907
+ : input.requestedScope === "loader"
908
+ ? "merged"
909
+ : input.requestedScope;
910
+ const score = (exactVersionMatch ? 5_000 : 0) +
911
+ (looksMerged ? 4_000 : 0) +
912
+ runtimeJarNamespaceHintScore(namespaceHint) +
913
+ (normalizedProjectPathLower && lower.startsWith(normalizedProjectPathLower) ? 2_000 : 0) +
914
+ (lower.includes("loom-cache") || lower.includes("/caches/fabric-loom/") ? 500 : 0) +
915
+ (lower.includes("minecraft-client") || lower.includes("client") ? 100 : 0);
916
+ candidates.push({
917
+ jarPath: normalizedPath,
918
+ score,
919
+ appliedScope,
920
+ origin: lower.includes("loom-cache") || lower.includes("/caches/fabric-loom/")
921
+ ? "loom-cache"
922
+ : "local-jar",
923
+ namespaceHint
924
+ });
925
+ }
926
+ }
927
+ candidates.sort((left, right) => {
928
+ if (right.score !== left.score) {
929
+ return right.score - left.score;
930
+ }
931
+ return left.jarPath.localeCompare(right.jarPath);
932
+ });
933
+ return {
934
+ searchedPaths,
935
+ candidateArtifacts: candidates.slice(0, 20).map((candidate) => candidate.jarPath),
936
+ selected: candidates[0]
937
+ };
938
+ }
939
+ async discoverAccessTransformerRuntimeCandidates(input) {
940
+ const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
941
+ const normalizedProjectPathLower = normalizedProjectPath
942
+ ? normalizePathStyle(normalizedProjectPath).toLowerCase()
943
+ : undefined;
944
+ const searchRoots = buildLoaderRuntimeSearchRoots(normalizedProjectPath);
945
+ const searchedPaths = [];
946
+ const candidates = [];
947
+ const seen = new Set();
948
+ const globs = [
949
+ "**/*minecraft*.jar",
950
+ "**/*patched*.jar",
951
+ "**/*srg*.jar",
952
+ "**/*joined*.jar",
953
+ "**/*client-extra*.jar",
954
+ "**/*forge*.jar",
955
+ "**/*neoforge*.jar",
956
+ "**/*moddev*.jar",
957
+ "**/*neoform*.jar"
958
+ ];
959
+ for (const root of searchRoots) {
960
+ searchedPaths.push(root);
961
+ if (!(await pathExists(root))) {
962
+ continue;
963
+ }
964
+ let discovered = [];
965
+ try {
966
+ discovered = await fastGlob.glob(globs, {
967
+ cwd: root,
968
+ absolute: true,
969
+ onlyFiles: true,
970
+ ignore: ["**/*sources.jar", "**/node_modules/**", "**/.git/**", "**/out/**"]
971
+ });
972
+ }
973
+ catch {
974
+ continue;
975
+ }
976
+ for (const candidatePath of discovered) {
977
+ const normalizedPath = normalizePathStyle(candidatePath);
978
+ if (seen.has(normalizedPath)) {
979
+ continue;
980
+ }
981
+ seen.add(normalizedPath);
982
+ const lower = normalizedPath.toLowerCase();
983
+ if (!hasExactVersionToken(normalizedPath, input.version)) {
984
+ continue;
985
+ }
986
+ const looksMerged = lower.includes("merged");
987
+ const looksSrg = lower.includes("srg");
988
+ const looksForge = lower.includes("forge");
989
+ const looksNeoForge = lower.includes("neoforge") || lower.includes("moddev") || lower.includes("neoform");
990
+ const looksPatchedRuntime = lower.includes("patched") || lower.includes("client-extra") || lower.includes("joined");
991
+ const appliedScope = looksMerged
992
+ ? "merged"
993
+ : "loader";
994
+ if (input.atNamespace === "srg" && !looksSrg) {
995
+ continue;
996
+ }
997
+ if (input.loader === "forge" && !looksForge && !looksSrg && !looksPatchedRuntime) {
998
+ continue;
999
+ }
1000
+ if (input.loader === "neoforge" && !looksNeoForge && !looksPatchedRuntime && !lower.includes("minecraft")) {
1001
+ continue;
1002
+ }
1003
+ const score = 10_000 +
1004
+ (normalizedProjectPathLower && lower.startsWith(normalizedProjectPathLower) ? 4_000 : 0) +
1005
+ (looksPatchedRuntime ? 3_000 : 0) +
1006
+ (looksSrg ? 2_500 : 0) +
1007
+ (input.loader === "forge" && looksForge ? 1_500 : 0) +
1008
+ (input.loader === "neoforge" && looksNeoForge ? 1_500 : 0) +
1009
+ (input.requestedScope === appliedScope ? 1_000 : 0) +
1010
+ (looksMerged ? -500 : 0);
1011
+ candidates.push({
1012
+ jarPath: normalizedPath,
1013
+ score,
1014
+ appliedScope,
1015
+ origin: "local-jar"
1016
+ });
1017
+ }
1018
+ }
1019
+ candidates.sort((left, right) => {
1020
+ if (right.score !== left.score) {
1021
+ return right.score - left.score;
1022
+ }
1023
+ return left.jarPath.localeCompare(right.jarPath);
1024
+ });
1025
+ return {
1026
+ searchedPaths,
1027
+ candidateArtifacts: candidates.slice(0, 20).map((candidate) => candidate.jarPath),
1028
+ selected: candidates[0]
1029
+ };
1030
+ }
1031
+ async resolveAccessWidenerRuntimeArtifact(input) {
1032
+ const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
1033
+ let version = input.version;
1034
+ if (input.preferProjectVersion && normalizedProjectPath) {
1035
+ const detected = await this.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
1036
+ version = detected ?? version;
1037
+ }
1038
+ const requestedScope = input.scope ?? (normalizedProjectPath ? "loader" : "vanilla");
1039
+ if (requestedScope === "vanilla") {
1040
+ const versionJar = await this.versionService.resolveVersionJar(version);
1041
+ return {
1042
+ version: versionJar.version,
1043
+ jarPath: versionJar.jarPath,
1044
+ requestedScope,
1045
+ appliedScope: "vanilla",
1046
+ requestedMapping: input.awNamespace,
1047
+ mappingApplied: "obfuscated",
1048
+ origin: "version-jar"
1049
+ };
1050
+ }
1051
+ const discovery = await this.discoverAccessWidenerRuntimeCandidates({
1052
+ version,
1053
+ projectPath: normalizedProjectPath,
1054
+ requestedScope
1055
+ });
1056
+ if (!discovery.selected) {
1057
+ throw createError({
1058
+ code: ERROR_CODES.CONTEXT_UNRESOLVED,
1059
+ message: "Could not resolve a runtime jar for Access Widener validation.",
1060
+ details: {
1061
+ version,
1062
+ requestedScope,
1063
+ projectPath: normalizedProjectPath,
1064
+ searchedPaths: discovery.searchedPaths,
1065
+ candidateArtifacts: discovery.candidateArtifacts,
1066
+ nextAction: "Provide projectPath for a Loom workspace with generated runtime jars, or run Gradle tasks that populate the Loom cache before retrying.",
1067
+ suggestedCall: {
1068
+ tool: "validate-access-widener",
1069
+ params: {
1070
+ version,
1071
+ scope: requestedScope,
1072
+ ...(normalizedProjectPath ? { projectPath: normalizedProjectPath } : {})
1073
+ }
1074
+ }
1075
+ }
1076
+ });
1077
+ }
1078
+ const appliedScope = discovery.selected.appliedScope;
1079
+ const scopeFallback = requestedScope !== appliedScope
1080
+ ? {
1081
+ requested: requestedScope,
1082
+ applied: appliedScope,
1083
+ reason: requestedScope === "loader"
1084
+ ? "Fabric loader runtime validation currently reuses the merged runtime jar."
1085
+ : "Selected runtime jar matched a nearby merged artifact."
1086
+ }
1087
+ : undefined;
1088
+ let detectedMapping;
1089
+ const notes = [];
1090
+ if (scopeFallback) {
1091
+ notes.push(scopeFallback.reason);
1092
+ }
1093
+ if (isUnobfuscatedVersion(version)) {
1094
+ detectedMapping = "obfuscated";
1095
+ }
1096
+ else if (discovery.selected.namespaceHint === "intermediary" ||
1097
+ discovery.selected.namespaceHint === "mojang") {
1098
+ detectedMapping = discovery.selected.namespaceHint;
1099
+ }
1100
+ else {
1101
+ const detection = await detectFabricLikeInputNamespace(discovery.selected.jarPath);
1102
+ detectedMapping = detection.fromNamespace;
1103
+ if (detection.warnings.length > 0) {
1104
+ notes.push(...detection.warnings);
1105
+ }
1106
+ }
1107
+ return {
1108
+ version,
1109
+ jarPath: discovery.selected.jarPath,
1110
+ requestedScope,
1111
+ appliedScope,
1112
+ requestedMapping: input.awNamespace,
1113
+ mappingApplied: detectedMapping,
1114
+ origin: discovery.selected.origin,
1115
+ resolutionNotes: notes.length > 0 ? notes : undefined,
1116
+ scopeFallback
1117
+ };
1118
+ }
1119
+ async resolveAccessTransformerNamespace(input) {
1120
+ const explicit = normalizeAccessTransformerNamespace(input.atNamespace);
1121
+ if (explicit) {
1122
+ return explicit;
1123
+ }
1124
+ const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
1125
+ if (!normalizedProjectPath) {
1126
+ throw createError({
1127
+ code: ERROR_CODES.INVALID_INPUT,
1128
+ message: "atNamespace is required when projectPath is not provided.",
1129
+ details: {
1130
+ nextAction: "Pass atNamespace explicitly, or provide projectPath for a Forge/NeoForge workspace so the namespace can be inferred."
1131
+ }
1132
+ });
1133
+ }
1134
+ const loaderDetection = await this.workspaceMappingService.detectProjectLoader(normalizedProjectPath);
1135
+ if (loaderDetection.resolved && loaderDetection.loader === "forge") {
1136
+ return "srg";
1137
+ }
1138
+ if (loaderDetection.resolved && loaderDetection.loader === "neoforge") {
1139
+ return "mojang";
1140
+ }
1141
+ throw createError({
1142
+ code: ERROR_CODES.INVALID_INPUT,
1143
+ message: "Could not infer atNamespace from the workspace.",
1144
+ details: {
1145
+ projectPath: normalizedProjectPath,
1146
+ evidence: loaderDetection.evidence,
1147
+ warnings: loaderDetection.warnings,
1148
+ nextAction: "Pass atNamespace explicitly, or point projectPath at a Forge/NeoForge workspace."
1149
+ }
1150
+ });
1151
+ }
1152
+ async resolveAccessTransformerRuntimeArtifact(input) {
1153
+ const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
1154
+ let version = input.version;
1155
+ if (input.preferProjectVersion && normalizedProjectPath) {
1156
+ const detected = await this.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
1157
+ version = detected ?? version;
1158
+ }
1159
+ const requestedScope = input.scope ?? (normalizedProjectPath ? "loader" : "vanilla");
1160
+ if (requestedScope === "vanilla") {
1161
+ if (input.atNamespace === "srg") {
1162
+ throw createError({
1163
+ code: ERROR_CODES.INVALID_INPUT,
1164
+ message: "atNamespace=srg requires projectPath and scope=loader so a Forge runtime jar can be resolved."
1165
+ });
1166
+ }
1167
+ const versionJar = await this.versionService.resolveVersionJar(version);
1168
+ return {
1169
+ version: versionJar.version,
1170
+ jarPath: versionJar.jarPath,
1171
+ requestedScope,
1172
+ appliedScope: "vanilla",
1173
+ requestedMapping: input.atNamespace,
1174
+ mappingApplied: "obfuscated",
1175
+ origin: "version-jar"
1176
+ };
1177
+ }
1178
+ const loaderDetection = normalizedProjectPath
1179
+ ? await this.workspaceMappingService.detectProjectLoader(normalizedProjectPath)
1180
+ : { resolved: false, loader: undefined, evidence: [], warnings: [] };
1181
+ const loader = loaderDetection.resolved ? loaderDetection.loader ?? "unknown" : "unknown";
1182
+ const discovery = await this.discoverAccessTransformerRuntimeCandidates({
1183
+ version,
1184
+ projectPath: normalizedProjectPath,
1185
+ requestedScope,
1186
+ atNamespace: input.atNamespace,
1187
+ loader
1188
+ });
1189
+ if (!discovery.selected) {
1190
+ throw createError({
1191
+ code: ERROR_CODES.CONTEXT_UNRESOLVED,
1192
+ message: "Could not resolve a runtime jar for Access Transformer validation.",
1193
+ details: {
1194
+ version,
1195
+ requestedScope,
1196
+ atNamespace: input.atNamespace,
1197
+ projectPath: normalizedProjectPath,
1198
+ searchedPaths: discovery.searchedPaths,
1199
+ candidateArtifacts: discovery.candidateArtifacts,
1200
+ loaderEvidence: loaderDetection.evidence,
1201
+ loaderWarnings: loaderDetection.warnings,
1202
+ nextAction: "Provide projectPath for a Forge/NeoForge workspace with generated runtime jars, or run the Gradle tasks that populate transformed runtime artifacts before retrying."
1203
+ }
1204
+ });
1205
+ }
1206
+ const selected = discovery.selected;
1207
+ const selectedLower = selected.jarPath.toLowerCase();
1208
+ const mappingApplied = input.atNamespace === "srg" || selectedLower.includes("srg")
1209
+ ? "srg"
1210
+ : loader === "neoforge" || selectedLower.includes("moddev") || selectedLower.includes("neoforge")
1211
+ ? "mojang"
1212
+ : "obfuscated";
1213
+ const scopeFallback = requestedScope !== selected.appliedScope
1214
+ ? {
1215
+ requested: requestedScope,
1216
+ applied: selected.appliedScope,
1217
+ reason: selected.appliedScope === "merged"
1218
+ ? "Resolved a nearby merged runtime jar because no transformed loader artifact was available."
1219
+ : "Resolved the closest transformed runtime artifact for validation."
1220
+ }
1221
+ : undefined;
1222
+ return {
1223
+ version,
1224
+ jarPath: selected.jarPath,
1225
+ requestedScope,
1226
+ appliedScope: selected.appliedScope,
1227
+ requestedMapping: input.atNamespace,
1228
+ mappingApplied,
1229
+ origin: selected.origin,
1230
+ resolutionNotes: scopeFallback ? [scopeFallback.reason] : undefined,
1231
+ scopeFallback
1232
+ };
1233
+ }
876
1234
  buildVersionSourceRecoveryCommand(projectPath) {
877
1235
  const normalizedProjectPath = normalizeOptionalProjectPath(projectPath);
878
1236
  const prefix = normalizedProjectPath
@@ -972,9 +1330,11 @@ export class SourceService {
972
1330
  let resolvedTarget = { kind, value };
973
1331
  let resolvedVersion;
974
1332
  let versionSourceDiscovery;
1333
+ let runtimeNamesUnobfuscated = false;
975
1334
  if (kind === "version") {
976
1335
  const versionJar = await this.versionService.resolveVersionJar(value);
977
1336
  resolvedVersion = versionJar.version;
1337
+ runtimeNamesUnobfuscated = isUnobfuscatedVersion(resolvedVersion);
978
1338
  resolvedTarget = {
979
1339
  kind: "jar",
980
1340
  value: versionJar.jarPath
@@ -989,6 +1349,9 @@ export class SourceService {
989
1349
  // coordinate validity is validated by resolver, keep version undefined on parse failure.
990
1350
  }
991
1351
  }
1352
+ if (!runtimeNamesUnobfuscated && resolvedVersion && isUnobfuscatedVersion(resolvedVersion)) {
1353
+ runtimeNamesUnobfuscated = true;
1354
+ }
992
1355
  // Unobfuscated versions (MC 26.1+) ship with deobfuscated runtime names; intermediary/yarn are not applicable.
993
1356
  let effectiveMapping = mapping;
994
1357
  if ((mapping === "intermediary" || mapping === "yarn") &&
@@ -997,7 +1360,11 @@ export class SourceService {
997
1360
  warnings.push(`Version ${resolvedVersion} is unobfuscated; ${mapping} mappings are not applicable. Using the obfuscated namespace label for the deobfuscated runtime names.`);
998
1361
  effectiveMapping = "obfuscated";
999
1362
  }
1000
- if (kind === "version" && resolvedVersion && effectiveMapping === "mojang" && scope !== "vanilla") {
1363
+ if (kind === "version" &&
1364
+ resolvedVersion &&
1365
+ effectiveMapping === "mojang" &&
1366
+ !runtimeNamesUnobfuscated &&
1367
+ scope !== "vanilla") {
1001
1368
  versionSourceDiscovery = await this.discoverVersionSourceJar({
1002
1369
  version: resolvedVersion,
1003
1370
  projectPath: input.projectPath
@@ -1032,7 +1399,8 @@ export class SourceService {
1032
1399
  mappingDecision = applyMappingPipeline({
1033
1400
  requestedMapping: effectiveMapping,
1034
1401
  target: { kind, value },
1035
- resolved
1402
+ resolved,
1403
+ runtimeNamesUnobfuscated
1036
1404
  });
1037
1405
  }
1038
1406
  catch (caughtError) {
@@ -1461,7 +1829,14 @@ export class SourceService {
1461
1829
  return this.mappingService.getClassApiMatrix(input);
1462
1830
  }
1463
1831
  async checkSymbolExists(input) {
1464
- return this.mappingService.checkSymbolExists(input);
1832
+ const result = await this.mappingService.checkSymbolExists(input);
1833
+ if (result.status !== "mapping_unavailable" ||
1834
+ !isUnobfuscatedVersion(input.version) ||
1835
+ (input.sourceMapping !== "mojang" && input.sourceMapping !== "obfuscated")) {
1836
+ return result;
1837
+ }
1838
+ const runtimeFallback = await this.checkSymbolExistsInUnobfuscatedRuntime(input, result);
1839
+ return runtimeFallback ?? result;
1465
1840
  }
1466
1841
  async resolveWorkspaceSymbol(input) {
1467
1842
  const projectPath = input.projectPath?.trim();
@@ -1683,6 +2058,147 @@ export class SourceService {
1683
2058
  warnings: [...warnings, ...mapped.warnings]
1684
2059
  };
1685
2060
  }
2061
+ async checkSymbolExistsInUnobfuscatedRuntime(input, fallbackBase) {
2062
+ const version = input.version.trim();
2063
+ const name = input.name.trim();
2064
+ const owner = input.owner?.trim();
2065
+ if (!version || !name) {
2066
+ return undefined;
2067
+ }
2068
+ if (input.kind === "class" && input.nameMode !== "fqcn" && !name.includes(".")) {
2069
+ return {
2070
+ ...fallbackBase,
2071
+ warnings: [
2072
+ ...fallbackBase.warnings,
2073
+ `Version ${version} is unobfuscated, but short class name "${name}" could not be checked against runtime bytecode without a fully-qualified name.`
2074
+ ]
2075
+ };
2076
+ }
2077
+ const querySymbol = input.kind === "class"
2078
+ ? {
2079
+ kind: "class",
2080
+ name,
2081
+ symbol: name
2082
+ }
2083
+ : input.kind === "field"
2084
+ ? {
2085
+ kind: "field",
2086
+ owner,
2087
+ name,
2088
+ symbol: `${owner}.${name}`
2089
+ }
2090
+ : {
2091
+ kind: "method",
2092
+ owner,
2093
+ name,
2094
+ descriptor: input.descriptor?.trim(),
2095
+ symbol: `${owner}.${name}${input.descriptor?.trim() ?? ""}`
2096
+ };
2097
+ const targetClass = input.kind === "class" ? name : owner;
2098
+ if (!targetClass) {
2099
+ return fallbackBase;
2100
+ }
2101
+ let jarPath;
2102
+ try {
2103
+ ({ jarPath } = await this.versionService.resolveVersionJar(version));
2104
+ }
2105
+ catch {
2106
+ return undefined;
2107
+ }
2108
+ let signature;
2109
+ try {
2110
+ signature = await this.explorerService.getSignature({
2111
+ fqn: targetClass,
2112
+ jarPath,
2113
+ access: "all"
2114
+ });
2115
+ }
2116
+ catch {
2117
+ return {
2118
+ ...fallbackBase,
2119
+ querySymbol,
2120
+ warnings: [
2121
+ ...fallbackBase.warnings,
2122
+ `Version ${version} is unobfuscated; runtime bytecode lookup could not load class "${targetClass}".`
2123
+ ]
2124
+ };
2125
+ }
2126
+ const warnings = [
2127
+ ...fallbackBase.warnings,
2128
+ ...signature.warnings,
2129
+ `Version ${version} is unobfuscated; validated symbol existence against runtime bytecode.`
2130
+ ];
2131
+ const buildResolved = (resolvedSymbol) => ({
2132
+ ...fallbackBase,
2133
+ querySymbol,
2134
+ resolved: true,
2135
+ status: "resolved",
2136
+ resolvedSymbol,
2137
+ candidates: resolvedSymbol
2138
+ ? [{
2139
+ ...resolvedSymbol,
2140
+ matchKind: "exact",
2141
+ confidence: 1
2142
+ }]
2143
+ : [],
2144
+ candidateCount: resolvedSymbol ? 1 : 0,
2145
+ warnings
2146
+ });
2147
+ const buildUnresolved = (status) => ({
2148
+ ...fallbackBase,
2149
+ querySymbol,
2150
+ resolved: false,
2151
+ status,
2152
+ resolvedSymbol: undefined,
2153
+ candidates: [],
2154
+ candidateCount: 0,
2155
+ warnings
2156
+ });
2157
+ if (input.kind === "class") {
2158
+ return buildResolved({
2159
+ kind: "class",
2160
+ name,
2161
+ symbol: name
2162
+ });
2163
+ }
2164
+ if (input.kind === "field") {
2165
+ const matched = signature.fields.filter((field) => field.name === name);
2166
+ if (matched.length !== 1) {
2167
+ return buildUnresolved(matched.length > 1 ? "ambiguous" : "not_found");
2168
+ }
2169
+ return buildResolved({
2170
+ kind: "field",
2171
+ owner,
2172
+ name,
2173
+ symbol: `${owner}.${name}`
2174
+ });
2175
+ }
2176
+ const methodCandidates = signature.methods.filter((method) => method.name === name);
2177
+ if (input.signatureMode === "name-only") {
2178
+ if (methodCandidates.length !== 1) {
2179
+ return buildUnresolved(methodCandidates.length > 1 ? "ambiguous" : "not_found");
2180
+ }
2181
+ return buildResolved({
2182
+ kind: "method",
2183
+ owner,
2184
+ name,
2185
+ descriptor: methodCandidates[0]?.jvmDescriptor,
2186
+ symbol: `${owner}.${name}${methodCandidates[0]?.jvmDescriptor ?? ""}`
2187
+ });
2188
+ }
2189
+ const descriptor = input.descriptor?.trim();
2190
+ const matched = methodCandidates.filter((method) => method.jvmDescriptor === descriptor);
2191
+ if (matched.length !== 1) {
2192
+ return buildUnresolved(matched.length > 1 ? "ambiguous" : "not_found");
2193
+ }
2194
+ return buildResolved({
2195
+ kind: "method",
2196
+ owner,
2197
+ name,
2198
+ descriptor,
2199
+ symbol: `${owner}.${name}${descriptor ?? ""}`
2200
+ });
2201
+ }
1686
2202
  async traceSymbolLifecycle(input) {
1687
2203
  const mapping = normalizeMapping(input.mapping);
1688
2204
  const { className: userClassName, methodName: userMethodName, inlineDescriptor } = parseQualifiedMethodSymbol(input.symbol);
@@ -2755,7 +3271,7 @@ export class SourceService {
2755
3271
  })), input, []);
2756
3272
  }
2757
3273
  const resolvedInput = mode === "project"
2758
- ? this.createProjectValidateMixinConfigInput(input)
3274
+ ? await this.createProjectValidateMixinConfigInput(input)
2759
3275
  : input;
2760
3276
  const { sources: configSources, warnings: configWarnings } = await this.resolveMixinConfigSources(resolvedInput);
2761
3277
  if (configSources.length === 0) {
@@ -2775,17 +3291,17 @@ export class SourceService {
2775
3291
  sourcePath: entry.sourcePath
2776
3292
  })), resolvedInput, configWarnings);
2777
3293
  }
2778
- createProjectValidateMixinConfigInput(input) {
3294
+ async createProjectValidateMixinConfigInput(input) {
2779
3295
  if (input.input.mode !== "project") {
2780
3296
  return input;
2781
3297
  }
2782
3298
  const resolvedProjectPath = this.resolveMixinInputPath(input.input.path, "path");
2783
- const configPaths = fastGlob.sync(["**/*.mixins.json"], {
3299
+ const configPaths = (await fastGlob.glob(["**/*.mixins.json"], {
2784
3300
  cwd: resolvedProjectPath,
2785
3301
  absolute: true,
2786
3302
  onlyFiles: true,
2787
3303
  ignore: [...MIXIN_PROJECT_DISCOVERY_IGNORES]
2788
- }).sort((left, right) => left.localeCompare(right));
3304
+ })).sort((left, right) => left.localeCompare(right));
2789
3305
  if (configPaths.length === 0) {
2790
3306
  throw createError({
2791
3307
  code: ERROR_CODES.INVALID_INPUT,
@@ -2828,7 +3344,8 @@ export class SourceService {
2828
3344
  name: input.className,
2829
3345
  sourceMapping: input.sourceMapping,
2830
3346
  targetMapping: input.targetMapping,
2831
- sourcePriority: input.sourcePriority
3347
+ sourcePriority: input.sourcePriority,
3348
+ projectPath: input.projectPath
2832
3349
  });
2833
3350
  }
2834
3351
  const cacheKey = [
@@ -2836,7 +3353,8 @@ export class SourceService {
2836
3353
  input.className,
2837
3354
  input.sourceMapping,
2838
3355
  input.targetMapping,
2839
- input.sourcePriority
3356
+ input.sourcePriority,
3357
+ input.projectPath ?? ""
2840
3358
  ].join("\0");
2841
3359
  const cached = cache.get(cacheKey);
2842
3360
  if (cached) {
@@ -2848,7 +3366,8 @@ export class SourceService {
2848
3366
  name: input.className,
2849
3367
  sourceMapping: input.sourceMapping,
2850
3368
  targetMapping: input.targetMapping,
2851
- sourcePriority: input.sourcePriority
3369
+ sourcePriority: input.sourcePriority,
3370
+ projectPath: input.projectPath
2852
3371
  }).catch((error) => {
2853
3372
  cache.delete(cacheKey);
2854
3373
  throw error;
@@ -3029,6 +3548,7 @@ export class SourceService {
3029
3548
  sourceMapping: requestedMapping,
3030
3549
  targetMapping: signatureLookupMapping,
3031
3550
  sourcePriority: currentSourcePriority,
3551
+ projectPath: input.projectPath,
3032
3552
  batchCaches: input.batchCaches
3033
3553
  });
3034
3554
  if (mapped.resolved && mapped.resolvedSymbol) {
@@ -3062,9 +3582,9 @@ export class SourceService {
3062
3582
  if (requestedMapping !== signatureLookupMapping) {
3063
3583
  try {
3064
3584
  const [ctorResult, methodResult, fieldResult] = await Promise.all([
3065
- this.remapSignatureMembers(sig.constructors, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings),
3066
- this.remapSignatureMembers(sig.methods, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings),
3067
- this.remapSignatureMembers(sig.fields, "field", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings)
3585
+ this.remapSignatureMembers(sig.constructors, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings, input.projectPath),
3586
+ this.remapSignatureMembers(sig.methods, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings, input.projectPath),
3587
+ this.remapSignatureMembers(sig.fields, "field", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings, input.projectPath)
3068
3588
  ]);
3069
3589
  constructors = ctorResult.members;
3070
3590
  methods = methodResult.members;
@@ -3341,11 +3861,21 @@ export class SourceService {
3341
3861
  sourceRootCandidates = input.sourceRoots;
3342
3862
  }
3343
3863
  else {
3344
- const detected = COMMON_SOURCE_ROOTS.filter((candidateRoot) => classNames.some((className) => {
3345
- const fqcn = pkg ? `${pkg}.${className}` : className;
3346
- const relative = fqcn.replace(/\./g, "/") + ".java";
3347
- return existsSync(resolvePath(projectBase, candidateRoot, relative));
3348
- }));
3864
+ const detected = [];
3865
+ for (const candidateRoot of COMMON_SOURCE_ROOTS) {
3866
+ let foundInRoot = false;
3867
+ for (const className of classNames) {
3868
+ const fqcn = pkg ? `${pkg}.${className}` : className;
3869
+ const relative = fqcn.replace(/\./g, "/") + ".java";
3870
+ if (await pathExists(resolvePath(projectBase, candidateRoot, relative))) {
3871
+ foundInRoot = true;
3872
+ break;
3873
+ }
3874
+ }
3875
+ if (foundInRoot) {
3876
+ detected.push(candidateRoot);
3877
+ }
3878
+ }
3349
3879
  sourceRootCandidates = detected.length > 0 ? detected : ["src/main/java"];
3350
3880
  }
3351
3881
  for (const cls of classNames) {
@@ -3354,7 +3884,7 @@ export class SourceService {
3354
3884
  let sourcePath = resolvePath(projectBase, sourceRootCandidates[0], relativePath);
3355
3885
  for (const root of sourceRootCandidates) {
3356
3886
  const candidate = resolvePath(projectBase, root, relativePath);
3357
- if (existsSync(candidate)) {
3887
+ if (await pathExists(candidate)) {
3358
3888
  sourcePath = candidate;
3359
3889
  break;
3360
3890
  }
@@ -3545,7 +4075,6 @@ export class SourceService {
3545
4075
  throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "content must be non-empty." });
3546
4076
  }
3547
4077
  const warnings = [];
3548
- const { jarPath } = await this.versionService.resolveVersionJar(version);
3549
4078
  const parsed = parseAccessWidener(content);
3550
4079
  const headerNamespaceRaw = normalizeOptionalString(parsed.namespace);
3551
4080
  const overrideMapping = input.mapping ? normalizeMapping(input.mapping) : undefined;
@@ -3557,7 +4086,27 @@ export class SourceService {
3557
4086
  if (overrideMapping && headerNamespace && overrideMapping !== headerNamespace) {
3558
4087
  warnings.push(`Using mapping override "${overrideMapping}" instead of header namespace "${headerNamespaceRaw}".`);
3559
4088
  }
3560
- const needsMapping = awNamespace !== "obfuscated";
4089
+ const runtimeAware = input.projectPath != null || input.scope != null || input.preferProjectVersion === true;
4090
+ let resolvedVersion = version;
4091
+ let jarPath;
4092
+ let lookupMapping = "obfuscated";
4093
+ let provenance;
4094
+ if (runtimeAware) {
4095
+ provenance = await this.resolveAccessWidenerRuntimeArtifact({
4096
+ version,
4097
+ awNamespace,
4098
+ projectPath: input.projectPath,
4099
+ scope: input.scope,
4100
+ preferProjectVersion: input.preferProjectVersion
4101
+ });
4102
+ resolvedVersion = provenance.version;
4103
+ jarPath = provenance.jarPath;
4104
+ lookupMapping = provenance.mappingApplied;
4105
+ }
4106
+ else {
4107
+ ({ jarPath } = await this.versionService.resolveVersionJar(version));
4108
+ }
4109
+ const needsLookupMapping = awNamespace !== lookupMapping;
3561
4110
  // Collect unique class FQNs from entries
3562
4111
  const classFqns = new Set();
3563
4112
  for (const entry of parsed.entries) {
@@ -3566,22 +4115,23 @@ export class SourceService {
3566
4115
  }
3567
4116
  const membersByClass = new Map();
3568
4117
  for (const fqn of classFqns) {
3569
- let obfuscatedFqn = fqn;
3570
- if (needsMapping) {
4118
+ let lookupFqn = fqn;
4119
+ if (needsLookupMapping) {
3571
4120
  try {
3572
4121
  const mapped = await this.mappingService.findMapping({
3573
- version,
4122
+ version: resolvedVersion,
3574
4123
  kind: "class",
3575
4124
  name: fqn,
3576
4125
  sourceMapping: awNamespace,
3577
- targetMapping: "obfuscated",
3578
- sourcePriority: input.sourcePriority
4126
+ targetMapping: lookupMapping,
4127
+ sourcePriority: input.sourcePriority,
4128
+ projectPath: input.projectPath
3579
4129
  });
3580
4130
  if (mapped.resolved && mapped.resolvedSymbol) {
3581
- obfuscatedFqn = mapped.resolvedSymbol.name;
4131
+ lookupFqn = mapped.resolvedSymbol.name;
3582
4132
  }
3583
4133
  else {
3584
- warnings.push(`Could not map class "${fqn}" from ${awNamespace} to obfuscated.`);
4134
+ warnings.push(`Could not map class "${fqn}" from ${awNamespace} to ${lookupMapping}.`);
3585
4135
  }
3586
4136
  }
3587
4137
  catch {
@@ -3590,23 +4140,156 @@ export class SourceService {
3590
4140
  }
3591
4141
  try {
3592
4142
  const sig = await this.explorerService.getSignature({
3593
- fqn: obfuscatedFqn,
4143
+ fqn: lookupFqn,
4144
+ jarPath,
4145
+ access: "all"
4146
+ });
4147
+ warnings.push(...sig.warnings);
4148
+ let constructors = sig.constructors;
4149
+ let methods = sig.methods;
4150
+ let fields = sig.fields;
4151
+ if (needsLookupMapping) {
4152
+ const [ctorResult, methodResult, fieldResult] = await Promise.all([
4153
+ this.remapSignatureMembers(sig.constructors, "method", resolvedVersion, lookupMapping, awNamespace, input.sourcePriority, warnings, input.projectPath),
4154
+ this.remapSignatureMembers(sig.methods, "method", resolvedVersion, lookupMapping, awNamespace, input.sourcePriority, warnings, input.projectPath),
4155
+ this.remapSignatureMembers(sig.fields, "field", resolvedVersion, lookupMapping, awNamespace, input.sourcePriority, warnings, input.projectPath)
4156
+ ]);
4157
+ constructors = ctorResult.members;
4158
+ methods = methodResult.members;
4159
+ fields = fieldResult.members;
4160
+ }
4161
+ membersByClass.set(fqn, {
4162
+ className: fqn,
4163
+ classAccessFlags: sig.classAccessFlags,
4164
+ constructors,
4165
+ methods,
4166
+ fields
4167
+ });
4168
+ }
4169
+ catch {
4170
+ warnings.push(`Could not load signature for class "${lookupFqn}".`);
4171
+ }
4172
+ }
4173
+ const result = validateParsedAccessWidener(parsed, membersByClass, warnings, {
4174
+ includeRuntimeEvidence: runtimeAware
4175
+ });
4176
+ if (provenance) {
4177
+ result.provenance = provenance;
4178
+ }
4179
+ return result;
4180
+ }
4181
+ async validateAccessTransformer(input) {
4182
+ const version = input.version.trim();
4183
+ if (!version) {
4184
+ throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "version must be non-empty." });
4185
+ }
4186
+ const content = input.content;
4187
+ if (!content.trim()) {
4188
+ throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "content must be non-empty." });
4189
+ }
4190
+ const warnings = [];
4191
+ const parsed = parseAccessTransformer(content);
4192
+ const atNamespace = await this.resolveAccessTransformerNamespace({
4193
+ atNamespace: input.atNamespace,
4194
+ projectPath: input.projectPath
4195
+ });
4196
+ const runtimeAware = input.projectPath != null || input.scope != null || input.preferProjectVersion === true;
4197
+ let resolvedVersion = version;
4198
+ let jarPath;
4199
+ let lookupMapping = "obfuscated";
4200
+ let provenance;
4201
+ if (runtimeAware) {
4202
+ provenance = await this.resolveAccessTransformerRuntimeArtifact({
4203
+ version,
4204
+ atNamespace,
4205
+ projectPath: input.projectPath,
4206
+ scope: input.scope,
4207
+ preferProjectVersion: input.preferProjectVersion
4208
+ });
4209
+ resolvedVersion = provenance.version;
4210
+ jarPath = provenance.jarPath;
4211
+ lookupMapping = provenance.mappingApplied;
4212
+ }
4213
+ else {
4214
+ if (atNamespace === "srg") {
4215
+ throw createError({
4216
+ code: ERROR_CODES.INVALID_INPUT,
4217
+ message: "atNamespace=srg requires projectPath and scope=loader so a Forge runtime jar can be resolved."
4218
+ });
4219
+ }
4220
+ ({ jarPath } = await this.versionService.resolveVersionJar(version));
4221
+ }
4222
+ const needsLookupMapping = atNamespace !== lookupMapping;
4223
+ const classFqns = new Set(parsed.entries.map((entry) => entry.owner));
4224
+ const membersByClass = new Map();
4225
+ for (const fqn of classFqns) {
4226
+ let lookupFqn = fqn;
4227
+ if (needsLookupMapping) {
4228
+ if (!isSourceMappingNamespace(atNamespace) || !isSourceMappingNamespace(lookupMapping)) {
4229
+ warnings.push(`Could not map class "${fqn}" from ${atNamespace} to ${lookupMapping}.`);
4230
+ }
4231
+ else {
4232
+ try {
4233
+ const mapped = await this.mappingService.findMapping({
4234
+ version: resolvedVersion,
4235
+ kind: "class",
4236
+ name: fqn,
4237
+ sourceMapping: atNamespace,
4238
+ targetMapping: lookupMapping,
4239
+ sourcePriority: input.sourcePriority,
4240
+ projectPath: input.projectPath
4241
+ });
4242
+ if (mapped.resolved && mapped.resolvedSymbol) {
4243
+ lookupFqn = mapped.resolvedSymbol.name;
4244
+ }
4245
+ else {
4246
+ warnings.push(`Could not map class "${fqn}" from ${atNamespace} to ${lookupMapping}.`);
4247
+ }
4248
+ }
4249
+ catch {
4250
+ warnings.push(`Mapping lookup failed for class "${fqn}".`);
4251
+ }
4252
+ }
4253
+ }
4254
+ try {
4255
+ const sig = await this.explorerService.getSignature({
4256
+ fqn: lookupFqn,
3594
4257
  jarPath,
3595
4258
  access: "all"
3596
4259
  });
3597
4260
  warnings.push(...sig.warnings);
4261
+ let constructors = sig.constructors;
4262
+ let methods = sig.methods;
4263
+ let fields = sig.fields;
4264
+ if (needsLookupMapping && isSourceMappingNamespace(atNamespace) && isSourceMappingNamespace(lookupMapping)) {
4265
+ const [ctorResult, methodResult, fieldResult] = await Promise.all([
4266
+ this.remapSignatureMembers(sig.constructors, "method", resolvedVersion, lookupMapping, atNamespace, input.sourcePriority, warnings, input.projectPath),
4267
+ this.remapSignatureMembers(sig.methods, "method", resolvedVersion, lookupMapping, atNamespace, input.sourcePriority, warnings, input.projectPath),
4268
+ this.remapSignatureMembers(sig.fields, "field", resolvedVersion, lookupMapping, atNamespace, input.sourcePriority, warnings, input.projectPath)
4269
+ ]);
4270
+ constructors = ctorResult.members;
4271
+ methods = methodResult.members;
4272
+ fields = fieldResult.members;
4273
+ }
3598
4274
  membersByClass.set(fqn, {
3599
4275
  className: fqn,
3600
- constructors: sig.constructors,
3601
- methods: sig.methods,
3602
- fields: sig.fields
4276
+ classAccessFlags: sig.classAccessFlags,
4277
+ constructors,
4278
+ methods,
4279
+ fields
3603
4280
  });
3604
4281
  }
3605
4282
  catch {
3606
- warnings.push(`Could not load signature for class "${obfuscatedFqn}".`);
4283
+ warnings.push(`Could not load signature for class "${lookupFqn}".`);
3607
4284
  }
3608
4285
  }
3609
- return validateParsedAccessWidener(parsed, membersByClass, warnings);
4286
+ const result = validateParsedAccessTransformer(parsed, membersByClass, warnings, {
4287
+ includeRuntimeEvidence: runtimeAware
4288
+ });
4289
+ if (provenance) {
4290
+ result.provenance = provenance;
4291
+ }
4292
+ return result;
3610
4293
  }
3611
4294
  getRuntimeMetrics() {
3612
4295
  return this.metrics.snapshot();
@@ -4210,21 +4893,22 @@ export class SourceService {
4210
4893
  descriptor: kind === "method" ? descriptor : undefined
4211
4894
  };
4212
4895
  }
4213
- async remapSignatureMembers(members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings) {
4896
+ async remapSignatureMembers(members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings, projectPath) {
4214
4897
  const failedNames = new Set();
4215
4898
  if (sourceMapping === targetMapping) {
4216
4899
  return { members, failedNames };
4217
4900
  }
4218
4901
  // Build deduplicated lookup tables for member names and owner FQNs
4219
4902
  const memberKeyToRemapped = new Map();
4903
+ const memberDescriptorRemapped = new Map();
4220
4904
  const ownerToRemapped = new Map();
4221
4905
  for (const member of members) {
4222
4906
  const memberKey = `${member.ownerFqn}\0${member.name}\0${member.jvmDescriptor}`;
4223
4907
  if (!memberKeyToRemapped.has(memberKey)) {
4224
- memberKeyToRemapped.set(memberKey, member.name); // default = obfuscated name
4908
+ memberKeyToRemapped.set(memberKey, member.name); // default = source name
4225
4909
  }
4226
4910
  if (!ownerToRemapped.has(member.ownerFqn)) {
4227
- ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = obfuscated FQN
4911
+ ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = source FQN
4228
4912
  }
4229
4913
  }
4230
4914
  // Phase 1: Remap owner FQNs first (needed for member disambiguation)
@@ -4237,22 +4921,98 @@ export class SourceService {
4237
4921
  name: obfuscatedFqn,
4238
4922
  sourceMapping,
4239
4923
  targetMapping,
4240
- sourcePriority
4924
+ sourcePriority,
4925
+ projectPath
4241
4926
  });
4242
4927
  if (mapped.resolved && mapped.resolvedSymbol) {
4243
4928
  ownerToRemapped.set(obfuscatedFqn, mapped.resolvedSymbol.name);
4244
4929
  }
4245
4930
  }
4246
4931
  catch {
4247
- // keep obfuscated FQN as fallback
4932
+ // keep source FQN as fallback
4248
4933
  }
4249
4934
  }));
4250
- // Phase 2: Remap member names using resolved owners for disambiguation
4935
+ // Phase 1.5: Collect class references from descriptors and remap them
4936
+ const descriptorClassRefs = new Set();
4937
+ for (const member of members) {
4938
+ for (const match of member.jvmDescriptor.matchAll(/L([^;]+);/g)) {
4939
+ const dotFqn = match[1].replace(/\//g, ".");
4940
+ if (!ownerToRemapped.has(dotFqn)) {
4941
+ descriptorClassRefs.add(dotFqn);
4942
+ }
4943
+ }
4944
+ }
4945
+ if (descriptorClassRefs.size > 0) {
4946
+ const refs = [...descriptorClassRefs];
4947
+ for (const ref of refs) {
4948
+ ownerToRemapped.set(ref, ref); // default = source name
4949
+ }
4950
+ await Promise.all(refs.map(async (dotFqn) => {
4951
+ try {
4952
+ const mapped = await this.mappingService.findMapping({
4953
+ version,
4954
+ kind: "class",
4955
+ name: dotFqn,
4956
+ sourceMapping,
4957
+ targetMapping,
4958
+ sourcePriority,
4959
+ projectPath
4960
+ });
4961
+ if (mapped.resolved && mapped.resolvedSymbol) {
4962
+ ownerToRemapped.set(dotFqn, mapped.resolvedSymbol.name);
4963
+ }
4964
+ }
4965
+ catch {
4966
+ // keep source name as fallback
4967
+ }
4968
+ }));
4969
+ }
4970
+ // Build a class map for descriptor remapping (dot-FQN → dot-FQN)
4971
+ const classMap = new Map();
4972
+ for (const [src, tgt] of ownerToRemapped) {
4973
+ if (src !== tgt) {
4974
+ classMap.set(src, tgt);
4975
+ }
4976
+ }
4977
+ // Phase 2: Remap member names (and descriptors for methods) using resolved owners
4978
+ const canResolveMethodExactly = kind === "method" &&
4979
+ "resolveMethodMappingExact" in this.mappingService &&
4980
+ typeof this.mappingService.resolveMethodMappingExact === "function";
4251
4981
  const memberEntries = [...memberKeyToRemapped.entries()];
4252
- await Promise.all(memberEntries.map(async ([key, _obfuscatedName]) => {
4982
+ await Promise.all(memberEntries.map(async ([key, _sourceName]) => {
4253
4983
  const [ownerFqn, name, descriptor] = key.split("\0");
4254
4984
  try {
4255
4985
  const targetOwner = ownerToRemapped.get(ownerFqn) ?? ownerFqn;
4986
+ // For methods with descriptors, try exact resolution first
4987
+ if (canResolveMethodExactly && descriptor) {
4988
+ try {
4989
+ const exactResult = await this.mappingService.resolveMethodMappingExact({
4990
+ version,
4991
+ owner: ownerFqn,
4992
+ name: name,
4993
+ descriptor,
4994
+ sourceMapping,
4995
+ targetMapping,
4996
+ sourcePriority,
4997
+ projectPath
4998
+ });
4999
+ if (exactResult.resolved && exactResult.resolvedSymbol) {
5000
+ memberKeyToRemapped.set(key, exactResult.resolvedSymbol.name);
5001
+ if (exactResult.resolvedSymbol.descriptor) {
5002
+ memberDescriptorRemapped.set(key, exactResult.resolvedSymbol.descriptor);
5003
+ }
5004
+ return; // exact resolution succeeded
5005
+ }
5006
+ // Fall through to findMapping with descriptorHint
5007
+ }
5008
+ catch (exactError) {
5009
+ warnings.push(`Exact method resolution failed for "${name}" (falling back to name-based lookup): ${exactError instanceof Error ? exactError.message : String(exactError)}`);
5010
+ }
5011
+ }
5012
+ // Fallback: findMapping with descriptorHint for overload disambiguation
5013
+ const remappedDescriptorHint = kind === "method" && descriptor
5014
+ ? remapJvmDescriptor(descriptor, classMap)
5015
+ : undefined;
4256
5016
  const mapped = await this.mappingService.findMapping({
4257
5017
  version,
4258
5018
  kind,
@@ -4262,10 +5022,17 @@ export class SourceService {
4262
5022
  sourceMapping,
4263
5023
  targetMapping,
4264
5024
  sourcePriority,
4265
- disambiguation: { ownerHint: targetOwner }
5025
+ projectPath,
5026
+ disambiguation: {
5027
+ ownerHint: targetOwner,
5028
+ descriptorHint: remappedDescriptorHint
5029
+ }
4266
5030
  });
4267
5031
  if (mapped.resolved && mapped.resolvedSymbol) {
4268
5032
  memberKeyToRemapped.set(key, mapped.resolvedSymbol.name);
5033
+ if (kind === "method" && mapped.resolvedSymbol.descriptor) {
5034
+ memberDescriptorRemapped.set(key, mapped.resolvedSymbol.descriptor);
5035
+ }
4269
5036
  }
4270
5037
  else if (mapped.status === "ambiguous" && mapped.candidates && mapped.candidates.length > 0) {
4271
5038
  // Disambiguate: filter by target owner and pick the best candidate
@@ -4293,13 +5060,20 @@ export class SourceService {
4293
5060
  failedNames.add(name);
4294
5061
  }
4295
5062
  }));
5063
+ const isField = kind === "field";
4296
5064
  return {
4297
5065
  members: members.map((member) => {
4298
5066
  const memberKey = `${member.ownerFqn}\0${member.name}\0${member.jvmDescriptor}`;
5067
+ const remappedName = memberKeyToRemapped.get(memberKey) ?? member.name;
5068
+ const remappedOwner = ownerToRemapped.get(member.ownerFqn) ?? member.ownerFqn;
5069
+ const remappedDescriptor = memberDescriptorRemapped.get(memberKey)
5070
+ ?? remapJvmDescriptor(member.jvmDescriptor, classMap);
4299
5071
  return {
4300
5072
  ...member,
4301
- name: memberKeyToRemapped.get(memberKey) ?? member.name,
4302
- ownerFqn: ownerToRemapped.get(member.ownerFqn) ?? member.ownerFqn
5073
+ name: remappedName,
5074
+ ownerFqn: remappedOwner,
5075
+ jvmDescriptor: remappedDescriptor,
5076
+ javaSignature: rebuildJavaSignature({ name: remappedName, ownerFqn: remappedOwner, accessFlags: member.accessFlags }, remappedDescriptor, isField)
4303
5077
  };
4304
5078
  }),
4305
5079
  failedNames
@@ -4644,4 +5418,39 @@ export class SourceService {
4644
5418
  this.metrics.setCacheArtifactByteAccountingRef(this.cacheMetricsState.lru);
4645
5419
  }
4646
5420
  }
5421
+ function remapJvmDescriptor(descriptor, classMap) {
5422
+ if (classMap.size === 0) {
5423
+ return descriptor;
5424
+ }
5425
+ return descriptor.replace(/L([^;]+);/g, (match, ref) => {
5426
+ const dotFqn = ref.replace(/\//g, ".");
5427
+ const remapped = classMap.get(dotFqn);
5428
+ return remapped ? `L${remapped.replace(/\./g, "/")};` : match;
5429
+ });
5430
+ }
5431
+ function rebuildJavaSignature(member, remappedDescriptor, isField) {
5432
+ const modifiers = modifierPrefix(member.accessFlags, isField ? "field" : "method");
5433
+ const prefix = modifiers ? `${modifiers} ` : "";
5434
+ if (isField) {
5435
+ try {
5436
+ const { type } = parseFieldType(remappedDescriptor, 0, { allowVoid: false });
5437
+ return `${prefix}${type} ${member.name}`.trim();
5438
+ }
5439
+ catch {
5440
+ return `${prefix}${member.name}`.trim();
5441
+ }
5442
+ }
5443
+ try {
5444
+ const { args, returnType } = parseMethodDescriptor(remappedDescriptor);
5445
+ const argStr = args.join(", ");
5446
+ if (member.name === "<init>") {
5447
+ const ownerSimple = member.ownerFqn.split(".").pop();
5448
+ return `${prefix}${ownerSimple}(${argStr})`.trim();
5449
+ }
5450
+ return `${prefix}${returnType} ${member.name}(${argStr})`.trim();
5451
+ }
5452
+ catch {
5453
+ return `${prefix}${member.name}`.trim();
5454
+ }
5455
+ }
4647
5456
  //# sourceMappingURL=source-service.js.map