@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
package/CHANGELOG.md CHANGED
@@ -7,6 +7,49 @@ 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
+
40
+ ## [7.0.0] - 2026-09-13
41
+
42
+ ### Fixed
43
+
44
+ - Several more ways a fault during the stdio supervisor's own fault-recovery work could leave a request permanently unanswered, or answered twice, are closed. A `validate-project` request could be left marked as still running forever if an internal recovery step for an earlier fault on that same request itself failed, blocking every later `validate-project` call for the rest of the session; that is now cleared first. If a client re-sent `initialize` to a worker that was already ready and the supervisor then faulted while recovering from that duplicate handshake, other requests still in flight on the worker being replaced were not answered and could hang for the rest of the session; they are now answered before the worker is replaced. And a request taken from the internal queue whose own reply step then failed in a narrow way could be left with no reply at all, or, in a different case, answered a second, spurious time; exactly one reply is now produced either way. Each of these requires an internal fault that is not known to occur in current Node.js and has not been observed in production.
45
+ - Two remaining internal call sites that build an error's logged description now use the same safe string conversion already used elsewhere for this reason, so a thrown value whose own string conversion itself throws can no longer slip past them either. Cosmetic; there is no known way to trigger it.
46
+ - Four more ways a fault on the stdio connection to the worker process could leave a request unanswered are closed. A write error to the worker other than a broken pipe used to be logged and otherwise ignored, leaving the connection pointed at a worker nothing could send to again, so every request already sent to it — and every request sent afterward — hung for the rest of the session; that fault now retires the broken worker and starts a replacement, answering the stranded request along the way. If that same fault lands while a still-starting successor worker is replaying the client's `initialize` handshake, the client's `initialize` call now fails immediately instead of waiting silently across every later restart attempt. A successful reply to `initialize` could also be lost outright if writing it to the client failed at the same moment as the fallback log message the supervisor writes about that failure; both are now recovered from together. And once `initialize` completes, a client may legally reuse its request id for a later call; if the worker then exited before answering that reused id, it used to be mistaken for the already-finished `initialize` and silently dropped instead of receiving its own reply.
47
+ - A large request sent in line-delimited framing right after a `Content-Length`-framed one was wrongly rejected as an oversized header, because the 8 KiB header-size ceiling kept applying even once the reader had already moved on to line framing; such a request is now accepted up to the ordinary frame-size limit. Separately, when an oversized line arrived without its terminating newline yet, discarding it did not also consume the rest of that same line once the newline did arrive; the tail could then be re-read as if it were a fresh, legitimate request of its own. That tail is now discarded along with the rest of the line it belongs to.
48
+ - Deleting a downloaded-jar entry with `manage-cache` could destroy a different jar than the one that was listed, when another resolve finished writing a fresh copy to the same cache slot in between; the delete now verifies it is still removing the jar it originally listed. A delete that could not actually remove a file — for example because the cache directory is read-only — is no longer reported as successful; it now returns a warning and the entry is not counted as deleted. Checking a cached download's size no longer treats a permission error the same as a missing file; previously that silently triggered a fresh download instead of surfacing the actionable error.
49
+ - `find-class` now resolves the correct fully qualified name for a class nested two or more levels inside another class. It previously dropped every enclosing type past the first level, producing a name that does not exist among the compiled classes, so a follow-up call using that name — reading its members or source, for example — failed with a spurious not-found error. A class nested exactly one level deep was already correct and is unaffected.
50
+ - An unrecognized failure opening the artifact index database — for example the database being locked or the disk being full — is now reported as the documented database-failure error instead of escaping unclassified. Previously only a failure already recognized as an I/O problem, a schema mismatch, or corruption was reported that way.
51
+ - Loading a Yarn/Tiny mapping file from a Maven-hosted jar is now bounded the same way nested-jar and download extraction already are. A `.tiny` entry inside such a jar could previously be decompressed in full with no size limit, so a small, highly compressed entry could expand to multiple gigabytes and exhaust memory before mapping parsing ever started.
52
+
10
53
  ## [7.0.0-rc.3] - 2026-09-07
