@adhisang/minecraft-modding-mcp 7.0.0 → 7.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +4 -3
  3. package/dist/access-transformer-parser.d.ts +8 -0
  4. package/dist/access-transformer-parser.js +8 -1
  5. package/dist/access-widener-parser.d.ts +17 -0
  6. package/dist/access-widener-parser.js +12 -1
  7. package/dist/cache-registry.js +19 -6
  8. package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
  9. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
  10. package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
  11. package/dist/entry-tools/batch-class-members-service.js +20 -6
  12. package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
  13. package/dist/entry-tools/batch-class-source-service.js +10 -0
  14. package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
  15. package/dist/entry-tools/compare-minecraft-service.js +65 -4
  16. package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
  17. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +15 -9
  18. package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
  19. package/dist/entry-tools/manage-cache-service.d.ts +2 -2
  20. package/dist/entry-tools/manage-cache-service.js +10 -14
  21. package/dist/entry-tools/validate-project/cases/access-transformer.js +22 -3
  22. package/dist/entry-tools/validate-project/cases/access-widener.js +31 -4
  23. package/dist/entry-tools/validate-project/cases/mixin.js +11 -3
  24. package/dist/entry-tools/validate-project/cases/project-summary.js +94 -16
  25. package/dist/entry-tools/validate-project-service.d.ts +2 -2
  26. package/dist/entry-tools/verify-mixin-target-service.js +19 -8
  27. package/dist/index.js +38 -15
  28. package/dist/java-process.d.ts +1 -0
  29. package/dist/java-process.js +14 -0
  30. package/dist/mapping/lookup.js +16 -1
  31. package/dist/mapping-service.d.ts +14 -0
  32. package/dist/mapping-service.js +35 -15
  33. package/dist/minecraft-explorer-service.js +70 -8
  34. package/dist/mixin/access-validators.js +38 -2
  35. package/dist/mixin/annotation-validators.js +137 -43
  36. package/dist/mixin/parsed-validator.js +21 -7
  37. package/dist/mixin-parser.d.ts +52 -0
  38. package/dist/mixin-parser.js +709 -130
  39. package/dist/mod-decompile-service.js +11 -1
  40. package/dist/mod-remap-service.js +6 -6
  41. package/dist/nbt/java-nbt-codec.js +7 -1
  42. package/dist/source/access-validate.js +10 -0
  43. package/dist/source/artifact-resolver.d.ts +27 -3
  44. package/dist/source/artifact-resolver.js +235 -30
  45. package/dist/source/class-source/members-builder.d.ts +7 -0
  46. package/dist/source/class-source/members-builder.js +4 -1
  47. package/dist/source/class-source.d.ts +9 -2
  48. package/dist/source/class-source.js +186 -27
  49. package/dist/source/indexer.js +69 -1
  50. package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
  51. package/dist/source/lifecycle/mapping-helpers.js +29 -3
  52. package/dist/source/lifecycle/runtime-check.d.ts +25 -0
  53. package/dist/source/lifecycle/runtime-check.js +68 -39
  54. package/dist/source/nested-jars.d.ts +15 -1
  55. package/dist/source/nested-jars.js +14 -5
  56. package/dist/source/search.d.ts +10 -2
  57. package/dist/source/search.js +60 -13
  58. package/dist/source/symbol-resolver.js +88 -0
  59. package/dist/source/validate-mixin/pipeline/mapping-health.js +20 -1
  60. package/dist/source/validate-mixin/pipeline/target-lookup.js +16 -7
  61. package/dist/source/validate-mixin.d.ts +5 -0
  62. package/dist/source/validate-mixin.js +136 -21
  63. package/dist/source/workspace-target.js +75 -7
  64. package/dist/source-jar-reader.d.ts +48 -1
  65. package/dist/source-jar-reader.js +93 -3
  66. package/dist/source-resolver.d.ts +7 -0
  67. package/dist/source-resolver.js +22 -14
  68. package/dist/source-service.d.ts +5 -0
  69. package/dist/source-service.js +7 -0
  70. package/dist/stdio-supervisor.d.ts +35 -1
  71. package/dist/stdio-supervisor.js +77 -2
  72. package/dist/storage/db.d.ts +62 -2
  73. package/dist/storage/db.js +181 -20
  74. package/dist/storage/files-repo.d.ts +7 -0
  75. package/dist/storage/files-repo.js +17 -4
  76. package/dist/storage/sqlite.d.ts +31 -1
  77. package/dist/storage/sqlite.js +125 -16
  78. package/dist/tool-contract-manifest.js +2 -2
  79. package/dist/tool-execution-gate.js +2 -1
  80. package/dist/tool-guidance.js +4 -1
  81. package/dist/tool-schemas.d.ts +64 -52
  82. package/dist/tool-schemas.js +9 -7
  83. package/dist/types.d.ts +9 -0
  84. package/dist/v1-parity-schemas.js +36 -2
  85. package/dist/version-diff-service.d.ts +23 -0
  86. package/dist/version-diff-service.js +101 -0
  87. package/dist/version-service.d.ts +14 -0
  88. package/dist/version-service.js +52 -3
  89. package/dist/workspace-context-cache.d.ts +25 -0
  90. package/dist/workspace-context-cache.js +52 -2
  91. package/dist/workspace-mapping-service.d.ts +8 -0
  92. package/dist/workspace-mapping-service.js +151 -21
  93. package/docs/README-ja.md +3 -1
  94. package/docs/tool-reference.md +69 -22
  95. package/package.json +1 -1
