@adhisang/minecraft-modding-mcp 2.0.0 → 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 (37) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +109 -29
  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 +392 -33
  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.js +9 -1
  10. package/dist/mapping-service.d.ts +9 -0
  11. package/dist/mapping-service.js +183 -60
  12. package/dist/minecraft-explorer-service.d.ts +0 -1
  13. package/dist/minecraft-explorer-service.js +119 -23
  14. package/dist/mixin-validator.d.ts +24 -2
  15. package/dist/mixin-validator.js +223 -98
  16. package/dist/mod-decompile-service.d.ts +5 -0
  17. package/dist/mod-decompile-service.js +40 -5
  18. package/dist/mod-remap-service.js +142 -30
  19. package/dist/path-resolver.js +41 -4
  20. package/dist/registry-service.d.ts +10 -1
  21. package/dist/registry-service.js +154 -22
  22. package/dist/search-hit-accumulator.js +23 -2
  23. package/dist/source-jar-reader.js +16 -2
  24. package/dist/source-resolver.js +6 -7
  25. package/dist/source-service.d.ts +42 -4
  26. package/dist/source-service.js +781 -127
  27. package/dist/stdio-supervisor.d.ts +46 -0
  28. package/dist/stdio-supervisor.js +349 -0
  29. package/dist/storage/files-repo.d.ts +3 -9
  30. package/dist/storage/files-repo.js +66 -43
  31. package/dist/symbols/symbol-extractor.js +6 -4
  32. package/dist/tool-execution-gate.d.ts +15 -0
  33. package/dist/tool-execution-gate.js +58 -0
  34. package/dist/version-diff-service.js +10 -5
  35. package/dist/version-service.js +7 -2
  36. package/dist/workspace-mapping-service.js +12 -0
  37. package/package.json +1 -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,7 +172,9 @@ 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
  }
