@adhisang/minecraft-modding-mcp 3.1.0 → 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 (44) hide show
  1. package/CHANGELOG.md +62 -34
  2. package/README.md +79 -100
  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/config.js +19 -11
  8. package/dist/decompiler/vineflower.js +22 -21
  9. package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
  10. package/dist/entry-tools/analyze-symbol-service.d.ts +22 -20
  11. package/dist/entry-tools/analyze-symbol-service.js +6 -3
  12. package/dist/entry-tools/inspect-minecraft-service.d.ts +166 -149
  13. package/dist/entry-tools/inspect-minecraft-service.js +318 -55
  14. package/dist/entry-tools/validate-project-service.d.ts +153 -16
  15. package/dist/entry-tools/validate-project-service.js +360 -23
  16. package/dist/gradle-paths.d.ts +4 -0
  17. package/dist/gradle-paths.js +57 -0
  18. package/dist/index.js +274 -13
  19. package/dist/mapping-pipeline-service.d.ts +3 -1
  20. package/dist/mapping-pipeline-service.js +16 -1
  21. package/dist/mapping-service.d.ts +5 -0
  22. package/dist/mapping-service.js +200 -84
  23. package/dist/minecraft-explorer-service.d.ts +13 -0
  24. package/dist/minecraft-explorer-service.js +8 -4
  25. package/dist/mixin-validator.d.ts +33 -2
  26. package/dist/mixin-validator.js +197 -11
  27. package/dist/mod-analyzer.d.ts +1 -0
  28. package/dist/mod-analyzer.js +17 -1
  29. package/dist/mod-decompile-service.js +4 -4
  30. package/dist/mod-remap-service.js +1 -54
  31. package/dist/mod-search-service.d.ts +1 -0
  32. package/dist/mod-search-service.js +84 -51
  33. package/dist/response-utils.d.ts +35 -0
  34. package/dist/response-utils.js +113 -0
  35. package/dist/source-jar-reader.d.ts +16 -0
  36. package/dist/source-jar-reader.js +103 -1
  37. package/dist/source-resolver.js +9 -10
  38. package/dist/source-service.d.ts +24 -2
  39. package/dist/source-service.js +1052 -139
  40. package/dist/tool-contract-manifest.js +74 -74
  41. package/dist/types.d.ts +17 -0
  42. package/dist/workspace-mapping-service.d.ts +13 -0
  43. package/dist/workspace-mapping-service.js +146 -14
  44. 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";
@@ -159,6 +161,7 @@ function clampLimit(limit, fallback, max) {
159
161
  }
160
162
  const MAX_REGEX_QUERY_LENGTH = 200;
161
163
  const MAX_REGEX_RESULT_LIMIT = 100;
164
+ const TRACE_LIFECYCLE_MAX_CONCURRENCY = 3;
162
165
  function normalizePathStyle(path) {
163
166
  return path.replaceAll("\\", "/");
164
167
  }
