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:
parent
833ac84059
commit
c965e3e6c0
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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);
|
||||
|
|
@ -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. P2–P6 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 },
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue