[AZ-650] mission_executor pre-flight BIT (F9) gate (batch 8)

AZ-650 (mission_executor pre-flight Built-In Test):
- BitEvaluator trait + BitItemStatus { Pass, Degraded, Fail, Skipped }
  + BitReport + BitOverall fusion. Pluggable per-item evaluators so
  the composition root decides which dependencies are wired today.
- BitController owns evaluator list + mpsc ack channel + sticky-pass
  + ack deadline. Publishes bit_ok via tokio watch — composition root
  pipes it into the telemetry projection where the existing FSM
  bit_ok guard already consumes it (no FSM changes needed).
- BitState { Idle, Pass, AwaitingAck { report_id }, Failed { reason } }
  with broadcast::Sender<BitEvent> for operator-side observability.
  Sticky-pass semantics: once Pass is reached (directly or via signed
  ack on a Degraded report), the controller stops re-evaluating —
  BIT is a one-shot pre-flight gate, not a continuous monitor.
- BitDegradedAck arrives pre-validated by operator_bridge; the
  controller only matches report_id and applies the operator id to
  the audit log.
- Concrete evaluators landed today (3 of 12 spec items, the rest
  depend on components still in todo/):
  - StateDirFreeSpaceEvaluator (dir creatable/readable; statvfs is
    documented follow-up).
  - WallClockBoundEvaluator (chrono::Utc::now vs configurable bound).
  - MissionLoadedEvaluator (waypoint count via Arc<Mutex<usize>>).
  - MapObjectsSyncedEvaluator (maps SyncState -> BIT status per Q9).

Tests:
- ac1_all_pass_proceeds, ac2_fail_blocks_transition,
  ac3_degraded_requires_signed_ack (+ mismatched_ack supplement),
  ac4_degraded_ack_timeout_fails_the_bit — all 4 ACs green.
- Pure next_state table covered by lib unit tests.
- Per-evaluator unit tests for Pass/Fail/Degraded branches.

Quality gates:
- cargo fmt: clean.
- cargo clippy -p mission_executor --tests -- -D warnings: 0 warns.
- cargo test --workspace: all green.
- Pre-existing flake in state_machine::ac3_bounded_retry_then_success
  (batch 7 report) remains pre-existing — passes on rerun.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-19 19:12:48 +03:00
parent 2bcd4a8059
commit 8a4bd00526
15 changed files with 1373 additions and 47 deletions
@@ -120,16 +120,9 @@ pub struct LadderOutput {
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum LadderEvent {
StateChanged {
from: LadderState,
to: LadderState,
},
RtlIssued {
rtl_count: u64,
},
RtlSendFailed {
rtl_count: u64,
},
StateChanged { from: LadderState, to: LadderState },
RtlIssued { rtl_count: u64 },
RtlSendFailed { rtl_count: u64 },
}
/// Pure ladder logic. Stateful only across ticks; one `LostLinkLadder`
@@ -421,17 +414,17 @@ impl<C: LostLinkCommandIssuer + 'static> LostLinkDriver<C> {
}
/// Override the clock — only used in tests. Production omits this.
pub fn with_now_source(
mut self,
f: Arc<dyn Fn() -> Instant + Send + Sync>,
) -> Self {
pub fn with_now_source(mut self, f: Arc<dyn Fn() -> Instant + Send + Sync>) -> Self {
self.now_source = Some(f);
self
}
/// Spawn the driver task. Returns a read-side handle plus the
/// background task's join handle.
pub fn spawn(self, mut shutdown: watch::Receiver<bool>) -> (LostLinkLadderHandle, JoinHandle<()>) {
pub fn spawn(
self,
mut shutdown: watch::Receiver<bool>,
) -> (LostLinkLadderHandle, JoinHandle<()>) {
let (events_tx, _events_rx) = broadcast::channel::<LadderEvent>(64);
let ladder = Arc::new(Mutex::new(LostLinkLadder::new(self.config)));
let handle = LostLinkLadderHandle {