@adhisang/minecraft-modding-mcp 4.2.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +6 -4
- package/dist/access-transformer-parser.d.ts +2 -1
- package/dist/access-transformer-parser.js +10 -1
- package/dist/build-suggested-call.js +31 -10
- package/dist/cache-registry.d.ts +8 -0
- package/dist/cache-registry.js +99 -6
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
- package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
- package/dist/entry-tools/analyze-symbol-service.js +124 -28
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-members-service.js +6 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-source-service.js +5 -6
- package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
- package/dist/entry-tools/batch-mappings-service.js +4 -5
- package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
- package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
- package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
- package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -2
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +1 -1
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
- package/dist/entry-tools/inspect-minecraft/internal.js +30 -14
- package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/response-contract.d.ts +1 -1
- package/dist/entry-tools/response-contract.js +1 -0
- package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
- package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
- package/dist/entry-tools/validate-project-service.d.ts +42 -36
- package/dist/entry-tools/validate-project-service.js +3 -1
- package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
- package/dist/entry-tools/verify-mixin-target-service.js +113 -22
- package/dist/error-mapping.d.ts +51 -0
- package/dist/error-mapping.js +157 -2
- package/dist/errors.js +4 -1
- package/dist/index.js +138 -71
- package/dist/java-process.d.ts +9 -0
- package/dist/java-process.js +11 -5
- package/dist/mapping/internal-types.d.ts +7 -0
- package/dist/mapping/lookup.d.ts +1 -1
- package/dist/mapping/lookup.js +7 -1
- package/dist/mapping/types.d.ts +6 -0
- package/dist/mapping-service.d.ts +26 -2
- package/dist/mapping-service.js +178 -36
- package/dist/mcp-helpers.d.ts +1 -0
- package/dist/mcp-helpers.js +25 -3
- package/dist/minecraft-explorer-service.js +24 -11
- package/dist/mixin/access-validators.js +33 -1
- package/dist/mixin/annotation-validators.js +53 -7
- package/dist/mixin/parsed-validator.js +2 -2
- package/dist/mixin-parser.js +48 -4
- package/dist/mod-decompile-service.js +2 -1
- package/dist/mod-remap-service.d.ts +20 -1
- package/dist/mod-remap-service.js +95 -40
- package/dist/nbt/java-nbt-codec.js +87 -2
- package/dist/observability.d.ts +6 -0
- package/dist/observability.js +10 -0
- package/dist/page-cursor.d.ts +17 -0
- package/dist/page-cursor.js +0 -0
- package/dist/registry-service.js +25 -1
- package/dist/resources.js +45 -9
- package/dist/response-utils.d.ts +29 -4
- package/dist/response-utils.js +95 -5
- package/dist/source/access-validate.js +12 -2
- package/dist/source/artifact-resolver.js +1 -1
- package/dist/source/cache-metrics.js +2 -2
- package/dist/source/class-source/members-builder.d.ts +44 -1
- package/dist/source/class-source/members-builder.js +51 -4
- package/dist/source/class-source/snippet-builder.d.ts +13 -0
- package/dist/source/class-source/snippet-builder.js +48 -4
- package/dist/source/class-source-helpers.js +67 -11
- package/dist/source/class-source.js +97 -20
- package/dist/source/indexer.d.ts +31 -6
- package/dist/source/indexer.js +95 -25
- package/dist/source/search.d.ts +1 -1
- package/dist/source/search.js +94 -22
- package/dist/source/validate-mixin.d.ts +2 -1
- package/dist/source/validate-mixin.js +45 -4
- package/dist/source/workspace-target.js +15 -9
- package/dist/source-jar-reader.d.ts +54 -1
- package/dist/source-jar-reader.js +121 -10
- package/dist/source-service.d.ts +28 -7
- package/dist/storage/files-repo.d.ts +12 -0
- package/dist/storage/files-repo.js +24 -0
- package/dist/storage/symbols-repo.d.ts +6 -0
- package/dist/storage/symbols-repo.js +6 -0
- package/dist/symbols/symbol-extractor.js +35 -6
- package/dist/text-truncate.d.ts +7 -0
- package/dist/text-truncate.js +21 -0
- package/dist/tiny-remapper-resolver.js +10 -0
- package/dist/tiny-remapper-service.js +3 -3
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +12 -1
- package/dist/tool-guidance.d.ts +4 -1
- package/dist/tool-guidance.js +37 -12
- package/dist/tool-schemas.d.ts +559 -494
- package/dist/tool-schemas.js +99 -63
- package/dist/types.d.ts +1 -0
- package/dist/version-diff-service.js +5 -0
- package/dist/version-service.d.ts +3 -0
- package/dist/version-service.js +18 -2
- package/dist/warning-details.d.ts +18 -0
- package/dist/warning-details.js +63 -0
- package/docs/README-ja.md +2 -2
- package/docs/examples.md +26 -2
- package/docs/tool-reference.md +23 -17
- package/package.json +1 -1
package/dist/source/search.js
CHANGED
|
@@ -182,6 +182,11 @@ export function findContentMatchIndex(content, query, match, pattern) {
|
|
|
182
182
|
}
|
|
183
183
|
const normalizedContent = toLower(content);
|
|
184
184
|
const normalizedQuery = toLower(query);
|
|
185
|
+
// Both "contains" and "prefix" map to a substring search here. For free-text
|
|
186
|
+
// content, `prefix` is intentionally treated as "contains": file bodies are
|
|
187
|
+
// searched grep-style and the default match for intent=text resolves to
|
|
188
|
+
// "prefix" (see normalizeMatch), so anchoring at the start of the file would
|
|
189
|
+
// yield almost no hits. Symbol/path prefix anchoring lives in findMatchIndex.
|
|
185
190
|
return normalizedContent.indexOf(normalizedQuery);
|
|
186
191
|
}
|
|
187
192
|
export function scoreSymbolMatch(match, index, symbolKind) {
|
|
@@ -336,6 +341,10 @@ export async function searchClassSource(svc, input) {
|
|
|
336
341
|
});
|
|
337
342
|
const decodedCursor = decodeSearchCursor(input.cursor);
|
|
338
343
|
const cursor = decodedCursor?.contextKey === cursorContext ? decodedCursor : undefined;
|
|
344
|
+
// A provided cursor that did not decode, or whose context key does not match
|
|
345
|
+
// this query, is silently dropped and the scan restarts from page one. Flag
|
|
346
|
+
// it so callers do not assume they are continuing a previous page.
|
|
347
|
+
const cursorIgnored = input.cursor != null && cursor == null;
|
|
339
348
|
const accumulator = createSearchHitAccumulator(limit, cursor);
|
|
340
349
|
const indexedSearchEnabled = svc.config.indexedSearchEnabled !== false;
|
|
341
350
|
if (match === "regex") {
|
|
@@ -345,13 +354,16 @@ export async function searchClassSource(svc, input) {
|
|
|
345
354
|
const recordHit = (hit) => {
|
|
346
355
|
accumulator.add(hit);
|
|
347
356
|
};
|
|
357
|
+
const recordWarning = (warning) => {
|
|
358
|
+
searchWarnings.push(warning);
|
|
359
|
+
};
|
|
348
360
|
const tokenOnlyTextIntent = intent === "text" && queryMode === "token";
|
|
349
361
|
if (intent === "symbol") {
|
|
350
362
|
searchSymbolIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
351
363
|
}
|
|
352
364
|
else if (queryMode === "literal" && intent === "text") {
|
|
353
365
|
svc.metrics.recordSearchFallback();
|
|
354
|
-
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
366
|
+
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit, recordWarning);
|
|
355
367
|
}
|
|
356
368
|
else if (!indexedSearchEnabled) {
|
|
357
369
|
svc.metrics.recordIndexedDisabled();
|
|
@@ -361,7 +373,7 @@ export async function searchClassSource(svc, input) {
|
|
|
361
373
|
searchPathIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
362
374
|
}
|
|
363
375
|
else {
|
|
364
|
-
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
376
|
+
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit, recordWarning);
|
|
365
377
|
}
|
|
366
378
|
}
|
|
367
379
|
}
|
|
@@ -388,7 +400,7 @@ export async function searchClassSource(svc, input) {
|
|
|
388
400
|
searchPathIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
389
401
|
}
|
|
390
402
|
else {
|
|
391
|
-
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
403
|
+
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit, recordWarning);
|
|
392
404
|
}
|
|
393
405
|
}
|
|
394
406
|
}
|
|
@@ -400,7 +412,7 @@ export async function searchClassSource(svc, input) {
|
|
|
400
412
|
searchPathIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
401
413
|
}
|
|
402
414
|
else {
|
|
403
|
-
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
415
|
+
searchTextIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit, recordWarning);
|
|
404
416
|
}
|
|
405
417
|
}
|
|
406
418
|
}
|
|
@@ -415,6 +427,7 @@ export async function searchClassSource(svc, input) {
|
|
|
415
427
|
return {
|
|
416
428
|
hits: page,
|
|
417
429
|
nextCursor,
|
|
430
|
+
...(cursorIgnored ? { cursorIgnored: true } : {}),
|
|
418
431
|
mappingApplied: artifact.mappingApplied ?? "obfuscated",
|
|
419
432
|
returnedNamespace: artifact.mappingApplied ?? "obfuscated",
|
|
420
433
|
artifactContents: svc.buildArtifactContentsSummary({
|
|
@@ -534,39 +547,98 @@ export function searchPathIntentIndexed(svc, artifactId, query, match, scope, on
|
|
|
534
547
|
});
|
|
535
548
|
}
|
|
536
549
|
}
|
|
537
|
-
|
|
538
|
-
|
|
550
|
+
const ASCII_RE = /^[\x00-\x7F]*$/;
|
|
551
|
+
/** True when every code unit is ASCII, so SQLite LIKE folds case identically to JS. */
|
|
552
|
+
function isAsciiNeedle(query) {
|
|
553
|
+
return ASCII_RE.test(query);
|
|
554
|
+
}
|
|
555
|
+
function searchScanBudgetWarning(byteBudget) {
|
|
556
|
+
return `search scan budget of ${byteBudget} bytes reached; results may be incomplete. Narrow the query or scope (packagePrefix / fileGlob) to scan fewer files.`;
|
|
557
|
+
}
|
|
558
|
+
export function searchTextIntent(svc, artifactId, query, match, scope, regexPattern, onHit, onWarning) {
|
|
539
559
|
const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
560
|
+
const byteBudget = Math.max(1, svc.config.searchScanMaxBytes ?? Number.MAX_SAFE_INTEGER);
|
|
561
|
+
let scannedBytes = 0;
|
|
562
|
+
let truncated = false;
|
|
563
|
+
const passesScope = (filePath) => checkPackagePrefix(filePath, scope?.packagePrefix) && (!glob || glob.test(filePath));
|
|
564
|
+
const emitIfMatch = (filePath, content) => {
|
|
565
|
+
const contentIndex = match === "regex"
|
|
566
|
+
? matchRegexIndex(content, regexPattern)
|
|
567
|
+
: findContentMatchIndex(content, query, match);
|
|
568
|
+
if (contentIndex < 0) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
onHit({
|
|
572
|
+
filePath,
|
|
573
|
+
score: scoreTextMatch(match, contentIndex),
|
|
574
|
+
matchedIn: "content",
|
|
575
|
+
reasonCodes: ["content_match", `text_${match}`]
|
|
576
|
+
});
|
|
577
|
+
};
|
|
578
|
+
// FAST PATH: an ASCII, non-regex needle narrows candidates via a content LIKE
|
|
579
|
+
// prefilter (a proven superset of the JS match), so only matching files are
|
|
580
|
+
// hydrated instead of scanning the whole corpus. The unchanged JS post-verify in
|
|
581
|
+
// emitIfMatch removes any over-included rows.
|
|
582
|
+
if (match !== "regex" && isAsciiNeedle(query)) {
|
|
583
|
+
const candidateLimit = indexedCandidateLimitForMatch(svc, match);
|
|
584
|
+
const { filePaths, scannedRows } = svc.filesRepo.searchContentLikeCandidatePaths(artifactId, query, candidateLimit);
|
|
585
|
+
svc.metrics.recordSearchDbRoundtrip();
|
|
586
|
+
svc.metrics.recordSearchRowsScanned(scannedRows);
|
|
587
|
+
// The prefilter caps candidates at candidateLimit ordered by file_path. Below the
|
|
588
|
+
// cap the candidate set is exhaustive, so the JS post-verify yields exactly the old
|
|
589
|
+
// full-scan hits. AT the cap more matches may exist beyond the path-ordered cut, and
|
|
590
|
+
// dropping them would change the scored top-K page and totalApprox/cursor — so fall
|
|
591
|
+
// through to the exhaustive paged scan below to preserve identical results.
|
|
592
|
+
if (scannedRows < candidateLimit) {
|
|
593
|
+
svc.metrics.recordSearchLikePrefilter();
|
|
594
|
+
const scopedPaths = filePaths.filter(passesScope);
|
|
595
|
+
const rows = svc.filesRepo.getFileContentsByPaths(artifactId, scopedPaths);
|
|
596
|
+
svc.metrics.recordSearchDbRoundtrip();
|
|
597
|
+
svc.metrics.recordSearchRowsScanned(rows.length);
|
|
598
|
+
for (const row of rows) {
|
|
599
|
+
if (scannedBytes >= byteBudget) {
|
|
600
|
+
truncated = true;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
scannedBytes += row.contentBytes;
|
|
604
|
+
emitIfMatch(row.filePath, row.content);
|
|
605
|
+
}
|
|
606
|
+
if (truncated) {
|
|
607
|
+
svc.metrics.recordSearchScanTruncated();
|
|
608
|
+
onWarning?.(searchScanBudgetWarning(byteBudget));
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
// Cap overflow: fall through to the exhaustive scan (rare; only common-token queries).
|
|
613
|
+
}
|
|
614
|
+
// SLOW PATH: regex (cannot be pushed to SQL) or non-ASCII needles (SQLite LIKE
|
|
615
|
+
// would fold case differently than JS) scan the corpus page by page.
|
|
616
|
+
const pageSize = Math.max(1, svc.config.searchScanPageSize ?? 250);
|
|
540
617
|
let cursor = undefined;
|
|
541
|
-
while (true) {
|
|
618
|
+
outer: while (true) {
|
|
542
619
|
const page = svc.filesRepo.listFileRows(artifactId, { limit: pageSize, cursor });
|
|
543
620
|
svc.metrics.recordSearchDbRoundtrip();
|
|
544
621
|
svc.metrics.recordSearchRowsScanned(page.items.length);
|
|
545
622
|
for (const row of page.items) {
|
|
546
|
-
if (!
|
|
623
|
+
if (!passesScope(row.filePath)) {
|
|
547
624
|
continue;
|
|
548
625
|
}
|
|
549
|
-
if (
|
|
550
|
-
|
|
626
|
+
if (scannedBytes >= byteBudget) {
|
|
627
|
+
truncated = true;
|
|
628
|
+
break outer;
|
|
551
629
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
: findContentMatchIndex(row.content, query, match);
|
|
555
|
-
if (contentIndex < 0) {
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
onHit({
|
|
559
|
-
filePath: row.filePath,
|
|
560
|
-
score: scoreTextMatch(match, contentIndex),
|
|
561
|
-
matchedIn: "content",
|
|
562
|
-
reasonCodes: ["content_match", `text_${match}`]
|
|
563
|
-
});
|
|
630
|
+
scannedBytes += row.contentBytes;
|
|
631
|
+
emitIfMatch(row.filePath, row.content);
|
|
564
632
|
}
|
|
565
633
|
if (!page.nextCursor) {
|
|
566
634
|
break;
|
|
567
635
|
}
|
|
568
636
|
cursor = page.nextCursor;
|
|
569
637
|
}
|
|
638
|
+
if (truncated) {
|
|
639
|
+
svc.metrics.recordSearchScanTruncated();
|
|
640
|
+
onWarning?.(searchScanBudgetWarning(byteBudget));
|
|
641
|
+
}
|
|
570
642
|
}
|
|
571
643
|
export function searchPathIntent(svc, artifactId, query, match, scope, regexPattern, onHit) {
|
|
572
644
|
const pageSize = Math.max(1, svc.config.searchScanPageSize ?? 250);
|
|
@@ -4,7 +4,8 @@ import type { ValidateMixinInput, ValidateMixinOptions, ValidateMixinOutput } fr
|
|
|
4
4
|
import { type StageEmitter } from "../stage-emitter.js";
|
|
5
5
|
import type { MappingSourcePriority } from "../types.js";
|
|
6
6
|
import type { FindMappingOutput as MappingFindMappingOutput } from "../mapping-service.js";
|
|
7
|
-
export type ValidateMixinSingleInput = Omit<ValidateMixinInput, "input"> & {
|
|
7
|
+
export type ValidateMixinSingleInput = Omit<ValidateMixinInput, "input" | "version"> & {
|
|
8
|
+
version: string;
|
|
8
9
|
source?: string;
|
|
9
10
|
sourcePath?: string;
|
|
10
11
|
batchCaches?: {
|
|
@@ -159,7 +159,43 @@ export async function validateMixin(svc, input, options = {}) {
|
|
|
159
159
|
throw annotateValidateMixinError(err, "input-validation");
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the Minecraft version for a validate-mixin request. Uses the explicit
|
|
164
|
+
* `version` when present; otherwise detects it from gradle.properties for
|
|
165
|
+
* project mode or when preferProjectVersion is set with a projectPath. Throws an
|
|
166
|
+
* actionable INVALID_INPUT error when no version can be determined.
|
|
167
|
+
*/
|
|
168
|
+
async function resolveRequestedMinecraftVersion(svc, input) {
|
|
169
|
+
const explicit = input.version?.trim();
|
|
170
|
+
if (explicit) {
|
|
171
|
+
return explicit;
|
|
172
|
+
}
|
|
173
|
+
const projectPathCandidate = input.projectPath ?? (input.input.mode === "project" ? input.input.path : undefined);
|
|
174
|
+
if (projectPathCandidate) {
|
|
175
|
+
const normalizedProjectPath = resolveMixinInputPath(projectPathCandidate, "path");
|
|
176
|
+
const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
|
|
177
|
+
if (detected?.trim()) {
|
|
178
|
+
return detected.trim();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
throw createError({
|
|
182
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
183
|
+
message: "Could not detect a Minecraft version from the project. Pass version explicitly, or ensure gradle.properties declares minecraft_version.",
|
|
184
|
+
details: {
|
|
185
|
+
failedStage: "input-validation",
|
|
186
|
+
nextAction: 'Pass version (e.g. "1.21.10"), or set preferProjectVersion=true with a projectPath whose gradle.properties contains minecraft_version.'
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async function runValidateMixinDispatcher(svc, rawInput, options = {}) {
|
|
191
|
+
const requestedVersion = await resolveRequestedMinecraftVersion(svc, rawInput);
|
|
192
|
+
// Service-level default so direct/internal/entry-tool callers (not just the MCP
|
|
193
|
+
// wire boundary, where Zod applies it) get the lean summary-first report shape.
|
|
194
|
+
const input = {
|
|
195
|
+
...rawInput,
|
|
196
|
+
version: requestedVersion,
|
|
197
|
+
reportMode: rawInput.reportMode ?? "summary-first"
|
|
198
|
+
};
|
|
163
199
|
const { input: sourceInput, ...sharedInput } = input;
|
|
164
200
|
const mode = sourceInput.mode;
|
|
165
201
|
const stageEmitter = options.stageEmitter ?? NOOP_STAGE_EMITTER;
|
|
@@ -495,7 +531,9 @@ async function finalizeValidateMixinPipeline(svc, ctx) {
|
|
|
495
531
|
result.aggregatedWarnings = undefined;
|
|
496
532
|
result.toolHealth = undefined;
|
|
497
533
|
result.confidenceBreakdown = undefined;
|
|
498
|
-
|
|
534
|
+
// resolutionTrace only exists when explain=true; keep it then so explain still
|
|
535
|
+
// delivers the per-issue trace even under the compact report shape.
|
|
536
|
+
if (result.provenance && input.explain !== true) {
|
|
499
537
|
result.provenance.resolutionTrace = undefined;
|
|
500
538
|
}
|
|
501
539
|
}
|
|
@@ -676,6 +714,9 @@ function applyValidateMixinOutputCompaction(output, input) {
|
|
|
676
714
|
? true
|
|
677
715
|
: warningCandidates.every((entry) => sameStringArray(entry, warningCandidates[0]));
|
|
678
716
|
if (input.reportMode === "summary-first") {
|
|
717
|
+
// explain=true keeps the per-result heavy detail (resolvedMembers/toolHealth)
|
|
718
|
+
// even under summary-first, so the diagnostic opt-in still works.
|
|
719
|
+
const keepHeavyDetail = input.explain === true;
|
|
679
720
|
nextOutput = {
|
|
680
721
|
...nextOutput,
|
|
681
722
|
results: nextOutput.results.map((entry) => (entry.result
|
|
@@ -686,8 +727,8 @@ function applyValidateMixinOutputCompaction(output, input) {
|
|
|
686
727
|
warnings: canHoistWarnings ? [] : entry.result.warnings,
|
|
687
728
|
structuredWarnings: undefined,
|
|
688
729
|
aggregatedWarnings: undefined,
|
|
689
|
-
resolvedMembers: undefined,
|
|
690
|
-
toolHealth: undefined,
|
|
730
|
+
resolvedMembers: keepHeavyDetail ? entry.result.resolvedMembers : undefined,
|
|
731
|
+
toolHealth: keepHeavyDetail ? entry.result.toolHealth : undefined,
|
|
691
732
|
confidenceBreakdown: undefined,
|
|
692
733
|
provenance: canHoistProvenance ? undefined : entry.result.provenance
|
|
693
734
|
}
|
|
@@ -256,15 +256,21 @@ export async function synthesizeDependencyTarget(svc, input, dep) {
|
|
|
256
256
|
nextAction,
|
|
257
257
|
...buildSuggestedCall({
|
|
258
258
|
tool: "resolve-artifact",
|
|
259
|
-
params:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
259
|
+
params: undefined,
|
|
260
|
+
examples: [
|
|
261
|
+
{
|
|
262
|
+
params: {
|
|
263
|
+
target: {
|
|
264
|
+
kind: "dependency",
|
|
265
|
+
group,
|
|
266
|
+
name,
|
|
267
|
+
version: "<your-version>"
|
|
268
|
+
},
|
|
269
|
+
projectPath
|
|
270
|
+
},
|
|
271
|
+
reason: "Replace <your-version> with the dependency version, or declare it in gradle.properties."
|
|
272
|
+
}
|
|
273
|
+
]
|
|
268
274
|
})
|
|
269
275
|
}
|
|
270
276
|
});
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
export declare function __getZipOpenCount(): number;
|
|
3
|
+
export declare function __resetZipOpenCount(): void;
|
|
4
|
+
interface ZipEntry {
|
|
5
|
+
fileName: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ZipFile {
|
|
8
|
+
readEntry(): void;
|
|
9
|
+
close(): void;
|
|
10
|
+
once(event: "entry", listener: (entry: ZipEntry) => void): this;
|
|
11
|
+
once(event: "end", listener: () => void): this;
|
|
12
|
+
once(event: "error", listener: (error: Error) => void): this;
|
|
13
|
+
removeListener(event: "entry", listener: (entry: ZipEntry) => void): this;
|
|
14
|
+
removeListener(event: "end", listener: () => void): this;
|
|
15
|
+
removeListener(event: "error", listener: (error: Error) => void): this;
|
|
16
|
+
openReadStream(entry: ZipEntry, callback: (error: Error | null, stream: Readable | null) => void): void;
|
|
17
|
+
}
|
|
1
18
|
export interface JavaEntryText {
|
|
2
19
|
filePath: string;
|
|
3
20
|
content: string;
|
|
@@ -12,6 +29,7 @@ export interface CollectMatchedJarEntriesOptions {
|
|
|
12
29
|
continueOnError?: boolean;
|
|
13
30
|
}
|
|
14
31
|
export declare function hasJavaSourceExtension(entryPath: string): boolean;
|
|
32
|
+
export declare function openZipFile(jarPath: string): Promise<ZipFile>;
|
|
15
33
|
export declare class EntryTooLargeError extends Error {
|
|
16
34
|
constructor(entryPath: string, jarPath: string, maxBytes: number);
|
|
17
35
|
}
|
|
@@ -20,10 +38,45 @@ export declare function listJavaEntries(jarPath: string): Promise<string[]>;
|
|
|
20
38
|
export declare function hasAnyJarEntry(jarPath: string, predicate: (entryPath: string) => boolean): Promise<boolean>;
|
|
21
39
|
export declare function readJarEntryAsUtf8(jarPath: string, entryPath: string): Promise<string>;
|
|
22
40
|
export declare function readJarEntryAsBuffer(jarPath: string, entryPath: string): Promise<Buffer>;
|
|
41
|
+
export interface JarEntryReader {
|
|
42
|
+
getEntryBuffer(entryPath: string): Promise<Buffer>;
|
|
43
|
+
close(): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Opens a jar ONCE, drains its central directory into a name->entry index, and
|
|
47
|
+
* serves repeated entry reads via O(1) lookup + openReadStream. Use this when a
|
|
48
|
+
* single logical operation reads many entries from the same jar (e.g. walking a
|
|
49
|
+
* class hierarchy) instead of calling {@link readJarEntryAsBuffer} per entry,
|
|
50
|
+
* which re-opens and re-scans the jar each time. Callers MUST close() the reader
|
|
51
|
+
* (in a finally) to release the file descriptor. getEntryBuffer throws the same
|
|
52
|
+
* INVALID_INPUT (unsafe path) / SOURCE_NOT_FOUND (missing entry) errors as
|
|
53
|
+
* readJarEntryAsBuffer; duplicate entry names resolve to the first occurrence.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createJarEntryReader(jarPath: string): Promise<JarEntryReader>;
|
|
23
56
|
export declare function collectMatchedJarEntriesAsUtf8(jarPath: string, predicate: (entryPath: string) => boolean, options?: CollectMatchedJarEntriesOptions): Promise<JarEntryText[]>;
|
|
57
|
+
export interface JarEntryBuffer {
|
|
58
|
+
filePath: string;
|
|
59
|
+
content: Buffer;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Seam for injecting the jar-open primitive so callers/tests can count opens.
|
|
63
|
+
* Defaults to the module-level {@link openZipFile}.
|
|
64
|
+
*/
|
|
65
|
+
export interface JarReaderOpenDeps {
|
|
66
|
+
openZipFile?: (jarPath: string) => Promise<ZipFile>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Raw-buffer counterpart of {@link collectMatchedJarEntriesAsUtf8}: opens the jar
|
|
70
|
+
* ONCE and returns the matched entries as raw Buffers (no UTF-8 decode), so binary
|
|
71
|
+
* inputs like `.class` bytes survive intact. Used to avoid re-opening the jar per
|
|
72
|
+
* sampled entry. Mirrors the sibling's maxBytes/maxEntries/continueOnError semantics
|
|
73
|
+
* and its finally-close, but takes an injectable open seam for open-count assertions.
|
|
74
|
+
*/
|
|
75
|
+
export declare function collectMatchedJarEntriesAsBuffers(jarPath: string, predicate: (entryPath: string) => boolean, options?: CollectMatchedJarEntriesOptions, deps?: JarReaderOpenDeps): Promise<JarEntryBuffer[]>;
|
|
24
76
|
export declare function iterateJavaEntriesAsUtf8(jarPath: string, maxBytes?: number): AsyncGenerator<JavaEntryText>;
|
|
25
77
|
export declare function readAllJavaEntriesAsUtf8(jarPath: string, maxBytes?: number): Promise<JavaEntryText[]>;
|
|
26
|
-
export declare function detectFabricLikeInputNamespace(inputJar: string): Promise<{
|
|
78
|
+
export declare function detectFabricLikeInputNamespace(inputJar: string, deps?: JarReaderOpenDeps): Promise<{
|
|
27
79
|
fromNamespace: "intermediary" | "mojang";
|
|
28
80
|
warnings: string[];
|
|
29
81
|
}>;
|
|
82
|
+
export {};
|
|
@@ -3,6 +3,15 @@ import { createError, ERROR_CODES } from "./errors.js";
|
|
|
3
3
|
import { log } from "./logger.js";
|
|
4
4
|
import { isSecureJarEntryPath } from "./path-resolver.js";
|
|
5
5
|
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
6
|
+
// Test-only instrumentation: counts real jar opens at the single openZipFile
|
|
7
|
+
// chokepoint so perf tests can assert N-opens-per-call collapses to 1.
|
|
8
|
+
let zipOpenCount = 0;
|
|
9
|
+
export function __getZipOpenCount() {
|
|
10
|
+
return zipOpenCount;
|
|
11
|
+
}
|
|
12
|
+
export function __resetZipOpenCount() {
|
|
13
|
+
zipOpenCount = 0;
|
|
14
|
+
}
|
|
6
15
|
function toErrorMessage(value) {
|
|
7
16
|
if (value instanceof Error) {
|
|
8
17
|
return value.message;
|
|
@@ -23,7 +32,7 @@ export function hasJavaSourceExtension(entryPath) {
|
|
|
23
32
|
}
|
|
24
33
|
return true;
|
|
25
34
|
}
|
|
26
|
-
function openZipFile(jarPath) {
|
|
35
|
+
export function openZipFile(jarPath) {
|
|
27
36
|
return new Promise((resolve, reject) => {
|
|
28
37
|
yauzl.open(jarPath, {
|
|
29
38
|
lazyEntries: true,
|
|
@@ -33,6 +42,7 @@ function openZipFile(jarPath) {
|
|
|
33
42
|
reject(new Error(`Failed to read jar "${jarPath}": ${toErrorMessage(error)}`));
|
|
34
43
|
return;
|
|
35
44
|
}
|
|
45
|
+
zipOpenCount += 1;
|
|
36
46
|
resolve(zipFile);
|
|
37
47
|
});
|
|
38
48
|
});
|
|
@@ -193,6 +203,66 @@ export async function readJarEntryAsBuffer(jarPath, entryPath) {
|
|
|
193
203
|
}
|
|
194
204
|
});
|
|
195
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Opens a jar ONCE, drains its central directory into a name->entry index, and
|
|
208
|
+
* serves repeated entry reads via O(1) lookup + openReadStream. Use this when a
|
|
209
|
+
* single logical operation reads many entries from the same jar (e.g. walking a
|
|
210
|
+
* class hierarchy) instead of calling {@link readJarEntryAsBuffer} per entry,
|
|
211
|
+
* which re-opens and re-scans the jar each time. Callers MUST close() the reader
|
|
212
|
+
* (in a finally) to release the file descriptor. getEntryBuffer throws the same
|
|
213
|
+
* INVALID_INPUT (unsafe path) / SOURCE_NOT_FOUND (missing entry) errors as
|
|
214
|
+
* readJarEntryAsBuffer; duplicate entry names resolve to the first occurrence.
|
|
215
|
+
*/
|
|
216
|
+
export async function createJarEntryReader(jarPath) {
|
|
217
|
+
const zipFile = await openZipFile(jarPath);
|
|
218
|
+
let closed = false;
|
|
219
|
+
const index = new Map();
|
|
220
|
+
try {
|
|
221
|
+
while (true) {
|
|
222
|
+
const entry = await readNextEntry(zipFile);
|
|
223
|
+
if (!entry) {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
if (!isSecureJarEntryPath(entry.fileName)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (!index.has(entry.fileName)) {
|
|
230
|
+
index.set(entry.fileName, entry);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
zipFile.close();
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
async getEntryBuffer(entryPath) {
|
|
240
|
+
const normalizedTargetPath = entryPath.replaceAll("\\", "/");
|
|
241
|
+
if (!isSecureJarEntryPath(normalizedTargetPath)) {
|
|
242
|
+
throw createError({
|
|
243
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
244
|
+
message: `Entry path "${normalizedTargetPath}" is not allowed.`,
|
|
245
|
+
details: { jarPath, entryPath: normalizedTargetPath }
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const entry = index.get(normalizedTargetPath);
|
|
249
|
+
if (!entry) {
|
|
250
|
+
throw createError({
|
|
251
|
+
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
252
|
+
message: `Entry "${normalizedTargetPath}" was not found in "${jarPath}".`,
|
|
253
|
+
details: { jarPath, entryPath: normalizedTargetPath }
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return readEntryStream(zipFile, entry, jarPath);
|
|
257
|
+
},
|
|
258
|
+
close() {
|
|
259
|
+
if (!closed) {
|
|
260
|
+
closed = true;
|
|
261
|
+
zipFile.close();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
196
266
|
export async function collectMatchedJarEntriesAsUtf8(jarPath, predicate, options = {}) {
|
|
197
267
|
return withZipFile(jarPath, async (zipFile) => {
|
|
198
268
|
const entries = [];
|
|
@@ -226,6 +296,48 @@ export async function collectMatchedJarEntriesAsUtf8(jarPath, predicate, options
|
|
|
226
296
|
}
|
|
227
297
|
});
|
|
228
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Raw-buffer counterpart of {@link collectMatchedJarEntriesAsUtf8}: opens the jar
|
|
301
|
+
* ONCE and returns the matched entries as raw Buffers (no UTF-8 decode), so binary
|
|
302
|
+
* inputs like `.class` bytes survive intact. Used to avoid re-opening the jar per
|
|
303
|
+
* sampled entry. Mirrors the sibling's maxBytes/maxEntries/continueOnError semantics
|
|
304
|
+
* and its finally-close, but takes an injectable open seam for open-count assertions.
|
|
305
|
+
*/
|
|
306
|
+
export async function collectMatchedJarEntriesAsBuffers(jarPath, predicate, options = {}, deps = {}) {
|
|
307
|
+
const open = deps.openZipFile ?? openZipFile;
|
|
308
|
+
const zipFile = await open(jarPath);
|
|
309
|
+
try {
|
|
310
|
+
const entries = [];
|
|
311
|
+
const maxEntries = options.maxEntries == null ? undefined : Math.max(1, Math.trunc(options.maxEntries));
|
|
312
|
+
while (true) {
|
|
313
|
+
const entry = await readNextEntry(zipFile);
|
|
314
|
+
if (!entry) {
|
|
315
|
+
return entries;
|
|
316
|
+
}
|
|
317
|
+
if (!isSecureJarEntryPath(entry.fileName)) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!predicate(entry.fileName)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const contentBuffer = await readEntryStream(zipFile, entry, jarPath, options.maxBytes);
|
|
325
|
+
entries.push({ filePath: entry.fileName, content: contentBuffer });
|
|
326
|
+
if (maxEntries != null && entries.length >= maxEntries) {
|
|
327
|
+
return entries;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (!options.continueOnError) {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
zipFile.close();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
229
341
|
export async function* iterateJavaEntriesAsUtf8(jarPath, maxBytes) {
|
|
230
342
|
const zipFile = await openZipFile(jarPath);
|
|
231
343
|
try {
|
|
@@ -285,7 +397,7 @@ function countMatches(input, pattern) {
|
|
|
285
397
|
}
|
|
286
398
|
return count;
|
|
287
399
|
}
|
|
288
|
-
export async function detectFabricLikeInputNamespace(inputJar) {
|
|
400
|
+
export async function detectFabricLikeInputNamespace(inputJar, deps = {}) {
|
|
289
401
|
const warnings = [];
|
|
290
402
|
const classEntries = (await listJarEntries(inputJar))
|
|
291
403
|
.filter((entry) => entry.endsWith(".class"))
|
|
@@ -297,16 +409,15 @@ export async function detectFabricLikeInputNamespace(inputJar) {
|
|
|
297
409
|
warnings
|
|
298
410
|
};
|
|
299
411
|
}
|
|
412
|
+
// Read all sampled class entries in a SINGLE jar open instead of re-opening the
|
|
413
|
+
// jar once per entry. The sample set and latin1 decode are unchanged, so scores —
|
|
414
|
+
// and therefore the detected namespace — are identical.
|
|
415
|
+
const sampleSet = new Set(classEntries);
|
|
416
|
+
const matched = await collectMatchedJarEntriesAsBuffers(inputJar, (name) => sampleSet.has(name), { continueOnError: true }, deps);
|
|
300
417
|
let mojangScore = 0;
|
|
301
418
|
let intermediaryScore = 0;
|
|
302
|
-
for (const
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
text = (await readJarEntryAsBuffer(inputJar, entry)).toString("latin1");
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
419
|
+
for (const { content } of matched) {
|
|
420
|
+
const text = content.toString("latin1");
|
|
310
421
|
mojangScore += countMatches(text, /net\/minecraft\/(?:advancements|client|commands|core|data|gametest|nbt|network|recipe|resources|server|sounds|stats|tags|util|world)\//g) * 3;
|
|
311
422
|
intermediaryScore += countMatches(text, /net\/minecraft\/class_\d+/g) * 3;
|
|
312
423
|
intermediaryScore += countMatches(text, /\b(?:method|field)_\d+\b/g);
|
package/dist/source-service.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as indexer from "./source/indexer.js";
|
|
|
11
11
|
import * as workspaceTarget from "./source/workspace-target.js";
|
|
12
12
|
import * as validateMixinModule from "./source/validate-mixin.js";
|
|
13
13
|
import * as artifactResolver from "./source/artifact-resolver.js";
|
|
14
|
+
import type { WireMembersBlock } from "./source/class-source/members-builder.js";
|
|
14
15
|
import { type StageEmitter } from "./stage-emitter.js";
|
|
15
16
|
import { WorkspaceMappingService, type WorkspaceCompileMappingOutput } from "./workspace-mapping-service.js";
|
|
16
17
|
import { type WorkspaceContextCache } from "./workspace-context-cache.js";
|
|
@@ -30,7 +31,8 @@ export type ResolveArtifactInput = {
|
|
|
30
31
|
scope?: ArtifactScope;
|
|
31
32
|
preferProjectVersion?: boolean;
|
|
32
33
|
strictVersion?: boolean;
|
|
33
|
-
|
|
34
|
+
/** When true, compute the optional `sampleEntries` listing (detail=full / include=samples). */
|
|
35
|
+
includeSampleEntries?: boolean;
|
|
34
36
|
};
|
|
35
37
|
export type ResolveArtifactOutput = {
|
|
36
38
|
artifactId: string;
|
|
@@ -112,6 +114,8 @@ export type SearchClassSourceInput = {
|
|
|
112
114
|
export type SearchClassSourceOutput = {
|
|
113
115
|
hits: SearchSourceHit[];
|
|
114
116
|
nextCursor?: string;
|
|
117
|
+
/** True when a provided cursor was malformed or belonged to a different query and was ignored (results restart from page one). */
|
|
118
|
+
cursorIgnored?: boolean;
|
|
115
119
|
mappingApplied: SourceMapping;
|
|
116
120
|
returnedNamespace: SourceMapping;
|
|
117
121
|
artifactContents: ArtifactContentsSummary;
|
|
@@ -205,6 +209,10 @@ export type GetClassSourceOutput = {
|
|
|
205
209
|
};
|
|
206
210
|
truncated: boolean;
|
|
207
211
|
charsTruncated?: boolean;
|
|
212
|
+
/** True when startLine begins past EOF; the returned range is empty (not clamped). */
|
|
213
|
+
outOfRange?: boolean;
|
|
214
|
+
/** First not-yet-returned line when truncated; pass back as startLine to continue. */
|
|
215
|
+
nextStartLine?: number;
|
|
208
216
|
origin: ResolvedSourceArtifact["origin"];
|
|
209
217
|
artifactId: string;
|
|
210
218
|
requestedMapping: SourceMapping;
|
|
@@ -213,6 +221,11 @@ export type GetClassSourceOutput = {
|
|
|
213
221
|
provenance: ArtifactProvenance;
|
|
214
222
|
qualityFlags: string[];
|
|
215
223
|
artifactContents: ArtifactContentsSummary;
|
|
224
|
+
/** Replayable get-class-source call that reads from nextStartLine (truncated, non-file output only). */
|
|
225
|
+
suggestedCall?: {
|
|
226
|
+
tool: string;
|
|
227
|
+
params: Record<string, unknown>;
|
|
228
|
+
};
|
|
216
229
|
outputFile?: string;
|
|
217
230
|
warnings: string[];
|
|
218
231
|
};
|
|
@@ -245,11 +258,14 @@ export type GetClassMembersInput = {
|
|
|
245
258
|
includeInherited?: boolean;
|
|
246
259
|
memberPattern?: string;
|
|
247
260
|
maxMembers?: number;
|
|
261
|
+
cursor?: string;
|
|
248
262
|
projectPath?: string;
|
|
249
263
|
gradleUserHome?: string;
|
|
250
264
|
scope?: ArtifactScope;
|
|
251
265
|
preferProjectVersion?: boolean;
|
|
252
266
|
strictVersion?: boolean;
|
|
267
|
+
/** When true, keep jvmDescriptor on FIELD members (always kept on methods/constructors). */
|
|
268
|
+
includeDescriptors?: boolean;
|
|
253
269
|
};
|
|
254
270
|
export type DecompiledMember = {
|
|
255
271
|
name: string;
|
|
@@ -265,11 +281,7 @@ export type DecompiledFallback = {
|
|
|
265
281
|
export type GetClassMembersStatus = "ok" | "members_unavailable" | "partial";
|
|
266
282
|
export type GetClassMembersOutput = {
|
|
267
283
|
className: string;
|
|
268
|
-
members:
|
|
269
|
-
constructors: SignatureMember[];
|
|
270
|
-
fields: SignatureMember[];
|
|
271
|
-
methods: SignatureMember[];
|
|
272
|
-
};
|
|
284
|
+
members: WireMembersBlock;
|
|
273
285
|
counts: {
|
|
274
286
|
constructors: number;
|
|
275
287
|
fields: number;
|
|
@@ -277,6 +289,10 @@ export type GetClassMembersOutput = {
|
|
|
277
289
|
total: number;
|
|
278
290
|
};
|
|
279
291
|
truncated: boolean;
|
|
292
|
+
/** Continuation cursor when more members remain; pass back as cursor to fetch the next page. */
|
|
293
|
+
nextCursor?: string;
|
|
294
|
+
/** True when a provided cursor was malformed or belonged to a different query and was ignored. */
|
|
295
|
+
cursorIgnored?: boolean;
|
|
280
296
|
context: ExplorerResponseContext;
|
|
281
297
|
origin: ResolvedSourceArtifact["origin"];
|
|
282
298
|
artifactId: string;
|
|
@@ -420,7 +436,12 @@ export type ValidateMixinInput = {
|
|
|
420
436
|
path: string;
|
|
421
437
|
};
|
|
422
438
|
sourceRoots?: string[];
|
|
423
|
-
|
|
439
|
+
/**
|
|
440
|
+
* Optional when input.mode='project', or preferProjectVersion=true with a
|
|
441
|
+
* projectPath set: the dispatcher detects the Minecraft version from
|
|
442
|
+
* gradle.properties before any version-dependent stage runs.
|
|
443
|
+
*/
|
|
444
|
+
version?: string;
|
|
424
445
|
mapping?: SourceMapping;
|
|
425
446
|
sourcePriority?: MappingSourcePriority;
|
|
426
447
|
scope?: ArtifactScope;
|