* fix(signal): circular phase variance for ghost-tap guard (ADR-154 §7.4 #1)
`phase_variance` computed a LINEAR sample variance over phase angles that
wrap at ±π, so a tightly-clustered set straddling the branch cut reported
spuriously HIGH dispersion — false-tripping the `> TAU` ghost-tap guard on
real, tightly-clustered CIR taps.
Replace with Mardia's circular variance V = 1 − R̄, bounded [0,1] and
invariant to where the cluster sits on the circle. Re-derive the guard
against the bounded metric via a named const
`GHOST_TAP_CIRCULAR_VARIANCE_MAX` (the old TAU-scaled threshold is
meaningless on [0,1]).
Grade: metric fix MEASURED; threshold value DATA-GATED — a clean single-path
ramp also sweeps the circle, so V alone cannot separate clean from
unsanitized without labelled frames. Conservative default (0.99) errs toward
never false-rejecting, strictly more permissive at the wrap boundary than the
buggy linear guard.
Fails-on-old test: `phase_variance_circular_not_fooled_by_branch_cut` —
inlines the old linear variance to show it exceeds TAU on wrap-straddling
phases while circular V≈0 and the guard no longer trips. Plus
`phase_variance_circular_is_bounded_and_extremal` (V∈[0,1], V≈0 identical,
V≈1 uniform).
cargo test -p wifi-densepose-signal --no-default-features --features cir --lib
→ 432 passed, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(signal): pin Welford n=0/n=1 finiteness guard (ADR-154 §7.4 #10)
The shared `WelfordStats` (field_model.rs, used by longitudinal.rs and others)
relies on `count < 2` guards in `variance`/`sample_variance`/`std_dev`/
`z_score` to stay finite at the boundaries. The guards existed but the n=0
boundary was UNTESTED — exactly the §4 divide-by-(n−1) family the ADR groups
this with.
Add `welford_finite_at_n0_and_n1` asserting every statistic is finite and
returns the documented sentinel (0.0) at n=0 and n=1, plus load-bearing doc
comments on the two guards.
Fails-on-old proof: with the `sample_variance` guard removed, the test FAILS
with "attempt to subtract with overflow" at the `(self.count - 1)` underflow
(0usize − 1); `variance` would similarly yield 0.0/0.0 = NaN. The guard is
restored; the test pins it so a future regression is caught.
Grade: MEASURED (boundary finiteness is asserted; the guard is the §4-family
fix made testable).
cargo test -p wifi-densepose-signal --no-default-features --lib field_model
→ 22 passed, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* refactor(signal): de-magic adversarial thresholds + boundary tests (ADR-154 §7.4 #13)
Lift the bare numeric literals buried in `check`/`check_consistency` into
named, documented module consts (FIELD_MODEL_GINI_VIOLATION=0.8,
ENERGY_RATIO_HIGH_VIOLATION=2.0, ENERGY_RATIO_LOW_VIOLATION=0.1,
CONSISTENCY_ACTIVE_FRACTION_OF_MEAN=0.1, SCORE_W_* weights). VALUES UNCHANGED —
each const equals the original literal; only names + pinning tests are new.
Grade: DATA-GATED. The operating values stay empirical (defensible values need
labelled spoofed/clean CSI — Wi-Spoof, §6.2/§7.3). The de-magicking +
characterization tests are MEASURED: `tuning_consts_unchanged_from_literals`,
`energy_ratio_high_boundary`, `energy_ratio_low_boundary`,
`field_model_gini_boundary`, `consistency_active_fraction_boundary` pin the
decision boundaries at/just-below/just-above each threshold, so a future
data-driven retune is a visible, tested change.
Fails-on-change proof: bumping ENERGY_RATIO_HIGH_VIOLATION 2.0→3.0 makes
`energy_ratio_high_boundary` FAIL (restored). Operating values explicitly
NOT changed.
cargo test -p wifi-densepose-signal --no-default-features --lib ruvsense::adversarial
→ 20 passed, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* refactor(signal): de-magic coherence drift/gate thresholds (ADR-154 §7.4 #9)
Lift the bare detection literals in `coherence.rs::classify_drift`
(DRIFT_STABLE_SCORE=0.85, DRIFT_STEP_CHANGE_MAX_STALE=10) and the
`coherence_gate.rs` Default impl (DEFAULT_ACCEPT_THRESHOLD=0.85,
DEFAULT_REJECT_THRESHOLD=0.5, DEFAULT_MAX_STALE_FRAMES=200,
DEFAULT_PREDICT_ONLY_NOISE=3.0) into named, documented consts. VALUES
UNCHANGED. The gate already exposed these via GatePolicyConfig (config seam);
this names + pins the defaults.
Grade: DATA-GATED. Operating values stay empirical (defensible Z-score
thresholds need labelled stable/drifting coherence traces). De-magicking +
boundary tests are MEASURED: `classify_drift_stable_score_boundary`,
`classify_drift_stale_count_boundary` pin the at/just-below/just-above
decisions; `drift_consts_unchanged_from_literals` /
`gate_default_consts_unchanged_from_literals` pin the values. Operating values
explicitly NOT changed.
cargo test -p wifi-densepose-signal --no-default-features --lib ruvsense::coherence
→ 40 passed, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-154): mark §7.4 P1 backlog cleared — Milestone-1 (#1,#10 RESOLVED; #9,#13 DATA-GATED)
Update ADR-154 §7.4 backlog rows #1, #9, #10, #13 with commit refs + grades,
the §7.4 intro count (four P1 items cleared, ~41 P2/P3 remain), the
Horizon-ledger one-liner (Milestone-1 DONE), and the §8 honest-limits #1 line
(metric now correct; threshold still DATA-GATED). Add CHANGELOG [Unreleased]
entry.
Grades: #1 RESOLVED (MEASURED metric / DATA-GATED threshold), #10 RESOLVED
(MEASURED), #9 & #13 RESOLVED-PARTIAL (DATA-GATED — de-magicked + boundary
tested, operating values unchanged).
Validation: cargo test --workspace --no-default-features → 2057 passed, 0
failed; wifi-densepose-signal lib → 442 passed (no-default + --features cir);
python archive/v1/data/proof/verify.py → VERDICT: PASS, hash f8e76f21…46f7a
UNCHANGED (CIR ghost-tap guard is not on the deterministic proof path).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(sensing-server): stop leaking internal errors in HTTP responses (ADR-080 #2)
Six handlers in `main.rs` serialized the internal error `Display` straight
into the JSON response body, leaking server internals to any client (ADR-080
finding #2, CWE-209; reframed onto the Rust boundary by ADR-164 G11):
- edge_registry_endpoint: a panicked spawn_blocking `JoinError`
("task … panicked") in a 500, and the raw upstream error in a 503
- delete_model / delete_recording / start_recording: std::io::Error
strings carrying OS detail / filesystem paths
- calibration_start / calibration_stop: the FieldModel error chain
New `error_response` module: `internal_error` / `internal_error_json` /
`upstream_unavailable` log the full detail server-side only (tagged with a
correlation id) and return a generic body
(`{"error":"internal_error","correlation_id":…}`) — no `panicked`, no file
paths, no Debug chain. The correlation id lets an operator join a client
report to the exact server log line without ever shipping the detail.
Pinned by 5 error_response tests, incl. a leak-substring guard
(internal_error_body_does_not_leak_detail) verified to FAIL on the reverted
old body (returns the panic message / path / "os error"). The HOMECORE sweep
(ADR-161) covered homecore-server, not this crate.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(sensing-server): pin XFF-immunity + no-query-token (ADR-080 #1, #3)
Findings #1 (XFF-spoofing bypass) and #3 (JWT-in-URL, CWE-598) were logged
against the Python v1 API but are VERIFIED ABSENT on the current Rust
sensing-server, so they get regression tests rather than redundant fixes:
- #1 XFF: there is no IP-based rate-limiter or IP-allowlist to bypass, and
neither security middleware reads a forwarded header. Added
bearer_auth::xff_header_never_affects_auth_decision (spoofed
X-Forwarded-For never flips a 401<->200 decision) and
host_validation::forwarded_headers_never_bypass_host_allowlist (spoofed
X-Forwarded-Host: localhost never lets Host: evil.com past the allowlist).
- #3 JWT-in-URL: require_bearer reads the token only from the Authorization
header; WS handlers take no query token; the sole Query extractor
(EdgeRegistryParams) is a non-secret refresh flag. Added
bearer_auth::query_string_token_is_never_accepted — ?token= / ?access_token=
in the URL never authenticates (stays 401) while the header path still 200s.
Verified to FAIL when a query-token path is injected into require_bearer.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-080): mark P0 security findings #1-#3 RESOLVED; close ADR-164 G11
- ADR-080: Status note + per-finding closure (#1 XFF and #3 JWT-in-URL
verified absent + regression-pinned; #2 leaked errors fixed via the
error_response module). Records the v1-vs-Rust boundary distinction
explicitly: v1 paths remain archived; this closure governs the shipped
Rust sensing-server.
- ADR-164: Gap Register G11 and the Open/Gated Backlog entry marked
RESOLVED with the fix + branch reference.
- CHANGELOG: [Unreleased] -> ### Security entry covering all three findings.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): renumber 6 displaced ADRs to resolve duplicate-number collisions (ADR-164 G1)
Resolves the 5 duplicate ADR numbers (6 displaced files) flagged by ADR-164
Gap Register item G1. Canonical keeper per number = first file committed at
that number (date tie-broken by inbound cross-reference count / parent-appendix
relationship). Displaced files renumbered to the next free numbers (166-171):
050 keeps provisioning-tool-enhancements (5 refs vs 1)
-> ADR-166-quality-engineering-security-hardening
052 keeps tauri-desktop-frontend (parent ADR)
-> ADR-167-ddd-bounded-contexts (its appendix)
147 keeps nvidia-cosmos/OccWorld (the actual ADR, has Status header)
-> ADR-168-benchmark-proof (proof companion, no Status)
-> ADR-169-adam-mode-light-theme (was untracked)
148 keeps drone-swarm-control-system (committed #862)
-> ADR-170-yoga-mode-pose-system (was untracked)
149 keeps public-community-leaderboard-huggingface (committed 16:47 vs 17:38)
-> ADR-171-swarm-benchmarking-evaluation-methodology
Updates in-file `# ADR-NNN` headers and intra-file self-references (yoga-modes
* docs(adr): repoint inbound cross-references to renumbered ADRs (166-171)
Follow-up to the ADR renumbering (ADR-164 G1). Updates every inbound reference
that pointed at a displaced ADR, disambiguating shared numbers by title/slug so
only references to the DISPLACED topic move and keeper references stay put.
ADR-168 (was 147 benchmark-proof): README, CHANGELOG, user-guide,
proof-of-capabilities, research docs 00/03 — all path/label refs updated.
ADR-169 (was 147 adam-mode) / ADR-170 (was 148 yoga-mode): docs/adr/README index.
ADR-171 (was 149 swarm-benchmarking): all ruview-swarm eval code+docs
(Cargo.toml, evals/, eval_swarm.rs, metrics/mod/report/runner.rs), research
doc 03 (every §-ref matched ADR-171 sections, not AetherArena), 00-system-review,
series README, CHANGELOG, and ADR-148's forward/"open issues" pointers.
ADR-166 (was 050 quality-engineering / security-hardening): disambiguated from the
ADR-050 provisioning KEEPER by topic. The HMAC/secure_tdm, directory-traversal,
bind-address, and OTA-PSK-auth references in code comments
(wifi-densepose-hardware Cargo.toml + secure_tdm.rs, sensing-server main.rs) and
in ADR-052-tauri / ADR-167 all describe the security-hardening ADR -> ADR-166.
ADR-167 (was 052 ddd-appendix): inbound appendix references.
Index/registry updates: docs/adr/README.md, gap-analysis/census.md (rows +
header count), gap-analysis/lens-findings.md (collision table marked RESOLVED),
and ADR-164 Gap Register G1 marked RESOLVED with the full renumber map.
Keeper references deliberately untouched: all ADR-147 OccWorld code, all ADR-148
drone-swarm code/docs, all ADR-149 AetherArena refs (incl. ADR-150's SSL/resampling
refs, which ADR-150 explicitly binds to the AetherArena benchmark), ADR-050
provisioning refs, ADR-052 tauri refs. The frozen GitHub blob URLs in
docs/adr/.issue-177-body.md (pinned to an old branch) are left as historical.
Comment-only code edits; no behavior change. wifi-densepose-hardware compiles
clean; the sensing-server build's sole blocker is the pre-existing upstream
midstreamer-temporal-compare@0.2.1 registry crate, unrelated to these edits.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): ADR-151 — Per-Room Calibration & Specialized Model Training
Room-first calibration -> bank of small specialised ruVector models
(breathing, heartbeat, restlessness, posture, presence, anomaly) distilled
from the frozen Hugging-Face-published RF Foundation Encoder (ADR-150).
Four-stage local-first pipeline: baseline (ADR-135 environmental fingerprint)
-> guided enrollment (NEW EnrollmentProtocol, clean anchors not hours) ->
feature extraction (reuse signal_features + ruvsense) -> specialist bank
training (rapid_adapt LoRA heads, RVF storage, HNSW prototypes).
Invariants: specialisation over scale; local heads over a shared public base;
honest STALE degradation on baseline drift. Indexes ADR-149/150/151.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(cli): calibration HTTP API for UI-driven baseline capture (ADR-135/151)
Adds `wifi-densepose calibrate-serve` — an Axum HTTP API that wraps the
ADR-135 CalibrationRecorder so a UI (or any client) can drive an empty-room
baseline capture remotely. Stage 1 ("teach the room") of the ADR-151 room
calibration & training pipeline.
A single background task owns the UDP socket (ESP32 0xC511_0001 frames) and
the optional active recorder; HTTP handlers talk to it over an mpsc command
channel and read a shared status snapshot, keeping the &mut recorder
lock-free. CORS permissive so a browser UI can call it.
Endpoints (/api/v1/calibration/*):
GET /health liveness + UDP ingest stats (frames_seen, streaming)
POST /start { tier?, duration_s?, room_id?, min_frames? }
GET /status live progress (state, frames, progress, z, eta) — poll for UI
POST /stop finalize the current session early
GET /result finalized baseline summary (amp/phase-dispersion averages)
GET /baselines list persisted baseline .bin files
Reuses the existing calibrate.rs ESP32 wire parser (made pub(crate)); honest
abort when <10 frames arrive in the window (e.g. ESP32 not streaming).
Verified end-to-end over loopback: start -> 300 replayed HT20 frames ->
state=complete, 52-subcarrier baseline, phase_dispersion_avg=0.00096
(concentrated/valid), persisted to disk; all 6 endpoints exercised.
CLI: 19 tests pass; crate builds clean.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(cli): firewall-free CSI UDP relay for local Windows ESP32 testing
Windows Defender blocks inbound LAN UDP to a freshly-built binary without an
admin allow-rule; python.exe is already allowed. This relay binds the public
CSI port and forwards each datagram verbatim to a loopback port where
`calibrate-serve --udp-bind 127.0.0.1 --udp-port 5006` listens (loopback is
firewall-exempt). No admin required.
Validated: ESP32-format 0xC5110001 frames -> :5005 -> relay -> :5006 ->
calibrate-serve -> state=complete, 52-subcarrier baseline,
phase_dispersion_avg=0.00098 (clean). Completes the no-admin live-test path.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(changelog): record ADR-151 calibration API (calibrate-serve)
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(calibration): ADR-151 Stages 2–5 — enrollment, extraction, specialist bank, runtime
New crate wifi-densepose-calibration implementing the per-room pipeline beyond
Stage-1 baseline:
- anchor.rs: guided-anchor sequence + event-sourced EnrollmentSession (Stage 2)
- enrollment.rs: AnchorQualityGate + AnchorRecorder — gates anchors against the
ADR-135 baseline deviation (presence/motion), re-prompts bad captures
- extract.rs: Features + AnchorFeature — autocorrelation periodicity (breathing/
HR bands), variance/motion (Stage 3)
- specialist.rs: 6 small room-calibrated models — presence (learned threshold),
posture (nearest-prototype), breathing/heartbeat (band periodicity),
restlessness (calm/active normalization), anomaly (novelty vs anchors) (Stage 4)
- bank.rs: SpecialistBank — train/persist + baseline-drift STALE invalidation
- runtime.rs: MixtureOfSpecialists — presence short-circuit + anomaly veto +
stale flagging (Stage 5)
Statistical heads make the pipeline runnable/validatable today; the ADR-150 HF
RF Foundation Encoder backbone is the documented upgrade path. 29 unit tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(cli): wire ADR-151 enroll / train-room / room-status / room-watch
Integrates the wifi-densepose-calibration crate into the CLI as four
subcommands driving the full Stage 2–5 pipeline against a live ESP32 raw-CSI
stream (edge_tier=0):
- enroll: walks the guided anchor sequence, gates each capture against the
ADR-135 baseline deviation (re-prompts bad anchors), writes labelled features
- train-room: fits the SpecialistBank from the enrollment, persists JSON
- room-status: prints a trained bank's summary
- room-watch: live mixture-of-specialists readout (presence/posture/breathing/
heart/restless) over a rolling window, with anomaly veto + STALE flagging
Per-frame scalar is the mean CSI amplitude (carries presence/motion + breathing
modulation). Validated end-to-end on the live ESP32 (COM8, edge_tier=0): the
real parser → feature extraction → runtime detected breathing (~16–31 BPM) on
hardware. Full multi-anchor enrollment accuracy requires the operator to perform
the poses; phase-based breathing extraction is a noted refinement.
48 tests pass (29 calibration + 19 CLI).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-151): mark Stages 1–5 implemented; expand CHANGELOG
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(cli): keep proven mean-amplitude carrier for room features
The max-variance-subcarrier carrier locked onto motion artifacts (not
breathing) and also had an out-of-bounds bug on variable CSI subcarrier
counts. Reverted to the mean-amplitude carrier, which is validated live to
detect breathing. Phase-based extraction on a stable subcarrier remains the
proper higher-SNR refinement (ADR-151 §4).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(calibration): multistatic fusion of co-located nodes (ADR-029/151)
MultiNodeMixture fuses several co-located nodes (each with its own
room-calibrated SpecialistBank) into one RoomState:
- presence: OR across nodes (any node seeing a person wins)
- posture/breathing/heartbeat: highest-confidence node (best viewpoint)
- restlessness/anomaly: max across nodes
- veto: any node's physically-implausible signal vetoes the room's vitals
(anti-hallucination, same as single-node runtime) + presence short-circuit
- stale: any node's STALE flag propagates
Same-room multistatic only; cross-room is federation (ADR-105), not fusion.
6 unit tests (presence OR, best-confidence breathing, single-node veto,
staleness). 35 calibration tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(cli): multistatic room-watch — fuse co-located nodes (ADR-029/151)
`room-watch --node-bank N:path` (repeatable) groups live CSI frames by node_id
and fuses per-node banks via MultiNodeMixture. Validated live on COM8 (node 9,
edge_tier=0): frames grouped + fused end-to-end. True 2-node fusion is covered
by unit tests; a second raw-CSI node is the hardware blocker. 54 tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(integration): calibration → cognitum-v0 appliance integration overview
Detailed cross-repo integration spec for cognitum-one/v0-appliance: data
contracts (CSI wire format, ADR-135 baseline binary, enrollment/bank/RoomState
JSON schemas), calibrate-serve HTTP API, public crate API, Pi5+Hailo tiering,
and a 5-step appliance integration plan. Grounded in the verified cognitum-v0
inventory (aarch64, cargo 1.96, HAILO10H, ruview-vitals-worker:50054).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(calibration): address PR review — aarch64 decouple, API auth, path traversal, throttle
Resolves the review on #989:
- **Cross-compile (the appliance blocker):** make wifi-densepose-mat optional
and feature-gate it (`mat`), so `cargo build -p wifi-densepose-cli
--no-default-features` excludes the mat→nn→ort(ONNX)→openssl-sys chain.
Verified: `cargo tree --no-default-features` shows 0 ort/openssl deps →
calibration cross-compiles clean for the Pi.
- **Security (must-fix before LAN):**
- `--token` / CALIBRATE_TOKEN bearer-auth middleware on every route; warns if
bound non-loopback without a token.
- sanitize client-supplied `room_id` to [A-Za-z0-9_-] (≤64) before it reaches
the baseline write path — kills the `../` file-write primitive. + test.
- **Perf:** stop locking shared status + cloning SessionStatus on every UDP
frame — counters/snapshot flush on the 200 ms tick instead (no CPU
starvation under flood). finalize write moved to async `tokio::fs::write`.
- **Docs:** ADR-151 STALE wording matches the impl (baseline-id change;
drift-threshold = P6 refinement); integration doc gets the
`--no-default-features` build + auth/sanitize notes.
35 calibration + 15 CLI tests (no-default) / 20 CLI (default) pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(worldgraph,worldmodel): add crates.io READMEs
Plain-language overviews + feature lists, comparison tables (symbolic graph vs
predictive occupancy; graph vs grid vs event-log), usage, and technical
details. Adds readme = "README.md" to both manifests so they render on
crates.io on the next release.
Co-Authored-By: claude-flow <ruv@ruv.net>
* release: worldgraph & worldmodel 0.3.1 (READMEs on crates.io)
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs: precise calibration validation scope (capture+API+auth proven; clean enroll→train→infer not yet on-target)
Aligns ADR-151 §7 + the appliance integration doc with the PR #989 scope
clarification: nothing has run a clean baseline → enroll → train → infer on
live CSI; the live breathing read used the stateless head, not a trained bank.
Adds --source-format adr018v6 to the backlog.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(calibrate-serve): live GET /room/state endpoint (mixture over CSI window)
Adds a live RoomState readout over HTTP — the appliance UI's main need. The
ingest task maintains a rolling per-frame scalar window (flushed on the 200 ms
tick, no per-frame lock); the handler loads a bank (resolved as a sanitized
name under output_dir — same path-traversal defense as room_id), runs the
MixtureOfSpecialists over the window, returns RoomState JSON.
Validated live (ESP32-S3 via relay): breathing 14-19 BPM over HTTP; a
bank=../../etc/passwd query is neutralized to 'etcpasswd' (no traversal).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(calibrate-serve): POST /room/train + fix AnchorLabel JSON to snake_case
- POST /api/v1/room/train: { room_id, baseline_id, anchors[] } → trains a
SpecialistBank and persists it as <output_dir>/<room_id>.json (path-sanitized),
readable via /room/state?bank=<room_id>. Completes the HTTP train→infer loop.
- Fix data-contract bug: AnchorLabel serialized as PascalCase variant names
(serde default) while as_str() + the integration doc used snake_case. Added
#[serde(rename_all = "snake_case")] so the JSON wire format matches the
documented contract (empty/stand_still/…). Locked with a roundtrip test.
Validated live (ESP32-S3): POST train (4 anchors → 6 specialists, persisted) →
GET /room/state returns RoomState with the trained presence/restlessness; the
synthetic-vs-real scale mismatch correctly triggers the anomaly veto. 36
calibration tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(calibrate-serve): live enroll-over-HTTP (POST /enroll/anchor + /enroll/status)
Closes the last HTTP gap — the appliance can now drive the ENTIRE calibration
pipeline over HTTP without the CLI:
baseline (start/stop) -> enroll/anchor x8 -> room/train -> room/state
- POST /enroll/anchor { room_id, baseline, label, duration_s? }: the ingest task
loads the baseline (sanitized name under output_dir), captures the anchor for
the duration against it (AnchorRecorder + per-frame series), runs the quality
gate, and on completion replies with the verdict + accumulates the AnchorFeature
in an in-server enrollment map keyed by room_id. Re-prompts on rejection.
- GET /enroll/status?room=<id>: accepted anchors, next, complete.
- POST /room/train now falls back to the in-server enrollment when anchors[] is
omitted.
Validated live (ESP32-S3): capture baseline -> enroll stand_still (271 frames,
6s) -> gate correctly rejects "no person detected (presence_z 0.90 < 1.50)"
relative to a same-occupancy baseline (a clean empty-room baseline is the
documented on-target prerequisite). Builds clean; CLI tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(calibrate-serve): HTTP integration tests for the room/enroll endpoints
Factor the router into build_router() (shared by execute + tests) and add
tower-oneshot integration tests (no network/ingest needed):
- health + descriptor → 200
- POST /room/train persists the bank; GET /room/state → 200; train with no
anchors/enrollment → 400
- path-traversal: /room/state?bank=../../etc/passwd → 404 (sanitized, never
reads outside output_dir)
- enroll/status empty; /enroll/anchor with an unknown label → 400
CI regression coverage for the endpoints added this session. 18 CLI tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(mat): make serde non-optional — unblocks `cargo test --workspace --no-default-features`
Making wifi-densepose-mat optional in the CLI (for the aarch64/ort decouple)
exposed a latent feature bug: mat's `api` module compiles unconditionally and
uses serde, but `serde` was an optional dep enabled only via the `api`/`serde`
features. Previously the CLI's *unconditional* mat dependency enabled those
features transitively, so `--workspace --no-default-features` still got serde;
once mat became optional+gated, the workspace build lost it →
`error[E0432]: unresolved import serde` across mat's api/* (CI red).
mat already pulls serde_json + axum unconditionally, so making `serde`
non-optional has no real cost and restores the workspace build. Does NOT affect
the aarch64 CLI build (mat isn't built there at all): verified
`cargo tree -p wifi-densepose-cli --no-default-features` still shows 0
ort/openssl deps, and `cargo test --workspace --no-default-features` compiles
clean.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(claude.md): add wifi-densepose-calibration to crate table (pre-merge)
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): ADR-152 — WiFi-pose SOTA 2026 intake (geometry-conditioned calibration, external benchmarks, encoder recipe)
Records the 2026-06-10 deep-research run (22 sources, 110 claims, 25
adversarially verified: 24 confirmed / 1 refuted) and the decisions it
implies:
- §2.1 ACCEPTED: geometry-condition the ADR-151 calibration system —
NodeGeometry at enrollment, geometry embeddings for future LoRA heads,
PerceptAlign-style two-checkerboard camera↔WiFi alignment for the
ADR-079 supervised path. PerceptAlign (MobiCom'26) names the failure
mode ("coordinate overfitting") that matches our own ADR-150 cross-
subject collapse.
- §2.2 ACCEPTED: benchmark protocol vs external "WiFlow-STD (DY2434)"
(claimed 97.25% PCK@20, Apache-2.0 weights+dataset) with a no-citation
rule until measured on our 17-keypoint ESP32 eval set. Name collision
with our internal WiFlow is disambiguated.
- §2.3 ACCEPTED: amend ADR-150 training recipe per UNSW MAE study —
80% masking, (30,3) patches, data-over-capacity priority (log-linear,
unsaturated at 1.3M samples).
- §2.4 watch items: IEEE 802.11bf-2025 published 2025-09-26;
esp_wifi_sensing as external presence baseline (drop-in claim REFUTED
0-3); ZTECSITool 160MHz/512-subcarrier anchor node (procurement-gated).
- §2.5 NOT adopted: non-WiFi "foundation model" papers; DensePose-UV
(no 2025-2026 work does UV regression from commodity WiFi).
Every number is evidence-graded CLAIMED vs MEASURED in the source
register. Re-check horizon 2026-12.
Co-Authored-By: RuFlo <ruv@ruv.net>
* test(calibration): full-loop integration test — baseline→enroll→train→infer proven in-process (ADR-151 §7 gap, software half)
Closes the software half of PR #989's headline validation gap: the
complete calibration loop had never run end-to-end anywhere, even
in-process. tests/full_loop.rs (412 lines, deterministic xorshift32
room simulator, HT20/52-subcarrier/20Hz, same fingerprint family as
the ADR-135 roundtrip test) now drives the CLI's exact stage order
through the public API:
1. baseline — 600 static frames, zero motion flags post-warmup,
calibration_uuid() exactly as the CLI derives it
2. enroll — all 8 AnchorLabel::SEQUENCE anchors through
AnchorQualityGate::default(), session is_complete()
3. extract — AnchorFeature::from_series recovers injected 0.25Hz
and 0.125Hz breathing within ±0.04Hz
4. train — SpecialistBank::train fits all 6 specialists; JSON
round-trip and the runtime consumes the RELOADED bank
5. infer — positive: never-enrolled 0.30Hz subject reads present,
18±2 BPM; negative: empty window reads absent;
degradation: foreign baseline_id flags STALE
Seed-robust (5 seeds), passes with and without default features:
36 unit + 1 integration green.
Validation docs updated (ADR-151 §7 + integration doc §7 matrix): what
remains is strictly the on-target hardware session (real CSI, physically
empty room, operator performing the guided anchors). Three behavioral
findings from building the test are recorded for pre-session triage:
z-band squeeze between baseline motion flagging (z>2.0) and the still-
anchor gate (presence_z≥1.5) — likeliest on-hardware enroll failure;
variance-only PresenceSpecialist missing motionless-person mean shift;
ungated breathing_hz/heart_hz in noise-window embeddings.
Co-Authored-By: RuFlo <ruv@ruv.net>
* fix(calibration): close all four ADR-152 behavioral findings pre-hardware-session
The full-loop integration test surfaced three findings; fixing the third
exposed a fourth. All four are fixed and regression-guarded:
1. z-band squeeze (enrollment.rs) — anchor motion is now measured from
frame-to-frame deltas of the deviation series (|Δz| > Z_DELTA_MOTION
0.5 ∨ |Δφ| > π/6), not from the absolute motion_flagged, which fires
at amplitude_z_median > 2.0 vs the EMPTY baseline and so conflated
presence strength with motion. A strongly-reflecting still person
(z = 3.0 — every frame flagged by the old heuristic) now enrolls.
The old unit tests mocked (z=3.0, motion=false), a combination the
real deviation() can never emit — which is exactly how the squeeze
hid; tests now derive the flag from z the way the producer does.
2. variance-only presence (specialist.rs) — PresenceSpecialist gains a
mean-shift channel: present when variance > threshold OR
|mean − empty_mean| > mean_dist_threshold (trained at half the
empty→occupied mean distance, None when the means don't separate).
Detects the motionless person whose body raises the scalar mean but
not its variance. Old persisted banks deserialize with the channel
inert (serde default None) — variance-only behavior preserved,
proven by a fixture test against pre-change JSON.
3. ungated hz embedding (extract.rs) — Features::embedding() zeroes
breathing_hz/heart_hz below EMBED_MIN_SCORE (0.25), keeping the
random in-band peaks of noise windows out of the posture/anomaly
prototype space. Raw fields stay ungated (specialists have their
own stricter gates).
4. heart-band lag-floor leakage (extract.rs, found while fixing 3) —
a pure 0.30 Hz breathing signal scored 0.67 in the heart band at
3.33 Hz: out-of-band rhythm leaks as a monotonic slope whose max
sits at the band's lag floor, so score gating alone cannot stop it.
autocorr_dominant now requires the winning lag to be an interior
local maximum; band-edge "peaks" are rejected, true in-band peaks
(interior by definition) are preserved.
full_loop.rs strengthened to drive the fixes end-to-end: the StandStill
anchor is now a z=3.0 strong reflector (unenrollable pre-fix), and a new
motionless-person runtime case proves mean-channel detection at empty-
level variance.
Validation: 41 calibration unit + 1 full-loop integration + 23 CLI tests
green; cargo test --workspace --no-default-features exit 0.
Co-Authored-By: RuFlo <ruv@ruv.net>
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>
* 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>
`vendor/midstream` is a git submodule of RuView but no `v2/crates/*` depends
on a `midstreamer-*` crate and no Rust source uses one — i.e. it is vendored
but not consumed, the same state `vendor/rvcsi` was in before ADR-097.
ADR-098 evaluates whether to change that. The candidate seams (from the
prompt) were:
1. Streaming / pub-sub for the WS fan-out (today: `tokio::sync::broadcast`
at `wifi-densepose-sensing-server/src/main.rs:4769`).
2. CSI → DSP → event pipeline (today: rvcsi-events::EventPipeline, just
adopted by ADR-097).
3. Multi-source merging / TDM for the ESP32 mesh (ADR-029, ADR-073).
4. Backpressure / flow control between the UDP receiver and downstream
consumers (firmware `stream_sender` ENOMEM; host-side bounded
broadcast channel).
Reading all six midstream workspace crates end-to-end
(`vendor/midstream/crates/{temporal-compare,nanosecond-scheduler,
temporal-attractor-studio,temporal-neural-solver,strange-loop,
quic-multistream}/src/*.rs` — ~3,455 LOC) shows midstream's identity
unambiguously: `Cargo.toml:16` calls itself "Real-time LLM streaming with
inflight analysis", the README frames it as analyzing *LLM token streams*
in real time, and zero hits across the workspace for `csi|wifi|sensing|
sensor`. midstream's abstractions are LLM-token / dashboard-telemetry
shaped; RuView's pipeline is RF-frame / event-detector shaped.
Decisions:
D1 — WS fan-out: keep `tokio::sync::broadcast::channel::<String>(256)`.
midstream offers no equivalent in-process broadcast primitive.
D2 — CSI pipeline: keep `rvcsi-events::EventPipeline` (deterministic,
single-frame-at-a-time, replayable per ADR-095 D9). midstream's
attractor / LTL crates operate on multi-dimensional trajectories,
not validated single CSI frames.
D3 — TDM / aggregator: keep `wifi-densepose-hardware::aggregator` +
firmware-side TDM. midstream has no UDP merger and no cross-device
wall-clock scheduler.
D4 — Backpressure: the firmware ENOMEM rate-limit and the bounded host
`broadcast` channel are correct at each end; midstream's QUIC
primitives don't help the actual UDP+WS topology.
D5 — Carve-out: `midstreamer-temporal-compare` (DTW / LCS / Levenshtein)
is a plausible future-evaluation option if a *second* DTW use case
appears in RuView. RuvSense already has one (`gesture.rs`).
D6 — Carve-out: `midstreamer-scheduler` (deadline-aware, EDF / LLF /
RM) is a plausible future option if the cluster-Pi aggregator ever
takes over real-time scheduling. Today that lives in firmware.
D7 — Submodule: keep `vendor/midstream` pinned at `30fe5eb` as reference
material; do not advance the pin per-release (unlike vendor/rvcsi
under ADR-097 D7) because there is no in-build consumer.
D8 — Docs: cross-reference, don't import. ADR-098 added to
`docs/adr/README.md`.
Status: Rejected (with named re-evaluation triggers in §6 — second DTW use
case, host-side real-time scheduler, midstream gains a CSI adapter, or a
QUIC-to-external-client requirement that WS can't service).
ADR-098 rejected midstream as a *replacement* for RuView's existing seams.
ADR-099 is the other half: midstream's `temporal-compare` (DTW) and
`temporal-attractor-studio` (Lyapunov + regime classification) crates as a
*parallel* per-frame introspection tap, alongside the existing window-aggregated
event pipeline.
The 8 decisions:
D1 — Only midstreamer-temporal-compare 0.2 + midstreamer-attractor 0.2;
scheduler / neural-solver / strange-loop are out of scope of this ADR.
D2 — Tap point: post-validate, parallel to WindowBuffer::push in csi.rs.
The existing /ws/sensing path is unchanged.
D3 — New /ws/introspection topic + /api/v1/introspection/snapshot REST endpoint
carrying IntrospectionSnapshot { regime, lyapunov_exponent,
attractor_dim, top_k_similarity }.
D4 — Per-frame updates only, never window-blocked. Soonest-event latency on
the "shape recognized" path collapses from ~533 ms (16-frame @ 30 Hz
window) to ~33 ms (one frame), a ~16× win.
D5 — temporal-neural-solver (LTL) is out of scope (separate MAT audit ADR).
D6 — ESP32 firmware unchanged; deployment is host-side only.
D7 — Signature library is JSON, on-disk, customer-owned; three reference
signatures ship as developer fixtures.
D8 — Promotion bar is empirical: ≥10× p99 latency reduction vs. the existing
/ws/sensing event path, or the feature stays behind a CLI flag.
Indexed in docs/adr/README.md. Phased adoption (P0 spike + benchmark → P1 first
real signature library → P2 dashboard widget → P3 capture workflow → P4 optional
adaptive_classifier hook). Implementation lands as ~150–250 lines + one
integration test in v2/crates/wifi-densepose-sensing-server in follow-up PRs.
Co-Authored-By: claude-flow <ruv@ruv.net>
rvCSI was extracted to its own repo (PR #542→#544): 9 crates on crates.io @
0.3.1, `@ruv/rvcsi` on npm, vendored at `vendor/rvcsi`. RuView currently
*vendors but does not consume* it — zero `rvcsi-*` deps in `v2/`, zero
`use rvcsi_…` imports, zero `@ruv/rvcsi` JS imports. ADR-097 decides:
D1 — Depend on the published crates from crates.io, not the submodule path.
D2 — Pilot in `wifi-densepose-sensing-server` (smallest, best-bounded
touchpoint: UDP receiver + handlers + WS fan-out).
D3 — `wifi-densepose-signal` is *layered on top of* rvCSI, not replaced.
The SOTA / RuvSense modules go beyond rvCSI's scope and stay in
RuView; they consume `rvcsi_core::CsiFrame`. Overlapping basic DSP
primitives delegate to `rvcsi-dsp` or become thin shims.
D4 — `wifi-densepose-hardware` stops carrying ESP32 wire-format parsing;
the parser moves to a new `rvcsi-adapter-esp32` crate (ADR-095 §1.2
/ D15 follow-up, owned in the rvCSI repo).
D5 — `wifi-densepose-ruvector` (training pipeline) and `rvcsi-ruvector`
(runtime RF memory) stay separate for now; a follow-up unifies them
once the production RuVector binding lands.
D6 — `rvcsi_core::CsiFrame` is the boundary type at the runtime edge;
one explicit `From`/`Into` conversion point at that edge.
D7 — Track via `rvcsi-* = "0.3"` SemVer ranges + bump the `vendor/rvcsi`
submodule pin per RuView release for reproducible offline builds.
D8 — Once every consumer depends on crates.io, decide (separately)
whether to drop the submodule.
Adoption is phased (P1 pilot → P2 signal shim → P3 ESP32 adapter →
P4 clean-up → P5 submodule review); each phase is one PR with tests.
Indexed in docs/adr/README.md.
Co-Authored-By: claude-flow <ruv@ruv.net>
Explains why ADRs matter for AI-generated code (prevents drift,
provides constraints and rationale), how they work with DDD domain
models, and indexes all 44 ADRs by category.
Also fixes ADR count 43 -> 44 in main README.
Co-Authored-By: claude-flow <ruv@ruv.net>