Files
ruvnet--RuView/CHANGELOG.md
T
rUv a91004e7b1 feat(adr-124): SENSE-BRIDGE — @ruvnet/rvagent MCP server + 6 sensing tools (v0.1.0) (#791)
* feat(adr-118/p1.4): BfldFrame (header + payload + CRC32) — 24/24 GREEN

Iter 4. Lands the central wire-format primitive: complete frames with
header + arbitrary-length payload, protected by CRC-32/ISO-HDLC.

Added:
- crc = "3" dependency (CRC-32/ISO-HDLC, same poly as Ethernet / zlib)
- src/frame.rs: CRC32_ALG const and crc32_of_payload(&[u8]) -> u32
- src/frame.rs: BfldFrame { header, payload: Vec<u8> } (gated on `std`)
  * BfldFrame::new(header, payload) — auto-syncs payload_len + payload_crc32
  * BfldFrame::to_bytes() -> Vec<u8> — header LE bytes ‖ payload
  * BfldFrame::from_bytes(&[u8]) -> Result<Self, BfldError>
- BfldError::TruncatedFrame { got, need } variant
- Doc strings on BfldError::Crc and BfldError::PrivacyViolation field names
- tests/frame_roundtrip.rs (7 named tests, gated on feature = "std"):
    frame_roundtrip_preserves_header_and_payload
    frame_new_syncs_payload_len_and_crc
    frame_serialization_is_deterministic
    frame_rejects_payload_crc_mismatch
    frame_rejects_truncated_buffer_smaller_than_header
    frame_rejects_truncated_buffer_smaller_than_payload
    empty_payload_is_valid (CRC of empty payload is 0x00000000)

Test config:
- cargo test --no-default-features → 17 passed (frame_roundtrip cfg-out)
- cargo test (default features = std)  → 24 passed (3+6+7+8)

ADR-119 ACs progressed:
- AC4 partial: bad-magic + bad-version + CRC-mismatch + truncation rejected
  with typed errors; field-level masking lives in the privacy_gate iter.
- AC5: BfldFrame round-trip preserves header + payload + CRC.
- AC6: Identical inputs produce bit-identical bytes (asserted explicitly).

Out of scope (next iter):
- Payload section parser (compressed_angle_matrix, amplitude_proxy, ...)
  — only the byte buffer is opaque so far; sections need length prefixes.
- BfldFrameRef<'_> for ESP32-S3 self-only mode (no-alloc, ADR-123 §2.5).
- PrivacyGate::demote(frame, target_class) transformer (ADR-120 §2.4).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p1.5): payload section parser (BfldPayload) — 32/32 GREEN

Iter 5. Implements ADR-119 §2.2 payload layout: 4-byte LE length prefix
followed by section bytes, in this fixed order:

  compressed_angle_matrix ‖ amplitude_proxy ‖ phase_proxy ‖ snr_vector
   ‖ csi_delta (iff flags.bit0)
   ‖ vendor_extension (length 0 allowed)

Added:
- src/payload.rs (gated on `feature = "std"`):
  * BfldPayload struct with 6 fields (csi_delta: Option<Vec<u8>>)
  * SECTION_PREFIX_LEN const (= 4)
  * to_bytes(include_csi_delta: bool) -> Vec<u8>
  * wire_len(include_csi_delta: bool) -> usize  (predictive, no allocation)
  * from_bytes(&[u8], expect_csi_delta: bool) -> Result<Self, BfldError>
  * push_section / read_section helpers (private)
- BfldError::MalformedSection { offset, reason } variant
- pub use BfldPayload from lib.rs (cfg-gated mirror of BfldFrame)

tests/payload_sections.rs (8 named tests, all green):
  payload_roundtrip_with_csi_delta
  payload_roundtrip_without_csi_delta
  wire_len_matches_to_bytes_length
  empty_payload_has_five_zero_length_sections
  parser_rejects_buffer_shorter_than_first_length_prefix
  parser_rejects_section_body_running_past_buffer_end
  parser_rejects_trailing_bytes_after_vendor_extension
  csi_delta_flag_mismatch_with_payload_is_detectable_via_trailing_bytes

ACs progressed:
- AC5 ↑ — full section-level round-trip preservation (round-trip with and
  without csi_delta both pass).
- AC6 ↑ — deterministic section encoding (length prefixes use to_le_bytes,
  body is byte-stable).
- AC1 partial — section layout now parses with bounded errors; CBFR-specific
  parsing (Phi/Psi Givens decoders) is a separate iter inside extractor.rs.

Test config:
- cargo test --no-default-features → 17 passed (payload module cfg-out)
- cargo test                       → 32 passed (3 + 6 + 7 + 8 + 8)

Out of scope (next iter target):
- Wire integration: feed BfldPayload bytes through BfldFrame::new so the
  header.payload_crc32 covers the section-prefixed bytes per ADR-119 §2.2
  ("CRC32 covers all section bytes including length prefixes").
- A no_std-friendly BfldPayloadRef<'_> borrowing variant (ESP32-S3 path).
- Givens-rotation angle decoder (Phi/Psi extraction from compressed_angle_matrix).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p1.6): BfldFrame <-> BfldPayload wire integration (39/39 GREEN)

Iter 6. Connects the typed payload parser (iter 5) to the framed
wire format (iter 4): the CRC32 now covers the section-prefixed
payload bytes per ADR-119 §2.2 ("CRC32 covers all section bytes
including length prefixes").

Added:
- BfldFrame::from_payload(header, &BfldPayload) -> Self
  Auto-syncs header.flags HAS_CSI_DELTA bit from payload.csi_delta.is_some(),
  serializes payload via to_bytes(), feeds BfldFrame::new() which computes
  payload_len + payload_crc32 over the section-prefixed bytes.
- BfldFrame::parse_payload(&self) -> Result<BfldPayload, BfldError>
  Reads HAS_CSI_DELTA bit from header.flags and dispatches to
  BfldPayload::from_bytes(&self.payload, expect_csi_delta).

tests/frame_payload_integration.rs (7 named tests, all green):
  from_payload_then_parse_payload_is_identity
  from_payload_autosets_has_csi_delta_flag
  from_payload_clears_has_csi_delta_flag_when_csi_absent
    (verifies the flag is cleared when csi_delta is None even if caller
     pre-set the bit; other flag bits like PRIVACY_MODE are preserved)
  frame_crc_covers_section_prefixed_bytes
    (mutating a byte inside section body trips CRC, not magic/length)
  frame_crc_covers_section_length_prefixes
    (mutating a section length-prefix byte trips CRC before parser ever runs)
  empty_typed_payload_roundtrips
  end_to_end_wire_roundtrip_via_bytes
    (BfldPayload -> from_payload -> to_bytes -> from_bytes -> parse_payload
     is the identity function modulo flag auto-set)

ACs progressed:
- AC5 ↑ — full payload round-trip through the framed bytes (closes
  the round-trip leg from BfldPayload through wire and back).
- AC6 ↑ — same input produces same bytes through both layers.
- AC4 ↑ — CRC mismatch on tampered section bodies and tampered section
  length prefixes both surface as BfldError::Crc, not as silent acceptance
  or as a deeper parser error.

Test config:
- cargo test --no-default-features → 17 passed (integration tests cfg-out)
- cargo test                       → 39 passed (3 + 6 + 7 + 8 + 8 + 7)

Out of scope (next iter target):
- PrivacyGate::demote(frame, target_class) — ADR-120 §2.4 class transition
  transformer with subtle::Zeroize on dropped fields.
- IdentityEmbedding newtype with no Serialize impl (ADR-120 §2.5 / I2).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p2.1): IdentityEmbedding newtype + zeroizing Drop — 44/44 GREEN

Iter 7. First structural enforcement of ADR-118 invariant I2 — the
identity embedding is in-RAM-only and cannot be serialized, cloned,
or copied. Lands the type itself; ring-buffer lifecycle is next.

Added:
- src/embedding.rs (no_std-compatible; lives in the lib regardless of features):
  * IdentityEmbedding wrapping [f32; EMBEDDING_DIM=128]
  * from_raw(values), as_slice() -> &[f32], l2_norm(), len(), is_empty()
  * NO Serialize, NO Clone, NO Copy impl
  * Custom Debug emits only dim + L2 norm + "<redacted>" — never raw values
  * Drop overwrites storage with 0.0 then core::hint::black_box(...) to defeat
    dead-store elimination (DSE would otherwise let the compiler skip the write)
- Compile-time structural guards via static_assertions:
    assert_impl_all!(IdentityEmbedding: Drop)
    assert_not_impl_any!(IdentityEmbedding: Copy, Clone)
- pub use IdentityEmbedding, EMBEDDING_DIM from lib.rs

tests/identity_embedding.rs (5 named tests, all green):
  from_raw_preserves_values_through_as_slice
  l2_norm_is_correct
  debug_output_redacts_raw_values
    (asserts the formatted output does NOT contain decimal text of values)
  embedding_is_not_clonable
    (runtime witness; compile-time assertion lives in src/embedding.rs)
  drop_overwrites_storage_with_zeros
    (Drop runs without panic; bit-level zeroization is asserted by the
     black_box-guarded loop. Unsafe peek-after-free is intentionally avoided.)

ACs progressed:
- AC5 ↑ — even in `privacy_mode`, the IdentityEmbedding type can't be reached
  from any serialization path because the type system rejects the impl.
- I2 ↑ — Drop, no Clone, no Copy, redacted Debug are all in place as
  compile-time guarantees.

Test config:
- cargo test --no-default-features → 22 passed
- cargo test                       → 44 passed (3 + 6 + 7 + 8 + 8 + 7 + 5)

Out of scope (next iter target):
- EmbeddingRing — 64-entry FIFO ring buffer holding IdentityEmbeddings,
  drained on coherence-gate Recalibrate (ADR-121 §2.4).
- PrivacyGate::demote(frame, target_class) transformer (ADR-120 §2.4).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p2.2): EmbeddingRing 64-entry FIFO buffer — 53/53 GREEN

Iter 8. Lands the lifecycle half of ADR-120 §2.5: a bounded, in-place,
no_std-compatible ring of IdentityEmbeddings. Insertion is O(1); when
full, push evicts the oldest entry, whose Drop runs and zeroizes the
f32 storage. drain() clears the ring on the coherence-gate Recalibrate
action (ADR-121 §2.4).

Added:
- src/embedding_ring.rs (no_std-compatible; no heap):
  * EmbeddingRing struct with [Option<IdentityEmbedding>; RING_CAPACITY=64]
    backing array, head cursor, count
  * EmbeddingRing::new() / Default impl
  * push(emb) -> Option<IdentityEmbedding>  (evicted oldest when full)
  * len / is_empty / capacity / is_full / iter
  * iter() returns occupied slots in insertion order (oldest first)
  * drain() -> usize  (empties the ring, returns count drained)
- pub use EmbeddingRing, RING_CAPACITY from lib.rs

Uses `[const { None }; RING_CAPACITY]` (stable since 1.79) to initialize
the slot array for a non-Copy element type.

tests/embedding_ring.rs (9 named tests, all green):
  new_ring_is_empty
  default_constructor_matches_new
  push_below_capacity_returns_none
  iter_yields_in_insertion_order
  push_at_capacity_evicts_oldest_and_returns_it
    (verifies eviction reports the FIRST pushed value, not the last)
  push_beyond_capacity_keeps_last_n_entries
    (after 74 pushes into a 64-slot ring, the surviving 64 are positions 10..74)
  drain_empties_the_ring_and_returns_count
  drain_on_empty_ring_returns_zero
  ring_can_be_refilled_after_drain
    (post-drain push lands cleanly at index 0; iter yields exactly that entry)

ACs progressed:
- I2 ↑ — ring eviction and explicit drain both drop IdentityEmbeddings,
  which the iter-7 Drop impl zeroizes. The "in-RAM-only" lifecycle is now
  end-to-end: bounded buffer in, FIFO out, drain on Recalibrate.

Test config:
- cargo test --no-default-features → 31 passed (22 + 9)
- cargo test                       → 53 passed (44 + 9)

Out of scope (next iter target):
- PrivacyGate::demote(frame, target_class) — ADR-120 §2.4 monotonic class
  transition with field zeroization, refusing demote-to-Raw (compile-fail).
- SoulMatchOracle stub trait + no-op default impl (ADR-121 §2.6) so the
  Recalibrate exemption hook is wireable from `--features soul-signature`.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p3.1): PrivacyGate::demote monotonic class transformer (60/60 GREEN)

Iter 9. Lands ADR-120 §2.4 — the only operation that can lower a frame's
information content. Demote is monotonic by construction (Result::Err
on non-monotone target), strips payload sections per the target class
table, and re-syncs header.privacy_class + CRC32.

Added:
- src/privacy_gate.rs (gated on `feature = "std"`):
  * PrivacyGate unit struct (+ Default impl)
  * PrivacyGate::demote(BfldFrame, target: PrivacyClass) -> Result<BfldFrame>
  * Stripping policy:
      target >= Anonymous (2): zeros + clears compressed_angle_matrix and
        csi_delta; sets csi_delta = None so from_payload clears HAS_CSI_DELTA
      target >= Restricted (3): also zeros + clears amplitude_proxy and phase_proxy
  * zeroize_then_clear helper — overwrite with 0 then black_box then truncate
- BfldError::InvalidDemote { from: u8, to: u8 } variant
- pub use PrivacyGate from lib.rs

Note: demote does NOT zero the original Vec capacity that the heap allocator
may still hold — the buffers we own are zeroed and cleared, but the
intermediate Vec passed back to BfldFrame::from_payload reallocates anew.
For strict heap zeroization in regulated deployments, a follow-up iter can
substitute zeroize::Zeroizing<Vec<u8>>.

tests/privacy_gate_demote.rs (7 named tests, all green):
  demote_to_same_class_is_identity
  demote_derived_to_anonymous_strips_compressed_angle_matrix
    (also asserts csi_delta dropped, snr_vector and amplitude_proxy preserved)
  demote_derived_to_restricted_strips_amplitude_and_phase_too
    (snr_vector and vendor_extension survive at class 3)
  demote_anonymous_to_derived_is_rejected
    (asserts InvalidDemote { from: 2, to: 1 })
  demote_to_raw_is_rejected_from_any_higher_class
    (parameterized over Derived, Anonymous, Restricted as sources)
  demote_preserves_frame_crc_consistency_through_wire_roundtrip
    (post-demote frame survives to_bytes -> from_bytes with no CRC error)
  demote_clears_has_csi_delta_flag_bit

ACs progressed:
- AC5 ↑ — privacy_mode enforcement at the frame-class boundary now works
  through PrivacyGate, not just the BfldEvent emitter (deferred). When the
  active class is Anonymous (2) or Restricted (3), the angle matrix /
  csi_delta / amplitude / phase sections that carry identity information
  are zeroed before any downstream code sees them.
- AC4 ↑ — demoted frames retain valid CRC; the round-trip-through-bytes
  test proves bit-correctness after the class transition.

Test config:
- cargo test --no-default-features → 31 passed (privacy_gate cfg-out)
- cargo test                       → 60 passed (53 + 7)

Out of scope (next iter target):
- SoulMatchOracle stub trait + no-op default impl (ADR-121 §2.6) so the
  Recalibrate exemption hook is wireable from `--features soul-signature`.
- IdentityRiskEngine — multiplicative formula on (sep, stab, consist, conf)
  with the coherence-gate GateAction enum (ADR-121 §2.2 + §2.4).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p3.2): identity_risk score + GateAction enum — 72/72 GREEN

Iter 10. Lands the stateless half of ADR-121 §2.2–§2.4: the
multiplicative risk-score formula and the 4-band gate classifier.
Hysteresis + 5s debounce (stateful CoherenceGate) land in iter 11.

Added (no_std-compatible):
- src/identity_risk.rs:
  * score(sep, stab, consist, conf) -> f32
    Each input clamped to [0,1]; NaN → 0 (conservative). Multiplicative
    combination: any near-zero factor collapses the score → privacy-biased.
  * Threshold constants: PREDICT_ONLY_THRESHOLD=0.5, REJECT_THRESHOLD=0.7,
    RECALIBRATE_THRESHOLD=0.9
  * GateAction enum: Accept | PredictOnly | Reject | Recalibrate
  * GateAction::from_score(f32) -> Self  — band-based classification with
    inclusive lower edges (0.7 maps to Reject, 0.9 maps to Recalibrate)
  * GateAction::allows_publish() / drops_event() / requires_recalibrate()
- pub use identity_risk_score (the function) and GateAction from lib.rs

tests/identity_risk_score.rs (12 named tests, all green):
  all_ones_yields_one
  any_zero_factor_collapses_score_to_zero (4 single-factor variants)
  score_is_monotonic_non_decreasing_in_single_factor
  out_of_range_inputs_are_clamped_to_unit_interval
  nan_inputs_treated_as_zero (verifies privacy-conservative NaN handling)
  known_score_matches_hand_calculation (0.8*0.9*0.85*0.95 to 1e-6)
  from_score_classifies_each_band (8 boundary-condition checks)
  threshold_constants_match_documented_values
  nan_score_maps_to_accept_conservatively
  allows_publish_partitions_actions_correctly
  drops_event_inverts_allows_publish (parameterized over all 4 actions)
  requires_recalibrate_is_unique_to_recalibrate

ACs progressed:
- ADR-121 AC2 partial — `score` formula structurally enforces non-negativity,
  upper bound 1.0, and conservative behavior under uncertainty (NaN, negative
  input, single near-zero factor).
- ADR-121 AC7 partial — score function is pure / deterministic; identical
  inputs always produce identical outputs (asserted by the known-value test).

Test config:
- cargo test --no-default-features → 43 passed (31 + 12)
- cargo test                       → 72 passed (60 + 12)

Out of scope (next iter target):
- CoherenceGate stateful struct: ±0.05 hysteresis + 5-second debounce
  (ADR-121 §2.5) so the gate doesn't oscillate near band boundaries.
- SoulMatchOracle stub trait (ADR-121 §2.6) — the Recalibrate exemption
  hook for `--features soul-signature` deployments.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p3.3): CoherenceGate hysteresis + 5s debounce — 85/85 GREEN

Iter 11. Wraps the stateless GateAction classifier from iter 10 with two
stabilizing mechanisms per ADR-121 §2.5:

  * ±0.05 HYSTERESIS — a score must clear the current band's edge by
    HYSTERESIS before the gate considers the next band.
  * 5-second DEBOUNCE_NS — a different action must persist that long
    before it becomes current; returning to the current band cancels it.

Added (no_std-compatible):
- src/coherence_gate.rs:
  * HYSTERESIS const (0.05) + DEBOUNCE_NS const (5_000_000_000)
  * CoherenceGate { current, pending: Option<(GateAction, u64)> }
  * new() / Default / current() / pending() (diagnostic accessors)
  * evaluate(score, timestamp_ns) -> GateAction
    Algorithm: compute effective_target via per-direction hysteresis check,
    promote pending after DEBOUNCE_NS elapsed, cancel pending on return to
    current band, reset debounce clock if pending target changes
  * Private helpers effective_target / action_idx / upper_edge_of / lower_edge_of
- pub use CoherenceGate from lib.rs

tests/coherence_gate.rs (13 named tests, all green):
  fresh_gate_starts_in_accept_with_no_pending
  low_score_stays_in_accept_with_no_pending
  score_just_past_boundary_but_within_hysteresis_does_not_pend
    (0.52: above 0.5 but inside hysteresis envelope — no pending)
  score_clearly_past_hysteresis_starts_pending
    (0.6: past 0.55 hysteresis edge — pending PredictOnly registered)
  pending_action_promotes_after_full_debounce
  pending_action_does_not_promote_before_debounce
    (verified at DEBOUNCE_NS - 1)
  returning_to_current_band_cancels_pending
  changing_pending_target_resets_the_debounce_clock
    (PredictOnly pending at t=0, then Recalibrate at t=1s — clock resets,
     must wait until t=1s+DEBOUNCE_NS before Recalibrate is current)
  downward_transitions_also_require_hysteresis
    (from PredictOnly, 0.48 stays put; 0.44 pends Accept)
  spike_to_one_then_back_to_zero_never_promotes_to_recalibrate
    (transient spike + return to baseline produces no transition)
  boundary_value_with_hysteresis_does_not_promote (0.5+0.05-epsilon)
  boundary_value_at_hysteresis_exact_does_pend (0.5+0.05)
  nan_score_stays_in_current_action_with_no_pending

ACs progressed:
- ADR-121 AC4 — Recalibrate fires when score >= 0.9 for >= DEBOUNCE_NS (5s).
  The debounce test above directly exercises this.
- ADR-121 AC5 — hysteresis test confirms action does not oscillate across
  ± 0.05 of a threshold within a 5-second window.

Test config:
- cargo test --no-default-features → 56 passed (43 + 13)
- cargo test                       → 85 passed (72 + 13)

Out of scope (next iter target):
- SoulMatchOracle stub trait (ADR-121 §2.6) + Recalibrate exemption —
  when --features soul-signature is enabled and the oracle reports a known
  enrolled person_id match, the gate downgrades Recalibrate → PredictOnly.
- BfldEvent struct (ADR-121 §2.1 output event) — first downstream consumer
  of the gate action.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p3.4): SoulMatchOracle + Recalibrate exemption (93/93 GREEN)

Iter 12. Wires the ADR-121 §2.6 Recalibrate exemption: when an enrolled
person_id matches the current high-separability cluster, the gate
downgrades the would-be Recalibrate to PredictOnly. The high score is
the *intended* outcome of a Soul Signature match, not an attacker-grade
sniffer arrival — so site_salt rotation is suppressed.

Added (no_std-compatible):
- src/coherence_gate.rs additions:
  * MatchOutcome enum: Match { person_id: u64 } | NotEnrolled | Suppressed
  * SoulMatchOracle trait with matches_enrolled() -> MatchOutcome
  * NullOracle (default-constructible, always reports NotEnrolled)
  * CoherenceGate::evaluate_with_oracle(score, ts, &O: SoulMatchOracle)
    — same hysteresis/debounce as evaluate(), but downgrades Recalibrate
    to PredictOnly when oracle returns Match { .. }
  * Refactored evaluate(): extracted advance_state(target, ts) shared with
    evaluate_with_oracle. evaluate is now a 4-line wrapper.
- pub use MatchOutcome, NullOracle, SoulMatchOracle from lib.rs

tests/soul_match_oracle.rs (8 named tests, all green):
  null_oracle_matches_default_evaluate_behavior
    (parameterized over 5 score points; oracle-aware and oracle-free
     gates produce identical trajectories)
  match_outcome_downgrades_recalibrate_to_predict_only
    (score=0.95 pends PredictOnly instead of Recalibrate)
  match_exemption_promotes_predict_only_after_debounce_not_recalibrate
    (after DEBOUNCE_NS, current is PredictOnly — never Recalibrate)
  match_outcome_does_not_affect_lower_actions
    (Reject pending stays Reject; oracle only intercepts Recalibrate)
  suppressed_outcome_does_not_exempt_recalibrate
    (Suppressed is functionally equivalent to NotEnrolled at the gate)
  not_enrolled_outcome_does_not_exempt_recalibrate
  match_outcome_carries_person_id
  null_oracle_default_constructor_works

ACs progressed:
- ADR-121 §2.6 fully covered as a stateless integration point — the
  hook is in place for the `--features soul-signature` Soul Signature
  crate (TBD) to plug in a real RaBitQ-backed oracle.
- ADR-118 §1.4 Soul Signature companion contract is now structurally
  enforced at the gate boundary: enrolled subjects do not trigger
  site_salt rotation; everyone else does.

Test config:
- cargo test --no-default-features → 64 passed (56 + 8)
- cargo test                       → 93 passed (85 + 8)

Out of scope (next iter target):
- BfldEvent struct (ADR-121 §2.1 output event JSON) — the downstream
  consumer of GateAction. Pairs the gate decision with presence/motion/
  person_count sensing fields.
- Optional: connect SoulMatchOracle into the actual `--features
  soul-signature` build (compile-time gate around a re-export).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p4.1): BfldEvent privacy-gated output + JSON (102/102 GREEN)

Iter 13. Lands ADR-121 §2.1 (output event) + ADR-122 §2.1 (field-gating
policy). BfldEvent collapses the GateAction-driven sensing pipeline
into the canonical wire-format publishable on MQTT.

Added:
- serde (workspace, derive feature, optional) + serde_json (workspace, optional) deps
- New crate feature `serde-json` (default-on; requires `std`)
- src/event.rs (gated on `feature = "std"`):
  * BfldEvent struct with all sensing + identity-derived fields
  * with_privacy_gating(...) constructor that applies field-gating policy:
      class < Restricted (3): identity_risk_score + rf_signature_hash kept
      class >= Restricted (3): both nulled to None
  * apply_privacy_gating() — idempotent in-place masking
  * to_json() -> Result<String, serde_json::Error> (gated on serde-json)
  * Custom ser_privacy_class serializer emits lowercase names
    ("anonymous", "restricted", etc.) per the BFLD JSON spec
  * skip_serializing_if = "Option::is_none" on identity-derived fields so
    privacy-gated events are observationally indistinguishable from
    events that never had the field set
- pub use BfldEvent from lib.rs

tests/event_privacy_gating.rs (9 named tests, all green):
  anonymous_event_retains_identity_risk_and_hash
  restricted_event_strips_identity_fields (class 3 → None)
  apply_privacy_gating_is_idempotent
  event_type_is_always_bfld_update (parameterized over 3 classes)
  json::json_round_trip_emits_type_field_first_or_last_but_present
  json::anonymous_json_includes_identity_fields
  json::restricted_json_omits_identity_fields_entirely
    (asserts the JSON string does NOT contain identity_risk_score or
     rf_signature_hash, verifying skip_serializing_if works as intended)
  json::privacy_class_serializes_to_lowercase_name
  json::zone_id_none_is_omitted_from_json

ACs progressed:
- ADR-121 AC6 (identity_risk score absent at class 3) — structurally
  enforced by with_privacy_gating + skip_serializing_if combination.
- ADR-122 AC1 — JSON shape matches the HA-DISCO publishable event
  contract; identity fields can be reliably stripped by privacy_class.
- ADR-118 AC5 — privacy_mode = engaged maps to PrivacyClass::Restricted
  with no identity fields in the published event.

Test config:
- cargo test --no-default-features → 64 passed (unchanged; event cfg-out)
- cargo test                       → 102 passed (93 + 9)

Out of scope (next iter target):
- Emitter struct that wires GateAction + privacy class + sensing inputs
  into BfldEvent construction (ADR-118 §2.1 pipeline diagram).
- MQTT topic publisher (ADR-122 §2.2) — depends on a runtime (tokio).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p4.2): BfldEmitter end-to-end pipeline (109/109 GREEN)

Iter 14. Wires every iter-1..13 primitive into a single ADR-118 §2.1
pipeline: per-frame sensing inputs go in, a privacy-gated BfldEvent
(or None) comes out. First time every constituent is exercised together.

Added (gated on `feature = "std"`):
- src/emitter.rs:
  * SensingInputs struct — 11 fields: timestamp_ns, presence, motion,
    person_count, sensing_confidence, sep, stab, consist, risk_conf,
    rf_signature_hash (Option)
  * BfldEmitter struct owning: node_id, default_zone_id, privacy_class,
    CoherenceGate, EmbeddingRing
  * Builder API: new(node_id) → with_zone(...) → with_privacy_class(...)
  * current_action() / ring_len() diagnostic accessors
  * emit(inputs, embedding) → Option<BfldEvent>
      1. score = identity_risk::score(sep, stab, consist, risk_conf)
      2. ring.push(embedding) if Some
      3. action = gate.evaluate_with_oracle(score, ts, &NullOracle)
      4. if action == Recalibrate { ring.drain() }
      5. if action.drops_event() { return None }
      6. else BfldEvent::with_privacy_gating(...) honoring privacy_class
  * emit_with_oracle(...) variant for `--features soul-signature` callers
- pub use BfldEmitter, SensingInputs from lib.rs

tests/emitter_pipeline.rs (7 named tests, all green):
  emitter_emits_event_under_low_risk
  emitter_drops_event_under_sustained_high_risk (debounce honored)
  emitter_drains_ring_on_recalibrate
    (fills ring to 5, then Recalibrate-grade score → ring_len() == 0)
  restricted_class_strips_identity_fields_in_emitted_event
    (class 3: identity_risk_score AND rf_signature_hash both None)
  with_zone_sets_default_zone_id_on_event
  embedding_is_pushed_to_ring_even_when_event_dropped
    (privacy gating drops the event but the ring still observes the
     embedding so subsequent separability calculations remain valid)
  ring_unchanged_when_no_embedding_supplied

ACs progressed:
- ADR-118 AC1 (BFLD core pipeline integration) — every component from
  iter 1 (frame format) through iter 13 (event) is now traversed by a
  single emit() call. This is the first end-to-end smoke proof.
- ADR-121 AC4 — Recalibrate-grade sustained score triggers ring drain
  (verified by ring_len() going from 5 to 0).
- ADR-122 AC1 — privacy_class threaded through the pipeline so the
  output event is correctly gated for HA/Matter consumption.

Test config:
- cargo test --no-default-features → 64 passed (emitter cfg-out)
- cargo test                       → 109 passed (102 + 7)

Out of scope (next iter target):
- Wiring rf_signature_hash computation from BLAKE3-keyed(site_salt,
  features) per ADR-120 §2.3 — the SensingInputs.rf_signature_hash
  is supplied by caller for now; needs a SignatureHasher with site_salt
  initialization in a follow-up iter.
- Embedding ring → identity_separability_score derivation (currently
  `sep` is caller-supplied; should be computed from ring contents).
- MQTT topic publisher wrapping BfldEmitter (ADR-122 §2.2) — depends
  on a runtime (tokio).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p3.5): SignatureHasher (BLAKE3-keyed) — 117/117 GREEN

Iter 15. Lands ADR-120 §2.3 — the cryptographic foundation of invariant
I3 ("cross-site identity correlation is impossible"). rf_signature_hash
is now derived from a per-site secret and a daily epoch, so two nodes
observing the same physical person produce uncorrelated 256-bit digests.

