@adhisang/minecraft-modding-mcp 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +25 -18
- package/dist/cache-registry.d.ts +1 -1
- package/dist/cache-registry.js +10 -2
- package/dist/config.d.ts +10 -1
- package/dist/config.js +52 -1
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
- package/dist/entry-tools/analyze-symbol-service.js +13 -2
- package/dist/entry-tools/inspect-minecraft-service.d.ts +20 -20
- package/dist/entry-tools/inspect-minecraft-service.js +8 -2
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/validate-project-service.js +84 -4
- package/dist/index.js +99 -33
- package/dist/lru-list.d.ts +31 -0
- package/dist/lru-list.js +102 -0
- package/dist/mapping-pipeline-service.d.ts +10 -1
- package/dist/mapping-pipeline-service.js +13 -1
- package/dist/mapping-service.d.ts +12 -0
- package/dist/mapping-service.js +252 -10
- package/dist/mixin-validator.js +22 -7
- package/dist/observability.d.ts +18 -1
- package/dist/observability.js +44 -1
- package/dist/response-utils.d.ts +44 -10
- package/dist/response-utils.js +131 -17
- package/dist/source-resolver.d.ts +9 -1
- package/dist/source-resolver.js +14 -6
- package/dist/source-service.d.ts +97 -1
- package/dist/source-service.js +922 -113
- package/dist/storage/artifacts-repo.d.ts +4 -1
- package/dist/storage/artifacts-repo.js +33 -5
- package/dist/storage/files-repo.d.ts +0 -2
- package/dist/storage/files-repo.js +0 -11
- package/dist/storage/migrations.d.ts +1 -1
- package/dist/storage/migrations.js +10 -2
- package/dist/storage/schema.d.ts +2 -0
- package/dist/storage/schema.js +25 -0
- package/dist/types.d.ts +3 -0
- package/package.json +3 -1
|
@@ -4,7 +4,8 @@ import { createError, ERROR_CODES } from "./errors.js";
|
|
|
4
4
|
* Current implementation enforces explicit guarantees:
|
|
5
5
|
* - obfuscated: always pass-through
|
|
6
6
|
* - mojang: requires source-backed artifacts on legacy obfuscated versions,
|
|
7
|
-
* but unobfuscated runtime jars can pass through directly
|
|
7
|
+
* but unobfuscated runtime jars can pass through directly,
|
|
8
|
+
* or binary-only artifacts may be remapped + decompiled when allowBinaryRemap=true
|
|
8
9
|
*/
|
|
9
10
|
export function applyMappingPipeline(input) {
|
|
10
11
|
const transformChain = [];
|
|
@@ -51,6 +52,17 @@ export function applyMappingPipeline(input) {
|
|
|
51
52
|
}
|
|
52
53
|
const hasSource = Boolean(input.resolved.sourceJarPath);
|
|
53
54
|
if (!hasSource) {
|
|
55
|
+
if (input.requestedMapping === "mojang" &&
|
|
56
|
+
input.allowBinaryRemap === true &&
|
|
57
|
+
Boolean(input.resolved.binaryJarPath)) {
|
|
58
|
+
transformChain.push("binary-remap:obf->mojang", "decompile:vineflower");
|
|
59
|
+
qualityFlags.push("binary-remapped", "decompiled");
|
|
60
|
+
return {
|
|
61
|
+
mappingApplied: "mojang",
|
|
62
|
+
qualityFlags,
|
|
63
|
+
transformChain
|
|
64
|
+
};
|
|
65
|
+
}
|
|
54
66
|
throw createError({
|
|
55
67
|
code: ERROR_CODES.MAPPING_NOT_APPLIED,
|
|
56
68
|
message: `Requested ${input.requestedMapping} mapping cannot be guaranteed for this artifact because only decompile path is available.`,
|
|
@@ -148,6 +148,16 @@ export declare class MappingService {
|
|
|
148
148
|
private readonly fetchFn;
|
|
149
149
|
private readonly graphCache;
|
|
150
150
|
private readonly buildLocks;
|
|
151
|
+
private readonly resolutionCache;
|
|
152
|
+
private static readonly RESOLUTION_CACHE_MAX;
|
|
153
|
+
private static readonly RESOLUTION_CACHE_TTL_MS;
|
|
154
|
+
private resolutionCacheHits;
|
|
155
|
+
private resolutionCacheMisses;
|
|
156
|
+
get resolutionCacheStats(): {
|
|
157
|
+
hits: number;
|
|
158
|
+
misses: number;
|
|
159
|
+
size: number;
|
|
160
|
+
};
|
|
151
161
|
constructor(config: Config, versionService?: VersionMappingsResolver, fetchFn?: typeof fetch);
|
|
152
162
|
findMapping(input: FindMappingInput): Promise<FindMappingOutput>;
|
|
153
163
|
ensureMappingAvailable(input: EnsureMappingAvailableInput): Promise<EnsureMappingAvailableOutput>;
|
|
@@ -182,6 +192,8 @@ export declare class MappingService {
|
|
|
182
192
|
private fetchYarnCoordinates;
|
|
183
193
|
private trimGraphCache;
|
|
184
194
|
releaseGraphCacheEntry(version: string, sourcePriority?: MappingSourcePriority): void;
|
|
195
|
+
private buildResolutionCacheKey;
|
|
196
|
+
private trimResolutionCache;
|
|
185
197
|
}
|
|
186
198
|
/**
|
|
187
199
|
* Resolve and cache a Tiny v2 mapping file for the given Minecraft version.
|
package/dist/mapping-service.js
CHANGED
|
@@ -708,15 +708,93 @@ function normalizeMemberName(name) {
|
|
|
708
708
|
}
|
|
709
709
|
return normalized;
|
|
710
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* Validate a JVM method descriptor such as `(I)V`, `()Lfoo/Bar;`, `(Lfoo/Bar;[I)V`.
|
|
713
|
+
* Rejects empty strings, missing/mis-positioned parens, empty return type, and invalid base
|
|
714
|
+
* type tokens so "(" or "()" style half-descriptors surface as ERR_INVALID_INPUT instead of
|
|
715
|
+
* being silently accepted.
|
|
716
|
+
*/
|
|
711
717
|
function normalizeMethodDescriptor(descriptor) {
|
|
712
718
|
const normalized = descriptor?.trim() ?? "";
|
|
713
|
-
if (!normalized
|
|
719
|
+
if (!normalized) {
|
|
720
|
+
throw invalidInputError("descriptor must be a valid JVM descriptor when kind=method.", {
|
|
721
|
+
descriptor
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
if (!isValidMethodDescriptor(normalized)) {
|
|
714
725
|
throw invalidInputError("descriptor must be a valid JVM descriptor when kind=method.", {
|
|
715
726
|
descriptor
|
|
716
727
|
});
|
|
717
728
|
}
|
|
718
729
|
return normalized;
|
|
719
730
|
}
|
|
731
|
+
function isValidMethodDescriptor(descriptor) {
|
|
732
|
+
if (!descriptor.startsWith("("))
|
|
733
|
+
return false;
|
|
734
|
+
const closingIndex = descriptor.indexOf(")");
|
|
735
|
+
if (closingIndex < 0)
|
|
736
|
+
return false;
|
|
737
|
+
const argsSection = descriptor.slice(1, closingIndex);
|
|
738
|
+
const returnSection = descriptor.slice(closingIndex + 1);
|
|
739
|
+
if (returnSection.length === 0)
|
|
740
|
+
return false;
|
|
741
|
+
let cursor = 0;
|
|
742
|
+
while (cursor < argsSection.length) {
|
|
743
|
+
const next = consumeFieldType(argsSection, cursor, /*allowVoid*/ false);
|
|
744
|
+
if (next < 0)
|
|
745
|
+
return false;
|
|
746
|
+
cursor = next;
|
|
747
|
+
}
|
|
748
|
+
const returnEnd = consumeFieldType(returnSection, 0, /*allowVoid*/ true);
|
|
749
|
+
return returnEnd === returnSection.length;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* JVM specification §4.3.2: "An array type descriptor is valid only if it represents a type
|
|
753
|
+
* with 255 or fewer dimensions." Matches the `multianewarray` / field-signature limit.
|
|
754
|
+
*/
|
|
755
|
+
const JVM_MAX_ARRAY_DIMENSIONS = 255;
|
|
756
|
+
function consumeFieldType(descriptor, position, allowVoid) {
|
|
757
|
+
// Arrays are handled iteratively so pathological inputs such as `(` + "[".repeat(20000) + `I)V`
|
|
758
|
+
// cannot blow the call stack. After consuming every leading `[`, only the element type token
|
|
759
|
+
// is dispatched through the switch below. Dimensions above the JVM limit are rejected rather
|
|
760
|
+
// than merely accepted as "syntactically valid but semantically absurd" — clients must not be
|
|
761
|
+
// able to push a 20000-dimension descriptor through cache-key construction.
|
|
762
|
+
let cursor = position;
|
|
763
|
+
let arrayDimensions = 0;
|
|
764
|
+
while (cursor < descriptor.length && descriptor[cursor] === "[") {
|
|
765
|
+
cursor += 1;
|
|
766
|
+
arrayDimensions += 1;
|
|
767
|
+
if (arrayDimensions > JVM_MAX_ARRAY_DIMENSIONS)
|
|
768
|
+
return -1;
|
|
769
|
+
}
|
|
770
|
+
if (cursor >= descriptor.length)
|
|
771
|
+
return -1;
|
|
772
|
+
// Void is only valid at the outermost position — inside an array element it is illegal.
|
|
773
|
+
const elementAllowsVoid = cursor === position && allowVoid;
|
|
774
|
+
const token = descriptor[cursor];
|
|
775
|
+
switch (token) {
|
|
776
|
+
case "B":
|
|
777
|
+
case "C":
|
|
778
|
+
case "D":
|
|
779
|
+
case "F":
|
|
780
|
+
case "I":
|
|
781
|
+
case "J":
|
|
782
|
+
case "S":
|
|
783
|
+
case "Z":
|
|
784
|
+
return cursor + 1;
|
|
785
|
+
case "V":
|
|
786
|
+
return elementAllowsVoid ? cursor + 1 : -1;
|
|
787
|
+
case "L": {
|
|
788
|
+
const end = descriptor.indexOf(";", cursor);
|
|
789
|
+
// Reject empty class names like L; and unterminated references.
|
|
790
|
+
if (end < 0 || end === cursor + 1)
|
|
791
|
+
return -1;
|
|
792
|
+
return end + 1;
|
|
793
|
+
}
|
|
794
|
+
default:
|
|
795
|
+
return -1;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
720
798
|
function normalizeQuerySymbol(input, signatureMode, options) {
|
|
721
799
|
if (input.kind !== "class" && input.kind !== "field" && input.kind !== "method") {
|
|
722
800
|
throw invalidInputError('kind must be one of "class", "field", or "method".', {
|
|
@@ -773,9 +851,19 @@ function normalizeQuerySymbol(input, signatureMode, options) {
|
|
|
773
851
|
querySymbol: toSymbolReference(record)
|
|
774
852
|
};
|
|
775
853
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
854
|
+
let descriptor;
|
|
855
|
+
if (signatureMode === "name-only") {
|
|
856
|
+
// name-only matches by owner+name only; a supplied descriptor is validated (so malformed
|
|
857
|
+
// input still surfaces as ERR_INVALID_INPUT) but discarded afterwards so downstream
|
|
858
|
+
// projection / filtering treats the query as "no descriptor".
|
|
859
|
+
if (input.descriptor?.trim()) {
|
|
860
|
+
normalizeMethodDescriptor(input.descriptor);
|
|
861
|
+
}
|
|
862
|
+
descriptor = "";
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
descriptor = normalizeMethodDescriptor(input.descriptor);
|
|
866
|
+
}
|
|
779
867
|
const record = createMethodSymbolRecord(owner, normalizeMemberName(normalizedName), descriptor);
|
|
780
868
|
return {
|
|
781
869
|
record,
|
|
@@ -916,6 +1004,18 @@ export class MappingService {
|
|
|
916
1004
|
fetchFn;
|
|
917
1005
|
graphCache = new Map();
|
|
918
1006
|
buildLocks = new Map();
|
|
1007
|
+
resolutionCache = new Map();
|
|
1008
|
+
static RESOLUTION_CACHE_MAX = 512;
|
|
1009
|
+
static RESOLUTION_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
1010
|
+
resolutionCacheHits = 0;
|
|
1011
|
+
resolutionCacheMisses = 0;
|
|
1012
|
+
get resolutionCacheStats() {
|
|
1013
|
+
return {
|
|
1014
|
+
hits: this.resolutionCacheHits,
|
|
1015
|
+
misses: this.resolutionCacheMisses,
|
|
1016
|
+
size: this.resolutionCache.size
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
919
1019
|
constructor(config, versionService = new VersionService(config), fetchFn = globalThis.fetch) {
|
|
920
1020
|
this.config = config;
|
|
921
1021
|
this.versionService = versionService;
|
|
@@ -932,9 +1032,25 @@ export class MappingService {
|
|
|
932
1032
|
}
|
|
933
1033
|
});
|
|
934
1034
|
}
|
|
935
|
-
|
|
1035
|
+
// Normalize the effective signatureMode exactly once so every downstream path — query
|
|
1036
|
+
// symbol normalization, the strict-overload filter, the cache key, and warning text —
|
|
1037
|
+
// sees the same value. The public tool schema defaults to "name-only", so an omitted
|
|
1038
|
+
// signatureMode reaching the service (e.g. internal callers, MCP resource handlers, the
|
|
1039
|
+
// resolution cache) must default to "name-only" too, otherwise the service contradicts
|
|
1040
|
+
// the advertised default and silently reverts to the old descriptor-required path.
|
|
1041
|
+
// Callers that genuinely need strict descriptor matching pass `signatureMode: "exact"`
|
|
1042
|
+
// explicitly.
|
|
1043
|
+
const effectiveSignatureMode = input.signatureMode ?? "name-only";
|
|
1044
|
+
const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, effectiveSignatureMode, {
|
|
936
1045
|
allowShortClassName: input.kind === "class" && input.sourceMapping === "obfuscated"
|
|
937
1046
|
});
|
|
1047
|
+
const cacheKey = this.buildResolutionCacheKey(version, input, querySymbol, effectiveSignatureMode);
|
|
1048
|
+
const cached = this.resolutionCache.get(cacheKey);
|
|
1049
|
+
if (cached && Date.now() - cached.cachedAt < MappingService.RESOLUTION_CACHE_TTL_MS) {
|
|
1050
|
+
this.resolutionCacheHits += 1;
|
|
1051
|
+
return cached.result;
|
|
1052
|
+
}
|
|
1053
|
+
this.resolutionCacheMisses += 1;
|
|
938
1054
|
const sourceMapping = input.sourceMapping;
|
|
939
1055
|
const targetMapping = input.targetMapping;
|
|
940
1056
|
if (!SUPPORTED_MAPPINGS.has(sourceMapping) || !SUPPORTED_MAPPINGS.has(targetMapping)) {
|
|
@@ -992,13 +1108,51 @@ export class MappingService {
|
|
|
992
1108
|
const descriptorProjection = queryRecord.kind === "method" && queryRecord.descriptor
|
|
993
1109
|
? this.projectMethodDescriptorToTarget(graph, path, queryRecord.descriptor)
|
|
994
1110
|
: undefined;
|
|
995
|
-
|
|
996
|
-
|
|
1111
|
+
// Partial projections are still useful for comparison: projectMethodDescriptorToTarget
|
|
1112
|
+
// leaves unmapped `L...;` references unchanged, so JDK / external classes pass through
|
|
1113
|
+
// while Minecraft class references get rewritten to the target namespace. Using the
|
|
1114
|
+
// projected descriptor even when `complete === false` produces descriptors shaped like
|
|
1115
|
+
// the stored records (`(Lnet/minecraft/class_1799;Ljava/lang/String;)V`) and avoids
|
|
1116
|
+
// false negatives for the very common mixed MC + JDK descriptor shape.
|
|
1117
|
+
const projectedDescriptor = descriptorProjection?.hadClassReferences ? descriptorProjection.descriptor : undefined;
|
|
1118
|
+
let rawCandidates = this
|
|
997
1119
|
.mapCandidatesAlongPath(graph, path, queryRecord)
|
|
998
1120
|
.map((candidate) => queryRecord.kind === "method" && queryRecord.descriptor
|
|
999
1121
|
? projectLookupCandidateDescriptor(candidate, queryRecord.descriptor, projectedDescriptor)
|
|
1000
1122
|
: candidate);
|
|
1001
1123
|
const warnings = [];
|
|
1124
|
+
// signatureMode="exact" on kind=method must not return descriptorless fallback candidates
|
|
1125
|
+
// (lookupCandidates adds owner+name fallbacks by design for the loose path). Without this
|
|
1126
|
+
// filter a caller who supplied `foo(I)V` could be told `foo(Z)V` is the exact mapping,
|
|
1127
|
+
// which would be wrong for migration tooling. Mirror resolveMethodMappingExact's strict
|
|
1128
|
+
// behavior: keep only candidates whose descriptor equals the (projected) requested
|
|
1129
|
+
// descriptor. If nothing passes, the normal "candidates.length === 0 -> not_found" path
|
|
1130
|
+
// takes over. Partial projection is accepted here for the same reason as above — we do
|
|
1131
|
+
// not reject mixed MC + JDK descriptors as mapping_unavailable just because the JDK
|
|
1132
|
+
// class reference was not in the mapping graph.
|
|
1133
|
+
if (queryRecord.kind === "method" &&
|
|
1134
|
+
queryRecord.descriptor &&
|
|
1135
|
+
effectiveSignatureMode === "exact") {
|
|
1136
|
+
// Tiny v2 stores a single descriptor per method entry (typically in the obfuscated
|
|
1137
|
+
// namespace) and shares it across every column, while client mappings attach a mojang
|
|
1138
|
+
// descriptor on the mojang side and an obfuscated descriptor on the obfuscated side.
|
|
1139
|
+
// In multi-hop paths (e.g. mojang -> obfuscated -> intermediary -> yarn) the final
|
|
1140
|
+
// candidate's owner and name live in the target namespace but its descriptor can still
|
|
1141
|
+
// be the obfuscated form that rode along the Tiny hop. A strict filter that compared
|
|
1142
|
+
// only against the fully projected target descriptor dropped those valid candidates
|
|
1143
|
+
// and produced false `not_found` for common Mojang -> Yarn lookups. Accept any
|
|
1144
|
+
// candidate whose descriptor matches the caller's descriptor, the target-space
|
|
1145
|
+
// projection, or the obfuscated-space projection — the three forms that actually
|
|
1146
|
+
// appear in the mapping graph.
|
|
1147
|
+
const strictDescriptor = projectedDescriptor ?? queryRecord.descriptor;
|
|
1148
|
+
const acceptedDescriptors = new Set([queryRecord.descriptor, strictDescriptor]);
|
|
1149
|
+
const toObfuscatedPath = namespacePath(graph, sourceMapping, "obfuscated");
|
|
1150
|
+
if (toObfuscatedPath) {
|
|
1151
|
+
const obfuscatedProjection = this.projectMethodDescriptorToTarget(graph, toObfuscatedPath, queryRecord.descriptor);
|
|
1152
|
+
acceptedDescriptors.add(obfuscatedProjection.descriptor);
|
|
1153
|
+
}
|
|
1154
|
+
rawCandidates = rawCandidates.filter((candidate) => candidate.descriptor !== undefined && acceptedDescriptors.has(candidate.descriptor));
|
|
1155
|
+
}
|
|
1002
1156
|
const disambiguatedCandidates = applyDisambiguationHints(rawCandidates, input.disambiguation);
|
|
1003
1157
|
if (rawCandidates.length > disambiguatedCandidates.length) {
|
|
1004
1158
|
warnings.push(`Disambiguation hints narrowed candidates from ${rawCandidates.length} to ${disambiguatedCandidates.length}.`);
|
|
@@ -1016,12 +1170,21 @@ export class MappingService {
|
|
|
1016
1170
|
}
|
|
1017
1171
|
else if (candidates.length > 1) {
|
|
1018
1172
|
warnings.push(`Ambiguous mapping: ${candidates.length} candidates matched. Provide a stricter symbol input or disambiguation hints.`);
|
|
1173
|
+
if (queryRecord.kind === "method") {
|
|
1174
|
+
// find-mapping defaults to signatureMode="name-only", which discards any supplied
|
|
1175
|
+
// descriptor. Telling the caller to "add descriptor" would be ineffective unless they
|
|
1176
|
+
// also switch mode, so we point to the exact alternatives instead.
|
|
1177
|
+
warnings.push("Retry with signatureMode=\"exact\" plus a JVM descriptor, or pass disambiguation.descriptorHint, or raise maxCandidates up to 200 to inspect the full candidate list.");
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
warnings.push("Raise maxCandidates up to 200 to inspect the full candidate list, or use disambiguation.ownerHint to narrow the search.");
|
|
1181
|
+
}
|
|
1019
1182
|
}
|
|
1020
1183
|
const status = candidates.length === 0 ? "not_found" : candidates.length === 1 ? "resolved" : "ambiguous";
|
|
1021
1184
|
const ambiguityReasons = candidates.length > 1
|
|
1022
1185
|
? inferAmbiguityReasons(candidates, pathUsesSource(graph.pairs, path, "mojang-client-mappings"))
|
|
1023
1186
|
: undefined;
|
|
1024
|
-
|
|
1187
|
+
const output = {
|
|
1025
1188
|
querySymbol,
|
|
1026
1189
|
mappingContext,
|
|
1027
1190
|
resolved: status === "resolved",
|
|
@@ -1034,6 +1197,9 @@ export class MappingService {
|
|
|
1034
1197
|
provenance: this.provenanceForPath(graph, path),
|
|
1035
1198
|
ambiguityReasons
|
|
1036
1199
|
};
|
|
1200
|
+
this.resolutionCache.set(cacheKey, { result: output, cachedAt: Date.now() });
|
|
1201
|
+
this.trimResolutionCache();
|
|
1202
|
+
return output;
|
|
1037
1203
|
}
|
|
1038
1204
|
async ensureMappingAvailable(input) {
|
|
1039
1205
|
const version = input.version.trim();
|
|
@@ -1193,6 +1359,9 @@ export class MappingService {
|
|
|
1193
1359
|
}
|
|
1194
1360
|
if (strictCandidates.length > 1) {
|
|
1195
1361
|
warnings.push("Exact method mapping is ambiguous for owner+method+descriptor.");
|
|
1362
|
+
if (limitedCandidates.candidatesTruncated) {
|
|
1363
|
+
warnings.push("Raise maxCandidates up to 200 to inspect the full candidate list, or narrow the lookup via find-mapping disambiguation hints.");
|
|
1364
|
+
}
|
|
1196
1365
|
return {
|
|
1197
1366
|
querySymbol,
|
|
1198
1367
|
mappingContext,
|
|
@@ -1495,6 +1664,9 @@ export class MappingService {
|
|
|
1495
1664
|
const buildOutput = (querySymbol, matched, status) => {
|
|
1496
1665
|
const candidates = matched.map((record) => toResolutionCandidate(toLookupCandidate(record)));
|
|
1497
1666
|
const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
|
|
1667
|
+
if (status === "ambiguous" && limitedCandidates.candidatesTruncated) {
|
|
1668
|
+
warnings.push("Raise maxCandidates up to 200 to inspect the full candidate list.");
|
|
1669
|
+
}
|
|
1498
1670
|
return {
|
|
1499
1671
|
querySymbol,
|
|
1500
1672
|
mappingContext,
|
|
@@ -1537,11 +1709,40 @@ export class MappingService {
|
|
|
1537
1709
|
if (input.signatureMode === "name-only") {
|
|
1538
1710
|
const status = methodCandidates.length === 1 ? "resolved" : methodCandidates.length > 1 ? "ambiguous" : "not_found";
|
|
1539
1711
|
if (status === "ambiguous") {
|
|
1540
|
-
|
|
1712
|
+
// name-only discards any supplied descriptor, so telling the caller to "provide
|
|
1713
|
+
// descriptor" would not disambiguate — they need to switch to signatureMode="exact".
|
|
1714
|
+
warnings.push(`Multiple method overloads matched name "${queryRecord.name}" in owner "${queryRecord.owner}". Retry with signatureMode="exact" plus a JVM descriptor to pick one overload.`);
|
|
1541
1715
|
}
|
|
1542
1716
|
return buildOutput(querySymbol, methodCandidates, status);
|
|
1543
1717
|
}
|
|
1544
|
-
|
|
1718
|
+
// Tiny parsing stores a single descriptor per method entry (typically in the obfuscated
|
|
1719
|
+
// namespace) and copies it into every namespace at load time. That means a descriptor
|
|
1720
|
+
// supplied in `sourceMapping` coordinates will not string-compare equal to the record
|
|
1721
|
+
// for any method whose descriptor references remapped Minecraft classes. Project the
|
|
1722
|
+
// caller's descriptor to obfuscated coordinates first so class references line up with
|
|
1723
|
+
// the stored record descriptors. The projection is accepted even when
|
|
1724
|
+
// `projection.complete` is false: `projectMethodDescriptorToTarget` leaves every
|
|
1725
|
+
// unresolvable `L...;` reference unchanged (JDK/external classes like
|
|
1726
|
+
// `Ljava/lang/String;` are never in the mapping graph and pass through by design), so a
|
|
1727
|
+
// partial projection still aligns the Minecraft class refs with the stored descriptor
|
|
1728
|
+
// form while leaving external class refs identical to the user input. Falling back to
|
|
1729
|
+
// verbatim comparison on `complete === false` would send mixed descriptors like
|
|
1730
|
+
// `(Lnet/minecraft/world/item/ItemStack;Ljava/lang/String;)V` down the raw-compare path
|
|
1731
|
+
// and produce false negatives in the most common lookup shape. When no class references
|
|
1732
|
+
// exist at all (primitives-only descriptors such as `(I)V`) the projector marks
|
|
1733
|
+
// `hadClassReferences === false` and we simply reuse the original descriptor.
|
|
1734
|
+
const queryDescriptor = queryRecord.descriptor;
|
|
1735
|
+
let effectiveDescriptor = queryDescriptor;
|
|
1736
|
+
if (sourceMapping !== "obfuscated") {
|
|
1737
|
+
const projectionPath = namespacePath(graph, sourceMapping, "obfuscated");
|
|
1738
|
+
if (projectionPath) {
|
|
1739
|
+
const projection = this.projectMethodDescriptorToTarget(graph, projectionPath, queryDescriptor);
|
|
1740
|
+
if (projection.hadClassReferences) {
|
|
1741
|
+
effectiveDescriptor = projection.descriptor;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
const descriptorMatched = methodCandidates.filter((record) => record.descriptor === effectiveDescriptor || record.descriptor === queryDescriptor);
|
|
1545
1746
|
if (descriptorMatched.length === 1) {
|
|
1546
1747
|
return buildOutput(querySymbol, descriptorMatched, "resolved");
|
|
1547
1748
|
}
|
|
@@ -2094,6 +2295,11 @@ export class MappingService {
|
|
|
2094
2295
|
this.graphCache.delete(oldestKey);
|
|
2095
2296
|
}
|
|
2096
2297
|
}
|
|
2298
|
+
// Note: in-flight buildLocks may re-populate graphCache after release.
|
|
2299
|
+
// Resolution cache entries created by concurrent findMapping() calls may also
|
|
2300
|
+
// survive this invalidation. Both are bounded by TTL (5 min) and will expire
|
|
2301
|
+
// naturally. A full epoch-based invalidation would add complexity for a rare
|
|
2302
|
+
// user-initiated operation (manage-cache).
|
|
2097
2303
|
releaseGraphCacheEntry(version, sourcePriority) {
|
|
2098
2304
|
const normalizedVersion = version.trim();
|
|
2099
2305
|
if (!normalizedVersion) {
|
|
@@ -2106,6 +2312,42 @@ export class MappingService {
|
|
|
2106
2312
|
this.graphCache.delete(key);
|
|
2107
2313
|
}
|
|
2108
2314
|
}
|
|
2315
|
+
const resolutionPrefix = `${normalizedVersion}\0`;
|
|
2316
|
+
for (const key of this.resolutionCache.keys()) {
|
|
2317
|
+
if (key.startsWith(resolutionPrefix)) {
|
|
2318
|
+
this.resolutionCache.delete(key);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
buildResolutionCacheKey(version, input, querySymbol, effectiveSignatureMode) {
|
|
2323
|
+
return [
|
|
2324
|
+
version,
|
|
2325
|
+
input.kind,
|
|
2326
|
+
querySymbol.symbol,
|
|
2327
|
+
querySymbol.descriptor ?? "",
|
|
2328
|
+
input.sourceMapping,
|
|
2329
|
+
input.targetMapping,
|
|
2330
|
+
input.sourcePriority ?? "",
|
|
2331
|
+
effectiveLoomSearchProjectPath(input.projectPath) ?? "",
|
|
2332
|
+
effectiveSignatureMode,
|
|
2333
|
+
String(input.maxCandidates ?? ""),
|
|
2334
|
+
JSON.stringify(input.disambiguation ?? "")
|
|
2335
|
+
].join("\0");
|
|
2336
|
+
}
|
|
2337
|
+
trimResolutionCache() {
|
|
2338
|
+
if (this.resolutionCache.size <= MappingService.RESOLUTION_CACHE_MAX)
|
|
2339
|
+
return;
|
|
2340
|
+
const now = Date.now();
|
|
2341
|
+
for (const [key, entry] of this.resolutionCache) {
|
|
2342
|
+
if (now - entry.cachedAt > MappingService.RESOLUTION_CACHE_TTL_MS) {
|
|
2343
|
+
this.resolutionCache.delete(key);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
while (this.resolutionCache.size > MappingService.RESOLUTION_CACHE_MAX) {
|
|
2347
|
+
const firstKey = this.resolutionCache.keys().next().value;
|
|
2348
|
+
if (firstKey !== undefined)
|
|
2349
|
+
this.resolutionCache.delete(firstKey);
|
|
2350
|
+
}
|
|
2109
2351
|
}
|
|
2110
2352
|
}
|
|
2111
2353
|
// ---------------------------------------------------------------------------
|
package/dist/mixin-validator.js
CHANGED
|
@@ -225,11 +225,23 @@ function computeValidationStatus(summary) {
|
|
|
225
225
|
}
|
|
226
226
|
return "full";
|
|
227
227
|
}
|
|
228
|
-
function buildQuickSummary(status, summary) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
228
|
+
function buildQuickSummary(status, summary, context) {
|
|
229
|
+
const base = status === "full"
|
|
230
|
+
? `${summary.membersValidated} member(s) validated successfully.`
|
|
231
|
+
: `${summary.definiteErrors} error(s), ${summary.uncertainErrors} uncertain, ${summary.warnings} warning(s). ${summary.membersValidated} validated, ${summary.membersSkipped} member(s) skipped, ${summary.membersMissing} member(s) missing.`;
|
|
232
|
+
const notes = [];
|
|
233
|
+
const scopeFallback = context?.provenance?.scopeFallback;
|
|
234
|
+
if (scopeFallback) {
|
|
235
|
+
notes.push(`Scope fell back from "${scopeFallback.requested}" to "${scopeFallback.applied}" (${scopeFallback.reason}).`);
|
|
236
|
+
}
|
|
237
|
+
const healthReport = context?.healthReport;
|
|
238
|
+
if (healthReport && !healthReport.overallHealthy) {
|
|
239
|
+
const degradations = healthReport.degradations.length > 0
|
|
240
|
+
? healthReport.degradations.join("; ")
|
|
241
|
+
: "mapping infrastructure degraded";
|
|
242
|
+
notes.push(`Mapping health degraded: ${degradations}.`);
|
|
243
|
+
}
|
|
244
|
+
return notes.length > 0 ? `${base} ${notes.join(" ")}` : base;
|
|
233
245
|
}
|
|
234
246
|
function addSkippedMembers(parsed, resolvedMembers) {
|
|
235
247
|
for (const inj of parsed.injections) {
|
|
@@ -271,7 +283,10 @@ export function refreshMixinValidationOutcome(result) {
|
|
|
271
283
|
};
|
|
272
284
|
result.validationStatus = computeValidationStatus(result.summary);
|
|
273
285
|
result.valid = result.summary.definiteErrors === 0;
|
|
274
|
-
result.quickSummary = buildQuickSummary(result.validationStatus, result.summary
|
|
286
|
+
result.quickSummary = buildQuickSummary(result.validationStatus, result.summary, {
|
|
287
|
+
provenance: result.provenance,
|
|
288
|
+
healthReport: result.toolHealth
|
|
289
|
+
});
|
|
275
290
|
return result;
|
|
276
291
|
}
|
|
277
292
|
function validateInjection(inj, targetMembers, targetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport) {
|
|
@@ -727,7 +742,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
727
742
|
parseWarnings: parseWarningCount
|
|
728
743
|
};
|
|
729
744
|
const validationStatus = computeValidationStatus(summary);
|
|
730
|
-
const quickSummary = buildQuickSummary(validationStatus, summary);
|
|
745
|
+
const quickSummary = buildQuickSummary(validationStatus, summary, { provenance, healthReport });
|
|
731
746
|
return {
|
|
732
747
|
className: parsed.className,
|
|
733
748
|
targets: targetNames,
|
package/dist/observability.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface RuntimeMetricSnapshot {
|
|
|
22
22
|
get_file_duration_ms: MetricTimingSnapshot;
|
|
23
23
|
list_files_duration_ms: MetricTimingSnapshot;
|
|
24
24
|
decompile_duration_ms: MetricTimingSnapshot;
|
|
25
|
+
binary_remap_duration_ms: MetricTimingSnapshot;
|
|
25
26
|
search_intent_symbol_duration_ms: MetricTimingSnapshot;
|
|
26
27
|
search_intent_text_duration_ms: MetricTimingSnapshot;
|
|
27
28
|
search_intent_path_duration_ms: MetricTimingSnapshot;
|
|
@@ -47,8 +48,13 @@ export interface RuntimeMetricSnapshot {
|
|
|
47
48
|
cache_artifact_bytes_lru: CacheArtifactByteAccountingRow[];
|
|
48
49
|
cache_hit_rate: number;
|
|
49
50
|
repo_failover_count: number;
|
|
51
|
+
tool_call_counts: Record<string, number>;
|
|
52
|
+
tool_call_duration_ms: Record<string, MetricTimingSnapshot>;
|
|
53
|
+
mapping_resolution_cache_hits: number;
|
|
54
|
+
mapping_resolution_cache_misses: number;
|
|
55
|
+
mapping_resolution_cache_size: number;
|
|
50
56
|
}
|
|
51
|
-
type DurationMetricName = keyof Pick<RuntimeMetricSnapshot, "resolve_duration_ms" | "search_duration_ms" | "get_file_duration_ms" | "list_files_duration_ms" | "decompile_duration_ms" | "search_intent_symbol_duration_ms" | "search_intent_text_duration_ms" | "search_intent_path_duration_ms">;
|
|
57
|
+
type DurationMetricName = keyof Pick<RuntimeMetricSnapshot, "resolve_duration_ms" | "search_duration_ms" | "get_file_duration_ms" | "list_files_duration_ms" | "decompile_duration_ms" | "binary_remap_duration_ms" | "search_intent_symbol_duration_ms" | "search_intent_text_duration_ms" | "search_intent_path_duration_ms">;
|
|
52
58
|
export declare class RuntimeMetrics {
|
|
53
59
|
private readonly timings;
|
|
54
60
|
private cacheHits;
|
|
@@ -74,6 +80,11 @@ export declare class RuntimeMetrics {
|
|
|
74
80
|
private cacheEntries;
|
|
75
81
|
private cacheTotalContentBytes;
|
|
76
82
|
private cacheArtifactBytesLruRef;
|
|
83
|
+
private toolCallCounts;
|
|
84
|
+
private toolCallTimings;
|
|
85
|
+
private mappingResolutionCacheHits;
|
|
86
|
+
private mappingResolutionCacheMisses;
|
|
87
|
+
private mappingResolutionCacheSize;
|
|
77
88
|
constructor();
|
|
78
89
|
recordDuration(name: DurationMetricName, durationMs: number): void;
|
|
79
90
|
recordArtifactCacheHit(): void;
|
|
@@ -97,6 +108,12 @@ export declare class RuntimeMetrics {
|
|
|
97
108
|
setCacheEntries(entries: number): void;
|
|
98
109
|
setCacheTotalContentBytes(totalBytes: number): void;
|
|
99
110
|
setCacheArtifactByteAccountingRef(entries: ReadonlyArray<CacheArtifactByteAccountingRefRow>): void;
|
|
111
|
+
recordToolCall(tool: string, durationMs: number): void;
|
|
112
|
+
setMappingResolutionCacheStats(stats: {
|
|
113
|
+
hits: number;
|
|
114
|
+
misses: number;
|
|
115
|
+
size: number;
|
|
116
|
+
}): void;
|
|
100
117
|
snapshot(): RuntimeMetricSnapshot;
|
|
101
118
|
private toSnapshot;
|
|
102
119
|
private resolveCacheHitRate;
|
package/dist/observability.js
CHANGED
|
@@ -32,6 +32,11 @@ export class RuntimeMetrics {
|
|
|
32
32
|
cacheEntries = 0;
|
|
33
33
|
cacheTotalContentBytes = 0;
|
|
34
34
|
cacheArtifactBytesLruRef = [];
|
|
35
|
+
toolCallCounts = new Map();
|
|
36
|
+
toolCallTimings = new Map();
|
|
37
|
+
mappingResolutionCacheHits = 0;
|
|
38
|
+
mappingResolutionCacheMisses = 0;
|
|
39
|
+
mappingResolutionCacheSize = 0;
|
|
35
40
|
constructor() {
|
|
36
41
|
const names = [
|
|
37
42
|
"resolve_duration_ms",
|
|
@@ -39,6 +44,7 @@ export class RuntimeMetrics {
|
|
|
39
44
|
"get_file_duration_ms",
|
|
40
45
|
"list_files_duration_ms",
|
|
41
46
|
"decompile_duration_ms",
|
|
47
|
+
"binary_remap_duration_ms",
|
|
42
48
|
"search_intent_symbol_duration_ms",
|
|
43
49
|
"search_intent_text_duration_ms",
|
|
44
50
|
"search_intent_path_duration_ms"
|
|
@@ -140,6 +146,27 @@ export class RuntimeMetrics {
|
|
|
140
146
|
setCacheArtifactByteAccountingRef(entries) {
|
|
141
147
|
this.cacheArtifactBytesLruRef = entries;
|
|
142
148
|
}
|
|
149
|
+
recordToolCall(tool, durationMs) {
|
|
150
|
+
this.toolCallCounts.set(tool, (this.toolCallCounts.get(tool) ?? 0) + 1);
|
|
151
|
+
let timing = this.toolCallTimings.get(tool);
|
|
152
|
+
if (!timing) {
|
|
153
|
+
timing = { count: 0, totalMs: 0, lastMs: 0, samples: [] };
|
|
154
|
+
this.toolCallTimings.set(tool, timing);
|
|
155
|
+
}
|
|
156
|
+
const normalizedDuration = Math.max(0, Math.trunc(durationMs));
|
|
157
|
+
timing.count += 1;
|
|
158
|
+
timing.totalMs += normalizedDuration;
|
|
159
|
+
timing.lastMs = normalizedDuration;
|
|
160
|
+
timing.samples.push(normalizedDuration);
|
|
161
|
+
if (timing.samples.length > MAX_TIMING_SAMPLES) {
|
|
162
|
+
timing.samples.shift();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
setMappingResolutionCacheStats(stats) {
|
|
166
|
+
this.mappingResolutionCacheHits = stats.hits;
|
|
167
|
+
this.mappingResolutionCacheMisses = stats.misses;
|
|
168
|
+
this.mappingResolutionCacheSize = stats.size;
|
|
169
|
+
}
|
|
143
170
|
snapshot() {
|
|
144
171
|
return {
|
|
145
172
|
resolve_duration_ms: this.toSnapshot("resolve_duration_ms"),
|
|
@@ -147,6 +174,7 @@ export class RuntimeMetrics {
|
|
|
147
174
|
get_file_duration_ms: this.toSnapshot("get_file_duration_ms"),
|
|
148
175
|
list_files_duration_ms: this.toSnapshot("list_files_duration_ms"),
|
|
149
176
|
decompile_duration_ms: this.toSnapshot("decompile_duration_ms"),
|
|
177
|
+
binary_remap_duration_ms: this.toSnapshot("binary_remap_duration_ms"),
|
|
150
178
|
search_intent_symbol_duration_ms: this.toSnapshot("search_intent_symbol_duration_ms"),
|
|
151
179
|
search_intent_text_duration_ms: this.toSnapshot("search_intent_text_duration_ms"),
|
|
152
180
|
search_intent_path_duration_ms: this.toSnapshot("search_intent_path_duration_ms"),
|
|
@@ -175,7 +203,22 @@ export class RuntimeMetrics {
|
|
|
175
203
|
updated_at: entry.updatedAt
|
|
176
204
|
})),
|
|
177
205
|
cache_hit_rate: this.resolveCacheHitRate(),
|
|
178
|
-
repo_failover_count: this.repoFailoverCount
|
|
206
|
+
repo_failover_count: this.repoFailoverCount,
|
|
207
|
+
tool_call_counts: Object.fromEntries(this.toolCallCounts),
|
|
208
|
+
tool_call_duration_ms: Object.fromEntries([...this.toolCallTimings].map(([tool, timing]) => [
|
|
209
|
+
tool,
|
|
210
|
+
{
|
|
211
|
+
count: timing.count,
|
|
212
|
+
totalMs: timing.totalMs,
|
|
213
|
+
avgMs: timing.count > 0 ? timing.totalMs / timing.count : 0,
|
|
214
|
+
lastMs: timing.lastMs,
|
|
215
|
+
p95Ms: percentile(timing.samples, 95),
|
|
216
|
+
p99Ms: percentile(timing.samples, 99)
|
|
217
|
+
}
|
|
218
|
+
])),
|
|
219
|
+
mapping_resolution_cache_hits: this.mappingResolutionCacheHits,
|
|
220
|
+
mapping_resolution_cache_misses: this.mappingResolutionCacheMisses,
|
|
221
|
+
mapping_resolution_cache_size: this.mappingResolutionCacheSize
|
|
179
222
|
};
|
|
180
223
|
}
|
|
181
224
|
toSnapshot(name) {
|