@adhisang/minecraft-modding-mcp 7.0.0 → 7.1.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 (95) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +4 -3
  3. package/dist/access-transformer-parser.d.ts +8 -0
  4. package/dist/access-transformer-parser.js +8 -1
  5. package/dist/access-widener-parser.d.ts +17 -0
  6. package/dist/access-widener-parser.js +12 -1
  7. package/dist/cache-registry.js +19 -6
  8. package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
  9. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
  10. package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
  11. package/dist/entry-tools/batch-class-members-service.js +20 -6
  12. package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
  13. package/dist/entry-tools/batch-class-source-service.js +10 -0
  14. package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
  15. package/dist/entry-tools/compare-minecraft-service.js +65 -4
  16. package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
  17. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +15 -9
  18. package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
  19. package/dist/entry-tools/manage-cache-service.d.ts +2 -2
  20. package/dist/entry-tools/manage-cache-service.js +10 -14
  21. package/dist/entry-tools/validate-project/cases/access-transformer.js +22 -3
  22. package/dist/entry-tools/validate-project/cases/access-widener.js +31 -4
  23. package/dist/entry-tools/validate-project/cases/mixin.js +11 -3
  24. package/dist/entry-tools/validate-project/cases/project-summary.js +94 -16
  25. package/dist/entry-tools/validate-project-service.d.ts +2 -2
  26. package/dist/entry-tools/verify-mixin-target-service.js +19 -8
  27. package/dist/index.js +38 -15
  28. package/dist/java-process.d.ts +1 -0
  29. package/dist/java-process.js +14 -0
  30. package/dist/mapping/lookup.js +16 -1
  31. package/dist/mapping-service.d.ts +14 -0
  32. package/dist/mapping-service.js +35 -15
  33. package/dist/minecraft-explorer-service.js +70 -8
  34. package/dist/mixin/access-validators.js +38 -2
  35. package/dist/mixin/annotation-validators.js +137 -43
  36. package/dist/mixin/parsed-validator.js +21 -7
  37. package/dist/mixin-parser.d.ts +52 -0
  38. package/dist/mixin-parser.js +709 -130
  39. package/dist/mod-decompile-service.js +11 -1
  40. package/dist/mod-remap-service.js +6 -6
  41. package/dist/nbt/java-nbt-codec.js +7 -1
  42. package/dist/source/access-validate.js +10 -0
  43. package/dist/source/artifact-resolver.d.ts +27 -3
  44. package/dist/source/artifact-resolver.js +235 -30
  45. package/dist/source/class-source/members-builder.d.ts +7 -0
  46. package/dist/source/class-source/members-builder.js +4 -1
  47. package/dist/source/class-source.d.ts +9 -2
  48. package/dist/source/class-source.js +186 -27
  49. package/dist/source/indexer.js +69 -1
  50. package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
  51. package/dist/source/lifecycle/mapping-helpers.js +29 -3
  52. package/dist/source/lifecycle/runtime-check.d.ts +25 -0
  53. package/dist/source/lifecycle/runtime-check.js +68 -39
  54. package/dist/source/nested-jars.d.ts +15 -1
  55. package/dist/source/nested-jars.js +14 -5
  56. package/dist/source/search.d.ts +10 -2
  57. package/dist/source/search.js +60 -13
  58. package/dist/source/symbol-resolver.js +88 -0
  59. package/dist/source/validate-mixin/pipeline/mapping-health.js +20 -1
  60. package/dist/source/validate-mixin/pipeline/target-lookup.js +16 -7
  61. package/dist/source/validate-mixin.d.ts +5 -0
  62. package/dist/source/validate-mixin.js +136 -21
  63. package/dist/source/workspace-target.js +75 -7
  64. package/dist/source-jar-reader.d.ts +48 -1
  65. package/dist/source-jar-reader.js +93 -3
  66. package/dist/source-resolver.d.ts +7 -0
  67. package/dist/source-resolver.js +22 -14
  68. package/dist/source-service.d.ts +5 -0
  69. package/dist/source-service.js +7 -0
  70. package/dist/stdio-supervisor.d.ts +35 -1
  71. package/dist/stdio-supervisor.js +77 -2
  72. package/dist/storage/db.d.ts +62 -2
  73. package/dist/storage/db.js +181 -20
  74. package/dist/storage/files-repo.d.ts +7 -0
  75. package/dist/storage/files-repo.js +17 -4
  76. package/dist/storage/sqlite.d.ts +31 -1
  77. package/dist/storage/sqlite.js +125 -16
  78. package/dist/tool-contract-manifest.js +2 -2
  79. package/dist/tool-execution-gate.js +2 -1
  80. package/dist/tool-guidance.js +4 -1
  81. package/dist/tool-schemas.d.ts +64 -52
  82. package/dist/tool-schemas.js +9 -7
  83. package/dist/types.d.ts +9 -0
  84. package/dist/v1-parity-schemas.js +36 -2
  85. package/dist/version-diff-service.d.ts +23 -0
  86. package/dist/version-diff-service.js +101 -0
  87. package/dist/version-service.d.ts +14 -0
  88. package/dist/version-service.js +52 -3
  89. package/dist/workspace-context-cache.d.ts +25 -0
  90. package/dist/workspace-context-cache.js +52 -2
  91. package/dist/workspace-mapping-service.d.ts +8 -0
  92. package/dist/workspace-mapping-service.js +151 -21
  93. package/docs/README-ja.md +3 -1
  94. package/docs/tool-reference.md +69 -22
  95. package/package.json +1 -1
