@adhisang/minecraft-modding-mcp 7.1.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.
- package/CHANGELOG.md +42 -0
- package/README.md +1 -1
- package/dist/access-transformer-parser.d.ts +8 -0
- package/dist/access-transformer-parser.js +8 -1
- package/dist/access-widener-parser.d.ts +17 -0
- package/dist/access-widener-parser.js +12 -1
- package/dist/cache-registry.js +19 -6
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +15 -9
- package/dist/entry-tools/manage-cache-service.js +10 -14
- package/dist/entry-tools/validate-project/cases/access-transformer.js +22 -3
- package/dist/entry-tools/validate-project/cases/access-widener.js +31 -4
- package/dist/entry-tools/validate-project/cases/mixin.js +11 -3
- package/dist/entry-tools/validate-project/cases/project-summary.js +24 -5
- package/dist/entry-tools/verify-mixin-target-service.js +19 -8
- package/dist/index.js +1 -0
- package/dist/java-process.d.ts +1 -0
- package/dist/java-process.js +14 -0
- package/dist/mapping/lookup.js +16 -1
- package/dist/mapping-service.d.ts +14 -0
- package/dist/mapping-service.js +35 -15
- package/dist/minecraft-explorer-service.js +70 -8
- package/dist/mixin/access-validators.js +38 -2
- package/dist/mixin/annotation-validators.js +137 -43
- package/dist/mixin/parsed-validator.js +21 -7
- package/dist/mixin-parser.d.ts +52 -0
- package/dist/mixin-parser.js +709 -130
- package/dist/mod-decompile-service.js +11 -1
- package/dist/mod-remap-service.js +6 -6
- package/dist/nbt/java-nbt-codec.js +7 -1
- package/dist/source/access-validate.js +10 -0
- package/dist/source/artifact-resolver.d.ts +13 -3
- package/dist/source/artifact-resolver.js +129 -18
- package/dist/source/class-source.js +13 -2
- package/dist/source/nested-jars.d.ts +15 -1
- package/dist/source/nested-jars.js +14 -5
- package/dist/source/search.d.ts +10 -2
- package/dist/source/search.js +60 -13
- package/dist/source/validate-mixin/pipeline/mapping-health.js +20 -1
- package/dist/source/validate-mixin/pipeline/target-lookup.js +16 -7
- package/dist/source/validate-mixin.d.ts +5 -0
- package/dist/source/validate-mixin.js +136 -21
- package/dist/source/workspace-target.js +75 -7
- package/dist/source-jar-reader.d.ts +15 -1
- package/dist/source-jar-reader.js +35 -3
- package/dist/source-resolver.js +2 -9
- package/dist/stdio-supervisor.d.ts +35 -1
- package/dist/stdio-supervisor.js +77 -2
- package/dist/storage/files-repo.d.ts +7 -0
- package/dist/storage/files-repo.js +17 -4
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +2 -1
- package/dist/version-service.js +7 -0
- package/dist/workspace-context-cache.d.ts +25 -0
- package/dist/workspace-context-cache.js +52 -2
- package/dist/workspace-mapping-service.js +116 -14
- package/docs/README-ja.md +1 -1
- package/docs/tool-reference.md +14 -11
- package/package.json +1 -1
|
@@ -135,7 +135,17 @@ export class ModDecompileService {
|
|
|
135
135
|
const outPath = isAbsolute(input.outputFile)
|
|
136
136
|
? input.outputFile
|
|
137
137
|
: resolvePath(input.outputFile);
|
|
138
|
-
|
|
138
|
+
try {
|
|
139
|
+
await writeFile(outPath, content, "utf8");
|
|
140
|
+
}
|
|
141
|
+
catch (writeError) {
|
|
142
|
+
const errno = writeError?.code;
|
|
143
|
+
throw createError({
|
|
144
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
145
|
+
message: `outputFile "${outPath}" could not be written (${errno ?? "unknown error"}).`,
|
|
146
|
+
details: { field: "outputFile", value: outPath, osErrorCode: errno }
|
|
147
|
+
});
|
|
148
|
+
}
|
|
139
149
|
outputFilePath = outPath;
|
|
140
150
|
content = `[Written to ${outPath}]`;
|
|
141
151
|
}
|
|
@@ -149,13 +149,13 @@ export async function remapModJar(input, config, deps = {}) {
|
|
|
149
149
|
mkdirSync(cacheDir, { recursive: true });
|
|
150
150
|
const cachedOutput = join(cacheDir, `${cacheKey}.jar`);
|
|
151
151
|
if (!input.forceRemap && existsSync(cachedOutput)) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
copyJarToDestination(cachedOutput,
|
|
156
|
-
log("info", "remap.cache-hit", { inputJar: normalizedInput, outputJar
|
|
152
|
+
// Always copy to the same `outputJar` location a cold run would use (the
|
|
153
|
+
// caller-supplied path, or resolveOutputJarPath's "next to input" default),
|
|
154
|
+
// so a cache hit and a cold run return the same path convention.
|
|
155
|
+
copyJarToDestination(cachedOutput, outputJar);
|
|
156
|
+
log("info", "remap.cache-hit", { inputJar: normalizedInput, outputJar });
|
|
157
157
|
return {
|
|
158
|
-
outputJar
|
|
158
|
+
outputJar,
|
|
159
159
|
mcVersion,
|
|
160
160
|
fromMapping: fromNamespace,
|
|
161
161
|
targetMapping: input.targetMapping,
|
|
@@ -480,7 +480,13 @@ export function decodeJavaNbt(buffer) {
|
|
|
480
480
|
throw error;
|
|
481
481
|
}
|
|
482
482
|
}
|
|
483
|
-
|
|
483
|
+
// Unlike every other parseError() call site in this module, nothing here
|
|
484
|
+
// has an offset to report - do not inherit PARSE_NEXT_ACTION's "at the
|
|
485
|
+
// reported offset" promise.
|
|
486
|
+
throw parseError("Failed to decode Java NBT payload.", {
|
|
487
|
+
nextAction: "The payload is not a well-formed Java NBT stream. Check that nbtBase64 holds the whole " +
|
|
488
|
+
'file and that compression matches how it was written (use compression "auto" to detect gzip).'
|
|
489
|
+
});
|
|
484
490
|
}
|
|
485
491
|
}
|
|
486
492
|
export function encodeJavaNbt(document) {
|
|
@@ -80,6 +80,16 @@ export async function validateAccessWidener(svc, input) {
|
|
|
80
80
|
if (!content.trim()) {
|
|
81
81
|
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "content must be non-empty." });
|
|
82
82
|
}
|
|
83
|
+
// Fabric Loader also reads the classTweaker format; it is out of scope here rather than an
|
|
84
|
+
// accessWidener file with a bad header, so it gets no verdict.
|
|
85
|
+
const header = content.split(/\r?\n/).map((line) => line.trim()).find((line) => line && !line.startsWith("#"));
|
|
86
|
+
if (header && /^classTweaker(\s|$)/.test(header)) {
|
|
87
|
+
throw createError({
|
|
88
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
89
|
+
message: `The classTweaker format is not supported (header "${header}"); only accessWidener v1/v2 files can be validated.`,
|
|
90
|
+
details: { unsupportedFormat: "classTweaker", header }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
83
93
|
const warnings = [];
|
|
84
94
|
const parsed = parseAccessWidener(content);
|
|
85
95
|
const headerNamespaceRaw = normalizeOptionalString(parsed.namespace);
|
|
@@ -140,8 +140,16 @@ export declare function hasLoaderRuntimeVersionToken(path: string, mcVersion: st
|
|
|
140
140
|
/**
|
|
141
141
|
* Minecraft version a runtime jar path carries, or undefined when the path
|
|
142
142
|
* names none. Loom lays its cache out as
|
|
143
|
-
* `<gradle>/caches/fabric-loom/<mcVersion>/...`, so the first
|
|
144
|
-
* of the path
|
|
143
|
+
* `<gradle>/caches/fabric-loom/<mcVersion>/...`, so the first token of the
|
|
144
|
+
* tool-written part of the path ({@link runtimeJarLayoutTail}; the whole path
|
|
145
|
+
* when it enters no layout root) that names a Minecraft version is the version the jar was built for — in
|
|
146
|
+
* either id family, and a pre-release names itself and not the release it
|
|
147
|
+
* precedes. Reading only `1.x[.y]` left every 26.x jar versionless, so no
|
|
148
|
+
* approximation was ever reported for one.
|
|
149
|
+
*
|
|
150
|
+
* A legacy `1.x` token is self-identifying; a 26.1+ token shares its shape with
|
|
151
|
+
* every loader's own version number, so it additionally has to sit where
|
|
152
|
+
* {@link tokenPositionNamesMinecraft} says Minecraft's version sits.
|
|
145
153
|
*/
|
|
146
154
|
export declare function inferRuntimeJarMinecraftVersion(path: string): string | undefined;
|
|
147
155
|
/**
|
|
@@ -177,7 +185,9 @@ export declare function inferRuntimeJarLoader(path: string): RuntimeLoader;
|
|
|
177
185
|
* `version` becomes the version the SERVED jar carries; the caller's original
|
|
178
186
|
* request is preserved under `requestedVersion` and flagged. A served loader
|
|
179
187
|
* that contradicts a KNOWN expected loader is flagged too — never silently
|
|
180
|
-
* dropped.
|
|
188
|
+
* dropped. When the jar's path names no version at all, the request stands as
|
|
189
|
+
* the answer and nothing is flagged: there is no served version to contradict
|
|
190
|
+
* it with.
|
|
181
191
|
*/
|
|
182
192
|
export declare function describeServedRuntimeJar(input: {
|
|
183
193
|
jarPath: string;
|
|
@@ -170,16 +170,30 @@ export function looksLikeMinecraftArtifactPath(path) {
|
|
|
170
170
|
function looksLikeMinecraftSourceArtifact(path, hasMinecraftNamespace) {
|
|
171
171
|
return hasMinecraftNamespace || looksLikeMinecraftArtifactPath(path);
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* What a Minecraft version id may be FOLLOWED by and still be the same id:
|
|
175
|
+
* nothing. A pre-release suffix extends the id rather than decorating it —
|
|
176
|
+
* `26.1-pre-1` is its own version and not `26.1` — so the hyphen forms Mojang
|
|
177
|
+
* publishes (`-pre1`, `-rc1`, `-pre-6`, `-rc-2`, `-snapshot-6`) have to close
|
|
178
|
+
* the token as firmly as a trailing digit does. Any OTHER hyphen is ordinary
|
|
179
|
+
* path decoration (`minecraft-1.21.1-merged.jar`) and must keep matching.
|
|
180
|
+
*
|
|
181
|
+
* The suffix grammar is `isUnobfuscatedVersion`'s in version-service.ts; the
|
|
182
|
+
* two are read together whenever either changes.
|
|
183
|
+
*/
|
|
184
|
+
const VERSION_ID_SUFFIX_LOOKAHEAD = "-(?:pre|rc)[0-9]|-(?:pre|rc|snapshot)-[0-9]";
|
|
173
185
|
export function hasExactVersionToken(path, version) {
|
|
174
186
|
const normalizedPath = normalizePathStyle(path).toLowerCase();
|
|
175
187
|
const normalizedVersion = version.trim().toLowerCase();
|
|
176
188
|
if (!normalizedVersion) {
|
|
177
189
|
return false;
|
|
178
190
|
}
|
|
179
|
-
// Avoid prefix false-positives like "1.21.1" matching "1.21.10"
|
|
180
|
-
|
|
191
|
+
// Avoid prefix false-positives like "1.21.1" matching "1.21.10", and
|
|
192
|
+
// pre-release false-positives like "26.1" matching "26.1-pre-1".
|
|
193
|
+
const cacheKey = normalizedVersion;
|
|
194
|
+
const cached = VERSION_TOKEN_REGEX_CACHE.get(cacheKey);
|
|
181
195
|
const pattern = cached
|
|
182
|
-
?? rememberCachedRegex(VERSION_TOKEN_REGEX_CACHE,
|
|
196
|
+
?? rememberCachedRegex(VERSION_TOKEN_REGEX_CACHE, cacheKey, new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}(?![0-9a-z]|\\.[0-9]|${VERSION_ID_SUFFIX_LOOKAHEAD})`, "i"));
|
|
183
197
|
return pattern.test(normalizedPath);
|
|
184
198
|
}
|
|
185
199
|
/**
|
|
@@ -209,15 +223,85 @@ export function hasLoaderRuntimeVersionToken(path, mcVersion) {
|
|
|
209
223
|
?? rememberCachedRegex(LOADER_VERSION_TOKEN_REGEX_CACHE, cacheKey, new RegExp(`(^|[^0-9a-z.])${minor}\\.${patch}\\.\\d`, "i"));
|
|
210
224
|
return pattern.test(normalizedPath);
|
|
211
225
|
}
|
|
212
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Every version-SHAPED token a path carries, in both id families: `<a>.<b>[.<c>]`
|
|
228
|
+
* with an optional pre-release suffix, and the `YYwNNa` snapshot form. Shape
|
|
229
|
+
* only — `inferRuntimeJarMinecraftVersion` decides which of them is Minecraft's.
|
|
230
|
+
*
|
|
231
|
+
* The alternatives are greedy, so the LONGEST token at a position is the one
|
|
232
|
+
* offered, suffix included. What closes it is another version component — a
|
|
233
|
+
* digit, or a dot that starts one — and nothing else: a dot that starts a file
|
|
234
|
+
* extension ends the token instead. Refusing every following dot made
|
|
235
|
+
* `minecraft-merged-26.1.jar` name no version and made
|
|
236
|
+
* `minecraft-merged-26.2-pre-6.jar` backtrack off its own suffix down to
|
|
237
|
+
* "26.2", the release that jar is not.
|
|
238
|
+
*/
|
|
239
|
+
const RUNTIME_JAR_VERSION_TOKEN_REGEX = /(?:^|[^0-9.])(\d+\.\d+(?:\.\d+)?(?:-(?:pre|rc)\d+|-(?:pre|rc|snapshot)-\d+)?|\d{2}w\d{2}[a-z])(?![0-9]|\.[0-9])/g;
|
|
240
|
+
/** The legacy obfuscated line, `1.21` / `1.21.10` / `1.14.4-pre7`. */
|
|
241
|
+
const LEGACY_MINECRAFT_VERSION_REGEX = /^1\.\d+(?:\.\d+)?(?:-(?:pre|rc)\d+|-(?:pre|rc|snapshot)-\d+)?$/;
|
|
242
|
+
/** Loom's cache root: the directory whose immediate children are Minecraft versions. */
|
|
243
|
+
const LOOM_CACHE_ROOT_SEGMENT_REGEX = /^(?:fabric-loom|loom-cache)$/;
|
|
244
|
+
/**
|
|
245
|
+
* Does the path POSITION a 26.1+ token sits at vouch for it being Minecraft's
|
|
246
|
+
* version?
|
|
247
|
+
*
|
|
248
|
+
* The new-format grammar is only a shape: `isUnobfuscatedVersion` accepts any
|
|
249
|
+
* id from year 26 up, and Forge's own versions have long since passed it
|
|
250
|
+
* (`forge-47.3.0-srg.jar`), so the token needs the same kind of corroboration
|
|
251
|
+
* the loader-version exclusions elsewhere in this file rely on. Three positions
|
|
252
|
+
* carry it, and they are the layouts the discovery helpers already walk: the
|
|
253
|
+
* artifact itself names Minecraft (`minecraft-merged-26.1.jar`), the entry
|
|
254
|
+
* directly inside the version directory does (neoformruntime's
|
|
255
|
+
* `<version>/minecraft-joined.jar`), or the version directory is a child of
|
|
256
|
+
* Loom's cache root (`<...>/fabric-loom/<mcVersion>/`).
|
|
257
|
+
*
|
|
258
|
+
* Only the token's own neighbourhood counts, never an ancestor — for the reason
|
|
259
|
+
* `runtimeJarLayoutRelativePath` exists: a checkout under `~/dev/minecraft/`
|
|
260
|
+
* names the user's directory, not the jar.
|
|
261
|
+
*/
|
|
262
|
+
function tokenPositionNamesMinecraft(segments, index) {
|
|
263
|
+
return ((segments[index]?.includes("minecraft") ?? false) ||
|
|
264
|
+
(segments[index + 1]?.includes("minecraft") ?? false) ||
|
|
265
|
+
LOOM_CACHE_ROOT_SEGMENT_REGEX.test(segments[index - 1] ?? ""));
|
|
266
|
+
}
|
|
213
267
|
/**
|
|
214
268
|
* Minecraft version a runtime jar path carries, or undefined when the path
|
|
215
269
|
* names none. Loom lays its cache out as
|
|
216
|
-
* `<gradle>/caches/fabric-loom/<mcVersion>/...`, so the first
|
|
217
|
-
* of the path
|
|
270
|
+
* `<gradle>/caches/fabric-loom/<mcVersion>/...`, so the first token of the
|
|
271
|
+
* tool-written part of the path ({@link runtimeJarLayoutTail}; the whole path
|
|
272
|
+
* when it enters no layout root) that names a Minecraft version is the version the jar was built for — in
|
|
273
|
+
* either id family, and a pre-release names itself and not the release it
|
|
274
|
+
* precedes. Reading only `1.x[.y]` left every 26.x jar versionless, so no
|
|
275
|
+
* approximation was ever reported for one.
|
|
276
|
+
*
|
|
277
|
+
* A legacy `1.x` token is self-identifying; a 26.1+ token shares its shape with
|
|
278
|
+
* every loader's own version number, so it additionally has to sit where
|
|
279
|
+
* {@link tokenPositionNamesMinecraft} says Minecraft's version sits.
|
|
218
280
|
*/
|
|
219
281
|
export function inferRuntimeJarMinecraftVersion(path) {
|
|
220
|
-
|
|
282
|
+
// A path that enters no layout root, such as a custom Gradle home's `loom-cache`, is read whole.
|
|
283
|
+
const normalizedPath = runtimeJarLayoutTail(path) ?? normalizePathStyle(path).toLowerCase();
|
|
284
|
+
const segments = normalizedPath.split("/");
|
|
285
|
+
for (const match of normalizedPath.matchAll(RUNTIME_JAR_VERSION_TOKEN_REGEX)) {
|
|
286
|
+
const token = match[1];
|
|
287
|
+
if (!token) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (LEGACY_MINECRAFT_VERSION_REGEX.test(token)) {
|
|
291
|
+
return token;
|
|
292
|
+
}
|
|
293
|
+
if (!isUnobfuscatedVersion(token)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
// `match[0]` carries the leading boundary character, so the token starts at
|
|
297
|
+
// its end minus the token's own length.
|
|
298
|
+
const tokenStart = (match.index ?? 0) + match[0].length - token.length;
|
|
299
|
+
const segmentIndex = normalizedPath.slice(0, tokenStart).split("/").length - 1;
|
|
300
|
+
if (tokenPositionNamesMinecraft(segments, segmentIndex)) {
|
|
301
|
+
return token;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return undefined;
|
|
221
305
|
}
|
|
222
306
|
/**
|
|
223
307
|
* The Minecraft version a `kind: "jar"` target PROVES it is the unobfuscated
|
|
@@ -271,6 +355,10 @@ const RUNTIME_JAR_LAYOUT_ROOT_RE = /^(?:\.[^/]*|caches|build)$/;
|
|
|
271
355
|
* loader for an artifact that names none.
|
|
272
356
|
*/
|
|
273
357
|
function runtimeJarLayoutRelativePath(path) {
|
|
358
|
+
return runtimeJarLayoutTail(path) ?? normalizePathStyle(path).toLowerCase().split("/").filter(Boolean).at(-1) ?? "";
|
|
359
|
+
}
|
|
360
|
+
/** Everything below the deepest layout root, or undefined when the path enters none. */
|
|
361
|
+
function runtimeJarLayoutTail(path) {
|
|
274
362
|
const segments = normalizePathStyle(path)
|
|
275
363
|
.toLowerCase()
|
|
276
364
|
.split("/")
|
|
@@ -280,7 +368,7 @@ function runtimeJarLayoutRelativePath(path) {
|
|
|
280
368
|
return segments.slice(index + 1).join("/");
|
|
281
369
|
}
|
|
282
370
|
}
|
|
283
|
-
return
|
|
371
|
+
return undefined;
|
|
284
372
|
}
|
|
285
373
|
/**
|
|
286
374
|
* Loader a runtime jar belongs to, read from the tool-written part of its path.
|
|
@@ -320,7 +408,9 @@ export function inferRuntimeJarLoader(path) {
|
|
|
320
408
|
* `version` becomes the version the SERVED jar carries; the caller's original
|
|
321
409
|
* request is preserved under `requestedVersion` and flagged. A served loader
|
|
322
410
|
* that contradicts a KNOWN expected loader is flagged too — never silently
|
|
323
|
-
* dropped.
|
|
411
|
+
* dropped. When the jar's path names no version at all, the request stands as
|
|
412
|
+
* the answer and nothing is flagged: there is no served version to contradict
|
|
413
|
+
* it with.
|
|
324
414
|
*/
|
|
325
415
|
export function describeServedRuntimeJar(input) {
|
|
326
416
|
const notes = [];
|
|
@@ -527,8 +617,9 @@ export async function probeMinecraftArtifact(svc, input) {
|
|
|
527
617
|
let value = input.target.value.trim();
|
|
528
618
|
const warnings = [];
|
|
529
619
|
const requestedMapping = normalizeMapping(input.mapping);
|
|
530
|
-
|
|
531
|
-
|
|
620
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
|
|
621
|
+
if (input.preferProjectVersion && normalizedProjectPath) {
|
|
622
|
+
const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
|
|
532
623
|
if (detected && detected !== value) {
|
|
533
624
|
warnings.push(`Overriding version "${value}" with project version "${detected}" from gradle.properties.`);
|
|
534
625
|
}
|
|
@@ -580,7 +671,7 @@ export async function probeMinecraftArtifact(svc, input) {
|
|
|
580
671
|
}
|
|
581
672
|
const versionSourceDiscovery = await svc.discoverVersionSourceJar({
|
|
582
673
|
version: resolvedVersion,
|
|
583
|
-
projectPath:
|
|
674
|
+
projectPath: normalizedProjectPath,
|
|
584
675
|
gradleUserHome: input.gradleUserHome
|
|
585
676
|
});
|
|
586
677
|
if (!versionSourceDiscovery.selectedSourceJarPath) {
|
|
@@ -1128,10 +1219,11 @@ export async function resolveVersionContext(svc, input) {
|
|
|
1128
1219
|
if (inferredVersion) {
|
|
1129
1220
|
return inferredVersion;
|
|
1130
1221
|
}
|
|
1131
|
-
|
|
1222
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
|
|
1223
|
+
if (!input.preferProjectVersion || !normalizedProjectPath) {
|
|
1132
1224
|
return undefined;
|
|
1133
1225
|
}
|
|
1134
|
-
const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(
|
|
1226
|
+
const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
|
|
1135
1227
|
if (detected) {
|
|
1136
1228
|
input.warnings.push(`Using project version "${detected}" from gradle.properties because the artifact metadata did not include a version.`);
|
|
1137
1229
|
}
|
|
@@ -1268,7 +1360,7 @@ export async function buildMappingFallbackSuggestedCall(svc, args) {
|
|
|
1268
1360
|
if (process.env.WORKSPACE_FALLBACK_LEGACY === "1") {
|
|
1269
1361
|
return buildLegacyMappingFallback({ kind, value, scope, isVanillaMojang, projectPath: input.projectPath });
|
|
1270
1362
|
}
|
|
1271
|
-
const projectPath = input.projectPath
|
|
1363
|
+
const projectPath = normalizeOptionalProjectPath(input.projectPath);
|
|
1272
1364
|
if (!projectPath) {
|
|
1273
1365
|
return buildLegacyMappingFallback({ kind, value, scope, isVanillaMojang, projectPath: undefined });
|
|
1274
1366
|
}
|
|
@@ -1428,8 +1520,15 @@ export async function resolveArtifact(svc, input) {
|
|
|
1428
1520
|
const mapping = normalizeMapping(input.mapping);
|
|
1429
1521
|
const scope = input.scope;
|
|
1430
1522
|
const warnings = [...synthesisWarnings];
|
|
1431
|
-
|
|
1432
|
-
|
|
1523
|
+
// Normalized only for the readers that need it: every other use of
|
|
1524
|
+
// projectPath below reaches a helper that normalizes on its own, and doing it
|
|
1525
|
+
// here unconditionally would newly refuse a malformed path on calls that
|
|
1526
|
+
// never read one.
|
|
1527
|
+
const preferProjectVersionPath = input.preferProjectVersion
|
|
1528
|
+
? normalizeOptionalProjectPath(input.projectPath)
|
|
1529
|
+
: undefined;
|
|
1530
|
+
if (preferProjectVersionPath && kind === "version") {
|
|
1531
|
+
const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(preferProjectVersionPath);
|
|
1433
1532
|
if (detected && detected !== value) {
|
|
1434
1533
|
warnings.push(`Overriding version "${value}" with project version "${detected}" from gradle.properties.`);
|
|
1435
1534
|
}
|
|
@@ -1640,9 +1739,21 @@ export async function resolveArtifact(svc, input) {
|
|
|
1640
1739
|
mapping: effectiveMapping,
|
|
1641
1740
|
target: { kind, value },
|
|
1642
1741
|
nextAction: "Use target: { kind: \"version\", value } or a versioned Maven coordinate so mapping artifacts can be resolved.",
|
|
1742
|
+
// This branch is reached precisely because no version is known, and
|
|
1743
|
+
// `value` is whatever the caller's own target carried - a jar path
|
|
1744
|
+
// for the kind that gets here. Replaying it as a version target
|
|
1745
|
+
// produced a `suggestedCall` that could never succeed, so the
|
|
1746
|
+
// version goes out as a placeholder the caller must fill in;
|
|
1747
|
+
// buildSuggestedCall routes such a template to exampleCalls.
|
|
1643
1748
|
...buildSuggestedCall({
|
|
1644
1749
|
tool: "resolve-artifact",
|
|
1645
|
-
params:
|
|
1750
|
+
params: undefined,
|
|
1751
|
+
examples: [
|
|
1752
|
+
{
|
|
1753
|
+
params: buildResolveArtifactParams({ kind: "version", value: "<your-mc-version>" }, { ...(scope ? { scope } : {}) }),
|
|
1754
|
+
reason: `Replace <your-mc-version> with the Minecraft version this artifact belongs to; ${effectiveMapping} mappings are resolved per version.`
|
|
1755
|
+
}
|
|
1756
|
+
]
|
|
1646
1757
|
})
|
|
1647
1758
|
}
|
|
1648
1759
|
});
|
|
@@ -626,7 +626,8 @@ export function findClass(svc, input) {
|
|
|
626
626
|
qualifiedName: isInnerMatch ? className : rowQualified,
|
|
627
627
|
filePath: row.filePath,
|
|
628
628
|
line: row.line,
|
|
629
|
-
symbolKind: row.symbolKind
|
|
629
|
+
symbolKind: row.symbolKind,
|
|
630
|
+
...(isInnerMatch ? { nested: true, enclosingClass: rowQualified } : {})
|
|
630
631
|
};
|
|
631
632
|
})
|
|
632
633
|
.slice(0, limit);
|
|
@@ -1115,7 +1116,17 @@ export async function getClassSource(svc, input) {
|
|
|
1115
1116
|
const outputPath = isAbsolute(outputFile)
|
|
1116
1117
|
? outputFile
|
|
1117
1118
|
: resolvePath(outputFile);
|
|
1118
|
-
|
|
1119
|
+
try {
|
|
1120
|
+
await writeFile(outputPath, sourceText, "utf8");
|
|
1121
|
+
}
|
|
1122
|
+
catch (writeError) {
|
|
1123
|
+
const errno = writeError?.code;
|
|
1124
|
+
throw createError({
|
|
1125
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1126
|
+
message: `outputFile "${outputPath}" could not be written (${errno ?? "unknown error"}).`,
|
|
1127
|
+
details: { field: "outputFile", value: outputPath, osErrorCode: errno }
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1119
1130
|
resolvedOutputFile = outputPath;
|
|
1120
1131
|
sourceText = `[Written to ${outputPath}]`;
|
|
1121
1132
|
}
|
|
@@ -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: `${
|
|
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
|
|
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 });
|
package/dist/source/search.d.ts
CHANGED
|
@@ -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):
|
|
46
|
-
export declare function searchPathIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => 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[];
|
package/dist/source/search.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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. */
|