Added (no_std-compatible):
- blake3 = "1.5", default-features = false (no_std, no SIMD by default)
- src/signature_hasher.rs:
  * Constants SECONDS_PER_DAY (86_400), SITE_SALT_LEN (32), RF_SIGNATURE_LEN (32)
  * SignatureHasher { site_salt: [u8; 32] } with new(salt) const ctor
  * compute(day_epoch, &features) -> [u8; 32]  (BLAKE3 keyed mode)
  * compute_at(unix_secs, &features) -> [u8; 32] convenience
  * day_epoch_from_unix_secs(unix_secs) -> u32 helper (floor(t / 86400))
- pub use SignatureHasher, RF_SIGNATURE_LEN, SITE_SALT_LEN from lib.rs

tests/signature_hasher.rs (8 named tests, all green):
  deterministic_under_identical_inputs
  different_site_salts_produce_different_hashes
  different_day_epochs_rotate_the_hash
  different_features_produce_different_hashes
  output_length_is_32_bytes
  day_epoch_from_unix_secs_matches_floor_division
    (covers 0, 86_399, 86_400, and the 1.7e9 modern timestamp)
  compute_at_matches_compute_with_derived_day
  cross_site_hamming_distance_is_statistically_high
    *** ADR-120 §2.7 AC2 acceptance test ***
    Runs 100 trials with distinct (salt_a, salt_b) pairs observing
    identical features, computes per-trial Hamming distance, asserts
    mean >= 120 bits and min >= 80 bits. Empirically lands at ~128 bits
    mean (the expected value for two independent 256-bit hashes), with
    no trial below 80 bits — i.e., zero suspicious near-collisions.

ACs progressed:
- ADR-120 §2.7 AC2 — structurally enforced cross-site isolation, now
  proven empirically by the Hamming-distance test. This is the
  cryptographic half of invariant I3 in code, not just docs.
- ADR-118 invariant I3 — first runtime witness that two sites with
  independent site_salts cannot correlate the same person's signature.

Test config:
- cargo test --no-default-features → 72 passed (64 + 8; signature_hasher is no_std)
- cargo test                       → 117 passed (109 + 8)

Out of scope (next iter target):
- Wire SignatureHasher into BfldEmitter: replace caller-supplied
  rf_signature_hash with hasher.compute_at(ts, &features) so the
  pipeline produces correct hashes end-to-end.
- IdentityFeatures canonical-bytes encoder so callers don't need to
  hand-serialize per-feature representations.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p4.3): wire SignatureHasher into BfldEmitter (123/123 GREEN)

Iter 16. End-to-end ADR-120 §2.3 wiring: BfldEmitter now produces
rf_signature_hash derived from (site_salt, day_epoch, features), with
the IdentityEmbedding bytes as the preferred feature source. Closes
the gap from iter 15 — the hasher is now reachable from the pipeline.

Added (in src/emitter.rs):
- BfldEmitter.signature_hasher: Option<SignatureHasher> field
- BfldEmitter::with_signature_hasher(SignatureHasher) -> Self builder
- emit_with_oracle computes derived_hash BEFORE pushing embedding to ring:
    1. unix_secs = inputs.timestamp_ns / NS_PER_SEC
    2. feature bytes: embedding.as_slice() flattened to LE f32 bytes,
       OR fallback canonical_risk_bytes(&inputs) (4-tuple of LE f32)
    3. hasher.compute_at(unix_secs, &bytes)
- Derived hash overrides inputs.rf_signature_hash; when hasher absent
  caller-supplied value passes through unchanged (backward compat)
- canonical_risk_bytes(&inputs) -> [u8; 16] private helper for fallback

tests/emitter_hasher.rs (6 named tests, all green):
  no_hasher_passes_caller_supplied_hash_through
  installed_hasher_overrides_caller_supplied_hash
  same_emitter_same_inputs_produce_same_hash (determinism through emitter)
  different_site_salts_produce_different_hashes_end_to_end
    *** cross-site isolation proven via the BfldEmitter API, not just
        via the SignatureHasher direct API (iter 15) ***
  no_embedding_falls_back_to_risk_factor_bytes
  fallback_hash_differs_from_embedding_hash
    (embedding-based and fallback-based hashes are distinct paths)

ACs progressed:
- ADR-120 §2.7 AC2 — cross-site isolation now provable at the public
  emitter surface, not just inside the hasher module.
- ADR-118 §2.1 pipeline integration — derived rf_signature_hash flows
  through to the BfldEvent without caller participation. Operators
  install the hasher once at boot; per-frame code never sees site_salt.

Test config:
- cargo test --no-default-features → 72 passed (emitter_hasher cfg-out)
- cargo test                       → 123 passed (117 + 6)

Out of scope (next iter target):
- IdentityFeatures struct — typed canonical-bytes encoder so callers
  don't need to know that embedding bytes feed the hasher directly.
- Cross-iter integration test: BfldEmitter → BfldEvent::to_json with
  derived hash, parsed back, hash field present and base64-encoded
  (or hex-encoded) per the JSON wire spec.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p4.4): rf_signature_hash JSON as "blake3:<hex>" (128/128 GREEN)

Iter 17. Lands the BFLD JSON wire spec format for rf_signature_hash —
a "blake3:" prefix followed by 64 lowercase hex chars. Replaces the
default serde array-of-integers encoding which was unusable for
downstream consumers (HA, Matter, MQTT).

Added (in src/event.rs):
- ser_rf_signature_hash<S>(hash: &Option<[u8;32]>, s) custom serializer
- Field attribute on BfldEvent.rf_signature_hash now uses
  serialize_with = "ser_rf_signature_hash" alongside skip_serializing_if
- nibble_to_hex(u8) -> char private const fn (no `hex` crate dep needed
  for 32 bytes; lowercase hex is trivial)
- Output format: "blake3:deadbeef..." exactly 71 ASCII chars

tests/json_hash_format.rs (5 named tests, all green):
  rf_signature_hash_serializes_as_blake3_prefixed_lowercase_hex
    (expected hex built programmatically via format!("{b:02x}"))
  hex_string_is_always_64_chars_when_present
    (parses the JSON, isolates the hash substring, asserts exact 64
     chars and lowercase-only — catches case-folding regressions)
  hash_field_omitted_entirely_when_none
  end_to_end_emitter_hasher_to_json_emits_blake3_hex_hash
    *** Cross-iter integration test: BfldEmitter::with_signature_hasher
        → SensingInputs.rf_signature_hash = None → emit derives via
        BLAKE3 → BfldEvent::to_json → contains "blake3:" prefix.
        Spans iters 13, 14, 15, 16, 17 in a single assertion. ***
  end_to_end_restricted_class_omits_hash_even_with_hasher_set
    (class 3: even with hasher installed, JSON omits the hash)

ACs progressed:
- BFLD wire spec §6 — rf_signature_hash JSON shape now matches the
  documented format ("blake3:..."); HA / Matter consumers can parse
  it without custom byte-array decoding.
- ADR-118 §1 invariant I3 — visibility: the JSON wire form now
  cryptographically tags the hash with its algorithm prefix, so
  consumers can verify they're not parsing a different (weaker)
  hash that a future PR might accidentally substitute.

Test config:
- cargo test --no-default-features → 72 passed (json_hash_format cfg-out)
- cargo test                       → 128 passed (123 + 5)

Out of scope (next iter target):
- IdentityFeatures typed encoder so callers feeding BfldEmitter don't
  need to know that embedding bytes serve as hasher input.
- Replace the manual hex push with `hex::encode` if/when the workspace
  takes on the `hex` crate dep for other reasons; current path saves
  the dep without sacrificing correctness.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p3.6): IdentityFeatures canonical-bytes encoder (137/137 GREEN)

Iter 18. Consolidates the embedding-vs-risk-factor hashing-input
selection behind a single typed API. Replaces the two ad-hoc paths
that lived in emitter.rs through iter 17:
  * inline `emb.as_slice().iter().flat_map(|f| f.to_le_bytes())`
  * private `canonical_risk_bytes(&inputs) -> [u8; 16]`

Added (gated on `feature = "std"`):
- src/identity_features.rs:
  * IdentityFeatures<'a> enum: Embedding(&'a IdentityEmbedding) |
    RiskFactors { sep, stab, consist, conf }
  * from_embedding / from_risk_factors const constructors
  * canonical_byte_len() const fn — no allocation, predicts wire length
  * write_canonical_bytes(&mut Vec<u8>) — reusable-buffer path
  * canonical_bytes() -> Vec<u8> — allocating convenience
  * compute_hash(&SignatureHasher, day_epoch) -> [u8; 32]
  * RISK_FACTOR_BYTES const (= 16)
- pub use IdentityFeatures, RISK_FACTOR_BYTES from lib.rs

Refactor:
- src/emitter.rs: derived_hash now uses
    let features = match &embedding {
        Some(emb) => IdentityFeatures::from_embedding(emb),
        None => IdentityFeatures::from_risk_factors(sep, stab, consist, conf),
    };
    features.compute_hash(h, day_epoch)
  Local canonical_risk_bytes helper removed (superseded).

tests/identity_features_encoder.rs (9 named tests, all green):
  embedding_canonical_length_is_dim_times_four
  risk_factor_canonical_length_is_sixteen_bytes
  embedding_canonical_bytes_match_manual_flatten
  risk_factor_canonical_bytes_match_explicit_le_layout
  write_canonical_bytes_appends_to_existing_buffer
  compute_hash_matches_direct_hasher_invocation
  embedding_and_risk_factors_produce_different_hashes
  iter_16_wire_compat_embedding_path   *** backward-compat regression ***
  iter_16_wire_compat_risk_factor_path *** backward-compat regression ***
    These two tests assert that the refactored encoder produces
    bit-identical hashes to iter 16's inline path. Existing deployed
    nodes upgrading to iter 18 see no rf_signature_hash flip.

ACs progressed:
- ADR-120 §2.3 — features canonical-bytes representation now has a
  single source of truth in the codebase; future feature additions
  pass through one named encoder rather than scattered byte-fiddling.
- ADR-118 invariant I2 — IdentityFeatures borrows &IdentityEmbedding,
  it doesn't take ownership. The embedding's Drop / no-Serialize
  guarantees continue to hold across the canonical-bytes path.

Test config:
- cargo test --no-default-features → 72 passed (identity_features cfg-out)
- cargo test                       → 137 passed (128 + 9)

Out of scope (next iter target):
- Wire IdentityFeatures into a public emitter input path so callers
  can supply pre-constructed IdentityFeatures rather than the bare
  embedding + risk factors. (Soft refactor; current API is sufficient.)
- BfldPipeline facade — single struct combining BfldEmitter +
  BfldFrame producer + MQTT publisher (ADR-118 §2.1 lib.rs entry point).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p4.5): BfldPipeline facade + BfldConfig (146/146 GREEN)

Iter 19. Public lib.rs entry point per ADR-118 §2.1. Thin facade over
BfldEmitter that adds a config-driven builder and a privacy_mode
toggle for emergency demote-to-Restricted without rebuilding the
gate/ring/hasher state.

Added (gated on `feature = "std"`):
- src/pipeline.rs:
  * BfldConfig { node_id, default_zone_id, privacy_class, signature_hasher }
    with new/with_zone/with_privacy_class/with_signature_hasher builder
  * BfldPipeline { baseline_class, privacy_mode, emitter }
  * BfldPipeline::new(config) — initializes the underlying emitter
  * process(inputs, embedding) -> Option<BfldEvent>
    Delegates to emitter.emit() then post-processes: if privacy_mode is
    engaged, demotes the resulting event to Restricted and calls
    apply_privacy_gating to strip identity fields
  * enable_privacy_mode() / disable_privacy_mode() / is_privacy_mode_enabled()
  * current_privacy_class() — returns Restricted when privacy_mode else baseline
  * current_gate_action() — delegate diagnostic
- pub use BfldConfig, BfldPipeline from lib.rs

Design note: the privacy_mode override is applied post-emission, NOT by
rebuilding the emitter. This preserves gate state (current action,
pending transitions), ring contents, and hasher salt across the toggle —
critical for incident response where the operator needs to keep
detecting anomalies while temporarily redacting the public surface.

tests/pipeline_facade.rs (9 named tests, all green):
  config_defaults_to_anonymous_no_zone_no_hasher
  config_builder_methods_chain
  fresh_pipeline_is_not_in_privacy_mode
  pipeline_process_returns_anonymous_event_under_low_risk
  enable_privacy_mode_demotes_published_events_to_restricted
    (verifies BOTH identity_risk_score AND rf_signature_hash become None)
  disable_privacy_mode_restores_baseline_class
    (round-trip: enable → demoted → disable → restored to Anonymous)
  privacy_mode_overrides_derived_baseline_too
    (research-mode operator can still flip the emergency switch)
  pipeline_with_hasher_emits_derived_rf_signature_hash
  zone_is_threaded_from_config_to_event

ACs progressed:
- ADR-118 §2.1 — public entry point now matches the implementation
  plan §1.2 sketch: BfldPipeline::new(config) → process() → BfldEvent.
  Future iters add process_to_frame() and the tokio MQTT loop.
- ADR-118 §1.5 enable_privacy_mode requirement — operator can engage
  Restricted-class redaction without restarting the pipeline or
  losing in-flight detection state. First runtime witness of this.

Test config:
- cargo test --no-default-features → 72 passed (pipeline cfg-out)
- cargo test                       → 146 passed (137 + 9)

Out of scope (next iter target):
- process_to_frame(inputs, payload, embedding) -> Option<BfldFrame>
  for callers that need wire-format bytes rather than JSON events.
- BfldPipelineHandle wrapping the pipeline in Arc<Mutex<...>> + a
  tokio task that pumps an MQTT loop (ADR-122 §2.2 emitter half).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p4.6): BfldPipeline::process_to_frame wire-bytes path (152/152 GREEN)

Iter 20. Adds the wire-bytes companion to BfldPipeline::process so
callers needing BfldFrame (for ESP-NOW, UDP, file dump, witness
bundles, etc.) don't have to drop down to BfldEmitter + manual
BfldFrame construction.

Added (in src/pipeline.rs):
- BfldPipeline::process_to_frame(
      inputs: SensingInputs,
      header_template: BfldFrameHeader,
      payload: BfldPayload,
      embedding: Option<IdentityEmbedding>,
  ) -> Option<BfldFrame>

  Algorithm:
    1. Cache timestamp_ns from inputs (consumed by the inner process()).
    2. Call self.process(inputs, embedding) — gate logic decides drop/emit.
       Returns None if the gate rejects, propagating to caller.
    3. Clone header_template, override timestamp_ns and privacy_class from
       the current pipeline state (privacy_mode-aware).
    4. Build via BfldFrame::from_payload — CRC covers the section-prefixed
       payload bytes per ADR-119 §2.2.

  Separation of concerns: pipeline owns gate / ring / hasher state; caller
  owns AP / STA / session identity (provided via header_template).

tests/pipeline_to_frame.rs (6 named tests, all green):
  process_to_frame_emits_frame_under_low_risk
    (timestamp_ns + privacy_class correctly propagated from pipeline)
  process_to_frame_returns_none_under_sustained_high_risk
    (gate Reject path: two consecutive high-risk calls → None)
  process_to_frame_round_trips_through_bytes
    (frame.to_bytes() → BfldFrame::from_bytes() → parse_payload() identity)
  process_to_frame_overrides_class_in_privacy_mode
    (enable_privacy_mode → frame.header.privacy_class = Restricted byte)
  process_to_frame_preserves_header_template_identity_fields
    (ap_hash, sta_hash, session_id, channel from template survive)
  process_to_frame_uses_input_timestamp_not_template_timestamp
    (template.timestamp_ns = 12345 is overridden by inputs.timestamp_ns)

ACs progressed:
- ADR-118 §2.1 wire-bytes consumer path now reachable from BfldPipeline,
  not just from low-level BfldEmitter + manual frame construction.
- ADR-119 AC5/AC6 — round-trip-through-bytes test exercises the full
  pipeline+frame stack, not just the frame in isolation.
- ADR-122 §2.2 prep — the BfldFrame is the wire format MQTT eventually
  publishes via tokio loop (next iter pair); process_to_frame is the
  per-frame producer that loop will call.

Test config:
- cargo test --no-default-features → 72 passed (pipeline_to_frame cfg-out)
- cargo test                       → 152 passed (146 + 6)

Out of scope (next iter target):
- BfldPipelineHandle: Arc<Mutex<BfldPipeline>> + tokio task that pumps
  an inbound (SensingInputs, IdentityEmbedding) channel into MQTT
  per-class topics (ADR-122 §2.2). Brings in tokio + rumqttc deps
  behind a `mqtt` feature.
- Cargo benchmark: pipeline throughput target ≥ 40 frames/sec on a
  Pi 5 core (ADR-118 §6 P2 effort estimate).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p5.1): MQTT topic router (BfldEvent → Vec<TopicMessage>) — 162/162 GREEN

Iter 21. Lands ADR-122 §2.2 topic shape + class-gated routing as a pure
function. No broker dep yet — that lands in iter 22 with tokio + rumqttc
behind an `mqtt` feature. This iter is the routing policy, separated for
testability.

Added (gated on `feature = "std"`):
- src/mqtt_topics.rs:
  * TopicMessage { topic: String, payload: String }
  * TopicMessage::ruview_topic(node, entity) builds the canonical
    `ruview/<node>/bfld/<entity>/state` shape
  * render_events(&BfldEvent) -> Vec<TopicMessage>:
      class < Anonymous (0/1): returns empty (raw/derived are local only)
      class >= Anonymous (2/3): emits presence + motion + person_count +
        confidence, plus zone_activity if zone_id set
      class == Anonymous (2) ONLY: also emits identity_risk
      class == Restricted (3): identity_risk is suppressed even with score
- pub use render_events, TopicMessage from lib.rs

Payload encoding:
- presence:     "true" | "false"
- motion:       "{:.6}" — fixed-precision decimal in [0.0, 1.0]
- person_count: bare integer string
- confidence:   "{:.6}"
- zone_activity: JSON-string with quotes — "\"living_room\""
- identity_risk: "{:.6}"

tests/mqtt_topic_routing.rs (10 named tests, all green):
  topic_format_is_ruview_node_bfld_entity_state
  anonymous_class_publishes_six_topics_with_zone
    (6 = presence/motion/count/conf/zone/identity_risk)
  anonymous_class_without_zone_omits_zone_activity_topic (5 topics)
  restricted_class_omits_identity_risk_topic (class 3 → 5 topics, no risk)
  raw_and_derived_classes_publish_nothing
    *** structural enforcement of "raw stays local" at the topic layer ***
  presence_payload_is_lowercase_json_bool
  motion_payload_is_fixed_precision_decimal
  person_count_payload_is_bare_integer
  zone_payload_is_json_string_with_quotes
  identity_risk_payload_is_fixed_precision_decimal

ACs progressed:
- ADR-122 §2.2 topic shape now matches the documented format byte-for-byte.
- ADR-122 AC4 — per-class topic gating: classes 2 / 3 publish disjoint
  sets, with identity_risk uniquely guarded.
- ADR-118 invariant I1 reaching the public surface — Raw frames produce
  zero topic messages, so even a buggy publisher loop cannot leak them.

Test config:
- cargo test --no-default-features → 72 passed (mqtt_topics cfg-out)
- cargo test                       → 162 passed (152 + 10)

Out of scope (next iter target):
- tokio + rumqttc behind a new `mqtt` feature gate
- BfldPipelineHandle: Arc<Mutex<BfldPipeline>> + a tokio task that pumps
  inbound SensingInputs, runs render_events on each emitted BfldEvent,
  and calls client.publish() for each TopicMessage
- mosquitto integration test pattern (cf. feedback_mqtt_integration_test_patterns
  memory: per-test client_id, pump until SubAck, wait for publisher discovery)

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p5.2): Publish trait + publish_event free function — 169/169 GREEN

Iter 22. Abstracts the MQTT publish boundary without pulling in tokio or
rumqttc yet. The trait is sync (callers can hold &mut self without an
async runtime); the production rumqttc-backed impl in iter 23 will drive
a tokio task internally and present the same sync surface here.

Added (in src/mqtt_topics.rs, gated on `feature = "std"`):
- Publish trait with associated Error type
- CapturePublisher (Vec-backed; default-constructible) for unit tests
- publish_event<P: Publish>(publisher, event) -> Result<usize, P::Error>
    Iterates render_events(event) and forwards each TopicMessage to
    publisher.publish(). Returns the count actually published, or the
    publisher's error short-circuited on first failure.
- pub use Publish, CapturePublisher, publish_event from lib.rs

tests/mqtt_publish_loop.rs (7 named tests, all green):
  capture_publisher_records_every_message
  publish_returns_zero_for_raw_and_derived_events
    (parameterized — class 0 and class 1 both produce zero publishes,
     reinforcing the invariant I1 surface enforcement from iter 21)
  published_topics_match_render_events_ordering
    (stable per-event topic sequence for MQTT consumers)
  restricted_class_publishes_no_identity_risk_topic
  anonymous_without_zone_publishes_five_messages (5 = no zone_activity)
  publisher_error_short_circuits_publish_event
    (FailingPublisher fails on 3rd publish; publish_event surfaces the
     error AND leaves the first two messages durably published)
  capture_publisher_error_type_is_infallible
    (compile-time witness that CapturePublisher cannot panic the loop)

ACs progressed:
- ADR-122 §2.2 publisher boundary — the broker-facing surface is now a
  named trait operators can mock, swap, or wrap with retries.
- ADR-122 AC4 — publish_event respects the iter-21 class gating; Raw /
  Derived events produce zero broker traffic by definition.
- ADR-118 invariant I1 — even if the broker connection somehow regressed,
  the trait-level publish_event cannot exfiltrate a Raw frame because
  render_events returns empty first.

Test config:
- cargo test --no-default-features → 72 passed (mqtt_publish_loop cfg-out)
- cargo test                       → 169 passed (162 + 7)

Out of scope (next iter target):
- New `mqtt` feature gate; tokio + rumqttc deps under it
- RumqttPublisher: impl Publish that holds an MqttClient + a small tokio
  block_on or oneshot send to bridge sync trait to async client
- Optional: BfldPipelineHandle that owns Arc<Mutex<BfldPipeline>> + a
  spawn-and-forget tokio task pumping inbound (inputs, embedding) →
  process → publish_event(&rumqtt_pub, &event)
- mosquitto integration test following the patterns from
  feedback_mqtt_integration_test_patterns memory note

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p5.3): RumqttPublisher behind mqtt feature gate (176/176 GREEN with mqtt)

Iter 23. Production Publish trait impl using rumqttc 0.24 (same crate
version + use-rustls feature pinning as wifi-densepose-sensing-server,
so both publishers can share broker connection posture).

Added:
- rumqttc = "0.24" optional dep (default-features = false, use-rustls)
- New `mqtt` cargo feature: ["std", "dep:rumqttc"]
- src/rumqttc_publisher.rs (gated on `feature = "mqtt"`):
  * RumqttPublisher wrapping rumqttc::Client + QoS + retain flag
  * RumqttPublisher::new(client, qos) const constructor
  * with_retain(bool) builder for availability-style topics
  * RumqttPublisher::connect(opts, capacity) -> (Self, Connection)
    Returns the unpumped Connection — caller spawns a thread that
    iterates connection.iter() to drive the MQTT protocol. Default
    QoS is AtLeastOnce (HA-DISCO recommendation for state topics).
  * impl Publish with Error = rumqttc::ClientError
- pub use RumqttPublisher from lib.rs

tests/rumqttc_publisher_smoke.rs (7 named tests, all green, gated on mqtt):
  rumqttc_publisher_constructs_without_broker
    (uses 127.0.0.1:1 — reserved port refuses immediately; no hang)
  with_retain_builder_yields_a_publisher
  publish_queues_message_without_blocking_on_broker_state
    *** Critical property: rumqttc's sync Client::publish queues into
        an unbounded channel; publish_event returns Ok without round-
        tripping to the (offline) broker. The queued packet only sends
        if a thread iterates Connection::iter(). ***
  restricted_event_publishes_four_messages_through_rumqttc
    (class 3 + no zone: presence/motion/count/confidence — 4 topics)
  publisher_trait_object_is_constructible
    (Box<dyn Publish<Error = rumqttc::ClientError>> works)
  direct_publish_call_through_trait_object
  default_qos_is_at_least_once_via_connect

ACs progressed:
- ADR-122 §2.2 broker integration — production publisher now wired,
  matching the sensing-server's TLS / version posture. The two
  crates can share a single broker connection if an operator wants
  both publishers in the same process.
- ADR-122 AC4 still enforced — publish_event's class-gated routing
  is upstream of rumqttc, so no broker-level config can leak Raw frames.

Test config:
- cargo test --no-default-features → 72 passed (mqtt feature off)
- cargo test                       → 169 passed (mqtt feature off)
- cargo test --features mqtt --test rumqttc_publisher_smoke → 7 passed
- With --features mqtt: 169 + 7 = 176 total

Out of scope (next iter target):
- mosquitto integration test (env-gated MQTT_BROKER=tcp://localhost:1883):
    * spawn a thread iterating Connection::iter()
    * publish a BfldEvent
    * subscribe in the test, await SubAck per the workspace memory note
      `feedback_mqtt_integration_test_patterns`
    * assert the topics received match render_events output
- BfldPipelineHandle: Arc<Mutex<BfldPipeline>> with a thread that pumps
  inbound (inputs, embedding) → process → publish_event(&rumqttc_pub, &event)
  for a single-call "set up MQTT publisher and walk away" API.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p5.4): mosquitto integration test (env-gated, 178/178 with mqtt)

Iter 24. Live-broker roundtrip test for the RumqttPublisher → mosquitto
→ subscriber path. CI-safe: silently skips when BFLD_MQTT_BROKER is
unset; opt-in locally with:

    scoop install mosquitto
    mosquitto -v -c mosquitto-allow-anon.conf &
    BFLD_MQTT_BROKER=tcp://localhost:1883 cargo test \
        -p wifi-densepose-bfld --features mqtt --test mosquitto_integration

Added (gated on `feature = "mqtt"`):
- tests/mosquitto_integration.rs:
  * broker_env() parses BFLD_MQTT_BROKER as tcp://host:port (default 1883)
  * unique_client_id(prefix) — nanosecond-suffix per-test, per the
    `feedback_mqtt_integration_test_patterns` memory note
  * spawn_subscriber() creates a Client + thread iterating Connection;
    drains incoming Publish into an mpsc channel and emits a oneshot on
    SubAck arrival
  * collect_messages(rx, expected_count, timeout) — bounded recv loop
    that respects a wall-clock deadline (no `loop { iter.recv() }`)
  * Two named tests:

      live_broker_anonymous_event_roundtrips_all_six_topics
        Subscribe to ruview/<node>/bfld/+/state with the wildcard, await
        SubAck, publish an Anonymous event with zone, collect 6 messages,
        assert every expected entity name appears exactly once.

      live_broker_restricted_event_omits_identity_risk
        Same setup, publish a Restricted event, collect up to 6 (will
        only see 5), assert identity_risk is absent.

Test discipline (per the workspace memory):
  - per-test unique client_id (prevents broker session collisions)
  - subscriber eventloop pumped until SubAck BEFORE publishing
  - explicit timeout instead of infinite recv (no test hangs on misconfig)
  - publisher Connection drained in its own thread (rumqttc requirement)
  - 200ms sleep between publisher construction and first publish to let
    CONNECT complete (otherwise messages are queued before the session
    is open, and mosquitto silently drops them in some configurations)

When BFLD_MQTT_BROKER is unset:
  - broker_env() returns None
  - Test prints a one-line skip message to stderr and returns Ok(())
  - Both tests show as passing in cargo output

ACs progressed:
- ADR-122 AC1 end-to-end demonstrable — when a broker is available,
  the test proves a BfldEvent traverses RumqttPublisher, the network,
  and an MQTT subscriber, arriving with the correct topic shape and
  payload encoding.
- ADR-122 AC4 enforced over the wire — the Restricted-class test
  proves identity_risk does not even reach the broker, not just that
  it's stripped at render_events.

Test config:
- cargo test --no-default-features → 72 passed
- cargo test                       → 169 passed
- cargo test --features mqtt       → 178 passed (176 + 2 skip-mode tests)

Out of scope (next iter target):
- BfldPipelineHandle: Arc<Mutex<BfldPipeline>> + a worker thread that
  pumps inbound (SensingInputs, IdentityEmbedding) channel into MQTT.
  Single-call "set up publisher and walk away" API for operators.
- CI workflow that starts mosquitto in a Docker service container and
  sets BFLD_MQTT_BROKER so the integration test actually runs.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p5.5): BfldPipelineHandle worker thread (177/177 GREEN)

Iter 25. Single-call operator surface: spawn() takes a BfldPipeline and
a Publish impl, returns a handle whose send() enqueues sensing inputs
into a worker thread. The worker drives pipeline.process() then
publish_event() per input. Drop or shutdown() joins cleanly.

Added (gated on `feature = "std"`):
- src/mqtt_topics.rs: impl<P: Publish> Publish for Arc<Mutex<P>>
  Lets a publisher owned by a worker thread remain inspectable from a
  test or operator post-shutdown.
- src/pipeline_handle.rs:
  * PipelineInput { inputs: SensingInputs, embedding: Option<...> }
  * BfldPipelineHandle { sender, worker: Option<JoinHandle<()>> }
  * spawn<P: Publish + Send + 'static>(pipeline, publisher) -> Self
      Worker loop: recv() → pipeline.process() → publish_event(); errors
      logged to stderr (single-frame failures must not kill the loop)
  * send(PipelineInput) -> Result<(), SendError<...>>
  * shutdown(self) — replaces sender with a dropped channel so worker
    recv() returns Err(RecvError); join propagates worker panics
  * Drop impl mirrors shutdown so forgotten handles still clean up
- pub use BfldPipelineHandle, PipelineInput from lib.rs

