@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
package/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [7.1.0] - 2026-09-19
11
+
12
+ ### Added
13
+
14
+ - `batch-class-source` and `batch-class-members` accept `target: { "kind": "artifact", "artifactId": "..." }`, the same target `get-class-source` and `get-class-members` accept, so a batch can reuse an artifact that an earlier call already resolved instead of resolving it again from its jar, version, or coordinate. This target was previously rejected with `ERR_INVALID_INPUT`. With it, `summary.sharedArtifactProvenance` is omitted, and an unknown `artifactId` fails each entry with `ERR_SOURCE_NOT_FOUND` rather than failing the whole batch.
15
+ - `compare-minecraft` with `task="migration-overview"` reports which libraries shipped with Minecraft itself were added or removed between the two versions, such as LWJGL's GLFW binding being replaced by SDL, which class and registry diffs do not show. At `detail: "standard"` or `"full"`, the new `migration.libraries` block lists added and removed libraries as `group:artifact:version` and counts libraries whose version changed; `summary.counts.librariesAdded` and `librariesRemoved` carry the same counts at every detail level. A version bump or a per-platform native jar of a library already present is not reported as an addition or removal. When the version details cannot be fetched, or not within 5 seconds, the block is left out with a warning and the rest of the result is unchanged.
16
+ - `provenance.unobfuscatedRuntime: true` marks an artifact whose runtime ships Mojang names (Minecraft 26.1+): version targets, Minecraft runtime coordinates, and jar targets proven to be a 26.1+ runtime jar. `resolve-artifact`, `get-class-source`, `get-class-members`, and the batch tools' `sharedArtifactProvenance` carry it; `provenance` itself is returned only with `include: ["provenance"]` or a fuller `detail` level. `mappingApplied` still reports the mapping you asked for, so `"obfuscated"` on such an artifact means the names as shipped, which are already Mojang names.
17
+
18
+ ### Changed
19
+
20
+ - `validate-project` with `task="project-summary"` infers an omitted `version` from the project's `gradle.properties` (`minecraft_version`, `mc_version`, or `minecraftVersion`) and names the inferred version in `warnings`; it previously returned `status: "blocked"`. To keep the previous behavior, pass `preferProjectVersion: false`: a call without `version` then returns `blocked`, and the retry suggestions of an invalid-input error keep that explicit `false`. When files are found but no version can be inferred, the blocked reply asks for an explicit `version` instead of suggesting the same call again.
21
+
22
+ ### Performance
23
+
24
+ - The first tool call after the server starts no longer stalls on a full consistency check of the local cache database. The check on open now uses SQLite's `quick_check` instead of `integrity_check`: on a 3.6 GB cache it takes about 2 s instead of about 19 s. A damaged cache file is still detected at startup, backed up and rebuilt. Rarer damage that the lighter check misses can surface later during a tool call; a call that hits it directly now fails with `ERR_DB_FAILURE` and restart guidance instead of `ERR_INTERNAL`, and the next start runs the full `integrity_check`. If the server cannot schedule that check, the error explains how to reset the cache by hand. A call where the damage surfaces inside another step may still report that step's own error.
25
+
26
+ ### Fixed
27
+
28
+ - `validate-project` with `task="project-summary"`: a run that finds no Mixin configs, access wideners, or access transformers now reports `Nothing to validate: …`, naming only the file kinds it searched, with a warning that nothing was validated, instead of "Validated 0 mixin config(s), 0 access widener(s), and 0 access transformer(s)." `status` stays `"ok"`.
29
+ - Minecraft 26.x snapshot, pre-release and release-candidate ids such as `26.3-snapshot-6`, `26.2-pre-6`, `26.2-rc-2` and `26.1.2-rc-1` are recognized as unobfuscated. They were treated as obfuscated legacy versions, so `mapping: "mojang"` was refused on them, `intermediary` and `yarn` did not fall back to `obfuscated`, and symbol checks did not use the runtime jar. Previously only weekly ids such as `26w14a` and unhyphenated forms such as `26.1-rc1`, which Mojang does not publish, were recognized.
30
+ - `mapping: "mojang"` is accepted on a `target.kind: "jar"` that points at a Minecraft 26.1+ runtime jar, such as a jar from the Loom cache; it was refused with `ERR_MAPPING_NOT_APPLIED` and a hint to switch to `obfuscated`. A jar counts as a 26.1+ runtime jar when it has a top-level `version.json` with a 26.1+ `id`, contains `net/minecraft/SharedConstants.class`, and has no `.java` sources. Such a jar reports that `id` as its version and falls back from `intermediary` or `yarn` to `obfuscated` like a version target. A jar indexed by an earlier release has its version recorded on its next resolve, after which calls by `artifactId` accept `mojang` as well.
31
+ - On Minecraft 26.1+ artifacts, class-not-found errors from `get-class-source` and `get-class-members`, and `find-class` misses, lead with the nearest class-name suggestion instead of telling you to retry with `mapping="mojang"`. When `mapping: "mojang"` is refused on a jar that is not a proven runtime jar, the error explains that `mapping: "obfuscated"` reads the names as shipped, and in a 26.1+ project it no longer suggests repeating the refused request. Hints for 1.x versions are unchanged.
32
+ - `resolve-workspace-symbol` and `analyze-symbol` with `task="workspace"` resolve symbols in a Minecraft 26.1+ Loom project that declares no `mappings` line by checking the name against the runtime jar; they returned `mapping_unavailable` (`partial` in `analyze-symbol`). A class that is not in the Minecraft jar returns `not_found`, and a lookup that could not be checked returns `mapping_unavailable` with the reason. A path without a Gradle build script behaves as before.
33
+ - On Minecraft 26.1+, `compare-minecraft` with `task="class-diff"` (and `diff-class-signatures`) and `trace-symbol-lifecycle` with `mapping: "mojang"` no longer mark every member as unmapped with "Could not remap" or "Could not map" warnings.
34
+ - `check-symbol-exists` on Minecraft 26.1+ finds constructors: a method query named `<init>` with an existing descriptor resolves instead of returning `not_found`.
35
+
36
+ ### Documentation
37
+
38
+ - The tool reference documents a known issue: on Minecraft 26.1+, responses label the runtime's Mojang names `obfuscated`, for example `mappingApplied: "obfuscated"`, whenever a request omits `mapping` or asks for `obfuscated`. The label stays until a future major release corrects it. Until then, `provenance.unobfuscatedRuntime: true` marks an artifact whose names are Mojang names, and `mapping: "mojang"` returns the same names labelled `mojang`.
39
+
10
40
  ## [7.0.0] - 2026-09-13
