Files
wristivo/docs/CONTRACTS.md
T

45 lines
8.3 KiB
Markdown

# Command contract v1
Source of truth: `Packages/WristivoKit/Sources/RemoteCore`, including `WatchSession.swift`. The integration owner maintains these types. The v1 control-plane DTOs, codec and actor protocol are now defined. WCSession delivery and the production phone actor are integrated. Handshake behavior is covered by fake-session tests; real paired-device delivery is pending.
## Commands and envelope
`RemoteCommand`: up, down, left, right, select, back, home, playPause, volumeUp, volumeDown, mute. Wire values are the enum's raw strings. Unknown values fail decoding. No raw arbitrary Samsung keys from UI or watch.
`CommandEnvelope`: `schemaVersion` (1), `id` (UUID), `sessionID` (UUID), `televisionID` (UUID), `sequence` (UInt64), `issuedAt` (Date), `expiresAt` (Date), `command`. UUIDs identify local opaque records, not TV serial numbers or network addresses. Date encoding on the bridge is explicitly JSON milliseconds since Unix epoch; both encoders/decoders must set the same strategy. Package round-trip tests alone are not sufficient to validate that adapter.
A session is issued by the phone for one selected paired TV, bound to the currently activated paired WCSession. The watch cannot select an arbitrary target by manufacturing an envelope. `BeginRemoteSessionRequest` returns `RemoteSessionGrant` with v1, sessionID, televisionID, issuedAt and maximumCommandLifetimeMilliseconds (1...1000); `EndRemoteSessionRequest` ends that exact session and returns `RemoteSessionEnded`. Errors use `RelayFailure`, which conforms to Error for Result return values. A new phone process, TV switch, re-pair, bridge deactivation or app stop invalidates old authorization. Starting a session must never issue a TV key.
Use `RelayJSON.encode(_:)` and `RelayJSON.decode(_:from:)` for all bridge payloads. They enforce a 4096-byte payload limit and the millisecond date strategy. Control DTO invariants are checked on encode and decode; direct Codable use bypasses that validation. The phone actor must still validate control requests from local callers. Command decoding performs no semantic admission and consumes no duplicate/sequence state. A grant is not a command; its issue date does not extend command lifetime. Apply the existing one-second command deadline independently to each command.
Sequence starts at 0 or 1 and strictly increases within a session; all senders in a given session must share one counter. At UInt64 exhaustion start a new session rather than wrapping. The receiver rejects wrong target/session/version, invalid/non-finite times, future timestamps beyond 250 ms, messages over one second old, expiry in the past, expiry preceding issue, or a declared lifetime longer than one second. No arbitrary clock-skew widening: report need to resync/start a new session. Test actual paired-device skew. Adapter also maintains a receiver-local monotonic deadline so wall-clock changes cannot extend pending work.
Validate decoded values at every trust boundary; Codable construction alone does not validate invariants. The bridge performs structural validation only and delegates to one authoritative phone command actor. Only that actor consumes CommandAdmission state and owns the authorized session; duplicate checks must not advance a second gate in the bridge. Admit each ID once with bounded storage and monotonic sequence. Consume admission before sending; on ambiguous transport failure do not retry the same action. Check expiry/session again immediately before the live WebSocket send; reject pending work on disconnect, never reconnect-and-flush.
## Receipt semantics
`CommandReceipt` correlates the envelope ID. Status is sentToTransport, rejected, expired, duplicate or unavailable. A successful send only means the adapter completed the socket write; Samsung may provide no per-key execution acknowledgement. Never display "TV confirmed" based only on that receipt. Timeout after possible delivery is uncertain; ask the user to issue a new action if needed rather than replaying.
Frozen bridge container: exactly `["schemaVersion": 1, "kind": String, "payload": Data]`, with bounded JSON payload (4 KiB maximum), delivered using sendMessage/replyHandler. `RelayMessageKind` freezes beginSession/session, endSession/ended, command/receipt, and error. Validate exact container keys/types and payload size before decoding. Boolean NSNumber is not an integer version; reject it explicitly. Unknown kinds, extra/missing container keys, inappropriate request/reply direction, unsupported container versions and malformed payloads fail closed with an error reply. DTO fields may evolve only under the compatibility policy below. Exactly one reply per incoming request, including errors. The request's WCSession reply closure supplies correlation for control requests; command receipts additionally match CommandEnvelope.id. Never execute a reply payload as a request. No passwords/tokens/raw motion in these messages.
## Adapter interfaces
Existing `RemoteTransport.send(_ envelope: CommandEnvelope) async -> CommandReceipt` is the command sink. The sender has no implicit retry guarantee. Platform agents propose small lifecycle protocols for connection, session acquisition, token repository and motion source through the integrator; do not hide setup inside `send` or start pairing in response to a gesture.
`PhoneCommandCoordinating: Actor` is now the injected phone seam. A/integration implements it on the same actor that owns CommandAdmission and TV dispatch, alongside RemoteTransport if useful. B does not implement a second authoritative actor. Its methods are `watchBridgeDidActivate(peer:)`, `beginWatchSession(_:peer:)`, `endWatchSession(_:peer:)`, `submitWatchCommand(_:peer:)` and `watchBridgeDidDeactivate(peer:)`. A fake conforming actor is sufficient for B's independent implementation/tests; the live A actor is required only for end-to-end integration.
The local `WatchPeerGeneration` is a fresh UUID per phone-side bridge activation. It is not Codable and must not cross the wire, persist, identify watch hardware, or come from the remote message. B serializes lifecycle delivery, awaits activation before forwarding requests, and passes the captured generation on every call. The phone actor rejects requests from non-current peers and ignores delayed deactivation for old peers. Repeated activation of the same peer is idempotent; activating a new peer invalidates previous grants. Repeated begin requests create fresh sessions and invalidate the previous session; B must serialize starts and ignore delayed replies after a lifecycle or local request-generation change. Stale end requests return sessionInvalidated and never end a newer session. After every suspension, the actor rechecks generation/session/target/deadline before sending. A protocol cannot enforce these semantics; A's tests must prove them.
GestureCore receives normalized rotation-rate inputs from the MotionCapture adapter. Its thresholds are algorithm fixtures, not physically calibrated degrees or validated human-motion confidence. Sensor coordinate transforms and physical traces are required before claiming wrist-swipe recognition. Watch and phone UI depend on domain contracts, never Samsung frame strings.
## Compatibility change policy
An additive optional field may remain v1 only if existing behavior stays safe. New command semantics, changed time units or altered authorization require a new schema version and explicit negotiation. Unsupported versions fail closed. Mixed-version phone/watch installations must show update guidance and preserve accessible phone control.
## Integration decision — 2026-09-18
PhoneControl composes one TVConnectionActor with KeychainTVAuthorizationStore. `sendPhoneCommand` creates a local envelope; watch envelopes enter through `submitWatchCommand`. Separate local/watch session counters feed a single actor-owned drain, with exactly one admission consumption per command. Phone actions do not replace the watch grant. Peer replacement or grant rotation during an active write closes the ambiguous socket; reconnect is explicit and never replays.
Configuration context now includes `isConnectionReady: Bool` (default false when absent) alongside the optional opaque ID/name. It never contains hostnames, certificates, tokens or commands. Old receivers that reject this additive key fail closed; deploy matching prototype phone/watch builds. A context snapshot is advisory, not authority: the phone actor still issues and validates every grant.