deploy: 42dcf49f4d
This commit is contained in:
parent
5b7e89f73f
commit
5cf60ec888
|
|
@ -5,7 +5,7 @@
|
|||
| Status | Proposed |
|
||||
| Date | 2026-03-06 |
|
||||
| Deciders | ruv |
|
||||
| Depends on | ADR-012 (ESP32 CSI Mesh), ADR-039 (Edge Intelligence), ADR-040 (WASM Programmable Sensing), ADR-044 (Provisioning Enhancements), ADR-050 (Security Hardening), ADR-051 (Server Decomposition) |
|
||||
| Depends on | ADR-012 (ESP32 CSI Mesh), ADR-039 (Edge Intelligence), ADR-040 (WASM Programmable Sensing), ADR-044 (Provisioning Enhancements), ADR-166 (Security Hardening, renumbered from ADR-050), ADR-051 (Server Decomposition) |
|
||||
| Issue | [#177](https://github.com/ruvnet/RuView/issues/177) |
|
||||
|
||||
## Context
|
||||
|
|
@ -211,7 +211,7 @@ pub struct FlashProgress {
|
|||
// commands/ota.rs
|
||||
|
||||
/// Push firmware to a node via HTTP OTA (port 8032).
|
||||
/// Includes PSK authentication per ADR-050.
|
||||
/// Includes PSK authentication per ADR-166.
|
||||
#[tauri::command]
|
||||
async fn ota_update(
|
||||
node_ip: String,
|
||||
|
|
@ -801,7 +801,7 @@ Total estimated effort: ~11 weeks for a single developer.
|
|||
- ADR-039: ESP32 Edge Intelligence
|
||||
- ADR-040: WASM Programmable Sensing
|
||||
- ADR-044: Provisioning Tool Enhancements
|
||||
- ADR-050: Quality Engineering — Security Hardening
|
||||
- ADR-166: Quality Engineering — Security Hardening (renumbered from ADR-050)
|
||||
- ADR-051: Sensing Server Decomposition
|
||||
- `firmware/esp32-csi-node/` — ESP32 firmware source
|
||||
- `firmware/esp32-csi-node/provision.py` — Current provisioning script
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# ADR-080: QE Analysis Remediation Plan
|
||||
|
||||
- **Status:** Proposed
|
||||
- **Status:** Proposed — P0 security findings #1–#3 **RESOLVED** on the shipped Rust sensing-server boundary (2026-06-13; closes ADR-164 G11)
|
||||
- **Date:** 2026-04-06
|
||||
- **Source:** [QE Analysis Gist (2026-04-05)](https://gist.github.com/proffesor-for-testing/a6b84d7a4e26b7bbef0cf12f932925b7)
|
||||
- **Full Reports:** [proffesor-for-testing/RuView `qe-reports` branch](https://github.com/proffesor-for-testing/RuView/tree/qe-reports/docs/qe-reports)
|
||||
|
|
@ -13,25 +13,38 @@ An 8-agent QE swarm analyzed ~305K lines across Rust, Python, C firmware, and Ty
|
|||
|
||||
Address the 15 prioritized issues from the QE analysis in three waves: P0 (immediate), P1 (this sprint), P2 (this quarter).
|
||||
|
||||
## Security P0 closure note (2026-06-13) — Rust sensing-server boundary
|
||||
|
||||
The three P0 security findings below were logged against the **Python v1** API
|
||||
(`archive/v1/src/…`). ADR-164 G11 re-scoped them to the *shipped* boundary:
|
||||
`wifi-densepose-sensing-server` (Rust). They were verified against the current
|
||||
Rust crate and closed on branch `fix/adr-080-sensing-server-security`. Each fix
|
||||
(or already-fixed finding) is pinned by a test that fails on the old behavior.
|
||||
**The Python v1 paths remain as-is** — v1 is archived and not the shipped
|
||||
surface; this closure governs the live Rust server only.
|
||||
|
||||
## P0 — Fix Immediately
|
||||
|
||||
### 1. Rate Limiter Bypass (Security HIGH)
|
||||
### 1. Rate Limiter Bypass / XFF spoofing (Security HIGH) — **RESOLVED (verified absent on Rust boundary)**
|
||||
|
||||
- **Location:** `archive/v1/src/middleware/rate_limit.py:200-206`
|
||||
- **Original location (v1):** `archive/v1/src/middleware/rate_limit.py:200-206`
|
||||
- **Problem:** Trusts `X-Forwarded-For` without validation. Any client bypasses rate limits via header spoofing.
|
||||
- **Fix:** Validate forwarded headers against trusted proxy list, or use connection IP directly.
|
||||
- **Rust verification (2026-06-13):** The Rust sensing-server has **no XFF-trusting control to bypass** — there is no IP-based rate-limiter and no IP-allowlist, and neither security middleware reads a forwarded header. `bearer_auth.rs` authenticates on the token alone (`require_bearer` inspects only the `AUTHORIZATION` header); `host_validation.rs` decides on the `Host` header only. A repo-wide grep for `x-forwarded-for|forwarded|peer_addr|client_ip|real-ip` over `wifi-densepose-sensing-server` returns nothing. The only "rate limiter" is the MQTT *sample-rate* gate (`mqtt/state.rs`), a per-entity publish throttle with no IP/header input.
|
||||
- **Resolution:** No code change needed (no vulnerable surface). Regression tests pin the immunity: `bearer_auth::tests::xff_header_never_affects_auth_decision` (spoofed XFF never flips a 401↔200 decision) and `host_validation::tests::forwarded_headers_never_bypass_host_allowlist` (spoofed `X-Forwarded-Host: localhost` never lets a foreign `Host: evil.com` past the allowlist). Residual: if an IP-based control is ever added, it must derive the peer from the socket (`ConnectInfo<SocketAddr>`) and only honor XFF from an explicit `--trusted-proxy` CIDR — captured as guidance in the test docstrings.
|
||||
|
||||
### 2. Exception Details Leaked in Responses (Security HIGH)
|
||||
### 2. Exception Details Leaked in Responses (Security HIGH, CWE-209) — **RESOLVED**
|
||||
|
||||
- **Location:** `archive/v1/src/api/routers/pose.py:140`, `stream.py:297`, +5 endpoints
|
||||
- **Problem:** Stack traces visible regardless of environment.
|
||||
- **Fix:** Wrap with generic error responses in production; log details server-side only.
|
||||
- **Original location (v1):** `archive/v1/src/api/routers/pose.py:140`, `stream.py:297`, +5 endpoints
|
||||
- **Problem:** Internal error/stack-trace detail serialized into client responses.
|
||||
- **Rust finding (2026-06-13):** Six handlers in `wifi-densepose-sensing-server/src/main.rs` serialized the internal error `Display` into the JSON body: `edge_registry_endpoint` returned a panicked `spawn_blocking` `JoinError` (`"task … panicked"`) in a `500` and the raw upstream error in a `503`; `delete_model`/`delete_recording`/`start_recording` returned `std::io::Error` strings (OS detail / path); `calibration_start`/`calibration_stop` returned the `FieldModel` error chain.
|
||||
- **Fix:** New `src/error_response.rs` 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":…}`) with no `panicked`, no file paths, no Debug chain. All six call-sites rewired. Pinned by `error_response::tests::internal_error_body_does_not_leak_detail` (leak-substring guard, verified to fail on the reverted old body) + 4 sibling tests.
|
||||
|
||||
### 3. WebSocket JWT in URL (Security HIGH, CWE-598)
|
||||
### 3. WebSocket JWT in URL (Security HIGH, CWE-598) — **RESOLVED (verified absent on Rust boundary)**
|
||||
|
||||
- **Location:** `archive/v1/src/api/routers/stream.py:74`, `archive/v1/src/middleware/auth.py:243`
|
||||
- **Original location (v1):** `archive/v1/src/api/routers/stream.py:74`, `archive/v1/src/middleware/auth.py:243`
|
||||
- **Problem:** Tokens in query strings visible in logs/proxies/browser history.
|
||||
- **Fix:** Use WebSocket subprotocol or first-message auth pattern.
|
||||
- **Rust verification (2026-06-13):** The Rust sensing-server never reads a token from the URL. `require_bearer` (`bearer_auth.rs`) inspects only the `Authorization` header; the WebSocket handlers (`ws_sensing_handler`/`ws_introspection_handler`/`ws_pose_handler`) take a bare `WebSocketUpgrade` with no `Query` extractor; the single `Query` in the crate (`EdgeRegistryParams`) is a non-secret `refresh` flag.
|
||||
- **Resolution:** No code change needed (no query-token path exists). Regression test `bearer_auth::tests::query_string_token_is_never_accepted` proves `?token=`/`?access_token=` in the URL never authenticates (stays `401`) while the same token in the header succeeds (`200`) — verified to fail if a query-token path is re-introduced.
|
||||
|
||||
### 4. Rust Tests Not in CI
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@
|
|||
| Relates to | ADR-134, ADR-136, ADR-139, ADR-140, ADR-143, ADR-144, ADR-146, ADR-147 |
|
||||
|
||||
> **Scope note:** ADR-147 deferred Cosmos WFM to "ADR-148" as an offline data generator.
|
||||
> That item is promoted to ADR-149. This ADR takes 148 to address the broader drone swarm
|
||||
> control architecture, which is the first consumer of ADR-147's OccWorld occupancy output.
|
||||
> That item is promoted to ADR-171 (the swarm-benchmarking/evaluation companion to this ADR;
|
||||
> renumbered from ADR-149 to resolve the ADR-149 duplicate-number collision). This ADR takes
|
||||
> 148 to address the broader drone swarm control architecture, which is the first consumer of
|
||||
> ADR-147's OccWorld occupancy output.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -874,9 +876,9 @@ validated; ITAR/EAR classification completed by export counsel.
|
|||
| GPS spoofing of full swarm simultaneously | Medium | Low | UWB mesh cross-check among all nodes; ≥ 3 nodes must agree on position to confirm |
|
||||
| 1000-UAV scale claims (not validated) | Low | High | SWARM+ demonstrated in simulation only; scale claims capped at 50 for production targets |
|
||||
|
||||
### 12.3 Open Issues (Forward to ADR-149)
|
||||
### 12.3 Open Issues (Forward to ADR-171)
|
||||
|
||||
- Cosmos WFM offline training data generation (deferred from ADR-147) — ADR-149
|
||||
- Cosmos WFM offline training data generation (deferred from ADR-147) — ADR-171
|
||||
- Fixed-wing hybrid platform support (endurance missions) — future ADR
|
||||
- Underwater-aerial cross-domain handoff protocol — future ADR
|
||||
- Quantum-enhanced task assignment (E6) — future ADR when hardware matures
|
||||
|
|
@ -998,4 +1000,4 @@ Implementation tracked at: https://github.com/ruvnet/RuView/issues/861
|
|||
|
||||
*ADR authored with research support from `ruflo-goals:deep-researcher` (2026-05-30).
|
||||
Implementation progress tracked by `ruflo-goals:horizon-tracker`.
|
||||
OccWorld integration basis: ADR-147. Next: ADR-149 (Cosmos WFM offline data generation).*
|
||||
OccWorld integration basis: ADR-147. Next: ADR-171 (Cosmos WFM offline data generation; renumbered from ADR-149).*
|
||||
|
|
|
|||
|
|
@ -195,13 +195,15 @@ The §2–§5 fixes are **ACCEPTED and committed**: dead CIR gate fixed, NaN byp
|
|||
- Evaluate the **diffusion CIR prior** (public weights, MEASURED) as an offline quality ceiling — *not* an edge target.
|
||||
- Bayesian multi-AP fusion (2512.02462, CLAIMED) — comparison only, pending released code.
|
||||
|
||||
### 7.4 Deferred Milestone-0 review findings (the ~45 not fixed here — explicit backlog)
|
||||
### 7.4 Deferred Milestone-0 review findings (explicit backlog)
|
||||
|
||||
Catalogued so nothing is silently dropped. Priority: **P1** correctness-adjacent, **P2** perf, **P3** clarity/style.
|
||||
|
||||
**Milestone-1 update (2026-06-13):** the **four P1 backlog items** (#1, #9, #10, #13) are now cleared — #1 and #10 **RESOLVED (MEASURED)**, #9 and #13 **RESOLVED-PARTIAL (DATA-GATED:** de-magicked + boundary-tested, operating values unchanged**)**. ~41 P2/P3 items remain deferred. Each fix is pinned by a regression test that fails on the old behaviour (commits `fd32f094a`, `4a9f2bcf4`, `d672fa602`, `5193f6369`); workspace `--no-default-features` green, Python proof unchanged (bit-exact).
|
||||
|
||||
| # | Module | Finding | Pri | Why deferred |
|
||||
|---|--------|---------|-----|--------------|
|
||||
| 1 | cir.rs ~937 | `phase_variance` uses **linear** variance on **wrapped** angles (doc says "variance of phase angles") — spuriously inflates near ±π | P1 | Used as the `> TAU` ghost-tap *guard*; a correct circular variance is bounded [0,1] and would need the threshold re-derived. Semantic change — defer with a real recalibration, don't risk a silent gate regression in a perf/correctness pass. |
|
||||
| 1 | cir.rs ~937 | `phase_variance` uses **linear** variance on **wrapped** angles (doc says "variance of phase angles") — spuriously inflates near ±π | P1 | **RESOLVED (`fd32f094a`) — metric MEASURED, threshold DATA-GATED.** Replaced with Mardia's circular variance V = 1 − R̄ ∈ **[0,1]**, invariant to the cluster's position on the circle (branch-cut artefact gone). Guard re-derived against the bounded metric via named const `GHOST_TAP_CIRCULAR_VARIANCE_MAX = 0.99` (fires only when R̄ ≤ 0.01 — essentially uniform phase). The **threshold value is DATA-GATED**: a clean single-path ramp also sweeps the circle, so V alone can't separate clean from unsanitized without labelled frames — the default is deliberately conservative (strictly more permissive at the wrap boundary than the buggy linear guard). Fails-on-old: `phase_variance_circular_not_fooled_by_branch_cut` (old linear variance > TAU on wrap-straddling phases while circular V≈0, guard no longer trips), `phase_variance_circular_is_bounded_and_extremal`. |
|
||||
| 2 | calibration.rs ~311 | `subtract_in_place` had a vacuous `if active_input {ki} else {ki}` branch implying a full-FFT→bin remap that didn't exist | P3 | **Resolved here** (branch removed, sequential-convention documented to match the sibling `extract_first_stream`). Listed for visibility — behavior unchanged. |
|
||||
| 3 | spectrogram.rs / bvp.rs | FFT planner built once-per-call (already amortized across frames) | P2 | Marginal vs the per-frame PSD site; cache if these become hot. |
|
||||
| 4 | features.rs ~347 | Doppler FFT planner planned once per call, reused across subcarriers | P2 | Already amortized within the call. |
|
||||
|
|
@ -209,11 +211,11 @@ Catalogued so nothing is silently dropped. Priority: **P1** correctness-adjacent
|
|||
| 6 | tomography.rs | ISTA L1 solver re-allocates voxel buffers per solve | P2 | Bench first. |
|
||||
| 7 | pose_tracker.rs | Kalman gain matrices reallocated per update | P2 | Bench first. |
|
||||
| 8 | field_model.rs | SVD recomputed on every perturbation extract | P2 | Incremental SVD is a real project, not a micro-fix. |
|
||||
| 9 | coherence.rs / coherence_gate.rs | Z-score thresholds are magic constants, untested at boundaries | P1 | Needs labelled data to set defensible thresholds. |
|
||||
| 10 | longitudinal.rs | Welford update not numerically guarded for n=0 | P1 | Add `n>=1` guard + test (same family as §4). |
|
||||
| 9 | coherence.rs / coherence_gate.rs | Z-score thresholds are magic constants, untested at boundaries | P1 | **RESOLVED-PARTIAL (`5193f6369`) — DATA-GATED.** De-magicked `classify_drift` (`DRIFT_STABLE_SCORE=0.85`, `DRIFT_STEP_CHANGE_MAX_STALE=10`) and the `coherence_gate.rs` defaults (`DEFAULT_ACCEPT_THRESHOLD`/`…REJECT…`/`…MAX_STALE_FRAMES`/`…PREDICT_ONLY_NOISE`) into named, documented consts marked EMPIRICAL DEFAULT; added at/just-below/just-above boundary tests (`classify_drift_*_boundary`) + `*_consts_unchanged_from_literals`. **Operating values explicitly NOT changed** — defensible values still need labelled stable/drifting traces. The gate already exposed these via `GatePolicyConfig` (config seam). |
|
||||
| 10 | longitudinal.rs | Welford update not numerically guarded for n=0 | P1 | **RESOLVED (`4a9f2bcf4`) — MEASURED.** The shared `WelfordStats` (`field_model.rs`, consumed by longitudinal.rs) `count < 2` guards already prevent the n=0 NaN / n=1 div0 / `(count−1)` underflow, but the boundary was untested. Added `welford_finite_at_n0_and_n1` (finite + documented 0.0 sentinel at n=0/n=1). Fails-on-old proof: removing the `sample_variance` guard makes the test panic with "attempt to subtract with overflow" at the `(count − 1)` underflow. |
|
||||
| 11 | cross_room.rs | Fingerprint hash collisions unhandled | P2 | Low collision prob; needs design. |
|
||||
| 12 | gesture.rs | `euclidean_distance` no length-mismatch guard | P3 | Caller-enforced; add `debug_assert`. |
|
||||
| 13 | adversarial.rs | Gini/consistency thresholds are magic constants | P1 | Same labelled-data dependency as #9. |
|
||||
| 13 | adversarial.rs | Gini/consistency thresholds are magic constants | P1 | **RESOLVED-PARTIAL (`d672fa602`) — DATA-GATED.** Lifted the bare literals in `check`/`check_consistency` (`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_*`) into named, documented consts marked EMPIRICAL DEFAULT; added at/just-below/just-above boundary tests (`energy_ratio_high_boundary`, `energy_ratio_low_boundary`, `field_model_gini_boundary`, `consistency_active_fraction_boundary`) + `tuning_consts_unchanged_from_literals`. **Operating values explicitly NOT changed** — defensible values still need labelled spoofed/clean CSI (Wi-Spoof, §6.2/§7.3). Bumping a const fails a boundary test (verified). |
|
||||
| 14 | cir.rs | `fft_operator` path changes the witness hash (documented) — no test that it's *numerically close* to dense | P2 | Add a tolerance test. |
|
||||
| 15 | multistatic.rs | `cir_gate_coherence` only estimates the **first** node/channel; multi-node CIR consensus unused | P2 | Design item (which node's CIR is authoritative?). |
|
||||
| 16 | phase_align.rs | Iterative LO offset estimation has no convergence cap test | P2 | Add iteration-cap test. |
|
||||
|
|
@ -223,12 +225,12 @@ Catalogued so nothing is silently dropped. Priority: **P1** correctness-adjacent
|
|||
| 20 | spectrogram.rs | `compute_multi_subcarrier_spectrogram` re-plans per subcarrier via `compute_spectrogram` | P2 | Hoist the planner (relates to #3). |
|
||||
| 21–45 | (assorted) | Remaining clarity/doc/magic-constant/missing-boundary-test findings across `ruvsense/*`, `features.rs`, `motion.rs` | P3 | Bulk-addressable in a dedicated "test-the-boundaries + de-magic-constant" follow-up; not high-leverage individually. |
|
||||
|
||||
> **Horizon-ledger one-liner.** Milestone-0 DONE: dead CIR gate (FIXED+proved), NaN/inf adversarial bypass (FIXED+proved), divide-by-(n−1) window trio (FIXED+proved), calibration dead-branch (FIXED), PSD FFT-planner cache (MEASURED), DTW band (MEASURED). DEFERRED to follow-up: the ~45 findings in §7.4 (P1: phase_variance circular bug #1, Welford guard #10, threshold magic-constants #9/#13; P2/P3: the rest) — none silently dropped.
|
||||
> **Horizon-ledger one-liner.** Milestone-0 DONE: dead CIR gate (FIXED+proved), NaN/inf adversarial bypass (FIXED+proved), divide-by-(n−1) window trio (FIXED+proved), calibration dead-branch (FIXED), PSD FFT-planner cache (MEASURED), DTW band (MEASURED). **Milestone-1 DONE (2026-06-13): all four P1 backlog items cleared — circular phase variance #1 (RESOLVED/MEASURED metric, DATA-GATED threshold), Welford n=0 guard #10 (RESOLVED/MEASURED), threshold magic-constants #9 & #13 (RESOLVED-PARTIAL/DATA-GATED — de-magicked + boundary-tested, values unchanged).** DEFERRED to follow-up: the ~41 remaining P2/P3 findings in §7.4 — none silently dropped.
|
||||
|
||||
---
|
||||
|
||||
## 8. Consequences
|
||||
|
||||
- **Positive:** the ADR-134 CIR gate is alive for the first time in production; the adversarial detector can no longer be NaN-bypassed; three latent divide-by-zero NaN sources are gone; the per-frame PSD path and gesture DTW are measurably faster with bit-identical output; the SOTA landscape and a concrete LISTA-for-CIR roadmap are graded and recorded.
|
||||
- **Negative / honest limits:** `canonical56()` models the canonical grid as a contiguous 56-tone band — a reasonable physical interpretation of a *resampled* grid, but not a literal hardware tone map; the CIR gate still uses only the first node's CIR (#15); the `phase_variance` circular bug (#1) remains until it can be re-thresholded with data.
|
||||
- **Negative / honest limits:** `canonical56()` models the canonical grid as a contiguous 56-tone band — a reasonable physical interpretation of a *resampled* grid, but not a literal hardware tone map; the CIR gate still uses only the first node's CIR (#15). The `phase_variance` **metric** is now correct (Mardia circular variance, Milestone-1 #1), so the branch-cut false-trip is gone — but its ghost-tap **threshold** (`GHOST_TAP_CIRCULAR_VARIANCE_MAX = 0.99`) is a conservative DATA-GATED default, not a calibrated operating point, and still awaits labelled sanitized/unsanitized frames to tune. Likewise the de-magicked coherence/adversarial thresholds (#9/#13) keep their pre-existing empirical values pending labelled calibration.
|
||||
- **Neutral:** no public API removed; `with_cir_ht20()` kept (warned); files stay scoped; new bench is additive.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
## Context
|
||||
|
||||
The corpus has grown to **162 ADR entries across 156 distinct files** (ADR-001 through ADR-163, plus 6 duplicate-number collisions). It now spans nine subsystems — signal/DSP, NN/training, ESP32 firmware, RuvSense multistatic, RuView desktop, Cognitum cogs, HOMECORE (HA reimplementation), BFLD privacy, and the streaming engine — written over roughly a year by many agent-driven sessions.
|
||||
The corpus has grown to **162 ADR entries across 156 distinct files** (ADR-001 through ADR-171; the 5 duplicate-number collisions / 6 displaced files originally noted here were RESOLVED by renumbering the displaced files to ADR-166…171 — see Gap Register G1). It now spans nine subsystems — signal/DSP, NN/training, ESP32 firmware, RuvSense multistatic, RuView desktop, Cognitum cogs, HOMECORE (HA reimplementation), BFLD privacy, and the streaming engine — written over roughly a year by many agent-driven sessions.
|
||||
|
||||
Two forces motivate a corpus-wide gap analysis *now*:
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ Counts are approximate (`~`) where a status string is non-canonical or dual-valu
|
|||
| Proposed (incl. conditional/research-only) | ~88 | partial | ~50 |
|
||||
| Superseded | 1 (ADR-002) | proposed-only | ~64 |
|
||||
| Rejected | 1 (ADR-098) | stale-or-contradicted | 3 (029/030/031) |
|
||||
| Missing / no Status header | 3 (ADR-147-proof, ADR-052-ddd, ADR-134) | unknown | 5 (034/044/052-ddd/147-proof/…) |
|
||||
| Missing / no Status header | 3 (ADR-168-proof [was 147], ADR-167-ddd [was 052], ADR-134) | unknown | 5 (034/044/167-ddd/168-proof/…) |
|
||||
| Mixed/dual status in one ADR | 3 (115, 149×2, 133) | superseded | 1 (ADR-002) |
|
||||
|
||||
**Headline:** ~114 of 162 ADRs (≈70%) are decisions that never fully landed (proposed-only + partial + stale + unknown). The dominant failure mode is **stale Status headers**, not abandoned work.
|
||||
|
|
@ -50,8 +50,8 @@ Severity: CRITICAL (corpus integrity / tooling-breaking / life-safety / security
|
|||
|
||||
| ID | Gap | Severity | Affected ADRs | Recommended action |
|
||||
|----|-----|----------|---------------|--------------------|
|
||||
| G1 | 6 duplicate ADR numbers (two ADRs answer to one number; breaks index/`/adr` tooling) | CRITICAL | 050×2, 052×2, 147×3, 148×2, 149×2, 134 (identity split) | renumber 2-of-3 at 147, 1 each at 050/148/149; demote 052-ddd to appendix; resolve 134 identity |
|
||||
| G2 | 3 files with no Status header (cannot triage) — **INVESTIGATED in `docs/adr-gap-remediation-1`: only 2 genuinely lack one, both owner-gated** | CRITICAL | 147-benchmark-proof, 052-ddd-appendix, ~~134-CIR~~ | add canonical `## Status`; relocate 147-proof to `benchmarks/`; label 052-ddd as appendix — **NOTE: ADR-134-CIR DOES have a Status (`\| Status \| Proposed \|` in its header table) — mislabeled here. The two real misses (147-benchmark-proof, 052-ddd) are both inside owner-gated duplicate-number collisions (147×3, 052×2), so left untouched pending owner. The early ADRs (048/049/068/070 etc.) use `\| Status \|` not `\| **Status** \|` — different-format-but-present, not missing. Net: 0 headers added.** |
|
||||
| G1 | ~~6 duplicate ADR numbers (two ADRs answer to one number; breaks index/`/adr` tooling)~~ **RESOLVED (duplicate-number item)** | CRITICAL | 050×2, 052×2, 147×3, 148×2, 149×2; 134 (identity split, separate) | ~~renumber 2-of-3 at 147, 1 each at 050/148/149; demote 052-ddd to appendix; resolve 134 identity~~ **DONE: displaced files renumbered to the next free numbers (166–171), keepers = first-committed file per number (date ties broken by inbound-ref count / parent-appendix relationship): 050 keeps provisioning-tool-enhancements → quality-engineering-security-hardening = ADR-166; 052 keeps tauri-desktop-frontend → ddd-bounded-contexts appendix = ADR-167 (still linked to parent 052); 147 keeps nvidia-cosmos/OccWorld → benchmark-proof = ADR-168, adam-mode-light-theme = ADR-169; 148 keeps drone-swarm-control-system → yoga-mode-pose-system = ADR-170; 149 keeps public-community-leaderboard-huggingface → swarm-benchmarking-evaluation-methodology = ADR-171. In-file headers, intra-file self-refs, all inbound cross-references (README index, census, lens-findings, user-guide, CHANGELOG, proof-of-capabilities, research docs), and this register updated. `ls docs/adr/ADR-*.md | … | uniq -d` is now EMPTY. The ADR-134 identity split is NOT a filename collision; resolved separately under G3 (→ ADR-165).** |
|
||||
| G2 | 3 files with no Status header (cannot triage) — **INVESTIGATED in `docs/adr-gap-remediation-1`: only 2 genuinely lack one, both owner-gated** | CRITICAL | ADR-168-benchmark-proof (was 147), ADR-167-ddd-appendix (was 052), ~~134-CIR~~ | add canonical `## Status`; relocate ADR-168-proof to `benchmarks/`; label ADR-167-ddd as appendix — **NOTE: ADR-134-CIR DOES have a Status (`\| Status \| Proposed \|` in its header table) — mislabeled here. The two real misses (ADR-168-benchmark-proof [was 147], ADR-167-ddd [was 052]) were inside the owner-gated duplicate-number collisions (147×3, 052×2); those collisions are now resolved (G1) but the missing Status headers themselves remain owner-gated, so left untouched pending owner. The early ADRs (048/049/068/070 etc.) use `\| Status \|` not `\| **Status** \|` — different-format-but-present, not missing. Net: 0 headers added.** |
|
||||
| G3 | ~~Shipped crates cite a non-existent or wrong-identity governing ADR~~ **RESOLVED in `docs/adr-gap-remediation-1`** | CRITICAL | homecore-recorder→"ADR-132" (no file); homecore-migrate→"ADR-134" (file is CIR) | ~~write-missing-ADR (HOMECORE-RECORDER, HOMECORE-MIGRATE)~~ DONE: wrote ADR-132 (recorder, Accepted) + ADR-165 (migrate, Accepted — P1 scaffold); repointed migrate's ADR-134 refs → ADR-165 |
|
||||
| G4 | Anti-slop retractions: accuracy/security/function provably false until sweep landed | CRITICAL | 155, 154, 079, 161 (see Contradictions) | already fixed in-code by 154/155/161/162; this ledger records the retraction |
|
||||
| G5 | ~~10 streaming-engine ADRs marked `Proposed` while §Impl-Status reports Built + commits + tests~~ **RESOLVED in `docs/adr-gap-remediation-1`** | HIGH | 136–145 | ~~mark-stale → "Accepted — partial (integration glue pending)" (one batch)~~ DONE: all 10 (136–145) flipped to "Accepted — partial"; each retains its commit-pinned Implementation-Status note. NB: notes describe *building blocks built + tested*, **not** live-path integration — "partial" is the honest label, not full "Accepted" |
|
||||
|
|
@ -60,7 +60,7 @@ Severity: CRITICAL (corpus integrity / tooling-breaking / life-safety / security
|
|||
| G8 | ADR-002 supersession not reciprocated by successors; 5 children stranded | HIGH | 002→016/017; children 003/007/008/009/010 | reconcile-docs (add reciprocal language or downgrade); split 002 to "partially superseded" |
|
||||
| G9 | Streaming-engine integrator crate has no governing ADR (composition/back-pressure/live-path seam) | HIGH | wifi-densepose-engine (composes 135–146) | write-missing-ADR |
|
||||
| G10 | CLAUDE.md doc-vs-header drift (doc says one status, header another) | HIGH | 017, 024, 027, 072, 152 | reconcile-docs |
|
||||
| G11 | Open security HIGH findings, gate FAILED, never marked done | HIGH | 080 (XFF bypass, leaked stack traces, JWT-in-URL CWE-598) | implement (sensing-server boundary — NOT covered by HOMECORE sweep 161/162) |
|
||||
| G11 | ~~Open security HIGH findings, gate FAILED, never marked done~~ **RESOLVED (2026-06-13, branch `fix/adr-080-sensing-server-security`)** | HIGH | 080 (XFF bypass, leaked stack traces, JWT-in-URL CWE-598) | ~~implement (sensing-server boundary — NOT covered by HOMECORE sweep 161/162)~~ DONE: verified all three against the *current Rust* `wifi-densepose-sensing-server`. **#2 leaked errors** was the one live exposure — 6 `main.rs` handlers serialized internal `Display`/`JoinError` into response bodies; fixed via a new `error_response` module (generic body + correlation id, detail logged server-side only). **#1 XFF** and **#3 JWT-in-URL** were verified *absent* on the Rust boundary (no IP-rate-limit/allowlist reads XFF; token is header-only, WS handlers take no query token) and pinned with regression tests that fail if either is re-introduced. ADR-080 P0 §1–3 marked RESOLVED. |
|
||||
| G12 | ADR-052→054 edge unacknowledged by successor; likely mis-modeled (impl, not replacement) | MEDIUM | 052-tauri, 054 | reconcile-docs (054 is the impl plan *for* 052, not a replacement) |
|
||||
| G13 | Capability governed only by remediation/deploy ADR, no creation/architecture ADR | MEDIUM | wasm-edge (only 160/163); occworld-candle (147 blessed Python path only); pointcloud (094 = viewer deploy only) | write-missing-ADR (taxonomy/ABI for wasm-edge; Candle backend swap; pointcloud data contract) |
|
||||
| G14 | Conflicting decisions on one topic, none superseding the others | MEDIUM | person-count 037/075/103; PQ-sign 007/109; fed key-exchange 107/108; provisioning 050/060/052; audit 010/028; RVF-WASM 009-vs-shipped | reconcile (pick one, supersede the rest) |
|
||||
|
|
@ -104,7 +104,7 @@ The ADR-154–163 sweep was narrowly scoped. The two largest **capability** gaps
|
|||
|
||||
- **CRITICAL — Camera-teacher training validation (ADR-079 / 072 / 150).** P7–P9 Pending; blocker is a real synchronized camera+ESP32 paired-capture session + GPU training on the fleet (ruvultra RTX 5080). Cross-subject collapse (11.6%) is data-gated on a heterogeneous multi-subject CSI dataset, per ADR-150 §F3 / ADR-152 F3 (the lever is *more data*, not capacity). Accepted-on-paper, not proven.
|
||||
- **HIGH — Federation + BFLD privacy chains (ADR-105–109, 118–125).** All Proposed-only, ACs unchecked. Blockers: KIT BFId dataset (121), Pi5/Nexmon CBFR capture hardware (123 — ESP32 structurally cannot sniff CBFR), Soul-Signature + cog-ha-matter (122/125). The privacy control *plane* (ADR-141) is built; the *capture/scoring* chain it gates is not.
|
||||
- **HIGH — Sensing-server security (ADR-080).** Distinct from the HOMECORE boundary the sweep fixed; XFF bypass / stack-trace leakage / JWT-in-URL remain open.
|
||||
- ~~**HIGH — Sensing-server security (ADR-080).** Distinct from the HOMECORE boundary the sweep fixed; XFF bypass / stack-trace leakage / JWT-in-URL remain open.~~ **RESOLVED (2026-06-13, G11):** verified against the current Rust sensing-server — stack-trace leakage was the one live finding (fixed via `error_response` generic bodies); XFF bypass and JWT-in-URL were verified absent and regression-pinned. See ADR-080 P0 §1–3.
|
||||
- **MEDIUM — gold-standard deferrals (model to follow):** ADR-163 (ESP32 on-hardware latency UNMEASURED), ADR-160 (medical/affect/weapon NOT validated, relabelled), ADR-158 (RF-through-rubble + learned counter DATA-GATED). Code is real, the claim is withheld pending absent hardware/labelled data — labels are honest.
|
||||
- **MEDIUM — purely hardware/data-gated Proposed decisions (no overreach):** ADR-023, 027, 042, 063/064, 065/066, 070, 073/078, 083, 086, 091, 103, 110 (HE-CSI needs ESP-IDF ≥5.5), 113, 114, 134/135, 143-v2, 144. *needs verification* where flags rely on downstream prose rather than direct file inspection.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# ADR-050: Quality Engineering Response — Security Hardening & Code Quality
|
||||
# ADR-166: Quality Engineering Response — Security Hardening & Code Quality
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
# ADR-052 Appendix: DDD Bounded Contexts — Tauri Desktop Frontend
|
||||
# ADR-167 Appendix: DDD Bounded Contexts — Tauri Desktop Frontend
|
||||
|
||||
> Appendix to [ADR-052](ADR-052-tauri-desktop-frontend.md). Renumbered from ADR-052
|
||||
> to ADR-167 to resolve the ADR-052 duplicate-number collision (per ADR-164 Gap Register
|
||||
> G1); the parent decision remains ADR-052.
|
||||
|
||||
This document maps out the domain model for the RuView Tauri desktop application
|
||||
described in ADR-052. It defines bounded contexts, their aggregates, entities,
|
||||
|
|
@ -158,7 +162,7 @@ Represents an over-the-air firmware update to a running node.
|
|||
| `target_node` | `MacAddress` | Target node MAC |
|
||||
| `target_ip` | `IpAddr` | Target node IP |
|
||||
| `firmware` | `FirmwareBinary` | The binary being pushed |
|
||||
| `psk` | `Option<SecureString>` | PSK for authentication (ADR-050) |
|
||||
| `psk` | `Option<SecureString>` | PSK for authentication (ADR-166) |
|
||||
| `phase` | `OtaPhase` | Uploading / Rebooting / Verifying / Done / Failed |
|
||||
| `progress` | `Progress` | Upload progress |
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# ADR-147 Benchmark Proof — OccWorld on RTX 5080
|
||||
# ADR-168 Benchmark Proof — OccWorld on RTX 5080
|
||||
Date: 2026-05-29
|
||||
Hardware: NVIDIA GeForce RTX 5080 (15.47 GB VRAM), CUDA 12.8
|
||||
Model: OccWorld TransVQVAE (random weights — pre-domain-fine-tuning baseline)
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
# ADR-169: adam-mode — light theme toggle for the three.js realtime demo
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-06-02 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **adam-mode** |
|
||||
| **Scope** | `examples/three.js/demos/05-skinned-realtime.html` (primary), demos 01–04 (follow-on) |
|
||||
| **Relates to** | ADR-019 (sensing-only UI), ADR-035 (live sensing UI accuracy) |
|
||||
| **Tracking issue** | none yet |
|
||||
|
||||
---
|
||||
|
||||
## 1. Context
|
||||
|
||||
`examples/three.js/demos/05-skinned-realtime.html` (build stamp `2026-05-15-fps-tune`) is the live MediaPipe → Mixamo retargeting + ESP32 CSI overlay demo. It currently ships a single, opinionated **dark theme**:
|
||||
|
||||
- Body `--bg: #050507` (near-black), `--text: #d8c69a` (warm beige).
|
||||
- Amber accents (`--amber: #ffb840`, `--amber-hot: #ffe09f`) on panels and controls.
|
||||
- Two full-screen overlays: a radial-vignette `.overlay-frame` and a 50%-opacity CRT-style `.scanlines` layer.
|
||||
- Three.js scene matches: `scene.background = new THREE.Color(0x050507)` and `scene.fog = new THREE.FogExp2(0x050507, 0.06)` (lines 269–270).
|
||||
|
||||
The dark/amber CRT aesthetic is intentional for screen-recording and "command-centre" feel, but it has real failure modes:
|
||||
|
||||
1. **Daylight visibility** — Demoing the live capture on a laptop in a sunlit room is unreadable; the dark background absorbs ambient glare and the amber-on-dark contrast disappears.
|
||||
2. **Recording for embedded/print contexts** — When the demo's screen is captured for documentation, blog posts, or HA blueprints, the dark theme bleeds into surrounding white content and looks heavy.
|
||||
3. **Accessibility** — A subset of users with light-sensitive retinas (the inverse of typical photophobia) report the high amber-on-near-black combination strains them; high-contrast light themes are easier.
|
||||
4. **Operator pairing with a light-mode IDE** — Many operators run a light-mode browser alongside a dark-mode IDE and want the demo to match the browser, not the IDE.
|
||||
|
||||
A toggle is the right answer because none of these reasons are universal — some sessions and some users want each mode.
|
||||
|
||||
### 1.1 What this ADR is *not*
|
||||
|
||||
- Not a redesign. The amber accent stays; only the surface colours and overlays swap. The information density, panel layout, and three.js scene geometry are unchanged.
|
||||
- Not a multi-theme system. We add exactly two themes: the existing dark (default, unnamed) and **adam-mode** (light). Future themes would need a new ADR.
|
||||
- Not a backend / data-model change. Pure presentation.
|
||||
- Not yet propagated to demos 01–04. Those follow-on after adam-mode lands on demo 05 and is validated.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Add a **client-side theme toggle** to `05-skinned-realtime.html` that switches between the existing dark theme and a new light theme called **adam-mode**, driven by a `data-theme="adam"` attribute on `<body>` plus a sibling `:root[data-theme="adam"]` CSS block that re-defines the existing custom properties. A new toggle button in the existing `#helpers` panel switches between modes and persists the choice in `localStorage` under the key `ruview.theme`.
|
||||
|
||||
### 2.1 CSS — the colour swap
|
||||
|
||||
Add immediately after the existing `:root { ... }` block in `<style>`:
|
||||
|
||||
```css
|
||||
:root[data-theme="adam"] {
|
||||
--bg: #f6f2ea;
|
||||
--bg-panel: rgba(252, 250, 246, 0.92);
|
||||
--amber: #b8741a; /* deeper amber, readable on cream */
|
||||
--amber-hot: #8a5612; /* deepest amber for emphasis text */
|
||||
--cyan: #1a6f8a; /* slate cyan */
|
||||
--magenta: #a8348a; /* slate magenta */
|
||||
--text: #2a241c; /* near-black warm */
|
||||
--text-mute: #7a6f5d; /* warm grey */
|
||||
--green: #1f7a32; /* forest green */
|
||||
--red: #b03a1a; /* burnt sienna */
|
||||
--border: rgba(184, 116, 26, 0.28);
|
||||
}
|
||||
```
|
||||
|
||||
Every existing element already reads from these custom properties, so the swap is automatic for panels, text, borders, and bar fills. No per-element CSS rewrites required.
|
||||
|
||||
### 2.2 Overlay handling
|
||||
|
||||
The vignette and scanlines are dark-theme aesthetics. In adam-mode they would muddy the cream background. Two new rules:
|
||||
|
||||
```css
|
||||
:root[data-theme="adam"] .overlay-frame {
|
||||
background:
|
||||
radial-gradient(ellipse at center, transparent 70%, rgba(184,116,26,0.10) 100%),
|
||||
linear-gradient(180deg, rgba(184,116,26,0.06) 0%, transparent 18%, transparent 82%, rgba(184,116,26,0.08) 100%);
|
||||
}
|
||||
:root[data-theme="adam"] .scanlines {
|
||||
opacity: 0.15;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
```
|
||||
|
||||
The vignette is preserved but inverted in colour and lightened; scanlines drop to 15 % opacity and switch from `overlay` to `multiply` blend so they read as faint paper texture rather than CRT lines.
|
||||
|
||||
### 2.3 Three.js scene reactivity
|
||||
|
||||
Two scene colours are hard-coded at construction (lines 269–270). Replace them with a function call that reads the current theme:
|
||||
|
||||
```js
|
||||
function themeSceneColors(theme) {
|
||||
return theme === 'adam'
|
||||
? { bg: 0xf6f2ea, fogDensity: 0.025 }
|
||||
: { bg: 0x050507, fogDensity: 0.06 };
|
||||
}
|
||||
function applySceneTheme(theme) {
|
||||
const c = themeSceneColors(theme);
|
||||
scene.background = new THREE.Color(c.bg);
|
||||
scene.fog = new THREE.FogExp2(c.bg, c.fogDensity);
|
||||
renderer.setClearColor(c.bg, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
Called once after `renderer` is constructed, then again from the toggle handler.
|
||||
|
||||
`scene.fog` density drops in adam-mode because exponential fog on a light background reads as "haze" much more strongly than on dark — 0.06 → 0.025 keeps the falloff visible without losing the figure into the background.
|
||||
|
||||
### 2.4 UI toggle
|
||||
|
||||
Add to the `#helpers` panel (top of its labels list):
|
||||
|
||||
```html
|
||||
<label class="theme-toggle">
|
||||
<input type="checkbox" id="adam-mode-toggle">
|
||||
<span>adam-mode (light)</span>
|
||||
<span class="swatch" style="background: var(--amber)"></span>
|
||||
</label>
|
||||
```
|
||||
|
||||
Handler:
|
||||
|
||||
```js
|
||||
const THEME_KEY = 'ruview.theme';
|
||||
const root = document.documentElement;
|
||||
const toggle = document.getElementById('adam-mode-toggle');
|
||||
|
||||
function applyTheme(theme) {
|
||||
if (theme === 'adam') {
|
||||
root.setAttribute('data-theme', 'adam');
|
||||
toggle.checked = true;
|
||||
} else {
|
||||
root.removeAttribute('data-theme');
|
||||
toggle.checked = false;
|
||||
}
|
||||
applySceneTheme(theme);
|
||||
try { localStorage.setItem(THEME_KEY, theme); } catch (_) {}
|
||||
}
|
||||
|
||||
const initialTheme = (() => {
|
||||
try { return localStorage.getItem(THEME_KEY) || 'dark'; }
|
||||
catch (_) { return 'dark'; }
|
||||
})();
|
||||
applyTheme(initialTheme);
|
||||
|
||||
toggle.addEventListener('change', e => {
|
||||
applyTheme(e.target.checked ? 'adam' : 'dark');
|
||||
});
|
||||
```
|
||||
|
||||
### 2.5 Why "adam-mode" as the codename
|
||||
|
||||
The user picked the name. It is a project-specific brand — distinct from the generic "light mode" terminology that other modes (`--theme=high-contrast`, `--theme=print`) may eventually need. Keeping a codename makes the toggle searchable in the codebase, the localStorage key portable across the demo set, and avoids ambiguity if dark itself is later renamed.
|
||||
|
||||
The string `"adam"` is the only literal value the `data-theme` attribute and the `localStorage` key ever take. `"dark"` is the implicit default (no attribute, no stored value).
|
||||
|
||||
### 2.6 Rejected alternatives
|
||||
|
||||
| Alternative | Rejected because |
|
||||
|---|---|
|
||||
| Use `prefers-color-scheme: light` only, no toggle | Operators frequently want the opposite of their OS preference for screen-recording or daylight desk use. Auto-only frustrates the actual use case. |
|
||||
| Ship two separate HTML files (`05-…-dark.html`, `05-…-light.html`) | Doubles maintenance for every future demo edit. No path to per-session toggle. |
|
||||
| Build a full multi-theme system with a runtime registry | Premature. Two themes don't need a registry; the `data-theme="adam"` attribute is the registry. |
|
||||
| Use Tailwind / DaisyUI / a CSS framework | Demos are intentionally stand-alone single-file HTML for portability. No build step exists; adding one for theming is wrong shape. |
|
||||
| Adopt the cognitum-v0 / HOMECORE design tokens (`--hc-*` from `examples/frontend/`) | That design system is dark-only by intent (ADR-131). adam-mode is the light counterpart needed in *demo* contexts, not HA dashboard contexts. |
|
||||
| Make adam-mode the default | Breaks the dark-aesthetic recording context this demo was originally built for. Default stays dark; toggle stays opt-in. |
|
||||
|
||||
## 3. Consequences
|
||||
|
||||
### 3.1 Positive
|
||||
|
||||
- Demo is usable in daylight, in printed documentation, on light-mode browsers, and by users who find the dark-amber combination fatiguing.
|
||||
- Toggle persists across reloads via `localStorage` — set once, sticks.
|
||||
- No structural change to information density, panel layout, or three.js scene geometry. Operators familiar with the dark theme can switch and still find every readout in the same place.
|
||||
- Implementation is contained — a single `<style>` block addition, a single button, a ~25-line JS handler, and a swap of two scene-construction lines.
|
||||
|
||||
### 3.2 Negative
|
||||
|
||||
- Two themes to maintain. Any future colour change requires updating both `:root` blocks. Mitigated by keeping the existing custom-property names — adam-mode's values are the only edits.
|
||||
- The vignette + scanlines lose some of the CRT charm in adam-mode. Tradeoff accepted by design.
|
||||
- One additional `localStorage` slot consumed per origin (`ruview.theme`).
|
||||
- The amber accent in adam-mode (`#b8741a`) is visibly different from the dark-mode amber (`#ffb840`) — they share the same CSS variable name but a screenshot from each mode is not pixel-comparable. This is the correct call for accessibility (the bright amber is unreadable on cream) but does mean side-by-side comparisons need both screenshots labelled.
|
||||
|
||||
### 3.3 Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|---|---|---|
|
||||
| Future demo edits update one `:root` block and forget the other | Medium | A lint script in `scripts/` could grep both blocks for matching key sets; documented as P2 follow-up. |
|
||||
| `localStorage` blocked by privacy settings | Low | All accesses are wrapped in try/catch; falls back to dark. |
|
||||
| Three.js fog density of 0.025 still washes out the model on adam-mode | Low | Empirically tuned during implementation; if it does, drop to 0.015 or remove fog entirely in adam-mode. |
|
||||
| User on a high-DPI display sees scanlines as visible paper texture even at 15 % opacity | Low | If reported, drop to 8 % or hide scanlines entirely in adam-mode. |
|
||||
|
||||
## 4. Implementation plan
|
||||
|
||||
Tiny scope — single file. No swarm needed.
|
||||
|
||||
1. Add `:root[data-theme="adam"]` CSS block and the two overlay overrides.
|
||||
2. Refactor scene background + fog into the two helper functions `themeSceneColors()` and `applySceneTheme()`.
|
||||
3. Add `<label>` markup and handler script.
|
||||
4. Verify in a browser at http://127.0.0.1:8765/examples/three.js/demos/05-skinned-realtime.html — toggle on, reload, confirm adam-mode persists; toggle off, reload, confirm dark persists.
|
||||
5. Smoke-screenshot both modes; commit.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Toggle checkbox visible in `#helpers` panel.
|
||||
- Clicking the toggle swaps colours within one frame.
|
||||
- Reload preserves last choice.
|
||||
- Three.js scene background follows the toggle (no dark frame visible behind a light HUD or vice-versa).
|
||||
- Existing dark-theme appearance is byte-identical when toggle is off.
|
||||
|
||||
## 5. Test plan
|
||||
|
||||
- Manual visual check in two themes (no automated visual regression — demos aren't in the CI test loop today).
|
||||
- `view-source` confirms the new CSS block, the toggle markup, and the handler are present.
|
||||
- DevTools `localStorage` shows `ruview.theme` after a toggle.
|
||||
- Three.js inspector (or a `console.log(scene.background.getHexString())`) confirms scene colour swap.
|
||||
|
||||
## 6. Follow-on work (out of scope for this ADR)
|
||||
|
||||
- Roll adam-mode into demos 01–04. Each demo has its own `<style>` block; the same `data-theme="adam"` selector and the same JS handler can be copied.
|
||||
- Honor `prefers-color-scheme: light` on first load *if* `localStorage` has no stored choice. Trivial three-line addition.
|
||||
- Add a high-contrast theme for accessibility (separate ADR).
|
||||
- Lint script that asserts both `:root` blocks declare the same custom-property names.
|
||||
|
||||
## 7. Related ADRs
|
||||
|
||||
- [ADR-019](ADR-019-sensing-only-ui-mode.md) — sensing-only UI mode (Gaussian splats viewer)
|
||||
- [ADR-035](ADR-035-live-sensing-ui-accuracy.md) — live sensing UI accuracy norms (which this demo follows)
|
||||
- [ADR-131](docs/adr/ADR-131-...) — HOMECORE / cognitum-v0 design tokens (dark-only, separate context)
|
||||
|
|
@ -0,0 +1,643 @@
|
|||
# ADR-170: yoga-mode — pose detection, classification, and scoring for the three.js realtime demo
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-06-02 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **yoga-mode** |
|
||||
| **Scope** | `examples/three.js/demos/05-skinned-realtime.html` (primary); new `examples/three.js/demos/06-yoga-mode.html` (secondary, slimmed-down) |
|
||||
| **Relates to** | ADR-169 (adam-mode light theme), ADR-019 (sensing-only UI), ADR-035 (live sensing UI accuracy) |
|
||||
| **Tracking issue** | none yet |
|
||||
|
||||
---
|
||||
|
||||
## 1. Context
|
||||
|
||||
`examples/three.js/demos/05-skinned-realtime.html` already runs the full MediaPipe Pose Heavy pipeline at ~30 Hz: 33 BlazePose landmarks flow through a one-euro-filter bank into joint-angle extraction and then into a Mixamo X Bot IK retarget. The `#pose-panel` HUD shows landmark count, visibility, and pose FPS. The `#helpers` panel (ADR-097) has adam-mode (ADR-169) and eight visualisation toggles.
|
||||
|
||||
This infrastructure is complete. Every frame, per-joint angles are already computable from the existing `liveKp` world-space landmark array. What does not yet exist is any layer that interprets those angles as a known yoga pose, scores the user's alignment against a target shape, and guides the user through a structured sequence.
|
||||
|
||||
### 1.1 Why yoga-mode in this demo
|
||||
|
||||
Three concrete use-cases drive this:
|
||||
|
||||
1. **Developer self-test for the retargeting pipeline.** Cycling through a Sun Salutation A is a systematic, reproducible way to exercise every major joint (shoulder, elbow, hip, knee, spine). A pose-scoring overlay makes regression immediately visible — if a code change breaks elbow retargeting, the yoga classifier will output a depressed alignment score on Chaturanga even before a visual inspection.
|
||||
|
||||
2. **Public demonstration value.** The demo is served at `http://127.0.0.1:8765/examples/three.js/demos/05-skinned-realtime.html` and shown to evaluators. A guided instructional mode that scores real-time body alignment against Tadasana or Downward Dog is immediately intelligible to a non-technical audience in a way that raw CSI amplitude bars are not.
|
||||
|
||||
3. **Future bridge to the Rust host.** The Rust-side `wifi-densepose-signal/src/ruvsense/pose_tracker.rs` maintains a 17-keypoint Kalman tracker in COCO convention. yoga-mode in the demo operates on the 33-landmark MediaPipe convention. These are not the same: MediaPipe indices 0–32 (BlazePose) map non-trivially to COCO 0–16. Deciding the mapping now — even in a pure-JS context — canonicalises it for the eventual Rust integration.
|
||||
|
||||
### 1.2 What this ADR is *not*
|
||||
|
||||
- Not a backend service. No WebSocket endpoint, no session record, no cloud upload. Pure client-side HTML.
|
||||
- Not a fitness-app competitor. The scope is Sun Salutation A (8 poses). The full 84-asana classical corpus is out of scope.
|
||||
- Not an integration with the Rust `pose_tracker.rs`. That bridge is documented here as a future consequence, not an immediate deliverable.
|
||||
- Not a redesign of demo 05. Panel layout, three.js scene geometry, and the CSI overlay are unchanged.
|
||||
- Not a new design system. yoga-mode inherits every existing CSS custom property.
|
||||
|
||||
### 1.3 COCO-17 ↔ BlazePose-33 mapping note
|
||||
|
||||
The Rust tracker uses COCO 17-keypoint indices (0=nose, 5=left-shoulder, 6=right-shoulder, 7=left-elbow, 8=right-elbow, 9=left-wrist, 10=right-wrist, 11=left-hip, 12=right-hip, 13=left-knee, 14=right-knee, 15=left-ankle, 16=right-ankle). MediaPipe BlazePose-33 uses a different, denser scheme where shoulders are at 11–12, elbows at 13–14, wrists at 15–16, hips at 23–24, knees at 25–26, ankles at 27–28.
|
||||
|
||||
The mapping for the 13 joints used in yoga-mode angle computation is:
|
||||
|
||||
| Joint role | COCO idx | BlazePose idx |
|
||||
|---|---|---|
|
||||
| nose | 0 | 0 |
|
||||
| left shoulder | 5 | 11 |
|
||||
| right shoulder | 6 | 12 |
|
||||
| left elbow | 7 | 13 |
|
||||
| right elbow | 8 | 14 |
|
||||
| left wrist | 9 | 15 |
|
||||
| right wrist | 10 | 16 |
|
||||
| left hip | 11 | 23 |
|
||||
| right hip | 12 | 24 |
|
||||
| left knee | 13 | 25 |
|
||||
| right knee | 14 | 26 |
|
||||
| left ankle | 15 | 27 |
|
||||
| right ankle | 16 | 28 |
|
||||
|
||||
When the Rust host integration is implemented, the joint-angle features extracted by yoga-mode in JS and by `pose_tracker.rs` in Rust will be computed from the same physical joints via this table. No translation layer is needed at runtime — yoga-mode always uses BlazePose indices; `pose_tracker.rs` always uses COCO indices.
|
||||
|
||||
### 1.4 Biomechanical basis for joint-angle targets
|
||||
|
||||
The joint-angle targets in this ADR are grounded in peer-reviewed measurements. Perez-Testor et al. (2019, PMC6521759) captured 10 trained practitioners performing Surya Namaskar A on a 12-camera Vicon system at 100 Hz, reporting sagittal-plane joint angles at each pose transition. Key ranges: elbow 22°–116°, hip 15° extension to 134° flexion, knee 3° hyperextension to 140° flexion, spine 44° extension to 58° flexion, shoulder 56°–183°. These empirical ranges set the upper and lower bounds for the tolerance bands in this ADR's pose templates. Where Perez-Testor does not report a joint (e.g. wrist flexion for Chaturanga arm angle), the Iyengar geometry — "elbows at 90° bent close to the body" — supplies the target value. A 2023 PMC yoga-pose review (PMC10280249) confirming angle-heuristic approaches as the most reliable real-time classification method validates the algorithmic choice.
|
||||
|
||||
---
|
||||
|
||||
## 2. Decision
|
||||
|
||||
### 2.1 Pose taxonomy — Sun Salutation A, 8 poses
|
||||
|
||||
Sun Salutation A is chosen for the first ship. It satisfies three criteria simultaneously: the poses are geometrically distinct from each other (no two share the same joint-angle signature), they form a complete bilateral sequence (both left and right sides are exercised), and they are among the best-documented asanas in the biomechanics literature. The Sanskrit and English names are unambiguous in the Ashtanga tradition.
|
||||
|
||||
The 8 poses in sequence order with their one-line joint-angle signatures:
|
||||
|
||||
| Stage | Sanskrit | English | Joint-angle signature |
|
||||
|---|---|---|---|
|
||||
| 1 | Tāḍāsana | Mountain Pose | All limbs extended: knees 180°, hips 180°, elbows 180°, spine vertical |
|
||||
| 2 | Ūrdhva Hastāsana | Upward Salute | Arms overhead: shoulders ~180° abducted, elbows 180°, torso elongated |
|
||||
| 3 | Uttānāsana | Standing Forward Fold | Hips ~0–30° (full fold), knees 180°, elbows relaxed, spine flexed |
|
||||
| 4 | Ardha Uttānāsana | Half Lift / Flat-Back | Hips ~90° (parallel torso), knees 180°, spine neutral (horizontal) |
|
||||
| 5 | Catvāri (Chaturanga Daṇḍāsana) | Four-Limbed Staff | Hips 180° (plank line), elbows ~90°, shoulders depressed, body horizontal |
|
||||
| 6 | Ūrdhva Mukha Śvānāsana | Upward-Facing Dog | Hips extended ~160°+, shoulders over wrists, spine extended, knees off floor |
|
||||
| 7 | Adho Mukha Śvānāsana | Downward-Facing Dog | Hips ~80–110° (inverted V), knees 180°, shoulders ~180° (arms overhead), spine long |
|
||||
| 8 | Uttānāsana | Standing Forward Fold (return) | Same as stage 3 — mirrors the descent; re-classified as stage 8 for sequence tracking |
|
||||
|
||||
"All 84 classical asanas" is explicitly rejected. Even the 26-pose Bikram set is rejected — the goal is a complete, self-contained instructional sequence for a 2–3 minute demo session, not exhaustive coverage. Eight poses are the minimum for a meaningful sequence narrative and the maximum that fits a single UI strip without horizontal scrolling on a 1080p screen.
|
||||
|
||||
### 2.2 Detection algorithm — joint-angle threshold matching with weighted scoring
|
||||
|
||||
**Chosen: joint-angle threshold matching.** For each frame, compute the angle at 6–10 named joints (one angle per joint, defined as the interior angle at the vertex formed by three landmarks). Compare each computed angle to the per-pose target. Score by weighted absolute deviation. Classify the argmax.
|
||||
|
||||
**Why not the alternatives:**
|
||||
|
||||
| Alternative | Verdict | Reason |
|
||||
|---|---|---|
|
||||
| Skeleton-as-vector cosine similarity | Rejected | Position-sensitive: a person standing 2 m from the camera vs. 1 m produces different vectors. Joint angles are translation- and scale-invariant by construction. |
|
||||
| Small MLP trained on a labelled dataset | Rejected | No labelled dataset exists in this codebase. Training a reliable MLP for 8 poses would require hundreds of labelled examples per class, a train/test split, and a model serialization format — none of which belongs in a single-file demo HTML. Joint-angle matching achieves the same discrimination for 8 geometrically distinct poses with zero training data. |
|
||||
| MediaPipe Tasks PoseClassifier (EfficientNet-based) | Rejected | Requires loading a separate `.task` bundle (~4 MB), adds a network dependency to the demo's offline-capable design, and uses a black-box embedding — undebuggable when a pose is misclassified. Threshold matching is fully inspectable in DevTools. |
|
||||
| DTW template matching on full landmark sequences | Rejected | Appropriate for gesture recognition over time (ADR-014's `gesture.rs`), not static pose classification. Sun Salutation transitions are slow (2–5 seconds per pose); per-frame angle scoring is sufficient. |
|
||||
|
||||
**Joint angle computation.** For three landmark positions A (proximal), B (vertex), C (distal), the interior angle at B is:
|
||||
|
||||
```
|
||||
angle_B = arccos( dot(A-B, C-B) / (|A-B| * |C-B|) ) in degrees
|
||||
```
|
||||
|
||||
This is computed in world-space from the existing `liveKp` THREE.Vector3 array. The computation is purely arithmetic — no matrix inversion, no DFT. At 30 Hz on any modern laptop it is unmeasurably fast relative to the MediaPipe inference cost.
|
||||
|
||||
**Named joints used in yoga-mode.** Joint names, their three-landmark triplets (proximal-vertex-distal), and the BlazePose indices:
|
||||
|
||||
| Joint name | Triplet (P-V-D) | Indices |
|
||||
|---|---|---|
|
||||
| `left_elbow` | shoulder→elbow→wrist | 11→13→15 |
|
||||
| `right_elbow` | shoulder→elbow→wrist | 12→14→16 |
|
||||
| `left_knee` | hip→knee→ankle | 23→25→27 |
|
||||
| `right_knee` | hip→knee→ankle | 24→26→28 |
|
||||
| `left_hip` | shoulder→hip→knee | 11→23→25 |
|
||||
| `right_hip` | shoulder→hip→knee | 12→24→26 |
|
||||
| `left_shoulder` | hip→shoulder→elbow | 23→11→13 |
|
||||
| `right_shoulder` | hip→shoulder→elbow | 24→12→14 |
|
||||
| `torso_lean` | hip-midpoint→shoulder-midpoint→vertical | synthetic |
|
||||
|
||||
`torso_lean` is the angle between the hip-to-shoulder axis and the world vertical (Y axis). It distinguishes standing-upright (≈0°) from folded-forward (≈90°) from plank-horizontal (≈90° in a different axis pattern). In practice, it is implemented as `acos(dot(hipToShoulder.normalize(), UP_VECTOR))` where `UP_VECTOR = (0,1,0)`.
|
||||
|
||||
### 2.3 Pose template format — inline JSON, single-file portable
|
||||
|
||||
Templates live as a JS object literal inside the `<script>` block of the demo file. A sibling `poses.json` would break the single-file portability that makes demos easy to share and locally serve. The inline approach imposes no additional HTTP request and no CORS constraint.
|
||||
|
||||
**Schema** (one template per pose):
|
||||
|
||||
```js
|
||||
{
|
||||
id: "tadasana", // machine-readable ID, localStorage key fragment
|
||||
name_en: "Mountain Pose", // English common name
|
||||
name_sa: "Tāḍāsana", // Sanskrit with diacritics
|
||||
stage: 1, // position in the Sun Salutation A sequence (1-8)
|
||||
joint_targets: {
|
||||
left_elbow: { angle_deg: 180, tolerance_deg: 15, weight: 0.5 },
|
||||
right_elbow: { angle_deg: 180, tolerance_deg: 15, weight: 0.5 },
|
||||
left_knee: { angle_deg: 180, tolerance_deg: 10, weight: 1.0 },
|
||||
right_knee: { angle_deg: 180, tolerance_deg: 10, weight: 1.0 },
|
||||
left_hip: { angle_deg: 180, tolerance_deg: 12, weight: 0.8 },
|
||||
right_hip: { angle_deg: 180, tolerance_deg: 12, weight: 0.8 },
|
||||
torso_lean: { angle_deg: 0, tolerance_deg: 12, weight: 1.2 },
|
||||
},
|
||||
instruction: "Stand tall. Feet hip-width, weight even. Arms relaxed at your sides. Lengthen through the crown.",
|
||||
min_hold_s: 3, // seconds the pose must be held to count as completed
|
||||
}
|
||||
```
|
||||
|
||||
**Schema decisions:**
|
||||
|
||||
- `tolerance_deg` is the half-width of the pass band. An angle within `[target - tolerance, target + tolerance]` contributes full score for that joint. Beyond the tolerance band the score degrades linearly to zero at `target ± (tolerance * 3)`, then clamps to zero. This linear-outside-band behaviour prevents cliff edges where being 16° off scores identically to 90° off.
|
||||
|
||||
- `weight` carries the importance signal. High-weight joints (torso_lean 1.2, knees 1.0) dominate the aggregate score. Low-weight joints (elbows 0.5 in Tadasana, where arm position is relaxed) have less influence. A weight of 0 would mask a joint entirely — used when the joint is not visible (see §2.7 graceful degradation).
|
||||
|
||||
- `min_hold_s` is per-template. Tadasana and Uttanasana are grounding poses that benefit from a 3-second hold. Chaturanga is a strength pose where 2 seconds is already challenging. The value lives in the template, not as a global constant, so future operators can tune it per pose without touching logic.
|
||||
|
||||
- There is no `max_hold_s`. Holding a pose longer than `min_hold_s` does not penalise the score.
|
||||
|
||||
**Why `tolerance_deg` over explicit pass/fail thresholds.** A binary pass/fail at a hard threshold creates a jarring UX: the alignment bar slams between 0% and 100% at a single degree of motion. Linear-outside-band degradation provides smooth visual feedback that guides the user toward the target incrementally.
|
||||
|
||||
### 2.4 Scoring formula
|
||||
|
||||
Per-frame alignment score for pose *p*, given measured angle `θ_j` at joint *j*:
|
||||
|
||||
```
|
||||
delta_j = |θ_j − target_j.angle_deg|
|
||||
|
||||
band_score_j =
|
||||
1.0 if delta_j ≤ tolerance_j
|
||||
1.0 − (delta_j − tolerance_j) / (2 * tolerance_j) if delta_j ≤ 3 * tolerance_j
|
||||
0.0 otherwise
|
||||
|
||||
raw_score_p = Σ_j ( weight_j * band_score_j ) / Σ_j ( weight_j )
|
||||
|
||||
alignment_score_p = clamp(raw_score_p, 0.0, 1.0)
|
||||
```
|
||||
|
||||
`alignment_score_p` is a value in [0, 1]. Displayed in the `#yoga-panel` as an integer percentage (0–100) with one decimal place for the progress ring to animate smoothly.
|
||||
|
||||
**Hold-time component.** The classifier reports a pose as *completed* when two conditions are simultaneously true:
|
||||
1. The pose has been the argmax classifier output for a contiguous streak of `K = 6` frames (see §2.5).
|
||||
2. Within that streak, the alignment score has remained above 0.6 (60%) for at least `min_hold_s` seconds.
|
||||
|
||||
Completion is a one-shot event per pose per sequence pass. It fires once, advances the sequence indicator, and triggers the audible cue. The user must drop out of the pose and re-enter it to re-trigger completion — this prevents accidental re-completion during a rest pause.
|
||||
|
||||
**Why 60% as the hold threshold.** At 60%, the user's joint angles are within the tolerance band on the majority of weighted joints. A strict 80% threshold would frustrate beginners; a lenient 40% threshold would fire on casual near-misses. 60% is consistent with the threshold used in the Google ML Kit PoseClassifier sample and the Perez-Testor study's reported inter-practitioner variance (mean joint-angle SD of ~10° across joints, which maps to roughly a 30% score drop relative to a perfect practitioner on a 15° tolerance band).
|
||||
|
||||
**Why not include a velocity component (punish fast transitions).** Velocity would require a second derivative of the landmark positions, which is already noisy from MediaPipe jitter even after the one-euro filter. Minimum hold time (2–3 s) implicitly penalises rushing through poses without adding noise sensitivity.
|
||||
|
||||
### 2.5 Pose classification flow and debounce
|
||||
|
||||
Every frame, after `ingestPoseLandmarks()` populates `liveKp`:
|
||||
|
||||
```js
|
||||
function classifyPose() {
|
||||
if (!yogaMode.enabled || !liveValid) return;
|
||||
computeJointAngles(); // fills yogaMode.angles from liveKp
|
||||
for (const p of yogaMode.activePoses) {
|
||||
p.frameScore = scorePose(p); // per-frame alignment_score_p
|
||||
}
|
||||
const best = yogaMode.activePoses.reduce((a, b) =>
|
||||
b.frameScore > a.frameScore ? b : a
|
||||
);
|
||||
if (best.frameScore > SCORE_NO_POSE_FLOOR) {
|
||||
yogaMode.streak = (yogaMode.candidate === best.id)
|
||||
? yogaMode.streak + 1 : 1;
|
||||
yogaMode.candidate = best.id;
|
||||
} else {
|
||||
yogaMode.streak = 0;
|
||||
yogaMode.candidate = null;
|
||||
}
|
||||
if (yogaMode.streak >= K_FRAMES && yogaMode.candidate !== yogaMode.current) {
|
||||
yogaMode.current = yogaMode.candidate;
|
||||
onPoseTransition(yogaMode.current);
|
||||
}
|
||||
updateYogaHUD();
|
||||
}
|
||||
```
|
||||
|
||||
**K = 6 frames** (debounce depth). At 30 Hz this corresponds to a 200 ms lag from first matching pose to classification announcement. This is long enough to suppress a one-frame flicker from a mediocre landmark result but short enough to feel instantaneous to a human moving at yoga pace (typical transition speed: 1–3 seconds).
|
||||
|
||||
Lowering K to 3 creates flickering when the user is near a pose boundary. Raising K to 12 introduces a 400 ms lag that makes the HUD feel unresponsive on quick transitions (e.g. Uttanasana → Ardha Uttanasana takes ~1 second in a vigorous practice). K = 6 is the correct value given the ~30 Hz landmark update rate.
|
||||
|
||||
**SCORE_NO_POSE_FLOOR = 0.40.** If no pose scores above 40%, yoga-mode reports "no recognised pose" and does not transition. This prevents the classifier from latching onto the closest-matching pose during, say, walking across the room or sitting at a desk. At 40%, at least a plurality of the weighted joints must be within their tolerance band — a constraint that a non-yoga posture reliably fails.
|
||||
|
||||
### 2.6 UI surfaces
|
||||
|
||||
**Toggle in `#helpers` panel.** Added below the adam-mode row:
|
||||
|
||||
```html
|
||||
<label class="yoga-toggle">
|
||||
<input type="checkbox" id="yoga-mode-toggle">
|
||||
<span>yoga-mode (instructional)</span>
|
||||
<span class="swatch" style="color: var(--green)"></span>
|
||||
</label>
|
||||
```
|
||||
|
||||
yoga-mode is orthogonal to adam-mode: both can be active simultaneously. It uses `data-yoga="on"` on `<body>`, not `data-theme`. The attribute is distinct so that CSS selectors like `:root[data-theme="adam"]` and `:root[data-yoga="on"]` compose without conflict.
|
||||
|
||||
**`#yoga-panel` — bottom-centre overlay.** A new `<div id="yoga-panel" class="panel">` appears at the bottom centre of the viewport when yoga-mode is enabled. It is hidden (`display: none`) when yoga-mode is off, so it does not interfere with the existing layout.
|
||||
|
||||
The panel contains:
|
||||
|
||||
1. **Current pose name** — large (18px), Sanskrit name above English name below, amber colour. Falls back to "—" when no pose is recognised.
|
||||
2. **Alignment score ring** — a small SVG `<circle>` progress ring (r=22, stroke-dasharray) updating on every classified frame. Score 0–100 shown as integer inside the ring.
|
||||
3. **Hold-time progress bar** — a `<div class="bar-track">` identical in style to the CSI bars, filling from 0% to 100% as the hold-time accumulates. Resets on pose transition.
|
||||
4. **Instruction text** — one line from the current pose's `instruction` field, `font-size: 10px`, `color: var(--text-mute)`.
|
||||
5. **Visibility warning** — a `<span class="yoga-warn">` shown in `var(--red)` when `torso_not_visible` is true (see §2.7).
|
||||
|
||||
**Sequence strip — top-centre.** A horizontal strip of 8 thumbnail slots (`<div class="yoga-strip">`) spanning the top of the viewport (z-index above the titlecard, below `#info`). Each slot contains the pose's stage number and a 3-letter abbreviation (TAD, URD, UTT, ARD, CAT, UPD, DOG, UT2). Slots are styled:
|
||||
|
||||
- **Dimmed** (opacity 0.3, `var(--text-mute)` text) — not yet reached.
|
||||
- **Active** (opacity 1.0, `var(--amber)` border glow, pulsing) — current pose.
|
||||
- **Completed** (opacity 0.7, `var(--green)` checkmark `✓`, no glow) — held for `min_hold_s` seconds.
|
||||
|
||||
The strip does not scroll. Eight slots at ~90px each fit a 720px-wide viewport. On narrower screens the strip compresses gracefully because the slots use `flex: 1` within a `display: flex` container.
|
||||
|
||||
**Audible cue.** A single `<audio id="yoga-bell" src="data:audio/wav;base64,..." preload="auto">` element. The WAV is a 0.4-second C5 bell tone encoded inline as base64 (~12 KB). This preserves the single-file portability. It fires once on pose completion via `yogaBell.currentTime = 0; yogaBell.play()`. A `muted` toggle in `#helpers` (beneath the yoga-mode checkbox) allows the user to silence it: `<label><input type="checkbox" id="yoga-mute-toggle"> mute bell</label>`. The bell is muted by default (`yogaBell.muted = true`) to avoid startling first-time users.
|
||||
|
||||
**Theme compatibility.** `#yoga-panel` and the sequence strip use only existing custom properties: `var(--bg-panel)`, `var(--border)`, `var(--amber)`, `var(--amber-hot)`, `var(--text)`, `var(--text-mute)`, `var(--green)`, `var(--red)`. No new CSS variables are introduced. The panel therefore inherits both the default dark theme and adam-mode automatically — the same mechanism described in ADR-169 §2.1.
|
||||
|
||||
### 2.7 Camera / MediaPipe assumptions and graceful degradation
|
||||
|
||||
**Expected input:** front-facing camera, full body from head to ankles in frame, neutral indoor lighting. The demo's existing camera pipeline already requests `{ video: { facingMode: 'user', width: 640, height: 480 } }`. No change to the MediaPipe setup.
|
||||
|
||||
**Graceful degradation when body is partially out of frame.** MediaPipe assigns a `visibility` score in [0, 1] to each landmark. When a landmark's visibility drops below 0.35, yoga-mode treats that joint as missing:
|
||||
|
||||
```js
|
||||
function effectiveWeight(jointName, angles) {
|
||||
const vis = jointVisibility(jointName); // min visibility of the 3 landmarks
|
||||
if (vis < 0.35) return 0.0; // joint masked — not counted
|
||||
if (vis < 0.65) return angles.weight * (vis / 0.65); // partial weight
|
||||
return angles.weight;
|
||||
}
|
||||
```
|
||||
|
||||
When two or more of the high-weight joints (knees, hips, torso_lean) are masked simultaneously, `Σ_j(weight_j)` falls below a minimum viable total, and `alignment_score_p` is set to 0 regardless of the numerator. This prevents spurious high scores from a partially visible body where only one or two low-weight joints (e.g. elbows) are visible and happen to match a pose.
|
||||
|
||||
The `#yoga-panel` surfaces a `torso_not_visible` warning ("Move back — full body not in frame") in `var(--red)` whenever `liveVis[23] < 0.35 || liveVis[24] < 0.35` (left or right hip not visible). The hips are the reference joint for torso_lean and for hip-angle computation; their absence makes the entire classifier unreliable.
|
||||
|
||||
### 2.8 Cross-demo applicability
|
||||
|
||||
**yoga-mode ships in demo 05 only for the first iteration.** Demos 03 and 04 do not have a MediaPipe pipeline; there are no `liveKp` landmarks to score. Adding yoga-mode to them would require pulling in the entire MediaPipe Pose Heavy CDN script — changing those demos' character and load time.
|
||||
|
||||
**New demo: `06-yoga-mode.html`.** A new file `examples/three.js/demos/06-yoga-mode.html` is introduced as a slimmed-down variant of demo 05 where yoga-mode is the primary focus rather than an optional overlay. Differences from demo 05:
|
||||
|
||||
- The CSI panel (`#csi`) and the tomography sweep are hidden by default (`display: none`).
|
||||
- The `#yoga-panel` is expanded to a larger centre-screen layout with a bigger score ring (r=44) and larger pose name text (24px).
|
||||
- The sequence strip is rendered larger (100px slot width).
|
||||
- The `#helpers` panel shows only the yoga-related toggles (yoga-mode, adam-mode, mute bell).
|
||||
- The titlecard text reads "RuView · Yoga Mode".
|
||||
|
||||
This file is created from a copy of demo 05 with the CSI and tomography sections stripped. It shares the `YogaMode` object and pose templates verbatim — no logic is duplicated.
|
||||
|
||||
The decision to introduce a sixth demo file rather than making demo 05's yoga features more prominent is: demo 05 is a complete multi-feature demo (CSI + MediaPipe + IK retarget); demo 06 is a single-purpose instructional demo. Evaluators who want to show the yoga system without the RF sensing noise get demo 06.
|
||||
|
||||
### 2.9 Persistence
|
||||
|
||||
User settings are persisted in `localStorage` under the `ruview.yoga.*` namespace:
|
||||
|
||||
| Key | Type | Value shape | Default |
|
||||
|---|---|---|---|
|
||||
| `ruview.yoga.enabled` | boolean string | `"true"` or `"false"` | `"false"` |
|
||||
| `ruview.yoga.muted` | boolean string | `"true"` or `"false"` | `"true"` |
|
||||
| `ruview.yoga.tolerance_scale` | float string | `"0.5"` to `"2.0"` | `"1.0"` |
|
||||
| `ruview.yoga.sequence` | JSON string | `["tadasana","urdhva_hastasana",…]` | full 8-pose sequence |
|
||||
|
||||
`tolerance_scale` is a global multiplier applied to every `tolerance_deg` value in every template. A scale of 0.5 makes the classifier strict (tight bands); a scale of 2.0 makes it forgiving (wide bands). The HUD exposes this as a simple "Difficulty" slider: Easy (2.0×), Normal (1.0×), Strict (0.5×). The default is Normal.
|
||||
|
||||
`ruview.yoga.sequence` allows an operator to load a custom subset or reordering of the 8 poses, or to load additional poses added via `YogaMode.addPose()`. The array contains pose `id` strings. On load, yoga-mode resolves each ID against the registered template map; unknown IDs are skipped with a console warning.
|
||||
|
||||
All `localStorage` accesses are wrapped in try/catch to handle privacy-restricted origins.
|
||||
|
||||
### 2.10 JS API surface
|
||||
|
||||
yoga-mode exposes a clean internal module object. Because the demo is a single-file HTML with no ES module bundler, the pattern is a plain object literal assigned to a local `const`:
|
||||
|
||||
```js
|
||||
const YogaMode = {
|
||||
// ---- Lifecycle ----
|
||||
init(opts = {}) {}, // wire up UI, register pose templates, restore localStorage
|
||||
enable() {}, // set data-yoga="on", show #yoga-panel, start classifying
|
||||
disable() {}, // remove data-yoga="on", hide #yoga-panel, reset state
|
||||
|
||||
// ---- Classification callbacks ----
|
||||
onPoseChanged(cb) {}, // cb(poseId: string | null) — fires on confirmed transition
|
||||
onPoseScored(cb) {}, // cb(scores: {[poseId]: number}) — fires every frame
|
||||
onPoseCompleted(cb) {}, // cb(poseId: string, holdMs: number) — fires on hold completion
|
||||
|
||||
// ---- Template management ----
|
||||
addPose(template) {}, // validate and register a custom pose template
|
||||
removePose(id) {}, // remove a template by id (built-ins can be removed)
|
||||
poses() {}, // returns Array<PoseTemplate> — current registered set
|
||||
|
||||
// ---- State accessors ----
|
||||
currentPose() {}, // returns current confirmed pose id or null
|
||||
currentScore() {}, // returns alignment score [0,1] of current pose or 0
|
||||
angles() {}, // returns the latest computed joint angles object
|
||||
|
||||
// ---- Sequence control ----
|
||||
resetSequence() {}, // clears all completion state, restarts from stage 1
|
||||
setSequence(ids) {}, // replace active sequence with a custom id array
|
||||
|
||||
// Internal state — not part of the public API:
|
||||
_state: { enabled, candidate, current, streak, holdStart, completedSet }
|
||||
};
|
||||
```
|
||||
|
||||
`onPoseChanged`, `onPoseScored`, and `onPoseCompleted` follow the same pattern as the demo's existing event hooks: they register a single callback (last-writer wins, not an array). This is sufficient for a single-file demo where there is at most one consumer per event. A future multi-listener pattern would need a `listeners` array; that is out of scope.
|
||||
|
||||
`addPose(template)` validates the template schema before registering it. A template missing `joint_targets` or with an `id` that contains non-alphanumeric characters is rejected with a `console.error` and returns `false`. Valid templates return `true`.
|
||||
|
||||
### 2.11 Pose templates — Sun Salutation A joint targets
|
||||
|
||||
The full 8-pose template set. Angle targets are derived from Perez-Testor et al. (2019) Vicon measurements and Iyengar alignment geometry. Tolerances are set to twice the reported inter-practitioner SD (~10°) rounded to the nearest 5°, then scaled by the user's `tolerance_scale`.
|
||||
|
||||
**Stage 1 — Tāḍāsana (Mountain Pose)**
|
||||
|
||||
All joints extended. Body in anatomical position. Baseline for comparison.
|
||||
|
||||
```js
|
||||
{ id: "tadasana", name_en: "Mountain Pose", name_sa: "Tāḍāsana", stage: 1,
|
||||
min_hold_s: 3,
|
||||
joint_targets: {
|
||||
left_knee: { angle_deg: 180, tolerance_deg: 10, weight: 1.0 },
|
||||
right_knee: { angle_deg: 180, tolerance_deg: 10, weight: 1.0 },
|
||||
left_hip: { angle_deg: 180, tolerance_deg: 12, weight: 0.8 },
|
||||
right_hip: { angle_deg: 180, tolerance_deg: 12, weight: 0.8 },
|
||||
torso_lean: { angle_deg: 0, tolerance_deg: 10, weight: 1.2 },
|
||||
left_elbow: { angle_deg: 180, tolerance_deg: 20, weight: 0.4 },
|
||||
right_elbow: { angle_deg: 180, tolerance_deg: 20, weight: 0.4 },
|
||||
},
|
||||
instruction: "Stand tall. Feet hip-width, weight even. Arms at sides. Lengthen through the crown.",
|
||||
}
|
||||
```
|
||||
|
||||
**Stage 2 — Ūrdhva Hastāsana (Upward Salute)**
|
||||
|
||||
Arms sweep overhead. Shoulders maximally abducted. Distinguishing feature: both elbows extended and arms overhead (shoulder angle approaches 180° abduction). Perez-Testor reports shoulder elevation of 183° at peak overhead position.
|
||||
|
||||
```js
|
||||
{ id: "urdhva_hastasana", name_en: "Upward Salute", name_sa: "Ūrdhva Hastāsana", stage: 2,
|
||||
min_hold_s: 2,
|
||||
joint_targets: {
|
||||
left_shoulder: { angle_deg: 165, tolerance_deg: 20, weight: 1.2 },
|
||||
right_shoulder: { angle_deg: 165, tolerance_deg: 20, weight: 1.2 },
|
||||
left_elbow: { angle_deg: 180, tolerance_deg: 15, weight: 0.8 },
|
||||
right_elbow: { angle_deg: 180, tolerance_deg: 15, weight: 0.8 },
|
||||
left_knee: { angle_deg: 180, tolerance_deg: 12, weight: 0.8 },
|
||||
right_knee: { angle_deg: 180, tolerance_deg: 12, weight: 0.8 },
|
||||
torso_lean: { angle_deg: 0, tolerance_deg: 15, weight: 0.7 },
|
||||
},
|
||||
instruction: "Inhale. Sweep arms overhead. Palms face each other. Gaze forward or slightly up.",
|
||||
}
|
||||
```
|
||||
|
||||
**Stage 3 — Uttānāsana (Standing Forward Fold)**
|
||||
|
||||
Deep hip flexion. Torso approaches vertical-inverted. Perez-Testor reports hip flexion of 134°. The angle at the hip joint as computed by our triplet (shoulder→hip→knee) goes to ~30° as the torso folds toward the legs. Knees remain extended.
|
||||
|
||||
```js
|
||||
{ id: "uttanasana", name_en: "Standing Forward Fold", name_sa: "Uttānāsana", stage: 3,
|
||||
min_hold_s: 3,
|
||||
joint_targets: {
|
||||
left_hip: { angle_deg: 40, tolerance_deg: 25, weight: 1.2 },
|
||||
right_hip: { angle_deg: 40, tolerance_deg: 25, weight: 1.2 },
|
||||
left_knee: { angle_deg: 175, tolerance_deg: 15, weight: 1.0 },
|
||||
right_knee: { angle_deg: 175, tolerance_deg: 15, weight: 1.0 },
|
||||
torso_lean: { angle_deg: 85, tolerance_deg: 20, weight: 1.0 },
|
||||
},
|
||||
instruction: "Exhale. Fold forward from the hips. Let the crown of the head drop toward the floor.",
|
||||
}
|
||||
```
|
||||
|
||||
**Stage 4 — Ardha Uttānāsana (Half Lift / Flat-Back)**
|
||||
|
||||
Torso lifts to horizontal. Hip angle opens to ~90°. Spine neutral. This is the most distinctive pose for classification: it is the only one where the torso is neither upright nor fully folded — the `torso_lean` angle is ~90° and the hips are also ~90°. Perez-Testor reports the half-lift as an intermediate transition posture; the distinguishing cue is the simultaneous hip angle and spine neutral (not flexed).
|
||||
|
||||
```js
|
||||
{ id: "ardha_uttanasana", name_en: "Half Lift", name_sa: "Ardha Uttānāsana", stage: 4,
|
||||
min_hold_s: 2,
|
||||
joint_targets: {
|
||||
left_hip: { angle_deg: 90, tolerance_deg: 20, weight: 1.2 },
|
||||
right_hip: { angle_deg: 90, tolerance_deg: 20, weight: 1.2 },
|
||||
left_knee: { angle_deg: 175, tolerance_deg: 12, weight: 0.8 },
|
||||
right_knee: { angle_deg: 175, tolerance_deg: 12, weight: 0.8 },
|
||||
torso_lean: { angle_deg: 90, tolerance_deg: 15, weight: 1.2 },
|
||||
left_elbow: { angle_deg: 180, tolerance_deg: 20, weight: 0.5 },
|
||||
right_elbow: { angle_deg: 180, tolerance_deg: 20, weight: 0.5 },
|
||||
},
|
||||
instruction: "Inhale. Lift the chest. Flat back. Fingertips on the shins or floor. Gaze forward.",
|
||||
}
|
||||
```
|
||||
|
||||
**Stage 5 — Catvāri / Chaturanga Daṇḍāsana (Four-Limbed Staff)**
|
||||
|
||||
Plank lowered. Elbows at 90°. Body horizontal. This is the hardest pose to classify from a front-facing camera alone: the body is horizontal and the depth axis is ambiguous. The key discriminator is `elbow_angle ≈ 90°` combined with `hip ≈ 180°` (no flexion) and `torso_lean ≈ 90°`. Note: from a front-facing camera, a person in Chaturanga facing the camera appears foreshortened. yoga-mode accepts this limitation and primarily tracks Chaturanga as the transition between Ardha Uttanasana and Upward Dog in the sequence, with lower weight on spatial cues and higher weight on elbow angle. Iyengar geometry specifies elbows at 90° against the body.
|
||||
|
||||
```js
|
||||
{ id: "chaturanga", name_en: "Four-Limbed Staff", name_sa: "Catvāri / Chaturanga Daṇḍāsana", stage: 5,
|
||||
min_hold_s: 2,
|
||||
joint_targets: {
|
||||
left_elbow: { angle_deg: 90, tolerance_deg: 20, weight: 1.5 },
|
||||
right_elbow: { angle_deg: 90, tolerance_deg: 20, weight: 1.5 },
|
||||
left_hip: { angle_deg: 175, tolerance_deg: 15, weight: 0.8 },
|
||||
right_hip: { angle_deg: 175, tolerance_deg: 15, weight: 0.8 },
|
||||
left_knee: { angle_deg: 175, tolerance_deg: 15, weight: 0.6 },
|
||||
right_knee: { angle_deg: 175, tolerance_deg: 15, weight: 0.6 },
|
||||
torso_lean: { angle_deg: 90, tolerance_deg: 20, weight: 0.7 },
|
||||
},
|
||||
instruction: "Lower down. Elbows at 90°, hugged to the ribs. Body in one straight line.",
|
||||
}
|
||||
```
|
||||
|
||||
**Stage 6 — Ūrdhva Mukha Śvānāsana (Upward-Facing Dog)**
|
||||
|
||||
Hips extend, spine extends (backbend), shoulders over wrists, knees off floor. Distinguishing feature: hips are near 160–180° (extended), which is the opposite of Uttanasana's deep flexion. The `torso_lean` reverses from ~90° horizontal to approaching 0° or slightly past vertical (slight backbend). Perez-Testor's spine extension of 44° is the reference for the backbend component; the hip angle opens to near-full extension.
|
||||
|
||||
```js
|
||||
{ id: "urdhva_mukha_svanasana", name_en: "Upward-Facing Dog", name_sa: "Ūrdhva Mukha Śvānāsana", stage: 6,
|
||||
min_hold_s: 2,
|
||||
joint_targets: {
|
||||
left_hip: { angle_deg: 165, tolerance_deg: 20, weight: 1.2 },
|
||||
right_hip: { angle_deg: 165, tolerance_deg: 20, weight: 1.2 },
|
||||
left_elbow: { angle_deg: 170, tolerance_deg: 20, weight: 0.8 },
|
||||
right_elbow: { angle_deg: 170, tolerance_deg: 20, weight: 0.8 },
|
||||
left_knee: { angle_deg: 170, tolerance_deg: 20, weight: 0.6 },
|
||||
right_knee: { angle_deg: 170, tolerance_deg: 20, weight: 0.6 },
|
||||
torso_lean: { angle_deg: 15, tolerance_deg: 20, weight: 0.8 },
|
||||
},
|
||||
instruction: "Press the tops of the feet down. Lift the chest. Shoulders away from the ears. Gaze forward.",
|
||||
}
|
||||
```
|
||||
|
||||
**Stage 7 — Adho Mukha Śvānāsana (Downward-Facing Dog)**
|
||||
|
||||
Hips high. Inverted V. The most geometrically distinct pose in the sequence: high hips, extended knees, arms overhead-ish (shoulder angle ~150° relative to torso), torso_lean ~90° but in the opposite direction to Chaturanga (body weight shifted back over the heels). The hip angle as measured by our shoulder→hip→knee triplet is ~80–110° (the pelvis is high, creating a roughly right-angle fold at the hip). Perez-Testor reports the hip-angle transition from Chaturanga to Downward Dog as the largest single-frame angle change in the sequence (~120° excursion), making it the easiest pose to classify correctly.
|
||||
|
||||
```js
|
||||
{ id: "adho_mukha_svanasana", name_en: "Downward-Facing Dog", name_sa: "Adho Mukha Śvānāsana", stage: 7,
|
||||
min_hold_s: 5,
|
||||
joint_targets: {
|
||||
left_hip: { angle_deg: 90, tolerance_deg: 25, weight: 1.2 },
|
||||
right_hip: { angle_deg: 90, tolerance_deg: 25, weight: 1.2 },
|
||||
left_knee: { angle_deg: 180, tolerance_deg: 15, weight: 1.0 },
|
||||
right_knee: { angle_deg: 180, tolerance_deg: 15, weight: 1.0 },
|
||||
left_shoulder: { angle_deg: 150, tolerance_deg: 25, weight: 0.8 },
|
||||
right_shoulder: { angle_deg: 150, tolerance_deg: 25, weight: 0.8 },
|
||||
torso_lean: { angle_deg: 90, tolerance_deg: 20, weight: 0.7 },
|
||||
},
|
||||
instruction: "Hips up and back. Heels reaching toward the floor. Arms and ears in one line. Breathe.",
|
||||
}
|
||||
```
|
||||
|
||||
**Stage 8 — Uttānāsana (Standing Forward Fold, return)**
|
||||
|
||||
Identical to stage 3 in geometry. Classified as stage 8 for sequence-tracking purposes only — same template joint targets, different `id` and `stage` value.
|
||||
|
||||
```js
|
||||
{ id: "uttanasana_return", name_en: "Standing Forward Fold (return)", name_sa: "Uttānāsana", stage: 8,
|
||||
min_hold_s: 2,
|
||||
joint_targets: { /* same as stage 3 */ },
|
||||
instruction: "Step or jump to the front. Exhale. Release the head. Return to stillness.",
|
||||
}
|
||||
```
|
||||
|
||||
Distinguishing stages 3 and 8 is handled by the sequence-tracking layer, not by the classifier. If yoga-mode is in stage 7 (Downward Dog) and detects a forward-fold shape, it advances to stage 8 rather than regressing to stage 3. If yoga-mode is in stages 1–2 and detects a forward-fold shape, it advances to stage 3. The sequence tracks forward direction only; there is no backward regression in the first implementation.
|
||||
|
||||
### 2.12 Test plan
|
||||
|
||||
**Manual — live camera:**
|
||||
Stand in front of the workstation USB camera (ruvzen, confirmed front-facing in CLAUDE.local.md). Enable yoga-mode from `#helpers`. Cycle through all 8 poses in order. For each pose: verify the HUD shows the correct Sanskrit and English name within 2 frames (~67 ms) of entering the pose, the alignment score exceeds 60%, and the sequence strip advances. Verify no pose is misclassified when standing in a casual at-rest position (score should be below 40% floor for all 8 poses).
|
||||
|
||||
**Synthetic — test mode triggered by `?test=1` URL parameter:**
|
||||
When `location.search` includes `test=1`, yoga-mode enters a headless test mode: instead of reading from `liveKp`, it reads from a pre-recorded `YOGA_TEST_FIXTURES` object — one synthetic landmark array per pose, generated at authoring time by capturing the real `liveKp` values during a manual demo session.
|
||||
|
||||
```js
|
||||
if (new URLSearchParams(location.search).has('test')) {
|
||||
for (const fixture of YOGA_TEST_FIXTURES) {
|
||||
ingestPoseLandmarks(fixture.landmarks);
|
||||
classifyPose();
|
||||
const result = YogaMode.currentPose();
|
||||
console.assert(result === fixture.expected_id,
|
||||
`FAIL: ${fixture.expected_id} got ${result}`);
|
||||
}
|
||||
console.log('YogaMode tests complete');
|
||||
}
|
||||
```
|
||||
|
||||
The fixture set is 8 entries (one per pose). Each entry is a hard-coded `landmarks` array of 33 objects with `{x, y, z, visibility}` values. These fixtures are inlined in the `<script>` block, gated behind `if (urlParams.has('test'))` so they are never executed in normal operation.
|
||||
|
||||
**Negative test:** A ninth fixture entry with the user standing in a neutral at-rest position (arms at sides but knees slightly bent, casual posture — not a yoga pose). Assert `YogaMode.currentPose() === null` (no pose above the 0.40 floor).
|
||||
|
||||
**Regression guard for joint-angle computation:** A tenth fixture that hard-codes known landmark positions forming a right angle at the left knee (three points forming a precise 90° angle). Assert `YogaMode.angles().left_knee` is within ±0.5° of 90.
|
||||
|
||||
### 2.13 Rejected alternatives
|
||||
|
||||
| Alternative | Rejected because |
|
||||
|---|---|
|
||||
| Train a custom MLP on a labelled yoga dataset | No labelled dataset in this codebase. Training requires hundreds of examples per class, a train/test pipeline, and a serialized model file — all incompatible with a single-file demo. Joint-angle matching achieves equivalent discrimination for 8 geometrically distinct poses with zero training data. |
|
||||
| Use a paid SaaS pose-classification API (e.g. a commercial yoga scoring cloud service) | Introduces an external network dependency, a per-request cost, and a privacy concern (camera frames leaving the browser). Pure client-side is a hard requirement. |
|
||||
| Ship audio/video instructional content (video of an instructor demonstrating each pose) | Massively increases the demo's asset footprint. A single instructor video per pose at 15 fps, 10 seconds, compressed, is ~500 KB × 8 = 4 MB minimum. The inline base64 bell (~12 KB) is the correct granularity of embedded media for this demo. |
|
||||
| Ship a backend yoga-tracking session record (store per-session completion data to a server) | No backend endpoint exists or is planned for the demos. Client-only; persistence via `localStorage`. |
|
||||
| Integrate with the Rust `pose_tracker.rs` now | Convention mismatch (BlazePose-33 vs COCO-17) documented in §1.3 but the cost of bridging it outweighs the benefit for a demo. The bridge is deferred: yoga-mode in JS is valuable without it. Rust integration becomes tractable once a WebSocket protocol for streaming joint angles (not raw CSI) from the sensing server is defined — a separate ADR. |
|
||||
| Use MediaPipe Tasks `PoseLandmarker` with a built-in `PoseClassifier` task | The Tasks API requires loading a `.task` bundle (~4 MB) from CDN at runtime. Demo 05 already uses the older `@mediapipe/pose@0.5` CDN script; switching APIs would require rewriting the entire landmark ingest pipeline. The classifier task is a black box undebuggable in DevTools. Threshold matching is fully transparent. |
|
||||
| Put yoga-mode on `data-theme` alongside adam-mode | yoga-mode is not a theme — it is a feature toggle. Mixing it with the theme attribute would prevent simultaneous adam-mode + yoga-mode activation and would conflate presentation with functionality. Separate `data-yoga="on"` attribute is the correct model. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Consequences
|
||||
|
||||
### 3.1 Positive
|
||||
|
||||
- The retargeting pipeline in demo 05 gains a per-pose regression test harness (`?test=1`) at no additional tooling cost.
|
||||
- yoga-mode operates on the existing `liveKp` stream — zero additional CPU cost beyond a few arctangent calls per frame (~50 µs at 30 Hz).
|
||||
- The pose-scoring formula is fully deterministic and inspectable: `console.log(YogaMode.angles())` in DevTools shows every joint angle on every frame.
|
||||
- Demo 06 provides a clean instructional-first presentation that separates yoga-mode from the RF sensing visualisations, making the feature accessible to a fitness-context audience.
|
||||
- The `YogaMode.addPose()` API allows operators to extend the template library without touching core logic — enabling future pose sets (Warrior series, Yin postures) as a follow-on.
|
||||
- The `tolerance_scale` persistence allows the same demo codebase to serve both beginners (2× tolerance) and experienced practitioners (0.5× tolerance) without code changes.
|
||||
|
||||
### 3.2 Negative
|
||||
|
||||
- Two HTML files to maintain (`05` and `06`) where previously there was one. Mitigated by the fact that yoga-mode logic is identical between them — demo 06 is a layout variant, not a code fork.
|
||||
- Chaturanga Dandasana classification is inherently degraded from a front-facing camera (the body is horizontal; the depth axis is ambiguous). The classifier can detect the pose if the user faces the camera sideways (profile view), but the existing camera setup on ruvzen is front-facing. This is a known limitation, documented in the instruction text ("face the camera from the side for best Chaturanga detection").
|
||||
- The inline base64 bell WAV adds ~12 KB to the HTML file size. Negligible at the scale of the demo but noted.
|
||||
- `localStorage` namespace `ruview.yoga.*` adds four keys per origin. No conflict with `ruview.theme` from adam-mode.
|
||||
|
||||
### 3.3 Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|---|---|---|
|
||||
| MediaPipe visibility scores are unreliable for floor-level landmarks (ankles, feet) during Dog poses | Medium | `effectiveWeight()` already masks low-visibility joints; Dog-pose templates weight knees (visible) more than ankles (may be occluded). |
|
||||
| The `?test=1` fixture landmarks become stale if the coordinate-space transform in `ingestPoseLandmarks()` changes | Low | Fixtures store raw `liveKp` world-space values, not normalized MediaPipe coords. If `ingestPoseLandmarks()` changes its output schema, the fixtures will produce obviously wrong joint angles in the assertion step — the failure is loud, not silent. |
|
||||
| Sequence-strip animation (CSS pulsing glow on the active stage) triggers repaint on every frame at 30 Hz | Low | The pulse is a CSS `animation` on `opacity` — composited by the GPU, no layout reflow. Negligible cost. |
|
||||
| User's camera position cuts off the hips (e.g. laptop on a desk) — `torso_not_visible` fires immediately | High for laptop use | The warning instructs the user to step back. This is the correct behaviour. Future: add a "camera too close" heuristic based on the ratio of shoulder distance to image width. |
|
||||
| Stage 8 (Uttanasana return) is classified identically to stage 3 by the angle classifier alone — the sequence layer must correctly disambiguate them | Medium | The sequence-tracking layer uses monotonic forward-only progression. Stage 3 can only fire when the current sequence position is 2 (after Urdhva Hastasana); stage 8 can only fire when the current sequence position is 7 (after Downward Dog). The classifier produces the angle score; the sequence layer decides which stage to credit. If the user skips a pose, the sequence layer waits — it does not leap to stage 8 from stage 2 even if a forward-fold shape is detected. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementation plan
|
||||
|
||||
Moderate scope — two HTML files, no build step, no new external dependencies.
|
||||
|
||||
1. **Define the `YOGA_POSES` array** — 8 template objects as specified in §2.11, inline in the `<script>` block of demo 05.
|
||||
2. **Implement `computeJointAngles()`** — read from the existing `liveKp` array, fill a `yogaAngles` object using the 9 joint triplets in §2.2.
|
||||
3. **Implement `scorePose(template)`** — the weighted-sum formula from §2.4, respecting `effectiveWeight()` for visibility masking.
|
||||
4. **Implement `classifyPose()`** — argmax with K=6 debounce as in §2.5; call from the existing `requestAnimationFrame` loop after `applyRetargeting()`.
|
||||
5. **Add `#yoga-panel` markup and CSS** — bottom-centre panel, score ring, hold-time bar, instruction text, visibility warning. All styles via existing custom properties.
|
||||
6. **Add the sequence strip** — `#yoga-strip` top-centre, 8 flex slots, 3-state styling (dimmed/active/completed).
|
||||
7. **Wire the `#helpers` toggle** — `yoga-mode-toggle` checkbox and `yoga-mute-toggle` checkbox; `localStorage` persistence.
|
||||
8. **Add `YogaMode` object** — wrapping steps 1–7 with the API surface from §2.10.
|
||||
9. **Add `YOGA_TEST_FIXTURES` and the `?test=1` harness** — 10 fixture entries (8 positive, 1 negative, 1 angle-computation).
|
||||
10. **Create `06-yoga-mode.html`** — copy of demo 05 with CSI/tomography sections hidden, larger yoga panel layout.
|
||||
11. **Manual validation** — stand in front of ruvzen camera, cycle all 8 poses, verify classification and sequence advancement.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- All 8 poses classified correctly in the `?test=1` synthetic harness (assertions pass with no console errors).
|
||||
- The negative fixture (casual stand) produces `currentPose() === null`.
|
||||
- The angle-computation fixture (`left_knee` at a known 90°) asserts within ±0.5°.
|
||||
- Manual: each of the 8 Sun Salutation A poses classified within 2 frames when held correctly.
|
||||
- Alignment score exceeds 60% when the user matches the pose by self-assessment.
|
||||
- Sequence strip advances in order; completed poses show green checkmark.
|
||||
- Bell fires on completion (when unmuted).
|
||||
- adam-mode + yoga-mode simultaneously active: both panels visible, correct theme.
|
||||
- `localStorage` persists enabled-state and tolerance-scale across page reloads.
|
||||
|
||||
---
|
||||
|
||||
## 5. Related ADRs
|
||||
|
||||
| ADR | Relationship |
|
||||
|---|---|
|
||||
| [ADR-169](ADR-169-adam-mode-light-theme.md) | Sibling demo-side feature. yoga-mode toggle lives in the same `#helpers` panel. Both are orthogonal and must compose. |
|
||||
| [ADR-019](ADR-019-sensing-only-ui-mode.md) | Sensing-only UI — yoga-mode is the opposite: camera-first, sensing secondary. |
|
||||
| [ADR-035](ADR-035-live-sensing-ui-accuracy.md) | Live sensing UI accuracy norms. yoga-mode scores the user's body against templates, not CSI accuracy — but the same principle of not misrepresenting measurement quality applies. |
|
||||
| [ADR-014](ADR-014-sota-signal-processing.md) | The Rust-side `gesture.rs` uses DTW for gesture recognition. yoga-mode explicitly rejects DTW for static pose classification (§2.2). The two systems are complementary: DTW for motion gestures, angle-threshold for static poses. |
|
||||
| [ADR-029](ADR-029-ruvsense-multistatic-sensing-mode.md) | The Rust `pose_tracker.rs` (COCO-17) that yoga-mode defers integrating with. The COCO↔BlazePose mapping in §1.3 is the foundation for the future bridge. |
|
||||
|
||||
---
|
||||
|
||||
## 6. References
|
||||
|
||||
### Production code
|
||||
- `examples/three.js/demos/05-skinned-realtime.html` — primary implementation target; `liveKp`, `liveVis`, `ingestPoseLandmarks()`, `#helpers`, `#pose-panel`, `RETARGETS`, `visForRetarget()` are all anchors for yoga-mode integration
|
||||
- `examples/three.js/demos/04-skinned-fbx.html` — sibling demo; lighting reference
|
||||
- `v2/crates/wifi-densepose-signal/src/ruvsense/pose_tracker.rs` — Rust COCO-17 tracker; convention mapping in §1.3 of this ADR targets this module
|
||||
|
||||
### External references
|
||||
|
||||
1. **Perez-Testor, S. et al. (2019).** "Kinematics of Suryanamaskar Using Three-Dimensional Motion Capture." *PMC6521759*. 10 trained practitioners, 12-camera Vicon, 100 Hz, sagittal-plane joint angles for each of the 12 standard Surya Namaskar positions. Primary source for angle targets and tolerance bounds in §2.11.
|
||||
|
||||
2. **Chidamber, S. and Harikumar, K. (2023).** "A novel approach for yoga pose estimation based on in-depth analysis of human body joint detection accuracy." *PMC10280249*. Validates joint-angle threshold matching as the dominant reliable real-time method for small-to-medium yoga pose sets; reports average inter-joint angle error of 10.017° across six common daily poses — the empirical basis for the ±10–25° tolerance bands in the templates.
|
||||
|
||||
3. **Lugaresi, C. et al. (2020 / MediaPipe team).** "On-device, Real-time Body Pose Tracking with MediaPipe BlazePose." Google Research Blog and arXiv:2006.10204. Defines the 33-landmark BlazePose topology used throughout §1.3 and §2.2. Confirms the landmark visibility score semantics used in §2.7.
|
||||
|
||||
4. **Google ML Kit team.** "Pose classification options." developers.google.com/ml-kit/vision/pose-detection/classifying-poses. Documents the `PoseClassifier` EfficientNet approach that this ADR rejects in §2.13; the 60% alignment threshold in §2.4 is consistent with the sample thresholds in this guide.
|
||||
|
||||
5. **Iyengar, B.K.S. (2001).** *Light on Yoga* (Schocken Books, revised edition). Chaturanga Dandasana description pp. 102–104: "elbows at right angles along the body" — the 90° elbow target for stage 5. Tadasana pp. 61–63: anatomical position as baseline. The Iyengar descriptions supply angle targets where Perez-Testor's Vicon study does not explicitly report a joint.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# ADR-149: Drone Swarm Benchmarking & Evaluation Methodology — Metrics, Leaderboards, and Statistical Rigor
|
||||
# ADR-171: Drone Swarm Benchmarking & Evaluation Methodology — Metrics, Leaderboards, and Statistical Rigor
|
||||
|
||||
| Field | Value |
|
||||
|------------|-----------------------------------------------------------------------------------------|
|
||||
|
|
@ -97,8 +97,8 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
|
|||
| [ADR-036](ADR-036-rvf-training-pipeline-ui.md) | Training Pipeline UI Integration | Proposed |
|
||||
| [ADR-043](ADR-043-sensing-server-ui-api-completion.md) | Sensing Server UI API Completion (14 endpoints) | Accepted |
|
||||
| [ADR-115](ADR-115-home-assistant-integration.md) | Home Assistant integration via MQTT auto-discovery + Matter bridge (HA-DISCO + HA-FABRIC + HA-MIND) | Accepted (MQTT track) / Proposed (Matter SDK P8b) |
|
||||
| [ADR-147](ADR-147-adam-mode-light-theme.md) | adam-mode — light theme toggle for the three.js realtime demo | Proposed |
|
||||
| [ADR-148](ADR-148-yoga-mode-pose-system.md) | yoga-mode — yoga pose detection, classification, and scoring for the three.js realtime demo | Proposed |
|
||||
| [ADR-169](ADR-169-adam-mode-light-theme.md) | adam-mode — light theme toggle for the three.js realtime demo | Proposed |
|
||||
| [ADR-170](ADR-170-yoga-mode-pose-system.md) | yoga-mode — yoga pose detection, classification, and scoring for the three.js realtime demo | Proposed |
|
||||
|
||||
### Architecture and infrastructure
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# ADR Corpus Census
|
||||
|
||||
Full per-ADR census underpinning ADR-164. **162 ADR entries across 156 distinct files** (6 duplicate-number collisions). Source of truth for the gap-analysis lenses. Where the census is uncertain it is marked *needs verification*.
|
||||
Full per-ADR census underpinning ADR-164. **162 ADR entries across 156 distinct files** (the 5 duplicate-number collisions / 6 displaced files have been RESOLVED — displaced files renumbered to ADR-166…171 per ADR-164 G1; the ADR-134 identity split is tracked separately under G3). Source of truth for the gap-analysis lenses. Where the census is uncertain it is marked *needs verification*.
|
||||
|
||||
| ADR | Title | Status | impl_state | Flags |
|
||||
|-----|-------|--------|-----------|-------|
|
||||
|
|
@ -53,10 +53,10 @@ Full per-ADR census underpinning ADR-164. **162 ADR entries across 156 distinct
|
|||
| ADR-047 | RuView Observatory — Three.js Visualization | Accepted (Implemented) | implemented | — |
|
||||
| ADR-048 | Adaptive CSI Activity Classifier | Accepted | implemented | depends on Proposed ADR-045 |
|
||||
| ADR-049 | Cross-Platform WiFi Detection & Graceful Degradation | Proposed | proposed-only | targets Python v1 legacy; abandonment risk |
|
||||
| ADR-050 | Provisioning Tool Enhancements | Proposed | partial | DUPLICATE NUMBER; partially fulfilled by ADR-060 |
|
||||
| ADR-050 | Quality Engineering Response — Security Hardening | Accepted | partial | DUPLICATE NUMBER; unverified claims (54K fps); findings #6-8 unconfirmed |
|
||||
| ADR-052 | DDD Bounded Contexts (appendix) | (none — appendix, no Status) | unknown | missing-status; DUPLICATE NUMBER; cross-ref errors (cites 044 for provisioning) |
|
||||
| ADR-052 | Tauri Desktop Frontend — Hardware Mgmt & Viz | Proposed | partial | DUPLICATE NUMBER; superseded_by ADR-054; status drift |
|
||||
| ADR-050 | Provisioning Tool Enhancements | Proposed | partial | keeps 050 (collision resolved); partially fulfilled by ADR-060 |
|
||||
| ADR-166 | Quality Engineering Response — Security Hardening | Accepted | partial | renumbered from ADR-050 (collision resolved); unverified claims (54K fps); findings #6-8 unconfirmed |
|
||||
| ADR-167 | DDD Bounded Contexts (appendix to ADR-052) | (none — appendix, no Status) | unknown | renumbered from ADR-052 (collision resolved); missing-status; cross-ref errors (cites 044 for provisioning) |
|
||||
| ADR-052 | Tauri Desktop Frontend — Hardware Mgmt & Viz | Proposed | partial | keeps 052 (collision resolved); superseded_by ADR-054; status drift |
|
||||
| ADR-053 | UI Design System — Dark Professional | Accepted | implemented | depends on Proposed ADR-052 |
|
||||
| ADR-054 | RuView Desktop Full Implementation | Accepted — in progress | partial | command matrix mostly Stub; espflash version drift vs 052 |
|
||||
| ADR-055 | Integrated Sensing Server in Desktop App | Accepted | implemented | — |
|
||||
|
|
@ -145,13 +145,13 @@ Full per-ADR census underpinning ADR-164. **162 ADR entries across 156 distinct
|
|||
| ADR-144 | UWB Range-Constraint Fusion | Proposed | partial | header stale (commit b10bc2e9a); no UWB radio in fleet |
|
||||
| ADR-145 | Ablation Evaluation Harness | Proposed | partial | referenced as existing by 149/150/151; F4/UWB variant HW-gated |
|
||||
| ADR-146 | RF Encoder Multi-Task Heads + Uncertainty | Proposed | proposed-only | no Impl note (unlike 141-144); depends on tch/libtorch |
|
||||
| ADR-147 | adam-mode — light theme toggle | Proposed | proposed-only | DUPLICATE NUMBER (3 files); referenced as landed by 148-yoga |
|
||||
| ADR-147 | Occupancy World Model (OccWorld/RoboOccWorld) | Accepted | partial | DUPLICATE NUMBER; self-revised from Cosmos; Phase B gated |
|
||||
| ADR-147 | Benchmark Proof — OccWorld on RTX 5080 | (none) | unknown | MISSING STATUS; DUPLICATE NUMBER; baseline-without-fine-tuning (random weights) |
|
||||
| ADR-148 | Drone Swarm Control System | In Progress | partial | DUPLICATE NUMBER; re-routes 147 Cosmos item to 149 |
|
||||
| ADR-148 | yoga-mode — pose detection/scoring demo | Proposed | proposed-only | DUPLICATE NUMBER; no tracking issue |
|
||||
| ADR-149 | AetherArena — Spatial-Intelligence Benchmark (HF) | Accepted | partial | DUPLICATE NUMBER; external repo out-of-tree; Wi-Pose dropped |
|
||||
| ADR-149 | Drone Swarm Benchmarking Methodology | Accepted (peer-reviewed) | partial | DUPLICATE NUMBER; critiques 148's own numbers |
|
||||
| ADR-169 | adam-mode — light theme toggle | Proposed | proposed-only | renumbered from ADR-147 (collision resolved); referenced by ADR-170 yoga |
|
||||
| ADR-147 | Occupancy World Model (OccWorld/RoboOccWorld) | Accepted | partial | keeps 147 (collision resolved); self-revised from Cosmos; Phase B gated |
|
||||
| ADR-168 | Benchmark Proof — OccWorld on RTX 5080 | (none) | unknown | renumbered from ADR-147 (collision resolved); MISSING STATUS; baseline-without-fine-tuning (random weights) |
|
||||
| ADR-148 | Drone Swarm Control System | In Progress | partial | keeps 148 (collision resolved); re-routes 147 Cosmos item to 149 |
|
||||
| ADR-170 | yoga-mode — pose detection/scoring demo | Proposed | proposed-only | renumbered from ADR-148 (collision resolved); no tracking issue |
|
||||
| ADR-149 | AetherArena — Spatial-Intelligence Benchmark (HF) | Accepted | partial | keeps 149 (collision resolved); external repo out-of-tree; Wi-Pose dropped |
|
||||
| ADR-171 | Drone Swarm Benchmarking Methodology | Accepted (peer-reviewed) | partial | renumbered from ADR-149 (collision resolved); critiques 148's own numbers |
|
||||
| ADR-150 | RuView RF Foundation Encoder | Proposed | partial | status Proposed but cites measured 81.63% in-domain vs ~11.6% cross-subject |
|
||||
| ADR-151 | Per-Room Calibration & Specialized Model Training | Accepted — Stages 1-5 impl | partial | HF-backbone distillation pending |
|
||||
| ADR-152 | WiFi-Pose SOTA 2026 Intake | Proposed | partial | header stale; §2.1-2.3/2.6 impl, WiFlow-STD ~96% PCK; 1/25 claim REFUTED |
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Research notes backing ADR-164. Each lens output is reproduced verbatim. Census:
|
|||
|
||||
## Lens 1: status-distribution
|
||||
|
||||
Confirmed: ADR-147-benchmark-proof.md and ADR-134-csi-to-cir have no `Status` line in their headers (the 052-ddd hits are Rust code in the body, not a header; the ADR-052 appendix lacks a real Status header per its first lines). Findings are evidence-grounded. Final analysis below.
|
||||
Confirmed: ADR-168-benchmark-proof.md (was ADR-147-benchmark-proof.md) and ADR-134-csi-to-cir have no `Status` line in their headers (the 167-ddd hits are Rust code in the body, not a header; the ADR-167 appendix, was ADR-052-ddd, lacks a real Status header per its first lines). Findings are evidence-grounded. Final analysis below.
|
||||
|
||||
### ADR Corpus — Status & Implementation Distribution
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ Census: **162 ADR entries** across **156 distinct files** (6 duplicate-number co
|
|||
| Proposed (incl. "Proposed — conditional/research-only") | ~88 |
|
||||
| Superseded | 1 (ADR-002) |
|
||||
| Rejected | 1 (ADR-098) |
|
||||
| Missing / no Status header | 3 (ADR-147-benchmark-proof, ADR-052-ddd appendix, ADR-134-CIR) |
|
||||
| Missing / no Status header | 3 (ADR-168-benchmark-proof [was 147], ADR-167-ddd appendix [was 052], ADR-134-CIR) |
|
||||
| Mixed/dual status in one ADR | 3 (ADR-115, ADR-149-AetherArena vs swarm, ADR-133) |
|
||||
|
||||
#### impl_state tally
|
||||
|
|
@ -31,29 +31,29 @@ Census: **162 ADR entries** across **156 distinct files** (6 duplicate-number co
|
|||
| partial | ~50 |
|
||||
| proposed-only | ~64 |
|
||||
| stale-or-contradicted | 3 (ADR-029, 030, 031) |
|
||||
| unknown | 5 (ADR-034, 044, 052-ddd, 147-proof, …) |
|
||||
| unknown | 5 (ADR-034, 044, 167-ddd [was 052], 168-proof [was 147], …) |
|
||||
| superseded | 1 (ADR-002) |
|
||||
|
||||
**Headline:** ~114 of 162 ADRs (70%) are decisions that never fully landed (proposed-only + partial + stale + unknown). The dominant failure mode is **stale Status headers** — Accepted/implemented work still labeled "Proposed."
|
||||
|
||||
#### SEVERITY: CRITICAL — Status header missing or structurally absent (cannot triage)
|
||||
|
||||
- **ADR-147-benchmark-proof.md** — *No `Status` header at all* (grep confirmed). Not a true ADR; it's a benchmark artifact (OccWorld @ ~213ms on RTX 5080, random weights) misfiled under the ADR-147 number. **Action: relocate to `docs/proof/` or `benchmarks/`, remove ADR number.**
|
||||
- **ADR-168-benchmark-proof.md** (renumbered from ADR-147 to resolve the 147 collision) — *No `Status` header at all* (grep confirmed). Not a true ADR; it's a benchmark artifact (OccWorld @ ~213ms on RTX 5080, random weights) that was misfiled under the ADR-147 number. **Action: relocate to `docs/proof/` or `benchmarks/`, remove ADR number.**
|
||||
- **ADR-134-csi-to-cir-time-domain-multipath.md** — *No `Status` header* (grep confirmed) in the header region. Body says Proposed but the field is not in canonical position. Compounded by a **number collision**: ADR-126/129 reference "ADR-134" as HOMECORE-MIGRATE, but the on-disk file is CIR. **Action: add canonical `## Status` line; resolve the 134 identity split.**
|
||||
- **ADR-052-ddd-bounded-contexts.md** — Appendix doc with no Status/Date header (grep found only Rust code, no header field). **Action: mark explicitly "Appendix to ADR-052 (no independent status)".**
|
||||
- **ADR-167-ddd-bounded-contexts.md** (renumbered from ADR-052 to resolve the 052 collision; still an appendix to parent ADR-052) — Appendix doc with no Status/Date header (grep found only Rust code, no header field). **Action: mark explicitly "Appendix to ADR-052 (no independent status)".**
|
||||
|
||||
#### SEVERITY: CRITICAL — Duplicate ADR numbers (6 collisions, all verified on disk)
|
||||
|
||||
| Number | Colliding files | Action |
|
||||
|---|---|---|
|
||||
| **147** | adam-mode-light-theme · nvidia-cosmos/OccWorld · benchmark-proof | Renumber 2 of 3 |
|
||||
| **148** | drone-swarm-control-system · yoga-mode-pose-system | Renumber 1 |
|
||||
| **149** | AetherArena-leaderboard · swarm-benchmarking | Renumber 1 |
|
||||
| **050** | provisioning-tool-enhancements · quality-engineering-security-hardening | Renumber 1 |
|
||||
| **052** | tauri-desktop-frontend · ddd-bounded-contexts (appendix) | Demote appendix |
|
||||
| **134** | csi-to-cir (on disk) · HOMECORE-MIGRATE (referenced, no file) | Resolve identity |
|
||||
| Number | Colliding files | Action | Resolution |
|
||||
|---|---|---|---|
|
||||
| **147** | adam-mode-light-theme · nvidia-cosmos/OccWorld · benchmark-proof | Renumber 2 of 3 | **RESOLVED** — 147 keeps nvidia-cosmos/OccWorld; benchmark-proof → **ADR-168**, adam-mode → **ADR-169** |
|
||||
| **148** | drone-swarm-control-system · yoga-mode-pose-system | Renumber 1 | **RESOLVED** — 148 keeps drone-swarm; yoga-mode → **ADR-170** |
|
||||
| **149** | AetherArena-leaderboard · swarm-benchmarking | Renumber 1 | **RESOLVED** — 149 keeps AetherArena; swarm-benchmarking → **ADR-171** |
|
||||
| **050** | provisioning-tool-enhancements · quality-engineering-security-hardening | Renumber 1 | **RESOLVED** — 050 keeps provisioning (5 refs vs 1); quality-engineering → **ADR-166** |
|
||||
| **052** | tauri-desktop-frontend · ddd-bounded-contexts (appendix) | Demote appendix | **RESOLVED** — 052 keeps tauri; ddd appendix renumbered → **ADR-167** (still linked to parent 052) |
|
||||
| **134** | csi-to-cir (on disk) · HOMECORE-MIGRATE (referenced, no file) | Resolve identity | Identity split (not a filename collision); resolved separately via G3 → ADR-165 |
|
||||
|
||||
These break the ADR index and `/adr` tooling — two ADRs answering to one number is a corpus-integrity defect, not cosmetics.
|
||||
These broke the ADR index and `/adr` tooling — two ADRs answering to one number is a corpus-integrity defect, not cosmetics. The five filename collisions are now resolved (six displaced files renumbered 166–171); see ADR-164 Gap Register G1.
|
||||
|
||||
#### SEVERITY: HIGH — Status header stale vs. shipped reality (Proposed header on landed code)
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ Cluster heads where the whole chain is Proposed with zero implementation evidenc
|
|||
|
||||
#### Ranked actionable backlog (do in this order)
|
||||
|
||||
1. **Resolve 6 duplicate ADR numbers + 3 missing-header files** (CRITICAL — breaks the index/tooling). Renumber 147×2, 148, 149, 050; demote 052-ddd appendix; resolve the 134 identity split; add Status headers to 147-proof, 134, 052-ddd.
|
||||
1. **Resolve 6 duplicate ADR numbers + 3 missing-header files** (CRITICAL — breaks the index/tooling). **Number collisions RESOLVED:** renumbered 147×2 (benchmark-proof→168, adam-mode→169), 148 (yoga→170), 149 (swarm-benchmarking→171), 050 (quality-engineering→166), 052 ddd appendix→167. Remaining: resolve the 134 identity split (done via G3→165); add Status headers to 168-proof, 134, 167-ddd (owner-gated).
|
||||
2. **Bulk-flip the 10 streaming-engine headers (ADR-136–145)** from Proposed → "Accepted — partial" — they have commit-pinned, test-backed Implementation Status notes. Highest ROI: one batch fixes the largest stale-status cluster.
|
||||
3. **Fix the status-graph inversions** (032/053/048/077 depend on Proposed parents; promote parents 029/030/031/045/052/075/076 to match their built reality, or downgrade the dependents).
|
||||
4. **Reconcile CLAUDE.md vs ADR headers** for 017, 024, 027, 072, 152 (doc says one thing, header another).
|
||||
|
|
@ -184,7 +184,7 @@ The sweep (ADR-154–163) is itself a structured retraction layer: each "Beyond-
|
|||
|
||||
**[MEDIUM] ADR-098 → ADR-099 partial reversal.** ADR-098 **Rejected** midstream as a system component; ADR-099 (Proposed) **adopts** midstream's temporal-compare (DTW) + temporal-attractor-studio as a parallel tap. Framed as "complementary," but it revives the exact carve-outs ADR-098 declined to integrate — a live decision conflict pending resolution.
|
||||
|
||||
**[MEDIUM] ADR-147 (OccWorld) self-retracts Cosmos.** The accepted ADR-147 title/decision was revised from "NVIDIA Cosmos WFM Integration" to OccWorld after a hardware finding (Cosmos needs 32.5 GB VRAM); Cosmos is retracted as primary. The companion ADR-147-benchmark-proof reports 213 ms/inference on **random weights, no checkpoint** — a baseline-without-fine-tuning number that must not be cited as a quality/target metric.
|
||||
**[MEDIUM] ADR-147 (OccWorld) self-retracts Cosmos.** The accepted ADR-147 title/decision was revised from "NVIDIA Cosmos WFM Integration" to OccWorld after a hardware finding (Cosmos needs 32.5 GB VRAM); Cosmos is retracted as primary. The companion ADR-168-benchmark-proof (renumbered from ADR-147) reports 213 ms/inference on **random weights, no checkpoint** — a baseline-without-fine-tuning number that must not be cited as a quality/target metric.
|
||||
|
||||
#### B. Pairs making CONFLICTING decisions on the same topic
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ A facade hides its failures. We document ours in detail:
|
|||
a 20 KB int4 edge model, with the quantization trade-offs shown.
|
||||
- **Retractions** — the "100% presence" figure was withdrawn in-place rather than quietly
|
||||
edited away.
|
||||
- **[ADR-147 benchmark proof](adr/ADR-147-benchmark-proof.md)** and
|
||||
- **[ADR-168 benchmark proof](adr/ADR-168-benchmark-proof.md)** and
|
||||
**[WITNESS-LOG-028](WITNESS-LOG-028.md)** — how the numbers are produced and a 33-row
|
||||
per-claim attestation matrix.
|
||||
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ Role mapping is normative per ADR-136 §2.1; maturity is this review's judgment
|
|||
| **signal** | `wifi-densepose-signal` (incl. `ruvsense/`) | 6-stage pipeline (`ruvsense/mod.rs:9-23`), `cir.rs`, `calibration.rs`, `hampel.rs`, `fresnel.rs`, `phase_sanitizer.rs` | 473 | **Production** (unit level); live multistatic wiring **beta** | §3 below; ADR-014 Accepted, ADR-029 Proposed |
|
||||
| **fusion** | `ruvsense/multistatic.rs`, `ruvsense/fusion_quality.rs`, `wifi-densepose-ruvector/src/viewpoint/` | `MultistaticFuser`, `QualityScore`, `CrossViewpointAttention`, GDI/Cramér-Rao (`viewpoint/geometry.rs`) | 20 (multistatic.rs), 3 (fusion_quality.rs), 136 (ruvector crate) | **Beta** — tested building blocks, composed only in `wifi-densepose-engine` tests | `viewpoint/mod.rs:1-30`; engine `lib.rs:317-319` |
|
||||
| **world** | `homecore`, `wifi-densepose-worldgraph`, `wifi-densepose-geo`, `wifi-densepose-worldmodel` | `StateMachine`, `EventBus`, `WorldGraph` (rooms/sensors/person-tracks/semantic states), ENU geo registration | 9+11, 7, 16+1, 12+1 | **Beta** — homecore is explicit "P1 scaffold"; persistence/service dispatch deferred to P2 | `homecore/src/lib.rs:7, 24-31`; ADR-127 Proposed |
|
||||
| **models** | `cog-pose-estimation`, `cog-person-count`, `wifi-densepose-nn`, `wifi-densepose-train`, `wifi-densepose-occworld-candle` | ONNX/Candle inference, training pipeline, OccWorld bridge | 7, 15, 30+1, 312, 12 | **Experimental** — no trained RF foundation encoder exists; ADR-147 benchmarked OccWorld with **random weights** | `ADR-147-benchmark-proof.md` ("random weights — pre-domain-fine-tuning baseline"); ADR-146/150 Proposed |
|
||||
| **models** | `cog-pose-estimation`, `cog-person-count`, `wifi-densepose-nn`, `wifi-densepose-train`, `wifi-densepose-occworld-candle` | ONNX/Candle inference, training pipeline, OccWorld bridge | 7, 15, 30+1, 312, 12 | **Experimental** — no trained RF foundation encoder exists; ADR-147 benchmarked OccWorld with **random weights** | `ADR-168-benchmark-proof.md` ("random weights — pre-domain-fine-tuning baseline"); ADR-146/150 Proposed |
|
||||
| **privacy** | `wifi-densepose-bfld` | `privacy_gate.rs`, `privacy_mode.rs` (mode registry + hash-chained attestation), `identity_risk.rs`, `signature_hasher.rs`, `embedding_ring.rs` | 369 | **Beta** — strongest-tested layer, but lib header still says "Status: P1 in progress" (`lib.rs:12`, stale vs 20 implemented modules) | ADR-118–123, 141 all Proposed |
|
||||
| **store** | `homecore-recorder` | trajectory/event recording | 8+12 | **Experimental** | ADR-136 §2.1 |
|
||||
| **api** | `homecore-api`, `homecore-server`, `cog-ha-matter`, `homecore-hap` | REST/WS, HA discovery, Matter, HomeKit | 7+11, 0, 63+1, 15+2 | **Experimental→Beta** (`homecore-server` has zero tests) | ADR-130/125/115 Proposed |
|
||||
| **eval** | `wifi-densepose-train/src/ablation.rs`, `ruview-swarm/src/evals/` | ablation harness (ADR-145), swarm eval suite (ADR-149) | included in 312 / 115 | **Experimental** — ADR-145 self-labels "skeleton/scaffolding, mostly not yet on the live 20 Hz path" | `ablation.rs` exists; ADR-149 (swarm benchmarking) Accepted |
|
||||
| **eval** | `wifi-densepose-train/src/ablation.rs`, `ruview-swarm/src/evals/` | ablation harness (ADR-145), swarm eval suite (ADR-171) | included in 312 / 115 | **Experimental** — ADR-145 self-labels "skeleton/scaffolding, mostly not yet on the live 20 Hz path" | `ablation.rs` exists; ADR-171 (swarm benchmarking, renumbered from ADR-149) Accepted |
|
||||
| **observe** | `homecore-automation`, `homecore-assist` | automation engine, assistant/Ruflo bridge | 20+14, 3+20 | **Experimental** | ADR-129/133 Proposed |
|
||||
| **(integration root)** | `wifi-densepose-engine` | `StreamingEngine`, `TrustedOutput`, privacy demotion, witness | 11 | **Beta** — the only crate that proves cross-role composition; not on a live I/O path | `engine/src/lib.rs:1-29, 457-751` |
|
||||
| **(swarm)** | `ruview-swarm` | Raft/gossip topology, RRT-APF planning, Candle PPO MARL, CSI sensing payload, failsafe, Ruflo | 115+19 | **Experimental/simulation** — M3 needs real ESP32-S3 hardware | ADR-148:940-953 ("Overall ~98%", M3 85%) |
|
||||
|
|
@ -148,7 +148,7 @@ This is genuinely strong design. But all inputs are synthetic `MultiBandCsiFrame
|
|||
| R5 | **Float nondeterminism in fusion** across thread counts could silently break the witness/replay contract once wired | Medium | High | ADR-136 §3.3 risk table (project's own assessment) |
|
||||
| R6 | **Privacy bypass via unwired paths**: BFLD invariants are enforced per-module, but until the engine is the *only* route from ingest to API, a sensing-server endpoint can emit ungated state (sensing-server already has 30+ modules incl. pose/vitals APIs predating the control plane) | Medium | Critical | `sensing-server/src/` module list vs engine isolation |
|
||||
| R7 | **Hardware dependence + scale**: multistatic TDMA/channel-hopping timing validated on small ESP32 sets; ADR-148 M3 explicitly blocked on real hardware; clock-quality model in engine uses a hardcoded `ClockQualityScore` (`engine/src/lib.rs:384`) | Medium | High | ADR-148:946; hardcoded 50 µs stdev |
|
||||
| R8 | **ADR/doc/status drift**: 150 ADRs with near-universal "Proposed" status, stale in-source status headers (`bfld/src/lib.rs:12`), CLAUDE.md "16 ruvsense modules" vs 22 on disk, duplicate ADR numbers (two ADR-050s, two ADR-147s, two ADR-149s, ADR-052 ×2) — institutional-memory value degrades | High | Medium | `ls docs/adr/`; this review §3 |
|
||||
| R8 | **ADR/doc/status drift**: 150 ADRs with near-universal "Proposed" status, stale in-source status headers (`bfld/src/lib.rs:12`), CLAUDE.md "16 ruvsense modules" vs 22 on disk, duplicate ADR numbers (two ADR-050s, two ADR-147s, two ADR-149s, ADR-052 ×2 — **now RESOLVED: displaced files renumbered to ADR-166…171 per ADR-164 G1**) — institutional-memory value degrades | High | Medium | `ls docs/adr/`; this review §3 |
|
||||
| R9 | **Workspace breadth vs maintenance capacity**: 38 workspace crates + 4 vendored subtrees + Python archive + firmware; several crates have 0 tests (`homecore-server`, `nvsim-server`, `wifi-densepose-wasm`, `homecore-plugin-example`); bus factor appears to be ~1 | High | Medium | crate test-count table §2 |
|
||||
| R10 | **Eval debt**: no end-to-end accuracy benchmark on real CSI with ground truth exists in-repo (ADR-145 harness is scaffolding; ADR-079 camera ground truth not exercised here) — "beyond SOTA" claims are currently unfalsifiable | High | High | ADR-145 status note; absence of ground-truth datasets in tree |
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ published from the layer it lives at.
|
|||
|-------|----------------|---------|-----------|-------------|
|
||||
| **L0** Unit/integration tests | Code correctness | `cargo test --workspace --no-default-features` + pytest | per commit | exact |
|
||||
| **L1** Deterministic proof + witness bundle | Pipeline is real, unchanged, reproducible | `archive/v1/data/proof/verify.py`, `scripts/generate-witness-bundle.sh` | per merge / release | exact (SHA-256) |
|
||||
| **L2** Criterion micro-benchmarks | Compute latency only — never quality (ADR-149 §2) | 15 bench targets across `v2/crates/*/benches/` | nightly / pre-release | statistical |
|
||||
| **L2** Criterion micro-benchmarks | Compute latency only — never quality (ADR-171 §2) | 15 bench targets across `v2/crates/*/benches/` | nightly / pre-release | statistical |
|
||||
| **L3** Dataset-level accuracy eval | Pose/presence/vitals quality vs published SOTA | MM-Fi / Wi-Pose (ADR-015), `ruview_metrics.rs` tiers, ADR-145 ablation harness | per model release | seeded |
|
||||
| **L4** Hardware-in-loop | Real CSI on real ESP32, no mocks | COM9 (S3) / COM12 (C6) protocol, witness firmware hashes | per firmware release | A/B controlled |
|
||||
| **L5** Field trials / live capture | End-to-end behavior in a real room | live-session captures (e.g. `benchmark_baseline.json`) | campaign | statistical |
|
||||
|
|
@ -69,7 +69,7 @@ from the check inventory.
|
|||
|
||||
### 1.3 L2 — Criterion micro-benchmark inventory (all 15 targets)
|
||||
|
||||
All bench sources read directly. Per ADR-149 §2 these are **latency regression gates
|
||||
All bench sources read directly. Per ADR-171 §2 these are **latency regression gates
|
||||
only, never quality evidence**.
|
||||
|
||||
| Bench target | Crate | Benchmark functions / groups | What it measures | Recorded value or in-source target (citation) |
|
||||
|
|
@ -86,7 +86,7 @@ only, never quality evidence**.
|
|||
| `detection_bench.rs` | wifi-densepose-mat | `breathing_detection`, `heartbeat_detection`, `movement_classification`, `detection_pipeline`, localization (triangulation/depth), alert generation | MAT survivor-detection algorithms at varying signal lengths / noise | no recorded baseline |
|
||||
| `transport_bench.rs` | wifi-densepose-hardware | `beacon_serialize_16byte/28byte_auth/quic_framed`, `auth_beacon_verify`, `replay_window`, `framed_message` encode/decode, `secure_tdm_cycle` (manual vs QUIC) | TDM beacon crypto + transport | no recorded baseline |
|
||||
| `mqtt_throughput.rs` | wifi-densepose-sensing-server | `discovery::build_*`, `state::*`, `rate_limiter::allow_*`, `privacy::decide_*`, `semantic::bus_tick_all_10_primitives` | ADR-115 MQTT hot path | Targets (header): discovery **<5 µs**, state encode **<2 µs**, rate limit **<100 ns**, privacy **<50 ns**, bus tick **<10 µs** |
|
||||
| `swarm_bench.rs` | ruview-swarm | `marl_actor_inference`, `rrt_apf_100iter`, `multiview_fusion_3drones`, `demo_coverage_estimate`, `ppo_update_64transitions` | ADR-148 swarm control-loop compute | Measured: **3.3 µs / 43 µs / 54–58.5 ns / 100 ps / 248 µs** (ADR-149 §4.3; `CHANGELOG.md` Performance section) |
|
||||
| `swarm_bench.rs` | ruview-swarm | `marl_actor_inference`, `rrt_apf_100iter`, `multiview_fusion_3drones`, `demo_coverage_estimate`, `ppo_update_64transitions` | ADR-148 swarm control-loop compute | Measured: **3.3 µs / 43 µs / 54–58.5 ns / 100 ps / 248 µs** (ADR-171 §4.3; `CHANGELOG.md` Performance section) |
|
||||
| `pipeline_throughput.rs` | nvsim | `pipeline_run` (sample-count sweep), `witness::run` vs `run_with_witness` | NV-diamond sim throughput + witness overhead | Acceptance: **≥1 kHz** simulated samples/s on Cortex-A53-class CPU — bench header |
|
||||
| `state_machine.rs` | homecore | `set` first/warm/no-op, `get` hit/miss, `all_snapshot`, `all_by_domain_light_20_of_100`, `broadcast_fan_out` | HOMECORE state-machine hot paths | no recorded baseline |
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ file itself); its producer must be identified and committed (§5.3). Summary val
|
|||
| `person_count_changes` | 10 |
|
||||
|
||||
Criterion latencies that *have* been recorded live in ADR documents instead
|
||||
(ADR-147-benchmark-proof.md, ADR-149 §4.3, CHANGELOG Performance) — §5 below defines
|
||||
(ADR-168-benchmark-proof.md, ADR-171 §4.3, CHANGELOG Performance) — §5 below defines
|
||||
how to consolidate them into a real machine-readable criterion baseline.
|
||||
|
||||
### 1.4 L3 — Dataset-level accuracy evaluation
|
||||
|
|
@ -150,7 +150,7 @@ how to consolidate them into a real machine-readable criterion baseline.
|
|||
### 1.6 L5 — Field trials
|
||||
|
||||
Live multi-node sessions captured as JSONL/JSON with summary statistics —
|
||||
`benchmark_baseline.json` (§1.3) is the existing exemplar. ADR-149 §6 adds the seeded
|
||||
`benchmark_baseline.json` (§1.3) is the existing exemplar. ADR-171 §6 adds the seeded
|
||||
`evals/` episode harness (Stage 1 kinematic full-matrix, Stage 2 Gazebo/PX4 SITL on the
|
||||
3 median seeds) for the swarm domain.
|
||||
|
||||
|
|
@ -168,42 +168,42 @@ statistical procedure of §3 followed. Current axes with measured status:
|
|||
| Edge efficiency frontier | torso-PCK@20 at deployed precision + params + batch-1 latency | same | MultiFormer 72.25% at full size | Pareto-dominance: smaller **and** above 72.25% at the deployed precision | int8 73.5 KB **74.70%**; int4-QAT 36.7 KB **74.46%**; shipped int4 verified **74.08%**, 0.135 ms 1-thread x86 (same file) |
|
||||
| Cross-subject generalization | torso-PCK@20, official MM-Fi cross-subject split (256,608 train / 64,152 test) | leakage-free split | own zero-shot baseline 63.99% | ADR-150 §4 gate: **+≥6 pts cross-subject without losing >2 pts random-split** | Best zero-shot **64.92%** (mixup+TTA+3-seed); gate judged unreachable without new capture (ADR-150 §3.2) |
|
||||
| Few-shot calibration (deployment) | PCK@20 after K labeled in-room samples; adapter size | MM-Fi cross-subject & cross-environment splits | zero-shot (64% / 10.6%) | SOTA-level (≳72%) from ≤200 samples with ≤~11 KB per-room adapter | cross-subject ~**72%** @100–200 samples (3 seeds); cross-env **10.6→73.1%** @200, 60.1% @5 (ADR-150 §3.5–3.6) |
|
||||
| Swarm SAR localization | CEP50/CEP95 (m), GDOP-stratified | seeded episode distribution (ADR-149 §6), not single geometry | Wi2SAR **5 m** (arxiv 2604.09115, paper-to-paper) | CEP50 < 5 m, IQM over ≥10 seeds, 95% CI excluding 5 m | 1.732 m single synthetic geometry — graded **Low–Medium**, not yet claimable (ADR-149 §7) |
|
||||
| Swarm coverage | coverage-rate@240 s; time-to-95% | episode rollouts | Wi2SAR 160k m²/13.5 min | rollout (not analytic) mean+CI beating baseline | 223 s is an analytic estimate — graded **Low** (ADR-149 §7) |
|
||||
| Control-loop latency | criterion wall-clock | local hardware, named | 10 ms / 100 Hz budget | all stages ≪ budget | 3.3 µs MARL / 43 µs RRT-APF / 54 ns fusion / 248 µs PPO (ADR-149 §4.3) |
|
||||
| World-model trajectory | MDE (m) at 5-frame horizon | RuView CSI-derived occupancy | pre-fine-tune random-weight baseline 9.49 m MDE | **≤1.0 m (2.0 vox)** at 5-frame horizon (ADR-147 §5 target, cited in benchmark-proof §4) | 9.49 m / FDE 16.23 m random weights; 208.45 ms median latency on real CSI (ADR-147-benchmark-proof §4, §7) |
|
||||
| Swarm SAR localization | CEP50/CEP95 (m), GDOP-stratified | seeded episode distribution (ADR-171 §6), not single geometry | Wi2SAR **5 m** (arxiv 2604.09115, paper-to-paper) | CEP50 < 5 m, IQM over ≥10 seeds, 95% CI excluding 5 m | 1.732 m single synthetic geometry — graded **Low–Medium**, not yet claimable (ADR-171 §7) |
|
||||
| Swarm coverage | coverage-rate@240 s; time-to-95% | episode rollouts | Wi2SAR 160k m²/13.5 min | rollout (not analytic) mean+CI beating baseline | 223 s is an analytic estimate — graded **Low** (ADR-171 §7) |
|
||||
| Control-loop latency | criterion wall-clock | local hardware, named | 10 ms / 100 Hz budget | all stages ≪ budget | 3.3 µs MARL / 43 µs RRT-APF / 54 ns fusion / 248 µs PPO (ADR-171 §4.3) |
|
||||
| World-model trajectory | MDE (m) at 5-frame horizon | RuView CSI-derived occupancy | pre-fine-tune random-weight baseline 9.49 m MDE | **≤1.0 m (2.0 vox)** at 5-frame horizon (ADR-147 §5 target, cited in benchmark-proof §4) | 9.49 m / FDE 16.23 m random weights; 208.45 ms median latency on real CSI (ADR-168-benchmark-proof §4, §7) |
|
||||
| Privacy leakage | MIA `leakage_score = 2·(AUC−0.5)` | fixed replay, fixed-seed shadow classifier | chance (0) | ≤ **0.05** (attacker AUC ≤ 0.525) | gate defined, harness built (ADR-145 §2.3) |
|
||||
| Vitals (hardware) | BPM error vs wearable ground truth | live A/B board protocol | control board behavior | within physiological agreement of ground truth, stable spread | 88–91 BPM vs 87 GT, spread 59→0 (CHANGELOG #987) |
|
||||
|
||||
### Claim-language discipline (from ADR-149 §7 grading)
|
||||
### Claim-language discipline (from ADR-171 §7 grading)
|
||||
|
||||
| Evidence | Permitted language |
|
||||
|---|---|
|
||||
| Single run / single geometry / analytic estimate | "directional", never "beats SOTA" |
|
||||
| Seeded multi-run with CIs vs paper baseline | "exceeds the published X result paper-to-paper" |
|
||||
| Same metric, same split, same protocol, CI excludes baseline | "beyond SOTA on <dataset>/<split>" |
|
||||
| No public leaderboard exists (swarm CSI-SAR) | never claim "leaderboard standing" (ADR-149 §3) |
|
||||
| No public leaderboard exists (swarm CSI-SAR) | never claim "leaderboard standing" (ADR-171 §3) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Statistical Procedure for Honest Claims
|
||||
|
||||
Adopted from ADR-149 §5 (Agarwal 2021 / Gorsane 2022 standard) and the practices
|
||||
Adopted from ADR-171 §5 (Agarwal 2021 / Gorsane 2022 standard) and the practices
|
||||
already used in ADR-150/efficiency-frontier measurements:
|
||||
|
||||
1. **Seeds.** ≥10 independent seeds for RL/episodic claims (ADR-149 §5); ≥3 seeds
|
||||
1. **Seeds.** ≥10 independent seeds for RL/episodic claims (ADR-171 §5); ≥3 seeds
|
||||
minimum for supervised dataset evals (ADR-150 §3.5 used 3 seeds; report all).
|
||||
Training seeds, eval seeds, and split files are versioned and committed.
|
||||
2. **Aggregate.** IQM (not mean/median) for episodic metrics + performance profiles;
|
||||
for dataset accuracy report mean across seeds with each seed's value listed.
|
||||
3. **Confidence intervals.** 95% stratified bootstrap, 1,000 resamples (ADR-149 §5;
|
||||
3. **Confidence intervals.** 95% stratified bootstrap, 1,000 resamples (ADR-171 §5;
|
||||
reference impl: `rliable`).
|
||||
4. **Paired comparisons.** When comparing model A vs B (e.g. `csi_plus_cir` vs
|
||||
`csi_only`, or ours vs a reproduced baseline), evaluate both on the **identical
|
||||
frozen test frames** and use a paired bootstrap over per-sample correctness
|
||||
(PCK hit/miss is per-joint binary — pair at the joint-sample level). For
|
||||
paper-to-paper comparisons where the baseline cannot be re-run, state so
|
||||
explicitly ("paper-to-paper", ADR-149 §2) and require the CI lower bound to clear
|
||||
explicitly ("paper-to-paper", ADR-171 §2) and require the CI lower bound to clear
|
||||
the published point value.
|
||||
5. **Pre-registration.** The threshold lives in an ADR **before** the run
|
||||
(precedent: ADR-150 §4 gate written before §3.2 measurements; the measurements
|
||||
|
|
@ -212,9 +212,9 @@ already used in ADR-150/efficiency-frontier measurements:
|
|||
capacity-hurts, and KD-didn't-help results in the record — required practice.
|
||||
7. **Eval episodes (swarm):** 50 fixed, versioned episodes per policy
|
||||
(10 victim layouts × 5 CSI-noise levels), ≥3 baselines (random walk,
|
||||
boustrophedon+triangulation, IPPO) (ADR-149 §5).
|
||||
boustrophedon+triangulation, IPPO) (ADR-171 §5).
|
||||
8. **GDOP stratification** for any localization claim, so geometry artifacts cannot
|
||||
produce the headline (ADR-149 §6.3).
|
||||
produce the headline (ADR-171 §6.3).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ already used in ADR-150/efficiency-frontier measurements:
|
|||
|
||||
### 4.2 Criterion baseline file (replaces the current gap)
|
||||
|
||||
Today criterion numbers live in prose (ADR-147-benchmark-proof, ADR-149 §4.3,
|
||||
Today criterion numbers live in prose (ADR-168-benchmark-proof, ADR-171 §4.3,
|
||||
CHANGELOG). Formalize:
|
||||
|
||||
1. `cargo bench --workspace -- --save-baseline main` on a **named, fixed runner**
|
||||
|
|
@ -293,7 +293,7 @@ Anyone outside the project must be able to re-run every claimed result:
|
|||
(`calibration_proof_runner.rs` pattern, ADR-145 §2.6) for libm portability.
|
||||
3. **Seeds are constants, committed:** `PROOF_SEED=42`, `MODEL_SEED=0`
|
||||
(`proof.rs`, ADR-015 Phase 5); dataset splits committed as `.npy`
|
||||
(`split_random.npy`); swarm configs as versioned YAML with all seeds (ADR-149 §5).
|
||||
(`split_random.npy`); swarm configs as versioned YAML with all seeds (ADR-171 §5).
|
||||
4. **Artifacts carry hashes.** Published model artifacts include SHA-256 (HuggingFace
|
||||
`pose_micro_int4.npz`, sha256 `c03eeb…` — efficiency-frontier doc); witness bundle
|
||||
has a `MANIFEST.sha256` over every file; provenance fields
|
||||
|
|
@ -318,9 +318,9 @@ Anyone outside the project must be able to re-run every claimed result:
|
|||
| 1 | **Subject leakage / split optimism.** In-domain `random_split` has temporal/subject-adjacency effects; the same model family scores 83.6% random-split but ~11.6% torso-PCK on the leakage-free cross-subject split | efficiency-frontier "Controlled claim" footnote; ADR-150 §1, §3.2 | Always report the split name; publish random-split and cross-subject numbers side by side; cross-subject claims only on the official split |
|
||||
| 2 | **Per-environment overfitting.** Zero-shot cross-environment collapses to 10.6%; subject-scaling saturates ~63.7% past 16–20 subjects because the residual is room/device shift | ADR-150 §3.3, §3.6 | Cross-room degradation + 17-joint heatmap in every ablation (ADR-145 §2.5); claim deployment accuracy only with the calibration protocol stated (K samples, adapter size) |
|
||||
| 3 | **Mock-mode contamination.** Mock firmware missed a real Kconfig threshold bug; the nn crate ships a `mock_inference` criterion group that must never be quoted as pipeline performance | `CLAUDE.md` firmware rule 7; `inference_bench.rs` `bench_mock_inference` | L4 mandatory before firmware release ("Always test with real WiFi CSI, not mock mode"); label mock benches in reports; ADR-147 §7 re-ran the benchmark on real CSI explicitly "no mocks" |
|
||||
| 4 | **Single-run point estimates.** 1.732 m localization from one synthetic geometry; 223 s coverage from an analytic formula | ADR-149 §1, §7 | §3 seed/CI protocol; evidence-grade table before publication |
|
||||
| 5 | **Random-weight / untrained baselines read as results.** OccWorld MDE 9.49 m is a pre-fine-tuning random-weight reading | ADR-147-benchmark-proof §4 | Label baseline-vs-target explicitly; never aggregate untrained-model numbers into capability claims |
|
||||
| 6 | **Latency conflated with quality.** Criterion µs numbers prove no compute bottleneck, nothing about accuracy | ADR-149 §2, §4.3 | L2 is gate-only; quality claims live in L3+ |
|
||||
| 4 | **Single-run point estimates.** 1.732 m localization from one synthetic geometry; 223 s coverage from an analytic formula | ADR-171 §1, §7 | §3 seed/CI protocol; evidence-grade table before publication |
|
||||
| 5 | **Random-weight / untrained baselines read as results.** OccWorld MDE 9.49 m is a pre-fine-tuning random-weight reading | ADR-168-benchmark-proof §4 | Label baseline-vs-target explicitly; never aggregate untrained-model numbers into capability claims |
|
||||
| 6 | **Latency conflated with quality.** Criterion µs numbers prove no compute bottleneck, nothing about accuracy | ADR-171 §2, §4.3 | L2 is gate-only; quality claims live in L3+ |
|
||||
| 7 | **Floating-point nondeterminism breaking proofs.** SciPy FFT SIMD reordering + multithreaded BLAS produced different hashes across CI microarchitectures | CHANGELOG #560; `calibration_proof_runner.rs` lines 1–13 (cited in ADR-145 §2.3) | Quantize before hashing; pin thread env vars; exclude wall-clock from hashes |
|
||||
| 8 | **Hash churn without procedure.** Three distinct historical values of the proof hash exist (`8c0680d7…` ADR-028, `667eb054…` CHANGELOG #560, `f8e76f21…` current file) | cited files | Every regeneration via `--generate-hash` + re-verify + CHANGELOG entry + witness bundle refresh |
|
||||
| 9 | **Aggregation bugs masking accuracy.** Person count clamped to 1 by EMA mapping; eigenvalue path leaking counts up to 10; both invisible to unit tests for months | CHANGELOG #803, #894 | L5 summary gates on `person_count_changes`/count distributions; convergence tests replaying the live loop |
|
||||
|
|
@ -336,7 +336,7 @@ Anyone outside the project must be able to re-run every claimed result:
|
|||
| Machine-readable criterion baseline (`v2/benchmarks/criterion-baseline.json`) + CI comparison job | L2 | §4.2 (numbers currently only in ADR prose) |
|
||||
| Provenance + producer script for `benchmark_baseline.json`; soft-gate job | L5 | §1.3, §4.3 (zero code references today) |
|
||||
| `ruview-cli --ablation mode=auto` wiring + `expected_ablation_<slug>.sha256` (currently placeholders → exit 2) | L3 | ADR-145 implementation status |
|
||||
| Seeded swarm `evals/` harness + `evals/RESULTS.md` internal leaderboard | L3/L5 | ADR-149 §6, §8 open issues |
|
||||
| Seeded swarm `evals/` harness + `evals/RESULTS.md` internal leaderboard | L3/L5 | ADR-171 §6, §8 open issues |
|
||||
| Fix `VERIFY.sh` hardcoded verdict count; reconcile `CLAUDE.md` "7/7" | L1 | §1.2 |
|
||||
| Curated paired room-A/room-B labeled replay set (frozen, SHA-pinned, never trained on) | L3 | ADR-145 §3.2 |
|
||||
| ARM/edge on-device latency validation for the int4 model (x86-only today) | L4 | efficiency-frontier doc ("Pi fleet pending") |
|
||||
|
|
@ -372,8 +372,8 @@ failing test, not a slogan.
|
|||
---
|
||||
|
||||
*All values cited from: `benchmark_baseline.json`, `v2/crates/*/benches/*.rs` (15
|
||||
files), `docs/adr/ADR-147-benchmark-proof.md`,
|
||||
`docs/adr/ADR-149-swarm-benchmarking-evaluation-methodology.md`,
|
||||
files), `docs/adr/ADR-168-benchmark-proof.md`,
|
||||
`docs/adr/ADR-171-swarm-benchmarking-evaluation-methodology.md`,
|
||||
`docs/adr/ADR-145-ablation-eval-harness-privacy-leakage.md`,
|
||||
`docs/adr/ADR-028-esp32-capability-audit.md`,
|
||||
`docs/adr/ADR-015-public-dataset-training-strategy.md`,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ validation pass run against the working tree.
|
|||
| [00-system-review.md](00-system-review.md) | Capability audit of the current engine | Signal layer is the deepest asset (`ruvsense/` ≈14.4k lines, 310 in-module tests); the model tier is the emptiest (no trained checkpoint in-tree); the live 20 Hz path is the main integration gap |
|
||||
| [01-sota-landscape-2026.md](01-sota-landscape-2026.md) | Published SOTA per capability axis (web-verified) | Defines the beyond-SOTA bar: 12-row capability → published SOTA → RuView-today → target table; IEEE 802.11bf-2025 is ratified and moves the moat up-stack |
|
||||
| [02-beyond-sota-architecture.md](02-beyond-sota-architecture.md) | Target architecture | 8 pillars (RF foundation encoder + UQ heads, differentiable RF forward model, RF-SLAM×WorldGraph loop, camera→RF distillation, swarm apertures, continual adaptation, deterministic WASM edge, NV fusion) — all landing inside existing crates, no rewrite (per ADR-136 §2.1) |
|
||||
| [03-benchmark-validation-methodology.md](03-benchmark-validation-methodology.md) | Test/validation/benchmark methodology | 6-layer validation pyramid; 15 criterion bench targets inventoried; `benchmark_baseline.json` is a live-capture anchor, not a criterion baseline; statistical protocol from ADR-149 (≥10 seeds, IQM, bootstrap CIs) |
|
||||
| [03-benchmark-validation-methodology.md](03-benchmark-validation-methodology.md) | Test/validation/benchmark methodology | 6-layer validation pyramid; 15 criterion bench targets inventoried; `benchmark_baseline.json` is a live-capture anchor, not a criterion baseline; statistical protocol from ADR-171 (≥10 seeds, IQM, bootstrap CIs) |
|
||||
| [04-optimization-roadmap.md](04-optimization-roadmap.md) | Performance review + 90-day plan | ISTA CIR solver is the dominant latency hazard (~1.1 GFLOP/frame at HE40); exact zero-risk wins identified; WorldGraph grows unboundedly (no eviction) — a real bug-class |
|
||||
|
||||
## Validation results (this session, 2026-06-09)
|
||||
|
|
@ -83,7 +83,7 @@ Correctness post-optimization: `wifi-densepose-signal` 456 tests green;
|
|||
|
||||
1. **"Beyond SOTA" is currently unfalsifiable** without a real-CSI
|
||||
ground-truth benchmark — standing one up (per doc 03's acceptance table
|
||||
and ADR-149's statistical protocol) is the highest-leverage next step.
|
||||
and ADR-171's statistical protocol) is the highest-leverage next step.
|
||||
2. **The path is evolution, not rewrite**: all eight architecture pillars in
|
||||
doc 02 land inside existing crates on the ADR-136 `Stage<I,O>`/`FrameMeta`
|
||||
contract spine.
|
||||
|
|
|
|||
|
|
@ -1113,7 +1113,7 @@ The Observatory is an immersive Three.js visualization that renders WiFi sensing
|
|||
|
||||
A pretrained CSI encoder + presence-detection head is published on Hugging Face at [`ruvnet/wifi-densepose-pretrained`](https://huggingface.co/ruvnet/wifi-densepose-pretrained). It was trained on 60,630 frames / 610,615 contrastive triplets (12.2M steps, final loss 0.065) and reports **82.3% held-out temporal-triplet accuracy** (the older "100% presence" figure was measured on a single-class recording and has been retracted) and ~164k embeddings/sec on an Apple M4 Pro.
|
||||
|
||||
> **Results & proof.** The SOTA 17-keypoint pose model is published separately at [`ruvnet/wifi-densepose-mmfi-pose`](https://huggingface.co/ruvnet/wifi-densepose-mmfi-pose) — **82.69% torso-PCK@20** on MM-Fi (83.59% ensemble + TTA), beating MultiFormer (72.25%) and CSI2Pose (68.41%). Browse the auditable [AetherArena leaderboard Space](https://huggingface.co/spaces/ruvnet/aether-arena), the full [MM-Fi study](benchmarks/mmfi-wifi-sensing-study.md), and the [efficiency frontier](benchmarks/wifi-pose-efficiency-frontier.md). Reproduce the deterministic pipeline proof with `python archive/v1/data/proof/verify.py` (must print `VERDICT: PASS`; see [ADR-147 benchmark proof](adr/ADR-147-benchmark-proof.md) and [WITNESS-LOG-028](WITNESS-LOG-028.md)).
|
||||
> **Results & proof.** The SOTA 17-keypoint pose model is published separately at [`ruvnet/wifi-densepose-mmfi-pose`](https://huggingface.co/ruvnet/wifi-densepose-mmfi-pose) — **82.69% torso-PCK@20** on MM-Fi (83.59% ensemble + TTA), beating MultiFormer (72.25%) and CSI2Pose (68.41%). Browse the auditable [AetherArena leaderboard Space](https://huggingface.co/spaces/ruvnet/aether-arena), the full [MM-Fi study](benchmarks/mmfi-wifi-sensing-study.md), and the [efficiency frontier](benchmarks/wifi-pose-efficiency-frontier.md). Reproduce the deterministic pipeline proof with `python archive/v1/data/proof/verify.py` (must print `VERDICT: PASS`; see [ADR-168 benchmark proof](adr/ADR-168-benchmark-proof.md) and [WITNESS-LOG-028](WITNESS-LOG-028.md)).
|
||||
|
||||
What it ships (and what it does not):
|
||||
|
||||
|
|
@ -1289,7 +1289,7 @@ Once trained, the adaptive model runs automatically:
|
|||
RuView integrates [OccWorld](https://github.com/wzzheng/OccWorld) (ECCV 2024) to predict
|
||||
future 3D occupancy from WiFi CSI — extending the Kalman tracker's 5-frame horizon to
|
||||
15 predicted frames (~7 s). See [ADR-147](adr/ADR-147-nvidia-cosmos-world-foundation-model-integration.md)
|
||||
and the [benchmark proof](adr/ADR-147-benchmark-proof.md) for full details.
|
||||
and the [benchmark proof](adr/ADR-168-benchmark-proof.md) for full details.
|
||||
|
||||
**Hardware requirement:** NVIDIA GPU with ≥4 GB VRAM (validated: RTX 5080 at 209 ms / 3.4 GB).
|
||||
|
||||
|
|
@ -1869,7 +1869,7 @@ Pre-trained models are available on HuggingFace:
|
|||
- **SOTA MM-Fi pose model** (82.69% torso-PCK@20) — https://huggingface.co/ruvnet/wifi-densepose-mmfi-pose
|
||||
- **AetherArena leaderboard Space** — https://huggingface.co/spaces/ruvnet/aether-arena
|
||||
|
||||
Download and start sensing immediately — no datasets, no GPU, no training needed. Results are reproducible via `python archive/v1/data/proof/verify.py` (deterministic SHA-256 proof) — see [ADR-147](adr/ADR-147-benchmark-proof.md).
|
||||
Download and start sensing immediately — no datasets, no GPU, no training needed. Results are reproducible via `python archive/v1/data/proof/verify.py` (deterministic SHA-256 proof) — see [ADR-168](adr/ADR-168-benchmark-proof.md).
|
||||
|
||||
### Quick Start with Pre-Trained Models
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue