# Cluster Persistence + Candidate Emission

**Task**: AZ-663_movement_detector_clustering_and_emission
**Name**: Residual-motion clustering + per-zoom-band persistence + candidate emission with source_zoom_band
**Description**: Subtract estimated ego-motion from per-pixel motion; cluster residuals; emit clusters meeting per-zoom-band minimum size + persistence threshold as `MovementCandidate`s. Self-disable in `TargetFollow` (consume frames to keep history warm; emit nothing).
**Complexity**: 5 points
**Dependencies**: AZ-640_initial_structure, AZ-662_movement_detector_ego_motion
**Component**: movement_detector
**Tracker**: AZ-663
**Epic**: AZ-629

## Problem

Once ego-motion is compensated, the remaining residual pixel motion is the candidate signal. Residuals must be clustered (connected components or DBSCAN-like spatial cluster) and tracked across frames; only clusters that persist for the per-zoom-band threshold count as candidates. Single-frame noise blips MUST NOT surface to the operator.

The candidate emission also carries `source_zoom_band` (`zoomed_out | zoomed_in`) so `scan_controller` can apply zoom-band-aware queueing logic.

The component must self-disable when `scan_controller` is in `TargetFollow` — emit zero candidates but keep consuming frames so the motion-history buffer stays warm for the next state transition.

## Outcome

- `MovementClusterer::ingest(frame, residual_motion)` updates per-cluster persistence counters per zoom band.
- A cluster meeting `min_size_px` + `min_persistence_frames` emits a `MovementCandidate { frame_seq, bbox_normalized, residual_velocity_estimate, telemetry_quality, source_frame_ts, source_zoom_band }`.
- Per-zoom-band knobs (defaults per `description.md §5`):
  - zoom-out: persistence 3–5 frames; residual-velocity floor low.
  - zoom-in: persistence 6–10 frames; residual-velocity floor higher.
- Active-state hint `disable` (during `TargetFollow`) suppresses emission but keeps history.
- Health surface: `candidates_per_min_zoomed_out`, `candidates_per_min_zoomed_in`, `current_zoom_band`, `compensation_quality_per_band`.

## Scope

### Included
- Connected-component (or spatial cluster) extraction over the residual motion field.
- Per-cluster persistence tracker, per zoom band.
- Per-band motion-history buffer (a few seconds of frames + residuals; one per zoom band).
- Candidate emission with full metadata.
- Active-state hint handling.

### Excluded
- Ego-motion estimation (task 23).
- Q14 fallback (task 25).
- POI queue ordering (`scan_controller`).

## Acceptance Criteria

**AC-1: Single-frame blip is suppressed**
Given a single isolated 5×5 px residual motion blip in one frame at zoom-out
When the clusterer runs over 30 frames
Then no `MovementCandidate` is emitted (below `min_persistence_frames = 3`).

**AC-2: Persistent moving target emits a candidate**
Given a 20×20 px residual cluster persisting across 5 consecutive frames at zoom-out
When the clusterer runs
Then exactly one `MovementCandidate` is emitted with `source_zoom_band = "zoomed_out"` and `bbox_normalized` localised around the cluster centre.

**AC-3: Zoom-in stricter threshold**
Given the same persistent cluster but at zoom-in with `min_persistence_frames = 8`
When the clusterer runs for only 5 frames
Then no candidate is emitted; 9th frame onwards emits one.

**AC-4: TargetFollow suppresses emission, keeps history warm**
Given the active-state hint is `disable`
When 30 frames with persistent clusters arrive
Then zero candidates are emitted; `compensation_quality_per_band` is still updated; when `disable` is lifted, the next persistent cluster is emitted on the SAME zoom band's threshold (history is warm).

## Non-Functional Requirements

**Performance**
- Per-frame clustering + emission: ≤20 ms p99.
- Candidate enqueue latency: zoom-out ≤1 s, zoom-in ≤1.5 s (per `description.md §9`).

**Reliability**
- Single-frame blips never surface as candidates.

## Runtime Completeness

- **Named capability**: persistent-cluster candidate detection with per-zoom-band tuning.
- **Production code that must exist**: real residual clustering; real per-band persistence tracker.
- **Unacceptable substitutes**: emitting every residual blip without persistence gating is unacceptable (operator would be flooded).