11
41
 
12
42
  ### Fixed
package/README.md CHANGED
@@ -170,7 +170,8 @@ These notes cover high-frequency decisions during onboarding. For the full pitfa
170
170
  - `trace-symbol-lifecycle` expects `Class.method` in `symbol`. Keep exact overload matching in the separate `descriptor` field.
171
171
  - For unobfuscated releases such as `26.1+`, `check-symbol-exists` and `analyze-symbol task="exists"` validate `mojang` lookups against runtime bytecode when no mapping graph exists, and return `mapping_unavailable` when the runtime JAR itself is unreachable.
172
172
  - `analyze-mod` and `validate-project` require structured `subject` objects and canonical `include` groups; stale string-subject or domain-include payloads return `ERR_INVALID_INPUT` with a retryable `suggestedCall`.
173
- - `validate-project task="project-summary"` propagates `preferProjectVersion=true` across discovered Mixin, Access Widener, and Access Transformer checks. If no version can be resolved from the request or `gradle.properties`, the summary returns recovery guidance instead of guessing.
173
+ - `validate-project task="project-summary"` infers an omitted `version` from `gradle.properties` and names the inferred version in `warnings`; pass `preferProjectVersion: false` to turn inference off, in which case a call without `version` returns `status: "blocked"`. The resolved version is passed to every discovered Mixin, Access Widener, and Access Transformer check. If files were discovered but no version can be inferred, the summary returns `status: "blocked"` with a retry that asks for an explicit `version` instead of guessing.
174
+ - When `validate-project task="project-summary"` discovers no files, `status` stays `"ok"` but the headline reads `Nothing to validate: ...` and `warnings` states that nothing was validated, so an `"ok"` status alone does not mean any file passed.
174
175
  - `validate-mixin` and `validate-project` keep `mapping-health` lightweight for `obfuscated` and `mojang` validation, avoiding full Tiny mapping graph loads unless `intermediary` or `yarn` namespaces are requested.
175
176
  - `validate-project task="project-summary"` uses a lightweight artifact probe for `tasks["minecraft.artifact.resolved"]`; it does not decompile Minecraft or rebuild the source index just to report per-probe status. It does read the runtime jar's bytes to derive the `artifactId` it reports, and repeat probes of the same jar in one process reuse that digest. Set `VALIDATE_PROJECT_TASKS_OFF=1` to omit the additive `tasks` field.
176
177
  - `validate-project` has a supervisor-owned end-to-end deadline of 120 seconds, including queue time. Set `MCP_VALIDATE_PROJECT_TIMEOUT_MS` to an ASCII-decimal value from `10000` through `600000` to override it. A timeout returns `ERR_TOOL_TIMEOUT`; a running timeout restarts the isolated worker before queued calls resume, while a queue timeout leaves the current worker untouched.
@@ -301,7 +302,7 @@ Tools for browsing Minecraft versions, resolving source artifacts, and reading o
301
302
 
302
303
  `find-class` accepts either an `artifactId` or the shared object `target` shape. For a workspace-relative dependency, pass `target: { kind: "dependency", group, name, versionFromProject: true }` with top-level `projectPath`. Fabric-style umbrella JARs are searched through their nested `.class` inventories; a top-level class match can then be passed to `get-class-source` or `get-class-members`, which resolve the containing nested JAR. Dotted inner-class matches are also readable through `get-class-source`. An empty result still means that the requested class name is absent from the resolved dependency version.
