@adhisang/minecraft-modding-mcp 4.2.0 → 5.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.
- package/CHANGELOG.md +40 -0
- package/README.md +6 -4
- package/dist/access-transformer-parser.d.ts +2 -1
- package/dist/access-transformer-parser.js +10 -1
- package/dist/build-suggested-call.js +31 -10
- package/dist/cache-registry.d.ts +8 -0
- package/dist/cache-registry.js +99 -6
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
- package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
- package/dist/entry-tools/analyze-symbol-service.js +124 -28
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-members-service.js +6 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-source-service.js +5 -6
- package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
- package/dist/entry-tools/batch-mappings-service.js +4 -5
- package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
- package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
- package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
- package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -2
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +1 -1
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
- package/dist/entry-tools/inspect-minecraft/internal.js +30 -14
- package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/response-contract.d.ts +1 -1
- package/dist/entry-tools/response-contract.js +1 -0
- package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
- package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
- package/dist/entry-tools/validate-project-service.d.ts +42 -36
- package/dist/entry-tools/validate-project-service.js +3 -1
- package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
- package/dist/entry-tools/verify-mixin-target-service.js +113 -22
- package/dist/error-mapping.d.ts +51 -0
- package/dist/error-mapping.js +157 -2
- package/dist/errors.js +4 -1
- package/dist/index.js +138 -71
- package/dist/java-process.d.ts +9 -0
- package/dist/java-process.js +11 -5
- package/dist/mapping/internal-types.d.ts +7 -0
- package/dist/mapping/lookup.d.ts +1 -1
- package/dist/mapping/lookup.js +7 -1
- package/dist/mapping/types.d.ts +6 -0
- package/dist/mapping-service.d.ts +26 -2
- package/dist/mapping-service.js +178 -36
- package/dist/mcp-helpers.d.ts +1 -0
- package/dist/mcp-helpers.js +25 -3
- package/dist/minecraft-explorer-service.js +24 -11
- package/dist/mixin/access-validators.js +33 -1
- package/dist/mixin/annotation-validators.js +53 -7
- package/dist/mixin/parsed-validator.js +2 -2
- package/dist/mixin-parser.js +48 -4
- package/dist/mod-decompile-service.js +2 -1
- package/dist/mod-remap-service.d.ts +20 -1
- package/dist/mod-remap-service.js +95 -40
- package/dist/nbt/java-nbt-codec.js +87 -2
- package/dist/observability.d.ts +6 -0
- package/dist/observability.js +10 -0
- package/dist/page-cursor.d.ts +17 -0
- package/dist/page-cursor.js +0 -0
- package/dist/registry-service.js +25 -1
- package/dist/resources.js +45 -9
- package/dist/response-utils.d.ts +29 -4
- package/dist/response-utils.js +95 -5
- package/dist/source/access-validate.js +12 -2
- package/dist/source/artifact-resolver.js +1 -1
- package/dist/source/cache-metrics.js +2 -2
- package/dist/source/class-source/members-builder.d.ts +44 -1
- package/dist/source/class-source/members-builder.js +51 -4
- package/dist/source/class-source/snippet-builder.d.ts +13 -0
- package/dist/source/class-source/snippet-builder.js +48 -4
- package/dist/source/class-source-helpers.js +67 -11
- package/dist/source/class-source.js +97 -20
- package/dist/source/indexer.d.ts +31 -6
- package/dist/source/indexer.js +95 -25
- package/dist/source/search.d.ts +1 -1
- package/dist/source/search.js +94 -22
- package/dist/source/validate-mixin.d.ts +2 -1
- package/dist/source/validate-mixin.js +45 -4
- package/dist/source/workspace-target.js +15 -9
- package/dist/source-jar-reader.d.ts +54 -1
- package/dist/source-jar-reader.js +121 -10
- package/dist/source-service.d.ts +28 -7
- package/dist/storage/files-repo.d.ts +12 -0
- package/dist/storage/files-repo.js +24 -0
- package/dist/storage/symbols-repo.d.ts +6 -0
- package/dist/storage/symbols-repo.js +6 -0
- package/dist/symbols/symbol-extractor.js +35 -6
- package/dist/text-truncate.d.ts +7 -0
- package/dist/text-truncate.js +21 -0
- package/dist/tiny-remapper-resolver.js +10 -0
- package/dist/tiny-remapper-service.js +3 -3
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +12 -1
- package/dist/tool-guidance.d.ts +4 -1
- package/dist/tool-guidance.js +37 -12
- package/dist/tool-schemas.d.ts +559 -494
- package/dist/tool-schemas.js +99 -63
- package/dist/types.d.ts +1 -0
- package/dist/version-diff-service.js +5 -0
- package/dist/version-service.d.ts +3 -0
- package/dist/version-service.js +18 -2
- package/dist/warning-details.d.ts +18 -0
- package/dist/warning-details.js +63 -0
- package/docs/README-ja.md +2 -2
- package/docs/examples.md +26 -2
- package/docs/tool-reference.md +23 -17
- package/package.json +1 -1
package/dist/mapping/types.d.ts
CHANGED
|
@@ -55,6 +55,7 @@ export type FindMappingInput = {
|
|
|
55
55
|
name: string;
|
|
56
56
|
owner?: string;
|
|
57
57
|
descriptor?: string;
|
|
58
|
+
nameMode?: "fqcn" | "auto";
|
|
58
59
|
signatureMode?: "exact" | "name-only";
|
|
59
60
|
sourceMapping: SourceMapping;
|
|
60
61
|
targetMapping: SourceMapping;
|
|
@@ -103,6 +104,7 @@ export type ClassApiMatrixInput = {
|
|
|
103
104
|
gradleUserHome?: string;
|
|
104
105
|
includeKinds?: ClassApiMatrixKind[];
|
|
105
106
|
maxRows?: number;
|
|
107
|
+
cursor?: string;
|
|
106
108
|
};
|
|
107
109
|
export type ClassApiMatrixEntry = {
|
|
108
110
|
symbol: string;
|
|
@@ -127,6 +129,10 @@ export type ClassApiMatrixOutput = {
|
|
|
127
129
|
rows: ClassApiMatrixRow[];
|
|
128
130
|
rowCount: number;
|
|
129
131
|
rowsTruncated?: boolean;
|
|
132
|
+
/** Continuation cursor when more rows remain; pass back as cursor for the next page. */
|
|
133
|
+
nextCursor?: string;
|
|
134
|
+
/** True when a provided cursor was malformed or belonged to a different query and was ignored. */
|
|
135
|
+
cursorIgnored?: boolean;
|
|
130
136
|
warnings: string[];
|
|
131
137
|
ambiguousRowCount?: number;
|
|
132
138
|
};
|
|
@@ -14,11 +14,21 @@ export declare class MappingService {
|
|
|
14
14
|
private static readonly RESOLUTION_CACHE_TTL_MS;
|
|
15
15
|
private resolutionCacheHits;
|
|
16
16
|
private resolutionCacheMisses;
|
|
17
|
+
private classProjectionComputes;
|
|
18
|
+
private apiMatrixRowMaps;
|
|
17
19
|
get resolutionCacheStats(): {
|
|
18
20
|
hits: number;
|
|
19
21
|
misses: number;
|
|
20
22
|
size: number;
|
|
21
23
|
};
|
|
24
|
+
/** Test seam: counts genuine class-projection computes (cache misses). */
|
|
25
|
+
get classProjectionStats(): {
|
|
26
|
+
computes: number;
|
|
27
|
+
};
|
|
28
|
+
/** Test seam: counts genuine per-row cross-namespace path walks in getClassApiMatrix. */
|
|
29
|
+
get apiMatrixStats(): {
|
|
30
|
+
rowMaps: number;
|
|
31
|
+
};
|
|
22
32
|
constructor(config: Config, versionService?: VersionMappingsResolver, fetchFn?: typeof fetch);
|
|
23
33
|
findMapping(input: FindMappingInput): Promise<FindMappingOutput>;
|
|
24
34
|
ensureMappingAvailable(input: EnsureMappingAvailableInput): Promise<EnsureMappingAvailableOutput>;
|
|
@@ -60,13 +70,27 @@ export declare class MappingService {
|
|
|
60
70
|
private buildResolutionCacheKey;
|
|
61
71
|
private trimResolutionCache;
|
|
62
72
|
}
|
|
73
|
+
export type ResolvedTinyMapping = {
|
|
74
|
+
path: string;
|
|
75
|
+
coordinate?: string;
|
|
76
|
+
};
|
|
63
77
|
/**
|
|
64
78
|
* Resolve and cache a Tiny v2 mapping file for the given Minecraft version.
|
|
65
79
|
*
|
|
80
|
+
* Intermediary is version-keyed (immutable per release). Yarn is coordinate-keyed:
|
|
81
|
+
* Fabric publishes incrementing yarn builds for the same MC version, so the tiny
|
|
82
|
+
* is cached at `mappings/yarn/${coordinate}.tiny` and the newest coordinate is
|
|
83
|
+
* re-checked once per {@link YARN_METADATA_TTL_MS}. The resolved yarn coordinate
|
|
84
|
+
* is returned so callers can key their own caches on the actual mappings used.
|
|
85
|
+
*
|
|
66
86
|
* @param version - Minecraft version (e.g. "1.20.4")
|
|
67
87
|
* @param mapping - "intermediary" or "yarn"
|
|
68
88
|
* @param cacheDir - The application cache directory
|
|
69
89
|
* @param fetchFn - Optional fetch implementation for testing
|
|
70
|
-
* @
|
|
90
|
+
* @param options - forceRefresh bypasses the yarn metadata TTL; now overrides the clock (testing)
|
|
91
|
+
* @returns The extracted Tiny v2 path and, for yarn, the resolved build coordinate
|
|
71
92
|
*/
|
|
72
|
-
export declare function resolveTinyMappingFile(version: string, mapping: "intermediary" | "yarn", cacheDir: string, fetchFn?: typeof fetch
|
|
93
|
+
export declare function resolveTinyMappingFile(version: string, mapping: "intermediary" | "yarn", cacheDir: string, fetchFn?: typeof fetch, options?: {
|
|
94
|
+
forceRefresh?: boolean;
|
|
95
|
+
now?: () => number;
|
|
96
|
+
}): Promise<ResolvedTinyMapping>;
|
package/dist/mapping-service.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { buildSuggestedCall } from "./build-suggested-call.js";
|
|
5
5
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
6
|
+
import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "./page-cursor.js";
|
|
6
7
|
import { resolveGradleUserHomePath } from "./gradle-paths.js";
|
|
7
8
|
import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
|
|
8
9
|
import { collectMatchedJarEntriesAsUtf8 } from "./source-jar-reader.js";
|
|
@@ -32,6 +33,8 @@ export class MappingService {
|
|
|
32
33
|
static RESOLUTION_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
33
34
|
resolutionCacheHits = 0;
|
|
34
35
|
resolutionCacheMisses = 0;
|
|
36
|
+
classProjectionComputes = 0;
|
|
37
|
+
apiMatrixRowMaps = 0;
|
|
35
38
|
get resolutionCacheStats() {
|
|
36
39
|
return {
|
|
37
40
|
hits: this.resolutionCacheHits,
|
|
@@ -39,6 +42,14 @@ export class MappingService {
|
|
|
39
42
|
size: this.resolutionCache.size
|
|
40
43
|
};
|
|
41
44
|
}
|
|
45
|
+
/** Test seam: counts genuine class-projection computes (cache misses). */
|
|
46
|
+
get classProjectionStats() {
|
|
47
|
+
return { computes: this.classProjectionComputes };
|
|
48
|
+
}
|
|
49
|
+
/** Test seam: counts genuine per-row cross-namespace path walks in getClassApiMatrix. */
|
|
50
|
+
get apiMatrixStats() {
|
|
51
|
+
return { rowMaps: this.apiMatrixRowMaps };
|
|
52
|
+
}
|
|
42
53
|
constructor(config, versionService = new VersionService(config), fetchFn = globalThis.fetch) {
|
|
43
54
|
this.config = config;
|
|
44
55
|
this.versionService = versionService;
|
|
@@ -65,7 +76,7 @@ export class MappingService {
|
|
|
65
76
|
// explicitly.
|
|
66
77
|
const effectiveSignatureMode = input.signatureMode ?? "name-only";
|
|
67
78
|
const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, effectiveSignatureMode, {
|
|
68
|
-
allowShortClassName: input.kind === "class" && input.sourceMapping === "obfuscated"
|
|
79
|
+
allowShortClassName: input.kind === "class" && (input.sourceMapping === "obfuscated" || input.nameMode === "auto")
|
|
69
80
|
});
|
|
70
81
|
const cacheKey = this.buildResolutionCacheKey(version, input, querySymbol, effectiveSignatureMode);
|
|
71
82
|
const cached = this.resolutionCache.get(cacheKey);
|
|
@@ -176,7 +187,7 @@ export class MappingService {
|
|
|
176
187
|
}
|
|
177
188
|
rawCandidates = rawCandidates.filter((candidate) => candidate.descriptor !== undefined && acceptedDescriptors.has(candidate.descriptor));
|
|
178
189
|
}
|
|
179
|
-
const disambiguatedCandidates = applyDisambiguationHints(rawCandidates, input.disambiguation);
|
|
190
|
+
const disambiguatedCandidates = applyDisambiguationHints(rawCandidates, input.disambiguation, warnings);
|
|
180
191
|
if (rawCandidates.length > disambiguatedCandidates.length) {
|
|
181
192
|
warnings.push(`Disambiguation hints narrowed candidates from ${rawCandidates.length} to ${disambiguatedCandidates.length}.`);
|
|
182
193
|
}
|
|
@@ -511,9 +522,6 @@ export class MappingService {
|
|
|
511
522
|
}
|
|
512
523
|
return includeKinds.has("method");
|
|
513
524
|
});
|
|
514
|
-
const rows = [];
|
|
515
|
-
let ambiguousRowCount = 0;
|
|
516
|
-
const rowSeen = new Set();
|
|
517
525
|
const rowKindOrder = {
|
|
518
526
|
class: 0,
|
|
519
527
|
field: 1,
|
|
@@ -530,12 +538,46 @@ export class MappingService {
|
|
|
530
538
|
}
|
|
531
539
|
return left.symbol.localeCompare(right.symbol);
|
|
532
540
|
});
|
|
541
|
+
// Dedup up front using the base-only symbol key. The sort key and buildSymbolKey
|
|
542
|
+
// read only base-namespace fields, so this stable first-occurrence-wins order is
|
|
543
|
+
// fully determined WITHOUT any cross-namespace mapping. That lets rowCount /
|
|
544
|
+
// nextCursor / row order stay byte-identical while we map only the requested page.
|
|
545
|
+
const rowSeen = new Set();
|
|
546
|
+
const dedupedBase = [];
|
|
533
547
|
for (const baseRecord of sortedBase) {
|
|
534
548
|
const key = buildSymbolKey(baseRecord);
|
|
535
549
|
if (rowSeen.has(key)) {
|
|
536
550
|
continue;
|
|
537
551
|
}
|
|
538
552
|
rowSeen.add(key);
|
|
553
|
+
dedupedBase.push(baseRecord);
|
|
554
|
+
}
|
|
555
|
+
const rowCount = dedupedBase.length;
|
|
556
|
+
const rowLimit = clampRowLimit(input.maxRows);
|
|
557
|
+
if (input.maxRows != null && Number.isFinite(input.maxRows) && input.maxRows > 5000) {
|
|
558
|
+
warnings.push(`maxRows was clamped to 5000 from ${input.maxRows}.`);
|
|
559
|
+
}
|
|
560
|
+
// Offset cursor over the stable row order; the context key ties a cursor to
|
|
561
|
+
// this exact query so a stale cursor restarts from the first page.
|
|
562
|
+
const rowCursorContext = buildPageContextKey([
|
|
563
|
+
version,
|
|
564
|
+
className,
|
|
565
|
+
classNameMapping,
|
|
566
|
+
(input.includeKinds ?? []).join(","),
|
|
567
|
+
input.sourcePriority
|
|
568
|
+
]);
|
|
569
|
+
const { offset: rowOffset, cursorIgnored: rowCursorIgnored } = resolveCursorOffset(input.cursor, rowCursorContext);
|
|
570
|
+
const windowBase = rowLimit != null ? dedupedBase.slice(rowOffset, rowOffset + rowLimit) : dedupedBase.slice(rowOffset);
|
|
571
|
+
const consumedRows = rowOffset + windowBase.length;
|
|
572
|
+
const rowsTruncated = consumedRows < rowCount;
|
|
573
|
+
const nextCursor = rowsTruncated ? encodeOffsetCursor(consumedRows, rowCursorContext) : undefined;
|
|
574
|
+
// Map ONLY the requested window. Per-row mapping cost is therefore O(window),
|
|
575
|
+
// not O(rowCount). warnings/ambiguousRowCount are accordingly page-scoped on
|
|
576
|
+
// paginated calls (caveat B1) and byte-identical to the old full-set values when
|
|
577
|
+
// no maxRows/cursor narrows the window.
|
|
578
|
+
const rows = [];
|
|
579
|
+
let ambiguousRowCount = 0;
|
|
580
|
+
for (const baseRecord of windowBase) {
|
|
539
581
|
let rowHadAmbiguity = false;
|
|
540
582
|
const row = {
|
|
541
583
|
kind: baseRecord.kind,
|
|
@@ -584,9 +626,6 @@ export class MappingService {
|
|
|
584
626
|
ambiguousRowCount += 1;
|
|
585
627
|
}
|
|
586
628
|
}
|
|
587
|
-
const rowCount = rows.length;
|
|
588
|
-
const rowLimit = clampRowLimit(input.maxRows);
|
|
589
|
-
const limitedRows = rowLimit != null && rowCount > rowLimit ? rows.slice(0, rowLimit) : rows;
|
|
590
629
|
return {
|
|
591
630
|
version,
|
|
592
631
|
className,
|
|
@@ -597,9 +636,11 @@ export class MappingService {
|
|
|
597
636
|
intermediary: classByMapping.intermediary?.symbol,
|
|
598
637
|
yarn: classByMapping.yarn?.symbol
|
|
599
638
|
},
|
|
600
|
-
rows
|
|
639
|
+
rows,
|
|
601
640
|
rowCount,
|
|
602
|
-
rowsTruncated:
|
|
641
|
+
rowsTruncated: rowsTruncated ? true : undefined,
|
|
642
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
643
|
+
...(rowCursorIgnored ? { cursorIgnored: true } : {}),
|
|
603
644
|
warnings,
|
|
604
645
|
ambiguousRowCount: ambiguousRowCount > 0 ? ambiguousRowCount : undefined
|
|
605
646
|
};
|
|
@@ -632,6 +673,11 @@ export class MappingService {
|
|
|
632
673
|
sourcePriorityApplied: priority
|
|
633
674
|
};
|
|
634
675
|
const classNameMode = input.nameMode === "auto" ? "auto" : "fqcn";
|
|
676
|
+
// Normalize the effective signatureMode exactly once (mirrors findMapping) so an omitted
|
|
677
|
+
// signatureMode reaching the service — direct/internal callers, MCP resource handlers, and
|
|
678
|
+
// batch-symbol-exists entries — defaults to name-only instead of taking the strict
|
|
679
|
+
// descriptor-required path. Callers needing strict matching pass signatureMode: "exact".
|
|
680
|
+
const effectiveSignatureMode = input.signatureMode ?? "name-only";
|
|
635
681
|
const normalizedQuery = input.kind === "class" && classNameMode === "auto"
|
|
636
682
|
? (() => {
|
|
637
683
|
const owner = input.owner?.trim();
|
|
@@ -663,7 +709,7 @@ export class MappingService {
|
|
|
663
709
|
};
|
|
664
710
|
})()
|
|
665
711
|
: (() => {
|
|
666
|
-
const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input,
|
|
712
|
+
const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, effectiveSignatureMode);
|
|
667
713
|
return {
|
|
668
714
|
mode: "strict",
|
|
669
715
|
queryRecord,
|
|
@@ -729,7 +775,7 @@ export class MappingService {
|
|
|
729
775
|
}
|
|
730
776
|
const methodCandidates = records.filter((record) => record.kind === "method" && record.owner === queryRecord.owner && record.name === queryRecord.name);
|
|
731
777
|
// name-only mode: skip descriptor matching, resolve by owner+name
|
|
732
|
-
if (
|
|
778
|
+
if (effectiveSignatureMode === "name-only") {
|
|
733
779
|
const status = methodCandidates.length === 1 ? "resolved" : methodCandidates.length > 1 ? "ambiguous" : "not_found";
|
|
734
780
|
if (status === "ambiguous") {
|
|
735
781
|
// name-only discards any supplied descriptor, so telling the caller to "provide
|
|
@@ -754,18 +800,27 @@ export class MappingService {
|
|
|
754
800
|
// and produce false negatives in the most common lookup shape. When no class references
|
|
755
801
|
// exist at all (primitives-only descriptors such as `(I)V`) the projector marks
|
|
756
802
|
// `hadClassReferences === false` and we simply reuse the original descriptor.
|
|
803
|
+
// Stored descriptors carry whatever coordinate the tiny column used: obfuscated
|
|
804
|
+
// for merged graphs, but intermediary for a standalone Fabric yarn tiny
|
|
805
|
+
// (`tiny 2 0 intermediary named`). The descriptor namespace is not recorded
|
|
806
|
+
// per record, so project the query descriptor along every reachable base
|
|
807
|
+
// namespace and accept a match in any of them rather than assuming obfuscated.
|
|
757
808
|
const queryDescriptor = queryRecord.descriptor;
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
809
|
+
const candidateDescriptors = new Set([queryDescriptor]);
|
|
810
|
+
for (const baseNamespace of ["obfuscated", "intermediary"]) {
|
|
811
|
+
if (sourceMapping === baseNamespace) {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const projectionPath = namespacePath(graph, sourceMapping, baseNamespace);
|
|
815
|
+
if (!projectionPath) {
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const projection = this.projectMethodDescriptorToTarget(graph, projectionPath, queryDescriptor);
|
|
819
|
+
if (projection.hadClassReferences) {
|
|
820
|
+
candidateDescriptors.add(projection.descriptor);
|
|
766
821
|
}
|
|
767
822
|
}
|
|
768
|
-
const descriptorMatched = methodCandidates.filter((record) => record.descriptor
|
|
823
|
+
const descriptorMatched = methodCandidates.filter((record) => record.descriptor != null && candidateDescriptors.has(record.descriptor));
|
|
769
824
|
if (descriptorMatched.length === 1) {
|
|
770
825
|
return buildOutput(querySymbol, descriptorMatched, "resolved");
|
|
771
826
|
}
|
|
@@ -782,6 +837,9 @@ export class MappingService {
|
|
|
782
837
|
if (sourceMapping === targetMapping) {
|
|
783
838
|
return [record];
|
|
784
839
|
}
|
|
840
|
+
// Count genuine cross-namespace path walks (the per-row cost getClassApiMatrix
|
|
841
|
+
// now scopes to the requested window); test seam via apiMatrixStats.
|
|
842
|
+
this.apiMatrixRowMaps += 1;
|
|
785
843
|
const path = resolvedPath ?? namespacePath(graph, sourceMapping, targetMapping);
|
|
786
844
|
if (!path) {
|
|
787
845
|
return [];
|
|
@@ -886,22 +944,35 @@ export class MappingService {
|
|
|
886
944
|
projectMethodDescriptorToTarget(graph, path, descriptor) {
|
|
887
945
|
let hadClassReferences = false;
|
|
888
946
|
let complete = true;
|
|
889
|
-
|
|
947
|
+
// Class-to-class projection depends only on (graph, path, internalName) — not on
|
|
948
|
+
// the descriptor — so memoize it on the graph and share it across every member
|
|
949
|
+
// lookup on this graph instead of rebuilding a fresh cache per call. A `null`
|
|
950
|
+
// entry memoizes an unmapped/ambiguous class so a repeat reference stays
|
|
951
|
+
// `complete=false` without recomputing.
|
|
952
|
+
const projectionKey = path.join(">");
|
|
953
|
+
const classProjectionCache = graph.classProjectionCache;
|
|
890
954
|
const projectedDescriptor = descriptor.replace(/L([^;]+);/g, (fullMatch, internalName) => {
|
|
891
955
|
hadClassReferences = true;
|
|
892
|
-
const
|
|
893
|
-
|
|
956
|
+
const cacheKey = `${projectionKey}\0${internalName}`;
|
|
957
|
+
const cached = classProjectionCache.get(cacheKey);
|
|
958
|
+
if (cached !== undefined) {
|
|
959
|
+
if (cached === null) {
|
|
960
|
+
complete = false;
|
|
961
|
+
return fullMatch;
|
|
962
|
+
}
|
|
894
963
|
return `L${cached};`;
|
|
895
964
|
}
|
|
965
|
+
this.classProjectionComputes += 1;
|
|
896
966
|
const projectedClassCandidates = this
|
|
897
967
|
.mapCandidatesAlongPath(graph, path, createClassSymbolRecord(internalName.replace(/\//g, ".")))
|
|
898
968
|
.filter((candidate) => candidate.kind === "class");
|
|
899
969
|
if (projectedClassCandidates.length !== 1) {
|
|
900
970
|
complete = false;
|
|
971
|
+
classProjectionCache.set(cacheKey, null);
|
|
901
972
|
return fullMatch;
|
|
902
973
|
}
|
|
903
974
|
const projectedInternalName = projectedClassCandidates[0].symbol.replace(/\./g, "/");
|
|
904
|
-
classProjectionCache.set(
|
|
975
|
+
classProjectionCache.set(cacheKey, projectedInternalName);
|
|
905
976
|
return `L${projectedInternalName};`;
|
|
906
977
|
});
|
|
907
978
|
return {
|
|
@@ -1031,6 +1102,7 @@ export class MappingService {
|
|
|
1031
1102
|
adjacency: new Map(),
|
|
1032
1103
|
pathCache: new Map(),
|
|
1033
1104
|
recordsByTarget: new Map(),
|
|
1105
|
+
classProjectionCache: new Map(),
|
|
1034
1106
|
warnings: [
|
|
1035
1107
|
`Version ${version} is unobfuscated; mapping graph is empty because the runtime already uses deobfuscated names.`
|
|
1036
1108
|
]
|
|
@@ -1044,6 +1116,7 @@ export class MappingService {
|
|
|
1044
1116
|
adjacency: new Map(),
|
|
1045
1117
|
pathCache: new Map(),
|
|
1046
1118
|
recordsByTarget: new Map(),
|
|
1119
|
+
classProjectionCache: new Map(),
|
|
1047
1120
|
warnings: []
|
|
1048
1121
|
};
|
|
1049
1122
|
const mojangLoad = await this.loadMojangPairs(version);
|
|
@@ -1212,22 +1285,63 @@ async function extractTinyFromJar(jarPath, outputPath) {
|
|
|
1212
1285
|
await writeFile(outputPath, tinyEntry.content, "utf8");
|
|
1213
1286
|
return true;
|
|
1214
1287
|
}
|
|
1288
|
+
/**
|
|
1289
|
+
* How long a resolved yarn build coordinate is trusted before re-checking the
|
|
1290
|
+
* Fabric Maven metadata for a newer build. Within this window, resolving a yarn
|
|
1291
|
+
* tiny is zero-network (it reuses the recorded coordinate); after it expires the
|
|
1292
|
+
* next resolve does ONE metadata fetch. This fence keeps the metadata fetch off
|
|
1293
|
+
* the remap cache-hit fast path while still picking up new yarn builds.
|
|
1294
|
+
*/
|
|
1295
|
+
const YARN_METADATA_TTL_MS = 24 * 60 * 60 * 1000;
|
|
1296
|
+
function yarnMetaPath(cacheDir, version) {
|
|
1297
|
+
return join(cacheDir, "mappings", "yarn", `${version}.meta.json`);
|
|
1298
|
+
}
|
|
1299
|
+
function yarnCoordinateTinyPath(cacheDir, coordinate) {
|
|
1300
|
+
return join(cacheDir, "mappings", "yarn", `${coordinate}.tiny`);
|
|
1301
|
+
}
|
|
1302
|
+
function readYarnCoordinateMeta(metaPath) {
|
|
1303
|
+
if (!existsSync(metaPath)) {
|
|
1304
|
+
return undefined;
|
|
1305
|
+
}
|
|
1306
|
+
try {
|
|
1307
|
+
const parsed = JSON.parse(readFileSync(metaPath, "utf8"));
|
|
1308
|
+
if (typeof parsed.coordinate === "string" && typeof parsed.fetchedAt === "number") {
|
|
1309
|
+
return { coordinate: parsed.coordinate, fetchedAt: parsed.fetchedAt };
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
catch {
|
|
1313
|
+
// Corrupt marker -> treat as absent and re-resolve.
|
|
1314
|
+
}
|
|
1315
|
+
return undefined;
|
|
1316
|
+
}
|
|
1317
|
+
function writeYarnCoordinateMeta(metaPath, meta) {
|
|
1318
|
+
mkdirSync(dirname(metaPath), { recursive: true });
|
|
1319
|
+
writeFileSync(metaPath, JSON.stringify(meta));
|
|
1320
|
+
}
|
|
1215
1321
|
/**
|
|
1216
1322
|
* Resolve and cache a Tiny v2 mapping file for the given Minecraft version.
|
|
1217
1323
|
*
|
|
1324
|
+
* Intermediary is version-keyed (immutable per release). Yarn is coordinate-keyed:
|
|
1325
|
+
* Fabric publishes incrementing yarn builds for the same MC version, so the tiny
|
|
1326
|
+
* is cached at `mappings/yarn/${coordinate}.tiny` and the newest coordinate is
|
|
1327
|
+
* re-checked once per {@link YARN_METADATA_TTL_MS}. The resolved yarn coordinate
|
|
1328
|
+
* is returned so callers can key their own caches on the actual mappings used.
|
|
1329
|
+
*
|
|
1218
1330
|
* @param version - Minecraft version (e.g. "1.20.4")
|
|
1219
1331
|
* @param mapping - "intermediary" or "yarn"
|
|
1220
1332
|
* @param cacheDir - The application cache directory
|
|
1221
1333
|
* @param fetchFn - Optional fetch implementation for testing
|
|
1222
|
-
* @
|
|
1334
|
+
* @param options - forceRefresh bypasses the yarn metadata TTL; now overrides the clock (testing)
|
|
1335
|
+
* @returns The extracted Tiny v2 path and, for yarn, the resolved build coordinate
|
|
1223
1336
|
*/
|
|
1224
|
-
export async function resolveTinyMappingFile(version, mapping, cacheDir, fetchFn) {
|
|
1225
|
-
const cachedTiny = join(cacheDir, "mappings", `${version}-${mapping}.tiny`);
|
|
1226
|
-
if (existsSync(cachedTiny)) {
|
|
1227
|
-
return cachedTiny;
|
|
1228
|
-
}
|
|
1337
|
+
export async function resolveTinyMappingFile(version, mapping, cacheDir, fetchFn, options) {
|
|
1229
1338
|
const effectiveFetch = fetchFn ?? globalThis.fetch;
|
|
1230
1339
|
if (mapping === "intermediary") {
|
|
1340
|
+
// Intermediary is immutable per MC release: version-keyed cache is correct.
|
|
1341
|
+
const cachedTiny = join(cacheDir, "mappings", `${version}-intermediary.tiny`);
|
|
1342
|
+
if (existsSync(cachedTiny)) {
|
|
1343
|
+
return { path: cachedTiny };
|
|
1344
|
+
}
|
|
1231
1345
|
const url = `${FABRIC_MAVEN}/net/fabricmc/intermediary/${version}/intermediary-${version}-v2.jar`;
|
|
1232
1346
|
const jarDest = defaultDownloadPath(cacheDir, url);
|
|
1233
1347
|
const downloaded = await downloadToCache(url, jarDest, {
|
|
@@ -1250,11 +1364,30 @@ export async function resolveTinyMappingFile(version, mapping, cacheDir, fetchFn
|
|
|
1250
1364
|
details: { version, jarPath: downloaded.path }
|
|
1251
1365
|
});
|
|
1252
1366
|
}
|
|
1253
|
-
return cachedTiny;
|
|
1367
|
+
return { path: cachedTiny };
|
|
1368
|
+
}
|
|
1369
|
+
// yarn: coordinate-keyed + metadata-TTL fenced.
|
|
1370
|
+
const nowFn = options?.now ?? Date.now;
|
|
1371
|
+
const metaPath = yarnMetaPath(cacheDir, version);
|
|
1372
|
+
const existingMeta = readYarnCoordinateMeta(metaPath);
|
|
1373
|
+
// Within-TTL fast path: reuse the recorded coordinate with zero network.
|
|
1374
|
+
if (!options?.forceRefresh && existingMeta && nowFn() - existingMeta.fetchedAt < YARN_METADATA_TTL_MS) {
|
|
1375
|
+
const cachedCoordTiny = yarnCoordinateTinyPath(cacheDir, existingMeta.coordinate);
|
|
1376
|
+
if (existsSync(cachedCoordTiny)) {
|
|
1377
|
+
return { path: cachedCoordTiny, coordinate: existingMeta.coordinate };
|
|
1378
|
+
}
|
|
1254
1379
|
}
|
|
1255
|
-
// yarn
|
|
1256
1380
|
const yarnCoordinates = await fetchYarnCoordinatesStandalone(version, effectiveFetch);
|
|
1381
|
+
// Network fallback: the metadata fetch failed/empty (fetchYarnCoordinatesStandalone
|
|
1382
|
+
// swallows errors -> []). Serve the last-known-good coordinate rather than failing,
|
|
1383
|
+
// so a transient Fabric Maven outage after TTL expiry does not break cache hits.
|
|
1257
1384
|
if (yarnCoordinates.length === 0) {
|
|
1385
|
+
if (existingMeta) {
|
|
1386
|
+
const cachedCoordTiny = yarnCoordinateTinyPath(cacheDir, existingMeta.coordinate);
|
|
1387
|
+
if (existsSync(cachedCoordTiny)) {
|
|
1388
|
+
return { path: cachedCoordTiny, coordinate: existingMeta.coordinate };
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1258
1391
|
throw createError({
|
|
1259
1392
|
code: ERROR_CODES.MAPPING_UNAVAILABLE,
|
|
1260
1393
|
message: `No yarn builds found for Minecraft ${version}.`,
|
|
@@ -1262,6 +1395,12 @@ export async function resolveTinyMappingFile(version, mapping, cacheDir, fetchFn
|
|
|
1262
1395
|
});
|
|
1263
1396
|
}
|
|
1264
1397
|
for (const coordinate of yarnCoordinates) {
|
|
1398
|
+
const coordTiny = yarnCoordinateTinyPath(cacheDir, coordinate);
|
|
1399
|
+
if (existsSync(coordTiny)) {
|
|
1400
|
+
// Already have this build's tiny; just refresh the TTL marker.
|
|
1401
|
+
writeYarnCoordinateMeta(metaPath, { coordinate, fetchedAt: nowFn() });
|
|
1402
|
+
return { path: coordTiny, coordinate };
|
|
1403
|
+
}
|
|
1265
1404
|
const url = `${FABRIC_MAVEN}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}-v2.jar`;
|
|
1266
1405
|
const jarDest = defaultDownloadPath(cacheDir, url);
|
|
1267
1406
|
const downloaded = await downloadToCache(url, jarDest, {
|
|
@@ -1272,9 +1411,12 @@ export async function resolveTinyMappingFile(version, mapping, cacheDir, fetchFn
|
|
|
1272
1411
|
if (!downloaded.ok || !downloaded.path) {
|
|
1273
1412
|
continue;
|
|
1274
1413
|
}
|
|
1275
|
-
const extracted = await extractTinyFromJar(downloaded.path,
|
|
1414
|
+
const extracted = await extractTinyFromJar(downloaded.path, coordTiny);
|
|
1276
1415
|
if (extracted) {
|
|
1277
|
-
|
|
1416
|
+
// Ordering matters: the tiny exists before the marker is written, so a
|
|
1417
|
+
// crash never leaves a TTL-valid marker pointing at a missing tiny.
|
|
1418
|
+
writeYarnCoordinateMeta(metaPath, { coordinate, fetchedAt: nowFn() });
|
|
1419
|
+
return { path: coordTiny, coordinate };
|
|
1278
1420
|
}
|
|
1279
1421
|
}
|
|
1280
1422
|
throw createError({
|
package/dist/mcp-helpers.d.ts
CHANGED
package/dist/mcp-helpers.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ERROR_CODES } from "./errors.js";
|
|
2
|
+
import { retryClassForErrorCode, issueOriginForErrorCode, extractAllowlistedContext } from "./error-mapping.js";
|
|
3
|
+
import { toHints, extractValidatedSuggestionAndExamples, extractFieldErrorsFromDetails } from "./tool-guidance.js";
|
|
2
4
|
export function objectResult(data, options = {}) {
|
|
3
5
|
return {
|
|
4
6
|
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
@@ -42,8 +44,18 @@ function statusForResourceErrorCode(code) {
|
|
|
42
44
|
return 500;
|
|
43
45
|
}
|
|
44
46
|
export function errorResource(uri, error) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
+
const isStr = typeof error === "string";
|
|
48
|
+
const detail = isStr ? error : error.message;
|
|
49
|
+
const code = isStr ? ERROR_CODES.INVALID_INPUT : error.code ?? ERROR_CODES.INTERNAL;
|
|
50
|
+
// Resource reads carry the same AppError as the equivalent tool call, so they
|
|
51
|
+
// get the same recovery metadata. Classifiers are always present; the rest is
|
|
52
|
+
// extracted from the AppError details (reusing the tool-error helpers as-is so
|
|
53
|
+
// the suggestedCall is validated through the single buildSuggestedCall gate).
|
|
54
|
+
const details = isStr ? undefined : error.details;
|
|
55
|
+
const hints = toHints(details);
|
|
56
|
+
const { suggestedCall, exampleCalls } = extractValidatedSuggestionAndExamples(details);
|
|
57
|
+
const fieldErrors = extractFieldErrorsFromDetails(details);
|
|
58
|
+
const context = extractAllowlistedContext(details);
|
|
47
59
|
return {
|
|
48
60
|
contents: [
|
|
49
61
|
{
|
|
@@ -51,12 +63,22 @@ export function errorResource(uri, error) {
|
|
|
51
63
|
mimeType: "application/json",
|
|
52
64
|
text: JSON.stringify({
|
|
53
65
|
error: {
|
|
66
|
+
// Keep the generic resource type/title (asserted by tests and
|
|
67
|
+
// intentionally distinct from the per-code tool ProblemDetails) so
|
|
68
|
+
// the two access paths stay distinguishable.
|
|
54
69
|
type: "https://minecraft-modding-mcp.dev/problems/resource",
|
|
55
70
|
title: "Resource read failed",
|
|
56
71
|
detail,
|
|
57
72
|
status: statusForResourceErrorCode(code),
|
|
58
73
|
code,
|
|
59
|
-
instance: uri
|
|
74
|
+
instance: uri,
|
|
75
|
+
retryClass: retryClassForErrorCode(code),
|
|
76
|
+
issueOrigin: issueOriginForErrorCode(code),
|
|
77
|
+
...(fieldErrors ? { fieldErrors } : {}),
|
|
78
|
+
...(hints ? { hints } : {}),
|
|
79
|
+
...(suggestedCall ? { suggestedCall } : {}),
|
|
80
|
+
...(exampleCalls ? { exampleCalls } : {}),
|
|
81
|
+
...(context ? { context } : {})
|
|
60
82
|
},
|
|
61
83
|
meta: { uri }
|
|
62
84
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
2
2
|
import { loadConfig } from "./config.js";
|
|
3
3
|
import { artifactSignatureFromFile, normalizeJarPath } from "./path-resolver.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createJarEntryReader } from "./source-jar-reader.js";
|
|
5
5
|
const CLASSFILE_MAGIC = 0xcafebabe;
|
|
6
6
|
const MAX_INHERITANCE_DEPTH = 64;
|
|
7
7
|
const ACC_PUBLIC = 0x0001;
|
|
@@ -452,18 +452,28 @@ export class MinecraftExplorerService {
|
|
|
452
452
|
};
|
|
453
453
|
}
|
|
454
454
|
const classEntryPath = `${toInternalName(fqn)}.class`;
|
|
455
|
-
|
|
455
|
+
// Open the jar ONCE for this call; every hierarchy class is served from the
|
|
456
|
+
// same handle (see reader.close() after hierarchy construction below).
|
|
457
|
+
const reader = await createJarEntryReader(jarPath);
|
|
458
|
+
let parsed;
|
|
456
459
|
try {
|
|
457
|
-
classBuffer
|
|
460
|
+
let classBuffer;
|
|
461
|
+
try {
|
|
462
|
+
classBuffer = await reader.getEntryBuffer(classEntryPath);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
throw createError({
|
|
466
|
+
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
467
|
+
message: `Class "${fqn}" was not found in "${jarPath}".`,
|
|
468
|
+
details: { fqn, jarPath, classEntryPath }
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
parsed = parseClassFile(classBuffer);
|
|
458
472
|
}
|
|
459
|
-
catch {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
message: `Class "${fqn}" was not found in "${jarPath}".`,
|
|
463
|
-
details: { fqn, jarPath, classEntryPath }
|
|
464
|
-
});
|
|
473
|
+
catch (error) {
|
|
474
|
+
reader.close();
|
|
475
|
+
throw error;
|
|
465
476
|
}
|
|
466
|
-
const parsed = parseClassFile(classBuffer);
|
|
467
477
|
const parsedClassCache = new Map([[parsed.internalName, parsed]]);
|
|
468
478
|
const warnings = [];
|
|
469
479
|
const warnMissingInheritedClass = (internalName, relation) => {
|
|
@@ -476,7 +486,7 @@ export class MinecraftExplorerService {
|
|
|
476
486
|
}
|
|
477
487
|
const classPath = `${internalName}.class`;
|
|
478
488
|
try {
|
|
479
|
-
const classBytes = await
|
|
489
|
+
const classBytes = await reader.getEntryBuffer(classPath);
|
|
480
490
|
const parsedClass = parseClassFile(classBytes);
|
|
481
491
|
parsedClassCache.set(parsedClass.internalName, parsedClass);
|
|
482
492
|
return parsedClass;
|
|
@@ -540,6 +550,9 @@ export class MinecraftExplorerService {
|
|
|
540
550
|
}
|
|
541
551
|
hierarchyClasses.push(...interfaceClasses);
|
|
542
552
|
}
|
|
553
|
+
// All jar reads are done; release the file descriptor before member projection
|
|
554
|
+
// (which performs no IO but may throw on malformed descriptors).
|
|
555
|
+
reader.close();
|
|
543
556
|
const toSignatureMember = (ownerFqn, ownerSimpleClassName, member, category) => {
|
|
544
557
|
if (category === "field") {
|
|
545
558
|
const parsedField = parseFieldType(member.descriptor, 0, { allowVoid: false });
|
|
@@ -183,6 +183,17 @@ export function validateParsedAccessTransformer(parsed, membersByClass, warnings
|
|
|
183
183
|
invalidCount++;
|
|
184
184
|
continue;
|
|
185
185
|
}
|
|
186
|
+
if (entry.targetKind === "wildcard-all" || entry.targetKind === "wildcard-method") {
|
|
187
|
+
// A wildcard transforms all members (or all methods); it is valid as long as
|
|
188
|
+
// the owner class resolved, which it has by this point.
|
|
189
|
+
validatedEntries.push({
|
|
190
|
+
...entry,
|
|
191
|
+
valid: true,
|
|
192
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: true } : {})
|
|
193
|
+
});
|
|
194
|
+
validCount++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
186
197
|
if (entry.targetKind === "field") {
|
|
187
198
|
const fieldNames = allFieldNames(members);
|
|
188
199
|
const matchedField = members.fields.find((member) => member.name === entry.name);
|
|
@@ -215,8 +226,29 @@ export function validateParsedAccessTransformer(parsed, membersByClass, warnings
|
|
|
215
226
|
continue;
|
|
216
227
|
}
|
|
217
228
|
const methodNames = allMethodNames(members);
|
|
218
|
-
const
|
|
229
|
+
const sameNameMethods = [...members.methods, ...members.constructors].filter((member) => member.name === entry.name);
|
|
230
|
+
// Match by descriptor when one is supplied (AT method lines always carry one);
|
|
231
|
+
// fall back to a unique name match when it is absent.
|
|
232
|
+
const matchedMethod = entry.descriptor
|
|
233
|
+
? sameNameMethods.find((member) => member.jvmDescriptor === entry.descriptor)
|
|
234
|
+
: sameNameMethods.length === 1
|
|
235
|
+
? sameNameMethods[0]
|
|
236
|
+
: undefined;
|
|
219
237
|
if (!matchedMethod) {
|
|
238
|
+
// Distinguish a true missing method from a descriptor mismatch so the user
|
|
239
|
+
// can see which overloads exist instead of a flat "not found".
|
|
240
|
+
if (entry.descriptor && sameNameMethods.length > 0) {
|
|
241
|
+
const available = [...new Set(sameNameMethods.map((member) => member.jvmDescriptor))];
|
|
242
|
+
validatedEntries.push({
|
|
243
|
+
...entry,
|
|
244
|
+
valid: false,
|
|
245
|
+
issue: `Method "${entry.name}" exists in "${ownerFqn}" but no overload matches descriptor ${entry.descriptor} (available: ${available.join(", ")}).`,
|
|
246
|
+
suggestions: available,
|
|
247
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
|
|
248
|
+
});
|
|
249
|
+
invalidCount++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
220
252
|
const suggestions = entry.name ? suggestSimilar(entry.name, methodNames) : [];
|
|
221
253
|
validatedEntries.push({
|
|
222
254
|
...entry,
|