tests/pipeline_handle_worker.rs (8 named tests, all green):
  handle_publishes_single_input (5 topics for Anonymous + no zone)
  handle_publishes_multiple_inputs_in_order (3 × 5 = 15 topics)
  handle_send_after_shutdown_errors
    (compile-time witness: shutdown(self) consumes the handle so
     post-shutdown send() is structurally impossible)
  handle_drop_without_explicit_shutdown_joins_worker_cleanly
    (validates the Drop path completes without hanging)
  handle_honors_privacy_mode_toggle_via_pipeline_state
    (4 topics for Restricted; identity_risk absent)
  handle_drops_event_when_gate_rejects
    (5 topics from first Accept-state input + 0 from Reject)
  handle_with_zone_threads_through_to_published_topics
    (zone_activity payload = "\"kitchen\"")
  class_3_pipeline_baseline_produces_four_topics_per_input

Test publisher pattern: Arc<Mutex<CapturePublisher>> lets the test thread
read out the worker thread's publish log post-shutdown without needing
custom channel plumbing per test.

ACs progressed:
- ADR-118 §2.1 lib.rs entry point now has the "set up MQTT and walk away"
  operator surface promised in the implementation plan. Two lines:
      let handle = BfldPipelineHandle::spawn(pipeline, rumqttc_pub);
      handle.send(PipelineInput { inputs, embedding })?;
- ADR-122 §2.2 per-frame publish path is now structurally guarded by
  worker-thread isolation: even if a Publish::publish call panics, only
  the worker thread dies; the main thread sees a clean error on send().

Test config:
- cargo test --no-default-features → 72 passed
- cargo test                       → 177 passed (169 + 8)
- cargo test --features mqtt       → 186 (178 + 8 — handle is std-only,
  reachable in both feature configs)

Out of scope (next iter target):
- GitHub Actions workflow with mosquitto Docker service so the iter-24
  integration test actually runs in CI with BFLD_MQTT_BROKER set.
- HA discovery payload publisher (ADR-122 §2.1) — the auto-discovery
  config messages HA needs alongside the state topics this handle ships.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs+plugins: rvAgent + RVF agentic-flow integration exploration

Land the rvAgent (vendor/ruvector/crates/rvAgent/) integration research
dossier and update both the Claude Code and Codex plugins so future
operators have a discoverable entry point for prototyping agentic flows
on top of RuView's existing sensing pipeline + RVF cognitive containers.

Added:
- docs/research/rvagent-rvf-integration/README.md
  Full integration thesis: rvAgent's 8 crates + 14 middlewares share
  RVF as their state-persistence format with RuView's existing
  v2/crates/wifi-densepose-sensing-server/src/rvf_container.rs. Three
  shippable touchpoints (each independent):
    1. Two new RVF segment types (SEG_AGENT_STATE = 0x08,
       SEG_DECISION = 0x09) so rvAgent sessions and RuView sensing
       sessions interleave in one witness-bundle-attestable blob
    2. BfldEvent → ToolOutput shim — agent reads BFLD events as
       tool context with no new IPC
    3. cog-* subagent registration under a queen-agent router
  Open questions: workspace inclusion path, sync/async adapter
  placement, privacy-class composition with rvagent-middleware
  sanitizer, Soul Signature ↔ SoulMatchOracle bridge, MCP surface.
  Proposed next: ADR-124 before scaffolding wifi-densepose-agent.

- plugins/ruview/skills/ruview-rvagent/SKILL.md
  New Claude Code skill exposing the integration surface, links to
  the research doc, and lists the three shippable touchpoints. Skill
  description tuned so Claude auto-discovers it for queries like
  "wire rvAgent into RuView" or "operator agent reacting to BFLD."

- plugins/ruview/codex/prompts/ruview-rvagent.md
  Codex counterpart prompt with trigger phrasing, reading order,
  same three touchpoints + open questions, and the ADR-124 next step.

Modified:
- plugins/ruview/.claude-plugin/plugin.json
  Version 0.1.0 → 0.2.0; description extended to mention "BFLD
  privacy layer" and "rvAgent + RVF agentic flows".

- plugins/ruview/codex/AGENTS.md
  Prompt table grows one row: `ruview-rvagent` for the new prompt.

No code changes; no test impact.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p5.6): HA auto-discovery payload publisher (187/187 GREEN)

Iter 26. Lands ADR-122 §2.1 HA-DISCO config-message generator.
Counterpart to iter 21's state-topic router: this produces the
homeassistant/<type>/<unique_id>/config messages HA reads on
startup to auto-create the six BFLD entities as a single device.

Discovery payloads are intended to be published once per node
session with retain = true (so HA finds them on subsequent starts).
The RumqttPublisher from iter 23 already exposes with_retain(true)
for this purpose; the state-topic loop must keep retain = false to
avoid stale-state flapping.

Added (gated on `feature = "std"`):
- src/ha_discovery.rs:
  * render_discovery_payloads(node_id, class) -> Vec<TopicMessage>
      class < Anonymous: empty vec (HA doesn't see raw/derived)
      class == Anonymous: 6 entities incl. identity_risk
      class == Restricted: 5 entities, no identity_risk
  * Per-entity HA metadata:
      presence       binary_sensor, device_class: occupancy
      motion         sensor, entity_category: diagnostic
      person_count   sensor, unit_of_measurement: people
      zone_activity  sensor, entity_category: diagnostic
      confidence     sensor, entity_category: diagnostic
      identity_risk  sensor, entity_category: diagnostic
  * Each payload carries:
      name, unique_id, state_topic (pointing at the iter-21 path),
      device block with identifiers / model: "BFLD" / manufacturer: "RuView"
  * Manual JSON builder with minimal escape coverage — node_id is
    ASCII alphanumeric + dash by convention; full escape via
    serde_json is a follow-up if operator-controlled names ever land.
- pub use render_discovery_payloads from lib.rs

tests/ha_discovery.rs (10 named tests, all green):
  raw_and_derived_classes_produce_no_discovery_payloads
  anonymous_class_produces_six_discovery_payloads
  restricted_class_omits_identity_risk_discovery
  discovery_topic_format_matches_ha_convention
    (validates all six homeassistant/.../config topics exist)
  presence_payload_carries_occupancy_device_class
  motion_payload_marked_as_diagnostic
  person_count_payload_carries_unit_of_measurement
  every_payload_contains_unique_id_and_state_topic_pointing_at_correct_state_topic
    (the state_topic in the discovery payload must match the topic the
     state-topic router from iter 21 actually publishes on — closes
     the discovery↔state loop)
  unique_id_matches_topic_segment
    (the unique_id baked into the payload equals the topic segment so
     HA dedupe works correctly across reboot/restart)
  class_2_discovery_includes_identity_risk_explicitly

ACs progressed:
- ADR-122 §2.1 — HA auto-discovery surface now complete: an operator
  can start mosquitto, publish-retained discovery once, and HA spins
  up the entire BFLD device on next start with zero YAML config.
- ADR-122 AC1 (six entities per node) — discovery + state-topic
  publishers are now symmetric: render_discovery_payloads emits the
  same six entity definitions render_events emits state messages for.
- ADR-118 §1.5 — privacy_mode = Restricted strips identity_risk at
  BOTH the discovery layer (entity not advertised to HA) AND the
  state layer (no state messages). Two-layer defense.

Test config:
- cargo test --no-default-features → 72 passed (ha_discovery cfg-out)
- cargo test                       → 187 passed (177 + 10)

Out of scope (next iter target):
- HA discovery + state publish coordinator: a small function or
  BfldPipelineHandle::publish_discovery(&mut self, retained: bool)
  that calls render_discovery_payloads + publish_event(retained=true)
  once at startup, then enters the per-frame loop.
- GitHub Actions workflow with mosquitto Docker service so the
  iter-24 integration test runs in CI with BFLD_MQTT_BROKER set.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p5.7): publish_discovery bootstrap helper (193/193 GREEN)

Iter 27. The free function that closes the discovery ↔ state loop on
the publishing side. Mirrors publish_event from iter 22 but for the
HA-DISCO config payloads from iter 26.

Added (in src/ha_discovery.rs, gated on `feature = "std"`):
- publish_discovery<P: Publish>(publisher, node_id, class) -> Result<usize, P::Error>
    Renders the per-class discovery payloads (iter 26) and forwards
    each through publisher.publish(). Returns the count or short-
    circuits on first error.
  Docstring documents the canonical bootstrap pattern: separate
  retain-true publisher for discovery, retain-false publisher for state,
  both sharing the same broker connection if desired.
- pub use publish_discovery from lib.rs

tests/ha_discovery_publish.rs (6 named tests, all green):
  publish_discovery_returns_six_for_anonymous_class
  publish_discovery_returns_five_for_restricted_class
    (no identity_risk in captured topics)
  publish_discovery_returns_zero_for_raw_and_derived
    (HA-DISCO + class gating composition: raw / derived never
     advertised to HA)
  publish_discovery_topics_are_homeassistant_config_format
  publish_discovery_short_circuits_on_publisher_error
    (FailingPub fails on 4th publish; first 3 messages land, then error)
  bootstrap_pattern_publishes_discovery_then_state_through_shared_publisher
    *** End-to-end bootstrap proof: one Arc<Mutex<CapturePublisher>>
        used for both discovery (publish_discovery) and state
        (BfldPipelineHandle::spawn + send). Asserts:
          - 6 + 5 = 11 messages captured in order
          - First 6 topics are homeassistant/.../config
          - Next 5 topics are ruview/<node>/bfld/.../state
        Validates the iter-25 Arc<Mutex<P>> Publish adapter + iter-26
        discovery + iter-27 bootstrap helper compose correctly. ***

ACs progressed:
- ADR-122 §2.1 — bootstrap surface complete. Operator writes one
  publish_discovery call at startup, then BfldPipelineHandle::send for
  every frame. HA finds the device on first restart after discovery
  was retained on the broker.
- ADR-122 AC1 (six entities per node) — discovery and state phases
  share the same six-entity definition; the bootstrap test proves they
  reach the broker in the documented order.

Test config:
- cargo test --no-default-features → 72 passed (publish_discovery cfg-out)
- cargo test                       → 193 passed (187 + 6)

Out of scope (next iter target):
- GitHub Actions workflow with mosquitto Docker service. Without this
  the iter-24 live integration test stays in skip mode in CI; with it,
  every PR would prove the full publish_discovery + handle stack works
  end-to-end against a real broker.
- HA blueprint shipping (ADR-122 §2.6): three operator-ready YAML
  blueprints (presence-driven lighting / motion-aware HVAC / identity-
  risk anomaly notification) packaged in cog-ha-matter/blueprints/.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p5.8): availability topic + LWT integration (203/203 GREEN)

Iter 28. Closes the per-node lifecycle on the MQTT side: HA can now
distinguish a node that is healthy + publishing zero events (nothing
detected) from a node that has lost the broker connection. Discovery
payloads now reference the availability topic so every entity inherits
the device-level offline marker.

Added (gated on `feature = "std"`):
- src/availability.rs:
  * PAYLOAD_AVAILABLE = "online", PAYLOAD_NOT_AVAILABLE = "offline"
  * availability_topic(node_id) -> "ruview/<node>/bfld/availability"
  * online_message / offline_message constructors returning TopicMessage
  * publish_availability_online / publish_availability_offline
    bootstrap helpers through Publish trait
- pub use the full availability surface from lib.rs

Discovery integration (src/ha_discovery.rs):
- Every entity config payload now carries:
    "availability_topic": "ruview/<node>/bfld/availability"
    "payload_available":  "online"
    "payload_not_available": "offline"
  HA uses these to grey out entities device-wide when the broker LWT
  fires or the node explicitly publishes "offline" during shutdown.

tests/availability_topic.rs (10 named tests, all green):
  availability_topic_format_matches_documented_path
  online_message_is_retained_friendly_payload
  offline_message_is_retained_friendly_payload
  publish_online_lands_one_message
  publish_offline_lands_one_message
  discovery_payload_includes_availability_topic_field
    (all 6 Anonymous-class discovery payloads carry the field)
  discovery_payload_includes_payload_available_and_not_available_strings
  restricted_class_discovery_still_carries_availability_fields
    (availability is not an identity field; class 3 retains it)
  bootstrap_sequence_online_then_discovery_lands_in_order
    *** End-to-end bootstrap proof: publish_availability_online +
        publish_discovery produces 1 + 6 = 7 messages, "online"
        first, six homeassistant/.../config payloads after. ***
  graceful_shutdown_sequence_publishes_offline_message_last

ACs progressed:
- ADR-122 §2.2 — availability topic now in place. Operators get HA
  online/offline indication without configuring LWT explicitly on
  rumqttc — the offline_message constructor + publish_availability_offline
  cover the explicit-shutdown path. Real LWT wiring (rumqttc's
  MqttOptions::set_last_will) is a follow-up.
- ADR-122 AC1 + AC4 — discovery now includes availability_topic, which
  HA needs to render the device as a unit; iter-26 tests continue to
  pass with the augmented payload (verified by full-suite count: 187 + 10).

Test config:
- cargo test --no-default-features → 72 passed (availability cfg-out)
- cargo test                       → 203 passed (193 + 10)

Out of scope (next iter target):
- Wire rumqttc::MqttOptions::set_last_will(...) so the broker
  auto-publishes "offline" when the TCP session drops; needs a small
  helper on RumqttPublisher to build options with LWT pre-configured.
- GitHub Actions workflow with mosquitto Docker so iter-24 live test
  runs in CI.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p5.9): RumqttPublisher::connect_with_lwt — broker auto-publishes "offline" (220/220 GREEN with mqtt)

Iter 29. Wires rumqttc::MqttOptions::set_last_will so the broker
auto-publishes "offline" on ruview/<node>/bfld/availability (retained,
QoS 1) when the publisher's TCP session drops without a clean
DISCONNECT. Closes the iter-28 lifecycle loop: explicit "online" on
connect + LWT-driven "offline" on session loss + explicit "offline"
on graceful shutdown.

Added (in src/rumqttc_publisher.rs, gated on `feature = "mqtt"`):
- RumqttPublisher::connect_with_lwt(node_id, opts, capacity) -> (Self, Connection)
  Convenience wrapping with_lwt(opts, node_id) then Self::connect(opts, capacity).
- with_lwt(opts, node_id) -> MqttOptions free helper for operators who
  build their own opts (custom TLS, credentials) and want to opt in to
  the LWT without using the connect_with_lwt shortcut.
- rumqttc 0.24 LastWill::new(topic, message, qos, retain) — 4-arg form;
  retain = true so HA sees "offline" on next start even if it was down
  when the session dropped.
- pub use with_lwt, RumqttPublisher from lib.rs

tests/rumqttc_lwt.rs (8 named tests, all green, gated on mqtt):
  with_lwt_returns_options_without_panic
  connect_with_lwt_constructs_publisher_and_connection
  connect_with_lwt_uses_documented_availability_topic
    (constructive proof — both LWT and discovery use the same
     availability_topic() function so they can't drift)
  connect_with_lwt_publisher_still_publishes_state_topics
    (LWT is purely additive — state topics work as before)
  publisher_trait_object_constructible_with_lwt_path
  with_lwt_is_idempotent_against_double_call
    (rumqttc replaces the will silently — useful for wrapper libraries)
  caller_built_options_can_opt_in_via_with_lwt_then_pass_to_connect
    (operator pattern: build opts with TLS/creds, attach LWT, then connect)
  placeholder_topicmessage_path_unaffected_by_lwt

Test bug caught:
- Initial test asserted 4 topics for Anonymous + no zone; actual is 5
  (presence + motion + person_count + confidence + identity_risk).
  rf_signature_hash is a BfldEvent JSON field, not its own MQTT topic.
  Fixed the assertion; documented the distinction in the test comment.

ACs progressed:
- ADR-122 §2.2 availability surface now fully operational. Three paths:
    1. Explicit publish_availability_online (iter 28) on connect
    2. LWT auto-publishes "offline" if connection drops (this iter)
    3. Explicit publish_availability_offline (iter 28) on graceful stop
  HA reads the same topic in all three cases; entities grey out
  device-wide via the iter-28 discovery `availability_topic` field.

Test config:
- cargo test --no-default-features → 72 passed
- cargo test                       → 203 passed
- cargo test --features mqtt       → 220 passed (212 + 8 new)

Out of scope (next iter target):
- GitHub Actions workflow with mosquitto Docker service. With iter
  24+29 now both depending on a live broker for full coverage, the
  CI lift is the next highest-value step.
- Three operator-ready HA blueprints (ADR-122 §2.6): presence-driven
  lighting, motion-aware HVAC, identity-risk anomaly notification.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p5.10): three HA operator blueprints (210/210 GREEN)

Iter 30. Ships the three ADR-122 §2.6 operator-ready Home Assistant
automation blueprints. Each blueprint binds to one BFLD MQTT entity
(presence / motion / identity_risk) and lets an HA operator import
+ configure without writing YAML by hand.

Added (under v2/crates/cog-ha-matter/blueprints/bfld/):
- presence-lighting.yaml
    binary_sensor.<node>_bfld_presence ⇒ light.turn_on / turn_off
    with a configurable hold_seconds delay before the off action
    (ADR-122 §2.6 requirement: "configurable hold time")
- motion-hvac.yaml
    sensor.<node>_bfld_motion ⇒ climate.set_temperature
    Operator picks motion_threshold (default 0.3, per ADR §2.6),
    delta_temperature_c (°C adjustment), and quiet_seconds debounce
- identity-risk-anomaly.yaml
    sensor.<node>_bfld_identity_risk ⇒ notify.<target>
    Two trigger paths:
      - Absolute spike (raw score >= spike_threshold, default 0.8)
      - Rolling 7-day z-score deviation (default 3 sigma)
    Requires a Statistics helper entity for the baseline; documented
    in the inline description and the blueprints README.
- README.md
    Lists the three blueprints + privacy caveat for identity_risk
    (only present at PrivacyClass::Anonymous; class 3 deployments
    will fail validation by design)

Added (in v2/crates/wifi-densepose-bfld/tests/ha_blueprints.rs):
- 7 named tests using include_str! to embed each YAML at build time
  and validate structure without adding a serde_yaml dep:
    presence_lighting_blueprint_is_structurally_valid
    motion_hvac_blueprint_is_structurally_valid
    identity_risk_blueprint_is_structurally_valid
    blueprints_carry_source_url_pointing_at_canonical_path
      (catches path drift when files move)
    presence_blueprint_uses_mqtt_integration_filter
    motion_blueprint_uses_mqtt_integration_filter
    identity_risk_blueprint_carries_privacy_class_caveat_in_description
      (operators running class 3 should know not to install)
- Helper assert_required_blueprint_fields(yaml, name_substring, label)
  enforces blueprint.{name,domain,input,trigger,action,mode} per HA spec

ACs progressed:
- ADR-122 §2.6 — all three blueprints shipped with the documented
  configurable inputs (hold_seconds for #1, motion_threshold +
  delta_temperature_c for #2, z_score_threshold + statistics_entity
  for #3). Operator installs via HA UI; no YAML editing required.
- ADR-118 §1.5 privacy_mode visibility — identity-risk blueprint
  documents the class-2-only availability so operators understand
  why the blueprint fails on class-3 deployments.

Test config:
- cargo test --no-default-features → 72 passed
- cargo test                       → 210 passed (203 + 7)

Out of scope (next iter target):
- GitHub Actions workflow with mosquitto Docker so iters 24 + 29
  e2e tests actually run in CI with BFLD_MQTT_BROKER set.
- cog-ha-matter cargo crate-internal test that loads each blueprint
  via serde_yaml + validates against an HA blueprint schema (instead
  of the string-only checks here). Optional; current coverage is
  sufficient to catch drift in the YAML files themselves.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.1): end-to-end I3 isolation proof via BfldPipeline (217/217 GREEN)

Iter 31. Lifts ADR-118 invariant I3 + ADR-120 §2.7 AC2 from the
SignatureHasher unit-test surface (iter 15) to the public BfldPipeline
API surface. Every assertion goes through pipeline.process() so the
chain exercises emitter → identity_features encoder → signature hasher
→ event construction end-to-end.

Added (in v2/crates/wifi-densepose-bfld/tests/pipeline_i3_isolation.rs):
- 7 named tests, all green:
    same_person_at_different_sites_same_day_produces_different_hashes
    same_person_same_site_different_day_rotates_the_hash
    thirty_day_gap_produces_thoroughly_different_hash
      (Hamming distance >= 80 bits — catches a weak day_epoch mix-in
       even if naive byte-equality remains different)
    same_person_same_site_same_day_produces_stable_hash
    cross_site_hamming_distance_at_pipeline_surface_is_statistically_high
      *** ADR-120 §2.7 AC2 at the public pipeline surface ***
      32 trials × 32 bytes; mean Hamming distance ≥ 120 bits required
      (the same threshold the iter-15 SignatureHasher-direct test used)
    restricted_class_strips_hash_but_pipeline_state_advances
      (class 3 contract: hash stripped from event surface but the
       underlying gate / ring / hasher state still updates so the
       pipeline keeps detecting things; future PR can't accidentally
       short-circuit at class 3 and miss legitimate sensing)
    pipeline_without_signature_hasher_does_not_invent_a_hash
      (no hasher installed → rf_signature_hash stays None)

ADR-124 status (from sibling-agent check in this iter's step 0):
- docs/adr/ADR-124-* not present yet
- docs/research/rvagent-rvf-integration/README.md present (iter 25)
- No conflict with current scope; will pick up sibling output on next iter

ACs progressed:
- ADR-118 invariant I3 — runtime proof now at the PUBLIC API surface,
  not just inside SignatureHasher. Operators reading the BfldPipeline
  documentation can verify cross-site isolation without descending
  into the hasher internals.
- ADR-120 §2.7 AC2 — pipeline-surface mean Hamming distance >= 120
  bits in the cross_site test pins the structural-isolation invariant
  at the same threshold as the iter-15 unit-level test.
- ADR-118 §1.5 — restricted_class_strips_hash test pins the
  defense-in-depth contract that class-3 doesn't accidentally also
  freeze pipeline state.

Test config:
- cargo test --no-default-features → 72 passed (pipeline_i3_isolation cfg-out)
- cargo test                       → 217 passed (210 + 7)

Out of scope (next iter target):
- GitHub Actions workflow with mosquitto Docker (lifts iters 24+29
  from skip-mode in CI).
- ADR-119 AC7 serialization throughput benchmark (50k frames/sec).
- ADR-122 AC3: 1Hz motion-publish rate integration test against the
  BfldPipelineHandle worker thread.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.2): serialization throughput test (ADR-119 AC7) — 221/221 GREEN

Iter 32. Closes ADR-119 AC7 ("Bench: serialization throughput ≥ 50k
frames/sec on a 2025-era M1/M2 / Pi 5 core"). Pure std::time::Instant
timing; no criterion / no dev-deps added.

Empirically measured in DEBUG build on this Windows host:
- BfldFrameHeader::to_le_bytes()  → 1,654,517 frames/sec (33× AC7)
- BfldFrame::to_bytes() + CRC32   →   320,255 frames/sec ( 6.4× AC7)
- Parse-cost ratio (1024B vs 512B payload): 1.59× (linear)

Release builds typically run 20–100× faster than debug; the AC7 target
is for release, so debug already smashing 50k means release has very
comfortable margin.

Added (tests/serialization_throughput.rs):
- pub const RELEASE_TARGET_FRAMES_PER_SEC = 50_000.0 (the AC7 number)
- const DEBUG_FLOOR_FRAMES_PER_SEC      = 5_000.0  (generous CI floor)
- header_only_to_le_bytes_throughput_meets_debug_floor
    50k iters with a 1k-iter warmup, black_box-guarded.
    Prints throughput to stderr so CI logs show the measured number.
- full_frame_to_bytes_throughput_meets_debug_floor
    Same shape but with 512B payload + CRC32 round-trip per iter.
- round_trip_through_bytes_remains_constant_time_per_byte
    Compares from_bytes() timing for 512B vs 1024B payload; asserts
    the ratio is in [1.0, 4.0] to catch an accidental O(n²) parser
    regression. Empirical ratio: 1.59× (expected ~2× for O(n)).
- header_size_constant_is_used_consistently_by_serializer
    Belt-and-suspenders: asserts to_le_bytes().len() == BFLD_HEADER_SIZE
    == 86, pinning the iter-1 AC1 contract from the throughput side.

ADR-124 status (iter step 0 sibling check):
- docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md NOW PRESENT
  (sibling agent landed it; 431 lines). Codename SENSE-BRIDGE. Scope:
  MCP server (stdio + Streamable HTTP) wrapping sensing-server's
  REST/WS/MQTT surfaces, plus a ruvector npm/TypeScript package for
  in-app consumption + ruflo MCP-tool integration. Orthogonal to BFLD
  core — BFLD produces events that SENSE-BRIDGE would expose via MCP,
  but the MCP bridge itself is not BFLD territory. No scope overlap
  with this iter or backlog targets.

ACs progressed:
- ADR-119 AC7 — debug-build serialization throughput is already 33×
  the documented release-build target. Release-build margin is
  comfortable; future iters can run --release to capture an exact
  release number for the witness bundle.

Test config:
- cargo test --no-default-features → 72 passed
- cargo test                       → 221 passed (217 + 4)

Out of scope (next iter target):
- GitHub Actions workflow with mosquitto Docker (lifts iter 24/29
  e2e from skip-mode in CI).
- ADR-122 AC3: 1Hz motion-publish-rate integration test against the
  BfldPipelineHandle worker thread (would use a Barrier + Instant
  delta over N sustained publishes).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.3): motion publish rate ≥ 1Hz integration test (ADR-122 AC3) — 224/224 GREEN

Iter 33. Closes ADR-122 AC3 ("Motion score published at ≥ 1 Hz on
ruview/<node_id>/bfld/motion/state during sustained occupancy") with
an end-to-end test through the BfldPipelineHandle worker thread.

Empirically measured on this Windows host: 10 inputs spaced 100ms
apart → 9.96 Hz motion-publish rate (10× the AC3 floor).

Added (in v2/crates/wifi-densepose-bfld/tests/motion_publish_rate.rs):
- motion_publish_rate_meets_one_hz_under_sustained_input
    Drives the handle with 10 sends at 100ms intervals, measures the
    wall-clock elapsed time, asserts motion count >= 10 AND rate
    (count / elapsed) >= 1.00 Hz. Prints throughput to stderr.
- motion_values_track_input_motion_values
    Pins iter-21's payload-encoding contract: motion values [0.10,
    0.25, 0.50, 0.75, 0.95] flow through as "{:.6}" strings without
    quantization drift.
- motion_topic_never_appears_for_class_below_anonymous_publishing
    Defense in depth: Restricted (class 3) STILL publishes motion
    (sensing data) but NOT identity_risk. Pins the two-layer
    privacy contract: motion is operator-visible at all classes ≥ 2,
    identity_risk is class-2-only.

Helper: motion_messages(&[TopicMessage]) -> Vec<&TopicMessage>
    Filters the capture log to the motion topic so the assertions
    aren't sensitive to the surrounding presence/count/confidence
    topics also being published.

ADR-124 status (iter step 0 sibling check):
- docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md present
  unchanged at 431 lines (sibling agent's SENSE-BRIDGE ADR). Scope
  remains orthogonal to BFLD core; no overlap with this iter.

ACs progressed:
- ADR-122 AC3 closed: motion publish rate measured at 9.96 Hz
  through the handle worker — 10× the documented floor. Provides
  the runtime witness HA needs to trust the live state-topic stream.
- ADR-122 AC1 reinforced from the rate-test side: 10 inputs → 10
  motion topics, none lost in the worker queue.
- ADR-118 §1.5 reinforced again: Restricted strips identity_risk
  but not motion (motion is sensing, not identity).

Test config:
- cargo test --no-default-features → 72 passed
- cargo test                       → 224 passed (221 + 3)

Out of scope (next iter target):
- GitHub Actions workflow with mosquitto Docker (lifts iters 24+29
  from skip-mode in CI). All remaining unmet ACs at this point
  either require external resources (KIT BFId dataset for ADR-121,
  Pi5/Nexmon hardware for ADR-123) or CI infra.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.4): spawn_with_oracle for Soul Signature deployments (227/227 GREEN)

Iter 34. Closes the gap where BfldPipelineHandle had no path for an
operator-supplied SoulMatchOracle to reach the worker thread. The
emit_with_oracle surface added in iter 14 was unreachable through the
handle API — Soul Signature deployments (ADR-118 §1.4) had to either
drop down to BfldEmitter directly or accept Recalibrate gate-drops on
known-enrolled matches.

Added (in src/pipeline.rs):
- BfldPipeline::process_with_oracle<O: SoulMatchOracle>(
      inputs, embedding, oracle,
  ) -> Option<BfldEvent>
  Wraps emitter.emit_with_oracle then applies the same privacy_mode
  post-processing as process(). Privacy_mode and oracle are independent
  — class-3 demote still happens AFTER any oracle Recalibrate exemption.

Added (in src/pipeline_handle.rs):
- BfldPipelineHandle::spawn_with_oracle<P, O>(pipeline, publisher, oracle) -> Self
  where O: SoulMatchOracle + Send + Sync + 'static
  The worker thread owns the oracle and consults it on every recv().
  Worker loop now calls pipeline.process_with_oracle(...) instead of
  pipeline.process(...).

tests/handle_soul_oracle.rs (3 named tests, all green):
  spawn_with_oracle_null_is_equivalent_to_spawn
    Parity: 3 identical low-risk inputs through spawn() and
    spawn_with_oracle(NullOracle) produce the same publish count
    and the same motion-topic count.
  spawn_with_always_match_oracle_lets_events_publish_under_high_risk
    *** Headline test ***
    3 high-risk inputs spaced > DEBOUNCE_NS apart. With AlwaysMatch
    oracle, all 3 produce motion topics — the gate never reaches
    Recalibrate because the oracle reports an enrolled-person match.
  spawn_with_null_oracle_drops_events_under_sustained_recalibrate_score
    Negative control for the above: same 3 inputs through NullOracle,
    only 1 motion topic survives (the first input lands at Accept;
    the second and third hit Recalibrate after debounce and are
    dropped per ADR-121 §2.4).

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 to BFLD core;
  no overlap with this iter.

ACs progressed:
- ADR-118 §1.4 Soul Signature companion contract end-to-end through
  the public handle API. Operators wiring Soul Signature into a
  RuView deployment now use:
      BfldPipelineHandle::spawn_with_oracle(pipeline, publisher, my_oracle)
  …and the rest of the per-frame flow stays identical to spawn().