303
304
 
304
- For unobfuscated releases such as `26.1+`, `mapping="mojang"` uses the runtime/decompile path directly and skips Loom source-jar discovery, while `intermediary` and `yarn` fall back to `obfuscated` with a warning.
305
+ For unobfuscated releases such as `26.1+`, `obfuscated` names the runtime jar's names as shipped, which are already Mojang names: both `obfuscated` and `mojang` are accepted, `mappingApplied` reports the label you asked for, and `provenance.unobfuscatedRuntime: true` marks the artifact. `mapping="mojang"` uses the runtime/decompile path directly and skips Loom source-jar discovery, while `intermediary` and `yarn` fall back to `obfuscated` with a warning. This applies to version targets, Minecraft runtime coordinates, and JAR targets whose own contents (a top-level `version.json` with a 26.1+ `id`, `net/minecraft/SharedConstants.class`, and no `.java` sources) prove a 26.1+ Minecraft runtime jar; see [docs/tool-reference.md → Lookup Rules](docs/tool-reference.md#lookup-rules). Labelling these Mojang names `obfuscated` is a known issue, kept unchanged for compatibility until a future major release corrects it; see [docs/tool-reference.md → Known issue: obfuscated label on unobfuscated runtimes](docs/tool-reference.md#known-issue-obfuscated-label-on-unobfuscated-runtimes).
305
306
 
306
307
  ### Version Comparison & Symbol Tracking
307
308
 
@@ -40,9 +40,9 @@ export declare const analyzeModShape: {
40
40
  apply: "apply";
41
41
  }>>;
42
42
  detail: z.ZodOptional<z.ZodEnum<{
43
+ full: "full";
43
44
  summary: "summary";
44
45
  standard: "standard";
45
- full: "full";
46
46
  }>>;
47
47
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
48
48
  [x: string]: string;
@@ -88,9 +88,9 @@ export declare const analyzeModSchema: z.ZodObject<{
88
88
  apply: "apply";
89
89
  }>>;
90
90
  detail: z.ZodOptional<z.ZodEnum<{
91
+ full: "full";
91
92
  summary: "summary";
92
93
  standard: "standard";
93
- full: "full";
94
94
  }>>;
95
95
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
96
96
  [x: string]: string;
