@adhisang/minecraft-modding-mcp 3.1.1 → 4.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 (61) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +37 -18
  3. package/dist/access-transformer-parser.d.ts +17 -0
  4. package/dist/access-transformer-parser.js +97 -0
  5. package/dist/cache-registry.d.ts +1 -1
  6. package/dist/cache-registry.js +10 -2
  7. package/dist/concurrency.d.ts +1 -0
  8. package/dist/concurrency.js +24 -0
  9. package/dist/config.d.ts +10 -1
  10. package/dist/config.js +52 -1
  11. package/dist/decompiler/vineflower.js +22 -21
  12. package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
  13. package/dist/entry-tools/analyze-symbol-service.d.ts +22 -22
  14. package/dist/entry-tools/analyze-symbol-service.js +13 -2
  15. package/dist/entry-tools/inspect-minecraft-service.d.ts +168 -168
  16. package/dist/entry-tools/inspect-minecraft-service.js +8 -2
  17. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  18. package/dist/entry-tools/validate-project-service.d.ts +153 -16
  19. package/dist/entry-tools/validate-project-service.js +442 -25
  20. package/dist/gradle-paths.d.ts +4 -0
  21. package/dist/gradle-paths.js +57 -0
  22. package/dist/index.js +148 -30
  23. package/dist/lru-list.d.ts +31 -0
  24. package/dist/lru-list.js +102 -0
  25. package/dist/mapping-pipeline-service.d.ts +12 -1
  26. package/dist/mapping-pipeline-service.js +28 -1
  27. package/dist/mapping-service.d.ts +16 -0
  28. package/dist/mapping-service.js +405 -68
  29. package/dist/minecraft-explorer-service.d.ts +13 -0
  30. package/dist/minecraft-explorer-service.js +8 -4
  31. package/dist/mixin-validator.d.ts +33 -2
  32. package/dist/mixin-validator.js +218 -17
  33. package/dist/mod-analyzer.d.ts +1 -0
  34. package/dist/mod-analyzer.js +17 -1
  35. package/dist/mod-decompile-service.js +4 -4
  36. package/dist/mod-remap-service.js +1 -54
  37. package/dist/mod-search-service.d.ts +1 -0
  38. package/dist/mod-search-service.js +84 -51
  39. package/dist/observability.d.ts +18 -1
  40. package/dist/observability.js +44 -1
  41. package/dist/response-utils.d.ts +69 -0
  42. package/dist/response-utils.js +227 -0
  43. package/dist/source-jar-reader.d.ts +16 -0
  44. package/dist/source-jar-reader.js +103 -1
  45. package/dist/source-resolver.d.ts +9 -1
  46. package/dist/source-resolver.js +23 -16
  47. package/dist/source-service.d.ts +119 -3
  48. package/dist/source-service.js +1836 -218
  49. package/dist/storage/artifacts-repo.d.ts +4 -1
  50. package/dist/storage/artifacts-repo.js +33 -5
  51. package/dist/storage/files-repo.d.ts +0 -2
  52. package/dist/storage/files-repo.js +0 -11
  53. package/dist/storage/migrations.d.ts +1 -1
  54. package/dist/storage/migrations.js +10 -2
  55. package/dist/storage/schema.d.ts +2 -0
  56. package/dist/storage/schema.js +25 -0
  57. package/dist/tool-contract-manifest.js +8 -6
  58. package/dist/types.d.ts +20 -0
  59. package/dist/workspace-mapping-service.d.ts +13 -0
  60. package/dist/workspace-mapping-service.js +146 -14
  61. package/package.json +3 -1
@@ -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;
@@ -133,6 +133,21 @@ function allMethodNames(members) {
133
133
  function allFieldNames(members) {
134
134
  return members.fields.map((m) => m.name);
135
135
  }
136
+ function accessLevelFromFlags(accessFlags) {
137
+ if (accessFlags == null) {
138
+ return undefined;
139
+ }
140
+ if ((accessFlags & 0x0001) !== 0) {
141
+ return "public";
142
+ }
143
+ if ((accessFlags & 0x0004) !== 0) {
144
+ return "protected";
145
+ }
146
+ if ((accessFlags & 0x0002) !== 0) {
147
+ return "private";
148
+ }
149
+ return "package-private";
150
+ }
136
151
  function computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence) {
137
152
  if (!healthReport)
138
153
  return undefined;
@@ -210,11 +225,23 @@ function computeValidationStatus(summary) {
210
225
  }
211
226
  return "full";
212
227
  }
