[AZ-680] [AZ-681] operator_bridge command dispatch + safety lane

Add the operator-command dispatcher behind a typed CommandAck:
60 s per-command-id idempotency cache, surfaced-POI registry with
unknown_poi_id + expired gates, BIT-degraded ack severity check, and
SafetyOverride forwarding to mission_executor with structured audit
log (redacts signature + session_token).

Cross-layer wiring goes through three new traits in shared::contracts
(ScanCommandRouter, MissionSafetyRouter, BitReportSeverityLookup) so
operator_bridge stays free of direct scan_controller / mission_executor
imports. scan_controller::ScanControllerHandle implements the scan
router; a new mission_executor::SafetyDispatchHandle wraps the BIT
ack channel + battery monitor handle and implements the safety router;
BitControllerHandle gains a bounded (16-entry) report-severity cache
for the lookup trait.

scan_controller also picks up ConfirmPoi handling: PoiQueue::confirm
removes the entry and SubmitOutcome::Confirmed carries the typed
(target_mgrs, target_class) hint for AZ-684/AZ-686 downstream.

Tests: 9 new integration tests in operator_bridge/tests/dispatcher.rs
cover AZ-680 AC-1..AC-5 + AZ-681 AC-1..AC-4. scan_controller adds 2
ConfirmPoi tests. All modified-crate suites green; one pre-existing
mission_executor state-machine test flake (already documented in
_docs/_process_leftovers) updated to note ac1 also affected.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-20 17:32:38 +03:00
parent aa4282f9f8
commit c4eff40dbc
24 changed files with 2017 additions and 53 deletions
@@ -0,0 +1,89 @@
# Batch Report
**Batch**: 17
**Cycle**: 1
**Tasks**: AZ-680, AZ-681
**Date**: 2026-05-20
## Task Results
| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|------|--------|---------------|-------|-------------|--------|
| AZ-680_operator_bridge_command_dispatch | Done | 14 files | scan_controller: 8 (2 new); operator_bridge: 20 lib + 9 integration; mission_executor: 35 lib | 5/5 ACs covered | None |
| AZ-681_operator_bridge_safety_and_bit_ack | Done | shared with AZ-680 | (counted above; 4 new integration tests cover AZ-681 ACs) | 4/4 ACs covered | None |
## AC Coverage map — AZ-680
| AC | Test | File | Notes |
|----|------|------|-------|
| AC-1 Confirm forwards target hint | `az680_ac1_confirm_forwards_to_scan_router` | `crates/operator_bridge/tests/dispatcher.rs` | Records POI in registry, dispatches `ConfirmPoi`, asserts `scan_router.route` invoked exactly once with the original command |
| AC-2 Re-transmit returns cached ack | `az680_ac2_retransmit_returns_cached_ack` | same file | Same `command_id` dispatched twice; second call returns `Ok` without re-invoking router (60 s `IdempotencyCache`) |
| AC-3 Unknown POI id rejected | `az680_ac3_unknown_poi_id_rejected` | same file | Asserts `CommandAck::Error { reason: "unknown_poi_id" }` and router never invoked |
| AC-4 Expired POI rejected | `az680_ac4_expired_poi_rejected` | same file | Pre-seeds a surfaced POI with past `deadline`; asserts `expired` ack and router not invoked |
| AC-5 Decline appends IgnoredItem via scan_controller | `az680_ac5_decline_forwards_to_scan_router` | same file | DeclinePoi dispatches into `scan_router.route` exactly once; ack `Ok` |
Plus scan_controller native coverage of the `ConfirmPoi` path (queue-side resolution): `confirm_poi_via_operator_command_emits_action` + `confirm_poi_unknown_id_is_validation_error` in `crates/scan_controller/tests/poi_queue.rs`.
## AC Coverage map — AZ-681
| AC | Test | File | Notes |
|----|------|------|-------|
| AC-1 BIT-DEGRADED ack succeeds | `az681_ac1_bit_degraded_ack_forwards` | `crates/operator_bridge/tests/dispatcher.rs` | Severity lookup returns `Some(true)`; safety_router.acknowledge_bit_degraded invoked exactly once with the report_id + operator_id |
| AC-2 BIT-FAIL ack rejected | `az681_ac2_bit_fail_ack_rejected` | same file | Severity lookup returns `Some(false)`; ack returns `cannot_acknowledge_fail`; safety_router not invoked |
| AC-3 Safety-override forwards with scope + duration | `az681_ac3_safety_override_forwards_with_audit_entry` | same file | SafetyOverride { BatteryRtl, 60s } dispatched; safety_router.apply_safety_override called once with the exact scope/duration; audit log contains exactly one matching `SafetyOverride` entry with `outcome: Ok` |
| AC-4 Audit log redacts secrets | `az681_ac4_audit_log_contains_no_signature_or_session_token` | same file | Every audit entry serialised to JSON; asserts no `signature` and no `session_token` substring. Lock-in: `AuditEntry` enum has no fields that could leak either secret |
## AC Test Coverage: All covered (9/9 across both tasks)
## Code Review Verdict: PASS (self-review — see findings below)
## Auto-Fix Attempts: 0
## Stuck Agents: None
## Files modified
```
M crates/shared/src/models/operator.rs (+SafetyOverrideScope)
M crates/shared/src/contracts/mod.rs (+ScanCommandRouter +MissionSafetyRouter +BitReportSeverityLookup)
M crates/scan_controller/Cargo.toml (+async-trait)
M crates/scan_controller/src/lib.rs (confirm_poi + ScanCommandRouter impl + SubmitOutcome::Confirmed)
M crates/scan_controller/src/internal/poi_queue/mod.rs (+ConfirmAction + PoiQueue::confirm)
M crates/scan_controller/tests/poi_queue.rs (+2 tests: confirm path; replaced exhaustive match with catch-all to handle new variant)
M crates/mission_executor/src/lib.rs (+pub use SafetyDispatchHandle)
M crates/mission_executor/src/internal/mod.rs (+safety_dispatch module)
A crates/mission_executor/src/internal/safety_dispatch.rs (NEW: MissionSafetyRouter impl)
M crates/mission_executor/src/internal/bit.rs (+bounded report_overalls FIFO; +report_overall + BitReportSeverityLookup impl on BitControllerHandle)
M crates/operator_bridge/src/lib.rs (registry+dispatcher wiring; with_scan_router/safety_router/bit_severity_lookup/audit_sink/dispatcher; dispatch_command; OperatorCommandSink impl now real; registry forget/record on dequeue/surface)
M crates/operator_bridge/src/internal/mod.rs (+audit +dispatcher +idempotency +poi_registry)
A crates/operator_bridge/src/ack.rs (NEW: CommandAck + ack_reasons)
A crates/operator_bridge/src/internal/audit.rs (NEW: AuditEntry / AuditSink / TracingAuditSink)
A crates/operator_bridge/src/internal/dispatcher.rs (NEW: OperatorCommandDispatcher + Builder)
A crates/operator_bridge/src/internal/idempotency.rs (NEW: IdempotencyCache 60s TTL)
A crates/operator_bridge/src/internal/poi_registry.rs (NEW: SurfacedPoi + SurfacedPoiRegistry)
A crates/operator_bridge/tests/dispatcher.rs (NEW: 9 integration tests)
M _docs/_process_leftovers/2026-05-20_mission_executor_ac3_flake.md (note: ac1 also flakes)
R _docs/02_tasks/todo/AZ-680_operator_bridge_command_dispatch.md → done/...
R _docs/02_tasks/todo/AZ-681_operator_bridge_safety_and_bit_ack.md → done/...
```
## Architecture notes
- The cross-component dispatch shape is now: `operator_bridge` (Layer 3) → `ScanCommandRouter` / `MissionSafetyRouter` / `BitReportSeverityLookup` traits in `shared::contracts` (Layer 1) → concrete impls on `ScanControllerHandle` and on the new `SafetyDispatchHandle` (constructed at the composition root from `BitController::ack_tx` + `BatteryMonitorHandle`).
- `BitControllerHandle` now retains a bounded FIFO of the last 16 `(report_id, overall)` pairs so `is_acknowledgeable` can answer for any report id observed in the current pre-flight gate cycle. Beyond that horizon, the dispatcher rejects with `unknown_bit_report` rather than guessing.
- `SafetyOverrideScope` is `#[non_exhaustive]` so future variants (`LinkLost`, `Geofence`) extend without breaking downstream matchers. `SafetyDispatchHandle::apply_safety_override` returns a typed Validation error on any unwired scope, so adding a variant to the enum without wiring the executor side fails closed.
- The audit log is a structured `tracing::info!` per entry by default (`TracingAuditSink`). The `AuditSink` trait keeps the door open for a file-based persistent sink later; integration tests substitute a recording sink.
- Idempotency cache TTL: 60 s per the task spec. Lazy eviction on each lookup/insert keeps the cache small without a background sweeper.
## Quality gates
- `cargo fmt --all`: clean
- `cargo clippy -p shared -p scan_controller -p mission_executor -p operator_bridge --all-targets -- -D warnings`: clean
- `cargo clippy --workspace --all-targets -- -D warnings`: pre-existing `Runtime::vlm_provider_name` dead-code lint (out-of-scope; tracked in `_docs/_process_leftovers/2026-05-20_autopilot_clippy.md`)
- `cargo test -p shared -p scan_controller -p operator_bridge -p mission_executor`: all green
- `cargo test --workspace`: one pre-existing flake — `mission_executor::ac1_multirotor_happy_path_reaches_done` (same `await_state` polling race as the documented `ac3` flake; passes on retry; leftover updated)
## Suggested next batch
From `_docs/02_tasks/_dependencies_table.md`, ready tasks after this batch:
- `AZ-659_frame_ingest_publisher` (3pt, no new deps) — was eligible for this batch but excluded for cohesion
- `AZ-682_scan_controller_state_machine_skeleton` follow-ups (AZ-684 evidence ladder) once `scan_controller` confirm path lands the FSM-side follow-through
- `AZ-685_mapobjects_store_ignored_items` (consumes the `DeclineAction` payload AZ-680 now produces end-to-end)