Files
Oleksandr Bezdieniezhnykh c4eff40dbc [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>
2026-05-20 17:32:59 +03:00

7.8 KiB

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)