# FP Cap Monitor + Q14 Fallback Hook

**Task**: AZ-664_movement_detector_fp_cap_and_q14_fallback
**Name**: FP cap monitor + Q14 fallback module hook
**Description**: Monitor per-zoom-band candidate flood. If sustained candidates_per_min exceeds the configured cap, suppress that band's emission (zoom-in only at first; zoom-out down-ranks lowest-confidence). Q14 fallback engages a learned-CV module behind a build-time feature flag — wire the `EgoMotionProvider` trait + a stub fallback impl that returns `not_engaged`; the real ML module is a follow-up if the benchmark gate triggers Q14.
**Complexity**: 3 points
**Dependencies**: AZ-640_initial_structure, AZ-662_movement_detector_ego_motion, AZ-663_movement_detector_clustering_and_emission
**Component**: movement_detector
**Tracker**: AZ-664
**Epic**: AZ-629

## Problem

If classical OpenCV optical flow fails to meet the per-zoom-band FP cap at zoom-in (Q14 trigger), the system must degrade safely: suppress zoom-in emission, keep zoom-out running, and engage the (optional) learned-CV fallback module if compiled in. The fallback's interface contract is fixed (`Frame + telemetry → Vec<MovementCandidate>`); the impl is a separate engineering effort gated on benchmark-gate results.

This task delivers the trait, the FP-cap monitor, the suppression behaviour, and a stub fallback impl. The real learned-CV impl is out of scope here (separate Q14 follow-up if and when the benchmark gate fires).

## Outcome

- `FpCapMonitor::tick(per_band_rate)` flags when `candidates_per_min_zoomed_in > cap`; suppresses zoom-in emission for the duration of the breach + a configurable hysteresis.
- Zoom-out FP-cap breach down-ranks lowest-confidence candidates rather than suppressing entirely (zoom-out is the only source for far-field threats).
- `EgoMotionProvider` trait with the fixed contract; default `OpenCvEgoMotion` impl wraps task 23; `LearnedCvFallback` stub returns `not_engaged`.
- Build-time feature flag `learned_cv_fallback` reserves the slot; if off, the build is identical and the stub is the only provider.

## Scope

### Included
- `EgoMotionProvider` trait (re-exported from `shared::contracts`).
- `FpCapMonitor` with sliding-window per-band rate + hysteresis.
- Zoom-in suppression behaviour.
- Zoom-out down-rank behaviour.
- Stub `LearnedCvFallback` returning `not_engaged`.

### Excluded
- Real learned-CV implementation (Q14 follow-up, gated on benchmark results).
- Benchmark-gate orchestration (out of scope; manual decision based on benchmark data).

## Acceptance Criteria

**AC-1: Zoom-in suppression on flood**
Given `candidates_per_min_zoomed_in = 20` over 60 s while cap is 10
When the FP-cap monitor evaluates
Then zoom-in emission is suppressed and `health → yellow`; when rate falls below cap + hysteresis, emission resumes.

**AC-2: Zoom-out down-ranks instead of suppressing**
Given a similar zoom-out flood
When the monitor evaluates
Then no emission is suppressed; instead, the lowest-confidence candidates are down-ranked (counted as `down_ranked_total`).

**AC-3: Feature-flag absence does not break build**
Given the binary is built WITHOUT the `learned_cv_fallback` feature
When the build runs
Then the binary builds cleanly and `EgoMotionProvider` is satisfied by `OpenCvEgoMotion` exclusively.

**AC-4: Stub fallback returns not_engaged**
Given the `learned_cv_fallback` feature IS enabled and the stub is registered
When `LearnedCvFallback::estimate(...)` is called
Then it returns `Status::NotEngaged` immediately; no real ML is run.

## Non-Functional Requirements

**Reliability**
- FP-cap monitor never spuriously toggles (hysteresis required).

## Runtime Completeness

- **Named capability**: FP-cap monitor + Q14 fallback trait wiring. Real learned-CV impl is explicitly out of scope here.
- **Production code that must exist**: real FP-cap monitor + real suppression logic + real trait.
- **Allowed external stubs**: `LearnedCvFallback` is a stub by design until benchmark-gate triggers Q14.
- **Unacceptable substitutes**: silently dropping zoom-in candidates without an observable signal is unacceptable.
