mirror of
https://github.com/ruvnet/RuView
synced 2026-08-08 20:11:43 +00:00
ae6fd75095
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>
153 lines
5.3 KiB
Rust
153 lines
5.3 KiB
Rust
//! # 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. P2–P6 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,
|
||
},
|
||
}
|