@adhisang/minecraft-modding-mcp 1.2.1 → 2.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 (51) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +184 -64
  3. package/dist/cli.js +31 -4
  4. package/dist/compat-stdio-transport.d.ts +2 -7
  5. package/dist/compat-stdio-transport.js +12 -154
  6. package/dist/index.js +537 -202
  7. package/dist/json-rpc-framing.d.ts +22 -0
  8. package/dist/json-rpc-framing.js +168 -0
  9. package/dist/mapping-pipeline-service.d.ts +1 -1
  10. package/dist/mapping-pipeline-service.js +13 -5
  11. package/dist/mapping-service.d.ts +12 -4
  12. package/dist/mapping-service.js +222 -105
  13. package/dist/mcp-helpers.d.ts +10 -2
  14. package/dist/mcp-helpers.js +59 -5
  15. package/dist/minecraft-explorer-service.d.ts +1 -2
  16. package/dist/minecraft-explorer-service.js +120 -24
  17. package/dist/mixin-validator.d.ts +24 -2
  18. package/dist/mixin-validator.js +228 -103
  19. package/dist/mod-decompile-service.d.ts +5 -0
  20. package/dist/mod-decompile-service.js +40 -5
  21. package/dist/mod-remap-service.js +142 -30
  22. package/dist/mojang-tiny-mapping-service.js +26 -26
  23. package/dist/path-resolver.js +41 -4
  24. package/dist/registry-service.d.ts +10 -1
  25. package/dist/registry-service.js +154 -22
  26. package/dist/resources.js +7 -7
  27. package/dist/search-hit-accumulator.d.ts +0 -3
  28. package/dist/search-hit-accumulator.js +27 -6
  29. package/dist/source-jar-reader.js +16 -2
  30. package/dist/source-resolver.d.ts +1 -0
  31. package/dist/source-resolver.js +93 -2
  32. package/dist/source-service.d.ts +76 -47
  33. package/dist/source-service.js +1344 -763
  34. package/dist/stdio-supervisor.d.ts +46 -0
  35. package/dist/stdio-supervisor.js +349 -0
  36. package/dist/storage/files-repo.d.ts +3 -0
  37. package/dist/storage/files-repo.js +66 -1
  38. package/dist/storage/migrations.d.ts +1 -1
  39. package/dist/storage/migrations.js +6 -2
  40. package/dist/storage/schema.d.ts +1 -0
  41. package/dist/storage/schema.js +7 -0
  42. package/dist/symbols/symbol-extractor.js +6 -4
  43. package/dist/tool-execution-gate.d.ts +15 -0
  44. package/dist/tool-execution-gate.js +58 -0
  45. package/dist/tool-input.d.ts +6 -0
  46. package/dist/tool-input.js +64 -0
  47. package/dist/types.d.ts +1 -1
  48. package/dist/version-diff-service.js +10 -5
  49. package/dist/version-service.js +7 -2
  50. package/dist/workspace-mapping-service.js +12 -0
  51. package/package.json +4 -1
@@ -7,7 +7,7 @@ import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
7
7
  import { listJarEntries, readJarEntryAsUtf8 } from "./source-jar-reader.js";
8
8
  import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
