Files
ruvnet--RuView/v2/crates/wifi-densepose-bfld/src/sink.rs
T
ruv eb996294fb feat(adr-118/p1.3): Sink marker traits + PrivacyClass::try_from (17/17 GREEN)
Iter 3. Lands the structural enforcement of ADR-118 invariant I1
("raw BFI never exits the node") and ADR-120 §2.2 ("Sink marker types").

Added:
- src/sink.rs:
  * Sink trait with MIN_CLASS and KIND associated constants
  * LocalSink (Raw OK), NetworkSink (Derived+ only), MatterSink (Anonymous+)
  * Hierarchy: MatterSink: NetworkSink (every Matter sink is a NetworkSink)
  * check_class<S>(class) runtime gate, returns PrivacyViolation{reason:KIND}
  * Zero-sized kind tags: LocalKind / NetworkKind / MatterKind
- PrivacyClass::as_u8() const helper
- TryFrom<u8> for PrivacyClass (0..=3 valid; 4..=255 → InvalidPrivacyClass)
- BfldError::InvalidPrivacyClass(u8) variant

tests/sink_enforcement.rs adds 8 tests:
  privacy_class_try_from_accepts_all_four_valid_bytes
  privacy_class_try_from_rejects_out_of_range_bytes
  privacy_class_byte_roundtrip_is_stable
  local_sink_accepts_all_classes
  network_sink_rejects_raw_frames
  network_sink_accepts_derived_anonymous_restricted
  matter_sink_rejects_raw_and_derived
  matter_sink_accepts_anonymous_and_restricted

Out of scope (next iter):
- BfldFrame (header + payload + section length-prefixes + CRC32 over payload)
  — needs the `crc` crate dependency.
- PrivacyGate::demote(frame, target_class) transformer (ADR-120 §2.4).
- compile-fail test that proves a sink-trait bound rejects Raw at compile
  time — needs `trybuild` integration; deferred to a separate iter.

cargo test -p wifi-densepose-bfld --no-default-features → 17 passed, 0 failed
  (3 frame_header_size + 6 header_roundtrip + 8 sink_enforcement)

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

93 lines
3.6 KiB
Rust

//! Sink marker traits — structural enforcement of invariant I1.
//!
//! Every output destination (memory buffer, MQTT topic, Matter cluster) implements
//! exactly one of [`LocalSink`], [`NetworkSink`], or [`MatterSink`]. The associated
//! constant [`Sink::MIN_CLASS`] declares the lowest `PrivacyClass` value that sink
//! is willing to accept; the runtime gate [`check_class`] enforces this on every
//! publish.
//!
//! Mapping (ADR-120 §2.2, ADR-122 §2.4):
//!
//! | Sink trait | `MIN_CLASS` | Accepts classes |
//! |---------------|----------------------|-----------------|
//! | `LocalSink` | `PrivacyClass::Raw` | 0, 1, 2, 3 |
//! | `NetworkSink` | `PrivacyClass::Derived` | 1, 2, 3 |
//! | `MatterSink` | `PrivacyClass::Anonymous` | 2, 3 |
//!
//! `MatterSink: NetworkSink` — every Matter sink is also a network sink.
use crate::{BfldError, PrivacyClass};
/// Base sink trait. Every sink type declares the minimum `PrivacyClass` it accepts.
pub trait Sink {
/// Lowest privacy class (highest information density) this sink will publish.
const MIN_CLASS: PrivacyClass;
/// Human-readable sink kind, used in `BfldError::PrivacyViolation` messages.
const KIND: &'static str;
}
/// Marker for sinks that stay on the originating node (memory, in-RAM channel,
/// local file with explicit operator opt-in). Accepts every class including `Raw`.
pub trait LocalSink: Sink {}
/// Marker for sinks that cross the node boundary (MQTT, HTTP, gRPC). Rejects
/// `Raw` frames by structural invariant I1.
pub trait NetworkSink: Sink {}
/// Marker for sinks that bridge into the Matter cluster surface. Rejects `Raw`
/// and `Derived`; the `cog-ha-matter` boundary filter consumes only classes 2/3.
pub trait MatterSink: NetworkSink {}
/// Runtime gate. Returns `Ok(())` if `class` is acceptable for `S`, otherwise
/// returns `BfldError::PrivacyViolation` with the offending sink kind.
///
/// Class numerical order *is* meaningful here: a sink that accepts `MIN_CLASS`
/// also accepts every higher-numbered class (less identity content). The check
/// is therefore a simple `>=` on the byte representation.
pub fn check_class<S: Sink>(class: PrivacyClass) -> Result<(), BfldError> {
if class.as_u8() >= S::MIN_CLASS.as_u8() {
Ok(())
} else {
Err(BfldError::PrivacyViolation {
reason: S::KIND,
})
}
}
// --- Default sink types ----------------------------------------------------
//
// Concrete sinks live in downstream crates (emitter.rs, mqtt.rs, the cog-ha-matter
// Matter bridge). These three "kind tags" are convenient zero-sized stand-ins for
// unit tests and for the privacy_gate compile-time tables.
/// Zero-sized tag: a local in-memory ring buffer or file sink.
#[derive(Debug, Clone, Copy, Default)]
pub struct LocalKind;
impl Sink for LocalKind {
const MIN_CLASS: PrivacyClass = PrivacyClass::Raw;
const KIND: &'static str = "LocalKind";
}
impl LocalSink for LocalKind {}
/// Zero-sized tag: a generic network sink (MQTT, HTTP, gRPC).
#[derive(Debug, Clone, Copy, Default)]
pub struct NetworkKind;
impl Sink for NetworkKind {
const MIN_CLASS: PrivacyClass = PrivacyClass::Derived;
const KIND: &'static str = "NetworkKind";
}
impl NetworkSink for NetworkKind {}
/// Zero-sized tag: the Matter cluster boundary in `cog-ha-matter`.
#[derive(Debug, Clone, Copy, Default)]
pub struct MatterKind;
impl Sink for MatterKind {
const MIN_CLASS: PrivacyClass = PrivacyClass::Anonymous;
const KIND: &'static str = "MatterKind";
}
impl NetworkSink for MatterKind {}
impl MatterSink for MatterKind {}