# Evidence Ladder + Zoom-In Candidate Handling

**Task**: AZ-684_scan_controller_evidence_ladder
**Name**: Evidence ladder Tier1 → Tier2 → optional VLM + source-zoom-band candidate dispatch
**Description**: When entering `ZoomedIn { roi }`, dispatch the ROI to `semantic_analyzer` (Tier 2) and, if `vlm_enabled` and concealment-score warrants, to `vlm_client`. Combine evidence into the surfaced POI. Handle `MovementCandidate { source_zoom_band }`: if `zoomed_in` and inside current ROI → bump confidence; if outside ROI but inside FOV → enqueue candidate-POI; only interrupt current hold if higher priority.
**Complexity**: 5 points
**Dependencies**: AZ-640_initial_structure, AZ-682_scan_controller_state_machine, AZ-683_scan_controller_poi_queue_and_window, AZ-660_detection_client_grpc_stream, AZ-671_semantic_analyzer_action_policy, AZ-672_vlm_client_provider_trait
**Component**: scan_controller
**Tracker**: AZ-684
**Epic**: AZ-635

## Problem

The system's brain combines three evidence sources at different cadences and costs: Tier 1 (per frame), Tier 2 (per zoom-in hold), Tier 3 / VLM (per endpoint hold, optional). The ladder is gated: VLM only fires when Tier 2 says "concealment warrants further inspection" AND `vlm_enabled`. Zoom-in MovementCandidates have priority semantics: a candidate in the current ROI bumps the current hold's confidence; outside the ROI it enqueues a candidate-POI; only a higher-priority candidate interrupts the current hold (per `description.md §4`).

## Outcome

- On `ZoomedOut → ZoomedIn { roi }` transition: emit `Tier2EvidenceRequest { roi }` to `semantic_analyzer`.
- On `Tier2Evidence` return: if `recommended_next_action ∈ {HoldEndpoint}` AND `vlm_enabled` AND concealment ≥ `vlm_invoke_threshold` → emit `VlmAssess { roi_crop, prompt }`; otherwise skip VLM (use Tier 1+2 evidence alone).
- Combine evidence into the surfaced POI (Tier 1 + Tier 2 evidence summary + VLM status).
- On `MovementCandidate { source_zoom_band: ZoomedIn }` while in `ZoomedIn { roi }`:
  - If candidate position ∈ current ROI → bump current-POI confidence; do NOT enqueue separately.
  - If candidate position ∉ current ROI but ∈ current zoomed FOV → enqueue as candidate-POI.
  - Interrupt current hold only if `candidate_priority > current_hold_priority`.
- `vlm_status` carried through to operator: `Ok | Inconclusive | Timeout | SchemaInvalid | IpcError | Disabled`.

## Scope

### Included
- Evidence dispatch on state transitions.
- VLM gating logic.
- Zoom-in candidate handling rules.
- Evidence-combination → POI.

### Excluded
- The state machine itself (task 43).
- Queue + cap (task 44).
- MapObjects dispatch (task 46).
- Gimbal issuance (task 47).
- The Tier-2 / VLM components (separate epics).

## Acceptance Criteria

**AC-1: ZoomedIn entry dispatches Tier 2**
Given the state transitions from `ZoomedOut` to `ZoomedIn { roi: R }`
When the transition completes
Then exactly one `Tier2EvidenceRequest { roi: R }` is dispatched.

**AC-2: VLM invoked when warranted + enabled**
Given `Tier2Evidence { recommended_next_action: HoldEndpoint, concealment: 0.85 }` arrives, `vlm_enabled = true`, `vlm_invoke_threshold = 0.7`
When the evidence is processed
Then exactly one `VlmAssess { roi_crop, prompt }` is dispatched.

**AC-3: VLM skipped when disabled**
Given the same evidence, but `vlm_enabled = false` (or VLM trait is `DisabledVlmProvider`)
When the evidence is processed
Then NO `VlmAssess` is dispatched; the surfaced POI carries `vlm_status: Disabled`.

**AC-4: In-ROI candidate bumps confidence**
Given `ZoomedIn { roi: R }` with current-POI confidence 0.6, and `MovementCandidate { source_zoom_band: ZoomedIn, position ∈ R }`
When the candidate is processed
Then the current POI's confidence is bumped (per the config rule); no new candidate-POI is enqueued.

**AC-5: Out-of-ROI in-FOV candidate enqueues**
Given the same state, and a candidate at position ∉ R but ∈ current FOV
When processed
Then a new candidate-POI is enqueued with `source_zoom_band: ZoomedIn` priority weighting.

**AC-6: Lower-priority candidate does NOT interrupt**
Given a current hold with priority 0.8 and a candidate with priority 0.5
When the candidate is processed
Then the current hold is NOT interrupted; the candidate is enqueued for later.

## Non-Functional Requirements

**Performance**
- Evidence-ladder dispatch latency: ≤5 ms p99 from trigger to emit.
- Tier 2 + VLM budgets enforced upstream (those components own their timeouts).

## Contract

- Canonical typed models: `data_model.md §POI`, `§MovementCandidate`, `§Tier2Evidence`, `§VlmAssessment`.

## Runtime Completeness

- **Named capability**: evidence ladder Tier 1 → Tier 2 → optional VLM with zoom-in candidate routing.
- **Production code that must exist**: real dispatch logic; real VLM gating; real candidate-routing rules.
- **Unacceptable substitutes**: always running VLM regardless of `vlm_enabled` or concealment score (wastes Tier-3 budget) is unacceptable.
