From e37f07b725de3e8f4e7045b03cb4005a7a025ea9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 06:47:48 +0000 Subject: [PATCH 1/6] refactor(contracts): declare the public API vocabulary below its consumers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The layering gate's largest remaining cluster was 28 type-only inversions from a single edge: `commands/` declaring itself in terms of `client/client-types.ts`. R2 forbids the reverse import, so a shape both surfaces need has to sit below both. The command/device vocabulary — connection config, the device and session views, and every per-command Options/Result — now lives in `contracts/client-api.ts`; `client/client-types.ts` keeps the `AgentDeviceClient` facade and re-exports the rest through one wildcard. R6 total: 42 -> 18. No new inversion in any pair. The published surface is unchanged, and that is verified rather than asserted: the built `index.d.ts` exports the same 216 type names as main, byte-identical. Eight shapes deliberately did NOT move, because each is stated in terms of a HIGHER-ranked zone: `ScrollOptions` (ScrollInputDirection, commands/), the four navigation Options plus `AgentDeviceCommandClient` (navigation-projection, commands/), and the two Metro result aliases (metro/). Declaring those in contracts/ would trade 28 commands->client edges for contracts->commands and contracts->metro ones — the foundation depending on the layers above it, worse in kind even though fewer in number. This is measured, not assumed: moving the whole file to contracts/ first took the gate from 42 to 48, which is how the floor was found. Two keystone moves made the other 84 movable: - `RemoteConnectionProfileFields` joined its sibling `CloudProviderProfileFields` in contracts/remote-config-fields.ts. It was the root of the base chain (AgentDeviceClientConfig -> AgentDeviceRequestOverrides -> DeviceCommandBaseOptions -> every per-command Options), so one rank-4 declaration was pinning ~80 shapes up with it. - `DaemonBatchStep` moved to contracts/batch-step.ts. Its `runtime` field was written `DaemonRequest['runtime']`, dragging the whole daemon request type in to say `SessionRuntimeHints` — the same type, three zones lower. `CompanionTunnelScope`/`MetroBridgeScope` also moved to contracts/, since the vocabulary needs the scope shape and it sat next to client-local env-var names. Six pass-through re-exports in client-types.ts are suppressed per-name with the reason inline: they exist only to publish contracts/kernel types through the package entrypoint wildcard, every internal consumer imports them from the declaring module, so "no consumer" is correct and not actionable — deleting them would remove names from the public types. `pnpm check` green, 4488 unit tests. Findings doc records the sequencing for the last 5: the upstream declarations have to come down before the shapes that need them can. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur --- docs/dependency-graph-findings.md | 49 +- scripts/layering/check.ts | 28 +- src/__tests__/cli-client-commands.test.ts | 2 +- src/agent-device-client.ts | 10 +- src/cli.ts | 2 +- src/cli/batch-steps.ts | 2 +- src/cli/commands/connection-runtime.ts | 2 +- .../client-companion-tunnel-contract.ts | 14 +- src/client/client-normalizers.ts | 2 +- src/client/client-types.ts | 1147 ++--------------- src/commands/__tests__/command-flags.test.ts | 2 +- src/commands/batch/index.ts | 2 +- src/commands/capture/alert.ts | 2 +- src/commands/capture/output.test.ts | 2 +- src/commands/capture/output.ts | 2 +- src/commands/capture/screenshot.ts | 2 +- src/commands/capture/settings.ts | 2 +- src/commands/capture/wait.ts | 2 +- src/commands/cli-grammar/common.ts | 2 +- src/commands/cli-grammar/types.ts | 2 +- src/commands/command-flags.ts | 2 +- src/commands/command-input.ts | 2 +- src/commands/interaction/index.ts | 4 +- src/commands/interaction/interactions.ts | 2 +- src/commands/interaction/output.ts | 2 +- src/commands/interaction/selectors.ts | 2 +- src/commands/management/app.ts | 2 +- src/commands/management/output.test.ts | 2 +- src/commands/management/output.ts | 2 +- src/commands/management/push.ts | 2 +- src/commands/management/viewport.ts | 2 +- src/commands/metro/index.ts | 8 +- src/commands/observability/index.ts | 2 +- src/commands/perf/index.ts | 2 +- src/commands/perf/output.ts | 2 +- src/commands/recording/index.ts | 2 +- src/commands/recording/output.ts | 2 +- src/commands/system/index.ts | 2 +- src/contracts/batch-step.ts | 17 + src/contracts/client-api.ts | 1053 +++++++++++++++ src/contracts/companion-tunnel-scope.ts | 18 + src/contracts/remote-config-fields.ts | 32 + src/contracts/result-serialization.ts | 2 +- src/core/batch.ts | 12 +- .../__tests__/snapshot-handler.test.ts | 2 +- .../__tests__/command-tools-parity.test.ts | 3 +- src/mcp/command-tools.ts | 3 +- src/remote/remote-config-schema.ts | 26 +- .../integration/provider-scenarios/harness.ts | 6 +- test/output-economy/fixtures.ts | 5 +- test/output-economy/routine-workflow.ts | 3 +- 51 files changed, 1363 insertions(+), 1141 deletions(-) create mode 100644 src/contracts/batch-step.ts create mode 100644 src/contracts/client-api.ts create mode 100644 src/contracts/companion-tunnel-scope.ts diff --git a/docs/dependency-graph-findings.md b/docs/dependency-graph-findings.md index 1f786332d..ddc8f6126 100644 --- a/docs/dependency-graph-findings.md +++ b/docs/dependency-graph-findings.md @@ -45,15 +45,41 @@ type-only inversions, R7 pins SessionState field ownership, and the shared selec `TYPE_INVERSION_BASELINE` in `scripts/layering/check.ts` holds both, with the reasoning inline. -**28 + 1 edges → `client/client-types.ts`** (1236 lines). The file does three jobs: the public -Node-client facade (`AgentDeviceClient`, config, transport), the per-command `*Options`/`*Result` -vocabulary that `commands/`, `contracts/` and `mcp/` all need, and a re-export hub for 24 -`contracts/` types. Only the first is client-owned. The fix is the pattern the file already uses -for those 24 re-exports: declare the command I/O vocabulary in `contracts/` and re-export it here, -so the public surface stays byte-identical. Note the coupling is mutual — client-types imports -`commands/interaction/runtime/gestures.ts` and `commands/system/navigation-projection.ts` — which -is what closes the 5-node type cycle in §4. This is also the deferred "Node client result types" -work `CONTEXT.md` already tracks. +**28 + 1 edges → `client/client-types.ts`** — *done, mostly.* Now 5 edges. The vocabulary moved to +`contracts/client-api.ts`, with `client/client-types.ts` keeping the `AgentDeviceClient` facade and +re-exporting the rest through one wildcard, so the published `.d.ts` surface is byte-identical +(verified by diffing the built `index.d.ts` name set against `main`: 216 names, no change). + +The mutual coupling this section already warned about is what set the floor. Eight shapes could NOT +move down, because each is stated in terms of a HIGHER-ranked zone: + +| Shape(s) | Blocked by | +|---|---| +| `ScrollOptions` | `ScrollInputDirection` (`commands/interaction/runtime/gestures.ts`) | +| `BackCommandOptions`, `OrientationCommandOptions`, `AppSwitcherCommandOptions`, `TvRemoteCommandOptions`, `AgentDeviceCommandClient` | `NavigationCommandOptions` / `ProjectedNavigationCommandClient` (`commands/system/navigation-projection.ts`) | +| `MetroPrepareResult`, `MetroReloadResult` | `PrepareMetroRuntimeResult` / `ReloadMetroResult` (`metro/client-metro.ts`) | + +Declaring those in `contracts/` would have traded 28 `commands -> client` inversions for +`contracts -> commands` and `contracts -> metro` ones — the foundation depending on the layers above +it, which is worse in kind even though it is fewer edges. Measured, not assumed: the first attempt +put the whole file in `contracts/` and the gate went from 42 to **48**. + +Two keystone moves made the other 84 shapes movable, and both are worth noting as a pattern: + +- `RemoteConnectionProfileFields` joined its sibling `CloudProviderProfileFields` in + `contracts/remote-config-fields.ts`. It was the root of the base chain + (`AgentDeviceClientConfig` → `AgentDeviceRequestOverrides` → `DeviceCommandBaseOptions` → every + per-command `*Options`), so one rank-4 declaration was pinning ~80 shapes up with it. +- `DaemonBatchStep` moved to `contracts/batch-step.ts`. Its `runtime` field was written as + `DaemonRequest['runtime']`, which dragged the whole daemon request type in to say + `SessionRuntimeHints` — the same type, three zones lower. + +**Remaining `commands -> client` (5) needs the upstream declarations to come down first**: move +`ScrollInputDirection` and the navigation-projection types out of `commands/`, and the Metro +prepare/reload result payloads out of `metro/`. Each is small; the sequencing is the point. The +`mcp -> client` edge is different in kind — it is the `AgentDeviceClient` facade itself, i.e. the +question of whether a command surface should know the client type. That is a design decision, not a +misplaced declaration. **5 + 1 edges → `daemon/daemon-command-registry.ts` and `daemon/types.ts`.** `core`'s descriptor registry composes the ADR 0003 daemon facet, whose shape the daemon declares. ADR 0003's @@ -218,8 +244,9 @@ Still duplicated across zones, each needing the same treatment: `fill requires t 1. **Move the 10 outward-facing `daemon/types.ts` types into `contracts/`** (§2). Mechanical, and it clears most of §1's second cluster. -2. **Split `client/client-types.ts`** (§1). Largest remaining inversion cluster, closes the 5-node - type cycle, and it is already-tracked deferred work. +2. ~~**Split `client/client-types.ts`** (§1).~~ Done — 42 → 18 total inversions. The follow-up is + the upstream moves that unblock the last 5 (§1): `ScrollInputDirection` and the + navigation-projection types out of `commands/`, Metro result payloads out of `metro/`. 3. **Retire platform branches into plugin facets** (§5b), highest-count files first. 4. **Share the remaining duplicated validators** (§6), following the `checkIsArgs` shape. 5. Optional: give `daemon/handlers/` the directory structure its filenames already imply (§5). diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index e2171aff1..e171c671f 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -189,29 +189,31 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): Violation[] { // R6 ratchet: type-only spine inversions, per zone pair. R5 cannot see these (a // type-only import is free at runtime), but "zone A is declared in terms of zone B" -// is still a boundary claim, and ranking type edges surfaced 61 of them. Two clusters -// remain, each needing its own change rather than a file move: +// is still a boundary claim, and ranking type edges surfaced 61 of them. Down to 18 +// after the public API vocabulary moved to contracts/client-api.ts. What remains: // -// commands/contracts/mcp the per-command Options/Result vocabulary, plus the client -// -> client facade itself, are declared inside the 1.2k-line public -// Node-client surface (client/client-types.ts) instead of in -// contracts/, where the 24 types it already re-exports live. +// commands -> client (5) the eight vocabulary shapes that could NOT move down, +// because each is stated in terms of a HIGHER zone: +// `commands/` (navigation projection, ScrollInputDirection) +// or `metro/` (prepare/reload results). Moving them needs +// those upstream declarations to come down first. +// mcp -> client (1) the AgentDeviceClient facade itself — a real design +// question (should a command surface know the client +// type?), not a declaration in the wrong place. // core/commands -> daemon-server the ADR 0003 daemon facet: core's descriptor registry -// composes a shape the daemon owns. The daemon keeps the +// (6) composes a shape the daemon owns. The daemon keeps the // VALUES; only the shape needs to move below core. -// replay -> daemon-server `SessionAction`, the recorded-action shape replay reads and -// writes. It belongs in contracts/, but it references -// `CommandFlags` (core) which references `DaemonBatchStep` -// (core), so it moves as a chain of three, not one file. +// replay -> daemon-server (6) `SessionAction`, the recorded-action shape replay reads +// and writes. It belongs in contracts/, but it references +// `CommandFlags` (core), so it moves as a chain. // // The counts may only go DOWN. Fixing edges without lowering the number fails too, so the // baseline cannot quietly stop describing the tree. // Exported so scripts/depgraph can assert its own graph build reproduces it — see the // baseline-parity test there. The gate remains the authority; the report follows. export const TYPE_INVERSION_BASELINE: Readonly> = { - 'commands -> client': 28, + 'commands -> client': 5, 'commands -> daemon-server': 1, - 'contracts -> client': 1, 'core -> daemon-server': 5, 'mcp -> client': 1, 'replay -> daemon-server': 6, diff --git a/src/__tests__/cli-client-commands.test.ts b/src/__tests__/cli-client-commands.test.ts index b7996c386..c788412c7 100644 --- a/src/__tests__/cli-client-commands.test.ts +++ b/src/__tests__/cli-client-commands.test.ts @@ -13,7 +13,7 @@ import type { MetroPrepareOptions, MetroReloadOptions, } from '../agent-device-client.ts'; -import type { SettingsUpdateOptions } from '../client/client-types.ts'; +import type { SettingsUpdateOptions } from '../contracts/client-api.ts'; import { AppError } from '../kernel/errors.ts'; import { resolveCliOptions } from '../cli/resolve-cli-options.ts'; diff --git a/src/agent-device-client.ts b/src/agent-device-client.ts index 9132f8d4c..65675ce49 100644 --- a/src/agent-device-client.ts +++ b/src/agent-device-client.ts @@ -34,8 +34,6 @@ import { import { readScreenshotResultData } from './utils/screenshot-result.ts'; import { isRecord } from './utils/parsing.ts'; import type { - AgentDeviceCommandClient, - AgentDeviceClient, AgentDeviceClientConfig, AgentDeviceDaemonTransport, AppCloseOptions, @@ -52,16 +50,20 @@ import type { Lease, MaterializationReleaseOptions, MetroPrepareOptions, - MetroPrepareResult, PanOptions, FlingOptions, - RotateCommandResult, SwipeGestureOptions, PinchOptions, RotateGestureOptions, SessionSaveScriptOptions, SessionSaveScriptResult, TransformGestureOptions, +} from './contracts/client-api.ts'; +import type { + AgentDeviceCommandClient, + AgentDeviceClient, + MetroPrepareResult, + RotateCommandResult, } from './client/client-types.ts'; import type { OrientationCommandResult } from './contracts/navigation.ts'; import type { CommandResult } from './core/command-descriptor/command-result.ts'; diff --git a/src/cli.ts b/src/cli.ts index fb7454aab..1a99db806 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,7 +7,7 @@ import { readVersion } from './utils/version.ts'; import { pathToFileURL } from 'node:url'; import { sendToDaemon } from './daemon/client/daemon-client.ts'; import fs from 'node:fs'; -import type { BatchStep } from './client/client-types.ts'; +import type { BatchStep } from './contracts/client-api.ts'; import type { ReplayTestReporterRuntime } from './replay/test/reporting.ts'; import { createAgentDeviceClient, diff --git a/src/cli/batch-steps.ts b/src/cli/batch-steps.ts index 6c351fe34..3753b9a86 100644 --- a/src/cli/batch-steps.ts +++ b/src/cli/batch-steps.ts @@ -1,4 +1,4 @@ -import type { BatchStep } from '../client/client-types.ts'; +import type { BatchStep } from '../contracts/client-api.ts'; import { type SessionRuntimeHints } from '../kernel/contracts.ts'; import { parseBatchStepRuntime } from '../contracts/batch-contract.ts'; import { readInputFromCli } from '../commands/cli-grammar.ts'; diff --git a/src/cli/commands/connection-runtime.ts b/src/cli/commands/connection-runtime.ts index 6419d18fb..adedd3aa0 100644 --- a/src/cli/commands/connection-runtime.ts +++ b/src/cli/commands/connection-runtime.ts @@ -21,7 +21,7 @@ import { type RemoteConnectionRequestMetadata, } from '../../remote/remote-connection-state.ts'; import { profileToCliFlags } from '../remote-config-flags.ts'; -import type { BatchStep } from '../../client/client-types.ts'; +import type { BatchStep } from '../../contracts/client-api.ts'; import { AppError } from '../../kernel/errors.ts'; import type { LeaseBackend, SessionRuntimeHints } from '../../kernel/contracts.ts'; import type { CliFlags } from '../../contracts/cli-flags.ts'; diff --git a/src/client/client-companion-tunnel-contract.ts b/src/client/client-companion-tunnel-contract.ts index 1380554b1..4e7d67177 100644 --- a/src/client/client-companion-tunnel-contract.ts +++ b/src/client/client-companion-tunnel-contract.ts @@ -17,13 +17,13 @@ export const ENV_COMPANION_TUNNEL_UNREGISTER_PATH = 'AGENT_DEVICE_COMPANION_TUNN export const ENV_COMPANION_TUNNEL_DEVICE_PORT = 'AGENT_DEVICE_COMPANION_TUNNEL_DEVICE_PORT'; export const ENV_COMPANION_TUNNEL_SESSION = 'AGENT_DEVICE_COMPANION_TUNNEL_SESSION'; -export type CompanionTunnelScope = { - tenantId: string; - runId: string; - leaseId: string; -}; - -export type MetroBridgeScope = CompanionTunnelScope; +// The scope SHAPE is declared in contracts/ so zones that only need the shape do not have to +// declare themselves in terms of client/. Re-exported here for this module's existing consumers. +export type { + CompanionTunnelScope, + MetroBridgeScope, +} from '../contracts/companion-tunnel-scope.ts'; +import type { CompanionTunnelScope } from '../contracts/companion-tunnel-scope.ts'; export class MissingCompanionEnvError extends Error { override name = 'MissingCompanionEnvError'; diff --git a/src/client/client-normalizers.ts b/src/client/client-normalizers.ts index 46b0d4b71..1730128b7 100644 --- a/src/client/client-normalizers.ts +++ b/src/client/client-normalizers.ts @@ -19,7 +19,7 @@ import type { InternalRequestOptions, MaterializationReleaseResult, StartupPerfSample, -} from './client-types.ts'; +} from '../contracts/client-api.ts'; import type { TargetShutdownResult } from '../contracts/target-shutdown-contract.ts'; import { asRecord, diff --git a/src/client/client-types.ts b/src/client/client-types.ts index 0dc42642c..018b374e6 100644 --- a/src/client/client-types.ts +++ b/src/client/client-types.ts @@ -1,72 +1,140 @@ -import type { PublicSnapshotCaptureAnnotations } from '../contracts/snapshot-capture-annotations.ts'; -import type { SnapshotDiagnosticsSummary } from '../contracts/snapshot-diagnostics.ts'; -import type { - DaemonResponseData, - DaemonInstallSource, - DaemonLockPolicy, - DaemonRequest, - DaemonResponse, - LeaseBackend, - NetworkIncludeMode, - ResponseLevel, - SessionIsolationMode, - SessionRuntimeHints, -} from '../kernel/contracts.ts'; +// The published Node client surface. +// +// The VOCABULARY it is stated in terms of — connection config, the device/session views, and every +// per-command Options/Result shape — lives in contracts/client-api.ts, below both this zone and +// `commands/`. R2 forbids the reverse import, so a shape both surfaces need has to sit below both. +// +// What is still declared HERE is the `AgentDeviceClient` facade plus the shapes that are themselves +// stated in terms of a HIGHER-ranked zone — `commands/` (navigation projection, ScrollInputDirection) +// and `metro/` (prepare/reload results). Declaring those in contracts/ would trade 28 +// commands->client inversions for contracts->commands and contracts->metro ones: the foundation +// depending on the layers above it, which is worse. They can move once their upstream declarations +// do — see docs/dependency-graph-findings.md. +// +// The published surface is assembled in agent-device-client.ts, which re-exports BOTH homes, so no +// name became unreachable from the package entrypoint. + +// The relocated vocabulary keeps its published import path through this module: one wildcard +// rather than 84 named re-exports, so no name is published twice under two spellings. +export type * from '../contracts/client-api.ts'; + +// Contracts/kernel types re-exported into the PUBLISHED surface: `agent-device-client.ts` picks +// these up via `export type *`, and that is their only job — every internal consumer imports +// them from the declaring module instead. Fallow therefore sees no consumer, which is exactly +// right and exactly not actionable: deleting them would remove names from the package's public +// types. Suppressed per name rather than baselined so the reason travels with the code. +// fallow-ignore-next-line unused-type +export type { TargetShutdownResult } from '../contracts/target-shutdown-contract.ts'; +// fallow-ignore-next-line unused-type +export type { MetroBridgeScope } from './client-companion-tunnel-contract.ts'; +// fallow-ignore-next-line unused-type +export type { AppsFilter } from '../contracts/app-inventory.ts'; +// fallow-ignore-next-line unused-type +export type { AlertAction } from '../contracts/alert-contract.ts'; +// fallow-ignore-next-line unused-type +export type { AppleOS } from '../kernel/device.ts'; +// fallow-ignore-next-line unused-type +export type { JsonObject } from '../contracts/json.ts'; + import type { - AppleOS, - DeviceKind, - DeviceTarget, - PublicPlatform, - PlatformSelector, -} from '../kernel/device.ts'; -import type { BackMode } from '../contracts/back-mode.ts'; + AgentDeviceCapabilitiesResult, + AgentDeviceClientConfig, + AgentDeviceDevice, + AgentDeviceRequestOverrides, + AgentDeviceSelectionOptions, + AgentDeviceSession, + AlertCommandOptions, + AppCloseOptions, + AppCloseResult, + AppDeployOptions, + AppDeployResult, + AppInstallFromSourceOptions, + AppInstallFromSourceResult, + AppInstallOptions, + AppListOptions, + AppOpenOptions, + AppOpenResult, + AppPushOptions, + AppStateCommandOptions, + AppTriggerEventOptions, + AudioOptions, + BatchRunOptions, + CaptureDiffOptions, + CaptureScreenshotOptions, + CaptureScreenshotResult, + CaptureSnapshotOptions, + CaptureSnapshotResult, + ClickOptions, + ClipboardCommandOptions, + CloudArtifactsOptions, + CommandRequestResult, + DeviceBootOptions, + DeviceCommandBaseOptions, + DeviceShutdownOptions, + DoctorCommandOptions, + EventsOptions, + FillOptions, + FindOptions, + FlingOptions, + FocusOptions, + GetOptions, + IsOptions, + KeyboardCommandOptions, + Lease, + LeaseAllocateOptions, + LeaseScopedOptions, + LogsOptions, + LongPressOptions, + MaterializationReleaseOptions, + MaterializationReleaseResult, + MetroPrepareOptions, + MetroReloadOptions, + NetworkOptions, + PanOptions, + PerfOptions, + PinchOptions, + PrepareCommandOptions, + PressOptions, + ReactNativeCommandOptions, + RecordOptions, + ReplayRunOptions, + ReplayTestOptions, + RotateGestureOptions, + SessionCloseResult, + SessionSaveScriptOptions, + SessionSaveScriptResult, + SettingsUpdateOptions, + SwipeGestureOptions, + SwipeOptions, + TraceOptions, + TransformGestureOptions, + TypeTextOptions, + ViewportCommandOptions, + WaitCommandOptions, +} from '../contracts/client-api.ts'; + import type { RotateCommandResult } from '../contracts/navigation.ts'; -import type { ClickButton } from '../contracts/click-button.ts'; -import type { RecordingExportQuality } from '../contracts/recording-export-quality.ts'; -import type { RecordingScope } from '../contracts/recording-scope.ts'; -import type { - ScrollDirection, - SwipePattern, - SwipePreset, - TransformGestureParams, -} from '../contracts/scroll-gesture.ts'; + import type { ScrollInputDirection } from '../commands/interaction/runtime/gestures.ts'; import type { NavigationCommandOptions, ProjectedNavigationCommandClient, } from '../commands/system/navigation-projection.ts'; -import type { GesturePointerCount } from '../contracts/gesture-plan.ts'; -import type { LogAction } from '../contracts/logs.ts'; -import type { SessionSurface } from '../contracts/session-surface.ts'; -import type { FindLocator } from '../selectors/find.ts'; -import type { SnapshotNode, SnapshotUnchanged, SnapshotVisibility } from '../kernel/snapshot.ts'; -import type { ScreenshotResultData } from '../utils/screenshot-result.ts'; + import type { PrepareMetroRuntimeResult, ReloadMetroResult } from '../metro/client-metro.ts'; -import type { MetroPrepareKind } from '../contracts/metro.ts'; -import type { MetroBridgeScope } from './client-companion-tunnel-contract.ts'; -import type { AppsFilter } from '../contracts/app-inventory.ts'; -import type { ScreenshotRequestFlags } from '../contracts/screenshot.ts'; -import type { BatchRunResult, DaemonBatchStep } from '../core/batch.ts'; + +import type { BatchRunResult } from '../core/batch.ts'; export type { BatchRunResult } from '../core/batch.ts'; -import type { TargetShutdownResult } from '../contracts/target-shutdown-contract.ts'; -export type { TargetShutdownResult } from '../contracts/target-shutdown-contract.ts'; -import type { PerfAction, PerfArea, PerfKind, PerfSubject } from '../contracts/perf.ts'; -import type { AlertAction } from '../contracts/alert-contract.ts'; + import type { DebugSymbolsOptions, DebugSymbolsResult } from '../contracts/debug-symbols.ts'; -import type { JsonObject } from '../contracts/json.ts'; -import type { RemoteConnectionProfileFields } from '../remote/remote-config-schema.ts'; -import type { CloudProviderProfileFields } from '../contracts/remote-config-fields.ts'; + import type { CommandResult } from '../core/command-descriptor/command-result.ts'; import type { AgentArtifactsResult, CloudProviderSessionResult, } from '../contracts/cloud-artifacts.ts'; -export type { MetroBridgeScope } from './client-companion-tunnel-contract.ts'; -export type { AppsFilter } from '../contracts/app-inventory.ts'; -export type { AlertAction } from '../contracts/alert-contract.ts'; export type { DebugSymbolsOptions, DebugSymbolsResult } from '../contracts/debug-symbols.ts'; -export type { AppleOS } from '../kernel/device.ts'; export type { /** @deprecated Renamed to `OrientationCommandResult`. Retained until the next major. */ RotateCommandResult, @@ -79,487 +147,11 @@ export type { DoctorCommandResult } from '../contracts/doctor.ts'; export type { DiffSnapshotCommandResult } from '../contracts/diff.ts'; export type { RecordingCommandResult, TraceCommandResult } from '../contracts/recording.ts'; export type { ReplayCommandResult, ReplaySuiteResult } from '../contracts/replay.ts'; -export type { JsonObject } from '../contracts/json.ts'; - -export type AgentDeviceDaemonTransport = ( - req: Omit, -) => Promise; - -export type AgentDeviceClientConfig = RemoteConnectionProfileFields & - CloudProviderProfileFields & { - session?: string; - lockPolicy?: DaemonLockPolicy; - lockPlatform?: PlatformSelector; - requestId?: string; - sessionIsolation?: SessionIsolationMode; - leaseBackend?: LeaseBackend; - leaseTtlMs?: number; - runtime?: SessionRuntimeHints; - cwd?: string; - debug?: boolean; - cost?: boolean; - responseLevel?: ResponseLevel; - iosXctestrunFile?: string; - iosXctestDerivedDataPath?: string; - iosXctestEnvDir?: string; - }; - -export type AgentDeviceRequestOverrides = Pick< - AgentDeviceClientConfig, - | 'session' - | 'lockPolicy' - | 'lockPlatform' - | 'requestId' - | 'daemonBaseUrl' - | 'daemonAuthToken' - | 'daemonTransport' - | 'daemonServerMode' - | 'tenant' - | 'sessionIsolation' - | 'runId' - | 'leaseId' - | 'leaseBackend' - | 'leaseProvider' - | 'deviceKey' - | 'clientId' - | 'providerApp' - | 'providerOsVersion' - | 'providerProject' - | 'providerBuild' - | 'providerSessionName' - | 'awsProjectArn' - | 'awsDeviceArn' - | 'awsAppArn' - | 'awsRegion' - | 'awsInteractionMode' - | 'leaseTtlMs' - | 'cwd' - | 'debug' - | 'cost' - | 'responseLevel' - | 'iosXctestrunFile' - | 'iosXctestDerivedDataPath' - | 'iosXctestEnvDir' ->; - -export type AgentDeviceIdentifiers = { - session?: string; - deviceId?: string; - deviceName?: string; - udid?: string; - serial?: string; - appId?: string; - appBundleId?: string; - package?: string; -}; - -export type AgentDeviceSelectionOptions = { - platform?: PlatformSelector; - target?: DeviceTarget; - device?: string; - udid?: string; - serial?: string; - iosSimulatorDeviceSet?: string; - androidDeviceAllowlist?: string; -}; - -export type AgentDeviceDevice = { - platform: PublicPlatform; - target: DeviceTarget; - kind: DeviceKind; - id: string; - name: string; - booted?: boolean; - /** - * Additive Apple-OS discriminant (iPhone/iPad/tvOS/visionOS/macOS). Present only for - * Apple devices; `platform` still carries the leaf (`ios`/`macos`). - */ - appleOs?: AppleOS; - identifiers: AgentDeviceIdentifiers; - ios?: { - udid: string; - }; - android?: { - serial: string; - }; - vega?: { - serial: string; - }; -}; - -export type AgentDeviceCapabilitiesResult = { - device: AgentDeviceDevice; - availableCommands: string[]; -}; - -export type AgentDeviceSessionDevice = { - platform: PublicPlatform; - target: DeviceTarget; - id: string; - name: string; - /** - * Additive Apple-OS discriminant (iPhone/iPad/tvOS/visionOS/macOS). Present only for - * Apple devices; `platform` still carries the leaf (`ios`/`macos`). - */ - appleOs?: AppleOS; - identifiers: AgentDeviceIdentifiers; - ios?: { - udid: string; - simulatorSetPath?: string | null; - }; - android?: { - serial: string; - }; - vega?: { - serial: string; - }; -}; - -export type AgentDeviceSession = { - name: string; - createdAt: number; - sessionStateDir?: string; - runnerLogPath?: string; - device: AgentDeviceSessionDevice; - identifiers: AgentDeviceIdentifiers; -}; - -export type StartupPerfSample = { - durationMs: number; - measuredAt: string; - method: string; - appTarget?: string; - appBundleId?: string; -}; - -export type SessionCloseResult = { - session: string; - shutdown?: TargetShutdownResult; - provider?: CloudProviderSessionResult; - /** - * #1258: absolute path of the committed session/healed script when this close - * published one (`close --save-script`, or a repair-armed session's finalize) - * — so a client that requested publication learns where the file landed. - */ - savedScript?: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type SessionSaveScriptOptions = AgentDeviceRequestOverrides & { - path?: string; - /** Atomically replace an existing target instead of refusing publication. */ - force?: boolean; -}; - -export type SessionSaveScriptResult = { - session: string; - savedScript: string; - actionCount: number; - identifiers: AgentDeviceIdentifiers; -}; - -export type CloudArtifactsOptions = AgentDeviceRequestOverrides & { - provider?: string; - providerSessionId?: string; -}; - -export type AppInstallOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - app?: string; - appPath: string; - }; - -export type AppDeployOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - app: string; - appPath: string; - }; - -export type AppDeployResult = { - app: string; - appPath: string; - platform: PublicPlatform; - appId?: string; - bundleId?: string; - package?: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type AppOpenOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - app?: string; - url?: string; - surface?: SessionSurface; - activity?: string; - launchConsole?: string; - launchArgs?: string[]; - relaunch?: boolean; - saveScript?: boolean | string; - /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ - force?: boolean; - deviceHub?: boolean; - testIme?: boolean; - noRecord?: boolean; - runtime?: SessionRuntimeHints; - }; - -export type AppOpenResult = { - session: string; - warnings?: string[]; - sessionStateDir?: string; - runnerLogPath?: string; - requestLogPath?: string; - eventLogPath?: string; - appName?: string; - appBundleId?: string; - appId?: string; - startup?: StartupPerfSample; - runtime?: SessionRuntimeHints; - device?: AgentDeviceSessionDevice; - identifiers: AgentDeviceIdentifiers; -}; - -export type AppCloseOptions = AgentDeviceRequestOverrides & { - app?: string; - shutdown?: boolean; - saveScript?: boolean | string; - /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ - force?: boolean; -}; - -export type AppCloseResult = { - session: string; - closedApp?: string; - shutdown?: TargetShutdownResult; - /** - * #1258: absolute path of the committed session/healed script when this close - * published one (`close --save-script`, or a repair-armed session's finalize) - * — so a client that requested publication learns where the file landed. - */ - savedScript?: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type AppInstallFromSourceOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - source: DaemonInstallSource; - retainPaths?: boolean; - retentionMs?: number; - }; - -export type AppInstallFromSourceResult = { - appName?: string; - appId?: string; - bundleId?: string; - packageName?: string; - launchTarget: string; - installablePath?: string; - archivePath?: string; - materializationId?: string; - materializationExpiresAt?: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type AppListOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - appsFilter?: AppsFilter; - }; - -export type MaterializationReleaseOptions = AgentDeviceRequestOverrides & { - materializationId: string; -}; - -export type MaterializationReleaseResult = { - released: boolean; - materializationId: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type Lease = { - leaseId: string; - tenantId: string; - runId: string; - backend: LeaseBackend; - leaseProvider?: string; - deviceKey?: string; - clientId?: string; - createdAt?: number; - heartbeatAt?: number; - expiresAt?: number; -}; - -export type LeaseOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - ttlMs?: number; - }; - -export type LeaseAllocateOptions = LeaseOptions & { - tenant: string; - runId: string; - leaseBackend?: LeaseBackend; - leaseProvider?: string; - provider?: string; - deviceKey?: string; - clientId?: string; -}; - -export type LeaseScopedOptions = LeaseOptions & { - tenant?: string; - runId?: string; - leaseId: string; - leaseBackend?: LeaseBackend; - leaseProvider?: string; - provider?: string; - deviceKey?: string; - clientId?: string; -}; - -export type MetroPrepareOptions = { - projectRoot?: string; - kind?: MetroPrepareKind; - publicBaseUrl?: string; - proxyBaseUrl?: string; - bearerToken?: string; - bridgeScope?: MetroBridgeScope; - launchUrl?: string; - companionProfileKey?: string; - companionConsumerKey?: string; - port?: number; - listenHost?: string; - statusHost?: string; - startupTimeoutMs?: number; - probeTimeoutMs?: number; - reuseExisting?: boolean; - installDependenciesIfNeeded?: boolean; - runtimeFilePath?: string; - logPath?: string; -}; export type MetroPrepareResult = PrepareMetroRuntimeResult; -export type MetroReloadOptions = { - metroHost?: string; - metroPort?: number; - bundleUrl?: string; - timeoutMs?: number; -}; - export type MetroReloadResult = ReloadMetroResult; -export type CaptureSnapshotOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - interactiveOnly?: boolean; - depth?: number; - scope?: string; - raw?: boolean; - forceFull?: boolean; - timeoutMs?: number; - /** - * #1271 stage 2 (ADR 0012 amendment): `snapshot` is observation-only and - * excluded from a repair-armed heal by default; `record` forces it - * through. Mutually exclusive with `noRecord`. - */ - noRecord?: boolean; - record?: boolean; - }; - -export type CaptureSnapshotResult = { - nodes: SnapshotNode[]; - truncated: boolean; - appName?: string; - appBundleId?: string; - visibility?: SnapshotVisibility; - unchanged?: SnapshotUnchanged; - snapshotDiagnostics?: SnapshotDiagnosticsSummary; - identifiers: AgentDeviceIdentifiers; - /** - * ADR 0014: the response-level ref-frame epoch the plain node refs were minted - * from. A ref-issuing snapshot carries it ONCE (nodes stay plain `@e12` for the - * token budget); pair a ref with it (`@e12~s`) before a mutation. - */ - refsGeneration?: number; - /** - * Digest response view only: a capped list of `{ ref, label? }` pairs taken - * from the full `nodes` tree so the MCP layer can still pin refs when the - * default-level `nodes` payload is intentionally omitted. - */ - refs?: Array<{ ref: string; label?: string }>; -} & PublicSnapshotCaptureAnnotations; - -export type CaptureScreenshotOptions = AgentDeviceRequestOverrides & { - path?: string; - overlayRefs?: boolean; - pixelDensity?: number; - fullscreen?: boolean; - maxSize?: number; - stabilize?: boolean; - normalizeStatusBar?: boolean; - surface?: SessionSurface; -}; - -export type CaptureScreenshotResult = ScreenshotResultData & { - path: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type DeviceCommandBaseOptions = AgentDeviceRequestOverrides & AgentDeviceSelectionOptions; - -type WaitCommandTarget = - | { - durationMs: number; - text?: never; - ref?: never; - selector?: never; - stable?: never; - quietMs?: never; - timeoutMs?: never; - } - | (SelectorSnapshotCommandOptions & { - text: string; - durationMs?: never; - ref?: never; - selector?: never; - stable?: never; - quietMs?: never; - timeoutMs?: number; - }) - | (SelectorSnapshotCommandOptions & { - ref: string; - durationMs?: never; - text?: never; - selector?: never; - stable?: never; - quietMs?: never; - timeoutMs?: number; - }) - | (SelectorSnapshotCommandOptions & { - selector: string; - durationMs?: never; - text?: never; - ref?: never; - stable?: never; - quietMs?: never; - timeoutMs?: number; - }) - | (SelectorSnapshotCommandOptions & { - stable: true; - durationMs?: never; - text?: never; - ref?: never; - selector?: never; - quietMs?: number; - timeoutMs?: number; - }); - -export type WaitCommandOptions = DeviceCommandBaseOptions & WaitCommandTarget; - -export type AlertCommandOptions = DeviceCommandBaseOptions & { - action?: AlertAction; - timeoutMs?: number; -}; - -export type AppStateCommandOptions = DeviceCommandBaseOptions; - export type BackCommandOptions = DeviceCommandBaseOptions & NavigationCommandOptions<'back'>; export type OrientationCommandOptions = DeviceCommandBaseOptions & @@ -571,41 +163,9 @@ export type RotateCommandOptions = OrientationCommandOptions; export type AppSwitcherCommandOptions = DeviceCommandBaseOptions & NavigationCommandOptions<'app-switcher'>; -export type KeyboardCommandOptions = DeviceCommandBaseOptions & { - action?: 'status' | 'dismiss' | 'enter' | 'return'; -}; - -export type ClipboardCommandOptions = - | (DeviceCommandBaseOptions & { - action: 'read'; - }) - | (DeviceCommandBaseOptions & { - action: 'write'; - text: string; - }); - export type TvRemoteCommandOptions = DeviceCommandBaseOptions & NavigationCommandOptions<'tv-remote'>; -export type ReactNativeCommandOptions = DeviceCommandBaseOptions & { - action: 'dismiss-overlay'; -}; - -export type PrepareCommandOptions = DeviceCommandBaseOptions & { - action: 'ios-runner'; - timeoutMs?: number; -}; - -export type DoctorCommandOptions = DeviceCommandBaseOptions & { - targetApp?: string; - remote?: boolean; -}; - -export type ViewportCommandOptions = DeviceCommandBaseOptions & { - width: number; - height: number; -}; - type NonNavigationCommandClient = { wait: (options: WaitCommandOptions) => Promise>; alert: (options?: AlertCommandOptions) => Promise; @@ -635,67 +195,6 @@ type DeprecatedCommandClient = { rotate: (options: RotateCommandOptions) => Promise; }; -type SelectorSnapshotCommandOptions = Pick; -type FindSnapshotCommandOptions = Pick; - -type PointTarget = { - x: number; - y: number; - ref?: never; - selector?: never; - label?: never; -}; - -type RefTarget = { - ref: string; - label?: string; - x?: never; - y?: never; - selector?: never; -}; - -type SelectorTarget = { - selector: string; - x?: never; - y?: never; - ref?: never; - label?: never; -}; - -export type InteractionTarget = PointTarget | RefTarget | SelectorTarget; - -export type ElementTarget = RefTarget | SelectorTarget; - -type RepeatedPressOptions = { - count?: number; - intervalMs?: number; - holdMs?: number; - jitterPx?: number; - doubleTap?: boolean; -}; - -export type DeviceBootOptions = DeviceCommandBaseOptions & { - headless?: boolean; -}; - -export type DeviceShutdownOptions = DeviceCommandBaseOptions; - -export type AppPushOptions = DeviceCommandBaseOptions & { - app: string; - payload: string | JsonObject; -}; - -export type AppTriggerEventOptions = DeviceCommandBaseOptions & { - event: string; - payload?: JsonObject; -}; - -export type CaptureDiffOptions = DeviceCommandBaseOptions & - Pick & { - kind: 'snapshot'; - out?: string; - }; - /** * Opt-in (#1101): after the action, wait for the UI to go quiet and return the * settled diff vs the pre-action tree (`settle` on the result) in the same @@ -704,89 +203,6 @@ export type CaptureDiffOptions = DeviceCommandBaseOptions & * 10s) when `settle` is true. A bare `timeoutMs` without `settle` is ignored * for compatibility; `settleQuietMs` still requires `settle`. */ -type SettleCommandOptions = { - settle?: boolean; - settleQuietMs?: number; - timeoutMs?: number; -}; - -export type ClickOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - InteractionTarget & - RepeatedPressOptions & - SettleCommandOptions & { - button?: ClickButton; - /** - * Opt-in (#1047): return cheap post-action evidence (AX digest, node counts, - * changedFromBefore) in the response instead of requiring a follow-up - * snapshot to confirm the action had an effect. - */ - verify?: boolean; - }; - -export type PressOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - InteractionTarget & - RepeatedPressOptions & - SettleCommandOptions & { - verify?: boolean; - }; - -export type LongPressOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - InteractionTarget & - SettleCommandOptions & { - durationMs?: number; - }; - -export type SwipeOptions = DeviceCommandBaseOptions & { - from: { x: number; y: number }; - to: { x: number; y: number }; - count?: number; - pauseMs?: number; - pattern?: SwipePattern; -}; - -export type PanOptions = DeviceCommandBaseOptions & { - x: number; - y: number; - dx: number; - dy: number; - pointerCount?: GesturePointerCount; - durationMs?: number; -}; - -export type FlingOptions = DeviceCommandBaseOptions & { - direction: ScrollDirection; - x: number; - y: number; - distance?: number; -}; - -export type SwipeGestureOptions = DeviceCommandBaseOptions & { - preset: SwipePreset; -}; - -export type FocusOptions = DeviceCommandBaseOptions & { - x: number; - y: number; -}; - -export type TypeTextOptions = DeviceCommandBaseOptions & { - text: string; - delayMs?: number; -}; - -export type FillOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - InteractionTarget & - SettleCommandOptions & { - text: string; - delayMs?: number; - /** Publish this fill value as `${VAR}` when script recording is armed. */ - recordAs?: string; - verify?: boolean; - }; export type ScrollOptions = DeviceCommandBaseOptions & { direction: ScrollInputDirection; @@ -795,335 +211,12 @@ export type ScrollOptions = DeviceCommandBaseOptions & { durationMs?: number; }; -export type PinchOptions = DeviceCommandBaseOptions & { - scale: number; - x?: number; - y?: number; -}; - -export type RotateGestureOptions = DeviceCommandBaseOptions & { - degrees: number; - x?: number; - y?: number; -}; - -export type TransformGestureOptions = DeviceCommandBaseOptions & TransformGestureParams; - /** * #1271 stage 2 (ADR 0012 amendment): `get`/`is`/`find` are observation-only * and excluded from a repair-armed heal by default. `record` forces this * action through (the corrective-read case); `noRecord` continues to opt the * action out entirely. Mutually exclusive. */ -type RecordControlOptions = { - noRecord?: boolean; - record?: boolean; -}; - -export type GetOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - ElementTarget & - RecordControlOptions & { - format: 'text' | 'attrs'; - }; - -type IsTextPredicateOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - RecordControlOptions & { - predicate: 'text'; - selector: string; - value: string; - }; - -type IsStatePredicateOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - RecordControlOptions & { - predicate: 'visible' | 'hidden' | 'exists' | 'editable' | 'selected' | 'focused'; - selector: string; - value?: never; - }; - -export type IsOptions = IsTextPredicateOptions | IsStatePredicateOptions; - -type FindBaseOptions = DeviceCommandBaseOptions & - FindSnapshotCommandOptions & - RecordControlOptions & { - locator?: FindLocator; - query: string; - first?: boolean; - last?: boolean; - }; - -export type FindOptions = - | (FindBaseOptions & { action?: 'click' | 'focus' | 'exists' | 'getText' | 'getAttrs' }) - | (FindBaseOptions & { action: 'wait'; timeoutMs?: number }) - | (FindBaseOptions & { action: 'fill' | 'type'; value: string }); - -export type ReplayRunOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - path: string; - runtime?: SessionRuntimeHints; - /** - * @deprecated ADR 0012 migration step 6: `--update` no longer rewrites - * the script. Accepted for backward compatibility; every divergence - * already carries ranked selector suggestions regardless of this flag. - */ - update?: boolean; - /** @deprecated Use backend: 'maestro'. */ - maestro?: boolean; - backend?: string; - env?: string[]; - timeoutMs?: number; - /** - * ADR 0012 decision 4 / migration step 5: resume at this 1-based plan - * step, skipping `1..resumeFrom-1` without executing them. Requires - * `resumePlanDigest` from the divergence report that reported this - * step as the failure. `replay` only — `test` has no resume fields. - */ - resumeFrom?: number; - /** The `resume.planDigest` from the divergence report `resumeFrom` came from. */ - resumePlanDigest?: string; - /** - * ADR 0012 decision 6, R1/R6: arms agent-supervised re-record repair - * from this replay attempt onward. Optional string value is the healed - * `.ad`'s output path; absent one, it defaults to the `` sibling - * `.healed.ad` when the repair ends with `close --save-script`. - */ - saveScript?: boolean | string; - /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ - force?: boolean; - }; - -export type ReplayTestOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - paths: string[]; - runtime?: SessionRuntimeHints; - update?: boolean; - /** @deprecated Use backend: 'maestro'. */ - maestro?: boolean; - backend?: string; - env?: string[]; - failFast?: boolean; - timeoutMs?: number; - retries?: number; - recordVideo?: boolean; - artifactsDir?: string; - /** @deprecated Use the CLI --reporter junit: or --report-junit . */ - reportJunit?: string; - shardAll?: number; - shardSplit?: number; - }; - -export type BatchStep = { - command: string; - input: Record; - runtime?: SessionRuntimeHints; -}; - -export type BatchRunOptions = AgentDeviceRequestOverrides & { - steps: BatchStep[]; - onError?: 'stop'; - maxSteps?: number; - out?: string; -}; - -export type PerfOptions = DeviceCommandBaseOptions & { - area?: PerfArea; - subject?: PerfSubject; - action?: PerfAction; - kind?: PerfKind; - template?: string; - out?: string; - tracePath?: string; -}; - -export type LogsOptions = AgentDeviceRequestOverrides & { - action?: LogAction; - message?: string; - restart?: boolean; -}; - -export type EventsOptions = AgentDeviceRequestOverrides & { - cursor?: string; - limit?: number; -}; - -export type NetworkOptions = AgentDeviceRequestOverrides & { - action?: 'dump' | 'log'; - limit?: number; - include?: NetworkIncludeMode; -}; - -export type AudioOptions = AgentDeviceRequestOverrides & { - action?: 'probe'; - probeAction?: 'start' | 'status' | 'stop'; - durationMs?: number; - bucketMs?: number; -}; - -export type RecordOptions = AgentDeviceRequestOverrides & { - action: 'start' | 'stop'; - path?: string; - fps?: number; - maxSize?: number; - quality?: RecordingExportQuality; - hideTouches?: boolean; - recordingScope?: RecordingScope; -}; - -export type TraceOptions = AgentDeviceRequestOverrides & { - action: 'start' | 'stop'; - path?: string; -}; - -export type PermissionTarget = - | 'camera' - | 'microphone' - | 'photos' - | 'contacts' - | 'contacts-limited' - | 'notifications' - | 'calendar' - | 'location' - | 'location-always' - | 'media-library' - | 'motion' - | 'reminders' - | 'siri' - | 'accessibility' - | 'screen-recording' - | 'input-monitoring'; - -export type SettingsUpdateOptions = - | (DeviceCommandBaseOptions & { - setting: 'clear-app-state'; - state: 'clear'; - app?: string; - }) - | (DeviceCommandBaseOptions & { - setting: 'wifi' | 'airplane' | 'location'; - state: 'on' | 'off'; - }) - | (DeviceCommandBaseOptions & { - setting: 'location'; - state: 'set'; - latitude: number; - longitude: number; - }) - | (DeviceCommandBaseOptions & { - setting: 'animations'; - state: 'on' | 'off'; - }) - | (DeviceCommandBaseOptions & { - setting: 'appearance'; - state: 'light' | 'dark' | 'toggle'; - }) - | (DeviceCommandBaseOptions & { - setting: 'faceid' | 'touchid'; - state: 'match' | 'nonmatch' | 'enroll' | 'unenroll'; - }) - | (DeviceCommandBaseOptions & { - setting: 'fingerprint'; - state: 'match' | 'nonmatch'; - }) - | (DeviceCommandBaseOptions & { - setting: 'permission'; - state: 'grant' | 'deny' | 'reset'; - permission: PermissionTarget; - mode?: 'full' | 'limited'; - }); - -type CommandExecutionOptions = Partial & { - positionals?: string[]; - kind?: string; - out?: string; - artifact?: string; - dsym?: string; - searchPath?: string; - interactiveOnly?: boolean; - depth?: number; - scope?: string; - raw?: boolean; - forceFull?: boolean; - count?: number; - fps?: number; - maxSize?: number; - recordingScope?: RecordingScope; - quality?: RecordingExportQuality; - hideTouches?: boolean; - intervalMs?: number; - delayMs?: number; - durationMs?: number; - holdMs?: number; - jitterPx?: number; - pixels?: number; - doubleTap?: boolean; - verify?: boolean; - settle?: boolean; - settleQuietMs?: number; - clickButton?: ClickButton; - pauseMs?: number; - pattern?: SwipePattern; - headless?: boolean; - restart?: boolean; - replayUpdate?: boolean; - replayBackend?: string; - replayEnv?: string[]; - replayShellEnv?: Record; - replayFrom?: number; - replayPlanDigest?: string; - failFast?: boolean; - timeoutMs?: number; - retries?: number; - recordVideo?: boolean; - artifactsDir?: string; - shardAll?: number; - shardSplit?: number; - findFirst?: boolean; - findLast?: boolean; - networkInclude?: NetworkIncludeMode; - batchOnError?: 'stop'; - batchMaxSteps?: number; - batchSteps?: DaemonBatchStep[]; -}; - -export type InternalRequestOptions = AgentDeviceClientConfig & - AgentDeviceSelectionOptions & - CommandExecutionOptions & { - runtime?: SessionRuntimeHints; - overlayRefs?: boolean; - surface?: SessionSurface; - activity?: string; - launchConsole?: string; - launchArgs?: string[]; - relaunch?: boolean; - shutdown?: boolean; - saveScript?: boolean | string; - /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ - force?: boolean; - deviceHub?: boolean; - testIme?: boolean; - noRecord?: boolean; - /** Fill-only script parameter name used to publish `${VAR}` instead of literal text. */ - recordAs?: string; - /** #1271 stage 2: force-record this action; mutually exclusive with `noRecord`. */ - record?: boolean; - backMode?: BackMode; - metroHost?: string; - metroPort?: number; - bundleUrl?: string; - launchUrl?: string; - appsFilter?: AppsFilter; - installSource?: DaemonInstallSource; - retainMaterializedPaths?: boolean; - materializedPathRetentionMs?: number; - materializationId?: string; - leaseTtlMs?: number; - provider?: string; - providerSessionId?: string; - }; - -export type CommandRequestResult = DaemonResponseData; export type AgentDeviceClient = { command: AgentDeviceCommandClient; diff --git a/src/commands/__tests__/command-flags.test.ts b/src/commands/__tests__/command-flags.test.ts index 45b2ff431..a264d54b5 100644 --- a/src/commands/__tests__/command-flags.test.ts +++ b/src/commands/__tests__/command-flags.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; -import type { InternalRequestOptions } from '../../client/client-types.ts'; +import type { InternalRequestOptions } from '../../contracts/client-api.ts'; import { findCommandMetadata } from '../command-metadata.ts'; import { readMetadataCommandFlags } from '../command-flags.ts'; diff --git a/src/commands/batch/index.ts b/src/commands/batch/index.ts index 6b2066c5b..309a5f3e3 100644 --- a/src/commands/batch/index.ts +++ b/src/commands/batch/index.ts @@ -1,4 +1,4 @@ -import type { BatchRunOptions } from '../../client/client-types.ts'; +import type { BatchRunOptions } from '../../contracts/client-api.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { commonInputFromFlags } from '../cli-grammar/common.ts'; import type { CliReader } from '../cli-grammar/types.ts'; diff --git a/src/commands/capture/alert.ts b/src/commands/capture/alert.ts index c3c005266..5bde314ad 100644 --- a/src/commands/capture/alert.ts +++ b/src/commands/capture/alert.ts @@ -1,6 +1,6 @@ import { ALERT_ACTIONS, type AlertAction } from '../../contracts/alert-contract.ts'; import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { AlertCommandOptions } from '../../client/client-types.ts'; +import type { AlertCommandOptions } from '../../contracts/client-api.ts'; import { compactRecord, enumField, integerField } from '../command-input.ts'; import { defineExecutableCommand } from '../command-contract.ts'; import { diff --git a/src/commands/capture/output.test.ts b/src/commands/capture/output.test.ts index 972888311..efb051a24 100644 --- a/src/commands/capture/output.test.ts +++ b/src/commands/capture/output.test.ts @@ -1,7 +1,7 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { attachRefs, type RawSnapshotNode } from '../../kernel/snapshot.ts'; -import type { CaptureSnapshotResult } from '../../client/client-types.ts'; +import type { CaptureSnapshotResult } from '../../contracts/client-api.ts'; import { snapshotCliOutput } from './output.ts'; function buildResult(raw: RawSnapshotNode[]): CaptureSnapshotResult { diff --git a/src/commands/capture/output.ts b/src/commands/capture/output.ts index eadce2a85..bfb34eaf1 100644 --- a/src/commands/capture/output.ts +++ b/src/commands/capture/output.ts @@ -1,5 +1,5 @@ import { serializeSnapshotResult } from '../../contracts/result-serialization.ts'; -import type { CaptureSnapshotResult } from '../../client/client-types.ts'; +import type { CaptureSnapshotResult } from '../../contracts/client-api.ts'; import { dedupeInheritedSnapshotLabels } from '../../snapshot/snapshot-label-dedup.ts'; import { formatSnapshotText } from '../../utils/output.ts'; import type { CliOutput } from '../command-contract.ts'; diff --git a/src/commands/capture/screenshot.ts b/src/commands/capture/screenshot.ts index e12a2a335..cfa30834c 100644 --- a/src/commands/capture/screenshot.ts +++ b/src/commands/capture/screenshot.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { CaptureScreenshotOptions } from '../../client/client-types.ts'; +import type { CaptureScreenshotOptions } from '../../contracts/client-api.ts'; import { SESSION_SURFACES } from '../../contracts/session-surface.ts'; import { SCREENSHOT_COMMAND_FLAG_KEYS, diff --git a/src/commands/capture/settings.ts b/src/commands/capture/settings.ts index ff7cee228..9489b5074 100644 --- a/src/commands/capture/settings.ts +++ b/src/commands/capture/settings.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { SettingsUpdateOptions } from '../../client/client-types.ts'; +import type { SettingsUpdateOptions } from '../../contracts/client-api.ts'; import { SETTINGS_USAGE_OVERRIDE } from '../../contracts/settings-contract.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import type { CliFlags } from '../../contracts/cli-flags.ts'; diff --git a/src/commands/capture/wait.ts b/src/commands/capture/wait.ts index 32a3fe2bf..d23faa823 100644 --- a/src/commands/capture/wait.ts +++ b/src/commands/capture/wait.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { WaitCommandOptions } from '../../client/client-types.ts'; +import type { WaitCommandOptions } from '../../contracts/client-api.ts'; import { parseWaitPositionals } from '../../core/wait-positionals.ts'; import { SELECTOR_SNAPSHOT_FLAGS } from '../cli-grammar/flag-groups.ts'; import { type CliFlags } from '../cli-grammar/flag-types.ts'; diff --git a/src/commands/cli-grammar/common.ts b/src/commands/cli-grammar/common.ts index b384a7ede..a2d94068d 100644 --- a/src/commands/cli-grammar/common.ts +++ b/src/commands/cli-grammar/common.ts @@ -2,7 +2,7 @@ import type { ElementTarget, InteractionTarget, InternalRequestOptions, -} from '../../client/client-types.ts'; +} from '../../contracts/client-api.ts'; import { splitSelectorFromArgs } from '../../selectors/parse.ts'; import { checkElementTargetArgs, diff --git a/src/commands/cli-grammar/types.ts b/src/commands/cli-grammar/types.ts index fc4d7a93c..82b20a981 100644 --- a/src/commands/cli-grammar/types.ts +++ b/src/commands/cli-grammar/types.ts @@ -1,4 +1,4 @@ -import type { InternalRequestOptions } from '../../client/client-types.ts'; +import type { InternalRequestOptions } from '../../contracts/client-api.ts'; import type { CommandFlags } from '../../core/dispatch-context.ts'; import type { CliFlags } from '../../contracts/cli-flags.ts'; import type { ClickButton } from '../../contracts/click-button.ts'; diff --git a/src/commands/command-flags.ts b/src/commands/command-flags.ts index 05db325a5..c832fbf65 100644 --- a/src/commands/command-flags.ts +++ b/src/commands/command-flags.ts @@ -3,7 +3,7 @@ import type { CommandFlags } from '../core/dispatch-context.ts'; import { leaseScopeFromOptions, leaseScopeToCommandFlags } from '../core/lease-scope.ts'; import { stripUndefined } from '../utils/parsing.ts'; import { getFlagDefinitions } from './cli-grammar/flag-registry.ts'; -import type { InternalRequestOptions } from '../client/client-types.ts'; +import type { InternalRequestOptions } from '../contracts/client-api.ts'; import type { CommandMetadata } from './command-contract.ts'; const CLI_FLAG_KEYS: ReadonlySet = new Set( diff --git a/src/commands/command-input.ts b/src/commands/command-input.ts index cf49baad6..142d83dcf 100644 --- a/src/commands/command-input.ts +++ b/src/commands/command-input.ts @@ -3,7 +3,7 @@ import type { AgentDeviceSelectionOptions, ElementTarget, InteractionTarget, -} from '../client/client-types.ts'; +} from '../contracts/client-api.ts'; import { DEVICE_TARGETS, PLATFORM_SELECTORS, diff --git a/src/commands/interaction/index.ts b/src/commands/interaction/index.ts index ae3cd69df..97d214f9d 100644 --- a/src/commands/interaction/index.ts +++ b/src/commands/interaction/index.ts @@ -11,12 +11,12 @@ import type { IsOptions, LongPressOptions, RotateGestureOptions, - ScrollOptions, SwipeGestureOptions, SwipeOptions, TransformGestureOptions, TypeTextOptions, -} from '../../client/client-types.ts'; +} from '../../contracts/client-api.ts'; +import type { ScrollOptions } from '../../client/client-types.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { REPEATED_TOUCH_FLAGS, diff --git a/src/commands/interaction/interactions.ts b/src/commands/interaction/interactions.ts index caecb5ada..67157cba4 100644 --- a/src/commands/interaction/interactions.ts +++ b/src/commands/interaction/interactions.ts @@ -5,7 +5,7 @@ import type { InteractionTarget, LongPressOptions, TypeTextOptions, -} from '../../client/client-types.ts'; +} from '../../contracts/client-api.ts'; import { readFillTargetFromPositionals, readInteractionTargetFromPositionals, diff --git a/src/commands/interaction/output.ts b/src/commands/interaction/output.ts index 99f40c478..ddc3e8912 100644 --- a/src/commands/interaction/output.ts +++ b/src/commands/interaction/output.ts @@ -1,4 +1,4 @@ -import type { CommandRequestResult } from '../../client/client-types.ts'; +import type { CommandRequestResult } from '../../contracts/client-api.ts'; import type { CliOutput } from '../command-contract.ts'; import { readCommandMessage } from '../../utils/success-text.ts'; import { messageCliOutput, resultOutput, type CliOutputFormatter } from '../output-common.ts'; diff --git a/src/commands/interaction/selectors.ts b/src/commands/interaction/selectors.ts index 883fe2ca0..32cd520d4 100644 --- a/src/commands/interaction/selectors.ts +++ b/src/commands/interaction/selectors.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { FindOptions, IsOptions } from '../../client/client-types.ts'; +import type { FindOptions, IsOptions } from '../../contracts/client-api.ts'; import type { CliFlags } from '../../contracts/cli-flags.ts'; import { AppError } from '../../kernel/errors.ts'; import { checkIsPredicate, normalizeIsPositionals } from '../../selectors/predicates.ts'; diff --git a/src/commands/management/app.ts b/src/commands/management/app.ts index 0858329e8..fee8ff8ad 100644 --- a/src/commands/management/app.ts +++ b/src/commands/management/app.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { AppCloseOptions, AppOpenOptions } from '../../client/client-types.ts'; +import type { AppCloseOptions, AppOpenOptions } from '../../contracts/client-api.ts'; import { DEFAULT_APPS_FILTER } from '../../contracts/app-inventory.ts'; import { SESSION_SURFACES } from '../../contracts/session-surface.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; diff --git a/src/commands/management/output.test.ts b/src/commands/management/output.test.ts index 75a847829..1414a66fa 100644 --- a/src/commands/management/output.test.ts +++ b/src/commands/management/output.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest'; import { doctorCliOutput, managementCliOutputFormatters, openCliOutput } from './output.ts'; import { markDoctorProgressRendered } from '../../contracts/cli-doctor-output.ts'; import { withNoColor } from '../../__tests__/test-utils/index.ts'; -import type { AppOpenResult } from '../../client/client-types.ts'; +import type { AppOpenResult } from '../../contracts/client-api.ts'; describe('openCliOutput', () => { test('prints session state directory on a second line', () => { diff --git a/src/commands/management/output.ts b/src/commands/management/output.ts index 39648f9a4..0315718d6 100644 --- a/src/commands/management/output.ts +++ b/src/commands/management/output.ts @@ -17,7 +17,7 @@ import type { CommandRequestResult, SessionCloseResult, SessionSaveScriptResult, -} from '../../client/client-types.ts'; +} from '../../contracts/client-api.ts'; import type { AgentArtifactsResult, CloudArtifactsResult, diff --git a/src/commands/management/push.ts b/src/commands/management/push.ts index a02c30c0a..6bea00aa9 100644 --- a/src/commands/management/push.ts +++ b/src/commands/management/push.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { AppPushOptions, AppTriggerEventOptions } from '../../client/client-types.ts'; +import type { AppPushOptions, AppTriggerEventOptions } from '../../contracts/client-api.ts'; import type { JsonObject } from '../../contracts/json.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { diff --git a/src/commands/management/viewport.ts b/src/commands/management/viewport.ts index 9fc0c09f5..d8ce9b52b 100644 --- a/src/commands/management/viewport.ts +++ b/src/commands/management/viewport.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { ViewportCommandOptions } from '../../client/client-types.ts'; +import type { ViewportCommandOptions } from '../../contracts/client-api.ts'; import { readViewportDimension } from '../../core/viewport-dimension.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { integerField, requiredField } from '../command-input.ts'; diff --git a/src/commands/metro/index.ts b/src/commands/metro/index.ts index 1ea77da46..d5b0186f3 100644 --- a/src/commands/metro/index.ts +++ b/src/commands/metro/index.ts @@ -1,9 +1,5 @@ -import type { - MetroPrepareOptions, - MetroPrepareResult, - MetroReloadOptions, - MetroReloadResult, -} from '../../client/client-types.ts'; +import type { MetroPrepareOptions, MetroReloadOptions } from '../../contracts/client-api.ts'; +import type { MetroPrepareResult, MetroReloadResult } from '../../client/client-types.ts'; import { AppError } from '../../kernel/errors.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { diff --git a/src/commands/observability/index.ts b/src/commands/observability/index.ts index b55060706..70ab5525a 100644 --- a/src/commands/observability/index.ts +++ b/src/commands/observability/index.ts @@ -3,7 +3,7 @@ import type { EventsOptions, LogsOptions, NetworkOptions, -} from '../../client/client-types.ts'; +} from '../../contracts/client-api.ts'; import { NETWORK_INCLUDE_MODES, type NetworkIncludeMode } from '../../kernel/contracts.ts'; import { AppError } from '../../kernel/errors.ts'; import { parseStringMember } from '../../utils/string-enum.ts'; diff --git a/src/commands/perf/index.ts b/src/commands/perf/index.ts index a6c1d2aec..df0530ce4 100644 --- a/src/commands/perf/index.ts +++ b/src/commands/perf/index.ts @@ -1,4 +1,4 @@ -import type { PerfOptions } from '../../client/client-types.ts'; +import type { PerfOptions } from '../../contracts/client-api.ts'; import { AppError } from '../../kernel/errors.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { enumField, stringField } from '../command-input.ts'; diff --git a/src/commands/perf/output.ts b/src/commands/perf/output.ts index 42efe5c85..0512a0a7e 100644 --- a/src/commands/perf/output.ts +++ b/src/commands/perf/output.ts @@ -1,4 +1,4 @@ -import type { CommandRequestResult } from '../../client/client-types.ts'; +import type { CommandRequestResult } from '../../contracts/client-api.ts'; import { isRecord } from '../../utils/parsing.ts'; import type { CliOutput } from '../command-contract.ts'; import { resultOutput, type CliOutputFormatter } from '../output-common.ts'; diff --git a/src/commands/recording/index.ts b/src/commands/recording/index.ts index f06216c22..0359c47d3 100644 --- a/src/commands/recording/index.ts +++ b/src/commands/recording/index.ts @@ -1,4 +1,4 @@ -import type { RecordOptions } from '../../client/client-types.ts'; +import type { RecordOptions } from '../../contracts/client-api.ts'; import { RECORDING_EXPORT_QUALITIES } from '../../contracts/recording-export-quality.ts'; import { RECORDING_SCOPE_VALUES } from '../../contracts/recording-scope.ts'; import { AppError } from '../../kernel/errors.ts'; diff --git a/src/commands/recording/output.ts b/src/commands/recording/output.ts index 06e136a24..d6443a5ed 100644 --- a/src/commands/recording/output.ts +++ b/src/commands/recording/output.ts @@ -1,4 +1,4 @@ -import type { CommandRequestResult } from '../../client/client-types.ts'; +import type { CommandRequestResult } from '../../contracts/client-api.ts'; import type { CliOutput } from '../command-contract.ts'; import { resultOutput, type CliOutputFormatter } from '../output-common.ts'; diff --git a/src/commands/system/index.ts b/src/commands/system/index.ts index 2b5ea91da..c3aa756d5 100644 --- a/src/commands/system/index.ts +++ b/src/commands/system/index.ts @@ -1,4 +1,4 @@ -import type { ClipboardCommandOptions } from '../../client/client-types.ts'; +import type { ClipboardCommandOptions } from '../../contracts/client-api.ts'; import type { BackMode } from '../../contracts/back-mode.ts'; import { BACK_MODES } from '../../contracts/back-mode.ts'; import { parseDeviceRotation, DEVICE_ROTATIONS } from '../../contracts/device-rotation.ts'; diff --git a/src/contracts/batch-step.ts b/src/contracts/batch-step.ts new file mode 100644 index 000000000..fd03484da --- /dev/null +++ b/src/contracts/batch-step.ts @@ -0,0 +1,17 @@ +import type { SessionRuntimeHints } from '../kernel/contracts.ts'; + +/** + * One step of a daemon batch, as submitted. + * + * Declared here rather than in `core/batch.ts` because the public API vocabulary + * (`contracts/client-api.ts`) is stated in terms of it, and `core/` sits above contracts. The + * `runtime` field used to be written as `DaemonRequest['runtime']`, which pulled the whole daemon + * request type in to say `SessionRuntimeHints` — the same type, one zone lower. + */ +export type DaemonBatchStep = { + command: string; + positionals?: string[]; + input?: Record; + flags?: Record; + runtime?: SessionRuntimeHints; +}; diff --git a/src/contracts/client-api.ts b/src/contracts/client-api.ts new file mode 100644 index 000000000..eb6e58b13 --- /dev/null +++ b/src/contracts/client-api.ts @@ -0,0 +1,1053 @@ +// The public API vocabulary: connection config, the device/session views, and every per-command +// Options/Result shape. Declared here rather than inside `client/` because BOTH `client/` (the +// published Node surface) and `commands/` (the CLI/daemon command surface) are stated in terms of +// it. It used to live in `client/client-types.ts`, which made 28 of the layering gate's 30 +// remaining type-only inversions a single edge: `commands` declaring itself in terms of the client +// it is a peer of. R2 forbids the reverse import, so the shape has to sit below both. +// +// `client/client-types.ts` re-exports all of this, so the published API is byte-identical; it now +// holds only the `AgentDeviceClient` facade that binds these shapes to methods. +// +// The contents below are a verbatim relocation — no declaration was edited in the move. + +import type { PublicSnapshotCaptureAnnotations } from '../contracts/snapshot-capture-annotations.ts'; +import type { SnapshotDiagnosticsSummary } from '../contracts/snapshot-diagnostics.ts'; +import type { + DaemonResponseData, + DaemonInstallSource, + DaemonLockPolicy, + DaemonRequest, + DaemonResponse, + LeaseBackend, + NetworkIncludeMode, + ResponseLevel, + SessionIsolationMode, + SessionRuntimeHints, +} from '../kernel/contracts.ts'; +import type { + AppleOS, + DeviceKind, + DeviceTarget, + PublicPlatform, + PlatformSelector, +} from '../kernel/device.ts'; +import type { BackMode } from '../contracts/back-mode.ts'; +import type { ClickButton } from '../contracts/click-button.ts'; +import type { RecordingExportQuality } from '../contracts/recording-export-quality.ts'; +import type { RecordingScope } from '../contracts/recording-scope.ts'; +import type { + ScrollDirection, + SwipePattern, + SwipePreset, + TransformGestureParams, +} from '../contracts/scroll-gesture.ts'; +import type { GesturePointerCount } from '../contracts/gesture-plan.ts'; +import type { LogAction } from '../contracts/logs.ts'; +import type { SessionSurface } from '../contracts/session-surface.ts'; +import type { FindLocator } from '../selectors/find.ts'; +import type { SnapshotNode, SnapshotUnchanged, SnapshotVisibility } from '../kernel/snapshot.ts'; +import type { ScreenshotResultData } from '../utils/screenshot-result.ts'; +import type { MetroPrepareKind } from '../contracts/metro.ts'; +import type { MetroBridgeScope } from './companion-tunnel-scope.ts'; +import type { AppsFilter } from '../contracts/app-inventory.ts'; +import type { ScreenshotRequestFlags } from '../contracts/screenshot.ts'; +import type { DaemonBatchStep } from './batch-step.ts'; +import type { TargetShutdownResult } from '../contracts/target-shutdown-contract.ts'; +import type { PerfAction, PerfArea, PerfKind, PerfSubject } from '../contracts/perf.ts'; +import type { AlertAction } from '../contracts/alert-contract.ts'; +import type { JsonObject } from '../contracts/json.ts'; +import type { + CloudProviderProfileFields, + RemoteConnectionProfileFields, +} from './remote-config-fields.ts'; +import type { CloudProviderSessionResult } from '../contracts/cloud-artifacts.ts'; + +export type AgentDeviceDaemonTransport = ( + req: Omit, +) => Promise; + +export type AgentDeviceClientConfig = RemoteConnectionProfileFields & + CloudProviderProfileFields & { + session?: string; + lockPolicy?: DaemonLockPolicy; + lockPlatform?: PlatformSelector; + requestId?: string; + sessionIsolation?: SessionIsolationMode; + leaseBackend?: LeaseBackend; + leaseTtlMs?: number; + runtime?: SessionRuntimeHints; + cwd?: string; + debug?: boolean; + cost?: boolean; + responseLevel?: ResponseLevel; + iosXctestrunFile?: string; + iosXctestDerivedDataPath?: string; + iosXctestEnvDir?: string; + }; + +export type AgentDeviceRequestOverrides = Pick< + AgentDeviceClientConfig, + | 'session' + | 'lockPolicy' + | 'lockPlatform' + | 'requestId' + | 'daemonBaseUrl' + | 'daemonAuthToken' + | 'daemonTransport' + | 'daemonServerMode' + | 'tenant' + | 'sessionIsolation' + | 'runId' + | 'leaseId' + | 'leaseBackend' + | 'leaseProvider' + | 'deviceKey' + | 'clientId' + | 'providerApp' + | 'providerOsVersion' + | 'providerProject' + | 'providerBuild' + | 'providerSessionName' + | 'awsProjectArn' + | 'awsDeviceArn' + | 'awsAppArn' + | 'awsRegion' + | 'awsInteractionMode' + | 'leaseTtlMs' + | 'cwd' + | 'debug' + | 'cost' + | 'responseLevel' + | 'iosXctestrunFile' + | 'iosXctestDerivedDataPath' + | 'iosXctestEnvDir' +>; + +export type AgentDeviceIdentifiers = { + session?: string; + deviceId?: string; + deviceName?: string; + udid?: string; + serial?: string; + appId?: string; + appBundleId?: string; + package?: string; +}; + +export type AgentDeviceSelectionOptions = { + platform?: PlatformSelector; + target?: DeviceTarget; + device?: string; + udid?: string; + serial?: string; + iosSimulatorDeviceSet?: string; + androidDeviceAllowlist?: string; +}; + +export type AgentDeviceDevice = { + platform: PublicPlatform; + target: DeviceTarget; + kind: DeviceKind; + id: string; + name: string; + booted?: boolean; + /** + * Additive Apple-OS discriminant (iPhone/iPad/tvOS/visionOS/macOS). Present only for + * Apple devices; `platform` still carries the leaf (`ios`/`macos`). + */ + appleOs?: AppleOS; + identifiers: AgentDeviceIdentifiers; + ios?: { + udid: string; + }; + android?: { + serial: string; + }; + vega?: { + serial: string; + }; +}; + +export type AgentDeviceCapabilitiesResult = { + device: AgentDeviceDevice; + availableCommands: string[]; +}; + +export type AgentDeviceSessionDevice = { + platform: PublicPlatform; + target: DeviceTarget; + id: string; + name: string; + /** + * Additive Apple-OS discriminant (iPhone/iPad/tvOS/visionOS/macOS). Present only for + * Apple devices; `platform` still carries the leaf (`ios`/`macos`). + */ + appleOs?: AppleOS; + identifiers: AgentDeviceIdentifiers; + ios?: { + udid: string; + simulatorSetPath?: string | null; + }; + android?: { + serial: string; + }; + vega?: { + serial: string; + }; +}; + +export type AgentDeviceSession = { + name: string; + createdAt: number; + sessionStateDir?: string; + runnerLogPath?: string; + device: AgentDeviceSessionDevice; + identifiers: AgentDeviceIdentifiers; +}; + +export type StartupPerfSample = { + durationMs: number; + measuredAt: string; + method: string; + appTarget?: string; + appBundleId?: string; +}; + +export type SessionCloseResult = { + session: string; + shutdown?: TargetShutdownResult; + provider?: CloudProviderSessionResult; + /** + * #1258: absolute path of the committed session/healed script when this close + * published one (`close --save-script`, or a repair-armed session's finalize) + * — so a client that requested publication learns where the file landed. + */ + savedScript?: string; + identifiers: AgentDeviceIdentifiers; +}; + +export type SessionSaveScriptOptions = AgentDeviceRequestOverrides & { + path?: string; + /** Atomically replace an existing target instead of refusing publication. */ + force?: boolean; +}; + +export type SessionSaveScriptResult = { + session: string; + savedScript: string; + actionCount: number; + identifiers: AgentDeviceIdentifiers; +}; + +export type CloudArtifactsOptions = AgentDeviceRequestOverrides & { + provider?: string; + providerSessionId?: string; +}; + +export type AppInstallOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + app?: string; + appPath: string; + }; + +export type AppDeployOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + app: string; + appPath: string; + }; + +export type AppDeployResult = { + app: string; + appPath: string; + platform: PublicPlatform; + appId?: string; + bundleId?: string; + package?: string; + identifiers: AgentDeviceIdentifiers; +}; + +export type AppOpenOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + app?: string; + url?: string; + surface?: SessionSurface; + activity?: string; + launchConsole?: string; + launchArgs?: string[]; + relaunch?: boolean; + saveScript?: boolean | string; + /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ + force?: boolean; + deviceHub?: boolean; + testIme?: boolean; + noRecord?: boolean; + runtime?: SessionRuntimeHints; + }; + +export type AppOpenResult = { + session: string; + warnings?: string[]; + sessionStateDir?: string; + runnerLogPath?: string; + requestLogPath?: string; + eventLogPath?: string; + appName?: string; + appBundleId?: string; + appId?: string; + startup?: StartupPerfSample; + runtime?: SessionRuntimeHints; + device?: AgentDeviceSessionDevice; + identifiers: AgentDeviceIdentifiers; +}; + +export type AppCloseOptions = AgentDeviceRequestOverrides & { + app?: string; + shutdown?: boolean; + saveScript?: boolean | string; + /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ + force?: boolean; +}; + +export type AppCloseResult = { + session: string; + closedApp?: string; + shutdown?: TargetShutdownResult; + /** + * #1258: absolute path of the committed session/healed script when this close + * published one (`close --save-script`, or a repair-armed session's finalize) + * — so a client that requested publication learns where the file landed. + */ + savedScript?: string; + identifiers: AgentDeviceIdentifiers; +}; + +export type AppInstallFromSourceOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + source: DaemonInstallSource; + retainPaths?: boolean; + retentionMs?: number; + }; + +export type AppInstallFromSourceResult = { + appName?: string; + appId?: string; + bundleId?: string; + packageName?: string; + launchTarget: string; + installablePath?: string; + archivePath?: string; + materializationId?: string; + materializationExpiresAt?: string; + identifiers: AgentDeviceIdentifiers; +}; + +export type AppListOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + appsFilter?: AppsFilter; + }; + +export type MaterializationReleaseOptions = AgentDeviceRequestOverrides & { + materializationId: string; +}; + +export type MaterializationReleaseResult = { + released: boolean; + materializationId: string; + identifiers: AgentDeviceIdentifiers; +}; + +export type Lease = { + leaseId: string; + tenantId: string; + runId: string; + backend: LeaseBackend; + leaseProvider?: string; + deviceKey?: string; + clientId?: string; + createdAt?: number; + heartbeatAt?: number; + expiresAt?: number; +}; + +export type LeaseOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + ttlMs?: number; + }; + +export type LeaseAllocateOptions = LeaseOptions & { + tenant: string; + runId: string; + leaseBackend?: LeaseBackend; + leaseProvider?: string; + provider?: string; + deviceKey?: string; + clientId?: string; +}; + +export type LeaseScopedOptions = LeaseOptions & { + tenant?: string; + runId?: string; + leaseId: string; + leaseBackend?: LeaseBackend; + leaseProvider?: string; + provider?: string; + deviceKey?: string; + clientId?: string; +}; + +export type MetroPrepareOptions = { + projectRoot?: string; + kind?: MetroPrepareKind; + publicBaseUrl?: string; + proxyBaseUrl?: string; + bearerToken?: string; + bridgeScope?: MetroBridgeScope; + launchUrl?: string; + companionProfileKey?: string; + companionConsumerKey?: string; + port?: number; + listenHost?: string; + statusHost?: string; + startupTimeoutMs?: number; + probeTimeoutMs?: number; + reuseExisting?: boolean; + installDependenciesIfNeeded?: boolean; + runtimeFilePath?: string; + logPath?: string; +}; + +export type MetroReloadOptions = { + metroHost?: string; + metroPort?: number; + bundleUrl?: string; + timeoutMs?: number; +}; + +export type CaptureSnapshotOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + interactiveOnly?: boolean; + depth?: number; + scope?: string; + raw?: boolean; + forceFull?: boolean; + timeoutMs?: number; + /** + * #1271 stage 2 (ADR 0012 amendment): `snapshot` is observation-only and + * excluded from a repair-armed heal by default; `record` forces it + * through. Mutually exclusive with `noRecord`. + */ + noRecord?: boolean; + record?: boolean; + }; + +export type CaptureSnapshotResult = { + nodes: SnapshotNode[]; + truncated: boolean; + appName?: string; + appBundleId?: string; + visibility?: SnapshotVisibility; + unchanged?: SnapshotUnchanged; + snapshotDiagnostics?: SnapshotDiagnosticsSummary; + identifiers: AgentDeviceIdentifiers; + /** + * ADR 0014: the response-level ref-frame epoch the plain node refs were minted + * from. A ref-issuing snapshot carries it ONCE (nodes stay plain `@e12` for the + * token budget); pair a ref with it (`@e12~s`) before a mutation. + */ + refsGeneration?: number; + /** + * Digest response view only: a capped list of `{ ref, label? }` pairs taken + * from the full `nodes` tree so the MCP layer can still pin refs when the + * default-level `nodes` payload is intentionally omitted. + */ + refs?: Array<{ ref: string; label?: string }>; +} & PublicSnapshotCaptureAnnotations; + +export type CaptureScreenshotOptions = AgentDeviceRequestOverrides & { + path?: string; + overlayRefs?: boolean; + pixelDensity?: number; + fullscreen?: boolean; + maxSize?: number; + stabilize?: boolean; + normalizeStatusBar?: boolean; + surface?: SessionSurface; +}; + +export type CaptureScreenshotResult = ScreenshotResultData & { + path: string; + identifiers: AgentDeviceIdentifiers; +}; + +export type DeviceCommandBaseOptions = AgentDeviceRequestOverrides & AgentDeviceSelectionOptions; + +type WaitCommandTarget = + | { + durationMs: number; + text?: never; + ref?: never; + selector?: never; + stable?: never; + quietMs?: never; + timeoutMs?: never; + } + | (SelectorSnapshotCommandOptions & { + text: string; + durationMs?: never; + ref?: never; + selector?: never; + stable?: never; + quietMs?: never; + timeoutMs?: number; + }) + | (SelectorSnapshotCommandOptions & { + ref: string; + durationMs?: never; + text?: never; + selector?: never; + stable?: never; + quietMs?: never; + timeoutMs?: number; + }) + | (SelectorSnapshotCommandOptions & { + selector: string; + durationMs?: never; + text?: never; + ref?: never; + stable?: never; + quietMs?: never; + timeoutMs?: number; + }) + | (SelectorSnapshotCommandOptions & { + stable: true; + durationMs?: never; + text?: never; + ref?: never; + selector?: never; + quietMs?: number; + timeoutMs?: number; + }); + +export type WaitCommandOptions = DeviceCommandBaseOptions & WaitCommandTarget; + +export type AlertCommandOptions = DeviceCommandBaseOptions & { + action?: AlertAction; + timeoutMs?: number; +}; + +export type AppStateCommandOptions = DeviceCommandBaseOptions; + +export type KeyboardCommandOptions = DeviceCommandBaseOptions & { + action?: 'status' | 'dismiss' | 'enter' | 'return'; +}; + +export type ClipboardCommandOptions = + | (DeviceCommandBaseOptions & { + action: 'read'; + }) + | (DeviceCommandBaseOptions & { + action: 'write'; + text: string; + }); + +export type ReactNativeCommandOptions = DeviceCommandBaseOptions & { + action: 'dismiss-overlay'; +}; + +export type PrepareCommandOptions = DeviceCommandBaseOptions & { + action: 'ios-runner'; + timeoutMs?: number; +}; + +export type DoctorCommandOptions = DeviceCommandBaseOptions & { + targetApp?: string; + remote?: boolean; +}; + +export type ViewportCommandOptions = DeviceCommandBaseOptions & { + width: number; + height: number; +}; + +type SelectorSnapshotCommandOptions = Pick; +type FindSnapshotCommandOptions = Pick; + +type PointTarget = { + x: number; + y: number; + ref?: never; + selector?: never; + label?: never; +}; + +type RefTarget = { + ref: string; + label?: string; + x?: never; + y?: never; + selector?: never; +}; + +type SelectorTarget = { + selector: string; + x?: never; + y?: never; + ref?: never; + label?: never; +}; + +export type InteractionTarget = PointTarget | RefTarget | SelectorTarget; + +export type ElementTarget = RefTarget | SelectorTarget; + +type RepeatedPressOptions = { + count?: number; + intervalMs?: number; + holdMs?: number; + jitterPx?: number; + doubleTap?: boolean; +}; + +export type DeviceBootOptions = DeviceCommandBaseOptions & { + headless?: boolean; +}; + +export type DeviceShutdownOptions = DeviceCommandBaseOptions; + +export type AppPushOptions = DeviceCommandBaseOptions & { + app: string; + payload: string | JsonObject; +}; + +export type AppTriggerEventOptions = DeviceCommandBaseOptions & { + event: string; + payload?: JsonObject; +}; + +export type CaptureDiffOptions = DeviceCommandBaseOptions & + Pick & { + kind: 'snapshot'; + out?: string; + }; + +/** + * Opt-in (#1101): after the action, wait for the UI to go quiet and return the + * settled diff vs the pre-action tree (`settle` on the result) in the same + * response. Best-effort — never fails the action. `settleQuietMs` tunes the + * quiet window (default 500ms); `timeoutMs` bounds the settle wait (default + * 10s) when `settle` is true. A bare `timeoutMs` without `settle` is ignored + * for compatibility; `settleQuietMs` still requires `settle`. + */ +type SettleCommandOptions = { + settle?: boolean; + settleQuietMs?: number; + timeoutMs?: number; +}; + +export type ClickOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + InteractionTarget & + RepeatedPressOptions & + SettleCommandOptions & { + button?: ClickButton; + /** + * Opt-in (#1047): return cheap post-action evidence (AX digest, node counts, + * changedFromBefore) in the response instead of requiring a follow-up + * snapshot to confirm the action had an effect. + */ + verify?: boolean; + }; + +export type PressOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + InteractionTarget & + RepeatedPressOptions & + SettleCommandOptions & { + verify?: boolean; + }; + +export type LongPressOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + InteractionTarget & + SettleCommandOptions & { + durationMs?: number; + }; + +export type SwipeOptions = DeviceCommandBaseOptions & { + from: { x: number; y: number }; + to: { x: number; y: number }; + count?: number; + pauseMs?: number; + pattern?: SwipePattern; +}; + +export type PanOptions = DeviceCommandBaseOptions & { + x: number; + y: number; + dx: number; + dy: number; + pointerCount?: GesturePointerCount; + durationMs?: number; +}; + +export type FlingOptions = DeviceCommandBaseOptions & { + direction: ScrollDirection; + x: number; + y: number; + distance?: number; +}; + +export type SwipeGestureOptions = DeviceCommandBaseOptions & { + preset: SwipePreset; +}; + +export type FocusOptions = DeviceCommandBaseOptions & { + x: number; + y: number; +}; + +export type TypeTextOptions = DeviceCommandBaseOptions & { + text: string; + delayMs?: number; +}; + +export type FillOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + InteractionTarget & + SettleCommandOptions & { + text: string; + delayMs?: number; + /** Publish this fill value as `${VAR}` when script recording is armed. */ + recordAs?: string; + verify?: boolean; + }; + +export type PinchOptions = DeviceCommandBaseOptions & { + scale: number; + x?: number; + y?: number; +}; + +export type RotateGestureOptions = DeviceCommandBaseOptions & { + degrees: number; + x?: number; + y?: number; +}; + +export type TransformGestureOptions = DeviceCommandBaseOptions & TransformGestureParams; + +/** + * #1271 stage 2 (ADR 0012 amendment): `get`/`is`/`find` are observation-only + * and excluded from a repair-armed heal by default. `record` forces this + * action through (the corrective-read case); `noRecord` continues to opt the + * action out entirely. Mutually exclusive. + */ +type RecordControlOptions = { + noRecord?: boolean; + record?: boolean; +}; + +export type GetOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + ElementTarget & + RecordControlOptions & { + format: 'text' | 'attrs'; + }; + +type IsTextPredicateOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + RecordControlOptions & { + predicate: 'text'; + selector: string; + value: string; + }; + +type IsStatePredicateOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + RecordControlOptions & { + predicate: 'visible' | 'hidden' | 'exists' | 'editable' | 'selected' | 'focused'; + selector: string; + value?: never; + }; + +export type IsOptions = IsTextPredicateOptions | IsStatePredicateOptions; + +type FindBaseOptions = DeviceCommandBaseOptions & + FindSnapshotCommandOptions & + RecordControlOptions & { + locator?: FindLocator; + query: string; + first?: boolean; + last?: boolean; + }; + +export type FindOptions = + | (FindBaseOptions & { action?: 'click' | 'focus' | 'exists' | 'getText' | 'getAttrs' }) + | (FindBaseOptions & { action: 'wait'; timeoutMs?: number }) + | (FindBaseOptions & { action: 'fill' | 'type'; value: string }); + +export type ReplayRunOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + path: string; + runtime?: SessionRuntimeHints; + /** + * @deprecated ADR 0012 migration step 6: `--update` no longer rewrites + * the script. Accepted for backward compatibility; every divergence + * already carries ranked selector suggestions regardless of this flag. + */ + update?: boolean; + /** @deprecated Use backend: 'maestro'. */ + maestro?: boolean; + backend?: string; + env?: string[]; + timeoutMs?: number; + /** + * ADR 0012 decision 4 / migration step 5: resume at this 1-based plan + * step, skipping `1..resumeFrom-1` without executing them. Requires + * `resumePlanDigest` from the divergence report that reported this + * step as the failure. `replay` only — `test` has no resume fields. + */ + resumeFrom?: number; + /** The `resume.planDigest` from the divergence report `resumeFrom` came from. */ + resumePlanDigest?: string; + /** + * ADR 0012 decision 6, R1/R6: arms agent-supervised re-record repair + * from this replay attempt onward. Optional string value is the healed + * `.ad`'s output path; absent one, it defaults to the `` sibling + * `.healed.ad` when the repair ends with `close --save-script`. + */ + saveScript?: boolean | string; + /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ + force?: boolean; + }; + +export type ReplayTestOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + paths: string[]; + runtime?: SessionRuntimeHints; + update?: boolean; + /** @deprecated Use backend: 'maestro'. */ + maestro?: boolean; + backend?: string; + env?: string[]; + failFast?: boolean; + timeoutMs?: number; + retries?: number; + recordVideo?: boolean; + artifactsDir?: string; + /** @deprecated Use the CLI --reporter junit: or --report-junit . */ + reportJunit?: string; + shardAll?: number; + shardSplit?: number; + }; + +export type BatchStep = { + command: string; + input: Record; + runtime?: SessionRuntimeHints; +}; + +export type BatchRunOptions = AgentDeviceRequestOverrides & { + steps: BatchStep[]; + onError?: 'stop'; + maxSteps?: number; + out?: string; +}; + +export type PerfOptions = DeviceCommandBaseOptions & { + area?: PerfArea; + subject?: PerfSubject; + action?: PerfAction; + kind?: PerfKind; + template?: string; + out?: string; + tracePath?: string; +}; + +export type LogsOptions = AgentDeviceRequestOverrides & { + action?: LogAction; + message?: string; + restart?: boolean; +}; + +export type EventsOptions = AgentDeviceRequestOverrides & { + cursor?: string; + limit?: number; +}; + +export type NetworkOptions = AgentDeviceRequestOverrides & { + action?: 'dump' | 'log'; + limit?: number; + include?: NetworkIncludeMode; +}; + +export type AudioOptions = AgentDeviceRequestOverrides & { + action?: 'probe'; + probeAction?: 'start' | 'status' | 'stop'; + durationMs?: number; + bucketMs?: number; +}; + +export type RecordOptions = AgentDeviceRequestOverrides & { + action: 'start' | 'stop'; + path?: string; + fps?: number; + maxSize?: number; + quality?: RecordingExportQuality; + hideTouches?: boolean; + recordingScope?: RecordingScope; +}; + +export type TraceOptions = AgentDeviceRequestOverrides & { + action: 'start' | 'stop'; + path?: string; +}; + +export type PermissionTarget = + | 'camera' + | 'microphone' + | 'photos' + | 'contacts' + | 'contacts-limited' + | 'notifications' + | 'calendar' + | 'location' + | 'location-always' + | 'media-library' + | 'motion' + | 'reminders' + | 'siri' + | 'accessibility' + | 'screen-recording' + | 'input-monitoring'; + +export type SettingsUpdateOptions = + | (DeviceCommandBaseOptions & { + setting: 'clear-app-state'; + state: 'clear'; + app?: string; + }) + | (DeviceCommandBaseOptions & { + setting: 'wifi' | 'airplane' | 'location'; + state: 'on' | 'off'; + }) + | (DeviceCommandBaseOptions & { + setting: 'location'; + state: 'set'; + latitude: number; + longitude: number; + }) + | (DeviceCommandBaseOptions & { + setting: 'animations'; + state: 'on' | 'off'; + }) + | (DeviceCommandBaseOptions & { + setting: 'appearance'; + state: 'light' | 'dark' | 'toggle'; + }) + | (DeviceCommandBaseOptions & { + setting: 'faceid' | 'touchid'; + state: 'match' | 'nonmatch' | 'enroll' | 'unenroll'; + }) + | (DeviceCommandBaseOptions & { + setting: 'fingerprint'; + state: 'match' | 'nonmatch'; + }) + | (DeviceCommandBaseOptions & { + setting: 'permission'; + state: 'grant' | 'deny' | 'reset'; + permission: PermissionTarget; + mode?: 'full' | 'limited'; + }); + +type CommandExecutionOptions = Partial & { + positionals?: string[]; + kind?: string; + out?: string; + artifact?: string; + dsym?: string; + searchPath?: string; + interactiveOnly?: boolean; + depth?: number; + scope?: string; + raw?: boolean; + forceFull?: boolean; + count?: number; + fps?: number; + maxSize?: number; + recordingScope?: RecordingScope; + quality?: RecordingExportQuality; + hideTouches?: boolean; + intervalMs?: number; + delayMs?: number; + durationMs?: number; + holdMs?: number; + jitterPx?: number; + pixels?: number; + doubleTap?: boolean; + verify?: boolean; + settle?: boolean; + settleQuietMs?: number; + clickButton?: ClickButton; + pauseMs?: number; + pattern?: SwipePattern; + headless?: boolean; + restart?: boolean; + replayUpdate?: boolean; + replayBackend?: string; + replayEnv?: string[]; + replayShellEnv?: Record; + replayFrom?: number; + replayPlanDigest?: string; + failFast?: boolean; + timeoutMs?: number; + retries?: number; + recordVideo?: boolean; + artifactsDir?: string; + shardAll?: number; + shardSplit?: number; + findFirst?: boolean; + findLast?: boolean; + networkInclude?: NetworkIncludeMode; + batchOnError?: 'stop'; + batchMaxSteps?: number; + batchSteps?: DaemonBatchStep[]; +}; + +export type InternalRequestOptions = AgentDeviceClientConfig & + AgentDeviceSelectionOptions & + CommandExecutionOptions & { + runtime?: SessionRuntimeHints; + overlayRefs?: boolean; + surface?: SessionSurface; + activity?: string; + launchConsole?: string; + launchArgs?: string[]; + relaunch?: boolean; + shutdown?: boolean; + saveScript?: boolean | string; + /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ + force?: boolean; + deviceHub?: boolean; + testIme?: boolean; + noRecord?: boolean; + /** Fill-only script parameter name used to publish `${VAR}` instead of literal text. */ + recordAs?: string; + /** #1271 stage 2: force-record this action; mutually exclusive with `noRecord`. */ + record?: boolean; + backMode?: BackMode; + metroHost?: string; + metroPort?: number; + bundleUrl?: string; + launchUrl?: string; + appsFilter?: AppsFilter; + installSource?: DaemonInstallSource; + retainMaterializedPaths?: boolean; + materializedPathRetentionMs?: number; + materializationId?: string; + leaseTtlMs?: number; + provider?: string; + providerSessionId?: string; + }; + +export type CommandRequestResult = DaemonResponseData; diff --git a/src/contracts/companion-tunnel-scope.ts b/src/contracts/companion-tunnel-scope.ts new file mode 100644 index 000000000..4f27938d5 --- /dev/null +++ b/src/contracts/companion-tunnel-scope.ts @@ -0,0 +1,18 @@ +/** + * The identity a companion tunnel is scoped to. + * + * This shape is shared vocabulary rather than a client concern: the public API surface declares + * command options in terms of it, and `client/`, `metro/` and `cli/` all pass it around. It lived + * inside `client/client-companion-tunnel-contract.ts` next to the env-var names and worker + * options that really are client-local, which made every zone that merely needed the shape + * declare itself in terms of the client. Declaring it below them is what lets the contract be + * shared without the dependency. + */ +export type CompanionTunnelScope = { + tenantId: string; + runId: string; + leaseId: string; +}; + +/** The companion-tunnel scope as Metro's bridge names it. */ +export type MetroBridgeScope = CompanionTunnelScope; diff --git a/src/contracts/remote-config-fields.ts b/src/contracts/remote-config-fields.ts index e99f7df1b..e918d9d27 100644 --- a/src/contracts/remote-config-fields.ts +++ b/src/contracts/remote-config-fields.ts @@ -1,3 +1,10 @@ +import type { + DaemonServerMode, + DaemonTransportPreference, + LeaseBackend, + SessionIsolationMode, +} from '../kernel/contracts.ts'; + // The remote-config profile field groups that `CliFlags` is composed from. // // `remote/` owns reading and validating a profile; the flag vocabulary those fields become @@ -34,3 +41,28 @@ export type RemoteConfigMetroOptions = { metroNoInstallDeps?: boolean; launchUrl?: string; }; + +/** + * How to reach a daemon and which lease/tenant the request belongs to. + * + * Sibling of `CloudProviderProfileFields` above, and here for the same reason: `remote/` owns + * reading and validating a profile, but the field vocabulary is a contract that zones below + * `remote/` are stated in terms of — the public API config is composed from both groups. Leaving + * this one in `remote/` made every consumer of that composition declare itself in terms of a + * rank-4 zone. + */ +export type RemoteConnectionProfileFields = { + stateDir?: string; + daemonBaseUrl?: string; + daemonAuthToken?: string; + daemonTransport?: DaemonTransportPreference; + daemonServerMode?: DaemonServerMode; + tenant?: string; + sessionIsolation?: SessionIsolationMode; + runId?: string; + leaseId?: string; + leaseBackend?: LeaseBackend; + leaseProvider?: string; + deviceKey?: string; + clientId?: string; +}; diff --git a/src/contracts/result-serialization.ts b/src/contracts/result-serialization.ts index 4995cacca..4fcbe432a 100644 --- a/src/contracts/result-serialization.ts +++ b/src/contracts/result-serialization.ts @@ -9,7 +9,7 @@ import type { AppOpenResult, CaptureSnapshotResult, SessionCloseResult, -} from '../client/client-types.ts'; +} from './client-api.ts'; import { publicSnapshotCaptureAnnotations, type SnapshotCaptureAnnotations, diff --git a/src/core/batch.ts b/src/core/batch.ts index eedcff353..ad9a4ed2c 100644 --- a/src/core/batch.ts +++ b/src/core/batch.ts @@ -1,3 +1,7 @@ +// The step SHAPE lives in contracts/ so the public API vocabulary can be stated in terms of it +// without depending on core/; re-exported here for this module's existing consumers. +export type { DaemonBatchStep } from '../contracts/batch-step.ts'; +import type { DaemonBatchStep } from '../contracts/batch-step.ts'; import { type DaemonRequest, type DaemonResponse, @@ -21,14 +25,6 @@ import { const batchAllowedStepKeys = new Set(BATCH_DAEMON_STEP_KEYS); -export type DaemonBatchStep = { - command: string; - positionals?: string[]; - input?: Record; - flags?: Record; - runtime?: DaemonRequest['runtime']; -}; - export type BatchFlags = Record & { batchOnError?: 'stop'; batchMaxSteps?: number; diff --git a/src/daemon/handlers/__tests__/snapshot-handler.test.ts b/src/daemon/handlers/__tests__/snapshot-handler.test.ts index f028cc1d9..1c031ff28 100644 --- a/src/daemon/handlers/__tests__/snapshot-handler.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-handler.test.ts @@ -13,7 +13,7 @@ import { buildSnapshotSignatures } from '../../android-snapshot-freshness.ts'; import { buildInteractionSurfaceSignature } from '../../interaction-outcome-policy.ts'; import { buildSnapshotPresentationKey } from '../../../kernel/snapshot.ts'; import { snapshotCliOutput } from '../../../commands/capture/output.ts'; -import type { CaptureSnapshotResult } from '../../../client/client-types.ts'; +import type { CaptureSnapshotResult } from '../../../contracts/client-api.ts'; vi.mock('../../../core/dispatch.ts', async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/mcp/__tests__/command-tools-parity.test.ts b/src/mcp/__tests__/command-tools-parity.test.ts index 4887d8964..64ca2fcd4 100644 --- a/src/mcp/__tests__/command-tools-parity.test.ts +++ b/src/mcp/__tests__/command-tools-parity.test.ts @@ -4,7 +4,8 @@ import os from 'node:os'; import path from 'node:path'; import { afterEach, test, vi } from 'vitest'; import { createAgentDeviceClient } from '../../agent-device-client.ts'; -import type { AgentDeviceClient, AgentDeviceDaemonTransport } from '../../client/client-types.ts'; +import type { AgentDeviceDaemonTransport } from '../../contracts/client-api.ts'; +import type { AgentDeviceClient } from '../../client/client-types.ts'; import type { CommandExecutionResult } from '../../commands/command-surface.ts'; import { createCommandToolExecutor, listCommandTools } from '../command-tools.ts'; import { validateAgainstSchema } from './output-schema-validator.ts'; diff --git a/src/mcp/command-tools.ts b/src/mcp/command-tools.ts index 6d21dcc29..b63a8a4cd 100644 --- a/src/mcp/command-tools.ts +++ b/src/mcp/command-tools.ts @@ -1,4 +1,5 @@ -import type { AgentDeviceClient, AgentDeviceClientConfig } from '../client/client-types.ts'; +import type { AgentDeviceClientConfig } from '../contracts/client-api.ts'; +import type { AgentDeviceClient } from '../client/client-types.ts'; import type { JsonSchema } from '../commands/command-contract.ts'; import type { CommandExecutionResult } from '../commands/command-surface.ts'; import { RESPONSE_LEVELS, type ResponseLevel } from '../kernel/contracts.ts'; diff --git a/src/remote/remote-config-schema.ts b/src/remote/remote-config-schema.ts index df729c796..02bcd9032 100644 --- a/src/remote/remote-config-schema.ts +++ b/src/remote/remote-config-schema.ts @@ -1,32 +1,14 @@ import type { CloudProviderProfileFields, RemoteConfigMetroOptions, + RemoteConnectionProfileFields, } from '../contracts/remote-config-fields.ts'; +// Declared in contracts/ so zones below remote/ can be stated in terms of the field vocabulary; +// re-exported here because this module is where consumers already import it from. +export type { RemoteConnectionProfileFields } from '../contracts/remote-config-fields.ts'; import { buildPrimaryEnvVarName } from '../utils/source-value.ts'; -import type { - DaemonServerMode, - DaemonTransportPreference, - LeaseBackend, - SessionIsolationMode, -} from '../kernel/contracts.ts'; import { PLATFORM_SELECTORS, type DeviceTarget, type PlatformSelector } from '../kernel/device.ts'; -export type RemoteConnectionProfileFields = { - stateDir?: string; - daemonBaseUrl?: string; - daemonAuthToken?: string; - daemonTransport?: DaemonTransportPreference; - daemonServerMode?: DaemonServerMode; - tenant?: string; - sessionIsolation?: SessionIsolationMode; - runId?: string; - leaseId?: string; - leaseBackend?: LeaseBackend; - leaseProvider?: string; - deviceKey?: string; - clientId?: string; -}; - export type RemoteConfigProfile = RemoteConfigMetroOptions & CloudProviderProfileFields & RemoteConnectionProfileFields & { diff --git a/test/integration/provider-scenarios/harness.ts b/test/integration/provider-scenarios/harness.ts index b8a2e5e18..1515ee61f 100644 --- a/test/integration/provider-scenarios/harness.ts +++ b/test/integration/provider-scenarios/harness.ts @@ -3,10 +3,8 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { createAgentDeviceClient } from '../../../src/agent-device-client.ts'; -import type { - AgentDeviceClient, - AgentDeviceDaemonTransport, -} from '../../../src/client/client-types.ts'; +import type { AgentDeviceDaemonTransport } from '../../../src/contracts/client-api.ts'; +import type { AgentDeviceClient } from '../../../src/client/client-types.ts'; import { createRequestHandler, type RequestRouterDeps, diff --git a/test/output-economy/fixtures.ts b/test/output-economy/fixtures.ts index 9580792ca..9074ed745 100644 --- a/test/output-economy/fixtures.ts +++ b/test/output-economy/fixtures.ts @@ -1,4 +1,7 @@ -import type { CaptureSnapshotResult, CommandRequestResult } from '../../src/client/client-types.ts'; +import type { + CaptureSnapshotResult, + CommandRequestResult, +} from '../../src/contracts/client-api.ts'; import type { DaemonResponseData } from '../../src/daemon/types.ts'; import { AppError } from '../../src/kernel/errors.ts'; import { attachRefs, type RawSnapshotNode } from '../../src/kernel/snapshot.ts'; diff --git a/test/output-economy/routine-workflow.ts b/test/output-economy/routine-workflow.ts index 8fedc04f2..2015ce811 100644 --- a/test/output-economy/routine-workflow.ts +++ b/test/output-economy/routine-workflow.ts @@ -1,4 +1,5 @@ -import type { AgentDeviceClient, CommandRequestResult } from '../../src/client/client-types.ts'; +import type { CommandRequestResult } from '../../src/contracts/client-api.ts'; +import type { AgentDeviceClient } from '../../src/client/client-types.ts'; import { AppError, normalizeError, type NormalizedError } from '../../src/kernel/errors.ts'; import { snapshotCliOutput } from '../../src/commands/capture/output.ts'; import { interactionCliOutputFormatters } from '../../src/commands/interaction/output.ts'; From c2f53e9033a2e3473df5758561a497f2aab63879 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 08:41:36 +0000 Subject: [PATCH 2/6] docs: drop the graph viewer, keep the query that replaces it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rendered dependency-graph viewer is not being merged (PR #1409 closed). It cost ~2200 lines plus a Fallow exemption for a 920-line canvas renderer, and nobody — human or agent — reached a conclusion from the picture. Every finding in this document came from short queries against the gate's own model. This file pointed at the `claude/depgraph-viewer` branch for the tooling, which would have dangled once that branch is deleted. Replaced with the thing that was actually load-bearing: a throwaway probe script, inlined, that re-derives the numbers from `scripts/layering/model.ts` and nothing else. Verified verbatim — it reproduces TYPE_INVERSION_BASELINE exactly, which is also the check that tells you whether either side has gone stale. Two numbers in the summary table were stale, describing an intermediate state rather than what shipped: R6 said "35 across 4" (actually 18 across 5 after the vocabulary move) and ranked coverage said "729 of 894" (actually 888 of 901). Both corrected, along with the file/edge counts in the header. Also notes the deduplication detail that makes the query agree with the gate: each file pair counts once, so a raw edge count reads higher. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur --- docs/dependency-graph-findings.md | 46 ++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/docs/dependency-graph-findings.md b/docs/dependency-graph-findings.md index ddc8f6126..d360fbf7c 100644 --- a/docs/dependency-graph-findings.md +++ b/docs/dependency-graph-findings.md @@ -1,19 +1,51 @@ # Dependency graph findings -Snapshot analysis of the production import graph — 894 files, 4619 edges, 25 zones — taken while -doing the boundary work in this branch. It is a dated observation, not a normative document; when -it disagrees with `scripts/layering/`, the gate wins. +Snapshot analysis of the production import graph — 901 files, 4768 edges, 25 zones — taken while +doing the boundary work across #1405 and its follow-up. It is a dated observation, not a normative +document; when it disagrees with `scripts/layering/`, the gate wins. + +There is no graph tool to install. A rendered viewer was built and then dropped: it cost ~2200 +lines and a Fallow exemption, and nobody — human or agent — drew a conclusion from the picture. +Every finding below came from short queries against the gate's own model, which is the point worth +keeping. Re-derive any of them like this: + +```ts +// scripts/layering/.probe.ts (throwaway; the gate's model is the only dependency) +import fs from 'node:fs'; +import { listSourceFiles } from './check.ts'; +import { resolveImportEdges, typeInversionPair, backEdgePair } from './model.ts'; + +const files = listSourceFiles(); +const sources = new Map(files.map((f) => [f, fs.readFileSync(f, 'utf8')])); +const edges = resolveImportEdges(sources); + +// e.g. R6 inversions per zone pair, deduplicated by file pair — reproduces +// TYPE_INVERSION_BASELINE, so a mismatch means one of the two is stale. +const seen = new Set(); +const byPair = new Map(); +for (const edge of edges) { + const pair = typeInversionPair(edge); + if (!pair) continue; + const id = `${edge.file} -> ${edge.target}`; + if (seen.has(id)) continue; + seen.add(id); + byPair.set(pair, (byPair.get(pair) ?? 0) + 1); +} +console.log([...byPair].sort((a, b) => b[1] - a[1])); +``` -The graph tool that produced it lives on the `claude/depgraph-viewer` branch (`pnpm depgraph`), -deliberately kept out of this change so the refactor stands on its own. +Run with `node --experimental-strip-types scripts/layering/.probe.ts` and delete it after. Swap +`typeInversionPair` for `backEdgePair` for R5, or group `edges` by `fromZone`/`toZone` for +zone-level traffic. Deduplicating by file pair matters: the gate counts each file pair once, so a +raw edge count reads higher. ## Where this round landed | | before | after | | -------------------------------------------------------- | ---------------------- | ----------------------------------------------- | -| type-only spine inversions (R6) | 61 across 6 zone pairs | **35 across 4**, ratcheted | +| type-only spine inversions (R6) | 61 across 6 zone pairs | **18 across 5**, ratcheted | | files in the unranked `(root)` zone | 29 | **13** — entrypoints and composition roots only | -| files covered by the ranked spine | 651 of 892 | **729 of 894** | +| files covered by the ranked spine | 713 of 898 | **888 of 901** | | type imports pointing at a re-export hub in another zone | 89 | **0** | | selector-command rules stated in both zones | 10 messages, 3 drifts | **0** — shared in `selectors/` | | modules writing ADR 0014's four ref-frame fields | 2 | **1**, enforced by R7 | From 547109e54f468a2395048015dc7a0d1fa9555c49 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 09:19:29 +0000 Subject: [PATCH 3/6] refactor(contracts): move the four keystones that pinned the rest of the inversions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R6 type-only spine inversions: 18 -> 7, and every one of the 7 that remains is a deliberate architectural position rather than a misplaced declaration. Four keystones moved to contracts/, each of which was pinning a much larger set: - `CommandFlags` (was core/dispatch-context.ts). One rank-2 declaration holding the daemon's request type and every recorded action above it. Its last non-contracts dependency was `DaemonBatchStep`, already moved in 3fdbfe0. - `SessionAction` (was daemon/types.ts). replay/ (6 modules) and compat/maestro/ read and write session scripts; declaring the shape inside the daemon made both depend on the server to describe a file format neither asks it to produce. The daemon still owns the recording — only the shape moved. - `TargetAnnotationV1` shape (was replay/target-identity.ts). ADR 0012 target evidence, written by 8 daemon modules and read by commands/; the parsing and classification logic stays in replay/. - `ScrollInputDirection` and the Metro prepare/reload result payloads, which unblocked `ScrollOptions` and `MetroPrepareResult`/`MetroReloadResult`. `DaemonRequest` also split into the three shapes it had been conflating: the kernel WIRE shape (`flags?: Record`, because a process boundary cannot enforce a vocabulary), the new `contracts/command-request.ts` `CommandRequest` (wire shape with flags typed — what a command surface needs), and the daemon's own refinement (+ `internal?: DaemonRequestInternal`, carrying SessionState callbacks and the admitted lease). core/command-descriptor/ had been importing the third to read `command`, `positionals` and `flags`. Two things deliberately NOT moved, because moving them would add coupling rather than remove it, and the baseline now argues both: - `DaemonCommandDescriptor`/`DaemonCommandRoute` — the route type is `keyof typeof DAEMON_ROUTE_HANDLERS`, derived from what the server implements. Moving it down means re-declaring route names in contracts plus a gate to prove the handler map still covers them. ADR 0003/0008 own that boundary. - `AgentDeviceClient` — used as an opaque handle by 4 files. The facade is built from commands/'s own NAVIGATION_COMMAND_PROJECTIONS, so this is a genuine zone-level cycle; breaking it is a design call about where that registry belongs. R5 is zero here: nothing imports the client at runtime, only its type. Also records the largest structural finding, which R6 does not measure: cycles by edge kind are 1 (value only), 87 (value + type-only), 1 (value + dynamic), 213 (all). At runtime the graph is a clean DAG; the 87-file type-level cluster means no one of those files' types can be read in isolation. Hubs are runtime-contract.ts, commands/runtime-types.ts, backend.ts, commands/runtime-common.ts. Not attempted here — it is a different and much larger change. `pnpm check` green, 4488 unit tests. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur --- CONTEXT.md | 7 ++- docs/dependency-graph-findings.md | 58 +++++++++++++++++++- scripts/layering/check.ts | 50 +++++++++-------- src/client/client-types.ts | 17 +----- src/commands/interaction/index.ts | 8 +-- src/commands/interaction/runtime/gestures.ts | 10 +++- src/commands/metro/index.ts | 8 ++- src/compat/maestro/export-flow.ts | 2 +- src/contracts/client-api.ts | 11 ++++ src/contracts/command-flags.ts | 45 +++++++++++++++ src/contracts/command-request.ts | 24 ++++++++ src/contracts/metro.ts | 50 +++++++++++++++++ src/contracts/ref-frame-effect.ts | 30 ++++++++++ src/contracts/scroll-gesture.ts | 7 +++ src/contracts/session-action.ts | 36 ++++++++++++ src/contracts/target-annotation.ts | 24 ++++++++ src/core/command-descriptor/registry.ts | 22 ++++---- src/core/command-descriptor/types.ts | 6 +- src/core/dispatch-context.ts | 36 ++---------- src/daemon/daemon-command-registry.ts | 21 ++----- src/daemon/types.ts | 39 +++++-------- src/metro/client-metro.ts | 32 +++-------- src/metro/metro-types.ts | 23 +------- src/replay/__tests__/plan-digest.test.ts | 2 +- src/replay/__tests__/script.test.ts | 2 +- src/replay/open-script.ts | 2 +- src/replay/plan-digest.ts | 2 +- src/replay/script-formatting.ts | 2 +- src/replay/script-utils.ts | 2 +- src/replay/script.ts | 2 +- src/replay/target-identity.ts | 31 +++++------ src/replay/vars.ts | 2 +- 32 files changed, 408 insertions(+), 205 deletions(-) create mode 100644 src/contracts/command-flags.ts create mode 100644 src/contracts/command-request.ts create mode 100644 src/contracts/ref-frame-effect.ts create mode 100644 src/contracts/session-action.ts create mode 100644 src/contracts/target-annotation.ts diff --git a/CONTEXT.md b/CONTEXT.md index 197bc7c26..f506895b4 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -271,7 +271,12 @@ The perfect-shape refactor is complete and merged. Its end-state: which R5 ignores by design: a type-only import is free at runtime, but "zone A is declared in terms of zone B" is still a boundary claim, and ranking type edges surfaced 61 inversions the gate had never seen. `TYPE_INVERSION_BASELINE` in `check.ts` holds the remaining pairs with their - counts; the numbers may only shrink, and a new pair fails outright. + counts; the numbers may only shrink, and a new pair fails outright. Down to **7**, and each is a + deliberate position rather than a misplaced declaration: 4 are `AgentDeviceClient` used as an + opaque handle (the facade is built from `commands/`'s own projection registry, so moving it down + is a design call about where that registry belongs, not a file move), and 3 are the ADR 0003 + daemon descriptor, whose route type is `keyof typeof DAEMON_ROUTE_HANDLERS` — derived from what + the server actually implements. Both are explained at the baseline. - SessionState ownership (R7). `SessionStore.get()` returns the live record out of a private Map and `set()` re-puts the same reference, so any `session. = …` in the daemon is a durable write to store-owned state — persistence depends on aliasing, not on an API call. That is diff --git a/docs/dependency-graph-findings.md b/docs/dependency-graph-findings.md index d360fbf7c..f77627485 100644 --- a/docs/dependency-graph-findings.md +++ b/docs/dependency-graph-findings.md @@ -43,7 +43,7 @@ raw edge count reads higher. | | before | after | | -------------------------------------------------------- | ---------------------- | ----------------------------------------------- | -| type-only spine inversions (R6) | 61 across 6 zone pairs | **18 across 5**, ratcheted | +| type-only spine inversions (R6) | 61 across 6 zone pairs | **7 across 4**, ratcheted | | files in the unranked `(root)` zone | 29 | **13** — entrypoints and composition roots only | | files covered by the ranked spine | 713 of 898 | **888 of 901** | | type imports pointing at a re-export hub in another zone | 89 | **0** | @@ -73,6 +73,62 @@ type-only inversions, R7 pins SessionState field ownership, and the shared selec - Still outside every rule: **dynamic** import direction (0 inversions today, nothing watching), and anything inside a zone. +## 0. Where the inversions ended up (and why 7 is the floor for now) + +61 → 7. The last pass moved four keystones, each of which was pinning a much larger set: + +| Keystone moved to `contracts/` | Unblocked | +|---|---| +| `DaemonBatchStep` (was `core/batch.ts`, typed via `DaemonRequest['runtime']`) | `CommandFlags` | +| `CommandFlags` (was `core/dispatch-context.ts`) | `SessionAction`, `CommandRequest` | +| `TargetAnnotationV1` shape (was `replay/target-identity.ts`) | `SessionAction` | +| `ScrollInputDirection`, Metro result payloads | `ScrollOptions`, `MetroPrepareResult`/`MetroReloadResult` | + +Two of those deserve their own note, because the pattern repeats: `DaemonBatchStep`'s `runtime` field +was written `DaemonRequest['runtime']`, pulling the whole daemon request type in to say +`SessionRuntimeHints` — the same type three zones lower. And `CommandFlags` was a single rank-2 +declaration pinning ~80 public API shapes above it. Neither looked like a keystone from the graph; +both were found by asking "what does the target itself import, and what rank is that?" + +`DaemonRequest` also split into the three shapes it had been conflating: + +- `kernel/contracts.ts` — the **wire** shape, `flags?: Record`, because a process + boundary cannot enforce a flag vocabulary; +- `contracts/command-request.ts` `CommandRequest` — the wire shape with `flags` typed. What a command + surface needs to reason about a request without knowing which server runs it; +- `daemon/types.ts` `DaemonRequest` — that plus `internal?: DaemonRequestInternal`, carrying + `SessionState` callbacks and the admitted lease. Server-private, and why it cannot move down. + +`core/command-descriptor/` had been importing the third to read `command`, `positionals` and `flags`. + +**The remaining 7 are positions, not debt.** 4 are `AgentDeviceClient` used as an opaque handle; +the facade is built from `commands/`'s own `NAVIGATION_COMMAND_PROJECTIONS`, so this is a genuine +zone-level cycle and breaking it means deciding where that registry belongs. 3 are the ADR 0003 +daemon descriptor, whose `DaemonCommandRoute` is `keyof typeof DAEMON_ROUTE_HANDLERS` — derived from +what the server implements, so moving it down would mean re-declaring route names plus a gate to +prove the handler map still covers them. Both are argued at `TYPE_INVERSION_BASELINE`. + +## 0b. The biggest structural finding is not an inversion + +Cycle size by edge kind, measured over the whole production graph: + +| edges considered | largest strongly-connected component | +|---|---| +| value only | **1** — no cycles, which is what R4 enforces | +| value + type-only | **87 files** | +| value + dynamic | **1** | +| all kinds | 213 files | + +At runtime the module graph is a clean DAG. The 87-file cluster is purely type-level: you cannot read +the types of any one of those files without transitively reaching all 87. That is not a correctness +problem — types are erased — but it is a comprehension one, and it is the single largest obstacle to +reading a subsystem in isolation. It spans `commands` (33), `daemon` (21), `platforms` (13), `core` +(12), hubs at `runtime-contract.ts` (25 in-cluster dependents), `commands/runtime-types.ts` (21), +`backend.ts` (15), `commands/runtime-common.ts` (12). + +Deliberately not attempted yet: it is a different and much larger change than moving declarations +down, and R6 does not measure it. Worth its own pass, starting at those four hubs. + ## 1. The two remaining type-inversion clusters `TYPE_INVERSION_BASELINE` in `scripts/layering/check.ts` holds both, with the reasoning inline. diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index e171c671f..4555cb547 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -187,36 +187,40 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): Violation[] { }); } -// R6 ratchet: type-only spine inversions, per zone pair. R5 cannot see these (a -// type-only import is free at runtime), but "zone A is declared in terms of zone B" -// is still a boundary claim, and ranking type edges surfaced 61 of them. Down to 18 -// after the public API vocabulary moved to contracts/client-api.ts. What remains: +// R6 ratchet: type-only spine inversions, per zone pair. R5 cannot see these (a type-only import +// is free at runtime), but "zone A is declared in terms of zone B" is still a boundary claim, and +// ranking type edges surfaced 61 of them. Down to 7, and every one of the 7 is now a deliberate +// architectural position rather than a misplaced declaration: // -// commands -> client (5) the eight vocabulary shapes that could NOT move down, -// because each is stated in terms of a HIGHER zone: -// `commands/` (navigation projection, ScrollInputDirection) -// or `metro/` (prepare/reload results). Moving them needs -// those upstream declarations to come down first. -// mcp -> client (1) the AgentDeviceClient facade itself — a real design -// question (should a command surface know the client -// type?), not a declaration in the wrong place. -// core/commands -> daemon-server the ADR 0003 daemon facet: core's descriptor registry -// (6) composes a shape the daemon owns. The daemon keeps the -// VALUES; only the shape needs to move below core. -// replay -> daemon-server (6) `SessionAction`, the recorded-action shape replay reads -// and writes. It belongs in contracts/, but it references -// `CommandFlags` (core), so it moves as a chain. +// commands/mcp -> client (4) `AgentDeviceClient`, used as an opaque handle ("the client this +// command runs against"). It cannot move below `commands/` because +// the facade is BUILT from the command surface's own projection +// registry: AgentDeviceClient -> AgentDeviceCommandClient -> +// ProjectedNavigationCommandClient -> NAVIGATION_COMMAND_PROJECTIONS +// in commands/system/. That is a genuine zone-level cycle, and +// breaking it means deciding where the projection registry belongs — +// a design call, not a file move. R5 is zero here: nothing imports +// the client at runtime, only its type. +// +// core/commands -> daemon-server (3) `DaemonCommandDescriptor` and `DaemonCommandRoute`. +// `DaemonCommandRoute` is `keyof typeof DAEMON_ROUTE_HANDLERS` and +// the descriptor resolves `refFrameEffect` against the daemon's own +// request type, so the shape is derived from what the server +// actually implements. Moving it down would mean re-declaring the +// route names in contracts plus a gate to prove the handler map +// still covers them: more coupling to remove a dependency. ADR +// 0003/0008 own this boundary deliberately. +// +// The counts may only go DOWN. Fixing edges without lowering the number fails too, so the baseline +// cannot quietly stop describing the tree. // -// The counts may only go DOWN. Fixing edges without lowering the number fails too, so the -// baseline cannot quietly stop describing the tree. // Exported so scripts/depgraph can assert its own graph build reproduces it — see the // baseline-parity test there. The gate remains the authority; the report follows. export const TYPE_INVERSION_BASELINE: Readonly> = { - 'commands -> client': 5, + 'commands -> client': 3, 'commands -> daemon-server': 1, - 'core -> daemon-server': 5, + 'core -> daemon-server': 2, 'mcp -> client': 1, - 'replay -> daemon-server': 6, }; function checkTypeInversions(edges: readonly ResolvedImportEdge[]): Violation[] { diff --git a/src/client/client-types.ts b/src/client/client-types.ts index 018b374e6..4a832cbe6 100644 --- a/src/client/client-types.ts +++ b/src/client/client-types.ts @@ -88,7 +88,9 @@ import type { MaterializationReleaseOptions, MaterializationReleaseResult, MetroPrepareOptions, + MetroPrepareResult, MetroReloadOptions, + MetroReloadResult, NetworkOptions, PanOptions, PerfOptions, @@ -100,6 +102,7 @@ import type { ReplayRunOptions, ReplayTestOptions, RotateGestureOptions, + ScrollOptions, SessionCloseResult, SessionSaveScriptOptions, SessionSaveScriptResult, @@ -115,14 +118,11 @@ import type { import type { RotateCommandResult } from '../contracts/navigation.ts'; -import type { ScrollInputDirection } from '../commands/interaction/runtime/gestures.ts'; import type { NavigationCommandOptions, ProjectedNavigationCommandClient, } from '../commands/system/navigation-projection.ts'; -import type { PrepareMetroRuntimeResult, ReloadMetroResult } from '../metro/client-metro.ts'; - import type { BatchRunResult } from '../core/batch.ts'; export type { BatchRunResult } from '../core/batch.ts'; @@ -148,10 +148,6 @@ export type { DiffSnapshotCommandResult } from '../contracts/diff.ts'; export type { RecordingCommandResult, TraceCommandResult } from '../contracts/recording.ts'; export type { ReplayCommandResult, ReplaySuiteResult } from '../contracts/replay.ts'; -export type MetroPrepareResult = PrepareMetroRuntimeResult; - -export type MetroReloadResult = ReloadMetroResult; - export type BackCommandOptions = DeviceCommandBaseOptions & NavigationCommandOptions<'back'>; export type OrientationCommandOptions = DeviceCommandBaseOptions & @@ -204,13 +200,6 @@ type DeprecatedCommandClient = { * for compatibility; `settleQuietMs` still requires `settle`. */ -export type ScrollOptions = DeviceCommandBaseOptions & { - direction: ScrollInputDirection; - amount?: number; - pixels?: number; - durationMs?: number; -}; - /** * #1271 stage 2 (ADR 0012 amendment): `get`/`is`/`find` are observation-only * and excluded from a repair-armed heal by default. `record` forces this diff --git a/src/commands/interaction/index.ts b/src/commands/interaction/index.ts index 97d214f9d..c3fc4edc1 100644 --- a/src/commands/interaction/index.ts +++ b/src/commands/interaction/index.ts @@ -1,22 +1,22 @@ import type { ClickOptions, - FindOptions, FillOptions, + FindOptions, FlingOptions, FocusOptions, GetOptions, + IsOptions, + LongPressOptions, PanOptions, PinchOptions, PressOptions, - IsOptions, - LongPressOptions, RotateGestureOptions, + ScrollOptions, SwipeGestureOptions, SwipeOptions, TransformGestureOptions, TypeTextOptions, } from '../../contracts/client-api.ts'; -import type { ScrollOptions } from '../../client/client-types.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { REPEATED_TOUCH_FLAGS, diff --git a/src/commands/interaction/runtime/gestures.ts b/src/commands/interaction/runtime/gestures.ts index 86a229ad2..46652f82f 100644 --- a/src/commands/interaction/runtime/gestures.ts +++ b/src/commands/interaction/runtime/gestures.ts @@ -1,6 +1,5 @@ import { AppError } from '../../../kernel/errors.ts'; import type { Point } from '../../../kernel/snapshot.ts'; -import type { ScrollDirection } from '../../../contracts/scroll-gesture.ts'; import { assertExclusiveScrollDistanceInputs, honoredScrollDurationMs, @@ -54,8 +53,13 @@ export type LongPressCommandOptions = CommandContext & { export type { LongPressCommandResult }; export type GestureDirection = ScrollDirection; -export const SCROLL_INPUT_DIRECTIONS = ['up', 'down', 'left', 'right', 'top', 'bottom'] as const; -export type ScrollInputDirection = (typeof SCROLL_INPUT_DIRECTIONS)[number]; +// The input vocabulary lives in contracts/scroll-gesture.ts beside the other scroll vocabularies, +// so the public API can declare `ScrollOptions` without depending on this command runtime. +export { + SCROLL_INPUT_DIRECTIONS, + type ScrollInputDirection, +} from '../../../contracts/scroll-gesture.ts'; +import type { ScrollDirection, ScrollInputDirection } from '../../../contracts/scroll-gesture.ts'; export type ScrollTarget = | InteractionTarget diff --git a/src/commands/metro/index.ts b/src/commands/metro/index.ts index d5b0186f3..56d022d87 100644 --- a/src/commands/metro/index.ts +++ b/src/commands/metro/index.ts @@ -1,5 +1,9 @@ -import type { MetroPrepareOptions, MetroReloadOptions } from '../../contracts/client-api.ts'; -import type { MetroPrepareResult, MetroReloadResult } from '../../client/client-types.ts'; +import type { + MetroPrepareOptions, + MetroPrepareResult, + MetroReloadOptions, + MetroReloadResult, +} from '../../contracts/client-api.ts'; import { AppError } from '../../kernel/errors.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { diff --git a/src/compat/maestro/export-flow.ts b/src/compat/maestro/export-flow.ts index 58075b1c8..03554d308 100644 --- a/src/compat/maestro/export-flow.ts +++ b/src/compat/maestro/export-flow.ts @@ -1,4 +1,4 @@ -import type { SessionAction } from '../../daemon/types.ts'; +import type { SessionAction } from '../../contracts/session-action.ts'; import { GESTURE_FLING_DURATION_MS } from '../../contracts/gesture-plan.ts'; import { parseSelectorChain, type Selector } from '../../selectors/index.ts'; import type { SelectorTerm } from '../../selectors/parse.ts'; diff --git a/src/contracts/client-api.ts b/src/contracts/client-api.ts index eb6e58b13..1133be2ab 100644 --- a/src/contracts/client-api.ts +++ b/src/contracts/client-api.ts @@ -1,3 +1,5 @@ +import type { ScrollInputDirection } from './scroll-gesture.ts'; +import type { PrepareMetroRuntimeResult, ReloadMetroResult } from './metro.ts'; // The public API vocabulary: connection config, the device/session views, and every per-command // Options/Result shape. Declared here rather than inside `client/` because BOTH `client/` (the // published Node surface) and `commands/` (the CLI/daemon command surface) are stated in terms of @@ -1051,3 +1053,12 @@ export type InternalRequestOptions = AgentDeviceClientConfig & }; export type CommandRequestResult = DaemonResponseData; + +export type ScrollOptions = DeviceCommandBaseOptions & { + direction: ScrollInputDirection; + amount?: number; + pixels?: number; + durationMs?: number; +}; +export type MetroPrepareResult = PrepareMetroRuntimeResult; +export type MetroReloadResult = ReloadMetroResult; diff --git a/src/contracts/command-flags.ts b/src/contracts/command-flags.ts new file mode 100644 index 000000000..53d297966 --- /dev/null +++ b/src/contracts/command-flags.ts @@ -0,0 +1,45 @@ +import type { CliFlags, DaemonExcludedCliFlag } from './cli-flags.ts'; +import type { DaemonBatchStep } from './batch-step.ts'; +import type { Point } from '../kernel/snapshot.ts'; + +// The flag vocabulary a dispatched command is stated in terms of. +// +// This is the CLI flag set minus the flags that never cross the daemon boundary, plus the +// daemon-side additions. It lives in contracts/ because the zones on both sides of the process +// boundary are declared in terms of it: `core/` composes descriptors around it, `daemon/` narrows +// its request type with it, and `replay/` reads it back off a recorded action. Declaring it in +// `core/` (rank 2) meant every one of those had to reach up or across to say "a command's flags". +// +// The kernel wire type deliberately keeps `flags?: Record` — untyped, because the +// wire cannot enforce a shape. This is the typed view of the same field, one rank above the wire +// and below every consumer. + +export type MaestroRuntimeFlags = { + allowNonHittableCoordinateFallback?: boolean; + expectedTapPoint?: Point; + prewarmRunnerBeforeOpen?: boolean; + screenshotCaptureBackend?: 'runner'; +}; + +export type CommandFlags = Omit & { + batchSteps?: DaemonBatchStep[]; + clearAppState?: boolean; + interactionOutcome?: { + retryOnNoChange?: boolean; + }; + launchArgs?: string[]; + kind?: string; + maestro?: MaestroRuntimeFlags; + postGestureStabilization?: boolean; + snapshotIncludeHiddenContentHints?: boolean; + leaseProvider?: string; + provider?: string; + deviceKey?: string; + clientId?: string; + devicePort?: number; + hostPort?: number; + portReverseName?: string; + replayBackend?: string; + shardCount?: number; + shardIndex?: number; +}; diff --git a/src/contracts/command-request.ts b/src/contracts/command-request.ts new file mode 100644 index 000000000..2dd9099de --- /dev/null +++ b/src/contracts/command-request.ts @@ -0,0 +1,24 @@ +import type { DaemonRequest as WireRequest } from '../kernel/contracts.ts'; +import type { CommandFlags } from './command-flags.ts'; + +/** + * A dispatched command with its flags typed. + * + * Three shapes describe a request, at three ranks, and the difference between them is the point: + * + * - `kernel/contracts.ts` `DaemonRequest` — the WIRE shape. `flags?: Record`, + * because a process boundary cannot enforce a flag vocabulary. + * - this — the wire shape with `flags` narrowed to `CommandFlags`. What a command surface needs to + * reason about a request without knowing anything about the server that will run it. + * - `daemon/types.ts` `DaemonRequest` — this plus `token`/`session` required and + * `internal?: DaemonRequestInternal`, which carries `SessionState` callbacks, the admitted lease + * and the resolved session scope. Server-private, and the reason the daemon's version cannot move + * down here. + * + * `core/command-descriptor/` used to import the daemon's version to read `command`, `positionals` + * and `flags` — reaching up two ranks for three fields, and taking a dependency on the server's + * private extension to get them. + */ +export type CommandRequest = Omit & { + flags?: CommandFlags; +}; diff --git a/src/contracts/metro.ts b/src/contracts/metro.ts index bb019a82a..5522f573e 100644 --- a/src/contracts/metro.ts +++ b/src/contracts/metro.ts @@ -1,4 +1,54 @@ +import type { SessionRuntimeHints } from '../kernel/contracts.ts'; + // Metro vocabulary shared by the command surface (which validates it) and metro/ (which // acts on it). export type MetroPrepareKind = 'auto' | 'react-native' | 'expo' | 'repack'; + +/** A prepare kind after resolution: `auto` is a request, never an outcome. */ +export type ResolvedMetroKind = Exclude; + +export type MetroBridgeResult = { + enabled: boolean; + baseUrl: string; + statusUrl: string; + bundleUrl: string; + iosRuntime: SessionRuntimeHints; + androidRuntime: SessionRuntimeHints; + upstream: { + bundleUrl: string; + host: string; + port: number; + statusUrl: string; + }; + probe: { + reachable: boolean; + statusCode: number; + latencyMs: number; + detail: string; + }; +}; + +export type PrepareMetroRuntimeResult = { + projectRoot: string; + kind: ResolvedMetroKind; + dependenciesInstalled: boolean; + packageManager: string | null; + started: boolean; + reused: boolean; + pid: number; + logPath: string; + statusUrl: string; + runtimeFilePath: string | null; + iosRuntime: SessionRuntimeHints; + androidRuntime: SessionRuntimeHints; + bridge: MetroBridgeResult | null; +}; + +export type ReloadMetroResult = { + reloaded: true; + reloadUrl: string; + status: number; + body: string; + transport: 'http' | 'message-socket'; +}; diff --git a/src/contracts/ref-frame-effect.ts b/src/contracts/ref-frame-effect.ts new file mode 100644 index 000000000..dcde9e167 --- /dev/null +++ b/src/contracts/ref-frame-effect.ts @@ -0,0 +1,30 @@ +// ADR 0014 ref-frame effect classification. +// +// A plain string union, and the only part of the daemon command descriptor that zones below the +// daemon actually need: `core/command-descriptor/registry.ts` classifies each command with it while +// composing the descriptor registry. Declaring it beside `DAEMON_ROUTE_HANDLERS` made core reach up +// four ranks for three string literals. +// +// The rest of the descriptor shape deliberately stays in the daemon: `DaemonCommandRoute` is +// `keyof typeof DAEMON_ROUTE_HANDLERS` and `DaemonRefFrameEffect` resolves against the daemon's own +// request type. Moving those down would mean re-declaring the route names in contracts and adding a +// gate to prove the handler map still covers them — more coupling to remove a dependency, which is +// the wrong trade. ADR 0003/0008 own that boundary. + +/** + * ADR 0014 session ref-frame lifetime. Declares how a daemon command relates to + * the session's authorized ref frame: + * - `preserve`: no successful path changes device-visible element identity, so + * the frame carries through untouched (snapshots, reads, inventory, ...); + * - `may-invalidate`: some successful path crosses a device side effect, so the + * leaf must expire the frame at its side-effect seam when that path runs; + * - `delegated`: an orchestrator (batch/replay/test) whose nested leaves own + * their own transitions — the outer command never expires a frame itself. + * + * This classification is an honesty/completeness guard, NOT the transition site: + * a `may-invalidate` command still calls the ref-frame module only when its + * mutating path is selected. The completeness gate + * (`__tests__/ref-frame-effect.test.ts`) fails if a daemon-projected command + * omits this classification. + */ +export type RefFrameEffect = 'preserve' | 'may-invalidate' | 'delegated'; diff --git a/src/contracts/scroll-gesture.ts b/src/contracts/scroll-gesture.ts index 4ff88cd73..8c194aaba 100644 --- a/src/contracts/scroll-gesture.ts +++ b/src/contracts/scroll-gesture.ts @@ -2,6 +2,13 @@ import { AppError } from '../kernel/errors.ts'; import { defineStringEnum } from '../utils/string-enum.ts'; import type { Rect, SnapshotNode } from '../kernel/snapshot.ts'; +// What a caller may ASK for, as opposed to `ScrollDirection` (what the gesture resolves to): +// `top`/`bottom` are scroll-to-extreme requests with no direction of their own. Declared here +// rather than in the command runtime that resolves them, so the public `ScrollOptions` can be +// stated without depending on `commands/`. +export const SCROLL_INPUT_DIRECTIONS = ['up', 'down', 'left', 'right', 'top', 'bottom'] as const; +export type ScrollInputDirection = (typeof SCROLL_INPUT_DIRECTIONS)[number]; + export const SCROLL_DIRECTIONS = ['up', 'down', 'left', 'right'] as const; export type ScrollDirection = (typeof SCROLL_DIRECTIONS)[number]; export const SWIPE_PRESETS = ['left', 'right', 'left-edge', 'right-edge'] as const; diff --git a/src/contracts/session-action.ts b/src/contracts/session-action.ts new file mode 100644 index 000000000..9a74fda8d --- /dev/null +++ b/src/contracts/session-action.ts @@ -0,0 +1,36 @@ +import type { SessionRuntimeHints } from '../kernel/contracts.ts'; +import type { CommandFlags } from './command-flags.ts'; +import type { TargetAnnotationV1 } from './target-annotation.ts'; + +// One recorded action in a session's script. +// +// `replay/` reads and writes these (6 modules) and `compat/maestro/` exports them, so declaring the +// shape inside `daemon/types.ts` made both zones depend on the daemon server to describe a file +// format neither of them asks the daemon to produce. The daemon still owns the RECORDING — it is +// the only thing that appends actions; this is only the shape they are appended in. +// +// It could not move until `CommandFlags` did, since a recorded action carries the flags it ran +// with: this was the tail of a three-link chain (DaemonBatchStep -> CommandFlags -> SessionAction). + +export type SessionAction = { + ts: number; + command: string; + positionals: string[]; + runtime?: SessionRuntimeHints; + flags: Partial & { + snapshotInteractiveOnly?: boolean; + snapshotDepth?: number; + snapshotScope?: string; + snapshotRaw?: boolean; + launchArgs?: string[]; + saveScript?: boolean | string; + noRecord?: boolean; + }; + result?: Record; + /** + * ADR 0012 decision 3: parsed or record-time-computed `target-v1` + * evidence, written as a comment immediately before this action's line. + * Inert until migration step 4 adds enforcement. + */ + targetEvidence?: TargetAnnotationV1; +}; diff --git a/src/contracts/target-annotation.ts b/src/contracts/target-annotation.ts new file mode 100644 index 000000000..64afb5f36 --- /dev/null +++ b/src/contracts/target-annotation.ts @@ -0,0 +1,24 @@ +// ADR 0012 target evidence: the shape of the `target-v1` annotation recorded beside a replayed +// action. +// +// Shared vocabulary, not a replay internal — the daemon writes it (8 modules), `replay/` parses and +// verifies it, and `commands/` reads it back. It was declared in `replay/target-identity.ts` +// alongside the parsing logic, which meant `SessionAction` could not be stated without depending on +// the replay zone. The parsing and classification logic stays there; only the shape moved. + +export type TargetAncestryEntry = { role: string; label?: string }; +export type TargetScrollRegion = { role: string; id?: string; label?: string }; +export type TargetRect = { x: number; y: number; width: number; height: number }; +export type TargetVerification = 'verified' | 'unverifiable'; + +export type TargetAnnotationV1 = { + id?: string; + role: string; + label?: string; + ancestry: TargetAncestryEntry[]; + sibling: number; + viewportOrder: number; + scrollRegion?: TargetScrollRegion; + rect?: TargetRect; + verification: TargetVerification; +}; diff --git a/src/core/command-descriptor/registry.ts b/src/core/command-descriptor/registry.ts index a8063b341..5c4ba88fa 100644 --- a/src/core/command-descriptor/registry.ts +++ b/src/core/command-descriptor/registry.ts @@ -1,8 +1,8 @@ import type { CommandCapability } from '../capabilities.ts'; -import type { DaemonRequest } from '../../daemon/types.ts'; -// Type-only back-edge, erased at runtime (same pattern as derive.ts importing -// DaemonCommandDescriptor); no runtime import cycle. -import type { RefFrameEffect } from '../../daemon/daemon-command-registry.ts'; +// The typed-flags request from contracts/, not the daemon's server-side refinement: these +// descriptors read `command`, `positionals` and `flags` and never touch `internal`. +import type { CommandRequest } from '../../contracts/command-request.ts'; +import type { RefFrameEffect } from '../../contracts/ref-frame-effect.ts'; import { isReadOnlyFindAction, parseFindArgs } from '../../selectors/find.ts'; import { resolveWaitBudgetMs } from '../wait-positionals.ts'; import { @@ -84,10 +84,10 @@ const REQUEST_EXECUTION_EXEMPT = { const allowAnyDeviceSessionless = (): boolean => true; -const isRecordingStartRequest = (req: DaemonRequest): boolean => +const isRecordingStartRequest = (req: CommandRequest): boolean => (req.positionals?.[0] ?? '').toLowerCase() === 'start'; -const isShardedTestRequest = (req: DaemonRequest): boolean => +const isShardedTestRequest = (req: CommandRequest): boolean => req.command === 'test' && (typeof req.flags?.shardAll === 'number' || typeof req.flags?.shardSplit === 'number'); @@ -101,15 +101,15 @@ const isShardedTestRequest = (req: DaemonRequest): boolean => // enter/return dispatch a real return key. Anything other than a read is // classified may-invalidate (the honest superset for unknown subactions). const KEYBOARD_READ_ONLY_ACTIONS = new Set(['status', 'get']); -const keyboardRefFrameEffect = (req: DaemonRequest): RefFrameEffect => +const keyboardRefFrameEffect = (req: CommandRequest): RefFrameEffect => readOnlySubactionRefFrameEffect(req, KEYBOARD_READ_ONLY_ACTIONS, 'status'); // alert actions are get/wait/accept/dismiss: get/wait read, accept/dismiss act. const ALERT_READ_ONLY_ACTIONS = new Set(['get', 'wait']); -const alertRefFrameEffect = (req: DaemonRequest): RefFrameEffect => +const alertRefFrameEffect = (req: CommandRequest): RefFrameEffect => readOnlySubactionRefFrameEffect(req, ALERT_READ_ONLY_ACTIONS, 'get'); -type RecordingEffectRequest = Pick; +type RecordingEffectRequest = Pick; const keyboardRecordingEffect = (req: RecordingEffectRequest): RecordingEffect => readOnlySubactionRecordingEffect(req, KEYBOARD_READ_ONLY_ACTIONS, 'status'); @@ -129,7 +129,7 @@ const findRecordingEffect = (req: RecordingEffectRequest): RecordingEffect => { }; function readOnlySubactionRefFrameEffect( - req: DaemonRequest, + req: CommandRequest, readOnlyActions: ReadonlySet, defaultAction: string, ): RefFrameEffect { @@ -1453,7 +1453,7 @@ export function resolveTargetIdentityVerification( /** ADR 0016 request-sensitive app-state effect for one recorded request. */ export function resolveCommandRecordingEffect( - req: Pick, + req: Pick, ): RecordingEffect | undefined { const descriptor = COMMAND_DESCRIPTOR_BY_NAME.get(req.command); if (!descriptor?.recordsSessionAction) return undefined; diff --git a/src/core/command-descriptor/types.ts b/src/core/command-descriptor/types.ts index 797a62e0e..d0789d679 100644 --- a/src/core/command-descriptor/types.ts +++ b/src/core/command-descriptor/types.ts @@ -1,6 +1,8 @@ import type { CommandCapability } from '../capabilities.ts'; import type { DaemonCommandDescriptor } from '../../daemon/daemon-command-registry.ts'; -import type { DaemonRequest } from '../../daemon/types.ts'; +// The typed-flags request from contracts/, not the daemon's server-side refinement: these +// descriptors read `command`, `positionals` and `flags` and never touch `internal`. +import type { CommandRequest } from '../../contracts/command-request.ts'; import type { PostActionObservationSupport } from './post-action-observation.ts'; export type ResponseDataFieldTransform = { @@ -101,7 +103,7 @@ export type CommandDispatchFacet = { export type RecordingEffect = 'mutates-app' | 'observes-app'; export type CommandRecordingEffect = | RecordingEffect - | ((req: Pick) => RecordingEffect); + | ((req: Pick) => RecordingEffect); /** * ADR 0012 / #1349: when replay verifies a recorded `target-v1` annotation. diff --git a/src/core/dispatch-context.ts b/src/core/dispatch-context.ts index e42e8d599..5953fed9a 100644 --- a/src/core/dispatch-context.ts +++ b/src/core/dispatch-context.ts @@ -1,6 +1,8 @@ -import type { CliFlags, DaemonExcludedCliFlag } from '../contracts/cli-flags.ts'; +// CommandFlags and MaestroRuntimeFlags are declared in contracts/ so both sides of the process +// boundary can be stated in terms of them; re-exported here because this is where consumers +// already import them from. +export type { CommandFlags } from '../contracts/command-flags.ts'; import type { ScreenshotDispatchFlags } from '../contracts/screenshot.ts'; -import type { DaemonBatchStep } from './batch.ts'; import type { BackMode } from '../contracts/back-mode.ts'; import type { ClickButton } from '../contracts/click-button.ts'; import type { ElementSelectorKey } from '../contracts/interactor-types.ts'; @@ -9,36 +11,6 @@ import type { SessionSurface } from '../contracts/session-surface.ts'; import type { RunnerLogicalLeaseContext } from '../contracts/runner-lease-context.ts'; import type { Point } from '../kernel/snapshot.ts'; -export type MaestroRuntimeFlags = { - allowNonHittableCoordinateFallback?: boolean; - expectedTapPoint?: Point; - prewarmRunnerBeforeOpen?: boolean; - screenshotCaptureBackend?: 'runner'; -}; - -export type CommandFlags = Omit & { - batchSteps?: DaemonBatchStep[]; - clearAppState?: boolean; - interactionOutcome?: { - retryOnNoChange?: boolean; - }; - launchArgs?: string[]; - kind?: string; - maestro?: MaestroRuntimeFlags; - postGestureStabilization?: boolean; - snapshotIncludeHiddenContentHints?: boolean; - leaseProvider?: string; - provider?: string; - deviceKey?: string; - clientId?: string; - devicePort?: number; - hostPort?: number; - portReverseName?: string; - replayBackend?: string; - shardCount?: number; - shardIndex?: number; -}; - export type DispatchContext = ScreenshotDispatchFlags & { requestId?: string; appBundleId?: string; diff --git a/src/daemon/daemon-command-registry.ts b/src/daemon/daemon-command-registry.ts index 5657d562e..c644027e2 100644 --- a/src/daemon/daemon-command-registry.ts +++ b/src/daemon/daemon-command-registry.ts @@ -7,23 +7,10 @@ export type { DaemonCommandRoute } from './request-handler-chain.ts'; export type SessionCommandKind = 'inventory' | 'state' | 'observability' | 'publication' | 'replay'; -/** - * ADR 0014 session ref-frame lifetime. Declares how a daemon command relates to - * the session's authorized ref frame: - * - `preserve`: no successful path changes device-visible element identity, so - * the frame carries through untouched (snapshots, reads, inventory, ...); - * - `may-invalidate`: some successful path crosses a device side effect, so the - * leaf must expire the frame at its side-effect seam when that path runs; - * - `delegated`: an orchestrator (batch/replay/test) whose nested leaves own - * their own transitions — the outer command never expires a frame itself. - * - * This classification is an honesty/completeness guard, NOT the transition site: - * a `may-invalidate` command still calls the ref-frame module only when its - * mutating path is selected. The completeness gate - * (`__tests__/ref-frame-effect.test.ts`) fails if a daemon-projected command - * omits this classification. - */ -export type RefFrameEffect = 'preserve' | 'may-invalidate' | 'delegated'; +// Declared in contracts/ so core/ can classify commands without importing the daemon; +// re-exported here because the descriptor shape below is stated in terms of it. +export type { RefFrameEffect } from '../contracts/ref-frame-effect.ts'; +import type { RefFrameEffect } from '../contracts/ref-frame-effect.ts'; /** * Request-sensitive form of {@link RefFrameEffect}. Commands whose subactions diff --git a/src/daemon/types.ts b/src/daemon/types.ts index df12b84cd..075102f65 100644 --- a/src/daemon/types.ts +++ b/src/daemon/types.ts @@ -1,6 +1,5 @@ import type { DaemonArtifact as PublicDaemonArtifact, - DaemonRequest as PublicDaemonRequest, DaemonRequestMeta as PublicDaemonRequestMeta, DaemonResponse as PublicDaemonResponse, DaemonResponseData as PublicDaemonResponseData, @@ -8,7 +7,8 @@ import type { LeaseBackend, SessionRuntimeHints as PublicSessionRuntimeHints, } from '../kernel/contracts.ts'; -import type { CommandFlags } from '../core/dispatch.ts'; +import type { CommandRequest } from '../contracts/command-request.ts'; +import type { CommandFlags } from '../contracts/command-flags.ts'; import type { GestureReferenceFrame, ScrollDirection } from '../contracts/scroll-gesture.ts'; import type { LogBackend } from '../contracts/logs.ts'; import type { SessionSurface } from '../contracts/session-surface.ts'; @@ -116,10 +116,15 @@ type DaemonRequestInternal = { replayPlanStep?: boolean; }; -export type DaemonRequest = Omit & { +/** + * The server-side request: the shared `CommandRequest` (wire shape with typed flags) plus what + * only the daemon may see. `token` and `session` are required by the time a request is dispatched, + * and `internal` carries `SessionState` callbacks and the admitted lease — which is why this type + * stays in the daemon while `CommandRequest` sits in contracts for everyone else. + */ +export type DaemonRequest = Omit & { token: string; session: string; - flags?: CommandFlags; meta?: DaemonRequestMeta; internal?: DaemonRequestInternal; }; @@ -494,25 +499,7 @@ export type SessionState = { appLogFailure?: AppLogFailure; }; -export type SessionAction = { - ts: number; - command: string; - positionals: string[]; - runtime?: SessionRuntimeHints; - flags: Partial & { - snapshotInteractiveOnly?: boolean; - snapshotDepth?: number; - snapshotScope?: string; - snapshotRaw?: boolean; - launchArgs?: string[]; - saveScript?: boolean | string; - noRecord?: boolean; - }; - result?: Record; - /** - * ADR 0012 decision 3: parsed or record-time-computed `target-v1` - * evidence, written as a comment immediately before this action's line. - * Inert until migration step 4 adds enforcement. - */ - targetEvidence?: TargetAnnotationV1; -}; +// The recorded-action SHAPE lives in contracts/ so replay/ and compat/ can read a script +// without depending on the server; re-exported here for the daemon's own consumers. +export type { SessionAction } from '../contracts/session-action.ts'; +import type { SessionAction } from '../contracts/session-action.ts'; diff --git a/src/metro/client-metro.ts b/src/metro/client-metro.ts index ec8fa1492..b13f290e5 100644 --- a/src/metro/client-metro.ts +++ b/src/metro/client-metro.ts @@ -1,4 +1,11 @@ -import type { MetroPrepareKind } from '../contracts/metro.ts'; +// The result PAYLOADS are declared in contracts/metro.ts so the public API can name them +// without depending on this zone; re-exported here for existing consumers. +export type { PrepareMetroRuntimeResult, ReloadMetroResult } from '../contracts/metro.ts'; +import type { + MetroPrepareKind, + PrepareMetroRuntimeResult, + ReloadMetroResult, +} from '../contracts/metro.ts'; import fs from 'node:fs'; import path from 'node:path'; import { sleep } from '../utils/timeouts.ts'; @@ -94,22 +101,6 @@ export type PrepareMetroRuntimeOptions = { env?: EnvSource; }; -export type PrepareMetroRuntimeResult = { - projectRoot: string; - kind: ResolvedMetroKind; - dependenciesInstalled: boolean; - packageManager: string | null; - started: boolean; - reused: boolean; - pid: number; - logPath: string; - statusUrl: string; - runtimeFilePath: string | null; - iosRuntime: MetroRuntimeHints; - androidRuntime: MetroRuntimeHints; - bridge: MetroBridgeResult | null; -}; - export type ReloadMetroOptions = { metroHost?: string; metroPort?: number | string; @@ -124,13 +115,6 @@ export type ReloadMetroOptions = { * route (Expo). `status`/`body` always describe the HTTP probe; on the websocket path `reloadUrl` * is the ws(s) message-socket URL. */ -export type ReloadMetroResult = { - reloaded: true; - reloadUrl: string; - status: number; - body: string; - transport: 'http' | 'message-socket'; -}; type ProxyBridgeRequestOptions = { baseUrl: string; diff --git a/src/metro/metro-types.ts b/src/metro/metro-types.ts index 0c75e3642..4c0a51def 100644 --- a/src/metro/metro-types.ts +++ b/src/metro/metro-types.ts @@ -3,26 +3,9 @@ import type { SessionRuntimeHints } from '../kernel/contracts.ts'; /** Re-export of {@link SessionRuntimeHints} under the Metro-specific alias used by public API consumers. */ export type MetroRuntimeHints = SessionRuntimeHints; -export type MetroBridgeResult = { - enabled: boolean; - baseUrl: string; - statusUrl: string; - bundleUrl: string; - iosRuntime: MetroRuntimeHints; - androidRuntime: MetroRuntimeHints; - upstream: { - bundleUrl: string; - host: string; - port: number; - statusUrl: string; - }; - probe: { - reachable: boolean; - statusCode: number; - latencyMs: number; - detail: string; - }; -}; +// The bridge RESULT shape is declared in contracts/metro.ts, because the public prepare result +// embeds it and that shape had to move below both `metro/` and the command surface. +export type { MetroBridgeResult } from '../contracts/metro.ts'; export type MetroBridgeRuntimePayload = { metro_host?: string; diff --git a/src/replay/__tests__/plan-digest.test.ts b/src/replay/__tests__/plan-digest.test.ts index 160cae0e0..0379caf11 100644 --- a/src/replay/__tests__/plan-digest.test.ts +++ b/src/replay/__tests__/plan-digest.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; import { computeReplayPlanDigest } from '../plan-digest.ts'; -import type { SessionAction } from '../../daemon/types.ts'; +import type { SessionAction } from '../../contracts/session-action.ts'; function action(overrides: Partial = {}): SessionAction { return { diff --git a/src/replay/__tests__/script.test.ts b/src/replay/__tests__/script.test.ts index 58959cbe0..bbe45d44e 100644 --- a/src/replay/__tests__/script.test.ts +++ b/src/replay/__tests__/script.test.ts @@ -8,7 +8,7 @@ import { } from '../script.ts'; import { formatPortableActionLine, formatTargetAnnotationLines } from '../script-formatting.ts'; import type { TargetAnnotationV1 } from '../target-identity.ts'; -import type { SessionAction } from '../../daemon/types.ts'; +import type { SessionAction } from '../../contracts/session-action.ts'; // `writeReplayScript` (the `--update` heal-and-rewrite serializer) was // deleted in ADR 0012 migration step 6 (`--update` retirement left it with diff --git a/src/replay/open-script.ts b/src/replay/open-script.ts index ebbccdc7d..29c87417d 100644 --- a/src/replay/open-script.ts +++ b/src/replay/open-script.ts @@ -1,4 +1,4 @@ -import type { SessionAction } from '../daemon/types.ts'; +import type { SessionAction } from '../contracts/session-action.ts'; import { appendRuntimeHintFlags, formatScriptArg, diff --git a/src/replay/plan-digest.ts b/src/replay/plan-digest.ts index 0a86510fc..4adaa42e4 100644 --- a/src/replay/plan-digest.ts +++ b/src/replay/plan-digest.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto'; -import type { SessionAction } from '../daemon/types.ts'; +import type { SessionAction } from '../contracts/session-action.ts'; import { canonicalJson } from '../utils/canonical-json.ts'; /** diff --git a/src/replay/script-formatting.ts b/src/replay/script-formatting.ts index cdcb2575d..901924f9c 100644 --- a/src/replay/script-formatting.ts +++ b/src/replay/script-formatting.ts @@ -7,7 +7,7 @@ import { appendSnapshotActionScriptArgs, } from './script-utils.ts'; import { formatTargetAnnotationCommentLine } from './target-identity.ts'; -import type { SessionAction } from '../daemon/types.ts'; +import type { SessionAction } from '../contracts/session-action.ts'; export function formatPortableActionLine( action: SessionAction, diff --git a/src/replay/script-utils.ts b/src/replay/script-utils.ts index ff06791ea..f50123020 100644 --- a/src/replay/script-utils.ts +++ b/src/replay/script-utils.ts @@ -1,4 +1,4 @@ -import type { SessionAction } from '../daemon/types.ts'; +import type { SessionAction } from '../contracts/session-action.ts'; import { appendScreenshotScriptFlags } from '../contracts/screenshot.ts'; import { splitRefGenerationSuffix } from '../kernel/snapshot.ts'; diff --git a/src/replay/script.ts b/src/replay/script.ts index 2da57f48f..13ad3da92 100644 --- a/src/replay/script.ts +++ b/src/replay/script.ts @@ -5,7 +5,7 @@ import { readScreenshotScriptFlag } from '../contracts/screenshot.ts'; import type { DeviceTarget, PlatformSelector } from '../kernel/device.ts'; import { PLATFORM_SELECTORS } from '../kernel/device.ts'; import { parseReplayOpenFlags } from './open-script.ts'; -import type { SessionAction } from '../daemon/types.ts'; +import type { SessionAction } from '../contracts/session-action.ts'; import { isClickLikeCommand, parseReplaySeriesFlags, diff --git a/src/replay/target-identity.ts b/src/replay/target-identity.ts index 1fae5c40a..644244aed 100644 --- a/src/replay/target-identity.ts +++ b/src/replay/target-identity.ts @@ -19,22 +19,21 @@ export const TARGET_ANNOTATION_MAX_FIELD_BYTES = 256; export const TARGET_ANNOTATION_MAX_PAYLOAD_BYTES = 4096; export const TARGET_ANNOTATION_MAX_ANCESTRY = 8; -export type TargetAncestryEntry = { role: string; label?: string }; -export type TargetScrollRegion = { role: string; id?: string; label?: string }; -export type TargetRect = { x: number; y: number; width: number; height: number }; -export type TargetVerification = 'verified' | 'unverifiable'; - -export type TargetAnnotationV1 = { - id?: string; - role: string; - label?: string; - ancestry: TargetAncestryEntry[]; - sibling: number; - viewportOrder: number; - scrollRegion?: TargetScrollRegion; - rect?: TargetRect; - verification: TargetVerification; -}; +// The annotation SHAPE lives in contracts/ so the recorded-action type can be stated without +// depending on this zone; re-exported here for existing consumers. +export type { + TargetAncestryEntry, + TargetAnnotationV1, + TargetScrollRegion, + TargetVerification, +} from '../contracts/target-annotation.ts'; +import type { + TargetAncestryEntry, + TargetAnnotationV1, + TargetRect, + TargetScrollRegion, + TargetVerification, +} from '../contracts/target-annotation.ts'; // --------------------------------------------------------------------------- // Normalization (decision 3 "Normalization"): all strings NFC; `label` fields diff --git a/src/replay/vars.ts b/src/replay/vars.ts index 0ced7d685..07c8f7c4c 100644 --- a/src/replay/vars.ts +++ b/src/replay/vars.ts @@ -1,5 +1,5 @@ import { AppError } from '../kernel/errors.ts'; -import type { SessionAction } from '../daemon/types.ts'; +import type { SessionAction } from '../contracts/session-action.ts'; export type ReplayVarScope = { values: Readonly>; From 249ca4e0554370e935d11c3cb84e96c305050229 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 11:58:02 +0000 Subject: [PATCH 4/6] feat(layering): ratchet type-cycle growth (R9), and rule out a narrower client port R9: the largest strongly-connected component over value + type-only edges may not grow. R4 keeps the VALUE graph acyclic, so every cycle counted here is created by type-only imports - free at runtime, invisible to R5/R6, and the largest single obstacle to reading a subsystem in isolation: inside a component of 102 files, no file has a self-contained slice. Baseline set to 102, which is what THIS branch achieves - main carries 107 and the boundary moves here bring it to 102. An earlier revision baselined 87, measured against an older main; after rebasing onto f19864e the real figure was 102 and the new rule fired on its own stale baseline. Worth stating because the failure looked like a regression and was not: attribution showed main at 107 and this branch reducing it, which is the check working rather than complaining. Growth-only, deliberately unlike R6. Reducing 102 is a real refactor rather than a file move, so a hard equality would turn every unrelated improvement into a baseline edit. A shrunk tree is reported in the success line instead of failing. Verified at the new baseline by adding one type-only import that closes a loop and watching 102 become 108 and the gate reject it. The refactor itself is still not attempted. Hubs by in-component dependents are runtime-contract.ts, commands/runtime-types.ts, backend.ts, commands/runtime-common.ts; a pass starts there. Separately, investigated the narrower-port idea for the 4 remaining -> client inversions and it does not work. Measured first: files NAMING AgentDeviceClient (the inversions) 4 files CALLING client methods 26 distinct facade namespaces reached 13 The narrowness is an artifact of where the type is named, not of what is used. Making those four generic over the client type pushes the concrete type into the 26 implementations, turning 4 inversions into up to 26. A port spanning 13 namespaces is the whole facade, so it would either duplicate the public API shape - a second source of truth for it - or derive from the facade and carry the same dependency. So the four are the minimum number of naming sites rather than an accident: they are the choke point. Recorded as a position with the numbers behind it. The remaining option is the question underneath it - whether NAVIGATION_COMMAND_PROJECTIONS belongs in commands/ - and that is a design decision about the command surface, not a dependency cleanup. pnpm check green, 4535 unit tests. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur --- CONTEXT.md | 8 ++++ docs/dependency-graph-findings.md | 53 +++++++++++++++++++------ scripts/layering/check.ts | 66 +++++++++++++++++++++++++++++-- scripts/layering/model.ts | 64 ++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 15 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index f506895b4..84fdc2e4c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -298,6 +298,14 @@ The perfect-shape refactor is complete and merged. Its end-state: two modules until `activateRefFrame` took the transition, and `snapshotScopeSource` + `snapshotGeneration` were assigned in `snapshot-runtime.ts` until `setSnapshotLineage` took theirs. +- Type-cycle growth (R9). R4 keeps the VALUE import graph acyclic, so every remaining cycle is + created by type-only imports — free at runtime, invisible to R5/R6, and the largest single + obstacle to reading a subsystem in isolation: inside a strongly-connected component of 102 files, + no file has a self-contained slice. `TYPE_CYCLE_BASELINE` in `check.ts` ratchets it for **growth + only**, deliberately unlike R6: reducing it is a real refactor rather than a file move, so a hard + equality would turn every unrelated improvement into a baseline edit. A shrunk tree is reported in + the success line instead of failing. Hubs by in-component dependents: `runtime-contract.ts` (25), + `commands/runtime-types.ts` (21), `backend.ts` (15), `commands/runtime-common.ts` (12). - Zero-dep CI jobs (R8). Some jobs run scripts straight from a checkout with `install-deps: false`, so they have no `node_modules`. Nothing local can feel that constraint — every dev machine has `node_modules` sitting right there — so a script grows a package import, passes locally, and fails diff --git a/docs/dependency-graph-findings.md b/docs/dependency-graph-findings.md index f77627485..2181e9eb4 100644 --- a/docs/dependency-graph-findings.md +++ b/docs/dependency-graph-findings.md @@ -1,13 +1,16 @@ # Dependency graph findings -Snapshot analysis of the production import graph — 901 files, 4768 edges, 25 zones — taken while -doing the boundary work across #1405 and its follow-up. It is a dated observation, not a normative +Snapshot analysis of the production import graph — 918 files, 25 zones — taken while doing the +boundary work across #1405 and its follow-ups. It is a dated observation, not a normative document; when it disagrees with `scripts/layering/`, the gate wins. -There is no graph tool to install. A rendered viewer was built and then dropped: it cost ~2200 -lines and a Fallow exemption, and nobody — human or agent — drew a conclusion from the picture. -Every finding below came from short queries against the gate's own model, which is the point worth -keeping. Re-derive any of them like this: +Two ways to reproduce any number below. `pnpm depgraph` (#1410) emits the whole graph as JSON plus a +summary, and is the tool to reach for when you want the reachability or cycle analysis it computes. A +*rendered* viewer was also built and dropped — ~2200 lines and a Fallow exemption for a picture +neither a human nor an agent drew a conclusion from; the analysis survived, the rendering did not. + +For a one-off question, a throwaway probe against the gate's own model is faster and leaves nothing +to clean up: ```ts // scripts/layering/.probe.ts (throwaway; the gate's model is the only dependency) @@ -115,19 +118,47 @@ Cycle size by edge kind, measured over the whole production graph: | edges considered | largest strongly-connected component | |---|---| | value only | **1** — no cycles, which is what R4 enforces | -| value + type-only | **87 files** | +| value + type-only | **102 files** | | value + dynamic | **1** | | all kinds | 213 files | -At runtime the module graph is a clean DAG. The 87-file cluster is purely type-level: you cannot read -the types of any one of those files without transitively reaching all 87. That is not a correctness +At runtime the module graph is a clean DAG. The 102-file cluster is purely type-level: you cannot +read the types of any one of those files without transitively reaching all 102. (`main` carries 107; +the boundary moves in this branch bring it to 102.) That is not a correctness problem — types are erased — but it is a comprehension one, and it is the single largest obstacle to reading a subsystem in isolation. It spans `commands` (33), `daemon` (21), `platforms` (13), `core` (12), hubs at `runtime-contract.ts` (25 in-cluster dependents), `commands/runtime-types.ts` (21), `backend.ts` (15), `commands/runtime-common.ts` (12). -Deliberately not attempted yet: it is a different and much larger change than moving declarations -down, and R6 does not measure it. Worth its own pass, starting at those four hubs. +Now ratcheted for growth by **R9** (`TYPE_CYCLE_BASELINE` in `check.ts`), so it cannot get worse +while nobody is looking — a type-only import that closes a new loop fails the gate, verified by +adding one type-only import that closes a loop and watching the gate reject it. Growth-only on +purpose: reducing it is a real refactor, so a +hard equality would turn every unrelated improvement into a baseline edit. The refactor itself is +still deliberately not attempted; it starts at those four hubs. + +### The facade cycle: investigated, no narrower port exists + +The 4 remaining `-> client` inversions are `AgentDeviceClient` used as an opaque handle. The obvious +fix is a narrower port in `contracts/` describing only what `commands/` needs, with `client/` +satisfying it. Measured before attempting it: + +| | count | +|---|---| +| Files *naming* `AgentDeviceClient` (i.e. the inversions) | 4 | +| Files *calling* client methods | 26 | +| Distinct facade namespaces reached | 13 | + +The narrowness is an artifact of where the type is *named*, not of what is *used*. Making the four +generic over the client type pushes the concrete type down into the 26 implementations, turning 4 +inversions into up to 26. A port covering 13 namespaces is the whole facade, so it would either +duplicate the public API shape — a second source of truth for it — or derive from the facade and +carry the same dependency. + +Those four files are therefore the minimum number of naming sites, not an accident: they are the +choke point. Accepted as a position, argued at `TYPE_INVERSION_BASELINE`. The remaining option is +the one that was always the real question — whether `NAVIGATION_COMMAND_PROJECTIONS` belongs in +`commands/` — and that is a design decision about the command surface, not a dependency cleanup. ## 1. The two remaining type-inversion clusters diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index 4555cb547..77ef866c4 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -18,6 +18,9 @@ // - Over the ZERO-DEP CI JOBS only: their scripts may import nothing that needs // installing (R8), a constraint no local run can feel because `node_modules` is // always there locally and never there in those jobs. +// - Over the TYPE GRAPH: the largest type-level import cycle may not grow (R9). R4 +// keeps the value graph acyclic, so these cycles are free at runtime but bound +// what can be read in isolation. Growth-only, deliberately loose. // Only `(root)` is unranked (see `UNRANKED_ZONES` in model.ts): it holds the // entrypoints and the composition roots that wire the command surface into the // daemon, which R2 forbids the daemon from importing, so they sit outside the @@ -38,6 +41,7 @@ import { uninstallableImports, zeroDepJobs } from './zero-dep-jobs.ts'; import { backEdgePair, findValueImportCycles, + largestTypeCycleSize, resolveImportEdges, topFolder, typeInversionPair, @@ -283,6 +287,45 @@ function checkTypeInversions(edges: readonly ResolvedImportEdge[]): Violation[] return violations; } +// R9: the largest type-level import cycle, ratcheted for GROWTH ONLY. +// +// R4 keeps the value graph acyclic, so every cycle counted here is created by type-only imports. +// That is free at runtime and invisible to R5/R6, but it bounds what can be read in isolation: +// inside a strongly-connected component of 102 files, no file has a self-contained slice — reading +// any one of them means reaching every other one's declarations. It is the largest single obstacle +// to understanding a subsystem on its own, and nothing else in this gate measures it. +// +// Deliberately loose. Unlike R6 this does NOT fail when the number drops, because the number is +// large and reducing it is a real refactor rather than a file move — a hard equality would turn +// every unrelated improvement into a baseline edit. It fails only on growth, and reports the slack +// when the tree has improved so the baseline can be lowered when someone is in here anyway. +// +// Hubs at the time of writing, by in-component dependents: runtime-contract.ts (25), +// commands/runtime-types.ts (21), backend.ts (15), commands/runtime-common.ts (12). A pass at this +// starts there. See docs/dependency-graph-findings.md. +// Set to what THIS branch achieves, not to what main carries: main is at 107, and the boundary +// moves here bring it to 102. Measured on the rebased tree — an earlier 87 was taken against an +// older main and was stale rather than violated, which is exactly the kind of drift a ratchet +// measured at merge time prevents. +const TYPE_CYCLE_BASELINE = 102; + +function checkTypeCycleGrowth(edges: readonly ResolvedImportEdge[]): Violation[] { + const actual = largestTypeCycleSize(edges); + if (actual <= TYPE_CYCLE_BASELINE) return []; + return [ + { + rule: 'R9 type-cycle-growth', + file: 'scripts/layering/check.ts', + line: 1, + message: + `the largest type-level import cycle grew to ${actual} files (baseline ` + + `${TYPE_CYCLE_BASELINE}). A type-only import that closes a loop makes every file in the ` + + `loop unreadable in isolation. Declare the shared type below both modules, or if the growth ` + + `is genuinely warranted, raise TYPE_CYCLE_BASELINE in the same commit and say why.`, + }, + ]; +} + function checkSessionStateOwnership(sources: ReadonlyMap): Violation[] { const types = sources.get('src/daemon/types.ts'); if (!types) { @@ -437,7 +480,21 @@ function sessionStateFieldCount(): number { ); } -function report(files: readonly string[], violations: readonly Violation[]): number { +/** R9 is growth-only, so a shrunk tree is reported rather than failed — see checkTypeCycleGrowth. */ +function typeCycleNote(edges: readonly ResolvedImportEdge[]): string { + const actual = largestTypeCycleSize(edges); + if (actual >= TYPE_CYCLE_BASELINE) return `the largest type-level cycle is ${actual} files (R9)`; + return ( + `the largest type-level cycle is down to ${actual} files, under the R9 baseline of ` + + `${TYPE_CYCLE_BASELINE} — lower TYPE_CYCLE_BASELINE when convenient` + ); +} + +function report( + files: readonly string[], + violations: readonly Violation[], + edges: readonly ResolvedImportEdge[], +): number { if (violations.length === 0) { process.stdout.write( `Layering guard: OK — ${files.length} source files satisfy R1-R3 and contain no ` + @@ -445,8 +502,8 @@ function report(files: readonly string[], violations: readonly Violation[]): num `back-edges (only the composition root is unranked), and its type-only ` + `inversions match the R6 ratchet (${Object.values(TYPE_INVERSION_BASELINE).reduce((sum, count) => sum + count, 0)} remaining); ` + `all ${sessionStateFieldCount()} SessionState fields are classified and every write is ` + - `inside its declared owner (R7); and every zero-dep CI job resolves without ` + - `node_modules (R8).\n`, + `inside its declared owner (R7); every zero-dep CI job resolves without ` + + `node_modules (R8); and ${typeCycleNote(edges)}.\n`, ); return 0; } @@ -482,9 +539,10 @@ export function main(): number { ...checkBackEdges(edges), ...checkTypeInversions(edges), ...checkSessionStateOwnership(sources), + ...checkTypeCycleGrowth(edges), ...checkZeroDepJobs(), ]; - return report(sourceFiles, violations); + return report(sourceFiles, violations, edges); } if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { diff --git a/scripts/layering/model.ts b/scripts/layering/model.ts index da5f380b5..d9821cfc9 100644 --- a/scripts/layering/model.ts +++ b/scripts/layering/model.ts @@ -334,3 +334,67 @@ export function collectBackEdges(edges: readonly ResolvedImportEdge[]): BackEdge .map(([pair, identities]) => [pair, [...identities].sort()]), ); } + +/** + * Size of the largest strongly-connected component over value AND type-only edges. + * + * R4 keeps the VALUE graph acyclic, so any cycle here is created by type-only imports. That costs + * nothing at runtime — types are erased — but it bounds what can be read in isolation: every file + * in the component transitively references every other one's declarations, so none of them has a + * self-contained slice. Dynamic edges are excluded deliberately: a dynamic import is a lazy seam, + * and a loop through one is not a comprehension barrier in the same way. + * + * Returned as a single number because that is all R9 ratchets. `largestTypeCycleMembers` gives the + * files when you need to act on it. + */ +export function largestTypeCycleSize(edges: readonly ResolvedImportEdge[]): number { + return largestTypeCycleMembers(edges).length; +} + +/** Members of the largest value+type strongly-connected component, sorted. */ +export function largestTypeCycleMembers(edges: readonly ResolvedImportEdge[]): string[] { + const successors = new Map(); + for (const edge of edges) { + if (edge.dynamic) continue; + const list = successors.get(edge.file) ?? []; + list.push(edge.target); + successors.set(edge.file, list); + } + + const index = new Map(); + const lowLink = new Map(); + const stack: string[] = []; + const onStack = new Set(); + let next = 0; + let biggest: string[] = []; + + function visit(file: string): void { + index.set(file, next); + lowLink.set(file, next); + next++; + stack.push(file); + onStack.add(file); + + for (const target of successors.get(file) ?? []) { + if (!index.has(target)) { + visit(target); + lowLink.set(file, Math.min(lowLink.get(file)!, lowLink.get(target)!)); + } else if (onStack.has(target)) { + lowLink.set(file, Math.min(lowLink.get(file)!, index.get(target)!)); + } + } + + if (lowLink.get(file) !== index.get(file)) return; + const component: string[] = []; + let member: string; + do { + member = stack.pop()!; + onStack.delete(member); + component.push(member); + } while (member !== file); + if (component.length > biggest.length) biggest = component; + } + + for (const file of successors.keys()) if (!index.has(file)) visit(file); + return biggest.sort(); +} From fffd3cd28e986430ecdf22adba0de131d722764d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 12:56:14 +0000 Subject: [PATCH 5/6] fix(layering): test R9, specify its floor, and drop two duplications Adversarial self-review of #1435 found four things worth fixing. R9 shipped with no unit test. Every other rule in this gate has one (R5 back-edges, R6 inversions, R7 session state, R8 zero-dep closures); R9's only verification was a manual injection CI cannot repeat. Added tests for the three distinctions it depends on: a type-only loop counts, a dynamic-only loop does not, a value loop still does. Writing that test immediately found an undocumented edge case, which is the argument for it. largestTypeCycleSize returns 1 for an acyclic graph that has non-dynamic edges but 0 when every edge is dynamic, because only edge-participating files enter the walk. Immaterial to a growth ratchet, but an inconsistent floor nobody had written down. Now specified in the doc comment and pinned by the test, so 0 and 1 cannot later be read as a meaningful difference. largestTypeCycleMembers was exported with no consumer - speculative API, and scripts/layering is in Fallow's ignorePatterns so nothing would have flagged it. Same pattern review caught on the previous head with MaestroRuntimeFlags and TargetRect. Made module-private. ResolvedMetroKind was declared twice after the Metro payload move: exported from contracts/metro.ts and still private in metro/client-metro.ts. client-metro.ts now imports it. The gate computed the SCC twice per run, once in the rule and once for the success line. Computed once and threaded, so the two can no longer disagree. Also re-verified the claim this PR rests on, with a stronger check than the one in the body: comparing DECLARATION names in index.d.ts counts inlined internals, and by that measure this branch appears to lose five names (PrepareMetroRuntimeResult, ReloadMetroResult, ResolvedMetroKind, SCROLL_INPUT_DIRECTIONS, ScrollInputDirection). All five are declared-but-not-exported helpers. The real surface - exported names across all eleven published entrypoints - is 69 on both sides, identical. Also proved DaemonRequest structurally equal to its pre-split shape with a type-level assertion rather than by reasoning, and confirmed SessionAction, CommandFlags and TargetAnnotationV1 moved byte-identically. pnpm check green, 4535 unit tests, 24 layering tests. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur --- scripts/layering/check.ts | 16 ++++++------ scripts/layering/model.test.ts | 46 ++++++++++++++++++++++++++++++++++ scripts/layering/model.ts | 11 +++++--- src/metro/client-metro.ts | 2 +- 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index 77ef866c4..57cba6eea 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -309,8 +309,7 @@ function checkTypeInversions(edges: readonly ResolvedImportEdge[]): Violation[] // measured at merge time prevents. const TYPE_CYCLE_BASELINE = 102; -function checkTypeCycleGrowth(edges: readonly ResolvedImportEdge[]): Violation[] { - const actual = largestTypeCycleSize(edges); +function checkTypeCycleGrowth(actual: number): Violation[] { if (actual <= TYPE_CYCLE_BASELINE) return []; return [ { @@ -481,8 +480,7 @@ function sessionStateFieldCount(): number { } /** R9 is growth-only, so a shrunk tree is reported rather than failed — see checkTypeCycleGrowth. */ -function typeCycleNote(edges: readonly ResolvedImportEdge[]): string { - const actual = largestTypeCycleSize(edges); +function typeCycleNote(actual: number): string { if (actual >= TYPE_CYCLE_BASELINE) return `the largest type-level cycle is ${actual} files (R9)`; return ( `the largest type-level cycle is down to ${actual} files, under the R9 baseline of ` + @@ -493,7 +491,7 @@ function typeCycleNote(edges: readonly ResolvedImportEdge[]): string { function report( files: readonly string[], violations: readonly Violation[], - edges: readonly ResolvedImportEdge[], + typeCycle: number, ): number { if (violations.length === 0) { process.stdout.write( @@ -503,7 +501,7 @@ function report( `inversions match the R6 ratchet (${Object.values(TYPE_INVERSION_BASELINE).reduce((sum, count) => sum + count, 0)} remaining); ` + `all ${sessionStateFieldCount()} SessionState fields are classified and every write is ` + `inside its declared owner (R7); every zero-dep CI job resolves without ` + - `node_modules (R8); and ${typeCycleNote(edges)}.\n`, + `node_modules (R8); and ${typeCycleNote(typeCycle)}.\n`, ); return 0; } @@ -533,16 +531,18 @@ export function main(): number { const sourceFiles = listSourceFiles(); const sources = readSources(sourceFiles); const edges = resolveImportEdges(sources); + // Computed once and threaded: the rule and the success line must report the same number. + const typeCycle = largestTypeCycleSize(edges); const violations = [ ...checkLayeringRules(edges), ...checkCycles(edges), ...checkBackEdges(edges), ...checkTypeInversions(edges), ...checkSessionStateOwnership(sources), - ...checkTypeCycleGrowth(edges), + ...checkTypeCycleGrowth(typeCycle), ...checkZeroDepJobs(), ]; - return report(sourceFiles, violations, edges); + return report(sourceFiles, violations, typeCycle); } if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { diff --git a/scripts/layering/model.test.ts b/scripts/layering/model.test.ts index 1962bb6c1..84508d231 100644 --- a/scripts/layering/model.test.ts +++ b/scripts/layering/model.test.ts @@ -12,6 +12,7 @@ import { } from './session-state.ts'; import { uninstallableImports, zeroDepJobs } from './zero-dep-jobs.ts'; import { + largestTypeCycleSize, RANKED_ZONES, typeInversionPair, UNRANKED_ZONES, @@ -467,3 +468,48 @@ test('classification drift is reported in all three directions', () => { ); assert.deepEqual(inBoth, [], 'the real tables must not overlap'); }); + +// R9 shipped its first revision with no test — every other rule here has one, and the only +// verification was a manual injection CI cannot repeat. These pin the three distinctions the rule +// depends on: which edge kinds count, and that an acyclic graph reports 1 rather than 0. +test('largestTypeCycleSize counts type-only cycles and ignores dynamic ones', () => { + // Acyclic: every component is a single file, so the largest is 1 (not 0). + const acyclic = resolveImportEdges( + new Map(Object.entries({ + 'src/core/a.ts': "import type { B } from '../contracts/b.ts';", + 'src/contracts/b.ts': 'export type B = 1;', + })), + ); + assert.equal(largestTypeCycleSize(acyclic), 1); + + // A three-file loop closed by type-only imports is exactly what R4 permits and R9 measures. + const typeCycle = resolveImportEdges( + new Map(Object.entries({ + 'src/core/a.ts': "import type { B } from './b.ts';", + 'src/core/b.ts': "import type { C } from './c.ts';\nexport type B = 1;", + 'src/core/c.ts': "import type { A } from './a.ts';\nexport type C = 1;", + })), + ); + assert.equal(largestTypeCycleSize(typeCycle), 3); + + // A loop closed through a DYNAMIC import is excluded on purpose: a lazy seam is not a + // comprehension barrier, and R3 relies on dynamic imports existing. With no non-dynamic edge at + // all no file enters the walk, so the floor here is 0 rather than 1 — specified, not incidental. + const dynamicCycle = resolveImportEdges( + new Map(Object.entries({ + 'src/core/a.ts': "void import('./b.ts');", + 'src/core/b.ts': "void import('./a.ts');", + })), + ); + assert.equal(largestTypeCycleSize(dynamicCycle), 0); + + // A value cycle counts too — R4 rejects it separately, so R9 must not be the thing that + // notices, but it must not under-report either. + const valueCycle = resolveImportEdges( + new Map(Object.entries({ + 'src/core/a.ts': "export { b } from './b.ts';", + 'src/core/b.ts': "export { a } from './a.ts';", + })), + ); + assert.equal(largestTypeCycleSize(valueCycle), 2); +}); diff --git a/scripts/layering/model.ts b/scripts/layering/model.ts index d9821cfc9..c76096855 100644 --- a/scripts/layering/model.ts +++ b/scripts/layering/model.ts @@ -344,15 +344,20 @@ export function collectBackEdges(edges: readonly ResolvedImportEdge[]): BackEdge * self-contained slice. Dynamic edges are excluded deliberately: a dynamic import is a lazy seam, * and a loop through one is not a comprehension barrier in the same way. * - * Returned as a single number because that is all R9 ratchets. `largestTypeCycleMembers` gives the - * files when you need to act on it. + * Returned as a single number because that is all R9 ratchets. + * + * Floor semantics, which are specified rather than incidental: only files that participate in at + * least one non-dynamic edge are considered, so an acyclic graph reports 1 (every such file is its + * own trivial component) and a graph whose only edges are dynamic reports 0 (no file enters the + * walk). Both are immaterial to a growth ratchet, but they are pinned in model.test.ts so nobody + * later reads 0 and 1 as a meaningful difference. */ export function largestTypeCycleSize(edges: readonly ResolvedImportEdge[]): number { return largestTypeCycleMembers(edges).length; } /** Members of the largest value+type strongly-connected component, sorted. */ -export function largestTypeCycleMembers(edges: readonly ResolvedImportEdge[]): string[] { +function largestTypeCycleMembers(edges: readonly ResolvedImportEdge[]): string[] { const successors = new Map(); for (const edge of edges) { if (edge.dynamic) continue; diff --git a/src/metro/client-metro.ts b/src/metro/client-metro.ts index b13f290e5..a68f6ac75 100644 --- a/src/metro/client-metro.ts +++ b/src/metro/client-metro.ts @@ -5,6 +5,7 @@ import type { MetroPrepareKind, PrepareMetroRuntimeResult, ReloadMetroResult, + ResolvedMetroKind, } from '../contracts/metro.ts'; import fs from 'node:fs'; import path from 'node:path'; @@ -37,7 +38,6 @@ const DEV_SERVER_STATUS_READY_TEXT = 'packager-status:running'; const METRO_TERM_TIMEOUT_MS = 1_000; const METRO_KILL_TIMEOUT_MS = 1_000; -type ResolvedMetroKind = Exclude; type EnvSource = NodeJS.ProcessEnv | Record; type RepackBundlerKind = 'rspack' | 'webpack'; From ee47b598de882eb8781370bd2fe0d2ca5c89ad21 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 13:50:52 +0000 Subject: [PATCH 6/6] refactor(contracts): one file per command family, one name per request Addresses review on #1435. contracts/client-api.ts was 1,064 LOC and grouped session, app, interaction, replay, observability and recording contracts together, so it answered no one question and crossed the >1,000-LOC architecture-debt tripwire in AGENTS.md:124. Split it into 14 domain-family files by the command families that already exist (client-connection, client-device-view, client-session, client-lease, client-app, client-capture, client-target, client-gesture, client-selector-read, client-replay, client-observability, client-settings, client-system, client-request); the four Metro client shapes went into the existing contracts/metro.ts so one file answers the Metro question. Largest resulting file is 137 LOC. client/client-types.ts re-exports one wildcard per family, so the published import path is unchanged. Published surface verified unchanged against main two ways: the exported-name set of all 11 published entrypoints is identical (70 names), and every declaration in the built index.d.ts is byte-identical after normalization -- 0 names added, 0 shapes changed. index.d.ts got smaller (1,726 -> 1,682 lines): 10 declarations main duplicated into it now resolve through a shared chunk. Also, from re-examining the two findings the review flagged as blind spots: - CommandRequest was a third name for "a request" that no consumer needed. Every core/command-descriptor/ use read only command/positionals/flags, in two spellings (the full type and a Pick of it). Replaced by contracts/dispatched-command.ts DispatchedCommand -- those three fields and nothing else, with command/positionals Picked from the wire type so they cannot drift. daemon/types.ts DaemonRequest now extends the wire shape directly. Two request shapes again, at two ranks. - The 7 remaining R6 inversions each get a mechanical reason rather than an appeal to an ADR: the 4 AgentDeviceClient edges are a real zone-level cycle (client-types.ts imports ProjectedNavigationCommandClient from commands/), and no narrower port exists (26 call sites across 13 namespaces); the 2 DaemonCommandDescriptor edges are unavoidable because that shape is stated in terms of the server-private DaemonRequest; the 1 DaemonCommandRoute edge is unavoidable because the type is computed from the daemon's handler table. Cleanups found on the way: three doc comments this branch had orphaned from their declarations (SettleCommandOptions, RecordControlOptions, ReloadMetroResult -- the last had drifted onto an unrelated type it misdescribed) are reattached; intra-contracts imports normalized from '../contracts/x.ts' to './x.ts', which is what the duplicate-import lint caught; and stale references to the deleted file removed from the docs. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur --- docs/dependency-graph-findings.md | 72 +- scripts/layering/check.ts | 24 +- src/__tests__/cli-client-commands.test.ts | 2 +- src/agent-device-client.ts | 30 +- src/cli.ts | 2 +- src/cli/batch-steps.ts | 2 +- src/cli/commands/connection-runtime.ts | 2 +- src/client/client-normalizers.ts | 12 +- src/client/client-types.ts | 163 +-- src/commands/__tests__/command-flags.test.ts | 2 +- src/commands/batch/index.ts | 2 +- src/commands/capture/alert.ts | 2 +- src/commands/capture/output.test.ts | 2 +- src/commands/capture/output.ts | 2 +- src/commands/capture/screenshot.ts | 2 +- src/commands/capture/settings.ts | 2 +- src/commands/capture/wait.ts | 2 +- src/commands/cli-grammar/common.ts | 7 +- src/commands/cli-grammar/types.ts | 2 +- src/commands/command-flags.ts | 2 +- src/commands/command-input.ts | 5 +- src/commands/interaction/index.ts | 6 +- src/commands/interaction/interactions.ts | 5 +- src/commands/interaction/output.ts | 2 +- src/commands/interaction/selectors.ts | 2 +- src/commands/management/app.ts | 2 +- src/commands/management/output.test.ts | 2 +- src/commands/management/output.ts | 14 +- src/commands/management/push.ts | 2 +- src/commands/management/viewport.ts | 2 +- src/commands/metro/index.ts | 2 +- src/commands/observability/index.ts | 2 +- src/commands/perf/index.ts | 2 +- src/commands/perf/output.ts | 2 +- src/commands/recording/index.ts | 2 +- src/commands/recording/output.ts | 2 +- src/commands/system/index.ts | 2 +- src/contracts/batch-step.ts | 2 +- src/contracts/client-api.ts | 1064 ----------------- src/contracts/client-app.ts | 137 +++ src/contracts/client-capture.ts | 82 ++ src/contracts/client-connection.ts | 100 ++ src/contracts/client-device-view.ts | 79 ++ src/contracts/client-gesture.ts | 135 +++ src/contracts/client-lease.ts | 51 + src/contracts/client-observability.ts | 57 + src/contracts/client-replay.ts | 75 ++ src/contracts/client-request.ts | 110 ++ src/contracts/client-selector-read.ts | 59 + src/contracts/client-session.ts | 31 + src/contracts/client-settings.ts | 60 + src/contracts/client-system.ts | 93 ++ src/contracts/client-target.ts | 29 + src/contracts/command-request.ts | 24 - src/contracts/dispatched-command.ts | 23 + src/contracts/interactor-types.ts | 12 +- src/contracts/metro.ts | 41 + src/contracts/result-serialization.ts | 16 +- src/core/command-descriptor/registry.ts | 26 +- src/core/command-descriptor/types.ts | 4 +- .../__tests__/snapshot-handler.test.ts | 2 +- src/daemon/types.ts | 14 +- .../__tests__/command-tools-parity.test.ts | 2 +- src/mcp/command-tools.ts | 2 +- src/metro/client-metro.ts | 7 - .../integration/provider-scenarios/harness.ts | 2 +- test/output-economy/fixtures.ts | 6 +- test/output-economy/routine-workflow.ts | 2 +- 68 files changed, 1436 insertions(+), 1303 deletions(-) delete mode 100644 src/contracts/client-api.ts create mode 100644 src/contracts/client-app.ts create mode 100644 src/contracts/client-capture.ts create mode 100644 src/contracts/client-connection.ts create mode 100644 src/contracts/client-device-view.ts create mode 100644 src/contracts/client-gesture.ts create mode 100644 src/contracts/client-lease.ts create mode 100644 src/contracts/client-observability.ts create mode 100644 src/contracts/client-replay.ts create mode 100644 src/contracts/client-request.ts create mode 100644 src/contracts/client-selector-read.ts create mode 100644 src/contracts/client-session.ts create mode 100644 src/contracts/client-settings.ts create mode 100644 src/contracts/client-system.ts create mode 100644 src/contracts/client-target.ts delete mode 100644 src/contracts/command-request.ts create mode 100644 src/contracts/dispatched-command.ts diff --git a/docs/dependency-graph-findings.md b/docs/dependency-graph-findings.md index 2181e9eb4..07896b743 100644 --- a/docs/dependency-graph-findings.md +++ b/docs/dependency-graph-findings.md @@ -83,7 +83,7 @@ type-only inversions, R7 pins SessionState field ownership, and the shared selec | Keystone moved to `contracts/` | Unblocked | |---|---| | `DaemonBatchStep` (was `core/batch.ts`, typed via `DaemonRequest['runtime']`) | `CommandFlags` | -| `CommandFlags` (was `core/dispatch-context.ts`) | `SessionAction`, `CommandRequest` | +| `CommandFlags` (was `core/dispatch-context.ts`) | `SessionAction`, `DispatchedCommand` | | `TargetAnnotationV1` shape (was `replay/target-identity.ts`) | `SessionAction` | | `ScrollInputDirection`, Metro result payloads | `ScrollOptions`, `MetroPrepareResult`/`MetroReloadResult` | @@ -93,23 +93,46 @@ was written `DaemonRequest['runtime']`, pulling the whole daemon request type in declaration pinning ~80 public API shapes above it. Neither looked like a keystone from the graph; both were found by asking "what does the target itself import, and what rank is that?" -`DaemonRequest` also split into the three shapes it had been conflating: - -- `kernel/contracts.ts` — the **wire** shape, `flags?: Record`, because a process - boundary cannot enforce a flag vocabulary; -- `contracts/command-request.ts` `CommandRequest` — the wire shape with `flags` typed. What a command - surface needs to reason about a request without knowing which server runs it; -- `daemon/types.ts` `DaemonRequest` — that plus `internal?: DaemonRequestInternal`, carrying - `SessionState` callbacks and the admitted lease. Server-private, and why it cannot move down. - -`core/command-descriptor/` had been importing the third to read `command`, `positionals` and `flags`. - -**The remaining 7 are positions, not debt.** 4 are `AgentDeviceClient` used as an opaque handle; -the facade is built from `commands/`'s own `NAVIGATION_COMMAND_PROJECTIONS`, so this is a genuine -zone-level cycle and breaking it means deciding where that registry belongs. 3 are the ADR 0003 -daemon descriptor, whose `DaemonCommandRoute` is `keyof typeof DAEMON_ROUTE_HANDLERS` — derived from -what the server implements, so moving it down would mean re-declaring route names plus a gate to -prove the handler map still covers them. Both are argued at `TYPE_INVERSION_BASELINE`. +`DaemonRequest` had been conflating a request with the command that request dispatches. There are +still only two request shapes — and that is the point, because a third would be a third name for the +same thing: + +- `kernel/contracts.ts` `DaemonRequest` — the **wire** shape, `flags?: Record`, + because a process boundary cannot enforce a flag vocabulary; +- `daemon/types.ts` `DaemonRequest` — the wire shape with `token`/`session` required, `flags` + narrowed to `CommandFlags`, and `internal?: DaemonRequestInternal` carrying `SessionState` + callbacks and the admitted lease. Server-private, and why it cannot move down. + +`core/command-descriptor/` had been importing the second to read `command`, `positionals` and +`flags` — reaching up two ranks for three fields. It now takes +`contracts/dispatched-command.ts` `DispatchedCommand`, which is those three fields and nothing else, +with `command`/`positionals` `Pick`ed from the wire so they cannot drift from it. Every descriptor +resolver already read only those three, in two spellings (the full type and a `Pick` of it); one +narrow name replaced both. + +**The remaining 7 are positions, not debt** — each for a mechanical reason, not an appeal to an ADR: + +- **4 × `AgentDeviceClient`** (`commands/command-contract.ts`, `commands/command-surface.ts`, + `commands/family/types.ts`, `mcp/command-tools.ts`). The facade cannot move below `commands/` + because it is *built from* the command surface: `client/client-types.ts` imports + `ProjectedNavigationCommandClient` from `commands/system/navigation-projection.ts`. That is a real + zone-level type cycle, and breaking it means deciding where the projection registry belongs — a + design call, not a file move. A narrower port does not exist either: 4 files *name* the facade, + but 26 call sites use methods across 13 of its namespaces, so any port would re-declare it. +- **2 × `DaemonCommandDescriptor`** (`core/command-descriptor/derive.ts`, `.../types.ts`). It is + *stated in terms of* the server-private `daemon/types.ts` `DaemonRequest` — + `refFrameEffect?: (req: DaemonRequest) => RefFrameEffect`, + `allowSessionlessDefaultDevice?: (req: DaemonRequest) => boolean` — so it cannot be declared below + the daemon. Having `core/` re-declare a parallel 13-field shape instead would trade one erased + edge for a second source of truth. +- **1 × `DaemonCommandRoute`** (`commands/command-explain.ts`). It is + `keyof typeof DAEMON_ROUTE_HANDLERS` — *computed from* the daemon's handler table, so it cannot + exist below that table. `command-explain.ts` uses it to key an exhaustive + `Record` of owner files; a hand-written union in `contracts/` would + drop exactly that exhaustiveness. + +All three are argued at `TYPE_INVERSION_BASELINE` in `scripts/layering/check.ts`, next to the +numbers they explain. ## 0b. The biggest structural finding is not an inversion @@ -164,10 +187,15 @@ the one that was always the real question — whether `NAVIGATION_COMMAND_PROJEC `TYPE_INVERSION_BASELINE` in `scripts/layering/check.ts` holds both, with the reasoning inline. -**28 + 1 edges → `client/client-types.ts`** — *done, mostly.* Now 5 edges. The vocabulary moved to -`contracts/client-api.ts`, with `client/client-types.ts` keeping the `AgentDeviceClient` facade and -re-exporting the rest through one wildcard, so the published `.d.ts` surface is byte-identical -(verified by diffing the built `index.d.ts` name set against `main`: 216 names, no change). +**28 + 1 edges → `client/client-types.ts`** — *done, mostly.* Now 5 edges. The vocabulary moved into +the `contracts/client-*.ts` family files — one file per command/domain family, largest 137 LOC — +with `client/client-types.ts` keeping the `AgentDeviceClient` facade and re-exporting the rest +through one wildcard per family. The published surface is unchanged, verified two ways against +`main`: the exported-name set of all 11 published entrypoints is identical (70 names), and every +declaration in the built `index.d.ts` is byte-identical after normalization (0 names added, 0 shapes +changed). `index.d.ts` in fact got *smaller* — 1,726 → 1,682 lines — because 10 declarations that +`main` duplicated into it (the Metro option/result shapes, `ScrollInputDirection`) now resolve +through a shared chunk once the vocabulary sits below both its consumers. The mutual coupling this section already warned about is what set the floor. Eight shapes could NOT move down, because each is stated in terms of a HIGHER-ranked zone: diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index 57cba6eea..15ae7d81b 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -206,16 +206,22 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): Violation[] { // a design call, not a file move. R5 is zero here: nothing imports // the client at runtime, only its type. // -// core/commands -> daemon-server (3) `DaemonCommandDescriptor` and `DaemonCommandRoute`. -// `DaemonCommandRoute` is `keyof typeof DAEMON_ROUTE_HANDLERS` and -// the descriptor resolves `refFrameEffect` against the daemon's own -// request type, so the shape is derived from what the server -// actually implements. Moving it down would mean re-declaring the -// route names in contracts plus a gate to prove the handler map -// still covers them: more coupling to remove a dependency. ADR -// 0003/0008 own this boundary deliberately. +// core -> daemon-server (2) `DaemonCommandDescriptor`, which is STATED IN TERMS OF the daemon's +// own server-private `DaemonRequest` (`refFrameEffect`, +// `allowSessionlessDefaultDevice`, `skipSessionlessProviderDevice` +// are all `(req: DaemonRequest) => …`). It therefore cannot be +// declared below the daemon, and having core/ re-declare a parallel +// 13-field shape would trade one erased edge for a second source of +// truth. Zones that only need to CLASSIFY a command take +// `contracts/dispatched-command.ts` instead. ADR 0003/0008. // -// The counts may only go DOWN. Fixing edges without lowering the number fails too, so the baseline +// commands -> daemon-server (1) `DaemonCommandRoute` = `keyof typeof DAEMON_ROUTE_HANDLERS`, so +// it is COMPUTED FROM the daemon's handler table and cannot exist +// below it. `commands/command-explain.ts` uses it to key an +// exhaustive `Record` of owner files; a +// hand-written union in contracts/ would drop that exhaustiveness. +// +// See docs/dependency-graph-findings.md §0 for the long form. The counts may only go DOWN. Fixing edges without lowering the number fails too, so the baseline // cannot quietly stop describing the tree. // // Exported so scripts/depgraph can assert its own graph build reproduces it — see the diff --git a/src/__tests__/cli-client-commands.test.ts b/src/__tests__/cli-client-commands.test.ts index c788412c7..f4c0d8a07 100644 --- a/src/__tests__/cli-client-commands.test.ts +++ b/src/__tests__/cli-client-commands.test.ts @@ -13,7 +13,7 @@ import type { MetroPrepareOptions, MetroReloadOptions, } from '../agent-device-client.ts'; -import type { SettingsUpdateOptions } from '../contracts/client-api.ts'; +import type { SettingsUpdateOptions } from '../contracts/client-settings.ts'; import { AppError } from '../kernel/errors.ts'; import { resolveCliOptions } from '../cli/resolve-cli-options.ts'; diff --git a/src/agent-device-client.ts b/src/agent-device-client.ts index 65675ce49..fc932eef0 100644 --- a/src/agent-device-client.ts +++ b/src/agent-device-client.ts @@ -34,31 +34,39 @@ import { import { readScreenshotResultData } from './utils/screenshot-result.ts'; import { isRecord } from './utils/parsing.ts'; import type { - AgentDeviceClientConfig, - AgentDeviceDaemonTransport, AppCloseOptions, AppDeployOptions, - AppInstallOptions, AppInstallFromSourceOptions, + AppInstallOptions, AppListOptions, AppOpenOptions, + MaterializationReleaseOptions, +} from './contracts/client-app.ts'; +import type { CaptureScreenshotOptions, CaptureScreenshotResult, CaptureSnapshotOptions, CaptureSnapshotResult, - InternalRequestOptions, - Lease, - MaterializationReleaseOptions, - MetroPrepareOptions, - PanOptions, +} from './contracts/client-capture.ts'; +import type { + AgentDeviceClientConfig, + AgentDeviceDaemonTransport, +} from './contracts/client-connection.ts'; +import type { FlingOptions, - SwipeGestureOptions, + PanOptions, PinchOptions, RotateGestureOptions, + SwipeGestureOptions, + TransformGestureOptions, +} from './contracts/client-gesture.ts'; +import type { Lease } from './contracts/client-lease.ts'; +import type { InternalRequestOptions } from './contracts/client-request.ts'; +import type { SessionSaveScriptOptions, SessionSaveScriptResult, - TransformGestureOptions, -} from './contracts/client-api.ts'; +} from './contracts/client-session.ts'; +import type { MetroPrepareOptions } from './contracts/metro.ts'; import type { AgentDeviceCommandClient, AgentDeviceClient, diff --git a/src/cli.ts b/src/cli.ts index 1a99db806..e8ca46733 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,7 +7,7 @@ import { readVersion } from './utils/version.ts'; import { pathToFileURL } from 'node:url'; import { sendToDaemon } from './daemon/client/daemon-client.ts'; import fs from 'node:fs'; -import type { BatchStep } from './contracts/client-api.ts'; +import type { BatchStep } from './contracts/client-replay.ts'; import type { ReplayTestReporterRuntime } from './replay/test/reporting.ts'; import { createAgentDeviceClient, diff --git a/src/cli/batch-steps.ts b/src/cli/batch-steps.ts index 3753b9a86..38283ac4a 100644 --- a/src/cli/batch-steps.ts +++ b/src/cli/batch-steps.ts @@ -1,4 +1,4 @@ -import type { BatchStep } from '../contracts/client-api.ts'; +import type { BatchStep } from '../contracts/client-replay.ts'; import { type SessionRuntimeHints } from '../kernel/contracts.ts'; import { parseBatchStepRuntime } from '../contracts/batch-contract.ts'; import { readInputFromCli } from '../commands/cli-grammar.ts'; diff --git a/src/cli/commands/connection-runtime.ts b/src/cli/commands/connection-runtime.ts index adedd3aa0..0c56481f1 100644 --- a/src/cli/commands/connection-runtime.ts +++ b/src/cli/commands/connection-runtime.ts @@ -21,7 +21,7 @@ import { type RemoteConnectionRequestMetadata, } from '../../remote/remote-connection-state.ts'; import { profileToCliFlags } from '../remote-config-flags.ts'; -import type { BatchStep } from '../../contracts/client-api.ts'; +import type { BatchStep } from '../../contracts/client-replay.ts'; import { AppError } from '../../kernel/errors.ts'; import type { LeaseBackend, SessionRuntimeHints } from '../../kernel/contracts.ts'; import type { CliFlags } from '../../contracts/cli-flags.ts'; diff --git a/src/client/client-normalizers.ts b/src/client/client-normalizers.ts index 1730128b7..c9d0415a2 100644 --- a/src/client/client-normalizers.ts +++ b/src/client/client-normalizers.ts @@ -11,15 +11,17 @@ import { } from '../kernel/device.ts'; import { leaseScopeFromOptions, leaseScopeToRequestMeta } from '../core/lease-scope.ts'; import type { - AgentDeviceDevice, - AgentDeviceSession, - AgentDeviceSessionDevice, AppDeployResult, AppInstallFromSourceResult, - InternalRequestOptions, MaterializationReleaseResult, +} from '../contracts/client-app.ts'; +import type { + AgentDeviceDevice, + AgentDeviceSession, + AgentDeviceSessionDevice, StartupPerfSample, -} from '../contracts/client-api.ts'; +} from '../contracts/client-device-view.ts'; +import type { InternalRequestOptions } from '../contracts/client-request.ts'; import type { TargetShutdownResult } from '../contracts/target-shutdown-contract.ts'; import { asRecord, diff --git a/src/client/client-types.ts b/src/client/client-types.ts index 4a832cbe6..0e1a8bf9f 100644 --- a/src/client/client-types.ts +++ b/src/client/client-types.ts @@ -1,22 +1,47 @@ // The published Node client surface. // // The VOCABULARY it is stated in terms of — connection config, the device/session views, and every -// per-command Options/Result shape — lives in contracts/client-api.ts, below both this zone and -// `commands/`. R2 forbids the reverse import, so a shape both surfaces need has to sit below both. +// per-command Options/Result shape — lives in the contracts/client-*.ts family files, one file per +// command/domain family. Both this module and `commands/` (the CLI and daemon command surface) are +// stated in terms of that vocabulary, and R2 forbids `commands/` importing `client/`, so a shape +// both surfaces need has to sit below both. // // What is still declared HERE is the `AgentDeviceClient` facade plus the shapes that are themselves -// stated in terms of a HIGHER-ranked zone — `commands/` (navigation projection, ScrollInputDirection) -// and `metro/` (prepare/reload results). Declaring those in contracts/ would trade 28 -// commands->client inversions for contracts->commands and contracts->metro ones: the foundation -// depending on the layers above it, which is worse. They can move once their upstream declarations -// do — see docs/dependency-graph-findings.md. +// stated in terms of a HIGHER-ranked zone: `commands/system/navigation-projection.ts` (the projected +// navigation client) and `core/` (`CommandResult`, `BatchRunResult`). Declaring those in contracts/ +// would trade 28 commands->client inversions for contracts->commands and contracts->core ones — the +// foundation depending on the layers above it, which is worse. They can move once their upstream +// declarations do; see docs/dependency-graph-findings.md §0, which also explains why the facade's +// own 4 remaining inversions are a position rather than debt. // // The published surface is assembled in agent-device-client.ts, which re-exports BOTH homes, so no // name became unreachable from the package entrypoint. -// The relocated vocabulary keeps its published import path through this module: one wildcard -// rather than 84 named re-exports, so no name is published twice under two spellings. -export type * from '../contracts/client-api.ts'; +// The relocated vocabulary keeps its published import path through this module — a wildcard per +// family rather than 84 named re-exports, so no name is published twice under two spellings. +export type * from '../contracts/client-app.ts'; +export type * from '../contracts/client-capture.ts'; +export type * from '../contracts/client-connection.ts'; +export type * from '../contracts/client-device-view.ts'; +export type * from '../contracts/client-gesture.ts'; +export type * from '../contracts/client-lease.ts'; +export type * from '../contracts/client-observability.ts'; +export type * from '../contracts/client-replay.ts'; +export type * from '../contracts/client-request.ts'; +export type * from '../contracts/client-selector-read.ts'; +export type * from '../contracts/client-session.ts'; +export type * from '../contracts/client-settings.ts'; +export type * from '../contracts/client-system.ts'; +export type * from '../contracts/client-target.ts'; + +// The Metro command vocabulary answers its question in the contracts/metro.ts domain file; named +// rather than wildcarded so the daemon-side Metro shapes there stay out of the published surface. +export type { + MetroPrepareOptions, + MetroPrepareResult, + MetroReloadOptions, + MetroReloadResult, +} from '../contracts/metro.ts'; // Contracts/kernel types re-exported into the PUBLISHED surface: `agent-device-client.ts` picks // these up via `export type *`, and that is their only job — every internal consumer imports @@ -37,13 +62,6 @@ export type { AppleOS } from '../kernel/device.ts'; export type { JsonObject } from '../contracts/json.ts'; import type { - AgentDeviceCapabilitiesResult, - AgentDeviceClientConfig, - AgentDeviceDevice, - AgentDeviceRequestOverrides, - AgentDeviceSelectionOptions, - AgentDeviceSession, - AlertCommandOptions, AppCloseOptions, AppCloseResult, AppDeployOptions, @@ -55,66 +73,91 @@ import type { AppOpenOptions, AppOpenResult, AppPushOptions, - AppStateCommandOptions, AppTriggerEventOptions, - AudioOptions, - BatchRunOptions, + MaterializationReleaseOptions, + MaterializationReleaseResult, +} from '../contracts/client-app.ts'; +import type { CaptureDiffOptions, CaptureScreenshotOptions, CaptureScreenshotResult, CaptureSnapshotOptions, CaptureSnapshotResult, - ClickOptions, - ClipboardCommandOptions, - CloudArtifactsOptions, - CommandRequestResult, - DeviceBootOptions, +} from '../contracts/client-capture.ts'; +import type { + AgentDeviceClientConfig, + AgentDeviceRequestOverrides, + AgentDeviceSelectionOptions, DeviceCommandBaseOptions, +} from '../contracts/client-connection.ts'; +import type { + AgentDeviceCapabilitiesResult, + AgentDeviceDevice, + AgentDeviceSession, + DeviceBootOptions, DeviceShutdownOptions, - DoctorCommandOptions, - EventsOptions, +} from '../contracts/client-device-view.ts'; +import type { + ClickOptions, FillOptions, - FindOptions, FlingOptions, FocusOptions, - GetOptions, - IsOptions, - KeyboardCommandOptions, + LongPressOptions, + PanOptions, + PinchOptions, + PressOptions, + RotateGestureOptions, + ScrollOptions, + SwipeGestureOptions, + SwipeOptions, + TransformGestureOptions, + TypeTextOptions, +} from '../contracts/client-gesture.ts'; +import type { + CloudArtifactsOptions, Lease, LeaseAllocateOptions, LeaseScopedOptions, +} from '../contracts/client-lease.ts'; +import type { + AudioOptions, + EventsOptions, LogsOptions, - LongPressOptions, - MaterializationReleaseOptions, - MaterializationReleaseResult, - MetroPrepareOptions, - MetroPrepareResult, - MetroReloadOptions, - MetroReloadResult, NetworkOptions, - PanOptions, PerfOptions, - PinchOptions, - PrepareCommandOptions, - PressOptions, - ReactNativeCommandOptions, RecordOptions, + TraceOptions, +} from '../contracts/client-observability.ts'; +import type { + BatchRunOptions, ReplayRunOptions, ReplayTestOptions, - RotateGestureOptions, - ScrollOptions, +} from '../contracts/client-replay.ts'; +import type { CommandRequestResult } from '../contracts/client-request.ts'; +import type { FindOptions, GetOptions, IsOptions } from '../contracts/client-selector-read.ts'; +import type { SessionCloseResult, SessionSaveScriptOptions, SessionSaveScriptResult, - SettingsUpdateOptions, - SwipeGestureOptions, - SwipeOptions, - TraceOptions, - TransformGestureOptions, - TypeTextOptions, +} from '../contracts/client-session.ts'; +import type { SettingsUpdateOptions } from '../contracts/client-settings.ts'; +import type { + AlertCommandOptions, + AppStateCommandOptions, + ClipboardCommandOptions, + DoctorCommandOptions, + KeyboardCommandOptions, + PrepareCommandOptions, + ReactNativeCommandOptions, ViewportCommandOptions, WaitCommandOptions, -} from '../contracts/client-api.ts'; +} from '../contracts/client-system.ts'; +import type { + MetroPrepareOptions, + MetroPrepareResult, + MetroReloadOptions, + MetroReloadResult, +} from '../contracts/metro.ts'; import type { RotateCommandResult } from '../contracts/navigation.ts'; @@ -191,22 +234,6 @@ type DeprecatedCommandClient = { rotate: (options: RotateCommandOptions) => Promise; }; -/** - * Opt-in (#1101): after the action, wait for the UI to go quiet and return the - * settled diff vs the pre-action tree (`settle` on the result) in the same - * response. Best-effort — never fails the action. `settleQuietMs` tunes the - * quiet window (default 500ms); `timeoutMs` bounds the settle wait (default - * 10s) when `settle` is true. A bare `timeoutMs` without `settle` is ignored - * for compatibility; `settleQuietMs` still requires `settle`. - */ - -/** - * #1271 stage 2 (ADR 0012 amendment): `get`/`is`/`find` are observation-only - * and excluded from a repair-armed heal by default. `record` forces this - * action through (the corrective-read case); `noRecord` continues to opt the - * action out entirely. Mutually exclusive. - */ - export type AgentDeviceClient = { command: AgentDeviceCommandClient; devices: { diff --git a/src/commands/__tests__/command-flags.test.ts b/src/commands/__tests__/command-flags.test.ts index a264d54b5..ee36d25f9 100644 --- a/src/commands/__tests__/command-flags.test.ts +++ b/src/commands/__tests__/command-flags.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; -import type { InternalRequestOptions } from '../../contracts/client-api.ts'; +import type { InternalRequestOptions } from '../../contracts/client-request.ts'; import { findCommandMetadata } from '../command-metadata.ts'; import { readMetadataCommandFlags } from '../command-flags.ts'; diff --git a/src/commands/batch/index.ts b/src/commands/batch/index.ts index 309a5f3e3..7c744c1a9 100644 --- a/src/commands/batch/index.ts +++ b/src/commands/batch/index.ts @@ -1,4 +1,4 @@ -import type { BatchRunOptions } from '../../contracts/client-api.ts'; +import type { BatchRunOptions } from '../../contracts/client-replay.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { commonInputFromFlags } from '../cli-grammar/common.ts'; import type { CliReader } from '../cli-grammar/types.ts'; diff --git a/src/commands/capture/alert.ts b/src/commands/capture/alert.ts index 5bde314ad..ca6051fa0 100644 --- a/src/commands/capture/alert.ts +++ b/src/commands/capture/alert.ts @@ -1,6 +1,6 @@ import { ALERT_ACTIONS, type AlertAction } from '../../contracts/alert-contract.ts'; import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { AlertCommandOptions } from '../../contracts/client-api.ts'; +import type { AlertCommandOptions } from '../../contracts/client-system.ts'; import { compactRecord, enumField, integerField } from '../command-input.ts'; import { defineExecutableCommand } from '../command-contract.ts'; import { diff --git a/src/commands/capture/output.test.ts b/src/commands/capture/output.test.ts index efb051a24..a59d083b6 100644 --- a/src/commands/capture/output.test.ts +++ b/src/commands/capture/output.test.ts @@ -1,7 +1,7 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { attachRefs, type RawSnapshotNode } from '../../kernel/snapshot.ts'; -import type { CaptureSnapshotResult } from '../../contracts/client-api.ts'; +import type { CaptureSnapshotResult } from '../../contracts/client-capture.ts'; import { snapshotCliOutput } from './output.ts'; function buildResult(raw: RawSnapshotNode[]): CaptureSnapshotResult { diff --git a/src/commands/capture/output.ts b/src/commands/capture/output.ts index bfb34eaf1..caeca340f 100644 --- a/src/commands/capture/output.ts +++ b/src/commands/capture/output.ts @@ -1,5 +1,5 @@ import { serializeSnapshotResult } from '../../contracts/result-serialization.ts'; -import type { CaptureSnapshotResult } from '../../contracts/client-api.ts'; +import type { CaptureSnapshotResult } from '../../contracts/client-capture.ts'; import { dedupeInheritedSnapshotLabels } from '../../snapshot/snapshot-label-dedup.ts'; import { formatSnapshotText } from '../../utils/output.ts'; import type { CliOutput } from '../command-contract.ts'; diff --git a/src/commands/capture/screenshot.ts b/src/commands/capture/screenshot.ts index cfa30834c..ba65e539e 100644 --- a/src/commands/capture/screenshot.ts +++ b/src/commands/capture/screenshot.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { CaptureScreenshotOptions } from '../../contracts/client-api.ts'; +import type { CaptureScreenshotOptions } from '../../contracts/client-capture.ts'; import { SESSION_SURFACES } from '../../contracts/session-surface.ts'; import { SCREENSHOT_COMMAND_FLAG_KEYS, diff --git a/src/commands/capture/settings.ts b/src/commands/capture/settings.ts index 9489b5074..f4586d885 100644 --- a/src/commands/capture/settings.ts +++ b/src/commands/capture/settings.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { SettingsUpdateOptions } from '../../contracts/client-api.ts'; +import type { SettingsUpdateOptions } from '../../contracts/client-settings.ts'; import { SETTINGS_USAGE_OVERRIDE } from '../../contracts/settings-contract.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import type { CliFlags } from '../../contracts/cli-flags.ts'; diff --git a/src/commands/capture/wait.ts b/src/commands/capture/wait.ts index d23faa823..552921967 100644 --- a/src/commands/capture/wait.ts +++ b/src/commands/capture/wait.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { WaitCommandOptions } from '../../contracts/client-api.ts'; +import type { WaitCommandOptions } from '../../contracts/client-system.ts'; import { parseWaitPositionals } from '../../core/wait-positionals.ts'; import { SELECTOR_SNAPSHOT_FLAGS } from '../cli-grammar/flag-groups.ts'; import { type CliFlags } from '../cli-grammar/flag-types.ts'; diff --git a/src/commands/cli-grammar/common.ts b/src/commands/cli-grammar/common.ts index a2d94068d..4d862bf20 100644 --- a/src/commands/cli-grammar/common.ts +++ b/src/commands/cli-grammar/common.ts @@ -1,8 +1,5 @@ -import type { - ElementTarget, - InteractionTarget, - InternalRequestOptions, -} from '../../contracts/client-api.ts'; +import type { InternalRequestOptions } from '../../contracts/client-request.ts'; +import type { ElementTarget, InteractionTarget } from '../../contracts/client-target.ts'; import { splitSelectorFromArgs } from '../../selectors/parse.ts'; import { checkElementTargetArgs, diff --git a/src/commands/cli-grammar/types.ts b/src/commands/cli-grammar/types.ts index 82b20a981..3b1d6480d 100644 --- a/src/commands/cli-grammar/types.ts +++ b/src/commands/cli-grammar/types.ts @@ -1,4 +1,4 @@ -import type { InternalRequestOptions } from '../../contracts/client-api.ts'; +import type { InternalRequestOptions } from '../../contracts/client-request.ts'; import type { CommandFlags } from '../../core/dispatch-context.ts'; import type { CliFlags } from '../../contracts/cli-flags.ts'; import type { ClickButton } from '../../contracts/click-button.ts'; diff --git a/src/commands/command-flags.ts b/src/commands/command-flags.ts index c832fbf65..c8177a4ef 100644 --- a/src/commands/command-flags.ts +++ b/src/commands/command-flags.ts @@ -3,7 +3,7 @@ import type { CommandFlags } from '../core/dispatch-context.ts'; import { leaseScopeFromOptions, leaseScopeToCommandFlags } from '../core/lease-scope.ts'; import { stripUndefined } from '../utils/parsing.ts'; import { getFlagDefinitions } from './cli-grammar/flag-registry.ts'; -import type { InternalRequestOptions } from '../contracts/client-api.ts'; +import type { InternalRequestOptions } from '../contracts/client-request.ts'; import type { CommandMetadata } from './command-contract.ts'; const CLI_FLAG_KEYS: ReadonlySet = new Set( diff --git a/src/commands/command-input.ts b/src/commands/command-input.ts index 142d83dcf..89bb05fd5 100644 --- a/src/commands/command-input.ts +++ b/src/commands/command-input.ts @@ -1,9 +1,8 @@ import type { AgentDeviceRequestOverrides, AgentDeviceSelectionOptions, - ElementTarget, - InteractionTarget, -} from '../contracts/client-api.ts'; +} from '../contracts/client-connection.ts'; +import type { ElementTarget, InteractionTarget } from '../contracts/client-target.ts'; import { DEVICE_TARGETS, PLATFORM_SELECTORS, diff --git a/src/commands/interaction/index.ts b/src/commands/interaction/index.ts index c3fc4edc1..31c3e23b2 100644 --- a/src/commands/interaction/index.ts +++ b/src/commands/interaction/index.ts @@ -1,11 +1,8 @@ import type { ClickOptions, FillOptions, - FindOptions, FlingOptions, FocusOptions, - GetOptions, - IsOptions, LongPressOptions, PanOptions, PinchOptions, @@ -16,7 +13,8 @@ import type { SwipeOptions, TransformGestureOptions, TypeTextOptions, -} from '../../contracts/client-api.ts'; +} from '../../contracts/client-gesture.ts'; +import type { FindOptions, GetOptions, IsOptions } from '../../contracts/client-selector-read.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { REPEATED_TOUCH_FLAGS, diff --git a/src/commands/interaction/interactions.ts b/src/commands/interaction/interactions.ts index 67157cba4..0234b681f 100644 --- a/src/commands/interaction/interactions.ts +++ b/src/commands/interaction/interactions.ts @@ -1,11 +1,10 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; import type { - ElementTarget, FillOptions, - InteractionTarget, LongPressOptions, TypeTextOptions, -} from '../../contracts/client-api.ts'; +} from '../../contracts/client-gesture.ts'; +import type { ElementTarget, InteractionTarget } from '../../contracts/client-target.ts'; import { readFillTargetFromPositionals, readInteractionTargetFromPositionals, diff --git a/src/commands/interaction/output.ts b/src/commands/interaction/output.ts index ddc3e8912..a650c785b 100644 --- a/src/commands/interaction/output.ts +++ b/src/commands/interaction/output.ts @@ -1,4 +1,4 @@ -import type { CommandRequestResult } from '../../contracts/client-api.ts'; +import type { CommandRequestResult } from '../../contracts/client-request.ts'; import type { CliOutput } from '../command-contract.ts'; import { readCommandMessage } from '../../utils/success-text.ts'; import { messageCliOutput, resultOutput, type CliOutputFormatter } from '../output-common.ts'; diff --git a/src/commands/interaction/selectors.ts b/src/commands/interaction/selectors.ts index 32cd520d4..4b8a705ee 100644 --- a/src/commands/interaction/selectors.ts +++ b/src/commands/interaction/selectors.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { FindOptions, IsOptions } from '../../contracts/client-api.ts'; +import type { FindOptions, IsOptions } from '../../contracts/client-selector-read.ts'; import type { CliFlags } from '../../contracts/cli-flags.ts'; import { AppError } from '../../kernel/errors.ts'; import { checkIsPredicate, normalizeIsPositionals } from '../../selectors/predicates.ts'; diff --git a/src/commands/management/app.ts b/src/commands/management/app.ts index fee8ff8ad..7ed6caba6 100644 --- a/src/commands/management/app.ts +++ b/src/commands/management/app.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { AppCloseOptions, AppOpenOptions } from '../../contracts/client-api.ts'; +import type { AppCloseOptions, AppOpenOptions } from '../../contracts/client-app.ts'; import { DEFAULT_APPS_FILTER } from '../../contracts/app-inventory.ts'; import { SESSION_SURFACES } from '../../contracts/session-surface.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; diff --git a/src/commands/management/output.test.ts b/src/commands/management/output.test.ts index 1414a66fa..4ad009ada 100644 --- a/src/commands/management/output.test.ts +++ b/src/commands/management/output.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest'; import { doctorCliOutput, managementCliOutputFormatters, openCliOutput } from './output.ts'; import { markDoctorProgressRendered } from '../../contracts/cli-doctor-output.ts'; import { withNoColor } from '../../__tests__/test-utils/index.ts'; -import type { AppOpenResult } from '../../contracts/client-api.ts'; +import type { AppOpenResult } from '../../contracts/client-app.ts'; describe('openCliOutput', () => { test('prints session state directory on a second line', () => { diff --git a/src/commands/management/output.ts b/src/commands/management/output.ts index 0315718d6..5ae5db777 100644 --- a/src/commands/management/output.ts +++ b/src/commands/management/output.ts @@ -7,17 +7,21 @@ import { serializeSessionListEntry, } from '../../contracts/result-serialization.ts'; import type { - AgentDeviceCapabilitiesResult, - AgentDeviceDevice, - AgentDeviceSession, AppCloseResult, AppDeployResult, AppInstallFromSourceResult, AppOpenResult, - CommandRequestResult, +} from '../../contracts/client-app.ts'; +import type { + AgentDeviceCapabilitiesResult, + AgentDeviceDevice, + AgentDeviceSession, +} from '../../contracts/client-device-view.ts'; +import type { CommandRequestResult } from '../../contracts/client-request.ts'; +import type { SessionCloseResult, SessionSaveScriptResult, -} from '../../contracts/client-api.ts'; +} from '../../contracts/client-session.ts'; import type { AgentArtifactsResult, CloudArtifactsResult, diff --git a/src/commands/management/push.ts b/src/commands/management/push.ts index 6bea00aa9..5d1e6b116 100644 --- a/src/commands/management/push.ts +++ b/src/commands/management/push.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { AppPushOptions, AppTriggerEventOptions } from '../../contracts/client-api.ts'; +import type { AppPushOptions, AppTriggerEventOptions } from '../../contracts/client-app.ts'; import type { JsonObject } from '../../contracts/json.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { diff --git a/src/commands/management/viewport.ts b/src/commands/management/viewport.ts index d8ce9b52b..d1bd07c87 100644 --- a/src/commands/management/viewport.ts +++ b/src/commands/management/viewport.ts @@ -1,5 +1,5 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; -import type { ViewportCommandOptions } from '../../contracts/client-api.ts'; +import type { ViewportCommandOptions } from '../../contracts/client-system.ts'; import { readViewportDimension } from '../../core/viewport-dimension.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { integerField, requiredField } from '../command-input.ts'; diff --git a/src/commands/metro/index.ts b/src/commands/metro/index.ts index 56d022d87..8248cbe1a 100644 --- a/src/commands/metro/index.ts +++ b/src/commands/metro/index.ts @@ -3,7 +3,7 @@ import type { MetroPrepareResult, MetroReloadOptions, MetroReloadResult, -} from '../../contracts/client-api.ts'; +} from '../../contracts/metro.ts'; import { AppError } from '../../kernel/errors.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { diff --git a/src/commands/observability/index.ts b/src/commands/observability/index.ts index 70ab5525a..17962ddef 100644 --- a/src/commands/observability/index.ts +++ b/src/commands/observability/index.ts @@ -3,7 +3,7 @@ import type { EventsOptions, LogsOptions, NetworkOptions, -} from '../../contracts/client-api.ts'; +} from '../../contracts/client-observability.ts'; import { NETWORK_INCLUDE_MODES, type NetworkIncludeMode } from '../../kernel/contracts.ts'; import { AppError } from '../../kernel/errors.ts'; import { parseStringMember } from '../../utils/string-enum.ts'; diff --git a/src/commands/perf/index.ts b/src/commands/perf/index.ts index df0530ce4..37cc585b3 100644 --- a/src/commands/perf/index.ts +++ b/src/commands/perf/index.ts @@ -1,4 +1,4 @@ -import type { PerfOptions } from '../../contracts/client-api.ts'; +import type { PerfOptions } from '../../contracts/client-observability.ts'; import { AppError } from '../../kernel/errors.ts'; import type { CommandSchemaOverride } from '../../cli-schema/types.ts'; import { enumField, stringField } from '../command-input.ts'; diff --git a/src/commands/perf/output.ts b/src/commands/perf/output.ts index 0512a0a7e..cbb136196 100644 --- a/src/commands/perf/output.ts +++ b/src/commands/perf/output.ts @@ -1,4 +1,4 @@ -import type { CommandRequestResult } from '../../contracts/client-api.ts'; +import type { CommandRequestResult } from '../../contracts/client-request.ts'; import { isRecord } from '../../utils/parsing.ts'; import type { CliOutput } from '../command-contract.ts'; import { resultOutput, type CliOutputFormatter } from '../output-common.ts'; diff --git a/src/commands/recording/index.ts b/src/commands/recording/index.ts index 0359c47d3..2cfbe79e2 100644 --- a/src/commands/recording/index.ts +++ b/src/commands/recording/index.ts @@ -1,4 +1,4 @@ -import type { RecordOptions } from '../../contracts/client-api.ts'; +import type { RecordOptions } from '../../contracts/client-observability.ts'; import { RECORDING_EXPORT_QUALITIES } from '../../contracts/recording-export-quality.ts'; import { RECORDING_SCOPE_VALUES } from '../../contracts/recording-scope.ts'; import { AppError } from '../../kernel/errors.ts'; diff --git a/src/commands/recording/output.ts b/src/commands/recording/output.ts index d6443a5ed..a10f04b9a 100644 --- a/src/commands/recording/output.ts +++ b/src/commands/recording/output.ts @@ -1,4 +1,4 @@ -import type { CommandRequestResult } from '../../contracts/client-api.ts'; +import type { CommandRequestResult } from '../../contracts/client-request.ts'; import type { CliOutput } from '../command-contract.ts'; import { resultOutput, type CliOutputFormatter } from '../output-common.ts'; diff --git a/src/commands/system/index.ts b/src/commands/system/index.ts index c3aa756d5..47b27a8da 100644 --- a/src/commands/system/index.ts +++ b/src/commands/system/index.ts @@ -1,4 +1,4 @@ -import type { ClipboardCommandOptions } from '../../contracts/client-api.ts'; +import type { ClipboardCommandOptions } from '../../contracts/client-system.ts'; import type { BackMode } from '../../contracts/back-mode.ts'; import { BACK_MODES } from '../../contracts/back-mode.ts'; import { parseDeviceRotation, DEVICE_ROTATIONS } from '../../contracts/device-rotation.ts'; diff --git a/src/contracts/batch-step.ts b/src/contracts/batch-step.ts index fd03484da..126e70ba6 100644 --- a/src/contracts/batch-step.ts +++ b/src/contracts/batch-step.ts @@ -4,7 +4,7 @@ import type { SessionRuntimeHints } from '../kernel/contracts.ts'; * One step of a daemon batch, as submitted. * * Declared here rather than in `core/batch.ts` because the public API vocabulary - * (`contracts/client-api.ts`) is stated in terms of it, and `core/` sits above contracts. The + * (`contracts/client-replay.ts`) is stated in terms of it, and `core/` sits above contracts. The * `runtime` field used to be written as `DaemonRequest['runtime']`, which pulled the whole daemon * request type in to say `SessionRuntimeHints` — the same type, one zone lower. */ diff --git a/src/contracts/client-api.ts b/src/contracts/client-api.ts deleted file mode 100644 index 1133be2ab..000000000 --- a/src/contracts/client-api.ts +++ /dev/null @@ -1,1064 +0,0 @@ -import type { ScrollInputDirection } from './scroll-gesture.ts'; -import type { PrepareMetroRuntimeResult, ReloadMetroResult } from './metro.ts'; -// The public API vocabulary: connection config, the device/session views, and every per-command -// Options/Result shape. Declared here rather than inside `client/` because BOTH `client/` (the -// published Node surface) and `commands/` (the CLI/daemon command surface) are stated in terms of -// it. It used to live in `client/client-types.ts`, which made 28 of the layering gate's 30 -// remaining type-only inversions a single edge: `commands` declaring itself in terms of the client -// it is a peer of. R2 forbids the reverse import, so the shape has to sit below both. -// -// `client/client-types.ts` re-exports all of this, so the published API is byte-identical; it now -// holds only the `AgentDeviceClient` facade that binds these shapes to methods. -// -// The contents below are a verbatim relocation — no declaration was edited in the move. - -import type { PublicSnapshotCaptureAnnotations } from '../contracts/snapshot-capture-annotations.ts'; -import type { SnapshotDiagnosticsSummary } from '../contracts/snapshot-diagnostics.ts'; -import type { - DaemonResponseData, - DaemonInstallSource, - DaemonLockPolicy, - DaemonRequest, - DaemonResponse, - LeaseBackend, - NetworkIncludeMode, - ResponseLevel, - SessionIsolationMode, - SessionRuntimeHints, -} from '../kernel/contracts.ts'; -import type { - AppleOS, - DeviceKind, - DeviceTarget, - PublicPlatform, - PlatformSelector, -} from '../kernel/device.ts'; -import type { BackMode } from '../contracts/back-mode.ts'; -import type { ClickButton } from '../contracts/click-button.ts'; -import type { RecordingExportQuality } from '../contracts/recording-export-quality.ts'; -import type { RecordingScope } from '../contracts/recording-scope.ts'; -import type { - ScrollDirection, - SwipePattern, - SwipePreset, - TransformGestureParams, -} from '../contracts/scroll-gesture.ts'; -import type { GesturePointerCount } from '../contracts/gesture-plan.ts'; -import type { LogAction } from '../contracts/logs.ts'; -import type { SessionSurface } from '../contracts/session-surface.ts'; -import type { FindLocator } from '../selectors/find.ts'; -import type { SnapshotNode, SnapshotUnchanged, SnapshotVisibility } from '../kernel/snapshot.ts'; -import type { ScreenshotResultData } from '../utils/screenshot-result.ts'; -import type { MetroPrepareKind } from '../contracts/metro.ts'; -import type { MetroBridgeScope } from './companion-tunnel-scope.ts'; -import type { AppsFilter } from '../contracts/app-inventory.ts'; -import type { ScreenshotRequestFlags } from '../contracts/screenshot.ts'; -import type { DaemonBatchStep } from './batch-step.ts'; -import type { TargetShutdownResult } from '../contracts/target-shutdown-contract.ts'; -import type { PerfAction, PerfArea, PerfKind, PerfSubject } from '../contracts/perf.ts'; -import type { AlertAction } from '../contracts/alert-contract.ts'; -import type { JsonObject } from '../contracts/json.ts'; -import type { - CloudProviderProfileFields, - RemoteConnectionProfileFields, -} from './remote-config-fields.ts'; -import type { CloudProviderSessionResult } from '../contracts/cloud-artifacts.ts'; - -export type AgentDeviceDaemonTransport = ( - req: Omit, -) => Promise; - -export type AgentDeviceClientConfig = RemoteConnectionProfileFields & - CloudProviderProfileFields & { - session?: string; - lockPolicy?: DaemonLockPolicy; - lockPlatform?: PlatformSelector; - requestId?: string; - sessionIsolation?: SessionIsolationMode; - leaseBackend?: LeaseBackend; - leaseTtlMs?: number; - runtime?: SessionRuntimeHints; - cwd?: string; - debug?: boolean; - cost?: boolean; - responseLevel?: ResponseLevel; - iosXctestrunFile?: string; - iosXctestDerivedDataPath?: string; - iosXctestEnvDir?: string; - }; - -export type AgentDeviceRequestOverrides = Pick< - AgentDeviceClientConfig, - | 'session' - | 'lockPolicy' - | 'lockPlatform' - | 'requestId' - | 'daemonBaseUrl' - | 'daemonAuthToken' - | 'daemonTransport' - | 'daemonServerMode' - | 'tenant' - | 'sessionIsolation' - | 'runId' - | 'leaseId' - | 'leaseBackend' - | 'leaseProvider' - | 'deviceKey' - | 'clientId' - | 'providerApp' - | 'providerOsVersion' - | 'providerProject' - | 'providerBuild' - | 'providerSessionName' - | 'awsProjectArn' - | 'awsDeviceArn' - | 'awsAppArn' - | 'awsRegion' - | 'awsInteractionMode' - | 'leaseTtlMs' - | 'cwd' - | 'debug' - | 'cost' - | 'responseLevel' - | 'iosXctestrunFile' - | 'iosXctestDerivedDataPath' - | 'iosXctestEnvDir' ->; - -export type AgentDeviceIdentifiers = { - session?: string; - deviceId?: string; - deviceName?: string; - udid?: string; - serial?: string; - appId?: string; - appBundleId?: string; - package?: string; -}; - -export type AgentDeviceSelectionOptions = { - platform?: PlatformSelector; - target?: DeviceTarget; - device?: string; - udid?: string; - serial?: string; - iosSimulatorDeviceSet?: string; - androidDeviceAllowlist?: string; -}; - -export type AgentDeviceDevice = { - platform: PublicPlatform; - target: DeviceTarget; - kind: DeviceKind; - id: string; - name: string; - booted?: boolean; - /** - * Additive Apple-OS discriminant (iPhone/iPad/tvOS/visionOS/macOS). Present only for - * Apple devices; `platform` still carries the leaf (`ios`/`macos`). - */ - appleOs?: AppleOS; - identifiers: AgentDeviceIdentifiers; - ios?: { - udid: string; - }; - android?: { - serial: string; - }; - vega?: { - serial: string; - }; -}; - -export type AgentDeviceCapabilitiesResult = { - device: AgentDeviceDevice; - availableCommands: string[]; -}; - -export type AgentDeviceSessionDevice = { - platform: PublicPlatform; - target: DeviceTarget; - id: string; - name: string; - /** - * Additive Apple-OS discriminant (iPhone/iPad/tvOS/visionOS/macOS). Present only for - * Apple devices; `platform` still carries the leaf (`ios`/`macos`). - */ - appleOs?: AppleOS; - identifiers: AgentDeviceIdentifiers; - ios?: { - udid: string; - simulatorSetPath?: string | null; - }; - android?: { - serial: string; - }; - vega?: { - serial: string; - }; -}; - -export type AgentDeviceSession = { - name: string; - createdAt: number; - sessionStateDir?: string; - runnerLogPath?: string; - device: AgentDeviceSessionDevice; - identifiers: AgentDeviceIdentifiers; -}; - -export type StartupPerfSample = { - durationMs: number; - measuredAt: string; - method: string; - appTarget?: string; - appBundleId?: string; -}; - -export type SessionCloseResult = { - session: string; - shutdown?: TargetShutdownResult; - provider?: CloudProviderSessionResult; - /** - * #1258: absolute path of the committed session/healed script when this close - * published one (`close --save-script`, or a repair-armed session's finalize) - * — so a client that requested publication learns where the file landed. - */ - savedScript?: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type SessionSaveScriptOptions = AgentDeviceRequestOverrides & { - path?: string; - /** Atomically replace an existing target instead of refusing publication. */ - force?: boolean; -}; - -export type SessionSaveScriptResult = { - session: string; - savedScript: string; - actionCount: number; - identifiers: AgentDeviceIdentifiers; -}; - -export type CloudArtifactsOptions = AgentDeviceRequestOverrides & { - provider?: string; - providerSessionId?: string; -}; - -export type AppInstallOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - app?: string; - appPath: string; - }; - -export type AppDeployOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - app: string; - appPath: string; - }; - -export type AppDeployResult = { - app: string; - appPath: string; - platform: PublicPlatform; - appId?: string; - bundleId?: string; - package?: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type AppOpenOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - app?: string; - url?: string; - surface?: SessionSurface; - activity?: string; - launchConsole?: string; - launchArgs?: string[]; - relaunch?: boolean; - saveScript?: boolean | string; - /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ - force?: boolean; - deviceHub?: boolean; - testIme?: boolean; - noRecord?: boolean; - runtime?: SessionRuntimeHints; - }; - -export type AppOpenResult = { - session: string; - warnings?: string[]; - sessionStateDir?: string; - runnerLogPath?: string; - requestLogPath?: string; - eventLogPath?: string; - appName?: string; - appBundleId?: string; - appId?: string; - startup?: StartupPerfSample; - runtime?: SessionRuntimeHints; - device?: AgentDeviceSessionDevice; - identifiers: AgentDeviceIdentifiers; -}; - -export type AppCloseOptions = AgentDeviceRequestOverrides & { - app?: string; - shutdown?: boolean; - saveScript?: boolean | string; - /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ - force?: boolean; -}; - -export type AppCloseResult = { - session: string; - closedApp?: string; - shutdown?: TargetShutdownResult; - /** - * #1258: absolute path of the committed session/healed script when this close - * published one (`close --save-script`, or a repair-armed session's finalize) - * — so a client that requested publication learns where the file landed. - */ - savedScript?: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type AppInstallFromSourceOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - source: DaemonInstallSource; - retainPaths?: boolean; - retentionMs?: number; - }; - -export type AppInstallFromSourceResult = { - appName?: string; - appId?: string; - bundleId?: string; - packageName?: string; - launchTarget: string; - installablePath?: string; - archivePath?: string; - materializationId?: string; - materializationExpiresAt?: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type AppListOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - appsFilter?: AppsFilter; - }; - -export type MaterializationReleaseOptions = AgentDeviceRequestOverrides & { - materializationId: string; -}; - -export type MaterializationReleaseResult = { - released: boolean; - materializationId: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type Lease = { - leaseId: string; - tenantId: string; - runId: string; - backend: LeaseBackend; - leaseProvider?: string; - deviceKey?: string; - clientId?: string; - createdAt?: number; - heartbeatAt?: number; - expiresAt?: number; -}; - -export type LeaseOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - ttlMs?: number; - }; - -export type LeaseAllocateOptions = LeaseOptions & { - tenant: string; - runId: string; - leaseBackend?: LeaseBackend; - leaseProvider?: string; - provider?: string; - deviceKey?: string; - clientId?: string; -}; - -export type LeaseScopedOptions = LeaseOptions & { - tenant?: string; - runId?: string; - leaseId: string; - leaseBackend?: LeaseBackend; - leaseProvider?: string; - provider?: string; - deviceKey?: string; - clientId?: string; -}; - -export type MetroPrepareOptions = { - projectRoot?: string; - kind?: MetroPrepareKind; - publicBaseUrl?: string; - proxyBaseUrl?: string; - bearerToken?: string; - bridgeScope?: MetroBridgeScope; - launchUrl?: string; - companionProfileKey?: string; - companionConsumerKey?: string; - port?: number; - listenHost?: string; - statusHost?: string; - startupTimeoutMs?: number; - probeTimeoutMs?: number; - reuseExisting?: boolean; - installDependenciesIfNeeded?: boolean; - runtimeFilePath?: string; - logPath?: string; -}; - -export type MetroReloadOptions = { - metroHost?: string; - metroPort?: number; - bundleUrl?: string; - timeoutMs?: number; -}; - -export type CaptureSnapshotOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - interactiveOnly?: boolean; - depth?: number; - scope?: string; - raw?: boolean; - forceFull?: boolean; - timeoutMs?: number; - /** - * #1271 stage 2 (ADR 0012 amendment): `snapshot` is observation-only and - * excluded from a repair-armed heal by default; `record` forces it - * through. Mutually exclusive with `noRecord`. - */ - noRecord?: boolean; - record?: boolean; - }; - -export type CaptureSnapshotResult = { - nodes: SnapshotNode[]; - truncated: boolean; - appName?: string; - appBundleId?: string; - visibility?: SnapshotVisibility; - unchanged?: SnapshotUnchanged; - snapshotDiagnostics?: SnapshotDiagnosticsSummary; - identifiers: AgentDeviceIdentifiers; - /** - * ADR 0014: the response-level ref-frame epoch the plain node refs were minted - * from. A ref-issuing snapshot carries it ONCE (nodes stay plain `@e12` for the - * token budget); pair a ref with it (`@e12~s`) before a mutation. - */ - refsGeneration?: number; - /** - * Digest response view only: a capped list of `{ ref, label? }` pairs taken - * from the full `nodes` tree so the MCP layer can still pin refs when the - * default-level `nodes` payload is intentionally omitted. - */ - refs?: Array<{ ref: string; label?: string }>; -} & PublicSnapshotCaptureAnnotations; - -export type CaptureScreenshotOptions = AgentDeviceRequestOverrides & { - path?: string; - overlayRefs?: boolean; - pixelDensity?: number; - fullscreen?: boolean; - maxSize?: number; - stabilize?: boolean; - normalizeStatusBar?: boolean; - surface?: SessionSurface; -}; - -export type CaptureScreenshotResult = ScreenshotResultData & { - path: string; - identifiers: AgentDeviceIdentifiers; -}; - -export type DeviceCommandBaseOptions = AgentDeviceRequestOverrides & AgentDeviceSelectionOptions; - -type WaitCommandTarget = - | { - durationMs: number; - text?: never; - ref?: never; - selector?: never; - stable?: never; - quietMs?: never; - timeoutMs?: never; - } - | (SelectorSnapshotCommandOptions & { - text: string; - durationMs?: never; - ref?: never; - selector?: never; - stable?: never; - quietMs?: never; - timeoutMs?: number; - }) - | (SelectorSnapshotCommandOptions & { - ref: string; - durationMs?: never; - text?: never; - selector?: never; - stable?: never; - quietMs?: never; - timeoutMs?: number; - }) - | (SelectorSnapshotCommandOptions & { - selector: string; - durationMs?: never; - text?: never; - ref?: never; - stable?: never; - quietMs?: never; - timeoutMs?: number; - }) - | (SelectorSnapshotCommandOptions & { - stable: true; - durationMs?: never; - text?: never; - ref?: never; - selector?: never; - quietMs?: number; - timeoutMs?: number; - }); - -export type WaitCommandOptions = DeviceCommandBaseOptions & WaitCommandTarget; - -export type AlertCommandOptions = DeviceCommandBaseOptions & { - action?: AlertAction; - timeoutMs?: number; -}; - -export type AppStateCommandOptions = DeviceCommandBaseOptions; - -export type KeyboardCommandOptions = DeviceCommandBaseOptions & { - action?: 'status' | 'dismiss' | 'enter' | 'return'; -}; - -export type ClipboardCommandOptions = - | (DeviceCommandBaseOptions & { - action: 'read'; - }) - | (DeviceCommandBaseOptions & { - action: 'write'; - text: string; - }); - -export type ReactNativeCommandOptions = DeviceCommandBaseOptions & { - action: 'dismiss-overlay'; -}; - -export type PrepareCommandOptions = DeviceCommandBaseOptions & { - action: 'ios-runner'; - timeoutMs?: number; -}; - -export type DoctorCommandOptions = DeviceCommandBaseOptions & { - targetApp?: string; - remote?: boolean; -}; - -export type ViewportCommandOptions = DeviceCommandBaseOptions & { - width: number; - height: number; -}; - -type SelectorSnapshotCommandOptions = Pick; -type FindSnapshotCommandOptions = Pick; - -type PointTarget = { - x: number; - y: number; - ref?: never; - selector?: never; - label?: never; -}; - -type RefTarget = { - ref: string; - label?: string; - x?: never; - y?: never; - selector?: never; -}; - -type SelectorTarget = { - selector: string; - x?: never; - y?: never; - ref?: never; - label?: never; -}; - -export type InteractionTarget = PointTarget | RefTarget | SelectorTarget; - -export type ElementTarget = RefTarget | SelectorTarget; - -type RepeatedPressOptions = { - count?: number; - intervalMs?: number; - holdMs?: number; - jitterPx?: number; - doubleTap?: boolean; -}; - -export type DeviceBootOptions = DeviceCommandBaseOptions & { - headless?: boolean; -}; - -export type DeviceShutdownOptions = DeviceCommandBaseOptions; - -export type AppPushOptions = DeviceCommandBaseOptions & { - app: string; - payload: string | JsonObject; -}; - -export type AppTriggerEventOptions = DeviceCommandBaseOptions & { - event: string; - payload?: JsonObject; -}; - -export type CaptureDiffOptions = DeviceCommandBaseOptions & - Pick & { - kind: 'snapshot'; - out?: string; - }; - -/** - * Opt-in (#1101): after the action, wait for the UI to go quiet and return the - * settled diff vs the pre-action tree (`settle` on the result) in the same - * response. Best-effort — never fails the action. `settleQuietMs` tunes the - * quiet window (default 500ms); `timeoutMs` bounds the settle wait (default - * 10s) when `settle` is true. A bare `timeoutMs` without `settle` is ignored - * for compatibility; `settleQuietMs` still requires `settle`. - */ -type SettleCommandOptions = { - settle?: boolean; - settleQuietMs?: number; - timeoutMs?: number; -}; - -export type ClickOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - InteractionTarget & - RepeatedPressOptions & - SettleCommandOptions & { - button?: ClickButton; - /** - * Opt-in (#1047): return cheap post-action evidence (AX digest, node counts, - * changedFromBefore) in the response instead of requiring a follow-up - * snapshot to confirm the action had an effect. - */ - verify?: boolean; - }; - -export type PressOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - InteractionTarget & - RepeatedPressOptions & - SettleCommandOptions & { - verify?: boolean; - }; - -export type LongPressOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - InteractionTarget & - SettleCommandOptions & { - durationMs?: number; - }; - -export type SwipeOptions = DeviceCommandBaseOptions & { - from: { x: number; y: number }; - to: { x: number; y: number }; - count?: number; - pauseMs?: number; - pattern?: SwipePattern; -}; - -export type PanOptions = DeviceCommandBaseOptions & { - x: number; - y: number; - dx: number; - dy: number; - pointerCount?: GesturePointerCount; - durationMs?: number; -}; - -export type FlingOptions = DeviceCommandBaseOptions & { - direction: ScrollDirection; - x: number; - y: number; - distance?: number; -}; - -export type SwipeGestureOptions = DeviceCommandBaseOptions & { - preset: SwipePreset; -}; - -export type FocusOptions = DeviceCommandBaseOptions & { - x: number; - y: number; -}; - -export type TypeTextOptions = DeviceCommandBaseOptions & { - text: string; - delayMs?: number; -}; - -export type FillOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - InteractionTarget & - SettleCommandOptions & { - text: string; - delayMs?: number; - /** Publish this fill value as `${VAR}` when script recording is armed. */ - recordAs?: string; - verify?: boolean; - }; - -export type PinchOptions = DeviceCommandBaseOptions & { - scale: number; - x?: number; - y?: number; -}; - -export type RotateGestureOptions = DeviceCommandBaseOptions & { - degrees: number; - x?: number; - y?: number; -}; - -export type TransformGestureOptions = DeviceCommandBaseOptions & TransformGestureParams; - -/** - * #1271 stage 2 (ADR 0012 amendment): `get`/`is`/`find` are observation-only - * and excluded from a repair-armed heal by default. `record` forces this - * action through (the corrective-read case); `noRecord` continues to opt the - * action out entirely. Mutually exclusive. - */ -type RecordControlOptions = { - noRecord?: boolean; - record?: boolean; -}; - -export type GetOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - ElementTarget & - RecordControlOptions & { - format: 'text' | 'attrs'; - }; - -type IsTextPredicateOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - RecordControlOptions & { - predicate: 'text'; - selector: string; - value: string; - }; - -type IsStatePredicateOptions = DeviceCommandBaseOptions & - SelectorSnapshotCommandOptions & - RecordControlOptions & { - predicate: 'visible' | 'hidden' | 'exists' | 'editable' | 'selected' | 'focused'; - selector: string; - value?: never; - }; - -export type IsOptions = IsTextPredicateOptions | IsStatePredicateOptions; - -type FindBaseOptions = DeviceCommandBaseOptions & - FindSnapshotCommandOptions & - RecordControlOptions & { - locator?: FindLocator; - query: string; - first?: boolean; - last?: boolean; - }; - -export type FindOptions = - | (FindBaseOptions & { action?: 'click' | 'focus' | 'exists' | 'getText' | 'getAttrs' }) - | (FindBaseOptions & { action: 'wait'; timeoutMs?: number }) - | (FindBaseOptions & { action: 'fill' | 'type'; value: string }); - -export type ReplayRunOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - path: string; - runtime?: SessionRuntimeHints; - /** - * @deprecated ADR 0012 migration step 6: `--update` no longer rewrites - * the script. Accepted for backward compatibility; every divergence - * already carries ranked selector suggestions regardless of this flag. - */ - update?: boolean; - /** @deprecated Use backend: 'maestro'. */ - maestro?: boolean; - backend?: string; - env?: string[]; - timeoutMs?: number; - /** - * ADR 0012 decision 4 / migration step 5: resume at this 1-based plan - * step, skipping `1..resumeFrom-1` without executing them. Requires - * `resumePlanDigest` from the divergence report that reported this - * step as the failure. `replay` only — `test` has no resume fields. - */ - resumeFrom?: number; - /** The `resume.planDigest` from the divergence report `resumeFrom` came from. */ - resumePlanDigest?: string; - /** - * ADR 0012 decision 6, R1/R6: arms agent-supervised re-record repair - * from this replay attempt onward. Optional string value is the healed - * `.ad`'s output path; absent one, it defaults to the `` sibling - * `.healed.ad` when the repair ends with `close --save-script`. - */ - saveScript?: boolean | string; - /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ - force?: boolean; - }; - -export type ReplayTestOptions = AgentDeviceRequestOverrides & - AgentDeviceSelectionOptions & { - paths: string[]; - runtime?: SessionRuntimeHints; - update?: boolean; - /** @deprecated Use backend: 'maestro'. */ - maestro?: boolean; - backend?: string; - env?: string[]; - failFast?: boolean; - timeoutMs?: number; - retries?: number; - recordVideo?: boolean; - artifactsDir?: string; - /** @deprecated Use the CLI --reporter junit: or --report-junit . */ - reportJunit?: string; - shardAll?: number; - shardSplit?: number; - }; - -export type BatchStep = { - command: string; - input: Record; - runtime?: SessionRuntimeHints; -}; - -export type BatchRunOptions = AgentDeviceRequestOverrides & { - steps: BatchStep[]; - onError?: 'stop'; - maxSteps?: number; - out?: string; -}; - -export type PerfOptions = DeviceCommandBaseOptions & { - area?: PerfArea; - subject?: PerfSubject; - action?: PerfAction; - kind?: PerfKind; - template?: string; - out?: string; - tracePath?: string; -}; - -export type LogsOptions = AgentDeviceRequestOverrides & { - action?: LogAction; - message?: string; - restart?: boolean; -}; - -export type EventsOptions = AgentDeviceRequestOverrides & { - cursor?: string; - limit?: number; -}; - -export type NetworkOptions = AgentDeviceRequestOverrides & { - action?: 'dump' | 'log'; - limit?: number; - include?: NetworkIncludeMode; -}; - -export type AudioOptions = AgentDeviceRequestOverrides & { - action?: 'probe'; - probeAction?: 'start' | 'status' | 'stop'; - durationMs?: number; - bucketMs?: number; -}; - -export type RecordOptions = AgentDeviceRequestOverrides & { - action: 'start' | 'stop'; - path?: string; - fps?: number; - maxSize?: number; - quality?: RecordingExportQuality; - hideTouches?: boolean; - recordingScope?: RecordingScope; -}; - -export type TraceOptions = AgentDeviceRequestOverrides & { - action: 'start' | 'stop'; - path?: string; -}; - -export type PermissionTarget = - | 'camera' - | 'microphone' - | 'photos' - | 'contacts' - | 'contacts-limited' - | 'notifications' - | 'calendar' - | 'location' - | 'location-always' - | 'media-library' - | 'motion' - | 'reminders' - | 'siri' - | 'accessibility' - | 'screen-recording' - | 'input-monitoring'; - -export type SettingsUpdateOptions = - | (DeviceCommandBaseOptions & { - setting: 'clear-app-state'; - state: 'clear'; - app?: string; - }) - | (DeviceCommandBaseOptions & { - setting: 'wifi' | 'airplane' | 'location'; - state: 'on' | 'off'; - }) - | (DeviceCommandBaseOptions & { - setting: 'location'; - state: 'set'; - latitude: number; - longitude: number; - }) - | (DeviceCommandBaseOptions & { - setting: 'animations'; - state: 'on' | 'off'; - }) - | (DeviceCommandBaseOptions & { - setting: 'appearance'; - state: 'light' | 'dark' | 'toggle'; - }) - | (DeviceCommandBaseOptions & { - setting: 'faceid' | 'touchid'; - state: 'match' | 'nonmatch' | 'enroll' | 'unenroll'; - }) - | (DeviceCommandBaseOptions & { - setting: 'fingerprint'; - state: 'match' | 'nonmatch'; - }) - | (DeviceCommandBaseOptions & { - setting: 'permission'; - state: 'grant' | 'deny' | 'reset'; - permission: PermissionTarget; - mode?: 'full' | 'limited'; - }); - -type CommandExecutionOptions = Partial & { - positionals?: string[]; - kind?: string; - out?: string; - artifact?: string; - dsym?: string; - searchPath?: string; - interactiveOnly?: boolean; - depth?: number; - scope?: string; - raw?: boolean; - forceFull?: boolean; - count?: number; - fps?: number; - maxSize?: number; - recordingScope?: RecordingScope; - quality?: RecordingExportQuality; - hideTouches?: boolean; - intervalMs?: number; - delayMs?: number; - durationMs?: number; - holdMs?: number; - jitterPx?: number; - pixels?: number; - doubleTap?: boolean; - verify?: boolean; - settle?: boolean; - settleQuietMs?: number; - clickButton?: ClickButton; - pauseMs?: number; - pattern?: SwipePattern; - headless?: boolean; - restart?: boolean; - replayUpdate?: boolean; - replayBackend?: string; - replayEnv?: string[]; - replayShellEnv?: Record; - replayFrom?: number; - replayPlanDigest?: string; - failFast?: boolean; - timeoutMs?: number; - retries?: number; - recordVideo?: boolean; - artifactsDir?: string; - shardAll?: number; - shardSplit?: number; - findFirst?: boolean; - findLast?: boolean; - networkInclude?: NetworkIncludeMode; - batchOnError?: 'stop'; - batchMaxSteps?: number; - batchSteps?: DaemonBatchStep[]; -}; - -export type InternalRequestOptions = AgentDeviceClientConfig & - AgentDeviceSelectionOptions & - CommandExecutionOptions & { - runtime?: SessionRuntimeHints; - overlayRefs?: boolean; - surface?: SessionSurface; - activity?: string; - launchConsole?: string; - launchArgs?: string[]; - relaunch?: boolean; - shutdown?: boolean; - saveScript?: boolean | string; - /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ - force?: boolean; - deviceHub?: boolean; - testIme?: boolean; - noRecord?: boolean; - /** Fill-only script parameter name used to publish `${VAR}` instead of literal text. */ - recordAs?: string; - /** #1271 stage 2: force-record this action; mutually exclusive with `noRecord`. */ - record?: boolean; - backMode?: BackMode; - metroHost?: string; - metroPort?: number; - bundleUrl?: string; - launchUrl?: string; - appsFilter?: AppsFilter; - installSource?: DaemonInstallSource; - retainMaterializedPaths?: boolean; - materializedPathRetentionMs?: number; - materializationId?: string; - leaseTtlMs?: number; - provider?: string; - providerSessionId?: string; - }; - -export type CommandRequestResult = DaemonResponseData; - -export type ScrollOptions = DeviceCommandBaseOptions & { - direction: ScrollInputDirection; - amount?: number; - pixels?: number; - durationMs?: number; -}; -export type MetroPrepareResult = PrepareMetroRuntimeResult; -export type MetroReloadResult = ReloadMetroResult; diff --git a/src/contracts/client-app.ts b/src/contracts/client-app.ts new file mode 100644 index 000000000..838d17dcd --- /dev/null +++ b/src/contracts/client-app.ts @@ -0,0 +1,137 @@ +// The public API vocabulary for app install, deploy, open, close and inventory. + +import type { AppsFilter } from './app-inventory.ts'; +import type { JsonObject } from './json.ts'; +import type { SessionSurface } from './session-surface.ts'; +import type { TargetShutdownResult } from './target-shutdown-contract.ts'; +import type { DaemonInstallSource, SessionRuntimeHints } from '../kernel/contracts.ts'; +import type { PublicPlatform } from '../kernel/device.ts'; +import type { + AgentDeviceIdentifiers, + AgentDeviceRequestOverrides, + AgentDeviceSelectionOptions, + DeviceCommandBaseOptions, +} from './client-connection.ts'; +import type { AgentDeviceSessionDevice, StartupPerfSample } from './client-device-view.ts'; + +export type AppInstallOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + app?: string; + appPath: string; + }; + +export type AppDeployOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + app: string; + appPath: string; + }; + +export type AppDeployResult = { + app: string; + appPath: string; + platform: PublicPlatform; + appId?: string; + bundleId?: string; + package?: string; + identifiers: AgentDeviceIdentifiers; +}; + +export type AppOpenOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + app?: string; + url?: string; + surface?: SessionSurface; + activity?: string; + launchConsole?: string; + launchArgs?: string[]; + relaunch?: boolean; + saveScript?: boolean | string; + /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ + force?: boolean; + deviceHub?: boolean; + testIme?: boolean; + noRecord?: boolean; + runtime?: SessionRuntimeHints; + }; + +export type AppOpenResult = { + session: string; + warnings?: string[]; + sessionStateDir?: string; + runnerLogPath?: string; + requestLogPath?: string; + eventLogPath?: string; + appName?: string; + appBundleId?: string; + appId?: string; + startup?: StartupPerfSample; + runtime?: SessionRuntimeHints; + device?: AgentDeviceSessionDevice; + identifiers: AgentDeviceIdentifiers; +}; + +export type AppCloseOptions = AgentDeviceRequestOverrides & { + app?: string; + shutdown?: boolean; + saveScript?: boolean | string; + /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ + force?: boolean; +}; + +export type AppCloseResult = { + session: string; + closedApp?: string; + shutdown?: TargetShutdownResult; + /** + * #1258: absolute path of the committed session/healed script when this close + * published one (`close --save-script`, or a repair-armed session's finalize) + * — so a client that requested publication learns where the file landed. + */ + savedScript?: string; + identifiers: AgentDeviceIdentifiers; +}; + +export type AppInstallFromSourceOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + source: DaemonInstallSource; + retainPaths?: boolean; + retentionMs?: number; + }; + +export type AppInstallFromSourceResult = { + appName?: string; + appId?: string; + bundleId?: string; + packageName?: string; + launchTarget: string; + installablePath?: string; + archivePath?: string; + materializationId?: string; + materializationExpiresAt?: string; + identifiers: AgentDeviceIdentifiers; +}; + +export type AppListOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + appsFilter?: AppsFilter; + }; + +export type AppPushOptions = DeviceCommandBaseOptions & { + app: string; + payload: string | JsonObject; +}; + +export type AppTriggerEventOptions = DeviceCommandBaseOptions & { + event: string; + payload?: JsonObject; +}; + +export type MaterializationReleaseOptions = AgentDeviceRequestOverrides & { + materializationId: string; +}; + +export type MaterializationReleaseResult = { + released: boolean; + materializationId: string; + identifiers: AgentDeviceIdentifiers; +}; diff --git a/src/contracts/client-capture.ts b/src/contracts/client-capture.ts new file mode 100644 index 000000000..bdd871268 --- /dev/null +++ b/src/contracts/client-capture.ts @@ -0,0 +1,82 @@ +// The public API vocabulary for snapshot, screenshot and diff capture. + +import type { SessionSurface } from './session-surface.ts'; +import type { PublicSnapshotCaptureAnnotations } from './snapshot-capture-annotations.ts'; +import type { SnapshotDiagnosticsSummary } from './snapshot-diagnostics.ts'; +import type { SnapshotNode, SnapshotUnchanged, SnapshotVisibility } from '../kernel/snapshot.ts'; +import type { ScreenshotResultData } from '../utils/screenshot-result.ts'; +import type { + AgentDeviceIdentifiers, + AgentDeviceRequestOverrides, + AgentDeviceSelectionOptions, + DeviceCommandBaseOptions, +} from './client-connection.ts'; + +export type CaptureSnapshotOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + interactiveOnly?: boolean; + depth?: number; + scope?: string; + raw?: boolean; + forceFull?: boolean; + timeoutMs?: number; + /** + * #1271 stage 2 (ADR 0012 amendment): `snapshot` is observation-only and + * excluded from a repair-armed heal by default; `record` forces it + * through. Mutually exclusive with `noRecord`. + */ + noRecord?: boolean; + record?: boolean; + }; + +export type CaptureSnapshotResult = { + nodes: SnapshotNode[]; + truncated: boolean; + appName?: string; + appBundleId?: string; + visibility?: SnapshotVisibility; + unchanged?: SnapshotUnchanged; + snapshotDiagnostics?: SnapshotDiagnosticsSummary; + identifiers: AgentDeviceIdentifiers; + /** + * ADR 0014: the response-level ref-frame epoch the plain node refs were minted + * from. A ref-issuing snapshot carries it ONCE (nodes stay plain `@e12` for the + * token budget); pair a ref with it (`@e12~s`) before a mutation. + */ + refsGeneration?: number; + /** + * Digest response view only: a capped list of `{ ref, label? }` pairs taken + * from the full `nodes` tree so the MCP layer can still pin refs when the + * default-level `nodes` payload is intentionally omitted. + */ + refs?: Array<{ ref: string; label?: string }>; +} & PublicSnapshotCaptureAnnotations; + +export type CaptureScreenshotOptions = AgentDeviceRequestOverrides & { + path?: string; + overlayRefs?: boolean; + pixelDensity?: number; + fullscreen?: boolean; + maxSize?: number; + stabilize?: boolean; + normalizeStatusBar?: boolean; + surface?: SessionSurface; +}; + +export type CaptureScreenshotResult = ScreenshotResultData & { + path: string; + identifiers: AgentDeviceIdentifiers; +}; + +export type CaptureDiffOptions = DeviceCommandBaseOptions & + Pick & { + kind: 'snapshot'; + out?: string; + }; + +export type SelectorSnapshotCommandOptions = Pick< + CaptureSnapshotOptions, + 'depth' | 'scope' | 'raw' +>; + +export type FindSnapshotCommandOptions = Pick; diff --git a/src/contracts/client-connection.ts b/src/contracts/client-connection.ts new file mode 100644 index 000000000..0675f152a --- /dev/null +++ b/src/contracts/client-connection.ts @@ -0,0 +1,100 @@ +// The public API vocabulary for how a client reaches a daemon and selects a device. + +import type { + DaemonLockPolicy, + DaemonRequest, + DaemonResponse, + LeaseBackend, + ResponseLevel, + SessionIsolationMode, + SessionRuntimeHints, +} from '../kernel/contracts.ts'; +import type { DeviceTarget, PlatformSelector } from '../kernel/device.ts'; +import type { + CloudProviderProfileFields, + RemoteConnectionProfileFields, +} from './remote-config-fields.ts'; + +export type AgentDeviceDaemonTransport = ( + req: Omit, +) => Promise; + +export type AgentDeviceClientConfig = RemoteConnectionProfileFields & + CloudProviderProfileFields & { + session?: string; + lockPolicy?: DaemonLockPolicy; + lockPlatform?: PlatformSelector; + requestId?: string; + sessionIsolation?: SessionIsolationMode; + leaseBackend?: LeaseBackend; + leaseTtlMs?: number; + runtime?: SessionRuntimeHints; + cwd?: string; + debug?: boolean; + cost?: boolean; + responseLevel?: ResponseLevel; + iosXctestrunFile?: string; + iosXctestDerivedDataPath?: string; + iosXctestEnvDir?: string; + }; + +export type AgentDeviceRequestOverrides = Pick< + AgentDeviceClientConfig, + | 'session' + | 'lockPolicy' + | 'lockPlatform' + | 'requestId' + | 'daemonBaseUrl' + | 'daemonAuthToken' + | 'daemonTransport' + | 'daemonServerMode' + | 'tenant' + | 'sessionIsolation' + | 'runId' + | 'leaseId' + | 'leaseBackend' + | 'leaseProvider' + | 'deviceKey' + | 'clientId' + | 'providerApp' + | 'providerOsVersion' + | 'providerProject' + | 'providerBuild' + | 'providerSessionName' + | 'awsProjectArn' + | 'awsDeviceArn' + | 'awsAppArn' + | 'awsRegion' + | 'awsInteractionMode' + | 'leaseTtlMs' + | 'cwd' + | 'debug' + | 'cost' + | 'responseLevel' + | 'iosXctestrunFile' + | 'iosXctestDerivedDataPath' + | 'iosXctestEnvDir' +>; + +export type AgentDeviceIdentifiers = { + session?: string; + deviceId?: string; + deviceName?: string; + udid?: string; + serial?: string; + appId?: string; + appBundleId?: string; + package?: string; +}; + +export type AgentDeviceSelectionOptions = { + platform?: PlatformSelector; + target?: DeviceTarget; + device?: string; + udid?: string; + serial?: string; + iosSimulatorDeviceSet?: string; + androidDeviceAllowlist?: string; +}; + +export type DeviceCommandBaseOptions = AgentDeviceRequestOverrides & AgentDeviceSelectionOptions; diff --git a/src/contracts/client-device-view.ts b/src/contracts/client-device-view.ts new file mode 100644 index 000000000..9d55ed5bb --- /dev/null +++ b/src/contracts/client-device-view.ts @@ -0,0 +1,79 @@ +// The public API vocabulary for what a device and an open session look like to a client. + +import type { AppleOS, DeviceKind, DeviceTarget, PublicPlatform } from '../kernel/device.ts'; +import type { AgentDeviceIdentifiers, DeviceCommandBaseOptions } from './client-connection.ts'; + +export type AgentDeviceDevice = { + platform: PublicPlatform; + target: DeviceTarget; + kind: DeviceKind; + id: string; + name: string; + booted?: boolean; + /** + * Additive Apple-OS discriminant (iPhone/iPad/tvOS/visionOS/macOS). Present only for + * Apple devices; `platform` still carries the leaf (`ios`/`macos`). + */ + appleOs?: AppleOS; + identifiers: AgentDeviceIdentifiers; + ios?: { + udid: string; + }; + android?: { + serial: string; + }; + vega?: { + serial: string; + }; +}; + +export type AgentDeviceCapabilitiesResult = { + device: AgentDeviceDevice; + availableCommands: string[]; +}; + +export type AgentDeviceSessionDevice = { + platform: PublicPlatform; + target: DeviceTarget; + id: string; + name: string; + /** + * Additive Apple-OS discriminant (iPhone/iPad/tvOS/visionOS/macOS). Present only for + * Apple devices; `platform` still carries the leaf (`ios`/`macos`). + */ + appleOs?: AppleOS; + identifiers: AgentDeviceIdentifiers; + ios?: { + udid: string; + simulatorSetPath?: string | null; + }; + android?: { + serial: string; + }; + vega?: { + serial: string; + }; +}; + +export type AgentDeviceSession = { + name: string; + createdAt: number; + sessionStateDir?: string; + runnerLogPath?: string; + device: AgentDeviceSessionDevice; + identifiers: AgentDeviceIdentifiers; +}; + +export type StartupPerfSample = { + durationMs: number; + measuredAt: string; + method: string; + appTarget?: string; + appBundleId?: string; +}; + +export type DeviceBootOptions = DeviceCommandBaseOptions & { + headless?: boolean; +}; + +export type DeviceShutdownOptions = DeviceCommandBaseOptions; diff --git a/src/contracts/client-gesture.ts b/src/contracts/client-gesture.ts new file mode 100644 index 000000000..998f54714 --- /dev/null +++ b/src/contracts/client-gesture.ts @@ -0,0 +1,135 @@ +// The public API vocabulary for the gesture and text-entry commands. + +import type { ClickButton } from './click-button.ts'; +import type { GesturePointerCount } from './gesture-plan.ts'; +import type { + ScrollDirection, + ScrollInputDirection, + SwipePattern, + SwipePreset, + TransformGestureParams, +} from './scroll-gesture.ts'; +import type { SelectorSnapshotCommandOptions } from './client-capture.ts'; +import type { DeviceCommandBaseOptions } from './client-connection.ts'; +import type { InteractionTarget } from './client-target.ts'; + +export type RepeatedPressOptions = { + count?: number; + intervalMs?: number; + holdMs?: number; + jitterPx?: number; + doubleTap?: boolean; +}; + +/** + * Opt-in (#1101): after the action, wait for the UI to go quiet and return the + * settled diff vs the pre-action tree (`settle` on the result) in the same + * response. Best-effort — never fails the action. `settleQuietMs` tunes the + * quiet window (default 500ms); `timeoutMs` bounds the settle wait (default + * 10s) when `settle` is true. A bare `timeoutMs` without `settle` is ignored + * for compatibility; `settleQuietMs` still requires `settle`. + */ +export type SettleCommandOptions = { + settle?: boolean; + settleQuietMs?: number; + timeoutMs?: number; +}; + +export type ClickOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + InteractionTarget & + RepeatedPressOptions & + SettleCommandOptions & { + button?: ClickButton; + /** + * Opt-in (#1047): return cheap post-action evidence (AX digest, node counts, + * changedFromBefore) in the response instead of requiring a follow-up + * snapshot to confirm the action had an effect. + */ + verify?: boolean; + }; + +export type PressOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + InteractionTarget & + RepeatedPressOptions & + SettleCommandOptions & { + verify?: boolean; + }; + +export type LongPressOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + InteractionTarget & + SettleCommandOptions & { + durationMs?: number; + }; + +export type SwipeOptions = DeviceCommandBaseOptions & { + from: { x: number; y: number }; + to: { x: number; y: number }; + count?: number; + pauseMs?: number; + pattern?: SwipePattern; +}; + +export type PanOptions = DeviceCommandBaseOptions & { + x: number; + y: number; + dx: number; + dy: number; + pointerCount?: GesturePointerCount; + durationMs?: number; +}; + +export type FlingOptions = DeviceCommandBaseOptions & { + direction: ScrollDirection; + x: number; + y: number; + distance?: number; +}; + +export type SwipeGestureOptions = DeviceCommandBaseOptions & { + preset: SwipePreset; +}; + +export type FocusOptions = DeviceCommandBaseOptions & { + x: number; + y: number; +}; + +export type TypeTextOptions = DeviceCommandBaseOptions & { + text: string; + delayMs?: number; +}; + +export type FillOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + InteractionTarget & + SettleCommandOptions & { + text: string; + delayMs?: number; + /** Publish this fill value as `${VAR}` when script recording is armed. */ + recordAs?: string; + verify?: boolean; + }; + +export type PinchOptions = DeviceCommandBaseOptions & { + scale: number; + x?: number; + y?: number; +}; + +export type RotateGestureOptions = DeviceCommandBaseOptions & { + degrees: number; + x?: number; + y?: number; +}; + +export type TransformGestureOptions = DeviceCommandBaseOptions & TransformGestureParams; + +export type ScrollOptions = DeviceCommandBaseOptions & { + direction: ScrollInputDirection; + amount?: number; + pixels?: number; + durationMs?: number; +}; diff --git a/src/contracts/client-lease.ts b/src/contracts/client-lease.ts new file mode 100644 index 000000000..019d6db1e --- /dev/null +++ b/src/contracts/client-lease.ts @@ -0,0 +1,51 @@ +// The public API vocabulary for device lease allocation and cloud artifacts. + +import type { LeaseBackend } from '../kernel/contracts.ts'; +import type { + AgentDeviceRequestOverrides, + AgentDeviceSelectionOptions, +} from './client-connection.ts'; + +export type Lease = { + leaseId: string; + tenantId: string; + runId: string; + backend: LeaseBackend; + leaseProvider?: string; + deviceKey?: string; + clientId?: string; + createdAt?: number; + heartbeatAt?: number; + expiresAt?: number; +}; + +export type LeaseOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + ttlMs?: number; + }; + +export type LeaseAllocateOptions = LeaseOptions & { + tenant: string; + runId: string; + leaseBackend?: LeaseBackend; + leaseProvider?: string; + provider?: string; + deviceKey?: string; + clientId?: string; +}; + +export type LeaseScopedOptions = LeaseOptions & { + tenant?: string; + runId?: string; + leaseId: string; + leaseBackend?: LeaseBackend; + leaseProvider?: string; + provider?: string; + deviceKey?: string; + clientId?: string; +}; + +export type CloudArtifactsOptions = AgentDeviceRequestOverrides & { + provider?: string; + providerSessionId?: string; +}; diff --git a/src/contracts/client-observability.ts b/src/contracts/client-observability.ts new file mode 100644 index 000000000..cc4c8f6e2 --- /dev/null +++ b/src/contracts/client-observability.ts @@ -0,0 +1,57 @@ +// The public API vocabulary for perf, logs, events, network, audio and recording capture. + +import type { LogAction } from './logs.ts'; +import type { PerfAction, PerfArea, PerfKind, PerfSubject } from './perf.ts'; +import type { RecordingExportQuality } from './recording-export-quality.ts'; +import type { RecordingScope } from './recording-scope.ts'; +import type { NetworkIncludeMode } from '../kernel/contracts.ts'; +import type { AgentDeviceRequestOverrides, DeviceCommandBaseOptions } from './client-connection.ts'; + +export type PerfOptions = DeviceCommandBaseOptions & { + area?: PerfArea; + subject?: PerfSubject; + action?: PerfAction; + kind?: PerfKind; + template?: string; + out?: string; + tracePath?: string; +}; + +export type LogsOptions = AgentDeviceRequestOverrides & { + action?: LogAction; + message?: string; + restart?: boolean; +}; + +export type EventsOptions = AgentDeviceRequestOverrides & { + cursor?: string; + limit?: number; +}; + +export type NetworkOptions = AgentDeviceRequestOverrides & { + action?: 'dump' | 'log'; + limit?: number; + include?: NetworkIncludeMode; +}; + +export type AudioOptions = AgentDeviceRequestOverrides & { + action?: 'probe'; + probeAction?: 'start' | 'status' | 'stop'; + durationMs?: number; + bucketMs?: number; +}; + +export type RecordOptions = AgentDeviceRequestOverrides & { + action: 'start' | 'stop'; + path?: string; + fps?: number; + maxSize?: number; + quality?: RecordingExportQuality; + hideTouches?: boolean; + recordingScope?: RecordingScope; +}; + +export type TraceOptions = AgentDeviceRequestOverrides & { + action: 'start' | 'stop'; + path?: string; +}; diff --git a/src/contracts/client-replay.ts b/src/contracts/client-replay.ts new file mode 100644 index 000000000..eaf5b1f16 --- /dev/null +++ b/src/contracts/client-replay.ts @@ -0,0 +1,75 @@ +// The public API vocabulary for replay and batch execution. + +import type { SessionRuntimeHints } from '../kernel/contracts.ts'; +import type { + AgentDeviceRequestOverrides, + AgentDeviceSelectionOptions, +} from './client-connection.ts'; + +export type ReplayRunOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + path: string; + runtime?: SessionRuntimeHints; + /** + * @deprecated ADR 0012 migration step 6: `--update` no longer rewrites + * the script. Accepted for backward compatibility; every divergence + * already carries ranked selector suggestions regardless of this flag. + */ + update?: boolean; + /** @deprecated Use backend: 'maestro'. */ + maestro?: boolean; + backend?: string; + env?: string[]; + timeoutMs?: number; + /** + * ADR 0012 decision 4 / migration step 5: resume at this 1-based plan + * step, skipping `1..resumeFrom-1` without executing them. Requires + * `resumePlanDigest` from the divergence report that reported this + * step as the failure. `replay` only — `test` has no resume fields. + */ + resumeFrom?: number; + /** The `resume.planDigest` from the divergence report `resumeFrom` came from. */ + resumePlanDigest?: string; + /** + * ADR 0012 decision 6, R1/R6: arms agent-supervised re-record repair + * from this replay attempt onward. Optional string value is the healed + * `.ad`'s output path; absent one, it defaults to the `` sibling + * `.healed.ad` when the repair ends with `close --save-script`. + */ + saveScript?: boolean | string; + /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ + force?: boolean; + }; + +export type ReplayTestOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + paths: string[]; + runtime?: SessionRuntimeHints; + update?: boolean; + /** @deprecated Use backend: 'maestro'. */ + maestro?: boolean; + backend?: string; + env?: string[]; + failFast?: boolean; + timeoutMs?: number; + retries?: number; + recordVideo?: boolean; + artifactsDir?: string; + /** @deprecated Use the CLI --reporter junit: or --report-junit . */ + reportJunit?: string; + shardAll?: number; + shardSplit?: number; + }; + +export type BatchStep = { + command: string; + input: Record; + runtime?: SessionRuntimeHints; +}; + +export type BatchRunOptions = AgentDeviceRequestOverrides & { + steps: BatchStep[]; + onError?: 'stop'; + maxSteps?: number; + out?: string; +}; diff --git a/src/contracts/client-request.ts b/src/contracts/client-request.ts new file mode 100644 index 000000000..85be9ec15 --- /dev/null +++ b/src/contracts/client-request.ts @@ -0,0 +1,110 @@ +// The public API vocabulary for the internal request envelope every client call is projected into. + +import type { AppsFilter } from './app-inventory.ts'; +import type { BackMode } from './back-mode.ts'; +import type { ClickButton } from './click-button.ts'; +import type { RecordingExportQuality } from './recording-export-quality.ts'; +import type { RecordingScope } from './recording-scope.ts'; +import type { ScreenshotRequestFlags } from './screenshot.ts'; +import type { SwipePattern } from './scroll-gesture.ts'; +import type { SessionSurface } from './session-surface.ts'; +import type { + DaemonInstallSource, + DaemonResponseData, + NetworkIncludeMode, + SessionRuntimeHints, +} from '../kernel/contracts.ts'; +import type { DaemonBatchStep } from './batch-step.ts'; +import type { AgentDeviceClientConfig, AgentDeviceSelectionOptions } from './client-connection.ts'; + +export type CommandExecutionOptions = Partial & { + positionals?: string[]; + kind?: string; + out?: string; + artifact?: string; + dsym?: string; + searchPath?: string; + interactiveOnly?: boolean; + depth?: number; + scope?: string; + raw?: boolean; + forceFull?: boolean; + count?: number; + fps?: number; + maxSize?: number; + recordingScope?: RecordingScope; + quality?: RecordingExportQuality; + hideTouches?: boolean; + intervalMs?: number; + delayMs?: number; + durationMs?: number; + holdMs?: number; + jitterPx?: number; + pixels?: number; + doubleTap?: boolean; + verify?: boolean; + settle?: boolean; + settleQuietMs?: number; + clickButton?: ClickButton; + pauseMs?: number; + pattern?: SwipePattern; + headless?: boolean; + restart?: boolean; + replayUpdate?: boolean; + replayBackend?: string; + replayEnv?: string[]; + replayShellEnv?: Record; + replayFrom?: number; + replayPlanDigest?: string; + failFast?: boolean; + timeoutMs?: number; + retries?: number; + recordVideo?: boolean; + artifactsDir?: string; + shardAll?: number; + shardSplit?: number; + findFirst?: boolean; + findLast?: boolean; + networkInclude?: NetworkIncludeMode; + batchOnError?: 'stop'; + batchMaxSteps?: number; + batchSteps?: DaemonBatchStep[]; +}; + +export type InternalRequestOptions = AgentDeviceClientConfig & + AgentDeviceSelectionOptions & + CommandExecutionOptions & { + runtime?: SessionRuntimeHints; + overlayRefs?: boolean; + surface?: SessionSurface; + activity?: string; + launchConsole?: string; + launchArgs?: string[]; + relaunch?: boolean; + shutdown?: boolean; + saveScript?: boolean | string; + /** #1258: overwrite an existing --save-script target instead of refusing. Alias: --overwrite. */ + force?: boolean; + deviceHub?: boolean; + testIme?: boolean; + noRecord?: boolean; + /** Fill-only script parameter name used to publish `${VAR}` instead of literal text. */ + recordAs?: string; + /** #1271 stage 2: force-record this action; mutually exclusive with `noRecord`. */ + record?: boolean; + backMode?: BackMode; + metroHost?: string; + metroPort?: number; + bundleUrl?: string; + launchUrl?: string; + appsFilter?: AppsFilter; + installSource?: DaemonInstallSource; + retainMaterializedPaths?: boolean; + materializedPathRetentionMs?: number; + materializationId?: string; + leaseTtlMs?: number; + provider?: string; + providerSessionId?: string; + }; + +export type CommandRequestResult = DaemonResponseData; diff --git a/src/contracts/client-selector-read.ts b/src/contracts/client-selector-read.ts new file mode 100644 index 000000000..8af0cbb03 --- /dev/null +++ b/src/contracts/client-selector-read.ts @@ -0,0 +1,59 @@ +// The public API vocabulary for the read commands that resolve a selector (get / is / find). + +import type { FindLocator } from '../selectors/find.ts'; +import type { + FindSnapshotCommandOptions, + SelectorSnapshotCommandOptions, +} from './client-capture.ts'; +import type { DeviceCommandBaseOptions } from './client-connection.ts'; +import type { ElementTarget } from './client-target.ts'; + +/** + * #1271 stage 2 (ADR 0012 amendment): `get`/`is`/`find` are observation-only + * and excluded from a repair-armed heal by default. `record` forces this + * action through (the corrective-read case); `noRecord` continues to opt the + * action out entirely. Mutually exclusive. + */ +export type RecordControlOptions = { + noRecord?: boolean; + record?: boolean; +}; + +export type GetOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + ElementTarget & + RecordControlOptions & { + format: 'text' | 'attrs'; + }; + +export type IsTextPredicateOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + RecordControlOptions & { + predicate: 'text'; + selector: string; + value: string; + }; + +export type IsStatePredicateOptions = DeviceCommandBaseOptions & + SelectorSnapshotCommandOptions & + RecordControlOptions & { + predicate: 'visible' | 'hidden' | 'exists' | 'editable' | 'selected' | 'focused'; + selector: string; + value?: never; + }; + +export type IsOptions = IsTextPredicateOptions | IsStatePredicateOptions; + +export type FindBaseOptions = DeviceCommandBaseOptions & + FindSnapshotCommandOptions & + RecordControlOptions & { + locator?: FindLocator; + query: string; + first?: boolean; + last?: boolean; + }; + +export type FindOptions = + | (FindBaseOptions & { action?: 'click' | 'focus' | 'exists' | 'getText' | 'getAttrs' }) + | (FindBaseOptions & { action: 'wait'; timeoutMs?: number }) + | (FindBaseOptions & { action: 'fill' | 'type'; value: string }); diff --git a/src/contracts/client-session.ts b/src/contracts/client-session.ts new file mode 100644 index 000000000..22e2e91be --- /dev/null +++ b/src/contracts/client-session.ts @@ -0,0 +1,31 @@ +// The public API vocabulary for session lifecycle and script publication. + +import type { CloudProviderSessionResult } from './cloud-artifacts.ts'; +import type { TargetShutdownResult } from './target-shutdown-contract.ts'; +import type { AgentDeviceIdentifiers, AgentDeviceRequestOverrides } from './client-connection.ts'; + +export type SessionCloseResult = { + session: string; + shutdown?: TargetShutdownResult; + provider?: CloudProviderSessionResult; + /** + * #1258: absolute path of the committed session/healed script when this close + * published one (`close --save-script`, or a repair-armed session's finalize) + * — so a client that requested publication learns where the file landed. + */ + savedScript?: string; + identifiers: AgentDeviceIdentifiers; +}; + +export type SessionSaveScriptOptions = AgentDeviceRequestOverrides & { + path?: string; + /** Atomically replace an existing target instead of refusing publication. */ + force?: boolean; +}; + +export type SessionSaveScriptResult = { + session: string; + savedScript: string; + actionCount: number; + identifiers: AgentDeviceIdentifiers; +}; diff --git a/src/contracts/client-settings.ts b/src/contracts/client-settings.ts new file mode 100644 index 000000000..a21a0db1a --- /dev/null +++ b/src/contracts/client-settings.ts @@ -0,0 +1,60 @@ +// The public API vocabulary for device settings and permission grants. + +import type { DeviceCommandBaseOptions } from './client-connection.ts'; + +export type PermissionTarget = + | 'camera' + | 'microphone' + | 'photos' + | 'contacts' + | 'contacts-limited' + | 'notifications' + | 'calendar' + | 'location' + | 'location-always' + | 'media-library' + | 'motion' + | 'reminders' + | 'siri' + | 'accessibility' + | 'screen-recording' + | 'input-monitoring'; + +export type SettingsUpdateOptions = + | (DeviceCommandBaseOptions & { + setting: 'clear-app-state'; + state: 'clear'; + app?: string; + }) + | (DeviceCommandBaseOptions & { + setting: 'wifi' | 'airplane' | 'location'; + state: 'on' | 'off'; + }) + | (DeviceCommandBaseOptions & { + setting: 'location'; + state: 'set'; + latitude: number; + longitude: number; + }) + | (DeviceCommandBaseOptions & { + setting: 'animations'; + state: 'on' | 'off'; + }) + | (DeviceCommandBaseOptions & { + setting: 'appearance'; + state: 'light' | 'dark' | 'toggle'; + }) + | (DeviceCommandBaseOptions & { + setting: 'faceid' | 'touchid'; + state: 'match' | 'nonmatch' | 'enroll' | 'unenroll'; + }) + | (DeviceCommandBaseOptions & { + setting: 'fingerprint'; + state: 'match' | 'nonmatch'; + }) + | (DeviceCommandBaseOptions & { + setting: 'permission'; + state: 'grant' | 'deny' | 'reset'; + permission: PermissionTarget; + mode?: 'full' | 'limited'; + }); diff --git a/src/contracts/client-system.ts b/src/contracts/client-system.ts new file mode 100644 index 000000000..a91043a81 --- /dev/null +++ b/src/contracts/client-system.ts @@ -0,0 +1,93 @@ +// The public API vocabulary for the system and diagnostic commands (wait, alert, keyboard, clipboard, doctor…). + +import type { AlertAction } from './alert-contract.ts'; +import type { SelectorSnapshotCommandOptions } from './client-capture.ts'; +import type { DeviceCommandBaseOptions } from './client-connection.ts'; + +export type WaitCommandTarget = + | { + durationMs: number; + text?: never; + ref?: never; + selector?: never; + stable?: never; + quietMs?: never; + timeoutMs?: never; + } + | (SelectorSnapshotCommandOptions & { + text: string; + durationMs?: never; + ref?: never; + selector?: never; + stable?: never; + quietMs?: never; + timeoutMs?: number; + }) + | (SelectorSnapshotCommandOptions & { + ref: string; + durationMs?: never; + text?: never; + selector?: never; + stable?: never; + quietMs?: never; + timeoutMs?: number; + }) + | (SelectorSnapshotCommandOptions & { + selector: string; + durationMs?: never; + text?: never; + ref?: never; + stable?: never; + quietMs?: never; + timeoutMs?: number; + }) + | (SelectorSnapshotCommandOptions & { + stable: true; + durationMs?: never; + text?: never; + ref?: never; + selector?: never; + quietMs?: number; + timeoutMs?: number; + }); + +export type WaitCommandOptions = DeviceCommandBaseOptions & WaitCommandTarget; + +export type AlertCommandOptions = DeviceCommandBaseOptions & { + action?: AlertAction; + timeoutMs?: number; +}; + +export type AppStateCommandOptions = DeviceCommandBaseOptions; + +export type KeyboardCommandOptions = DeviceCommandBaseOptions & { + action?: 'status' | 'dismiss' | 'enter' | 'return'; +}; + +export type ClipboardCommandOptions = + | (DeviceCommandBaseOptions & { + action: 'read'; + }) + | (DeviceCommandBaseOptions & { + action: 'write'; + text: string; + }); + +export type ReactNativeCommandOptions = DeviceCommandBaseOptions & { + action: 'dismiss-overlay'; +}; + +export type PrepareCommandOptions = DeviceCommandBaseOptions & { + action: 'ios-runner'; + timeoutMs?: number; +}; + +export type DoctorCommandOptions = DeviceCommandBaseOptions & { + targetApp?: string; + remote?: boolean; +}; + +export type ViewportCommandOptions = DeviceCommandBaseOptions & { + width: number; + height: number; +}; diff --git a/src/contracts/client-target.ts b/src/contracts/client-target.ts new file mode 100644 index 000000000..3197a8b47 --- /dev/null +++ b/src/contracts/client-target.ts @@ -0,0 +1,29 @@ +// The public API vocabulary for how a client names the element an interaction acts on. + +export type PointTarget = { + x: number; + y: number; + ref?: never; + selector?: never; + label?: never; +}; + +export type RefTarget = { + ref: string; + label?: string; + x?: never; + y?: never; + selector?: never; +}; + +export type SelectorTarget = { + selector: string; + x?: never; + y?: never; + ref?: never; + label?: never; +}; + +export type InteractionTarget = PointTarget | RefTarget | SelectorTarget; + +export type ElementTarget = RefTarget | SelectorTarget; diff --git a/src/contracts/command-request.ts b/src/contracts/command-request.ts deleted file mode 100644 index 2dd9099de..000000000 --- a/src/contracts/command-request.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { DaemonRequest as WireRequest } from '../kernel/contracts.ts'; -import type { CommandFlags } from './command-flags.ts'; - -/** - * A dispatched command with its flags typed. - * - * Three shapes describe a request, at three ranks, and the difference between them is the point: - * - * - `kernel/contracts.ts` `DaemonRequest` — the WIRE shape. `flags?: Record`, - * because a process boundary cannot enforce a flag vocabulary. - * - this — the wire shape with `flags` narrowed to `CommandFlags`. What a command surface needs to - * reason about a request without knowing anything about the server that will run it. - * - `daemon/types.ts` `DaemonRequest` — this plus `token`/`session` required and - * `internal?: DaemonRequestInternal`, which carries `SessionState` callbacks, the admitted lease - * and the resolved session scope. Server-private, and the reason the daemon's version cannot move - * down here. - * - * `core/command-descriptor/` used to import the daemon's version to read `command`, `positionals` - * and `flags` — reaching up two ranks for three fields, and taking a dependency on the server's - * private extension to get them. - */ -export type CommandRequest = Omit & { - flags?: CommandFlags; -}; diff --git a/src/contracts/dispatched-command.ts b/src/contracts/dispatched-command.ts new file mode 100644 index 000000000..3ab79a085 --- /dev/null +++ b/src/contracts/dispatched-command.ts @@ -0,0 +1,23 @@ +import type { DaemonRequest as WireRequest } from '../kernel/contracts.ts'; +import type { CommandFlags } from './command-flags.ts'; + +/** + * The command a request dispatches: its name, its positionals, and its typed flags. + * + * Deliberately not a third name for "a request" — there are two request shapes, at two ranks: + * + * - `kernel/contracts.ts` `DaemonRequest` — the WIRE shape, with `flags?: Record`, + * because a process boundary cannot enforce a flag vocabulary. + * - `daemon/types.ts` `DaemonRequest` — the wire shape with `token`/`session` required, `flags` + * narrowed to `CommandFlags`, and `internal?: DaemonRequestInternal` carrying `SessionState` + * callbacks, the admitted lease and the resolved session scope. Server-private, which is why it + * cannot move down here. + * + * `core/command-descriptor/` classifies commands — ADR 0014 ref-frame effects, ADR 0016 recording + * effects — and needs exactly these three fields to do it. It used to import the daemon's + * `DaemonRequest`: reaching up two ranks, and depending on the server's private extension to read a + * command name. `command` and `positionals` are `Pick`ed from the wire so they cannot drift from it. + */ +export type DispatchedCommand = Pick & { + flags?: CommandFlags; +}; diff --git a/src/contracts/interactor-types.ts b/src/contracts/interactor-types.ts index 132fc347f..3931450b8 100644 --- a/src/contracts/interactor-types.ts +++ b/src/contracts/interactor-types.ts @@ -1,10 +1,10 @@ -import type { BackMode } from '../contracts/back-mode.ts'; -import type { DeviceRotation } from '../contracts/device-rotation.ts'; -import type { ScrollDirection } from '../contracts/scroll-gesture.ts'; -import type { TvRemoteButton } from '../contracts/tv-remote.ts'; -import type { GesturePlan } from '../contracts/gesture-plan-types.ts'; +import type { BackMode } from './back-mode.ts'; +import type { DeviceRotation } from './device-rotation.ts'; +import type { ScrollDirection } from './scroll-gesture.ts'; +import type { TvRemoteButton } from './tv-remote.ts'; +import type { GesturePlan } from './gesture-plan-types.ts'; import type { SettingOptions } from '../platforms/permission-utils.ts'; -import type { SessionSurface } from '../contracts/session-surface.ts'; +import type { SessionSurface } from './session-surface.ts'; import type { BackendSnapshotResult } from '../backend.ts'; import type { RunnerLogicalLeaseContext } from './runner-lease-context.ts'; import type { diff --git a/src/contracts/metro.ts b/src/contracts/metro.ts index 5522f573e..badc46b43 100644 --- a/src/contracts/metro.ts +++ b/src/contracts/metro.ts @@ -1,3 +1,4 @@ +import type { MetroBridgeScope } from './companion-tunnel-scope.ts'; import type { SessionRuntimeHints } from '../kernel/contracts.ts'; // Metro vocabulary shared by the command surface (which validates it) and metro/ (which @@ -45,6 +46,12 @@ export type PrepareMetroRuntimeResult = { bridge: MetroBridgeResult | null; }; +/** + * `transport` says which channel delivered the reload: `http` for the classic GET /reload route, + * `message-socket` for the /message websocket broadcast used when the server has no HTTP reload + * route (Expo). `status`/`body` always describe the HTTP probe; on the websocket path `reloadUrl` + * is the ws(s) message-socket URL. + */ export type ReloadMetroResult = { reloaded: true; reloadUrl: string; @@ -52,3 +59,37 @@ export type ReloadMetroResult = { body: string; transport: 'http' | 'message-socket'; }; + +// The client-facing Metro command vocabulary lives here rather than in a contracts/client-*.ts +// family file, so that one file answers the Metro question. +export type MetroPrepareOptions = { + projectRoot?: string; + kind?: MetroPrepareKind; + publicBaseUrl?: string; + proxyBaseUrl?: string; + bearerToken?: string; + bridgeScope?: MetroBridgeScope; + launchUrl?: string; + companionProfileKey?: string; + companionConsumerKey?: string; + port?: number; + listenHost?: string; + statusHost?: string; + startupTimeoutMs?: number; + probeTimeoutMs?: number; + reuseExisting?: boolean; + installDependenciesIfNeeded?: boolean; + runtimeFilePath?: string; + logPath?: string; +}; + +export type MetroReloadOptions = { + metroHost?: string; + metroPort?: number; + bundleUrl?: string; + timeoutMs?: number; +}; + +export type MetroPrepareResult = PrepareMetroRuntimeResult; + +export type MetroReloadResult = ReloadMetroResult; diff --git a/src/contracts/result-serialization.ts b/src/contracts/result-serialization.ts index 4fcbe432a..3a0af80ed 100644 --- a/src/contracts/result-serialization.ts +++ b/src/contracts/result-serialization.ts @@ -1,15 +1,17 @@ import type { - AgentDeviceDevice, - AgentDeviceIdentifiers, - AgentDeviceSession, - AgentDeviceSessionDevice, AppCloseResult, AppDeployResult, AppInstallFromSourceResult, AppOpenResult, - CaptureSnapshotResult, - SessionCloseResult, -} from './client-api.ts'; +} from './client-app.ts'; +import type { CaptureSnapshotResult } from './client-capture.ts'; +import type { AgentDeviceIdentifiers } from './client-connection.ts'; +import type { + AgentDeviceDevice, + AgentDeviceSession, + AgentDeviceSessionDevice, +} from './client-device-view.ts'; +import type { SessionCloseResult } from './client-session.ts'; import { publicSnapshotCaptureAnnotations, type SnapshotCaptureAnnotations, diff --git a/src/core/command-descriptor/registry.ts b/src/core/command-descriptor/registry.ts index 5c4ba88fa..e27dc60c5 100644 --- a/src/core/command-descriptor/registry.ts +++ b/src/core/command-descriptor/registry.ts @@ -1,7 +1,7 @@ import type { CommandCapability } from '../capabilities.ts'; // The typed-flags request from contracts/, not the daemon's server-side refinement: these // descriptors read `command`, `positionals` and `flags` and never touch `internal`. -import type { CommandRequest } from '../../contracts/command-request.ts'; +import type { DispatchedCommand } from '../../contracts/dispatched-command.ts'; import type { RefFrameEffect } from '../../contracts/ref-frame-effect.ts'; import { isReadOnlyFindAction, parseFindArgs } from '../../selectors/find.ts'; import { resolveWaitBudgetMs } from '../wait-positionals.ts'; @@ -84,10 +84,10 @@ const REQUEST_EXECUTION_EXEMPT = { const allowAnyDeviceSessionless = (): boolean => true; -const isRecordingStartRequest = (req: CommandRequest): boolean => +const isRecordingStartRequest = (req: DispatchedCommand): boolean => (req.positionals?.[0] ?? '').toLowerCase() === 'start'; -const isShardedTestRequest = (req: CommandRequest): boolean => +const isShardedTestRequest = (req: DispatchedCommand): boolean => req.command === 'test' && (typeof req.flags?.shardAll === 'number' || typeof req.flags?.shardSplit === 'number'); @@ -101,23 +101,21 @@ const isShardedTestRequest = (req: CommandRequest): boolean => // enter/return dispatch a real return key. Anything other than a read is // classified may-invalidate (the honest superset for unknown subactions). const KEYBOARD_READ_ONLY_ACTIONS = new Set(['status', 'get']); -const keyboardRefFrameEffect = (req: CommandRequest): RefFrameEffect => +const keyboardRefFrameEffect = (req: DispatchedCommand): RefFrameEffect => readOnlySubactionRefFrameEffect(req, KEYBOARD_READ_ONLY_ACTIONS, 'status'); // alert actions are get/wait/accept/dismiss: get/wait read, accept/dismiss act. const ALERT_READ_ONLY_ACTIONS = new Set(['get', 'wait']); -const alertRefFrameEffect = (req: CommandRequest): RefFrameEffect => +const alertRefFrameEffect = (req: DispatchedCommand): RefFrameEffect => readOnlySubactionRefFrameEffect(req, ALERT_READ_ONLY_ACTIONS, 'get'); -type RecordingEffectRequest = Pick; - -const keyboardRecordingEffect = (req: RecordingEffectRequest): RecordingEffect => +const keyboardRecordingEffect = (req: DispatchedCommand): RecordingEffect => readOnlySubactionRecordingEffect(req, KEYBOARD_READ_ONLY_ACTIONS, 'status'); -const alertRecordingEffect = (req: RecordingEffectRequest): RecordingEffect => +const alertRecordingEffect = (req: DispatchedCommand): RecordingEffect => readOnlySubactionRecordingEffect(req, ALERT_READ_ONLY_ACTIONS, 'get'); -const findRecordingEffect = (req: RecordingEffectRequest): RecordingEffect => { +const findRecordingEffect = (req: DispatchedCommand): RecordingEffect => { try { return isReadOnlyFindAction(parseFindArgs(req.positionals ?? []).action) ? 'observes-app' @@ -129,7 +127,7 @@ const findRecordingEffect = (req: RecordingEffectRequest): RecordingEffect => { }; function readOnlySubactionRefFrameEffect( - req: CommandRequest, + req: DispatchedCommand, readOnlyActions: ReadonlySet, defaultAction: string, ): RefFrameEffect { @@ -139,7 +137,7 @@ function readOnlySubactionRefFrameEffect( } function readOnlySubactionRecordingEffect( - req: RecordingEffectRequest, + req: DispatchedCommand, readOnlyActions: ReadonlySet, defaultAction: string, ): RecordingEffect { @@ -1452,9 +1450,7 @@ export function resolveTargetIdentityVerification( } /** ADR 0016 request-sensitive app-state effect for one recorded request. */ -export function resolveCommandRecordingEffect( - req: Pick, -): RecordingEffect | undefined { +export function resolveCommandRecordingEffect(req: DispatchedCommand): RecordingEffect | undefined { const descriptor = COMMAND_DESCRIPTOR_BY_NAME.get(req.command); if (!descriptor?.recordsSessionAction) return undefined; return typeof descriptor.recordingEffect === 'function' diff --git a/src/core/command-descriptor/types.ts b/src/core/command-descriptor/types.ts index d0789d679..cadc69c65 100644 --- a/src/core/command-descriptor/types.ts +++ b/src/core/command-descriptor/types.ts @@ -2,7 +2,7 @@ import type { CommandCapability } from '../capabilities.ts'; import type { DaemonCommandDescriptor } from '../../daemon/daemon-command-registry.ts'; // The typed-flags request from contracts/, not the daemon's server-side refinement: these // descriptors read `command`, `positionals` and `flags` and never touch `internal`. -import type { CommandRequest } from '../../contracts/command-request.ts'; +import type { DispatchedCommand } from '../../contracts/dispatched-command.ts'; import type { PostActionObservationSupport } from './post-action-observation.ts'; export type ResponseDataFieldTransform = { @@ -103,7 +103,7 @@ export type CommandDispatchFacet = { export type RecordingEffect = 'mutates-app' | 'observes-app'; export type CommandRecordingEffect = | RecordingEffect - | ((req: Pick) => RecordingEffect); + | ((req: DispatchedCommand) => RecordingEffect); /** * ADR 0012 / #1349: when replay verifies a recorded `target-v1` annotation. diff --git a/src/daemon/handlers/__tests__/snapshot-handler.test.ts b/src/daemon/handlers/__tests__/snapshot-handler.test.ts index 1c031ff28..e71da15b5 100644 --- a/src/daemon/handlers/__tests__/snapshot-handler.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-handler.test.ts @@ -13,7 +13,7 @@ import { buildSnapshotSignatures } from '../../android-snapshot-freshness.ts'; import { buildInteractionSurfaceSignature } from '../../interaction-outcome-policy.ts'; import { buildSnapshotPresentationKey } from '../../../kernel/snapshot.ts'; import { snapshotCliOutput } from '../../../commands/capture/output.ts'; -import type { CaptureSnapshotResult } from '../../../contracts/client-api.ts'; +import type { CaptureSnapshotResult } from '../../../contracts/client-capture.ts'; vi.mock('../../../core/dispatch.ts', async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/daemon/types.ts b/src/daemon/types.ts index 075102f65..a480ec4c8 100644 --- a/src/daemon/types.ts +++ b/src/daemon/types.ts @@ -1,5 +1,6 @@ import type { DaemonArtifact as PublicDaemonArtifact, + DaemonRequest as WireRequest, DaemonRequestMeta as PublicDaemonRequestMeta, DaemonResponse as PublicDaemonResponse, DaemonResponseData as PublicDaemonResponseData, @@ -7,7 +8,6 @@ import type { LeaseBackend, SessionRuntimeHints as PublicSessionRuntimeHints, } from '../kernel/contracts.ts'; -import type { CommandRequest } from '../contracts/command-request.ts'; import type { CommandFlags } from '../contracts/command-flags.ts'; import type { GestureReferenceFrame, ScrollDirection } from '../contracts/scroll-gesture.ts'; import type { LogBackend } from '../contracts/logs.ts'; @@ -117,14 +117,16 @@ type DaemonRequestInternal = { }; /** - * The server-side request: the shared `CommandRequest` (wire shape with typed flags) plus what - * only the daemon may see. `token` and `session` are required by the time a request is dispatched, - * and `internal` carries `SessionState` callbacks and the admitted lease — which is why this type - * stays in the daemon while `CommandRequest` sits in contracts for everyone else. + * The server-side request: the wire shape plus what only the daemon may see. `token` and `session` + * are required by the time a request is dispatched, `flags` is narrowed to the `CommandFlags` + * vocabulary the wire cannot enforce, and `internal` carries `SessionState` callbacks and the + * admitted lease — which is why this type stays in the daemon. Zones below it that only need to + * classify a command take `contracts/dispatched-command.ts` instead. */ -export type DaemonRequest = Omit & { +export type DaemonRequest = Omit & { token: string; session: string; + flags?: CommandFlags; meta?: DaemonRequestMeta; internal?: DaemonRequestInternal; }; diff --git a/src/mcp/__tests__/command-tools-parity.test.ts b/src/mcp/__tests__/command-tools-parity.test.ts index 64ca2fcd4..a3d5d0e49 100644 --- a/src/mcp/__tests__/command-tools-parity.test.ts +++ b/src/mcp/__tests__/command-tools-parity.test.ts @@ -4,7 +4,7 @@ import os from 'node:os'; import path from 'node:path'; import { afterEach, test, vi } from 'vitest'; import { createAgentDeviceClient } from '../../agent-device-client.ts'; -import type { AgentDeviceDaemonTransport } from '../../contracts/client-api.ts'; +import type { AgentDeviceDaemonTransport } from '../../contracts/client-connection.ts'; import type { AgentDeviceClient } from '../../client/client-types.ts'; import type { CommandExecutionResult } from '../../commands/command-surface.ts'; import { createCommandToolExecutor, listCommandTools } from '../command-tools.ts'; diff --git a/src/mcp/command-tools.ts b/src/mcp/command-tools.ts index b63a8a4cd..bd5a89961 100644 --- a/src/mcp/command-tools.ts +++ b/src/mcp/command-tools.ts @@ -1,4 +1,4 @@ -import type { AgentDeviceClientConfig } from '../contracts/client-api.ts'; +import type { AgentDeviceClientConfig } from '../contracts/client-connection.ts'; import type { AgentDeviceClient } from '../client/client-types.ts'; import type { JsonSchema } from '../commands/command-contract.ts'; import type { CommandExecutionResult } from '../commands/command-surface.ts'; diff --git a/src/metro/client-metro.ts b/src/metro/client-metro.ts index a68f6ac75..b73538f88 100644 --- a/src/metro/client-metro.ts +++ b/src/metro/client-metro.ts @@ -109,13 +109,6 @@ export type ReloadMetroOptions = { timeoutMs?: number | string; }; -/** - * `transport` says which channel delivered the reload: `http` for the classic GET /reload route, - * `message-socket` for the /message websocket broadcast used when the server has no HTTP reload - * route (Expo). `status`/`body` always describe the HTTP probe; on the websocket path `reloadUrl` - * is the ws(s) message-socket URL. - */ - type ProxyBridgeRequestOptions = { baseUrl: string; bearerToken: string; diff --git a/test/integration/provider-scenarios/harness.ts b/test/integration/provider-scenarios/harness.ts index 1515ee61f..9d93eae36 100644 --- a/test/integration/provider-scenarios/harness.ts +++ b/test/integration/provider-scenarios/harness.ts @@ -3,7 +3,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { createAgentDeviceClient } from '../../../src/agent-device-client.ts'; -import type { AgentDeviceDaemonTransport } from '../../../src/contracts/client-api.ts'; +import type { AgentDeviceDaemonTransport } from '../../../src/contracts/client-connection.ts'; import type { AgentDeviceClient } from '../../../src/client/client-types.ts'; import { createRequestHandler, diff --git a/test/output-economy/fixtures.ts b/test/output-economy/fixtures.ts index 9074ed745..3d98a6f64 100644 --- a/test/output-economy/fixtures.ts +++ b/test/output-economy/fixtures.ts @@ -1,7 +1,5 @@ -import type { - CaptureSnapshotResult, - CommandRequestResult, -} from '../../src/contracts/client-api.ts'; +import type { CaptureSnapshotResult } from '../../src/contracts/client-capture.ts'; +import type { CommandRequestResult } from '../../src/contracts/client-request.ts'; import type { DaemonResponseData } from '../../src/daemon/types.ts'; import { AppError } from '../../src/kernel/errors.ts'; import { attachRefs, type RawSnapshotNode } from '../../src/kernel/snapshot.ts'; diff --git a/test/output-economy/routine-workflow.ts b/test/output-economy/routine-workflow.ts index 2015ce811..4cab0b070 100644 --- a/test/output-economy/routine-workflow.ts +++ b/test/output-economy/routine-workflow.ts @@ -1,4 +1,4 @@ -import type { CommandRequestResult } from '../../src/contracts/client-api.ts'; +import type { CommandRequestResult } from '../../src/contracts/client-request.ts'; import type { AgentDeviceClient } from '../../src/client/client-types.ts'; import { AppError, normalizeError, type NormalizedError } from '../../src/kernel/errors.ts'; import { snapshotCliOutput } from '../../src/commands/capture/output.ts';