Files
reverse-engineering-anything/src/application/ProviderOperationExecution.ts
T
morluto ccdeda7a27 fix(inputs): centralize admission and preserve caller selections
Move shared request parsing into its application owner, migrate all consumers, and remove six duplicated admission blocks while preserving cancellation and validation policy.

Keep operator-selected JavaScript paths in verifier parent Evidence while retaining canonical comparison guards. Validated with 204 passing tests, 3 Linux-x64-only skips, typecheck, lint, and test discovery.
2026-10-26 17:35:18 +08:00

143 lines
4.7 KiB
TypeScript

import { z } from "zod";
import type {
AnalysisError,
AnalysisErrorOptions,
AnalysisPartialObservation,
} from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import type { Evidence } from "../domain/evidence.js";
import {
freezeJsonSnapshot,
isImmutableJsonSnapshot,
} from "../domain/immutableJson.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { analysisOperationInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../domain/result.js";
import type {
AnalysisExecution,
ExecutionOptions,
} from "./AnalysisProvider.js";
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
/** Validated caller-selected request carrying its operation input. */
export interface ProviderOperationRequest {
readonly input: unknown;
}
/** Provider boundary executing one validated caller-selected request. */
export interface ProviderOperationPort<
TRequest extends ProviderOperationRequest,
> {
execute(
request: TRequest,
options?: ExecutionOptions,
): Promise<Result<AnalysisExecution, AnalysisError>>;
}
/**
* Shared CLI/MCP admission, execution, and Evidence composition for
* single-request providers. The caller retains its operation vocabulary and
* confidence; this normalizes cancellation, input validation, and Evidence
* assembly once instead of per provider.
*/
export const executeProviderOperation = async <
TRequest extends ProviderOperationRequest,
>(
operation: string,
input: unknown,
options: ExecutionOptions | undefined,
schema: z.ZodType<TRequest>,
provider: ProviderOperationPort<TRequest>,
confidence: "observed" | "derived",
): Promise<Result<Evidence, AnalysisError>> => {
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(operation, { cause: options.signal.reason }),
);
const request = schema.safeParse({ operation, input });
if (!request.success)
return err(
analysisOperationInputErrorFromIssues(
operation,
request.error.issues,
input,
request.error,
),
);
const executed = await provider.execute(request.data, options);
if (!executed.ok) return executed;
const evidence = createAnalysisExecutionEvidence(
operation,
request.data.input,
executed.value,
confidence,
);
return completeProviderEvidence(evidence, options?.signal);
};
/** Classify completion after Evidence assembly, preserving facts on late abort. */
export const completeProviderEvidence = (
evidence: Evidence,
signal?: AbortSignal,
capturedOutput?: AnalysisErrorOptions["capturedOutput"],
): Result<Evidence, AnalysisCancelledError> => {
if (signal?.aborted)
return err(
new AnalysisCancelledError(evidence.operation, {
cause: signal.reason,
partialObservation: evidence,
...(capturedOutput === undefined ? {} : { capturedOutput }),
}),
);
return ok(evidence);
};
/**
* Retain an incomplete observation when projection can serialize it.
* A JSON partial is replaced by one frozen snapshot, so a later mutation
* cannot make projection throw and projection does not copy it again.
* A partial that is not JSON is cleared from the returned error and from
* every cleanup primary projection can select later. Each cleared error
* records an output failure, leaving cancellation or timeout primary.
*/
export const keepIncompleteObservation = (
error: AnalysisError,
operation: string,
reason: string,
): AnalysisError => {
const seen = new Set<AnalysisError>();
// One source object can sit on a wrapper and its cleanup primary.
const judged = new Map<object, AnalysisPartialObservation | undefined>();
let current: AnalysisError | undefined = error;
while (current !== undefined && !seen.has(current)) {
seen.add(current);
const partial = current.partialObservation;
if (
partial !== undefined &&
!isImmutableJsonSnapshot(partial) &&
typeof partial === "object"
) {
// safeParse clones only after validation succeeds. Freeze that copy once.
let snapshot = judged.get(partial);
if (!judged.has(partial)) {
const parsed = jsonValueSchema.safeParse(partial);
snapshot = parsed.success
? (freezeJsonSnapshot(parsed.data) as AnalysisPartialObservation)
: undefined;
judged.set(partial, snapshot);
}
if (snapshot === undefined) {
current.partialObservation = undefined;
current.retainSecondaryFailure(
new AnalysisOutputError(operation, reason),
);
} else current.partialObservation = snapshot;
}
current = current.cleanupPrimary;
}
return error;
};