feat(adr-118/p1.7): reserved-flag-bits forward-compat (243/243 GREEN)

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 <ruv@ruv.net>
This commit is contained in:
ruv 2026-05-24 18:55:04 -04:00
parent 9ee7c5df04
commit a3d26a4fad
2 changed files with 109 additions and 0 deletions

View File

@ -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.

View File

@ -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);
}