83
180
  function looksLikeDeobfuscatedClassName(value) {
@@ -97,6 +194,15 @@ function obfuscatedNamespaceHint(className) {
97
194
  function hasPartialNetMinecraftCoverage(qualityFlags) {
98
195
  return qualityFlags.includes("partial-source-no-net-minecraft");
99
196
  }
197
+ function buildResolveArtifactParams(target, extra = {}) {
198
+ return {
199
+ target: {
200
+ kind: target.kind,
201
+ value: target.value
202
+ },
203
+ ...extra
204
+ };
205
+ }
100
206
  function normalizeOptionalProjectPath(projectPath) {
101
207
  if (!projectPath) {
102
208
  return undefined;
@@ -108,19 +214,68 @@ function normalizeOptionalProjectPath(projectPath) {
108
214
  const normalized = normalizePathForHost(trimmed, undefined, "projectPath");
109
215
  return isAbsolute(normalized) ? normalized : resolvePath(process.cwd(), normalized);
110
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
+ }
111
225
  function buildVersionSourceSearchRoots(projectPath) {
112
226
  const roots = new Set();
113
227
  if (projectPath) {
114
228
  roots.add(resolvePath(projectPath, ".gradle", "loom-cache"));
115
229
  roots.add(resolvePath(projectPath, ".gradle-user", "caches", "fabric-loom"));
116
230
  roots.add(resolvePath(projectPath, ".gradle", "caches", "fabric-loom"));
117
- 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"));
118
234
  }
119
- const homeGradle = resolvePath(homedir(), ".gradle");
235
+ const homeGradle = resolveGradleUserHomePath();
120
236
  roots.add(resolvePath(homeGradle, "loom-cache"));
121
237
  roots.add(resolvePath(homeGradle, "caches", "fabric-loom"));
122
238
  return [...roots];
123
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
+ }
124
279
  function parseQualifiedMethodSymbol(symbol) {
125
280
  const trimmed = symbol.trim();
126
281
  const separator = trimmed.lastIndexOf(".");
@@ -183,6 +338,13 @@ const COMMON_SOURCE_ROOTS = [
183
338
  "quilt/src/main/java",
184
339
  "quilt/src/client/java"
185
340
  ];
341
+ const MIXIN_PROJECT_DISCOVERY_IGNORES = [
342
+ "**/.git/**",
343
+ "**/.gradle/**",
344
+ "**/build/**",
345
+ "**/out/**",
346
+ "**/node_modules/**"
347
+ ];
186
348
  function normalizeMapping(mapping) {
187
349
  if (mapping == null) {
188
350
  return "obfuscated";
@@ -251,11 +413,11 @@ function sortDiffMemberChanges(changes) {
251
413
  if (keyCompare !== 0) {
252
414
  return keyCompare;
253
415
  }
254
- const fromOwnerCompare = left.from.ownerFqn.localeCompare(right.from.ownerFqn);
416
+ const fromOwnerCompare = (left.from?.ownerFqn ?? "").localeCompare(right.from?.ownerFqn ?? "");
255
417
  if (fromOwnerCompare !== 0) {
256
418
  return fromOwnerCompare;
257
419
  }
258
- return left.to.ownerFqn.localeCompare(right.to.ownerFqn);
420
+ return (left.to?.ownerFqn ?? "").localeCompare(right.to?.ownerFqn ?? "");
259
421
  });
260
422
  }
261
423
  function changedMemberFields(fromMember, toMember, includeDescriptor) {
@@ -328,6 +490,16 @@ function emptyDiffDelta() {
328
490
  modified: []
329
491
  };
330
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
+ }
331
503
  function normalizeIntent(intent) {
332
504
  if (intent === "path" || intent === "text") {
333
505
  return intent;
@@ -354,6 +526,12 @@ function canUseIndexedSearchPath(indexedSearchEnabled, intent, match, _scope) {
354
526
  return true;
355
527
  }
356
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
+ }
357
535
  const REGEX_META = /[-/\\^$+.()|[\]{}]/;
358
536
  let result = "";
359
537
  let i = 0;
@@ -380,7 +558,7 @@ function buildGlobRegex(pattern) {
380
558
  i += 1;
381
559
  }
382
560
  }
383
- return new RegExp(`^${result}$`);
561
+ return rememberCachedRegex(GLOB_REGEX_CACHE, pattern, new RegExp(`^${result}$`));
384
562
  }
385
563
  function globToSqlLike(pattern) {
386
564
  let result = "";
@@ -556,6 +734,11 @@ export class SourceService {
556
734
  versionDiffService;
557
735
  modDecompileService;
558
736
  modSearchService;
737
+ cacheMetricsState = {
738
+ entries: 0,
739
+ totalContentBytes: 0,
740
+ lru: []
741
+ };
559
742
  constructor(explicitConfig, metrics = new RuntimeMetrics()) {
560
743
  this.config = explicitConfig ?? loadConfig();
561
744
  this.metrics = metrics;
@@ -615,14 +798,18 @@ export class SourceService {
615
798
  continue;
616
799
  }
617
800
  const hasMinecraftNamespace = javaEntries.some((entry) => normalizePathStyle(entry).startsWith("net/minecraft/"));
618
- 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) +
619
805
  (lower.includes("minecraft-merged") ? 2_000 : 0) +
620
- (lower.includes(input.version.toLowerCase()) ? 1_000 : 0) +
806
+ (exactVersionMatch ? 1_000 : 0) +
621
807
  Math.min(javaEntries.length, 500);
622
808
  candidates.push({
623
809
  jarPath: normalizedPath,
624
810
  javaEntryCount: javaEntries.length,
625
811
  hasMinecraftNamespace,
812
+ looksLikeMinecraftArtifact,
626
813
  score
627
814
  });
628
815
  }
@@ -633,7 +820,8 @@ export class SourceService {
633
820
  }
634
821
  return left.jarPath.localeCompare(right.jarPath);
635
822
  });
636
- 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);
637
825
  const candidateArtifacts = candidates
638
826
  .slice(0, 20)
639
827
  .map((candidate) => `${candidate.jarPath}#java=${candidate.javaEntryCount}#net_minecraft=${candidate.hasMinecraftNamespace ? 1 : 0}`);
@@ -651,6 +839,58 @@ export class SourceService {
651
839
  : "";
652
840
  return `${prefix}./gradlew genSources --no-daemon`;
653
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
+ }
654
894
  async resolveArtifact(input) {
655
895
  const kind = input.target.kind;
656
896
  let value = input.target.value?.trim();
@@ -762,7 +1002,7 @@ export class SourceService {
762
1002
  if (isVanillaMojang && input.projectPath) {
763
1003
  suggestedCall = {
764
1004
  tool: "resolve-artifact",
765
- params: { targetKind: kind, targetValue: value, mapping: "mojang", scope: "merged", projectPath: input.projectPath }
1005
+ params: buildResolveArtifactParams({ kind, value }, { mapping: "mojang", scope: "merged", projectPath: input.projectPath })
766
1006
  };
767
1007
  nextAction =
768
1008
  "scope=vanilla blocks Loom cache discovery needed for mojang mapping. " +
@@ -771,7 +1011,7 @@ export class SourceService {
771
1011
  else if (isVanillaMojang) {
772
1012
  suggestedCall = {
773
1013
  tool: "resolve-artifact",
774
- params: { targetKind: kind, targetValue: value, mapping: "obfuscated", scope: "vanilla" }
1014
+ params: buildResolveArtifactParams({ kind, value }, { mapping: "obfuscated", scope: "vanilla" })
775
1015
  };
776
1016
  nextAction =
777
1017
  "scope=vanilla blocks Loom cache discovery needed for mojang mapping. " +
@@ -780,7 +1020,7 @@ export class SourceService {
780
1020
  else {
781
1021
  suggestedCall = {
782
1022
  tool: "resolve-artifact",
783
- params: { targetKind: kind, targetValue: value, mapping: "obfuscated", ...(scope ? { scope } : {}) }
1023
+ params: buildResolveArtifactParams({ kind, value }, { mapping: "obfuscated", ...(scope ? { scope } : {}) })
784
1024
  };
785
1025
  nextAction = "Retry with mapping=obfuscated to use the runtime obfuscated namespace.";
786
1026
  }
@@ -789,6 +1029,7 @@ export class SourceService {
789
1029
  message: caughtError.message,
790
1030
  details: {
791
1031
  ...(caughtError.details ?? {}),
1032
+ artifactOrigin: resolved.origin,
792
1033
  searchedPaths: versionSourceDiscovery?.searchedPaths ?? [],
793
1034
  candidateArtifacts: versionSourceDiscovery?.candidateArtifacts ?? resolved.adjacentSourceCandidates ?? [],
794
1035
  recommendedCommand: this.buildVersionSourceRecoveryCommand(input.projectPath),
@@ -808,8 +1049,11 @@ export class SourceService {
808
1049
  details: {
809
1050
  mapping: effectiveMapping,
810
1051
  target: { kind, value },
811
- nextAction: "Use targetKind=version or a versioned Maven coordinate so mapping artifacts can be resolved.",
812
- 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
+ }
813
1057
  }
814
1058
  });
815
1059
  }
@@ -852,7 +1096,10 @@ export class SourceService {
852
1096
  selectedSourceJar: versionSourceDiscovery.selectedSourceJarPath,
853
1097
  candidateArtifacts: versionSourceDiscovery.candidateArtifacts,
854
1098
  nextAction: "Use strictVersion=false (default) to allow approximation, or ensure the exact version source jar is in the Loom cache.",
855
- 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
+ }
856
1103
  }
857
1104
  });
858
1105
  }
@@ -860,7 +1107,7 @@ export class SourceService {
860
1107
  warnings.push(`Requested version "${value}" but resolved source jar does not contain exact version string: ${versionSourceDiscovery.selectedSourceJarPath}`);
861
1108
  }
862
1109
  }
863
- resolved.qualityFlags = [...new Set(resolved.qualityFlags)];
1110
+ resolved.qualityFlags = dedupeQualityFlags(resolved.qualityFlags);
864
1111
  await this.ingestIfNeeded(resolved);
865
1112
  let sampleEntries;
866
1113
  if (resolved.sourceJarPath) {
@@ -890,6 +1137,12 @@ export class SourceService {
890
1137
  provenance,
891
1138
  qualityFlags: resolved.qualityFlags,
892
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
+ }),
893
1146
  warnings,
894
1147
  sampleEntries
895
1148
  };
@@ -920,7 +1173,14 @@ export class SourceService {
920
1173
  if (!query) {
921
1174
  return {
922
1175
  hits: [],
923
- mappingApplied: artifact.mappingApplied ?? "obfuscated"
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
+ })
924
1184
  };
925
1185
  }
926
1186
  const intent = normalizeIntent(input.intent);
@@ -1044,7 +1304,14 @@ export class SourceService {
1044
1304
  return {
1045
1305
  hits: page,
1046
1306
  nextCursor,
1047
- mappingApplied: artifact.mappingApplied ?? "obfuscated"
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
+ })
1048
1315
  };
1049
1316
  }
1050
1317
  finally {
@@ -1081,7 +1348,14 @@ export class SourceService {
1081
1348
  content,
1082
1349
  contentBytes: fullBytes,
1083
1350
  truncated,
1084
- mappingApplied: artifact.mappingApplied ?? "obfuscated"
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
+ })
1085
1359
  };
1086
1360
  }
