diff --git a/docs/adr/0009-apple-platform-consolidation.md b/docs/adr/0009-apple-platform-consolidation.md index e7832c9ae..062d8474d 100644 --- a/docs/adr/0009-apple-platform-consolidation.md +++ b/docs/adr/0009-apple-platform-consolidation.md @@ -60,5 +60,5 @@ Implementation status as of 2026-06: capability tables, and any watchOS unsupported sentinel. watchOS remains out of scope for the current consolidation. -This ADR owns the architectural decision; implementation progress for the remaining platform-plugin work lives -in `plans/phase3-platform-plugin-progress.md`. +This ADR owns the architectural decision; implementation progress for the remaining platform-plugin work is +tracked in GitHub issues under the Phase 3 umbrella (#972). diff --git a/plans/perfect-shape.md b/plans/perfect-shape.md index d93f389cb..27242adfb 100644 --- a/plans/perfect-shape.md +++ b/plans/perfect-shape.md @@ -293,7 +293,7 @@ but "add a platform" still touches the `device.ts` union line. already builds `ios|macos|tvos` and ~85% of `platforms/ios` is already the OS-agnostic Apple engine, so this is mostly relocate-and-rename for iOS/iPadOS/tvOS/macOS; visionOS is scoped net-new work and watchOS is an explicit unsupported sentinel (XCUITest can't drive it). ADR 0009 owns the AppleOS decision; remaining -implementation state is tracked in [phase3-platform-plugin-progress.md](./phase3-platform-plugin-progress.md). +implementation state is tracked in the [Phase 3 tracking issue #972](https://github.com/callstack/agent-device/issues/972). ### 5.2 `CommandDescriptor` (the command axis) — *facet composition*, honoring ADR 0003 @@ -398,7 +398,7 @@ a step can't ship alone, the plan has failed. | **0 · confidence builders** (behaviorless) | **(b) ✅ shipped** — exhaustive capability platform selection; **(c) ✅ shipped** — generic `RecordingBackend
` (5 casts deleted); (a) parse-at-boundary on MCP/HTTP edge; (d) collapse the 3 platform allow-lists + 5-layer batch validation | low | correctness/security; builds muscle memory; touches no identity table |
| **1 · command spine** | (a) **invert the import graph** — `commandRegistry` becomes root, `command-catalog`/`capabilities`/`daemon-registry`/`batch-policy` derive (parity-tested, no deletion yet); (b) promote each family's facet → `CommandDescriptor` additively; (c) replace the 24-arm switch with the total map, arm-by-arm | **med** (the import-cycle inversion is the real first-week blocker: `command-catalog` has ~95 importers and the facet imports `AgentDeviceClient` today) | finishes a proven seam; add-command → ~2 files; enables everything below |
| **2 · typed results** (the parity oracle) | (a) `CommandResultMap` with `Record` default, migrate per-command from real runner payloads; (b) graft `TypedError`; fold the disowned 'generic' family into `handlers/` **last**; (c) kill the `client-types.ts` mirror (~550 LOC) | med (must be per-command, never a big-bang retype of 203 files) | the safety net the platform unwind needs; biggest single LOC win |
-| **3 · platform plugin** (now safe) | (a) define `PlatformPlugin`, **lazy** factories (cold-start benchmark guards latency); (b) move capability columns onto plugin grants, **porting every `supports()` closure verbatim**, pinned by the table-equivalence test before deletion; (c) ✅ unwind macOS and the OS-agnostic Apple engine out of `platforms/ios`; (d) finish Apple plugin facets, tvOS leaf, final `Platform` collapse, and watchOS sentinel last. The **Apple plugin is the first instance** and owns the `AppleOS` leaves — see ADR 0009 and [phase3-platform-plugin-progress.md](./phase3-platform-plugin-progress.md) | **high** (touches shared platform routing and the XCTest runner) | add-platform wiring → ~3 files; kills the 231-branch smear |
+| **3 · platform plugin** (now safe) | (a) define `PlatformPlugin`, **lazy** factories (cold-start benchmark guards latency); (b) move capability columns onto plugin grants, **porting every `supports()` closure verbatim**, pinned by the table-equivalence test before deletion; (c) ✅ unwind macOS and the OS-agnostic Apple engine out of `platforms/ios`; (d) finish Apple plugin facets, tvOS leaf, final `Platform` collapse, and watchOS sentinel last. The **Apple plugin is the first instance** and owns the `AppleOS` leaves — see ADR 0009 and the [Phase 3 tracking issue #972](https://github.com/callstack/agent-device/issues/972) | **high** (touches shared platform routing and the XCTest runner) | add-platform wiring → ~3 files; kills the 231-branch smear |
| **4 · agent-cost** (opt-in) | (a) `ResponseView.toView` with `default`==today; `responseLevel` knob defaulting to `default`; (b) typed `BatchStepResult` → intermediate steps digest; per-command MCP `outputSchema`; generalize zero-load fast-paths | med (wire-shape risk vs Maestro — strictly opt-in) | the north-star-#2 token/latency wins |
| **5 · layering + legacy** (quiet windows) | intent-folder moves + utils extraction as pure path codemods; at next major drop the ~175 LOC of legacy aliases/barrels | low-per-step, high-diff | scoped ownership + import lint; merge-pain risk → land fast, small |
@@ -433,7 +433,7 @@ grants; bundling the folder reorg with the registry work (maximizes diff-noise a
## 8. Before / after — the command axis
(The platform-axis decision lives in ADR 0009; current implementation status lives in
-[phase3-platform-plugin-progress.md](./phase3-platform-plugin-progress.md).)
+the [Phase 3 tracking issue #972](https://github.com/callstack/agent-device/issues/972).)
```
BEFORE — a command's identity is RESTATED in ~10 hand-synced tables, aligned "by convention"
diff --git a/plans/phase3-platform-plugin-progress.md b/plans/phase3-platform-plugin-progress.md
deleted file mode 100644
index c84ddd51f..000000000
--- a/plans/phase3-platform-plugin-progress.md
+++ /dev/null
@@ -1,152 +0,0 @@
-# Phase 3 — PlatformPlugin: progress + plan for the risky remainder
-
-> Tracks the platform-axis work from [perfect-shape.md](./perfect-shape.md) §5.1 / §6 (row "3 · platform
-> plugin") and ADR-0009.
-
-## Status
-
-| Step | What | State |
-|---|---|---|
-| **(a)** | `PlatformPlugin` registry + exhaustiveness + parity tests; route `getInteractor` through it | **✅ shipped (this PR — behaviorless)** |
-| **(b)** | Move capability columns + daemon columns onto plugin grants; port `supports()`/`unsupportedHint()` closures verbatim | ⛔ planned — **HUMAN REVIEW ONLY, DO NOT AUTO-MERGE** |
-| **(c)** | Unwind macOS and the OS-agnostic Apple engine out of `platforms/ios` into `platforms/apple` | **✅ shipped in #968** |
-| **(d)** | Finish the public Apple leaf model: plugin facets, tvOS leaf, final `Platform` collapse, watchOS sentinel | ⛔ planned — **HUMAN REVIEW ONLY, DO NOT AUTO-MERGE** |
-
-## Step (a) — what shipped (behaviorless foundation)
-
-- `src/core/platform-plugin/plugin.ts` — the `PlatformPlugin` type (type-only imports; lazy `createInteractor`
- / `discoverDevices`) + the registry: `registerPlatformPlugin`, `getPlugin` (throws the same
- `UNSUPPORTED_PLATFORM` AppError as the old switch default), `tryGetPlugin`, `registeredPlatforms`.
-- `src/core/platform-plugin/register-builtins.ts` — `apple` (owns `ios`+`macos`), `android`, `linux`, `web`
- plugins that WRAP today's `core/interactors/*` factories and the `platform-inventory.ts` branches via lazy
- dynamic `import()`. `BuiltinPluginsCoverAllPlatforms` is the compile-time exhaustiveness assertion (a new
- `Platform` literal without a plugin fails the build).
-- `src/core/interactors.ts` — `getInteractor` now `return getPlugin(device.platform).createInteractor(...)`
- after the unchanged provider-device check. Byte-identical (same lazy imports, same factory calls, same
- throw).
-- `src/core/platform-inventory.ts` — `WEB_DESKTOP_DEVICE` and `shouldUseHostMacFastPath` exported so the
- web/apple plugins reuse the SAME instance/predicate (no divergent copy).
-- Parity test `src/core/platform-plugin/__tests__/parity.test.ts`.
-
-**Contract scope (step-a discipline):** the `PlatformPlugin` type carries ONLY the facets this slice actually
-implements and parity-tests — `id`, `platforms`, `familySelector?`, `createInteractor`, `discoverDevices`,
-`capability { bucket, supportsByDefault? }`. The daemon-owned columns (`providers` / `recording` / `appLog` /
-`perf`) are deliberately NOT declared yet. An earlier draft declared a `recording?: { start(req:
-IosSimulatorRecordingRequest): RecordingProcess }` facet; that was REMOVED because it baked the
-iOS-simulator provider seam into the contract (it cannot represent the Android / web / macOS-runner /
-iOS-device-runner / stop-path recording contracts, which need the daemon recording context, not
-`{device,outPath} -> child/wait`). Those facets arrive in step (b), platform-neutral — see §b.3.
-
-**Placement note (deviation from §5.1's `src/platforms/plugin.ts`):** the registry lives under
-`src/core/platform-plugin/` — mirroring the existing `src/core/platform-descriptor/` and
-`src/core/command-descriptor/` foundations (#905–911), and because everything it wraps today
-(`core/interactors/*`, `core/platform-inventory.ts`, the `core/capabilities` bucket) lives in `core/`. Keeping
-it in `core/` makes `getInteractor`'s routing and the `createInteractor` wraps `core→core` (the allowed
-direction); a `platforms/`-resident registry would have to import `core/interactors/*` backwards at runtime.
-The move to `src/platforms/apple/` is part of step (c)'s leaf relocation, not the behaviorless foundation.
-
-**Deliberately NOT done (left hand-authored — parity-tested, not derived):** `PLATFORMS`
-(`src/kernel/device.ts:8`) and `parsePlatform` (`src/utils/parsing.ts:109-117`) remain the source of truth.
-The parity test proves `registeredPlatforms()` is byte-for-byte equal to both; nothing is derived FROM the
-registry yet (per the roadmap's "err toward leaving hand lists"). The CLI `--platform` enum already derives
-from `PLATFORM_SELECTORS` (`src/utils/cli-flags.ts:352`), so it is not a hand-sync hazard.
-
----
-
-## Step (b) — capability + daemon columns onto plugin grants ⛔ DO NOT AUTO-MERGE
-
-**Principle (perfect-shape §7):** RELOCATE the device-shaped `supports()`/`unsupportedHint()` closures
-verbatim; NEVER flatten them to data. Each derived table is pinned by a **table-equivalence parity test that
-asserts byte-for-byte equality across the full sample-device matrix BEFORE any hand table is deleted.**
-
-### (b.1) Route the capability-bucket selection through the plugin (pure swap, lowest risk)
-
-- Today: `selectCapabilityForPlatform` (`src/core/capabilities.ts:80-85`) already derives from
- `platformDescriptors` via `deriveCapabilityForPlatform`. The new plugin carries the SAME bucket in
- `capability.bucket` (already parity-tested here against `platformDescriptors`).
-- Change: have `isCommandSupportedOnDevice` (`capabilities.ts:87-95`) read the bucket via
- `getPlugin(device.platform).capability.bucket` (falling through `tryGetPlugin` exactly as the §5.1 sketch:
- `if (!plugin) return false`).
-- Gate: a parity test asserting `isCommandSupportedOnDevice` is unchanged for the full
- `{command × sample-device}` matrix (reuse `src/__tests__/test-utils/device-fixtures.ts`) before removing the
- `platformDescriptors` indirection. **Keep `platformDescriptors` until proven redundant.**
-
-### (b.2) Port the `supports()` / `unsupportedHint()` device closures verbatim
-
-These encode the irreducible device nuance and live today in `src/core/command-descriptor/registry.ts`:
-- `isNotMacOs` (`:41`), `isMacOsOrAppleSimulator` (`:42-43`), `isIosMobileSimulator` (`:44`),
- `supportsAndroidOrIosNonTv` (`:46-47`), `supportsSynthesisGesture`, and
- `synthesisGestureUnsupportedHint` (`:51-`) — the latter encodes **macOS-coordinate-pinch** (`:52`) and
- **tvOS-no-touch** (`:54`, `device.platform === 'ios' && device.target === 'tv'`).
-- Used at the `supports:`/`unsupportedHint:` sites (`:81, :145, :212-215, :227, :260, :319, :330-331, :429,
- :440, :463-464, :505-506, :517-518, :530`), notably the two-finger synthesis commands (pinch / rotateGesture
- / transformGesture).
-- Plan: move these closures verbatim onto the relevant plugin's `capability.supportsByDefault` (declared but
- unpopulated today) OR keep them on the command facet and have the platform-level default flow through the
- plugin — **do not rewrite the predicate bodies.** Pin with a closure-equivalence test (same inputs → same
- boolean / same hint string) before deleting any hand site.
-
-### (b.3) INTRODUCE the daemon-column facets (platform-neutral) onto the plugin
-
-Step (a) deliberately ships **no** `providers` / `recording` / `appLog` / `perf` facets (an earlier draft's
-iOS-shaped `recording` facet was removed — see "Contract scope" above). Step (b) ADDS each facet to the
-`PlatformPlugin` type, **typed against a PLATFORM-NEUTRAL, daemon-owned wrapper** — never the
-`IosSimulatorRecordingRequest` provider seam — then populates it by wrapping the existing daemon branch, pins
-it with a table-equivalence parity test, and only then routes the daemon lookup through `getPlugin(...)`:
-
-| Facet | Hand branch to wrap (file:line) | Neutral wrapper the facet must be typed against | Parity oracle |
-|---|---|---|---|
-| `providers` | `REQUEST_PLATFORM_PROVIDER_DESCRIPTORS` `src/daemon/request-platform-providers.ts:117-233` (per-platform `resolve` gates) | `() => Partial