@adhisang/minecraft-modding-mcp 3.0.0 → 3.1.1

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.
@@ -159,6 +159,7 @@ function clampLimit(limit, fallback, max) {
159
159
  }
160
160
  const MAX_REGEX_QUERY_LENGTH = 200;
161
161
  const MAX_REGEX_RESULT_LIMIT = 100;
162
+ const TRACE_LIFECYCLE_MAX_CONCURRENCY = 3;
162
163
  function normalizePathStyle(path) {
163
164
  return path.replaceAll("\\", "/");
164
165
  }
@@ -214,6 +215,18 @@ function normalizeOptionalProjectPath(projectPath) {
214
215
  const normalized = normalizePathForHost(trimmed, undefined, "projectPath");
215
216
  return isAbsolute(normalized) ? normalized : resolvePath(process.cwd(), normalized);
216
217
  }
218
+ function looksLikeClassSegment(name) {
219
+ const trimmed = name.trim();
220
+ return /^[A-Z_$]/.test(trimmed);
221
+ }
222
+ function looksLikeJvmMethodDescriptor(descriptor) {
223
+ const trimmed = descriptor?.trim();
224
+ if (!trimmed || !trimmed.startsWith("(")) {
225
+ return false;
226
+ }
227
+ const closing = trimmed.indexOf(")");
228
+ return closing > 0 && closing < trimmed.length - 1;
229
+ }
217
230
  function resolveGradleUserHomePath() {
218
231
  const configured = process.env.GRADLE_USER_HOME?.trim();
219
232
  if (!configured) {
@@ -278,16 +291,19 @@ function scopeToJarType(scope) {
278
291
  }
279
292
  function parseQualifiedMethodSymbol(symbol) {
280
293
  const trimmed = symbol.trim();
281
- const separator = trimmed.lastIndexOf(".");
282
- if (separator <= 0 || separator >= trimmed.length - 1) {
294
+ const descriptorStart = trimmed.indexOf("(");
295
+ const qualifiedSymbol = descriptorStart >= 0 ? trimmed.slice(0, descriptorStart) : trimmed;
296
+ const inlineDescriptor = descriptorStart >= 0 ? trimmed.slice(descriptorStart).trim() : undefined;
297
+ const separator = qualifiedSymbol.lastIndexOf(".");
298
+ if (separator <= 0 || separator >= qualifiedSymbol.length - 1) {
283
299
  throw createError({
284
300
  code: ERROR_CODES.INVALID_INPUT,
285
301
  message: `symbol must be in the form "fully.qualified.Class.method".`,
286
302
  details: { symbol }
287
303
  });
288
304
  }
289
- const className = trimmed.slice(0, separator);
290
- const methodName = trimmed.slice(separator + 1);
305
+ const className = qualifiedSymbol.slice(0, separator);
306
+ const methodName = qualifiedSymbol.slice(separator + 1);
291
307
  if (!className ||
292
308
  !methodName ||
293
309
  className.includes("/") ||
@@ -299,7 +315,11 @@ function parseQualifiedMethodSymbol(symbol) {
299
315
  details: { symbol }
300
316
  });
301
317
  }
302
- return { className, methodName };
318
+ return {
319
+ className,
320
+ methodName,
321
+ ...(inlineDescriptor ? { inlineDescriptor } : {})
322
+ };
303
323
  }
304
324
  function normalizeOptionalString(value) {
305
325
  if (value == null) {
@@ -308,6 +328,27 @@ function normalizeOptionalString(value) {
308
328
  const trimmed = value.trim();
309
329
  return trimmed ? trimmed : undefined;
310
330
  }
331
+ async function mapWithConcurrencyLimit(items, limit, mapper) {
332
+ if (items.length === 0) {
333
+ return [];
334
+ }
335
+ const results = new Array(items.length);
336
+ let nextIndex = 0;
337
+ const workerCount = Math.max(1, Math.min(Math.trunc(limit), items.length));
338
+ await Promise.all(Array.from({ length: workerCount }, async () => {
339
+ while (true) {
340
+ // Safe in Node's single-threaded event loop because no await occurs between
341
+ // reading and incrementing nextIndex inside this synchronous dispatch section.
342
+ const currentIndex = nextIndex;
343
+ nextIndex += 1;
344
+ if (currentIndex >= items.length) {
345
+ return;
346
+ }
347
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
348
+ }
349
+ }));
350
+ return results;
351
+ }
311
352
  function normalizeStrictPositiveInt(value, field) {
312
353
  if (value == null) {
313
354
  return undefined;
@@ -1208,6 +1249,7 @@ export class SourceService {
1208
1249
  const limit = clampLimit(input.limit, 20, searchLimitCap);
1209
1250
  const regexPattern = match === "regex" ? compileRegex(query) : undefined;
1210
1251
  const queryMode = input.queryMode ?? "auto";
1252
+ this.metrics.recordSearchQueryMode(queryMode);
1211
1253
  const cursorContext = buildSearchCursorContext({
1212
1254
  artifactId: artifact.artifactId,
1213
1255
  query,
@@ -1227,7 +1269,6 @@ export class SourceService {
1227
1269
  const recordHit = (hit) => {
1228
1270
  accumulator.add(hit);
1229
1271
  };
1230
- const hasSeparators = /[._$]/.test(query);
1231
1272
  const tokenOnlyTextIntent = intent === "text" && queryMode === "token";
1232
1273
  if (intent === "symbol") {
1233
1274
  this.searchSymbolIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
@@ -1256,10 +1297,6 @@ export class SourceService {
1256
1297
  }
1257
1298
  else {
1258
1299
  this.searchTextIntentIndexed(artifact.artifactId, query, match, scope, recordHit);
1259
- // F-03: queryMode=auto fallback — when indexed returns 0 hits and query has separators, retry with literal scan
1260
- if (queryMode === "auto" && hasSeparators && accumulator.currentCount() === 0) {
1261
- this.searchTextIntent(artifact.artifactId, query, match, scope, regexPattern, recordHit);
1262
- }
1263
1300
  }
1264
1301
  this.metrics.recordSearchIndexedHit();
1265
1302
  }
@@ -1648,8 +1685,9 @@ export class SourceService {
1648
1685
  }
1649
1686
  async traceSymbolLifecycle(input) {
1650
1687
  const mapping = normalizeMapping(input.mapping);
1651
- const { className: userClassName, methodName: userMethodName } = parseQualifiedMethodSymbol(input.symbol);
1652
- const descriptor = normalizeOptionalString(input.descriptor);
1688
+ const { className: userClassName, methodName: userMethodName, inlineDescriptor } = parseQualifiedMethodSymbol(input.symbol);
1689
+ const descriptor = normalizeOptionalString(input.descriptor)
1690
+ ?? (looksLikeJvmMethodDescriptor(inlineDescriptor) ? normalizeOptionalString(inlineDescriptor) : undefined);
1653
1691
  const includeTimeline = input.includeTimeline ?? false;
1654
1692
  const includeSnapshots = input.includeSnapshots ?? false;
1655
1693
  const maxVersions = clampLimit(input.maxVersions, 120, 400);
@@ -1705,60 +1743,75 @@ export class SourceService {
1705
1743
  selectedVersions = selectedVersions.slice(selectedVersions.length - maxVersions);
1706
1744
  warnings.push(`Version scan truncated to ${maxVersions} entries. Effective fromVersion is now "${selectedVersions[0]}".`);
1707
1745
  }
1708
- const resolvedSymbolsByVersion = new Map();
1709
- const scanned = [];
1710
- for (const version of selectedVersions) {
1746
+ const referenceVersion = selectedVersions[selectedVersions.length - 1];
1747
+ await this.rejectLifecycleClassLikeInput({
1748
+ symbol: input.symbol,
1749
+ className: userClassName,
1750
+ methodName: userMethodName,
1751
+ mapping,
1752
+ version: referenceVersion,
1753
+ sourcePriority: input.sourcePriority
1754
+ });
1755
+ const scannedResults = await mapWithConcurrencyLimit(selectedVersions, TRACE_LIFECYCLE_MAX_CONCURRENCY, async (version) => {
1756
+ const versionWarnings = [];
1711
1757
  try {
1712
- let resolvedSymbols = resolvedSymbolsByVersion.get(version);
1713
- if (!resolvedSymbols) {
1714
- const [obfuscatedClassName, obfuscatedMethod] = await Promise.all([
1715
- this.resolveToObfuscatedClassName(userClassName, version, mapping, input.sourcePriority, warnings),
1716
- this.resolveToObfuscatedMemberName(userMethodName, userClassName, descriptor, "method", version, mapping, input.sourcePriority, warnings)
1717
- ]);
1718
- resolvedSymbols = {
1719
- className: obfuscatedClassName,
1720
- methodName: obfuscatedMethod.name,
1721
- methodDescriptor: obfuscatedMethod.descriptor
1722
- };
1723
- resolvedSymbolsByVersion.set(version, resolvedSymbols);
1724
- }
1725
- const resolvedJar = await this.versionService.resolveVersionJar(version);
1758
+ const [obfuscatedClassName, obfuscatedMethod, resolvedJar] = await Promise.all([
1759
+ this.resolveToObfuscatedClassName(userClassName, version, mapping, input.sourcePriority, versionWarnings),
1760
+ this.resolveToObfuscatedMemberName(userMethodName, userClassName, descriptor, "method", version, mapping, input.sourcePriority, versionWarnings),
1761
+ this.versionService.resolveVersionJar(version)
1762
+ ]);
1726
1763
  const signature = await this.explorerService.getSignature({
1727
- fqn: resolvedSymbols.className,
1764
+ fqn: obfuscatedClassName,
1728
1765
  jarPath: resolvedJar.jarPath,
1729
1766
  access: "all",
1730
1767
  includeSynthetic: true
1731
1768
  });
1732
- const sameNameMethods = signature.methods.filter((method) => method.name === resolvedSymbols.methodName);
1733
- const matchesDescriptor = (resolvedSymbols.methodDescriptor ?? descriptor)
1734
- ? sameNameMethods.some((method) => method.jvmDescriptor === (resolvedSymbols.methodDescriptor ?? descriptor))
1769
+ const sameNameMethods = signature.methods.filter((method) => method.name === obfuscatedMethod.name);
1770
+ const effectiveDescriptor = obfuscatedMethod.descriptor ?? descriptor;
1771
+ const matchesDescriptor = effectiveDescriptor
1772
+ ? sameNameMethods.some((method) => method.jvmDescriptor === effectiveDescriptor)
1735
1773
  : sameNameMethods.length > 0;
1736
1774
  const reason = !matchesDescriptor && descriptor && sameNameMethods.length > 0 ? "descriptor-mismatch" : undefined;
1737
- scanned.push({
1738
- version,
1739
- exists: matchesDescriptor,
1740
- reason,
1741
- determinate: true
1742
- });
1775
+ return {
1776
+ entry: {
1777
+ version,
1778
+ exists: matchesDescriptor,
1779
+ reason,
1780
+ determinate: true
1781
+ },
1782
+ warnings: versionWarnings
1783
+ };
1743
1784
  }
1744
1785
  catch (caughtError) {
1745
1786
  if (isAppError(caughtError) && caughtError.code === ERROR_CODES.CLASS_NOT_FOUND) {
1746
- scanned.push({
1787
+ return {
1788
+ entry: {
1789
+ version,
1790
+ exists: false,
1791
+ reason: "class-not-found",
1792
+ determinate: true
1793
+ },
1794
+ warnings: versionWarnings
1795
+ };
1796
+ }
1797
+ versionWarnings.push(`Failed to evaluate ${version}: ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`);
1798
+ return {
1799
+ entry: {
1747
1800
  version,
1748
1801
  exists: false,
1749
- reason: "class-not-found",
1750
- determinate: true
1751
- });
1752
- continue;
1753
- }
1754
- scanned.push({
1755
- version,
1756
- exists: false,
1757
- reason: "unresolved",
1758
- determinate: false
1759
- });
1760
- warnings.push(`Failed to evaluate ${version}: ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`);
1802
+ reason: "unresolved",
1803
+ determinate: false
1804
+ },
1805
+ warnings: versionWarnings
1806
+ };
1807
+ }
1808
+ finally {
1809
+ this.releaseLifecycleMappingGraph(version, input.sourcePriority);
1761
1810
  }
1811
+ });
1812
+ const scanned = scannedResults.map((result) => result.entry);
1813
+ for (const result of scannedResults) {
1814
+ warnings.push(...result.warnings);
1762
1815
  }
1763
1816
  const determinate = scanned.filter((entry) => entry.determinate);
1764
1817
  const present = determinate.filter((entry) => entry.exists);
@@ -2699,17 +2752,18 @@ export class SourceService {
2699
2752
  path: this.resolveMixinInputPath(path, "path")
2700
2753
  },
2701
2754
  sourcePath: path
2702
- })), input);
2755
+ })), input, []);
2703
2756
  }
2704
2757
  const resolvedInput = mode === "project"
2705
2758
  ? this.createProjectValidateMixinConfigInput(input)
2706
2759
  : input;
2707
- const configSources = await this.resolveMixinConfigSources(resolvedInput);
2760
+ const { sources: configSources, warnings: configWarnings } = await this.resolveMixinConfigSources(resolvedInput);
2708
2761
  if (configSources.length === 0) {
2709
- throw createError({
2710
- code: ERROR_CODES.INVALID_INPUT,
2711
- message: "Mixin config(s) contain no mixin class entries."
2712
- });
2762
+ const emptyOutput = this.buildValidateMixinOutput(mode, []);
2763
+ return this.applyValidateMixinOutputCompaction({
2764
+ ...emptyOutput,
2765
+ warnings: [...new Set([...emptyOutput.warnings, ...configWarnings])]
2766
+ }, input);
2713
2767
  }
2714
2768
  return this.validateMixinMany(mode, configSources.map((entry) => ({
2715
2769
  source: {
@@ -2719,7 +2773,7 @@ export class SourceService {
2719
2773
  configPath: entry.configPath
2720
2774
  },
2721
2775
  sourcePath: entry.sourcePath
2722
- })), resolvedInput);
2776
+ })), resolvedInput, configWarnings);
2723
2777
  }
