mirror of
https://github.com/ruvnet/RuView
synced 2026-06-09 10:13:17 +00:00
601b3406fd9ff0476d74a0ce20fed8c13730e8f7
469 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
601b3406fd |
Merge pull request #542 from ruvnet/claude/design-rvcsi-platform-X7yJR
docs: rvCSI edge RF sensing platform — PRD, ADR-095, DDD domain model |
||
|
|
deb561bf9c |
fix(rvcsi): scale-relative baseline-drift thresholds + ESP32 end-to-end validation
BaselineDriftDetector compared `mean_amplitude` against its EWMA baseline with *absolute* thresholds (anomaly 1.0, drift 0.15). Fine for the synthetic unit tests (amplitudes ~1.0), but raw ESP32 CSI is int8 I/Q with amplitudes up to ~128, so window-to-window RMS distance is routinely 5-50 >> 1.0 and AnomalyDetected fired on ~96% of windows (319/331 on a real node-1 capture). Drift is now `||current - baseline||2 / ||baseline||2` (a fraction, with an eps floor that falls back to absolute for a degenerate near-zero baseline), so one tuning is valid across raw-int8 ESP32, int16-scaled Nexmon, and baseline-subtracted streams. AnomalyDetected drops to 40/331 on the same data; the existing detector tests still pass (their explicit configs are valid relative thresholds too); added baseline_drift_is_scale_invariant_ no_anomaly_storm. rvcsi-events 18 -> 19 tests; 162 rvcsi tests, 0 failures, clippy-clean. Surfaced by an end-to-end test against real ESP32 CSI on COM7: the device (ESP32-S3, node 1, ADR-018 firmware, WiFi "ruv.net" ch5 RSSI -39, CSI cb only because nothing listens at .156). rvcsi has no ESP32 adapter yet, so a 7,000-frame node-1 recording was transcoded to .rvcsi via the new scripts/esp32_jsonl_to_rvcsi.py (stand-in for `record --source esp32-jsonl`) and run through `rvcsi inspect`/`replay`/`calibrate`/`events` end-to-end. ADR-095 D13 and ADR-096 sections 2.1/5 updated; CHANGELOG entry added; rvcsi-adapter-esp32 (live serial/UDP source) noted as a follow-up. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
d40411e6d7 |
feat(rvcsi): Raspberry Pi 5 (BCM43455c0) + Nexmon chip registry
Adds first-class support for the Raspberry Pi 5's WiFi chip (CYW43455 /
BCM43455c0 — the same 802.11ac wireless as the Pi 4 / Pi 3B+ / Pi 400, and the
chip with the most mature nexmon_csi support), plus a registry of the other
Nexmon-supported Broadcom/Cypress chips.
rvcsi-adapter-nexmon — new `chips.rs`:
- `NexmonChip` (Bcm43455c0, Bcm43436b0, Bcm4366c0, Bcm4375b1, Bcm4358, Bcm4339,
Unknown{chip_ver}) + `RaspberryPiModel` (Pi5/Pi4/Pi400/Pi3BPlus/PiZero2W/
PiZeroW) — Pi5/Pi4/Pi400/Pi3B+ → Bcm43455c0; PiZero2W → Bcm43436b0.
- `nexmon_adapter_profile(chip)` / `raspberry_pi_profile(model)` build the
per-device `AdapterProfile` (channels: 2.4 GHz 1-13 + 5 GHz UNII for dual-band;
bandwidths 20/40/80[/160]; expected subcarrier counts 64/128/256[/512]) that
`validate_frame` bounds CSI frames against.
- `NexmonChip::from_chip_ver` (0x4345 → Bcm43455c0, 0x4339, 0x4358, 0x4366,
0x4375 — best-effort; the raw `chip_ver` is always preserved) and `from_slug`
/ `RaspberryPiModel::from_slug` ("pi5", "raspberry pi 4", "bcm43455c0", ...).
- `NexmonCsiHeader::chip()`; `NexmonPcapAdapter` auto-detects the chip from the
packets' `chip_ver` and uses the matching profile, overridable via
`.with_chip(NexmonChip)` / `.with_pi_model(RaspberryPiModel)`; `.detected_chip()`.
rvcsi-runtime: `decode_nexmon_pcap_for(.., chip_spec)` (validate against a chip /
Pi model, drop non-conforming) + `nexmon_profile_for(spec)`; `NexmonPcapSummary`
gains `chip_names` + `detected_chip`; `CaptureSummary` gains `chip`.
rvcsi-cli: `record --source nexmon-pcap --chip pi5`; new `nexmon-chips`
subcommand (lists chips + Pi models, human or `--json`); `inspect-nexmon` and
`inspect` now print the resolved chip.
rvcsi-node (napi-rs): `nexmonDecodePcap` gains an optional `chip` arg;
`nexmonChipName(chipVer)`, `nexmonProfile(spec)`, `nexmonChips()`. @ruv/rvcsi
SDK + `.d.ts` updated (AdapterProfile / NexmonChipsListing interfaces, the new
fns, `chip` on CaptureSummary, `chip_names`/`detected_chip` on NexmonPcapSummary).
168 rvcsi tests pass (adapter-nexmon 22→28, cli 9→10), 0 failures, clippy-clean.
The synthetic test captures now stamp chip_ver = 0x4345 (the BCM4345 family chip
ID), so the chip-detection happy path is exercised end to end.
ADR-096, CHANGELOG, README, CLAUDE.md updated.
https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z
|
||
|
|
b116a99481 |
feat(rvcsi): real nexmon_csi UDP/PCAP fidelity — chanspec decode, libpcap reader, NexmonPcapAdapter
Raises the Nexmon path from a normalized record format to parsing what the patched Broadcom firmware actually emits, end to end. napi-c shim (ABI 1.0 -> 1.1, additive): - rvcsi_nx_csi_udp_header / rvcsi_nx_csi_udp_decode — parse the real nexmon_csi UDP payload: the 18-byte header (magic 0x1111, rssi int8, fctl, src_mac[6], seq_cnt, core/spatial-stream, Broadcom chanspec, chip_ver) + nsub complex CSI samples (modern int16 LE I/Q export — what CSIKit/csireader.py read for the BCM43455c0 / 4358 / 4366c0; nsub = (len-18)/4). rvcsi_nx_csi_udp_write to synthesize payloads for tests. rvcsi_nx_decode_chanspec — d11ac chanspec -> channel (chanspec & 0xff) / bandwidth (bits [13:11], cross-checked against the FFT size) / band (bits [15:14], cross-checked against the channel number). Still allocation-free, bounds-checked, structured errors, never panics. - ffi.rs wraps it: decode_chanspec / parse_nexmon_udp_header / decode_nexmon_udp / encode_nexmon_udp + DecodedChanspec / NexmonCsiHeader; every unsafe block documented; the ABI guard now expects 1.1. rvcsi-adapter-nexmon: - pcap.rs — a dependency-free classic-libpcap reader (all four byte-order / timestamp-resolution magics; Ethernet / raw-IPv4 / Linux-SLL link types; tolerates a truncated final record; pcapng is a follow-up) + extract_udp_payload + a synthetic_udp_pcap / synthetic_nexmon_pcap test/example generator. - NexmonPcapAdapter (a CsiSource) — reads the CSI UDP packets out of a `tcpdump -i wlan0 dst port 5500 -w csi.pcap` capture, decodes each via the C shim, stamps the frame timestamp from the pcap packet time; non-CSI packets counted as "skipped" in health. rvcsi-runtime: decode_nexmon_pcap, summarize_nexmon_pcap (+ NexmonPcapSummary: link type, CSI frame count, channels, bandwidths, subcarrier counts, chip versions, RSSI range, time span), CaptureRuntime::open_nexmon_pcap[_bytes]. rvcsi-node (napi-rs): nexmonDecodePcap, inspectNexmonPcap, decodeChanspec, RvcsiRuntime.openNexmonPcap. @ruv/rvcsi SDK + .d.ts updated (NexmonPcapSummary, DecodedChanspec). rvcsi-cli: `record --source nexmon-pcap`, `inspect-nexmon`, `decode-chanspec`. 161 rvcsi tests pass (adapter-nexmon 9->22), 0 failures, clippy-clean. ADR-096 §2.2/§2.3/§5, CHANGELOG, CLAUDE.md updated. https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z |
||
|
|
684a064816 |
docs(rvcsi): update CHANGELOG, CLAUDE.md crate table, README docs index
- CHANGELOG: expand the rvCSI entry to cover all 9 crates (incl. rvcsi-runtime and the @ruv/rvcsi npm SDK), the napi-c / napi-rs seams, and the 142-test / clippy-clean status; note the daemon + MCP server are follow-ups. - CLAUDE.md: add the 9 `rvcsi-*` crates to the Key Rust Crates table. - README: add an rvCSI row to the docs index; bump the ADR count (79→96) and DDD-model count (7→8). https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z |
||
|
|
7393cc2b73 |
feat(rvcsi): rvcsi-runtime composition + rvcsi-node (napi-rs) + rvcsi-cli + @ruv/rvcsi TS SDK
- rvcsi-runtime — the composition layer (no FFI): CaptureRuntime (CsiSource + validate_frame + SignalPipeline + EventPipeline, with next_validated_frame / next_clean_frame / drain_events / health) plus one-shot helpers (summarize_capture → CaptureSummary, decode_nexmon_records, events_from_capture, export_capture_to_rf_memory, rf_memory_self_check). 10 tests. - rvcsi-node — the napi-rs seam (cdylib+rlib, build.rs runs napi_build::setup): thin #[napi] wrappers over rvcsi-runtime — rvcsiVersion / nexmonShimAbiVersion / nexmonDecodeRecords / inspectCaptureFile / eventsFromCaptureFile / exportCaptureToRfMemory + an RvcsiRuntime streaming class. Everything that crosses the boundary is a validated/normalized rvCSI struct serialized to JSON (D6). deny(clippy::all). - @ruv/rvcsi npm package (package.json + index.js + index.d.ts + README + __test__/api.test.cjs) — curated JS surface that JSON-parses the addon's output into plain CsiFrame/CsiWindow/CsiEvent/SourceHealth/CaptureSummary objects; lazy native-addon load with a helpful "not built" error. - rvcsi-cli — the `rvcsi` binary: record (Nexmon dump → .rvcsi, validating), inspect, replay, stream, events, health, calibrate (v0 baseline), export ruvector. 7 tests exercising every subcommand against in-memory captures. - rvcsi-cli no longer depends on rvcsi-node (a binary can't link the napi addon); the shared logic moved to rvcsi-runtime. .gitignore: ignore the generated *.node / binding.js / binding.d.ts / npm/ under rvcsi-node. All rvcsi crates: build together OK, clippy-clean, 140 unit/integration tests + 2 doctests, 0 failures (core 29, dsp 28, events 18, adapter-file 20+1, adapter-nexmon 9, ruvector 20+1, runtime 10, cli 7). https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z |
||
|
|
6432dfbd2d |
feat(rvcsi): rvcsi-adapter-file (.rvcsi capture/replay) + rvcsi-ruvector (RF memory)
- rvcsi-adapter-file (ADR-095 FR1/FR10, D9): the `.rvcsi` JSONL capture format (CaptureHeader line + one CsiFrame per line), FileRecorder, FileReplayAdapter (a CsiSource — deterministic replay, preserves timestamps/ordering/validation verbatim, carries an unenforced replay_speed for the daemon/CLI), read_all(). 20 unit tests + 1 doctest. - rvcsi-ruvector (ADR-095 FR8, D8) — standin for the production RuVector binding: deterministic embeddings (window_embedding = 32 resampled mean_amplitude bins + 32 resampled phase_variance bins + [motion_energy, presence_score, quality_score, ln1p(frame_count)], L2-normalized, dim 68; event_embedding = 10-wide kind one-hot + confidence + ln1p(evidence count), dim 12), cosine_similarity, the RfMemoryStore trait + value objects (EmbeddingId/RecordKind/SimilarHit/ DriftReport), and InMemoryRfMemory + JsonlRfMemory (file-backed append log, identical query semantics, latest-baseline-per-room-wins on reopen). 20 unit tests + 1 doctest. All rvcsi crates build and test together: core 29, dsp 28, events 18, adapter-file 20(+1), adapter-nexmon 9, ruvector 20(+1) — 124 unit + 2 doc tests, 0 failures. forbid(unsafe_code) everywhere except rvcsi-adapter-nexmon (FFI). https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z |
||
|
|
46f701bca8 |
feat(rvcsi): rvcsi-events — window aggregation + event detectors (ADR-095 FR5)
- WindowBuffer: buffers exposable CsiFrames from one (session,source), emits a CsiWindow on a frame-count or duration threshold; computes per-subcarrier mean_amplitude / phase_variance and scalar motion_energy / presence_score / quality_score; skips mixed source/session and mismatched-subcarrier frames. - EventDetector trait + 4 state machines: PresenceDetector (hysteresis on presence_score), MotionDetector (debounced rising/falling edges on motion_energy), QualityDetector (SignalQualityDropped + once-per-stretch CalibrationRequired), BaselineDriftDetector (EWMA baseline → BaselineChanged / AnomalyDetected). Each with new()/with_config() + a public config struct. - EventPipeline: owns a WindowBuffer + Vec<Box<dyn EventDetector>> + IdGenerator; process_frame / flush / add_detector / recent_windows (32-window ring) / with_defaults. - 18 tests (incl. a 150-frame quiet/active/quiet end-to-end run via a seeded LCG + a determinism check). clippy-clean, forbid(unsafe_code), no heavy deps. https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z |
||
|
|
94745242a8 |
feat(rvcsi): rvcsi-dsp (DSP stages + SignalPipeline) + ADR-096 (FFI/crate layout)
- rvcsi-dsp — reusable signal-processing stages (ADR-095 FR4): mean/variance/ std_dev/median, remove_dc_offset, unwrap_phase, moving_average, ewma, hampel_filter(_count), short_window_variance, subtract_baseline + DspError; scalar features motion_energy(_series), presence_score (logistic, ≈0.5 at threshold), confidence_score, breathing_band_estimate (heuristic, FFT-free); SignalPipeline (hampel → smooth → DC-remove → baseline-subtract → unwrap, non-destructive of validation state) + learn_baseline. 28 tests, clippy-clean, forbid(unsafe_code), no heavy deps. - docs/adr/ADR-096-rvcsi-ffi-crate-layout.md — the implementation ADR: 8-crate topology, the napi-c shim record format + contract, the napi-rs Node surface, build/test invariants, alternatives. Indexed in docs/adr/README.md. - CHANGELOG: rvCSI entry updated to cover the implementation crates. https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z |
||
|
|
1e684cb208 |
feat(rvcsi): rvcsi-core + napi-c Nexmon shim + crate skeletons (ADR-095/096)
First implementation milestone for the rvCSI edge RF sensing runtime:
- rvcsi-core — the foundation: CsiFrame/CsiWindow/CsiEvent normalized schema,
ValidationStatus, AdapterProfile, CsiSource plugin trait, id newtypes +
IdGenerator, RvcsiError, and the validate_frame pipeline (length/finiteness/
subcarrier/RSSI/monotonicity hard checks + multiplicative quality scoring →
Accepted/Degraded/Recovered/Rejected). 29 unit tests, forbid(unsafe_code).
- rvcsi-adapter-nexmon — the napi-c boundary: native/rvcsi_nexmon_shim.{c,h}
(the only C in the runtime, allocation-free, bounds-checked, parses/writes a
byte-defined "rvCSI Nexmon record" — a normalized superset of the nexmon_csi
UDP payload), compiled via build.rs + cc, wrapped by a documented ffi module
and a NexmonAdapter implementing CsiSource. 9 tests round-tripping through C.
- Workspace registration in v2/Cargo.toml (8 new members + napi/cc workspace
deps) and compiling skeletons for rvcsi-dsp, rvcsi-events, rvcsi-adapter-file,
rvcsi-ruvector, rvcsi-node (napi-rs cdylib + build.rs napi_build::setup) and
rvcsi-cli (`rvcsi` binary) — to be filled in by the implementation swarm.
cargo build -p rvcsi-core -p rvcsi-adapter-nexmon -p rvcsi-node -p rvcsi-cli: OK
cargo test -p rvcsi-core -p rvcsi-adapter-nexmon: 38 passed, 0 failed
https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z
|
||
|
|
d98b7e3f65 |
docs: rvCSI edge RF sensing platform — PRD, ADR-095, DDD domain model
Adds design documentation for rvCSI, a Rust-first / TypeScript-accessible / hardware-abstracted edge RF sensing runtime that normalizes WiFi CSI from Nexmon, ESP32, Intel, Atheros, file and replay sources into one validated CsiFrame schema, runs reusable DSP, emits typed confidence-scored events, and bridges to RuVector RF memory, an MCP tool server and a TS SDK. - docs/prd/rvcsi-platform-prd.md — purpose, users, success criteria, FR1-FR10, NFRs (safety/perf/reliability/privacy/security/portability), system architecture, runtime components, reference layout, data model - docs/adr/ADR-095-rvcsi-edge-rf-sensing-platform.md — the 15 architectural decisions (Rust core, C-at-the-boundary, TS SDK via napi-rs, normalized schema, validate-before-FFI, CSI-as-temporal-delta, RuVector as RF memory, replayability, detection != decision, local-first, read-first/write-gated MCP, mandatory quality scoring, versioned calibration, plugin adapters) - docs/ddd/rvcsi-domain-model.md — 7 bounded contexts (Capture, Validation, Signal, Calibration, Event, Memory, Agent) with aggregates, invariants, context map, data model and domain services - indexed in docs/adr/README.md and docs/ddd/README.md; CHANGELOG entry Design-only; no code or crates added yet. https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z |
||
|
|
6f77b37f5e |
chore(release): wifi-densepose-train 0.3.0 -> 0.3.1
Publishing the additive changes from PRs #536/#537 to crates.io: - `signal_features` module — wires `wifi-densepose-signal` into the pipeline (audit #1/#2) - `TrainingConfig::for_subcarriers` / `ht40_192()` / `multiband_168()` presets + the real `MmFiDataset` loader integration test (audit #4/#6/#7) No public API removals or changes — additive only, so 0.3.0 -> 0.3.1 is semver-correct. No other workspace crate depends on `wifi-densepose-train`, so this is a standalone bump. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
c604ca1150 |
feat(train): TrainingConfig subcarrier-layout presets + real MmFiDataset loader test (#537)
Closes the remaining doable items from the 2026-05-11 training-pipeline audit: #6 (CSI format default = 56-sc / 1 NIC) + #7 (multi-band 168-sc mesh not in config): new `TrainingConfig::for_subcarriers(native, target)` plus named presets `mmfi()` (114→56), `ht40_192()` (≈192-sc ESP32 HT40 → 56) and `multiband_168()` (168-sc ADR-078 multi-band mesh → 56). Non-MM-Fi CSI shapes are now first-class instead of requiring manual `native_subcarriers` / `num_subcarriers` overrides; the field docs list the supported source counts and the multi-NIC mapping (a 2–3-node mesh currently rides on `n_rx` until a dedicated node dimension lands). Model input width stays `num_subcarriers`; the presets only vary the resampling input. #4 (proof.rs uses synthetic data): reframed — a deterministic proof *must* use a reproducible source, so `verify-training` correctly stays on `SyntheticCsiDataset`. The real gap was that nothing exercised the on-disk `MmFiDataset` path. New `tests/test_real_loader.rs` writes synthetic CSI to `.npy` files in the `MmFiDataset::discover` layout, loads it back, and checks the resulting `CsiSample` — covering the no-interp case, the subcarrier-interpolation branch, and the empty-root case. Adds `ndarray` / `ndarray-npy` as dev-deps for the fixture writing. cargo check + cargo test -p wifi-densepose-train --no-default-features: clean, all existing tests green, 3 new loader tests + the updated config doctest pass. Purely additive — no model-shape change, no tch-module change. |
||
|
|
eaedfded6f |
fix(train): wire wifi-densepose-signal into the pipeline; correct MODEL_CARD env-sensor claim (#536)
Addresses three findings from the 2026-05-11 training-pipeline audit: #1/#2 — `wifi-densepose-signal` was a phantom dependency of `wifi-densepose-train` (listed in Cargo.toml, never imported), and vitals/CSI signal features were absent from the pipeline. New module `wifi_densepose_train::signal_features`: `extract_signal_features(&Array4<f32>, &Array4<f32>) -> Array1<f32>` (and the convenience method `CsiSample::signal_features()`) runs a windowed observation's centre frame through `wifi_densepose_signal::features::FeatureExtractor`, producing a fixed-length (FEATURE_LEN=12) amplitude / phase-coherence / PSD feature vector — the hook for a future vitals / multi-task supervision head (breathing- and heart-rate-band power are read off the PSD summary). The vector is produced on demand and is not yet fed back into the loss; wiring it as a training target is the documented follow-up. `wifi-densepose-signal` is now an actually-used dependency. 5 new tests (2 unit in signal_features.rs, 3 integration in tests/test_dataset.rs); existing wifi-densepose-train tests unchanged and green. #3 — `docs/huggingface/MODEL_CARD.md` presented PIR/BME280 environmental-sensor weak-label fine-tuning as a current capability; there is no env-sensor ingestion in the training pipeline. Marked that path as planned/not-implemented in the training-steps list and the data-provenance section. (#5 — README's "92.9% PCK@20" overclaim — fixed separately in PR #535.) CHANGELOG updated. |
||
|
|
bd4f81749a |
fix(docs): correct unsubstantiated 92.9% PCK@20 camera-supervised claim (#535)
The README claimed "92.9% PCK@20" for camera-supervised pose training. That
figure appears nowhere in ADR-079 (the source ADR) and is ~2.6x the ADR's own
success target (">35% PCK@20"). ADR-079 phases P7 (data collection), P8
(training + evaluation on real paired data) and P9 (cross-room LoRA) are all
still `Pending`, so no measured camera-supervised PCK@20 has been published.
- README: replace the two "92.9% PCK@20" claims with the proxy-supervised
baseline (~2.5%) and the ADR-079 target (35%+), noting the eval phases are
pending.
- CHANGELOG: add an Unreleased entry.
Surfaced by the PowerPlatePulse training-pipeline audit (2026-05-11). Six other
audit findings (vitals features absent from training; wifi-densepose-signal
ghost dep; PIR/BME280 in MODEL_CARD unimplemented; proof.rs uses
SyntheticCsiDataset only; 56-subcarrier/1-NIC default; multi-band 168-subcarrier
mesh not in training config) are listed in the PR body for follow-up.
|
||
|
|
df9d3b0eea |
fix(plugins): move marketplace manifest to repo root for /plugin marketplace add ruvnet/RuView
Claude Code looks for `.claude-plugin/marketplace.json` at the cloned repo's ROOT — not in a subdirectory — so `/plugin marketplace add ruvnet/RuView` (and `claude plugin marketplace add ruvnet/RuView`) was failing with "Marketplace file not found". - Move `plugins/.claude-plugin/marketplace.json` → `.claude-plugin/marketplace.json` (repo root); the `ruview` plugin's `source` is now `./plugins/ruview`. - README.md / plugins/ruview/README.md: install instructions now use `/plugin marketplace add ruvnet/RuView` + `/plugin install ruview@ruview` (with `claude --plugin-dir ./plugins/ruview` as the no-install fallback); manifest path references updated. - plugins/ruview/scripts/smoke.sh: resolve the manifest at the repo root; also assert the plugin `source` is `./plugins/ruview`. - ADR-0001 updated (scope, directory contract, smoke contract, consequences). Verified: `claude plugin validate .` + `./plugins/ruview` pass; smoke 13/13; `claude plugin marketplace add ./` → `claude plugin install ruview@ruview` → `claude plugin details ruview` works end-to-end (16 skill-entries + 3 agents). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
298543913e |
docs(readme): add Claude Code / Codex plugin + marketplace install instructions
New "🧩 Claude Code & Codex Plugin" section in README.md covering `claude --plugin-dir`, `claude plugin marketplace add` / `install`, the seven /ruview-* commands, the Codex prompt mirror, and the smoke check; plus a Documentation-table row linking to plugins/ruview/README.md. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
8ff7c2c35a |
feat(plugins): RuView Claude Code + Codex marketplace plugin
Add `plugins/ruview` — an end-to-end toolkit for working with RuView (WiFi-DensePose) from Claude Code, mirrored as Codex prompts. Marketplace: `plugins/.claude-plugin/marketplace.json` (one plugin, `ruview`). Skills (9): ruview-quickstart, ruview-hardware-setup, ruview-configure, ruview-applications, ruview-model-training, ruview-advanced-sensing, ruview-cli-api, ruview-mmwave, ruview-verify — shell-first (cargo / python / idf.py / docker / node), no claude-flow MCP dependency. Commands (7): /ruview-start, /ruview-flash, /ruview-provision, /ruview-app, /ruview-train, /ruview-advanced, /ruview-verify. Agents (3): ruview-onboarding-guide, ruview-config-engineer, ruview-training-engineer. Codex mirror: codex/AGENTS.md + codex/README.md + codex/prompts/*.md (full command parity, enforced by scripts/smoke.sh). Docs: docs/adrs/0001-ruview-plugin-contract.md (Proposed). Verification: scripts/smoke.sh (13 structural checks). Provisioning docs reflect the full `provision.py` flag set (TDM mesh, edge tiers, vitals, hop channels, Cognitum Seed, swarm intervals) and the issue #391 NVS-namespace-replace gotcha. Verified: `claude plugin validate` (plugin + marketplace), loads via `claude --plugin-dir`, smoke 13/13, and confirmed against an attached ESP32-S3 on COM8 running the RuView CSI firmware (live adaptive_ctrl + csi_collector serial output). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
19ee207d51 |
Merge pull request #528 from ruvnet/fix/update-submodules-workflow
ci: fix "Update vendor submodules" workflow (git identity + drop --merge) |
||
|
|
8aa7fb9e9f |
ci: fix "Update vendor submodules" workflow (identity + drop --merge)
The scheduled job has been failing on every run with:
fatal: empty ident name (...) not allowed
fatal: Unable to merge '...' in submodule path 'vendor/ruvector'
Two bugs:
1. `git config user.name/email` was only set inside the "Create PR" step,
but `git submodule update --remote --merge` runs first and the merge
inside vendor/ruvector needs a committer when the pinned commit isn't a
fast-forward of upstream `main` → "Committer identity unknown".
2. `--merge` is the wrong operation here. We only want to bump the
superproject's gitlink to the latest upstream commit on each submodule's
tracked branch — there's no reason to create merge commits inside the
vendored repos, and `--merge` breaks whenever the current pin has diverged.
Fix:
- Add a "Configure git identity" step before any commit-creating operation.
- Replace `git submodule update --remote --merge` with
`git submodule sync --recursive && git submodule update --remote --recursive`
(detached checkout at each `.gitmodules` branch tip).
- Log the pointer diff in the "Check for changes" step for reviewability.
- Tidy the PR-creation step (identity now set globally; clearer commit/PR text).
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
f2e3a6a392 |
Merge pull request #526 from ruvnet/fix/esp32-issues-505-517-521
fix: ESP32 CSI 0pps (#521), aggregator sibling magics (#517), version.txt (#505) + fix-marker CI guard |
||
|
|
eda45a6857 |
ci: fix-marker regression guard (witness-style)
Adds a fast per-PR gate that asserts previously-shipped fixes are still present in the tree — the CI analogue of the ruflo witness fix-marker system, but self-contained (no plugin dependency, reviewable as plain JSON). Complements the heavier checks (firmware build, deterministic pipeline proof, release witness bundle) by catching the silent-revert class of regression that build+test wouldn't. - scripts/fix-markers.json manifest: 11 markers (RuView#396, #521, #517, #505, #354, #263, #266/#321, #265, #232/#375/#385/#386/#390, ADR-028 proof + witness bundle). Each has files / require (literal substring or /regex/) / optional forbid / rationale / ref. - scripts/check_fix_markers.py stdlib-only checker. Exit 0 clean / 1 regression / 2 bad manifest. Modes: --list, --json, --only ID. - .github/workflows/fix-regression-guard.yml runs on PR + push to main/master; gates on the checker and writes the result table into the run summary + an artifact. If a fix is intentionally removed, update scripts/fix-markers.json in the same PR with a rationale — the diff becomes the audit trail. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
a1cb6bd8e5 |
fix(firmware): bump version.txt to 0.6.4 + CI guard for tag/version match (#505)
version.txt on main was still 0.6.2. CMake reads PROJECT_VER from it, so
esp_app_get_description()->version (and the boot log line) reported 0.6.2
for any source build — and v0.6.3-esp32 shipped a release binary that
internally identified as 0.6.2 because the bump never landed on main.
- version.txt: 0.6.2 -> 0.6.4 (matches the latest release tag)
- firmware-ci.yml: new `version-guard` job that runs on v*-esp32 tag
pushes and fails the run if the tag's X.Y.Z != version.txt, so a
future release can't ship a mislabeled binary.
Closes #505
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
4d0521ca08 |
fix(hardware): aggregator tolerates sibling RuView UDP packet magics (#517)
The ESP32 firmware multiplexes several wire packet types onto the same
UDP port as ADR-018 raw CSI frames (magic 0xC5110001):
0xC5110002 ADR-039 edge vitals (32 B)
0xC5110003 ADR-069 feature vector
0xC5110004 ADR-063 fused vitals
0xC5110005 ADR-039 compressed CSI
0xC5110006 ADR-081 feature state
0xC5110007 ADR-095/#513 temporal classification
Esp32CsiParser only knew 0xC5110001, so the standalone `aggregator`
binary printed "parse error: Invalid magic: expected 0xc5110001, got
0xc5110002" for every vitals packet. No CSI data was lost — just noise.
Add the sibling-magic constants + ruview_sibling_packet_name(), classify
recognized siblings before the CSI-frame length gate, and return a new
ParseError::NonCsiPacket { magic, kind } instead of InvalidMagic. The
`aggregator` CLI now skips them quietly (logs "[skipped ADR-039 edge
vitals packet — not a CSI frame]" only with --verbose); the library-level
CsiAggregator already dropped them silently. New regression tests cover
all seven magics.
Closes #517
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
3f55c95b34 |
fix(esp32): disable WiFi modem sleep so CSI capture isn't starved (#521)
csi_collector_init() never called esp_wifi_set_ps(), leaving the radio on the ESP-IDF STA default WIFI_PS_MIN_MODEM. The modem then sleeps between DTIM beacons; combined with the MGMT-only promiscuous filter (#396) the CSI callback is starved and the per-second yield collapses toward 0 pps, which is what users on a clean multi-node setup were seeing (motion=0.00 presence=0.00 yield=0pps). Force WIFI_PS_NONE before enabling promiscuous mode — the textbook requirement for reliable CSI capture (every ESP-IDF CSI example does it). New boot line: "csi_collector: WiFi modem sleep disabled (WIFI_PS_NONE) for CSI capture". Battery duty-cycling is unaffected: power_mgmt_init() runs after this and re-enables modem sleep when provision.py is given --duty-cycle <100. Builds clean for esp32s3 (idf.py build, 48% flash free). Closes #521 Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
e7904786f0 |
Update README.md
Added Spatial Intelligence to readme, since that seems to be a common description |
||
|
|
9a078e4ac8 |
fix(pointcloud): exponential backoff on unreachable backend + status banner
When ?backend=<url> pointed at a server that wasn't running (e.g. user forgot to start ruview-pointcloud serve before clicking Connect ESP32), the viewer was retrying 10 Hz forever — flooding the console with ERR_CONNECTION_REFUSED and offering no guidance about what was wrong. Two fixes: 1. Replace setInterval(fetchCloud, 100) with self-rescheduling setTimeout. On success: 250 ms steady cadence. On failure for an explicit backend: 250 ms → 500 → 1 s → 2 s → 4 s → 8 s → 16 s → capped at 30 s. Resets to 250 ms the moment the backend comes back. Auto mode (Pages with no backend) still disables network entirely after the first 404. Strict-live mode (?live=1) also backs off so it doesn't spam. 2. Show an actionable status banner in the info panel when the chosen backend is unreachable: the URL, the actual error string, the next retry time, and the exact `cargo run` command to start the server. Visitor sees the diagnosis instead of staring at a 'demo' badge wondering why their ESP32 feed isn't visible. The scene keeps animating (face mesh / synthetic) while the viewer waits, so the tab never goes blank. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
0e39faac73 |
feat(pointcloud): overlay browser face mesh on top of ESP32 backend feed
Lets the visitor enable their browser webcam face mesh in addition to (not instead of) a connected ESP32 backend. Both render in the same Three.js scene — the live ESP32-driven splats from /api/splats plus the visitor's own face as a 478-vertex MediaPipe point cloud. Use cases: - Local development: see your face overlaid on the camera+CSI fusion output to debug coordinate-frame alignment. - Demos: show 'this is the room as ESP32 sees it, and this is me as MediaPipe sees me' side-by-side in one scene. Implementation: - Extract pushFaceSplats(splats) — pushes the 478 face vertices plus ~8000 edge-interpolated samples into the array, with no Foundation context. Reused by faceMeshFrame (demo path) and handleData (overlay path) so there is one source of truth for face-splat geometry. - handleData now appends pushFaceSplats output to data.splats when the source is not 'face-mesh' AND the user has clicked the camera CTA. Sets data._faceOverlay so the badge can show '+ face overlay'. - Camera CTA is no longer hidden in remote/live modes — it relabels to '▶ Add face overlay' so the affordance is clear. Strict-live mode (?live=1) still hides it because the offline panel takes over. - Splat count in the info panel reflects the rendered total (backend + overlay) when the overlay is active. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
ad41a89960 |
feat(pointcloud): integrate ESP32 CSI as optional data stream from hosted viewer
The hosted GitHub Pages viewer can now act as a thin client for a locally-running ruview-pointcloud serve instance — flip a button, the ESP32's CSI fusion (camera depth + WiFi CSI + mmWave) renders inside the same Three.js scene that previously only showed the face mesh demo. No clone, no rebuild, no toolchain on the visitor's side. Server (stream.rs): - Add tower_http::cors::CorsLayer with a deliberate allowlist: https://ruvnet.github.io, http://localhost:*, http://127.0.0.1:*, and 'null' (for file:// origins). Anything else is denied — not a wildcard CORS. Modern browsers (Chrome 94+, Firefox 116+, Safari 16.4+) treat 127.0.0.1 as a "potentially trustworthy" origin so HTTPS Pages → HTTP loopback is permitted. The new layer wraps the existing /api/cloud, /api/splats, /api/status, /health routes. - Cargo.toml: pull in workspace tower-http (cors feature already on). Viewer: - New "📡 Connect ESP32…" CTA bottom-right. Clicking prompts for a ruview-pointcloud serve URL (default http://127.0.0.1:9880), persists the last-used value in localStorage, and reloads with ?backend=<url> so the existing remote-mode fetch path takes over. When already connected the button toggles to "disconnect" and reloads back to the demo. - Reuses the existing transport selector — no new code path to maintain. The face mesh / synthetic demo render path is unaffected; this is purely an additive UI affordance over the ?backend= query. Docs: - ADR-094 §2.3 expanded with the local-ESP32 workflow and the CORS posture rationale. - Workflow README documents ?backend=http://127.0.0.1:9880 as the intended local-ESP32 path. Tests: cargo test -p wifi-densepose-pointcloud → 15/15 passed. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
e3021c777c |
chore(pointcloud): inline amber-dot favicon to silence /favicon.ico 404
Browsers auto-request /favicon.ico when none is declared in <head>. On a static GitHub Pages host that's a guaranteed 404 in the console. Inline a 32x32 SVG amber dot via data: URL so the browser is satisfied without an extra network round-trip. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
b4c2f7d20b |
fix(pointcloud): stop polling /api/splats on Pages after first 404
When the viewer is hosted on a static origin (GitHub Pages, S3) it has no backend at /api/splats. The default ?backend=auto path was issuing a fetch every 100 ms, getting a 404, falling back to the demo, and flooding the console with one 404 per tick. Cosmetic on the surface but real network/CPU waste over time. After the first 404 in auto mode, set networkDisabled=true and skip fetch on subsequent ticks — the interval still fires but goes straight to pickDemoFrame() so the face mesh / synthetic render path keeps animating. Remote (?backend=<url>) and live (?live=1) modes keep retrying so a transient outage doesn't permanently downgrade them. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
aea9892aed |
Revert "feat(pointcloud): Hollywood face fx — webcam texture, wireframe, scan line"
This reverts commit
|
||
|
|
347ad4bb11 |
feat(pointcloud): Hollywood face fx — webcam texture, wireframe, scan line
Adds optional cinematic effects to the face-mesh demo, all toggleable via a new ?fx= URL param. Default is 'all' (texture + mesh + scan + halo). Lightweight modes available: ?fx=clean (texture only) or ?fx=points (original solid amber). - Texture: per-frame webcam → hidden 2D canvas → getImageData lookup at each landmark (and each interpolated edge sample). Splats now carry the visitor's actual skin tone, not solid amber. Sampling is mirrored on x to match the selfie convention used by the face mesh vertex placement. All on-device — no frames leave the browser. - Mesh: persistent THREE.LineSegments overlay drawn from FACEMESH_TESSELATION (~1300 edges). Translucent (opacity 0.35), amber, additive blending, depthWrite off — gives a holographic wireframe wrapping the point cloud. Geometry is updated in place each frame; only positions get re-uploaded. - Scan: vertical bright slab sweeps top→bottom every 4 seconds, amplifying splat color up to 2.6× when within ±0.08 world units of the line. Westworld-style scanning. - Halo: existing 60-particle ring around the face is now opt-in via FX_HALO. Cleaner default for the texture-mesh combination. Info panel surfaces active fx list in face-mesh mode. Synthetic fallback hides the wireframe overlay so it doesn't render against an empty figure. Workflow README updated with the new ?fx= options. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
5d7fccce79 |
feat(pointcloud): fix upside-down face, densify mesh, add Foundation aesthetic
Three fixes in one pass to address visitor feedback:
1. Face was rendering upside down — MediaPipe's lm.y is image-down (0=top
of frame, 1=bottom) and the existing updateSplats() already does a
y-negate to convert to Three.js Y-up. Pre-flipping in lmToCenter was a
double flip. Use lm.y directly so the renderer's single flip lands the
head at the top of the screen.
2. Density and fidelity — interpolate 6 splats per FACEMESH_TESSELATION
edge (~1300 edges → ~8000 face splats vs 478 vertex-only). Amplify
lm.z mapping (×8 vs ×4) so eye sockets, nose, and chin show real 3D
depth. Smaller splat scale (0.006 surface, 0.010 vertices) for finer
point appearance.
3. Foundation-inspired aesthetic — the demo now renders the subject
(face mesh OR procedural fallback) inside a Hari Seldon time-vault:
* Holographic surveyor grid in amber, breathing brightness pattern.
* Slow-rotating two-arm galactic spiral receding behind the subject
(~640 stars, warm core to cool edges, Trantor-evocation).
* 800-star deterministic distant starfield on a spherical shell
(fixed LCG seed so visitors don't see noise flicker).
* 60-particle holographic halo orbiting the subject plane.
Shared pushFoundationContext() drives both face-mesh and synthetic
paths. Synthetic procedural figure densified 4x (240 vs 60 points)
and re-oriented (head→top, feet→bottom) so the y-down convention is
internally consistent.
Camera pulled back to (0, 0.2, -3.5) to frame the galactic context.
Poll cadence 4 Hz → 10 Hz so the spiral animates smoothly. Info panel
gets a Seldon quote and "Seldon Vault" branding. CTA copy reframed to
"Project Subject — render your face into the Vault".
ADR-094 already documents the dual-transport intent; the aesthetic
choices here are content, not architecture, so no ADR update needed.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
cbedbce9e3 |
feat(pointcloud): use MediaPipe Face Mesh for the live demo (ADR-094)
The previous synthetic procedural demo did not represent what the local fusion pipeline produces — a real depth-backprojected point cloud of the user's face and surroundings. This commit ports the closest browser equivalent: MediaPipe Face Mesh runs in-browser at ~30 fps and emits 478 3D landmarks per frame. Each visitor now sees the outline of their own face rendered as a point cloud, with a small floor + back wall for spatial context. - Adds MediaPipe Face Mesh + Camera Utils via jsdelivr CDN. - Adds an "▶ Enable camera" CTA so getUserMedia is gated on a user gesture (required by some browsers and good UX regardless). - New face-mesh frame generator uses the same splat shape as the live /api/splats payload, so a single render path drives both modes. - Mirrors x to match selfie convention; maps lm.z (relative depth) to the world-coord range used by the live pipeline. - Falls back automatically to the procedural floor + walls + figure when the camera is denied, dismissed, or unavailable. - Badge surfaces the new state: '● DEMO Your Face (MediaPipe)'. - Bumps poll cadence to 4 Hz so face mesh updates feel live. - ADR-094 updated to reflect the new default behavior. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
7343bdc4dd |
docs(readme): retarget Live 3D Point Cloud link to hosted demo
Now that ADR-094 is deployed, point the README's demo link at https://ruvnet.github.io/RuView/pointcloud/ instead of the docs/readme-details.md anchor. Matches the pattern of the sibling Observatory and Pose Fusion demo links. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
21b2b3352f |
feat(pointcloud): GitHub Pages demo with optional live backend (ADR-094) (#495)
Publishes the live 3D point cloud viewer to gh-pages/pointcloud/ so it
can be linked from the README alongside the Observatory and Dual-Modal
Pose Fusion demos. The viewer auto-selects its transport from URL
parameters:
- default / ?backend=auto — try /api/splats, fall back to synthetic demo
- ?backend=demo — synthetic in-browser only, no network
- ?backend=<url> — fetch from a CORS-permitting host running
ruview-pointcloud serve
- ?live=1 — strict mode, show offline panel instead of demo fallback
The synthetic frame matches the live API JSON shape (splats, count,
frame, live, pipeline.{skeleton,vitals}) so a single render path drives
both modes. New workflow uses keep_files: true to preserve the existing
observatory/, pose-fusion/, and nvsim/ deployments on gh-pages.
See docs/adr/ADR-094-pointcloud-github-pages-deployment.md for the full
decision record and 6 acceptance gates.
|
||
|
|
e11d569a39 |
docs(readme): split details to docs/readme-details.md and reorganize
- Move Latest Additions, Key Features, and everything from Installation through Changelog (1855 lines) into docs/readme-details.md. - Keep README focused on overview, capability table, How It Works, Use Cases, Documentation, License, and Support. - Add per-row emojis to the top capability table. - Add 3D point cloud row noting optional camera + WiFi CSI + mmWave fusion with link to the live viewer demo. - Move Documentation table closer to the bottom (just above License). - Collapse Edge Intelligence (ADR-041) into a <details> block matching the sibling Use Case sections. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
36e70bf229 |
security: pin GitHub Actions to SHAs and bump vulnerable npm deps (#442)
* security: pin GitHub Actions to SHAs and bump vulnerable npm deps (#442) Addresses confirmed findings from issue #442 (Pentesterra/DevGuard). GitHub Actions — pin all third-party Action references in security-scan.yml and ci.yml to verified commit SHAs (with the matching version in a trailing comment for legibility): * snyk/actions/python -> v1.0.0 * aquasecurity/trivy-action -> v0.36.0 (security-scan.yml + ci.yml) * bridgecrewio/checkov-action -> v12.1347.0 * tenable/terrascan-action -> v1.4.1 * checkmarx/kics-github-action -> v2.1.20 (the action #442 named) * trufflesecurity/trufflehog -> v3.95.2 Verification: grep -rE 'uses:.*@(main|master|latest)$' .github/workflows/ returns no matches. npm deps in ui/mobile — add `overrides` forcing patched versions of the three packages flagged by the DevGuard scanner, regenerate package-lock.json: * @xmldom/xmldom@0.8.11 -> 0.8.13 * node-forge@1.3.3 -> ^1.4.0 (closes 3 HIGH advisories) * picomatch@2.3.1 -> ^2.3.2 (transitive in jest tooling) npm audit totals: 25 -> 22 advisories (5 HIGH -> 2 HIGH). Out of scope for this PR (tracked separately): * Sensing-server unauth REST API surface — opened as #443 pending design-intent confirmation from @ruvnet. * Bearer-token-shaped string in git history — confirmed test seed per repo owner; no rotation required. Refs: #442 Co-Authored-By: claude-flow <ruv@ruv.net> * chore: add Dependabot config for github-actions and ui/mobile npm (#442) Pairs with the SHA pinning from the previous commit so the pinned versions get automated weekly bumps rather than drifting back to mutable refs over time. Scoped to the two ecosystems #442 surfaced findings in: * github-actions (root) — the supply-chain risk * npm (ui/mobile) — the @xmldom/xmldom, node-forge, picomatch advisories Other ecosystems (pip, cargo, desktop UI npm) deliberately omitted — they can be added in a separate PR if desired. Refs: #442 Co-Authored-By: claude-flow <ruv@ruv.net> * chore(dependabot): expand to pip, cargo, and desktop UI npm (#442) Broadens the Dependabot config from the initial 2 ecosystems (github-actions + ui/mobile npm) to cover all 5 package surfaces in the repo so pinned dependencies stay current across the board: + npm /v2/crates/wifi-densepose-desktop/ui (vite advisory live) + pip / (requirements.txt loose pins) + cargo /v2 (no cargo audit in CI yet) Marginal cost is zero — Dependabot only opens PRs when an upstream bump exists, and per-ecosystem pull-request limits cap the noise. Each ecosystem labelled distinctly so PRs route cleanly. Refs: #442 Co-Authored-By: claude-flow <ruv@ruv.net> --------- Co-authored-by: claude-flow <ruv@ruv.net> |
||
|
|
f06d0c6ab5 |
fix(firmware): SPI cache crash fix + node_id/filter_mac defensive copies + esptool v5 (rebased #397)
* fix(firmware): move defensive node_id capture before wifi_init_sta()
The original defensive copy in csi_collector_init() (line 172 of main.c)
runs AFTER wifi_init_sta() (line 147), which on some ESP32-S3 devices
corrupts g_nvs_config.node_id back to the Kconfig default of 1.
Reproduced on device 80:b5:4e:c1:be:b8 (ESP32-S3 QFN56 rev v0.2):
- NVS provisioned with node_id=5
- Release firmware (no fix): seed receives node_id=1 (clobbered)
- This patch: seed receives node_id=5 (correct)
Changes:
- Add csi_collector_set_node_id() called from main.c immediately
after nvs_config_load(), before wifi_init_sta() runs
- csi_collector_init() now detects and logs the clobber if early
capture disagrees with current g_nvs_config value
- Fallback path preserved: if set_node_id() is never called,
init() still captures from g_nvs_config (backwards compatible)
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(firmware): defensive copy of filter_mac to prevent callback crash
The CSI callback reads g_nvs_config.filter_mac_set and filter_mac on
every invocation (100-500 Hz). If wifi_init_sta() corrupts g_nvs_config
(same root cause as the node_id clobber), the callback reads garbage
from the struct, leading to Core 0 LoadProhibited panic after ~2400
callbacks (~70 seconds of operation).
Extends the early-capture pattern from the node_id fix to also copy
filter_mac_set and filter_mac into module-local statics before WiFi
init runs. Adds canary logging to detect filter_mac corruption.
Observed on device 80:b5:4e:c1:be:b8 via serial:
CSI cb #2400 → Guru Meditation Error: Core 0 panic'ed (LoadProhibited)
→ TG0WDT_SYS_RST → reboot → crash again at ~2900 callbacks
Refs #232 #375 #385 #386 #390
Co-Authored-By: Ruflo & AQE
* fix(firmware): MGMT-only promiscuous filter to prevent SPI cache crash
The WiFi driver's wDev_ProcessFiq interrupt handler crashes with
LoadProhibited in cache_ll_l1_resume_icache when promiscuous mode
captures MGMT+DATA frames (100-500 interrupts/sec). The high interrupt
rate races with SPI flash cache operations, corrupting cache state.
Changes:
- Promiscuous filter: MGMT+DATA → MGMT-only (~10 Hz beacons)
- CSI config: disable htltf_en and stbc_htltf2_en (LLTF-only)
LLTF provides 64 subcarriers (HT20) — sufficient for presence,
breathing, and fall detection. The 10 Hz beacon rate eliminates
the SPI flash cache contention that caused the crash.
Verified on device 80:b5:4e:c1:be:b8:
- Before: LoadProhibited crash at ~1600-2400 callbacks (every ~70s)
- After: 2700+ callbacks over 4.7 minutes, zero crashes
Backtrace decode confirmed crash in ESP-IDF closed-source WiFi blob:
_xt_lowint1 → wDev_ProcessFiq → spi_flash_restore_cache
→ cache_ll_l1_resume_icache → EXCVADDR=0x00000004 (NULL deref)
Co-Authored-By: Ruflo & AQE
* fix(provision): write-flash → write_flash for esptool v5 compat
esptool v5+ rejects hyphenated subcommands. The provision script
used 'write-flash' which fails with "invalid choice". Changed to
'write_flash' (underscore) which works with both old and new esptool.
Co-Authored-By: Ruflo & AQE
* fix(firmware): 50 Hz callback rate gate + sdkconfig extra IRAM opt
- Add early rate gate in wifi_csi_callback at 50 Hz (defense-in-depth,
does not prevent crash alone but reduces callback execution time)
- Add null-data injection timer infrastructure (disabled — TX adds
interrupt pressure that triggers the SPI cache crash, RuView#396)
- sdkconfig.defaults: add CONFIG_ESP_WIFI_EXTRA_IRAM_OPT=y
- sdkconfig.defaults: document SPIRAM XIP attempt (crashes differently)
Co-Authored-By: Ruflo & AQE
* fix(firmware): address PR #397 review feedback
Applies @ruvnet's five review requests on PR #397 (RuView#397 comment
4289417527):
1. **Inline comment on `provision.py` `write_flash`** — ESP-IDF v5.4
bundles esptool 4.10.0 (underscore-only). #391's hyphen swap broke
the documented venv flow; kept the underscore form and added a
three-line comment warning future maintainers not to "re-fix" it.
2. **Correct `edge_processing.c` sample_rate** (blocking) — changed
hard-coded `20.0f` → `10.0f` at line 718 so
`estimate_bpm_zero_crossing()` matches the MGMT-only CSI rate.
Without this, breathing and heart-rate reports were 2× the true
value. Added a comment tying the constant to the callback rate gate.
3. **Removed disabled probe-injection infrastructure** — dropped the
forward declaration, the `CSI_PROBE_INTERVAL_MS` define, six static
variables (`s_probe_timer`, `s_probe_tx_count`, `s_probe_tx_fail`,
`s_ap_bssid`, `s_ap_bssid_known`), and three functions
(`csi_send_probe_request`, `probe_timer_cb`,
`csi_collector_start_probe_timer`). None were reachable.
`csi_inject_ndp_frame()` reverted to the original ADR-029 stub.
Can be revived from this commit's parent if needed.
4. **Cleaned `sdkconfig.defaults`** — removed the SPIRAM prose and
commented-out `# CONFIG_SPIRAM is not set` line. Kept only the live
`CONFIG_ESP_WIFI_EXTRA_IRAM_OPT=y` with a concise rationale.
5. **Bumped firmware version 0.6.1 → 0.6.2** and added four
`[Unreleased]` CHANGELOG entries covering the SPI cache crash fix,
the `filter_mac` / `node_id` clobber defense, the sample-rate
correction, and the `write_flash` command-form revert.
Net: +39 / -128 across six files.
Validation in this devcontainer:
- Static sanity on modified C files: braces balance (csi_collector.c
59/59; edge_processing.c 96/96), zero dangling references to removed
probe-injection symbols.
- Rust workspace tests and Python proof not executed here — cargo not
installed and pip blocked by PEP 668. Deferring hardware build +
flash + miniterm verification to @ruvnet's COM7 per his offer in
the review comment.
Co-Authored-By: claude-flow <ruv@ruv.net>
---------
Co-authored-by: Dragan Spiridonov <spiridonovdragan@gmail.com>
v0.6.3-esp32
|
||
|
|
b123879b25 |
fix(dashboard): settings drawer scrim covers viewport (host transform fix)
* fix(ci): wasm-pack PATH + Dockerfile workspace stub Closes the two post-merge failures from #436: 1. wasm-pack: command not found — cargo install doesn't reliably leave the binary on PATH. Switched to the canonical installer in both the Pages and a11y workflows. 2. nvsim-server Docker build — cargo couldn't resolve workspace.dependencies from a partial copy. Dockerfile now generates a stub workspace Cargo.toml inline that lists just nvsim + nvsim-server. Co-Authored-By: claude-flow <ruv@ruv.net> * fix(dashboard): settings drawer scrim — escape host transform's containing-block trap The drawer's :host had transform: translateX(...) which makes it the containing block for any fixed-position descendants. The .scrim at 'position: fixed; inset: 0' therefore covered only the drawer's own 420 px panel area, not the viewport. Visible symptoms: - Page behind the drawer didn't dim - Click outside the drawer didn't dismiss it (no scrim to receive) - Felt like the drawer wasn't really 'modal' Fix: keep :host as a fixed full-viewport overlay (no transform), move the drawer body into an inner .panel div, transform only that. Now the scrim covers the viewport correctly and outside-clicks dismiss. Same trap exists nowhere else; nv-modal already follows this pattern. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
f02d9f0617 |
fix(ci): wasm-pack PATH + Dockerfile workspace stub (#440)
Closes the two post-merge failures from #436: 1. wasm-pack: command not found — cargo install doesn't reliably leave the binary on PATH. Switched to the canonical installer in both the Pages and a11y workflows. 2. nvsim-server Docker build — cargo couldn't resolve workspace.dependencies from a partial copy. Dockerfile now generates a stub workspace Cargo.toml inline that lists just nvsim + nvsim-server. |
||
|
|
7f5a692632 |
feat(nvsim): full simulator stack — Rust crate, dashboard, server, App Store, Ghost Murmur [ADR-089/090/091/092/093]
Squashed merge of feat/nvsim-pipeline-simulator (29 commits). ## Shipped - ADR-089 nvsim crate (Accepted) — 50/50 tests, ~4.5 M samples/s, pinned witness cc8de9b01b0ff5bd… - ADR-092 dashboard implementation (Implemented) — 8/12 §11 gates ✅, 4/12 ⚠ (external infra) - ADR-093 dashboard gap analysis (Implemented) — 21/21 catalogued gaps closed - Plus ADR-090 (proposed conditional) and ADR-091 (proposed research-only) ## Live deploy https://ruvnet.github.io/RuView/nvsim/ ## Infra - nvsim-server Dockerfile + GHCR publish workflow (.github/workflows/nvsim-server-docker.yml) - axe-core + Playwright cross-browser CI (.github/workflows/dashboard-a11y.yml) - gh-pages auto-deploy workflow already in place (preserves observatory + pose-fusion siblings) Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
905b680747 |
docs(adr): ADR-084 — promote Proposed → Accepted
All five implementation passes plus four security-review hardenings shipped in PR #435 (squash-merged as |
||
|
|
d71ef9aefa |
docs(adr): ADR-086 — edge novelty gate (proposed) (#434)
Pushes the ADR-084 novelty sensor down into the ESP32 sensor MCU's Layer 4 (On-device Feature Extraction) of ADR-081's 5-layer kernel: sketch + 32-slot ring bank in IRAM, suppress UDP send when novelty < CONFIG_RV_EDGE_NOVELTY_THRESHOLD (default 0.05). Wire format bumps to magic 0xC5110007 with two new fields (suppressed_since_last: u16, gate_version: u8) packed in by narrowing the existing 16-bit quality_flags to 8-bit (only 8 bits were ever defined). Frame size stays at 60 bytes; v6 receivers fall back gracefully. Stuck-gate self-heal at CONFIG_RV_EDGE_MAX_CONSEC_SUPPRESS (default 50 frames ≈ 10 s) so a wedged threshold can't silently disappear a node. Default-off Kconfig so existing deployments are unaffected. Validation commitments: - ≤ 200 µs sketch insert+score on Xtensa LX7 - ≥ 30% UDP TX-energy reduction in steady-state quiet rooms - ≤ 5 pp drop on cluster-Pi novelty top-K coverage vs unsuppressed - ≥ 50% bandwidth reduction in stable-room scenarios Six-pass implementation plan, default-off Kconfig, QEMU + COM7 hardware-in-loop validation. Honest gaps flagged: Xtensa LX7 POPCNT absence is conjecture (Pass 2 bench is the falsifier); interaction with ADR-082's Tentative→Active gate is the likeliest weak point (Open Q4). ADR-087 / ADR-088 reserved as pointer stubs at end: - ADR-087: Pass-4 mesh-exchange scope (cluster↔cluster vs sensor→Pi) - ADR-088: Firmware-release coordination policy Status: Proposed. SOTA review by goal-planner agent. |
||
|
|
17509a2a41 |
feat(ruvector,signal,sensing-server): ADR-084 Passes 1/1.5/2/3 — RaBitQ similarity sensor implementation (#435)
* feat(ruvector): ADR-084 Pass 1 — sketch module foundation
Implements Pass 1 of ADR-084 (RaBitQ similarity sensor): a thin
RuView-flavored API over `ruvector_core::quantization::BinaryQuantized`,
exposed at `wifi_densepose_ruvector::{Sketch, SketchBank, SketchError}`.
API surface:
- `Sketch::from_embedding(&[f32], sketch_version: u16)` — sign-quantize
a dense embedding into a 1-bit-per-dim packed sketch.
- `Sketch::distance` — hamming distance with schema-mismatch error.
- `Sketch::distance_unchecked` — hot-path variant for sketches already
validated as same-schema.
- `SketchBank::insert/topk/novelty` — bank with caller-assigned u32 IDs,
schema locked at first insert, novelty = min_distance / embedding_dim.
Schema versioning (`sketch_version: u16` + `embedding_dim: u16`) prevents
silent comparisons across embedding-model generations. Bumping the model
forces re-sketch of the candidate bank.
Pass 1 establishes the API and unit-test foundation. Acceptance criteria
(8x-30x compare-cost reduction, 90% top-K coverage, <1pp accuracy regression)
are measured per-site in Passes 2-5.
Validated:
- 12 new tests pass (sketch construction, hamming, top-K ordering,
schema lock, schema rejection, novelty)
- cargo test --workspace --no-default-features → 1,551 passed, 0 failed,
8 ignored (was 1,539 before; +12 new tests)
- ESP32-S3 on COM7 still streaming live CSI (cb #117300)
Co-Authored-By: claude-flow <ruv@ruv.net>
* bench(ruvector): ADR-084 acceptance — sketch-vs-float compare cost
Adds sketch_bench measuring the first ADR-084 acceptance criterion
(8x-30x compare cost reduction) at three dimensions and a realistic
top-K@k=8 over 1024 sketches.
Measured (Windows host, criterion --warm-up 1s --measurement 3s):
compare_d512:
float_l2: 197.03 ns/op
float_cosine: 231.17 ns/op
sketch_hamming: 4.56 ns/op → 43-51x speedup
topk_d128_n1024_k8:
float_l2_topk: 47.59 us
sketch_hamming: 6.34 us → 7.5x speedup
Pair-wise compare exceeds the 8-30x acceptance criterion by an order
of magnitude. Top-K is at 7.5x — close to the threshold; the sort
dominates at this bank size, which is a Pass 1.5 optimization
opportunity (partial-sort heap for small K).
Co-Authored-By: claude-flow <ruv@ruv.net>
* perf(ruvector): ADR-084 Pass 1.5 — partial-sort heap in SketchBank::topk
Replace `sort_by_key + truncate` (O(n log n)) with a fixed-size max-heap
(O(n log k)) for top-K queries when n > k. Fast path when n ≤ k stays
on the simple sort.
Bench at d=128, n=1024, k=8 (Windows host, criterion 3s measurement):
Before (sort + truncate): 6.34 µs/op
After (heap): 3.83 µs/op -39.4% / +1.65× faster
Combined with the 32× memory shrink and 47.6 µs → 3.83 µs total path
saving:
topk_d128_n1024_k8 vs float_l2_topk:
Pass 1 sort_by_key: 47.59 µs / 6.34 µs = 7.5× speedup
Pass 1.5 heap: 47.59 µs / 3.83 µs = 12.4× speedup
Now over the ADR-084 acceptance criterion of 8× minimum. Heap pays off
strictly more at larger n; benchmark at n=4096 is a Pass-2 follow-up.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(signal): ADR-084 Pass 2 — sketch-prefilter for EmbeddingHistory::search
Adds `EmbeddingHistory::with_sketch(...)` and `search_prefilter(query, k,
prefilter_factor)`. The prefilter sketches the query, hamming-ranks the
parallel sketch array to take the top `k * prefilter_factor` candidates,
then refines those with exact cosine and returns the top-K.
`EmbeddingHistory::new(...)` is unchanged — sketches are opt-in via the
new constructor. `search_prefilter` falls back to brute-force `search`
when sketches are disabled, so callers never see incorrect results.
ADR-084 acceptance criterion empirically validated:
Synthetic 128-d AETHER-shape, n=256, 16 queries:
k=8, prefilter_factor=4 → 78.9% top-K coverage (FAIL <90%)
k=8, prefilter_factor=8 → ≥90% top-K coverage (PASS)
k=16, prefilter_factor=8 → ≥90% top-K coverage (PASS)
The factor=4 default that I'd planned in Pass 1 falls below the 90% bar
on uniform-random synthetic data. Production callers should use **8**
unless their embeddings carry enough structure (real AETHER traces
likely will) to clear the bar at lower factors. Documented in the
search_prefilter docstring and asserted in
test_search_prefilter_topk_coverage_meets_adr_084.
FIFO eviction now drains the parallel sketches array in lockstep —
test_search_prefilter_evicts_sketches_on_fifo guards against the two
arrays drifting (which would silently corrupt top-K via index
mismatch).
Validated:
- cargo test --workspace --no-default-features → 1,554 passed,
0 failed, 8 ignored (was 1,551; +3 new prefilter tests)
- ESP32-S3 on COM7 still streaming live CSI (cb #3200)
Co-Authored-By: claude-flow <ruv@ruv.net>
* bench(signal): ADR-084 Pass 2 — end-to-end search_prefilter speedup
Measures EmbeddingHistory::search_prefilter (sketch + cosine refine)
vs the brute-force EmbeddingHistory::search baseline at three realistic
AETHER bank sizes, with the empirically validated prefilter_factor=8.
Measured (Windows host, criterion --warm-up 1s --measurement 3s):
d=128, k=8:
n=256 brute_force_cosine = 31.98 us, prefilter = 13.78 us → 2.3x
n=1024 brute_force_cosine = 110.4 us, prefilter = 16.64 us → 6.6x
n=4096 brute_force_cosine = 507.4 us, prefilter = 66.37 us → 7.6x
Speedup grows with bank size (sketch overhead is fixed; brute-force
scales linearly with n). At n=4k the prefilter approaches the 8x
ADR-084 acceptance criterion; at n=10k+ (realistic multi-day
deployment banks) it crosses cleanly. Below n=512 the brute-force
path is already cheap (sub-50 us) so the prefilter's narrower wins
don't materially affect the hot path.
Coverage acceptance (≥90% top-K agreement) is exercised in the
unit-test suite, not the bench. The bench measures cost only.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(signal): ADR-084 Pass 3 — EmbeddingHistory::novelty primitive
Adds the cluster-Pi novelty-sensor primitive: `EmbeddingHistory::novelty(query)`
returns `Option<f32>` in [0.0, 1.0] where 0.0 = exact-match-in-bank
and 1.0 = no-overlap. Returns None when sketches are disabled so
callers can fall back gracefully (existing `EmbeddingHistory::new`
constructor stays sketch-disabled).
This is the building block of the cluster-Pi novelty gate
described in ADR-084 §"cluster-Pi novelty sensor": each sensor node
maintains a bank of recent feature vectors, the gate scores the
incoming frame's novelty against the bank, and the heavy CNN /
pose-model wake gate consumes the score.
Wiring novelty into sensing-server's NodeState happens in a
follow-up — that's a ~50-line surgical change touching main.rs that
deserves its own commit. This patch lands the primitive + tests so
the wiring is straightforward.
Three regression tests added:
- test_novelty_returns_none_without_sketches
(graceful fallback when bank is sketch-less)
- test_novelty_zero_for_exact_match_one_for_empty_bank
(semantic boundaries)
- test_novelty_decreases_as_bank_grows_around_query
(gradient direction — guards against reversed comparator)
Validated:
- cargo test --workspace --no-default-features → 1,557 passed,
0 failed, 8 ignored (was 1,554; +3 new novelty tests)
- ESP32-S3 on COM7 still streaming live CSI (cb #7600)
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(sensing-server): ADR-084 Pass 3 — wire novelty into NodeState
Wires the EmbeddingHistory::novelty primitive (Pass 3 prior commit)
into the per-node frame ingestion path on the cluster Pi. Each
incoming CSI frame now updates a per-node sketch bank of the last
6.4 s of feature vectors and produces a novelty score in [0.0, 1.0]
that downstream model-wake gates can consume.
Two NodeState structs were touched (one in types.rs and a
refactoring-leftover duplicate in main.rs that the call site uses);
both gain feature_history + last_novelty_score fields and an
update_novelty helper that:
- truncates / zero-pads incoming amplitudes to NOVELTY_VECTOR_DIM (56)
- scores novelty *before* inserting (so a frame doesn't see itself)
- FIFO-evicts when the bank reaches NOVELTY_HISTORY_CAPACITY (64)
Wired at the per-node ESP32 frame path in main.rs:3772 (immediately
before frame_history.push_back). Existing call sites that operate on
the singleton SensingState (not per-node) intentionally untouched —
they will be wired in a follow-up alongside the WebSocket update
envelope's novelty_score field.
Two new unit tests in novelty_tests:
- first_frame_yields_max_novelty_then_zero_on_repeat
(semantic boundaries: empty bank = 1.0, exact repeat = 0.0)
- handles_short_and_long_amplitude_vectors
(truncate / zero-pad robustness across hardware variants)
Validated:
- cargo test --workspace --no-default-features → 1,559 passed,
0 failed, 8 ignored (was 1,557; +2 new novelty tests)
- ESP32-S3 on COM7 still streaming live CSI (cb #3900)
Co-Authored-By: claude-flow <ruv@ruv.net>
* hardening(ruvector): L2 from PR #435 review — overflow on >u16::MAX dims
Pass 1.6 hardening, addressing L2 finding from the security review on
PR #435 (https://github.com/ruvnet/RuView/pull/435#issuecomment-4321285519):
The original `Sketch::from_embedding` used `debug_assert!` for the
`embedding.len() <= u16::MAX` invariant, which compiled out in release
builds. A caller passing a 65,536+ -dim embedding would silently
truncate the dimension count via `as u16` cast — two over-long inputs
would then compare as same-dimensional rather than as 64k vs 70k, and
the dimension confusion would not surface anywhere.
Two-part fix:
- `from_embedding` (infallible) now SATURATES `embedding_dim` to
`u16::MAX` rather than truncating. Two over-long inputs still get
packed bit-correctly by `BinaryQuantized` and the saturated dim is
consistent across both, so they compare predictably (just with an
upper-bounded distance).
- `try_from_embedding` (new, fallible) returns
`Err(SketchError::EmbeddingDimOverflow{got, max})` when the input
exceeds `u16::MAX`. Use this when an over-long input should fail
loudly rather than be silently saturated.
- New error variant `SketchError::EmbeddingDimOverflow` with the
observed `got` and the `max` (`u16::MAX as usize`).
- New regression test `try_from_embedding_rejects_over_long_input`
asserts both paths: try_ → Err, infallible → saturate.
Validated:
- 13 sketch unit tests pass (was 12; +1 for L2 boundary).
- cargo test --workspace --no-default-features → 1,560 passed,
0 failed, 8 ignored (was 1,559; +1).
- ESP32-S3 on COM7 streaming live CSI (cb #100, fresh boot RSSI -48 dBm).
Co-Authored-By: claude-flow <ruv@ruv.net>
* hardening(ruvector,signal): L1+L3 from PR #435 review
Two follow-ups to the security review on PR #435:
L1 — Defensive `if let Some(...)` for SketchBank::topk heap peek.
The original `.expect("heap len == k > 0")` was mathematically
unreachable (k > 0 enforced at function entry, heap.len() >= k branch
guards), but a structural pattern makes the impossibility a type
property rather than a runtime invariant. Same hot-path cost; zero
panic risk in the production binary.
L3 — Guard `embedding_dim == 0` in `EmbeddingHistory::novelty`.
A 0-dim history is constructible via `with_sketch(0, ...)`; without
the guard the function returned `NaN` (min_d as f32 / 0.0), silently
poisoning every downstream gate (model-wake, anomaly-emit, etc).
Now returns Some(1.0) — fail-loud at "no comparison possible →
maximally novel," never NaN. New regression test
`test_novelty_zero_dim_history_returns_one_not_nan` pins it down.
Validated:
- cargo test --workspace --no-default-features → 1,561 passed,
0 failed, 8 ignored (was 1,560; +1 for the L3 NaN guard test).
- ESP32-S3 on COM7 streaming live CSI (cb #12400, RSSI fresh).
L4 (f64→f32 cast) is documentation-only and lands in a follow-up
patch; L8 (always-on novelty sensor) is an observation, not a fix.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(sensing-server): ADR-084 Pass 3.5 — novelty_score on PerNodeFeatureInfo
Adds an optional `novelty_score: Option<f32>` field to
PerNodeFeatureInfo, the per-node WebSocket envelope shape. Mirrored
on both struct definitions (types.rs canonical + main.rs's
refactoring-leftover duplicate) so the schema is consistent.
`#[serde(skip_serializing_if = "Option::is_none")]` keeps existing
WebSocket consumers unaffected — old clients see no extra field
unless the server populates it. No PerNodeFeatureInfo literal
construction sites exist today (all `node_features: None`), so this
is a schema-only addition; live population from
`NodeState::last_novelty_score` lands in a Pass 3.6 follow-up that
also wires `node_features: Some(...)` at the per-node ESP32 frame
emit path.
Validated:
- cargo test --workspace --no-default-features → 1,561 passed,
0 failed, 8 ignored (no change; schema-only).
- ESP32-S3 on COM7 streaming live CSI (cb #2100, fresh boot).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(sensing-server): ADR-084 Pass 3.6 — populate node_features with novelty_score
Wires `node_features: Some(...)` at the two per-node ESP32 frame
emit sites (formerly `node_features: None`). Adds a `build_node_features`
helper that constructs `Vec<PerNodeFeatureInfo>` from `s.node_states`,
including the per-node `last_novelty_score`.
This completes the Pass 3.x track — novelty score now flows from
NodeState → PerNodeFeatureInfo → SensingUpdate envelope → WebSocket
clients. Cluster-Pi UI / model-wake / anomaly-emit gates can read
it without round-tripping back to the server.
Three other call sites (singleton paths at 1772, 1911, 4170) keep
`node_features: None` for now — those are for the offline /
simulated paths that don't have per-node ESP32 state. They'll get
populated when their parent flows wire up real multi-node fanout.
Stale flag uses `ESP32_OFFLINE_TIMEOUT` (5s) — same threshold the
rest of the system uses to decide a node has dropped.
Validated:
- cargo test --workspace --no-default-features → 1,561 passed,
0 failed, 8 ignored (no change; integration test would be wire-
format diff in a follow-up).
- ESP32-S3 on COM7 streaming live CSI (cb #100, fresh boot,
RSSI -49 dBm).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(ruvector): ADR-084 Pass 4 — WireSketch wire-format primitive
Adds `WireSketch::serialize` / `deserialize` for transmitting a
sketch + novelty score over any byte-stream channel — cluster↔cluster
mesh (ADR-066 swarm bridge when it exists), sensor→cluster-Pi UDP
(ADR-086 edge gate complement), gateway→cloud QUIC. Channel-agnostic
by design.
Wire layout (12-byte header + ceil(dim/8) bytes payload, little-endian):
[0..4] magic = 0xC5110084
[4..6] format_version = 1
[6..8] sketch_version (embedding-model schema)
[8..10] embedding_dim
[10..12] novelty_q15 (novelty * 32_767, saturated)
[12..] packed sketch bits
A 128-d AETHER sketch fits in exactly 28 bytes (12 header + 16 bits).
Deserializer is paranoid by design — every untrusted byte buffer
gets validated against:
- length floor (>= header bytes)
- length ceiling (WIRE_SKETCH_MAX_BYTES = 9 KiB; defends against
memory-exhaustion attacks via claimed-but-impossible large dims)
- magic match
- format_version supported
- embedding_dim → payload bytes consistency
A malformed UDP packet from a non-RuView sender produces a typed
`WireSketchError` (variant per failure class), never a panic.
Re-exported from lib.rs alongside `Sketch` / `SketchBank`.
Seven new tests:
- wire_serialize_round_trip (correctness)
- wire_rejects_short_buffer (length floor)
- wire_rejects_oversized_buffer (length ceiling, DoS guard)
- wire_rejects_bad_magic (cross-protocol confusion guard)
- wire_rejects_unsupported_format_version (forward-compat)
- wire_rejects_payload_size_mismatch (header/body consistency)
- wire_envelope_size_for_aether_128d (sizing contract: 28 bytes)
Validated:
- cargo test --workspace --no-default-features → 1,568 passed,
0 failed, 8 ignored (was 1,561; +7 wire-format tests).
- ESP32-S3 on COM7 streaming live CSI (cb #15100, RSSI -48 dBm).
Pass 4's wire-format primitive ships first; the channel that
carries it (ADR-066 swarm-bridge or ADR-086 sensor→Pi gate) is
out-of-scope for this commit and tracked separately.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(ruvector): ADR-084 Pass 5 — privacy-preserving event log + L4 docstring
Pass 5 — `PrivacyEventLog` and `NoveltyEvent` types in a new
`wifi_densepose_ruvector::event_log` module. Each event stores
`(timestamp, sketch_bytes, sketch_version, embedding_dim, novelty,
witness_sha256)` — explicitly NOT the raw float embedding. The
witness is SHA-256 of the WireSketch serialization (12-byte header +
packed bits + q15 novelty), making events content-addressable: two
pushes of the same `(sketch, novelty)` produce byte-identical
witnesses, enabling dedup at the receiver and verifier.
Privacy properties (ADR-084 §"Privacy-preserving event log"):
1. Non-invertibility — 1-bit sign quantization is lossy; an attacker
with read access cannot reconstruct the source CSI / embedding.
2. Content addressing — `(sketch_version, witness)` is fully qualified.
3. Bounded memory — fixed capacity ring; misbehaving senders cannot
exhaust receiver memory.
Seven new tests:
- push_grows_until_capacity_then_fifo_evicts
- zero_capacity_log_silently_drops_pushes (no-op stub case)
- witness_is_deterministic_for_same_sketch_and_novelty
(witness must NOT depend on timestamp)
- witness_differs_for_different_novelty_scores
- find_by_witness_returns_most_recent_match
- find_by_witness_returns_none_on_miss
- event_does_not_carry_raw_embedding (structural privacy guarantee)
L4 hardening (PR #435 security review) — the `f64 → f32` cast in
NodeState::update_novelty now has a docstring noting the boundary
behaviour: `f64::INFINITY` survives as `f32::INFINITY`, `f64::NAN`
propagates as `f32::NAN`. Neither panics. CSI amplitudes from healthy
firmware are well within f32 finite range.
Validated:
- cargo test --workspace --no-default-features → 1,575 passed,
0 failed, 8 ignored (was 1,568; +7 event-log tests).
- ESP32-S3 on COM7 streaming live CSI (cb #2800, RSSI -52 dBm).
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
d3020fec6b |
docs(adr): ADR-085 — RaBitQ pipeline expansion (proposed) (#433)
Extends ADR-084's RaBitQ-as-similarity-sensor pattern from five sites to twelve, adding seven additional pipeline locations the user identified during ADR-084 implementation: - Per-room adaptive classifier short-circuit (Mahalanobis prefilter) - Recording-search REST endpoint (GET /api/v1/recordings/similar) - WiFi BSSID fingerprinting (channel-hop scheduler input) - mmWave (LD2410 / MR60BHA2) signature wake-gate - Witness bundle drift detection (CI ratchet) - Agent / swarm memory routing (ADR-066 swarm bridge) - Log / event-pattern anomaly detection (cluster Pi) Each site has a 2-3 sentence decision (what gets sketched, what triggers the comparison, what the refinement does on miss) and a witness-hash artifact (what the system stores in place of the raw embedding/event/signal). Implementation plan ordered cheapest-first / least-risky-first. Acceptance criteria align with ADR-084 (8x-30x compare cost, ≥90% top-K coverage, <1pp accuracy regression) where applicable; non-vector sites (witness bundle, BSSID time-series, event log) have site-specific criteria. Three open questions explicitly flagged: 1. Mahalanobis-after-binary-sketch is novel — no published primary source found, marked conjecture, decision deferred to bench 2. Canonical "non-vector → sketchable" encoding is unsolved 3. MERIDIAN (ADR-027) cross-environment domain interaction needs site-by-site analysis before bank rebuild semantics are committed Status: Proposed. SOTA review by goal-planner agent. |
||
|
|
c19a33ee1c |
docs(adr): ADR-084 — RaBitQ similarity sensor for CSI/pose/memory (proposed) (#429)
Adopt RaBitQ-style binary sketches as a first-class cheap similarity sensor at four points in the RuView pipeline: AETHER re-ID hot-cache filter, per-room novelty / drift detection, mesh-exchange compression, and privacy-preserving event logs. Implementation home is ruvector-core::quantization::BinaryQuantized (already vendored, already SIMD-accelerated NEON+POPCNT, 32x compression, 1-bit sign quantization + hamming distance), re-exported through a thin RuView-flavored API in wifi-densepose-ruvector::sketch. Pattern at every site: dense embedding -> RaBitQ sketch -> hamming pre-filter to top-K -> full-precision refinement only on miss. Decision boundary unchanged; sketch is a sensor that gates *which* comparisons run, not *what* they decide. Acceptance test (per source proposal): - sketch compare cost reduction: 8x-30x vs full float - top-K candidate coverage: >= 90% agreement with full-float pass - end-to-end accuracy regression: < 1 percentage point Site-by-site rollback if any criterion fails at a given site; remaining sites continue. Five implementation passes, each independently testable: ruvector module wrap, AETHER re-ID pre-filter, cluster-Pi novelty sensor, mesh-exchange compression, privacy log. Sensor MCU unchanged; sketches happen at the cluster Pi (ADR-083). Validation requires acceptance numbers on >= 3 of 5 passes. Open question (out-of-scope until pass-1 benchmark): whether RuView embeddings need a Johnson-Lindenstrauss / RaBitQ-paper randomized rotation before sign-quantization, or whether pure 1-bit sign quantization (today's BinaryQuantized) is sufficient. |
||
|
|
259939b7ec |
docs(adr): ADR-083 — per-cluster Pi compute hop (proposed) (#428)
Adopt one Pi per cluster of 3-6 ESP32-S3 sensor nodes as the canonical fleet-shape, rather than the full three-tier (dual-MCU + per-node Pi) shape. Sensor nodes are unchanged from ADR-028 / ADR-081; the cluster Pi gains the responsibilities the ESP32-S3 cannot carry — pose-grade ML inference, QUIC backhaul to gateway/cloud, and a cluster-level OTA + secure-boot anchor. The cluster-Pi shape is the L3-hybrid path identified in docs/research/architecture/decision-tree.md §2 — the cheapest viable upgrade. The full three-tier shape remains the long-term exploration target, gated behind no_std CSI maturity (decision-tree L4) and per-node ISR-jitter evidence (L2). Status: Proposed. Acceptance gated on: 1. Cross-compile to aarch64 / armv7 with workspace tests passing 2. 3-sensor + 1-Pi field test demonstrating end-to-end CSI → fusion → cloud at <=100 ms cluster latency 3. Cluster-Pi SoC choice ADR (decision-tree L6) approved References: - docs/research/architecture/three-tier-rust-node.md (seed exploration) - docs/research/architecture/decision-tree.md (L3 hybrid path) - docs/research/sota/2026-Q2-rf-sensing-and-edge-rust.md (SOTA evidence) |
||
|
|
81cc241b9e |
chore(repo): move v1/ → archive/v1/ + add archive/README.md (#430)
The Rust port at v2/ has been the primary codebase since the rename in #427. The Python implementation at v1/ is no longer the active target; the only load-bearing path is the deterministic proof bundle at v1/data/proof/ (per ADR-011 / ADR-028 witness verification). Move the whole Python tree into archive/v1/ and document the policy in archive/README.md: no new features, bug fixes only when they affect a still-load-bearing path (currently just the proof), CI continues to verify the proof on every push and PR. Path references updated in 26 files via path-pattern sed (only matches v1/<known-child> patterns, never bare v1 or API URLs like /api/v1/). Two double-prefix typos (archive/archive/v1/) caught and hand-fixed in verify-pipeline.yml and ADR-011. Validated: - Python proof verify.py imports cleanly at archive/v1/data/proof/ (numpy/scipy still required; CI installs requirements-lock.txt from archive/v1/ now) - cargo test --workspace --no-default-features → 1,539 passed, 0 failed, 8 ignored (unaffected by Python tree relocation) - ESP32-S3 on COM7 untouched (no firmware paths changed) After-merge: contributors should re-run any local `python v1/...` commands as `python archive/v1/...` (CLAUDE.md and CHANGELOG already updated). |