@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.
Files changed (106) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +765 -0
  4. package/dist/access-widener-parser.d.ts +24 -0
  5. package/dist/access-widener-parser.js +77 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +4 -0
  8. package/dist/config.d.ts +27 -0
  9. package/dist/config.js +178 -0
  10. package/dist/decompiler/vineflower.d.ts +15 -0
  11. package/dist/decompiler/vineflower.js +185 -0
  12. package/dist/errors.d.ts +50 -0
  13. package/dist/errors.js +49 -0
  14. package/dist/hash.d.ts +1 -0
  15. package/dist/hash.js +12 -0
  16. package/dist/index.d.ts +7 -0
  17. package/dist/index.js +1447 -0
  18. package/dist/java-process.d.ts +16 -0
  19. package/dist/java-process.js +120 -0
  20. package/dist/logger.d.ts +3 -0
  21. package/dist/logger.js +21 -0
  22. package/dist/mapping-pipeline-service.d.ts +18 -0
  23. package/dist/mapping-pipeline-service.js +60 -0
  24. package/dist/mapping-service.d.ts +161 -0
  25. package/dist/mapping-service.js +1706 -0
  26. package/dist/maven-resolver.d.ts +22 -0
  27. package/dist/maven-resolver.js +122 -0
  28. package/dist/minecraft-explorer-service.d.ts +43 -0
  29. package/dist/minecraft-explorer-service.js +562 -0
  30. package/dist/mixin-parser.d.ts +34 -0
  31. package/dist/mixin-parser.js +194 -0
  32. package/dist/mixin-validator.d.ts +59 -0
  33. package/dist/mixin-validator.js +274 -0
  34. package/dist/mod-analyzer.d.ts +23 -0
  35. package/dist/mod-analyzer.js +346 -0
  36. package/dist/mod-decompile-service.d.ts +39 -0
  37. package/dist/mod-decompile-service.js +136 -0
  38. package/dist/mod-remap-service.d.ts +17 -0
  39. package/dist/mod-remap-service.js +186 -0
  40. package/dist/mod-search-service.d.ts +28 -0
  41. package/dist/mod-search-service.js +174 -0
  42. package/dist/mojang-tiny-mapping-service.d.ts +13 -0
  43. package/dist/mojang-tiny-mapping-service.js +351 -0
  44. package/dist/nbt/java-nbt-codec.d.ts +3 -0
  45. package/dist/nbt/java-nbt-codec.js +385 -0
  46. package/dist/nbt/json-patch.d.ts +3 -0
  47. package/dist/nbt/json-patch.js +352 -0
  48. package/dist/nbt/pipeline.d.ts +39 -0
  49. package/dist/nbt/pipeline.js +173 -0
  50. package/dist/nbt/typed-json.d.ts +10 -0
  51. package/dist/nbt/typed-json.js +205 -0
  52. package/dist/nbt/types.d.ts +66 -0
  53. package/dist/nbt/types.js +2 -0
  54. package/dist/observability.d.ts +88 -0
  55. package/dist/observability.js +165 -0
  56. package/dist/path-converter.d.ts +12 -0
  57. package/dist/path-converter.js +161 -0
  58. package/dist/path-resolver.d.ts +19 -0
  59. package/dist/path-resolver.js +78 -0
  60. package/dist/registry-service.d.ts +29 -0
  61. package/dist/registry-service.js +214 -0
  62. package/dist/repo-downloader.d.ts +15 -0
  63. package/dist/repo-downloader.js +111 -0
  64. package/dist/resources.d.ts +3 -0
  65. package/dist/resources.js +154 -0
  66. package/dist/search-hit-accumulator.d.ts +38 -0
  67. package/dist/search-hit-accumulator.js +153 -0
  68. package/dist/source-jar-reader.d.ts +13 -0
  69. package/dist/source-jar-reader.js +216 -0
  70. package/dist/source-resolver.d.ts +14 -0
  71. package/dist/source-resolver.js +274 -0
  72. package/dist/source-service.d.ts +404 -0
  73. package/dist/source-service.js +2881 -0
  74. package/dist/storage/artifacts-repo.d.ts +45 -0
  75. package/dist/storage/artifacts-repo.js +209 -0
  76. package/dist/storage/db.d.ts +14 -0
  77. package/dist/storage/db.js +132 -0
  78. package/dist/storage/files-repo.d.ts +78 -0
  79. package/dist/storage/files-repo.js +437 -0
  80. package/dist/storage/index-meta-repo.d.ts +35 -0
  81. package/dist/storage/index-meta-repo.js +97 -0
  82. package/dist/storage/migrations.d.ts +11 -0
  83. package/dist/storage/migrations.js +71 -0
  84. package/dist/storage/schema.d.ts +1 -0
  85. package/dist/storage/schema.js +160 -0
  86. package/dist/storage/sqlite.d.ts +20 -0
  87. package/dist/storage/sqlite.js +111 -0
  88. package/dist/storage/symbols-repo.d.ts +63 -0
  89. package/dist/storage/symbols-repo.js +401 -0
  90. package/dist/symbols/symbol-extractor.d.ts +7 -0
  91. package/dist/symbols/symbol-extractor.js +64 -0
  92. package/dist/tiny-remapper-resolver.d.ts +1 -0
  93. package/dist/tiny-remapper-resolver.js +62 -0
  94. package/dist/tiny-remapper-service.d.ts +16 -0
  95. package/dist/tiny-remapper-service.js +73 -0
  96. package/dist/types.d.ts +120 -0
  97. package/dist/types.js +2 -0
  98. package/dist/version-diff-service.d.ts +41 -0
  99. package/dist/version-diff-service.js +222 -0
  100. package/dist/version-service.d.ts +70 -0
  101. package/dist/version-service.js +411 -0
  102. package/dist/vineflower-resolver.d.ts +1 -0
  103. package/dist/vineflower-resolver.js +62 -0
  104. package/dist/workspace-mapping-service.d.ts +18 -0
  105. package/dist/workspace-mapping-service.js +89 -0
  106. 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