2724
2778
  createProjectValidateMixinConfigInput(input) {
2725
2779
  if (input.input.mode !== "project") {
@@ -3249,9 +3303,13 @@ export class SourceService {
3249
3303
  }
3250
3304
  async resolveMixinConfigSources(input) {
3251
3305
  if (input.input.mode !== "config") {
3252
- return [];
3306
+ return {
3307
+ sources: [],
3308
+ warnings: []
3309
+ };
3253
3310
  }
3254
3311
  const results = [];
3312
+ const warnings = [];
3255
3313
  for (const rawConfigPath of input.input.configPaths) {
3256
3314
  const resolvedConfigPath = this.resolveMixinInputPath(rawConfigPath, "configPath");
3257
3315
  let configJson;
@@ -3272,6 +3330,7 @@ export class SourceService {
3272
3330
  ...(configJson.server ?? [])
3273
3331
  ];
3274
3332
  if (classNames.length === 0) {
3333
+ warnings.push(`Mixin config "${resolvedConfigPath}" contains no mixin class entries.`);
3275
3334
  continue;
3276
3335
  }
3277
3336
  const projectBase = input.projectPath
@@ -3306,9 +3365,12 @@ export class SourceService {
3306
3365
  });
3307
3366
  }
3308
3367
  }
3309
- return results;
3368
+ return {
3369
+ sources: results,
3370
+ warnings
3371
+ };
3310
3372
  }
