Skip to content

Commit 29e19b8

Browse files
authored
docs: add ADR 0008 (command descriptor) + ADR 0009 (Apple consolidation) (#905)
Locks the two axis decisions and starts retiring plans/ into ADRs. ADR 0008 (Proposed) records the command-descriptor registry composing domain-owned facets and deriving the ~10 tables, bound by ADR 0003's four invariants; ADR 0009 (Accepted, groundwork shipped in #896) records the AppleOS leaf axis under one 'apple' Platform. perfect-shape.md links both and marks Phase 0 + Tier-A dedup as merged.
1 parent 91d1b4c commit 29e19b8

3 files changed

Lines changed: 134 additions & 0 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# ADR 0008: Command Descriptor Registry
2+
3+
## Status
4+
5+
Proposed
6+
7+
## Context
8+
9+
A command's identity is restated, by hand, across roughly ten tables that must stay aligned by
10+
convention: `PUBLIC_COMMANDS` (`src/command-catalog.ts`), the per-command metadata and family facets
11+
(`src/commands/**`), the capability matrix (`src/core/capabilities.ts`), the daemon command registry
12+
(`src/daemon/daemon-command-registry.ts`, ADR 0003), the structured-batch allowlist
13+
(`src/batch-policy.ts`), the MCP exposure sets, the Node client interface and impl (`src/client-types.ts`,
14+
`src/client.ts`), and the generic-dispatch `switch` (`src/core/dispatch.ts`, whose `default: throw` makes a
15+
missing or renamed command a runtime error, not a compile error). Adding one command touches ~24 files, the
16+
argument shape is (de)serialized ~4 times, and the gesture set is retyped in three places.
17+
18+
The codebase already proves the cure works for part of this: the `CommandFamilyFacet`
19+
(`src/commands/family/`) derives the MCP tools, the CLI schema, and the batch writer from a single array.
20+
It simply stops at the command-surface boundary; everything past it is hand-maintained.
21+
22+
ADR 0003 deliberately separated daemon route/policy into its own internally-owned registry with a small
23+
predicate interface, and its 2026-06 update set four invariants that any single-declaration/derivation
24+
model must preserve. This ADR is that model.
25+
26+
## Decision
27+
28+
Introduce one `CommandDescriptor` per command that **composes facets owned by their domains** and from which
29+
every consumer table is **derived** by pure, parity-tested projection:
30+
31+
- The descriptor composes a `surface` facet (owned by `src/commands/**`: identity, CLI schema/reader, MCP),
32+
a `capability` facet (owned by `src/core/capabilities`), and a `daemon` facet (route + request-policy
33+
traits, **owned under `src/daemon/`** per ADR 0003), plus a typed result.
34+
- The public catalog, capability matrix, daemon command registry, batch allowlist, MCP tool list, CLI
35+
schema, and the Node client surface become pure projections of the descriptor set. The
36+
`src/core/dispatch.ts` `switch` is replaced by a total map keyed on the command-name union, so a missing
37+
handler is a compile error.
38+
- The cross-process `invoke` (client) and in-daemon `execute` seams stay distinct; the process boundary is
39+
never collapsed.
40+
41+
This **composes with**, and is bound by, ADR 0003's four invariants: daemon-owned declaration (never inlined
42+
into the public surface), the predicate interface unchanged, no leakage of daemon-only traits into public
43+
projections, and one declaration per concern enforced by the type system.
44+
45+
## Alternatives Considered
46+
47+
- Keep the hand-synced tables: no migration risk, but it is the status quo this ADR exists to remove —
48+
~24-file cost per command and drift kept in check only by convention and tests.
49+
- A single flat public descriptor with daemon fields inlined: re-contaminates the public command surface
50+
with daemon-only policy, which is exactly what ADR 0003 (and its update) forbid.
51+
- Build-time code generation: a real option, but runtime derivation with `as const satisfies` keeps the
52+
source of truth in type-checked TypeScript with no separate build step or generated artifacts to review.
53+
54+
## Consequences
55+
56+
Adding a plain command touches ~1–2 files; per-platform behavior remains N implementations behind the
57+
descriptor's `execute`. The descriptor is the prerequisite for typed per-command results (ADR 0010, which
58+
deletes the `src/client-types.ts` mirror) and supplies the capability facet the platform-plugin work
59+
(ADR 0009) hooks into.
60+
61+
Migration is **strangler-fig and sequential** — never a big-bang:
62+
63+
1. Introduce the `commandRegistry` as the root and **invert the import graph** so `command-catalog`,
64+
`capabilities`, `daemon-command-registry`, and `batch-policy` become leaves that derive from it.
65+
2. Promote each family's facet to a full `CommandDescriptor`, family by family.
66+
3. Replace the dispatch `switch` with the registry-driven total map, arm by arm.
67+
68+
Each derived table must be asserted **byte-for-byte equivalent** to the hand-authored table by a parity test
69+
**before** the hand table is deleted. The principal risk is the import-cycle inversion: `command-catalog.ts`
70+
has ~95 importers and the family facet currently imports `AgentDeviceClient`, so the descriptor module must
71+
own the `Input`/`Result` types and the client must be derived as a view type, enforced by a lint boundary.
72+
Until this lands and the registry tests pin it, the hand-authored tables remain the source of truth.
73+
74+
`plans/perfect-shape.md` (§5.2) holds the prototype; this ADR owns the decision and its constraints.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# ADR 0009: Apple Platform Consolidation (AppleOS leaf axis)
2+
3+
## Status
4+
5+
Accepted
6+
7+
## Context
8+
9+
Apple support is modeled asymmetrically and is physically smeared across an `ios` directory that is really
10+
the shared Apple engine. `Platform` carries `ios` and `macos` as separate literals, but tvOS is not a
11+
platform at all — it is `platform: 'ios' + target: 'tv'`, with the OS name reconstructed late and lossily by
12+
`resolveApplePlatformName` (`src/utils/device.ts`). Meanwhile ~697 LOC of macOS code lives inside
13+
`src/platforms/ios/`, `src/platforms/macos/devices.ts` is a 19-line stub that the iOS discovery imports (the
14+
dependency arrow points backwards), and the Apple interactor in `src/core/interactors/apple.ts` reaches into
15+
`platforms/ios`. A four-investigator survey found that ~85% of `platforms/ios` (the runner stack,
16+
tool-provider, discovery, snapshot, screenshot, perf, debug-symbols) is already OS-agnostic, and the XCTest
17+
runner already builds `ios | macos | tvos` from one Xcode project. Distinguishing or adding an Apple OS today
18+
is therefore costly out of proportion to the actual work, and iPadOS/visionOS/watchOS are unmodeled.
19+
20+
## Decision
21+
22+
Model Apple OSes with an **`AppleOS` discriminant** (`ios | ipados | tvos | watchos | visionos | macos`)
23+
under a single `apple` Platform — **not** six `Platform` literals. The OS-agnostic Apple engine consolidates
24+
under `src/platforms/apple/core/`, with genuinely per-OS code in `src/platforms/apple/os/<os>/` leaves;
25+
the Apple plugin is the first instance of the platform-plugin registry (the platform axis of the
26+
`perfect-shape` plan). Per-OS capability differences become data keyed by `AppleOS`. The additive,
27+
non-breaking `appleOs` discriminant — the groundwork for this — shipped in #896.
28+
29+
## Alternatives Considered
30+
31+
- Promote each Apple OS to its own `Platform` literal: rejected. `DeviceTarget` (`mobile | tv | desktop`) is
32+
already cross-platform (Android TV uses `target: 'tv'`), so a `tvos` literal collides with the form-factor
33+
axis; it would also force the ~15 `isApplePlatform` and ~52 `macos` branch sites to enumerate six literals
34+
and break the single-bucket `apple` capability/selector model that already works.
35+
- Keep the status quo: rejected — the tvOS/macOS asymmetry, the mislabeled macOS code, and the lossy
36+
target→OS inference persist, with no path to iPadOS/visionOS.
37+
- Exclude macOS from the consolidation: rejected. macOS is already entangled — it builds via the same XCUITest
38+
project and ~697 LOC of its code already lives inside `platforms/ios`. Excluding it would leave the
39+
mislabel in place; including it as a distinct AppKit leaf normalizes it without homogenizing it.
40+
41+
## Consequences
42+
43+
Adding a first-class Apple OS becomes cheap: a leaf module plus a runner-profile row. iOS/iPadOS/tvOS/macOS
44+
are mostly relocate-and-rename (the engine never needed to know which Apple OS it drives); visionOS is scoped
45+
net-new work (XCUITest supports it — a profile row, a build case, `#if os(visionOS)`, a widened discovery
46+
filter, plus real spatial-input QA); watchOS is an explicit **unsupported sentinel** because XCUITest cannot
47+
drive watchOS UI. macOS stays a distinct AppKit leaf (its helper binary and menubar/desktop surface model are
48+
preserved). The tvOS focus-only interaction contract (no coordinate `tap`) must not be flattened across OSes,
49+
and snapshot fidelity is uneven (the deep-RN AX-server fallback is iOS-simulator-only). The final
50+
`Platform` collapse of `ios`+`macos` into `apple` is the last, highest-diff step.
51+
52+
This composes with ADR 0008 (the descriptor's capability facet) and ADR 0003. The phased sequencing and
53+
per-OS readiness live in `plans/apple-platform-consolidation.md`; this ADR owns the decision.

plans/perfect-shape.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ wiring cheap and make *half-wired* platforms a compile error — they do not mak
3131
reduction is real but modest (~**-1k to -3k LOC**, dominated by deleting the `client-types.ts` mirror); the
3232
*real* prize is **files-per-change** and **type safety**, not raw line count.
3333

34+
**Decision records:** the two axes are now ADRs — [ADR 0008](../docs/adr/0008-command-descriptor-registry.md)
35+
(command descriptor, composing with ADR 0003) and
36+
[ADR 0009](../docs/adr/0009-apple-platform-consolidation.md) (Apple / `AppleOS`).
37+
**Status (2026-06):** Phase 0 (type-safety, parse-at-boundary, derived allow-lists, `AppleOS` groundwork,
38+
replay derivation) and the Tier-A dedup sweep are merged. The next gateway is the command-descriptor spine
39+
(§5.2, ADR 0008); everything substantive cascades from it.
40+
3441
---
3542

3643
## 1. Mind map — the codebase today

0 commit comments

Comments
 (0)