@adhisang/minecraft-modding-mcp 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +3 -2
  3. package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
  4. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
  5. package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
  6. package/dist/entry-tools/batch-class-members-service.js +20 -6
  7. package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
  8. package/dist/entry-tools/batch-class-source-service.js +10 -0
  9. package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
  10. package/dist/entry-tools/compare-minecraft-service.js +65 -4
  11. package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
  12. package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
  13. package/dist/entry-tools/manage-cache-service.d.ts +2 -2
  14. package/dist/entry-tools/validate-project/cases/project-summary.js +71 -12
  15. package/dist/entry-tools/validate-project-service.d.ts +2 -2
  16. package/dist/index.js +37 -15
  17. package/dist/source/artifact-resolver.d.ts +14 -0
  18. package/dist/source/artifact-resolver.js +106 -12
  19. package/dist/source/class-source/members-builder.d.ts +7 -0
  20. package/dist/source/class-source/members-builder.js +4 -1
  21. package/dist/source/class-source.d.ts +9 -2
  22. package/dist/source/class-source.js +173 -25
  23. package/dist/source/indexer.js +69 -1
  24. package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
  25. package/dist/source/lifecycle/mapping-helpers.js +29 -3
  26. package/dist/source/lifecycle/runtime-check.d.ts +25 -0
  27. package/dist/source/lifecycle/runtime-check.js +68 -39
  28. package/dist/source/symbol-resolver.js +88 -0
  29. package/dist/source-jar-reader.d.ts +33 -0
  30. package/dist/source-jar-reader.js +58 -0
  31. package/dist/source-resolver.d.ts +7 -0
  32. package/dist/source-resolver.js +20 -5
  33. package/dist/source-service.d.ts +5 -0
  34. package/dist/source-service.js +7 -0
  35. package/dist/storage/db.d.ts +62 -2
  36. package/dist/storage/db.js +181 -20
  37. package/dist/storage/sqlite.d.ts +31 -1
  38. package/dist/storage/sqlite.js +125 -16
  39. package/dist/tool-guidance.js +4 -1
  40. package/dist/tool-schemas.d.ts +64 -52
  41. package/dist/tool-schemas.js +9 -7
  42. package/dist/types.d.ts +9 -0
  43. package/dist/v1-parity-schemas.js +36 -2
  44. package/dist/version-diff-service.d.ts +23 -0
  45. package/dist/version-diff-service.js +101 -0
  46. package/dist/version-service.d.ts +14 -0
  47. package/dist/version-service.js +45 -3
  48. package/dist/workspace-mapping-service.d.ts +8 -0
  49. package/dist/workspace-mapping-service.js +35 -7
  50. package/docs/README-ja.md +2 -0
  51. package/docs/tool-reference.md +55 -11
  52. package/package.json +1 -1
@@ -99,11 +99,13 @@ export const resolveArtifactTargetSchema = z.discriminatedUnion("kind", [
99
99
  workspaceTargetSchema,
100
100
  dependencyTargetSchema
101
101
  ]);
