@adhisang/minecraft-modding-mcp 1.2.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +184 -64
  3. package/dist/cli.js +31 -4
  4. package/dist/compat-stdio-transport.d.ts +2 -7
  5. package/dist/compat-stdio-transport.js +12 -154
  6. package/dist/index.js +537 -202
  7. package/dist/json-rpc-framing.d.ts +22 -0
  8. package/dist/json-rpc-framing.js +168 -0
  9. package/dist/mapping-pipeline-service.d.ts +1 -1
  10. package/dist/mapping-pipeline-service.js +13 -5
  11. package/dist/mapping-service.d.ts +12 -4
  12. package/dist/mapping-service.js +222 -105
  13. package/dist/mcp-helpers.d.ts +10 -2
  14. package/dist/mcp-helpers.js +59 -5
  15. package/dist/minecraft-explorer-service.d.ts +1 -2
  16. package/dist/minecraft-explorer-service.js +120 -24
  17. package/dist/mixin-validator.d.ts +24 -2
  18. package/dist/mixin-validator.js +228 -103
  19. package/dist/mod-decompile-service.d.ts +5 -0
  20. package/dist/mod-decompile-service.js +40 -5
  21. package/dist/mod-remap-service.js +142 -30
  22. package/dist/mojang-tiny-mapping-service.js +26 -26
  23. package/dist/path-resolver.js +41 -4
  24. package/dist/registry-service.d.ts +10 -1
  25. package/dist/registry-service.js +154 -22
  26. package/dist/resources.js +7 -7
  27. package/dist/search-hit-accumulator.d.ts +0 -3
  28. package/dist/search-hit-accumulator.js +27 -6
  29. package/dist/source-jar-reader.js +16 -2
  30. package/dist/source-resolver.d.ts +1 -0
  31. package/dist/source-resolver.js +93 -2
  32. package/dist/source-service.d.ts +76 -47
  33. package/dist/source-service.js +1344 -763
  34. package/dist/stdio-supervisor.d.ts +46 -0
  35. package/dist/stdio-supervisor.js +349 -0
  36. package/dist/storage/files-repo.d.ts +3 -0
  37. package/dist/storage/files-repo.js +66 -1
  38. package/dist/storage/migrations.d.ts +1 -1
  39. package/dist/storage/migrations.js +6 -2
  40. package/dist/storage/schema.d.ts +1 -0
  41. package/dist/storage/schema.js +7 -0
  42. package/dist/symbols/symbol-extractor.js +6 -4
  43. package/dist/tool-execution-gate.d.ts +15 -0
  44. package/dist/tool-execution-gate.js +58 -0
  45. package/dist/tool-input.d.ts +6 -0
  46. package/dist/tool-input.js +64 -0
  47. package/dist/types.d.ts +1 -1
  48. package/dist/version-diff-service.js +10 -5
  49. package/dist/version-service.js +7 -2
  50. package/dist/workspace-mapping-service.js +12 -0
  51. package/package.json +4 -1
@@ -12,7 +12,7 @@ import { parseCoordinate } from "./maven-resolver.js";
12
12
  import { MinecraftExplorerService } 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, validateParsedAccessWidener } from "./mixin-validator.js";
15
+ import { validateParsedMixin, refreshMixinValidationOutcome, validateParsedAccessWidener } from "./mixin-validator.js";
16
16
  import { resolveSourceTarget as resolveSourceTargetInternal } from "./source-resolver.js";
17
17
  import { applyMappingPipeline } from "./mapping-pipeline-service.js";
18
18
  import { MappingService } from "./mapping-service.js";
@@ -34,6 +34,19 @@ import { VersionDiffService } from "./version-diff-service.js";
34
34
  import { ModDecompileService } from "./mod-decompile-service.js";
35
35
  import { ModSearchService } from "./mod-search-service.js";
36
36
  const utf8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
37
+ const VERSION_TOKEN_REGEX_CACHE = new Map();
38
+ const GLOB_REGEX_CACHE = new Map();
39
+ const MAX_HELPER_REGEX_CACHE = 128;
40
+ function rememberCachedRegex(cache, key, regex) {
41
+ if (cache.size >= MAX_HELPER_REGEX_CACHE) {
42
+ const oldestKey = cache.keys().next().value;
43
+ if (oldestKey) {
44
+ cache.delete(oldestKey);
45
+ }
46
+ }
47
+ cache.set(key, regex);
48
+ return regex;
49
+ }
37
50
  function truncateUtf8ToMaxBytes(content, maxBytes) {
38
51
  const encoded = Buffer.from(content, "utf8");
39
52
  if (encoded.length <= maxBytes) {
@@ -51,6 +64,88 @@ function truncateUtf8ToMaxBytes(content, maxBytes) {
51
64
  }
52
65
  return "";
53
66
  }
67
+ function dedupeQualityFlags(qualityFlags) {
68
+ const seen = new Set();
69
+ const deduped = [];
70
+ for (const qualityFlag of qualityFlags) {
71
+ if (seen.has(qualityFlag)) {
72
+ continue;
73
+ }
74
+ seen.add(qualityFlag);
75
+ deduped.push(qualityFlag);
76
+ }
77
+ return deduped;
78
+ }
79
+ function sameStringArray(left, right) {
80
+ if (left === right) {
81
+ return true;
82
+ }
83
+ if (!left || !right || left.length !== right.length) {
84
+ return false;
85
+ }
86
+ for (let index = 0; index < left.length; index += 1) {
87
+ if (left[index] !== right[index]) {
88
+ return false;
89
+ }
90
+ }
91
+ return true;
92
+ }
93
+ function sameScopeFallback(left, right) {
94
+ if (left === right) {
95
+ return true;
96
+ }
97
+ if (!left || !right) {
98
+ return false;
99
+ }
100
+ return left.requested === right.requested && left.applied === right.applied && left.reason === right.reason;
101
+ }
102
+ function sameResolutionTrace(left, right) {
103
+ if (left === right) {
104
+ return true;
105
+ }
106
+ if (!left || !right || left.length !== right.length) {
107
+ return false;
108
+ }
109
+ for (let index = 0; index < left.length; index += 1) {
110
+ const leftEntry = left[index];
111
+ const rightEntry = right[index];
112
+ if (!leftEntry || !rightEntry) {
113
+ return false;
114
+ }
115
+ if (leftEntry.target !== rightEntry.target ||
116
+ leftEntry.step !== rightEntry.step ||
117
+ leftEntry.input !== rightEntry.input ||
118
+ leftEntry.output !== rightEntry.output ||
119
+ leftEntry.success !== rightEntry.success ||
120
+ leftEntry.detail !== rightEntry.detail) {
121
+ return false;
122
+ }
123
+ }
124
+ return true;
125
+ }
126
+ function sameMixinValidationProvenance(left, right) {
127
+ if (left === right) {
128
+ return true;
129
+ }
130
+ if (!left || !right) {
131
+ return false;
132
+ }
133
+ return (left.version === right.version &&
134
+ left.jarPath === right.jarPath &&
135
+ left.requestedMapping === right.requestedMapping &&
136
+ left.mappingApplied === right.mappingApplied &&
137
+ left.requestedScope === right.requestedScope &&
138
+ left.appliedScope === right.appliedScope &&
139
+ left.requestedSourcePriority === right.requestedSourcePriority &&
140
+ left.appliedSourcePriority === right.appliedSourcePriority &&
141
+ sameStringArray(left.resolutionNotes, right.resolutionNotes) &&
142
+ left.jarType === right.jarType &&
143
+ sameStringArray(left.mappingChain, right.mappingChain) &&
144
+ left.remapFailures === right.remapFailures &&
145
+ left.mappingAutoDetected === right.mappingAutoDetected &&
146
+ sameScopeFallback(left.scopeFallback, right.scopeFallback) &&
147
+ sameResolutionTrace(left.resolutionTrace, right.resolutionTrace));
148
+ }
54
149
  const INDEX_SCHEMA_VERSION = 1;
55
150
  const SYMBOL_KINDS = ["class", "interface", "enum", "record", "method", "field"];
56
151
  function isSymbolKind(value) {
@@ -77,9 +172,37 @@ function hasExactVersionToken(path, version) {
77
172
  return false;
78
173
  }
79
174
  // Avoid prefix false-positives like "1.21.1" matching "1.21.10".
80
- const pattern = new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}([^0-9a-z]|$)`, "i");
175
+ const cached = VERSION_TOKEN_REGEX_CACHE.get(normalizedVersion);
176
+ const pattern = cached
177
+ ?? rememberCachedRegex(VERSION_TOKEN_REGEX_CACHE, normalizedVersion, new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}([^0-9a-z]|$)`, "i"));
81
178
  return pattern.test(normalizedPath);
82
179
  }