213
- function buildQuickSummary(status, summary) {
214
- if (status === "full") {
215
- return `${summary.membersValidated} member(s) validated successfully.`;
228
+ function buildQuickSummary(status, summary, context) {
229
+ const base = status === "full"
230
+ ? `${summary.membersValidated} member(s) validated successfully.`
231
+ : `${summary.definiteErrors} error(s), ${summary.uncertainErrors} uncertain, ${summary.warnings} warning(s). ${summary.membersValidated} validated, ${summary.membersSkipped} member(s) skipped, ${summary.membersMissing} member(s) missing.`;
232
+ const notes = [];
233
+ const scopeFallback = context?.provenance?.scopeFallback;
234
+ if (scopeFallback) {
235
+ notes.push(`Scope fell back from "${scopeFallback.requested}" to "${scopeFallback.applied}" (${scopeFallback.reason}).`);
216
236
  }
217
- return `${summary.definiteErrors} error(s), ${summary.uncertainErrors} uncertain, ${summary.warnings} warning(s). ${summary.membersValidated} validated, ${summary.membersSkipped} member(s) skipped, ${summary.membersMissing} member(s) missing.`;
237
+ const healthReport = context?.healthReport;
238
+ if (healthReport && !healthReport.overallHealthy) {
239
+ const degradations = healthReport.degradations.length > 0
240
+ ? healthReport.degradations.join("; ")
241
+ : "mapping infrastructure degraded";
242
+ notes.push(`Mapping health degraded: ${degradations}.`);
243
+ }
244
+ return notes.length > 0 ? `${base} ${notes.join(" ")}` : base;
218
245
  }
219
246
  function addSkippedMembers(parsed, resolvedMembers) {
220
247
  for (const inj of parsed.injections) {
@@ -256,7 +283,10 @@ export function refreshMixinValidationOutcome(result) {
256
283
  };
257
284
  result.validationStatus = computeValidationStatus(result.summary);
258
285
  result.valid = result.summary.definiteErrors === 0;
259
- result.quickSummary = buildQuickSummary(result.validationStatus, result.summary);
286
+ result.quickSummary = buildQuickSummary(result.validationStatus, result.summary, {
287
+ provenance: result.provenance,
288
+ healthReport: result.toolHealth
289
+ });
260
290
  return result;
261
291
  }
262
292
  function validateInjection(inj, targetMembers, targetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport) {
@@ -712,7 +742,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
712
742
  parseWarnings: parseWarningCount
713
743
  };
714
744
  const validationStatus = computeValidationStatus(summary);
715
- const quickSummary = buildQuickSummary(validationStatus, summary);
745
+ const quickSummary = buildQuickSummary(validationStatus, summary, { provenance, healthReport });
716
746
  return {
717
747
  className: parsed.className,
718
748
  targets: targetNames,
@@ -735,7 +765,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
735
765
  /* ------------------------------------------------------------------ */
736
766
  /* Access Widener validation */
737
767
  /* ------------------------------------------------------------------ */
738
- export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
768
+ export function validateParsedAccessWidener(parsed, membersByClass, warnings, options) {
739
769
  warnings.push(...parsed.parseWarnings);
740
770
  const validatedEntries = [];
741
771
  let validCount = 0;
@@ -743,15 +773,29 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
743
773
  for (const entry of parsed.entries) {
744
774
  const ownerFqn = entry.target.replace(/\//g, ".");
745
775
  if (entry.targetKind === "class") {
746
- if (membersByClass.has(ownerFqn)) {
747
- validatedEntries.push({ ...entry, valid: true });
776
+ const members = membersByClass.get(ownerFqn);
777
+ if (members) {
778
+ const runtimeAccess = accessLevelFromFlags(members.classAccessFlags);
779
+ validatedEntries.push({
780
+ ...entry,
781
+ valid: true,
782
+ ...(options?.includeRuntimeEvidence
783
+ ? {
784
+ resolvedInRuntime: true,
785
+ ...(runtimeAccess
786
+ ? { resolvedRuntimeAccess: runtimeAccess }
787
+ : {})
788
+ }
789
+ : {})
790
+ });
748
791
  validCount++;
749
792
  }
750
793
  else {
751
794
  validatedEntries.push({
752
795
  ...entry,
753
796
  valid: false,
754
- issue: `Class "${ownerFqn}" not found in game jar.`
797
+ issue: `Class "${ownerFqn}" not found in game jar.`,
798
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
755
799
  });
756
800
  invalidCount++;
757
801
  }
@@ -763,16 +807,37 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
763
807
  validatedEntries.push({
764
808
  ...entry,
765
809
  valid: false,
766
- issue: `Owner class "${ownerFqn}" not found in game jar.`
810
+ issue: `Owner class "${ownerFqn}" not found in game jar.`,
811
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
767
812
  });
768
813
  invalidCount++;
769
814
  continue;
770
815
  }
771
816
  if (entry.targetKind === "method") {
772
817
  const methodNames = allMethodNames(members);
773
- const found = members.methods.some((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor)) || members.constructors.some((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
818
+ const matchedMember = members.methods.find((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor)) ?? members.constructors.find((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
819
+ const found = matchedMember != null;
774
820
  if (found) {
775
- validatedEntries.push({ ...entry, valid: true });
821
+ const runtimeMember = matchedMember;
822
+ const runtimeAccess = accessLevelFromFlags(runtimeMember.accessFlags);
823
+ validatedEntries.push({
824
+ ...entry,
825
+ valid: true,
826
+ ...(options?.includeRuntimeEvidence
827
+ ? {
828
+ resolvedInRuntime: true,
829
+ ...(runtimeAccess
830
+ ? { resolvedRuntimeAccess: runtimeAccess }
831
+ : {}),
832
+ ...(runtimeMember.jvmDescriptor
833
+ ? { resolvedRuntimeJvmDescriptor: runtimeMember.jvmDescriptor }
834
+ : {}),
835
+ ...(runtimeMember.javaSignature
836
+ ? { resolvedRuntimeJavaSignature: runtimeMember.javaSignature }
837
+ : {})
838
+ }
839
+ : {})
840
+ });
776
841
  validCount++;
777
842
  }
778
843
  else {
@@ -781,7 +846,8 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
781
846
  ...entry,
782
847
  valid: false,
783
848
  issue: `Method "${entry.name}" not found in class "${ownerFqn}".`,
784
- suggestions: suggestions.length > 0 ? suggestions : undefined
849
+ suggestions: suggestions.length > 0 ? suggestions : undefined,
850
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
785
851
  });
786
852
  invalidCount++;
787
853
  }
@@ -789,9 +855,29 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
789
855
  else {
790
856
  // field
791
857
  const fieldNames = allFieldNames(members);
792
- const found = members.fields.some((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
858
+ const matchedMember = members.fields.find((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
859
+ const found = matchedMember != null;
793
860
  if (found) {
794
- validatedEntries.push({ ...entry, valid: true });
861
+ const runtimeMember = matchedMember;
862
+ const runtimeAccess = accessLevelFromFlags(runtimeMember.accessFlags);
863
+ validatedEntries.push({
864
+ ...entry,
865
+ valid: true,
866
+ ...(options?.includeRuntimeEvidence
867
+ ? {
868
+ resolvedInRuntime: true,
869
+ ...(runtimeAccess
870
+ ? { resolvedRuntimeAccess: runtimeAccess }
871
+ : {}),
872
+ ...(runtimeMember.jvmDescriptor
873
+ ? { resolvedRuntimeJvmDescriptor: runtimeMember.jvmDescriptor }
874
+ : {}),
875
+ ...(runtimeMember.javaSignature
876
+ ? { resolvedRuntimeJavaSignature: runtimeMember.javaSignature }
877
+ : {})
878
+ }
879
+ : {})
880
+ });
795
881
  validCount++;
796
882
  }
797
883
  else {
@@ -800,7 +886,8 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
800
886
  ...entry,
801
887
  valid: false,
802
888
  issue: `Field "${entry.name}" not found in class "${ownerFqn}".`,
803
- suggestions: suggestions.length > 0 ? suggestions : undefined
889
+ suggestions: suggestions.length > 0 ? suggestions : undefined,
890
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
804
891
  });
805
892
  invalidCount++;
806
893
  }
@@ -819,4 +906,118 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
819
906
  warnings
820
907
  };
821
908
  }
909
+ export function validateParsedAccessTransformer(parsed, membersByClass, warnings, options) {
910
+ warnings.push(...parsed.parseWarnings);
911
+ const validatedEntries = [];
912
+ let validCount = 0;
913
+ let invalidCount = 0;
914
+ for (const entry of parsed.entries) {
915
+ const ownerFqn = entry.owner;
916
+ const members = membersByClass.get(ownerFqn);
917
+ if (entry.targetKind === "class") {
918
+ if (!members) {
919
+ validatedEntries.push({
920
+ ...entry,
921
+ valid: false,
922
+ issue: `Class "${ownerFqn}" not found in runtime jar.`,
923
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
924
+ });
925
+ invalidCount++;
926
+ continue;
927
+ }
928
+ const runtimeAccess = accessLevelFromFlags(members.classAccessFlags);
929
+ validatedEntries.push({
930
+ ...entry,
931
+ valid: true,
932
+ ...(options?.includeRuntimeEvidence
933
+ ? {
934
+ resolvedInRuntime: true,
935
+ ...(runtimeAccess ? { resolvedRuntimeAccess: runtimeAccess } : {})
936
+ }
937
+ : {})
938
+ });
939
+ validCount++;
940
+ continue;
941
+ }
942
+ if (!members) {
943
+ validatedEntries.push({
944
+ ...entry,
945
+ valid: false,
946
+ issue: `Owner class "${ownerFqn}" not found in runtime jar.`,
947
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
948
+ });
949
+ invalidCount++;
950
+ continue;
951
+ }
952
+ if (entry.targetKind === "field") {
953
+ const fieldNames = allFieldNames(members);
954
+ const matchedField = members.fields.find((member) => member.name === entry.name);
955
+ if (!matchedField) {
956
+ const suggestions = entry.name ? suggestSimilar(entry.name, fieldNames) : [];
957
+ validatedEntries.push({
958
+ ...entry,
959
+ valid: false,
960
+ issue: `Field "${entry.name}" not found in class "${ownerFqn}".`,
961
+ ...(suggestions.length > 0 ? { suggestions } : {}),
962
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
963
+ });
964
+ invalidCount++;
965
+ continue;
966
+ }
967
+ const runtimeAccess = accessLevelFromFlags(matchedField.accessFlags);
968
+ validatedEntries.push({
969
+ ...entry,
970
+ valid: true,
971
+ ...(options?.includeRuntimeEvidence
972
+ ? {
973
+ resolvedInRuntime: true,
974
+ ...(runtimeAccess ? { resolvedRuntimeAccess: runtimeAccess } : {}),
975
+ ...(matchedField.jvmDescriptor ? { resolvedRuntimeJvmDescriptor: matchedField.jvmDescriptor } : {}),
976
+ ...(matchedField.javaSignature ? { resolvedRuntimeJavaSignature: matchedField.javaSignature } : {})
977
+ }
978
+ : {})
979
+ });
980
+ validCount++;
981
+ continue;
982
+ }
983
+ const methodNames = allMethodNames(members);
984
+ const matchedMethod = members.methods.find((member) => member.name === entry.name && member.jvmDescriptor === entry.descriptor) ?? members.constructors.find((member) => member.name === entry.name && member.jvmDescriptor === entry.descriptor);
985
+ if (!matchedMethod) {
986
+ const suggestions = entry.name ? suggestSimilar(entry.name, methodNames) : [];
987
+ validatedEntries.push({
988
+ ...entry,
989
+ valid: false,
990
+ issue: `Method "${entry.name}" not found in class "${ownerFqn}".`,
991
+ ...(suggestions.length > 0 ? { suggestions } : {}),
992
+ ...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
993
+ });
994
+ invalidCount++;
995
+ continue;
996
+ }
997
+ const runtimeAccess = accessLevelFromFlags(matchedMethod.accessFlags);
998
+ validatedEntries.push({
999
+ ...entry,
1000
+ valid: true,
1001
+ ...(options?.includeRuntimeEvidence
1002
+ ? {
1003
+ resolvedInRuntime: true,
1004
+ ...(runtimeAccess ? { resolvedRuntimeAccess: runtimeAccess } : {}),
1005
+ ...(matchedMethod.jvmDescriptor ? { resolvedRuntimeJvmDescriptor: matchedMethod.jvmDescriptor } : {}),
1006
+ ...(matchedMethod.javaSignature ? { resolvedRuntimeJavaSignature: matchedMethod.javaSignature } : {})
1007
+ }
1008
+ : {})
1009
+ });
1010
+ validCount++;
1011
+ }
1012
+ return {
1013
+ valid: invalidCount === 0,
1014
+ entries: validatedEntries,
1015
+ summary: {
1016
+ total: parsed.entries.length,
1017
+ valid: validCount,
1018
+ invalid: invalidCount
1019
+ },
1020
+ warnings
1021
+ };
1022
+ }
822
1023
  //# sourceMappingURL=mixin-validator.js.map
@@ -15,6 +15,7 @@ export interface ModAnalysisResult {
15
15
  entrypoints?: Record<string, string[]>;
16
16
  mixinConfigs?: string[];
17
17
  accessWidener?: string;
18
+ accessTransformers?: string[];
18
19
  dependencies?: ModDependency[];
19
20
  classCount: number;
20
21
  classes?: string[];
@@ -72,7 +72,8 @@ const forgeModsTomlSchema = z
72
72
  .passthrough())
73
73
  .optional(),
74
74
  dependencies: z.record(z.array(z.unknown())).optional(),
75
- mixins: z.array(z.object({ config: z.string() }).passthrough()).optional()
75
+ mixins: z.array(z.object({ config: z.string() }).passthrough()).optional(),
76
+ accessTransformers: z.array(z.object({ file: z.string().optional() }).passthrough()).optional()
76
77
  })
77
78
  .passthrough();
78
79
  const legacyForgeSchema = z.array(z
@@ -214,6 +215,9 @@ function parseForgeMod(content, entries) {
214
215
  }
215
216
  // Mixin configs
216
217
  const mixinConfigs = toml.mixins?.map((m) => m.config);
218
+ const accessTransformers = toml.accessTransformers
219
+ ?.map((entry) => entry.file)
220
+ .filter((file) => typeof file === "string" && file.trim().length > 0);
217
221
  return {
218
222
  detectedLoader,
219
223
  modId: firstMod?.modId,
@@ -221,6 +225,7 @@ function parseForgeMod(content, entries) {
221
225
  modVersion: firstMod?.version,
222
226
  description: firstMod?.description,
223
227
  mixinConfigs: mixinConfigs && mixinConfigs.length > 0 ? mixinConfigs : undefined,
228
+ accessTransformers: accessTransformers && accessTransformers.length > 0 ? accessTransformers : undefined,
224
229
  dependencies: dependencies.length > 0 ? dependencies : undefined
225
230
  };
226
231
  }
@@ -288,6 +293,9 @@ export async function analyzeModJar(jarPath, options) {
288
293
  // Detect loader and parse metadata
289
294
  let loader = "unknown";
290
295
  let metadata = {};
296
+ const packagedAccessTransformers = [...new Set(entries.filter((entry) => /(^|\/)META-INF\/accesstransformer\.cfg$/i.test(entry) ||
297
+ /(^|\/)[^/]+_at\.cfg$/i.test(entry) ||
298
+ /(^|\/)accesstransformer[^/]*\.cfg$/i.test(entry)))].sort((left, right) => left.localeCompare(right));
291
299
  if (entries.includes("fabric.mod.json")) {
292
300
  loader = "fabric";
293
301
  try {
@@ -346,6 +354,14 @@ export async function analyzeModJar(jarPath, options) {
346
354
  loader,
347
355
  jarKind,
348
356
  ...metadata,
357
+ ...(packagedAccessTransformers.length > 0 || metadata.accessTransformers
358
+ ? {
359
+ accessTransformers: [...new Set([
360
+ ...(metadata.accessTransformers ?? []),
361
+ ...packagedAccessTransformers
362
+ ])]
363
+ }
364
+ : {}),
349
365
  classCount,
350
366
  ...(classes !== undefined ? { classes } : {})
351
367
  };
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFileSync, writeFileSync } from "node:fs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
3
  import { isAbsolute, join, resolve as resolvePath } from "node:path";
4
4
  import { createError, ERROR_CODES } from "./errors.js";
5
5
  import { log } from "./logger.js";
@@ -41,7 +41,7 @@ export class ModDecompileService {
41
41
  const targetFile = classNameToFilePath(input.className);
42
42
  const matched = files.find((f) => f === targetFile || f.endsWith(`/${targetFile}`) || f === input.className);
43
43
  if (matched) {
44
- const content = readFileSync(join(outputDir, matched), "utf8");
44
+ const content = await readFile(join(outputDir, matched), "utf8");
45
45
  sourceResult = {
46
46
  className: filePathToClassName(matched),
47
47
  content,
@@ -106,7 +106,7 @@ export class ModDecompileService {
106
106
  details: { className, jarPath, availableCount: files.length }
107
107
  });
108
108
  }
109
- const fullContent = readFileSync(join(outputDir, matched), "utf8");
109
+ const fullContent = await readFile(join(outputDir, matched), "utf8");
110
110
  const totalLines = fullContent.split("\n").length;
111
111
  let content = fullContent;
112
112
  let truncated;
@@ -131,7 +131,7 @@ export class ModDecompileService {
131
131
  const outPath = isAbsolute(input.outputFile)
132
132
  ? input.outputFile
133
133
  : resolvePath(input.outputFile);
134
- writeFileSync(outPath, content, "utf8");
134
+ await writeFile(outPath, content, "utf8");
135
135
  outputFilePath = outPath;
136
136
  content = `[Written to ${outPath}]`;
137
137
  }
@@ -8,7 +8,7 @@ import { resolveTinyMappingFile } from "./mapping-service.js";
8
8
  import { resolveMojangTinyFile } from "./mojang-tiny-mapping-service.js";
9
9
  import { analyzeModJar } from "./mod-analyzer.js";
10
10
  import { normalizePathForHost } from "./path-converter.js";
11
- import { listJarEntries, readJarEntryAsBuffer } from "./source-jar-reader.js";
11
+ import { detectFabricLikeInputNamespace } from "./source-jar-reader.js";
12
12
  import { remapJar } from "./tiny-remapper-service.js";
13
13
  import { resolveTinyRemapperJar } from "./tiny-remapper-resolver.js";
14
14
  function normalizeTargetNamespace(target) {
@@ -36,59 +36,6 @@ function extractMinecraftVersion(dependencies) {
36
36
  const match = mcDep.versionRange.match(/(\d+\.\d+(?:\.\d+)?)/);
37
37
  return match?.[1];
38
38
  }
39
- function countMatches(input, pattern) {
40
- const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
41
- const globalPattern = new RegExp(pattern.source, flags);
42
- let count = 0;
43
- while (globalPattern.exec(input)) {
44
- count += 1;
45
- }
46
- return count;
47
- }
48
- async function detectFabricLikeInputNamespace(inputJar) {
49
- const warnings = [];
50
- const classEntries = (await listJarEntries(inputJar))
51
- .filter((entry) => entry.endsWith(".class"))
52
- .slice(0, 24);
53
- if (classEntries.length === 0) {
54
- warnings.push("Could not inspect class entries to detect input mapping; assuming intermediary.");
55
- return {
56
- fromNamespace: "intermediary",
57
- warnings
58
- };
59
- }
60
- let mojangScore = 0;
61
- let intermediaryScore = 0;
62
- for (const entry of classEntries) {
63
- let text = "";
64
- try {
65
- text = (await readJarEntryAsBuffer(inputJar, entry)).toString("latin1");
66
- }
67
- catch {
68
- continue;
69
- }
70
- mojangScore += countMatches(text, /net\/minecraft\/(?:advancements|client|commands|core|data|gametest|nbt|network|recipe|resources|server|sounds|stats|tags|util|world)\//g) * 3;
71
- intermediaryScore += countMatches(text, /net\/minecraft\/class_\d+/g) * 3;
72
- intermediaryScore += countMatches(text, /\b(?:method|field)_\d+\b/g);
73
- }
74
- if (mojangScore > intermediaryScore && mojangScore > 0) {
75
- return {
76
- fromNamespace: "mojang",
77
- warnings
78
- };
79
- }
80
- if (intermediaryScore > mojangScore && intermediaryScore > 0) {
81
- return {
82
- fromNamespace: "intermediary",
83
- warnings
84
- };
85
- }
86
- warnings.push("Could not confidently detect whether the input jar uses intermediary or mojang names; assuming intermediary.");
87
- return {
88
- fromNamespace: "intermediary",
89
- warnings
90
- };
91
- }
92
39
  async function detectInputNamespaceForLoader(inputJar, loader) {
93
40
  if (loader === "fabric" || loader === "quilt") {
94
41
  return detectFabricLikeInputNamespace(inputJar);
@@ -26,4 +26,5 @@ export declare class ModSearchService {
26
26
  constructor(modDecompileService: ModDecompileService);
27
27
  searchModSource(input: SearchModSourceInput): Promise<SearchModSourceOutput>;
28
28
  private searchSourceJarEntriesDirect;
29
+ private searchDecompiledClassFile;
29
30
  }