//! Live trust-path bridge: drive the governed [`StreamingEngine`] from the //! sensing-server's live `NodeState` map. //! //! `multistatic_bridge.rs` already converts `NodeState` → `MultiBandCsiFrame` //! and runs the *bare* `MultistaticFuser`. That path produces fused amplitudes //! but skips the trust control plane: privacy demotion on contradiction, the //! WorldGraph belief with mandatory provenance, and the deterministic witness //! (ADR-135..146). This bridge routes the same live frames through //! [`StreamingEngine::process_cycle`], so every governed belief carries //! evidence + model + calibration + privacy decision and a BLAKE3 witness //! (narrowing the gap called out in ADR-136 §8 and the beyond-SOTA system //! review). //! //! ## Honest scope of the live-path governance //! //! The engine runs *alongside* the bare fusion path that feeds the live //! `SensingUpdate`; it does not replace it. What the engine's decision **does** //! gate on the live wire today: when a cycle is emitted at //! [`PrivacyClass::Restricted`] (base mode or contradiction/mesh-risk //! demotion), [`EngineBridge::suppress_raw_outputs`] is true and `main.rs` //! strips the per-node raw amplitude vectors from the published update — the //! same field mapping `wifi-densepose-bfld`'s privacy gate applies at //! `Restricted` (drop amplitude/phase proxies). Trust state (latest witness, //! effective class, recalibration flag, engine-error count) is readable on //! `GET /api/v1/status`. Gating of the remaining *derived* outputs //! (person count, classification, signal field) by privacy class is tracked //! as a follow-up; until then those fields are published ungoverned. //! //! Determinism: this module reads server state and forwards explicit //! timestamps/calibration ids; it introduces no wall-clock reads of its own, so //! a given `(frames, calibration, now_ms)` always yields the same //! [`TrustedOutput`] witness. use std::collections::HashMap; use std::time::{Duration, Instant}; use wifi_densepose_bfld::{PrivacyClass, PrivacyMode}; use wifi_densepose_engine::{AdapterInfo, EngineError, StreamingEngine, TrustedOutput}; use wifi_densepose_geo::types::GeoRegistration; use wifi_densepose_signal::ruvsense::fusion_quality::CalibrationId; use wifi_densepose_worldgraph::WorldId; use super::multistatic_bridge::node_frames_from_states; use super::NodeState; /// Minimum spacing between engine-error warn logs (errors are still counted /// every cycle; only the log line is rate-limited — a 20 Hz loop must not /// emit 20 warns/s). const ENGINE_ERROR_WARN_INTERVAL: Duration = Duration::from_secs(10); /// Owns a [`StreamingEngine`] and the WorldGraph scope (one room + sensor) the /// live sensing loop publishes beliefs into. pub struct EngineBridge { engine: StreamingEngine, room: WorldId, /// Nodes already wired into the WorldGraph as sensors (by `node_id`). registered_nodes: HashMap, /// Calibration epoch applied to live frames until the ADR-135 baseline /// stage supplies a real per-node id. Stable so witnesses are reproducible. calibration: CalibrationId, // ── Trust state observed from the most recent cycles (review finding 1: // previously write-only fields on AppState; now recorded here and // exposed via the status endpoint + output gating). ────────────────── /// BLAKE3 witness of the most recent successful governed cycle. last_witness: Option<[u8; 32]>, /// Latest drift→recalibration recommendation (ADR-135 → ADR-150 §3.4). recalibration_recommended: bool, /// Privacy class the most recent cycle was emitted under (post-demotion). effective_class: Option, /// Whether the most recent cycle was demoted (contradiction / mesh risk). demoted: bool, /// Total engine cycles that returned an error (previously swallowed by /// `if let Some(Ok(..))` at the call sites). engine_error_count: u64, /// Last time an engine error was actually logged (rate limiter). last_error_warn_at: Option, } impl EngineBridge { /// Build a bridge for one installation. `room_area_id`/`room_name` name the /// observation scope; `mode` is the starting privacy mode. pub fn new(mode: PrivacyMode, model_version: u16, room_area_id: &str, room_name: &str) -> Self { let mut engine = StreamingEngine::new(mode, model_version, GeoRegistration::default()); let room = engine.add_room(room_area_id, room_name); Self { engine, room, registered_nodes: HashMap::new(), calibration: CalibrationId(0x5256_0001), // "RV\0\x01" — placeholder epoch last_witness: None, recalibration_recommended: false, effective_class: None, demoted: false, engine_error_count: 0, last_error_warn_at: None, } } /// Override the calibration epoch stamped onto live frames (ADR-135). pub fn set_calibration(&mut self, calibration: CalibrationId) { self.calibration = calibration; } /// Override the WorldGraph belief-retention cap (bounds memory on the live /// loop; see `WorldGraph::prune_semantic_states`). pub fn set_semantic_retention(&mut self, max_states: usize) { self.engine.set_semantic_retention(max_states); } /// Switch the active privacy mode (operator/control-plane action). pub fn set_privacy_mode(&mut self, mode: PrivacyMode) { self.engine.set_privacy_mode(mode); } /// Activate a per-room calibration adapter (ADR-150 §3.4). The adapter's /// content-derived id becomes part of provenance/witness from the next /// cycle — weights can never swap silently on the live path. pub fn set_room_adapter(&mut self, info: AdapterInfo) { self.engine.set_room_adapter(info); } /// Deactivate the per-room adapter (revert to the shared base model). pub fn clear_room_adapter(&mut self) { self.engine.clear_room_adapter(); } /// Borrow the engine (queries, WorldGraph snapshot, privacy audit). pub fn engine(&self) -> &StreamingEngine { &self.engine } /// Number of sensor nodes wired into the WorldGraph so far. pub fn registered_node_count(&self) -> usize { self.registered_nodes.len() } /// Run one governed trust cycle over the current live node states. /// /// Returns `None` when no active node yields a frame (nothing to fuse — /// the engine is not invoked, so no spurious belief is published). On a /// real cycle it lazily wires any newly-seen node as a WorldGraph sensor, /// then returns the witnessed [`TrustedOutput`] (or a fusion error). /// /// `now_ms` is supplied by the caller (the sensing loop's clock), keeping /// the bridge deterministic and replayable. pub fn process_cycle_from_states( &mut self, node_states: &HashMap, now_ms: i64, ) -> Option> { let frames = node_frames_from_states(node_states); if frames.is_empty() { return None; } // Lazily register each contributing node as a sensor observing the room, // so the privacy rollup can suppress it under identity-strict modes. for f in &frames { self.registered_nodes.entry(f.node_id).or_insert_with(|| { self.engine .add_sensor(&format!("node-{}", f.node_id), self.room) }); } Some( self.engine .process_cycle(&frames, self.calibration, self.room, now_ms), ) } /// Run one governed cycle **and record the trust state** (review finding /// 1): on success the witness / effective class / demotion / /// recalibration flag are stored for the status endpoint and output /// gating; on error the error counter is incremented and a rate-limited /// warning is logged (never silently swallowed). Returns the trusted /// output on success, `None` when there was nothing to fuse or the cycle /// errored. pub fn observe_cycle( &mut self, node_states: &HashMap, now_ms: i64, ) -> Option { match self.process_cycle_from_states(node_states, now_ms)? { Ok(trust) => { self.last_witness = Some(trust.witness); self.recalibration_recommended = trust.recalibration_recommended; self.effective_class = Some(trust.effective_class); self.demoted = trust.demoted; Some(trust) } Err(e) => { self.engine_error_count += 1; let now = Instant::now(); let warn_due = self.last_error_warn_at.map_or(true, |t| { now.duration_since(t) >= ENGINE_ERROR_WARN_INTERVAL }); if warn_due { self.last_error_warn_at = Some(now); tracing::warn!( total_engine_errors = self.engine_error_count, "governed trust cycle failed (warn rate-limited to one per {:?}): {e}", ENGINE_ERROR_WARN_INTERVAL ); } None } } } /// BLAKE3 witness of the most recent successful governed cycle. pub fn last_trust_witness(&self) -> Option<[u8; 32]> { self.last_witness } /// Latest drift→recalibration recommendation from the governed engine. pub fn recalibration_recommended(&self) -> bool { self.recalibration_recommended } /// Privacy class the most recent cycle was emitted under (post-demotion); /// `None` until a governed cycle has run. pub fn effective_class(&self) -> Option { self.effective_class } /// Whether the most recent cycle was demoted (contradiction / mesh risk). pub fn demoted(&self) -> bool { self.demoted } /// Engine cycles that returned an error since startup. pub fn engine_error_count(&self) -> u64 { self.engine_error_count } /// ADR-141 output mapping for the live publish path (review finding 1c): /// at effective class [`PrivacyClass::Restricted`] the bfld privacy gate /// drops the amplitude + phase proxies; the live `SensingUpdate` applies /// the same field mapping by suppressing the per-node raw amplitude /// vectors when this returns true. Classes below `Restricted` leave the /// publish unchanged. pub fn suppress_raw_outputs(&self) -> bool { self.effective_class .is_some_and(|c| c.as_u8() >= PrivacyClass::Restricted.as_u8()) } } #[cfg(test)] mod tests { use super::*; use std::collections::VecDeque; use std::time::Instant; use wifi_densepose_bfld::PrivacyClass; fn node_state_with_history(amp: f64, n_sub: usize) -> NodeState { let mut ns = NodeState::new(); let frame: Vec = (0..n_sub).map(|i| amp + 0.1 * i as f64).collect(); ns.frame_history = VecDeque::from(vec![frame]); ns.last_frame_time = Some(Instant::now()); ns } fn two_node_states() -> HashMap { let mut m = HashMap::new(); m.insert(0u8, node_state_with_history(1.0, 56)); m.insert(1u8, node_state_with_history(1.05, 56)); m } #[test] fn empty_states_produce_no_belief() { let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "living_room", "Living Room"); let out = bridge.process_cycle_from_states(&HashMap::new(), 1_000); assert!(out.is_none()); // No belief published, no sensor wired. assert_eq!(bridge.registered_node_count(), 0); } #[test] fn live_cycle_produces_witnessed_belief_with_provenance() { let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "living_room", "Living Room"); let states = two_node_states(); let out = bridge .process_cycle_from_states(&states, 10_000) .expect("frames present") .expect("fusion succeeds"); // Full provenance: evidence + model + calibration + privacy decision. assert!(!out.provenance.evidence.is_empty()); assert_eq!(out.provenance.model_version, "rfenc-v1"); assert!(out.provenance.calibration_version.starts_with("cal:")); assert!(out.provenance.privacy_decision.starts_with("PrivateHome/")); // A witness was produced and the belief is in the WorldGraph. assert_ne!(out.witness, [0u8; 32]); assert!(bridge.engine().world().node(out.semantic_id).is_some()); // Both nodes are now wired as sensors. assert_eq!(bridge.registered_node_count(), 2); } #[test] fn live_path_is_deterministic() { let states = two_node_states_fixed(); let run = || { let mut b = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); b.process_cycle_from_states(&states, 5_000).unwrap().unwrap() }; let a = run(); let b = run(); assert_eq!(a.witness, b.witness); assert_eq!(a.provenance.calibration_version, b.provenance.calibration_version); assert_eq!(a.effective_class, b.effective_class); } // Deterministic node states (no wall-clock in amplitude/history). fn two_node_states_fixed() -> HashMap { let mut m = HashMap::new(); for (id, amp) in [(0u8, 1.0_f64), (1u8, 1.05)] { let mut ns = NodeState::new(); ns.frame_history = VecDeque::from(vec![(0..56) .map(|i| amp + 0.1 * i as f64) .collect::>()]); ns.last_frame_time = Some(Instant::now()); m.insert(id, ns); } m } #[test] fn nodes_registered_once_across_cycles() { let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); let states = two_node_states(); bridge.process_cycle_from_states(&states, 1_000); bridge.process_cycle_from_states(&states, 2_000); bridge.process_cycle_from_states(&states, 3_000); // Still exactly two sensors — idempotent registration. assert_eq!(bridge.registered_node_count(), 2); } #[test] fn retention_bounds_world_graph_growth() { let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); bridge.set_semantic_retention(5); let states = two_node_states(); for i in 0..20i64 { bridge.process_cycle_from_states(&states, 1_000 + i * 50); } // room + 2 sensors + at most 5 retained beliefs. assert!(bridge.engine().world().node_count() <= 3 + 5); } #[test] fn adapter_identity_flows_into_live_witness() { let states = two_node_states_fixed(); let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); let base = bridge .process_cycle_from_states(&states, 1_000) .unwrap() .unwrap(); bridge.set_room_adapter(AdapterInfo { adapter_id: "deadbeefcafef00d".into(), trained_samples: 120, }); let adapted = bridge .process_cycle_from_states(&states, 2_000) .unwrap() .unwrap(); assert!(adapted .provenance .model_version .ends_with("+adapter:deadbeefcafef00d")); assert_ne!(adapted.witness, base.witness); // Clearing reverts to the base model identity. bridge.clear_room_adapter(); let back = bridge .process_cycle_from_states(&states, 3_000) .unwrap() .unwrap(); assert_eq!(back.provenance.model_version, "rfenc-v1"); } /// Wiring (review finding 1): a live frame in → trust state recorded on /// the bridge (witness, effective class, recalibration flag), readable by /// the status endpoint, with a zero error count on the happy path. #[test] fn observe_cycle_records_trust_state() { let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); assert!(bridge.last_trust_witness().is_none()); assert_eq!(bridge.effective_class(), None); let out = bridge .observe_cycle(&two_node_states(), 1_000) .expect("two fresh nodes → governed cycle runs"); assert_eq!(bridge.last_trust_witness(), Some(out.witness)); assert_eq!(bridge.effective_class(), Some(out.effective_class)); assert_eq!( bridge.recalibration_recommended(), out.recalibration_recommended ); assert_eq!(bridge.demoted(), out.demoted); assert_eq!(bridge.engine_error_count(), 0); // PrivateHome clean cycle → Anonymous → raw outputs NOT suppressed. assert_eq!(bridge.effective_class(), Some(PrivacyClass::Anonymous)); assert!(!bridge.suppress_raw_outputs()); } /// Error wiring (review finding 1a): two live nodes with mismatched /// subcarrier counts make fusion return a `DimensionMismatch` → /// `EngineError` — previously dropped by `if let Some(Ok(..))` at the /// call sites. The counter must increment and the last good trust state /// must survive a later failure. #[test] fn observe_cycle_counts_engine_errors() { let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); let mut mismatched = HashMap::new(); mismatched.insert(0u8, node_state_with_history(1.0, 56)); mismatched.insert(1u8, node_state_with_history(1.05, 30)); // 30 ≠ 56 subcarriers assert!(bridge.observe_cycle(&mismatched, 1_000).is_none()); assert_eq!(bridge.engine_error_count(), 1); assert!( bridge.last_trust_witness().is_none(), "no witness from a failed cycle" ); assert!(bridge.observe_cycle(&mismatched, 2_000).is_none()); assert_eq!(bridge.engine_error_count(), 2); // A later good cycle records trust state; the audit count is kept. let out = bridge.observe_cycle(&two_node_states(), 3_000); assert!(out.is_some()); assert!(bridge.last_trust_witness().is_some()); assert_eq!(bridge.engine_error_count(), 2); // And a subsequent failure keeps the last good witness readable. assert!(bridge.observe_cycle(&mismatched, 4_000).is_none()); assert_eq!(bridge.engine_error_count(), 3); assert!(bridge.last_trust_witness().is_some()); } /// ADR-141 mapping (review finding 1c): a cycle emitted at class /// Restricted flips `suppress_raw_outputs`, which `main.rs` uses to strip /// per-node raw amplitude vectors from the live publish — the same field /// mapping bfld's privacy gate applies at `Restricted`. #[test] fn restricted_class_suppresses_raw_outputs() { let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); bridge.set_privacy_mode(PrivacyMode::StrictNoIdentity); // base = Restricted bridge .observe_cycle(&two_node_states(), 1_000) .expect("cycle runs"); assert_eq!(bridge.effective_class(), Some(PrivacyClass::Restricted)); assert!(bridge.suppress_raw_outputs()); } #[test] fn identity_strict_mode_is_carried_into_provenance() { let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R"); bridge.set_privacy_mode(PrivacyMode::StrictNoIdentity); let out = bridge .process_cycle_from_states(&two_node_states(), 7_000) .unwrap() .unwrap(); assert!(out.provenance.privacy_decision.starts_with("StrictNoIdentity/")); // Effective class is a valid privacy class (sanity). let _ = matches!( out.effective_class, PrivacyClass::Raw | PrivacyClass::Derived | PrivacyClass::Anonymous | PrivacyClass::Restricted ); } }