@@ -1,5 +1,18 @@
1
1
  import { ERROR_CODES, isAppError } from "../../errors.js";
2
+ /**
3
+ * check-symbol-exists's runtime fallback: the result of `runUnobfuscatedRuntimeCheck`
4
+ * alone. Its callers keep the fallback base's status whenever the check could not be
5
+ * performed, so they need no `verified` flag.
6
+ */
2
7
  export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbackBase) {
8
+ return (await runUnobfuscatedRuntimeCheck(svc, input, fallbackBase))?.result;
9
+ }
10
+ /**
11
+ * Checks a symbol against the Minecraft runtime jar of an unobfuscated version, whose
12
+ * names are the Mojang names. Undefined when the version or name is empty or the
13
+ * runtime jar cannot be resolved.
14
+ */
15
+ export async function runUnobfuscatedRuntimeCheck(svc, input, fallbackBase) {
3
16
  const version = input.version.trim();
4
17
  const name = input.name.trim();
5
18
  const owner = input.owner?.trim();
@@ -8,11 +21,14 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
8
21
  }
9
22
  if (input.kind === "class" && input.nameMode !== "fqcn" && !name.includes(".")) {
10
23
  return {
11
- ...fallbackBase,
12
- warnings: [
13
- ...fallbackBase.warnings,
14
- `Version ${version} is unobfuscated, but short class name "${name}" could not be checked against runtime bytecode without a fully-qualified name.`
15
- ]
24
+ verified: false,
25
+ result: {
26
+ ...fallbackBase,
27
+ warnings: [
28
+ ...fallbackBase.warnings,
29
+ `Version ${version} is unobfuscated, but short class name "${name}" could not be checked against runtime bytecode without a fully-qualified name.`
30
+ ]
31
+ }
16
32
  };
17
33
  }
18
34
  const querySymbol = input.kind === "class"
@@ -37,7 +53,7 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
37
53
  };
38
54
  const targetClass = input.kind === "class" ? name : owner;
39
55
  if (!targetClass) {
40
- return fallbackBase;
56
+ return { verified: false, result: fallbackBase };
41
57
  }
42
58
  let jarPath;
43
59
  try {
@@ -61,16 +77,20 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
61
77
  });
62
78
  }
63
79
  catch (error) {
80
+ // Only CLASS_NOT_FOUND is an answer; any other failure means the lookup never completed.
64
81
  const classMissing = isAppError(error) && error.code === ERROR_CODES.CLASS_NOT_FOUND;
65
82
  return {
66
- ...fallbackBase,
67
- querySymbol,
68
- warnings: [
69
- ...fallbackBase.warnings,
70
- classMissing
71
- ? `Class "${targetClass}" was not found in the Minecraft ${version} runtime jar; it does not exist (or is not in this jar).`
72
- : `Version ${version} is unobfuscated; runtime bytecode lookup could not load class "${targetClass}".`
73
- ]
83
+ verified: classMissing,
84
+ result: {
85
+ ...fallbackBase,
86
+ querySymbol,
87
+ warnings: [
88
+ ...fallbackBase.warnings,
89
+ classMissing
90
+ ? `Class "${targetClass}" was not found in the Minecraft ${version} runtime jar; it does not exist (or is not in this jar).`
91
+ : `Version ${version} is unobfuscated; runtime bytecode lookup could not load class "${targetClass}".`
92
+ ]
93
+ }
74
94
  };
75
95
  }
76
96
  const warnings = [...fallbackBase.warnings, ...signature.warnings];
@@ -80,33 +100,40 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
80
100
  ...fallbackBase.mappingContext,
81
101
  runtimeValidated: true
82
102
  };
