mirror of
https://github.com/ruvnet/RuView
synced 2026-08-06 19:51:43 +00:00
b2f9c8d28dca6f03724925aba69cadcb6e69675d
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b2f9c8d28d |
chore: version-bump and republish 10 of 12 documented crates to crates.io
Bumped each crate to its next patch version above whatever was already
live on crates.io (several had drifted ahead of what this checkout's
Cargo.toml showed) and published:
wifi-densepose-core 0.3.2, -vitals 0.3.2, -wifiscan 0.3.2,
-hardware 0.3.2, -signal 0.3.6, -nn 0.3.2, -ruvector 0.3.3,
-train 0.3.3, -mat 0.3.2, -wasm 0.3.1 (workspace.package version).
wifi-densepose-signal's default `eigenvalue` feature pulls in
ndarray-linalg -> openblas-src, which needs vcpkg on native Windows;
verified+published with --no-default-features instead (the crate
already builds clean without it; nothing about the published tarball
changes based on the local verify build's feature flags).
wifi-densepose-sensing-server and wifi-densepose-cli were bumped
(0.3.5, 0.3.2) but NOT published: both path-depend on ruview-auth,
which is `publish = false` and not on crates.io, so `cargo publish`
correctly refuses them ("all dependencies must have a version
requirement"). Left as-is pending a decision on whether ruview-auth
should become publishable.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
1f05456588 |
feat(ADR-261 M2): multi-bit + large-N ANN scaling study — measured, no crossover (refutes M1 prediction) (#1066)
* feat(ADR-261): multi-bit (b∈{1,2,4}) quantized HNSW traversal + scaling harness
Generalize the SymphonyQG-style quantized-traversal HNSW from 1-bit Hamming to a
b-bit-per-dimension code (b ∈ {1,2,4}), mirroring ADR-156 §10's multi-bit RaBitQ
scheme (rotate via FHT Pass-2, uniform mid-rise scalar quantizer over [-3,3],
ranked by per-dim L1). b=1 is byte-for-byte the original construction (codes in
{0,1} ⇒ L1 == Hamming), pinned by one_bit_build_bits_matches_legacy_build.
Bytes/node scales linearly: 128-d → 16/32/64 B for b=1/2/4.
- hnsw_quantized.rs: QuantizedHnswIndex::build_bits(...,bits,...), bits()/
bytes_per_node() accessors, code-L1 greedy+beam traversal. build(...) kept as
the b=1 backward-compatible entry point. +4 tests (multi-bit recall regression,
bits clamp, bytes/node, legacy parity).
- ann_measure.rs: build_indices_bits / build_quant_bits / run_scaling_study +
best_float_op / best_quant_op; scaling_report (#[ignore], --release) and a
CI-safe scaling_study_small_is_consistent.
- ann_bench.rs: 2-bit and 4-bit quant criterion benches over the shared graph.
ruvector lib 151 → 156 passed, 0 failed, 1 ignored (scaling_report).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-261): record M2 multi-bit scaling study — measured, no crossover (refutes M1 prediction)
Multi-bit (b∈{1,2,4}) quantized HNSW traversal + N∈{10k,100k,250k} scaling study,
measured on this box. No crossover at any (N,b): at 10k more bits help (ratio
0.19→0.48×, b≥2 reaches 0.90 recall) but quant stays slower than float HNSW at
equal recall; at 100k/250k quant recall collapses (b=4: 1.0→0.788→0.624, never
≥0.90) while float holds ≥0.92. The predicted large-N crossover moved the wrong
way. Published negative with the mechanism explained. ADR-261 §11.
Co-Authored-By: claude-flow <ruv@ruv.net>
---------
Co-authored-by: ruv <ruvnet@gmail.com>
|
||
|
|
f756a8af49 |
feat(ADR-261): ruvector HNSW graph-ANN (25x measured vs linear) + honest SymphonyQG-direction refutation (#1063)
* feat(ruvector): real float HNSW + SymphonyQG-style quantized-traversal index (ADR-261) Adds the graph-ANN index the ruvector retrieval path was missing (ADR-156 §5 #1 noted there was no HNSW baseline to measure SymphonyQG against). - hnsw.rs: correct float HNSW (Malkov & Yashunin) — multi-layer NSW graph, ef_construction/ef_search, Algorithm-4 neighbour selection, seeded- deterministic level assignment (SplitMix64, reused from rotation.rs), L2 + cosine, brute-force ground truth, full degenerate-case guards. recall@10 correctness gate >=0.95 vs brute force (L2 + cosine). - hnsw_quantized.rs: SymphonyQG-style variant — same graph, traversal scored by cheap 1-bit Hamming over the RaBitQ Pass-2 rotated sign code, final exact-float rerank. - ann_measure.rs: shared deterministic planted-cluster fixture + recall/QPS measurement (ann_bench_report is the ADR source of truth). Fixes an index-out-of-bounds bug the recall gate caught: insert wired bidirectional edges before pushing the node's own link row. +20 tests, ruvector lib 131->151, 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> * bench(ruvector): criterion ann_bench for HNSW vs quantized vs linear (ADR-261) Times the same shared ann_measure fixture/indices through criterion so the bench and the report test can never measure different graphs. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(adr-261): graph-ANN index ADR with MEASURED HNSW vs quantized verdict ADR-261 (Accepted): float HNSW ~25x QPS over linear scan at recall >=0.99 (the baseline ADR-156 said was missing). Honest negative: the 1-bit quantized traversal is too coarse to beat float HNSW at equal recall at N=10k (best recall 0.738, no >=0.90 equal-recall point) — the SymphonyQG 3.5-17x is NOT reproduced by our 1-bit construction; expected crossover at large N + a multi-bit code. Caveat: our HNSW + our quant, not SymphonyQG's system — direction tested, not a 1:1 reproduction. ADR-156 §5 #1 + §8 backlog: CLAIMED -> MEASURED-direction-tested. CHANGELOG [Unreleased] entry. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
91248536bc |
feat(beyond-sota): ADR-156 M2 — RaBitQ unbiased distance estimator (rigorous published negative on strict-K) (#1056)
* feat(ruvector): RaBitQ unbiased distance estimator (ADR-156 M2) Implement the real Gao & Long (SIGMOD 2024) RaBitQ contribution on top of the existing Pass-2 rotation: an unbiased estimator of the inner product / squared distance recovered from the 1-bit code plus 8 B/vec per-vector side info (residual_norm + x_dot_o), used to rerank the candidate set instead of raw Hamming. - src/estimator.rs (new): EstimatorSketch, SideInfo, EstimatorQuery, DistanceEstimator (estimate_inner_product / estimate_sq_distance / ranking_key / cosine_ranking_key), EstimatorBank (topk_estimated[_cosine], with_centroid). Zero-centroid simplification documented; paper-faithful centroid path also built. - src/rotation.rs: extract apply_padded() (full padded FHT frame the code lives in); apply() now truncates apply_padded(). No behaviour change. - lib.rs: export estimator types. Additive + backward-compatible: Pass-1 Sketch / Pass-2 SketchBank / WireSketch wire format unchanged; all external callers use Pass-1 and are unaffected. Co-Authored-By: claude-flow <ruv@ruv.net> * test(ruvector): estimator strict-K coverage harness (ADR-156 M2) Add measure_estimator (cosine rerank) + measure_estimator_euclidean to the coverage harness, on the BIT-IDENTICAL fixture / cluster centres / query stream / cosine ground truth as measure_pass1/measure_pass2 — apples-to-apples sign-Hamming vs unbiased-estimator-rerank. Regression tests: - estimator_rerank_not_worse_than_sign (>= sign-only Pass-2 on a fixed fixture) - estimator_coverage_is_deterministic - estimator_coverage_report (--nocapture prints the strict-K table) MEASURED strict-K (candidate_k=K=8): Pass-1 36.13% -> Pass-2-sign 46.39% -> estimator-cosine 49.71%. Still short of the ADR-084 90% strict bar; estimator reaches 95.12% at candidate_k=24 (vs sign 91.60%). Published negative. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(ruvector): record RaBitQ estimator measured negative (ADR-156 §11, ADR-084) - sketch_bench: estimator cosine/euclid columns in the coverage table. - ADR-156 §11 (new): estimator formula + zero-centroid simplification stated honestly; strict-K coverage table; RESOLVED-NEGATIVE verdict (49.71% strict, short of 90%); pinning test names. §5 #2 + §10.5 updated. - ADR-084 'Pass 2b' (new): estimator landed + measured strict-K vs the bar. - CHANGELOG [Unreleased]: ADR-156 §11 Milestone-2 entry. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
9b07dff298 |
feat(beyond-sota): ADR-155 metric unification + ADR-156 RaBitQ Pass-2 (honest negative + latent topk bugfix) (#1053)
* refactor(train): hoist canonical PCK/OKS to un-gated metrics_core; fold test_metrics onto production (ADR-155 M1 §8) ADR-155 §8 deferred item: test_metrics.rs reference kernels validated production against their OWN reimplementation — a test that cannot catch a canonical-impl bug (both could be wrong the same way). - Extract canonical_torso_size / pck_canonical / oks_canonical / sigmas / bounding_box_diagonal into a new NON-tch-gated `metrics_core` module, so the single metric definition is reachable under `cargo test --no-default-features` (the `metrics` module is tch-gated). `metrics` re-exports every item → still exactly ONE implementation. - Rewrite tests/test_metrics.rs to assert the PRODUCTION pck_canonical / oks_canonical equal hand-computed fixtures (not a reimplementation): canonical_pck_matches_hand_computed_fixture (corr=3/total=4/pck=0.75), hip↔hip normalizer pin, zero-visible⇒0.0, OKS perfect⇒1.0, fake-Gold pin. - Keep an INDEPENDENT raw-threshold reference kernel only as a differential cross-check: test_kernel_agrees_with_canonical asserts it AGREES with canonical where torso==1.0 (genuine cross-check, not duplication). Grade: MEASURED. test_metrics 10→12 tests, 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> * fix(sensing-server): relabel divergent live PCK/OKS so they're never conflated with canonical (ADR-155 M1 §2.1/§8 Goal C) Goal C named training_api.rs:804 (torso-HEIGHT PCK). Auditing it surfaced TWO findings the ADR-155 §1 table missed: 1. training_api.rs is an ORPHAN file — not declared `mod` in lib.rs OR main.rs, so it does NOT compile into the crate. It does not drive the live server. 2. The REAL live `best_pck`/`best_oks` (main.rs training path → RVF metadata JSON read by model_manager.rs) come from trainer.rs: - `pck_at_threshold` = RAW-threshold PCK, NO torso normalization (the most divergent kind), printed/serialized as bare "PCK@0.2". - `oks_map` calls `oks_single(area=1.0)` = the EXACT fake-Gold pattern ADR-155 §2.1 claimed closed elsewhere — still live here, inflating best_oks. Resolution = RELABEL (torso/raw math is load-bearing on different data; the pub fns can't be renamed without breaking API; sensing-server has no train/ ndarray dep). Honest unify is a tracked §8 backlog item. - training_api.rs: `compute_pck` → `compute_pck_torso_height` + divergence doc; val_pck/best_pck/val_oks struct fields documented as torso-HEIGHT proxies; logs say `pck_torso_h@0.2`. Test torso_pck_is_labelled_distinctly_from_canonical. - trainer.rs (LIVE): `pck_at_threshold` documented raw-unnormalized; `oks_map` area=1.0 flagged fake-Gold; test pck_at_threshold_is_raw_unnormalized_not_canonical. - main.rs: live print relabelled `pck_raw@0.2` / `oks_map(area=1.0 proxy)`. No wire-format field renames (back-compat); no pub-API rename (no silent break). Grade: MEASURED (relabel + divergence pinned). sensing-server 450→451 lib tests, 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(adr-155): mark §8 metric items RESOLVED + audit map + honest §1 under-count correction (M1b Goals A/D) - §8.1: full PCK/OKS audit map (every def: file:line, basis, canonical/ legacy/distinct), the two §8 items marked RESOLVED with resolution+why. - Honest finding: §1's "seven divergent metrics" was an UNDER-count — sensing-server's LIVE trainer.rs has a raw-unnormalized PCK and an area=1.0 fake-Gold OKS the table omitted, and the file §8 named (training_api.rs) is orphaned dead code. §9 honest-limits updated. - Goal D: metrics.rs *_v2 variants confirmed caller-less + deprecated; noted for future cleanup, NOT deleted (public API, tch-gated). - CHANGELOG [Unreleased] Fixed entry. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(ruvector): RaBitQ Pass-2 randomized rotation + topk bugfix (ADR-156 §8) Implements the deferred "Multi-bit / Extended RaBitQ Pass 2" backlog item from ADR-156 §8: a deterministic randomized orthogonal rotation applied before sign-quantization, the published RaBitQ construction (Gao & Long, SIGMOD 2024). Rotation construction: Fast Hadamard Transform + seeded ±1 sign flips ("HD" / randomized Hadamard), O(d log d) time and O(d) memory — a dense d×d rotation is O(d²) and infeasible at the 65,535-d the wire format provisions for. Pads to the next power of two; SplitMix64 seeds the sign stream so index-time and query-time rotations are bit-identical. API is additive and backward-compatible: Pass 1 (`from_embedding`) is untouched; Pass 2 is opt-in via `Sketch::from_embedding_rotated` and `SketchBank::with_rotation` (+ `insert_embedding` / `topk_embedding` / `novelty_embedding` helpers that rotate consistently). Default behaviour is unchanged. While building the Pass-2 coverage harness, found and fixed a PRE-EXISTING correctness bug in `SketchBank::topk`: the n>k heap path used `BinaryHeap<Reverse<(d,id)>>` (a min-heap) but treated its peek as the max, so it returned the k FARTHEST sketches as "nearest". The shipped unit tests only exercised the n≤k fast path, so it went unnoticed. Fixed to a plain max-heap; pinned by `topk_heap_path_returns_nearest` and `tight_clusters_give_high_coverage_with_overfetch` (the latter measured 0.072 on the old code). New tests (+17, 100→117 in the crate): rotation determinism/norm-preservation (`rotation_is_deterministic_for_seed`, `rotation_preserves_norm`), Pass-2 shape-compatibility, `pass2_coverage_not_worse_than_pass1`, and a deterministic coverage report. MEASURED top-K coverage (anisotropic planted-cluster fixture, cosine ground truth; dim=128 N=2048 K=8 64 clusters noise=0.35 128 queries): candidate_k=K=8 : Pass1 36.13% -> Pass2 46.39% (both << 90% bar) candidate_k=24 : Pass1 83.89% -> Pass2 91.60% (Pass2 clears 90%) candidate_k=32 : Pass1/Pass2 100% Honest result: rotation consistently helps (+10pp at strict K), but neither pass clears the ADR-084 90% bar at candidate_k==K on this distribution. Pass 2 reaches 90% only with ~3x over-fetch (the ADR-084 "candidate set" deployment pattern). Multi-bit Pass 3 evaluated separately. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(ruvector): multi-bit Pass-3 experiment + ADR-156/084 measured results Adds the multi-bit half of the ADR-156 §8 "Multi-bit / Extended RaBitQ" item as a MEASURED experiment (coverage::measure_multibit): rotate, then b-bit uniform scalar-quantize each coord, rank by L1 over codes — the natural multi-bit generalization of hamming. Measures the bit/coverage tradeoff the backlog item asked for. MEASURED at the strict bar (candidate_k=K=8, anisotropic planted-cluster fixture, cosine ground truth): Pass1 (1-bit, no rot) 36.13% 16 B/vec Pass2 (1-bit, rot) 46.39% 16 B/vec Pass3 (rot, 2-bit) 54.39% 32 B/vec Pass3 (rot, 3-bit) 66.70% 48 B/vec Pass3 (rot, 4-bit) 74.22% 64 B/vec Honest: multi-bit monotonically helps but even 4-bit (4x memory) reaches only 74% at the strict bar — neither rotation nor <=4-bit multi-bit clears the strict-K 90% bar on this distribution. The bar is met via over-fetch (Pass2 @ candidate_k=24). Tests: multibit_tradeoff_report, multibit_1bit_matches_pass2_approx (+ sanity that 1-bit ~= Pass-2). Docs: - ADR-156 §8 item #2 marked RESOLVED-PARTIAL; §5 #2 grade CLAIMED -> MEASURED-on-our-hardware; new §10 with full measured tables, the topk bugfix disclosure, and graded deferred sub-items. - ADR-084: "Pass 2" section answering the rotation open-question with measured numbers + the topk bug note. - CHANGELOG [Unreleased]: Added (Pass-2 milestone) + Fixed (topk heap). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
0d6c20c278 |
chore(v2): zero-warnings hygiene — clear 13 build warnings across 4 crates
Removed unused Matter imports (sensing-server bridge/commissioning), dropped needless mut (bridge, desktop discovery), documented ClockGateDecision variant fields (ruvector coherence), and marked deferred-P2/platform-only helpers #[allow(dead_code)] with honest notes (entity_on_matter/next_endpoint = Matter-publisher API deferred per ADR-159 §A5; decode_jpeg_to_rgb = macOS-only). Behavior-neutral; touched-crate tests green. Remaining 1 warning is a benign Windows .pdb filename collision inherent to the Tauri lib+bin desktop crate (renaming the bin would break Tauri bundling — won't-fix for a cosmetic warning). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
2e4461d64d |
release: bump 9 crates changed in the beyond-SOTA sweep for crates.io
vitals/wifiscan/hardware/nn 0.3.0->0.3.1, ruvector 0.3.1->0.3.2, signal 0.3.2->0.3.3, train 0.3.1->0.3.2, mat 0.3.0->0.3.1, sensing-server 0.3.1->0.3.2. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
a92b043143 |
perf(ruvector): eliminate fuse() double-clone (~2.17x marshalling) + bench (ADR-156 §2.4, §4)
MultistaticArray::fuse / fuse_ungated cloned every viewpoint embedding twice per fusion (once into `extracted`, again when building the attention input). Now the embeddings are MOVED out of `extracted` (one clone per viewpoint instead of two), capturing geometry/ids by Copy in the same pass. Correctness-neutral — all 100 viewpoint/mat lib tests pass unchanged. MEASURED (new benches/fusion_bench.rs, embedding_extract A/B, 8 vp x 128-d): before_double_clone 1.0029 us -> after_single_clone 461.6 ns (~2.17x) End-to-end fusion_pipeline (8 vp): 202 us — marshalling is <1% of fusion (n*n attention dominates), so end-to-end win is modest; the A/B isolates the clone elimination. Reproduce: cargo bench -p wifi-densepose-ruvector --bench fusion_bench Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
a2daa2e443 |
fix(ruvector): crafted-input DoS — no panic on out-of-range indices (ADR-156 §2.2)
Security fix: two functions on a fusion/localisation path that can carry network-sourced multistatic frames panicked on crafted input (remote DoS). - triangulation::solve_triangulation indexed ap_positions[0] (empty table) and ap_positions[i]/[j] (crafted out-of-range AP index in a TDoA tuple). Now uses .first()? / .get(i)? / .get(j)? — returns None, never panics. - heartbeat::band_power computed n_freq_bins-1 (usize underflow on a zero-bin spectrogram) and did not clamp low_bin. Now guards n_freq_bins==0 and clamps both bounds into [0,last]; returns 0.0 for empty/inverted ranges. Tests (each panics on old code, verified by revert): triangulation_out_of_range_index_returns_none_no_panic, triangulation_empty_ap_positions_returns_none_no_panic, heartbeat_band_power_zero_bins_no_panic, heartbeat_band_power_out_of_range_bounds_no_panic. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
5b3e337c6d |
fix(ruvector): honest GDOP + canonical wrapped angular distance (ADR-156 §2.1, §2.3)
Two correctness/integrity fixes on the cross-viewpoint fusion geometry path, each pinned by a regression test that fails on the old code. - GDOP mislabel (§2.3): CramerRaoBound.gdop was `sqrt(crb_x+crb_y)` — identical to rmse_lower_bound (metres, noise-dependent), NOT a dimensionless GDOP. Now computes true GDOP = sqrt(trace(G^-1)) on the unit-variance bearing geometry, in both estimate() and estimate_regularised(); INFINITY (not NaN) for degenerate collinear geometry. Test gdop_is_dimensionless_and_noise_independent asserts GDOP is unchanged under 10x noise while RMSE scales 10x (old code failed: it scaled with noise, proving it was RMSE). - Angular wrap (§2.1): GeometricBias::build_matrix used raw |delta-azimuth| (can exceed pi, mis-states the 0/2pi seam) instead of the wrapped distance. angular_distance made pub and reused as the single canonical helper. HONEST: under the current cos() kernel this is a NUMERIC NO-OP (cos is even/periodic, cos(raw)==cos(wrapped)); landed for contract correctness + single-source-of- truth + future non-even kernels, not as a behaviour change. Tests pin the contract (wrapped value in [0,pi], seam symmetry). ruvector lib tests: 100 passed / 0 failed (+ new tests). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
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> |
||
|
|
fc7674bde9 |
feat(signal,ruvector): ADR-138 LinkGroup/ArrayCoordinator clock-quality gating (#842)
- ruvector viewpoint/coherence.rs: ClockQualityScore, ClockQualityGate, ClockGateDecision (Admit/MonitorOnly/Reject), ClockRejectReason. 200us floor, 9s staleness ceiling per ADR-110. - signal ruvsense/array_coordinator.rs: ArrayCoordinator domain service + DirectionalEvidence. Gates nodes, computes GDI + Cramer-Rao credence, builds attention weights (real node_attention_weights when amplitudes present, else clock-quality softmax), emits CoherenceDrop + GeometryInsufficient flags. - Cycle resolution: ArrayCoordinator lives in signal (depends on ruvector), not ruvector, so it can emit ADR-137 canonical ContradictionFlag. Documented. - 8 tests (5 coordinator + 3 clock gate); workspace 0 errors. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
004a63e82d |
fix(security): audit — fix RUSTSEC vulns, clippy warnings, dead code (#769)
- Upgrade openssl to 0.10.78 (CVE-2026-41676), jsonwebtoken to 9.4 - Suppress unmaintained-only/no-CVE advisories in .cargo/audit.toml with per-entry rationale - Fix all `cargo clippy --all-targets -- -D warnings` errors across 35 crates: derivable_impls, needless_range_loop, map_or→is_some_and/ is_none_or, await_holding_lock (drop MutexGuard before .await), ptr_arg (&mut Vec→&mut [T]), useless_conversion, approximate_constant (2.718→E, 3.14→PI), field_reassign_with_default, manual_inspect, useless_vec, lines_filter_map_ok, print_literal, dead_code - Apply `cargo fmt --all` - Pre-existing test failure in wifi-densepose-signal (test_estimate_occupancy_noise_only) is not introduced by this PR |
||
|
|
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>
|
||
|
|
f49c722764 |
chore(repo): rename rust-port/wifi-densepose-rs → v2/ (flatten to one level) (#427)
The Rust port lived two directories deep (rust-port/wifi-densepose-rs/) without any sibling under rust-port/ that warranted the extra level. Move the whole workspace up to v2/ to match v1/ (Python) at the same depth and shorten every cd / build command across the repo. git mv preserves history for all tracked files. 60 files updated for path references (CI workflows, ADRs, docs, scripts, READMEs, internal .claude-flow state). Two manual fixes for relative-cd paths in CLAUDE.md and ADR-043 that became wrong after the depth change (cd ../.. → cd ..). Validated: - cargo check --workspace --no-default-features → clean (after target/ nuke; the gitignored target/ was carried by the OS rename and had hard-coded old paths in build scripts) - cargo test --workspace --no-default-features → 1,539 passed, 0 failed, 8 ignored (same totals as pre-rename) - ESP32-S3 on COM7 → still streaming live CSI (cb #40300, RSSI -64 dBm) After-merge follow-up: contributors should `rm -rf v2/target` once and let cargo regenerate from the new path. |