@adhisang/minecraft-modding-mcp 1.1.1 → 1.2.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 +49 -0
- package/README.md +83 -16
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/decompiler/vineflower.d.ts +1 -0
- package/dist/decompiler/vineflower.js +78 -29
- package/dist/index.js +167 -23
- package/dist/mapping-service.d.ts +22 -0
- package/dist/mapping-service.js +309 -30
- package/dist/mixin-parser.d.ts +1 -0
- package/dist/mixin-parser.js +134 -16
- package/dist/mixin-validator.d.ts +93 -2
- package/dist/mixin-validator.js +464 -41
- package/dist/mod-analyzer.d.ts +2 -0
- package/dist/mod-analyzer.js +7 -0
- package/dist/mod-decompile-service.d.ts +6 -0
- package/dist/mod-decompile-service.js +36 -4
- package/dist/mod-search-service.d.ts +1 -0
- package/dist/mod-search-service.js +96 -0
- package/dist/search-hit-accumulator.d.ts +1 -0
- package/dist/search-hit-accumulator.js +3 -0
- package/dist/source-resolver.js +0 -4
- package/dist/source-service.d.ts +91 -4
- package/dist/source-service.js +1153 -112
- package/dist/storage/files-repo.js +35 -8
- package/dist/types.d.ts +1 -0
- package/dist/version-service.js +30 -6
- package/dist/workspace-mapping-service.d.ts +1 -0
- package/dist/workspace-mapping-service.js +24 -0
- package/package.json +1 -1
package/dist/source-service.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, isAbsolute, resolve as resolvePath } from "node:path";
|
|
6
|
+
import fastGlob from "fast-glob";
|
|
2
7
|
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
3
8
|
import { loadConfig } from "./config.js";
|
|
4
9
|
import { decompileBinaryJar } from "./decompiler/vineflower.js";
|
|
@@ -12,7 +17,7 @@ import { resolveSourceTarget as resolveSourceTargetInternal } from "./source-res
|
|
|
12
17
|
import { applyMappingPipeline } from "./mapping-pipeline-service.js";
|
|
13
18
|
import { MappingService } from "./mapping-service.js";
|
|
14
19
|
import { extractSymbolsFromSource } from "./symbols/symbol-extractor.js";
|
|
15
|
-
import { iterateJavaEntriesAsUtf8 } from "./source-jar-reader.js";
|
|
20
|
+
import { iterateJavaEntriesAsUtf8, listJavaEntries } from "./source-jar-reader.js";
|
|
16
21
|
import { openDatabase } from "./storage/db.js";
|
|
17
22
|
import { ArtifactsRepo } from "./storage/artifacts-repo.js";
|
|
18
23
|
import { FilesRepo } from "./storage/files-repo.js";
|
|
@@ -20,6 +25,7 @@ import { IndexMetaRepo } from "./storage/index-meta-repo.js";
|
|
|
20
25
|
import { SymbolsRepo } from "./storage/symbols-repo.js";
|
|
21
26
|
import { RuntimeMetrics } from "./observability.js";
|
|
22
27
|
import { log } from "./logger.js";
|
|
28
|
+
import { normalizePathForHost } from "./path-converter.js";
|
|
23
29
|
import { createSearchHitAccumulator, decodeSearchCursor, encodeSearchCursor } from "./search-hit-accumulator.js";
|
|
24
30
|
import { WorkspaceMappingService } from "./workspace-mapping-service.js";
|
|
25
31
|
import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
|
|
@@ -27,6 +33,24 @@ import { RegistryService } from "./registry-service.js";
|
|
|
27
33
|
import { VersionDiffService } from "./version-diff-service.js";
|
|
28
34
|
import { ModDecompileService } from "./mod-decompile-service.js";
|
|
29
35
|
import { ModSearchService } from "./mod-search-service.js";
|
|
36
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
37
|
+
function truncateUtf8ToMaxBytes(content, maxBytes) {
|
|
38
|
+
const encoded = Buffer.from(content, "utf8");
|
|
39
|
+
if (encoded.length <= maxBytes) {
|
|
40
|
+
return content;
|
|
41
|
+
}
|
|
42
|
+
let end = Math.max(0, Math.min(maxBytes, encoded.length));
|
|
43
|
+
while (end > 0) {
|
|
44
|
+
try {
|
|
45
|
+
const decoded = utf8Decoder.decode(encoded.subarray(0, end));
|
|
46
|
+
return decoded;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
end -= 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
30
54
|
const INDEX_SCHEMA_VERSION = 1;
|
|
31
55
|
const SYMBOL_KINDS = ["class", "interface", "enum", "record", "method", "field"];
|
|
32
56
|
function isSymbolKind(value) {
|
|
@@ -43,6 +67,43 @@ const MAX_REGEX_RESULT_LIMIT = 100;
|
|
|
43
67
|
function normalizePathStyle(path) {
|
|
44
68
|
return path.replaceAll("\\", "/");
|
|
45
69
|
}
|
|
70
|
+
function escapeRegexLiteral(value) {
|
|
71
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
72
|
+
}
|
|
73
|
+
function hasExactVersionToken(path, version) {
|
|
74
|
+
const normalizedPath = normalizePathStyle(path).toLowerCase();
|
|
75
|
+
const normalizedVersion = version.trim().toLowerCase();
|
|
76
|
+
if (!normalizedVersion) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
// Avoid prefix false-positives like "1.21.1" matching "1.21.10".
|
|
80
|
+
const pattern = new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}([^0-9a-z]|$)`, "i");
|
|
81
|
+
return pattern.test(normalizedPath);
|
|
82
|
+
}
|
|
83
|
+
function normalizeOptionalProjectPath(projectPath) {
|
|
84
|
+
if (!projectPath) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const trimmed = projectPath.trim();
|
|
88
|
+
if (!trimmed) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const normalized = normalizePathForHost(trimmed, undefined, "projectPath");
|
|
92
|
+
return isAbsolute(normalized) ? normalized : resolvePath(process.cwd(), normalized);
|
|
93
|
+
}
|
|
94
|
+
function buildVersionSourceSearchRoots(projectPath) {
|
|
95
|
+
const roots = new Set();
|
|
96
|
+
if (projectPath) {
|
|
97
|
+
roots.add(resolvePath(projectPath, ".gradle", "loom-cache"));
|
|
98
|
+
roots.add(resolvePath(projectPath, ".gradle-user", "caches", "fabric-loom"));
|
|
99
|
+
roots.add(resolvePath(projectPath, ".gradle", "caches", "fabric-loom"));
|
|
100
|
+
return [...roots];
|
|
101
|
+
}
|
|
102
|
+
const homeGradle = resolvePath(homedir(), ".gradle");
|
|
103
|
+
roots.add(resolvePath(homeGradle, "loom-cache"));
|
|
104
|
+
roots.add(resolvePath(homeGradle, "caches", "fabric-loom"));
|
|
105
|
+
return [...roots];
|
|
106
|
+
}
|
|
46
107
|
function parseQualifiedMethodSymbol(symbol) {
|
|
47
108
|
const trimmed = symbol.trim();
|
|
48
109
|
const separator = trimmed.lastIndexOf(".");
|
|
@@ -91,6 +152,20 @@ function normalizeStrictPositiveInt(value, field) {
|
|
|
91
152
|
}
|
|
92
153
|
return value;
|
|
93
154
|
}
|
|
155
|
+
const COMMON_SOURCE_ROOTS = [
|
|
156
|
+
"src/main/java",
|
|
157
|
+
"src/client/java",
|
|
158
|
+
"common/src/main/java",
|
|
159
|
+
"common/src/client/java",
|
|
160
|
+
"fabric/src/main/java",
|
|
161
|
+
"fabric/src/client/java",
|
|
162
|
+
"neoforge/src/main/java",
|
|
163
|
+
"neoforge/src/client/java",
|
|
164
|
+
"forge/src/main/java",
|
|
165
|
+
"forge/src/client/java",
|
|
166
|
+
"quilt/src/main/java",
|
|
167
|
+
"quilt/src/client/java"
|
|
168
|
+
];
|
|
94
169
|
function normalizeMapping(mapping) {
|
|
95
170
|
if (mapping == null) {
|
|
96
171
|
return "official";
|
|
@@ -104,7 +179,11 @@ function normalizeMapping(mapping) {
|
|
|
104
179
|
throw createError({
|
|
105
180
|
code: ERROR_CODES.MAPPING_UNAVAILABLE,
|
|
106
181
|
message: `Unsupported mapping "${mapping}".`,
|
|
107
|
-
details: {
|
|
182
|
+
details: {
|
|
183
|
+
mapping,
|
|
184
|
+
nextAction: "Try mapping=official which is always available.",
|
|
185
|
+
suggestedCall: { tool: "resolve-artifact", params: { mapping: "official" } }
|
|
186
|
+
}
|
|
108
187
|
});
|
|
109
188
|
}
|
|
110
189
|
function normalizeAccessWidenerNamespace(namespace) {
|
|
@@ -347,6 +426,7 @@ function buildSearchCursorContext(input) {
|
|
|
347
426
|
query: input.query,
|
|
348
427
|
intent: input.intent,
|
|
349
428
|
match: input.match,
|
|
429
|
+
queryMode: input.queryMode,
|
|
350
430
|
includeDefinition: input.includeDefinition,
|
|
351
431
|
packagePrefix: input.scope?.packagePrefix ?? "",
|
|
352
432
|
fileGlob: input.scope?.fileGlob ?? "",
|
|
@@ -522,11 +602,95 @@ export class SourceService {
|
|
|
522
602
|
this.symbolsRepo = new SymbolsRepo(this.db);
|
|
523
603
|
this.refreshCacheMetrics();
|
|
524
604
|
}
|
|
605
|
+
async discoverVersionSourceJar(input) {
|
|
606
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
|
|
607
|
+
const searchRoots = buildVersionSourceSearchRoots(normalizedProjectPath);
|
|
608
|
+
const searchedPaths = [];
|
|
609
|
+
const candidates = [];
|
|
610
|
+
const seen = new Set();
|
|
611
|
+
for (const root of searchRoots) {
|
|
612
|
+
searchedPaths.push(root);
|
|
613
|
+
let discovered = [];
|
|
614
|
+
try {
|
|
615
|
+
discovered = fastGlob.sync("**/*sources.jar", {
|
|
616
|
+
cwd: root,
|
|
617
|
+
absolute: true,
|
|
618
|
+
onlyFiles: true
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
for (const candidatePath of discovered) {
|
|
625
|
+
const normalizedPath = normalizePathStyle(candidatePath);
|
|
626
|
+
if (seen.has(normalizedPath)) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
seen.add(normalizedPath);
|
|
630
|
+
const lower = normalizedPath.toLowerCase();
|
|
631
|
+
if (!lower.includes(input.version.toLowerCase()) && !lower.includes("minecraft")) {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
let javaEntries = [];
|
|
635
|
+
try {
|
|
636
|
+
javaEntries = await listJavaEntries(normalizedPath);
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (javaEntries.length === 0) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const hasMinecraftNamespace = javaEntries.some((entry) => normalizePathStyle(entry).startsWith("net/minecraft/"));
|
|
645
|
+
const score = (hasMinecraftNamespace ? 10_000 : 0) +
|
|
646
|
+
(lower.includes("minecraft-merged") ? 2_000 : 0) +
|
|
647
|
+
(lower.includes(input.version.toLowerCase()) ? 1_000 : 0) +
|
|
648
|
+
Math.min(javaEntries.length, 500);
|
|
649
|
+
candidates.push({
|
|
650
|
+
jarPath: normalizedPath,
|
|
651
|
+
javaEntryCount: javaEntries.length,
|
|
652
|
+
hasMinecraftNamespace,
|
|
653
|
+
score
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
candidates.sort((left, right) => {
|
|
658
|
+
if (right.score !== left.score) {
|
|
659
|
+
return right.score - left.score;
|
|
660
|
+
}
|
|
661
|
+
return left.jarPath.localeCompare(right.jarPath);
|
|
662
|
+
});
|
|
663
|
+
const selected = candidates.find((candidate) => candidate.hasMinecraftNamespace) ?? candidates[0];
|
|
664
|
+
const candidateArtifacts = candidates
|
|
665
|
+
.slice(0, 20)
|
|
666
|
+
.map((candidate) => `${candidate.jarPath}#java=${candidate.javaEntryCount}`);
|
|
667
|
+
return {
|
|
668
|
+
searchedPaths,
|
|
669
|
+
candidateArtifacts,
|
|
670
|
+
selectedSourceJarPath: selected?.jarPath
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
buildVersionSourceRecoveryCommand(projectPath) {
|
|
674
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(projectPath);
|
|
675
|
+
const prefix = normalizedProjectPath
|
|
676
|
+
? `cd ${JSON.stringify(normalizedProjectPath)} && `
|
|
677
|
+
: "";
|
|
678
|
+
return `${prefix}./gradlew genSources --no-daemon`;
|
|
679
|
+
}
|
|
525
680
|
async resolveArtifact(input) {
|
|
526
681
|
const kind = input.target.kind;
|
|
527
|
-
|
|
682
|
+
let value = input.target.value?.trim();
|
|
528
683
|
const mapping = normalizeMapping(input.mapping);
|
|
684
|
+
const scope = input.scope;
|
|
529
685
|
const warnings = [];
|
|
686
|
+
// P5: preferProjectVersion - detect MC version from gradle.properties
|
|
687
|
+
if (input.preferProjectVersion && input.projectPath && kind === "version") {
|
|
688
|
+
const detected = await this.workspaceMappingService.detectProjectMinecraftVersion(input.projectPath);
|
|
689
|
+
if (detected && detected !== value) {
|
|
690
|
+
warnings.push(`Overriding version "${value}" with project version "${detected}" from gradle.properties.`);
|
|
691
|
+
}
|
|
692
|
+
value = detected ?? value;
|
|
693
|
+
}
|
|
530
694
|
if (!value) {
|
|
531
695
|
throw createError({
|
|
532
696
|
code: ERROR_CODES.INVALID_INPUT,
|
|
@@ -552,6 +716,7 @@ export class SourceService {
|
|
|
552
716
|
try {
|
|
553
717
|
let resolvedTarget = { kind, value };
|
|
554
718
|
let resolvedVersion;
|
|
719
|
+
let versionSourceDiscovery;
|
|
555
720
|
if (kind === "version") {
|
|
556
721
|
const versionJar = await this.versionService.resolveVersionJar(value);
|
|
557
722
|
resolvedVersion = versionJar.version;
|
|
@@ -577,6 +742,19 @@ export class SourceService {
|
|
|
577
742
|
warnings.push(`Version ${resolvedVersion} is unobfuscated; ${mapping} mappings are not applicable. Using official names.`);
|
|
578
743
|
effectiveMapping = "official";
|
|
579
744
|
}
|
|
745
|
+
if (kind === "version" && resolvedVersion && effectiveMapping === "mojang" && scope !== "vanilla") {
|
|
746
|
+
versionSourceDiscovery = await this.discoverVersionSourceJar({
|
|
747
|
+
version: resolvedVersion,
|
|
748
|
+
projectPath: input.projectPath
|
|
749
|
+
});
|
|
750
|
+
if (versionSourceDiscovery.selectedSourceJarPath) {
|
|
751
|
+
resolvedTarget = {
|
|
752
|
+
kind: "jar",
|
|
753
|
+
value: versionSourceDiscovery.selectedSourceJarPath
|
|
754
|
+
};
|
|
755
|
+
warnings.push(`Resolved source-backed artifact from Loom cache candidate: ${versionSourceDiscovery.selectedSourceJarPath}.`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
580
758
|
const resolved = await resolveSourceTargetInternal(resolvedTarget, {
|
|
581
759
|
// mojang requires source-backed artifact guarantee; force resolution to consider decompile candidate
|
|
582
760
|
// and reject later if mapping cannot be applied.
|
|
@@ -594,11 +772,59 @@ export class SourceService {
|
|
|
594
772
|
}
|
|
595
773
|
}, this.config);
|
|
596
774
|
resolved.version = resolvedVersion;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
775
|
+
let mappingDecision;
|
|
776
|
+
try {
|
|
777
|
+
mappingDecision = applyMappingPipeline({
|
|
778
|
+
requestedMapping: effectiveMapping,
|
|
779
|
+
target: { kind, value },
|
|
780
|
+
resolved
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
catch (caughtError) {
|
|
784
|
+
if (isAppError(caughtError) && caughtError.code === ERROR_CODES.MAPPING_NOT_APPLIED) {
|
|
785
|
+
const isVanillaMojang = scope === "vanilla" && effectiveMapping === "mojang";
|
|
786
|
+
let suggestedCall;
|
|
787
|
+
let nextAction;
|
|
788
|
+
if (isVanillaMojang && input.projectPath) {
|
|
789
|
+
suggestedCall = {
|
|
790
|
+
tool: "resolve-artifact",
|
|
791
|
+
params: { targetKind: kind, targetValue: value, mapping: "mojang", scope: "merged", projectPath: input.projectPath }
|
|
792
|
+
};
|
|
793
|
+
nextAction =
|
|
794
|
+
"scope=vanilla blocks Loom cache discovery needed for mojang mapping. " +
|
|
795
|
+
"Retry with scope=merged to allow source-jar resolution from the project cache.";
|
|
796
|
+
}
|
|
797
|
+
else if (isVanillaMojang) {
|
|
798
|
+
suggestedCall = {
|
|
799
|
+
tool: "resolve-artifact",
|
|
800
|
+
params: { targetKind: kind, targetValue: value, mapping: "official", scope: "vanilla" }
|
|
801
|
+
};
|
|
802
|
+
nextAction =
|
|
803
|
+
"scope=vanilla blocks Loom cache discovery needed for mojang mapping. " +
|
|
804
|
+
"Without a projectPath, use mapping=official to read vanilla obfuscated names.";
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
suggestedCall = {
|
|
808
|
+
tool: "resolve-artifact",
|
|
809
|
+
params: { targetKind: kind, targetValue: value, mapping: "official", ...(scope ? { scope } : {}) }
|
|
810
|
+
};
|
|
811
|
+
nextAction = "Retry with mapping=official to use obfuscated names.";
|
|
812
|
+
}
|
|
813
|
+
throw createError({
|
|
814
|
+
code: ERROR_CODES.MAPPING_NOT_APPLIED,
|
|
815
|
+
message: caughtError.message,
|
|
816
|
+
details: {
|
|
817
|
+
...(caughtError.details ?? {}),
|
|
818
|
+
searchedPaths: versionSourceDiscovery?.searchedPaths ?? [],
|
|
819
|
+
candidateArtifacts: versionSourceDiscovery?.candidateArtifacts ?? resolved.adjacentSourceCandidates ?? [],
|
|
820
|
+
recommendedCommand: this.buildVersionSourceRecoveryCommand(input.projectPath),
|
|
821
|
+
nextAction,
|
|
822
|
+
suggestedCall
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
throw caughtError;
|
|
827
|
+
}
|
|
602
828
|
const additionalTransformChain = [];
|
|
603
829
|
if (effectiveMapping === "intermediary" || effectiveMapping === "yarn") {
|
|
604
830
|
if (!resolved.version) {
|
|
@@ -608,7 +834,8 @@ export class SourceService {
|
|
|
608
834
|
details: {
|
|
609
835
|
mapping: effectiveMapping,
|
|
610
836
|
target: { kind, value },
|
|
611
|
-
nextAction: "Use targetKind=version or a versioned Maven coordinate so mapping artifacts can be resolved."
|
|
837
|
+
nextAction: "Use targetKind=version or a versioned Maven coordinate so mapping artifacts can be resolved.",
|
|
838
|
+
suggestedCall: { tool: "resolve-artifact", params: { targetKind: "version", targetValue: value, ...(scope ? { scope } : {}) } }
|
|
612
839
|
}
|
|
613
840
|
});
|
|
614
841
|
}
|
|
@@ -631,8 +858,46 @@ export class SourceService {
|
|
|
631
858
|
resolved.requestedMapping = effectiveMapping;
|
|
632
859
|
resolved.mappingApplied = mappingDecision.mappingApplied;
|
|
633
860
|
resolved.provenance = provenance;
|
|
634
|
-
resolved.qualityFlags = mappingDecision.qualityFlags;
|
|
861
|
+
resolved.qualityFlags = [...mappingDecision.qualityFlags];
|
|
862
|
+
if (versionSourceDiscovery?.candidateArtifacts.length) {
|
|
863
|
+
resolved.qualityFlags.push("source-jar-found");
|
|
864
|
+
}
|
|
865
|
+
if (versionSourceDiscovery?.selectedSourceJarPath) {
|
|
866
|
+
resolved.qualityFlags.push("source-jar-validated");
|
|
867
|
+
if (kind === "version" && !hasExactVersionToken(versionSourceDiscovery.selectedSourceJarPath, value)) {
|
|
868
|
+
if (input.strictVersion) {
|
|
869
|
+
throw createError({
|
|
870
|
+
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
871
|
+
message: `Strict version match failed: requested "${value}" but nearest source jar is for a different version.`,
|
|
872
|
+
details: {
|
|
873
|
+
requestedVersion: value,
|
|
874
|
+
selectedSourceJar: versionSourceDiscovery.selectedSourceJarPath,
|
|
875
|
+
candidateArtifacts: versionSourceDiscovery.candidateArtifacts,
|
|
876
|
+
nextAction: "Use strictVersion=false (default) to allow approximation, or ensure the exact version source jar is in the Loom cache.",
|
|
877
|
+
suggestedCall: { tool: "resolve-artifact", params: { targetKind: "version", targetValue: value, strictVersion: false } }
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
resolved.qualityFlags.push("version-approximated");
|
|
882
|
+
warnings.push(`Requested version "${value}" but resolved source jar does not contain exact version string: ${versionSourceDiscovery.selectedSourceJarPath}`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
resolved.qualityFlags = [...new Set(resolved.qualityFlags)];
|
|
635
886
|
await this.ingestIfNeeded(resolved);
|
|
887
|
+
let sampleEntries;
|
|
888
|
+
if (resolved.sourceJarPath) {
|
|
889
|
+
try {
|
|
890
|
+
const javaEntries = await listJavaEntries(resolved.sourceJarPath);
|
|
891
|
+
const MAX_SAMPLE = 10;
|
|
892
|
+
sampleEntries = javaEntries.slice(0, MAX_SAMPLE);
|
|
893
|
+
if (javaEntries.length > MAX_SAMPLE) {
|
|
894
|
+
sampleEntries.push(`... and ${javaEntries.length - MAX_SAMPLE} more .java entries`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
// non-fatal: sampleEntries remains undefined
|
|
899
|
+
}
|
|
900
|
+
}
|
|
636
901
|
return {
|
|
637
902
|
artifactId: resolved.artifactId,
|
|
638
903
|
origin: resolved.origin,
|
|
@@ -645,9 +910,10 @@ export class SourceService {
|
|
|
645
910
|
requestedMapping: effectiveMapping,
|
|
646
911
|
mappingApplied: mappingDecision.mappingApplied,
|
|
647
912
|
provenance,
|
|
648
|
-
qualityFlags:
|
|
913
|
+
qualityFlags: resolved.qualityFlags,
|
|
649
914
|
repoUrl: resolved.repoUrl,
|
|
650
|
-
warnings
|
|
915
|
+
warnings,
|
|
916
|
+
sampleEntries
|
|
651
917
|
};
|
|
652
918
|
}
|
|
653
919
|
catch (caughtError) {
|
|
@@ -701,11 +967,13 @@ export class SourceService {
|
|
|
701
967
|
const snippetWindow = buildSnippetWindow(input.include?.snippetLines);
|
|
702
968
|
const regexPattern = match === "regex" ? compileRegex(query) : undefined;
|
|
703
969
|
const scope = input.scope;
|
|
970
|
+
const queryMode = input.queryMode ?? "auto";
|
|
704
971
|
const cursorContext = buildSearchCursorContext({
|
|
705
972
|
artifactId: artifact.artifactId,
|
|
706
973
|
query,
|
|
707
974
|
intent,
|
|
708
975
|
match,
|
|
976
|
+
queryMode,
|
|
709
977
|
scope,
|
|
710
978
|
includeDefinition
|
|
711
979
|
});
|
|
@@ -720,6 +988,8 @@ export class SourceService {
|
|
|
720
988
|
const recordHit = (hit) => {
|
|
721
989
|
accumulator.add(hit);
|
|
722
990
|
};
|
|
991
|
+
const hasSeparators = /[._$]/.test(query);
|
|
992
|
+
const tokenOnlyTextIntent = intent === "text" && queryMode === "token";
|
|
723
993
|
if (intent === "symbol") {
|
|
724
994
|
this.searchSymbolIntent(artifact.artifactId, query, match, scope, snippetWindow, regexPattern, recordHit);
|
|
725
995
|
// WS4: Use repo-level COUNT for symbol totalApprox when not regex
|
|
@@ -734,14 +1004,21 @@ export class SourceService {
|
|
|
734
1004
|
accumulator.setTotalApproxOverride(approxCount);
|
|
735
1005
|
}
|
|
736
1006
|
}
|
|
1007
|
+
else if (queryMode === "literal" && intent === "text") {
|
|
1008
|
+
// F-03: queryMode=literal forces substring scan for text intent
|
|
1009
|
+
this.metrics.recordSearchFallback();
|
|
1010
|
+
this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
1011
|
+
}
|
|
737
1012
|
else if (!indexedSearchEnabled) {
|
|
738
1013
|
this.metrics.recordIndexedDisabled();
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1014
|
+
if (!tokenOnlyTextIntent) {
|
|
1015
|
+
this.metrics.recordSearchFallback();
|
|
1016
|
+
if (intent === "path") {
|
|
1017
|
+
this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
1021
|
+
}
|
|
745
1022
|
}
|
|
746
1023
|
}
|
|
747
1024
|
else if (canUseIndexedSearchPath(indexedSearchEnabled, intent, match, scope)) {
|
|
@@ -757,6 +1034,10 @@ export class SourceService {
|
|
|
757
1034
|
// WS4: Use repo-level COUNT for totalApprox instead of accumulator count
|
|
758
1035
|
const approxCount = this.filesRepo.countTextCandidates(artifact.artifactId, query);
|
|
759
1036
|
accumulator.setTotalApproxOverride(approxCount);
|
|
1037
|
+
// F-03: queryMode=auto fallback — when indexed returns 0 hits and query has separators, retry with literal scan
|
|
1038
|
+
if (queryMode === "auto" && hasSeparators && accumulator.currentCount() === 0) {
|
|
1039
|
+
this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
1040
|
+
}
|
|
760
1041
|
}
|
|
761
1042
|
this.metrics.recordSearchIndexedHit();
|
|
762
1043
|
}
|
|
@@ -768,6 +1049,20 @@ export class SourceService {
|
|
|
768
1049
|
match,
|
|
769
1050
|
reason: caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
770
1051
|
});
|
|
1052
|
+
// F-03: queryMode=token suppresses error-path fallback to brute-force scan
|
|
1053
|
+
if (!tokenOnlyTextIntent) {
|
|
1054
|
+
if (intent === "path") {
|
|
1055
|
+
this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
if (!tokenOnlyTextIntent) {
|
|
1065
|
+
this.metrics.recordSearchFallback();
|
|
771
1066
|
if (intent === "path") {
|
|
772
1067
|
this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
773
1068
|
}
|
|
@@ -776,15 +1071,6 @@ export class SourceService {
|
|
|
776
1071
|
}
|
|
777
1072
|
}
|
|
778
1073
|
}
|
|
779
|
-
else {
|
|
780
|
-
this.metrics.recordSearchFallback();
|
|
781
|
-
if (intent === "path") {
|
|
782
|
-
this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
783
|
-
}
|
|
784
|
-
else {
|
|
785
|
-
this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
1074
|
this.metrics.recordSearchIntentDuration(intent, Date.now() - intentStartedAt);
|
|
789
1075
|
const finalizedHits = accumulator.finalize();
|
|
790
1076
|
const page = finalizedHits.page;
|
|
@@ -808,11 +1094,14 @@ export class SourceService {
|
|
|
808
1094
|
this.metrics.recordOneHopExpansion(relations.length);
|
|
809
1095
|
}
|
|
810
1096
|
this.metrics.recordSearchTokenBytesReturned(Buffer.byteLength(JSON.stringify({ hits: page, relations }), "utf8"));
|
|
1097
|
+
// B5: If post-filtering eliminated all hits on the first page, the SQL-based
|
|
1098
|
+
// totalApprox is misleading — correct it to 0.
|
|
1099
|
+
const totalApprox = page.length === 0 && !cursor ? 0 : finalizedHits.totalApprox;
|
|
811
1100
|
return {
|
|
812
1101
|
hits: page,
|
|
813
1102
|
relations: relations && relations.length > 0 ? relations : undefined,
|
|
814
1103
|
nextCursor,
|
|
815
|
-
totalApprox
|
|
1104
|
+
totalApprox,
|
|
816
1105
|
mappingApplied: artifact.mappingApplied ?? "official"
|
|
817
1106
|
};
|
|
818
1107
|
}
|
|
@@ -835,9 +1124,7 @@ export class SourceService {
|
|
|
835
1124
|
const maxBytes = clampLimit(input.maxBytes, this.config.maxContentBytes, Number.MAX_SAFE_INTEGER);
|
|
836
1125
|
const fullBytes = Buffer.byteLength(row.content, "utf8");
|
|
837
1126
|
const truncated = fullBytes > maxBytes;
|
|
838
|
-
const content = truncated
|
|
839
|
-
? Buffer.from(row.content, "utf8").slice(0, maxBytes).toString("utf8")
|
|
840
|
-
: row.content;
|
|
1127
|
+
const content = truncated ? truncateUtf8ToMaxBytes(row.content, maxBytes) : row.content;
|
|
841
1128
|
if (truncated) {
|
|
842
1129
|
log("warn", "source.get_file.truncated", {
|
|
843
1130
|
artifactId: input.artifactId,
|
|
@@ -1133,7 +1420,11 @@ export class SourceService {
|
|
|
1133
1420
|
throw createError({
|
|
1134
1421
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1135
1422
|
message: "No Minecraft versions were returned by manifest.",
|
|
1136
|
-
details: {
|
|
1423
|
+
details: {
|
|
1424
|
+
includeSnapshots,
|
|
1425
|
+
nextAction: "Use list-versions to see available Minecraft versions.",
|
|
1426
|
+
suggestedCall: { tool: "list-versions", params: {} }
|
|
1427
|
+
}
|
|
1137
1428
|
});
|
|
1138
1429
|
}
|
|
1139
1430
|
const chronological = [...manifestOrder].reverse();
|
|
@@ -1145,14 +1436,22 @@ export class SourceService {
|
|
|
1145
1436
|
throw createError({
|
|
1146
1437
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1147
1438
|
message: `fromVersion "${requestedFrom}" was not found in manifest.`,
|
|
1148
|
-
details: {
|
|
1439
|
+
details: {
|
|
1440
|
+
fromVersion: requestedFrom,
|
|
1441
|
+
nextAction: "Use list-versions to see available Minecraft versions.",
|
|
1442
|
+
suggestedCall: { tool: "list-versions", params: {} }
|
|
1443
|
+
}
|
|
1149
1444
|
});
|
|
1150
1445
|
}
|
|
1151
1446
|
if (toIndex < 0) {
|
|
1152
1447
|
throw createError({
|
|
1153
1448
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1154
1449
|
message: `toVersion "${requestedTo}" was not found in manifest.`,
|
|
1155
|
-
details: {
|
|
1450
|
+
details: {
|
|
1451
|
+
toVersion: requestedTo,
|
|
1452
|
+
nextAction: "Use list-versions to see available Minecraft versions.",
|
|
1453
|
+
suggestedCall: { tool: "list-versions", params: {} }
|
|
1454
|
+
}
|
|
1156
1455
|
});
|
|
1157
1456
|
}
|
|
1158
1457
|
if (fromIndex > toIndex) {
|
|
@@ -1291,7 +1590,11 @@ export class SourceService {
|
|
|
1291
1590
|
if (manifestOrder.length === 0) {
|
|
1292
1591
|
throw createError({
|
|
1293
1592
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1294
|
-
message: "No Minecraft versions were returned by manifest."
|
|
1593
|
+
message: "No Minecraft versions were returned by manifest.",
|
|
1594
|
+
details: {
|
|
1595
|
+
nextAction: "Use list-versions to see available Minecraft versions.",
|
|
1596
|
+
suggestedCall: { tool: "list-versions", params: {} }
|
|
1597
|
+
}
|
|
1295
1598
|
});
|
|
1296
1599
|
}
|
|
1297
1600
|
const chronological = [...manifestOrder].reverse();
|
|
@@ -1301,14 +1604,22 @@ export class SourceService {
|
|
|
1301
1604
|
throw createError({
|
|
1302
1605
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1303
1606
|
message: `fromVersion "${fromVersion}" was not found in manifest.`,
|
|
1304
|
-
details: {
|
|
1607
|
+
details: {
|
|
1608
|
+
fromVersion,
|
|
1609
|
+
nextAction: "Use list-versions to see available Minecraft versions.",
|
|
1610
|
+
suggestedCall: { tool: "list-versions", params: {} }
|
|
1611
|
+
}
|
|
1305
1612
|
});
|
|
1306
1613
|
}
|
|
1307
1614
|
if (toIndex < 0) {
|
|
1308
1615
|
throw createError({
|
|
1309
1616
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1310
1617
|
message: `toVersion "${toVersion}" was not found in manifest.`,
|
|
1311
|
-
details: {
|
|
1618
|
+
details: {
|
|
1619
|
+
toVersion,
|
|
1620
|
+
nextAction: "Use list-versions to see available Minecraft versions.",
|
|
1621
|
+
suggestedCall: { tool: "list-versions", params: {} }
|
|
1622
|
+
}
|
|
1312
1623
|
});
|
|
1313
1624
|
}
|
|
1314
1625
|
if (fromIndex > toIndex) {
|
|
@@ -1431,18 +1742,18 @@ export class SourceService {
|
|
|
1431
1742
|
: diffMembersByKey(fromMembers.fields, toMembers.fields, (member) => member.name, true);
|
|
1432
1743
|
// Remap diff delta members for non-official mappings
|
|
1433
1744
|
const remapDelta = async (delta, kind) => {
|
|
1434
|
-
const [
|
|
1745
|
+
const [addedResult, removedResult] = await Promise.all([
|
|
1435
1746
|
this.remapSignatureMembers(delta.added, kind, toVersion, mapping, input.sourcePriority, warnings),
|
|
1436
1747
|
this.remapSignatureMembers(delta.removed, kind, fromVersion, mapping, input.sourcePriority, warnings)
|
|
1437
1748
|
]);
|
|
1438
1749
|
const remappedModified = await Promise.all(delta.modified.map(async (change) => {
|
|
1439
|
-
const [
|
|
1750
|
+
const [fromResult, toResult] = await Promise.all([
|
|
1440
1751
|
this.remapSignatureMembers([change.from], kind, fromVersion, mapping, input.sourcePriority, warnings),
|
|
1441
1752
|
this.remapSignatureMembers([change.to], kind, toVersion, mapping, input.sourcePriority, warnings)
|
|
1442
1753
|
]);
|
|
1443
|
-
return { ...change, from:
|
|
1754
|
+
return { ...change, from: fromResult.members[0], to: toResult.members[0] };
|
|
1444
1755
|
}));
|
|
1445
|
-
return { added:
|
|
1756
|
+
return { added: addedResult.members, removed: removedResult.members, modified: remappedModified };
|
|
1446
1757
|
};
|
|
1447
1758
|
const [remappedConstructors, remappedMethods, remappedFields] = await Promise.all([
|
|
1448
1759
|
remapDelta(constructors, "method"),
|
|
@@ -1490,6 +1801,78 @@ export class SourceService {
|
|
|
1490
1801
|
warnings
|
|
1491
1802
|
};
|
|
1492
1803
|
}
|
|
1804
|
+
findClass(input) {
|
|
1805
|
+
const className = input.className.trim();
|
|
1806
|
+
if (!className) {
|
|
1807
|
+
throw createError({
|
|
1808
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1809
|
+
message: "className must be non-empty."
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
const artifactId = input.artifactId.trim();
|
|
1813
|
+
if (!artifactId) {
|
|
1814
|
+
throw createError({
|
|
1815
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1816
|
+
message: "artifactId must be non-empty."
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
// Verify artifact exists
|
|
1820
|
+
this.getArtifact(artifactId);
|
|
1821
|
+
const limit = Math.max(1, Math.min(input.limit ?? 20, 200));
|
|
1822
|
+
const warnings = [];
|
|
1823
|
+
const isQualified = className.includes(".");
|
|
1824
|
+
if (isQualified) {
|
|
1825
|
+
// Qualified name: fetch a broad candidate set first, then filter to exact class path/FQCN.
|
|
1826
|
+
// Limiting before filtering can miss the target when many packages share the same simple name.
|
|
1827
|
+
const classPath = className.replace(/\./g, "/");
|
|
1828
|
+
const result = this.symbolsRepo.findScopedSymbols({
|
|
1829
|
+
artifactId,
|
|
1830
|
+
query: className.split(".").at(-1) ?? className,
|
|
1831
|
+
match: "exact",
|
|
1832
|
+
limit: 5000
|
|
1833
|
+
});
|
|
1834
|
+
const matches = result.items
|
|
1835
|
+
.filter((row) => {
|
|
1836
|
+
const isTypeSymbol = row.symbolKind === "class" || row.symbolKind === "interface" ||
|
|
1837
|
+
row.symbolKind === "enum" || row.symbolKind === "record";
|
|
1838
|
+
if (!isTypeSymbol)
|
|
1839
|
+
return false;
|
|
1840
|
+
const rowQualified = row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
1841
|
+
return rowQualified === className || row.filePath === `${classPath}.java`;
|
|
1842
|
+
})
|
|
1843
|
+
.map((row) => ({
|
|
1844
|
+
qualifiedName: row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", "."),
|
|
1845
|
+
filePath: row.filePath,
|
|
1846
|
+
line: row.line,
|
|
1847
|
+
symbolKind: row.symbolKind
|
|
1848
|
+
}))
|
|
1849
|
+
.slice(0, limit);
|
|
1850
|
+
return { matches, total: matches.length, warnings };
|
|
1851
|
+
}
|
|
1852
|
+
// Simple name: search for exact symbol name match among type symbols
|
|
1853
|
+
const result = this.symbolsRepo.findScopedSymbols({
|
|
1854
|
+
artifactId,
|
|
1855
|
+
query: className,
|
|
1856
|
+
match: "exact",
|
|
1857
|
+
limit: limit * 5 // over-fetch to filter by kind
|
|
1858
|
+
});
|
|
1859
|
+
const matches = [];
|
|
1860
|
+
for (const row of result.items) {
|
|
1861
|
+
if (matches.length >= limit)
|
|
1862
|
+
break;
|
|
1863
|
+
const isTypeSymbol = row.symbolKind === "class" || row.symbolKind === "interface" ||
|
|
1864
|
+
row.symbolKind === "enum" || row.symbolKind === "record";
|
|
1865
|
+
if (!isTypeSymbol)
|
|
1866
|
+
continue;
|
|
1867
|
+
matches.push({
|
|
1868
|
+
qualifiedName: row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", "."),
|
|
1869
|
+
filePath: row.filePath,
|
|
1870
|
+
line: row.line,
|
|
1871
|
+
symbolKind: row.symbolKind
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
return { matches, total: matches.length, warnings };
|
|
1875
|
+
}
|
|
1493
1876
|
async getClassSource(input) {
|
|
1494
1877
|
const className = input.className.trim();
|
|
1495
1878
|
if (!className) {
|
|
@@ -1498,9 +1881,16 @@ export class SourceService {
|
|
|
1498
1881
|
message: "className must be non-empty."
|
|
1499
1882
|
});
|
|
1500
1883
|
}
|
|
1884
|
+
const mode = input.mode ?? "metadata";
|
|
1501
1885
|
const startLine = normalizeStrictPositiveInt(input.startLine, "startLine");
|
|
1502
1886
|
const endLine = normalizeStrictPositiveInt(input.endLine, "endLine");
|
|
1503
|
-
|
|
1887
|
+
let maxLines = normalizeStrictPositiveInt(input.maxLines, "maxLines");
|
|
1888
|
+
const maxChars = normalizeStrictPositiveInt(input.maxChars, "maxChars");
|
|
1889
|
+
const outputFile = normalizeOptionalString(input.outputFile);
|
|
1890
|
+
// In snippet mode, default maxLines to 200 when no range or maxLines is specified
|
|
1891
|
+
if (mode === "snippet" && startLine == null && endLine == null && maxLines == null) {
|
|
1892
|
+
maxLines = 200;
|
|
1893
|
+
}
|
|
1504
1894
|
if (startLine != null && endLine != null && startLine > endLine) {
|
|
1505
1895
|
throw createError({
|
|
1506
1896
|
code: ERROR_CODES.INVALID_LINE_RANGE,
|
|
@@ -1540,7 +1930,11 @@ export class SourceService {
|
|
|
1540
1930
|
target: input.target,
|
|
1541
1931
|
mapping: input.mapping,
|
|
1542
1932
|
sourcePriority: input.sourcePriority,
|
|
1543
|
-
allowDecompile: input.allowDecompile
|
|
1933
|
+
allowDecompile: input.allowDecompile,
|
|
1934
|
+
projectPath: input.projectPath,
|
|
1935
|
+
scope: input.scope,
|
|
1936
|
+
preferProjectVersion: input.preferProjectVersion,
|
|
1937
|
+
strictVersion: input.strictVersion
|
|
1544
1938
|
});
|
|
1545
1939
|
artifactId = resolved.artifactId;
|
|
1546
1940
|
origin = resolved.origin;
|
|
@@ -1560,41 +1954,100 @@ export class SourceService {
|
|
|
1560
1954
|
}
|
|
1561
1955
|
const filePath = this.resolveClassFilePath(artifactId, className);
|
|
1562
1956
|
if (!filePath) {
|
|
1957
|
+
const simpleName = className.split(/[.$]/).at(-1) ?? className;
|
|
1958
|
+
const targetKind = input.target?.kind;
|
|
1959
|
+
const targetValue = input.target?.value;
|
|
1960
|
+
const scope = input.scope;
|
|
1961
|
+
let nextAction = `Use find-class to resolve the correct fully-qualified name for "${simpleName}".`;
|
|
1962
|
+
if (targetKind === "version" && scope && scope !== "merged" && !input.projectPath) {
|
|
1963
|
+
nextAction +=
|
|
1964
|
+
` If the class exists in a modded environment, retry with scope: "merged" and projectPath pointing to your mod project.`;
|
|
1965
|
+
}
|
|
1966
|
+
else if (targetKind === "version" && scope && scope !== "merged" && input.projectPath) {
|
|
1967
|
+
nextAction +=
|
|
1968
|
+
` The class may exist in merged sources; retry with scope: "merged".`;
|
|
1969
|
+
}
|
|
1563
1970
|
throw createError({
|
|
1564
1971
|
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
1565
1972
|
message: `Source for class "${className}" was not found.`,
|
|
1566
1973
|
details: {
|
|
1567
1974
|
artifactId,
|
|
1568
|
-
className
|
|
1975
|
+
className,
|
|
1976
|
+
...(scope ? { scope } : {}),
|
|
1977
|
+
...(targetKind ? { targetKind } : {}),
|
|
1978
|
+
...(targetValue ? { targetValue } : {}),
|
|
1979
|
+
mapping: mappingApplied,
|
|
1980
|
+
nextAction,
|
|
1981
|
+
suggestedCall: { tool: "find-class", params: { className: simpleName, artifactId } }
|
|
1569
1982
|
}
|
|
1570
1983
|
});
|
|
1571
1984
|
}
|
|
1572
1985
|
const row = this.filesRepo.getFileContent(artifactId, filePath);
|
|
1573
1986
|
if (!row) {
|
|
1987
|
+
const simpleName = className.split(/[.$]/).at(-1) ?? className;
|
|
1574
1988
|
throw createError({
|
|
1575
1989
|
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
1576
1990
|
message: `Source for class "${className}" was not found.`,
|
|
1577
1991
|
details: {
|
|
1578
1992
|
artifactId,
|
|
1579
1993
|
className,
|
|
1580
|
-
filePath
|
|
1994
|
+
filePath,
|
|
1995
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
1996
|
+
...(input.target?.kind ? { targetKind: input.target.kind } : {}),
|
|
1997
|
+
...(input.target?.value ? { targetValue: input.target.value } : {}),
|
|
1998
|
+
nextAction: `Use find-class to resolve the correct fully-qualified name for "${simpleName}".`,
|
|
1999
|
+
suggestedCall: { tool: "find-class", params: { className: simpleName, artifactId } }
|
|
1581
2000
|
}
|
|
1582
2001
|
});
|
|
1583
2002
|
}
|
|
1584
2003
|
const lines = row.content.split(/\r?\n/);
|
|
1585
2004
|
const totalLines = lines.length;
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
let
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
2005
|
+
let sourceText;
|
|
2006
|
+
let returnedStart;
|
|
2007
|
+
let returnedEnd;
|
|
2008
|
+
let truncated = false;
|
|
2009
|
+
let charsTruncated = false;
|
|
2010
|
+
if (mode === "metadata") {
|
|
2011
|
+
const metadataText = this.extractClassMetadata(filePath, row.content);
|
|
2012
|
+
sourceText = metadataText;
|
|
2013
|
+
returnedStart = 1;
|
|
2014
|
+
returnedEnd = totalLines;
|
|
2015
|
+
truncated = false;
|
|
2016
|
+
}
|
|
2017
|
+
else {
|
|
2018
|
+
// snippet and full modes use the existing line-range logic
|
|
2019
|
+
const requestedStart = startLine ?? 1;
|
|
2020
|
+
const requestedEnd = endLine ?? totalLines;
|
|
2021
|
+
const normalizedStart = Math.min(Math.max(1, requestedStart), Math.max(totalLines, 1));
|
|
2022
|
+
const normalizedEnd = Math.min(Math.max(normalizedStart, requestedEnd), Math.max(totalLines, 1));
|
|
2023
|
+
let selectedLines = lines.slice(normalizedStart - 1, normalizedEnd);
|
|
2024
|
+
const clippedByRange = normalizedStart !== requestedStart || normalizedEnd !== requestedEnd;
|
|
2025
|
+
let clippedByMax = false;
|
|
2026
|
+
if (maxLines != null && selectedLines.length > maxLines) {
|
|
2027
|
+
selectedLines = selectedLines.slice(0, maxLines);
|
|
2028
|
+
clippedByMax = true;
|
|
2029
|
+
}
|
|
2030
|
+
sourceText = selectedLines.join("\n");
|
|
2031
|
+
returnedStart = normalizedStart;
|
|
2032
|
+
returnedEnd = normalizedStart + Math.max(0, selectedLines.length - 1);
|
|
2033
|
+
truncated = clippedByRange || clippedByMax;
|
|
2034
|
+
}
|
|
2035
|
+
// Apply maxChars truncation
|
|
2036
|
+
if (maxChars != null && sourceText.length > maxChars) {
|
|
2037
|
+
sourceText = sourceText.slice(0, maxChars);
|
|
2038
|
+
charsTruncated = true;
|
|
2039
|
+
truncated = true;
|
|
2040
|
+
}
|
|
2041
|
+
// Write to file if outputFile is specified
|
|
2042
|
+
let resolvedOutputFile;
|
|
2043
|
+
if (outputFile) {
|
|
2044
|
+
const outputPath = isAbsolute(outputFile)
|
|
2045
|
+
? outputFile
|
|
2046
|
+
: resolvePath(outputFile);
|
|
2047
|
+
await writeFile(outputPath, sourceText, "utf8");
|
|
2048
|
+
resolvedOutputFile = outputPath;
|
|
2049
|
+
sourceText = `[Written to ${outputPath}]`;
|
|
2050
|
+
}
|
|
1598
2051
|
const normalizedProvenance = provenance ??
|
|
1599
2052
|
this.buildFallbackProvenance({
|
|
1600
2053
|
artifactId,
|
|
@@ -1604,19 +2057,22 @@ export class SourceService {
|
|
|
1604
2057
|
});
|
|
1605
2058
|
return {
|
|
1606
2059
|
className,
|
|
1607
|
-
|
|
2060
|
+
mode,
|
|
2061
|
+
sourceText,
|
|
1608
2062
|
totalLines,
|
|
1609
2063
|
returnedRange: {
|
|
1610
|
-
start:
|
|
2064
|
+
start: returnedStart,
|
|
1611
2065
|
end: returnedEnd
|
|
1612
2066
|
},
|
|
1613
|
-
truncated
|
|
2067
|
+
truncated,
|
|
2068
|
+
...(charsTruncated ? { charsTruncated } : {}),
|
|
1614
2069
|
origin,
|
|
1615
2070
|
artifactId,
|
|
1616
2071
|
requestedMapping,
|
|
1617
2072
|
mappingApplied,
|
|
1618
2073
|
provenance: normalizedProvenance,
|
|
1619
2074
|
qualityFlags,
|
|
2075
|
+
...(resolvedOutputFile ? { outputFile: resolvedOutputFile } : {}),
|
|
1620
2076
|
warnings
|
|
1621
2077
|
};
|
|
1622
2078
|
}
|
|
@@ -1668,7 +2124,11 @@ export class SourceService {
|
|
|
1668
2124
|
target: input.target,
|
|
1669
2125
|
mapping: requestedMapping,
|
|
1670
2126
|
sourcePriority: input.sourcePriority,
|
|
1671
|
-
allowDecompile: input.allowDecompile
|
|
2127
|
+
allowDecompile: input.allowDecompile,
|
|
2128
|
+
projectPath: input.projectPath,
|
|
2129
|
+
scope: input.scope,
|
|
2130
|
+
preferProjectVersion: input.preferProjectVersion,
|
|
2131
|
+
strictVersion: input.strictVersion
|
|
1672
2132
|
});
|
|
1673
2133
|
artifactId = resolved.artifactId;
|
|
1674
2134
|
origin = resolved.origin;
|
|
@@ -1694,7 +2154,8 @@ export class SourceService {
|
|
|
1694
2154
|
message: `Non-official mapping "${requestedMapping}" requires a version, but none was resolved.`,
|
|
1695
2155
|
details: {
|
|
1696
2156
|
mapping: requestedMapping,
|
|
1697
|
-
nextAction: "Resolve with targetKind=version or specify a versioned coordinate."
|
|
2157
|
+
nextAction: "Resolve with targetKind=version or specify a versioned coordinate.",
|
|
2158
|
+
suggestedCall: { tool: "resolve-artifact", params: { targetKind: "version", targetValue: "latest" } }
|
|
1698
2159
|
}
|
|
1699
2160
|
});
|
|
1700
2161
|
}
|
|
@@ -1722,13 +2183,13 @@ export class SourceService {
|
|
|
1722
2183
|
});
|
|
1723
2184
|
warnings.push(...signature.warnings);
|
|
1724
2185
|
let remappedConstructors = version != null
|
|
1725
|
-
? await this.remapSignatureMembers(signature.constructors, "method", version, requestedMapping, input.sourcePriority, warnings)
|
|
2186
|
+
? (await this.remapSignatureMembers(signature.constructors, "method", version, requestedMapping, input.sourcePriority, warnings)).members
|
|
1726
2187
|
: signature.constructors;
|
|
1727
2188
|
let remappedFields = version != null
|
|
1728
|
-
? await this.remapSignatureMembers(signature.fields, "field", version, requestedMapping, input.sourcePriority, warnings)
|
|
2189
|
+
? (await this.remapSignatureMembers(signature.fields, "field", version, requestedMapping, input.sourcePriority, warnings)).members
|
|
1729
2190
|
: signature.fields;
|
|
1730
2191
|
let remappedMethods = version != null
|
|
1731
|
-
? await this.remapSignatureMembers(signature.methods, "method", version, requestedMapping, input.sourcePriority, warnings)
|
|
2192
|
+
? (await this.remapSignatureMembers(signature.methods, "method", version, requestedMapping, input.sourcePriority, warnings)).members
|
|
1732
2193
|
: signature.methods;
|
|
1733
2194
|
// Apply memberPattern post-remap for non-official mappings
|
|
1734
2195
|
if (requestedMapping !== "official" && memberPattern) {
|
|
@@ -1787,40 +2248,272 @@ export class SourceService {
|
|
|
1787
2248
|
};
|
|
1788
2249
|
}
|
|
1789
2250
|
async validateMixin(input) {
|
|
1790
|
-
|
|
2251
|
+
// Mixin config mode: read JSON config(s) and derive sourcePaths
|
|
2252
|
+
if (input.mixinConfigPath) {
|
|
2253
|
+
const configPaths = Array.isArray(input.mixinConfigPath) ? input.mixinConfigPath : [input.mixinConfigPath];
|
|
2254
|
+
const allSourcePaths = [];
|
|
2255
|
+
for (const rawConfigPath of configPaths) {
|
|
2256
|
+
const normalizedConfigPath = normalizePathForHost(rawConfigPath, undefined, "mixinConfigPath");
|
|
2257
|
+
const resolvedConfigPath = isAbsolute(normalizedConfigPath)
|
|
2258
|
+
? normalizedConfigPath
|
|
2259
|
+
: resolvePath(process.cwd(), normalizedConfigPath);
|
|
2260
|
+
let configJson;
|
|
2261
|
+
try {
|
|
2262
|
+
const raw = await readFile(resolvedConfigPath, "utf-8");
|
|
2263
|
+
configJson = JSON.parse(raw);
|
|
2264
|
+
}
|
|
2265
|
+
catch (err) {
|
|
2266
|
+
throw createError({
|
|
2267
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
2268
|
+
message: `Could not read/parse mixinConfigPath "${rawConfigPath}": ${err instanceof Error ? err.message : String(err)}`
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
const pkg = configJson.package ?? "";
|
|
2272
|
+
const classNames = [
|
|
2273
|
+
...(configJson.mixins ?? []),
|
|
2274
|
+
...(configJson.client ?? []),
|
|
2275
|
+
...(configJson.server ?? [])
|
|
2276
|
+
];
|
|
2277
|
+
if (classNames.length === 0) {
|
|
2278
|
+
continue; // Skip empty configs in array mode
|
|
2279
|
+
}
|
|
2280
|
+
// Determine source root(s)
|
|
2281
|
+
const projectBase = input.projectPath
|
|
2282
|
+
? (isAbsolute(input.projectPath) ? input.projectPath : resolvePath(process.cwd(), input.projectPath))
|
|
2283
|
+
: dirname(resolvedConfigPath);
|
|
2284
|
+
let sourceRootCandidates;
|
|
2285
|
+
if (input.sourceRoots && input.sourceRoots.length > 0) {
|
|
2286
|
+
sourceRootCandidates = input.sourceRoots;
|
|
2287
|
+
}
|
|
2288
|
+
else if (input.sourceRoot) {
|
|
2289
|
+
sourceRootCandidates = [input.sourceRoot];
|
|
2290
|
+
}
|
|
2291
|
+
else {
|
|
2292
|
+
// Auto-detect: include any root that contains at least one configured mixin class.
|
|
2293
|
+
const detected = COMMON_SOURCE_ROOTS.filter((candidateRoot) => classNames.some((className) => {
|
|
2294
|
+
const fqcn = pkg ? `${pkg}.${className}` : className;
|
|
2295
|
+
const relative = fqcn.replace(/\./g, "/") + ".java";
|
|
2296
|
+
return existsSync(resolvePath(projectBase, candidateRoot, relative));
|
|
2297
|
+
}));
|
|
2298
|
+
if (detected.length > 0) {
|
|
2299
|
+
sourceRootCandidates = detected;
|
|
2300
|
+
}
|
|
2301
|
+
else {
|
|
2302
|
+
sourceRootCandidates = ["src/main/java"];
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
// Build sourcePaths by probing each class against candidate roots
|
|
2306
|
+
for (const cls of classNames) {
|
|
2307
|
+
const fqcn = pkg ? `${pkg}.${cls}` : cls;
|
|
2308
|
+
const relativePath = fqcn.replace(/\./g, "/") + ".java";
|
|
2309
|
+
let found = false;
|
|
2310
|
+
for (const root of sourceRootCandidates) {
|
|
2311
|
+
const candidate = resolvePath(projectBase, root, relativePath);
|
|
2312
|
+
if (existsSync(candidate)) {
|
|
2313
|
+
allSourcePaths.push(candidate);
|
|
2314
|
+
found = true;
|
|
2315
|
+
break;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
if (!found) {
|
|
2319
|
+
// Fallback to first root for error reporting
|
|
2320
|
+
allSourcePaths.push(resolvePath(projectBase, sourceRootCandidates[0], relativePath));
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
if (allSourcePaths.length === 0) {
|
|
2325
|
+
throw createError({
|
|
2326
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
2327
|
+
message: `Mixin config(s) contain no mixin class entries.`
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
return this.validateMixinBatch({ ...input, mixinConfigPath: undefined, sourcePaths: allSourcePaths });
|
|
2331
|
+
}
|
|
2332
|
+
// Batch mode: delegate to validateMixinBatch when sourcePaths is provided
|
|
2333
|
+
if (input.sourcePaths && input.sourcePaths.length > 0) {
|
|
2334
|
+
return this.validateMixinBatch(input);
|
|
2335
|
+
}
|
|
2336
|
+
let version = input.version.trim();
|
|
1791
2337
|
if (!version) {
|
|
1792
2338
|
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "version must be non-empty." });
|
|
1793
2339
|
}
|
|
1794
|
-
|
|
2340
|
+
// Resolve source from source or sourcePath
|
|
2341
|
+
let source;
|
|
2342
|
+
if (input.sourcePath) {
|
|
2343
|
+
const normalizedSourcePath = normalizePathForHost(input.sourcePath, undefined, "sourcePath");
|
|
2344
|
+
const resolvedSourcePath = isAbsolute(normalizedSourcePath)
|
|
2345
|
+
? normalizedSourcePath
|
|
2346
|
+
: resolvePath(process.cwd(), normalizedSourcePath);
|
|
2347
|
+
try {
|
|
2348
|
+
source = await readFile(resolvedSourcePath, "utf-8");
|
|
2349
|
+
}
|
|
2350
|
+
catch (err) {
|
|
2351
|
+
throw createError({
|
|
2352
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
2353
|
+
message: `Could not read sourcePath "${input.sourcePath}" (resolved to "${resolvedSourcePath}"):` +
|
|
2354
|
+
` ${err instanceof Error ? err.message : String(err)}`
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
else {
|
|
2359
|
+
source = input.source ?? "";
|
|
2360
|
+
}
|
|
1795
2361
|
if (!source.trim()) {
|
|
1796
2362
|
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "source must be non-empty." });
|
|
1797
2363
|
}
|
|
1798
2364
|
const warnings = [];
|
|
1799
|
-
|
|
1800
|
-
|
|
2365
|
+
let mappingAutoDetected = false;
|
|
2366
|
+
// Auto-detect mapping from project config when not explicitly provided (or when preferProjectMapping is set)
|
|
2367
|
+
let detectedMapping;
|
|
2368
|
+
if ((!input.mapping || input.preferProjectMapping) && input.projectPath) {
|
|
2369
|
+
try {
|
|
2370
|
+
const detection = await this.workspaceMappingService.detectCompileMapping({ projectPath: input.projectPath });
|
|
2371
|
+
if (detection.resolved && detection.mappingApplied) {
|
|
2372
|
+
detectedMapping = detection.mappingApplied;
|
|
2373
|
+
mappingAutoDetected = true;
|
|
2374
|
+
warnings.push(`Auto-detected mapping '${detectedMapping}' from project configuration.`);
|
|
2375
|
+
warnings.push(...detection.warnings);
|
|
2376
|
+
}
|
|
2377
|
+
else {
|
|
2378
|
+
warnings.push(...detection.warnings);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
catch {
|
|
2382
|
+
// Detection failed — fall through to default
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
const requestedMapping = normalizeMapping(detectedMapping ?? input.mapping);
|
|
2386
|
+
let mappingApplied = requestedMapping;
|
|
2387
|
+
// preferProjectVersion: detect MC version from gradle.properties
|
|
2388
|
+
if (input.preferProjectVersion && input.projectPath) {
|
|
2389
|
+
const detected = await this.workspaceMappingService.detectProjectMinecraftVersion(input.projectPath);
|
|
2390
|
+
if (detected && detected !== version) {
|
|
2391
|
+
warnings.push(`Overriding version "${version}" with project version "${detected}" from gradle.properties.`);
|
|
2392
|
+
}
|
|
2393
|
+
version = detected ?? version;
|
|
2394
|
+
}
|
|
2395
|
+
// Resolve jar: use Loom cache for non-vanilla scope with projectPath
|
|
2396
|
+
let jarPath;
|
|
2397
|
+
let scopeFallback;
|
|
2398
|
+
if (input.scope && input.scope !== "vanilla" && input.projectPath) {
|
|
2399
|
+
try {
|
|
2400
|
+
const resolved = await this.resolveArtifact({
|
|
2401
|
+
target: { kind: "version", value: version },
|
|
2402
|
+
mapping: requestedMapping,
|
|
2403
|
+
sourcePriority: input.sourcePriority,
|
|
2404
|
+
projectPath: input.projectPath,
|
|
2405
|
+
scope: input.scope,
|
|
2406
|
+
preferProjectVersion: false
|
|
2407
|
+
});
|
|
2408
|
+
jarPath = resolved.binaryJarPath ?? (await this.versionService.resolveVersionJar(version)).jarPath;
|
|
2409
|
+
warnings.push(...resolved.warnings);
|
|
2410
|
+
mappingApplied = resolved.mappingApplied;
|
|
2411
|
+
if (resolved.version) {
|
|
2412
|
+
version = resolved.version;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
catch (scopeErr) {
|
|
2416
|
+
// Scope preflight failed — fall back to vanilla
|
|
2417
|
+
scopeFallback = {
|
|
2418
|
+
requested: input.scope,
|
|
2419
|
+
applied: "vanilla",
|
|
2420
|
+
reason: `Loom cache unavailable: ${scopeErr instanceof Error ? scopeErr.message : String(scopeErr)}`
|
|
2421
|
+
};
|
|
2422
|
+
warnings.push(`Scope "${input.scope}" resolution failed; falling back to vanilla. ${scopeFallback.reason}`);
|
|
2423
|
+
jarPath = (await this.versionService.resolveVersionJar(version)).jarPath;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
else {
|
|
2427
|
+
jarPath = (await this.versionService.resolveVersionJar(version)).jarPath;
|
|
2428
|
+
}
|
|
2429
|
+
// Guard: reject sources jars — they contain Java source, not bytecode
|
|
2430
|
+
if (jarPath.includes("-sources.jar")) {
|
|
2431
|
+
warnings.push(`Resolved jar appears to be a sources jar. Falling back to vanilla client jar.`);
|
|
2432
|
+
jarPath = (await this.versionService.resolveVersionJar(version)).jarPath;
|
|
2433
|
+
scopeFallback = {
|
|
2434
|
+
requested: input.scope ?? "vanilla",
|
|
2435
|
+
applied: "vanilla",
|
|
2436
|
+
reason: "Resolved jar was a sources jar, not a binary class jar."
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
// Health check: probe mapping infrastructure
|
|
2440
|
+
let healthReport;
|
|
2441
|
+
try {
|
|
2442
|
+
const health = await this.mappingService.checkMappingHealth({
|
|
2443
|
+
version,
|
|
2444
|
+
requestedMapping,
|
|
2445
|
+
sourcePriority: input.sourcePriority
|
|
2446
|
+
});
|
|
2447
|
+
const jarAvailable = existsSync(jarPath);
|
|
2448
|
+
healthReport = {
|
|
2449
|
+
jarAvailable,
|
|
2450
|
+
jarPath,
|
|
2451
|
+
mojangMappingsAvailable: health.mojangMappingsAvailable,
|
|
2452
|
+
tinyMappingsAvailable: health.tinyMappingsAvailable,
|
|
2453
|
+
memberRemapAvailable: health.memberRemapAvailable,
|
|
2454
|
+
overallHealthy: jarAvailable && health.mojangMappingsAvailable,
|
|
2455
|
+
degradations: [
|
|
2456
|
+
...(jarAvailable ? [] : ["Game jar not found."]),
|
|
2457
|
+
...health.degradations
|
|
2458
|
+
]
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
catch {
|
|
2462
|
+
// Health check failed — proceed without it
|
|
2463
|
+
}
|
|
1801
2464
|
const parsed = parseMixinSource(source);
|
|
1802
2465
|
const targetMembers = new Map();
|
|
2466
|
+
const mappingFailedTargets = new Set();
|
|
2467
|
+
const remapFailedMembers = new Map();
|
|
2468
|
+
const signatureFailedTargets = new Set();
|
|
2469
|
+
const symbolExistsButSignatureFailed = new Set();
|
|
2470
|
+
const resolutionTrace = input.explain ? [] : undefined;
|
|
1803
2471
|
for (const target of parsed.targets) {
|
|
1804
|
-
|
|
2472
|
+
// Bug 1 fix: resolve simple names via imports
|
|
2473
|
+
let resolvedClassName = target.className;
|
|
2474
|
+
if (!resolvedClassName.includes(".")) {
|
|
2475
|
+
// Simple name — look up in imports
|
|
2476
|
+
const fqcn = parsed.imports.get(resolvedClassName);
|
|
2477
|
+
if (fqcn) {
|
|
2478
|
+
resolvedClassName = fqcn;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
else {
|
|
2482
|
+
// Might be inner class like Foo.Bar where Foo is imported
|
|
2483
|
+
const segments = resolvedClassName.split(".");
|
|
2484
|
+
const firstSegment = segments[0];
|
|
2485
|
+
if (firstSegment && /^[A-Z]/.test(firstSegment)) {
|
|
2486
|
+
const outerFqcn = parsed.imports.get(firstSegment);
|
|
2487
|
+
if (outerFqcn) {
|
|
2488
|
+
resolvedClassName = outerFqcn + "$" + segments.slice(1).join("$");
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
let officialName = resolvedClassName;
|
|
1805
2493
|
if (requestedMapping !== "official") {
|
|
1806
2494
|
try {
|
|
1807
2495
|
const mapped = await this.mappingService.findMapping({
|
|
1808
2496
|
version,
|
|
1809
2497
|
kind: "class",
|
|
1810
|
-
name:
|
|
2498
|
+
name: resolvedClassName,
|
|
1811
2499
|
sourceMapping: requestedMapping,
|
|
1812
2500
|
targetMapping: "official",
|
|
1813
2501
|
sourcePriority: input.sourcePriority
|
|
1814
2502
|
});
|
|
1815
2503
|
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
1816
2504
|
officialName = mapped.resolvedSymbol.name;
|
|
2505
|
+
resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: officialName, success: true });
|
|
1817
2506
|
}
|
|
1818
2507
|
else {
|
|
1819
|
-
warnings.push(`Could not map class "${
|
|
2508
|
+
warnings.push(`Could not map class "${resolvedClassName}" from ${requestedMapping} to official; using "${officialName}" for lookup.`);
|
|
2509
|
+
mappingFailedTargets.add(target.className);
|
|
2510
|
+
resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: officialName, success: false, detail: "No mapping found" });
|
|
1820
2511
|
}
|
|
1821
2512
|
}
|
|
1822
|
-
catch {
|
|
1823
|
-
warnings.push(`Mapping lookup failed for class "${
|
|
2513
|
+
catch (mapErr) {
|
|
2514
|
+
warnings.push(`Mapping lookup failed for class "${resolvedClassName}"; using "${officialName}" for lookup.`);
|
|
2515
|
+
mappingFailedTargets.add(target.className);
|
|
2516
|
+
resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: officialName, success: false, detail: mapErr instanceof Error ? mapErr.message : String(mapErr) });
|
|
1824
2517
|
}
|
|
1825
2518
|
}
|
|
1826
2519
|
try {
|
|
@@ -1830,18 +2523,288 @@ export class SourceService {
|
|
|
1830
2523
|
access: "all"
|
|
1831
2524
|
});
|
|
1832
2525
|
warnings.push(...sig.warnings);
|
|
2526
|
+
resolutionTrace?.push({ target: target.className, step: "signature", input: officialName, output: `${sig.methods.length} methods, ${sig.fields.length} fields`, success: true });
|
|
2527
|
+
// Bug 2 fix: remap signature members to requested mapping
|
|
2528
|
+
let constructors = sig.constructors;
|
|
2529
|
+
let methods = sig.methods;
|
|
2530
|
+
let fields = sig.fields;
|
|
2531
|
+
if (requestedMapping !== "official") {
|
|
2532
|
+
try {
|
|
2533
|
+
const [ctorResult, methodResult, fieldResult] = await Promise.all([
|
|
2534
|
+
this.remapSignatureMembers(sig.constructors, "method", version, requestedMapping, input.sourcePriority, warnings),
|
|
2535
|
+
this.remapSignatureMembers(sig.methods, "method", version, requestedMapping, input.sourcePriority, warnings),
|
|
2536
|
+
this.remapSignatureMembers(sig.fields, "field", version, requestedMapping, input.sourcePriority, warnings)
|
|
2537
|
+
]);
|
|
2538
|
+
constructors = ctorResult.members;
|
|
2539
|
+
methods = methodResult.members;
|
|
2540
|
+
fields = fieldResult.members;
|
|
2541
|
+
// Collect remap-failed member names for this target
|
|
2542
|
+
const targetFailed = new Set();
|
|
2543
|
+
for (const n of ctorResult.failedNames)
|
|
2544
|
+
targetFailed.add(n);
|
|
2545
|
+
for (const n of methodResult.failedNames)
|
|
2546
|
+
targetFailed.add(n);
|
|
2547
|
+
for (const n of fieldResult.failedNames)
|
|
2548
|
+
targetFailed.add(n);
|
|
2549
|
+
if (targetFailed.size > 0) {
|
|
2550
|
+
remapFailedMembers.set(target.className, targetFailed);
|
|
2551
|
+
resolutionTrace?.push({ target: target.className, step: "remap", input: `${targetFailed.size} members`, output: "failed", success: false });
|
|
2552
|
+
}
|
|
2553
|
+
else {
|
|
2554
|
+
resolutionTrace?.push({ target: target.className, step: "remap", input: `${methods.length + fields.length} members`, output: "remapped", success: true });
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
catch (remapErr) {
|
|
2558
|
+
warnings.push(`Member remapping failed for "${resolvedClassName}"; falling back to official names. Member names shown may be in official (obfuscated) namespace.`);
|
|
2559
|
+
mappingApplied = "official";
|
|
2560
|
+
resolutionTrace?.push({ target: target.className, step: "remap", input: resolvedClassName, output: "official fallback", success: false, detail: remapErr instanceof Error ? remapErr.message : String(remapErr) });
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
1833
2563
|
targetMembers.set(target.className, {
|
|
1834
2564
|
className: target.className,
|
|
1835
|
-
constructors
|
|
1836
|
-
methods
|
|
1837
|
-
fields
|
|
2565
|
+
constructors,
|
|
2566
|
+
methods,
|
|
2567
|
+
fields
|
|
1838
2568
|
});
|
|
1839
2569
|
}
|
|
1840
|
-
catch {
|
|
1841
|
-
warnings.push(`Could not load signature for class "${officialName}".`);
|
|
2570
|
+
catch (sigErr) {
|
|
2571
|
+
warnings.push(`Could not load signature for class "${resolvedClassName}" (official: "${officialName}").`);
|
|
2572
|
+
signatureFailedTargets.add(target.className);
|
|
2573
|
+
resolutionTrace?.push({ target: target.className, step: "signature", input: officialName, output: "CLASS_NOT_FOUND", success: false, detail: sigErr instanceof Error ? sigErr.message : String(sigErr) });
|
|
2574
|
+
// Fallback: check if the symbol exists in the mapping graph even though getSignature failed
|
|
2575
|
+
try {
|
|
2576
|
+
const existenceCheck = await this.mappingService.checkSymbolExists({
|
|
2577
|
+
version, kind: "class", name: resolvedClassName,
|
|
2578
|
+
sourceMapping: requestedMapping, nameMode: "auto"
|
|
2579
|
+
});
|
|
2580
|
+
if (existenceCheck.resolved) {
|
|
2581
|
+
signatureFailedTargets.delete(target.className);
|
|
2582
|
+
symbolExistsButSignatureFailed.add(target.className);
|
|
2583
|
+
resolutionTrace?.push({ target: target.className, step: "fallback-check", input: resolvedClassName, output: "exists in mapping graph", success: true });
|
|
2584
|
+
}
|
|
2585
|
+
else {
|
|
2586
|
+
resolutionTrace?.push({ target: target.className, step: "fallback-check", input: resolvedClassName, output: "not found", success: false });
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
catch {
|
|
2590
|
+
// Fallback check failed — keep as signatureFailedTarget
|
|
2591
|
+
resolutionTrace?.push({ target: target.className, step: "fallback-check", input: resolvedClassName, output: "check failed", success: false });
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
// Fix toolHealth accuracy: reflect actual failures after target resolution
|
|
2596
|
+
if (healthReport) {
|
|
2597
|
+
const hasFailures = signatureFailedTargets.size > 0 || mappingFailedTargets.size > 0;
|
|
2598
|
+
if (hasFailures && healthReport.overallHealthy) {
|
|
2599
|
+
healthReport.overallHealthy = false;
|
|
2600
|
+
healthReport.degradations.push(`${mappingFailedTargets.size} mapping failure(s), ${signatureFailedTargets.size} signature failure(s).`);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
const resolutionNotes = [];
|
|
2604
|
+
if (requestedMapping !== mappingApplied) {
|
|
2605
|
+
resolutionNotes.push(`Mapping fallback: requested "${requestedMapping}" but applied "${mappingApplied}" due to remapping failure.`);
|
|
2606
|
+
}
|
|
2607
|
+
// Count remap failures from warnings
|
|
2608
|
+
const REMAP_WARNING_RE = /^(?:Could not remap|Remap failed for)\b/;
|
|
2609
|
+
const remapFailures = warnings.filter((w) => REMAP_WARNING_RE.test(w)).length;
|
|
2610
|
+
// Determine confidence level
|
|
2611
|
+
let confidence = "definite";
|
|
2612
|
+
if (requestedMapping !== mappingApplied) {
|
|
2613
|
+
confidence = "uncertain";
|
|
2614
|
+
}
|
|
2615
|
+
else if (remapFailures > 0) {
|
|
2616
|
+
confidence = "likely";
|
|
2617
|
+
}
|
|
2618
|
+
// Build mapping chain description
|
|
2619
|
+
const mappingChain = [];
|
|
2620
|
+
if (requestedMapping !== "official") {
|
|
2621
|
+
mappingChain.push(`${requestedMapping} → official`);
|
|
2622
|
+
if (mappingApplied !== requestedMapping) {
|
|
2623
|
+
mappingChain.push(`fallback to ${mappingApplied}`);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
const provenance = {
|
|
2627
|
+
version,
|
|
2628
|
+
jarPath,
|
|
2629
|
+
requestedMapping,
|
|
2630
|
+
mappingApplied,
|
|
2631
|
+
resolutionNotes: resolutionNotes.length > 0 ? resolutionNotes : undefined,
|
|
2632
|
+
jarType: scopeFallback ? "vanilla-client" : (input.scope && input.scope !== "vanilla" && input.projectPath) ? "merged" : "vanilla-client",
|
|
2633
|
+
mappingChain: mappingChain.length > 0 ? mappingChain : undefined,
|
|
2634
|
+
remapFailures: remapFailures > 0 ? remapFailures : undefined,
|
|
2635
|
+
mappingAutoDetected: mappingAutoDetected || undefined,
|
|
2636
|
+
scopeFallback,
|
|
2637
|
+
resolutionTrace: resolutionTrace && resolutionTrace.length > 0 ? resolutionTrace : undefined
|
|
2638
|
+
};
|
|
2639
|
+
const result = validateParsedMixin(parsed, targetMembers, warnings, provenance, confidence, mappingFailedTargets, input.explain, remapFailedMembers, signatureFailedTargets, input.explain ? { scope: input.scope, sourcePriority: input.sourcePriority, projectPath: input.projectPath, mapping: requestedMapping } : undefined, input.warningMode, healthReport, symbolExistsButSignatureFailed.size > 0 ? symbolExistsButSignatureFailed : undefined);
|
|
2640
|
+
// Apply minSeverity / hideUncertain filters
|
|
2641
|
+
const minSeverity = input.minSeverity ?? "all";
|
|
2642
|
+
const hideUncertain = input.hideUncertain ?? false;
|
|
2643
|
+
if (minSeverity !== "all" || hideUncertain) {
|
|
2644
|
+
const unfilteredSummary = { ...result.summary };
|
|
2645
|
+
let filtered = result.issues;
|
|
2646
|
+
if (minSeverity === "error") {
|
|
2647
|
+
filtered = filtered.filter((i) => i.severity === "error");
|
|
2648
|
+
}
|
|
2649
|
+
else if (minSeverity === "warning") {
|
|
2650
|
+
filtered = filtered.filter((i) => i.severity === "error" || i.severity === "warning");
|
|
2651
|
+
}
|
|
2652
|
+
if (hideUncertain) {
|
|
2653
|
+
filtered = filtered.filter((i) => i.confidence !== "uncertain");
|
|
2654
|
+
}
|
|
2655
|
+
const filteredErrors = filtered.filter((i) => i.severity === "error").length;
|
|
2656
|
+
const filteredWarnings = filtered.filter((i) => i.severity === "warning").length;
|
|
2657
|
+
const filteredDefiniteErrors = filtered.filter((i) => i.severity === "error" && i.confidence !== "uncertain").length;
|
|
2658
|
+
const filteredUncertainErrors = filtered.filter((i) => i.severity === "error" && i.confidence === "uncertain").length;
|
|
2659
|
+
const filteredResolutionErrors = filtered.filter((i) => i.resolutionPath != null).length;
|
|
2660
|
+
const filteredParseWarnings = filtered.filter((i) => i.category === "parse").length;
|
|
2661
|
+
result.issues = filtered;
|
|
2662
|
+
result.summary = {
|
|
2663
|
+
...result.summary,
|
|
2664
|
+
errors: filteredErrors,
|
|
2665
|
+
warnings: filteredWarnings,
|
|
2666
|
+
definiteErrors: filteredDefiniteErrors,
|
|
2667
|
+
uncertainErrors: filteredUncertainErrors,
|
|
2668
|
+
resolutionErrors: filteredResolutionErrors,
|
|
2669
|
+
parseWarnings: filteredParseWarnings
|
|
2670
|
+
};
|
|
2671
|
+
result.unfilteredSummary = unfilteredSummary;
|
|
2672
|
+
result.valid = filteredDefiniteErrors === 0;
|
|
2673
|
+
}
|
|
2674
|
+
// Apply warningCategoryFilter
|
|
2675
|
+
if (input.warningCategoryFilter && input.warningCategoryFilter.length > 0) {
|
|
2676
|
+
const allowedCategories = new Set(input.warningCategoryFilter);
|
|
2677
|
+
result.issues = result.issues.filter((i) => i.category && allowedCategories.has(i.category));
|
|
2678
|
+
if (result.structuredWarnings) {
|
|
2679
|
+
result.structuredWarnings = result.structuredWarnings.filter((sw) => sw.category && allowedCategories.has(sw.category));
|
|
2680
|
+
if (result.structuredWarnings.length === 0)
|
|
2681
|
+
result.structuredWarnings = undefined;
|
|
2682
|
+
}
|
|
2683
|
+
// Re-compute summary after category filter
|
|
2684
|
+
const catErrors = result.issues.filter((i) => i.severity === "error").length;
|
|
2685
|
+
const catWarnings = result.issues.filter((i) => i.severity === "warning").length;
|
|
2686
|
+
const catDefiniteErrors = result.issues.filter((i) => i.severity === "error" && i.confidence !== "uncertain").length;
|
|
2687
|
+
result.summary = {
|
|
2688
|
+
...result.summary,
|
|
2689
|
+
errors: catErrors,
|
|
2690
|
+
warnings: catWarnings,
|
|
2691
|
+
definiteErrors: catDefiniteErrors,
|
|
2692
|
+
uncertainErrors: result.issues.filter((i) => i.severity === "error" && i.confidence === "uncertain").length,
|
|
2693
|
+
resolutionErrors: result.issues.filter((i) => i.resolutionPath != null).length,
|
|
2694
|
+
parseWarnings: result.issues.filter((i) => i.category === "parse").length
|
|
2695
|
+
};
|
|
2696
|
+
result.valid = catDefiniteErrors === 0;
|
|
2697
|
+
}
|
|
2698
|
+
// Apply treatInfoAsWarning filter
|
|
2699
|
+
if (input.treatInfoAsWarning === false && result.structuredWarnings) {
|
|
2700
|
+
result.structuredWarnings = result.structuredWarnings.filter((sw) => sw.severity !== "info");
|
|
2701
|
+
if (result.structuredWarnings.length === 0)
|
|
2702
|
+
result.structuredWarnings = undefined;
|
|
2703
|
+
}
|
|
2704
|
+
// Apply compact report mode
|
|
2705
|
+
if (input.reportMode === "compact") {
|
|
2706
|
+
result.resolvedMembers = undefined;
|
|
2707
|
+
result.structuredWarnings = undefined;
|
|
2708
|
+
result.aggregatedWarnings = undefined;
|
|
2709
|
+
result.toolHealth = undefined;
|
|
2710
|
+
if (result.provenance) {
|
|
2711
|
+
result.provenance.resolutionTrace = undefined;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
return result;
|
|
2715
|
+
}
|
|
2716
|
+
async validateMixinBatch(input) {
|
|
2717
|
+
const paths = input.sourcePaths;
|
|
2718
|
+
const results = [];
|
|
2719
|
+
let validCount = 0;
|
|
2720
|
+
let invalidCount = 0;
|
|
2721
|
+
let errorCount = 0;
|
|
2722
|
+
// P5: default warningMode to "aggregated" in batch mode
|
|
2723
|
+
const batchWarningMode = input.warningMode ?? "aggregated";
|
|
2724
|
+
for (const sp of paths) {
|
|
2725
|
+
try {
|
|
2726
|
+
const singleResult = await this.validateMixin({
|
|
2727
|
+
...input,
|
|
2728
|
+
sourcePaths: undefined,
|
|
2729
|
+
source: undefined,
|
|
2730
|
+
sourcePath: sp,
|
|
2731
|
+
warningMode: batchWarningMode
|
|
2732
|
+
});
|
|
2733
|
+
results.push({ sourcePath: sp, result: singleResult });
|
|
2734
|
+
if (singleResult.valid) {
|
|
2735
|
+
validCount++;
|
|
2736
|
+
}
|
|
2737
|
+
else {
|
|
2738
|
+
invalidCount++;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
catch (err) {
|
|
2742
|
+
results.push({
|
|
2743
|
+
sourcePath: sp,
|
|
2744
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2745
|
+
});
|
|
2746
|
+
errorCount++;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
// Build issueSummary: aggregate issues across all results by (kind, confidence, category)
|
|
2750
|
+
const issueGroupMap = new Map();
|
|
2751
|
+
for (const r of results) {
|
|
2752
|
+
if (!r.result)
|
|
2753
|
+
continue;
|
|
2754
|
+
for (const issue of r.result.issues) {
|
|
2755
|
+
const key = `${issue.kind}\0${issue.confidence ?? "unknown"}\0${issue.category ?? "validation"}`;
|
|
2756
|
+
const existing = issueGroupMap.get(key);
|
|
2757
|
+
if (existing) {
|
|
2758
|
+
existing.count++;
|
|
2759
|
+
if (existing.sampleTargets.length < 3) {
|
|
2760
|
+
existing.sampleTargets.push(issue.target);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
else {
|
|
2764
|
+
issueGroupMap.set(key, {
|
|
2765
|
+
kind: issue.kind,
|
|
2766
|
+
confidence: issue.confidence ?? "unknown",
|
|
2767
|
+
category: issue.category ?? "validation",
|
|
2768
|
+
count: 1,
|
|
2769
|
+
sampleTargets: [issue.target]
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
1842
2772
|
}
|
|
1843
2773
|
}
|
|
1844
|
-
|
|
2774
|
+
const issueSummary = issueGroupMap.size > 0
|
|
2775
|
+
? [...issueGroupMap.values()]
|
|
2776
|
+
: undefined;
|
|
2777
|
+
// Aggregate validation-level errors/warnings across all results
|
|
2778
|
+
let totalValidationErrors = 0;
|
|
2779
|
+
let totalValidationWarnings = 0;
|
|
2780
|
+
for (const r of results) {
|
|
2781
|
+
if (r.result) {
|
|
2782
|
+
totalValidationErrors += r.result.summary.errors;
|
|
2783
|
+
totalValidationWarnings += r.result.summary.warnings;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
// Extract shared toolHealth from first result (all share same version/mapping)
|
|
2787
|
+
const sharedHealth = results.find((r) => r.result?.toolHealth)?.result?.toolHealth;
|
|
2788
|
+
// Batch confidenceScore = min of all individual scores
|
|
2789
|
+
const scores = results
|
|
2790
|
+
.map((r) => r.result?.confidenceScore)
|
|
2791
|
+
.filter((s) => s != null);
|
|
2792
|
+
const batchConfidenceScore = scores.length > 0 ? Math.min(...scores) : undefined;
|
|
2793
|
+
return {
|
|
2794
|
+
results,
|
|
2795
|
+
summary: {
|
|
2796
|
+
total: paths.length,
|
|
2797
|
+
valid: validCount,
|
|
2798
|
+
invalid: invalidCount,
|
|
2799
|
+
errors: errorCount,
|
|
2800
|
+
processingErrors: errorCount,
|
|
2801
|
+
totalValidationErrors,
|
|
2802
|
+
totalValidationWarnings,
|
|
2803
|
+
confidenceScore: batchConfidenceScore
|
|
2804
|
+
},
|
|
2805
|
+
issueSummary,
|
|
2806
|
+
toolHealth: sharedHealth
|
|
2807
|
+
};
|
|
1845
2808
|
}
|
|
1846
2809
|
async validateAccessWidener(input) {
|
|
1847
2810
|
const version = input.version.trim();
|
|
@@ -2344,6 +3307,33 @@ export class SourceService {
|
|
|
2344
3307
|
// Contains matches need more candidates
|
|
2345
3308
|
return base;
|
|
2346
3309
|
}
|
|
3310
|
+
extractClassMetadata(filePath, content) {
|
|
3311
|
+
const lines = content.split(/\r?\n/);
|
|
3312
|
+
const symbols = extractSymbolsFromSource(filePath, content);
|
|
3313
|
+
const outputParts = [];
|
|
3314
|
+
// Include package + import header (lines before first symbol declaration)
|
|
3315
|
+
const firstSymbolLine = symbols.length > 0 ? symbols[0].line : lines.length + 1;
|
|
3316
|
+
for (let i = 0; i < Math.min(firstSymbolLine - 1, lines.length); i++) {
|
|
3317
|
+
const line = lines[i];
|
|
3318
|
+
const trimmed = line.trim();
|
|
3319
|
+
if (trimmed.startsWith("package ") || trimmed.startsWith("import ") || trimmed === "") {
|
|
3320
|
+
outputParts.push(line);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
// Add each symbol's declaration line
|
|
3324
|
+
for (const symbol of symbols) {
|
|
3325
|
+
const lineIndex = symbol.line - 1;
|
|
3326
|
+
if (lineIndex >= 0 && lineIndex < lines.length) {
|
|
3327
|
+
const prefix = symbol.symbolKind === "class" || symbol.symbolKind === "interface" ||
|
|
3328
|
+
symbol.symbolKind === "enum" || symbol.symbolKind === "record"
|
|
3329
|
+
? `\n// [${symbol.symbolKind}] line ${symbol.line}`
|
|
3330
|
+
: `// [${symbol.symbolKind}] line ${symbol.line}`;
|
|
3331
|
+
outputParts.push(prefix);
|
|
3332
|
+
outputParts.push(lines[lineIndex]);
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
return outputParts.join("\n");
|
|
3336
|
+
}
|
|
2347
3337
|
resolveClassFilePath(artifactId, className) {
|
|
2348
3338
|
const normalizedClassName = className.trim();
|
|
2349
3339
|
const classPath = classNameToClassPath(normalizedClassName);
|
|
@@ -2619,8 +3609,9 @@ export class SourceService {
|
|
|
2619
3609
|
};
|
|
2620
3610
|
}
|
|
2621
3611
|
async remapSignatureMembers(members, kind, version, mapping, sourcePriority, warnings) {
|
|
3612
|
+
const failedNames = new Set();
|
|
2622
3613
|
if (mapping === "official") {
|
|
2623
|
-
return members;
|
|
3614
|
+
return { members, failedNames };
|
|
2624
3615
|
}
|
|
2625
3616
|
// Build deduplicated lookup tables for member names and owner FQNs
|
|
2626
3617
|
const memberKeyToRemapped = new Map();
|
|
@@ -2634,60 +3625,83 @@ export class SourceService {
|
|
|
2634
3625
|
ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = official FQN
|
|
2635
3626
|
}
|
|
2636
3627
|
}
|
|
2637
|
-
// Remap
|
|
2638
|
-
const
|
|
2639
|
-
await Promise.all(
|
|
2640
|
-
const [ownerFqn, name, descriptor] = key.split("\0");
|
|
3628
|
+
// Phase 1: Remap owner FQNs first (needed for member disambiguation)
|
|
3629
|
+
const ownerEntries = [...ownerToRemapped.entries()];
|
|
3630
|
+
await Promise.all(ownerEntries.map(async ([officialFqn]) => {
|
|
2641
3631
|
try {
|
|
2642
3632
|
const mapped = await this.mappingService.findMapping({
|
|
2643
3633
|
version,
|
|
2644
|
-
kind,
|
|
2645
|
-
name,
|
|
2646
|
-
owner: ownerFqn,
|
|
2647
|
-
descriptor: kind === "method" ? descriptor : undefined,
|
|
3634
|
+
kind: "class",
|
|
3635
|
+
name: officialFqn,
|
|
2648
3636
|
sourceMapping: "official",
|
|
2649
3637
|
targetMapping: mapping,
|
|
2650
3638
|
sourcePriority
|
|
2651
3639
|
});
|
|
2652
3640
|
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
2653
|
-
|
|
2654
|
-
}
|
|
2655
|
-
else {
|
|
2656
|
-
warnings.push(`Could not remap ${kind} "${name}" to ${mapping}.`);
|
|
3641
|
+
ownerToRemapped.set(officialFqn, mapped.resolvedSymbol.name);
|
|
2657
3642
|
}
|
|
2658
3643
|
}
|
|
2659
3644
|
catch {
|
|
2660
|
-
|
|
3645
|
+
// keep official FQN as fallback
|
|
2661
3646
|
}
|
|
2662
3647
|
}));
|
|
2663
|
-
// Remap
|
|
2664
|
-
const
|
|
2665
|
-
await Promise.all(
|
|
3648
|
+
// Phase 2: Remap member names using resolved owners for disambiguation
|
|
3649
|
+
const memberEntries = [...memberKeyToRemapped.entries()];
|
|
3650
|
+
await Promise.all(memberEntries.map(async ([key, _officialName]) => {
|
|
3651
|
+
const [ownerFqn, name, descriptor] = key.split("\0");
|
|
2666
3652
|
try {
|
|
3653
|
+
const targetOwner = ownerToRemapped.get(ownerFqn) ?? ownerFqn;
|
|
2667
3654
|
const mapped = await this.mappingService.findMapping({
|
|
2668
3655
|
version,
|
|
2669
|
-
kind
|
|
2670
|
-
name
|
|
3656
|
+
kind,
|
|
3657
|
+
name,
|
|
3658
|
+
owner: ownerFqn,
|
|
3659
|
+
descriptor: kind === "method" ? descriptor : undefined,
|
|
2671
3660
|
sourceMapping: "official",
|
|
2672
3661
|
targetMapping: mapping,
|
|
2673
|
-
sourcePriority
|
|
3662
|
+
sourcePriority,
|
|
3663
|
+
disambiguation: { ownerHint: targetOwner }
|
|
2674
3664
|
});
|
|
2675
3665
|
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
2676
|
-
|
|
3666
|
+
memberKeyToRemapped.set(key, mapped.resolvedSymbol.name);
|
|
3667
|
+
}
|
|
3668
|
+
else if (mapped.status === "ambiguous" && mapped.candidates && mapped.candidates.length > 0) {
|
|
3669
|
+
// Disambiguate: filter by target owner and pick the best candidate
|
|
3670
|
+
const ownerMatched = mapped.candidates.filter((c) => c.owner === targetOwner);
|
|
3671
|
+
const best = ownerMatched.length > 0 ? ownerMatched : mapped.candidates;
|
|
3672
|
+
if (best.length > 0) {
|
|
3673
|
+
memberKeyToRemapped.set(key, best[0].name);
|
|
3674
|
+
// Only mark as failed if the best candidate is not a high-confidence match
|
|
3675
|
+
if (best[0].confidence < 0.9) {
|
|
3676
|
+
failedNames.add(name);
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
else {
|
|
3680
|
+
warnings.push(`Could not remap ${kind} "${name}" to ${mapping}.`);
|
|
3681
|
+
failedNames.add(name);
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
else {
|
|
3685
|
+
warnings.push(`Could not remap ${kind} "${name}" to ${mapping}.`);
|
|
3686
|
+
failedNames.add(name);
|
|
2677
3687
|
}
|
|
2678
3688
|
}
|
|
2679
3689
|
catch {
|
|
2680
|
-
|
|
3690
|
+
warnings.push(`Remap failed for ${kind} "${name}".`);
|
|
3691
|
+
failedNames.add(name);
|
|
2681
3692
|
}
|
|
2682
3693
|
}));
|
|
2683
|
-
return
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
3694
|
+
return {
|
|
3695
|
+
members: members.map((member) => {
|
|
3696
|
+
const memberKey = `${member.ownerFqn}\0${member.name}\0${member.jvmDescriptor}`;
|
|
3697
|
+
return {
|
|
3698
|
+
...member,
|
|
3699
|
+
name: memberKeyToRemapped.get(memberKey) ?? member.name,
|
|
3700
|
+
ownerFqn: ownerToRemapped.get(member.ownerFqn) ?? member.ownerFqn
|
|
3701
|
+
};
|
|
3702
|
+
}),
|
|
3703
|
+
failedNames
|
|
3704
|
+
};
|
|
2691
3705
|
}
|
|
2692
3706
|
fallbackArtifactSignature(artifactId) {
|
|
2693
3707
|
return createHash("sha256").update(artifactId).digest("hex");
|
|
@@ -2798,6 +3812,25 @@ export class SourceService {
|
|
|
2798
3812
|
contentHash: createHash("sha256").update(entry.content).digest("hex")
|
|
2799
3813
|
}));
|
|
2800
3814
|
}
|
|
3815
|
+
catch (caughtError) {
|
|
3816
|
+
if (isAppError(caughtError) && caughtError.code === ERROR_CODES.DECOMPILER_FAILED) {
|
|
3817
|
+
throw createError({
|
|
3818
|
+
code: ERROR_CODES.DECOMPILER_FAILED,
|
|
3819
|
+
message: caughtError.message,
|
|
3820
|
+
details: {
|
|
3821
|
+
...(caughtError.details ?? {}),
|
|
3822
|
+
artifactId: resolved.artifactId,
|
|
3823
|
+
binaryJarPath: resolved.binaryJarPath,
|
|
3824
|
+
producedJavaCount: typeof caughtError.details?.producedJavaCount === "number"
|
|
3825
|
+
? caughtError.details.producedJavaCount
|
|
3826
|
+
: 0,
|
|
3827
|
+
nextAction: "Verify Java runtime and Vineflower availability, then retry. If available, prefer source-backed artifacts.",
|
|
3828
|
+
recommendedCommand: "echo $MCP_VINEFLOWER_JAR_PATH"
|
|
3829
|
+
}
|
|
3830
|
+
});
|
|
3831
|
+
}
|
|
3832
|
+
throw caughtError;
|
|
3833
|
+
}
|
|
2801
3834
|
finally {
|
|
2802
3835
|
this.metrics.recordDuration("decompile_duration_ms", Date.now() - decompileStartedAt);
|
|
2803
3836
|
}
|
|
@@ -2806,7 +3839,11 @@ export class SourceService {
|
|
|
2806
3839
|
throw createError({
|
|
2807
3840
|
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
2808
3841
|
message: "No source artifact available.",
|
|
2809
|
-
details: {
|
|
3842
|
+
details: {
|
|
3843
|
+
artifactId: resolved.artifactId,
|
|
3844
|
+
nextAction: "Use list-artifact-files to inspect the artifact's contents.",
|
|
3845
|
+
suggestedCall: { tool: "list-artifact-files", params: { artifactId: resolved.artifactId } }
|
|
3846
|
+
}
|
|
2810
3847
|
});
|
|
2811
3848
|
}
|
|
2812
3849
|
const symbols = [];
|
|
@@ -2840,7 +3877,11 @@ export class SourceService {
|
|
|
2840
3877
|
throw createError({
|
|
2841
3878
|
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
2842
3879
|
message: "Artifact not found. Resolve context first.",
|
|
2843
|
-
details: {
|
|
3880
|
+
details: {
|
|
3881
|
+
artifactId,
|
|
3882
|
+
nextAction: "Use resolve-artifact to resolve a source artifact first.",
|
|
3883
|
+
suggestedCall: { tool: "resolve-artifact", params: { targetKind: "version", targetValue: "latest" } }
|
|
3884
|
+
}
|
|
2844
3885
|
});
|
|
2845
3886
|
}
|
|
2846
3887
|
return artifact;
|