mirror of
https://github.com/ruvnet/RuView.git
synced 2026-06-02 00:58:56 +02:00
c7ddb2d7d195b85f153cd71060cdf01ab2ce8318
150 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c7ddb2d7d1 |
feat(worldmodel): ADR-147 — OccWorld world model integration, wifi-densepose-worldmodel v0.3.0 (#856)
* feat(worldmodel): ADR-147 — OccWorld integration, wifi-densepose-worldmodel v0.3.0 (#854) - New crate `wifi-densepose-worldmodel` v0.3.0: async Unix-socket bridge to OccWorld Python inference server; `OccWorldBridge`, `OccupancyGrid3D`, `TrajectoryPrior`, `worldgraph_to_occupancy` encoder (14/14 tests pass) - `scripts/occworld_server.py`: long-lived Python inference server for OccWorld TransVQVAE (72.4M params); applies API-bug patches; dummy mode for CI testing; graceful SIGTERM shutdown - `pose_tracker.rs`: `trajectory_prior` soft-blend injection (80/20 Kalman/prior) on torso keypoint; `set_trajectory_prior()` public method - CI: added `Run ADR-147 worldmodel tests` step - ADR-147: accepted — OccWorld primary (209 ms, 3.37 GB VRAM, RTX 5080); Cosmos deferred to ADR-148 (32.54 GB VRAM exceeds hardware) - Benchmark proof: 208.7 ms P50, 3.37 GB peak VRAM, 12.1 GB headroom Co-Authored-By: claude-flow <ruv@ruv.net> * chore: update ruvector.db state Co-Authored-By: claude-flow <ruv@ruv.net> * chore: ruvector.db sync Co-Authored-By: claude-flow <ruv@ruv.net> * fix(cli): add missing min_frames field to CalibrateArgs test helper E0063 in calibrate.rs:448 — CalibrateArgs gained min_frames in ADR-135 but the default_args() test helper was not updated. min_frames=0 means 'use tier default', matching the existing runtime behaviour. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
baba851a89 |
docs(readme): link ecosystem badges + move Beta callout to bottom
Three operator-feedback corrections to the README:
1. Every ecosystem badge in the top row now links to a real
destination — Home Assistant -> integrations/home-assistant.md,
Matter -> ADR-122, Apple Home -> user-guide-apple-homepod.md,
Google Home + Alexa -> the HA integration doc (both ecosystems
reach RuView through HA's bridge today). Added an Alexa badge
alongside the existing four so all four major ecosystems are
represented. Dropped the now-redundant separate "HomePod
Integration" badge — the Apple Home badge linking to the same
guide is enough.
2. Beta callout moved from line 14 (under the hero image) to a
dedicated `## Beta software` section immediately before the
License. The callout's content is unchanged; it just no longer
gates the elevator pitch. Readers see the value proposition
first, the caveats at the bottom alongside license + support.
3. The intro paragraph ("Turn ordinary WiFi into ...") now ends
with a one-line summary of native ecosystem support naming all
four — Home Assistant, Apple Home & HomePod, Google Home, Alexa —
plus the Matter endpoint, each linked. The previous mention of
ecosystems was buried further down the page; this surfaces it
in the intro where the user reads first.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
2bccdf5065 |
ADR-125 APPLE-FABRIC: RuView <-> Apple Home native HAP bridge (e2e on real C6) (#797)
* feat(adr-125 iter 3): BFLD PrivacyGate + semantic-event naming at HAP boundary
Inserts a Python equivalent of `wifi-densepose-bfld::PrivacyClass` +
`PrivacyGate` between the rv_feature_state parser and the HAP toggle
file. ADR-125 §2.1.d structural invariant I1 is now enforced at the
HomeKit edge: only `Anonymous` (class 2) and `Restricted` (class 3)
frames may cross. `Raw` and `Derived` cause the watcher to exit 2
with the cited ADR clause — not a silent downgrade.
Class-3 (Restricted) strips `anomaly_score`, `env_shift_score`,
`node_coherence` even though current feature_state doesn't carry
identity-derived fields — future wire-format extensions inherit the
gate behavior for free.
Operator-facing semantic naming follows ADR-125 §2.1.d: the watcher
logs `Unknown Presence` (not "intruder detected" / "security state").
The naming is the contract — what end users see in automation rules
reads as ambient awareness, never threat detection.
Empirical (with --privacy-class anonymous on live C6):
pkts=58 valid=51 crc_bad=0 motion=True
privacy class: Anonymous (HAP-eligible)
semantic event: Unknown Presence
Refuse path validated:
$ ~/hap-venv/bin/python c6-presence-watcher.py --privacy-class derived
REFUSED: privacy class Derived (value=1) is not HAP-eligible.
ADR-125 §2.1.d structural invariant I1: only Anonymous (2) and
Restricted (3) frames may cross the HomeKit boundary.
$ echo $?
2
Branch: feat/adr-125-apple-fabric (kept off main while docker build
for sha
|
||
|
|
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
|
||
|
|
faecee9a37 |
feat(adr-118): BFLD — Beamforming Feedback Layer for Detection (#789)
* 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
* docs(adr-118): user-guide.md BFLD subsection (345/345 GREEN)
Iter 52. PR-readiness pivot iter #3. Closes pre-merge checklist item #6
(user-guide.md update for new setup steps / CLI flags / integrations).
Adds a BFLD subsection inside the existing HA chapter so operators
already reading about HA-DISCO discover BFLD as the natural next layer.
Notes on iter context:
- Local branch was hard-reset earlier in the session (working tree
showed only iters 1-3 state); remote origin/feat/adr-118-bfld-impl
retained the full chain plus a sibling agent's ADR-124 commit
(
|
||
|
|
833ac84059 |
docs(adr-117): point README + user-guide at the live PyPI releases
Both packages are now live on PyPI; bring the in-repo docs up to match. Keep both updates brief — the canonical surface documentation lives on the PyPI project pages themselves. Root README (Option 4 block): - Switch the default `pip install` example to `ruview` (the brand name) and note `wifi-densepose` is equivalent. - Add live PyPI version badges for both packages. docs/user-guide.md (§Python wheel): - Replace the single-install example with a table showing both PyPI projects and their import names so users see the choice immediately. - Add three short usage snippets (vitals, live sensing-server WS, HA-MIND semantic-primitive MQTT listener) so the guide doubles as a "what does this thing do?" reference for someone landing via pip. - Note the cibuildwheel matrix for multi-arch wheels. - Add the `pytest tests/` + `pytest bench/` source-build verify steps. No code or test changes. Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md Refs: #786 Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
0bffe27288 |
feat(adr-117): pip wifi-densepose modernization (PIP-PHOENIX) + ruview sibling release (#786)
* docs(adr-117): seed branch — ADR-117 pip-modernization spec + soul-signature research bundle
Two artifacts landing together on this new branch as the prerequisite
documentation for the v2.0.0 Python wheel modernization work:
1. **docs/adr/ADR-117-pip-wifi-densepose-modernization.md** (644 lines)
— Plan to bring the 2025-published `wifi-densepose` PyPI package
(last release v1.1.0, 2025-06-07, 11.5 months out of sync) up to
the current Rust v2/ workspace SOTA. Recommends PyO3 + maturin
with abi3-py310 (one binary covers Python 3.10–3.13 per OS/arch),
first-wheel scope = core + vitals + signal crates (~5 MB), v1.99.0
tombstone + 90-day un-yank window for v1.1.0, v2.0.0 hard break.
Open questions catalogued; phases P1–P6+ laid out with concrete
acceptance criteria.
2. **docs/research/soul/** (5 files, ~1,450 lines) — Soul Signature
research spec: 7-channel electromagnetic biometric fingerprint
(AETHER 128-dim + cardiac HR/HRV + cardiac waveform morphology +
respiratory pattern + gait timing + skeletal proportions +
subcarrier reflection profile), fused into one RVF graph file.
Includes 60s scanning protocol, 5-layer security model,
threat-model + mitigations, references to existing ADRs (014,
021, 024, 027, 030, 039, 079, 106, 108, 109, 110, 115). Marked
"Research Specification (Pre-Implementation)". Explicit "what
this is NOT" disclaimers preempt pseudoscience drift; every
discriminative-power claim either cites a measurement or is
marked "open research; baseline TBD".
Branch off main at HEAD; ready for /loop 10m implementation
iterations.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p1): scaffold python/ workspace — PyO3 + maturin + smoke tests (refs #785)
ADR-117 P1 — the python/ directory is now a working maturin-buildable
crate that produces the v2.x replacement for the legacy pure-Python
wifi-densepose==1.1.0 PyPI wheel.
## What lands
- `python/Cargo.toml` — PyO3 0.22 with `extension-module` + `abi3-py310`
(one binary covers Python 3.10–3.13 per OS/arch — keeps the
cibuildwheel matrix to 5 wheels per release, not 20). Depends on
`wifi-densepose-core` from the existing v2/ workspace via relative
path.
- `python/pyproject.toml` — maturin>=1.7 build backend with
`python-source = "python"` and `module-name = "wifi_densepose._native"`
so the compiled module loads as an internal underscore-private
submodule of the user-facing `wifi_densepose` package. PEP 621
metadata + classifiers + project URLs. Optional-deps:
`wifi-densepose[client]` for the P4 WS/MQTT pure-Python layer,
`wifi-densepose[dev]` for the test toolchain (pytest, ruff, mypy).
- `python/src/lib.rs` — minimal `#[pymodule] wifi_densepose_native`
exporting `__rust_version__`, `__rust_build_tag__`,
`__build_features__`, and a `hello()` smoke function. P2 will land
the core type bindings here.
- `python/wifi_densepose/__init__.py` — pure-Python facade re-exporting
the compiled module's symbols under their stable user-facing names.
Docstring teaches the v1→v2 migration story up-front.
- `python/wifi_densepose/py.typed` — PEP 561 marker so `mypy --strict`
in user code treats the wheel as fully typed (real stubs land in P2).
- `python/tests/test_smoke.py` — 6 P1 acceptance tests:
1. package imports without error
2. version string is PEP 440-compliant
3. `__rust_version__` is reachable from Python (the diagnostic
surface ADR-117 §5.2 promised)
4. `__build_features__` lists `p1-scaffold` marker
5. `wifi_densepose.hello()` returns "ok" (FFI round-trip)
6. `wifi_densepose._native` is reachable but the leading underscore
conveys "private; users should import the parent package"
- `python/README.md` — phase ledger, local build instructions
(`maturin develop`), layout diagram.
## What's deferred to P2+
- Core type bindings (`CsiFrame`, `Keypoint`, `PoseEstimate`) — P2
- Vitals + signal DSP bindings + witness v2 — P3
- Pure-Python WS/MQTT client layer (`wifi_densepose[client]`) — P4
- cibuildwheel + PyPI publish — P5
- v1.99.0 tombstone — concurrent with P5
The new `python/` crate is intentionally OUTSIDE the v2/ Cargo
workspace — it has its own Cargo.toml with `[package]` not
`[workspace.package]` inheritance — to keep maturin's `python-source`
+ `module-name` config self-contained and to avoid forcing every
`cargo test --workspace` invocation in v2/ to compile pyo3.
Refs ADR-117 §5 (Detailed design) and §6 (Phased migration).
Refs #785 (tracking issue).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p1): standalone Cargo.toml + python-source=. + #[pyo3(name=_native)] (P1 GREEN)
Three fixes to make maturin develop actually work locally:
1. `python/Cargo.toml` removed `*.workspace = true` inheritance —
the python/ crate is intentionally outside the v2/ workspace
(ADR-117 §5.2) so it needs every `[package]` field local.
2. `python/pyproject.toml` `python-source = "python"` was wrong
because pyproject.toml lives at python/ — maturin was looking for
python/python/. Changed to `python-source = "."` so the
`wifi_densepose/` package directory sibling-to-pyproject is found.
3. `python/src/lib.rs` `#[pymodule] fn wifi_densepose_native` →
`#[pymodule] #[pyo3(name = "_native")] fn wifi_densepose_native`.
PyO3 generates `PyInit__native` from the pyo3-name attribute, which
must match the `module-name` in pyproject.toml's [tool.maturin]
block ("wifi_densepose._native"). Without this attribute the wheel
builds but `import wifi_densepose._native` fails with
ModuleNotFoundError.
## Local validation (P1 acceptance gate)
```
$ python -m venv .venv && .venv/Scripts/python -m pip install maturin pytest
$ VIRTUAL_ENV=… maturin develop --release
…
Finished `release` profile [optimized] target(s)
📦 Built wheel for abi3 Python ≥ 3.10
🛠 Installed wifi-densepose-2.0.0a1
$ .venv/Scripts/python -c 'import wifi_densepose; print(wifi_densepose.__version__, wifi_densepose.__rust_version__, wifi_densepose.hello())'
2.0.0a1 2.0.0-alpha.1 ok
$ .venv/Scripts/python -m pytest tests/ -v
tests/test_smoke.py::test_package_imports PASSED
tests/test_smoke.py::test_version_string_well_formed PASSED
tests/test_smoke.py::test_rust_version_surfaced PASSED
tests/test_smoke.py::test_build_features_listed PASSED
tests/test_smoke.py::test_hello_returns_ok PASSED
tests/test_smoke.py::test_native_module_private PASSED
======================== 6 passed in 0.05s =========================
```
P1 closed. Moving to P2 (core type bindings).
Refs #785, ADR-117 §6.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p2): Keypoint + KeypointType bindings — 23 new tests (29/29 GREEN)
Lands the first chunk of P2: PyO3 bindings for `Keypoint` and
`KeypointType` from `wifi_densepose_core`. Bound types surface to
Python as `wifi_densepose.Keypoint` / `wifi_densepose.KeypointType`.
## Design choices that affect the API surface
1. **`Confidence` is NOT bound as a separate class.** Users hate
wrapping a float in a constructor. Python-side, confidence is just
a `float in [0.0, 1.0]`; the binding validates on construction
(`ValueError` for out-of-range, matching the Rust core error).
2. **`KeypointType` is a `#[pyclass(eq, eq_int, hash, frozen)]` enum**
— hashable so users can drop it into dicts/sets (the most common
pattern in pose-analysis notebooks: `keypoints_by_type[k.type] = k`).
3. **`Keypoint.__init__` keyword-only `z`** so 2D users don't have to
write `None` and 3D users get a clear named arg:
`Keypoint(KeypointType.LeftWrist, 0.2, 0.4, 0.8, z=0.1)`.
4. **`Keypoint` is `#[pyclass(frozen)]`** — no in-place mutation. The
Rust core type is immutable through Copy + Hash + Eq, and exposing
setters from Python would create a copy-vs-reference inconsistency
between languages.
## Files
- `python/src/bindings/keypoint.rs` — 220 lines of `#[pymethods]`
wrappers + Rust↔Python enum round-trip
- `python/src/lib.rs` — `mod bindings { pub mod keypoint; }` +
`bindings::keypoint::register(m)?` call from `#[pymodule]`
- `python/wifi_densepose/__init__.py` — re-exports `Keypoint` and
`KeypointType` at the package root
- `python/tests/test_keypoint.py` — 23 tests covering:
- 17-element COCO ordering of `KeypointType.all()`
- index→type mapping for every variant
- snake_name matches COCO spec
- `is_face()` / `is_upper_body()` predicates
- hashability (the bug I caught when I added the set-based face
test — fixed by adding `hash` to the `#[pyclass]` attribute)
- 2D + 3D constructor variants
- position_2d / position_3d tuples
- is_visible threshold
- confidence validation (Err on out-of-range)
- distance_to (2D Euclidean, 3D Euclidean, fallback when one is 2D
and the other is 3D)
- __repr__ + __eq__
- the new `p2-keypoint-bindings` feature marker landed
## Local validation
\`\`\`
$ cd python && .venv/Scripts/python -m pytest tests/ -v
tests/test_smoke.py::test_package_imports PASSED
tests/test_smoke.py::test_version_string_well_formed PASSED
tests/test_smoke.py::test_rust_version_surfaced PASSED
tests/test_smoke.py::test_build_features_listed PASSED
tests/test_smoke.py::test_hello_returns_ok PASSED
tests/test_smoke.py::test_native_module_private PASSED
tests/test_keypoint.py::test_keypoint_type_all_returns_17 PASSED
…
======================== 29 passed in 0.06s =========================
\`\`\`
Wheel size after both bindings: still well under the 5 MB ADR §5.4
budget (release build with --strip on Windows: ~340 KB).
Also adds `python/.gitignore` to prevent the `.venv/` + `target/` +
`_native.abi3.pyd` artifacts from getting committed.
## What's left in P2
CsiFrame + PoseEstimate bindings land in the next iteration. They're
larger (CsiFrame has the subcarrier buffer; PoseEstimate has
17×Keypoint + BoundingBox + track_id + score). Pattern is now proven
so they go faster.
Refs #785, ADR-117 §6.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p2): BoundingBox + PersonPose + PoseEstimate — P2 COMPLETE (57/57 tests GREEN)
Lands the second + third chunks of P2: PyO3 bindings for `BoundingBox`,
`PersonPose`, `PoseEstimate` from `wifi_densepose_core`. Combined with
the prior Keypoint + KeypointType bindings (
|
||
|
|
4bbb004f2d |
docs(readme): tighten ADR-079 caveat + drop What's-new callout
Tighten the ADR-079 camera-supervised limitation line and remove the prominent iter-50 'What's new (2026-05-23)' callout block — both preferred local edits. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
62af91beb1 |
docs(readme): add 'What's new (2026-05-23)' callout for ADR-110 + ADR-115
Iter 50 — both ADRs merged today (PR #764 + PR #778). README's beta-software warning block was the natural location for a release callout above the main pitch; users hitting the README see today's shipped work first. Two-bullet block: - ADR-110 ESP32-C6 firmware substrate at v0.7.0-esp32 with the headline measured numbers (99.56 % match / 104 µs stdev / 3.95x EMA suppression) and the host-side surface (decoders + REST + Prometheus + WebSocket). - ADR-115 HA+Matter integration with the entity-count / blueprint / Lovelace count and the privacy-mode architectural win. Both link to their ADRs + PRs so reviewers can follow back. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
249d6c327f |
ADR-115: Home Assistant + Matter integration (#778)
Closes ADR-115's MQTT track (HA-DISCO + HA-MIND + HA-FABRIC scaffolding). Headline: - 21 entity kinds per node (11 raw + 10 semantic primitives) - MQTT auto-discovery with HA conventions - Matter Bridge scaffolding (SDK wiring deferred to v0.7.1 per ADR §9.10) - Privacy mode strips biometrics at the wire, semantic primitives keep working - 420+ lib tests, mosquitto-backed integration tests, property-based fuzzing - 8 starter HA Blueprints + 3 Lovelace dashboards shipped Tracking issue: #776 |
||
|
|
00a234eda8 |
ADR-110: ESP32-C6 firmware extension (#764)
Closes the firmware-side ADR-110 design at v0.7.0-esp32 after a 38-iter /loop SOTA sprint. Headline (bench, COM9+COM12 ESP32-C6): - 99.56% cross-board RX, 104.1 µs smoothed offset stdev (≤100 µs §2.4 target met) - 3.95× EMA suppression, 1.4 ppm crystal skew preserved 4 firmware releases: v0.6.7 / v0.6.8 / v0.6.9 / v0.7.0-esp32. 42 ADR-110 unit tests, 1761 v2 workspace tests, full Firmware CI + QEMU green. |
||
|
|
423dc9fd5c |
docs(readme): add Cognitum creator affiliate program reference
Brief callout for TikTok/Instagram/YouTube creators — 25% commission, instant click-tracking, ~24h manual review. Links to cognitum.one/affiliate. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
68abb385ae |
docs(readme): swap hero image to ruview-seed.png (#753)
Replaces assets/ruview-small-gemini.jpg with assets/ruview-seed.png as the hero image. Same Cognitum Seed link target. |
||
|
|
e00cee6146 |
docs(readme): add Cognitum Seed image after hero — links to cognitum.one/seed
Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
5dcafc9c37 |
Update README.md
https://cognitum.one/seed |
||
|
|
bdd1efeb03 |
Update README.md
🌿 GH-header
Cognitum.One/RuView
|
||
|
|
dc865c236e |
docs(readme): add 10M+ downloads badge (#655)
Adds a 'downloads 10M+' badge to the existing shields.io row, linking to the Edge Module Catalog section (where the cog binaries / HF weights / npm + crates packages are surfaced). Uses img.shields.io/badge/downloads-10M%2B-brightgreen.svg — static, no external counter API hit per page load. |
||
|
|
96bc4b4ede |
docs(readme): refresh capability table — positive voice, current state (#654)
The previous table mixed status badges (✅ / ⚠️ / 🔬) and verbose "pending wiring / not yet released" caveat columns. Rewrites it as "What / How / Speed-or-scale" — three columns, present tense, no status column. Captures what actually shipped this week: * Presence detection now points at the trained head shipped on HF (100% validation accuracy), with the phase-variance fallback reframed as a no-model option rather than a "loader pending" caveat. * 17-keypoint pose is its own row now — cog-pose-estimation v0.0.1 binaries on GCS, 8.4 ms cold-start on Pi 5, train-your-own in 2.1 s on RTX 5080. References ADR-101 + the benchmark log. * Multi-person counting drops the "Heuristic, not learned" framing. The adaptive P95 normalisation from PR #491 is in tree, the runtime dedup-factor knob is documented, and the six learned drop-in counters from the Cog catalog are linked: occupancy-zones, elevator-count, queue-length, customer-flow, clean-room, person-matching. * Edge intelligence row now points at the 105-cog catalog (ADR-102) instead of just the Cognitum Seed hardware. * Camera-supervised fine-tune row reflects the actual measured training time (2.1 s on RTX 5080 for 400 epochs) instead of the laptop estimate. * Drops the status-legend footer (no more ✅/⚠️/🔬 column to legend). Replaces it with a pointer down to the Edge Module Catalog. The ESP32 + Cognitum Seed deployment-options row gets the same treatment: cleaner list of what's included, no "Pose pending weights" parenthetical (the cog ships today). Net effect: same information, present tense, positive voice. Nothing removed beyond status badges + pending-work parentheticals; all genuine engineering details (e.g. "needs ~30 s ambient calibration" for the fallback) are preserved inline. |
||
|
|
feda871e02 |
docs(readme): drop the two Edge Intelligence collapsibles from the home page (#653)
Removes both: * 🧩 Edge Intelligence (ADR-041) — 60 WASM modules across 13 categories * 🧩 Edge Intelligence — All 65 Modules Implemented (ADR-041 complete) …and the 172 lines between them. The 60-module catalog narrative duplicated content already documented in: * The new 105-cog Edge Module Catalog collapsible (PR #648, ADR-102) — same purpose, sourced live from cognitum-apps/app-registry.json instead of hand-curated. * docs/edge-modules/* — per-category guides linked from the catalog. * ADR-041 itself. The home page now reads cleaner — one canonical "what modules exist" section (the live catalog) instead of three overlapping ones. |
||
|
|
43ac76a17f |
docs(readme): rewrite hero paragraph in plain language (#652)
The previous version listed every artifact format, every pending integration, and every not-yet-released model — useful as a status log but not as a what-this-system-does sentence for a first-time reader. Replaces it with a single paragraph that answers: - What does it do? (turn WiFi into a contactless sensor) - What hardware? ($9 ESP32) - What does it tell you? (who's there, breathing, heart rate) - How small is the model? (8 KB q4 fits anywhere) - What does it NOT need? (no cameras / wearables / phone apps) Everything that got removed — pending wiring, JSONL-vs-binary RVF, the 17-keypoint pose follow-up, the heuristic-fallback caveat — is already covered in dedicated sections later in the README (the Capability table, the Pretrained Model section, the Edge Module Catalog) and in #509 / ADR-079. The hero paragraph isn't the right place for the engineering caveat tour. |
||
|
|
d67d9872c1 |
feat(pages): deploy three.js demos to gh-pages/three.js/ (#649)
Adds a new GitHub Pages workflow that publishes the ADR-097 three.js demo gallery alongside the existing observatory/, pose-fusion/, pointcloud/, and nvsim/ deployments. Uses keep_files: true so the other deployments are preserved. What ships: * `examples/three.js/index.html` — new landing page that lists all 5 demos with screenshots, "standalone" vs "needs FBX" badges, and an honest note explaining the Mixamo X Bot.fbx license boundary (demos 04 and 05 need a local download from mixamo.com; demos 01-03 run standalone in any modern browser). * `.github/workflows/threejs-pages.yml` — staged copy of demos/, screenshots/, README.md, and the new index.html into `_site/three.js/`. Drops an `assets/README.txt` placeholder explaining the FBX-not-shipped policy. Triggered on changes to examples/three.js/** or the workflow itself. * README.md — adds the live link to the existing demo row (`▶ three.js Demos (5)`) plus a one-line callout describing the gallery and the FBX caveat. After this PR merges, the workflow runs and publishes: https://ruvnet.github.io/RuView/three.js/ |
||
|
|
67fec45e61 |
feat(edge-registry): ADR-102 — surface Cognitum cog catalog via /api/v1/edge/registry (#648)
* feat(edge-registry): ADR-102 — surface Cognitum cog catalog via /api/v1/edge/registry Adds a new sensing-server endpoint that fetches and caches the canonical Cognitum app registry at https://storage.googleapis.com/cognitum-apps/app-registry.json (105 cogs across 11 categories as of v2.1.0). RuView previously had no live awareness of the catalog — the README's capability table was hand- curated and went stale as Cognitum shipped new cogs (the registry was last updated 6 days ago). ADR: * docs/adr/ADR-102-edge-module-registry.md — full design, response shape, configuration flags, failure modes, and a 12-row security review covering SSRF, response inflation, ?refresh abuse, stale-serve semantics, TLS, cache poisoning, JSON-panic resistance, etc. Code: * v2/.../edge_registry.rs — EdgeRegistry struct + UreqFetcher + MockFetcher trait + 7 unit tests. RwLock<Option<CachedEntry>> with stale-on-error fallback. MAX_PAYLOAD_BYTES=8 MiB, 10s wire timeout. * v2/.../main.rs — constructs Option<Arc<EdgeRegistry>> at startup, registers GET /api/v1/edge/registry handler, wires Extension layer. Handler runs the blocking ureq fetch via tokio::task::spawn_blocking so the async runtime stays free. * v2/.../cli.rs / main.rs Args — three new flags (per user request to "allow the registry to be disabled or changed"): --edge-registry-url <URL> (env RUVIEW_EDGE_REGISTRY_URL) --edge-registry-ttl-secs <N> (env RUVIEW_EDGE_REGISTRY_TTL_SECS) --no-edge-registry (env RUVIEW_NO_EDGE_REGISTRY) When --no-edge-registry is set or the URL is empty, the endpoint returns 404. Cargo.toml: adds ureq (rustls), sha2, thiserror as direct deps. README: * New collapsed "🧩 Edge Module Catalog" section with the full 105-cog table generated from the registry, grouped by category with practical one-line descriptions (e.g. "Spots irregular heartbeats and abnormal heart rhythms", "Detects walking problems and scores fall risk"). Links to https://seed.cognitum.one/store and the local appliance /cogs page. Sits between the HF model section and How It Works. Tests (7/7 pass): first_call_hits_upstream_and_caches ttl_expiry_triggers_refetch force_refresh_bypasses_fresh_cache stale_serve_on_upstream_failure_after_cached_success no_cache_no_upstream_returns_error upstream_invalid_json_is_treated_as_error upstream_sha256_is_deterministic Security highlights (full review in ADR-102 §"Security review"): - The registry is metadata-only; per-cog binary signatures (ADR-100) remain the trust root for installs. A compromised registry can mislead a human reader but cannot ship malicious binaries. - 8 MiB cap + 10s timeout + Option<Arc<...>> via Extension layer means the endpoint can't be used to exhaust memory or pin tokio threads. - Stale-on-error responses carry an explicit `stale: true` field so upstream outages are visible to consumers rather than silently masked. - Endpoint sits behind the existing RUVIEW_API_TOKEN bearer gate when set, otherwise unauthenticated (registry contents are public anyway). * chore: refresh Cargo.lock for ureq/sha2/thiserror deps added by ADR-102 |
||
|
|
ad15f1b049 |
docs: truth-up README + user-guide on Hugging Face model release (#637)
The previous wording in both README.md and docs/user-guide.md claimed
no pretrained weights were released yet. That was wrong — the
contrastive CSI encoder + presence-detection head + per-node LoRA
adapters have been published as
ruvnet/wifi-densepose-pretrained on Hugging Face for several weeks
(124 downloads at time of writing), with 100% presence accuracy on
the validation set and 164,183 emb/s on M4 Pro.
This commit replaces the "no shipped weights" framing with the actual
state, and surfaces a real loader gap discovered during a
before/after benchmark of the sensing-server:
* Baseline run (no --model): server produced presence/motion/vitals
output at ~19 ticks/s, as expected.
* After run (--model models/wifi-densepose-pretrained.rvf): the
progressive RVF loader errored with
"invalid magic at offset 0: expected 0x52564653, got 0x7974227B"
(0x7974227B is the ASCII bytes {"ty… from the JSONL header).
v2/.../rvf_container.rs only parses the binary RVF segment
format; the HF artifact is JSONL RVF. When the load fails the
pipeline degraded to null output (variance=0, presence=None) rather
than falling back to heuristic mode.
The docs now describe (a) what works today — Python / training-side
consumption of model.safetensors — and (b) what is gated on a JSONL
adapter or a binary-RVF republish — sensing-server --model loading.
The 17-keypoint pose model remains separately pending (#509,
ADR-079 phases P7–P9).
|
||
|
|
8247d28d90 |
docs(README): truth-up capability table — separate shipped/heuristic/pending (#568 follow-up) (#635)
@xiaofuchen's audit in #568 was technically correct: the project page claimed capabilities (\"Pose estimation\", \"Presence sensing — trained model + PIR fusion — 100% accuracy\") that aren't what the code actually does. PR #573 fixed this in the firmware README; this commit applies the same truth-up to the main repo README so first-time visitors get an honest picture. Specific changes: 1. **Hero paragraph (line 35)** — was \"RuView also supports pose estimation (17 COCO keypoints …)\" with no caveat. Now: ships the training infrastructure; pretrained weights are not yet released (links #509 and ADR-079 P7-P9 Pending). 2. **Capability table (lines 50-61)** — was a single 11-row \"What/How/ Speed\" table that mixed shipped, heuristic, and pipeline-only capabilities under the same emoji. Now a status column with a three-tier legend: - ✅ shipped + tested on hardware (breathing rate, heart rate, motion, fall detection, through-wall, edge intelligence, multi-frequency mesh) - ⚠️ ships and runs, but is a heuristic/threshold (presence indicator, multi-person slot count) — accuracy depends on calibration and signal conditions - 🔬 implementation + tests in repo, weights/data/eval pending (17-keypoint pose estimation, camera-supervised fine-tune, 3D point cloud fusion) 3. **Hardware capability column (lines 91-93)** — was \"Pose, breathing, heartbeat, motion, presence\" for the ESP32 options. Replaced with the literal list of capabilities that actually work today (presence indicator, motion, breathing, heart rate, fall detection, slot-count heuristic) with an explicit \"Pose pending weights — see #509\" qualifier. Pointing also to the v0.6.5-esp32 release-aligned firmware README that already has the firmware-side truth-up (PR #573). This is documentation only — no code change, no behaviour change. The project's capabilities haven't changed; the project page now describes them honestly. |
||
|
|
bf30844835 | Update README.md | ||
|
|
d0b64bdeb6 |
chore(rvcsi): drop inline v2/crates/rvcsi-* — consume the vendor/rvcsi submodule / crates.io instead
rvCSI now lives in its own repo (github.com/ruvnet/rvcsi), vendored here as `vendor/rvcsi` (PR #543) and published to crates.io as `rvcsi-* 0.3.x` / to npm as `@ruv/rvcsi`. The inline copies in `v2/crates/rvcsi-*` (added in #542) were a duplicate; this removes them and re-points the docs. - `git rm -r v2/crates/rvcsi-{core,dsp,events,adapter-file,adapter-nexmon,ruvector,runtime,node,cli}` - `v2/Cargo.toml`: remove the 9 from `members` (note: `vendor/rvcsi/Cargo.toml` is its own workspace — depend on the published crates or the submodule paths, not as v2 workspace members). - `CLAUDE.md`: the 9 crate-table rows collapse to one `vendor/rvcsi` row. - `README.md` docs table: rvCSI entry points at the standalone repo + notes the submodule / crates.io / npm / plugin. - `CHANGELOG.md`: `[Unreleased]` entry. The ADRs (ADR-095, ADR-096), PRD, and DDD model stay in `docs/` as the design record of the incubation. `cargo build --workspace --no-default-features` and `cargo test --workspace --no-default-features` stay green. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
d40411e6d7 |
feat(rvcsi): Raspberry Pi 5 (BCM43455c0) + Nexmon chip registry
Adds first-class support for the Raspberry Pi 5's WiFi chip (CYW43455 /
BCM43455c0 — the same 802.11ac wireless as the Pi 4 / Pi 3B+ / Pi 400, and the
chip with the most mature nexmon_csi support), plus a registry of the other
Nexmon-supported Broadcom/Cypress chips.
rvcsi-adapter-nexmon — new `chips.rs`:
- `NexmonChip` (Bcm43455c0, Bcm43436b0, Bcm4366c0, Bcm4375b1, Bcm4358, Bcm4339,
Unknown{chip_ver}) + `RaspberryPiModel` (Pi5/Pi4/Pi400/Pi3BPlus/PiZero2W/
PiZeroW) — Pi5/Pi4/Pi400/Pi3B+ → Bcm43455c0; PiZero2W → Bcm43436b0.
- `nexmon_adapter_profile(chip)` / `raspberry_pi_profile(model)` build the
per-device `AdapterProfile` (channels: 2.4 GHz 1-13 + 5 GHz UNII for dual-band;
bandwidths 20/40/80[/160]; expected subcarrier counts 64/128/256[/512]) that
`validate_frame` bounds CSI frames against.
- `NexmonChip::from_chip_ver` (0x4345 → Bcm43455c0, 0x4339, 0x4358, 0x4366,
0x4375 — best-effort; the raw `chip_ver` is always preserved) and `from_slug`
/ `RaspberryPiModel::from_slug` ("pi5", "raspberry pi 4", "bcm43455c0", ...).
- `NexmonCsiHeader::chip()`; `NexmonPcapAdapter` auto-detects the chip from the
packets' `chip_ver` and uses the matching profile, overridable via
`.with_chip(NexmonChip)` / `.with_pi_model(RaspberryPiModel)`; `.detected_chip()`.
rvcsi-runtime: `decode_nexmon_pcap_for(.., chip_spec)` (validate against a chip /
Pi model, drop non-conforming) + `nexmon_profile_for(spec)`; `NexmonPcapSummary`
gains `chip_names` + `detected_chip`; `CaptureSummary` gains `chip`.
rvcsi-cli: `record --source nexmon-pcap --chip pi5`; new `nexmon-chips`
subcommand (lists chips + Pi models, human or `--json`); `inspect-nexmon` and
`inspect` now print the resolved chip.
rvcsi-node (napi-rs): `nexmonDecodePcap` gains an optional `chip` arg;
`nexmonChipName(chipVer)`, `nexmonProfile(spec)`, `nexmonChips()`. @ruv/rvcsi
SDK + `.d.ts` updated (AdapterProfile / NexmonChipsListing interfaces, the new
fns, `chip` on CaptureSummary, `chip_names`/`detected_chip` on NexmonPcapSummary).
168 rvcsi tests pass (adapter-nexmon 22→28, cli 9→10), 0 failures, clippy-clean.
The synthetic test captures now stamp chip_ver = 0x4345 (the BCM4345 family chip
ID), so the chip-detection happy path is exercised end to end.
ADR-096, CHANGELOG, README, CLAUDE.md updated.
https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z
|
||
|
|
684a064816 |
docs(rvcsi): update CHANGELOG, CLAUDE.md crate table, README docs index
- CHANGELOG: expand the rvCSI entry to cover all 9 crates (incl. rvcsi-runtime and the @ruv/rvcsi npm SDK), the napi-c / napi-rs seams, and the 142-test / clippy-clean status; note the daemon + MCP server are follow-ups. - CLAUDE.md: add the 9 `rvcsi-*` crates to the Key Rust Crates table. - README: add an rvCSI row to the docs index; bump the ADR count (79→96) and DDD-model count (7→8). https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z |
||
|
|
bd4f81749a |
fix(docs): correct unsubstantiated 92.9% PCK@20 camera-supervised claim (#535)
The README claimed "92.9% PCK@20" for camera-supervised pose training. That
figure appears nowhere in ADR-079 (the source ADR) and is ~2.6x 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 all
still `Pending`, so no measured camera-supervised PCK@20 has been published.
- README: replace the two "92.9% PCK@20" claims with the proxy-supervised
baseline (~2.5%) and the ADR-079 target (35%+), noting the eval phases are
pending.
- CHANGELOG: add an Unreleased entry.
Surfaced by the PowerPlatePulse training-pipeline audit (2026-05-11). Six other
audit findings (vitals features absent from training; wifi-densepose-signal
ghost dep; PIR/BME280 in MODEL_CARD unimplemented; proof.rs uses
SyntheticCsiDataset only; 56-subcarrier/1-NIC default; multi-band 168-subcarrier
mesh not in training config) are listed in the PR body for follow-up.
|
||
|
|
df9d3b0eea |
fix(plugins): move marketplace manifest to repo root for /plugin marketplace add ruvnet/RuView
Claude Code looks for `.claude-plugin/marketplace.json` at the cloned repo's ROOT — not in a subdirectory — so `/plugin marketplace add ruvnet/RuView` (and `claude plugin marketplace add ruvnet/RuView`) was failing with "Marketplace file not found". - Move `plugins/.claude-plugin/marketplace.json` → `.claude-plugin/marketplace.json` (repo root); the `ruview` plugin's `source` is now `./plugins/ruview`. - README.md / plugins/ruview/README.md: install instructions now use `/plugin marketplace add ruvnet/RuView` + `/plugin install ruview@ruview` (with `claude --plugin-dir ./plugins/ruview` as the no-install fallback); manifest path references updated. - plugins/ruview/scripts/smoke.sh: resolve the manifest at the repo root; also assert the plugin `source` is `./plugins/ruview`. - ADR-0001 updated (scope, directory contract, smoke contract, consequences). Verified: `claude plugin validate .` + `./plugins/ruview` pass; smoke 13/13; `claude plugin marketplace add ./` → `claude plugin install ruview@ruview` → `claude plugin details ruview` works end-to-end (16 skill-entries + 3 agents). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
298543913e |
docs(readme): add Claude Code / Codex plugin + marketplace install instructions
New "🧩 Claude Code & Codex Plugin" section in README.md covering `claude --plugin-dir`, `claude plugin marketplace add` / `install`, the seven /ruview-* commands, the Codex prompt mirror, and the smoke check; plus a Documentation-table row linking to plugins/ruview/README.md. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
e7904786f0 |
Update README.md
Added Spatial Intelligence to readme, since that seems to be a common description |
||
|
|
7343bdc4dd |
docs(readme): retarget Live 3D Point Cloud link to hosted demo
Now that ADR-094 is deployed, point the README's demo link at https://ruvnet.github.io/RuView/pointcloud/ instead of the docs/readme-details.md anchor. Matches the pattern of the sibling Observatory and Pose Fusion demo links. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
e11d569a39 |
docs(readme): split details to docs/readme-details.md and reorganize
- Move Latest Additions, Key Features, and everything from Installation through Changelog (1855 lines) into docs/readme-details.md. - Keep README focused on overview, capability table, How It Works, Use Cases, Documentation, License, and Support. - Add per-row emojis to the top capability table. - Add 3D point cloud row noting optional camera + WiFi CSI + mmWave fusion with link to the live viewer demo. - Move Documentation table closer to the bottom (just above License). - Collapse Edge Intelligence (ADR-041) into a <details> block matching the sibling Use Case sections. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
81cc241b9e |
chore(repo): move v1/ → archive/v1/ + add archive/README.md (#430)
The Rust port at v2/ has been the primary codebase since the rename in #427. The Python implementation at v1/ is no longer the active target; the only load-bearing path is the deterministic proof bundle at v1/data/proof/ (per ADR-011 / ADR-028 witness verification). Move the whole Python tree into archive/v1/ and document the policy in archive/README.md: no new features, bug fixes only when they affect a still-load-bearing path (currently just the proof), CI continues to verify the proof on every push and PR. Path references updated in 26 files via path-pattern sed (only matches v1/<known-child> patterns, never bare v1 or API URLs like /api/v1/). Two double-prefix typos (archive/archive/v1/) caught and hand-fixed in verify-pipeline.yml and ADR-011. Validated: - Python proof verify.py imports cleanly at archive/v1/data/proof/ (numpy/scipy still required; CI installs requirements-lock.txt from archive/v1/ now) - cargo test --workspace --no-default-features → 1,539 passed, 0 failed, 8 ignored (unaffected by Python tree relocation) - ESP32-S3 on COM7 untouched (no firmware paths changed) After-merge: contributors should re-run any local `python v1/...` commands as `python archive/v1/...` (CLAUDE.md and CHANGELOG already updated). |
||
|
|
f49c722764 |
chore(repo): rename rust-port/wifi-densepose-rs → v2/ (flatten to one level) (#427)
The Rust port lived two directories deep (rust-port/wifi-densepose-rs/) without any sibling under rust-port/ that warranted the extra level. Move the whole workspace up to v2/ to match v1/ (Python) at the same depth and shorten every cd / build command across the repo. git mv preserves history for all tracked files. 60 files updated for path references (CI workflows, ADRs, docs, scripts, READMEs, internal .claude-flow state). Two manual fixes for relative-cd paths in CLAUDE.md and ADR-043 that became wrong after the depth change (cd ../.. → cd ..). Validated: - cargo check --workspace --no-default-features → clean (after target/ nuke; the gitignored target/ was carried by the OS rename and had hard-coded old paths in build scripts) - cargo test --workspace --no-default-features → 1,539 passed, 0 failed, 8 ignored (same totals as pre-rename) - ESP32-S3 on COM7 → still streaming live CSI (cb #40300, RSSI -64 dBm) After-merge follow-up: contributors should `rm -rf v2/target` once and let cargo regenerate from the new path. |
||
|
|
0943a32248 |
feat: Real-time dense point cloud from camera + WiFi CSI (#405)
* Add wifi-densepose-pointcloud: real-time dense point cloud from camera + WiFi CSI
New crate with 5 modules:
- depth: monocular depth estimation + 3D backprojection (ONNX-ready, synthetic fallback)
- pointcloud: Point3D/ColorPoint types, PLY export, Gaussian splat conversion
- fusion: WiFi occupancy volume → point cloud + multi-modal voxel fusion
- stream: HTTP + Three.js viewer server (Axum, port 9880)
- main: CLI with serve/capture/demo subcommands
Demo output: 271 WiFi points + 19,200 depth points → 4,886 fused → 1,718 Gaussian splats.
Serves interactive 3D viewer at http://localhost:9880 with Three.js orbit controls.
ADR-SYS-0021 documents the architecture for camera + WiFi CSI dense point cloud pipeline.
Co-Authored-By: claude-flow <ruv@ruv.net>
* Optimize pointcloud: larger splat voxels, smaller responses, faster fusion
- Gaussian splat voxel size: 0.10 → 0.15 (42% fewer splats: 1718 → 994)
- Splat response: 399 KB → 225 KB (44% smaller)
- Pipeline: 22.2ms mean (100 runs, σ=0.3ms)
- Cloud API: 1.11ms avg, 905 req/s
- Splats API: 1.39ms avg, 719 req/s
- Binary: 1.0 MB arm64 (Mac Mini), tested
Co-Authored-By: claude-flow <ruv@ruv.net>
* Complete implementation: camera capture, WiFi CSI receiver, training pipeline
Three new modules added to wifi-densepose-pointcloud:
1. camera.rs — Cross-platform camera capture
- macOS: AVFoundation via Swift, ffmpeg avfoundation
- Linux: V4L2, ffmpeg v4l2
- Camera detection, listing, frame capture to RGB
- Graceful fallback to synthetic data when no camera
2. csi.rs — WiFi CSI receiver for ESP32 nodes
- UDP listener for CSI JSON frames from ESP32
- Per-link attenuation tracking with EMA smoothing
- Simplified RF tomography (backprojection to occupancy grid)
- Test frame sender for development without hardware
- Ready for real ESP32 CSI data from ruvzen
3. training.rs — Calibration and training pipeline
- Depth calibration: grid search over scale/offset/gamma
- Occupancy training: threshold optimization for presence detection
- Ground truth reference points for depth RMSE measurement
- Preference pair export (JSONL) for DPO training on ruOS brain
- Brain integration: submit observations as memories
- Persistent calibration files (JSON)
New CLI commands:
ruview-pointcloud cameras # list available cameras
ruview-pointcloud train # run calibration + training
ruview-pointcloud csi-test # send test CSI frames
ruview-pointcloud serve --csi # serve with live CSI input
All tested: demo, training (10 samples, 4 reference points, 3 pairs),
CSI receiver (50 test frames), server API.
Co-Authored-By: claude-flow <ruv@ruv.net>
* Fix viewer: replace WebSocket with fetch polling
Co-Authored-By: claude-flow <ruv@ruv.net>
* Wire live camera into server — real-time updating point cloud
- Server captures from /dev/video0 at 2fps via ffmpeg
- Background tokio task refreshes cloud + splats every 500ms
- Viewer polls /api/splats every 500ms, only updates on new frame
- Shows 🟢 LIVE / 🔴 DEMO indicator
- Camera position set for first-person view (looking forward into scene)
- Downsample 4x for performance (19,200 points per frame)
- Graceful fallback to demo data if camera capture fails
Co-Authored-By: claude-flow <ruv@ruv.net>
* Add MiDaS GPU depth, serial CSI reader, full sensor fusion
- MiDaS depth server: PyTorch on CUDA, real monocular depth estimation
- Rust server calls MiDaS via HTTP for neural depth (falls back to luminance)
- Serial CSI reader for ESP32 with motion detection + presence estimation
- CSI disabled by default (RUVIEW_CSI=1 to enable) — serial reader needs baud config
- Edge-enhanced depth for better object boundaries
- All sensors wired: camera, ESP32 CSI, mmWave (CSI gated until serial fixed)
Co-Authored-By: claude-flow <ruv@ruv.net>
* Complete 7-component sensor fusion pipeline (all working)
1. ADR-018 binary parser — decodes ESP32 CSI UDP frames, extracts I/Q subcarriers
2. WiFlow pose — 17 COCO keypoints from CSI (186K param model loaded)
3. Camera depth — MiDaS on CUDA + luminance fallback
4. Sensor fusion — camera depth + CSI occupancy grid + skeleton overlay
5. RF tomography — ISTA-inspired backprojection from per-node RSSI
6. Vital signs — breathing rate from CSI phase analysis
7. Motion-adaptive — skip expensive depth when CSI shows no motion
Live results: 510 CSI frames/session, 17 keypoints, 26% motion, 40 BPM breathing.
Both ESP32 nodes provisioned to send CSI to 192.168.1.123:3333.
Magic number fix: supports both 0xC5110001 (v1) and 0xC5110006 (v6) frames.
Co-Authored-By: claude-flow <ruv@ruv.net>
* Add brain bridge — sparse spatial observation sync every 60s
Stores room scan summaries, motion events, and vital signs
in the ruOS brain as memories. Only syncs every 120 frames
(~60 seconds) to keep the brain sparse and optimized.
Categories: spatial-observation, spatial-motion, spatial-vitals.
Co-Authored-By: claude-flow <ruv@ruv.net>
* Update README + user guide with dense point cloud features
Added pointcloud section to README (quick start, CLI, performance).
Added comprehensive user guide section: setup, sensors, commands,
pipeline components, API endpoints, training, output formats,
deep room scan, ESP32 provisioning.
Co-Authored-By: claude-flow <ruv@ruv.net>
* Add ruview-geo: geospatial satellite integration (11 modules, 8/8 tests)
New crate with free satellite imagery, terrain, OSM, weather, and brain integration.
Modules: types, coord, locate, cache, tiles, terrain, osm, register, fuse, brain, temporal
Tests: 8 passed (haversine, ENU roundtrip, tiles, HGT parse, registration)
Validation: real data — 43.49N 79.71W, 4 Sentinel-2 tiles, 2°C weather, brain stored
Data sources (all free, no API keys):
- EOX Sentinel-2 cloudless (10m satellite tiles)
- SRTM GL1 (30m elevation)
- Overpass API (OSM buildings/roads)
- ip-api.com (geolocation)
- Open Meteo (weather)
ADR-044 documents architecture decisions.
README.md in crate subdirectory.
Co-Authored-By: claude-flow <ruv@ruv.net>
* Update ADR-044: add Common Crawl WET, NASA FIRMS, OpenAQ, Overture Maps sources
Extended geospatial data sources leveraging ruvector's existing web_ingest
and Common Crawl support for hyperlocal context.
Co-Authored-By: claude-flow <ruv@ruv.net>
* Fix OSM/SRTM queries, add change detection + night mode
- OSM: use inclusive building filter with relation query and 25s timeout
- SRTM: switch to NASA public mirror with viewfinderpanoramas fallback
- Add detect_tile_changes() for pixel-diff satellite change detection
- Add is_night() solar-declination model for CSI-only night mode
- 6 new unit tests (night mode + tile change detection)
Co-Authored-By: claude-flow <ruv@ruv.net>
* Enhance viewer: skeleton overlay, weather, buildings, better camera
Add COCO skeleton rendering with yellow keypoint spheres and white bone
lines, info panel sections for weather/buildings/CSI rate/confidence,
overhead camera at (0,2,-4), and denser point size with sizeAttenuation.
Co-Authored-By: claude-flow <ruv@ruv.net>
* Add CSI fingerprint DB + night mode detection
Co-Authored-By: claude-flow <ruv@ruv.net>
* Fix ADR-044 numbering conflict, update geo README
Renumbered provisioning tool ADR from 044 to 050 to avoid conflict
with geospatial satellite integration ADR-044.
Co-Authored-By: claude-flow <ruv@ruv.net>
* Clean up warnings: suppress dead_code for conditional pipeline modules
Removes unused imports/variables via cargo fix and adds #[allow(dead_code)]
for modules used conditionally at runtime (CSI, depth, fusion, serial).
Pointcloud: 28 → 0 warnings. Geo: 2 → 0 warnings. 8/8 tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* Fix PR #405 blockers: async runtime panic, crate rename, path traversal, brain URL config
- brain_bridge.rs: replace `Handle::current().block_on(...)` inside async fn
with `.await` (was a guaranteed "runtime within runtime" panic). Brain URL
now read from RUVIEW_BRAIN_URL env var (default http://127.0.0.1:9876),
logged once via OnceLock.
- wifi-densepose-geo: rename Cargo package from `ruview-geo` to
`wifi-densepose-geo` to match directory and workspace conventions. Update
all use sites (tests/examples/README). Same env-var pattern for brain URL
in brain.rs + temporal.rs.
- training.rs: add sanitize_data_path() rejecting `..` components and
safe_join() that canonicalises + enforces base-dir containment on every
write (calibration.json, samples.json, preference_pairs.jsonl,
occupancy_calibration.json). Defence-in-depth check also in main.rs
before TrainingSession::new.
- osm.rs: clamp Overpass radius to MAX_RADIUS_M=5000m; return Err beyond
that. Add parse_overpass_json() that rejects malformed payloads
(missing top-level `elements` array).
Co-Authored-By: claude-flow <ruv@ruv.net>
* csi_pipeline: rename WiFlow stub to heuristic_pose_from_amplitude, decouple UDP
Blocker 3 (PR #405 review): The "WiFlow inference" path was a stub that
built a model from empty weight vectors and synthesised keypoints from
amplitude energy. Presenting this as "WiFlow inference" was misleading.
- Rename WiFlowModel to PoseModelMetadata (empty tag struct; we only care
if the on-disk file exists)
- Rename load_wiflow_model() -> detect_pose_model_metadata() and log
"amplitude-energy heuristic enabled/disabled" (no "WiFlow" claim)
- Rename estimate_pose() -> heuristic_pose_from_amplitude() with
prominent `STUB:` doc comment saying this is NOT a trained model
Blocker 4 (PR #405 review): The UDP receiver held the shared Arc<Mutex>
across a synchronous process_frame() call, starving HTTP handlers.
- Introduce a std::sync::mpsc channel between the UDP thread (which only
parses + pushes) and a dedicated processor thread (which locks only
briefly around a single process_frame). HTTP snapshots via
get_pipeline_output no longer contend with the socket read loop.
Also:
- Move ADR-018 parser to parser.rs (see next commit); csi_pipeline re-exports
- send_test_frames now uses parser::build_test_frame for synthetic frames
- Log a one-line node stats summary every 500 frames (reads every public
CsiFrame field on the runtime path)
Co-Authored-By: claude-flow <ruv@ruv.net>
* Extract ADR-018 parser into parser.rs + wire Fingerprint CLI
File-split (strong concern #9 in PR #405 review): csi_pipeline.rs was 602
LOC; extract the pure-function ADR-018 parser + synthetic frame builder
into src/parser.rs. Inline unit tests in parser.rs cover:
- 0xC5110001 (raw CSI, v1) roundtrip
- 0xC5110006 (feature state, v6) roundtrip
- wrong magic is rejected
- truncated header is rejected
- truncated payload is rejected
main.rs: expose `fingerprint NAME [--seconds N]` subcommand wiring
record_fingerprint() (this was the only caller needed to make the public
API non-dead on the runtime path). Also:
- Replace `--host/--port` + external `--csi` with a single `--bind`
defaulting to loopback (`127.0.0.1:9880`) — addresses strong concern
#7 about exposing camera/CSI/vitals by default.
- Update synthetic `csi-test` to target UDP 3333 (matching the ADR-018
listener) and use the shared parser::build_test_frame.
- Defence-in-depth: call training::sanitize_data_path on the expanded
--data-dir before TrainingSession::new does the same.
Co-Authored-By: claude-flow <ruv@ruv.net>
* stream: extract viewer HTML to viewer.html, default bind to loopback
Strong concern #7 (PR #405): default HTTP bind leaked camera/CSI/vitals
to the LAN. The `serve` fn now takes a single `bind` arg and prints a
loud WARNING when bound outside loopback.
Strong concern #10 (PR #405): embedded HTML+JS was ~220 LOC of the 418
LOC stream.rs. Moved the markup verbatim into viewer.html and inlined
via `include_str!("viewer.html")`. Also:
- Drop the #![allow(dead_code)] crate-level silencing (reviewer point
#11). Remove the now-unused AppState.csi_pipeline field.
- capture_camera_cloud_with_luminance returns the mean luminance of the
captured frame; the background loop feeds that to
CsiPipelineState::set_light_level so the night-mode flag actually
toggles at runtime (previously it could only be set from tests).
Net effect on file size: stream.rs 418 → 232 LOC.
Co-Authored-By: claude-flow <ruv@ruv.net>
* Dead-code cleanup + tests for fusion/depth/OSM/training/fingerprinting
Reviewer point #11 (PR #405): remove the `#![allow(dead_code)]`
silencing added in
|
||
|
|
55c5ddfc40 |
docs: collapse all details sections in README for cleaner view
Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
c5fef33c6a |
docs: reorder README sections — v0.7.0 first, then descending
Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
599ea61a17 |
docs: update README and user guide for v0.7.0 camera-supervised training
- Add v0.7.0 section with 92.9% PCK@20 result and new scripts - Add camera-supervised training section to user guide with step-by-step - Update release table (v0.7.0 as latest) - Update ADR count (62 → 79) - Update beta notice with camera ground-truth link Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
23b4491e7b |
docs: update HuggingFace links to ruv/ruview (primary repo)
Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
b3fd0e2951 |
docs: add HuggingFace models, 17 sensing apps, v0.6.0 to README + user guide
README: - New "Pre-Trained Models" section with HuggingFace download link - Model table (safetensors, q4, q2, presence head, LoRA adapters) - Updated benchmarks (0.008ms, 164K emb/s, 51.6% contrastive) - "17 Sensing Applications" section (health, environment, multi-freq) - v0.6.0 in release table as Latest User guide: - "Pre-Trained Models" section with quick start + huggingface-cli - What the models do (presence, fingerprinting, anomaly, activity) - Retraining instructions - "Health & Wellness Applications" section with all 4 health scripts - Medical disclaimer Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
085af0c2be |
docs: update quick start with 3 deployment options
Option 1: Docker (simulated, no hardware) Option 2: ESP32 live sensing ($9) Option 3: Full system with Cognitum Seed ($140) Also shows RF scan, SNN, and MinCut commands for v0.5.5 capabilities. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
f4e636aaa2 |
docs: refocus README introduction on WiFi sensing
WiFi sensing (presence, vitals, activity, sleep, environment) is now the primary narrative. Pose estimation repositioned as an advanced capability. Highlights: multi-frequency mesh, SNN adaptation, witness chain, Cognitum Seed integration. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
582d51aed6 |
docs: fix Cognitum Seed pricing — $131 (not $15)
Updated all BOM references: ESP32 $9 + Cognitum Seed $131 = $140 total Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
b31efe5e92 |
docs: improve README benchmarks — results-focused with context
Replace dry metric table with human-readable results that explain why each number matters. 14 benchmarks with real-world significance. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
f03b484dd1 |
docs: update README limitations — remove 2 resolved items
Removed: - "No pre-trained model weights" — weights now published (v0.5.4+) - "Multi-person counting overcounts #348" — fixed by MinCut (ADR-075) Added: - Camera-free pose accuracy limitation (2.5% PCK@20, honest about it) Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
73ce72d39c |
docs: update README with v0.5.5 capabilities and benchmarks
- New "What's New in v0.5.5" section: SNN, MinCut (#348 fix), CNN spectrogram, WiFlow, multi-frequency mesh, graph transformer - Before/after comparison table (person counting, channels, model) - 15 new script commands with usage examples - Release table updated with v0.5.5 as Latest - v0.5.4 section collapsed (not open by default) Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
b6c032d665 |
docs: add multi-frequency mesh + RF scanner to README
New capabilities: 6-channel hopping, neighbor APs as passive radar, real-time RF spectrum visualization with null/reflector/movement detection Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
73d4cb9fc2 |
docs: update README + user guide with v0.5.4 capabilities
README: - Test badge 1300+ → 1463 - Updated capability table (171K emb/s, 100% presence, 0.012ms) - Added "What's New in v0.5.4" section with full benchmark table - Training pipeline quick start commands User guide: - Camera-Free Pose Training section (10 sensor signals, 5-phase pipeline) - ruvllm Training Pipeline section (5 phases, quantization options) - Publishing to HuggingFace section - Updated table of contents Co-Authored-By: claude-flow <ruv@ruv.net> |