1087
1361
  finally {
@@ -1093,15 +1367,29 @@ export class SourceService {
1093
1367
  try {
1094
1368
  const artifact = this.getArtifact(input.artifactId);
1095
1369
  const limit = clampLimit(input.limit, 200, 2000);
1370
+ const warnings = [];
1096
1371
  const page = this.filesRepo.listFiles(artifact.artifactId, {
1097
1372
  limit,
1098
1373
  cursor: input.cursor,
1099
1374
  prefix: input.prefix
1100
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
+ }
1101
1382
  return {
1102
1383
  items: page.items,
1103
1384
  nextCursor: page.nextCursor,
1104
- mappingApplied: artifact.mappingApplied ?? "obfuscated"
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
1105
1393
  };
1106
1394
  }
1107
1395
  finally {
@@ -1240,6 +1528,7 @@ export class SourceService {
1240
1528
  resolved: false,
1241
1529
  status: "mapping_unavailable",
1242
1530
  candidates: [],
1531
+ candidateCount: 0,
1243
1532
  workspaceDetection,
1244
1533
  warnings
1245
1534
  };
@@ -1255,7 +1544,8 @@ export class SourceService {
1255
1544
  descriptor: methodDescriptor,
1256
1545
  sourceMapping: input.sourceMapping,
1257
1546
  targetMapping: mappingApplied,
1258
- sourcePriority: input.sourcePriority
1547
+ sourcePriority: input.sourcePriority,
1548
+ maxCandidates: input.maxCandidates
1259
1549
  });
1260
1550
  return {
1261
1551
  ...exact,
@@ -1285,6 +1575,7 @@ export class SourceService {
1285
1575
  resolved: false,
1286
1576
  status: "not_found",
1287
1577
  candidates: [],
1578
+ candidateCount: 0,
1288
1579
  workspaceDetection,
1289
1580
  warnings: [...warnings, ...matrix.warnings]
1290
1581
  };
@@ -1312,6 +1603,7 @@ export class SourceService {
1312
1603
  status: "resolved",
1313
1604
  resolvedSymbol,
1314
1605
  candidates: [resolvedCandidate],
1606
+ candidateCount: 1,
1315
1607
  workspaceDetection,
1316
1608
  warnings: [...warnings, ...matrix.warnings]
1317
1609
  };
@@ -1324,7 +1616,8 @@ export class SourceService {
1324
1616
  descriptor,
1325
1617
  sourceMapping: input.sourceMapping,
1326
1618
  targetMapping: mappingApplied,
1327
- sourcePriority: input.sourcePriority
1619
+ sourcePriority: input.sourcePriority,
1620
+ maxCandidates: input.maxCandidates
1328
1621
  });
1329
1622
  const filtered = mapped.candidates.filter((candidate) => candidate.kind === kind);
1330
1623
  let status;
@@ -1347,6 +1640,8 @@ export class SourceService {
1347
1640
  status,
1348
1641
  resolvedSymbol: status === "resolved" ? filtered[0] : undefined,
1349
1642
  candidates: filtered,
1643
+ candidateCount: mapped.candidateCount,
1644
+ candidatesTruncated: mapped.candidatesTruncated,
1350
1645
  workspaceDetection,
1351
1646
  warnings: [...warnings, ...mapped.warnings]
1352
1647
  };
@@ -1517,6 +1812,7 @@ export class SourceService {
1517
1812
  const className = input.className.trim();
1518
1813
  const fromVersion = input.fromVersion.trim();
1519
1814
  const toVersion = input.toVersion.trim();
1815
+ const includeFullDiff = input.includeFullDiff ?? true;
1520
1816
  if (!className || !fromVersion || !toVersion) {
1521
1817
  throw createError({
1522
1818
  code: ERROR_CODES.INVALID_INPUT,
@@ -1690,11 +1986,39 @@ export class SourceService {
1690
1986
  this.remapSignatureMembers(delta.removed, kind, fromVersion, "obfuscated", mapping, input.sourcePriority, warnings)
1691
1987
  ]);
1692
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
+ }
1693
2002
  const [fromResult, toResult] = await Promise.all([
1694
2003
  this.remapSignatureMembers([change.from], kind, fromVersion, "obfuscated", mapping, input.sourcePriority, warnings),
1695
2004
  this.remapSignatureMembers([change.to], kind, toVersion, "obfuscated", mapping, input.sourcePriority, warnings)
1696
2005
  ]);
1697
- 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 };
1698
2022
  }));
1699
2023
  return { added: addedResult.members, removed: removedResult.members, modified: remappedModified };
1700
2024
  };
@@ -1737,9 +2061,9 @@ export class SourceService {
1737
2061
  toVersion
1738
2062
  },
1739
2063
  classChange,
1740
- constructors: remappedConstructors,
1741
- methods: remappedMethods,
1742
- fields: remappedFields,
2064
+ constructors: includeFullDiff ? remappedConstructors : compactDiffDelta(remappedConstructors),
2065
+ methods: includeFullDiff ? remappedMethods : compactDiffDelta(remappedMethods),
2066
+ fields: includeFullDiff ? remappedFields : compactDiffDelta(remappedFields),
1743
2067
  summary,
1744
2068
  warnings
1745
2069
  };
