@adhisang/minecraft-modding-mcp 7.0.0-rc.3 → 7.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 (60) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +3 -2
  3. package/dist/cache-registry.d.ts +16 -0
  4. package/dist/cache-registry.js +78 -10
  5. package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
  6. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
  7. package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
  8. package/dist/entry-tools/batch-class-members-service.js +20 -6
  9. package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
  10. package/dist/entry-tools/batch-class-source-service.js +10 -0
  11. package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
  12. package/dist/entry-tools/compare-minecraft-service.js +65 -4
  13. package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
  14. package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
  15. package/dist/entry-tools/manage-cache-service.d.ts +2 -2
  16. package/dist/entry-tools/validate-project/cases/project-summary.js +71 -12
  17. package/dist/entry-tools/validate-project-service.d.ts +2 -2
  18. package/dist/index.js +37 -15
  19. package/dist/json-rpc-framing.d.ts +20 -0
  20. package/dist/json-rpc-framing.js +80 -7
  21. package/dist/mapping/loaders/tiny-maven.d.ts +9 -0
  22. package/dist/mapping/loaders/tiny-maven.js +10 -2
  23. package/dist/repo-downloader.js +13 -2
  24. package/dist/source/artifact-resolver.d.ts +14 -0
  25. package/dist/source/artifact-resolver.js +106 -12
  26. package/dist/source/class-source/members-builder.d.ts +7 -0
  27. package/dist/source/class-source/members-builder.js +4 -1
  28. package/dist/source/class-source.d.ts +9 -2
  29. package/dist/source/class-source.js +229 -26
  30. package/dist/source/indexer.js +69 -1
  31. package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
  32. package/dist/source/lifecycle/mapping-helpers.js +29 -3
  33. package/dist/source/lifecycle/runtime-check.d.ts +25 -0
  34. package/dist/source/lifecycle/runtime-check.js +68 -39
  35. package/dist/source/symbol-resolver.js +88 -0
  36. package/dist/source-jar-reader.d.ts +33 -0
  37. package/dist/source-jar-reader.js +58 -0
  38. package/dist/source-resolver.d.ts +7 -0
  39. package/dist/source-resolver.js +20 -5
  40. package/dist/source-service.d.ts +5 -0
  41. package/dist/source-service.js +7 -0
  42. package/dist/stdio-supervisor.js +193 -34
  43. package/dist/storage/db.d.ts +62 -2
  44. package/dist/storage/db.js +186 -21
  45. package/dist/storage/sqlite.d.ts +31 -1
  46. package/dist/storage/sqlite.js +125 -16
  47. package/dist/tool-guidance.js +4 -1
  48. package/dist/tool-schemas.d.ts +64 -52
  49. package/dist/tool-schemas.js +9 -7
  50. package/dist/types.d.ts +9 -0
  51. package/dist/v1-parity-schemas.js +36 -2
  52. package/dist/version-diff-service.d.ts +23 -0
  53. package/dist/version-diff-service.js +101 -0
  54. package/dist/version-service.d.ts +14 -0
  55. package/dist/version-service.js +45 -3
  56. package/dist/workspace-mapping-service.d.ts +8 -0
  57. package/dist/workspace-mapping-service.js +35 -7
  58. package/docs/README-ja.md +2 -0
  59. package/docs/tool-reference.md +55 -11
  60. package/package.json +1 -1
@@ -1,5 +1,18 @@
1
1
  import { ERROR_CODES, isAppError } from "../../errors.js";
2
+ /**
3
+ * check-symbol-exists's runtime fallback: the result of `runUnobfuscatedRuntimeCheck`
4
+ * alone. Its callers keep the fallback base's status whenever the check could not be
5
+ * performed, so they need no `verified` flag.
6
+ */
2
7
  export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbackBase) {
8
+ return (await runUnobfuscatedRuntimeCheck(svc, input, fallbackBase))?.result;
9
+ }
10
+ /**
11
+ * Checks a symbol against the Minecraft runtime jar of an unobfuscated version, whose
12
+ * names are the Mojang names. Undefined when the version or name is empty or the
13
+ * runtime jar cannot be resolved.
14
+ */
15
+ export async function runUnobfuscatedRuntimeCheck(svc, input, fallbackBase) {
3
16
  const version = input.version.trim();
4
17
  const name = input.name.trim();
5
18
  const owner = input.owner?.trim();
@@ -8,11 +21,14 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
8
21
  }
