Files
reverse-engineering-anything/src/application/AnalysisProvider.ts
T
morluto 150e996a86 refactor(providers)!: await owned readiness observations
Carry asynchronous readiness through provider selection, session status, CLI and MCP. Join observations during cancellation and shutdown, keep status readable during finalization, and retain mixed provider errors and exact cleanup dependencies through the shared inspection failure owner.

Validation: 255 focused tests across 18 files; 21 registry tests rerun with primary restoration assertion; check:fast; docs:check and 5 README conformance tests. Registered MCP and SDK schema checks use controlled providers; no real Ghidra/JVM verification.

This is a prerequisite for #4561. The synchronous Ghidra Java producer and its doctor/client ownership migration remain unfinished.

BREAKING CHANGE: binary_session candidate availability can be unknown during owner finalization. Consumers must handle this state; the internal session status API is now asynchronous.

Refs: #4561
2026-10-17 09:57:43 +08:00

318 lines
12 KiB
TypeScript

import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { OfficialToolName } from "../contracts/officialToolContracts.js";
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js";
import type { ManagedToolName } from "../contracts/managed/managedToolContracts.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { Result } from "../domain/result.js";
import type { EvidenceLocation } from "../domain/evidence.js";
import type { EvidenceSubjectTarget } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import type { ProgressReporter } from "./ProgressReporter.js";
import type { ProviderRejectionCode } from "../contracts/providerSelection.js";
import type { ProcessLineageObservation } from "../process/ProcessOwnership.js";
import type { ProviderOperationHealth } from "../domain/providerOperationHealth.js";
import {
relationshipCoverageSchema,
type RelationshipCoverage,
} from "../domain/relationshipCoverage.js";
export interface ExecutionOptions {
readonly signal?: AbortSignal;
readonly progress?: ProgressReporter;
/**
* Set when a procedure selector came from provider output, such as a
* revisited callee, rather than from the caller. A provider-derived
* selector is never the caller's input mistake. Absent means the caller
* supplied it.
*/
readonly selectorOrigin?: "provider";
}
/** Closed operations accepted by provider-backed analysis sessions. */
export type AnalysisOperation =
| OfficialToolName
| EnhancedToolName
| NativeToolName
| ArtifactAnalysisOperation
| ManagedToolName
| "health";
/** Atomic successful provider observation with exact producing provenance. */
export interface AnalysisExecution {
readonly result: JsonValue;
readonly rawResult: JsonValue | null;
readonly provider: ProviderIdentity;
readonly analysisProfile?: AnalysisProfileCommitment;
readonly limitations: readonly string[];
/** Provider-owned completeness assessment for caller/callee observations. */
readonly relationshipCoverage?: RelationshipCoverage;
/**
* Whether the provider capped an `xrefs` read: true when it reported a
* truncation, false when it explicitly reported none. Absent when it reports
* nothing, which is not a completeness claim. Never part of Evidence or tool
* output.
*/
readonly referencesTruncated?: boolean;
readonly locations: readonly EvidenceLocation[];
readonly subject: EvidenceSubjectTarget | null;
}
interface CreateAnalysisExecutionOptions {
readonly rawResult?: unknown;
readonly analysisProfile?: AnalysisProfileCommitment;
readonly limitations?: readonly string[];
readonly relationshipCoverage?: RelationshipCoverage;
readonly referencesTruncated?: boolean;
readonly locations?: readonly EvidenceLocation[];
readonly subject?: EvidenceSubjectTarget;
}
/**
* Build a validated atomic provider observation at an adapter boundary.
*
* Pass `rawResult` only for a provider representation distinct from
* `result`. Omitting it records `null` rather than a second complete copy of
* the normalized result in every Evidence record and tool response.
*/
export function createAnalysisExecution<Subject extends EvidenceSubjectTarget>(
result: unknown,
provider: ProviderIdentity,
options: CreateAnalysisExecutionOptions & { readonly subject: Subject },
): Omit<AnalysisExecution, "subject"> & { readonly subject: Subject };
export function createAnalysisExecution(
result: unknown,
provider: ProviderIdentity,
options?: CreateAnalysisExecutionOptions,
): AnalysisExecution;
export function createAnalysisExecution(
result: unknown,
provider: ProviderIdentity,
options: CreateAnalysisExecutionOptions = {},
): AnalysisExecution {
return {
result: jsonValueSchema.parse(result),
rawResult:
options.rawResult === undefined
? null
: jsonValueSchema.parse(options.rawResult),
provider,
...(options.analysisProfile === undefined
? {}
: { analysisProfile: structuredClone(options.analysisProfile) }),
limitations: [...(options.limitations ?? [])],
...(options.relationshipCoverage === undefined
? {}
: {
relationshipCoverage: relationshipCoverageSchema.parse(
options.relationshipCoverage,
),
}),
...(options.referencesTruncated === undefined
? {}
: { referencesTruncated: options.referencesTruncated }),
locations: [...(options.locations ?? [])],
subject: options.subject ?? null,
};
}
/** Provider-neutral application capability for one analysis operation. */
export interface AnalysisOperationPort {
execute(
operation: AnalysisOperation,
parameters: Readonly<Record<string, JsonValue>>,
options?: ExecutionOptions,
): Promise<Result<AnalysisExecution, AnalysisError>>;
}
/** Retained provider-process observation attributed to its producing provider. */
export interface ProviderRuntimeLineageSnapshot {
readonly provider: ProviderIdentity;
readonly observation: ProcessLineageObservation;
}
/** One provider's current serial-request activity, including detached callers. */
export interface ProviderRequestActivitySnapshot {
readonly provider: ProviderIdentity;
readonly active: {
readonly requestId: number;
readonly operation: string;
readonly elapsedMs: number;
readonly callerState: "waiting" | "cancelled";
} | null;
readonly queuedRequests: number;
}
/** Provider session bound to exactly one parsed artifact. */
export interface AnalysisClient extends AnalysisOperationPort {
/** Execute only if this operation can run without loading or starting a provider. */
executeIfReady?(
operation: AnalysisOperation,
parameters: Readonly<Record<string, JsonValue>>,
options?: ExecutionOptions,
): Promise<Result<AnalysisExecution, AnalysisError> | undefined>;
/** Retained token-verified process snapshots for dynamic providers this client started. */
runtimeLineageSnapshots?(): readonly ProviderRuntimeLineageSnapshot[];
/** Current provider work that may outlive a timed-out or cancelled caller. */
requestActivitySnapshots?(): readonly ProviderRequestActivitySnapshot[];
/** Current operational health for the active provider bridge. */
operationHealthSnapshot?(): ProviderOperationHealth | undefined;
/** Close owned resources and retain failed cleanup for a later retry. */
close(
options?: Pick<ExecutionOptions, "progress"> & {
readonly retainDocument?: boolean;
},
): Promise<Result<null, AnalysisError>>;
}
/** Per-client identity allocated before any provider side effect starts. */
export interface AnalysisClientContext {
readonly runId: string;
}
/** One lifecycle owner for descriptor cleanup and abortable inspection work. */
export interface FileCloseResourceOwner {
retain(owner: { close(): Promise<void> }, resource: string): void;
run<T>(operation: () => Promise<T>): Promise<T>;
}
export interface ProviderIdentity {
readonly id: string;
readonly name: string;
readonly version: string | null;
}
/** Typed analysis profile resolved before provider startup; null means unresolved. */
export interface AnalysisProfileResolution {
readonly profile: AnalysisProfileCommitment | null;
}
/** Cancellation context for side-effect-free provider profile resolution. */
export interface AnalysisProfileResolutionOptions {
readonly signal?: AbortSignal;
readonly fileCloseResources?: FileCloseResourceOwner;
}
interface CapabilityEffects {
readonly mutatesArtifact: boolean;
readonly launchesProcess: boolean;
readonly mayShowUi: boolean;
readonly mayAccessNetwork: boolean;
readonly mayWriteFilesystem: boolean;
readonly changesPermissions: boolean;
readonly requiresRoot: boolean;
}
type CapabilityAvailability =
| {
readonly available: true;
readonly reason: null;
readonly availabilityCode?: null;
}
| {
readonly available: false;
readonly reason: string;
readonly availabilityCode?: ProviderRejectionCode;
};
export type CapabilityDescriptor = CapabilityAvailability & {
/**
* Snapshot opts immutable, profile-bound reads into replay despite incidental
* private filesystem writes. Live state must always be re-observed. Omitting
* this policy retains conservative effect checks; mutation and UI-state
* exclusions apply to every policy.
*/
readonly cachePolicy?: "snapshot" | "live";
readonly provider: ProviderIdentity;
readonly operation: Exclude<AnalysisOperation, "health">;
readonly effects: CapabilityEffects;
readonly limitations: readonly string[];
};
/** Factory and capability declaration for an analysis implementation. */
export interface AnalysisProvider {
identity(): ProviderIdentity;
capabilities(): readonly CapabilityDescriptor[];
resolveAnalysisProfile?(
target: BinaryTarget,
options?: AnalysisProfileResolutionOptions,
): Promise<Result<AnalysisProfileResolution, AnalysisError>>;
createClient(
target: BinaryTarget,
profile?: AnalysisProfileCommitment,
context?: AnalysisClientContext,
): AnalysisClient;
}
/** Bounded host observation made without starting an analysis process. */
export type ProviderAvailability =
| {
readonly status: "available";
readonly code: null;
readonly reason: null;
readonly diagnostics: Readonly<Record<string, JsonValue>>;
}
| {
readonly status: "unavailable";
readonly code: ProviderRejectionCode;
readonly reason: string;
readonly diagnostics: Readonly<Record<string, JsonValue>>;
};
/** Provider-owned target support observation before profile resolution. */
export type ProviderTargetSupport =
| {
readonly status: "supported";
readonly code: null;
readonly reason: null;
readonly diagnostics: Readonly<Record<string, JsonValue>>;
}
| {
readonly status: "unsupported";
readonly code:
| "target_kind_unsupported"
| "target_format_unsupported"
| "architecture_unsupported"
| "target_role_unsupported"
| "managed_target_unsupported";
readonly reason: string;
readonly diagnostics: Readonly<Record<string, JsonValue>>;
};
/** One owner-observed availability/capability snapshot for provider discovery. */
export interface AnalysisProviderReadinessSnapshot {
readonly availability: ProviderAvailability;
readonly capabilities: readonly CapabilityDescriptor[];
}
/** Deep provider candidate discoverable before one target-bound route is chosen. */
export interface AnalysisProviderCandidate extends AnalysisProvider {
/**
* Own inspection through final reads and cleanup. Observe the supplied owner
* signal cooperatively; resolve only after finalization settles, retaining any
* failed cleanup capability. Registry cancellation and shutdown join this
* completion rather than abandoning the promise.
*/
resolveAnalysisProfile(
target: BinaryTarget,
options?: AnalysisProfileResolutionOptions,
): Promise<Result<AnalysisProfileResolution, AnalysisError>>;
/** Await prerequisite observations and finalization under the supplied owner signal. */
inspectAvailability(
signal?: AbortSignal,
): ProviderAvailability | Promise<ProviderAvailability>;
inspectReadinessSnapshot?(
signal?: AbortSignal,
):
| AnalysisProviderReadinessSnapshot
| Promise<AnalysisProviderReadinessSnapshot>;
inspectTargetSupport(target: BinaryTarget): ProviderTargetSupport;
/** Observe retained candidate cleanup without starting or retrying work. */
hasPendingOwnedResources?(): boolean;
/** Release candidate resources that are independent of any active client. */
closeOwnedResources?(): Promise<Result<null, AnalysisError>>;
}