Files
ruvnet--RuView/v2/crates/wifi-densepose-bfld/src/lib.rs
T
ruv ae6fd75095 feat(adr-118/p3.4): SoulMatchOracle + Recalibrate exemption (93/93 GREEN)
Iter 12. Wires the ADR-121 §2.6 Recalibrate exemption: when an enrolled
person_id matches the current high-separability cluster, the gate
downgrades the would-be Recalibrate to PredictOnly. The high score is
the *intended* outcome of a Soul Signature match, not an attacker-grade
sniffer arrival — so site_salt rotation is suppressed.

Added (no_std-compatible):
- src/coherence_gate.rs additions:
  * MatchOutcome enum: Match { person_id: u64 } | NotEnrolled | Suppressed
  * SoulMatchOracle trait with matches_enrolled() -> MatchOutcome
  * NullOracle (default-constructible, always reports NotEnrolled)
  * CoherenceGate::evaluate_with_oracle(score, ts, &O: SoulMatchOracle)
    — same hysteresis/debounce as evaluate(), but downgrades Recalibrate
    to PredictOnly when oracle returns Match { .. }
  * Refactored evaluate(): extracted advance_state(target, ts) shared with
    evaluate_with_oracle. evaluate is now a 4-line wrapper.
- pub use MatchOutcome, NullOracle, SoulMatchOracle from lib.rs

tests/soul_match_oracle.rs (8 named tests, all green):
  null_oracle_matches_default_evaluate_behavior
    (parameterized over 5 score points; oracle-aware and oracle-free
     gates produce identical trajectories)
  match_outcome_downgrades_recalibrate_to_predict_only
    (score=0.95 pends PredictOnly instead of Recalibrate)
  match_exemption_promotes_predict_only_after_debounce_not_recalibrate
    (after DEBOUNCE_NS, current is PredictOnly — never Recalibrate)
  match_outcome_does_not_affect_lower_actions
    (Reject pending stays Reject; oracle only intercepts Recalibrate)
  suppressed_outcome_does_not_exempt_recalibrate
    (Suppressed is functionally equivalent to NotEnrolled at the gate)
  not_enrolled_outcome_does_not_exempt_recalibrate
  match_outcome_carries_person_id
  null_oracle_default_constructor_works

ACs progressed:
- ADR-121 §2.6 fully covered as a stateless integration point — the
  hook is in place for the `--features soul-signature` Soul Signature
  crate (TBD) to plug in a real RaBitQ-backed oracle.
- ADR-118 §1.4 Soul Signature companion contract is now structurally
  enforced at the gate boundary: enrolled subjects do not trigger
  site_salt rotation; everyone else does.

Test config:
- cargo test --no-default-features → 64 passed (56 + 8)
- cargo test                       → 93 passed (85 + 8)

Out of scope (next iter target):
- BfldEvent struct (ADR-121 §2.1 output event JSON) — the downstream
  consumer of GateAction. Pairs the gate decision with presence/motion/
  person_count sensing fields.
- Optional: connect SoulMatchOracle into the actual `--features
  soul-signature` build (compile-time gate around a re-export).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 15:17:24 -04:00

