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