- ADR-121 §2.6 Recalibrate exemption proven over the worker-thread
  boundary, not just at the unit level (iter 12 covered the gate-only
  case).

Test config:
- cargo test --no-default-features → 72 passed
- cargo test                       → 227 passed (224 + 3)

Out of scope (next iter target):
- GitHub Actions workflow with mosquitto Docker (lifts iters 24+29
  live-broker e2e from skip-mode). Remaining unmet ACs require
  either external resources (KIT BFId, Pi5/Nexmon) or CI infra.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.5): GitHub Actions mosquitto Docker CI workflow (235/235 GREEN)

Iter 35. Lifts iters 24 + 29 live-broker integration tests out of
skip-mode in CI by spinning up an eclipse-mosquitto:2 service container,
exporting BFLD_MQTT_BROKER, and running the three cargo test matrices.

Added:
- .github/workflows/bfld-mqtt-integration.yml
    * Triggers: push to main / feat/adr-118-* / feat/bfld-*, PR, manual
    * Path filter: only runs when v2/crates/wifi-densepose-bfld/** or the
      workflow file itself changes — protects PR throughput for unrelated
      crate work
    * Service container: eclipse-mosquitto:2 on port 1883 with a
      mosquitto_pub-based healthcheck (5s interval, 10 retries) so the
      runner waits for a real publish-ready broker, not just liveness
    * Top-level timeout-minutes: 15 (bounds runner cost if rumqttc
      handshake hangs)
    * Three cargo test invocations:
        cargo test -p wifi-densepose-bfld --no-default-features
        cargo test -p wifi-densepose-bfld
        cargo test -p wifi-densepose-bfld --features mqtt
      The third one now actually exercises the mosquitto_integration and
      rumqttc_lwt tests, not just the skip-mode path.
    * Belt-and-suspenders nc -z port poll before tests start (service
      container can take a few seconds to bind even with healthcheck)
    * cargo clippy --features mqtt as a continue-on-error gate (signals
      drift; doesn't block the merge yet)
    * RUSTFLAGS=-D warnings, CARGO_INCREMENTAL=0 for stable runs

- v2/crates/wifi-densepose-bfld/tests/ci_workflow.rs (8 named tests):
    Validates the workflow YAML via include_str! — same pattern iter 30
    used for HA blueprints. Catches drift in CI infra:
      workflow_declares_mosquitto_service_container
      workflow_exports_broker_env_for_iter_24_and_29_tests
        (BFLD_MQTT_BROKER pointing at the service container)
      workflow_runs_three_cargo_test_invocations
        (no_default + default + mqtt — three classes of bug surface)
      workflow_waits_for_mosquitto_readiness_before_testing
        (nc -z 1883 port poll)
      workflow_uses_health_check_on_the_service
        (mosquitto_pub-based, not just process liveness)
      workflow_only_triggers_on_bfld_paths
        (path filter to v2/crates/wifi-densepose-bfld/**)
      workflow_pins_runner_to_ubuntu_latest_for_docker_service_support
        (GitHub Actions `services:` doesn't work on macOS/Windows)
      workflow_has_timeout_guard
        (top-level timeout-minutes pinned)

ADR-124 status (iter step 0 sibling check):
- docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md unchanged
  at 431 lines (SENSE-BRIDGE ADR). Scope remains orthogonal.

ACs progressed:
- ADR-122 §2.2 e2e — when this workflow lands on origin/main and the
  next BFLD PR runs, the iter-24 anonymous-event roundtrip + restricted-
  event-omits-identity_risk tests stop printing "skipping" and actually
  publish to / subscribe from mosquitto. Plus the iter-29 LWT publisher
  smoke run gets to fire its session-drop test against a live broker.
- ADR-118 §2.1 ⇄ §2.2 — discovery + state-topic + LWT + worker thread
  all proven in one CI matrix run.

Test config:
- cargo test --no-default-features → 72 passed (ci_workflow cfg-out)
- cargo test                       → 235 passed (227 + 8)

Out of scope (skipped — external resources or hardware):
- ADR-121 calibration — KIT BFId dataset
- ADR-123 production capture — Pi 5 / Nexmon hardware

All other in-crate ACs from the ADR-118 / 119 / 120 / 121 / 122 series
are now covered by the iter 1-35 chain. The cron loop should
consider closing out at this point or pivoting to documentation /
witness-bundle generation for the PR.

Co-Authored-By: claude-flow <ruv@ruv.net>

* 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>

* feat(adr-118/p6.6): pipeline event-stream JSON determinism (248/248 GREEN)

Iter 37. Adds the cross-pipeline counterpart to iter 31's I3 isolation
tests. Iter 31 proved hash DIFFERENCES across sites and days; this
iter proves event-stream EQUALITY across two pipeline instances with
matching configuration. Operators capturing BFI for offline replay
analysis can now trust that replaying the same input stream produces
byte-identical JSON output across BFLD versions.

Added (in v2/crates/wifi-densepose-bfld/tests/pipeline_determinism.rs):
- 5 named tests, all green:

  two_pipelines_with_identical_config_produce_identical_event_streams
    Build two BfldPipelines from the same BfldConfig (same node_id,
    same SignatureHasher salt, same class), drive both with 5
    identical (timestamp, motion, embedding) tuples, then walk both
    event vecs field-by-field asserting equality of every
    publishable BfldEvent field including the derived
    rf_signature_hash and identity_risk_score.

  two_pipelines_produce_byte_identical_event_json_streams
    (gated on serde-json) — same fixture, but compares the
    serde_json::to_string output as Vec<String>. This is the
    operator's true wire-form replay guarantee.

  replaying_same_input_sequence_after_pipeline_reset_reproduces_events
    Catches accidental hidden state by building, draining, and
    rebuilding the pipeline twice; asserts the hash sequences match.
    If a future PR adds an internal counter that affects output,
    this test fires.

  different_input_sequences_diverge_after_the_first_difference
    Negative control: identical first two inputs produce identical
    hashes; changing the third input (different embedding) produces
    a different hash. Pins that the determinism is genuine, not
    "always returns the same value."

  class_3_pipelines_produce_identical_stripped_event_streams
    Determinism property must hold across privacy classes too —
    operators running Restricted deployments need replay to work
    even though identity fields are stripped.

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 AC6 (deterministic serialization) lifted from the
  BfldFrame layer (iter 2) to the BfldEvent + JSON layer.
  Operators get end-to-end determinism guarantees from sensing
  input through to MQTT topic payload.
- ADR-118 §2.1 pipeline correctness — two-pipeline equality is the
  strongest form of the "same input → same output" contract the
  facade can offer. Combined with iter 31's I3 difference proof,
  the pipeline now has both "should match" and "should differ"
  invariants pinned at the public-API level.

Test config:
- cargo test --no-default-features → 80 passed (pipeline_determinism cfg-out)
- cargo test                       → 248 passed (243 + 5)

Out of scope (next iter target):
- PR-readiness pivot — CHANGELOG batch, witness bundle, AC closeout
  table for the eventual PR description. All in-crate ACs are now
  covered by iters 1-37; remaining work is either external-resource-
  gated (KIT BFId, Pi5/Nexmon) or PR-prep.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.7): apply_privacy_gating irreversibility tests (255/255 GREEN)

Iter 38. Pins ADR-120 §2.4 ("There is no `promote` operation") at the
BfldEvent::apply_privacy_gating soft-mutation surface. Iter 9's
PrivacyGate::demote tests already proved this for the explicit
class-transition transformer; this iter proves it for the *soft*
in-place re-classifier used by BfldPipeline::process() under
enable_privacy_mode().

Defense-in-depth property: an attacker who manages to flip
event.privacy_class from Restricted back to Anonymous cannot then
resurrect the stripped identity fields through apply_privacy_gating
alone. They'd have to fabricate the fields via direct field assignment
or rebuild via with_privacy_gating — both of which are conspicuous in
code review (single byte flip is not).

Added (in tests/event_gating_irreversibility.rs):
- 7 named tests, all green:

  apply_at_anonymous_preserves_identity_fields
    Sanity: apply doesn't strip when class is Anonymous.

  manual_class_flip_to_restricted_then_apply_strips_both_fields
    Direct path: class Anonymous → flip to Restricted → apply
    → identity_risk_score and rf_signature_hash both None.

  one_way_strip_survives_class_flip_back_to_anonymous
    *** HEADLINE TEST ***
    Anonymous → flip to Restricted → apply (strip) → flip back to
    Anonymous → apply → fields STILL None. apply_privacy_gating
    must not resurrect.

  manual_field_restoration_after_strip_only_works_via_explicit_assignment
    The escape hatch is direct field assignment (visible in code
    review), not the soft gate. Confirms: after explicit
    Some(0.42) reassignment + class=Anonymous + apply, the
    values survive.

  apply_at_already_restricted_with_already_none_fields_is_a_noop
    Idempotency on stripped-state.

  one_way_property_holds_through_multiple_class_round_trips
    Stress: 5 Restricted→apply→Anonymous→apply cycles. Fields
    must stay None throughout — no slow-resurrection bug.

  rebuilding_via_with_privacy_gating_is_the_documented_restoration_path
    Pins the doc contract: to publish identity fields again after
    a strip, build a fresh BfldEvent. The constructor accepts
    explicit Some(...) values; apply_privacy_gating then doesn't
    strip because class is Anonymous.

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-120 §2.4 "no promote operation" now structurally proven at the
  SOFT (apply_privacy_gating) path in addition to the EXPLICIT
  (PrivacyGate::demote) path that iter 9 covered. Both layers of
  the privacy gate carry the one-way-only invariant.
- ADR-118 invariant I1 — once stripped, raw identity fields can only
  be re-introduced through paths visible in code review (direct
  field assignment, fresh constructor). No subtle byte-flip path
  resurrects them.

Test config:
- cargo test --no-default-features → 80 passed (event_gating_irreversibility cfg-out)
- cargo test                       → 255 passed (248 + 7)

Out of scope (next iter target):
- PR-readiness pivot: CHANGELOG, witness bundle, AC closeout table.
  External-resource-gated work (KIT BFId, Pi5/Nexmon) still skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p1.8): CRC-32/ISO-HDLC polynomial pinning (262/262 GREEN)

Iter 39. Defends the wire-format CRC contract from silent polynomial
substitution. ADR-119 §2.4 specifies CRC-32/ISO-HDLC (same as Ethernet
and zlib), NOT CRC-32C (Castagnoli) or any other variant. Two BFLD
implementations that disagree on the polynomial treat every frame
from the other as corrupt.

Added (in tests/crc32_polynomial.rs):
- 7 named tests using canonical CRC vectors from the reveng catalogue
  (https://reveng.sourceforge.io/crc-catalogue/all.htm):

  check_string_matches_canonical_iso_hdlc_value
    CRC-32/ISO-HDLC of the standard "123456789" check string is
    0xCBF43926. This is THE canonical vector for the algorithm.

  empty_payload_yields_zero_crc
    init=0xFFFFFFFF, xorout=0xFFFFFFFF → empty payload CRC is 0.

  single_zero_byte_has_a_specific_value
    CRC-32/ISO-HDLC of [0x00] is 0xD202EF8D — well-known constant.

  flipping_a_single_payload_byte_changes_the_crc
    Sensitivity property: any one-bit flip MUST change the CRC.
    Catches a stuck CRC implementation.

  iso_hdlc_distinguishes_from_castagnoli_for_same_input
    CRC-32C/Castagnoli of "123456789" is 0xE3069283.
    Our value MUST differ. Documents the failure mode for a future
    reviewer who fires the test.

  known_short_inputs_have_documented_crcs
    Three additional vectors: "a", "abc", "hello world".
    Each pins a specific 32-bit value against the active polynomial.

  crc_is_deterministic_across_repeated_calls
    Sanity for pure-function correctness.

These tests are no_std-compatible so they run in BOTH feature configs.
The no_default count therefore jumps from 80 to 87.

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.4 "CRC-32/ISO-HDLC" contract — the test surface now
  catches any future PR that swaps the polynomial. crc 4.x ships
  CRC_32_ISO_HDLC alongside half a dozen other CRC-32 variants;
  a typo in src/frame.rs::CRC32_ALG could otherwise silently flip
  the wire-format contract.

Test config:
- cargo test --no-default-features → 87 passed (80 + 7 no_std-compat)
- cargo test                       → 262 passed (255 + 7)

Out of scope (next iter target):
- PR-readiness pivot: CHANGELOG, witness bundle, AC closeout table.
  External-resource-gated work (KIT BFId, Pi5/Nexmon) still skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.8): pipeline gate-state observability (269/269 GREEN)

Iter 40. Pins BfldPipeline::current_gate_action() as a stable operator-
facing diagnostic surface. Iter 11 covered the underlying CoherenceGate
state machine; this iter validates the same transitions through the
public BfldPipeline facade so operators can observe gate behavior
without descending into the lower-level types.

Added (in tests/pipeline_gate_observability.rs, 7 named tests):
  fresh_pipeline_starts_in_accept
  low_risk_processing_stays_in_accept (3 inputs at 0.1^4 risk)
  first_high_risk_input_does_not_immediately_promote_gate
    (pending != current — debounce hasn't elapsed)
  sustained_high_risk_promotes_gate_to_reject_after_debounce
    (two inputs across DEBOUNCE_NS boundary → Reject)
  sustained_recalibrate_grade_score_reaches_recalibrate
    (same pattern with 1.0^4 score → Recalibrate)
  returning_to_low_risk_restores_accept_via_hysteresis
    (round trip: 0.9^3 * 0.85 PredictOnly → 0.1^4 Accept via debounce)
  current_gate_action_is_read_only_does_not_advance_state
    *** Important property for operator-facing surface ***
    Three reads between processes must return the same value and not
    perturb pipeline state. A polling monitor calling this in a tight
    loop must not influence what the next process() observes.

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-118 §2.1 operator diagnostic surface — current_gate_action()
  now provably read-only and observably transitioning through the
  full 4-action band. Operators wiring HA notifications or fleet
  dashboards to "gate Reject means something to investigate" have
  a stable contract.
- ADR-121 §2.4 + §2.5 — gate transitions visible at the facade
  layer match the underlying CoherenceGate semantics; hysteresis
  and debounce work end-to-end through process().

Test config:
- cargo test --no-default-features → 80 passed (gate_observability cfg-out)
- cargo test                       → 269 passed (262 + 7)

Out of scope (next iter target):
- PR-readiness pivot: CHANGELOG batch, witness bundle regeneration,
  AC closeout table for the eventual PR description. All 5 ACs of
  ADR-118 / 7 ACs of ADR-119 / 7 ACs of ADR-120 / 7 ACs of ADR-121 /
  6 ACs of ADR-122 are now covered by iters 1-40. Remaining work is
  external-resource-gated (KIT BFId, Pi5/Nexmon hardware) or PR-prep.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p1.9): PrivacyClass capability-helper truth tables (279/279 GREEN)

Iter 41. Pins the const-helper API (PrivacyClass::allows_network /
allows_matter) and proves it stays in sync with the Sink::MIN_CLASS
trait-level enforcement. Drift between these two APIs would be a
silent correctness bug — an operator checking allows_network() might
get a different answer than the actual NetworkSink::check_class()
runtime gate.

Added (in tests/privacy_class_capability.rs, no_std-compatible):
- 10 named tests, all green:

  allows_network_truth_table     (4 classes × bool)
  allows_matter_truth_table      (4 classes × bool)
  allows_matter_implies_allows_network
    Monotonicity: Matter is a strict subset of Network. Any class
    that allows Matter MUST allow Network. The reverse is not true
    (Derived is Network-eligible but not Matter-eligible).
  allows_network_strictly_excludes_raw
    Class 0 is the ONLY class that fails allows_network. Any future
    refactor that lets Raw cross a NetworkSink violates ADR-118 I1.
  allows_matter_strictly_requires_class_two_or_three
  local_sink_accepts_every_class_per_helper
    Cross-consistency: LocalSink::MIN_CLASS = Raw, accepts all.
  network_sink_consistency_matches_allows_network
    For every class, check_class<NetworkKind> agrees with allows_network().
  matter_sink_consistency_matches_allows_matter
    Same for Matter.
  as_u8_returns_documented_byte_values    (0, 1, 2, 3)
  class_byte_ordering_matches_information_density  (raw < derived < anon < restr)

Helper:
  check_consistency<S: Sink>(class, helper_says_allowed) compares the
  Boolean helper against (class_byte >= S::MIN_CLASS.as_u8()) and asserts
  equality. Catches drift before it reaches operator-visible behavior.

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-118 invariant I1 reinforced at the const-helper layer: a future
  PR refactoring PrivacyClass::Raw to be Network-eligible breaks 4 of
  the 10 tests (truth table + monotonicity + Raw exclusion + sink
  consistency), so the regression is loud rather than silent.
- ADR-120 §2.2 sink-class contract pinned at the helper layer. The
  iter 3 (Sink + check_class) and iter 1 (allows_network) APIs now
  have a regression test enforcing their agreement.

Test config:
- cargo test --no-default-features → 90 passed (+10 no_std-compat)
- cargo test                       → 279 passed (269 + 10)

Out of scope (next iter target):
- PR-readiness pivot remains the genuine next step: CHANGELOG batch,
  witness bundle regeneration, AC closeout table. All ADR-118/119/120/
  121/122 ACs are now empirically covered. External-resource-gated
  work (KIT BFId, Pi5/Nexmon hardware) stays skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.9): BfldError Display format pinning (290/290 GREEN)

Iter 42. Pins the thiserror-derived Display output for every BfldError
variant. Operators grep log lines for these strings; format drift
between minor versions breaks monitoring queries and alerting rules.
This iter locks the contract.

Added (in tests/bfld_error_display.rs, 11 named tests):
- One test per BfldError variant asserting the documented substrings
  appear in to_string():
    invalid_magic_displays_both_expected_and_actual_in_hex
    unsupported_version_displays_the_offending_version
    crc_mismatch_displays_both_values_in_hex
    privacy_violation_displays_the_sink_reason
    invalid_privacy_class_displays_the_offending_byte
    truncated_frame_displays_got_and_need_byte_counts
    malformed_section_displays_offset_and_reason
    invalid_demote_displays_both_from_and_to_class_bytes
- Meta tests:
    bfld_error_implements_std_error_trait
      (compile-time witness via fn assert_error_trait<E: std::error::Error>())
    bfld_error_is_debug_so_panic_unwrap_messages_carry_diagnostics
    every_variant_has_a_non_empty_display_string
      (catch-all: 8 variants × non-empty Display assertion;
       guards against a future PR that adds a new variant without
       the #[error(...)] attribute)

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-118 §2.1 operator observability — error-message contract now
  pinned. A monitoring rule that greps for "payload CRC mismatch"
  or "privacy violation" continues to fire correctly across BFLD
  versions.

Test config:
- cargo test --no-default-features → 90 passed (bfld_error_display cfg-out)
- cargo test                       → 290 passed (279 + 11)

Out of scope (next iter target):
- PR-readiness pivot remains the genuine next move: CHANGELOG batch,
  witness bundle regeneration, AC closeout table. All in-crate ACs
  empirically covered; remaining work is external-resource-gated
  (KIT BFId, Pi5/Nexmon hardware) or PR-prep.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p1.10): frame parser trailing-bytes contract (296/296 GREEN)

Iter 43. Pins BfldFrame::from_bytes behavior on buffers carrying bytes
past `BFLD_HEADER_SIZE + header.payload_len`. The parser currently
accepts these and silently slices to the declared length. Useful when
the transport (UDP MTU padding, ESP-NOW trailer alignment) adds noise
the application layer doesn't strip.

Pinning this behavior makes any future tightening (reject as
MalformedFrame) a deliberate, traceable policy change rather than
silent breakage.

Added (in tests/frame_trailing_bytes.rs, 6 named tests):
  parser_accepts_buffer_with_one_trailing_byte
    (smoke: one extra 0xFF byte tolerated; payload.last() != Some(0xFF))
  parser_accepts_many_trailing_bytes
    (256 trailing bytes — UDP MTU padding scale)
  parsed_payload_round_trips_back_to_typed_payload_with_trailing_bytes_present
    *** Sanity: trailing-bytes leniency must not corrupt the section
        parser downstream. from_bytes → parse_payload still yields
        the original BfldPayload byte-for-byte. ***
  header_only_buffer_at_exactly_header_size_with_zero_payload_len_succeeds
    (boundary: empty-payload frame is exactly 86 bytes)
  header_only_buffer_with_trailing_bytes_but_zero_payload_len_ignores_them
    (100 trailing bytes; parsed.payload stays empty)
  trailing_bytes_do_not_affect_crc_validation_when_payload_intact
    (CRC is over payload bytes only; 32 trailing bytes leave CRC
     intact and parse succeeds)

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 wire-format parser contract: trailing-bytes tolerance is
  now an explicit, tested behavior. Operators building stream-based
  frame readers (where multiple frames concatenate) know the parser
  treats `header.payload_len` as authoritative, not buffer.len().

Test config:
- cargo test --no-default-features → 90 passed (frame_trailing_bytes cfg-out)
- cargo test                       → 296 passed (290 + 6)

Out of scope (next iter target):
- PR-readiness pivot: CHANGELOG, witness bundle, AC closeout table.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p3.4): CoherenceGate clock-skew resilience (303/303 GREEN)

Iter 44. Pins the gate's saturating_sub-based debounce as safe under
clock perturbation. NTP rollback, system-clock adjustment, monotonic-
source switch — all can produce a backward `timestamp_ns` between
calls. The gate must NOT promote spuriously on backward jumps and
MUST NOT panic on identical / zero / u64::MAX-ish timestamps.

Added (in tests/gate_clock_skew.rs, no_std-compatible):
- 7 named tests, all green:

  backward_jump_after_pending_does_not_promote_prematurely
    Pending at t = DEBOUNCE_NS + 100; backward jump to t = 0.
    saturating_sub(0, DEBOUNCE_NS+100) = 0 < DEBOUNCE_NS → no promotion.

  forward_recovery_after_backward_jump_still_promotes_correctly
    Backward jump doesn't corrupt the pending `since` stamp; once wall
    time advances past since + DEBOUNCE_NS, promotion fires normally.

  identical_timestamps_across_repeated_polls_do_not_progress_state
    Five identical timestamps in a row — gate never promotes; both
    current and pending remain stable. Important for HA dashboards
    polling at >1Hz: the polling itself must not cause transitions.

  backward_jump_with_no_pending_is_a_noop
    Edge: no pending in flight, backward jump — gate stays clean.

  very_large_forward_jump_promotes_but_does_not_panic
    Stress: t = u64::MAX/2 jump. No overflow, no panic, promotes.

  backward_then_forward_into_different_action_band_resets_pending_correctly
    More subtle: pending PredictOnly → backward jump WITH a different
    score (recalibrate-grade) — pending target changes, debounce
    clock resets to the new (smaller) timestamp; forward by DEBOUNCE_NS
    promotes to Recalibrate.

  no_panic_on_zero_timestamp_with_predict_only_pending
    Regression guard: a poorly-initialized monotonic clock could
    deliver t=0 as the first sample. Gate must not panic.

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-121 §2.5 debounce property — saturating_sub usage now has a
  regression test. A future PR that swaps to plain `-` (panic on
  underflow) fires `no_panic_on_zero_timestamp_with_predict_only_pending`.
- ADR-118 §2.1 operator-facing diagnostic safety — current_gate_action
  polled at the same timestamp from a Prometheus exporter or HA
  dashboard cannot cause unintended state transitions.

Test config:
- cargo test --no-default-features → 97 passed (90 + 7 no_std-compat)
- cargo test                       → 303 passed (296 + 7)

Out of scope (next iter target):
- PR-readiness pivot still pending: CHANGELOG, witness bundle,
  AC closeout table. External-resource-gated work (KIT BFId,
  Pi5/Nexmon) still skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.10): public API surface snapshot (308/308 GREEN)

Iter 45. Compile-time witness that every `pub use` re-export from
lib.rs survives refactors. A future PR removing one fires a named
test failure instead of producing a silent SemVer break.

Added (in tests/public_api_snapshot.rs):
- 5 named tests across feature flags:

  always_available_types_are_re_exported (no_std-compatible)
    Witnesses PrivacyClass, GateAction, MatchOutcome, BfldFrameHeader,
    CoherenceGate, NullOracle, EmbeddingRing, SignatureHasher,
    IdentityEmbedding + 11 const re-exports + 5 flag bits.

  sink_trait_hierarchy_re_exported (no_std-compatible)
    Witnesses Sink, LocalSink, NetworkSink, MatterSink, LocalKind,
    NetworkKind, MatterKind + check_class function. Trait bounds
    asserted via fn assert_sink<S: Sink>() etc. so missing impls
    fire here too.

  soul_match_oracle_trait_re_exported (no_std-compatible)
    Witnesses SoulMatchOracle trait + NullOracle impl.

  bfld_error_re_exported_with_all_named_variants (no_std-compatible)
    Constructs every BfldError variant — removing one fires.

  std_only_types_are_re_exported (gated on `std`)
    BfldConfig, BfldPipeline, BfldEmitter, PrivacyGate,
    CapturePublisher, BfldPipelineHandle, PipelineInput,
    SensingInputs, IdentityFeatures, BfldEvent, BfldFrame,
    BfldPayload, TopicMessage + 12 free-function re-exports
    (identity_risk_score, availability_topic, online_message,
    offline_message, publish_availability_*, publish_discovery,
    publish_event, render_*, with_privacy_gating) +
    PAYLOAD_AVAILABLE, PAYLOAD_NOT_AVAILABLE, RISK_FACTOR_BYTES.

  mqtt_publisher_types_are_re_exported (gated on `mqtt`)
    RumqttPublisher type + with_lwt free function signature.

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-118 §2.1 public-API stability — every documented re-export
  has a named-symbol regression test. Accidental removal fires
  loudly at build time rather than as a silent SemVer break on
  downstream consumers (cog-ha-matter, wifi-densepose-sensing-server,
  pip wifi-densepose, sibling-agent SENSE-BRIDGE crate).

Test config:
- cargo test --no-default-features → 101 passed (97 + 4 no_std-compat
  — the std-only mod test is cfg-out)
- cargo test                       → 308 passed (303 + 5)

Out of scope (next iter target):
- PR-readiness pivot still pending: CHANGELOG batch across iters
  1-45, witness bundle regeneration, AC closeout table for the PR
  description. External-resource-gated work (KIT BFId, Pi5/Nexmon)
  still skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.11): presence detection latency p95 (ADR-119 AC2) — 311/311 GREEN

Iter 46. Closes ADR-119 AC2 ("Presence detection latency is ≤ 1s p95
from the first non-empty BFI frame in a new occupancy event"). Per-
call BfldPipeline::process() latency measured at the public facade
surface via pure std::time::Instant — no criterion dep.

Empirically measured on this Windows host (debug build):
- p50:           0.9µs    (1.1M frames/sec)
- p95:           0.9µs    (~1,000,000× under the 1s AC2 target)
- p99:           1.2µs
- First call:    2.9µs    (no lazy-init regression)
- Long-run growth: 1.55× from first-100 mean to last-100 mean
                  (10× ceiling guards against unbounded internal state)

Added (in tests/presence_latency.rs):
- pub const ADR_119_AC2_P95_TARGET = Duration::from_secs(1) (the AC number)
- const DEBUG_P95_FLOOR = Duration::from_millis(100) (generous CI floor)

Three named tests, all green:
  process_call_p95_latency_meets_debug_floor
    500 samples after a 50-sample warmup, sort, take p50/p95/p99,
    print to stderr, assert p95 <= 100ms AND p95 <= 1s.
  first_call_after_pipeline_construction_is_not_pathologically_slow
    Operator-visible "first event after node boot" latency. Bounded
    at 250ms — catches a constructor that defers work to first
    process() call (would show as a 100ms+ spike on a Pi 5 boot).
  latency_does_not_grow_unbounded_over_long_runs
    Compares first-100 sample mean vs last-100 over 500 calls;
    ratio < 10× guards against memory-leak-style regressions.

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 AC2 closed — p95 latency runs 6 orders of magnitude under
  the 1s target. Release-build margin is comfortable.
- ADR-118 §2.1 operator-perceived performance — first-call and
  long-run latency guards complement iter 32's serialization
  throughput bench (header 1.65M/s, full-frame 320k/s). Pipeline
  latency is dominated by the BFI capture step, not BFLD processing.

Test config:
- cargo test --no-default-features → 101 passed (presence_latency cfg-out)
- cargo test                       → 311 passed (308 + 3)

Out of scope (next iter target):
- PR-readiness pivot remains the genuine next step. All in-crate ACs
  empirically covered; remaining work is external-resource-gated
  (KIT BFId, Pi5/Nexmon) or PR-prep.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.12): examples/bfld_minimal.rs operator quickstart (315/315 GREEN)

Iter 47. Ships the operator-facing quickstart as doc-as-code. Three
goals:

1. New operators reading the crate get a 50-line working example
   instead of having to assemble pipeline + config + hasher + inputs
   + embedding + JSON publish themselves.
2. CI proves the example COMPILES and RUNS end-to-end via a
   separate test that re-executes the same flow inline.
3. The example output is the canonical BfldEvent JSON, demonstrating
   every documented field (presence/motion/count/conf/zone/class/
   identity_risk_score/rf_signature_hash) for a typical Anonymous
   class publish.

Added:
- v2/crates/wifi-densepose-bfld/examples/bfld_minimal.rs (~70 LOC):
    * Per-site secret salt
    * BfldPipeline::new(BfldConfig::new(...).with_signature_hasher(...))
    * SensingInputs with low-risk factors so the gate emits
    * IdentityEmbedding from a deterministic ramp
    * pipeline.process(...).ok_or(...) for the gate-drop case
    * event.to_json() printed to stdout
    * Run command in the doc comment:
        cargo run -p wifi-densepose-bfld --example bfld_minimal

- v2/crates/wifi-densepose-bfld/tests/example_minimal.rs (4 tests):
    minimal_example_documents_the_operator_quickstart_flow
      (asserts file contains BfldPipeline, SignatureHasher,
       SensingInputs, IdentityEmbedding, BfldConfig, .process(,
       to_json — catches doc drift if the example removes a key
       symbol)
    minimal_example_carries_run_instructions_in_doc_comments
      (the cargo run --example line must be present)
    minimal_example_flow_produces_valid_json_with_documented_fields
      *** Re-runs the example flow inline and asserts every
          documented JSON field appears in the output ***
    example_returns_box_dyn_error_for_main_signature
      (canonical Rust-example main signature)

- v2/crates/wifi-densepose-bfld/Cargo.toml:
    [[example]] name = "bfld_minimal", required-features = ["serde-json"]
    so `cargo test --no-default-features` doesn't try to build the
    example (which needs to_json gated on serde-json).

Example run output (sanity check before commit):
  {"type":"bfld_update","node_id":"seed-example","timestamp_ns":...,
   "presence":true,"motion":0.42,"person_count":1,"confidence":0.91,
   "privacy_class":"anonymous","identity_risk_score":0.0016000001,
   "rf_signature_hash":"blake3:cc3615c7aaab9d0867a0c15327444b8f...bf"}

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-118 §2.1 documentation surface — first operator-facing example
  shipped as part of the crate. Discoverable via
  `cargo run --example bfld_minimal` and verified via cargo test.

Test config:
- cargo test --no-default-features → 101 passed (example_minimal cfg-out)
- cargo test                       → 315 passed (311 + 4 example_minimal)

Out of scope (next iter target):
- PR-readiness pivot still pending: CHANGELOG, witness bundle,
  AC closeout table. External-resource-gated work still skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-118/p6.13): examples/bfld_handle.rs worker-thread pattern (319/319 GREEN)

Iter 48. Ships the production-recommended operator example: full
lifecycle through the worker-thread handle. Companion to iter-47's
minimal example which uses BfldPipeline::process directly. The
handle example demonstrates the multi-thread pattern operators
actually deploy with HA + MQTT.

Lifecycle demonstrated in the example:
  1. publish_availability_online (retained → HA marks device online)
  2. publish_discovery (retained → HA auto-creates 6 BFLD entities)
  3. BfldPipelineHandle::spawn (worker owns gate + ring + hasher)
  4. handle.send(input) per BFI frame (worker process + publish)
  5. handle.shutdown() (clean worker join)
  6. publish_availability_offline (explicit graceful disconnect)

Example output (verified pre-commit):
  bootstrap: 1 availability + 6 discovery payloads
  total messages published: 33
  first three topics:
    ruview/seed-handle-demo/bfld/availability
    homeassistant/binary_sensor/seed-handle-demo_bfld_presence/config
    homeassistant/sensor/seed-handle-demo_bfld_motion/config
  last three topics:
    ruview/seed-handle-demo/bfld/confidence/state
    ruview/seed-handle-demo/bfld/identity_risk/state
    ruview/seed-handle-demo/bfld/availability

Added:
- v2/crates/wifi-densepose-bfld/examples/bfld_handle.rs (~110 LOC):
    * Documents the 6-phase lifecycle with inline comments
    * Pointer to RumqttPublisher::connect_with_lwt for prod use
    * 5 sensing frames × 5 state topics = 25 per-frame messages
- v2/crates/wifi-densepose-bfld/tests/example_handle.rs (4 named tests):
    handle_example_documents_full_lifecycle_phases
      (doc drift guard: 8 operator-facing symbols must appear)
    handle_example_carries_run_instructions_and_prod_pointer
      (cargo run line + RumqttPublisher pointer present)
    handle_example_lifecycle_produces_expected_message_counts
      *** Re-executes full lifecycle inline; asserts total == 33,
          first message payload == "online", last == "offline" ***
    handle_example_returns_box_dyn_error_for_main_signature
- v2/crates/wifi-densepose-bfld/Cargo.toml:
    [[example]] name = "bfld_handle", required-features = ["std"]

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-118 §2.1 documentation surface — two runnable operator examples
  now shipped (iter 47 minimal, iter 48 worker-thread). Together
  they cover the two operator patterns: simple in-process consumer
  (process + to_json) and the full HA-integration deployment
  (handle + bootstrap + lifecycle).
- ADR-122 §2.1 + §2.2 + §2.6 — the worker example exercises every
  layer of the HA-DISCO publish chain in one runnable file:
  availability, discovery, state, graceful shutdown.

Test config:
- cargo test --no-default-features → 101 passed (example_handle cfg-out)
- cargo test                       → 319 passed (315 + 4)

Out of scope (next iter target):
- PR-readiness pivot still pending. External-resource-gated work
  (KIT BFId, Pi5/Nexmon) still skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-118/p6.14): crate README.md + Cargo.toml readme field (327/327 GREEN)

Iter 49. Ships the crate's first README — genuinely missing artifact.
crates.io renders this file; the rendered page is what downstream
operators see when they `cargo doc --open` or browse the registry.

Added:
- v2/crates/wifi-densepose-bfld/README.md (~135 lines):
    * Three structural invariants (I1/I2/I3) table with enforcement
      mechanism per invariant
    * Quickstart snippet: in-process consumer (BfldPipeline::process)
    * Quickstart snippet: production worker (BfldPipelineHandle +
      bootstrap helpers)
    * Feature flag matrix (std / serde-json / mqtt / soul-signature)
    * Two runnable example invocations
    * Testing matrix (no_default / default / mqtt)
    * Companion artifacts pointer (ADRs, research bundle, HA
      blueprints, CI workflow)
    * ADR cross-reference table (ADR-118 through ADR-123)
    * BFLD_MQTT_BROKER env-var doc for live mosquitto opt-in

- v2/crates/wifi-densepose-bfld/Cargo.toml:
    readme = "README.md"
    (so crates.io picks it up on publish)

- v2/crates/wifi-densepose-bfld/tests/crate_readme.rs (8 tests):
    readme_documents_three_structural_invariants
    readme_documents_feature_flag_matrix
    readme_documents_both_runnable_examples
    readme_documents_three_test_invocations
    readme_references_companion_adrs_118_through_123
    readme_quickstart_uses_canonical_public_api
      (8 symbol-presence checks: BfldPipeline::new, BfldConfig::new,
       SignatureHasher::new, SensingInputs, IdentityEmbedding::from_raw,
       pipeline.process, publish_availability_online, publish_discovery,
       BfldPipelineHandle::spawn, PipelineInput)
    readme_points_at_research_bundle_and_blueprints
    readme_documents_env_gated_mosquitto_integration

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-118 §2.1 documentation surface — crates.io / cargo doc landing
  page now exists. Operators encountering wifi-densepose-bfld for the
  first time get the three structural invariants, quickstart snippets
  for both deployment patterns, feature matrix, and ADR map without
  having to read source.

Test config:
- cargo test --no-default-features → 101 passed (crate_readme cfg-out)
- cargo test                       → 327 passed (319 + 8)

Out of scope (next iter target):
- PR-readiness pivot. CHANGELOG, witness bundle, AC closeout table.
  External-resource-gated work (KIT BFId, Pi5/Nexmon) still skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-118): CHANGELOG [Unreleased] BFLD entry + validation test (332/332 GREEN)

Iter 50. PR-readiness pivot iter #1. Lands the BFLD entry under
CHANGELOG.md's [Unreleased] section per the project's pre-merge
checklist (CLAUDE.md). Plus a validation test that catches drift if
someone edits the entry and breaks the operator-facing summary.

Added (in CHANGELOG.md):
- New top-of-[Unreleased]-Added bullet for BFLD spanning:
  * ADR-118 umbrella + invariants I1/I2/I3 + their enforcement
    mechanism (Sink traits / Drop+no-Serialize / per-site BLAKE3)
  * ADR-119 frame format (86-byte header, payload sections, CRC32)
  * ADR-120 privacy classes + PrivacyGate::demote + apply_privacy_gating
  * ADR-121 multiplicative risk score + CoherenceGate + SoulMatchOracle
  * ADR-122 MQTT topic router + HA discovery + availability + LWT
  * ADR-123 capture path (reference; production capture is Pi5/Nexmon
    hardware-gated and remains skipped)
  * BfldPipelineHandle worker + spawn_with_oracle for Soul Signature
  * 3 operator HA blueprints (presence-lighting / motion-HVAC /
    identity-risk-anomaly)
  * Two runnable examples (bfld_minimal, bfld_handle)
  * eclipse-mosquitto:2 CI service container workflow
  * Performance measurements: 320k frames/sec, p95 0.9µs, 9.96 Hz
  * 327 default-feature tests, 101 no_std-compatible, 220+ with mqtt
  * Companion research dossier docs/research/BFLD/ (11 files, 13,544 words)
  * try-it command: cargo run -p wifi-densepose-bfld --example bfld_handle

Added (in tests/changelog_entry.rs, 5 tests):
- changelog_documents_bfld_entry_under_unreleased
    Slices CHANGELOG from `## [Unreleased]` to the first numbered
    version header and asserts the block contains BFLD,
    wifi-densepose-bfld, and the #787 tracking link.
- changelog_bfld_entry_cites_companion_adrs
    Substring asserts ADR-118..123 each appear at least once.
- changelog_bfld_entry_names_three_structural_invariants
    **I1**, **I2**, **I3** must be called out by name.
- changelog_bfld_entry_documents_a_runnable_example
    Operators get a copy-pasteable cargo command.
- changelog_bfld_entry_references_research_bundle

Caught + fixed during iter:
- First draft used "ADR-118 through ADR-123" shorthand; the
  per-ADR substring test fired for ADR-120 (not literally present).
  Re-wrote the parenthetical to "ADR-118 umbrella + ADR-119 frame
  format + ADR-120 privacy class + ADR-121 identity risk scoring +
  ADR-122 RuView HA/Matter exposure + ADR-123 capture path" so each
  ADR number is its own grep-discoverable token.

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:
- Pre-merge checklist item #5 (CLAUDE.md) — CHANGELOG `[Unreleased]`
  entry shipped. PR description can now link to the line + commit
  range as evidence.

Test config:
- cargo test --no-default-features → 101 passed (changelog_entry cfg-out)
- cargo test                       → 332 passed (327 + 5)

Out of scope (next iter target):
- Pre-merge checklist remaining: README.md update (#3 — points at the
  new crate from the workspace level), user-guide.md (#6), witness
  bundle regeneration (#8). External-resource-gated work (KIT BFId,
  Pi5/Nexmon) still skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-118): root README Documentation table BFLD row (337/337 GREEN)

Iter 51. PR-readiness pivot iter #2. Adds BFLD to the workspace-root
README.md Documentation table — closes pre-merge checklist item #3
(README.md update if scope changed). GitHub renders this; new
contributors / operators browsing ruvnet/RuView see the entry on
landing.

Added (in README.md, top-level Documentation table):
- New row right after the Home Assistant + Matter row, linking to
  v2/crates/wifi-densepose-bfld/README.md (iter-49 crate README).
- Summary covers:
    * 3 type-enforced structural invariants
      (raw BFI never exits / in-RAM-only embedding / cross-site
       cryptographically impossible)
    * Full operator surface (BfldPipeline, BfldPipelineHandle,
      SoulMatchOracle)
    * MQTT topic router + HA-DISCO + availability + LWT
    * 3 operator HA blueprints
    * Two runnable examples
    * eclipse-mosquitto:2 CI service container
    * 327+ tests
- Per-ADR links: 118 (umbrella), 119 (frame), 120 (privacy class),
  121 (risk scoring), 122 (HA/Matter), 123 (capture path)
- Research dossier pointer: docs/research/BFLD/ (11 files, 13,544 words)

Added (in v2/crates/wifi-densepose-bfld/tests/root_readme_link.rs):
- 5 named tests via include_str!:
    root_readme_links_to_bfld_crate_readme
    root_readme_mentions_bfld_acronym_and_full_name
    root_readme_cites_all_six_bfld_adrs (per-ADR substring check)
    root_readme_points_at_research_bundle
    root_readme_documents_three_structural_invariants_in_summary
      ("raw BFI never exits", "in-RAM-only", "cross-site" — three
       invariants surfaced in the short table summary)

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:
- Pre-merge checklist item #3 (CLAUDE.md) — root README updated to
  point at the new crate. Operator discovery path now reaches BFLD
  from the GitHub repo landing page in 1 click.
- ADR-118 §2.1 documentation surface — discovery path complete:
  GitHub README → crate README → operator examples → ADRs → research
  dossier. All hops covered by include_str + link tests.

Test config:
- cargo test --no-default-features → 101 passed (root_readme_link cfg-out)
- cargo test                       → 337 passed (332 + 5)

Out of scope (next iter target):
- Pre-merge checklist remaining: user-guide.md update (#6) if new CLI
  flags / setup steps, witness bundle regeneration (#8). External-
  resource-gated work (KIT BFId, Pi5/Nexmon) still skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-124): RUVIEW-POLICY layer + Q4 cache resolution + multi-modal vision

Three additive sections per maintainer review of SENSE-BRIDGE
(the original 13-section draft is unchanged below; these are
inserts):

§4.1a — RUVIEW-POLICY governance layer (NEW). Five tools:
- ruview.policy.can_access_vitals(agent_id, node_id, vital)
- ruview.policy.can_query_presence(agent_id, scope, node_id?, zone?)
- ruview.policy.can_subscribe(agent_id, topic, duration_s)
- ruview.policy.redact_identity_fields(payload, agent_id)
- ruview.policy.audit_log(agent_id?, since_ts?)

Enforcement is server-side, not client-side — agents cannot bypass.
Default policy when no file exists: deny vitals + audit_log; allow
presence.now + node.list; allow primitives.list_active with
redact_identity_fields applied. "Explore safely" default.

Q4 — RESOLVED. The library MUST take continuous local cache +
event-driven invalidation + bounded freshness windows. Tools
never wait on the next CSI frame; cache hits return in <1 ms;
every tool accepts max_age_ms and returns
{ value: null, reason: "stale", last_seen_ms, threshold_ms }
when stale rather than blocking. Decouples agent orchestration
latency from RF acquisition jitter — required to scale to dozens
of concurrent Streamable HTTP sessions per Q8.

§11.3 — Strategic implication: ambient-sensing normalization
layer (NEW). The §4 tool catalog shape is modality-agnostic.
Same surface absorbs BLE / mmWave (already on COM4) / LiDAR /
thermal / camera / radar / UWB. Position as semantic-environment
API, not WiFi client. Follow-on ADR-13x RUVIEW-FUSION formalizes
per-modality adapter contract. Out of scope for 124; designed in.

§11.2 risk table — added the "sensing-tool surface becomes
surveillance API" row, mitigation = RUVIEW-POLICY layer + server-
side redaction.

Refs: docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md

* feat(adr-124/packaging): rename to @ruvnet/rvagent 0.1.0 + manifest test (ADR-124 §2)

Advances SPARC Phase 1 (Specification) for ADR-124 SENSE-BRIDGE by establishing
the correct npm package identity that all subsequent implementation iters depend on.

Changes:
- tools/ruview-mcp/package.json
  - name: @ruv/ruview-mcp → @ruvnet/rvagent  (ADR-124 §2.1)
  - version: 0.0.1 → 0.1.0  (initial publishable milestone)
  - removed private:true so the package is publishable  (ADR-124 §2.6)
  - bin: added rvagent key alongside legacy ruview-mcp alias  (ADR-124 §2.4)
  - exports: added "." entry with import+types keys for ESM+CJS dual output  (ADR-124 §2.5)
  - files: added README.md and CHANGELOG.md slots  (ADR-124 §5 npm publish plan)
  - keywords: expanded with sense-bridge, rvagent, ruvnet
  - repository / homepage / bugs: wired to github.com/ruvnet/RuView

- tools/ruview-mcp/src/index.ts
  - SERVER_NAME: "ruview" → "rvagent"
  - PACKAGE_VERSION: "0.0.1" → "0.1.0"
  - stderr log prefix: [ruview-mcp] → [@ruvnet/rvagent]

- tools/ruview-mcp/tests/manifest.test.ts  (NEW)
  - 10 ADR-124 §2 acceptance-criterion assertions, all green
  - Guards name, version >=0.1.0, engines.node >=20, bin.rvagent, exports structure,
    publishConfig.access, @modelcontextprotocol/sdk dep, zod dep, ESM type, license

Test results: 26/26 PASS (manifest.test.ts ×10 + tools.test.ts ×5 + validate.test.ts ×11)
Build: tsc clean, zero errors.

Next iter target: (A) Zod schema barrel for the 15+5 tool catalog from ADR-124 §4.1/4.1a

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-124/pseudocode): Zod schema barrel for all 20 ADR-124 §4.1+§4.1a tools

Advances SPARC Phase 2 (Pseudocode) — typed schemas are the language-level
design artifact that defines the complete tool surface before any HTTP/WS
plumbing is written. The schema map + TOOL_NAMES catalog are the pseudocode
contract that Phase 3 (Architecture) wires to the MCP Server dispatch loop.

New files under tools/ruview-mcp/src/schemas/:

  common.ts — shared Zod sub-schemas
    NodeIdSchema, DurationSSchema (max 3600 s), WindowSSchema (max 300 s),
    SemanticPrimitiveKindSchema (10 ADR-115 primitives enum), PosePersonResultSchema
    (17-keypoint COCO array + confidence + optional AETHER person_id)

  tools.ts — 20 input schemas + TOOL_NAMES catalog + TOOL_INPUT_SCHEMAS dispatch map
    §4.1 sensing (15): presence.now, vitals.get_{breathing,heart_rate,all},
      pose.{latest,subscribe}, primitives.{get,list_active,subscribe},
      bfld.{last_scan,subscribe}, node.{list,status},
      vector.{search_pose,store_pose}
    §4.1a policy (5): policy.{can_access_vitals, can_query_presence,
      can_subscribe, redact_identity_fields, audit_log}

  index.ts — barrel re-export of both modules

New test: tests/schemas.test.ts (24 assertions)
  - Catalog completeness: exactly 20 tools, all §4.1 + §4.1a names present,
    TOOL_INPUT_SCHEMAS one-to-one with catalog (no extras)
  - Happy-path parse: 11 representative schemas accept valid inputs
  - Constraint rejection: 8 schemas reject invalid inputs (empty NodeId,
    DurationS=0 / >3600, unknown primitive, wrong keypoint length, k>100,
    unknown vital, missing required node_id)

Fix: use Object.prototype.hasOwnProperty instead of Jest toHaveProperty for
dotted-key names (Jest interprets dots as nested path separators).

Test results: 50/50 PASS (schemas ×24 + manifest ×10 + tools ×5 + validate ×11)
Build: tsc clean, zero errors.

ACs touched: ADR-124 §4.1 complete tool surface; §4.1a policy layer surface;
  Phase 2 gate: pseudocode covers all acceptance criteria from spec.

Next iter target: Phase 3 (Architecture) — wire TOOL_INPUT_SCHEMAS into the
  MCP Server CallTool handler as a uniform validation gate; add Streamable HTTP
  transport scaffold with Origin-validation middleware (option C).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-124/architecture): schema-validation gate + Streamable HTTP transport (ADR-124 §3)

Advances SPARC Phase 3 (Architecture): wires the phase-2 schema barrel into
the MCP CallTool dispatch loop, and scaffolds the Streamable HTTP transport
with Origin-validation and bearer-token auth as specified in ADR-124 §3/§6.

Sub-task (a) — Uniform Zod validation gate in src/index.ts:
  - Import TOOL_INPUT_SCHEMAS + McpError + ErrorCode from SDK
  - CallTool handler: before dispatch, looks up schema by tool name using
    Object.prototype.hasOwnProperty (safe for dotted keys) then runs
    schema.safeParse(args); failures throw McpError(InvalidParams) so the
    caller receives a typed JSON-RPC error rather than a wrapped string
  - Re-throws McpError instances unchanged (policy errors propagate cleanly)

Sub-task (b) — src/http-transport.ts (new, 145 LOC):
  - buildHttpApp(mcpServer, opts): creates Node.js http.Server +
    StreamableHTTPServerTransport without binding; testable in isolation
  - createHttpTransport(mcpServer, opts): binds and resolves when listening
  - isOriginAllowed(origin, allowedOrigins): pure function — undefined origin
    allowed (non-browser), present origin validated against allowlist,
    '*' disables gate for local-dev
  - Bearer-token gate: RVAGENT_HTTP_TOKEN env or opts.bearerToken; missing/
    wrong token → 401 before any JSON-RPC processing
  - Bind default: 127.0.0.1 per MCP spec security requirement (ADR-124 §3)
  - Transport connect() only in createHttpTransport (not buildHttpApp) to
    avoid exactOptionalPropertyTypes false-incompatibility in test contexts

New test: tests/http-transport.test.ts (11 assertions):
  - isOriginAllowed() unit ×5: undefined allowed, allowlist hit/miss, wildcard,
    case-sensitivity (RFC 6454)
  - Origin-validation integration ×3: cross-origin → 403 with error body,
    allowed origin → non-403, no Origin → non-403
  - Bearer-token integration ×3: missing → 401, wrong → 401, correct → non-401

Fix: @types/express added as devDep (express is transitive from SDK ^1.29.0).

Test results: 61/61 PASS (+11 new)
Build: tsc clean, zero errors.

ACs touched: ADR-124 §3 (dual-transport architecture), §6 (Origin validation,
  127.0.0.1 bind, bearer-token auth slot). SPARC Phase 3 gate criteria met:
  API contracts typed, module boundaries established, no circular deps.

Next iter target: Phase 4 (Refinement) — implement ruview.bfld.last_scan +
  ruview.bfld.subscribe tool handlers (BFLD wire format stable post-ADR-118),
  register them in the TOOLS array using the new schema-validation gate.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-124/phase4): BFLD tool family — bfld.last_scan + bfld.subscribe (ADR-124 §4.1)

Advances SPARC Phase 4 (Refinement): implements the first two ADR-124 §4.1
sensing tools, which also serve as integration tests for the schema-validation
gate wired in Phase 3 (iter 3).

New files:
  src/tools/bfld-last-scan.ts
    - bfldLastScanSchema: z.object with optional node_id (min 1) + optional
      sensing_server_url — enforces the ADR-124 §4.1 input contract
    - bfldLastScan(): proxies GET /api/v1/bfld/<node_id>/last_scan from the
      sensing-server; returns BfldLastScanResult{ok,node_id,identity_risk_score,
      privacy_class,n_frames,timestamp_ms} on success
    - Converts BfldEvent.timestamp_ns (ns) → timestamp_ms (ms)
    - Uses person_count as n_frames proxy per ADR-118 BfldEvent shape
    - Returns {ok:false,warn:true} when server unreachable (soft-failure convention)

  src/tools/bfld-subscribe.ts
    - bfldSubscribeSchema: z.object with required duration_s (positive, max 3600)
    - bfldSubscribe(): POST /api/v1/bfld/<node_id>/subscribe?duration_s=<n>
    - Synthetic envelope fallback: when server unreachable, synthesises a valid
      {subscription_id (UUID v4), expires_at, topic} locally so the schema gate
      is always exercised and the caller can track the intent
    - topic format: ruview/<node_id>/bfld/* (ADR-122 §2.2 wildcard)

src/index.ts:
    - Import bfldLastScan + bfldSubscribe
    - Two new TOOLS entries: ruview.bfld.last_scan + ruview.bfld.subscribe
    - Both go through the TOOL_INPUT_SCHEMAS schema-validation gate (iter 3)

New test: tests/bfld-tools.test.ts (14 assertions):
    - bfldLastScan: unreachable → ok:false+warn:true, malformed path,
      ns→ms arithmetic, null identity_risk_score coalescing
    - BfldLastScanInputSchema: empty object accepted, empty node_id rejected
    - bfldSubscribe: subscription_id defined + future expires_at, UUID v4 format,
      expires_at timing accuracy (±50ms), topic pattern match
    - BfldSubscribeInputSchema: duration_s > 3600 rejected, duration_s=0 rejected

Test results: 75/75 PASS (+14). Build: tsc clean.

ACs touched: ADR-124 §4.1 ruview.bfld.last_scan + ruview.bfld.subscribe.
  SPARC Phase 4 gate: acceptance criteria have passing tests; code review
  against spec complete; no critical issues.

Next iter target: Phase 4 continued — ruview.presence.now + ruview.vitals.*
  tool handlers (4 tools), following the same pattern; then Phase 5 (Completion)
  with package metadata, CHANGELOG, and witness-bundle extension.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-124/phase4): presence.now + vitals.get_* tool family (ADR-124 §4.1)

Advances SPARC Phase 4 (Refinement) iter 5: implements ruview.presence.now
and all three ruview.vitals.* tools sharing a single fetchVitals() helper.

src/types.ts:
  - Added EdgeVitalsMessage interface (mirrors Python ws.py:74-88 per ADR-124 §6):
    node_id, timestamp_ms, presence, n_persons, confidence, breathing_rate_bpm,
    heartrate_bpm, motion, zone_id

src/tools/vitals-fetch.ts (new):
  - fetchVitals(nodeId, baseUrl, token): GET /api/v1/vitals/<node_id>/latest
  - Returns VitalsFetchOk | VitalsFetchErr — all four tools project from one fetch
  - resolveNodeId(): "default" fallback for optional node_id

src/tools/presence-now.ts (new):
  - presenceNow(): projects {present, n_persons, confidence, timestamp_ms}

src/tools/vitals-get-breathing.ts (new):
  - vitalsGetBreathing(): projects {breathing_rate_bpm|null, confidence, timestamp_ms}

src/tools/vitals-get-heart-rate.ts (new):
  - vitalsGetHeartRate(): projects {heartrate_bpm|null, confidence, timestamp_ms}

src/tools/vitals-get-all.ts (new):
  - vitalsGetAll(): spreads full EdgeVitalsMessage (raw never present server-side)

src/index.ts:
  - 4 new TOOLS entries; all route through Phase 3 schema-validation gate

tests/vitals-tools.test.ts (new, 18 assertions):
  - resolveNodeId ×2; fetchVitals soft-fail ×1
  - presence.now: soft-fail, field projection, schema accept/reject ×4
  - vitals.get_breathing: soft-fail, bpm projection, null bpm, window_s ×4
  - vitals.get_heart_rate: soft-fail, bpm projection, schema ×3
  - vitals.get_all: soft-fail, full spread + no raw field, schema ×3

Test results: 93/93 PASS (+18). Build: tsc clean.

ACs touched: ADR-124 §4.1 ruview.presence.now, ruview.vitals.get_breathing,
  ruview.vitals.get_heart_rate, ruview.vitals.get_all. Phase 4 gate: all
  acceptance criteria have passing tests; coverage expanding toward threshold.

Next iter target: Phase 5 (Completion) — CHANGELOG entry, package metadata
  review, witness-bundle extension for npm tarball sha256, then open the PR.
  (Remaining §4.1 tools — pose, primitives, node, vector — can land as post-
  merge follow-up iters given Phase 5 gate criteria are otherwise met.)

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-124/phase5): SENSE-BRIDGE docs batch — README, CHANGELOG, workspace docs

Advances SPARC Phase 5 (Completion) docs gate: landing page, changelog entry,
workspace documentation table row, and user-guide subsection.

tools/ruview-mcp/README.md (NEW, 60 lines):
  - npm-rendered landing page for @ruvnet/rvagent
  - Quickstart: claude mcp add / npx stdio / HTTP with RVAGENT_HTTP_TOKEN
  - Feature matrix: 6 wired tools + next-iter placeholders, transport security
    summary (Origin validation → 403, bearer token → 401, 127.0.0.1 bind)
  - Schema validation gate + RUVIEW-POLICY default-deny description
  - ADR cross-reference table: ADR-124/118/122/115/055

CHANGELOG.md (Unreleased Added bullet):
  - SENSE-BRIDGE entry after BFLD bullet; names all 6 wired tools by MCP
    tool name, stdio + Streamable HTTP transports, security model, Zod schema
    barrel (20 tools + 5 policy), EdgeVitalsMessage Python parity,
    93 tests / 7 suites, try-it quickstart command

README.md (Documentation table):
  - New row after BFLD row: SENSE-BRIDGE summary with 6 tool names, transport
    security summary, ADR-124 link, npx quickstart

docs/user-guide.md (subsection after BFLD):
  - ### SENSE-BRIDGE — rvagent MCP server for AI agents (ADR-124)
  - Claude Code install command + remote sensing-server variant
  - 6-tool markdown table with return shapes
  - Streamable HTTP usage block (RVAGENT_HTTP_TOKEN, 403/401 behavior)
  - Links to tools/ruview-mcp/README.md, ADR-124, issue #787

Test count: 93/93 PASS (unchanged — docs-only iter). Build: tsc clean.

ACs touched: Phase 5 gate — documentation complete; every wired tool
  documented in README, CHANGELOG, workspace docs, and user-guide.

Next iter target: iter 7 — extend scripts/generate-witness-bundle.sh for
  npm tarball sha256, run a full witness, then open PR → main.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-124/phase5): witness bundle — npm tarball sha256 for @ruvnet/rvagent

Extends scripts/generate-witness-bundle.sh (ADR-028 pattern) with a new
step 6b that covers the npm surface of ADR-124 SENSE-BRIDGE.

Changes to generate-witness-bundle.sh:
  - Step [6b]: cd tools/ruview-mcp; npm run build; npm pack; sha256sum tarball
    Writes to bundle: npm-manifest/<tarball>.sha256, tarball-name.txt,
    tarball-sha256.txt. Removes local tarball after hashing (recorded not shipped).
  - VERIFY.sh heredoc: new Check 6 asserts npm-manifest/tarball-sha256.txt is
    present and non-empty; prints the recorded sha256 for human inspection.
    Old Check 6 (proof log) renumbered to Check 7, Check 7→8.
  - Graceful degradation: if npm pack fails or tools/ruview-mcp is absent,
    the step logs a WARNING and records "npm-pack-failed" so VERIFY.sh
    marks it FAIL without aborting the rest of the bundle.

Recorded sha256 for ruvnet-rvagent-0.1.0.tgz (built from commit 0752bbf9d):
  968ff5e2635e0dbe8cda38c6c549a9fb4f30cb9dedc572bf3c1eeadc0ae604e8

Test count: 93/93 PASS (unchanged). Build: tsc clean.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 22:55:47 -04:00

89 KiB
Raw Permalink Blame History

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

Security

  • ESP32 OTA upload now fails closed when no PSK is provisioned (#596 audit finding — critical, breaking change for unprovisioned nodes). ota_check_auth() previously returned true when s_ota_psk[0] == '\0', so a freshly-flashed node would accept attacker-controlled firmware over plain HTTP on port 8032 from any host on the WiFi. No Secure Boot V2, no signed-image verification — a single LAN call could brick or backdoor a node. The fix rejects every OTA upload until a PSK is written to NVS (the OTA HTTP server still starts so operators can run provision.py --ota-psk <hex> over USB-CDC without reflashing). Operators affected: any deployment that relied on the unauthenticated OTA endpoint working out of the box now needs to provision a PSK before subsequent OTA pushes will succeed. Boot-time ESP_LOGW makes the new posture visible.

  • Path-traversal vulnerabilities patched in five sensing-server endpoints (closes #615 — critical). New wifi_densepose_sensing_server::path_safety::safe_id() enforces [A-Za-z0-9._-] only (no leading ., max 64 chars) before any user-controlled identifier reaches a format!() building a filesystem path. Applied at:

    • POST /api/v1/recording/start (recording.rssession_name)
    • GET /api/v1/recording/download/:id (recording.rsid)
    • DELETE /api/v1/recording/delete/:id (recording.rsid)
    • POST /api/v1/models/load (model_manager.rsmodel_id)
    • training_api.rs load_recording_frames (dataset_ids)

    Pre-fix, unauthenticated callers could read ../../etc/passwd-style paths, write arbitrary JSONL files, load attacker-controlled .rvf model files, or delete arbitrary files the server process could touch. 9 unit tests in path_safety::tests exercise the rejection envelope (empty, too-long, path separators, parent-dir traversal, null byte, whitespace/specials, non-ASCII).

Fixed

  • WebSocket /ws/sensing now reports esp32:offline when ESP32 hardware goes stale (closes #618). broadcast_tick_task was re-emitting the cached latest_update with a frozen source: "esp32" field forever after the hardware lost power or network. The REST /health endpoint already called effective_source() (which returns "esp32:offline" after ESP32_OFFLINE_TIMEOUT = 5 s with no UDP frames), but the WS broadcast path was the one consumer that didn't. Result: the UI's "LIVE — ESP32 HARDWARE Connected" banner stayed green long after the hardware went away, and vital_signs/features/classification re-broadcasted the last-seen values indefinitely. Fix: clone the cached latest_update per tick, overwrite source with s.effective_source(), then serialize and broadcast. UI can now switch to an offline state on the same 5-second budget the REST surface uses.

  • Proof replay (archive/v1/data/proof/verify.py) is now cross-platform deterministic (closes #560). Three changes together: (1) features_to_bytes() now np.round(.., HASH_QUANTIZATION_DECIMALS=6)s each feature array before packing as little-endian f64, collapsing ULP-level drift from scipy.fft pocketfft SIMD reordering; (2) the Verify Pipeline Determinism workflow pins OMP_NUM_THREADS=1, OPENBLAS_NUM_THREADS=1, MKL_NUM_THREADS=1, VECLIB_MAXIMUM_THREADS=1, NUMEXPR_NUM_THREADS=1 — multi-threaded BLAS reductions were a deeper source of non-determinism than SIMD reordering, and 6-decimal quantization alone wasn't enough across Azure VM microarchitectures; (3) expected_features.sha256 regenerated under the new conditions. CI now passes the determinism check (same hash across consecutive runs on canonical Linux x86_64 CI runner: 667eb054c44ac510342665bf9c93d608868a8ead948ae8774b2796ebce6f8fe7). scripts/probe-fft-platform.py updated to mirror HASH_QUANTIZATION_DECIMALS=6 for cross-machine spot-checks.

  • archive/v1/src/services/pose_service.py:223 calls the right method on PhaseSanitizer (closes #612). The call was self.phase_sanitizer.sanitize(phase_data), but PhaseSanitizer's full-pipeline entry point is named sanitize_phase() (unwrap_phase + remove_outliers + smooth_phase chained, see archive/v1/src/core/phase_sanitizer.py:266). The shorter sanitize name doesn't exist on the class, so any path that reached this branch raised AttributeError and crashed the pose service mid-frame.

  • adaptive_classifier.rs:94 no longer panics on NaN feature values (closes #611). sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()) returned None and panicked whenever a single NaN reached the classifier from real ESP32 hardware (silent DSP div-by-zero, empty buffer). One bad frame killed the entire sensing-server process. Swapped for unwrap_or(Ordering::Equal), matching the pattern the same file already used at lines 149-150 and 155. Per-frame hot path; this was a real production crash vector.

  • Completed the #611 NaN-panic audit across the sensing-server crate (follow-up to #613). The original audit grepped for the literal partial_cmp(b).unwrap() and missed seven additional production sites that use comparator variants (partial_cmp(b.1).unwrap(), partial_cmp(&variances[b]).unwrap()). All share the same crash class — a single NaN in CSI-derived state panics the whole sensing-server. Fixed:

    • adaptive_classifier.rs:205AdaptiveModel::classify() argmax over softmax probs. Same per-frame hot path as #611; NaN flows through normalise → logits → softmax and still reaches this site even after the #613 IQR fix.
    • adaptive_classifier.rs:480, 500 — training-loop argmax in train() (training/per-class accuracy reporting).
    • main.rs:2446, 2449 and csi.rs:602, 605 — variance-based source/sink selection in count_persons_mincut. The outer unwrap_or((0, &0)) only catches an empty iterator; it cannot rescue a comparator panic.

    Remaining partial_cmp(...).unwrap() sites in the workspace are all inside #[cfg(test)] / #[test] blocks (spectrogram.rs:269, depth.rs:234, connectivity.rs:477, vital_signs.rs:737) where inputs are controlled.

  • ui/utils/pose-renderer.js no longer divides by zero when two render frames land in the same performance.now() tick (issue #519 Bug 2). deltaTime is now Math.max(currentTime - lastFrameTime, 1) before the 1000 / deltaTime division, capping displayed FPS at 1000 — far above any real render rate, but finite so the EMA averageFps = averageFps * 0.9 + fps * 0.1 no longer poisons itself to Infinity on a single zero-dt tick.

Removed

  • Stub crates wifi-densepose-api, wifi-densepose-db, wifi-densepose-config (closes #578). Each was a single-line doc-comment placeholder with an empty [dependencies] section and zero references from any source file or Cargo.toml. The names were reserved early for an envisioned REST/database/config split that never materialised; the functionality they would provide is covered today by wifi-densepose-sensing-server (Axum REST/WS), per-crate config + CLI args, and the project's real-time-only (no-persistent-state) posture. Removing them from the workspace prevents cargo from listing dead crates and shipping empty published artifacts. If any of these names is needed in the future, they can be reintroduced with a real implementation.

Added

  • BFLD — Beamforming Feedback Layer for Detection (ADR-118 umbrella + ADR-119 frame format + ADR-120 privacy class + ADR-121 identity risk scoring + ADR-122 RuView HA/Matter exposure + ADR-123 capture path, #787). New crate wifi-densepose-bfld (v2/crates/wifi-densepose-bfld/) — the privacy-gated WiFi sensing layer that detects when RF data crosses from "ambient sensing" into "identity record" and structurally prevents identity-correlated data from leaving the node. Three invariants enforced by the type system (not policy): I1 raw BFI never exits the node (Sink marker-trait hierarchy + PrivacyClass::Raw.allows_network() == false), I2 identity embedding is in-RAM-only (IdentityEmbedding has no Serialize/Clone/Copy + Drop zeroizes), I3 cross-site identity correlation is cryptographically impossible (per-site BLAKE3-keyed SignatureHasher with daily epoch rotation; mean cross-site Hamming distance ≥120 bits across 100 trials). Ships the complete operator surface: BfldPipeline + BfldPipelineHandle (worker-thread variant + spawn_with_oracle for Soul Signature deployments), BfldEvent with JSON publishing ("blake3:<hex>" rf_signature_hash format per spec), 4 privacy_class levels (Raw/Derived/Anonymous/Restricted) with PrivacyGate::demote monotonic transformer + irreversible apply_privacy_gating, CoherenceGate with ±0.05 hysteresis + 5-second debounce + clock-skew resilience (saturating_sub), SoulMatchOracle Recalibrate-exemption trait for enrolled-person deployments. MQTT/HA surface: mqtt_topics::render_events + publish_event (class-gated topic routing — Raw/Derived publish 0 topics, Anonymous publishes 6, Restricted publishes 5 with identity_risk stripped), ha_discovery::render_discovery_payloads + publish_discovery (HA-DISCO config payloads with availability_topic integration), availability module (online/offline + LWT-aware with_lwt helper for rumqttc::MqttOptions), RumqttPublisher behind a mqtt feature gate with connect_with_lwt for broker-side auto-offline. 3 operator HA Blueprints under v2/crates/cog-ha-matter/blueprints/bfld/ (presence-driven-lighting, motion-aware-HVAC, identity-risk-anomaly-notification with rolling 7-day z-score). Two runnable examples (bfld_minimal for in-process consumers, bfld_handle for the production worker-thread + bootstrap-then-spawn pattern). GitHub Actions CI workflow (.github/workflows/bfld-mqtt-integration.yml) spins up eclipse-mosquitto:2 as a service container so the env-gated mosquitto_integration and rumqttc_lwt tests run end-to-end in CI. Performance: BfldFrame::to_bytes() measured at 320,255 frames/sec debug (6.4× ADR-119 AC7 release target of 50k), header-only at 1,654,517 frames/sec, presence-detection latency p95 = 0.9µs (~1,000,000× under ADR-119 AC2's 1s target), 9.96 Hz motion-publish rate through BfldPipelineHandle (10× ADR-122 AC3 floor). Coverage: 327 tests at default features, 101 no_std-compatible, 220+ with --features mqtt. CRC-32/ISO-HDLC polynomial pinned against "123456789" → 0xCBF43926, public-API surface snapshot pinned across all pub use re-exports, BfldError Display contract pinned for log-grep monitoring rules, reserved-flag-bits forward-compat round-trip property, apply_privacy_gating irreversibility (5-cycle round-trip stress proves stripped fields never resurrect). Companion research dossier in docs/research/BFLD/ (11 files, 13,544 words). 49-iter implementation chain from scaffold (feat/adr-118/p1, c965e3e6c) through current head with per-iter progress comments on issue #787. Try it: cargo run -p wifi-densepose-bfld --example bfld_handle.
  • SENSE-BRIDGE — rvagent MCP server + ruvector npm + ruflo integration (ADR-124, #787). New npm package @ruvnet/rvagent (tools/ruview-mcp/) — a dual-transport Model Context Protocol server that bridges the RuView WiFi-DensePose sensing stack to AI agents (Claude Code, Cursor, ruflo swarms). 6 of 20 ADR-124 §4.1 tools wired in this initial release: ruview.presence.now (occupancy), ruview.vitals.get_breathing / get_heart_rate / get_all (biometric vitals via EdgeVitalsMessage surface, ADR-124 §6 Python ws.py:74-88 parity), ruview.bfld.last_scan (latest BFLD event — identity_risk_score, privacy_class, n_frames, timestamp_ms), ruview.bfld.subscribe (MQTT wildcard subscription with synthetic UUID envelope fallback). Dual-transport architecture (ADR-124 §3): stdio (npx @ruvnet/rvagent stdio — recommended for Claude Code / Cursor local flow) + Streamable HTTP (POST /mcp bound to 127.0.0.1:3001 by default — for remote ruflo swarms across the Tailscale fleet). Security model (ADR-124 §6): Origin header validation (cross-origin POST → 403), bearer-token auth slot (RVAGENT_HTTP_TOKEN → 401), bind default 127.0.0.1 per MCP spec requirement. Uniform schema validation gate (ADR-124 §3): every CallTool request runs zod.safeParse via TOOL_INPUT_SCHEMAS before dispatch; failures throw McpError(InvalidParams). Full Zod schema barrel (ADR-124 §4.1 + §4.1a): src/schemas/tools.ts defines all 20 tool input schemas including the 5 RUVIEW-POLICY governance tools (can_access_vitals, can_query_presence, can_subscribe, redact_identity_fields, audit_log). Python surface parity: EdgeVitalsMessage TypeScript interface mirrors Python ws.py:74-88; ADR-124 §6 parity table drives the field names. 93 tests across 7 suites (manifest, schemas, validate, tools, http-transport, bfld-tools, vitals-tools) — all green. Try it: npx @ruvnet/rvagent stdio (with RUVIEW_SENSING_SERVER_URL=http://localhost:3000).
  • Home Assistant + Matter integration (ADR-115). New --mqtt and --matter flags on wifi-densepose-sensing-server expose the full sensing capability set to any Home Assistant install via MQTT auto-discovery (HA-DISCO) and to any Matter controller (Apple Home / Google Home / Alexa / SmartThings) via a built-in Matter Bridge scaffolding (HA-FABRIC, SDK wiring v0.7.1). Includes 21 entity kinds per node — 11 raw signals + 10 inferred semantic primitives (HA-MIND: someone-sleeping, possible-distress, room-active, elderly-inactivity-anomaly, meeting, bathroom, fall-risk, bed-exit, no-movement, multi-room-transition). The semantic primitives run server-side so --privacy-mode strips HR/BR/pose values from the wire while still publishing the inferred states — the architectural win for healthcare and AAL deployments. Ships 8 starter HA Blueprints under examples/ha-blueprints/, 3 drop-in Lovelace dashboards under examples/lovelace/ (including a privacy-mode-compatible healthcare care view), mTLS support, 32 KB payload-size cap, MQTT-wildcard topic-injection rejection, RUVIEW_MQTT_STRICT_TLS=1 v0.8.0 upgrade path. 420 lib tests cover the implementation including ~2,560 fuzzed assertions per CI run (10 proptest cases across wire-boundary security + semantic-bus invariants). Plus mosquitto-backed integration tests in .github/workflows/mqtt-integration.yml, criterion benchmarks beating every ADR target by 1.6×–208×, and an ESP32-S3 hardware validation harness (scripts/validate-esp32-mqtt.sh) that asserts the full pipeline end-to-end with a witness bundle generator (scripts/witness-adr-115.sh) that self-verifies. See docs/releases/v0.7.0-mqtt-matter.md, docs/integrations/home-assistant.md, docs/integrations/semantic-primitives-metrics.md, docs/integrations/benchmarks.md, docs/adr/ADR-115-home-assistant-integration.md, tracking issue #776, PR #778. Matter SDK wiring (P8b) and CSA-certification path (P10) deferred to v0.7.1+ per ADR §9.10. Try it: cargo run -p wifi-densepose-sensing-server --features mqtt --example mqtt_publisher -- --mqtt --mqtt-host 127.0.0.1.
  • ESP32-C6 firmware target with Wi-Fi 6 / 802.15.4 / TWT / LP-core support (ADR-110, #762). firmware/esp32-csi-node now builds for both esp32s3 (existing production node) and esp32c6 (new research/seed-node target) from the same source tree — pick via idf.py set-target esp32c6 and ESP-IDF auto-applies the new sdkconfig.defaults.esp32c6 overlay. Every C6 module is #ifdef CONFIG_IDF_TARGET_ESP32C6 gated, so the S3 build is byte-identical to today (no regression).
    • Wi-Fi 6 HE-LTF subcarrier taggingcsi_collector.c now reads rx_ctrl.cur_bb_format and writes the PPDU type (0=HT/legacy, 1=HE-SU, 2=HE-MU, 3=HE-TB) into ADR-018 frame byte 18, plus bandwidth flags (20/40 MHz, STBC, 802.15.4-sync-valid) into byte 19. Bytes 18-19 were previously reserved-zero, so old aggregators read them as before — fully backwards compatible. Magic stays 0xC5110001. Default on via CONFIG_CSI_FRAME_HE_TAGGING. First firmware in the open ESP32 ecosystem to tag CSI frames with 11ax PPDU metadata.
    • 802.15.4 mesh time-sync — new c6_timesync.{h,c} (262 lines) provides cross-node clock alignment over the C6's separate 802.15.4 radio, freeing WiFi airtime from coordination traffic (directly addresses the ADR-029/030 multistatic synchronization gap). Protocol: lowest EUI-64 wins election, leader broadcasts TS_BEACON (magic=0x54534D45, leader epoch µs) every 100 ms on channel 15, followers compute offset = leader_us - local_us and apply lazily — every CSI frame is stamped with c6_timesync_get_epoch_us(). Target alignment ±100 µs. Default on via CONFIG_C6_TIMESYNC_ENABLE. Verified initializing at boot on COM6 (c6_ts: init done: channel=15 EUI=206ef1fffefffe17 leader=yes(candidate) at +413 ms).
    • TWT (Target Wake Time) — new c6_twt.{h,c} (223 lines) wraps esp_wifi_sta_itwt_setup from esp_wifi_he.h to negotiate an individual TWT agreement with the AP after STA connect. Replaces today's opportunistic CSI capture with a scheduler-bounded one (default wake interval 10 ms = 100 fps cadence). Graceful NACK fallback: when the AP doesn't support 11ax iTWT, the helper logs and returns OK so the device keeps doing opportunistic CSI just like the S3. Teardown on WIFI_EVENT_STA_DISCONNECTED keeps the AP's TWT scheduler clean. Gated on SOC_WIFI_HE_SUPPORT (auto-set on C6/C5 chips).
    • LP-core wake-on-motion hibernation — new c6_lp_core.{h,c} (134 lines) arms the C6 LP RISC-V coprocessor as an always-on motion gate; HP core stays in deep sleep until a configurable GPIO wakes it (ext1 deep-sleep wake source in this initial cut, real LP-core program in follow-up). Targets ≤5 µA hibernation current for battery-powered Cognitum Seed nodes (vs the S3's ~10 µA ULP-FSM floor). Opt-in via CONFIG_C6_LP_CORE_ENABLE (default off — only enabled on nodes flashed for battery-powered seed duty).
    • Build matrix: S3 stays partitions_display.csv (8 MB + display + WASM), C6 uses partitions_4mb.csv (4 MB single OTA, no display, no WASM3, no LCD). C6 final binary 1003 KB (46% partition slack), 9 % smaller than S3 production. Free heap 310 KiB at boot, app_main reached in 343 ms, 802.15.4 stack up in another 70 ms.
    • Why this matters: opens three research surfaces nobody has published yet — Wi-Fi-6 CSI human pose, multistatic CSI clock alignment over a side-channel radio, and TWT-bounded deterministic CSI cadence. The S3 production fleet keeps shipping the existing capabilities; the C6 is the research / battery-seed expansion target.
    • Docs: ADR-110 (186 lines, Status=Accepted), tracking issue ruvnet/RuView#762 with per-phase progress comments, README hardware table + Quick-Start Option 2b, docs/user-guide.md full ESP32-C6 section (build, flash, provision, multi-room time-sync, battery seed mode), full empirical record in docs/WITNESS-LOG-110.md with verified / claimed / bugs-fixed / bugs-found sections.
    • Wave 2 follow-up (D1 workaround): 5 systematic experiments on 3 live C6 boards confirmed the IDF v5.4 802.15.4 RX path is unfixable from user code (TX works 100 %, RX delivers 0 frames; coex/channel/OpenThread/manual-rearm all ruled out). Pivoted to ESP-NOW for the cross-node sync transport — main/c6_sync_espnow.{h,c} is the same TS_BEACON protocol over WiFi peer-to-peer, same get_epoch_us / is_valid / is_leader API surface. 120 s single-board soak: 1151 transmits, 0 failures (0.00 %), 9.6 tx/s sustained, no crash or reset. The 802.15.4 path stays in source as documented-broken (D1) for when the IDF driver gets fixed.
    • Host-side dual-pipeline decoder for ADR-018 byte 18-19 (ADR-110 protocol closure):
      • Rust (v2/crates/wifi-densepose-hardware): new PpduType enum (HtLegacy/HeSu/HeMu/HeTb/Unknown) and Adr018Flags struct (bw40/stbc/ldpc/ieee802154_sync_valid) on CsiMetadata. 6 new deterministic unit tests; 122/122 hardware-crate tests pass.
      • Python (archive/v1/src/hardware/csi_extractor.py): HEADER_FMT extended from <IBBHIIBB2x to <IBBHIIBBBB; new metadata fields (ppdu_type, he_capable, bw40, stbc, ldpc, ieee802154_sync_valid). 5 new TestAdr110ByteEncoding cases; 11/11 parser tests pass.
      • Both decoders match the firmware encoder bit-for-bit. Pre-ADR-110 firmware sends zeros that round-trip as HtLegacy + default flags — fully backwards compatible.
    • Security fix (scripts/redact-secrets.py + generate-witness-bundle.sh): the Python proof step was echoing .env contents into the bundled verification-output.log via Pydantic validation errors. Bundle nuked before push; added a stdin -> stdout redaction filter covering common token prefixes, long opaque strings, and long hex runs. Verified zero leaks on rebuild.
    • Wave 3 — firmware v0.6.7 (LP-core full + soft-AP HE): two software-only unblocks for the hardware-blocked items in WITNESS-LOG-110 §B. (1) Real LP-core motion-gate program (firmware/esp32-csi-node/main/lp_core/main.c + integration in c6_lp_core.c). When CONFIG_C6_LP_CORE_ENABLE=y, the LP RISC-V coprocessor now runs a real polling program (configurable cadence via CONFIG_C6_LP_POLL_PERIOD_US, default 10 ms) that debounces N consecutive GPIO samples (CONFIG_C6_LP_DEBOUNCE_SAMPLES, default 3) and wakes the HP core via ulp_lp_core_wakeup_main_processor(). HP entry uses esp_sleep_enable_ulp_wakeup + ESP_SLEEP_WAKEUP_ULP. Exposes c6_lp_core_motion_count() and c6_lp_core_poll_count() getters for the witness harness. Replaces the v0.6.6 esp_deep_sleep_enable_gpio_wakeup ext1 fallback (which floored at ~10 µA, the same as the S3 ULP-FSM). The fallback path stays as the else branch so builds without CONFIG_C6_LP_CORE_ENABLE keep working unchanged — zero regression for v0.6.6-era fleets. Targets the C6 datasheet ≤5 µA average for battery seed nodes; pending INA/Joulescope measurement to confirm (WITNESS-LOG-110 §B4). (2) Wi-Fi 6 soft-AP with TWT Responder=1 (c6_softap_he.{h,c} + main.c AP+STA mode switch). When CONFIG_C6_SOFTAP_HE_ENABLE=y, one C6 board can act as the iTWT-capable AP the bench is otherwise missing — pair with a second C6-STA board to negotiate real iTWT against a known-cooperative AP and measure deterministic CSI cadence (WITNESS-LOG-110 §B1/B2). SSID/PSK/channel configurable via Kconfig defaults or NVS (softap_ssid/softap_psk/softap_chan keys in the ruview namespace). Default off so existing nodes are unaffected. Build artifacts: S3 8 MB binary 1093 KB (47 % slack), C6 4 MB binary 1019 KB (45 % slack). Tag: v0.6.7-esp32.
    • Wave 4 — firmware v0.6.8 (ESP-NOW mesh offset smoother): c6_sync_espnow.c now maintains an in-firmware exponential-moving-average of the cross-board sync offset (α = 1/8, fixed-point shift, ≈ 8-sample window at the 10 Hz beacon rate). New getter c6_sync_espnow_get_offset_us_smoothed(). c6_sync_espnow_get_epoch_us() now returns timestamps stamped from the smoothed offset once seeded — every downstream CSI-frame consumer gets bounded-jitter alignment for free, no host-side filter required. Measured on the bench: 5-min two-board soak (WITNESS-LOG-110 §A0.10) drops raw offset stdev 411.5 µs → smoothed 104.1 µs (3.95× suppression on stdev, 4.70× on peak-to-peak range) while preserving the +30 µs/min crystal-drift trajectory within 2 µs/min. The ADR-110 §2.4 ≤100 µs multistatic alignment target that v0.6.6 designed is now empirically measured, not just stated. Cross-board beacon match rate 99.56% over 5 min, 0 TX failures. Binary cost: +32 bytes (one int64, one bool, one getter). Diag log adds smoothed=… field. Tag: v0.6.8-esp32. Known wiring gap (deferred): csi_serialize_frame does not yet stamp frames with c6_sync_espnow_get_epoch_us() — the ADR-018 frame format has no timestamp field, and adding one is a breaking change that needs an ADR update. Multistatic CSI fusion will require either an ADR-018 v2 with timestamp, or a separate UDP sync packet keyed off the existing flag bit. Tracked in WITNESS-LOG-110 §A0.11.
    • Wave 5 — firmware v0.6.9 + v0.7.0 + host wiring (loop iter 8 → iter 26): closes the §A0.11 gap and lights up the substrate end-to-end across firmware → host → JSON broadcast. Firmware: (a) v0.6.9-esp32csi_collector.c emits a 32-byte UDP sync packet (magic 0xC511A110, distinct from CSI frame magic 0xC5110001) every CONFIG_C6_SYNC_EVERY_N_FRAMES (default 20) CSI frames, carrying node_id, local_us, mesh-aligned epoch_us (from the Wave 4 smoothed offset), and the CSI sequence high-water for host-side pairing. Same UDP socket as CSI; host dispatches by leading magic. Operator-tunable cadence via the new Kconfig knob — N=1 (10 Hz) for tight multistatic, N=200 (~20 s) for low-power seeds. Live-verified on COM9+COM12 (§A0.12): follower reports local epoch = 1 163 565 µs, matches the §A0.10 boot-delta measurement within 285 µs of WiFi MAC TX jitter. (b) v0.7.0-esp32csi_collector.c:221 ADR-018 byte 19 bit 4 ("cross-node sync valid") now ORs in c6_sync_espnow_is_valid() so frames from sync'd ESP-NOW nodes correctly advertise sync (previously only sourced from the broken 802.15.4 path — false-negative bug, §A0.13). Side effect: S3 boards now also set the bit since c6_sync_espnow is cross-target. Host decoders + 25 unit tests: Python SyncPacketParser + SyncPacket dataclass with apply_to_local / mesh_aligned_us_for_sequence / local_minus_epoch_us (10 tests in TestSyncPacketParser); Rust wifi_densepose_hardware::SyncPacket + SyncPacketFlags + SYNC_PACKET_MAGIC re-exported from the crate root with identical API surface (15 tests in sync_packet::tests). Cross-language conformance gate (loop iter 21): the same 32-byte canonical hex 10a111c509010600f26db70100000000c5aca501000000001400000000000000 is pinned in both test suites; if either decoder drifts from the wire, exactly one named test fires and points at the moved side. Sensing-server wiring: udp_receiver_task magic-dispatches 0xC511A110 and stores per-node latest_sync: Option<SyncPacket> + latest_sync_at: Option<Instant> on NodeState. New helpers: NodeState::mesh_aligned_us(local_us), NodeState::mesh_aligned_us_for_csi_frame(sequence) (uses the per-node measured fps EMA with 5-sample warmup + 9 s staleness gate), NodeState::observe_csi_frame_arrival(now) (feeds update_csi_fps_ema α=1/8, called once per accepted CSI frame). 4 fps-EMA tests + 3 NodeSyncSnapshot serialization tests on the binary target. Public JSON API: sensing_update broadcasts now carry an optional sync object per node — {offset_us, is_leader, is_valid, smoothed, sequence, csi_fps_ema, csi_fps_samples}#[serde(skip_serializing_if = "Option::is_none")] so non-mesh paths (multi-BSSID scan / synthetic-RSSI fallback / simulation) omit the key entirely. Existing pre-v0.7.0 UI clients ignore it cleanly. Documented in docs/user-guide.md "Per-node mesh sync (ADR-110)" section with field table, UI rendering rules, and the timestamp-recovery recipe. Branch-coordination: docs/ADR-110-BRANCH-STATE.md maps which files each of adr-110-esp32c6 vs feat/adr-115-ha-mqtt-matter touches (regions are disjoint, merges should be clean line-merges). Verification baselines: full v2 cargo workspace at 1437 tests passing (no regression across 17 crate batches), full wifi-densepose-hardware crate at 137 tests. ADR-110 §B substrate is now end-to-end visible to UI clients and ready for ADR-029/030 multistatic CSI fusion consumption.
  • Real-time CSI introspection / low-latency tap on wifi-densepose-sensing-server (ADR-099). New wifi_densepose_sensing_server::introspection module wires midstream's temporal-attractor (Lyapunov + regime classification) and temporal-compare (DTW pattern matching) as a parallel tap alongside RuView's existing event pipeline — no replacement, no behaviour change to the existing /ws/sensing fan-out or wifi-densepose-signal DSP. Two new endpoints (off by default, enabled via --introspection):
    • GET /ws/introspection — newline-delimited JSON snapshots streamed at the CSI frame rate. Each snapshot carries frame_count, regime (Idle / Periodic / Transient / Chaotic / Unknown), lyapunov_exponent, attractor_dim, attractor_confidence, regime_changed (boolean — flips on the first frame after a regime transition), and top_k_similarity[] (highest-scoring signature matches against a per-deployment library).
    • GET /api/v1/introspection/snapshot — single-shot JSON snapshot, auth-gated when RUVIEW_API_TOKEN is set. Per-frame update() budget measured at 0.041 ms p99 on the I5 bench (~24× under ADR-099 D4's 1 ms target). Shape-match latency on a 1-D mean-amplitude L1 stand-in: 5 frames (3.20× ratio vs the 16-frame event-path floor). ADR-099 D8 honestly amended — the aspirational 10× bar is contingent on ADR-208 Phase 2 multi-dim NPU embeddings; this release ships the tap off-by-default while the foundation lands. 8 lib tests + 5 latency/regression tests (tests/introspection_latency.rs, including a 200-frame noise warm-up → 10-frame motion-ramp signature benchmark).
  • Opt-in bearer-token auth on wifi-densepose-sensing-server's /api/v1/* HTTP surface (closes #443). New wifi_densepose_sensing_server::bearer_auth module: when the RUVIEW_API_TOKEN env var is set, every request whose path begins with /api/v1/ must carry an Authorization: Bearer <token> header (constant-time compared) or the server responds 401 Unauthorized. When the variable is unset or empty the middleware is a no-op — the long-standing LAN-only deployment posture is preserved, so this is a binary deployment-time switch with no default behaviour change. /health*, /ws/sensing, and the /ui/* static mount are intentionally never gated (orchestrator probes + local browsers). Startup logs which mode is active and warns when auth is on with a 0.0.0.0 bind. 8 unit tests on the middleware (lib test count 191 → 199). Resolves the security audit raised in #443.

Changed

  • Docker image: build-time guard for the UI assets, plus a CI workflow that rebuilds and pushes on every change (closes #520, #514). docker/Dockerfile.rust now RUNs a guard after COPY ui/ that fails the build if any of index.html / observatory.html / pose-fusion.html / viz.html / the observatory/ / pose-fusion/ / components/ / services/ directories are missing, so a stale image can never be silently produced again. New .github/workflows/sensing-server-docker.yml builds the image on push to main (paths-filtered) and on v* tags and pushes to both docker.io/ruvnet/wifi-densepose and ghcr.io/ruvnet/wifi-densepose with latest + vX.Y.Z + sha-<short> tags, then smoke-tests the published artifact: /health, /api/v1/info, the observatory + pose-fusion UI assets, and the RUVIEW_API_TOKEN auth path (no token → 401, wrong → 401, correct → 200). Uses DOCKERHUB_USERNAME / DOCKERHUB_TOKEN repo secrets for the Docker Hub push; ghcr.io uses the workflow's GITHUB_TOKEN.
  • rvCSI moved to its own repo and is now vendored as a submodule. The 9 rvcsi-* crates (rvcsi-core/-dsp/-events/-adapter-file/-adapter-nexmon/-ruvector/ -runtime/-node/-cli — added inline in #542) now live in github.com/ruvnet/rvcsi: published to crates.io as rvcsi-* 0.3.x, to npm as @ruv/rvcsi, with a Claude Code plugin marketplace and a RuView-style README. RuView vendors it under vendor/rvcsi (alongside vendor/ruvector / vendor/midstream / vendor/sublinear-time-solver) and no longer carries inline copies in v2/crates/; consumers depend on the published crates (or the submodule's crates/rvcsi-* paths). v2/Cargo.toml, CLAUDE.md, and the README docs table updated accordingly. The ADRs (ADR-095, ADR-096), PRD, and DDD model stay in docs/ here as the design record of the incubation.

Fixed

  • README: corrected the camera-supervised pose-accuracy claim. The README stated "92.9% PCK@20" for camera-supervised training; that figure does not appear in ADR-079 and is ~2.6× the ADR's own success target (>35% PCK@20). ADR-079 phases P7 (data collection), P8 (training + evaluation on real paired data) and P9 (cross-room LoRA) are still Pending, so no measured camera-supervised PCK@20 has been published. README now states the proxy-supervised baseline (≈2.5%) and the ADR-079 target (35%+), and notes the eval phases are pending. Surfaced by the PowerPlatePulse training-pipeline audit (2026-05-11); 6 remaining audit findings tracked in the PR.
  • rvCSI BaselineDriftDetector: drift thresholds are now scale-relative, not absolute. The detector compared mean_amplitude against its EWMA baseline with absolute thresholds (anomaly_threshold = 1.0, drift_threshold = 0.15) — fine for the synthetic unit tests (amplitudes ≈ 1.0), but raw ESP32 CSI is int8 I/Q with amplitudes up to ~128, so the window-to-window RMS distance is routinely 550 ≫ 1.0 and AnomalyDetected fired on ~96 % of windows (319/331 on a real node-1 capture). Drift is now ‖current baseline‖₂ / ‖baseline‖₂ (a fraction, with an eps floor for a degenerate near-zero baseline), so one tuning works across raw-int8 ESP32, int16-scaled Nexmon, and baseline-subtracted streams alike — AnomalyDetected drops to 40/331 on the same data, the existing detector tests still pass, and a baseline_drift_is_scale_invariant_no_anomaly_storm regression test was added. ADR-095 D13 / ADR-096 §2.1, §5 updated. Surfaced by an end-to-end test against real ESP32 CSI (a 7,000-frame node-1 capture; transcoder at scripts/esp32_jsonl_to_rvcsi.py).

Added

  • rvCSI — edge RF sensing runtime (design + first implementation). New subsystem rvCSI: a Rust-first / TypeScript-accessible / hardware-abstracted edge RF sensing runtime that normalizes WiFi CSI from Nexmon, ESP32, Intel, Atheros, file and replay sources into one validated CsiFrame schema, runs reusable DSP, emits typed confidence-scored events, and bridges to RuVector RF memory, an MCP tool server and a TS SDK.
    • Design docs: docs/prd/rvcsi-platform-prd.md (purpose, users, success criteria, FR1FR10, NFRs, system architecture, data model); docs/adr/ADR-095-rvcsi-edge-rf-sensing-platform.md (the 15 architectural decisions: Rust core, C-at-the-boundary, TS SDK via napi-rs, normalized schema, validate-before-FFI, CSI-as-temporal-delta, RuVector as RF memory, replayability, detection≠decision, local-first, read-first/write-gated MCP, mandatory quality scoring, versioned calibration, plugin adapters); docs/adr/ADR-096-rvcsi-ffi-crate-layout.md (crate topology, the napi-c shim record format & contract, the napi-rs Node surface, build/test invariants); docs/ddd/rvcsi-domain-model.md (7 bounded contexts: Capture, Validation, Signal, Calibration, Event, Memory, Agent — with aggregates, invariants, context map and domain services). Indexed in docs/adr/README.md and docs/ddd/README.md.
    • Crates (9 new v2/crates/rvcsi-* workspace members): rvcsi-core (normalized CsiFrame/CsiWindow/CsiEvent schema, AdapterProfile, CsiSource plugin trait, id newtypes + IdGenerator, RvcsiError, the validate_frame pipeline + quality scoring; forbid(unsafe_code)); rvcsi-adapter-nexmon — the napi-c seam: native/rvcsi_nexmon_shim.{c,h} (the only C in the runtime — allocation-free, bounds-checked, ABI 1.1), compiled via build.rs+cc, handling two byte formats — the compact self-describing "rvCSI Nexmon record", and the real nexmon_csi UDP payload (the 18-byte magic 0x1111 · rssi · fctl · src_mac · seq · core/stream · chanspec · chip_ver header + nsub int16 I/Q samples, the modern BCM43455c0/4358/4366c0 export read by CSIKit/csireader.py), with a Broadcom d11ac chanspec decoder (channel/bandwidth/band) — plus a pure-Rust libpcap reader (classic .pcap, all byte-order/timestamp-resolution magics, Ethernet/raw-IPv4/Linux-SLL link types) and a Nexmon-chip / Raspberry-Pi-model registry (NexmonChip / RaspberryPiModel — including the Raspberry Pi 5 (CYW43455/BCM43455c0, same wireless as the Pi 4 — 20/40/80 MHz, 2.4+5 GHz, 64/128/256 subcarriers), the Pi 3B+/4/400, and the Pi Zero 2 W (BCM43436b0); nexmon_adapter_profile / raspberry_pi_profile build the per-chip AdapterProfile; chip_ver words auto-resolve to a chip). Wrapped by a documented ffi module and two CsiSources: NexmonAdapter (record buffers) and NexmonPcapAdapter (real nexmon_csi UDP inside a tcpdump -i wlan0 dst port 5500 -w csi.pcap capture — the pcap timestamp stamps each frame; the chip is auto-detected from chip_ver, overridable via .with_pi_model(Pi5) / .with_chip(...)). rvcsi-dsp (DC removal, phase unwrap, smoothing, Hampel/MAD filter, sliding variance, baseline subtraction, motion-energy/presence/confidence features, heuristic breathing-band estimate, non-destructive SignalPipeline); rvcsi-events (WindowBuffer, the EventDetector trait + presence/motion/quality/baseline-drift state machines, EventPipeline; the baseline-drift detector uses scale-relative thresholds — drift as a fraction of the baseline's RMS magnitude — so one tuning works across raw-int8 ESP32, int16-scaled Nexmon, and baseline-subtracted streams alike); rvcsi-adapter-file (the .rvcsi JSONL capture format, FileRecorder, FileReplayAdapter deterministic replay); rvcsi-ruvector (deterministic window/event embeddings, cosine_similarity, the RfMemoryStore trait, InMemoryRfMemory + JsonlRfMemory — a standin until the production RuVector binding); rvcsi-runtime (the no-FFI composition layer: CaptureRuntime = CsiSource + validate_frame + SignalPipeline + EventPipeline, plus one-shot helpers summarize_capture/decode_nexmon_records/decode_nexmon_pcap/summarize_nexmon_pcap/events_from_capture/export_capture_to_rf_memory); rvcsi-node — the napi-rs seam (a ["cdylib","rlib"] Node addon, build.rs runs napi_build::setup(); thin #[napi] wrappers over rvcsi-runtimenexmonDecodeRecords/nexmonDecodePcap (with optional chip)/inspectNexmonPcap/decodeChanspec/nexmonChipName/nexmonProfile/nexmonChips/inspectCaptureFile/eventsFromCaptureFile/exportCaptureToRfMemory + an RvcsiRuntime streaming class; everything that crosses to JS is a validated/normalized struct serialized to JSON); rvcsi-cli (the rvcsi binary: record (Nexmon-dump or --source nexmon-pcap [--chip pi5].rvcsi), inspect, inspect-nexmon, nexmon-chips, decode-chanspec, replay, stream, events, health, calibrate v0-baseline, export ruvector). Plus the @ruv/rvcsi npm package (package.json/index.js/index.d.ts/README/__test__) alongside rvcsi-node — a curated JS surface that parses the addon's JSON into plain CsiFrame/CsiWindow/CsiEvent/SourceHealth/CaptureSummary/NexmonPcapSummary/DecodedChanspec objects, with a lazy native-addon load.
    • Tests: 169 across the rvcsi crates (core 29, dsp 28, events 19 — incl. a baseline-drift scale-invariance regression, adapter-file 20 + 1 doctest, adapter-nexmon 28 — round-tripping through the C shim and synthetic libpcap files, incl. Pi 5 / chip-detection, ruvector 20 + 1 doctest, runtime 13, cli 10), 0 failures; all rvcsi crates build together and are clippy-clean (rvcsi-node under deny(clippy::all)); forbid(unsafe_code) everywhere except rvcsi-adapter-nexmon (FFI, every unsafe block documented). Also exercised end-to-end against a real 7,000-frame ESP32 node-1 capture (transcoded with scripts/esp32_jsonl_to_rvcsi.py — the stand-in for the not-yet-shipped record --source esp32-jsonl): rvcsi inspect/replay/calibrate/events all run on real hardware data. Not yet wired in: live radio capture, rvcsi-adapter-esp32 (live serial/UDP ESP32 source), the WebSocket daemon (rvcsi-daemon), the MCP tool server (rvcsi-mcp), and the legacy nexmon packed-float CSI export — follow-ups on top of these crates.
  • wifi-densepose-train: signal_features module — wires wifi-densepose-signal into the training pipeline. wifi-densepose-signal was previously a phantom dependency of wifi-densepose-train (listed in Cargo.toml, never imported). New wifi_densepose_train::signal_features::extract_signal_features (and CsiSample::signal_features()) run a windowed CSI observation's centre frame through wifi_densepose_signal::features::FeatureExtractor, producing a fixed-length (FEATURE_LEN = 12) amplitude/phase/PSD feature vector — the hook for a future vitals / multi-task supervision head (breathing- and heart-rate-band power are read off the PSD summary). The vector is produced on demand and not yet fed back into the loss. Surfaced by the 2026-05-11 training-pipeline audit (findings #1 "vitals features absent from training" and #2 "wifi-densepose-signal ghost dep").
  • wifi-densepose-train: TrainingConfig subcarrier-layout presets + a real-loader integration test. New TrainingConfig::for_subcarriers(native, target) plus named presets ht40_192() (≈192-sc ESP32 HT40 → 56) and multiband_168() (168-sc ADR-078 multi-band mesh → 56), so non-MM-Fi CSI shapes are first-class instead of requiring manual native_subcarriers/num_subcarriers overrides; field docs now list the supported source counts and the multi-NIC mapping. New tests/test_real_loader.rs round-trips synthetic CSI through .npy files → MmFiDataset::discover/get (including the subcarrier-interpolation branch and the empty-root case) — exercising the on-disk loader path the deterministic verify-training proof intentionally bypasses. Addresses training-pipeline audit findings #6 (56-sc/1-NIC config default) and #7 (multi-band mesh not in config); the #4 concern ("proof uses synthetic data") is reframed — the proof should use a reproducible source, and this test covers the real loader it skips.

Fixed

  • HuggingFace MODEL_CARD.md: marked the PIR/BME280 environmental-sensor ground-truth path as planned, not implemented (training-pipeline audit finding #3) — the card presented PIR/BME280 weak-label fine-tuning as a current capability; there is no env-sensor ingestion in the training pipeline today.
  • README: corrected the camera-supervised pose-accuracy claim (audit finding #5; see PR #535) — "92.9% PCK@20" → the ADR-079 target (35%+; proxy baseline 35.3%), noting P7/P8/P9 are pending.

Added

  • RollingP95 adaptive feature normalizer (v2/crates/wifi-densepose-sensing-server) — Streaming P95 estimator (600-sample / ~30 s sliding window) that self-calibrates feature normalization to whatever distribution the deployment produces. Replaces fixed-scale denominators (variance/300, motion/250, spectral/500) which saturated when live ESP32 values exceeded those limits, collapsing dynamic range to zero. Cold-start (<60 samples) falls back to the legacy denominators so day-0 behaviour is preserved. Deployment-neutral: no hardcoded values. (ADR-044 §5.2)

  • dedup_factor runtime configuration API (v2/crates/wifi-densepose-sensing-server) — Exposes the multi-node person-count deduplication divisor at runtime via REST:

    • GET /api/v1/config/dedup-factor — read current value.
    • POST /api/v1/config/dedup-factor — set value (clamped 1.010.0, persisted).
    • POST /api/v1/config/ground-truth — auto-tunes dedup_factor from a known person count ({"count": N}); derives optimal divisor from current node-sum. Config is persisted to data/config.json and reloaded on restart. (ADR-044 §5.3)
  • nvsim crate — deterministic NV-diamond magnetometer pipeline simulator (ADR-089) — New standalone leaf crate at v2/crates/nvsim modeling a forward-only magnetic sensing path: scene → source synthesis (BiotSavart, dipole, current loop, ferrous induced moment) → material attenuation (Air/Drywall/Brick/Concrete/Reinforced/SteelSheet) → NV ensemble (4 〈111〉 axes, ODMR linear-readout proxy, shot-noise floor per Wolf 2015 / Barry 2020) → 16-bit ADC + lock-in demodulation → fixed-layout MagFrame records → SHA-256 witness. Six-pass build per docs/research/quantum-sensing/15-nvsim-implementation-plan.md. 50 tests, ~4.5 M samples/s on x86_64 (4500× the Cortex-A53 1 kHz acceptance gate), pinned reference witness cc8de9b01b0ff5bd97a6c17848a3f156c174ea7589d0888164a441584ec593b4 for byte-equivalence regression. WASM-ready by construction (zero std::time/fs/env/process/thread); builds cleanly for wasm32-unknown-unknown. ADR-090 (Proposed, conditional) tracks the optional Lindblad/Hamiltonian extension if AC magnetometry, MW power saturation, hyperfine spectroscopy, or pulsed protocols become required.

Fixed

  • WebSocket broadcast handler now handles Lagged events gracefully and sends periodic ping keepalives to prevent dashboard disconnectshandle_ws_client and handle_ws_pose_client in wifi-densepose-sensing-server were treating RecvError::Lagged as a fatal error, causing instant disconnect when clients fell behind the 256-frame broadcast buffer at 10 Hz ingest. Clients would reconnect, immediately lag again, and rapid-cycle every 24 s. Lagged now continues (drops missed frames, logs debug) rather than breaking. Added 30 s ping keepalive on the sensing handler to prevent proxy idle timeouts.
  • Ghost skeletons in live UI with multi-node ESP32 setups (#420, ADR-082) — tracker_bridge::tracker_to_person_detections documented itself as filtering to is_alive() tracks but in fact passed every non-Terminated track to the WebSocket stream. Lost tracks — kept inside reid_window for re-identification but not currently observed — were rendering as phantom skeletons, accumulating to 22-24 with 3 nodes × 10 Hz CSI while estimated_persons correctly reported 1. Added PoseTracker::confirmed_tracks() (Tentative + Active only) and rewired the bridge to use it. Lost tracks remain in the tracker for re-ID; they just no longer ship to the UI. Regression test: test_lost_tracks_excluded_from_bridge_output.
  • Rust workspace build with --no-default-features on Windows (#366, #415) — wifi-densepose-mat, wifi-densepose-sensing-server, and wifi-densepose-train all depended on wifi-densepose-signal with default features enabled, which pulled ndarray-linalgopenblas-src → vcpkg/system-BLAS through the entire workspace. --no-default-features at the workspace root then could not opt out of BLAS, breaking cargo build / cargo test on Windows without vcpkg. All three consumers now declare wifi-densepose-signal = { ..., default-features = false }, so cargo test --workspace --no-default-features builds cleanly without vcpkg/openblas. Validated: 1,538 tests pass, 0 fail, 8 ignored.
  • signal test test_estimate_occupancy_noise_only failed without eigenvalue — The test unwrapped the NotCalibrated stub returned when the BLAS-backed estimate_occupancy is compiled out. Gated with #[cfg(feature = "eigenvalue")] so it only runs when the real implementation is available.

[v0.6.2-esp32] — 2026-04-20

Firmware release cutting ADR-081 and the Timer Svc stack fix discovered during on-hardware validation. Cut from main at commit pointing to this entry. Tested on ESP32-S3 (QFN56 rev v0.2, MAC 3c:0f:02:e9:b5:f8), 30 s continuous run: no crashes, 149 rv_feature_state_t emissions (~5 Hz), medium/slow ticks firing cleanly, HEALTH mesh packets sent.

Fixed

  • Firmware: Timer Svc stack overflow on ADR-081 fast loopemit_feature_state() runs inside the FreeRTOS Timer Svc task via the fast-loop callback; it calls stream_sender network I/O which pushes past the ESP-IDF 2 KiB default timer stack and panics ~1 s after boot. Bumped CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH to 8 KiB in sdkconfig.defaults, sdkconfig.defaults.template, and sdkconfig.defaults.4mb. Follow-up (tracked separately): move heavy work out of the timer daemon into a dedicated worker task.
  • Firmware: adaptive_controller.c implicit declaration (#404) — fast_loop_cb called emit_feature_state() before its static definition, triggering -Werror=implicit-function-declaration. Added a forward declaration above the first use.

Changed

  • CI: firmware build matrix (8MB + 4MB)firmware-ci.yml now matrix-builds both the default 8MB (sdkconfig.defaults) and 4MB SuperMini (sdkconfig.defaults.4mb) variants, uploading distinct artifacts and producing variant-named release binaries (esp32-csi-node.bin / esp32-csi-node-4mb.bin, partition-table.bin / partition-table-4mb.bin).

Added

  • ADR-081: Adaptive CSI Mesh Firmware Kernel — New 5-layer architecture (Radio Abstraction Layer / Adaptive Controller / Mesh Sensing Plane / On-device Feature Extraction / Rust handoff) that reframes the existing ESP32 firmware modules as components of a chipset-agnostic kernel. ADR in docs/adr/ADR-081-adaptive-csi-mesh-firmware-kernel.md. Goal: swap one radio family for another without changing the Rust signal / ruvector / train / mat crates.
  • Firmware: radio abstraction vtable (rv_radio_ops_t) — New firmware/esp32-csi-node/main/rv_radio_ops.{h} defines the chipset-agnostic ops (init, set_channel, set_mode, set_csi_enabled, set_capture_profile, get_health), profile enum (RV_PROFILE_PASSIVE_LOW_RATE / ACTIVE_PROBE / RESP_HIGH_SENS / FAST_MOTION / CALIBRATION), and health snapshot struct. rv_radio_ops_esp32.c provides the ESP32 binding wrapping csi_collector + esp_wifi_*. A second binding (mock or alternate chipset) is the portability acceptance test for ADR-081.
  • Firmware: rv_feature_state_t packet (magic 0xC5110006) — New 60-byte compact per-node sensing state (packed, verified by _Static_assert) in firmware/esp32-csi-node/main/rv_feature_state.h: motion, presence, respiration BPM/conf, heartbeat BPM/conf, anomaly score, env-shift score, node coherence, quality flags, IEEE CRC32. Replaces raw ADR-018 CSI as the default upstream stream (~99.7% bandwidth reduction: 300 B/s at 5 Hz vs. ~100 KB/s raw).
  • Firmware: mock radio ops binding for QEMU — New firmware/esp32-csi-node/main/rv_radio_ops_mock.c, compiled only when CONFIG_CSI_MOCK_ENABLED. Satisfies ADR-081's portability acceptance test: a second rv_radio_ops_t binding compiles and runs against the same controller + mesh-plane code as the ESP32 binding.
  • Firmware: feature-state emitter wired into controller fast loopadaptive_controller.c now emits one 60-byte rv_feature_state_t per fast tick (default 200 ms → 5 Hz), pulling from the latest edge vitals and controller observation. This is the first end-to-end Layer 4/5 path for ADR-081.
  • Firmware: csi_collector_get_pkt_yield_per_sec() / _get_send_fail_count() accessors — Expose the CSI callback rate and UDP send-failure counter so the ESP32 radio ops binding can populate rv_radio_health_t.pkt_yield_per_sec and .send_fail_count, closing the adaptive controller's observation loop.
  • Firmware: host-side unit test suite for ADR-081 pure logic — New firmware/esp32-csi-node/tests/host/ (Makefile + 2 test files + shim esp_err.h). Exercises adaptive_controller_decide() (9 test cases: degraded gate on pkt-yield collapse + coherence loss, anomaly > motion, motion → SENSE_ACTIVE, aggressive cadence, stable presence → RESP_HIGH_SENS, empty-room default, hysteresis, NULL safety) and rv_feature_state_* helpers (size assertion, IEEE CRC32 known vectors, determinism, receiver-side verification). 33/33 assertions pass. Benchmarks: decide() 3.2 ns/call, CRC32(56 B) 614 ns/pkt (87 MB/s), full finalize() 616 ns/call. Pure function adaptive_controller_decide() extracted to adaptive_controller_decide.c so the firmware build and the host tests share a single source-of-truth implementation.
  • Scripts: validate_qemu_output.py ADR-081 checks — Validator (invoked by ADR-061 scripts/qemu-esp32s3-test.sh in CI) gains three checks for adaptive controller boot line, mock radio ops registration, and slow-loop heartbeat, so QEMU runs regression-gate Layer 1/2 presence.
  • Firmware: ADR-081 Layer 3 mesh sensing plane — New firmware/esp32-csi-node/main/rv_mesh.{h,c} defines 4 node roles (Anchor / Observer / Fusion relay / Coordinator), 7 on-wire message types (TIME_SYNC, ROLE_ASSIGN, CHANNEL_PLAN, CALIBRATION_START, FEATURE_DELTA, HEALTH, ANOMALY_ALERT), 3 authorization classes (None / HMAC-SHA256-session / Ed25519-batch), rv_node_status_t (28 B), rv_anomaly_alert_t (28 B), rv_time_sync_t, rv_role_assign_t, rv_channel_plan_t, rv_calibration_start_t. Pure-C encoder/decoder (rv_mesh_encode() / rv_mesh_decode()) with 16-byte envelope + payload + IEEE CRC32 trailer; convenience encoders for each message type. Controller now emits HEALTH every slow-loop tick (30 s default) and ANOMALY_ALERT on state transitions to ALERT or DEGRADED. Host tests: test_rv_mesh exercises 27 assertions covering roundtrip, bad magic, truncation, CRC flipping, oversize payload rejection, and encode+decode throughput (1.0 μs/roundtrip on host).
  • Rust: ADR-081 Layer 1/3 mirror module — New crates/wifi-densepose-hardware/src/radio_ops.rs mirrors the firmware-side rv_radio_ops_t vtable as the Rust RadioOps trait (init, set_channel, set_mode, set_csi_enabled, set_capture_profile, get_health) and provides MockRadio for offline testing. Also mirrors the rv_mesh.h types (MeshHeader, NodeStatus, AnomalyAlert, MeshRole, MeshMsgType, AuthClass) and ships byte-identical crc32_ieee(), decode_mesh(), decode_node_status(), decode_anomaly_alert(), and encode_health(). Exported from lib.rs. 8 unit tests pass; crc32_matches_firmware_vectors verifies parity with the firmware-side test vectors (0xCBF43926 for "123456789", 0xD202EF8D for single-byte zero), and mesh_constants_match_firmware asserts MESH_MAGIC, MESH_VERSION, MESH_HEADER_SIZE, and MESH_MAX_PAYLOAD match rv_mesh.h byte-for-byte. Satisfies ADR-081's portability acceptance test: signal/ruvector/train/mat crates are untouched.
  • Firmware: adaptive controller — New firmware/esp32-csi-node/main/adaptive_controller.{c,h} implements the three-loop closed-loop control specified by ADR-081: fast (~200 ms) for cadence and active probing, medium (~1 s) for channel selection and role transitions, slow (~30 s) for baseline recalibration. Pure adaptive_controller_decide() policy function is exposed in the header for offline unit testing. Default policy is conservative (enable_channel_switch and enable_role_change off); Kconfig surface added under "Adaptive Controller (ADR-081)".

Fixed

  • Firmware: SPI flash cache crash under high CSI callback pressure (RuView#396, #397) — ESP32-S3 nodes crashed in cache_ll_l1_resume_icache / wDev_ProcessFiq after ~2400 callbacks when the promiscuous filter admitted DATA frames at 100500 Hz. Fixed by narrowing the filter mask to WIFI_PROMIS_FILTER_MASK_MGMT (~10 Hz beacons), adding a 50 Hz early callback rate gate (CSI_MIN_PROCESS_INTERVAL_US) that drops excess callbacks before any processing work, and enabling CONFIG_ESP_WIFI_EXTRA_IRAM_OPT=y as defense-in-depth. Stability validated with a 4-min-per-node soak.
  • Firmware: filter_mac / node_id clobber by WiFi driver init (#232, #375, #385, #386, #390, #397) — g_nvs_config can be corrupted during wifi_init_sta() on some devices (confirmed on 80:b5:4e:c1:be:b8), reverting node_id to the Kconfig default and producing garbage MAC-filter reads in the CSI callback (100500 Hz). New csi_collector_set_node_id() API called from app_main() before wifi_init_sta() captures both fields into module-local statics (s_node_id, s_filter_mac, s_filter_mac_set). csi_collector_init() now runs a canary that distinguishes "early≠g_nvs_config" (corruption confirmed) from a no-op match. All CSI runtime paths use the defensive copies exclusively.
  • Firmware: edge_processing sample rate mismatch (#397) — estimate_bpm_zero_crossing() was called with a hard-coded sample_rate = 20.0f, but MGMT-only promiscuous delivers ~10 Hz. Breathing and heart-rate reports were 2× too high. Corrected to 10.0f with an explicit comment tying it to the callback rate.
  • provision.py esptool command form (#391, #397) — ESP-IDF v5.4 bundles esptool 4.10.0, which only accepts write_flash (underscore). Standalone pip install esptool v5.x accepts both forms but prefers write-flash. #391 switched to write-flash which broke the documented ESP-IDF Python venv flow; #397 reverts to write_flash (works with both esptool 4.x and 5.x) with an inline comment warning future maintainers not to "re-fix" it.
  • provision.py esptool v5 dry-run hint (#391) — Stale write_flash (underscore) syntax in the dry-run manual-flash hint now uses write-flash (hyphenated) for esptool >= 5.x. The primary flash command was already correct.
  • provision.py silent NVS wipe (#391) — The script replaces the entire csi_cfg NVS namespace on every run, so partial invocations were silently erasing WiFi credentials and causing Retrying WiFi connection (10/10) in the field. Now refuses to run without --ssid, --password, and --target-ip unless --force-partial is passed. --force-partial prints a warning listing which keys will be wiped.
  • Firmware: defensive node_id capture (#232, #375, #385, #386, #390) — Users on multi-node deployments reported node_id reverting to the Kconfig default (1) in UDP frames and in the csi_collector init log, despite NVS loading the correct value. The root cause (memory corruption of g_nvs_config) has not been definitively isolated, but the UDP frame header is now tamper-proof: csi_collector_init() captures g_nvs_config.node_id into a module-local s_node_id once, and csi_serialize_frame() plus all other consumers (edge_processing.c, wasm_runtime.c, display_ui.c, swarm_bridge_init) read it via the new csi_collector_get_node_id() accessor. A canary logs WARN if g_nvs_config.node_id diverges from s_node_id at end-of-init, helping isolate the upstream corruption path. Validated on attached ESP32-S3 (COM8): NVS node_id=2 propagates through boot log, capture log, init log, and byte[4] of every UDP frame.

Docs

  • CHANGELOG catch-up (#367) — Added missing entries for v0.5.5, v0.6.0, and v0.7.0 releases.

[v0.7.0] — 2026-04-06

Model release (no new firmware binary). Firmware remains at v0.6.0-esp32.

Added

  • Camera ground-truth training pipeline (ADR-079) — End-to-end supervised WiFlow pose training using MediaPipe + real ESP32 CSI.
    • scripts/collect-ground-truth.py — MediaPipe PoseLandmarker webcam capture (17 COCO keypoints, 30fps), synchronized with CSI recording over nanosecond timestamps.
    • scripts/align-ground-truth.js — Time-aligns camera keypoints with 20-frame CSI windows by binary search, confidence-weighted averaging.
    • scripts/train-wiflow-supervised.js — 3-phase curriculum training (contrastive → supervised SmoothL1 → bone/temporal refinement) with 4 scale presets (lite/small/medium/full).
    • scripts/eval-wiflow.js — PCK@10/20/50, MPJPE, per-joint breakdown, baseline proxy mode.
    • scripts/record-csi-udp.py — Lightweight ESP32 CSI UDP recorder (no Rust build required).
  • ruvector optimizations (O6-O10) — Subcarrier selection (70→35, 50% reduction), attention-weighted subcarriers, Stoer-Wagner min-cut person separation, multi-SPSA gradient estimation, Mac M4 Pro training via Tailscale.
  • Scalable WiFlow presetslite (189K params, ~19 min) through full (7.7M params, ~8 hrs) to match dataset size.
  • Pre-trained WiFlow v1 model — 92.9% PCK@20, 974 KB, 186,946 params. Published to HuggingFace under wiflow-v1/.

Validated

  • 92.9% PCK@20 pose accuracy from a 5-minute data collection session with one $9 ESP32-S3 and one laptop webcam.
  • Training pipeline validated on real paired data: 345 samples, 19 min training, eval loss 0.082, bone constraint 0.008.

[v0.6.0-esp32] — 2026-04-03

Added

  • Pre-trained CSI sensing weights published — First official pre-trained models on HuggingFace. model.safetensors (48 KB), model-q4.bin (8 KB 4-bit), model-q2.bin (4 KB), presence-head.json, per-node LoRA adapters.
  • 17 sensing applications — Sleep monitor, apnea detector, stress monitor, gait analyzer, RF tomography, passive radar, material classifier, through-wall detector, device fingerprint, and more. Each as a standalone scripts/*.js.
  • ADRs 069-078 — 10 new architecture decisions covering Cognitum Seed integration, self-supervised pretraining, ruvllm pipeline, WiFlow architecture, channel hopping, SNN, MinCut person separation, CNN spectrograms, novel RF applications, multi-frequency mesh.
  • Kalman tracker (PR #341 by @taylorjdawson) — temporal smoothing of pose keypoints.

Fixed

  • Security fix merged via PR #310.

Performance

  • Presence detection: 100% accuracy on 60,630 overnight samples.
  • Inference: 0.008 ms per sample, 164K embeddings/sec.
  • Contrastive self-supervised training: 51.6% improvement over baseline.

[v0.5.5-esp32] — 2026-04-03

Added

  • WiFlow SOTA architecture (ADR-072) — TCN + axial attention pose decoder, 1.8M params, 881 KB at 4-bit. 17 COCO keypoints from CSI amplitude only (no phase).
  • Multi-frequency mesh scanning (ADR-073) — ESP32 nodes hop across channels 1/3/5/6/9/11 at 200ms dwell. Neighbor WiFi networks used as passive radar illuminators. Null subcarriers reduced from 19% to 16%.
  • Spiking neural network (ADR-074) — STDP online learning, adapts to new rooms in <30s with no labels, 16-160x less compute than batch training.
  • MinCut person counting (ADR-075) — Stoer-Wagner min-cut on subcarrier correlation graph. Fixes #348 (was always reporting 4 people).
  • CNN spectrogram embeddings (ADR-076) — Treat 64×20 CSI as an image, produce 128-dim environment fingerprints (0.95+ same-room similarity).
  • Graph transformer fusion — Multi-node CSI fusion via GATv2 attention (replaces naive averaging).
  • Camera-free pose training pipeline — Trains 17-keypoint model from 10 sensor signals with no camera required.

Fixed

  • #348 person counting — MinCut correctly counts 1-4 people (24/24 validation windows).

[v0.5.4-esp32] — 2026-04-02

Added

  • ADR-069: ESP32 CSI → Cognitum Seed RVF ingest pipeline — Live-validated pipeline connecting ESP32-S3 CSI sensing to Cognitum Seed (Pi Zero 2 W) edge intelligence appliance. 339 vectors ingested, 100% kNN validation, SHA-256 witness chain verified.
  • Feature vector packet (magic 0xC5110003) — New 48-byte packet with 8 normalized dimensions (presence, motion, breathing, heart rate, phase variance, person count, fall, RSSI) sent at 1 Hz alongside vitals.
  • scripts/seed_csi_bridge.py — Python bridge: UDP listener → HTTPS ingest with bearer token auth, --validate (kNN + PIR ground truth), --stats, --compact modes, hash-based vector IDs, NaN/inf rejection, source IP filtering, retry logic.
  • Arena Physica research — 26 research documents in docs/research/ covering Maxwell's equations in WiFi sensing, Arena Physica Studio analysis, SOTA WiFi sensing 2025-2026, GOAP implementation plan for ESP32 + Pi Zero.
  • Cognitum Seed MCP integration — 114-tool MCP proxy enables AI assistants to query sensing state, vectors, witness chain, and device status directly.

Fixed

  • Compressed frame magic collision — Reassigned compressed frame magic from 0xC5110003 to 0xC5110005 to free 0xC5110003 for feature vectors.
  • Uninitialized s_top_k[0] read — Guarded variance computation against s_top_k_count == 0 in send_feature_vector().
  • Presence score normalization — Bridge now divides by 15.0 instead of clamping, preserving dynamic range for raw values 1.41-14.92.
  • Stale magic references — Updated ADR-039, DDD model to reflect 0xC5110005 for compressed frames.

Security

  • Credential exposure remediation — Removed hardcoded WiFi passwords and bearer tokens from source files. Added NVS binary/CSV patterns to .gitignore. Environment variable fallback for bearer token.
  • NaN/Inf injection prevention — Bridge validates all feature dimensions are finite before Seed ingest.
  • UDP source filtering--allowed-sources argument restricts packet acceptance to known ESP32 IPs.

Changed

  • Wire format table now includes 6 magic numbers: 0xC5110001 (raw), 0xC5110002 (vitals), 0xC5110003 (features), 0xC5110004 (WASM events), 0xC5110005 (compressed), 0xC5110006 (fused vitals).

[v0.5.3-esp32] — 2026-03-30

Added

  • Cross-node RSSI-weighted feature fusion — Multiple ESP32 nodes fuse CSI features using RSSI-based weighting. Closer node gets higher weight. Reduces variance noise by 29%, keypoint jitter by 72%.
  • DynamicMinCut person separation — Uses ruvector_mincut::DynamicMinCut on the subcarrier temporal correlation graph to detect independent motion clusters. Replaces variance-based heuristic for multi-person counting.
  • RSSI-based position tracking — Skeleton position driven by RSSI differential between nodes. Walk between ESP32s and the skeleton follows you.
  • Per-node state pipeline (ADR-068) — Each ESP32 node gets independent HashMap<u8, NodeState> with frame history, classification, vitals, and person count. Fixes #249 (the #1 user-reported issue).
  • RuVector Phase 1-3 integration — Subcarrier importance weighting, temporal keypoint smoothing (EMA), coherence gating, skeleton kinematic constraints (Jakobsen relaxation), compressed pose history.
  • Client-side lerp smoothing — UI keypoints interpolate between frames (alpha=0.15) for fluid skeleton movement.
  • Multi-node mesh tests — 8 integration tests covering 1-255 node configurations.
  • wifi_densepose Python packagefrom wifi_densepose import WiFiDensePose now works (#314).

Fixed

  • Watchdog crash on busy LANs (#321) — Batch-limited edge_dsp to 4 frames before 20ms yield. Fixed idle-path busy-spin (pdMS_TO_TICKS(5)==0).
  • No detection from edge vitals (#323) — Server now generates sensing_update from Tier 2+ vitals packets.
  • RSSI byte offset mismatch (#332) — Server parsed RSSI from wrong byte (was reading sequence counter).
  • Stack overflow risk — Moved 4KB of BPM scratch buffers from stack to static storage.
  • Stale node memory leaknode_states HashMap evicts nodes inactive >60s.
  • Unsafe raw pointer removed — Replaced with safe .clone() for adaptive model borrow.
  • Firmware CI — Upgraded to IDF v5.4, replaced xxd with od (#327).
  • Person count double-counting — Multi-node aggregation changed from sum to max.
  • Skeleton jitter — Removed tick-based noise, dampened procedural animation, recalibrated feature scaling for real ESP32 data.

Changed

  • Motion-responsive skeleton: arm swing (0-80px) driven by CSI variance, leg kick (0-50px) by motion_band_power, vertical bob when walking.
  • Person count thresholds recalibrated for real ESP32 hardware (1→2 at 0.70, EMA alpha 0.04).
  • Vital sign filtering: larger median window (31), faster EMA (0.05), looser HR jump filter (15 BPM).
  • Vendored ruvector updated to v2.1.0-40 (316 commits ahead).

Benchmarks (2-node mesh, COM6 + COM9, 30s)

Metric Baseline v0.5.3 Improvement
Variance noise 109.4 77.6 -29%
Feature stability std=154.1 std=105.4 -32%
Keypoint jitter std=4.5px std=1.3px -72%
Confidence 0.643 0.686 +7%
Presence accuracy 93.4% 94.6% +1.3pp

Verified

  • Real hardware: COM6 (node 1) + COM9 (node 2) on ruv.net WiFi
  • All 284 Rust tests pass, 352 signal crate tests pass
  • Firmware builds clean at 843 KB
  • QEMU CI: 11/11 jobs green

[v0.5.2-esp32] — 2026-03-28

Fixed

  • RSSI byte offset in frame parser (#332)
  • Per-node state pipeline for multi-node sensing (#249)
  • Firmware CI upgraded to IDF v5.4 (#327)

[v0.5.1-esp32] — 2026-03-27

Fixed

  • Watchdog crash on busy LANs (#321)
  • No detection from edge vitals (#323)
  • wifi_densepose Python package import (#314)
  • Pre-compiled firmware binaries added to release

[v0.5.0-esp32] — 2026-03-15

Added

  • 60 GHz mmWave sensor fusion (ADR-063) — Auto-detects Seeed MR60BHA2 (60 GHz, HR/BR/presence) and HLK-LD2410 (24 GHz, presence/distance) on UART at boot. Probes 115200 then 256000 baud, registers device capabilities, starts background parser.
  • 48-byte fused vitals packet (magic 0xC5110004) — Kalman-style fusion: mmWave 80% + CSI 20% when both available. Automatic fallback to standard 32-byte CSI-only packet.
  • Server-side fusion bridge (scripts/mmwave_fusion_bridge.py) — Reads two serial ports simultaneously for dual-sensor setups where mmWave runs on a separate ESP32.
  • Multimodal ambient intelligence roadmap (ADR-064) — 25+ applications from fall detection to sleep monitoring to RF tomography.

Verified

  • Real hardware: ESP32-S3 (COM7) WiFi CSI + ESP32-C6/MR60BHA2 (COM4) 60 GHz mmWave running concurrently. HR=75 bpm, BR=25/min at 52 cm range. All 11 QEMU CI jobs green.

[v0.4.3-esp32] — 2026-03-15

Fixed

  • Fall detection false positives (#263) — Default threshold raised from 2.0 to 15.0 rad/s²; normal walking (2-5 rad/s²) no longer triggers alerts. Added 3-consecutive-frame debounce and 5-second cooldown between alerts. Verified on real ESP32-S3 hardware: 0 false alerts in 60s / 1,300+ live WiFi CSI frames.
  • Kconfig default mismatchCONFIG_EDGE_FALL_THRESH Kconfig default was still 2000 (=2.0) while nvs_config.c fallback was updated to 15.0. Fixed Kconfig to 15000. Caught by real hardware testing — mock data did not reproduce.
  • provision.py NVS generator API changeesp_idf_nvs_partition_gen package changed its generate() signature; switched to subprocess-first invocation for cross-version compatibility.
  • QEMU CI pipeline (11 jobs) — Fixed all failures: fuzz test esp_timer stubs, QEMU libgcrypt dependency, NVS matrix generator, IDF container pip path, flash image padding, validation WARN handling, swarm ip/cargo missing.

Added

  • 4MB flash support (#265)partitions_4mb.csv and sdkconfig.defaults.4mb for ESP32-S3 boards with 4MB flash (e.g. SuperMini). Dual OTA slots, 1.856 MB each. Thanks to @sebbu for the community workaround that confirmed feasibility.
  • --strict flag for validate_qemu_output.py — WARNs now pass by default in CI (no real WiFi in QEMU); use --strict to fail on warnings.

Unreleased

Added

  • QEMU ESP32-S3 testing platform (ADR-061) — 9-layer firmware testing without hardware
    • Mock CSI generator with 10 physics-based scenarios (empty room, walking, fall, multi-person, etc.)
    • Single-node QEMU runner with 16-check UART validation
    • Multi-node TDM mesh simulation (TAP networking, 2-6 nodes)
    • GDB remote debugging with VS Code integration
    • Code coverage via gcov/lcov + apptrace
    • Fuzz testing (3 libFuzzer targets + ASAN/UBSAN)
    • NVS provisioning matrix (14 configs)
    • Snapshot-based regression testing (sub-second VM restore)
    • Chaos testing with fault injection + health monitoring
  • QEMU Swarm Configurator (ADR-062) — YAML-driven multi-ESP32 test orchestration
    • 4 topologies: star, mesh, line, ring
    • 3 node roles: sensor, coordinator, gateway
    • 9 swarm-level assertions (boot, crashes, TDM, frame rate, fall detection, etc.)
    • 7 presets: smoke (2n/15s), standard (3n/60s), ci-matrix, large-mesh, line-relay, ring-fault, heterogeneous
    • Health oracle with cross-node validation
  • QEMU installer (install-qemu.sh) — auto-detects OS, installs deps, builds Espressif QEMU fork
  • Unified QEMU CLI (qemu-cli.sh) — single entry point for all 11 QEMU test commands
  • CI: firmware-qemu.yml workflow with QEMU test matrix, fuzz testing, NVS validation, and swarm test jobs
  • User guide: QEMU testing and swarm configurator section with plain-language walkthrough

Fixed

  • Firmware now boots in QEMU: WiFi/UDP/OTA/display guards for mock CSI mode

  • 9 bugs in mock_csi.c (LFSR bias, MAC filter init, scenario loop, overflow burst timing)

  • 23 bugs from ADR-061 deep review (inject_fault.py writes, CI cache, snapshot log corruption, etc.)

  • 16 bugs from ADR-062 deep review (log filename mismatch, SLIRP port collision, heap false positives, etc.)

  • All scripts: --help flags, prerequisite checks with install hints, standardized exit codes

  • Sensing server UI API completion (ADR-043) — 14 fully-functional REST endpoints for model management, CSI recording, and training control

    • Model CRUD: GET /api/v1/models, GET /api/v1/models/active, POST /api/v1/models/load, POST /api/v1/models/unload, DELETE /api/v1/models/:id, GET /api/v1/models/lora/profiles, POST /api/v1/models/lora/activate
    • CSI recording: GET /api/v1/recording/list, POST /api/v1/recording/start, POST /api/v1/recording/stop, DELETE /api/v1/recording/:id
    • Training control: GET /api/v1/train/status, POST /api/v1/train/start, POST /api/v1/train/stop
    • Recording writes CSI frames to .jsonl files via tokio background task
    • Model/recording directories scanned at startup, state managed via Arc<RwLock<AppStateInner>>
  • ADR-044: Provisioning tool enhancements — 5-phase plan for complete NVS coverage (7 missing keys), JSON config files, mesh presets, read-back/verify, and auto-detect

  • 25 real mobile tests replacing it.todo() placeholders — 205 assertions covering components, services, stores, hooks, screens, and utils

  • Project MERIDIAN (ADR-027) — Cross-environment domain generalization for WiFi pose estimation (1,858 lines, 72 tests)

    • HardwareNormalizer — Catmull-Rom cubic interpolation resamples any hardware CSI to canonical 56 subcarriers; z-score + phase sanitization
    • DomainFactorizer + GradientReversalLayer — adversarial disentanglement of pose-relevant vs environment-specific features
    • GeometryEncoder + FilmLayer — Fourier positional encoding + DeepSets + FiLM for zero-shot deployment given AP positions
    • VirtualDomainAugmentor — synthetic environment diversity (room scale, wall material, scatterers, noise) for 4x training augmentation
    • RapidAdaptation — 10-second unsupervised calibration via contrastive test-time training + LoRA adapters
    • CrossDomainEvaluator — 6-metric evaluation protocol (MPJPE in-domain/cross-domain/few-shot/cross-hardware, domain gap ratio, adaptation speedup)
  • ADR-027: Cross-Environment Domain Generalization — 10 SOTA citations (PerceptAlign, X-Fi ICLR 2025, AM-FM, DGSense, CVPR 2024)

  • Cross-platform RSSI adapters — macOS CoreWLAN (MacosCoreWlanScanner) and Linux iw (LinuxIwScanner) Rust adapters with #[cfg(target_os)] gating

  • macOS CoreWLAN Python sensing adapter with Swift helper (mac_wifi.swift)

  • macOS synthetic BSSID generation (FNV-1a hash) for Sonoma 14.4+ BSSID redaction

  • Linux iw dev <iface> scan parser with freq-to-channel conversion and scan dump (no-root) mode

  • ADR-025: macOS CoreWLAN WiFi Sensing (ORCA)

Fixed

  • sendto ENOMEM crash (Issue #127) — CSI callbacks in promiscuous mode exhaust lwIP pbuf pool causing guru meditation crash. Fixed with 50 Hz rate limiter in csi_collector.c and 100 ms ENOMEM backoff in stream_sender.c. Hardware-verified on ESP32-S3 (200+ callbacks, zero crashes)
  • Provisioning script missing TDM/edge flags (Issue #130) — Added --tdm-slot, --tdm-total, --edge-tier, --pres-thresh, --fall-thresh, --vital-win, --vital-int, --subk-count to provision.py
  • WebSocket "RECONNECTING" on Dashboard/Live DemosensingService.start() now called on app init in app.js so WebSocket connects immediately instead of waiting for Sensing tab visit
  • Mobile WebSocket portws.service.ts buildWsUrl() uses same-origin port instead of hardcoded port 3001
  • Mobile Jest configtestPathIgnorePatterns no longer silently ignores the entire test directory
  • Removed synthetic byte counters from Python MacosWifiCollector — now reports tx_bytes=0, rx_bytes=0 instead of fake incrementing values

3.0.0 - 2026-03-01

Major release: AETHER contrastive embedding model, Docker Hub images, and comprehensive UI overhaul.

Added — AETHER Contrastive Embedding Model (ADR-024)

  • Project AETHER — self-supervised contrastive learning for WiFi CSI fingerprinting, similarity search, and anomaly detection (9bbe956)
  • embedding.rs module: ProjectionHead, InfoNceLoss, CsiAugmenter, FingerprintIndex, PoseEncoder, EmbeddingExtractor (909 lines, zero external ML dependencies)
  • SimCLR-style pretraining with 5 physically-motivated augmentations (temporal jitter, subcarrier masking, Gaussian noise, phase rotation, amplitude scaling)
  • CLI flags: --pretrain, --pretrain-epochs, --embed, --build-index <type>
  • Four HNSW-compatible fingerprint index types: env_fingerprint, activity_pattern, temporal_baseline, person_track
  • Cross-modal PoseEncoder for WiFi-to-camera embedding alignment
  • VICReg regularization for embedding collapse prevention
  • 53K total parameters (55 KB at INT8) — fits on ESP32

Added — Docker & Deployment

  • Published Docker Hub images: ruvnet/wifi-densepose:latest (132 MB Rust) and ruvnet/wifi-densepose:python (569 MB) (add9f19)
  • Multi-stage Dockerfile for Rust sensing server with RuVector crates
  • docker-compose.yml orchestrating both Rust and Python services
  • RVF model export via --export-rvf and load via --load-rvf CLI flags

Added — Documentation

  • 33 use cases across 4 vertical tiers: Everyday, Specialized, Robotics & Industrial, Extreme (0afd9c5)
  • "Why WiFi Wins" comparison table (WiFi vs camera vs LIDAR vs wearable vs PIR)
  • Mermaid architecture diagrams: end-to-end pipeline, signal processing detail, deployment topology (50f0fc9)
  • Models & Training section with RuVector crate links (GitHub + crates.io), SONA component table (965a1cc)
  • RVF container section with deployment targets table (ESP32 0.7 MB to server 50+ MB)
  • Collapsible README sections for improved navigation (478d964, 99ec980, 0ebd6be)
  • Installation and Quick Start moved above Table of Contents (50acbf7)
  • CSI hardware requirement notice (528b394)

Fixed

  • UI auto-detects server port from page origin — no more hardcoded localhost:8080; works on any port (Docker :3000, native :8080, custom) (3b72f35, closes #55)
  • Docker port mismatch — server now binds 3000/3001 inside container as documented (44b9c30)
  • Added /ws/sensing WebSocket route to the HTTP server so UI only needs one port
  • Fixed README API endpoint references: /api/v1/health/health, /api/v1/sensing/api/v1/sensing/latest
  • Multi-person tracking limit corrected: configurable default 10, no hard software cap (e2ce250)

2.0.0 - 2026-02-28

Major release: complete Rust sensing server, full DensePose training pipeline, RuVector v2.0.4 integration, ESP32-S3 firmware, and 6 security hardening patches.

Added — Rust Sensing Server

  • Full DensePose-compatible REST API served by Axum (d956c30)
    • GET /health — server health
    • GET /api/v1/sensing/latest — live CSI sensing data
    • GET /api/v1/vital-signs — breathing rate (6-30 BPM) and heartbeat (40-120 BPM)
    • GET /api/v1/pose/current — 17 COCO keypoints derived from WiFi signal field
    • GET /api/v1/info — server build and feature info
    • GET /api/v1/model/info — RVF model container metadata
    • ws://host/ws/sensing — real-time WebSocket stream
  • Three data sources: --source esp32 (UDP CSI), --source windows (netsh RSSI), --source simulated (deterministic reference)
  • Auto-detection: server probes ESP32 UDP and Windows WiFi, falls back to simulated
  • Three.js visualization UI with 3D body skeleton, signal heatmap, phase plot, Doppler bars, vital signs panel
  • Static UI serving via --ui-path flag
  • Throughput: 9,52011,665 frames/sec (release build)

Added — ADR-021: Vital Sign Detection

  • VitalSignDetector with breathing (6-30 BPM) and heartbeat (40-120 BPM) extraction from CSI fluctuations (1192de9)
  • FFT-based spectral analysis with configurable band-pass filters
  • Confidence scoring based on spectral peak prominence
  • REST endpoint /api/v1/vital-signs with real-time JSON output

Added — ADR-023: DensePose Training Pipeline (Phases 1-8)

  • wifi-densepose-train crate with complete 8-phase pipeline (fc409df, ec98e40, fce1271)
    • Phase 1: DataPipeline with MM-Fi and Wi-Pose dataset loaders
    • Phase 2: CsiToPoseTransformer — 4-head cross-attention + 2-layer GCN on COCO skeleton
    • Phase 3: 6-term composite loss (MSE, bone length, symmetry, joint angle, temporal, confidence)
    • Phase 4: DynamicPersonMatcher via ruvector-mincut (O(n^1.5 log n) Hungarian assignment)
    • Phase 5: SonaAdapter — MicroLoRA rank-4 with EWC++ memory preservation
    • Phase 6: SparseInference — progressive 3-layer model loading (A: essential, B: refinement, C: full)
    • Phase 7: RvfContainer — single-file model packaging with segment-based binary format
    • Phase 8: End-to-end training with cosine-annealing LR, early stopping, checkpoint saving
  • CLI: --train, --dataset, --epochs, --save-rvf, --load-rvf, --export-rvf
  • Benchmark: ~11,665 fps inference, 229 tests passing

Added — ADR-016: RuVector Training Integration (all 5 crates)

  • ruvector-mincutDynamicPersonMatcher in metrics.rs + subcarrier selection (81ad09d, a7dd31c)
  • ruvector-attn-mincut → antenna attention in model.rs + noise-gated spectrogram
  • ruvector-temporal-tensorCompressedCsiBuffer in dataset.rs + compressed breathing/heartbeat
  • ruvector-solver → sparse subcarrier interpolation (114→56) + Fresnel triangulation
  • ruvector-attention → spatial attention in model.rs + attention-weighted BVP
  • Vendored all 11 RuVector crates under vendor/ruvector/ (d803bfe)

Added — ADR-017: RuVector Signal & MAT Integration (7 integration points)

  • gate_spectrogram() — attention-gated noise suppression (18170d7)
  • attention_weighted_bvp() — sensitivity-weighted velocity profiles
  • mincut_subcarrier_partition() — dynamic sensitive/insensitive subcarrier split
  • solve_fresnel_geometry() — TX-body-RX distance estimation
  • CompressedBreathingBuffer + CompressedHeartbeatSpectrogram
  • BreathingDetector + HeartbeatDetector (MAT crate, real FFT + micro-Doppler)
  • Feature-gated behind cfg(feature = "ruvector") (ab2453e)

Added — ADR-018: ESP32-S3 Firmware & Live CSI Pipeline

  • ESP32-S3 firmware with FreeRTOS CSI extraction (92a5182)
  • ADR-018 binary frame format: [0xAD, 0x18, len_hi, len_lo, payload]
  • Rust Esp32Aggregator receiving UDP frames on port 5005
  • bridge.rs converting I/Q pairs to amplitude/phase vectors
  • NVS provisioning for WiFi credentials
  • Pre-built binary quick start documentation (696a726)

Added — ADR-014: SOTA Signal Processing

  • 6 algorithms, 83 tests (fcb93cc)
    • Hampel filter (median + MAD, resistant to 50% contamination)
    • Conjugate multiplication (reference-antenna ratio, cancels common-mode noise)
    • Phase sanitization (unwrap + linear detrend, removes CFO/SFO)
    • Fresnel zone geometry (TX-body-RX distance from first-principles physics)
    • Body Velocity Profile (micro-Doppler extraction, 5.7x speedup)
    • Attention-gated spectrogram (learned noise suppression)

Added — ADR-015: Public Dataset Training Strategy

  • MM-Fi and Wi-Pose dataset specifications with download links (4babb32, 5dc2f66)
  • Verified dataset dimensions, sampling rates, and annotation formats
  • Cross-dataset evaluation protocol

Added — WiFi-Mat Disaster Detection Module

  • Multi-AP triangulation for through-wall survivor detection (a17b630, 6b20ff0)
  • Triage classification (breathing, heartbeat, motion)
  • Domain events: survivor_detected, survivor_updated, alert_created
  • WebSocket broadcast at /ws/mat/stream

Added — Infrastructure

  • Guided 7-step interactive installer with 8 hardware profiles (8583f3e)
  • Comprehensive build guide for Linux, macOS, Windows, Docker, ESP32 (45f8a0d)
  • 12 Architecture Decision Records (ADR-001 through ADR-012) (337dd96)

Added — UI & Visualization

  • Sensing-only UI mode with Gaussian splat visualization (b7e0f07)
  • Three.js 3D body model (17 joints, 16 limbs) with signal-viz components
  • Tabs: Dashboard, Hardware, Live Demo, Sensing, Architecture, Performance, Applications
  • WebSocket client with automatic reconnection and exponential backoff

Added — Rust Signal Processing Crate

  • Complete Rust port of WiFi-DensePose with modular workspace (6ed69a3)
    • wifi-densepose-signal — CSI processing, phase sanitization, feature extraction
    • wifi-densepose-core — shared types and configuration
    • wifi-densepose-nn — neural network inference (DensePose head, RCNN)
    • wifi-densepose-hardware — ESP32 aggregator, hardware interfaces
    • wifi-densepose-config — configuration management
  • Comprehensive benchmarks and validation tests (3ccb301)

Added — Python Sensing Pipeline

  • WindowsWifiCollector — RSSI collection via netsh wlan show networks
  • RssiFeatureExtractor — variance, spectral bands (motion 0.5-4 Hz, breathing 0.1-0.5 Hz), change points
  • PresenceClassifier — rule-based 3-state classification (ABSENT / PRESENT_STILL / ACTIVE)
  • Cross-receiver agreement scoring for multi-AP confidence boosting
  • WebSocket sensing server (ws_server.py) broadcasting JSON at 2 Hz
  • Deterministic CSI proof bundles for reproducible verification (archive/v1/data/proof/)
  • Commodity sensing unit tests (b391638)

Changed

  • Rust hardware adapters now return explicit errors instead of silent empty data (6e0e539)

Fixed

  • Review fixes for end-to-end training pipeline (45f0304)
  • Dockerfile paths updated from src/ to archive/v1/src/ (7872987)
  • IoT profile installer instructions updated for aggregator CLI (f460097)
  • process.env reference removed from browser ES module (e320bc9)

Performance

  • 5.7x Doppler extraction speedup via optimized FFT windowing (32c75c8)
  • Single 2.1 MB static binary, zero Python dependencies for Rust server

Security

  • Fix SQL injection in status command and migrations (f9d125d)
  • Fix XSS vulnerabilities in UI components (5db55fd)
  • Fix command injection in statusline.cjs (4cb01fd)
  • Fix path traversal vulnerabilities (896c4fc)
  • Fix insecure WebSocket connections — enforce wss:// on non-localhost (ac094d4)
  • Fix GitHub Actions shell injection (ab2e7b4)
  • Fix 10 additional vulnerabilities, remove 12 dead code instances (7afdad0)

1.1.0 - 2025-06-07

Added

  • Complete Python WiFi-DensePose system with CSI data extraction and router interface
  • CSI processing and phase sanitization modules
  • Batch processing for CSI data in CSIProcessor and PhaseSanitizer
  • Hardware, pose, and stream services for WiFi-DensePose API
  • Comprehensive CSS styles for UI components and dark mode support
  • API and Deployment documentation

Fixed

  • Badge links for PyPI and Docker in README
  • Async engine creation poolclass specification

1.0.0 - 2024-12-01

Added

  • Initial release of WiFi-DensePose
  • Real-time WiFi-based human pose estimation using Channel State Information (CSI)
  • DensePose neural network integration for body surface mapping
  • RESTful API with comprehensive endpoint coverage
  • WebSocket streaming for real-time pose data
  • Multi-person tracking with configurable capacity (default 10, up to 50+)
  • Fall detection and activity recognition
  • Domain configurations: healthcare, fitness, smart home, security
  • CLI interface for server management and configuration
  • Hardware abstraction layer for multiple WiFi chipsets
  • Phase sanitization and signal processing pipeline
  • Authentication and rate limiting
  • Background task management
  • Cross-platform support (Linux, macOS, Windows)

Documentation

  • User guide and API reference
  • Deployment and troubleshooting guides
  • Hardware setup and calibration instructions
  • Performance benchmarks
  • Contributing guidelines