@adhisang/minecraft-modding-mcp 2.0.0 → 2.1.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.
- package/CHANGELOG.md +44 -0
- package/README.md +109 -29
- package/dist/cli.js +31 -4
- package/dist/compat-stdio-transport.d.ts +2 -7
- package/dist/compat-stdio-transport.js +12 -154
- package/dist/index.js +392 -33
- package/dist/json-rpc-framing.d.ts +22 -0
- package/dist/json-rpc-framing.js +168 -0
- package/dist/mapping-pipeline-service.js +9 -1
- package/dist/mapping-service.d.ts +9 -0
- package/dist/mapping-service.js +183 -60
- package/dist/minecraft-explorer-service.d.ts +0 -1
- package/dist/minecraft-explorer-service.js +119 -23
- package/dist/mixin-validator.d.ts +24 -2
- package/dist/mixin-validator.js +223 -98
- package/dist/mod-decompile-service.d.ts +5 -0
- package/dist/mod-decompile-service.js +40 -5
- package/dist/mod-remap-service.js +142 -30
- package/dist/path-resolver.js +41 -4
- package/dist/registry-service.d.ts +10 -1
- package/dist/registry-service.js +154 -22
- package/dist/search-hit-accumulator.js +23 -2
- package/dist/source-jar-reader.js +16 -2
- package/dist/source-resolver.js +6 -7
- package/dist/source-service.d.ts +42 -4
- package/dist/source-service.js +781 -127
- package/dist/stdio-supervisor.d.ts +46 -0
- package/dist/stdio-supervisor.js +349 -0
- package/dist/storage/files-repo.d.ts +3 -9
- package/dist/storage/files-repo.js +66 -43
- package/dist/symbols/symbol-extractor.js +6 -4
- package/dist/tool-execution-gate.d.ts +15 -0
- package/dist/tool-execution-gate.js +58 -0
- package/dist/version-diff-service.js +10 -5
- package/dist/version-service.js +7 -2
- package/dist/workspace-mapping-service.js +12 -0
- package/package.json +1 -1
package/dist/mixin-validator.js
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
* Validation engine for parsed Mixin sources and Access Widener files.
|
|
3
3
|
* Compares parsed annotations against resolved Minecraft bytecode signatures.
|
|
4
4
|
*/
|
|
5
|
+
const TOOL_RESOLUTION_PATHS = [
|
|
6
|
+
"target-mapping-failed",
|
|
7
|
+
"member-remap-failed",
|
|
8
|
+
"source-signature-unavailable"
|
|
9
|
+
];
|
|
10
|
+
const MAPPING_WARNING_RE = /(?:mapping|remap|fallback|could not map)/i;
|
|
11
|
+
const CONFIG_WARNING_RE = /(?:version|gradle|jar\b|properties|project)/i;
|
|
12
|
+
const PARSE_WARNING_RE = /(?:could not parse|parse\s+warning|missing method attribute)/i;
|
|
13
|
+
function classifyStructuredWarning(message) {
|
|
14
|
+
return {
|
|
15
|
+
severity: MAPPING_WARNING_RE.test(message) ? "warning" : PARSE_WARNING_RE.test(message) ? "warning" : "info",
|
|
16
|
+
message,
|
|
17
|
+
category: MAPPING_WARNING_RE.test(message)
|
|
18
|
+
? "mapping"
|
|
19
|
+
: PARSE_WARNING_RE.test(message)
|
|
20
|
+
? "parse"
|
|
21
|
+
: CONFIG_WARNING_RE.test(message)
|
|
22
|
+
? "configuration"
|
|
23
|
+
: "validation"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
5
26
|
/* ------------------------------------------------------------------ */
|
|
6
27
|
/* Levenshtein distance */
|
|
7
28
|
/* ------------------------------------------------------------------ */
|
|
@@ -33,9 +54,14 @@ export function levenshteinDistance(a, b) {
|
|
|
33
54
|
return prev[lb];
|
|
34
55
|
}
|
|
35
56
|
export function suggestSimilar(name, candidates, maxDistance = 3, maxResults = 3) {
|
|
57
|
+
const normalizedName = name.toLowerCase();
|
|
36
58
|
const scored = [];
|
|
37
59
|
for (const candidate of candidates) {
|
|
38
|
-
const
|
|
60
|
+
const normalizedCandidate = candidate.toLowerCase();
|
|
61
|
+
if (Math.abs(normalizedName.length - normalizedCandidate.length) > maxDistance) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const distance = levenshteinDistance(normalizedName, normalizedCandidate);
|
|
39
65
|
if (distance <= maxDistance && distance > 0) {
|
|
40
66
|
scored.push({ candidate, distance });
|
|
41
67
|
}
|
|
@@ -127,22 +153,111 @@ function computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence)
|
|
|
127
153
|
}
|
|
128
154
|
return undefined;
|
|
129
155
|
}
|
|
130
|
-
function
|
|
131
|
-
|
|
156
|
+
function computeConfidenceBreakdown(healthReport, provenance, remapFailureCount, skippedMemberCount) {
|
|
157
|
+
const baseScore = 100;
|
|
158
|
+
const penalties = [];
|
|
159
|
+
let score = baseScore;
|
|
132
160
|
if (healthReport) {
|
|
133
|
-
if (!healthReport.overallHealthy)
|
|
161
|
+
if (!healthReport.overallHealthy) {
|
|
162
|
+
penalties.push({ reason: "mapping-health", points: 30 });
|
|
134
163
|
score -= 30;
|
|
135
|
-
|
|
164
|
+
}
|
|
165
|
+
if (!healthReport.tinyMappingsAvailable) {
|
|
166
|
+
penalties.push({ reason: "tiny-mappings-unavailable", points: 20 });
|
|
136
167
|
score -= 20;
|
|
137
|
-
|
|
168
|
+
}
|
|
169
|
+
if (!healthReport.memberRemapAvailable) {
|
|
170
|
+
penalties.push({ reason: "member-remap-unavailable", points: 15 });
|
|
138
171
|
score -= 15;
|
|
172
|
+
}
|
|
139
173
|
}
|
|
140
|
-
if (provenance?.scopeFallback)
|
|
174
|
+
if (provenance?.scopeFallback) {
|
|
175
|
+
penalties.push({ reason: "scope-fallback", points: 10 });
|
|
141
176
|
score -= 10;
|
|
142
|
-
|
|
177
|
+
}
|
|
178
|
+
if (provenance && provenance.requestedMapping !== provenance.mappingApplied) {
|
|
179
|
+
penalties.push({ reason: "mapping-mismatch", points: 15 });
|
|
143
180
|
score -= 15;
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
}
|
|
182
|
+
if (skippedMemberCount > 0) {
|
|
183
|
+
penalties.push({ reason: "members-skipped", points: 25 });
|
|
184
|
+
score -= 25;
|
|
185
|
+
}
|
|
186
|
+
const remapPenalty = Math.min(remapFailureCount * 2, 20);
|
|
187
|
+
if (remapPenalty > 0) {
|
|
188
|
+
penalties.push({ reason: "remap-failures", points: remapPenalty });
|
|
189
|
+
score -= remapPenalty;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
baseScore,
|
|
193
|
+
score: Math.max(score, 0),
|
|
194
|
+
penalties
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function summarizeResolvedMembers(resolvedMembers) {
|
|
198
|
+
return {
|
|
199
|
+
membersValidated: resolvedMembers.filter((member) => member.status === "resolved").length,
|
|
200
|
+
membersSkipped: resolvedMembers.filter((member) => member.status === "skipped").length,
|
|
201
|
+
membersMissing: resolvedMembers.filter((member) => member.status === "not-found").length
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function computeValidationStatus(summary) {
|
|
205
|
+
if (summary.errors > 0 || summary.definiteErrors > 0) {
|
|
206
|
+
return "invalid";
|
|
207
|
+
}
|
|
208
|
+
if (summary.warnings > 0 || summary.membersSkipped > 0) {
|
|
209
|
+
return "partial";
|
|
210
|
+
}
|
|
211
|
+
return "full";
|
|
212
|
+
}
|
|
213
|
+
function buildQuickSummary(status, summary) {
|
|
214
|
+
if (status === "full") {
|
|
215
|
+
return `${summary.membersValidated} member(s) validated successfully.`;
|
|
216
|
+
}
|
|
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.`;
|
|
218
|
+
}
|
|
219
|
+
function addSkippedMembers(parsed, resolvedMembers) {
|
|
220
|
+
for (const inj of parsed.injections) {
|
|
221
|
+
resolvedMembers.push({
|
|
222
|
+
annotation: `@${inj.annotation}`,
|
|
223
|
+
name: extractMethodName(inj.method),
|
|
224
|
+
line: inj.line,
|
|
225
|
+
status: "skipped"
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
for (const shadow of parsed.shadows) {
|
|
229
|
+
resolvedMembers.push({
|
|
230
|
+
annotation: "@Shadow",
|
|
231
|
+
name: shadow.name,
|
|
232
|
+
line: shadow.line,
|
|
233
|
+
status: "skipped"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
for (const accessor of parsed.accessors) {
|
|
237
|
+
resolvedMembers.push({
|
|
238
|
+
annotation: `@${accessor.annotation}`,
|
|
239
|
+
name: accessor.targetName,
|
|
240
|
+
line: accessor.line,
|
|
241
|
+
status: "skipped"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export function refreshMixinValidationOutcome(result) {
|
|
246
|
+
const memberSummary = result.resolvedMembers
|
|
247
|
+
? summarizeResolvedMembers(result.resolvedMembers)
|
|
248
|
+
: {
|
|
249
|
+
membersValidated: result.summary.membersValidated,
|
|
250
|
+
membersSkipped: result.summary.membersSkipped,
|
|
251
|
+
membersMissing: result.summary.membersMissing
|
|
252
|
+
};
|
|
253
|
+
result.summary = {
|
|
254
|
+
...result.summary,
|
|
255
|
+
...memberSummary
|
|
256
|
+
};
|
|
257
|
+
result.validationStatus = computeValidationStatus(result.summary);
|
|
258
|
+
result.valid = result.summary.definiteErrors === 0;
|
|
259
|
+
result.quickSummary = buildQuickSummary(result.validationStatus, result.summary);
|
|
260
|
+
return result;
|
|
146
261
|
}
|
|
147
262
|
function validateInjection(inj, targetMembers, targetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport) {
|
|
148
263
|
for (const targetName of targetNames) {
|
|
@@ -335,7 +450,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
335
450
|
// Symbol exists in mapping graph but getSignature failed — tool limitation, not code issue
|
|
336
451
|
issues.push({
|
|
337
452
|
severity: "warning",
|
|
338
|
-
kind: "
|
|
453
|
+
kind: "validation-incomplete",
|
|
339
454
|
annotation: "@Mixin",
|
|
340
455
|
target: target.className,
|
|
341
456
|
message: `Target class "${target.className}" exists in mapping data but could not be loaded from game jar (tool limitation). Members not validated.`,
|
|
@@ -346,49 +461,23 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
346
461
|
issueOrigin: "tool_issue",
|
|
347
462
|
falsePositiveRisk: "high"
|
|
348
463
|
});
|
|
349
|
-
|
|
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
|
-
}
|
|
464
|
+
addSkippedMembers(parsed, resolvedMembers);
|
|
375
465
|
}
|
|
376
466
|
else if (signatureFailedTargets?.has(target.className)) {
|
|
377
|
-
const degraded = healthReport?.overallHealthy === false;
|
|
378
467
|
issues.push({
|
|
379
|
-
severity:
|
|
380
|
-
kind: "
|
|
468
|
+
severity: "warning",
|
|
469
|
+
kind: "validation-incomplete",
|
|
381
470
|
annotation: "@Mixin",
|
|
382
471
|
target: target.className,
|
|
383
|
-
message: `Target class "${target.className}" not
|
|
384
|
-
confidence:
|
|
385
|
-
confidenceReason:
|
|
386
|
-
? "Mapping infrastructure is degraded; result may be inaccurate."
|
|
387
|
-
: confidenceReason,
|
|
472
|
+
message: `Target class "${target.className}" could not load enough target metadata for reliable validation. Members were not validated.`,
|
|
473
|
+
confidence: "uncertain",
|
|
474
|
+
confidenceReason: "Target bytecode could not be loaded and fallback existence checks were unavailable.",
|
|
388
475
|
category: "resolution",
|
|
389
476
|
resolutionPath: "source-signature-unavailable",
|
|
390
|
-
|
|
477
|
+
issueOrigin: "tool_issue",
|
|
478
|
+
falsePositiveRisk: "high"
|
|
391
479
|
});
|
|
480
|
+
addSkippedMembers(parsed, resolvedMembers);
|
|
392
481
|
}
|
|
393
482
|
else {
|
|
394
483
|
issues.push({
|
|
@@ -439,20 +528,22 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
439
528
|
}
|
|
440
529
|
}
|
|
441
530
|
// Contradiction detection: if some same-annotation members resolved OK but parse failed for others, note it
|
|
442
|
-
const resolvedAnnotations = new Set(
|
|
531
|
+
const resolvedAnnotations = new Set();
|
|
532
|
+
for (const member of resolvedMembers) {
|
|
533
|
+
if (member.status === "resolved") {
|
|
534
|
+
resolvedAnnotations.add(member.annotation);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
let errorCount = 0;
|
|
538
|
+
let warningCount = 0;
|
|
539
|
+
let definiteErrors = 0;
|
|
540
|
+
let uncertainErrors = 0;
|
|
541
|
+
let resolutionErrors = 0;
|
|
542
|
+
let parseWarningCount = 0;
|
|
443
543
|
for (const issue of issues) {
|
|
444
544
|
if (issue.category === "parse" && resolvedAnnotations.has(issue.annotation)) {
|
|
445
545
|
issue.message += " (Note: other members with the same annotation resolved successfully.)";
|
|
446
546
|
}
|
|
447
|
-
}
|
|
448
|
-
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
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
547
|
if (!issue.category) {
|
|
457
548
|
issue.category = issue.resolutionPath ? "resolution" : "validation";
|
|
458
549
|
}
|
|
@@ -461,27 +552,47 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
461
552
|
issue.issueOrigin = "parser_limitation";
|
|
462
553
|
}
|
|
463
554
|
else {
|
|
464
|
-
|
|
465
|
-
issue.issueOrigin = issue.resolutionPath && toolPaths.includes(issue.resolutionPath)
|
|
555
|
+
issue.issueOrigin = issue.resolutionPath && TOOL_RESOLUTION_PATHS.includes(issue.resolutionPath)
|
|
466
556
|
? "tool_issue"
|
|
467
557
|
: "code_issue";
|
|
468
558
|
}
|
|
469
559
|
}
|
|
560
|
+
if (issue.severity === "error") {
|
|
561
|
+
errorCount++;
|
|
562
|
+
if (issue.confidence === "uncertain") {
|
|
563
|
+
uncertainErrors++;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
definiteErrors++;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
warningCount++;
|
|
571
|
+
}
|
|
572
|
+
if (issue.resolutionPath != null) {
|
|
573
|
+
resolutionErrors++;
|
|
574
|
+
}
|
|
575
|
+
if (issue.category === "parse") {
|
|
576
|
+
parseWarningCount++;
|
|
577
|
+
}
|
|
470
578
|
}
|
|
471
579
|
// Enrich issues with explanations and suggested calls when explain=true
|
|
472
580
|
if (explain) {
|
|
473
581
|
const version = provenance?.version;
|
|
474
582
|
const mapping = provenance?.requestedMapping;
|
|
475
|
-
|
|
476
|
-
|
|
583
|
+
const symbolLookupContext = {};
|
|
584
|
+
if (suggestedCallContext?.sourcePriority) {
|
|
585
|
+
symbolLookupContext.sourcePriority = suggestedCallContext.sourcePriority;
|
|
586
|
+
}
|
|
587
|
+
const classSourceContext = {};
|
|
477
588
|
if (suggestedCallContext?.scope)
|
|
478
|
-
|
|
589
|
+
classSourceContext.scope = suggestedCallContext.scope;
|
|
479
590
|
if (suggestedCallContext?.sourcePriority)
|
|
480
|
-
|
|
591
|
+
classSourceContext.sourcePriority = suggestedCallContext.sourcePriority;
|
|
481
592
|
if (suggestedCallContext?.projectPath)
|
|
482
|
-
|
|
593
|
+
classSourceContext.projectPath = suggestedCallContext.projectPath;
|
|
483
594
|
if (suggestedCallContext?.mapping)
|
|
484
|
-
|
|
595
|
+
classSourceContext.mapping = suggestedCallContext.mapping;
|
|
485
596
|
for (const issue of issues) {
|
|
486
597
|
switch (issue.kind) {
|
|
487
598
|
case "target-not-found":
|
|
@@ -489,7 +600,22 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
489
600
|
if (version && mapping) {
|
|
490
601
|
issue.suggestedCall = {
|
|
491
602
|
tool: "check-symbol-exists",
|
|
492
|
-
params: { kind: "class", name: issue.target, version, sourceMapping: mapping, nameMode: "auto", ...
|
|
603
|
+
params: { kind: "class", name: issue.target, version, sourceMapping: mapping, nameMode: "auto", ...symbolLookupContext }
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
break;
|
|
607
|
+
case "validation-incomplete":
|
|
608
|
+
issue.explanation = `Target metadata for "${issue.target}" could not be loaded reliably, so validation was only partial. This usually indicates a tool or environment limitation rather than a confirmed code error.`;
|
|
609
|
+
if (version) {
|
|
610
|
+
issue.suggestedCall = {
|
|
611
|
+
tool: "get-class-source",
|
|
612
|
+
params: {
|
|
613
|
+
className: issue.target,
|
|
614
|
+
target: { type: "resolve", kind: "version", value: version },
|
|
615
|
+
...(mapping ? { mapping } : {}),
|
|
616
|
+
mode: "metadata",
|
|
617
|
+
...classSourceContext
|
|
618
|
+
}
|
|
493
619
|
};
|
|
494
620
|
}
|
|
495
621
|
break;
|
|
@@ -498,7 +624,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
498
624
|
if (version && mapping) {
|
|
499
625
|
issue.suggestedCall = {
|
|
500
626
|
tool: "check-symbol-exists",
|
|
501
|
-
params: { kind: "class", name: issue.target, version, sourceMapping: mapping, nameMode: "auto", ...
|
|
627
|
+
params: { kind: "class", name: issue.target, version, sourceMapping: mapping, nameMode: "auto", ...symbolLookupContext }
|
|
502
628
|
};
|
|
503
629
|
}
|
|
504
630
|
break;
|
|
@@ -509,7 +635,13 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
509
635
|
if (version) {
|
|
510
636
|
issue.suggestedCall = {
|
|
511
637
|
tool: "get-class-source",
|
|
512
|
-
params: {
|
|
638
|
+
params: {
|
|
639
|
+
className,
|
|
640
|
+
target: { type: "resolve", kind: "version", value: version },
|
|
641
|
+
...(mapping ? { mapping } : {}),
|
|
642
|
+
mode: "metadata",
|
|
643
|
+
...classSourceContext
|
|
644
|
+
}
|
|
513
645
|
};
|
|
514
646
|
}
|
|
515
647
|
break;
|
|
@@ -522,7 +654,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
522
654
|
if (version && mapping) {
|
|
523
655
|
issue.suggestedCall = {
|
|
524
656
|
tool: "check-symbol-exists",
|
|
525
|
-
params: { kind: "field", owner: ownerName, name: fieldName, version, sourceMapping: mapping, ...
|
|
657
|
+
params: { kind: "field", owner: ownerName, name: fieldName, version, sourceMapping: mapping, ...symbolLookupContext }
|
|
526
658
|
};
|
|
527
659
|
}
|
|
528
660
|
break;
|
|
@@ -530,18 +662,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
530
662
|
}
|
|
531
663
|
}
|
|
532
664
|
}
|
|
533
|
-
|
|
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
|
-
}));
|
|
665
|
+
const structuredWarnings = warnings.map(classifyStructuredWarning);
|
|
545
666
|
// Warning aggregation mode
|
|
546
667
|
let aggregatedWarnings;
|
|
547
668
|
let outputWarnings = warnings;
|
|
@@ -571,32 +692,35 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
571
692
|
}
|
|
572
693
|
// Compute confidence score
|
|
573
694
|
const remapFailureCount = provenance?.remapFailures ?? 0;
|
|
574
|
-
const
|
|
575
|
-
|
|
695
|
+
const memberSummary = summarizeResolvedMembers(resolvedMembers);
|
|
696
|
+
const confidenceBreakdown = healthReport
|
|
697
|
+
? computeConfidenceBreakdown(healthReport, provenance, remapFailureCount, memberSummary.membersSkipped)
|
|
576
698
|
: undefined;
|
|
577
|
-
|
|
699
|
+
const confidenceScore = confidenceBreakdown?.score;
|
|
578
700
|
const total = parsed.injections.length + parsed.shadows.length + parsed.accessors.length;
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
:
|
|
701
|
+
const summary = {
|
|
702
|
+
injections: parsed.injections.length,
|
|
703
|
+
shadows: parsed.shadows.length,
|
|
704
|
+
accessors: parsed.accessors.length,
|
|
705
|
+
total,
|
|
706
|
+
...memberSummary,
|
|
707
|
+
errors: errorCount,
|
|
708
|
+
warnings: warningCount,
|
|
709
|
+
definiteErrors,
|
|
710
|
+
uncertainErrors,
|
|
711
|
+
resolutionErrors,
|
|
712
|
+
parseWarnings: parseWarningCount
|
|
713
|
+
};
|
|
714
|
+
const validationStatus = computeValidationStatus(summary);
|
|
715
|
+
const quickSummary = buildQuickSummary(validationStatus, summary);
|
|
582
716
|
return {
|
|
583
717
|
className: parsed.className,
|
|
584
718
|
targets: targetNames,
|
|
585
719
|
priority: parsed.priority,
|
|
586
720
|
valid: definiteErrors === 0,
|
|
721
|
+
validationStatus,
|
|
587
722
|
issues,
|
|
588
|
-
summary
|
|
589
|
-
injections: parsed.injections.length,
|
|
590
|
-
shadows: parsed.shadows.length,
|
|
591
|
-
accessors: parsed.accessors.length,
|
|
592
|
-
total,
|
|
593
|
-
errors: errorCount,
|
|
594
|
-
warnings: warningCount,
|
|
595
|
-
definiteErrors,
|
|
596
|
-
uncertainErrors,
|
|
597
|
-
resolutionErrors,
|
|
598
|
-
parseWarnings: parseWarningCount
|
|
599
|
-
},
|
|
723
|
+
summary,
|
|
600
724
|
provenance,
|
|
601
725
|
warnings: outputWarnings,
|
|
602
726
|
structuredWarnings: outputStructuredWarnings,
|
|
@@ -604,6 +728,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
604
728
|
resolvedMembers: resolvedMembers.length > 0 ? resolvedMembers : undefined,
|
|
605
729
|
toolHealth: healthReport,
|
|
606
730
|
confidenceScore,
|
|
731
|
+
confidenceBreakdown,
|
|
607
732
|
quickSummary
|
|
608
733
|
};
|
|
609
734
|
}
|
|
@@ -2,6 +2,8 @@ import type { Config } from "./types.js";
|
|
|
2
2
|
export type DecompileModJarInput = {
|
|
3
3
|
jarPath: string;
|
|
4
4
|
className?: string;
|
|
5
|
+
includeFiles?: boolean;
|
|
6
|
+
maxFiles?: number;
|
|
5
7
|
};
|
|
6
8
|
export type DecompileModJarOutput = {
|
|
7
9
|
modId: string;
|
|
@@ -11,6 +13,9 @@ export type DecompileModJarOutput = {
|
|
|
11
13
|
outputDir: string;
|
|
12
14
|
fileCount: number;
|
|
13
15
|
files?: string[];
|
|
16
|
+
returnedFileCount?: number;
|
|
17
|
+
filesTruncated?: boolean;
|
|
18
|
+
filesOmitted?: boolean;
|
|
14
19
|
source?: {
|
|
15
20
|
className: string;
|
|
16
21
|
content: string;
|
|
@@ -17,6 +17,12 @@ function classNameToFilePath(className) {
|
|
|
17
17
|
function filePathToClassName(filePath) {
|
|
18
18
|
return filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
19
19
|
}
|
|
20
|
+
function clampPositiveInt(value) {
|
|
21
|
+
if (!Number.isFinite(value) || value == null) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return Math.max(1, Math.trunc(value));
|
|
25
|
+
}
|
|
20
26
|
export class ModDecompileService {
|
|
21
27
|
config;
|
|
22
28
|
// Cache: jarPath hash → decompiled output dir + file list
|
|
@@ -28,6 +34,8 @@ export class ModDecompileService {
|
|
|
28
34
|
const jarPath = validateAndNormalizeJarPath(input.jarPath);
|
|
29
35
|
const warnings = [];
|
|
30
36
|
const { outputDir, files, analysis } = await this.ensureDecompiled(jarPath, warnings);
|
|
37
|
+
const includeFiles = input.includeFiles ?? true;
|
|
38
|
+
const maxFiles = clampPositiveInt(input.maxFiles);
|
|
31
39
|
let sourceResult;
|
|
32
40
|
if (input.className) {
|
|
33
41
|
const targetFile = classNameToFilePath(input.className);
|
|
@@ -44,6 +52,25 @@ export class ModDecompileService {
|
|
|
44
52
|
warnings.push(`Class "${input.className}" not found in decompiled output. Use the files list to find available classes.`);
|
|
45
53
|
}
|
|
46
54
|
}
|
|
55
|
+
let returnedFiles;
|
|
56
|
+
let returnedFileCount;
|
|
57
|
+
let filesTruncated;
|
|
58
|
+
let filesOmitted;
|
|
59
|
+
if (!input.className) {
|
|
60
|
+
const classNames = files.map(filePathToClassName);
|
|
61
|
+
if (!includeFiles) {
|
|
62
|
+
returnedFileCount = 0;
|
|
63
|
+
filesOmitted = true;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const limitedFiles = maxFiles != null && classNames.length > maxFiles
|
|
67
|
+
? classNames.slice(0, maxFiles)
|
|
68
|
+
: classNames;
|
|
69
|
+
returnedFiles = limitedFiles;
|
|
70
|
+
returnedFileCount = limitedFiles.length;
|
|
71
|
+
filesTruncated = limitedFiles.length < classNames.length || undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
47
74
|
return {
|
|
48
75
|
modId: analysis.modId ?? "unknown",
|
|
49
76
|
modName: analysis.modName,
|
|
@@ -51,7 +78,10 @@ export class ModDecompileService {
|
|
|
51
78
|
loader: analysis.loader,
|
|
52
79
|
outputDir,
|
|
53
80
|
fileCount: files.length,
|
|
54
|
-
files:
|
|
81
|
+
files: returnedFiles,
|
|
82
|
+
returnedFileCount,
|
|
83
|
+
filesTruncated,
|
|
84
|
+
filesOmitted,
|
|
55
85
|
source: sourceResult,
|
|
56
86
|
warnings
|
|
57
87
|
};
|
|
@@ -119,8 +149,11 @@ export class ModDecompileService {
|
|
|
119
149
|
async ensureDecompiled(jarPath, warnings) {
|
|
120
150
|
const cacheKey = modDecompileCacheKey(jarPath);
|
|
121
151
|
const cached = this.decompileCache.get(cacheKey);
|
|
122
|
-
if (cached)
|
|
152
|
+
if (cached) {
|
|
153
|
+
this.decompileCache.delete(cacheKey);
|
|
154
|
+
this.decompileCache.set(cacheKey, cached);
|
|
123
155
|
return cached;
|
|
156
|
+
}
|
|
124
157
|
log("info", "mod-decompile.start", { jarPath });
|
|
125
158
|
const startedAt = Date.now();
|
|
126
159
|
// Analyze mod metadata
|
|
@@ -152,10 +185,12 @@ export class ModDecompileService {
|
|
|
152
185
|
};
|
|
153
186
|
this.decompileCache.set(cacheKey, result);
|
|
154
187
|
// Trim cache
|
|
155
|
-
|
|
188
|
+
while (this.decompileCache.size > 8) {
|
|
156
189
|
const oldest = this.decompileCache.keys().next().value;
|
|
157
|
-
if (oldest
|
|
158
|
-
|
|
190
|
+
if (oldest === undefined) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
this.decompileCache.delete(oldest);
|
|
159
194
|
}
|
|
160
195
|
log("info", "mod-decompile.done", {
|
|
161
196
|
jarPath,
|