@adhisang/minecraft-modding-mcp 1.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 (106) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +765 -0
  4. package/dist/access-widener-parser.d.ts +24 -0
  5. package/dist/access-widener-parser.js +77 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +4 -0
  8. package/dist/config.d.ts +27 -0
  9. package/dist/config.js +178 -0
  10. package/dist/decompiler/vineflower.d.ts +15 -0
  11. package/dist/decompiler/vineflower.js +185 -0
  12. package/dist/errors.d.ts +50 -0
  13. package/dist/errors.js +49 -0
  14. package/dist/hash.d.ts +1 -0
  15. package/dist/hash.js +12 -0
  16. package/dist/index.d.ts +7 -0
  17. package/dist/index.js +1447 -0
  18. package/dist/java-process.d.ts +16 -0
  19. package/dist/java-process.js +120 -0
  20. package/dist/logger.d.ts +3 -0
  21. package/dist/logger.js +21 -0
  22. package/dist/mapping-pipeline-service.d.ts +18 -0
  23. package/dist/mapping-pipeline-service.js +60 -0
  24. package/dist/mapping-service.d.ts +161 -0
  25. package/dist/mapping-service.js +1706 -0
  26. package/dist/maven-resolver.d.ts +22 -0
  27. package/dist/maven-resolver.js +122 -0
  28. package/dist/minecraft-explorer-service.d.ts +43 -0
  29. package/dist/minecraft-explorer-service.js +562 -0
  30. package/dist/mixin-parser.d.ts +34 -0
  31. package/dist/mixin-parser.js +194 -0
  32. package/dist/mixin-validator.d.ts +59 -0
  33. package/dist/mixin-validator.js +274 -0
  34. package/dist/mod-analyzer.d.ts +23 -0
  35. package/dist/mod-analyzer.js +346 -0
  36. package/dist/mod-decompile-service.d.ts +39 -0
  37. package/dist/mod-decompile-service.js +136 -0
  38. package/dist/mod-remap-service.d.ts +17 -0
  39. package/dist/mod-remap-service.js +186 -0
  40. package/dist/mod-search-service.d.ts +28 -0
  41. package/dist/mod-search-service.js +174 -0
  42. package/dist/mojang-tiny-mapping-service.d.ts +13 -0
  43. package/dist/mojang-tiny-mapping-service.js +351 -0
  44. package/dist/nbt/java-nbt-codec.d.ts +3 -0
  45. package/dist/nbt/java-nbt-codec.js +385 -0
  46. package/dist/nbt/json-patch.d.ts +3 -0
  47. package/dist/nbt/json-patch.js +352 -0
  48. package/dist/nbt/pipeline.d.ts +39 -0
  49. package/dist/nbt/pipeline.js +173 -0
  50. package/dist/nbt/typed-json.d.ts +10 -0
  51. package/dist/nbt/typed-json.js +205 -0
  52. package/dist/nbt/types.d.ts +66 -0
  53. package/dist/nbt/types.js +2 -0
  54. package/dist/observability.d.ts +88 -0
  55. package/dist/observability.js +165 -0
  56. package/dist/path-converter.d.ts +12 -0
  57. package/dist/path-converter.js +161 -0
  58. package/dist/path-resolver.d.ts +19 -0
  59. package/dist/path-resolver.js +78 -0
  60. package/dist/registry-service.d.ts +29 -0
  61. package/dist/registry-service.js +214 -0
  62. package/dist/repo-downloader.d.ts +15 -0
  63. package/dist/repo-downloader.js +111 -0
  64. package/dist/resources.d.ts +3 -0
  65. package/dist/resources.js +154 -0
  66. package/dist/search-hit-accumulator.d.ts +38 -0
  67. package/dist/search-hit-accumulator.js +153 -0
  68. package/dist/source-jar-reader.d.ts +13 -0
  69. package/dist/source-jar-reader.js +216 -0
  70. package/dist/source-resolver.d.ts +14 -0
  71. package/dist/source-resolver.js +274 -0
  72. package/dist/source-service.d.ts +404 -0
  73. package/dist/source-service.js +2881 -0
  74. package/dist/storage/artifacts-repo.d.ts +45 -0
  75. package/dist/storage/artifacts-repo.js +209 -0
  76. package/dist/storage/db.d.ts +14 -0
  77. package/dist/storage/db.js +132 -0
  78. package/dist/storage/files-repo.d.ts +78 -0
  79. package/dist/storage/files-repo.js +437 -0
  80. package/dist/storage/index-meta-repo.d.ts +35 -0
  81. package/dist/storage/index-meta-repo.js +97 -0
  82. package/dist/storage/migrations.d.ts +11 -0
  83. package/dist/storage/migrations.js +71 -0
  84. package/dist/storage/schema.d.ts +1 -0
  85. package/dist/storage/schema.js +160 -0
  86. package/dist/storage/sqlite.d.ts +20 -0
  87. package/dist/storage/sqlite.js +111 -0
  88. package/dist/storage/symbols-repo.d.ts +63 -0
  89. package/dist/storage/symbols-repo.js +401 -0
  90. package/dist/symbols/symbol-extractor.d.ts +7 -0
  91. package/dist/symbols/symbol-extractor.js +64 -0
  92. package/dist/tiny-remapper-resolver.d.ts +1 -0
  93. package/dist/tiny-remapper-resolver.js +62 -0
  94. package/dist/tiny-remapper-service.d.ts +16 -0
  95. package/dist/tiny-remapper-service.js +73 -0
  96. package/dist/types.d.ts +120 -0
  97. package/dist/types.js +2 -0
  98. package/dist/version-diff-service.d.ts +41 -0
  99. package/dist/version-diff-service.js +222 -0
  100. package/dist/version-service.d.ts +70 -0
  101. package/dist/version-service.js +411 -0
  102. package/dist/vineflower-resolver.d.ts +1 -0
  103. package/dist/vineflower-resolver.js +62 -0
  104. package/dist/workspace-mapping-service.d.ts +18 -0
  105. package/dist/workspace-mapping-service.js +89 -0
  106. package/package.json +61 -0
