@adhisang/minecraft-modding-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +765 -0
- package/dist/access-widener-parser.d.ts +24 -0
- package/dist/access-widener-parser.js +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +178 -0
- package/dist/decompiler/vineflower.d.ts +15 -0
- package/dist/decompiler/vineflower.js +185 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.js +49 -0
- package/dist/hash.d.ts +1 -0
- package/dist/hash.js +12 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1447 -0
- package/dist/java-process.d.ts +16 -0
- package/dist/java-process.js +120 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +21 -0
- package/dist/mapping-pipeline-service.d.ts +18 -0
- package/dist/mapping-pipeline-service.js +60 -0
- package/dist/mapping-service.d.ts +161 -0
- package/dist/mapping-service.js +1706 -0
- package/dist/maven-resolver.d.ts +22 -0
- package/dist/maven-resolver.js +122 -0
- package/dist/minecraft-explorer-service.d.ts +43 -0
- package/dist/minecraft-explorer-service.js +562 -0
- package/dist/mixin-parser.d.ts +34 -0
- package/dist/mixin-parser.js +194 -0
- package/dist/mixin-validator.d.ts +59 -0
- package/dist/mixin-validator.js +274 -0
- package/dist/mod-analyzer.d.ts +23 -0
- package/dist/mod-analyzer.js +346 -0
- package/dist/mod-decompile-service.d.ts +39 -0
- package/dist/mod-decompile-service.js +136 -0
- package/dist/mod-remap-service.d.ts +17 -0
- package/dist/mod-remap-service.js +186 -0
- package/dist/mod-search-service.d.ts +28 -0
- package/dist/mod-search-service.js +174 -0
- package/dist/mojang-tiny-mapping-service.d.ts +13 -0
- package/dist/mojang-tiny-mapping-service.js +351 -0
- package/dist/nbt/java-nbt-codec.d.ts +3 -0
- package/dist/nbt/java-nbt-codec.js +385 -0
- package/dist/nbt/json-patch.d.ts +3 -0
- package/dist/nbt/json-patch.js +352 -0
- package/dist/nbt/pipeline.d.ts +39 -0
- package/dist/nbt/pipeline.js +173 -0
- package/dist/nbt/typed-json.d.ts +10 -0
- package/dist/nbt/typed-json.js +205 -0
- package/dist/nbt/types.d.ts +66 -0
- package/dist/nbt/types.js +2 -0
- package/dist/observability.d.ts +88 -0
- package/dist/observability.js +165 -0
- package/dist/path-converter.d.ts +12 -0
- package/dist/path-converter.js +161 -0
- package/dist/path-resolver.d.ts +19 -0
- package/dist/path-resolver.js +78 -0
- package/dist/registry-service.d.ts +29 -0
- package/dist/registry-service.js +214 -0
- package/dist/repo-downloader.d.ts +15 -0
- package/dist/repo-downloader.js +111 -0
- package/dist/resources.d.ts +3 -0
- package/dist/resources.js +154 -0
- package/dist/search-hit-accumulator.d.ts +38 -0
- package/dist/search-hit-accumulator.js +153 -0
- package/dist/source-jar-reader.d.ts +13 -0
- package/dist/source-jar-reader.js +216 -0
- package/dist/source-resolver.d.ts +14 -0
- package/dist/source-resolver.js +274 -0
- package/dist/source-service.d.ts +404 -0
- package/dist/source-service.js +2881 -0
- package/dist/storage/artifacts-repo.d.ts +45 -0
- package/dist/storage/artifacts-repo.js +209 -0
- package/dist/storage/db.d.ts +14 -0
- package/dist/storage/db.js +132 -0
- package/dist/storage/files-repo.d.ts +78 -0
- package/dist/storage/files-repo.js +437 -0
- package/dist/storage/index-meta-repo.d.ts +35 -0
- package/dist/storage/index-meta-repo.js +97 -0
- package/dist/storage/migrations.d.ts +11 -0
- package/dist/storage/migrations.js +71 -0
- package/dist/storage/schema.d.ts +1 -0
- package/dist/storage/schema.js +160 -0
- package/dist/storage/sqlite.d.ts +20 -0
- package/dist/storage/sqlite.js +111 -0
- package/dist/storage/symbols-repo.d.ts +63 -0
- package/dist/storage/symbols-repo.js +401 -0
- package/dist/symbols/symbol-extractor.d.ts +7 -0
- package/dist/symbols/symbol-extractor.js +64 -0
- package/dist/tiny-remapper-resolver.d.ts +1 -0
- package/dist/tiny-remapper-resolver.js +62 -0
- package/dist/tiny-remapper-service.d.ts +16 -0
- package/dist/tiny-remapper-service.js +73 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.js +2 -0
- package/dist/version-diff-service.d.ts +41 -0
- package/dist/version-diff-service.js +222 -0
- package/dist/version-service.d.ts +70 -0
- package/dist/version-service.js +411 -0
- package/dist/vineflower-resolver.d.ts +1 -0
- package/dist/vineflower-resolver.js +62 -0
- package/dist/workspace-mapping-service.d.ts +18 -0
- package/dist/workspace-mapping-service.js +89 -0
- package/package.json +61 -0
|
@@ -0,0 +1,2881 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { decompileBinaryJar } from "./decompiler/vineflower.js";
|
|
5
|
+
import { resolveVineflowerJar } from "./vineflower-resolver.js";
|
|
6
|
+
import { parseCoordinate } from "./maven-resolver.js";
|
|
7
|
+
import { MinecraftExplorerService } from "./minecraft-explorer-service.js";
|
|
8
|
+
import { parseMixinSource } from "./mixin-parser.js";
|
|
9
|
+
import { parseAccessWidener } from "./access-widener-parser.js";
|
|
10
|
+
import { validateParsedMixin, validateParsedAccessWidener } from "./mixin-validator.js";
|
|
11
|
+
import { resolveSourceTarget as resolveSourceTargetInternal } from "./source-resolver.js";
|
|
12
|
+
import { applyMappingPipeline } from "./mapping-pipeline-service.js";
|
|
13
|
+
import { MappingService } from "./mapping-service.js";
|
|
14
|
+
import { extractSymbolsFromSource } from "./symbols/symbol-extractor.js";
|
|
15
|
+
import { iterateJavaEntriesAsUtf8 } from "./source-jar-reader.js";
|
|
16
|
+
import { openDatabase } from "./storage/db.js";
|
|
17
|
+
import { ArtifactsRepo } from "./storage/artifacts-repo.js";
|
|
18
|
+
import { FilesRepo } from "./storage/files-repo.js";
|
|
19
|
+
import { IndexMetaRepo } from "./storage/index-meta-repo.js";
|
|
20
|
+
import { SymbolsRepo } from "./storage/symbols-repo.js";
|
|
21
|
+
import { RuntimeMetrics } from "./observability.js";
|
|
22
|
+
import { log } from "./logger.js";
|
|
23
|
+
import { createSearchHitAccumulator, decodeSearchCursor, encodeSearchCursor } from "./search-hit-accumulator.js";
|
|
24
|
+
import { WorkspaceMappingService } from "./workspace-mapping-service.js";
|
|
25
|
+
import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
|
|
26
|
+
import { RegistryService } from "./registry-service.js";
|
|
27
|
+
import { VersionDiffService } from "./version-diff-service.js";
|
|
28
|
+
import { ModDecompileService } from "./mod-decompile-service.js";
|
|
29
|
+
import { ModSearchService } from "./mod-search-service.js";
|
|
30
|
+
const INDEX_SCHEMA_VERSION = 1;
|
|
31
|
+
const SYMBOL_KINDS = ["class", "interface", "enum", "record", "method", "field"];
|
|
32
|
+
function isSymbolKind(value) {
|
|
33
|
+
return SYMBOL_KINDS.includes(value);
|
|
34
|
+
}
|
|
35
|
+
function clampLimit(limit, fallback, max) {
|
|
36
|
+
if (!Number.isFinite(limit) || limit == null) {
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
return Math.max(1, Math.min(max, Math.trunc(limit)));
|
|
40
|
+
}
|
|
41
|
+
const MAX_REGEX_QUERY_LENGTH = 200;
|
|
42
|
+
const MAX_REGEX_RESULT_LIMIT = 100;
|
|
43
|
+
function normalizePathStyle(path) {
|
|
44
|
+
return path.replaceAll("\\", "/");
|
|
45
|
+
}
|
|
46
|
+
function parseQualifiedMethodSymbol(symbol) {
|
|
47
|
+
const trimmed = symbol.trim();
|
|
48
|
+
const separator = trimmed.lastIndexOf(".");
|
|
49
|
+
if (separator <= 0 || separator >= trimmed.length - 1) {
|
|
50
|
+
throw createError({
|
|
51
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
52
|
+
message: `symbol must be in the form "fully.qualified.Class.method".`,
|
|
53
|
+
details: { symbol }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const className = trimmed.slice(0, separator);
|
|
57
|
+
const methodName = trimmed.slice(separator + 1);
|
|
58
|
+
if (!className ||
|
|
59
|
+
!methodName ||
|
|
60
|
+
className.includes("/") ||
|
|
61
|
+
methodName.includes(".") ||
|
|
62
|
+
/\s/.test(methodName)) {
|
|
63
|
+
throw createError({
|
|
64
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
65
|
+
message: `symbol must be in the form "fully.qualified.Class.method".`,
|
|
66
|
+
details: { symbol }
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return { className, methodName };
|
|
70
|
+
}
|
|
71
|
+
function normalizeOptionalString(value) {
|
|
72
|
+
if (value == null) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const trimmed = value.trim();
|
|
76
|
+
return trimmed ? trimmed : undefined;
|
|
77
|
+
}
|
|
78
|
+
function normalizeStrictPositiveInt(value, field) {
|
|
79
|
+
if (value == null) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
83
|
+
throw createError({
|
|
84
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
85
|
+
message: `${field} must be a positive integer.`,
|
|
86
|
+
details: {
|
|
87
|
+
field,
|
|
88
|
+
value
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
function normalizeMapping(mapping) {
|
|
95
|
+
if (mapping == null) {
|
|
96
|
+
return "official";
|
|
97
|
+
}
|
|
98
|
+
if (mapping === "official" ||
|
|
99
|
+
mapping === "mojang" ||
|
|
100
|
+
mapping === "intermediary" ||
|
|
101
|
+
mapping === "yarn") {
|
|
102
|
+
return mapping;
|
|
103
|
+
}
|
|
104
|
+
throw createError({
|
|
105
|
+
code: ERROR_CODES.MAPPING_UNAVAILABLE,
|
|
106
|
+
message: `Unsupported mapping "${mapping}".`,
|
|
107
|
+
details: { mapping }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function normalizeAccessWidenerNamespace(namespace) {
|
|
111
|
+
const normalized = namespace?.trim().toLowerCase();
|
|
112
|
+
if (!normalized) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
if (normalized === "named") {
|
|
116
|
+
return "yarn";
|
|
117
|
+
}
|
|
118
|
+
if (normalized === "official" ||
|
|
119
|
+
normalized === "mojang" ||
|
|
120
|
+
normalized === "intermediary" ||
|
|
121
|
+
normalized === "yarn") {
|
|
122
|
+
return normalized;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
function normalizeMemberAccess(access) {
|
|
127
|
+
if (access == null) {
|
|
128
|
+
return "public";
|
|
129
|
+
}
|
|
130
|
+
if (access === "public" || access === "all") {
|
|
131
|
+
return access;
|
|
132
|
+
}
|
|
133
|
+
throw createError({
|
|
134
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
135
|
+
message: `access must be "public" or "all".`,
|
|
136
|
+
details: { access }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function sortDiffMembers(members) {
|
|
140
|
+
return [...members].sort((left, right) => {
|
|
141
|
+
const nameCompare = left.name.localeCompare(right.name);
|
|
142
|
+
if (nameCompare !== 0) {
|
|
143
|
+
return nameCompare;
|
|
144
|
+
}
|
|
145
|
+
const descriptorCompare = left.jvmDescriptor.localeCompare(right.jvmDescriptor);
|
|
146
|
+
if (descriptorCompare !== 0) {
|
|
147
|
+
return descriptorCompare;
|
|
148
|
+
}
|
|
149
|
+
return left.ownerFqn.localeCompare(right.ownerFqn);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function sortDiffMemberChanges(changes) {
|
|
153
|
+
return [...changes].sort((left, right) => {
|
|
154
|
+
const keyCompare = left.key.localeCompare(right.key);
|
|
155
|
+
if (keyCompare !== 0) {
|
|
156
|
+
return keyCompare;
|
|
157
|
+
}
|
|
158
|
+
const fromOwnerCompare = left.from.ownerFqn.localeCompare(right.from.ownerFqn);
|
|
159
|
+
if (fromOwnerCompare !== 0) {
|
|
160
|
+
return fromOwnerCompare;
|
|
161
|
+
}
|
|
162
|
+
return left.to.ownerFqn.localeCompare(right.to.ownerFqn);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function changedMemberFields(fromMember, toMember, includeDescriptor) {
|
|
166
|
+
const changed = [];
|
|
167
|
+
if (fromMember.accessFlags !== toMember.accessFlags) {
|
|
168
|
+
changed.push("accessFlags");
|
|
169
|
+
}
|
|
170
|
+
if (fromMember.isSynthetic !== toMember.isSynthetic) {
|
|
171
|
+
changed.push("isSynthetic");
|
|
172
|
+
}
|
|
173
|
+
if (fromMember.javaSignature !== toMember.javaSignature) {
|
|
174
|
+
changed.push("javaSignature");
|
|
175
|
+
}
|
|
176
|
+
if (includeDescriptor && fromMember.jvmDescriptor !== toMember.jvmDescriptor) {
|
|
177
|
+
changed.push("jvmDescriptor");
|
|
178
|
+
}
|
|
179
|
+
return changed;
|
|
180
|
+
}
|
|
181
|
+
function diffMembersByKey(fromMembersInput, toMembersInput, buildKey, includeDescriptorInModified) {
|
|
182
|
+
const fromMembers = sortDiffMembers(fromMembersInput);
|
|
183
|
+
const toMembers = sortDiffMembers(toMembersInput);
|
|
184
|
+
const fromByKey = new Map();
|
|
185
|
+
const toByKey = new Map();
|
|
186
|
+
for (const member of fromMembers) {
|
|
187
|
+
const key = buildKey(member);
|
|
188
|
+
if (!fromByKey.has(key)) {
|
|
189
|
+
fromByKey.set(key, member);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const member of toMembers) {
|
|
193
|
+
const key = buildKey(member);
|
|
194
|
+
if (!toByKey.has(key)) {
|
|
195
|
+
toByKey.set(key, member);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const added = [];
|
|
199
|
+
const removed = [];
|
|
200
|
+
const modified = [];
|
|
201
|
+
for (const [key, toMember] of toByKey.entries()) {
|
|
202
|
+
const fromMember = fromByKey.get(key);
|
|
203
|
+
if (!fromMember) {
|
|
204
|
+
added.push(toMember);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const changed = changedMemberFields(fromMember, toMember, includeDescriptorInModified);
|
|
208
|
+
if (changed.length > 0) {
|
|
209
|
+
modified.push({
|
|
210
|
+
key,
|
|
211
|
+
from: fromMember,
|
|
212
|
+
to: toMember,
|
|
213
|
+
changed
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const [key, fromMember] of fromByKey.entries()) {
|
|
218
|
+
if (!toByKey.has(key)) {
|
|
219
|
+
removed.push(fromMember);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
added: sortDiffMembers(added),
|
|
224
|
+
removed: sortDiffMembers(removed),
|
|
225
|
+
modified: sortDiffMemberChanges(modified)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function emptyDiffDelta() {
|
|
229
|
+
return {
|
|
230
|
+
added: [],
|
|
231
|
+
removed: [],
|
|
232
|
+
modified: []
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function normalizeIntent(intent) {
|
|
236
|
+
if (intent === "path" || intent === "text") {
|
|
237
|
+
return intent;
|
|
238
|
+
}
|
|
239
|
+
return "symbol";
|
|
240
|
+
}
|
|
241
|
+
function normalizeMatch(match) {
|
|
242
|
+
if (match === "exact" || match === "contains" || match === "regex") {
|
|
243
|
+
return match;
|
|
244
|
+
}
|
|
245
|
+
return "prefix";
|
|
246
|
+
}
|
|
247
|
+
function canUseIndexedSearchPath(indexedSearchEnabled, intent, match, _scope) {
|
|
248
|
+
if (!indexedSearchEnabled) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
if (intent !== "text" && intent !== "path") {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
if (match === "regex") {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
// fileGlob and symbolKind are now applied as post-filters on indexed candidates
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
function buildGlobRegex(pattern) {
|
|
261
|
+
const escaped = pattern
|
|
262
|
+
.replace(/[-/\\^$+?.()|[\]{}]/g, "\\$&")
|
|
263
|
+
.replace(/\\\*/g, ".*")
|
|
264
|
+
.replace(/\\\?/g, ".");
|
|
265
|
+
return new RegExp(`^${escaped}$`);
|
|
266
|
+
}
|
|
267
|
+
function globToSqlLike(pattern) {
|
|
268
|
+
let result = "";
|
|
269
|
+
for (const char of pattern) {
|
|
270
|
+
if (char === "*") {
|
|
271
|
+
result += "%";
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (char === "?") {
|
|
275
|
+
result += "_";
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (char === "%" || char === "_" || char === "\\") {
|
|
279
|
+
result += `\\${char}`;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
result += char;
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
function checkPackagePrefix(filePath, packagePrefix) {
|
|
287
|
+
if (!packagePrefix) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
const normalizedPrefix = packagePrefix.replace(/\.+/g, "/").replace(/\/+$/, "");
|
|
291
|
+
return normalizePathStyle(filePath).startsWith(`${normalizedPrefix}/`);
|
|
292
|
+
}
|
|
293
|
+
function buildSnippetWindow(lines) {
|
|
294
|
+
const totalLines = clampLimit(lines, 8, 80);
|
|
295
|
+
const before = Math.floor((totalLines - 1) / 2);
|
|
296
|
+
return {
|
|
297
|
+
before,
|
|
298
|
+
after: Math.max(0, totalLines - 1 - before)
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function buildSearchCursorContext(input) {
|
|
302
|
+
return JSON.stringify({
|
|
303
|
+
artifactId: input.artifactId,
|
|
304
|
+
query: input.query,
|
|
305
|
+
intent: input.intent,
|
|
306
|
+
match: input.match,
|
|
307
|
+
includeDefinition: input.includeDefinition,
|
|
308
|
+
packagePrefix: input.scope?.packagePrefix ?? "",
|
|
309
|
+
fileGlob: input.scope?.fileGlob ?? "",
|
|
310
|
+
symbolKind: input.scope?.symbolKind ?? ""
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function toLower(value) {
|
|
314
|
+
return value.toLocaleLowerCase();
|
|
315
|
+
}
|
|
316
|
+
function compileRegex(query) {
|
|
317
|
+
try {
|
|
318
|
+
return new RegExp(query, "i");
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
throw createError({
|
|
322
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
323
|
+
message: "Invalid regex query.",
|
|
324
|
+
details: { query }
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function findMatchIndex(target, query, match, pattern) {
|
|
329
|
+
if (!query) {
|
|
330
|
+
return -1;
|
|
331
|
+
}
|
|
332
|
+
if (match === "regex") {
|
|
333
|
+
if (!pattern) {
|
|
334
|
+
return -1;
|
|
335
|
+
}
|
|
336
|
+
pattern.lastIndex = 0;
|
|
337
|
+
const result = pattern.exec(target);
|
|
338
|
+
return result?.index ?? -1;
|
|
339
|
+
}
|
|
340
|
+
if (match === "exact") {
|
|
341
|
+
return target === query ? 0 : -1;
|
|
342
|
+
}
|
|
343
|
+
const normalizedTarget = toLower(target);
|
|
344
|
+
const normalizedQuery = toLower(query);
|
|
345
|
+
if (match === "prefix") {
|
|
346
|
+
return normalizedTarget.startsWith(normalizedQuery) ? 0 : -1;
|
|
347
|
+
}
|
|
348
|
+
return normalizedTarget.indexOf(normalizedQuery);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Content-aware variant of findMatchIndex for searching within file text.
|
|
352
|
+
* Unlike findMatchIndex (designed for short identifiers/paths), this handles:
|
|
353
|
+
* - exact: case-sensitive substring search (indexOf)
|
|
354
|
+
* - prefix: case-insensitive substring search (same as contains for content)
|
|
355
|
+
* - contains: case-insensitive substring search
|
|
356
|
+
* - regex: delegated to pattern.exec
|
|
357
|
+
*/
|
|
358
|
+
function findContentMatchIndex(content, query, match, pattern) {
|
|
359
|
+
if (!query) {
|
|
360
|
+
return -1;
|
|
361
|
+
}
|
|
362
|
+
if (match === "regex") {
|
|
363
|
+
if (!pattern) {
|
|
364
|
+
return -1;
|
|
365
|
+
}
|
|
366
|
+
pattern.lastIndex = 0;
|
|
367
|
+
const result = pattern.exec(content);
|
|
368
|
+
return result?.index ?? -1;
|
|
369
|
+
}
|
|
370
|
+
if (match === "exact") {
|
|
371
|
+
return content.indexOf(query);
|
|
372
|
+
}
|
|
373
|
+
const normalizedContent = toLower(content);
|
|
374
|
+
const normalizedQuery = toLower(query);
|
|
375
|
+
return normalizedContent.indexOf(normalizedQuery);
|
|
376
|
+
}
|
|
377
|
+
function scoreSymbolMatch(match, index, symbolKind) {
|
|
378
|
+
const matchBase = match === "exact" ? 350 : match === "prefix" ? 310 : match === "contains" ? 270 : 250;
|
|
379
|
+
const kindBonus = symbolKind === "class" || symbolKind === "interface" || symbolKind === "record"
|
|
380
|
+
? 25
|
|
381
|
+
: symbolKind === "enum"
|
|
382
|
+
? 20
|
|
383
|
+
: symbolKind === "method"
|
|
384
|
+
? 15
|
|
385
|
+
: 8;
|
|
386
|
+
return matchBase + kindBonus + Math.max(0, 80 - Math.min(80, index));
|
|
387
|
+
}
|
|
388
|
+
function scoreTextMatch(match, index) {
|
|
389
|
+
const matchBase = match === "exact" ? 280 : match === "prefix" ? 250 : match === "contains" ? 220 : 200;
|
|
390
|
+
return matchBase + Math.max(0, 90 - Math.min(90, Math.floor(index / 2)));
|
|
391
|
+
}
|
|
392
|
+
function scorePathMatch(match, index) {
|
|
393
|
+
const matchBase = match === "exact" ? 260 : match === "prefix" ? 230 : match === "contains" ? 210 : 190;
|
|
394
|
+
return matchBase + Math.max(0, 100 - Math.min(100, index));
|
|
395
|
+
}
|
|
396
|
+
function matchRegexIndex(target, regex) {
|
|
397
|
+
regex.lastIndex = 0;
|
|
398
|
+
const result = regex.exec(target);
|
|
399
|
+
return result?.index ?? -1;
|
|
400
|
+
}
|
|
401
|
+
function indexToLine(content, index) {
|
|
402
|
+
if (index <= 0) {
|
|
403
|
+
return 1;
|
|
404
|
+
}
|
|
405
|
+
return content.slice(0, index).split(/\r?\n/).length;
|
|
406
|
+
}
|
|
407
|
+
function lineToSymbol(symbol) {
|
|
408
|
+
if (!isSymbolKind(symbol.symbolKind)) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
symbolKind: symbol.symbolKind,
|
|
413
|
+
symbolName: symbol.symbolName,
|
|
414
|
+
qualifiedName: symbol.qualifiedName,
|
|
415
|
+
line: symbol.line
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function toContextSnippet(content, centerLineInput, beforeLines, afterLines, withLineNumbers) {
|
|
419
|
+
const lines = content.split(/\r?\n/);
|
|
420
|
+
const centerLine = Math.min(Math.max(1, centerLineInput), Math.max(lines.length, 1));
|
|
421
|
+
const requestedStart = Math.max(1, centerLine - beforeLines);
|
|
422
|
+
const requestedEnd = centerLine + afterLines;
|
|
423
|
+
const startLine = Math.min(requestedStart, Math.max(lines.length, 1));
|
|
424
|
+
const endLine = Math.min(requestedEnd, Math.max(lines.length, 1));
|
|
425
|
+
const snippetLines = lines.slice(startLine - 1, endLine);
|
|
426
|
+
const snippet = withLineNumbers
|
|
427
|
+
? snippetLines.map((line, index) => `${startLine + index}: ${line}`).join("\n")
|
|
428
|
+
: snippetLines.join("\n");
|
|
429
|
+
return {
|
|
430
|
+
startLine,
|
|
431
|
+
endLine,
|
|
432
|
+
snippet,
|
|
433
|
+
truncated: requestedStart !== startLine || requestedEnd !== endLine
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function chunkArray(items, chunkSize) {
|
|
437
|
+
const size = Math.max(1, Math.trunc(chunkSize));
|
|
438
|
+
if (items.length === 0) {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
const chunks = [];
|
|
442
|
+
for (let index = 0; index < items.length; index += size) {
|
|
443
|
+
chunks.push(items.slice(index, index + size));
|
|
444
|
+
}
|
|
445
|
+
return chunks;
|
|
446
|
+
}
|
|
447
|
+
export class SourceService {
|
|
448
|
+
config;
|
|
449
|
+
db;
|
|
450
|
+
artifactsRepo;
|
|
451
|
+
filesRepo;
|
|
452
|
+
indexMetaRepo;
|
|
453
|
+
symbolsRepo;
|
|
454
|
+
metrics;
|
|
455
|
+
versionService;
|
|
456
|
+
mappingService;
|
|
457
|
+
workspaceMappingService;
|
|
458
|
+
explorerService;
|
|
459
|
+
registryService;
|
|
460
|
+
versionDiffService;
|
|
461
|
+
modDecompileService;
|
|
462
|
+
modSearchService;
|
|
463
|
+
constructor(explicitConfig, metrics = new RuntimeMetrics()) {
|
|
464
|
+
this.config = explicitConfig ?? loadConfig();
|
|
465
|
+
this.metrics = metrics;
|
|
466
|
+
this.versionService = new VersionService(this.config);
|
|
467
|
+
this.mappingService = new MappingService(this.config, this.versionService);
|
|
468
|
+
this.workspaceMappingService = new WorkspaceMappingService();
|
|
469
|
+
this.explorerService = new MinecraftExplorerService(this.config);
|
|
470
|
+
this.registryService = new RegistryService(this.config, this.versionService);
|
|
471
|
+
this.versionDiffService = new VersionDiffService(this.config, this.versionService, this.registryService);
|
|
472
|
+
this.modDecompileService = new ModDecompileService(this.config);
|
|
473
|
+
this.modSearchService = new ModSearchService(this.modDecompileService);
|
|
474
|
+
const initialized = openDatabase(this.config);
|
|
475
|
+
this.db = initialized.db;
|
|
476
|
+
this.artifactsRepo = new ArtifactsRepo(this.db);
|
|
477
|
+
this.filesRepo = new FilesRepo(this.db);
|
|
478
|
+
this.indexMetaRepo = new IndexMetaRepo(this.db);
|
|
479
|
+
this.symbolsRepo = new SymbolsRepo(this.db);
|
|
480
|
+
this.refreshCacheMetrics();
|
|
481
|
+
}
|
|
482
|
+
async resolveArtifact(input) {
|
|
483
|
+
const kind = input.target.kind;
|
|
484
|
+
const value = input.target.value?.trim();
|
|
485
|
+
const mapping = normalizeMapping(input.mapping);
|
|
486
|
+
const warnings = [];
|
|
487
|
+
if (!value) {
|
|
488
|
+
throw createError({
|
|
489
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
490
|
+
message: "target.value must be non-empty.",
|
|
491
|
+
details: { target: input.target }
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
if (kind !== "jar" && kind !== "coordinate" && kind !== "version") {
|
|
495
|
+
throw createError({
|
|
496
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
497
|
+
message: `Unsupported target kind "${kind}".`,
|
|
498
|
+
details: { target: input.target }
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
if (kind === "jar" && !value.toLowerCase().endsWith(".jar")) {
|
|
502
|
+
throw createError({
|
|
503
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
504
|
+
message: "target.kind=jar requires a .jar path.",
|
|
505
|
+
details: { target: input.target }
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
const startedAt = Date.now();
|
|
509
|
+
try {
|
|
510
|
+
let resolvedTarget = { kind, value };
|
|
511
|
+
let resolvedVersion;
|
|
512
|
+
if (kind === "version") {
|
|
513
|
+
const versionJar = await this.versionService.resolveVersionJar(value);
|
|
514
|
+
resolvedVersion = versionJar.version;
|
|
515
|
+
resolvedTarget = {
|
|
516
|
+
kind: "jar",
|
|
517
|
+
value: versionJar.jarPath
|
|
518
|
+
};
|
|
519
|
+
warnings.push(`Resolved Minecraft ${versionJar.version} from ${versionJar.clientJarUrl}.`);
|
|
520
|
+
}
|
|
521
|
+
if (kind === "coordinate") {
|
|
522
|
+
try {
|
|
523
|
+
resolvedVersion = parseCoordinate(value).version;
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
// coordinate validity is validated by resolver, keep version undefined on parse failure.
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Unobfuscated versions (MC 26.1+) ship with official names; intermediary/yarn are not applicable.
|
|
530
|
+
let effectiveMapping = mapping;
|
|
531
|
+
if ((mapping === "intermediary" || mapping === "yarn") &&
|
|
532
|
+
resolvedVersion &&
|
|
533
|
+
isUnobfuscatedVersion(resolvedVersion)) {
|
|
534
|
+
warnings.push(`Version ${resolvedVersion} is unobfuscated; ${mapping} mappings are not applicable. Using official names.`);
|
|
535
|
+
effectiveMapping = "official";
|
|
536
|
+
}
|
|
537
|
+
const resolved = await resolveSourceTargetInternal(resolvedTarget, {
|
|
538
|
+
// mojang requires source-backed artifact guarantee; force resolution to consider decompile candidate
|
|
539
|
+
// and reject later if mapping cannot be applied.
|
|
540
|
+
allowDecompile: effectiveMapping === "mojang" ? true : input.allowDecompile ?? true,
|
|
541
|
+
onRepoFailover: (event) => {
|
|
542
|
+
this.metrics.recordRepoFailover();
|
|
543
|
+
log("warn", "repo.failover", {
|
|
544
|
+
stage: event.stage,
|
|
545
|
+
repoUrl: event.repoUrl,
|
|
546
|
+
statusCode: event.statusCode,
|
|
547
|
+
reason: event.reason,
|
|
548
|
+
attempt: event.attempt,
|
|
549
|
+
totalAttempts: event.totalAttempts
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}, this.config);
|
|
553
|
+
resolved.version = resolvedVersion;
|
|
554
|
+
const mappingDecision = applyMappingPipeline({
|
|
555
|
+
requestedMapping: effectiveMapping,
|
|
556
|
+
target: { kind, value },
|
|
557
|
+
resolved
|
|
558
|
+
});
|
|
559
|
+
const additionalTransformChain = [];
|
|
560
|
+
if (effectiveMapping === "intermediary" || effectiveMapping === "yarn") {
|
|
561
|
+
if (!resolved.version) {
|
|
562
|
+
throw createError({
|
|
563
|
+
code: ERROR_CODES.MAPPING_NOT_APPLIED,
|
|
564
|
+
message: `Requested ${effectiveMapping} mapping cannot be guaranteed because artifact version is unknown.`,
|
|
565
|
+
details: {
|
|
566
|
+
mapping: effectiveMapping,
|
|
567
|
+
target: { kind, value },
|
|
568
|
+
nextAction: "Use targetKind=version or a versioned Maven coordinate so mapping artifacts can be resolved."
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
const mappingAvailability = await this.mappingService.ensureMappingAvailable({
|
|
573
|
+
version: resolved.version,
|
|
574
|
+
sourceMapping: "official",
|
|
575
|
+
targetMapping: effectiveMapping,
|
|
576
|
+
sourcePriority: input.sourcePriority
|
|
577
|
+
});
|
|
578
|
+
additionalTransformChain.push(...mappingAvailability.transformChain);
|
|
579
|
+
if (mappingAvailability.warnings.length > 0) {
|
|
580
|
+
warnings.push(...mappingAvailability.warnings);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const provenance = this.buildProvenance({
|
|
584
|
+
requestedTarget: { kind, value },
|
|
585
|
+
resolved,
|
|
586
|
+
transformChain: [...mappingDecision.transformChain, ...additionalTransformChain]
|
|
587
|
+
});
|
|
588
|
+
resolved.requestedMapping = effectiveMapping;
|
|
589
|
+
resolved.mappingApplied = mappingDecision.mappingApplied;
|
|
590
|
+
resolved.provenance = provenance;
|
|
591
|
+
resolved.qualityFlags = mappingDecision.qualityFlags;
|
|
592
|
+
await this.ingestIfNeeded(resolved);
|
|
593
|
+
return {
|
|
594
|
+
artifactId: resolved.artifactId,
|
|
595
|
+
origin: resolved.origin,
|
|
596
|
+
isDecompiled: resolved.isDecompiled,
|
|
597
|
+
resolvedSourceJarPath: resolved.sourceJarPath,
|
|
598
|
+
adjacentSourceCandidates: resolved.adjacentSourceCandidates,
|
|
599
|
+
binaryJarPath: resolved.binaryJarPath,
|
|
600
|
+
coordinate: resolved.coordinate,
|
|
601
|
+
version: resolved.version,
|
|
602
|
+
requestedMapping: effectiveMapping,
|
|
603
|
+
mappingApplied: mappingDecision.mappingApplied,
|
|
604
|
+
provenance,
|
|
605
|
+
qualityFlags: mappingDecision.qualityFlags,
|
|
606
|
+
repoUrl: resolved.repoUrl,
|
|
607
|
+
warnings
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
catch (caughtError) {
|
|
611
|
+
if (isAppError(caughtError)) {
|
|
612
|
+
throw caughtError;
|
|
613
|
+
}
|
|
614
|
+
throw createError({
|
|
615
|
+
code: ERROR_CODES.ARTIFACT_RESOLUTION_FAILED,
|
|
616
|
+
message: "Failed to resolve artifact.",
|
|
617
|
+
details: {
|
|
618
|
+
target: input.target,
|
|
619
|
+
mapping,
|
|
620
|
+
reason: caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
finally {
|
|
625
|
+
this.metrics.recordDuration("resolve_duration_ms", Date.now() - startedAt);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
async searchClassSource(input) {
|
|
629
|
+
const startedAt = Date.now();
|
|
630
|
+
try {
|
|
631
|
+
const artifact = this.getArtifact(input.artifactId);
|
|
632
|
+
const query = input.query.trim();
|
|
633
|
+
if (!query) {
|
|
634
|
+
return {
|
|
635
|
+
hits: [],
|
|
636
|
+
totalApprox: 0,
|
|
637
|
+
mappingApplied: artifact.mappingApplied ?? "official"
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const intent = normalizeIntent(input.intent);
|
|
641
|
+
const match = normalizeMatch(input.match);
|
|
642
|
+
if (match === "regex" && query.length > MAX_REGEX_QUERY_LENGTH) {
|
|
643
|
+
throw createError({
|
|
644
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
645
|
+
message: `Regex query exceeds max length of ${MAX_REGEX_QUERY_LENGTH} characters.`,
|
|
646
|
+
details: {
|
|
647
|
+
queryLength: query.length,
|
|
648
|
+
maxLength: MAX_REGEX_QUERY_LENGTH
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
const searchLimitCap = match === "regex"
|
|
653
|
+
? Math.max(1, Math.min(this.config.maxSearchHits, MAX_REGEX_RESULT_LIMIT))
|
|
654
|
+
: this.config.maxSearchHits;
|
|
655
|
+
const limit = clampLimit(input.limit, 20, searchLimitCap);
|
|
656
|
+
const includeDefinition = input.include?.includeDefinition ?? false;
|
|
657
|
+
const includeOneHop = input.include?.includeOneHop ?? false;
|
|
658
|
+
const snippetWindow = buildSnippetWindow(input.include?.snippetLines);
|
|
659
|
+
const regexPattern = match === "regex" ? compileRegex(query) : undefined;
|
|
660
|
+
const scope = input.scope;
|
|
661
|
+
const cursorContext = buildSearchCursorContext({
|
|
662
|
+
artifactId: artifact.artifactId,
|
|
663
|
+
query,
|
|
664
|
+
intent,
|
|
665
|
+
match,
|
|
666
|
+
scope,
|
|
667
|
+
includeDefinition
|
|
668
|
+
});
|
|
669
|
+
const decodedCursor = decodeSearchCursor(input.cursor);
|
|
670
|
+
const cursor = decodedCursor?.contextKey === cursorContext ? decodedCursor : undefined;
|
|
671
|
+
const accumulator = createSearchHitAccumulator(limit, cursor);
|
|
672
|
+
const indexedSearchEnabled = this.config.indexedSearchEnabled !== false;
|
|
673
|
+
if (match === "regex") {
|
|
674
|
+
this.metrics.recordSearchRegexFallback();
|
|
675
|
+
}
|
|
676
|
+
const intentStartedAt = Date.now();
|
|
677
|
+
const recordHit = (hit) => {
|
|
678
|
+
accumulator.add(hit);
|
|
679
|
+
};
|
|
680
|
+
if (intent === "symbol") {
|
|
681
|
+
this.searchSymbolIntent(artifact.artifactId, query, match, scope, snippetWindow, regexPattern, recordHit);
|
|
682
|
+
// WS4: Use repo-level COUNT for symbol totalApprox when not regex
|
|
683
|
+
if (match !== "regex") {
|
|
684
|
+
const approxCount = this.symbolsRepo.countScopedSymbols({
|
|
685
|
+
artifactId: artifact.artifactId,
|
|
686
|
+
query,
|
|
687
|
+
match,
|
|
688
|
+
symbolKind: scope?.symbolKind,
|
|
689
|
+
packagePrefix: scope?.packagePrefix
|
|
690
|
+
});
|
|
691
|
+
accumulator.setTotalApproxOverride(approxCount);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
else if (!indexedSearchEnabled) {
|
|
695
|
+
this.metrics.recordIndexedDisabled();
|
|
696
|
+
this.metrics.recordSearchFallback();
|
|
697
|
+
if (intent === "path") {
|
|
698
|
+
this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
else if (canUseIndexedSearchPath(indexedSearchEnabled, intent, match, scope)) {
|
|
705
|
+
try {
|
|
706
|
+
if (intent === "path") {
|
|
707
|
+
this.searchPathIntentIndexed(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, recordHit);
|
|
708
|
+
// WS4: Use repo-level COUNT for totalApprox instead of accumulator count
|
|
709
|
+
const approxCount = this.filesRepo.countPathCandidates(artifact.artifactId, query);
|
|
710
|
+
accumulator.setTotalApproxOverride(approxCount);
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
this.searchTextIntentIndexed(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, recordHit);
|
|
714
|
+
// WS4: Use repo-level COUNT for totalApprox instead of accumulator count
|
|
715
|
+
const approxCount = this.filesRepo.countTextCandidates(artifact.artifactId, query);
|
|
716
|
+
accumulator.setTotalApproxOverride(approxCount);
|
|
717
|
+
}
|
|
718
|
+
this.metrics.recordSearchIndexedHit();
|
|
719
|
+
}
|
|
720
|
+
catch (caughtError) {
|
|
721
|
+
this.metrics.recordSearchFallback();
|
|
722
|
+
log("warn", "search.indexed_fallback", {
|
|
723
|
+
artifactId: artifact.artifactId,
|
|
724
|
+
intent,
|
|
725
|
+
match,
|
|
726
|
+
reason: caughtError instanceof Error ? caughtError.message : String(caughtError)
|
|
727
|
+
});
|
|
728
|
+
if (intent === "path") {
|
|
729
|
+
this.searchPathIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
this.searchTextIntent(artifact.artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, recordHit);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
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
|
+
this.metrics.recordSearchIntentDuration(intent, Date.now() - intentStartedAt);
|
|
746
|
+
const finalizedHits = accumulator.finalize();
|
|
747
|
+
const page = finalizedHits.page;
|
|
748
|
+
this.metrics.recordSearchRowsReturned(page.length);
|
|
749
|
+
const nextCursor = finalizedHits.nextCursorHit
|
|
750
|
+
? encodeSearchCursor(finalizedHits.nextCursorHit, cursorContext)
|
|
751
|
+
: undefined;
|
|
752
|
+
const relations = includeOneHop
|
|
753
|
+
? this.buildOneHopRelations(artifact.artifactId, page.flatMap((hit) => hit.symbol && isSymbolKind(hit.symbol.symbolKind)
|
|
754
|
+
? [
|
|
755
|
+
{
|
|
756
|
+
symbolKind: hit.symbol.symbolKind,
|
|
757
|
+
symbolName: hit.symbol.symbolName,
|
|
758
|
+
filePath: hit.filePath,
|
|
759
|
+
line: hit.symbol.line
|
|
760
|
+
}
|
|
761
|
+
]
|
|
762
|
+
: []), 10)
|
|
763
|
+
: undefined;
|
|
764
|
+
if (relations?.length) {
|
|
765
|
+
this.metrics.recordOneHopExpansion(relations.length);
|
|
766
|
+
}
|
|
767
|
+
this.metrics.recordSearchTokenBytesReturned(Buffer.byteLength(JSON.stringify({ hits: page, relations }), "utf8"));
|
|
768
|
+
return {
|
|
769
|
+
hits: page,
|
|
770
|
+
relations: relations && relations.length > 0 ? relations : undefined,
|
|
771
|
+
nextCursor,
|
|
772
|
+
totalApprox: finalizedHits.totalApprox,
|
|
773
|
+
mappingApplied: artifact.mappingApplied ?? "official"
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
finally {
|
|
777
|
+
this.metrics.recordDuration("search_duration_ms", Date.now() - startedAt);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
async getArtifactFile(input) {
|
|
781
|
+
const startedAt = Date.now();
|
|
782
|
+
try {
|
|
783
|
+
const artifact = this.getArtifact(input.artifactId);
|
|
784
|
+
const row = this.filesRepo.getFileContent(artifact.artifactId, normalizePathStyle(input.filePath));
|
|
785
|
+
if (!row) {
|
|
786
|
+
throw createError({
|
|
787
|
+
code: ERROR_CODES.FILE_NOT_FOUND,
|
|
788
|
+
message: `Source file "${input.filePath}" was not found.`,
|
|
789
|
+
details: { artifactId: input.artifactId, filePath: input.filePath }
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
const maxBytes = clampLimit(input.maxBytes, this.config.maxContentBytes, Number.MAX_SAFE_INTEGER);
|
|
793
|
+
const fullBytes = Buffer.byteLength(row.content, "utf8");
|
|
794
|
+
const truncated = fullBytes > maxBytes;
|
|
795
|
+
const content = truncated
|
|
796
|
+
? Buffer.from(row.content, "utf8").slice(0, maxBytes).toString("utf8")
|
|
797
|
+
: row.content;
|
|
798
|
+
if (truncated) {
|
|
799
|
+
log("warn", "source.get_file.truncated", {
|
|
800
|
+
artifactId: input.artifactId,
|
|
801
|
+
filePath: input.filePath,
|
|
802
|
+
maxBytes,
|
|
803
|
+
returnedBytes: Buffer.byteLength(content, "utf8"),
|
|
804
|
+
fullBytes
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
filePath: row.filePath,
|
|
809
|
+
content,
|
|
810
|
+
contentBytes: fullBytes,
|
|
811
|
+
truncated,
|
|
812
|
+
mappingApplied: artifact.mappingApplied ?? "official"
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
finally {
|
|
816
|
+
this.metrics.recordDuration("get_file_duration_ms", Date.now() - startedAt);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async listArtifactFiles(input) {
|
|
820
|
+
const startedAt = Date.now();
|
|
821
|
+
try {
|
|
822
|
+
const artifact = this.getArtifact(input.artifactId);
|
|
823
|
+
const limit = clampLimit(input.limit, 200, 2000);
|
|
824
|
+
const page = this.filesRepo.listFiles(artifact.artifactId, {
|
|
825
|
+
limit,
|
|
826
|
+
cursor: input.cursor,
|
|
827
|
+
prefix: input.prefix
|
|
828
|
+
});
|
|
829
|
+
return {
|
|
830
|
+
items: page.items,
|
|
831
|
+
nextCursor: page.nextCursor,
|
|
832
|
+
mappingApplied: artifact.mappingApplied ?? "official"
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
finally {
|
|
836
|
+
this.metrics.recordDuration("list_files_duration_ms", Date.now() - startedAt);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
async listVersions(input = {}) {
|
|
840
|
+
return this.versionService.listVersions(input);
|
|
841
|
+
}
|
|
842
|
+
async getRegistryData(input) {
|
|
843
|
+
return this.registryService.getRegistryData(input);
|
|
844
|
+
}
|
|
845
|
+
async compareVersions(input) {
|
|
846
|
+
return this.versionDiffService.compareVersions(input);
|
|
847
|
+
}
|
|
848
|
+
async decompileModJar(input) {
|
|
849
|
+
return this.modDecompileService.decompileModJar(input);
|
|
850
|
+
}
|
|
851
|
+
async getModClassSource(input) {
|
|
852
|
+
return this.modDecompileService.getModClassSource(input);
|
|
853
|
+
}
|
|
854
|
+
async searchModSource(input) {
|
|
855
|
+
return this.modSearchService.searchModSource(input);
|
|
856
|
+
}
|
|
857
|
+
async findMapping(input) {
|
|
858
|
+
return this.mappingService.findMapping(input);
|
|
859
|
+
}
|
|
860
|
+
async resolveMethodMappingExact(input) {
|
|
861
|
+
return this.mappingService.resolveMethodMappingExact(input);
|
|
862
|
+
}
|
|
863
|
+
async getClassApiMatrix(input) {
|
|
864
|
+
return this.mappingService.getClassApiMatrix(input);
|
|
865
|
+
}
|
|
866
|
+
async checkSymbolExists(input) {
|
|
867
|
+
return this.mappingService.checkSymbolExists(input);
|
|
868
|
+
}
|
|
869
|
+
async resolveWorkspaceSymbol(input) {
|
|
870
|
+
const projectPath = input.projectPath?.trim();
|
|
871
|
+
const version = input.version?.trim();
|
|
872
|
+
const kind = input.kind;
|
|
873
|
+
const name = input.name?.trim();
|
|
874
|
+
const owner = input.owner?.trim();
|
|
875
|
+
const descriptor = input.descriptor?.trim();
|
|
876
|
+
if (!projectPath || !version || !name) {
|
|
877
|
+
throw createError({
|
|
878
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
879
|
+
message: "projectPath, version, and name must be non-empty strings.",
|
|
880
|
+
details: {
|
|
881
|
+
projectPath: input.projectPath,
|
|
882
|
+
version: input.version,
|
|
883
|
+
name: input.name
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
if (kind !== "class" && kind !== "field" && kind !== "method") {
|
|
888
|
+
throw createError({
|
|
889
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
890
|
+
message: `Unsupported symbol kind "${kind}".`,
|
|
891
|
+
details: {
|
|
892
|
+
kind
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
if (kind === "class") {
|
|
897
|
+
if (owner) {
|
|
898
|
+
throw createError({
|
|
899
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
900
|
+
message: "owner is not allowed when kind=class. Use name as FQCN.",
|
|
901
|
+
details: {
|
|
902
|
+
owner: input.owner,
|
|
903
|
+
name: input.name
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
if (descriptor) {
|
|
908
|
+
throw createError({
|
|
909
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
910
|
+
message: "descriptor is not allowed when kind=class.",
|
|
911
|
+
details: {
|
|
912
|
+
descriptor: input.descriptor
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
else if (!owner) {
|
|
918
|
+
throw createError({
|
|
919
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
920
|
+
message: "owner is required when kind is field or method.",
|
|
921
|
+
details: {
|
|
922
|
+
kind,
|
|
923
|
+
owner: input.owner
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
if (kind === "field" && descriptor) {
|
|
928
|
+
throw createError({
|
|
929
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
930
|
+
message: "descriptor is not allowed when kind=field.",
|
|
931
|
+
details: {
|
|
932
|
+
descriptor: input.descriptor
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
if (kind === "method" && !descriptor) {
|
|
937
|
+
throw createError({
|
|
938
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
939
|
+
message: "descriptor is required when kind=method."
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
const querySymbol = kind === "class"
|
|
943
|
+
? {
|
|
944
|
+
kind,
|
|
945
|
+
name: name.replace(/\//g, "."),
|
|
946
|
+
symbol: name.replace(/\//g, ".")
|
|
947
|
+
}
|
|
948
|
+
: {
|
|
949
|
+
kind,
|
|
950
|
+
name,
|
|
951
|
+
owner: owner?.replace(/\//g, "."),
|
|
952
|
+
descriptor: kind === "method" ? descriptor : undefined,
|
|
953
|
+
symbol: `${owner?.replace(/\//g, ".")}.${name}${kind === "method" ? descriptor : ""}`
|
|
954
|
+
};
|
|
955
|
+
const sourcePriorityApplied = input.sourcePriority ?? this.config.mappingSourcePriority;
|
|
956
|
+
const workspaceDetection = await this.workspaceMappingService.detectCompileMapping({
|
|
957
|
+
projectPath
|
|
958
|
+
});
|
|
959
|
+
const warnings = [...workspaceDetection.warnings];
|
|
960
|
+
if (!workspaceDetection.resolved || !workspaceDetection.mappingApplied) {
|
|
961
|
+
return {
|
|
962
|
+
querySymbol,
|
|
963
|
+
mappingContext: {
|
|
964
|
+
version,
|
|
965
|
+
sourceMapping: input.sourceMapping,
|
|
966
|
+
sourcePriorityApplied
|
|
967
|
+
},
|
|
968
|
+
resolved: false,
|
|
969
|
+
status: "mapping_unavailable",
|
|
970
|
+
candidates: [],
|
|
971
|
+
workspaceDetection,
|
|
972
|
+
warnings
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
const mappingApplied = workspaceDetection.mappingApplied;
|
|
976
|
+
if (kind === "method") {
|
|
977
|
+
const exact = await this.mappingService.resolveMethodMappingExact({
|
|
978
|
+
version,
|
|
979
|
+
kind: "method",
|
|
980
|
+
owner,
|
|
981
|
+
name,
|
|
982
|
+
descriptor: descriptor,
|
|
983
|
+
sourceMapping: input.sourceMapping,
|
|
984
|
+
targetMapping: mappingApplied,
|
|
985
|
+
sourcePriority: input.sourcePriority
|
|
986
|
+
});
|
|
987
|
+
return {
|
|
988
|
+
...exact,
|
|
989
|
+
workspaceDetection,
|
|
990
|
+
warnings: [...warnings, ...exact.warnings]
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
if (kind === "class") {
|
|
994
|
+
const className = name.replace(/\//g, ".");
|
|
995
|
+
const matrix = await this.mappingService.getClassApiMatrix({
|
|
996
|
+
version,
|
|
997
|
+
className,
|
|
998
|
+
classNameMapping: input.sourceMapping,
|
|
999
|
+
includeKinds: ["class"],
|
|
1000
|
+
sourcePriority: input.sourcePriority
|
|
1001
|
+
});
|
|
1002
|
+
const resolvedClass = matrix.classIdentity[mappingApplied];
|
|
1003
|
+
if (!resolvedClass) {
|
|
1004
|
+
return {
|
|
1005
|
+
querySymbol,
|
|
1006
|
+
mappingContext: {
|
|
1007
|
+
version,
|
|
1008
|
+
sourceMapping: input.sourceMapping,
|
|
1009
|
+
targetMapping: mappingApplied,
|
|
1010
|
+
sourcePriorityApplied
|
|
1011
|
+
},
|
|
1012
|
+
resolved: false,
|
|
1013
|
+
status: "not_found",
|
|
1014
|
+
candidates: [],
|
|
1015
|
+
workspaceDetection,
|
|
1016
|
+
warnings: [...warnings, ...matrix.warnings]
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
const normalizedClass = resolvedClass.replace(/\//g, ".");
|
|
1020
|
+
const resolvedSymbol = {
|
|
1021
|
+
kind: "class",
|
|
1022
|
+
name: normalizedClass,
|
|
1023
|
+
symbol: normalizedClass
|
|
1024
|
+
};
|
|
1025
|
+
const resolvedCandidate = {
|
|
1026
|
+
...resolvedSymbol,
|
|
1027
|
+
matchKind: "exact",
|
|
1028
|
+
confidence: 1
|
|
1029
|
+
};
|
|
1030
|
+
return {
|
|
1031
|
+
querySymbol,
|
|
1032
|
+
mappingContext: {
|
|
1033
|
+
version,
|
|
1034
|
+
sourceMapping: input.sourceMapping,
|
|
1035
|
+
targetMapping: mappingApplied,
|
|
1036
|
+
sourcePriorityApplied
|
|
1037
|
+
},
|
|
1038
|
+
resolved: true,
|
|
1039
|
+
status: "resolved",
|
|
1040
|
+
resolvedSymbol,
|
|
1041
|
+
candidates: [resolvedCandidate],
|
|
1042
|
+
workspaceDetection,
|
|
1043
|
+
warnings: [...warnings, ...matrix.warnings]
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
const mapped = await this.mappingService.findMapping({
|
|
1047
|
+
version,
|
|
1048
|
+
kind,
|
|
1049
|
+
name,
|
|
1050
|
+
owner,
|
|
1051
|
+
descriptor,
|
|
1052
|
+
sourceMapping: input.sourceMapping,
|
|
1053
|
+
targetMapping: mappingApplied,
|
|
1054
|
+
sourcePriority: input.sourcePriority
|
|
1055
|
+
});
|
|
1056
|
+
const filtered = mapped.candidates.filter((candidate) => candidate.kind === kind);
|
|
1057
|
+
let status;
|
|
1058
|
+
if (mapped.status === "mapping_unavailable") {
|
|
1059
|
+
status = "mapping_unavailable";
|
|
1060
|
+
}
|
|
1061
|
+
else if (filtered.length === 1) {
|
|
1062
|
+
status = "resolved";
|
|
1063
|
+
}
|
|
1064
|
+
else if (filtered.length > 1) {
|
|
1065
|
+
status = "ambiguous";
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
status = "not_found";
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
querySymbol: mapped.querySymbol,
|
|
1072
|
+
mappingContext: mapped.mappingContext,
|
|
1073
|
+
resolved: status === "resolved",
|
|
1074
|
+
status,
|
|
1075
|
+
resolvedSymbol: status === "resolved" ? filtered[0] : undefined,
|
|
1076
|
+
candidates: filtered,
|
|
1077
|
+
workspaceDetection,
|
|
1078
|
+
warnings: [...warnings, ...mapped.warnings]
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
async traceSymbolLifecycle(input) {
|
|
1082
|
+
const mapping = normalizeMapping(input.mapping);
|
|
1083
|
+
const { className: userClassName, methodName: userMethodName } = parseQualifiedMethodSymbol(input.symbol);
|
|
1084
|
+
const descriptor = normalizeOptionalString(input.descriptor);
|
|
1085
|
+
const includeTimeline = input.includeTimeline ?? false;
|
|
1086
|
+
const includeSnapshots = input.includeSnapshots ?? false;
|
|
1087
|
+
const maxVersions = clampLimit(input.maxVersions, 120, 400);
|
|
1088
|
+
const manifestOrder = await this.versionService.listVersionIds({ includeSnapshots });
|
|
1089
|
+
if (manifestOrder.length === 0) {
|
|
1090
|
+
throw createError({
|
|
1091
|
+
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1092
|
+
message: "No Minecraft versions were returned by manifest.",
|
|
1093
|
+
details: { includeSnapshots }
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
const chronological = [...manifestOrder].reverse();
|
|
1097
|
+
const requestedFrom = normalizeOptionalString(input.fromVersion) ?? chronological[0];
|
|
1098
|
+
const requestedTo = normalizeOptionalString(input.toVersion) ?? chronological[chronological.length - 1];
|
|
1099
|
+
const fromIndex = chronological.indexOf(requestedFrom);
|
|
1100
|
+
const toIndex = chronological.indexOf(requestedTo);
|
|
1101
|
+
if (fromIndex < 0) {
|
|
1102
|
+
throw createError({
|
|
1103
|
+
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1104
|
+
message: `fromVersion "${requestedFrom}" was not found in manifest.`,
|
|
1105
|
+
details: { fromVersion: requestedFrom }
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
if (toIndex < 0) {
|
|
1109
|
+
throw createError({
|
|
1110
|
+
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1111
|
+
message: `toVersion "${requestedTo}" was not found in manifest.`,
|
|
1112
|
+
details: { toVersion: requestedTo }
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
if (fromIndex > toIndex) {
|
|
1116
|
+
throw createError({
|
|
1117
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1118
|
+
message: "fromVersion must be older than or equal to toVersion.",
|
|
1119
|
+
details: { fromVersion: requestedFrom, toVersion: requestedTo }
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
let selectedVersions = chronological.slice(fromIndex, toIndex + 1);
|
|
1123
|
+
const warnings = [];
|
|
1124
|
+
if (selectedVersions.length > maxVersions) {
|
|
1125
|
+
selectedVersions = selectedVersions.slice(selectedVersions.length - maxVersions);
|
|
1126
|
+
warnings.push(`Version scan truncated to ${maxVersions} entries. Effective fromVersion is now "${selectedVersions[0]}".`);
|
|
1127
|
+
}
|
|
1128
|
+
const resolvedSymbolsByVersion = new Map();
|
|
1129
|
+
const scanned = [];
|
|
1130
|
+
for (const version of selectedVersions) {
|
|
1131
|
+
try {
|
|
1132
|
+
let resolvedSymbols = resolvedSymbolsByVersion.get(version);
|
|
1133
|
+
if (!resolvedSymbols) {
|
|
1134
|
+
const [officialClassName, officialMethod] = await Promise.all([
|
|
1135
|
+
this.resolveToOfficialClassName(userClassName, version, mapping, input.sourcePriority, warnings),
|
|
1136
|
+
this.resolveToOfficialMemberName(userMethodName, userClassName, descriptor, "method", version, mapping, input.sourcePriority, warnings)
|
|
1137
|
+
]);
|
|
1138
|
+
resolvedSymbols = {
|
|
1139
|
+
className: officialClassName,
|
|
1140
|
+
methodName: officialMethod.name,
|
|
1141
|
+
methodDescriptor: officialMethod.descriptor
|
|
1142
|
+
};
|
|
1143
|
+
resolvedSymbolsByVersion.set(version, resolvedSymbols);
|
|
1144
|
+
}
|
|
1145
|
+
const resolvedJar = await this.versionService.resolveVersionJar(version);
|
|
1146
|
+
const signature = await this.explorerService.getSignature({
|
|
1147
|
+
fqn: resolvedSymbols.className,
|
|
1148
|
+
jarPath: resolvedJar.jarPath,
|
|
1149
|
+
access: "all",
|
|
1150
|
+
includeSynthetic: true
|
|
1151
|
+
});
|
|
1152
|
+
const sameNameMethods = signature.methods.filter((method) => method.name === resolvedSymbols.methodName);
|
|
1153
|
+
const matchesDescriptor = (resolvedSymbols.methodDescriptor ?? descriptor)
|
|
1154
|
+
? sameNameMethods.some((method) => method.jvmDescriptor === (resolvedSymbols.methodDescriptor ?? descriptor))
|
|
1155
|
+
: sameNameMethods.length > 0;
|
|
1156
|
+
const reason = !matchesDescriptor && descriptor && sameNameMethods.length > 0 ? "descriptor-mismatch" : undefined;
|
|
1157
|
+
scanned.push({
|
|
1158
|
+
version,
|
|
1159
|
+
exists: matchesDescriptor,
|
|
1160
|
+
reason,
|
|
1161
|
+
determinate: true
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
catch (caughtError) {
|
|
1165
|
+
if (isAppError(caughtError) && caughtError.code === ERROR_CODES.CLASS_NOT_FOUND) {
|
|
1166
|
+
scanned.push({
|
|
1167
|
+
version,
|
|
1168
|
+
exists: false,
|
|
1169
|
+
reason: "class-not-found",
|
|
1170
|
+
determinate: true
|
|
1171
|
+
});
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
scanned.push({
|
|
1175
|
+
version,
|
|
1176
|
+
exists: false,
|
|
1177
|
+
reason: "unresolved",
|
|
1178
|
+
determinate: false
|
|
1179
|
+
});
|
|
1180
|
+
warnings.push(`Failed to evaluate ${version}: ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
const determinate = scanned.filter((entry) => entry.determinate);
|
|
1184
|
+
const present = determinate.filter((entry) => entry.exists);
|
|
1185
|
+
const firstSeen = present[0]?.version;
|
|
1186
|
+
const lastSeen = present[present.length - 1]?.version;
|
|
1187
|
+
const missingBetween = [];
|
|
1188
|
+
if (firstSeen && lastSeen) {
|
|
1189
|
+
const firstSeenIndex = determinate.findIndex((entry) => entry.version === firstSeen);
|
|
1190
|
+
const lastSeenIndex = determinate.findIndex((entry) => entry.version === lastSeen);
|
|
1191
|
+
for (let index = firstSeenIndex; index <= lastSeenIndex; index += 1) {
|
|
1192
|
+
const entry = determinate[index];
|
|
1193
|
+
if (entry && !entry.exists) {
|
|
1194
|
+
missingBetween.push(entry.version);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const toVersionEntry = scanned.find((entry) => entry.version === selectedVersions[selectedVersions.length - 1]);
|
|
1199
|
+
const existsNow = toVersionEntry?.determinate ? toVersionEntry.exists : false;
|
|
1200
|
+
if (toVersionEntry && !toVersionEntry.determinate) {
|
|
1201
|
+
warnings.push(`Latest requested version "${toVersionEntry.version}" could not be evaluated.`);
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
query: {
|
|
1205
|
+
className: userClassName,
|
|
1206
|
+
methodName: userMethodName,
|
|
1207
|
+
descriptor,
|
|
1208
|
+
mapping
|
|
1209
|
+
},
|
|
1210
|
+
range: {
|
|
1211
|
+
fromVersion: selectedVersions[0],
|
|
1212
|
+
toVersion: selectedVersions[selectedVersions.length - 1],
|
|
1213
|
+
scannedCount: selectedVersions.length
|
|
1214
|
+
},
|
|
1215
|
+
presence: {
|
|
1216
|
+
firstSeen,
|
|
1217
|
+
lastSeen,
|
|
1218
|
+
missingBetween,
|
|
1219
|
+
existsNow
|
|
1220
|
+
},
|
|
1221
|
+
timeline: includeTimeline
|
|
1222
|
+
? scanned.map((entry) => ({
|
|
1223
|
+
version: entry.version,
|
|
1224
|
+
exists: entry.exists,
|
|
1225
|
+
reason: entry.reason
|
|
1226
|
+
}))
|
|
1227
|
+
: undefined,
|
|
1228
|
+
warnings
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
async diffClassSignatures(input) {
|
|
1232
|
+
const className = input.className.trim();
|
|
1233
|
+
const fromVersion = input.fromVersion.trim();
|
|
1234
|
+
const toVersion = input.toVersion.trim();
|
|
1235
|
+
if (!className || !fromVersion || !toVersion) {
|
|
1236
|
+
throw createError({
|
|
1237
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1238
|
+
message: "className, fromVersion, and toVersion must be non-empty strings.",
|
|
1239
|
+
details: {
|
|
1240
|
+
className: input.className,
|
|
1241
|
+
fromVersion: input.fromVersion,
|
|
1242
|
+
toVersion: input.toVersion
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
const mapping = normalizeMapping(input.mapping);
|
|
1247
|
+
const manifestOrder = await this.versionService.listVersionIds();
|
|
1248
|
+
if (manifestOrder.length === 0) {
|
|
1249
|
+
throw createError({
|
|
1250
|
+
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1251
|
+
message: "No Minecraft versions were returned by manifest."
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
const chronological = [...manifestOrder].reverse();
|
|
1255
|
+
const fromIndex = chronological.indexOf(fromVersion);
|
|
1256
|
+
const toIndex = chronological.indexOf(toVersion);
|
|
1257
|
+
if (fromIndex < 0) {
|
|
1258
|
+
throw createError({
|
|
1259
|
+
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1260
|
+
message: `fromVersion "${fromVersion}" was not found in manifest.`,
|
|
1261
|
+
details: { fromVersion }
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
if (toIndex < 0) {
|
|
1265
|
+
throw createError({
|
|
1266
|
+
code: ERROR_CODES.VERSION_NOT_FOUND,
|
|
1267
|
+
message: `toVersion "${toVersion}" was not found in manifest.`,
|
|
1268
|
+
details: { toVersion }
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
if (fromIndex > toIndex) {
|
|
1272
|
+
throw createError({
|
|
1273
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1274
|
+
message: "fromVersion must be older than or equal to toVersion.",
|
|
1275
|
+
details: { fromVersion, toVersion }
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
const mappingWarnings = [];
|
|
1279
|
+
const officialFromClassName = await this.resolveToOfficialClassName(className, fromVersion, mapping, input.sourcePriority, mappingWarnings);
|
|
1280
|
+
const officialToClassName = fromVersion === toVersion
|
|
1281
|
+
? officialFromClassName
|
|
1282
|
+
: await this.resolveToOfficialClassName(className, toVersion, mapping, input.sourcePriority, mappingWarnings);
|
|
1283
|
+
const [fromResolved, toResolved] = await Promise.all([
|
|
1284
|
+
this.versionService.resolveVersionJar(fromVersion),
|
|
1285
|
+
this.versionService.resolveVersionJar(toVersion)
|
|
1286
|
+
]);
|
|
1287
|
+
const loadSignature = async (version, jarPath, officialClassName) => {
|
|
1288
|
+
try {
|
|
1289
|
+
const signature = await this.explorerService.getSignature({
|
|
1290
|
+
fqn: officialClassName,
|
|
1291
|
+
jarPath,
|
|
1292
|
+
access: "all",
|
|
1293
|
+
includeSynthetic: false,
|
|
1294
|
+
includeInherited: false
|
|
1295
|
+
});
|
|
1296
|
+
return {
|
|
1297
|
+
constructors: signature.constructors,
|
|
1298
|
+
fields: signature.fields,
|
|
1299
|
+
methods: signature.methods,
|
|
1300
|
+
warnings: signature.warnings
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
catch (caughtError) {
|
|
1304
|
+
if (isAppError(caughtError) && caughtError.code === ERROR_CODES.CLASS_NOT_FOUND) {
|
|
1305
|
+
return undefined;
|
|
1306
|
+
}
|
|
1307
|
+
throw caughtError;
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
const [fromSignature, toSignature] = await Promise.all([
|
|
1311
|
+
loadSignature(fromVersion, fromResolved.jarPath, officialFromClassName),
|
|
1312
|
+
loadSignature(toVersion, toResolved.jarPath, officialToClassName)
|
|
1313
|
+
]);
|
|
1314
|
+
const warnings = [...mappingWarnings];
|
|
1315
|
+
if (fromSignature) {
|
|
1316
|
+
warnings.push(...fromSignature.warnings.map((warning) => `[${fromVersion}] ${warning}`));
|
|
1317
|
+
}
|
|
1318
|
+
if (toSignature) {
|
|
1319
|
+
warnings.push(...toSignature.warnings.map((warning) => `[${toVersion}] ${warning}`));
|
|
1320
|
+
}
|
|
1321
|
+
let classChange = "present_in_both";
|
|
1322
|
+
if (!fromSignature && !toSignature) {
|
|
1323
|
+
classChange = "absent_in_both";
|
|
1324
|
+
warnings.push(`Class "${className}" was not found in both versions.`);
|
|
1325
|
+
}
|
|
1326
|
+
else if (!fromSignature) {
|
|
1327
|
+
classChange = "added";
|
|
1328
|
+
}
|
|
1329
|
+
else if (!toSignature) {
|
|
1330
|
+
classChange = "removed";
|
|
1331
|
+
}
|
|
1332
|
+
const fromMembers = fromSignature ?? {
|
|
1333
|
+
constructors: [],
|
|
1334
|
+
fields: [],
|
|
1335
|
+
methods: [],
|
|
1336
|
+
warnings: []
|
|
1337
|
+
};
|
|
1338
|
+
const toMembers = toSignature ?? {
|
|
1339
|
+
constructors: [],
|
|
1340
|
+
fields: [],
|
|
1341
|
+
methods: [],
|
|
1342
|
+
warnings: []
|
|
1343
|
+
};
|
|
1344
|
+
const constructors = classChange === "added"
|
|
1345
|
+
? {
|
|
1346
|
+
added: sortDiffMembers(toMembers.constructors),
|
|
1347
|
+
removed: [],
|
|
1348
|
+
modified: []
|
|
1349
|
+
}
|
|
1350
|
+
: classChange === "removed"
|
|
1351
|
+
? {
|
|
1352
|
+
added: [],
|
|
1353
|
+
removed: sortDiffMembers(fromMembers.constructors),
|
|
1354
|
+
modified: []
|
|
1355
|
+
}
|
|
1356
|
+
: classChange === "absent_in_both"
|
|
1357
|
+
? emptyDiffDelta()
|
|
1358
|
+
: diffMembersByKey(fromMembers.constructors, toMembers.constructors, (member) => member.jvmDescriptor, false);
|
|
1359
|
+
const methods = classChange === "added"
|
|
1360
|
+
? {
|
|
1361
|
+
added: sortDiffMembers(toMembers.methods),
|
|
1362
|
+
removed: [],
|
|
1363
|
+
modified: []
|
|
1364
|
+
}
|
|
1365
|
+
: classChange === "removed"
|
|
1366
|
+
? {
|
|
1367
|
+
added: [],
|
|
1368
|
+
removed: sortDiffMembers(fromMembers.methods),
|
|
1369
|
+
modified: []
|
|
1370
|
+
}
|
|
1371
|
+
: classChange === "absent_in_both"
|
|
1372
|
+
? emptyDiffDelta()
|
|
1373
|
+
: diffMembersByKey(fromMembers.methods, toMembers.methods, (member) => `${member.name}#${member.jvmDescriptor}`, false);
|
|
1374
|
+
const fields = classChange === "added"
|
|
1375
|
+
? {
|
|
1376
|
+
added: sortDiffMembers(toMembers.fields),
|
|
1377
|
+
removed: [],
|
|
1378
|
+
modified: []
|
|
1379
|
+
}
|
|
1380
|
+
: classChange === "removed"
|
|
1381
|
+
? {
|
|
1382
|
+
added: [],
|
|
1383
|
+
removed: sortDiffMembers(fromMembers.fields),
|
|
1384
|
+
modified: []
|
|
1385
|
+
}
|
|
1386
|
+
: classChange === "absent_in_both"
|
|
1387
|
+
? emptyDiffDelta()
|
|
1388
|
+
: diffMembersByKey(fromMembers.fields, toMembers.fields, (member) => member.name, true);
|
|
1389
|
+
// Remap diff delta members for non-official mappings
|
|
1390
|
+
const remapDelta = async (delta, kind) => {
|
|
1391
|
+
const [remappedAdded, remappedRemoved] = await Promise.all([
|
|
1392
|
+
this.remapSignatureMembers(delta.added, kind, toVersion, mapping, input.sourcePriority, warnings),
|
|
1393
|
+
this.remapSignatureMembers(delta.removed, kind, fromVersion, mapping, input.sourcePriority, warnings)
|
|
1394
|
+
]);
|
|
1395
|
+
const remappedModified = await Promise.all(delta.modified.map(async (change) => {
|
|
1396
|
+
const [fromArr, toArr] = await Promise.all([
|
|
1397
|
+
this.remapSignatureMembers([change.from], kind, fromVersion, mapping, input.sourcePriority, warnings),
|
|
1398
|
+
this.remapSignatureMembers([change.to], kind, toVersion, mapping, input.sourcePriority, warnings)
|
|
1399
|
+
]);
|
|
1400
|
+
return { ...change, from: fromArr[0], to: toArr[0] };
|
|
1401
|
+
}));
|
|
1402
|
+
return { added: remappedAdded, removed: remappedRemoved, modified: remappedModified };
|
|
1403
|
+
};
|
|
1404
|
+
const [remappedConstructors, remappedMethods, remappedFields] = await Promise.all([
|
|
1405
|
+
remapDelta(constructors, "method"),
|
|
1406
|
+
remapDelta(methods, "method"),
|
|
1407
|
+
remapDelta(fields, "field")
|
|
1408
|
+
]);
|
|
1409
|
+
const summary = {
|
|
1410
|
+
constructors: {
|
|
1411
|
+
added: remappedConstructors.added.length,
|
|
1412
|
+
removed: remappedConstructors.removed.length,
|
|
1413
|
+
modified: remappedConstructors.modified.length
|
|
1414
|
+
},
|
|
1415
|
+
methods: {
|
|
1416
|
+
added: remappedMethods.added.length,
|
|
1417
|
+
removed: remappedMethods.removed.length,
|
|
1418
|
+
modified: remappedMethods.modified.length
|
|
1419
|
+
},
|
|
1420
|
+
fields: {
|
|
1421
|
+
added: remappedFields.added.length,
|
|
1422
|
+
removed: remappedFields.removed.length,
|
|
1423
|
+
modified: remappedFields.modified.length
|
|
1424
|
+
},
|
|
1425
|
+
total: {
|
|
1426
|
+
added: remappedConstructors.added.length + remappedMethods.added.length + remappedFields.added.length,
|
|
1427
|
+
removed: remappedConstructors.removed.length + remappedMethods.removed.length + remappedFields.removed.length,
|
|
1428
|
+
modified: remappedConstructors.modified.length + remappedMethods.modified.length + remappedFields.modified.length
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
return {
|
|
1432
|
+
query: {
|
|
1433
|
+
className,
|
|
1434
|
+
fromVersion,
|
|
1435
|
+
toVersion,
|
|
1436
|
+
mapping
|
|
1437
|
+
},
|
|
1438
|
+
range: {
|
|
1439
|
+
fromVersion,
|
|
1440
|
+
toVersion
|
|
1441
|
+
},
|
|
1442
|
+
classChange,
|
|
1443
|
+
constructors: remappedConstructors,
|
|
1444
|
+
methods: remappedMethods,
|
|
1445
|
+
fields: remappedFields,
|
|
1446
|
+
summary,
|
|
1447
|
+
warnings
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
async getClassSource(input) {
|
|
1451
|
+
const className = input.className.trim();
|
|
1452
|
+
if (!className) {
|
|
1453
|
+
throw createError({
|
|
1454
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1455
|
+
message: "className must be non-empty."
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
const startLine = normalizeStrictPositiveInt(input.startLine, "startLine");
|
|
1459
|
+
const endLine = normalizeStrictPositiveInt(input.endLine, "endLine");
|
|
1460
|
+
const maxLines = normalizeStrictPositiveInt(input.maxLines, "maxLines");
|
|
1461
|
+
if (startLine != null && endLine != null && startLine > endLine) {
|
|
1462
|
+
throw createError({
|
|
1463
|
+
code: ERROR_CODES.INVALID_LINE_RANGE,
|
|
1464
|
+
message: `Invalid line range: startLine (${startLine}) is greater than endLine (${endLine}).`,
|
|
1465
|
+
details: {
|
|
1466
|
+
startLine,
|
|
1467
|
+
endLine
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
const normalizedArtifactId = normalizeOptionalString(input.artifactId);
|
|
1472
|
+
if (normalizedArtifactId && input.target) {
|
|
1473
|
+
throw createError({
|
|
1474
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1475
|
+
message: "artifactId and targetKind/targetValue are mutually exclusive.",
|
|
1476
|
+
details: {
|
|
1477
|
+
artifactId: normalizedArtifactId,
|
|
1478
|
+
target: input.target
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
let artifactId = normalizedArtifactId;
|
|
1483
|
+
let origin = "local-jar";
|
|
1484
|
+
let warnings = [];
|
|
1485
|
+
let requestedMapping = normalizeMapping(input.mapping);
|
|
1486
|
+
let mappingApplied = requestedMapping;
|
|
1487
|
+
let provenance;
|
|
1488
|
+
let qualityFlags = [];
|
|
1489
|
+
if (!artifactId) {
|
|
1490
|
+
if (!input.target) {
|
|
1491
|
+
throw createError({
|
|
1492
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1493
|
+
message: "Either artifactId or target must be provided."
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
const resolved = await this.resolveArtifact({
|
|
1497
|
+
target: input.target,
|
|
1498
|
+
mapping: input.mapping,
|
|
1499
|
+
sourcePriority: input.sourcePriority,
|
|
1500
|
+
allowDecompile: input.allowDecompile
|
|
1501
|
+
});
|
|
1502
|
+
artifactId = resolved.artifactId;
|
|
1503
|
+
origin = resolved.origin;
|
|
1504
|
+
warnings = [...resolved.warnings];
|
|
1505
|
+
requestedMapping = resolved.requestedMapping;
|
|
1506
|
+
mappingApplied = resolved.mappingApplied;
|
|
1507
|
+
provenance = resolved.provenance;
|
|
1508
|
+
qualityFlags = [...resolved.qualityFlags];
|
|
1509
|
+
}
|
|
1510
|
+
else {
|
|
1511
|
+
const artifact = this.getArtifact(artifactId);
|
|
1512
|
+
origin = artifact.origin;
|
|
1513
|
+
requestedMapping = artifact.requestedMapping ?? requestedMapping;
|
|
1514
|
+
mappingApplied = artifact.mappingApplied ?? requestedMapping;
|
|
1515
|
+
provenance = artifact.provenance;
|
|
1516
|
+
qualityFlags = artifact.qualityFlags;
|
|
1517
|
+
}
|
|
1518
|
+
const filePath = this.resolveClassFilePath(artifactId, className);
|
|
1519
|
+
if (!filePath) {
|
|
1520
|
+
throw createError({
|
|
1521
|
+
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
1522
|
+
message: `Source for class "${className}" was not found.`,
|
|
1523
|
+
details: {
|
|
1524
|
+
artifactId,
|
|
1525
|
+
className
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
const row = this.filesRepo.getFileContent(artifactId, filePath);
|
|
1530
|
+
if (!row) {
|
|
1531
|
+
throw createError({
|
|
1532
|
+
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
1533
|
+
message: `Source for class "${className}" was not found.`,
|
|
1534
|
+
details: {
|
|
1535
|
+
artifactId,
|
|
1536
|
+
className,
|
|
1537
|
+
filePath
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
const lines = row.content.split(/\r?\n/);
|
|
1542
|
+
const totalLines = lines.length;
|
|
1543
|
+
const requestedStart = startLine ?? 1;
|
|
1544
|
+
const requestedEnd = endLine ?? totalLines;
|
|
1545
|
+
const normalizedStart = Math.min(Math.max(1, requestedStart), Math.max(totalLines, 1));
|
|
1546
|
+
const normalizedEnd = Math.min(Math.max(normalizedStart, requestedEnd), Math.max(totalLines, 1));
|
|
1547
|
+
let selectedLines = lines.slice(normalizedStart - 1, normalizedEnd);
|
|
1548
|
+
const clippedByRange = normalizedStart !== requestedStart || normalizedEnd !== requestedEnd;
|
|
1549
|
+
let clippedByMax = false;
|
|
1550
|
+
if (maxLines != null && selectedLines.length > maxLines) {
|
|
1551
|
+
selectedLines = selectedLines.slice(0, maxLines);
|
|
1552
|
+
clippedByMax = true;
|
|
1553
|
+
}
|
|
1554
|
+
const returnedEnd = normalizedStart + Math.max(0, selectedLines.length - 1);
|
|
1555
|
+
const normalizedProvenance = provenance ??
|
|
1556
|
+
this.buildFallbackProvenance({
|
|
1557
|
+
artifactId,
|
|
1558
|
+
origin,
|
|
1559
|
+
requestedMapping,
|
|
1560
|
+
mappingApplied
|
|
1561
|
+
});
|
|
1562
|
+
return {
|
|
1563
|
+
className,
|
|
1564
|
+
sourceText: selectedLines.join("\n"),
|
|
1565
|
+
totalLines,
|
|
1566
|
+
returnedRange: {
|
|
1567
|
+
start: normalizedStart,
|
|
1568
|
+
end: returnedEnd
|
|
1569
|
+
},
|
|
1570
|
+
truncated: clippedByRange || clippedByMax,
|
|
1571
|
+
origin,
|
|
1572
|
+
artifactId,
|
|
1573
|
+
requestedMapping,
|
|
1574
|
+
mappingApplied,
|
|
1575
|
+
provenance: normalizedProvenance,
|
|
1576
|
+
qualityFlags,
|
|
1577
|
+
warnings
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
async getClassMembers(input) {
|
|
1581
|
+
const className = input.className.trim();
|
|
1582
|
+
if (!className) {
|
|
1583
|
+
throw createError({
|
|
1584
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1585
|
+
message: "className must be non-empty."
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
const requestedMapping = normalizeMapping(input.mapping);
|
|
1589
|
+
const access = normalizeMemberAccess(input.access);
|
|
1590
|
+
const includeSynthetic = input.includeSynthetic ?? false;
|
|
1591
|
+
const includeInherited = input.includeInherited ?? false;
|
|
1592
|
+
const memberPattern = normalizeOptionalString(input.memberPattern);
|
|
1593
|
+
const parsedMaxMembers = normalizeStrictPositiveInt(input.maxMembers, "maxMembers");
|
|
1594
|
+
const maxMembers = parsedMaxMembers == null ? 500 : Math.min(parsedMaxMembers, 5000);
|
|
1595
|
+
const normalizedArtifactId = normalizeOptionalString(input.artifactId);
|
|
1596
|
+
if (normalizedArtifactId && input.target) {
|
|
1597
|
+
throw createError({
|
|
1598
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1599
|
+
message: "artifactId and targetKind/targetValue are mutually exclusive.",
|
|
1600
|
+
details: {
|
|
1601
|
+
artifactId: normalizedArtifactId,
|
|
1602
|
+
target: input.target
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
let artifactId = normalizedArtifactId;
|
|
1607
|
+
let origin = "local-jar";
|
|
1608
|
+
let warnings = [];
|
|
1609
|
+
let mappingApplied = requestedMapping;
|
|
1610
|
+
let provenance;
|
|
1611
|
+
let qualityFlags = [];
|
|
1612
|
+
let binaryJarPath;
|
|
1613
|
+
if (parsedMaxMembers != null && parsedMaxMembers > 5000) {
|
|
1614
|
+
warnings.push(`maxMembers was clamped to 5000 from ${parsedMaxMembers}.`);
|
|
1615
|
+
}
|
|
1616
|
+
let version;
|
|
1617
|
+
if (!artifactId) {
|
|
1618
|
+
if (!input.target) {
|
|
1619
|
+
throw createError({
|
|
1620
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1621
|
+
message: "Either artifactId or target must be provided."
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
const resolved = await this.resolveArtifact({
|
|
1625
|
+
target: input.target,
|
|
1626
|
+
mapping: requestedMapping,
|
|
1627
|
+
sourcePriority: input.sourcePriority,
|
|
1628
|
+
allowDecompile: input.allowDecompile
|
|
1629
|
+
});
|
|
1630
|
+
artifactId = resolved.artifactId;
|
|
1631
|
+
origin = resolved.origin;
|
|
1632
|
+
warnings.push(...resolved.warnings);
|
|
1633
|
+
mappingApplied = resolved.mappingApplied;
|
|
1634
|
+
provenance = resolved.provenance;
|
|
1635
|
+
qualityFlags = [...resolved.qualityFlags];
|
|
1636
|
+
binaryJarPath = resolved.binaryJarPath;
|
|
1637
|
+
version = resolved.version;
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
const artifact = this.getArtifact(artifactId);
|
|
1641
|
+
origin = artifact.origin;
|
|
1642
|
+
mappingApplied = artifact.mappingApplied ?? requestedMapping;
|
|
1643
|
+
provenance = artifact.provenance;
|
|
1644
|
+
qualityFlags = artifact.qualityFlags;
|
|
1645
|
+
binaryJarPath = artifact.binaryJarPath;
|
|
1646
|
+
version = artifact.version;
|
|
1647
|
+
}
|
|
1648
|
+
if (requestedMapping !== "official" && !version) {
|
|
1649
|
+
throw createError({
|
|
1650
|
+
code: ERROR_CODES.MAPPING_NOT_APPLIED,
|
|
1651
|
+
message: `Non-official mapping "${requestedMapping}" requires a version, but none was resolved.`,
|
|
1652
|
+
details: {
|
|
1653
|
+
mapping: requestedMapping,
|
|
1654
|
+
nextAction: "Resolve with targetKind=version or specify a versioned coordinate."
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
if (!binaryJarPath) {
|
|
1659
|
+
throw createError({
|
|
1660
|
+
code: ERROR_CODES.CONTEXT_UNRESOLVED,
|
|
1661
|
+
message: `Class members require a binary jar, but artifact "${artifactId}" has no binaryJarPath.`,
|
|
1662
|
+
details: {
|
|
1663
|
+
artifactId,
|
|
1664
|
+
className,
|
|
1665
|
+
nextAction: "Resolve with targetKind=jar or targetKind=version, or use an artifact that has a binary jar."
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
const officialClassName = version != null
|
|
1670
|
+
? await this.resolveToOfficialClassName(className, version, requestedMapping, input.sourcePriority, warnings)
|
|
1671
|
+
: className;
|
|
1672
|
+
const signature = await this.explorerService.getSignature({
|
|
1673
|
+
fqn: officialClassName,
|
|
1674
|
+
jarPath: binaryJarPath,
|
|
1675
|
+
access,
|
|
1676
|
+
includeSynthetic,
|
|
1677
|
+
includeInherited,
|
|
1678
|
+
memberPattern: requestedMapping === "official" ? memberPattern : undefined
|
|
1679
|
+
});
|
|
1680
|
+
warnings.push(...signature.warnings);
|
|
1681
|
+
let remappedConstructors = version != null
|
|
1682
|
+
? await this.remapSignatureMembers(signature.constructors, "method", version, requestedMapping, input.sourcePriority, warnings)
|
|
1683
|
+
: signature.constructors;
|
|
1684
|
+
let remappedFields = version != null
|
|
1685
|
+
? await this.remapSignatureMembers(signature.fields, "field", version, requestedMapping, input.sourcePriority, warnings)
|
|
1686
|
+
: signature.fields;
|
|
1687
|
+
let remappedMethods = version != null
|
|
1688
|
+
? await this.remapSignatureMembers(signature.methods, "method", version, requestedMapping, input.sourcePriority, warnings)
|
|
1689
|
+
: signature.methods;
|
|
1690
|
+
// Apply memberPattern post-remap for non-official mappings
|
|
1691
|
+
if (requestedMapping !== "official" && memberPattern) {
|
|
1692
|
+
const lowerPattern = memberPattern.toLowerCase();
|
|
1693
|
+
remappedConstructors = remappedConstructors.filter((m) => m.name.toLowerCase().includes(lowerPattern));
|
|
1694
|
+
remappedFields = remappedFields.filter((m) => m.name.toLowerCase().includes(lowerPattern));
|
|
1695
|
+
remappedMethods = remappedMethods.filter((m) => m.name.toLowerCase().includes(lowerPattern));
|
|
1696
|
+
}
|
|
1697
|
+
const counts = {
|
|
1698
|
+
constructors: remappedConstructors.length,
|
|
1699
|
+
fields: remappedFields.length,
|
|
1700
|
+
methods: remappedMethods.length,
|
|
1701
|
+
total: remappedConstructors.length + remappedFields.length + remappedMethods.length
|
|
1702
|
+
};
|
|
1703
|
+
let remaining = maxMembers;
|
|
1704
|
+
const takeWithinLimit = (members) => {
|
|
1705
|
+
if (remaining <= 0) {
|
|
1706
|
+
return [];
|
|
1707
|
+
}
|
|
1708
|
+
const slice = members.slice(0, remaining);
|
|
1709
|
+
remaining -= slice.length;
|
|
1710
|
+
return slice;
|
|
1711
|
+
};
|
|
1712
|
+
const constructors = takeWithinLimit(remappedConstructors);
|
|
1713
|
+
const fields = takeWithinLimit(remappedFields);
|
|
1714
|
+
const methods = takeWithinLimit(remappedMethods);
|
|
1715
|
+
const returnedTotal = constructors.length + fields.length + methods.length;
|
|
1716
|
+
const truncated = returnedTotal < counts.total;
|
|
1717
|
+
if (truncated) {
|
|
1718
|
+
warnings.push(`Member list was truncated to ${returnedTotal} entries (from ${counts.total}).`);
|
|
1719
|
+
}
|
|
1720
|
+
const normalizedProvenance = provenance ??
|
|
1721
|
+
this.buildFallbackProvenance({
|
|
1722
|
+
artifactId,
|
|
1723
|
+
origin,
|
|
1724
|
+
requestedMapping,
|
|
1725
|
+
mappingApplied
|
|
1726
|
+
});
|
|
1727
|
+
return {
|
|
1728
|
+
className,
|
|
1729
|
+
members: {
|
|
1730
|
+
constructors,
|
|
1731
|
+
fields,
|
|
1732
|
+
methods
|
|
1733
|
+
},
|
|
1734
|
+
counts,
|
|
1735
|
+
truncated,
|
|
1736
|
+
context: signature.context,
|
|
1737
|
+
origin,
|
|
1738
|
+
artifactId,
|
|
1739
|
+
requestedMapping,
|
|
1740
|
+
mappingApplied,
|
|
1741
|
+
provenance: normalizedProvenance,
|
|
1742
|
+
qualityFlags,
|
|
1743
|
+
warnings
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
async validateMixin(input) {
|
|
1747
|
+
const version = input.version.trim();
|
|
1748
|
+
if (!version) {
|
|
1749
|
+
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "version must be non-empty." });
|
|
1750
|
+
}
|
|
1751
|
+
const source = input.source;
|
|
1752
|
+
if (!source.trim()) {
|
|
1753
|
+
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "source must be non-empty." });
|
|
1754
|
+
}
|
|
1755
|
+
const warnings = [];
|
|
1756
|
+
const requestedMapping = normalizeMapping(input.mapping);
|
|
1757
|
+
const { jarPath } = await this.versionService.resolveVersionJar(version);
|
|
1758
|
+
const parsed = parseMixinSource(source);
|
|
1759
|
+
const targetMembers = new Map();
|
|
1760
|
+
for (const target of parsed.targets) {
|
|
1761
|
+
let officialName = target.className;
|
|
1762
|
+
if (requestedMapping !== "official") {
|
|
1763
|
+
try {
|
|
1764
|
+
const mapped = await this.mappingService.findMapping({
|
|
1765
|
+
version,
|
|
1766
|
+
kind: "class",
|
|
1767
|
+
name: target.className,
|
|
1768
|
+
sourceMapping: requestedMapping,
|
|
1769
|
+
targetMapping: "official",
|
|
1770
|
+
sourcePriority: input.sourcePriority
|
|
1771
|
+
});
|
|
1772
|
+
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
1773
|
+
officialName = mapped.resolvedSymbol.name;
|
|
1774
|
+
}
|
|
1775
|
+
else {
|
|
1776
|
+
warnings.push(`Could not map class "${target.className}" from ${requestedMapping} to official.`);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
catch {
|
|
1780
|
+
warnings.push(`Mapping lookup failed for class "${target.className}".`);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
try {
|
|
1784
|
+
const sig = await this.explorerService.getSignature({
|
|
1785
|
+
fqn: officialName,
|
|
1786
|
+
jarPath,
|
|
1787
|
+
access: "all"
|
|
1788
|
+
});
|
|
1789
|
+
warnings.push(...sig.warnings);
|
|
1790
|
+
targetMembers.set(target.className, {
|
|
1791
|
+
className: target.className,
|
|
1792
|
+
constructors: sig.constructors,
|
|
1793
|
+
methods: sig.methods,
|
|
1794
|
+
fields: sig.fields
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
catch {
|
|
1798
|
+
warnings.push(`Could not load signature for class "${officialName}".`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return validateParsedMixin(parsed, targetMembers, warnings);
|
|
1802
|
+
}
|
|
1803
|
+
async validateAccessWidener(input) {
|
|
1804
|
+
const version = input.version.trim();
|
|
1805
|
+
if (!version) {
|
|
1806
|
+
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "version must be non-empty." });
|
|
1807
|
+
}
|
|
1808
|
+
const content = input.content;
|
|
1809
|
+
if (!content.trim()) {
|
|
1810
|
+
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "content must be non-empty." });
|
|
1811
|
+
}
|
|
1812
|
+
const warnings = [];
|
|
1813
|
+
const { jarPath } = await this.versionService.resolveVersionJar(version);
|
|
1814
|
+
const parsed = parseAccessWidener(content);
|
|
1815
|
+
const headerNamespaceRaw = normalizeOptionalString(parsed.namespace);
|
|
1816
|
+
const overrideMapping = input.mapping ? normalizeMapping(input.mapping) : undefined;
|
|
1817
|
+
const headerNamespace = normalizeAccessWidenerNamespace(headerNamespaceRaw);
|
|
1818
|
+
if (!headerNamespace && headerNamespaceRaw && !overrideMapping) {
|
|
1819
|
+
warnings.push(`Unsupported access widener namespace "${headerNamespaceRaw}". Assuming intermediary.`);
|
|
1820
|
+
}
|
|
1821
|
+
const awNamespace = overrideMapping ?? headerNamespace ?? "intermediary";
|
|
1822
|
+
if (overrideMapping && headerNamespace && overrideMapping !== headerNamespace) {
|
|
1823
|
+
warnings.push(`Using mapping override "${overrideMapping}" instead of header namespace "${headerNamespaceRaw}".`);
|
|
1824
|
+
}
|
|
1825
|
+
const needsMapping = awNamespace !== "official";
|
|
1826
|
+
// Collect unique class FQNs from entries
|
|
1827
|
+
const classFqns = new Set();
|
|
1828
|
+
for (const entry of parsed.entries) {
|
|
1829
|
+
const fqn = entry.target.replace(/\//g, ".");
|
|
1830
|
+
classFqns.add(fqn);
|
|
1831
|
+
}
|
|
1832
|
+
const membersByClass = new Map();
|
|
1833
|
+
for (const fqn of classFqns) {
|
|
1834
|
+
let officialFqn = fqn;
|
|
1835
|
+
if (needsMapping) {
|
|
1836
|
+
try {
|
|
1837
|
+
const mapped = await this.mappingService.findMapping({
|
|
1838
|
+
version,
|
|
1839
|
+
kind: "class",
|
|
1840
|
+
name: fqn,
|
|
1841
|
+
sourceMapping: awNamespace,
|
|
1842
|
+
targetMapping: "official",
|
|
1843
|
+
sourcePriority: input.sourcePriority
|
|
1844
|
+
});
|
|
1845
|
+
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
1846
|
+
officialFqn = mapped.resolvedSymbol.name;
|
|
1847
|
+
}
|
|
1848
|
+
else {
|
|
1849
|
+
warnings.push(`Could not map class "${fqn}" from ${awNamespace} to official.`);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
catch {
|
|
1853
|
+
warnings.push(`Mapping lookup failed for class "${fqn}".`);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
try {
|
|
1857
|
+
const sig = await this.explorerService.getSignature({
|
|
1858
|
+
fqn: officialFqn,
|
|
1859
|
+
jarPath,
|
|
1860
|
+
access: "all"
|
|
1861
|
+
});
|
|
1862
|
+
warnings.push(...sig.warnings);
|
|
1863
|
+
membersByClass.set(fqn, {
|
|
1864
|
+
className: fqn,
|
|
1865
|
+
constructors: sig.constructors,
|
|
1866
|
+
methods: sig.methods,
|
|
1867
|
+
fields: sig.fields
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
catch {
|
|
1871
|
+
warnings.push(`Could not load signature for class "${officialFqn}".`);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return validateParsedAccessWidener(parsed, membersByClass, warnings);
|
|
1875
|
+
}
|
|
1876
|
+
getRuntimeMetrics() {
|
|
1877
|
+
return this.metrics.snapshot();
|
|
1878
|
+
}
|
|
1879
|
+
async indexArtifact(input) {
|
|
1880
|
+
const artifactId = input.artifactId?.trim();
|
|
1881
|
+
if (!artifactId) {
|
|
1882
|
+
throw createError({
|
|
1883
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1884
|
+
message: "artifactId must be non-empty."
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
const artifact = this.getArtifact(artifactId);
|
|
1888
|
+
const force = input.force ?? false;
|
|
1889
|
+
const hasFiles = this.hasAnyFiles(artifact.artifactId);
|
|
1890
|
+
const meta = this.indexMetaRepo.get(artifact.artifactId);
|
|
1891
|
+
const expectedSignature = artifact.artifactSignature ?? this.fallbackArtifactSignature(artifact.artifactId);
|
|
1892
|
+
const reason = this.resolveIndexRebuildReason({
|
|
1893
|
+
force,
|
|
1894
|
+
expectedSignature,
|
|
1895
|
+
hasFiles,
|
|
1896
|
+
meta
|
|
1897
|
+
});
|
|
1898
|
+
if (reason === "already_current") {
|
|
1899
|
+
this.metrics.recordReindexSkip();
|
|
1900
|
+
const currentMeta = meta;
|
|
1901
|
+
return {
|
|
1902
|
+
artifactId: artifact.artifactId,
|
|
1903
|
+
reindexed: false,
|
|
1904
|
+
reason,
|
|
1905
|
+
counts: {
|
|
1906
|
+
files: currentMeta.filesCount,
|
|
1907
|
+
symbols: currentMeta.symbolsCount,
|
|
1908
|
+
ftsRows: currentMeta.ftsRowsCount
|
|
1909
|
+
},
|
|
1910
|
+
indexedAt: currentMeta.indexedAt,
|
|
1911
|
+
durationMs: 0,
|
|
1912
|
+
mappingApplied: artifact.mappingApplied ?? "official"
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
const resolved = this.toResolvedArtifact(artifact);
|
|
1916
|
+
const rebuilt = await this.rebuildAndPersistArtifactIndex(resolved, reason);
|
|
1917
|
+
this.metrics.recordReindex();
|
|
1918
|
+
return {
|
|
1919
|
+
artifactId: artifact.artifactId,
|
|
1920
|
+
reindexed: true,
|
|
1921
|
+
reason,
|
|
1922
|
+
counts: {
|
|
1923
|
+
files: rebuilt.files.length,
|
|
1924
|
+
symbols: rebuilt.symbols.length,
|
|
1925
|
+
ftsRows: rebuilt.files.length
|
|
1926
|
+
},
|
|
1927
|
+
indexedAt: rebuilt.indexedAt,
|
|
1928
|
+
durationMs: rebuilt.indexDurationMs,
|
|
1929
|
+
mappingApplied: artifact.mappingApplied ?? "official"
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
searchSymbolIntent(artifactId, query, match, scope, snippetWindow, regexPattern, onHit) {
|
|
1933
|
+
const matchedSymbols = this.findSymbolHits(artifactId, query, match, scope, regexPattern);
|
|
1934
|
+
const filePaths = [...new Set(matchedSymbols.map((item) => item.symbol.filePath))];
|
|
1935
|
+
const rows = this.filesRepo.getFileContentsByPaths(artifactId, filePaths);
|
|
1936
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
1937
|
+
this.metrics.recordSearchRowsScanned(rows.length);
|
|
1938
|
+
const rowsByPath = new Map(rows.map((row) => [row.filePath, row]));
|
|
1939
|
+
for (const item of matchedSymbols) {
|
|
1940
|
+
const row = rowsByPath.get(item.symbol.filePath);
|
|
1941
|
+
const snippet = row
|
|
1942
|
+
? toContextSnippet(row.content, item.symbol.line, snippetWindow.before, snippetWindow.after, true)
|
|
1943
|
+
: {
|
|
1944
|
+
startLine: item.symbol.line,
|
|
1945
|
+
endLine: item.symbol.line,
|
|
1946
|
+
snippet: "",
|
|
1947
|
+
truncated: false
|
|
1948
|
+
};
|
|
1949
|
+
onHit({
|
|
1950
|
+
filePath: item.symbol.filePath,
|
|
1951
|
+
score: item.score,
|
|
1952
|
+
matchedIn: "symbol",
|
|
1953
|
+
startLine: snippet.startLine,
|
|
1954
|
+
endLine: snippet.endLine,
|
|
1955
|
+
snippet: snippet.snippet,
|
|
1956
|
+
reasonCodes: [`symbol_${match}`],
|
|
1957
|
+
symbol: {
|
|
1958
|
+
symbolKind: item.symbol.symbolKind,
|
|
1959
|
+
symbolName: item.symbol.symbolName,
|
|
1960
|
+
qualifiedName: item.symbol.qualifiedName,
|
|
1961
|
+
line: item.symbol.line
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
searchTextIntentIndexed(artifactId, query, match, scope, includeDefinition, snippetWindow, onHit) {
|
|
1967
|
+
const candidateLimit = this.indexedCandidateLimitForMatch(match);
|
|
1968
|
+
const indexed = this.filesRepo.searchFileCandidates(artifactId, {
|
|
1969
|
+
query,
|
|
1970
|
+
limit: candidateLimit,
|
|
1971
|
+
mode: "text"
|
|
1972
|
+
});
|
|
1973
|
+
this.metrics.recordSearchDbRoundtrip(indexed.dbRoundtrips);
|
|
1974
|
+
this.metrics.recordSearchRowsScanned(indexed.scannedRows);
|
|
1975
|
+
// Zero-result short-circuit: if indexed search returns nothing, skip hydration
|
|
1976
|
+
if (indexed.items.length === 0) {
|
|
1977
|
+
this.metrics.recordSearchIndexedZeroShortcircuit();
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
const globFilter = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
1981
|
+
const candidatePaths = indexed.items
|
|
1982
|
+
.filter((candidate) => candidate.matchedIn !== "path")
|
|
1983
|
+
.map((candidate) => candidate.filePath)
|
|
1984
|
+
.filter((filePath) => checkPackagePrefix(filePath, scope?.packagePrefix))
|
|
1985
|
+
.filter((filePath) => !globFilter || globFilter.test(filePath));
|
|
1986
|
+
const candidateContentRows = this.filesRepo.getFileContentsByPaths(artifactId, candidatePaths);
|
|
1987
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
1988
|
+
this.metrics.recordSearchRowsScanned(candidateContentRows.length);
|
|
1989
|
+
const candidateRows = [];
|
|
1990
|
+
for (const candidate of candidateContentRows) {
|
|
1991
|
+
const contentIndex = findContentMatchIndex(candidate.content, query, match);
|
|
1992
|
+
if (contentIndex < 0) {
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
const line = indexToLine(candidate.content, contentIndex);
|
|
1996
|
+
candidateRows.push({
|
|
1997
|
+
filePath: candidate.filePath,
|
|
1998
|
+
content: candidate.content,
|
|
1999
|
+
line,
|
|
2000
|
+
contentIndex
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
const needSymbols = includeDefinition || !!scope?.symbolKind;
|
|
2004
|
+
const symbolsByFile = needSymbols
|
|
2005
|
+
? this.symbolsRepo.listSymbolsForFiles(artifactId, candidateRows.map((candidate) => candidate.filePath), scope?.symbolKind)
|
|
2006
|
+
: new Map();
|
|
2007
|
+
if (needSymbols) {
|
|
2008
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2009
|
+
this.metrics.recordSearchRowsScanned([...symbolsByFile.values()].reduce((acc, symbols) => acc + symbols.length, 0));
|
|
2010
|
+
}
|
|
2011
|
+
for (const candidate of candidateRows) {
|
|
2012
|
+
// When symbolKind filter is set, skip files that have no symbols of that kind
|
|
2013
|
+
if (scope?.symbolKind && !symbolsByFile.has(candidate.filePath)) {
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
const snippet = toContextSnippet(candidate.content, candidate.line, snippetWindow.before, snippetWindow.after, true);
|
|
2017
|
+
const definition = includeDefinition
|
|
2018
|
+
? this.findNearestSymbolFromList(symbolsByFile.get(candidate.filePath) ?? [], candidate.line)
|
|
2019
|
+
: undefined;
|
|
2020
|
+
const resolvedSymbol = definition ? lineToSymbol(definition) : undefined;
|
|
2021
|
+
onHit({
|
|
2022
|
+
filePath: candidate.filePath,
|
|
2023
|
+
score: scoreTextMatch(match, candidate.contentIndex) + (resolvedSymbol ? 20 : 0),
|
|
2024
|
+
matchedIn: "content",
|
|
2025
|
+
startLine: snippet.startLine,
|
|
2026
|
+
endLine: snippet.endLine,
|
|
2027
|
+
snippet: snippet.snippet,
|
|
2028
|
+
reasonCodes: ["content_match", `text_${match}`, "indexed"],
|
|
2029
|
+
symbol: resolvedSymbol
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
searchPathIntentIndexed(artifactId, query, match, scope, includeDefinition, snippetWindow, onHit) {
|
|
2034
|
+
const candidateLimit = this.indexedCandidateLimitForMatch(match);
|
|
2035
|
+
const indexed = this.filesRepo.searchFileCandidates(artifactId, {
|
|
2036
|
+
query,
|
|
2037
|
+
limit: candidateLimit,
|
|
2038
|
+
mode: "path"
|
|
2039
|
+
});
|
|
2040
|
+
this.metrics.recordSearchDbRoundtrip(indexed.dbRoundtrips);
|
|
2041
|
+
this.metrics.recordSearchRowsScanned(indexed.scannedRows);
|
|
2042
|
+
// Zero-result short-circuit: if indexed search returns nothing, skip hydration
|
|
2043
|
+
if (indexed.items.length === 0) {
|
|
2044
|
+
this.metrics.recordSearchIndexedZeroShortcircuit();
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const globFilter = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
2048
|
+
const candidateRows = [];
|
|
2049
|
+
for (const candidate of indexed.items) {
|
|
2050
|
+
if (candidate.matchedIn === "content") {
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
if (!checkPackagePrefix(candidate.filePath, scope?.packagePrefix)) {
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
if (globFilter && !globFilter.test(candidate.filePath)) {
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
const pathIndex = findMatchIndex(candidate.filePath, query, match);
|
|
2060
|
+
if (pathIndex < 0) {
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
candidateRows.push({
|
|
2064
|
+
filePath: candidate.filePath,
|
|
2065
|
+
pathIndex
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
const candidateContentRows = this.filesRepo.getFileContentsByPaths(artifactId, candidateRows.map((candidate) => candidate.filePath));
|
|
2069
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2070
|
+
this.metrics.recordSearchRowsScanned(candidateContentRows.length);
|
|
2071
|
+
const contentByPath = new Map(candidateContentRows.map((row) => [row.filePath, row.content]));
|
|
2072
|
+
const needSymbols = includeDefinition || !!scope?.symbolKind;
|
|
2073
|
+
const symbolsByFile = needSymbols
|
|
2074
|
+
? this.symbolsRepo.listSymbolsForFiles(artifactId, candidateRows.map((candidate) => candidate.filePath), scope?.symbolKind)
|
|
2075
|
+
: new Map();
|
|
2076
|
+
if (needSymbols) {
|
|
2077
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2078
|
+
this.metrics.recordSearchRowsScanned([...symbolsByFile.values()].reduce((acc, symbols) => acc + symbols.length, 0));
|
|
2079
|
+
}
|
|
2080
|
+
for (const candidate of candidateRows) {
|
|
2081
|
+
const content = contentByPath.get(candidate.filePath);
|
|
2082
|
+
if (!content) {
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
// When symbolKind filter is set, skip files that have no symbols of that kind
|
|
2086
|
+
if (scope?.symbolKind && !symbolsByFile.has(candidate.filePath)) {
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
const definition = includeDefinition
|
|
2090
|
+
? this.findNearestSymbolFromList(symbolsByFile.get(candidate.filePath) ?? [], 1)
|
|
2091
|
+
: undefined;
|
|
2092
|
+
const centerLine = definition?.line ?? 1;
|
|
2093
|
+
const snippet = toContextSnippet(content, centerLine, snippetWindow.before, snippetWindow.after, true);
|
|
2094
|
+
const resolvedSymbol = definition ? lineToSymbol(definition) : undefined;
|
|
2095
|
+
onHit({
|
|
2096
|
+
filePath: candidate.filePath,
|
|
2097
|
+
score: scorePathMatch(match, candidate.pathIndex) + (resolvedSymbol ? 10 : 0),
|
|
2098
|
+
matchedIn: "path",
|
|
2099
|
+
startLine: snippet.startLine,
|
|
2100
|
+
endLine: snippet.endLine,
|
|
2101
|
+
snippet: snippet.snippet,
|
|
2102
|
+
reasonCodes: ["path_match", `path_${match}`, "indexed"],
|
|
2103
|
+
symbol: resolvedSymbol
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
searchTextIntent(artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, onHit) {
|
|
2108
|
+
const filePaths = this.loadScopedFilePaths(artifactId, scope);
|
|
2109
|
+
const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
|
|
2110
|
+
for (const chunk of chunkArray(filePaths, pageSize)) {
|
|
2111
|
+
const rows = this.filesRepo.getFileContentsByPaths(artifactId, chunk);
|
|
2112
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2113
|
+
this.metrics.recordSearchRowsScanned(rows.length);
|
|
2114
|
+
const symbolsByFile = includeDefinition
|
|
2115
|
+
? this.symbolsRepo.listSymbolsForFiles(artifactId, rows.map((row) => row.filePath), scope?.symbolKind)
|
|
2116
|
+
: new Map();
|
|
2117
|
+
if (includeDefinition) {
|
|
2118
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2119
|
+
this.metrics.recordSearchRowsScanned([...symbolsByFile.values()].reduce((acc, symbols) => acc + symbols.length, 0));
|
|
2120
|
+
}
|
|
2121
|
+
for (const row of rows) {
|
|
2122
|
+
const contentIndex = match === "regex"
|
|
2123
|
+
? matchRegexIndex(row.content, regexPattern)
|
|
2124
|
+
: findContentMatchIndex(row.content, query, match);
|
|
2125
|
+
if (contentIndex < 0) {
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
const line = indexToLine(row.content, contentIndex);
|
|
2129
|
+
const snippet = toContextSnippet(row.content, line, snippetWindow.before, snippetWindow.after, true);
|
|
2130
|
+
const definition = includeDefinition
|
|
2131
|
+
? this.findNearestSymbolFromList(symbolsByFile.get(row.filePath) ?? [], line)
|
|
2132
|
+
: undefined;
|
|
2133
|
+
const resolvedSymbol = definition ? lineToSymbol(definition) : undefined;
|
|
2134
|
+
onHit({
|
|
2135
|
+
filePath: row.filePath,
|
|
2136
|
+
score: scoreTextMatch(match, contentIndex) + (resolvedSymbol ? 20 : 0),
|
|
2137
|
+
matchedIn: "content",
|
|
2138
|
+
startLine: snippet.startLine,
|
|
2139
|
+
endLine: snippet.endLine,
|
|
2140
|
+
snippet: snippet.snippet,
|
|
2141
|
+
reasonCodes: ["content_match", `text_${match}`],
|
|
2142
|
+
symbol: resolvedSymbol
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
searchPathIntent(artifactId, query, match, scope, includeDefinition, snippetWindow, regexPattern, onHit) {
|
|
2148
|
+
const filePaths = this.loadScopedFilePaths(artifactId, scope);
|
|
2149
|
+
const matching = filePaths.flatMap((filePath) => {
|
|
2150
|
+
const pathIndex = match === "regex"
|
|
2151
|
+
? matchRegexIndex(filePath, regexPattern)
|
|
2152
|
+
: findMatchIndex(filePath, query, match);
|
|
2153
|
+
if (pathIndex < 0) {
|
|
2154
|
+
return [];
|
|
2155
|
+
}
|
|
2156
|
+
return [{ filePath, pathIndex }];
|
|
2157
|
+
});
|
|
2158
|
+
const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
|
|
2159
|
+
for (const chunk of chunkArray(matching, pageSize)) {
|
|
2160
|
+
const rows = this.filesRepo.getFileContentsByPaths(artifactId, chunk.map((item) => item.filePath));
|
|
2161
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2162
|
+
this.metrics.recordSearchRowsScanned(rows.length);
|
|
2163
|
+
const contentByPath = new Map(rows.map((row) => [row.filePath, row.content]));
|
|
2164
|
+
const symbolsByFile = includeDefinition
|
|
2165
|
+
? this.symbolsRepo.listSymbolsForFiles(artifactId, chunk.map((item) => item.filePath), scope?.symbolKind)
|
|
2166
|
+
: new Map();
|
|
2167
|
+
if (includeDefinition) {
|
|
2168
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2169
|
+
this.metrics.recordSearchRowsScanned([...symbolsByFile.values()].reduce((acc, symbols) => acc + symbols.length, 0));
|
|
2170
|
+
}
|
|
2171
|
+
for (const candidate of chunk) {
|
|
2172
|
+
const content = contentByPath.get(candidate.filePath);
|
|
2173
|
+
if (!content) {
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
const definition = includeDefinition
|
|
2177
|
+
? this.findNearestSymbolFromList(symbolsByFile.get(candidate.filePath) ?? [], 1)
|
|
2178
|
+
: undefined;
|
|
2179
|
+
const centerLine = definition?.line ?? 1;
|
|
2180
|
+
const snippet = toContextSnippet(content, centerLine, snippetWindow.before, snippetWindow.after, true);
|
|
2181
|
+
const resolvedSymbol = definition ? lineToSymbol(definition) : undefined;
|
|
2182
|
+
onHit({
|
|
2183
|
+
filePath: candidate.filePath,
|
|
2184
|
+
score: scorePathMatch(match, candidate.pathIndex) + (resolvedSymbol ? 10 : 0),
|
|
2185
|
+
matchedIn: "path",
|
|
2186
|
+
startLine: snippet.startLine,
|
|
2187
|
+
endLine: snippet.endLine,
|
|
2188
|
+
snippet: snippet.snippet,
|
|
2189
|
+
reasonCodes: ["path_match", `path_${match}`],
|
|
2190
|
+
symbol: resolvedSymbol
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
findSymbolHits(artifactId, query, match, scope, regexPattern) {
|
|
2196
|
+
if (match !== "regex") {
|
|
2197
|
+
const filePathLike = scope?.fileGlob ? globToSqlLike(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
2198
|
+
const scoped = this.symbolsRepo.findScopedSymbols({
|
|
2199
|
+
artifactId,
|
|
2200
|
+
query,
|
|
2201
|
+
match,
|
|
2202
|
+
symbolKind: scope?.symbolKind,
|
|
2203
|
+
packagePrefix: scope?.packagePrefix,
|
|
2204
|
+
filePathLike,
|
|
2205
|
+
limit: this.indexedCandidateLimit()
|
|
2206
|
+
});
|
|
2207
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2208
|
+
this.metrics.recordSearchRowsScanned(scoped.items.length);
|
|
2209
|
+
const result = [];
|
|
2210
|
+
for (const symbol of scoped.items) {
|
|
2211
|
+
if (!isSymbolKind(symbol.symbolKind)) {
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
const index = findMatchIndex(symbol.symbolName, query, match);
|
|
2215
|
+
if (index < 0) {
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
result.push({
|
|
2219
|
+
symbol,
|
|
2220
|
+
score: scoreSymbolMatch(match, index, symbol.symbolKind),
|
|
2221
|
+
matchIndex: index
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
return result;
|
|
2225
|
+
}
|
|
2226
|
+
const candidates = this.symbolsRepo.listSymbolsForArtifact(artifactId, scope?.symbolKind);
|
|
2227
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2228
|
+
this.metrics.recordSearchRowsScanned(candidates.length);
|
|
2229
|
+
const result = [];
|
|
2230
|
+
for (const symbol of candidates) {
|
|
2231
|
+
if (!checkPackagePrefix(symbol.filePath, scope?.packagePrefix)) {
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
if (scope?.fileGlob) {
|
|
2235
|
+
const glob = buildGlobRegex(normalizePathStyle(scope.fileGlob));
|
|
2236
|
+
if (!glob.test(symbol.filePath)) {
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
if (!isSymbolKind(symbol.symbolKind)) {
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2243
|
+
const index = match === "regex"
|
|
2244
|
+
? matchRegexIndex(symbol.symbolName, regexPattern)
|
|
2245
|
+
: findMatchIndex(symbol.symbolName, query, match);
|
|
2246
|
+
if (index < 0) {
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
result.push({
|
|
2250
|
+
symbol,
|
|
2251
|
+
score: scoreSymbolMatch(match, index, symbol.symbolKind),
|
|
2252
|
+
matchIndex: index
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
return result;
|
|
2256
|
+
}
|
|
2257
|
+
loadScopedFilePaths(artifactId, scope) {
|
|
2258
|
+
const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
2259
|
+
const scopedFilesBySymbolKind = scope?.symbolKind
|
|
2260
|
+
? new Set(this.symbolsRepo.listDistinctFilePathsByKind(artifactId, scope.symbolKind))
|
|
2261
|
+
: undefined;
|
|
2262
|
+
if (scopedFilesBySymbolKind) {
|
|
2263
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2264
|
+
this.metrics.recordSearchRowsScanned(scopedFilesBySymbolKind.size);
|
|
2265
|
+
}
|
|
2266
|
+
const result = [];
|
|
2267
|
+
let cursor = undefined;
|
|
2268
|
+
const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
|
|
2269
|
+
while (true) {
|
|
2270
|
+
const page = this.filesRepo.listFiles(artifactId, { limit: pageSize, cursor });
|
|
2271
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2272
|
+
this.metrics.recordSearchRowsScanned(page.items.length);
|
|
2273
|
+
for (const filePath of page.items) {
|
|
2274
|
+
if (!checkPackagePrefix(filePath, scope?.packagePrefix)) {
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
2277
|
+
if (glob && !glob.test(filePath)) {
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
if (scopedFilesBySymbolKind && !scopedFilesBySymbolKind.has(filePath)) {
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
2283
|
+
result.push(filePath);
|
|
2284
|
+
}
|
|
2285
|
+
if (!page.nextCursor) {
|
|
2286
|
+
break;
|
|
2287
|
+
}
|
|
2288
|
+
cursor = page.nextCursor;
|
|
2289
|
+
}
|
|
2290
|
+
return result;
|
|
2291
|
+
}
|
|
2292
|
+
indexedCandidateLimit() {
|
|
2293
|
+
return Math.min(Math.max(this.config.maxSearchHits * 5, 500), 5000);
|
|
2294
|
+
}
|
|
2295
|
+
indexedCandidateLimitForMatch(match) {
|
|
2296
|
+
const base = this.indexedCandidateLimit();
|
|
2297
|
+
if (match === "exact" || match === "prefix") {
|
|
2298
|
+
// Exact/prefix matches are more selective — fewer candidates needed
|
|
2299
|
+
return Math.min(base, 500);
|
|
2300
|
+
}
|
|
2301
|
+
// Contains matches need more candidates
|
|
2302
|
+
return base;
|
|
2303
|
+
}
|
|
2304
|
+
resolveClassFilePath(artifactId, className) {
|
|
2305
|
+
const normalizedClassName = className.trim();
|
|
2306
|
+
const classPath = normalizePathStyle(normalizedClassName.replace(/\./g, "/"));
|
|
2307
|
+
const candidates = new Set([`${classPath}.java`]);
|
|
2308
|
+
const innerIndex = classPath.indexOf("$");
|
|
2309
|
+
if (innerIndex > 0) {
|
|
2310
|
+
candidates.add(`${classPath.slice(0, innerIndex)}.java`);
|
|
2311
|
+
}
|
|
2312
|
+
for (const candidate of candidates) {
|
|
2313
|
+
const row = this.filesRepo.getFileContent(artifactId, candidate);
|
|
2314
|
+
if (row) {
|
|
2315
|
+
return row.filePath;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
const simpleName = normalizedClassName.split(/[.$]/).at(-1);
|
|
2319
|
+
if (!simpleName) {
|
|
2320
|
+
return undefined;
|
|
2321
|
+
}
|
|
2322
|
+
const classPathBySymbol = this.symbolsRepo.findBestClassFilePath(artifactId, normalizedClassName, simpleName);
|
|
2323
|
+
if (classPathBySymbol) {
|
|
2324
|
+
return classPathBySymbol;
|
|
2325
|
+
}
|
|
2326
|
+
return this.filesRepo.findFirstFilePathByName(artifactId, `${simpleName}.java`);
|
|
2327
|
+
}
|
|
2328
|
+
findNearestSymbolForLine(artifactId, filePath, line, symbolKind) {
|
|
2329
|
+
const symbols = this.symbolsRepo
|
|
2330
|
+
.listSymbolsForFile(artifactId, filePath)
|
|
2331
|
+
.filter((symbol) => (symbolKind ? symbol.symbolKind === symbolKind : true));
|
|
2332
|
+
return this.findNearestSymbolFromList(symbols, line);
|
|
2333
|
+
}
|
|
2334
|
+
findNearestSymbolFromList(symbols, line) {
|
|
2335
|
+
let best;
|
|
2336
|
+
for (const symbol of symbols) {
|
|
2337
|
+
if (symbol.line > line) {
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
if (!best || symbol.line >= best.line) {
|
|
2341
|
+
best = symbol;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return best ?? symbols[0];
|
|
2345
|
+
}
|
|
2346
|
+
buildOneHopRelations(artifactId, roots, maxRelations) {
|
|
2347
|
+
if (roots.length === 0 || maxRelations <= 0) {
|
|
2348
|
+
return [];
|
|
2349
|
+
}
|
|
2350
|
+
const rootRows = this.filesRepo.getFileContentsByPaths(artifactId, roots.map((root) => root.filePath));
|
|
2351
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2352
|
+
this.metrics.recordSearchRowsScanned(rootRows.length);
|
|
2353
|
+
const rootRowsByPath = new Map(rootRows.map((row) => [row.filePath, row]));
|
|
2354
|
+
const rootTokens = roots.map((root) => {
|
|
2355
|
+
const contentRow = rootRowsByPath.get(root.filePath);
|
|
2356
|
+
if (!contentRow) {
|
|
2357
|
+
return {
|
|
2358
|
+
root,
|
|
2359
|
+
calls: [],
|
|
2360
|
+
types: [],
|
|
2361
|
+
imports: []
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
const aroundRoot = toContextSnippet(contentRow.content, root.line, 2, 3, false).snippet;
|
|
2365
|
+
return {
|
|
2366
|
+
root,
|
|
2367
|
+
calls: Array.from(aroundRoot.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g))
|
|
2368
|
+
.map((match) => match[1])
|
|
2369
|
+
.filter((token) => Boolean(token)),
|
|
2370
|
+
types: Array.from(aroundRoot.matchAll(/\b([A-Z][A-Za-z0-9_$]*)\b/g))
|
|
2371
|
+
.map((match) => match[1])
|
|
2372
|
+
.filter((token) => Boolean(token)),
|
|
2373
|
+
imports: Array.from(aroundRoot.matchAll(/import\s+([\w.$]+);/g))
|
|
2374
|
+
.map((match) => match[1]?.split(".").at(-1))
|
|
2375
|
+
.filter((token) => Boolean(token))
|
|
2376
|
+
};
|
|
2377
|
+
});
|
|
2378
|
+
const tokenSet = new Set();
|
|
2379
|
+
for (const entry of rootTokens) {
|
|
2380
|
+
for (const token of entry.calls) {
|
|
2381
|
+
tokenSet.add(toLower(token));
|
|
2382
|
+
}
|
|
2383
|
+
for (const token of entry.types) {
|
|
2384
|
+
tokenSet.add(toLower(token));
|
|
2385
|
+
}
|
|
2386
|
+
for (const token of entry.imports) {
|
|
2387
|
+
tokenSet.add(toLower(token));
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
const matchedSymbols = this.symbolsRepo
|
|
2391
|
+
.findBySymbolNames(artifactId, [...tokenSet])
|
|
2392
|
+
.filter((symbol) => isSymbolKind(symbol.symbolKind));
|
|
2393
|
+
this.metrics.recordSearchDbRoundtrip();
|
|
2394
|
+
this.metrics.recordSearchRowsScanned(matchedSymbols.length);
|
|
2395
|
+
const symbolMap = new Map();
|
|
2396
|
+
for (const symbol of matchedSymbols) {
|
|
2397
|
+
const key = toLower(symbol.symbolName);
|
|
2398
|
+
const bucket = symbolMap.get(key) ?? [];
|
|
2399
|
+
bucket.push(symbol);
|
|
2400
|
+
symbolMap.set(key, bucket);
|
|
2401
|
+
}
|
|
2402
|
+
const dedupe = new Set();
|
|
2403
|
+
const relations = [];
|
|
2404
|
+
for (const entry of rootTokens) {
|
|
2405
|
+
const root = entry.root;
|
|
2406
|
+
const attach = (token, relationKind) => {
|
|
2407
|
+
const matches = symbolMap.get(toLower(token)) ?? [];
|
|
2408
|
+
for (const target of matches) {
|
|
2409
|
+
if (!isSymbolKind(target.symbolKind)) {
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
if (target.filePath === root.filePath &&
|
|
2413
|
+
target.line === root.line &&
|
|
2414
|
+
target.symbolName === root.symbolName &&
|
|
2415
|
+
target.symbolKind === root.symbolKind) {
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
const key = `${root.symbolKind}:${root.symbolName}:${root.filePath}:${root.line}->${target.symbolKind}:${target.symbolName}:${target.filePath}:${target.line}:${relationKind}`;
|
|
2419
|
+
if (dedupe.has(key)) {
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2422
|
+
dedupe.add(key);
|
|
2423
|
+
relations.push({
|
|
2424
|
+
fromSymbol: {
|
|
2425
|
+
symbolKind: root.symbolKind,
|
|
2426
|
+
symbolName: root.symbolName,
|
|
2427
|
+
filePath: root.filePath,
|
|
2428
|
+
line: root.line
|
|
2429
|
+
},
|
|
2430
|
+
toSymbol: {
|
|
2431
|
+
symbolKind: target.symbolKind,
|
|
2432
|
+
symbolName: target.symbolName,
|
|
2433
|
+
filePath: target.filePath,
|
|
2434
|
+
line: target.line
|
|
2435
|
+
},
|
|
2436
|
+
relation: relationKind
|
|
2437
|
+
});
|
|
2438
|
+
if (relations.length >= maxRelations) {
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
};
|
|
2443
|
+
for (const token of entry.calls) {
|
|
2444
|
+
attach(token, "calls");
|
|
2445
|
+
if (relations.length >= maxRelations) {
|
|
2446
|
+
return relations;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
for (const token of entry.types) {
|
|
2450
|
+
attach(token, "uses-type");
|
|
2451
|
+
if (relations.length >= maxRelations) {
|
|
2452
|
+
return relations;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
for (const token of entry.imports) {
|
|
2456
|
+
attach(token, "imports");
|
|
2457
|
+
if (relations.length >= maxRelations) {
|
|
2458
|
+
return relations;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
return relations;
|
|
2463
|
+
}
|
|
2464
|
+
buildProvenance(input) {
|
|
2465
|
+
const provenance = {
|
|
2466
|
+
target: input.requestedTarget,
|
|
2467
|
+
resolvedAt: input.resolved.resolvedAt,
|
|
2468
|
+
resolvedFrom: {
|
|
2469
|
+
origin: input.resolved.origin,
|
|
2470
|
+
sourceJarPath: input.resolved.sourceJarPath,
|
|
2471
|
+
binaryJarPath: input.resolved.binaryJarPath,
|
|
2472
|
+
coordinate: input.resolved.coordinate,
|
|
2473
|
+
version: input.resolved.version,
|
|
2474
|
+
repoUrl: input.resolved.repoUrl
|
|
2475
|
+
},
|
|
2476
|
+
transformChain: [...input.transformChain]
|
|
2477
|
+
};
|
|
2478
|
+
if (!provenance.resolvedAt || !provenance.target.kind || !provenance.target.value) {
|
|
2479
|
+
throw createError({
|
|
2480
|
+
code: ERROR_CODES.PROVENANCE_INCOMPLETE,
|
|
2481
|
+
message: "Artifact provenance is incomplete.",
|
|
2482
|
+
details: {
|
|
2483
|
+
artifactId: input.resolved.artifactId,
|
|
2484
|
+
provenance
|
|
2485
|
+
}
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
return provenance;
|
|
2489
|
+
}
|
|
2490
|
+
buildFallbackProvenance(input) {
|
|
2491
|
+
const artifact = this.getArtifact(input.artifactId);
|
|
2492
|
+
const fallbackTarget = artifact.version
|
|
2493
|
+
? { kind: "version", value: artifact.version }
|
|
2494
|
+
: artifact.coordinate
|
|
2495
|
+
? { kind: "coordinate", value: artifact.coordinate }
|
|
2496
|
+
: { kind: "jar", value: artifact.sourceJarPath ?? artifact.binaryJarPath ?? input.artifactId };
|
|
2497
|
+
const transformChain = artifact.provenance?.transformChain && artifact.provenance.transformChain.length > 0
|
|
2498
|
+
? artifact.provenance.transformChain
|
|
2499
|
+
: [`mapping:${input.requestedMapping}->${input.mappingApplied}`];
|
|
2500
|
+
return {
|
|
2501
|
+
target: fallbackTarget,
|
|
2502
|
+
resolvedAt: artifact.updatedAt,
|
|
2503
|
+
resolvedFrom: {
|
|
2504
|
+
origin: artifact.origin,
|
|
2505
|
+
sourceJarPath: artifact.sourceJarPath,
|
|
2506
|
+
binaryJarPath: artifact.binaryJarPath,
|
|
2507
|
+
coordinate: artifact.coordinate,
|
|
2508
|
+
version: artifact.version,
|
|
2509
|
+
repoUrl: artifact.repoUrl
|
|
2510
|
+
},
|
|
2511
|
+
transformChain
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
async resolveToOfficialClassName(className, version, mapping, sourcePriority, warnings) {
|
|
2515
|
+
if (mapping === "official") {
|
|
2516
|
+
return className;
|
|
2517
|
+
}
|
|
2518
|
+
try {
|
|
2519
|
+
const mapped = await this.mappingService.findMapping({
|
|
2520
|
+
version,
|
|
2521
|
+
kind: "class",
|
|
2522
|
+
name: className,
|
|
2523
|
+
sourceMapping: mapping,
|
|
2524
|
+
targetMapping: "official",
|
|
2525
|
+
sourcePriority
|
|
2526
|
+
});
|
|
2527
|
+
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
2528
|
+
return mapped.resolvedSymbol.name;
|
|
2529
|
+
}
|
|
2530
|
+
warnings.push(`Could not map class "${className}" from ${mapping} to official.`);
|
|
2531
|
+
}
|
|
2532
|
+
catch {
|
|
2533
|
+
warnings.push(`Mapping lookup failed for class "${className}".`);
|
|
2534
|
+
}
|
|
2535
|
+
return className;
|
|
2536
|
+
}
|
|
2537
|
+
async resolveToOfficialMemberName(name, ownerInSourceMapping, descriptor, kind, version, mapping, sourcePriority, warnings) {
|
|
2538
|
+
if (mapping === "official") {
|
|
2539
|
+
return {
|
|
2540
|
+
name,
|
|
2541
|
+
descriptor: kind === "method" ? descriptor : undefined
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
try {
|
|
2545
|
+
const mapped = await this.mappingService.findMapping({
|
|
2546
|
+
version,
|
|
2547
|
+
kind,
|
|
2548
|
+
name,
|
|
2549
|
+
owner: ownerInSourceMapping,
|
|
2550
|
+
descriptor,
|
|
2551
|
+
sourceMapping: mapping,
|
|
2552
|
+
targetMapping: "official",
|
|
2553
|
+
sourcePriority
|
|
2554
|
+
});
|
|
2555
|
+
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
2556
|
+
return {
|
|
2557
|
+
name: mapped.resolvedSymbol.name,
|
|
2558
|
+
descriptor: kind === "method" ? mapped.resolvedSymbol.descriptor ?? descriptor : undefined
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
warnings.push(`Could not map ${kind} "${name}" from ${mapping} to official.`);
|
|
2562
|
+
}
|
|
2563
|
+
catch {
|
|
2564
|
+
warnings.push(`Mapping lookup failed for ${kind} "${name}".`);
|
|
2565
|
+
}
|
|
2566
|
+
return {
|
|
2567
|
+
name,
|
|
2568
|
+
descriptor: kind === "method" ? descriptor : undefined
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
async remapSignatureMembers(members, kind, version, mapping, sourcePriority, warnings) {
|
|
2572
|
+
if (mapping === "official") {
|
|
2573
|
+
return members;
|
|
2574
|
+
}
|
|
2575
|
+
// Build deduplicated lookup tables for member names and owner FQNs
|
|
2576
|
+
const memberKeyToRemapped = new Map();
|
|
2577
|
+
const ownerToRemapped = new Map();
|
|
2578
|
+
for (const member of members) {
|
|
2579
|
+
const memberKey = `${member.ownerFqn}\0${member.name}\0${member.jvmDescriptor}`;
|
|
2580
|
+
if (!memberKeyToRemapped.has(memberKey)) {
|
|
2581
|
+
memberKeyToRemapped.set(memberKey, member.name); // default = official name
|
|
2582
|
+
}
|
|
2583
|
+
if (!ownerToRemapped.has(member.ownerFqn)) {
|
|
2584
|
+
ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = official FQN
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
// Remap unique member names
|
|
2588
|
+
const memberEntries = [...memberKeyToRemapped.entries()];
|
|
2589
|
+
await Promise.all(memberEntries.map(async ([key, _officialName]) => {
|
|
2590
|
+
const [ownerFqn, name, descriptor] = key.split("\0");
|
|
2591
|
+
try {
|
|
2592
|
+
const mapped = await this.mappingService.findMapping({
|
|
2593
|
+
version,
|
|
2594
|
+
kind,
|
|
2595
|
+
name,
|
|
2596
|
+
owner: ownerFqn,
|
|
2597
|
+
descriptor: kind === "method" ? descriptor : undefined,
|
|
2598
|
+
sourceMapping: "official",
|
|
2599
|
+
targetMapping: mapping,
|
|
2600
|
+
sourcePriority
|
|
2601
|
+
});
|
|
2602
|
+
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
2603
|
+
memberKeyToRemapped.set(key, mapped.resolvedSymbol.name);
|
|
2604
|
+
}
|
|
2605
|
+
else {
|
|
2606
|
+
warnings.push(`Could not remap ${kind} "${name}" to ${mapping}.`);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
catch {
|
|
2610
|
+
warnings.push(`Remap failed for ${kind} "${name}".`);
|
|
2611
|
+
}
|
|
2612
|
+
}));
|
|
2613
|
+
// Remap unique owner FQNs
|
|
2614
|
+
const ownerEntries = [...ownerToRemapped.entries()];
|
|
2615
|
+
await Promise.all(ownerEntries.map(async ([officialFqn]) => {
|
|
2616
|
+
try {
|
|
2617
|
+
const mapped = await this.mappingService.findMapping({
|
|
2618
|
+
version,
|
|
2619
|
+
kind: "class",
|
|
2620
|
+
name: officialFqn,
|
|
2621
|
+
sourceMapping: "official",
|
|
2622
|
+
targetMapping: mapping,
|
|
2623
|
+
sourcePriority
|
|
2624
|
+
});
|
|
2625
|
+
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
2626
|
+
ownerToRemapped.set(officialFqn, mapped.resolvedSymbol.name);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
catch {
|
|
2630
|
+
// keep official FQN as fallback
|
|
2631
|
+
}
|
|
2632
|
+
}));
|
|
2633
|
+
return members.map((member) => {
|
|
2634
|
+
const memberKey = `${member.ownerFqn}\0${member.name}\0${member.jvmDescriptor}`;
|
|
2635
|
+
return {
|
|
2636
|
+
...member,
|
|
2637
|
+
name: memberKeyToRemapped.get(memberKey) ?? member.name,
|
|
2638
|
+
ownerFqn: ownerToRemapped.get(member.ownerFqn) ?? member.ownerFqn
|
|
2639
|
+
};
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
fallbackArtifactSignature(artifactId) {
|
|
2643
|
+
return createHash("sha256").update(artifactId).digest("hex");
|
|
2644
|
+
}
|
|
2645
|
+
resolveIndexRebuildReason(input) {
|
|
2646
|
+
if (input.force) {
|
|
2647
|
+
return "force";
|
|
2648
|
+
}
|
|
2649
|
+
if (!input.hasFiles || !input.meta) {
|
|
2650
|
+
return "missing_meta";
|
|
2651
|
+
}
|
|
2652
|
+
if (input.meta.indexSchemaVersion !== INDEX_SCHEMA_VERSION) {
|
|
2653
|
+
return "schema_mismatch";
|
|
2654
|
+
}
|
|
2655
|
+
if (input.meta.artifactSignature !== input.expectedSignature) {
|
|
2656
|
+
return "signature_mismatch";
|
|
2657
|
+
}
|
|
2658
|
+
return "already_current";
|
|
2659
|
+
}
|
|
2660
|
+
toResolvedArtifact(artifact) {
|
|
2661
|
+
return {
|
|
2662
|
+
artifactId: artifact.artifactId,
|
|
2663
|
+
artifactSignature: artifact.artifactSignature ?? this.fallbackArtifactSignature(artifact.artifactId),
|
|
2664
|
+
origin: artifact.origin,
|
|
2665
|
+
binaryJarPath: artifact.binaryJarPath,
|
|
2666
|
+
sourceJarPath: artifact.sourceJarPath,
|
|
2667
|
+
coordinate: artifact.coordinate,
|
|
2668
|
+
version: artifact.version,
|
|
2669
|
+
requestedMapping: artifact.requestedMapping,
|
|
2670
|
+
mappingApplied: artifact.mappingApplied,
|
|
2671
|
+
repoUrl: artifact.repoUrl,
|
|
2672
|
+
provenance: artifact.provenance,
|
|
2673
|
+
qualityFlags: artifact.qualityFlags,
|
|
2674
|
+
isDecompiled: artifact.isDecompiled,
|
|
2675
|
+
resolvedAt: new Date().toISOString()
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
async rebuildAndPersistArtifactIndex(resolved, reason) {
|
|
2679
|
+
const rebuilt = await this.buildRebuiltArtifactData(resolved);
|
|
2680
|
+
const timestamp = new Date().toISOString();
|
|
2681
|
+
const chunkSize = Math.max(1, this.config.indexInsertChunkSize ?? 200);
|
|
2682
|
+
const tx = this.db.transaction(() => {
|
|
2683
|
+
this.artifactsRepo.upsertArtifact({
|
|
2684
|
+
artifactId: resolved.artifactId,
|
|
2685
|
+
origin: resolved.origin,
|
|
2686
|
+
coordinate: resolved.coordinate,
|
|
2687
|
+
version: resolved.version,
|
|
2688
|
+
binaryJarPath: resolved.binaryJarPath,
|
|
2689
|
+
sourceJarPath: resolved.sourceJarPath,
|
|
2690
|
+
repoUrl: resolved.repoUrl,
|
|
2691
|
+
requestedMapping: resolved.requestedMapping,
|
|
2692
|
+
mappingApplied: resolved.mappingApplied,
|
|
2693
|
+
provenance: resolved.provenance,
|
|
2694
|
+
qualityFlags: resolved.qualityFlags,
|
|
2695
|
+
artifactSignature: resolved.artifactSignature,
|
|
2696
|
+
isDecompiled: resolved.isDecompiled,
|
|
2697
|
+
timestamp
|
|
2698
|
+
});
|
|
2699
|
+
this.filesRepo.clearFilesForArtifact(resolved.artifactId);
|
|
2700
|
+
for (const chunk of chunkArray(rebuilt.files, chunkSize)) {
|
|
2701
|
+
this.filesRepo.insertFilesForArtifact(resolved.artifactId, chunk);
|
|
2702
|
+
}
|
|
2703
|
+
this.symbolsRepo.clearSymbolsForArtifact(resolved.artifactId);
|
|
2704
|
+
for (const chunk of chunkArray(rebuilt.symbols, chunkSize)) {
|
|
2705
|
+
this.symbolsRepo.insertSymbolsForArtifact(resolved.artifactId, chunk);
|
|
2706
|
+
}
|
|
2707
|
+
this.indexMetaRepo.upsert({
|
|
2708
|
+
artifactId: resolved.artifactId,
|
|
2709
|
+
artifactSignature: resolved.artifactSignature,
|
|
2710
|
+
indexSchemaVersion: INDEX_SCHEMA_VERSION,
|
|
2711
|
+
filesCount: rebuilt.files.length,
|
|
2712
|
+
symbolsCount: rebuilt.symbols.length,
|
|
2713
|
+
ftsRowsCount: rebuilt.files.length,
|
|
2714
|
+
indexedAt: rebuilt.indexedAt,
|
|
2715
|
+
indexDurationMs: rebuilt.indexDurationMs
|
|
2716
|
+
});
|
|
2717
|
+
});
|
|
2718
|
+
tx();
|
|
2719
|
+
log("info", "index.rebuild.done", {
|
|
2720
|
+
artifactId: resolved.artifactId,
|
|
2721
|
+
reason,
|
|
2722
|
+
files: rebuilt.files.length,
|
|
2723
|
+
symbols: rebuilt.symbols.length,
|
|
2724
|
+
indexDurationMs: rebuilt.indexDurationMs
|
|
2725
|
+
});
|
|
2726
|
+
return rebuilt;
|
|
2727
|
+
}
|
|
2728
|
+
async buildRebuiltArtifactData(resolved) {
|
|
2729
|
+
const indexStartedAt = Date.now();
|
|
2730
|
+
let files = [];
|
|
2731
|
+
if (resolved.sourceJarPath) {
|
|
2732
|
+
files = await this.loadFromSourceJar(resolved.sourceJarPath);
|
|
2733
|
+
}
|
|
2734
|
+
else if (resolved.binaryJarPath) {
|
|
2735
|
+
const vineflowerPath = await resolveVineflowerJar(this.config.cacheDir, this.config.vineflowerJarPath);
|
|
2736
|
+
const decompileStartedAt = Date.now();
|
|
2737
|
+
try {
|
|
2738
|
+
const decompileResult = await decompileBinaryJar(resolved.binaryJarPath, this.config.cacheDir, {
|
|
2739
|
+
vineflowerJarPath: vineflowerPath,
|
|
2740
|
+
artifactIdCandidate: resolved.artifactId,
|
|
2741
|
+
timeoutMs: 120_000,
|
|
2742
|
+
signature: resolved.artifactId
|
|
2743
|
+
});
|
|
2744
|
+
files = decompileResult.javaFiles.map((entry) => ({
|
|
2745
|
+
filePath: normalizePathStyle(entry.filePath),
|
|
2746
|
+
content: entry.content,
|
|
2747
|
+
contentBytes: Buffer.byteLength(entry.content, "utf8"),
|
|
2748
|
+
contentHash: createHash("sha256").update(entry.content).digest("hex")
|
|
2749
|
+
}));
|
|
2750
|
+
}
|
|
2751
|
+
finally {
|
|
2752
|
+
this.metrics.recordDuration("decompile_duration_ms", Date.now() - decompileStartedAt);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
else {
|
|
2756
|
+
throw createError({
|
|
2757
|
+
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
2758
|
+
message: "No source artifact available.",
|
|
2759
|
+
details: { artifactId: resolved.artifactId }
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
const symbols = [];
|
|
2763
|
+
for (const file of files) {
|
|
2764
|
+
const extracted = extractSymbolsFromSource(file.filePath, file.content);
|
|
2765
|
+
for (const symbol of extracted) {
|
|
2766
|
+
symbols.push({
|
|
2767
|
+
filePath: file.filePath,
|
|
2768
|
+
...symbol
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
return {
|
|
2773
|
+
files,
|
|
2774
|
+
symbols,
|
|
2775
|
+
indexedAt: new Date().toISOString(),
|
|
2776
|
+
indexDurationMs: Date.now() - indexStartedAt
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
getArtifact(artifactId) {
|
|
2780
|
+
if (artifactId.includes("..") || artifactId.includes("/")) {
|
|
2781
|
+
// intentionally reject suspicious IDs that are not artifact hashes
|
|
2782
|
+
throw createError({
|
|
2783
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
2784
|
+
message: "artifactId contains invalid characters.",
|
|
2785
|
+
details: { artifactId }
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
const artifact = this.artifactsRepo.getArtifact(artifactId);
|
|
2789
|
+
if (!artifact) {
|
|
2790
|
+
throw createError({
|
|
2791
|
+
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
2792
|
+
message: "Artifact not found. Resolve context first.",
|
|
2793
|
+
details: { artifactId }
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
return artifact;
|
|
2797
|
+
}
|
|
2798
|
+
async ingestIfNeeded(resolved) {
|
|
2799
|
+
const existing = this.artifactsRepo.getArtifact(resolved.artifactId);
|
|
2800
|
+
const hasFiles = this.hasAnyFiles(resolved.artifactId);
|
|
2801
|
+
const meta = this.indexMetaRepo.get(resolved.artifactId);
|
|
2802
|
+
const reason = this.resolveIndexRebuildReason({
|
|
2803
|
+
force: false,
|
|
2804
|
+
expectedSignature: resolved.artifactSignature,
|
|
2805
|
+
hasFiles,
|
|
2806
|
+
meta
|
|
2807
|
+
});
|
|
2808
|
+
if (existing && reason === "already_current") {
|
|
2809
|
+
this.metrics.recordArtifactCacheHit();
|
|
2810
|
+
this.artifactsRepo.touchArtifact(resolved.artifactId, new Date().toISOString());
|
|
2811
|
+
this.refreshCacheMetrics();
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
this.metrics.recordArtifactCacheMiss();
|
|
2815
|
+
this.metrics.recordReindex();
|
|
2816
|
+
log("info", "index.rebuild.start", {
|
|
2817
|
+
artifactId: resolved.artifactId,
|
|
2818
|
+
reason
|
|
2819
|
+
});
|
|
2820
|
+
await this.rebuildAndPersistArtifactIndex(resolved, reason === "already_current" ? "missing_meta" : reason);
|
|
2821
|
+
this.enforceCacheLimits();
|
|
2822
|
+
this.refreshCacheMetrics();
|
|
2823
|
+
}
|
|
2824
|
+
async loadFromSourceJar(sourceJarPath) {
|
|
2825
|
+
const files = [];
|
|
2826
|
+
for await (const entry of iterateJavaEntriesAsUtf8(sourceJarPath, this.config.maxContentBytes)) {
|
|
2827
|
+
files.push({
|
|
2828
|
+
filePath: normalizePathStyle(entry.filePath),
|
|
2829
|
+
content: entry.content,
|
|
2830
|
+
contentBytes: Buffer.byteLength(entry.content, "utf8"),
|
|
2831
|
+
contentHash: createHash("sha256").update(entry.content).digest("hex")
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
return files;
|
|
2835
|
+
}
|
|
2836
|
+
hasAnyFiles(artifactId) {
|
|
2837
|
+
return this.filesRepo.listFiles(artifactId, { limit: 1 }).items.length > 0;
|
|
2838
|
+
}
|
|
2839
|
+
enforceCacheLimits() {
|
|
2840
|
+
let artifactCount = this.artifactsRepo.countArtifacts();
|
|
2841
|
+
let totalBytes = this.artifactsRepo.totalContentBytes();
|
|
2842
|
+
if (artifactCount <= this.config.maxArtifacts && totalBytes <= this.config.maxCacheBytes) {
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
const candidates = this.artifactsRepo.listArtifactsByLruWithContentBytes(Math.max(artifactCount, 1));
|
|
2846
|
+
for (const candidate of candidates) {
|
|
2847
|
+
const shouldEvict = artifactCount > this.config.maxArtifacts || totalBytes > this.config.maxCacheBytes;
|
|
2848
|
+
if (!shouldEvict || artifactCount <= 1) {
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
const artifactCountBefore = artifactCount;
|
|
2852
|
+
const totalBytesBefore = totalBytes;
|
|
2853
|
+
this.filesRepo.deleteFilesForArtifact(candidate.artifactId);
|
|
2854
|
+
this.artifactsRepo.deleteArtifact(candidate.artifactId);
|
|
2855
|
+
artifactCount = Math.max(0, artifactCount - 1);
|
|
2856
|
+
totalBytes = Math.max(0, totalBytes - candidate.totalContentBytes);
|
|
2857
|
+
this.metrics.recordCacheEviction();
|
|
2858
|
+
log("warn", "cache.evict", {
|
|
2859
|
+
artifactId: candidate.artifactId,
|
|
2860
|
+
artifactCountBefore,
|
|
2861
|
+
totalBytesBefore,
|
|
2862
|
+
artifactBytes: candidate.totalContentBytes
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
refreshCacheMetrics() {
|
|
2867
|
+
const cacheEntries = this.artifactsRepo.countArtifacts();
|
|
2868
|
+
const totalContentBytes = this.artifactsRepo.totalContentBytes();
|
|
2869
|
+
const lruAccounting = this.artifactsRepo
|
|
2870
|
+
.listArtifactsByLruWithContentBytes(Math.max(cacheEntries, 1))
|
|
2871
|
+
.map((row) => ({
|
|
2872
|
+
artifact_id: row.artifactId,
|
|
2873
|
+
content_bytes: row.totalContentBytes,
|
|
2874
|
+
updated_at: row.updatedAt
|
|
2875
|
+
}));
|
|
2876
|
+
this.metrics.setCacheEntries(cacheEntries);
|
|
2877
|
+
this.metrics.setCacheTotalContentBytes(totalContentBytes);
|
|
2878
|
+
this.metrics.setCacheArtifactByteAccounting(lruAccounting);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
//# sourceMappingURL=source-service.js.map
|