@adhisang/minecraft-modding-mcp 3.1.1 → 4.0.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 (61) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +37 -18
  3. package/dist/access-transformer-parser.d.ts +17 -0
  4. package/dist/access-transformer-parser.js +97 -0
  5. package/dist/cache-registry.d.ts +1 -1
  6. package/dist/cache-registry.js +10 -2
  7. package/dist/concurrency.d.ts +1 -0
  8. package/dist/concurrency.js +24 -0
  9. package/dist/config.d.ts +10 -1
  10. package/dist/config.js +52 -1
  11. package/dist/decompiler/vineflower.js +22 -21
  12. package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
  13. package/dist/entry-tools/analyze-symbol-service.d.ts +22 -22
  14. package/dist/entry-tools/analyze-symbol-service.js +13 -2
  15. package/dist/entry-tools/inspect-minecraft-service.d.ts +168 -168
  16. package/dist/entry-tools/inspect-minecraft-service.js +8 -2
  17. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  18. package/dist/entry-tools/validate-project-service.d.ts +153 -16
  19. package/dist/entry-tools/validate-project-service.js +442 -25
  20. package/dist/gradle-paths.d.ts +4 -0
  21. package/dist/gradle-paths.js +57 -0
  22. package/dist/index.js +148 -30
  23. package/dist/lru-list.d.ts +31 -0
  24. package/dist/lru-list.js +102 -0
  25. package/dist/mapping-pipeline-service.d.ts +12 -1
  26. package/dist/mapping-pipeline-service.js +28 -1
  27. package/dist/mapping-service.d.ts +16 -0
  28. package/dist/mapping-service.js +405 -68
  29. package/dist/minecraft-explorer-service.d.ts +13 -0
  30. package/dist/minecraft-explorer-service.js +8 -4
  31. package/dist/mixin-validator.d.ts +33 -2
  32. package/dist/mixin-validator.js +218 -17
  33. package/dist/mod-analyzer.d.ts +1 -0
  34. package/dist/mod-analyzer.js +17 -1
  35. package/dist/mod-decompile-service.js +4 -4
  36. package/dist/mod-remap-service.js +1 -54
  37. package/dist/mod-search-service.d.ts +1 -0
  38. package/dist/mod-search-service.js +84 -51
  39. package/dist/observability.d.ts +18 -1
  40. package/dist/observability.js +44 -1
  41. package/dist/response-utils.d.ts +69 -0
  42. package/dist/response-utils.js +227 -0
  43. package/dist/source-jar-reader.d.ts +16 -0
  44. package/dist/source-jar-reader.js +103 -1
  45. package/dist/source-resolver.d.ts +9 -1
  46. package/dist/source-resolver.js +23 -16
  47. package/dist/source-service.d.ts +119 -3
  48. package/dist/source-service.js +1836 -218
  49. package/dist/storage/artifacts-repo.d.ts +4 -1
  50. package/dist/storage/artifacts-repo.js +33 -5
  51. package/dist/storage/files-repo.d.ts +0 -2
  52. package/dist/storage/files-repo.js +0 -11
  53. package/dist/storage/migrations.d.ts +1 -1
  54. package/dist/storage/migrations.js +10 -2
  55. package/dist/storage/schema.d.ts +2 -0
  56. package/dist/storage/schema.js +25 -0
  57. package/dist/tool-contract-manifest.js +8 -6
  58. package/dist/types.d.ts +20 -0
  59. package/dist/workspace-mapping-service.d.ts +13 -0
  60. package/dist/workspace-mapping-service.js +146 -14
  61. package/package.json +3 -1
@@ -2,12 +2,14 @@ import { readFile } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import fastGlob from "fast-glob";
4
4
  import { z } from "zod";
5
+ import { mapWithConcurrencyLimit } from "../concurrency.js";
5
6
  import { createError, ERROR_CODES } from "../errors.js";
6
7
  import { buildIncludeSchema, detailSchema } from "./entry-tool-schema.js";
7
8
  import { buildEntryToolResult, createSummarySubject } from "./response-contract.js";