@@ -177,6 +180,30 @@ function hasExactVersionToken(path, version) {
177
180
  ?? rememberCachedRegex(VERSION_TOKEN_REGEX_CACHE, normalizedVersion, new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}([^0-9a-z]|$)`, "i"));
178
181
  return pattern.test(normalizedPath);
179
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
+ }
180
207
  function looksLikeDeobfuscatedClassName(value) {
181
208
  const trimmed = value.trim();
182
209
  if (!trimmed) {
@@ -203,39 +230,17 @@ function buildResolveArtifactParams(target, extra = {}) {
203
230
  ...extra
204
231
  };
205
232
  }
206
- function normalizeOptionalProjectPath(projectPath) {
207
- if (!projectPath) {
208
- return undefined;
209
- }
210
- const trimmed = projectPath.trim();
211
- if (!trimmed) {
212
- return undefined;
213
- }
214
- const normalized = normalizePathForHost(trimmed, undefined, "projectPath");
215
- return isAbsolute(normalized) ? normalized : resolvePath(process.cwd(), normalized);
233
+ function looksLikeClassSegment(name) {
234
+ const trimmed = name.trim();
235
+ return /^[A-Z_$]/.test(trimmed);
216
236
  }
217
- function resolveGradleUserHomePath() {
218
- const configured = process.env.GRADLE_USER_HOME?.trim();
219
- if (!configured) {
220
- return resolvePath(homedir(), ".gradle");
237
+ function looksLikeJvmMethodDescriptor(descriptor) {
238
+ const trimmed = descriptor?.trim();
239
+ if (!trimmed || !trimmed.startsWith("(")) {
240
+ return false;
221
241
  }
222
- const normalized = normalizePathForHost(configured, undefined, "GRADLE_USER_HOME");
223
- return isAbsolute(normalized) ? normalized : resolvePath(process.cwd(), normalized);
224
- }
225
- function buildVersionSourceSearchRoots(projectPath) {
226
- const roots = new Set();
227
- if (projectPath) {
228
- roots.add(resolvePath(projectPath, ".gradle", "loom-cache"));
229
- roots.add(resolvePath(projectPath, ".gradle-user", "caches", "fabric-loom"));
230
- roots.add(resolvePath(projectPath, ".gradle", "caches", "fabric-loom"));
231
- const projectParent = dirname(projectPath);
232
- roots.add(resolvePath(projectParent, ".gradle-user-home", "loom-cache"));
233
- roots.add(resolvePath(projectParent, ".gradle-user-home", "caches", "fabric-loom"));
234
- }
235
- const homeGradle = resolveGradleUserHomePath();
236
- roots.add(resolvePath(homeGradle, "loom-cache"));
237
- roots.add(resolvePath(homeGradle, "caches", "fabric-loom"));
238
- return [...roots];
242
+ const closing = trimmed.indexOf(")");
243
+ return closing > 0 && closing < trimmed.length - 1;
239
244
  }
240
245
  function looksLikeMinecraftSourceArtifact(path, hasMinecraftNamespace) {
241
246
  if (hasMinecraftNamespace) {
@@ -278,16 +283,19 @@ function scopeToJarType(scope) {
278
283
  }
279
284
  function parseQualifiedMethodSymbol(symbol) {
280
285
  const trimmed = symbol.trim();
281
- const separator = trimmed.lastIndexOf(".");
282
- if (separator <= 0 || separator >= trimmed.length - 1) {
286
+ const descriptorStart = trimmed.indexOf("(");
287
+ const qualifiedSymbol = descriptorStart >= 0 ? trimmed.slice(0, descriptorStart) : trimmed;
288
+ const inlineDescriptor = descriptorStart >= 0 ? trimmed.slice(descriptorStart).trim() : undefined;
289
+ const separator = qualifiedSymbol.lastIndexOf(".");
290
+ if (separator <= 0 || separator >= qualifiedSymbol.length - 1) {
283
291
  throw createError({
284
292
  code: ERROR_CODES.INVALID_INPUT,
285
293
  message: `symbol must be in the form "fully.qualified.Class.method".`,
286
294
  details: { symbol }
287
295
  });
288
296
  }
289
- const className = trimmed.slice(0, separator);
290
- const methodName = trimmed.slice(separator + 1);
297
+ const className = qualifiedSymbol.slice(0, separator);
298
+ const methodName = qualifiedSymbol.slice(separator + 1);
291
299
  if (!className ||
292
300
  !methodName ||
293
301
  className.includes("/") ||
@@ -299,7 +307,11 @@ function parseQualifiedMethodSymbol(symbol) {
299
307
  details: { symbol }
300
308
  });
301
309
  }
302
- return { className, methodName };
310
+ return {
311
+ className,
312
+ methodName,
313
+ ...(inlineDescriptor ? { inlineDescriptor } : {})
314
+ };
303
315
  }
304
316
  function normalizeOptionalString(value) {
305
317
  if (value == null) {
@@ -345,6 +357,15 @@ const MIXIN_PROJECT_DISCOVERY_IGNORES = [
345
357
  "**/out/**",
346
358
  "**/node_modules/**"
347
359
  ];
360
+ async function pathExists(filePath) {
361
+ try {
362
+ await access(filePath);
363
+ return true;
364
+ }
365
+ catch {
366
+ return false;
367
+ }
368
+ }
348
369
  function normalizeMapping(mapping) {
349
370
  if (mapping == null) {
350
371
  return "obfuscated";
@@ -381,6 +402,19 @@ function normalizeAccessWidenerNamespace(namespace) {
381
402
  }
382
403
  return undefined;
383
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
+ }
384
418
  function normalizeMemberAccess(access) {
385
419
  if (access == null) {
386
420
  return "public";
@@ -768,7 +802,7 @@ export class SourceService {
768
802
  searchedPaths.push(root);
769
803
  let discovered = [];
770
804
  try {
771
- discovered = fastGlob.sync("**/*sources.jar", {
805
+ discovered = await fastGlob.glob("**/*sources.jar", {
772
806
  cwd: root,
773
807
  absolute: true,
774
808
  onlyFiles: true
@@ -832,6 +866,371 @@ export class SourceService {
832
866
  selectedHasMinecraftNamespace: selected?.hasMinecraftNamespace
833
867
  };
834
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
+ }
835
1234
  buildVersionSourceRecoveryCommand(projectPath) {
836
1235
  const normalizedProjectPath = normalizeOptionalProjectPath(projectPath);
837
1236
  const prefix = normalizedProjectPath
@@ -931,9 +1330,11 @@ export class SourceService {
931
1330
  let resolvedTarget = { kind, value };
932
1331
  let resolvedVersion;
933
1332
  let versionSourceDiscovery;
1333
+ let runtimeNamesUnobfuscated = false;
934
1334
  if (kind === "version") {
935
1335
  const versionJar = await this.versionService.resolveVersionJar(value);
936
1336
  resolvedVersion = versionJar.version;
1337
+ runtimeNamesUnobfuscated = isUnobfuscatedVersion(resolvedVersion);
937
1338
  resolvedTarget = {
938
1339
  kind: "jar",
939
1340
  value: versionJar.jarPath
@@ -948,6 +1349,9 @@ export class SourceService {
948
1349
  // coordinate validity is validated by resolver, keep version undefined on parse failure.
949
1350
  }
950
1351
  }
1352
+ if (!runtimeNamesUnobfuscated && resolvedVersion && isUnobfuscatedVersion(resolvedVersion)) {
1353
+ runtimeNamesUnobfuscated = true;
1354
+ }
951
1355
  // Unobfuscated versions (MC 26.1+) ship with deobfuscated runtime names; intermediary/yarn are not applicable.
952
1356
  let effectiveMapping = mapping;
953
1357
  if ((mapping === "intermediary" || mapping === "yarn") &&
@@ -956,7 +1360,11 @@ export class SourceService {
956
1360
  warnings.push(`Version ${resolvedVersion} is unobfuscated; ${mapping} mappings are not applicable. Using the obfuscated namespace label for the deobfuscated runtime names.`);
957
1361
  effectiveMapping = "obfuscated";
958
1362
  }
959
- if (kind === "version" && resolvedVersion && effectiveMapping === "mojang" && scope !== "vanilla") {
1363
+ if (kind === "version" &&
1364
+ resolvedVersion &&
1365
+ effectiveMapping === "mojang" &&
1366
+ !runtimeNamesUnobfuscated &&
1367
+ scope !== "vanilla") {
960
1368
  versionSourceDiscovery = await this.discoverVersionSourceJar({
961
1369
  version: resolvedVersion,
962
1370
  projectPath: input.projectPath
@@ -991,7 +1399,8 @@ export class SourceService {
991
1399
  mappingDecision = applyMappingPipeline({
992
1400
  requestedMapping: effectiveMapping,
993
1401
  target: { kind, value },
994
- resolved
1402
+ resolved,
1403
+ runtimeNamesUnobfuscated
995
1404
  });
996
1405
  }
997
1406
  catch (caughtError) {
@@ -1420,7 +1829,14 @@ export class SourceService {
1420
1829
  return this.mappingService.getClassApiMatrix(input);
1421
1830
  }
1422
1831
  async checkSymbolExists(input) {
1423
- 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;
1424
1840
  }
1425
1841
  async resolveWorkspaceSymbol(input) {
1426
1842
  const projectPath = input.projectPath?.trim();
@@ -1642,10 +2058,152 @@ export class SourceService {
1642
2058
  warnings: [...warnings, ...mapped.warnings]
1643
2059
  };
1644
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
+ }
1645
2202
  async traceSymbolLifecycle(input) {
1646
2203
  const mapping = normalizeMapping(input.mapping);
1647
- const { className: userClassName, methodName: userMethodName } = parseQualifiedMethodSymbol(input.symbol);
1648
- const descriptor = normalizeOptionalString(input.descriptor);
2204
+ const { className: userClassName, methodName: userMethodName, inlineDescriptor } = parseQualifiedMethodSymbol(input.symbol);
2205
+ const descriptor = normalizeOptionalString(input.descriptor)
2206
+ ?? (looksLikeJvmMethodDescriptor(inlineDescriptor) ? normalizeOptionalString(inlineDescriptor) : undefined);
1649
2207
  const includeTimeline = input.includeTimeline ?? false;
1650
2208
  const includeSnapshots = input.includeSnapshots ?? false;
1651
2209
  const maxVersions = clampLimit(input.maxVersions, 120, 400);
@@ -1701,60 +2259,75 @@ export class SourceService {
1701
2259
  selectedVersions = selectedVersions.slice(selectedVersions.length - maxVersions);
1702
2260
  warnings.push(`Version scan truncated to ${maxVersions} entries. Effective fromVersion is now "${selectedVersions[0]}".`);
1703
2261
  }
1704
- const resolvedSymbolsByVersion = new Map();
1705
- const scanned = [];
1706
- for (const version of selectedVersions) {
2262
+ const referenceVersion = selectedVersions[selectedVersions.length - 1];
2263
+ await this.rejectLifecycleClassLikeInput({
2264
+ symbol: input.symbol,
2265
+ className: userClassName,
2266
+ methodName: userMethodName,
2267
+ mapping,
2268
+ version: referenceVersion,
2269
+ sourcePriority: input.sourcePriority
2270
+ });
2271
+ const scannedResults = await mapWithConcurrencyLimit(selectedVersions, TRACE_LIFECYCLE_MAX_CONCURRENCY, async (version) => {
2272
+ const versionWarnings = [];
1707
2273
  try {
1708
- let resolvedSymbols = resolvedSymbolsByVersion.get(version);
1709
- if (!resolvedSymbols) {
1710
- const [obfuscatedClassName, obfuscatedMethod] = await Promise.all([
1711
- this.resolveToObfuscatedClassName(userClassName, version, mapping, input.sourcePriority, warnings),
1712
- this.resolveToObfuscatedMemberName(userMethodName, userClassName, descriptor, "method", version, mapping, input.sourcePriority, warnings)
1713
- ]);
1714
- resolvedSymbols = {
1715
- className: obfuscatedClassName,
1716
- methodName: obfuscatedMethod.name,
1717
- methodDescriptor: obfuscatedMethod.descriptor
1718
- };
1719
- resolvedSymbolsByVersion.set(version, resolvedSymbols);
1720
- }
1721
- const resolvedJar = await this.versionService.resolveVersionJar(version);
2274
+ const [obfuscatedClassName, obfuscatedMethod, resolvedJar] = await Promise.all([
2275
+ this.resolveToObfuscatedClassName(userClassName, version, mapping, input.sourcePriority, versionWarnings),
2276
+ this.resolveToObfuscatedMemberName(userMethodName, userClassName, descriptor, "method", version, mapping, input.sourcePriority, versionWarnings),
2277
+ this.versionService.resolveVersionJar(version)
2278
+ ]);
1722
2279
  const signature = await this.explorerService.getSignature({
1723
- fqn: resolvedSymbols.className,
2280
+ fqn: obfuscatedClassName,
1724
2281
  jarPath: resolvedJar.jarPath,
1725
2282
  access: "all",
1726
2283
  includeSynthetic: true
1727
2284
  });
1728
- const sameNameMethods = signature.methods.filter((method) => method.name === resolvedSymbols.methodName);
1729
- const matchesDescriptor = (resolvedSymbols.methodDescriptor ?? descriptor)
1730
- ? sameNameMethods.some((method) => method.jvmDescriptor === (resolvedSymbols.methodDescriptor ?? descriptor))
2285
+ const sameNameMethods = signature.methods.filter((method) => method.name === obfuscatedMethod.name);
2286
+ const effectiveDescriptor = obfuscatedMethod.descriptor ?? descriptor;
2287
+ const matchesDescriptor = effectiveDescriptor
2288
+ ? sameNameMethods.some((method) => method.jvmDescriptor === effectiveDescriptor)
1731
2289
  : sameNameMethods.length > 0;
1732
2290
  const reason = !matchesDescriptor && descriptor && sameNameMethods.length > 0 ? "descriptor-mismatch" : undefined;
1733
- scanned.push({
1734
- version,
1735
- exists: matchesDescriptor,
1736
- reason,
1737
- determinate: true
1738
- });
2291
+ return {
2292
+ entry: {
2293
+ version,
2294
+ exists: matchesDescriptor,
2295
+ reason,
2296
+ determinate: true
2297
+ },
2298
+ warnings: versionWarnings
2299
+ };
1739
2300
  }
1740
2301
  catch (caughtError) {
1741
2302
  if (isAppError(caughtError) && caughtError.code === ERROR_CODES.CLASS_NOT_FOUND) {
1742
- scanned.push({
2303
+ return {
2304
+ entry: {
2305
+ version,
2306
+ exists: false,
2307
+ reason: "class-not-found",
2308
+ determinate: true
2309
+ },
2310
+ warnings: versionWarnings
2311
+ };
2312
+ }
2313
+ versionWarnings.push(`Failed to evaluate ${version}: ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`);
2314
+ return {
2315
+ entry: {
1743
2316
  version,
1744
2317
  exists: false,
1745
- reason: "class-not-found",
1746
- determinate: true
1747
- });
1748
- continue;
1749
- }
1750
- scanned.push({
1751
- version,
1752
- exists: false,
1753
- reason: "unresolved",
1754
- determinate: false
1755
- });
1756
- warnings.push(`Failed to evaluate ${version}: ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`);
2318
+ reason: "unresolved",
2319
+ determinate: false
2320
+ },
2321
+ warnings: versionWarnings
2322
+ };
1757
2323
  }
2324
+ finally {
2325
+ this.releaseLifecycleMappingGraph(version, input.sourcePriority);
2326
+ }
2327
+ });
2328
+ const scanned = scannedResults.map((result) => result.entry);
2329
+ for (const result of scannedResults) {
2330
+ warnings.push(...result.warnings);
1758
2331
  }
1759
2332
  const determinate = scanned.filter((entry) => entry.determinate);
1760
2333
  const present = determinate.filter((entry) => entry.exists);
@@ -2695,17 +3268,18 @@ export class SourceService {
2695
3268
  path: this.resolveMixinInputPath(path, "path")
2696
3269
  },
2697
3270
  sourcePath: path
2698
- })), input);
3271
+ })), input, []);
2699
3272
  }
2700
3273
  const resolvedInput = mode === "project"
2701
- ? this.createProjectValidateMixinConfigInput(input)
3274
+ ? await this.createProjectValidateMixinConfigInput(input)
2702
3275
  : input;
2703
- const configSources = await this.resolveMixinConfigSources(resolvedInput);
3276
+ const { sources: configSources, warnings: configWarnings } = await this.resolveMixinConfigSources(resolvedInput);
2704
3277
  if (configSources.length === 0) {
2705
- throw createError({
2706
- code: ERROR_CODES.INVALID_INPUT,
2707
- message: "Mixin config(s) contain no mixin class entries."
2708
- });
3278
+ const emptyOutput = this.buildValidateMixinOutput(mode, []);
3279
+ return this.applyValidateMixinOutputCompaction({
3280
+ ...emptyOutput,
3281
+ warnings: [...new Set([...emptyOutput.warnings, ...configWarnings])]
3282
+ }, input);
2709
3283
  }
2710
3284
  return this.validateMixinMany(mode, configSources.map((entry) => ({
2711
3285
  source: {
@@ -2715,19 +3289,19 @@ export class SourceService {
2715
3289
  configPath: entry.configPath
2716
3290
  },
2717
3291
  sourcePath: entry.sourcePath
2718
- })), resolvedInput);
3292
+ })), resolvedInput, configWarnings);
2719
3293
  }
2720
- createProjectValidateMixinConfigInput(input) {
3294
+ async createProjectValidateMixinConfigInput(input) {
2721
3295
  if (input.input.mode !== "project") {
2722
3296
  return input;
2723
3297
  }
2724
3298
  const resolvedProjectPath = this.resolveMixinInputPath(input.input.path, "path");
2725
- const configPaths = fastGlob.sync(["**/*.mixins.json"], {
3299
+ const configPaths = (await fastGlob.glob(["**/*.mixins.json"], {
2726
3300
  cwd: resolvedProjectPath,
2727
3301
  absolute: true,
2728
3302
  onlyFiles: true,
2729
3303
  ignore: [...MIXIN_PROJECT_DISCOVERY_IGNORES]
2730
- }).sort((left, right) => left.localeCompare(right));
3304
+ })).sort((left, right) => left.localeCompare(right));
2731
3305
  if (configPaths.length === 0) {
2732
3306
  throw createError({
2733
3307
  code: ERROR_CODES.INVALID_INPUT,
@@ -2770,7 +3344,8 @@ export class SourceService {
2770
3344
  name: input.className,
2771
3345
  sourceMapping: input.sourceMapping,
2772
3346
  targetMapping: input.targetMapping,
2773
- sourcePriority: input.sourcePriority
3347
+ sourcePriority: input.sourcePriority,
3348
+ projectPath: input.projectPath
2774
3349
  });
2775
3350
  }
2776
3351
  const cacheKey = [
@@ -2778,7 +3353,8 @@ export class SourceService {
2778
3353
  input.className,
2779
3354
  input.sourceMapping,
2780
3355
  input.targetMapping,
2781
- input.sourcePriority
3356
+ input.sourcePriority,
3357
+ input.projectPath ?? ""
2782
3358
  ].join("\0");
2783
3359
  const cached = cache.get(cacheKey);
2784
3360
  if (cached) {
@@ -2790,7 +3366,8 @@ export class SourceService {
2790
3366
  name: input.className,
2791
3367
  sourceMapping: input.sourceMapping,
2792
3368
  targetMapping: input.targetMapping,
2793
- sourcePriority: input.sourcePriority
3369
+ sourcePriority: input.sourcePriority,
3370
+ projectPath: input.projectPath
2794
3371
  }).catch((error) => {
2795
3372
  cache.delete(cacheKey);
2796
3373
  throw error;
@@ -2971,6 +3548,7 @@ export class SourceService {
2971
3548
  sourceMapping: requestedMapping,
2972
3549
  targetMapping: signatureLookupMapping,
2973
3550
  sourcePriority: currentSourcePriority,
3551
+ projectPath: input.projectPath,
2974
3552
  batchCaches: input.batchCaches
2975
3553
  });
2976
3554
  if (mapped.resolved && mapped.resolvedSymbol) {
@@ -3004,9 +3582,9 @@ export class SourceService {
3004
3582
  if (requestedMapping !== signatureLookupMapping) {
3005
3583
  try {
3006
3584
  const [ctorResult, methodResult, fieldResult] = await Promise.all([
3007
- this.remapSignatureMembers(sig.constructors, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings),
3008
- this.remapSignatureMembers(sig.methods, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings),
3009
- 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)
3010
3588
  ]);
3011
3589
  constructors = ctorResult.members;
3012
3590
  methods = methodResult.members;
@@ -3245,9 +3823,13 @@ export class SourceService {
3245
3823
  }
3246
3824
  async resolveMixinConfigSources(input) {
3247
3825
  if (input.input.mode !== "config") {
3248
- return [];
3826
+ return {
3827
+ sources: [],
3828
+ warnings: []
3829
+ };
3249
3830
  }
3250
3831
  const results = [];
3832
+ const warnings = [];
3251
3833
  for (const rawConfigPath of input.input.configPaths) {
3252
3834
  const resolvedConfigPath = this.resolveMixinInputPath(rawConfigPath, "configPath");
3253
3835
  let configJson;
@@ -3268,6 +3850,7 @@ export class SourceService {
3268
3850
  ...(configJson.server ?? [])
3269
3851
  ];
3270
3852
  if (classNames.length === 0) {
3853
+ warnings.push(`Mixin config "${resolvedConfigPath}" contains no mixin class entries.`);
3271
3854
  continue;
3272
3855
  }
3273
3856
  const projectBase = input.projectPath
@@ -3278,11 +3861,21 @@ export class SourceService {
3278
3861
  sourceRootCandidates = input.sourceRoots;
3279
3862
  }
3280
3863
  else {
3281
- const detected = COMMON_SOURCE_ROOTS.filter((candidateRoot) => classNames.some((className) => {
3282
- const fqcn = pkg ? `${pkg}.${className}` : className;
3283
- const relative = fqcn.replace(/\./g, "/") + ".java";
3284
- return existsSync(resolvePath(projectBase, candidateRoot, relative));
3285
- }));
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
+ }
3286
3879
  sourceRootCandidates = detected.length > 0 ? detected : ["src/main/java"];
3287
3880
  }
3288
3881
  for (const cls of classNames) {
@@ -3291,7 +3884,7 @@ export class SourceService {
3291
3884
  let sourcePath = resolvePath(projectBase, sourceRootCandidates[0], relativePath);
3292
3885
  for (const root of sourceRootCandidates) {
3293
3886
  const candidate = resolvePath(projectBase, root, relativePath);
3294
- if (existsSync(candidate)) {
3887
+ if (await pathExists(candidate)) {
3295
3888
  sourcePath = candidate;
3296
3889
  break;
3297
3890
  }
@@ -3302,9 +3895,12 @@ export class SourceService {
3302
3895
  });
3303
3896
  }
3304
3897
  }
3305
- return results;
3898
+ return {
3899
+ sources: results,
3900
+ warnings
3901
+ };
3306
3902
  }
3307
- async validateMixinMany(mode, entries, input) {
3903
+ async validateMixinMany(mode, entries, input, additionalWarnings) {
3308
3904
  const results = [];
3309
3905
  const batchWarningMode = input.warningMode ?? "aggregated";
3310
3906
  const { input: _discardedInput, ...sharedInput } = input;
@@ -3331,7 +3927,13 @@ export class SourceService {
3331
3927
  });
3332
3928
  }
3333
3929
  }
3334
- return this.applyValidateMixinOutputCompaction(this.buildValidateMixinOutput(mode, results), input);
3930
+ const output = this.buildValidateMixinOutput(mode, results);
3931
+ return this.applyValidateMixinOutputCompaction({
3932
+ ...output,
3933
+ warnings: additionalWarnings.length === 0
3934
+ ? output.warnings
3935
+ : [...new Set([...output.warnings, ...additionalWarnings])]
3936
+ }, input);
3335
3937
  }
3336
3938
  applyValidateMixinOutputCompaction(output, input) {
3337
3939
  let nextOutput = output;
@@ -3473,7 +4075,6 @@ export class SourceService {
3473
4075
  throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "content must be non-empty." });
3474
4076
  }
3475
4077
  const warnings = [];
3476
- const { jarPath } = await this.versionService.resolveVersionJar(version);
3477
4078
  const parsed = parseAccessWidener(content);
3478
4079
  const headerNamespaceRaw = normalizeOptionalString(parsed.namespace);
3479
4080
  const overrideMapping = input.mapping ? normalizeMapping(input.mapping) : undefined;
@@ -3485,7 +4086,27 @@ export class SourceService {
3485
4086
  if (overrideMapping && headerNamespace && overrideMapping !== headerNamespace) {
3486
4087
  warnings.push(`Using mapping override "${overrideMapping}" instead of header namespace "${headerNamespaceRaw}".`);
3487
4088
  }
3488
- 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;
3489
4110
  // Collect unique class FQNs from entries
3490
4111
  const classFqns = new Set();
3491
4112
  for (const entry of parsed.entries) {
@@ -3494,22 +4115,23 @@ export class SourceService {
3494
4115
  }
3495
4116
  const membersByClass = new Map();
3496
4117
  for (const fqn of classFqns) {
3497
- let obfuscatedFqn = fqn;
3498
- if (needsMapping) {
4118
+ let lookupFqn = fqn;
4119
+ if (needsLookupMapping) {
3499
4120
  try {
3500
4121
  const mapped = await this.mappingService.findMapping({
3501
- version,
4122
+ version: resolvedVersion,
3502
4123
  kind: "class",
3503
4124
  name: fqn,
3504
4125
  sourceMapping: awNamespace,
3505
- targetMapping: "obfuscated",
3506
- sourcePriority: input.sourcePriority
4126
+ targetMapping: lookupMapping,
4127
+ sourcePriority: input.sourcePriority,
4128
+ projectPath: input.projectPath
3507
4129
  });
3508
4130
  if (mapped.resolved && mapped.resolvedSymbol) {
3509
- obfuscatedFqn = mapped.resolvedSymbol.name;
4131
+ lookupFqn = mapped.resolvedSymbol.name;
3510
4132
  }
3511
4133
  else {
3512
- warnings.push(`Could not map class "${fqn}" from ${awNamespace} to obfuscated.`);
4134
+ warnings.push(`Could not map class "${fqn}" from ${awNamespace} to ${lookupMapping}.`);
3513
4135
  }
3514
4136
  }
3515
4137
  catch {
@@ -3518,23 +4140,156 @@ export class SourceService {
3518
4140
  }
3519
4141
  try {
3520
4142
  const sig = await this.explorerService.getSignature({
3521
- fqn: obfuscatedFqn,
4143
+ fqn: lookupFqn,
3522
4144
  jarPath,
3523
4145
  access: "all"
3524
4146
  });
3525
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
+ }
3526
4161
  membersByClass.set(fqn, {
3527
4162
  className: fqn,
3528
- constructors: sig.constructors,
3529
- methods: sig.methods,
3530
- fields: sig.fields
4163
+ classAccessFlags: sig.classAccessFlags,
4164
+ constructors,
4165
+ methods,
4166
+ fields
3531
4167
  });
3532
4168
  }
3533
4169
  catch {
3534
- warnings.push(`Could not load signature for class "${obfuscatedFqn}".`);
4170
+ warnings.push(`Could not load signature for class "${lookupFqn}".`);
3535
4171
  }
3536
4172
  }
3537
- return validateParsedAccessWidener(parsed, membersByClass, warnings);
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,
4257
+ jarPath,
4258
+ access: "all"
4259
+ });
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
+ }
4274
+ membersByClass.set(fqn, {
4275
+ className: fqn,
4276
+ classAccessFlags: sig.classAccessFlags,
4277
+ constructors,
4278
+ methods,
4279
+ fields
4280
+ });
4281
+ }
4282
+ catch {
4283
+ warnings.push(`Could not load signature for class "${lookupFqn}".`);
4284
+ }
4285
+ }
4286
+ const result = validateParsedAccessTransformer(parsed, membersByClass, warnings, {
4287
+ includeRuntimeEvidence: runtimeAware
4288
+ });
4289
+ if (provenance) {
4290
+ result.provenance = provenance;
4291
+ }
4292
+ return result;
3538
4293
  }
3539
4294
  getRuntimeMetrics() {
3540
4295
  return this.metrics.snapshot();
@@ -4045,6 +4800,38 @@ export class SourceService {
4045
4800
  details
4046
4801
  });
4047
4802
  }
4803
+ rejectLifecycleClassLikeInput(input) {
4804
+ if (!looksLikeClassSegment(input.methodName)) {
4805
+ return;
4806
+ }
4807
+ const classLikeSymbol = `${input.className}.${input.methodName}`;
4808
+ throw createError({
4809
+ code: ERROR_CODES.INVALID_INPUT,
4810
+ message: `symbol must be in the form "fully.qualified.Class.method".`,
4811
+ details: {
4812
+ symbol: input.symbol,
4813
+ classLikeSymbol,
4814
+ nextAction: "Pass lifecycle input as Class.method and use the separate descriptor field for exact overload matching.",
4815
+ suggestedCall: input.version
4816
+ ? {
4817
+ tool: "check-symbol-exists",
4818
+ params: {
4819
+ version: input.version,
4820
+ kind: "class",
4821
+ name: classLikeSymbol,
4822
+ sourceMapping: input.mapping
4823
+ }
4824
+ }
4825
+ : undefined
4826
+ }
4827
+ });
4828
+ }
4829
+ releaseLifecycleMappingGraph(version, sourcePriority) {
4830
+ if ("releaseGraphCacheEntry" in this.mappingService &&
4831
+ typeof this.mappingService.releaseGraphCacheEntry === "function") {
4832
+ this.mappingService.releaseGraphCacheEntry(version, sourcePriority);
4833
+ }
4834
+ }
4048
4835
  async resolveToObfuscatedClassName(className, version, mapping, sourcePriority, warnings) {
4049
4836
  return this.resolveClassNameForLookup({
4050
4837
  className,
@@ -4106,21 +4893,22 @@ export class SourceService {
4106
4893
  descriptor: kind === "method" ? descriptor : undefined
4107
4894
  };
4108
4895
  }
4109
- async remapSignatureMembers(members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings) {
4896
+ async remapSignatureMembers(members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings, projectPath) {
4110
4897
  const failedNames = new Set();
4111
4898
  if (sourceMapping === targetMapping) {
4112
4899
  return { members, failedNames };
4113
4900
  }
4114
4901
  // Build deduplicated lookup tables for member names and owner FQNs
4115
4902
  const memberKeyToRemapped = new Map();
4903
+ const memberDescriptorRemapped = new Map();
4116
4904
  const ownerToRemapped = new Map();
4117
4905
  for (const member of members) {
4118
4906
  const memberKey = `${member.ownerFqn}\0${member.name}\0${member.jvmDescriptor}`;
4119
4907
  if (!memberKeyToRemapped.has(memberKey)) {
4120
- memberKeyToRemapped.set(memberKey, member.name); // default = obfuscated name
4908
+ memberKeyToRemapped.set(memberKey, member.name); // default = source name
4121
4909
  }
4122
4910
  if (!ownerToRemapped.has(member.ownerFqn)) {
4123
- ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = obfuscated FQN
4911
+ ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = source FQN
4124
4912
  }
4125
4913
  }
4126
4914
  // Phase 1: Remap owner FQNs first (needed for member disambiguation)
@@ -4133,22 +4921,98 @@ export class SourceService {
4133
4921
  name: obfuscatedFqn,
4134
4922
  sourceMapping,
4135
4923
  targetMapping,
4136
- sourcePriority
4924
+ sourcePriority,
4925
+ projectPath
4137
4926
  });
4138
4927
  if (mapped.resolved && mapped.resolvedSymbol) {
4139
4928
  ownerToRemapped.set(obfuscatedFqn, mapped.resolvedSymbol.name);
4140
4929
  }
4141
4930
  }
4142
4931
  catch {
4143
- // keep obfuscated FQN as fallback
4932
+ // keep source FQN as fallback
4144
4933
  }
4145
4934
  }));
4146
- // 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";
4147
4981
  const memberEntries = [...memberKeyToRemapped.entries()];
4148
- await Promise.all(memberEntries.map(async ([key, _obfuscatedName]) => {
4982
+ await Promise.all(memberEntries.map(async ([key, _sourceName]) => {
4149
4983
  const [ownerFqn, name, descriptor] = key.split("\0");
4150
4984
  try {
4151
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;
4152
5016
  const mapped = await this.mappingService.findMapping({
4153
5017
  version,
4154
5018
  kind,
@@ -4158,10 +5022,17 @@ export class SourceService {
4158
5022
  sourceMapping,
4159
5023
  targetMapping,
4160
5024
  sourcePriority,
4161
- disambiguation: { ownerHint: targetOwner }
5025
+ projectPath,
5026
+ disambiguation: {
5027
+ ownerHint: targetOwner,
5028
+ descriptorHint: remappedDescriptorHint
5029
+ }
4162
5030
  });
4163
5031
  if (mapped.resolved && mapped.resolvedSymbol) {
4164
5032
  memberKeyToRemapped.set(key, mapped.resolvedSymbol.name);
5033
+ if (kind === "method" && mapped.resolvedSymbol.descriptor) {
5034
+ memberDescriptorRemapped.set(key, mapped.resolvedSymbol.descriptor);
5035
+ }
4165
5036
  }
4166
5037
  else if (mapped.status === "ambiguous" && mapped.candidates && mapped.candidates.length > 0) {
4167
5038
  // Disambiguate: filter by target owner and pick the best candidate
@@ -4189,13 +5060,20 @@ export class SourceService {
4189
5060
  failedNames.add(name);
4190
5061
  }
4191
5062
  }));
5063
+ const isField = kind === "field";
4192
5064
  return {
4193
5065
  members: members.map((member) => {
4194
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);
4195
5071
  return {
4196
5072
  ...member,
4197
- name: memberKeyToRemapped.get(memberKey) ?? member.name,
4198
- 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)
4199
5077
  };
4200
5078
  }),
4201
5079
  failedNames
@@ -4540,4 +5418,39 @@ export class SourceService {
4540
5418
  this.metrics.setCacheArtifactByteAccountingRef(this.cacheMetricsState.lru);
4541
5419
  }
4542
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
+ }
4543
5456
  //# sourceMappingURL=source-service.js.map