@adhisang/minecraft-modding-mcp 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +84 -16
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/decompiler/vineflower.d.ts +1 -0
- package/dist/decompiler/vineflower.js +80 -31
- package/dist/index.js +166 -23
- package/dist/mapping-service.d.ts +22 -0
- package/dist/mapping-service.js +309 -30
- package/dist/mixin-parser.d.ts +1 -0
- package/dist/mixin-parser.js +134 -16
- package/dist/mixin-validator.d.ts +93 -2
- package/dist/mixin-validator.js +464 -41
- package/dist/mod-analyzer.d.ts +2 -0
- package/dist/mod-analyzer.js +7 -0
- package/dist/mod-decompile-service.d.ts +6 -0
- package/dist/mod-decompile-service.js +36 -4
- package/dist/mod-search-service.d.ts +1 -0
- package/dist/mod-search-service.js +96 -0
- package/dist/search-hit-accumulator.d.ts +1 -0
- package/dist/search-hit-accumulator.js +3 -0
- package/dist/source-resolver.js +0 -4
- package/dist/source-service.d.ts +91 -4
- package/dist/source-service.js +1153 -112
- package/dist/storage/files-repo.js +35 -8
- package/dist/types.d.ts +1 -0
- package/dist/version-service.js +30 -6
- package/dist/workspace-mapping-service.d.ts +1 -0
- package/dist/workspace-mapping-service.js +24 -0
- package/package.json +1 -1
package/dist/mixin-validator.js
CHANGED
|
@@ -44,6 +44,58 @@ export function suggestSimilar(name, candidates, maxDistance = 3, maxResults = 3
|
|
|
44
44
|
return scored.slice(0, maxResults).map((s) => s.candidate);
|
|
45
45
|
}
|
|
46
46
|
/* ------------------------------------------------------------------ */
|
|
47
|
+
/* Method reference helpers */
|
|
48
|
+
/* ------------------------------------------------------------------ */
|
|
49
|
+
/**
|
|
50
|
+
* Strip owner prefix (`Lowner;`) and JVM descriptor (`(...)V`) from a
|
|
51
|
+
* Mixin method reference, returning just the method name.
|
|
52
|
+
*
|
|
53
|
+
* Examples:
|
|
54
|
+
* "playerTouch(Lnet/minecraft/world/entity/player/Player;)V" → "playerTouch"
|
|
55
|
+
* "Lnet/minecraft/SomeClass;tick(I)V" → "tick"
|
|
56
|
+
* "<init>" → "<init>"
|
|
57
|
+
* "<init>()V" → "<init>"
|
|
58
|
+
* "tick" → "tick"
|
|
59
|
+
*/
|
|
60
|
+
function stripOwnerPrefix(ref) {
|
|
61
|
+
if (!ref.startsWith("L"))
|
|
62
|
+
return ref;
|
|
63
|
+
const ownerEnd = ref.indexOf(";");
|
|
64
|
+
if (ownerEnd === -1)
|
|
65
|
+
return ref;
|
|
66
|
+
const parenIdx = ref.indexOf("(");
|
|
67
|
+
// Owner prefixes appear before the descriptor, e.g. Lpkg/Class;method(I)V.
|
|
68
|
+
// If ';' appears inside the descriptor, this is not an owner prefix.
|
|
69
|
+
if (parenIdx !== -1 && ownerEnd > parenIdx)
|
|
70
|
+
return ref;
|
|
71
|
+
return ref.substring(ownerEnd + 1);
|
|
72
|
+
}
|
|
73
|
+
export function extractMethodName(ref) {
|
|
74
|
+
let s = stripOwnerPrefix(ref);
|
|
75
|
+
// Remove descriptor: everything from '(' onwards
|
|
76
|
+
const parenIdx = s.indexOf("(");
|
|
77
|
+
if (parenIdx !== -1) {
|
|
78
|
+
s = s.substring(0, parenIdx);
|
|
79
|
+
}
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Extract the JVM descriptor portion from a method reference, if present.
|
|
84
|
+
*
|
|
85
|
+
* Examples:
|
|
86
|
+
* "playerTouch(Lnet/minecraft/world/entity/player/Player;)V" → "(Lnet/minecraft/world/entity/player/Player;)V"
|
|
87
|
+
* "tick" → undefined
|
|
88
|
+
*/
|
|
89
|
+
export function extractMethodDescriptor(ref) {
|
|
90
|
+
// After stripping optional owner prefix, find '('
|
|
91
|
+
const s = stripOwnerPrefix(ref);
|
|
92
|
+
const parenIdx = s.indexOf("(");
|
|
93
|
+
if (parenIdx !== -1) {
|
|
94
|
+
return s.substring(parenIdx);
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
/* ------------------------------------------------------------------ */
|
|
47
99
|
/* Mixin validation */
|
|
48
100
|
/* ------------------------------------------------------------------ */
|
|
49
101
|
function allMethodNames(members) {
|
|
@@ -55,49 +107,133 @@ function allMethodNames(members) {
|
|
|
55
107
|
function allFieldNames(members) {
|
|
56
108
|
return members.fields.map((m) => m.name);
|
|
57
109
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
110
|
+
function computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence) {
|
|
111
|
+
if (!healthReport)
|
|
112
|
+
return undefined;
|
|
113
|
+
if (healthReport.overallHealthy === false) {
|
|
114
|
+
if (resolutionPath === "source-signature-unavailable" ||
|
|
115
|
+
resolutionPath === "target-mapping-failed" ||
|
|
116
|
+
resolutionPath === "member-remap-failed")
|
|
117
|
+
return "high";
|
|
118
|
+
if (issueConfidence === "uncertain")
|
|
119
|
+
return "medium";
|
|
120
|
+
return "medium";
|
|
121
|
+
}
|
|
122
|
+
if (healthReport.memberRemapAvailable === false) {
|
|
123
|
+
if (resolutionPath === "member-remap-failed")
|
|
124
|
+
return "high";
|
|
125
|
+
if (issueConfidence === "uncertain")
|
|
126
|
+
return "medium";
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
60
129
|
}
|
|
61
|
-
function
|
|
130
|
+
function computeConfidenceScore(healthReport, provenance, remapFailureCount) {
|
|
131
|
+
let score = 100;
|
|
132
|
+
if (healthReport) {
|
|
133
|
+
if (!healthReport.overallHealthy)
|
|
134
|
+
score -= 30;
|
|
135
|
+
if (!healthReport.tinyMappingsAvailable)
|
|
136
|
+
score -= 20;
|
|
137
|
+
if (!healthReport.memberRemapAvailable)
|
|
138
|
+
score -= 15;
|
|
139
|
+
}
|
|
140
|
+
if (provenance?.scopeFallback)
|
|
141
|
+
score -= 10;
|
|
142
|
+
if (provenance && provenance.requestedMapping !== provenance.mappingApplied)
|
|
143
|
+
score -= 15;
|
|
144
|
+
score -= Math.min(remapFailureCount * 2, 20);
|
|
145
|
+
return Math.max(score, 0);
|
|
146
|
+
}
|
|
147
|
+
function validateInjection(inj, targetMembers, targetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport) {
|
|
62
148
|
for (const targetName of targetNames) {
|
|
63
149
|
const members = targetMembers.get(targetName);
|
|
64
150
|
if (!members)
|
|
65
151
|
continue;
|
|
66
152
|
const methodNames = allMethodNames(members);
|
|
67
|
-
//
|
|
68
|
-
const
|
|
69
|
-
if (!methodNames.includes(
|
|
70
|
-
const suggestions = suggestSimilar(
|
|
153
|
+
// Strip owner prefix and JVM descriptor from the method reference
|
|
154
|
+
const methodName = extractMethodName(inj.method);
|
|
155
|
+
if (!methodNames.includes(methodName)) {
|
|
156
|
+
const suggestions = suggestSimilar(methodName, methodNames);
|
|
157
|
+
const descriptor = extractMethodDescriptor(inj.method);
|
|
158
|
+
const descriptorHint = descriptor ? ` (descriptor: ${descriptor})` : "";
|
|
159
|
+
// Determine if this is a remap artifact or signature unavailability
|
|
160
|
+
const isRemapFailed = remapFailedMembers?.get(targetName)?.has(methodName);
|
|
161
|
+
const isSigFailed = signatureFailedTargets?.has(targetName);
|
|
162
|
+
const issueConfidence = isRemapFailed ? "uncertain" : confidence;
|
|
163
|
+
const issueConfidenceReason = isRemapFailed
|
|
164
|
+
? `Member remap from official→mapping failed; name mismatch may be a remap artifact, not a true missing member.`
|
|
165
|
+
: confidenceReason;
|
|
166
|
+
const resolutionPath = isRemapFailed
|
|
167
|
+
? "member-remap-failed"
|
|
168
|
+
: isSigFailed ? "source-signature-unavailable" : undefined;
|
|
169
|
+
const memberDegraded = isRemapFailed && healthReport?.memberRemapAvailable === false;
|
|
71
170
|
issues.push({
|
|
72
|
-
severity: "error",
|
|
171
|
+
severity: memberDegraded ? "warning" : "error",
|
|
73
172
|
kind: "method-not-found",
|
|
74
173
|
annotation: `@${inj.annotation}`,
|
|
75
174
|
target: `${targetName}#${inj.method}`,
|
|
76
|
-
message: `Method "${
|
|
175
|
+
message: `Method "${methodName}" not found in target class "${targetName}".${descriptorHint}${memberDegraded ? " (infrastructure degraded; may be false positive)" : ""}`,
|
|
77
176
|
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
78
|
-
line: inj.line
|
|
177
|
+
line: inj.line,
|
|
178
|
+
confidence: issueConfidence,
|
|
179
|
+
confidenceReason: issueConfidenceReason,
|
|
180
|
+
resolutionPath,
|
|
181
|
+
falsePositiveRisk: computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence)
|
|
182
|
+
});
|
|
183
|
+
resolvedMembers.push({
|
|
184
|
+
annotation: `@${inj.annotation}`,
|
|
185
|
+
name: methodName,
|
|
186
|
+
line: inj.line,
|
|
187
|
+
status: "not-found"
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
resolvedMembers.push({
|
|
192
|
+
annotation: `@${inj.annotation}`,
|
|
193
|
+
name: methodName,
|
|
194
|
+
line: inj.line,
|
|
195
|
+
resolvedTo: `${targetName}#${methodName}`,
|
|
196
|
+
status: "resolved"
|
|
79
197
|
});
|
|
80
198
|
}
|
|
81
199
|
}
|
|
82
200
|
}
|
|
83
|
-
function validateShadow(shadow, targetMembers, targetNames, issues) {
|
|
201
|
+
function validateShadow(shadow, targetMembers, targetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport) {
|
|
84
202
|
for (const targetName of targetNames) {
|
|
85
203
|
const members = targetMembers.get(targetName);
|
|
86
204
|
if (!members)
|
|
87
205
|
continue;
|
|
206
|
+
const isRemapFailed = remapFailedMembers?.get(targetName)?.has(shadow.name);
|
|
207
|
+
const isSigFailed = signatureFailedTargets?.has(targetName);
|
|
208
|
+
const issueConfidence = isRemapFailed ? "uncertain" : confidence;
|
|
209
|
+
const issueConfidenceReason = isRemapFailed
|
|
210
|
+
? `Member remap from official→mapping failed; name mismatch may be a remap artifact, not a true missing member.`
|
|
211
|
+
: confidenceReason;
|
|
212
|
+
const resolutionPath = isRemapFailed
|
|
213
|
+
? "member-remap-failed"
|
|
214
|
+
: isSigFailed ? "source-signature-unavailable" : undefined;
|
|
215
|
+
const memberDegraded = isRemapFailed && healthReport?.memberRemapAvailable === false;
|
|
88
216
|
if (shadow.kind === "field") {
|
|
89
217
|
const fieldNames = allFieldNames(members);
|
|
90
218
|
if (!fieldNames.includes(shadow.name)) {
|
|
91
219
|
const suggestions = suggestSimilar(shadow.name, fieldNames);
|
|
92
220
|
issues.push({
|
|
93
|
-
severity: "error",
|
|
221
|
+
severity: memberDegraded ? "warning" : "error",
|
|
94
222
|
kind: "field-not-found",
|
|
95
223
|
annotation: "@Shadow",
|
|
96
224
|
target: `${targetName}#${shadow.name}`,
|
|
97
|
-
message: `Field "${shadow.name}" not found in target class "${targetName}"
|
|
225
|
+
message: `Field "${shadow.name}" not found in target class "${targetName}" (${fieldNames.length} field(s) available).${memberDegraded ? " (infrastructure degraded; may be false positive)" : ""}`,
|
|
98
226
|
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
99
|
-
line: shadow.line
|
|
227
|
+
line: shadow.line,
|
|
228
|
+
confidence: issueConfidence,
|
|
229
|
+
confidenceReason: issueConfidenceReason,
|
|
230
|
+
resolutionPath,
|
|
231
|
+
falsePositiveRisk: computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence)
|
|
100
232
|
});
|
|
233
|
+
resolvedMembers.push({ annotation: "@Shadow", name: shadow.name, line: shadow.line, status: "not-found" });
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
resolvedMembers.push({ annotation: "@Shadow", name: shadow.name, line: shadow.line, resolvedTo: `${targetName}#${shadow.name}`, status: "resolved" });
|
|
101
237
|
}
|
|
102
238
|
}
|
|
103
239
|
else {
|
|
@@ -105,83 +241,370 @@ function validateShadow(shadow, targetMembers, targetNames, issues) {
|
|
|
105
241
|
if (!methodNames.includes(shadow.name)) {
|
|
106
242
|
const suggestions = suggestSimilar(shadow.name, methodNames);
|
|
107
243
|
issues.push({
|
|
108
|
-
severity: "error",
|
|
244
|
+
severity: memberDegraded ? "warning" : "error",
|
|
109
245
|
kind: "method-not-found",
|
|
110
246
|
annotation: "@Shadow",
|
|
111
247
|
target: `${targetName}#${shadow.name}`,
|
|
112
|
-
message: `Method "${shadow.name}" not found in target class "${targetName}"
|
|
248
|
+
message: `Method "${shadow.name}" not found in target class "${targetName}" (${methodNames.length} method(s) available).${memberDegraded ? " (infrastructure degraded; may be false positive)" : ""}`,
|
|
113
249
|
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
114
|
-
line: shadow.line
|
|
250
|
+
line: shadow.line,
|
|
251
|
+
confidence: issueConfidence,
|
|
252
|
+
confidenceReason: issueConfidenceReason,
|
|
253
|
+
resolutionPath,
|
|
254
|
+
falsePositiveRisk: computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence)
|
|
115
255
|
});
|
|
256
|
+
resolvedMembers.push({ annotation: "@Shadow", name: shadow.name, line: shadow.line, status: "not-found" });
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
resolvedMembers.push({ annotation: "@Shadow", name: shadow.name, line: shadow.line, resolvedTo: `${targetName}#${shadow.name}`, status: "resolved" });
|
|
116
260
|
}
|
|
117
261
|
}
|
|
118
262
|
}
|
|
119
263
|
}
|
|
120
|
-
function validateAccessor(accessor, targetMembers, targetNames, issues) {
|
|
264
|
+
function validateAccessor(accessor, targetMembers, targetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport) {
|
|
121
265
|
for (const targetName of targetNames) {
|
|
122
266
|
const members = targetMembers.get(targetName);
|
|
123
267
|
if (!members)
|
|
124
268
|
continue;
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
269
|
+
const candidateNames = accessor.annotation === "Invoker"
|
|
270
|
+
? allMethodNames(members)
|
|
271
|
+
: allFieldNames(members);
|
|
272
|
+
if (!candidateNames.includes(accessor.targetName)) {
|
|
273
|
+
const isRemapFailed = remapFailedMembers?.get(targetName)?.has(accessor.targetName);
|
|
274
|
+
const isSigFailed = signatureFailedTargets?.has(targetName);
|
|
275
|
+
const issueConfidence = isRemapFailed ? "uncertain" : confidence;
|
|
276
|
+
const issueConfidenceReason = isRemapFailed
|
|
277
|
+
? `Member remap from official→mapping failed; name mismatch may be a remap artifact, not a true missing member.`
|
|
278
|
+
: confidenceReason;
|
|
279
|
+
const resolutionPath = isRemapFailed
|
|
280
|
+
? "member-remap-failed"
|
|
281
|
+
: isSigFailed ? "source-signature-unavailable" : undefined;
|
|
282
|
+
const memberDegraded = isRemapFailed && healthReport?.memberRemapAvailable === false;
|
|
283
|
+
const suggestions = suggestSimilar(accessor.targetName, candidateNames);
|
|
284
|
+
const inferenceHint = accessor.targetName !== accessor.name
|
|
285
|
+
? ` (inferred "${accessor.targetName}" from "${accessor.name}" via prefix removal)`
|
|
286
|
+
: "";
|
|
128
287
|
issues.push({
|
|
129
|
-
severity: "error",
|
|
288
|
+
severity: memberDegraded ? "warning" : "error",
|
|
130
289
|
kind: accessor.annotation === "Invoker" ? "method-not-found" : "field-not-found",
|
|
131
290
|
annotation: `@${accessor.annotation}`,
|
|
132
291
|
target: `${targetName}#${accessor.targetName}`,
|
|
133
|
-
message: `Target "${accessor.targetName}"
|
|
292
|
+
message: `Target "${accessor.targetName}" not found in class "${targetName}".${inferenceHint}${memberDegraded ? " (infrastructure degraded; may be false positive)" : ""}`,
|
|
134
293
|
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
135
|
-
line: accessor.line
|
|
294
|
+
line: accessor.line,
|
|
295
|
+
confidence: issueConfidence,
|
|
296
|
+
confidenceReason: issueConfidenceReason,
|
|
297
|
+
resolutionPath,
|
|
298
|
+
falsePositiveRisk: computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence)
|
|
136
299
|
});
|
|
300
|
+
resolvedMembers.push({ annotation: `@${accessor.annotation}`, name: accessor.targetName, line: accessor.line, status: "not-found" });
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
resolvedMembers.push({ annotation: `@${accessor.annotation}`, name: accessor.targetName, line: accessor.line, resolvedTo: `${targetName}#${accessor.targetName}`, status: "resolved" });
|
|
137
304
|
}
|
|
138
305
|
}
|
|
139
306
|
}
|
|
140
|
-
export function validateParsedMixin(parsed, targetMembers, warnings) {
|
|
307
|
+
export function validateParsedMixin(parsed, targetMembers, warnings, provenance, confidence, mappingFailedTargets, explain, remapFailedMembers, signatureFailedTargets, suggestedCallContext, warningMode, healthReport, symbolExistsButSignatureFailed) {
|
|
141
308
|
const issues = [];
|
|
142
309
|
const targetNames = parsed.targets.map((t) => t.className);
|
|
310
|
+
const confidenceReason = confidence === "uncertain"
|
|
311
|
+
? `Mapping fallback: requested "${provenance?.requestedMapping}" but applied "${provenance?.mappingApplied}".`
|
|
312
|
+
: confidence === "likely"
|
|
313
|
+
? "Some members could not be remapped."
|
|
314
|
+
: undefined;
|
|
315
|
+
const resolvedMembers = [];
|
|
143
316
|
// Check target classes exist
|
|
144
317
|
for (const target of parsed.targets) {
|
|
145
318
|
if (!targetMembers.has(target.className)) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
319
|
+
if (mappingFailedTargets?.has(target.className)) {
|
|
320
|
+
// Mapping failure — report as warning with distinct kind
|
|
321
|
+
issues.push({
|
|
322
|
+
severity: "warning",
|
|
323
|
+
kind: "target-mapping-failed",
|
|
324
|
+
annotation: "@Mixin",
|
|
325
|
+
target: target.className,
|
|
326
|
+
message: `Could not map target class "${target.className}" to official namespace; class may still exist under a different mapping.`,
|
|
327
|
+
confidence: "uncertain",
|
|
328
|
+
confidenceReason: `Mapping from "${provenance?.requestedMapping}" to official failed for this class.`,
|
|
329
|
+
category: "mapping",
|
|
330
|
+
resolutionPath: "target-mapping-failed",
|
|
331
|
+
falsePositiveRisk: healthReport?.overallHealthy === false ? "high" : "medium"
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
else if (symbolExistsButSignatureFailed?.has(target.className)) {
|
|
335
|
+
// Symbol exists in mapping graph but getSignature failed — tool limitation, not code issue
|
|
336
|
+
issues.push({
|
|
337
|
+
severity: "warning",
|
|
338
|
+
kind: "target-not-found",
|
|
339
|
+
annotation: "@Mixin",
|
|
340
|
+
target: target.className,
|
|
341
|
+
message: `Target class "${target.className}" exists in mapping data but could not be loaded from game jar (tool limitation). Members not validated.`,
|
|
342
|
+
confidence: "uncertain",
|
|
343
|
+
confidenceReason: "Class exists in mapping graph but bytecode signature extraction failed.",
|
|
344
|
+
category: "resolution",
|
|
345
|
+
resolutionPath: "source-signature-unavailable",
|
|
346
|
+
issueOrigin: "tool_issue",
|
|
347
|
+
falsePositiveRisk: "high"
|
|
348
|
+
});
|
|
349
|
+
// Skip member validation for this target — add skipped entries for each declared member
|
|
350
|
+
for (const inj of parsed.injections) {
|
|
351
|
+
const methodName = extractMethodName(inj.method);
|
|
352
|
+
resolvedMembers.push({
|
|
353
|
+
annotation: `@${inj.annotation}`,
|
|
354
|
+
name: methodName,
|
|
355
|
+
line: inj.line,
|
|
356
|
+
status: "skipped"
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
for (const shadow of parsed.shadows) {
|
|
360
|
+
resolvedMembers.push({
|
|
361
|
+
annotation: "@Shadow",
|
|
362
|
+
name: shadow.name,
|
|
363
|
+
line: shadow.line,
|
|
364
|
+
status: "skipped"
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
for (const accessor of parsed.accessors) {
|
|
368
|
+
resolvedMembers.push({
|
|
369
|
+
annotation: `@${accessor.annotation}`,
|
|
370
|
+
name: accessor.targetName,
|
|
371
|
+
line: accessor.line,
|
|
372
|
+
status: "skipped"
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else if (signatureFailedTargets?.has(target.className)) {
|
|
377
|
+
const degraded = healthReport?.overallHealthy === false;
|
|
378
|
+
issues.push({
|
|
379
|
+
severity: degraded ? "warning" : "error",
|
|
380
|
+
kind: "target-not-found",
|
|
381
|
+
annotation: "@Mixin",
|
|
382
|
+
target: target.className,
|
|
383
|
+
message: `Target class "${target.className}" not found in game jar.${degraded ? " (infrastructure degraded; may be false positive)" : ""}`,
|
|
384
|
+
confidence: degraded ? "uncertain" : confidence,
|
|
385
|
+
confidenceReason: degraded
|
|
386
|
+
? "Mapping infrastructure is degraded; result may be inaccurate."
|
|
387
|
+
: confidenceReason,
|
|
388
|
+
category: "resolution",
|
|
389
|
+
resolutionPath: "source-signature-unavailable",
|
|
390
|
+
falsePositiveRisk: degraded ? "high" : undefined
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
issues.push({
|
|
395
|
+
severity: "error",
|
|
396
|
+
kind: "target-not-found",
|
|
397
|
+
annotation: "@Mixin",
|
|
398
|
+
target: target.className,
|
|
399
|
+
message: `Target class "${target.className}" not found in game jar.`,
|
|
400
|
+
confidence,
|
|
401
|
+
confidenceReason,
|
|
402
|
+
category: "validation",
|
|
403
|
+
resolutionPath: "target-class-missing"
|
|
404
|
+
});
|
|
405
|
+
}
|
|
153
406
|
}
|
|
154
407
|
}
|
|
155
408
|
// Only validate members against targets that were resolved
|
|
156
409
|
const resolvedTargetNames = targetNames.filter((t) => targetMembers.has(t));
|
|
157
410
|
for (const inj of parsed.injections) {
|
|
158
|
-
validateInjection(inj, targetMembers, resolvedTargetNames, issues);
|
|
411
|
+
validateInjection(inj, targetMembers, resolvedTargetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport);
|
|
159
412
|
}
|
|
160
413
|
for (const shadow of parsed.shadows) {
|
|
161
|
-
validateShadow(shadow, targetMembers, resolvedTargetNames, issues);
|
|
414
|
+
validateShadow(shadow, targetMembers, resolvedTargetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport);
|
|
162
415
|
}
|
|
163
416
|
for (const accessor of parsed.accessors) {
|
|
164
|
-
validateAccessor(accessor, targetMembers, resolvedTargetNames, issues);
|
|
417
|
+
validateAccessor(accessor, targetMembers, resolvedTargetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport);
|
|
418
|
+
}
|
|
419
|
+
// Add parse warnings — escalate @Accessor/@Invoker/@Shadow parse failures to issues
|
|
420
|
+
for (const pw of parsed.parseWarnings) {
|
|
421
|
+
if (/@(Accessor|Invoker|Shadow)\b/.test(pw)) {
|
|
422
|
+
const annotation = pw.includes("@Accessor") ? "@Accessor"
|
|
423
|
+
: pw.includes("@Invoker") ? "@Invoker" : "@Shadow";
|
|
424
|
+
issues.push({
|
|
425
|
+
severity: "warning",
|
|
426
|
+
kind: "unknown-annotation",
|
|
427
|
+
annotation,
|
|
428
|
+
target: parsed.className,
|
|
429
|
+
message: pw,
|
|
430
|
+
confidence: "uncertain",
|
|
431
|
+
confidenceReason: "Parser could not extract member declaration; the annotation may be valid.",
|
|
432
|
+
category: "parse",
|
|
433
|
+
issueOrigin: "parser_limitation",
|
|
434
|
+
falsePositiveRisk: "high"
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
warnings.push(pw);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Contradiction detection: if some same-annotation members resolved OK but parse failed for others, note it
|
|
442
|
+
const resolvedAnnotations = new Set(resolvedMembers.filter((m) => m.status === "resolved").map((m) => m.annotation));
|
|
443
|
+
for (const issue of issues) {
|
|
444
|
+
if (issue.category === "parse" && resolvedAnnotations.has(issue.annotation)) {
|
|
445
|
+
issue.message += " (Note: other members with the same annotation resolved successfully.)";
|
|
446
|
+
}
|
|
165
447
|
}
|
|
166
|
-
// Add parse warnings
|
|
167
|
-
warnings.push(...parsed.parseWarnings);
|
|
168
448
|
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
169
449
|
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
450
|
+
const definiteErrors = issues.filter((i) => i.severity === "error" && i.confidence !== "uncertain").length;
|
|
451
|
+
const uncertainErrors = issues.filter((i) => i.severity === "error" && i.confidence === "uncertain").length;
|
|
452
|
+
const resolutionErrors = issues.filter((i) => i.resolutionPath != null).length;
|
|
453
|
+
const parseWarningCount = issues.filter((i) => i.category === "parse").length;
|
|
454
|
+
// Assign category and issueOrigin to issues that don't have them yet
|
|
455
|
+
for (const issue of issues) {
|
|
456
|
+
if (!issue.category) {
|
|
457
|
+
issue.category = issue.resolutionPath ? "resolution" : "validation";
|
|
458
|
+
}
|
|
459
|
+
if (!issue.issueOrigin) {
|
|
460
|
+
if (issue.category === "parse") {
|
|
461
|
+
issue.issueOrigin = "parser_limitation";
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
const toolPaths = ["target-mapping-failed", "member-remap-failed", "source-signature-unavailable"];
|
|
465
|
+
issue.issueOrigin = issue.resolutionPath && toolPaths.includes(issue.resolutionPath)
|
|
466
|
+
? "tool_issue"
|
|
467
|
+
: "code_issue";
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// Enrich issues with explanations and suggested calls when explain=true
|
|
472
|
+
if (explain) {
|
|
473
|
+
const version = provenance?.version;
|
|
474
|
+
const mapping = provenance?.requestedMapping;
|
|
475
|
+
// Build context fields to spread into suggestedCall params
|
|
476
|
+
const ctx = {};
|
|
477
|
+
if (suggestedCallContext?.scope)
|
|
478
|
+
ctx.scope = suggestedCallContext.scope;
|
|
479
|
+
if (suggestedCallContext?.sourcePriority)
|
|
480
|
+
ctx.sourcePriority = suggestedCallContext.sourcePriority;
|
|
481
|
+
if (suggestedCallContext?.projectPath)
|
|
482
|
+
ctx.projectPath = suggestedCallContext.projectPath;
|
|
483
|
+
if (suggestedCallContext?.mapping)
|
|
484
|
+
ctx.mapping = suggestedCallContext.mapping;
|
|
485
|
+
for (const issue of issues) {
|
|
486
|
+
switch (issue.kind) {
|
|
487
|
+
case "target-not-found":
|
|
488
|
+
issue.explanation = `The class "${issue.target}" was not found in the game jar. It may be misspelled, from a different version, or use a different mapping namespace.`;
|
|
489
|
+
if (version && mapping) {
|
|
490
|
+
issue.suggestedCall = {
|
|
491
|
+
tool: "check-symbol-exists",
|
|
492
|
+
params: { kind: "class", name: issue.target, version, sourceMapping: mapping, nameMode: "auto", ...ctx }
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
break;
|
|
496
|
+
case "target-mapping-failed":
|
|
497
|
+
issue.explanation = `Mapping lookup failed for "${issue.target}". The class may exist under a different name in the target namespace.`;
|
|
498
|
+
if (version && mapping) {
|
|
499
|
+
issue.suggestedCall = {
|
|
500
|
+
tool: "check-symbol-exists",
|
|
501
|
+
params: { kind: "class", name: issue.target, version, sourceMapping: mapping, nameMode: "auto", ...ctx }
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
break;
|
|
505
|
+
case "method-not-found": {
|
|
506
|
+
const parts = issue.target.split("#");
|
|
507
|
+
const className = parts[0] ?? issue.target;
|
|
508
|
+
issue.explanation = `The method was not found in the target class. It may be named differently in the current mapping, or might not exist in this version.`;
|
|
509
|
+
if (version) {
|
|
510
|
+
issue.suggestedCall = {
|
|
511
|
+
tool: "get-class-source",
|
|
512
|
+
params: { className, targetKind: "version", targetValue: version, ...(mapping ? { mapping } : {}), mode: "metadata", ...ctx }
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case "field-not-found": {
|
|
518
|
+
const parts = issue.target.split("#");
|
|
519
|
+
const ownerName = parts[0] ?? issue.target;
|
|
520
|
+
const fieldName = parts[1] ?? issue.target;
|
|
521
|
+
issue.explanation = `The field "${fieldName}" was not found in the target class. Verify the field name matches the expected mapping namespace.`;
|
|
522
|
+
if (version && mapping) {
|
|
523
|
+
issue.suggestedCall = {
|
|
524
|
+
tool: "check-symbol-exists",
|
|
525
|
+
params: { kind: "field", owner: ownerName, name: fieldName, version, sourceMapping: mapping, ...ctx }
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Build structuredWarnings — classify by severity and category
|
|
534
|
+
const MAPPING_WARNING_RE = /(?:mapping|remap|fallback|could not map)/i;
|
|
535
|
+
const CONFIG_WARNING_RE = /(?:version|gradle|jar\b|properties|project)/i;
|
|
536
|
+
const PARSE_WARNING_RE = /(?:could not parse|parse\s+warning|missing method attribute)/i;
|
|
537
|
+
const structuredWarnings = warnings.map((msg) => ({
|
|
538
|
+
severity: MAPPING_WARNING_RE.test(msg) ? "warning" : PARSE_WARNING_RE.test(msg) ? "warning" : "info",
|
|
539
|
+
message: msg,
|
|
540
|
+
category: MAPPING_WARNING_RE.test(msg) ? "mapping"
|
|
541
|
+
: PARSE_WARNING_RE.test(msg) ? "parse"
|
|
542
|
+
: CONFIG_WARNING_RE.test(msg) ? "configuration"
|
|
543
|
+
: "validation"
|
|
544
|
+
}));
|
|
545
|
+
// Warning aggregation mode
|
|
546
|
+
let aggregatedWarnings;
|
|
547
|
+
let outputWarnings = warnings;
|
|
548
|
+
let outputStructuredWarnings = structuredWarnings.length > 0 ? structuredWarnings : undefined;
|
|
549
|
+
if (warningMode === "aggregated" && structuredWarnings.length > 0) {
|
|
550
|
+
const groupMap = new Map();
|
|
551
|
+
for (const sw of structuredWarnings) {
|
|
552
|
+
const cat = sw.category ?? "validation";
|
|
553
|
+
const existing = groupMap.get(cat);
|
|
554
|
+
if (existing) {
|
|
555
|
+
existing.count++;
|
|
556
|
+
if (existing.samples.length < 2) {
|
|
557
|
+
existing.samples.push(sw.message);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
groupMap.set(cat, { count: 1, samples: [sw.message] });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
aggregatedWarnings = [...groupMap.entries()].map(([category, { count, samples }]) => ({
|
|
565
|
+
category,
|
|
566
|
+
count,
|
|
567
|
+
samples
|
|
568
|
+
}));
|
|
569
|
+
outputWarnings = [];
|
|
570
|
+
outputStructuredWarnings = undefined;
|
|
571
|
+
}
|
|
572
|
+
// Compute confidence score
|
|
573
|
+
const remapFailureCount = provenance?.remapFailures ?? 0;
|
|
574
|
+
const confidenceScore = healthReport
|
|
575
|
+
? computeConfidenceScore(healthReport, provenance, remapFailureCount)
|
|
576
|
+
: undefined;
|
|
577
|
+
// Build quick summary
|
|
578
|
+
const total = parsed.injections.length + parsed.shadows.length + parsed.accessors.length;
|
|
579
|
+
const quickSummary = issues.length === 0
|
|
580
|
+
? `${total} member(s) validated successfully.`
|
|
581
|
+
: `${definiteErrors} error(s), ${uncertainErrors} uncertain, ${warningCount} warning(s).`;
|
|
170
582
|
return {
|
|
171
583
|
className: parsed.className,
|
|
172
584
|
targets: targetNames,
|
|
173
585
|
priority: parsed.priority,
|
|
174
|
-
valid:
|
|
586
|
+
valid: definiteErrors === 0,
|
|
175
587
|
issues,
|
|
176
588
|
summary: {
|
|
177
589
|
injections: parsed.injections.length,
|
|
178
590
|
shadows: parsed.shadows.length,
|
|
179
591
|
accessors: parsed.accessors.length,
|
|
180
|
-
total
|
|
592
|
+
total,
|
|
181
593
|
errors: errorCount,
|
|
182
|
-
warnings: warningCount
|
|
594
|
+
warnings: warningCount,
|
|
595
|
+
definiteErrors,
|
|
596
|
+
uncertainErrors,
|
|
597
|
+
resolutionErrors,
|
|
598
|
+
parseWarnings: parseWarningCount
|
|
183
599
|
},
|
|
184
|
-
|
|
600
|
+
provenance,
|
|
601
|
+
warnings: outputWarnings,
|
|
602
|
+
structuredWarnings: outputStructuredWarnings,
|
|
603
|
+
aggregatedWarnings,
|
|
604
|
+
resolvedMembers: resolvedMembers.length > 0 ? resolvedMembers : undefined,
|
|
605
|
+
toolHealth: healthReport,
|
|
606
|
+
confidenceScore,
|
|
607
|
+
quickSummary
|
|
185
608
|
};
|
|
186
609
|
}
|
|
187
610
|
/* ------------------------------------------------------------------ */
|
package/dist/mod-analyzer.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type ModLoader = "fabric" | "quilt" | "forge" | "neoforge" | "unknown";
|
|
2
|
+
export type ModJarKind = "binary" | "source" | "mixed";
|
|
2
3
|
export interface ModDependency {
|
|
3
4
|
modId: string;
|
|
4
5
|
versionRange?: string;
|
|
@@ -6,6 +7,7 @@ export interface ModDependency {
|
|
|
6
7
|
}
|
|
7
8
|
export interface ModAnalysisResult {
|
|
8
9
|
loader: ModLoader;
|
|
10
|
+
jarKind: ModJarKind;
|
|
9
11
|
modId?: string;
|
|
10
12
|
modName?: string;
|
|
11
13
|
modVersion?: string;
|
package/dist/mod-analyzer.js
CHANGED
|
@@ -277,8 +277,14 @@ export async function analyzeModJar(jarPath, options) {
|
|
|
277
277
|
}
|
|
278
278
|
// Class counting
|
|
279
279
|
const classEntries = entries.filter((e) => e.endsWith(".class"));
|
|
280
|
+
const javaEntries = entries.filter((e) => e.endsWith(".java"));
|
|
280
281
|
const classCount = classEntries.length;
|
|
281
282
|
const classes = options?.includeClasses ? classEntries : undefined;
|
|
283
|
+
const jarKind = classEntries.length > 0 && javaEntries.length > 0
|
|
284
|
+
? "mixed"
|
|
285
|
+
: javaEntries.length > 0
|
|
286
|
+
? "source"
|
|
287
|
+
: "binary";
|
|
282
288
|
// Detect loader and parse metadata
|
|
283
289
|
let loader = "unknown";
|
|
284
290
|
let metadata = {};
|
|
@@ -338,6 +344,7 @@ export async function analyzeModJar(jarPath, options) {
|
|
|
338
344
|
}
|
|
339
345
|
return {
|
|
340
346
|
loader,
|
|
347
|
+
jarKind,
|
|
341
348
|
...metadata,
|
|
342
349
|
classCount,
|
|
343
350
|
...(classes !== undefined ? { classes } : {})
|
|
@@ -21,11 +21,17 @@ export type DecompileModJarOutput = {
|
|
|
21
21
|
export type GetModClassSourceInput = {
|
|
22
22
|
jarPath: string;
|
|
23
23
|
className: string;
|
|
24
|
+
maxLines?: number;
|
|
25
|
+
maxChars?: number;
|
|
26
|
+
outputFile?: string;
|
|
24
27
|
};
|
|
25
28
|
export type GetModClassSourceOutput = {
|
|
26
29
|
className: string;
|
|
27
30
|
content: string;
|
|
28
31
|
totalLines: number;
|
|
32
|
+
truncated?: boolean;
|
|
33
|
+
charsTruncated?: boolean;
|
|
34
|
+
outputFilePath?: string;
|
|
29
35
|
modId?: string;
|
|
30
36
|
warnings: string[];
|
|
31
37
|
};
|