Files
rea/src/application/EvidenceBundleFiles.ts
T
morluto 949966712e fix(files): preserve physical read failures across late cancellation
Tag physical read outcomes at the owning boundary and preserve opaque caller cancellation reasons through JSON pipelines and reference imports. Share named bounded-read options across consumers and retain failed cleanup ownership for retry.

Closes #5686
2026-10-21 06:14:03 +08:00

152 lines
4.5 KiB
TypeScript

import { absoluteCallerPath } from "../filesystem/callerPath.js";
import {
describeEvidenceBundleFailure,
describeValidationFailure,
parseImmutableEvidenceBundle,
parseEvidenceBundle,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import {
EvidenceFileError,
EvidenceIntegrityError,
} from "../domain/evidenceErrors.js";
import { err, ok, type Result } from "../domain/result.js";
import {
AnalysisCancelledError,
type AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
import {
bufferedJsonParts,
canonicalJsonParts,
} from "../domain/jsonSerialization.js";
import { readJsonFile, writeTextParts } from "./JsonFiles.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
type EvidenceReadFailure =
| EvidenceFileError
| EvidenceIntegrityError
| AnalysisResourceConstraintError
| AnalysisCancelledError;
type EvidenceWriteFailure =
| EvidenceFileError
| EvidenceIntegrityError
| AnalysisCancelledError;
/** Read and validate an evidence bundle at the caller-supplied path. */
export const readEvidenceBundle = (
path: string,
signal?: AbortSignal,
resources?: ArtifactResourceScope,
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> =>
readBundle(path, parseEvidenceBundle, signal, resources);
/** Read an authenticated immutable snapshot for a subsequent session import. */
export const readImmutableEvidenceBundle = (
path: string,
signal?: AbortSignal,
resources?: ArtifactResourceScope,
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> =>
readBundle(path, parseImmutableEvidenceBundle, signal, resources);
const readBundle = async (
path: string,
parseBundle: (input: unknown) => EvidenceBundle,
signal?: AbortSignal,
resources?: ArtifactResourceScope,
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> => {
if (signal?.aborted)
return err(
new AnalysisCancelledError("import_evidence_bundle", {
cause: signal.reason,
}),
);
const loaded = await readJsonFile(path, signal, resources);
if (!loaded.ok) {
if (loaded.error instanceof AnalysisCancelledError)
return err(
new AnalysisCancelledError("import_evidence_bundle", {
cause: loaded.error.cause,
}),
);
return loaded;
}
if (signal?.aborted)
return err(
new AnalysisCancelledError("import_evidence_bundle", {
cause: signal.reason,
}),
);
let bundle: EvidenceBundle;
try {
bundle = parseBundle(loaded.value);
} catch (cause: unknown) {
const failure = describeEvidenceBundleFailure(loaded.value, cause);
if (failure.kind === "single-record")
return err(
new EvidenceFileError("read", "wrong-kind", {
path: absoluteCallerPath(path),
cause,
userMessage: failure.message,
}),
);
return err(
new EvidenceIntegrityError("Evidence bundle validation failed", {
cause,
userMessage: failure.message,
}),
);
}
for (const record of bundle.records) {
if (record.predicate_type !== "rea.process-capture") continue;
try {
parseProcessCapture(record.normalized_result);
} catch (cause: unknown) {
return err(
new EvidenceIntegrityError("Evidence bundle validation failed", {
cause,
userMessage: `Process capture record ${record.evidence_id} has an invalid normalized_result (${describeValidationFailure(cause)}). Recreate or re-export the bundle, then try again.`,
}),
);
}
}
return ok(bundle);
};
/** Atomically write deterministic evidence JSON at the caller-supplied path. */
export const writeEvidenceBundle = async (
bundle: EvidenceBundle,
path: string,
overwrite: boolean,
resources: ArtifactResourceScope,
signal?: AbortSignal,
): Promise<
Result<
{ readonly path: string; readonly bytes: number },
EvidenceWriteFailure
>
> => {
if (signal?.aborted === true)
return err(new AnalysisCancelledError("export_evidence_bundle"));
let checked: EvidenceBundle;
try {
checked = parseEvidenceBundle(bundle);
} catch (cause: unknown) {
return err(
new EvidenceIntegrityError("Evidence bundle validation failed", {
cause,
}),
);
}
return writeTextParts(
bufferedJsonParts(canonicalJsonParts(checked)),
path,
overwrite,
resources,
signal === undefined
? undefined
: { signal, operation: "export_evidence_bundle" },
);
};