@adhisang/minecraft-modding-mcp 6.3.0 → 7.0.0-rc.1

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 (101) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +13 -3
  3. package/dist/cache-policy.d.ts +71 -0
  4. package/dist/cache-policy.js +83 -0
  5. package/dist/cache-registry.js +45 -8
  6. package/dist/cli.js +74 -3
  7. package/dist/compat-stdio-transport.d.ts +1 -1
  8. package/dist/compat-stdio-transport.js +13 -1
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +8 -2
  11. package/dist/decompiler/vineflower.d.ts +1 -0
  12. package/dist/decompiler/vineflower.js +8 -5
  13. package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
  14. package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
  15. package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
  16. package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
  17. package/dist/entry-tools/entry-tool-schema.js +4 -1
  18. package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
  19. package/dist/entry-tools/inspect-minecraft/internal.js +50 -13
  20. package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
  21. package/dist/entry-tools/manage-cache-service.d.ts +81 -91
  22. package/dist/entry-tools/validate-project/cases/mixin.js +26 -6
  23. package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
  24. package/dist/entry-tools/validate-project-service.d.ts +164 -592
  25. package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
  26. package/dist/entry-tools/verify-mixin-target-service.js +19 -1
  27. package/dist/era-classifier.d.ts +161 -0
  28. package/dist/era-classifier.js +292 -0
  29. package/dist/error-mapping.d.ts +76 -0
  30. package/dist/error-mapping.js +116 -8
  31. package/dist/index.d.ts +42 -4
  32. package/dist/index.js +636 -473
  33. package/dist/java-process.d.ts +2 -0
  34. package/dist/java-process.js +22 -2
  35. package/dist/json-rpc-framing.d.ts +77 -1
  36. package/dist/json-rpc-framing.js +249 -13
  37. package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
  38. package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
  39. package/dist/mapping/loaders/tiny-loom.js +45 -33
  40. package/dist/mapping/loaders/tiny-maven.js +6 -11
  41. package/dist/mapping/parsers/tiny.d.ts +57 -0
  42. package/dist/mapping/parsers/tiny.js +99 -22
  43. package/dist/mapping-service.d.ts +19 -0
  44. package/dist/mapping-service.js +93 -9
  45. package/dist/maven-resolver.d.ts +18 -0
  46. package/dist/maven-resolver.js +20 -0
  47. package/dist/mcp-helpers.d.ts +19 -2
  48. package/dist/mcp-helpers.js +58 -9
  49. package/dist/minecraft-explorer-service.d.ts +1 -1
  50. package/dist/mixin/types.d.ts +8 -0
  51. package/dist/mod-analyzer.js +7 -7
  52. package/dist/mod-decompile-service.js +1 -0
  53. package/dist/nbt/java-nbt-codec.js +12 -2
  54. package/dist/nbt/json-patch.js +14 -3
  55. package/dist/nbt/pipeline.js +40 -3
  56. package/dist/nbt/typed-json.js +26 -1
  57. package/dist/registration-adapter.d.ts +32 -0
  58. package/dist/registration-adapter.js +52 -0
  59. package/dist/repo-downloader.d.ts +165 -0
  60. package/dist/repo-downloader.js +568 -10
  61. package/dist/request-context.d.ts +7 -0
  62. package/dist/request-context.js +9 -0
  63. package/dist/resources.d.ts +1 -1
  64. package/dist/resources.js +25 -19
  65. package/dist/server-identity.d.ts +27 -0
  66. package/dist/server-identity.js +26 -0
  67. package/dist/source/access-validate.js +53 -0
  68. package/dist/source/artifact-resolver.d.ts +81 -2
  69. package/dist/source/artifact-resolver.js +227 -15
  70. package/dist/source/class-source.d.ts +36 -0
  71. package/dist/source/class-source.js +222 -38
  72. package/dist/source/did-you-mean.d.ts +12 -1
  73. package/dist/source/did-you-mean.js +6 -2
  74. package/dist/source/file-access.js +150 -46
  75. package/dist/source/indexer.js +1 -0
  76. package/dist/source/shared-utils.d.ts +21 -0
  77. package/dist/source/shared-utils.js +23 -0
  78. package/dist/source-resolver.js +224 -57
  79. package/dist/source-service.d.ts +12 -1
  80. package/dist/stdio-supervisor.d.ts +357 -2
  81. package/dist/stdio-supervisor.js +1031 -80
  82. package/dist/storage/db.d.ts +2 -1
  83. package/dist/storage/db.js +15 -8
  84. package/dist/synthetic-decorator.d.ts +24 -0
  85. package/dist/synthetic-decorator.js +48 -0
  86. package/dist/tool-guidance.d.ts +17 -1
  87. package/dist/tool-guidance.js +323 -18
  88. package/dist/tool-schema-registry.d.ts +2 -0
  89. package/dist/tool-schema-registry.js +4 -0
  90. package/dist/tool-schemas.d.ts +2212 -3919
  91. package/dist/tool-schemas.js +33 -7
  92. package/dist/types.d.ts +35 -0
  93. package/dist/v1-parity-schemas.d.ts +7 -0
  94. package/dist/v1-parity-schemas.js +5584 -0
  95. package/dist/version-diff-service.d.ts +33 -0
  96. package/dist/version-diff-service.js +148 -3
  97. package/dist/version-service.js +36 -14
  98. package/dist/warning-details.js +18 -1
  99. package/docs/README-ja.md +5 -3
  100. package/docs/tool-reference.md +196 -19
  101. package/package.json +13 -8
@@ -3,8 +3,8 @@ import { basename, dirname, join, resolve as resolvePath } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import fastGlob from "fast-glob";
5
5
  import { createError, ERROR_CODES } from "./errors.js";
6
- import { buildRemoteBinaryUrls, buildRemoteSourceUrls, hasExistingJar, parseCoordinate, normalizedCoordinateValue } from "./maven-resolver.js";
7
- import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
6
+ import { buildRemoteBinaryUrls, buildRemoteSourceUrls, hasExistingJar, isMutableMavenCoordinate, parseCoordinate, normalizedCoordinateValue } from "./maven-resolver.js";
7
+ import { defaultDownloadPath, discardCachedDownload, resolveCachedDownload } from "./repo-downloader.js";
8
8
  import { artifactSignatureFromFile, normalizeJarPath } from "./path-resolver.js";
9
9
  import { stableArtifactId } from "./config.js";
10
10
  import { hasAnyJarEntry, hasJavaSourceExtension } from "./source-jar-reader.js";
@@ -16,7 +16,13 @@ async function hasJavaSources(jarPath) {
16
16
  if (!hasExistingJar(jarPath)) {
17
17
  return false;
18
18
  }
19
- return hasAnyJarEntry(jarPath, hasJavaSourceExtension);
19
+ // Archive errors deliberately propagate. This runs on the jar-target path
20
+ // (:336) where the jar IS the subject, and the reader's rejection of an
21
+ // unsafe archive - a traversal-named entry, for one - is the fail-closed
22
+ // refusal itself. Swallowing it here would downgrade "this archive is not
23
+ // safe to open" into "this archive has no sources" and let resolution
24
+ // continue past a jar it had already refused.
25
+ return await hasAnyJarEntry(jarPath, hasJavaSourceExtension);
20
26
  }
21
27
  function resolveExactJarSourceCandidate(inputJarPath) {
22
28
  const directory = dirname(inputJarPath);
@@ -70,17 +76,51 @@ function resolveLocalCoordinateCandidates(localM2Path, coordinate) {
70
76
  }
71
77
  return [...existing];
72
78
  }
