Files
reverse-engineering-anything/src/domain/analysisErrorBase.ts
T
morlutoandCursor Agent 654feeef9c fix(api-contract): retain partial OpenAPI evidence (#9908)
* fix(api-contract): retain partial OpenAPI evidence

* docs(api-contract): describe partial OpenAPI Evidence on interruption

Early failures still keep the file snapshot hash. Cancellation or the
retained-facts limit after a row now returns authenticated Evidence, and
the guide explains how to reuse that partial inventory.

* fix(api-contract): count finished paths and keep earlier diagnostics

A path counts only after its item is read, so a path refused before
traversal does not increase path_entries_examined. A paths diagnostic
that exhausts the retained-facts allowance still keeps diagnostics
already stored, including invalid info.

* test(api-contract): keep invalid info when paths exhaust the allowance

The paths diagnostic can fail after invalid info is already stored. The
partial report must still carry that diagnostic and count no path entries.

* docs(api-contract): name every retained row and the view source

The hash remains only when no operation, reference, or diagnostic row was
kept. Partial Evidence is reread through inspect_analysis_view's source
and view, not as separate Evidence fields.

* test(api-contract): verify interrupted inventory evidence

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-11 17:27:00 +08:00

562 lines
21 KiB
TypeScript

import type { InterfaceBuilderAnalysis } from "./apple/interfaceBuilderGraph.js";
import type { PartialTaskMeasurement } from "./desktop/taskDifficulty.js";
import type { AppleIpsPartialObservation } from "./native/appleIps.js";
import type { PlaywrightTrace } from "./playwrightTrace/playwrightTrace.js";
import type { ApiContract } from "./apiContract/apiContract.js";
import type { GraphqlContract } from "./apiContract/graphqlContract.js";
import type { SavedAnalysisSnapshotPartialObservation } from "./savedAnalysisSnapshot.js";
import type { AnalysisSettingLimit } from "./analysisSettings.js";
import type { JsonValue } from "./jsonValue.js";
import type { AnalysisCleanupRetry } from "./analysisErrorRecovery.js";
import type { EnvironmentBundlePublication } from "./environmentBundleExport.js";
import type { HistoricalSourceGraph } from "./referenceSourceGraph.js";
/** Stable tags exposed by safe analysis-error projections. */
const ANALYSIS_ERROR_TAGS = [
"AnalysisProtocolError",
"AnalysisInputError",
"AnalysisAccessDeniedError",
"AnalysisArtifactChangedError",
"AnalysisOutputError",
"AnalysisCapabilityUnavailableError",
"AnalysisUnsupportedTargetError",
"AnalysisCancelledError",
"AnalysisTimeoutError",
"AnalysisResourceConstraintError",
"ProviderSelectionError",
"ProviderAdapterError",
"BrowserObservationError",
"ArtifactOperationError",
"ProcessCaptureError",
"EvidenceIntegrityError",
"EvidenceFileError",
"UnknownRegistryError",
"HopperTimeoutError",
"HopperCancelledError",
"HopperProtocolError",
"HopperRemoteError",
"HopperProcessError",
"HopperStartError",
"ConfigurationError",
"NoBinaryOpenError",
"BinaryTargetError",
] as const;
/** Stable tag for an expected analysis failure. */
export type AnalysisErrorTag = (typeof ANALYSIS_ERROR_TAGS)[number];
/** Retained command output associated with a typed failure; truncation stays explicit. */
export interface AnalysisCapturedOutput {
readonly stdout: string;
readonly stderr: string;
readonly truncated: boolean;
readonly stdout_bytes?: number;
readonly stderr_bytes?: number;
readonly exit_code?: number | null;
readonly signal?: string | null;
}
/** Cleanup uncertainty attached to the original typed analysis failure. */
export interface AnalysisCleanupObservation {
readonly reason: string;
readonly resources: readonly string[];
}
/** Publication stopped after a workflow produced its Evidence. Counts describe
* question occurrences, not distinct Unknown identities. */
export interface WorkflowUnknownPublication {
readonly evidence_id: string;
readonly evidence_retention: "retained" | "unverified";
readonly total_questions: number;
readonly created_questions: number;
readonly existing_questions: number;
readonly next_question_index: number;
}
/**
* How long the adapter delivering a failure keeps REA's runtime and provider
* session for its caller. An MCP connection continues: later calls reuse the
* session and can update runtime settings. A one-shot invocation, such as a
* CLI command, releases its provider session and exits before its caller can
* act; only a new invocation's launch settings or arguments apply.
*/
export type AnalysisCallerLifetime = "continuing" | "invocation";
/**
* Producer guidance that holds only for a continuing caller, kept apart from
* the request's outcome. The error's own message and remediation must remain
* true after an invocation has released the provider session.
*/
export interface AnalysisContinuingCallerGuidance {
/** Appended to the outcome, such as that the provider session remains usable. */
readonly userMessage?: string;
/** Replaces the remediation, such as a same-session runtime setting update. */
readonly remediationAction?: string;
}
/** Optional provider-neutral context that must survive typed error projection. */
export interface AnalysisErrorOptions extends ErrorOptions {
/** Typed primary retained when a later lifecycle failure composes this error. */
readonly cleanupPrimary?: AnalysisError | undefined;
/** Producer-owned, caller-safe facts retained through every typed projection. */
readonly diagnostics?: Readonly<Record<string, JsonValue>> | undefined;
readonly limit?: AnalysisSettingLimit | undefined;
readonly continuingCallerGuidance?:
| AnalysisContinuingCallerGuidance
| undefined;
/**
* The same diagnostics recorded for an invocation caller, present only when
* an embedded child failure projects differently for that lifetime.
*/
readonly invocationDiagnostics?:
| Readonly<Record<string, JsonValue>>
| undefined;
readonly capturedOutput?: AnalysisCapturedOutput | undefined;
readonly cleanup?: AnalysisCleanupObservation | undefined;
readonly partialObservation?: AnalysisPartialObservation | undefined;
}
/** Exact cleanup owner that can attribute its currently unresolved resources. */
export interface AnalysisCleanupResourceOwner {
readonly cleanupResources: readonly string[] | undefined;
}
/** Result completed before its source Evidence could be published. */
export type SourcePublicationCompletion =
| {
readonly kind: "unretained-evidence";
readonly source_closure: "not-established";
readonly evidence: Evidence;
}
| {
readonly kind: "retained-evidence";
readonly source_closure: "not-established";
readonly evidence_id: string;
}
| {
readonly kind: "unretained-observation";
readonly source_closure: "not-established";
readonly observation: Readonly<Record<string, JsonValue>>;
}
| {
readonly kind: "inline-evidence";
readonly retention: "one-shot-session" | "unknown";
readonly source_closure: "not-established";
readonly evidence: Evidence;
}
| {
readonly kind: "omitted";
readonly source_closure: "not-established";
readonly reason:
| "primary-authentication-failed"
| "insufficient-authentication-headroom"
| "recovery-input-invalid";
};
export interface SourcePublicationRecovery {
readonly source: AnalysisPartialObservation | undefined;
readonly completed: SourcePublicationCompletion;
}
/** Base class for expected analysis, provider, and session failures. */
export abstract class AnalysisError extends Error {
limit: Readonly<AnalysisSettingLimit> | undefined;
/** Bind the accepted setting at its producing boundary. */
bindLimit(limit: AnalysisSettingLimit): this {
this.limit = Object.freeze({ ...limit });
return this;
}
readonly #cleanupPrimary: AnalysisError | undefined;
// Undefined means unbound; a bound empty set records confirmed completion.
#cleanupDependencies: Set<AnalysisError> | undefined;
#cleanupResourceProjection:
| { readonly cleanupResources: readonly string[] }
| undefined;
#cleanupDependents: WeakRef<AnalysisError>[] = [];
readonly diagnostics: Readonly<Record<string, JsonValue>> | undefined;
readonly continuingCallerGuidance:
| Readonly<AnalysisContinuingCallerGuidance>
| undefined;
readonly invocationDiagnostics:
| Readonly<Record<string, JsonValue>>
| undefined;
#capturedOutput: AnalysisCapturedOutput | undefined;
get capturedOutput(): AnalysisCapturedOutput | undefined {
return this.#capturedOutput;
}
readonly cleanup: AnalysisCleanupObservation | undefined;
cleanupIncomplete: boolean;
#ownedCleanupResources: readonly string[];
#cleanupResourceOwner: AnalysisCleanupResourceOwner | undefined;
#cleanupCompletionOwner: AnalysisCleanupResourceOwner | undefined;
#cleanupRetry: AnalysisCleanupRetry | undefined;
#secondaryFailures: AnalysisError[] = [];
constructor(message: string, options?: AnalysisErrorOptions) {
super(message, options);
this.#cleanupPrimary = options?.cleanupPrimary;
if (options?.limit !== undefined) this.bindLimit(options.limit);
this.diagnostics =
options?.diagnostics === undefined
? undefined
: structuredClone(options.diagnostics);
this.continuingCallerGuidance =
options?.continuingCallerGuidance === undefined
? undefined
: Object.freeze({ ...options.continuingCallerGuidance });
this.invocationDiagnostics =
options?.invocationDiagnostics === undefined
? undefined
: structuredClone(options.invocationDiagnostics);
this.#capturedOutput =
options?.capturedOutput === undefined
? undefined
: { ...options.capturedOutput };
this.cleanup = options?.cleanup;
this.cleanupIncomplete = options?.cleanup !== undefined;
this.#ownedCleanupResources = [...(options?.cleanup?.resources ?? [])];
this.partialObservation = options?.partialObservation;
}
abstract readonly _tag: AnalysisErrorTag;
readonly userMessage: string | undefined = undefined;
readonly userCategory: "cancelled" | undefined = undefined;
readonly executionFailure: string | undefined = undefined;
partialObservation: AnalysisPartialObservation | undefined;
sourcePublicationRecovery: SourcePublicationRecovery | undefined;
workflowUnknownPublication: WorkflowUnknownPublication | undefined;
/** Keep the failed source and completed result with their error owner. */
retainSourcePublication(completed: SourcePublicationCompletion): this {
this.sourcePublicationRecovery = {
source: this.partialObservation,
completed,
};
this.partialObservation = undefined;
return this;
}
get cleanupPrimary(): AnalysisError | undefined {
return this.#cleanupPrimary;
}
/** Later typed failures retained without changing the primary error cause. */
get secondaryFailures(): readonly AnalysisError[] {
return [...this.#secondaryFailures];
}
readonly cleanupReport: ProcessCaptureCleanupReport | undefined = undefined;
/** Live dependencies own current resource attribution; cleanup keeps history. */
get cleanupResources(): readonly string[] {
if (this.#cleanupDependencies === undefined)
return (
this.#cleanupResourceOwner?.cleanupResources ??
this.#ownedCleanupResources
);
const pending = [...this.#cleanupDependencies].filter(
(dependency) => dependency.cleanupIncomplete,
);
if (pending.length === 0) return [];
if (this.#cleanupResourceProjection !== undefined)
return this.#cleanupResourceProjection.cleanupResources;
return [
...new Set(pending.flatMap((dependency) => dependency.cleanupResources)),
];
}
/** Whether cleanup follows an explicit owner or dependent errors. */
get hasCleanupOwner(): boolean {
return (
this.#cleanupResourceOwner !== undefined ||
this.#cleanupCompletionOwner !== undefined ||
this.#cleanupDependencies !== undefined
);
}
/** Bind attribution only to the producer that owns the complete cleanup set. */
bindCleanupResourceOwner(owner: AnalysisCleanupResourceOwner): this {
this.#cleanupResourceOwner = owner;
return this;
}
/** Observe exact completion without replacing the producer's resource facts. */
bindCleanupCompletionOwner(owner: AnalysisCleanupResourceOwner): this {
this.#cleanupCompletionOwner = owner;
return this;
}
/** Caller retry for the live cleanup; undefined once cleanup completes. */
get cleanupRetry(): AnalysisCleanupRetry | undefined {
return this.cleanupIncomplete ? this.#cleanupRetry : undefined;
}
/** Bind only from the owner retaining every reported resource for this retry. */
bindCleanupRetry(retry: AnalysisCleanupRetry): this {
this.#cleanupRetry = Object.freeze({ ...retry });
return this;
}
/** Initialize resource facts supplied by a more specific error producer. */
protected setCleanupResources(resources: readonly string[]): void {
this.#ownedCleanupResources = [...resources];
}
/** Retain diagnostic facts without replacing the original typed error. */
retainDiagnostics(diagnostics: Readonly<Record<string, JsonValue>>): this {
Object.assign(this, {
diagnostics: structuredClone({ ...this.diagnostics, ...diagnostics }),
...(this.invocationDiagnostics === undefined
? {}
: {
invocationDiagnostics: structuredClone({
...this.invocationDiagnostics,
...diagnostics,
}),
}),
});
return this;
}
/** Retain facts on every primary that cleanup settlement can reveal. */
retainPrimaryDiagnostics(
diagnostics: Readonly<Record<string, JsonValue>>,
): this {
for (
let error: AnalysisError | undefined = this;
error !== undefined;
error = error.cleanupPrimary
)
error.retainDiagnostics(diagnostics);
return this;
}
/** Diagnostics recorded for a caller with this lifetime. */
diagnosticsFor(
callerLifetime: AnalysisCallerLifetime,
): Readonly<Record<string, JsonValue>> | undefined {
return callerLifetime === "invocation"
? (this.invocationDiagnostics ?? this.diagnostics)
: this.diagnostics;
}
retainSecondaryFailure(
failure: AnalysisError,
diagnostics?: Readonly<Record<string, JsonValue>>,
): this {
// Secondary resources remain owned by their cleanup lifecycle; preserve
// the primary error's live cleanup status and original output.
if (diagnostics !== undefined) this.retainDiagnostics(diagnostics);
this.#secondaryFailures.push(failure);
return this;
}
/** Keep a composed error incomplete until all explicitly owned source errors resolve. */
bindCleanupDependencies(
dependencies: readonly AnalysisError[],
resourceProjection?: { readonly cleanupResources: readonly string[] },
): this {
const pending = (this.#cleanupDependencies ??= new Set<AnalysisError>());
if (resourceProjection !== undefined)
this.#cleanupResourceProjection = resourceProjection;
for (const dependency of dependencies) {
if (!dependency.cleanupIncomplete || pending.has(dependency)) continue;
pending.add(dependency);
dependency.#cleanupDependents = [
...dependency.#cleanupDependents.filter(
(reference) => reference.deref() !== undefined,
),
new WeakRef(this),
];
}
if (pending.size === 0) this.resolveCleanup();
return this;
}
/** Clear live cleanup status only after its owning resource confirms retry success. */
resolveCleanup(): this {
if (
[...(this.#cleanupDependencies ?? [])].some(
(dependency) => dependency.cleanupIncomplete,
)
)
return this;
this.#cleanupDependencies?.clear();
this.cleanupIncomplete = false;
for (const reference of this.#cleanupDependents)
reference.deref()?.resolveCleanup();
this.#cleanupDependents = [];
return this;
}
/** Retain a completed observation when a later step fails. */
retainPartialObservation(observation: AnalysisPartialObservation): this {
this.partialObservation ??= observation;
return this;
}
/** Keep command output observed before this error without replacing the owner. */
retainCapturedOutput(output: AnalysisCapturedOutput): this {
this.#capturedOutput ??= { ...output };
return this;
}
/** Preserve the exact committed prefix when automatic question publication stops. */
retainWorkflowUnknownPublication(
publication: WorkflowUnknownPublication,
): this {
this.workflowUnknownPublication ??= Object.freeze({ ...publication });
return this;
}
}
/** Confirmed cleanup reveals the typed primary; unresolved ownership stays visible. */
export const resolvedAnalysisError = (error: AnalysisError): AnalysisError => {
while (!error.cleanupIncomplete && error.cleanupPrimary !== undefined)
error = error.cleanupPrimary;
return error;
};
/** Original operation semantics remain distinct from the live cleanup envelope. */
export const primaryAnalysisError = (error: AnalysisError): AnalysisError => {
while (error.cleanupPrimary !== undefined) error = error.cleanupPrimary;
return error;
};
/** Find the completed publication outcome owned anywhere in a cleanup chain. */
export const sourcePublicationRecoveryOf = (
error: AnalysisError,
): SourcePublicationRecovery | undefined => {
for (
let owner: AnalysisError | undefined = error;
owner !== undefined;
owner = owner.cleanupPrimary
)
if (owner.sourcePublicationRecovery !== undefined)
return owner.sourcePublicationRecovery;
return undefined;
};
import type {
PartialProcessCaptureObservation,
ProcessCaptureCleanupReport,
} from "./process/processCapture.js";
import type { FileOffsetPartialObservation } from "./native/fileOffsetPartialObservation.js";
import type { NativeCallPartialObservation } from "./native/nativeCallPartialObservation.js";
import type { HopperCallerReconciliationPartial } from "./native/hopperCallerReconciliation.js";
import type {
ElectronActiveObservationResult,
ElectronActivePartialObservation,
} from "./javascript/electronActiveObservation.js";
import type { JavaScriptRuntimeObservation } from "./javascript/javascriptRuntimeObservation.js";
import type {
ArtifactInventoryPartialObservation,
ArtifactExtractionPartialObservation,
} from "./artifactPartialObservation.js";
import type { WebScriptExportPartialObservation } from "./webScriptExport.js";
import type { JavaScriptRecoveryPartialObservation } from "./javascript/javascriptRecovery.js";
import type { BrowserScenarioPartialObservation } from "./browserScenarioCapture.js";
import type { Evidence } from "./evidence.js";
import type {
ElectronPageInspection,
ElectronPagePartialObservation,
} from "./javascript/electronObservation.js";
import type { AndroidPartialObservation } from "./android/androidPartialObservation.js";
import type { FirmwarePartialObservation } from "./firmware/firmwareAnalysis.js";
import type {
AdbActivityStartPartialObservation,
AdbAcquiredFilePartialObservation,
AdbPackagePullPartialObservation,
AdbDisplayPartialObservation,
} from "./adb/adbDeviceAnalysis.js";
import type {
InspectMacho,
InspectSignature,
} from "./native/nativeInspection.js";
import type { NativeUiObservationResult } from "./native/nativeUiObservation.js";
import type { BinaryLayout } from "./native/binaryLayout.js";
import type {
RecordedCrash,
RecordedCrashPartialObservation,
} from "./native/recordedCrash.js";
import type { WebPageInspection } from "./browserObservationSchemas.js";
import type {
WebBundleAcquisitionPartialObservation,
WebBundleAnalysis,
WebSourceMapFetchPartialObservation,
} from "./webBundleAnalysis.js";
import type { WebExecutionObservation } from "./webExecution.js";
import type { WebSourceMapReport } from "./webSourceLocation.js";
import type { WebEventListenersObservation } from "./webEventListeners.js";
import type { NativeLoadImagePartialObservation } from "./native/nativeLoadImage.js";
import type { NativeAotSnapshotObservation } from "./native/nativeAotSnapshotObservation.js";
import type { FileSnapshotPartialObservation } from "./fileSnapshotObservation.js";
import type { WebModuleResolutionObservation } from "./webModuleTrace.js";
import type { WebNetworkCapture } from "./webNetworkCapture.js";
import type { FridaDeviceList, FridaProcessList } from "./fridaDiscovery.js";
import type { CutterBridgeDiscovery } from "./cutterDiscovery.js";
import type { WebTraceObservation } from "./webTraceObservation.js";
import type { WebArtifactAcquisition } from "./webArtifactAcquisition.js";
/** Provider-neutral evidence collected before a typed analysis failure. */
export type AnalysisPartialObservation =
| InterfaceBuilderAnalysis
| PartialTaskMeasurement
| {
readonly kind: "omitted-observation";
readonly reason:
| "insufficient-authentication-headroom"
| "recovery-input-invalid";
}
| { readonly kind: "unretained-evidence"; readonly evidence: Evidence }
| { readonly kind: "retained-evidence"; readonly evidence_id: string }
| ApiContract
| GraphqlContract
| PlaywrightTrace
| SavedAnalysisSnapshotPartialObservation
| WebArtifactAcquisition
| WebTraceObservation
| CutterBridgeDiscovery
| FridaDeviceList
| FridaProcessList
| WebModuleResolutionObservation
| HistoricalSourceGraph
| WebNetworkCapture
| NativeLoadImagePartialObservation
| NativeAotSnapshotObservation
| FileSnapshotPartialObservation
| PartialProcessCaptureObservation
| NativeCallPartialObservation
| HopperCallerReconciliationPartial
| FileOffsetPartialObservation
| ElectronActivePartialObservation
| ElectronActiveObservationResult
| BrowserScenarioPartialObservation
| WebPageInspection
| WebBundleAnalysis
| WebBundleAcquisitionPartialObservation
| WebSourceMapFetchPartialObservation
| WebSourceMapReport
| WebExecutionObservation
| WebEventListenersObservation
| JavaScriptRuntimeObservation
| ArtifactInventoryPartialObservation
| ArtifactExtractionPartialObservation
| WebScriptExportPartialObservation
| EnvironmentBundlePublication
| JavaScriptRecoveryPartialObservation
| Evidence
| BinaryLayout
| RecordedCrash
| AppleIpsPartialObservation
| RecordedCrashPartialObservation
| AndroidPartialObservation
| FirmwarePartialObservation
| NativeUiObservationResult
| InspectSignature
| InspectMacho
| ElectronPagePartialObservation
| ElectronPageInspection
| AdbActivityStartPartialObservation
| AdbAcquiredFilePartialObservation
| AdbPackagePullPartialObservation
| AdbDisplayPartialObservation;