@adhisang/minecraft-modding-mcp 5.0.0 → 6.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 (72) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/access-widener-parser.d.ts +1 -0
  3. package/dist/access-widener-parser.js +6 -3
  4. package/dist/cache-registry.js +35 -31
  5. package/dist/decompiler/vineflower.js +8 -2
  6. package/dist/entry-tools/analyze-mod-service.js +2 -1
  7. package/dist/entry-tools/analyze-symbol-service.js +18 -9
  8. package/dist/entry-tools/compare-minecraft-service.js +1 -1
  9. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -1
  10. package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -1
  11. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +2 -1
  12. package/dist/entry-tools/inspect-minecraft/internal.js +2 -0
  13. package/dist/entry-tools/response-contract.d.ts +2 -0
  14. package/dist/entry-tools/response-contract.js +2 -2
  15. package/dist/entry-tools/validate-project-service.d.ts +4 -4
  16. package/dist/entry-tools/verify-mixin-target-service.js +7 -7
  17. package/dist/index.js +20 -16
  18. package/dist/java-process.js +1 -1
  19. package/dist/json-rpc-framing.d.ts +4 -0
  20. package/dist/json-rpc-framing.js +23 -1
  21. package/dist/mapping/internal-types.d.ts +17 -0
  22. package/dist/mapping/loaders/tiny-maven.js +46 -20
  23. package/dist/mapping/lookup.d.ts +12 -1
  24. package/dist/mapping/lookup.js +53 -1
  25. package/dist/mapping-service.js +36 -21
  26. package/dist/mixin/annotation-validators.js +21 -3
  27. package/dist/mixin/parsed-validator.js +1 -0
  28. package/dist/mixin-parser.js +71 -3
  29. package/dist/mod-decompile-service.d.ts +2 -0
  30. package/dist/mod-decompile-service.js +19 -2
  31. package/dist/mod-remap-service.js +6 -5
  32. package/dist/mojang-tiny-mapping-service.js +5 -2
  33. package/dist/nbt/java-nbt-codec.js +40 -16
  34. package/dist/nbt/json-patch.js +51 -12
  35. package/dist/nbt/typed-json.d.ts +1 -0
  36. package/dist/nbt/typed-json.js +12 -2
  37. package/dist/nbt/types.d.ts +2 -2
  38. package/dist/path-converter.js +10 -3
  39. package/dist/registry-service.d.ts +2 -0
  40. package/dist/registry-service.js +16 -1
  41. package/dist/repo-downloader.js +4 -3
  42. package/dist/source/artifact-resolver.js +1 -1
  43. package/dist/source/class-source/snippet-builder.js +6 -0
  44. package/dist/source/class-source-helpers.d.ts +4 -4
  45. package/dist/source/class-source-helpers.js +12 -11
  46. package/dist/source/class-source.d.ts +19 -0
  47. package/dist/source/class-source.js +48 -21
  48. package/dist/source/file-access.js +3 -2
  49. package/dist/source/indexer.js +24 -7
  50. package/dist/source/lifecycle/mapping-helpers.js +28 -3
  51. package/dist/source/lifecycle/runtime-check.js +20 -6
  52. package/dist/source/search.js +23 -8
  53. package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
  54. package/dist/source/workspace-target.js +2 -1
  55. package/dist/source-resolver.js +2 -2
  56. package/dist/source-service.js +9 -1
  57. package/dist/stdio-supervisor.d.ts +2 -2
  58. package/dist/stdio-supervisor.js +29 -9
  59. package/dist/storage/db.js +24 -1
  60. package/dist/storage/files-repo.d.ts +3 -0
  61. package/dist/storage/files-repo.js +43 -37
  62. package/dist/storage/symbols-repo.d.ts +9 -0
  63. package/dist/storage/symbols-repo.js +47 -19
  64. package/dist/symbols/symbol-extractor.js +1 -1
  65. package/dist/tool-guidance.d.ts +2 -1
  66. package/dist/tool-guidance.js +26 -1
  67. package/dist/tool-schemas.d.ts +4 -4
  68. package/dist/tool-schemas.js +91 -91
  69. package/dist/warning-details.d.ts +12 -0
  70. package/dist/warning-details.js +17 -0
  71. package/dist/workspace-mapping-service.js +9 -0
  72. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import { remapModJar } from "./mod-remap-service.js";
15
15
  import { registerResources } from "./resources.js";
16
16
  import { SourceService } from "./source-service.js";
17
17
  import { ToolExecutionGate } from "./tool-execution-gate.js";
18
- import { classifyWarnings } from "./warning-details.js";
18
+ import { capWarningDetailsForSummary, classifyWarnings } from "./warning-details.js";
19
19
  import { WorkspaceMappingService } from "./workspace-mapping-service.js";
20
20
  import { InspectMinecraftService, inspectMinecraftSchema, inspectMinecraftShape } from "./entry-tools/inspect-minecraft-service.js";
21
21
  import { AnalyzeSymbolService, analyzeSymbolSchema, analyzeSymbolShape } from "./entry-tools/analyze-symbol-service.js";
@@ -90,7 +90,7 @@ server.validateToolInput = async (_tool, args) => args;
90
90
  // server.tool() but appends a note steering agents to the entry tools first.
91
91
  // Entry tools, batch tools, and the NBT/runtime utilities (which have no entry
92
92
  // equivalent) keep their plain descriptions via server.tool().
93
- const EXPERT_TOOL_NOTE = " Expert/follow-up tool: prefer the entry tools (inspect-minecraft, analyze-symbol, compare-minecraft, analyze-mod, validate-project) first; reach for this only for the narrow operation it names.";
93
+ const EXPERT_TOOL_NOTE = " Expert tool: prefer the entry tools (inspect-minecraft, analyze-symbol, compare-minecraft, analyze-mod, validate-project) first.";
94
94
  const expertTool = ((name, description, ...rest) => server.tool(name, description + EXPERT_TOOL_NOTE, ...rest));