102
- // Extended target schema for the source-lookup tools (get-class-source / get-class-members):
103
- // the same kind-based shape as resolveArtifactTargetSchema, PLUS a `kind:"artifact"` variant
104
- // that short-circuits resolution by reusing an already-resolved artifactId. The shared
105
- // resolveArtifactTargetSchema is intentionally NOT widened the artifact kind has no
106
- // resolution meaning for resolve-artifact / verify-mixin-target / the batch tools.
102
+ // Extended target schema for the source-lookup tools (get-class-source / get-class-members)
103
+ // and the batch tools that fan out over one shared artifact (batch-class-source /
104
+ // batch-class-members): the same kind-based shape as resolveArtifactTargetSchema, PLUS a
105
+ // `kind:"artifact"` variant that short-circuits resolution by reusing an already-resolved
106
+ // artifactId. The shared resolveArtifactTargetSchema is intentionally NOT widened
107
+ // the artifact kind has no resolution meaning for resolve-artifact / verify-mixin-target,
108
+ // which resolve a target rather than reuse one.
107
109
  export const sourceLookupTargetSchema = z.discriminatedUnion("kind", [
108
110
  z.object({ kind: z.literal("version"), value: nonEmptyString }),
109
111
  z.object({ kind: z.literal("jar"), value: nonEmptyString }),
@@ -238,7 +240,7 @@ export const batchClassSourceEntrySchema = z.object({
238
240
  outputFile: optionalNonEmptyString
239
241
  });
240
242
  export const batchClassSourceShape = {
241
- target: resolveArtifactTargetSchema.describe(RESOLVE_ARTIFACT_TARGET_DESCRIPTION),
243
+ target: sourceLookupTargetSchema.describe(SOURCE_LOOKUP_TARGET_DESCRIPTION),
242
244
  mapping: sourceMappingSchema.optional(),
243
245
  sourcePriority: mappingSourcePrioritySchema.optional(),
244
246
  allowDecompile: z.boolean().optional(),
@@ -294,7 +296,7 @@ export const batchClassMembersEntrySchema = z.object({
294
296
  maxMembers: optionalPositiveInt
295
297
  });
296
298
  export const batchClassMembersShape = {
297
- target: resolveArtifactTargetSchema.describe(RESOLVE_ARTIFACT_TARGET_DESCRIPTION),
299
+ target: sourceLookupTargetSchema.describe(SOURCE_LOOKUP_TARGET_DESCRIPTION),
298
300
  mapping: sourceMappingSchema.optional(),
299
301
  sourcePriority: mappingSourcePrioritySchema.optional(),
300
302
  allowDecompile: z.boolean().optional(),
package/dist/types.d.ts CHANGED
@@ -75,6 +75,15 @@ export interface ArtifactProvenance {
75
75
  repoUrl?: string;
76
76
  };
77
77
  transformChain: string[];
78
+ /**
79
+ * The artifact's runtime names ship unobfuscated (Minecraft 26.1+): set for a
80
+ * 26.1+ version target, a 26.1+ Minecraft runtime coordinate, and a jar target
81
+ * proven to be a 26.1+ runtime jar; absent otherwise. `mappingApplied` keeps the
82
+ * label the caller asked for, so on such an artifact "obfuscated" names the
83
+ * as-shipped names, which are already Mojang names. Same flag name as
84
+ * `mappingContext.unobfuscatedRuntime`.
85
+ */
86
+ unobfuscatedRuntime?: boolean;
78
87
  workspaceResolution?: WorkspaceResolutionProvenance;
79
88
  dependencyResolution?: DependencyResolutionProvenance;
80
89
  warnings?: string[];
@@ -446,9 +446,26 @@ export const V1_PARITY_SCHEMAS = {
446
446
  "name"
447
447
  ],
448
448
  "additionalProperties": false
449
+ },
450
+ {
451
+ "type": "object",
452
+ "properties": {
453
+ "kind": {
454
+ "type": "string",
455
+ "const": "artifact"
456
+ },
457
+ "artifactId": {
458
+ "$ref": "#/properties/target/anyOf/0/properties/value"
459
+ }
460
+ },
461
+ "required": [
462
+ "kind",
463
+ "artifactId"
464
+ ],
465
+ "additionalProperties": false
449
466
  }
450
467
  ],
451
- "description": "Object, not string. e.g. {\"kind\":\"version\",\"value\":\"1.21.10\"}, {\"kind\":\"workspace\"}, or to inspect a loader/Fabric dependency like vanilla: {\"kind\":\"dependency\",\"group\":\"net.fabricmc.fabric-api\",\"name\":\"fabric-api\",\"versionFromProject\":true} (needs projectPath) or with an explicit \"version\"."
468
+ "description": "Same shape as resolve-artifact target (incl. {\"kind\":\"dependency\",...} to read a Fabric/loader dependency class like vanilla), plus {\"kind\":\"artifact\",\"artifactId\":\"...\"} to reuse a resolved artifact. Object, not string."
452
469
  },
453
470
  "mapping": {
454
471
  "type": "string",
@@ -706,9 +723,26 @@ export const V1_PARITY_SCHEMAS = {
706
723
  "name"
707
724
  ],
708
725
  "additionalProperties": false
726
+ },
727
+ {
728
+ "type": "object",
729
+ "properties": {
730
+ "kind": {
731
+ "type": "string",
732
+ "const": "artifact"
733
+ },
734
+ "artifactId": {
735
+ "$ref": "#/properties/target/anyOf/0/properties/value"
736
+ }
737
+ },
738
+ "required": [
739
+ "kind",
740
+ "artifactId"
741
+ ],
742
+ "additionalProperties": false
709
743
  }
710
744
  ],