@@ -62,9 +62,9 @@ export declare const analyzeSymbolShape: {
62
62
  maxRows: z.ZodOptional<z.ZodNumber>;
63
63
  maxCandidates: z.ZodDefault<z.ZodNumber>;
64
64
  detail: z.ZodOptional<z.ZodEnum<{
65
+ full: "full";
65
66
  summary: "summary";
66
67
  standard: "standard";
67
- full: "full";
68
68
  }>>;
69
69
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
70
70
  [x: string]: string;
@@ -132,9 +132,9 @@ export declare const analyzeSymbolSchema: z.ZodObject<{
132
132
  maxRows: z.ZodOptional<z.ZodNumber>;
133
133
  maxCandidates: z.ZodDefault<z.ZodNumber>;
134
134
  detail: z.ZodOptional<z.ZodEnum<{
135
+ full: "full";
135
136
  summary: "summary";
136
137
  standard: "standard";
137
- full: "full";
138
138
  }>>;
139
139
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
140
140
  [x: string]: string;
@@ -1,6 +1,7 @@
1
1
  import { type ResponseDetailLevel } from "../response-utils.js";
2
2
  import type { GetClassMembersInput, GetClassMembersOutput, ResolveArtifactInput, ResolveArtifactOutput } from "../source-service.js";
3
- import type { ArtifactScope, MappingSourcePriority, ResolveArtifactTargetInput, SourceMapping } from "../types.js";
3
+ import type { ArtifactScope, MappingSourcePriority, SourceMapping } from "../types.js";
4
+ import type { SourceLookupTargetInput } from "../tool-schemas.js";
4
5
  import type { MemberProjection } from "../source/class-source/members-builder.js";
5
6
  import { type BatchOutput } from "./batch-runner.js";
6
7
  export type BatchClassMembersDeps = {
@@ -16,7 +17,7 @@ export type BatchClassMembersEntry = {
16
17
  maxMembers?: number;
17
18
  };
18
19
  export type BatchClassMembersInput = {
19
- target: ResolveArtifactTargetInput;
20
+ target: SourceLookupTargetInput;
20
21
  mapping?: SourceMapping;
21
22
  sourcePriority?: MappingSourcePriority;
22
23
  allowDecompile?: boolean;
@@ -16,6 +16,16 @@ export class BatchClassMembersService {
16
16
  concurrency,
17
17
  failFast,
18
18
  resolveSharedArtifact: async () => {
19
+ // target.kind === "artifact" reuses an already-resolved artifactId,
20
+ // short-circuiting resolution exactly as get-class-members's own
21
+ // `kind:"artifact"` target does (src/index.ts normalizeSourceLookupTarget).
22
+ // No provenance is invented for a reused artifact: an unknown id is left
23
+ // to surface per-entry (the same SOURCE_NOT_FOUND getClassMembers raises
24
+ // for an unknown artifactId), since resolveSharedArtifact has no way to
25
+ // validate existence without calling deps.resolveArtifact.
26
+ if (input.target.kind === "artifact") {
27
+ return { artifactId: input.target.artifactId };
28
+ }
19
29
  const resolved = await this.deps.resolveArtifact({
20
30
  target: input.target,
21
31
  mapping: input.mapping,
@@ -49,12 +59,16 @@ export class BatchClassMembersService {
49
59
  const raw = (await this.deps.getClassMembers({
50
60
  artifactId: sharedArtifact.artifactId,
51
61
  // This artifactId is OURS, not the caller's: the shared target was
52
- // resolved above and every entry is dispatched by the result. Without
53
- // saying so, get-class-members reads the bare presence of an
54
- // artifactId as the caller having named the artifact, and reports a
55
- // missing binary jar as their mistake - once per entry - though
56
- // `target` here cannot name an artifact at all. Only a jar the caller
57
- // named themselves is genuinely their choice.
62
+ // resolved (or, for target.kind==="artifact", reused) above and every
63
+ // entry is dispatched by the result. Without saying so, get-class-members
64
+ // reads the bare presence of an artifactId as the caller having named
65
+ // the artifact, and reports a missing binary jar as their mistake - once
66
+ // per entry. A `kind:"artifact"` target is a resolved-id handle, not a
67
+ // caller choice either (mirrors resolveClassArtifactReference's
68
+ // artifactSelectedByFor in inspect-minecraft/handlers/class-members.ts):
69
+ // it says nothing about whether the artifact carries a binary jar and
70
+ // cannot be re-resolved into one that does. Only a jar the caller named
71
+ // themselves is genuinely their choice.
58
72
  artifactSelectedBy: input.target.kind === "jar" ? "caller" : "tool",
59
73
  className: entry.className,
60
74
  access: entry.access,
@@ -1,6 +1,7 @@
1
1
  import { type ResponseDetailLevel } from "../response-utils.js";
2
2
  import type { GetClassSourceInput, GetClassSourceOutput, ResolveArtifactInput, ResolveArtifactOutput } from "../source-service.js";
3
- import type { ArtifactScope, MappingSourcePriority, ResolveArtifactTargetInput, SourceMapping } from "../types.js";
3
+ import type { ArtifactScope, MappingSourcePriority, SourceMapping } from "../types.js";
4
+ import type { SourceLookupTargetInput } from "../tool-schemas.js";
4
5
  import { type BatchOutput } from "./batch-runner.js";
5
6
  type SourceMode = "metadata" | "snippet" | "full";
6
7
  export type BatchClassSourceEntry = {
@@ -13,7 +14,7 @@ export type BatchClassSourceEntry = {
13
14
  outputFile?: string;
14
15
  };
15
16
  export type BatchClassSourceInput = {
16
- target: ResolveArtifactTargetInput;
17
+ target: SourceLookupTargetInput;
17
18
  mapping?: SourceMapping;
18
19
  sourcePriority?: MappingSourcePriority;
19
20
  allowDecompile?: boolean;
@@ -16,6 +16,16 @@ export class BatchClassSourceService {
16
16
  concurrency,
17
17
  failFast,
18
18
  resolveSharedArtifact: async () => {
19
+ // target.kind === "artifact" reuses an already-resolved artifactId,
20
+ // short-circuiting resolution exactly as get-class-source's own
21
+ // `kind:"artifact"` target does (src/index.ts normalizeSourceLookupTarget).
22
+ // No provenance is invented for a reused artifact: an unknown id is left
23
+ // to surface per-entry (the same SOURCE_NOT_FOUND getClassSource raises
24
+ // for an unknown artifactId), since resolveSharedArtifact has no way to
25
+ // validate existence without calling deps.resolveArtifact.
26
+ if (input.target.kind === "artifact") {
27
+ return { artifactId: input.target.artifactId };
28
+ }
19
29
  const resolved = await this.deps.resolveArtifact({
20
30
  target: input.target,
21
31
  mapping: input.mapping,
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import type { DiffClassSignaturesOutput } from "../source-service.js";
3
- import type { CompareVersionsOutput } from "../version-diff-service.js";
3
+ import { type CompareVersionsOutput } from "../version-diff-service.js";
4
4
  import type { GetRegistryDataOutput } from "../registry-service.js";
5
5
  export declare const compareMinecraftShape: {
6
6
  task: z.ZodOptional<z.ZodEnum<{
@@ -38,9 +38,9 @@ export declare const compareMinecraftShape: {
38
38
  registry: z.ZodOptional<z.ZodString>;
39
39
  }, z.core.$strip>], "kind">;
40
40
  detail: z.ZodOptional<z.ZodEnum<{
41
+ full: "full";
41
42
  summary: "summary";
42
43
  standard: "standard";
43
- full: "full";
44
44
  }>>;
45
45
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
46
46
  [x: string]: string;
@@ -86,9 +86,9 @@ export declare const compareMinecraftSchema: z.ZodObject<{
86
86
  registry: z.ZodOptional<z.ZodString>;
87
87
  }, z.core.$strip>], "kind">;
88
88
  detail: z.ZodOptional<z.ZodEnum<{
89
+ full: "full";
89
90
  summary: "summary";
90
91
  standard: "standard";
91
- full: "full";
92
92
  }>>;
93
93
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
94
94
  [x: string]: string;
@@ -122,10 +122,33 @@ type CompareMinecraftDeps = {
122
122
  includeData?: boolean;
123
123
  maxEntriesPerRegistry?: number;
124
124
  }) => Promise<GetRegistryDataOutput>;
125
+ /**
126
+ * Raw `libraries[].name` coordinates for one version, used only by
127
+ * migration-overview to surface library swaps (e.g. LWJGL GLFW replaced by
128
+ * SDL) that a class/registry diff cannot see. Optional so existing callers
129
+ * that do not wire it keep working: migration-overview simply omits the
130
+ * libraries block when this is absent.
131
+ */
132
+ getVersionLibraries?: (input: {
133
+ version: string;
134
+ }) => Promise<string[]>;
135
+ };
136
+ export type CompareMinecraftOptions = {
137
+ /**
138
+ * Deadline in milliseconds for fetching both sides' library lists during
139
+ * migration-overview, so a restart with cached jars but no network access
140
+ * fails fast instead of waiting the full underlying fetch timeout. On
141
+ * expiry the libraries block is omitted and a warning is added; the
142
+ * in-flight fetch is left to continue in the background (it may still
143
+ * warm the version-detail cache). Test-only injection point — never read
144
+ * from an environment variable.
145
+ */
146
+ libraryDiffDeadlineMs?: number;
125
147
  };
126
148
  export declare class CompareMinecraftService {
127
149
  private readonly deps;
128
- constructor(deps: CompareMinecraftDeps);
150
+ private readonly libraryDiffDeadlineMs;
151
+ constructor(deps: CompareMinecraftDeps, options?: CompareMinecraftOptions);
129
152
  execute(input: CompareMinecraftInput): Promise<Record<string, unknown> & {
130
153
  warnings?: string[];
131
154
  }>;
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { diffLibraries } from "../version-diff-service.js";
2
3
  import { createError, ERROR_CODES } from "../errors.js";
3
4
  import { buildIncludeSchema, detailSchema, positiveIntSchema } from "./entry-tool-schema.js";
4
5
  import { buildEntryToolMeta, buildEntryToolResult, createNextAction, createSummarySubject, createTruncationMeta } from "./response-contract.js";
@@ -42,10 +43,37 @@ export const compareMinecraftSchema = z.object(compareMinecraftShape);
42
43
  function compareStatusFromCounts(changedCount) {
43
44
  return changedCount > 0 ? "changed" : "unchanged";
44
45
  }
46
+ /** Default bound on migration-overview's library enrichment (see `libraryDiffDeadlineMs`). */
47
+ const DEFAULT_LIBRARY_DIFF_DEADLINE_MS = 5000;
48
+ class LibraryDiffTimeoutError extends Error {
49
+ }
50
+ /**
51
+ * Races `promise` against a `ms` timer. The timer is always cleared (on
52
+ * either settlement) and unref'd so it can never keep the process alive.
53
+ */
54
+ function withDeadline(promise, ms) {
55
+ return new Promise((resolve, reject) => {
56
+ const timer = setTimeout(() => {
57
+ reject(new LibraryDiffTimeoutError(`timed out after ${ms}ms`));
58
+ }, ms);
59
+ if (typeof timer.unref === "function") {
60
+ timer.unref();
61
+ }
62
+ promise.then((value) => {
63
+ clearTimeout(timer);
64
+ resolve(value);
65
+ }, (error) => {
66
+ clearTimeout(timer);
67
+ reject(error);
68
+ });
69
+ });
70
+ }
45
71
  export class CompareMinecraftService {
46
72
  deps;
47
- constructor(deps) {
73
+ libraryDiffDeadlineMs;
74
+ constructor(deps, options) {
48
75
  this.deps = deps;
76
+ this.libraryDiffDeadlineMs = options?.libraryDiffDeadlineMs ?? DEFAULT_LIBRARY_DIFF_DEADLINE_MS;
49
77
  }
50
78
  async execute(input) {
51
79
  const task = input.task && input.task !== "auto"
@@ -362,6 +390,32 @@ export class CompareMinecraftService {
362
390
  const registrySignals = compare.registry?.summary.registriesChanged ?? 0;
363
391
  const status = compareStatusFromCounts(classSignals + registrySignals);
364
392
  const representativeClassName = compare.classes?.added[0] ?? compare.classes?.removed[0];
393
+ // Library changes (e.g. LWJGL GLFW replaced by SDL) never show up in the
394
+ // class/registry diff above, so fetch and diff them separately. This must
395
+ // never fail the whole migration-overview: a missing dependency wiring, a
396
+ // fetch failure, or a deadline expiry (e.g. offline after a restart, with
397
+ // only cached jars) just omits the block and adds a warning. On a
398
+ // timeout, the in-flight fetch is left running — it may still warm the
399
+ // version-detail cache for a later call — we simply stop waiting on it.
400
+ let librariesResult;
401
+ const libraryWarnings = [];
402
+ if (this.deps.getVersionLibraries) {
403
+ try {
404
+ const [fromLibraries, toLibraries] = await withDeadline(Promise.all([
405
+ this.deps.getVersionLibraries({ version: subject.fromVersion }),
406
+ this.deps.getVersionLibraries({ version: subject.toVersion })
407
+ ]), this.libraryDiffDeadlineMs);
408
+ librariesResult = diffLibraries(fromLibraries, toLibraries);
409
+ }
410
+ catch (error) {
411
+ if (error instanceof LibraryDiffTimeoutError) {
412
+ libraryWarnings.push(`Library comparison between ${subject.fromVersion} and ${subject.toVersion} timed out after ${this.libraryDiffDeadlineMs}ms.`);
413
+ }
414
+ else {
415
+ libraryWarnings.push(`Could not compare libraries between ${subject.fromVersion} and ${subject.toVersion}: ${error instanceof Error ? error.message : String(error)}`);
416
+ }
417
+ }
418
+ }
365
419
  const nextActions = representativeClassName
366
420
  ? [
367
421
  createNextAction("compare-minecraft", {
@@ -399,7 +453,13 @@ export class CompareMinecraftService {
399
453
  }),
400
454
  counts: {
401
455
  classSignals,
402
- registrySignals
456
+ registrySignals,
457
+ ...(librariesResult
458
+ ? {
459
+ librariesAdded: librariesResult.addedCount,
460
+ librariesRemoved: librariesResult.removedCount
461
+ }
462
+ : {})
403
463
  },
404
464
  nextActions
405
465
  },
@@ -413,11 +473,12 @@ export class CompareMinecraftService {
413
473
  : registrySignals > 0
414
474
  ? "registry"
415
475
  : "minimal",
416
- nextActions
476
+ nextActions,
477
+ ...(librariesResult ? { libraries: librariesResult } : {})
417
478
  }
418
479
  }
419
480
  }),
420
- warnings: compare.warnings
481
+ warnings: [...compare.warnings, ...libraryWarnings]
421
482
  };
422
483
  }
423
484
  default:
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
2
  export declare const detailSchema: z.ZodEnum<{
3
+ full: "full";
3
4
  summary: "summary";
4
5
  standard: "standard";
5
- full: "full";
6
6
  }>;
7
7
  export declare const includeGroupSchema: z.ZodEnum<{
8
8
  workspace: "workspace";
@@ -17,6 +17,7 @@ export declare const includeGroupSchema: z.ZodEnum<{
17
17
  samples: "samples";
18
18
  timings: "timings";
19
19
  matrix: "matrix";
20
+ artifact: "artifact";
20
21
  provenance: "provenance";
21
22
  descriptors: "descriptors";
22
23
  diff: "diff";
@@ -27,7 +28,6 @@ export declare const includeGroupSchema: z.ZodEnum<{
27
28
  paths: "paths";
28
29
  owners: "owners";
29
30
  cacheEntries: "cacheEntries";
30
- artifact: "artifact";
31
31
  classes: "classes";
32
32
  }>;
33
33
  export declare const executionModeSchema: z.ZodEnum<{
@@ -251,9 +251,9 @@ export declare const inspectMinecraftShape: {
251
251
  }, z.core.$strip>], "kind">>;
252
252
  includeSnapshots: z.ZodDefault<z.ZodBoolean>;
253
253
  detail: z.ZodOptional<z.ZodEnum<{
254
+ full: "full";
254
255
  summary: "summary";
255
256
  standard: "standard";
256
- full: "full";
257
257
  }>>;
258
258
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
259
259
  [x: string]: string;
@@ -512,9 +512,9 @@ export declare const inspectMinecraftSchema: z.ZodObject<{
512
512
  }, z.core.$strip>], "kind">>;
513
513
  includeSnapshots: z.ZodDefault<z.ZodBoolean>;
514
514
  detail: z.ZodOptional<z.ZodEnum<{
515
+ full: "full";
515
516
  summary: "summary";
516
517
  standard: "standard";
517
- full: "full";
518
518
  }>>;
519
519
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
520
520
  [x: string]: string;
@@ -40,9 +40,9 @@ export declare const manageCacheShape: {
40
40
  projectPath: z.ZodOptional<z.ZodString>;
41
41
  }, z.core.$strip>>;
42
42
  detail: z.ZodOptional<z.ZodEnum<{
43
+ full: "full";
43
44
  summary: "summary";
44
45
  standard: "standard";
45
- full: "full";
46
46
  }>>;
47
47
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
48
48
  [x: string]: string;
@@ -94,9 +94,9 @@ export declare const manageCacheSchema: z.ZodObject<{
94
94
  projectPath: z.ZodOptional<z.ZodString>;
95
95
  }, z.core.$strip>>;
96
96
  detail: z.ZodOptional<z.ZodEnum<{
97
+ full: "full";
97
98
  summary: "summary";
98
99
  standard: "standard";
99
- full: "full";
100
100
  }>>;
101
101
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
102
102
  [x: string]: string;
@@ -15,6 +15,40 @@ async function safeEmit(emitter, stage, payload) {
15
15
  // swallow telemetry failure
16
16
  }
17
17
  }
18
+ const DISCOVER_KIND_LABELS = [
19
+ ["mixins", "mixin configs"],
20
+ ["access-wideners", "access wideners"],
21
+ ["access-transformers", "access transformers"]
22
+ ];
23
+ function joinWithOr(items) {
24
+ if (items.length <= 2) {
25
+ return items.join(" or ");
26
+ }
27
+ return `${items.slice(0, -1).join(", ")}, or ${items[items.length - 1]}`;
28
+ }
29
+ // A run that discovered no files keeps status "ok" (nothing failed), so the
30
+ // headline and a warning must say that nothing was checked; otherwise the
31
+ // result reads as a pass. Only the kinds subject.discover searched are named.
32
+ function describeNothingToValidate(discover, projectPath) {
33
+ const searched = DISCOVER_KIND_LABELS
34
+ .filter(([kind]) => discover.includes(kind))
35
+ .map(([, label]) => label);
36
+ const notOk = "so status \"ok\" does not mean any file passed validation.";
37
+ const accessTransformerHint = discover.includes("access-transformers")
38
+ ? ""
39
+ : " Access Transformer files are searched only when subject.discover includes \"access-transformers\".";
40
+ if (searched.length === 0) {
41
+ return {
42
+ headline: "Nothing to validate: subject.discover selected no file kinds to search.",
43
+ warning: `Nothing was validated: subject.discover selected no file kinds to search, ${notOk}${accessTransformerHint}`
44
+ };
45
+ }
46
+ const list = joinWithOr(searched);
47
+ return {
48
+ headline: `Nothing to validate: no ${list} were found.`,
49
+ warning: `Nothing was validated: no ${list} were found under ${projectPath}, ${notOk}${accessTransformerHint}`
50
+ };
51
+ }
18
52
  export async function handleProjectSummary(deps, input, detail, include, options = {}) {
19
53
  // Forwarded emitter for nested validators and probes; same swallow contract
20
54
  // as safeEmit so a rejecting raw emitter cannot leak into their outcomes.
@@ -27,7 +61,9 @@ export async function handleProjectSummary(deps, input, detail, include, options
27
61
  message: "task=project-summary requires subject.kind=workspace."
28
62
  });
29
63
  }
30
- if (!input.version && !input.preferProjectVersion) {
64
+ // An omitted version is inferred from the project below; only an explicit
65
+ // preferProjectVersion=false opts out of that inference.
66
+ if (!input.version && input.preferProjectVersion === false) {
31
67
  const baseResult = buildEntryToolResult({
32
68
  task: "project-summary",
33
69
  detail,
@@ -52,7 +88,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
52
88
  }
53
89
  ],
54
90
  notes: [
55
- "Pass version explicitly, or retry with preferProjectVersion=true when gradle.properties declares the Minecraft version. The suggested retry sets preferProjectVersion=true for you."
91
+ "Version inference was suppressed by preferProjectVersion=false. Pass version explicitly, or retry without preferProjectVersion=false to infer it from gradle.properties. The suggested retry sets preferProjectVersion=true for you."
56
92
  ]
57
93
  },
58
94
  blocks: {
@@ -79,10 +115,21 @@ export async function handleProjectSummary(deps, input, detail, include, options
79
115
  projectPath,
80
116
  discover
81
117
  });
82
- const detectedProjectVersion = input.preferProjectVersion
118
+ // preferProjectVersion=true overrides an explicit version; an omitted version is
119
+ // inferred (the explicit-false opt-out returned above).
120
+ const detectedProjectVersion = input.preferProjectVersion === true || !input.version
83
121
  ? await deps.detectProjectMinecraftVersion?.(projectPath)
84
122
  : undefined;
85
123
  const resolvedVersion = detectedProjectVersion ?? input.version;
124
+ const versionInferred = !input.version && Boolean(detectedProjectVersion);
125
+ // An inferred version is a project-version resolution: sub-validators and the
126
+ // artifact probe see exactly what an explicit preferProjectVersion=true run sees.
127
+ const effectivePreferProjectVersion = versionInferred ? true : input.preferProjectVersion;
128
+ const versionInferenceWarnings = versionInferred
129
+ ? [
130
+ `version was inferred from the workspace: ${detectedProjectVersion} (source: projectPath:gradle.properties (${projectPath})).`
131
+ ]
132
+ : [];
86
133
  const [mixinConfigs, accessWideners, accessTransformers] = await Promise.all([
87
134
  discover.includes("mixins")
88
135
  ? deps.discoverMixins(projectPath, input.configPaths)
@@ -111,17 +158,20 @@ export async function handleProjectSummary(deps, input, detail, include, options
111
158
  sourcePriority: input.sourcePriority,
112
159
  scope: input.scope
113
160
  }),
161
+ // Retrying without a version would infer, fail, and block again, so the
162
+ // recovery asks for an explicit version instead.
114
163
  nextActions: [
115
164
  {
116
165
  tool: "validate-project",
117
166
  params: {
118
167
  task: "project-summary",
119
- subject: input.subject
168
+ subject: input.subject,
169
+ version: "<minecraft-version>"
120
170
  }
121
171
  }
122
172
  ],
123
173
  notes: [
124
- "Pass version explicitly, or make sure gradle.properties declares the Minecraft version before using preferProjectVersion=true."
174
+ "The Minecraft version could not be inferred from gradle.properties (minecraft_version, mc_version, or minecraftVersion). Retry with version set explicitly: replace \"<minecraft-version>\" in the suggested call with the project's Minecraft version, or declare it in gradle.properties."
125
175
  ]
126
176
  },
127
177
  blocks: {
@@ -150,14 +200,16 @@ export async function handleProjectSummary(deps, input, detail, include, options
150
200
  ]
151
201
  };
152
202
  }
203
+ // Reached only when discovery found nothing (the branch above handles found files).
153
204
  if (!resolvedVersion) {
205
+ const nothingToValidate = describeNothingToValidate(discover, projectPath);
154
206
  const baseResult = buildEntryToolResult({
155
207
  task: "project-summary",
156
208
  detail,
157
209
  include,
158
210
  summary: {
159
211
  status: "ok",
160
- headline: `Validated ${mixinConfigs.length} mixin config(s), ${accessWideners.length} access widener(s), and ${accessTransformers.length} access transformer(s).`,
212
+ headline: nothingToValidate.headline,
161
213
  subject: createSummarySubject({
162
214
  task: "project-summary",
163
215
  kind: input.subject.kind,
@@ -187,11 +239,11 @@ export async function handleProjectSummary(deps, input, detail, include, options
187
239
  return {
188
240
  ...baseResult,
189
241
  ...(tasks ? { tasks } : {}),
190
- warnings: []
242
+ warnings: [nothingToValidate.warning]
191
243
  };
192
244
  }
193
245
  const validationVersion = resolvedVersion;
194
- const warnings = [];
246
+ const warnings = [...versionInferenceWarnings];
195
247
  const mixinDurationStart = Date.now();
196
248
  let validMixins = 0;
197
249
  let partialMixins = 0;
@@ -288,7 +340,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
288
340
  projectPath,
289
341
  gradleUserHome,
290
342
  scope: input.scope,
291
- preferProjectVersion: input.preferProjectVersion
343
+ preferProjectVersion: effectivePreferProjectVersion
292
344
  });
293
345
  if (output.valid) {
294
346
  validAw += 1;
@@ -337,7 +389,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
337
389
  projectPath,
338
390
  gradleUserHome,
339
391
  scope: input.scope,
340
- preferProjectVersion: input.preferProjectVersion
392
+ preferProjectVersion: effectivePreferProjectVersion
341
393
  });
342
394
  if (output.valid) {
343
395
  validAt += 1;
@@ -361,13 +413,20 @@ export async function handleProjectSummary(deps, input, detail, include, options
361
413
  const invalidCount = invalidMixins + invalidAw + invalidAt;
362
414
  const partialCount = partialMixins;
363
415
  const status = invalidCount > 0 ? "invalid" : partialCount > 0 ? "partial" : "ok";
416
+ const nothingToValidate = mixinConfigs.length === 0 && accessWideners.length === 0 && accessTransformers.length === 0
417
+ ? describeNothingToValidate(discover, projectPath)
418
+ : undefined;
419
+ if (nothingToValidate) {
420
+ warnings.push(nothingToValidate.warning);
421
+ }
364
422
  const baseResult = buildEntryToolResult({
365
423
  task: "project-summary",
366
424
  detail,
367
425
  include,
368
426
  summary: {
369
427
  status,
370
- headline: `Validated ${mixinConfigs.length} mixin config(s), ${accessWideners.length} access widener(s), and ${accessTransformers.length} access transformer(s).`,
428
+ headline: nothingToValidate?.headline ??
429
+ `Validated ${mixinConfigs.length} mixin config(s), ${accessWideners.length} access widener(s), and ${accessTransformers.length} access transformer(s).`,
371
430
  subject: createSummarySubject({
372
431
  task: "project-summary",
373
432
  kind: input.subject.kind,
@@ -416,7 +475,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
416
475
  sourcePriority: input.sourcePriority,
417
476
  gradleUserHome,
418
477
  scope: input.scope,
419
- preferProjectVersion: input.preferProjectVersion,
478
+ preferProjectVersion: effectivePreferProjectVersion,
420
479
  mixinDiscoveryCount: mixinConfigs.length,
421
480
  mixinCaughtErrors,
422
481
  mixinCounts: { ok: validMixins, partial: partialMixins, invalid: invalidMixins },