@@ -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<{
@@ -5,6 +5,10 @@ export async function handleVersions(deps, input, detail, include) {
5
5
  includeSnapshots: input.includeSnapshots,
6
6
  limit: input.limit
7
7
  });
8
+ // versions.latest.release and releases[0] can both be absent for an effectively
9
+ // empty version manifest; omit the nextAction rather than suggest an unusable
10
+ // follow-up call with no version.
11
+ const resolvedNextVersion = versions.latest.release ?? versions.releases[0]?.id;
8
12
  const summary = {
9
13
  status: "ok",
10
14
  headline: `Found ${versions.totalAvailable} Minecraft versions.`,
@@ -18,15 +22,17 @@ export async function handleVersions(deps, input, detail, include) {
18
22
  releases: versions.releases.length,
19
23
  snapshots: versions.snapshots?.length ?? 0
20
24
  },
21
- nextActions: nextActionsOrUndefined([
22
- createNextAction("inspect-minecraft", {
23
- task: "artifact",
24
- subject: {
25
- kind: "version",
26
- version: versions.latest.release ?? versions.releases[0]?.id
27
- }
28
- })
29
- ])
25
+ nextActions: nextActionsOrUndefined(resolvedNextVersion !== undefined
26
+ ? [
27
+ createNextAction("inspect-minecraft", {
28
+ task: "artifact",
29
+ subject: {
30
+ kind: "version",
31
+ version: resolvedNextVersion
32
+ }
33
+ })
34
+ ]
35
+ : [])
30
36
  };
