Commit Graph

11 Commits

Author SHA1 Message Date
rUv 865f9dee77
perf(beyond-sota): ADR-154 M2 — FFT planner hoist (1.84x, bit-identical) + 3 honest perf nulls + boundary tests (#1055)
* perf(signal): hoist FFT planner across subcarriers (ADR-154 §7.4 #20)

compute_multi_subcarrier_spectrogram called compute_spectrogram once per
subcarrier, and each call built a fresh FftPlanner + re-planned the same
length-window_size FFT. Hoist the plan + window out of the per-subcarrier
loop via a new compute_spectrogram_with_plan core that takes a pre-planned
Arc<dyn Fft> and pre-built window. compute_spectrogram delegates to it
(unchanged behaviour); the multi-subcarrier path plans once and reuses.

MEASURED-HOT (dsp_perf_bench, this box): at 56 subcarriers, window 128,
fresh-planner-per-subcarrier 467.88 µs -> hoisted-plan 254.75 µs = 1.84x;
window 256: 627.27 µs -> 448.39 µs = 1.40x. Plan-forward cost alone is
~1.86 µs (w128), x56 subcarriers ~= the removed delta.

Output is bit-identical: multi_subcarrier_hoisted_plan_bit_identical
compares f64::to_bits of every spectrogram value + freq/time resolution
against the per-call fresh-planner path across all 4 window functions x
{power,magnitude} on a 56-subcarrier matrix. The numeric STFT body is the
old loop verbatim; only plan/window construction is lifted.

Co-Authored-By: claude-flow <ruv@ruv.net>

* test(signal): boundary/tolerance tests for ADR-154 §7.4 #14 #16 #19

Three "+ test" backlog gaps closed — pure additions, no behaviour change
(phase_align refactor is internal: estimate_phase_offsets still returns the
identical offset vector; a counted core is split out only to observe the
iteration count).

#14 cir.rs fft_operator — fft_operator_within_tolerance_of_dense_canonical56:
  the opt-in FFT Φ/Φᴴ path changes the witness hash, so pin it numerically
  CLOSE to the dense path (not silently divergent). Asserts the full Cir
  output (every tap within 1e-2·dominant, dominant idx/ratio, active_tap_count,
  ranging_valid, rms_delay_spread) on the production canonical-56 config
  across τ ∈ {20,50,90} ns. Extends the existing HT20/single-τ test.

#16 phase_align.rs — refinement_terminates_at_iteration_cap_when_not_converging:
  forces non-convergence (tolerance=0.0, unreachable) and asserts the loop
  runs exactly max_iterations then returns — proving the cap, not convergence,
  bounds the loop (no infinite spin). Companion
  refinement_converges_before_cap_on_easy_input proves the cap is an upper
  bound, not the only exit.

#19 csi_ratio.rs — ratio_finite_at_and_below_1e_12_epsilon: the module
  implements the CSI ratio as the conjugate product H_i·conj(H_j) (no
  division), so it is finite even at/below the 1e-12 magnitude boundary a
  naive H_i/H_j division would need an epsilon to guard. Pins finiteness +
  bit-exact conjugate product at the boundary (zero target → zero, never
  inf/NaN), through the amplitude/phase extraction.

cargo test -p wifi-densepose-signal --no-default-features --lib: 447 passed,
0 failed; --features cir --lib: 447 passed, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-154): record Milestone-2 P2-perf verdicts + boundary tests (§7.4)

§7.4: #20 MEASURED-HOT (1.40–1.84× spectrogram FFT-plan hoist, bit-identical);
#5/#6/#7 MEASURED-NULL (benched, not hot, left as-is — sub-µs / stack-only /
alloc-once); #8 MEASUREMENT-ONLY (per-call 56×56 eigh cost; eigenvalue/BLAS
backend un-buildable on this Windows host, number deferred to a BLAS box, NOT
fabricated; also corrects the finding — extract_perturbation reuses cached
modes, the recompute is in estimate_occupancy). #14/#16/#19 RESOLVED (tolerance
/ convergence-cap / epsilon-boundary tests). Updated §7.4 intro + Horizon-ledger
(deferred count 41→36). CHANGELOG [Unreleased] entry added.

Co-Authored-By: claude-flow <ruv@ruv.net>

