# 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)