@adhisang/minecraft-modding-mcp 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -246,12 +246,85 @@ function ensurePairIndex(indexes, from, to) {
246
246
  indexes.set(key, created);
247
247
  return created;
248
248
  }
249
+ /** Map of proguard primitive type names to JVM type characters. */
250
+ const PROGUARD_PRIMITIVES = {
251
+ void: "V", boolean: "Z", byte: "B", char: "C",
252
+ short: "S", int: "I", long: "J", float: "F", double: "D"
253
+ };
254
+ /**
255
+ * Convert a single proguard type (e.g. "int", "net.minecraft.Foo", "int[][]")
256
+ * to JVM notation (e.g. "I", "Lnet/minecraft/Foo;", "[[I").
257
+ * `classLookup` maps mojang class names → official class names (for the official descriptor).
258
+ * Pass `undefined` to skip class name translation (for mojang descriptors).
259
+ */
260
+ function proguardTypeToJvm(type, classLookup) {
261
+ let arrayDepth = 0;
262
+ let base = type;
263
+ while (base.endsWith("[]")) {
264
+ arrayDepth += 1;
265
+ base = base.slice(0, -2);
266
+ }
267
+ const prefix = "[".repeat(arrayDepth);
268
+ const primitive = PROGUARD_PRIMITIVES[base];
269
+ if (primitive) {
270
+ return `${prefix}${primitive}`;
271
+ }
272
+ const translated = classLookup ? (classLookup.get(base) ?? base) : base;
273
+ return `${prefix}L${translated.replace(/\./g, "/")};`;
274
+ }
275
+ /**
276
+ * Parse a proguard method signature (after stripLineInfo) into a JVM descriptor.
277
+ * Input format: "returnType methodName(paramType1,paramType2,...)"
278
+ * Returns `{ name, descriptor }` or `undefined` if parsing fails.
279
+ */
280
+ function parseProguardMethod(value, classLookup) {
281
+ const match = /^(.+?)\s+([^\s(]+)\((.*)\)$/.exec(value);
282
+ if (!match) {
283
+ return undefined;
284
+ }
285
+ const returnType = match[1].trim();
286
+ const name = match[2].trim();
287
+ const params = match[3].trim();
288
+ if (!name) {
289
+ return undefined;
290
+ }
291
+ const paramParts = params ? params.split(",").map((p) => p.trim()) : [];
292
+ const paramDescriptor = paramParts.map((p) => proguardTypeToJvm(p, classLookup)).join("");
293
+ const returnDescriptor = proguardTypeToJvm(returnType, classLookup);
294
+ return { name, descriptor: `(${paramDescriptor})${returnDescriptor}` };
295
+ }
249
296
  function parseClientMappings(text) {
250
297
  const officialToMojang = createDirectionIndex();
251
298
  const mojangToOfficial = createDirectionIndex();
299
+ // Two-pass parsing: first collect class name mappings, then parse members with descriptors.
300
+ const lines = text.split(/\r?\n/);
301
+ // Pass 1: collect class name mappings (mojang → official)
302
+ const mojangToOfficialClass = new Map();
252
303
  let classCount = 0;
304
+ for (const rawLine of lines) {
305
+ const line = rawLine.trim();
306
+ if (!line || line.startsWith("#")) {
307
+ continue;
308
+ }
309
+ const classMatch = /^(.+?)\s+->\s+(.+):$/.exec(line);
310
+ if (classMatch) {
311
+ const mojangClass = classMatch[1]?.trim() ?? "";
312
+ const officialClass = classMatch[2]?.trim() ?? "";
313
+ if (mojangClass && officialClass) {
314
+ mojangToOfficialClass.set(mojangClass, officialClass);
315
+ classCount += 1;
316
+ }
317
+ }
318
+ }
319
+ if (classCount === 0) {
320
+ throw createError({
321
+ code: ERROR_CODES.MAPPING_UNAVAILABLE,
322
+ message: "No class mappings could be parsed from client mappings."
323
+ });
324
+ }
325
+ // Pass 2: build full index with descriptors
253
326
  let currentClass;
254
- for (const rawLine of text.split(/\r?\n/)) {
327
+ for (const rawLine of lines) {
255
328
  const line = rawLine.trim();
256
329
  if (!line || line.startsWith("#")) {
257
330
  continue;
@@ -264,7 +337,6 @@ function parseClientMappings(text) {
264
337
  currentClass = undefined;
265
338
  continue;
266
339
  }
267
- classCount += 1;
268
340
  currentClass = {
269
341
  official: officialClass,
270
342
  mojang: mojangClass
@@ -286,10 +358,14 @@ function parseClientMappings(text) {
286
358
  continue;
287
359
  }
288
360
  const mojangMemberSignature = stripLineInfo(leftRaw);
289
- const methodName = parseMethodName(mojangMemberSignature);
290
- if (methodName) {
291
- addLookupEntries(officialToMojang, createMethodSymbolRecord(currentClass.official, rightRaw, undefined), createMethodSymbolRecord(currentClass.mojang, methodName, undefined));
292
- addLookupEntries(mojangToOfficial, createMethodSymbolRecord(currentClass.mojang, methodName, undefined), createMethodSymbolRecord(currentClass.official, rightRaw, undefined));
361
+ // Try method parsing with JVM descriptor
362
+ const officialMethod = parseProguardMethod(mojangMemberSignature, mojangToOfficialClass);
363
+ if (officialMethod) {
364
+ const mojangMethod = parseProguardMethod(mojangMemberSignature, undefined);
365
+ const officialDescriptor = officialMethod.descriptor;
366
+ 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));
293
369
  continue;
294
370
  }
295
371
  const fieldName = parseFieldName(mojangMemberSignature);
@@ -299,12 +375,6 @@ function parseClientMappings(text) {
299
375
  addLookupEntries(officialToMojang, createFieldSymbolRecord(currentClass.official, rightRaw), createFieldSymbolRecord(currentClass.mojang, fieldName));
300
376
  addLookupEntries(mojangToOfficial, createFieldSymbolRecord(currentClass.mojang, fieldName), createFieldSymbolRecord(currentClass.official, rightRaw));
301
377
  }
302
- if (classCount === 0) {
303
- throw createError({
304
- code: ERROR_CODES.MAPPING_UNAVAILABLE,
305
- message: "No class mappings could be parsed from client mappings."
306
- });
307
- }
308
378
  const result = new Map();
309
379
  result.set(pairKey("official", "mojang"), officialToMojang);
310
380
  result.set(pairKey("mojang", "official"), mojangToOfficial);
@@ -591,7 +661,7 @@ function normalizeMethodDescriptor(descriptor) {
591
661
  }
592
662
  return normalized;
593
663
  }
594
- function normalizeQuerySymbol(input) {
664
+ function normalizeQuerySymbol(input, signatureMode) {
595
665
  if (input.kind !== "class" && input.kind !== "field" && input.kind !== "method") {
596
666
  throw invalidInputError('kind must be one of "class", "field", or "method".', {
597
667
  kind: input.kind
@@ -647,12 +717,53 @@ function normalizeQuerySymbol(input) {
647
717
  querySymbol: toSymbolReference(record)
648
718
  };
649
719
  }
650
- const record = createMethodSymbolRecord(owner, normalizeMemberName(normalizedName), normalizeMethodDescriptor(input.descriptor));
720
+ const descriptor = signatureMode === "name-only"
721
+ ? (input.descriptor?.trim() || "")
722
+ : normalizeMethodDescriptor(input.descriptor);
723
+ const record = createMethodSymbolRecord(owner, normalizeMemberName(normalizedName), descriptor);
651
724
  return {
652
725
  record,
653
726
  querySymbol: toSymbolReference(record)
654
727
  };
655
728
  }
729
+ function normalizeOwnerHint(ownerHint) {
730
+ const normalized = ownerHint?.trim();
731
+ if (!normalized) {
732
+ return undefined;
733
+ }
734
+ return normalizeMappedSymbolOutput(normalized);
735
+ }
736
+ function normalizeDescriptorHint(descriptorHint) {
737
+ const normalized = descriptorHint?.trim();
738
+ return normalized || undefined;
739
+ }
740
+ function applyDisambiguationHints(candidates, disambiguation) {
741
+ if (!disambiguation || candidates.length <= 1) {
742
+ return candidates;
743
+ }
744
+ let filtered = [...candidates];
745
+ const ownerHint = normalizeOwnerHint(disambiguation.ownerHint);
746
+ if (ownerHint) {
747
+ const ownerMatched = filtered.filter((candidate) => {
748
+ if (candidate.owner) {
749
+ return normalizeMappedSymbolOutput(candidate.owner) === ownerHint;
750
+ }
751
+ const normalizedSymbol = normalizeMappedSymbolOutput(candidate.symbol);
752
+ return normalizedSymbol.startsWith(`${ownerHint}.`);
753
+ });
754
+ if (ownerMatched.length > 0) {
755
+ filtered = ownerMatched;
756
+ }
757
+ }
758
+ const descriptorHint = normalizeDescriptorHint(disambiguation.descriptorHint);
759
+ if (descriptorHint) {
760
+ const descriptorMatched = filtered.filter((candidate) => candidate.descriptor === descriptorHint);
761
+ if (descriptorMatched.length > 0) {
762
+ filtered = descriptorMatched;
763
+ }
764
+ }
765
+ return filtered;
766
+ }
656
767
  function collectTargetRecords(graph, targetMapping) {
657
768
  const merged = new Map();
658
769
  for (const [key, pair] of graph.pairs.entries()) {
@@ -681,6 +792,37 @@ function normalizeIncludedKinds(inputKinds) {
681
792
  }
682
793
  return normalized;
683
794
  }
795
+ function inferAmbiguityReasons(candidates, usedMojangClientMappings) {
796
+ if (candidates.length <= 1) {
797
+ return [];
798
+ }
799
+ const reasons = [];
800
+ const owners = [...new Set(candidates.map((c) => c.owner).filter(Boolean))];
801
+ if (owners.length > 1) {
802
+ reasons.push(`Multiple owner classes matched: ${owners.join(", ")}`);
803
+ }
804
+ const matchKinds = [...new Set(candidates.map((c) => c.matchKind))];
805
+ if (matchKinds.length > 1) {
806
+ reasons.push(`Candidates matched at different precision levels: ${matchKinds.join(", ")}`);
807
+ }
808
+ if (usedMojangClientMappings) {
809
+ const hasDescriptor = candidates.some((c) => c.descriptor);
810
+ const missingDescriptor = candidates.some((c) => !c.descriptor);
811
+ if (hasDescriptor && missingDescriptor) {
812
+ reasons.push("Method descriptor was lost through mojang-client-mappings path, causing broader matching.");
813
+ }
814
+ }
815
+ if (owners.length <= 1) {
816
+ const descriptors = [...new Set(candidates.map((c) => c.descriptor).filter(Boolean))];
817
+ if (descriptors.length > 1) {
818
+ reasons.push(`Overloaded method: ${descriptors.length} variants`);
819
+ }
820
+ }
821
+ if (reasons.length === 0) {
822
+ reasons.push(`${candidates.length} candidates matched with similar confidence scores.`);
823
+ }
824
+ return reasons;
825
+ }
684
826
  export class MappingService {
685
827
  config;
686
828
  versionService;
@@ -755,17 +897,28 @@ export class MappingService {
755
897
  };
756
898
  }
757
899
  const rawCandidates = this.mapCandidatesAlongPath(graph, path, queryRecord);
758
- const candidates = rawCandidates.map(toResolutionCandidate);
759
900
  const warnings = [];
901
+ const disambiguatedCandidates = applyDisambiguationHints(rawCandidates, input.disambiguation);
902
+ if (rawCandidates.length > disambiguatedCandidates.length) {
903
+ warnings.push(`Disambiguation hints narrowed candidates from ${rawCandidates.length} to ${disambiguatedCandidates.length}.`);
904
+ }
905
+ const candidates = disambiguatedCandidates.map(toResolutionCandidate);
760
906
  if (queryRecord.kind === "method" &&
761
907
  queryRecord.descriptor &&
762
- pathUsesSource(graph.pairs, path, "mojang-client-mappings")) {
908
+ pathUsesSource(graph.pairs, path, "mojang-client-mappings") &&
909
+ candidates.length !== 1) {
763
910
  warnings.push("Method descriptor could not be preserved through mojang-client-mappings and may have used name-based fallback.");
764
911
  }
765
912
  if (candidates.length === 0) {
766
913
  warnings.push("No mapping candidate matched the input symbol.");
767
914
  }
915
+ else if (candidates.length > 1) {
916
+ warnings.push(`Ambiguous mapping: ${candidates.length} candidates matched. Provide a stricter symbol input or disambiguation hints.`);
917
+ }
768
918
  const status = candidates.length === 0 ? "not_found" : candidates.length === 1 ? "resolved" : "ambiguous";
919
+ const ambiguityReasons = candidates.length > 1
920
+ ? inferAmbiguityReasons(candidates, pathUsesSource(graph.pairs, path, "mojang-client-mappings"))
921
+ : undefined;
769
922
  return {
770
923
  querySymbol,
771
924
  mappingContext,
@@ -774,7 +927,8 @@ export class MappingService {
774
927
  resolvedSymbol: status === "resolved" ? candidates[0] : undefined,
775
928
  candidates,
776
929
  warnings,
777
- provenance: this.provenanceForPath(graph, path)
930
+ provenance: this.provenanceForPath(graph, path),
931
+ ambiguityReasons
778
932
  };
779
933
  }
780
934
  async ensureMappingAvailable(input) {
@@ -818,7 +972,9 @@ export class MappingService {
818
972
  version,
819
973
  sourceMapping,
820
974
  targetMapping,
821
- sourcePriority: priority
975
+ sourcePriority: priority,
976
+ nextAction: "Try mapping=official which is always available.",
977
+ suggestedCall: { tool: "resolve-artifact", params: { mapping: "official" } }
822
978
  }
823
979
  });
824
980
  }
@@ -996,7 +1152,8 @@ export class MappingService {
996
1152
  }
997
1153
  const mapped = this.mapRecordBetweenMappings(graph, classNameMapping, mapping, classByMapping[classNameMapping]);
998
1154
  if (mapped.length > 1) {
999
- warnings.push(`Class identity mapping to ${mapping} is ambiguous for "${className}".`);
1155
+ const competing = mapped.slice(0, 5).map((c) => c.symbol);
1156
+ warnings.push(`Class identity mapping to ${mapping} is ambiguous for "${className}": competing=[${competing.join(", ")}].`);
1000
1157
  }
1001
1158
  if (mapped.length > 0) {
1002
1159
  classByMapping[mapping] = mapped[0];
@@ -1032,6 +1189,7 @@ export class MappingService {
1032
1189
  return includeKinds.has("method");
1033
1190
  });
1034
1191
  const rows = [];
1192
+ let ambiguousRowCount = 0;
1035
1193
  const rowSeen = new Set();
1036
1194
  const rowKindOrder = {
1037
1195
  class: 0,
@@ -1055,6 +1213,7 @@ export class MappingService {
1055
1213
  continue;
1056
1214
  }
1057
1215
  rowSeen.add(key);
1216
+ let rowHadAmbiguity = false;
1058
1217
  const row = {
1059
1218
  kind: baseRecord.kind,
1060
1219
  descriptor: baseRecord.descriptor,
@@ -1079,7 +1238,9 @@ export class MappingService {
1079
1238
  }
1080
1239
  }
1081
1240
  if (filtered.length > 1) {
1082
- warnings.push(`Row mapping to ${mapping} is ambiguous for "${baseRecord.symbol}". Using highest-ranked candidate.`);
1241
+ const competing = filtered.slice(0, 5).map((c) => c.symbol);
1242
+ warnings.push(`Row mapping to ${mapping} is ambiguous for "${baseRecord.symbol}": competing=[${competing.join(", ")}]. Using highest-ranked candidate.`);
1243
+ rowHadAmbiguity = true;
1083
1244
  }
1084
1245
  resolved = filtered[0];
1085
1246
  }
@@ -1096,6 +1257,9 @@ export class MappingService {
1096
1257
  }
1097
1258
  row.completeness = Boolean(row.official && row.mojang && row.intermediary && row.yarn);
1098
1259
  rows.push(row);
1260
+ if (rowHadAmbiguity) {
1261
+ ambiguousRowCount += 1;
1262
+ }
1099
1263
  }
1100
1264
  return {
1101
1265
  version,
@@ -1108,7 +1272,8 @@ export class MappingService {
1108
1272
  yarn: classByMapping.yarn?.symbol
1109
1273
  },
1110
1274
  rows,
1111
- warnings
1275
+ warnings,
1276
+ ambiguousRowCount: ambiguousRowCount > 0 ? ambiguousRowCount : undefined
1112
1277
  };
1113
1278
  }
1114
1279
  async checkSymbolExists(input) {
@@ -1122,7 +1287,6 @@ export class MappingService {
1122
1287
  }
1123
1288
  });
1124
1289
  }
1125
- const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input);
1126
1290
  const sourceMapping = input.sourceMapping;
1127
1291
  if (!SUPPORTED_MAPPINGS.has(sourceMapping)) {
1128
1292
  throw createError({
@@ -1139,12 +1303,51 @@ export class MappingService {
1139
1303
  sourceMapping,
1140
1304
  sourcePriorityApplied: priority
1141
1305
  };
1306
+ const classNameMode = input.nameMode === "auto" ? "auto" : "fqcn";
1307
+ const normalizedQuery = input.kind === "class" && classNameMode === "auto"
1308
+ ? (() => {
1309
+ const owner = input.owner?.trim();
1310
+ if (owner) {
1311
+ throw invalidInputError("owner is not allowed when kind=class. Use name as FQCN.", {
1312
+ owner: input.owner,
1313
+ nextAction: 'Provide class as name, e.g. "net.minecraft.server.Main".'
1314
+ });
1315
+ }
1316
+ if (input.descriptor?.trim()) {
1317
+ throw invalidInputError("descriptor is not allowed when kind=class.", {
1318
+ descriptor: input.descriptor
1319
+ });
1320
+ }
1321
+ const autoClassName = normalizeMappedSymbolOutput(input.name.trim());
1322
+ if (!autoClassName) {
1323
+ throw invalidInputError("name must be a non-empty string.", {
1324
+ name: input.name
1325
+ });
1326
+ }
1327
+ return {
1328
+ mode: "auto-class",
1329
+ className: autoClassName,
1330
+ querySymbol: {
1331
+ kind: "class",
1332
+ name: autoClassName,
1333
+ symbol: autoClassName
1334
+ }
1335
+ };
1336
+ })()
1337
+ : (() => {
1338
+ const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, input.signatureMode);
1339
+ return {
1340
+ mode: "strict",
1341
+ queryRecord,
1342
+ querySymbol
1343
+ };
1344
+ })();
1142
1345
  const graph = await this.loadGraph(version, priority);
1143
1346
  const warnings = [...graph.warnings];
1144
1347
  const records = collectTargetRecords(graph, sourceMapping);
1145
1348
  if (records.length === 0) {
1146
1349
  return {
1147
- querySymbol,
1350
+ querySymbol: normalizedQuery.querySymbol,
1148
1351
  mappingContext,
1149
1352
  resolved: false,
1150
1353
  status: "mapping_unavailable",
@@ -1152,7 +1355,7 @@ export class MappingService {
1152
1355
  warnings
1153
1356
  };
1154
1357
  }
1155
- const buildOutput = (matched, status) => {
1358
+ const buildOutput = (querySymbol, matched, status) => {
1156
1359
  const candidates = matched.map((record) => toResolutionCandidate(toLookupCandidate(record)));
1157
1360
  return {
1158
1361
  querySymbol,
@@ -1164,29 +1367,52 @@ export class MappingService {
1164
1367
  warnings
1165
1368
  };
1166
1369
  };
1370
+ if (normalizedQuery.mode === "auto-class") {
1371
+ const autoClassName = normalizedQuery.className;
1372
+ if (autoClassName.includes(".")) {
1373
+ const matched = records.filter((record) => record.kind === "class" && record.symbol === autoClassName);
1374
+ const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
1375
+ return buildOutput(normalizedQuery.querySymbol, matched, status);
1376
+ }
1377
+ const matched = records.filter((record) => record.kind === "class" && record.name === autoClassName);
1378
+ const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
1379
+ if (status === "ambiguous") {
1380
+ warnings.push(`Multiple class symbols matched short name "${autoClassName}". Provide fully-qualified class name.`);
1381
+ }
1382
+ return buildOutput(normalizedQuery.querySymbol, matched, status);
1383
+ }
1384
+ const { queryRecord, querySymbol } = normalizedQuery;
1167
1385
  if (queryRecord.kind === "class") {
1168
1386
  const matched = records.filter((record) => record.kind === "class" && record.symbol === queryRecord.symbol);
1169
1387
  const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
1170
- return buildOutput(matched, status);
1388
+ return buildOutput(querySymbol, matched, status);
1171
1389
  }
1172
1390
  if (queryRecord.kind === "field") {
1173
1391
  const matched = records.filter((record) => record.kind === "field" && record.owner === queryRecord.owner && record.name === queryRecord.name);
1174
1392
  const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
1175
- return buildOutput(matched, status);
1393
+ return buildOutput(querySymbol, matched, status);
1176
1394
  }
1177
1395
  const methodCandidates = records.filter((record) => record.kind === "method" && record.owner === queryRecord.owner && record.name === queryRecord.name);
1396
+ // name-only mode: skip descriptor matching, resolve by owner+name
1397
+ if (input.signatureMode === "name-only") {
1398
+ const status = methodCandidates.length === 1 ? "resolved" : methodCandidates.length > 1 ? "ambiguous" : "not_found";
1399
+ if (status === "ambiguous") {
1400
+ warnings.push(`Multiple method overloads matched name "${queryRecord.name}" in owner "${queryRecord.owner}". Provide descriptor for exact match.`);
1401
+ }
1402
+ return buildOutput(querySymbol, methodCandidates, status);
1403
+ }
1178
1404
  const descriptorMatched = methodCandidates.filter((record) => record.descriptor === queryRecord.descriptor);
1179
1405
  if (descriptorMatched.length === 1) {
1180
- return buildOutput(descriptorMatched, "resolved");
1406
+ return buildOutput(querySymbol, descriptorMatched, "resolved");
1181
1407
  }
1182
1408
  if (descriptorMatched.length > 1) {
1183
- return buildOutput(descriptorMatched, "ambiguous");
1409
+ return buildOutput(querySymbol, descriptorMatched, "ambiguous");
1184
1410
  }
1185
1411
  if (methodCandidates.some((candidate) => candidate.descriptor == null)) {
1186
1412
  warnings.push("Descriptor-level existence checks are unavailable for descriptorless mapping entries.");
1187
- return buildOutput(methodCandidates, "mapping_unavailable");
1413
+ return buildOutput(querySymbol, methodCandidates, "mapping_unavailable");
1188
1414
  }
1189
- return buildOutput([], "not_found");
1415
+ return buildOutput(querySymbol, [], "not_found");
1190
1416
  }
1191
1417
  mapRecordBetweenMappings(graph, sourceMapping, targetMapping, record) {
1192
1418
  if (sourceMapping === targetMapping) {
@@ -1308,6 +1534,59 @@ export class MappingService {
1308
1534
  priority: graph.priority
1309
1535
  };
1310
1536
  }
1537
+ /**
1538
+ * Probe the mapping graph health for a given version.
1539
+ * Returns availability of mojang mappings, tiny mappings, and member remap paths.
1540
+ */
1541
+ async checkMappingHealth(input) {
1542
+ const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
1543
+ const degradations = [];
1544
+ let graph;
1545
+ try {
1546
+ graph = await this.loadGraph(input.version, priority);
1547
+ }
1548
+ catch {
1549
+ return {
1550
+ mojangMappingsAvailable: false,
1551
+ tinyMappingsAvailable: false,
1552
+ memberRemapAvailable: false,
1553
+ degradations: ["Mapping graph could not be loaded."]
1554
+ };
1555
+ }
1556
+ // Check for mojang-client-mappings pairs
1557
+ let mojangAvailable = false;
1558
+ let tinyAvailable = false;
1559
+ for (const [, record] of graph.pairs) {
1560
+ if (record.source === "mojang-client-mappings")
1561
+ mojangAvailable = true;
1562
+ if (record.source === "loom-cache" || record.source === "maven")
1563
+ tinyAvailable = true;
1564
+ }
1565
+ if (!mojangAvailable) {
1566
+ degradations.push("Mojang client mappings are not available for this version.");
1567
+ }
1568
+ if (!tinyAvailable) {
1569
+ degradations.push("No intermediary/yarn tiny mappings were found for this version.");
1570
+ }
1571
+ // Check if member remap path exists (requestedMapping → official)
1572
+ let memberRemapAvailable = false;
1573
+ if (input.requestedMapping === "official") {
1574
+ memberRemapAvailable = true;
1575
+ }
1576
+ else {
1577
+ const path = namespacePath(graph.pairs, input.requestedMapping, "official");
1578
+ memberRemapAvailable = path != null && path.length > 1;
1579
+ if (!memberRemapAvailable) {
1580
+ degradations.push(`No mapping path from ${input.requestedMapping} to official; member remap will fail.`);
1581
+ }
1582
+ }
1583
+ return {
1584
+ mojangMappingsAvailable: mojangAvailable,
1585
+ tinyMappingsAvailable: tinyAvailable,
1586
+ memberRemapAvailable,
1587
+ degradations
1588
+ };
1589
+ }
1311
1590
  async loadGraph(version, priority) {
1312
1591
  const cacheKey = `${version}|${priority}`;
1313
1592
  const cached = this.graphCache.get(cacheKey);
@@ -26,6 +26,7 @@ export type ParsedMixin = {
26
26
  className: string;
27
27
  targets: ParsedMixinTarget[];
28
28
  priority?: number;
29
+ imports: Map<string, string>;
29
30
  injections: ParsedInjection[];
30
31
  shadows: ParsedShadow[];
31
32
  accessors: ParsedAccessor[];