# Operator Command Dispatch (confirm / decline / target-follow start/release) + Idempotency

**Task**: AZ-680_operator_bridge_command_dispatch
**Name**: Dispatch validated operator commands to scan_controller + per-command-id idempotency
**Description**: After `validate()` (task 39), dispatch the command to `scan_controller`: confirm → `(target_mgrs, target_class)`, decline → `(MGRS, class_group)` for IgnoredItem append, target-follow start/release → state transition. Per-command-id idempotency cache (60 s window) so re-transmits get the cached result rather than double-acting.
**Complexity**: 3 points
**Dependencies**: AZ-640_initial_structure, AZ-678_operator_bridge_command_auth
**Component**: operator_bridge
**Tracker**: AZ-680
**Epic**: AZ-628

## Problem

Operator commands can be re-transmitted over a lossy modem link; the autopilot must not double-act. Each command carries a unique `command_id`; results are cached for 60 s. The dispatch step validates the command is for a known POI (or a target-follow session, BIT report, or safety-override scope) before forwarding to `scan_controller`.

## Outcome

- `CommandDispatcher::dispatch(validated_cmd) -> CommandAck` routes the command into `scan_controller`'s API and returns an ack.
- Idempotency cache `command_id → CommandAck` with 60 s TTL; cache-hit returns the cached ack and does NOT re-dispatch.
- POI-id-bound commands validated against currently surfaced POI set; unknown POI id → `Ack::Error { reason: unknown_poi_id }`.
- Deadline expiration handled: command past deadline → `Ack::Error { reason: expired }`.
- Health: `commands_in_flight`, `decision_latency_p50/p99`.

## Scope

### Included
- Dispatch switch on command kind (`Confirm | Decline | TargetFollowStart | TargetFollowRelease`).
- Idempotency cache with TTL.
- POI id validity check.
- Deadline check.

### Excluded
- Auth/replay (task 39).
- BIT-degraded ack + safety-override (task 42; separate command kinds with separate flow).
- POI queue mechanics (task 44).

## Acceptance Criteria

**AC-1: Confirm forwards target hint**
Given a validated `Confirm { command_id, poi_id }` for a currently-surfaced POI
When `dispatch(cmd)` runs
Then `scan_controller::on_confirm(target_mgrs, target_class)` is invoked exactly once; ack `Ok` returned; cache populated.

**AC-2: Re-transmit returns cached ack**
Given the same `command_id` re-arrives within 60 s of the first dispatch
When `dispatch(cmd)` runs
Then the cached ack is returned; `scan_controller::on_confirm` is NOT invoked again.

**AC-3: Unknown POI id rejected**
Given a `Confirm` with `poi_id` not in the surfaced set
When `dispatch(cmd)` runs
Then `Ack::Error { reason: unknown_poi_id }` returned; nothing dispatched.

**AC-4: Expired POI rejected**
Given a `Confirm` whose deadline has passed
When `dispatch(cmd)` runs
Then `Ack::Error { reason: expired }` returned; nothing dispatched.

**AC-5: Decline appends IgnoredItem via scan_controller**
Given a validated `Decline { command_id, poi_id }`
When `dispatch(cmd)` runs
Then `scan_controller::on_decline(mgrs, class_group)` invoked exactly once.

## Non-Functional Requirements

**Performance**
- Dispatch overhead: ≤2 ms p99.
- Operator command → autopilot effect: ≤1 s under normal modem (end-to-end).

## Contract

- Canonical typed model: `data_model.md §OperatorCommand`, `§POI`.

## Runtime Completeness

- **Named capability**: operator command dispatch + idempotency.
- **Production code that must exist**: real dispatch switch; real TTL cache; real validity + deadline checks.
- **Unacceptable substitutes**: no idempotency cache (the operator UI WILL retransmit on flaky modem) is unacceptable.
