Files
reverse-engineering-anything/src/application/EvidenceBundleCommands.ts
T
Kaoru0822andClaude Opus 5.5 c2bee63e99 fix(evidence): clear staged write cleanup state after a confirmed retry (#5852)
The atomic JSON writer reported a failed staging-directory release as
cleanup state on the returned EvidenceFileError or AnalysisCancelledError,
but never bound that error to the retained staging lease. A later scope
retry that removed the stage could not clear the error, so
`rea evidence-export` (and other writers sharing the scope, such as
--evidence-output delivery) kept reporting cleanup_incomplete and a
retained resource after cleanup had completed.

Bind each staged-output cleanup failure, and each admission retry
failure, to its exact leases so confirmed release restores the primary
outcome. Export recovery now reads that bound state instead of a local
close flag. A retry that still fails keeps the error incomplete with the
same retained stage.

Fixes #5842

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 04:57:08 -07:00

100 lines
3.3 KiB
TypeScript

import {
EvidenceFileError,
EvidenceIntegrityError,
} from "../domain/evidenceErrors.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { EvidenceBundle } from "../domain/evidenceBundle.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import { compareBundles } from "../domain/bundleComparison.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import {
readEvidenceBundle,
readImmutableEvidenceBundle,
writeEvidenceBundle,
} from "./EvidenceBundleFiles.js";
/** Validate a standalone bundle and report its complete record/revision counts. */
export const importEvidenceBundleCommand = async (
path: string,
): Promise<Result<JsonValue, AnalysisError>> => {
const loaded = await readEvidenceBundle(path);
if (!loaded.ok) return loaded;
return ok({
imported: loaded.value.records.length,
unknowns_added: loaded.value.unknowns.length,
total: loaded.value.records.length,
});
};
/** Validate a source bundle and atomically export canonical bytes. */
export const exportEvidenceBundleCommand = async (
sourcePath: string,
outputPath: string,
overwrite: boolean,
): Promise<Result<JsonValue, AnalysisError>> => {
// The sealed snapshot lets the writer reuse this authentication.
const loaded = await readImmutableEvidenceBundle(sourcePath);
if (!loaded.ok) return loaded;
const resources = new ArtifactResourceScope();
const written = await writeEvidenceBundle(
loaded.value,
outputPath,
overwrite,
resources,
);
if (written.ok) return projectWrite(loaded.value, written);
// Confirmed release resolves the bound error to its primary outcome; a
// failed retry leaves that error incomplete with its exact retained owner.
await resources.close().catch(() => undefined);
if (
!written.error.cleanupIncomplete &&
written.error instanceof EvidenceFileError &&
written.error.published
) {
return projectWrite(loaded.value, {
ok: true,
value: written.error.published,
});
}
return written;
};
/** Compare two validated canonical Evidence bundles without session state. */
export const compareEvidenceBundlesCommand = async (input: {
readonly leftPath: string;
readonly rightPath: string;
}): Promise<Result<JsonValue, AnalysisError>> => {
// Sealed snapshots let comparison and its bundle digests reuse this
// authentication instead of re-validating every record twice more.
const [left, right] = await Promise.all([
readImmutableEvidenceBundle(input.leftPath),
readImmutableEvidenceBundle(input.rightPath),
]);
if (!left.ok) return left;
if (!right.ok) return right;
try {
return ok(jsonValueSchema.parse(compareBundles(left.value, right.value)));
} catch (cause: unknown) {
return err(
new EvidenceIntegrityError("Evidence bundle comparison failed", {
cause,
}),
);
}
};
const projectWrite = (
bundle: EvidenceBundle,
written: Awaited<ReturnType<typeof writeEvidenceBundle>>,
): Result<JsonValue, AnalysisError> =>
written.ok
? ok({
path: written.value.path,
bytes: written.value.bytes,
records: bundle.records.length,
})
: err(written.error);