# Typed State Machine: ZoomedOut / ZoomedIn / TargetFollow

**Task**: AZ-682_scan_controller_state_machine
**Name**: Typed enum state machine with explicit transitions and no ad-hoc booleans
**Description**: Define the typed state enum (`ZoomedOut | ZoomedIn { roi, hold_started_at } | TargetFollow { target_id, started_at }`) and all explicit transitions. Tick latency budget ≤10 ms p99. Frame-rate floor monitor suppresses `ZoomedOut → ZoomedIn` when sustained FPS < 10.
**Complexity**: 5 points
**Dependencies**: AZ-640_initial_structure, AZ-649_mission_executor_telemetry_forwarding
**Component**: scan_controller
**Tracker**: AZ-682
**Epic**: AZ-635

## Problem

`scan_controller` is the system's brain. State variables and transitions must be exhaustive and typed — ad-hoc booleans drift and create unreachable bugs. The frame-rate-floor guard (`sustained FPS < 10 → suppress zoom-in`) prevents the controller from entering a state where Tier 2 + VLM saturate the budget while Tier 1 starves.

## Outcome

- `ScanState` enum exhausting all variants; transitions modeled as functions returning new `ScanState`.
- `Tick` function: pure transition step taking inputs (`DetectionBatch | MovementCandidate | Tier2Evidence | VlmAssessment | OperatorEvent | TelemetrySample | MissionState | MapObjectsSyncState`) and returning `(new_state, emitted_actions)`.
- Frame-rate floor monitor: rolling FPS window from `frame_ingest` health pulses; below 10 fps sustained over `floor_window_secs`, suppress `ZoomedOut → ZoomedIn` transitions and surface health → yellow.
- Health: `state`, `tick_latency_p99`, `last_state_change_ts`, `fps_floor_active`.

## Scope

### Included
- State enum + transition functions.
- Tick function (pure, single-input).
- Frame-rate floor monitor.
- Restart starts in `ZoomedOut` with empty queue (per `description.md §5`).

### Excluded
- POI queue + rate-cap + decision window (task 44).
- Evidence ladder + zoom-in candidate handling (task 45).
- MapObjects dispatch (task 46).
- Gimbal command issuance + degraded-sync handling (task 47).

## Acceptance Criteria

**AC-1: State enum is exhausted by tick**
Given any `ScanState` variant and any `TickInput`
When `tick(state, input)` runs
Then it returns a `(new_state, actions)` with no `_ => …` catch-all in transition logic; new variants force compile errors elsewhere.

**AC-2: Transition catalogue is complete**
Given the architecture catalogue of allowed transitions (per `system-flows.md §F4`)
When transitions are enumerated in the code
Then every `(from_state, trigger) → to_state` from the spec is covered; spec-disallowed transitions are typed-impossible OR explicitly rejected with a recorded reason.

**AC-3: Frame-rate floor suppresses zoom-in**
Given sustained FPS < 10 over the `floor_window_secs` window
When a tick that would otherwise transition `ZoomedOut → ZoomedIn` runs
Then the transition is suppressed; state remains `ZoomedOut`; `fps_floor_active = true`; health → yellow.

**AC-4: Tick latency budget**
Given a stream of 1000 sequential ticks
When measured
Then `tick_latency_p99 ≤ 10 ms`.

## Non-Functional Requirements

**Performance**
- Tick latency: ≤10 ms p99.
- State transition is deterministic (same inputs in same order → same outputs).

## Contract

- Canonical typed model: `data_model.md §POI`, `§MapObject`, `§IgnoredItem`. State variants per `description.md §1`.

## Runtime Completeness

- **Named capability**: deterministic typed state machine for scan_controller.
- **Production code that must exist**: real state enum; real transitions; real frame-rate floor monitor.
- **Unacceptable substitutes**: `is_zoomed_in: bool` instead of a sum type is unacceptable per `description.md §4` ("no ad-hoc booleans").