9
9
  const SUPPORTED_MAPPINGS = new Set([
10
- "official",
10
+ "obfuscated",
11
11
  "mojang",
12
12
  "intermediary",
13
13
  "yarn"
@@ -37,13 +37,21 @@ function addToSetMap(map, key, value) {
37
37
  map.set(normalizedKey, existing);
38
38
  }
39
39
  function normalizedVariants(symbol) {
40
- const variants = new Set();
41
- variants.add(symbol);
42
- const dotted = symbol.replace(/\//g, ".");
43
- variants.add(dotted);
44
- const slashed = symbol.replace(/\./g, "/");
45
- variants.add(slashed);
46
- return [...variants];
40
+ const variants = [symbol];
41
+ let dotted;
42
+ if (symbol.includes("/")) {
43
+ dotted = symbol.replace(/\//g, ".");
44
+ if (dotted !== symbol) {
45
+ variants.push(dotted);
46
+ }
47
+ }
48
+ if (symbol.includes(".")) {
49
+ const slashed = symbol.replace(/\./g, "/");
50
+ if (slashed !== symbol && slashed !== dotted) {
51
+ variants.push(slashed);
52
+ }
53
+ }
54
+ return variants;
47
55
  }
48
56
  function simpleName(symbol) {
49
57
  const trimmed = symbol.trim();
@@ -230,12 +238,42 @@ function pairKey(sourceMapping, targetMapping) {
230
238
  return `${sourceMapping}->${targetMapping}`;
231
239
  }
232
240
  function parsePairKey(key) {
233
- const [source, target] = key.split("->");
241
+ const separator = key.indexOf("->");
242
+ const source = separator >= 0 ? key.slice(0, separator) : key;
243
+ const target = separator >= 0 ? key.slice(separator + 2) : "";
234
244
  return {
235
245
  sourceMapping: source,
236
246
  targetMapping: target
237
247
  };
238
248
  }
249
+ function buildAdjacency(pairs) {
250
+ const adjacency = new Map();
251
+ for (const key of pairs.keys()) {
252
+ const { sourceMapping, targetMapping } = parsePairKey(key);
253
+ let neighbors = adjacency.get(sourceMapping);
254
+ if (!neighbors) {
255
+ neighbors = new Set();
256
+ adjacency.set(sourceMapping, neighbors);
257
+ }
258
+ neighbors.add(targetMapping);
259
+ }
260
+ return new Map([...adjacency.entries()].map(([mapping, neighbors]) => [mapping, [...neighbors]]));
261
+ }
262
+ function buildTargetRecordIndex(pairs) {
263
+ const recordsByTarget = new Map();
264
+ for (const [key, pair] of pairs.entries()) {
265
+ const { targetMapping } = parsePairKey(key);
266
+ let bucket = recordsByTarget.get(targetMapping);
267
+ if (!bucket) {
268
+ bucket = new Map();
269
+ recordsByTarget.set(targetMapping, bucket);
270
+ }
271
+ for (const record of pair.index.records.values()) {
272
+ bucket.set(buildSymbolKey(record), record);
273
+ }
274
+ }
275
+ return new Map([...recordsByTarget.entries()].map(([mapping, records]) => [mapping, [...records.values()]]));
276
+ }
239
277
  function ensurePairIndex(indexes, from, to) {
240
278
  const key = pairKey(from, to);
241
279
  const existing = indexes.get(key);
@@ -254,7 +292,7 @@ const PROGUARD_PRIMITIVES = {
254
292
  /**
255
293
  * Convert a single proguard type (e.g. "int", "net.minecraft.Foo", "int[][]")
256
294
  * to JVM notation (e.g. "I", "Lnet/minecraft/Foo;", "[[I").
257
- * `classLookup` maps mojang class names → official class names (for the official descriptor).
295
+ * `classLookup` maps mojang class names → obfuscated class names (for the obfuscated descriptor).
258
296
  * Pass `undefined` to skip class name translation (for mojang descriptors).
259
297
  */
260
298
  function proguardTypeToJvm(type, classLookup) {
@@ -294,12 +332,12 @@ function parseProguardMethod(value, classLookup) {
294
332
  return { name, descriptor: `(${paramDescriptor})${returnDescriptor}` };
295
333
  }
296
334
  function parseClientMappings(text) {
297
- const officialToMojang = createDirectionIndex();
298
- const mojangToOfficial = createDirectionIndex();
335
+ const obfuscatedToMojang = createDirectionIndex();
336
+ const mojangToObfuscated = createDirectionIndex();
299
337
  // Two-pass parsing: first collect class name mappings, then parse members with descriptors.
300
338
  const lines = text.split(/\r?\n/);
301
- // Pass 1: collect class name mappings (mojang → official)
302
- const mojangToOfficialClass = new Map();
339
+ // Pass 1: collect class name mappings (mojang → obfuscated)
340
+ const mojangToObfuscatedClass = new Map();
303
341
  let classCount = 0;
304
342
  for (const rawLine of lines) {
305
343
  const line = rawLine.trim();
@@ -309,9 +347,9 @@ function parseClientMappings(text) {
309
347
  const classMatch = /^(.+?)\s+->\s+(.+):$/.exec(line);
310
348
  if (classMatch) {
311
349
  const mojangClass = classMatch[1]?.trim() ?? "";
312
- const officialClass = classMatch[2]?.trim() ?? "";
313
- if (mojangClass && officialClass) {
314
- mojangToOfficialClass.set(mojangClass, officialClass);
350
+ const obfuscatedClass = classMatch[2]?.trim() ?? "";
351
+ if (mojangClass && obfuscatedClass) {
352
+ mojangToObfuscatedClass.set(mojangClass, obfuscatedClass);
315
353
  classCount += 1;
316
354
  }
317
355
  }
@@ -332,17 +370,17 @@ function parseClientMappings(text) {
332
370
  const classMatch = /^(.+?)\s+->\s+(.+):$/.exec(line);
333
371
  if (classMatch) {
334
372
  const mojangClass = classMatch[1]?.trim() ?? "";
335
- const officialClass = classMatch[2]?.trim() ?? "";
336
- if (!mojangClass || !officialClass) {
373
+ const obfuscatedClass = classMatch[2]?.trim() ?? "";
374
+ if (!mojangClass || !obfuscatedClass) {
337
375
  currentClass = undefined;
338
376
  continue;
339
377
  }
340
378
  currentClass = {
341
- official: officialClass,
379
+ obfuscated: obfuscatedClass,
342
380
  mojang: mojangClass
343
381
  };
344
- addLookupEntries(officialToMojang, createClassSymbolRecord(officialClass), createClassSymbolRecord(mojangClass));
345
- addLookupEntries(mojangToOfficial, createClassSymbolRecord(mojangClass), createClassSymbolRecord(officialClass));
382
+ addLookupEntries(obfuscatedToMojang, createClassSymbolRecord(obfuscatedClass), createClassSymbolRecord(mojangClass));
383
+ addLookupEntries(mojangToObfuscated, createClassSymbolRecord(mojangClass), createClassSymbolRecord(obfuscatedClass));
346
384
  continue;
347
385
  }
348
386
  if (!currentClass) {
@@ -359,31 +397,31 @@ function parseClientMappings(text) {
359
397
  }
360
398
  const mojangMemberSignature = stripLineInfo(leftRaw);
361
399
  // Try method parsing with JVM descriptor
362
- const officialMethod = parseProguardMethod(mojangMemberSignature, mojangToOfficialClass);
363
- if (officialMethod) {
400
+ const obfuscatedMethod = parseProguardMethod(mojangMemberSignature, mojangToObfuscatedClass);
401
+ if (obfuscatedMethod) {
364
402
  const mojangMethod = parseProguardMethod(mojangMemberSignature, undefined);
365
- const officialDescriptor = officialMethod.descriptor;
403
+ const obfuscatedDescriptor = obfuscatedMethod.descriptor;
366
404
  const mojangDescriptor = mojangMethod?.descriptor;
367
- addLookupEntries(officialToMojang, createMethodSymbolRecord(currentClass.official, rightRaw, officialDescriptor), createMethodSymbolRecord(currentClass.mojang, officialMethod.name, mojangDescriptor));
368
- addLookupEntries(mojangToOfficial, createMethodSymbolRecord(currentClass.mojang, officialMethod.name, mojangDescriptor), createMethodSymbolRecord(currentClass.official, rightRaw, officialDescriptor));
405
+ addLookupEntries(obfuscatedToMojang, createMethodSymbolRecord(currentClass.obfuscated, rightRaw, obfuscatedDescriptor), createMethodSymbolRecord(currentClass.mojang, obfuscatedMethod.name, mojangDescriptor));
406
+ addLookupEntries(mojangToObfuscated, createMethodSymbolRecord(currentClass.mojang, obfuscatedMethod.name, mojangDescriptor), createMethodSymbolRecord(currentClass.obfuscated, rightRaw, obfuscatedDescriptor));
369
407
  continue;
370
408
  }
371
409
  const fieldName = parseFieldName(mojangMemberSignature);
372
410
  if (!fieldName) {
373
411
  continue;
374
412
  }
375
- addLookupEntries(officialToMojang, createFieldSymbolRecord(currentClass.official, rightRaw), createFieldSymbolRecord(currentClass.mojang, fieldName));
376
- addLookupEntries(mojangToOfficial, createFieldSymbolRecord(currentClass.mojang, fieldName), createFieldSymbolRecord(currentClass.official, rightRaw));
413
+ addLookupEntries(obfuscatedToMojang, createFieldSymbolRecord(currentClass.obfuscated, rightRaw), createFieldSymbolRecord(currentClass.mojang, fieldName));
414
+ addLookupEntries(mojangToObfuscated, createFieldSymbolRecord(currentClass.mojang, fieldName), createFieldSymbolRecord(currentClass.obfuscated, rightRaw));
377
415
  }
378
416
  const result = new Map();
379
- result.set(pairKey("official", "mojang"), officialToMojang);
380
- result.set(pairKey("mojang", "official"), mojangToOfficial);
417
+ result.set(pairKey("obfuscated", "mojang"), obfuscatedToMojang);
418
+ result.set(pairKey("mojang", "obfuscated"), mojangToObfuscated);
381
419
  return result;
382
420
  }
383
421
  function normalizeTinyNamespace(namespace) {
384
422
  const normalized = namespace.trim().toLowerCase();
385
- if (normalized === "official") {
386
- return "official";
423
+ if (normalized === "obfuscated") {
424
+ return "obfuscated";
387
425
  }
388
426
  if (normalized === "mojang") {
389
427
  return "mojang";
@@ -552,38 +590,48 @@ function mappingSourceOrder(priority) {
552
590
  }
553
591
  return ["loom-cache", "maven"];
554
592
  }
555
- function namespacePath(pairs, sourceMapping, targetMapping) {
593
+ function namespacePath(graph, sourceMapping, targetMapping) {
556
594
  if (sourceMapping === targetMapping) {
557
595
  return [sourceMapping];
558
596
  }
597
+ const key = pairKey(sourceMapping, targetMapping);
598
+ if (graph.pathCache.has(key)) {
599
+ return graph.pathCache.get(key);
600
+ }
601
+ const { adjacency } = graph;
559
602
  const queue = [sourceMapping];
603
+ let queueIndex = 0;
560
604
  const parent = new Map([[sourceMapping, undefined]]);
561
- while (queue.length > 0) {
562
- const current = queue.shift();
605
+ while (queueIndex < queue.length) {
606
+ const current = queue[queueIndex];
607
+ queueIndex += 1;
563
608
  if (current === targetMapping) {
564
609
  break;
565
610
  }
566
- for (const key of pairs.keys()) {
567
- const parsed = parsePairKey(key);
568
- if (parsed.sourceMapping !== current) {
569
- continue;
570
- }
571
- if (parent.has(parsed.targetMapping)) {
611
+ const neighbors = adjacency.get(current);
612
+ if (!neighbors) {
613
+ continue;
614
+ }
615
+ for (const neighbor of neighbors) {
616
+ if (parent.has(neighbor)) {
572
617
  continue;
573
618
  }
574
- parent.set(parsed.targetMapping, current);
575
- queue.push(parsed.targetMapping);
619
+ parent.set(neighbor, current);
620
+ queue.push(neighbor);
576
621
  }
577
622
  }
578
623
  if (!parent.has(targetMapping)) {
624
+ graph.pathCache.set(key, undefined);
579
625
  return undefined;
580
626
  }
581
- const path = [];
627
+ const reversedPath = [];
582
628
  let cursor = targetMapping;
583
629
  while (cursor) {
584
- path.unshift(cursor);
630
+ reversedPath.push(cursor);
585
631
  cursor = parent.get(cursor);
586
632
  }
633
+ const path = reversedPath.reverse();
634
+ graph.pathCache.set(key, path);
587
635
  return path;
588
636
  }
589
637
  function pathUsesSource(pairs, path, source) {
@@ -765,17 +813,7 @@ function applyDisambiguationHints(candidates, disambiguation) {
765
813
  return filtered;
766
814
  }
767
815
  function collectTargetRecords(graph, targetMapping) {
768
- const merged = new Map();
769
- for (const [key, pair] of graph.pairs.entries()) {
770
- const parsed = parsePairKey(key);
771
- if (parsed.targetMapping !== targetMapping) {
772
- continue;
773
- }
774
- for (const record of pair.index.records.values()) {
775
- merged.set(buildSymbolKey(record), record);
776
- }
777
- }
778
- return [...merged.values()];
816
+ return [...(graph.recordsByTarget.get(targetMapping) ?? [])];
779
817
  }
780
818
  function normalizeIncludedKinds(inputKinds) {
781
819
  const normalized = new Set();
@@ -823,6 +861,28 @@ function inferAmbiguityReasons(candidates, usedMojangClientMappings) {
823
861
  }
824
862
  return reasons;
825
863
  }
864
+ function clampCandidateLimit(limit) {
865
+ if (!Number.isFinite(limit) || limit == null) {
866
+ return MAX_CANDIDATES;
867
+ }
868
+ return Math.max(1, Math.min(MAX_CANDIDATES, Math.trunc(limit)));
869
+ }
870
+ function limitResolutionCandidates(candidates, requestedLimit) {
871
+ const candidateCount = candidates.length;
872
+ const limit = clampCandidateLimit(requestedLimit);
873
+ const limitedCandidates = candidateCount > limit ? candidates.slice(0, limit) : candidates;
874
+ return {
875
+ candidates: limitedCandidates,
876
+ candidateCount,
877
+ ...(limitedCandidates.length < candidateCount ? { candidatesTruncated: true } : {})
878
+ };
879
+ }
880
+ function clampRowLimit(limit) {
881
+ if (!Number.isFinite(limit) || limit == null) {
882
+ return undefined;
883
+ }
884
+ return Math.max(1, Math.min(5000, Math.trunc(limit)));
885
+ }
826
886
  export class MappingService {
827
887
  config;
828
888
  versionService;
@@ -845,7 +905,7 @@ export class MappingService {
845
905
  }
846
906
  });
847
907
  }
848
- const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input);
908
+ const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, input.signatureMode);
849
909
  const sourceMapping = input.sourceMapping;
850
910
  const targetMapping = input.targetMapping;
851
911
  if (!SUPPORTED_MAPPINGS.has(sourceMapping) || !SUPPORTED_MAPPINGS.has(targetMapping)) {
@@ -872,18 +932,21 @@ export class MappingService {
872
932
  matchKind: "exact",
873
933
  confidence: 1
874
934
  });
935
+ const limited = limitResolutionCandidates([identity], input.maxCandidates);
875
936
  return {
876
937
  querySymbol,
877
938
  mappingContext,
878
939
  resolved: true,
879
940
  status: "resolved",
880
941
  resolvedSymbol: querySymbol,
881
- candidates: [identity],
942
+ candidates: limited.candidates,
943
+ candidateCount: limited.candidateCount,
944
+ candidatesTruncated: limited.candidatesTruncated,
882
945
  warnings: []
883
946
  };
884
947
  }
885
948
  const graph = await this.loadGraph(version, priority);
886
- const path = namespacePath(graph.pairs, sourceMapping, targetMapping);
949
+ const path = namespacePath(graph, sourceMapping, targetMapping);
887
950
  if (!path) {
888
951
  return {
889
952
  querySymbol,
@@ -891,6 +954,7 @@ export class MappingService {
891
954
  resolved: false,
892
955
  status: "mapping_unavailable",
893
956
  candidates: [],
957
+ candidateCount: 0,
894
958
  warnings: [
895
959
  `No mapping path is available for ${sourceMapping} -> ${targetMapping} on version "${version}".`
896
960
  ]
@@ -903,6 +967,7 @@ export class MappingService {
903
967
  warnings.push(`Disambiguation hints narrowed candidates from ${rawCandidates.length} to ${disambiguatedCandidates.length}.`);
904
968
  }
905
969
  const candidates = disambiguatedCandidates.map(toResolutionCandidate);
970
+ const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
906
971
  if (queryRecord.kind === "method" &&
907
972
  queryRecord.descriptor &&
908
973
  pathUsesSource(graph.pairs, path, "mojang-client-mappings") &&
@@ -925,7 +990,9 @@ export class MappingService {
925
990
  resolved: status === "resolved",
926
991
  status,
927
992
  resolvedSymbol: status === "resolved" ? candidates[0] : undefined,
928
- candidates,
993
+ candidates: limitedCandidates.candidates,
994
+ candidateCount: limitedCandidates.candidateCount,
995
+ candidatesTruncated: limitedCandidates.candidatesTruncated,
929
996
  warnings,
930
997
  provenance: this.provenanceForPath(graph, path),
931
998
  ambiguityReasons
@@ -963,7 +1030,7 @@ export class MappingService {
963
1030
  };
964
1031
  }
965
1032
  const graph = await this.loadGraph(version, priority);
966
- const path = namespacePath(graph.pairs, sourceMapping, targetMapping);
1033
+ const path = namespacePath(graph, sourceMapping, targetMapping);
967
1034
  if (!path) {
968
1035
  throw createError({
969
1036
  code: ERROR_CODES.MAPPING_UNAVAILABLE,
@@ -973,8 +1040,8 @@ export class MappingService {
973
1040
  sourceMapping,
974
1041
  targetMapping,
975
1042
  sourcePriority: priority,
976
- nextAction: "Try mapping=official which is always available.",
977
- suggestedCall: { tool: "resolve-artifact", params: { mapping: "official" } }
1043
+ nextAction: "Try mapping=obfuscated which is always available.",
1044
+ suggestedCall: { tool: "resolve-artifact", params: { mapping: "obfuscated" } }
978
1045
  }
979
1046
  });
980
1047
  }
@@ -1000,16 +1067,10 @@ export class MappingService {
1000
1067
  }
1001
1068
  });
1002
1069
  }
1003
- if (input.kind !== "method") {
1004
- throw createError({
1005
- code: ERROR_CODES.INVALID_INPUT,
1006
- message: 'resolveMethodMappingExact requires kind="method".',
1007
- details: {
1008
- kind: input.kind
1009
- }
1010
- });
1011
- }
1012
- const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input);
1070
+ const { record: queryRecord, querySymbol } = normalizeQuerySymbol({
1071
+ ...input,
1072
+ kind: "method"
1073
+ });
1013
1074
  const owner = queryRecord.owner;
1014
1075
  const method = queryRecord.name;
1015
1076
  const descriptor = queryRecord.descriptor;
@@ -1039,18 +1100,21 @@ export class MappingService {
1039
1100
  matchKind: "exact",
1040
1101
  confidence: 1
1041
1102
  });
1103
+ const limited = limitResolutionCandidates([resolvedCandidate], input.maxCandidates);
1042
1104
  return {
1043
1105
  querySymbol,
1044
1106
  mappingContext,
1045
1107
  resolved: true,
1046
1108
  status: "resolved",
1047
1109
  resolvedSymbol: resolvedCandidate,
1048
- candidates: [resolvedCandidate],
1110
+ candidates: limited.candidates,
1111
+ candidateCount: limited.candidateCount,
1112
+ candidatesTruncated: limited.candidatesTruncated,
1049
1113
  warnings: []
1050
1114
  };
1051
1115
  }
1052
1116
  const graph = await this.loadGraph(version, priority);
1053
- const path = namespacePath(graph.pairs, sourceMapping, targetMapping);
1117
+ const path = namespacePath(graph, sourceMapping, targetMapping);
1054
1118
  if (!path) {
1055
1119
  return {
1056
1120
  querySymbol,
@@ -1058,6 +1122,7 @@ export class MappingService {
1058
1122
  resolved: false,
1059
1123
  status: "mapping_unavailable",
1060
1124
  candidates: [],
1125
+ candidateCount: 0,
1061
1126
  warnings: [
1062
1127
  `No mapping path is available for ${sourceMapping} -> ${targetMapping} on version "${version}".`
1063
1128
  ]
@@ -1068,6 +1133,7 @@ export class MappingService {
1068
1133
  .mapCandidatesAlongPath(graph, path, queryRecord)
1069
1134
  .filter((candidate) => candidate.kind === "method");
1070
1135
  const candidates = rawCandidates.map(toResolutionCandidate);
1136
+ const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
1071
1137
  const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor === descriptor);
1072
1138
  if (strictCandidates.length === 1) {
1073
1139
  const resolved = toResolutionCandidate(strictCandidates[0]);
@@ -1077,7 +1143,9 @@ export class MappingService {
1077
1143
  resolved: true,
1078
1144
  status: "resolved",
1079
1145
  resolvedSymbol: resolved,
1080
- candidates,
1146
+ candidates: limitedCandidates.candidates,
1147
+ candidateCount: limitedCandidates.candidateCount,
1148
+ candidatesTruncated: limitedCandidates.candidatesTruncated,
1081
1149
  warnings,
1082
1150
  provenance: this.provenanceForPath(graph, path)
1083
1151
  };
@@ -1089,7 +1157,9 @@ export class MappingService {
1089
1157
  mappingContext,
1090
1158
  resolved: false,
1091
1159
  status: "ambiguous",
1092
- candidates,
1160
+ candidates: limitedCandidates.candidates,
1161
+ candidateCount: limitedCandidates.candidateCount,
1162
+ candidatesTruncated: limitedCandidates.candidatesTruncated,
1093
1163
  warnings,
1094
1164
  provenance: this.provenanceForPath(graph, path)
1095
1165
  };
@@ -1101,7 +1171,9 @@ export class MappingService {
1101
1171
  mappingContext,
1102
1172
  resolved: false,
1103
1173
  status: "mapping_unavailable",
1104
- candidates,
1174
+ candidates: limitedCandidates.candidates,
1175
+ candidateCount: limitedCandidates.candidateCount,
1176
+ candidatesTruncated: limitedCandidates.candidatesTruncated,
1105
1177
  warnings,
1106
1178
  provenance: this.provenanceForPath(graph, path)
1107
1179
  };
@@ -1111,7 +1183,9 @@ export class MappingService {
1111
1183
  mappingContext,
1112
1184
  resolved: false,
1113
1185
  status: "not_found",
1114
- candidates,
1186
+ candidates: limitedCandidates.candidates,
1187
+ candidateCount: limitedCandidates.candidateCount,
1188
+ candidatesTruncated: limitedCandidates.candidatesTruncated,
1115
1189
  warnings,
1116
1190
  provenance: this.provenanceForPath(graph, path)
1117
1191
  };
@@ -1143,6 +1217,19 @@ export class MappingService {
1143
1217
  const graph = await this.loadGraph(version, priority);
1144
1218
  const warnings = [...graph.warnings];
1145
1219
  const includeKinds = normalizeIncludedKinds(input.includeKinds);
1220
+ const pathCache = new Map();
1221
+ const resolvePath = (sourceMapping, targetMapping) => {
1222
+ if (sourceMapping === targetMapping) {
1223
+ return [sourceMapping];
1224
+ }
1225
+ const key = pairKey(sourceMapping, targetMapping);
1226
+ if (pathCache.has(key)) {
1227
+ return pathCache.get(key);
1228
+ }
1229
+ const path = namespacePath(graph, sourceMapping, targetMapping);
1230
+ pathCache.set(key, path);
1231
+ return path;
1232
+ };
1146
1233
  const classByMapping = {
1147
1234
  [classNameMapping]: createClassSymbolRecord(className)
1148
1235
  };
@@ -1150,7 +1237,7 @@ export class MappingService {
1150
1237
  if (mapping === classNameMapping) {
1151
1238
  continue;
1152
1239
  }
1153
- const mapped = this.mapRecordBetweenMappings(graph, classNameMapping, mapping, classByMapping[classNameMapping]);
1240
+ const mapped = this.mapRecordBetweenMappings(graph, classNameMapping, mapping, classByMapping[classNameMapping], resolvePath(classNameMapping, mapping));
1154
1241
  if (mapped.length > 1) {
1155
1242
  const competing = mapped.slice(0, 5).map((c) => c.symbol);
1156
1243
  warnings.push(`Class identity mapping to ${mapping} is ambiguous for "${className}": competing=[${competing.join(", ")}].`);
@@ -1159,7 +1246,7 @@ export class MappingService {
1159
1246
  classByMapping[mapping] = mapped[0];
1160
1247
  }
1161
1248
  }
1162
- const baseMapping = classByMapping.official ? "official" : classNameMapping;
1249
+ const baseMapping = classByMapping.obfuscated ? "obfuscated" : classNameMapping;
1163
1250
  const baseClass = classByMapping[baseMapping];
1164
1251
  if (!baseClass) {
1165
1252
  return {
@@ -1167,12 +1254,13 @@ export class MappingService {
1167
1254
  className,
1168
1255
  classNameMapping,
1169
1256
  classIdentity: {
1170
- official: classByMapping.official?.symbol,
1257
+ obfuscated: classByMapping.obfuscated?.symbol,
1171
1258
  mojang: classByMapping.mojang?.symbol,
1172
1259
  intermediary: classByMapping.intermediary?.symbol,
1173
1260
  yarn: classByMapping.yarn?.symbol
1174
1261
  },
1175
1262
  rows: [],
1263
+ rowCount: 0,
1176
1264
  warnings
1177
1265
  };
1178
1266
  }
@@ -1226,7 +1314,7 @@ export class MappingService {
1226
1314
  resolved = baseRecord;
1227
1315
  }
1228
1316
  else {
1229
- const mapped = this.mapRecordBetweenMappings(graph, baseMapping, mapping, baseRecord);
1317
+ const mapped = this.mapRecordBetweenMappings(graph, baseMapping, mapping, baseRecord, resolvePath(baseMapping, mapping));
1230
1318
  let filtered = mapped;
1231
1319
  if (baseRecord.kind !== "class" && classIdentity) {
1232
1320
  filtered = filtered.filter((candidate) => candidate.owner === classIdentity.symbol);
@@ -1255,23 +1343,28 @@ export class MappingService {
1255
1343
  };
1256
1344
  row[mapping] = entry;
1257
1345
  }
1258
- row.completeness = Boolean(row.official && row.mojang && row.intermediary && row.yarn);
1346
+ row.completeness = Boolean(row.obfuscated && row.mojang && row.intermediary && row.yarn);
1259
1347
  rows.push(row);
1260
1348
  if (rowHadAmbiguity) {
1261
1349
  ambiguousRowCount += 1;
1262
1350
  }
1263
1351
  }
1352
+ const rowCount = rows.length;
1353
+ const rowLimit = clampRowLimit(input.maxRows);
1354
+ const limitedRows = rowLimit != null && rowCount > rowLimit ? rows.slice(0, rowLimit) : rows;
1264
1355
  return {
1265
1356
  version,
1266
1357
  className,
1267
1358
  classNameMapping,
1268
1359
  classIdentity: {
1269
- official: classByMapping.official?.symbol,
1360
+ obfuscated: classByMapping.obfuscated?.symbol,
1270
1361
  mojang: classByMapping.mojang?.symbol,
1271
1362
  intermediary: classByMapping.intermediary?.symbol,
1272
1363
  yarn: classByMapping.yarn?.symbol
1273
1364
  },
1274
- rows,
1365
+ rows: limitedRows,
1366
+ rowCount,
1367
+ rowsTruncated: limitedRows.length < rowCount ? true : undefined,
1275
1368
  warnings,
1276
1369
  ambiguousRowCount: ambiguousRowCount > 0 ? ambiguousRowCount : undefined
1277
1370
  };
@@ -1352,18 +1445,22 @@ export class MappingService {
1352
1445
  resolved: false,
1353
1446
  status: "mapping_unavailable",
1354
1447
  candidates: [],
1448
+ candidateCount: 0,
1355
1449
  warnings
1356
1450
  };
1357
1451
  }
1358
1452
  const buildOutput = (querySymbol, matched, status) => {
1359
1453
  const candidates = matched.map((record) => toResolutionCandidate(toLookupCandidate(record)));
1454
+ const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
1360
1455
  return {
1361
1456
  querySymbol,
1362
1457
  mappingContext,
1363
1458
  resolved: status === "resolved",
1364
1459
  status,
1365
1460
  resolvedSymbol: status === "resolved" ? candidates[0] : undefined,
1366
- candidates,
1461
+ candidates: limitedCandidates.candidates,
1462
+ candidateCount: limitedCandidates.candidateCount,
1463
+ candidatesTruncated: limitedCandidates.candidatesTruncated,
1367
1464
  warnings
1368
1465
  };
1369
1466
  };
@@ -1414,11 +1511,11 @@ export class MappingService {
1414
1511
  }
1415
1512
  return buildOutput(querySymbol, [], "not_found");
1416
1513
  }
1417
- mapRecordBetweenMappings(graph, sourceMapping, targetMapping, record) {
1514
+ mapRecordBetweenMappings(graph, sourceMapping, targetMapping, record, resolvedPath) {
1418
1515
  if (sourceMapping === targetMapping) {
1419
1516
  return [record];
1420
1517
  }
1421
- const path = namespacePath(graph.pairs, sourceMapping, targetMapping);
1518
+ const path = resolvedPath ?? namespacePath(graph, sourceMapping, targetMapping);
1422
1519
  if (!path) {
1423
1520
  return [];
1424
1521
  }
@@ -1568,16 +1665,16 @@ export class MappingService {
1568
1665
  if (!tinyAvailable) {
1569
1666
  degradations.push("No intermediary/yarn tiny mappings were found for this version.");
1570
1667
  }
1571
- // Check if member remap path exists (requestedMapping → official)
1668
+ // Check if member remap path exists (requestedMapping → obfuscated)
1572
1669
  let memberRemapAvailable = false;
1573
- if (input.requestedMapping === "official") {
1670
+ if (input.requestedMapping === "obfuscated") {
1574
1671
  memberRemapAvailable = true;
1575
1672
  }
1576
1673
  else {
1577
- const path = namespacePath(graph.pairs, input.requestedMapping, "official");
1674
+ const path = namespacePath(graph, input.requestedMapping, "obfuscated");
1578
1675
  memberRemapAvailable = path != null && path.length > 1;
1579
1676
  if (!memberRemapAvailable) {
1580
- degradations.push(`No mapping path from ${input.requestedMapping} to official; member remap will fail.`);
1677
+ degradations.push(`No mapping path from ${input.requestedMapping} to obfuscated; member remap will fail.`);
1581
1678
  }
1582
1679
  }
1583
1680
  return {
@@ -1617,8 +1714,11 @@ export class MappingService {
1617
1714
  version,
1618
1715
  priority,
1619
1716
  pairs: new Map(),
1717
+ adjacency: new Map(),
1718
+ pathCache: new Map(),
1719
+ recordsByTarget: new Map(),
1620
1720
  warnings: [
1621
- `Version ${version} is unobfuscated; mapping graph is empty (official names are final).`
1721
+ `Version ${version} is unobfuscated; mapping graph is empty because the runtime already uses deobfuscated names.`
1622
1722
  ]
1623
1723
  };
1624
1724
  }
@@ -1626,27 +1726,38 @@ export class MappingService {
1626
1726
  version,
1627
1727
  priority,
1628
1728
  pairs: new Map(),
1729
+ adjacency: new Map(),
1730
+ pathCache: new Map(),
1731
+ recordsByTarget: new Map(),
1629
1732
  warnings: []
1630
1733
  };
1631
1734
  const mojangLoad = await this.loadMojangPairs(version);
1632
1735
  graph.warnings.push(...mojangLoad.warnings);
1633
1736
  this.mergePairs(graph.pairs, mojangLoad.pairs, "mojang-client-mappings", mojangLoad.mappingArtifact);
1634
1737
  let tinyLoaded = false;
1738
+ const deferredTinyWarnings = [];
1635
1739
  for (const source of mappingSourceOrder(priority)) {
1636
1740
  const tinyLoad = source === "loom-cache"
1637
1741
  ? await this.loadTinyPairsFromLoom(version)
1638
1742
  : await this.loadTinyPairsFromMaven(version);
1639
- graph.warnings.push(...tinyLoad.warnings);
1640
1743
  if (tinyLoad.pairs.size === 0) {
1744
+ deferredTinyWarnings.push(...tinyLoad.warnings);
1641
1745
  continue;
1642
1746
  }
1643
1747
  tinyLoaded = true;
1644
1748
  this.mergePairs(graph.pairs, tinyLoad.pairs, source, tinyLoad.mappingArtifact);
1749
+ graph.warnings.push(...tinyLoad.warnings);
1750
+ if (deferredTinyWarnings.length > 0) {
1751
+ graph.warnings.push(`Used ${source === "maven" ? "Maven" : "Loom cache"} tiny mappings for "${version}" after an earlier source lookup returned no data.`);
1752
+ }
1645
1753
  break;
1646
1754
  }
1647
1755
  if (!tinyLoaded) {
1756
+ graph.warnings.push(...deferredTinyWarnings);
1648
1757
  graph.warnings.push("No intermediary/yarn tiny mappings were found for this version.");
1649
1758
  }
1759
+ graph.adjacency = buildAdjacency(graph.pairs);
1760
+ graph.recordsByTarget = buildTargetRecordIndex(graph.pairs);
1650
1761
  return graph;
1651
1762
  }
1652
1763
  mergePairs(target, source, pairSource, mappingArtifact) {
@@ -1769,31 +1880,37 @@ export class MappingService {
1769
1880
  async loadTinyPairsFromMaven(version) {
1770
1881
  const warnings = [];
1771
1882
  const merged = new Map();
1772
- const attemptedArtifacts = [];
1773
1883
  const repos = this.config.sourceRepos;
1774
1884
  const intermediaryUrls = [];
1775
1885
  const yarnUrls = [];
1776
- for (const repo of repos) {
1777
- const base = repo.replace(/\/+$/, "");
1886
+ const repoBases = repos.map((repo) => repo.replace(/\/+$/, ""));
1887
+ const yarnCoordinatesByRepo = await Promise.all(repoBases.map(async (base) => ({
1888
+ base,
1889
+ yarnCoordinates: await this.fetchYarnCoordinates(base, version)
1890
+ })));
1891
+ for (const { base, yarnCoordinates } of yarnCoordinatesByRepo) {
1778
1892
  intermediaryUrls.push(`${base}/net/fabricmc/intermediary/${version}/intermediary-${version}-v2.jar`, `${base}/net/fabricmc/intermediary/${version}/intermediary-${version}.jar`);
1779
- const yarnCoordinates = await this.fetchYarnCoordinates(base, version);
1780
1893
  for (const coordinate of yarnCoordinates) {
1781
1894
  yarnUrls.push(`${base}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}-v2.jar`, `${base}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}.jar`);
1782
1895
  }
1783
1896
  }
1784
1897
  const allUrls = [...intermediaryUrls, ...yarnUrls];
1785
- for (const url of allUrls) {
1786
- attemptedArtifacts.push(url);
1898
+ const parsedResults = await Promise.allSettled(allUrls.map(async (url) => {
1787
1899
  const downloaded = await downloadToCache(url, defaultDownloadPath(this.config.cacheDir, url), {
1788
1900
  fetchFn: this.fetchFn,
1789
1901
  retries: this.config.fetchRetries,
1790
1902
  timeoutMs: this.config.fetchTimeoutMs
1791
1903
  });
1792
1904
  if (!downloaded.ok || !downloaded.path) {
1905
+ return undefined;
1906
+ }
1907
+ return this.parseTinyFromJar(downloaded.path);
1908
+ }));
1909
+ for (const result of parsedResults) {
1910
+ if (result.status !== "fulfilled" || !result.value) {
1793
1911
  continue;
1794
1912
  }
1795
- const parsed = await this.parseTinyFromJar(downloaded.path);
1796
- for (const [key, index] of parsed.entries()) {
1913
+ for (const [key, index] of result.value.entries()) {
1797
1914
  const existing = merged.get(key);
1798
1915
  if (!existing) {
1799
1916
  merged.set(key, index);
@@ -1809,7 +1926,7 @@ export class MappingService {
1809
1926
  return {
1810
1927
  pairs: merged,
1811
1928
  warnings,
1812
- mappingArtifact: attemptedArtifacts[0] ?? "maven:none"
1929
+ mappingArtifact: allUrls[0] ?? "maven:none"
1813
1930
  };
1814
1931
  }
1815
1932
  async parseTinyFromJar(jarPath) {