@adhisang/minecraft-modding-mcp 3.1.0 → 3.2.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 (44) hide show
  1. package/CHANGELOG.md +62 -34
  2. package/README.md +79 -100
  3. package/dist/access-transformer-parser.d.ts +17 -0
  4. package/dist/access-transformer-parser.js +97 -0
  5. package/dist/concurrency.d.ts +1 -0
  6. package/dist/concurrency.js +24 -0
  7. package/dist/config.js +19 -11
  8. package/dist/decompiler/vineflower.js +22 -21
  9. package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
  10. package/dist/entry-tools/analyze-symbol-service.d.ts +22 -20
  11. package/dist/entry-tools/analyze-symbol-service.js +6 -3
  12. package/dist/entry-tools/inspect-minecraft-service.d.ts +166 -149
  13. package/dist/entry-tools/inspect-minecraft-service.js +318 -55
  14. package/dist/entry-tools/validate-project-service.d.ts +153 -16
  15. package/dist/entry-tools/validate-project-service.js +360 -23
  16. package/dist/gradle-paths.d.ts +4 -0
  17. package/dist/gradle-paths.js +57 -0
  18. package/dist/index.js +274 -13
  19. package/dist/mapping-pipeline-service.d.ts +3 -1
  20. package/dist/mapping-pipeline-service.js +16 -1
  21. package/dist/mapping-service.d.ts +5 -0
  22. package/dist/mapping-service.js +200 -84
  23. package/dist/minecraft-explorer-service.d.ts +13 -0
  24. package/dist/minecraft-explorer-service.js +8 -4
  25. package/dist/mixin-validator.d.ts +33 -2
  26. package/dist/mixin-validator.js +197 -11
  27. package/dist/mod-analyzer.d.ts +1 -0
  28. package/dist/mod-analyzer.js +17 -1
  29. package/dist/mod-decompile-service.js +4 -4
  30. package/dist/mod-remap-service.js +1 -54
  31. package/dist/mod-search-service.d.ts +1 -0
  32. package/dist/mod-search-service.js +84 -51
  33. package/dist/response-utils.d.ts +35 -0
  34. package/dist/response-utils.js +113 -0
  35. package/dist/source-jar-reader.d.ts +16 -0
  36. package/dist/source-jar-reader.js +103 -1
  37. package/dist/source-resolver.js +9 -10
  38. package/dist/source-service.d.ts +24 -2
  39. package/dist/source-service.js +1052 -139
  40. package/dist/tool-contract-manifest.js +74 -74
  41. package/dist/types.d.ts +17 -0
  42. package/dist/workspace-mapping-service.d.ts +13 -0
  43. package/dist/workspace-mapping-service.js +146 -14
  44. package/package.json +1 -1
@@ -3,8 +3,9 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import fastGlob from "fast-glob";
5
5
  import { createError, ERROR_CODES } from "./errors.js";
6
+ import { buildVersionSourceSearchRoots, normalizeOptionalProjectPath } from "./gradle-paths.js";
6
7
  import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
7
- import { listJarEntries, readJarEntryAsUtf8 } from "./source-jar-reader.js";
8
+ import { collectMatchedJarEntriesAsUtf8 } from "./source-jar-reader.js";
8
9
  import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
