[AZ-683] scan_controller POI queue + 5/min cap + decision window
ci/woodpecker/push/build-arm Pipeline failed

Adds the prioritized POI queue on top of the AZ-682 FSM substrate:
priority = confidence x proximity x age_factor; rolling 60s window
caps surfaces at 5; confidence-scaled decision window (40% -> 30s,
100% -> 120s, linear; <40% never surfaces); tick() runs the timeout
sweep and silently forgets expired POIs (no IgnoredItem per spec);
DeclinePoi via operator command returns a DeclineAction for AZ-685
to persist.

ScanControllerHandle gains submit_poi_candidate /
next_poi_for_surface / decline_poi / poi_queue_len /
pois_in_window. submit_operator_cmd return type widens from
Result<()> to Result<SubmitOutcome>. ScanMetrics and health()
surface queue depth and counters.

Tests: 26 unit + 11 integration in scan_controller (all AC1..AC5 +
DeclinePoi end-to-end). Workspace clippy on scan_controller clean.
Pre-existing autopilot::Runtime::vlm_provider_name dead-code error
from batch 4 still open (see cumulative C5).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-20 09:04:29 +03:00
parent 745ab806f1
commit 9fe0bbeac9
10 changed files with 885 additions and 23 deletions
@@ -1,80 +0,0 @@
# POI Queue: Priority Ordering + 5/min Rate Cap + Confidence-Scaled Decision Window
**Task**: AZ-683_scan_controller_poi_queue_and_window
**Name**: POI queue ordered by confidence × proximity × age + 5/min hard cap + decision-window mapping
**Description**: Maintain the POI queue ordered by `confidence × proximity_to_current_camera × age_factor`. Hard-cap output to ≤5 POIs/min. Map confidence to operator-decision deadline: 40% → 30 s, 100% → 120 s, linear; below 40% the POI is NOT surfaced. Timeout → forget. Decline → IgnoredItem append (via dispatch in task 46).
**Complexity**: 5 points
**Dependencies**: AZ-640_initial_structure, AZ-682_scan_controller_state_machine
**Component**: scan_controller
**Tracker**: AZ-683
**Epic**: AZ-635
## Problem
The 5-POI/min cap is a product-level invariant (operator cognitive load). The decision window is confidence-scaled because the operator deserves more time on ambiguous targets and less on obvious ones. The priority ordering keeps the most actionable POI in front of the operator first. Confidence < 40 % means "don't surface" — silent suppression, not a low-priority enqueue.
## Outcome
- `PoiQueue` with ordered insertion by `priority = f(confidence, proximity, age)`.
- Hard cap: rolling 60-s window tracks POIs surfaced; new POI surface request blocked if cap would be exceeded; blocked POI stays in queue for later.
- `decision_window(confidence)` returns `Duration` per the linear 40 % → 30 s / 100 % → 120 s mapping; returns `None` for confidence < 40 %.
- Timeout → `forget` (POI removed from queue, no IgnoredItem).
- Decline command from `operator_bridge` → emit `MapObjectsAction::AppendIgnored(mgrs, class_group)` (dispatched by task 46).
- Health: `pois_in_queue`, `pois_per_min`.
## Scope
### Included
- Priority-ordered queue.
- Rate cap with rolling-window enforcement.
- Decision-window mapping.
- Timeout → forget.
### Excluded
- State machine (task 43).
- Evidence ladder (task 45).
- MapObjects dispatch (task 46).
- Gimbal issuance (task 47).
- Operator command dispatch (`operator_bridge` task 41).
## Acceptance Criteria
**AC-1: Priority ordering correct**
Given 3 POIs with `(confidence, proximity, age) = (0.9, 0.5, 0), (0.6, 0.9, 0), (0.7, 0.6, 60)`
When `next()` is called repeatedly
Then the order respects `confidence × proximity × age_factor`.
**AC-2: 5/min hard cap**
Given 10 POIs above 40 % confidence in 30 s
When the cap window is queried
Then at most 5 are surfaced; the rest remain queued until window rolls.
**AC-3: Decision window linear mapping**
Given confidences `[0.40, 0.70, 1.00]`
When `decision_window(c)` is called for each
Then the returned `Duration` is `[30 s, 75 s, 120 s]` (linear).
**AC-4: Sub-40 % not surfaced**
Given a POI with confidence 0.39
When considered for surface
Then `decision_window` returns `None`; the POI is NOT surfaced.
**AC-5: Timeout forgets**
Given a surfaced POI whose deadline expires with no operator action
When timeout fires
Then the POI is removed from the queue; no IgnoredItem is created.
## Non-Functional Requirements
**Performance**
- Queue insertion: ≤1 ms p99.
- Cap-window check: O(window_size) where window_size is small (5).
**Product**
- 5/min hard cap is a non-negotiable invariant from `description.md §8`.
## Runtime Completeness
- **Named capability**: priority POI queue + 5/min rate cap + confidence-scaled deadline.
- **Production code that must exist**: real priority math; real cap enforcement; real linear mapping.
- **Unacceptable substitutes**: ignoring the 5/min cap "for testing" in production is unacceptable.