3311
- async validateMixinMany(mode, entries, input) {
3373
+ async validateMixinMany(mode, entries, input, additionalWarnings) {
3312
3374
  const results = [];
3313
3375
  const batchWarningMode = input.warningMode ?? "aggregated";
3314
3376
  const { input: _discardedInput, ...sharedInput } = input;
@@ -3335,7 +3397,13 @@ export class SourceService {
3335
3397
  });
3336
3398
  }
3337
3399
  }
3338
- return this.applyValidateMixinOutputCompaction(this.buildValidateMixinOutput(mode, results), input);
3400
+ const output = this.buildValidateMixinOutput(mode, results);
3401
+ return this.applyValidateMixinOutputCompaction({
3402
+ ...output,
3403
+ warnings: additionalWarnings.length === 0
3404
+ ? output.warnings
3405
+ : [...new Set([...output.warnings, ...additionalWarnings])]
3406
+ }, input);
3339
3407
  }
3340
3408
  applyValidateMixinOutputCompaction(output, input) {
3341
3409
  let nextOutput = output;
@@ -3617,6 +3685,7 @@ export class SourceService {
3617
3685
  const candidateLimit = this.indexedCandidateLimitForMatch(match);
3618
3686
  const indexed = this.filesRepo.searchFileCandidates(artifactId, {
3619
3687
  query,
3688
+ match,
3620
3689
  limit: candidateLimit,
3621
3690
  mode: "text"
3622
3691
  });
@@ -3701,13 +3770,20 @@ export class SourceService {
3701
3770
  }
3702
3771
  }
3703
3772
  searchTextIntent(artifactId, query, match, scope, regexPattern, onHit) {
3704
- const filePaths = this.loadScopedFilePaths(artifactId, scope);
3705
3773
  const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
3706
- for (const chunk of chunkArray(filePaths, pageSize)) {
3707
- const rows = this.filesRepo.getFileContentsByPaths(artifactId, chunk);
3774
+ const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
3775
+ let cursor = undefined;
3776
+ while (true) {
3777
+ const page = this.filesRepo.listFileRows(artifactId, { limit: pageSize, cursor });
3708
3778
  this.metrics.recordSearchDbRoundtrip();
3709
- this.metrics.recordSearchRowsScanned(rows.length);
3710
- for (const row of rows) {
3779
+ this.metrics.recordSearchRowsScanned(page.items.length);
3780
+ for (const row of page.items) {
3781
+ if (!checkPackagePrefix(row.filePath, scope?.packagePrefix)) {
3782
+ continue;
3783
+ }
3784
+ if (glob && !glob.test(row.filePath)) {
3785
+ continue;
3786
+ }
3711
3787
  const contentIndex = match === "regex"
3712
3788
  ? matchRegexIndex(row.content, regexPattern)
3713
3789
  : findContentMatchIndex(row.content, query, match);
@@ -3721,29 +3797,44 @@ export class SourceService {
3721
3797
  reasonCodes: ["content_match", `text_${match}`]
3722
3798
  });
3723
3799
  }
3800
+ if (!page.nextCursor) {
3801
+ break;
3802
+ }
3803
+ cursor = page.nextCursor;
3724
3804
  }
3725
3805
  }
3726
3806
  searchPathIntent(artifactId, query, match, scope, regexPattern, onHit) {
3727
- const filePaths = this.loadScopedFilePaths(artifactId, scope);
3728
- const matching = filePaths.flatMap((filePath) => {
3729
- const pathIndex = match === "regex"
3730
- ? matchRegexIndex(filePath, regexPattern)
3731
- : findMatchIndex(filePath, query, match);
3732
- if (pathIndex < 0) {
3733
- return [];
3734
- }
3735
- return [{ filePath, pathIndex }];
3736
- });
3737
3807
  const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
3738
- for (const chunk of chunkArray(matching, pageSize)) {
3739
- for (const candidate of chunk) {
3808
+ const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
3809
+ let cursor = undefined;
3810
+ while (true) {
3811
+ const page = this.filesRepo.listFiles(artifactId, { limit: pageSize, cursor });
3812
+ this.metrics.recordSearchDbRoundtrip();
3813
+ this.metrics.recordSearchRowsScanned(page.items.length);
3814
+ for (const filePath of page.items) {
3815
+ if (!checkPackagePrefix(filePath, scope?.packagePrefix)) {
3816
+ continue;
3817
+ }
3818
+ if (glob && !glob.test(filePath)) {
3819
+ continue;
3820
+ }
3821
+ const pathIndex = match === "regex"
3822
+ ? matchRegexIndex(filePath, regexPattern)
3823
+ : findMatchIndex(filePath, query, match);
3824
+ if (pathIndex < 0) {
3825
+ continue;
3826
+ }
3740
3827
  onHit({
3741
- filePath: candidate.filePath,
3742
- score: scorePathMatch(match, candidate.pathIndex),
3828
+ filePath,
3829
+ score: scorePathMatch(match, pathIndex),
3743
3830
  matchedIn: "path",
3744
3831
  reasonCodes: ["path_match", `path_${match}`]
3745
3832
  });
3746
3833
  }
3834
+ if (!page.nextCursor) {
3835
+ break;
3836
+ }
3837
+ cursor = page.nextCursor;
3747
3838
  }
3748
3839
  }
3749
3840
  findSymbolHits(artifactId, query, match, scope, regexPattern) {
@@ -3806,31 +3897,6 @@ export class SourceService {
3806
3897
  }
3807
3898
  return result;
3808
3899
  }
3809
- loadScopedFilePaths(artifactId, scope) {
3810
- const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
3811
- const result = [];
3812
- let cursor = undefined;
3813
- const pageSize = Math.max(1, this.config.searchScanPageSize ?? 250);
3814
- while (true) {
3815
- const page = this.filesRepo.listFiles(artifactId, { limit: pageSize, cursor });
3816
- this.metrics.recordSearchDbRoundtrip();
3817
- this.metrics.recordSearchRowsScanned(page.items.length);
3818
- for (const filePath of page.items) {
3819
- if (!checkPackagePrefix(filePath, scope?.packagePrefix)) {
3820
- continue;
3821
- }
3822
- if (glob && !glob.test(filePath)) {
3823
- continue;
3824
- }
3825
- result.push(filePath);
3826
- }
3827
- if (!page.nextCursor) {
3828
- break;
3829
- }
3830
- cursor = page.nextCursor;
3831
- }
3832
- return result;
3833
- }
3834
3900
  indexedCandidateLimit() {
3835
3901
  return Math.min(Math.max(this.config.maxSearchHits * 5, 500), 5000);
3836
3902
  }
@@ -4051,6 +4117,38 @@ export class SourceService {
4051
4117
  details
4052
4118
  });
4053
4119
  }
4120
+ rejectLifecycleClassLikeInput(input) {
4121
+ if (!looksLikeClassSegment(input.methodName)) {
4122
+ return;
4123
+ }
4124
+ const classLikeSymbol = `${input.className}.${input.methodName}`;
4125
+ throw createError({
4126
+ code: ERROR_CODES.INVALID_INPUT,
4127
+ message: `symbol must be in the form "fully.qualified.Class.method".`,
4128
+ details: {
4129
+ symbol: input.symbol,
4130
+ classLikeSymbol,
4131
+ nextAction: "Pass lifecycle input as Class.method and use the separate descriptor field for exact overload matching.",
4132
+ suggestedCall: input.version
4133
+ ? {
4134
+ tool: "check-symbol-exists",
4135
+ params: {
4136
+ version: input.version,
4137
+ kind: "class",
4138
+ name: classLikeSymbol,
4139
+ sourceMapping: input.mapping
4140
+ }
4141
+ }
4142
+ : undefined
4143
+ }
4144
+ });
4145
+ }
4146
+ releaseLifecycleMappingGraph(version, sourcePriority) {
4147
+ if ("releaseGraphCacheEntry" in this.mappingService &&
4148
+ typeof this.mappingService.releaseGraphCacheEntry === "function") {
4149
+ this.mappingService.releaseGraphCacheEntry(version, sourcePriority);
4150
+ }
4151
+ }
4054
4152
  async resolveToObfuscatedClassName(className, version, mapping, sourcePriority, warnings) {
4055
4153
  return this.resolveClassNameForLookup({
4056
4154
  className,
@@ -4543,11 +4641,7 @@ export class SourceService {
4543
4641
  publishCacheMetrics() {
4544
4642
  this.metrics.setCacheEntries(this.cacheMetricsState.entries);
4545
4643
  this.metrics.setCacheTotalContentBytes(this.cacheMetricsState.totalContentBytes);
4546
- this.metrics.setCacheArtifactByteAccounting(this.cacheMetricsState.lru.map((row) => ({
4547
- artifact_id: row.artifactId,
4548
- content_bytes: row.totalContentBytes,
4549
- updated_at: row.updatedAt
4550
- })));
4644
+ this.metrics.setCacheArtifactByteAccountingRef(this.cacheMetricsState.lru);
4551
4645
  }
4552
4646
  }
4553
4647
  //# sourceMappingURL=source-service.js.map
@@ -15,6 +15,7 @@ export interface ListFilesOptions {
15
15
  export interface SearchFilesOptions {
16
16
  limit: number;
17
17
  query: string;
18
+ match?: "exact" | "prefix" | "contains" | "regex";
18
19
  cursor?: string;
19
20
  mode?: "mixed" | "text" | "path";
20
21
  fetchLimitOverride?: number;
@@ -102,6 +102,25 @@ function buildPreview(content, query) {
102
102
  const suffix = end < content.length ? "..." : "";
103
103
  return `${prefix}${content.slice(start, end)}${suffix}`.replace(/\s+/g, " ").trim();
104
104
  }
105
+ function tokenizeIndexedQuery(query) {
106
+ return query.match(/[\p{L}\p{N}]+/gu) ?? [];
107
+ }
108
+ function buildIndexedMatchQuery(query, match) {
109
+ const tokens = tokenizeIndexedQuery(query.trim());
110
+ if (tokens.length === 0) {
111
+ return query.trim();
112
+ }
113
+ // Separator-heavy queries become an order-agnostic FTS5 MATCH expression.
114
+ // Whitespace is implicit AND, which keeps "dispatcher.register" on the indexed
115
+ // path while SourceService re-checks hydrated path/content matches before
116
+ // returning hits. Callers can still use literal mode for exact substring scans.
117
+ return tokens.map((token, index) => {
118
+ if (match === "prefix" && index === tokens.length - 1) {
119
+ return `${token}*`;
120
+ }
121
+ return token;
122
+ }).join(" ");
123
+ }
105
124
  export class FilesRepo {
106
125
  db;
107
126
  deleteStmt;
@@ -257,6 +276,7 @@ export class FilesRepo {
257
276
  }
258
277
  const cursor = parseSearchCursor(options.cursor);
259
278
  const likeQuery = `%${normalized}%`;
279
+ const ftsQuery = buildIndexedMatchQuery(normalized, options.match);
260
280
  const mode = options.mode ?? "mixed";
261
281
  // Cursor-adaptive fetch limit: when no cursor, use a generous limit;
262
282
  // with cursor + SQL pushdown, we need far fewer rows.
@@ -294,9 +314,9 @@ export class FilesRepo {
294
314
  }));
295
315
  const mergedByPath = new Map(merged.map((hit) => [hit.filePath, hit]));
296
316
  let contentRows = [];
297
- if (includeContent) {
317
+ if (includeContent && ftsQuery) {
298
318
  try {
299
- contentRows = this.searchFtsStmt.all(artifactId, normalized, fetchLimit);
319
+ contentRows = this.searchFtsStmt.all(artifactId, ftsQuery, fetchLimit);
300
320
  }
301
321
  catch (error) {
302
322
  const message = error instanceof Error ? error.message : String(error);
@@ -305,7 +325,7 @@ export class FilesRepo {
305
325
  }
306
326
  log("warn", "storage.files.fts_syntax_error", {
307
327
  artifactId,
308
- query: normalized,
328
+ query: ftsQuery,
309
329
  message
310
330
  });
311
331
  }
@@ -379,14 +399,18 @@ export class FilesRepo {
379
399
  if (!normalized) {
380
400
  return 0;
381
401
  }
402
+ const ftsQuery = buildIndexedMatchQuery(normalized, "contains");
403
+ if (!ftsQuery) {
404
+ return 0;
405
+ }
382
406
  try {
383
- const row = this.db.prepare(`SELECT COUNT(*) AS cnt FROM files_fts WHERE artifact_id = ? AND files_fts MATCH ?`).get(artifactId, normalized);
407
+ const row = this.db.prepare(`SELECT COUNT(*) AS cnt FROM files_fts WHERE artifact_id = ? AND files_fts MATCH ?`).get(artifactId, ftsQuery);
384
408
  return row?.cnt ?? 0;
385
409
  }
386
410
  catch {
387
411
  log("warn", "storage.files.count_text_candidates_failed", {
388
412
  artifactId,
389
- query: normalized
413
+ query: ftsQuery
390
414
  });
391
415
  return 0;
392
416
  }
@@ -0,0 +1,4 @@
1
+ export declare const TOOL_SURFACE_SECTION_IDS: readonly ["v3-entry-tools", "source-exploration", "version-comparison-symbol-tracking", "mapping-symbols", "nbt-utilities", "mod-analysis", "validation", "registry-diagnostics"];
2
+ export type ToolSurfaceSectionId = (typeof TOOL_SURFACE_SECTION_IDS)[number];
3
+ export type ToolSurfaceLocale = "en" | "ja";
4
+ export declare function renderToolSurfaceSection(locale: ToolSurfaceLocale, sectionId: ToolSurfaceSectionId): string;