# Changelog

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

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- **Multistatic fusion guard interval is now operator-configurable — fixes permanent trust demotion with WiFi-synced ESP32 nodes (#1049).** Two independently-clocked ESP32-S3 boards on ESP-NOW sync drift 10–150 ms (typ. ~70 ms) — the 100 ms beacon + WiFi-MAC jitter cannot hold them within the published 60 ms default guard, so the governed-trust cycle permanently demoted to `Restricted`, suppressed all pose output, and spun the error counter to 200k+ with **no escape hatch but a container restart**. Added a **direct `WDP_GUARD_INTERVAL_US` override** (+ optional `WDP_SOFT_GUARD_US`) to `multistatic_guard_config_from_env`, so a deployment can lift the hard guard past its measured spread (e.g. `WDP_GUARD_INTERVAL_US=200000`) without having to know its exact TDM schedule. Precedence is most-specific-wins: a direct override beats the existing `WDP_TDM_SLOTS`+`WDP_TDM_SLOT_US` schedule-derived guard, which beats the 60 ms/20 ms default; the override is applied on top of whichever base is selected, the soft band is always clamped strictly below the hard guard, and a malformed/zero value is ignored (falls back to the base rather than breaking fusion). The effective guard is now logged at startup. Pinned by 6 new tests (`multistatic_guard_config_tests`): direct-override-wins / beats-TDM-derived / soft-clamped-below-hard / lowering-hard-pulls-soft-down / malformed-or-zero-falls-back / default-when-unset. `wifi-densepose-sensing-server` bin tests **449 → 455**, 0 failed; Python proof VERDICT PASS, hash unchanged (off the signal proof path).

### Security
- **`wifi-densepose-occworld-candle` — beyond-SOTA security + correctness review (Milestone #9, crate 4/4).** (1) **HIGH (MEASURED) — checkpoint-load crash on any int32 tensor** (`model.rs::safetensor_dtype_to_candle`). `safetensors::Dtype::I32` was mapped to `candle_core::DType::I64` and the raw int32 byte buffer (4 bytes/elem) was then handed to `Tensor::from_raw_buffer(.., I64, shape, ..)`. Candle derives `elem_count = data.len() / dtype.size_in_bytes()`, so the I64 path halved the element count while keeping the *original* shape — yielding a tensor whose declared shape claims twice as many elements as its backing storage holds. Reading it **panics** (`range end index 6 out of range for slice of length 3` — slice OOB inside candle-core) on any attacker-supplied or PyTorch-exported checkpoint containing an int32 tensor (common: index/buffer tensors). Fixed by mapping `I32 → DType::I32` (and `I16 → DType::I16`), both first-class candle dtypes. Reproduction recorded on old code; pinned by `tests/checkpoint_loading.rs::int32_tensor_loads_with_consistent_shape_and_values` (panics on old, passes on new) plus F32/I64/corrupt-file control cases. (2) **LOW (MEASURED) — `predict()` lacked frame/batch validation at the input boundary** (`inference.rs`). It validated H/W/D but not the externally-supplied frame count; an `f_in > num_frames*2` over-indexed the temporal positional embedding deep in the transformer and surfaced as a cryptic candle "gather" `InvalidIndex` (returned error, not a panic — candle bounds-checks), and a zero frame/batch dim fed a zero-element tensor into the pipeline. Now rejected at the boundary with a clear `ShapeMismatch`. Pinned by `predict_rejects_zero_frames` / `predict_rejects_too_many_frames` / `predict_accepts_frame_count_at_capacity`. (3) **LOW (MEASURED) — divide-by-zero panic on a degenerate input to the public `VQCodebook::encode`** (`vqvae.rs`): a rank-0 / empty-last-dim tensor made `last == 0` and panicked on `elem_count() / last`. Now fails closed with a clear error. Pinned by `encode_rejects_scalar_without_panicking`. **Dimensions confirmed CLEAN with evidence:** panic surface — zero `unwrap()`/`expect()`/`panic!`/`unreachable!` in production code paths (grep evidence; all error handling via `?`/`map_err`); NaN-state-poisoning — N/A (engine is stateless between `predict` calls, input is `u8` class indices so non-finite input is structurally impossible, no persistent world-model buffer to latch into); unbounded-alloc / shape-data mismatch from malformed weights — defended upstream by `safetensors::validate()` (overflow-checked `nelements*dtype.size()` vs declared byte range, rejected before reaching candle); secrets — none (grep clean, only `token_h`/`token_w` config fields match). `unsafe_code = forbid` in the crate manifest. **Build/validation status (MEASURED on Windows):** crate builds and tests under `cargo test -p wifi-densepose-occworld-candle --no-default-features` — **29/29 pass** (20 unit + 4 checkpoint_loading + 3 predict_honesty + 2 doc) after fixes; `cargo test --workspace --no-default-features` = 0 failed across all crates (lone `wifi-densepose-desktop` `api_integration` failure was a Windows "Access is denied (os error 5)" file-lock flake — re-ran in isolation **21/21 pass**); Python proof VERDICT PASS, hash `f8e76f21…446f7a` unchanged. *Warrants ADR slot 179 (parent to author).*
- **`wifi-densepose-wasm-edge` beyond-SOTA closing review — boundary NaN-state-poisoning guard + clean-with-evidence attestation (ADR-040 edge crate, ~70 modules).** Closing pass of the security campaign over the last untouched sizeable crate. **One real finding fixed (LOW / source-analysis + reproduced):** the two WASM↔host frame boundaries (`lib.rs::on_frame`/`on_timer` and `bin/ghost_hunter.rs::on_frame`) read raw IEEE-754 `f32` from the `csi_get_phase`/`csi_get_amplitude`/`csi_get_variance`/`csi_get_motion_energy` host imports **without any finiteness check** — the entire crate had **zero** `is_finite`/`is_nan` guards, and the in-crate `clamp` helpers propagate NaN (`NaN < lo` and `NaN > hi` are both false). A single non-finite value (firmware DSP bug, uninitialised buffer, or hostile host) latches NaN into the long-lived per-module accumulators (EMA, Welford, phasor sums, anomaly baselines); once latched, every downstream comparison evaluates `false`, so detectors fail **degraded** (stuck gate state, silently-disabled anomaly checks) — silent corruption, not a crash (WASM `panic=abort` is *not* tripped: no indexing/`unwrap` on the poisoned value). Threat model is a **semi-trusted** boundary (the Tier-2 DSP firmware supplies the imports, not direct network/JS), hence LOW severity / defense-in-depth. **Fix:** added `sanitize_host_f32()` (maps non-finite→`0.0`, `core`-only so it holds in `no_std`) applied at every `host_get_*` float read — a single chokepoint covering all ~70 downstream modules, mirroring the existing M-01 negative-`n_subcarriers` boundary clamp. **Pinned by** `boundary_tests::{sanitize_passes_finite_values_through, sanitize_maps_non_finite_to_zero, coherence_monitor_nan_latches_without_sanitize_but_not_with}` — the last asserts on the *current* `CoherenceMonitor` that a raw NaN frame latches the smoothed score (documents the hazard) while the boundary-sanitized path stays finite. **Dimensions attested CLEAN with evidence (source-analysis):** (a) **panic-on-input** — every non-test `unwrap()`/`expect()` is either `#[cfg(test)]` or in the `std`-gated RVF *builder* host tool writing to an in-memory `Vec` (infallible); no `panic!`/`unreachable!`/`todo!`/`get_unchecked` in any hot path. (b) **shape/bounds** — all frame-buffer access is `min()`-clamped (`MAX_SC=32`, `DTW_MAX_LEN`, `LCS_WINDOW`, `PATTERN_LEN`), all index-by-cast sites (`feature_id as usize`, `conclusion_id`, `minute_counter`, `plan_step`) are either compile-time-const-bounded or `if idx <`/`%`-guarded; negative `n_subcarriers` already mapped to 0 (M-01). (c) **memory/leak** — no `move ||` closures, no `mem::forget`/`Box::leak`/`.leak()`; the only `Box::new` is in the `std`-gated `skill_registry` (one-time init, bounded). (d) **secrets** — none (grep clean). **MEASURED build/test evidence:** host `cargo test --features std,medical-experimental` = **672 passed / 0 failed** (was 669 pre-fix; +3 new tests); the real deployment artifacts all build clean on the actual target — `cargo build --target wasm32-unknown-unknown --release` (no_std/panic=abort default lib), `--bin ghost_hunter --no-default-features --features standalone-bin`, and `--features medical-experimental` (toolchain 1.89 per `rust-toolchain.toml`). No ADR slot needed — a single LOW defense-in-depth boundary fix; CHANGELOG attestation suffices.
- **ADR-131 HOMECORE-UI BFF gateway — public-PR review fixes (PR #1082).** (1) **HIGH — path-traversal / confused-deputy SSRF closed in the `/api/cal/*` reverse-proxy** (`homecore-server/src/gateway.rs`). The wildcard proxy path was interpolated straight into the upstream URL while `proxy()` attaches the server-side calibration bearer, so `/api/cal/v1/../../x` (and percent-encoded `..%2f`, `%2e%2e`, leading `/`, backslash, double-encoded `%252e`) could escape the `…/api/` scope **with the privileged token**. Now `validate_proxy_path()` decode-then-checks and rejects absolute/backslash/dot-segment/encoded-traversal paths with a typed **400 BEFORE the URL is built** (applies to GET **and** POST); legit `v1/...` paths still pass. Pinned by `cal_proxy_rejects_traversal_with_400_before_upstream` (fails on old code) + `validate_proxy_path_rejects_traversal_variants`. (2) **CORS + request-tracing now cover the gateway routes.** `/api/homecore/*` and `/api/cal/*` were `.merge()`d **outside** the layers `homecore-api::router()` applies, leaving them with no CORS allowlist and untraced; the audited `build_cors_layer()` (HC-05) + `TraceLayer` are now applied to the whole merged surface in `main.rs`. Pinned by `gateway_routes_are_cors_covered_after_merge` (Vite-dev-origin preflight succeeds on a gateway route). (3) **Fabricated-data honesty (§6 invariant 3):** the gateway no longer injects a hardcoded `anomaly.threshold: 0.5` — it passes through the REAL upstream threshold or emits `null` (withheld); the dashboard renders a not-available `—` instead of `"null%"`/`"null°C"` for null appliance metrics; the COG panel's Hailo-worker pill reflects the real appliance probe instead of a hardcoded `"connected"`; `rooms.js` treats a null anomaly threshold as withheld, not a fake `0.8` default. (4) **Robustness:** a forwarded `hef` that is a string (not an array) no longer throws in the COG panel; the calibration wizard guards `frames/target` against `NaN%`/`Infinity%` and clears its baseline poll timer on Restart / panel teardown (leaked `setTimeout` loop fixed). (5) **Perf:** per-bank RoomState fetches and the appliance service probes now run concurrently (`futures::join_all`; async `tokio::net::TcpStream` + `timeout` replaces the blocking `connect_timeout` that parked a worker per probe); the mock fixture module is now a dynamic `import()` gated on demo mode so production never bundles it. **Note (workspace-wide, not fixed here):** `homecore-server` requests `reqwest`'s `rustls-tls` only, but cargo feature-unification means a sibling crate enabling the default `native-tls` re-introduces OpenSSL into the final binary regardless — a true "no OpenSSL on the appliance" guarantee requires aligning every reqwest-pulling crate on rustls-only. **Note (pre-existing, out of scope):** DEV-mode `allow_any_non_empty()` bearer auth when `HOMECORE_TOKENS` is unset on `0.0.0.0` is unchanged; the loud `warn!` at boot is retained — provision real tokens before network exposure. **Verified:** `cargo test -p homecore-server --no-default-features` = **18/18 pass**, `cargo build -p homecore-server` clean, UI suite (`node tests`) all green, Python proof VERDICT PASS (hash unchanged).
- **`wifi-densepose-desktop` (Tauri v2 desktop app) beyond-SOTA security review (needs ADR slot 178) — one real IPC serial-command-injection fix + one over-broad shell-capability removal, each MEASURED on Windows; remaining IPC/path/secret dimensions confirmed clean with evidence.** Beyond-SOTA review of the Tauri desktop crate (the real attack surface is the webview→Rust IPC boundary + the capability allowlist). The crate **builds + tests on this Windows box** (`cargo check`/`cargo test -p wifi-densepose-desktop --no-default-features` — Tauri 2.10 + GTK-less Windows webview2 path), so both findings are **MEASURED**, not source-analysis. **WDP-DESK-01 (serial command injection via `configure_esp32_wifi`, MODERATE) — FIXED.** The `#[tauri::command] configure_esp32_wifi(port, ssid, password)` handler took `ssid`/`password` straight from the webview and concatenated them into newline-terminated serial commands (`format!("wifi_config {} {}\r\n", ssid, password)`, `set ssid {}\r\n`, …) with **zero validation** before writing them to the ESP32 over the line-oriented serial protocol. A `\r\n` embedded in either field lets a malicious/compromised webview **terminate the command line early and inject an arbitrary follow-up firmware command** (`reboot`, `erase_nvs`, etc.) — a command-injection-into-device-protocol crossing the IPC trust boundary. Ironic note: the crate already shipped `test_wifi_credentials_validation` documenting the WPA2 length bounds, but the handler never enforced them. **Fix:** a new `validate_wifi_credentials(ssid, password)` rejects out-of-range lengths (SSID 1-32, password 8-63 — WPA2 PSK bounds) **and any control character** (`char::is_control()` catches `\r`/`\n`/NUL), called at the top of the handler before any serial write — fail-closed (`Err` → no bytes sent). Pinned fails-on-old / passes-on-new by `test_validate_wifi_credentials_rejects_injection` (`"net\r\nreboot"`, `"net\ninjected"`, `"pass\r\nerase_nvs"`, embedded NUL — all rejected; would splice into the command stream pre-fix), `test_validate_wifi_credentials_rejects_out_of_range`, and `test_validate_wifi_credentials_accepts_valid` (boundary 32-char SSID / 8- and 63-char passwords still accepted). **WDP-DESK-02 (over-broad shell capability, MODERATE) — REMOVED.** `capabilities/default.json` granted the webview `shell:allow-execute` + `shell:allow-open`, but the Rust backend spawns every process via `std::process::Command` directly (espflash/which/sensing-server — which **bypasses** the Tauri allowlist entirely) and the React UI only ever calls `dialog.open` (file picker) — verified by grep: `tauri_plugin_shell` is `init()`-ed but its `Command`/`open` API is **never invoked from Rust or TS**. The two `shell:` permissions were therefore unused privilege: a webview compromise (e.g. XSS in a UI dep) would have gained **arbitrary host command execution** via `shell.execute` with no scope restriction (no `shell` scope object was even defined). **Fix:** removed both `shell:` permissions from the capability (kept `core:default` + the two `dialog:` perms the UI actually uses). MEASURED: the build-regenerated `gen/schemas/capabilities.json` now reads `"permissions":["core:default","dialog:allow-open","dialog:allow-save"]` (shell perms gone), and the crate still builds + all tests pass — confirming nothing depended on the granted shell scope. (Plugin `init()` + the npm dep left in place to keep the blast radius minimal and avoid touching the off-limits generated ACL manifests; with no permission granted the plugin is inert.) **Dimensions confirmed clean (with evidence):** (1) **No directory-traversal / arbitrary-file primitive crossing the boundary** — the path-taking commands (`flash_firmware`/`verify_firmware`/`ota_update`/`wasm_upload`/`provision_*`) pass the webview-supplied path to `std::fs`/`espflash` to **read a firmware/wasm blob the local user themselves selected via the `dialog.open` native picker**; there is no command that *writes* to or *reads back* an arbitrary attacker-named path to the webview — `settings` read/write is confined to `app_data_dir().join("settings.json")` (fixed filename, no user path component), so no traversal sink exists. (2) **No shell-string interpolation** — every subprocess uses `Command::new(prog).args([...])` (argv vector, no shell), so the `port`/`source`/`chip`/`baud` args cannot inject a second command even though they are unvalidated (the `source` value flows only as a single `--source <value>` argv element). (3) **No SSRF-to-secret** — the `node_ip`-built URLs (`http://{ip}:8032/...`) target the local ESP32 mesh and return only device status; no credential is returned to the webview. (4) **Panic-on-input** — handlers use `.map_err(|e| e.to_string())?` throughout; the one `srv.pid.expect(...)` in `server_status` is guarded by an explicit `is_none()` early-return on the line above (unreachable), and the discovery/provision deserializers bounds-check before every slice index (`pos + len > data.len()` guards, NVS size capped at 4096). (5) **No hardcoded secrets** — `ota_psk` is an `Option<String>` supplied per-call/from settings, never embedded; grep for embedded keys/tokens over `src/` is empty. (6) **Tauri config** — `tauri.conf.json` ships no `"all": true` / `"$HOME/**"` FS or HTTP scope (no `fs`/`http` plugin enabled at all); the window set is a single fixed main window. `cargo test -p wifi-densepose-desktop --no-default-features`: lib **18 → 21 passed** (+3 validator pins), integration **21 → 21**, 0 failed. Workspace otherwise unchanged; Python deterministic proof unchanged (`f8e76f21a0f9852b70b6d9dd5318239f6b20cbcb4cdd995863263cecdc446f7a`, bit-exact — the desktop crate is off the signal proof path). Both findings warrant **ADR slot 178**.
- **`ruview-swarm` beyond-SOTA security + correctness review (ADR-148 drone swarm control plane; needs ADR slot 176) — 4 real fail-open / DoS bugs fixed in the NaN-state-poisoning class, each pinned fails-on-old / passes-on-new; 5 dimensions confirmed clean with evidence.** The shared theme is **IEEE-754 NaN/Inf silently defeating a safety comparison** on data that crosses the untrusted swarm-comm trust boundary (`SwarmOrchestrator::receive_peer_state` / `receive_peer_detection` accept full `DroneState`/`CsiDetection` whose f64/f32 fields deserialize with no finite-check; the integer-encoded MAVLink wire formats in `mavlink_messages.rs` cannot carry NaN, but the serde struct path can). **(1) HIGH — `failsafe::FailSafeMachine::tick` collision-avoidance + battery fail-open** (`failsafe/mod.rs:51,75`). `nearest_neighbor_dist < collision_dist_m` and `battery_pct <= rth_pct` both evaluate `false` for a NaN operand, so a poisoned peer position (→ NaN `nearest_peer_distance` via `Position3D::distance_to`) **silently disabled collision avoidance** and a NaN battery reading kept a drone Nominal — the worst failure for a physical airframe. Fixed to fail CLOSED (`!is_finite() ||` → `EmergencyDiverge` / `ReturnToHome`). MEASURED fails-on-old: `test_nan_neighbor_distance_fails_closed_to_diverge` / `test_nan_battery_fails_closed_to_rth` both returned `Nominal` pre-fix. **(2) MEDIUM — `security::geofence::Geofence::check` NaN-altitude bypass** (`security/geofence.rs:33`). A NaN `z` (altitude) with valid x/y skipped the altitude breach (`NaN < min || NaN > max` = `false`) and returned **`Safe`** through the point-in-polygon path — a silent geofence bypass. Fixed with a leading non-finite-coordinate → `HardBreach` guard. MEASURED fails-on-old: `test_nan_altitude_fails_closed` returned `Safe` pre-fix. **(3) MEDIUM/DoS — `security::antijamming::FhssRadio` `% 0` panic on empty `channels_mhz`** (`security/antijamming.rs:65,71,102`). `FhssConfig` is `Deserialize`; an empty channel list (malformed/hostile config) made `next_hop`/`current_channel_mhz`/`evasive_hop`/`tick` panic with `remainder with a divisor of zero`, crashing the radio task. Fixed with `len == 0` early-returns (benign `0.0` sentinel). MEASURED fails-on-old: `test_empty_channels_does_not_panic` **panicked** (`divisor of zero`) pre-fix. **(4) LOW — `sensing::multiview::MultiViewFusion::fuse` NaN victim-position propagation** (`sensing/multiview.rs:70`). A NaN `victim_position` passed the `is_some()` filter and propagated through the confidence-weighted average into the fused "confirmed victim" location dispatched to the swarm. Fixed by requiring finite `confidence` + finite position components (fail-closed drop). MEASURED fails-on-old: `test_nan_victim_position_dropped_from_fusion` produced a non-finite fused position pre-fix. **Dimensions confirmed clean (with evidence):** (a) **MAVLink decode panic-safety** — `SwarmNodeState::decode(&[u8;20])` `try_into().unwrap()`s are over fixed const ranges of a fixed-size array (provably infallible; no arbitrary-length `&[u8]` path exists). (b) **UWB GPS anti-spoofing is NaN-safe** — `(gps_dist - uwb_dist).abs() <= tol` already fails CLOSED on a NaN range/position (counts as inconsistent → spoof rejected), verified by reasoning + existing `test_spoofed_gps_invalid`. (c) **Bounded grid / no allocation-from-length-field** — `ProbabilityGrid::update_bayesian`/`mark_scanned` bounds-check `cx >= width || cy >= height`; `pos_to_cell` uses saturating `as u32` (Rust `as` saturates, no UB). (d) **Mesh `nearest_k` NaN-safe sort** — `partial_cmp(..).unwrap_or(Equal)` cannot panic on NaN distances. (e) **No hardcoded secrets** — `MavlinkSigner` key is constructor-injected (`[u8;32]`), nothing embedded. **Documented-not-fixed (for ADR-176, not churned to avoid test-rewrite risk):** (i) **Raft `AppendEntries` lacks the Log-Matching consistency check** (`topology/raft.rs:187`) — a follower appends leader entries on `term >= current_term` without validating `prev_log_index`/`prev_log_term`, so a malformed/byzantine leader can corrupt a follower's log (a genuine consensus-safety gap; vote tallying is also delegated to the caller per the existing `handle_message` comment). (ii) **`MavlinkSigner::verify` uses a non-constant-time tag `==` and has no replay/timestamp-window rejection** (`security/mavlink_signing.rs:64`) — the doc comment already flags the replay limitation as a known demo/test simplification. `cargo test -p ruview-swarm --no-default-features`: **117 → 123 passed, 0 failed** (+6 pins). Workspace green; Python deterministic proof unchanged (`f8e76f21a0f9852b70b6d9dd5318239f6b20cbcb4cdd995863263cecdc446f7a`, bit-exact — `ruview-swarm` is off the signal proof path).
- **`nvsim` (ADR-089 NV-diamond magnetometer simulator) beyond-SOTA security review — two real degenerate-input findings fixed (config-induced panic/DoS + NaN-state-poisoning silent-corruption), each pinned by a fails-on-old test; determinism integrity, panic-free deserialisation, and RNG-seeding confirmed clean with evidence. Needs ADR slot 177.** Beyond-SOTA review of the standalone WASM-ready forward-only pipeline (`scene → source → propagation → NV ensemble → digitiser → MagFrame + SHA-256 witness`). The real risk surface is degenerate physical-parameter input (the scene + config are the external boundary, especially via the WASM `config_json`/`scene_json` entry points). **Finding 1 — NVSIM-DT-01 (config-induced panic / DoS, MEDIUM; `pipeline.rs:58,95`).** `dt` was derived as `config.dt_s.unwrap_or(1.0 / f_s_hz)`; an externally-supplied `f_s_hz == 0.0` makes `dt == +Inf`, `(dt*1e6) as u64` saturates to `u64::MAX`, and `(sample as u64) * dt_us` then **panics `attempt to multiply with overflow`** for `sample >= 2` (MEASURED: probe panicked at `pipeline.rs:95:30` under the debug/test profile; in `panic=abort` WASM this aborts the module, in release it silently wraps `t_us` to garbage). **Fixed** by sanitising `dt` (non-finite/non-positive → 1 µs fallback), capping the `u64` cast at `u64::MAX`, and using `saturating_mul` for the timestamp so no config can ever overflow it. **Finding 2 — NVSIM-NAN-01 (NaN-state-poisoning silent corruption, MEDIUM; funnel at `digitiser.rs::adc_quantise`).** A non-finite scene parameter (e.g. a `NaN`/`Inf` dipole **position**, `Inf` **moment**, or `NaN` loop **radius**) flows through `scene_field_at` and **bypasses the near-field clamp** — `NaN < R_MIN_M` is `false`, so the `1/r³` path is taken and produces a `NaN`/`Inf` field (MEASURED: `b=[NaN,NaN,NaN], sat=false`). At the ADC that non-finite value hit the `else` branch and **`NaN as i32 == 0`** (Rust saturating cast), emitting a frame with `b_pt=[0,0,0]` and **the `ADC_SATURATED` flag CLEAR** — a frame *indistinguishable from a legitimate zero-field reading* (MEASURED: `b_pt=[0.0,0.0,0.0] flags=0b0000`). This is the same NaN-poisoning class flagged across the calibration/vitals crates; the `propagation` module already guards its NaN paths, but the source→ADC path did not. **Fixed** at the single funnel point: `adc_quantise` now treats any non-finite input as out-of-range → clamps to code `0` **and raises the saturation flag**, so the corruption is visible downstream (the pipeline's existing `adc_sat` OR-reduction propagates `ADC_SATURATED` onto the frame). **Dimensions confirmed clean (with evidence):** (1) **Determinism integrity — clean.** The only RNG is `ChaCha20Rng::seed_from_u64(seed)` fully seeded from the caller's `u64` (grep: one `seed_from_u64`, zero `thread_rng`/`getrandom`/`SystemTime`/`Instant`/`HashMap`); Cargo.toml pins `rand`/`rand_chacha` with `default-features=false` (no OS-entropy path). Box–Muller draws from `gen_range(f64::EPSILON..=1.0)` (avoids `ln(0) = -Inf` by construction). Frame serialisation is fixed little-endian; source summation order is fixed by `Vec` order. The published cross-machine witness `cc8de9b0…93b4` (`proof::tests::proof_witness_publishes_a_known_value`) **still passes unchanged** after both fixes — the happy-path output is byte-identical, confirming the guards only affect degenerate inputs. (Attested caveat, not a finding: libm `cos`/`ln`/`sqrt` *could* differ x86↔wasm; witness is documented as x86_64-captured.) (2) **Panic-free deserialisation — clean.** `MagFrame::from_bytes` validates `len`/magic/version, then the per-field `buf[a..b].try_into().expect(...)` calls are over **fixed sub-ranges of an already-length-checked 60-byte buffer** → provably infallible, not reachable panic vectors. No `unsafe`, no `panic!`/`unreachable!` in production code; every other `unwrap`/`expect` is `#[cfg(test)]`. (3) **Div-by-zero — clean.** `dipole_field`/`current_loop_field` clamp `r_norm < R_MIN_M` (1 mm) before the `1/r³`/`1/r²` divide (finite inputs); `shot_noise_floor` guards `denom <= 0.0 → f64::INFINITY`; `vec3_normalise` guards `n < 1e-20`. (The only gap was the NaN case that *bypasses* the `r_norm` clamp — fixed at the ADC funnel above.) **Pinning tests (fails-on-old / passes-on-new, MEASURED):** `pipeline::degenerate_zero_sample_rate_does_not_panic` (panicked on old code; now finite frames), `pipeline::non_finite_scene_input_flags_frame_instead_of_silently_zeroing` (old: `flags=0b0000`; now `ADC_SATURATED` set, `b_pt` finite), `digitiser::adc_quantise_flags_non_finite_as_saturated` (old: `(0,false)` for NaN; now `(0,true)`). `cargo test -p nvsim --no-default-features`: **50 → 53 passed, 0 failed**. Workspace green; Python deterministic proof unchanged (`f8e76f21…46f7a`, bit-exact — nvsim is off the signal proof path). Needs ADR slot 177.
- **`wifi-densepose-core` + `wifi-densepose-cli` beyond-SOTA security review (ADR-127 note; CLI needs ADR slot) — NaN-state-poisoning bug class does NOT originate in core (verdict: no + evidence); both crates confirmed clean on all reviewed dimensions; 4 regression pins added locking in two real DoS guards.** **Load-bearing question — verdict NO (with evidence, MEASURED).** The NaN-state-poisoning class that hit `wifi-densepose-calibration`/`-vitals`/`-geo` (a non-finite input latching into a persistent IIR/Welford/von-Mises/voxel accumulator → silent permanent feature failure) does **not** live in a shared `wifi-densepose-core` primitive: core exposes **no stateful accumulator at all** — no Welford/running-mean, no von-Mises/circular-mean, no IIR/biquad filter state, no voxel grid. Grep over `core/src` for `welford|von_mises|biquad|y1|y2|running_mean|accumulat|voxel|self.*+=` matched only the `InvalidState` *error* enum, "reset state" doc comments, and a test-only LCG — zero stateful logic (MEASURED). Each downstream crate rolls its own accumulator, so each fix is correctly local; corroborated by `wifi-densepose-calibration::Features::from_series`, which **already** filters non-finite samples and returns `Features::ZERO` (the downstream re-implementation of the fix). The only float math in core's hot path is construction-time projection (`CsiFrame::new` → `amplitude`/`phase` via `mapv`) and pure stateless `utils` functions — none persists state across frames. **Dimensions confirmed clean (with evidence):** (1) **panic-on-adversarial-input = 0** — `CsiFrame::from_canonical_bytes` (the replay/forward deserialisation boundary) returns a typed `CanonicalDecodeError` for every malformed input (truncation, bad discriminant, non-UTF-8 device id, nonzero reserved bytes, shape/payload mismatch, trailing bytes); the CLI UDP parser `parse_csi_packet` (the widest CLI attack surface — bound to `0.0.0.0` by default) returns `None` on any malformed datagram. Both proven panic-free over deterministic-LCG fuzz sweeps (new pins). (2) **`Confidence::new` rejects NaN** (`!(0.0..=1.0).contains(&NaN)` ⇒ `true` ⇒ `Err`); `compute_bounding_box`/`to_flat_array` are NaN-tolerant (f32 `min`/`max` ignore NaN). (3) **`amplitude_variance`/`mean_amplitude` panic-free on empty frames** — ndarray 0.17 `var(0.0)`/`mean()` return finite/`None` (handled), not a panic (MEASURED via throwaway probe). (4) **Unbounded-memory DoS bounded** in both deserialisers: `from_canonical_bytes` guards the `Vec::with_capacity(rows*cols)` with `rows.saturating_mul(cols).saturating_mul(16) <= bytes.len()`; `parse_csi_packet` guards `Array2::zeros` with `buf.len() < 20 + n_pairs*2` — a header lying about an enormous `rows×cols` / `n_antennas×n_subcarriers` is rejected before allocation. (5) **CLI path-traversal** in `calibrate-serve` already defended by `sanitize_room_id` (keeps `[A-Za-z0-9_-]`, caps 64 chars, with tests) on every client-supplied `room_id`/`bank`/`baseline` name that reaches a file path; bearer-auth gate + non-loopback-bind warning present. (6) **No hardcoded secrets** (`--token` read from `CALIBRATE_TOKEN` env, never embedded). **Regression pins added (fails-on-old / passes-on-new):** core `canonical_decode_oversized_shape_is_bounded_not_allocated` (MEASURED: with the saturating guard removed it panics `capacity overflow` at `types.rs:801`; passes with the guard) and `canonical_decode_never_panics_on_arbitrary_bytes`; CLI `test_parse_csi_packet_oversized_claim_is_rejected_not_allocated` (a 255×65535-pair claim ≈ 33 MB in a 2 KB datagram → `None`, never OOMs) and `test_parse_csi_packet_never_panics_on_arbitrary_bytes`. `cargo test -p wifi-densepose-core`: **35 → 37** lib tests, 0 failed; `cargo test -p wifi-densepose-cli --no-default-features`: **24 → 26**, 0 failed. Workspace green; Python deterministic proof unchanged (`f8e76f21…46f7a`, bit-exact — core/cli are off the signal proof path). No production code changed — review is clean-with-evidence plus pins.
- **`homecore` foundational state-machine review (ADR-127) — one real concurrency bug fixed (state-set TOCTOU dropping/reordering `state_changed` events) + two hardening fixes (entity_id memory-DoS, service-handler panic isolation), each pinned by a fails-on-old test; event-bus lag & lock discipline confirmed clean with evidence.** Beyond-SOTA security+concurrency review of the crate every other HOMECORE module builds on (state store `state.rs`, event bus `bus.rs`, service/entity registries, the `HomeCore` coordinator), un-covered by the ADR-154–159 sweep — a bug here is high-blast-radius. **HC-RACE-01 (state-set TOCTOU, the crux — race/lost-event).** `StateMachine::set` did `get()` (releasing the DashMap shard lock) → compute the next snapshot + the no-op/`last_changed` decision → `insert()` (re-acquiring the lock) → `send()`; the read-modify-write was **not atomic** w.r.t. a concurrent writer on the same entity, contradicting ADR-127 §2.1's promise that "the writer atomically replaces the map entry." A writer that read a **stale `old`** could mis-classify a genuine transition as a no-op and **silently drop its `state_changed` event** (a missed automation trigger) or fire an event whose `new_state` duplicated the previously delivered one (a spurious trigger for any automation keyed on `old_state != new_state`). **Fixed** by holding the shard write-lock across the whole read→decide→insert→fire sequence via `entry()`/`insert_entry()` — `tx.send` is non-blocking, non-async, and never re-enters the map, so firing under the shard lock cannot deadlock and keeps global event order in lock-step with global commit order. Pinned by `concurrent_set_fires_no_duplicate_adjacent_events` (4 writers toggling one entity A/B; asserts no two consecutive fired events carry an identical `new_state` — impossible under correct serialisation; an instrumented probe observed ~93k such duplicate-adjacent events across 200 trials on the racy code, **zero** on the fix; the test fails reliably on the first trial pre-fix). **HC-EID-LEN-01 (unbounded `entity_id`, memory-DoS).** `homecore-api/src/rest.rs` parses untrusted REST path segments straight through `EntityId::parse`; with no length cap an otherwise-valid id (`a.` + many MB of `[a-z0-9_]`) was accepted, and a `POST /api/states/<giant>` would persist it into the DashMap state store (permanent growth across distinct ids). **Fixed** by rejecting ids longer than `MAX_ENTITY_ID_LEN` (255, HA-compatible) up front in `parse()`, before any per-char scan, with a new `EntityIdError::TooLong` — fail-closed at the boundary type protects every caller (REST, registry deserialize, automation). Pinned by `entity_id_length_boundary` (exactly-MAX accepted; MAX+1 and a 4 MiB id rejected — oversized parses `Ok` on old code). **HC-SVC-PANIC-01 (service-handler panic not isolated).** `ServiceRegistry::call` already ran handlers **outside** the registry lock (the `Arc<dyn ServiceHandler>` is cloned out of the read guard first → no `RwLock` poisoning, no blocking of other callers — clean), but a panicking handler unwound through `call()` into the caller's task (the task driving the engine). **Hardened** by wrapping the handler future in `AssertUnwindSafe` + `catch_unwind`, converting a panic to `ServiceError::HandlerPanicked`; the registry stays fully usable (a sibling healthy service still returns, the bad service stays registered). Pinned by `panicking_handler_is_isolated_and_registry_survives` (unwinds through `call` on old code). **Dimensions confirmed clean (with evidence, no invented issues):** (1) **event-bus bounds / lag** (the homecore-api WS lag-DoS class) — both `StateMachine` and `EventBus` use **bounded** `tokio::sync::broadcast` (capacity 4,096); a slow subscriber gets a recoverable `Lagged(n)` (drop-oldest + re-sync) while `fire_*` is non-blocking and never waits on slow receivers, so a lagging subscriber **cannot block the publisher, grow the channel without bound, or kill a fast subscriber** (evidenced by `slow_subscriber_does_not_block_publisher_or_kill_the_bus` — fire 3× capacity at an idle subscriber, publisher unblocked, bus stays live, fresh fast subscriber receives, lagged one recovers); (2) **lock ordering / lock-across-await** (deadlock) — no code path holds two of `{state DashMap, registry RwLock, service RwLock}` simultaneously, so no inconsistent-ordering deadlock can exist; every `tokio::sync::RwLock` guard in `registry.rs`/`service.rs` is used in one synchronous statement and dropped before any `.await` (`call` explicitly scopes the read guard out before awaiting the handler); the only guard held across a send is the DashMap shard lock in `set`, across a **synchronous** broadcast send — safe; (3) **panic-on-input** — no reachable `unwrap`/`expect`/index in non-test code beyond the safe `send().unwrap_or(0)` and the dead-but-harmless `split_once(...).unwrap_or(...)` fallbacks on already-validated ids. `cargo test -p homecore --no-default-features`: **20 → 24 passed, 0 failed** (+4 pins). Workspace green; Python deterministic proof unchanged (`f8e76f21…46f7a`, bit-exact — `homecore` is off the signal proof path). Review notes appended to ADR-127 §9.
- **`homecore-migrate` security review (ADR-165 surfaces) — one real secret-leak fix; traversal / data-loss / panic / injection dimensions confirmed clean with evidence.** Beyond-SOTA review of the Home-Assistant `.storage`/`secrets.yaml`/`automations.yaml` migrator, the two sharp surfaces being secret handling (`secrets.rs`) and untrusted-file parsing. **Finding + fix (secret-leak, `secrets.rs`):** a malformed `secrets.yaml` whose offending scalar fails a typed-tag coercion (e.g. `port: !!int <value>`) produced a `serde_yaml` error whose message **embeds the scalar verbatim** — `invalid value: string "<the-secret-value>"`. The old code wrapped that message into `MigrateError::YamlParse { source }`; the error propagates out of `read_secrets`, is `?`-returned by the `InspectSecrets` CLI path in `main.rs`, and printed to stderr by `anyhow` — **leaking a secret value despite the CLI's deliberate `<redacted>` design** (`main.rs` only ever prints keys as `<key> = <redacted>`). Fix: `secrets.yaml` parse failures now map to a dedicated redacting variant `MigrateError::SecretsParse { path, line, column }` carrying only the file path + a coarse location (from `serde_yaml::Error::location()`), never the scalar; other (non-secret) YAML files keep `YamlParse`. **Pinned** by `secrets::tests::malformed_secrets_error_never_contains_secret_value`, which asserts the rendered error **and its full `#[source]` chain** never contain the secret value and that the error is still the structured `SecretsParse` (fail-closed) — it **fails on the old `YamlParse` path** (observed leak: `... invalid value: string "s3cr3t_TOKEN_VALUE" ...`) and passes on the fix; plus `malformed_secrets_error_reports_location` (still locatable). **Confirmed clean with evidence:** *secret leakage elsewhere* — the only secret sink is the value map; `main.rs` redacts values, and the `MissingField`/`Io` paths surface only the path, never content. *Source mutation / data-loss* — **structurally impossible**: there is no `fs::write`/`fs::remove`/`fs::create`/`File::create`/`OpenOptions` anywhere in the crate; P1 reads source and writes nothing (`import-entities` is in-memory only), so re-runs are trivially idempotent and the HA source is never touched. *Path traversal* — CLI takes a `--config-dir`/`--storage` dir and joins **fixed** filenames (`secrets.yaml`, `core.entity_registry`, …); no user-controlled path component, no `..`/absolute escape beyond the user's own privileges. *Panic-on-input* — probed duplicate-key, bad-indent, tab/control-char, multi-doc, non-mapping-root, unterminated-flow, `!input` blueprint tags, deep nesting, anchors: **every** malformed/typed/truncated input **errors, never panics** (all production code is panic-free; every `unwrap`/`expect` is `#[cfg(test)]`). *Fail-closed versioning* — unknown storage `minor_version` hard-errors (no silent fallback to an older parser). *Injection* — no SQL/shell/path interpolation; the tool emits diagnostics only and persists nothing in P1. `homecore-migrate` **19 → 21** tests (`--no-default-features`), 0 failed. Behaviour otherwise unchanged; Python deterministic proof PASS, hash unchanged (`homecore-migrate` is off the signal proof path).
- **`homecore-recorder` security review (ADR-132 surfaces) — two real bounding fixes; SQL-injection & NaN-index dimensions confirmed clean with evidence.** Beyond-SOTA review of the HA-compat state recorder (DB persistence + history + ruvector semantic search), the crux being its DB-backed SQL-injection surface. **Findings + fixes:** (1) **Memory-DoS — unbounded `get_state_history`.** The history query carried no `LIMIT`, so a wide `[since, until]` window over a high-frequency entity (a per-second sensor ≈ 86k rows/day) would load an unbounded row set into a single in-memory `Vec`. Added a hard `LIMIT MAX_HISTORY_ROWS` (1,000,000 — generous enough never to truncate a realistic history graph, bounded enough to cap the worst case); the sibling search paths were already `k`-bounded. (2) **Disk-DoS / documented-but-missing `purge`.** The README + HA-compat table advertised `Recorder::purge(older_than)` as a capability, but **no such method existed** — i.e. no retention path at all → unbounded disk growth. Implemented a **transactional** `purge` that deletes `states` + `events` strictly **older than** the cutoff (**exclusive** boundary — idempotent, no off-by-one; a row at the cutoff instant is kept) and **garbage-collects** orphaned `state_attributes` blobs (a dedup-shared blob is dropped only once its last referencing state is gone); all three deletes run in one transaction so a mid-purge failure rolls back cleanly (no states-deleted-but-events-kept corruption). **Confirmed clean with evidence:** SQL injection — **every** query in `db.rs` uses bound `?` parameters (no `format!`/string-concat of user data into SQL); the lone `format!` builds the LIKE *pattern*, which is itself bound as a parameter with `ESCAPE '\\'` and metacharacter escaping. Pinned: a state value `'; DROP TABLE states; --` is stored/queried **literally** (table survives), and a `%`/`_` in a search query matches **literally**, not as a wildcard. NaN-index poisoning (the calibration/vitals/geo class) — **structurally impossible** here: embeddings are SHA-256 → `i32` → `f32` (an `i32` cast to `f32` is always finite, never NaN/Inf), with an all-zero-digest norm guard; probed empty-index search, empty-string query, and `k=0` — all return `Ok(0)`, **no panic**. Fail-closed write path — a removal event yields `Ok(None)`, semantic-index failure is logged not propagated (best-effort, never blocks the durable SQLite write), and `EntityId` parsing failures fall back rather than panic. **6 new pinning tests** (SQL-injection literal-storage, LIKE-metacharacter literalness, history `LIMIT`, purge exclusive-boundary, purge attribute-GC-keeps-shared, purge old-events): `homecore-recorder` **19 → 25** (`--no-default-features`) / **25 → 31** (`--features ruvector`), 0 failed; the purge-boundary test is a true pin (fails deleting 2 rows under an inclusive cutoff, passes deleting 1 under the exclusive cutoff). Behaviour otherwise unchanged; Python deterministic proof unchanged (recorder is off the signal proof path).

### Added
- **ADR-131 §11–§12: HOMECORE-UI wired to a real backend — single-origin BFF gateway + production front-end (no mock in prod).** Implements the §11 wiring decision so the dashboard stops rendering fabricated data. **Front-end (DONE + verified under Node):** `api.js` rewritten so every data accessor is async and calls the §11.2 gateway routes; the in-browser mock is demoted to a **dev-only fixture** reachable only via `?demo=1`/`HOMECORE_UI_DEMO` (§2.2); all ten panels now `await` and render a **typed empty/error state** on upstream failure (no mock fallback in production) — 3 panels converted by hand, 7 via a parallel agent swarm. **New `homecore-server` BFF gateway (`src/gateway.rs`, compile-pending — no Rust toolchain in the authoring env):** promotes `homecore-server` to the single origin (§2.1); adds `/api/homecore/*` + `/api/cal/*` merged into `build_app`, with `reqwest` + CLI/env flags (`--calibration-url`/`--calibration-token`/`--apps-dir`/`--gateway-timeout-ms`). Real handlers: calibration **reverse-proxy** (W2), `GET /api/homecore/rooms` with the §11.3 **RoomState adapter** (`breathing`→`breathing_bpm`, `heartbeat`→`heart_bpm`, `None`→`null` preserving not-trained-vs-withheld, injected `anomaly.threshold`/`room_id`), **COG supervisor** over `/var/lib/cognitum/apps/` (W4), and **appliance metrics** from `/proc` + TCP service probes (W6); SEED-device/appliance routes (seeds/federation/witness/privacy/settings/automations/events-history/hailo/tokens — W3/W5) return a typed `503 upstream_unavailable` and the UI shows error states. **Tests:** front-end **5 files green** — import-graph, boot, render-smoke (22), interaction (3), and a **new prod-errors suite (13)** that runs with demo OFF + gateway unreachable and proves every panel renders an error state, never mock, never throws (it caught + fixed a real unhandled-rejection in the events automation builder). **Gateway compiled, tested, and run on Rust 1.89:** `cargo test -p homecore-server --no-default-features` = **12/12 pass** (6 gateway + 6 UI mount); the binary was **run live** — `GET /api/homecore/appliance` returns real `/proc` metrics + TCP service probes, unauth → `401`, `cogs` → `[]` (no apps dir), SEED-tier → typed `503`, and against a mock calibration upstream the `/api/cal/*` proxy passes through (`200`) and `GET /api/homecore/rooms` adapts `RoomState` to the UI shape (`breathing`→`breathing_bpm`, `heartbeat:null`→`heart_bpm:null`, injected `anomaly.threshold`/`room_id`). **Live testing caught + fixed a real bug** — a double-`v1` segment in the `/api/cal/*` proxy URL. **Remaining (intrinsic, not an env limit):** W3/W5/W6-Hailo/federation depend on services/hardware **not in this repo** (recorder/automation HTTP wrappers, real SEED nodes, Hailo stat source), so they return honest `503`s rather than fabricate data; W1/W2/W4/W6-appliance are functional now. ADR-131 §10/§12.1 updated with per-wave status.
- **ADR-131: HOMECORE-UI — the complete operational dashboard for the two-tier Cognitum stack, served by `homecore-server` at `/homecore`.** A zero-dependency, no-build-step vanilla TS/JS + CSS frontend (the `rufield-viewer` "Axum + vanilla-JS" pattern) that extends the Cognitum Appliance shell as a first-class nav section (Framework | Guide | Cog Store | **HOMECORE** | Status). **Complete, not a scaffold** (per the ADR's revised §2/§7): all **10 panels** ship fully built and rendered — §4.1 System Dashboard (v0 Appliance health strip + SEED fleet grid + ESP32 summary + COG status row + event-bus sparkline), §4.2 SEED Detail (vector store / witness chain / 5 onboard sensors / reflex rules / cognitive-fragility / ingest packet-type), §4.3 SEED Fleet Map (Appliance→SEED→ESP32 hierarchy, ESP-NOW mesh, cross-SEED fusion badges, ADR-105 federation), §4.4 Entity & State Browser (domain-grouped, **live WebSocket `subscribe_events` patching — never polls**, first-class provenance badges, keyword filter, context-causality slide-over), §4.5 RoomState/Sensing (mixture-of-specialists), §4.6 COG Management + App Registry, §4.7 Calibration Wizard (5-step baseline→enroll→train→verify), §4.8 Event Bus + Automation builder, §4.9 Witness/Audit log (two-tier SHA-256 + Ed25519 timeline, privacy-mode banner, pagination, export), §4.10 Settings. **Design system is the exact production Cognitum palette** (`tokens.css` carries `--cyan #4ecdc4` … `--r 10px` verbatim, §3.1) so there is no visual seam with the Cog Store (§3.3 invariant). **§6 UX invariants enforced in code and pinned by tests:** tier-origin provenance is always-visible (never collapsed); `stale`/`vetoed` flags and the kNN fragility score are prominent (amber/red tint + banners, never grey-on-grey); a `null` specialist renders "Not trained / calibrate to enable" **visually distinct from** veto-`withheld` (rendered as explicitly withheld, never zero) **distinct from** an error; all IDs/hashes/endpoints/payloads use `--mono`; Hailo-sourced COGs (`arch: hailo10`) are visually distinguished from CPU-only (`arch: arm`). **Wiring:** `homecore-server` gains a `--ui-dir`/`HOMECORE_UI_DIR` flag and mounts the assets via `tower-http` `ServeDir` at `/homecore` alongside the unchanged HA-compat `/api` surface (new testable `build_app()`), with **5 Rust integration tests** (`#[cfg(test)] mod ui_tests`, `tower::oneshot`) asserting index / design tokens / all-10-panels are served, the API coexists, and an empty `--ui-dir` disables the mount. **JS test + benchmark suite (`ui/`, runs under plain `node`, no npm install): 24 checks / 0 failed** — an import/export graph verifier (15 modules consistent), a DOM-shim render-smoke that *executes every panel* (21 checks: ui helpers + mock contracts + all 10 panels render without throwing), and an interaction suite (3 checks: live WS state-patch, ws.js handshake/parse, calibration backend contract). **Benchmark:** total bundle **136.8 KB uncompressed across 18 files — ~37× smaller than HA's ~5 MB Lit bundle** (the ADR-126 §1.1 foil), slowest panel **1.5 ms/cold-render**. **Honest scope (§7.1):** the live HOMECORE REST API (`/api/config|states|services`) and the WebSocket `subscribe_events` feed are driven for real; panels whose backing service is **not** in this binary (SEED HTTPS API, calibration ADR-151, ADR-105 federation) render against a **contract-conformant mock layer flagged with a DEMO banner** and swap to live the moment those endpoints land — no mock data is ever presented as real. **Not verified in this environment:** the Rust crate was edited and the integration tests written but **not compiled/run here** (no Rust toolchain present); `cargo test -p homecore-server` + `cargo build` must be run on a Rust host before merge.
- **ADR-175: int8 quantization of the WiFlow-STD "half" pose model — MEASURED fp32-vs-int8 accuracy/size trade-off (honest negative).** Sub-deliverable 8.2 of the benchmark/optimization milestone, and the reading of the SOTA brief's "one untested edge lever" (QAT-int8 on the 843,834-param half model that strictly dominates the published 2.23M model). A new committed script `v2/crates/wifi-densepose-train/scripts/quantize_half_int8.py` quantizes `half_best.pth` to int8 two ways and scores both with the **same** upstream `calculate_pck`/`calculate_mpjpe` that produced the fp32 sweep numbers, under **one locked normalization** (ADR-173 torso-diameter PCK — neck idx2→pelvis idx12, `use_torso_norm=True`, the standard MM-Fi/GraphPose-Fi convention), on the **same** seed-42 file-level 70/15/15 test split (52,560 NaN-free / 54,000 full windows). **MEASURED on ruvultra (RTX 5080, torch 2.11.0+cu128, fbgemm; clean test, torso-PCK):** fp32 = 96.62% PCK@20 / 99.47% PCK@50 / 0.008981 MPJPE / 3.351 MB (fp32-CPU reproduces fp32-GPU to 4 dp, so the int8 deltas are pure quantization, not CPU/GPU drift); **int8 static PTQ = 40.98% PCK@20 (−55.64 pp), 1.046 MB** — naive static QDQ **collapses** on this model (the brief's 2.23M "sweet spot" does NOT transfer to the 843k half model at the tight @20 threshold); **int8 QAT (3-epoch FX fake-quant fine-tune from half_best) = 67.48% PCK@20 (−29.15 pp) / 98.69% PCK@50 (−0.78 pp), 1.043 MB.** **Verdict (honest no):** int8 is **not a win** at the strict PCK@20 edge target — QAT recovers a large share of the PTQ collapse and is near-lossless at the loose PCK@50 (coarse localization survives int8, fine does not), but a **3.2× size win at −29 pp PCK@20** is a bad trade when the half model already fits edge flash at fp32 → **keep fp32/fp16 on the edge for now.** **Disclosed gap:** the QAT *fake-quant* val PCK@20 reached 83.45% but the *converted* int8 model scores 67.48% — a real ~16 pp `convert_fx` gap (fbgemm int8 kernels ≠ straight-through estimate, esp. the axial-attention einsum/softmax); we report the converted-int8 number, not the fake-quant proxy. **MEASURED:** every table number + the PTQ collapse + the QAT partial recovery + the conversion gap. **CLAIMED/not done:** ONNX/TFLite export, on-edge-SoC latency/energy (int8 measured on x86 fbgemm — size transfers, latency does NOT), mixed-precision keeping attention fp32, longer/better-tuned QAT. **Honest limitations:** single in-domain eval split (no cross-environment split), x86-int8 not edge-SoC-int8, lightly-tuned QAT. Additive only — no production Rust or signal-pipeline change; Python deterministic proof unchanged (`f8e76f21…46f7a`, bit-exact — off the signal proof path).
- **Metric-locked PCK/MPJPE accuracy harness — resolves the PCK-definition ambiguity (`wifi-densepose-train`, needs ADR slot 173).** The SOTA brief (`docs/research/sota-nn-train-benchmark-brief.md` §1, §3.1, §4) found the single biggest threat to any "beyond-SOTA" claim is **metric ambiguity**: three PCK@20 figures (96.09% WiFlow-STD image-normalized, 81.63% AetherArena torso-PCK, 61.1% GraphPose-Fi standard PCK) cannot be lined up because each silently uses a different normalization — the project was retracted twice over this (a withdrawn "92.9%" used *absolute* pixels, not torso). New `src/accuracy.rs` makes the normalizer **explicit, selectable, and carried with every reported number**: a `PckNormalization` enum (`TorsoDiameter` = standard MM-Fi/GraphPose-Fi hip↔hip; `BoundingBoxDiagonal` = looser WiFlow-STD image-normalized; `AbsolutePixels(threshold)` = the retracted convention, included so historical numbers are reproducible and clearly labeled non-comparable); one canonical `pck_at(pred, gt, vis, k, normalization)` reusing the `metrics_core` geometric primitives (hip distance, bbox diagonal — no duplicate kernel); `mpjpe(pred, gt, vis)` (2D/3D, mm); and a self-describing `PoseAccuracy { pck_at: BTreeMap<u8,f32>, mpjpe, normalization, n_keypoints, n_frames }` returned by `accuracy_report(frames, ks, normalization)` so an **unlabeled PCK number is structurally impossible**. **17 hand-computed deterministic tests** (no GPU, no datasets) prove the harness arithmetic: perfect→PCK=1.0/MPJPE=0; all-just-outside→0.0; half-in-half-out→0.5; the **key proof** that identical predictions score 0.50 (torso) / 1.00 (bbox) / 0.75 (abs) under the three normalizations (the ambiguity is real and the definitions are distinct); MPJPE 2D/3D fixtures; and graceful degenerate handling (zero torso, empty frames, NaN coords — no panic, never a false-perfect). **This is measurement infrastructure, not an accuracy claim** — the tests prove the harness is correct, not that any model is good. `wifi-densepose-train` lib 191→206, `test_metrics` 12→14, 0 failed. Python deterministic proof unchanged (off the signal proof path).
- **CI bench-regression guard (`.github/workflows/bench-regression.yml`) — wires the v2/ criterion benches into CI as a real, hard-failing COMPILE-VERIFY gate + an informational fast-run; caught + fixed one already-bit-rotted bench (benchmark/optimization milestone sub-deliverable 8.3; needs ADR slot 174).** The v2/ workspace ships **26 criterion benches across 18 crates** (e.g. `nvsim/pipeline_throughput`, `wifi-densepose-ruvector/{ann,sketch,fusion}_bench`, `wifi-densepose-signal/{signal,dsp_perf,features,calibration,aether_prefilter,cir}_bench`, `wifi-densepose-mat/detection_bench`, `wifi-densepose-nn/{inference,native_conv,onnx}_bench`, `wifi-densepose-engine/engine_cycle`, …) but, because benches are **not** part of `cargo test`, nothing in CI compiled them — so they silently rot when a public API they call changes. **Proof this matters (MEASURED):** running the new gate on the current tree immediately caught `wifi-densepose-mat/detection_bench` failing to compile (`E0063: missing field last_rssi in initializer of SensorPosition` — the struct gained a field, the bench was never updated); fixed in this change (`last_rssi: None`, the simulated-zone convention) and re-verified (`cargo bench -p wifi-densepose-mat --no-default-features --bench detection_bench --no-run` → `Finished`, Executable produced). **HONEST SCOPE — what gates vs what is informational:** (1) `bench-compile` (HARD GATE) runs `cargo bench --workspace --no-default-features --no-run` (compile + link every default-feature bench, no measurement) plus a `--features cir` compile of the gated `cir_bench` — a deterministic, real regression guard against bench bit-rot; (2) `bench-fast-run` (INFORMATIONAL, `continue-on-error: true`, NEVER gates) runs a curated pure-CPU subset (`nvsim/pipeline_throughput`, `ruvector/{sketch,fusion}_bench`) in criterion quick-mode (1s warm-up / 2s measure / 10 samples), targeted per-`--bench` (the crates' libtest lib targets reject criterion flags), and uploads the logs as an artifact. **No timing-regression gate, by design and stated in the workflow header:** wall-clock on shared GitHub runners varies 2-3x run-to-run, so a hard threshold or a cross-runner `criterion --baseline` compare would manufacture false failures; that becomes honest only on a frequency-pinned self-hosted runner (documented as the re-add condition). The `crv`-gated `ruvector/crv_bench` is deliberately NOT compiled by the gate because its crates.io dep `ruvector-crv 0.1.1` currently fails to build on stable (upstream E0308 in its own `stage_iii.rs`) — noted in-workflow with the re-add condition. Checkout is `submodules: recursive` (the workspace path-deps `vendor/rufield`) and installs the Tauri/GTK dev libs like `ci.yml`'s rust-tests job (a `--workspace` bench link pulls the whole graph). **MEASURED locally (Windows, `--no-default-features`):** `nvsim`, `wifi-densepose-ruvector` (sketch/fusion/ann), `wifi-densepose-signal/cir_bench`, `wifi-densepose-mat/detection_bench` (post-fix), `wifi-densepose-vitals/vitals_bench`, and `ruview-swarm/swarm_bench` all compile + the fast subset runs (sample baseline: `nvsim pipeline_run/d1/256` ≈ 55 µs, `d16/1024` ≈ 315 µs; `ruvector sketch_hamming` ≈ 3-7 ns vs `float_l2` ≈ 63-371 ns). The full `--workspace` `--no-run` could **not** be fully validated on Windows (Tauri-`desktop` needs GTK, `candle-core` fails on MSVC, `swarm_bench` LTO-links OOM under parallel pressure) — those are Windows-env artifacts that build in the Linux CI runner (each affected bench was confirmed to compile standalone here). No baseline JSON is committed (a cross-runner baseline would be dishonest). Python deterministic proof unchanged (`f8e76f21…46f7a`, bit-exact — off the signal proof path).
- **RuField `rufield-viewer` live-ingest mode — closes the RuView↔RuField visual loop (ADR-262 surfaces).** The dashboard gains `--source live --upstream <RuView-URL>`: it consumes RuView's `/ws/field` SSE (falling back to polling `/api/field`), **verifies every event's ed25519 provenance receipt on ingest** (`is_fusable`) — forged/tampered events are flagged ✗ and **never fused** into trusted inferences — and renders real RuView `FieldEvent`s through the same room-state/privacy-badge/fusion-graph/receipt path the synthetic mode uses (wire-compatible by construction: both sides use `rufield_core::FieldEvent` serde). **Strict banner honesty:** a single `BannerState` shows `SYNTHETIC` / `LIVE — <upstream>` / `DISCONNECTED — <upstream> unreachable`, mutually exclusive — never SYNTHETIC while showing live data or vice versa; live mode returns **409** on `/api/run` rather than fabricate a synthetic run, and starts DISCONNECTED until first verified contact. Default stays synthetic. 26 tests / 0 failed. `ruvnet/rufield` `crates/rufield-viewer`; `vendor/rufield` submodule bumped.
- **ADR-262 P3 — live RuField surface: RuView's running sensing-server now speaks RuField on `/api/field` + `/ws/field`.** Wires the P1 `wifi-densepose-rufield` bridge into the live `wifi-densepose-sensing-server` (the bridge is the only added coupling, ADR-262 §5.4). A new `src/rufield_surface.rs` module (kept out of the 8k-line `main.rs`) holds a `FieldSurface` with a **dedicated ed25519 `Signer`**, a bounded ring buffer of recent signed events (`FIELD_RING_CAPACITY = 64`), and the `/ws/field` broadcast topic; it exposes `GET /api/field` (latest signed `FieldEvent`s + signer pubkey + a `dev_signing_key` flag) and `GET /ws/field` (per-cycle stream, mirroring `/ws/sensing`), plus a standalone `router()` for isolated testing. **Tap:** at the ESP32 governed-trust cycle (`main.rs` `observe_cycle` ~`:5886` / `SensingUpdate` build ~`:5938`), `emit_rufield_event` joins the cycle's real `SensingUpdate` (features/classification/signal_field) with the engine's recorded `effective_class`/`demoted` trust state into a `SensingSnapshot` and surfaces a signed `FieldEvent` — **existing endpoints (`/ws/sensing` etc.) are unchanged; this is purely additive.** **Signer (defers the P2 key decision, §8 Q1):** a **standalone dev/sensing key** from `WDP_RUFIELD_SIGNING_SEED` (64-hex or ≥32-byte value), else a deterministic dev default with a logged `WARN` — reusing the `cog-ha-matter` Ed25519 key is the deferred P2 call, so P3 does not pre-empt it. **Egress privacy (fail-closed):** `network_egress_allowed` is *stricter* than `DefaultPrivacyGuard` for an unattended live surface — only **P1/P2** leave the box; P0 (raw) and P3/P4/P5 are held edge-local, so a `Derived → P4/P5` cycle **never** surfaces; no-presence cycles emit **no phantom event**. **P3 acceptance gates (`tests/rufield_surface_test.rs`, 4 integration via `tower::oneshot` + 4 module unit, 0 failed):** a well-formed **signed** event (`Modality::WifiCsi`, P2 not P1, `is_fusable` ed25519-verified, real timestamp); empty cycle → no phantom; **privacy-safety** — an injected `Derived` trust never surfaces; a mixed stream surfaces only egress-safe events. **Honest scope (ADR-262 §0/§6):** real plumbing on a **live endpoint**, **NOT accuracy** — single-link CSI with its existing caveats (no validated room-coordinate accuracy — `field_localize`), a dedicated dev signing key pending the P2 ownership decision, no accuracy claim. The win is narrowly: "RuView's live sensing now speaks RuField on `/ws/field`."
- **ADR-262 P1 — `wifi-densepose-rufield` anti-corruption bridge: RuView WiFi-CSI sensing → signed RuField `FieldEvent`s.** A new v2 workspace member (the *single coupling point* between RuView and the standalone RuField MFS spec, ADR-262 §5.4) that **path-deps** the `vendor/rufield` submodule crates (`rufield-core`/`-provenance`/`-privacy`/`-fusion` — pure-Rust, `--no-default-features`-buildable: serde/sha2/ed25519/toml only, no tch/openblas/ndarray/candle) and **no** RuView internal crate. The bridge takes owned primitives — `SensingSnapshot` mirrors the `/ws/sensing` `SensingUpdate` (features + classification + signal_field) joined with the `TrustedOutput` trust state (`trust_class`/`demoted`/`identity_bound`) — and `snapshot_to_field_event()` emits one **signed** `FieldEvent` (`Modality::WifiCsi`, axis `[Frequency]`): a real `FieldTensor` from the feature scalars with the real `timestamp_ns`; an `Observation` whose `range_m`/`motion_vector`/`space_cell` are derived from the strongest **signal-field peak** when present (else `None` — coordinates are **never fabricated**, per the `field_localize` caveat) and `confidence` from the classification; a real `ProvenanceRef` (sha256 over the tensor bytes, `synthetic=false`) **ed25519-signed** so `rufield_provenance::is_fusable` passes. **The §3.3 privacy mapping is the critical correctness item**, implemented as `map_privacy()` mapping RuView's class onto RuField P0–P5 **by information content, NEVER by byte value** and **fail-closed**: RuView `Derived` (byte `1`, which sorts *below* `Anonymous` byte `2`) carries an identity embedding → maps to **P4** (or **P5** if identity-bound), **never P1** (the single most dangerous mapping mistake); `Raw → P0`, `Anonymous → P2`, `Restricted → P2`; a governed-engine `demoted` cycle floors the egress class to ≥ P2 with raw suppressed. **P1 acceptance gates (15 tests / 0 failed — 5 unit + 9 integration + 1 doc):** round-trip (`SensingSnapshot → FieldEvent →` serde `→` equal), `is_fusable` (verified ed25519 receipt), `RuFieldFusion::ingest` accept + `infer()` runs, **privacy-safety** (`gate_privacy_safety_derived_never_maps_to_low_privacy` — `Derived → P4/P5`, never P1; a table test over every RuView class; fail-closed demotion), and determinism (same snapshot + same signer seed → byte-identical event). **Honest scope:** this is **P1 plumbing** — a tested conversion + a safe privacy mapping. It is **not** wired into the live server (that is P3) and makes **no accuracy claim** (RuField v0.1 is synthetic; RuView's single-link CSI carries its own caveats). CI: the `rust-tests` workflow checkout gains `submodules: recursive` so the path-deps resolve. Python deterministic proof unchanged (off the signal proof path).
- **ADR-262 (Proposed): RuField MFS ↔ RuView integration — a live `SensingServerAdapter`, a privacy/provenance bridge, MAPPED not papered-over.** Researched integration design for wiring RuField into RuView. Recommends: a thin **`wifi-densepose-rufield` bridge crate** (anti-corruption layer, path-deps on the `vendor/rufield` submodule — the `vendor/rvcsi` pattern, since rufield crates are unpublished); a **live `SensingServerAdapter`** that taps the real `SensingUpdate` emit site joined with `TrustedOutput` trust state and emits one signed `FieldEvent`/cycle (the file-based `CsiReplayAdapter` stays for offline replay); **vertical fusion composition** (ruvsense fuses *within* WiFi → one `wifi_csi` event → rufield-fusion graph fuses *across* modalities above it); and **one canonical privacy/provenance model** (RuView `effective_class` is source-of-truth, mapped to RuField P0–P5 at egress; reuse the existing `cog-ha-matter` SHA-256+Ed25519 chain for the `ProvenanceReceipt`). **Key honest finding:** RuView has **two privacy enums + three witness mechanisms across two hash algorithms** that do not map 1:1 onto P0–P5, and a real trap — RuView's `Derived` privacy byte (`1`) sorts *below* `Anonymous` (`2`) yet carries identity embeddings, so the bridge must map by **information content** (`Derived → P4/P5`), never by byte value, or it would leak identity as low-privacy P1. 4 independently-shippable phases, each with a test gate (round-trip / `is_fusable` / privacy-monotonicity / ed25519-verify). Honest scope: this is **plumbing architecture, not accuracy** — RuField v0.1 is synthetic and RuView's only real-CSI path is unlabeled replay; the ADR claims only architecture, gated by round-trip/monotonicity/signature tests.
- **RuField `CsiReplayAdapter` — first real (non-synthetic) WiFi-CSI adapter (ADR-260 §17).** RuField now ingests **real captured WiFi CSI** instead of only the synthetic simulator. New `rufield-adapters::csi_replay` parses RuView's `.csi.jsonl` recording format (`{timestamp, subcarriers[]}`), normalizes each frame to a `FieldTensor` (`WifiCsi`, real amplitudes + real `timestamp_ns`), establishes a per-subcarrier Welford **empty-room baseline** via `calibrate()`, derives a **physically-grounded CSI-variance motion/presence proxy** (normalized MAD vs baseline → P2 motion/presence, else P1), and emits `FieldEvent`s with a **real sha256 + ed25519 provenance receipt** (`synthetic=false`). **Measured on 199 real captured frames:** 184 presence-proxy / 69 motion-proxy → fed through `RuFieldFusion` → **182 fused inferences (115 breathing, 67 person_present) from real signal.** 12 tests (9 unit + 3 integration over real-CSI fixtures), deterministic (byte-identical stream per file). **Honest caveats (stated everywhere):** it's **replay from file, not live hardware**; recordings are **unlabeled**, so the motion/presence output is a **proxy, NOT validated accuracy** (no pose, no accuracy numbers); live streaming + labeled validation remain roadmap; mmWave/thermal stay synthetic. The win is "RuField ingests real WiFi CSI and produces fused events from it." [`ruvnet/rufield`](https://github.com/ruvnet/rufield) `crates/rufield-adapters`; `vendor/rufield` submodule bumped.
- **RuField `rufield-viewer` web dashboard — completes ADR-260 §27.9 (all §27 criteria 1–10 now PASS).** A read-only Axum + vanilla-JS dashboard (no build step — `cargo run -p rufield-viewer`) that streams the deterministic SyntheticSim→fusion camera-free room-intelligence demo: live room-state inferences with confidence, a scrolling event log where every event carries its modality + a colour-coded **P0–P5 privacy badge**, the fusion graph (supporting=green / contradicting=red per inference), and a click-to-open **provenance-receipt modal** (sha256 + ed25519 signer + verified ✓ / fusable ✓) — behind a permanent, undismissable `SYNTHETIC — simulated sensors, no hardware` banner. Endpoints `/` · `/app.js` · `/health` · `/api/run` (full deterministic JSON) · `/events` (SSE). 12 new tests. Honest scope: a read-only SYNTHETIC demo viewer, **not** a device-management console — fleet/real-adapter management is a separate later milestone. Lives in [`ruvnet/rufield`](https://github.com/ruvnet/rufield) (`crates/rufield-viewer`, repo now 7 crates / 72 tests); `vendor/rufield` submodule bumped to include it.
- **ADR-261: RuVector graph-ANN index — a real HNSW baseline + a SymphonyQG-style quantized variant, MEASURED (honest negative).** Closes the [ADR-156 §5 #1](docs/adr/ADR-156-ruvector-fusion-beyond-sota.md) gap: the SymphonyQG (SIGMOD 2025) **3.5–17× QPS-over-HNSW** claim was CLAIMED-only because **no HNSW baseline existed to compare against**. This adds one. New pure-Rust, `--no-default-features`-buildable modules in `wifi-densepose-ruvector`: `hnsw.rs` (a correct float HNSW — Malkov & Yashunin: multi-layer NSW graph, `ef_construction`/`ef_search`, Algorithm-4 neighbour selection, **seeded-deterministic** level assignment via SplitMix64, L2 + cosine, full degenerate-case guards), `hnsw_quantized.rs` (the SymphonyQG-style variant — the **same** graph traversed by a cheap **1-bit Hamming** score over the RaBitQ Pass-2 rotated sign code, then **exact-float rerank**), `ann_measure.rs` + `benches/ann_bench.rs` (one shared deterministic planted-cluster fixture; the `ann_bench_report` test is the source of truth). **MEASURED (dim=128, N=10k, K=10, `--release`):** float HNSW = **~25× QPS over linear scan at recall ≥0.99** (the baseline this gap needed; recall@10 correctness gate ≥0.95 holds, L2 + cosine). **Honest negative:** the 1-bit quantized traversal is **too coarse to beat float HNSW at equal recall at this scale** — its best recall is **0.738**, never reaching the ≥0.90 equal-recall point, so there is **no QPS win** over float HNSW; the 3.5–17× is **not reproduced** by our 1-bit construction here. The recall gate also **caught a real index-out-of-bounds bug** in the insert path (disclosed in ADR-261 §4). Caveat: this is **our** HNSW + **our** 1-bit quant, not SymphonyQG's exact system — it tests the *direction* of the claim, with the expected crossover at large N + a multi-bit traversal code. **We did not tune to manufacture a speedup.** +20 tests (ruvector lib 131→151, 0 failed). ADR-156 §5 #1 / §8 backlog: CLAIMED → **MEASURED-direction-tested**. Python deterministic proof unchanged (off the signal proof path).
- **ADR-261 Milestone-2: multi-bit quantized HNSW traversal + large-N scaling study — MEASURED (honest negative).** Extends ADR-261's quantized index from 1-bit to **`b`-bit-per-dimension** (`b ∈ {1,2,4}`, 16/32/64 B/node) over the Pass-2 rotated coordinates, and runs a deterministic scaling study (N ∈ {10k, 100k, 250k}) to test M1's *prediction* of a large-N crossover. **Result: no crossover at any measured (N, b), and the trend refutes the prediction.** At N=10k more bits lift the equal-recall QPS ratio (0.19×→0.46×→0.48×) and let b≥2 reach the 0.90 recall bar 1-bit missed — but quant stays slower than float HNSW at equal recall; at N=100k/250k quant recall *collapses* (b=4: 1.000→0.788→0.624, never ≥0.90) while float holds ≥0.92 (denser graph → low-bit codes can't separate near-neighbours, beam goes off-path faster than the float-distance saving repays). Caveat: our HNSW + our per-node multi-bit code, not SymphonyQG's RaBitQ-fused graph — refutes the *direction* at ≤250k, not their million-scale numbers. ruvector lib **151→156** (+5 tests; `scaling_report` `#[ignore]` produced the table). A published negative with the mechanism explained. ADR-261 §11.
- **ADR-260: RuField MFS — the open specification for camera-free multimodal field sensing.** A common event / tensor / calibration / privacy / provenance model that sits *above* WiFi CSI/CIR/BFLD, UWB, BLE Channel Sounding, mmWave radar, ultrasound, subsonic, infrared, and future quantum sensors (each modality emits a normalized `FieldEvent` → `FieldTensor` → `FusionGraph` → `PrivacyClass` → `ProvenanceReceipt`). Published as a **standalone repo** [`ruvnet/rufield`](https://github.com/ruvnet/rufield) and vendored here as the `vendor/rufield` submodule (the `vendor/rvcsi` pattern — not a `v2/` workspace member). The v0.1 reference stack is a self-contained 6-crate Rust workspace (`rufield-core`, `-provenance` [sha256 + ed25519], `-privacy` [P0–P5 guard], `-adapters` [deterministic `SyntheticSim` across wifi_csi/mmwave_radar/infrared_thermal], `-fusion` [graph + TOML weighted-Bayes rules → 7 room-state inferences], `-bench` [deterministic runner + the §31 acceptance test]). **60 tests / 0 failed, clippy-clean.** §27 acceptance criteria 1–8 and 10 PASS; the live dashboard (9) is deferred. **All benchmark metrics are SYNTHETIC** (scored against the simulator's own ground truth — presence/breathing/bed_exit/room_transition F1 = 1.000, nocturnal_scratch 0.923 reported honestly, p95 latency ~0.01 ms, provenance coverage 100%, 0 privacy violations) — they prove the pipeline recovers known truth, **not** field accuracy; real hardware adapters (ESP32 CSI, mmWave, thermal IR) are a documented roadmap item, none validated in v0.1. The Python deterministic proof is unchanged (rufield is off the signal-processing proof path).

### Security
- **`homecore-assist` voice/intent pipeline security review — one real unbounded-utterance DoS fixed (fail-closed length bound), pinned by fails-on-old tests; command-injection / ReDoS / NaN-poisoning / intent-confusion dimensions confirmed clean with evidence (ADR-133).** Beyond-SOTA review of the HA-compat Assist pipeline (utterance → recognizer → intent → handler → action, plus the `RufloRunner`) — the untrusted-input → action path, un-covered by the ADR-154–159 sweep. **One real finding fixed.** **HC-ASSIST-01 (unbounded-utterance DoS, LOW):** both `RegexIntentRecognizer::recognize` and the semantic `recognize_scored` accepted utterances of unbounded length from untrusted callers (voice transcripts / the WebSocket `assist` command) and ran `to_lowercase()` (a full clone) + a per-registered-pattern scan (and, in the semantic path, full tokenisation + feature-hash embedding) before any bound — an allocation/CPU amplification on attacker-controlled input. The `regex` crate is **linear-time** (no catastrophic backtracking), so this was a throughput/memory DoS, not a hang. **Fixed** by a named `MAX_UTTERANCE_BYTES = 4096` (far above any real spoken command) checked at both recognizer boundaries **before** any allocation/scan; an over-length utterance **fails closed** to `Ok(None)` (no intent, no action), identical to an unrecognised phrase, so it can never be coerced into firing a handler. Legitimate commands unaffected. Pinned by `over_length_utterance_fails_closed` (an over-length utterance that *contains* a valid command resolves to `None` — would have matched on old code) and `over_length_utterance_fails_closed_semantic`. **Dimensions confirmed clean (with evidence, no invented issues):** (1) **command/argument injection** — there is **no subprocess surface**: the `RufloRunner` has exactly two impls, `NoopRunner` (no process) and `LocalRunner` (runs the local recognizer, no process); no `std::process`/`tokio::process`/`Command`/`.spawn()` on any process exists in the crate (`spawn` is a `started: bool` lifecycle flag), and `RufloRunnerOpts.{script_path,env}` are inert data **never consumed** — the live `node ruflo-agent.js` runner is genuinely data-gated/future per the doc-comments. Additionally the `entity_id` capture class `[a-z_][a-z0-9_ .]*` **excludes every shell/SQL metacharacter**, so even when an injection-shaped utterance resolves (the regex is not exact-anchored) the captured slot is a clean token — sanitisation by construction (pinned by `shell_metachars_never_survive_into_a_resolved_slot`, `runner_opts_are_inert_no_process_spawned`, `pipeline_injection_shaped_utterance_carries_no_metachars_to_service`). (2) **ReDoS** — `regex 1.12.3` (no `fancy-regex` in the tree) is a linear-time finite automaton; a classic `(a+)+$` shape on adversarial input completes in bounded time (`pathological_backtracking_pattern_completes_in_bounded_time`). (3) **NaN-poisoning** — embeddings are **structurally finite** (FNV feature-hash + guarded L2 normalise, no external float input, no unguarded division), so a crafted utterance cannot inject NaN/Inf into the cosine k-NN; cosine vs the zero vector is a finite `0.0`; empty-index `max_by` returns `None` (no panic); the NaN-safe `partial_cmp().unwrap_or(Equal)` is already in place (`embeddings_are_structurally_finite`, `cosine_with_zero_vector_is_finite_not_nan`, `empty_utterance_against_empty_index_no_panic_no_match`). (4) **intent confusion / fail-closed** — an unrecognised utterance returns `not_understood()` (no service call), a recognised intent with no registered handler also returns `not_understood()`, semantic below-threshold/empty-index falls back to regex; no default high-privilege intent, no fail-open (`pipeline_injection_shaped_utterance_fires_no_handler` evidence + existing pipeline tests). (5) **panic-on-input** — no `unwrap`/`expect`/index reachable from a crafted utterance (the one `exemplars[id]` index uses an `id` from `enumerate()` over the append-only Vec). `cargo test -p homecore-assist --no-default-features`: **29→36 passed, 0 failed** (+7); default/`semantic`: **39→48, 0 failed** (+9). Workspace green; Python deterministic proof unchanged (homecore-assist is off the signal proof path). Review notes appended to ADR-133.
- **`homecore-automation` security review — two real DoS findings fixed (template unbounded-expansion + delay panic-on-config), each pinned by a fails-on-old test; condition-bypass / fail-closed / action-authz dimensions confirmed clean (ADR-129 §8a).** Beyond-SOTA review of the HA-compat automation engine (the execution/eval surface: triggers → conditions → actions, with user-config Jinja2 templates), un-covered by the ADR-154–159 sweep. **HC-SEC-01 (template DoS, HIGH):** a `template:` condition / `value_template` is user config and was rendered with MiniJinja's defaults — **no instruction budget, no output cap**. A single nested-loop condition rendered a **100 MB string in ~11 s on one render call** (measured) — the bfld-class unbounded expansion (MiniJinja's per-call `range()` 10k cap does **not** stop nesting). **Fixed** by enabling MiniJinja's `fuel` feature + `set_fuel(Some(1_000_000))` (the attack now fails fast ~90 ms with "engine ran out of fuel") and a 64 KiB source-length cap; legitimate templates unaffected. **HC-SEC-02 (panic-on-config DoS, MEDIUM):** `Action::Delay`/`WaitForTrigger` fed the user float straight into `Duration::from_secs_f64`, which **panics** on negative/NaN/inf/overflow — all reachable from a crafted or typo'd YAML (`delay: {seconds: -1}`, `.nan`, `.inf`, `1e308`), aborting the spawned run task (measured panic). **Fixed** by a `safe_duration_from_secs` guard that saturates (NaN/±inf/negative → `0`, matching HA's lenient "non-positive delay = no delay"; huge → clamped to ~100 yr). **Dimensions probed clean (evidence in ADR-129 §8a):** condition eval is **fail-closed** (template-render error → `false`; un-parseable `choose` branch condition → branch skipped, never silently passing); run-modes are **bounded** (Single/Restart/Queued/`max:N` — a self-triggering automation does not livelock, ADR-162 tests); templates are **read-only sandboxed** (no service-call/state-set global exposed to template scope, so a template cannot escalate to an action); no `unwrap`/`expect`/index panic reachable from a crafted config in the eval/exec path beyond the fixed `from_secs_f64`. Fails-on-old verified by reverting each fix in isolation (delay tests panic; template nested-loop test runs unbounded >60 s; oversized-source test fails). `cargo test -p homecore-automation --no-default-features`: **40 → 54 passed, 0 failed** (+14: 4 template-DoS, 1 no-regression render, 5 delay/wait + safe-duration unit). Workspace green; Python deterministic proof unchanged (homecore-automation is off the signal proof path).
- **`cog-ha-matter` witness/manifest crypto review — engine-class signed-digest collision confirmed ABSENT (length-prefixing already correct); domain-separation tag ADDED + `verify_strict` HARDENED; key-handling & verify-before-trust confirmed clean (ADR-116 §2.2).** Beyond-SOTA crypto+security review of the Cognitum/HA-Matter bridge's SHA-256 + Ed25519 witness chain — the exact signing chain ADR-262 P2 proposes to reuse — un-covered by the ADR-154–159 sweep. **Top-priority check: the sibling `wifi-densepose-engine` bug class (unframed boundary-to-boundary concatenation of operator-influenceable strings into a signed/hashed digest).** Result reported honestly: **that bug class is ABSENT here** — `witness::canonical_bytes` already length-prefixes the two variable-length operator-influenceable fields (`kind_len:u32-be ‖ kind`, `payload_len:u32-be ‖ payload`) over fixed-width `prev_hash[32] ‖ seq:u64-be ‖ ts:u64-be`, an injective encoding (proven pre-existing by `canonical_bytes_length_prefixing_prevents_ambiguity`), and `witness_signing::sign_event`/`verify_signature` sign/verify the **identical** bytes the hash chain commits to (no separate unframed concatenation). The manifest `binary_signature` (Ed25519 over the fixed 64-hex-char `binary_sha256`) is signed **at build time by the Makefile**, not in-crate, and over a single fixed-length value — no in-crate manifest-signing concatenation surface. **Two real hardening gaps fixed, the first pinned by fails-on-old tests:**
  - **CHM-WIT-01 (missing domain-separation tag, LOW) — ADDED.** The engine review's prescribed fix is "domain-tag **+** length-prefix"; the length-prefix half was present, the **domain tag was absent**. The witness SHA-256 preimage / Ed25519 message carried no tag distinguishing it from any other signing context that shares key infrastructure — notably the manifest `binary_signature`, the very chain ADR-262 P2 reuses. **Fix:** prepend a versioned, NUL-terminated `WITNESS_DOMAIN_TAG = b"cog-ha-matter/witness-event/v1\x00"` to `canonical_bytes` (the doc-comment already anticipated a leading version migration). Cross-protocol separation now holds: a witness signature can never be replayed as a message for another Ed25519 context. **Witness-bytes change by design** (prior on-disk witness hashes/signatures invalidated, like the engine fix) — verified safe: **no in-repo crate consumes cog-ha-matter's witness bytes/signatures programmatically** (all references are doc-comment mentions; the crate is self-contained, no `use cog_ha_matter::` anywhere). Pinned by `canonical_bytes_is_domain_separated`, `canonical_bytes_starts_with_domain_tag_then_prev_hash`, `witness_preimage_cannot_collide_with_a_bare_manifest_digest` (witness.rs) and `signature_commits_to_domain_tag_not_bare_fields` (witness_signing.rs — a signature over the **un-tagged** field concatenation must NOT verify); the domain-separation guard **FAILED on the reverted un-tagged encoding** ("canonical message is not domain-separated").
  - **CHM-WIT-02 (permissive Ed25519 verification, LOW) — HARDENED to `verify_strict`.** For a tamper-evident **audit** chain the signature is the attestation, so `verify_signature` now uses `VerifyingKey::verify_strict` (rejects non-canonical encodings + small-order public keys per RFC 8032) instead of the permissive `Verifier::verify` — giving auditors the "one canonical signature per event" property they rely on when comparing/deduplicating signed records. Not a forgery fix (the public key is caller-pinned, never parsed from the event), reported at true LOW severity. Guarded by `verify_uses_strict_path_and_pins_caller_key`.
  - **Dimensions confirmed clean (with evidence, no invented issues):** (1) **verify-before-trust + key-pinning** — `verify_signature` takes the verifying key as a **caller-supplied parameter** (the Seed's known key), never reads a key from the event/manifest, so a forged event carrying its own key cannot self-attest; `WitnessChain::read_jsonl` re-derives and re-checks every `this_hash` on load (tampered bundle → `HashMismatch`) and runs a chain-level `verify()` catching reordered/spliced events (existing `verify_rejects_*`, `jsonl_parser_rejects_tampered_payload`, `read_jsonl_chain_verify_catches_reordered_events`). (2) **key handling** — the crate **never generates, stores, logs, or serializes** a signing key: `sign_event` takes `&SigningKey` by reference, the manifest struct has no key field, and the only key material in-crate is the **test-only** fixed seed (clearly documented "DO NOT use in production"); production keys come from the Seed's secure key store (out of scope, ADR-116 §key-management). No hardcoded/default/predictable production key, no key in the manifest, no world-readable key path (the crate does no key file I/O). (3) **determinism/canonicalization** — `canonical_bytes` is pure positional bytes (no HashMap iteration, no float formatting); Ed25519 is deterministic (pinned by `signature_is_deterministic_for_same_event_and_key`); the JSONL wire form is hand-rolled with **alphabetically-locked** field order (`jsonl_field_order_is_alphabetical_for_byte_stability`) and the mdns TXT records are `sort()`-ed for byte-stable advertisement — no iteration-order or float-format nondeterminism feeds any hash/signature. (4) **fail-closed parsing / DoS** — `from_jsonl_line`/`from_hex`/`hex_decode` return structured errors (never panic) on wrong length, non-hex, missing field, odd-length payload, or hash mismatch (`jsonl_parser_rejects_non_hex_hash`, `hex_decode_rejects_odd_length`, …); `main.rs` reads no untrusted files/paths (clap args only; `--print-manifest` emits a static template) — no path/injection surface. (5) **de-magic** — the witness/signing byte layout is already expressed as named widths; no bare security-relevant literals worth extracting beyond the new named `WITNESS_DOMAIN_TAG`. `cog-ha-matter --no-default-features`: **64→68 tests**, 0 failed (+3 domain-tag witness, +1 signing-layer domain-commit, +1 strict-verify key-pin; one pre-existing test renamed to assert the tag). Workspace green; Python deterministic proof unchanged (`f8e76f21…46f7a`, bit-exact — cog-ha-matter is off the signal proof path). Review notes appended to ADR-116 §2.2.
- **`homecore-api` (HA-wire-compat REST + WebSocket) beyond-SOTA security review — `GET /api/` auth-gate gap FIXED + WS event-stream lag-DoS robustness FIXED; auth/traversal/injection/info-leak dimensions confirmed clean (ADR-161 / ADR-130).** Network-facing review of the HA-wire-compat API layer (remote attack surface), not covered by the ADR-154–159 sweep — same scrutiny the sibling `wifi-densepose-engine` and `-bfld` reviews got. **Two real bugs fixed, each pinned by a fails-on-old test.**
  - **HC-API-AUTH-01 (auth-gate gap, LOW) — `GET /api/` was unauthenticated; FIXED.** Every sibling REST route (`/api/config`, `/api/states`, `/api/services`, …) calls `BearerAuth::from_headers` first, but `rest::api_root` took no headers and unconditionally returned `200 {"message":"API running."}`. HA's `APIStatusView` inherits `requires_auth = True`, so an unauthenticated/wrong-token request to `/api/` must be **401** — HA clients use this status route as a token-validation probe, and a 200 both told a bad-token client its token was good and let an unauthenticated party confirm a live endpoint. Severity is LOW (the body is a static string — no entity/state data leaks), reported at true severity, not inflated. **Fix:** `api_root` now validates the bearer like its siblings. Pinned by `api_root_rejects_missing_bearer` + `api_root_rejects_wrong_bearer` (both 200→assert-401 on old code) and guarded by `api_root_accepts_correct_bearer`.
  - **HC-WS-LAG-01 (DoS-adjacent silent failure, LOW) — `subscribe_events` killed the event stream on a broadcast lag; FIXED.** The per-subscription task matched `Err(_) => break` on both `broadcast::Receiver::recv()` arms, but `Lagged(n)` (a slow consumer falling >4,096 events — `EVENT_CHANNEL_CAPACITY` — behind) is **recoverable**: the bus doc itself says "Lagged receivers must re-sync", and HA's WS contract keeps the subscription alive across a lag. The old code treated the first lag as fatal, so after an event burst the client's stream went **permanently silent** with no error frame — a self-inflicted event-delivery DoS under load. **Fix:** `Lagged(_) => continue` (skip the dropped window, re-sync), `Closed => break`, on both the system and domain arms. Pinned by `subscription_survives_broadcast_lag` (subscribes, floods 6,000 filtered events past the 4,096 capacity to force a `Lagged`, then asserts a subsequent subscribed event is still delivered — 5s-timeout panic on old code).
  - **Dimensions confirmed clean (with evidence, no invented issues):** (1) **AuthN/AuthZ** — all 7 other REST handlers (`get_config`/`get_states`/`get_state`/`set_state`/`delete_state`/`get_services`/`call_service`) gate on `BearerAuth::from_headers` → `LongLivedTokenStore::is_valid` before any work; the WS handshake validates the `auth` token against the **same** store before entering the command loop and the privileged commands are unreachable pre-`auth_ok` (HC-WS-01, already fixed). Token compare is a `HashSet::contains` (content-independent timing, not the byte-`==` oracle ADR-157 §B4 fixed in hardware) — no timing-oracle finding. No route skips the gate, no result-ignored check, no default/empty token accepted (`is_valid` rejects empty internally; `from_env` is non-dev). (2) **Path traversal** — **no route maps user input to a filesystem path** (state lives in an in-memory `DashMap`); `:entity_id` is funneled through `EntityId::parse`, a strict `[a-z0-9_]+\.[a-z0-9_]+` ASCII allowlist that rejects `..`, `/`, `\`, and absolute paths. No traversal surface exists. (3) **Injection** — no SQL, no shell/subprocess, no `format!`-into-response; `call_service`/`set_state` bodies are typed `serde_json::Value` passed to the in-process service registry (matches HA). (4) **Info-leak** — `ApiError` maps to fixed status + a `{message}` derived only from typed variants; `call_service`'s `ServiceError::HandlerFailed(String)` is integration-controlled (mirrors HA surfacing the handler error), not framework internals/paths/stack-traces (no ADR-080-class leak). (5) **CORS** is an explicit allowlist (`allow_credentials(false)`, HC-05 already fixed), not `permissive()`. (6) **De-magic** — no bare security-relevant literals in this crate worth extracting (`EVENT_CHANNEL_CAPACITY` already named in `homecore`; CORS dev-default ports are documented). `homecore-api --no-default-features`: **25→29 tests**, 0 failed (+2 api-root auth, +1 api-root accept-guard, +1 WS lag-survival); workspace green; Python deterministic proof unchanged (homecore-api is off the signal proof path). Review notes appended to ADR-161.
- **`wifi-densepose-calibration` per-room calibration review — NaN-poisoning fail-closed gap FIXED + file/path & receipt surfaces confirmed clean (ADR-151).** Beyond-SOTA correctness+security review of the ADR-151 `baseline → enroll → extract → train → bank` pipeline (the appliance-deployed per-room specialist core), un-covered by the ADR-154–159 sweep. **One real numerical-robustness bug fixed.** `Features::from_series` — the live-inference *and* training feature path — computed `mean`/`variance`/`motion` over the raw scalar series with **no non-finite guard**, so a single `NaN`/`±inf` sample (a corrupt CSI frame) produced `mean=NaN, variance=NaN` and an all-`NaN` prototype embedding. Baked into a persisted `PresenceSpecialist::threshold`/`empty_mean` at train time, that `NaN` **silently disabled presence detection** for the life of the bank (every `f.variance > NaN` and `|mean − NaN|` comparison is false → presence always reads *absent*, confidence 0), with **no error raised** — the exact "produce NaN that poisons a specialist / silently accept garbage" failure, and an asymmetry vs the meticulously NaN-guarded `geometry_embedding.rs`. **Fix at the production boundary:** filter non-finite samples before any statistic (a corrupt frame counts as no frame); a wholly-non-finite series degrades to the new `Features::ZERO`, exactly like the empty series. **Value-identical for all-finite input** — `full_loop.rs` and every existing `extract` test pass unchanged. Pinned by two fails-on-old tests (`non_finite_samples_do_not_poison_features`, `all_non_finite_series_is_zero`, both FAILED pre-fix). **Dimensions confirmed clean (with evidence, no invented issues):** (1) **file/path handling** — the crate does **zero** file/path I/O (no `std::fs`/`Path`/`File`/`read`/`write` anywhere in `src/`; only in-memory `serde_json`), so path-traversal / unbounded-read / artifact-path concerns do not exist at the crate boundary — they live in the `wifi-densepose-cli` consumer (`room.rs`), out of this crate's scope; (2) **untrusted-load** — `SpecialistBank::from_json` parse-validates shape via serde (malformed → `CalibrationError::Serde`), and per ADR-151 invariant (B) banks are local-first, never network-received; (3) **receipt/hash integrity** — the crate emits **no** hash/receipt/witness/signature (no `CalibrationReceipt` analogue), so the engine's unframed-concatenation bug class is structurally absent — nothing to mis-frame; (4) **other numerical paths already robust** — `geometry_embedding.rs` sanitizes every input + sweeps to finite (verified by its `adversarial_inputs_never_produce_nan` test); presence/restlessness/anomaly divisions are all `.max(1e-3)`-guarded; `autocorr_dominant` guards `r0 ≤ 1e-6`, `n < 16`, empty bands; `SpecialistBank::train` rejects empty anchors; anomaly requires ≥2 anchors. De-magicked the bare specialist threshold literals (breathing 0.25 / heartbeat 0.3 default min-scores, anomaly 2.0× spread / >0.5 label cutoff) into named documented consts, value-identical, pinned by `default_min_score_constants_match_prior_literals` + `anomaly_constants_match_prior_literals`. `wifi-densepose-calibration --no-default-features`: **58→62 unit tests** (+2 NaN fail-closed, +2 de-magic pins) + 1 full-loop integration, 0 failed. Python deterministic proof unchanged (`f8e76f21…46f7a`, bit-exact — calibration is off the signal proof path). Review notes appended to ADR-151 §6.
- **`wifi-densepose-engine` governed-trust review — witness domain-separation gap FIXED + privacy monotonicity confirmed clean (ADR-137 / ADR-141 / ADR-032).** Beyond-SOTA correctness+security review of the security-critical composition root (the cycle enforcing RuView's privacy guarantees), not covered by the ADR-154–159 sweep. **One real witness-integrity bug fixed.** `witness_of` concatenated `model_version`, `calibration_version`, and `privacy_decision` boundary-to-boundary and left the variable-length evidence list without a count, so a string straddling a field boundary collided with a *different* trust decision — e.g. a per-room adapter id (ADR-150 §3.4, operator-influenceable) absorbing the leading bytes of the calibration epoch (`model="…cal:00a"`,`cal="b"`) yields the same witness as `model="…"`,`cal="cal:00ab"`. Two distinct privacy-relevant input tuples → one witness defeats the ADR-137 §2.7 "any privacy-relevant delta → different witness" tamper/drift audit. **Fix:** domain-tag the BLAKE3 hash (`ruview.engine.witness.v1`), write an explicit evidence count, and **length-prefix every field** (8-byte LE length ‖ bytes) — unambiguous framing regardless of contents. Witness-layout change by design (prior witness bytes invalidated); downstream consumers (`engine_bridge`, rufield) assert only witness *relationships* (`assert_ne`/`assert_eq` across runs), never absolute bytes, so nothing breaks. Pinned by two fails-on-old tests: `witness_distinguishes_model_calibration_boundary`, `witness_distinguishes_evidence_model_boundary`. **Dimensions confirmed clean (with evidence, no invented issues):** (1) **privacy monotonicity** — `effective_class` is recomputed each cycle from the active mode's floor with at most a single-step `demote_one` (clamped at `Restricted`), no cross-cycle state, proven over **all 5 modes** by `forced_contradiction_never_relaxes_class` (forced contradiction only ever raises the class byte; clean cycle == base); (2) **fail-closed** — empty cycle errors with no degenerate output (`empty_cycle_fails_closed`), single-node boundary characterized (`single_node_cycle_is_well_formed`), NaN coupling → `max(0.0)`→absent edge→at-risk (more restrictive); (3) **witness determinism** — no HashMap iteration / float formatting feeds the hash; (4) **mesh_guard** (ADR-032) — partition-risk → demotion path verified, thresholds already named documented fields. De-magicked the engine-construction literals (coherence accept gate, ADR-143 SLAM discovery + static-anchor thresholds) into named documented consts, value-identical, pinned by `engine_constants_match_prior_values`. `wifi-densepose-engine --no-default-features`: **27→33 tests**, 0 failed (+2 witness, +1 monotonicity property, +2 fail-closed boundary, +1 de-magic pin). Python deterministic proof unchanged (`f8e76f21…46f7a`, bit-exact — the engine is off the signal proof path). Review notes appended to ADR-137 (witness) and ADR-141 (monotonicity).
- **ADR-141 BFLD privacy-bypass closed — `process_to_frame` now routes the payload through `PrivacyGate` (`wifi-densepose-bfld`).** `BfldPipeline::process_to_frame` stamped the emitted `BfldFrame` header with the active `PrivacyClass` but serialized the caller-supplied `BfldPayload` **unchanged** via `BfldFrame::from_payload`. A frame labeled `Anonymous`(2) or `Restricted`(3) therefore carried the full identity-leaky `compressed_angle_matrix` (the beamforming-angle identity surface) + amplitude/phase proxies + `csi_delta` — exactly the sections `PrivacyGate::demote` is documented and tested (`privacy_gate_demote.rs`) to strip at those classes. Because a `NetworkSink` accepts class ≥ `Derived`(1), such a frame would publish the identity surface across the node boundary despite its restrictive class byte; the class byte lied about payload content. **Fix:** after building the frame at the active class, apply `PrivacyGate::demote` to the same class — a no-op class transition that strips the sections that class forbids (research classes `Raw`/`Derived` keep the full payload). Pinned by three fails-on-old tests in `pipeline_to_frame.rs` (`…_at_anonymous_strips_identity_leaky_sections`, `…_in_privacy_mode_strips_amplitude_and_phase` — both FAILED pre-fix; `…_at_derived_preserves_full_payload` guards against over-stripping). Grade: privacy-bypass FIXED + regression-pinned.
- **ADR-157 Milestone-1 B4 - constant-time HMAC sync-beacon tag compare (`wifi-densepose-hardware`).** `AuthenticatedBeacon::verify` compared the 8-byte HMAC-SHA256 tag with `self.hmac_tag == expected`, which short-circuits on the first differing byte and leaks, through verification latency, how many leading bytes an attacker's forged tag matched - a byte-by-byte tag-recovery oracle (~256*N trials instead of 256^N). Replaced with a hand-rolled branch-free `constant_time_tag_eq` (XOR-accumulate every byte difference into a single `u8`, no early exit, `#[inline(never)]` + `core::hint::black_box` to stop the optimizer reintroducing a short-circuit or a non-constant-time `memcmp`). **No new dependency** - ADR-157 had deferred this only to avoid adding the `subtle` crate; a fixed 8-byte compare needs none. Grade MEASURED (constant-time *construction*; micro-timing on a noisy host is a smoke check only, gated `#[ignore]`). Pinned by `tag_compare_is_constant_time_shape` (equal/first-differ/last-differ/all-differ/length-mismatch + an end-to-end `verify()` last-byte tamper), proven to fail on a last-byte-skipping constant-time bug. ADR-157 §8 B4 -> RESOLVED.
- **ADR-080 open HIGH findings closed on the Rust `wifi-densepose-sensing-server` boundary (ADR-164 G11).** The QE sweep's three HIGH findings — XFF-spoofing bypass, leaked stack traces, JWT-in-URL (CWE-598) — were logged against the Python v1 API and never re-verified against the shipped Rust sensing-server; the HOMECORE/M7 sweep (ADR-161) covered `homecore-server`, not this crate.
  - **#2 leaked internal errors (the one live exposure) — FIXED.** Six handlers in `main.rs` serialized the internal error `Display` straight into the JSON response body: `edge_registry_endpoint` returned a panicked `spawn_blocking` `JoinError` (`"task … panicked"`) in a `500`, plus the raw upstream error in a `503`; `delete_model`/`delete_recording`/`start_recording` returned `std::io::Error` strings (OS detail / path); `calibration_start`/`calibration_stop` returned the `FieldModel` error chain. New `error_response` module logs the full detail **server-side only** (with a correlation id) and returns a generic body (`{"error":"internal_error","correlation_id":…}`) — no `panicked`, no file paths, no Debug chain. 5 module tests (a leak-substring guard proven to fail on the reverted old body) + the existing handler suite.
  - **#1 XFF-spoofing bypass — VERIFIED ABSENT, regression-pinned.** The sensing-server has no XFF-trusting control to bypass: there is no IP-based rate-limiter or IP-allowlist, and neither `bearer_auth` (token-only) nor `host_validation` (Host-header only) reads `X-Forwarded-For`/`X-Forwarded-Host` (no `forwarded`/`peer_addr`/`client_ip` anywhere in the crate). Added regression tests proving a spoofed `X-Forwarded-For` never flips an auth decision and a spoofed `X-Forwarded-Host` never bypasses the Host allowlist.
  - **#3 JWT-in-URL (CWE-598) — VERIFIED ABSENT, regression-pinned.** `require_bearer` reads the token only from the `Authorization` header; the WebSocket handlers take no token query param and the sole `Query` extractor (`EdgeRegistryParams`) is a non-secret `refresh` flag. Added a regression proving `?token=`/`?access_token=` in the URL never authenticates while the header path still does.

### Fixed
- **`wifi-densepose-geo` numerical-robustness audit — `parse_hgt` degenerate-input panic FIXED + `haversine` antipodal NaN FIXED; pole-singularity & pointcloud NaN-state-poisoning confirmed clean (ADR-154-class sweep).** Targeted numerical-robustness audit of `wifi-densepose-geo` + `wifi-densepose-pointcloud`, hunting the proven non-finite-input-poisons-persistent-state class. **Two real bugs in `geo`, each pinned by a fails-on-old test.** (1) **`terrain.rs::parse_hgt` usize-underflow panic** — `side = sqrt(n_samples)`; for an empty / sub-2x2 buffer `side ≤ 1`, so `1.0 / (side - 1)` underflows `usize` (panic "attempt to subtract with overflow" in debug; wraps to a huge value in release → garbage/inf `cell_size_deg` that then poisons every `ElevationGrid::get` lookup). A truncated SRTM download, a 404 HTML body, or an empty response all reach `parse_hgt` — now `bail!`s with a clear error when `side < 2`. Pinned by `parse_hgt_empty_data_errors_not_panics` (panicked pre-fix) + `parse_hgt_single_sample_errors` (returned inf pre-fix) + a `parse_hgt_minimal_2x2_is_finite` guard. (2) **`coord.rs::haversine` asin-domain → NaN** — for (near-)antipodal points floating rounding can push `h.sqrt()` to `1.0 + ~4e-16`, and `asin(>1)` is NaN, silently breaking every downstream `<`/`>` distance comparison (verified: pair `(-44.4994,-178.95722)→(44.49939999,1.04278001)` yields `h=1.0000000000000004`). Fixed by clamping into `[0,1]` before `asin`. Pinned by `haversine_near_antipodal_is_finite_not_nan` (NaN pre-fix). The ±90° pole-singularity (`cos(lat)=0` division in the ENU transforms) is pinned as no-panic without changing the transform (value-identical for valid inputs). **`wifi-densepose-pointcloud` is confirmed-robust — no bug, no manufactured finding:** the only persistent auto-accumulating state (`occupancy` EMA, vitals) is fed exclusively from the integer-rssi/`sqrt`/`atan2` parser, which can only emit finite values, and the persistent state is provably self-healing even under an adversarial hand-built `CsiFrame` carrying NaN/inf amplitudes+phases (`motion_score=(NaN/100).min(1.0)→1.0`; breathing path `→0→clamp(5,40)→5.0`; tomography EMA uses only integer rssi). Pinned by `nonfinite_frame_does_not_poison_persistent_state` (injects 40 poisoned frames, asserts occupancy/vitals stay finite + the pipeline recovers) and three degenerate-voxel-fusion no-panic tests (empty/single/all-coincident). `wifi-densepose-geo --no-default-features`: 9→15 lib (+6), 8 integration unchanged; `wifi-densepose-pointcloud`: 18→22 (+4); 0 failed; workspace green; Python proof unchanged (`f8e76f21…46f7a`, bit-exact — both crates off the signal proof path).
- **Vitals IIR filters self-heal after a non-finite CSI frame — a single NaN/inf no longer permanently kills breathing & heart-rate extraction (`wifi-densepose-vitals`, safety; ADR-021 / ADR-158 §A1).** The 2nd-order resonator in `breathing::BreathingExtractor::bandpass_filter` and `heartrate::HeartRateExtractor::bandpass_filter` latches each output `y[n]` into the filter state (`y1`/`y2`). A non-finite input — one NaN/inf amplitude residual from a corrupt CSI frame — produced a NaN `output` that was written into the state. The existing `extract()` `is_finite()` guard correctly dropped that single sample from history, **but never sanitized the poisoned filter state**, so every subsequent output stayed NaN, was rejected too, and the sliding-window history *never refilled*: the extractor went silently dead (returning `None` forever) until `reset()`. On the vitals alert path this is a safety-relevant denial of service — one bad frame and breathing **and** heart-rate monitoring stop, with no error surfaced. Fix: when `bandpass_filter` computes a non-finite `output` it now resets the IIR state to default and returns `0.0`, so the resonator recovers on the next clean frame (the `0.0` is still dropped by the caller's finite-check — no spurious sample enters history). Same class as the calibration NaN bug (ADR-154 §3) and the firmware vitals fixes (#998/#996/#987): the prior hardening guarded the *history boundary* but not the *filter-state boundary*. Pinned by `breathing::tests::nan_frame_does_not_permanently_poison_filter`, `breathing::tests::inf_mid_stream_does_not_freeze_history`, and `heartrate::tests::nan_frame_does_not_permanently_poison_filter` (all three FAIL on the pre-fix code, verified by reverting). Also de-magicked the safety-critical HR physiological plausibility band into named `HR_PLAUSIBLE_MIN_BPM`/`HR_PLAUSIBLE_MAX_BPM` consts (value-identical 40/180 BPM, pinned by `plausibility_band_constants_pinned`) and added a fabricated-vital negative (`pure_noise_is_never_reported_valid` — broadband noise never yields a clinically `Valid` HR). `wifi-densepose-vitals --no-default-features`: 55→60 lib tests, 0 failed; workspace green; Python proof unchanged (vitals is off the deterministic proof's signal path).
- **BFLD MQTT `zone_activity` payload now JSON-escapes the zone name (`wifi-densepose-bfld`).** `mqtt_topics::render_events` emitted the zone payload as `format!("\"{zone}\"")` with no escaping, while `ha_discovery.rs` already escapes operator-controlled strings. A zone name containing a `"` or `\` produced malformed/injectable JSON on the Home-Assistant state topic (e.g. zone `a"b` → payload `"a"b"`). Added a `json_string_literal` escaper mirroring `ha_discovery::push_str_field` and applied it to the zone payload — value-identical for normal zone names (`living_room`, …). Pinned by `zone_payload_escapes_json_metacharacters` (FAILED pre-fix; round-trips through `serde_json`); the existing `zone_payload_is_json_string_with_quotes` still passes unchanged.
- **ESP32 vitals: `n_persons` over-counted (reported 4 for one person) + presence flag flickered at close range (#998, #996).** Two firmware logic bugs in `firmware/esp32-csi-node/main/edge_processing.c`, both robustness/logic fixes — **not** validated-accuracy claims (true count/PCK vs labelled ground truth stays hardware/data-gated on the COM9 ESP32-S3).
  - **#998 over-count — root cause + fix.** `update_multi_person_vitals()` split the top-K subcarriers into `top_k_count/2` groups and marked **every** group `active` unconditionally, so one body's multipath always reported the full `EDGE_MAX_PERSONS` (=4). New pure, host-testable `count_distinct_persons()` gates each candidate group: (1) **energy gate** — a group's phase variance must be ≥ `EDGE_PERSON_MIN_ENERGY_RATIO` (0.35) × the strongest group's, so weak multipath echoes don't count; (2) **spatial dedup** — groups whose representative subcarriers sit within `EDGE_PERSON_MIN_SC_SEP` (4) of each other are the same body. A `person_count_debounce()` then requires the gated count to hold `EDGE_PERSON_PERSIST_FRAMES` (3) consecutive frames before it's emitted, so a single noisy frame can't promote a phantom. The strongest group always counts (a present body yields ≥1). All thresholds are named, documented constants in `edge_processing.h`.
  - **#996 presence flicker — root cause + fix.** Presence was a bare `score > threshold` compare on a noisy `presence_score` (field-observed 2.6–26.7 frame-to-frame for one stationary person), so the boolean chattered at the boundary while the score clearly indicated a person. New pure `presence_flag_update()` is a Schmitt trigger + clear-debounce: assert above `threshold`, **hold** in the dead band down to `threshold × EDGE_PRESENCE_HYST_RATIO` (0.5), and only clear after the score stays below the low threshold for `EDGE_PRESENCE_CLEAR_FRAMES` (5) consecutive frames. The score itself is unchanged (and still emitted at packet offset 20 for consumer-side thresholding). Constants named/documented in `edge_processing.h`.
  - **Tests:** `firmware/esp32-csi-node/test/test_vitals_count_presence.c` (host C99, `make run_vitals`) — 13 cases / 22 assertions, all passing under gcc 13 `-Wall -Wextra`. Pins: single-strong-signature + multipath → count==1; two well-separated → count==2; two strong-but-adjacent → 1 (dedup); transient count spike rejected; sustained change accepted; dithering presence trace → stable flag (no flicker); genuine departure → clears within hold window. The named tuning constants are `#include`d from the real header so the test and firmware can't disagree. **Hardware-gated caveat:** these pin the decision *logic*; the exact energy/separation/hysteresis values that best match a real room vs labelled occupancy remain on-device tuning (COM9 ESP32-S3 + ground truth).
- **Observatory 3D figure never animated — `/ws/sensing` omitted per-person `position`/`motion_score`/`pose` (#1050).** The `sensing_update` frame shipped `nodes`/`features`/`classification`/`signal_field` and a `persons[]` carrying only image-space `keypoints`/`bbox`/`zone`; the Observatory's `FigurePool`/`PoseSystem` (and `demo-data.js`'s own contract) animate each figure from `persons[i].position` (room-world `[x,y,z]`), `persons[i].motion_score` (0..100), and `persons[i].pose`, none of which the live stream emitted — so the figure sat static while signal metrics updated. **Honest scope (Case 2 — no calibrated per-person localizer exists):** a single ESP32 link does not produce calibrated room-coordinate localization or per-person skeletal pose, so the fix emits only what is *truthfully derivable*. New `field_localize` module reads the **strongest peak(s)** out of the frame's real `signal_field` grid (already built from measured subcarrier variances × measured motion-band power) and maps the peak cell to Observatory world coordinates with the **exact** `_buildSignalField` transform (`x=(ix−nx/2)·0.6`, `z=(iz−nz/2)·0.5`, `y=0`), so the figure lands on the field hotspot it stands on. `motion_score` is the measured `motion_band_power` passed through (clamped 0..100); `pose` is set **only** from a real aggregate `posture` estimate when one exists, else `None` (never a fabricated skeleton — per-person pose keypoints in room coordinates stay gated on the pose model + ADR-079 paired data). An empty / below-threshold field yields `persons: []` (no phantom person); a present person on a field with no resolvable peak keeps `position=[0,0,0]` (not invented coords) while `motion_score` stays real. `attach_field_positions` runs after the tracker step at all five broadcast sites. **No UI change required** — the Observatory already reads these fields and defaults `pose`→`'standing'` when absent. New `PersonDetection.position`/`motion_score`/`pose` fields added to both the `main.rs`-local and `types.rs` structs. Pinned by 10 tests: `field_localize` peak-extraction/coordinate-mapping/empty-field/separation unit tests + `observatory_persons_field_position_tests` (`sensing_update_emits_persons_with_field_derived_position` feeds a synthetic field with a known peak at cell (15,4) and asserts the emitted `position` = `[3.0, 0, −3.0]` within tolerance; `empty_room_yields_no_phantom_person`; `pose_is_real_when_posture_present_and_absent_otherwise`; `present_but_below_threshold_field_keeps_position_at_origin_not_fabricated`). `wifi-densepose-sensing-server --no-default-features`: bin **441→451**, 0 failed; workspace green; Python proof unchanged (off the deterministic proof path).
- **ADR-155 Milestone-1b — metric-definition unification, the §8 backlog subset (Goals A/B/C).** Closed the two §8 metric-integrity items; every change pinned by a test, graded MEASURED. The audit (Goal A) also surfaced findings the §1 table under-counted — recorded honestly in ADR-155 §8.1, not hidden. Workspace stays green; Python proof unchanged (metrics are not on the deterministic proof's signal path).
  - **Goal B — `test_metrics.rs` now validates the production metric, not a reimplementation.** The integration test previously asserted properties of its OWN local `compute_pck`/`compute_oks` (a test that can't catch a canonical-impl bug — both could be wrong the same way). Hoisted the canonical core (`pck_canonical`/`oks_canonical`/`canonical_torso_size`/sigmas/`bounding_box_diagonal`) into a new **un-gated** `metrics_core` module so the single definition is reachable under `cargo test --no-default-features` (the `metrics` module is `tch-backend`-gated); `metrics` re-exports it → still exactly ONE implementation. Rewrote the test to assert the production `pck_canonical`/`oks_canonical` equal **hand-computed** fixtures (`canonical_pck_matches_hand_computed_fixture` = 3/4 correct ⇒ 0.75; hip↔hip normalizer pin; zero-visible⇒0.0; OKS perfect⇒1.0; fake-Gold pin) plus a differential cross-check (`test_kernel_agrees_with_canonical`: an independent raw-threshold kernel must AGREE with canonical where torso==1.0). `wifi-densepose-train --no-default-features`: test_metrics **10→12**, 0 failed.
  - **Goal C — divergent live-server PCK/OKS relabelled so they're never conflated with canonical.** Goal C named `training_api.rs:804` (torso-HEIGHT PCK); the audit found that file is an **orphan (not `mod`-declared, does not compile)** and the **real** live `best_pck`/`best_oks` come from `trainer.rs` — a **raw, unnormalized** `pck_at_threshold` and an **`area=1.0` fake-Gold** `oks_map` (both MISSED by ADR-155 §1, both on the claim-inflating side, both serialized as bare "PCK@0.2"/"OKS"). Torso-height/raw math is load-bearing (pixel-space, different scale axis, no `ndarray`/train dep), so the honest fix is **relabel, not force-unify**: `training_api.rs` `compute_pck` → `compute_pck_torso_height` + field/log docs; `trainer.rs` kernels documented raw/fake-Gold; `main.rs` prints `pck_raw@0.2` / `oks_map(area=1.0 proxy)`. No wire-format field or `pub`-fn renames (no silent API break). Pinned by `torso_pck_is_labelled_distinctly_from_canonical` + `pck_at_threshold_is_raw_unnormalized_not_canonical`. `wifi-densepose-sensing-server --no-default-features`: lib **450→451**, 0 failed. True unification onto `pck_canonical`/`oks_canonical` remains a tracked ADR-155 §8 item.
- **Pre-existing `SketchBank::topk` heap inversion returned the FARTHEST sketches (found during ADR-156 §8 Pass-2 work).** The `n > k` partial-sort path in `wifi-densepose-ruvector/src/sketch.rs` used `BinaryHeap<Reverse<(dist,id)>>` (a min-heap) but its eviction logic treated the peek as the max, so it kept the k *farthest* sketches and returned them as "nearest." The shipped unit tests only exercised the `n ≤ k` fast path (≤ 3 entries), so the inversion shipped silently in ADR-084. Fixed to a plain max-heap. Pinned by `topk_heap_path_returns_nearest` (farthest-first insertion exposes it) and `tight_clusters_give_high_coverage_with_overfetch` (**measured 0.072 coverage on the old code** — effectively random — vs >0.99 fixed). Every ADR-084 top-K coverage number depends on the fixed path. MEASURED, not a no-op.
- **ADR-154 Milestone-1 — cleared the P1 deferred backlog in `wifi-densepose-signal` (§7.4 #1, #10; partial #9, #13).** Each fix pinned by a regression test that fails on the old behaviour; every claim graded MEASURED / DATA-GATED; no fabricated thresholds. Python proof unchanged (`f8e76f21…46f7a`, bit-exact — the CIR ghost-tap guard is not on the deterministic proof path).
  - **#1 (MEASURED metric / DATA-GATED threshold): circular phase variance.** `cir.rs::phase_variance` computed a *linear* sample variance over phase angles that wrap at ±π, so a tightly-clustered set straddling the branch cut reported spuriously HIGH dispersion — false-tripping the `> TAU` ghost-tap **guard** on real, tightly-clustered CIR taps. Replaced with Mardia's **circular variance** V = 1 − R̄, bounded **[0,1]** and invariant to where the cluster sits on the circle. The old TAU-scaled threshold is meaningless on [0,1]; re-derived against a named const `GHOST_TAP_CIRCULAR_VARIANCE_MAX = 0.99` (fires only when R̄ ≤ 0.01 — essentially uniform phase). The **metric is MEASURED**; the **threshold value is DATA-GATED** (a clean single-path ramp also sweeps the circle, so V alone can't separate clean from unsanitized without labelled frames — the default is deliberately conservative, strictly more permissive at the wrap boundary than the buggy linear guard). Fails-on-old: `phase_variance_circular_not_fooled_by_branch_cut` (old linear variance > TAU on wrap-straddling phases while circular V≈0, guard no longer trips) + `phase_variance_circular_is_bounded_and_extremal` (V∈[0,1], V≈0 identical, V≈1 uniform).
  - **#10 (MEASURED): Welford n=0/n=1 finiteness guard pinned.** The shared `WelfordStats` (`field_model.rs`) `count < 2` guards keep `variance`/`sample_variance`/`std_dev`/`z_score` finite at the boundaries, but the n=0 case was untested (same family as the §4 divide-by-(n−1) trio). Added `welford_finite_at_n0_and_n1` — finite + documented-sentinel (0.0) at n=0/n=1. Fails-on-old proof: removing the `sample_variance` guard makes the test panic with "attempt to subtract with overflow" at the `(count − 1)` underflow (guard restored).
  - **#9, #13 (DATA-GATED): de-magicked thresholds + boundary tests (values UNCHANGED).** Lifted the bare detection literals in `adversarial.rs` (`check`/`check_consistency`: Gini 0.8, energy ratios 2.0/0.1, consistency 0.1·mean, score weights), `coherence.rs::classify_drift` (0.85, 10) and `coherence_gate.rs` defaults (0.85/0.5/200/3.0) into named, documented consts marked EMPIRICAL DEFAULT pending labelled calibration. Added characterization/boundary tests pinning each decision at/just-below/just-above its threshold (`energy_ratio_high_boundary`, `energy_ratio_low_boundary`, `field_model_gini_boundary`, `consistency_active_fraction_boundary`, `classify_drift_*_boundary`, `*_consts_unchanged_from_literals`) so a future labelled-data retune is a visible, tested change. The operating **values were not changed**; the de-magicking + tests are MEASURED, the values stay DATA-GATED.
- **Multistatic fusion guard was too tight for real TDM hardware (#1031).** `MultistaticConfig::default().guard_interval_us` was 5,000 µs (5 ms) with a comment claiming "well within the 50 ms TDMA cycle" — but on a real N-slot TDM schedule node `k` transmits in slot `k`, so two nodes are separated by the *slot offset*, not clock jitter. A real 2-node mesh (slots 0/1) measured an **18,194 µs** spread, so every real frame set exceeded the 5 ms guard and `fuse()` silently fell back to per-node sum/dedup — multistatic fusion never actually ran on hardware. Raised the default hard guard to **60 ms** (a full 50 ms TDMA cycle + 20% jitter headroom, derived from the slot model and documented in the field doc) and the soft guard to **20 ms** (just above the observed 18.2 ms 2-slot spread, so a normal cycle fuses cleanly with no privacy demotion). Added `MultistaticConfig::for_tdm_schedule(total_slots, slot_duration_us)` to derive the guard from a deployment's exact schedule, and a `WDP_TDM_SLOTS`+`WDP_TDM_SLOT_US` env seam in sensing-server. The honest per-node fallback remains for genuinely-mismatched frames — now the exception, not the default. Pinned by `fuse_real_tdm_spread_18194us_fuses_with_default_guard` (fails on the old 5 ms default) + `configurable_guard_rejects_too_large_spread` (guard still rejects a spread beyond one cycle).
- **Published HuggingFace model was unloadable — RVF format mismatch (#894).** The `ProgressiveLoader` rejected the published `ruvnet/wifi-densepose-pretrained` model with the opaque `invalid magic at offset 0: expected 0x52564653 (RVFS), got 0x77455735`, then silently fell back to signal heuristics (the "10 persons for 1" garbage reporters saw). The HF repo ships `model.safetensors`, `model-q{2,4,8}.bin` (magic `0x77455735` = "5WEw"), and `model.rvf.jsonl` — none carry the binary-RVF magic. New `model_format` module **auto-detects** RVFS / safetensors / HF-quant-bin / JSONL by magic+name, returns a **typed actionable** `ModelLoadError` (lists accepted formats + the one-command convert path — never the opaque magic), and **converts** `model.safetensors` / `model.rvf.jsonl` → RVF in-memory so the published full-precision model now loads via `--model`. A `--convert-model <in> --convert-out <out>` CLI subcommand gives a one-command offline path; the silent heuristics fallback is now a loud, actionable error. **Honest scope:** the converter wires the format/load path (safetensors F32 tensors → RVF weight segment, manifest written, Layer A/B/C all succeed, weights round-trip) — it does **not** claim end-to-end pose accuracy, since the HF pose-decoder architecture differs from this crate's inference head (still data-gated in #894). Quantized `.bin` blobs are rejected with a typed error pointing at the safetensors path. Pinned by `safetensors_converts_and_loads` + `hf_quant_classifies_to_actionable_error` (both fail on the old opaque-magic path).

### Changed
- **ADR-157 Milestone-1 §5 #4 - native `wlanapi.dll` multi-BSSID throughput MEASURED on real hardware (`wifi-densepose-wifiscan`).** The ADR's prior status ("asserted but NOT implemented; live scanner is the ~2 Hz netsh shim") is now stale: `wlanapi_native.rs` already implements the real `WlanOpenHandle` -> `WlanEnumInterfaces` -> `WlanGetNetworkBssList` -> `WlanFreeMemory`/`WlanCloseHandle` FFI and `WlanApiScanner` already wires it native-first with a netsh fallback. This milestone **measured it on this box** (Intel Wi-Fi 7 BE201 320MHz, 2026-06-13): a new `benchmark_backend(backend, window)` drives each backend over the same fixed 10 s wall-clock window so netsh is timed independently (the prior `benchmark()` picked native-first and never measured netsh on a Windows box where native works). **MEASURED: native 21.42 Hz vs netsh 3.84 Hz = 5.57x** (mean 5.0 BSSIDs/scan, both paths); a separate native-only run measured 18.0 Hz. Native genuinely beats netsh - this is a real positive result, not a fabricated "10x". 50 back-to-back native scans completed 50/50 with no handle leak/degradation. Live-WLAN tests (`measure_native_vs_netsh_throughput`, `native_scans_dont_leak_handles`, `measure_native_scan_rate`) are `#[ignore]` for CI but were RUN here; `native_scan_runs_real_ffi_on_windows` is a non-ignored schema-valid pin. ADR-157 §5 #4 + §8 -> MEASURED (was ACCEPTED-FUTURE / CLAIMED-unmeasured).
- **Mesh partition risk now demotes the privacy class and is witnessed (ADR-032).** The dynamic min-cut guard's `at_risk` signal was advisory-only (it fed the recalibration advisor). It now also contributes to the ADR-141 privacy demotion alongside fusion- and array-level contradictions: a mesh close to partitioning makes the fused belief less trustworthy, so the cycle emits at a more restricted class (monotonic — information only removed). Because `effective_class` feeds the BLAKE3 witness, a fragmenting array now shifts the witness — partition risk is auditable, not just logged. The mesh computation moved ahead of the demotion step in `process_cycle`; new `mesh_guard_mut()` exposes risk-threshold tuning. Test proves a forced-risk 3-node cycle demotes PrivateHome Anonymous→Restricted and shifts the witness vs a clean *same-topology* baseline (the only delta between the two cycles is the forced risk).

### Added
- **ADR-155 Milestone-2 — cleared the host-verifiable subset of the §8 P3 backlog in `wifi-densepose-train` (+ the pure-Rust `rf_encoder.rs`/`densepose.rs` the §3/§4 items named).** Mirrors the ADR-154 M3 cleanup discipline. **Honest enumeration first (grep, not the ADR's "~40" estimate):** the actual non-tch train/nn surface is smaller — **7 de-magicked (const + `*_consts_unchanged_from_literals` pin == prior literal), 9 boundary/characterization tests, 1 added input guard (`rf_encoder::LinearHead::try_new`) + test, 2 doc-only fixes, 1 perf item bench-first → MEASURED-INCONCLUSIVE (not shipped)**. **This is cleanup — no operating value or behaviour changed:** each lifted literal is bit-identical to its prior value, each boundary test pins CURRENT behaviour. De-magicked: `metrics_core.rs` (`VISIBILITY_THRESHOLD`/`MIN_REFERENCE_EXTENT`/`OKS_FALLBACK_SIGMA`), `ruview_metrics.rs` (`NUM_KEYPOINTS`/`VISIBILITY_THRESHOLD`/`PCK_THRESHOLD`/`MIN_BBOX_DIAG`/`MIN_DURATION_MINUTES`), `subcarrier.rs` (6 `SPARSE_*` consts), `eval.rs` (`MIN_POSITIVE_MPJPE`), `domain.rs` (`LAYER_NORM_EPS`), `virtual_aug.rs` (`BOX_MULLER_U1_FLOOR`/`MIN_ROOM_SCALE`), `rf_encoder.rs` (`SOFTPLUS_LINEAR_THRESHOLD`). **§3 `rf_encoder.rs`:** added a pure-Rust fallible `LinearHead::try_new` → typed `RfHeadError` so untrusted/deserialized checkpoint weights can be shape-validated without the `new()` panic (`new` unchanged; additive). **§4 native-conv:** `densepose.rs::apply_conv_layer` (pure-Rust naive loop) was benched (committed `benches/native_conv_bench.rs`); a bit-identical range-clamped rewrite measured ~35% faster on padding-heavy small-channel maps but ~3% *slower* on channel-heavy maps, all inside a ±20% host-noise floor — **MEASURED-INCONCLUSIVE, so NOT shipped** (no fabricated number), characterized by `native_conv_matches_reference` and honestly deferred. **Skipped honestly (not-real / already-handled):** `ablation.rs` (NaN-sort + boundaries already fixed/tested in M1), `signal_features.rs` (consts already named, n=0 tested), `mae.rs` (no bare guard literals). `wifi-densepose-train --no-default-features`: **303 passed** (was 288, +15), 0 failed; `wifi-densepose-nn --no-default-features` lib: **38** (was 35, +3). Workspace `--no-default-features`: GREEN (single clean run). Python proof **VERDICT: PASS**, hash **`f8e76f21…46f7a` UNCHANGED, bit-exact** (asserted — the metrics path is off the deterministic signal proof path). **Remaining §8 backlog stays deferred-not-dropped:** GraphPose-Fi / ONNX-INT4 / CSI-JEPA (data/model-gated), ONNX read-lock (upstream `ort`-gated), tch-gated panic sites in `proof.rs`/`trainer.rs`/`model.rs` + `metrics.rs` `*_v2` dead-code (tch-gated — need a libtorch host). **The non-tch-verifiable subset of §8 is now cleared.**
- **ADR-154 Milestone-3 — cleared the §7.4 row #21–45 P3 backlog in `wifi-densepose-signal` (the lumped "remaining clarity/doc/magic-constant/missing-boundary-test findings across `ruvsense/*`, `features.rs`, `motion.rs`").** Honest enumeration first (grep, not the ADR's estimate): the lumped row was **~25 findings → 22 real, de-magicked across 11 modules; 6 boundary/characterization tests added; ~4 doc-only; the rest were already-handled or not-real and are reported as such** (the "row #21–45" count was an estimate — there were not 25 *distinct* magic constants left after M0–M2). **This is cleanup — no operating value or behaviour changed:** every de-magicked literal becomes a named, documented EMPIRICAL-DEFAULT const that **equals the prior literal exactly** (each module ships a `*_consts_unchanged_from_literals` pin test), and every boundary test pins **current** behaviour so a future retune is a visible, tested change. Modules touched: `motion.rs` (#18, fusion weights/normalization/adaptive-threshold consts + 5 tests), `gesture.rs` (#12, `euclidean_distance` length-mismatch `debug_assert` documenting the silent-truncation contract + DTW n=0/m=0 boundary), `longitudinal.rs` (drift thresholds 7-day/2σ/3-day/7-day/EMA + day-6/7 + zero-vector cosine), `cross_room.rs`/`multiband.rs`/`intention.rs`/`hampel.rs` (division-guard epsilons + zero-norm/zero-variance/zero-MAD boundary + `half_window==0` error path), `rf_slam.rs` (`NS_PER_DAY` + fixed-map defaults + zero-span guard), `attractor_drift.rs` (buffer/recent-window consts + documented the implicit `recent.len()≥1` divide-safety + `min_observations` off-by-one boundary), `coherence.rs` (#9 completion — variance-floor + default-decay), `calibration.rs` (#2 — `DEFAULT_MIN_FRAMES` deduped across 4 tier constructors + motion/subtract thresholds), `fusion_quality.rs` (contradiction penalty/bounds + n=0 identity), `temporal_gesture.rs` (confidence epsilon + quantization scale). **A "magic" the agents flagged that was NOT real:** an `attractor_drift.rs:301` "divide-by-zero" is unreachable (the `count < min_observations` guard guarantees `recent.len()≥1`) — documented + boundary-tested rather than guarded, per the no-behaviour-change rule. Signal crate lib `--no-default-features`: **476 passed, 0 failed, 1 ignored**; `--no-default-features --features cir`: **476 passed, 0 failed** (plain `--features cir` is unbuildable on this Windows host — the default `eigenvalue` feature pulls `openblas-src`, the same BLAS gate documented in M2 #8). Workspace `--no-default-features`: **3,275 / 0 failed** (single clean run). Python proof **VERDICT: PASS**, hash **`f8e76f21…46f7a` UNCHANGED, bit-exact** (asserted explicitly — these modules are off the deterministic PSD/Doppler proof path, and the de-magicked consts are bit-identical regardless). **This clears ADR-154's §7.4 deferred backlog to zero across M0–M3.**
- **ADR-154 Milestone-2 — bench-first P2 perf subset + missing boundary tests (`wifi-densepose-signal`, §7.4 #5/#6/#7/#8/#14/#16/#19/#20).** PROOF discipline (ADR-154 §0): every perf item was **benched before being touched** (new committed `benches/dsp_perf_bench.rs`, criterion, this Windows box); only the one item the bench proved hot was optimized, the rest are committed MEASURED-NULLs — a benched null is the proof the micro-opt was unnecessary, the §5.1 "already amortized" pattern. Every behaviour-changing edit is pinned bit-identical (or documented-tolerance). Signal crate lib `--no-default-features`: **447 passed, 0 failed, 1 ignored**; `--features cir`: **447 passed, 0 failed**.
  - **#20 MEASURED-HOT, optimized (bit-identical).** `compute_multi_subcarrier_spectrogram` re-planned a fresh `FftPlanner` for *every* subcarrier (via `compute_spectrogram`). Hoisted the plan + window out of the per-subcarrier loop (new `compute_spectrogram_with_plan` core; `compute_spectrogram` delegates, unchanged). **56-subcarrier: 467.88 µs → 254.75 µs = 1.84×** (window 128); **627.27 µs → 448.39 µs = 1.40×** (window 256). Bit-identical via `multi_subcarrier_hoisted_plan_bit_identical` (`f64::to_bits` of every value across all 4 window functions × {power,magnitude}). The §7.4 intro's predicted "most likely real win" — confirmed.
  - **#5 / #6 / #7 MEASURED-NULL, left as-is.** `node_attention_weights` 181 ns (2 nodes)…848 ns (8) — sub-µs, no hot-path alloc. `tomography reconstruct` (full 50-iter ISTA, 256 voxels) 47.5 µs (16 links) / 60.4 µs (32) — the 2 voxel buffers are already alloc-once + `.fill`-reused, negligible vs O(iters·links·voxels). `pose_tracker` Kalman cycle 150 ns (17 keypoints) / 2.82 µs (170) — the "gain matrices" are fixed-size **stack** arrays, zero heap to reuse. No rewrite shipped; the committed benches prove each is not hot.
  - **#8 MEASUREMENT-ONLY, BLAS-gated (number deferred, not fabricated).** Correction to the finding: `extract_perturbation` does **not** recompute the SVD (it projects against cached `finalize_calibration` modes); the real per-call eigendecomposition is the `eigenvalue`-feature `estimate_occupancy` (`cov.eigh()` on a 56×56 covariance). The `eig` bench is committed but `openblas-src` won't build on this Windows host ("Non-vcpkg builds are not supported on Windows" — the exact reason the project gate runs `--no-default-features`), so its µs cost must come from a Linux/BLAS box. Recorded, not estimated. Incremental SVD stays a sized future item.
  - **#14 / #16 / #19 RESOLVED — tests added (no behaviour change).** `fft_operator_within_tolerance_of_dense_canonical56` pins the full `Cir` output of the opt-in FFT path within a documented relative tolerance of the dense path on the production canonical-56 config (τ ∈ {20,50,90} ns) — it changes the witness hash, so it must be provably *close*, not silently divergent. `refinement_terminates_at_iteration_cap_when_not_converging` (+ convergent companion) proves the LO-offset refinement terminates at exactly `max_iterations` on a non-converging input (cap, not convergence, bounds the loop; internal `…_counted` refactor returns the identical offsets). `ratio_finite_at_and_below_1e_12_epsilon` pins that the conjugate-product CSI-ratio (no division → no `1e-12` divide-guard needed) is finite + bit-exact at/below the epsilon boundary and at exact zero (where a naive `H_i/H_j` ratio is ±inf/NaN).
- **ADR-156 §11 Milestone-2: RaBitQ unbiased distance estimator — IMPLEMENTED & MEASURED (RESOLVED-NEGATIVE on the strict-K bar).** Closes the §10.5 / §8 backlog "full RaBitQ residual-distance estimator (not just a uniform scalar code)" item — the **real** Gao & Long (SIGMOD 2024) contribution, not just sign bits. New `wifi-densepose-ruvector/src/estimator.rs`: `EstimatorSketch` carries the Pass-2 sign code (over the padded FHT length `D = next_pow2(dim)`) **plus 8 B/vec side info** (`residual_norm` + `x_dot_o = ⟨x̄, o'⟩`, 2× f32); `DistanceEstimator` computes the **unbiased** estimate `⟨o',q'⟩ ≈ ⟨x̄,q'⟩ / x_dot_o` (the random rotation makes the 1-bit code's quantization error orthogonal-in-expectation to the query, paper `O(1/√D)` bound); `EstimatorBank::topk_estimated_cosine` reranks the candidate set by the estimate instead of raw Hamming. **Zero-centroid simplification (`c = 0`) stated honestly** — the paper-faithful per-cluster centroid path (`from_embedding_centred` / `EstimatorBank::with_centroid`) is also built so the simplification is a measured choice (no centroid coverage number is reported against the cosine ground truth, because cosine-of-residual ≠ cosine-of-raw would be a metric mismatch). **Purely additive + backward-compatible** — new types only; Pass-1 `Sketch` / Pass-2 `SketchBank` / `WireSketch` wire format unchanged; all external callers (`event_log.rs`, `signal/longitudinal.rs`, `sensing-server`) use Pass-1 and are unaffected. **MEASURED strict-K coverage** (same fixture/seeds as §10: dim=128 N=2048 K=8, 64 clusters, noise=0.35, 128 queries, cosine ground truth): the estimator lifts the strict `candidate_k=K` bar **46.39% (Pass-2 sign) → 49.71% (estimator, cosine rerank)** — a real **+3.3 pp** lift, **still ~40 pp short of the ADR-084 ≥90% strict bar.** At over-fetch the estimator beats sign (candidate_k=24: **95.12%** vs 91.60%). **Honest verdict — RESOLVED-NEGATIVE: the unbiased estimator does NOT clear the strict-K 90% bar on this distribution** (the binding constraint is the 1-bit code's information ceiling, not estimator variance); the bar is still met only via the over-fetch "candidate set" pattern ADR-084 specifies, though the estimator **reduces the over-fetch factor** needed. A published negative, reported as such — no benchmark tuned to manufacture a pass. Unbiasedness pinned by `estimator_unbiased_on_fixture` (Monte-Carlo mean over 4000 rotation seeds → true inner product within tolerance); not-worse-than-sign pinned by `estimator_rerank_not_worse_than_sign`; determinism by `estimator_is_deterministic`. +12 tests in the crate (119→131). Workspace **3,228 / 0 failed** (`cargo test --workspace --no-default-features`, 162 test binaries, single clean run), Python proof **VERDICT: PASS** (`f8e76f21…46f7a`, unchanged — estimator is not on the proof's signal path). Full numbers + reproduce commands in ADR-156 §11 / ADR-084 "Pass 2b".
- **ADR-156 §8 Milestone-1: RaBitQ Pass-2 randomized rotation + multi-bit experiment — IMPLEMENTED & MEASURED (RESOLVED-PARTIAL).** Closes the §8 "Multi-bit / Extended RaBitQ" backlog item. New `wifi-densepose-ruvector/src/rotation.rs`: a deterministic randomized orthogonal rotation `R = H·D` — **Fast Hadamard Transform** (`O(d log d)`, in-place, `1/√m`-normalized so norm-preserving) + seeded ±1 sign flips (SplitMix64 from a stored `u64` seed; identical at index + query time). Chosen over a dense `d×d` matrix (`O(d²)`, infeasible at the 65,535-d the wire format provisions for); pads to `next_pow2(d)`. Additive, backward-compatible API (`Sketch::from_embedding_rotated`, `SketchBank::with_rotation` + `insert_embedding`/`topk_embedding`/`novelty_embedding`); Pass-1 and the wire format are byte-for-byte unchanged. New `coverage.rs` single-source-of-truth top-K coverage harness (anisotropic planted-cluster fixture, cosine ground truth) backs both a `#[test]` report and the `sketch_bench` coverage table. **MEASURED (dim=128 N=2048 K=8, 64 clusters, noise=0.35, 128 queries, seeded):** at the strict `candidate_k=K` bar, rotation lifts coverage **36.13% → 46.39%**; Pass-2 reaches the **ADR-084 ≥90% bar at candidate_k=24 (~3× over-fetch)**; multi-bit Pass-3 reaches 54%/67%/74% at 2/3/4-bit (strict bar). **Honest verdict: neither rotation nor ≤4-bit multi-bit clears the strict-K 90% bar on this distribution — the bar is met only via the over-fetch "candidate set" pattern ADR-084 specifies.** No benchmark was tuned to manufacture a pass; the strict-bar gap is documented (ADR-156 §10, ADR-084 "Pass 2" section). +19 tests in the crate (100→119), workspace **3,225 / 0 failed**, Python proof VERDICT: PASS (`f8e76f21…`, unchanged — sketch is not on the proof's signal path).
- **Beyond-SOTA `v2/crates/` sweep (ADR-154–158) + full stub-implementation push — every claim MEASURED or graded.** A 5-milestone review/optimize/secure/benchmark/validate sweep, then a verified-audit-driven push to replace every production stub with real, tested logic (no labels, no placeholders). Each fix is pinned by a test that fails on the old code; every number ships with a reproduce command. Workspace: **3,122 tests / 0 failed** (`cargo test --workspace --no-default-features`), Python proof **VERDICT: PASS** (bit-exact).
  - **ADR-154 Signal/DSP** — revived a dead ADR-134 CIR coherence gate (canonical-56 vs ht20 mismatch meant it never ran in production: 8/8 Err → 8/8 Ok); NaN-bypass + window div0 guards; PSD FFT-planner cache (**2.0–3.1×**) + honored DTW band (**2.4–4.1×**).
  - **ADR-155 NN/Training** — unified 7 divergent PCK/OKS metric definitions into one canonical torso-normalized source (fixed two claim-inflating bugs: zero-visible PCK 1.0→0.0, OKS fake-Gold); leak-free subject-disjoint MM-Fi split + injected-leak detector; rapid_adapt replaced fake gradients with real finite-difference; proof.rs gained a min-decrease margin + committed-hash requirement; zero-copy ORT input (**1.48×**).
  - **ADR-156 RuVector/Fusion** — closed crafted-input DoS panics (triangulation/heartbeat); honest dimensionless GDOP = √(trace(G⁻¹)) replacing an RMSE mislabel; canonical wrapped angular distance; fuse() double-clone removed (**~2.17×** marshalling). SOTA graded: SymphonyQG (CLAIMED), multi-bit RaBitQ (near-term), GraphPose-Fi (data-gated).
  - **ADR-157 Hardware/Sensing** — `Vec::remove(0)` O(n²) sliding windows → `VecDeque`; breathing partial-weight renormalization; IIR low-sample-rate divergence clamp. Centerpiece: a MEASURED **negative-results** audit showing the layer (802.11bf model, parsers, calibration) was already hardened — cited file:line, NO-ACTION.
  - **ADR-158 MAT/world-model** — **unified two divergent triage engines** (the confidence-gated result was computed then discarded; gate==record now); **killed survivor count-inflation** (real RSSI localization + vitals-signature dedup, MEASURED 3→1); real ESP32/UDP/PCAP CSI ingest with honest typed `HardwareUnavailable`/`UnsupportedAdapter` errors for hardware-gated adapters (Intel5300/Atheros/PicoScenes — never fabricated CSI); real parabolic peak interpolation; real GDOP.
  - **Soul Signature §3.6 matcher made real (`wifi-densepose-bfld`, issue #1021).** An external audit correctly found person-identification was spec-only behind a no-op `NullOracle`. Now a real per-channel weighted-cosine matcher + `EnrolledMatcher: SoulMatchOracle` (364 tests). MEASURED: same-person 1.0000 vs cross-person 0.8088; and the audit's own claim proven — on WiFi-only cardiac+respiratory channels alone two people are **not separable** (gap 0.0005). Named identity is honestly **data-gated** on the AETHER/body-resonance channel being fed by a real enrollment; no working-named-identity claim is made.
  - **OccWorld real forward pass** — replaced `Tensor::randn` encoder/decoder stubs (which emitted trajectory priors from pure noise) with a real deterministic conv VQ-VAE forward pass (input-dependent, proven by tests that fail on the old randn) + a `weights_trained` honesty flag (false until a real checkpoint loads); pointcloud `to_gaussian_splats` 9→2 passes (**1.24×** MEASURED).
  - **Native multi-BSSID `wlanapi.dll` FFI** (`wifi-densepose-wifiscan`) — real `WlanOpenHandle`/`WlanEnumInterfaces`/`WlanGetNetworkBssList`, **MEASURED 9.74 Hz** on Windows (vs netsh ~2 Hz; no fabricated "10×"), typed `Unsupported` off-Windows. Real Matter 1.3 manual-pairing-code field-packing (canonical 34970112332, lossless decode) replacing a lossy-modulo placeholder.
  - **HOMECORE assistant** — real `LocalRunner` response path, real semantic intent recognizer (exact in-memory cosine k-NN; MEASURED 0.855 match / 0.106 no-match), real SQL state text-search — three always-empty stubs removed.
- **ADR-152 WiFi-Pose SOTA 2026 intake — verified external benchmark + four Rust integrations.** A 22-source adversarially-verified survey of the 2025–2026 WiFi-sensing SOTA, with every adopted number reproduced or graded before integration:
  - **WiFlow-STD (DY2434) reproduction (`benchmarks/wiflow-std/`)** — the external "97.25% PCK@20, 2.23M params" claim audited end-to-end: the **shipped checkpoint is REFUTED** (0.08% PCK@20 — wrong keypoint normalization, predates the published code), the released code does not run as published (6 documented defects, incl. an import that fails and an unreachable test phase), and the released dataset's final 13 files are corrupted (9,072 windows of NaN + float32-max garbage that NaN-poisons fp16 BatchNorm training). After repairing both, retraining with upstream defaults on an RTX 5080 reproduced **96.09% PCK@20 (full test) / 96.61% (corruption-free)** — claims graded MEASURED-EQUIVALENT; params (2,225,042) and FLOPs (~0.055 G) verified exactly. Full forensics in `benchmarks/wiflow-std/RESULTS.md`.
  - **`GeometryEmbedding` (ADR-152 §2.1.2, `wifi-densepose-calibration`)** — 32-slot permutation-invariant, NaN-proof featurization of the §2.1.1 `NodeGeometry` records (centroid/spread, measured-first pairwise distances, circular azimuth stats, covariance-eigenvalue geometric diversity, per-node flags), schema-versioned for the ADR-151 P6 LoRA heads; derived `SpecialistBank::geometry_embedding()` accessor. The PerceptAlign "coordinate overfitting" defense, transplanted to per-room banks.
  - **MAE pretraining recipe (ADR-152 §2.3, `wifi-densepose-train/src/mae.rs`)** — `MaePretrainConfig` pinning the UNSW-measured recipe (80% masking, (30,3) patches) with pure-Rust patchify/random-mask (exact counts, seed-deterministic, error-not-truncate divisibility, NaN rejection), property-tested; the consumption seam for the future ADR-150 ViT-Small encoder.
  - **`WiFlowStdModel` Rust port (`wifi-densepose-train/src/wiflow_std/`)** — tch-gated idiomatic port of the verified spatio-temporal-decoupled architecture (grouped causal TCN → asymmetric conv stack → dual axial attention); ungated param formula asserted equal to the reference 2,225,042; 15/17-keypoint variants share weights (enables the ADR-152 §2.2(b) ESP32 fine-tune).
  - **RuVector vendor sync + §2.6 opportunity survey** — vendor at `a083bd77f`; graded ADOPT/EVALUATE/WATCH table; crates.io bumps applied (mincut/solver 2.0.6, attention 2.1.0, gnn 2.2.0; RUSTSEC #504 audit: no pinned crate affected); top WATCH: unpublished `ruvector-graph-condense` differentiable min-cut for trainable subcarrier grouping.
- **ADR-153 IEEE 802.11bf-2025 forward-compatibility protocol model (`wifi-densepose-hardware/src/ieee80211bf/`)** — typed WLAN-sensing procedures (measurement setup/instance/report, SBP, termination) with `SpecProfile` version gates, `SensingCapabilities` negotiation, and **required** `ConsentMode` governance metadata on every setup; deterministic session FSM with rejection/timeout paths; `SensingTransport` seam with `SimTransport` and an `OpportunisticCsiBridge` mapping live ESP32 CSI batches into standardized report shape (a future chipset adapter replaces the bridge without touching RuvSense consumers). Not a certified implementation — simulation-tested protocol surface; OTA binding lands when silicon does. 19 acceptance tests.
- **Dynamic min-cut mesh partition guard in the streaming engine (`mesh_guard`).** Maintains a `ruvector-mincut` exact min-cut over the live mesh coupling graph (nodes = sensing nodes, coupling = product of fusion attention weights), surfacing per cycle: the global **cut value** (how close the array is to splitting — a structural measure per-node heuristics miss), the **weak side** (which specific nodes would partition: failure/jamming triage feeding ADR-032 posture), and an **at-risk flag** that counts as a structural event for the drift→recalibration advisor. Surfaced as `TrustedOutput::mesh`. **Measured cost policy** (criterion, 12-node mesh): weights are quantized (1/64; a *nonzero* coupling below one quantum saturates to quantum 1 so quantization never erases a live coupling — without the floor, balanced meshes of ≥ 65 nodes had every ~1/n coupling erased and sat permanently "at risk") and updates change-gated, so the steady-state cycle does zero graph work (~7.3 µs, ~23× cheaper than building); on any real change a full exact rebuild (~171 µs) is used because one `DynamicMinCut` delete+insert measured ~240 µs — the incremental machinery's overhead targets much larger graphs, so rebuild-on-change is the measured optimum at mesh scale (one-edge case −28% after the policy switch). Degenerate cases fail toward risk: a node with zero coupling is reported as already partitioned (cut 0). 9 mesh-guard tests + an engine-level wiring test; full `process_cycle` with the guard: ~33 µs for 4 nodes (50 ms budget).
- **Opt-in FFT operator for the CIR ISTA solver (8–14× measured).** Φ is a sub-DFT, so each ISTA mat-vec can run as one length-G FFT (O(G log G)) instead of a dense O(K·G) product. New `CirConfig::fft_operator` (default **false** — the dense path stays the bit-exact witness default; the FFT evaluates the same sums in a different order, so enabling it shifts float results and requires regenerating any pinned witness). `FftOperator` (rustfft, planned once at construction, scratch reused across the ISTA loop) dispatches inside `ista_solve`; warm-start/Lipschitz stay dense at construction. Measured (criterion, same run): ht20 2.22 ms → 265 µs (**8.4×**), ht40 10.26 ms → 717 µs (**14.3×**); the real HE40 grid (K=484, G=1452) scales further. 3 new tests: FFT↔dense matvec equivalence to float tolerance (ht20 + he40 grids), end-to-end dominant-tap agreement on a single-path frame, and all default configs keep FFT off. New `cir_estimate_fft` bench group.
- **Per-room adapter provenance + drift→recalibration advisor in the streaming engine.** Closes the trust-chain gap where an ~11 KB per-room LoRA adapter (ADR-150 §3.4) could silently change inference without the witness noticing. `StreamingEngine::set_room_adapter(AdapterInfo)` pins the adapter's content-derived id into provenance `model_version` (`rfenc-v1+adapter:<id>`) — and therefore into the BLAKE3 witness — so swapping or clearing adapter weights always shifts the witness (engine test proves base → adapter → other-adapter → cleared all witness differently, and cleared == base). New `RecalibrationAdvisor` recommends re-running the ADR-135 baseline / refitting the adapter on sustained low fusion coherence (streak threshold, default 60 cycles ≈ 3 s at 20 Hz) or an ADR-142 change-point; surfaced as `TrustedOutput::recalibration_recommended` and recorded on the sensing-server's `EngineBridge` alongside the witness. Bridge plumbing: `EngineBridge::{set_room_adapter, clear_room_adapter}` + live-path test that the adapter id flows into the live witness. *Scope note: this is the deployable provenance/trigger half of the "retrained model" roadmap item — fitting the adapter itself runs in the existing external calibration service (`aether-arena/calibration/`), and a trained RF-encoder checkpoint still does not exist in-tree.*
- **RuView beyond-SOTA research series** (`docs/research/ruview-beyond-sota/`, 6 docs) — research-swarm output defining the beyond-SOTA bar and the path to it: system capability audit (role→crate maturity matrix, gap analysis, risk register), web-verified 2026 SOTA landscape per capability axis (incl. ratified IEEE 802.11bf-2025), 8-pillar target architecture on the ADR-136 contract spine (no rewrite), 6-layer benchmark/validation methodology (all 15 criterion bench targets inventoried; ADR-171 statistical protocol), and a determinism-safe optimization roadmap. Includes session validation evidence: 2,797 workspace tests / 0 failed, Python proof PASS (bit-exact), paired pre/post criterion runs.

### Performance
- **CIR estimator warm-start precompute** — the diagonal Tikhonov preconditioner `diag(Φ^H Φ)+λI` and its CSR matrix were rebuilt every frame although they depend only on Φ and λ (fixed at `CirEstimator::new`); now precomputed at construction (`ruvsense/cir.rs`). Bit-identical floats (summation order unchanged, witness chain unaffected). Measured: `cir_estimate/he40` −3.9% (p<0.01), multiband groups −1.2/−1.4%; smaller configs within container noise.
- **RF tomography solver hoisting** — ISTA gradient buffer no longer allocated inside the 100-iteration loop, and the Frobenius Lipschitz bound moved from per-`reconstruct` to construction (`ruvsense/tomography.rs`). Bit-identical results.

### Added
- **Falsifiable occupancy benchmark (`wifi-densepose-train::occupancy_bench`).** Makes the presence/person-count "beyond SOTA" claim falsifiable in code instead of aspirational (the unfalsifiability gap from the beyond-SOTA system review). Grades predictions vs ground truth and gates a SOTA claim behind one `claim_allowed` invariant requiring all of: `DataProvenance::Measured` (synthetic/mock is scorable but **never claimable** — anti-mock-contamination per the CLAUDE.md Kconfig-bug lesson), a leak-free `EvalSplit` (refuses any split where a subject *or* environment id appears in both train and test — subject leakage / per-environment overfitting), `n_test ≥ min`, a **non-degenerate test set** (both truth classes represented: present-rate ≥ `min_positive_rate` and ≥ 1 absent sample — an all-absent set plus an always-absent predictor cannot release a claim; vacuous F1 scores 0.0, never 1.0), presence-F1 **bootstrap-CI lower bound** (deterministic seeded splitmix64) clearing the threshold, and count MAE within threshold. The claim string is unreadable except through the gate (`NO_CLAIM` otherwise). What remains is data, not method: a frozen, SHA-pinned, subject/environment-disjoint measured replay set turns the claim into a passing/failing test. 12 tests cover each refusal path, including the point-above/CI-below case (claim withheld on the CI lower bound even when the point estimate clears the threshold).
- **Live trust path: sensing-server routes real frames through the governed `StreamingEngine` (parallel governed path with partial output gating).** Previously the live server ran only the *bare* `MultistaticFuser` (fused amplitudes, no trust control plane), while the privacy/provenance/witness engine (ADR-135..146) ran only on synthetic in-test frames — the gap called out in ADR-136 §8 and the beyond-SOTA system review. New `engine_bridge` module drives `StreamingEngine::process_cycle` from the server's live `NodeState` map (reusing the existing `NodeState → MultiBandCsiFrame` conversion), lazily wiring each node as a WorldGraph sensor and bounding belief growth via the retention cap; every *governed belief* carries evidence + model + calibration + privacy decision and a deterministic witness. **Honest scope:** the engine runs alongside (not instead of) the bare fusion path that feeds the live `SensingUpdate`. What its decision gates on the wire today: a cycle emitted at class `Restricted` (base mode or contradiction/mesh-risk demotion) suppresses the per-node raw amplitude vectors from the live publish — the same field mapping `wifi-densepose-bfld`'s privacy gate applies at `Restricted`; gating the remaining derived outputs (person count, classification, signal field) is tracked as a follow-up. Trust state is no longer write-only: the latest witness, effective privacy class, demotion flag, recalibration recommendation, and an engine-error counter are readable on `GET /api/v1/status`, and engine errors are counted + rate-limit logged instead of silently swallowed (`EngineBridge::observe_cycle`). Adds `wifi-densepose-engine/-worldgraph/-bfld/-geo` deps. Bridge tests cover witnessed belief with provenance, determinism, idempotent node registration, retention bound, privacy-mode propagation, trust-state recording, the error-counter path, and Restricted-class raw-output suppression.

### Fixed
- **Real HE20 CSI no longer silently dropped or replaced with simulated data (fixes #1009, #1004).** Two ingest bugs caused real ESP32-C6 HE20 frames to be discarded or never received — the exact "real data silently lost" failure class the project fights. Each fix is pinned by a test that fails on the old code.
  - **#1009 §1b — HE20 baseline recorder trimmed 256 → 242 bins by sequential index (`wifi-densepose-signal/src/ruvsense/calibration.rs`).** ESP-IDF v5.5.2 delivers all 256 FFT bins for an HE20 frame; `CalibrationConfig::he20()` carried `num_active: 242`, so the recorder (which has no HE20 tone map — `extract_first_stream` takes the first `num_active` columns *sequentially*) kept bins 0..242 of the 256-bin grid. Those are the lower guard band + DC, **not** the 242 active tones, silently corrupting the empty-room baseline. Now `num_active: 256` records every delivered bin, staying aligned 1:1 with the live `deviation()` path. The exact-242 tone map deliberately stays only in `cir.rs` (`HE20_ACTIVE`), where the Φ sensing matrix genuinely needs it. Test `he20_records_all_256_bins_not_trimmed_to_242` asserts the finalized baseline covers all 256 bins (was 242). HE20 synthetic/bench fixtures updated to feed 256-bin frames (the real wire format).
  - **#1009 §1a/§1c — already-fixed u8→u16 `n_subcarriers` truncation, now regression-pinned.** The ADR-018 wire format carries `n_subcarriers` as u16 LE at bytes 6–7. A 256-bin HE20 frame (byte6=0x00, byte7=0x01) read as a single byte decodes to **0 subcarriers** → every frame skipped (invisible until HE20: ESP32-S3's ≤192 bins fit in one byte). The CLI parser (`wifi-densepose-cli/calibrate.rs`) and the sensing-server template parser (`wifi-densepose-sensing-server` `parse_esp32_frame`) were already corrected to u16 under #1005/ADR-110; added regression tests (`parse_esp32_frame_he20_256_bins_not_truncated`, CLI `test_parse_csi_packet_he_su_256_bins`) that fail on the old single-byte read so the truncation cannot silently return.
  - **#1004 — `--source auto` latched on `simulate` forever, never binding UDP :5005 (`wifi-densepose-sensing-server/src/main.rs`).** A one-shot boot probe resolved the source once; with no CSI flowing at boot (the normal firmware/server startup race) it served simulated poses for the whole process and ignored real CSI that arrived seconds later (the prior #937 fix hard-exited instead — equally wrong, the server could never pick up late-starting CSI). New `plan_source()` state machine: in `auto` mode **always bind the UDP receiver** and serve simulated data only until the first real frame, at which point `udp_receiver_task` promotes `source` → `esp32` (mirroring the existing `esp32 → esp32:offline` reversion in `effective_source()`); `simulated_data_task` self-suspends once promoted so it never clobbers live CSI. Explicit `--source simulated` stays a hard, UDP-free override for offline demos. 6 unit tests pin the resolution/promotion machine (`auto_with_no_boot_source_still_binds_udp_and_simulates`, etc.); the auto-binds-UDP assertion fails on the old behavior.
- **`wifi-densepose-mat` standalone `--no-default-features` build (101 errors → 0).** `pub mod api` was unconditional while its only dependency, serde, is optional behind the `api` feature — so any build without default features failed with unresolved serde imports (masked in `--workspace` runs by feature unification). The `api` module and its `create_router`/`AppState` re-export are now `#[cfg(feature = "api")]`-gated (with docsrs annotations). All feature combos compile: bare `--no-default-features`, `--no-default-features --features api`, and full default (177 tests pass).
- **WorldGraph no longer grows unboundedly under the live loop.** `StreamingEngine::process_cycle` appended one `SemanticState` belief per cycle with no eviction — ~1.7M nodes/day at 20 Hz (identified in `docs/research/ruview-beyond-sota/04-optimization-roadmap.md`). Added `WorldGraph::prune_semantic_states(max)` — deterministic eviction of the oldest beliefs by `(valid_from_unix_ms, id)`, structural nodes (rooms/zones/sensors/anchors/tracks/events) never eligible — and wired it into the engine after each belief append (`StreamingEngine::DEFAULT_SEMANTIC_RETENTION` = 7,200 ≈ 6 min at 20 Hz; tunable via `set_semantic_retention`). The WorldGraph holds *current* beliefs; durable history is the recorder's job, so no audit data is lost. 3 new tests (bounded growth end-to-end, oldest-only eviction, deterministic tie-break).
- **ESP32 edge heart rate no longer stuck at ~45 BPM / dropping wildly — #987.** The on-device HR estimator (`edge_processing.c`, `0xC5110002`) reported ~45 BPM regardless of true heart rate (Apple-Watch ground truth 87 BPM read as ~45) and swung frame-to-frame. Two root causes: (1) a hardcoded `sample_rate = 10.0f` that became wrong after #985's self-ping raised the CSI callback rate to a variable ~13–19 Hz — BPM scales as `assumed/actual × true`, so 87 read ~45 and the reading swung as CSI yield fluctuated; (2) the zero-crossing estimator locked onto a breathing harmonic (a 0.25 Hz breathing fundamental puts its 3rd harmonic at ~0.74 Hz ≈ 44 BPM inside the HR band). Fix: measure the real sample rate from inter-frame timestamps (used for BPM conversion + biquad re-tuning on >15% drift); replace the HR zero-crossing with an autocorrelation estimator that rejects breathing harmonics (driven by a robust autocorr breathing period); median-13 smooth the output. Hardware A/B (fixed vs unmodified control board, both `edge_tier=2`): control pegged 40–49 BPM; fixed reaches the true 88–91 BPM (vs 87 GT) and holds a stable physiological value (spread 59→0 for a steady subject). Known limitation: heavy subject motion still degrades the estimate (motion gating is a follow-up).
- **Person count no longer leaks up to 10 in heuristic mode — addresses #894.** `field_bridge::occupancy_or_fallback` returned the eigenvalue-based `FieldModel::estimate_occupancy` count **unbounded** (its internal ceiling is 10), while the sibling estimators on the same single-link data — the perturbation-energy fallback right below it and `score_to_person_count` — both cap at 3 ("1-3 for single ESP32"). On noisy / under-calibrated CSI the eigenvalue count inflated, producing the "10 persons reported when 1 present" symptom (seen when `--model` fails to load and the server runs on heuristics). Bounded the eigenvalue path to the shared `MAX_SINGLE_LINK_OCCUPANCY` (3) so every estimator on one link agrees; genuine higher counts come from the multistatic fusion path, not a single-link covariance estimate.
- **MQTT multi-node deployments now create one Home-Assistant device per node — closes #898.** After the #872 MQTT wiring landed, the JSON→`VitalsSnapshot` bridge hard-coded a single `node_id` (the MQTT client id) and the publisher used a single `OwnedDiscoveryBuilder`, so every physical node collapsed into one device (`identifiers:["wifi_densepose_wifi-densepose-1"]`), contradicting the "one device per node" docs. The bridge now emits one snapshot per node in the sensing update's `nodes[]` (each with its own `node_id` + RSSI, falling back to a single aggregate snapshot for wifi/simulate sources), and the publisher derives a per-node builder (`OwnedDiscoveryBuilder::for_node`) that publishes discovery + availability lazily on first sight of each `node_id` and routes state to per-node topics — yielding N distinct HA devices with per-node availability/LWT. Unit-tested (distinct nodes → distinct `wifi_densepose_<node>` identifiers); 71 MQTT tests pass.
- **Person count no longer pinned to 1 — addresses #803.** The aggregate occupancy reported by the sensing server was derived from `smoothed_person_score`, an EMA-smoothed *activity* score (amplitude variance / motion / spectral energy). That score saturates near a single occupant — one moving person maxes it out — so it cannot discriminate occupancy *count* and stayed clamped at 1 across S3/C6 and the Python/Docker/Rust servers. Meanwhile the count-aware per-node estimates the ESP32 paths already compute (firmware `n_persons`, and the DynamicMinCut `corr_persons`) were stashed in `NodeState::prev_person_count` and then **discarded** by the aggregator (same dead-wiring class as #872). The aggregator now takes `max(activity_count, node_max)` via a unit-tested `aggregate_person_count` helper, so a node positively estimating 2–3 occupants is surfaced instead of overwritten. The fix can only ever *raise* the count when a node reports more people, so the single-occupant case is provably never inflated (regression-guarded by test). **Second half:** the pure-CSI per-node path itself clamped its own estimate — the DynamicMinCut occupancy (`estimate_persons_from_correlation`, 0–3) was mapped to a score via `corr_persons / 3.0`, putting 2 people at 0.667, *just under* the 0.70 up-threshold of `score_to_person_count`, so the per-node count never climbed past 1 (so `node_max` was also stuck at 1 for CSI-only nodes). Replaced it with a threshold-aligned `corr_persons_to_score` mapping (1→0.40, 2→0.74, 3→0.96) whose steady state round-trips back to the same count through the EMA + hysteresis, while still gating transient noise. A convergence test replays the exact EMA loop to prove min-cut=2 now reports 2 (and documents that the old `/3.0` mapping reported 1). Full multi-person accuracy still depends on the underlying estimator quality; this removes the two server-side clamps that masked it. 586 sensing-server tests pass.
- **MQTT publisher now actually runs (`--mqtt`) — closes #872.** The `--mqtt*` flags were defined only in `cli::Args` (dead code, referenced nowhere) while the binary parses a *separate* `main::Args` with no mqtt fields, and `main.rs` never started the `mqtt::` publisher — so MQTT/Home-Assistant integration was completely unwired (`--mqtt` errored as an unexpected argument, and even with the Docker image's `--features mqtt` build the publisher never ran). Earlier attempts chased a Docker *rebuild*; the real cause was disconnected *code*. Extracted the flags into a shared `cli::MqttArgs` (`#[command(flatten)]` into both structs), spawn the publisher on `--mqtt`, and bridge the JSON sensing broadcast into the typed `VitalsSnapshot` stream with a defensive `serde_json::Value` mapping. Verified end-to-end against `mosquitto`: 20 HA auto-discovery entities + live state (presence/person-count/…). 577 (default) / 580 (`--features mqtt`) tests pass.
- **Mass Casualty triage never reports a survivor with a heartbeat as Deceased (safety) — PR #926.** Both triage paths in `wifi-densepose-mat` — `TriageCalculator::calculate` (`combine_assessments(Absent, None) ⇒ Deceased`) and the detection path `EnsembleClassifier::determine_triage` (`!has_breathing && !has_movement ⇒ Deceased`) — ignored the `heartbeat` field. A survivor with a detectable **pulse** but no sensed breathing/movement (respiratory arrest — the most time-critical *savable* state, Immediate/Red) was therefore reported **Deceased (Black)** and deprioritized for rescue. The domain path was in fact only reachable *because* a heartbeat made `has_vitals()` true, so every "Deceased" was a live person. Both paths now escalate to **Immediate** when a heartbeat is present; total absence of breathing, movement *and* heartbeat is unchanged (domain → `Unknown`, ensemble → `Deceased`). 2 safety regression tests; full MAT suite (177) green.
- **Per-node Home-Assistant devices now report each node's *own* presence/motion — PR #918.** After the one-device-per-node fan-out landed, the MQTT bridge still applied the *room-level aggregate* `classification` to every node, so in a multi-node deployment a node watching an empty corner inherited another node's "present" (and `motion_level: "absent"` was mis-mapped to full motion). Each node in the broadcast `nodes[]` already carries its own `classification`; the bridge now reads it per node (extracted into a testable `vitals_snapshots_from_sensing_json`), keeping vitals + person count room-level. 4 unit tests.
- **`--model` gives an actionable diagnostic instead of a cryptic magic error — PR #919 (refs #894).** Passing a HuggingFace `ruvnet/wifi-densepose-pretrained` file (`model.safetensors` / `model-q4.bin` / `model.rvf.jsonl`) to `--model` produced `invalid magic at offset 0: … got 0x77455735`, then a silent fall back to heuristics. The load-failure path now detects the format (safetensors / quantized blob / JSONL manifest) and explains that those files are a different format **and** encoder architecture than the RVF binary container the progressive loader expects, pointing to #894. Pure `diagnose_model_load_error` + 4 tests.
- **`--export-rvf` no longer silently produces a placeholder model — PR #920.** The `--export-rvf` handler ran *before* `--train`/`--pretrain` and unconditionally wrote placeholder sine-wave weights, so the documented `--train … --export-rvf <path>` workflow short-circuited to a fake model and never trained (while printing "exported successfully"). It now emits the placeholder **container-format demo** only standalone (with a clear warning), and falls through to real training when `--train`/`--pretrain` is set; docs point to `--save-rvf` for the real model. 3 guard tests.

### Added
- **ADR-151 per-room calibration & specialist training — full `baseline → enroll → extract → train` pipeline (new `wifi-densepose-calibration` crate).** "Teach the room before you teach the model": a local-first pipeline that turns a few minutes of clean human anchors — layered on the ADR-135 empty-room baseline — into a versioned bank of small, room-calibrated specialists for **presence, posture, breathing, heartbeat, restlessness, and anomaly**. Stages: guided enrollment with an adaptive quality gate (event-sourced `EnrollmentSession`, re-prompts bad anchors); feature extraction (autocorrelation periodicity in breathing/HR bands + variance/motion); six small specialists (learned threshold / nearest-prototype / band-limited periodicity / novelty); a `SpecialistBank` with baseline-drift **STALE** invalidation; and a `MixtureOfSpecialists` runtime with presence short-circuit + anomaly veto + confidence gating. Specialists are statistical heads today (runnable + hardware-validated); the frozen ADR-150 HF RF Foundation Encoder backbone is the documented upgrade path.
  - **CLI:** `enroll` / `train-room` / `room-status` / `room-watch`, plus the Stage-1 `calibrate-serve` HTTP API (CORS-enabled: `POST /start`, `GET /status`, `POST /stop`, `GET /result`, `GET /baselines`, `GET /health`) and a firewall-free `scripts/csi-udp-relay.py` for local Windows ESP32 testing without admin.
  - **Multistatic fusion (ADR-029):** `MultiNodeMixture` fuses several co-located nodes (each with its own room-calibrated bank) into one room state — presence OR'd across nodes, posture/breathing/heartbeat from the highest-confidence node, a single implausible node vetoes the room's vitals. Driven via `room-watch --node-bank N:path` (repeatable), which groups live frames by `node_id` and fuses. Same-room only; cross-room is federation (ADR-105).
  - **Validated on live ESP32-S3 (COM8, `edge_tier=0` raw CSI):** baseline capture (120 frames → 52-subcarrier baseline); the real parser → feature-extraction → mixture runtime detecting breathing (~16–31 BPM); and the multistatic ingest grouping/fusing by node-id end-to-end. Full multi-anchor enrollment accuracy requires the operator to perform the poses; true 2-node fusion + phase-based breathing + RVF/HNSW storage are noted follow-ups. 54 tests pass (35 calibration + 19 CLI).
- **WiFi-CSI pose: efficiency frontier + per-room calibration service** (ADR-150 §3.2–3.6). Two beyond-SOTA results on the MM-Fi benchmark, plus the deployment mechanism that resolves real-world generalization:
  - **Efficiency frontier** — a **75 K-param model beats published SOTA** (74.3% vs MultiFormer 72.25% torso-PCK@20); every config from `micro` up is Pareto-dominant (smaller *and* more accurate than prior work). Shipped a deployable **int4 edge model (~20 KB, verified 74.08%, 0.135 ms single-thread CPU)** — published at [`ruvnet/wifi-densepose-mmfi-pose/edge`](https://huggingface.co/ruvnet/wifi-densepose-mmfi-pose). See [`docs/benchmarks/wifi-pose-efficiency-frontier.md`](docs/benchmarks/wifi-pose-efficiency-frontier.md).
  - **Generalization solved by few-shot calibration** — zero-shot cross-subject (~64%) and cross-environment (~10%) are *not* closeable by algorithms (CORAL, DANN, instance-norm, contrastive foundation-pretraining all tested, all failed) or by more training subjects (saturates ~64%). But **~100–200 labeled in-room samples recover SOTA-level pose**: cross-subject 64→76%, **cross-environment 10→73% (60% from just 5 samples)** — deployable as a **~11 KB per-room LoRA adapter** on a frozen shared base. Full empirical chain in ADR-150 §3.2–3.6.
  - **Calibration service (complete, both model paths, cross-language verified)** — `aether-arena/calibration/`: `calibrate.py` (transformer model, `.npz` adapter) + `infer.py` (verified 3.09%→74.29% on an unseen MM-Fi room), **and `cog_calibrate.py`** which fits a `fc1.a/fc1.b/fc2.a/fc2.b` **safetensors** adapter for the deployed cog conv+MLP model (`pose_v1.safetensors`). Consumed by the Rust product engine: `InferenceEngine::with_adapter()` + `cog-pose-estimation run --config <cfg> --adapter <room.safetensors>`. Self-contained regression tests for both Python producers (`test_calibration.py`, `test_cog_calibration.py`) **plus a cross-language Rust integration test** that loads a real `cog_calibrate.py`-generated adapter fixture and asserts it activates + changes engine output. All green.
- **Windows workspace build + test now green** (cross-platform fixes). `wifi-densepose-worldmodel` imported `tokio::net::UnixStream` unconditionally, so `cargo build/test --workspace` failed to compile on Windows (E0432) — now the OccWorld Unix-socket bridge is `#[cfg(unix)]`-gated with a clear non-unix fallback. And `wifi-densepose-bfld`'s `readme_quickstart_uses_canonical_public_api` test checked a multi-line `pipeline\n    .process` needle that never matched on a CRLF checkout — now normalizes line endings. Result: **2,682 workspace tests pass / 0 fail on Windows** (the pre-merge gate was previously unrunnable there).
- **`ruview-swarm` crate (ADR-148)** — drone swarm control system with hierarchical-mesh topology, Raft consensus, MAPPO multi-agent reinforcement learning, and CSI sensing integration. 14 modules: topology (Raft/Gossip/Mesh), formation control (virtual-structure/leader-follower/Reynolds flocking), RRT-APF path planning, auction+FNN task allocation, MARL actor + PPO training loop, security (MAVLink v2 HMAC-SHA256 signing, UWB anti-spoofing, geofencing, Remote ID, FHSS anti-jamming), 10-state fail-safe machine, and SwarmOrchestrator. ITAR-gated coordination features (USML Category VIII(h)(12)) behind `itar-unrestricted` feature.
- **Ruflo integration for `ruview-swarm`** — feature-gated (`ruflo`) AI-agent capability layer connecting to the claude-flow daemon: AgentDB mission memory (`memory_store`/`memory_search`), HNSW pattern learning (`agentdb_pattern-store`/`-search`), AIDefence MAVLink message scanning, and SONA intelligence trajectory hooks. `RufloBackend` trait with `HttpRufloBackend` (JSON-RPC 2.0) and `MockRufloBackend` implementations.

### Performance
- `ruview-swarm` benchmarks (criterion, release): MARL actor inference 3.3 µs, RRT-APF planning 0.043 ms, multi-view CSI fusion 58.5 ns, 3-view localization 1.732 m (beats Wi2SAR 5 m SOTA baseline), 4-drone SAR coverage 223 s for 400×400 m (under 240 s target).

### Added
- **ADR-147 — OccWorld world model integration** (`wifi-densepose-worldmodel` v0.3.0 published to crates.io). 15-frame trajectory prediction at 209 ms / 3.37 GB VRAM on RTX 5080. Phase 3 domain adapter `scripts/ruview_occ_dataset.py` (`RuViewOccDataset`) converts WorldGraph snapshots to OccWorld tensors with indoor class remapping + zero ego-poses (validated). Phase 5 retraining pipeline `scripts/occworld_retrain.py` — VQVAE + transformer fine-tuning on RuView occupancy snapshots. See [ADR-147](docs/adr/ADR-147-nvidia-cosmos-world-foundation-model-integration.md) · [benchmark proof](docs/adr/ADR-168-benchmark-proof.md).

### Added
- **ADR-125 (APPLE-FABRIC) — RuView ↔ Apple Home native HAP bridge proposal + reference impl** (issue #796). New ADR-125 lays out a three-phase plan to expose RuView as a discoverable HomeKit accessory on the LAN so a HomePod (as Home Hub) sees presence / vitals / BFLD-derived events natively — zero Home-Assistant intermediary. Two architectural decisions resolved in the ADR per design review: (1) **one HAP bridge with N child accessories** (single pairing, matches Hue/Eve pattern), and (2) **identity-risk mapping is semantic, not probabilistic** — `identity_risk_score` and Soul-Signature match probability never cross the HAP boundary; instead three thresholded events are exposed (`Unknown Presence`, `Unexpected Occupancy`, `Unrecognized Activity Pattern`) so RuView reads as calm-tech ambient awareness, not surveillance UX. ADR-125 §2.1.a reference impl ships now: `scripts/hap-test-sensor.py` (HAP-1.1 bridge advertised over mDNS, paired with operator's iPhone) + `scripts/c6-presence-watcher.py` (parses ESP32 `RV_FEATURE_STATE_MAGIC = 0xC5110006` UDP packets with IEEE CRC32 validation, hysteresis, and a Python port of `wifi-densepose-bfld::PrivacyClass` that enforces ADR-125 §2.1.d invariant I1 at the HomeKit edge — only `Anonymous` (2) and `Restricted` (3) frames may cross; `Raw`/`Derived` are refused with exit code 2 and the cited ADR clause). Validated end-to-end on real hardware (no mocks): ESP32-C6 on `ruv.net` → UDP/5005 → mac-mini watcher → BFLD gate → HAP bridge → iPhone Home app shows `Unknown Presence` live characteristic flip. **Empirical**: 50-51 valid CRC-passing feature_state packets per 10 s window from the live C6; zero CRC errors. P2 (Rust-native HAP via the `hap` crate, replaces the Python sidecar) and P3 (Matter Controller once `matter-rs` stabilizes) follow.

### Security
- **ESP32 OTA upload now fails closed when no PSK is provisioned** (#596 audit finding — critical, **breaking change for unprovisioned nodes**). `ota_check_auth()` previously returned `true` when `s_ota_psk[0] == '\0'`, so a freshly-flashed node would accept attacker-controlled firmware over plain HTTP on port 8032 from any host on the WiFi. No Secure Boot V2, no signed-image verification — a single LAN call could brick or backdoor a node. The fix rejects every OTA upload until a PSK is written to NVS (the OTA HTTP server still starts so operators can run `provision.py --ota-psk <hex>` over USB-CDC without reflashing). **Operators affected**: any deployment that relied on the unauthenticated OTA endpoint working out of the box now needs to provision a PSK before subsequent OTA pushes will succeed. Boot-time `ESP_LOGW` makes the new posture visible.
- **Bearer-token auth accepts the scheme case-insensitively (RFC 6750) — PR #929.** `require_bearer` parsed the `Authorization` header with a case-sensitive `strip_prefix("Bearer ")`, so a *correct* `RUVIEW_API_TOKEN` sent as `Authorization: bearer <token>` (or `BEARER`, or with extra whitespace) was rejected with a confusing 401 — needless friction when enabling auth. The scheme is now matched with `eq_ignore_ascii_case` (per RFC 6750 §2.1 / RFC 7235 §2.1); the token compare is unchanged — still exact and constant-time (`ct_eq`) — so a wrong token or a non-Bearer scheme (`Basic …`) still returns 401. Audited the surrounding code while here: `ct_eq` correctly rejects length mismatch (no prefix-auth bypass) and the middleware fails closed. New `accepts_case_insensitive_bearer_scheme` test.
- **Path-traversal vulnerabilities patched in five sensing-server endpoints** (closes #615 — critical). New `wifi_densepose_sensing_server::path_safety::safe_id()` enforces `[A-Za-z0-9._-]` only (no leading `.`, max 64 chars) before any user-controlled identifier reaches a `format!()` building a filesystem path. Applied at:
  - `POST /api/v1/recording/start` (`recording.rs` — `session_name`)
  - `GET /api/v1/recording/download/:id` (`recording.rs` — `id`)
  - `DELETE /api/v1/recording/delete/:id` (`recording.rs` — `id`)
  - `POST /api/v1/models/load` (`model_manager.rs` — `model_id`)
  - `training_api.rs` `load_recording_frames` (`dataset_id`s)

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

### Fixed
- **WebSocket `/ws/sensing` now reports `esp32:offline` when ESP32 hardware goes stale** (closes #618). `broadcast_tick_task` was re-emitting the cached `latest_update` with a frozen `source: "esp32"` field forever after the hardware lost power or network. The REST `/health` endpoint already called `effective_source()` (which returns `"esp32:offline"` after `ESP32_OFFLINE_TIMEOUT` = 5 s with no UDP frames), but the WS broadcast path was the one consumer that didn't. Result: the UI's "LIVE — ESP32 HARDWARE Connected" banner stayed green long after the hardware went away, and `vital_signs`/`features`/`classification` re-broadcasted the last-seen values indefinitely. Fix: clone the cached `latest_update` per tick, overwrite `source` with `s.effective_source()`, then serialize and broadcast. UI can now switch to an offline state on the same 5-second budget the REST surface uses.
- **Proof replay (`archive/v1/data/proof/verify.py`) is now cross-platform deterministic** (closes #560). Three changes together: (1) `features_to_bytes()` now `np.round(.., HASH_QUANTIZATION_DECIMALS=6)`s each feature array before packing as little-endian f64, collapsing ULP-level drift from scipy.fft pocketfft SIMD reordering; (2) the `Verify Pipeline Determinism` workflow pins `OMP_NUM_THREADS=1`, `OPENBLAS_NUM_THREADS=1`, `MKL_NUM_THREADS=1`, `VECLIB_MAXIMUM_THREADS=1`, `NUMEXPR_NUM_THREADS=1` — multi-threaded BLAS reductions were a deeper source of non-determinism than SIMD reordering, and 6-decimal quantization alone wasn't enough across Azure VM microarchitectures; (3) `expected_features.sha256` regenerated under the new conditions. CI now passes the determinism check (same hash across consecutive runs on canonical Linux x86_64 CI runner: `667eb054c44ac510342665bf9c93d608868a8ead948ae8774b2796ebce6f8fe7`). `scripts/probe-fft-platform.py` updated to mirror `HASH_QUANTIZATION_DECIMALS=6` for cross-machine spot-checks.
- **`archive/v1/src/services/pose_service.py:223` calls the right method on `PhaseSanitizer`** (closes #612). The call was `self.phase_sanitizer.sanitize(phase_data)`, but `PhaseSanitizer`'s full-pipeline entry point is named `sanitize_phase()` (`unwrap_phase` + `remove_outliers` + `smooth_phase` chained, see `archive/v1/src/core/phase_sanitizer.py:266`). The shorter `sanitize` name doesn't exist on the class, so any path that reached this branch raised `AttributeError` and crashed the pose service mid-frame.
- **`adaptive_classifier.rs:94` no longer panics on NaN feature values** (closes #611).
  `sorted.sort_by(|a, b| a.partial_cmp(b).unwrap())` returned `None` and panicked
  whenever a single `NaN` reached the classifier from real ESP32 hardware (silent
  DSP div-by-zero, empty buffer). One bad frame killed the entire sensing-server
  process. Swapped for `unwrap_or(Ordering::Equal)`, matching the pattern the
  same file already used at lines 149-150 and 155. Per-frame hot path; this was
  a real production crash vector.
- **Completed the #611 NaN-panic audit across the sensing-server crate** (follow-up
  to #613). The original audit grepped for the literal `partial_cmp(b).unwrap()`
  and missed seven additional production sites that use comparator variants
  (`partial_cmp(b.1).unwrap()`, `partial_cmp(&variances[b]).unwrap()`). All share
  the same crash class — a single `NaN` in CSI-derived state panics the whole
  sensing-server. Fixed:
  - `adaptive_classifier.rs:205` — `AdaptiveModel::classify()` argmax over softmax
    probs. **Same per-frame hot path as #611**; NaN flows through normalise →
    logits → softmax and still reaches this site even after the #613 IQR fix.
  - `adaptive_classifier.rs:480, 500` — training-loop argmax in `train()`
    (training/per-class accuracy reporting).
  - `main.rs:2446, 2449` and `csi.rs:602, 605` — variance-based source/sink
    selection in `count_persons_mincut`. The outer `unwrap_or((0, &0))` only
    catches an empty iterator; it cannot rescue a comparator panic.

  Remaining `partial_cmp(...).unwrap()` sites in the workspace are all inside
  `#[cfg(test)]` / `#[test]` blocks (`spectrogram.rs:269`, `depth.rs:234`,
  `connectivity.rs:477`, `vital_signs.rs:737`) where inputs are controlled.
- **`ui/utils/pose-renderer.js` no longer divides by zero** when two render frames land in the same `performance.now()` tick (issue #519 Bug 2). `deltaTime` is now `Math.max(currentTime - lastFrameTime, 1)` before the `1000 / deltaTime` division, capping displayed FPS at 1000 — far above any real render rate, but finite so the EMA `averageFps = averageFps * 0.9 + fps * 0.1` no longer poisons itself to `Infinity` on a single zero-dt tick.

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

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

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

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

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

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

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

- **`dedup_factor` runtime configuration API** (`v2/crates/wifi-densepose-sensing-server`) —
  Exposes the multi-node person-count deduplication divisor at runtime via REST:
  - `GET  /api/v1/config/dedup-factor` — read current value.
  - `POST /api/v1/config/dedup-factor` — set value (clamped 1.0–10.0, persisted).
  - `POST /api/v1/config/ground-truth` — auto-tunes `dedup_factor` from a known
    person count (`{"count": N}`); derives optimal divisor from current node-sum.
  Config is persisted to `data/config.json` and reloaded on restart. (ADR-044 §5.3)

- **`nvsim` crate — deterministic NV-diamond magnetometer pipeline simulator** (ADR-089) —
  New standalone leaf crate at `v2/crates/nvsim` modeling a forward-only
  magnetic sensing path: scene → source synthesis (Biot–Savart, dipole,
  current loop, ferrous induced moment) → material attenuation
  (Air/Drywall/Brick/Concrete/Reinforced/SteelSheet) → NV ensemble
  (4 〈111〉 axes, ODMR linear-readout proxy, shot-noise floor per
  Wolf 2015 / Barry 2020) → 16-bit ADC + lock-in demodulation →
  fixed-layout `MagFrame` records → SHA-256 witness. Six-pass build
  per `docs/research/quantum-sensing/15-nvsim-implementation-plan.md`.
  50 tests, ~4.5 M samples/s on x86_64 (4500× the Cortex-A53 1 kHz
  acceptance gate), pinned reference witness
  `cc8de9b01b0ff5bd97a6c17848a3f156c174ea7589d0888164a441584ec593b4`
  for byte-equivalence regression. WASM-ready by construction
  (zero `std::time/fs/env/process/thread`); builds cleanly for
  `wasm32-unknown-unknown`. ADR-090 (Proposed, conditional) tracks the
  optional Lindblad/Hamiltonian extension if AC magnetometry, MW power
  saturation, hyperfine spectroscopy, or pulsed protocols become required.

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

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

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

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

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

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

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

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

## [v0.7.0] — 2026-04-06

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

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

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

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

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

### Fixed
- Security fix merged via PR #310.

### Performance
- Presence detection: 100% accuracy on 60,630 overnight samples. *(Retracted — that recording was single-class (one sleeping person, 6,062/6,063 frames "present"), so a constant "yes" scores ~99.98%. Superseded by the honest 82.3% held-out temporal-triplet metric; see [#882](https://github.com/ruvnet/RuView/issues/882). Kept here as the in-place public record.)*
- Inference: 0.008 ms per sample, 164K embeddings/sec.
- Contrastive self-supervised training: 51.6% improvement over baseline.

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

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

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

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

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

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

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

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

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

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

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

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

### Benchmarks (2-node mesh, COM6 + COM9, 30s)
| Metric | Baseline | v0.5.3 | Improvement |
|--------|----------|--------|-------------|
| Variance noise | 109.4 | 77.6 | **-29%** |
| Feature stability | std=154.1 | std=105.4 | **-32%** |
| Keypoint jitter | std=4.5px | std=1.3px | **-72%** |
| Confidence | 0.643 | 0.686 | **+7%** |
| Presence accuracy | 93.4% | 94.6% | **+1.3pp** |

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

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

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

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

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

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

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

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

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

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

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

## [Unreleased]

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

### Fixed
- Firmware now boots in QEMU: WiFi/UDP/OTA/display guards for mock CSI mode
- 9 bugs in mock_csi.c (LFSR bias, MAC filter init, scenario loop, overflow burst timing)
- 23 bugs from ADR-061 deep review (inject_fault.py writes, CI cache, snapshot log corruption, etc.)
- 16 bugs from ADR-062 deep review (log filename mismatch, SLIRP port collision, heap false positives, etc.)
- All scripts: `--help` flags, prerequisite checks with install hints, standardized exit codes

- **Sensing server UI API completion (ADR-043)** — 14 fully-functional REST endpoints for model management, CSI recording, and training control
  - Model CRUD: `GET /api/v1/models`, `GET /api/v1/models/active`, `POST /api/v1/models/load`, `POST /api/v1/models/unload`, `DELETE /api/v1/models/:id`, `GET /api/v1/models/lora/profiles`, `POST /api/v1/models/lora/activate`
  - CSI recording: `GET /api/v1/recording/list`, `POST /api/v1/recording/start`, `POST /api/v1/recording/stop`, `DELETE /api/v1/recording/:id`
  - Training control: `GET /api/v1/train/status`, `POST /api/v1/train/start`, `POST /api/v1/train/stop`
  - Recording writes CSI frames to `.jsonl` files via tokio background task
  - Model/recording directories scanned at startup, state managed via `Arc<RwLock<AppStateInner>>`
- **ADR-044: Provisioning tool enhancements** — 5-phase plan for complete NVS coverage (7 missing keys), JSON config files, mesh presets, read-back/verify, and auto-detect
- **25 real mobile tests** replacing `it.todo()` placeholders — 205 assertions covering components, services, stores, hooks, screens, and utils
- **Project MERIDIAN (ADR-027)** — Cross-environment domain generalization for WiFi pose estimation (1,858 lines, 72 tests)
  - `HardwareNormalizer` — Catmull-Rom cubic interpolation resamples any hardware CSI to canonical 56 subcarriers; z-score + phase sanitization
  - `DomainFactorizer` + `GradientReversalLayer` — adversarial disentanglement of pose-relevant vs environment-specific features
  - `GeometryEncoder` + `FilmLayer` — Fourier positional encoding + DeepSets + FiLM for zero-shot deployment given AP positions
  - `VirtualDomainAugmentor` — synthetic environment diversity (room scale, wall material, scatterers, noise) for 4x training augmentation
  - `RapidAdaptation` — 10-second unsupervised calibration via contrastive test-time training + LoRA adapters
  - `CrossDomainEvaluator` — 6-metric evaluation protocol (MPJPE in-domain/cross-domain/few-shot/cross-hardware, domain gap ratio, adaptation speedup)
- ADR-027: Cross-Environment Domain Generalization — 10 SOTA citations (PerceptAlign, X-Fi ICLR 2025, AM-FM, DGSense, CVPR 2024)
- **Cross-platform RSSI adapters** — macOS CoreWLAN (`MacosCoreWlanScanner`) and Linux `iw` (`LinuxIwScanner`) Rust adapters with `#[cfg(target_os)]` gating
- macOS CoreWLAN Python sensing adapter with Swift helper (`mac_wifi.swift`)
- macOS synthetic BSSID generation (FNV-1a hash) for Sonoma 14.4+ BSSID redaction
- Linux `iw dev <iface> scan` parser with freq-to-channel conversion and `scan dump` (no-root) mode
- ADR-025: macOS CoreWLAN WiFi Sensing (ORCA)

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

---

## [3.0.0] - 2026-03-01

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

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

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

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

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

---

## [2.0.0] - 2026-02-28

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

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

### Added — ADR-021: Vital Sign Detection
- `VitalSignDetector` with breathing (6-30 BPM) and heartbeat (40-120 BPM) extraction from CSI fluctuations (`1192de9`)
- FFT-based spectral analysis with configurable band-pass filters
- Confidence scoring based on spectral peak prominence
- REST endpoint `/api/v1/vital-signs` with real-time JSON output

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

### Added — ADR-016: RuVector Training Integration (all 5 crates)
- `ruvector-mincut` → `DynamicPersonMatcher` in `metrics.rs` + subcarrier selection (`81ad09d`, `a7dd31c`)
- `ruvector-attn-mincut` → antenna attention in `model.rs` + noise-gated spectrogram
- `ruvector-temporal-tensor` → `CompressedCsiBuffer` in `dataset.rs` + compressed breathing/heartbeat
- `ruvector-solver` → sparse subcarrier interpolation (114→56) + Fresnel triangulation
- `ruvector-attention` → spatial attention in `model.rs` + attention-weighted BVP
- Vendored all 11 RuVector crates under `vendor/ruvector/` (`d803bfe`)

### Added — ADR-017: RuVector Signal & MAT Integration (7 integration points)
- `gate_spectrogram()` — attention-gated noise suppression (`18170d7`)
- `attention_weighted_bvp()` — sensitivity-weighted velocity profiles
- `mincut_subcarrier_partition()` — dynamic sensitive/insensitive subcarrier split
- `solve_fresnel_geometry()` — TX-body-RX distance estimation
- `CompressedBreathingBuffer` + `CompressedHeartbeatSpectrogram`
- `BreathingDetector` + `HeartbeatDetector` (MAT crate, real FFT + micro-Doppler)
- Feature-gated behind `cfg(feature = "ruvector")` (`ab2453e`)

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

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

### Added — ADR-015: Public Dataset Training Strategy
- MM-Fi and Wi-Pose dataset specifications with download links (`4babb32`, `5dc2f66`)
- Verified dataset dimensions, sampling rates, and annotation formats
- Cross-dataset evaluation protocol

### Added — WiFi-Mat Disaster Detection Module
- Multi-AP triangulation for through-wall survivor detection (`a17b630`, `6b20ff0`)
- Triage classification (breathing, heartbeat, motion)
- Domain events: `survivor_detected`, `survivor_updated`, `alert_created`
- WebSocket broadcast at `/ws/mat/stream`

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

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

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

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

### Changed
- Rust hardware adapters now return explicit errors instead of silent empty data (`6e0e539`)

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

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

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

---

## [1.1.0] - 2025-06-07

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

### Fixed
- Badge links for PyPI and Docker in README
- Async engine creation poolclass specification

---

## [1.0.0] - 2024-12-01

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

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

[Unreleased]: https://github.com/ruvnet/wifi-densepose/compare/v3.0.0...HEAD
[3.0.0]: https://github.com/ruvnet/wifi-densepose/compare/v2.0.0...v3.0.0
[2.0.0]: https://github.com/ruvnet/wifi-densepose/compare/v1.1.0...v2.0.0
[1.1.0]: https://github.com/ruvnet/wifi-densepose/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/ruvnet/wifi-densepose/releases/tag/v1.0.0