@@ -0,0 +1,1706 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import fastGlob from "fast-glob";
5
+ import { createError, ERROR_CODES } from "./errors.js";
6
+ import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
7
+ import { listJarEntries, readJarEntryAsUtf8 } from "./source-jar-reader.js";
8
+ import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
9
+ const SUPPORTED_MAPPINGS = new Set([
10
+ "official",
11
+ "mojang",
12
+ "intermediary",
13
+ "yarn"
14
+ ]);
15
+ const MATCH_RANK = {
16
+ exact: 3,
17
+ normalized: 2,
18
+ "simple-name": 1
19
+ };
20
+ const DESCRIPTOR_FALLBACK_CONFIDENCE = 0.85;
21
+ const MAX_CANDIDATES = 200;
22
+ function createDirectionIndex() {
23
+ return {
24
+ exact: new Map(),
25
+ normalized: new Map(),
26
+ simple: new Map(),
27
+ records: new Map()
28
+ };
29
+ }
30
+ function addToSetMap(map, key, value) {
31
+ const normalizedKey = key.trim();
32
+ if (!normalizedKey) {
33
+ return;
34
+ }
35
+ const existing = map.get(normalizedKey) ?? new Set();
36
+ existing.add(value);
37
+ map.set(normalizedKey, existing);
38
+ }
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];
47
+ }
48
+ function simpleName(symbol) {
49
+ const trimmed = symbol.trim();
50
+ if (!trimmed) {
51
+ return undefined;
52
+ }
53
+ const withoutDescriptor = trimmed.includes("(") ? trimmed.slice(0, trimmed.indexOf("(")) : trimmed;
54
+ const base = withoutDescriptor.split(/[./]/).at(-1)?.trim();
55
+ return base || undefined;
56
+ }
57
+ function normalizeMappedSymbolOutput(symbol) {
58
+ return symbol.replace(/\//g, ".");
59
+ }
60
+ function splitOwnerAndName(symbol) {
61
+ const trimmed = symbol.trim();
62
+ const separatorIndex = Math.max(trimmed.lastIndexOf("."), trimmed.lastIndexOf("/"));
63
+ if (separatorIndex <= 0 || separatorIndex >= trimmed.length - 1) {
64
+ return undefined;
65
+ }
66
+ return {
67
+ owner: trimmed.slice(0, separatorIndex),
68
+ name: trimmed.slice(separatorIndex + 1)
69
+ };
70
+ }
71
+ function stripLineInfo(input) {
72
+ let value = input.trim();
73
+ while (/^\d+:\d+:/.test(value)) {
74
+ value = value.replace(/^\d+:\d+:/, "");
75
+ }
76
+ return value.replace(/:\d+:\d+$/, "").trim();
77
+ }
78
+ function parseMethodName(value) {
79
+ const match = /^(.+?)\s+([^\s(]+)\((.*)\)$/.exec(value);
80
+ if (!match) {
81
+ return undefined;
82
+ }
83
+ return match[2]?.trim() || undefined;
84
+ }
85
+ function parseFieldName(value) {
86
+ const match = /^(.+?)\s+([^\s]+)$/.exec(value);
87
+ if (!match) {
88
+ return undefined;
89
+ }
90
+ return match[2]?.trim() || undefined;
91
+ }
92
+ function buildSymbolKey(record) {
93
+ return `${record.kind}|${record.owner ?? ""}|${record.name}|${record.descriptor ?? ""}`;
94
+ }
95
+ function classNameParts(classFqn) {
96
+ const separatorIndex = classFqn.lastIndexOf(".");
97
+ if (separatorIndex <= 0 || separatorIndex >= classFqn.length - 1) {
98
+ return {
99
+ owner: undefined,
100
+ name: classFqn
101
+ };
102
+ }
103
+ return {
104
+ owner: classFqn.slice(0, separatorIndex),
105
+ name: classFqn.slice(separatorIndex + 1)
106
+ };
107
+ }
108
+ function createClassSymbolRecord(className) {
109
+ const symbol = normalizeMappedSymbolOutput(className.trim());
110
+ const parts = classNameParts(symbol);
111
+ return {
112
+ kind: "class",
113
+ symbol,
114
+ owner: parts.owner,
115
+ name: parts.name
116
+ };
117
+ }
118
+ function createFieldSymbolRecord(owner, fieldName) {
119
+ const normalizedOwner = normalizeMappedSymbolOutput(owner.trim());
120
+ const normalizedName = fieldName.trim();
121
+ return {
122
+ kind: "field",
123
+ symbol: `${normalizedOwner}.${normalizedName}`,
124
+ owner: normalizedOwner,
125
+ name: normalizedName
126
+ };
127
+ }
128
+ function createMethodSymbolRecord(owner, methodName, descriptor) {
129
+ const normalizedOwner = normalizeMappedSymbolOutput(owner.trim());
130
+ const normalizedName = methodName.trim();
131
+ const normalizedDescriptor = descriptor?.trim() || undefined;
132
+ return {
133
+ kind: "method",
134
+ symbol: `${normalizedOwner}.${normalizedName}${normalizedDescriptor ?? ""}`,
135
+ owner: normalizedOwner,
136
+ name: normalizedName,
137
+ descriptor: normalizedDescriptor
138
+ };
139
+ }
140
+ function parseInputSymbol(symbol) {
141
+ const trimmed = symbol.trim();
142
+ if (!trimmed || /\s/.test(trimmed)) {
143
+ return undefined;
144
+ }
145
+ const openIndex = trimmed.indexOf("(");
146
+ if (openIndex >= 0) {
147
+ const closeIndex = trimmed.indexOf(")", openIndex);
148
+ if (closeIndex < 0) {
149
+ return undefined;
150
+ }
151
+ const ownerAndMethod = splitOwnerAndName(trimmed.slice(0, openIndex));
152
+ if (!ownerAndMethod) {
153
+ return undefined;
154
+ }
155
+ const descriptor = trimmed.slice(openIndex);
156
+ return createMethodSymbolRecord(ownerAndMethod.owner, ownerAndMethod.name, descriptor);
157
+ }
158
+ const ownerAndName = splitOwnerAndName(trimmed);
159
+ if (!ownerAndName) {
160
+ return createClassSymbolRecord(trimmed);
161
+ }
162
+ if (/^[A-Z$]/.test(ownerAndName.name)) {
163
+ return createClassSymbolRecord(trimmed);
164
+ }
165
+ return createFieldSymbolRecord(ownerAndName.owner, ownerAndName.name);
166
+ }
167
+ function exactLookupKeys(record) {
168
+ const keys = new Set([record.symbol]);
169
+ if (record.kind === "method" && record.owner && record.descriptor) {
170
+ keys.add(`${record.owner}.${record.name}`);
171
+ }
172
+ return [...keys];
173
+ }
174
+ function simpleLookupKeys(record) {
175
+ if (record.kind === "class") {
176
+ return [record.name];
177
+ }
178
+ if (record.kind === "field") {
179
+ return [record.name];
180
+ }
181
+ if (record.descriptor) {
182
+ return [record.name, `${record.name}${record.descriptor}`];
183
+ }
184
+ return [record.name];
185
+ }
186
+ function registerRecord(index, record) {
187
+ const key = buildSymbolKey(record);
188
+ if (!index.records.has(key)) {
189
+ index.records.set(key, record);
190
+ }
191
+ return key;
192
+ }
193
+ function addLookupEntries(index, fromRecord, toRecord) {
194
+ if (!fromRecord.symbol || !toRecord.symbol) {
195
+ return;
196
+ }
197
+ const targetKey = registerRecord(index, toRecord);
198
+ for (const key of exactLookupKeys(fromRecord)) {
199
+ addToSetMap(index.exact, key, targetKey);
200
+ for (const variant of normalizedVariants(key)) {
201
+ if (variant !== key) {
202
+ addToSetMap(index.normalized, variant, targetKey);
203
+ }
204
+ }
205
+ }
206
+ for (const key of simpleLookupKeys(fromRecord)) {
207
+ addToSetMap(index.simple, key, targetKey);
208
+ }
209
+ }
210
+ function mergeDirectionIndexes(target, source) {
211
+ const mergeMap = (targetMap, sourceMap) => {
212
+ for (const [key, values] of sourceMap.entries()) {
213
+ const existing = targetMap.get(key) ?? new Set();
214
+ for (const value of values) {
215
+ existing.add(value);
216
+ }
217
+ targetMap.set(key, existing);
218
+ }
219
+ };
220
+ mergeMap(target.exact, source.exact);
221
+ mergeMap(target.normalized, source.normalized);
222
+ mergeMap(target.simple, source.simple);
223
+ for (const [key, value] of source.records.entries()) {
224
+ if (!target.records.has(key)) {
225
+ target.records.set(key, value);
226
+ }
227
+ }
228
+ }
229
+ function pairKey(sourceMapping, targetMapping) {
230
+ return `${sourceMapping}->${targetMapping}`;
231
+ }
232
+ function parsePairKey(key) {
233
+ const [source, target] = key.split("->");
234
+ return {
235
+ sourceMapping: source,
236
+ targetMapping: target
237
+ };
238
+ }
239
+ function ensurePairIndex(indexes, from, to) {
240
+ const key = pairKey(from, to);
241
+ const existing = indexes.get(key);
242
+ if (existing) {
243
+ return existing;
244
+ }
245
+ const created = createDirectionIndex();
246
+ indexes.set(key, created);
247
+ return created;
248
+ }
249
+ function parseClientMappings(text) {
250
+ const officialToMojang = createDirectionIndex();
251
+ const mojangToOfficial = createDirectionIndex();
252
+ let classCount = 0;
253
+ let currentClass;
254
+ for (const rawLine of text.split(/\r?\n/)) {
255
+ const line = rawLine.trim();
256
+ if (!line || line.startsWith("#")) {
257
+ continue;
258
+ }
259
+ const classMatch = /^(.+?)\s+->\s+(.+):$/.exec(line);
260
+ if (classMatch) {
261
+ const mojangClass = classMatch[1]?.trim() ?? "";
262
+ const officialClass = classMatch[2]?.trim() ?? "";
263
+ if (!mojangClass || !officialClass) {
264
+ currentClass = undefined;
265
+ continue;
266
+ }
267
+ classCount += 1;
268
+ currentClass = {
269
+ official: officialClass,
270
+ mojang: mojangClass
271
+ };
272
+ addLookupEntries(officialToMojang, createClassSymbolRecord(officialClass), createClassSymbolRecord(mojangClass));
273
+ addLookupEntries(mojangToOfficial, createClassSymbolRecord(mojangClass), createClassSymbolRecord(officialClass));
274
+ continue;
275
+ }
276
+ if (!currentClass) {
277
+ continue;
278
+ }
279
+ const arrowIndex = line.indexOf(" -> ");
280
+ if (arrowIndex < 0) {
281
+ continue;
282
+ }
283
+ const leftRaw = line.slice(0, arrowIndex).trim();
284
+ const rightRaw = line.slice(arrowIndex + 4).trim();
285
+ if (!leftRaw || !rightRaw) {
286
+ continue;
287
+ }
288
+ 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));
293
+ continue;
294
+ }
295
+ const fieldName = parseFieldName(mojangMemberSignature);
296
+ if (!fieldName) {
297
+ continue;
298
+ }
299
+ addLookupEntries(officialToMojang, createFieldSymbolRecord(currentClass.official, rightRaw), createFieldSymbolRecord(currentClass.mojang, fieldName));
300
+ addLookupEntries(mojangToOfficial, createFieldSymbolRecord(currentClass.mojang, fieldName), createFieldSymbolRecord(currentClass.official, rightRaw));
301
+ }
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
+ const result = new Map();
309
+ result.set(pairKey("official", "mojang"), officialToMojang);
310
+ result.set(pairKey("mojang", "official"), mojangToOfficial);
311
+ return result;
312
+ }
313
+ function normalizeTinyNamespace(namespace) {
314
+ const normalized = namespace.trim().toLowerCase();
315
+ if (normalized === "official") {
316
+ return "official";
317
+ }
318
+ if (normalized === "mojang") {
319
+ return "mojang";
320
+ }
321
+ if (normalized === "intermediary") {
322
+ return "intermediary";
323
+ }
324
+ if (normalized === "named" || normalized === "yarn") {
325
+ return "yarn";
326
+ }
327
+ return undefined;
328
+ }
329
+ function addPairRecords(target, records) {
330
+ for (const [sourceMapping, sourceRecord] of records.entries()) {
331
+ for (const [targetMapping, targetRecord] of records.entries()) {
332
+ if (sourceMapping === targetMapping) {
333
+ continue;
334
+ }
335
+ addLookupEntries(ensurePairIndex(target, sourceMapping, targetMapping), sourceRecord, targetRecord);
336
+ }
337
+ }
338
+ }
339
+ function parseTinyMappings(text) {
340
+ const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
341
+ if (lines.length === 0) {
342
+ return new Map();
343
+ }
344
+ const header = lines[0].split("\t");
345
+ if (header.length < 5 || header[0] !== "tiny" || header[1] !== "2") {
346
+ return new Map();
347
+ }
348
+ const namespaceColumns = header.slice(3).map((namespace, index) => ({
349
+ mapping: normalizeTinyNamespace(namespace),
350
+ columnIndex: index + 1
351
+ }));
352
+ const recognized = namespaceColumns.filter((entry) => entry.mapping != null);
353
+ if (recognized.length < 2) {
354
+ return new Map();
355
+ }
356
+ const result = new Map();
357
+ const currentClassNames = new Map();
358
+ for (const line of lines.slice(1)) {
359
+ const columns = line.split("\t");
360
+ if (columns[0] === "c") {
361
+ const classRecords = new Map();
362
+ for (const namespace of recognized) {
363
+ const value = columns[namespace.columnIndex]?.trim() ?? "";
364
+ if (!value) {
365
+ continue;
366
+ }
367
+ currentClassNames.set(namespace.mapping, value);
368
+ classRecords.set(namespace.mapping, createClassSymbolRecord(value));
369
+ }
370
+ addPairRecords(result, classRecords);
371
+ continue;
372
+ }
373
+ if (columns[0] === "" && columns[1] === "f") {
374
+ const fieldRecords = new Map();
375
+ for (const namespace of recognized) {
376
+ const owner = currentClassNames.get(namespace.mapping);
377
+ const value = columns[namespace.columnIndex + 2]?.trim() ?? "";
378
+ if (!owner || !value) {
379
+ continue;
380
+ }
381
+ fieldRecords.set(namespace.mapping, createFieldSymbolRecord(owner, value));
382
+ }
383
+ addPairRecords(result, fieldRecords);
384
+ continue;
385
+ }
386
+ if (columns[0] === "" && columns[1] === "m") {
387
+ const descriptor = columns[2]?.trim() || undefined;
388
+ const methodRecords = new Map();
389
+ for (const namespace of recognized) {
390
+ const owner = currentClassNames.get(namespace.mapping);
391
+ const value = columns[namespace.columnIndex + 2]?.trim() ?? "";
392
+ if (!owner || !value) {
393
+ continue;
394
+ }
395
+ methodRecords.set(namespace.mapping, createMethodSymbolRecord(owner, value, descriptor));
396
+ }
397
+ addPairRecords(result, methodRecords);
398
+ }
399
+ }
400
+ return result;
401
+ }
402
+ function addCandidates(target, index, symbols, kind, confidence) {
403
+ if (!symbols || symbols.size === 0) {
404
+ return;
405
+ }
406
+ const rank = MATCH_RANK[kind];
407
+ for (const key of symbols) {
408
+ const record = index.records.get(key);
409
+ if (!record) {
410
+ continue;
411
+ }
412
+ const current = target.get(key);
413
+ if (!current || rank > current.rank || (rank === current.rank && confidence > current.confidence)) {
414
+ target.set(key, {
415
+ key,
416
+ record,
417
+ matchKind: kind,
418
+ confidence,
419
+ rank
420
+ });
421
+ }
422
+ }
423
+ }
424
+ function lookupCandidates(index, query) {
425
+ const trimmedQuery = query.symbol.trim();
426
+ const collected = new Map();
427
+ addCandidates(collected, index, index.exact.get(trimmedQuery), "exact", 1);
428
+ for (const variant of normalizedVariants(trimmedQuery)) {
429
+ addCandidates(collected, index, index.normalized.get(variant), "normalized", 0.9);
430
+ }
431
+ if (query.kind === "method" && query.owner && query.descriptor) {
432
+ const descriptorlessKey = `${query.owner}.${query.name}`;
433
+ addCandidates(collected, index, index.exact.get(descriptorlessKey), "normalized", DESCRIPTOR_FALLBACK_CONFIDENCE);
434
+ for (const variant of normalizedVariants(descriptorlessKey)) {
435
+ addCandidates(collected, index, index.normalized.get(variant), "normalized", DESCRIPTOR_FALLBACK_CONFIDENCE);
436
+ }
437
+ }
438
+ const simpleKeys = new Set();
439
+ const shortName = simpleName(trimmedQuery);
440
+ if (shortName) {
441
+ simpleKeys.add(shortName);
442
+ }
443
+ if (query.kind !== "class") {
444
+ simpleKeys.add(query.name);
445
+ }
446
+ if (query.kind === "method" && query.descriptor) {
447
+ simpleKeys.add(`${query.name}${query.descriptor}`);
448
+ }
449
+ for (const key of simpleKeys) {
450
+ addCandidates(collected, index, index.simple.get(key), "simple-name", 0.75);
451
+ }
452
+ return [...collected.values()]
453
+ .sort((left, right) => {
454
+ if (right.confidence !== left.confidence) {
455
+ return right.confidence - left.confidence;
456
+ }
457
+ if (right.rank !== left.rank) {
458
+ return right.rank - left.rank;
459
+ }
460
+ return left.record.symbol.localeCompare(right.record.symbol);
461
+ })
462
+ .slice(0, MAX_CANDIDATES)
463
+ .map(({ record, matchKind, confidence }) => ({
464
+ symbol: record.symbol,
465
+ matchKind,
466
+ confidence,
467
+ kind: record.kind,
468
+ owner: record.owner,
469
+ name: record.name,
470
+ descriptor: record.descriptor
471
+ }));
472
+ }
473
+ function mappingPriorityFromInput(configPriority, override) {
474
+ if (override === "loom-first" || override === "maven-first") {
475
+ return override;
476
+ }
477
+ return configPriority;
478
+ }
479
+ function mappingSourceOrder(priority) {
480
+ if (priority === "maven-first") {
481
+ return ["maven", "loom-cache"];
482
+ }
483
+ return ["loom-cache", "maven"];
484
+ }
485
+ function namespacePath(pairs, sourceMapping, targetMapping) {
486
+ if (sourceMapping === targetMapping) {
487
+ return [sourceMapping];
488
+ }
489
+ const queue = [sourceMapping];
490
+ const parent = new Map([[sourceMapping, undefined]]);
491
+ while (queue.length > 0) {
492
+ const current = queue.shift();
493
+ if (current === targetMapping) {
494
+ break;
495
+ }
496
+ for (const key of pairs.keys()) {
497
+ const parsed = parsePairKey(key);
498
+ if (parsed.sourceMapping !== current) {
499
+ continue;
500
+ }
501
+ if (parent.has(parsed.targetMapping)) {
502
+ continue;
503
+ }
504
+ parent.set(parsed.targetMapping, current);
505
+ queue.push(parsed.targetMapping);
506
+ }
507
+ }
508
+ if (!parent.has(targetMapping)) {
509
+ return undefined;
510
+ }
511
+ const path = [];
512
+ let cursor = targetMapping;
513
+ while (cursor) {
514
+ path.unshift(cursor);
515
+ cursor = parent.get(cursor);
516
+ }
517
+ return path;
518
+ }
519
+ function pathUsesSource(pairs, path, source) {
520
+ for (let index = 0; index < path.length - 1; index += 1) {
521
+ const hop = pairs.get(pairKey(path[index], path[index + 1]));
522
+ if (hop?.source === source) {
523
+ return true;
524
+ }
525
+ }
526
+ return false;
527
+ }
528
+ function pathToTransformChain(path) {
529
+ if (path.length <= 1) {
530
+ return [];
531
+ }
532
+ const transform = [];
533
+ for (let index = 0; index < path.length - 1; index += 1) {
534
+ transform.push(`mapping:${path[index]}->${path[index + 1]}`);
535
+ }
536
+ return transform;
537
+ }
538
+ function toLookupCandidate(record) {
539
+ return {
540
+ symbol: record.symbol,
541
+ matchKind: "exact",
542
+ confidence: 1,
543
+ kind: record.kind,
544
+ owner: record.owner,
545
+ name: record.name,
546
+ descriptor: record.descriptor
547
+ };
548
+ }
549
+ function toSymbolReference(record) {
550
+ return {
551
+ kind: record.kind,
552
+ name: record.kind === "class" ? record.symbol : record.name,
553
+ owner: record.kind === "class" ? undefined : record.owner,
554
+ descriptor: record.kind === "method" ? record.descriptor : undefined,
555
+ symbol: record.symbol
556
+ };
557
+ }
558
+ function toResolutionCandidate(candidate) {
559
+ return {
560
+ kind: candidate.kind,
561
+ name: candidate.kind === "class" ? candidate.symbol : candidate.name,
562
+ owner: candidate.kind === "class" ? undefined : candidate.owner,
563
+ descriptor: candidate.kind === "method" ? candidate.descriptor : undefined,
564
+ symbol: candidate.symbol,
565
+ matchKind: candidate.matchKind,
566
+ confidence: candidate.confidence
567
+ };
568
+ }
569
+ function invalidInputError(message, details) {
570
+ return createError({
571
+ code: ERROR_CODES.INVALID_INPUT,
572
+ message,
573
+ details
574
+ });
575
+ }
576
+ function normalizeMemberName(name) {
577
+ const normalized = name.trim();
578
+ if (!normalized || /[\s./()]/.test(normalized)) {
579
+ throw invalidInputError("name must be a simple member name without separators when kind is field or method.", {
580
+ name
581
+ });
582
+ }
583
+ return normalized;
584
+ }
585
+ function normalizeMethodDescriptor(descriptor) {
586
+ const normalized = descriptor?.trim() ?? "";
587
+ if (!normalized || !normalized.startsWith("(") || !normalized.includes(")")) {
588
+ throw invalidInputError("descriptor must be a valid JVM descriptor when kind=method.", {
589
+ descriptor
590
+ });
591
+ }
592
+ return normalized;
593
+ }
594
+ function normalizeQuerySymbol(input) {
595
+ if (input.kind !== "class" && input.kind !== "field" && input.kind !== "method") {
596
+ throw invalidInputError('kind must be one of "class", "field", or "method".', {
597
+ kind: input.kind
598
+ });
599
+ }
600
+ const normalizedName = input.name?.trim() ?? "";
601
+ if (!normalizedName) {
602
+ throw invalidInputError("name must be a non-empty string.", {
603
+ name: input.name
604
+ });
605
+ }
606
+ if (input.kind === "class") {
607
+ const owner = input.owner?.trim();
608
+ if (owner) {
609
+ throw invalidInputError("owner is not allowed when kind=class. Use name as FQCN.", {
610
+ owner: input.owner,
611
+ nextAction: 'Provide class as name, e.g. "net.minecraft.server.Main".'
612
+ });
613
+ }
614
+ if (input.descriptor?.trim()) {
615
+ throw invalidInputError("descriptor is not allowed when kind=class.", {
616
+ descriptor: input.descriptor
617
+ });
618
+ }
619
+ const className = normalizeMappedSymbolOutput(normalizedName);
620
+ if (!className.includes(".")) {
621
+ throw invalidInputError("name must be a fully qualified class name when kind=class.", {
622
+ name: input.name
623
+ });
624
+ }
625
+ const record = createClassSymbolRecord(className);
626
+ return {
627
+ record,
628
+ querySymbol: toSymbolReference(record)
629
+ };
630
+ }
631
+ const owner = normalizeMappedSymbolOutput(input.owner?.trim() ?? "");
632
+ if (!owner) {
633
+ throw invalidInputError("owner is required when kind is field or method.", {
634
+ owner: input.owner,
635
+ kind: input.kind
636
+ });
637
+ }
638
+ if (input.kind === "field") {
639
+ if (input.descriptor?.trim()) {
640
+ throw invalidInputError("descriptor is not allowed when kind=field.", {
641
+ descriptor: input.descriptor
642
+ });
643
+ }
644
+ const record = createFieldSymbolRecord(owner, normalizeMemberName(normalizedName));
645
+ return {
646
+ record,
647
+ querySymbol: toSymbolReference(record)
648
+ };
649
+ }
650
+ const record = createMethodSymbolRecord(owner, normalizeMemberName(normalizedName), normalizeMethodDescriptor(input.descriptor));
651
+ return {
652
+ record,
653
+ querySymbol: toSymbolReference(record)
654
+ };
655
+ }
656
+ function collectTargetRecords(graph, targetMapping) {
657
+ const merged = new Map();
658
+ for (const [key, pair] of graph.pairs.entries()) {
659
+ const parsed = parsePairKey(key);
660
+ if (parsed.targetMapping !== targetMapping) {
661
+ continue;
662
+ }
663
+ for (const record of pair.index.records.values()) {
664
+ merged.set(buildSymbolKey(record), record);
665
+ }
666
+ }
667
+ return [...merged.values()];
668
+ }
669
+ function normalizeIncludedKinds(inputKinds) {
670
+ const normalized = new Set();
671
+ const kinds = inputKinds ?? ["class", "field", "method"];
672
+ for (const kind of kinds) {
673
+ if (kind === "class" || kind === "field" || kind === "method") {
674
+ normalized.add(kind);
675
+ }
676
+ }
677
+ if (normalized.size === 0) {
678
+ normalized.add("class");
679
+ normalized.add("field");
680
+ normalized.add("method");
681
+ }
682
+ return normalized;
683
+ }
684
+ export class MappingService {
685
+ config;
686
+ versionService;
687
+ fetchFn;
688
+ graphCache = new Map();
689
+ buildLocks = new Map();
690
+ constructor(config, versionService = new VersionService(config), fetchFn = globalThis.fetch) {
691
+ this.config = config;
692
+ this.versionService = versionService;
693
+ this.fetchFn = fetchFn;
694
+ }
695
+ async findMapping(input) {
696
+ const version = input.version.trim();
697
+ if (!version) {
698
+ throw createError({
699
+ code: ERROR_CODES.INVALID_INPUT,
700
+ message: "version must be non-empty.",
701
+ details: {
702
+ version: input.version
703
+ }
704
+ });
705
+ }
706
+ const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input);
707
+ const sourceMapping = input.sourceMapping;
708
+ const targetMapping = input.targetMapping;
709
+ if (!SUPPORTED_MAPPINGS.has(sourceMapping) || !SUPPORTED_MAPPINGS.has(targetMapping)) {
710
+ throw createError({
711
+ code: ERROR_CODES.MAPPING_UNAVAILABLE,
712
+ message: "Unsupported mapping pair for lookup.",
713
+ details: {
714
+ version,
715
+ sourceMapping,
716
+ targetMapping
717
+ }
718
+ });
719
+ }
720
+ const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
721
+ const mappingContext = {
722
+ version,
723
+ sourceMapping,
724
+ targetMapping,
725
+ sourcePriorityApplied: priority
726
+ };
727
+ if (sourceMapping === targetMapping) {
728
+ const identity = toResolutionCandidate({
729
+ ...toLookupCandidate(queryRecord),
730
+ matchKind: "exact",
731
+ confidence: 1
732
+ });
733
+ return {
734
+ querySymbol,
735
+ mappingContext,
736
+ resolved: true,
737
+ status: "resolved",
738
+ resolvedSymbol: querySymbol,
739
+ candidates: [identity],
740
+ warnings: []
741
+ };
742
+ }
743
+ const graph = await this.loadGraph(version, priority);
744
+ const path = namespacePath(graph.pairs, sourceMapping, targetMapping);
745
+ if (!path) {
746
+ return {
747
+ querySymbol,
748
+ mappingContext,
749
+ resolved: false,
750
+ status: "mapping_unavailable",
751
+ candidates: [],
752
+ warnings: [
753
+ `No mapping path is available for ${sourceMapping} -> ${targetMapping} on version "${version}".`
754
+ ]
755
+ };
756
+ }
757
+ const rawCandidates = this.mapCandidatesAlongPath(graph, path, queryRecord);
758
+ const candidates = rawCandidates.map(toResolutionCandidate);
759
+ const warnings = [];
760
+ if (queryRecord.kind === "method" &&
761
+ queryRecord.descriptor &&
762
+ pathUsesSource(graph.pairs, path, "mojang-client-mappings")) {
763
+ warnings.push("Method descriptor could not be preserved through mojang-client-mappings and may have used name-based fallback.");
764
+ }
765
+ if (candidates.length === 0) {
766
+ warnings.push("No mapping candidate matched the input symbol.");
767
+ }
768
+ const status = candidates.length === 0 ? "not_found" : candidates.length === 1 ? "resolved" : "ambiguous";
769
+ return {
770
+ querySymbol,
771
+ mappingContext,
772
+ resolved: status === "resolved",
773
+ status,
774
+ resolvedSymbol: status === "resolved" ? candidates[0] : undefined,
775
+ candidates,
776
+ warnings,
777
+ provenance: this.provenanceForPath(graph, path)
778
+ };
779
+ }
780
+ async ensureMappingAvailable(input) {
781
+ const version = input.version.trim();
782
+ if (!version) {
783
+ throw createError({
784
+ code: ERROR_CODES.INVALID_INPUT,
785
+ message: "version must be non-empty.",
786
+ details: {
787
+ version: input.version
788
+ }
789
+ });
790
+ }
791
+ const sourceMapping = input.sourceMapping;
792
+ const targetMapping = input.targetMapping;
793
+ if (!SUPPORTED_MAPPINGS.has(sourceMapping) || !SUPPORTED_MAPPINGS.has(targetMapping)) {
794
+ throw createError({
795
+ code: ERROR_CODES.MAPPING_UNAVAILABLE,
796
+ message: "Unsupported mapping pair.",
797
+ details: {
798
+ version,
799
+ sourceMapping,
800
+ targetMapping
801
+ }
802
+ });
803
+ }
804
+ const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
805
+ if (sourceMapping === targetMapping) {
806
+ return {
807
+ transformChain: [`mapping:${sourceMapping}->${targetMapping}`],
808
+ warnings: []
809
+ };
810
+ }
811
+ const graph = await this.loadGraph(version, priority);
812
+ const path = namespacePath(graph.pairs, sourceMapping, targetMapping);
813
+ if (!path) {
814
+ throw createError({
815
+ code: ERROR_CODES.MAPPING_UNAVAILABLE,
816
+ message: `No mapping path is available for ${sourceMapping} -> ${targetMapping} on version "${version}".`,
817
+ details: {
818
+ version,
819
+ sourceMapping,
820
+ targetMapping,
821
+ sourcePriority: priority
822
+ }
823
+ });
824
+ }
825
+ const provenance = this.provenanceForPath(graph, path);
826
+ const transformChain = [
827
+ provenance ? `mapping-source:${provenance.source}` : undefined,
828
+ ...pathToTransformChain(path)
829
+ ].filter((entry) => Boolean(entry));
830
+ return {
831
+ transformChain,
832
+ warnings: [...graph.warnings],
833
+ provenance
834
+ };
835
+ }
836
+ async resolveMethodMappingExact(input) {
837
+ const version = input.version.trim();
838
+ if (!version) {
839
+ throw createError({
840
+ code: ERROR_CODES.INVALID_INPUT,
841
+ message: "version must be non-empty.",
842
+ details: {
843
+ version: input.version
844
+ }
845
+ });
846
+ }
847
+ if (input.kind !== "method") {
848
+ throw createError({
849
+ code: ERROR_CODES.INVALID_INPUT,
850
+ message: 'resolveMethodMappingExact requires kind="method".',
851
+ details: {
852
+ kind: input.kind
853
+ }
854
+ });
855
+ }
856
+ const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input);
857
+ const owner = queryRecord.owner;
858
+ const method = queryRecord.name;
859
+ const descriptor = queryRecord.descriptor;
860
+ const sourceMapping = input.sourceMapping;
861
+ const targetMapping = input.targetMapping;
862
+ if (!SUPPORTED_MAPPINGS.has(sourceMapping) || !SUPPORTED_MAPPINGS.has(targetMapping)) {
863
+ throw createError({
864
+ code: ERROR_CODES.MAPPING_UNAVAILABLE,
865
+ message: "Unsupported mapping pair for exact method resolution.",
866
+ details: {
867
+ version,
868
+ sourceMapping,
869
+ targetMapping
870
+ }
871
+ });
872
+ }
873
+ const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
874
+ const mappingContext = {
875
+ version,
876
+ sourceMapping,
877
+ targetMapping,
878
+ sourcePriorityApplied: priority
879
+ };
880
+ if (sourceMapping === targetMapping) {
881
+ const resolvedCandidate = toResolutionCandidate({
882
+ ...toLookupCandidate(queryRecord),
883
+ matchKind: "exact",
884
+ confidence: 1
885
+ });
886
+ return {
887
+ querySymbol,
888
+ mappingContext,
889
+ resolved: true,
890
+ status: "resolved",
891
+ resolvedSymbol: resolvedCandidate,
892
+ candidates: [resolvedCandidate],
893
+ warnings: []
894
+ };
895
+ }
896
+ const graph = await this.loadGraph(version, priority);
897
+ const path = namespacePath(graph.pairs, sourceMapping, targetMapping);
898
+ if (!path) {
899
+ return {
900
+ querySymbol,
901
+ mappingContext,
902
+ resolved: false,
903
+ status: "mapping_unavailable",
904
+ candidates: [],
905
+ warnings: [
906
+ `No mapping path is available for ${sourceMapping} -> ${targetMapping} on version "${version}".`
907
+ ]
908
+ };
909
+ }
910
+ const warnings = [];
911
+ const rawCandidates = this
912
+ .mapCandidatesAlongPath(graph, path, queryRecord)
913
+ .filter((candidate) => candidate.kind === "method");
914
+ const candidates = rawCandidates.map(toResolutionCandidate);
915
+ const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor === descriptor);
916
+ if (strictCandidates.length === 1) {
917
+ const resolved = toResolutionCandidate(strictCandidates[0]);
918
+ return {
919
+ querySymbol,
920
+ mappingContext,
921
+ resolved: true,
922
+ status: "resolved",
923
+ resolvedSymbol: resolved,
924
+ candidates,
925
+ warnings,
926
+ provenance: this.provenanceForPath(graph, path)
927
+ };
928
+ }
929
+ if (strictCandidates.length > 1) {
930
+ warnings.push("Exact method mapping is ambiguous for owner+method+descriptor.");
931
+ return {
932
+ querySymbol,
933
+ mappingContext,
934
+ resolved: false,
935
+ status: "ambiguous",
936
+ candidates,
937
+ warnings,
938
+ provenance: this.provenanceForPath(graph, path)
939
+ };
940
+ }
941
+ if (pathUsesSource(graph.pairs, path, "mojang-client-mappings")) {
942
+ warnings.push("Method descriptor could not be preserved through mojang-client-mappings and exact resolution is unavailable.");
943
+ return {
944
+ querySymbol,
945
+ mappingContext,
946
+ resolved: false,
947
+ status: "mapping_unavailable",
948
+ candidates,
949
+ warnings,
950
+ provenance: this.provenanceForPath(graph, path)
951
+ };
952
+ }
953
+ return {
954
+ querySymbol,
955
+ mappingContext,
956
+ resolved: false,
957
+ status: "not_found",
958
+ candidates,
959
+ warnings,
960
+ provenance: this.provenanceForPath(graph, path)
961
+ };
962
+ }
963
+ async getClassApiMatrix(input) {
964
+ const version = input.version.trim();
965
+ const className = normalizeMappedSymbolOutput(input.className.trim());
966
+ if (!version || !className) {
967
+ throw createError({
968
+ code: ERROR_CODES.INVALID_INPUT,
969
+ message: "version and className must be non-empty strings.",
970
+ details: {
971
+ version: input.version,
972
+ className: input.className
973
+ }
974
+ });
975
+ }
976
+ const classNameMapping = input.classNameMapping;
977
+ if (!SUPPORTED_MAPPINGS.has(classNameMapping)) {
978
+ throw createError({
979
+ code: ERROR_CODES.MAPPING_UNAVAILABLE,
980
+ message: "Unsupported classNameMapping.",
981
+ details: {
982
+ classNameMapping
983
+ }
984
+ });
985
+ }
986
+ const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
987
+ const graph = await this.loadGraph(version, priority);
988
+ const warnings = [...graph.warnings];
989
+ const includeKinds = normalizeIncludedKinds(input.includeKinds);
990
+ const classByMapping = {
991
+ [classNameMapping]: createClassSymbolRecord(className)
992
+ };
993
+ for (const mapping of SUPPORTED_MAPPINGS) {
994
+ if (mapping === classNameMapping) {
995
+ continue;
996
+ }
997
+ const mapped = this.mapRecordBetweenMappings(graph, classNameMapping, mapping, classByMapping[classNameMapping]);
998
+ if (mapped.length > 1) {
999
+ warnings.push(`Class identity mapping to ${mapping} is ambiguous for "${className}".`);
1000
+ }
1001
+ if (mapped.length > 0) {
1002
+ classByMapping[mapping] = mapped[0];
1003
+ }
1004
+ }
1005
+ const baseMapping = classByMapping.official ? "official" : classNameMapping;
1006
+ const baseClass = classByMapping[baseMapping];
1007
+ if (!baseClass) {
1008
+ return {
1009
+ version,
1010
+ className,
1011
+ classNameMapping,
1012
+ classIdentity: {
1013
+ official: classByMapping.official?.symbol,
1014
+ mojang: classByMapping.mojang?.symbol,
1015
+ intermediary: classByMapping.intermediary?.symbol,
1016
+ yarn: classByMapping.yarn?.symbol
1017
+ },
1018
+ rows: [],
1019
+ warnings
1020
+ };
1021
+ }
1022
+ const baseRecords = collectTargetRecords(graph, baseMapping).filter((record) => {
1023
+ if (record.kind === "class") {
1024
+ return includeKinds.has("class") && record.symbol === baseClass.symbol;
1025
+ }
1026
+ if (record.owner !== baseClass.symbol) {
1027
+ return false;
1028
+ }
1029
+ if (record.kind === "field") {
1030
+ return includeKinds.has("field");
1031
+ }
1032
+ return includeKinds.has("method");
1033
+ });
1034
+ const rows = [];
1035
+ const rowSeen = new Set();
1036
+ const rowKindOrder = {
1037
+ class: 0,
1038
+ field: 1,
1039
+ method: 2
1040
+ };
1041
+ const sortedBase = [...baseRecords].sort((left, right) => {
1042
+ const leftKind = rowKindOrder[left.kind];
1043
+ const rightKind = rowKindOrder[right.kind];
1044
+ if (leftKind !== rightKind) {
1045
+ return leftKind - rightKind;
1046
+ }
1047
+ if ((left.descriptor ?? "") !== (right.descriptor ?? "")) {
1048
+ return (left.descriptor ?? "").localeCompare(right.descriptor ?? "");
1049
+ }
1050
+ return left.symbol.localeCompare(right.symbol);
1051
+ });
1052
+ for (const baseRecord of sortedBase) {
1053
+ const key = buildSymbolKey(baseRecord);
1054
+ if (rowSeen.has(key)) {
1055
+ continue;
1056
+ }
1057
+ rowSeen.add(key);
1058
+ const row = {
1059
+ kind: baseRecord.kind,
1060
+ descriptor: baseRecord.descriptor,
1061
+ completeness: false
1062
+ };
1063
+ for (const mapping of SUPPORTED_MAPPINGS) {
1064
+ const classIdentity = classByMapping[mapping];
1065
+ let resolved;
1066
+ if (mapping === baseMapping) {
1067
+ resolved = baseRecord;
1068
+ }
1069
+ else {
1070
+ const mapped = this.mapRecordBetweenMappings(graph, baseMapping, mapping, baseRecord);
1071
+ let filtered = mapped;
1072
+ if (baseRecord.kind !== "class" && classIdentity) {
1073
+ filtered = filtered.filter((candidate) => candidate.owner === classIdentity.symbol);
1074
+ }
1075
+ if (baseRecord.kind === "method" && baseRecord.descriptor) {
1076
+ const descriptorMatched = filtered.filter((candidate) => candidate.descriptor === baseRecord.descriptor);
1077
+ if (descriptorMatched.length > 0) {
1078
+ filtered = descriptorMatched;
1079
+ }
1080
+ }
1081
+ if (filtered.length > 1) {
1082
+ warnings.push(`Row mapping to ${mapping} is ambiguous for "${baseRecord.symbol}". Using highest-ranked candidate.`);
1083
+ }
1084
+ resolved = filtered[0];
1085
+ }
1086
+ if (!resolved) {
1087
+ continue;
1088
+ }
1089
+ const entry = {
1090
+ symbol: resolved.symbol,
1091
+ owner: resolved.owner,
1092
+ name: resolved.name,
1093
+ descriptor: resolved.descriptor
1094
+ };
1095
+ row[mapping] = entry;
1096
+ }
1097
+ row.completeness = Boolean(row.official && row.mojang && row.intermediary && row.yarn);
1098
+ rows.push(row);
1099
+ }
1100
+ return {
1101
+ version,
1102
+ className,
1103
+ classNameMapping,
1104
+ classIdentity: {
1105
+ official: classByMapping.official?.symbol,
1106
+ mojang: classByMapping.mojang?.symbol,
1107
+ intermediary: classByMapping.intermediary?.symbol,
1108
+ yarn: classByMapping.yarn?.symbol
1109
+ },
1110
+ rows,
1111
+ warnings
1112
+ };
1113
+ }
1114
+ async checkSymbolExists(input) {
1115
+ const version = input.version.trim();
1116
+ if (!version) {
1117
+ throw createError({
1118
+ code: ERROR_CODES.INVALID_INPUT,
1119
+ message: "version must be non-empty.",
1120
+ details: {
1121
+ version: input.version
1122
+ }
1123
+ });
1124
+ }
1125
+ const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input);
1126
+ const sourceMapping = input.sourceMapping;
1127
+ if (!SUPPORTED_MAPPINGS.has(sourceMapping)) {
1128
+ throw createError({
1129
+ code: ERROR_CODES.MAPPING_UNAVAILABLE,
1130
+ message: "Unsupported mapping namespace for existence check.",
1131
+ details: {
1132
+ sourceMapping
1133
+ }
1134
+ });
1135
+ }
1136
+ const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
1137
+ const mappingContext = {
1138
+ version,
1139
+ sourceMapping,
1140
+ sourcePriorityApplied: priority
1141
+ };
1142
+ const graph = await this.loadGraph(version, priority);
1143
+ const warnings = [...graph.warnings];
1144
+ const records = collectTargetRecords(graph, sourceMapping);
1145
+ if (records.length === 0) {
1146
+ return {
1147
+ querySymbol,
1148
+ mappingContext,
1149
+ resolved: false,
1150
+ status: "mapping_unavailable",
1151
+ candidates: [],
1152
+ warnings
1153
+ };
1154
+ }
1155
+ const buildOutput = (matched, status) => {
1156
+ const candidates = matched.map((record) => toResolutionCandidate(toLookupCandidate(record)));
1157
+ return {
1158
+ querySymbol,
1159
+ mappingContext,
1160
+ resolved: status === "resolved",
1161
+ status,
1162
+ resolvedSymbol: status === "resolved" ? candidates[0] : undefined,
1163
+ candidates,
1164
+ warnings
1165
+ };
1166
+ };
1167
+ if (queryRecord.kind === "class") {
1168
+ const matched = records.filter((record) => record.kind === "class" && record.symbol === queryRecord.symbol);
1169
+ const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
1170
+ return buildOutput(matched, status);
1171
+ }
1172
+ if (queryRecord.kind === "field") {
1173
+ const matched = records.filter((record) => record.kind === "field" && record.owner === queryRecord.owner && record.name === queryRecord.name);
1174
+ const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
1175
+ return buildOutput(matched, status);
1176
+ }
1177
+ const methodCandidates = records.filter((record) => record.kind === "method" && record.owner === queryRecord.owner && record.name === queryRecord.name);
1178
+ const descriptorMatched = methodCandidates.filter((record) => record.descriptor === queryRecord.descriptor);
1179
+ if (descriptorMatched.length === 1) {
1180
+ return buildOutput(descriptorMatched, "resolved");
1181
+ }
1182
+ if (descriptorMatched.length > 1) {
1183
+ return buildOutput(descriptorMatched, "ambiguous");
1184
+ }
1185
+ if (methodCandidates.some((candidate) => candidate.descriptor == null)) {
1186
+ warnings.push("Descriptor-level existence checks are unavailable for descriptorless mapping entries.");
1187
+ return buildOutput(methodCandidates, "mapping_unavailable");
1188
+ }
1189
+ return buildOutput([], "not_found");
1190
+ }
1191
+ mapRecordBetweenMappings(graph, sourceMapping, targetMapping, record) {
1192
+ if (sourceMapping === targetMapping) {
1193
+ return [record];
1194
+ }
1195
+ const path = namespacePath(graph.pairs, sourceMapping, targetMapping);
1196
+ if (!path) {
1197
+ return [];
1198
+ }
1199
+ let mapped = this
1200
+ .mapCandidatesAlongPath(graph, path, record)
1201
+ .filter((candidate) => candidate.kind === record.kind)
1202
+ .map((candidate) => ({
1203
+ kind: candidate.kind,
1204
+ symbol: candidate.symbol,
1205
+ owner: candidate.owner,
1206
+ name: candidate.name,
1207
+ descriptor: candidate.descriptor
1208
+ }));
1209
+ if (record.kind === "method" && record.descriptor) {
1210
+ const descriptorMatched = mapped.filter((candidate) => candidate.descriptor === record.descriptor);
1211
+ if (descriptorMatched.length > 0) {
1212
+ mapped = descriptorMatched;
1213
+ }
1214
+ }
1215
+ const deduped = new Map();
1216
+ for (const candidate of mapped) {
1217
+ deduped.set(buildSymbolKey(candidate), candidate);
1218
+ }
1219
+ return [...deduped.values()];
1220
+ }
1221
+ mapCandidatesAlongPath(graph, path, query) {
1222
+ const queryKey = buildSymbolKey(query);
1223
+ let current = new Map([
1224
+ [
1225
+ queryKey,
1226
+ {
1227
+ key: queryKey,
1228
+ record: query,
1229
+ matchKind: "exact",
1230
+ confidence: 1,
1231
+ rank: MATCH_RANK.exact
1232
+ }
1233
+ ]
1234
+ ]);
1235
+ for (let index = 0; index < path.length - 1; index += 1) {
1236
+ const from = path[index];
1237
+ const to = path[index + 1];
1238
+ const record = graph.pairs.get(pairKey(from, to));
1239
+ if (!record) {
1240
+ return [];
1241
+ }
1242
+ const next = new Map();
1243
+ for (const candidate of current.values()) {
1244
+ const mapped = lookupCandidates(record.index, candidate.record);
1245
+ for (const item of mapped) {
1246
+ const mappedRecord = {
1247
+ kind: item.kind,
1248
+ symbol: item.symbol,
1249
+ owner: item.owner,
1250
+ name: item.name,
1251
+ descriptor: item.descriptor
1252
+ };
1253
+ const mappedKey = buildSymbolKey(mappedRecord);
1254
+ const rank = MATCH_RANK[item.matchKind];
1255
+ const composedConfidence = candidate.confidence * item.confidence;
1256
+ const existing = next.get(mappedKey);
1257
+ if (!existing ||
1258
+ composedConfidence > existing.confidence ||
1259
+ (composedConfidence === existing.confidence && rank > existing.rank)) {
1260
+ next.set(mappedKey, {
1261
+ key: mappedKey,
1262
+ record: mappedRecord,
1263
+ matchKind: item.matchKind,
1264
+ confidence: composedConfidence,
1265
+ rank
1266
+ });
1267
+ }
1268
+ }
1269
+ }
1270
+ current = next;
1271
+ if (current.size === 0) {
1272
+ return [];
1273
+ }
1274
+ }
1275
+ return [...current.values()]
1276
+ .sort((left, right) => {
1277
+ if (right.confidence !== left.confidence) {
1278
+ return right.confidence - left.confidence;
1279
+ }
1280
+ if (right.rank !== left.rank) {
1281
+ return right.rank - left.rank;
1282
+ }
1283
+ return left.record.symbol.localeCompare(right.record.symbol);
1284
+ })
1285
+ .slice(0, MAX_CANDIDATES)
1286
+ .map((item) => ({
1287
+ symbol: item.record.symbol,
1288
+ matchKind: item.matchKind,
1289
+ confidence: Number(item.confidence.toFixed(6)),
1290
+ kind: item.record.kind,
1291
+ owner: item.record.owner,
1292
+ name: item.record.name,
1293
+ descriptor: item.record.descriptor
1294
+ }));
1295
+ }
1296
+ provenanceForPath(graph, path) {
1297
+ if (path.length <= 1) {
1298
+ return undefined;
1299
+ }
1300
+ const first = graph.pairs.get(pairKey(path[0], path[1]));
1301
+ if (!first) {
1302
+ return undefined;
1303
+ }
1304
+ return {
1305
+ source: first.source,
1306
+ mappingArtifact: first.mappingArtifact,
1307
+ version: graph.version,
1308
+ priority: graph.priority
1309
+ };
1310
+ }
1311
+ async loadGraph(version, priority) {
1312
+ const cacheKey = `${version}|${priority}`;
1313
+ const cached = this.graphCache.get(cacheKey);
1314
+ if (cached) {
1315
+ this.graphCache.delete(cacheKey);
1316
+ this.graphCache.set(cacheKey, cached);
1317
+ return cached;
1318
+ }
1319
+ const existingLock = this.buildLocks.get(cacheKey);
1320
+ if (existingLock) {
1321
+ return existingLock;
1322
+ }
1323
+ const buildPromise = this.buildGraph(version, priority);
1324
+ this.buildLocks.set(cacheKey, buildPromise);
1325
+ try {
1326
+ const built = await buildPromise;
1327
+ this.graphCache.set(cacheKey, built);
1328
+ this.trimGraphCache();
1329
+ return built;
1330
+ }
1331
+ finally {
1332
+ this.buildLocks.delete(cacheKey);
1333
+ }
1334
+ }
1335
+ async buildGraph(version, priority) {
1336
+ if (isUnobfuscatedVersion(version)) {
1337
+ return {
1338
+ version,
1339
+ priority,
1340
+ pairs: new Map(),
1341
+ warnings: [
1342
+ `Version ${version} is unobfuscated; mapping graph is empty (official names are final).`
1343
+ ]
1344
+ };
1345
+ }
1346
+ const graph = {
1347
+ version,
1348
+ priority,
1349
+ pairs: new Map(),
1350
+ warnings: []
1351
+ };
1352
+ const mojangLoad = await this.loadMojangPairs(version);
1353
+ graph.warnings.push(...mojangLoad.warnings);
1354
+ this.mergePairs(graph.pairs, mojangLoad.pairs, "mojang-client-mappings", mojangLoad.mappingArtifact);
1355
+ let tinyLoaded = false;
1356
+ for (const source of mappingSourceOrder(priority)) {
1357
+ const tinyLoad = source === "loom-cache"
1358
+ ? await this.loadTinyPairsFromLoom(version)
1359
+ : await this.loadTinyPairsFromMaven(version);
1360
+ graph.warnings.push(...tinyLoad.warnings);
1361
+ if (tinyLoad.pairs.size === 0) {
1362
+ continue;
1363
+ }
1364
+ tinyLoaded = true;
1365
+ this.mergePairs(graph.pairs, tinyLoad.pairs, source, tinyLoad.mappingArtifact);
1366
+ break;
1367
+ }
1368
+ if (!tinyLoaded) {
1369
+ graph.warnings.push("No intermediary/yarn tiny mappings were found for this version.");
1370
+ }
1371
+ return graph;
1372
+ }
1373
+ mergePairs(target, source, pairSource, mappingArtifact) {
1374
+ for (const [key, incoming] of source.entries()) {
1375
+ const existing = target.get(key);
1376
+ if (!existing) {
1377
+ target.set(key, {
1378
+ index: incoming,
1379
+ source: pairSource,
1380
+ mappingArtifact
1381
+ });
1382
+ continue;
1383
+ }
1384
+ if (existing.source !== pairSource) {
1385
+ continue;
1386
+ }
1387
+ mergeDirectionIndexes(existing.index, incoming);
1388
+ }
1389
+ }
1390
+ async loadMojangPairs(version) {
1391
+ const warnings = [];
1392
+ let metadata;
1393
+ try {
1394
+ metadata = await this.versionService.resolveVersionMappings(version);
1395
+ }
1396
+ catch (caughtError) {
1397
+ return {
1398
+ pairs: new Map(),
1399
+ warnings: [
1400
+ `Failed to resolve version metadata for "${version}": ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`
1401
+ ],
1402
+ mappingArtifact: `version:${version}`
1403
+ };
1404
+ }
1405
+ const clientMappingsUrl = metadata.clientMappingsUrl ?? metadata.mappingsUrl;
1406
+ if (!clientMappingsUrl) {
1407
+ warnings.push(`Minecraft version "${version}" does not expose client mappings URL.`);
1408
+ return {
1409
+ pairs: new Map(),
1410
+ warnings,
1411
+ mappingArtifact: metadata.versionDetailUrl
1412
+ };
1413
+ }
1414
+ const mappingsPath = join(this.config.cacheDir, "mappings", version, "client_mappings.txt");
1415
+ if (!existsSync(mappingsPath)) {
1416
+ await mkdir(dirname(mappingsPath), { recursive: true });
1417
+ const downloaded = await downloadToCache(clientMappingsUrl, mappingsPath, {
1418
+ fetchFn: this.fetchFn,
1419
+ retries: this.config.fetchRetries,
1420
+ timeoutMs: this.config.fetchTimeoutMs
1421
+ });
1422
+ if (!downloaded.ok || !downloaded.path) {
1423
+ warnings.push(`Failed to download client mappings from "${clientMappingsUrl}" (status: ${downloaded.statusCode ?? "unknown"}).`);
1424
+ return {
1425
+ pairs: new Map(),
1426
+ warnings,
1427
+ mappingArtifact: clientMappingsUrl
1428
+ };
1429
+ }
1430
+ }
1431
+ try {
1432
+ const content = await readFile(mappingsPath, "utf8");
1433
+ return {
1434
+ pairs: parseClientMappings(content),
1435
+ warnings,
1436
+ mappingArtifact: clientMappingsUrl
1437
+ };
1438
+ }
1439
+ catch (caughtError) {
1440
+ warnings.push(`Failed to parse client mappings for "${version}": ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`);
1441
+ return {
1442
+ pairs: new Map(),
1443
+ warnings,
1444
+ mappingArtifact: clientMappingsUrl
1445
+ };
1446
+ }
1447
+ }
1448
+ async loadTinyPairsFromLoom(version) {
1449
+ const patterns = [".gradle/loom-cache/**/*.tiny", ".gradle/loom-cache/**/*.tinyv2"];
1450
+ const candidates = fastGlob.sync(patterns, {
1451
+ cwd: process.cwd(),
1452
+ absolute: true,
1453
+ onlyFiles: true
1454
+ });
1455
+ const byVersion = candidates
1456
+ .filter((p) => p.replaceAll("\\", "/").includes(`/${version}/`))
1457
+ .sort((left, right) => left.localeCompare(right));
1458
+ if (byVersion.length === 0) {
1459
+ return {
1460
+ pairs: new Map(),
1461
+ warnings: [`No Loom tiny mapping files matched version "${version}".`],
1462
+ mappingArtifact: "loom-cache:none"
1463
+ };
1464
+ }
1465
+ const merged = new Map();
1466
+ for (const path of byVersion) {
1467
+ try {
1468
+ const content = await readFile(path, "utf8");
1469
+ const parsed = parseTinyMappings(content);
1470
+ for (const [key, index] of parsed.entries()) {
1471
+ const existing = merged.get(key);
1472
+ if (!existing) {
1473
+ merged.set(key, index);
1474
+ }
1475
+ else {
1476
+ mergeDirectionIndexes(existing, index);
1477
+ }
1478
+ }
1479
+ }
1480
+ catch {
1481
+ // best effort: skip unreadable or invalid files
1482
+ }
1483
+ }
1484
+ return {
1485
+ pairs: merged,
1486
+ warnings: [],
1487
+ mappingArtifact: byVersion[0]
1488
+ };
1489
+ }
1490
+ async loadTinyPairsFromMaven(version) {
1491
+ const warnings = [];
1492
+ const merged = new Map();
1493
+ const attemptedArtifacts = [];
1494
+ const repos = this.config.sourceRepos;
1495
+ const intermediaryUrls = [];
1496
+ const yarnUrls = [];
1497
+ for (const repo of repos) {
1498
+ const base = repo.replace(/\/+$/, "");
1499
+ intermediaryUrls.push(`${base}/net/fabricmc/intermediary/${version}/intermediary-${version}-v2.jar`, `${base}/net/fabricmc/intermediary/${version}/intermediary-${version}.jar`);
1500
+ const yarnCoordinates = await this.fetchYarnCoordinates(base, version);
1501
+ for (const coordinate of yarnCoordinates) {
1502
+ yarnUrls.push(`${base}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}-v2.jar`, `${base}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}.jar`);
1503
+ }
1504
+ }
1505
+ const allUrls = [...intermediaryUrls, ...yarnUrls];
1506
+ for (const url of allUrls) {
1507
+ attemptedArtifacts.push(url);
1508
+ const downloaded = await downloadToCache(url, defaultDownloadPath(this.config.cacheDir, url), {
1509
+ fetchFn: this.fetchFn,
1510
+ retries: this.config.fetchRetries,
1511
+ timeoutMs: this.config.fetchTimeoutMs
1512
+ });
1513
+ if (!downloaded.ok || !downloaded.path) {
1514
+ continue;
1515
+ }
1516
+ const parsed = await this.parseTinyFromJar(downloaded.path);
1517
+ for (const [key, index] of parsed.entries()) {
1518
+ const existing = merged.get(key);
1519
+ if (!existing) {
1520
+ merged.set(key, index);
1521
+ }
1522
+ else {
1523
+ mergeDirectionIndexes(existing, index);
1524
+ }
1525
+ }
1526
+ }
1527
+ if (merged.size === 0) {
1528
+ warnings.push(`No Maven tiny mappings could be loaded for "${version}".`);
1529
+ }
1530
+ return {
1531
+ pairs: merged,
1532
+ warnings,
1533
+ mappingArtifact: attemptedArtifacts[0] ?? "maven:none"
1534
+ };
1535
+ }
1536
+ async parseTinyFromJar(jarPath) {
1537
+ const entries = await listJarEntries(jarPath);
1538
+ const tinyEntries = entries
1539
+ .filter((entry) => entry.toLowerCase().endsWith(".tiny") || entry.toLowerCase().endsWith(".tinyv2"))
1540
+ .sort((left, right) => left.localeCompare(right));
1541
+ const merged = new Map();
1542
+ for (const entry of tinyEntries) {
1543
+ try {
1544
+ const text = await readJarEntryAsUtf8(jarPath, entry);
1545
+ const parsed = parseTinyMappings(text);
1546
+ for (const [key, index] of parsed.entries()) {
1547
+ const existing = merged.get(key);
1548
+ if (!existing) {
1549
+ merged.set(key, index);
1550
+ }
1551
+ else {
1552
+ mergeDirectionIndexes(existing, index);
1553
+ }
1554
+ }
1555
+ }
1556
+ catch {
1557
+ // skip malformed tiny entries
1558
+ }
1559
+ }
1560
+ return merged;
1561
+ }
1562
+ async fetchYarnCoordinates(repoBase, version) {
1563
+ const metadataUrl = `${repoBase}/net/fabricmc/yarn/maven-metadata.xml`;
1564
+ try {
1565
+ const response = await this.fetchFn(metadataUrl);
1566
+ if (!response.ok) {
1567
+ return [];
1568
+ }
1569
+ const xml = await response.text();
1570
+ const versions = [...xml.matchAll(/<version>([^<]+)<\/version>/g)]
1571
+ .map((match) => match[1]?.trim() ?? "")
1572
+ .filter((value) => value.startsWith(`${version}+build.`));
1573
+ const sorted = versions.sort((left, right) => {
1574
+ const leftBuild = Number.parseInt(left.split("+build.")[1] ?? "0", 10);
1575
+ const rightBuild = Number.parseInt(right.split("+build.")[1] ?? "0", 10);
1576
+ return rightBuild - leftBuild;
1577
+ });
1578
+ if (sorted.length > 0) {
1579
+ return sorted.slice(0, 3);
1580
+ }
1581
+ return [version];
1582
+ }
1583
+ catch {
1584
+ return [version];
1585
+ }
1586
+ }
1587
+ trimGraphCache() {
1588
+ const maxEntries = Math.max(1, this.config.maxMappingGraphCache ?? 16);
1589
+ while (this.graphCache.size > maxEntries) {
1590
+ const oldestKey = this.graphCache.keys().next().value;
1591
+ if (!oldestKey) {
1592
+ return;
1593
+ }
1594
+ this.graphCache.delete(oldestKey);
1595
+ }
1596
+ }
1597
+ }
1598
+ // ---------------------------------------------------------------------------
1599
+ // Standalone: Tiny v2 mapping file resolution for remapping
1600
+ // ---------------------------------------------------------------------------
1601
+ const FABRIC_MAVEN = "https://maven.fabricmc.net";
1602
+ async function fetchYarnCoordinatesStandalone(version, fetchFn = globalThis.fetch) {
1603
+ const metadataUrl = `${FABRIC_MAVEN}/net/fabricmc/yarn/maven-metadata.xml`;
1604
+ try {
1605
+ const response = await fetchFn(metadataUrl);
1606
+ if (!response.ok) {
1607
+ return [];
1608
+ }
1609
+ const xml = await response.text();
1610
+ const versions = [...xml.matchAll(/<version>([^<]+)<\/version>/g)]
1611
+ .map((match) => match[1]?.trim() ?? "")
1612
+ .filter((value) => value.startsWith(`${version}+build.`));
1613
+ const sorted = versions.sort((left, right) => {
1614
+ const leftBuild = Number.parseInt(left.split("+build.")[1] ?? "0", 10);
1615
+ const rightBuild = Number.parseInt(right.split("+build.")[1] ?? "0", 10);
1616
+ return rightBuild - leftBuild;
1617
+ });
1618
+ return sorted.length > 0 ? sorted.slice(0, 3) : [];
1619
+ }
1620
+ catch {
1621
+ return [];
1622
+ }
1623
+ }
1624
+ async function extractTinyFromJar(jarPath, outputPath) {
1625
+ const entries = await listJarEntries(jarPath);
1626
+ const tinyEntry = entries.find((entry) => entry === "mappings/mappings.tiny" || entry.toLowerCase().endsWith(".tiny"));
1627
+ if (!tinyEntry) {
1628
+ return false;
1629
+ }
1630
+ const content = await readJarEntryAsUtf8(jarPath, tinyEntry);
1631
+ await mkdir(dirname(outputPath), { recursive: true });
1632
+ await writeFile(outputPath, content, "utf8");
1633
+ return true;
1634
+ }
1635
+ /**
1636
+ * Resolve and cache a Tiny v2 mapping file for the given Minecraft version.
1637
+ *
1638
+ * @param version - Minecraft version (e.g. "1.20.4")
1639
+ * @param mapping - "intermediary" or "yarn"
1640
+ * @param cacheDir - The application cache directory
1641
+ * @param fetchFn - Optional fetch implementation for testing
1642
+ * @returns Path to the extracted Tiny v2 file
1643
+ */
1644
+ export async function resolveTinyMappingFile(version, mapping, cacheDir, fetchFn) {
1645
+ const cachedTiny = join(cacheDir, "mappings", `${version}-${mapping}.tiny`);
1646
+ if (existsSync(cachedTiny)) {
1647
+ return cachedTiny;
1648
+ }
1649
+ const effectiveFetch = fetchFn ?? globalThis.fetch;
1650
+ if (mapping === "intermediary") {
1651
+ const url = `${FABRIC_MAVEN}/net/fabricmc/intermediary/${version}/intermediary-${version}-v2.jar`;
1652
+ const jarDest = defaultDownloadPath(cacheDir, url);
1653
+ const downloaded = await downloadToCache(url, jarDest, {
1654
+ fetchFn: effectiveFetch,
1655
+ retries: 2,
1656
+ timeoutMs: 30_000
1657
+ });
1658
+ if (!downloaded.ok || !downloaded.path) {
1659
+ throw createError({
1660
+ code: ERROR_CODES.MAPPING_UNAVAILABLE,
1661
+ message: `Failed to download intermediary mappings for ${version}.`,
1662
+ details: { version, url }
1663
+ });
1664
+ }
1665
+ const extracted = await extractTinyFromJar(downloaded.path, cachedTiny);
1666
+ if (!extracted) {
1667
+ throw createError({
1668
+ code: ERROR_CODES.MAPPING_UNAVAILABLE,
1669
+ message: `No tiny mapping found in intermediary JAR for ${version}.`,
1670
+ details: { version, jarPath: downloaded.path }
1671
+ });
1672
+ }
1673
+ return cachedTiny;
1674
+ }
1675
+ // yarn
1676
+ const yarnCoordinates = await fetchYarnCoordinatesStandalone(version, effectiveFetch);
1677
+ if (yarnCoordinates.length === 0) {
1678
+ throw createError({
1679
+ code: ERROR_CODES.MAPPING_UNAVAILABLE,
1680
+ message: `No yarn builds found for Minecraft ${version}.`,
1681
+ details: { version }
1682
+ });
1683
+ }
1684
+ for (const coordinate of yarnCoordinates) {
1685
+ const url = `${FABRIC_MAVEN}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}-v2.jar`;
1686
+ const jarDest = defaultDownloadPath(cacheDir, url);
1687
+ const downloaded = await downloadToCache(url, jarDest, {
1688
+ fetchFn: effectiveFetch,
1689
+ retries: 2,
1690
+ timeoutMs: 30_000
1691
+ });
1692
+ if (!downloaded.ok || !downloaded.path) {
1693
+ continue;
1694
+ }
1695
+ const extracted = await extractTinyFromJar(downloaded.path, cachedTiny);
1696
+ if (extracted) {
1697
+ return cachedTiny;
1698
+ }
1699
+ }
1700
+ throw createError({
1701
+ code: ERROR_CODES.MAPPING_UNAVAILABLE,
1702
+ message: `Failed to download yarn mappings for ${version}.`,
1703
+ details: { version, triedCoordinates: yarnCoordinates }
1704
+ });
1705
+ }
1706
+ //# sourceMappingURL=mapping-service.js.map