103
+ // From here on the class loaded, so every answer below is a completed lookup.
83
104
  const buildResolved = (resolvedSymbol) => ({
84
- ...fallbackBase,
85
- mappingContext: runtimeValidatedContext,
86
- querySymbol,
87
- resolved: true,
88
- status: "resolved",
89
- resolvedSymbol,
90
- candidates: resolvedSymbol
91
- ? [{
92
- ...resolvedSymbol,
93
- matchKind: "exact",
94
- confidence: 1
95
- }]
96
- : [],
97
- candidateCount: resolvedSymbol ? 1 : 0,
98
- warnings
105
+ verified: true,
106
+ result: {
107
+ ...fallbackBase,
108
+ mappingContext: runtimeValidatedContext,
109
+ querySymbol,
110
+ resolved: true,
111
+ status: "resolved",
112
+ resolvedSymbol,
113
+ candidates: resolvedSymbol
114
+ ? [{
115
+ ...resolvedSymbol,
116
+ matchKind: "exact",
117
+ confidence: 1
118
+ }]
119
+ : [],
120
+ candidateCount: resolvedSymbol ? 1 : 0,
121
+ warnings
122
+ }
99
123
  });
100
124
  const buildUnresolved = (status) => ({
101
- ...fallbackBase,
102
- mappingContext: runtimeValidatedContext,
103
- querySymbol,
104
- resolved: false,
105
- status,
106
- resolvedSymbol: undefined,
107
- candidates: [],
108
- candidateCount: 0,
109
- warnings
125
+ verified: true,
126
+ result: {
127
+ ...fallbackBase,
128
+ mappingContext: runtimeValidatedContext,
129
+ querySymbol,
130
+ resolved: false,
131
+ status,
132
+ resolvedSymbol: undefined,
133
+ candidates: [],
134
+ candidateCount: 0,
135
+ warnings
136
+ }
110
137
  });
111
138
  if (input.kind === "class") {
112
139
  return buildResolved({
@@ -127,7 +154,9 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
127
154
  symbol: `${owner}.${name}`
128
155
  });
129
156
  }
130
- const methodCandidates = signature.methods.filter((method) => method.name === name);
157
+ // The bytecode reader lists constructors apart from methods (and only the owner's
158
+ // own: constructors are not inherited), so "<init>" is answered from them.
159
+ const methodCandidates = (name === "<init>" ? signature.constructors : signature.methods).filter((method) => method.name === name);
131
160
  const signatureMode = input.signatureMode ?? "name-only";
132
161
  if (signatureMode === "name-only") {
133
162
  // Existence semantics: any overload with this name means the method exists. Multiple
@@ -1,3 +1,4 @@
1
+ import { rename } from "node:fs/promises";
1
2
  /**
2
3
  * A jar with at most this many own `.class` entries can qualify as a
3
4
  * Jar-in-Jar shell. Real Fabric API umbrella jars carry zero to a handful of
@@ -23,6 +24,10 @@ export interface NestedClassMatch {
23
24
  filePath: string;
24
25
  line: number;
25
26
  symbolKind: "class";
27
+ /** Set when the match is a type declared INSIDE another type. */
28
+ nested?: boolean;
29
+ /** The enclosing top-level type, present only on a nested match. */
30
+ enclosingClass?: string;
26
31
  }
27
32
  /**
28
33
  * Finds exact class-name matches across a shell's nested bytecode inventory.
@@ -50,6 +55,15 @@ export declare function detectShellJarInventory(jarPath: string): Promise<string
50
55
  * shell always maps to the same extracted file and re-resolution reuses it.
51
56
  */
52
57
  export declare function nestedJarCachePath(cacheDir: string, outerJarPath: string, outerSignature: string, entryName: string): string;
58
+ /**
59
+ * Seam for injecting the temp->final rename primitive so tests can simulate a
60
+ * non-race rename failure (e.g. EBUSY/EPERM/ENOSPC), which cannot be provoked
61
+ * for real against a plain filesystem under the allowed test commands.
62
+ * Defaults to the module-level `rename` from node:fs/promises.
63
+ */
64
+ export interface ExtractNestedJarDeps {
65
+ rename?: typeof rename;
66
+ }
53
67
  /**
54
68
  * Extracts one nested jar to the content-addressed cache (no-op when already
55
69
  * present). Entry-name safety is enforced by readJarEntryAsBuffer, and the
@@ -62,7 +76,7 @@ export declare function nestedJarCachePath(cacheDir: string, outerJarPath: strin
62
76
  * one extraction with ERR_LIMIT_EXCEEDED — `loadNestedJarClassSet` swallows it
63
77
  * and keeps resolving the shell's other nested jars.
64
78
  */
