//! `BfldEvent` — privacy-gated output event. ADR-121 §2.1, ADR-122 §2.1. //! //! Field exposure per privacy_class (ADR-122 §2.1): //! //! | Field | Raw(0) | Derived(1) | Anonymous(2) | Restricted(3) | //! |------------------------|--------|------------|--------------|---------------| //! | presence | y | y | y | y | //! | motion | y | y | y | y | //! | person_count | y | y | y | y | //! | confidence | y | y | y | y | //! | zone_id | y | y | y | y | //! | identity_risk_score | y | y | **y** | **n** | //! | rf_signature_hash | y | y | **y** | **n** | //! //! Construction defers to [`BfldEvent::with_privacy_gating`] which applies //! the policy by stripping disallowed fields to `None` based on the supplied //! `privacy_class`. Direct field access remains possible (for unit tests), //! but the JSON serializer always honors the gating because the dropped //! fields are `None` and the `Serialize` derive uses `skip_serializing_if`. #![cfg(feature = "std")] use crate::PrivacyClass; #[cfg(feature = "serde-json")] use serde::Serialize; /// Privacy-gated output event published by the BFLD pipeline. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde-json", derive(Serialize))] pub struct BfldEvent { /// Always `"bfld_update"`. Tags the event type for downstream routers. #[cfg_attr(feature = "serde-json", serde(rename = "type"))] pub event_type: &'static str, /// Originating BFLD node identifier. pub node_id: String, /// Monotonic capture-clock timestamp in nanoseconds. pub timestamp_ns: u64, /// Whether an occupant is present in the sensing zone. pub presence: bool, /// Normalized motion magnitude in `[0.0, 1.0]`. pub motion: f32, /// Estimated number of occupants. pub person_count: u8, /// Sensing confidence in `[0.0, 1.0]`. pub confidence: f32, /// Optional zone identifier; absent if the deployment is single-zone. #[cfg_attr(feature = "serde-json", serde(skip_serializing_if = "Option::is_none"))] pub zone_id: Option, /// Privacy classification byte for this event. #[cfg_attr(feature = "serde-json", serde(serialize_with = "ser_privacy_class"))] pub privacy_class: PrivacyClass, /// Identity-risk score, `[0.0, 1.0]`. Class 2 only; `None` at class 3. #[cfg_attr(feature = "serde-json", serde(skip_serializing_if = "Option::is_none"))] pub identity_risk_score: Option, /// 256-bit BLAKE3 keyed hash of the current cluster. Class 2 only; `None` at class 3. #[cfg_attr(feature = "serde-json", serde(skip_serializing_if = "Option::is_none"))] pub rf_signature_hash: Option<[u8; 32]>, } impl BfldEvent { /// Build an event from sensing fields, applying the privacy_class policy /// to mask identity-derived fields. `identity_risk_score` and /// `rf_signature_hash` are nulled out at class `Restricted`. #[must_use] pub fn with_privacy_gating( node_id: String, timestamp_ns: u64, presence: bool, motion: f32, person_count: u8, confidence: f32, zone_id: Option, privacy_class: PrivacyClass, identity_risk_score: Option, rf_signature_hash: Option<[u8; 32]>, ) -> Self { let mut e = Self { event_type: "bfld_update", node_id, timestamp_ns, presence, motion, person_count, confidence, zone_id, privacy_class, identity_risk_score, rf_signature_hash, }; e.apply_privacy_gating(); e } /// Idempotently mask fields disallowed at the current `privacy_class`. /// Called by [`Self::with_privacy_gating`]; exposed for callers that /// mutate the event in place before publication. pub fn apply_privacy_gating(&mut self) { if self.privacy_class.as_u8() >= PrivacyClass::Restricted.as_u8() { self.identity_risk_score = None; self.rf_signature_hash = None; } } /// Serialize to canonical JSON. Fields masked by privacy gating are omitted /// entirely (not emitted as `null`), so a privacy-gated event is /// observationally indistinguishable from one that never had the field set. #[cfg(feature = "serde-json")] pub fn to_json(&self) -> Result { serde_json::to_string(self) } } #[cfg(feature = "serde-json")] fn ser_privacy_class( class: &PrivacyClass, s: S, ) -> Result { let name = match class { PrivacyClass::Raw => "raw", PrivacyClass::Derived => "derived", PrivacyClass::Anonymous => "anonymous", PrivacyClass::Restricted => "restricted", }; s.serialize_str(name) }