@adhisang/minecraft-modding-mcp 1.2.1 → 2.0.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.
@@ -80,6 +80,23 @@ function hasExactVersionToken(path, version) {
80
80
  const pattern = new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}([^0-9a-z]|$)`, "i");
81
81
  return pattern.test(normalizedPath);
82
82
  }
83
+ function looksLikeDeobfuscatedClassName(value) {
84
+ const trimmed = value.trim();
85
+ if (!trimmed) {
86
+ return false;
87
+ }
88
+ if (trimmed.startsWith("net.minecraft.") || trimmed.startsWith("com.mojang.")) {
89
+ return true;
90
+ }
91
+ const simpleName = trimmed.split(/[.$]/).at(-1) ?? trimmed;
92
+ return /^[A-Z][A-Za-z0-9_$]{2,}$/.test(simpleName);
93
+ }
94
+ function obfuscatedNamespaceHint(className) {
95
+ return `Artifact is indexed in obfuscated runtime names. Deobfuscated names like "${className}" usually require mapping="mojang" or a find-mapping lookup to obfuscated names.`;
96
+ }
97
+ function hasPartialNetMinecraftCoverage(qualityFlags) {
98
+ return qualityFlags.includes("partial-source-no-net-minecraft");
99
+ }
83
100
  function normalizeOptionalProjectPath(projectPath) {
84
101
  if (!projectPath) {
85
102
  return undefined;
@@ -168,9 +185,9 @@ const COMMON_SOURCE_ROOTS = [
168
185
  ];
169
186
  function normalizeMapping(mapping) {
170
187
  if (mapping == null) {
171
- return "official";
188
+ return "obfuscated";
172
189
  }
173
- if (mapping === "official" ||
190
+ if (mapping === "obfuscated" ||
174
191
  mapping === "mojang" ||
175
192
  mapping === "intermediary" ||
176
193
  mapping === "yarn") {
@@ -181,8 +198,8 @@ function normalizeMapping(mapping) {
181
198
  message: `Unsupported mapping "${mapping}".`,
182
199
  details: {
183
200
  mapping,
184
- nextAction: "Try mapping=official which is always available.",
185
- suggestedCall: { tool: "resolve-artifact", params: { mapping: "official" } }
201
+ nextAction: "Try mapping=obfuscated which is always available.",
202
+ suggestedCall: { tool: "resolve-artifact", params: { mapping: "obfuscated" } }
186
203
  }
187
204
  });
188
205
  }
@@ -194,7 +211,7 @@ function normalizeAccessWidenerNamespace(namespace) {
194
211
  if (normalized === "named") {
195
212
  return "yarn";
196
213
  }
197
- if (normalized === "official" ||
214
+ if (normalized === "obfuscated" ||
198
215
  normalized === "mojang" ||
199
216
  normalized === "intermediary" ||
200
217
  normalized === "yarn") {
@@ -333,7 +350,7 @@ function canUseIndexedSearchPath(indexedSearchEnabled, intent, match, _scope) {
333
350
  if (match === "regex") {
334
351
  return false;
335
352
  }
336
- // fileGlob and symbolKind are now applied as post-filters on indexed candidates
353
+ // packagePrefix and fileGlob are applied as post-filters on indexed candidates.
337
354
  return true;
338
355
  }
339
356
  function buildGlobRegex(pattern) {
@@ -412,14 +429,6 @@ function checkPackagePrefix(filePath, packagePrefix) {
412
429
  const normalizedPrefix = packagePrefix.replace(/\.+/g, "/").replace(/\/+$/, "");
413
430
  return normalizePathStyle(filePath).startsWith(`${normalizedPrefix}/`);
414
431
  }
415
- function buildSnippetWindow(lines) {
416
- const totalLines = clampLimit(lines, 8, 80);
417
- const before = Math.floor((totalLines - 1) / 2);
418
- return {
419
- before,
420
- after: Math.max(0, totalLines - 1 - before)
421
- };
422
- }
423
432
  function buildSearchCursorContext(input) {
424
433
  return JSON.stringify({
425
434
  artifactId: input.artifactId,
@@ -427,7 +436,6 @@ function buildSearchCursorContext(input) {
427
436
  intent: input.intent,
428
437
  match: input.match,
429
438
  queryMode: input.queryMode,
430
- includeDefinition: input.includeDefinition,
431
439
  packagePrefix: input.scope?.packagePrefix ?? "",
432
440
  fileGlob: input.scope?.fileGlob ?? "",
433
441
  symbolKind: input.scope?.symbolKind ?? ""
@@ -521,41 +529,6 @@ function matchRegexIndex(target, regex) {
521
529
  const result = regex.exec(target);
522
530
  return result?.index ?? -1;
523
531
  }
524
- function indexToLine(content, index) {
525
- if (index <= 0) {
526
- return 1;
527
- }
528
- return content.slice(0, index).split(/\r?\n/).length;
529
- }
530
- function lineToSymbol(symbol) {
531
- if (!isSymbolKind(symbol.symbolKind)) {
532
- return undefined;
533
- }
534
- return {
535
- symbolKind: symbol.symbolKind,
536
- symbolName: symbol.symbolName,
537
- qualifiedName: symbol.qualifiedName,
538
- line: symbol.line
539
- };
540
- }
541
- function toContextSnippet(content, centerLineInput, beforeLines, afterLines, withLineNumbers) {
542
- const lines = content.split(/\r?\n/);
543
- const centerLine = Math.min(Math.max(1, centerLineInput), Math.max(lines.length, 1));
544
- const requestedStart = Math.max(1, centerLine - beforeLines);
545
- const requestedEnd = centerLine + afterLines;
546
- const startLine = Math.min(requestedStart, Math.max(lines.length, 1));
547
- const endLine = Math.min(requestedEnd, Math.max(lines.length, 1));
548
- const snippetLines = lines.slice(startLine - 1, endLine);
549
- const snippet = withLineNumbers
550
- ? snippetLines.map((line, index) => `${startLine + index}: ${line}`).join("\n")
551
- : snippetLines.join("\n");
552
- return {
553
- startLine,
554
- endLine,
555
- snippet,
556
- truncated: requestedStart !== startLine || requestedEnd !== endLine
557
- };
558
- }
559
532
  function chunkArray(items, chunkSize) {
560
533
  const size = Math.max(1, Math.trunc(chunkSize));
561
534
  if (items.length === 0) {
@@ -663,11 +636,12 @@ export class SourceService {
663
636
  const selected = candidates.find((candidate) => candidate.hasMinecraftNamespace) ?? candidates[0];
664
637
  const candidateArtifacts = candidates
665
638
  .slice(0, 20)
666
- .map((candidate) => `${candidate.jarPath}#java=${candidate.javaEntryCount}`);
639
+ .map((candidate) => `${candidate.jarPath}#java=${candidate.javaEntryCount}#net_minecraft=${candidate.hasMinecraftNamespace ? 1 : 0}`);
667
640
  return {
668
641
  searchedPaths,
669
642
  candidateArtifacts,
670
- selectedSourceJarPath: selected?.jarPath
643
+ selectedSourceJarPath: selected?.jarPath,
644
+ selectedHasMinecraftNamespace: selected?.hasMinecraftNamespace
671
645
  };
672
646
  }
673
647
  buildVersionSourceRecoveryCommand(projectPath) {
@@ -734,13 +708,13 @@ export class SourceService {
734
708
  // coordinate validity is validated by resolver, keep version undefined on parse failure.
735
709
  }
736
710
  }
737
- // Unobfuscated versions (MC 26.1+) ship with official names; intermediary/yarn are not applicable.
711
+ // Unobfuscated versions (MC 26.1+) ship with deobfuscated runtime names; intermediary/yarn are not applicable.
738
712
  let effectiveMapping = mapping;
739
713
  if ((mapping === "intermediary" || mapping === "yarn") &&
740
714
  resolvedVersion &&
741
715
  isUnobfuscatedVersion(resolvedVersion)) {
742
- warnings.push(`Version ${resolvedVersion} is unobfuscated; ${mapping} mappings are not applicable. Using official names.`);
743
- effectiveMapping = "official";
716
+ warnings.push(`Version ${resolvedVersion} is unobfuscated; ${mapping} mappings are not applicable. Using the obfuscated namespace label for the deobfuscated runtime names.`);
717
+ effectiveMapping = "obfuscated";
744
718
  }
