@adhisang/minecraft-modding-mcp 7.0.0 → 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 (52) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +3 -2
  3. package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
  4. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
  5. package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
  6. package/dist/entry-tools/batch-class-members-service.js +20 -6
  7. package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
  8. package/dist/entry-tools/batch-class-source-service.js +10 -0
  9. package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
  10. package/dist/entry-tools/compare-minecraft-service.js +65 -4
  11. package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
  12. package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
  13. package/dist/entry-tools/manage-cache-service.d.ts +2 -2
  14. package/dist/entry-tools/validate-project/cases/project-summary.js +71 -12
  15. package/dist/entry-tools/validate-project-service.d.ts +2 -2
  16. package/dist/index.js +37 -15
  17. package/dist/source/artifact-resolver.d.ts +14 -0
  18. package/dist/source/artifact-resolver.js +106 -12
  19. package/dist/source/class-source/members-builder.d.ts +7 -0
  20. package/dist/source/class-source/members-builder.js +4 -1
  21. package/dist/source/class-source.d.ts +9 -2
  22. package/dist/source/class-source.js +173 -25
  23. package/dist/source/indexer.js +69 -1
  24. package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
  25. package/dist/source/lifecycle/mapping-helpers.js +29 -3
  26. package/dist/source/lifecycle/runtime-check.d.ts +25 -0
  27. package/dist/source/lifecycle/runtime-check.js +68 -39
  28. package/dist/source/symbol-resolver.js +88 -0
  29. package/dist/source-jar-reader.d.ts +33 -0
  30. package/dist/source-jar-reader.js +58 -0
  31. package/dist/source-resolver.d.ts +7 -0
  32. package/dist/source-resolver.js +20 -5
  33. package/dist/source-service.d.ts +5 -0
  34. package/dist/source-service.js +7 -0
  35. package/dist/storage/db.d.ts +62 -2
  36. package/dist/storage/db.js +181 -20
  37. package/dist/storage/sqlite.d.ts +31 -1
  38. package/dist/storage/sqlite.js +125 -16
  39. package/dist/tool-guidance.js +4 -1
  40. package/dist/tool-schemas.d.ts +64 -52
  41. package/dist/tool-schemas.js +9 -7
  42. package/dist/types.d.ts +9 -0
  43. package/dist/v1-parity-schemas.js +36 -2
  44. package/dist/version-diff-service.d.ts +23 -0
  45. package/dist/version-diff-service.js +101 -0
  46. package/dist/version-service.d.ts +14 -0
  47. package/dist/version-service.js +45 -3
  48. package/dist/workspace-mapping-service.d.ts +8 -0
  49. package/dist/workspace-mapping-service.js +35 -7
  50. package/docs/README-ja.md +2 -0
  51. package/docs/tool-reference.md +55 -11
  52. 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
  }
@@ -1,5 +1,6 @@
1
- import Database from "./sqlite.js";
1
+ import Database, { isRawSqliteCorruptionError } from "./sqlite.js";
2
2
  import type { Config } from "../types.js";
3
+ export { isRawSqliteCorruptionError };
3
4
  type SqliteDatabase = InstanceType<typeof Database>;
4
5
  type Logger = {
5
6
  warn: (message: string, details?: Record<string, unknown>) => void;
@@ -11,5 +12,64 @@ export interface InitializedDatabase {
11
12
  schemaVersion: number;
12
13
  }
13
14
  type DatabaseConfig = Pick<Config, "sqlitePath"> & Partial<Pick<Config, "sqliteCacheKb" | "sqliteMmapSize">>;
15
+ /**
16
+ * Best-effort request that the NEXT `openDatabase` for `sqlitePath` run the
17
+ * full `PRAGMA integrity_check` instead of the default `quick_check`. Creates
18
+ * a new, uniquely-named request file (see the block comment above) with
19
+ * exclusive-create so it can never collide with or overwrite another
20
+ * pending request. Never throws: a failure to write the file only means the
21
+ * escalation is missed, which is logged rather than allowed to mask the
22
+ * caller's real error. Returns whether the file was actually written, so a
23
+ * caller that cannot schedule the follow-up check can say so instead of
24
+ * promising one.
25
+ */
26
+ export declare function requestFullIntegrityCheck(sqlitePath: string, logger?: Logger): boolean;
27
+ /**
28
+ * Lists the full paths of every currently pending full-integrity-check
29
+ * request file for `sqlitePath`. Best-effort: a missing or unreadable
30
+ * directory is treated as "no requests pending" rather than as an error,
31
+ * since the fallback (a quick_check) is always safe to run.
32
+ */
33
+ export declare function listIntegrityCheckRequestFiles(sqlitePath: string): string[];
34
+ /**
35
+ * Deletes EXACTLY the given request file paths - the ones an earlier
36
+ * `listIntegrityCheckRequestFiles` call returned, never a fresh listing - so
37
+ * a request file created after that decision point is never deleted by a
38
+ * clear that only ever meant to honor the requests it actually acted on.
39
+ * Best-effort per file: ENOENT (already gone) is ignored, anything else is
40
+ * logged, not thrown.
41
+ *
42
+ * Exported so a unit test can exercise the race guard directly:
43
+ * `openDatabase` lists-then-clears synchronously with no `await` in between,
44
+ * so there is no way to interleave a concurrent request file appearing
45
+ * between "decide" and "clear" from outside that call.
46
+ */
47
+ export declare function clearIntegrityCheckRequestFiles(requestFiles: string[], logger?: Logger): void;
48
+ /**
49
+ * Wires a Database instance's corruption observer (see sqlite.ts) so that a
50
+ * raw corruption error thrown by ANY later statement on this handle - no
51
+ * matter what catches and wraps it further up the call stack, including a
52
+ * tool-specific error wrapper that never reaches runTool's own catch block -
53
+ * still schedules the next open's full integrity_check. Exported so a unit
54
+ * test can attach the same wiring to a Database instance opened directly
55
+ * (bypassing openDatabase's own open-time check, which would otherwise catch
56
+ * a test's injected corruption before a "successfully opened" handle ever
57
+ * existed to query against).
58
+ */
59
+ export declare function attachRuntimeCorruptionObserver(db: SqliteDatabase, sqlitePath: string, logger?: Logger): void;
60
+ /**
61
+ * runTool catch-path helper: when `caughtError` is a raw SQLite corruption
62
+ * error observed while serving a tool call (not at open time - open-time
63
+ * corruption is already an AppError by the time it reaches here), this
64
+ * requests a full integrity check on the next open and returns a typed
65
+ * ERR_DB_FAILURE the caller should report instead of the raw error. Any other
66
+ * error is returned unchanged so non-corruption failures are unaffected.
67
+ *
68
+ * The marker request here is redundant with the Database corruption observer
69
+ * (attachRuntimeCorruptionObserver) for an UNWRAPPED raw error, since that
70
+ * observer already fired deeper in the call stack; it is kept because
71
+ * `convertRuntimeSqliteCorruption` is the only place with reliable access to
72
+ * whether scheduling succeeded, which decides which `nextAction` to publish.
73
+ */
74
+ export declare function convertRuntimeSqliteCorruption(error: unknown, sqlitePath: string, logger?: Logger): unknown;
14
75
  export declare function openDatabase(config: DatabaseConfig, logger?: Logger): InitializedDatabase;
15
- export {};