65
- export declare function extractNestedJar(cacheDir: string, outerJarPath: string, outerSignature: string, entryName: string, maxEntryBytes?: number): Promise<string>;
79
+ export declare function extractNestedJar(cacheDir: string, outerJarPath: string, outerSignature: string, entryName: string, maxEntryBytes?: number, deps?: ExtractNestedJarDeps): Promise<string>;
66
80
  /**
67
81
  * Finds every nested jar of a shell that contains the class. Zero matches
68
82
  * means the class genuinely is not bundled; more than one means the caller
@@ -98,11 +98,17 @@ function nestedClassMatch(entry) {
98
98
  if (innerSegments.some((segment) => segment.length === 0 || /^\d/.test(segment))) {
99
99
  return undefined;
100
100
  }
101
+ // Mirror the indexed find-class branch's shape (class-source.ts): a `$`
102
+ // inner-class match reports `nested`/`enclosingClass` against its top-level
103
+ // enclosing type, exactly like an indexed lookup would.
104
+ const outerBinaryName = internalName.split("$")[0];
105
+ const isNested = innerSegments.length > 0;
101
106
  return {
102
107
  qualifiedName: internalName.replaceAll("/", ".").replaceAll("$", "."),
103
- filePath: `${internalName.split("$")[0]}.java`,
108
+ filePath: `${outerBinaryName}.java`,
104
109
  line: 1,
105
- symbolKind: "class"
110
+ symbolKind: "class",
111
+ ...(isNested ? { nested: true, enclosingClass: outerBinaryName.replaceAll("/", ".") } : {})
106
112
  };
107
113
  }
108
114
  /**
@@ -206,7 +212,8 @@ export function nestedJarCachePath(cacheDir, outerJarPath, outerSignature, entry
206
212
  * one extraction with ERR_LIMIT_EXCEEDED — `loadNestedJarClassSet` swallows it
207
213
  * and keeps resolving the shell's other nested jars.
208
214
  */
