Files
reverse-engineering-anything/src/domain/analysisErrorProjection.ts
T
morluto 6df2ac1d7e fix(evidence): report partial workflow question publication (#8848)
* fix(evidence): report partial workflow question publication

* fix: project nested recovery for caller lifetime
2026-11-05 01:49:36 +08:00

833 lines
28 KiB
TypeScript

import { projectArtifactIntegrityDetails } from "./artifactIntegrity.js";
import {
cleanupRetryRecovery,
cleanupRetryRemediation,
type AnalysisErrorRecovery,
} from "./analysisErrorRecovery.js";
import { jsonValueSchema, type JsonValue } from "./jsonValue.js";
import { isImmutableJsonSnapshot } from "./immutableJson.js";
import {
analysisErrorCategory,
analysisErrorRemediationAction,
analysisErrorUserMessage,
assertKnownAnalysisErrorTag,
} from "./analysisErrorPresentation.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisUnsupportedTargetError,
AnalysisInputError,
AnalysisOutputError,
AnalysisTimeoutError,
AnalysisResourceConstraintError,
} from "./analysisErrorCore.js";
import { ArtifactOperationError } from "./artifactOperationError.js";
import {
BinaryTargetError,
ConfigurationError,
} from "./configurationErrors.js";
import { BrowserObservationError } from "./browserObservationError.js";
import {
AnalysisSnapshotMismatchError,
EvidenceFileError,
EvidenceIntegrityError,
EvidenceReferenceError,
} from "./evidenceErrors.js";
import {
HopperCancelledError,
HopperProcessError,
HopperRemoteError,
HopperStartError,
HopperTimeoutError,
} from "./hopperErrors.js";
import { ProviderAdapterError } from "./providerAdapterError.js";
import { ProviderSelectionError } from "./providerSelectionError.js";
import { UnknownRegistryError } from "./unknownRegistryError.js";
import { providerRetryAction } from "./providerOperationHealth.js";
import {
primaryAnalysisError,
resolvedAnalysisError,
type AnalysisCallerLifetime,
type AnalysisError,
type AnalysisErrorOptions,
type AnalysisErrorTag,
} from "./analysisErrorBase.js";
import { invocationSettingLimit } from "./analysisSettingDefinitions.js";
/**
* Project expected failures into exhaustive, secret-safe caller fields. The
* default describes a continuing caller, such as an MCP connection, and is
* also the adapter-independent form retained inside Evidence. An adapter that
* releases its provider session and runtime after this request, such as a
* one-shot CLI command, passes `"invocation"`.
*/
export const projectAnalysisError = (
error: AnalysisError,
callerLifetime: AnalysisCallerLifetime = "continuing",
): AnalysisErrorProjection => projectError(error, true, callerLifetime);
/** Cleanup history references output retained by its enclosing error owner. */
export const projectAnalysisErrorWithoutCapturedOutput = (
error: AnalysisError,
callerLifetime: AnalysisCallerLifetime = "continuing",
): AnalysisErrorProjection => projectError(error, false, callerLifetime);
/** A child failure embedded in a composed error's diagnostics. */
export interface AnalysisChildFailure {
readonly error: AnalysisError;
/** Omit output that the composing error already retains. */
readonly capturedOutput?: "omit";
}
/**
* Record child failures as composition-time snapshots, so cleanup history
* keeps the state it had. The invocation form is recorded only when a child's
* projection depends on the caller lifetime; the final projection selects it.
*/
export const childFailureDiagnostics = (
children: Readonly<Record<string, AnalysisChildFailure>>,
diagnostics: Readonly<Record<string, JsonValue>> = {},
): Pick<AnalysisErrorOptions, "diagnostics" | "invocationDiagnostics"> => {
const entries = Object.entries(children);
const project = (
{ error, capturedOutput }: AnalysisChildFailure,
callerLifetime: AnalysisCallerLifetime,
) => projectError(error, capturedOutput !== "omit", callerLifetime);
const continuing = {
...diagnostics,
...Object.fromEntries(
entries.map(([key, child]) => [key, project(child, "continuing")]),
),
};
const varying = entries.filter(([, { error }]) =>
projectionDependsOnCallerLifetime(error),
);
return varying.length === 0
? { diagnostics: continuing }
: {
diagnostics: continuing,
invocationDiagnostics: {
...continuing,
...Object.fromEntries(
varying.map(([key, child]) => [key, project(child, "invocation")]),
),
},
};
};
/** Every lifetime-dependent input the projection reads lies on this chain. */
const projectionDependsOnCallerLifetime = (error: AnalysisError): boolean => {
for (
let node: AnalysisError | undefined = error;
node !== undefined;
node = node.cleanupPrimary
)
if (
node.limit !== undefined ||
node.continuingCallerGuidance !== undefined ||
node.invocationDiagnostics !== undefined ||
node.workflowUnknownPublication !== undefined
)
return true;
return false;
};
const projectError = (
error: AnalysisError,
includeCapturedOutput: boolean,
callerLifetime: AnalysisCallerLifetime,
): AnalysisErrorProjection => {
const composed = error;
error = resolvedAnalysisError(error);
let diagnosticSource = error;
while (
diagnosticSource.diagnostics === undefined &&
diagnosticSource.cleanupPrimary !== undefined
)
diagnosticSource = diagnosticSource.cleanupPrimary;
const diagnostics = diagnosticSource.diagnosticsFor(callerLifetime);
const cleanupHistory =
composed === error ? undefined : composed.diagnosticsFor(callerLifetime);
const capturedOutput = composed.capturedOutput ?? error.capturedOutput;
const partialObservation =
composed.partialObservation ?? error.partialObservation;
// A finalizer can add a report above the semantic primary, including while
// its cleanup is pending or beneath another cleanup composition.
let cleanupReport = composed.cleanupReport;
for (
let owner = composed.cleanupPrimary;
cleanupReport === undefined && owner !== undefined;
owner = owner.cleanupPrimary
)
cleanupReport = owner.cleanupReport;
assertKnownAnalysisErrorTag(error._tag);
const primary = primaryAnalysisError(error);
assertKnownAnalysisErrorTag(primary._tag);
const underlyingCode = underlyingErrorCode(primary);
const code = error.cleanupIncomplete ? "cleanup_incomplete" : underlyingCode;
const primaryDetails = errorDetails(primary);
const publication =
composed.workflowUnknownPublication ?? primary.workflowUnknownPublication;
const details = {
...primaryDetails,
...(publication === undefined
? {}
: {
workflow_unknown_publication: {
...publication,
evidence_retention:
callerLifetime === "invocation" &&
publication.evidence_retention === "retained"
? "invocation-only"
: publication.evidence_retention,
},
}),
...(primary.limit === undefined
? {}
: {
limit:
callerLifetime === "invocation"
? { ...invocationSettingLimit(primary.limit) }
: { ...primary.limit },
}),
...(cleanupReport === undefined
? {}
: { cleanup_report: jsonValueSchema.parse(cleanupReport) }),
...(diagnostics === undefined
? {}
: {
diagnostics,
...(primaryDetails?.diagnostics === undefined
? {}
: { provider_diagnostics: primaryDetails.diagnostics }),
}),
...(cleanupHistory === undefined
? {}
: { cleanup_history: cleanupHistory }),
...(error.cleanupIncomplete
? {
cleanup: "incomplete",
resources: [...error.cleanupResources],
...(error.cleanup === undefined
? {}
: { cleanup_reason: error.cleanup.reason }),
execution_failure:
primaryDetails?.execution_failure ?? underlyingCode,
}
: {}),
...(!includeCapturedOutput || capturedOutput === undefined
? {}
: {
captured_output: {
...capturedOutput,
},
}),
...(partialObservation === undefined
? {}
: {
partial_observation: isImmutableJsonSnapshot(partialObservation)
? partialObservation
: jsonValueSchema.parse(partialObservation),
}),
};
// The retained cleanup owner's retry precedes the failed operation's hint.
const cleanupRetry = error.cleanupRetry;
const recovery =
cleanupRetry === undefined
? errorRecovery(error)
: cleanupRetryRecovery(cleanupRetry);
const remediation = analysisErrorRemediationAction(error, callerLifetime);
const publicationRemediation =
callerLifetime === "continuing" &&
publication?.evidence_retention === "retained"
? " Use get_evidence_bundle with the Evidence ID in details.workflow_unknown_publication to retrieve the completed result; use list_unknowns to inspect recorded questions. Publication stopped at the reported index; later questions were not checked in this attempt."
: "";
return {
code,
category: analysisErrorCategory(error),
message: analysisErrorUserMessage(error, callerLifetime),
retryable:
RETRYABLE_CODES.has(code) ||
(error instanceof AnalysisCapabilityUnavailableError &&
error.retryable) ||
(code === "resource_constraint" &&
error instanceof AnalysisResourceConstraintError &&
(error.retryable ?? error.resource === "capacity")),
remediation: {
action:
cleanupRetry === undefined
? `${remediation}${publicationRemediation}`
: `${remediation} ${cleanupRetryRemediation(cleanupRetry)}${publicationRemediation}`,
},
...(recovery === undefined ? {} : { recovery }),
...(Object.keys(details).length === 0 ? {} : { details }),
};
};
const errorRecovery = (
error: AnalysisError,
): AnalysisErrorRecovery | undefined => {
if (error instanceof AnalysisInputError)
return error.conformsToInputContract
? undefined
: { kind: "inspect_input_contract", operation: error.operation };
if (error instanceof AnalysisTimeoutError)
return {
kind: "inspect_timeout_state",
operation: error.operation,
timeout_ms: error.timeoutMs,
};
return undefined;
};
const underlyingErrorCode = (
error: AnalysisError,
): AnalysisErrorProjection["code"] => {
if (
error instanceof HopperRemoteError &&
error.diagnosticType === "invalid_request"
)
return "invalid_request";
if (error instanceof ProviderSelectionError)
return error.reason === "provider_unavailable"
? "provider_unavailable"
: "capability_unavailable";
if (error instanceof BrowserObservationError)
return error.userCategory === "cancelled"
? "cancelled"
: BROWSER_ERROR_CODES[error.reason];
if (error instanceof ArtifactOperationError)
return artifactOperationCode(error);
if (error instanceof BinaryTargetError)
return error.constraint === "load_selection_rejected"
? "invalid_request"
: error.systemCode === undefined
? "target_unavailable"
: "access_denied";
if (error instanceof EvidenceFileError) return evidenceFileCode(error.reason);
if (error instanceof UnknownRegistryError)
return unknownRegistryCode(error.reason);
if (error._tag === "ProcessCaptureError") return processCaptureCode(error);
return staticErrorCode(error._tag);
};
const BROWSER_ERROR_CODES: Readonly<
Record<BrowserObservationError["reason"], AnalysisErrorProjection["code"]>
> = {
endpoint_unreachable: "provider_unavailable",
invalid_endpoint_response: "unreadable_output",
target_not_found: "target_unavailable",
target_not_allowed: "target_unavailable",
service_worker_blocked: "target_unavailable",
target_changed: "target_unavailable",
target_session_detached: "target_unavailable",
protocol_error: "unreadable_output",
disconnected: "provider_unavailable",
payload_limit: "truncated",
window_not_found: "unreadable_output",
window_metadata_missing: "unreadable_output",
process_ownership_unavailable: "unreadable_output",
cleanup_failed: "execution_failure",
cancelled: "cancelled",
timeout: "provider_timeout",
};
const artifactOperationCode = (
error: ArtifactOperationError,
): AnalysisErrorProjection["code"] => {
if (error.reason === "integrity" && error.artifactDetails !== undefined)
return "artifact_integrity_mismatch";
if (error.reason === "limit") return "truncated";
if (error.reason === "cancelled") return "cancelled";
return "artifact_operation_failed";
};
const evidenceFileCode = (
reason: EvidenceFileError["reason"],
): AnalysisErrorProjection["code"] => {
if (reason === "invalid-json") return "evidence_integrity_mismatch";
if (reason === "path-limit" || reason === "storage-limit")
return "resource_constraint";
// A missing, non-file or already-existing path is the caller's selection.
if (reason === "io") return "execution_failure";
return "invalid_request";
};
const unknownRegistryCode = (
reason: UnknownRegistryError["reason"],
): AnalysisErrorProjection["code"] => {
if (reason === "revision-conflict") return "revision_conflict";
if (reason === "already-exists" || reason === "constraint")
return "invalid_request";
if (reason === "limit") return "truncated";
if (reason === "integrity") return "evidence_integrity_mismatch";
return "execution_failure";
};
const processCaptureCode = (
error: AnalysisError,
): AnalysisErrorProjection["code"] => {
if (error.cleanupIncomplete) return "cleanup_incomplete";
if (error.userCategory === "cancelled") return "cancelled";
return "process_capture_failed";
};
type SpecializedErrorTag =
| "ArtifactOperationError"
| "BinaryTargetError"
| "BrowserObservationError"
| "EvidenceFileError"
| "ProcessCaptureError"
| "ProviderSelectionError"
| "UnknownRegistryError";
const STATIC_ERROR_CODES = {
AnalysisProtocolError: "unreadable_output",
AnalysisOutputError: "unreadable_output",
HopperProtocolError: "unreadable_output",
AnalysisInputError: "invalid_request",
AnalysisAccessDeniedError: "access_denied",
AnalysisArtifactChangedError: "artifact_changed",
AnalysisCapabilityUnavailableError: "capability_unavailable",
AnalysisUnsupportedTargetError: "unsupported_target",
AnalysisCancelledError: "cancelled",
HopperCancelledError: "cancelled",
AnalysisTimeoutError: "provider_timeout",
AnalysisResourceConstraintError: "resource_constraint",
HopperTimeoutError: "provider_timeout",
HopperProcessError: "provider_unavailable",
HopperStartError: "provider_unavailable",
ConfigurationError: "configuration_invalid",
NoBinaryOpenError: "target_unavailable",
EvidenceIntegrityError: "evidence_integrity_mismatch",
ProviderAdapterError: "execution_failure",
HopperRemoteError: "execution_failure",
} as const satisfies Readonly<
Record<
Exclude<AnalysisErrorTag, SpecializedErrorTag>,
AnalysisErrorProjection["code"]
>
>;
const staticErrorCode = (
tag: AnalysisErrorTag,
): AnalysisErrorProjection["code"] => {
switch (tag) {
case "ArtifactOperationError":
case "BinaryTargetError":
case "BrowserObservationError":
case "EvidenceFileError":
case "ProcessCaptureError":
case "ProviderSelectionError":
case "UnknownRegistryError":
throw new TypeError(`Unhandled specialized analysis error: ${tag}`);
default:
return STATIC_ERROR_CODES[tag];
}
};
const errorDetails = (
error: AnalysisError,
): Readonly<Record<string, JsonValue>> | undefined =>
requestErrorDetails(error) ??
artifactStateErrorDetails(error) ??
providerErrorDetails(error) ??
lifecycleErrorDetails(error);
const requestErrorDetails = (
error: AnalysisError,
): Readonly<Record<string, JsonValue>> | undefined => {
if (error instanceof AnalysisUnsupportedTargetError)
return {
operation: error.operation,
path: error.path,
reason: error.reason,
};
if (error instanceof AnalysisResourceConstraintError)
return {
operation: error.operation,
resource: error.resource,
reason: error.reason,
reported_limits: error.reportedLimits,
};
if (error instanceof AnalysisArtifactChangedError)
return {
operation: error.operation,
path: error.path,
reason: error.reason,
boundary: "stable-artifact-read",
};
if (error instanceof AnalysisAccessDeniedError)
return {
operation: error.operation,
path: error.path,
system_code: error.systemCode,
boundary: "filesystem-read",
};
if (error instanceof AnalysisOutputError)
return {
operation: error.operation,
reason: error.reason,
...(error.diagnostics === undefined
? {}
: { diagnostics: error.diagnostics }),
};
if (error instanceof AnalysisInputError && error.issues.length > 0)
return {
operation: error.operation,
issues: error.issues.map((issue) => ({
path: [...issue.path],
reason: issue.reason,
...(issue.message === undefined ? {} : { message: issue.message }),
...(issue.expected === undefined ? {} : { expected: issue.expected }),
...(issue.observed === undefined ? {} : { observed: issue.observed }),
...(issue.minimum === undefined ? {} : { minimum: issue.minimum }),
...(issue.maximum === undefined ? {} : { maximum: issue.maximum }),
})),
};
if (error instanceof EvidenceReferenceError)
return {
evidence_id: error.evidenceId,
reason: error.reason,
...(typeof error.diagnostics?.operation === "string"
? { operation: error.diagnostics.operation }
: {}),
...(error.referenceScope === undefined
? {}
: { reference_scope: error.referenceScope }),
expected: error.expected,
actual: error.actual,
};
if (error instanceof AnalysisSnapshotMismatchError)
return {
reason: error.message,
...(error.bindingMismatch === undefined
? {}
: { binding_mismatch: jsonValueSchema.parse(error.bindingMismatch) }),
};
if (error instanceof EvidenceIntegrityError) return { reason: error.message };
return undefined;
};
const artifactStateErrorDetails = (
error: AnalysisError,
): Readonly<Record<string, JsonValue>> | undefined => {
if (error instanceof ArtifactOperationError && error.artifactDetails)
return projectArtifactIntegrityDetails(error.artifactDetails);
if (error instanceof ArtifactOperationError)
return {
operation: error.operation,
reason: error.reason,
...(error.reason === "limit" ? { truncated: true } : {}),
...(error.detail === undefined ? {} : { detail: error.detail }),
...(error.boundary === undefined ? {} : { boundary: error.boundary }),
...(error.admission === undefined
? {}
: {
admission: error.admission.kind,
maximum_header_bytes: error.admission.maximumBytes,
maximum_header_bytes_source: error.admission.source,
setting: "REA_ASAR_MAX_HEADER_BYTES",
}),
};
if (error instanceof UnknownRegistryError)
return {
reason: error.reason,
...(error.constraint === undefined
? {}
: { constraint: error.constraint }),
};
if (error instanceof EvidenceFileError)
return {
operation: error.operation,
reason: error.reason,
...(error.reason === "path-limit" ? { boundary: "filesystem-path" } : {}),
...(error.systemCode === undefined
? {}
: { system_code: error.systemCode }),
...(error.path === undefined ? {} : { path: error.path }),
...(error.contentIssues === undefined
? {}
: {
content_issues: error.contentIssues.map((issue) => ({
...issue,
path: [...issue.path],
})),
}),
...(error.published === undefined
? {}
: {
published: true,
bytes: error.published.bytes,
...(error.published.path === undefined
? {}
: { published_path: error.published.path }),
}),
};
return undefined;
};
const providerErrorDetails = (
error: AnalysisError,
): Readonly<Record<string, JsonValue>> | undefined => {
if (error instanceof AnalysisCapabilityUnavailableError)
return {
provider_id: error.providerId,
operation: error.operation,
reason: error.reason,
};
if (error instanceof ProviderSelectionError)
return {
operation: error.operation,
selection_reason: error.reason,
requested_provider_id: error.requestedProviderId,
candidate_ids: [...error.candidateIds],
rejections: error.rejections.map((rejection) => ({
provider_id: rejection.providerId,
origin: rejection.origin,
code: rejection.code,
reason: rejection.reason,
...(rejection.remediation === undefined
? {}
: { remediation: rejection.remediation }),
diagnostics: rejection.diagnostics,
})),
};
if (error instanceof ProviderAdapterError)
return {
provider_id: error.providerId,
operation: error.operation,
...(error.cleanupIncomplete
? {
cleanup: "incomplete",
resources: [...error.cleanupResources],
}
: {}),
};
if (error instanceof BrowserObservationError) {
const primaryReason = primaryBrowserFailureReason(error);
return {
operation: error.operation,
reason: error.reason,
...(error.cleanupIncomplete
? {
cleanup: "incomplete",
resources: [...error.cleanupResources],
...(primaryReason === undefined
? {}
: { primary_reason: primaryReason }),
}
: {}),
};
}
if (error instanceof HopperRemoteError)
return {
stage: "analysis",
provider_code: error.code,
diagnostic_type: error.diagnosticType,
...(error.operation === undefined ? {} : { operation: error.operation }),
...(error.requestId === undefined ? {} : { request_id: error.requestId }),
};
if (error instanceof HopperProcessError) {
const stage = error.stage;
return {
exit_code: error.exitCode,
stage,
provider_state: error.providerState,
retry_action: providerRetryAction(
error.providerState,
error.failureCode !== undefined || error.stage === "launch",
),
...(error.failureCode === undefined
? {}
: { failure_code: error.failureCode }),
...(error.operation === undefined ? {} : { operation: error.operation }),
...(error.requestId === undefined ? {} : { request_id: error.requestId }),
...(error.diagnostic === undefined
? {}
: { diagnostics: { ...error.diagnostic } }),
...(error.launcherFailure === undefined
? {}
: { launcher: error.launcherFailure }),
};
}
if (error instanceof HopperStartError)
return {
stage: "launch",
provider_state: "unknown",
...(error.launchRejection === undefined
? {}
: { launch_rejection: { ...error.launchRejection } }),
retry_action: error.ownerRunId === undefined ? "retry" : "unknown",
...(error.ownerRunId === undefined
? {}
: { owner_run_id: error.ownerRunId }),
...(error.launcherFailure === undefined
? {}
: { launcher: error.launcherFailure }),
};
return undefined;
};
const primaryBrowserFailureReason = (
error: BrowserObservationError,
): string | undefined => {
const aggregate = error.cause instanceof AggregateError;
const primary =
error.cause instanceof AggregateError ? error.cause.errors[0] : error.cause;
if (primary instanceof BrowserObservationError) return primary.reason;
if (primary instanceof AnalysisCancelledError) return "cancelled";
if (primary instanceof AnalysisTimeoutError) return "timeout";
if (typeof primary === "object" && primary !== null && "_tag" in primary)
return typeof primary._tag === "string"
? primary._tag
: "execution_failure";
return aggregate ? "execution_failure" : undefined;
};
const lifecycleErrorDetails = (
error: AnalysisError,
): Readonly<Record<string, JsonValue>> | undefined => {
if (error instanceof AnalysisCancelledError)
return {
operation: error.operation,
cleanup: error.cleanupIncomplete ? "incomplete" : "complete",
...(error.cleanup === undefined || !error.cleanupIncomplete
? {}
: {
cleanup_reason: error.cleanup.reason,
resources: [...error.cleanup.resources],
execution_failure: "cancelled",
}),
};
if (error instanceof HopperCancelledError)
return { operation: "hopper", cleanup: "complete" };
if (error instanceof AnalysisTimeoutError)
return {
operation: error.operation,
timeout_ms: error.timeoutMs,
...(error.cleanup === undefined || !error.cleanupIncomplete
? {}
: {
cleanup: "incomplete",
cleanup_reason: error.cleanup.reason,
resources: [...error.cleanup.resources],
}),
};
if (error instanceof HopperTimeoutError)
return {
stage: error.operation === undefined ? "startup" : error.stage,
timeout_ms: error.timeoutMs,
...(error.launcherOutcome === undefined
? {}
: { launcher: error.launcherOutcome }),
provider_state: error.providerState,
retry_action: error.providerState === "busy" ? "wait" : "retry",
...(error.operation === undefined ? {} : { operation: error.operation }),
...(error.requestId === undefined ? {} : { request_id: error.requestId }),
};
if (error._tag === "ProcessCaptureError" && error.cleanupIncomplete)
return {
cleanup: "incomplete",
resources: [...error.cleanupResources],
...(error.cleanupReport === undefined
? {}
: { cleanup_report: jsonValueSchema.parse(error.cleanupReport) }),
...(error.executionFailure === undefined
? {}
: { execution_failure: error.executionFailure }),
};
if (error._tag === "ProcessCaptureError") {
const details = {
...(error.userCategory === "cancelled"
? { operation: "process_capture", cleanup: "complete" }
: {}),
...(error.cleanupReport === undefined
? {}
: { cleanup_report: jsonValueSchema.parse(error.cleanupReport) }),
...(error.executionFailure === undefined
? {}
: { execution_failure: error.executionFailure }),
};
return Object.keys(details).length === 0 ? undefined : details;
}
if (error instanceof ConfigurationError && error.settings.length > 0)
return {
settings: error.settings.map(({ setting, constraint }) => ({
setting,
constraint,
})),
};
if (error instanceof BinaryTargetError)
return {
path: error.path,
reason: error.reason,
...(error.constraint === undefined
? {}
: { constraint: error.constraint }),
...(error.infoPlist === undefined ? {} : { info_plist: error.infoPlist }),
...(error.probedPaths === undefined
? {}
: { probed_paths: [...error.probedPaths] }),
...(error.systemCode === undefined
? {}
: { system_code: error.systemCode, boundary: "filesystem-read" }),
};
return undefined;
};
const RETRYABLE_CODES: ReadonlySet<AnalysisErrorProjection["code"]> = new Set([
"artifact_changed",
"provider_timeout",
"cancelled",
"revision_conflict",
"provider_unavailable",
]);
export interface AnalysisErrorProjection extends Readonly<
Record<string, JsonValue>
> {
readonly code:
| "invalid_request"
| "access_denied"
| "artifact_changed"
| "unreadable_output"
| "capability_unavailable"
| "unsupported_target"
| "provider_unavailable"
| "provider_timeout"
| "resource_constraint"
| "cancelled"
| "artifact_integrity_mismatch"
| "artifact_operation_failed"
| "evidence_integrity_mismatch"
| "truncated"
| "process_capture_failed"
| "cleanup_incomplete"
| "revision_conflict"
| "configuration_invalid"
| "target_unavailable"
| "execution_failure";
readonly category:
| "invalid_input"
| "unsupported_provider"
| "unsupported_target"
| "integrity_mismatch"
| "truncated"
| "cancelled"
| "timeout"
| "resource_constraint"
| "unavailable"
| "execution_failure";
readonly message: string;
readonly retryable: boolean;
readonly remediation: Readonly<{
action: string;
}>;
readonly recovery?: AnalysisErrorRecovery;
readonly details?: Readonly<Record<string, JsonValue>>;
}