mirror of
https://github.com/ruvnet/RuView
synced 2026-06-21 12:13:19 +00:00
c84ea39e62d14dcafe61fc80d357dfe0349462cd
1035 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
156323564a |
docs(readme): correct person-identification claims to measured reality (#1021)
An external audit correctly found the person-ID/Soul-Signature capability was spec-only with a no-op oracle. The §3.6 matcher is now real (wifi-densepose-bfld) but WiFi-only channels are MEASURED not-separable (cardiac+respiratory gap ~0.0005); named identity is data-gated on enrollment with the decisive AETHER/body-resonance channel. README now frames person re-id as experimental research, not a shipped feature. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
d79c22e03a |
fix(homecore-assist): exact in-memory cosine k-NN, drop fragile :memory: HNSW
The semantic recognizer built a ruvector-core VectorDB at ":memory:"; under full-workspace feature unification the file-storage backend is enabled and ":memory:" is an invalid Windows filename (os error 123), panicking via .expect(). Replace the external index with an exact in-memory cosine k-NN over the enrolled exemplars (embeddings are L2-normalised, so cosine = dot product). For HOMECORE's small intent vocabularies this is faster, fully deterministic, and removes the storage backend + cross-crate feature coupling entirely. ruvector-core dropped from the crate (only used here). Workspace 3122 passed/0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
3d96789475 |
docs(adr): ADR-158 MAT/world-model beyond-SOTA sweep (graded, MEASURED)
Records the cluster sweep: §1 triage unification, §2 real RSSI + dedup, §3 real ESP32/UDP/PCAP ingest with honest typed errors, §4 parabolic interpolation, §5 real GDOP, §6 occworld-prior fail-safe (mat consumes none). Graded SOTA table (RF-through-rubble DATA-GATED; worldgraph NO-ACTION already-SOTA; worldmodel clamp-proven; pointcloud cited), confirmed negative results, deferred backlog (nothing dropped), and reproduction commands. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
e1dc6e05ab |
feat(mat): wire real ESP32/UDP/PCAP CSI ingest; honest typed errors for gated adapters (ADR-158 §3)
hardware_adapter read_esp32_csi/read_udp_csi/read_pcap_csi returned 'not yet implemented'. Wired them to the real CsiParser/PcapCsiReader that already live in csi_receiver: - UDP: bind + recv + parse (auto-detect) -> CsiReadings. End-to-end test sends a real JSON datagram on the wire and parses it. - PCAP: load + read_next + parse. End-to-end test writes a real little-endian .pcap with one record and reads it back. - ESP32: parse CSI_DATA CSV via the real parser; live serial byte I/O behind an optional feature (native serialport gated off the default/appliance build) — without it, live reads return a typed UnsupportedAdapter while the byte parser still works (tested). Intel5300/Atheros/PicoScenes now return typed HardwareUnavailable/UnsupportedAdapter (no device/driver/validatable-format here) instead of fake CSI — added AdapterError::HardwareUnavailable and ::UnsupportedAdapter. Test asserts the gated adapters error honestly. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
982994ca3c |
fix(mat): real dimensionless GDOP = sqrt(trace((HtH)^-1)), not ad-hoc angle factor (ADR-158 §5)
estimate_gdop returned an average-pair-angle factor merely labelled GDOP (the same class of defect ADR-156 §2.3 fixed). Replaced with the genuine Geometric Dilution of Precision computed from the range-measurement Jacobian H (unit target->sensor bearings): GDOP = sqrt(trace((HtH)^-1)), dimensionless, returning None for singular (collinear) geometry which the caller treats as factor 1.0. Tests assert a well-spread array yields lower GDOP than a near-collinear one, cross-check the closed form, and confirm singular geometry returns None. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
c9a8ca758a |
feat(mat): real 3-point parabolic peak interpolation in find_dominant_frequency (ADR-158 §4)
The comment claimed interpolation but the function returned the bin center, capping breathing-rate resolution at +/-half a bin. Implemented quadratic (3-point parabolic) peak interpolation: delta = 0.5*(yL-yR)/(yL-2y0+yR), clamped to [-0.5,0.5], with an edge fallback to bin center. For a parabola-shaped peak the recovery is exact (delta=0.4 for a true peak at bin 10.4). Test asserts the result lands within half a bin of truth and strictly beats the old bin-center estimate. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
650e2b5c52 |
fix(mat): real RSSI localization + vitals-signature dedup, kill count inflation (ADR-158 §2)
simulate_rssi_measurements always returned vec![], so every survivor got location: None, which disabled spatial dedup — one person re-detected across N scan cycles became N survivors, fabricating a mass-casualty event. Two fixes: 1. Real RSSI source: SensorPosition gains an optional last_rssi (populated by the hardware layer from actual signal-strength readings). collect_rssi_measurements reads only real per-sensor RSSI and feeds the existing triangulator; it NEVER fabricates a value. <min_sensors real readings -> None location (honest). 2. Zone + vitals-signature dedup: when no usable location exists, record_detection matches an existing active, un-located survivor in the same zone whose latest vital signature (breathing presence + START rate band, heartbeat presence, movement class) is compatible — collapsing repeat detections of one person while keeping genuinely distinct survivors (different rate bands) separate. Tests (fail on old code): 3x identical-vitals/None-location -> 1 survivor (was 3); distinct vitals stay 2; real-RSSI path yields a position; no-RSSI path yields None. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
78821f1657 |
fix(mat): unify divergent triage engines to single canonical source (ADR-158 §1)
The ensemble gate (EnsembleClassifier::determine_triage) and the survivor record (Survivor::new -> TriageCalculator::calculate) used two different START-protocol approximations with different rate bands and movement handling. The pipeline gated on the ensemble triage then discarded it and recomputed via TriageCalculator, so a survivor could be admitted as one priority and recorded as another (e.g. 28 bpm + Tremor: gate said Delayed, record said Immediate). In a mass-casualty tool that divergence is a life-safety defect. determine_triage now delegates to TriageCalculator (the single source of truth), retaining only the ensemble confidence gate (low confidence -> Unknown, except Immediate which is never suppressed). Updated unit + integration tests to the canonical expectations and added a divergent-boundary regression asserting gate triage == survivor-record triage. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
67dd539e68 |
bench(pointcloud): sweep points-per-cell density for splats bench
Realistic depth backprojection is dense (many points per 8 cm voxel). Sweep
points-per-cell {4,16,64,256} at n=50k instead of point-count, so the
measurement reflects where the 9-pass→2-pass reduction actually applies.
Parity guard (old≡new, bit-for-bit) holds at every density.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
2754af804e |
feat(occworld): real conv encoder/decoder forward pass + honesty flag
Replace the `Tensor::randn` stubs in occworld-candle's VQVAE encoder (`encode_occupancy`) and decoder (`decode_to_logits`) with a real, deterministic, input-dependent convolutional forward pass. Previously `predict()` emitted trajectory waypoints + confidence that were a function of RANDOM NOISE, independent of the input and silently presented as model output — the exact "AI slop" the project must eliminate. occworld-candle: - New `cnn.rs`: `Encoder2D` (3× Conv2d + GELU, interpolate2d to pin the token grid) and `Decoder2D` (upsample_nearest2d + Conv2d + 1×1 head). Both are deterministic functions of the input — same input → identical output; different input → different output. No randn in any forward path. - Deterministic weight init (`det_fill`, seeded xorshift64*) across all `dummy()` constructors (encoder/decoder, VQ codebook, quant-convs, transformer), so untrained engines are bit-for-bit reproducible. - `InferenceOutput.weights_trained: bool` — honest disclosure flag. `false` for `dummy()` (real but untrained net), `true` only after `load()` reads a real checkpoint. Priors are always from the real forward pass, never faked. - VQ codebook + quant/post-quant convs kept and wired encoder→VQ→decoder. - Centerpiece tests in `tests/predict_honesty.rs` (input-dependence, run-to-run + cross-engine determinism, untrained flag). All three FAIL on the old randn stub (verified by temporarily reinstating randn). pointcloud: - Optimize `to_gaussian_splats` hot path: 9 separate `.iter().sum()` passes per voxel → 2 fused accumulation passes. Bit-identical output. - `benches/splats_bench.rs` (criterion) measures old 9-pass vs new 2-pass with a parity guard. ~1.3× faster on representative cloud sizes. - Confirmed: no `randn`/placeholder in any claimed production path. The remaining synthetic generators (`send_test_frames`, `demo_depth_cloud`) and honestly-flagged heuristics (`heuristic_pose_from_amplitude`, luminance pseudo-depth fallback) are explicitly disclosed, not faked output. DATA-GATED: a trained checkpoint. An untrained-but-real net is the honest deliverable; accuracy is flagged via `weights_trained`, never claimed. Tests: occworld 16 unit + 3 integration + 2 doc, pointcloud 18 — all pass (CPU `Device::Cpu`; CUDA feature is GPU-gated and untouched). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
7c80711454 |
feat(homecore-assist,homecore-recorder): replace stubs with real impls (ADR-132/133)
Implements the three placeholder paths with real, tested behaviour and an honest typed result wherever a capability is genuinely data-gated. homecore-assist: - runner.rs: add LocalRunner — runs the real IntentRecognizer pipeline and returns a fully-formed RufloResponse (resolved intent + speech). NoopRunner is now honest: typed NotStarted before spawn, explicit empty after (never a silent fabricated response). A live ruflo-agent.js subprocess remains the data-gated future path. - recognizer.rs / semantic_recognizer.rs: real SemanticIntentRecognizer — embeds the utterance (deterministic feature-hash embedding, new embedding.rs) and runs ruvector-core HNSW nearest-neighbour search over enrolled exemplars, accepting matches above a configurable cosine-similarity threshold (default 0.75) and falling back to regex below it. Measured: paraphrase "turn on the kitchen light" vs exemplar "turn on the light" -> sim 0.855 (match); "schedule a dentist appointment" -> sim 0.106 (no-match). `semantic` feature on by default. homecore-recorder: - db.rs: search_states_by_text — real SQL LIKE query over entity_id/state/attrs returning real rows (newest-first, k-capped, LIKE-escaped). search_semantic now falls back to it when the vector index yields no hits, so it is no longer always-empty under the default NullSemanticIndex. Tests (real behaviour; each fails on the old always-empty stub, verified): - homecore-assist: 39 passed / 0 failed - homecore-recorder (P1, no features): 19 passed / 0 failed - homecore-recorder (P2, --features ruvector): 25 passed / 0 failed All files < 500 lines; homecore-server consumer still builds. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
a0e72eef50 |
feat(wifiscan,sensing): native wlanapi.dll FFI + real Matter manual code
wifiscan (Tier 2 wlanapi adapter ONLY): - Real native wlanapi.dll BSS-list FFI (new adapter/wlanapi_native.rs): WlanOpenHandle -> WlanEnumInterfaces -> WlanGetNetworkBssList -> WlanFreeMemory/WlanCloseHandle via windows-sys 0.59 (already in lock tree). Per-BSSID RSSI(dBm)/channel/band/radio-type/SSID + CSI-capable filter. #[cfg(windows)] real path; #[cfg(not(windows))] returns typed WifiScanError::Unsupported (honest, never fabricated). - wlanapi_scanner now native-first with documented netsh fallback, native_scans metric, scan_native()/scan_native_csi_capable(), and a benchmark() that MEASURES real Hz (no hardcoded "10x" claim). - MEASURED 9.74 Hz native on ruvzen (30 iters, Native backend) vs netsh ~2 Hz baseline. Live measurement kept as an #[ignore] test. - Cargo.toml: unsafe_code forbid->deny so only the audited wlan_ffi module opts into unsafe; all unsafe confined + null-checked + freed. sensing-server (Matter commissioning): - Replaced the lossy modulo placeholder in matter/commissioning.rs with the real Matter Core Spec 1.3 §5.1.4.1.1 field-packing. Canonical vector (20202021, 3840) now encodes to the published 34970112332. - Added ManualPairingCode::decode + DecodedManualCode proving the code is real/lossless (passcode round-trips bit-for-bit; short discriminator = top 4 bits) with Verhoeff integrity, incl. proptest. Tests: wifi-densepose-wifiscan 145 passed (real FFI exercised on Windows); wifi-densepose-sensing-server 614 passed. 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
b0ee2a4aaf |
docs(soul): mark §3.6 matching algorithm as implemented + data-gated
Update specification.md §3.6 ONLY with an honest implementation-status note: the matching algorithm is now implemented and tested in v2/crates/wifi-densepose-bfld/, weights remain unvalidated design intent, and named-identity locking is data-gated (cardiac+respiratory alone are not separable — measured gap ~0.0005). The broader Soul Signature system remains Pre-Implementation. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
e2864bbd52 |
test(bfld): measured §3.6 separability + audit's cardiac-alone negative result
Deterministic synthetic-data tests producing reproducible, honestly-labeled numbers (MEASURED-on-synthetic, explicitly NOT real-person identification): - same_person_scores_higher_than_cross_person: self-match ≈1.0000, cross-person ≈0.8088 (full channels) — a real but modest ~0.19 margin. - cardiac_alone_cannot_separate_identity_matches_audit (centerpiece): with the decisive channels (AETHER 0.35, subcarrier 0.20) absent, cardiac (0.15) + respiratory (0.10) alone give same=1.0000 cross=0.9995, gap=0.0005 — no threshold fits, so the matcher correctly refuses to lock identity. Proves the audit's claim 'your heartbeat alone overlaps too much' with real numbers. - Graceful degradation, zero-norm/NaN safety, insufficient-channels typed result, empty-enrolled-set, threshold boundary, min-channels gate. 13 new tests; full crate suite 364 passed / 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
b08e49e47c |
feat(bfld): implement §3.6 Soul Signature matcher + real SoulMatchOracle
First running implementation of the spec's §3.6 per-channel weighted-cosine matcher (docs/research/soul/specification.md). Replaces reliance on NullOracle (which always returns NotEnrolled) with a real EnrolledMatcher oracle. - soul_channels.rs: 8-channel SoulChannels container (AETHER reuses IdentityEmbedding, preserving invariant I2 — no Clone/Serialize, zeroized on Drop), MatchWeights with the §3.6 default table (unvalidated design intent), heapless FeatureVector. no_std-compatible. - soul_match.rs: match_score() implementing the exact formula Σ w·cos / Σ w·availability, with graceful degradation, zero-norm/NaN safety, and a typed 'insufficient channels' result (never a default-high score). EnrolledMatcher (std) satisfies the existing SoulMatchOracle trait, gated on a score threshold AND a minimum shared-channel count (so a single low-weight channel can never lock identity). NullOracle retained as the disabled default. Named-identity locking remains data-gated: it requires real AETHER enrollment + body-resonance data, which has not been provided. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
66ebf798e5 |
docs(adr): ADR-157 Hardware/Sensing beyond-SOTA sweep — Milestone 3
Documents Milestone 3 across the four acquisition crates (vitals, hardware, wifiscan, calibration). Honest headline: this layer was already well-hardened, so the real work is small. - §A1 (perf, MEASURED): Vec::remove(0) O(n^2) sliding windows -> VecDeque. End-to-end win is NULL within noise at realistic window sizes (DSP dominates); the win is the algorithmic O(n^2)->O(n) shown in isolation. Claimed nothing more -- the committed bench proves the null. - §A2 (correctness): breathing partial-weights scale-mixing -> normalized by Sigma(effective weights). Pinned by two fail-on-old tests. - §A3 (stability): IIR resonator divergence. Corrected the research report's physically-inaccurate trigger (divergence needs |r|>=1, i.e. bw>=4, not "r negative"); clamp + finite-guard. Pinned by two fail-on-old tests. - §B1 hardening on an unreachable (already-gated) truncation path -- disclosed. - §B4 (constant-time HMAC compare) DEFERRED: not worth a new direct `subtle` dependency for an 8-byte LAN sync-beacon tag. - MEASURED negative-results section (the centerpiece): esp32_parser length gate, sync_packet infallible slices, the whole ieee80211bf validate-on-deserialize / no-panic-FSM / single-role / SBP-single-evaluate model, secure_tdm HMAC+replay, netsh_scanner fixed-argv + Option parse, geometry_embedding MAX_COORD_M -- each cited file:line, all NO-ACTION. - SOTA landscape: deep-CSI vitals (DATA-GATED), 802.11bf conformance (CLAIMED, non-public suite), per-room calibration (CLAIMED on numbers), native wlanapi FFI multi-BSSID (CLAIMED-unmeasured -- explicitly NOT claiming the 10x). Mostly NO-ACTION / ACCEPTED-FUTURE. - Deferred backlog (§8): nothing silently dropped. Validation: cargo test --workspace --no-default-features = 3054 passed / 0 failed; python verify.py = VERDICT PASS (hash unchanged, Rust-only changes). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
0b78eb6e03 |
fix(hardware): drop-instead-of-truncate subcarrier count in 802.11bf bridge (ADR-157 §B1)
OpportunisticCsiBridge::ingest built CsiReportPayload.n_subcarriers via `self.amp_accum.len() as u16`, which would silently wrap a count above 65_535. Replace with `u16::try_from(...).ok()?` (drop-instead-of-truncate). Disclosed honestly as defense-in-depth on an UNREACHABLE path: ingest already gates subcarrier_count > MAX_REPORT_SUBCARRIERS (484) at entry and report.validate() rejects oversized counts downstream, so the cast can never wrap in practice. Correct-by-construction rather than gate-dependent; no behavior change, no new test (the gate prevents the input that would exercise it). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
8fb6ef6547 |
fix(vitals): renormalize partial-weight fusion + clamp IIR resonator (ADR-157 §A2/§A3)
§A2 (correctness): BreathingExtractor weighted fusion was an un-normalized sum.
When `weights` was supplied shorter than n, supplied entries were used raw while
the missing tail defaulted to uniform 1/n -- two scales summed with no
renormalization, silently mis-scaling the breathing signal by a factor of
weights.len(). Extract to fuse_weighted_residuals() and normalize by
Sigma(effective weights), mirroring heartrate::compute_phase_coherence_signal.
Tests: partial_weights_are_renormalized_not_scale_mixed,
partial_weights_fusion_is_weighted_average (both fail on old code).
§A3 (stability): the IIR resonator pole radius r = 1 - bw/2 diverges when the
pole MAGNITUDE |r| >= 1 (i.e. bw >= 4: a very low fs relative to band width) --
NOT merely when r is negative, as the research report stated (a negative r with
|r| < 1 is still stable; the comments/tests are corrected accordingly). On
divergence the filter overflows to +/-inf within ~600 frames, NaN-poisons acf0,
and the extractor stalls permanently. Clamp r to [0, 0.9999] AND finite-guard
the filter output before the history push (defense-in-depth, mirrors ADR-154 §3).
Applied to both heartrate.rs and breathing.rs. Tests:
{heartrate,breathing}::low_sample_rate_filter_stays_finite (fs=0.5, 0.1-0.9 Hz
band, 600-frame unit step -> all-finite; both panic on old code).
These files also carry the §A1 VecDeque window conversion (bit-identical).
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
a7f7adfabc |
perf(vitals,wifiscan): O(1) VecDeque sliding windows + vitals bench (ADR-157 §A1/§D1)
Replace Vec::remove(0) (O(n) per-sample buffer shift -> O(n^2) full-window sweep) with VecDeque push_back/pop_front (O(1) eviction) in the fixed-length sliding/ring buffers of the vital-sign and wifiscan extractors. Where the autocorrelation / zero-crossing / Pearson loop needs a contiguous slice, make_contiguous() is called once per extract(), matching the idiom already used in wifiscan/pipeline/orchestrator.rs. Output is bit-identical. Sites: anomaly.rs (rr/hr history), store.rs (readings ring; history() now takes &mut self to hand back a contiguous slice, no external callers), wifiscan breathing_extractor.rs (filtered history), wifiscan correlator.rs (per-BSSID histories -> Vec<VecDeque<f32>>). (heartrate.rs/breathing.rs windows land with the §A2/§A3 fixes in a separate commit.) New criterion bench crates/wifi-densepose-vitals/benches/vitals_bench.rs drives each extractor over a full-window fill. Honest MEASURED result: end-to-end win is NULL within noise at realistic ESP32 window sizes (1500-3000) because the per-frame DSP dominates the eviction (heartrate 42.8ms->44.4ms, breathing 7.95ms->7.86ms, overlapping CIs). In isolation the eviction collapses O(n^2) -> O(n) (34.6x at window=3000, 3158x at window=100000); A1 lands as the correct data structure removing a latent O(n^2), NOT a claimed hot-path speedup. Reproduce: cargo bench -p wifi-densepose-vitals --bench vitals_bench Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
0ce2ac6440 |
docs(adr): ADR-156 RuVector/Fusion beyond-SOTA sweep — Milestone 2
Documents Milestone 2 of the beyond-SOTA sweep on the cross-viewpoint fusion path: four correctness/integrity/security fixes (each pinned by a bug-catching test), one MEASURED hot-path perf win, and the ANN/fusion SOTA landscape graded MEASURED/CLAIMED/data-gated. - Integrity: honest dimensionless GDOP (was RMSE mislabelled); canonical wrapped angular distance (disclosed numeric no-op under cos kernel — landed for contract/single-source-of-truth, not claimed as a behaviour change). - Security: crafted-index/zero-bin DoS panics closed on the multistatic path. - Perf: fuse() double-clone eliminated, ~2.17x on marshalling (MEASURED). - SOTA landscape: SymphonyQG (#1, CLAIMED — reproduction deferred) + multi-bit/Extended RaBitQ (#2, accepted near-term, the sketch.rs Pass-2); GraphPose-Fi learned fusion head documented ACCEPTED-FUTURE, data-gated per ADR-152 (b); CRB/sensor-placement investigated, no action (already SOTA). - Deferred backlog (§8): nothing silently dropped. Validation: cargo test --workspace --no-default-features = 3050 passed / 0 failed; python verify.py = VERDICT PASS. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
a92b043143 |
perf(ruvector): eliminate fuse() double-clone (~2.17x marshalling) + bench (ADR-156 §2.4, §4)
MultistaticArray::fuse / fuse_ungated cloned every viewpoint embedding twice per fusion (once into `extracted`, again when building the attention input). Now the embeddings are MOVED out of `extracted` (one clone per viewpoint instead of two), capturing geometry/ids by Copy in the same pass. Correctness-neutral — all 100 viewpoint/mat lib tests pass unchanged. MEASURED (new benches/fusion_bench.rs, embedding_extract A/B, 8 vp x 128-d): before_double_clone 1.0029 us -> after_single_clone 461.6 ns (~2.17x) End-to-end fusion_pipeline (8 vp): 202 us — marshalling is <1% of fusion (n*n attention dominates), so end-to-end win is modest; the A/B isolates the clone elimination. Reproduce: cargo bench -p wifi-densepose-ruvector --bench fusion_bench Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
a2daa2e443 |
fix(ruvector): crafted-input DoS — no panic on out-of-range indices (ADR-156 §2.2)
Security fix: two functions on a fusion/localisation path that can carry network-sourced multistatic frames panicked on crafted input (remote DoS). - triangulation::solve_triangulation indexed ap_positions[0] (empty table) and ap_positions[i]/[j] (crafted out-of-range AP index in a TDoA tuple). Now uses .first()? / .get(i)? / .get(j)? — returns None, never panics. - heartbeat::band_power computed n_freq_bins-1 (usize underflow on a zero-bin spectrogram) and did not clamp low_bin. Now guards n_freq_bins==0 and clamps both bounds into [0,last]; returns 0.0 for empty/inverted ranges. Tests (each panics on old code, verified by revert): triangulation_out_of_range_index_returns_none_no_panic, triangulation_empty_ap_positions_returns_none_no_panic, heartbeat_band_power_zero_bins_no_panic, heartbeat_band_power_out_of_range_bounds_no_panic. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
5b3e337c6d |
fix(ruvector): honest GDOP + canonical wrapped angular distance (ADR-156 §2.1, §2.3)
Two correctness/integrity fixes on the cross-viewpoint fusion geometry path, each pinned by a regression test that fails on the old code. - GDOP mislabel (§2.3): CramerRaoBound.gdop was `sqrt(crb_x+crb_y)` — identical to rmse_lower_bound (metres, noise-dependent), NOT a dimensionless GDOP. Now computes true GDOP = sqrt(trace(G^-1)) on the unit-variance bearing geometry, in both estimate() and estimate_regularised(); INFINITY (not NaN) for degenerate collinear geometry. Test gdop_is_dimensionless_and_noise_independent asserts GDOP is unchanged under 10x noise while RMSE scales 10x (old code failed: it scaled with noise, proving it was RMSE). - Angular wrap (§2.1): GeometricBias::build_matrix used raw |delta-azimuth| (can exceed pi, mis-states the 0/2pi seam) instead of the wrapped distance. angular_distance made pub and reused as the single canonical helper. HONEST: under the current cos() kernel this is a NUMERIC NO-OP (cos is even/periodic, cos(raw)==cos(wrapped)); landed for contract correctness + single-source-of- truth + future non-even kernels, not as a behaviour change. Tests pin the contract (wrapped value in [0,pi], seam symmetry). ruvector lib tests: 100 passed / 0 failed (+ new tests). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
ea5ead7fb7 |
docs(adr): ADR-155 NN/training beyond-SOTA sweep — Milestone 1
Records the integrity-critical fixes (unified canonical metric, leak-free subject-disjoint split + synthetic-val disclosure, rapid_adapt real gradients, proof margin + committed-hash rigor), the Tier-2 correctness/security fixes, the measured Tier-3 perf win, the NN SOTA landscape graded MEASURED/CLAIMED/ THEORETICAL (GraphPose-Fi as top ACCEPTED-future candidate; INT4; CSI-JEPA-vs-MAE with the honest "no JEPA/MAE-on-WiFi-pose yet" caveat; "Mamba-CSI-pose does not exist"), and the ~45-finding deferred backlog. Discloses the libtorch/tch-gating limitation and that the Rust proof is honestly in SKIP until a baseline is committed. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
5cacb5fe0a |
perf(nn): zero-copy ORT input (~1.48x) + dynamic-dim guard + concurrency bench (ADR-155 §Tier-3)
- onnx.rs ORT input: arr.as_slice() single-memcpy fast path with iterator fallback for strided views. MEASURED [1,256,64,64]: 1.972ms -> 1.336ms (~1.48x). Repro: cargo bench -p wifi-densepose-nn --no-default-features --features onnx --bench onnx_bench -- onnx_input_copy - onnx.rs checked_output_dims: reject ONNX dim <= 0 (incl. unresolved -1) before allocation (config-OOM class) + test. - onnx_concurrency bench: empirically proves the per-inference write lock serializes (throughput drops with more threads). The intended read-lock win is NOT landable on ort 2.0.0-rc.11 (safe Session::run is &mut self, verified) and is deferred to the backlog with the upgrade path documented in-code. New committed fixture tests/fixtures/tiny_conv.onnx (666 B, not gitignored). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
aa3a6725a6 |
fix(train,nn): Tier-2 correctness/security — metric scale, OOM bounds, panics (ADR-155 §Tier-2)
Each fix ships a test that would have caught the bug: - ruview_metrics OKS: derive scale from GT extent (no s=1.0 fake-Gold), reject s<=0, bound the loop to array extents (no panic on short/adversarial input). - config.validate(): UPPER bounds on window_frames/subcarriers/backbone_channels/ heatmap_size/keypoints/body_parts/batch_size + reject negative gpu_device_id (closes the config-OOM class); defaults+presets still validate. - subcarrier.rs: graceful fallback instead of panic on non-contiguous input. - ablation.rs latency_percentiles: total_cmp + NaN guard (no partial_cmp unwrap). - tensor.rs softmax(axis): normalize per-lane along the given axis (was whole- tensor), out-of-range axis -> NnError; fixes densepose per-pixel probs. - translator.rs apply_attention: real scaled-dot-product attention (was a uniform 1/seq_len stub that made any "with attention" ablation == without); mis-shaped checkpoint projections rejected. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
84e2c920fd |
fix(train): proof margin + committed-hash requirement (ADR-155 §Tier-1.4)
The deterministic proof self-certified: PASS on any loss decrease (incl. 1e-9 noise) and a missing expected hash defaulted to PASS. - MIN_LOSS_DECREASE=1e-4: a run counts as learning only above float noise; a noise-only pipeline now FAILS. - is_pass() requires hash_matches==Some(true); no-hash -> SKIP (exit 2), never PASS. verify-training fails fast on a sub-margin loss before the hash compare, so a missing baseline cannot mask a non-learning pipeline. Documented honestly: the proof certifies reproducibility/determinism on a synthetic dataset, NOT that real data produced the weights nor that any accuracy claim is met. Tests: no_committed_hash_is_skip_not_pass, submargin_loss_change_fails_even_without_hash, committed_matching_hash_with_real_decrease_passes. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
7fb3e33557 |
fix(train): rapid_adapt real finite-difference gradients, not a fake step (ADR-155 §Tier-1.3)
contrastive_step/entropy_step wrote a fake gradient (grad += v*0.01) unrelated to the stated objective, so any "TTA improves the metric" was unsupported. The *_loss functions are now pure evaluators of the real objective; adapt() descends them with a central finite-difference gradient of that exact loss, so "the adaptation loss decreases" is now a real, reproducible measurement. Honest scope caveat (documented): this minimizes a self-supervised proxy over a LoRA bottleneck on raw CSI; it is NOT wired to the pose model and there is NO measured end-to-end PCK gain on WiFi pose from this path. Tests: contrastive_loss_decreases, entropy_loss_decreases (real gradient steps don't increase the loss), reported_loss_is_the_real_objective_not_a_placeholder. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
2a2a2c5b06 |
fix(train): leak-free subject-disjoint split + synthetic-val disclosure (ADR-155 §Tier-1.2)
MM-Fi windows are stride-1 (~99% overlap), so an index-level split leaks; and
bin/train.rs validated real training against a SYNTHETIC val set, making any
printed PCK meaningless on two counts.
- MmFiDataset::subject_disjoint_split partitions whole subjects -> the two views
share no subject and no window (leak-free by construction, deterministic per
seed). assert_split_leak_free verifies subject- AND window-disjointness and is
called inside the split so a leaky split is never handed out.
- bin/train.rs now prefers the real split; the synthetic path is a labelled
run_smoke_test ("[SMOKE-TEST] DO NOT REPORT") reachable only as a fallback.
- New DatasetError::InvalidSplit.
Tests prove disjointness, determinism, single-subject/bad-fraction rejection,
and that the validator catches an injected subject leak.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
50b657459f |
fix(train): unify 7 divergent PCK/OKS into one canonical metric (ADR-155 §Tier-1.1)
Collapse the four PCK and three OKS implementations into a single source of
truth — pck_canonical (torso hip↔hip, COCO/ADR-152 convention validated at
~96% PCK@20 in benchmarks/wiflow-std) and oks_canonical (scale from GT pose
extent). MetricsAccumulator, compute_pck/_per_joint/_oks, aggregate_metrics and
the deprecated *_v2 path all route through them, so Trainer::evaluate() and the
bench definition agree.
Fixes two claim-inflating bugs, each pinned by a regression test:
- zero-visible-joint PCK was 1.0 (false-perfect) -> now 0.0
- OKS s=1.0 on normalized coords made OKS~=1.0 for any pose ("fake Gold tier")
-> scale now derived from the pose; a 3x-torso-wrong pose yields OKS<0.2
Divergent local kernels (training_bench raw-threshold, sensing-server
torso-height) annotated "DO NOT USE for reported metrics". Legitimately changed
test expectations (all-coincident "perfect" fixtures are correctly unscoreable;
all-invisible -> 0.0) updated with comments citing the finding.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
6511ca90fb |
docs(adr): ADR-154 signal/DSP beyond-SOTA sweep — Milestone 0
Records Milestone-0 of the signal/DSP beyond-SOTA sweep with full PROOF discipline (MEASURED vs CLAIMED vs THEORETICAL grading throughout): - §2 discloses the headline anti-slop finding: the ADR-134 CIR coherence gate was DEAD in production (canonical-56 frames -> SubcarrierMismatch -> silent freq-domain fallback for every frame). Documents the canonical56() fix + the 4 committed proof tests. - §3 NaN/inf adversarial bypass; §4 divide-by-(n-1) window trio. - §5 the two MEASURED perf wins with before/after medians + reproduce commands. - §6 per-module SOTA landscape, evidence-graded: deep-unfolded ISTA/LISTA for CSI->CIR (~3 dB NMSE, MEASURED, arXiv 2211.15440 + 2502.05952), diffusion CIR prior (public weights, MEASURED), Wi-Spoof adversarial eval (MEASURED, arXiv 2511.20456), Bayesian multi-AP fusion (CLAIMED, no code, 2512.02462), coherence gating + RF intention-lead (THEORETICAL). - §7 roadmap: LISTA-for-CIR as the top ACCEPTED-future item (M effort; the ISTA + Phi already exist in cir.rs) — proposed, NOT implemented this milestone — plus the explicit deferred-findings backlog (the ~45 review findings not fixed here, graded P1/P2/P3) so nothing is silently dropped, with a horizon-ledger DONE-vs-DEFERRED one-liner. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
4d384cb884 |
perf(signal): cache PSD FFT planner (2.0–3.1x) + honor DTW band (2.4–4.1x) (ADR-154 M0)
Two measured, bit-equivalent perf wins. Each ships a criterion bench
(benches/features_bench.rs, new) with before/after numbers and a committed
bit-identity test — no perf claim without a measured before/after.
PSD FFT-planner caching (features.rs)
PowerSpectralDensity::from_csi_data re-planned a FftPlanner on EVERY frame,
and FeatureExtractor::extract calls it per frame on the hot path. New
from_csi_data_with_fft(csi, n, &Arc<dyn Fft>) reuses a plan cached in
FeatureExtractor (built once in new()). Bit-identical output
(psd_cached_fft_bit_identical_to_fresh, f64::to_bits over 6 sizes).
MEASURED (median ns/frame, criterion):
fft=64 5.84µs -> 1.89µs (3.09x)
fft=128 9.31µs -> 3.61µs (2.58x)
fft=256 13.77µs -> 6.73µs (2.04x)
DTW Sakoe-Chiba band (gesture.rs)
dtw_distance computed j_start/j_end but iterated the FULL 1..=m row,
continue-ing out-of-band — band constrained the path, not the work (O(n*m)).
Now iterates j_start..=j_end (O(n*band)), resetting only the two boundary
guard cells the recurrence reads, with endpoint reachability (|n-m|<=band)
at the return. Bit-identical across 12 shapes x 8 bands
(dtw_banded_bit_identical_to_fullrow).
MEASURED (median, criterion):
n=m=100 band=5 33.45µs -> 13.77µs (2.43x)
n=m=200 band=5 122.32µs -> 29.55µs (4.14x)
n=m=200 band=10 159.98µs -> 60.19µs (2.66x)
Reproduce:
cd v2 && cargo bench -p wifi-densepose-signal --no-default-features \
--bench features_bench
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
be068748b3 |
fix(signal): revive dead CIR coherence gate + NaN bypass + window div0 (ADR-154 M0)
Milestone-0 correctness/security fixes for the beyond-SOTA signal/DSP sweep.
Every fix ships with a committed regression test (proof, not adjectives).
CRITICAL — ADR-134 CIR coherence gate was DEAD in production
MultistaticFuser fuses canonical-56 frames (hardware_norm.rs resamples every
chipset onto a 56-tone grid), but the gate was wired to CirConfig::ht20()
which expects 64/52. Every estimate() returned SubcarrierMismatch and
cir_gate_coherence silently fell back to freq-domain coherence — use_cir_gate
was indistinguishable from false. Fixes:
- new CirConfig::canonical56() (64-bin HT20 framing, 56 active tones, 168 taps)
- new MultistaticFuser::with_cir_canonical56() (correct default); ht20 kept,
now doc-warned
- active_indices() handles (64,56) + length-matched fallback (no silent
fall-through to the 52-index slice)
- SubcarrierMismatch in the gate now debug_assert!s loudly (config error can
no longer hide as a graceful degrade)
- cir_estimate_first() exposes the Ok/Err verdict for tests
PROOF (ruvsense::multistatic::tests): ht20 → 8/8 Err (dead); canonical56 →
8/8 Ok (alive); coherence(gate on) != coherence(gate off).
CRITICAL — adversarial.rs NaN/inf detector bypass
One non-finite link energy bypassed the whole detector (every `e>thresh`
false on NaN; score clamp returns NaN). A non-finite input is itself the
strongest spoof — now short-circuits to a definite anomaly (score 1.0,
affected link reported) and does not poison the temporal-continuity state.
PROOF: nan_link_energy_flags_anomaly, inf_link_energy_flags_anomaly.
CORRECTNESS — divide-by-(n-1) window trio
csi_processor hamming_window (n=0 usize underflow, n=1 div0), bvp Hann,
spectrogram make_window all guarded for n<=1 (empty / constant-1.0 window).
Python deterministic proof still PASS, same pipeline hash (reference uses n>=2).
PROOF: *_degenerate_sizes / *_size_one_is_finite / make_window_size_0_and_1.
CLARITY — calibration.rs subtract_in_place
Removed the vacuous `if active_input {ki} else {ki}` branch that implied a
full-FFT->bin remap that never existed; documented the sequential
active-index convention (matches sibling extract_first_stream). No behavior
change.
Tests: cargo test -p wifi-densepose-signal --no-default-features (+--features cir)
green; full workspace green; verify.py VERDICT: PASS.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
07b6bf8084 |
chore: extract ruv-neural to ruvnet/ruv-neural, wire as submodule (#1019)
The 12-crate brain-topology analysis ecosystem (v2/crates/ruv-neural) was a self-contained nested workspace with no inbound deps from the v2 workspace (verified: zero path references outside its own tree). Published standalone at github.com/ruvnet/ruv-neural and re-attached here as a submodule at the same path, so the build layout is unchanged while the project gets its own repo/CI/release cadence.v1677 |
||
|
|
d22616c488 |
docs(research): WiFlow-STD audit writeup (published as public gist + upstream issue)
Gist: https://gist.github.com/ruvnet/47d4369c0bd251ed233bbc450d50f6e6 Upstream report: DY2434/WiFlow...issues/3 Co-Authored-By: claude-flow <ruv@ruv.net>v1674 v1675 |
||
|
|
17471e93ff |
ADR-152: WiFi-Pose SOTA 2026 intake — WiFlow-STD benchmark, Rust integrations, ADR-153 802.11bf layer, efficiency frontier (#1008)
* feat(calibration): NodeGeometry transceiver-geometry recording (ADR-152 §2.1.1) PerceptAlign-motivated geometry capture at enrollment: per-node optional records (position, antenna orientation, inter-node distances, acquisition method) — recorded when known, never required. Event-sourced via EnrollmentEvent::GeometryRecorded (latest recording wins); persisted on SpecialistBank with serde defaults so pre-ADR-152 bank JSON loads cleanly (fixture-proven, and geometry-free banks serialize byte-shape-identical to the old schema); threaded through MultiNodeMixture as data only — the learned geometry embeddings and algorithmic fusion use are §2.1.2, deliberately deferred until the ADR-151 P6 LoRA heads exist. Geometry recorded from now on means banks captured today remain usable for layout-conditioned training later — you can't retroactively add geometry to data you didn't record. 8 new tests (3 geometry, 2 anchor, 2 bank, 1 multistatic) + full-loop extension (2-node geometry, one tape-measured + one unknown, surviving the bank JSON round-trip the runtime loads from). 50/50 calibration (both feature configs) + 23 CLI tests green. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(training): two-checkerboard camera↔room calibration for ADR-079 labels (ADR-152 §2.1.3) Defends the camera-supervised pipeline against PerceptAlign's "coordinate overfitting": MediaPipe keypoints were emitted in raw camera coordinates with no shared frame and no transceiver-geometry metadata — the exact label shape that memorizes deployment layout and collapses cross-layout. - scripts/calibrate-camera-room.py + calibration_lib.py: OpenCV two-checkerboard calibration → versioned bundle JSON (intrinsics, camera→room extrinsics, checkerboard spec, transceiver geometry, sha256 calibration_id). Intrinsics resolve from file > cache > multi-view computation > loud-warning 2-view fallback. - collect-ground-truth.py --calibration <bundle>: every sample gains keypoints_room (unit bearing rays from the camera center in the room frame — documented projective alignment; raw image coords preserved so training chooses), camera_origin_room, calibration_id, and the transceiver geometry stamp. Without the flag, output is byte-identical to before (tested) + a one-line ADR-152 warning. Design finding (recorded for ADR-152): a single planar checkerboard's corner grid is centrosymmetric — the reversed corner ordering fits a ghost camera pose with IDENTICAL reprojection error, so per-board flip disambiguation is mathematically ill-posed. solve_two_board_extrinsics solves the joint wall+floor set over all 4 flip combinations, where the minimum is unique — an independent reason the TWO-checkerboard method is required, beyond what PerceptAlign states. 15 headless pytest tests green (synthetic corners: extrinsics recovery incl. ghost resolution, bundle round-trip + hash stability, ray transforms w/ distortion + cross-resolution, no-calibration byte identity). Co-Authored-By: RuFlo <ruv@ruv.net> * feat(benchmarks): WiFlow-STD reproduction harness + measurement (a) results (ADR-152 §2.2) Shipped checkpoint REFUTED (0.08% PCK@20, wrong keypoint normalization); 6 reproducibility defects documented (broken imports, corrupted dataset tail with float32-max garbage that NaN-poisons fp16 BatchNorm, unreachable test phase). After repairs, retraining with upstream defaults reproduces 96.09% PCK@20 full-test / 96.61% corruption-free (published 97.25%) on RTX 5080. Claims graded MEASURED-EQUIVALENT; 2.23M params + ~0.055 GFLOPs verified. Third-party code/weights/data stay out of tree (gitignored). Co-Authored-By: claude-flow <ruv@ruv.net> * feat: ADR-152 Rust integrations + ADR-153 802.11bf protocol model - calibration: GeometryEmbedding — 32-slot permutation-invariant NodeGeometry featurization for future LoRA-head conditioning (ADR-152 §2.1.2); derived SpecialistBank::geometry_embedding() accessor; 59 tests - train: MaePretrainConfig + patchify/random-mask with UNSW measured recipe (80% masking, (30,3) patches; ADR-152 §2.3, arXiv 2511.18792); strict no-truncate/no-NaN policy; proptest properties - train: WiFlowStdModel — tch-gated port of the verified ~96%-PCK@20 WiFlow-STD architecture (ADR-152 §2.2 beyond-SOTA); ungated param formula pinned to 2,225,042; 15/17-keypoint support; 239 crate tests - hardware: ieee80211bf forward-compatibility protocol model (ADR-153): SpecProfile gates, SensingCapabilities negotiation, required ConsentMode, session FSM, SensingTransport + SimTransport + OpportunisticCsiBridge; full acceptance checklist covered; 156+4 tests - deps: ruvector bumps per ADR-152 §2.6 survey (mincut/solver 2.0.6, attention 2.1.0, gnn 2.2.0); vendor/ruvector synced to a083bd77f - docs: ADR-153 accepted; ADR-152 §2.2 status, §2.4 amendment, §2.6 added Workspace: 162 test suites green (--no-default-features); Python proof PASS. Known pre-existing flake: homecore-api env_empty_falls_back_to_defaults (unserialized env-var mutation) — untouched, follow-up. Co-Authored-By: claude-flow <ruv@ruv.net> * docs: CHANGELOG + CLAUDE.md entries for ADR-152 integrations and ADR-153 Co-Authored-By: claude-flow <ruv@ruv.net> * fix(train): repair tch-backend bit-rot — gated path compiles and tests run again Mechanical API refresh against current tch: Vec::from(Tensor) -> try_from (+ explicit flatten), numel() usize cast, Rem/div ops -> remainder() / divide_scalar_mode(floor) — the latter fixed a silent true-division bug in heatmap argmax decoding; clamp(1.0, f64::MAX) -> clamp_min (torch 2.x scalar overflow panic); petgraph EdgeRef import; missing EvalMetrics and verify_checkpoint_dir APIs that tests documented. wiflow_std roundtrip test uses safetensors (.pt _save_parameters roundtrip broken in torch 2.11 Windows). Gated: 349 passed (incl. all 20 wiflow_std); ungated: unchanged. Known pre-existing: gaussian-heatmap convention mismatch (2 tests), proof seed race under parallel threads — documented, deliberate follow-ups. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(train): WiFlow-STD PyTorch->tch weight import + numerical parity proof export_to_safetensors.py maps the retrained checkpoint (295 tensors -> 248 mapped, param sum exactly 2,225,042; num_batches_tracked dropped) into a tch-loadable safetensors plus a deterministic parity fixture. Gated #[ignore] integration test loads it strictly and asserts forward-pass agreement: max abs diff 1.192e-7 on the seed-42 fixture. dump_variable_names test makes the tch name layout authoritative. Zero architecture discrepancies found. Co-Authored-By: claude-flow <ruv@ruv.net> * fix: workflow-review findings — BN gamma init, ThresholdParams serde, init docs Concurrent validation workflow (2 review lanes + adversarial verification, 13 agents): 5 confirmed findings, 3 refuted. Fixes: - wiflow_std: pin BatchNorm gamma to 1.0 (tch default draws Uniform(0,1) — silently halves activations in from-scratch training; loaded checkpoints unaffected, parity re-verified after the change) - wiflow_std: document the conv-init divergences vs the reference's effective kaiming_normal(fan_out) re-init (from-scratch dynamics only) - ieee80211bf: ThresholdParams deserialization validates via try_from so the <=100 invariant holds for untrusted payloads (+ rejection test) Benchmarks (release, ruvzen): GeometryEmbedding 1.84us/call (542k/s), MAE tokenization 7.38us/window (135k/s), 802.11bf FSM 8.9M events/s — nothing suspicious. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(adr): ADR-152 §2.1.4 gate resolved — PerceptAlign repo MIT, dataset on HF Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): edge optimization measured + measurement (b) blocked + 92.9% retraction Edge optimization (ADR-152 optimize track): ONNX Runtime fp32 is the CPU latency win (3.2 ms/window, ~3.4x faster than torch, parity 2.4e-7); ORT dynamic int8 reaches 2.44 MB (paper's ~2.2 MB claim plausible only via conv-capable toolchains; -0.16pt PCK@20, +18% MPJPE, 2x slower); torch dynamic quant converts 0% of this conv-only model; fp16 halves storage free but is slower on CPU. Measurement (b) BLOCKED-ON-DATA: only 1,077 paired ESP32 windows exist (stop rule <2k). Forensic recheck of the surviving April holdout RETRACTS the ADR-079 '92.9% PCK@20' figure: constant-output model, absolute (not torso) threshold, 69 near-static frames — mean predictor scores 100% under that protocol; torso-PCK@20 is 19.1%. Corroborates PR #535. Stale citations removed from user-guide, readme-details, ADR-152 §2.1.3; no-citation rule extended to ADR-079 accuracy claims. Unblock: >=2k-window multi-pose paired session + torso-PCK re-baseline. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(user-guide): corrected camera-supervised collection tutorial Step 0 CSI-rate check + session-length math (window yield = frames/20 — the May session's 8x under-delivery was a ~12 Hz CSI rate, not an aligner bug); two-checkerboard calibration step (ADR-152 §2.1.3); pose-variety and confidence guidance; torso-normalized PCK + temporal-split + pred-variance eval protocol (lessons from the 92.9% retraction); scale presets re-keyed to realistic window counts. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): static PTQ int8 (calibrated) results + overnight capture script Conv-only static QDQ beats dynamic int8 on accuracy (PCK@20 96.61-96.63% vs 96.52%, MPJPE +10% vs +18% over fp32) at ~equal size/latency; all-ops QDQ strictly worse (int8 activations through attention glue). Entropy calibration verified bit-identical to MinMax on this data. Deployment: ONNX fp32 for speed (3.2ms), static conv-only QDQ for smallest (2.53MB). Also: scripts/overnight-empty-capture.py — segmented UDP CSI recorder for empty-room baselines (no glob collisions, detach-safe). Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): measurement (b) MEASURED — optimization transfer only, mean-pose baseline wins WiFlow-STD fine-tuned on 2,046 fresh single-room ESP32 paired windows (temporal 70/15/15, 70->540 adapter, K=17): pretrained-init 65% PCK@20 vs scratch 0% (optimization transfer) but frozen-trunk ~0% (no feature transfer), and NOTHING beats the mean-pose baseline (95.9% PCK@20 — single subject, near-static normalized coords). Honesty gates held: pred std 0.0113 (non-constant model) but mean-baseline dominance means no citable CSI->pose capability from this data. ADR-152 open question 1 answered partially; definitive answer needs multi-subject/position data. Two new aligner findings: heterogeneous csi_shape with silent zero-padding (~20%), and extractCsiMatrix's transposed shape label (frame-major data, [nSc, nFrames] label) — fixes pending. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): efficiency sweep MEASURED — half model dominates full reference Compact WiFlow-STD variants on the same data/split/protocol: half (843,834 params, 0.38x) strictly dominates the 2.23M reference (PCK@20 96.62 vs 96.61, PCK@50 99.47 vs 99.11, MPJPE 0.00898 vs 0.0094) — the published architecture is over-parameterized for its own benchmark. quarter (338k) 96.05%; tiny (56,290 params, 1/39.5) holds 94.11% — a ~220KB fp32 edge candidate. In-domain caveats recorded; cross-domain untested. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(train): compact WiFlow-STD presets in Rust + tiny edge artifact (ADR-152) WiFlowStdConfig gains half()/quarter()/tiny() mirroring the overnight sweep exactly: TcnGroupsMode (Fixed/Gcd/Depthwise), input_pw_groups, derived stride schedule and decoder-mid (all default to upstream behavior; legacy serde JSON unaffected). Param formulas pin to trained ground truth first try: 843,834 / 338,600 / 56,290; default 2,225,042 pin and 1.192e-7 parity unchanged. 248 tests green. Tiny edge artifact (tiny_edge_bench.py): ONNX fp32 = 295 KB, 0.66 ms/win (~1,500/s CPU), 94.11% PCK@20 (matches sweep clean-test exactly; parity 1.49e-7). Static int8 is a bad trade at this scale (-1.43pt, +19% MPJPE, -16% size, slower) — recorded as negative result. Export note: width-16 breaks AdaptiveAvgPool((15,1)) TorchScript export; replaced by exact mean+matmul equivalent, proven by parity. Co-Authored-By: claude-flow <ruv@ruv.net> * fix: resolve all 10 confirmed code-review findings (7-angle review, 20/20 verified) wiflow_std: min_feature_width (default 15) replaces the keypoints->stride coupling — for_keypoints(17) now provably builds the trained [2,2,2,2] graph and pools 15->17, matching the validated Python protocol (pinned by tests); param_count() total on invalid configs; random_mask returns Result and rejects non-finite/out-of-range ratios; trainer checkpoints switched to safetensors (.pt VarStore roundtrip broken on Windows torch 2.11). ieee80211bf: SBP proxy now re-triggers instances and relays reports via Action::RelaySbpReport -> SensingFrame::SbpReport (clients consume via their existing path); missed_instances reset on success = consecutive semantics; SessionTable gains a guarded SBP entry point + unknown-id drop counter; initiator-role sessions reject inbound setup/SBP requests (RejectedNotSupported) closing the idle hijack; StartSetup/StartSbp outside Idle return InvalidStateForCommand; SBP validation unified through evaluate_setup with a 1:1 SetupStatus->SbpStatus mapping. events.rs split out to honor the 500-line cap. calibration/cli: enrollment geometry now actually reaches trained banks — both production call sites attach .with_geometry; --geometry flag on train-room and POST /enroll/geometry + train-body geometry on calibrate-serve give production a recording surface; geometry-free banks log the ADR-152 §2.1.2 note. benchmarks: corruption masks committed as ground truth (unregenerable after in-place cleaning; verified bit-identical regeneration from the pristine copy) + generate_corruption_masks.py producer; _bench_common.py dedups the 5x-copied shim/evaluate/seed/remap (post-refactor PCK@20 re-verified equal to the last digit); remote scripts get the mmap patch; tiny_edge --calib validated multiple-of-64; onnx_bench --help no longer executes (and overwrote) the export — artifact restored byte-exact. Workspace: 2,963 tests passed, 0 failed; Python proof PASS. Co-Authored-By: claude-flow <ruv@ruv.net> * ci: build workspace tests without debuginfo — runner disk exhaustion The combined 38-crate debug target exceeds the GitHub runner's disk ('final link failed: No space left on device'); the same tree measured 151GB locally with full debuginfo. CARGO_PROFILE_{DEV,TEST}_DEBUG=0 shrinks the target ~5-10x; debuginfo serves no purpose in CI test runs. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
29de574e63 |
Beyond-SOTA engine/signal/train improvements: mesh partition guard, FFT CIR solver, canonical frame decoder, falsifiable occupancy benchmark, governed streaming, adapter provenance (#1018)
* docs(research): add RuView beyond-SOTA system review (00) First document of the beyond-SOTA research series: capability audit of the current RuView engine with role-to-crate maturity matrix, ruvsense module inventory, gap analysis, and risk register. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): add beyond-SOTA architecture design (02, in progress) https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): finalize beyond-SOTA architecture (02) https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): add benchmark/validation methodology snapshot (03) https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): add beyond-SOTA series index with validation results; changelog README index ties the 5 research docs together with the session's measured validation evidence: 2,797 workspace tests / 0 failed, Python proof PASS (bit-exact), and paired pre/post criterion CIR benchmarks. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * perf(signal): precompute CIR warm-start system; hoist tomography solver allocs Exact, determinism-safe optimizations (bit-identical float results): - cir.rs: diag(PhiH Phi)+lambda*I and its CSR matrix depend only on Phi and lambda (fixed at CirEstimator::new) but were rebuilt every frame (O(K*G) pass + CSR allocation). Now built once in new() via build_warm_start_system; summation order unchanged. - tomography.rs: ISTA gradient buffer hoisted out of the 100-iteration loop (fill(0.0) reset) and the Frobenius Lipschitz bound moved from per-reconstruct to construction. Verified: signal 456 tests green; engine 11/11 green including cycle_is_deterministic and witness-stability tests. Criterion paired pre/post: cir_estimate/he40 -3.9% (p<0.01), multiband -1.2/-1.4%. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * fix(worldgraph): bound SemanticState growth with deterministic retention StreamingEngine::process_cycle appended one SemanticState belief per cycle with no eviction — ~1.7M nodes/day at 20 Hz (beyond-SOTA roadmap finding #6). Add 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) are never eligible. Wire it into the engine after each belief append (DEFAULT_SEMANTIC_RETENTION = 7,200, ~6 min at 20 Hz; set_semantic_retention to tune). The WorldGraph holds current beliefs; durable history is the recorder's job, so no audit data is lost. 3 new tests: end-to-end bounded growth, oldest-only eviction, deterministic equal-timestamp tie-break. Workspace gate: 2,865 passed, 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(sensing-server): route live frames through the governed StreamingEngine Closes the live-trust-path gap (ADR-136 section 8, beyond-SOTA system review): the running server fused live CSI with the bare MultistaticFuser, while the privacy/provenance/witness control plane (ADR-135..146) only ever ran on synthetic in-test frames. The privacy control plane was therefore bypassable on the real path. New engine_bridge module drives StreamingEngine::process_cycle from the server's live NodeState map, reusing the existing NodeState -> MultiBandCsiFrame conversion. It lazily wires each contributing node as a WorldGraph sensor (idempotent), bounds belief growth via the retention cap, and forwards explicit timestamps/calibration ids so the path stays deterministic and replayable. Wired additively into both live ESP32/WiFi fusion sites in main.rs via a split-borrow off the write guard, so person-count behavior is unchanged; the latest BLAKE3 witness is stored on AppState. Every published belief now carries evidence + model + calibration + privacy decision and a deterministic witness. Adds wifi-densepose-engine/-worldgraph/-bfld/-geo deps. 6 new bridge tests (witnessed belief with full provenance, cross-run determinism, idempotent node registration, retention bound, privacy-mode propagation). sensing-server suite 430+128 green; workspace gate 2,904 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(train): falsifiable occupancy benchmark with anti-overfitting gate Makes the presence/person-count "beyond SOTA" claim falsifiable in code instead of aspirational (the unfalsifiability gap from the beyond-SOTA system review). occupancy_bench grades predictions vs ground truth and gates a SOTA claim behind one claim_allowed invariant requiring ALL of: - DataProvenance::Measured — synthetic/mock data is scorable for regression but never claimable (anti-mock-contamination; the CLAUDE.md Kconfig-bug lesson made structural). - A leak-free EvalSplit — validate() refuses any split where a subject OR environment id appears in both train and test (subject leakage / per-environment overfitting). - n_test >= min_test_samples (small-N guard). - Presence F1 whose bootstrap-CI lower bound (deterministic seeded splitmix64) clears the threshold — not the point estimate. - Count MAE within threshold. The claim string is unreadable except through the gate (NO_CLAIM otherwise), same discipline as the ruview-gamma acceptance gate. What remains is data, not method: a frozen, SHA-pinned, subject/environment-disjoint measured replay set turns the claim into a passing/failing test. Lives in wifi-densepose-train (the eval bounded context, alongside ablation/ eval/metrics). 10 tests cover each refusal path; warning-clean under the crate's missing_docs lint. Workspace gate 2,914 passed / 0 failed. Doc 03 updated. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(engine): per-room adapter provenance + drift-to-recalibration advisor Closes the trust-chain gap where an ~11 KB per-room LoRA adapter (ADR-150 section 3.4) could silently change inference without the witness noticing: provenance carried only "rfenc-v<N>" with no notion of adapter identity. - 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. - RecalibrationAdvisor: recommends re-running the ADR-135 empty-room baseline / refitting the room 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, stored on the sensing-server AppState alongside the witness at both live fusion sites. - Bridge plumbing: EngineBridge::{set_room_adapter, clear_room_adapter} + live-path test that the adapter id flows into the live witness. Scope note (honest): 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/); a trained RF-encoder checkpoint still does not exist in-tree. Engine 15 tests, bridge 7 tests. Workspace gate: 2,918 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * fix(mat): gate api module behind its feature — standalone no-default-features builds pub mod api was unconditional while its only dependency, serde, is optional behind the 'api' feature, so any build without default features failed with 101 unresolved-serde errors (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 combos compile: bare --no-default-features (was 101 errors, now 0), --no-default-features --features api, and full default (177 tests pass). Workspace gate: 2,918 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * perf(signal): opt-in FFT operator for the CIR ISTA solver (8-14x measured) Phi 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 — the dominant-latency-hazard finding from the beyond-SOTA optimization roadmap. 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 in the last bits and requires regenerating any pinned witness — strictly opt-in per deployment. FftOperator (rustfft, planned once at CirEstimator::new, scratch buffers reused across the ISTA loop) dispatches inside ista_solve: Phi x = scale * forward-FFT(x) sampled at bins (k_idx mod G) Phi^H v = scale * unnormalised inverse-FFT of v scattered into those bins Warm-start and Lipschitz estimation stay dense at construction. Measured (criterion, same run, same machine): ht20: 2.22 ms -> 265 us (8.4x) ht40: 10.26 ms -> 717 us (14.3x) The real HE40 grid (K=484, G=1452) scales further per the O(K*G)/O(G log G) ratio. 3 new tests: FFT<->dense matvec equivalence to float tolerance on ht20 and he40 grids; end-to-end dominant-tap agreement on a single-path frame; all default configs keep FFT off. New cir_estimate_fft bench group. Workspace gate: 2,921 passed / 0 failed (default path bit-exact, witnesses unchanged). https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(core): canonical frame decoder — capture-to-claim replay (ADR-136) The encode half of the ADR-136 frame contract existed (ComplexSample, to_canonical_bytes, witness_hash) but there was no decoder: a captured canonical frame could be witnessed but never reconstructed, blocking replay-from-capture. CsiFrame::from_canonical_bytes is the exact inverse: same id, metadata, complex payload, and witness hash (tested as the round-trip law AC7 — the replayed frame re-encodes byte-identically). Amplitude/phase are recomputed from the payload (projections, not independent state). Every malformed-input class fails closed (AC8): header truncation -> Truncated, payload truncation -> PayloadMismatch, unknown discriminants, non-UTF-8 device id, trailing bytes. Nil calibration uuid decodes as None per the documented encoding. Core: 36 tests pass. Workspace gate: 2,937 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(engine): dynamic min-cut mesh partition guard (ruvector-mincut) Maintains an exact min-cut over the live mesh coupling graph — nodes are sensing nodes, coupling is the product of fusion attention weights — and surfaces per cycle, as TrustedOutput::mesh: - cut value: the global "how close is the array to partitioning" number, a structural measure per-node heuristics miss; - weak side: which specific nodes would split off (failure/jamming triage, feeds ADR-032 posture); - at-risk flag: counts as a structural event for the drift->recalibration advisor (alongside ADR-142 change-points). Degenerate cases fail toward risk: a node with zero coupling is reported as already partitioned (cut 0, that node as the weak side). Measured cost policy (criterion, 12-node mesh — the honest part): - weights quantized (1/64) + change-gated: steady-state cycles do ZERO graph work and reuse the cached cut (~7.3 us, ~23x cheaper than building); - on any real change a full exact rebuild (~171 us) is used, because ONE DynamicMinCut delete+insert measured ~240 us — the subpolynomial machinery amortizes on much larger graphs, so rebuild-on-change is the measured optimum at mesh scale (one-edge case -28% after switching policy); - full process_cycle with the guard: ~33 us for 4 nodes vs the 50 ms budget. 9 mesh_guard tests (weak-node detection, steady-state zero updates, sub-quantum gating, join/drop rebuild, determinism, disconnection) + an engine-level wiring test (down-weighted node -> weak side -> recalibration). Engine 24 tests; workspace gate 2,946 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(engine): mesh partition risk demotes privacy + enters the witness (ADR-032) Completes the mesh-guard integration: its at_risk signal was advisory-only (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; mesh_guard_mut exposes risk-threshold tuning. Test: a forced-risk 3-node cycle demotes PrivateHome Anonymous->Restricted and shifts the witness vs a clean baseline. Engine 25 tests; workspace gate 2,947 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * fix: public-PR review findings — privacy-path honesty, gate holes, mesh-guard cliff - sensing-server: engine errors logged+counted (no silent swallow), trust state exposed via status surface, privacy-demotion claims aligned with the actual parallel-audit-path behavior - occupancy_bench: vacuous-F1 hole closed (degenerate test sets fail with their own criterion); CI-lower-bound test made probative - mesh_guard: quantization scaled to observed coupling range — >=65-node balanced meshes no longer permanently at_risk (regression test) - engine: both wiring tests made probative (same-topology witness compare, deterministic risk-crossing fixture) - mat: axum/tokio optional behind api; real serde feature (api enables it) - core: canonical decoder strict (non-zero reserved bytes and nil UUID rejected — injective on accepted domain, forged-bytes tests) - CHANGELOG: un-spliced the FFT/adapter bullet mangle Co-Authored-By: claude-flow <ruv@ruv.net> * chore: strip private-track references for public PR Reword the occupancy-benchmark changelog bullet to drop a cross-reference to the private research track, and restore the WorldGraph retention bullet header that was glued onto the preceding MAT bullet. Co-Authored-By: claude-flow <ruv@ruv.net> * chore: lockfile refresh for cherry-picked feature set Co-Authored-By: claude-flow <ruv@ruv.net> --------- Co-authored-by: Claude <noreply@anthropic.com>v1669 |
||
|
|
d0e27e652e |
fix(firmware): C6 IDF v5.5 guard + HE-LTF host ingest + WITNESS-LOG-110 B1 resolution (#1005) (#1011)
* fix(firmware): c6_sync_espnow IDF v5.5 send-callback guard + B1 HE-LTF resolution (#1005)
Espressif backported the esp_now_send_cb_t signature change to v5.5
(esp_now_send_info_t = wifi_tx_info_t there), so the #944 guard must be
ESP_IDF_VERSION >= VAL(5,5,0), not MAJOR >= 6.
Validated on this repo's hardware toolchain:
- WITHOUT fix, IDF v5.5.2 esp32c6 build fails with the reporter's exact
incompatible-pointer error at c6_sync_espnow.c:199 (reproduced)
- WITH fix, clean build on IDF v5.5.2 (esp32c6) AND IDF v5.4 (regression)
Docs: WITNESS-LOG-110 §B1 marked RESOLVED WITH MEASUREMENT (external,
@stuinfla, issue #1005): IDF v5.4 driver downconverts HE->HT; v5.5.2
delivers true HE-LTF (532B / 256 bins / 242 tones, PPDU 0x01 HE-SU).
ADR-110 capability table updated accordingly.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs: WITNESS-LOG-110 §B1 — in-house HE-LTF replication on the original COM12 C6
84% of 1,525 frames at 532B/PPDU 0x01 (HE-SU) with IDF v5.5.2 + the #1005
guard fix, AP ruv.net 11ax 2.4GHz. Two independent rigs now confirm:
v5.4 downconverts, v5.5.2 delivers 242-tone HE20.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(host): 256-bin HE-LTF ingest end-to-end + latent offset bugs (#1005)
Audit of every ADR-018 consumer against live C6 HE20 frames (532B/256-bin):
- sensing-server + CLI calibrate parsers read n_subcarriers from one byte
(256 decoded as 0) with stale seq/rssi offsets (rssi always 0 — latent,
pre-existing, confirmed vs firmware csi_collector.c). Fixed to the real
ADR-018 layout; n_subcarriers u8->u16; byte 18 surfaced as typed PpduType.
- sensing-server probe buffer 256B -> 2048B (532B datagram errored on Windows)
- per-node grid gate: lock densest (n_subcarriers, ppdu_type) grid, re-warm
on upgrade, skip sparser minority frames — HT-64 never mixes into an
HE-256 baseline window
- hardware parser: HE-aware bandwidth classification (256-FFT HE20 = 20MHz,
was Bw160); PpduType/Adr018Flags re-exported
- verbatim live frames (532B HE-SU, 148B HT) embedded as regression fixtures
- archive python parser: bandwidth heuristic mirror fix
Live-validated: calibrate --tier he20 consumed 600x 256-bin frames into an
ADR-135 He20 baseline (242 tones) skipping 94 HT frames; sensing-server
shows node 12 active with real RSSI (-40dBm). 765 tests green across the
three crates; workspace check clean; Python proof PASS.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(fuzz): esp_netif/ping_sock/ip_addr stubs — un-break ADR-061 fuzz build after #954
csi_collector.c gained esp_netif.h / ping/ping_sock.h / lwip/ip_addr.h
includes for the #954 gateway self-ping; the host-fuzz stub env lacked
them, breaking the fuzz build on main since
v0.8.0-esp32
v1659
|
||
|
|
2a307138f2 |
feat: per-room calibration system (ADR-151) + cognitum-v0 appliance integration spec (#989)
* docs(adr): ADR-151 — Per-Room Calibration & Specialized Model Training Room-first calibration -> bank of small specialised ruVector models (breathing, heartbeat, restlessness, posture, presence, anomaly) distilled from the frozen Hugging-Face-published RF Foundation Encoder (ADR-150). Four-stage local-first pipeline: baseline (ADR-135 environmental fingerprint) -> guided enrollment (NEW EnrollmentProtocol, clean anchors not hours) -> feature extraction (reuse signal_features + ruvsense) -> specialist bank training (rapid_adapt LoRA heads, RVF storage, HNSW prototypes). Invariants: specialisation over scale; local heads over a shared public base; honest STALE degradation on baseline drift. Indexes ADR-149/150/151. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(cli): calibration HTTP API for UI-driven baseline capture (ADR-135/151) Adds `wifi-densepose calibrate-serve` — an Axum HTTP API that wraps the ADR-135 CalibrationRecorder so a UI (or any client) can drive an empty-room baseline capture remotely. Stage 1 ("teach the room") of the ADR-151 room calibration & training pipeline. A single background task owns the UDP socket (ESP32 0xC511_0001 frames) and the optional active recorder; HTTP handlers talk to it over an mpsc command channel and read a shared status snapshot, keeping the &mut recorder lock-free. CORS permissive so a browser UI can call it. Endpoints (/api/v1/calibration/*): GET /health liveness + UDP ingest stats (frames_seen, streaming) POST /start { tier?, duration_s?, room_id?, min_frames? } GET /status live progress (state, frames, progress, z, eta) — poll for UI POST /stop finalize the current session early GET /result finalized baseline summary (amp/phase-dispersion averages) GET /baselines list persisted baseline .bin files Reuses the existing calibrate.rs ESP32 wire parser (made pub(crate)); honest abort when <10 frames arrive in the window (e.g. ESP32 not streaming). Verified end-to-end over loopback: start -> 300 replayed HT20 frames -> state=complete, 52-subcarrier baseline, phase_dispersion_avg=0.00096 (concentrated/valid), persisted to disk; all 6 endpoints exercised. CLI: 19 tests pass; crate builds clean. Co-Authored-By: claude-flow <ruv@ruv.net> * test(cli): firewall-free CSI UDP relay for local Windows ESP32 testing Windows Defender blocks inbound LAN UDP to a freshly-built binary without an admin allow-rule; python.exe is already allowed. This relay binds the public CSI port and forwards each datagram verbatim to a loopback port where `calibrate-serve --udp-bind 127.0.0.1 --udp-port 5006` listens (loopback is firewall-exempt). No admin required. Validated: ESP32-format 0xC5110001 frames -> :5005 -> relay -> :5006 -> calibrate-serve -> state=complete, 52-subcarrier baseline, phase_dispersion_avg=0.00098 (clean). Completes the no-admin live-test path. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(changelog): record ADR-151 calibration API (calibrate-serve) Co-Authored-By: claude-flow <ruv@ruv.net> * feat(calibration): ADR-151 Stages 2–5 — enrollment, extraction, specialist bank, runtime New crate wifi-densepose-calibration implementing the per-room pipeline beyond Stage-1 baseline: - anchor.rs: guided-anchor sequence + event-sourced EnrollmentSession (Stage 2) - enrollment.rs: AnchorQualityGate + AnchorRecorder — gates anchors against the ADR-135 baseline deviation (presence/motion), re-prompts bad captures - extract.rs: Features + AnchorFeature — autocorrelation periodicity (breathing/ HR bands), variance/motion (Stage 3) - specialist.rs: 6 small room-calibrated models — presence (learned threshold), posture (nearest-prototype), breathing/heartbeat (band periodicity), restlessness (calm/active normalization), anomaly (novelty vs anchors) (Stage 4) - bank.rs: SpecialistBank — train/persist + baseline-drift STALE invalidation - runtime.rs: MixtureOfSpecialists — presence short-circuit + anomaly veto + stale flagging (Stage 5) Statistical heads make the pipeline runnable/validatable today; the ADR-150 HF RF Foundation Encoder backbone is the documented upgrade path. 29 unit tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(cli): wire ADR-151 enroll / train-room / room-status / room-watch Integrates the wifi-densepose-calibration crate into the CLI as four subcommands driving the full Stage 2–5 pipeline against a live ESP32 raw-CSI stream (edge_tier=0): - enroll: walks the guided anchor sequence, gates each capture against the ADR-135 baseline deviation (re-prompts bad anchors), writes labelled features - train-room: fits the SpecialistBank from the enrollment, persists JSON - room-status: prints a trained bank's summary - room-watch: live mixture-of-specialists readout (presence/posture/breathing/ heart/restless) over a rolling window, with anomaly veto + STALE flagging Per-frame scalar is the mean CSI amplitude (carries presence/motion + breathing modulation). Validated end-to-end on the live ESP32 (COM8, edge_tier=0): the real parser → feature extraction → runtime detected breathing (~16–31 BPM) on hardware. Full multi-anchor enrollment accuracy requires the operator to perform the poses; phase-based breathing extraction is a noted refinement. 48 tests pass (29 calibration + 19 CLI). Co-Authored-By: claude-flow <ruv@ruv.net> * docs(adr-151): mark Stages 1–5 implemented; expand CHANGELOG Co-Authored-By: claude-flow <ruv@ruv.net> * fix(cli): keep proven mean-amplitude carrier for room features The max-variance-subcarrier carrier locked onto motion artifacts (not breathing) and also had an out-of-bounds bug on variable CSI subcarrier counts. Reverted to the mean-amplitude carrier, which is validated live to detect breathing. Phase-based extraction on a stable subcarrier remains the proper higher-SNR refinement (ADR-151 §4). Co-Authored-By: claude-flow <ruv@ruv.net> * feat(calibration): multistatic fusion of co-located nodes (ADR-029/151) MultiNodeMixture fuses several co-located nodes (each with its own room-calibrated SpecialistBank) into one RoomState: - presence: OR across nodes (any node seeing a person wins) - posture/breathing/heartbeat: highest-confidence node (best viewpoint) - restlessness/anomaly: max across nodes - veto: any node's physically-implausible signal vetoes the room's vitals (anti-hallucination, same as single-node runtime) + presence short-circuit - stale: any node's STALE flag propagates Same-room multistatic only; cross-room is federation (ADR-105), not fusion. 6 unit tests (presence OR, best-confidence breathing, single-node veto, staleness). 35 calibration tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(cli): multistatic room-watch — fuse co-located nodes (ADR-029/151) `room-watch --node-bank N:path` (repeatable) groups live CSI frames by node_id and fuses per-node banks via MultiNodeMixture. Validated live on COM8 (node 9, edge_tier=0): frames grouped + fused end-to-end. True 2-node fusion is covered by unit tests; a second raw-CSI node is the hardware blocker. 54 tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(integration): calibration → cognitum-v0 appliance integration overview Detailed cross-repo integration spec for cognitum-one/v0-appliance: data contracts (CSI wire format, ADR-135 baseline binary, enrollment/bank/RoomState JSON schemas), calibrate-serve HTTP API, public crate API, Pi5+Hailo tiering, and a 5-step appliance integration plan. Grounded in the verified cognitum-v0 inventory (aarch64, cargo 1.96, HAILO10H, ruview-vitals-worker:50054). Co-Authored-By: claude-flow <ruv@ruv.net> * fix(calibration): address PR review — aarch64 decouple, API auth, path traversal, throttle Resolves the review on #989: - **Cross-compile (the appliance blocker):** make wifi-densepose-mat optional and feature-gate it (`mat`), so `cargo build -p wifi-densepose-cli --no-default-features` excludes the mat→nn→ort(ONNX)→openssl-sys chain. Verified: `cargo tree --no-default-features` shows 0 ort/openssl deps → calibration cross-compiles clean for the Pi. - **Security (must-fix before LAN):** - `--token` / CALIBRATE_TOKEN bearer-auth middleware on every route; warns if bound non-loopback without a token. - sanitize client-supplied `room_id` to [A-Za-z0-9_-] (≤64) before it reaches the baseline write path — kills the `../` file-write primitive. + test. - **Perf:** stop locking shared status + cloning SessionStatus on every UDP frame — counters/snapshot flush on the 200 ms tick instead (no CPU starvation under flood). finalize write moved to async `tokio::fs::write`. - **Docs:** ADR-151 STALE wording matches the impl (baseline-id change; drift-threshold = P6 refinement); integration doc gets the `--no-default-features` build + auth/sanitize notes. 35 calibration + 15 CLI tests (no-default) / 20 CLI (default) pass. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(worldgraph,worldmodel): add crates.io READMEs Plain-language overviews + feature lists, comparison tables (symbolic graph vs predictive occupancy; graph vs grid vs event-log), usage, and technical details. Adds readme = "README.md" to both manifests so they render on crates.io on the next release. Co-Authored-By: claude-flow <ruv@ruv.net> * release: worldgraph & worldmodel 0.3.1 (READMEs on crates.io) Co-Authored-By: claude-flow <ruv@ruv.net> * docs: precise calibration validation scope (capture+API+auth proven; clean enroll→train→infer not yet on-target) Aligns ADR-151 §7 + the appliance integration doc with the PR #989 scope clarification: nothing has run a clean baseline → enroll → train → infer on live CSI; the live breathing read used the stateless head, not a trained bank. Adds --source-format adr018v6 to the backlog. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(calibrate-serve): live GET /room/state endpoint (mixture over CSI window) Adds a live RoomState readout over HTTP — the appliance UI's main need. The ingest task maintains a rolling per-frame scalar window (flushed on the 200 ms tick, no per-frame lock); the handler loads a bank (resolved as a sanitized name under output_dir — same path-traversal defense as room_id), runs the MixtureOfSpecialists over the window, returns RoomState JSON. Validated live (ESP32-S3 via relay): breathing 14-19 BPM over HTTP; a bank=../../etc/passwd query is neutralized to 'etcpasswd' (no traversal). Co-Authored-By: claude-flow <ruv@ruv.net> * feat(calibrate-serve): POST /room/train + fix AnchorLabel JSON to snake_case - POST /api/v1/room/train: { room_id, baseline_id, anchors[] } → trains a SpecialistBank and persists it as <output_dir>/<room_id>.json (path-sanitized), readable via /room/state?bank=<room_id>. Completes the HTTP train→infer loop. - Fix data-contract bug: AnchorLabel serialized as PascalCase variant names (serde default) while as_str() + the integration doc used snake_case. Added #[serde(rename_all = "snake_case")] so the JSON wire format matches the documented contract (empty/stand_still/…). Locked with a roundtrip test. Validated live (ESP32-S3): POST train (4 anchors → 6 specialists, persisted) → GET /room/state returns RoomState with the trained presence/restlessness; the synthetic-vs-real scale mismatch correctly triggers the anomaly veto. 36 calibration tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(calibrate-serve): live enroll-over-HTTP (POST /enroll/anchor + /enroll/status) Closes the last HTTP gap — the appliance can now drive the ENTIRE calibration pipeline over HTTP without the CLI: baseline (start/stop) -> enroll/anchor x8 -> room/train -> room/state - POST /enroll/anchor { room_id, baseline, label, duration_s? }: the ingest task loads the baseline (sanitized name under output_dir), captures the anchor for the duration against it (AnchorRecorder + per-frame series), runs the quality gate, and on completion replies with the verdict + accumulates the AnchorFeature in an in-server enrollment map keyed by room_id. Re-prompts on rejection. - GET /enroll/status?room=<id>: accepted anchors, next, complete. - POST /room/train now falls back to the in-server enrollment when anchors[] is omitted. Validated live (ESP32-S3): capture baseline -> enroll stand_still (271 frames, 6s) -> gate correctly rejects "no person detected (presence_z 0.90 < 1.50)" relative to a same-occupancy baseline (a clean empty-room baseline is the documented on-target prerequisite). Builds clean; CLI tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> * test(calibrate-serve): HTTP integration tests for the room/enroll endpoints Factor the router into build_router() (shared by execute + tests) and add tower-oneshot integration tests (no network/ingest needed): - health + descriptor → 200 - POST /room/train persists the bank; GET /room/state → 200; train with no anchors/enrollment → 400 - path-traversal: /room/state?bank=../../etc/passwd → 404 (sanitized, never reads outside output_dir) - enroll/status empty; /enroll/anchor with an unknown label → 400 CI regression coverage for the endpoints added this session. 18 CLI tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> * fix(mat): make serde non-optional — unblocks `cargo test --workspace --no-default-features` Making wifi-densepose-mat optional in the CLI (for the aarch64/ort decouple) exposed a latent feature bug: mat's `api` module compiles unconditionally and uses serde, but `serde` was an optional dep enabled only via the `api`/`serde` features. Previously the CLI's *unconditional* mat dependency enabled those features transitively, so `--workspace --no-default-features` still got serde; once mat became optional+gated, the workspace build lost it → `error[E0432]: unresolved import serde` across mat's api/* (CI red). mat already pulls serde_json + axum unconditionally, so making `serde` non-optional has no real cost and restores the workspace build. Does NOT affect the aarch64 CLI build (mat isn't built there at all): verified `cargo tree -p wifi-densepose-cli --no-default-features` still shows 0 ort/openssl deps, and `cargo test --workspace --no-default-features` compiles clean. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(claude.md): add wifi-densepose-calibration to crate table (pre-merge) Co-Authored-By: claude-flow <ruv@ruv.net> * docs(adr): ADR-152 — WiFi-pose SOTA 2026 intake (geometry-conditioned calibration, external benchmarks, encoder recipe) Records the 2026-06-10 deep-research run (22 sources, 110 claims, 25 adversarially verified: 24 confirmed / 1 refuted) and the decisions it implies: - §2.1 ACCEPTED: geometry-condition the ADR-151 calibration system — NodeGeometry at enrollment, geometry embeddings for future LoRA heads, PerceptAlign-style two-checkerboard camera↔WiFi alignment for the ADR-079 supervised path. PerceptAlign (MobiCom'26) names the failure mode ("coordinate overfitting") that matches our own ADR-150 cross- subject collapse. - §2.2 ACCEPTED: benchmark protocol vs external "WiFlow-STD (DY2434)" (claimed 97.25% PCK@20, Apache-2.0 weights+dataset) with a no-citation rule until measured on our 17-keypoint ESP32 eval set. Name collision with our internal WiFlow is disambiguated. - §2.3 ACCEPTED: amend ADR-150 training recipe per UNSW MAE study — 80% masking, (30,3) patches, data-over-capacity priority (log-linear, unsaturated at 1.3M samples). - §2.4 watch items: IEEE 802.11bf-2025 published 2025-09-26; esp_wifi_sensing as external presence baseline (drop-in claim REFUTED 0-3); ZTECSITool 160MHz/512-subcarrier anchor node (procurement-gated). - §2.5 NOT adopted: non-WiFi "foundation model" papers; DensePose-UV (no 2025-2026 work does UV regression from commodity WiFi). Every number is evidence-graded CLAIMED vs MEASURED in the source register. Re-check horizon 2026-12. Co-Authored-By: RuFlo <ruv@ruv.net> * test(calibration): full-loop integration test — baseline→enroll→train→infer proven in-process (ADR-151 §7 gap, software half) Closes the software half of PR #989's headline validation gap: the complete calibration loop had never run end-to-end anywhere, even in-process. tests/full_loop.rs (412 lines, deterministic xorshift32 room simulator, HT20/52-subcarrier/20Hz, same fingerprint family as the ADR-135 roundtrip test) now drives the CLI's exact stage order through the public API: 1. baseline — 600 static frames, zero motion flags post-warmup, calibration_uuid() exactly as the CLI derives it 2. enroll — all 8 AnchorLabel::SEQUENCE anchors through AnchorQualityGate::default(), session is_complete() 3. extract — AnchorFeature::from_series recovers injected 0.25Hz and 0.125Hz breathing within ±0.04Hz 4. train — SpecialistBank::train fits all 6 specialists; JSON round-trip and the runtime consumes the RELOADED bank 5. infer — positive: never-enrolled 0.30Hz subject reads present, 18±2 BPM; negative: empty window reads absent; degradation: foreign baseline_id flags STALE Seed-robust (5 seeds), passes with and without default features: 36 unit + 1 integration green. Validation docs updated (ADR-151 §7 + integration doc §7 matrix): what remains is strictly the on-target hardware session (real CSI, physically empty room, operator performing the guided anchors). Three behavioral findings from building the test are recorded for pre-session triage: z-band squeeze between baseline motion flagging (z>2.0) and the still- anchor gate (presence_z≥1.5) — likeliest on-hardware enroll failure; variance-only PresenceSpecialist missing motionless-person mean shift; ungated breathing_hz/heart_hz in noise-window embeddings. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(calibration): close all four ADR-152 behavioral findings pre-hardware-session The full-loop integration test surfaced three findings; fixing the third exposed a fourth. All four are fixed and regression-guarded: 1. z-band squeeze (enrollment.rs) — anchor motion is now measured from frame-to-frame deltas of the deviation series (|Δz| > Z_DELTA_MOTION 0.5 ∨ |Δφ| > π/6), not from the absolute motion_flagged, which fires at amplitude_z_median > 2.0 vs the EMPTY baseline and so conflated presence strength with motion. A strongly-reflecting still person (z = 3.0 — every frame flagged by the old heuristic) now enrolls. The old unit tests mocked (z=3.0, motion=false), a combination the real deviation() can never emit — which is exactly how the squeeze hid; tests now derive the flag from z the way the producer does. 2. variance-only presence (specialist.rs) — PresenceSpecialist gains a mean-shift channel: present when variance > threshold OR |mean − empty_mean| > mean_dist_threshold (trained at half the empty→occupied mean distance, None when the means don't separate). Detects the motionless person whose body raises the scalar mean but not its variance. Old persisted banks deserialize with the channel inert (serde default None) — variance-only behavior preserved, proven by a fixture test against pre-change JSON. 3. ungated hz embedding (extract.rs) — Features::embedding() zeroes breathing_hz/heart_hz below EMBED_MIN_SCORE (0.25), keeping the random in-band peaks of noise windows out of the posture/anomaly prototype space. Raw fields stay ungated (specialists have their own stricter gates). 4. heart-band lag-floor leakage (extract.rs, found while fixing 3) — a pure 0.30 Hz breathing signal scored 0.67 in the heart band at 3.33 Hz: out-of-band rhythm leaks as a monotonic slope whose max sits at the band's lag floor, so score gating alone cannot stop it. autocorr_dominant now requires the winning lag to be an interior local maximum; band-edge "peaks" are rejected, true in-band peaks (interior by definition) are preserved. full_loop.rs strengthened to drive the fixes end-to-end: the StandStill anchor is now a z=3.0 strong reflector (unenrollable pre-fix), and a new motionless-person runtime case proves mean-channel detection at empty- level variance. Validation: 41 calibration unit + 1 full-loop integration + 23 CLI tests green; cargo test --workspace --no-default-features exit 0. Co-Authored-By: RuFlo <ruv@ruv.net>v1647 |
||
|
|
992c2b25cb |
fix(firmware): correct ESP32 edge heart rate — sample-rate + harmonic lock (#987) (#988)
* fix(firmware): correct heart-rate estimation — sample-rate + harmonic lock The edge vitals HR was stuck at ~45 BPM regardless of true heart rate (Apple Watch ground truth 87 BPM read as ~45) and "dropped a lot" between frames. Two root causes: 1. Stale fixed sample rate. estimate_bpm_zero_crossing() used a hardcoded `sample_rate = 10.0f` (and the biquads a separate `fs = 20.0f`). That constant was correct when CSI came from ~10 Hz beacons, but #985's self-ping raised the callback rate to a VARIABLE ~13-19 Hz. BPM scales as (assumed_rate / actual_rate) x true, so a true 87 read ~45, and because the real rate fluctuates with CSI yield while the code assumed a fixed value, the reported HR swung frame-to-frame (the "drops"). 2. Breathing-harmonic lock. Zero-crossing HR estimation locked onto a breathing harmonic — a 0.25 Hz breathing fundamental puts its 3rd harmonic at ~0.74 Hz ~= 44 BPM, right in the HR band — so it parked at ~45 BPM independent of the real heartbeat. Fix: - Measure the real sample rate from inter-frame timestamps (EMA-smoothed, clamped 8-30 Hz); use it for both BPM conversion and biquad design, and re-tune the filters when the rate drifts >15% so the passbands stay in real Hz. - Replace the HR zero-crossing with estimate_hr_autocorr(): autocorrelation peak in the 45-180 BPM band that explicitly rejects lags within 8% of any breathing harmonic (k=1..6), with parabolic interpolation and a peak- confidence gate (returns 0 rather than a noise value). - Median-smooth (N=9) the emitted HR over valid estimates to kill residual single-frame outliers. Validated on hardware (ESP32-S3, COM8/192.168.1.80) vs an unmodified board (192.168.1.67) and an Apple Watch (87 BPM): - old firmware: HR pegged 40-52 BPM (median ~45) - fixed firmware: HR reaches the true 88-91 BPM range (peak 88.5, vs 87 GT) Known limitation: under subject motion (motion=Y) HR is still noisy because the breathing estimate degrades and misguides harmonic rejection; motion gating + breathing robustness are follow-ups. Co-Authored-By: claude-flow <ruv@ruv.net> * fix(firmware): robust HR harmonic rejection via autocorr breathing period (#987) Follow-up to 332c2a98d. The HR harmonic rejection was fed the noisy zero-crossing breathing estimate, which under motion notched the wrong frequencies and let the autocorr lock onto the ~0.75 Hz breathing harmonic (~45 BPM). Generalize estimate_hr_autocorr -> estimate_periodicity_autocorr and drive HR harmonic rejection from a robust autocorrelation breathing period instead; widen the HR median smoother to N=13. Hardware A/B (fixed .80 vs unmodified control .67, both edge_tier=2, subject in motion 100% of frames): - control (old fw): HR pegged 40-43 BPM (median 40.6) - fixed: HR 60-91 BPM (median 71.9) — sub-60 harmonic locks eliminated, spread 42->31 BPM vs previous build Reported breathing is unchanged (still zero-crossing); the autocorr breathing period is used only internally for HR harmonic rejection. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(changelog): record ESP32 heart-rate fix (#987) Co-Authored-By: claude-flow <ruv@ruv.net>v0.7.1-esp32 v1613 |
||
|
|
5789351b78 |
fix(esp32): add connected-STA self-ping CSI traffic source (#954) (#985)
The ESP32 CSI engine only produces CSI for received OFDM frames (L-LTF/ HT-LTF). On a quiet network — or on a display-enabled build where the #893 MGMT->MGMT+DATA promiscuous upgrade is skipped (has_display=true) — the only CSI-eligible frames are sparse beacons (often non-OFDM DSSS), so wifi_csi_callback can starve to yield=0pps -> DEGRADED -> motion=0 (#521, #954). Fix (additive): pin a ~50 Hz OFDM unicast floor by pinging the STA's own DHCP gateway. The router's ICMP echo replies are OFDM frames destined to this station and drive the CSI engine regardless of promiscuous filter state or ambient traffic. Mirrors Espressif's esp-csi csi_recv_router reference. Promiscuous capture (#396/#893) is left fully intact so multistatic/multi-node sensing still hears other stations' frames. Reconciles PR #955 (which removed promiscuous entirely and conflicted with the already-shipped #893 DATA-capture path) into an additive change on current main. Verified on ESP32-S3 (N16R8, COM8), ESP-IDF v5.4: Promiscuous mode enabled (MGMT-only, RuView#396) self-ping started -> 192.168.1.1 @50Hz (CSI OFDM source, fix #521/#954) CSI cb #1: len=128 rssi=-40 ch=5 adaptive_ctrl: state=6 yield=13-19pps motion=1.00 presence>0 (SENSE_ACTIVE) DEGRADED cleared; CSI yield stable ~15 pps over 60 s. Co-authored-by: Meraj <merajmehrabi@gmail.com>v1611 |
||
|
|
b6420ac9ba |
fix(server): make synthetic CSI opt-in only (sibling fix to #937) (#979)
Background Issue #937 in the cognitum-v0 appliance repo flagged that the `cognitum-csi-capture` systemd unit shipped `--simulate` by default, silently serving synthetic CSI tagged as production telemetry on `/api/v1/sensor/stream`. That's a textbook trust-eroding pattern — the single most-cited "where's the real data?" evidence external reviewers (#943, #934) point at when they call the project AI-slop. A grep across THIS tree surfaced the exact same anti-pattern in three places: docker/docker-compose.yml:27 # auto (default) — probe ESP32, fall back to simulation docker/docker-entrypoint.sh:14 # CSI_SOURCE — data source: auto (default), ... main.rs:6435 info!("No hardware detected, using simulation"); "simulate" The sensing-server's `auto` source resolver at main.rs:6425-6440 silently fell back to synthetic with only an `info!` log line as the signal. Downstream consumers calling `/api/v1/sensing/latest` or `/ws/sensing` had no in-band way to know they were being served fake data. Fix `auto` now refuses to fall back. When neither ESP32 UDP nor host WiFi is detected, the server logs a clear `error!` explaining the situation and exits 78 (EX_CONFIG). The error message names the two ways to proceed: provision real hardware, or set `--source simulated` / `CSI_SOURCE=simulated` explicitly. Existing operators who already use `--source simulated` (or its legacy `simulate` alias) are unaffected — the alias is preserved for back-compat. Docker entrypoint comment, docker-compose comment, and the Tauri desktop app's source-default path also updated to reflect the new posture. The desktop app keeps its `simulated` default because it's an explicit demo product — the value passed downstream is the *explicit* `simulated`, not `auto`, so the server tags it correctly and never lies about its data source. Validation cargo build -p wifi-densepose-sensing-server --no-default-features cargo test -p wifi-densepose-sensing-server --no-default-features → 122 / 122 pass, build clean (existing pre-fix warnings unchanged). Deployment ⚠ Breaking change for unattended deployments that relied on the `auto → simulated` silent fallback. That is exactly the failure mode this PR fixes: pretending to serve real sensing data when the source is fake. Operators who genuinely want demo mode set `CSI_SOURCE=simulated` explicitly; the error message and the docker-compose comment both point them there.v1609 |
||
|
|
c353255672 |
fix: firmware cluster — wasm3 IDF v6.0 build (#946) + swarm TLS stack (#949) + Docker unauth default (#864) (#975)
* fix(firmware,docker): clear three high-severity bugs in one sweep Closes #946 — wasm3 fails on Xtensa GCC 15.2.0 (ESP-IDF v6.0.1) cannot tail-call: machine description does not have a sibcall_epilogue instruction pattern wasm3's `M3_MUSTTAIL return jumpOpImpl(...)` uses `__attribute__((musttail))` which GCC 15 enforces strictly on Xtensa, where the backend never reliably implemented sibling-call epilogues. Define `M3_NO_MUSTTAIL=1` in the wasm3 component compile-defs so the macro expands to plain `return` — slightly slower per opcode dispatch but functionally identical, and the only change needed in this tree. Older IDF / GCC builds accept the define as a no-op so the IDF v5.4 CI build is unchanged. Closes #949 — swarm task stack overflow on Seed TLS init The reporter provisioned with `--seed-url https://...` which exercises TLS, and the task panicked with the FreeRTOS stack-fill sentinel `0xa5a5a5a5` immediately after the bridge init line. `SWARM_TASK_STACK` was 3 KB ("HTTP client uses ~2.5 KB" per the original comment) — fine for plain HTTP, far too small for mbedTLS handshake which alone wants 4-6 KB for the cipher suite + cert chain + ECDH state, plus another 1.5-2 KB for esp_http_client. Bumped to 8192 with the why in the comment. Plain-HTTP deployments waste ~5 KB headroom (negligible PSRAM cost) but the bug class is closed. Closes #864 — Docker default exposes unauthenticated sensing API + WS `docker-entrypoint.sh` started the sensing-server with `--bind-addr 0.0.0.0` AND empty `RUVIEW_API_TOKEN` AND docker-compose published 3000/3001/5005 — anyone on a reachable network segment could read /api/v1/sensing/latest and the /ws/sensing live frame stream. Now the entrypoint refuses to start when: RUVIEW_API_TOKEN is empty AND RUVIEW_ALLOW_UNAUTHENTICATED is not "1" AND RUVIEW_BIND_ADDR is not loopback / localhost / ::1 …and prints exactly which three escape hatches the operator can take (set the token, opt in explicitly, or pin to loopback). Also wires RUVIEW_BIND_ADDR through to --bind-addr so the loopback escape hatch is one env var, not a flag override. cog-ha-matter / homecore routes are excluded from this check since they own their own auth lifecycle. This is a breaking change for unattended LAN deployments — exactly what the reporter asked for. Validation * `idf.py build` for esp32s3 target — succeeds (#946 fix doesn't affect default IDF v5.4 build path). * `idf.py set-target esp32c6 && idf.py build` — succeeds, binary 1015 KB / 45% partition free. * Hardware flash to COM12 (C6) failed with "No serial data received" — XIAO C6 needs manual BOOT-hold+RESET; couldn't drive that without operator. Code is correct per build + review; runtime validation needs the operator to press the BOOT button at flash time. * docker-entrypoint.sh changes are shell-only — exercised by reading the path under the four escape-hatch conditions. Out of scope — cross-repo issues Issues #935 (cognitum-agent mesh panics), #936 (CSI relay routing), and #937 (cognitum-csi-capture --simulate default) reference `cognitum-agent` / `csi-capture` / `csi-relay-routes.json` artifacts that live in the cognitum-v0 appliance repo, not this tree. Issue #954 (CSI callback never fires on S3 v0.6.5/v0.7.0) is not addressed here — the reporter is on the S3 (COM9 in this lab) but the hardware path needs an interactive debug session with a configurable AP traffic source to pin the root cause (MGMT-only filter, traffic filter MAC, or driver-level callback wiring). Will tackle in a follow-up. Co-Authored-By: claude-flow <ruv@ruv.net> * fix(firmware): bump LWIP UDP / WiFi TX buffer pools to ease ENOMEM Hardware validation on COM8 (S3) and COM9 (C6) surfaced a v0.7.0 regression not captured in the existing issue tracker: stock IDF v5.4 defaults (UDP recv mbox = 6, TCPIP recv mbox = 32, WiFi dynamic TX buffers = 32) are too small for the v0.7.0 packet mix once CSI promiscuous mode is active. The boot trace showed `stream_sender: sendto ENOMEM — backing off for 100 ms` repeating every capture cycle, with the csi_collector path reporting `fail #1..5` within seconds of associating to an AP. Modest bumps applied (~3 KB extra heap each): CONFIG_LWIP_UDP_RECVMBOX_SIZE 6 → 32 CONFIG_LWIP_TCPIP_RECVMBOX_SIZE 32 → 64 CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM 32 → 64 Empirical 25 s measurement on S3 / COM8 post-fix: csi_collector fail # : 1-5 → 0 (full path drained) stream_sender ENOMEM hits / sec : 8-15 → 8 (capped by 100 ms backoff) CSI cb rate : ~28 cb/s, yield max 18 pps feature_state emit failed : still present A second, more aggressive iteration (DYNAMIC_TX=128, PBUF_POOL=32, TCP SND/WND=16384) was tested and reverted — the ENOMEM count was identical to the modest bump. The residual 8/s is structural: it's the 100 ms backoff window ceiling × the adaptive_controller emit cadence which currently fires roughly every 50 ms instead of the intended 1 Hz. Bigger buffers don't fix that — only rate-limiting the emitter does. Code-level rate-limit refactor is tracked separately to keep this PR scoped to the bundle that landed mechanically. Co-Authored-By: claude-flow <ruv@ruv.net> * fix(firmware): rate-limit feature_state emit from 5 Hz → 1 Hz Completes the ENOMEM cure that the LWIP/WiFi buffer bumps started. Root cause (verified on COM8 / S3 + COM9 / C6) `fast_loop_cb` runs every 200 ms (5 Hz) and unconditionally called `emit_feature_state()`. Combined with CSI capture in promiscuous mode (radio mostly in RX), the WiFi TX airtime got saturated and every 100 ms backoff window had at least one ENOMEM. Bumping the LWIP/WiFi buffer pools to 4× had no effect on the ENOMEM rate because the bottleneck was radio TX time, not pool size. The ADR-081 spec calls out "1–10 Hz" for feature_state; 5 Hz was at the top of the range and not necessary — operators consuming the telemetry want a sample every second, not five times. Dropping to 1 Hz frees ~80 % of the feature_state TX traffic. Measurement on COM8 (25 s windows, otherwise-idle environment) csi_collector lost sends : 1-5 / 25 s → 0 / 25 s (✓ fixed) feature_state emit failed : 75 / 25 s → 25 / 25 s (3× ↓) total sendto ENOMEM log lines: 200/25 s → 212 / 25 s (unchanged — bound by 100 ms backoff window ceiling, not by emit rate) CSI yield : 18 pps (steady) The unchanged total ENOMEM is a measurement artifact: the backoff window emits exactly one ENOMEM record per 100 ms when *anything* collides with a TX-busy moment. The packet-loss numbers (which is what actually matters) all dropped to zero or near-zero on the CSI path. Implementation Pure-static `s_emit_divider` counter in `fast_loop_cb`. Every 5th tick calls the emit. Zero allocation, zero extra state, zero interaction with the existing observation snapshot under `s_obs_lock`. Could be made config-driven if any operator ever wants 2-5 Hz back — out of scope here. Co-Authored-By: claude-flow <ruv@ruv.net>v1606 |
||
|
|
872d7593bb |
fix: IDF v6.0 ESP-NOW callback compat (#944) + occupancy noise-floor anchor (#942) (#945)
* fix(firmware): on_send ESP-NOW callback compat for IDF v6.0 (closes #944) ESP-IDF v6.0 changed `esp_now_send_cb_t` from void (*)(const uint8_t *mac, esp_now_send_status_t status) to void (*)(const esp_now_send_info_t *tx_info, esp_now_send_status_t status) The C6 sync ESP-NOW path's `on_recv` was already version-guarded with `#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)` (lines 102-112) but the `on_send` sibling missed the equivalent guard. CI runs against IDF v5.4 so the regression slipped through; the reporter on IDF v6.0.1 with xtensa-esp-elf esp-15.2.0_20251204 hit: c6_sync_espnow.c:182:30: error: passing argument 1 of 'esp_now_register_send_cb' from incompatible pointer type [-Wincompatible-pointer-types] Fix: mirror the recv guard with `#if ESP_IDF_VERSION_MAJOR >= 6` since the send-callback signature change happened at IDF v6.0 (not v5.x like the recv-callback). Both branches ignore the address-side argument since `on_send` only inspects `status` to bump the TX-fail counter. Adds `#include "esp_idf_version.h"` so the macro is in scope. Closes #944 Co-Authored-By: claude-flow <ruv@ruv.net> * fix(signal): anchor estimate_occupancy noise floor to calibration (closes #942) `test_estimate_occupancy_noise_only` asserts that 20 noise-only frames fed through a 50-frame calibrated `FieldModel` yield 0 occupancy. Failure reported on the upstream Linux + BLAS build. Root cause Calibration and estimation each compute their own Marcenko-Pastur threshold: threshold = noise_var · (1 + sqrt(p / N))² with `noise_var` = median of the bottom half of positive eigenvalues from their own covariance. The MP ratio differs across the two phases: calibration (50 frames, p=8): ratio = 0.16, factor ≈ 1.96 estimation (20 frames, p=8): ratio = 0.40, factor ≈ 2.66 On a small estimation window the local `noise_var` estimate can also be smaller than the calibration's (fewer samples → bottom-half median hits lower-magnitude eigenvalues). The combination of a smaller noise_var on estimation and the larger MP factor can flip eigenvalues on/off the "significant" line in a sample-size-dependent way, so an identical-distribution test window scores `significant > baseline_eigenvalue_count` and reports phantom persons. Fix Persist the calibration `noise_var` on `FieldNormalMode` (new field `baseline_noise_var: f64`) and use `max(local_noise_var, baseline_noise_var)` as the noise floor inside `estimate_occupancy`. This anchors the threshold to the calibration scale and prevents the short-window collapse without changing behavior when the local window's own noise dominates (the real-motion case). `baseline_noise_var` defaults to 0.0 in the diagonal-fallback paths; the estimation code treats 0.0 as "no anchored floor available" and preserves the pre-#942 single-window behavior — so older `FieldNormalMode` instances deserialised from disk continue to work unchanged. Test results cargo test --workspace --no-default-features → 413 lib tests pass (signal crate), 0 fail, 1 ignored. The actual `eigenvalue`-gated test still requires BLAS (not buildable on Windows). Logic-trace via the four numerical anchors above shows the fix flips `noise_var` from the smaller local value back up to the calibration scale, dropping `significant` to or below `baseline_eigenvalue_count` so the saturating subtraction returns 0. Closes #942 Co-Authored-By: claude-flow <ruv@ruv.net>v1596 |
||
|
|
2c136aca74 |
fix(protocol): resolve 0xC511_0004 magic collision (closes #928) (#931)
* fix(ci): SAST actually scans the code + drop deprecated flaky semgrep action Two real problems in the Static Application Security Testing job: 1. **It scanned a path that no longer exists.** `bandit -r src/` and `semgrep … src/` pointed at the repo-root `src/`, but the Python code moved to `archive/v1/src/` (64 .py files) when the runtime was rewritten in Rust. So the SAST scan matched nothing — a silent no-op (this is also why `bandit-results.sarif` was "Path does not exist" on recent runs). Fixed both to `archive/v1/src/`. 2. **Deprecated + redundant + flaky semgrep step.** The `returntocorp/semgrep-action@v1` step pulled `returntocorp/semgrep-agent:v1` from Docker Hub every run (intermittently timing out → red check, e.g. on #929) and is EOL. It was redundant: the pip `semgrep --sarif` step is what feeds GitHub Security; the action only pushed to the Semgrep cloud app via SEMGREP_APP_TOKEN. Removed it and folded its `p/docker` + `p/kubernetes` rulesets into the pip semgrep command, so coverage is preserved with no Docker pull. The job stays `continue-on-error: true` (non-gating). YAML validated. Co-Authored-By: claude-flow <ruv@ruv.net> * fix(protocol): resolve 0xC511_0004 magic collision (closes #928) Background `0xC511_0004` was assigned to two different packet formats in firmware — `EDGE_FUSED_MAGIC` (ADR-063, 48-byte `edge_fused_vitals_pkt_t`) and `WASM_OUTPUT_MAGIC` (ADR-040, variable-length `wasm_output_pkt_t`). Both were transmitted. The sensing-server only had a WASM parser for that magic and no fused-vitals parser, so on the ESP32-C6 + MR60BHA2 mmWave configuration the fused-vitals packet was silently misparsed as a malformed WASM output — `breathing_rate` was read as `event_count`, mmWave-fused vitals were lost, and spurious WASM events were emitted to subscribers. Fix 1. Reassign `WASM_OUTPUT_MAGIC` to `0xC511_0007` (next free slot per the registry in `rv_feature_state.h`). Smaller blast radius than moving fused-vitals — the registry already treats `0xC511_0004` as fused-vitals canonical and several years of deployed feature tracking depends on that assignment. 2. Add `parse_edge_fused_vitals` + `EdgeFusedVitalsPacket` in `wifi-densepose-sensing-server::main`. Byte layout taken directly from `edge_processing.h:129`, mirroring the firmware's `_Static_assert(sizeof(edge_fused_vitals_pkt_t) == 48)` so future firmware changes that grow the packet will break this parser loudly instead of silently. 3. Add a dispatch arm in the UDP receive loop. Fused-vitals is tried BEFORE WASM so a stale firmware (still emitting 0xC511_0004 with the WASM payload) fails to parse as fused-vitals (size mismatch), then fails to parse as WASM (magic mismatch on the new 0x...0007), and gets dropped — a deliberate "fail loud" outcome rather than the pre-fix silent garbage. 4. Update the registry comment in `rv_feature_state.h` to add the new 0x...0007 row. 5. Add five tests in a new `issue_928_magic_collision_tests` mod: - `parse_edge_fused_vitals_extracts_fields_correctly` - `parse_edge_fused_vitals_rejects_short_buffer` - `parse_edge_fused_vitals_rejects_wrong_magic` - `parse_wasm_output_rejects_legacy_0004_magic` - `parse_wasm_output_accepts_new_0007_magic` WebSocket payload Fused-vitals now broadcasts as `{"type": "edge_fused_vitals", ...}` with the mmWave-specific block nested under `mmwave`. Schema is additive — existing subscribers that only inspect `type` are unaffected; subscribers that switch on `type` gain a new branch. Deployment note This is a wire-protocol change. Firmware older than this commit that emits WASM output on 0xC511_0004 will lose its WASM event stream against an updated host (host expects 0xC511_0007). Per the issue discussion, "fail loud" is preferred to silent misparsing. Operators running C6+mmWave should reflash firmware concurrent with the host upgrade. Test results cargo test -p wifi-densepose-sensing-server --no-default-features --bin sensing-server → 122 passed / 0 failed (5 new + 117 existing, unchanged) Co-Authored-By: claude-flow <ruv@ruv.net>v1591 v1590 |
||
|
|
69e61e3437 |
docs(changelog): record this cycle's behavior-changing fixes (#932)
Per the CLAUDE.md pre-merge checklist (item 5, "Add entry under [Unreleased]"), several recently-merged PRs landed without CHANGELOG entries. Backfilling the user/operator-facing ones — most importantly the MAT triage safety fix: - #926 (Security/safety): survivor with a heartbeat never triaged Deceased - #918: per-node HA devices report each node's own presence/motion - #919: actionable --model load diagnostic (refs #894) - #920: --export-rvf no longer silently produces a placeholder model - #929 (Security): bearer scheme matched case-insensitively (RFC 6750) CI-internal fixes (#925 rust-cache, #930 SAST) are intentionally omitted — they don't change product behavior. Docs-only. |
||
|
|
d9e87e13b4 |
fix(ci): SAST actually scans the code + drop deprecated flaky semgrep action (#930)
Two real problems in the Static Application Security Testing job: 1. **It scanned a path that no longer exists.** `bandit -r src/` and `semgrep … src/` pointed at the repo-root `src/`, but the Python code moved to `archive/v1/src/` (64 .py files) when the runtime was rewritten in Rust. So the SAST scan matched nothing — a silent no-op (this is also why `bandit-results.sarif` was "Path does not exist" on recent runs). Fixed both to `archive/v1/src/`. 2. **Deprecated + redundant + flaky semgrep step.** The `returntocorp/semgrep-action@v1` step pulled `returntocorp/semgrep-agent:v1` from Docker Hub every run (intermittently timing out → red check, e.g. on #929) and is EOL. It was redundant: the pip `semgrep --sarif` step is what feeds GitHub Security; the action only pushed to the Semgrep cloud app via SEMGREP_APP_TOKEN. Removed it and folded its `p/docker` + `p/kubernetes` rulesets into the pip semgrep command, so coverage is preserved with no Docker pull. The job stays `continue-on-error: true` (non-gating). YAML validated.v1588 |
||
|
|
be48143f77 |
fix(auth): match the Bearer scheme case-insensitively (RFC 6750) (#929)
`require_bearer` parsed the Authorization header with
`strip_prefix("Bearer ")`, which is case-sensitive. Per RFC 6750 §2.1 /
RFC 7235 §2.1 the auth-scheme is case-insensitive, so a correct token sent
as `Authorization: bearer <token>` (or `BEARER`, or with extra whitespace)
was rejected with a confusing "invalid bearer token" 401 — needless friction
when setting up `RUVIEW_API_TOKEN` (the active #864/#924 theme).
Now the scheme is matched with `eq_ignore_ascii_case` and leading token
whitespace trimmed. The token comparison itself is unchanged — still exact
and constant-time (`ct_eq`) — so this does not weaken auth: a wrong token or
a non-Bearer scheme (`Basic …`) still returns 401.
New test `accepts_case_insensitive_bearer_scheme` covers `bearer`/`BEARER`/
extra-space (accept) and wrong-token/`Basic` (still reject). bearer_auth
suite: 9 passed.
v1585
|
||
|
|
c453268002 |
fix(mat): never triage a survivor with a heartbeat as Deceased (safety) (#926)
Both triage paths in the Mass Casualty Assessment tool classified a survivor as Deceased (Black) on "no breathing + no movement" while completely ignoring the heartbeat signal: - domain `TriageCalculator::calculate` → `combine_assessments(Absent, None)` returned Deceased. That branch is in fact only reachable *because* a heartbeat makes `has_vitals()` true (breathing+movement absent alone → Unknown) — so every "Deceased" was a live person with a pulse. - detection `EnsembleClassifier::determine_triage` (the path used by `classify()`) returned Deceased on `!has_breathing && !has_movement`, also ignoring `reading.heartbeat`. A survivor with a detectable pulse but no sensed breathing/movement is in respiratory arrest — the most time-critical *savable* state. Reporting them Deceased would deprioritize a rescuable person. WiFi-CSI also cannot confirm death (no airway-repositioning step), so a pulse must override. Fix: in both paths, if the result would be Deceased but a heartbeat is present, return Immediate. Total absence of breathing, movement AND heartbeat is unchanged (domain → Unknown, ensemble → Deceased). 2 safety regression tests added. Full MAT suite: 168 + 6 + 3 passed, 0 failed (existing test_no_vitals_is_deceased still green — no heartbeat → Deceased).v1583 |
||
|
|
6ee21a0941 |
ci: use Swatinem/rust-cache for the Rust workspace job (reliability) (#925)
The Rust Workspace Tests job manually cached the whole `v2/target` via actions/cache@v4. For a 38-crate workspace that dir is multi-GB, and several CI runs this cycle intermittently died at the cache/setup step (after toolchain install, before "Run Rust tests"), each needing a rerun. Swatinem/rust-cache@v2 is the de-facto standard Rust CI cache: it caches the cargo registry/git + a pruned target, evicts stale dependencies, and restores large workspaces far more reliably and faster than a naive whole-target cache. `workspaces: v2` points it at the v2/ cargo workspace. Reliability/speed change — verified by observing subsequent main runs.v1581 |