9
10
  const SUPPORTED_MAPPINGS = new Set([
10
11
  "obfuscated",
@@ -19,6 +20,7 @@ const MATCH_RANK = {
19
20
  };
20
21
  const DESCRIPTOR_FALLBACK_CONFIDENCE = 0.85;
21
22
  const MAX_CANDIDATES = 200;
23
+ const GLOB_SPECIAL_CHARS = /[\\!*+?()[\]{}@|]/g;
22
24
  function createDirectionIndex() {
23
25
  return {
24
26
  exact: new Map(),
@@ -420,7 +422,7 @@ function parseClientMappings(text) {
420
422
  }
421
423
  function normalizeTinyNamespace(namespace) {
422
424
  const normalized = namespace.trim().toLowerCase();
423
- if (normalized === "obfuscated") {
425
+ if (normalized === "obfuscated" || normalized === "official") {
424
426
  return "obfuscated";
425
427
  }
426
428
  if (normalized === "mojang") {
@@ -590,6 +592,12 @@ function mappingSourceOrder(priority) {
590
592
  }
591
593
  return ["loom-cache", "maven"];
592
594
  }
595
+ function requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) {
596
+ return sourceMapping !== "intermediary" &&
597
+ sourceMapping !== "yarn" &&
598
+ targetMapping !== "intermediary" &&
599
+ targetMapping !== "yarn";
600
+ }
593
601
  function namespacePath(graph, sourceMapping, targetMapping) {
594
602
  if (sourceMapping === targetMapping) {
595
603
  return [sourceMapping];
@@ -709,7 +717,7 @@ function normalizeMethodDescriptor(descriptor) {
709
717
  }
710
718
  return normalized;
711
719
  }
712
- function normalizeQuerySymbol(input, signatureMode) {
720
+ function normalizeQuerySymbol(input, signatureMode, options) {
713
721
  if (input.kind !== "class" && input.kind !== "field" && input.kind !== "method") {
714
722
  throw invalidInputError('kind must be one of "class", "field", or "method".', {
715
723
  kind: input.kind
@@ -735,7 +743,7 @@ function normalizeQuerySymbol(input, signatureMode) {
735
743
  });
736
744
  }
737
745
  const className = normalizeMappedSymbolOutput(normalizedName);
738
- if (!className.includes(".")) {
746
+ if (!className.includes(".") && !options?.allowShortClassName) {
739
747
  throw invalidInputError("name must be a fully qualified class name when kind=class.", {
740
748
  name: input.name
741
749
  });
@@ -805,13 +813,32 @@ function applyDisambiguationHints(candidates, disambiguation) {
805
813
  }
806
814
  const descriptorHint = normalizeDescriptorHint(disambiguation.descriptorHint);
807
815
  if (descriptorHint) {
808
- const descriptorMatched = filtered.filter((candidate) => candidate.descriptor === descriptorHint);
816
+ const descriptorMatched = filtered.filter((candidate) => candidate.descriptor != null && candidate.descriptor === descriptorHint);
809
817
  if (descriptorMatched.length > 0) {
810
818
  filtered = descriptorMatched;
811
819
  }
812
820
  }
813
821
  return filtered;
814
822
  }
823
+ function projectLookupCandidateDescriptor(candidate, sourceDescriptor, targetDescriptor) {
824
+ // Tiny mappings preserve method descriptors verbatim, so single-hop tiny paths often
825
+ // return the source descriptor even though the final symbol is already in the target
826
+ // namespace. Multi-hop paths that already produced a target-side descriptor are left
827
+ // unchanged by design.
828
+ if (candidate.kind !== "method" ||
829
+ !candidate.descriptor ||
830
+ !targetDescriptor ||
831
+ candidate.descriptor !== sourceDescriptor) {
832
+ return candidate;
833
+ }
834
+ return {
835
+ ...candidate,
836
+ descriptor: targetDescriptor
837
+ };
838
+ }
839
+ function effectiveLoomSearchProjectPath(projectPath) {
840
+ return normalizeOptionalProjectPath(projectPath) ?? normalizeOptionalProjectPath(process.cwd());
841
+ }
815
842
  function collectTargetRecords(graph, targetMapping) {
816
843
  return [...(graph.recordsByTarget.get(targetMapping) ?? [])];
817
844
  }
@@ -905,7 +932,9 @@ export class MappingService {
905
932
  }
906
933
  });
907
934
  }
908
- const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, input.signatureMode);
935
+ const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, input.signatureMode, {
936
+ allowShortClassName: input.kind === "class" && input.sourceMapping === "obfuscated"
937
+ });
909
938
  const sourceMapping = input.sourceMapping;
910
939
  const targetMapping = input.targetMapping;
911
940
  if (!SUPPORTED_MAPPINGS.has(sourceMapping) || !SUPPORTED_MAPPINGS.has(targetMapping)) {
@@ -945,7 +974,7 @@ export class MappingService {
945
974
  warnings: []
946
975
  };
947
976
  }
948
- const graph = await this.loadGraph(version, priority);
977
+ const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath);
949
978
  const path = namespacePath(graph, sourceMapping, targetMapping);
950
979
  if (!path) {
951
980
  return {
@@ -960,7 +989,15 @@ export class MappingService {
960
989
  ]
961
990
  };
962
991
  }
963
- const rawCandidates = this.mapCandidatesAlongPath(graph, path, queryRecord);
992
+ const descriptorProjection = queryRecord.kind === "method" && queryRecord.descriptor
993
+ ? this.projectMethodDescriptorToTarget(graph, path, queryRecord.descriptor)
994
+ : undefined;
995
+ const projectedDescriptor = descriptorProjection?.complete ? descriptorProjection.descriptor : undefined;
996
+ const rawCandidates = this
997
+ .mapCandidatesAlongPath(graph, path, queryRecord)
998
+ .map((candidate) => queryRecord.kind === "method" && queryRecord.descriptor
999
+ ? projectLookupCandidateDescriptor(candidate, queryRecord.descriptor, projectedDescriptor)
1000
+ : candidate);
964
1001
  const warnings = [];
965
1002
  const disambiguatedCandidates = applyDisambiguationHints(rawCandidates, input.disambiguation);
966
1003
  if (rawCandidates.length > disambiguatedCandidates.length) {
@@ -1029,7 +1066,7 @@ export class MappingService {
1029
1066
  warnings: []
1030
1067
  };
1031
1068
  }
1032
- const graph = await this.loadGraph(version, priority);
1069
+ const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath);
1033
1070
  const path = namespacePath(graph, sourceMapping, targetMapping);
1034
1071
  if (!path) {
1035
1072
  throw createError({
@@ -1113,7 +1150,7 @@ export class MappingService {
1113
1150
  warnings: []
1114
1151
  };
1115
1152
  }
1116
- const graph = await this.loadGraph(version, priority);
1153
+ const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath);
1117
1154
  const path = namespacePath(graph, sourceMapping, targetMapping);
1118
1155
  if (!path) {
1119
1156
  return {
@@ -1129,12 +1166,16 @@ export class MappingService {
1129
1166
  };
1130
1167
  }
1131
1168
  const warnings = [];
1169
+ const descriptorProjection = this.projectMethodDescriptorToTarget(graph, path, descriptor);
1170
+ const projectedDescriptor = descriptorProjection.complete ? descriptorProjection.descriptor : undefined;
1132
1171
  const rawCandidates = this
1133
1172
  .mapCandidatesAlongPath(graph, path, queryRecord)
1134
- .filter((candidate) => candidate.kind === "method");
1173
+ .filter((candidate) => candidate.kind === "method")
1174
+ .map((candidate) => projectLookupCandidateDescriptor(candidate, descriptor, projectedDescriptor));
1135
1175
  const candidates = rawCandidates.map(toResolutionCandidate);
1136
1176
  const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
1137
- const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor === descriptor);
1177
+ const strictDescriptor = projectedDescriptor ?? descriptor;
1178
+ const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor === strictDescriptor);
1138
1179
  if (strictCandidates.length === 1) {
1139
1180
  const resolved = toResolutionCandidate(strictCandidates[0]);
1140
1181
  return {
@@ -1164,8 +1205,10 @@ export class MappingService {
1164
1205
  provenance: this.provenanceForPath(graph, path)
1165
1206
  };
1166
1207
  }
1167
- if (pathUsesSource(graph.pairs, path, "mojang-client-mappings")) {
1168
- warnings.push("Method descriptor could not be preserved through mojang-client-mappings and exact resolution is unavailable.");
1208
+ if (descriptorProjection.hadClassReferences && !descriptorProjection.complete) {
1209
+ warnings.push(pathUsesSource(graph.pairs, path, "mojang-client-mappings")
1210
+ ? "Method descriptor could not be preserved through mojang-client-mappings and exact resolution is unavailable."
1211
+ : "Method descriptor could not be fully remapped across the mapping path and exact resolution is unavailable.");
1169
1212
  return {
1170
1213
  querySymbol,
1171
1214
  mappingContext,
@@ -1214,7 +1257,7 @@ export class MappingService {
1214
1257
  });
1215
1258
  }
1216
1259
  const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
1217
- const graph = await this.loadGraph(version, priority);
1260
+ const graph = await this.loadGraph(version, priority, "full");
1218
1261
  const warnings = [...graph.warnings];
1219
1262
  const includeKinds = normalizeIncludedKinds(input.includeKinds);
1220
1263
  const pathCache = new Map();
@@ -1246,7 +1289,7 @@ export class MappingService {
1246
1289
  classByMapping[mapping] = mapped[0];
1247
1290
  }
1248
1291
  }
1249
- const baseMapping = classByMapping.obfuscated ? "obfuscated" : classNameMapping;
1292
+ const baseMapping = classNameMapping;
1250
1293
  const baseClass = classByMapping[baseMapping];
1251
1294
  if (!baseClass) {
1252
1295
  return {
@@ -1435,7 +1478,7 @@ export class MappingService {
1435
1478
  querySymbol
1436
1479
  };
1437
1480
  })();
1438
- const graph = await this.loadGraph(version, priority);
1481
+ const graph = await this.loadGraph(version, priority, sourceMapping === "mojang" ? "obfuscated-mojang-only" : "full");
1439
1482
  const warnings = [...graph.warnings];
1440
1483
  const records = collectTargetRecords(graph, sourceMapping);
1441
1484
  if (records.length === 0) {
@@ -1616,6 +1659,33 @@ export class MappingService {
1616
1659
  descriptor: item.record.descriptor
1617
1660
  }));
1618
1661
  }
1662
+ projectMethodDescriptorToTarget(graph, path, descriptor) {
1663
+ let hadClassReferences = false;
1664
+ let complete = true;
1665
+ const classProjectionCache = new Map();
1666
+ const projectedDescriptor = descriptor.replace(/L([^;]+);/g, (fullMatch, internalName) => {
1667
+ hadClassReferences = true;
1668
+ const cached = classProjectionCache.get(internalName);
1669
+ if (cached) {
1670
+ return `L${cached};`;
1671
+ }
1672
+ const projectedClassCandidates = this
1673
+ .mapCandidatesAlongPath(graph, path, createClassSymbolRecord(internalName.replace(/\//g, ".")))
1674
+ .filter((candidate) => candidate.kind === "class");
1675
+ if (projectedClassCandidates.length !== 1) {
1676
+ complete = false;
1677
+ return fullMatch;
1678
+ }
1679
+ const projectedInternalName = projectedClassCandidates[0].symbol.replace(/\./g, "/");
1680
+ classProjectionCache.set(internalName, projectedInternalName);
1681
+ return `L${projectedInternalName};`;
1682
+ });
1683
+ return {
1684
+ descriptor: projectedDescriptor,
1685
+ hadClassReferences,
1686
+ complete
1687
+ };
1688
+ }
1619
1689
  provenanceForPath(graph, path) {
1620
1690
  if (path.length <= 1) {
1621
1691
  return undefined;
@@ -1638,9 +1708,21 @@ export class MappingService {
1638
1708
  async checkMappingHealth(input) {
1639
1709
  const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
1640
1710
  const degradations = [];
1711
+ if (isUnobfuscatedVersion(input.version)) {
1712
+ const requestFulfillable = input.requestedMapping === "obfuscated" || input.requestedMapping === "mojang";
1713
+ if (!requestFulfillable) {
1714
+ degradations.push(`Version ${input.version} is unobfuscated; ${input.requestedMapping} mappings are not applicable.`);
1715
+ }
1716
+ return {
1717
+ mojangMappingsAvailable: true,
1718
+ tinyMappingsAvailable: false,
1719
+ memberRemapAvailable: requestFulfillable,
1720
+ degradations
1721
+ };
1722
+ }
1641
1723
  let graph;
1642
1724
  try {
1643
- graph = await this.loadGraph(input.version, priority);
1725
+ graph = await this.loadGraph(input.version, priority, "full");
1644
1726
  }
1645
1727
  catch {
1646
1728
  return {
@@ -1684,8 +1766,9 @@ export class MappingService {
1684
1766
  degradations
1685
1767
  };
1686
1768
  }
1687
- async loadGraph(version, priority) {
1688
- const cacheKey = `${version}|${priority}`;
1769
+ async loadGraph(version, priority, mode, projectPath) {
1770
+ const effectiveProjectPath = effectiveLoomSearchProjectPath(projectPath);
1771
+ const cacheKey = `${version}|${priority}|${mode}|${effectiveProjectPath ?? ""}`;
1689
1772
  const cached = this.graphCache.get(cacheKey);
1690
1773
  if (cached) {
1691
1774
  this.graphCache.delete(cacheKey);
@@ -1696,7 +1779,7 @@ export class MappingService {
1696
1779
  if (existingLock) {
1697
1780
  return existingLock;
1698
1781
  }
1699
- const buildPromise = this.buildGraph(version, priority);
1782
+ const buildPromise = this.buildGraph(version, priority, mode, effectiveProjectPath);
1700
1783
  this.buildLocks.set(cacheKey, buildPromise);
1701
1784
  try {
1702
1785
  const built = await buildPromise;
@@ -1708,11 +1791,12 @@ export class MappingService {
1708
1791
  this.buildLocks.delete(cacheKey);
1709
1792
  }
1710
1793
  }
1711
- async buildGraph(version, priority) {
1794
+ async buildGraph(version, priority, mode, projectPath) {
1712
1795
  if (isUnobfuscatedVersion(version)) {
1713
1796
  return {
1714
1797
  version,
1715
1798
  priority,
1799
+ mode,
1716
1800
  pairs: new Map(),
1717
1801
  adjacency: new Map(),
1718
1802
  pathCache: new Map(),
@@ -1725,6 +1809,7 @@ export class MappingService {
1725
1809
  const graph = {
1726
1810
  version,
1727
1811
  priority,
1812
+ mode,
1728
1813
  pairs: new Map(),
1729
1814
  adjacency: new Map(),
1730
1815
  pathCache: new Map(),
@@ -1734,27 +1819,29 @@ export class MappingService {
1734
1819
  const mojangLoad = await this.loadMojangPairs(version);
1735
1820
  graph.warnings.push(...mojangLoad.warnings);
1736
1821
  this.mergePairs(graph.pairs, mojangLoad.pairs, "mojang-client-mappings", mojangLoad.mappingArtifact);
1737
- let tinyLoaded = false;
1738
- const deferredTinyWarnings = [];
1739
- for (const source of mappingSourceOrder(priority)) {
1740
- const tinyLoad = source === "loom-cache"
1741
- ? await this.loadTinyPairsFromLoom(version)
1742
- : await this.loadTinyPairsFromMaven(version);
1743
- if (tinyLoad.pairs.size === 0) {
1744
- deferredTinyWarnings.push(...tinyLoad.warnings);
1745
- continue;
1822
+ if (mode === "full") {
1823
+ let tinyLoaded = false;
1824
+ const deferredTinyWarnings = [];
1825
+ for (const source of mappingSourceOrder(priority)) {
1826
+ const tinyLoad = source === "loom-cache"
1827
+ ? await this.loadTinyPairsFromLoom(version, projectPath)
1828
+ : await this.loadTinyPairsFromMaven(version);
1829
+ if (tinyLoad.pairs.size === 0) {
1830
+ deferredTinyWarnings.push(...tinyLoad.warnings);
1831
+ continue;
1832
+ }
1833
+ tinyLoaded = true;
1834
+ this.mergePairs(graph.pairs, tinyLoad.pairs, source, tinyLoad.mappingArtifact);
1835
+ graph.warnings.push(...tinyLoad.warnings);
1836
+ if (deferredTinyWarnings.length > 0) {
1837
+ graph.warnings.push(`Used ${source === "maven" ? "Maven" : "Loom cache"} tiny mappings for "${version}" after an earlier source lookup returned no data.`);
1838
+ }
1839
+ break;
1746
1840
  }
1747
- tinyLoaded = true;
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.`);
1841
+ if (!tinyLoaded) {
1842
+ graph.warnings.push(...deferredTinyWarnings);
1843
+ graph.warnings.push("No intermediary/yarn tiny mappings were found for this version.");
1752
1844
  }
1753
- break;
1754
- }
1755
- if (!tinyLoaded) {
1756
- graph.warnings.push(...deferredTinyWarnings);
1757
- graph.warnings.push("No intermediary/yarn tiny mappings were found for this version.");
1758
1845
  }
1759
1846
  graph.adjacency = buildAdjacency(graph.pairs);
1760
1847
  graph.recordsByTarget = buildTargetRecordIndex(graph.pairs);
@@ -1835,46 +1922,67 @@ export class MappingService {
1835
1922
  };
1836
1923
  }
1837
1924
  }
1838
- async loadTinyPairsFromLoom(version) {
1839
- const patterns = [".gradle/loom-cache/**/*.tiny", ".gradle/loom-cache/**/*.tinyv2"];
1840
- const candidates = fastGlob.sync(patterns, {
1841
- cwd: process.cwd(),
1842
- absolute: true,
1843
- onlyFiles: true
1844
- });
1845
- const byVersion = candidates
1846
- .filter((p) => p.replaceAll("\\", "/").includes(`/${version}/`))
1847
- .sort((left, right) => left.localeCompare(right));
1848
- if (byVersion.length === 0) {
1849
- return {
1850
- pairs: new Map(),
1851
- warnings: [`No Loom tiny mapping files matched version "${version}".`],
1852
- mappingArtifact: "loom-cache:none"
1853
- };
1854
- }
1925
+ async loadTinyPairsFromLoom(version, projectPath) {
1926
+ const searchRoots = buildVersionSourceSearchRoots(effectiveLoomSearchProjectPath(projectPath));
1855
1927
  const merged = new Map();
1856
- for (const path of byVersion) {
1928
+ const discoveredPaths = new Set();
1929
+ for (const root of searchRoots) {
1930
+ let discovered = [];
1931
+ const versionRoot = join(root, version);
1857
1932
  try {
1858
- const content = await readFile(path, "utf8");
1859
- const parsed = parseTinyMappings(content);
1860
- for (const [key, index] of parsed.entries()) {
1861
- const existing = merged.get(key);
1862
- if (!existing) {
1863
- merged.set(key, index);
1864
- }
1865
- else {
1866
- mergeDirectionIndexes(existing, index);
1867
- }
1868
- }
1933
+ discovered = existsSync(versionRoot)
1934
+ ? await fastGlob.glob(["**/*.tiny", "**/*.tinyv2"], {
1935
+ cwd: versionRoot,
1936
+ absolute: true,
1937
+ onlyFiles: true
1938
+ })
1939
+ : await fastGlob.glob([`${version.replace(GLOB_SPECIAL_CHARS, "\\$&")}/**/*.tiny`, `${version.replace(GLOB_SPECIAL_CHARS, "\\$&")}/**/*.tinyv2`], {
1940
+ cwd: root,
1941
+ absolute: true,
1942
+ onlyFiles: true
1943
+ });
1869
1944
  }
1870
1945
  catch {
1871
- // best effort: skip unreadable or invalid files
1946
+ continue;
1947
+ }
1948
+ const byVersion = discovered
1949
+ .filter((path) => path.replaceAll("\\", "/").includes(`/${version}/`))
1950
+ .sort((left, right) => left.localeCompare(right));
1951
+ if (byVersion.length === 0) {
1952
+ continue;
1953
+ }
1954
+ for (const path of byVersion) {
1955
+ discoveredPaths.add(path);
1956
+ try {
1957
+ const content = await readFile(path, "utf8");
1958
+ const parsed = parseTinyMappings(content);
1959
+ for (const [key, index] of parsed.entries()) {
1960
+ const existing = merged.get(key);
1961
+ if (!existing) {
1962
+ merged.set(key, index);
1963
+ }
1964
+ else {
1965
+ mergeDirectionIndexes(existing, index);
1966
+ }
1967
+ }
1968
+ }
1969
+ catch {
1970
+ // best effort: skip unreadable or invalid files
1971
+ }
1872
1972
  }
1873
1973
  }
1974
+ const orderedPaths = [...discoveredPaths].sort((left, right) => left.localeCompare(right));
1975
+ if (orderedPaths.length > 0) {
1976
+ return {
1977
+ pairs: merged,
1978
+ warnings: [],
1979
+ mappingArtifact: orderedPaths[0]
1980
+ };
1981
+ }
1874
1982
  return {
1875
- pairs: merged,
1876
- warnings: [],
1877
- mappingArtifact: byVersion[0]
1983
+ pairs: new Map(),
1984
+ warnings: [`No Loom tiny mapping files matched version "${version}".`],
1985
+ mappingArtifact: "loom-cache:none"
1878
1986
  };
1879
1987
  }
1880
1988
  async loadTinyPairsFromMaven(version) {
@@ -1930,15 +2038,11 @@ export class MappingService {
1930
2038
  };
1931
2039
  }
1932
2040
  async parseTinyFromJar(jarPath) {
1933
- const entries = await listJarEntries(jarPath);
1934
- const tinyEntries = entries
1935
- .filter((entry) => entry.toLowerCase().endsWith(".tiny") || entry.toLowerCase().endsWith(".tinyv2"))
1936
- .sort((left, right) => left.localeCompare(right));
2041
+ const tinyEntries = (await collectMatchedJarEntriesAsUtf8(jarPath, (entry) => entry.toLowerCase().endsWith(".tiny") || entry.toLowerCase().endsWith(".tinyv2"), { continueOnError: true })).sort((left, right) => left.filePath.localeCompare(right.filePath));
1937
2042
  const merged = new Map();
1938
2043
  for (const entry of tinyEntries) {
1939
2044
  try {
1940
- const text = await readJarEntryAsUtf8(jarPath, entry);
1941
- const parsed = parseTinyMappings(text);
2045
+ const parsed = parseTinyMappings(entry.content);
1942
2046
  for (const [key, index] of parsed.entries()) {
1943
2047
  const existing = merged.get(key);
1944
2048
  if (!existing) {
@@ -1990,6 +2094,19 @@ export class MappingService {
1990
2094
  this.graphCache.delete(oldestKey);
1991
2095
  }
1992
2096
  }
2097
+ releaseGraphCacheEntry(version, sourcePriority) {
2098
+ const normalizedVersion = version.trim();
2099
+ if (!normalizedVersion) {
2100
+ return;
2101
+ }
2102
+ const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, sourcePriority);
2103
+ const prefix = `${normalizedVersion}|${priority}|`;
2104
+ for (const key of this.graphCache.keys()) {
2105
+ if (key.startsWith(prefix)) {
2106
+ this.graphCache.delete(key);
2107
+ }
2108
+ }
2109
+ }
1993
2110
  }
1994
2111
  // ---------------------------------------------------------------------------
1995
2112
  // Standalone: Tiny v2 mapping file resolution for remapping
@@ -2018,14 +2135,13 @@ async function fetchYarnCoordinatesStandalone(version, fetchFn = globalThis.fetc
2018
2135
  }
2019
2136
  }
2020
2137
  async function extractTinyFromJar(jarPath, outputPath) {
2021
- const entries = await listJarEntries(jarPath);
2022
- const tinyEntry = entries.find((entry) => entry === "mappings/mappings.tiny" || entry.toLowerCase().endsWith(".tiny"));
2138
+ const matchedEntries = await collectMatchedJarEntriesAsUtf8(jarPath, (entry) => entry === "mappings/mappings.tiny" || entry.toLowerCase().endsWith(".tiny"), { maxEntries: 1 });
2139
+ const tinyEntry = matchedEntries[0];
2023
2140
  if (!tinyEntry) {
2024
2141
  return false;
2025
2142
  }
2026
- const content = await readJarEntryAsUtf8(jarPath, tinyEntry);
2027
2143
  await mkdir(dirname(outputPath), { recursive: true });
2028
- await writeFile(outputPath, content, "utf8");
2144
+ await writeFile(outputPath, tinyEntry.content, "utf8");
2029
2145
  return true;
2030
2146
  }
2031
2147
  /**
@@ -25,12 +25,25 @@ export interface GetSignatureInput {
25
25
  includeInherited?: boolean;
26
26
  }
27
27
  export interface GetSignatureOutput {
28
+ classAccessFlags?: number;
28
29
  constructors: SignatureMember[];
29
30
  methods: SignatureMember[];
30
31
  fields: SignatureMember[];
31
32
  warnings: string[];
32
33
  context: ResponseContext;
33
34
  }
35
+ export declare function modifierPrefix(flags: number, category: "method" | "field"): string;
36
+ export declare function parseFieldType(descriptor: string, position?: number, options?: {
37
+ allowVoid?: boolean;
38
+ invalidVoidMessage?: string;
39
+ }): {
40
+ type: string;
41
+ next: number;
42
+ };
43
+ export declare function parseMethodDescriptor(descriptor: string): {
44
+ args: string[];
45
+ returnType: string;
46
+ };
34
47
  export declare class MinecraftExplorerService {
35
48
  private readonly config;
36
49
  private readonly signatureCache;
@@ -21,7 +21,7 @@ const ACC_SYNTHETIC = 0x1000;
21
21
  function lower(value) {
22
22
  return value.toLocaleLowerCase();
23
23
  }
24
- function modifierPrefix(flags, category) {
24
+ export function modifierPrefix(flags, category) {
25
25
  const parts = [];
26
26
  if ((flags & ACC_PUBLIC) !== 0) {
27
27
  parts.push("public");
@@ -68,7 +68,7 @@ function modifierPrefix(flags, category) {
68
68
  }
69
69
  return parts.join(" ");
70
70
  }
71
- function parseFieldType(descriptor, position = 0, options = {}) {
71
+ export function parseFieldType(descriptor, position = 0, options = {}) {
72
72
  if (position >= descriptor.length) {
73
73
  throw createError({
74
74
  code: ERROR_CODES.INVALID_INPUT,
@@ -127,7 +127,7 @@ function parseFieldType(descriptor, position = 0, options = {}) {
127
127
  });
128
128
  }
129
129
  }
130
- function parseMethodDescriptor(descriptor) {
130
+ export function parseMethodDescriptor(descriptor) {
131
131
  if (!descriptor.startsWith("(")) {
132
132
  throw createError({
133
133
  code: ERROR_CODES.INVALID_INPUT,
@@ -372,7 +372,7 @@ function parseClassFile(buffer) {
372
372
  });
373
373
  }
374
374
  }
375
- reader.readU2();
375
+ const accessFlags = reader.readU2();
376
376
  const thisClassIndex = reader.readU2();
377
377
  const superClassIndex = reader.readU2();
378
378
  const interfacesCount = reader.readU2();
@@ -404,6 +404,7 @@ function parseClassFile(buffer) {
404
404
  const attributesCount = reader.readU2();
405
405
  readAttributes(reader, cp, attributesCount);
406
406
  return {
407
+ accessFlags,
407
408
  internalName: readClassName(cp, thisClassIndex),
408
409
  superInternalName: readOptionalClassName(cp, superClassIndex),
409
410
  interfaceInternalNames,
@@ -442,6 +443,7 @@ export class MinecraftExplorerService {
442
443
  const cached = this.signatureCache.get(cacheKey);
443
444
  if (cached) {
444
445
  return {
446
+ classAccessFlags: cached.classAccessFlags,
445
447
  constructors: cached.constructors,
446
448
  methods: cached.methods,
447
449
  fields: cached.fields,
@@ -619,6 +621,7 @@ export class MinecraftExplorerService {
619
621
  const fields = dedupeMembers(hierarchyClasses.flatMap((classFile) => toMembers(classFile, "field", (member) => shouldIncludeMember(member))));
620
622
  const methods = dedupeMembers(hierarchyClasses.flatMap((classFile) => toMembers(classFile, "method", (member) => member.name !== "<init>" && shouldIncludeMember(member))));
621
623
  const output = {
624
+ classAccessFlags: parsed.accessFlags,
622
625
  constructors,
623
626
  methods,
624
627
  fields,
@@ -626,6 +629,7 @@ export class MinecraftExplorerService {
626
629
  };
627
630
  this.signatureCache.set(cacheKey, output);
628
631
  return {
632
+ classAccessFlags: output.classAccessFlags,
629
633
  constructors: output.constructors,
630
634
  methods: output.methods,
631
635
  fields: output.fields,
@@ -5,7 +5,8 @@
5
5
  import type { SignatureMember } from "./minecraft-explorer-service.js";
6
6
  import type { ParsedMixin } from "./mixin-parser.js";
7
7
  import type { ParsedAccessWidener, AccessWidenerEntry } from "./access-widener-parser.js";
8
- import type { SourceMapping } from "./types.js";
8
+ import type { ParsedAccessTransformer, AccessTransformerEntry } from "./access-transformer-parser.js";
9
+ import type { AccessTransformerNamespace, RuntimeValidationProvenance, SourceMapping } from "./types.js";
9
10
  export type MappingHealthReport = {
10
11
  jarAvailable: boolean;
11
12
  jarPath: string;
@@ -131,6 +132,7 @@ export type MixinValidationResult = {
131
132
  };
132
133
  export type ResolvedTargetMembers = {
133
134
  className: string;
135
+ classAccessFlags?: number;
134
136
  constructors: SignatureMember[];
135
137
  methods: SignatureMember[];
136
138
  fields: SignatureMember[];
@@ -143,12 +145,36 @@ export type AccessWidenerValidationResult = {
143
145
  valid: boolean;
144
146
  issue?: string;
145
147
  suggestions?: string[];
148
+ resolvedInRuntime?: boolean;
149
+ resolvedRuntimeAccess?: "public" | "protected" | "private" | "package-private";
150
+ resolvedRuntimeJvmDescriptor?: string;
151
+ resolvedRuntimeJavaSignature?: string;
146
152
  }>;
147
153
  summary: {
148
154
  total: number;
149
155
  valid: number;
150
156
  invalid: number;
151
157
  };
158
+ provenance?: RuntimeValidationProvenance<SourceMapping>;
159
+ warnings: string[];
160
+ };
161
+ export type AccessTransformerValidationResult = {
162
+ valid: boolean;
163
+ entries: Array<AccessTransformerEntry & {
164
+ valid: boolean;
165
+ issue?: string;
166
+ suggestions?: string[];
167
+ resolvedInRuntime?: boolean;
168
+ resolvedRuntimeAccess?: "public" | "protected" | "private" | "package-private";
169
+ resolvedRuntimeJvmDescriptor?: string;
170
+ resolvedRuntimeJavaSignature?: string;
171
+ }>;
172
+ summary: {
173
+ total: number;
174
+ valid: number;
175
+ invalid: number;
176
+ };
177
+ provenance?: RuntimeValidationProvenance<AccessTransformerNamespace>;
152
178
  warnings: string[];
153
179
  };
154
180
  export declare function levenshteinDistance(a: string, b: string): number;
@@ -169,4 +195,9 @@ export declare function validateParsedMixin(parsed: ParsedMixin, targetMembers:
169
195
  projectPath?: string;
170
196
  mapping?: string;
171
197
  }, warningMode?: "full" | "aggregated", healthReport?: MappingHealthReport, symbolExistsButSignatureFailed?: Set<string>): MixinValidationResult;
172
- export declare function validateParsedAccessWidener(parsed: ParsedAccessWidener, membersByClass: Map<string, ResolvedTargetMembers>, warnings: string[]): AccessWidenerValidationResult;
198
+ export declare function validateParsedAccessWidener(parsed: ParsedAccessWidener, membersByClass: Map<string, ResolvedTargetMembers>, warnings: string[], options?: {
199
+ includeRuntimeEvidence?: boolean;
200
+ }): AccessWidenerValidationResult;
201
+ export declare function validateParsedAccessTransformer(parsed: ParsedAccessTransformer, membersByClass: Map<string, ResolvedTargetMembers>, warnings: string[], options?: {
202
+ includeRuntimeEvidence?: boolean;
203
+ }): AccessTransformerValidationResult;