feat(adr-118/p1): scaffold wifi-densepose-bfld crate + frame header (3/3 tests GREEN)

Land P1 of the BFLD rollout — the wire-format primitives:

- New workspace member: v2/crates/wifi-densepose-bfld
- PrivacyClass enum (Raw/Derived/Anonymous/Restricted) with allows_network()
  and allows_matter() const helpers reflecting ADR-120 §2.2 and ADR-122 §2.4
- BfldFrameHeader (#[repr(C, packed)]) per ADR-119 §2.1
- BFLD_MAGIC = 0xBF1D_0001, BFLD_VERSION = 1
- BfldError variants for InvalidMagic / UnsupportedVersion / Crc / PrivacyViolation
- soul-signature cargo feature (gated, default OFF) per ADR-118 §1.4
- Compile-time size assertion via static_assertions::const_assert_eq!
- 3 acceptance tests in tests/frame_header_size.rs (all pass)

Bug fix:
- ADR-119 AC1 claimed BfldFrameHeader is 40 bytes. Actual packed layout sums
  to 86 bytes. Updated AC1 and §2.1 prose to match. const_assert in frame.rs
  pins the value structurally — a future field addition that breaks the size
  fails to compile.

Out of scope for this iter (deferred to later P1 commits):
- Field-level missing-docs warnings (21) — addressed alongside accessor helpers
- Payload section parsing — needs the section-length prefix tests
- Round-trip serialize/parse — covered by a fixture-based test in the next iter

cargo test -p wifi-densepose-bfld --no-default-features → 3 passed, 0 failed

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-05-24 13:34:05 -04:00
parent 833ac84059
commit c965e3e6c0
7 changed files with 222 additions and 2 deletions

View File

@ -57,7 +57,7 @@ pub struct BfldFrameHeader {
}
```
Total header size: 40 bytes (validated by `static_assertions::const_assert_eq!`).
Total header size: **86 bytes packed** (validated by `static_assertions::const_assert_eq!` in `wifi-densepose-bfld/src/frame.rs`). Earlier drafts stated 40 bytes — that was a counting error caught during P1 scaffold; see AC1 below.
### 2.2 Payload structure
@ -144,7 +144,7 @@ Rejected: CRC must be computed after the payload, so its value would otherwise f
## 5. Acceptance Criteria
- [ ] **AC1**: `BfldFrameHeader` size is exactly 40 bytes on x86_64, aarch64, and xtensa-esp32s3.
- [ ] **AC1**: `BfldFrameHeader` size is exactly **86 bytes** (packed) on x86_64, aarch64, and xtensa-esp32s3. The size was initially documented as 40 bytes during ADR drafting — that was a counting error; the implementation in `wifi-densepose-bfld/src/frame.rs` enforces the correct value via `const_assert_eq!`.
- [ ] **AC2**: 1,000 serializations of a fixed `BfiCapture` fixture produce a bit-identical BLAKE3 hash.
- [ ] **AC3**: `privacy_class = 0` frame returned through `NetworkSink::publish()` returns `Err(BfldError::PrivacyViolation)`.
- [ ] **AC4**: Payload CRC32 mismatch causes `BfldFrame::parse()` to return `Err(BfldError::Crc)` without exposing partial payload state.

15
v2/Cargo.lock generated
View File

@ -7255,6 +7255,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strength_reduce"
version = "0.2.4"
@ -9133,6 +9139,15 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]]
name = "wifi-densepose-bfld"
version = "0.3.0"
dependencies = [
"proptest",
"static_assertions",
"thiserror 2.0.18",
]
[[package]]
name = "wifi-densepose-cli"
version = "0.3.0"

View File

@ -42,6 +42,11 @@ members = [
# ADR-115 MQTT publisher as a Seed-installable artifact with
# mDNS, embedded broker, RuVector thresholds, Ed25519 witness.
"crates/cog-ha-matter",
# ADR-118: BFLD — Beamforming Feedback Layer for Detection. The
# privacy/safety layer that measures and gates identity leakage from
# WiFi BFI captures. Sub-ADRs: 119 (frame), 120 (privacy class),
# 121 (identity risk), 122 (HA/Matter), 123 (capture path).
"crates/wifi-densepose-bfld",
# rvCSI — edge RF sensing runtime (ADR-095 platform, ADR-096 FFI/crate layout):
# lives in its own repo (https://github.com/ruvnet/rvcsi), vendored here as
# `vendor/rvcsi` and published to crates.io as `rvcsi-*` 0.3.x. Depend on the

View File

@ -0,0 +1,38 @@
[package]
name = "wifi-densepose-bfld"
description = "BFLD — Beamforming Feedback Layer for Detection. Privacy-gated WiFi BFI sensing primitives. See ADR-118."
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
documentation.workspace = true
keywords.workspace = true
categories.workspace = true
[features]
default = ["std"]
std = []
# Soul Signature integration (ADR-118 §1.4, ADR-120 §2.7, ADR-121 §2.6) —
# enables privacy_class = 1 (derived) mode and the SoulMatchOracle gate
# exemption. Disabled by default per the structural class-2 default.
soul-signature = []
[dependencies]
thiserror.workspace = true
static_assertions = "1.1"
[dev-dependencies]
proptest.workspace = true
[lints.rust]
unsafe_code = "forbid"
missing_docs = "warn"
[lints.clippy]
all = "warn"
pedantic = "warn"
nursery = "warn"
module_name_repetitions = "allow"
missing_const_for_fn = "allow"
missing_panics_doc = "allow"

View File

@ -0,0 +1,63 @@
//! `BfldFrame` wire-format primitives. See ADR-119.
//!
//! The header is `#[repr(C, packed)]` so the wire byte order is fixed across
//! x86_64, aarch64, and xtensa-esp32s3 — and so the witness-bundle pattern
//! (ADR-028) extends cleanly to BFLD frames.
use static_assertions::const_assert_eq;
/// Magic value identifying a `BfldFrame`. Reads as "BFLD" in hex-dump tools.
pub const BFLD_MAGIC: u32 = 0xBF1D_0001;
/// Current `BfldFrame` major version. Bumps on any incompatible layout change.
pub const BFLD_VERSION: u16 = 1;
/// Size of the packed header in bytes. Asserted at compile time below.
///
/// Note: ADR-119 AC1 initially claimed 40 bytes — that was a counting error.
/// Actual packed layout sums to 86. Updated 2026-05-24 to match implementation.
pub const BFLD_HEADER_SIZE: usize = 86;
/// Flag bits in `BfldFrameHeader::flags`. See ADR-119 §2.1.
pub mod flags {
/// Payload contains an optional CSI delta section.
pub const HAS_CSI_DELTA: u16 = 1 << 0;
/// `privacy_mode` is engaged: identity-derived fields suppressed.
pub const PRIVACY_MODE: u16 = 1 << 1;
/// ESP32-S3 self-only adapter (ADR-123 §2.5): no `identity_risk_score`.
pub const SELF_ONLY: u16 = 1 << 3;
}
/// On-the-wire BFLD frame header. 86 bytes, little-endian, packed.
///
/// All multi-byte integer fields are little-endian when serialized. The packed
/// layout guarantees zero internal padding; readers must use `read_unaligned`
/// (or the accessor helpers added in a later commit).
#[repr(C, packed)]
#[derive(Debug, Clone, Copy)]
pub struct BfldFrameHeader {
pub magic: u32,
pub version: u16,
pub flags: u16,
pub timestamp_ns: u64,
pub ap_hash: [u8; 16],
pub sta_hash: [u8; 16],
pub session_id: [u8; 16],
pub channel: u16,
pub bandwidth_mhz: u16,
pub rssi_dbm: i16,
pub noise_floor_dbm: i16,
pub n_subcarriers: u16,
pub n_tx: u8,
pub n_rx: u8,
pub quantization: u8,
pub privacy_class: u8,
pub payload_len: u32,
pub payload_crc32: u32,
}
const_assert_eq!(core::mem::size_of::<BfldFrameHeader>(), BFLD_HEADER_SIZE);

View File

@ -0,0 +1,71 @@
//! # 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 scaffold — frame format only. P2P6 land in subsequent commits.
#![cfg_attr(not(feature = "std"), no_std)]
pub mod frame;
pub use frame::{BfldFrameHeader, BFLD_MAGIC, BFLD_VERSION, BFLD_HEADER_SIZE};
/// 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)
}
}
/// 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 { expected: u32, actual: u32 },
/// Attempted to publish a class-0 (`Raw`) frame through a network sink.
/// Enforces structural invariant I1.
#[error("privacy violation: {reason}")]
PrivacyViolation { reason: &'static str },
}

View File

@ -0,0 +1,28 @@
//! Acceptance test ADR-119 AC1: `BfldFrameHeader` size is platform-stable.
//!
//! The static assertion in `frame.rs` already enforces this at compile time on
//! the local target. This runtime test exists so CI surfaces the failure with
//! a useful message rather than a `const_assert_eq!` link error.
use wifi_densepose_bfld::{BfldFrameHeader, BFLD_HEADER_SIZE, BFLD_MAGIC, BFLD_VERSION};
#[test]
fn header_size_is_86_bytes() {
assert_eq!(
core::mem::size_of::<BfldFrameHeader>(),
BFLD_HEADER_SIZE,
"BfldFrameHeader must be exactly {BFLD_HEADER_SIZE} bytes (packed)",
);
}
#[test]
fn magic_reads_as_bfld_in_hex() {
// 0xBF1D_0001 — "BF1D" looks like "BFLD" in xxd output; final 0001 is the
// major version that lives in the dedicated `version` field as well.
assert_eq!(BFLD_MAGIC, 0xBF1D_0001);
}
#[test]
fn version_is_one() {
assert_eq!(BFLD_VERSION, 1);
}