From a3d26a4fade0b4abb406e301e90634de67d6390e Mon Sep 17 00:00:00 2001 From: ruv Date: Sun, 24 May 2026 18:55:04 -0400 Subject: [PATCH] feat(adr-118/p1.7): reserved-flag-bits forward-compat (243/243 GREEN) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iter 36. Locks down the ADR-119 §2.1 forward-compat promise that reserved flag bits round-trip unchanged through the parser. A future protocol revision may light up bits 2 or 4..=15; today's parser preserves them so a node running iter N can forward unknown bits to a peer running iter N+M without losing information. Added (in src/frame.rs::flags): - pub const KNOWN_FLAGS_MASK = HAS_CSI_DELTA | PRIVACY_MODE | SELF_ONLY (the three currently-named flags, occupying bits 0, 1, 3) - pub const RESERVED_FLAGS_MASK = !KNOWN_FLAGS_MASK (bit 2 + bits 4..=15 — every position not currently assigned) - Docstrings reference ADR-119 §2.1 verbatim so a future reviewer understands why the constants exist. tests/reserved_flags.rs (8 named tests, all green, no_std-compatible so they run in BOTH feature configs): known_flags_mask_covers_exactly_three_named_flags (count_ones() == 3 catches accidental flag additions that should also update KNOWN_FLAGS_MASK) reserved_and_known_masks_are_complementary (mask | reserved == u16::MAX; mask & reserved == 0) known_flags_do_not_overlap_with_each_other (HAS_CSI_DELTA, PRIVACY_MODE, SELF_ONLY all on distinct bits) header_preserves_reserved_flag_bits_through_round_trip *** Headline test: set RESERVED_FLAGS_MASK on a header, serialize, parse, verify the bits survived. *** header_preserves_mixed_known_and_reserved_bits (HAS_CSI_DELTA | PRIVACY_MODE | (1<<7) | (1<<14) — mixed case) reserved_bits_do_not_collide_with_self_only_bit_3 (bit 2 is reserved but bit 3 is named — pins the asymmetry) all_zero_flags_round_trip_cleanly all_one_flags_round_trip_cleanly (stress: every bit set) The new tests are no_std-compatible (no Vec / no serde) so they run in both `cargo test --no-default-features` and default feature configs. The no_default test count therefore jumps from 72 to 80. ADR-124 status (iter step 0 sibling check): - docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md unchanged at 431 lines. SENSE-BRIDGE scope remains orthogonal. ACs progressed: - ADR-119 §2.1 "Reserved flag bits 2-15 lock in future-extension order; any new bit assignment is a version bump." — the test now enforces the OTHER half of this contract: a peer running the future version can set a reserved bit and our parser will preserve it through the round-trip rather than masking it off. Test config: - cargo test --no-default-features → 80 passed (72 + 8 no_std-compat) - cargo test → 243 passed (235 + 8) Out of scope (next iter target): - PR-readiness pivot: witness bundle regeneration, CHANGELOG batch across iters 1-36, AC closeout table for the PR description. All in-crate ACs are now covered; remaining work is either external-resource-gated (KIT BFId, Pi5/Nexmon) or PR-prep. Co-Authored-By: claude-flow --- v2/crates/wifi-densepose-bfld/src/frame.rs | 14 +++ .../tests/reserved_flags.rs | 95 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 v2/crates/wifi-densepose-bfld/tests/reserved_flags.rs diff --git a/v2/crates/wifi-densepose-bfld/src/frame.rs b/v2/crates/wifi-densepose-bfld/src/frame.rs index c9c68459..e922270e 100644 --- a/v2/crates/wifi-densepose-bfld/src/frame.rs +++ b/v2/crates/wifi-densepose-bfld/src/frame.rs @@ -47,6 +47,20 @@ pub mod flags { 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; + + /// Bitmask covering every named flag this version of the crate knows + /// about. Useful for "did the wire form set any flags I don't recognize?" + /// forward-compat checks. + pub const KNOWN_FLAGS_MASK: u16 = HAS_CSI_DELTA | PRIVACY_MODE | SELF_ONLY; + + /// Complement of [`KNOWN_FLAGS_MASK`] — every bit position not currently + /// assigned a meaning. Bits set in this mask MUST round-trip unchanged + /// per ADR-119 §2.1 ("Reserved flag bits 2-15 lock in future-extension + /// order; any new bit assignment is a version bump"). A future protocol + /// revision may light these up; today's parser preserves them so a node + /// running iter N can forward unknown bits to a peer running iter N+M + /// without losing information. + pub const RESERVED_FLAGS_MASK: u16 = !KNOWN_FLAGS_MASK; } /// On-the-wire BFLD frame header. 86 bytes, little-endian, packed. diff --git a/v2/crates/wifi-densepose-bfld/tests/reserved_flags.rs b/v2/crates/wifi-densepose-bfld/tests/reserved_flags.rs new file mode 100644 index 00000000..52d7acee --- /dev/null +++ b/v2/crates/wifi-densepose-bfld/tests/reserved_flags.rs @@ -0,0 +1,95 @@ +//! ADR-119 §2.1 reserved-flag-bits forward-compat. The 16-bit `flags` field +//! currently uses bits 0 (HAS_CSI_DELTA), 1 (PRIVACY_MODE), and 3 (SELF_ONLY). +//! Bits 2 and 4..=15 are reserved. The parser must preserve any reserved bit +//! set by a future peer — otherwise round-tripping a frame through a node +//! running an older crate version silently drops information that a newer +//! peer might depend on. + +use wifi_densepose_bfld::frame::flags; +use wifi_densepose_bfld::{BfldFrameHeader, BFLD_HEADER_SIZE}; + +fn header_with_flags(flags_value: u16) -> BfldFrameHeader { + let mut h = BfldFrameHeader::empty(); + h.flags = flags_value; + h +} + +#[test] +fn known_flags_mask_covers_exactly_three_named_flags() { + assert_eq!( + flags::KNOWN_FLAGS_MASK, + flags::HAS_CSI_DELTA | flags::PRIVACY_MODE | flags::SELF_ONLY, + ); + // The three currently-named flags occupy bits 0, 1, 3 — three bits set. + assert_eq!(flags::KNOWN_FLAGS_MASK.count_ones(), 3); +} + +#[test] +fn reserved_and_known_masks_are_complementary() { + assert_eq!(flags::KNOWN_FLAGS_MASK | flags::RESERVED_FLAGS_MASK, u16::MAX); + assert_eq!(flags::KNOWN_FLAGS_MASK & flags::RESERVED_FLAGS_MASK, 0); +} + +#[test] +fn known_flags_do_not_overlap_with_each_other() { + // Each named flag uses exactly one bit and no two of them share a bit. + let pairs = [ + (flags::HAS_CSI_DELTA, flags::PRIVACY_MODE), + (flags::HAS_CSI_DELTA, flags::SELF_ONLY), + (flags::PRIVACY_MODE, flags::SELF_ONLY), + ]; + for (a, b) in pairs { + assert_eq!(a & b, 0, "named flag overlap: 0x{a:04X} & 0x{b:04X}"); + } +} + +#[test] +fn header_preserves_reserved_flag_bits_through_round_trip() { + // Light bit 2 + bits 4..=15 — the full reserved space. + let reserved_set = flags::RESERVED_FLAGS_MASK; + let h = header_with_flags(reserved_set); + let bytes = h.to_le_bytes(); + let parsed = BfldFrameHeader::from_le_bytes(&bytes).expect("parse"); + assert_eq!( + { parsed.flags }, + reserved_set, + "reserved bits must round-trip unchanged for forward-compat", + ); + assert_eq!(bytes.len(), BFLD_HEADER_SIZE); +} + +#[test] +fn header_preserves_mixed_known_and_reserved_bits() { + let mixed = flags::HAS_CSI_DELTA | flags::PRIVACY_MODE | (1 << 7) | (1 << 14); + let h = header_with_flags(mixed); + let parsed = BfldFrameHeader::from_le_bytes(&h.to_le_bytes()).expect("parse"); + assert_eq!({ parsed.flags }, mixed); + // Known flags still readable via the named constants. + assert_ne!(({ parsed.flags }) & flags::HAS_CSI_DELTA, 0); + assert_ne!(({ parsed.flags }) & flags::PRIVACY_MODE, 0); +} + +#[test] +fn reserved_bits_do_not_collide_with_self_only_bit_3() { + // SELF_ONLY uses bit 3 — bit 2 is the only unused bit in the 0..=3 range + // and IS part of the reserved mask. + assert_ne!(flags::SELF_ONLY & flags::RESERVED_FLAGS_MASK, flags::SELF_ONLY); + assert_eq!(flags::RESERVED_FLAGS_MASK & (1 << 2), 1 << 2); + assert_eq!(flags::RESERVED_FLAGS_MASK & (1 << 3), 0); +} + +#[test] +fn all_zero_flags_round_trip_cleanly() { + let h = header_with_flags(0); + let parsed = BfldFrameHeader::from_le_bytes(&h.to_le_bytes()).expect("parse"); + assert_eq!({ parsed.flags }, 0); +} + +#[test] +fn all_one_flags_round_trip_cleanly() { + // Stress: every bit set. The parser has no business interpreting this + // configuration but must preserve it. + let h = header_with_flags(u16::MAX); + let parsed = BfldFrameHeader::from_le_bytes(&h.to_le_bytes()).expect("parse"); + assert_eq!({ parsed.flags }, u16::MAX); +}