31
37
  return {
32
38
  ...buildEntryToolResult({
@@ -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;
@@ -225,23 +225,19 @@ export class ManageCacheService {
225
225
  include,
226
226
  summary: {
227
227
  status: output.rebuiltEntries > 0 && executionMode === "apply" ? "changed" : "unchanged",
228
- headline: `${executionMode === "apply" ? "Applied" : "Previewed"} rebuild for ${cacheKinds.length} cache kind(s).`,
228
+ // Neither headline may read as work done: rebuild is not
229
+ // implemented for any cache kind, so apply changes nothing and
230
+ // preview has nothing to promise.
231
+ headline: executionMode === "apply"
232
+ ? `Rebuild is not implemented for ${cacheKinds.length} cache kind(s); nothing changed.`
233
+ : `Previewed rebuild for ${cacheKinds.length} cache kind(s): nothing would change.`,
229
234
  subject: summarySubject,
230
235
  counts: {
231
236
  rebuiltEntries: output.rebuiltEntries
232
- },
233
- ...(executionMode === "preview"
234
- ? {
235
- nextActions: [
236
- createNextAction("manage-cache", {
237
- action: "rebuild",
238
- cacheKinds,
239
- executionMode: "apply",
240
- selector: input.selector
241
- })
242
- ]
243
- }
244
- : {})
237
+ }
238
+ // No apply follow-up: rebuild is not implemented for any cache
239
+ // kind, so advertising one would promise a call that changes
240
+ // nothing. The registry's warning names what to do instead.
245
241
  },
246
242
  blocks: {
247
243
  operation: {
@@ -34,9 +34,28 @@ export async function handleAccessTransformer(deps, input, detail, include) {
34
34
  }
35
35
  });
36
36
  }
37
- const content = input.subject.input.mode === "inline"
38
- ? input.subject.input.content
39
- : await readFile(input.subject.input.path, "utf8");
37
+ let content;
38
+ if (input.subject.input.mode === "inline") {
39
+ content = input.subject.input.content;
40
+ }
41
+ else {
42
+ const atPath = input.subject.input.path;
43
+ try {
44
+ content = await readFile(atPath, "utf8");
45
+ }
46
+ catch (err) {
47
+ const osCode = err?.code ?? "UNKNOWN";
48
+ throw createError({
49
+ code: ERROR_CODES.INVALID_INPUT,
50
+ message: `Could not read access transformer file "${atPath}": ${osCode} - ${err instanceof Error ? err.message : String(err)}`,
51
+ details: {
52
+ task: "access-transformer",
53
+ failedStage: "input-validation",
54
+ nextAction: `Verify the file exists and is readable at "${atPath}".`
55
+ }
56
+ });
57
+ }
58
+ }
40
59
  if (!deps.validateAccessTransformer) {
41
60
  throw createError({
42
61
  code: ERROR_CODES.CONTEXT_UNRESOLVED,
@@ -34,9 +34,28 @@ export async function handleAccessWidener(deps, input, detail, include) {
34
34
  }
35
35
  });
36
36
  }
37
- const content = input.subject.input.mode === "inline"
38
- ? input.subject.input.content
39
- : await readFile(input.subject.input.path, "utf8");
37
+ let content;
38
+ if (input.subject.input.mode === "inline") {
39
+ content = input.subject.input.content;
40
+ }
41
+ else {
42
+ const awPath = input.subject.input.path;
43
+ try {
44
+ content = await readFile(awPath, "utf8");
45
+ }
46
+ catch (err) {
47
+ const osCode = err?.code ?? "UNKNOWN";
48
+ throw createError({
49
+ code: ERROR_CODES.INVALID_INPUT,
50
+ message: `Could not read access widener file "${awPath}": ${osCode} - ${err instanceof Error ? err.message : String(err)}`,
51
+ details: {
52
+ task: "access-widener",
53
+ failedStage: "input-validation",
54
+ nextAction: `Verify the file exists and is readable at "${awPath}".`
55
+ }
56
+ });
57
+ }
58
+ }
40
59
  const output = await deps.validateAccessWidener({
41
60
  content,
42
61
  version: input.version,
@@ -45,6 +64,14 @@ export async function handleAccessWidener(deps, input, detail, include) {
45
64
  scope: input.scope,
46
65
  preferProjectVersion: input.preferProjectVersion
47
66
  });
67
+ const issueEntries = Array.isArray(output.entries)
68
+ ? output.entries.filter((entry) => {
69
+ if (!entry || typeof entry !== "object" || !("valid" in entry)) {
70
+ return true;
71
+ }
72
+ return entry.valid !== true;
73
+ })
74
+ : undefined;
48
75
  return {
49
76
  ...buildEntryToolResult({
50
77
  task: "access-widener",
@@ -76,7 +103,7 @@ export async function handleAccessWidener(deps, input, detail, include) {
76
103
  invalid: output.valid ? 0 : 1
77
104
  }
78
105
  },
79
- issues: include.includes("issues") || detail !== "summary" ? output.issues : undefined
106
+ issues: include.includes("issues") || detail !== "summary" ? issueEntries : undefined
80
107
  },
81
108
  alwaysBlocks: ["project"]
82
109
  }),
@@ -74,14 +74,21 @@ export async function handleMixin(deps, input, detail, include) {
74
74
  const summary = output.summary;
75
75
  const invalidCount = summary?.invalid ?? 0;
76
76
  const partialCount = summary?.partial ?? 0;
77
+ const processingErrorsCount = summary?.processingErrors ?? 0;
77
78
  return {
78
79
  ...buildEntryToolResult({
79
80
  task: "mixin",
80
81
  detail,
81
82
  include,
82
83
  summary: {
83
- status: invalidCount > 0 ? "invalid" : partialCount > 0 ? "partial" : "ok",
84
- headline: `Validated ${summary?.total ?? 0} mixin input(s).`,
84
+ status: invalidCount > 0
85
+ ? "invalid"
86
+ : partialCount > 0 || processingErrorsCount > 0
87
+ ? "partial"
88
+ : "ok",
89
+ headline: processingErrorsCount > 0
90
+ ? `Validated ${summary?.total ?? 0} mixin input(s); ${processingErrorsCount} could not be processed.`
91
+ : `Validated ${summary?.total ?? 0} mixin input(s).`,
85
92
  subject: createSummarySubject({
86
93
  task: "mixin",
87
94
  kind: input.subject.kind,
@@ -94,7 +101,8 @@ export async function handleMixin(deps, input, detail, include) {
94
101
  counts: {
95
102
  valid: summary?.valid ?? 0,
96
103
  partial: partialCount,
97
- invalid: invalidCount
104
+ invalid: invalidCount,
105
+ processingErrors: processingErrorsCount
98
106
  }
99
107
  },
100
108
  blocks: {
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { mapWithConcurrencyLimit } from "../../../concurrency.js";
3
3
  import { buildEntryToolResult, createSummarySubject } from "../../response-contract.js";
4
- import { ERROR_CODES, createError } from "../../../errors.js";
4
+ import { ERROR_CODES, createError, isAppError } from "../../../errors.js";
5
5
  import { buildEarlyTasksForBlocked, buildFullTaskStatusReport } from "../internal.js";
6
6
  // Telemetry failures must not change validation outcomes; swallow rejections
7
7
  // so a broken emitter does not abort the summary or count as a validation error.
@@ -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,15 +239,16 @@ 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;
198
250
  let invalidMixins = 0;
251
+ let mixinProcessingErrors = 0;
199
252
  let mixinCaughtErrors = 0;
200
253
  await safeEmit(options.stageEmitter, "validate-project:mixin-validation", {
201
254
  targetTotal: mixinConfigs.length
@@ -244,6 +297,8 @@ export async function handleProjectSummary(deps, input, detail, include, options
244
297
  valid: summary?.valid ?? 0,
245
298
  partial: summary?.partial ?? 0,
246
299
  invalid: summary?.invalid ?? 0,
300
+ // Mixin sources that failed to load count in none of valid/partial/invalid.
301
+ processingErrors: summary?.processingErrors ?? 0,
247
302
  caught: 0,
248
303
  warnings: Array.isArray(mixinResult.warnings) ? [...mixinResult.warnings] : []
249
304
  };
@@ -253,6 +308,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
253
308
  valid: 0,
254
309
  partial: 0,
255
310
  invalid: 1,
311
+ processingErrors: 0,
256
312
  caught: 1,
257
313
  warnings: error instanceof Error ? [`${configPath}: ${error.message}`] : []
258
314
  };
@@ -262,6 +318,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
262
318
  validMixins += result.valid;
263
319
  partialMixins += result.partial;
264
320
  invalidMixins += result.invalid;
321
+ mixinProcessingErrors += result.processingErrors;
265
322
  mixinCaughtErrors += result.caught;
266
323
  warnings.push(...result.warnings);
267
324
  }
@@ -270,6 +327,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
270
327
  let validAw = 0;
271
328
  let invalidAw = 0;
272
329
  let awCaughtErrors = 0;
330
+ let awProcessingErrors = 0;
273
331
  await safeEmit(options.stageEmitter, "validate-project:access-widener-validation", {
274
332
  targetTotal: accessWideners.length
275
333
  });
@@ -288,7 +346,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
288
346
  projectPath,
289
347
  gradleUserHome,
290
348
  scope: input.scope,
291
- preferProjectVersion: input.preferProjectVersion
349
+ preferProjectVersion: effectivePreferProjectVersion
292
350
  });
293
351
  if (output.valid) {
294
352
  validAw += 1;
@@ -301,6 +359,12 @@ export async function handleProjectSummary(deps, input, detail, include, options
301
359
  }
302
360
  }
303
361
  catch (error) {
362
+ if (isAppError(error) && error.details?.unsupportedFormat === "classTweaker") {
363
+ // A format this server does not read is not a verdict on the file.
364
+ awProcessingErrors += 1;
365
+ warnings.push(`${awPath}: ${error.message}`);
366
+ continue;
367
+ }
304
368
  invalidAw += 1;
305
369
  awCaughtErrors += 1;
306
370
  if (error instanceof Error) {
@@ -337,7 +401,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
337
401
  projectPath,
338
402
  gradleUserHome,
339
403
  scope: input.scope,
340
- preferProjectVersion: input.preferProjectVersion
404
+ preferProjectVersion: effectivePreferProjectVersion
341
405
  });
342
406
  if (output.valid) {
343
407
  validAt += 1;
@@ -360,14 +424,26 @@ export async function handleProjectSummary(deps, input, detail, include, options
360
424
  const atDurationMs = Date.now() - atDurationStart;
361
425
  const invalidCount = invalidMixins + invalidAw + invalidAt;
362
426
  const partialCount = partialMixins;
363
- const status = invalidCount > 0 ? "invalid" : partialCount > 0 ? "partial" : "ok";
427
+ const processingErrors = mixinProcessingErrors + awProcessingErrors;
428
+ const status = invalidCount > 0
429
+ ? "invalid"
430
+ : partialCount > 0 || processingErrors > 0
431
+ ? "partial"
432
+ : "ok";
433
+ const nothingToValidate = mixinConfigs.length === 0 && accessWideners.length === 0 && accessTransformers.length === 0
434
+ ? describeNothingToValidate(discover, projectPath)
435
+ : undefined;
436
+ if (nothingToValidate) {
437
+ warnings.push(nothingToValidate.warning);
438
+ }
364
439
  const baseResult = buildEntryToolResult({
365
440
  task: "project-summary",
366
441
  detail,
367
442
  include,
368
443
  summary: {
369
444
  status,
370
- headline: `Validated ${mixinConfigs.length} mixin config(s), ${accessWideners.length} access widener(s), and ${accessTransformers.length} access transformer(s).`,
445
+ headline: nothingToValidate?.headline ??
446
+ `Validated ${mixinConfigs.length} mixin config(s), ${accessWideners.length} access widener(s), and ${accessTransformers.length} access transformer(s)${mixinProcessingErrors > 0 ? `; ${mixinProcessingErrors} mixin input(s) could not be processed` : ""}${awProcessingErrors > 0 ? `; ${awProcessingErrors} access widener(s) could not be processed` : ""}.`,
371
447
  subject: createSummarySubject({
372
448
  task: "project-summary",
373
449
  kind: input.subject.kind,
@@ -381,7 +457,8 @@ export async function handleProjectSummary(deps, input, detail, include, options
381
457
  counts: {
382
458
  valid: validMixins + validAw + validAt,
383
459
  partial: partialCount,
384
- invalid: invalidCount
460
+ invalid: invalidCount,
461
+ processingErrors
385
462
  }
386
463
  },
387
464
  blocks: {
@@ -389,7 +466,8 @@ export async function handleProjectSummary(deps, input, detail, include, options
389
466
  summary: {
390
467
  valid: validMixins + validAw + validAt,
391
468
  partial: partialCount,
392
- invalid: invalidCount
469
+ invalid: invalidCount,
470
+ processingErrors
393
471
  }
394
472
  },
395
473
  workspace: {
@@ -416,7 +494,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
416
494
  sourcePriority: input.sourcePriority,
417
495
  gradleUserHome,
418
496
  scope: input.scope,
419
- preferProjectVersion: input.preferProjectVersion,
497
+ preferProjectVersion: effectivePreferProjectVersion,
420
498
  mixinDiscoveryCount: mixinConfigs.length,
421
499
  mixinCaughtErrors,
422
500
  mixinCounts: { ok: validMixins, partial: partialMixins, invalid: invalidMixins },