mirror of
https://github.com/morluto/rea
synced 2026-10-28 15:07:26 -04:00
272 lines
10 KiB
TypeScript
272 lines
10 KiB
TypeScript
import type { JsonValue } from "./jsonValue.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[];
|
|
}
|
|
|
|
/** 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 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;
|
|
}
|
|
|
|
/** Base class for expected analysis, provider, and session failures. */
|
|
export abstract class AnalysisError extends Error {
|
|
readonly #cleanupPrimary: AnalysisError | undefined;
|
|
// Undefined means unbound; a bound empty set records confirmed completion.
|
|
#cleanupDependencies: Set<AnalysisError> | undefined;
|
|
#cleanupDependents: WeakRef<AnalysisError>[] = [];
|
|
readonly diagnostics: Readonly<Record<string, JsonValue>> | undefined;
|
|
readonly capturedOutput: AnalysisCapturedOutput | undefined;
|
|
readonly cleanup: AnalysisCleanupObservation | undefined;
|
|
cleanupIncomplete: boolean;
|
|
#ownedCleanupResources: readonly string[];
|
|
#cleanupResourceOwner: AnalysisCleanupResourceOwner | undefined;
|
|
constructor(message: string, options?: AnalysisErrorOptions) {
|
|
super(message, options);
|
|
this.#cleanupPrimary = options?.cleanupPrimary;
|
|
this.diagnostics =
|
|
options?.diagnostics === undefined
|
|
? undefined
|
|
: structuredClone(options.diagnostics);
|
|
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;
|
|
|
|
get cleanupPrimary(): AnalysisError | undefined {
|
|
return this.#cleanupPrimary;
|
|
}
|
|
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
|
|
);
|
|
return [
|
|
...new Set(
|
|
[...this.#cleanupDependencies]
|
|
.filter((dependency) => dependency.cleanupIncomplete)
|
|
.flatMap((dependency) => dependency.cleanupResources),
|
|
),
|
|
];
|
|
}
|
|
|
|
/** Bind attribution only to the producer that owns the complete cleanup set. */
|
|
bindCleanupResourceOwner(owner: AnalysisCleanupResourceOwner): this {
|
|
this.#cleanupResourceOwner = owner;
|
|
return this;
|
|
}
|
|
|
|
/** Initialize resource facts supplied by a more specific error producer. */
|
|
protected setCleanupResources(resources: readonly string[]): void {
|
|
this.#ownedCleanupResources = [...resources];
|
|
}
|
|
|
|
/** Retain a later lifecycle failure without replacing the original typed error. */
|
|
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.
|
|
Object.assign(this, {
|
|
diagnostics: structuredClone({ ...this.diagnostics, ...diagnostics }),
|
|
cause: new AggregateError(
|
|
this.cause === undefined ? [failure] : [this.cause, failure],
|
|
"Analysis failed and a later lifecycle operation also failed.",
|
|
{ cause: this.cause },
|
|
),
|
|
});
|
|
return this;
|
|
}
|
|
|
|
/** Keep a composed error incomplete until all explicitly owned source errors resolve. */
|
|
bindCleanupDependencies(dependencies: readonly AnalysisError[]): this {
|
|
const pending = (this.#cleanupDependencies ??= new Set<AnalysisError>());
|
|
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;
|
|
}
|
|
}
|
|
|
|
/** 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;
|
|
};
|
|
import type {
|
|
PartialProcessCaptureObservation,
|
|
ProcessCaptureCleanupReport,
|
|
} from "./process/processCapture.js";
|
|
import type { FileOffsetPartialObservation } from "./native/fileOffsetPartialObservation.js";
|
|
import type { NativeCallPartialObservation } from "./native/nativeCallPartialObservation.js";
|
|
import type { 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 { ElectronPagePartialObservation } from "./javascript/electronObservation.js";
|
|
import type { AndroidPartialObservation } from "./android/androidPartialObservation.js";
|
|
import type { FirmwarePartialObservation } from "./firmware/firmwareAnalysis.js";
|
|
import type {
|
|
AdbActivityStartPartialObservation,
|
|
AdbAcquiredFilePartialObservation,
|
|
AdbPackagePullPartialObservation,
|
|
} from "./adb/adbDeviceAnalysis.js";
|
|
import type { 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 { WebExecution } from "./webExecution.js";
|
|
import type { WebEventListeners } 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";
|
|
|
|
/** Provider-neutral evidence collected before a typed analysis failure. */
|
|
export type AnalysisPartialObservation =
|
|
| WebModuleResolutionObservation
|
|
| NativeLoadImagePartialObservation
|
|
| NativeAotSnapshotObservation
|
|
| FileSnapshotPartialObservation
|
|
| PartialProcessCaptureObservation
|
|
| NativeCallPartialObservation
|
|
| FileOffsetPartialObservation
|
|
| ElectronActivePartialObservation
|
|
| BrowserScenarioPartialObservation
|
|
| WebPageInspection
|
|
| WebExecution
|
|
| WebEventListeners
|
|
| JavaScriptRuntimeObservation
|
|
| ArtifactInventoryPartialObservation
|
|
| ArtifactExtractionPartialObservation
|
|
| WebScriptExportPartialObservation
|
|
| JavaScriptRecoveryPartialObservation
|
|
| Evidence
|
|
| BinaryLayout
|
|
| RecordedCrash
|
|
| RecordedCrashPartialObservation
|
|
| AndroidPartialObservation
|
|
| FirmwarePartialObservation
|
|
| NativeUiObservationResult
|
|
| InspectSignature
|
|
| ElectronPagePartialObservation
|
|
| AdbActivityStartPartialObservation
|
|
| AdbAcquiredFilePartialObservation
|
|
| AdbPackagePullPartialObservation;
|