180
+ function looksLikeDeobfuscatedClassName(value) {
181
+ const trimmed = value.trim();
182
+ if (!trimmed) {
183
+ return false;
184
+ }
185
+ if (trimmed.startsWith("net.minecraft.") || trimmed.startsWith("com.mojang.")) {
186
+ return true;
187
+ }
188
+ const simpleName = trimmed.split(/[.$]/).at(-1) ?? trimmed;
189
+ return /^[A-Z][A-Za-z0-9_$]{2,}$/.test(simpleName);
190
+ }
191
+ function obfuscatedNamespaceHint(className) {
192
+ return `Artifact is indexed in obfuscated runtime names. Deobfuscated names like "${className}" usually require mapping="mojang" or a find-mapping lookup to obfuscated names.`;
193
+ }
194
+ function hasPartialNetMinecraftCoverage(qualityFlags) {
195
+ return qualityFlags.includes("partial-source-no-net-minecraft");
196
+ }
197
+ function buildResolveArtifactParams(target, extra = {}) {
198
+ return {
199
+ target: {
200
+ kind: target.kind,
201
+ value: target.value
202
+ },
203
+ ...extra
204
+ };
205
+ }
83
206
  function normalizeOptionalProjectPath(projectPath) {
84
207
  if (!projectPath) {
85
208
  return undefined;
@@ -91,19 +214,68 @@ function normalizeOptionalProjectPath(projectPath) {
91
214
  const normalized = normalizePathForHost(trimmed, undefined, "projectPath");
92
215
  return isAbsolute(normalized) ? normalized : resolvePath(process.cwd(), normalized);
93
216
  }
217
+ function resolveGradleUserHomePath() {
218
+ const configured = process.env.GRADLE_USER_HOME?.trim();
219
+ if (!configured) {
220
+ return resolvePath(homedir(), ".gradle");
221
+ }
222
+ const normalized = normalizePathForHost(configured, undefined, "GRADLE_USER_HOME");
223
+ return isAbsolute(normalized) ? normalized : resolvePath(process.cwd(), normalized);
224
+ }
94
225
  function buildVersionSourceSearchRoots(projectPath) {
95
226
  const roots = new Set();
96
227
  if (projectPath) {
97
228
  roots.add(resolvePath(projectPath, ".gradle", "loom-cache"));
98
229
  roots.add(resolvePath(projectPath, ".gradle-user", "caches", "fabric-loom"));
99
230
  roots.add(resolvePath(projectPath, ".gradle", "caches", "fabric-loom"));
100
- return [...roots];
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"));
101
234
  }
102
- const homeGradle = resolvePath(homedir(), ".gradle");
235
+ const homeGradle = resolveGradleUserHomePath();
103
236
  roots.add(resolvePath(homeGradle, "loom-cache"));
104
237
  roots.add(resolvePath(homeGradle, "caches", "fabric-loom"));
105
238
  return [...roots];
106
239
  }
240
+ function looksLikeMinecraftSourceArtifact(path, hasMinecraftNamespace) {
241
+ if (hasMinecraftNamespace) {
242
+ return true;
243
+ }
244
+ const normalizedPath = normalizePathStyle(path).toLowerCase();
245
+ return (normalizedPath.includes("/minecraftmaven/") ||
246
+ normalizedPath.includes("/net/minecraft/") ||
247
+ /(?:^|\/)minecraft(?:-[a-z0-9._+]+)*-sources\.jar$/i.test(normalizedPath) ||
248
+ normalizedPath.includes("minecraft-merged") ||
249
+ normalizedPath.includes("minecraft-common") ||
250
+ normalizedPath.includes("minecraft-clientonly") ||
251
+ normalizedPath.includes("minecraft-client") ||
252
+ normalizedPath.includes("minecraft-server"));
253
+ }
254
+ function normalizeRequestedArtifactScope(scope) {
255
+ return scope ?? "vanilla";
256
+ }
257
+ function inferAppliedArtifactScope(input) {
258
+ if (input.scopeFallback?.applied === "vanilla") {
259
+ return "vanilla";
260
+ }
261
+ if (input.requestedScope === "vanilla") {
262
+ return "vanilla";
263
+ }
264
+ const joinedPath = `${normalizePathStyle(input.jarPath)} ${normalizePathStyle(input.resolvedSourceJarPath ?? "")}`.toLowerCase();
265
+ if (joinedPath.includes("minecraft-merged")) {
266
+ return "merged";
267
+ }
268
+ if (input.requestedScope === "loader" && joinedPath.includes("merged")) {
269
+ return "merged";
270
+ }
271
+ return input.requestedScope;
272
+ }
273
+ function scopeToJarType(scope) {
274
+ if (scope === "vanilla") {
275
+ return "vanilla-client";
276
+ }
277
+ return scope;
278
+ }
107
279
  function parseQualifiedMethodSymbol(symbol) {
108
280
  const trimmed = symbol.trim();
109
281
  const separator = trimmed.lastIndexOf(".");
@@ -166,11 +338,18 @@ const COMMON_SOURCE_ROOTS = [
166
338
  "quilt/src/main/java",
167
339
  "quilt/src/client/java"
168
340
  ];
341
+ const MIXIN_PROJECT_DISCOVERY_IGNORES = [
342
+ "**/.git/**",
343
+ "**/.gradle/**",
344
+ "**/build/**",
345
+ "**/out/**",
346
+ "**/node_modules/**"
347
+ ];
169
348
  function normalizeMapping(mapping) {
170
349
  if (mapping == null) {
171
- return "official";
350
+ return "obfuscated";
172
351
  }
173
- if (mapping === "official" ||
352
+ if (mapping === "obfuscated" ||
174
353
  mapping === "mojang" ||
175
354
  mapping === "intermediary" ||
176
355
  mapping === "yarn") {
@@ -181,8 +360,8 @@ function normalizeMapping(mapping) {
181
360
  message: `Unsupported mapping "${mapping}".`,
182
361
  details: {
183
362
  mapping,
184
- nextAction: "Try mapping=official which is always available.",
185
- suggestedCall: { tool: "resolve-artifact", params: { mapping: "official" } }
363
+ nextAction: "Try mapping=obfuscated which is always available.",
364
+ suggestedCall: { tool: "resolve-artifact", params: { mapping: "obfuscated" } }
186
365
  }
187
366
  });
188
367
  }
@@ -194,7 +373,7 @@ function normalizeAccessWidenerNamespace(namespace) {
194
373
  if (normalized === "named") {
195
374
  return "yarn";
196
375
  }
197
- if (normalized === "official" ||
376
+ if (normalized === "obfuscated" ||
198
377
  normalized === "mojang" ||
199
378
  normalized === "intermediary" ||
200
379
  normalized === "yarn") {
@@ -234,11 +413,11 @@ function sortDiffMemberChanges(changes) {
234
413
  if (keyCompare !== 0) {
235
414
  return keyCompare;
236
415
  }
237
- const fromOwnerCompare = left.from.ownerFqn.localeCompare(right.from.ownerFqn);
416
+ const fromOwnerCompare = (left.from?.ownerFqn ?? "").localeCompare(right.from?.ownerFqn ?? "");
238
417
  if (fromOwnerCompare !== 0) {
239
418
  return fromOwnerCompare;
240
419
  }
241
- return left.to.ownerFqn.localeCompare(right.to.ownerFqn);
420
+ return (left.to?.ownerFqn ?? "").localeCompare(right.to?.ownerFqn ?? "");
242
421
  });
243
422
  }
244
423
  function changedMemberFields(fromMember, toMember, includeDescriptor) {
@@ -311,6 +490,16 @@ function emptyDiffDelta() {
311
490
  modified: []
312
491
  };
313
492
  }
493
+ function compactDiffDelta(delta) {
494
+ return {
495
+ added: delta.added,
496
+ removed: delta.removed,
497
+ modified: delta.modified.map((change) => ({
498
+ key: change.key,
499
+ changed: [...change.changed]
500
+ }))
501
+ };
502
+ }
314
503
  function normalizeIntent(intent) {
315
504
  if (intent === "path" || intent === "text") {
316
505
  return intent;
@@ -333,10 +522,16 @@ function canUseIndexedSearchPath(indexedSearchEnabled, intent, match, _scope) {
333
522
  if (match === "regex") {
334
523
  return false;
335
524
  }
336
- // fileGlob and symbolKind are now applied as post-filters on indexed candidates
525
+ // packagePrefix and fileGlob are applied as post-filters on indexed candidates.
337
526
  return true;
338
527
  }
339
528
  function buildGlobRegex(pattern) {
529
+ const cached = GLOB_REGEX_CACHE.get(pattern);
530
+ if (cached) {
531
+ GLOB_REGEX_CACHE.delete(pattern);
532
+ GLOB_REGEX_CACHE.set(pattern, cached);
533
+ return cached;
534
+ }
340
535
  const REGEX_META = /[-/\\^$+.()|[\]{}]/;
341
536
  let result = "";
342
537
  let i = 0;
@@ -363,7 +558,7 @@ function buildGlobRegex(pattern) {
363
558
  i += 1;
364
559
  }
365
560
  }
366
- return new RegExp(`^${result}$`);
561
+ return rememberCachedRegex(GLOB_REGEX_CACHE, pattern, new RegExp(`^${result}$`));
367
562
  }
368
563
  function globToSqlLike(pattern) {
369
564
  let result = "";
@@ -412,14 +607,6 @@ function checkPackagePrefix(filePath, packagePrefix) {
412
607
  const normalizedPrefix = packagePrefix.replace(/\.+/g, "/").replace(/\/+$/, "");
413
608
  return normalizePathStyle(filePath).startsWith(`${normalizedPrefix}/`);
414
609
  }
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
610
  function buildSearchCursorContext(input) {
424
611
  return JSON.stringify({
425
612
  artifactId: input.artifactId,
@@ -427,7 +614,6 @@ function buildSearchCursorContext(input) {
427
614
  intent: input.intent,
428
615
  match: input.match,
429
616
  queryMode: input.queryMode,
430
- includeDefinition: input.includeDefinition,
431
617
  packagePrefix: input.scope?.packagePrefix ?? "",
432
618
  fileGlob: input.scope?.fileGlob ?? "",
433
619
  symbolKind: input.scope?.symbolKind ?? ""
@@ -521,41 +707,6 @@ function matchRegexIndex(target, regex) {
521
707
  const result = regex.exec(target);
522
708
  return result?.index ?? -1;
523
709
  }
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
710
  function chunkArray(items, chunkSize) {
560
711
  const size = Math.max(1, Math.trunc(chunkSize));
561
712
  if (items.length === 0) {
@@ -583,6 +734,11 @@ export class SourceService {
583
734
  versionDiffService;
584
735
  modDecompileService;
585
736
  modSearchService;
737
+ cacheMetricsState = {
738
+ entries: 0,
739
+ totalContentBytes: 0,
740
+ lru: []
741
+ };
586
742
  constructor(explicitConfig, metrics = new RuntimeMetrics()) {
587
743
  this.config = explicitConfig ?? loadConfig();
588
744
  this.metrics = metrics;
@@ -642,14 +798,18 @@ export class SourceService {
642
798
  continue;
643
799
  }
644
800
  const hasMinecraftNamespace = javaEntries.some((entry) => normalizePathStyle(entry).startsWith("net/minecraft/"));
645
- const score = (hasMinecraftNamespace ? 10_000 : 0) +
801
+ const looksLikeMinecraftArtifact = looksLikeMinecraftSourceArtifact(normalizedPath, hasMinecraftNamespace);
802
+ const exactVersionMatch = hasExactVersionToken(normalizedPath, input.version);
803
+ const score = (looksLikeMinecraftArtifact ? 20_000 : 0) +
804
+ (hasMinecraftNamespace ? 10_000 : 0) +
646
805
  (lower.includes("minecraft-merged") ? 2_000 : 0) +
647
- (lower.includes(input.version.toLowerCase()) ? 1_000 : 0) +
806
+ (exactVersionMatch ? 1_000 : 0) +
648
807
  Math.min(javaEntries.length, 500);
649
808
  candidates.push({
650
809
  jarPath: normalizedPath,
651
810
  javaEntryCount: javaEntries.length,
652
811
  hasMinecraftNamespace,
812
+ looksLikeMinecraftArtifact,
653
813
  score
654
814
  });
655
815
  }
@@ -660,14 +820,16 @@ export class SourceService {
660
820
  }
661
821
  return left.jarPath.localeCompare(right.jarPath);
662
822
  });
663
- const selected = candidates.find((candidate) => candidate.hasMinecraftNamespace) ?? candidates[0];
823
+ const selected = candidates.find((candidate) => candidate.looksLikeMinecraftArtifact && candidate.hasMinecraftNamespace) ??
824
+ candidates.find((candidate) => candidate.looksLikeMinecraftArtifact);
664
825
  const candidateArtifacts = candidates
665
826
  .slice(0, 20)
666
- .map((candidate) => `${candidate.jarPath}#java=${candidate.javaEntryCount}`);
827
+ .map((candidate) => `${candidate.jarPath}#java=${candidate.javaEntryCount}#net_minecraft=${candidate.hasMinecraftNamespace ? 1 : 0}`);
667
828
  return {
668
829
  searchedPaths,
669
830
  candidateArtifacts,
670
- selectedSourceJarPath: selected?.jarPath
831
+ selectedSourceJarPath: selected?.jarPath,
832
+ selectedHasMinecraftNamespace: selected?.hasMinecraftNamespace
671
833
  };
672
834
  }
673
835
  buildVersionSourceRecoveryCommand(projectPath) {
@@ -677,6 +839,58 @@ export class SourceService {
677
839
  : "";
678
840
  return `${prefix}./gradlew genSources --no-daemon`;
679
841
  }
842
+ buildArtifactContentsSummary(input) {
843
+ const sourceKind = input.isDecompiled || input.origin === "decompiled" || !normalizeOptionalString(input.sourceJarPath)
844
+ ? "decompiled-binary"
845
+ : "source-jar";
846
+ const sourceCoverage = hasPartialNetMinecraftCoverage(input.qualityFlags) ? "partial" : "full";
847
+ return {
848
+ sourceKind,
849
+ indexedContentKinds: ["java-source"],
850
+ resourcesIncluded: false,
851
+ sourceCoverage
852
+ };
853
+ }
854
+ inferVersionFromContext(input) {
855
+ const direct = normalizeOptionalString(input.version);
856
+ if (direct) {
857
+ return direct;
858
+ }
859
+ const resolvedFromVersion = normalizeOptionalString(input.provenance?.resolvedFrom.version);
860
+ if (resolvedFromVersion) {
861
+ return resolvedFromVersion;
862
+ }
863
+ if (input.provenance?.target.kind === "version") {
864
+ const targetVersion = normalizeOptionalString(input.provenance.target.value);
865
+ if (targetVersion) {
866
+ return targetVersion;
867
+ }
868
+ }
869
+ const coordinate = normalizeOptionalString(input.coordinate);
870
+ if (coordinate) {
871
+ try {
872
+ return parseCoordinate(coordinate).version;
873
+ }
874
+ catch {
875
+ return undefined;
876
+ }
877
+ }
878
+ return undefined;
879
+ }
880
+ async resolveVersionContext(input) {
881
+ const inferredVersion = this.inferVersionFromContext(input);
882
+ if (inferredVersion) {
883
+ return inferredVersion;
884
+ }
885
+ if (!input.preferProjectVersion || !input.projectPath) {
886
+ return undefined;
887
+ }
888
+ const detected = await this.workspaceMappingService.detectProjectMinecraftVersion(input.projectPath);
889
+ if (detected) {
890
+ input.warnings.push(`Using project version "${detected}" from gradle.properties because the artifact metadata did not include a version.`);
891
+ }
892
+ return detected;
893
+ }
680
894
  async resolveArtifact(input) {
681
895
  const kind = input.target.kind;
682
896
  let value = input.target.value?.trim();
@@ -734,13 +948,13 @@ export class SourceService {
734
948
  // coordinate validity is validated by resolver, keep version undefined on parse failure.
735
949
  }
736
950
  }
737
- // Unobfuscated versions (MC 26.1+) ship with official names; intermediary/yarn are not applicable.
951
+ // Unobfuscated versions (MC 26.1+) ship with deobfuscated runtime names; intermediary/yarn are not applicable.
738
952
  let effectiveMapping = mapping;
739
953
  if ((mapping === "intermediary" || mapping === "yarn") &&
740
954
  resolvedVersion &&
741
955
  isUnobfuscatedVersion(resolvedVersion)) {
742
- warnings.push(`Version ${resolvedVersion} is unobfuscated; ${mapping} mappings are not applicable. Using official names.`);
743
- effectiveMapping = "official";
956
+ warnings.push(`Version ${resolvedVersion} is unobfuscated; ${mapping} mappings are not applicable. Using the obfuscated namespace label for the deobfuscated runtime names.`);
957
+ effectiveMapping = "obfuscated";
744
958
  }
745
959
  if (kind === "version" && resolvedVersion && effectiveMapping === "mojang" && scope !== "vanilla") {
746
960
  versionSourceDiscovery = await this.discoverVersionSourceJar({
@@ -788,7 +1002,7 @@ export class SourceService {
788
1002
  if (isVanillaMojang && input.projectPath) {
789
1003
  suggestedCall = {
790
1004
  tool: "resolve-artifact",
791
- params: { targetKind: kind, targetValue: value, mapping: "mojang", scope: "merged", projectPath: input.projectPath }
1005
+ params: buildResolveArtifactParams({ kind, value }, { mapping: "mojang", scope: "merged", projectPath: input.projectPath })
792
1006
  };
793
1007
  nextAction =
794
1008
  "scope=vanilla blocks Loom cache discovery needed for mojang mapping. " +
@@ -797,24 +1011,25 @@ export class SourceService {
797
1011
  else if (isVanillaMojang) {
798
1012
  suggestedCall = {
799
1013
  tool: "resolve-artifact",
800
- params: { targetKind: kind, targetValue: value, mapping: "official", scope: "vanilla" }
1014
+ params: buildResolveArtifactParams({ kind, value }, { mapping: "obfuscated", scope: "vanilla" })
801
1015
  };
802
1016
  nextAction =
803
1017
  "scope=vanilla blocks Loom cache discovery needed for mojang mapping. " +
804
- "Without a projectPath, use mapping=official to read vanilla obfuscated names.";
1018
+ "Without a projectPath, use mapping=obfuscated to read vanilla runtime names directly.";
805
1019
  }
806
1020
  else {
807
1021
  suggestedCall = {
808
1022
  tool: "resolve-artifact",
809
- params: { targetKind: kind, targetValue: value, mapping: "official", ...(scope ? { scope } : {}) }
1023
+ params: buildResolveArtifactParams({ kind, value }, { mapping: "obfuscated", ...(scope ? { scope } : {}) })
810
1024
  };
811
- nextAction = "Retry with mapping=official to use obfuscated names.";
1025
+ nextAction = "Retry with mapping=obfuscated to use the runtime obfuscated namespace.";
812
1026
  }
813
1027
  throw createError({
814
1028
  code: ERROR_CODES.MAPPING_NOT_APPLIED,
815
1029
  message: caughtError.message,
816
1030
  details: {
817
1031
  ...(caughtError.details ?? {}),
1032
+ artifactOrigin: resolved.origin,
818
1033
  searchedPaths: versionSourceDiscovery?.searchedPaths ?? [],
819
1034
  candidateArtifacts: versionSourceDiscovery?.candidateArtifacts ?? resolved.adjacentSourceCandidates ?? [],
820
1035
  recommendedCommand: this.buildVersionSourceRecoveryCommand(input.projectPath),
@@ -834,14 +1049,17 @@ export class SourceService {
834
1049
  details: {
835
1050
  mapping: effectiveMapping,
836
1051
  target: { kind, value },
837
- nextAction: "Use targetKind=version or a versioned Maven coordinate so mapping artifacts can be resolved.",
838
- suggestedCall: { tool: "resolve-artifact", params: { targetKind: "version", targetValue: value, ...(scope ? { scope } : {}) } }
1052
+ nextAction: "Use target: { kind: \"version\", value } or a versioned Maven coordinate so mapping artifacts can be resolved.",
1053
+ suggestedCall: {
1054
+ tool: "resolve-artifact",
1055
+ params: buildResolveArtifactParams({ kind: "version", value }, { ...(scope ? { scope } : {}) })
1056
+ }
839
1057
  }
840
1058
  });
841
1059
  }
842
1060
  const mappingAvailability = await this.mappingService.ensureMappingAvailable({
843
1061
  version: resolved.version,
844
- sourceMapping: "official",
1062
+ sourceMapping: "obfuscated",
845
1063
  targetMapping: effectiveMapping,
846
1064
  sourcePriority: input.sourcePriority
847
1065
  });
@@ -864,6 +1082,10 @@ export class SourceService {
864
1082
  }
865
1083
  if (versionSourceDiscovery?.selectedSourceJarPath) {
866
1084
  resolved.qualityFlags.push("source-jar-validated");
1085
+ if (versionSourceDiscovery.selectedHasMinecraftNamespace === false) {
1086
+ resolved.qualityFlags.push("partial-source-no-net-minecraft");
1087
+ warnings.push(`Source coverage does not include net.minecraft for ${versionSourceDiscovery.selectedSourceJarPath}; class lookups may fall back to the binary artifact.`);
1088
+ }
867
1089
  if (kind === "version" && !hasExactVersionToken(versionSourceDiscovery.selectedSourceJarPath, value)) {
868
1090
  if (input.strictVersion) {
869
1091
  throw createError({
@@ -874,7 +1096,10 @@ export class SourceService {
874
1096
  selectedSourceJar: versionSourceDiscovery.selectedSourceJarPath,
875
1097
  candidateArtifacts: versionSourceDiscovery.candidateArtifacts,
876
1098
  nextAction: "Use strictVersion=false (default) to allow approximation, or ensure the exact version source jar is in the Loom cache.",
877
- suggestedCall: { tool: "resolve-artifact", params: { targetKind: "version", targetValue: value, strictVersion: false } }
1099
+ suggestedCall: {
1100
+ tool: "resolve-artifact",
1101
+ params: buildResolveArtifactParams({ kind: "version", value }, { strictVersion: false })
1102
+ }
878
1103
  }
879
1104
  });
880
1105
  }
@@ -882,7 +1107,7 @@ export class SourceService {
882
1107
  warnings.push(`Requested version "${value}" but resolved source jar does not contain exact version string: ${versionSourceDiscovery.selectedSourceJarPath}`);
883
1108
  }
884
1109
  }
885
- resolved.qualityFlags = [...new Set(resolved.qualityFlags)];
1110
+ resolved.qualityFlags = dedupeQualityFlags(resolved.qualityFlags);
886
1111
  await this.ingestIfNeeded(resolved);
887
1112
  let sampleEntries;
888
1113
  if (resolved.sourceJarPath) {
@@ -912,6 +1137,12 @@ export class SourceService {
912
1137
  provenance,
913
1138
  qualityFlags: resolved.qualityFlags,
914
1139
  repoUrl: resolved.repoUrl,
1140
+ artifactContents: this.buildArtifactContentsSummary({
1141
+ origin: resolved.origin,
1142
+ sourceJarPath: resolved.sourceJarPath,
1143
+ isDecompiled: resolved.isDecompiled,
1144
+ qualityFlags: resolved.qualityFlags
1145
+ }),
915
1146
  warnings,
916
1147
  sampleEntries
917
1148
  };
@@ -942,8 +1173,14 @@ export class SourceService {
942
1173
  if (!query) {
943
1174
  return {
944
1175
  hits: [],
945
- totalApprox: 0,
946
- mappingApplied: artifact.mappingApplied ?? "official"
1176
+ mappingApplied: artifact.mappingApplied ?? "obfuscated",
1177
+ returnedNamespace: artifact.mappingApplied ?? "obfuscated",
1178
+ artifactContents: this.buildArtifactContentsSummary({
1179
+ origin: artifact.origin,
1180
+ sourceJarPath: artifact.sourceJarPath,
1181
+ isDecompiled: artifact.isDecompiled,
1182
+ qualityFlags: artifact.qualityFlags
1183
+ })
947
1184
  };
948
1185
  }
949
1186
  const intent = normalizeIntent(input.intent);
@@ -961,12 +1198,15 @@ export class SourceService {
961
1198
  const searchLimitCap = match === "regex"
962
1199
  ? Math.max(1, Math.min(this.config.maxSearchHits, MAX_REGEX_RESULT_LIMIT))
963
1200
  : this.config.maxSearchHits;
1201
+ const scope = input.scope;
1202
+ if (scope?.symbolKind && intent !== "symbol") {
1203
+ throw createError({
1204
+ code: ERROR_CODES.INVALID_INPUT,
1205
+ message: 'symbolKind filter is only supported when intent="symbol".'
1206
+ });
1207
+ }
964
1208
  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
1209
  const regexPattern = match === "regex" ? compileRegex(query) : undefined;
969
- const scope = input.scope;
970
1210
  const queryMode = input.queryMode ?? "auto";
971
1211
  const cursorContext = buildSearchCursorContext({
972
1212
  artifactId: artifact.artifactId,
@@ -974,8 +1214,7 @@ export class SourceService {
974
1214
  intent,
975
1215
  match,
976
1216
  queryMode,
977
- scope,
978
- includeDefinition
1217
+ scope
979
1218
  });
980
1219
  const decodedCursor = decodeSearchCursor(input.cursor);
981
1220
  const cursor = decodedCursor?.contextKey === cursorContext ? decodedCursor : undefined;
@@ -991,52 +1230,35 @@ export class SourceService {
991
1230
  const hasSeparators = /[._$]/.test(query);
992
1231
  const tokenOnlyTextIntent = intent === "text" && queryMode === "token";
993
1232
  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
- }
1233
+ this.searchSymbolIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1006
1234
  }
1007
1235
  else if (queryMode === "literal" && intent === "text") {
1008
1236
  // F-03: queryMode=literal forces substring scan for text intent
1009
1237
  this.metrics.recordSearchFallback();
1010
- this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1238
+ this.searchTextIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1011
1239
  }
1012
1240
  else if (!indexedSearchEnabled) {
1013
1241
  this.metrics.recordIndexedDisabled();
1014
1242
  if (!tokenOnlyTextIntent) {
1015
1243
  this.metrics.recordSearchFallback();
1016
1244
  if (intent === "path") {
1017
- this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1245
+ this.searchPathIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1018
1246
  }
1019
1247
  else {
1020
- this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1248
+ this.searchTextIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1021
1249
  }
1022
1250
  }
1023
1251
  }
1024
1252
  else if (canUseIndexedSearchPath(indexedSearchEnabled, intent, match, scope)) {
1025
1253
  try {
1026
1254
  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);
1255
+ this.searchPathIntentIndexed(artifact.artifactId, query, match, scope, recordHit);
1031
1256
  }
1032
1257
  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);
1258
+ this.searchTextIntentIndexed(artifact.artifactId, query, match, scope, recordHit);
1037
1259
  // F-03: queryMode=auto fallback — when indexed returns 0 hits and query has separators, retry with literal scan
1038
1260
  if (queryMode === "auto" && hasSeparators && accumulator.currentCount() === 0) {
1039
- this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1261
+ this.searchTextIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1040
1262
  }
1041
1263
  }
1042
1264
  this.metrics.recordSearchIndexedHit();
@@ -1052,10 +1274,10 @@ export class SourceService {
1052
1274
  // F-03: queryMode=token suppresses error-path fallback to brute-force scan
1053
1275
  if (!tokenOnlyTextIntent) {
1054
1276
  if (intent === "path") {
1055
- this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1277
+ this.searchPathIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1056
1278
  }
1057
1279
  else {
1058
- this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1280
+ this.searchTextIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1059
1281
  }
1060
1282
  }
1061
1283
  }
@@ -1064,10 +1286,10 @@ export class SourceService {
1064
1286
  if (!tokenOnlyTextIntent) {
1065
1287
  this.metrics.recordSearchFallback();
1066
1288
  if (intent === "path") {
1067
- this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1289
+ this.searchPathIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1068
1290
  }
1069
1291
  else {
1070
- this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
1292
+ this.searchTextIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1071
1293
  }
1072
1294
  }
1073
1295
  }
@@ -1078,31 +1300,18 @@ export class SourceService {
1078
1300
  const nextCursor = finalizedHits.nextCursorHit
1079
1301
  ? encodeSearchCursor(finalizedHits.nextCursorHit, cursorContext)
1080
1302
  : 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;
1303
+ this.metrics.recordSearchTokenBytesReturned(Buffer.byteLength(JSON.stringify({ hits: page }), "utf8"));
1100
1304
  return {
1101
1305
  hits: page,
1102
- relations: relations && relations.length > 0 ? relations : undefined,
1103
1306
  nextCursor,
1104
- totalApprox,
1105
- mappingApplied: artifact.mappingApplied ?? "official"
1307
+ mappingApplied: artifact.mappingApplied ?? "obfuscated",
1308
+ returnedNamespace: artifact.mappingApplied ?? "obfuscated",
1309
+ artifactContents: this.buildArtifactContentsSummary({
1310
+ origin: artifact.origin,
1311
+ sourceJarPath: artifact.sourceJarPath,
1312
+ isDecompiled: artifact.isDecompiled,
1313
+ qualityFlags: artifact.qualityFlags
1314
+ })
1106
1315
  };
1107
1316
  }
1108
1317
  finally {
@@ -1139,7 +1348,14 @@ export class SourceService {
1139
1348
  content,
1140
1349
  contentBytes: fullBytes,
1141
1350
  truncated,
1142
- mappingApplied: artifact.mappingApplied ?? "official"
1351
+ mappingApplied: artifact.mappingApplied ?? "obfuscated",
1352
+ returnedNamespace: artifact.mappingApplied ?? "obfuscated",
1353
+ artifactContents: this.buildArtifactContentsSummary({
1354
+ origin: artifact.origin,
1355
+ sourceJarPath: artifact.sourceJarPath,
1356
+ isDecompiled: artifact.isDecompiled,
1357
+ qualityFlags: artifact.qualityFlags
1358
+ })
1143
1359
  };
1144
1360
  }
1145
1361
  finally {
@@ -1151,15 +1367,29 @@ export class SourceService {
1151
1367
  try {
1152
1368
  const artifact = this.getArtifact(input.artifactId);
1153
1369
  const limit = clampLimit(input.limit, 200, 2000);
1370
+ const warnings = [];
1154
1371
  const page = this.filesRepo.listFiles(artifact.artifactId, {
1155
1372
  limit,
1156
1373
  cursor: input.cursor,
1157
1374
  prefix: input.prefix
1158
1375
  });
1376
+ const normalizedPrefix = normalizeOptionalString(input.prefix);
1377
+ if (normalizedPrefix &&
1378
+ page.items.length === 0 &&
1379
+ (normalizedPrefix.startsWith("assets/") || normalizedPrefix.startsWith("data/"))) {
1380
+ warnings.push("Indexed artifacts currently include Java source only; non-Java resources are not indexed. Inspect the original jar on disk if you need assets or data files.");
1381
+ }
1159
1382
  return {
1160
1383
  items: page.items,
1161
1384
  nextCursor: page.nextCursor,
1162
- mappingApplied: artifact.mappingApplied ?? "official"
1385
+ mappingApplied: artifact.mappingApplied ?? "obfuscated",
1386
+ artifactContents: this.buildArtifactContentsSummary({
1387
+ origin: artifact.origin,
1388
+ sourceJarPath: artifact.sourceJarPath,
1389
+ isDecompiled: artifact.isDecompiled,
1390
+ qualityFlags: artifact.qualityFlags
1391
+ }),
1392
+ warnings
1163
1393
  };
1164
1394
  }
1165
1395
  finally {
@@ -1298,21 +1528,24 @@ export class SourceService {
1298
1528
  resolved: false,
1299
1529
  status: "mapping_unavailable",
1300
1530
  candidates: [],
1531
+ candidateCount: 0,
1301
1532
  workspaceDetection,
1302
1533
  warnings
1303
1534
  };
1304
1535
  }
1305
1536
  const mappingApplied = workspaceDetection.mappingApplied;
1306
1537
  if (kind === "method") {
1538
+ const methodOwner = owner;
1539
+ const methodDescriptor = descriptor;
1307
1540
  const exact = await this.mappingService.resolveMethodMappingExact({
1308
1541
  version,
1309
- kind: "method",
1310
- owner,
1542
+ owner: methodOwner,
1311
1543
  name,
1312
- descriptor: descriptor,
1544
+ descriptor: methodDescriptor,
1313
1545
  sourceMapping: input.sourceMapping,
1314
1546
  targetMapping: mappingApplied,
1315
- sourcePriority: input.sourcePriority
1547
+ sourcePriority: input.sourcePriority,
1548
+ maxCandidates: input.maxCandidates
1316
1549
  });
1317
1550
  return {
1318
1551
  ...exact,
@@ -1342,6 +1575,7 @@ export class SourceService {
1342
1575
  resolved: false,
1343
1576
  status: "not_found",
1344
1577
  candidates: [],
1578
+ candidateCount: 0,
1345
1579
  workspaceDetection,
1346
1580
  warnings: [...warnings, ...matrix.warnings]
1347
1581
  };
@@ -1369,6 +1603,7 @@ export class SourceService {
1369
1603
  status: "resolved",
1370
1604
  resolvedSymbol,
1371
1605
  candidates: [resolvedCandidate],
1606
+ candidateCount: 1,
1372
1607
  workspaceDetection,
1373
1608
  warnings: [...warnings, ...matrix.warnings]
1374
1609
  };
@@ -1381,7 +1616,8 @@ export class SourceService {
1381
1616
  descriptor,
1382
1617
  sourceMapping: input.sourceMapping,
1383
1618
  targetMapping: mappingApplied,
1384
- sourcePriority: input.sourcePriority
1619
+ sourcePriority: input.sourcePriority,
1620
+ maxCandidates: input.maxCandidates
1385
1621
  });
1386
1622
  const filtered = mapped.candidates.filter((candidate) => candidate.kind === kind);
1387
1623
  let status;
@@ -1404,6 +1640,8 @@ export class SourceService {
1404
1640
  status,
1405
1641
  resolvedSymbol: status === "resolved" ? filtered[0] : undefined,
1406
1642
  candidates: filtered,
1643
+ candidateCount: mapped.candidateCount,
1644
+ candidatesTruncated: mapped.candidatesTruncated,
1407
1645
  workspaceDetection,
1408
1646
  warnings: [...warnings, ...mapped.warnings]
1409
1647
  };
@@ -1473,14 +1711,14 @@ export class SourceService {
1473
1711
  try {
1474
1712
  let resolvedSymbols = resolvedSymbolsByVersion.get(version);
1475
1713
  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)
1714
+ const [obfuscatedClassName, obfuscatedMethod] = await Promise.all([
1715
+ this.resolveToObfuscatedClassName(userClassName, version, mapping, input.sourcePriority, warnings),
1716
+ this.resolveToObfuscatedMemberName(userMethodName, userClassName, descriptor, "method", version, mapping, input.sourcePriority, warnings)
1479
1717
  ]);
1480
1718
  resolvedSymbols = {
1481
- className: officialClassName,
1482
- methodName: officialMethod.name,
1483
- methodDescriptor: officialMethod.descriptor
1719
+ className: obfuscatedClassName,
1720
+ methodName: obfuscatedMethod.name,
1721
+ methodDescriptor: obfuscatedMethod.descriptor
1484
1722
  };
1485
1723
  resolvedSymbolsByVersion.set(version, resolvedSymbols);
1486
1724
  }
@@ -1574,6 +1812,7 @@ export class SourceService {
1574
1812
  const className = input.className.trim();
1575
1813
  const fromVersion = input.fromVersion.trim();
1576
1814
  const toVersion = input.toVersion.trim();
1815
+ const includeFullDiff = input.includeFullDiff ?? true;
1577
1816
  if (!className || !fromVersion || !toVersion) {
1578
1817
  throw createError({
1579
1818
  code: ERROR_CODES.INVALID_INPUT,
@@ -1630,18 +1869,18 @@ export class SourceService {
1630
1869
  });
1631
1870
  }
1632
1871
  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);
1872
+ const obfuscatedFromClassName = await this.resolveToObfuscatedClassName(className, fromVersion, mapping, input.sourcePriority, mappingWarnings);
1873
+ const obfuscatedToClassName = fromVersion === toVersion
1874
+ ? obfuscatedFromClassName
1875
+ : await this.resolveToObfuscatedClassName(className, toVersion, mapping, input.sourcePriority, mappingWarnings);
1637
1876
  const [fromResolved, toResolved] = await Promise.all([
1638
1877
  this.versionService.resolveVersionJar(fromVersion),
1639
1878
  this.versionService.resolveVersionJar(toVersion)
1640
1879
  ]);
1641
- const loadSignature = async (version, jarPath, officialClassName) => {
1880
+ const loadSignature = async (version, jarPath, obfuscatedClassName) => {
1642
1881
  try {
1643
1882
  const signature = await this.explorerService.getSignature({
1644
- fqn: officialClassName,
1883
+ fqn: obfuscatedClassName,
1645
1884
  jarPath,
1646
1885
  access: "all",
1647
1886
  includeSynthetic: false,
@@ -1662,8 +1901,8 @@ export class SourceService {
1662
1901
  }
1663
1902
  };
1664
1903
  const [fromSignature, toSignature] = await Promise.all([
1665
- loadSignature(fromVersion, fromResolved.jarPath, officialFromClassName),
1666
- loadSignature(toVersion, toResolved.jarPath, officialToClassName)
1904
+ loadSignature(fromVersion, fromResolved.jarPath, obfuscatedFromClassName),
1905
+ loadSignature(toVersion, toResolved.jarPath, obfuscatedToClassName)
1667
1906
  ]);
1668
1907
  const warnings = [...mappingWarnings];
1669
1908
  if (fromSignature) {
@@ -1740,18 +1979,46 @@ export class SourceService {
1740
1979
  : classChange === "absent_in_both"
1741
1980
  ? emptyDiffDelta()
1742
1981
  : diffMembersByKey(fromMembers.fields, toMembers.fields, (member) => member.name, true);
1743
- // Remap diff delta members for non-official mappings
1982
+ // Remap diff delta members for non-obfuscated mappings
1744
1983
  const remapDelta = async (delta, kind) => {
1745
1984
  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)
1985
+ this.remapSignatureMembers(delta.added, kind, toVersion, "obfuscated", mapping, input.sourcePriority, warnings),
1986
+ this.remapSignatureMembers(delta.removed, kind, fromVersion, "obfuscated", mapping, input.sourcePriority, warnings)
1748
1987
  ]);
1749
1988
  const remappedModified = await Promise.all(delta.modified.map(async (change) => {
1989
+ if (!change.from || !change.to) {
1990
+ throw createError({
1991
+ code: ERROR_CODES.INTERNAL,
1992
+ message: "Modified diff members are missing before remap.",
1993
+ details: {
1994
+ key: change.key,
1995
+ kind,
1996
+ fromVersion,
1997
+ toVersion,
1998
+ mapping
1999
+ }
2000
+ });
2001
+ }
1750
2002
  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)
2003
+ this.remapSignatureMembers([change.from], kind, fromVersion, "obfuscated", mapping, input.sourcePriority, warnings),
2004
+ this.remapSignatureMembers([change.to], kind, toVersion, "obfuscated", mapping, input.sourcePriority, warnings)
1753
2005
  ]);
1754
- return { ...change, from: fromResult.members[0], to: toResult.members[0] };
2006
+ const fromMember = fromResult.members[0];
2007
+ const toMember = toResult.members[0];
2008
+ if (!fromMember || !toMember) {
2009
+ throw createError({
2010
+ code: ERROR_CODES.INTERNAL,
2011
+ message: "Failed to remap modified diff members.",
2012
+ details: {
2013
+ key: change.key,
2014
+ kind,
2015
+ fromVersion,
2016
+ toVersion,
2017
+ mapping
2018
+ }
2019
+ });
2020
+ }
2021
+ return { ...change, from: fromMember, to: toMember };
1755
2022
  }));
1756
2023
  return { added: addedResult.members, removed: removedResult.members, modified: remappedModified };
1757
2024
  };
@@ -1794,9 +2061,9 @@ export class SourceService {
1794
2061
  toVersion
1795
2062
  },
1796
2063
  classChange,
1797
- constructors: remappedConstructors,
1798
- methods: remappedMethods,
1799
- fields: remappedFields,
2064
+ constructors: includeFullDiff ? remappedConstructors : compactDiffDelta(remappedConstructors),
2065
+ methods: includeFullDiff ? remappedMethods : compactDiffDelta(remappedMethods),
2066
+ fields: includeFullDiff ? remappedFields : compactDiffDelta(remappedFields),
1800
2067
  summary,
1801
2068
  warnings
1802
2069
  };
@@ -1817,7 +2084,7 @@ export class SourceService {
1817
2084
  });
1818
2085
  }
1819
2086
  // Verify artifact exists
1820
- this.getArtifact(artifactId);
2087
+ const artifact = this.getArtifact(artifactId);
1821
2088
  const limit = Math.max(1, Math.min(input.limit ?? 20, 200));
1822
2089
  const warnings = [];
1823
2090
  const isQualified = className.includes(".");
@@ -1847,7 +2114,17 @@ export class SourceService {
1847
2114
  symbolKind: row.symbolKind
1848
2115
  }))
1849
2116
  .slice(0, limit);
1850
- return { matches, total: matches.length, warnings };
2117
+ const partialVanillaLookup = hasPartialNetMinecraftCoverage(artifact.qualityFlags) && looksLikeDeobfuscatedClassName(className);
2118
+ const filteredMatches = partialVanillaLookup && matches.every((match) => !match.qualifiedName.startsWith("net.minecraft.") && !match.qualifiedName.startsWith("com.mojang."))
2119
+ ? []
2120
+ : matches;
2121
+ if (filteredMatches.length === 0 && partialVanillaLookup) {
2122
+ 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.`);
2123
+ }
2124
+ if (filteredMatches.length === 0 && artifact.mappingApplied === "obfuscated" && looksLikeDeobfuscatedClassName(className)) {
2125
+ warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
2126
+ }
2127
+ return { matches: filteredMatches, total: filteredMatches.length, warnings };
1851
2128
  }
1852
2129
  // Simple name: search for exact symbol name match among type symbols
1853
2130
  const result = this.symbolsRepo.findScopedSymbols({
@@ -1871,7 +2148,17 @@ export class SourceService {
1871
2148
  symbolKind: row.symbolKind
1872
2149
  });
1873
2150
  }
1874
- return { matches, total: matches.length, warnings };
2151
+ const partialVanillaLookup = hasPartialNetMinecraftCoverage(artifact.qualityFlags) && looksLikeDeobfuscatedClassName(className);
2152
+ const filteredMatches = partialVanillaLookup && matches.every((match) => !match.qualifiedName.startsWith("net.minecraft.") && !match.qualifiedName.startsWith("com.mojang."))
2153
+ ? []
2154
+ : matches;
2155
+ if (filteredMatches.length === 0 && partialVanillaLookup) {
2156
+ 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.`);
2157
+ }
2158
+ if (filteredMatches.length === 0 && artifact.mappingApplied === "obfuscated" && looksLikeDeobfuscatedClassName(className)) {
2159
+ warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
2160
+ }
2161
+ return { matches: filteredMatches, total: filteredMatches.length, warnings };
1875
2162
  }
1876
2163
  async getClassSource(input) {
1877
2164
  const className = input.className.trim();
@@ -1905,7 +2192,7 @@ export class SourceService {
1905
2192
  if (normalizedArtifactId && input.target) {
1906
2193
  throw createError({
1907
2194
  code: ERROR_CODES.INVALID_INPUT,
1908
- message: "artifactId and targetKind/targetValue are mutually exclusive.",
2195
+ message: "artifactId and target are mutually exclusive.",
1909
2196
  details: {
1910
2197
  artifactId: normalizedArtifactId,
1911
2198
  target: input.target
@@ -1919,6 +2206,10 @@ export class SourceService {
1919
2206
  let mappingApplied = requestedMapping;
1920
2207
  let provenance;
1921
2208
  let qualityFlags = [];
2209
+ let sourceJarPath;
2210
+ let binaryJarPath;
2211
+ let version;
2212
+ let coordinate;
1922
2213
  if (!artifactId) {
1923
2214
  if (!input.target) {
1924
2215
  throw createError({
@@ -1943,6 +2234,10 @@ export class SourceService {
1943
2234
  mappingApplied = resolved.mappingApplied;
1944
2235
  provenance = resolved.provenance;
1945
2236
  qualityFlags = [...resolved.qualityFlags];
2237
+ sourceJarPath = resolved.resolvedSourceJarPath;
2238
+ binaryJarPath = resolved.binaryJarPath;
2239
+ version = resolved.version;
2240
+ coordinate = resolved.coordinate;
1946
2241
  }
1947
2242
  else {
1948
2243
  const artifact = this.getArtifact(artifactId);
@@ -1951,53 +2246,130 @@ export class SourceService {
1951
2246
  mappingApplied = artifact.mappingApplied ?? requestedMapping;
1952
2247
  provenance = artifact.provenance;
1953
2248
  qualityFlags = artifact.qualityFlags;
2249
+ sourceJarPath = artifact.sourceJarPath;
2250
+ binaryJarPath = artifact.binaryJarPath;
2251
+ version = artifact.version;
2252
+ coordinate = artifact.coordinate;
1954
2253
  }
1955
- const filePath = this.resolveClassFilePath(artifactId, className);
1956
- 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.`;
2254
+ version = await this.resolveVersionContext({
2255
+ version,
2256
+ provenance,
2257
+ coordinate,
2258
+ projectPath: input.projectPath,
2259
+ preferProjectVersion: input.preferProjectVersion,
2260
+ warnings
2261
+ });
2262
+ let activeArtifactId = artifactId;
2263
+ let activeOrigin = origin;
2264
+ let activeProvenance = provenance;
2265
+ let activeQualityFlags = [...qualityFlags];
2266
+ let activeMappingApplied = mappingApplied;
2267
+ let activeSourceJarPath = sourceJarPath;
2268
+ let attemptedBinaryFallback = false;
2269
+ const tryBinaryFallback = async () => {
2270
+ if (attemptedBinaryFallback) {
2271
+ return false;
1965
2272
  }
1966
- else if (targetKind === "version" && scope && scope !== "merged" && input.projectPath) {
1967
- nextAction +=
1968
- ` The class may exist in merged sources; retry with scope: "merged".`;
2273
+ attemptedBinaryFallback = true;
2274
+ const normalizedBinaryJarPath = normalizeOptionalString(binaryJarPath);
2275
+ if (!normalizedBinaryJarPath) {
2276
+ return false;
1969
2277
  }
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
- }
2278
+ if (activeSourceJarPath &&
2279
+ normalizePathStyle(activeSourceJarPath) === normalizePathStyle(normalizedBinaryJarPath)) {
2280
+ return false;
2281
+ }
2282
+ const fallbackResolved = await this.resolveBinaryFallbackArtifact({
2283
+ binaryJarPath: normalizedBinaryJarPath,
2284
+ version,
2285
+ coordinate,
2286
+ requestedMapping,
2287
+ mappingApplied,
2288
+ provenance: activeProvenance,
2289
+ qualityFlags: activeQualityFlags
2290
+ });
2291
+ if (!fallbackResolved || fallbackResolved.artifactId === activeArtifactId) {
2292
+ return false;
2293
+ }
2294
+ activeArtifactId = fallbackResolved.artifactId;
2295
+ activeOrigin = fallbackResolved.origin;
2296
+ activeMappingApplied = fallbackResolved.mappingApplied ?? activeMappingApplied;
2297
+ activeProvenance = fallbackResolved.provenance ?? activeProvenance;
2298
+ activeQualityFlags = dedupeQualityFlags([...(fallbackResolved.qualityFlags ?? []), "binary-fallback"]);
2299
+ activeSourceJarPath = fallbackResolved.sourceJarPath;
2300
+ warnings.push(`Falling back to binary artifact "${normalizedBinaryJarPath}" because source coverage for "${className}" was incomplete.`);
2301
+ if (activeMappingApplied !== requestedMapping) {
2302
+ warnings.push(`Fallback source text is indexed in ${activeMappingApplied} names; returned source is not remapped to ${requestedMapping}.`);
2303
+ }
2304
+ return true;
2305
+ };
2306
+ let activeLookupClassName = await this.resolveClassNameForLookup({
2307
+ className,
2308
+ version,
2309
+ sourceMapping: requestedMapping,
2310
+ targetMapping: activeMappingApplied,
2311
+ sourcePriority: input.sourcePriority,
2312
+ warnings,
2313
+ context: "source lookup"
2314
+ });
2315
+ let filePath = this.resolveClassFilePath(activeArtifactId, activeLookupClassName);
2316
+ if (!filePath && (await tryBinaryFallback())) {
2317
+ activeLookupClassName = await this.resolveClassNameForLookup({
2318
+ className,
2319
+ version,
2320
+ sourceMapping: requestedMapping,
2321
+ targetMapping: activeMappingApplied,
2322
+ sourcePriority: input.sourcePriority,
2323
+ warnings,
2324
+ context: "source lookup"
1983
2325
  });
2326
+ filePath = this.resolveClassFilePath(activeArtifactId, activeLookupClassName);
2327
+ }
2328
+ if (!filePath) {
2329
+ throw this.buildClassSourceNotFoundError({
2330
+ artifactId: activeArtifactId,
2331
+ className,
2332
+ lookupClassName: activeLookupClassName,
2333
+ mappingApplied: activeMappingApplied,
2334
+ requestedMapping,
2335
+ qualityFlags: activeQualityFlags,
2336
+ attemptedBinaryFallback,
2337
+ targetKind: input.target?.kind,
2338
+ targetValue: input.target?.value,
2339
+ scope: input.scope,
2340
+ projectPath: input.projectPath,
2341
+ version
2342
+ });
2343
+ }
2344
+ let row = this.filesRepo.getFileContent(activeArtifactId, filePath);
2345
+ if (!row && (await tryBinaryFallback())) {
2346
+ activeLookupClassName = await this.resolveClassNameForLookup({
2347
+ className,
2348
+ version,
2349
+ sourceMapping: requestedMapping,
2350
+ targetMapping: activeMappingApplied,
2351
+ sourcePriority: input.sourcePriority,
2352
+ warnings,
2353
+ context: "source lookup"
2354
+ });
2355
+ filePath = this.resolveClassFilePath(activeArtifactId, activeLookupClassName) ?? filePath;
2356
+ row = this.filesRepo.getFileContent(activeArtifactId, filePath);
1984
2357
  }
1985
- const row = this.filesRepo.getFileContent(artifactId, filePath);
1986
2358
  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
- }
2359
+ throw this.buildClassSourceNotFoundError({
2360
+ artifactId: activeArtifactId,
2361
+ className,
2362
+ lookupClassName: activeLookupClassName,
2363
+ mappingApplied: activeMappingApplied,
2364
+ requestedMapping,
2365
+ qualityFlags: activeQualityFlags,
2366
+ attemptedBinaryFallback,
2367
+ filePath,
2368
+ targetKind: input.target?.kind,
2369
+ targetValue: input.target?.value,
2370
+ scope: input.scope,
2371
+ projectPath: input.projectPath,
2372
+ version
2001
2373
  });
2002
2374
  }
2003
2375
  const lines = row.content.split(/\r?\n/);
@@ -2048,12 +2420,12 @@ export class SourceService {
2048
2420
  resolvedOutputFile = outputPath;
2049
2421
  sourceText = `[Written to ${outputPath}]`;
2050
2422
  }
2051
- const normalizedProvenance = provenance ??
2423
+ const normalizedProvenance = activeProvenance ??
2052
2424
  this.buildFallbackProvenance({
2053
- artifactId,
2054
- origin,
2425
+ artifactId: activeArtifactId,
2426
+ origin: activeOrigin,
2055
2427
  requestedMapping,
2056
- mappingApplied
2428
+ mappingApplied: activeMappingApplied
2057
2429
  });
2058
2430
  return {
2059
2431
  className,
@@ -2066,12 +2438,19 @@ export class SourceService {
2066
2438
  },
2067
2439
  truncated,
2068
2440
  ...(charsTruncated ? { charsTruncated } : {}),
2069
- origin,
2070
- artifactId,
2441
+ origin: activeOrigin,
2442
+ artifactId: activeArtifactId,
2071
2443
  requestedMapping,
2072
- mappingApplied,
2444
+ mappingApplied: activeMappingApplied,
2445
+ returnedNamespace: activeMappingApplied,
2073
2446
  provenance: normalizedProvenance,
2074
- qualityFlags,
2447
+ qualityFlags: activeQualityFlags,
2448
+ artifactContents: this.buildArtifactContentsSummary({
2449
+ origin: activeOrigin,
2450
+ sourceJarPath: activeSourceJarPath,
2451
+ isDecompiled: activeOrigin === "decompiled",
2452
+ qualityFlags: activeQualityFlags
2453
+ }),
2075
2454
  ...(resolvedOutputFile ? { outputFile: resolvedOutputFile } : {}),
2076
2455
  warnings
2077
2456
  };
@@ -2095,7 +2474,7 @@ export class SourceService {
2095
2474
  if (normalizedArtifactId && input.target) {
2096
2475
  throw createError({
2097
2476
  code: ERROR_CODES.INVALID_INPUT,
2098
- message: "artifactId and targetKind/targetValue are mutually exclusive.",
2477
+ message: "artifactId and target are mutually exclusive.",
2099
2478
  details: {
2100
2479
  artifactId: normalizedArtifactId,
2101
2480
  target: input.target
@@ -2109,6 +2488,8 @@ export class SourceService {
2109
2488
  let provenance;
2110
2489
  let qualityFlags = [];
2111
2490
  let binaryJarPath;
2491
+ let sourceJarPath;
2492
+ let coordinate;
2112
2493
  if (parsedMaxMembers != null && parsedMaxMembers > 5000) {
2113
2494
  warnings.push(`maxMembers was clamped to 5000 from ${parsedMaxMembers}.`);
2114
2495
  }
@@ -2137,7 +2518,9 @@ export class SourceService {
2137
2518
  provenance = resolved.provenance;
2138
2519
  qualityFlags = [...resolved.qualityFlags];
2139
2520
  binaryJarPath = resolved.binaryJarPath;
2521
+ sourceJarPath = resolved.resolvedSourceJarPath;
2140
2522
  version = resolved.version;
2523
+ coordinate = resolved.coordinate;
2141
2524
  }
2142
2525
  else {
2143
2526
  const artifact = this.getArtifact(artifactId);
@@ -2146,16 +2529,29 @@ export class SourceService {
2146
2529
  provenance = artifact.provenance;
2147
2530
  qualityFlags = artifact.qualityFlags;
2148
2531
  binaryJarPath = artifact.binaryJarPath;
2532
+ sourceJarPath = artifact.sourceJarPath;
2149
2533
  version = artifact.version;
2534
+ coordinate = artifact.coordinate;
2150
2535
  }
2151
- if (requestedMapping !== "official" && !version) {
2536
+ version = await this.resolveVersionContext({
2537
+ version,
2538
+ provenance,
2539
+ coordinate,
2540
+ projectPath: input.projectPath,
2541
+ preferProjectVersion: input.preferProjectVersion,
2542
+ warnings
2543
+ });
2544
+ if (requestedMapping !== "obfuscated" && !version) {
2152
2545
  throw createError({
2153
2546
  code: ERROR_CODES.MAPPING_NOT_APPLIED,
2154
- message: `Non-official mapping "${requestedMapping}" requires a version, but none was resolved.`,
2547
+ message: `Non-obfuscated mapping "${requestedMapping}" requires a version, but none was resolved.`,
2155
2548
  details: {
2156
2549
  mapping: requestedMapping,
2157
- nextAction: "Resolve with targetKind=version or specify a versioned coordinate.",
2158
- suggestedCall: { tool: "resolve-artifact", params: { targetKind: "version", targetValue: "latest" } }
2550
+ nextAction: "Resolve with target: { kind: \"version\", value: ... } or specify a versioned coordinate.",
2551
+ suggestedCall: {
2552
+ tool: "resolve-artifact",
2553
+ params: buildResolveArtifactParams({ kind: "version", value: "latest" })
2554
+ }
2159
2555
  }
2160
2556
  });
2161
2557
  }
@@ -2166,33 +2562,39 @@ export class SourceService {
2166
2562
  details: {
2167
2563
  artifactId,
2168
2564
  className,
2169
- nextAction: "Resolve with targetKind=jar or targetKind=version, or use an artifact that has a binary jar."
2565
+ nextAction: "Resolve with target: { kind: \"jar\" | \"version\", value: ... } or use an artifact that has a binary jar."
2170
2566
  }
2171
2567
  });
2172
2568
  }
2173
- const officialClassName = version != null
2174
- ? await this.resolveToOfficialClassName(className, version, requestedMapping, input.sourcePriority, warnings)
2175
- : className;
2569
+ const lookupClassName = await this.resolveClassNameForLookup({
2570
+ className,
2571
+ version,
2572
+ sourceMapping: requestedMapping,
2573
+ targetMapping: mappingApplied,
2574
+ sourcePriority: input.sourcePriority,
2575
+ warnings,
2576
+ context: "binary lookup"
2577
+ });
2176
2578
  const signature = await this.explorerService.getSignature({
2177
- fqn: officialClassName,
2579
+ fqn: lookupClassName,
2178
2580
  jarPath: binaryJarPath,
2179
2581
  access,
2180
2582
  includeSynthetic,
2181
2583
  includeInherited,
2182
- memberPattern: requestedMapping === "official" ? memberPattern : undefined
2584
+ memberPattern: requestedMapping === mappingApplied ? memberPattern : undefined
2183
2585
  });
2184
2586
  warnings.push(...signature.warnings);
2185
2587
  let remappedConstructors = version != null
2186
- ? (await this.remapSignatureMembers(signature.constructors, "method", version, requestedMapping, input.sourcePriority, warnings)).members
2588
+ ? (await this.remapSignatureMembers(signature.constructors, "method", version, mappingApplied, requestedMapping, input.sourcePriority, warnings)).members
2187
2589
  : signature.constructors;
2188
2590
  let remappedFields = version != null
2189
- ? (await this.remapSignatureMembers(signature.fields, "field", version, requestedMapping, input.sourcePriority, warnings)).members
2591
+ ? (await this.remapSignatureMembers(signature.fields, "field", version, mappingApplied, requestedMapping, input.sourcePriority, warnings)).members
2190
2592
  : signature.fields;
2191
2593
  let remappedMethods = version != null
2192
- ? (await this.remapSignatureMembers(signature.methods, "method", version, requestedMapping, input.sourcePriority, warnings)).members
2594
+ ? (await this.remapSignatureMembers(signature.methods, "method", version, mappingApplied, requestedMapping, input.sourcePriority, warnings)).members
2193
2595
  : signature.methods;
2194
- // Apply memberPattern post-remap for non-official mappings
2195
- if (requestedMapping !== "official" && memberPattern) {
2596
+ // Apply memberPattern after remap when the lookup namespace differs from the requested namespace.
2597
+ if (requestedMapping !== mappingApplied && memberPattern) {
2196
2598
  const lowerPattern = memberPattern.toLowerCase();
2197
2599
  remappedConstructors = remappedConstructors.filter((m) => m.name.toLowerCase().includes(lowerPattern));
2198
2600
  remappedFields = remappedFields.filter((m) => m.name.toLowerCase().includes(lowerPattern));
@@ -2242,98 +2644,169 @@ export class SourceService {
2242
2644
  artifactId,
2243
2645
  requestedMapping,
2244
2646
  mappingApplied,
2647
+ returnedNamespace: requestedMapping,
2245
2648
  provenance: normalizedProvenance,
2246
2649
  qualityFlags,
2650
+ artifactContents: this.buildArtifactContentsSummary({
2651
+ origin,
2652
+ sourceJarPath,
2653
+ isDecompiled: origin === "decompiled",
2654
+ qualityFlags
2655
+ }),
2247
2656
  warnings
2248
2657
  };
2249
2658
  }
2250
2659
  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];
2660
+ const { input: sourceInput, ...sharedInput } = input;
2661
+ const mode = sourceInput.mode;
2662
+ if (mode === "inline") {
2663
+ const singleResult = await this.validateMixinSingle({
2664
+ ...sharedInput,
2665
+ source: sourceInput.source
2666
+ });
2667
+ return this.applyValidateMixinOutputCompaction(this.buildValidateMixinOutput(mode, [
2668
+ {
2669
+ source: {
2670
+ kind: "inline",
2671
+ label: "<inline>"
2672
+ },
2673
+ result: singleResult
2290
2674
  }
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
- }
2675
+ ]), input);
2676
+ }
2677
+ if (mode === "path") {
2678
+ const resolvedPath = this.resolveMixinInputPath(sourceInput.path, "path");
2679
+ const singleResult = await this.validateMixinSingle({
2680
+ ...sharedInput,
2681
+ sourcePath: sourceInput.path
2682
+ });
2683
+ return this.applyValidateMixinOutputCompaction(this.buildValidateMixinOutput(mode, [
2684
+ {
2685
+ source: {
2686
+ kind: "path",
2687
+ label: resolvedPath,
2688
+ path: resolvedPath
2689
+ },
2690
+ result: singleResult
2304
2691
  }
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
- }
2692
+ ]), input);
2693
+ }
2694
+ if (mode === "paths") {
2695
+ return this.validateMixinMany(mode, sourceInput.paths.map((path) => ({
2696
+ source: {
2697
+ kind: "path",
2698
+ label: this.resolveMixinInputPath(path, "path"),
2699
+ path: this.resolveMixinInputPath(path, "path")
2700
+ },
2701
+ sourcePath: path
2702
+ })), input);
2703
+ }
2704
+ const resolvedInput = mode === "project"
2705
+ ? this.createProjectValidateMixinConfigInput(input)
2706
+ : input;
2707
+ const configSources = await this.resolveMixinConfigSources(resolvedInput);
2708
+ if (configSources.length === 0) {
2709
+ throw createError({
2710
+ code: ERROR_CODES.INVALID_INPUT,
2711
+ message: "Mixin config(s) contain no mixin class entries."
2712
+ });
2713
+ }
2714
+ return this.validateMixinMany(mode, configSources.map((entry) => ({
2715
+ source: {
2716
+ kind: "config",
2717
+ label: entry.sourcePath,
2718
+ path: entry.sourcePath,
2719
+ configPath: entry.configPath
2720
+ },
2721
+ sourcePath: entry.sourcePath
2722
+ })), resolvedInput);
2723
+ }
2724
+ createProjectValidateMixinConfigInput(input) {
2725
+ if (input.input.mode !== "project") {
2726
+ return input;
2727
+ }
2728
+ const resolvedProjectPath = this.resolveMixinInputPath(input.input.path, "path");
2729
+ const configPaths = fastGlob.sync(["**/*.mixins.json"], {
2730
+ cwd: resolvedProjectPath,
2731
+ absolute: true,
2732
+ onlyFiles: true,
2733
+ ignore: [...MIXIN_PROJECT_DISCOVERY_IGNORES]
2734
+ }).sort((left, right) => left.localeCompare(right));
2735
+ if (configPaths.length === 0) {
2736
+ throw createError({
2737
+ code: ERROR_CODES.INVALID_INPUT,
2738
+ message: `No mixin config JSON files were found under project path "${input.input.path}".`,
2739
+ details: {
2740
+ nextAction: "Use input.mode='config' with explicit configPaths[], or point input.path at the workspace root that contains *.mixins.json files."
2322
2741
  }
2742
+ });
2743
+ }
2744
+ return {
2745
+ ...input,
2746
+ projectPath: input.projectPath ?? resolvedProjectPath,
2747
+ input: {
2748
+ mode: "config",
2749
+ configPaths
2323
2750
  }
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 });
2751
+ };
2752
+ }
2753
+ shouldRetryValidateMixinWithMavenFirst(input, result) {
2754
+ const initialPriority = input.retryState?.initialSourcePriority ?? input.sourcePriority ?? this.config.mappingSourcePriority;
2755
+ if (input.retryState?.attempted || initialPriority !== "loom-first") {
2756
+ return false;
2757
+ }
2758
+ if (result.validationStatus !== "partial") {
2759
+ return false;
2331
2760
  }
2332
- // Batch mode: delegate to validateMixinBatch when sourcePaths is provided
2333
- if (input.sourcePaths && input.sourcePaths.length > 0) {
2334
- return this.validateMixinBatch(input);
2761
+ if (result.summary.membersSkipped > 0) {
2762
+ return true;
2335
2763
  }
2764
+ return result.issues.some((issue) => issue.resolutionPath === "source-signature-unavailable" ||
2765
+ issue.resolutionPath === "target-mapping-failed" ||
2766
+ issue.resolutionPath === "member-remap-failed");
2767
+ }
2768
+ findValidateMixinClassMapping(input) {
2769
+ const cache = input.batchCaches?.classMappings;
2770
+ if (!cache) {
2771
+ return this.mappingService.findMapping({
2772
+ version: input.version,
2773
+ kind: "class",
2774
+ name: input.className,
2775
+ sourceMapping: input.sourceMapping,
2776
+ targetMapping: input.targetMapping,
2777
+ sourcePriority: input.sourcePriority
2778
+ });
2779
+ }
2780
+ const cacheKey = [
2781
+ input.version,
2782
+ input.className,
2783
+ input.sourceMapping,
2784
+ input.targetMapping,
2785
+ input.sourcePriority
2786
+ ].join("\0");
2787
+ const cached = cache.get(cacheKey);
2788
+ if (cached) {
2789
+ return cached;
2790
+ }
2791
+ const pending = this.mappingService.findMapping({
2792
+ version: input.version,
2793
+ kind: "class",
2794
+ name: input.className,
2795
+ sourceMapping: input.sourceMapping,
2796
+ targetMapping: input.targetMapping,
2797
+ sourcePriority: input.sourcePriority
2798
+ }).catch((error) => {
2799
+ cache.delete(cacheKey);
2800
+ throw error;
2801
+ });
2802
+ cache.set(cacheKey, pending);
2803
+ return pending;
2804
+ }
2805
+ async validateMixinSingle(input) {
2336
2806
  let version = input.version.trim();
2807
+ const requestedScope = normalizeRequestedArtifactScope(input.scope);
2808
+ const currentSourcePriority = input.sourcePriority ?? this.config.mappingSourcePriority;
2809
+ const initialSourcePriority = input.retryState?.initialSourcePriority ?? currentSourcePriority;
2337
2810
  if (!version) {
2338
2811
  throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "version must be non-empty." });
2339
2812
  }
@@ -2394,22 +2867,25 @@ export class SourceService {
2394
2867
  }
2395
2868
  // Resolve jar: use Loom cache for non-vanilla scope with projectPath
2396
2869
  let jarPath;
2870
+ let resolvedArtifact;
2871
+ let signatureLookupMapping = "obfuscated";
2397
2872
  let scopeFallback;
2398
2873
  if (input.scope && input.scope !== "vanilla" && input.projectPath) {
2399
2874
  try {
2400
- const resolved = await this.resolveArtifact({
2875
+ resolvedArtifact = await this.resolveArtifact({
2401
2876
  target: { kind: "version", value: version },
2402
2877
  mapping: requestedMapping,
2403
- sourcePriority: input.sourcePriority,
2878
+ sourcePriority: currentSourcePriority,
2404
2879
  projectPath: input.projectPath,
2405
2880
  scope: input.scope,
2406
2881
  preferProjectVersion: false
2407
2882
  });
2408
- jarPath = resolved.binaryJarPath ?? (await this.versionService.resolveVersionJar(version)).jarPath;
2409
- warnings.push(...resolved.warnings);
2410
- mappingApplied = resolved.mappingApplied;
2411
- if (resolved.version) {
2412
- version = resolved.version;
2883
+ jarPath = resolvedArtifact.binaryJarPath ?? (await this.versionService.resolveVersionJar(version)).jarPath;
2884
+ warnings.push(...resolvedArtifact.warnings);
2885
+ mappingApplied = resolvedArtifact.mappingApplied;
2886
+ signatureLookupMapping = resolvedArtifact.mappingApplied;
2887
+ if (resolvedArtifact.version) {
2888
+ version = resolvedArtifact.version;
2413
2889
  }
2414
2890
  }
2415
2891
  catch (scopeErr) {
@@ -2430,6 +2906,7 @@ export class SourceService {
2430
2906
  if (jarPath.includes("-sources.jar")) {
2431
2907
  warnings.push(`Resolved jar appears to be a sources jar. Falling back to vanilla client jar.`);
2432
2908
  jarPath = (await this.versionService.resolveVersionJar(version)).jarPath;
2909
+ signatureLookupMapping = "obfuscated";
2433
2910
  scopeFallback = {
2434
2911
  requested: input.scope ?? "vanilla",
2435
2912
  applied: "vanilla",
@@ -2442,7 +2919,7 @@ export class SourceService {
2442
2919
  const health = await this.mappingService.checkMappingHealth({
2443
2920
  version,
2444
2921
  requestedMapping,
2445
- sourcePriority: input.sourcePriority
2922
+ sourcePriority: currentSourcePriority
2446
2923
  });
2447
2924
  const jarAvailable = existsSync(jarPath);
2448
2925
  healthReport = {
@@ -2489,51 +2966,51 @@ export class SourceService {
2489
2966
  }
2490
2967
  }
2491
2968
  }
2492
- let officialName = resolvedClassName;
2493
- if (requestedMapping !== "official") {
2969
+ let obfuscatedName = resolvedClassName;
2970
+ if (requestedMapping !== signatureLookupMapping) {
2494
2971
  try {
2495
- const mapped = await this.mappingService.findMapping({
2972
+ const mapped = await this.findValidateMixinClassMapping({
2496
2973
  version,
2497
- kind: "class",
2498
- name: resolvedClassName,
2974
+ className: resolvedClassName,
2499
2975
  sourceMapping: requestedMapping,
2500
- targetMapping: "official",
2501
- sourcePriority: input.sourcePriority
2976
+ targetMapping: signatureLookupMapping,
2977
+ sourcePriority: currentSourcePriority,
2978
+ batchCaches: input.batchCaches
2502
2979
  });
2503
2980
  if (mapped.resolved && mapped.resolvedSymbol) {
2504
- officialName = mapped.resolvedSymbol.name;
2505
- resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: officialName, success: true });
2981
+ obfuscatedName = mapped.resolvedSymbol.name;
2982
+ resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: obfuscatedName, success: true });
2506
2983
  }
2507
2984
  else {
2508
- warnings.push(`Could not map class "${resolvedClassName}" from ${requestedMapping} to official; using "${officialName}" for lookup.`);
2985
+ warnings.push(`Could not map class "${resolvedClassName}" from ${requestedMapping} to ${signatureLookupMapping}; using "${obfuscatedName}" for lookup.`);
2509
2986
  mappingFailedTargets.add(target.className);
2510
- resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: officialName, success: false, detail: "No mapping found" });
2987
+ resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: obfuscatedName, success: false, detail: "No mapping found" });
2511
2988
  }
2512
2989
  }
2513
2990
  catch (mapErr) {
2514
- warnings.push(`Mapping lookup failed for class "${resolvedClassName}"; using "${officialName}" for lookup.`);
2991
+ warnings.push(`Mapping lookup failed for class "${resolvedClassName}" while preparing ${signatureLookupMapping} lookup; using "${obfuscatedName}" for lookup.`);
2515
2992
  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) });
2993
+ resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: obfuscatedName, success: false, detail: mapErr instanceof Error ? mapErr.message : String(mapErr) });
2517
2994
  }
2518
2995
  }
2519
2996
  try {
2520
2997
  const sig = await this.explorerService.getSignature({
2521
- fqn: officialName,
2998
+ fqn: obfuscatedName,
2522
2999
  jarPath,
2523
3000
  access: "all"
2524
3001
  });
2525
3002
  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 });
3003
+ resolutionTrace?.push({ target: target.className, step: "signature", input: obfuscatedName, output: `${sig.methods.length} methods, ${sig.fields.length} fields`, success: true });
2527
3004
  // Bug 2 fix: remap signature members to requested mapping
2528
3005
  let constructors = sig.constructors;
2529
3006
  let methods = sig.methods;
2530
3007
  let fields = sig.fields;
2531
- if (requestedMapping !== "official") {
3008
+ if (requestedMapping !== signatureLookupMapping) {
2532
3009
  try {
2533
3010
  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)
3011
+ this.remapSignatureMembers(sig.constructors, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings),
3012
+ this.remapSignatureMembers(sig.methods, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings),
3013
+ this.remapSignatureMembers(sig.fields, "field", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings)
2537
3014
  ]);
2538
3015
  constructors = ctorResult.members;
2539
3016
  methods = methodResult.members;
@@ -2555,9 +3032,17 @@ export class SourceService {
2555
3032
  }
2556
3033
  }
2557
3034
  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) });
3035
+ warnings.push(`Member remapping failed for "${resolvedClassName}"; falling back to ${signatureLookupMapping} names. ` +
3036
+ `Member names shown may be in the ${signatureLookupMapping} runtime namespace.`);
3037
+ mappingApplied = signatureLookupMapping;
3038
+ resolutionTrace?.push({
3039
+ target: target.className,
3040
+ step: "remap",
3041
+ input: resolvedClassName,
3042
+ output: `${signatureLookupMapping} fallback`,
3043
+ success: false,
3044
+ detail: remapErr instanceof Error ? remapErr.message : String(remapErr)
3045
+ });
2561
3046
  }
2562
3047
  }
2563
3048
  targetMembers.set(target.className, {
@@ -2568,17 +3053,15 @@ export class SourceService {
2568
3053
  });
2569
3054
  }
2570
3055
  catch (sigErr) {
2571
- warnings.push(`Could not load signature for class "${resolvedClassName}" (official: "${officialName}").`);
2572
- 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) });
3056
+ warnings.push(`Could not load signature for class "${resolvedClassName}" (obfuscated: "${obfuscatedName}").`);
3057
+ resolutionTrace?.push({ target: target.className, step: "signature", input: obfuscatedName, output: "CLASS_NOT_FOUND", success: false, detail: sigErr instanceof Error ? sigErr.message : String(sigErr) });
2574
3058
  // Fallback: check if the symbol exists in the mapping graph even though getSignature failed
2575
3059
  try {
2576
3060
  const existenceCheck = await this.mappingService.checkSymbolExists({
2577
3061
  version, kind: "class", name: resolvedClassName,
2578
- sourceMapping: requestedMapping, nameMode: "auto"
3062
+ sourceMapping: requestedMapping, nameMode: "auto", sourcePriority: currentSourcePriority
2579
3063
  });
2580
3064
  if (existenceCheck.resolved) {
2581
- signatureFailedTargets.delete(target.className);
2582
3065
  symbolExistsButSignatureFailed.add(target.className);
2583
3066
  resolutionTrace?.push({ target: target.className, step: "fallback-check", input: resolvedClassName, output: "exists in mapping graph", success: true });
2584
3067
  }
@@ -2587,23 +3070,35 @@ export class SourceService {
2587
3070
  }
2588
3071
  }
2589
3072
  catch {
2590
- // Fallback check failed — keep as signatureFailedTarget
3073
+ // Fallback check failed — treat as tool-limited partial validation.
3074
+ signatureFailedTargets.add(target.className);
2591
3075
  resolutionTrace?.push({ target: target.className, step: "fallback-check", input: resolvedClassName, output: "check failed", success: false });
2592
3076
  }
2593
3077
  }
2594
3078
  }
2595
3079
  // Fix toolHealth accuracy: reflect actual failures after target resolution
2596
3080
  if (healthReport) {
2597
- const hasFailures = signatureFailedTargets.size > 0 || mappingFailedTargets.size > 0;
3081
+ const hasFailures = signatureFailedTargets.size > 0 ||
3082
+ mappingFailedTargets.size > 0 ||
3083
+ symbolExistsButSignatureFailed.size > 0;
2598
3084
  if (hasFailures && healthReport.overallHealthy) {
2599
3085
  healthReport.overallHealthy = false;
2600
- healthReport.degradations.push(`${mappingFailedTargets.size} mapping failure(s), ${signatureFailedTargets.size} signature failure(s).`);
3086
+ healthReport.degradations.push(`${mappingFailedTargets.size} mapping failure(s), ${signatureFailedTargets.size} signature failure(s), ${symbolExistsButSignatureFailed.size} partial validation target(s).`);
2601
3087
  }
2602
3088
  }
2603
3089
  const resolutionNotes = [];
2604
3090
  if (requestedMapping !== mappingApplied) {
2605
3091
  resolutionNotes.push(`Mapping fallback: requested "${requestedMapping}" but applied "${mappingApplied}" due to remapping failure.`);
2606
3092
  }
3093
+ const appliedScope = inferAppliedArtifactScope({
3094
+ requestedScope,
3095
+ scopeFallback,
3096
+ jarPath,
3097
+ resolvedSourceJarPath: resolvedArtifact?.resolvedSourceJarPath
3098
+ });
3099
+ if (!scopeFallback && requestedScope !== appliedScope) {
3100
+ resolutionNotes.push(`Scope adjusted during validation: requested "${requestedScope}" but resolved artifact looks like "${appliedScope}".`);
3101
+ }
2607
3102
  // Count remap failures from warnings
2608
3103
  const REMAP_WARNING_RE = /^(?:Could not remap|Remap failed for)\b/;
2609
3104
  const remapFailures = warnings.filter((w) => REMAP_WARNING_RE.test(w)).length;
@@ -2617,26 +3112,30 @@ export class SourceService {
2617
3112
  }
2618
3113
  // Build mapping chain description
2619
3114
  const mappingChain = [];
2620
- if (requestedMapping !== "official") {
2621
- mappingChain.push(`${requestedMapping} → official`);
2622
- if (mappingApplied !== requestedMapping) {
2623
- mappingChain.push(`fallback to ${mappingApplied}`);
2624
- }
3115
+ if (requestedMapping !== signatureLookupMapping) {
3116
+ mappingChain.push(`${requestedMapping} → ${signatureLookupMapping}`);
3117
+ }
3118
+ if (mappingApplied !== signatureLookupMapping) {
3119
+ mappingChain.push(`fallback to ${mappingApplied}`);
2625
3120
  }
2626
3121
  const provenance = {
2627
3122
  version,
2628
3123
  jarPath,
2629
3124
  requestedMapping,
2630
3125
  mappingApplied,
3126
+ requestedScope,
3127
+ appliedScope,
3128
+ requestedSourcePriority: initialSourcePriority,
3129
+ appliedSourcePriority: currentSourcePriority,
2631
3130
  resolutionNotes: resolutionNotes.length > 0 ? resolutionNotes : undefined,
2632
- jarType: scopeFallback ? "vanilla-client" : (input.scope && input.scope !== "vanilla" && input.projectPath) ? "merged" : "vanilla-client",
3131
+ jarType: scopeToJarType(appliedScope),
2633
3132
  mappingChain: mappingChain.length > 0 ? mappingChain : undefined,
2634
3133
  remapFailures: remapFailures > 0 ? remapFailures : undefined,
2635
3134
  mappingAutoDetected: mappingAutoDetected || undefined,
2636
3135
  scopeFallback,
2637
3136
  resolutionTrace: resolutionTrace && resolutionTrace.length > 0 ? resolutionTrace : undefined
2638
3137
  };
2639
- const result = validateParsedMixin(parsed, targetMembers, warnings, provenance, confidence, mappingFailedTargets, input.explain, remapFailedMembers, signatureFailedTargets, input.explain ? { scope: input.scope, sourcePriority: input.sourcePriority, projectPath: input.projectPath, mapping: requestedMapping } : undefined, input.warningMode, healthReport, symbolExistsButSignatureFailed.size > 0 ? symbolExistsButSignatureFailed : undefined);
3138
+ const result = refreshMixinValidationOutcome(validateParsedMixin(parsed, targetMembers, warnings, provenance, confidence, mappingFailedTargets, input.explain, remapFailedMembers, signatureFailedTargets, input.explain ? { scope: requestedScope, sourcePriority: currentSourcePriority, projectPath: input.projectPath, mapping: requestedMapping } : undefined, input.warningMode, healthReport, symbolExistsButSignatureFailed.size > 0 ? symbolExistsButSignatureFailed : undefined));
2640
3139
  // Apply minSeverity / hideUncertain filters
2641
3140
  const minSeverity = input.minSeverity ?? "all";
2642
3141
  const hideUncertain = input.hideUncertain ?? false;
@@ -2669,7 +3168,6 @@ export class SourceService {
2669
3168
  parseWarnings: filteredParseWarnings
2670
3169
  };
2671
3170
  result.unfilteredSummary = unfilteredSummary;
2672
- result.valid = filteredDefiniteErrors === 0;
2673
3171
  }
2674
3172
  // Apply warningCategoryFilter
2675
3173
  if (input.warningCategoryFilter && input.warningCategoryFilter.length > 0) {
@@ -2693,7 +3191,6 @@ export class SourceService {
2693
3191
  resolutionErrors: result.issues.filter((i) => i.resolutionPath != null).length,
2694
3192
  parseWarnings: result.issues.filter((i) => i.category === "parse").length
2695
3193
  };
2696
- result.valid = catDefiniteErrors === 0;
2697
3194
  }
2698
3195
  // Apply treatInfoAsWarning filter
2699
3196
  if (input.treatInfoAsWarning === false && result.structuredWarnings) {
@@ -2707,51 +3204,217 @@ export class SourceService {
2707
3204
  result.structuredWarnings = undefined;
2708
3205
  result.aggregatedWarnings = undefined;
2709
3206
  result.toolHealth = undefined;
3207
+ result.confidenceBreakdown = undefined;
2710
3208
  if (result.provenance) {
2711
3209
  result.provenance.resolutionTrace = undefined;
2712
3210
  }
2713
3211
  }
3212
+ refreshMixinValidationOutcome(result);
3213
+ if (this.shouldRetryValidateMixinWithMavenFirst(input, result)) {
3214
+ const retryWarning = `Retrying validate-mixin with sourcePriority="maven-first" after partial validation using "${currentSourcePriority}".`;
3215
+ try {
3216
+ const retried = await this.validateMixinSingle({
3217
+ ...input,
3218
+ source,
3219
+ sourcePath: undefined,
3220
+ sourcePriority: "maven-first",
3221
+ retryState: {
3222
+ attempted: true,
3223
+ initialSourcePriority
3224
+ }
3225
+ });
3226
+ retried.warnings = [retryWarning, ...retried.warnings];
3227
+ if (retried.provenance) {
3228
+ retried.provenance.requestedSourcePriority = initialSourcePriority;
3229
+ retried.provenance.appliedSourcePriority = "maven-first";
3230
+ retried.provenance.resolutionNotes = [
3231
+ ...(retried.provenance.resolutionNotes ?? []),
3232
+ `Validation retried with sourcePriority "maven-first" after partial result from "${currentSourcePriority}".`
3233
+ ];
3234
+ }
3235
+ return refreshMixinValidationOutcome(retried);
3236
+ }
3237
+ catch (retryErr) {
3238
+ result.warnings.unshift(`${retryWarning} Retry failed: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`);
3239
+ return result;
3240
+ }
3241
+ }
2714
3242
  return result;
2715
3243
  }
2716
- async validateMixinBatch(input) {
2717
- const paths = input.sourcePaths;
3244
+ resolveMixinInputPath(rawPath, fieldName) {
3245
+ const normalizedPath = normalizePathForHost(rawPath, undefined, fieldName);
3246
+ return isAbsolute(normalizedPath)
3247
+ ? normalizedPath
3248
+ : resolvePath(process.cwd(), normalizedPath);
3249
+ }
3250
+ async resolveMixinConfigSources(input) {
3251
+ if (input.input.mode !== "config") {
3252
+ return [];
3253
+ }
2718
3254
  const results = [];
2719
- let validCount = 0;
2720
- let invalidCount = 0;
2721
- let errorCount = 0;
2722
- // P5: default warningMode to "aggregated" in batch mode
2723
- const batchWarningMode = input.warningMode ?? "aggregated";
2724
- for (const sp of paths) {
3255
+ for (const rawConfigPath of input.input.configPaths) {
3256
+ const resolvedConfigPath = this.resolveMixinInputPath(rawConfigPath, "configPath");
3257
+ let configJson;
2725
3258
  try {
2726
- const singleResult = await this.validateMixin({
2727
- ...input,
2728
- sourcePaths: undefined,
2729
- source: undefined,
2730
- sourcePath: sp,
2731
- warningMode: batchWarningMode
3259
+ const raw = await readFile(resolvedConfigPath, "utf-8");
3260
+ configJson = JSON.parse(raw);
3261
+ }
3262
+ catch (err) {
3263
+ throw createError({
3264
+ code: ERROR_CODES.INVALID_INPUT,
3265
+ message: `Could not read/parse mixin config "${rawConfigPath}": ${err instanceof Error ? err.message : String(err)}`
2732
3266
  });
2733
- results.push({ sourcePath: sp, result: singleResult });
2734
- if (singleResult.valid) {
2735
- validCount++;
2736
- }
2737
- else {
2738
- invalidCount++;
3267
+ }
3268
+ const pkg = configJson.package ?? "";
3269
+ const classNames = [
3270
+ ...(configJson.mixins ?? []),
3271
+ ...(configJson.client ?? []),
3272
+ ...(configJson.server ?? [])
3273
+ ];
3274
+ if (classNames.length === 0) {
3275
+ continue;
3276
+ }
3277
+ const projectBase = input.projectPath
3278
+ ? (isAbsolute(input.projectPath) ? input.projectPath : resolvePath(process.cwd(), input.projectPath))
3279
+ : dirname(resolvedConfigPath);
3280
+ let sourceRootCandidates;
3281
+ if (input.sourceRoots && input.sourceRoots.length > 0) {
3282
+ sourceRootCandidates = input.sourceRoots;
3283
+ }
3284
+ else {
3285
+ const detected = COMMON_SOURCE_ROOTS.filter((candidateRoot) => classNames.some((className) => {
3286
+ const fqcn = pkg ? `${pkg}.${className}` : className;
3287
+ const relative = fqcn.replace(/\./g, "/") + ".java";
3288
+ return existsSync(resolvePath(projectBase, candidateRoot, relative));
3289
+ }));
3290
+ sourceRootCandidates = detected.length > 0 ? detected : ["src/main/java"];
3291
+ }
3292
+ for (const cls of classNames) {
3293
+ const fqcn = pkg ? `${pkg}.${cls}` : cls;
3294
+ const relativePath = fqcn.replace(/\./g, "/") + ".java";
3295
+ let sourcePath = resolvePath(projectBase, sourceRootCandidates[0], relativePath);
3296
+ for (const root of sourceRootCandidates) {
3297
+ const candidate = resolvePath(projectBase, root, relativePath);
3298
+ if (existsSync(candidate)) {
3299
+ sourcePath = candidate;
3300
+ break;
3301
+ }
2739
3302
  }
3303
+ results.push({
3304
+ sourcePath,
3305
+ configPath: resolvedConfigPath
3306
+ });
3307
+ }
3308
+ }
3309
+ return results;
3310
+ }
3311
+ async validateMixinMany(mode, entries, input) {
3312
+ const results = [];
3313
+ const batchWarningMode = input.warningMode ?? "aggregated";
3314
+ const { input: _discardedInput, ...sharedInput } = input;
3315
+ const batchCaches = {
3316
+ classMappings: new Map()
3317
+ };
3318
+ for (const entry of entries) {
3319
+ try {
3320
+ const singleResult = await this.validateMixinSingle({
3321
+ ...sharedInput,
3322
+ sourcePath: entry.sourcePath,
3323
+ warningMode: batchWarningMode,
3324
+ batchCaches
3325
+ });
3326
+ results.push({
3327
+ source: entry.source,
3328
+ result: singleResult
3329
+ });
2740
3330
  }
2741
3331
  catch (err) {
2742
3332
  results.push({
2743
- sourcePath: sp,
3333
+ source: entry.source,
2744
3334
  error: err instanceof Error ? err.message : String(err)
2745
3335
  });
2746
- errorCount++;
2747
3336
  }
2748
3337
  }
2749
- // Build issueSummary: aggregate issues across all results by (kind, confidence, category)
3338
+ return this.applyValidateMixinOutputCompaction(this.buildValidateMixinOutput(mode, results), input);
3339
+ }
3340
+ applyValidateMixinOutputCompaction(output, input) {
3341
+ let nextOutput = output;
3342
+ const canHoistProvenance = nextOutput.provenance != null;
3343
+ const warningCandidates = nextOutput.results
3344
+ .map((entry) => entry.result?.warnings)
3345
+ .filter((entry) => entry != null);
3346
+ const canHoistWarnings = warningCandidates.length === 0
3347
+ ? true
3348
+ : warningCandidates.every((entry) => sameStringArray(entry, warningCandidates[0]));
3349
+ if (input.reportMode === "summary-first") {
3350
+ nextOutput = {
3351
+ ...nextOutput,
3352
+ results: nextOutput.results.map((entry) => (entry.result
3353
+ ? {
3354
+ ...entry,
3355
+ result: {
3356
+ ...entry.result,
3357
+ warnings: canHoistWarnings ? [] : entry.result.warnings,
3358
+ structuredWarnings: undefined,
3359
+ aggregatedWarnings: undefined,
3360
+ resolvedMembers: undefined,
3361
+ toolHealth: undefined,
3362
+ confidenceBreakdown: undefined,
3363
+ provenance: canHoistProvenance ? undefined : entry.result.provenance
3364
+ }
3365
+ }
3366
+ : entry))
3367
+ };
3368
+ }
3369
+ if (input.includeIssues !== false) {
3370
+ return nextOutput;
3371
+ }
3372
+ return {
3373
+ ...nextOutput,
3374
+ results: nextOutput.results.map((entry) => (entry.result
3375
+ ? {
3376
+ ...entry,
3377
+ result: {
3378
+ ...entry.result,
3379
+ issues: []
3380
+ }
3381
+ }
3382
+ : entry))
3383
+ };
3384
+ }
3385
+ buildValidateMixinOutput(mode, results) {
3386
+ let valid = 0;
3387
+ let partial = 0;
3388
+ let invalid = 0;
3389
+ let processingErrors = 0;
3390
+ let totalValidationErrors = 0;
3391
+ let totalValidationWarnings = 0;
3392
+ const warningSet = new Set();
3393
+ const incompleteReasonSet = new Set();
2750
3394
  const issueGroupMap = new Map();
2751
- for (const r of results) {
2752
- if (!r.result)
3395
+ for (const entry of results) {
3396
+ if (!entry.result) {
3397
+ processingErrors++;
2753
3398
  continue;
2754
- for (const issue of r.result.issues) {
3399
+ }
3400
+ if (entry.result.valid) {
3401
+ valid++;
3402
+ }
3403
+ else {
3404
+ invalid++;
3405
+ }
3406
+ if (entry.result.validationStatus === "partial") {
3407
+ partial++;
3408
+ }
3409
+ totalValidationErrors += entry.result.summary.errors;
3410
+ totalValidationWarnings += entry.result.summary.warnings;
3411
+ for (const warning of entry.result.warnings) {
3412
+ warningSet.add(warning);
3413
+ }
3414
+ for (const issue of entry.result.issues) {
3415
+ if (issue.kind === "validation-incomplete") {
3416
+ incompleteReasonSet.add(`validation-incomplete: ${issue.message}`);
3417
+ }
2755
3418
  const key = `${issue.kind}\0${issue.confidence ?? "unknown"}\0${issue.category ?? "validation"}`;
2756
3419
  const existing = issueGroupMap.get(key);
2757
3420
  if (existing) {
@@ -2771,39 +3434,37 @@ export class SourceService {
2771
3434
  }
2772
3435
  }
2773
3436
  }
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;
3437
+ const issueSummary = issueGroupMap.size > 0 ? [...issueGroupMap.values()] : undefined;
3438
+ const provenanceCandidates = results
3439
+ .map((entry) => entry.result?.provenance)
3440
+ .filter((entry) => entry != null);
3441
+ const provenance = provenanceCandidates.length === 0
3442
+ ? undefined
3443
+ : provenanceCandidates.every((entry) => sameMixinValidationProvenance(entry, provenanceCandidates[0]))
3444
+ ? provenanceCandidates[0]
3445
+ : undefined;
3446
+ const toolHealth = results.find((entry) => entry.result?.toolHealth)?.result?.toolHealth;
3447
+ const confidenceScores = results
3448
+ .map((entry) => entry.result?.confidenceScore)
3449
+ .filter((score) => score != null);
2793
3450
  return {
3451
+ mode,
2794
3452
  results,
2795
3453
  summary: {
2796
- total: paths.length,
2797
- valid: validCount,
2798
- invalid: invalidCount,
2799
- errors: errorCount,
2800
- processingErrors: errorCount,
3454
+ total: results.length,
3455
+ valid,
3456
+ partial,
3457
+ invalid,
3458
+ processingErrors,
2801
3459
  totalValidationErrors,
2802
- totalValidationWarnings,
2803
- confidenceScore: batchConfidenceScore
3460
+ totalValidationWarnings
2804
3461
  },
2805
3462
  issueSummary,
2806
- toolHealth: sharedHealth
3463
+ provenance,
3464
+ incompleteReasons: incompleteReasonSet.size > 0 ? [...incompleteReasonSet] : undefined,
3465
+ toolHealth,
3466
+ confidenceScore: confidenceScores.length > 0 ? Math.min(...confidenceScores) : undefined,
3467
+ warnings: [...warningSet]
2807
3468
  };
2808
3469
  }
2809
3470
  async validateAccessWidener(input) {
@@ -2828,7 +3489,7 @@ export class SourceService {
2828
3489
  if (overrideMapping && headerNamespace && overrideMapping !== headerNamespace) {
2829
3490
  warnings.push(`Using mapping override "${overrideMapping}" instead of header namespace "${headerNamespaceRaw}".`);
2830
3491
  }
2831
- const needsMapping = awNamespace !== "official";
3492
+ const needsMapping = awNamespace !== "obfuscated";
2832
3493
  // Collect unique class FQNs from entries
2833
3494
  const classFqns = new Set();
2834
3495
  for (const entry of parsed.entries) {
@@ -2837,7 +3498,7 @@ export class SourceService {
2837
3498
  }
2838
3499
  const membersByClass = new Map();
2839
3500
  for (const fqn of classFqns) {
2840
- let officialFqn = fqn;
3501
+ let obfuscatedFqn = fqn;
2841
3502
  if (needsMapping) {
2842
3503
  try {
2843
3504
  const mapped = await this.mappingService.findMapping({
@@ -2845,14 +3506,14 @@ export class SourceService {
2845
3506
  kind: "class",
2846
3507
  name: fqn,
2847
3508
  sourceMapping: awNamespace,
2848
- targetMapping: "official",
3509
+ targetMapping: "obfuscated",
2849
3510
  sourcePriority: input.sourcePriority
2850
3511
  });
2851
3512
  if (mapped.resolved && mapped.resolvedSymbol) {
2852
- officialFqn = mapped.resolvedSymbol.name;
3513
+ obfuscatedFqn = mapped.resolvedSymbol.name;
2853
3514
  }
2854
3515
  else {
2855
- warnings.push(`Could not map class "${fqn}" from ${awNamespace} to official.`);
3516
+ warnings.push(`Could not map class "${fqn}" from ${awNamespace} to obfuscated.`);
2856
3517
  }
2857
3518
  }
2858
3519
  catch {
@@ -2861,7 +3522,7 @@ export class SourceService {
2861
3522
  }
2862
3523
  try {
2863
3524
  const sig = await this.explorerService.getSignature({
2864
- fqn: officialFqn,
3525
+ fqn: obfuscatedFqn,
2865
3526
  jarPath,
2866
3527
  access: "all"
2867
3528
  });
@@ -2874,7 +3535,7 @@ export class SourceService {
2874
3535
  });
2875
3536
  }
2876
3537
  catch {
2877
- warnings.push(`Could not load signature for class "${officialFqn}".`);
3538
+ warnings.push(`Could not load signature for class "${obfuscatedFqn}".`);
2878
3539
  }
2879
3540
  }
2880
3541
  return validateParsedAccessWidener(parsed, membersByClass, warnings);
@@ -2915,7 +3576,7 @@ export class SourceService {
2915
3576
  },
2916
3577
  indexedAt: currentMeta.indexedAt,
2917
3578
  durationMs: 0,
2918
- mappingApplied: artifact.mappingApplied ?? "official"
3579
+ mappingApplied: artifact.mappingApplied ?? "obfuscated"
2919
3580
  };
2920
3581
  }
2921
3582
  const resolved = this.toResolvedArtifact(artifact);
@@ -2932,33 +3593,16 @@ export class SourceService {
2932
3593
  },
2933
3594
  indexedAt: rebuilt.indexedAt,
2934
3595
  durationMs: rebuilt.indexDurationMs,
2935
- mappingApplied: artifact.mappingApplied ?? "official"
3596
+ mappingApplied: artifact.mappingApplied ?? "obfuscated"
2936
3597
  };
2937
3598
  }
2938
- searchSymbolIntent(artifactId, query, match, scope, snippetWindow, regexPattern, onHit) {
3599
+ searchSymbolIntent(artifactId, query, match, scope, regexPattern, onHit) {
2939
3600
  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
3601
  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
3602
  onHit({
2956
3603
  filePath: item.symbol.filePath,
2957
3604
  score: item.score,
2958
3605
  matchedIn: "symbol",
2959
- startLine: snippet.startLine,
2960
- endLine: snippet.endLine,
2961
- snippet: snippet.snippet,
2962
3606
  reasonCodes: [`symbol_${match}`],
2963
3607
  symbol: {
2964
3608
  symbolKind: item.symbol.symbolKind,
@@ -2969,7 +3613,7 @@ export class SourceService {
2969
3613
  });
2970
3614
  }
2971
3615
  }
2972
- searchTextIntentIndexed(artifactId, query, match, scope, includeDefinition, snippetWindow, onHit) {
3616
+ searchTextIntentIndexed(artifactId, query, match, scope, onHit) {
2973
3617
  const candidateLimit = this.indexedCandidateLimitForMatch(match);
2974
3618
  const indexed = this.filesRepo.searchFileCandidates(artifactId, {
2975
3619
  query,
@@ -2998,45 +3642,21 @@ export class SourceService {
2998
3642
  if (contentIndex < 0) {
2999
3643
  continue;
3000
3644
  }
3001
- const line = indexToLine(candidate.content, contentIndex);
3002
3645
  candidateRows.push({
3003
3646
  filePath: candidate.filePath,
3004
- content: candidate.content,
3005
- line,
3006
3647
  contentIndex
3007
3648
  });
3008
3649
  }
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
3650
  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
3651
  onHit({
3028
3652
  filePath: candidate.filePath,
3029
- score: scoreTextMatch(match, candidate.contentIndex) + (resolvedSymbol ? 20 : 0),
3653
+ score: scoreTextMatch(match, candidate.contentIndex),
3030
3654
  matchedIn: "content",
3031
- startLine: snippet.startLine,
3032
- endLine: snippet.endLine,
3033
- snippet: snippet.snippet,
3034
- reasonCodes: ["content_match", `text_${match}`, "indexed"],
3035
- symbol: resolvedSymbol
3655
+ reasonCodes: ["content_match", `text_${match}`, "indexed"]
3036
3656
  });
3037
3657
  }
3038
3658
  }
3039
- searchPathIntentIndexed(artifactId, query, match, scope, includeDefinition, snippetWindow, onHit) {
3659
+ searchPathIntentIndexed(artifactId, query, match, scope, onHit) {
3040
3660
  const candidateLimit = this.indexedCandidateLimitForMatch(match);
3041
3661
  const indexed = this.filesRepo.searchFileCandidates(artifactId, {
3042
3662
  query,
@@ -3071,59 +3691,22 @@ export class SourceService {
3071
3691
  pathIndex
3072
3692
  });
3073
3693
  }
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
3694
  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
3695
  onHit({
3102
3696
  filePath: candidate.filePath,
3103
- score: scorePathMatch(match, candidate.pathIndex) + (resolvedSymbol ? 10 : 0),
3697
+ score: scorePathMatch(match, candidate.pathIndex),
3104
3698
  matchedIn: "path",
3105
- startLine: snippet.startLine,
3106
- endLine: snippet.endLine,
3107
- snippet: snippet.snippet,
3108
- reasonCodes: ["path_match", `path_${match}`, "indexed"],
3109
- symbol: resolvedSymbol
3699
+ reasonCodes: ["path_match", `path_${match}`, "indexed"]
3110
3700
  });
3111
3701
  }
3112
3702
  }
3113
- searchTextIntent(artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, onHit) {
3703
+ searchTextIntent(artifactId, query, match, scope, regexPattern, onHit) {
3114
3704
  const filePaths = this.loadScopedFilePaths(artifactId, scope);
3115
3705
  const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
3116
3706
  for (const chunk of chunkArray(filePaths, pageSize)) {
3117
3707
  const rows = this.filesRepo.getFileContentsByPaths(artifactId, chunk);
3118
3708
  this.metrics.recordSearchDbRoundtrip();
3119
3709
  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
3710
  for (const row of rows) {
3128
3711
  const contentIndex = match === "regex"
3129
3712
  ? matchRegexIndex(row.content, regexPattern)
@@ -3131,26 +3714,16 @@ export class SourceService {
3131
3714
  if (contentIndex < 0) {
3132
3715
  continue;
3133
3716
  }
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
3717
  onHit({
3141
3718
  filePath: row.filePath,
3142
- score: scoreTextMatch(match, contentIndex) + (resolvedSymbol ? 20 : 0),
3719
+ score: scoreTextMatch(match, contentIndex),
3143
3720
  matchedIn: "content",
3144
- startLine: snippet.startLine,
3145
- endLine: snippet.endLine,
3146
- snippet: snippet.snippet,
3147
- reasonCodes: ["content_match", `text_${match}`],
3148
- symbol: resolvedSymbol
3721
+ reasonCodes: ["content_match", `text_${match}`]
3149
3722
  });
3150
3723
  }
3151
3724
  }
3152
3725
  }
3153
- searchPathIntent(artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, onHit) {
3726
+ searchPathIntent(artifactId, query, match, scope, regexPattern, onHit) {
3154
3727
  const filePaths = this.loadScopedFilePaths(artifactId, scope);
3155
3728
  const matching = filePaths.flatMap((filePath) => {
3156
3729
  const pathIndex = match === "regex"
@@ -3163,37 +3736,12 @@ export class SourceService {
3163
3736
  });
3164
3737
  const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
3165
3738
  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
3739
  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
3740
  onHit({
3189
3741
  filePath: candidate.filePath,
3190
- score: scorePathMatch(match, candidate.pathIndex) + (resolvedSymbol ? 10 : 0),
3742
+ score: scorePathMatch(match, candidate.pathIndex),
3191
3743
  matchedIn: "path",
3192
- startLine: snippet.startLine,
3193
- endLine: snippet.endLine,
3194
- snippet: snippet.snippet,
3195
- reasonCodes: ["path_match", `path_${match}`],
3196
- symbol: resolvedSymbol
3744
+ reasonCodes: ["path_match", `path_${match}`]
3197
3745
  });
3198
3746
  }
3199
3747
  }
@@ -3233,15 +3781,13 @@ export class SourceService {
3233
3781
  this.metrics.recordSearchDbRoundtrip();
3234
3782
  this.metrics.recordSearchRowsScanned(candidates.length);
3235
3783
  const result = [];
3784
+ const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
3236
3785
  for (const symbol of candidates) {
3237
3786
  if (!checkPackagePrefix(symbol.filePath, scope?.packagePrefix)) {
3238
3787
  continue;
3239
3788
  }
3240
- if (scope?.fileGlob) {
3241
- const glob = buildGlobRegex(normalizePathStyle(scope.fileGlob));
3242
- if (!glob.test(symbol.filePath)) {
3243
- continue;
3244
- }
3789
+ if (glob && !glob.test(symbol.filePath)) {
3790
+ continue;
3245
3791
  }
3246
3792
  if (!isSymbolKind(symbol.symbolKind)) {
3247
3793
  continue;
@@ -3262,13 +3808,6 @@ export class SourceService {
3262
3808
  }
3263
3809
  loadScopedFilePaths(artifactId, scope) {
3264
3810
  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
3811
  const result = [];
3273
3812
  let cursor = undefined;
3274
3813
  const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
@@ -3283,9 +3822,6 @@ export class SourceService {
3283
3822
  if (glob && !glob.test(filePath)) {
3284
3823
  continue;
3285
3824
  }
3286
- if (scopedFilesBySymbolKind && !scopedFilesBySymbolKind.has(filePath)) {
3287
- continue;
3288
- }
3289
3825
  result.push(filePath);
3290
3826
  }
3291
3827
  if (!page.nextCursor) {
@@ -3345,161 +3881,37 @@ export class SourceService {
3345
3881
  if (innerIndex > 0) {
3346
3882
  candidates.add(`${classPath.slice(0, innerIndex)}.java`);
3347
3883
  }
3348
- for (const candidate of candidates) {
3349
- const row = this.filesRepo.getFileContent(artifactId, candidate);
3350
- if (row) {
3351
- return row.filePath;
3352
- }
3353
- }
3354
3884
  const simpleName = normalizedClassName.split(/[.$]/).at(-1);
3355
3885
  if (!simpleName) {
3356
3886
  return undefined;
3357
3887
  }
3358
- const classPathBySymbol = this.symbolsRepo.findBestClassFilePath(artifactId, normalizedClassName, simpleName);
3359
- if (classPathBySymbol && isPackageCompatible(classPathBySymbol, classPath)) {
3360
- return classPathBySymbol;
3361
- }
3362
- const byName = this.filesRepo.findFirstFilePathByName(artifactId, `${simpleName}.java`);
3363
- if (byName && isPackageCompatible(byName, classPath)) {
3364
- return byName;
3365
- }
3366
- return undefined;
3888
+ const lastSlash = classPath.lastIndexOf("/");
3889
+ const expectedPrefix = lastSlash < 0 ? "" : classPath.slice(0, lastSlash + 1);
3890
+ return this.filesRepo.findBestClassLookupPath(artifactId, [...candidates], normalizedClassName, simpleName, expectedPrefix);
3367
3891
  }
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 [];
3892
+ async resolveBinaryFallbackArtifact(input) {
3893
+ const binaryJarPath = normalizeOptionalString(input.binaryJarPath);
3894
+ if (!binaryJarPath) {
3895
+ return undefined;
3389
3896
  }
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
- }
3897
+ try {
3898
+ const fallbackResolved = await resolveSourceTargetInternal({ kind: "jar", value: binaryJarPath }, { allowDecompile: true, preferBinaryOnly: true }, this.config);
3899
+ fallbackResolved.version = fallbackResolved.version ?? input.version;
3900
+ fallbackResolved.coordinate = fallbackResolved.coordinate ?? input.coordinate;
3901
+ fallbackResolved.requestedMapping = input.requestedMapping;
3902
+ fallbackResolved.mappingApplied = input.mappingApplied;
3903
+ fallbackResolved.provenance = input.provenance;
3904
+ fallbackResolved.qualityFlags = dedupeQualityFlags([
3905
+ ...(fallbackResolved.qualityFlags ?? []),
3906
+ ...input.qualityFlags,
3907
+ "binary-fallback"
3908
+ ]);
3909
+ await this.ingestIfNeeded(fallbackResolved);
3910
+ return fallbackResolved;
3429
3911
  }
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
- }
3912
+ catch {
3913
+ return undefined;
3501
3914
  }
3502
- return relations;
3503
3915
  }
3504
3916
  buildProvenance(input) {
3505
3917
  const provenance = {
@@ -3551,66 +3963,158 @@ export class SourceService {
3551
3963
  transformChain
3552
3964
  };
3553
3965
  }
3554
- async resolveToOfficialClassName(className, version, mapping, sourcePriority, warnings) {
3555
- if (mapping === "official") {
3556
- return className;
3966
+ async resolveClassNameForLookup(input) {
3967
+ if (input.sourceMapping === input.targetMapping) {
3968
+ return input.className;
3969
+ }
3970
+ if (!input.version) {
3971
+ input.warnings.push(`Could not map class "${input.className}" from ${input.sourceMapping} to ${input.targetMapping} for ${input.context} because version is unavailable.`);
3972
+ return input.className;
3557
3973
  }
3558
3974
  try {
3559
3975
  const mapped = await this.mappingService.findMapping({
3560
- version,
3976
+ version: input.version,
3561
3977
  kind: "class",
3562
- name: className,
3563
- sourceMapping: mapping,
3564
- targetMapping: "official",
3565
- sourcePriority
3978
+ name: input.className,
3979
+ sourceMapping: input.sourceMapping,
3980
+ targetMapping: input.targetMapping,
3981
+ sourcePriority: input.sourcePriority
3566
3982
  });
3567
3983
  if (mapped.resolved && mapped.resolvedSymbol) {
3568
3984
  return mapped.resolvedSymbol.name;
3569
3985
  }
3570
- warnings.push(`Could not map class "${className}" from ${mapping} to official.`);
3986
+ input.warnings.push(`Could not map class "${input.className}" from ${input.sourceMapping} to ${input.targetMapping} for ${input.context}.`);
3571
3987
  }
3572
3988
  catch {
3573
- warnings.push(`Mapping lookup failed for class "${className}".`);
3989
+ input.warnings.push(`Mapping lookup failed for class "${input.className}" while preparing ${input.context} in ${input.targetMapping}.`);
3990
+ }
3991
+ return input.className;
3992
+ }
3993
+ buildClassSourceNotFoundError(input) {
3994
+ const simpleName = input.className.split(/[.$]/).at(-1) ?? input.className;
3995
+ const details = {
3996
+ artifactId: input.artifactId,
3997
+ className: input.className,
3998
+ mapping: input.mappingApplied,
3999
+ qualityFlags: input.qualityFlags,
4000
+ ...(input.lookupClassName !== input.className ? { lookupClassName: input.lookupClassName } : {}),
4001
+ ...(input.filePath ? { filePath: input.filePath } : {}),
4002
+ ...(input.scope ? { scope: input.scope } : {}),
4003
+ ...(input.targetKind ? { targetKind: input.targetKind } : {}),
4004
+ ...(input.targetValue ? { targetValue: input.targetValue } : {}),
4005
+ ...(input.attemptedBinaryFallback ? { binaryFallbackAttempted: true } : {})
4006
+ };
4007
+ let nextAction = `Use find-class to resolve the correct fully-qualified name for "${simpleName}".`;
4008
+ let suggestedCall = {
4009
+ tool: "find-class",
4010
+ params: { className: simpleName, artifactId: input.artifactId }
4011
+ };
4012
+ if (input.targetKind === "version" && input.scope && input.scope !== "merged" && !input.projectPath) {
4013
+ nextAction +=
4014
+ ` If the class exists in a modded environment, retry with scope: "merged" and projectPath pointing to your mod project.`;
4015
+ }
4016
+ else if (input.targetKind === "version" && input.scope && input.scope !== "merged" && input.projectPath) {
4017
+ nextAction += ` The class may exist in merged sources; retry with scope: "merged".`;
4018
+ }
4019
+ if (hasPartialNetMinecraftCoverage(input.qualityFlags)) {
4020
+ nextAction =
4021
+ `Resolved source coverage does not include net.minecraft for "${input.className}",` +
4022
+ (input.attemptedBinaryFallback
4023
+ ? " and binary fallback did not produce source for that class."
4024
+ : " and a binary fallback has not produced source for that class.") +
4025
+ " Use get-class-api-matrix or find-mapping instead of find-class for vanilla API discovery.";
4026
+ if (input.version) {
4027
+ suggestedCall = {
4028
+ tool: "get-class-api-matrix",
4029
+ params: {
4030
+ version: input.version,
4031
+ className: input.className,
4032
+ classNameMapping: input.requestedMapping
4033
+ }
4034
+ };
4035
+ }
4036
+ else {
4037
+ suggestedCall = {
4038
+ tool: "find-class",
4039
+ params: { className: simpleName, artifactId: input.artifactId }
4040
+ };
4041
+ }
3574
4042
  }
3575
- return className;
4043
+ if (input.mappingApplied === "obfuscated" && looksLikeDeobfuscatedClassName(input.className)) {
4044
+ nextAction += ` ${obfuscatedNamespaceHint(input.className)}`;
4045
+ }
4046
+ details.nextAction = nextAction;
4047
+ details.suggestedCall = suggestedCall;
4048
+ return createError({
4049
+ code: ERROR_CODES.CLASS_NOT_FOUND,
4050
+ message: `Source for class "${input.className}" was not found.`,
4051
+ details
4052
+ });
3576
4053
  }
3577
- async resolveToOfficialMemberName(name, ownerInSourceMapping, descriptor, kind, version, mapping, sourcePriority, warnings) {
3578
- if (mapping === "official") {
4054
+ async resolveToObfuscatedClassName(className, version, mapping, sourcePriority, warnings) {
4055
+ return this.resolveClassNameForLookup({
4056
+ className,
4057
+ version,
4058
+ sourceMapping: mapping,
4059
+ targetMapping: "obfuscated",
4060
+ sourcePriority,
4061
+ warnings,
4062
+ context: "bytecode lookup"
4063
+ });
4064
+ }
4065
+ async resolveToObfuscatedMemberName(name, ownerInSourceMapping, descriptor, kind, version, mapping, sourcePriority, warnings) {
4066
+ if (mapping === "obfuscated") {
3579
4067
  return {
3580
4068
  name,
3581
4069
  descriptor: kind === "method" ? descriptor : undefined
3582
4070
  };
3583
4071
  }
3584
4072
  try {
3585
- const mapped = await this.mappingService.findMapping({
3586
- version,
3587
- kind,
3588
- name,
3589
- owner: ownerInSourceMapping,
3590
- descriptor,
3591
- sourceMapping: mapping,
3592
- targetMapping: "official",
3593
- sourcePriority
3594
- });
4073
+ const canResolveMethodExactly = kind === "method" &&
4074
+ descriptor &&
4075
+ "resolveMethodMappingExact" in this.mappingService &&
4076
+ typeof this.mappingService.resolveMethodMappingExact === "function";
4077
+ const mapped = canResolveMethodExactly
4078
+ ? await this.mappingService.resolveMethodMappingExact({
4079
+ version,
4080
+ owner: ownerInSourceMapping,
4081
+ name,
4082
+ descriptor,
4083
+ sourceMapping: mapping,
4084
+ targetMapping: "obfuscated",
4085
+ sourcePriority
4086
+ })
4087
+ : await this.mappingService.findMapping({
4088
+ version,
4089
+ kind,
4090
+ name,
4091
+ owner: ownerInSourceMapping,
4092
+ descriptor,
4093
+ signatureMode: kind === "method" && !descriptor ? "name-only" : undefined,
4094
+ sourceMapping: mapping,
4095
+ targetMapping: "obfuscated",
4096
+ sourcePriority
4097
+ });
4098
+ warnings.push(...mapped.warnings);
3595
4099
  if (mapped.resolved && mapped.resolvedSymbol) {
3596
4100
  return {
3597
4101
  name: mapped.resolvedSymbol.name,
3598
4102
  descriptor: kind === "method" ? mapped.resolvedSymbol.descriptor ?? descriptor : undefined
3599
4103
  };
3600
4104
  }
3601
- warnings.push(`Could not map ${kind} "${name}" from ${mapping} to official.`);
4105
+ warnings.push(`Could not map ${kind} "${name}" from ${mapping} to obfuscated.`);
3602
4106
  }
3603
- catch {
3604
- warnings.push(`Mapping lookup failed for ${kind} "${name}".`);
4107
+ catch (caughtError) {
4108
+ warnings.push(`Mapping lookup failed for ${kind} "${name}": ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`);
3605
4109
  }
3606
4110
  return {
3607
4111
  name,
3608
4112
  descriptor: kind === "method" ? descriptor : undefined
3609
4113
  };
3610
4114
  }
3611
- async remapSignatureMembers(members, kind, version, mapping, sourcePriority, warnings) {
4115
+ async remapSignatureMembers(members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings) {
3612
4116
  const failedNames = new Set();
3613
- if (mapping === "official") {
4117
+ if (sourceMapping === targetMapping) {
3614
4118
  return { members, failedNames };
3615
4119
  }
3616
4120
  // Build deduplicated lookup tables for member names and owner FQNs
@@ -3619,35 +4123,35 @@ export class SourceService {
3619
4123
  for (const member of members) {
3620
4124
  const memberKey = `${member.ownerFqn}\0${member.name}\0${member.jvmDescriptor}`;
3621
4125
  if (!memberKeyToRemapped.has(memberKey)) {
3622
- memberKeyToRemapped.set(memberKey, member.name); // default = official name
4126
+ memberKeyToRemapped.set(memberKey, member.name); // default = obfuscated name
3623
4127
  }
3624
4128
  if (!ownerToRemapped.has(member.ownerFqn)) {
3625
- ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = official FQN
4129
+ ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = obfuscated FQN
3626
4130
  }
3627
4131
  }
3628
4132
  // Phase 1: Remap owner FQNs first (needed for member disambiguation)
3629
4133
  const ownerEntries = [...ownerToRemapped.entries()];
3630
- await Promise.all(ownerEntries.map(async ([officialFqn]) => {
4134
+ await Promise.all(ownerEntries.map(async ([obfuscatedFqn]) => {
3631
4135
  try {
3632
4136
  const mapped = await this.mappingService.findMapping({
3633
4137
  version,
3634
4138
  kind: "class",
3635
- name: officialFqn,
3636
- sourceMapping: "official",
3637
- targetMapping: mapping,
4139
+ name: obfuscatedFqn,
4140
+ sourceMapping,
4141
+ targetMapping,
3638
4142
  sourcePriority
3639
4143
  });
3640
4144
  if (mapped.resolved && mapped.resolvedSymbol) {
3641
- ownerToRemapped.set(officialFqn, mapped.resolvedSymbol.name);
4145
+ ownerToRemapped.set(obfuscatedFqn, mapped.resolvedSymbol.name);
3642
4146
  }
3643
4147
  }
3644
4148
  catch {
3645
- // keep official FQN as fallback
4149
+ // keep obfuscated FQN as fallback
3646
4150
  }
3647
4151
  }));
3648
4152
  // Phase 2: Remap member names using resolved owners for disambiguation
3649
4153
  const memberEntries = [...memberKeyToRemapped.entries()];
3650
- await Promise.all(memberEntries.map(async ([key, _officialName]) => {
4154
+ await Promise.all(memberEntries.map(async ([key, _obfuscatedName]) => {
3651
4155
  const [ownerFqn, name, descriptor] = key.split("\0");
3652
4156
  try {
3653
4157
  const targetOwner = ownerToRemapped.get(ownerFqn) ?? ownerFqn;
@@ -3657,8 +4161,8 @@ export class SourceService {
3657
4161
  name,
3658
4162
  owner: ownerFqn,
3659
4163
  descriptor: kind === "method" ? descriptor : undefined,
3660
- sourceMapping: "official",
3661
- targetMapping: mapping,
4164
+ sourceMapping,
4165
+ targetMapping,
3662
4166
  sourcePriority,
3663
4167
  disambiguation: { ownerHint: targetOwner }
3664
4168
  });
@@ -3677,17 +4181,17 @@ export class SourceService {
3677
4181
  }
3678
4182
  }
3679
4183
  else {
3680
- warnings.push(`Could not remap ${kind} "${name}" to ${mapping}.`);
4184
+ warnings.push(`Could not remap ${kind} "${name}" from ${sourceMapping} to ${targetMapping}.`);
3681
4185
  failedNames.add(name);
3682
4186
  }
3683
4187
  }
3684
4188
  else {
3685
- warnings.push(`Could not remap ${kind} "${name}" to ${mapping}.`);
4189
+ warnings.push(`Could not remap ${kind} "${name}" from ${sourceMapping} to ${targetMapping}.`);
3686
4190
  failedNames.add(name);
3687
4191
  }
3688
4192
  }
3689
4193
  catch {
3690
- warnings.push(`Remap failed for ${kind} "${name}".`);
4194
+ warnings.push(`Remap failed for ${kind} "${name}" from ${sourceMapping} to ${targetMapping}.`);
3691
4195
  failedNames.add(name);
3692
4196
  }
3693
4197
  }));
@@ -3780,6 +4284,7 @@ export class SourceService {
3780
4284
  });
3781
4285
  });
3782
4286
  tx();
4287
+ this.upsertCacheMetrics(resolved.artifactId, rebuilt.totalContentBytes, timestamp);
3783
4288
  log("info", "index.rebuild.done", {
3784
4289
  artifactId: resolved.artifactId,
3785
4290
  reason,
@@ -3860,7 +4365,8 @@ export class SourceService {
3860
4365
  files,
3861
4366
  symbols,
3862
4367
  indexedAt: new Date().toISOString(),
3863
- indexDurationMs: Date.now() - indexStartedAt
4368
+ indexDurationMs: Date.now() - indexStartedAt,
4369
+ totalContentBytes: files.reduce((sum, file) => sum + file.contentBytes, 0)
3864
4370
  };
3865
4371
  }
3866
4372
  getArtifact(artifactId) {
@@ -3880,7 +4386,10 @@ export class SourceService {
3880
4386
  details: {
3881
4387
  artifactId,
3882
4388
  nextAction: "Use resolve-artifact to resolve a source artifact first.",
3883
- suggestedCall: { tool: "resolve-artifact", params: { targetKind: "version", targetValue: "latest" } }
4389
+ suggestedCall: {
4390
+ tool: "resolve-artifact",
4391
+ params: buildResolveArtifactParams({ kind: "version", value: "latest" })
4392
+ }
3884
4393
  }
3885
4394
  });
3886
4395
  }
@@ -3898,8 +4407,9 @@ export class SourceService {
3898
4407
  });
3899
4408
  if (existing && reason === "already_current") {
3900
4409
  this.metrics.recordArtifactCacheHit();
3901
- this.artifactsRepo.touchArtifact(resolved.artifactId, new Date().toISOString());
3902
- this.refreshCacheMetrics();
4410
+ const touchedAt = new Date().toISOString();
4411
+ this.artifactsRepo.touchArtifact(resolved.artifactId, touchedAt);
4412
+ this.touchCacheMetrics(resolved.artifactId, touchedAt);
3903
4413
  return;
3904
4414
  }
3905
4415
  this.metrics.recordArtifactCacheMiss();
@@ -3910,7 +4420,6 @@ export class SourceService {
3910
4420
  });
3911
4421
  await this.rebuildAndPersistArtifactIndex(resolved, reason === "already_current" ? "missing_meta" : reason);
3912
4422
  this.enforceCacheLimits();
3913
- this.refreshCacheMetrics();
3914
4423
  }
3915
4424
  async loadFromSourceJar(sourceJarPath) {
3916
4425
  const files = [];
@@ -3928,21 +4437,22 @@ export class SourceService {
3928
4437
  return this.filesRepo.listFiles(artifactId, { limit: 1 }).items.length > 0;
3929
4438
  }
3930
4439
  enforceCacheLimits() {
3931
- let artifactCount = this.artifactsRepo.countArtifacts();
3932
- let totalBytes = this.artifactsRepo.totalContentBytes();
4440
+ let artifactCount = this.cacheMetricsState.entries;
4441
+ let totalBytes = this.cacheMetricsState.totalContentBytes;
3933
4442
  if (artifactCount <= this.config.maxArtifacts && totalBytes <= this.config.maxCacheBytes) {
3934
4443
  return;
3935
4444
  }
3936
- const candidates = this.artifactsRepo.listArtifactsByLruWithContentBytes(Math.max(artifactCount, 1));
4445
+ const candidates = [...this.cacheMetricsState.lru];
3937
4446
  for (const candidate of candidates) {
3938
4447
  const shouldEvict = artifactCount > this.config.maxArtifacts || totalBytes > this.config.maxCacheBytes;
3939
4448
  if (!shouldEvict || artifactCount <= 1) {
3940
- return;
4449
+ break;
3941
4450
  }
3942
4451
  const artifactCountBefore = artifactCount;
3943
4452
  const totalBytesBefore = totalBytes;
3944
4453
  this.filesRepo.deleteFilesForArtifact(candidate.artifactId);
3945
4454
  this.artifactsRepo.deleteArtifact(candidate.artifactId);
4455
+ this.removeCacheMetrics(candidate.artifactId, false);
3946
4456
  artifactCount = Math.max(0, artifactCount - 1);
3947
4457
  totalBytes = Math.max(0, totalBytes - candidate.totalContentBytes);
3948
4458
  this.metrics.recordCacheEviction();
@@ -3953,6 +4463,7 @@ export class SourceService {
3953
4463
  artifactBytes: candidate.totalContentBytes
3954
4464
  });
3955
4465
  }
4466
+ this.publishCacheMetrics();
3956
4467
  }
3957
4468
  refreshCacheMetrics() {
3958
4469
  const cacheEntries = this.artifactsRepo.countArtifacts();
@@ -3960,13 +4471,83 @@ export class SourceService {
3960
4471
  const lruAccounting = this.artifactsRepo
3961
4472
  .listArtifactsByLruWithContentBytes(Math.max(cacheEntries, 1))
3962
4473
  .map((row) => ({
4474
+ artifactId: row.artifactId,
4475
+ totalContentBytes: row.totalContentBytes,
4476
+ updatedAt: row.updatedAt
4477
+ }));
4478
+ this.cacheMetricsState = {
4479
+ entries: cacheEntries,
4480
+ totalContentBytes,
4481
+ lru: lruAccounting
4482
+ };
4483
+ this.publishCacheMetrics();
4484
+ }
4485
+ touchCacheMetrics(artifactId, updatedAt) {
4486
+ const existingIndex = this.cacheMetricsState.lru.findIndex((row) => row.artifactId === artifactId);
4487
+ if (existingIndex < 0) {
4488
+ this.refreshCacheMetrics();
4489
+ return;
4490
+ }
4491
+ const [existing] = this.cacheMetricsState.lru.splice(existingIndex, 1);
4492
+ if (!existing) {
4493
+ this.refreshCacheMetrics();
4494
+ return;
4495
+ }
4496
+ existing.updatedAt = updatedAt;
4497
+ this.cacheMetricsState.lru.push(existing);
4498
+ this.publishCacheMetrics();
4499
+ }
4500
+ upsertCacheMetrics(artifactId, totalContentBytes, updatedAt) {
4501
+ const normalizedBytes = Math.max(0, Math.trunc(totalContentBytes));
4502
+ const existingIndex = this.cacheMetricsState.lru.findIndex((row) => row.artifactId === artifactId);
4503
+ if (existingIndex >= 0) {
4504
+ const [existing] = this.cacheMetricsState.lru.splice(existingIndex, 1);
4505
+ if (!existing) {
4506
+ this.refreshCacheMetrics();
4507
+ return;
4508
+ }
4509
+ this.cacheMetricsState.totalContentBytes = Math.max(0, this.cacheMetricsState.totalContentBytes - existing.totalContentBytes + normalizedBytes);
4510
+ existing.totalContentBytes = normalizedBytes;
4511
+ existing.updatedAt = updatedAt;
4512
+ this.cacheMetricsState.lru.push(existing);
4513
+ }
4514
+ else {
4515
+ this.cacheMetricsState.entries += 1;
4516
+ this.cacheMetricsState.totalContentBytes += normalizedBytes;
4517
+ this.cacheMetricsState.lru.push({
4518
+ artifactId,
4519
+ totalContentBytes: normalizedBytes,
4520
+ updatedAt
4521
+ });
4522
+ }
4523
+ this.cacheMetricsState.entries = this.cacheMetricsState.lru.length;
4524
+ this.publishCacheMetrics();
4525
+ }
4526
+ removeCacheMetrics(artifactId, publish = true) {
4527
+ const existingIndex = this.cacheMetricsState.lru.findIndex((row) => row.artifactId === artifactId);
4528
+ if (existingIndex < 0) {
4529
+ this.refreshCacheMetrics();
4530
+ return;
4531
+ }
4532
+ const [existing] = this.cacheMetricsState.lru.splice(existingIndex, 1);
4533
+ if (!existing) {
4534
+ this.refreshCacheMetrics();
4535
+ return;
4536
+ }
4537
+ this.cacheMetricsState.entries = this.cacheMetricsState.lru.length;
4538
+ this.cacheMetricsState.totalContentBytes = Math.max(0, this.cacheMetricsState.totalContentBytes - existing.totalContentBytes);
4539
+ if (publish) {
4540
+ this.publishCacheMetrics();
4541
+ }
4542
+ }
4543
+ publishCacheMetrics() {
4544
+ this.metrics.setCacheEntries(this.cacheMetricsState.entries);
4545
+ this.metrics.setCacheTotalContentBytes(this.cacheMetricsState.totalContentBytes);
4546
+ this.metrics.setCacheArtifactByteAccounting(this.cacheMetricsState.lru.map((row) => ({
3963
4547
  artifact_id: row.artifactId,
3964
4548
  content_bytes: row.totalContentBytes,
3965
4549
  updated_at: row.updatedAt
3966
- }));
3967
- this.metrics.setCacheEntries(cacheEntries);
3968
- this.metrics.setCacheTotalContentBytes(totalContentBytes);
3969
- this.metrics.setCacheArtifactByteAccounting(lruAccounting);
4550
+ })));
3970
4551
  }
3971
4552
  }
3972
4553
  //# sourceMappingURL=source-service.js.map