@adhisang/minecraft-modding-mcp 1.2.1 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +184 -64
  3. package/dist/cli.js +31 -4
  4. package/dist/compat-stdio-transport.d.ts +2 -7
  5. package/dist/compat-stdio-transport.js +12 -154
  6. package/dist/index.js +537 -202
  7. package/dist/json-rpc-framing.d.ts +22 -0
  8. package/dist/json-rpc-framing.js +168 -0
  9. package/dist/mapping-pipeline-service.d.ts +1 -1
  10. package/dist/mapping-pipeline-service.js +13 -5
  11. package/dist/mapping-service.d.ts +12 -4
  12. package/dist/mapping-service.js +222 -105
  13. package/dist/mcp-helpers.d.ts +10 -2
  14. package/dist/mcp-helpers.js +59 -5
  15. package/dist/minecraft-explorer-service.d.ts +1 -2
  16. package/dist/minecraft-explorer-service.js +120 -24
  17. package/dist/mixin-validator.d.ts +24 -2
  18. package/dist/mixin-validator.js +228 -103
  19. package/dist/mod-decompile-service.d.ts +5 -0
  20. package/dist/mod-decompile-service.js +40 -5
  21. package/dist/mod-remap-service.js +142 -30
  22. package/dist/mojang-tiny-mapping-service.js +26 -26
  23. package/dist/path-resolver.js +41 -4
  24. package/dist/registry-service.d.ts +10 -1
  25. package/dist/registry-service.js +154 -22
  26. package/dist/resources.js +7 -7
  27. package/dist/search-hit-accumulator.d.ts +0 -3
  28. package/dist/search-hit-accumulator.js +27 -6
  29. package/dist/source-jar-reader.js +16 -2
  30. package/dist/source-resolver.d.ts +1 -0
  31. package/dist/source-resolver.js +93 -2
  32. package/dist/source-service.d.ts +76 -47
  33. package/dist/source-service.js +1344 -763
  34. package/dist/stdio-supervisor.d.ts +46 -0
  35. package/dist/stdio-supervisor.js +349 -0
  36. package/dist/storage/files-repo.d.ts +3 -0
  37. package/dist/storage/files-repo.js +66 -1
  38. package/dist/storage/migrations.d.ts +1 -1
  39. package/dist/storage/migrations.js +6 -2
  40. package/dist/storage/schema.d.ts +1 -0
  41. package/dist/storage/schema.js +7 -0
  42. package/dist/symbols/symbol-extractor.js +6 -4
  43. package/dist/tool-execution-gate.d.ts +15 -0
  44. package/dist/tool-execution-gate.js +58 -0
  45. package/dist/tool-input.d.ts +6 -0
  46. package/dist/tool-input.js +64 -0
  47. package/dist/types.d.ts +1 -1
  48. package/dist/version-diff-service.js +10 -5
  49. package/dist/version-service.js +7 -2
  50. package/dist/workspace-mapping-service.js +12 -0
  51. package/package.json +4 -1
