fix(readers): preserve completed snapshots through cleanup retries

Retain exact completed file path, digest and length in Python pyc and Erlang BEAM failure observations. Translate Python close failures at the provider boundary and bind them to the retained descriptor cleanup leases so later successful retries resolve prior errors.

Exercise actual services and file handles with completed reads, cancellation during finalization and physical read failure followed by cancellation. Assert no fabricated snapshot on early failure and resolution of every prior cleanup report.

Validation: six focused descriptor regressions plus existing filesystem, parser and MCP coverage passed; build and static checks passed. Authored format fixtures establish portable reader behavior, not CPython/Erlang runtime execution.
This commit is contained in:
morluto
2026-10-27 01:01:19 +08:00
parent 74d5cab372
commit 9d2c087d83
3 changed files with 202 additions and 4 deletions
+33 -3
View File
@@ -6,9 +6,13 @@ import {
bindArtifactFileCleanup,
} from "../artifacts/ArtifactFileOperation.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import {
completedStableArtifact,
readStableArtifact,
} from "../artifacts/readStableArtifact.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { fileSnapshotPartialObservationSchema } from "../domain/fileSnapshotObservation.js";
import type { PythonPycPort } from "../application/python/PythonPycPort.js";
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
@@ -164,6 +168,16 @@ const failureForPrimary = (
const failureFor = (cause: unknown, input: InspectPythonPycInput) => {
const primary = failureForPrimary(artifactFilePrimaryFacts(cause), input);
const snapshot = completedStableArtifact(cause);
if (snapshot !== undefined)
primary.retainPartialObservation(
fileSnapshotPartialObservationSchema.parse({
kind: "file-snapshot-hash",
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
}),
);
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
return primary;
const cleanup = new ProviderCleanupError(
@@ -182,8 +196,24 @@ export class PythonPycProvider implements PythonPycPort {
readonly identity = PYTHON_PYC_PROVIDER_IDENTITY;
readonly #resources = new ArtifactResourceScope();
close(): Promise<void> {
return this.#resources.close();
async close(): Promise<void> {
try {
await this.#resources.close();
} catch (cause: unknown) {
if (
!(cause instanceof ArtifactReaderFailure) ||
cause.cleanup === undefined
)
throw cause;
const error = new ProviderCleanupError(
PYTHON_PYC_PROVIDER_IDENTITY.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ cause, operation: OPERATION },
);
bindArtifactFileCleanup(cause, error);
throw error;
}
}
async inspect(input: InspectPythonPycInput, options?: ExecutionOptions) {