9
22
  if (input.kind === "class" && input.nameMode !== "fqcn" && !name.includes(".")) {
10
23
  return {
11
- ...fallbackBase,
12
- warnings: [
13
- ...fallbackBase.warnings,
14
- `Version ${version} is unobfuscated, but short class name "${name}" could not be checked against runtime bytecode without a fully-qualified name.`
15
- ]
24
+ verified: false,
25
+ result: {
26
+ ...fallbackBase,
27
+ warnings: [
28
+ ...fallbackBase.warnings,
29
+ `Version ${version} is unobfuscated, but short class name "${name}" could not be checked against runtime bytecode without a fully-qualified name.`
30
+ ]
31
+ }
16
32
  };
17
33
  }
18
34
  const querySymbol = input.kind === "class"
@@ -37,7 +53,7 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
37
53
  };
38
54
  const targetClass = input.kind === "class" ? name : owner;
39
55
  if (!targetClass) {
40
- return fallbackBase;
56
+ return { verified: false, result: fallbackBase };
41
57
  }
42
58
  let jarPath;
43
59
  try {
@@ -61,16 +77,20 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
61
77
  });
62
78
  }
63
79
  catch (error) {
80
+ // Only CLASS_NOT_FOUND is an answer; any other failure means the lookup never completed.
64
81
  const classMissing = isAppError(error) && error.code === ERROR_CODES.CLASS_NOT_FOUND;
65
82
  return {
66
- ...fallbackBase,
67
- querySymbol,
68
- warnings: [
69
- ...fallbackBase.warnings,
70
- classMissing
71
- ? `Class "${targetClass}" was not found in the Minecraft ${version} runtime jar; it does not exist (or is not in this jar).`
72
- : `Version ${version} is unobfuscated; runtime bytecode lookup could not load class "${targetClass}".`
73
- ]
83
+ verified: classMissing,
84
+ result: {
85
+ ...fallbackBase,
86
+ querySymbol,
87
+ warnings: [
88
+ ...fallbackBase.warnings,
89
+ classMissing
90
+ ? `Class "${targetClass}" was not found in the Minecraft ${version} runtime jar; it does not exist (or is not in this jar).`
91
+ : `Version ${version} is unobfuscated; runtime bytecode lookup could not load class "${targetClass}".`
92
+ ]
93
+ }
74
94
  };
75
95
  }
76
96
  const warnings = [...fallbackBase.warnings, ...signature.warnings];
@@ -80,33 +100,40 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
80
100
  ...fallbackBase.mappingContext,
81
101
  runtimeValidated: true
82
102
  };
103
+ // From here on the class loaded, so every answer below is a completed lookup.
83
104
  const buildResolved = (resolvedSymbol) => ({
84
- ...fallbackBase,
85
- mappingContext: runtimeValidatedContext,
86
- querySymbol,
87
- resolved: true,
88
- status: "resolved",
89
- resolvedSymbol,
90
- candidates: resolvedSymbol
91
- ? [{
92
- ...resolvedSymbol,
93
- matchKind: "exact",
94
- confidence: 1
95
- }]
96
- : [],
97
- candidateCount: resolvedSymbol ? 1 : 0,
98
- warnings
105
+ verified: true,
106
+ result: {
107
+ ...fallbackBase,
108
+ mappingContext: runtimeValidatedContext,
109
+ querySymbol,
110
+ resolved: true,
111
+ status: "resolved",
112
+ resolvedSymbol,
113
+ candidates: resolvedSymbol
114
+ ? [{
115
+ ...resolvedSymbol,
116
+ matchKind: "exact",
117
+ confidence: 1
118
+ }]
119
+ : [],
120
+ candidateCount: resolvedSymbol ? 1 : 0,
121
+ warnings
122
+ }
99
123
  });
100
124
  const buildUnresolved = (status) => ({
101
- ...fallbackBase,
102
- mappingContext: runtimeValidatedContext,
103
- querySymbol,
104
- resolved: false,
105
- status,
106
- resolvedSymbol: undefined,
107
- candidates: [],
108
- candidateCount: 0,
109
- warnings
125
+ verified: true,
126
+ result: {
127
+ ...fallbackBase,
128
+ mappingContext: runtimeValidatedContext,
129
+ querySymbol,
130
+ resolved: false,
131
+ status,
132
+ resolvedSymbol: undefined,
133
+ candidates: [],
134
+ candidateCount: 0,
135
+ warnings
136
+ }
110
137
  });
111
138
  if (input.kind === "class") {
112
139
  return buildResolved({
@@ -127,7 +154,9 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
127
154
  symbol: `${owner}.${name}`
128
155
  });
129
156
  }