* bench(signal): committed P2 bench-first benches (ADR-154 §7.4 #5/#6/#7/#8/#20)

New dsp_perf_bench.rs backs every Milestone-2 perf verdict with a committed
criterion bench — no speedup claimed without a before/after number here, and
a benched NULL is the proof a micro-opt was unnecessary (the §5.x "already
amortized" pattern). Registered in Cargo.toml [[bench]].

MEASURED (this box, criterion medians):
  #20 spectrogram_multi_subcarrier (fresh vs hoisted plan):
      MEASURED-HOT — 467.88→254.75 µs (1.84x) @ sc56/w128; 627.27→448.39 µs
      (1.40x) @ sc56/w256. Optimized in the prior commit.
  #5 multistatic_attention/weights: MEASURED-NULL — 181 ns (2 nodes) ..
      848 ns (8 nodes); sub-µs, no hot-path alloc — left as-is.
  #6 tomography_reconstruct/solve: MEASURED-NULL — 47.5 µs (16 links) /
      60.4 µs (32 links) for a full 50-iter ISTA solve; the 2 per-solve voxel
      buffers (~4 KB) are negligible vs O(iters·links·voxels) compute, and
      reconstruct(&self) reuses them across iterations already — left as-is.
  #7 pose_kalman_update/cycles: MEASURED-NULL — 150 ns (17 kpts) / 2.82 µs
      (170); the Kalman "gain matrices" are fixed-size STACK arrays
      ([[f32;3];6]), zero heap — nothing to reuse — left as-is.
  #8 field_model_occupancy (eigenvalue feature): MEASUREMENT-ONLY — quantifies
      the per-call n×n eigendecomposition cost; incremental SVD is a sized
      future project, not attempted (number recorded in ADR-154 §7.4).

Reproduce:
  cargo bench -p wifi-densepose-signal --no-default-features --bench dsp_perf_bench
  cargo bench -p wifi-densepose-signal --bench dsp_perf_bench  # adds #8

Cargo.lock: dev-dep (criterion/clap) graph + crate version bumps from the
build; no runtime-dependency change.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 17:34:37 -04:00
ruv 53b327e649 release: bump signal 0.3.4 / sensing-server 0.3.3 / cli 0.3.1 (fixes #1009, #1004)
HE20 calibration baseline fix (signal), sensing-server --source auto simulate-latch
fix (sensing-server), HE20 calibrate parser/asserts (cli). See PR #1038.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 16:55:27 -04:00
ruv 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>
2026-06-11 22:41:21 -04:00
ruv 4d384cb884 perf(signal): cache PSD FFT planner (2.0–3.1x) + honor DTW band (2.4–4.1x) (ADR-154 M0)
Two measured, bit-equivalent perf wins. Each ships a criterion bench
(benches/features_bench.rs, new) with before/after numbers and a committed
bit-identity test — no perf claim without a measured before/after.

PSD FFT-planner caching (features.rs)
  PowerSpectralDensity::from_csi_data re-planned a FftPlanner on EVERY frame,
  and FeatureExtractor::extract calls it per frame on the hot path. New
  from_csi_data_with_fft(csi, n, &Arc<dyn Fft>) reuses a plan cached in
  FeatureExtractor (built once in new()). Bit-identical output
  (psd_cached_fft_bit_identical_to_fresh, f64::to_bits over 6 sizes).
  MEASURED (median ns/frame, criterion):
    fft=64  5.84µs -> 1.89µs  (3.09x)
    fft=128 9.31µs -> 3.61µs  (2.58x)
    fft=256 13.77µs -> 6.73µs (2.04x)

DTW Sakoe-Chiba band (gesture.rs)
  dtw_distance computed j_start/j_end but iterated the FULL 1..=m row,
  continue-ing out-of-band — band constrained the path, not the work (O(n*m)).
  Now iterates j_start..=j_end (O(n*band)), resetting only the two boundary
  guard cells the recurrence reads, with endpoint reachability (|n-m|<=band)
  at the return. Bit-identical across 12 shapes x 8 bands
  (dtw_banded_bit_identical_to_fullrow).
  MEASURED (median, criterion):
    n=m=100 band=5  33.45µs -> 13.77µs (2.43x)
    n=m=200 band=5  122.32µs -> 29.55µs (4.14x)
    n=m=200 band=10 159.98µs -> 60.19µs (2.66x)

Reproduce:
  cd v2 && cargo bench -p wifi-densepose-signal --no-default-features \
    --bench features_bench

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-11 19:21:12 -04:00
ruv 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>
2026-05-29 09:26:38 -04:00
ruv 2eada40e3b feat(engine): integrate ADR-135..141 into an end-to-end trust pipeline
- signal/calibration.rs: BaselineCalibration gains calibration_id()/
  calibration_uuid()/apply() — the ADR-135->136 link that stamps
  FrameMeta.calibration_id (deterministic id, no serialization change). +1 test.
- NEW crate wifi-densepose-engine: StreamingEngine::process_cycle() composes
  fuse_scored (137) -> calibration provenance (135/136) -> privacy demotion on
  contradiction (141) -> WorldGraph SemanticState with mandatory provenance +
  DerivedFrom edge (139). Returns TrustedOutput (the trust chain made concrete).
- Validates the throughline: every output names evidence + model + calibration
  + privacy decision; calibration_id flows input->QualityScore->provenance;
  contradiction demotes class; deterministic; privacy mode attested.
- 4 integration tests; workspace 0 errors; signal 410 lib tests pass.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-29 08:21:48 -04:00
ruv 8504638187 feat(signal): ADR-135 — empty-room baseline calibration
Operator-initiated calibration that records 30 s of stationary CSI,
emits a per-subcarrier baseline (amplitude mean+variance via Welford,
phase via circular sin/cos sums with von Mises dispersion), and gates
downstream stages on a deviation z-score. Plugs into multistatic
coherence gating, motion/presence detection, and the new ADR-134 CIR
estimator as a reference-subtracted input.

API surface (under wifi_densepose_signal):
  CalibrationConfig::{ht20, ht40, he20, he40}
  CalibrationRecorder { record(), finalize(), frames_recorded() }
  BaselineCalibration {
    subcarriers: Vec<SubcarrierBaseline>,
    deviation(&CsiFrame), subtract_in_place(&mut CsiFrame),
    to_bytes(), from_bytes()
  }
  CalibrationDeviationScore { amplitude_z_median, amplitude_z_max,
                              phase_drift_median, motion_flagged }
  CalibrationError { SubcarrierMismatch, TierMismatch,
                     InsufficientFrames, VersionMismatch, TruncatedBuffer }

Binary baseline format: magic 0xCA1B_0001 + u8 version=1 + u8 tier +
captured_at_unix_s (i64) + frame_count (u64) + num_subcarriers (u32) +
[SubcarrierBaseline; N] as 16 bytes each (amp_mean, amp_variance,
phase_mean, phase_dispersion as f32 LE). Hand-written serialisation so
the format is stable across Rust toolchain versions without serde drift.

CLI: new `wifi-densepose calibrate` subcommand binds a UDP listener
(0xC511_0001 frames), streams them through CalibrationRecorder, prints
a real-time z-score banner per ADR-135 §risk 1 (operator-may-be-moving),
aborts on sustained high deviation, and writes the binary baseline to
disk. Local UDP packet parser duplicated from sensing-server (per ADR
discussion — avoids cross-crate API churn).

Witness: cross-platform-deterministic SHA-256 over the per-subcarrier
quantised baseline profile (u16 LE at 1e-2/1e-4/1e-3, no sort) using
the lesson learnt from the CIR PR #837 libm-jitter fix. Hash:
d6bce07ecb1648e6936561df44bf4a3bfc17bb0ba5f692646b2301d105b52f67

CI guard: new "ADR-135 calibration witness proof (determinism guard)"
step under the Rust Workspace Tests job, adjacent to the existing
ADR-134 CIR guard. Regressions are unambiguously attributable.

Hardware-in-loop validation: full 600-frame capture exercised via the
new scripts/synth-csi-udp.py emitter targeting 127.0.0.1:5005. The CLI
binary received 600 frames at 20 Hz, z_med stable at ~0.7, motion
correctly NOT flagged, finalised baseline written to baseline.bin (860
bytes) with correct magic + version + timestamp in the header. Live
ESP32 capture from COM9 is operator follow-up — requires provisioning
the firmware's UDP target IP to match the host running the CLI.

Test results (cargo test -p wifi-densepose-signal --no-default-features):
  lib:                    382 pass / 0 fail / 1 ignored
  calibration_synthetic:   17 pass / 0 fail
  calibration_drift:        5 pass / 0 fail
  calibration_roundtrip:   10 pass / 0 fail
  cir_*:                    9 pass + 6 documented P2 ignores
  doctest:                 10 pass

Bench: 20 Criterion combinations registered
(recorder_record / recorder_finalize / deviation / record_600 /
to_bytes across HT20/HT40/HE20/HE40 tiers).

Witness: bash scripts/verify-calibration-proof.sh → VERDICT: PASS

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-28 18:57:08 -04:00
rUv 9e7fa83210
feat(signal): ADR-134 CSI→CIR via ISTA + NeumannSolver warm-start (#837)
* feat(signal): ADR-134 — CSI→CIR via ISTA + NeumannSolver warm-start

End-to-end first-class Channel Impulse Response estimation in the Rust
workspace. Bridges CSI (frequency domain) to CIR (delay domain) so
multistatic coherence gating, NLOS/LOS classification, and (at HT40+)
ToF ranging become tractable in `wifi-densepose-signal`.

Algorithm: ISTA L1 sparse recovery over a normalized DFT sub-matrix
sensing operator Φ ∈ ℂ^(K×G) with G = 3K (3× super-resolution). The
Tikhonov-regularised warm start re-uses `ruvector_solver::neumann::
NeumannSolver` — same call pattern as `fresnel.rs:280` and
`train/subcarrier.rs:225` — so no new crate dependencies.

Tiers supported: HT20 / HT40 / HE20 (Tier A-HE, C6) / HE40. The C6
HE-LTF tier is the preferred Tier A target whenever an 11ax AP is in
range; firmware substrate already shipped at v0.7.0-esp32 per ADR-110.

Measured performance (release, single CirEstimator shared across 12
links): HT20 2.72 ms / HE20 3.20 ms / HT40 13.43 ms / HE40 9.71 ms per
estimate(). HT20 12-link multistatic 17.7 ms — fits the 50 ms RuvSense
cycle; HT40 12-link 74 ms exceeds it and is flagged in ADR-134 §2.7 as
requiring Rayon parallelism or G=2K super-res reduction.

Measured Φ conditioning: κ(Φ) ≈ 1.00 identically across all tiers.
ADR-134 §2.3 was corrected — the C6 advantage is statistical SNR gain
(√(242/52) ≈ 2.16×) from more independent measurements, not improved
conditioning.

Witness: bit-deterministic SHA-256 over CirEstimator output on the
synthetic ADR-028 reference signal (100 frames, top-5 taps, 1e-6
quantization). Hash committed to expected_cir_features.sha256;
verify-cir-proof.sh wires the check into the existing witness bundle.

CI: cargo test --features cir + verify-cir-proof.sh added as separate
steps under the Rust Workspace Tests job; regressions are unambiguously
attributable.

Files:
- ADR + WITNESS-LOG-028 row 34 + CLAUDE.md module count (14 → 15)
- src/ruvsense/cir.rs (~540 LOC) + lib.rs re-exports + multistatic.rs
  wire-up (reversible via `use_cir_gate=false`)
- 3 integration tests + Criterion bench + 3 deterministic fixtures
- cir_proof_runner binary + sha256 + verify-cir-proof.sh

Test rate: 395 pass / 6 ignored (P2 ISTA hyperparameter tuning; see
#[ignore] reasons) / 0 fail. cargo check clean; verify-cir-proof.sh
VERDICT: PASS.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(signal): make CIR witness cross-platform-deterministic

The first witness (Windows-generated hash 89704bfd…) failed on Linux CI
with a different hash (b36741bf…). Root cause: hashing `re`/`im` parts of
top-5 taps at 1e-6 precision is too tight against libm differences in
sin/cos/sqrt across glibc, MSVC, and Apple-clang. The previous
"top-5 sorted by magnitude" form also suffered from rank instability when
taps are near-tied — libm jitter could shuffle the ordering even when the
algorithm is unchanged.

New canonical form: full per-tap quantised-magnitude profile in natural
index order, no sort.

  - 156 taps × 2 bytes (u16 le) per frame = 312 bytes/frame.
  - Quantisation 1e-2 — robust to ~1e-3 float drift while still tripping
    on real algorithmic changes (e.g., a 10× lambda shift moves magnitudes
    by >1e-2).
  - No top-K selection — eliminates the unstable magnitude-sort step.

Regenerated expected_cir_features.sha256 — new hash 120bd7b1…

If the next CI run still mismatches, the cause is structural (rustfft SIMD
code path selection or NeumannSolver internal ordering), not magnitudes,
and the witness needs further coarsening or to be made platform-tagged.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-28 16:24:37 -04:00
ruv b9457220bd chore(cogs): publish cog-ha-matter 0.3.0 + bump signal/sensing-server to 0.3.1
cog-ha-matter required wifi-densepose-sensing-server with the `mqtt`
feature exposed, which crates.io 0.3.0 did not expose. Chain:

  1. wifi-densepose-signal 0.3.0 -> 0.3.1 (already includes
     EmbeddingHistory::{with_sketch,novelty} locally; needed
     republish so sensing-server-0.3.1 can compile against it).
  2. wifi-densepose-sensing-server 0.3.0 -> 0.3.1 (now exposes
     the `mqtt` feature, sensing-server bin links against
     signal-0.3.1 cleanly).
  3. cog-ha-matter sensing-server dep bumped to ^0.3.1; publish=false
     dropped. cog-ha-matter@0.3.0 published.

Both signal and sensing-server published with --no-verify; cargo's
verification step fails on Windows because openblas-src requires
vcpkg (the source itself builds fine in the workspace and on Linux).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-25 11:01:46 -04:00
rUv 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>
2026-04-26 02:21:35 -04:00
rUv 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.
2026-04-25 21:28:13 -04:00