95
95
  const config = loadConfig();
96
96
  const nbtLimits = {
@@ -385,18 +385,23 @@ async function runTool(tool, rawInput, schema, action) {
385
385
  if (DETAIL_ENABLED_TOOL_NAMES.has(tool)) {
386
386
  projectedResult = projectByDetail(tool, projectedResult, effectiveDetail, shapeInput.include);
387
387
  }
388
- // Entry, expert, and batch tools all report the applied detail/include shape in meta.
388
+ // Entry, expert, and batch tools report the applied detail/include shape in meta.
389
+ // detailApplied is omitted when it matches the tool's default to keep responses lean.
390
+ const defaultDetail = ENTRY_TOOL_NAMES.has(tool)
391
+ ? "summary"
392
+ : DEFAULT_DETAIL_BY_TOOL[tool] ?? "summary";
389
393
  const entryMeta = ENTRY_TOOL_NAMES.has(tool) ||
390
394
  DETAIL_ENABLED_TOOL_NAMES.has(tool) ||
391
395
  BATCH_DETAIL_TOOL_NAMES.has(tool)
392
396
  ? buildEntryToolMeta({
393
397
  detail: effectiveDetail,
398
+ defaultDetail,
394
399
  include: shapeInput.include.size > 0 ? [...shapeInput.include] : undefined
395
400
  })
396
401
  : undefined;
397
402
  const durationMs = Date.now() - startedAt;
398
403
  sourceService.recordToolCall(tool, durationMs);
399
- const warningDetails = classifyWarnings(warnings);
404
+ const warningDetails = capWarningDetailsForSummary(classifyWarnings(warnings), effectiveDetail === "summary");
400
405
  return objectResult({
401
406
  result: projectedResult,
402
407
  meta: {
@@ -405,7 +410,7 @@ async function runTool(tool, rawInput, schema, action) {
405
410
  requestId,
406
411
  tool,
407
412
  durationMs,
408
- warnings,
413
+ ...(warnings.length > 0 ? { warnings } : {}),
409
414
  ...(warningDetails.length > 0 ? { warningDetails } : {})
410
415
  }
411
416
  });
@@ -450,8 +455,7 @@ async function runTool(tool, rawInput, schema, action) {
450
455
  const errorMeta = {
451
456
  requestId,
452
457
  tool,
453
- durationMs: errorDurationMs,
454
- warnings: []
458
+ durationMs: errorDurationMs
455
459
  };
456
460
  applyErrorMetaExtensions(errorMeta, caughtError);
457
461
  return objectResult({
@@ -467,7 +471,7 @@ expertTool("list-versions", "List available Minecraft versions from Mojang manif
467
471
  registerToolSchema("list-versions", listVersionsSchema);
468
472
  server.tool("inspect-minecraft", "Top-level workflow tool for version discovery, artifact resolution, class inspection, source search, file reads, and file listings.", inspectMinecraftShape, { readOnlyHint: true }, async (args) => runTool("inspect-minecraft", args, inspectMinecraftSchema, async (input) => inspectMinecraftService.execute(input)));
469
473
  registerToolSchema("inspect-minecraft", inspectMinecraftSchema);
470
- server.tool("analyze-symbol", "Top-level workflow tool for symbol existence, mapping, lifecycle (with fromVersion/toVersion/maxVersions/includeTimeline range controls), workspace analysis, and API overview. subject.kind accepts 'symbol' to auto-detect class/field/method from the selector (inferred kind is returned as a warning).", analyzeSymbolShape, { readOnlyHint: true }, async (args) => runTool("analyze-symbol", args, analyzeSymbolSchema, async (input) => analyzeSymbolService.execute(input)));
474
+ server.tool("analyze-symbol", "Top-level workflow tool for symbol existence, mapping, lifecycle, workspace analysis, and API overview. subject.kind='symbol' auto-detects class/field/method from the selector.", analyzeSymbolShape, { readOnlyHint: true }, async (args) => runTool("analyze-symbol", args, analyzeSymbolSchema, async (input) => analyzeSymbolService.execute(input)));
471
475
  registerToolSchema("analyze-symbol", analyzeSymbolSchema);
472
476
  server.tool("compare-minecraft", "Top-level workflow tool for version comparisons, class diffs, registry diffs, and migration overviews.", compareMinecraftShape, { readOnlyHint: true }, async (args) => runTool("compare-minecraft", args, compareMinecraftSchema, async (input) => compareMinecraftService.execute(input)));
473
477
  registerToolSchema("compare-minecraft", compareMinecraftSchema);
@@ -497,16 +501,16 @@ if (!VERIFY_MIXIN_TARGET_OFF) {
497
501
  registerToolSchema("verify-mixin-target", verifyMixinTargetSchema);
498
502
  }
499
503
  if (!BATCH_TOOLS_OFF) {
500
- server.tool("batch-class-source", "Batch lookup: read source for many classes in one call, sharing a single resolved artifact. Returns per-entry { status, result?, error? } plus aggregate summary. Per-entry retry suggestions point at get-class-source. Not read-only: per-entry outputFile writes source files to disk.", batchClassSourceShape, { readOnlyHint: false }, async (args) => runTool("batch-class-source", args, batchClassSourceSchema, async (input) => batchClassSourceService.execute(input)));
504
+ server.tool("batch-class-source", "Batch get-class-source for many classes sharing one resolved artifact; returns per-entry status plus aggregate summary. Not read-only: per-entry outputFile writes to disk.", batchClassSourceShape, { readOnlyHint: false }, async (args) => runTool("batch-class-source", args, batchClassSourceSchema, async (input) => batchClassSourceService.execute(input)));
501
505
  registerToolSchema("batch-class-source", batchClassSourceSchema);
502
- server.tool("batch-class-members", "Batch lookup: list members for many classes in one call, sharing a single resolved artifact. Returns per-entry { status, result?, error? } plus aggregate summary. Per-entry retry suggestions point at get-class-members.", batchClassMembersShape, { readOnlyHint: true }, async (args) => runTool("batch-class-members", args, batchClassMembersSchema, async (input) => batchClassMembersService.execute(input)));
506
+ server.tool("batch-class-members", "Batch get-class-members for many classes sharing one resolved artifact; returns per-entry status plus aggregate summary.", batchClassMembersShape, { readOnlyHint: true }, async (args) => runTool("batch-class-members", args, batchClassMembersSchema, async (input) => batchClassMembersService.execute(input)));
503
507
  registerToolSchema("batch-class-members", batchClassMembersSchema);
504
- server.tool("batch-symbol-exists", "Batch existence/mapping query: probe many symbols in one call against a shared Minecraft-version artifact. Accepts target.kind=workspace or version only (other kinds carry library versions, not Minecraft versions). Per-entry retry suggestions point at check-symbol-exists.", batchSymbolExistsShape, { readOnlyHint: true }, async (args) => runTool("batch-symbol-exists", args, batchSymbolExistsSchema, async (input) => batchSymbolExistsService.execute(input)));
508
+ server.tool("batch-symbol-exists", "Batch check-symbol-exists for many symbols against one shared Minecraft version (target.kind=version or workspace only).", batchSymbolExistsShape, { readOnlyHint: true }, async (args) => runTool("batch-symbol-exists", args, batchSymbolExistsSchema, async (input) => batchSymbolExistsService.execute(input)));
505
509
  registerToolSchema("batch-symbol-exists", batchSymbolExistsSchema);
506
- server.tool("batch-mappings", "Batch mapping translation: resolve many symbols across mapping namespaces with one shared Minecraft version. Per-entry retry suggestions point at find-mapping.", batchMappingsShape, { readOnlyHint: true }, async (args) => runTool("batch-mappings", args, batchMappingsSchema, async (input) => batchMappingsService.execute(input)));
510
+ server.tool("batch-mappings", "Batch find-mapping: resolve many symbols across mapping namespaces with one shared Minecraft version.", batchMappingsShape, { readOnlyHint: true }, async (args) => runTool("batch-mappings", args, batchMappingsSchema, async (input) => batchMappingsService.execute(input)));
507
511
  registerToolSchema("batch-mappings", batchMappingsSchema);
508
512
  }
509
- expertTool("resolve-artifact", "Resolve source artifact from a target object ({ kind, value }) and return artifact metadata. For target.kind=jar, only <basename>-sources.jar is auto-adopted; other adjacent *-sources.jar files are informational.", resolveArtifactShape, { readOnlyHint: true }, async (args) => runTool("resolve-artifact", args, resolveArtifactSchema, async (input) => sourceService.resolveArtifact({
513
+ expertTool("resolve-artifact", "Resolve a source artifact and return artifact metadata. For target.kind=jar, only <basename>-sources.jar is auto-adopted.", resolveArtifactShape, { readOnlyHint: true }, async (args) => runTool("resolve-artifact", args, resolveArtifactSchema, async (input) => sourceService.resolveArtifact({
510
514
  target: input.target,
511
515
  mapping: input.mapping,
512
516
  sourcePriority: input.sourcePriority,
@@ -532,7 +536,7 @@ expertTool("find-class", "Resolve a simple or qualified class name to fully-qual
532
536
  limit: input.limit
533
537
  })));
534
538
  registerToolSchema("find-class", findClassSchema);
535
- expertTool("get-class-source", "Get Java source for a class by target ({ kind: 'artifact', artifactId } or { kind: 'version'|'jar'|'coordinate'|'workspace'|'dependency', ... } — same shape as resolve-artifact). To read source text, pass mode=snippet (bounded excerpt) or mode=full (entire source); the default mode=metadata returns a symbol outline only, not the body. Not read-only: outputFile writes the source to disk.", getClassSourceShape, { readOnlyHint: false }, async (args) => runTool("get-class-source", args, getClassSourceSchema, async (input) => {
539
+ expertTool("get-class-source", "Get Java source for a class. Default mode=metadata returns a symbol outline only; pass mode=snippet or mode=full to read source text. Not read-only: outputFile writes to disk.", getClassSourceShape, { readOnlyHint: false }, async (args) => runTool("get-class-source", args, getClassSourceSchema, async (input) => {
536
540
  const normalizedTarget = normalizeSourceLookupTarget(input.target);
537
541
  return sourceService.getClassSource({
538
542
  className: input.className,
@@ -555,7 +559,7 @@ expertTool("get-class-source", "Get Java source for a class by target ({ kind: '
555
559
  });
556
560
  }));
557
561
  registerToolSchema("get-class-source", getClassSourceSchema);
558
- expertTool("get-class-members", "Get fields/methods/constructors for one class from binary bytecode by target ({ kind: 'artifact', artifactId } or { kind: 'version'|'jar'|'coordinate'|'workspace'|'dependency', ... } — same shape as resolve-artifact).", getClassMembersShape, { readOnlyHint: true }, async (args) => runTool("get-class-members", args, getClassMembersSchema, async (input) => {
562
+ expertTool("get-class-members", "Get fields/methods/constructors for one class from binary bytecode.", getClassMembersShape, { readOnlyHint: true }, async (args) => runTool("get-class-members", args, getClassMembersSchema, async (input) => {
559
563
  const normalizedTarget = normalizeSourceLookupTarget(input.target);
560
564
  return sourceService.getClassMembers({
561
565
  className: input.className,
@@ -657,7 +661,7 @@ expertTool("find-mapping", "Find symbol mapping candidates between namespaces us
657
661
  maxCandidates: input.maxCandidates
658
662
  })));
659
663
  registerToolSchema("find-mapping", findMappingSchema);
660
- expertTool("resolve-method-mapping-exact", "Strict shortcut for find-mapping(kind=method, signatureMode=exact): resolve one method mapping by owner+name+descriptor between namespaces and report resolved/not_found/ambiguous. Stricter than find-mapping's exact mode — it requires a COMPLETE descriptor projection and returns mapping_unavailable when the descriptor's class references cannot all be projected to the target namespace (find-mapping is more lenient there). Use find-mapping kind=method signatureMode=exact unless you specifically need that strict-completeness guarantee.", resolveMethodMappingExactShape, { readOnlyHint: true }, async (args) => runTool("resolve-method-mapping-exact", args, resolveMethodMappingExactSchema, async (input) => sourceService.resolveMethodMappingExact({
664
+ expertTool("resolve-method-mapping-exact", "Strict variant of find-mapping(kind=method, signatureMode=exact): requires a COMPLETE descriptor projection and returns mapping_unavailable when any descriptor class reference cannot be projected. Prefer find-mapping unless you need that guarantee.", resolveMethodMappingExactShape, { readOnlyHint: true }, async (args) => runTool("resolve-method-mapping-exact", args, resolveMethodMappingExactSchema, async (input) => sourceService.resolveMethodMappingExact({
661
665
  version: input.version,
662
666
  name: input.name,
663
667
  owner: input.owner,
@@ -107,7 +107,7 @@ export function runJavaProcess(options) {
107
107
  }
108
108
  }));
109
109
  });
110
- proc.once("exit", (code) => {
110
+ proc.once("close", (code) => {
111
111
  clearTimeout(timer);
112
112
  resolve({
113
113
  exitCode: code ?? -1,
@@ -9,6 +9,9 @@ export declare function encodeJsonRpcMessage(message: JSONRPCMessage, mode: Conc
9
9
  export declare class JsonRpcFrameReader {
10
10
  private mode;
11
11
  private buffer;
12
+ private pendingChunks;
13
+ private pendingBytes;
14
+ private awaitedFrameEnd;
12
15
  get currentMode(): FramingMode;
13
16
  reset(): void;
14
17
  clear(): void;
@@ -16,6 +19,7 @@ export declare class JsonRpcFrameReader {
16
19
  onFrame: (frame: ParsedJsonRpcFrame) => void;
17
20
  onError: (error: Error) => void;
18
21
  }): void;
22
+ private canCompleteFrame;
19
23
  private detectMode;
20
24
  private readLineDelimitedMessage;
21
25
  private readContentLengthMessage;
@@ -25,21 +25,35 @@ export function encodeJsonRpcMessage(message, mode) {
25
25
  export class JsonRpcFrameReader {
26
26
  mode = "unknown";
27
27
  buffer = Buffer.alloc(0);
28
+ pendingChunks = [];
29
+ pendingBytes = 0;
30
+ awaitedFrameEnd = -1;
28
31
  get currentMode() {
29
32
  return this.mode;
30
33
  }
31
34
  reset() {
32
35
  this.mode = "unknown";
36
+ this.awaitedFrameEnd = -1;
33
37
  }
34
38
  clear() {
35
39
  this.mode = "unknown";
36
40
  this.buffer = Buffer.alloc(0);
41
+ this.pendingChunks = [];
42
+ this.pendingBytes = 0;
43
+ this.awaitedFrameEnd = -1;
37
44
  }
38
45
  processChunk(chunk, handlers) {
39
46
  if (chunk.length === 0) {
40
47
  return;
41
48
  }
42
- this.buffer = Buffer.concat([this.buffer, chunk]);
49
+ this.pendingChunks.push(chunk);
50
+ this.pendingBytes += chunk.length;
51
+ if (!this.canCompleteFrame(chunk)) {
52
+ return;
53
+ }
54
+ this.buffer = Buffer.concat([this.buffer, ...this.pendingChunks]);
55
+ this.pendingChunks = [];
56
+ this.pendingBytes = 0;
43
57
  while (true) {
44
58
  try {
45
59
  if (this.mode === "unknown") {
@@ -71,6 +85,12 @@ export class JsonRpcFrameReader {
71
85
  }
72
86
  }
73
87
  }
88
+ canCompleteFrame(chunk) {
89
+ if (this.mode === "content-length" && this.awaitedFrameEnd >= 0) {
90
+ return this.buffer.length + this.pendingBytes >= this.awaitedFrameEnd;
91
+ }
92
+ return chunk.includes(0x0a);
93
+ }
74
94
  detectMode() {
75
95
  while (this.buffer.length > 0) {
76
96
  if (this.buffer[0] === 0x0a) {
@@ -124,6 +144,7 @@ export class JsonRpcFrameReader {
124
144
  }
125
145
  }
126
146
  readContentLengthMessage() {
147
+ this.awaitedFrameEnd = -1;
127
148
  const headerBoundary = findHeaderBoundary(this.buffer);
128
149
  if (!headerBoundary) {
129
150
  return undefined;
@@ -158,6 +179,7 @@ export class JsonRpcFrameReader {
158
179
  const messageStart = headerBoundary.index + headerBoundary.delimiterBytes;
159
180
  const frameEnd = messageStart + contentLength;
160
181
  if (this.buffer.length < frameEnd) {
182
+ this.awaitedFrameEnd = frameEnd;
161
183
  return undefined;
162
184
  }
163
185
  const body = this.buffer.subarray(messageStart, frameEnd).toString("utf8");
@@ -20,6 +20,18 @@ export type DirectionIndex = {
20
20
  simple: Map<string, Set<string>>;
21
21
  records: Map<string, MappingSymbolRecord>;
22
22
  };
23
+ /**
24
+ * O(1) exact-lookup index over a single target namespace's records, used by
25
+ * existence checks. `byKey` is keyed by `kind|owner|name` (owner empty for
26
+ * classes, which key on their FQCN symbol); `classBySimpleName` resolves the
27
+ * auto-class short-name path. Values are arrays so callers can still tell a
28
+ * unique match (resolved) from multiple (ambiguous).
29
+ */
30
+ export type ExactRecordIndex = {
31
+ byKey: Map<string, MappingSymbolRecord[]>;
32
+ classBySimpleName: Map<string, MappingSymbolRecord[]>;
33
+ membersByOwner: Map<string, MappingSymbolRecord[]>;
34
+ };
23
35
  export type MappingLookupSource = "loom-cache" | "maven" | "mojang-client-mappings";
24
36
  export type PairRecord = {
25
37
  index: DirectionIndex;
@@ -50,6 +62,11 @@ export type LoadedGraph = {
50
62
  adjacency: Map<import("../types.js").SourceMapping, import("../types.js").SourceMapping[]>;
51
63
  pathCache: Map<PairKey, import("../types.js").SourceMapping[] | undefined>;
52
64
  recordsByTarget: Map<import("../types.js").SourceMapping, MappingSymbolRecord[]>;
65
+ /**
66
+ * Graph-scoped, lazily-built exact-lookup indexes keyed by target namespace.
67
+ * Populated on first existence check for a namespace; dies with graph eviction.
68
+ */
69
+ exactRecordIndex: Map<import("../types.js").SourceMapping, ExactRecordIndex>;
53
70
  /**
54
71
  * Graph-scoped cache of class-to-class descriptor projections, keyed by
55
72
  * `path.join(">") + NUL + internalName`. Value is the projected internal name,
@@ -53,36 +53,62 @@ export async function loadTinyPairsFromMaven(deps, version) {
53
53
  const warnings = [];
54
54
  const merged = new Map();
55
55
  const repos = deps.config.sourceRepos;
56
- const intermediaryUrls = [];
57
- const yarnUrls = [];
56
+ const attemptedUrls = [];
58
57
  const repoBases = repos.map((repo) => repo.replace(/\/+$/, ""));
59
58
  const yarnCoordinatesByRepo = await Promise.all(repoBases.map(async (base) => ({
60
59
  base,
61
60
  yarnCoordinates: await fetchYarnCoordinates(deps.fetchFn, base, version)
62
61
  })));
62
+ const tryUrls = async (urls) => {
63
+ for (const url of urls) {
64
+ attemptedUrls.push(url);
65
+ try {
66
+ const downloaded = await downloadToCache(url, defaultDownloadPath(deps.config.cacheDir, url), {
67
+ fetchFn: deps.fetchFn,
68
+ retries: deps.config.fetchRetries,
69
+ timeoutMs: deps.config.fetchTimeoutMs
70
+ });
71
+ if (!downloaded.ok || !downloaded.path) {
72
+ continue;
73
+ }
74
+ const parsed = await parseTinyFromJar(downloaded.path);
75
+ if (parsed.size > 0) {
76
+ return parsed;
77
+ }
78
+ }
79
+ catch {
80
+ // try the next candidate URL
81
+ }
82
+ }
83
+ return undefined;
84
+ };
85
+ let intermediaryParsed;
86
+ let yarnParsed;
63
87
  for (const { base, yarnCoordinates } of yarnCoordinatesByRepo) {
64
- intermediaryUrls.push(`${base}/net/fabricmc/intermediary/${version}/intermediary-${version}-v2.jar`, `${base}/net/fabricmc/intermediary/${version}/intermediary-${version}.jar`);
88
+ if (!intermediaryParsed) {
89
+ intermediaryParsed = await tryUrls([
90
+ `${base}/net/fabricmc/intermediary/${version}/intermediary-${version}-v2.jar`,
91
+ `${base}/net/fabricmc/intermediary/${version}/intermediary-${version}.jar`
92
+ ]);
93
+ }
65
94
  for (const coordinate of yarnCoordinates) {
66
- yarnUrls.push(`${base}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}-v2.jar`, `${base}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}.jar`);
95
+ if (yarnParsed) {
96
+ break;
97
+ }
98
+ yarnParsed = await tryUrls([
99
+ `${base}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}-v2.jar`,
100
+ `${base}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}.jar`
101
+ ]);
67
102
  }
68
- }
69
- const allUrls = [...intermediaryUrls, ...yarnUrls];
70
- const parsedResults = await Promise.allSettled(allUrls.map(async (url) => {
71
- const downloaded = await downloadToCache(url, defaultDownloadPath(deps.config.cacheDir, url), {
72
- fetchFn: deps.fetchFn,
73
- retries: deps.config.fetchRetries,
74
- timeoutMs: deps.config.fetchTimeoutMs
75
- });
76
- if (!downloaded.ok || !downloaded.path) {
77
- return undefined;
103
+ if (intermediaryParsed && yarnParsed) {
104
+ break;
78
105
  }
79
- return parseTinyFromJar(downloaded.path);
80
- }));
81
- for (const result of parsedResults) {
82
- if (result.status !== "fulfilled" || !result.value) {
106
+ }
107
+ for (const parsed of [intermediaryParsed, yarnParsed]) {
108
+ if (!parsed) {
83
109
  continue;
84
110
  }
85
- for (const [key, index] of result.value.entries()) {
111
+ for (const [key, index] of parsed.entries()) {
86
112
  const existing = merged.get(key);
87
113
  if (!existing) {
88
114
  merged.set(key, index);
@@ -98,7 +124,7 @@ export async function loadTinyPairsFromMaven(deps, version) {
98
124
  return {
99
125
  pairs: merged,
100
126
  warnings,
101
- mappingArtifact: allUrls[0] ?? "maven:none"
127
+ mappingArtifact: attemptedUrls[0] ?? "maven:none"
102
128
  };
103
129
  }
104
130
  //# sourceMappingURL=tiny-maven.js.map
@@ -1,5 +1,5 @@
1
1
  import type { MappingSourcePriority, SourceMapping } from "../types.js";
2
- import type { CandidateAccumulator, DirectionIndex, LoadedGraph, MappingLookupSource, MappingSymbolRecord, MatchRankKey, PairKey, PairRecord } from "./internal-types.js";
2
+ import type { CandidateAccumulator, DirectionIndex, ExactRecordIndex, LoadedGraph, MappingLookupSource, MappingSymbolRecord, MatchRankKey, PairKey, PairRecord } from "./internal-types.js";
3
3
  import { MAX_CANDIDATES } from "./internal-types.js";
4
4
  import type { ClassApiMatrixKind, DescriptorProjection, MappingLookupCandidate, ResolutionCandidate, SymbolQueryInput, SymbolReference } from "./types.js";
5
5
  export type { DescriptorProjection };
@@ -41,6 +41,17 @@ export declare function applyDisambiguationHints(candidates: MappingLookupCandid
41
41
  export declare function projectLookupCandidateDescriptor(candidate: MappingLookupCandidate, sourceDescriptor: string, targetDescriptor: string | undefined): MappingLookupCandidate;
42
42
  export declare function effectiveLoomSearchProjectPath(projectPath: string | undefined): string | undefined;
43
43
  export declare function collectTargetRecords(graph: LoadedGraph, targetMapping: SourceMapping): MappingSymbolRecord[];
44
+ /**
45
+ * Lazily build (and memoize on the graph) an O(1) exact-lookup index for a
46
+ * target namespace. Classes key on their FQCN symbol (owner left empty) and are
47
+ * additionally indexed by simple name for the auto-class short-name path;
48
+ * fields/methods key on `kind|owner|name`.
49
+ */
50
+ export declare function collectExactRecordIndex(graph: LoadedGraph, targetMapping: SourceMapping): ExactRecordIndex;
51
+ export declare function lookupExactClassBySymbol(index: ExactRecordIndex, symbol: string): MappingSymbolRecord[];
52
+ export declare function lookupExactMembersByOwner(index: ExactRecordIndex, owner: string): MappingSymbolRecord[];
53
+ export declare function lookupExactClassBySimpleName(index: ExactRecordIndex, name: string): MappingSymbolRecord[];
54
+ export declare function lookupExactMembers(index: ExactRecordIndex, kind: "field" | "method", owner: string, name: string): MappingSymbolRecord[];
44
55
  export declare function normalizeIncludedKinds(inputKinds: ClassApiMatrixKind[] | undefined): Set<ClassApiMatrixKind>;
45
56
  export declare function inferAmbiguityReasons(candidates: ResolutionCandidate[], usedMojangClientMappings: boolean): string[];
46
57
  export declare function clampCandidateLimit(limit: number | undefined): number;
@@ -429,7 +429,59 @@ export function effectiveLoomSearchProjectPath(projectPath) {
429
429
  return normalizeOptionalProjectPath(projectPath) ?? normalizeOptionalProjectPath(process.cwd());
430
430
  }
431
431
  export function collectTargetRecords(graph, targetMapping) {
432
- return [...(graph.recordsByTarget.get(targetMapping) ?? [])];
432
+ return graph.recordsByTarget.get(targetMapping) ?? [];
433
+ }
434
+ function exactRecordKey(kind, owner, name) {
435
+ return `${kind}|${owner}|${name}`;
436
+ }
437
+ /**
438
+ * Lazily build (and memoize on the graph) an O(1) exact-lookup index for a
439
+ * target namespace. Classes key on their FQCN symbol (owner left empty) and are
440
+ * additionally indexed by simple name for the auto-class short-name path;
441
+ * fields/methods key on `kind|owner|name`.
442
+ */
443
+ export function collectExactRecordIndex(graph, targetMapping) {
444
+ const cached = graph.exactRecordIndex.get(targetMapping);
445
+ if (cached) {
446
+ return cached;
447
+ }
448
+ const byKey = new Map();
449
+ const classBySimpleName = new Map();
450
+ const membersByOwner = new Map();
451
+ const push = (map, key, record) => {
452
+ const bucket = map.get(key);
453
+ if (bucket) {
454
+ bucket.push(record);
455
+ }
456
+ else {
457
+ map.set(key, [record]);
458
+ }
459
+ };
460
+ for (const record of collectTargetRecords(graph, targetMapping)) {
461
+ if (record.kind === "class") {
462
+ push(byKey, exactRecordKey("class", "", record.symbol), record);
463
+ push(classBySimpleName, record.name, record);
464
+ }
465
+ else {
466
+ push(byKey, exactRecordKey(record.kind, record.owner ?? "", record.name), record);
467
+ push(membersByOwner, record.owner ?? "", record);
468
+ }
469
+ }
470
+ const index = { byKey, classBySimpleName, membersByOwner };
471
+ graph.exactRecordIndex.set(targetMapping, index);
472
+ return index;
473
+ }
474
+ export function lookupExactClassBySymbol(index, symbol) {
475
+ return index.byKey.get(exactRecordKey("class", "", symbol)) ?? [];
476
+ }
477
+ export function lookupExactMembersByOwner(index, owner) {
478
+ return index.membersByOwner.get(owner) ?? [];
479
+ }
480
+ export function lookupExactClassBySimpleName(index, name) {
481
+ return index.classBySimpleName.get(name) ?? [];
482
+ }
483
+ export function lookupExactMembers(index, kind, owner, name) {
484
+ return index.byKey.get(exactRecordKey(kind, owner, name)) ?? [];
433
485
  }
434
486
  export function normalizeIncludedKinds(inputKinds) {
435
487
  const normalized = new Set();
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { mkdir, rename, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
+ import { randomBytes } from "node:crypto";
4
5
  import { buildSuggestedCall } from "./build-suggested-call.js";
5
6
  import { createError, ERROR_CODES } from "./errors.js";
6
7
  import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "./page-cursor.js";
@@ -11,7 +12,7 @@ import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
11
12
  import { buildSymbolKey, createClassSymbolRecord, mergeDirectionIndexes, normalizeMappedSymbolOutput } from "./mapping/parsers/symbol-records.js";
12
13
  import { buildAdjacency, buildTargetRecordIndex, pairKey } from "./mapping/parsers/normalize.js";
13
14
  import { MATCH_RANK, MAX_CANDIDATES } from "./mapping/internal-types.js";
14
- import { applyDisambiguationHints, clampRowLimit, collectTargetRecords, effectiveLoomSearchProjectPath, inferAmbiguityReasons, invalidInputError, limitResolutionCandidates, lookupCandidates, mappingPriorityFromInput, mappingSourceOrder, namespacePath, normalizeIncludedKinds, normalizeQuerySymbol, pathToTransformChain, pathUsesSource, projectLookupCandidateDescriptor, requiresOnlyObfuscatedMojangGraph, toLookupCandidate, toResolutionCandidate } from "./mapping/lookup.js";
15
+ import { applyDisambiguationHints, clampRowLimit, collectExactRecordIndex, collectTargetRecords, effectiveLoomSearchProjectPath, inferAmbiguityReasons, invalidInputError, limitResolutionCandidates, lookupCandidates, lookupExactClassBySimpleName, lookupExactClassBySymbol, lookupExactMembers, lookupExactMembersByOwner, mappingPriorityFromInput, mappingSourceOrder, namespacePath, normalizeIncludedKinds, normalizeQuerySymbol, pathToTransformChain, pathUsesSource, projectLookupCandidateDescriptor, requiresOnlyObfuscatedMojangGraph, toLookupCandidate, toResolutionCandidate } from "./mapping/lookup.js";
15
16
  import { loadMojangPairs } from "./mapping/loaders/mojang.js";
16
17
  import { loadTinyPairsFromLoom } from "./mapping/loaders/tiny-loom.js";
17
18
  import { loadTinyPairsFromMaven } from "./mapping/loaders/tiny-maven.js";
@@ -375,7 +376,13 @@ export class MappingService {
375
376
  const candidates = rawCandidates.map(toResolutionCandidate);
376
377
  const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
377
378
  const strictDescriptor = projectedDescriptor ?? descriptor;
378
- const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor === strictDescriptor);
379
+ const acceptedDescriptors = new Set([descriptor, strictDescriptor]);
380
+ const toObfuscatedPath = namespacePath(graph, sourceMapping, "obfuscated");
381
+ if (toObfuscatedPath) {
382
+ const obfuscatedProjection = this.projectMethodDescriptorToTarget(graph, toObfuscatedPath, descriptor);
383
+ acceptedDescriptors.add(obfuscatedProjection.descriptor);
384
+ }
385
+ const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor !== undefined && acceptedDescriptors.has(candidate.descriptor));
379
386
  if (strictCandidates.length === 1) {
380
387
  const resolved = toResolutionCandidate(strictCandidates[0]);
381
388
  return {
@@ -510,18 +517,18 @@ export class MappingService {
510
517
  warnings
511
518
  };
512
519
  }
513
- const baseRecords = collectTargetRecords(graph, baseMapping).filter((record) => {
514
- if (record.kind === "class") {
515
- return includeKinds.has("class") && record.symbol === baseClass.symbol;
516
- }
517
- if (record.owner !== baseClass.symbol) {
518
- return false;
520
+ const exactIndex = collectExactRecordIndex(graph, baseMapping);
521
+ const baseRecords = [];
522
+ if (includeKinds.has("class")) {
523
+ for (const record of lookupExactClassBySymbol(exactIndex, baseClass.symbol)) {
524
+ baseRecords.push(record);
519
525
  }
520
- if (record.kind === "field") {
521
- return includeKinds.has("field");
526
+ }
527
+ for (const record of lookupExactMembersByOwner(exactIndex, baseClass.symbol)) {
528
+ if (record.kind === "field" ? includeKinds.has("field") : includeKinds.has("method")) {
529
+ baseRecords.push(record);
522
530
  }
523
- return includeKinds.has("method");
524
- });
531
+ }
525
532
  const rowKindOrder = {
526
533
  class: 0,
527
534
  field: 1,
@@ -564,7 +571,8 @@ export class MappingService {
564
571
  className,
565
572
  classNameMapping,
566
573
  (input.includeKinds ?? []).join(","),
567
- input.sourcePriority
574
+ input.sourcePriority,
575
+ resolveGradleUserHomePath(input.gradleUserHome)
568
576
  ]);
569
577
  const { offset: rowOffset, cursorIgnored: rowCursorIgnored } = resolveCursorOffset(input.cursor, rowCursorContext);
570
578
  const windowBase = rowLimit != null ? dedupedBase.slice(rowOffset, rowOffset + rowLimit) : dedupedBase.slice(rowOffset);
@@ -709,7 +717,9 @@ export class MappingService {
709
717
  };
710
718
  })()
711
719
  : (() => {
712
- const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, effectiveSignatureMode);
720
+ const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, effectiveSignatureMode, {
721
+ allowShortClassName: input.kind === "class" && sourceMapping === "obfuscated"
722
+ });
713
723
  return {
714
724
  mode: "strict",
715
725
  queryRecord,
@@ -730,6 +740,7 @@ export class MappingService {
730
740
  warnings
731
741
  };
732
742
  }
743
+ const exactIndex = collectExactRecordIndex(graph, sourceMapping);
733
744
  const buildOutput = (querySymbol, matched, status) => {
734
745
  const candidates = matched.map((record) => toResolutionCandidate(toLookupCandidate(record)));
735
746
  const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
@@ -751,11 +762,11 @@ export class MappingService {
751
762
  if (normalizedQuery.mode === "auto-class") {
752
763
  const autoClassName = normalizedQuery.className;
753
764
  if (autoClassName.includes(".")) {
754
- const matched = records.filter((record) => record.kind === "class" && record.symbol === autoClassName);
765
+ const matched = lookupExactClassBySymbol(exactIndex, autoClassName);
755
766
  const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
756
767
  return buildOutput(normalizedQuery.querySymbol, matched, status);
757
768
  }
758
- const matched = records.filter((record) => record.kind === "class" && record.name === autoClassName);
769
+ const matched = lookupExactClassBySimpleName(exactIndex, autoClassName);
759
770
  const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
760
771
  if (status === "ambiguous") {
761
772
  warnings.push(`Multiple class symbols matched short name "${autoClassName}". Provide fully-qualified class name.`);
@@ -764,16 +775,16 @@ export class MappingService {
764
775
  }
765
776
  const { queryRecord, querySymbol } = normalizedQuery;
766
777
  if (queryRecord.kind === "class") {
767
- const matched = records.filter((record) => record.kind === "class" && record.symbol === queryRecord.symbol);
778
+ const matched = lookupExactClassBySymbol(exactIndex, queryRecord.symbol);
768
779
  const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
769
780
  return buildOutput(querySymbol, matched, status);
770
781
  }
771
782
  if (queryRecord.kind === "field") {
772
- const matched = records.filter((record) => record.kind === "field" && record.owner === queryRecord.owner && record.name === queryRecord.name);
783
+ const matched = lookupExactMembers(exactIndex, "field", queryRecord.owner ?? "", queryRecord.name);
773
784
  const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
774
785
  return buildOutput(querySymbol, matched, status);
775
786
  }
776
- const methodCandidates = records.filter((record) => record.kind === "method" && record.owner === queryRecord.owner && record.name === queryRecord.name);
787
+ const methodCandidates = lookupExactMembers(exactIndex, "method", queryRecord.owner ?? "", queryRecord.name);
777
788
  // name-only mode: skip descriptor matching, resolve by owner+name
778
789
  if (effectiveSignatureMode === "name-only") {
779
790
  const status = methodCandidates.length === 1 ? "resolved" : methodCandidates.length > 1 ? "ambiguous" : "not_found";
@@ -1102,6 +1113,7 @@ export class MappingService {
1102
1113
  adjacency: new Map(),
1103
1114
  pathCache: new Map(),
1104
1115
  recordsByTarget: new Map(),
1116
+ exactRecordIndex: new Map(),
1105
1117
  classProjectionCache: new Map(),
1106
1118
  warnings: [
1107
1119
  `Version ${version} is unobfuscated; mapping graph is empty because the runtime already uses deobfuscated names.`
@@ -1116,6 +1128,7 @@ export class MappingService {
1116
1128
  adjacency: new Map(),
1117
1129
  pathCache: new Map(),
1118
1130
  recordsByTarget: new Map(),
1131
+ exactRecordIndex: new Map(),
1119
1132
  classProjectionCache: new Map(),
1120
1133
  warnings: []
1121
1134
  };
@@ -1282,7 +1295,9 @@ async function extractTinyFromJar(jarPath, outputPath) {
1282
1295
  return false;
1283
1296
  }
1284
1297
  await mkdir(dirname(outputPath), { recursive: true });
1285
- await writeFile(outputPath, tinyEntry.content, "utf8");
1298
+ const tempPath = `${outputPath}.${randomBytes(4).toString("hex")}.tmp`;
1299
+ await writeFile(tempPath, tinyEntry.content, "utf8");
1300
+ await rename(tempPath, outputPath);
1286
1301
  return true;
1287
1302
  }
1288
1303
  /**
@@ -6,8 +6,26 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
6
6
  continue;
7
7
  const methodNames = allMethodNames(members);
8
8
  // Strip owner prefix and JVM descriptor from the method reference
9
- const methodName = extractMethodName(inj.method);
10
- if (!methodNames.includes(methodName)) {
9
+ const rawMethodName = extractMethodName(inj.method);
10
+ // Strip a trailing target-selector quantifier (e.g. "tick*", "tick+", "tick{2,3}")
11
+ const quantifier = /(?:\*|\+|\{\d+(?:,\d+)?\})$/.exec(rawMethodName)?.[0];
12
+ const methodName = quantifier
13
+ ? rawMethodName.slice(0, rawMethodName.length - quantifier.length)
14
+ : rawMethodName;
15
+ if (quantifier && methodName === "") {
16
+ // Bare wildcard selector (e.g. method = "*") matches everything; unverifiable.
17
+ resolvedMembers.push({
18
+ annotation: `@${inj.annotation}`,
19
+ name: rawMethodName,
20
+ line: inj.line,
21
+ status: "skipped"
22
+ });
23
+ continue;
24
+ }
25
+ const matched = (quantifier === "*" || quantifier === "+")
26
+ ? methodNames.some((n) => n.startsWith(methodName))
27
+ : methodNames.includes(methodName);
28
+ if (!matched) {
11
29
  const suggestions = suggestSimilar(methodName, methodNames);
12
30
  const descriptor = extractMethodDescriptor(inj.method);
13
31
  const descriptorHint = descriptor ? ` (descriptor: ${descriptor})` : "";
@@ -46,7 +64,7 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
46
64
  // Name matched. If the reference carries a JVM descriptor, require an
47
65
  // overload to match it; otherwise a wrong-signature target (e.g. tick(D)V
48
66
  // when only tick(I)V exists) is silently reported as resolved.
49
- const refDescriptor = extractMethodDescriptor(inj.method);
67
+ const refDescriptor = quantifier ? undefined : extractMethodDescriptor(inj.method);
50
68
  const sameNameMembers = [...members.constructors, ...members.methods].filter((member) => member.name === methodName);
51
69
  if (refDescriptor &&
52
70
  sameNameMembers.length > 0 &&
@@ -27,6 +27,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
27
27
  resolutionPath: "target-mapping-failed",
28
28
  falsePositiveRisk: healthReport?.overallHealthy === false ? "high" : "medium"
29
29
  });
30
+ addSkippedMembers(parsed, resolvedMembers);
30
31
  }
31
32
  else if (symbolExistsButSignatureFailed?.has(target.className)) {
32
33
  // Symbol exists in mapping graph but getSignature failed — tool limitation, not code issue