209
- export async function extractNestedJar(cacheDir, outerJarPath, outerSignature, entryName, maxEntryBytes) {
215
+ export async function extractNestedJar(cacheDir, outerJarPath, outerSignature, entryName, maxEntryBytes, deps = {}) {
216
+ const renameFn = deps.rename ?? rename;
210
217
  const finalPath = nestedJarCachePath(cacheDir, outerJarPath, outerSignature, entryName);
211
218
  try {
212
219
  await access(finalPath);
@@ -243,16 +250,18 @@ export async function extractNestedJar(cacheDir, outerJarPath, outerSignature, e
243
250
  const tempPath = `${finalPath}.tmp.${process.pid}.${Date.now()}.${randomBytes(6).toString("hex")}`;
244
251
  await writeFile(tempPath, bytes);
245
252
  try {
246
- await rename(tempPath, finalPath);
253
+ await renameFn(tempPath, finalPath);
247
254
  }
248
255
  catch (renameError) {
249
256
  // A concurrent extraction of the same entry may have won the rename.
250
257
  // The content-addressed final file being present makes this call a
251
- // success; anything else is a real failure.
258
+ // success; anything else is a real failure. Either way, best-effort
259
+ // remove the orphaned temp file before returning/rethrowing.
252
260
  try {
253
261
  await access(finalPath);
254
262
  }
255
263
  catch {
264
+ await rm(tempPath, { force: true }).catch(() => { });
256
265
  throw renameError;
257
266
  }
258
267
  await rm(tempPath, { force: true });
@@ -22,6 +22,14 @@ export declare function canUseIndexedSearchPath(indexedSearchEnabled: boolean, i
22
22
  export declare function buildGlobRegex(pattern: string): RegExp;
23
23
  export declare function globToSqlLike(pattern: string): string;
24
24
  export declare function checkPackagePrefix(filePath: string, packagePrefix?: string): boolean;
25
+ /**
26
+ * A literal file_path prefix every path accepted by the scope starts with, or
27
+ * undefined when the scope implies none. The indexed candidate query matches it
28
+ * with an ASCII case-insensitive LIKE, a superset of checkPackagePrefix and the
29
+ * glob regex (indexed paths are stored with `/` separators), so both exact
30
+ * checks still run on every candidate.
31
+ */
32
+ export declare function indexedScopePathPrefix(scope: SearchScope | undefined): string | undefined;
25
33
  export declare function buildSearchCursorContext(input: {
26
34
  artifactId: string;
27
35
  query: string;
@@ -42,8 +50,8 @@ export declare function scorePathMatch(match: SearchMatch, index: number): numbe
42
50
  export declare function matchRegexIndex(target: string, regex: RegExp): number;
43
51
  export declare function searchClassSource(svc: SourceService, input: SearchClassSourceInput): Promise<SearchClassSourceOutput>;
44
52
  export declare function searchSymbolIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void): void;
45
- export declare function searchTextIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void): void;
46
- export declare function searchPathIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void): void;
53
+ export declare function searchTextIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void): boolean;
54
+ export declare function searchPathIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void): boolean;
47
55
  export declare function searchTextIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void, onWarning?: (warning: string) => void): void;
48
56
  export declare function searchPathIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void): void;
49
57
  export declare function findSymbolHits(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined): IndexedSymbolHit[];
@@ -49,7 +49,9 @@ export function canUseIndexedSearchPath(indexedSearchEnabled, intent, match, _sc
49
49
  if (match === "regex") {
50
50
  return false;
51
51
  }
52
- // packagePrefix and fileGlob are applied as post-filters on indexed candidates.
52
+ // packagePrefix and a fileGlob's literal directory prefix narrow the indexed
53
+ // candidate query (see indexedScopePathPrefix); both scope filters are also
54
+ // checked exactly on every candidate.
53
55
  return true;
54
56
  }
55
57
  export function buildGlobRegex(pattern) {
@@ -108,12 +110,36 @@ export function globToSqlLike(pattern) {
108
110
  }
109
111
  return result;
110
112
  }
113
+ function packagePrefixPath(packagePrefix) {
114
+ return `${packagePrefix.replace(/\.+/g, "/").replace(/\/+$/, "")}/`;
115
+ }
111
116
  export function checkPackagePrefix(filePath, packagePrefix) {
112
117
  if (!packagePrefix) {
113
118
  return true;
114
119
  }
115
- const normalizedPrefix = packagePrefix.replace(/\.+/g, "/").replace(/\/+$/, "");
116
- return normalizePathStyle(filePath).startsWith(`${normalizedPrefix}/`);
120
+ return normalizePathStyle(filePath).startsWith(packagePrefixPath(packagePrefix));
121
+ }
122
+ /**
123
+ * A literal file_path prefix every path accepted by the scope starts with, or
124
+ * undefined when the scope implies none. The indexed candidate query matches it
125
+ * with an ASCII case-insensitive LIKE, a superset of checkPackagePrefix and the
126
+ * glob regex (indexed paths are stored with `/` separators), so both exact
127
+ * checks still run on every candidate.
128
+ */
129
+ export function indexedScopePathPrefix(scope) {
130
+ const packagePath = scope?.packagePrefix ? packagePrefixPath(scope.packagePrefix) : undefined;
131
+ let globPath;
132
+ if (scope?.fileGlob) {
133
+ // buildGlobRegex treats everything before the first `*`/`?` as literal.
134
+ const glob = normalizePathStyle(scope.fileGlob);
135
+ const wildcardIndex = glob.search(/[*?]/);
136
+ globPath = (wildcardIndex < 0 ? glob : glob.slice(0, wildcardIndex)) || undefined;
137
+ }
138
+ if (packagePath && globPath) {
139
+ // Keep the longer when one extends the other; otherwise either is a superset.
140
+ return globPath.startsWith(packagePath) ? globPath : packagePath;
141
+ }
142
+ return packagePath ?? globPath;
117
143
  }
118
144
  export function buildSearchCursorContext(input) {
119
145
  return JSON.stringify({
@@ -363,6 +389,7 @@ export async function searchClassSource(svc, input) {
363
389
  searchWarnings.push(warning);
364
390
  };
365
391
  const tokenOnlyTextIntent = intent === "text" && queryMode === "token";
392
+ let indexedWindowFull = false;
366
393
  if (intent === "symbol") {
367
394
  searchSymbolIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
368
395
  }
@@ -384,12 +411,9 @@ export async function searchClassSource(svc, input) {
384
411
  }
385
412
  else if (canUseIndexedSearchPath(indexedSearchEnabled, intent, match, scope)) {
386
413
  try {
387
- if (intent === "path") {
388
- searchPathIntentIndexed(svc, artifact.artifactId, query, match, scope, recordHit);
389
- }
390
- else {
391
- searchTextIntentIndexed(svc, artifact.artifactId, query, match, scope, recordHit);
392
- }
414
+ indexedWindowFull = intent === "path"
415
+ ? searchPathIntentIndexed(svc, artifact.artifactId, query, match, scope, recordHit)
416
+ : searchTextIntentIndexed(svc, artifact.artifactId, query, match, scope, recordHit);
393
417
  svc.metrics.recordSearchIndexedHit();
394
418
  }
395
419
  catch (caughtError) {
@@ -424,6 +448,9 @@ export async function searchClassSource(svc, input) {
424
448
  svc.metrics.recordSearchIntentDuration(intent, Date.now() - intentStartedAt);
425
449
  const finalizedHits = accumulator.finalize();
426
450
  const page = finalizedHits.page;
451
+ if (indexedWindowFull && page.length < limit) {
452
+ searchWarnings.push(indexedCandidateWindowWarning(intent, indexedCandidateLimitForMatch(svc, match)));
453
+ }
427
454
  svc.metrics.recordSearchRowsReturned(page.length);
428
455
  const nextCursor = finalizedHits.nextCursorHit
429
456
  ? encodeSearchCursor(finalizedHits.nextCursorHit, cursorContext)
@@ -468,17 +495,19 @@ export function searchSymbolIntent(svc, artifactId, query, match, scope, regexPa
468
495
  }
469
496
  export function searchTextIntentIndexed(svc, artifactId, query, match, scope, onHit) {
470
497
  const candidateLimit = indexedCandidateLimitForMatch(svc, match);
498
+ const pathPrefix = indexedScopePathPrefix(scope);
471
499
  const indexed = svc.filesRepo.searchFileCandidates(artifactId, {
472
500
  query,
473
501
  match,
474
502
  limit: candidateLimit,
475
- mode: "text"
503
+ mode: "text",
504
+ ...(pathPrefix ? { pathPrefix } : {})
476
505
  });
477
506
  svc.metrics.recordSearchDbRoundtrip(indexed.dbRoundtrips);
478
507
  svc.metrics.recordSearchRowsScanned(indexed.scannedRows);
479
508
  if (indexed.items.length === 0) {
480
509
  svc.metrics.recordSearchIndexedZeroShortcircuit();
481
- return;
510
+ return false;
482
511
  }
483
512
  const globFilter = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
484
513
  const candidatePaths = indexed.items
@@ -508,19 +537,22 @@ export function searchTextIntentIndexed(svc, artifactId, query, match, scope, on
508
537
  reasonCodes: ["content_match", `text_${match}`, "indexed"]
509
538
  });
510
539
  }
540
+ return isCandidateWindowFull(indexed);
511
541
  }
512
542
  export function searchPathIntentIndexed(svc, artifactId, query, match, scope, onHit) {
513
543
  const candidateLimit = indexedCandidateLimitForMatch(svc, match);
544
+ const pathPrefix = indexedScopePathPrefix(scope);
514
545
  const indexed = svc.filesRepo.searchFileCandidates(artifactId, {
515
546
  query,
516
547
  limit: candidateLimit,
517
- mode: "path"
548
+ mode: "path",
549
+ ...(pathPrefix ? { pathPrefix } : {})
518
550
  });
519
551
  svc.metrics.recordSearchDbRoundtrip(indexed.dbRoundtrips);
520
552
  svc.metrics.recordSearchRowsScanned(indexed.scannedRows);
521
553
  if (indexed.items.length === 0) {
522
554
  svc.metrics.recordSearchIndexedZeroShortcircuit();
523
- return;
555
+ return false;
524
556
  }
525
557
  const globFilter = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
526
558
  const candidateRows = [];
@@ -551,6 +583,21 @@ export function searchPathIntentIndexed(svc, artifactId, query, match, scope, on
551
583
  reasonCodes: ["path_match", `path_${match}`, "indexed"]
552
584
  });
553
585
  }
586
+ return isCandidateWindowFull(indexed);
587
+ }
588
+ /**
589
+ * True when the index held more candidates than the window it returned: the
590
+ * repository reports that with a continuation cursor, which the single-window
591
+ * indexed search never follows.
592
+ */
593
+ function isCandidateWindowFull(indexed) {
594
+ return indexed.nextCursor !== undefined;
595
+ }
596
+ function indexedCandidateWindowWarning(intent, candidateLimit) {
597
+ const fullScan = intent === "text"
598
+ ? 'queryMode="literal" for a full substring scan'
599
+ : 'match="regex" for a full path scan';
600
+ return `indexed ${intent} search examined only its candidate window (the first ${candidateLimit} index candidates) and more matched the index, so results may be incomplete. Use ${fullScan}, or narrow the scope with packagePrefix or a fileGlob that starts with a literal directory.`;
554
601
  }
555
602
  const ASCII_RE = /^[\x00-\x7F]*$/;
556
603
  /** True when every code unit is ASCII, so SQLite LIKE folds case identically to JS. */
@@ -1,4 +1,83 @@
1
1
  import { ERROR_CODES, createError } from "../errors.js";
2
+ import { isUnobfuscatedVersion } from "../version-service.js";
3
+ import { runUnobfuscatedRuntimeCheck } from "./lifecycle/runtime-check.js";
4
+ /**
5
+ * Minecraft 26.1+ ships its runtime in Mojang names, so a Loom project for it has no
6
+ * `mappings` declaration: it compiles against the runtime names directly. Resolving a
7
+ * workspace symbol there is an identity lookup, checked against runtime bytecode the
8
+ * same way checkSymbolExists validates unobfuscated versions.
9
+ */
10
+ async function resolveUnobfuscatedWorkspaceSymbol(svc, input, context) {
11
+ const { version, querySymbol, sourcePriorityApplied } = context;
12
+ const workspaceDetection = {
13
+ resolved: true,
14
+ mappingApplied: "mojang",
15
+ evidence: [],
16
+ warnings: [
17
+ `Minecraft ${version} is unobfuscated; no mappings declaration is needed — symbols are resolved against runtime (Mojang) names.`
18
+ ]
19
+ };
20
+ // The requested/defaulted sourceMapping label is echoed unchanged: on 26.1+ an
21
+ // "obfuscated" label already means the as-shipped Mojang names, and rewriting it to
22
+ // "mojang" would break callers that compare namespace labels for equality.
23
+ const base = {
24
+ querySymbol,
25
+ mappingContext: {
26
+ version,
27
+ sourceMapping: input.sourceMapping,
28
+ targetMapping: "mojang",
29
+ sourcePriorityApplied,
30
+ unobfuscatedRuntime: true
31
+ },
32
+ resolved: false,
33
+ status: "not_found",
34
+ candidates: [],
35
+ candidateCount: 0,
36
+ warnings: [...workspaceDetection.warnings]
37
+ };
38
+ // The runtime names are both the "obfuscated" (as-shipped) and the Mojang names;
39
+ // intermediary and yarn names do not exist for these versions.
40
+ if (input.sourceMapping !== "obfuscated" && input.sourceMapping !== "mojang") {
41
+ return {
42
+ ...base,
43
+ status: "mapping_unavailable",
44
+ workspaceDetection,
45
+ warnings: [
46
+ ...base.warnings,
47
+ `sourceMapping "${input.sourceMapping}" has no names on unobfuscated Minecraft ${version}; pass sourceMapping "mojang" (or "obfuscated") to resolve against runtime names.`
48
+ ]
49
+ };
50
+ }
51
+ const runtime = await runUnobfuscatedRuntimeCheck(svc, {
52
+ version,
53
+ kind: querySymbol.kind,
54
+ name: querySymbol.name,
55
+ owner: querySymbol.owner,
56
+ descriptor: querySymbol.descriptor,
57
+ sourceMapping: input.sourceMapping,
58
+ signatureMode: "exact"
59
+ }, base);
60
+ if (!runtime) {
61
+ // Mirrors checkSymbolExists: a symbol that could not be checked is not reported missing.
62
+ return {
63
+ ...base,
64
+ status: "mapping_unavailable",
65
+ workspaceDetection,
66
+ warnings: [
67
+ ...base.warnings,
68
+ `Minecraft ${version} runtime jar could not be resolved; symbol existence was not verified.`
69
+ ]
70
+ };
71
+ }
72
+ if (!runtime.verified) {
73
+ // The jar was reached but did not answer (short class name, class that failed to
74
+ // load for a reason other than being absent): the result carries the reason, and
75
+ // an unanswered lookup is not a definitive miss. `not_found` stays reserved for a
76
+ // class confirmed missing or a member lookup that completed without a match.
77
+ return { ...runtime.result, status: "mapping_unavailable", workspaceDetection };
78
+ }
79
+ return { ...runtime.result, workspaceDetection };
80
+ }
2
81
  export async function resolveWorkspaceSymbol(svc, input) {
3
82
  const projectPath = input.projectPath?.trim();
4
83
  const version = input.version?.trim();
@@ -77,6 +156,15 @@ export async function resolveWorkspaceSymbol(svc, input) {
77
156
  const workspaceDetection = await svc.workspaceMappingService.detectCompileMapping({
78
157
  projectPath
79
158
  });
159
+ // No evidence also means no build script was found at all (a missing, mistyped or
160
+ // empty projectPath). Only a build that was read and declares no mappings is a
161
+ // 26.1+ Loom project compiling against the runtime names.
162
+ if (!workspaceDetection.resolved &&
163
+ workspaceDetection.evidence.length === 0 &&
164
+ isUnobfuscatedVersion(version) &&
165
+ (await svc.workspaceMappingService.hasReadableBuildScript(projectPath))) {
166
+ return resolveUnobfuscatedWorkspaceSymbol(svc, input, { version, querySymbol, sourcePriorityApplied });
167
+ }
80
168
  const warnings = [...workspaceDetection.warnings];
81
169
  if (!workspaceDetection.resolved || !workspaceDetection.mappingApplied) {
82
170
  return {
@@ -1,4 +1,23 @@
1
1
  import { existsSync } from "node:fs";
2
+ /**
3
+ * Whether the mapping data required by `requestedMapping` is present.
4
+ *
5
+ * Mojang mappings are only on the resolution path for `"mojang"` requests.
6
+ * `"yarn"`/`"intermediary"` requests resolve through Tiny mappings instead
7
+ * and never touch Mojang mappings. `"obfuscated"` requests need neither —
8
+ * the jar itself (checked separately via `jarAvailable`) is sufficient.
9
+ */
10
+ function requiredMappingHealthy(requestedMapping, health) {
11
+ switch (requestedMapping) {
12
+ case "mojang":
13
+ return health.mojangMappingsAvailable;
14
+ case "yarn":
15
+ case "intermediary":
16
+ return health.tinyMappingsAvailable;
17
+ case "obfuscated":
18
+ return true;
19
+ }
20
+ }
2
21
  export async function runMappingHealthStage(svc, ctx) {
3
22
  const mappingHealthStartedAt = await ctx.enterStage("mapping-health");
4
23
  try {
@@ -15,7 +34,7 @@ export async function runMappingHealthStage(svc, ctx) {
15
34
  mojangMappingsAvailable: health.mojangMappingsAvailable,
16
35
  tinyMappingsAvailable: health.tinyMappingsAvailable,
17
36
  memberRemapAvailable: health.memberRemapAvailable,
18
- overallHealthy: jarAvailable && health.mojangMappingsAvailable,
37
+ overallHealthy: jarAvailable && requiredMappingHealthy(ctx.requestedMapping, health),
19
38
  degradations: [
20
39
  ...(jarAvailable ? [] : ["Game jar not found."]),
21
40
  ...health.degradations
@@ -101,10 +101,13 @@ export async function processSingleMixinTarget(svc, ctx, target, targetIndex) {
101
101
  }
102
102
  }
103
103
  try {
104
+ // Mixin can target synthetic members (lambda bodies, bridges, $VALUES, this$0) and counts
105
+ // synthetic methods in quantifiers.
104
106
  const sig = await svc.explorerService.getSignature({
105
107
  fqn: obfuscatedName,
106
108
  jarPath: ctx.jarPath,
107
- access: "all"
109
+ access: "all",
110
+ includeSynthetic: true
108
111
  });
109
112
  ctx.warnings.push(...sig.warnings);
110
113
  ctx.resolutionTrace?.push({ target: target.className, step: "signature", input: obfuscatedName, output: `${sig.methods.length} methods, ${sig.fields.length} fields`, success: true });
@@ -112,15 +115,21 @@ export async function processSingleMixinTarget(svc, ctx, target, targetIndex) {
112
115
  let methods = sig.methods;
113
116
  let fields = sig.fields;
114
117
  if (ctx.requestedMapping !== ctx.signatureLookupMapping) {
118
+ const remap = (members, kind, warnings) => svc.remapSignatureMembers(members, kind, ctx.version, ctx.signatureLookupMapping, ctx.requestedMapping, ctx.currentSourcePriority, warnings, ctx.input.projectPath, ctx.input.gradleUserHome);
119
+ // Unnamed lambdas routinely have no mapping; a synthetic member that fails to remap
120
+ // stays listed under its source name without degrading the target or the result.
121
+ const syntheticWarnings = [];
115
122
  try {
116
- const [ctorResult, methodResult, fieldResult] = await Promise.all([
117
- svc.remapSignatureMembers(sig.constructors, "method", ctx.version, ctx.signatureLookupMapping, ctx.requestedMapping, ctx.currentSourcePriority, ctx.warnings, ctx.input.projectPath, ctx.input.gradleUserHome),
118
- svc.remapSignatureMembers(sig.methods, "method", ctx.version, ctx.signatureLookupMapping, ctx.requestedMapping, ctx.currentSourcePriority, ctx.warnings, ctx.input.projectPath, ctx.input.gradleUserHome),
119
- svc.remapSignatureMembers(sig.fields, "field", ctx.version, ctx.signatureLookupMapping, ctx.requestedMapping, ctx.currentSourcePriority, ctx.warnings, ctx.input.projectPath, ctx.input.gradleUserHome)
123
+ const [ctorResult, methodResult, fieldResult, syntheticMethodResult, syntheticFieldResult] = await Promise.all([
124
+ remap(sig.constructors, "method", ctx.warnings),
125
+ remap(sig.methods.filter((member) => !member.isSynthetic), "method", ctx.warnings),
126
+ remap(sig.fields.filter((member) => !member.isSynthetic), "field", ctx.warnings),
127
+ remap(sig.methods.filter((member) => member.isSynthetic), "method", syntheticWarnings),
128
+ remap(sig.fields.filter((member) => member.isSynthetic), "field", syntheticWarnings)
120
129
  ]);
121
130
  constructors = ctorResult.members;
122
- methods = methodResult.members;
123
- fields = fieldResult.members;
131
+ methods = [...methodResult.members, ...syntheticMethodResult.members];
132
+ fields = [...fieldResult.members, ...syntheticFieldResult.members];
124
133
  const targetFailed = new Set();
125
134
  for (const n of ctorResult.failedNames)
126
135
  targetFailed.add(n);
@@ -8,6 +8,11 @@ export type ValidateMixinSingleInput = Omit<ValidateMixinInput, "input" | "versi
8
8
  version: string;
9
9
  source?: string;
10
10
  sourcePath?: string;
11
+ /**
12
+ * Validate only the `@Mixin` class at this simple-name path from the source's top-level class
13
+ * down (`["Outer"]` for a config entry `Outer`, `["Outer", "Inner"]` for `Outer$Inner`).
14
+ */
15
+ nestedClassPath?: string[];
11
16
  batchCaches?: {
12
17
  classMappings: Map<string, Promise<MappingFindMappingOutput>>;
13
18
  };