8
9
  import { resolveDetail, resolveInclude } from "./request-normalizers.js";
9
10
  const nonEmptyString = z.string().trim().min(1);
10
11
  const INCLUDE_GROUPS = ["warnings", "issues", "workspace", "recovery"];
12
+ const WORKSPACE_TEXT_FILE_READ_CONCURRENCY = 4;
11
13
  const mixinInputSchema = z.discriminatedUnion("mode", [
12
14
  z.object({ mode: z.literal("inline"), source: nonEmptyString }),
13
15
  z.object({ mode: z.literal("path"), path: nonEmptyString }),
@@ -19,11 +21,15 @@ const accessWidenerInputSchema = z.discriminatedUnion("mode", [
19
21
  z.object({ mode: z.literal("inline"), content: nonEmptyString }),
20
22
  z.object({ mode: z.literal("path"), path: nonEmptyString })
21
23
  ]);
24
+ const accessTransformerInputSchema = z.discriminatedUnion("mode", [
25
+ z.object({ mode: z.literal("inline"), content: nonEmptyString }),
26
+ z.object({ mode: z.literal("path"), path: nonEmptyString })
27
+ ]);
22
28
  const subjectSchema = z.discriminatedUnion("kind", [
23
29
  z.object({
24
30
  kind: z.literal("workspace"),
25
31
  projectPath: nonEmptyString,
26
- discover: z.array(z.enum(["mixins", "access-wideners"])).optional()
32
+ discover: z.array(z.enum(["mixins", "access-wideners", "access-transformers"])).optional()
27
33
  }),
28
34
  z.object({
29
35
  kind: z.literal("mixin"),
@@ -32,13 +38,18 @@ const subjectSchema = z.discriminatedUnion("kind", [
32
38
  z.object({
33
39
  kind: z.literal("access-widener"),
34
40
  input: accessWidenerInputSchema
41
+ }),
42
+ z.object({
43
+ kind: z.literal("access-transformer"),
44
+ input: accessTransformerInputSchema
35
45
  })
36
46
  ]);
37
47
  export const validateProjectShape = {
38
- task: z.enum(["project-summary", "mixin", "access-widener"]),
48
+ task: z.enum(["project-summary", "mixin", "access-widener", "access-transformer"]),
39
49
  subject: subjectSchema,
40
50
  version: nonEmptyString.optional(),
41
51
  mapping: z.enum(["obfuscated", "mojang", "intermediary", "yarn"]).optional(),
52
+ atNamespace: z.enum(["srg", "mojang", "obfuscated"]).optional(),
42
53
  sourcePriority: z.enum(["loom-first", "maven-first"]).optional(),
43
54
  scope: z.enum(["vanilla", "merged", "loader"]).optional(),
44
55
  preferProjectVersion: z.boolean().optional(),
@@ -77,6 +88,13 @@ export const validateProjectSchema = z.object(validateProjectShape).superRefine(
77
88
  message: "task=access-widener requires subject.kind=access-widener."
78
89
  });
79
90
  }
91
+ if (value.task === "access-transformer" && value.subject.kind !== "access-transformer") {
92
+ ctx.addIssue({
93
+ code: z.ZodIssueCode.custom,
94
+ path: ["subject", "kind"],
95
+ message: "task=access-transformer requires subject.kind=access-transformer."
96
+ });
97
+ }
80
98
  if (value.configPaths?.length && value.task !== "project-summary") {
81
99
  ctx.addIssue({
82
100
  code: z.ZodIssueCode.custom,
@@ -89,33 +107,153 @@ export async function discoverWorkspaceMixins(projectPath, configPaths) {
89
107
  if (configPaths?.length) {
90
108
  return [...configPaths];
91
109
  }
92
- return fastGlob.sync(["**/*.mixins.json"], {
110
+ return (await fastGlob.glob(["**/*.mixins.json"], {
93
111
  cwd: projectPath,
94
112
  absolute: true,
95
113
  onlyFiles: true,
96
114
  ignore: ["**/.git/**", "**/build/**", "**/out/**", "**/node_modules/**"]
97
- });
115
+ })).sort((left, right) => left.localeCompare(right));
98
116
  }
99
117
  export async function discoverWorkspaceAccessWideners(projectPath) {
100
- const descriptorFiles = fastGlob.sync(["fabric.mod.json", "quilt.mod.json", "**/fabric.mod.json", "**/quilt.mod.json"], {
118
+ const descriptorFiles = (await fastGlob.glob(["fabric.mod.json", "quilt.mod.json", "**/fabric.mod.json", "**/quilt.mod.json"], {
101
119
  cwd: projectPath,
102
120
  absolute: true,
103
121
  onlyFiles: true,
104
122
  ignore: ["**/.git/**", "**/build/**", "**/out/**", "**/node_modules/**"]
105
- });
123
+ })).sort((left, right) => left.localeCompare(right));
106
124
  const discovered = new Set();
107
- for (const descriptorPath of descriptorFiles) {
125
+ const matches = await mapWithConcurrencyLimit(descriptorFiles, WORKSPACE_TEXT_FILE_READ_CONCURRENCY, async (descriptorPath) => {
108
126
  try {
109
127
  const parsed = JSON.parse(await readFile(descriptorPath, "utf8"));
110
128
  const relative = parsed.accessWidener ?? parsed.access_widener;
111
- if (relative) {
112
- discovered.add(resolve(descriptorPath, "..", relative));
129
+ return relative ? [resolve(descriptorPath, "..", relative)] : [];
130
+ }
131
+ catch {
132
+ return [];
133
+ }
134
+ });
135
+ for (const matchList of matches) {
136
+ for (const match of matchList) {
137
+ discovered.add(match);
138
+ }
139
+ }
140
+ return [...discovered].sort((left, right) => left.localeCompare(right));
141
+ }
142
+ function addDiscoveredPath(discovered, filePath, relativePath) {
143
+ const trimmed = relativePath?.trim();
144
+ if (!trimmed) {
145
+ return;
146
+ }
147
+ discovered.add(resolve(filePath, "..", trimmed));
148
+ }
149
+ function collectFileArgumentMatches(content, filePath, discovered, pattern) {
150
+ for (const match of content.matchAll(pattern)) {
151
+ addDiscoveredPath(discovered, filePath, match[2]);
152
+ }
153
+ }
154
+ function extractNamedDslBlocks(content, blockName) {
155
+ const blocks = [];
156
+ const blockPattern = new RegExp(`${blockName}\\s*\\{`, "g");
157
+ for (const match of content.matchAll(blockPattern)) {
158
+ const blockStart = (match.index ?? -1) + match[0].length;
159
+ if (blockStart < match[0].length) {
160
+ continue;
161
+ }
162
+ let depth = 1;
163
+ for (let index = blockStart; index < content.length; index++) {
164
+ const char = content[index];
165
+ if (char === "{") {
166
+ depth++;
113
167
  }
168
+ else if (char === "}") {
169
+ depth--;
170
+ }
171
+ if (depth === 0) {
172
+ blocks.push(content.slice(blockStart, index));
173
+ break;
174
+ }
175
+ }
176
+ }
177
+ return blocks;
178
+ }
179
+ function collectTomlAccessTransformerEntries(content, filePath, discovered) {
180
+ const lines = content.split(/\r?\n/);
181
+ let currentBlock = [];
182
+ const flushBlock = () => {
183
+ if (currentBlock.length === 0) {
184
+ return;
185
+ }
186
+ for (const match of currentBlock.join("\n").matchAll(/^\s*file\s*=\s*(["'])(.+?)\1\s*$/gm)) {
187
+ addDiscoveredPath(discovered, filePath, match[2]);
188
+ }
189
+ currentBlock = [];
190
+ };
191
+ for (const line of lines) {
192
+ if (/^\s*\[\[accessTransformers\]\]\s*$/.test(line)) {
193
+ flushBlock();
194
+ currentBlock.push(line);
195
+ continue;
196
+ }
197
+ if (currentBlock.length > 0 && /^\s*(?:\[\[.*\]\]|\[[^\[])/.test(line)) {
198
+ flushBlock();
199
+ }
200
+ if (currentBlock.length > 0) {
201
+ currentBlock.push(line);
202
+ }
203
+ }
204
+ flushBlock();
205
+ }
206
+ export async function discoverWorkspaceAccessTransformers(projectPath) {
207
+ const discovered = new Set();
208
+ const textFiles = (await fastGlob.glob([
209
+ "build.gradle",
210
+ "build.gradle.kts",
211
+ "META-INF/mods.toml",
212
+ "META-INF/neoforge.mods.toml",
213
+ "**/build.gradle",
214
+ "**/build.gradle.kts",
215
+ "**/META-INF/mods.toml",
216
+ "**/META-INF/neoforge.mods.toml"
217
+ ], {
218
+ cwd: projectPath,
219
+ absolute: true,
220
+ onlyFiles: true,
221
+ ignore: ["**/.git/**", "**/build/**", "**/out/**", "**/node_modules/**"]
222
+ })).sort((left, right) => left.localeCompare(right));
223
+ const discoveredByFile = await mapWithConcurrencyLimit(textFiles, WORKSPACE_TEXT_FILE_READ_CONCURRENCY, async (filePath) => {
224
+ let content;
225
+ try {
226
+ content = await readFile(filePath, "utf8");
114
227
  }
115
228
  catch {
116
- // ignore malformed descriptors in discovery mode
229
+ return [];
230
+ }
231
+ const perFileDiscovered = new Set();
232
+ collectFileArgumentMatches(content, filePath, perFileDiscovered, /accessTransformer\s*=\s*file\(\s*(["'])(.+?)\1\s*\)/g);
233
+ collectFileArgumentMatches(content, filePath, perFileDiscovered, /accessTransformers\.from\s*\(\s*file\(\s*(["'])(.+?)\1\s*\)\s*\)/g);
234
+ for (const block of extractNamedDslBlocks(content, "accessTransformers")) {
235
+ collectFileArgumentMatches(block, filePath, perFileDiscovered, /file\(\s*(["'])(.+?)\1\s*\)/g);
236
+ }
237
+ collectTomlAccessTransformerEntries(content, filePath, perFileDiscovered);
238
+ return [...perFileDiscovered];
239
+ });
240
+ for (const matchList of discoveredByFile) {
241
+ for (const match of matchList) {
242
+ discovered.add(match);
117
243
  }
118
244
  }
245
+ for (const fallbackPath of (await fastGlob.glob([
246
+ "**/META-INF/accesstransformer.cfg",
247
+ "**/*_at.cfg",
248
+ "**/accesstransformer*.cfg"
249
+ ], {
250
+ cwd: projectPath,
251
+ absolute: true,
252
+ onlyFiles: true,
253
+ ignore: ["**/.git/**", "**/build/**", "**/out/**", "**/node_modules/**"]
254
+ })).sort((left, right) => left.localeCompare(right))) {
255
+ discovered.add(fallbackPath);
256
+ }
119
257
  return [...discovered].sort((left, right) => left.localeCompare(right));
120
258
  }
121
259
  export class ValidateProjectService {
@@ -131,7 +269,32 @@ export class ValidateProjectService {
131
269
  if (input.subject.kind !== "mixin") {
132
270
  throw createError({
133
271
  code: ERROR_CODES.INVALID_INPUT,
134
- message: "task=mixin requires subject.kind=mixin."
272
+ message: "task=mixin requires subject.kind=mixin.",
273
+ details: {
274
+ task: input.task,
275
+ subjectKind: input.subject.kind,
276
+ failedStage: "input-validation",
277
+ nextAction: "Set subject.kind to \"mixin\" for task=\"mixin\"."
278
+ }
279
+ });
280
+ }
281
+ if (!input.version) {
282
+ throw createError({
283
+ code: ERROR_CODES.INVALID_INPUT,
284
+ message: "task=mixin requires version.",
285
+ details: {
286
+ task: "mixin",
287
+ failedStage: "input-validation",
288
+ nextAction: "Pass version explicitly (e.g. \"1.21.10\"). task=\"project-summary\" supports preferProjectVersion for auto-detection from gradle.properties, but direct task=\"mixin\" requires an explicit version.",
289
+ suggestedCall: {
290
+ tool: "validate-project",
291
+ params: {
292
+ task: "mixin",
293
+ subject: input.subject,
294
+ version: "1.21.10"
295
+ }
296
+ }
297
+ }
135
298
  });
136
299
  }
137
300
  const output = await this.deps.validateMixin({
@@ -192,7 +355,32 @@ export class ValidateProjectService {
192
355
  if (input.subject.kind !== "access-widener") {
193
356
  throw createError({
194
357
  code: ERROR_CODES.INVALID_INPUT,
195
- message: "task=access-widener requires subject.kind=access-widener."
358
+ message: "task=access-widener requires subject.kind=access-widener.",
359
+ details: {
360
+ task: input.task,
361
+ subjectKind: input.subject.kind,
362
+ failedStage: "input-validation",
363
+ nextAction: "Set subject.kind to \"access-widener\" for task=\"access-widener\"."
364
+ }
365
+ });
366
+ }
367
+ if (!input.version) {
368
+ throw createError({
369
+ code: ERROR_CODES.INVALID_INPUT,
370
+ message: "task=access-widener requires version.",
371
+ details: {
372
+ task: "access-widener",
373
+ failedStage: "input-validation",
374
+ nextAction: "Pass version explicitly (e.g. \"1.21.10\"). Access Widener validation resolves class names against a specific Minecraft version.",
375
+ suggestedCall: {
376
+ tool: "validate-project",
377
+ params: {
378
+ task: "access-widener",
379
+ subject: input.subject,
380
+ version: "1.21.10"
381
+ }
382
+ }
383
+ }
196
384
  });
197
385
  }
198
386
  const content = input.subject.input.mode === "inline"
@@ -202,7 +390,9 @@ export class ValidateProjectService {
202
390
  content,
203
391
  version: input.version,
204
392
  mapping: input.mapping,
205
- sourcePriority: input.sourcePriority
393
+ sourcePriority: input.sourcePriority,
394
+ scope: input.scope,
395
+ preferProjectVersion: input.preferProjectVersion
206
396
  });
207
397
  return {
208
398
  ...buildEntryToolResult({
@@ -242,6 +432,107 @@ export class ValidateProjectService {
242
432
  warnings: Array.isArray(output.warnings) ? output.warnings : []
243
433
  };
244
434
  }
435
+ case "access-transformer": {
436
+ if (input.subject.kind !== "access-transformer") {
437
+ throw createError({
438
+ code: ERROR_CODES.INVALID_INPUT,
439
+ message: "task=access-transformer requires subject.kind=access-transformer.",
440
+ details: {
441
+ task: input.task,
442
+ subjectKind: input.subject.kind,
443
+ failedStage: "input-validation",
444
+ nextAction: "Set subject.kind to \"access-transformer\" for task=\"access-transformer\"."
445
+ }
446
+ });
447
+ }
448
+ if (!input.version) {
449
+ throw createError({
450
+ code: ERROR_CODES.INVALID_INPUT,
451
+ message: "task=access-transformer requires version.",
452
+ details: {
453
+ task: "access-transformer",
454
+ failedStage: "input-validation",
455
+ nextAction: "Pass version explicitly (e.g. \"1.21.10\"). Access Transformer validation resolves class names against a specific Minecraft version.",
456
+ suggestedCall: {
457
+ tool: "validate-project",
458
+ params: {
459
+ task: "access-transformer",
460
+ subject: input.subject,
461
+ version: "1.21.10"
462
+ }
463
+ }
464
+ }
465
+ });
466
+ }
467
+ const content = input.subject.input.mode === "inline"
468
+ ? input.subject.input.content
469
+ : await readFile(input.subject.input.path, "utf8");
470
+ if (!this.deps.validateAccessTransformer) {
471
+ throw createError({
472
+ code: ERROR_CODES.CONTEXT_UNRESOLVED,
473
+ message: "Access Transformer validation is not configured.",
474
+ details: {
475
+ task: "access-transformer",
476
+ failedStage: "dependency-resolution",
477
+ nextAction: "The current runtime was built without an Access Transformer validator. Rebuild the MCP server with validateAccessTransformer configured, or use task=\"access-widener\" if the workspace uses Fabric AccessWideners."
478
+ }
479
+ });
480
+ }
481
+ const output = await this.deps.validateAccessTransformer({
482
+ content,
483
+ version: input.version,
484
+ atNamespace: input.atNamespace,
485
+ sourcePriority: input.sourcePriority,
486
+ scope: input.scope,
487
+ preferProjectVersion: input.preferProjectVersion
488
+ });
489
+ const issueEntries = Array.isArray(output.entries)
490
+ ? output.entries.filter((entry) => {
491
+ if (!entry || typeof entry !== "object" || !("valid" in entry)) {
492
+ return true;
493
+ }
494
+ return entry.valid !== true;
495
+ })
496
+ : undefined;
497
+ return {
498
+ ...buildEntryToolResult({
499
+ task: "access-transformer",
500
+ detail,
501
+ include,
502
+ summary: {
503
+ status: output.valid ? "ok" : "invalid",
504
+ headline: output.valid
505
+ ? "Access Transformer is valid."
506
+ : "Access Transformer contains validation issues.",
507
+ subject: createSummarySubject({
508
+ task: "access-transformer",
509
+ kind: input.subject.kind,
510
+ input: input.subject.input,
511
+ version: input.version,
512
+ sourcePriority: input.sourcePriority,
513
+ scope: input.scope,
514
+ atNamespace: input.atNamespace
515
+ }),
516
+ counts: {
517
+ valid: output.valid ? 1 : 0,
518
+ invalid: output.valid ? 0 : 1
519
+ }
520
+ },
521
+ blocks: {
522
+ project: {
523
+ summary: {
524
+ total: 1,
525
+ valid: output.valid ? 1 : 0,
526
+ invalid: output.valid ? 0 : 1
527
+ }
528
+ },
529
+ issues: include.includes("issues") || detail !== "summary" ? issueEntries : undefined
530
+ },
531
+ alwaysBlocks: ["project"]
532
+ }),
533
+ warnings: Array.isArray(output.warnings) ? output.warnings : []
534
+ };
535
+ }
245
536
  case "project-summary": {
246
537
  if (input.subject.kind !== "workspace") {
247
538
  throw createError({
@@ -269,10 +560,12 @@ export class ValidateProjectService {
269
560
  tool: "validate-project",
270
561
  params: {
271
562
  task: "project-summary",
272
- version: "1.21.10",
273
563
  subject: input.subject
274
564
  }
275
565
  }
566
+ ],
567
+ notes: [
568
+ "Pass version explicitly, or retry with preferProjectVersion=true when gradle.properties declares the Minecraft version."
276
569
  ]
277
570
  },
278
571
  blocks: {
@@ -285,15 +578,98 @@ export class ValidateProjectService {
285
578
  };
286
579
  }
287
580
  const projectPath = input.subject.projectPath;
581
+ const detectedProjectVersion = input.preferProjectVersion
582
+ ? await this.deps.detectProjectMinecraftVersion?.(projectPath)
583
+ : undefined;
584
+ const resolvedVersion = detectedProjectVersion ?? input.version;
288
585
  const discover = input.subject.discover ?? ["mixins", "access-wideners"];
289
- const [mixinConfigs, accessWideners] = await Promise.all([
586
+ const [mixinConfigs, accessWideners, accessTransformers] = await Promise.all([
290
587
  discover.includes("mixins")
291
588
  ? this.deps.discoverMixins(projectPath, input.configPaths)
292
589
  : Promise.resolve([]),
293
590
  discover.includes("access-wideners")
294
591
  ? this.deps.discoverAccessWideners(projectPath)
592
+ : Promise.resolve([]),
593
+ discover.includes("access-transformers")
594
+ ? this.deps.discoverAccessTransformers?.(projectPath) ?? Promise.resolve([])
295
595
  : Promise.resolve([])
296
596
  ]);
597
+ if (!resolvedVersion && (mixinConfigs.length > 0 || accessWideners.length > 0 || accessTransformers.length > 0)) {
598
+ return {
599
+ ...buildEntryToolResult({
600
+ task: "project-summary",
601
+ detail,
602
+ include,
603
+ summary: {
604
+ status: "blocked",
605
+ headline: "Could not resolve Minecraft version for discovered workspace validators.",
606
+ subject: createSummarySubject({
607
+ task: "project-summary",
608
+ kind: input.subject.kind,
609
+ projectPath,
610
+ discover: input.subject.discover,
611
+ mapping: input.mapping,
612
+ sourcePriority: input.sourcePriority,
613
+ scope: input.scope
614
+ }),
615
+ nextActions: [
616
+ {
617
+ tool: "validate-project",
618
+ params: {
619
+ task: "project-summary",
620
+ subject: input.subject
621
+ }
622
+ }
623
+ ],
624
+ notes: [
625
+ "Pass version explicitly, or make sure gradle.properties declares the Minecraft version before using preferProjectVersion=true."
626
+ ]
627
+ },
628
+ blocks: {
629
+ workspace: {
630
+ projectPath
631
+ }
632
+ }
633
+ }),
634
+ warnings: [
635
+ "Could not resolve Minecraft version from gradle.properties for discovered workspace validators."
636
+ ]
637
+ };
638
+ }
639
+ if (!resolvedVersion) {
640
+ return {
641
+ ...buildEntryToolResult({
642
+ task: "project-summary",
643
+ detail,
644
+ include,
645
+ summary: {
646
+ status: "ok",
647
+ headline: `Validated ${mixinConfigs.length} mixin config(s), ${accessWideners.length} access widener(s), and ${accessTransformers.length} access transformer(s).`,
648
+ subject: createSummarySubject({
649
+ task: "project-summary",
650
+ kind: input.subject.kind,
651
+ projectPath,
652
+ discover: input.subject.discover,
653
+ mapping: input.mapping,
654
+ sourcePriority: input.sourcePriority,
655
+ scope: input.scope
656
+ }),
657
+ counts: {
658
+ valid: 0,
659
+ partial: 0,
660
+ invalid: 0
661
+ }
662
+ },
663
+ blocks: {
664
+ workspace: {
665
+ projectPath
666
+ }
667
+ }
668
+ }),
669
+ warnings: []
670
+ };
671
+ }
672
+ const validationVersion = resolvedVersion;
297
673
  const warnings = [];
298
674
  let validMixins = 0;
299
675
  let partialMixins = 0;
@@ -305,11 +681,12 @@ export class ValidateProjectService {
305
681
  mode: "config",
306
682
  configPaths: [configPath]
307
683
  },
308
- version: input.version,
684
+ version: validationVersion,
309
685
  mapping: input.mapping,
310
686
  sourcePriority: input.sourcePriority,
311
687
  scope: input.scope,
312
- preferProjectVersion: input.preferProjectVersion,
688
+ projectPath,
689
+ preferProjectVersion: false,
313
690
  preferProjectMapping: input.preferProjectMapping,
314
691
  sourceRoots: input.sourceRoots,
315
692
  minSeverity: input.minSeverity,
@@ -341,9 +718,12 @@ export class ValidateProjectService {
341
718
  try {
342
719
  const output = await this.deps.validateAccessWidener({
343
720
  content: await readFile(awPath, "utf8"),
344
- version: input.version,
721
+ version: validationVersion,
345
722
  mapping: input.mapping,
346
- sourcePriority: input.sourcePriority
723
+ sourcePriority: input.sourcePriority,
724
+ projectPath,
725
+ scope: input.scope,
726
+ preferProjectVersion: input.preferProjectVersion
347
727
  });
348
728
  if (output.valid) {
349
729
  validAw += 1;
@@ -362,7 +742,43 @@ export class ValidateProjectService {
362
742
  }
363
743
  }
364
744
  }
365
- const invalidCount = invalidMixins + invalidAw;
745
+ let validAt = 0;
746
+ let invalidAt = 0;
747
+ for (const atPath of accessTransformers) {
748
+ try {
749
+ if (!this.deps.validateAccessTransformer) {
750
+ throw createError({
751
+ code: ERROR_CODES.CONTEXT_UNRESOLVED,
752
+ message: "Access Transformer validation is not configured."
753
+ });
754
+ }
755
+ const output = await this.deps.validateAccessTransformer({
756
+ content: await readFile(atPath, "utf8"),
757
+ version: validationVersion,
758
+ atNamespace: input.atNamespace,
759
+ sourcePriority: input.sourcePriority,
760
+ projectPath,
761
+ scope: input.scope,
762
+ preferProjectVersion: input.preferProjectVersion
763
+ });
764
+ if (output.valid) {
765
+ validAt += 1;
766
+ }
767
+ else {
768
+ invalidAt += 1;
769
+ }
770
+ if (Array.isArray(output.warnings)) {
771
+ warnings.push(...output.warnings);
772
+ }
773
+ }
774
+ catch (error) {
775
+ invalidAt += 1;
776
+ if (error instanceof Error) {
777
+ warnings.push(error.message);
778
+ }
779
+ }
780
+ }
781
+ const invalidCount = invalidMixins + invalidAw + invalidAt;
366
782
  const partialCount = partialMixins;
367
783
  const status = invalidCount > 0 ? "invalid" : partialCount > 0 ? "partial" : "ok";
368
784
  return {
@@ -372,19 +788,19 @@ export class ValidateProjectService {
372
788
  include,
373
789
  summary: {
374
790
  status,
375
- headline: `Validated ${mixinConfigs.length} mixin config(s) and ${accessWideners.length} access widener(s).`,
791
+ headline: `Validated ${mixinConfigs.length} mixin config(s), ${accessWideners.length} access widener(s), and ${accessTransformers.length} access transformer(s).`,
376
792
  subject: createSummarySubject({
377
793
  task: "project-summary",
378
794
  kind: input.subject.kind,
379
795
  projectPath,
380
796
  discover: input.subject.discover,
381
- version: input.version,
797
+ version: resolvedVersion,
382
798
  mapping: input.mapping,
383
799
  sourcePriority: input.sourcePriority,
384
800
  scope: input.scope
385
801
  }),
386
802
  counts: {
387
- valid: validMixins + validAw,
803
+ valid: validMixins + validAw + validAt,
388
804
  partial: partialCount,
389
805
  invalid: invalidCount
390
806
  }
@@ -392,7 +808,7 @@ export class ValidateProjectService {
392
808
  blocks: {
393
809
  project: {
394
810
  summary: {
395
- valid: validMixins + validAw,
811
+ valid: validMixins + validAw + validAt,
396
812
  partial: partialCount,
397
813
  invalid: invalidCount
398
814
  }
@@ -400,7 +816,8 @@ export class ValidateProjectService {
400
816
  workspace: {
401
817
  projectPath,
402
818
  mixinConfigs,
403
- accessWideners
819
+ accessWideners,
820
+ accessTransformers
404
821
  }
405
822
  },
406
823
  alwaysBlocks: ["project"]
@@ -0,0 +1,4 @@
1
+ export declare function normalizeOptionalProjectPath(projectPath: string | undefined): string | undefined;
2
+ export declare function resolveGradleUserHomePath(): string;
3
+ export declare function buildVersionSourceSearchRoots(projectPath: string | undefined): string[];
4
+ export declare function buildLoaderRuntimeSearchRoots(projectPath: string | undefined): string[];