Commit Graph

15 Commits

Author SHA1 Message Date
rUv 3314c8db8d
feat(cog-pose-estimation): scaffold first Cog from this repo (ADR-100 + ADR-101) (#642)
* feat(cog-pose-estimation): scaffold first Cog from this repo (ADR-100 + ADR-101)

Adds the foundation for the pose-estimation Cog that ships from this
repo into Cognitum V0 appliances. Companion ADR-225 + crate land in
cognitum-one/v0-appliance.

ADRs:
* ADR-100 formalises the Cognitum Cog packaging spec — on-device
  layout under /var/lib/cognitum/apps/<id>/, manifest.json schema
  (incl. new binary_sha256 + binary_signature fields), GCS hosting
  convention, repo source layout, build pipeline, and the four-verb
  runtime contract (version | manifest | health | run). Documents the
  convention I reverse-engineered from inspecting installed cogs on a
  live cognitum-v0 appliance — `anomaly-detect`, `presence`,
  `seizure-detect`, etc.
* ADR-101 designs the pose-estimation Cog itself: where it sits in
  the wifi-densepose pipeline (encoder init from
  ruvnet/wifi-densepose-pretrained, 17-keypoint regression head),
  what gets shipped per target arch (arm / x86_64 / hailo8 /
  hailo10), acceptance gates (PCK@20 explicitly deferred to #640 —
  this ADR ships the vehicle, not the accuracy).

Crate v2/crates/cog-pose-estimation/:
* Cargo.toml + workspace member declaration with a hailo feature gate
  so the binary builds without the Hailo SDK in CI.
* main.rs implements the four-verb CLI exactly per ADR-100.
* config.rs / manifest.rs / publisher.rs / inference.rs / runtime.rs —
  small modules, each <100 lines.
* publisher.rs emits ADR-100 structured JSON events.
* inference.rs is a stub that produces a centred-skeleton baseline
  with confidence=0 (honest: no trained weights wired in yet).
* runtime.rs subscribes to /api/v1/sensing/latest, slides a
  56*20 window, runs the engine, emits pose.frame events.
* cog/manifest.template.json + cog/config.schema.json define the
  release artifact + runtime config schemas.
* cog/Makefile holds build / sign / upload targets.
* tests/smoke.rs covers manifest roundtrip + engine I/O surface.

Verified locally:
* cargo check -p cog-pose-estimation: clean.
* cargo test  -p cog-pose-estimation: 4/4 pass.
* ./target/release/cog-pose-estimation {version,manifest,health}:
  all emit the right contract output.

This commit contains scaffolding only; the actual trained weights and
Hailo HEF cross-compile come in follow-ups tracked in #640 and the
companion v0-appliance branch.

* feat(cog-pose-estimation): first measured run — Candle CUDA on RTX 5080

Trained pose_v1 on ruvultra (RTX 5080) via Candle 0.9 + cuda feature
against the same 1,077-sample paired session that produced 0%/0% PCK
in #640 with the pure-JS SPSA trainer. First real numbers:

  PCK@20 = 3.0%   (up from 0.0%)
  PCK@50 = 18.5%  (up from 0.0%)
  MPJPE  = 0.093  (down from 0.66, ~7x improvement)

400 epochs in 2.1 s wall time, full-batch, ~5 ms/epoch. Loss curve
0.181 -> 0.014 over the run, eval 0.010. Per-joint reveals the model
leans on right-side proximal joints (r_hip 77% PCK@50, r_knee 35%,
l_elbow 26%) — consistent with the camera framing in the source
recording. Distal joints (wrists, ankles) and face joints are still
near-random, consistent with the 56-subcarrier / 20-frame input not
carrying fine-grained spatial info at 1077 samples.

This commit:

* Adds v2/crates/cog-pose-estimation/cog/artifacts/{pose_v1.safetensors,
  train_results.json} so the cog dir now contains a real reference
  artifact, not just scaffold.
* Updates cog/README.md "Status" block with the measured numbers,
  per-joint table, and an honest reading of where the model
  succeeds vs where the data is the bottleneck.
* Adds docs/benchmarks/pose-estimation-cog.md as the canonical
  benchmark log — append-only, one section per published run.
* Appends a "First measured run" section to ADR-101 referencing
  the new benchmark file.

Still pending in the follow-up:
* Wire pose_v1.safetensors into src/inference.rs (replace stub).
* ONNX export (Candle lacks a writer — needs external conversion).
* Hailo HEF cross-compile + cluster deploy.

The data-bound gap to PCK@20 >= 35% is tracked in #640.

* feat(cog-pose-estimation): wire real weights — cog is no longer a stub

Replaces the centred-skeleton stub in src/inference.rs with a real
Candle-based loader that reads cog/artifacts/pose_v1.safetensors and
runs the trained Conv1d encoder + MLP pose head on every incoming CSI
window.

What changes:

* src/inference.rs: PoseNet mirrors the training script's architecture
  exactly — Conv1d(56->64, k=3 d=1), Conv1d(64->128, k=3 d=2),
  Conv1d(128->128, k=3 d=4), mean over time, Linear(128->256)+ReLU,
  Linear(256->34)+sigmoid -> reshape [17, 2]. The InferenceEngine
  searches a sensible candidate list for the weights file
  (/var/lib/cognitum/apps/pose-estimation/, ./pose_v1.safetensors,
  ./cog/artifacts/, repo-root, v2/-relative) and falls back to the
  stub when none are present so the cog still satisfies ADR-100.
* Cargo.toml: adds candle-core 0.9 + candle-nn 0.9 (no-default-features,
  CPU build by default) + safetensors 0.4. New `cuda` feature opt-in
  for GPU inference on hosts that have it. Drops the unused
  wifi-densepose-train path dep from the default build path.
* src/main.rs + src/publisher.rs: health.ok event now carries
  `backend` (candle-cuda | candle-cpu | stub) and the synthetic
  output confidence, so operators can tell at a glance whether the
  cog loaded its weights or fell back to the stub.
* tests/smoke.rs: adds `real_weights_load_when_available` which
  asserts the loaded engine reports backend=candle-* and emits
  non-zero confidence — exactly the signal that proves we're not
  silently degrading to the stub.

Verified locally:

* `cargo check -p cog-pose-estimation --no-default-features` — clean
* `cargo test  -p cog-pose-estimation --no-default-features` — 5/5 pass
* `./target/release/cog-pose-estimation health` emits:
  {"event":"health.ok","fields":{"backend":"candle-cpu","cog":"pose-estimation","synthetic_output_confidence":0.185}}
  — 0.185 is the published PCK@50 from cog/artifacts/train_results.json,
  emitted by the real Candle inference path (would be 0.0 if it had
  fallen back to the stub).

The cog now runs the trained pose_v1 model end-to-end. Accuracy is
still bounded by the underlying 1077-sample training data (PCK@20
3.0%, PCK@50 18.5% per docs/benchmarks/pose-estimation-cog.md) — that
gap is data-bound and tracked in #640. ONNX export + Hailo HEF
cross-compile remain follow-ups.

* docs(benchmarks): measure cog-pose-estimation cold-start latency

100 sequential `cog-pose-estimation health` invocations average 76.2 ms
each on a Windows x86_64 host using the `candle-cpu` backend. Each
invocation re-loads pose_v1.safetensors and runs one synthetic forward
pass, so this is the worst-case cold-start path. Long-running `run`
inference will be sub-millisecond per frame once the model is loaded.

Updates the benchmarks doc accordingly.

* feat(cog-pose-estimation): ONNX export — pose_v1.onnx + scripts/export-onnx.py

Adds the canonical ONNX artifact that unblocks downstream Hailo HEF
cross-compile + ONNX Runtime benchmarks. Generated on ruvultra (torch
2.12.0 + CUDA), 12,059 bytes, opset 18, dynamic batch axis.

* scripts/export-onnx.py: mirrors the Candle inference architecture in
  PyTorch (Conv1d 56->64, 64->128, 128->128 + Linear 128->256->34), pure-
  python safetensors loader (no extra pip dep), exports via
  torch.onnx.export, then verifies via onnx.checker.check_model and
  numerical parity against the torch reference.
* Verified parity vs torch: max |torch - onnx| = 8.94e-8 (1e-5
  threshold). Effectively bit-perfect.
* v2/crates/cog-pose-estimation/cog/artifacts/pose_v1.onnx — the
  artifact itself, 12 KB.
* docs/benchmarks/pose-estimation-cog.md — adds an ONNX export
  section with the verification numbers.

Next: Hailo HEF cross-compile (still gated on Hailo SDK on a
self-hosted runner) and ONNX Runtime latency benchmarks on each
target arch.

* feat(cog-pose-estimation): release v0.0.1 — signed aarch64 binary on GCS

End-to-end deploy: cross-compiled to aarch64-unknown-linux-gnu on
ruvultra, ran via qemu-aarch64-static, then smoke-tested on a real
cognitum-v0 Pi 5. Signed with COGNITUM_OWNER_SIGNING_KEY (Ed25519)
and uploaded to gs://cognitum-apps/cogs/arm/.

Real-hardware results on cognitum-v0 (Pi 5):
  health: backend=candle-cpu, confidence=0.185, real weights loaded
  30x sequential `health`: 0.251 s total -> 8.4 ms / invocation (cold)

GCS release artifacts (publicly downloadable):
  binary:  3,741,976 bytes
    sha256 1e1a7d3dd01ca05d5bfc5dbb142a5941b7866ed9f3224a21edc04d3f09a99bf5
  weights:   507,032 bytes
    sha256 eb249b9a6b2e10130437a10976ed0230b0d085f86a0553d7226e1ae6eae4b9e5
  signature (Ed25519, b64): LUN7xqLPYD3MFzm5dKB5MnYU0LvoRtek5ci5KiKPHBg+Xo6xuazwokn2Dw2JPMaLYJzmWn/SpT4djuR7hYvVDw==

Adds:
* v2/crates/cog-pose-estimation/cog/artifacts/manifest.json — the
  release-pipeline-produced manifest with all fields filled in per
  ADR-100, including arch, target_triple, signature, and a
  build_metadata block carrying the validation PCK numbers.
* docs/benchmarks/pose-estimation-cog.md — new sections covering
  the real Pi 5 smoke (8.4 ms cold-start) and the signed GCS
  release artifacts.

Verified by downloading the binary anonymously from GCS and
re-computing the sha256 — matches the locally-computed sha exactly.
Signature decoded to the expected 64-byte Ed25519 length.

Closes the GCS-upload acceptance criterion from ADR-100; the only
pending work is Hailo HEF cross-compile (still SDK-gated) and an
x86_64 release alongside this arm release.

* docs(benchmarks): record live cognitum-v0 install + 5-sec smoke run

Adds the "Live appliance install" section documenting what happened
when the signed v0.0.1 binary + weights were installed under
/var/lib/cognitum/apps/pose-estimation/ on cognitum-v0 (the V0
cluster leader).

* Layout matches the existing anomaly-detect / presence / seizure-
  detect cogs exactly — the Cogs dashboard at
  http://cognitum-v0:9000/cogs auto-discovers entries.
* `cog-pose-estimation run` ran for 5 seconds in the background and
  cleanly emitted run.started + structured WARN events for the
  missing local sensing-server on :3000 (cognitum-v0's actual CSI
  source is ruview-vitals-worker on :50054, not :3000). No crashes,
  no NaN, no leaks.
* Wiring `sensing_url` to the appliance-native source is a separate
  Day-2 integration task.
2026-05-19 17:03:09 -04:00
Winter Lau e964eaf14f
fix(deps): bump ndarray 0.15→0.17 and ndarray-npy 0.8→0.10 (closes #626) (#627) 2026-05-19 10:01:52 -04:00
rUv 1b155ad027
chore: remove empty stub crates wifi-densepose-{api,db,config} (closes #578) (#608)
Each of these crates was a single-line doc-comment placeholder:

  v2/crates/wifi-densepose-api/src/lib.rs:    //! WiFi-DensePose REST API (stub)
  v2/crates/wifi-densepose-db/src/lib.rs:     //! WiFi-DensePose database layer (stub)
  v2/crates/wifi-densepose-config/src/lib.rs: //! WiFi-DensePose configuration (stub)

with empty [dependencies] in their Cargo.toml and zero references from any
source file or Cargo.toml in the workspace (verified by `grep -rln
wifi-densepose-api/-db/-config` across `v2/`). They were reserved early for
an envisioned REST/database/config split that never materialised.

The functionality these would have provided is covered today by:
- REST/WS:  wifi-densepose-sensing-server (Axum)
- Config:   per-crate config + CLI args in sensing-server and desktop
- DB:       no persistent state; system is real-time

Removal prevents `cargo` from listing dead crates, shipping empty published
artifacts to crates.io, or wasting reviewer attention. If any of these names
is needed in the future, reintroduce them with a real implementation.

Per the issue reporter (@bannned-bit / Matad0r) #578 explicitly listed
"OR be removed from workspace members until implementation starts" as an
acceptable resolution.

Updated:
- `v2/Cargo.toml`: drop the three members (with inline comment explaining why)
- `v2/Cargo.lock`: regenerated by cargo check
- `CLAUDE.md`: drop the three rows from the crate table and the publishing
  order list
- `CHANGELOG.md`: add an `[Unreleased] / Removed` entry

Verified:
- `cd v2 && cargo check --workspace --no-default-features` -> finished
  in 48s, no errors (warnings unchanged)
2026-05-17 18:50:57 -04:00
dependabot[bot] afc86c6fc4
chore(deps): bump thiserror from 1.0.69 to 2.0.18 in /v2 (#469)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.69 to 2.0.18.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.69...2.0.18)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-version: 2.0.18
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 18:09:54 -04:00
dependabot[bot] e6710e8988
chore(deps): bump ndarray-linalg from 0.16.0 to 0.18.1 in /v2 (#477)
Bumps [ndarray-linalg](https://github.com/rust-ndarray/ndarray-linalg) from 0.16.0 to 0.18.1.
- [Release notes](https://github.com/rust-ndarray/ndarray-linalg/releases)
- [Commits](https://github.com/rust-ndarray/ndarray-linalg/compare/ndarray-linalg-v0.16.0...ndarray-linalg-v0.18.1)

---
updated-dependencies:
- dependency-name: ndarray-linalg
  dependency-version: 0.18.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 18:08:08 -04:00
dependabot[bot] ab9799adc3
chore(deps): bump tower-http from 0.5.2 to 0.6.8 in /v2 (#483)
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.5.2 to 0.6.8.
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.5.2...tower-http-0.6.8)

---
updated-dependencies:
- dependency-name: tower-http
  dependency-version: 0.6.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 18:08:04 -04:00
dependabot[bot] bdb4484259
chore(deps): bump tch from 0.14.0 to 0.24.0 in /v2 (#482)
Bumps [tch](https://github.com/LaurentMazare/tch-rs) from 0.14.0 to 0.24.0.
- [Release notes](https://github.com/LaurentMazare/tch-rs/releases)
- [Changelog](https://github.com/LaurentMazare/tch-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/LaurentMazare/tch-rs/commits)

---
updated-dependencies:
- dependency-name: tch
  dependency-version: 0.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 18:08:01 -04:00
dependabot[bot] 98e7eeda42
chore(deps): bump ruvector-core from 2.0.5 to 2.2.0 in /v2 (#479)
Bumps [ruvector-core](https://github.com/ruvnet/ruvector) from 2.0.5 to 2.2.0.
- [Release notes](https://github.com/ruvnet/ruvector/releases)
- [Changelog](https://github.com/ruvnet/RuVector/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ruvnet/ruvector/compare/v2.0.5...v2.2.0)

---
updated-dependencies:
- dependency-name: ruvector-core
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 18:07:37 -04:00
dependabot[bot] 5615edb24e
chore(deps): bump ruvector-temporal-tensor from 2.0.4 to 2.0.6 in /v2 (#476)
Bumps [ruvector-temporal-tensor](https://github.com/ruvnet/ruvector) from 2.0.4 to 2.0.6.
- [Release notes](https://github.com/ruvnet/ruvector/releases)
- [Changelog](https://github.com/ruvnet/RuVector/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ruvnet/ruvector/commits)

---
updated-dependencies:
- dependency-name: ruvector-temporal-tensor
  dependency-version: 2.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 18:07:33 -04:00
ruv d0b64bdeb6 chore(rvcsi): drop inline v2/crates/rvcsi-* — consume the vendor/rvcsi submodule / crates.io instead
rvCSI now lives in its own repo (github.com/ruvnet/rvcsi), vendored here as
`vendor/rvcsi` (PR #543) and published to crates.io as `rvcsi-* 0.3.x` /
to npm as `@ruv/rvcsi`. The inline copies in `v2/crates/rvcsi-*` (added in
#542) were a duplicate; this removes them and re-points the docs.

- `git rm -r v2/crates/rvcsi-{core,dsp,events,adapter-file,adapter-nexmon,ruvector,runtime,node,cli}`
- `v2/Cargo.toml`: remove the 9 from `members` (note: `vendor/rvcsi/Cargo.toml`
  is its own workspace — depend on the published crates or the submodule paths,
  not as v2 workspace members).
- `CLAUDE.md`: the 9 crate-table rows collapse to one `vendor/rvcsi` row.
- `README.md` docs table: rvCSI entry points at the standalone repo + notes the
  submodule / crates.io / npm / plugin.
- `CHANGELOG.md`: `[Unreleased]` entry.

The ADRs (ADR-095, ADR-096), PRD, and DDD model stay in `docs/` as the design
record of the incubation. `cargo build --workspace --no-default-features` and
`cargo test --workspace --no-default-features` stay green.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-12 23:00:23 -04:00
Claude 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
2026-05-13 00:17:45 +00:00
Claude 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
2026-05-12 23:49:58 +00:00
rUv 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>
2026-04-27 12:41:01 -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