73
- function resolveLocalCoordinateBinaryCandidate(localM2Path, coordinate) {
79
+ function resolveLocalCoordinateBinaryCandidates(localM2Path, coordinate) {
74
80
  const parsed = parseCoordinate(coordinate);
75
81
  const groupPath = parsed.groupId.replace(/\./g, "/");
76
82
  const baseDir = resolvePath(localM2Path, groupPath, parsed.artifactId, parsed.version);
77
83
  const base = `${parsed.artifactId}-${parsed.version}`;
78
84
  const classifierSuffix = parsed.classifier ? `-${parsed.classifier}` : "";
79
- const candidates = [
80
- resolvePath(baseDir, `${base}${classifierSuffix}.jar`),
81
- ...(classifierSuffix ? [resolvePath(baseDir, `${base}.jar`)] : [])
82
- ];
83
- return candidates.find((candidate) => hasExistingJar(candidate));
85
+ const exactCandidate = resolvePath(baseDir, `${base}${classifierSuffix}.jar`);
86
+ const fallbackCandidates = classifierSuffix ? [resolvePath(baseDir, `${base}.jar`)] : [];
87
+ const exact = hasExistingJar(exactCandidate) ? exactCandidate : undefined;
88
+ const companion = exact ?? fallbackCandidates.find((candidate) => hasExistingJar(candidate));
89
+ return { exact, companion };
90
+ }
91
+ /**
92
+ * Whether a jar on disk can actually be opened as an archive.
93
+ *
94
+ * `hasExistingJar` only proves that a file is present: an interrupted Gradle or
95
+ * Maven copy leaves a 0-byte or truncated jar behind that passes that check and
96
+ * then throws inside the decompiler on every call. Every sources branch of the
97
+ * cascade already proves its candidate by opening the zip; a binary branch that
98
+ * suppresses a remote download owes the caller the same proof.
99
+ */
100
+ async function isReadableJarArchive(jarPath) {
101
+ try {
102
+ return await hasAnyJarEntry(jarPath, () => true);
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ }
108
+ /**
109
+ * The first candidate that can actually be opened as an archive, in preference
110
+ * order.
111
+ *
112
+ * Preference order is not the same as a pick: choosing the most-preferred
113
+ * candidate and only then proving it lets one corrupt file veto every good one
114
+ * behind it. Each candidate is proved before the next is considered, and the
115
+ * archives are opened lazily, so the usual case still opens exactly one zip.
116
+ */
117
+ async function firstReadableJarArchive(candidates) {
118
+ for (const candidate of candidates) {
119
+ if (await isReadableJarArchive(candidate)) {
120
+ return candidate;
121
+ }
122
+ }
123
+ return undefined;
84
124
  }
85
125
  function resolveGradleUserHome() {
86
126
  const configured = process.env.GRADLE_USER_HOME?.trim();
@@ -98,10 +138,8 @@ async function resolveGradleCacheCoordinateCandidate(coordinate) {
98
138
  `${base}${classifierSuffix}-sources.jar`,
99
139
  ...(classifierSuffix ? [`${base}-sources.jar`] : [])
100
140
  ];
101
- const preferredBinaryNames = [
102
- `${base}${classifierSuffix}.jar`,
103
- ...(classifierSuffix ? [`${base}.jar`] : [])
104
- ];
141
+ const exactBinaryName = `${base}${classifierSuffix}.jar`;
142
+ const preferredBinaryNames = [exactBinaryName, ...(classifierSuffix ? [`${base}.jar`] : [])];
105
143
  let discoveredFiles = [];
106
144
  try {
107
145
  discoveredFiles = await fastGlob.glob("*/*", {
@@ -128,7 +166,7 @@ async function resolveGradleCacheCoordinateCandidate(coordinate) {
128
166
  if (!sourceJarPath && !binaryJarPath) {
129
167
  return undefined;
130
168
  }
131
- return { sourceJarPath, binaryJarPath };
169
+ return { sourceJarPath, binaryJarPath, exactBinaryJarPath: pickFirst([exactBinaryName]) };
132
170
  }
133
171
  function resolveRemoteBinaryCandidate(coordinate, repos) {
134
172
  return buildRemoteBinaryUrls(repos, coordinate);
@@ -150,6 +188,20 @@ function artifactIdForCoordinate(coordinate, source, signature, mappingVariant =
150
188
  function resolvedAtNow() {
151
189
  return new Date().toISOString();
152
190
  }
191
+ /** Shared shape for every artifact the coordinate cascade can return. */
192
+ function coordinateArtifact(spec) {
193
+ return {
194
+ artifactId: artifactIdForCoordinate(spec.coordinate, spec.idSource, spec.signature, spec.mappingVariant ?? "pass"),
195
+ artifactSignature: spec.signature,
196
+ origin: spec.origin,
197
+ sourceJarPath: spec.sourceJarPath,
198
+ binaryJarPath: spec.binaryJarPath,
199
+ coordinate: spec.coordinate,
200
+ repoUrl: spec.repoUrl,
201
+ isDecompiled: spec.isDecompiled,
202
+ resolvedAt: resolvedAtNow()
203
+ };
204
+ }
153
205
  export async function resolveSourceTarget(input, options, explicitConfig) {
154
206
  const repos = options.preferredRepos?.length ? options.preferredRepos : explicitConfig.sourceRepos;
155
207
  let sawRemoteRepoFailure = false;
@@ -216,51 +268,109 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
216
268
  });
217
269
  }
218
270
  const coordinate = normalizedCoordinateValue(input.value);
271
+ // Maven republishes -SNAPSHOT coordinates under the same name, so their cached
272
+ // downloads have to be confirmed with the repository; every other version is
273
+ // immutable and is served straight from the download cache. Decided once so
274
+ // the sources leg and the binary leg cannot disagree.
275
+ const downloadFreshness = isMutableMavenCoordinate(coordinate)
276
+ ? "revalidate"
277
+ : "immutable";
219
278
  const isTransientFailure = (statusCode) => statusCode === undefined || statusCode >= 500 || statusCode === 429;
279
+ const localM2Binary = resolveLocalCoordinateBinaryCandidates(explicitConfig.localM2Path, coordinate);
280
+ const localM2BinaryJarPath = localM2Binary.companion;
220
281
  for (const candidate of resolveLocalCoordinateCandidates(explicitConfig.localM2Path, coordinate)) {
221
282
  if (await hasJavaSources(candidate)) {
222
283
  const signature = readStatsSignature(candidate);
223
- return {
224
- artifactId: artifactIdForCoordinate(coordinate, "local-m2", signature),
225
- artifactSignature: signature,
284
+ return coordinateArtifact({
285
+ coordinate,
286
+ idSource: "local-m2",
287
+ signature,
226
288
  origin: "local-m2",
227
289
  sourceJarPath: candidate,
228
- binaryJarPath: resolveLocalCoordinateBinaryCandidate(explicitConfig.localM2Path, coordinate),
229
- coordinate,
230
- isDecompiled: false,
231
- resolvedAt: resolvedAtNow()
232
- };
290
+ binaryJarPath: localM2BinaryJarPath,
291
+ isDecompiled: false
292
+ });
233
293
  }
234
294
  }
235
295
  const gradleCacheCandidate = await resolveGradleCacheCoordinateCandidate(coordinate);
236
296
  if (gradleCacheCandidate?.sourceJarPath && (await hasJavaSources(gradleCacheCandidate.sourceJarPath))) {
237
297
  const signature = readStatsSignature(gradleCacheCandidate.sourceJarPath);
238
- return {
239
- artifactId: artifactIdForCoordinate(coordinate, "local-m2", signature),
240
- artifactSignature: signature,
298
+ return coordinateArtifact({
299
+ coordinate,
300
+ idSource: "local-m2",
301
+ signature,
241
302
  origin: "local-m2",
242
303
  sourceJarPath: gradleCacheCandidate.sourceJarPath,
243
304
  binaryJarPath: gradleCacheCandidate.binaryJarPath,
244
- coordinate,
245
- isDecompiled: false,
246
- resolvedAt: resolvedAtNow()
247
- };
305
+ isDecompiled: false
306
+ });
248
307
  }
308
+ // Both local branches above are gated on SOURCES: a module that ships a binary jar
309
+ // locally but publishes no sources jar falls through them, and the binary jar they
310
+ // already discovered would be dropped even though it is still perfectly usable. Keep
311
+ // it here so the artifact that is ultimately returned still carries a binaryJarPath
312
+ // for binary-only consumers (get-class-members and friends).
313
+ //
314
+ // Existence alone is not proof: an interrupted local copy would otherwise get
315
+ // to veto a perfectly good companion in the other cache, exactly the failure
316
+ // `firstReadableJarArchive` exists to prevent elsewhere in this cascade. Kept
317
+ // as a candidate list rather than resolved here, so the readable one is only
318
+ // picked - lazily, at the one site that attaches it - once a remote sources
319
+ // jar has actually been found and this is known to be needed.
320
+ const localBinaryJarCandidates = [
321
+ ...new Set([localM2BinaryJarPath, gradleCacheCandidate?.binaryJarPath].filter((candidate) => candidate !== undefined))
322
+ ];
323
+ // The companion above may be the classifier-less jar, which is a different
324
+ // artifact than a classified coordinate asks for. That substitution is fine
325
+ // for a jar that merely rides along beside a sources jar, and wrong for one
326
+ // returned as the resolution itself, so the decompile branch below considers
327
+ // only jars whose name spells the coordinate out in full.
328
+ //
329
+ // A list rather than a single pick: ~/.m2 is preferred over the Gradle cache,
330
+ // but preferring it must not mean an interrupted copy sitting there gets to
331
+ // veto a perfectly good jar behind it - forcing a needless download, or an
332
+ // outright failure when offline.
333
+ const localDecompilableBinaryCandidates = [
334
+ ...new Set([localM2Binary.exact, gradleCacheCandidate?.exactBinaryJarPath].filter((candidate) => candidate !== undefined))
335
+ ];
249
336
  const remoteSourceUrls = buildRemoteSourceUrls(repos, coordinate);
250
337
  for (let index = 0; index < remoteSourceUrls.length; index++) {
251
338
  const sourceUrl = remoteSourceUrls[index];
252
339
  const hasNextAttempt = index < remoteSourceUrls.length - 1;
253
340
  try {
254
- const download = await downloadToCache(sourceUrl, defaultDownloadPath(explicitConfig.cacheDir, sourceUrl), {
341
+ const sourceDestinationPath = defaultDownloadPath(explicitConfig.cacheDir, sourceUrl);
342
+ const download = await resolveCachedDownload(sourceUrl, sourceDestinationPath, {
343
+ freshness: downloadFreshness,
255
344
  retries: explicitConfig.fetchRetries,
256
345
  timeoutMs: explicitConfig.fetchTimeoutMs
257
346
  });
258
- if (!download.ok || !download.path || !(await hasJavaSources(download.path))) {
259
- const transient = download.ok
260
- ? false
261
- : download.statusCode !== 404 && isTransientFailure(download.statusCode);
347
+ let sourceJarHasJavaSources;
348
+ try {
349
+ sourceJarHasJavaSources = download.ok && (await hasJavaSources(download.path));
350
+ }
351
+ catch {
352
+ // The download reports success, but the bytes are not a readable
353
+ // archive - the same "200 but not really a jar" shape the binary leg
354
+ // guards against below. Evict it now: this url is immutable for every
355
+ // non-SNAPSHOT coordinate, so a future call would otherwise be served
356
+ // the same poison with no request made at all.
357
+ if (download.ok) {
358
+ discardCachedDownload(download.path);
359
+ }
360
+ sourceJarHasJavaSources = false;
361
+ }
362
+ if (!download.ok || !sourceJarHasJavaSources) {
363
+ // Transience only decides the error CODE at the end of the cascade: an
364
+ // unstable repository earns ERR_REPO_FETCH_FAILED, while "this
365
+ // repository does not publish it" stays a plain not-found.
366
+ const transient = !download.ok && isTransientFailure(download.statusCode);
262
367
  sawRemoteRepoFailure = sawRemoteRepoFailure || transient;
263
- if (hasNextAttempt && (transient || download.ok)) {
368
+ // Moving off this repository is reportable however it happened. A
369
+ // withdrawn artifact reaches here as an ordinary failure now that the
370
+ // downloader refuses to launder a 404 into a stale success, and leaving
371
+ // that silent would hide the one event that explains why the resolved
372
+ // artifact came from somewhere else.
373
+ if (hasNextAttempt) {
264
374
  options.onRepoFailover?.({
265
375
  stage: "source",
266
376
  repoUrl: sourceUrl,
@@ -272,17 +382,20 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
272
382
  }
273
383
  continue;
274
384
  }
275
- const signature = `${download.contentLength ?? 0}:${download.etag ?? ""}:${download.lastModified ?? ""}`;
276
- return {
277
- artifactId: artifactIdForCoordinate(coordinate, "remote-repo", signature),
278
- artifactSignature: signature,
385
+ // Identity follows the bytes. The HTTP validators that used to form this
386
+ // signature rotate whenever a CDN or repository migration happens, even
387
+ // when the jar is byte-identical.
388
+ const signature = download.contentSha256;
389
+ return coordinateArtifact({
390
+ coordinate,
391
+ idSource: "remote-repo",
392
+ signature,
279
393
  origin: "remote-repo",
280
394
  sourceJarPath: download.path,
281
- coordinate,
395
+ binaryJarPath: await firstReadableJarArchive(localBinaryJarCandidates),
282
396
  repoUrl: sourceUrl,
283
- isDecompiled: false,
284
- resolvedAt: resolvedAtNow()
285
- };
397
+ isDecompiled: false
398
+ });
286
399
  }
287
400
  catch (caughtError) {
288
401
  sawRemoteRepoFailure = true;
@@ -306,43 +419,97 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
306
419
  details: { coordinate }
307
420
  });
308
421
  }
422
+ // A module that publishes no sources jar but whose binary jar is already on
423
+ // local disk has nothing left to fetch: ingest decompiles the binary, and the
424
+ // local copy is the same artifact the repository would hand back. Deliberately
425
+ // placed after the allowDecompile guard - returning it earlier would perform
426
+ // the decompile the caller just declined. The dedicated "local-binary" id space
427
+ // keeps it apart from the remote "decompiled" one; no coordinate resolve has
428
+ // ever returned a local binary jar, so nothing needs migrating.
429
+ //
430
+ // The jar has to be a readable archive before it may stand in for the download:
431
+ // a truncated local copy would otherwise be handed to the decompiler on every
432
+ // call with no way out. When none of the candidates is, the remote binary loop
433
+ // below is exactly the repair path, so this falls through to it instead of
434
+ // failing. The zips are opened here and only here - nothing upstream has looked
435
+ // inside these jars, and control only reaches this point when the branch is
436
+ // about to be taken.
437
+ const localDecompilableBinaryJarPath = await firstReadableJarArchive(localDecompilableBinaryCandidates);
438
+ if (localDecompilableBinaryJarPath) {
439
+ const signature = readStatsSignature(localDecompilableBinaryJarPath);
440
+ return coordinateArtifact({
441
+ coordinate,
442
+ idSource: "local-binary",
443
+ signature,
444
+ origin: "local-m2",
445
+ binaryJarPath: localDecompilableBinaryJarPath,
446
+ isDecompiled: true,
447
+ mappingVariant: options.mappingVariant ?? "pass"
448
+ });
449
+ }
309
450
  const binaryCandidates = resolveRemoteBinaryCandidate(coordinate, repos);
310
451
  for (let index = 0; index < binaryCandidates.length; index++) {
311
452
  const binaryUrl = binaryCandidates[index];
312
453
  const hasNextAttempt = index < binaryCandidates.length - 1;
313
454
  try {
314
- const downloaded = await downloadToCache(binaryUrl, defaultDownloadPath(explicitConfig.cacheDir, binaryUrl), {
455
+ const binaryDestinationPath = defaultDownloadPath(explicitConfig.cacheDir, binaryUrl);
456
+ const downloaded = await resolveCachedDownload(binaryUrl, binaryDestinationPath, {
457
+ freshness: downloadFreshness,
315
458
  retries: explicitConfig.fetchRetries,
316
459
  timeoutMs: explicitConfig.fetchTimeoutMs
317
460
  });
318
- if (!downloaded.ok || !downloaded.path) {
319
- const transient = downloaded.ok
320
- ? false
321
- : downloaded.statusCode !== 404 && isTransientFailure(downloaded.statusCode);
461
+ if (!downloaded.ok) {
462
+ const transient = isTransientFailure(downloaded.statusCode);
322
463
  sawRemoteRepoFailure = sawRemoteRepoFailure || transient;
323
- if (hasNextAttempt && (transient || downloaded.ok)) {
464
+ if (hasNextAttempt) {
324
465
  options.onRepoFailover?.({
325
466
  stage: "binary",
326
467
  repoUrl: binaryUrl,
327
468
  statusCode: downloaded.statusCode,
328
- reason: downloaded.ok ? "downloaded-no-binary" : "download-failed",
469
+ reason: "download-failed",
329
470
  attempt: index + 1,
330
471
  totalAttempts: binaryCandidates.length
331
472
  });
332
473
  }
333
474
  continue;
334
475
  }
335
- const signature = readStatsSignature(downloaded.path);
336
- return {
337
- artifactId: artifactIdForCoordinate(coordinate, "decompiled", signature, options.mappingVariant ?? "pass"),
338
- artifactSignature: signature,
476
+ // A 200 is not proof of a jar. A repository, a mirror, or a proxy in front
477
+ // of one answers a jar request with an HTML error page often enough that
478
+ // accepting any non-empty body writes it into the (immutable) download
479
+ // cache, returns it as the artifact, and blocks failover to a repository
480
+ // that has the real thing - with the failure surfacing much later, inside
481
+ // the decompiler. The sources leg has always proved its download by opening
482
+ // it; this one owes the caller the same proof.
483
+ if (!(await isReadableJarArchive(downloaded.path))) {
484
+ // And the body must not survive as a cache entry: this url is immutable
485
+ // for every non-SNAPSHOT coordinate, so the next run would be served the
486
+ // same poison with no request made at all.
487
+ discardCachedDownload(downloaded.path);
488
+ if (hasNextAttempt) {
489
+ options.onRepoFailover?.({
490
+ stage: "binary",
491
+ repoUrl: binaryUrl,
492
+ statusCode: downloaded.statusCode,
493
+ reason: "downloaded-not-an-archive",
494
+ attempt: index + 1,
495
+ totalAttempts: binaryCandidates.length
496
+ });
497
+ }
498
+ continue;
499
+ }
500
+ // A file in the URL-keyed download cache is identified by its bytes, not
501
+ // by a stat signature that a re-download would change for free.
502
+ const signature = downloaded.contentSha256;
503
+ return coordinateArtifact({
504
+ coordinate,
505
+ idSource: "decompiled",
506
+ signature,
339
507
  origin: "decompiled",
340
508
  binaryJarPath: downloaded.path,
341
- coordinate,
342
509
  repoUrl: binaryUrl,
343
510
  isDecompiled: true,
344
- resolvedAt: resolvedAtNow()
345
- };
511
+ mappingVariant: options.mappingVariant ?? "pass"
512
+ });
346
513
  }
347
514
  catch (caughtError) {
348
515
  sawRemoteRepoFailure = true;
@@ -268,11 +268,20 @@ export type FindClassMatch = {
268
268
  filePath: string;
269
269
  line: number;
270
270
  symbolKind: string;
271
+ /** Set when the match is a type declared INSIDE another type. */
272
+ nested?: boolean;
273
+ /** The enclosing top-level type, present only on a nested match. */
274
+ enclosingClass?: string;
271
275
  };
272
276
  export type FindClassOutput = {
273
277
  matches: FindClassMatch[];
274
278
  total: number;
275
279
  warnings: string[];
280
+ /** Recovery route when the index legitimately cannot answer (partial coverage). */
281
+ suggestedCall?: {
282
+ tool: string;
283
+ params: Record<string, unknown>;
284
+ };
276
285
  };
277
286
  type MemberAccess = "public" | "all";
278
287
  export type GetClassMembersInput = {
@@ -617,7 +626,7 @@ export declare class SourceService {
617
626
  buildArtifactContentsSummary(input: {
618
627
  origin: ResolvedSourceArtifact["origin"];
619
628
  sourceJarPath?: string;
620
- isDecompiled?: boolean;
629
+ isDecompiled: boolean;
621
630
  qualityFlags: string[];
622
631
  }): ArtifactContentsSummary;
623
632
  discoverVersionSourceJar(input: {
@@ -638,6 +647,7 @@ export declare class SourceService {
638
647
  requestedScope: ArtifactScope;
639
648
  atNamespace: AccessTransformerNamespace;
640
649
  loader: import("./workspace-mapping-service.js").WorkspaceProjectLoader | "unknown";
650
+ projectMinecraftVersion?: string;
641
651
  }): ReturnType<typeof artifactResolver.discoverAccessTransformerRuntimeCandidates>;
642
652
  buildMappingFallbackSuggestedCall(args: {
643
653
  input: ResolveArtifactInput;
@@ -702,6 +712,7 @@ export declare class SourceService {
702
712
  mappingApplied: SourceMapping;
703
713
  provenance?: ArtifactProvenance;
704
714
  qualityFlags: string[];
715
+ allowDecompile?: boolean;
705
716
  }): Promise<ResolvedSourceArtifact | undefined>;
706
717
  getArtifact(artifactId: string): ArtifactRow;
707
718
  ingestIfNeeded(resolved: ResolvedSourceArtifact): Promise<void>;