mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 16:57:19 +00:00
Reuse the nonempty NUL-free local path contract before normalization in artifact commands, auxiliary inputs, and output destinations. Empty selections now fail at parsing instead of selecting the working directory; explicit relative paths preserve their target. Public CLI checks cover primary and auxiliary empty paths, exact relative-file digest, explicit current-directory selection, symlink parent semantics, and environment export inputs. Correct the desktop fixture to assert the existing CLI validation envelope.
168 lines
5.6 KiB
TypeScript
168 lines
5.6 KiB
TypeScript
import { readArtifactInspectionSources } from "../domain/artifactInspection.js";
|
|
import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js";
|
|
import { z } from "incur";
|
|
import { parseEvidence, type Evidence } from "../domain/evidence.js";
|
|
import type { JsonValue } from "../domain/jsonValue.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import { ok, type Result } from "../domain/result.js";
|
|
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
|
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
|
|
import { publishEvidenceDelivery } from "../application/EvidenceBundleCommands.js";
|
|
import { inspectAnalysisViewValidated } from "../application/analysisView/AnalysisViewService.js";
|
|
import { localPathStringSchema } from "../domain/localPath.js";
|
|
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
|
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
|
|
|
export const analysisEvidenceDeliveryOptions = {
|
|
detail: z
|
|
.enum(["complete", "summary"])
|
|
.default("complete")
|
|
.describe("Deliver complete Evidence, or a summary with --evidence_output"),
|
|
evidence_output: localPathStringSchema
|
|
.optional()
|
|
.describe(
|
|
"Save complete Evidence as a bundle; required for summary delivery. Never overwrites.",
|
|
),
|
|
};
|
|
|
|
const isEvidence = (value: JsonValue): value is Evidence =>
|
|
typeof value === "object" &&
|
|
value !== null &&
|
|
!Array.isArray(value) &&
|
|
"evidence_id" in value &&
|
|
"normalized_result" in value;
|
|
|
|
interface CliEvidenceDelivery {
|
|
readonly view?: AnalysisViewRequest;
|
|
readonly providerId: string;
|
|
readonly operation: string;
|
|
readonly signal: AbortSignal;
|
|
readonly detail: "complete" | "summary";
|
|
readonly evidenceOutput: string | undefined;
|
|
/** Derived workflows publish their selected source closure with the result. */
|
|
readonly sourceEvidence?: readonly Evidence[];
|
|
}
|
|
|
|
export const cliEvidenceDeliveryInputError = (
|
|
input: Pick<CliEvidenceDelivery, "detail" | "evidenceOutput" | "operation">,
|
|
): AnalysisInputError | undefined =>
|
|
input.detail === "summary" && input.evidenceOutput === undefined
|
|
? new AnalysisInputError(input.operation, undefined, [
|
|
{
|
|
path: ["evidence_output"],
|
|
reason: "invalid_value",
|
|
message:
|
|
"Summary delivery requires --evidence_output so complete source Evidence remains available.",
|
|
},
|
|
])
|
|
: undefined;
|
|
|
|
/** One-shot delivery owns report publication; analysis keeps its own cleanup owner. */
|
|
export const deliverCliAnalysisEvidence = async (
|
|
input: CliEvidenceDelivery & {
|
|
readonly run: () => Promise<Result<Evidence, AnalysisError>>;
|
|
readonly service: { close(): Promise<void> };
|
|
},
|
|
) => {
|
|
const {
|
|
run,
|
|
service,
|
|
providerId,
|
|
operation,
|
|
signal,
|
|
detail,
|
|
evidenceOutput,
|
|
} = input;
|
|
const inputError = cliEvidenceDeliveryInputError(input);
|
|
if (inputError !== undefined)
|
|
return finishAnalysisCommand(
|
|
async () => ({ ok: false, error: inputError }),
|
|
service,
|
|
providerId,
|
|
operation,
|
|
signal,
|
|
);
|
|
const complete = await finishAnalysisCommand(
|
|
run,
|
|
service,
|
|
providerId,
|
|
operation,
|
|
signal,
|
|
);
|
|
return publishCliAnalysisEvidence(complete, {
|
|
providerId,
|
|
operation,
|
|
signal,
|
|
detail,
|
|
evidenceOutput,
|
|
...(input.view === undefined ? {} : { view: input.view }),
|
|
...(input.sourceEvidence === undefined
|
|
? {}
|
|
: { sourceEvidence: input.sourceEvidence }),
|
|
});
|
|
};
|
|
|
|
/** Publish already finalized CLI analysis without opening a second provider/session. */
|
|
export const publishCliAnalysisEvidence = async (
|
|
value: JsonValue,
|
|
input: CliEvidenceDelivery,
|
|
): Promise<JsonValue> => {
|
|
const { operation, signal, detail, evidenceOutput } = input;
|
|
if (!isEvidence(value)) return value;
|
|
const complete = parseEvidence(value);
|
|
const inputError = cliEvidenceDeliveryInputError(input);
|
|
if (inputError !== undefined) {
|
|
inputError.retainPartialObservation(complete);
|
|
return projectAnalysisError(inputError);
|
|
}
|
|
if (evidenceOutput === undefined) return complete;
|
|
// Project before publication so a view failure cannot commit an
|
|
// unreported bundle; publication then owns every post-commit outcome.
|
|
const delivered =
|
|
detail === "summary"
|
|
? inspectAnalysisViewValidated({
|
|
source: { kind: "inline", evidence: complete },
|
|
view: input.view ?? { kind: "summary" },
|
|
})
|
|
: ok(complete);
|
|
if (!delivered.ok)
|
|
return projectAnalysisError(
|
|
delivered.error.retainPartialObservation(complete),
|
|
);
|
|
const inspectionSources =
|
|
complete.operation === "inspect_artifact"
|
|
? readArtifactInspectionSources(complete.normalized_result)
|
|
: ok([]);
|
|
if (!inspectionSources.ok)
|
|
return projectAnalysisError(
|
|
inspectionSources.error.retainPartialObservation(complete),
|
|
);
|
|
const published = await publishEvidenceDelivery({
|
|
bundle: createEvidenceBundle([
|
|
...new Map(
|
|
[
|
|
...(input.sourceEvidence ?? []),
|
|
...inspectionSources.value,
|
|
complete,
|
|
].map((record) => [record.evidence_id, record]),
|
|
).values(),
|
|
]),
|
|
complete,
|
|
delivered: delivered.value,
|
|
outputPath: absoluteCallerPath(evidenceOutput),
|
|
operation,
|
|
signal,
|
|
});
|
|
if (!published.ok) return projectAnalysisError(published.error);
|
|
return detail === "summary"
|
|
? {
|
|
summary: published.value.delivered,
|
|
evidence_bundle: {
|
|
path: published.value.evidenceBundle.path,
|
|
bytes: published.value.evidenceBundle.bytes,
|
|
},
|
|
}
|
|
: published.value.delivered;
|
|
};
|