Files
reverse-engineering-anything/src/application/EvidenceBundleCommands.ts
T

262 lines
8.5 KiB
TypeScript

import { readArtifactInspectionSources } from "../domain/artifactInspection.js";
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { requireAuthenticationHeadroom } from "./JsonAuthenticationHeadroom.js";
import {
EvidenceFileError,
type PublishedEvidenceFile,
} from "../domain/evidenceErrors.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
} from "../domain/analysisErrorCore.js";
import type { Evidence } from "../domain/evidence.js";
import {
createImmutableEvidenceBundle,
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,
type BundleRecordPair,
} from "../domain/bundleComparison.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import {
readEvidenceBundle,
readImmutableEvidenceBundle,
writeEvidenceBundle,
} from "./EvidenceBundleFiles.js";
import { inspectAnalysisViewValidated } from "./analysisView/AnalysisViewService.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,
undefined,
undefined,
"import_evidence_bundle",
);
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,
undefined,
undefined,
"export_evidence_bundle",
);
if (!loaded.ok) return loaded;
return projectWrite(
loaded.value,
await persistEvidenceBundleCommand(loaded.value, outputPath, overwrite),
);
};
/** Publish complete parent Evidence before delivering it or its linked summary. */
export const deliverAnalysisEvidence = async (
evidence: Evidence,
outputPath: string,
detail: "complete" | "summary" = "complete",
signal?: AbortSignal,
): Promise<Result<Evidence, AnalysisError>> => {
const headroom = requireAuthenticationHeadroom(evidence, {
operation: evidence.operation,
path: absoluteCallerPath(outputPath),
description:
"Insufficient heap headroom to authenticate the complete parent Evidence",
});
if (!headroom.ok)
return err(headroom.error.retainPartialObservation(evidence));
const sourceEvidence =
evidence.operation === "inspect_artifact"
? readArtifactInspectionSources(evidence.normalized_result)
: ok([]);
if (!sourceEvidence.ok)
return err(sourceEvidence.error.retainPartialObservation(evidence));
const bundle = createImmutableEvidenceBundle([
...sourceEvidence.value,
evidence,
]);
const delivered =
detail === "summary"
? inspectAnalysisViewValidated(
{
source: {
kind: "retained-evidence",
evidence_id: evidence.evidence_id,
},
view: { kind: "summary" },
},
(id) => bundle.records.find((record) => record.evidence_id === id),
)
: ok(evidence);
if (!delivered.ok) return delivered;
const published = await publishEvidenceDelivery({
bundle,
complete: evidence,
delivered: delivered.value,
outputPath,
operation: evidence.operation,
signal,
});
return published.ok ? ok(published.value.delivered) : published;
};
/** Selected Evidence delivered after its complete bundle was committed. */
export interface PublishedEvidenceDelivery {
readonly delivered: Evidence;
readonly evidenceBundle: PublishedEvidenceFile;
}
/**
* Publish a complete bundle, then deliver Evidence already projected from it.
* Cancellation observed before commit leaves no output and retains the
* complete Evidence. A committed publication wins a later cancellation race;
* its receipt stays in the typed cancellation as diagnostics.evidence_bundle,
* and a cleanup failure after commit keeps its own published receipt.
*/
export const publishEvidenceDelivery = async (input: {
readonly bundle: EvidenceBundle;
readonly complete: Evidence;
readonly delivered: Evidence;
readonly outputPath: string;
readonly operation: string;
readonly signal?: AbortSignal | undefined;
}): Promise<Result<PublishedEvidenceDelivery, AnalysisError>> => {
const { signal, operation } = input;
const written = await persistEvidenceBundleCommand(
input.bundle,
input.outputPath,
false,
signal,
operation,
);
// Deliver interrupts queued during final publication cleanup before the
// command's signal owner accepts completion and removes its listeners.
await dispatchQueuedInterrupts();
if (!written.ok) {
const original = written.error;
// This publication workflow never overwrites. The standalone export
// command retains its own overwrite-capable recovery guidance.
const failure =
original instanceof EvidenceFileError && original.reason === "exists"
? new EvidenceFileError(original.operation, original.reason, {
cause: original,
...(original.path === undefined ? {} : { path: original.path }),
userMessage:
"Evidence file already exists. Choose another output path.",
cleanup: original.cleanup,
diagnostics: original.diagnostics,
}).bindCleanupDependencies([original])
: original;
return err(failure.retainPartialObservation(input.complete));
}
if (signal?.aborted)
return err(
new AnalysisCancelledError(operation, {
cause: signal.reason,
partialObservation: input.delivered,
diagnostics: { evidence_bundle: { ...written.value } },
}),
);
return ok({ delivered: input.delivered, evidenceBundle: written.value });
};
/** Publish a completed bundle and retry its exact staged-output cleanup owner. */
const persistEvidenceBundleCommand = async (
bundle: EvidenceBundle,
outputPath: string,
overwrite: boolean,
signal?: AbortSignal,
operation?: string,
): Promise<Result<PublishedEvidenceFile, AnalysisError>> => {
const resources = new ArtifactResourceScope();
const written = await writeEvidenceBundle(
bundle,
outputPath,
overwrite,
resources,
signal,
operation,
);
if (written.ok) return 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 ok(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;
readonly recordPairs?: readonly BundleRecordPair[];
}): 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,
undefined,
undefined,
"compare_bundles",
),
readImmutableEvidenceBundle(
input.rightPath,
undefined,
undefined,
"compare_bundles",
),
]);
if (!left.ok) return left;
if (!right.ok) return right;
try {
return ok(
jsonValueSchema.parse(
compareBundles(left.value, right.value, input.recordPairs),
),
);
} catch (cause: unknown) {
if (cause instanceof AnalysisInputError) return err(cause);
throw cause;
}
};
const projectWrite = (
bundle: EvidenceBundle,
written: Result<PublishedEvidenceFile, AnalysisError>,
): Result<JsonValue, AnalysisError> =>
written.ok
? ok({
path: written.value.path,
bytes: written.value.bytes,
records: bundle.records.length,
})
: err(written.error);