11
54
 
12
55
  ### Changed
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
 
@@ -42,6 +42,22 @@ type CacheEntryPage = {
42
42
  * while still allowing major.minor sweeps where "1.21" matches "1.21.4".
43
43
  */
44
44
  export declare function pathContainsVersion(path: string, version: string): boolean;
45
+ /**
46
+ * The identity `deleteEntries` captures for a downloads-cache jar at listing
47
+ * time, so a prune that only ever looked at the file once cannot destroy a
48
+ * concurrent resolve's freshly-written replacement at the same path.
49
+ *
50
+ * A missing, unreadable, or malformed sidecar answers with no identity to
51
+ * prove - {@link discardCachedDownload} already treats that as licence to
52
+ * evict unconditionally, the "cannot prove otherwise" rule this module's own
53
+ * sidecar-less entries already rely on elsewhere.
54
+ *
55
+ * Exported for direct testing, the same way {@link pathContainsVersion} is.
56
+ */
57
+ export declare function readDownloadEntryIdentity(jarPath: string): Promise<{
58
+ url: string;
59
+ contentSha256: string;
60
+ } | undefined>;
45
61
  export type CacheRegistryConfig = {
46
62
  cacheDir: string;
47
63
  sqlitePath: string;
@@ -4,7 +4,7 @@ import { join, resolve } from "node:path";
4
4
  import { mapWithConcurrencyLimit } from "./concurrency.js";
5
5
  import { createError, ERROR_CODES } from "./errors.js";
6
6
  import { normalizeOptionalPathForHost } from "./path-converter.js";
7
- import { downloadSidecarPath, isDownloadSidecarPath } from "./repo-downloader.js";
7
+ import { discardCachedDownload, downloadSidecarPath, isDownloadSidecarPath } from "./repo-downloader.js";
8
8
  import { openDatabase } from "./storage/db.js";
9
9
  import { getProcessWorkspaceContextCache } from "./workspace-context-cache.js";
10
10
  export const PUBLIC_CACHE_KINDS = [
@@ -528,6 +528,33 @@ async function downloadSidecarSizeBytes(downloadPath) {
528
528
  return 0;
529
529
  }
530
530
  }
531
+ /**
532
+ * The identity `deleteEntries` captures for a downloads-cache jar at listing
533
+ * time, so a prune that only ever looked at the file once cannot destroy a
534
+ * concurrent resolve's freshly-written replacement at the same path.
535
+ *
536
+ * A missing, unreadable, or malformed sidecar answers with no identity to
537
+ * prove - {@link discardCachedDownload} already treats that as licence to
538
+ * evict unconditionally, the "cannot prove otherwise" rule this module's own
539
+ * sidecar-less entries already rely on elsewhere.
540
+ *
541
+ * Exported for direct testing, the same way {@link pathContainsVersion} is.
542
+ */
543
+ export async function readDownloadEntryIdentity(jarPath) {
544
+ try {
545
+ const parsed = JSON.parse(await readFile(downloadSidecarPath(jarPath), "utf8"));
546
+ if (typeof parsed.url === "string" &&
547
+ typeof parsed.contentSha256 === "string" &&
548
+ parsed.contentSha256.length > 0) {
549
+ return { url: parsed.url, contentSha256: parsed.contentSha256 };
550
+ }
551
+ }
552
+ catch {
553
+ // Missing, unreadable, or malformed sidecar: nothing to identify these
554
+ // bytes with.
555
+ }
556
+ return undefined;
557
+ }
531
558
  /**
532
559
  * Binary-remap cache entries are keyed by the final artifact id even when the
533
560
  * on-disk entry is a corrupt final directory or a leftover temp path.
@@ -682,7 +709,19 @@ export function createCacheRegistry(config) {
682
709
  async deleteEntries(input) {
683
710
  const entries = await collectEntries(input.cacheKinds, input.selector);
684
711
  const selectedBytes = entries.reduce((total, entry) => total + entry.sizeBytes, 0);
712
+ const warnings = [];
713
+ const failedEntries = [];
685
714
  if (input.executionMode === "apply") {
715
+ // Captured now, against the listing this call just made, rather than
716
+ // re-read right before each unlink below: the identity has to describe
717
+ // what THIS call selected, not whatever a concurrent resolve may have
718
+ // already replaced it with by the time the loop below reaches it.
719
+ const downloadIdentities = new Map();
720
+ for (const entry of entries) {
721
+ if (entry.cacheKind === "downloads") {
722
+ downloadIdentities.set(entry.path, await readDownloadEntryIdentity(entry.path));
723
+ }
724
+ }
686
725
  const db = openDb(config);
687
726
  try {
688
727
  for (const entry of entries) {
@@ -695,12 +734,40 @@ export function createCacheRegistry(config) {
695
734
  continue;
696
735
  }
697
736
  if (entry.cacheKind === "downloads") {
698
- // The sidecar is part of this entry, so it goes with the jar —
699
- // outside the existsSync guard below, so a jar that vanished
700
- // out-of-band since the listing still takes its sidecar with it
701
- // instead of leaving an orphan behind. `force` makes a missing
702
- // sidecar a no-op.
703
- await rm(downloadSidecarPath(entry.path), { force: true });
737
+ // Route through the same sidecar-identity check every other
738
+ // eviction of this shared cache goes through, instead of an
739
+ // unconditional unlink that cannot tell a poisoned jar from a
740
+ // concurrent resolve's good one sitting at the same path. This
741
+ // also retires the sidecar, so it goes with the jar exactly as
742
+ // before - but only when the check above says the jar is still
743
+ // the one this call listed. See discardCachedDownload's doc
744
+ // comment in repo-downloader.ts for what the check does and does
745
+ // not close.
746
+ const expectedIdentity = downloadIdentities.get(entry.path);
747
+ discardCachedDownload(entry.path, expectedIdentity);
748
+ // discardCachedDownload swallows a genuine unlink failure
749
+ // (EACCES, EBUSY, a locked file, ...) - documented there as an
750
+ // accepted limitation for its own best-effort callers. That is
751
+ // wrong for this explicit, user-facing delete/prune request:
752
+ // verify the postcondition instead of trusting the call's
753
+ // silence. The jar surviving is only a LEGITIMATE decline when
754
+ // the identity recorded beside it now genuinely differs from
755
+ // what this call captured at listing time - i.e. a concurrent
756
+ // resolve's replacement is sitting there, exactly the case
757
+ // discardCachedDownload itself declines for. A jar that is
758
+ // still present with no such change (or no readable identity at
759
+ // all) means the removal itself failed.
760
+ if (existsSync(entry.path)) {
761
+ const currentIdentity = await readDownloadEntryIdentity(entry.path);
762
+ const declinedForConcurrentReplacement = expectedIdentity !== undefined &&
763
+ currentIdentity !== undefined &&
764
+ currentIdentity.contentSha256 !== expectedIdentity.contentSha256;
765
+ if (!declinedForConcurrentReplacement) {
766
+ warnings.push(`Could not delete cached download (it may be locked or read-only): ${entry.path}`);
767
+ failedEntries.push(entry);
768
+ }
769
+ }
770
+ continue;
704
771
  }
705
772
  if (existsSync(entry.path)) {
706
773
  // Only binary-remap inventory can return directories as entries;
@@ -713,10 +780,11 @@ export function createCacheRegistry(config) {
713
780
  db?.close();
714
781
  }
715
782
  }
783
+ const failedBytes = failedEntries.reduce((total, entry) => total + entry.sizeBytes, 0);
716
784
  return {
717
- deletedEntries: entries.length,
718
- deletedBytes: selectedBytes,
719
- warnings: []
785
+ deletedEntries: entries.length - failedEntries.length,
786
+ deletedBytes: selectedBytes - failedBytes,
787
+ warnings
720
788
  };
721
789
  },
722
790
  async pruneEntries(input) {
@@ -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;