diff --git a/CONTEXT.md b/CONTEXT.md index 197bc7c26..84fdc2e4c 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 @@ -293,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 1f786332d..07896b743 100644 --- a/docs/dependency-graph-findings.md +++ b/docs/dependency-graph-findings.md @@ -1,19 +1,54 @@ # 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 — 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. + +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) +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 | **7 across 4**, 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 | @@ -41,19 +76,157 @@ 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`, `DispatchedCommand` | +| `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` 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 + +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 | **102 files** | +| value + dynamic | **1** | +| all kinds | 213 files | + +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). + +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 `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 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: + +| 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 +391,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..15ae7d81b 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, @@ -187,34 +191,46 @@ 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: +// 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/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. -// core/commands -> daemon-server the ADR 0003 daemon facet: core's descriptor registry -// 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. +// 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 -> 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. +// +// 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. // -// 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': 3, 'commands -> daemon-server': 1, - 'contracts -> client': 1, - 'core -> daemon-server': 5, + 'core -> daemon-server': 2, 'mcp -> client': 1, - 'replay -> daemon-server': 6, }; function checkTypeInversions(edges: readonly ResolvedImportEdge[]): Violation[] { @@ -277,6 +293,44 @@ 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(actual: number): Violation[] { + 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) { @@ -431,7 +485,20 @@ 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(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 ` + + `${TYPE_CYCLE_BASELINE} — lower TYPE_CYCLE_BASELINE when convenient` + ); +} + +function report( + files: readonly string[], + violations: readonly Violation[], + typeCycle: number, +): number { if (violations.length === 0) { process.stdout.write( `Layering guard: OK — ${files.length} source files satisfy R1-R3 and contain no ` + @@ -439,8 +506,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(typeCycle)}.\n`, ); return 0; } @@ -470,15 +537,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(typeCycle), ...checkZeroDepJobs(), ]; - return report(sourceFiles, violations); + 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 da5f380b5..c76096855 100644 --- a/scripts/layering/model.ts +++ b/scripts/layering/model.ts @@ -334,3 +334,72 @@ 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. + * + * 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. */ +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(); +} diff --git a/src/__tests__/cli-client-commands.test.ts b/src/__tests__/cli-client-commands.test.ts index b7996c386..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 '../client/client-types.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 9132f8d4c..fc932eef0 100644 --- a/src/agent-device-client.ts +++ b/src/agent-device-client.ts @@ -34,34 +34,44 @@ import { import { readScreenshotResultData } from './utils/screenshot-result.ts'; import { isRecord } from './utils/parsing.ts'; import type { - AgentDeviceCommandClient, - AgentDeviceClient, - 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, - MetroPrepareResult, - PanOptions, +} from './contracts/client-capture.ts'; +import type { + AgentDeviceClientConfig, + AgentDeviceDaemonTransport, +} from './contracts/client-connection.ts'; +import type { FlingOptions, - RotateCommandResult, - 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-session.ts'; +import type { MetroPrepareOptions } from './contracts/metro.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..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 './client/client-types.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 6c351fe34..38283ac4a 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-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 6419d18fb..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 '../../client/client-types.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-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..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 './client-types.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 0dc42642c..0e1a8bf9f 100644 --- a/src/client/client-types.ts +++ b/src/client/client-types.ts @@ -1,72 +1,183 @@ -import type { PublicSnapshotCaptureAnnotations } from '../contracts/snapshot-capture-annotations.ts'; -import type { SnapshotDiagnosticsSummary } from '../contracts/snapshot-diagnostics.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 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/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 — 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 +// 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 { - DaemonResponseData, - DaemonInstallSource, - DaemonLockPolicy, - DaemonRequest, - DaemonResponse, - LeaseBackend, - NetworkIncludeMode, - ResponseLevel, - SessionIsolationMode, - SessionRuntimeHints, -} from '../kernel/contracts.ts'; + AppCloseOptions, + AppCloseResult, + AppDeployOptions, + AppDeployResult, + AppInstallFromSourceOptions, + AppInstallFromSourceResult, + AppInstallOptions, + AppListOptions, + AppOpenOptions, + AppOpenResult, + AppPushOptions, + AppTriggerEventOptions, + MaterializationReleaseOptions, + MaterializationReleaseResult, +} from '../contracts/client-app.ts'; import type { - AppleOS, - DeviceKind, - DeviceTarget, - PublicPlatform, - PlatformSelector, -} from '../kernel/device.ts'; -import type { BackMode } from '../contracts/back-mode.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'; + CaptureDiffOptions, + CaptureScreenshotOptions, + CaptureScreenshotResult, + CaptureSnapshotOptions, + CaptureSnapshotResult, +} from '../contracts/client-capture.ts'; +import type { + AgentDeviceClientConfig, + AgentDeviceRequestOverrides, + AgentDeviceSelectionOptions, + DeviceCommandBaseOptions, +} from '../contracts/client-connection.ts'; +import type { + AgentDeviceCapabilitiesResult, + AgentDeviceDevice, + AgentDeviceSession, + DeviceBootOptions, + DeviceShutdownOptions, +} from '../contracts/client-device-view.ts'; +import type { + ClickOptions, + FillOptions, + FlingOptions, + FocusOptions, + 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, + NetworkOptions, + PerfOptions, + RecordOptions, + TraceOptions, +} from '../contracts/client-observability.ts'; import type { - ScrollDirection, - SwipePattern, - SwipePreset, - TransformGestureParams, -} from '../contracts/scroll-gesture.ts'; -import type { ScrollInputDirection } from '../commands/interaction/runtime/gestures.ts'; + BatchRunOptions, + ReplayRunOptions, + ReplayTestOptions, +} 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, +} 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-system.ts'; +import type { + MetroPrepareOptions, + MetroPrepareResult, + MetroReloadOptions, + MetroReloadResult, +} from '../contracts/metro.ts'; + +import type { RotateCommandResult } from '../contracts/navigation.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,486 +190,6 @@ 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'>; @@ -571,41 +202,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,496 +234,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 - * 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 ScrollOptions = DeviceCommandBaseOptions & { - direction: ScrollInputDirection; - amount?: number; - pixels?: number; - 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; devices: { diff --git a/src/commands/__tests__/command-flags.test.ts b/src/commands/__tests__/command-flags.test.ts index 45b2ff431..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 '../../client/client-types.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 6b2066c5b..7c744c1a9 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-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 c3c005266..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 '../../client/client-types.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 972888311..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 '../../client/client-types.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 eadce2a85..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 '../../client/client-types.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 e12a2a335..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 '../../client/client-types.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 ff7cee228..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 '../../client/client-types.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 32a3fe2bf..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 '../../client/client-types.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 b384a7ede..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 '../../client/client-types.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 fc4d7a93c..3b1d6480d 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-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 05db325a5..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 '../client/client-types.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 cf49baad6..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 '../client/client-types.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 ae3cd69df..31c3e23b2 100644 --- a/src/commands/interaction/index.ts +++ b/src/commands/interaction/index.ts @@ -1,22 +1,20 @@ import type { ClickOptions, - FindOptions, FillOptions, FlingOptions, FocusOptions, - GetOptions, + LongPressOptions, PanOptions, PinchOptions, PressOptions, - IsOptions, - LongPressOptions, RotateGestureOptions, ScrollOptions, SwipeGestureOptions, SwipeOptions, TransformGestureOptions, TypeTextOptions, -} from '../../client/client-types.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 caecb5ada..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 '../../client/client-types.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 99f40c478..a650c785b 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-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/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/interaction/selectors.ts b/src/commands/interaction/selectors.ts index 883fe2ca0..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 '../../client/client-types.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 0858329e8..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 '../../client/client-types.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 75a847829..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 '../../client/client-types.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 39648f9a4..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 '../../client/client-types.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 a02c30c0a..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 '../../client/client-types.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 9fc0c09f5..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 '../../client/client-types.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 1ea77da46..8248cbe1a 100644 --- a/src/commands/metro/index.ts +++ b/src/commands/metro/index.ts @@ -3,7 +3,7 @@ import type { MetroPrepareResult, MetroReloadOptions, MetroReloadResult, -} from '../../client/client-types.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 b55060706..17962ddef 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-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 a6c1d2aec..37cc585b3 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-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 42efe5c85..cbb136196 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-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 f06216c22..2cfbe79e2 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-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 06e136a24..a10f04b9a 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-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 2b5ea91da..47b27a8da 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-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/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/batch-step.ts b/src/contracts/batch-step.ts new file mode 100644 index 000000000..126e70ba6 --- /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-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. + */ +export type DaemonBatchStep = { + command: string; + positionals?: string[]; + input?: Record; + flags?: Record; + runtime?: SessionRuntimeHints; +}; 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-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/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/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 bb019a82a..badc46b43 100644 --- a/src/contracts/metro.ts +++ b/src/contracts/metro.ts @@ -1,4 +1,95 @@ +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 // 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; +}; + +/** + * `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; + status: number; + 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/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/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..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/client-types.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/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/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/core/command-descriptor/registry.ts b/src/core/command-descriptor/registry.ts index a8063b341..e27dc60c5 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 { 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'; import { @@ -84,10 +84,10 @@ const REQUEST_EXECUTION_EXEMPT = { const allowAnyDeviceSessionless = (): boolean => true; -const isRecordingStartRequest = (req: DaemonRequest): boolean => +const isRecordingStartRequest = (req: DispatchedCommand): boolean => (req.positionals?.[0] ?? '').toLowerCase() === 'start'; -const isShardedTestRequest = (req: DaemonRequest): 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: 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: 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: DaemonRequest): 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: DaemonRequest, + 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 797a62e0e..cadc69c65 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 { DispatchedCommand } from '../../contracts/dispatched-command.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: DispatchedCommand) => 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/handlers/__tests__/snapshot-handler.test.ts b/src/daemon/handlers/__tests__/snapshot-handler.test.ts index f028cc1d9..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 '../../../client/client-types.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 df12b84cd..a480ec4c8 100644 --- a/src/daemon/types.ts +++ b/src/daemon/types.ts @@ -1,6 +1,6 @@ import type { DaemonArtifact as PublicDaemonArtifact, - DaemonRequest as PublicDaemonRequest, + DaemonRequest as WireRequest, DaemonRequestMeta as PublicDaemonRequestMeta, DaemonResponse as PublicDaemonResponse, DaemonResponseData as PublicDaemonResponseData, @@ -8,7 +8,7 @@ import type { LeaseBackend, SessionRuntimeHints as PublicSessionRuntimeHints, } from '../kernel/contracts.ts'; -import type { CommandFlags } from '../core/dispatch.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,7 +116,14 @@ type DaemonRequestInternal = { replayPlanStep?: boolean; }; -export type DaemonRequest = Omit & { +/** + * 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 & { token: string; session: string; flags?: CommandFlags; @@ -494,25 +501,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/mcp/__tests__/command-tools-parity.test.ts b/src/mcp/__tests__/command-tools-parity.test.ts index 4887d8964..a3d5d0e49 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-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'; import { validateAgainstSchema } from './output-schema-validator.ts'; diff --git a/src/mcp/command-tools.ts b/src/mcp/command-tools.ts index 6d21dcc29..bd5a89961 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-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'; import { RESPONSE_LEVELS, type ResponseLevel } from '../kernel/contracts.ts'; diff --git a/src/metro/client-metro.ts b/src/metro/client-metro.ts index ec8fa1492..b73538f88 100644 --- a/src/metro/client-metro.ts +++ b/src/metro/client-metro.ts @@ -1,4 +1,12 @@ -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, + ResolvedMetroKind, +} from '../contracts/metro.ts'; import fs from 'node:fs'; import path from 'node:path'; import { sleep } from '../utils/timeouts.ts'; @@ -30,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'; @@ -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; @@ -118,20 +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. - */ -export type ReloadMetroResult = { - reloaded: true; - reloadUrl: string; - status: number; - body: string; - transport: 'http' | 'message-socket'; -}; - type ProxyBridgeRequestOptions = { baseUrl: string; bearerToken: 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/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/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>; diff --git a/test/integration/provider-scenarios/harness.ts b/test/integration/provider-scenarios/harness.ts index b8a2e5e18..9d93eae36 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-connection.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..3d98a6f64 100644 --- a/test/output-economy/fixtures.ts +++ b/test/output-economy/fixtures.ts @@ -1,4 +1,5 @@ -import type { CaptureSnapshotResult, CommandRequestResult } from '../../src/client/client-types.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 8fedc04f2..4cab0b070 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-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'; import { interactionCliOutputFormatters } from '../../src/commands/interaction/output.ts';