745
719
  if (kind === "version" && resolvedVersion && effectiveMapping === "mojang" && scope !== "vanilla") {
746
720
  versionSourceDiscovery = await this.discoverVersionSourceJar({
@@ -797,18 +771,18 @@ export class SourceService {
797
771
  else if (isVanillaMojang) {
798
772
  suggestedCall = {
799
773
  tool: "resolve-artifact",
800
- params: { targetKind: kind, targetValue: value, mapping: "official", scope: "vanilla" }
774
+ params: { targetKind: kind, targetValue: value, mapping: "obfuscated", scope: "vanilla" }
801
775
  };
802
776
  nextAction =
803
777
  "scope=vanilla blocks Loom cache discovery needed for mojang mapping. " +
804
- "Without a projectPath, use mapping=official to read vanilla obfuscated names.";
778
+ "Without a projectPath, use mapping=obfuscated to read vanilla runtime names directly.";
805
779
  }
806
780
  else {
807
781
  suggestedCall = {
808
782
  tool: "resolve-artifact",
809
- params: { targetKind: kind, targetValue: value, mapping: "official", ...(scope ? { scope } : {}) }
783
+ params: { targetKind: kind, targetValue: value, mapping: "obfuscated", ...(scope ? { scope } : {}) }
810
784
  };
811
- nextAction = "Retry with mapping=official to use obfuscated names.";
785
+ nextAction = "Retry with mapping=obfuscated to use the runtime obfuscated namespace.";
812
786
  }
813
787
  throw createError({
814
788
  code: ERROR_CODES.MAPPING_NOT_APPLIED,
@@ -841,7 +815,7 @@ export class SourceService {
841
815
  }
842
816
  const mappingAvailability = await this.mappingService.ensureMappingAvailable({
843
817
  version: resolved.version,
844
- sourceMapping: "official",
818
+ sourceMapping: "obfuscated",
845
819
  targetMapping: effectiveMapping,
846
820
  sourcePriority: input.sourcePriority
847
821
  });
@@ -864,6 +838,10 @@ export class SourceService {
864
838
  }
865
839
  if (versionSourceDiscovery?.selectedSourceJarPath) {
866
840
  resolved.qualityFlags.push("source-jar-validated");
841
+ if (versionSourceDiscovery.selectedHasMinecraftNamespace === false) {
842
+ resolved.qualityFlags.push("partial-source-no-net-minecraft");
843
+ warnings.push(`Source coverage does not include net.minecraft for ${versionSourceDiscovery.selectedSourceJarPath}; class lookups may fall back to the binary artifact.`);
844
+ }
867
845
  if (kind === "version" && !hasExactVersionToken(versionSourceDiscovery.selectedSourceJarPath, value)) {
868
846
  if (input.strictVersion) {
869
847
  throw createError({
@@ -942,8 +920,7 @@ export class SourceService {
942
920
  if (!query) {
943
921
  return {
944
922
  hits: [],
945
- totalApprox: 0,
946
- mappingApplied: artifact.mappingApplied ?? "official"
923
+ mappingApplied: artifact.mappingApplied ?? "obfuscated"
947
924
  };
948
925
  }
949
926
  const intent = normalizeIntent(input.intent);
@@ -961,12 +938,15 @@ export class SourceService {
961
938
  const searchLimitCap = match === "regex"
962
939
  ? Math.max(1, Math.min(this.config.maxSearchHits, MAX_REGEX_RESULT_LIMIT))
963
940
  : this.config.maxSearchHits;
941
+ const scope = input.scope;
942
+ if (scope?.symbolKind && intent !== "symbol") {
943
+ throw createError({
944
+ code: ERROR_CODES.INVALID_INPUT,
945
+ message: 'symbolKind filter is only supported when intent="symbol".'
946
+ });
947
+ }
964
948
  const limit = clampLimit(input.limit, 20, searchLimitCap);
965
- const includeDefinition = input.include?.includeDefinition ?? false;
966
- const includeOneHop = input.include?.includeOneHop ?? false;
967
- const snippetWindow = buildSnippetWindow(input.include?.snippetLines);
968
949
  const regexPattern = match === "regex" ? compileRegex(query) : undefined;
969
- const scope = input.scope;
970
950
  const queryMode = input.queryMode ?? "auto";
971
951
  const cursorContext = buildSearchCursorContext({
972
952
  artifactId: artifact.artifactId,
@@ -974,8 +954,7 @@ export class SourceService {
974
954
  intent,
975
955
  match,
976
956
  queryMode,
977
- scope,
978
- includeDefinition
957
+ scope
979
958
  });
980
959
  const decodedCursor = decodeSearchCursor(input.cursor);
981
960
  const cursor = decodedCursor?.contextKey === cursorContext ? decodedCursor : undefined;
@@ -991,52 +970,35 @@ export class SourceService {
991
970
  const hasSeparators = /[._$]/.test(query);
992
971
  const tokenOnlyTextIntent = intent === "text" && queryMode === "token";
993
972
  if (intent === "symbol") {
994
- this.searchSymbolIntent(artifact.artifactId, query, match, scope, snippetWindow, regexPattern, recordHit);
995
- // WS4: Use repo-level COUNT for symbol totalApprox when not regex
996
- if (match !== "regex") {
997
- const approxCount = this.symbolsRepo.countScopedSymbols({
998
- artifactId: artifact.artifactId,
999
- query,
1000
- match,
1001
- symbolKind: scope?.symbolKind,
1002
- packagePrefix: scope?.packagePrefix
1003
- });
1004
- accumulator.setTotalApproxOverride(approxCount);
1005
- }
973
+ this.searchSymbolIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1006
974
  }
1007
975
  else if (queryMode === "literal" && intent === "text") {
1008
976
  // F-03: queryMode=literal forces substring scan for text intent
1009
977
  this.metrics.recordSearchFallback();
1010
- this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
978
+ this.searchTextIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1011
979
  }
1012
980
  else if (!indexedSearchEnabled) {
1013
981
  this.metrics.recordIndexedDisabled();
1014
982
  if (!tokenOnlyTextIntent) {
1015
983
  this.metrics.recordSearchFallback();
1016
984
  if (intent === "path") {
1017
- this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
985
+ this.searchPathIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1018
986
  }
1019
987
  else {
1020
- this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
988
+ this.searchTextIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1021
989
  }
1022
990
  }
1023
991
  }
1024
992
  else if (canUseIndexedSearchPath(indexedSearchEnabled, intent, match, scope)) {
1025
993
  try {
1026
994
  if (intent === "path") {
1027
- this.searchPathIntentIndexed(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, recordHit);
1028
- // WS4: Use repo-level COUNT for totalApprox instead of accumulator count
1029
- const approxCount = this.filesRepo.countPathCandidates(artifact.artifactId, query);
1030
- accumulator.setTotalApproxOverride(approxCount);
995
+ this.searchPathIntentIndexed(artifact.artifactId, query, match, scope, recordHit);
1031
996
  }
1032
997
  else {
1033
- this.searchTextIntentIndexed(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, recordHit);
1034
- // WS4: Use repo-level COUNT for totalApprox instead of accumulator count
1035
- const approxCount = this.filesRepo.countTextCandidates(artifact.artifactId, query);
1036
- accumulator.setTotalApproxOverride(approxCount);
998
+ this.searchTextIntentIndexed(artifact.artifactId, query, match, scope, recordHit);
1037
999
  // F-03: queryMode=auto fallback — when indexed returns 0 hits and query has separators, retry with literal scan
1038
1000
  if (queryMode === "auto" && hasSeparators && accumulator.currentCount() === 0) {
1039
- this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1001
+ this.searchTextIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1040
1002
  }
1041
1003
  }
1042
1004
  this.metrics.recordSearchIndexedHit();
@@ -1052,10 +1014,10 @@ export class SourceService {
1052
1014
  // F-03: queryMode=token suppresses error-path fallback to brute-force scan
1053
1015
  if (!tokenOnlyTextIntent) {
1054
1016
  if (intent === "path") {
1055
- this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1017
+ this.searchPathIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1056
1018
  }
1057
1019
  else {
1058
- this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1020
+ this.searchTextIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1059
1021
  }
1060
1022
  }
1061
1023
  }
@@ -1064,10 +1026,10 @@ export class SourceService {
1064
1026
  if (!tokenOnlyTextIntent) {
1065
1027
  this.metrics.recordSearchFallback();
1066
1028
  if (intent === "path") {
1067
- this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1029
+ this.searchPathIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1068
1030
  }
1069
1031
  else {
1070
- this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1032
+ this.searchTextIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1071
1033
  }
1072
1034
  }
1073
1035
  }
@@ -1078,31 +1040,11 @@ export class SourceService {
1078
1040
  const nextCursor = finalizedHits.nextCursorHit
1079
1041
  ? encodeSearchCursor(finalizedHits.nextCursorHit, cursorContext)
1080
1042
  : undefined;
1081
- const relations = includeOneHop
1082
- ? this.buildOneHopRelations(artifact.artifactId, page.flatMap((hit) => hit.symbol && isSymbolKind(hit.symbol.symbolKind)
1083
- ? [
1084
- {
1085
- symbolKind: hit.symbol.symbolKind,
1086
- symbolName: hit.symbol.symbolName,
1087
- filePath: hit.filePath,
1088
- line: hit.symbol.line
1089
- }
1090
- ]
1091
- : []), 10)
1092
- : undefined;
1093
- if (relations?.length) {
1094
- this.metrics.recordOneHopExpansion(relations.length);
1095
- }
1096
- this.metrics.recordSearchTokenBytesReturned(Buffer.byteLength(JSON.stringify({ hits: page, relations }), "utf8"));
1097
- // B5: If post-filtering eliminated all hits on the first page, the SQL-based
1098
- // totalApprox is misleading — correct it to 0.
1099
- const totalApprox = page.length === 0 && !cursor ? 0 : finalizedHits.totalApprox;
1043
+ this.metrics.recordSearchTokenBytesReturned(Buffer.byteLength(JSON.stringify({ hits: page }), "utf8"));
1100
1044
  return {
1101
1045
  hits: page,
1102
- relations: relations && relations.length > 0 ? relations : undefined,
1103
1046
  nextCursor,
1104
- totalApprox,
1105
- mappingApplied: artifact.mappingApplied ?? "official"
1047
+ mappingApplied: artifact.mappingApplied ?? "obfuscated"
1106
1048
  };
1107
1049
  }
1108
1050
  finally {
@@ -1139,7 +1081,7 @@ export class SourceService {
1139
1081
  content,
1140
1082
  contentBytes: fullBytes,
1141
1083
  truncated,
1142
- mappingApplied: artifact.mappingApplied ?? "official"
1084
+ mappingApplied: artifact.mappingApplied ?? "obfuscated"
1143
1085
  };
1144
1086
  }
1145
1087
  finally {
@@ -1159,7 +1101,7 @@ export class SourceService {
1159
1101
  return {
1160
1102
  items: page.items,
1161
1103
  nextCursor: page.nextCursor,
1162
- mappingApplied: artifact.mappingApplied ?? "official"
1104
+ mappingApplied: artifact.mappingApplied ?? "obfuscated"
1163
1105
  };
1164
1106
  }
1165
1107
  finally {
@@ -1304,12 +1246,13 @@ export class SourceService {
1304
1246
  }
1305
1247
  const mappingApplied = workspaceDetection.mappingApplied;
1306
1248
  if (kind === "method") {
1249
+ const methodOwner = owner;
1250
+ const methodDescriptor = descriptor;
1307
1251
  const exact = await this.mappingService.resolveMethodMappingExact({
1308
1252
  version,
1309
- kind: "method",
1310
- owner,
1253
+ owner: methodOwner,
1311
1254
  name,
1312
- descriptor: descriptor,
1255
+ descriptor: methodDescriptor,
1313
1256
  sourceMapping: input.sourceMapping,
1314
1257
  targetMapping: mappingApplied,
1315
1258
  sourcePriority: input.sourcePriority
@@ -1473,14 +1416,14 @@ export class SourceService {
1473
1416
  try {
1474
1417
  let resolvedSymbols = resolvedSymbolsByVersion.get(version);
1475
1418
  if (!resolvedSymbols) {
1476
- const [officialClassName, officialMethod] = await Promise.all([
1477
- this.resolveToOfficialClassName(userClassName, version, mapping, input.sourcePriority, warnings),
1478
- this.resolveToOfficialMemberName(userMethodName, userClassName, descriptor, "method", version, mapping, input.sourcePriority, warnings)
1419
+ const [obfuscatedClassName, obfuscatedMethod] = await Promise.all([
1420
+ this.resolveToObfuscatedClassName(userClassName, version, mapping, input.sourcePriority, warnings),
1421
+ this.resolveToObfuscatedMemberName(userMethodName, userClassName, descriptor, "method", version, mapping, input.sourcePriority, warnings)
1479
1422
  ]);
1480
1423
  resolvedSymbols = {
1481
- className: officialClassName,
1482
- methodName: officialMethod.name,
1483
- methodDescriptor: officialMethod.descriptor
1424
+ className: obfuscatedClassName,
1425
+ methodName: obfuscatedMethod.name,
1426
+ methodDescriptor: obfuscatedMethod.descriptor
1484
1427
  };
1485
1428
  resolvedSymbolsByVersion.set(version, resolvedSymbols);
1486
1429
  }
@@ -1630,18 +1573,18 @@ export class SourceService {
1630
1573
  });
1631
1574
  }
1632
1575
  const mappingWarnings = [];
1633
- const officialFromClassName = await this.resolveToOfficialClassName(className, fromVersion, mapping, input.sourcePriority, mappingWarnings);
1634
- const officialToClassName = fromVersion === toVersion
1635
- ? officialFromClassName
1636
- : await this.resolveToOfficialClassName(className, toVersion, mapping, input.sourcePriority, mappingWarnings);
1576
+ const obfuscatedFromClassName = await this.resolveToObfuscatedClassName(className, fromVersion, mapping, input.sourcePriority, mappingWarnings);
1577
+ const obfuscatedToClassName = fromVersion === toVersion
1578
+ ? obfuscatedFromClassName
1579
+ : await this.resolveToObfuscatedClassName(className, toVersion, mapping, input.sourcePriority, mappingWarnings);
1637
1580
  const [fromResolved, toResolved] = await Promise.all([
1638
1581
  this.versionService.resolveVersionJar(fromVersion),
1639
1582
  this.versionService.resolveVersionJar(toVersion)
1640
1583
  ]);
1641
- const loadSignature = async (version, jarPath, officialClassName) => {
1584
+ const loadSignature = async (version, jarPath, obfuscatedClassName) => {
1642
1585
  try {
1643
1586
  const signature = await this.explorerService.getSignature({
1644
- fqn: officialClassName,
1587
+ fqn: obfuscatedClassName,
1645
1588
  jarPath,
1646
1589
  access: "all",
1647
1590
  includeSynthetic: false,
@@ -1662,8 +1605,8 @@ export class SourceService {
1662
1605
  }
1663
1606
  };
1664
1607
  const [fromSignature, toSignature] = await Promise.all([
1665
- loadSignature(fromVersion, fromResolved.jarPath, officialFromClassName),
1666
- loadSignature(toVersion, toResolved.jarPath, officialToClassName)
1608
+ loadSignature(fromVersion, fromResolved.jarPath, obfuscatedFromClassName),
1609
+ loadSignature(toVersion, toResolved.jarPath, obfuscatedToClassName)
1667
1610
  ]);
1668
1611
  const warnings = [...mappingWarnings];
1669
1612
  if (fromSignature) {
@@ -1740,16 +1683,16 @@ export class SourceService {
1740
1683
  : classChange === "absent_in_both"
1741
1684
  ? emptyDiffDelta()
1742
1685
  : diffMembersByKey(fromMembers.fields, toMembers.fields, (member) => member.name, true);
1743
- // Remap diff delta members for non-official mappings
1686
+ // Remap diff delta members for non-obfuscated mappings
1744
1687
  const remapDelta = async (delta, kind) => {
1745
1688
  const [addedResult, removedResult] = await Promise.all([
1746
- this.remapSignatureMembers(delta.added, kind, toVersion, mapping, input.sourcePriority, warnings),
1747
- this.remapSignatureMembers(delta.removed, kind, fromVersion, mapping, input.sourcePriority, warnings)
1689
+ this.remapSignatureMembers(delta.added, kind, toVersion, "obfuscated", mapping, input.sourcePriority, warnings),
1690
+ this.remapSignatureMembers(delta.removed, kind, fromVersion, "obfuscated", mapping, input.sourcePriority, warnings)
1748
1691
  ]);
1749
1692
  const remappedModified = await Promise.all(delta.modified.map(async (change) => {
1750
1693
  const [fromResult, toResult] = await Promise.all([
1751
- this.remapSignatureMembers([change.from], kind, fromVersion, mapping, input.sourcePriority, warnings),
1752
- this.remapSignatureMembers([change.to], kind, toVersion, mapping, input.sourcePriority, warnings)
1694
+ this.remapSignatureMembers([change.from], kind, fromVersion, "obfuscated", mapping, input.sourcePriority, warnings),
1695
+ this.remapSignatureMembers([change.to], kind, toVersion, "obfuscated", mapping, input.sourcePriority, warnings)
1753
1696
  ]);
1754
1697
  return { ...change, from: fromResult.members[0], to: toResult.members[0] };
1755
1698
  }));
@@ -1817,7 +1760,7 @@ export class SourceService {
1817
1760
  });
1818
1761
  }
1819
1762
  // Verify artifact exists
1820
- this.getArtifact(artifactId);
1763
+ const artifact = this.getArtifact(artifactId);
1821
1764
  const limit = Math.max(1, Math.min(input.limit ?? 20, 200));
1822
1765
  const warnings = [];
1823
1766
  const isQualified = className.includes(".");
@@ -1847,7 +1790,17 @@ export class SourceService {
1847
1790
  symbolKind: row.symbolKind
1848
1791
  }))
1849
1792
  .slice(0, limit);
1850
- return { matches, total: matches.length, warnings };
1793
+ const partialVanillaLookup = hasPartialNetMinecraftCoverage(artifact.qualityFlags) && looksLikeDeobfuscatedClassName(className);
1794
+ const filteredMatches = partialVanillaLookup && matches.every((match) => !match.qualifiedName.startsWith("net.minecraft.") && !match.qualifiedName.startsWith("com.mojang."))
1795
+ ? []
1796
+ : matches;
1797
+ if (filteredMatches.length === 0 && partialVanillaLookup) {
1798
+ warnings.push(`Artifact source coverage is partial and excludes net.minecraft; returning non-vanilla matches for "${className}" would be misleading. Use get-class-source/get-class-members for binary fallback or get-class-api-matrix for mapped API inspection.`);
1799
+ }
1800
+ if (filteredMatches.length === 0 && artifact.mappingApplied === "obfuscated" && looksLikeDeobfuscatedClassName(className)) {
1801
+ warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
1802
+ }
1803
+ return { matches: filteredMatches, total: filteredMatches.length, warnings };
1851
1804
  }
1852
1805
  // Simple name: search for exact symbol name match among type symbols
1853
1806
  const result = this.symbolsRepo.findScopedSymbols({
@@ -1871,7 +1824,17 @@ export class SourceService {
1871
1824
  symbolKind: row.symbolKind
1872
1825
  });
1873
1826
  }
1874
- return { matches, total: matches.length, warnings };
1827
+ const partialVanillaLookup = hasPartialNetMinecraftCoverage(artifact.qualityFlags) && looksLikeDeobfuscatedClassName(className);
1828
+ const filteredMatches = partialVanillaLookup && matches.every((match) => !match.qualifiedName.startsWith("net.minecraft.") && !match.qualifiedName.startsWith("com.mojang."))
1829
+ ? []
1830
+ : matches;
1831
+ if (filteredMatches.length === 0 && partialVanillaLookup) {
1832
+ warnings.push(`Artifact source coverage is partial and excludes net.minecraft; returning non-vanilla matches for "${className}" would be misleading. Use get-class-source/get-class-members for binary fallback or get-class-api-matrix for mapped API inspection.`);
1833
+ }
1834
+ if (filteredMatches.length === 0 && artifact.mappingApplied === "obfuscated" && looksLikeDeobfuscatedClassName(className)) {
1835
+ warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
1836
+ }
1837
+ return { matches: filteredMatches, total: filteredMatches.length, warnings };
1875
1838
  }
1876
1839
  async getClassSource(input) {
1877
1840
  const className = input.className.trim();
@@ -1919,6 +1882,10 @@ export class SourceService {
1919
1882
  let mappingApplied = requestedMapping;
1920
1883
  let provenance;
1921
1884
  let qualityFlags = [];
1885
+ let sourceJarPath;
1886
+ let binaryJarPath;
1887
+ let version;
1888
+ let coordinate;
1922
1889
  if (!artifactId) {
1923
1890
  if (!input.target) {
1924
1891
  throw createError({
@@ -1943,6 +1910,10 @@ export class SourceService {
1943
1910
  mappingApplied = resolved.mappingApplied;
1944
1911
  provenance = resolved.provenance;
1945
1912
  qualityFlags = [...resolved.qualityFlags];
1913
+ sourceJarPath = resolved.resolvedSourceJarPath;
1914
+ binaryJarPath = resolved.binaryJarPath;
1915
+ version = resolved.version;
1916
+ coordinate = resolved.coordinate;
1946
1917
  }
1947
1918
  else {
1948
1919
  const artifact = this.getArtifact(artifactId);
@@ -1951,53 +1922,122 @@ export class SourceService {
1951
1922
  mappingApplied = artifact.mappingApplied ?? requestedMapping;
1952
1923
  provenance = artifact.provenance;
1953
1924
  qualityFlags = artifact.qualityFlags;
1925
+ sourceJarPath = artifact.sourceJarPath;
1926
+ binaryJarPath = artifact.binaryJarPath;
1927
+ version = artifact.version;
1928
+ coordinate = artifact.coordinate;
1929
+ }
1930
+ let activeArtifactId = artifactId;
1931
+ let activeOrigin = origin;
1932
+ let activeProvenance = provenance;
1933
+ let activeQualityFlags = [...qualityFlags];
1934
+ let activeMappingApplied = mappingApplied;
1935
+ let activeSourceJarPath = sourceJarPath;
1936
+ let attemptedBinaryFallback = false;
1937
+ const tryBinaryFallback = async () => {
1938
+ if (attemptedBinaryFallback) {
1939
+ return false;
1940
+ }
1941
+ attemptedBinaryFallback = true;
1942
+ const normalizedBinaryJarPath = normalizeOptionalString(binaryJarPath);
1943
+ if (!normalizedBinaryJarPath) {
1944
+ return false;
1945
+ }
1946
+ if (activeSourceJarPath &&
1947
+ normalizePathStyle(activeSourceJarPath) === normalizePathStyle(normalizedBinaryJarPath)) {
1948
+ return false;
1949
+ }
1950
+ const fallbackResolved = await this.resolveBinaryFallbackArtifact({
1951
+ binaryJarPath: normalizedBinaryJarPath,
1952
+ version,
1953
+ coordinate,
1954
+ requestedMapping,
1955
+ mappingApplied,
1956
+ provenance: activeProvenance,
1957
+ qualityFlags: activeQualityFlags
1958
+ });
1959
+ if (!fallbackResolved || fallbackResolved.artifactId === activeArtifactId) {
1960
+ return false;
1961
+ }
1962
+ activeArtifactId = fallbackResolved.artifactId;
1963
+ activeOrigin = fallbackResolved.origin;
1964
+ activeMappingApplied = fallbackResolved.mappingApplied ?? activeMappingApplied;
1965
+ activeProvenance = fallbackResolved.provenance ?? activeProvenance;
1966
+ activeQualityFlags = [...new Set([...(fallbackResolved.qualityFlags ?? []), "binary-fallback"])];
1967
+ activeSourceJarPath = fallbackResolved.sourceJarPath;
1968
+ warnings.push(`Falling back to binary artifact "${normalizedBinaryJarPath}" because source coverage for "${className}" was incomplete.`);
1969
+ if (activeMappingApplied !== requestedMapping) {
1970
+ warnings.push(`Fallback source text is indexed in ${activeMappingApplied} names; returned source is not remapped to ${requestedMapping}.`);
1971
+ }
1972
+ return true;
1973
+ };
1974
+ let activeLookupClassName = await this.resolveClassNameForLookup({
1975
+ className,
1976
+ version,
1977
+ sourceMapping: requestedMapping,
1978
+ targetMapping: activeMappingApplied,
1979
+ sourcePriority: input.sourcePriority,
1980
+ warnings,
1981
+ context: "source lookup"
1982
+ });
1983
+ let filePath = this.resolveClassFilePath(activeArtifactId, activeLookupClassName);
1984
+ if (!filePath && (await tryBinaryFallback())) {
1985
+ activeLookupClassName = await this.resolveClassNameForLookup({
1986
+ className,
1987
+ version,
1988
+ sourceMapping: requestedMapping,
1989
+ targetMapping: activeMappingApplied,
1990
+ sourcePriority: input.sourcePriority,
1991
+ warnings,
1992
+ context: "source lookup"
1993
+ });
1994
+ filePath = this.resolveClassFilePath(activeArtifactId, activeLookupClassName);
1954
1995
  }
1955
- const filePath = this.resolveClassFilePath(artifactId, className);
1956
1996
  if (!filePath) {
1957
- const simpleName = className.split(/[.$]/).at(-1) ?? className;
1958
- const targetKind = input.target?.kind;
1959
- const targetValue = input.target?.value;
1960
- const scope = input.scope;
1961
- let nextAction = `Use find-class to resolve the correct fully-qualified name for "${simpleName}".`;
1962
- if (targetKind === "version" && scope && scope !== "merged" && !input.projectPath) {
1963
- nextAction +=
1964
- ` If the class exists in a modded environment, retry with scope: "merged" and projectPath pointing to your mod project.`;
1965
- }
1966
- else if (targetKind === "version" && scope && scope !== "merged" && input.projectPath) {
1967
- nextAction +=
1968
- ` The class may exist in merged sources; retry with scope: "merged".`;
1969
- }
1970
- throw createError({
1971
- code: ERROR_CODES.CLASS_NOT_FOUND,
1972
- message: `Source for class "${className}" was not found.`,
1973
- details: {
1974
- artifactId,
1975
- className,
1976
- ...(scope ? { scope } : {}),
1977
- ...(targetKind ? { targetKind } : {}),
1978
- ...(targetValue ? { targetValue } : {}),
1979
- mapping: mappingApplied,
1980
- nextAction,
1981
- suggestedCall: { tool: "find-class", params: { className: simpleName, artifactId } }
1982
- }
1997
+ throw this.buildClassSourceNotFoundError({
1998
+ artifactId: activeArtifactId,
1999
+ className,
2000
+ lookupClassName: activeLookupClassName,
2001
+ mappingApplied: activeMappingApplied,
2002
+ requestedMapping,
2003
+ qualityFlags: activeQualityFlags,
2004
+ attemptedBinaryFallback,
2005
+ targetKind: input.target?.kind,
2006
+ targetValue: input.target?.value,
2007
+ scope: input.scope,
2008
+ projectPath: input.projectPath,
2009
+ version
1983
2010
  });
1984
2011
  }
1985
- const row = this.filesRepo.getFileContent(artifactId, filePath);
2012
+ let row = this.filesRepo.getFileContent(activeArtifactId, filePath);
2013
+ if (!row && (await tryBinaryFallback())) {
2014
+ activeLookupClassName = await this.resolveClassNameForLookup({
2015
+ className,
2016
+ version,
2017
+ sourceMapping: requestedMapping,
2018
+ targetMapping: activeMappingApplied,
2019
+ sourcePriority: input.sourcePriority,
2020
+ warnings,
2021
+ context: "source lookup"
2022
+ });
2023
+ filePath = this.resolveClassFilePath(activeArtifactId, activeLookupClassName) ?? filePath;
2024
+ row = this.filesRepo.getFileContent(activeArtifactId, filePath);
2025
+ }
1986
2026
  if (!row) {
1987
- const simpleName = className.split(/[.$]/).at(-1) ?? className;
1988
- throw createError({
1989
- code: ERROR_CODES.CLASS_NOT_FOUND,
1990
- message: `Source for class "${className}" was not found.`,
1991
- details: {
1992
- artifactId,
1993
- className,
1994
- filePath,
1995
- ...(input.scope ? { scope: input.scope } : {}),
1996
- ...(input.target?.kind ? { targetKind: input.target.kind } : {}),
1997
- ...(input.target?.value ? { targetValue: input.target.value } : {}),
1998
- nextAction: `Use find-class to resolve the correct fully-qualified name for "${simpleName}".`,
1999
- suggestedCall: { tool: "find-class", params: { className: simpleName, artifactId } }
2000
- }
2027
+ throw this.buildClassSourceNotFoundError({
2028
+ artifactId: activeArtifactId,
2029
+ className,
2030
+ lookupClassName: activeLookupClassName,
2031
+ mappingApplied: activeMappingApplied,
2032
+ requestedMapping,
2033
+ qualityFlags: activeQualityFlags,
2034
+ attemptedBinaryFallback,
2035
+ filePath,
2036
+ targetKind: input.target?.kind,
2037
+ targetValue: input.target?.value,
2038
+ scope: input.scope,
2039
+ projectPath: input.projectPath,
2040
+ version
2001
2041
  });
2002
2042
  }
2003
2043
  const lines = row.content.split(/\r?\n/);
@@ -2048,12 +2088,12 @@ export class SourceService {
2048
2088
  resolvedOutputFile = outputPath;
2049
2089
  sourceText = `[Written to ${outputPath}]`;
2050
2090
  }
2051
- const normalizedProvenance = provenance ??
2091
+ const normalizedProvenance = activeProvenance ??
2052
2092
  this.buildFallbackProvenance({
2053
- artifactId,
2054
- origin,
2093
+ artifactId: activeArtifactId,
2094
+ origin: activeOrigin,
2055
2095
  requestedMapping,
2056
- mappingApplied
2096
+ mappingApplied: activeMappingApplied
2057
2097
  });
2058
2098
  return {
2059
2099
  className,
@@ -2066,12 +2106,12 @@ export class SourceService {
2066
2106
  },
2067
2107
  truncated,
2068
2108
  ...(charsTruncated ? { charsTruncated } : {}),
2069
- origin,
2070
- artifactId,
2109
+ origin: activeOrigin,
2110
+ artifactId: activeArtifactId,
2071
2111
  requestedMapping,
2072
- mappingApplied,
2112
+ mappingApplied: activeMappingApplied,
2073
2113
  provenance: normalizedProvenance,
2074
- qualityFlags,
2114
+ qualityFlags: activeQualityFlags,
2075
2115
  ...(resolvedOutputFile ? { outputFile: resolvedOutputFile } : {}),
2076
2116
  warnings
2077
2117
  };
@@ -2148,10 +2188,10 @@ export class SourceService {
2148
2188
  binaryJarPath = artifact.binaryJarPath;
2149
2189
  version = artifact.version;
2150
2190
  }
2151
- if (requestedMapping !== "official" && !version) {
2191
+ if (requestedMapping !== "obfuscated" && !version) {
2152
2192
  throw createError({
2153
2193
  code: ERROR_CODES.MAPPING_NOT_APPLIED,
2154
- message: `Non-official mapping "${requestedMapping}" requires a version, but none was resolved.`,
2194
+ message: `Non-obfuscated mapping "${requestedMapping}" requires a version, but none was resolved.`,
2155
2195
  details: {
2156
2196
  mapping: requestedMapping,
2157
2197
  nextAction: "Resolve with targetKind=version or specify a versioned coordinate.",
@@ -2170,29 +2210,35 @@ export class SourceService {
2170
2210
  }
2171
2211
  });
2172
2212
  }
2173
- const officialClassName = version != null
2174
- ? await this.resolveToOfficialClassName(className, version, requestedMapping, input.sourcePriority, warnings)
2175
- : className;
2213
+ const lookupClassName = await this.resolveClassNameForLookup({
2214
+ className,
2215
+ version,
2216
+ sourceMapping: requestedMapping,
2217
+ targetMapping: mappingApplied,
2218
+ sourcePriority: input.sourcePriority,
2219
+ warnings,
2220
+ context: "binary lookup"
2221
+ });
2176
2222
  const signature = await this.explorerService.getSignature({
2177
- fqn: officialClassName,
2223
+ fqn: lookupClassName,
2178
2224
  jarPath: binaryJarPath,
2179
2225
  access,
2180
2226
  includeSynthetic,
2181
2227
  includeInherited,
2182
- memberPattern: requestedMapping === "official" ? memberPattern : undefined
2228
+ memberPattern: requestedMapping === mappingApplied ? memberPattern : undefined
2183
2229
  });
2184
2230
  warnings.push(...signature.warnings);
2185
2231
  let remappedConstructors = version != null
2186
- ? (await this.remapSignatureMembers(signature.constructors, "method", version, requestedMapping, input.sourcePriority, warnings)).members
2232
+ ? (await this.remapSignatureMembers(signature.constructors, "method", version, mappingApplied, requestedMapping, input.sourcePriority, warnings)).members
2187
2233
  : signature.constructors;
2188
2234
  let remappedFields = version != null
2189
- ? (await this.remapSignatureMembers(signature.fields, "field", version, requestedMapping, input.sourcePriority, warnings)).members
2235
+ ? (await this.remapSignatureMembers(signature.fields, "field", version, mappingApplied, requestedMapping, input.sourcePriority, warnings)).members
2190
2236
  : signature.fields;
2191
2237
  let remappedMethods = version != null
2192
- ? (await this.remapSignatureMembers(signature.methods, "method", version, requestedMapping, input.sourcePriority, warnings)).members
2238
+ ? (await this.remapSignatureMembers(signature.methods, "method", version, mappingApplied, requestedMapping, input.sourcePriority, warnings)).members
2193
2239
  : signature.methods;
2194
- // Apply memberPattern post-remap for non-official mappings
2195
- if (requestedMapping !== "official" && memberPattern) {
2240
+ // Apply memberPattern after remap when the lookup namespace differs from the requested namespace.
2241
+ if (requestedMapping !== mappingApplied && memberPattern) {
2196
2242
  const lowerPattern = memberPattern.toLowerCase();
2197
2243
  remappedConstructors = remappedConstructors.filter((m) => m.name.toLowerCase().includes(lowerPattern));
2198
2244
  remappedFields = remappedFields.filter((m) => m.name.toLowerCase().includes(lowerPattern));
@@ -2248,91 +2294,68 @@ export class SourceService {
2248
2294
  };
2249
2295
  }
2250
2296
  async validateMixin(input) {
2251
- // Mixin config mode: read JSON config(s) and derive sourcePaths
2252
- if (input.mixinConfigPath) {
2253
- const configPaths = Array.isArray(input.mixinConfigPath) ? input.mixinConfigPath : [input.mixinConfigPath];
2254
- const allSourcePaths = [];
2255
- for (const rawConfigPath of configPaths) {
2256
- const normalizedConfigPath = normalizePathForHost(rawConfigPath, undefined, "mixinConfigPath");
2257
- const resolvedConfigPath = isAbsolute(normalizedConfigPath)
2258
- ? normalizedConfigPath
2259
- : resolvePath(process.cwd(), normalizedConfigPath);
2260
- let configJson;
2261
- try {
2262
- const raw = await readFile(resolvedConfigPath, "utf-8");
2263
- configJson = JSON.parse(raw);
2264
- }
2265
- catch (err) {
2266
- throw createError({
2267
- code: ERROR_CODES.INVALID_INPUT,
2268
- message: `Could not read/parse mixinConfigPath "${rawConfigPath}": ${err instanceof Error ? err.message : String(err)}`
2269
- });
2270
- }
2271
- const pkg = configJson.package ?? "";
2272
- const classNames = [
2273
- ...(configJson.mixins ?? []),
2274
- ...(configJson.client ?? []),
2275
- ...(configJson.server ?? [])
2276
- ];
2277
- if (classNames.length === 0) {
2278
- continue; // Skip empty configs in array mode
2279
- }
2280
- // Determine source root(s)
2281
- const projectBase = input.projectPath
2282
- ? (isAbsolute(input.projectPath) ? input.projectPath : resolvePath(process.cwd(), input.projectPath))
2283
- : dirname(resolvedConfigPath);
2284
- let sourceRootCandidates;
2285
- if (input.sourceRoots && input.sourceRoots.length > 0) {
2286
- sourceRootCandidates = input.sourceRoots;
2287
- }
2288
- else if (input.sourceRoot) {
2289
- sourceRootCandidates = [input.sourceRoot];
2290
- }
2291
- else {
2292
- // Auto-detect: include any root that contains at least one configured mixin class.
2293
- const detected = COMMON_SOURCE_ROOTS.filter((candidateRoot) => classNames.some((className) => {
2294
- const fqcn = pkg ? `${pkg}.${className}` : className;
2295
- const relative = fqcn.replace(/\./g, "/") + ".java";
2296
- return existsSync(resolvePath(projectBase, candidateRoot, relative));
2297
- }));
2298
- if (detected.length > 0) {
2299
- sourceRootCandidates = detected;
2300
- }
2301
- else {
2302
- sourceRootCandidates = ["src/main/java"];
2303
- }
2297
+ const { input: sourceInput, ...sharedInput } = input;
2298
+ const mode = sourceInput.mode;
2299
+ if (mode === "inline") {
2300
+ const singleResult = await this.validateMixinSingle({
2301
+ ...sharedInput,
2302
+ source: sourceInput.source
2303
+ });
2304
+ return this.buildValidateMixinOutput(mode, [
2305
+ {
2306
+ source: {
2307
+ kind: "inline",
2308
+ label: "<inline>"
2309
+ },
2310
+ result: singleResult
2304
2311
  }
2305
- // Build sourcePaths by probing each class against candidate roots
2306
- for (const cls of classNames) {
2307
- const fqcn = pkg ? `${pkg}.${cls}` : cls;
2308
- const relativePath = fqcn.replace(/\./g, "/") + ".java";
2309
- let found = false;
2310
- for (const root of sourceRootCandidates) {
2311
- const candidate = resolvePath(projectBase, root, relativePath);
2312
- if (existsSync(candidate)) {
2313
- allSourcePaths.push(candidate);
2314
- found = true;
2315
- break;
2316
- }
2317
- }
2318
- if (!found) {
2319
- // Fallback to first root for error reporting
2320
- allSourcePaths.push(resolvePath(projectBase, sourceRootCandidates[0], relativePath));
2321
- }
2312
+ ]);
2313
+ }
2314
+ if (mode === "path") {
2315
+ const resolvedPath = this.resolveMixinInputPath(sourceInput.path, "path");
2316
+ const singleResult = await this.validateMixinSingle({
2317
+ ...sharedInput,
2318
+ sourcePath: sourceInput.path
2319
+ });
2320
+ return this.buildValidateMixinOutput(mode, [
2321
+ {
2322
+ source: {
2323
+ kind: "path",
2324
+ label: resolvedPath,
2325
+ path: resolvedPath
2326
+ },
2327
+ result: singleResult
2322
2328
  }
2323
- }
2324
- if (allSourcePaths.length === 0) {
2325
- throw createError({
2326
- code: ERROR_CODES.INVALID_INPUT,
2327
- message: `Mixin config(s) contain no mixin class entries.`
2328
- });
2329
- }
2330
- return this.validateMixinBatch({ ...input, mixinConfigPath: undefined, sourcePaths: allSourcePaths });
2329
+ ]);
2331
2330
  }
2332
- // Batch mode: delegate to validateMixinBatch when sourcePaths is provided
2333
- if (input.sourcePaths && input.sourcePaths.length > 0) {
2334
- return this.validateMixinBatch(input);
2331
+ if (mode === "paths") {
2332
+ return this.validateMixinMany(mode, sourceInput.paths.map((path) => ({
2333
+ source: {
2334
+ kind: "path",
2335
+ label: this.resolveMixinInputPath(path, "path"),
2336
+ path: this.resolveMixinInputPath(path, "path")
2337
+ },
2338
+ sourcePath: path
2339
+ })), input);
2340
+ }
2341
+ const configSources = await this.resolveMixinConfigSources(input);
2342
+ if (configSources.length === 0) {
2343
+ throw createError({
2344
+ code: ERROR_CODES.INVALID_INPUT,
2345
+ message: "Mixin config(s) contain no mixin class entries."
2346
+ });
2335
2347
  }
2348
+ return this.validateMixinMany(mode, configSources.map((entry) => ({
2349
+ source: {
2350
+ kind: "config",
2351
+ label: entry.sourcePath,
2352
+ path: entry.sourcePath,
2353
+ configPath: entry.configPath
2354
+ },
2355
+ sourcePath: entry.sourcePath
2356
+ })), input);
2357
+ }
2358
+ async validateMixinSingle(input) {
2336
2359
  let version = input.version.trim();
2337
2360
  if (!version) {
2338
2361
  throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "version must be non-empty." });
@@ -2489,51 +2512,51 @@ export class SourceService {
2489
2512
  }
2490
2513
  }
2491
2514
  }
2492
- let officialName = resolvedClassName;
2493
- if (requestedMapping !== "official") {
2515
+ let obfuscatedName = resolvedClassName;
2516
+ if (requestedMapping !== "obfuscated") {
2494
2517
  try {
2495
2518
  const mapped = await this.mappingService.findMapping({
2496
2519
  version,
2497
2520
  kind: "class",
2498
2521
  name: resolvedClassName,
2499
2522
  sourceMapping: requestedMapping,
2500
- targetMapping: "official",
2523
+ targetMapping: "obfuscated",
2501
2524
  sourcePriority: input.sourcePriority
2502
2525
  });
2503
2526
  if (mapped.resolved && mapped.resolvedSymbol) {
2504
- officialName = mapped.resolvedSymbol.name;
2505
- resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: officialName, success: true });
2527
+ obfuscatedName = mapped.resolvedSymbol.name;
2528
+ resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: obfuscatedName, success: true });
2506
2529
  }
2507
2530
  else {
2508
- warnings.push(`Could not map class "${resolvedClassName}" from ${requestedMapping} to official; using "${officialName}" for lookup.`);
2531
+ warnings.push(`Could not map class "${resolvedClassName}" from ${requestedMapping} to obfuscated; using "${obfuscatedName}" for lookup.`);
2509
2532
  mappingFailedTargets.add(target.className);
2510
- resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: officialName, success: false, detail: "No mapping found" });
2533
+ resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: obfuscatedName, success: false, detail: "No mapping found" });
2511
2534
  }
2512
2535
  }
2513
2536
  catch (mapErr) {
2514
- warnings.push(`Mapping lookup failed for class "${resolvedClassName}"; using "${officialName}" for lookup.`);
2537
+ warnings.push(`Mapping lookup failed for class "${resolvedClassName}"; using "${obfuscatedName}" for lookup.`);
2515
2538
  mappingFailedTargets.add(target.className);
2516
- resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: officialName, success: false, detail: mapErr instanceof Error ? mapErr.message : String(mapErr) });
2539
+ resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: obfuscatedName, success: false, detail: mapErr instanceof Error ? mapErr.message : String(mapErr) });
2517
2540
  }
2518
2541
  }
2519
2542
  try {
2520
2543
  const sig = await this.explorerService.getSignature({
2521
- fqn: officialName,
2544
+ fqn: obfuscatedName,
2522
2545
  jarPath,
2523
2546
  access: "all"
2524
2547
  });
2525
2548
  warnings.push(...sig.warnings);
2526
- resolutionTrace?.push({ target: target.className, step: "signature", input: officialName, output: `${sig.methods.length} methods, ${sig.fields.length} fields`, success: true });
2549
+ resolutionTrace?.push({ target: target.className, step: "signature", input: obfuscatedName, output: `${sig.methods.length} methods, ${sig.fields.length} fields`, success: true });
2527
2550
  // Bug 2 fix: remap signature members to requested mapping
2528
2551
  let constructors = sig.constructors;
2529
2552
  let methods = sig.methods;
2530
2553
  let fields = sig.fields;
2531
- if (requestedMapping !== "official") {
2554
+ if (requestedMapping !== "obfuscated") {
2532
2555
  try {
2533
2556
  const [ctorResult, methodResult, fieldResult] = await Promise.all([
2534
- this.remapSignatureMembers(sig.constructors, "method", version, requestedMapping, input.sourcePriority, warnings),
2535
- this.remapSignatureMembers(sig.methods, "method", version, requestedMapping, input.sourcePriority, warnings),
2536
- this.remapSignatureMembers(sig.fields, "field", version, requestedMapping, input.sourcePriority, warnings)
2557
+ this.remapSignatureMembers(sig.constructors, "method", version, "obfuscated", requestedMapping, input.sourcePriority, warnings),
2558
+ this.remapSignatureMembers(sig.methods, "method", version, "obfuscated", requestedMapping, input.sourcePriority, warnings),
2559
+ this.remapSignatureMembers(sig.fields, "field", version, "obfuscated", requestedMapping, input.sourcePriority, warnings)
2537
2560
  ]);
2538
2561
  constructors = ctorResult.members;
2539
2562
  methods = methodResult.members;
@@ -2555,9 +2578,9 @@ export class SourceService {
2555
2578
  }
2556
2579
  }
2557
2580
  catch (remapErr) {
2558
- warnings.push(`Member remapping failed for "${resolvedClassName}"; falling back to official names. Member names shown may be in official (obfuscated) namespace.`);
2559
- mappingApplied = "official";
2560
- resolutionTrace?.push({ target: target.className, step: "remap", input: resolvedClassName, output: "official fallback", success: false, detail: remapErr instanceof Error ? remapErr.message : String(remapErr) });
2581
+ warnings.push(`Member remapping failed for "${resolvedClassName}"; falling back to obfuscated names. Member names shown may be in the obfuscated runtime namespace.`);
2582
+ mappingApplied = "obfuscated";
2583
+ resolutionTrace?.push({ target: target.className, step: "remap", input: resolvedClassName, output: "obfuscated fallback", success: false, detail: remapErr instanceof Error ? remapErr.message : String(remapErr) });
2561
2584
  }
2562
2585
  }
2563
2586
  targetMembers.set(target.className, {
@@ -2568,9 +2591,9 @@ export class SourceService {
2568
2591
  });
2569
2592
  }
2570
2593
  catch (sigErr) {
2571
- warnings.push(`Could not load signature for class "${resolvedClassName}" (official: "${officialName}").`);
2594
+ warnings.push(`Could not load signature for class "${resolvedClassName}" (obfuscated: "${obfuscatedName}").`);
2572
2595
  signatureFailedTargets.add(target.className);
2573
- resolutionTrace?.push({ target: target.className, step: "signature", input: officialName, output: "CLASS_NOT_FOUND", success: false, detail: sigErr instanceof Error ? sigErr.message : String(sigErr) });
2596
+ resolutionTrace?.push({ target: target.className, step: "signature", input: obfuscatedName, output: "CLASS_NOT_FOUND", success: false, detail: sigErr instanceof Error ? sigErr.message : String(sigErr) });
2574
2597
  // Fallback: check if the symbol exists in the mapping graph even though getSignature failed
2575
2598
  try {
2576
2599
  const existenceCheck = await this.mappingService.checkSymbolExists({
@@ -2617,8 +2640,8 @@ export class SourceService {
2617
2640
  }
2618
2641
  // Build mapping chain description
2619
2642
  const mappingChain = [];
2620
- if (requestedMapping !== "official") {
2621
- mappingChain.push(`${requestedMapping} → official`);
2643
+ if (requestedMapping !== "obfuscated") {
2644
+ mappingChain.push(`${requestedMapping} → obfuscated`);
2622
2645
  if (mappingApplied !== requestedMapping) {
2623
2646
  mappingChain.push(`fallback to ${mappingApplied}`);
2624
2647
  }
@@ -2713,45 +2736,123 @@ export class SourceService {
2713
2736
  }
2714
2737
  return result;
2715
2738
  }
2716
- async validateMixinBatch(input) {
2717
- const paths = input.sourcePaths;
2739
+ resolveMixinInputPath(rawPath, fieldName) {
2740
+ const normalizedPath = normalizePathForHost(rawPath, undefined, fieldName);
2741
+ return isAbsolute(normalizedPath)
2742
+ ? normalizedPath
2743
+ : resolvePath(process.cwd(), normalizedPath);
2744
+ }
2745
+ async resolveMixinConfigSources(input) {
2746
+ if (input.input.mode !== "config") {
2747
+ return [];
2748
+ }
2749
+ const results = [];
2750
+ for (const rawConfigPath of input.input.configPaths) {
2751
+ const resolvedConfigPath = this.resolveMixinInputPath(rawConfigPath, "configPath");
2752
+ let configJson;
2753
+ try {
2754
+ const raw = await readFile(resolvedConfigPath, "utf-8");
2755
+ configJson = JSON.parse(raw);
2756
+ }
2757
+ catch (err) {
2758
+ throw createError({
2759
+ code: ERROR_CODES.INVALID_INPUT,
2760
+ message: `Could not read/parse mixin config "${rawConfigPath}": ${err instanceof Error ? err.message : String(err)}`
2761
+ });
2762
+ }
2763
+ const pkg = configJson.package ?? "";
2764
+ const classNames = [
2765
+ ...(configJson.mixins ?? []),
2766
+ ...(configJson.client ?? []),
2767
+ ...(configJson.server ?? [])
2768
+ ];
2769
+ if (classNames.length === 0) {
2770
+ continue;
2771
+ }
2772
+ const projectBase = input.projectPath
2773
+ ? (isAbsolute(input.projectPath) ? input.projectPath : resolvePath(process.cwd(), input.projectPath))
2774
+ : dirname(resolvedConfigPath);
2775
+ let sourceRootCandidates;
2776
+ if (input.sourceRoots && input.sourceRoots.length > 0) {
2777
+ sourceRootCandidates = input.sourceRoots;
2778
+ }
2779
+ else {
2780
+ const detected = COMMON_SOURCE_ROOTS.filter((candidateRoot) => classNames.some((className) => {
2781
+ const fqcn = pkg ? `${pkg}.${className}` : className;
2782
+ const relative = fqcn.replace(/\./g, "/") + ".java";
2783
+ return existsSync(resolvePath(projectBase, candidateRoot, relative));
2784
+ }));
2785
+ sourceRootCandidates = detected.length > 0 ? detected : ["src/main/java"];
2786
+ }
2787
+ for (const cls of classNames) {
2788
+ const fqcn = pkg ? `${pkg}.${cls}` : cls;
2789
+ const relativePath = fqcn.replace(/\./g, "/") + ".java";
2790
+ let sourcePath = resolvePath(projectBase, sourceRootCandidates[0], relativePath);
2791
+ for (const root of sourceRootCandidates) {
2792
+ const candidate = resolvePath(projectBase, root, relativePath);
2793
+ if (existsSync(candidate)) {
2794
+ sourcePath = candidate;
2795
+ break;
2796
+ }
2797
+ }
2798
+ results.push({
2799
+ sourcePath,
2800
+ configPath: resolvedConfigPath
2801
+ });
2802
+ }
2803
+ }
2804
+ return results;
2805
+ }
2806
+ async validateMixinMany(mode, entries, input) {
2718
2807
  const results = [];
2719
- let validCount = 0;
2720
- let invalidCount = 0;
2721
- let errorCount = 0;
2722
- // P5: default warningMode to "aggregated" in batch mode
2723
2808
  const batchWarningMode = input.warningMode ?? "aggregated";
2724
- for (const sp of paths) {
2809
+ const { input: _discardedInput, ...sharedInput } = input;
2810
+ for (const entry of entries) {
2725
2811
  try {
2726
- const singleResult = await this.validateMixin({
2727
- ...input,
2728
- sourcePaths: undefined,
2729
- source: undefined,
2730
- sourcePath: sp,
2812
+ const singleResult = await this.validateMixinSingle({
2813
+ ...sharedInput,
2814
+ sourcePath: entry.sourcePath,
2731
2815
  warningMode: batchWarningMode
2732
2816
  });
2733
- results.push({ sourcePath: sp, result: singleResult });
2734
- if (singleResult.valid) {
2735
- validCount++;
2736
- }
2737
- else {
2738
- invalidCount++;
2739
- }
2817
+ results.push({
2818
+ source: entry.source,
2819
+ result: singleResult
2820
+ });
2740
2821
  }
2741
2822
  catch (err) {
2742
2823
  results.push({
2743
- sourcePath: sp,
2824
+ source: entry.source,
2744
2825
  error: err instanceof Error ? err.message : String(err)
2745
2826
  });
2746
- errorCount++;
2747
2827
  }
2748
2828
  }
2749
- // Build issueSummary: aggregate issues across all results by (kind, confidence, category)
2829
+ return this.buildValidateMixinOutput(mode, results);
2830
+ }
2831
+ buildValidateMixinOutput(mode, results) {
2832
+ let valid = 0;
2833
+ let invalid = 0;
2834
+ let processingErrors = 0;
2835
+ let totalValidationErrors = 0;
2836
+ let totalValidationWarnings = 0;
2837
+ const warningSet = new Set();
2750
2838
  const issueGroupMap = new Map();
2751
- for (const r of results) {
2752
- if (!r.result)
2839
+ for (const entry of results) {
2840
+ if (!entry.result) {
2841
+ processingErrors++;
2753
2842
  continue;
2754
- for (const issue of r.result.issues) {
2843
+ }
2844
+ if (entry.result.valid) {
2845
+ valid++;
2846
+ }
2847
+ else {
2848
+ invalid++;
2849
+ }
2850
+ totalValidationErrors += entry.result.summary.errors;
2851
+ totalValidationWarnings += entry.result.summary.warnings;
2852
+ for (const warning of entry.result.warnings) {
2853
+ warningSet.add(warning);
2854
+ }
2855
+ for (const issue of entry.result.issues) {
2755
2856
  const key = `${issue.kind}\0${issue.confidence ?? "unknown"}\0${issue.category ?? "validation"}`;
2756
2857
  const existing = issueGroupMap.get(key);
2757
2858
  if (existing) {
@@ -2771,39 +2872,26 @@ export class SourceService {
2771
2872
  }
2772
2873
  }
2773
2874
  }
2774
- const issueSummary = issueGroupMap.size > 0
2775
- ? [...issueGroupMap.values()]
2776
- : undefined;
2777
- // Aggregate validation-level errors/warnings across all results
2778
- let totalValidationErrors = 0;
2779
- let totalValidationWarnings = 0;
2780
- for (const r of results) {
2781
- if (r.result) {
2782
- totalValidationErrors += r.result.summary.errors;
2783
- totalValidationWarnings += r.result.summary.warnings;
2784
- }
2785
- }
2786
- // Extract shared toolHealth from first result (all share same version/mapping)
2787
- const sharedHealth = results.find((r) => r.result?.toolHealth)?.result?.toolHealth;
2788
- // Batch confidenceScore = min of all individual scores
2789
- const scores = results
2790
- .map((r) => r.result?.confidenceScore)
2791
- .filter((s) => s != null);
2792
- const batchConfidenceScore = scores.length > 0 ? Math.min(...scores) : undefined;
2875
+ const issueSummary = issueGroupMap.size > 0 ? [...issueGroupMap.values()] : undefined;
2876
+ const toolHealth = results.find((entry) => entry.result?.toolHealth)?.result?.toolHealth;
2877
+ const confidenceScores = results
2878
+ .map((entry) => entry.result?.confidenceScore)
2879
+ .filter((score) => score != null);
2793
2880
  return {
2881
+ mode,
2794
2882
  results,
2795
2883
  summary: {
2796
- total: paths.length,
2797
- valid: validCount,
2798
- invalid: invalidCount,
2799
- errors: errorCount,
2800
- processingErrors: errorCount,
2884
+ total: results.length,
2885
+ valid,
2886
+ invalid,
2887
+ processingErrors,
2801
2888
  totalValidationErrors,
2802
- totalValidationWarnings,
2803
- confidenceScore: batchConfidenceScore
2889
+ totalValidationWarnings
2804
2890
  },
2805
2891
  issueSummary,
2806
- toolHealth: sharedHealth
2892
+ toolHealth,
2893
+ confidenceScore: confidenceScores.length > 0 ? Math.min(...confidenceScores) : undefined,
2894
+ warnings: [...warningSet]
2807
2895
  };
2808
2896
  }
2809
2897
  async validateAccessWidener(input) {
@@ -2828,7 +2916,7 @@ export class SourceService {
2828
2916
  if (overrideMapping && headerNamespace && overrideMapping !== headerNamespace) {
2829
2917
  warnings.push(`Using mapping override "${overrideMapping}" instead of header namespace "${headerNamespaceRaw}".`);
2830
2918
  }
2831
- const needsMapping = awNamespace !== "official";
2919
+ const needsMapping = awNamespace !== "obfuscated";
2832
2920
  // Collect unique class FQNs from entries
2833
2921
  const classFqns = new Set();
2834
2922
  for (const entry of parsed.entries) {
@@ -2837,7 +2925,7 @@ export class SourceService {
2837
2925
  }
2838
2926
  const membersByClass = new Map();
2839
2927
  for (const fqn of classFqns) {
2840
- let officialFqn = fqn;
2928
+ let obfuscatedFqn = fqn;
2841
2929
  if (needsMapping) {
2842
2930
  try {
2843
2931
  const mapped = await this.mappingService.findMapping({
@@ -2845,14 +2933,14 @@ export class SourceService {
2845
2933
  kind: "class",
2846
2934
  name: fqn,
2847
2935
  sourceMapping: awNamespace,
2848
- targetMapping: "official",
2936
+ targetMapping: "obfuscated",
2849
2937
  sourcePriority: input.sourcePriority
2850
2938
  });
2851
2939
  if (mapped.resolved && mapped.resolvedSymbol) {
2852
- officialFqn = mapped.resolvedSymbol.name;
2940
+ obfuscatedFqn = mapped.resolvedSymbol.name;
2853
2941
  }
2854
2942
  else {
2855
- warnings.push(`Could not map class "${fqn}" from ${awNamespace} to official.`);
2943
+ warnings.push(`Could not map class "${fqn}" from ${awNamespace} to obfuscated.`);
2856
2944
  }
2857
2945
  }
2858
2946
  catch {
@@ -2861,7 +2949,7 @@ export class SourceService {
2861
2949
  }
2862
2950
  try {
2863
2951
  const sig = await this.explorerService.getSignature({
2864
- fqn: officialFqn,
2952
+ fqn: obfuscatedFqn,
2865
2953
  jarPath,
2866
2954
  access: "all"
2867
2955
  });
@@ -2874,7 +2962,7 @@ export class SourceService {
2874
2962
  });
2875
2963
  }
2876
2964
  catch {
2877
- warnings.push(`Could not load signature for class "${officialFqn}".`);
2965
+ warnings.push(`Could not load signature for class "${obfuscatedFqn}".`);
2878
2966
  }
2879
2967
  }
2880
2968
  return validateParsedAccessWidener(parsed, membersByClass, warnings);
@@ -2915,7 +3003,7 @@ export class SourceService {
2915
3003
  },
2916
3004
  indexedAt: currentMeta.indexedAt,
2917
3005
  durationMs: 0,
2918
- mappingApplied: artifact.mappingApplied ?? "official"
3006
+ mappingApplied: artifact.mappingApplied ?? "obfuscated"
2919
3007
  };
2920
3008
  }
2921
3009
  const resolved = this.toResolvedArtifact(artifact);
@@ -2932,33 +3020,16 @@ export class SourceService {
2932
3020
  },
2933
3021
  indexedAt: rebuilt.indexedAt,
2934
3022
  durationMs: rebuilt.indexDurationMs,
2935
- mappingApplied: artifact.mappingApplied ?? "official"
3023
+ mappingApplied: artifact.mappingApplied ?? "obfuscated"
2936
3024
  };
2937
3025
  }
2938
- searchSymbolIntent(artifactId, query, match, scope, snippetWindow, regexPattern, onHit) {
3026
+ searchSymbolIntent(artifactId, query, match, scope, regexPattern, onHit) {
2939
3027
  const matchedSymbols = this.findSymbolHits(artifactId, query, match, scope, regexPattern);
2940
- const filePaths = [...new Set(matchedSymbols.map((item) => item.symbol.filePath))];
2941
- const rows = this.filesRepo.getFileContentsByPaths(artifactId, filePaths);
2942
- this.metrics.recordSearchDbRoundtrip();
2943
- this.metrics.recordSearchRowsScanned(rows.length);
2944
- const rowsByPath = new Map(rows.map((row) => [row.filePath, row]));
2945
3028
  for (const item of matchedSymbols) {
2946
- const row = rowsByPath.get(item.symbol.filePath);
2947
- const snippet = row
2948
- ? toContextSnippet(row.content, item.symbol.line, snippetWindow.before, snippetWindow.after, true)
2949
- : {
2950
- startLine: item.symbol.line,
2951
- endLine: item.symbol.line,
2952
- snippet: "",
2953
- truncated: false
2954
- };
2955
3029
  onHit({
2956
3030
  filePath: item.symbol.filePath,
2957
3031
  score: item.score,
2958
3032
  matchedIn: "symbol",
2959
- startLine: snippet.startLine,
2960
- endLine: snippet.endLine,
2961
- snippet: snippet.snippet,
2962
3033
  reasonCodes: [`symbol_${match}`],
2963
3034
  symbol: {
2964
3035
  symbolKind: item.symbol.symbolKind,
@@ -2969,7 +3040,7 @@ export class SourceService {
2969
3040
  });
2970
3041
  }
2971
3042
  }
2972
- searchTextIntentIndexed(artifactId, query, match, scope, includeDefinition, snippetWindow, onHit) {
3043
+ searchTextIntentIndexed(artifactId, query, match, scope, onHit) {
2973
3044
  const candidateLimit = this.indexedCandidateLimitForMatch(match);
2974
3045
  const indexed = this.filesRepo.searchFileCandidates(artifactId, {
2975
3046
  query,
@@ -2998,45 +3069,21 @@ export class SourceService {
2998
3069
  if (contentIndex < 0) {
2999
3070
  continue;
3000
3071
  }
3001
- const line = indexToLine(candidate.content, contentIndex);
3002
3072
  candidateRows.push({
3003
3073
  filePath: candidate.filePath,
3004
- content: candidate.content,
3005
- line,
3006
3074
  contentIndex
3007
3075
  });
3008
3076
  }
3009
- const needSymbols = includeDefinition || !!scope?.symbolKind;
3010
- const symbolsByFile = needSymbols
3011
- ? this.symbolsRepo.listSymbolsForFiles(artifactId, candidateRows.map((candidate) => candidate.filePath), scope?.symbolKind)
3012
- : new Map();
3013
- if (needSymbols) {
3014
- this.metrics.recordSearchDbRoundtrip();
3015
- this.metrics.recordSearchRowsScanned([...symbolsByFile.values()].reduce((acc, symbols) => acc + symbols.length, 0));
3016
- }
3017
3077
  for (const candidate of candidateRows) {
3018
- // When symbolKind filter is set, skip files that have no symbols of that kind
3019
- if (scope?.symbolKind && !symbolsByFile.has(candidate.filePath)) {
3020
- continue;
3021
- }
3022
- const snippet = toContextSnippet(candidate.content, candidate.line, snippetWindow.before, snippetWindow.after, true);
3023
- const definition = includeDefinition
3024
- ? this.findNearestSymbolFromList(symbolsByFile.get(candidate.filePath) ?? [], candidate.line)
3025
- : undefined;
3026
- const resolvedSymbol = definition ? lineToSymbol(definition) : undefined;
3027
3078
  onHit({
3028
3079
  filePath: candidate.filePath,
3029
- score: scoreTextMatch(match, candidate.contentIndex) + (resolvedSymbol ? 20 : 0),
3080
+ score: scoreTextMatch(match, candidate.contentIndex),
3030
3081
  matchedIn: "content",
3031
- startLine: snippet.startLine,
3032
- endLine: snippet.endLine,
3033
- snippet: snippet.snippet,
3034
- reasonCodes: ["content_match", `text_${match}`, "indexed"],
3035
- symbol: resolvedSymbol
3082
+ reasonCodes: ["content_match", `text_${match}`, "indexed"]
3036
3083
  });
3037
3084
  }
3038
3085
  }
3039
- searchPathIntentIndexed(artifactId, query, match, scope, includeDefinition, snippetWindow, onHit) {
3086
+ searchPathIntentIndexed(artifactId, query, match, scope, onHit) {
3040
3087
  const candidateLimit = this.indexedCandidateLimitForMatch(match);
3041
3088
  const indexed = this.filesRepo.searchFileCandidates(artifactId, {
3042
3089
  query,
@@ -3071,59 +3118,22 @@ export class SourceService {
3071
3118
  pathIndex
3072
3119
  });
3073
3120
  }
3074
- const candidateContentRows = this.filesRepo.getFileContentsByPaths(artifactId, candidateRows.map((candidate) => candidate.filePath));
3075
- this.metrics.recordSearchDbRoundtrip();
3076
- this.metrics.recordSearchRowsScanned(candidateContentRows.length);
3077
- const contentByPath = new Map(candidateContentRows.map((row) => [row.filePath, row.content]));
3078
- const needSymbols = includeDefinition || !!scope?.symbolKind;
3079
- const symbolsByFile = needSymbols
3080
- ? this.symbolsRepo.listSymbolsForFiles(artifactId, candidateRows.map((candidate) => candidate.filePath), scope?.symbolKind)
3081
- : new Map();
3082
- if (needSymbols) {
3083
- this.metrics.recordSearchDbRoundtrip();
3084
- this.metrics.recordSearchRowsScanned([...symbolsByFile.values()].reduce((acc, symbols) => acc + symbols.length, 0));
3085
- }
3086
3121
  for (const candidate of candidateRows) {
3087
- const content = contentByPath.get(candidate.filePath);
3088
- if (!content) {
3089
- continue;
3090
- }
3091
- // When symbolKind filter is set, skip files that have no symbols of that kind
3092
- if (scope?.symbolKind && !symbolsByFile.has(candidate.filePath)) {
3093
- continue;
3094
- }
3095
- const definition = includeDefinition
3096
- ? this.findNearestSymbolFromList(symbolsByFile.get(candidate.filePath) ?? [], 1)
3097
- : undefined;
3098
- const centerLine = definition?.line ?? 1;
3099
- const snippet = toContextSnippet(content, centerLine, snippetWindow.before, snippetWindow.after, true);
3100
- const resolvedSymbol = definition ? lineToSymbol(definition) : undefined;
3101
3122
  onHit({
3102
3123
  filePath: candidate.filePath,
3103
- score: scorePathMatch(match, candidate.pathIndex) + (resolvedSymbol ? 10 : 0),
3124
+ score: scorePathMatch(match, candidate.pathIndex),
3104
3125
  matchedIn: "path",
3105
- startLine: snippet.startLine,
3106
- endLine: snippet.endLine,
3107
- snippet: snippet.snippet,
3108
- reasonCodes: ["path_match", `path_${match}`, "indexed"],
3109
- symbol: resolvedSymbol
3126
+ reasonCodes: ["path_match", `path_${match}`, "indexed"]
3110
3127
  });
3111
3128
  }
3112
3129
  }
3113
- searchTextIntent(artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, onHit) {
3130
+ searchTextIntent(artifactId, query, match, scope, regexPattern, onHit) {
3114
3131
  const filePaths = this.loadScopedFilePaths(artifactId, scope);
3115
3132
  const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
3116
3133
  for (const chunk of chunkArray(filePaths, pageSize)) {
3117
3134
  const rows = this.filesRepo.getFileContentsByPaths(artifactId, chunk);
3118
3135
  this.metrics.recordSearchDbRoundtrip();
3119
3136
  this.metrics.recordSearchRowsScanned(rows.length);
3120
- const symbolsByFile = includeDefinition
3121
- ? this.symbolsRepo.listSymbolsForFiles(artifactId, rows.map((row) => row.filePath), scope?.symbolKind)
3122
- : new Map();
3123
- if (includeDefinition) {
3124
- this.metrics.recordSearchDbRoundtrip();
3125
- this.metrics.recordSearchRowsScanned([...symbolsByFile.values()].reduce((acc, symbols) => acc + symbols.length, 0));
3126
- }
3127
3137
  for (const row of rows) {
3128
3138
  const contentIndex = match === "regex"
3129
3139
  ? matchRegexIndex(row.content, regexPattern)
@@ -3131,26 +3141,16 @@ export class SourceService {
3131
3141
  if (contentIndex < 0) {
3132
3142
  continue;
3133
3143
  }
3134
- const line = indexToLine(row.content, contentIndex);
3135
- const snippet = toContextSnippet(row.content, line, snippetWindow.before, snippetWindow.after, true);
3136
- const definition = includeDefinition
3137
- ? this.findNearestSymbolFromList(symbolsByFile.get(row.filePath) ?? [], line)
3138
- : undefined;
3139
- const resolvedSymbol = definition ? lineToSymbol(definition) : undefined;
3140
3144
  onHit({
3141
3145
  filePath: row.filePath,
3142
- score: scoreTextMatch(match, contentIndex) + (resolvedSymbol ? 20 : 0),
3146
+ score: scoreTextMatch(match, contentIndex),
3143
3147
  matchedIn: "content",
3144
- startLine: snippet.startLine,
3145
- endLine: snippet.endLine,
3146
- snippet: snippet.snippet,
3147
- reasonCodes: ["content_match", `text_${match}`],
3148
- symbol: resolvedSymbol
3148
+ reasonCodes: ["content_match", `text_${match}`]
3149
3149
  });
3150
3150
  }
3151
3151
  }
3152
3152
  }
3153
- searchPathIntent(artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, onHit) {
3153
+ searchPathIntent(artifactId, query, match, scope, regexPattern, onHit) {
3154
3154
  const filePaths = this.loadScopedFilePaths(artifactId, scope);
3155
3155
  const matching = filePaths.flatMap((filePath) => {
3156
3156
  const pathIndex = match === "regex"
@@ -3163,37 +3163,12 @@ export class SourceService {
3163
3163
  });
3164
3164
  const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
3165
3165
  for (const chunk of chunkArray(matching, pageSize)) {
3166
- const rows = this.filesRepo.getFileContentsByPaths(artifactId, chunk.map((item) => item.filePath));
3167
- this.metrics.recordSearchDbRoundtrip();
3168
- this.metrics.recordSearchRowsScanned(rows.length);
3169
- const contentByPath = new Map(rows.map((row) => [row.filePath, row.content]));
3170
- const symbolsByFile = includeDefinition
3171
- ? this.symbolsRepo.listSymbolsForFiles(artifactId, chunk.map((item) => item.filePath), scope?.symbolKind)
3172
- : new Map();
3173
- if (includeDefinition) {
3174
- this.metrics.recordSearchDbRoundtrip();
3175
- this.metrics.recordSearchRowsScanned([...symbolsByFile.values()].reduce((acc, symbols) => acc + symbols.length, 0));
3176
- }
3177
3166
  for (const candidate of chunk) {
3178
- const content = contentByPath.get(candidate.filePath);
3179
- if (!content) {
3180
- continue;
3181
- }
3182
- const definition = includeDefinition
3183
- ? this.findNearestSymbolFromList(symbolsByFile.get(candidate.filePath) ?? [], 1)
3184
- : undefined;
3185
- const centerLine = definition?.line ?? 1;
3186
- const snippet = toContextSnippet(content, centerLine, snippetWindow.before, snippetWindow.after, true);
3187
- const resolvedSymbol = definition ? lineToSymbol(definition) : undefined;
3188
3167
  onHit({
3189
3168
  filePath: candidate.filePath,
3190
- score: scorePathMatch(match, candidate.pathIndex) + (resolvedSymbol ? 10 : 0),
3169
+ score: scorePathMatch(match, candidate.pathIndex),
3191
3170
  matchedIn: "path",
3192
- startLine: snippet.startLine,
3193
- endLine: snippet.endLine,
3194
- snippet: snippet.snippet,
3195
- reasonCodes: ["path_match", `path_${match}`],
3196
- symbol: resolvedSymbol
3171
+ reasonCodes: ["path_match", `path_${match}`]
3197
3172
  });
3198
3173
  }
3199
3174
  }
@@ -3262,13 +3237,6 @@ export class SourceService {
3262
3237
  }
3263
3238
  loadScopedFilePaths(artifactId, scope) {
3264
3239
  const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
3265
- const scopedFilesBySymbolKind = scope?.symbolKind
3266
- ? new Set(this.symbolsRepo.listDistinctFilePathsByKind(artifactId, scope.symbolKind))
3267
- : undefined;
3268
- if (scopedFilesBySymbolKind) {
3269
- this.metrics.recordSearchDbRoundtrip();
3270
- this.metrics.recordSearchRowsScanned(scopedFilesBySymbolKind.size);
3271
- }
3272
3240
  const result = [];
3273
3241
  let cursor = undefined;
3274
3242
  const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
@@ -3283,9 +3251,6 @@ export class SourceService {
3283
3251
  if (glob && !glob.test(filePath)) {
3284
3252
  continue;
3285
3253
  }
3286
- if (scopedFilesBySymbolKind && !scopedFilesBySymbolKind.has(filePath)) {
3287
- continue;
3288
- }
3289
3254
  result.push(filePath);
3290
3255
  }
3291
3256
  if (!page.nextCursor) {
@@ -3365,141 +3330,27 @@ export class SourceService {
3365
3330
  }
3366
3331
  return undefined;
3367
3332
  }
3368
- findNearestSymbolForLine(artifactId, filePath, line, symbolKind) {
3369
- const symbols = this.symbolsRepo
3370
- .listSymbolsForFile(artifactId, filePath)
3371
- .filter((symbol) => (symbolKind ? symbol.symbolKind === symbolKind : true));
3372
- return this.findNearestSymbolFromList(symbols, line);
3373
- }
3374
- findNearestSymbolFromList(symbols, line) {
3375
- let best;
3376
- for (const symbol of symbols) {
3377
- if (symbol.line > line) {
3378
- continue;
3379
- }
3380
- if (!best || symbol.line >= best.line) {
3381
- best = symbol;
3382
- }
3383
- }
3384
- return best ?? symbols[0];
3385
- }
3386
- buildOneHopRelations(artifactId, roots, maxRelations) {
3387
- if (roots.length === 0 || maxRelations <= 0) {
3388
- return [];
3333
+ async resolveBinaryFallbackArtifact(input) {
3334
+ const binaryJarPath = normalizeOptionalString(input.binaryJarPath);
3335
+ if (!binaryJarPath) {
3336
+ return undefined;
3389
3337
  }
3390
- const rootRows = this.filesRepo.getFileContentsByPaths(artifactId, roots.map((root) => root.filePath));
3391
- this.metrics.recordSearchDbRoundtrip();
3392
- this.metrics.recordSearchRowsScanned(rootRows.length);
3393
- const rootRowsByPath = new Map(rootRows.map((row) => [row.filePath, row]));
3394
- const rootTokens = roots.map((root) => {
3395
- const contentRow = rootRowsByPath.get(root.filePath);
3396
- if (!contentRow) {
3397
- return {
3398
- root,
3399
- calls: [],
3400
- types: [],
3401
- imports: []
3402
- };
3403
- }
3404
- const aroundRoot = toContextSnippet(contentRow.content, root.line, 2, 3, false).snippet;
3405
- return {
3406
- root,
3407
- calls: Array.from(aroundRoot.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g))
3408
- .map((match) => match[1])
3409
- .filter((token) => Boolean(token)),
3410
- types: Array.from(aroundRoot.matchAll(/\b([A-Z][A-Za-z0-9_$]*)\b/g))
3411
- .map((match) => match[1])
3412
- .filter((token) => Boolean(token)),
3413
- imports: Array.from(aroundRoot.matchAll(/import\s+([\w.$]+);/g))
3414
- .map((match) => match[1]?.split(".").at(-1))
3415
- .filter((token) => Boolean(token))
3416
- };
3417
- });
3418
- const tokenSet = new Set();
3419
- for (const entry of rootTokens) {
3420
- for (const token of entry.calls) {
3421
- tokenSet.add(toLower(token));
3422
- }
3423
- for (const token of entry.types) {
3424
- tokenSet.add(toLower(token));
3425
- }
3426
- for (const token of entry.imports) {
3427
- tokenSet.add(toLower(token));
3428
- }
3338
+ try {
3339
+ const fallbackResolved = await resolveSourceTargetInternal({ kind: "jar", value: binaryJarPath }, { allowDecompile: true, preferBinaryOnly: true }, this.config);
3340
+ fallbackResolved.version = fallbackResolved.version ?? input.version;
3341
+ fallbackResolved.coordinate = fallbackResolved.coordinate ?? input.coordinate;
3342
+ fallbackResolved.requestedMapping = input.requestedMapping;
3343
+ fallbackResolved.mappingApplied = input.mappingApplied;
3344
+ fallbackResolved.provenance = input.provenance;
3345
+ fallbackResolved.qualityFlags = [
3346
+ ...new Set([...(fallbackResolved.qualityFlags ?? []), ...input.qualityFlags, "binary-fallback"])
3347
+ ];
3348
+ await this.ingestIfNeeded(fallbackResolved);
3349
+ return fallbackResolved;
3429
3350
  }
3430
- const matchedSymbols = this.symbolsRepo
3431
- .findBySymbolNames(artifactId, [...tokenSet])
3432
- .filter((symbol) => isSymbolKind(symbol.symbolKind));
3433
- this.metrics.recordSearchDbRoundtrip();
3434
- this.metrics.recordSearchRowsScanned(matchedSymbols.length);
3435
- const symbolMap = new Map();
3436
- for (const symbol of matchedSymbols) {
3437
- const key = toLower(symbol.symbolName);
3438
- const bucket = symbolMap.get(key) ?? [];
3439
- bucket.push(symbol);
3440
- symbolMap.set(key, bucket);
3441
- }
3442
- const dedupe = new Set();
3443
- const relations = [];
3444
- for (const entry of rootTokens) {
3445
- const root = entry.root;
3446
- const attach = (token, relationKind) => {
3447
- const matches = symbolMap.get(toLower(token)) ?? [];
3448
- for (const target of matches) {
3449
- if (!isSymbolKind(target.symbolKind)) {
3450
- continue;
3451
- }
3452
- if (target.filePath === root.filePath &&
3453
- target.line === root.line &&
3454
- target.symbolName === root.symbolName &&
3455
- target.symbolKind === root.symbolKind) {
3456
- continue;
3457
- }
3458
- const key = `${root.symbolKind}:${root.symbolName}:${root.filePath}:${root.line}->${target.symbolKind}:${target.symbolName}:${target.filePath}:${target.line}:${relationKind}`;
3459
- if (dedupe.has(key)) {
3460
- continue;
3461
- }
3462
- dedupe.add(key);
3463
- relations.push({
3464
- fromSymbol: {
3465
- symbolKind: root.symbolKind,
3466
- symbolName: root.symbolName,
3467
- filePath: root.filePath,
3468
- line: root.line
3469
- },
3470
- toSymbol: {
3471
- symbolKind: target.symbolKind,
3472
- symbolName: target.symbolName,
3473
- filePath: target.filePath,
3474
- line: target.line
3475
- },
3476
- relation: relationKind
3477
- });
3478
- if (relations.length >= maxRelations) {
3479
- return;
3480
- }
3481
- }
3482
- };
3483
- for (const token of entry.calls) {
3484
- attach(token, "calls");
3485
- if (relations.length >= maxRelations) {
3486
- return relations;
3487
- }
3488
- }
3489
- for (const token of entry.types) {
3490
- attach(token, "uses-type");
3491
- if (relations.length >= maxRelations) {
3492
- return relations;
3493
- }
3494
- }
3495
- for (const token of entry.imports) {
3496
- attach(token, "imports");
3497
- if (relations.length >= maxRelations) {
3498
- return relations;
3499
- }
3500
- }
3351
+ catch {
3352
+ return undefined;
3501
3353
  }
3502
- return relations;
3503
3354
  }
3504
3355
  buildProvenance(input) {
3505
3356
  const provenance = {
@@ -3551,31 +3402,107 @@ export class SourceService {
3551
3402
  transformChain
3552
3403
  };
3553
3404
  }
3554
- async resolveToOfficialClassName(className, version, mapping, sourcePriority, warnings) {
3555
- if (mapping === "official") {
3556
- return className;
3405
+ async resolveClassNameForLookup(input) {
3406
+ if (input.sourceMapping === input.targetMapping) {
3407
+ return input.className;
3408
+ }
3409
+ if (!input.version) {
3410
+ input.warnings.push(`Could not map class "${input.className}" from ${input.sourceMapping} to ${input.targetMapping} for ${input.context} because version is unavailable.`);
3411
+ return input.className;
3557
3412
  }
3558
3413
  try {
3559
3414
  const mapped = await this.mappingService.findMapping({
3560
- version,
3415
+ version: input.version,
3561
3416
  kind: "class",
3562
- name: className,
3563
- sourceMapping: mapping,
3564
- targetMapping: "official",
3565
- sourcePriority
3417
+ name: input.className,
3418
+ sourceMapping: input.sourceMapping,
3419
+ targetMapping: input.targetMapping,
3420
+ sourcePriority: input.sourcePriority
3566
3421
  });
3567
3422
  if (mapped.resolved && mapped.resolvedSymbol) {
3568
3423
  return mapped.resolvedSymbol.name;
3569
3424
  }
3570
- warnings.push(`Could not map class "${className}" from ${mapping} to official.`);
3425
+ input.warnings.push(`Could not map class "${input.className}" from ${input.sourceMapping} to ${input.targetMapping} for ${input.context}.`);
3571
3426
  }
3572
3427
  catch {
3573
- warnings.push(`Mapping lookup failed for class "${className}".`);
3428
+ input.warnings.push(`Mapping lookup failed for class "${input.className}" while preparing ${input.context} in ${input.targetMapping}.`);
3429
+ }
3430
+ return input.className;
3431
+ }
3432
+ buildClassSourceNotFoundError(input) {
3433
+ const simpleName = input.className.split(/[.$]/).at(-1) ?? input.className;
3434
+ const details = {
3435
+ artifactId: input.artifactId,
3436
+ className: input.className,
3437
+ mapping: input.mappingApplied,
3438
+ qualityFlags: input.qualityFlags,
3439
+ ...(input.lookupClassName !== input.className ? { lookupClassName: input.lookupClassName } : {}),
3440
+ ...(input.filePath ? { filePath: input.filePath } : {}),
3441
+ ...(input.scope ? { scope: input.scope } : {}),
3442
+ ...(input.targetKind ? { targetKind: input.targetKind } : {}),
3443
+ ...(input.targetValue ? { targetValue: input.targetValue } : {}),
3444
+ ...(input.attemptedBinaryFallback ? { binaryFallbackAttempted: true } : {})
3445
+ };
3446
+ let nextAction = `Use find-class to resolve the correct fully-qualified name for "${simpleName}".`;
3447
+ let suggestedCall = {
3448
+ tool: "find-class",
3449
+ params: { className: simpleName, artifactId: input.artifactId }
3450
+ };
3451
+ if (input.targetKind === "version" && input.scope && input.scope !== "merged" && !input.projectPath) {
3452
+ nextAction +=
3453
+ ` If the class exists in a modded environment, retry with scope: "merged" and projectPath pointing to your mod project.`;
3454
+ }
3455
+ else if (input.targetKind === "version" && input.scope && input.scope !== "merged" && input.projectPath) {
3456
+ nextAction += ` The class may exist in merged sources; retry with scope: "merged".`;
3457
+ }
3458
+ if (hasPartialNetMinecraftCoverage(input.qualityFlags)) {
3459
+ nextAction =
3460
+ `Resolved source coverage does not include net.minecraft for "${input.className}",` +
3461
+ (input.attemptedBinaryFallback
3462
+ ? " and binary fallback did not produce source for that class."
3463
+ : " and a binary fallback has not produced source for that class.") +
3464
+ " Use get-class-api-matrix or find-mapping instead of find-class for vanilla API discovery.";
3465
+ if (input.version) {
3466
+ suggestedCall = {
3467
+ tool: "get-class-api-matrix",
3468
+ params: {
3469
+ version: input.version,
3470
+ className: input.className,
3471
+ classNameMapping: input.requestedMapping
3472
+ }
3473
+ };
3474
+ }
3475
+ else {
3476
+ suggestedCall = {
3477
+ tool: "find-class",
3478
+ params: { className: simpleName, artifactId: input.artifactId }
3479
+ };
3480
+ }
3481
+ }
3482
+ if (input.mappingApplied === "obfuscated" && looksLikeDeobfuscatedClassName(input.className)) {
3483
+ nextAction += ` ${obfuscatedNamespaceHint(input.className)}`;
3574
3484
  }
3575
- return className;
3485
+ details.nextAction = nextAction;
3486
+ details.suggestedCall = suggestedCall;
3487
+ return createError({
3488
+ code: ERROR_CODES.CLASS_NOT_FOUND,
3489
+ message: `Source for class "${input.className}" was not found.`,
3490
+ details
3491
+ });
3492
+ }
3493
+ async resolveToObfuscatedClassName(className, version, mapping, sourcePriority, warnings) {
3494
+ return this.resolveClassNameForLookup({
3495
+ className,
3496
+ version,
3497
+ sourceMapping: mapping,
3498
+ targetMapping: "obfuscated",
3499
+ sourcePriority,
3500
+ warnings,
3501
+ context: "bytecode lookup"
3502
+ });
3576
3503
  }
3577
- async resolveToOfficialMemberName(name, ownerInSourceMapping, descriptor, kind, version, mapping, sourcePriority, warnings) {
3578
- if (mapping === "official") {
3504
+ async resolveToObfuscatedMemberName(name, ownerInSourceMapping, descriptor, kind, version, mapping, sourcePriority, warnings) {
3505
+ if (mapping === "obfuscated") {
3579
3506
  return {
3580
3507
  name,
3581
3508
  descriptor: kind === "method" ? descriptor : undefined
@@ -3589,7 +3516,7 @@ export class SourceService {
3589
3516
  owner: ownerInSourceMapping,
3590
3517
  descriptor,
3591
3518
  sourceMapping: mapping,
3592
- targetMapping: "official",
3519
+ targetMapping: "obfuscated",
3593
3520
  sourcePriority
3594
3521
  });
3595
3522
  if (mapped.resolved && mapped.resolvedSymbol) {
@@ -3598,7 +3525,7 @@ export class SourceService {
3598
3525
  descriptor: kind === "method" ? mapped.resolvedSymbol.descriptor ?? descriptor : undefined
3599
3526
  };
3600
3527
  }
3601
- warnings.push(`Could not map ${kind} "${name}" from ${mapping} to official.`);
3528
+ warnings.push(`Could not map ${kind} "${name}" from ${mapping} to obfuscated.`);
3602
3529
  }
3603
3530
  catch {
3604
3531
  warnings.push(`Mapping lookup failed for ${kind} "${name}".`);
@@ -3608,9 +3535,9 @@ export class SourceService {
3608
3535
  descriptor: kind === "method" ? descriptor : undefined
3609
3536
  };
3610
3537
  }
3611
- async remapSignatureMembers(members, kind, version, mapping, sourcePriority, warnings) {
3538
+ async remapSignatureMembers(members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings) {
3612
3539
  const failedNames = new Set();
3613
- if (mapping === "official") {
3540
+ if (sourceMapping === targetMapping) {
3614
3541
  return { members, failedNames };
3615
3542
  }
3616
3543
  // Build deduplicated lookup tables for member names and owner FQNs
@@ -3619,35 +3546,35 @@ export class SourceService {
3619
3546
  for (const member of members) {
3620
3547
  const memberKey = `${member.ownerFqn}\0${member.name}\0${member.jvmDescriptor}`;
3621
3548
  if (!memberKeyToRemapped.has(memberKey)) {
3622
- memberKeyToRemapped.set(memberKey, member.name); // default = official name
3549
+ memberKeyToRemapped.set(memberKey, member.name); // default = obfuscated name
3623
3550
  }
3624
3551
  if (!ownerToRemapped.has(member.ownerFqn)) {
3625
- ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = official FQN
3552
+ ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = obfuscated FQN
3626
3553
  }
3627
3554
  }
3628
3555
  // Phase 1: Remap owner FQNs first (needed for member disambiguation)
3629
3556
  const ownerEntries = [...ownerToRemapped.entries()];
3630
- await Promise.all(ownerEntries.map(async ([officialFqn]) => {
3557
+ await Promise.all(ownerEntries.map(async ([obfuscatedFqn]) => {
3631
3558
  try {
3632
3559
  const mapped = await this.mappingService.findMapping({
3633
3560
  version,
3634
3561
  kind: "class",
3635
- name: officialFqn,
3636
- sourceMapping: "official",
3637
- targetMapping: mapping,
3562
+ name: obfuscatedFqn,
3563
+ sourceMapping,
3564
+ targetMapping,
3638
3565
  sourcePriority
3639
3566
  });
3640
3567
  if (mapped.resolved && mapped.resolvedSymbol) {
3641
- ownerToRemapped.set(officialFqn, mapped.resolvedSymbol.name);
3568
+ ownerToRemapped.set(obfuscatedFqn, mapped.resolvedSymbol.name);
3642
3569
  }
3643
3570
  }
3644
3571
  catch {
3645
- // keep official FQN as fallback
3572
+ // keep obfuscated FQN as fallback
3646
3573
  }
3647
3574
  }));
3648
3575
  // Phase 2: Remap member names using resolved owners for disambiguation
3649
3576
  const memberEntries = [...memberKeyToRemapped.entries()];
3650
- await Promise.all(memberEntries.map(async ([key, _officialName]) => {
3577
+ await Promise.all(memberEntries.map(async ([key, _obfuscatedName]) => {
3651
3578
  const [ownerFqn, name, descriptor] = key.split("\0");
3652
3579
  try {
3653
3580
  const targetOwner = ownerToRemapped.get(ownerFqn) ?? ownerFqn;
@@ -3657,8 +3584,8 @@ export class SourceService {
3657
3584
  name,
3658
3585
  owner: ownerFqn,
3659
3586
  descriptor: kind === "method" ? descriptor : undefined,
3660
- sourceMapping: "official",
3661
- targetMapping: mapping,
3587
+ sourceMapping,
3588
+ targetMapping,
3662
3589
  sourcePriority,
3663
3590
  disambiguation: { ownerHint: targetOwner }
3664
3591
  });
@@ -3677,17 +3604,17 @@ export class SourceService {
3677
3604
  }
3678
3605
  }
3679
3606
  else {
3680
- warnings.push(`Could not remap ${kind} "${name}" to ${mapping}.`);
3607
+ warnings.push(`Could not remap ${kind} "${name}" from ${sourceMapping} to ${targetMapping}.`);
3681
3608
  failedNames.add(name);
3682
3609
  }
3683
3610
  }
3684
3611
  else {
3685
- warnings.push(`Could not remap ${kind} "${name}" to ${mapping}.`);
3612
+ warnings.push(`Could not remap ${kind} "${name}" from ${sourceMapping} to ${targetMapping}.`);
3686
3613
  failedNames.add(name);
3687
3614
  }
3688
3615
  }
3689
3616
  catch {
3690
- warnings.push(`Remap failed for ${kind} "${name}".`);
3617
+ warnings.push(`Remap failed for ${kind} "${name}" from ${sourceMapping} to ${targetMapping}.`);
3691
3618
  failedNames.add(name);
3692
3619
  }
3693
3620
  }));