711
- "description": "Object, not string. e.g. {\"kind\":\"version\",\"value\":\"1.21.10\"}, {\"kind\":\"workspace\"}, or to inspect a loader/Fabric dependency like vanilla: {\"kind\":\"dependency\",\"group\":\"net.fabricmc.fabric-api\",\"name\":\"fabric-api\",\"versionFromProject\":true} (needs projectPath) or with an explicit \"version\"."
745
+ "description": "Same shape as resolve-artifact target (incl. {\"kind\":\"dependency\",...} to read a Fabric/loader dependency class like vanilla), plus {\"kind\":\"artifact\",\"artifactId\":\"...\"} to reuse a resolved artifact. Object, not string."
712
746
  },
713
747
  "mapping": {
714
748
  "type": "string",
@@ -52,6 +52,29 @@ export type CompareVersionsOutput = {
52
52
  };
53
53
  warnings: string[];
54
54
  };
55
+ /**
56
+ * `added`/`removed` entries are `group:artifact:<versions>`, where
57
+ * `<versions>` is the sorted, comma-joined set of distinct versions seen for
58
+ * that key on the side it was diffed from or to (a single version renders
59
+ * as before, e.g. "g:a:1"; multiple platform-specific versions render as
60
+ * "g:a:1,2").
61
+ */
62
+ export type LibraryDiffResult = {
63
+ added: string[];
64
+ removed: string[];
65
+ addedCount: number;
66
+ removedCount: number;
67
+ /** Count only (per design contract) — no list of which libraries bumped. */
68
+ versionChangedCount: number;
69
+ };
70
+ /**
71
+ * Pure diff of two versions' `libraries` name lists, keyed by
72
+ * `group:artifact` so a per-release version bump or a per-platform natives
73
+ * classifier never reads as churn. Used by migration-overview to surface
74
+ * library swaps (e.g. LWJGL GLFW replaced by SDL) that a class/registry diff
75
+ * cannot see.
76
+ */
77
+ export declare function diffLibraries(fromNames: string[], toNames: string[]): LibraryDiffResult;
55
78
  export declare class VersionDiffService {
56
79
  private readonly config;
57
80
  private readonly versionService;
@@ -88,6 +88,107 @@ function diffSets(from, to) {
88
88
  removed.sort();
89
89
  return { added, removed, unchanged };
90
90
  }
91
+ // Matches a Maven-style library coordinate "group:artifact:version" with an
92
+ // optional trailing classifier ("...:natives-linux"). Group/artifact/version
93
+ // segments must be non-empty and colon-free; anything else is not a library
94
+ // name this diff can key on, so it is silently ignored rather than crashing
95
+ // or reading as a spurious added/removed entry. A trailing "@extension"
96
+ // (e.g. "...@jar") is stripped by the caller before this regex runs, so it
97
+ // never leaks into the captured version.
98
+ const LIBRARY_NAME_RE = /^([^:\s]+):([^:\s]+):([^:\s]+)(?::[^:\s]*)?$/;
99
+ function parseLibraryName(name) {
100
+ // Strip a trailing "@extension" (e.g. "g:a:1@jar", "g:a:1:natives-linux@jar")
101
+ // before parsing, so it never gets captured as part of the version.
102
+ const withoutExtension = name.trim().replace(/@[^@]*$/, "");
103
+ const match = LIBRARY_NAME_RE.exec(withoutExtension);
104
+ if (!match) {
105
+ return undefined;
106
+ }
107
+ const [, group, artifact, version] = match;
108
+ return { key: `${group}:${artifact}`, version };
109
+ }
110
+ /**
111
+ * Collapses a version's raw library name list to the set of distinct
112
+ * versions seen per `group:artifact` key, dropping the per-platform natives
113
+ * classifier and any entry whose name does not parse as a library
114
+ * coordinate. Real Mojang manifests can list the same `group:artifact` with
115
+ * different versions under different platform rules, so every version seen
116
+ * is kept (not just the first) — collapsing to one would make the diff
117
+ * depend on input order.
118
+ */
119
+ function collapseLibraryNames(names) {
120
+ const map = new Map();
121
+ for (const name of names) {
122
+ if (typeof name !== "string") {
123
+ continue;
124
+ }
125
+ const parsed = parseLibraryName(name);
126
+ if (!parsed) {
127
+ continue;
128
+ }
129
+ let versions = map.get(parsed.key);
130
+ if (!versions) {
131
+ versions = new Set();
132
+ map.set(parsed.key, versions);
133
+ }
134
+ versions.add(parsed.version);
135
+ }
136
+ return map;
137
+ }
138
+ /** Renders a key's version set as the sorted, comma-joined suffix used in `added`/`removed`. */
139
+ function renderVersions(versions) {
140
+ return Array.from(versions).sort().join(",");
141
+ }
142
+ function versionSetsEqual(a, b) {
143
+ if (a.size !== b.size) {
144
+ return false;
145
+ }
146
+ for (const version of a) {
147
+ if (!b.has(version)) {
148
+ return false;
149
+ }
150
+ }
151
+ return true;
152
+ }
153
+ /**
154
+ * Pure diff of two versions' `libraries` name lists, keyed by
155
+ * `group:artifact` so a per-release version bump or a per-platform natives
156
+ * classifier never reads as churn. Used by migration-overview to surface
157
+ * library swaps (e.g. LWJGL GLFW replaced by SDL) that a class/registry diff
158
+ * cannot see.
159
+ */
160
+ export function diffLibraries(fromNames, toNames) {
161
+ const fromMap = collapseLibraryNames(fromNames ?? []);
162
+ const toMap = collapseLibraryNames(toNames ?? []);
163
+ const added = [];
164
+ const removed = [];
165
+ let versionChangedCount = 0;
166
+ for (const [key, versions] of toMap) {
167
+ if (!fromMap.has(key)) {
168
+ added.push(`${key}:${renderVersions(versions)}`);
169
+ }
170
+ }
171
+ for (const [key, versions] of fromMap) {
172
+ if (!toMap.has(key)) {
173
+ removed.push(`${key}:${renderVersions(versions)}`);
174
+ }
175
+ }
176
+ for (const [key, fromVersions] of fromMap) {
177
+ const toVersions = toMap.get(key);
178
+ if (toVersions !== undefined && !versionSetsEqual(fromVersions, toVersions)) {
179
+ versionChangedCount += 1;
180
+ }
181
+ }
182
+ added.sort();
183
+ removed.sort();
184
+ return {
185
+ added,
186
+ removed,
187
+ addedCount: added.length,
188
+ removedCount: removed.length,
189
+ versionChangedCount
190
+ };
191
+ }
91
192
  function diffRegistries(fromRegistries, toRegistries) {
92
193
  const fromKeys = new Set(Object.keys(fromRegistries));
93
194
  const toKeys = new Set(Object.keys(toRegistries));
@@ -54,6 +54,14 @@ export declare class VersionService {
54
54
  listVersionIds(input?: ListVersionIdsInput): Promise<string[]>;
55
55
  resolveVersionJar(version: string): Promise<ResolvedVersionJar>;
56
56
  resolveVersionMappings(version: string): Promise<ResolvedVersionMappings>;
57
+ /**
58
+ * Raw `libraries[].name` coordinates (e.g. "org.lwjgl:lwjgl-glfw:3.4.1")
59
+ * for one version's per-version JSON, in manifest order. Entries missing a
60
+ * non-empty string `name` are dropped here at the JSON boundary; further
61
+ * validation (does the name actually parse as a library coordinate) is the
62
+ * caller's job — see diffLibraries in version-diff-service.ts.
63
+ */
64
+ getVersionLibraries(version: string): Promise<string[]>;
57
65
  resolveServerJar(version: string): Promise<ResolvedServerJar>;
58
66
  private fetchManifest;
59
67
  private resolveVersionJarInternal;
@@ -69,5 +77,11 @@ export declare class VersionService {
69
77
  * MC 26.1+ uses new YY.N version format and ships unobfuscated source.
70
78
  * Legacy 1.x.y versions remain obfuscated.
71
79
  * Snapshots: "26w01a" (year >= 26) → unobfuscated, "24w01a" → obfuscated.
80
+ *
81
+ * Pre-release / rc / snapshot suffixes: Mojang's real version-manifest ids
82
+ * use a hyphenated "-pre-N" / "-rc-N" / "-snapshot-N" form (e.g.
83
+ * "26.2-pre-6", "26.2-rc-2", "26.3-snapshot-6"). The older no-hyphen
84
+ * "-preN"/"-rcN" form is also accepted for backwards compatibility, but an
85
+ * arbitrary or numberless suffix (e.g. "26.1-foo", "26.1-snapshot") is not.
72
86
  */
73
87
  export declare function isUnobfuscatedVersion(version: string): boolean;
@@ -136,6 +136,39 @@ export class VersionService {
136
136
  mappingsUrl: clientMappingsUrl
137
137
  };
138
138
  }
139
+ /**
140
+ * Raw `libraries[].name` coordinates (e.g. "org.lwjgl:lwjgl-glfw:3.4.1")
141
+ * for one version's per-version JSON, in manifest order. Entries missing a
142
+ * non-empty string `name` are dropped here at the JSON boundary; further
143
+ * validation (does the name actually parse as a library coordinate) is the
144
+ * caller's job — see diffLibraries in version-diff-service.ts.
145
+ */
146
+ async getVersionLibraries(version) {
147
+ const normalizedVersion = version.trim();
148
+ if (!normalizedVersion) {
149
+ throw createError({
150
+ code: ERROR_CODES.INVALID_INPUT,
151
+ message: "version must be non-empty."
152
+ });
153
+ }
154
+ const manifest = await this.fetchManifest();
155
+ const versionEntry = (manifest.versions ?? []).find((entry) => entry.id === normalizedVersion);
156
+ if (!versionEntry) {
157
+ throw createError({
158
+ code: ERROR_CODES.VERSION_NOT_FOUND,
159
+ message: `Minecraft version "${normalizedVersion}" was not found in version manifest.`,
160
+ details: {
161
+ version: normalizedVersion,
162
+ nextAction: "Use list-versions to see available Minecraft versions.",
163
+ ...buildSuggestedCall({ tool: "list-versions", params: {} })
164
+ }
165
+ });
166
+ }
167
+ const details = await this.fetchVersionDetails(versionEntry.url, normalizedVersion);
168
+ return (details.libraries ?? [])
169
+ .map((lib) => lib?.name)
170
+ .filter((name) => typeof name === "string" && name.length > 0);
171
+ }
139
172
  async resolveServerJar(version) {
140
173
  const normalizedVersion = version.trim();
141
174
  if (!normalizedVersion) {
@@ -473,6 +506,12 @@ export class VersionService {
473
506
  * MC 26.1+ uses new YY.N version format and ships unobfuscated source.
474
507
  * Legacy 1.x.y versions remain obfuscated.
475
508
  * Snapshots: "26w01a" (year >= 26) → unobfuscated, "24w01a" → obfuscated.
509
+ *
510
+ * Pre-release / rc / snapshot suffixes: Mojang's real version-manifest ids
511
+ * use a hyphenated "-pre-N" / "-rc-N" / "-snapshot-N" form (e.g.
512
+ * "26.2-pre-6", "26.2-rc-2", "26.3-snapshot-6"). The older no-hyphen
513
+ * "-preN"/"-rcN" form is also accepted for backwards compatibility, but an
514
+ * arbitrary or numberless suffix (e.g. "26.1-foo", "26.1-snapshot") is not.
476
515
  */
477
516
  export function isUnobfuscatedVersion(version) {
478
517
  if (!version)
@@ -482,9 +521,12 @@ export function isUnobfuscatedVersion(version) {
482
521
  if (snapshotMatch) {
483
522
  return Number(snapshotMatch[1]) >= 26;
484
523
  }
485
- // New format: YY.N or YY.N.P, optionally with -preN/-rcN suffix.
486
- // Examples: "26.1", "27.3.1", "26.1-pre1", "26.1-rc1"
487
- const newFormatMatch = version.match(/^(\d{2,})\.\d+(?:\.\d+)?(?:-(?:pre|rc)\d+)?$/);
524
+ // New format: YY.N or YY.N.P, optionally with a pre-release/rc/snapshot
525
+ // suffix in either the legacy no-hyphen form (-preN, -rcN) or Mojang's
526
+ // real hyphenated form (-pre-N, -rc-N, -snapshot-N).
527
+ // Examples: "26.1", "27.3.1", "26.1-pre1", "26.1-rc1", "26.2-pre-6",
528
+ // "26.2-rc-2", "26.3-snapshot-6"
529
+ const newFormatMatch = version.match(/^(\d{2,})\.\d+(?:\.\d+)?(?:-(?:pre|rc)\d+|-(?:pre|rc|snapshot)-\d+)?$/);
488
530
  if (newFormatMatch) {
489
531
  return Number(newFormatMatch[1]) >= 26;
490
532
  }
@@ -50,6 +50,14 @@ export type DependencyVersionOptions = {
50
50
  export { isSafeMavenVersionToken };
51
51
  export declare class WorkspaceMappingService {
52
52
  detectCompileMapping(input: WorkspaceCompileMappingInput): Promise<WorkspaceCompileMappingOutput>;
53
+ /**
54
+ * Whether `projectPath` holds at least one build.gradle(.kts) that
55
+ * `detectCompileMapping` reads: the same files, found the same way, and readable.
56
+ * `detectCompileMapping` answers "no evidence" both when a build declares no
57
+ * mappings and when there was no build to read (a missing, mistyped or empty
58
+ * directory); this tells the two apart without changing that output.
59
+ */
60
+ hasReadableBuildScript(projectPath: string): Promise<boolean>;
53
61
  detectProjectMinecraftVersion(projectPath: string): Promise<string | undefined>;
54
62
  detectDependencyVersion(projectPath: string, group: string, name: string, opts?: DependencyVersionOptions): Promise<DependencyVersionResolution>;
55
63
  detectProjectLoader(projectPath: string): Promise<WorkspaceProjectLoaderOutput>;
@@ -1,4 +1,5 @@
1
- import { readdir, readFile } from "node:fs/promises";
1
+ import { constants } from "node:fs";
2
+ import { access, readdir, readFile } from "node:fs/promises";
2
3
  import { homedir } from "node:os";
3
4
  import { resolve } from "node:path";
4
5
  import fastGlob from "fast-glob";
@@ -271,6 +272,15 @@ function detectLoadersFromContent(content) {
271
272
  }
272
273
  return detections;
273
274
  }
275
+ /** The build.gradle(.kts) files `detectCompileMapping` scans under `root`, sorted. */
276
+ async function listCompileMappingBuildScripts(root) {
277
+ return (await fastGlob.glob(["build.gradle", "build.gradle.kts", "**/build.gradle", "**/build.gradle.kts"], {
278
+ cwd: root,
279
+ absolute: true,
280
+ onlyFiles: true,
281
+ ignore: ["**/.git/**", "**/.gradle/**", "**/build/**", "**/out/**", "**/node_modules/**"]
282
+ })).sort((left, right) => left.localeCompare(right));
283
+ }
274
284
  export class WorkspaceMappingService {
275
285
  async detectCompileMapping(input) {
276
286
  const projectPath = input.projectPath?.trim();
@@ -284,12 +294,7 @@ export class WorkspaceMappingService {
284
294
  });
285
295
  }
286
296
  const root = resolve(projectPath);
287
- const files = (await fastGlob.glob(["build.gradle", "build.gradle.kts", "**/build.gradle", "**/build.gradle.kts"], {
288
- cwd: root,
289
- absolute: true,
290
- onlyFiles: true,
291
- ignore: ["**/.git/**", "**/.gradle/**", "**/build/**", "**/out/**", "**/node_modules/**"]
292
- })).sort((left, right) => left.localeCompare(right));
297
+ const files = await listCompileMappingBuildScripts(root);
293
298
  const evidence = (await mapWithConcurrencyLimit(files, WORKSPACE_FILE_READ_CONCURRENCY, async (filePath) => {
294
299
  let content;
295
300
  try {
@@ -328,6 +333,29 @@ export class WorkspaceMappingService {
328
333
  warnings: []
329
334
  };
330
335
  }
336
+ /**
337
+ * Whether `projectPath` holds at least one build.gradle(.kts) that
338
+ * `detectCompileMapping` reads: the same files, found the same way, and readable.
339
+ * `detectCompileMapping` answers "no evidence" both when a build declares no
340
+ * mappings and when there was no build to read (a missing, mistyped or empty
341
+ * directory); this tells the two apart without changing that output.
342
+ */
343
+ async hasReadableBuildScript(projectPath) {
344
+ const trimmed = projectPath.trim();
345
+ if (!trimmed) {
346
+ return false;
347
+ }
348
+ for (const filePath of await listCompileMappingBuildScripts(resolve(trimmed))) {
349
+ try {
350
+ await access(filePath, constants.R_OK);
351
+ return true;
352
+ }
353
+ catch {
354
+ // detectCompileMapping skips an unreadable script too.
355
+ }
356
+ }
357
+ return false;
358
+ }
331
359
  async detectProjectMinecraftVersion(projectPath) {
332
360
  const root = resolve(projectPath);
333
361
  const propsPath = resolve(root, "gradle.properties");
package/docs/README-ja.md CHANGED
@@ -161,6 +161,8 @@ stdio トランスポートは、改行区切り形式と `Content-Length` フ
161
161
  - ワークスペースのソースカバレッジが部分的な場合でも、バニラクラスを確認できます。`inspect-minecraft task="list-files"` は、その場合に部分的な結果とフォローアップガイダンスを返します。
162
162
  - `analyze-mod` と `validate-project` は、オブジェクト形式の `subject` と正規の `include` グループを要求します。古い文字列形式の `subject` やドメイン名形式の `include` には `ERR_INVALID_INPUT` と、再試行しやすい `suggestedCall` を返します。
163
163
  - `validate-mixin` と `validate-project` は、`obfuscated` / `mojang` 検証では `mapping-health` を軽量に保ちます。`intermediary` / `yarn` 名前空間を要求しない限り、完全な Tiny マッピンググラフは読み込みません。
164
+ - `validate-project task="project-summary"` は `version` を省略すると `gradle.properties` から Minecraft バージョンを推定し、推定したバージョンを `warnings` に示します。推定を止めるには `preferProjectVersion: false` を指定します。その場合、`version` のない呼び出しは `status: "blocked"` を返します。解決したバージョン(明示指定または推定)は、検出したすべての Mixin / Access Widener / Access Transformer の検証に渡されます。ファイルを検出したのにバージョンを推定できない場合は、推測せずに、明示的な `version` を求める再試行案付きで `status: "blocked"` を返します。
165
+ - `validate-project task="project-summary"` が検証対象のファイルを 1 つも検出しなかった場合、`status` は `"ok"` のままですが、headline が `Nothing to validate: ...` となり、`warnings` にも何も検証していないことが示されます。`"ok"` だけでは、いずれかのファイルが検証に通ったことを意味しません。
164
166
  - `validate-project task="project-summary"` の `tasks["minecraft.artifact.resolved"]` は軽量なアーティファクト probe です。probe 状態を返すためだけに Minecraft のデコンパイルやソースインデックス再構築は行いません。追加の `tasks` フィールドを省きたい場合は `VALIDATE_PROJECT_TASKS_OFF=1` を使います。
165
167
 
166
168
  ### あるバージョンの Minecraft ソースを確認する