@@ -1868,7 +2192,7 @@ export class SourceService {
1868
2192
  if (normalizedArtifactId && input.target) {
1869
2193
  throw createError({
1870
2194
  code: ERROR_CODES.INVALID_INPUT,
1871
- message: "artifactId and targetKind/targetValue are mutually exclusive.",
2195
+ message: "artifactId and target are mutually exclusive.",
1872
2196
  details: {
1873
2197
  artifactId: normalizedArtifactId,
1874
2198
  target: input.target
@@ -1927,6 +2251,14 @@ export class SourceService {
1927
2251
  version = artifact.version;
1928
2252
  coordinate = artifact.coordinate;
1929
2253
  }
2254
+ version = await this.resolveVersionContext({
2255
+ version,
2256
+ provenance,
2257
+ coordinate,
2258
+ projectPath: input.projectPath,
2259
+ preferProjectVersion: input.preferProjectVersion,
2260
+ warnings
2261
+ });
1930
2262
  let activeArtifactId = artifactId;
1931
2263
  let activeOrigin = origin;
1932
2264
  let activeProvenance = provenance;
@@ -1963,7 +2295,7 @@ export class SourceService {
1963
2295
  activeOrigin = fallbackResolved.origin;
1964
2296
  activeMappingApplied = fallbackResolved.mappingApplied ?? activeMappingApplied;
1965
2297
  activeProvenance = fallbackResolved.provenance ?? activeProvenance;
1966
- activeQualityFlags = [...new Set([...(fallbackResolved.qualityFlags ?? []), "binary-fallback"])];
2298
+ activeQualityFlags = dedupeQualityFlags([...(fallbackResolved.qualityFlags ?? []), "binary-fallback"]);
1967
2299
  activeSourceJarPath = fallbackResolved.sourceJarPath;
1968
2300
  warnings.push(`Falling back to binary artifact "${normalizedBinaryJarPath}" because source coverage for "${className}" was incomplete.`);
1969
2301
  if (activeMappingApplied !== requestedMapping) {
@@ -2110,8 +2442,15 @@ export class SourceService {
2110
2442
  artifactId: activeArtifactId,
2111
2443
  requestedMapping,
2112
2444
  mappingApplied: activeMappingApplied,
2445
+ returnedNamespace: activeMappingApplied,
2113
2446
  provenance: normalizedProvenance,
2114
2447
  qualityFlags: activeQualityFlags,
2448
+ artifactContents: this.buildArtifactContentsSummary({
2449
+ origin: activeOrigin,
2450
+ sourceJarPath: activeSourceJarPath,
2451
+ isDecompiled: activeOrigin === "decompiled",
2452
+ qualityFlags: activeQualityFlags
2453
+ }),
2115
2454
  ...(resolvedOutputFile ? { outputFile: resolvedOutputFile } : {}),
2116
2455
  warnings
2117
2456
  };
@@ -2135,7 +2474,7 @@ export class SourceService {
2135
2474
  if (normalizedArtifactId && input.target) {
2136
2475
  throw createError({
2137
2476
  code: ERROR_CODES.INVALID_INPUT,
2138
- message: "artifactId and targetKind/targetValue are mutually exclusive.",
2477
+ message: "artifactId and target are mutually exclusive.",
2139
2478
  details: {
2140
2479
  artifactId: normalizedArtifactId,
2141
2480
  target: input.target
@@ -2149,6 +2488,8 @@ export class SourceService {
2149
2488
  let provenance;
2150
2489
  let qualityFlags = [];
2151
2490
  let binaryJarPath;
2491
+ let sourceJarPath;
2492
+ let coordinate;
2152
2493
  if (parsedMaxMembers != null && parsedMaxMembers > 5000) {
2153
2494
  warnings.push(`maxMembers was clamped to 5000 from ${parsedMaxMembers}.`);
2154
2495
  }
@@ -2177,7 +2518,9 @@ export class SourceService {
2177
2518
  provenance = resolved.provenance;
2178
2519
  qualityFlags = [...resolved.qualityFlags];
2179
2520
  binaryJarPath = resolved.binaryJarPath;
2521
+ sourceJarPath = resolved.resolvedSourceJarPath;
2180
2522
  version = resolved.version;
2523
+ coordinate = resolved.coordinate;
2181
2524
  }
2182
2525
  else {
2183
2526
  const artifact = this.getArtifact(artifactId);
@@ -2186,16 +2529,29 @@ export class SourceService {
2186
2529
  provenance = artifact.provenance;
2187
2530
  qualityFlags = artifact.qualityFlags;
2188
2531
  binaryJarPath = artifact.binaryJarPath;
2532
+ sourceJarPath = artifact.sourceJarPath;
2189
2533
  version = artifact.version;
2534
+ coordinate = artifact.coordinate;
2190
2535
  }
2536
+ version = await this.resolveVersionContext({
2537
+ version,
2538
+ provenance,
2539
+ coordinate,
2540
+ projectPath: input.projectPath,
2541
+ preferProjectVersion: input.preferProjectVersion,
2542
+ warnings
2543
+ });
2191
2544
  if (requestedMapping !== "obfuscated" && !version) {
2192
2545
  throw createError({
2193
2546
  code: ERROR_CODES.MAPPING_NOT_APPLIED,
2194
2547
  message: `Non-obfuscated mapping "${requestedMapping}" requires a version, but none was resolved.`,
2195
2548
  details: {
2196
2549
  mapping: requestedMapping,
2197
- nextAction: "Resolve with targetKind=version or specify a versioned coordinate.",
2198
- 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
+ }
2199
2555
  }
2200
2556
  });
2201
2557
  }
@@ -2206,7 +2562,7 @@ export class SourceService {
2206
2562
  details: {
2207
2563
  artifactId,
2208
2564
  className,
2209
- 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."
2210
2566
  }
2211
2567
  });
2212
2568
  }
@@ -2288,8 +2644,15 @@ export class SourceService {
2288
2644
  artifactId,
2289
2645
  requestedMapping,
2290
2646
  mappingApplied,
2647
+ returnedNamespace: requestedMapping,
2291
2648
  provenance: normalizedProvenance,
2292
2649
  qualityFlags,
2650
+ artifactContents: this.buildArtifactContentsSummary({
2651
+ origin,
2652
+ sourceJarPath,
2653
+ isDecompiled: origin === "decompiled",
2654
+ qualityFlags
2655
+ }),
2293
2656
  warnings
2294
2657
  };
2295
2658
  }
@@ -2301,7 +2664,7 @@ export class SourceService {
2301
2664
  ...sharedInput,
2302
2665
  source: sourceInput.source
2303
2666
  });
2304
- return this.buildValidateMixinOutput(mode, [
2667
+ return this.applyValidateMixinOutputCompaction(this.buildValidateMixinOutput(mode, [
2305
2668
  {
2306
2669
  source: {
2307
2670
  kind: "inline",
@@ -2309,7 +2672,7 @@ export class SourceService {
2309
2672
  },
2310
2673
  result: singleResult
2311
2674
  }
2312
- ]);
2675
+ ]), input);
2313
2676
  }
2314
2677
  if (mode === "path") {
2315
2678
  const resolvedPath = this.resolveMixinInputPath(sourceInput.path, "path");
@@ -2317,7 +2680,7 @@ export class SourceService {
2317
2680
  ...sharedInput,
2318
2681
  sourcePath: sourceInput.path
2319
2682
  });
2320
- return this.buildValidateMixinOutput(mode, [
2683
+ return this.applyValidateMixinOutputCompaction(this.buildValidateMixinOutput(mode, [
2321
2684
  {
2322
2685
  source: {
2323
2686
  kind: "path",
@@ -2326,7 +2689,7 @@ export class SourceService {
2326
2689
  },
2327
2690
  result: singleResult
2328
2691
  }
2329
- ]);
2692
+ ]), input);
2330
2693
  }
2331
2694
  if (mode === "paths") {
2332
2695
  return this.validateMixinMany(mode, sourceInput.paths.map((path) => ({
@@ -2338,7 +2701,10 @@ export class SourceService {
2338
2701
  sourcePath: path
2339
2702
  })), input);
2340
2703
  }
2341
- const configSources = await this.resolveMixinConfigSources(input);
2704
+ const resolvedInput = mode === "project"
2705
+ ? this.createProjectValidateMixinConfigInput(input)
2706
+ : input;
2707
+ const configSources = await this.resolveMixinConfigSources(resolvedInput);
2342
2708
  if (configSources.length === 0) {
2343
2709
  throw createError({
2344
2710
  code: ERROR_CODES.INVALID_INPUT,
@@ -2353,10 +2719,94 @@ export class SourceService {
2353
2719
  configPath: entry.configPath
2354
2720
  },
2355
2721
  sourcePath: entry.sourcePath
2356
- })), input);
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."
2741
+ }
2742
+ });
2743
+ }
2744
+ return {
2745
+ ...input,
2746
+ projectPath: input.projectPath ?? resolvedProjectPath,
2747
+ input: {
2748
+ mode: "config",
2749
+ configPaths
2750
+ }
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;
2760
+ }
2761
+ if (result.summary.membersSkipped > 0) {
2762
+ return true;
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;
2357
2804
  }
2358
2805
  async validateMixinSingle(input) {
2359
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;
2360
2810
  if (!version) {
2361
2811
  throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "version must be non-empty." });
2362
2812
  }
@@ -2417,22 +2867,25 @@ export class SourceService {
2417
2867
  }
2418
2868
  // Resolve jar: use Loom cache for non-vanilla scope with projectPath
2419
2869
  let jarPath;
2870
+ let resolvedArtifact;
2871
+ let signatureLookupMapping = "obfuscated";
2420
2872
  let scopeFallback;
2421
2873
  if (input.scope && input.scope !== "vanilla" && input.projectPath) {
2422
2874
  try {
2423
- const resolved = await this.resolveArtifact({
2875
+ resolvedArtifact = await this.resolveArtifact({
2424
2876
  target: { kind: "version", value: version },
2425
2877
  mapping: requestedMapping,
2426
- sourcePriority: input.sourcePriority,
2878
+ sourcePriority: currentSourcePriority,
2427
2879
  projectPath: input.projectPath,
2428
2880
  scope: input.scope,
2429
2881
  preferProjectVersion: false
2430
2882
  });
2431
- jarPath = resolved.binaryJarPath ?? (await this.versionService.resolveVersionJar(version)).jarPath;
2432
- warnings.push(...resolved.warnings);
2433
- mappingApplied = resolved.mappingApplied;
2434
- if (resolved.version) {
2435
- 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;
2436
2889
  }
2437
2890
  }
2438
2891
  catch (scopeErr) {
@@ -2453,6 +2906,7 @@ export class SourceService {
2453
2906
  if (jarPath.includes("-sources.jar")) {
2454
2907
  warnings.push(`Resolved jar appears to be a sources jar. Falling back to vanilla client jar.`);
2455
2908
  jarPath = (await this.versionService.resolveVersionJar(version)).jarPath;
2909
+ signatureLookupMapping = "obfuscated";
2456
2910
  scopeFallback = {
2457
2911
  requested: input.scope ?? "vanilla",
2458
2912
  applied: "vanilla",
@@ -2465,7 +2919,7 @@ export class SourceService {
2465
2919
  const health = await this.mappingService.checkMappingHealth({
2466
2920
  version,
2467
2921
  requestedMapping,
2468
- sourcePriority: input.sourcePriority
2922
+ sourcePriority: currentSourcePriority
2469
2923
  });
2470
2924
  const jarAvailable = existsSync(jarPath);
2471
2925
  healthReport = {
@@ -2513,28 +2967,28 @@ export class SourceService {
2513
2967
  }
2514
2968
  }
2515
2969
  let obfuscatedName = resolvedClassName;
2516
- if (requestedMapping !== "obfuscated") {
2970
+ if (requestedMapping !== signatureLookupMapping) {
2517
2971
  try {
2518
- const mapped = await this.mappingService.findMapping({
2972
+ const mapped = await this.findValidateMixinClassMapping({
2519
2973
  version,
2520
- kind: "class",
2521
- name: resolvedClassName,
2974
+ className: resolvedClassName,
2522
2975
  sourceMapping: requestedMapping,
2523
- targetMapping: "obfuscated",
2524
- sourcePriority: input.sourcePriority
2976
+ targetMapping: signatureLookupMapping,
2977
+ sourcePriority: currentSourcePriority,
2978
+ batchCaches: input.batchCaches
2525
2979
  });
2526
2980
  if (mapped.resolved && mapped.resolvedSymbol) {
2527
2981
  obfuscatedName = mapped.resolvedSymbol.name;
2528
2982
  resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: obfuscatedName, success: true });
2529
2983
  }
2530
2984
  else {
2531
- warnings.push(`Could not map class "${resolvedClassName}" from ${requestedMapping} to obfuscated; using "${obfuscatedName}" for lookup.`);
2985
+ warnings.push(`Could not map class "${resolvedClassName}" from ${requestedMapping} to ${signatureLookupMapping}; using "${obfuscatedName}" for lookup.`);
2532
2986
  mappingFailedTargets.add(target.className);
2533
2987
  resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: obfuscatedName, success: false, detail: "No mapping found" });
2534
2988
  }
2535
2989
  }
2536
2990
  catch (mapErr) {
2537
- warnings.push(`Mapping lookup failed for class "${resolvedClassName}"; using "${obfuscatedName}" for lookup.`);
2991
+ warnings.push(`Mapping lookup failed for class "${resolvedClassName}" while preparing ${signatureLookupMapping} lookup; using "${obfuscatedName}" for lookup.`);
2538
2992
  mappingFailedTargets.add(target.className);
2539
2993
  resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: obfuscatedName, success: false, detail: mapErr instanceof Error ? mapErr.message : String(mapErr) });
2540
2994
  }
@@ -2551,12 +3005,12 @@ export class SourceService {
2551
3005
  let constructors = sig.constructors;
2552
3006
  let methods = sig.methods;
2553
3007
  let fields = sig.fields;
2554
- if (requestedMapping !== "obfuscated") {
3008
+ if (requestedMapping !== signatureLookupMapping) {
2555
3009
  try {
2556
3010
  const [ctorResult, methodResult, fieldResult] = await Promise.all([
2557
- this.remapSignatureMembers(sig.constructors, "method", version, "obfuscated", requestedMapping, input.sourcePriority, warnings),
2558
- this.remapSignatureMembers(sig.methods, "method", version, "obfuscated", requestedMapping, input.sourcePriority, warnings),
2559
- this.remapSignatureMembers(sig.fields, "field", version, "obfuscated", requestedMapping, input.sourcePriority, warnings)
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)
2560
3014
  ]);
2561
3015
  constructors = ctorResult.members;
2562
3016
  methods = methodResult.members;
@@ -2578,9 +3032,17 @@ export class SourceService {
2578
3032
  }
2579
3033
  }
2580
3034
  catch (remapErr) {
2581
- warnings.push(`Member remapping failed for "${resolvedClassName}"; falling back to obfuscated names. Member names shown may be in the obfuscated runtime namespace.`);
2582
- mappingApplied = "obfuscated";
2583
- resolutionTrace?.push({ target: target.className, step: "remap", input: resolvedClassName, output: "obfuscated fallback", success: false, detail: remapErr instanceof Error ? remapErr.message : String(remapErr) });
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
+ });
2584
3046
  }
2585
3047
  }
2586
3048
  targetMembers.set(target.className, {
@@ -2592,16 +3054,14 @@ export class SourceService {
2592
3054
  }
2593
3055
  catch (sigErr) {
2594
3056
  warnings.push(`Could not load signature for class "${resolvedClassName}" (obfuscated: "${obfuscatedName}").`);
2595
- signatureFailedTargets.add(target.className);
2596
3057
  resolutionTrace?.push({ target: target.className, step: "signature", input: obfuscatedName, output: "CLASS_NOT_FOUND", success: false, detail: sigErr instanceof Error ? sigErr.message : String(sigErr) });
2597
3058
  // Fallback: check if the symbol exists in the mapping graph even though getSignature failed
2598
3059
  try {
2599
3060
  const existenceCheck = await this.mappingService.checkSymbolExists({
2600
3061
  version, kind: "class", name: resolvedClassName,
2601
- sourceMapping: requestedMapping, nameMode: "auto"
3062
+ sourceMapping: requestedMapping, nameMode: "auto", sourcePriority: currentSourcePriority
2602
3063
  });
2603
3064
  if (existenceCheck.resolved) {
2604
- signatureFailedTargets.delete(target.className);
2605
3065
  symbolExistsButSignatureFailed.add(target.className);
2606
3066
  resolutionTrace?.push({ target: target.className, step: "fallback-check", input: resolvedClassName, output: "exists in mapping graph", success: true });
2607
3067
  }
@@ -2610,23 +3070,35 @@ export class SourceService {
2610
3070
  }
2611
3071
  }
2612
3072
  catch {
2613
- // Fallback check failed — keep as signatureFailedTarget
3073
+ // Fallback check failed — treat as tool-limited partial validation.
3074
+ signatureFailedTargets.add(target.className);
2614
3075
  resolutionTrace?.push({ target: target.className, step: "fallback-check", input: resolvedClassName, output: "check failed", success: false });
2615
3076
  }
2616
3077
  }
2617
3078
  }
2618
3079
  // Fix toolHealth accuracy: reflect actual failures after target resolution
2619
3080
  if (healthReport) {
2620
- const hasFailures = signatureFailedTargets.size > 0 || mappingFailedTargets.size > 0;
3081
+ const hasFailures = signatureFailedTargets.size > 0 ||
3082
+ mappingFailedTargets.size > 0 ||
3083
+ symbolExistsButSignatureFailed.size > 0;
2621
3084
  if (hasFailures && healthReport.overallHealthy) {
2622
3085
  healthReport.overallHealthy = false;
2623
- 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).`);
2624
3087
  }
2625
3088
  }
2626
3089
  const resolutionNotes = [];
2627
3090
  if (requestedMapping !== mappingApplied) {
2628
3091
  resolutionNotes.push(`Mapping fallback: requested "${requestedMapping}" but applied "${mappingApplied}" due to remapping failure.`);
2629
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
+ }
2630
3102
  // Count remap failures from warnings
2631
3103
  const REMAP_WARNING_RE = /^(?:Could not remap|Remap failed for)\b/;
2632
3104
  const remapFailures = warnings.filter((w) => REMAP_WARNING_RE.test(w)).length;
@@ -2640,26 +3112,30 @@ export class SourceService {
2640
3112
  }
2641
3113
  // Build mapping chain description
2642
3114
  const mappingChain = [];
2643
- if (requestedMapping !== "obfuscated") {
2644
- mappingChain.push(`${requestedMapping} → obfuscated`);
2645
- if (mappingApplied !== requestedMapping) {
2646
- mappingChain.push(`fallback to ${mappingApplied}`);
2647
- }
3115
+ if (requestedMapping !== signatureLookupMapping) {
3116
+ mappingChain.push(`${requestedMapping} → ${signatureLookupMapping}`);
3117
+ }
3118
+ if (mappingApplied !== signatureLookupMapping) {
3119
+ mappingChain.push(`fallback to ${mappingApplied}`);
2648
3120
  }
2649
3121
  const provenance = {
2650
3122
  version,
2651
3123
  jarPath,
2652
3124
  requestedMapping,
2653
3125
  mappingApplied,
3126
+ requestedScope,
3127
+ appliedScope,
3128
+ requestedSourcePriority: initialSourcePriority,
3129
+ appliedSourcePriority: currentSourcePriority,
2654
3130
  resolutionNotes: resolutionNotes.length > 0 ? resolutionNotes : undefined,
2655
- jarType: scopeFallback ? "vanilla-client" : (input.scope && input.scope !== "vanilla" && input.projectPath) ? "merged" : "vanilla-client",
3131
+ jarType: scopeToJarType(appliedScope),
2656
3132
  mappingChain: mappingChain.length > 0 ? mappingChain : undefined,
2657
3133
  remapFailures: remapFailures > 0 ? remapFailures : undefined,
2658
3134
  mappingAutoDetected: mappingAutoDetected || undefined,
2659
3135
  scopeFallback,
2660
3136
  resolutionTrace: resolutionTrace && resolutionTrace.length > 0 ? resolutionTrace : undefined
2661
3137
  };
2662
- 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));
2663
3139
  // Apply minSeverity / hideUncertain filters
2664
3140
  const minSeverity = input.minSeverity ?? "all";
2665
3141
  const hideUncertain = input.hideUncertain ?? false;
@@ -2692,7 +3168,6 @@ export class SourceService {
2692
3168
  parseWarnings: filteredParseWarnings
2693
3169
  };
2694
3170
  result.unfilteredSummary = unfilteredSummary;
2695
- result.valid = filteredDefiniteErrors === 0;
2696
3171
  }
2697
3172
  // Apply warningCategoryFilter
2698
3173
  if (input.warningCategoryFilter && input.warningCategoryFilter.length > 0) {
@@ -2716,7 +3191,6 @@ export class SourceService {
2716
3191
  resolutionErrors: result.issues.filter((i) => i.resolutionPath != null).length,
2717
3192
  parseWarnings: result.issues.filter((i) => i.category === "parse").length
2718
3193
  };
2719
- result.valid = catDefiniteErrors === 0;
2720
3194
  }
2721
3195
  // Apply treatInfoAsWarning filter
2722
3196
  if (input.treatInfoAsWarning === false && result.structuredWarnings) {
@@ -2730,10 +3204,41 @@ export class SourceService {
2730
3204
  result.structuredWarnings = undefined;
2731
3205
  result.aggregatedWarnings = undefined;
2732
3206
  result.toolHealth = undefined;
3207
+ result.confidenceBreakdown = undefined;
2733
3208
  if (result.provenance) {
2734
3209
  result.provenance.resolutionTrace = undefined;
2735
3210
  }
2736
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
+ }
2737
3242
  return result;
2738
3243
  }
2739
3244
  resolveMixinInputPath(rawPath, fieldName) {
@@ -2807,12 +3312,16 @@ export class SourceService {
2807
3312
  const results = [];
2808
3313
  const batchWarningMode = input.warningMode ?? "aggregated";
2809
3314
  const { input: _discardedInput, ...sharedInput } = input;
3315
+ const batchCaches = {
3316
+ classMappings: new Map()
3317
+ };
2810
3318
  for (const entry of entries) {
2811
3319
  try {
2812
3320
  const singleResult = await this.validateMixinSingle({
2813
3321
  ...sharedInput,
2814
3322
  sourcePath: entry.sourcePath,
2815
- warningMode: batchWarningMode
3323
+ warningMode: batchWarningMode,
3324
+ batchCaches
2816
3325
  });
2817
3326
  results.push({
2818
3327
  source: entry.source,
@@ -2826,15 +3335,62 @@ export class SourceService {
2826
3335
  });
2827
3336
  }
2828
3337
  }
2829
- return this.buildValidateMixinOutput(mode, results);
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
+ };
2830
3384
  }
2831
3385
  buildValidateMixinOutput(mode, results) {
2832
3386
  let valid = 0;
3387
+ let partial = 0;
2833
3388
  let invalid = 0;
2834
3389
  let processingErrors = 0;
2835
3390
  let totalValidationErrors = 0;
2836
3391
  let totalValidationWarnings = 0;
2837
3392
  const warningSet = new Set();
3393
+ const incompleteReasonSet = new Set();
2838
3394
  const issueGroupMap = new Map();
2839
3395
  for (const entry of results) {
2840
3396
  if (!entry.result) {
@@ -2847,12 +3403,18 @@ export class SourceService {
2847
3403
  else {
2848
3404
  invalid++;
2849
3405
  }
3406
+ if (entry.result.validationStatus === "partial") {
3407
+ partial++;
3408
+ }
2850
3409
  totalValidationErrors += entry.result.summary.errors;
2851
3410
  totalValidationWarnings += entry.result.summary.warnings;
2852
3411
  for (const warning of entry.result.warnings) {
2853
3412
  warningSet.add(warning);
2854
3413
  }
2855
3414
  for (const issue of entry.result.issues) {
3415
+ if (issue.kind === "validation-incomplete") {
3416
+ incompleteReasonSet.add(`validation-incomplete: ${issue.message}`);
3417
+ }
2856
3418
  const key = `${issue.kind}\0${issue.confidence ?? "unknown"}\0${issue.category ?? "validation"}`;
2857
3419
  const existing = issueGroupMap.get(key);
2858
3420
  if (existing) {
@@ -2873,6 +3435,14 @@ export class SourceService {
2873
3435
  }
2874
3436
  }
2875
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;
2876
3446
  const toolHealth = results.find((entry) => entry.result?.toolHealth)?.result?.toolHealth;
2877
3447
  const confidenceScores = results
2878
3448
  .map((entry) => entry.result?.confidenceScore)
@@ -2883,12 +3453,15 @@ export class SourceService {
2883
3453
  summary: {
2884
3454
  total: results.length,
2885
3455
  valid,
3456
+ partial,
2886
3457
  invalid,
2887
3458
  processingErrors,
2888
3459
  totalValidationErrors,
2889
3460
  totalValidationWarnings
2890
3461
  },
2891
3462
  issueSummary,
3463
+ provenance,
3464
+ incompleteReasons: incompleteReasonSet.size > 0 ? [...incompleteReasonSet] : undefined,
2892
3465
  toolHealth,
2893
3466
  confidenceScore: confidenceScores.length > 0 ? Math.min(...confidenceScores) : undefined,
2894
3467
  warnings: [...warningSet]
@@ -3208,15 +3781,13 @@ export class SourceService {
3208
3781
  this.metrics.recordSearchDbRoundtrip();
3209
3782
  this.metrics.recordSearchRowsScanned(candidates.length);
3210
3783
  const result = [];
3784
+ const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
3211
3785
  for (const symbol of candidates) {
3212
3786
  if (!checkPackagePrefix(symbol.filePath, scope?.packagePrefix)) {
3213
3787
  continue;
3214
3788
  }
3215
- if (scope?.fileGlob) {
3216
- const glob = buildGlobRegex(normalizePathStyle(scope.fileGlob));
3217
- if (!glob.test(symbol.filePath)) {
3218
- continue;
3219
- }
3789
+ if (glob && !glob.test(symbol.filePath)) {
3790
+ continue;
3220
3791
  }
3221
3792
  if (!isSymbolKind(symbol.symbolKind)) {
3222
3793
  continue;
@@ -3310,25 +3881,13 @@ export class SourceService {
3310
3881
  if (innerIndex > 0) {
3311
3882
  candidates.add(`${classPath.slice(0, innerIndex)}.java`);
3312
3883
  }
3313
- for (const candidate of candidates) {
3314
- const row = this.filesRepo.getFileContent(artifactId, candidate);
3315
- if (row) {
3316
- return row.filePath;
3317
- }
3318
- }
3319
3884
  const simpleName = normalizedClassName.split(/[.$]/).at(-1);
3320
3885
  if (!simpleName) {
3321
3886
  return undefined;
3322
3887
  }
3323
- const classPathBySymbol = this.symbolsRepo.findBestClassFilePath(artifactId, normalizedClassName, simpleName);
3324
- if (classPathBySymbol && isPackageCompatible(classPathBySymbol, classPath)) {
3325
- return classPathBySymbol;
3326
- }
3327
- const byName = this.filesRepo.findFirstFilePathByName(artifactId, `${simpleName}.java`);
3328
- if (byName && isPackageCompatible(byName, classPath)) {
3329
- return byName;
3330
- }
3331
- 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);
3332
3891
  }
3333
3892
  async resolveBinaryFallbackArtifact(input) {
3334
3893
  const binaryJarPath = normalizeOptionalString(input.binaryJarPath);
@@ -3342,9 +3901,11 @@ export class SourceService {
3342
3901
  fallbackResolved.requestedMapping = input.requestedMapping;
3343
3902
  fallbackResolved.mappingApplied = input.mappingApplied;
3344
3903
  fallbackResolved.provenance = input.provenance;
3345
- fallbackResolved.qualityFlags = [
3346
- ...new Set([...(fallbackResolved.qualityFlags ?? []), ...input.qualityFlags, "binary-fallback"])
3347
- ];
3904
+ fallbackResolved.qualityFlags = dedupeQualityFlags([
3905
+ ...(fallbackResolved.qualityFlags ?? []),
3906
+ ...input.qualityFlags,
3907
+ "binary-fallback"
3908
+ ]);
3348
3909
  await this.ingestIfNeeded(fallbackResolved);
3349
3910
  return fallbackResolved;
3350
3911
  }
@@ -3509,16 +4070,32 @@ export class SourceService {
3509
4070
  };
3510
4071
  }
3511
4072
  try {
3512
- const mapped = await this.mappingService.findMapping({
3513
- version,
3514
- kind,
3515
- name,
3516
- owner: ownerInSourceMapping,
3517
- descriptor,
3518
- sourceMapping: mapping,
3519
- targetMapping: "obfuscated",
3520
- sourcePriority
3521
- });
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);
3522
4099
  if (mapped.resolved && mapped.resolvedSymbol) {
3523
4100
  return {
3524
4101
  name: mapped.resolvedSymbol.name,
@@ -3527,8 +4104,8 @@ export class SourceService {
3527
4104
  }
3528
4105
  warnings.push(`Could not map ${kind} "${name}" from ${mapping} to obfuscated.`);
3529
4106
  }
3530
- catch {
3531
- 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)}`);
3532
4109
  }
3533
4110
  return {
3534
4111
  name,
@@ -3707,6 +4284,7 @@ export class SourceService {
3707
4284
  });
3708
4285
  });
3709
4286
  tx();
4287
+ this.upsertCacheMetrics(resolved.artifactId, rebuilt.totalContentBytes, timestamp);
3710
4288
  log("info", "index.rebuild.done", {
3711
4289
  artifactId: resolved.artifactId,
3712
4290
  reason,
@@ -3787,7 +4365,8 @@ export class SourceService {
3787
4365
  files,
3788
4366
  symbols,
3789
4367
  indexedAt: new Date().toISOString(),
3790
- indexDurationMs: Date.now() - indexStartedAt
4368
+ indexDurationMs: Date.now() - indexStartedAt,
4369
+ totalContentBytes: files.reduce((sum, file) => sum + file.contentBytes, 0)
3791
4370
  };
3792
4371
  }
3793
4372
  getArtifact(artifactId) {
@@ -3807,7 +4386,10 @@ export class SourceService {
3807
4386
  details: {
3808
4387
  artifactId,
3809
4388
  nextAction: "Use resolve-artifact to resolve a source artifact first.",
3810
- suggestedCall: { tool: "resolve-artifact", params: { targetKind: "version", targetValue: "latest" } }
4389
+ suggestedCall: {
4390
+ tool: "resolve-artifact",
4391
+ params: buildResolveArtifactParams({ kind: "version", value: "latest" })
4392
+ }
3811
4393
  }
3812
4394
  });
3813
4395
  }
@@ -3825,8 +4407,9 @@ export class SourceService {
3825
4407
  });
3826
4408
  if (existing && reason === "already_current") {
3827
4409
  this.metrics.recordArtifactCacheHit();
3828
- this.artifactsRepo.touchArtifact(resolved.artifactId, new Date().toISOString());
3829
- this.refreshCacheMetrics();
4410
+ const touchedAt = new Date().toISOString();
4411
+ this.artifactsRepo.touchArtifact(resolved.artifactId, touchedAt);
4412
+ this.touchCacheMetrics(resolved.artifactId, touchedAt);
3830
4413
  return;
3831
4414
  }
3832
4415
  this.metrics.recordArtifactCacheMiss();
@@ -3837,7 +4420,6 @@ export class SourceService {
3837
4420
  });
3838
4421
  await this.rebuildAndPersistArtifactIndex(resolved, reason === "already_current" ? "missing_meta" : reason);
3839
4422
  this.enforceCacheLimits();
3840
- this.refreshCacheMetrics();
3841
4423
  }
3842
4424
  async loadFromSourceJar(sourceJarPath) {
3843
4425
  const files = [];
@@ -3855,21 +4437,22 @@ export class SourceService {
3855
4437
  return this.filesRepo.listFiles(artifactId, { limit: 1 }).items.length > 0;
3856
4438
  }
3857
4439
  enforceCacheLimits() {
3858
- let artifactCount = this.artifactsRepo.countArtifacts();
3859
- let totalBytes = this.artifactsRepo.totalContentBytes();
4440
+ let artifactCount = this.cacheMetricsState.entries;
4441
+ let totalBytes = this.cacheMetricsState.totalContentBytes;
3860
4442
  if (artifactCount <= this.config.maxArtifacts && totalBytes <= this.config.maxCacheBytes) {
3861
4443
  return;
3862
4444
  }
3863
- const candidates = this.artifactsRepo.listArtifactsByLruWithContentBytes(Math.max(artifactCount, 1));
4445
+ const candidates = [...this.cacheMetricsState.lru];
3864
4446
  for (const candidate of candidates) {
3865
4447
  const shouldEvict = artifactCount > this.config.maxArtifacts || totalBytes > this.config.maxCacheBytes;
3866
4448
  if (!shouldEvict || artifactCount <= 1) {
3867
- return;
4449
+ break;
3868
4450
  }
3869
4451
  const artifactCountBefore = artifactCount;
3870
4452
  const totalBytesBefore = totalBytes;
3871
4453
  this.filesRepo.deleteFilesForArtifact(candidate.artifactId);
3872
4454
  this.artifactsRepo.deleteArtifact(candidate.artifactId);
4455
+ this.removeCacheMetrics(candidate.artifactId, false);
3873
4456
  artifactCount = Math.max(0, artifactCount - 1);
3874
4457
  totalBytes = Math.max(0, totalBytes - candidate.totalContentBytes);
3875
4458
  this.metrics.recordCacheEviction();
@@ -3880,6 +4463,7 @@ export class SourceService {
3880
4463
  artifactBytes: candidate.totalContentBytes
3881
4464
  });
3882
4465
  }
4466
+ this.publishCacheMetrics();
3883
4467
  }
3884
4468
  refreshCacheMetrics() {
3885
4469
  const cacheEntries = this.artifactsRepo.countArtifacts();
@@ -3887,13 +4471,83 @@ export class SourceService {
3887
4471
  const lruAccounting = this.artifactsRepo
3888
4472
  .listArtifactsByLruWithContentBytes(Math.max(cacheEntries, 1))
3889
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) => ({
3890
4547
  artifact_id: row.artifactId,
3891
4548
  content_bytes: row.totalContentBytes,
3892
4549
  updated_at: row.updatedAt
3893
- }));
3894
- this.metrics.setCacheEntries(cacheEntries);
3895
- this.metrics.setCacheTotalContentBytes(totalContentBytes);
3896
- this.metrics.setCacheArtifactByteAccounting(lruAccounting);
4550
+ })));
3897
4551
  }
3898
4552
  }
3899
4553
  //# sourceMappingURL=source-service.js.map