@@ -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 distance = levenshteinDistance(name.toLowerCase(), candidate.toLowerCase());
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 computeConfidenceScore(healthReport, provenance, remapFailureCount) {
131
- let score = 100;
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
- if (!healthReport.tinyMappingsAvailable)
164
+ }
165
+ if (!healthReport.tinyMappingsAvailable) {
166
+ penalties.push({ reason: "tiny-mappings-unavailable", points: 20 });
136
167
  score -= 20;
137
- if (!healthReport.memberRemapAvailable)
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
- if (provenance && provenance.requestedMapping !== provenance.mappingApplied)
177
+ }
178
+ if (provenance && provenance.requestedMapping !== provenance.mappingApplied) {
179
+ penalties.push({ reason: "mapping-mismatch", points: 15 });
143
180
  score -= 15;
144
- score -= Math.min(remapFailureCount * 2, 20);
145
- return Math.max(score, 0);
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) {
@@ -161,7 +276,7 @@ function validateInjection(inj, targetMembers, targetNames, issues, resolvedMemb
161
276
  const isSigFailed = signatureFailedTargets?.has(targetName);
162
277
  const issueConfidence = isRemapFailed ? "uncertain" : confidence;
163
278
  const issueConfidenceReason = isRemapFailed
164
- ? `Member remap from official→mapping failed; name mismatch may be a remap artifact, not a true missing member.`
279
+ ? `Member remap from obfuscated→mapping failed; name mismatch may be a remap artifact, not a true missing member.`
165
280
  : confidenceReason;
166
281
  const resolutionPath = isRemapFailed
167
282
  ? "member-remap-failed"
@@ -207,7 +322,7 @@ function validateShadow(shadow, targetMembers, targetNames, issues, resolvedMemb
207
322
  const isSigFailed = signatureFailedTargets?.has(targetName);
208
323
  const issueConfidence = isRemapFailed ? "uncertain" : confidence;
209
324
  const issueConfidenceReason = isRemapFailed
210
- ? `Member remap from official→mapping failed; name mismatch may be a remap artifact, not a true missing member.`
325
+ ? `Member remap from obfuscated→mapping failed; name mismatch may be a remap artifact, not a true missing member.`
211
326
  : confidenceReason;
212
327
  const resolutionPath = isRemapFailed
213
328
  ? "member-remap-failed"
@@ -274,7 +389,7 @@ function validateAccessor(accessor, targetMembers, targetNames, issues, resolved
274
389
  const isSigFailed = signatureFailedTargets?.has(targetName);
275
390
  const issueConfidence = isRemapFailed ? "uncertain" : confidence;
276
391
  const issueConfidenceReason = isRemapFailed
277
- ? `Member remap from official→mapping failed; name mismatch may be a remap artifact, not a true missing member.`
392
+ ? `Member remap from obfuscated→mapping failed; name mismatch may be a remap artifact, not a true missing member.`
278
393
  : confidenceReason;
279
394
  const resolutionPath = isRemapFailed
280
395
  ? "member-remap-failed"
@@ -323,9 +438,9 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
323
438
  kind: "target-mapping-failed",
324
439
  annotation: "@Mixin",
325
440
  target: target.className,
326
- message: `Could not map target class "${target.className}" to official namespace; class may still exist under a different mapping.`,
441
+ message: `Could not map target class "${target.className}" to obfuscated namespace; class may still exist under a different mapping.`,
327
442
  confidence: "uncertain",
328
- confidenceReason: `Mapping from "${provenance?.requestedMapping}" to official failed for this class.`,
443
+ confidenceReason: `Mapping from "${provenance?.requestedMapping}" to obfuscated failed for this class.`,
329
444
  category: "mapping",
330
445
  resolutionPath: "target-mapping-failed",
331
446
  falsePositiveRisk: healthReport?.overallHealthy === false ? "high" : "medium"
@@ -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: "target-not-found",
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
- // 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
- }
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: degraded ? "warning" : "error",
380
- kind: "target-not-found",
468
+ severity: "warning",
469
+ kind: "validation-incomplete",
381
470
  annotation: "@Mixin",
382
471
  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,
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
- falsePositiveRisk: degraded ? "high" : undefined
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(resolvedMembers.filter((m) => m.status === "resolved").map((m) => m.annotation));
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
- const toolPaths = ["target-mapping-failed", "member-remap-failed", "source-signature-unavailable"];
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
- // Build context fields to spread into suggestedCall params
476
- const ctx = {};
583
+ const symbolLookupContext = {};
584
+ if (suggestedCallContext?.sourcePriority) {
585
+ symbolLookupContext.sourcePriority = suggestedCallContext.sourcePriority;
586
+ }
587
+ const classSourceContext = {};
477
588
  if (suggestedCallContext?.scope)
478
- ctx.scope = suggestedCallContext.scope;
589
+ classSourceContext.scope = suggestedCallContext.scope;
479
590
  if (suggestedCallContext?.sourcePriority)
480
- ctx.sourcePriority = suggestedCallContext.sourcePriority;
591
+ classSourceContext.sourcePriority = suggestedCallContext.sourcePriority;
481
592
  if (suggestedCallContext?.projectPath)
482
- ctx.projectPath = suggestedCallContext.projectPath;
593
+ classSourceContext.projectPath = suggestedCallContext.projectPath;
483
594
  if (suggestedCallContext?.mapping)
484
- ctx.mapping = suggestedCallContext.mapping;
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", ...ctx }
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", ...ctx }
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: { className, targetKind: "version", targetValue: version, ...(mapping ? { mapping } : {}), mode: "metadata", ...ctx }
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, ...ctx }
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
- // 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
- }));
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 confidenceScore = healthReport
575
- ? computeConfidenceScore(healthReport, provenance, remapFailureCount)
695
+ const memberSummary = summarizeResolvedMembers(resolvedMembers);
696
+ const confidenceBreakdown = healthReport
697
+ ? computeConfidenceBreakdown(healthReport, provenance, remapFailureCount, memberSummary.membersSkipped)
576
698
  : undefined;
577
- // Build quick summary
699
+ const confidenceScore = confidenceBreakdown?.score;
578
700
  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).`;
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: input.className ? undefined : files.map(filePathToClassName),
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
- if (this.decompileCache.size > 8) {
188
+ while (this.decompileCache.size > 8) {
156
189
  const oldest = this.decompileCache.keys().next().value;
157
- if (oldest !== undefined)
158
- this.decompileCache.delete(oldest);
190
+ if (oldest === undefined) {
191
+ break;
192
+ }
193
+ this.decompileCache.delete(oldest);
159
194
  }
160
195
  log("info", "mod-decompile.done", {
161
196
  jarPath,