mirror of
https://github.com/ruvnet/RuView
synced 2026-07-17 16:33:18 +00:00
v1669
165 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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). |
||
|
|
0cfd255730 |
fix: --export-rvf no longer silently produces a placeholder model (#920)
The --export-rvf handler ran *before* the --train/--pretrain handlers and unconditionally wrote placeholder sine-wave weights, then returned. So the documented `--train --dataset … --export-rvf <path>` workflow (user-guide.md) short-circuited to a PLACEHOLDER model and never trained — printing "exported successfully" for a non-functional model. Given the project's anti-"is it fake" stance, silently emitting a fake model is the wrong default. Fix: - Only emit the placeholder container-format demo when --export-rvf is used *standalone* (new `export_emits_placeholder_demo` guard). With --train/--pretrain, fall through so the real training pipeline runs and exports calibrated weights. - The standalone path now prints a clear WARNING that it writes a container-format demo with placeholder weights — not a trained model — pointing to --train / a pretrained encoder (#894). - Docs: flag --export-rvf as a placeholder demo in the flag table, and fix the Docker training example to use --save-rvf (consistent with the from-source example) instead of the placeholder --export-rvf. 3 unit tests for the guard. Full crate unit suite: 429 + 117 passed, 0 failed. |
||
|
|
f5d0e1e69e |
fix(#894): actionable diagnostic when --model gets a non-RVF file (#919)
Users who downloaded ruvnet/wifi-densepose-pretrained and passed model.safetensors / model-q4.bin / model.rvf.jsonl to --model hit a bare "Progressive loader init failed: invalid magic at offset 0: expected 0x52564653, got 0x77455735" and were stuck — the server then silently fell back to signal heuristics (which over-count, feeding "is it fake" reports). The HF files are a different *format* and encoder architecture than the RVF binary container the progressive loader expects, so they can't load directly. Now the load-failure path detects the common cases (safetensors header, JSONL manifest, quantized .bin blob) and emits a plain explanation naming the format, what --model actually expects (RVF `RVFS` container from wifi-densepose-train), and that it's continuing with heuristics — with a pointer to #894. Pure, testable `diagnose_model_load_error()` + 4 unit tests (run under the default `--no-default-features` CI). Full crate unit suite: 429 + 114 passed, 0 failed. |
||
|
|
b12662a54d |
fix(mqtt): per-node HA devices use each node's own presence/motion (#872) (#918)
The MQTT bridge fanned out one Home-Assistant device per node (#898) but applied the *room-level aggregate* classification to every node — so in a multi-node setup a node in an empty corner inherited another node's "present", and `motion_level: "absent"` was mis-mapped to full motion (the aggregate match fell through `Some(_) => 1.0`). Each node in the sensing broadcast's `nodes` array already carries its own `classification` (`motion_level`/`presence`/`confidence`, see PerNodeFeatureInfo) and RSSI. Now each per-node snapshot reads that node's own classification, deferring to the room aggregate only for fields a node omits. Vitals (breathing/heart rate) and person count stay room-level. Extracted the JSON→VitalsSnapshot mapping into a pure, testable function (`vitals_snapshots_from_sensing_json`) and added 4 unit tests covering per-node divergence, partial-field fallback, the no-nodes aggregate path, and the absent→zero-motion fix. Supersedes #899, which targeted the right bug but read non-existent fields (`node["motion_level"]` / `node["status"]` instead of the nested `node["classification"]` + `stale`). Verified: builds with `--features mqtt`; new tests pass; full crate unit suite 432 + 114 passed, 0 failed. |
||
|
|
4c87f04919 |
Merge remote-tracking branch 'origin/main' into fix/894-occupancy-cap
# Conflicts: # CHANGELOG.md |
||
|
|
f34b94aa46 |
fix(occupancy): bound eigenvalue person-count to single-link max — #894
field_bridge::occupancy_or_fallback returned FieldModel::estimate_occupancy
unbounded (internal ceiling 10), while the perturbation fallback below it
and score_to_person_count both cap at 3 ("1-3 for single ESP32"). On noisy
or under-calibrated CSI the eigenvalue count inflated → "10 persons when 1
present" (#894, seen when --model fails to load → heuristic mode). Bound the
eigenvalue path to a shared MAX_SINGLE_LINK_OCCUPANCY const (3) so every
single-link estimator agrees. Genuine higher counts come from the
multistatic fusion path. Build clean, field_bridge tests pass.
|
||
|
|
27edf153dc |
test(mqtt): drive per-node snapshots in discovery integration tests — #898
After the per-node discovery change, discovery configs are published the
first time a snapshot for a node_id arrives (not eagerly at startup). The
two discovery integration tests (discovery_topics_appear_on_broker,
privacy_mode_suppresses_biometric_discovery) spawned the publisher with an
empty broadcast channel and never sent a snapshot, so they collected []
and failed ("missing presence discovery topic in []").
Drive snapshots for the test node_id throughout the capture window (same
pattern as state_messages_published_on_snapshot_broadcast) so the per-node
device's discovery lands. Verified against a local mosquitto: 3 passed.
|
||
|
|
9ddcf0c9fc |
fix(mqtt): one HA device per node — closes #898
After the #872 MQTT wiring, the JSON->VitalsSnapshot bridge hard-coded a single node_id (the MQTT client id) and the publisher used one OwnedDiscoveryBuilder, so every physical node collapsed into a single Home-Assistant device (identifiers:["wifi_densepose_wifi-densepose-1"]), contradicting the one-device-per-node docs. - Bridge (main.rs): emit one VitalsSnapshot per node in the sensing update's nodes[] (each carries its own node_id + RSSI; shared aggregate presence/vitals), falling back to a single aggregate snapshot when there is no per-node data (wifi/simulate sources). - Publisher (publisher.rs): add OwnedDiscoveryBuilder::for_node(), and publish discovery + availability lazily on first sight of each node_id, routing state to per-node topics. Heartbeat/refresh/offline-LWT iterate all known nodes. Result: N distinct HA devices, one per node. 3 new unit tests (distinct nodes -> distinct wifi_densepose_<node> identifiers); full MQTT suite 71 passed, example builds. |
||
|
|
810ee656de |
fix(bfld): gate PrivacyAttestationProof::compute behind std
CI `cargo test --no-default-features (baseline regression)` failed with `error: associated function compute is never used` under -D warnings. compute() is only reachable via PrivacyModeRegistry (#[cfg(feature = "std")]); without std there is no caller. Gate the impl to match its only callers. Verified clean under --no-default-features, default, and --features mqtt with RUSTFLAGS=-D warnings. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
29e698a05c |
fix(ruview-swarm): clippy manual_is_multiple_of in lawnmower planner
CI `clippy (-D warnings, --no-deps)` failed on patterns.rs:131 — `row % 2 == 0` is flagged by clippy::manual_is_multiple_of. Use `row.is_multiple_of(2)` (identical even-row check). Both CI clippy variants (--no-default-features and --features full,train) now pass. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
138449a378 |
Merge remote-tracking branch 'origin/main' into feat/adr-149-aether-arena
# Conflicts: # CHANGELOG.md |
||
|
|
4007db5d13 |
fix(sensing-server): fix CSI per-node count clamp — #803 (part 2)
The pure-CSI per-node path clamped its own occupancy estimate before the aggregator could read it. estimate_persons_from_correlation (DynamicMinCut) returns 0-3, but it was mapped to a score via `corr_persons / 3.0`, putting 2 people at 0.667 — just under the 0.70 up-threshold of score_to_person_count — so the per-node count never climbed past 1, leaving node_max stuck at 1 for CSI-only nodes even when the min-cut cleanly separated two people. Replace the lossy /3.0 mapping with a threshold-aligned corr_persons_to_score (1->0.40, 2->0.74, 3->0.96) whose steady state round-trips back to the same count through the EMA + hysteresis bands, while still gating transient noise. A convergence test replays the exact CSI-loop EMA and asserts min-cut=2 now reports 2 / 3 reports 3 / 1 reports 1, plus a regression test documenting that the old /3.0 mapping pinned two people to 1. Full suite: 586 passed, 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
a933fc7732 |
fix(sensing-server): surface count-aware per-node estimates — #803
Person count was pinned to 1 because the aggregate was derived from `smoothed_person_score`, an EMA-smoothed *activity* score (amplitude variance / motion / spectral energy) that saturates near a single occupant and cannot discriminate count. The count-aware per-node estimates the ESP32 paths already compute (firmware n_persons, mincut corr_persons) were stored in NodeState::prev_person_count then discarded by the aggregator — the same dead-wiring class as #872. Add `aggregate_person_count(activity_count, node_states)` = max(activity, node_max) and use it at both ESP32 aggregation sites (edge-vitals + CSI loop, Some + fallback arms). It can only raise the count when a node positively reports more occupants, so the lone-occupant case is provably never inflated (regression-guarded). 5 new unit tests + full suite: 582 passed, 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
a3f80b0cda |
fix(sensing-server): wire MQTT publisher into the binary — closes #872
#872 reported '--mqtt: unexpected argument' on the Docker image; prior attempts chased a Docker *rebuild*, but the real cause was disconnected *code*: the --mqtt* flags lived only in cli::Args (dead code — referenced nowhere), while the binary parses a separate main::Args with no mqtt fields, and main.rs never declared/started the mqtt:: publisher. So MQTT was fully unwired: flags didn't parse, and the publisher never ran. Fix: - Extract the mqtt + privacy flags into a shared (#[derive(clap::Args)]); retarget mqtt::config::{from_args,build_tls} to it. - #[command(flatten)] MqttArgs into the binary's main::Args (using the *lib* crate's type so it matches from_args), so --mqtt* now parse. - Spawn the publisher on --mqtt: build MqttConfig, validate, and bridge the existing JSON sensing broadcast into the typed VitalsSnapshot stream the publisher consumes (defensive serde_json::Value mapping — absent fields default, never wrong values). #[cfg(feature=mqtt)]-gated; without the feature --mqtt WARNs and no-ops (documented contract). Fix the mqtt_publisher example for the new signature. Verified end-to-end against local mosquitto: publisher connects and emits 20 HA auto-discovery entities + live state (presence ON, person_count, …). Tests: 577 pass default / 580 pass --features mqtt / 0 fail; both configs build. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
edbe57378a |
fix(signal/cir): un-ignore end-to-end CIR pipeline test — ADR-134 P2 fully resolved
The cir_pipeline end-to-end test was gated on the same dominant_tap_ratio floor; the windowed-ratio fix resolves it. All 6 ADR-134 P2 CIR tests (cir_synthetic 5 + cir_pipeline 1) now pass. signal+cir: 472 pass / 0 fail. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
821f441af0 |
fix(signal/cir): causal-delay-window rms spread — resolves last ADR-134 P2 cir test
Found the principled fix for the rms-delay-spread inflation (superseding my prior 'needs ISTA work' note): the spurious ~15-20% tap at ~bin 150 is an ALIAS of the near-zero dominant tap — the ISTA delay grid is circular (Φ is DFT-like), so bins >= G/2 are non-causal negative delays. Computing the delay spread over only the causal half [0, G/2) drops rms from 389ns to 65ns (true value), cleanly and robustly (no fragile magnitude threshold). Un-ignores should_produce_positive_rms_delay_spread. ADR-134 P2 cir_synthetic now FULLY resolved: all 5 previously-ignored tests pass via two physics-justified fixes (windowed dominant-ratio for super- resolution leakage + causal-window rms for circular-grid aliasing). signal+cir: 471 pass / 0 fail / 0 ignored in cir_synthetic. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
bce5765d89 |
docs(signal/cir): precise diagnosis of remaining ADR-134 P2 rms-spread failure
Diagnosed the one still-ignored CIR test: ISTA emits a spurious ~15-20%-of- dominant tap at an implausible far delay (~bin 150 / ~3us) that inflates rms_delay_spread to ~390ns (vs ~53ns true). It sits too close to the real weakest tap (~30% of dominant) for a safe magnitude cutoff, so the proper fix is ISTA recovery-quality work (grid de-aliasing / far-tap suppression), not a band-aid threshold. Sharpened the #[ignore] note accordingly. signal+cir: 470 pass / 0 fail. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
d55c4d4b65 |
fix(signal/cir): resolve ADR-134 P2 dominant-tap-ratio — un-ignore 4 CIR tests
The CIR estimator's dominant_tap_ratio measured a single grid bin, but on the
3x super-resolved ISTA grid a single physical tap leaks across ~3 adjacent
bins — so the ratio under-counted the dominant tap and sat far below the
per-tier floors (HT20 0.158<0.30, HT40 0.133<0.35, HE20 0.102<0.40), forcing
the 3-tap recovery + 40MHz-ToF tests to be #[ignore]d.
Fix (data-backed via a lambda sweep): (1) compute dominant_tap_ratio over a
+/-1-bin window around the peak — the physical tap's true footprint; (2) tune
L1 lambda for sparse multipath (HT20 .05->.08, HT40 .03->.08, HE20 .03->.18).
Result: ratios 0.367/0.406/0.474, comfortably above floors with all 3 taps
preserved. Un-ignores should_recover_3tap_channel_{ht20,ht40,he20} and
should_return_tof_at_40mhz. signal crate: 470 pass / 0 fail; change isolated
to CIR (no external consumers). The rms-delay-spread test stays ignored with a
re-scoped note (far-tap robustness is separate remaining work).
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
0fede72ec4 |
test(cog-pose): cross-language adapter integration (Python producer -> Rust engine)
Closes the last verification gap in the calibration feature: previously the Python producer and Rust consumer were proven compatible only by format matching. Now a real ~11KB adapter fitted by cog_calibrate.py on the in-repo pose_v1.safetensors is committed as a fixture, and a Rust test loads it via the engine and asserts is_calibrated() + that it changes inference output. The full Python->Rust calibration contract is verified with a real artifact. 7/7 cog-pose tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
946acf2d10 |
docs(cog-pose): correct misleading adapter cross-reference
The --adapter docs claimed the adapter is produced by aether-arena/calibration/calibrate.py, but that reference tool targets the MM-Fi *transformer* model and emits .npz with proj/head LoRA keys, while this cog runs a *conv+MLP* model expecting safetensors with fc1.a/fc1.b/ fc2.a/fc2.b. Same LoRA mechanism, different model -> adapters are model-specific and NOT interchangeable. Clarify the expected key layout and that the Python tool is a mechanism reference, not a drop-in producer. 6/6 tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
1b48b6f5c8 |
fix(bfld): make README quickstart test robust to CRLF line endings
readme_quickstart_uses_canonical_public_api checked a multi-line needle 'pipeline\n .process' against the include_str! README. On a CRLF checkout (Windows / core.autocrlf) the content is 'pipeline\r\n .process', so the LF needle never matched and the test failed deterministically (only surfaced once the worldmodel fix let cargo test --workspace run on Windows; the test is #[cfg(feature=std)]-gated, enabled via workspace feature unification). Normalize CRLF->LF before the check. Full workspace now green 3/3 runs on Windows. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
c9539433b8 |
fix(worldmodel): compile on non-unix targets (Windows workspace build)
bridge.rs imported tokio::net::UnixStream unconditionally, so the whole workspace failed to build on Windows (E0432) — blocking cargo test --workspace and the pre-merge gate there. The OccWorld Unix-socket bridge is a Linux-appliance feature (Python inference server on the GPU host), so gate it #[cfg(unix)] and add a #[cfg(not(unix))] send_recv that fails fast with a clear 'unsupported on this target' Protocol error. Workspace now builds on Windows; worldmodel 12 tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
83299b4d04 |
feat(cog-pose): --adapter CLI flag for per-room calibration
Completes the end-to-end product path: cog-pose-estimation run --config <cfg> --adapter <room.safetensors> loads the shared base + a per-room LoRA adapter for calibrated inference. Adds InferenceEngine::with_adapter() (default weights + adapter) and logs when a calibration adapter is active. 6/6 tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
3760db6c9a |
feat(cog-pose): per-room LoRA calibration adapter in the Rust inference path
Ports the calibration mechanism (ADR-150 §3.5-3.6, reference impl in aether-arena/calibration/) into the real product pose engine. The Candle InferenceEngine now loads an optional per-room adapter safetensors and applies low-rank deltas (y + (x.A).B) on the fc1/fc2 head at inference. Architecture-agnostic LoRA; base behaviour unchanged when no adapter. New API: with_weights_and_adapter(), is_calibrated(). Tested: adapter detection + output-change integration test (6/6 pass). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
8d64434d21 |
feat(swarm): ADR-149 evaluation harness — GDOP, IQM+bootstrap CI, noise sweep (#875)
Stage-1 kinematic evaluator per ADR-149 (peer-reviewed). Pure Rust, no new deps. evals/: - gdop.rs: 2D Geometric Dilution of Precision ((HᵀH)⁻¹ trace-sqrt); None for <2 observers or collinear/singular geometry - stats.rs: IQM (Agarwal 2021) + 95% stratified-bootstrap CI (deterministic LCG) + probability_of_improvement - metrics.rs: EpisodeMetrics + AggregateMetrics::from_strata (IQM±CI, seed-stratified) - runner.rs: seeded kinematic rollout (FlightPattern-driven), seed×episode matrix, 3σ×3κ default noise sweep (Gaussian amplitude × von Mises phase) - report.rs + eval_swarm bin: generates evals/RESULTS.md leaderboard RESULTS.md surfaces the real coverage-vs-localization-precision trade-off via GDOP: partitioned wins coverage (100%) but single-drone sightings (GDOP 0 → 7.0m); pheromone gets multistatic fusion (GDOP 1.6 → 4.1m). Wi2SAR 5m paper-baseline row included. Stage-2 (Gazebo/PX4 SITL false-alarm + collision on median seeds) is documented follow-on. Tests: 116 default / 133 full+train (+13 eval tests), 0 failed. Clippy clean (-D warnings). |
||
|
|
483bfa4660 |
feat(aether-arena): benchmark-first scorer + witness chain + repeatability (M2/M5/M7)
Per direction "remove the initial number, optimize for benchmark first" + "include witness chain capabilities for proof and repeatability analysis": - Empty board, no seeded numbers: ledger seeds to genesis only. Every result is a real scoring-pipeline witness; RuView gets no hand-entered baseline. - Real model scoring: aa_score_runner now loads predictions + an eval split (--split/--pred) and scores them through the real ruview_metrics pose harness — not just a synthetic fixture. Committed public smoke split (fixtures/smoke_*.json). - Witness chain: each score emits a witness = inputs_sha256 (binds it to the exact inputs) + proof_sha256 (cross-platform-stable score hash) + harness_version. - Repeatability analysis: --repeat N runs the harness N× and fails if it ever yields >=2 distinct proof hashes (16/16 identical locally). - Witness ledger: ledger/ledger_tools.py — append-only, hash-chained, tamper- evident (seed/append/verify); editing any past row breaks the chain. - CI gate extended: determinism + repeatability(16) + real-scoring smoke + ledger chain verify on every PR. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
a6808568a2 |
feat(aether-arena): ADR-149 spatial-intelligence benchmark — scorer + CI harness gate (M1-M4)
AetherArena ("AA") — the official, project-agnostic Spatial-Intelligence Benchmark
(ADR-149, Accepted). Iteration 1 of the long-horizon build:
- ADR-149 accepted: name locked (ruvnet/aether-arena), v0 metrics locked
(pose/presence/latency/determinism), dataset legality resolved (MM-Fi CC BY-NC
only; Wi-Pose excluded). Adds four-part framing, threat model, arena_score
formula, submission state machine, neutrality/governance, and the §7 acceptance test.
- aa_score_runner: deterministic scorer bin reusing the real ruview_metrics pose
harness on a fixed seed=42 fixture → RuViewTier-style verdict + cross-platform
SHA-256 proof hash. Builds --no-default-features (no torch/GPU). VERDICT: PASS.
- CI harness gate: .github/workflows/aether-arena-harness.yml runs the scorer on
every PR — the "PR that runs the harness as part of the build" requirement.
- Scaffold: aether-arena/{README,VERIFY,STATUS}.md + schema/aa-submission.toml.
- Horizon record persisted (.claude-flow/horizons/aether-arena-aa.json).
Infra = the deliverable; model SOTA (MM-Fi PCK@20) is a separate effort blocked on
ADR-079 data collection, tracked as a stretch goal, not an infra exit.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
0d3d835bf8 |
feat(swarm): add ruview-swarm crate — drone swarm control system (ADR-148) (#862)
* feat(swarm): add wifi-densepose-swarm crate implementing ADR-148 drone swarm control system
New crate `wifi-densepose-swarm` with hierarchical-mesh swarm topology,
Raft consensus, MAPPO MARL, CSI sensing integration, and ITAR-gated
coordination features. Closes 3 of 7 milestones (M1, M2, M5) with 5/5
ADR-148 SOTA performance targets met.
## Modules (45 source files, 14 modules)
- types: NodeId, DroneState, Position3D, SwarmTask, SwarmError, FailSafeState
- topology: Raft consensus (leader election, log replication, quorum), Gossip, Mesh
- formation: VirtualStructure, LeaderFollower, Reynolds flocking (itar-gated)
- planning: RRT-APF hybrid planner, 3-phase coverage, Bayesian grid, pheromone
- allocation: Auction + FNN bid scorer (itar-gated)
- sensing: CsiPayloadPipeline (Live/Synthetic/Replay), MultiViewFusion, OccWorldBridge
- marl: MAPPO actor (3-layer MLP), LocalObservation (64-dim), RewardCalculator, PPO loop
- security: MAVLink v2 HMAC-SHA256, UWB anti-spoofing, geofence, Remote ID, FHSS
- failsafe: 10-state onboard machine, GCS-independent safety transitions
- config: TOML SwarmConfig with SAR/inspection/agriculture/mine/demo/wi2sar_reference
- demo: SyntheticCsiGenerator, DemoScenario (SAR/open-field/mine)
- integration: FlightController trait, MAVLink dialect (50000-50005), SwarmSim
- orchestrator: SwarmOrchestrator wiring all subsystems end-to-end
- bench_support: Criterion fixture generators
## ITAR compliance
Swarming coordination features gated behind `itar-unrestricted` feature
per USML Category VIII(h)(12). Default build compiles clean stubs.
## Benchmark results (criterion, release mode)
- MARL actor inference: 3.3 µs (target ≤ 5 ms — 1,516× headroom)
- RRT-APF planning (100 iter): 0.043 ms (target < 300 ms — 6,946× headroom)
- MultiView CSI fusion (3 UAVs): 58.5 ns (target < 10 ms — 171,000× headroom)
- 3-view localization: 1.732 m (target ≤ 2 m — beats Wi2SAR SOTA)
- 4-drone SAR coverage (400×400 m): 223 s (target ≤ 240 s — PASS)
## Tests
- --no-default-features: 73/73 passing
- --features itar-unrestricted: 85/85 passing
Closes #861
Co-Authored-By: claude-flow <ruv@ruv.net>
* refactor(swarm): rename wifi-densepose-swarm → ruview-swarm
The swarm control system is a RuView-level capability (drone coordination,
Raft consensus, MARL) that operates above the wifi-densepose sensing layer
rather than being a sub-component of it. Rename aligns with the project
identity and separates coordination infrastructure from sensing modules.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(swarm): resolve all clippy warnings + add MARL convergence test
- planning/probability_grid: map_or(true,…) → is_none_or (clippy::unnecessary_map_or)
- planning/pheromone: &mut Vec<T> → &mut [T] on evaporate+deposit (clippy::ptr_arg)
- marl/observation: fix doc lazy-continuation warning on TOTAL line
- marl/trainer: manual Default impl → #[derive(Default)] + #[default] on Demo variant
Also adds test_marl_convergence_improves_mean_return: fills 64-transition
ReplayBuffer with mixed rewards (steps 0-31: negative, 32-63: positive),
runs ppo_update, asserts mean_return is finite and non-zero.
Result: 0 clippy warnings · 74/74 tests (default) · 86/86 (itar-unrestricted)
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(swarm): integrate Ruflo AI-agent capabilities into ruview-swarm
Adds a feature-gated Ruflo integration layer connecting ruview-swarm to the
claude-flow daemon's AgentDB, AIDefence, and SONA intelligence subsystems.
Default build is unaffected (all paths behind `Option<Box<dyn RufloBackend>>`).
## New module: src/ruflo/
- backend.rs: RufloBackend trait (9 async methods) + RufloError, MissionMemoryEntry,
PatternEntry, MavlinkScanResult types (always compiled)
- mock_backend.rs: MockRufloBackend in-memory impl for testing (always compiled, 5 tests)
- http_backend.rs: HttpRufloBackend — JSON-RPC 2.0 → claude-flow daemon localhost:3000
(gated behind `ruflo` feature, requires reqwest)
- mission_summary.rs: MissionSummary serializer with pattern description + confidence
scoring from victim recall, coverage %, collision penalty (always compiled, 3 tests)
## 4 capability areas
1. MissionMemory → memory_store / memory_search (cross-mission victim memory)
2. PatternLearner → agentdb_pattern-store / -search (HNSW SONA trajectory patterns)
3. MavlinkDefence → aidefence_is_safe / aidefence_scan (scan MAVLink before accepting)
4. IntelligenceHooks → trajectory-start/step/end (SONA learning loop)
## SwarmOrchestrator integration
- with_ruflo(backend): builder to attach a backend
- start_trajectory(task) / finish_trajectory(success, key): SONA mission lifecycle
- receive_peer_detection_checked(): AIDefence scan before accepting peer detections
## Cargo feature
`ruflo = ["dep:reqwest", "dep:serde_json"]` — optional, not in default
## Tests
- --no-default-features: 82/82 pass (8 new ruflo tests)
- --features ruflo,itar-unrestricted: 94/94 pass
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(swarm): M7 mission profiles with victim confirmation reports + pre-merge docs
Adds end-to-end mission runners producing structured MissionReport output,
and updates project docs (CHANGELOG, README, CLAUDE.md) per pre-merge checklist.
## M7 Mission Profiles (integration/mission_report.rs + swarm_sim.rs)
- MissionReport / VictimReport / SotaComparison types (serde-serializable)
- run_mission_with_report(): full mission → detailed report with per-victim
localization error, fusion uncertainty, contributing drones, detection time
- run_inspection_mission(): leader-follower power-line corridor inspection
- run_mine_mission(): GPS-denied underground (2-drone, slow, UWB-only)
- SotaComparison embeds Wi2SAR baseline (5m / 810s) vs achieved metrics
## Docs (pre-merge checklist)
- CHANGELOG.md: ruview-swarm + Ruflo integration + performance entries
- README.md: ruview-swarm row
- CLAUDE.md: Key Rust Crates table row + ADR-148 in ADR list
## Tests
- --no-default-features: 86/86 pass
- --features ruflo,itar-unrestricted: 98/98 pass
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(swarm): convergence-assist for victim fusion + 5s Ruflo HTTP timeout
Follow-up to
|
||
|
|
9ad550d95f |
feat(worldmodel): Candle Rust port + GCP GPU scripts (ADR-147 Phase 4+6)
Candle native port — wifi-densepose-occworld-candle v0.3.0: - config.rs: OccWorldConfig (14 params matching occworld.py) - vqvae.rs: ClassEmbedding(18→64), VQCodebook(512×512, squared-L2), QuantConv/PostQuantConv(1×1 Conv2d), fold_3d_to_2d helpers ResNet encoder/decoder are documented stubs (Phase 5 checkpoint pending) - transformer.rs: full Candle MHA transformer (2 layers, temporal+spatial cross-attention, FFN, pre-norm residuals) - inference.rs: OccWorldCandle::dummy() + ::load() + predict() InferenceOutput: sem_pred(1,15,200,200,16) + trajectory_priors - 14/14 tests pass (12 lib + 2 doctests) GCP GPU scripts — scripts/gcp/: - provision_training.sh: a2-highgpu-8g (8×A100 40GB) for Phase 5 retraining - run_training.sh: rsync + torchrun 8-GPU train + checkpoint download - provision_cosmos.sh: a2-ultragpu-1g (A100 80GB) for Cosmos evaluation - cosmos_eval.sh: run Cosmos-Transfer2.5 inference, download results - teardown.sh: safe checkpoint download + instance delete Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
cd1c391afc |
feat(worldmodel): ADR-147 Phase 3+5 — RuViewOccDataset domain adapter + retraining pipeline
Phase 3 — scripts/ruview_occ_dataset.py: - RuViewOccDataset: WorldGraph JSON snapshots → OccWorld (F,H,W,D) tensors - Indoor class remapping: person→7, floor→9, wall→11, furniture→16, free→17 - Zero ego-poses (fixed indoor sensor, no ego-motion) - record_snapshot() helper for training data accumulation - Validated: 5 windows, (16,200,200,16) tensor, person+floor voxels confirmed Phase 5 — scripts/occworld_retrain.py: - record: stream WorldGraph snapshots from sensing server REST API - vqvae: fine-tune VQVAE tokenizer on RuView occupancy (200 epochs, AdamW) - transformer: fine-tune autoregressive transformer with frozen VQVAE wifi-densepose-worldmodel v0.3.0 published to crates.io Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
28a27bbfd8 |
fix(worldmodel): use published worldgraph v0.3.0 instead of path dep (crates.io publish prep)
Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
c7ddb2d7d1 |
feat(worldmodel): ADR-147 — OccWorld world model integration, wifi-densepose-worldmodel v0.3.0 (#856)
* feat(worldmodel): ADR-147 — OccWorld integration, wifi-densepose-worldmodel v0.3.0 (#854) - New crate `wifi-densepose-worldmodel` v0.3.0: async Unix-socket bridge to OccWorld Python inference server; `OccWorldBridge`, `OccupancyGrid3D`, `TrajectoryPrior`, `worldgraph_to_occupancy` encoder (14/14 tests pass) - `scripts/occworld_server.py`: long-lived Python inference server for OccWorld TransVQVAE (72.4M params); applies API-bug patches; dummy mode for CI testing; graceful SIGTERM shutdown - `pose_tracker.rs`: `trajectory_prior` soft-blend injection (80/20 Kalman/prior) on torso keypoint; `set_trajectory_prior()` public method - CI: added `Run ADR-147 worldmodel tests` step - ADR-147: accepted — OccWorld primary (209 ms, 3.37 GB VRAM, RTX 5080); Cosmos deferred to ADR-148 (32.54 GB VRAM exceeds hardware) - Benchmark proof: 208.7 ms P50, 3.37 GB peak VRAM, 12.1 GB headroom Co-Authored-By: claude-flow <ruv@ruv.net> * chore: update ruvector.db state Co-Authored-By: claude-flow <ruv@ruv.net> * chore: ruvector.db sync Co-Authored-By: claude-flow <ruv@ruv.net> * fix(cli): add missing min_frames field to CalibrateArgs test helper E0063 in calibrate.rs:448 — CalibrateArgs gained min_frames in ADR-135 but the default_args() test helper was not updated. min_frames=0 means 'use tier default', matching the existing runtime behaviour. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
d24bf36110 |
release: version bumps for crates.io publish (streaming-engine cascade)
- core 0.3.0->0.3.1 (ComplexSample/CanonicalFrame/provenance + blake3 dep) - ruvector 0.3.0->0.3.1 (ClockQualityGate) - bfld 0.3.0->0.3.1 (privacy control plane) - signal 0.3.1->0.3.2 (fuse_scored_calibrated/ArrayCoordinator/evolution/rf_slam) - geo: add license/repository for first publish; worldgraph/engine pin geo version - new: geo 0.1.0, worldgraph 0.3.0, engine 0.3.0 Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
95bdd37e76 |
bench+test: engine per-cycle benchmark + ADR-142 acceptance path
- engine: criterion benchmark engine_cycle — full process_cycle (4 nodes / 56 subcarriers) measured at ~6.35 us/cycle, ~7800x under the 50ms (20Hz) budget. - signal: ADR-142 acceptance test — 3 links drift 30 frames -> ChangePoint -> VoxelMap accumulates -> low-confidence voxels suppressed -> VoxelGate Restricted emits histogram only -> ADR-137 contradiction recorded. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
020aa08049 |
test(sensing-server): ADR-140 live acceptance — snapshot to expired-rejection
Drives a real SemanticBus: raw snapshot (fall_detected, past warmup) -> FallRisk primitive -> SemanticStateRecord (provenance) -> single-signal rule fires / multi-signal agreement rule does NOT (no false escalation) -> expired record rejected. Proves the ADR-140 credibility path end to end. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
5878868060 |
feat(signal,engine): ADR-137 calibration-mismatch contradiction + trust witness
- signal: MultistaticFuser::fuse_scored_calibrated() threads per-node CalibrationId; agreeing epochs → calibration_id set + CalibrationApplied evidence; disagreeing → calibration_id None + CalibrationIdMismatch flag (forces demotion). +2 tests. - engine: process_cycle_calibrated() per-node calibration path; process_cycle delegates with a uniform epoch. TrustedOutput gains a deterministic BLAKE3 witness over (provenance || class). calibration_version='cal:none' on mismatch. - ADR-137 acceptance test: two frames + mismatched calibration -> QualityScore contradiction -> Restricted -> calibration_id None -> witness stable. +happy path. - 11 engine tests, signal 411+ lib tests; workspace 0 errors. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
2517a16d88 |
feat(engine): compose ADR-138/142/143 + ADR-139 live loop
- ADR-138: process_cycle runs ArrayCoordinator when node geometry is registered; array contradictions (CoherenceDrop/GeometryInsufficient) fold into the privacy demotion; DirectionalEvidence surfaced in TrustedOutput - ADR-142: per-node mean-amplitude → EvolutionTracker; cross-link change-point recorded as a WorldGraph Event node - ADR-143: ingest_reflectors() runs Rf-SLAM discovery, writes stable Wall/Furniture reflectors as ObjectAnchor nodes - ADR-139 live loop: update_person_track(), apply_active_privacy_mode() (PrivacyRollup suppresses person_track under identity-strict modes), snapshot_json() - Acceptance test live_frame_to_reload_same_contents: full path fusion->worldgraph->privacy_rollup->persist->reload->same contents, no raw RF - 9 engine tests; workspace 0 errors Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
2eada40e3b |
feat(engine): integrate ADR-135..141 into an end-to-end trust pipeline
- signal/calibration.rs: BaselineCalibration gains calibration_id()/ calibration_uuid()/apply() — the ADR-135->136 link that stamps FrameMeta.calibration_id (deterministic id, no serialization change). +1 test. - NEW crate wifi-densepose-engine: StreamingEngine::process_cycle() composes fuse_scored (137) -> calibration provenance (135/136) -> privacy demotion on contradiction (141) -> WorldGraph SemanticState with mandatory provenance + DerivedFrom edge (139). Returns TrustedOutput (the trust chain made concrete). - Validates the throughline: every output names evidence + model + calibration + privacy decision; calibration_id flows input->QualityScore->provenance; contradiction demotes class; deterministic; privacy mode attested. - 4 integration tests; workspace 0 errors; signal 410 lib tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
f18b096f2f |
feat(nn): ADR-146 RF encoder multi-task heads + uncertainty (#850)
- nn/rf_encoder.rs (forward-looking; extends ADR-024 AETHER):
- RfEmbedding (256-d pure-Rust f32 ABI), TaskKind (7 heads)
- LinearHead: W*emb+b + separate log-variance projection → HeadOutput with
softplus uncertainty + confidence(); MultiTaskHeads.forward_subset() for
ADR-145 ablation toggling
- calibration_robustness_loss (ADR-135 invariance), triplet_loss (ADR-024)
- ContrastiveBatcher: deterministic cross-environment positive / different-
state negative triplet sampling (ADR-027 MERIDIAN)
- 7 tests; workspace 0 errors
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
0f336b7d36 |
feat(train): ADR-145 ablation eval harness + privacy-leakage/latency metrics (#849)
- train/ablation.rs: FeatureSet matrix (CSI/CIR/CSI+CIR/+Doppler/+BFLD/+UWB); AblationMetrics (presence acc, loc err, FP/FN, latency p50/p95, privacy leakage, cross-room degradation) derived deterministically from VariantRun - membership_inference_leakage(): MIA proxy = |AUC-0.5|*2 (0 indistinguishable, 1 perfectly separable); latency_percentiles_ms (nearest-rank); confusion_rates - AblationReport.to_markdown() (deterministic), csi_cir_beats_csi_only() acceptance check - 5 tests; workspace 0 errors Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
b10bc2e9ab |
feat(mat): ADR-144 UWB range-constraint fusion (#848)
- mat/localization/range_constraint.rs (forward-looking; no UWB hw yet):
- RangeConstraint domain model (anchor_id/pos/measured_range/uncertainty/
signal_quality); predicted_range/residual/mahalanobis/is_consistent
- RangeConstraintFusion::refine() — Newton-normalized weighted least-squares
that constrains a CSI/CIR prior toward range spheres, Mahalanobis-gates
inconsistent (NLOS/multipath) ranges; returns RefineResult with rejected
anchors + RMS residual
- associate() disambiguates which track a range belongs to (re-ID hook)
- 4 tests (converges to truth, absurd range gated, consistency math, track
association); workspace 0 errors
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
2d4f3dea53 |
feat(signal): ADR-143 RF-SLAM reflector discovery + anchor learning (#847)
- ruvsense/rf_slam.rs (forward-looking, ships v1 fixed-map first):
- RfSlam::fixed_map() — discovery disabled (v1); with_discovery() — v2
- ReflectorObservation (CIR-tap sighting), PersistentReflector (per-axis
Welford position, migration_m_per_day, classify Wall/Furniture/Mobile)
- observe(): nearest-reflector association within assoc_radius or seed new;
coherence-gated; static_anchors() rejects Mobile → ADR-139 ObjectAnchor set
- persistent_count() for topology-change detection
- 6 tests (fixed-map no-op, persistence, low-coherence reject, cluster split,
mobile excluded, static→Wall); workspace 0 errors
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
1f8e180d69 |
feat(signal): ADR-142 evolution tracker + temporal VoxelMap (#846)
- ruvsense/evolution.rs (extends ADR-030):
- TemporalVoxel: Bayesian log-odds occupancy update, evidence_count,
confidence = 1-exp(-count/5) (5-frame low-confidence floor), Welford
variance, doppler attribution, last_update_ns
- TemporalVoxelMap: persistent grid, observe(), low_confidence_indices()
- EvolutionTracker: per-link Welford baselines + cross-link change-point
(>=3 links beyond 2sigma in one window); divergence checked vs prior baseline
- VoxelGate: privacy demotion (Anonymous clears doppler+confidence, keeps
occupancy; Restricted → occupancy histogram only, raw map cleared)
- reuses field_model::WelfordStats; 6 tests; workspace 0 errors
Co-Authored-By: claude-flow <ruv@ruv.net>
|