130
- const methodCandidates = signature.methods.filter((method) => method.name === name);
157
+ // The bytecode reader lists constructors apart from methods (and only the owner's
158
+ // own: constructors are not inherited), so "<init>" is answered from them.
159
+ const methodCandidates = (name === "<init>" ? signature.constructors : signature.methods).filter((method) => method.name === name);
131
160
  const signatureMode = input.signatureMode ?? "name-only";
132
161
  if (signatureMode === "name-only") {
133
162
  // Existence semantics: any overload with this name means the method exists. Multiple
@@ -1,4 +1,83 @@
1
1
  import { ERROR_CODES, createError } from "../errors.js";
2
+ import { isUnobfuscatedVersion } from "../version-service.js";
3
+ import { runUnobfuscatedRuntimeCheck } from "./lifecycle/runtime-check.js";
4
+ /**
5
+ * Minecraft 26.1+ ships its runtime in Mojang names, so a Loom project for it has no
6
+ * `mappings` declaration: it compiles against the runtime names directly. Resolving a
7
+ * workspace symbol there is an identity lookup, checked against runtime bytecode the
8
+ * same way checkSymbolExists validates unobfuscated versions.
9
+ */
10
+ async function resolveUnobfuscatedWorkspaceSymbol(svc, input, context) {
11
+ const { version, querySymbol, sourcePriorityApplied } = context;
12
+ const workspaceDetection = {
13
+ resolved: true,
14
+ mappingApplied: "mojang",
15
+ evidence: [],
16
+ warnings: [
17
+ `Minecraft ${version} is unobfuscated; no mappings declaration is needed — symbols are resolved against runtime (Mojang) names.`
18
+ ]
19
+ };
20
+ // The requested/defaulted sourceMapping label is echoed unchanged: on 26.1+ an
21
+ // "obfuscated" label already means the as-shipped Mojang names, and rewriting it to
22
+ // "mojang" would break callers that compare namespace labels for equality.
23
+ const base = {
24
+ querySymbol,
25
+ mappingContext: {
26
+ version,
27
+ sourceMapping: input.sourceMapping,
28
+ targetMapping: "mojang",
29
+ sourcePriorityApplied,
30
+ unobfuscatedRuntime: true
31
+ },
32
+ resolved: false,
33
+ status: "not_found",
34
+ candidates: [],
35
+ candidateCount: 0,
36
+ warnings: [...workspaceDetection.warnings]
37
+ };
38
+ // The runtime names are both the "obfuscated" (as-shipped) and the Mojang names;
39
+ // intermediary and yarn names do not exist for these versions.
40
+ if (input.sourceMapping !== "obfuscated" && input.sourceMapping !== "mojang") {
41
+ return {
42
+ ...base,
43
+ status: "mapping_unavailable",
44
+ workspaceDetection,
45
+ warnings: [
46
+ ...base.warnings,
47
+ `sourceMapping "${input.sourceMapping}" has no names on unobfuscated Minecraft ${version}; pass sourceMapping "mojang" (or "obfuscated") to resolve against runtime names.`
48
+ ]
49
+ };
50
+ }
51
+ const runtime = await runUnobfuscatedRuntimeCheck(svc, {
52
+ version,
53
+ kind: querySymbol.kind,
54
+ name: querySymbol.name,
55
+ owner: querySymbol.owner,
56
+ descriptor: querySymbol.descriptor,
57
+ sourceMapping: input.sourceMapping,
58
+ signatureMode: "exact"
59
+ }, base);
60
+ if (!runtime) {
61
+ // Mirrors checkSymbolExists: a symbol that could not be checked is not reported missing.
62
+ return {
63
+ ...base,
64
+ status: "mapping_unavailable",
65
+ workspaceDetection,
66
+ warnings: [
67
+ ...base.warnings,
68
+ `Minecraft ${version} runtime jar could not be resolved; symbol existence was not verified.`
69
+ ]
70
+ };
71
+ }
72
+ if (!runtime.verified) {
73
+ // The jar was reached but did not answer (short class name, class that failed to
74
+ // load for a reason other than being absent): the result carries the reason, and
75
+ // an unanswered lookup is not a definitive miss. `not_found` stays reserved for a
76
+ // class confirmed missing or a member lookup that completed without a match.
77
+ return { ...runtime.result, status: "mapping_unavailable", workspaceDetection };
78
+ }
79
+ return { ...runtime.result, workspaceDetection };
80
+ }
2
81
  export async function resolveWorkspaceSymbol(svc, input) {
3
82
  const projectPath = input.projectPath?.trim();
4
83
  const version = input.version?.trim();
@@ -77,6 +156,15 @@ export async function resolveWorkspaceSymbol(svc, input) {
77
156
  const workspaceDetection = await svc.workspaceMappingService.detectCompileMapping({
78
157
  projectPath
79
158
  });
159
+ // No evidence also means no build script was found at all (a missing, mistyped or
160
+ // empty projectPath). Only a build that was read and declares no mappings is a
161
+ // 26.1+ Loom project compiling against the runtime names.
162
+ if (!workspaceDetection.resolved &&
163
+ workspaceDetection.evidence.length === 0 &&
164
+ isUnobfuscatedVersion(version) &&
165
+ (await svc.workspaceMappingService.hasReadableBuildScript(projectPath))) {
166
+ return resolveUnobfuscatedWorkspaceSymbol(svc, input, { version, querySymbol, sourcePriorityApplied });
167
+ }
80
168
  const warnings = [...workspaceDetection.warnings];
81
169
  if (!workspaceDetection.resolved || !workspaceDetection.mappingApplied) {
82
170
  return {
@@ -53,6 +53,39 @@ export declare class EntryTooLargeError extends Error {
53
53
  export declare function listJarEntries(jarPath: string): Promise<string[]>;
54
54
  export declare function listJavaEntries(jarPath: string): Promise<string[]>;
55
55
  export declare function hasAnyJarEntry(jarPath: string, predicate: (entryPath: string) => boolean): Promise<boolean>;
56
+ /** Root entry in which a Minecraft runtime jar names its own release (`"id": "26.2"`). */
57
+ export declare const MINECRAFT_VERSION_JSON_ENTRY = "version.json";
58
+ /** Class every Minecraft runtime jar ships, under its Mojang name, from 26.1 on. */
59
+ export declare const MINECRAFT_SHARED_CONSTANTS_ENTRY = "net/minecraft/SharedConstants.class";
60
+ /**
61
+ * What an archive walk saw that bears on "is this the Minecraft runtime jar?".
62
+ * Raw observations only: judging them is the resolver's job.
63
+ */
64
+ export interface MinecraftRuntimeJarSignals {
65
+ hasSharedConstantsClass: boolean;
66
+ /**
67
+ * Text of the root `version.json`; absent when the entry is missing, larger
68
+ * than the bound, unreadable, or not UTF-8.
69
+ */
70
+ versionJsonText?: string;
71
+ }
72
+ export interface JavaSourceScan {
73
+ hasJavaSources: boolean;
74
+ /** Set only when the walk reached the end without meeting a `.java` entry. */
75
+ minecraftRuntimeSignals?: MinecraftRuntimeJarSignals;
76
+ }
77
+ /**
78
+ * `hasAnyJarEntry(jarPath, hasJavaSourceExtension)` that also collects the
79
+ * Minecraft runtime signals on the same walk.
80
+ *
81
+ * A jar without sources is already walked to its last entry to prove it has none,
82
+ * so noting two entry names on the way costs no extra open and no extra pass. The
83
+ * one entry read, `version.json`, uses the handle that is already open. A failure
84
+ * to read it only withholds the signal: it is evidence for a mapping decision, not
85
+ * the archive's verdict, so it never fails the scan. Errors opening or walking the
86
+ * archive propagate exactly as they do from `hasAnyJarEntry`.
87
+ */
88
+ export declare function scanJarForJavaSources(jarPath: string): Promise<JavaSourceScan>;
56
89
  export declare function readJarEntryAsUtf8(jarPath: string, entryPath: string): Promise<string>;
57
90
  /**
58
91
  * Reads one entry fully into memory. `maxBytes` bounds that materialization and
@@ -183,6 +183,64 @@ export async function hasAnyJarEntry(jarPath, predicate) {
183
183
  }
184
184
  });
185
185
  }
186
+ /** Root entry in which a Minecraft runtime jar names its own release (`"id": "26.2"`). */
187
+ export const MINECRAFT_VERSION_JSON_ENTRY = "version.json";
188
+ /** Class every Minecraft runtime jar ships, under its Mojang name, from 26.1 on. */
189
+ export const MINECRAFT_SHARED_CONSTANTS_ENTRY = "net/minecraft/SharedConstants.class";
190
+ /** The real file is under 1 KiB; anything far larger is not the file this looks for. */
191
+ const MAX_VERSION_JSON_BYTES = 64 * 1024;
192
+ /**
193
+ * `hasAnyJarEntry(jarPath, hasJavaSourceExtension)` that also collects the
194
+ * Minecraft runtime signals on the same walk.
195
+ *
196
+ * A jar without sources is already walked to its last entry to prove it has none,
197
+ * so noting two entry names on the way costs no extra open and no extra pass. The
198
+ * one entry read, `version.json`, uses the handle that is already open. A failure
199
+ * to read it only withholds the signal: it is evidence for a mapping decision, not
200
+ * the archive's verdict, so it never fails the scan. Errors opening or walking the
201
+ * archive propagate exactly as they do from `hasAnyJarEntry`.
202
+ */
203
+ export async function scanJarForJavaSources(jarPath) {
204
+ return withZipFile(jarPath, async (zipFile) => {
205
+ let hasSharedConstantsClass = false;
206
+ let versionJsonEntry;
207
+ while (true) {
208
+ const entry = await readNextEntry(zipFile);
209
+ if (!entry) {
210
+ break;
211
+ }
212
+ if (!isSecureJarEntryPath(entry.fileName)) {
213
+ continue;
214
+ }
215
+ if (hasJavaSourceExtension(entry.fileName)) {
216
+ return { hasJavaSources: true };
217
+ }
218
+ if (entry.fileName === MINECRAFT_SHARED_CONSTANTS_ENTRY) {
219
+ hasSharedConstantsClass = true;
220
+ }
221
+ else if (entry.fileName === MINECRAFT_VERSION_JSON_ENTRY) {
222
+ versionJsonEntry = entry;
223
+ }
224
+ }
225
+ let versionJsonText;
226
+ if (versionJsonEntry && versionJsonEntry.uncompressedSize <= MAX_VERSION_JSON_BYTES) {
227
+ try {
228
+ const buffer = await readEntryStream(zipFile, versionJsonEntry, jarPath, MAX_VERSION_JSON_BYTES);
229
+ versionJsonText = UTF8_DECODER.decode(buffer);
230
+ }
231
+ catch {
232
+ versionJsonText = undefined;
233
+ }
234
+ }
235
+ return {
236
+ hasJavaSources: false,
237
+ minecraftRuntimeSignals: {
238
+ hasSharedConstantsClass,
239
+ ...(versionJsonText !== undefined ? { versionJsonText } : {})
240
+ }
241
+ };
242
+ });
243
+ }
186
244
  export async function readJarEntryAsUtf8(jarPath, entryPath) {
187
245
  const contentBuffer = await readJarEntryAsBuffer(jarPath, entryPath);
188
246
  return decodeUtf8OrThrow(contentBuffer, jarPath, entryPath);
@@ -1,5 +1,6 @@
1
1
  import type { Config, MappingVariant, ResolvedSourceArtifact, SourceTargetInput } from "./types.js";
2
2
  import { type MavenCoordinate } from "./maven-resolver.js";
3
+ import { type JavaSourceScan } from "./source-jar-reader.js";
3
4
  /**
4
5
  * Every `~/.m2` path a coordinate could name, before any of them is checked
5
6
  * against the filesystem.
@@ -59,6 +60,12 @@ export interface ResolveSourceTargetOptions {
59
60
  * legacy hash for obfuscated and source-backed artifacts.
60
61
  */
61
62
  mappingVariant?: MappingVariant;
63
+ /**
64
+ * Receives the result of walking the jar a `kind: "jar"` request names, once,
65
+ * when that walk ran. The resolver uses it to recognise a Minecraft runtime jar
66
+ * from its contents without walking the archive a second time.
67
+ */
68
+ onSubjectJarScanned?: (scan: JavaSourceScan) => void;
62
69
  onRepoFailover?: (event: {
63
70
  stage: "source" | "binary";
64
71
  repoUrl: string;
@@ -7,15 +7,16 @@ import { buildRemoteBinaryUrls, buildRemoteSourceUrls, groupToPath, hasExistingJ
7
7
  import { defaultDownloadPath, discardCachedDownload, resolveCachedDownload } from "./repo-downloader.js";
8
8
  import { normalizeJarPath } from "./path-resolver.js";
9
9
  import { composeArtifactId, contentDigestSignature, contentSignature, jarArtifactIdentity, DECOMPILE_SIGNATURE_QUALIFIER } from "./artifact-identity.js";
10
- import { hasAnyJarEntry, hasJavaSourceExtension } from "./source-jar-reader.js";
10
+ import { hasAnyJarEntry, hasJavaSourceExtension, scanJarForJavaSources } from "./source-jar-reader.js";
11
11
  /**
12
12
  * Whether a jar contains java sources, with archive errors deliberately
13
13
  * propagating.
14
14
  *
15
15
  * This is the check for a jar that IS the subject of the request - the
16
- * `input.kind === "jar"` branch of `resolveSourceTarget` calling it on
17
- * `resolvedJarPath`. What propagates is the reader's refusal to OPEN the
18
- * archive: a truncated file, a non-zip file, an unreadable central directory,
16
+ * `input.kind === "jar"` branch of `resolveSourceTarget` runs it on
17
+ * `resolvedJarPath` as `scanSubjectJarForJavaSources`, the same walk that also
18
+ * collects Minecraft runtime signals. What propagates is the reader's refusal
19
+ * to OPEN the archive: a truncated file, a non-zip file, an unreadable central directory,
19
20
  * an I/O failure. On the subject jar that refusal is the verdict itself, and
20
21
  * swallowing it would downgrade "this archive could not be read" into "this
21
22
  * archive has no sources" - handing back a sibling `-sources.jar` as if the
@@ -73,6 +74,18 @@ async function candidateHasJavaSources(jarPath) {
73
74
  return false;
74
75
  }
75
76
  }
77
+ /**
78
+ * `hasJavaSources` for the subject jar of a `kind: "jar"` request, returning the
79
+ * Minecraft runtime signals the same walk collected. Same contract on both counts
80
+ * that matter: an absent file answers "no sources" without opening anything, and
81
+ * an archive that cannot be opened propagates.
82
+ */
83
+ async function scanSubjectJarForJavaSources(jarPath) {
84
+ if (!hasExistingJar(jarPath)) {
85
+ return { hasJavaSources: false };
86
+ }
87
+ return await scanJarForJavaSources(jarPath);
88
+ }
76
89
  function resolveExactJarSourceCandidate(inputJarPath) {
77
90
  const directory = dirname(inputJarPath);
78
91
  const jarName = basename(inputJarPath);
@@ -526,7 +539,9 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
526
539
  const adjacentSourceCandidates = await listAdjacentJarSourceCandidates(resolvedJarPath);
527
540
  const maybeAdjacentSourceCandidates = adjacentSourceCandidates.length > 0 ? adjacentSourceCandidates : undefined;
528
541
  const preferBinaryOnly = options.preferBinaryOnly ?? false;
529
- if (await hasJavaSources(resolvedJarPath)) {
542
+ const subjectScan = await scanSubjectJarForJavaSources(resolvedJarPath);
543
+ options.onSubjectJarScanned?.(subjectScan);
544
+ if (subjectScan.hasJavaSources) {
530
545
  const siblingBinaryJarPath = resolveSiblingBinaryJarCandidate(resolvedJarPath);
531
546
  const binaryJarPath = siblingBinaryJarPath ??
532
547
  (basename(resolvedJarPath).endsWith("-sources.jar") ? undefined : resolvedJarPath);
@@ -671,6 +671,11 @@ export declare class SourceService {
671
671
  listVersions(input?: ListVersionsInput): Promise<ListVersionsOutput>;
672
672
  getRegistryData(input: GetRegistryDataInput): Promise<GetRegistryDataOutput>;
673
673
  compareVersions(input: CompareVersionsInput): Promise<CompareVersionsOutput>;
674
+ /**
675
+ * Thin passthrough so entry tools (migration-overview) can diff a version
676
+ * pair's `libraries` without reaching into VersionService directly.
677
+ */
678
+ getVersionLibraries(version: string): Promise<string[]>;
674
679
  decompileModJar(input: DecompileModJarInput): Promise<DecompileModJarOutput>;
675
680
  /**
676
681
  * Member-level view of a third-party mod jar class, read from bytecode
@@ -123,6 +123,13 @@ export class SourceService {
123
123
  async compareVersions(input) {
124
124
  return this.versionDiffService.compareVersions(input);
125
125
  }
126
+ /**
127
+ * Thin passthrough so entry tools (migration-overview) can diff a version
128
+ * pair's `libraries` without reaching into VersionService directly.
129
+ */
130
+ async getVersionLibraries(version) {
131
+ return this.versionService.getVersionLibraries(version);
132
+ }
126
133
  async decompileModJar(input) {
127
134
  return this.modDecompileService.decompileModJar(input);
128
135
  }