@adhisang/minecraft-modding-mcp 7.0.0-rc.2 → 7.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 +50 -3
- package/README.md +1 -1
- package/dist/artifact-identity.d.ts +130 -0
- package/dist/artifact-identity.js +142 -0
- package/dist/cache-registry.d.ts +16 -0
- package/dist/cache-registry.js +78 -10
- package/dist/config.js +2 -2
- package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
- package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
- package/dist/json-rpc-framing.d.ts +20 -0
- package/dist/json-rpc-framing.js +155 -23
- package/dist/mapping/loaders/tiny-maven.d.ts +9 -0
- package/dist/mapping/loaders/tiny-maven.js +10 -2
- package/dist/minecraft-explorer-service.js +2 -2
- package/dist/path-resolver.d.ts +13 -2
- package/dist/path-resolver.js +12 -1
- package/dist/repo-downloader.d.ts +19 -7
- package/dist/repo-downloader.js +123 -3
- package/dist/source/artifact-resolver.js +14 -6
- package/dist/source/class-source.js +56 -1
- package/dist/source-resolver.d.ts +0 -1
- package/dist/source-resolver.js +25 -94
- package/dist/stdio-supervisor.d.ts +150 -4
- package/dist/stdio-supervisor.js +710 -68
- package/dist/storage/db.js +5 -1
- package/dist/tool-schemas.d.ts +55 -55
- package/dist/types.d.ts +18 -1
- package/docs/tool-reference.md +24 -12
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fastGlob from "fast-glob";
|
|
2
|
+
import { DECOMPILE_SIGNATURE_QUALIFIER, jarArtifactIdentity } from "../artifact-identity.js";
|
|
2
3
|
import { buildArtifactAlias } from "../config.js";
|
|
3
4
|
import { buildSuggestedCall } from "../build-suggested-call.js";
|
|
4
5
|
import { ERROR_CODES, createError, isAppError } from "../errors.js";
|
|
@@ -7,9 +8,8 @@ import { log } from "../logger.js";
|
|
|
7
8
|
import { applyMappingPipeline } from "../mapping-pipeline-service.js";
|
|
8
9
|
import { parseCoordinate } from "../maven-resolver.js";
|
|
9
10
|
import { resolveMojangTinyFile } from "../mojang-tiny-mapping-service.js";
|
|
10
|
-
import { artifactSignatureFromFile } from "../path-resolver.js";
|
|
11
11
|
import { detectFabricLikeInputNamespace, listJavaEntries } from "../source-jar-reader.js";
|
|
12
|
-
import {
|
|
12
|
+
import { resolveSourceTarget as resolveSourceTargetInternal } from "../source-resolver.js";
|
|
13
13
|
import { resolveTinyRemapperJar } from "../tiny-remapper-resolver.js";
|
|
14
14
|
import { isUnobfuscatedVersion } from "../version-service.js";
|
|
15
15
|
import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
|
|
@@ -562,8 +562,10 @@ export async function probeMinecraftArtifact(svc, input) {
|
|
|
562
562
|
});
|
|
563
563
|
}
|
|
564
564
|
const selectedSourceJarPath = versionSourceDiscovery.selectedSourceJarPath;
|
|
565
|
-
|
|
566
|
-
|
|
565
|
+
// The same helper `resolveSourceTarget`'s jar branch uses, so the id this
|
|
566
|
+
// probe publishes and the id a full resolve mints for the same jar are
|
|
567
|
+
// derived and composed by one piece of code.
|
|
568
|
+
const { artifactId } = await jarArtifactIdentity(selectedSourceJarPath);
|
|
567
569
|
warnings.push(`Resolved source-backed artifact from Loom cache candidate: ${selectedSourceJarPath}.`);
|
|
568
570
|
if (versionSourceDiscovery.selectedHasMinecraftNamespace === false) {
|
|
569
571
|
warnings.push(`Source coverage does not include net.minecraft for ${selectedSourceJarPath}; class lookups may fall back to the binary artifact.`);
|
|
@@ -577,8 +579,14 @@ export async function probeMinecraftArtifact(svc, input) {
|
|
|
577
579
|
...(warnings.length > 0 ? { warnings } : {})
|
|
578
580
|
};
|
|
579
581
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
+
// "Lightweight" describes what this probe SKIPS: it neither decompiles nor
|
|
583
|
+
// rebuilds the index. It does read the version jar's bytes, because that is
|
|
584
|
+
// what the jar route derives an id from, so the first probe of a given jar in
|
|
585
|
+
// a process pays for one hash of it; the digest memo in
|
|
586
|
+
// `src/artifact-identity.ts` covers the ones after that.
|
|
587
|
+
const { artifactId } = await jarArtifactIdentity(versionJar.jarPath, {
|
|
588
|
+
signatureQualifier: DECOMPILE_SIGNATURE_QUALIFIER
|
|
589
|
+
});
|
|
582
590
|
return {
|
|
583
591
|
artifactId,
|
|
584
592
|
mappingApplied: effectiveMapping,
|
|
@@ -9,6 +9,7 @@ import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire, pro
|
|
|
9
9
|
import { collectDidYouMeanCandidates } from "./did-you-mean.js";
|
|
10
10
|
import { matchesMemberPattern } from "./member-pattern.js";
|
|
11
11
|
import { findNestedJarClasses, resolveUniqueNestedJarForClass } from "./nested-jars.js";
|
|
12
|
+
import { extractSymbolsFromSource } from "../symbols/symbol-extractor.js";
|
|
12
13
|
import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
|
|
13
14
|
import { dedupeQualityFlags, inheritArtifactMapping, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
|
|
14
15
|
import { isUnobfuscatedVersion } from "../version-service.js";
|
|
@@ -407,6 +408,50 @@ function projectDecompiledFallback(fallback, level) {
|
|
|
407
408
|
// The class-like symbol kinds findClass returns. MUST stay in sync with the JS-side
|
|
408
409
|
// isTypeSymbol checks below; pushed down to SQL so non-type rows are never fetched.
|
|
409
410
|
const TYPE_SYMBOL_KINDS = ["class", "interface", "enum", "record"];
|
|
411
|
+
/**
|
|
412
|
+
* Reconstruct the full nesting chain between a file's top-level type and a
|
|
413
|
+
* type declared two or more levels deeper inside it. The extractor stores
|
|
414
|
+
* only ONE qualifiedName per FILE (the top-level type), so naively
|
|
415
|
+
* concatenating `<topLevelFQN>.<symbolName>` is only correct for exactly one
|
|
416
|
+
* level of nesting — it silently drops every intermediate enclosing type.
|
|
417
|
+
*
|
|
418
|
+
* Walks the file's own type declarations from the top-level type downward,
|
|
419
|
+
* using brace-range containment to find which declaration directly encloses
|
|
420
|
+
* `targetLine` at each level, collecting each intermediate simple name.
|
|
421
|
+
*
|
|
422
|
+
* Returns the intermediate simple names (outermost first, target excluded)
|
|
423
|
+
* or undefined when the chain cannot be reconstructed — e.g. the file's
|
|
424
|
+
* content is unavailable — in which case the caller falls back to the
|
|
425
|
+
* single-level formula.
|
|
426
|
+
*/
|
|
427
|
+
function resolveNestedTypeChain(svc, artifactId, filePath, topSimpleName, targetLine) {
|
|
428
|
+
const fileRow = svc.filesRepo.getFileContent(artifactId, filePath);
|
|
429
|
+
if (!fileRow)
|
|
430
|
+
return undefined;
|
|
431
|
+
const lines = fileRow.content.split(/\r?\n/);
|
|
432
|
+
const fileSymbols = extractSymbolsFromSource(filePath, fileRow.content);
|
|
433
|
+
let currentBody = classSourceHelpers.computeBraceRange(lines, fileSymbols, topSimpleName);
|
|
434
|
+
if (!currentBody)
|
|
435
|
+
return undefined;
|
|
436
|
+
const chain = [];
|
|
437
|
+
// Bounded by the number of type symbols in the file; guards against ever
|
|
438
|
+
// looping on a malformed structure.
|
|
439
|
+
for (let step = 0; step <= fileSymbols.length; step += 1) {
|
|
440
|
+
const childRanges = classSourceHelpers.computeNestedTypeRanges(lines, fileSymbols, currentBody);
|
|
441
|
+
if (childRanges.some((range) => range.declarationLine === targetLine)) {
|
|
442
|
+
return chain;
|
|
443
|
+
}
|
|
444
|
+
const containingChild = childRanges.find((range) => range.declarationLine < targetLine && targetLine <= range.endLine);
|
|
445
|
+
if (!containingChild)
|
|
446
|
+
return undefined;
|
|
447
|
+
const childSymbol = fileSymbols.find((symbol) => symbol.line === containingChild.declarationLine);
|
|
448
|
+
if (!childSymbol)
|
|
449
|
+
return undefined;
|
|
450
|
+
chain.push(childSymbol.symbolName);
|
|
451
|
+
currentBody = containingChild;
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
410
455
|
export function findClass(svc, input) {
|
|
411
456
|
const className = input.className.trim();
|
|
412
457
|
if (!className) {
|
|
@@ -500,8 +545,18 @@ export function findClass(svc, input) {
|
|
|
500
545
|
const enclosingQualifiedName = row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
501
546
|
const enclosingSimpleName = enclosingQualifiedName.split(".").at(-1) ?? enclosingQualifiedName;
|
|
502
547
|
const nested = enclosingSimpleName !== row.symbolName;
|
|
548
|
+
let qualifiedName = nested ? `${enclosingQualifiedName}.${row.symbolName}` : enclosingQualifiedName;
|
|
549
|
+
if (nested) {
|
|
550
|
+
// The formula above assumes exactly one level of nesting. When the type
|
|
551
|
+
// is nested two or more levels deep, reconstruct the real chain so the
|
|
552
|
+
// intermediate enclosing type(s) are not silently dropped.
|
|
553
|
+
const intermediateChain = resolveNestedTypeChain(svc, artifactId, row.filePath, enclosingSimpleName, row.line);
|
|
554
|
+
if (intermediateChain && intermediateChain.length > 0) {
|
|
555
|
+
qualifiedName = `${enclosingQualifiedName}.${[...intermediateChain, row.symbolName].join(".")}`;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
503
558
|
candidates.push({
|
|
504
|
-
qualifiedName
|
|
559
|
+
qualifiedName,
|
|
505
560
|
filePath: row.filePath,
|
|
506
561
|
line: row.line,
|
|
507
562
|
symbolKind: row.symbolKind,
|
|
@@ -48,7 +48,6 @@ export declare function localM2CoordinateCandidatePaths(localM2Path: string, par
|
|
|
48
48
|
*/
|
|
49
49
|
export declare const BINARY_JAR_NO_CLASSES_FLAG = "binary-jar-no-classes";
|
|
50
50
|
export type { MappingVariant } from "./types.js";
|
|
51
|
-
export declare function artifactIdForJar(inputKind: string, artifactPath: string, signature: string, suffix?: string, mappingVariant?: MappingVariant): string;
|
|
52
51
|
export interface ResolveSourceTargetOptions {
|
|
53
52
|
allowDecompile: boolean;
|
|
54
53
|
preferBinaryOnly?: boolean;
|
package/dist/source-resolver.js
CHANGED
|
@@ -1,78 +1,13 @@
|
|
|
1
|
-
import { statSync } from "node:fs";
|
|
2
1
|
import { readdir } from "node:fs/promises";
|
|
3
2
|
import { basename, dirname, join, resolve as resolvePath, sep } from "node:path";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
4
|
import fastGlob from "fast-glob";
|
|
6
5
|
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
7
6
|
import { buildRemoteBinaryUrls, buildRemoteSourceUrls, groupToPath, hasExistingJar, isMutableMavenCoordinate, parseCoordinate, normalizedCoordinateValue } from "./maven-resolver.js";
|
|
8
|
-
import { defaultDownloadPath,
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
7
|
+
import { defaultDownloadPath, discardCachedDownload, resolveCachedDownload } from "./repo-downloader.js";
|
|
8
|
+
import { normalizeJarPath } from "./path-resolver.js";
|
|
9
|
+
import { composeArtifactId, contentDigestSignature, contentSignature, jarArtifactIdentity, DECOMPILE_SIGNATURE_QUALIFIER } from "./artifact-identity.js";
|
|
11
10
|
import { hasAnyJarEntry, hasJavaSourceExtension } from "./source-jar-reader.js";
|
|
12
|
-
function readStatsSignature(filePath) {
|
|
13
|
-
const stats = artifactSignatureFromFile(filePath);
|
|
14
|
-
return stats.signature;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Digests already derived from a local jar, each pinned to the stat that
|
|
18
|
-
* produced it.
|
|
19
|
-
*
|
|
20
|
-
* Keyed by the symlink-resolved path, so a jar reached through two names is
|
|
21
|
-
* hashed once. The entry is only ever *reused*, never trusted on its own: a
|
|
22
|
-
* mismatched mtime or size discards it, so the map cannot serve a digest for
|
|
23
|
-
* bytes that have since been replaced.
|
|
24
|
-
*
|
|
25
|
-
* Bounded, like the helper caches in `src/source/artifact-resolver.ts`: this map
|
|
26
|
-
* is module-level and lives as long as the process, and a long-running server
|
|
27
|
-
* walks this cascade once per target-driven tool call, so an unbounded map grows
|
|
28
|
-
* with every distinct jar path the server has ever seen. Eviction costs at most
|
|
29
|
-
* one re-hash, which is exactly what a cache miss already costs.
|
|
30
|
-
*/
|
|
31
|
-
const contentSignatureCache = new Map();
|
|
32
|
-
const MAX_CONTENT_SIGNATURE_CACHE = 512;
|
|
33
|
-
/** Insert, dropping the oldest key first when the bound is reached. */
|
|
34
|
-
function rememberContentSignature(resolvedPath, entry) {
|
|
35
|
-
if (!contentSignatureCache.has(resolvedPath) && contentSignatureCache.size >= MAX_CONTENT_SIGNATURE_CACHE) {
|
|
36
|
-
const oldestKey = contentSignatureCache.keys().next().value;
|
|
37
|
-
if (oldestKey) {
|
|
38
|
-
contentSignatureCache.delete(oldestKey);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
contentSignatureCache.set(resolvedPath, entry);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* The identity of a jar sitting on local disk: a sha256 of its bytes.
|
|
45
|
-
*
|
|
46
|
-
* `~/.m2` and the Gradle module cache move a file's mtime for reasons that have
|
|
47
|
-
* nothing to do with its contents - an eviction followed by a re-fetch of
|
|
48
|
-
* byte-identical bytes, a filesystem restore, a plain `touch`. An `mtimeMs:size`
|
|
49
|
-
* signature turns every one of those into a fresh artifactId and a fresh
|
|
50
|
-
* decompile, which is exactly the instability the download cache's
|
|
51
|
-
* content-addressed identity removed from the remote half of this cascade.
|
|
52
|
-
*
|
|
53
|
-
* Hashing is not free and this cascade is re-walked on every target-driven tool
|
|
54
|
-
* call, so the digest is memoized against the stat that produced it. The stat is
|
|
55
|
-
* taken *before* the digest on purpose: bytes replaced mid-hash are recorded
|
|
56
|
-
* against a stat they no longer have, so the entry is rejected on the next call
|
|
57
|
-
* and re-derived - a wasted hash, never a wrong identity.
|
|
58
|
-
*/
|
|
59
|
-
async function contentSignature(jarPath) {
|
|
60
|
-
// The same normalization `artifactSignatureFromFile` applied, kept so this
|
|
61
|
-
// path still refuses a vanished or non-jar file the way it always has.
|
|
62
|
-
const resolvedPath = normalizeJarPath(jarPath);
|
|
63
|
-
const stats = statSync(resolvedPath);
|
|
64
|
-
const cached = contentSignatureCache.get(resolvedPath);
|
|
65
|
-
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
66
|
-
return cached.sha256;
|
|
67
|
-
}
|
|
68
|
-
const { contentSha256 } = await digestFile(resolvedPath);
|
|
69
|
-
rememberContentSignature(resolvedPath, {
|
|
70
|
-
mtimeMs: stats.mtimeMs,
|
|
71
|
-
size: stats.size,
|
|
72
|
-
sha256: contentSha256
|
|
73
|
-
});
|
|
74
|
-
return contentSha256;
|
|
75
|
-
}
|
|
76
11
|
/**
|
|
77
12
|
* Whether a jar contains java sources, with archive errors deliberately
|
|
78
13
|
* propagating.
|
|
@@ -445,28 +380,20 @@ async function resolveGradleCacheCoordinateCandidate(coordinate) {
|
|
|
445
380
|
function resolveRemoteBinaryCandidate(coordinate, repos) {
|
|
446
381
|
return buildRemoteBinaryUrls(repos, coordinate);
|
|
447
382
|
}
|
|
448
|
-
export function artifactIdForJar(inputKind, artifactPath, signature, suffix, mappingVariant = "pass") {
|
|
449
|
-
const parts = [inputKind, artifactPath, signature, suffix ?? "source"];
|
|
450
|
-
if (mappingVariant === "mojang-remapped") {
|
|
451
|
-
parts.push("mojang-remapped");
|
|
452
|
-
}
|
|
453
|
-
return stableArtifactId(parts);
|
|
454
|
-
}
|
|
455
|
-
function artifactIdForCoordinate(coordinate, source, signature, mappingVariant = "pass") {
|
|
456
|
-
const parts = ["coord", coordinate, source, signature];
|
|
457
|
-
if (mappingVariant === "mojang-remapped") {
|
|
458
|
-
parts.push("mojang-remapped");
|
|
459
|
-
}
|
|
460
|
-
return stableArtifactId(parts);
|
|
461
|
-
}
|
|
462
383
|
function resolvedAtNow() {
|
|
463
384
|
return new Date().toISOString();
|
|
464
385
|
}
|
|
465
386
|
/** Shared shape for every artifact the coordinate cascade can return. */
|
|
466
387
|
function coordinateArtifact(spec) {
|
|
467
388
|
return {
|
|
468
|
-
artifactId:
|
|
469
|
-
|
|
389
|
+
artifactId: composeArtifactId({
|
|
390
|
+
space: "coordinate",
|
|
391
|
+
coordinate: spec.coordinate,
|
|
392
|
+
idSource: spec.idSource,
|
|
393
|
+
signature: spec.signature,
|
|
394
|
+
mappingVariant: spec.mappingVariant
|
|
395
|
+
}),
|
|
396
|
+
artifactSignature: spec.signature.value,
|
|
470
397
|
origin: spec.origin,
|
|
471
398
|
sourceJarPath: spec.sourceJarPath,
|
|
472
399
|
binaryJarPath: spec.binaryJarPath,
|
|
@@ -595,7 +522,6 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
595
522
|
};
|
|
596
523
|
if (input.kind === "jar") {
|
|
597
524
|
const resolvedJarPath = normalizeJarPath(input.value);
|
|
598
|
-
const binarySignature = readStatsSignature(resolvedJarPath);
|
|
599
525
|
const exactSourceJarPath = resolveExactJarSourceCandidate(resolvedJarPath);
|
|
600
526
|
const adjacentSourceCandidates = await listAdjacentJarSourceCandidates(resolvedJarPath);
|
|
601
527
|
const maybeAdjacentSourceCandidates = adjacentSourceCandidates.length > 0 ? adjacentSourceCandidates : undefined;
|
|
@@ -604,9 +530,10 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
604
530
|
const siblingBinaryJarPath = resolveSiblingBinaryJarCandidate(resolvedJarPath);
|
|
605
531
|
const binaryJarPath = siblingBinaryJarPath ??
|
|
606
532
|
(basename(resolvedJarPath).endsWith("-sources.jar") ? undefined : resolvedJarPath);
|
|
533
|
+
const identity = await jarArtifactIdentity(resolvedJarPath);
|
|
607
534
|
return {
|
|
608
|
-
artifactId:
|
|
609
|
-
artifactSignature:
|
|
535
|
+
artifactId: identity.artifactId,
|
|
536
|
+
artifactSignature: identity.signature,
|
|
610
537
|
origin: "local-jar",
|
|
611
538
|
binaryJarPath,
|
|
612
539
|
sourceJarPath: resolvedJarPath,
|
|
@@ -616,10 +543,10 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
616
543
|
};
|
|
617
544
|
}
|
|
618
545
|
if (!preferBinaryOnly && await candidateHasJavaSources(exactSourceJarPath)) {
|
|
619
|
-
const
|
|
546
|
+
const identity = await jarArtifactIdentity(exactSourceJarPath);
|
|
620
547
|
return {
|
|
621
|
-
artifactId:
|
|
622
|
-
artifactSignature:
|
|
548
|
+
artifactId: identity.artifactId,
|
|
549
|
+
artifactSignature: identity.signature,
|
|
623
550
|
origin: "local-jar",
|
|
624
551
|
binaryJarPath: resolvedJarPath,
|
|
625
552
|
sourceJarPath: exactSourceJarPath,
|
|
@@ -648,9 +575,13 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
648
575
|
// no `.java` entries.
|
|
649
576
|
const subjectArchive = await inspectSubjectJarArchive(resolvedJarPath);
|
|
650
577
|
const subjectQualityFlags = subjectArchive ? binaryJarQualityFlags(subjectArchive) : [];
|
|
578
|
+
const identity = await jarArtifactIdentity(resolvedJarPath, {
|
|
579
|
+
signatureQualifier: DECOMPILE_SIGNATURE_QUALIFIER,
|
|
580
|
+
mappingVariant: options.mappingVariant
|
|
581
|
+
});
|
|
651
582
|
return {
|
|
652
|
-
artifactId:
|
|
653
|
-
artifactSignature:
|
|
583
|
+
artifactId: identity.artifactId,
|
|
584
|
+
artifactSignature: identity.signature,
|
|
654
585
|
origin: "decompiled",
|
|
655
586
|
binaryJarPath: resolvedJarPath,
|
|
656
587
|
adjacentSourceCandidates: maybeAdjacentSourceCandidates,
|
|
@@ -843,7 +774,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
843
774
|
// Identity follows the bytes. The HTTP validators that used to form this
|
|
844
775
|
// signature rotate whenever a CDN or repository migration happens, even
|
|
845
776
|
// when the jar is byte-identical.
|
|
846
|
-
const signature = download.contentSha256;
|
|
777
|
+
const signature = contentDigestSignature(download.contentSha256);
|
|
847
778
|
return coordinateArtifact({
|
|
848
779
|
coordinate,
|
|
849
780
|
idSource: "remote-repo",
|
|
@@ -983,7 +914,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
983
914
|
}
|
|
984
915
|
// A file in the URL-keyed download cache is identified by its bytes, not
|
|
985
916
|
// by a stat signature that a re-download would change for free.
|
|
986
|
-
const signature = downloaded.contentSha256;
|
|
917
|
+
const signature = contentDigestSignature(downloaded.contentSha256);
|
|
987
918
|
return coordinateArtifact({
|
|
988
919
|
coordinate,
|
|
989
920
|
idSource: "decompiled",
|
|
@@ -238,6 +238,22 @@ export declare class StdioSupervisor {
|
|
|
238
238
|
*/
|
|
239
239
|
private readonly syntheticTombstones;
|
|
240
240
|
private child;
|
|
241
|
+
/**
|
|
242
|
+
* The one response `handleWorkerMessage` is part-way through settling.
|
|
243
|
+
*
|
|
244
|
+
* It removes a response's pending entry BEFORE writing the reply, so between
|
|
245
|
+
* those two statements the id is owed a response that NOTHING records: the
|
|
246
|
+
* entry is gone, no deadline is armed for anything but validate-project, and
|
|
247
|
+
* the worker considers the request answered. A fault in that window used to
|
|
248
|
+
* be indistinguishable from an id already settled, and the request stayed
|
|
249
|
+
* unanswered for the life of an otherwise healthy session.
|
|
250
|
+
*
|
|
251
|
+
* Set the moment the entry is deleted and cleared the moment the write
|
|
252
|
+
* returns, so it is defined only inside that window and only ever names one
|
|
253
|
+
* request. `answerFaultedWorkerResponse` is the sole reader, and it consumes
|
|
254
|
+
* the marker rather than merely reading it.
|
|
255
|
+
*/
|
|
256
|
+
private settlingWorkerResponse;
|
|
241
257
|
private childReady;
|
|
242
258
|
/**
|
|
243
259
|
* Monotonic timestamp of the current generation's adoption, or undefined
|
|
@@ -338,17 +354,47 @@ export declare class StdioSupervisor {
|
|
|
338
354
|
*/
|
|
339
355
|
private readonly handleFatalError;
|
|
340
356
|
/**
|
|
341
|
-
* Admission entry point, wrapped so a fault
|
|
357
|
+
* Admission entry point, wrapped so a fault cannot silently drop a request.
|
|
342
358
|
*
|
|
343
359
|
* Anything thrown while classifying, queueing or forwarding a client frame
|
|
344
360
|
* propagates out of the frame reader's `onFrame`, where the reader swallows
|
|
345
361
|
* it as a parse error — the request then gets NO reply and the client waits
|
|
346
362
|
* on that id forever (and on a modern-era request the one-way era lock has
|
|
347
|
-
* already happened).
|
|
348
|
-
*
|
|
349
|
-
*
|
|
363
|
+
* already happened). The catch below answers the id with -32603 instead.
|
|
364
|
+
*
|
|
365
|
+
* The reply is conditional, and on exactly one thing: that nothing this
|
|
366
|
+
* admission installed is still live at the id. A surviving instance is
|
|
367
|
+
* already tracked and will be settled by an ordinary path, so answering
|
|
368
|
+
* alongside it would make two terminal replies for one request; the catch
|
|
369
|
+
* reports and stands down in that case rather than answering. Id-less frames
|
|
370
|
+
* (notifications, malformed ids) have nothing to answer and are only logged.
|
|
350
371
|
*/
|
|
351
372
|
private handleClientMessage;
|
|
373
|
+
/**
|
|
374
|
+
* Runs one step of a fault-recovery path so that it cannot throw.
|
|
375
|
+
*
|
|
376
|
+
* The two frame-reader callers — the admission catch in handleClientMessage
|
|
377
|
+
* and the worker-frame catch in handleWorkerData — execute inside a
|
|
378
|
+
* {@link JsonRpcFrameReader}'s `onFrame`, where the reader deliberately
|
|
379
|
+
* converts an escaping throw into a plain parse error: the frame is then
|
|
380
|
+
* silently dropped, which for a recovery path means the request it was
|
|
381
|
+
* rescuing is lost after all. `dispatchQueuedRequest` is the third caller,
|
|
382
|
+
* and its exposure is wider rather than narrower: a drain is reached from
|
|
383
|
+
* those two `onFrame` paths, but also from timer callbacks
|
|
384
|
+
* (handleValidateProjectDeadline) and from process-event handlers
|
|
385
|
+
* (handleWorkerExit), where nothing above it is prepared to contain a throw
|
|
386
|
+
* at all.
|
|
387
|
+
*
|
|
388
|
+
* Reporting goes through `log` rather than the injected `eventWriter` so
|
|
389
|
+
* that an INJECTED writer — the collaborator most likely to have thrown — is
|
|
390
|
+
* not also the reporter. That NARROWS the failure; it does not remove it.
|
|
391
|
+
* `eventWriter` defaults to `log` (see the constructor), so in the default
|
|
392
|
+
* configuration, where nothing is injected, the reporter IS the writer that
|
|
393
|
+
* just faulted. The report therefore carries its own try/catch whose handler
|
|
394
|
+
* does nothing: past this point there is no reporting channel left, and a
|
|
395
|
+
* throw here would drop the frame this method exists to save.
|
|
396
|
+
*/
|
|
397
|
+
private runRecoveryStep;
|
|
352
398
|
/**
|
|
353
399
|
* Every request INSTANCE live at `key` right now: the forwarded entry plus any
|
|
354
400
|
* queued ones. Captured before admission runs so a rollback can tell the state
|
|
@@ -374,6 +420,20 @@ export declare class StdioSupervisor {
|
|
|
374
420
|
* Returns whether anything was rolled back.
|
|
375
421
|
*/
|
|
376
422
|
private rollbackFailedAdmission;
|
|
423
|
+
/**
|
|
424
|
+
* Removes every QUEUED instance at `key` that is absent from `preexisting`.
|
|
425
|
+
*
|
|
426
|
+
* Split out of rollbackFailedAdmission so the admission catch's finality
|
|
427
|
+
* check can repeat the removal without re-entering the method that may just
|
|
428
|
+
* have thrown. Returns whether anything was removed; calling it twice is
|
|
429
|
+
* harmless, because the second call finds nothing.
|
|
430
|
+
*/
|
|
431
|
+
private dropQueuedInstances;
|
|
432
|
+
/**
|
|
433
|
+
* Whether any request instance live at `key` is absent from `preexisting` —
|
|
434
|
+
* that is, whether the admission being rolled back still owns its id.
|
|
435
|
+
*/
|
|
436
|
+
private hasInstanceOutside;
|
|
377
437
|
private routeClientMessage;
|
|
378
438
|
/**
|
|
379
439
|
* Era-aware notification dispatch. notifications/cancelled stays
|
|
@@ -499,8 +559,94 @@ export declare class StdioSupervisor {
|
|
|
499
559
|
*/
|
|
500
560
|
private handleValidateProjectDeadline;
|
|
501
561
|
private drainQueue;
|
|
562
|
+
/**
|
|
563
|
+
* Forwards one queued entry, containing a fault in the forward itself.
|
|
564
|
+
*
|
|
565
|
+
* `forwardRequest` installs the pending entry and takes the validate
|
|
566
|
+
* slot/barrier BEFORE it writes to the worker's stdin, so a throw from the
|
|
567
|
+
* encode or the write leaves the request holding an id it never reached the
|
|
568
|
+
* worker on. Unguarded, that request is unreachable: it has been shifted out
|
|
569
|
+
* of the queue, so no later drain can dispatch it, and no worker will answer
|
|
570
|
+
* an id it never saw — and only validate-project arms a deadline, so nothing
|
|
571
|
+
* else settles it either. The one containing guard above this
|
|
572
|
+
* (`runRecoveryStep` around the recovery drain) contains the exception
|
|
573
|
+
* without recovering the request, and stops the drain on top of that,
|
|
574
|
+
* leaving everything behind it parked as well.
|
|
575
|
+
*
|
|
576
|
+
* Both are repaired here: the entry is released — which records the finality
|
|
577
|
+
* tombstone, so a worker that did somehow see the bytes cannot answer over
|
|
578
|
+
* the reply below — terminally answered, and the drain carries on to the
|
|
579
|
+
* next entry. Containment is per-entry because the fault this catches is
|
|
580
|
+
* per-message: an encode failure on one payload, or a stdin `write` that
|
|
581
|
+
* throws. A stdin that is merely destroyed never reaches this catch at all —
|
|
582
|
+
* `forwardRequest` tests for that first and takes its own no-child fallback,
|
|
583
|
+
* which does not throw.
|
|
584
|
+
*
|
|
585
|
+
* The `initialize` carve-out in `releaseForwardedRequest` is honoured: if it
|
|
586
|
+
* declines, nothing is answered here and the handshake lifecycle keeps the
|
|
587
|
+
* entry. Admission parks `initialize` in `queuedNotifications` rather than in
|
|
588
|
+
* `queuedRequests`, so the ordinary path never puts one in front of this
|
|
589
|
+
* drain; `forwardRequest`'s own no-child fallback is the one way an
|
|
590
|
+
* initialize can end up queued, and the carve-out is what covers it.
|
|
591
|
+
*
|
|
592
|
+
* Returns whether the entry reached the worker.
|
|
593
|
+
*/
|
|
594
|
+
private dispatchQueuedRequest;
|
|
502
595
|
private spawnWorker;
|
|
503
596
|
private handleWorkerData;
|
|
597
|
+
/**
|
|
598
|
+
* Rescues the request a faulted worker frame was answering.
|
|
599
|
+
*
|
|
600
|
+
* `handleWorkerMessage` runs inside the worker reader's `onFrame`, where a
|
|
601
|
+
* throw becomes a parse error and the frame is simply dropped. For a
|
|
602
|
+
* RESPONSE frame that left the pending entry live with no reply and nothing
|
|
603
|
+
* left to settle it — deadlines are armed for validate-project only, so any
|
|
604
|
+
* other tool had no rescue at all and the client waited on that id for the
|
|
605
|
+
* life of the session. The id is released and answered here instead.
|
|
606
|
+
*
|
|
607
|
+
* The queue drain is the OTHER half, and it is why the answering half lives
|
|
608
|
+
* in its own method: `handleWorkerMessage` deletes a response's pending
|
|
609
|
+
* entry before the `writeToClient` that can throw, and its `drainQueue()` is
|
|
610
|
+
* the last statement of all. So the commonest fault arrives here with the
|
|
611
|
+
* entry already gone AND the queue undrained, and every early return in the
|
|
612
|
+
* answering path is a case where the drain is the only rescue left.
|
|
613
|
+
*
|
|
614
|
+
* What this recovery is worth, stated exactly. It answers the id when this
|
|
615
|
+
* supervisor is the one entitled to (see `answerFaultedWorkerResponse`), and
|
|
616
|
+
* every id it answers is tombstoned, so the worker's own answer for that id
|
|
617
|
+
* cannot become a second reply. It does NOT make one-response-per-id a
|
|
618
|
+
* property of the whole file: an id whose entry could not be released is
|
|
619
|
+
* deliberately left unanswered here rather than answered twice, the
|
|
620
|
+
* `initialize` carve-out is settled by the handshake lifecycle instead, and
|
|
621
|
+
* tombstone retention is capped at {@link MAX_SYNTHETIC_TOMBSTONES} — past
|
|
622
|
+
* 1024 live tombstones in one worker generation the oldest is evicted, and a
|
|
623
|
+
* very old worker's late answer for an evicted id would pass through.
|
|
624
|
+
*
|
|
625
|
+
* What this deliberately does NOT do: tear the session down or touch framing
|
|
626
|
+
* state. One frame the supervisor could not handle is not evidence that the
|
|
627
|
+
* worker's stream desynchronized (that is `supervisor.worker_framing_fatal`,
|
|
628
|
+
* reported by the reader itself), and the reader's own state already
|
|
629
|
+
* describes the stream correctly. The one exception is an in-flight
|
|
630
|
+
* `initialize`, whose generation cannot finish its handshake once its answer
|
|
631
|
+
* has been lost — see `answerFaultedWorkerResponse`.
|
|
632
|
+
*/
|
|
633
|
+
private recoverFaultedWorkerMessage;
|
|
634
|
+
/**
|
|
635
|
+
* Terminally answers the client request a faulted worker RESPONSE frame was
|
|
636
|
+
* carrying the answer for, if this supervisor is still the one entitled to
|
|
637
|
+
* answer it. Returns without replying otherwise; the caller's drain runs
|
|
638
|
+
* either way.
|
|
639
|
+
*/
|
|
640
|
+
private answerFaultedWorkerResponse;
|
|
641
|
+
/**
|
|
642
|
+
* Answers a request whose pending entry `handleWorkerMessage` had already
|
|
643
|
+
* removed when it faulted, and which therefore never reached the client.
|
|
644
|
+
*
|
|
645
|
+
* Consumed once: the marker is cleared before the reply is attempted, so a
|
|
646
|
+
* fault in the reply cannot leave a stale claim on the id for a later,
|
|
647
|
+
* unrelated fault to act on.
|
|
648
|
+
*/
|
|
649
|
+
private answerUndeliveredWorkerResponse;
|
|
504
650
|
private handleWorkerStdinError;
|
|
505
651
|
/**
|
|
506
652
|
* Reassembles the worker's stderr into lines (the ready marker may be split
|