153 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # BFLD — Beamforming Feedback Layer for Detection
//!
//! Privacy-gated WiFi sensing primitives derived from 802.11ac/ax Beamforming
//! Feedback Information (BFI). See [`docs/adr/ADR-118-bfld-beamforming-feedback-layer-for-detection.md`](../../../docs/adr/ADR-118-bfld-beamforming-feedback-layer-for-detection.md).
//!
//! ## Three structural invariants
//!
//! - **I1**: Raw BFI never exits the node.
//! - **I2**: Identity embedding is in-RAM-only.
//! - **I3**: Cross-site identity correlation is cryptographically impossible.
//!
//! Status: P1 in progress — frame format + sink marker traits. P2P6 follow.
#![cfg_attr(not(feature = "std"), no_std)]
pub mod coherence_gate;
pub mod embedding;
pub mod embedding_ring;
pub mod frame;
pub mod identity_risk;
#[cfg(feature = "std")]
pub mod payload;
#[cfg(feature = "std")]
pub mod privacy_gate;
pub mod sink;
pub use coherence_gate::{CoherenceGate, MatchOutcome, NullOracle, SoulMatchOracle};
pub use embedding::{IdentityEmbedding, EMBEDDING_DIM};
pub use embedding_ring::{EmbeddingRing, RING_CAPACITY};
pub use identity_risk::{score as identity_risk_score, GateAction};
pub use frame::{BfldFrameHeader, BFLD_MAGIC, BFLD_VERSION, BFLD_HEADER_SIZE};
#[cfg(feature = "std")]
pub use frame::BfldFrame;
#[cfg(feature = "std")]
pub use payload::BfldPayload;
#[cfg(feature = "std")]
pub use privacy_gate::PrivacyGate;
pub use sink::{check_class, LocalSink, MatterSink, NetworkSink, Sink};
/// Privacy classification carried in every `BfldFrame`. See ADR-120 §2.1.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PrivacyClass {
/// Local-only research data including raw BFI matrix. Never networked.
Raw = 0,
/// Operator-acknowledged research mode over LAN. Downsampled angles +
/// identity_embedding + identity_risk_score available. Required for
/// Soul Signature deployments (ADR-120 §2.7).
Derived = 1,
/// Production default: aggregate sensing only, no identity-derived fields.
Anonymous = 2,
/// Care-home / regulated deployments: class 2 minus risk score and hash.
Restricted = 3,
}
impl PrivacyClass {
/// Returns `true` if frames of this class may cross a `NetworkSink`.
/// Class 0 (`Raw`) is local-only by structural invariant I1.
#[must_use]
pub const fn allows_network(self) -> bool {
!matches!(self, Self::Raw)
}
/// Returns `true` if frames of this class may cross the Matter boundary.
/// Only classes 2 and 3 are Matter-eligible. See ADR-122 §2.4.
#[must_use]
pub const fn allows_matter(self) -> bool {
matches!(self, Self::Anonymous | Self::Restricted)
}
/// Returns the byte value of this class (0..=3) for serialization.
#[must_use]
pub const fn as_u8(self) -> u8 {
self as u8
}
}
impl TryFrom<u8> for PrivacyClass {
type Error = BfldError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Raw),
1 => Ok(Self::Derived),
2 => Ok(Self::Anonymous),
3 => Ok(Self::Restricted),
other => Err(BfldError::InvalidPrivacyClass(other)),
}
}
}
/// Errors produced by BFLD operations.
#[derive(Debug, thiserror::Error)]
pub enum BfldError {
/// Header magic did not match `BFLD_MAGIC`.
#[error("invalid BFLD magic: expected 0x{BFLD_MAGIC:08X}, got 0x{0:08X}")]
InvalidMagic(u32),
/// Header version unsupported.
#[error("unsupported BFLD version: {0}")]
UnsupportedVersion(u16),
/// Payload CRC32 mismatch — frame corrupted or tampered.
#[error("payload CRC mismatch: expected 0x{expected:08X}, got 0x{actual:08X}")]
Crc {
/// CRC value the header declared.
expected: u32,
/// CRC value computed over the received payload.
actual: u32,
},
/// Attempted to publish a class-0 (`Raw`) frame through a network sink.
/// Enforces structural invariant I1.
#[error("privacy violation: {reason}")]
PrivacyViolation {
/// `Sink::KIND` of the sink that rejected the frame.
reason: &'static str,
},
/// Byte value did not map to any defined `PrivacyClass` (0..=3).
#[error("invalid PrivacyClass byte: {0}")]
InvalidPrivacyClass(u8),
/// Buffer too short for header (86 bytes) or header + declared payload.
#[error("truncated frame: got {got} bytes, need at least {need}")]
TruncatedFrame {
/// Bytes available in the input buffer.
got: usize,
/// Bytes the header indicates are required.
need: usize,
},
/// Payload section length-prefix decoding failed or trailing bytes left over.
#[error("malformed payload section at offset {offset}: {reason}")]
MalformedSection {
/// Byte offset within the payload where parsing failed.
offset: usize,
/// Human-readable reason for the failure.
reason: &'static str,
},
/// Attempted to demote a frame to a class with MORE information than the
/// current class (lower numerical value). `demote` is monotonic; the only
/// way to add information back is to receive a fresh frame.
#[error("invalid demote: cannot move from class {from} to class {to}")]
InvalidDemote {
/// Source class byte value.
from: u8,
/// Refused target class byte value.
to: u8,
},
}