@adhisang/minecraft-modding-mcp 1.1.0 → 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 +58 -0
- package/README.md +88 -17
- package/dist/cli.js +1 -16
- package/dist/compat-stdio-transport.d.ts +27 -0
- package/dist/compat-stdio-transport.js +217 -0
- 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 +169 -25
- 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 +1211 -120
- 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) {
|
|
@@ -258,11 +337,33 @@ function canUseIndexedSearchPath(indexedSearchEnabled, intent, match, _scope) {
|
|
|
258
337
|
return true;
|
|
259
338
|
}
|
|
260
339
|
function buildGlobRegex(pattern) {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
340
|
+
const REGEX_META = /[-/\\^$+.()|[\]{}]/;
|
|
341
|
+
let result = "";
|
|
342
|
+
let i = 0;
|
|
343
|
+
while (i < pattern.length) {
|
|
344
|
+
const ch = pattern[i];
|
|
345
|
+
if (ch === "*" && pattern[i + 1] === "*") {
|
|
346
|
+
result += ".*";
|
|
347
|
+
i += 2;
|
|
348
|
+
if (pattern[i] === "/") {
|
|
349
|
+
result += "(?:/)?";
|
|
350
|
+
i += 1;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (ch === "*") {
|
|
354
|
+
result += "[^/]*";
|
|
355
|
+
i += 1;
|
|
356
|
+
}
|
|
357
|
+
else if (ch === "?") {
|
|
358
|
+
result += "[^/]";
|
|
359
|
+
i += 1;
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
result += REGEX_META.test(ch) ? `\\${ch}` : ch;
|
|
363
|
+
i += 1;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return new RegExp(`^${result}$`);
|
|
266
367
|
}
|
|
267
368
|
function globToSqlLike(pattern) {
|
|
268
369
|
let result = "";
|
|
@@ -283,6 +384,27 @@ function globToSqlLike(pattern) {
|
|
|
283
384
|
}
|
|
284
385
|
return result;
|
|
285
386
|
}
|
|
387
|
+
function isPackageCompatible(filePath, classPath) {
|
|
388
|
+
const lastSlash = classPath.lastIndexOf("/");
|
|
389
|
+
if (lastSlash < 0)
|
|
390
|
+
return true;
|
|
391
|
+
const expectedPrefix = classPath.slice(0, lastSlash + 1);
|
|
392
|
+
return filePath.startsWith(expectedPrefix);
|
|
393
|
+
}
|
|
394
|
+
function classNameToClassPath(className) {
|
|
395
|
+
const normalized = normalizePathStyle(className.trim()).replace(/\//g, ".");
|
|
396
|
+
const segments = normalized.split(".").filter((segment) => segment.length > 0);
|
|
397
|
+
if (segments.length === 0) {
|
|
398
|
+
return "";
|
|
399
|
+
}
|
|
400
|
+
const firstTypeSegment = segments.findIndex((segment) => /^[A-Z_$]/.test(segment));
|
|
401
|
+
if (firstTypeSegment < 0) {
|
|
402
|
+
return segments.join("/");
|
|
403
|
+
}
|
|
404
|
+
const packagePath = segments.slice(0, firstTypeSegment).join("/");
|
|
405
|
+
const typePath = segments.slice(firstTypeSegment).join("$");
|
|
406
|
+
return packagePath ? `${packagePath}/${typePath}` : typePath;
|
|
407
|
+
}
|
|
286
408
|
function checkPackagePrefix(filePath, packagePrefix) {
|
|
287
409
|
if (!packagePrefix) {
|
|
288
410
|
return true;
|
|
@@ -304,6 +426,7 @@ function buildSearchCursorContext(input) {
|
|
|
304
426
|
query: input.query,
|
|
305
427
|
intent: input.intent,
|
|
306
428
|
match: input.match,
|
|
429
|
+
queryMode: input.queryMode,
|
|
307
430
|
includeDefinition: input.includeDefinition,
|
|
308
431
|
packagePrefix: input.scope?.packagePrefix ?? "",
|
|
309
432
|
fileGlob: input.scope?.fileGlob ?? "",
|
|
@@ -479,11 +602,95 @@ export class SourceService {
|
|
|
479
602
|
this.symbolsRepo = new SymbolsRepo(this.db);
|
|
480
603
|
this.refreshCacheMetrics();
|
|
481
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
|
+
}
|
|
482
680
|
async resolveArtifact(input) {
|
|
483
681
|
const kind = input.target.kind;
|
|
484
|
-
|
|
682
|
+
let value = input.target.value?.trim();
|
|
485
683
|
const mapping = normalizeMapping(input.mapping);
|
|
684
|
+
const scope = input.scope;
|
|
486
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
|
+
}
|
|
487
694
|
if (!value) {
|
|
488
695
|
throw createError({
|
|
489
696
|
code: ERROR_CODES.INVALID_INPUT,
|
|
@@ -509,6 +716,7 @@ export class SourceService {
|
|
|
509
716
|
try {
|
|
510
717
|
let resolvedTarget = { kind, value };
|
|
511
718
|
let resolvedVersion;
|
|
719
|
+
let versionSourceDiscovery;
|
|
512
720
|
if (kind === "version") {
|
|
513
721
|
const versionJar = await this.versionService.resolveVersionJar(value);
|
|
514
722
|
resolvedVersion = versionJar.version;
|
|
@@ -534,6 +742,19 @@ export class SourceService {
|
|
|
534
742
|
warnings.push(`Version ${resolvedVersion} is unobfuscated; ${mapping} mappings are not applicable. Using official names.`);
|
|
535
743
|
effectiveMapping = "official";
|
|
536
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
|
+
}
|
|
537
758
|
const resolved = await resolveSourceTargetInternal(resolvedTarget, {
|
|
538
759
|
// mojang requires source-backed artifact guarantee; force resolution to consider decompile candidate
|
|
539
760
|
// and reject later if mapping cannot be applied.
|
|
@@ -551,11 +772,59 @@ export class SourceService {
|
|
|
551
772
|
}
|
|
552
773
|
}, this.config);
|
|
553
774
|
resolved.version = resolvedVersion;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
+
}
|
|
559
828
|
const additionalTransformChain = [];
|
|
560
829
|
if (effectiveMapping === "intermediary" || effectiveMapping === "yarn") {
|
|
561
830
|
if (!resolved.version) {
|
|
@@ -565,7 +834,8 @@ export class SourceService {
|
|
|
565
834
|
details: {
|
|
566
835
|
mapping: effectiveMapping,
|
|
567
836
|
target: { kind, value },
|
|
568
|
-
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 } : {}) } }
|
|
569
839
|
}
|
|
570
840
|
});
|
|
571
841
|
}
|
|
@@ -588,8 +858,46 @@ export class SourceService {
|
|
|
588
858
|
resolved.requestedMapping = effectiveMapping;
|
|
589
859
|
resolved.mappingApplied = mappingDecision.mappingApplied;
|
|
590
860
|
resolved.provenance = provenance;
|
|
591
|
-
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)];
|
|
592
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
|
+
}
|
|
593
901
|
return {
|
|
594
902
|
artifactId: resolved.artifactId,
|
|
595
903
|
origin: resolved.origin,
|
|
@@ -602,9 +910,10 @@ export class SourceService {
|
|
|
602
910
|
requestedMapping: effectiveMapping,
|
|
603
911
|
mappingApplied: mappingDecision.mappingApplied,
|
|
604
912
|
provenance,
|
|
605
|
-
qualityFlags:
|
|
913
|
+
qualityFlags: resolved.qualityFlags,
|
|
606
914
|
repoUrl: resolved.repoUrl,
|
|
607
|
-
warnings
|
|
915
|
+
warnings,
|
|
916
|
+
sampleEntries
|
|
608
917
|
};
|
|
609
918
|
}
|
|
610
919
|
catch (caughtError) {
|
|
@@ -658,11 +967,13 @@ export class SourceService {
|
|
|
658
967
|
const snippetWindow = buildSnippetWindow(input.include?.snippetLines);
|
|
659
968
|
const regexPattern = match === "regex" ? compileRegex(query) : undefined;
|
|
660
969
|
const scope = input.scope;
|
|
970
|
+
const queryMode = input.queryMode ?? "auto";
|
|
661
971
|
const cursorContext = buildSearchCursorContext({
|
|
662
972
|
artifactId: artifact.artifactId,
|
|
663
973
|
query,
|
|
664
974
|
intent,
|
|
665
975
|
match,
|
|
976
|
+
queryMode,
|
|
666
977
|
scope,
|
|
667
978
|
includeDefinition
|
|
668
979
|
});
|
|
@@ -677,6 +988,8 @@ export class SourceService {
|
|
|
677
988
|
const recordHit = (hit) => {
|
|
678
989
|
accumulator.add(hit);
|
|
679
990
|
};
|
|
991
|
+
const hasSeparators = /[._$]/.test(query);
|
|
992
|
+
const tokenOnlyTextIntent = intent === "text" && queryMode === "token";
|
|
680
993
|
if (intent === "symbol") {
|
|
681
994
|
this.searchSymbolIntent(artifact.artifactId, query, match, scope, snippetWindow, regexPattern, recordHit);
|
|
682
995
|
// WS4: Use repo-level COUNT for symbol totalApprox when not regex
|
|
@@ -691,14 +1004,21 @@ export class SourceService {
|
|
|
691
1004
|
accumulator.setTotalApproxOverride(approxCount);
|
|
692
1005
|
}
|
|
693
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
|
+
}
|
|
694
1012
|
else if (!indexedSearchEnabled) {
|
|
695
1013
|
this.metrics.recordIndexedDisabled();
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
+
}
|
|
702
1022
|
}
|
|
703
1023
|
}
|
|
704
1024
|
else if (canUseIndexedSearchPath(indexedSearchEnabled, intent, match, scope)) {
|
|
@@ -714,6 +1034,10 @@ export class SourceService {
|
|
|
714
1034
|
// WS4: Use repo-level COUNT for totalApprox instead of accumulator count
|
|
715
1035
|
const approxCount = this.filesRepo.countTextCandidates(artifact.artifactId, query);
|
|
716
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
|
+
}
|
|
717
1041
|
}
|
|
718
1042
|
this.metrics.recordSearchIndexedHit();
|
|
719
1043
|
}
|
|
@@ -725,6 +1049,20 @@ export class SourceService {
|
|
|
725
1049
|
match,
|
|
726
1050
|
reason: caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
727
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();
|
|
728
1066
|
if (intent === "path") {
|
|
729
1067
|
this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
730
1068
|
}
|
|
@@ -733,15 +1071,6 @@ export class SourceService {
|
|
|
733
1071
|
}
|
|
734
1072
|
}
|
|
735
1073
|
}
|
|
736
|
-
else {
|
|
737
|
-
this.metrics.recordSearchFallback();
|
|
738
|
-
if (intent === "path") {
|
|
739
|
-
this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
740
|
-
}
|
|
741
|
-
else {
|
|
742
|
-
this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
1074
|
this.metrics.recordSearchIntentDuration(intent, Date.now() - intentStartedAt);
|
|
746
1075
|
const finalizedHits = accumulator.finalize();
|
|
747
1076
|
const page = finalizedHits.page;
|
|
@@ -765,11 +1094,14 @@ export class SourceService {
|
|
|
765
1094
|
this.metrics.recordOneHopExpansion(relations.length);
|
|
766
1095
|
}
|
|
767
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;
|
|
768
1100
|
return {
|
|
769
1101
|
hits: page,
|
|
770
1102
|
relations: relations && relations.length > 0 ? relations : undefined,
|
|
771
1103
|
nextCursor,
|
|
772
|
-
totalApprox
|
|
1104
|
+
totalApprox,
|
|
773
1105
|
mappingApplied: artifact.mappingApplied ?? "official"
|
|
774
1106
|
};
|
|
775
1107
|
}
|
|
@@ -792,9 +1124,7 @@ export class SourceService {
|
|
|
792
1124
|
const maxBytes = clampLimit(input.maxBytes, this.config.maxContentBytes, Number.MAX_SAFE_INTEGER);
|
|
793
1125
|
const fullBytes = Buffer.byteLength(row.content, "utf8");
|
|
794
1126
|
const truncated = fullBytes > maxBytes;
|
|
795
|
-
const content = truncated
|
|
796
|
-
? Buffer.from(row.content, "utf8").slice(0, maxBytes).toString("utf8")
|
|
797
|
-
: row.content;
|
|
1127
|
+
const content = truncated ? truncateUtf8ToMaxBytes(row.content, maxBytes) : row.content;
|
|
798
1128
|
if (truncated) {
|
|
799
1129
|
log("warn", "source.get_file.truncated", {
|
|
800
1130
|
artifactId: input.artifactId,
|
|
@@ -1090,7 +1420,11 @@ export class SourceService {
|
|
|
1090
1420
|
throw createError({
|
|
1091
1421
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1092
1422
|
message: "No Minecraft versions were returned by manifest.",
|
|
1093
|
-
details: {
|
|
1423
|
+
details: {
|
|
1424
|
+
includeSnapshots,
|
|
1425
|
+
nextAction: "Use list-versions to see available Minecraft versions.",
|
|
1426
|
+
suggestedCall: { tool: "list-versions", params: {} }
|
|
1427
|
+
}
|
|
1094
1428
|
});
|
|
1095
1429
|
}
|
|
1096
1430
|
const chronological = [...manifestOrder].reverse();
|
|
@@ -1102,14 +1436,22 @@ export class SourceService {
|
|
|
1102
1436
|
throw createError({
|
|
1103
1437
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1104
1438
|
message: `fromVersion "${requestedFrom}" was not found in manifest.`,
|
|
1105
|
-
details: {
|
|
1439
|
+
details: {
|
|
1440
|
+
fromVersion: requestedFrom,
|
|
1441
|
+
nextAction: "Use list-versions to see available Minecraft versions.",
|
|
1442
|
+
suggestedCall: { tool: "list-versions", params: {} }
|
|
1443
|
+
}
|
|
1106
1444
|
});
|
|
1107
1445
|
}
|
|
1108
1446
|
if (toIndex < 0) {
|
|
1109
1447
|
throw createError({
|
|
1110
1448
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1111
1449
|
message: `toVersion "${requestedTo}" was not found in manifest.`,
|
|
1112
|
-
details: {
|
|
1450
|
+
details: {
|
|
1451
|
+
toVersion: requestedTo,
|
|
1452
|
+
nextAction: "Use list-versions to see available Minecraft versions.",
|
|
1453
|
+
suggestedCall: { tool: "list-versions", params: {} }
|
|
1454
|
+
}
|
|
1113
1455
|
});
|
|
1114
1456
|
}
|
|
1115
1457
|
if (fromIndex > toIndex) {
|
|
@@ -1248,7 +1590,11 @@ export class SourceService {
|
|
|
1248
1590
|
if (manifestOrder.length === 0) {
|
|
1249
1591
|
throw createError({
|
|
1250
1592
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1251
|
-
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
|
+
}
|
|
1252
1598
|
});
|
|
1253
1599
|
}
|
|
1254
1600
|
const chronological = [...manifestOrder].reverse();
|
|
@@ -1258,14 +1604,22 @@ export class SourceService {
|
|
|
1258
1604
|
throw createError({
|
|
1259
1605
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1260
1606
|
message: `fromVersion "${fromVersion}" was not found in manifest.`,
|
|
1261
|
-
details: {
|
|
1607
|
+
details: {
|
|
1608
|
+
fromVersion,
|
|
1609
|
+
nextAction: "Use list-versions to see available Minecraft versions.",
|
|
1610
|
+
suggestedCall: { tool: "list-versions", params: {} }
|
|
1611
|
+
}
|
|
1262
1612
|
});
|
|
1263
1613
|
}
|
|
1264
1614
|
if (toIndex < 0) {
|
|
1265
1615
|
throw createError({
|
|
1266
1616
|
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1267
1617
|
message: `toVersion "${toVersion}" was not found in manifest.`,
|
|
1268
|
-
details: {
|
|
1618
|
+
details: {
|
|
1619
|
+
toVersion,
|
|
1620
|
+
nextAction: "Use list-versions to see available Minecraft versions.",
|
|
1621
|
+
suggestedCall: { tool: "list-versions", params: {} }
|
|
1622
|
+
}
|
|
1269
1623
|
});
|
|
1270
1624
|
}
|
|
1271
1625
|
if (fromIndex > toIndex) {
|
|
@@ -1388,18 +1742,18 @@ export class SourceService {
|
|
|
1388
1742
|
: diffMembersByKey(fromMembers.fields, toMembers.fields, (member) => member.name, true);
|
|
1389
1743
|
// Remap diff delta members for non-official mappings
|
|
1390
1744
|
const remapDelta = async (delta, kind) => {
|
|
1391
|
-
const [
|
|
1745
|
+
const [addedResult, removedResult] = await Promise.all([
|
|
1392
1746
|
this.remapSignatureMembers(delta.added, kind, toVersion, mapping, input.sourcePriority, warnings),
|
|
1393
1747
|
this.remapSignatureMembers(delta.removed, kind, fromVersion, mapping, input.sourcePriority, warnings)
|
|
1394
1748
|
]);
|
|
1395
1749
|
const remappedModified = await Promise.all(delta.modified.map(async (change) => {
|
|
1396
|
-
const [
|
|
1750
|
+
const [fromResult, toResult] = await Promise.all([
|
|
1397
1751
|
this.remapSignatureMembers([change.from], kind, fromVersion, mapping, input.sourcePriority, warnings),
|
|
1398
1752
|
this.remapSignatureMembers([change.to], kind, toVersion, mapping, input.sourcePriority, warnings)
|
|
1399
1753
|
]);
|
|
1400
|
-
return { ...change, from:
|
|
1754
|
+
return { ...change, from: fromResult.members[0], to: toResult.members[0] };
|
|
1401
1755
|
}));
|
|
1402
|
-
return { added:
|
|
1756
|
+
return { added: addedResult.members, removed: removedResult.members, modified: remappedModified };
|
|
1403
1757
|
};
|
|
1404
1758
|
const [remappedConstructors, remappedMethods, remappedFields] = await Promise.all([
|
|
1405
1759
|
remapDelta(constructors, "method"),
|
|
@@ -1447,6 +1801,78 @@ export class SourceService {
|
|
|
1447
1801
|
warnings
|
|
1448
1802
|
};
|
|
1449
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
|
+
}
|
|
1450
1876
|
async getClassSource(input) {
|
|
1451
1877
|
const className = input.className.trim();
|
|
1452
1878
|
if (!className) {
|
|
@@ -1455,9 +1881,16 @@ export class SourceService {
|
|
|
1455
1881
|
message: "className must be non-empty."
|
|
1456
1882
|
});
|
|
1457
1883
|
}
|
|
1884
|
+
const mode = input.mode ?? "metadata";
|
|
1458
1885
|
const startLine = normalizeStrictPositiveInt(input.startLine, "startLine");
|
|
1459
1886
|
const endLine = normalizeStrictPositiveInt(input.endLine, "endLine");
|
|
1460
|
-
|
|
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
|
+
}
|
|
1461
1894
|
if (startLine != null && endLine != null && startLine > endLine) {
|
|
1462
1895
|
throw createError({
|
|
1463
1896
|
code: ERROR_CODES.INVALID_LINE_RANGE,
|
|
@@ -1497,7 +1930,11 @@ export class SourceService {
|
|
|
1497
1930
|
target: input.target,
|
|
1498
1931
|
mapping: input.mapping,
|
|
1499
1932
|
sourcePriority: input.sourcePriority,
|
|
1500
|
-
allowDecompile: input.allowDecompile
|
|
1933
|
+
allowDecompile: input.allowDecompile,
|
|
1934
|
+
projectPath: input.projectPath,
|
|
1935
|
+
scope: input.scope,
|
|
1936
|
+
preferProjectVersion: input.preferProjectVersion,
|
|
1937
|
+
strictVersion: input.strictVersion
|
|
1501
1938
|
});
|
|
1502
1939
|
artifactId = resolved.artifactId;
|
|
1503
1940
|
origin = resolved.origin;
|
|
@@ -1517,41 +1954,100 @@ export class SourceService {
|
|
|
1517
1954
|
}
|
|
1518
1955
|
const filePath = this.resolveClassFilePath(artifactId, className);
|
|
1519
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
|
+
}
|
|
1520
1970
|
throw createError({
|
|
1521
1971
|
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
1522
1972
|
message: `Source for class "${className}" was not found.`,
|
|
1523
1973
|
details: {
|
|
1524
1974
|
artifactId,
|
|
1525
|
-
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 } }
|
|
1526
1982
|
}
|
|
1527
1983
|
});
|
|
1528
1984
|
}
|
|
1529
1985
|
const row = this.filesRepo.getFileContent(artifactId, filePath);
|
|
1530
1986
|
if (!row) {
|
|
1987
|
+
const simpleName = className.split(/[.$]/).at(-1) ?? className;
|
|
1531
1988
|
throw createError({
|
|
1532
1989
|
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
1533
1990
|
message: `Source for class "${className}" was not found.`,
|
|
1534
1991
|
details: {
|
|
1535
1992
|
artifactId,
|
|
1536
1993
|
className,
|
|
1537
|
-
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 } }
|
|
1538
2000
|
}
|
|
1539
2001
|
});
|
|
1540
2002
|
}
|
|
1541
2003
|
const lines = row.content.split(/\r?\n/);
|
|
1542
2004
|
const totalLines = lines.length;
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
let
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
+
}
|
|
1555
2051
|
const normalizedProvenance = provenance ??
|
|
1556
2052
|
this.buildFallbackProvenance({
|
|
1557
2053
|
artifactId,
|
|
@@ -1561,19 +2057,22 @@ export class SourceService {
|
|
|
1561
2057
|
});
|
|
1562
2058
|
return {
|
|
1563
2059
|
className,
|
|
1564
|
-
|
|
2060
|
+
mode,
|
|
2061
|
+
sourceText,
|
|
1565
2062
|
totalLines,
|
|
1566
2063
|
returnedRange: {
|
|
1567
|
-
start:
|
|
2064
|
+
start: returnedStart,
|
|
1568
2065
|
end: returnedEnd
|
|
1569
2066
|
},
|
|
1570
|
-
truncated
|
|
2067
|
+
truncated,
|
|
2068
|
+
...(charsTruncated ? { charsTruncated } : {}),
|
|
1571
2069
|
origin,
|
|
1572
2070
|
artifactId,
|
|
1573
2071
|
requestedMapping,
|
|
1574
2072
|
mappingApplied,
|
|
1575
2073
|
provenance: normalizedProvenance,
|
|
1576
2074
|
qualityFlags,
|
|
2075
|
+
...(resolvedOutputFile ? { outputFile: resolvedOutputFile } : {}),
|
|
1577
2076
|
warnings
|
|
1578
2077
|
};
|
|
1579
2078
|
}
|
|
@@ -1625,7 +2124,11 @@ export class SourceService {
|
|
|
1625
2124
|
target: input.target,
|
|
1626
2125
|
mapping: requestedMapping,
|
|
1627
2126
|
sourcePriority: input.sourcePriority,
|
|
1628
|
-
allowDecompile: input.allowDecompile
|
|
2127
|
+
allowDecompile: input.allowDecompile,
|
|
2128
|
+
projectPath: input.projectPath,
|
|
2129
|
+
scope: input.scope,
|
|
2130
|
+
preferProjectVersion: input.preferProjectVersion,
|
|
2131
|
+
strictVersion: input.strictVersion
|
|
1629
2132
|
});
|
|
1630
2133
|
artifactId = resolved.artifactId;
|
|
1631
2134
|
origin = resolved.origin;
|
|
@@ -1651,7 +2154,8 @@ export class SourceService {
|
|
|
1651
2154
|
message: `Non-official mapping "${requestedMapping}" requires a version, but none was resolved.`,
|
|
1652
2155
|
details: {
|
|
1653
2156
|
mapping: requestedMapping,
|
|
1654
|
-
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" } }
|
|
1655
2159
|
}
|
|
1656
2160
|
});
|
|
1657
2161
|
}
|
|
@@ -1679,13 +2183,13 @@ export class SourceService {
|
|
|
1679
2183
|
});
|
|
1680
2184
|
warnings.push(...signature.warnings);
|
|
1681
2185
|
let remappedConstructors = version != null
|
|
1682
|
-
? await this.remapSignatureMembers(signature.constructors, "method", version, requestedMapping, input.sourcePriority, warnings)
|
|
2186
|
+
? (await this.remapSignatureMembers(signature.constructors, "method", version, requestedMapping, input.sourcePriority, warnings)).members
|
|
1683
2187
|
: signature.constructors;
|
|
1684
2188
|
let remappedFields = version != null
|
|
1685
|
-
? await this.remapSignatureMembers(signature.fields, "field", version, requestedMapping, input.sourcePriority, warnings)
|
|
2189
|
+
? (await this.remapSignatureMembers(signature.fields, "field", version, requestedMapping, input.sourcePriority, warnings)).members
|
|
1686
2190
|
: signature.fields;
|
|
1687
2191
|
let remappedMethods = version != null
|
|
1688
|
-
? await this.remapSignatureMembers(signature.methods, "method", version, requestedMapping, input.sourcePriority, warnings)
|
|
2192
|
+
? (await this.remapSignatureMembers(signature.methods, "method", version, requestedMapping, input.sourcePriority, warnings)).members
|
|
1689
2193
|
: signature.methods;
|
|
1690
2194
|
// Apply memberPattern post-remap for non-official mappings
|
|
1691
2195
|
if (requestedMapping !== "official" && memberPattern) {
|
|
@@ -1744,40 +2248,272 @@ export class SourceService {
|
|
|
1744
2248
|
};
|
|
1745
2249
|
}
|
|
1746
2250
|
async validateMixin(input) {
|
|
1747
|
-
|
|
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();
|
|
1748
2337
|
if (!version) {
|
|
1749
2338
|
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "version must be non-empty." });
|
|
1750
2339
|
}
|
|
1751
|
-
|
|
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
|
+
}
|
|
1752
2361
|
if (!source.trim()) {
|
|
1753
2362
|
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "source must be non-empty." });
|
|
1754
2363
|
}
|
|
1755
2364
|
const warnings = [];
|
|
1756
|
-
|
|
1757
|
-
|
|
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
|
+
}
|
|
1758
2464
|
const parsed = parseMixinSource(source);
|
|
1759
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;
|
|
1760
2471
|
for (const target of parsed.targets) {
|
|
1761
|
-
|
|
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;
|
|
1762
2493
|
if (requestedMapping !== "official") {
|
|
1763
2494
|
try {
|
|
1764
2495
|
const mapped = await this.mappingService.findMapping({
|
|
1765
2496
|
version,
|
|
1766
2497
|
kind: "class",
|
|
1767
|
-
name:
|
|
2498
|
+
name: resolvedClassName,
|
|
1768
2499
|
sourceMapping: requestedMapping,
|
|
1769
2500
|
targetMapping: "official",
|
|
1770
2501
|
sourcePriority: input.sourcePriority
|
|
1771
2502
|
});
|
|
1772
2503
|
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
1773
2504
|
officialName = mapped.resolvedSymbol.name;
|
|
2505
|
+
resolutionTrace?.push({ target: target.className, step: "mapping", input: resolvedClassName, output: officialName, success: true });
|
|
1774
2506
|
}
|
|
1775
2507
|
else {
|
|
1776
|
-
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" });
|
|
1777
2511
|
}
|
|
1778
2512
|
}
|
|
1779
|
-
catch {
|
|
1780
|
-
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) });
|
|
1781
2517
|
}
|
|
1782
2518
|
}
|
|
1783
2519
|
try {
|
|
@@ -1787,18 +2523,288 @@ export class SourceService {
|
|
|
1787
2523
|
access: "all"
|
|
1788
2524
|
});
|
|
1789
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
|
+
}
|
|
1790
2563
|
targetMembers.set(target.className, {
|
|
1791
2564
|
className: target.className,
|
|
1792
|
-
constructors
|
|
1793
|
-
methods
|
|
1794
|
-
fields
|
|
2565
|
+
constructors,
|
|
2566
|
+
methods,
|
|
2567
|
+
fields
|
|
1795
2568
|
});
|
|
1796
2569
|
}
|
|
1797
|
-
catch {
|
|
1798
|
-
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}`);
|
|
1799
2624
|
}
|
|
1800
2625
|
}
|
|
1801
|
-
|
|
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
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
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
|
+
};
|
|
1802
2808
|
}
|
|
1803
2809
|
async validateAccessWidener(input) {
|
|
1804
2810
|
const version = input.version.trim();
|
|
@@ -2301,9 +3307,39 @@ export class SourceService {
|
|
|
2301
3307
|
// Contains matches need more candidates
|
|
2302
3308
|
return base;
|
|
2303
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
|
+
}
|
|
2304
3337
|
resolveClassFilePath(artifactId, className) {
|
|
2305
3338
|
const normalizedClassName = className.trim();
|
|
2306
|
-
const classPath =
|
|
3339
|
+
const classPath = classNameToClassPath(normalizedClassName);
|
|
3340
|
+
if (!classPath) {
|
|
3341
|
+
return undefined;
|
|
3342
|
+
}
|
|
2307
3343
|
const candidates = new Set([`${classPath}.java`]);
|
|
2308
3344
|
const innerIndex = classPath.indexOf("$");
|
|
2309
3345
|
if (innerIndex > 0) {
|
|
@@ -2320,10 +3356,14 @@ export class SourceService {
|
|
|
2320
3356
|
return undefined;
|
|
2321
3357
|
}
|
|
2322
3358
|
const classPathBySymbol = this.symbolsRepo.findBestClassFilePath(artifactId, normalizedClassName, simpleName);
|
|
2323
|
-
if (classPathBySymbol) {
|
|
3359
|
+
if (classPathBySymbol && isPackageCompatible(classPathBySymbol, classPath)) {
|
|
2324
3360
|
return classPathBySymbol;
|
|
2325
3361
|
}
|
|
2326
|
-
|
|
3362
|
+
const byName = this.filesRepo.findFirstFilePathByName(artifactId, `${simpleName}.java`);
|
|
3363
|
+
if (byName && isPackageCompatible(byName, classPath)) {
|
|
3364
|
+
return byName;
|
|
3365
|
+
}
|
|
3366
|
+
return undefined;
|
|
2327
3367
|
}
|
|
2328
3368
|
findNearestSymbolForLine(artifactId, filePath, line, symbolKind) {
|
|
2329
3369
|
const symbols = this.symbolsRepo
|
|
@@ -2569,8 +3609,9 @@ export class SourceService {
|
|
|
2569
3609
|
};
|
|
2570
3610
|
}
|
|
2571
3611
|
async remapSignatureMembers(members, kind, version, mapping, sourcePriority, warnings) {
|
|
3612
|
+
const failedNames = new Set();
|
|
2572
3613
|
if (mapping === "official") {
|
|
2573
|
-
return members;
|
|
3614
|
+
return { members, failedNames };
|
|
2574
3615
|
}
|
|
2575
3616
|
// Build deduplicated lookup tables for member names and owner FQNs
|
|
2576
3617
|
const memberKeyToRemapped = new Map();
|
|
@@ -2584,60 +3625,83 @@ export class SourceService {
|
|
|
2584
3625
|
ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = official FQN
|
|
2585
3626
|
}
|
|
2586
3627
|
}
|
|
2587
|
-
// Remap
|
|
2588
|
-
const
|
|
2589
|
-
await Promise.all(
|
|
2590
|
-
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]) => {
|
|
2591
3631
|
try {
|
|
2592
3632
|
const mapped = await this.mappingService.findMapping({
|
|
2593
3633
|
version,
|
|
2594
|
-
kind,
|
|
2595
|
-
name,
|
|
2596
|
-
owner: ownerFqn,
|
|
2597
|
-
descriptor: kind === "method" ? descriptor : undefined,
|
|
3634
|
+
kind: "class",
|
|
3635
|
+
name: officialFqn,
|
|
2598
3636
|
sourceMapping: "official",
|
|
2599
3637
|
targetMapping: mapping,
|
|
2600
3638
|
sourcePriority
|
|
2601
3639
|
});
|
|
2602
3640
|
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
2603
|
-
|
|
2604
|
-
}
|
|
2605
|
-
else {
|
|
2606
|
-
warnings.push(`Could not remap ${kind} "${name}" to ${mapping}.`);
|
|
3641
|
+
ownerToRemapped.set(officialFqn, mapped.resolvedSymbol.name);
|
|
2607
3642
|
}
|
|
2608
3643
|
}
|
|
2609
3644
|
catch {
|
|
2610
|
-
|
|
3645
|
+
// keep official FQN as fallback
|
|
2611
3646
|
}
|
|
2612
3647
|
}));
|
|
2613
|
-
// Remap
|
|
2614
|
-
const
|
|
2615
|
-
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");
|
|
2616
3652
|
try {
|
|
3653
|
+
const targetOwner = ownerToRemapped.get(ownerFqn) ?? ownerFqn;
|
|
2617
3654
|
const mapped = await this.mappingService.findMapping({
|
|
2618
3655
|
version,
|
|
2619
|
-
kind
|
|
2620
|
-
name
|
|
3656
|
+
kind,
|
|
3657
|
+
name,
|
|
3658
|
+
owner: ownerFqn,
|
|
3659
|
+
descriptor: kind === "method" ? descriptor : undefined,
|
|
2621
3660
|
sourceMapping: "official",
|
|
2622
3661
|
targetMapping: mapping,
|
|
2623
|
-
sourcePriority
|
|
3662
|
+
sourcePriority,
|
|
3663
|
+
disambiguation: { ownerHint: targetOwner }
|
|
2624
3664
|
});
|
|
2625
3665
|
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
2626
|
-
|
|
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);
|
|
2627
3687
|
}
|
|
2628
3688
|
}
|
|
2629
3689
|
catch {
|
|
2630
|
-
|
|
3690
|
+
warnings.push(`Remap failed for ${kind} "${name}".`);
|
|
3691
|
+
failedNames.add(name);
|
|
2631
3692
|
}
|
|
2632
3693
|
}));
|
|
2633
|
-
return
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
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
|
+
};
|
|
2641
3705
|
}
|
|
2642
3706
|
fallbackArtifactSignature(artifactId) {
|
|
2643
3707
|
return createHash("sha256").update(artifactId).digest("hex");
|
|
@@ -2748,6 +3812,25 @@ export class SourceService {
|
|
|
2748
3812
|
contentHash: createHash("sha256").update(entry.content).digest("hex")
|
|
2749
3813
|
}));
|
|
2750
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
|
+
}
|
|
2751
3834
|
finally {
|
|
2752
3835
|
this.metrics.recordDuration("decompile_duration_ms", Date.now() - decompileStartedAt);
|
|
2753
3836
|
}
|
|
@@ -2756,7 +3839,11 @@ export class SourceService {
|
|
|
2756
3839
|
throw createError({
|
|
2757
3840
|
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
2758
3841
|
message: "No source artifact available.",
|
|
2759
|
-
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
|
+
}
|
|
2760
3847
|
});
|
|
2761
3848
|
}
|
|
2762
3849
|
const symbols = [];
|
|
@@ -2790,7 +3877,11 @@ export class SourceService {
|
|
|
2790
3877
|
throw createError({
|
|
2791
3878
|
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
2792
3879
|
message: "Artifact not found. Resolve context first.",
|
|
2793
|
-
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
|
+
}
|
|
2794
3885
|
});
|
|
2795
3886
|
}
|
|
2796
3887
|
return artifact;
|