Skip to content

Commit baffa9c

Browse files
deploy: f783df2
1 parent f854f02 commit baffa9c

57 files changed

Lines changed: 7406 additions & 53 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api-docs/adr/ADR-021-vital-sign-detection-rvdna-pipeline.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,23 @@ The `wifi-densepose-vitals` crate (ESP32 CSI-grade vital signs) has not yet been
10811081
- SONA-based environment adaptation
10821082
- VitalSignStore with tiered temporal compression
10831083

1084+
## Implementation Notes
1085+
1086+
### 2026-06 — ESP32 edge vitals: person-count over-count + presence flicker (#998, #996)
1087+
1088+
Two robustness bugs were fixed in the on-device edge path (`firmware/esp32-csi-node/main/edge_processing.c`, the ADR-039 packet `0xC5110002`). These touch the *boolean/count emission logic*, not the underlying CSI signal-processing math, and do **not** constitute a validated-accuracy claim — true occupancy-count and presence accuracy vs labelled ground truth remain hardware/data-gated (COM9 ESP32-S3 + labelled capture).
1089+
1090+
- **#998 `n_persons` over-count (reported 4 for one person).** `update_multi_person_vitals()` divided the top-K subcarriers into `top_k_count/2` groups and marked *every* group `active`, so one body's multipath always read the full `EDGE_MAX_PERSONS`. Added an energy gate (`EDGE_PERSON_MIN_ENERGY_RATIO`), spatial dedup (`EDGE_PERSON_MIN_SC_SEP`), and a persistence debounce (`EDGE_PERSON_PERSIST_FRAMES`) via two pure functions `count_distinct_persons()` / `person_count_debounce()`.
1091+
- **#996 presence flag flicker at ~50 cm.** Single-threshold compare on a noisy `presence_score` chattered at the boundary. Replaced with a Schmitt trigger + clear-debounce (`presence_flag_update()`, constants `EDGE_PRESENCE_HYST_RATIO` / `EDGE_PRESENCE_CLEAR_FRAMES`); `presence_score` is unchanged and still emitted for consumer-side thresholding.
1092+
1093+
Both are pinned by host-buildable C99 tests in `firmware/esp32-csi-node/test/test_vitals_count_presence.c` (`make run_vitals`). The exact thresholds are documented constants pending on-device calibration against ground truth.
1094+
1095+
### 2026-06 — Rust `wifi-densepose-vitals`: IIR filter NaN/inf self-heal (ADR-158 §A1)
1096+
1097+
A correctness/safety review of the Rust extraction crate found a real bug parallel to the firmware robustness class above. The 2nd-order resonator `bandpass_filter` in both `breathing.rs` and `heartrate.rs` latches each output `y[n]` into its filter state (`y1`/`y2`). A single non-finite amplitude residual from a corrupt CSI frame produced a NaN `output` that was written into the state; the existing `extract()` `is_finite()` guard dropped that one sample from the history buffer **but never sanitized the poisoned filter state**, so every later output stayed NaN, was rejected too, and the sliding-window history never refilled — breathing **and** heart-rate extraction went silently dead (returning `None` forever) until `reset()`. On the alert path this is a safety-relevant denial of service (one bad frame stops vitals monitoring with no error surfaced).
1098+
1099+
Fix: when `bandpass_filter` computes a non-finite `output`, it resets the IIR state to default and returns `0.0`, so the resonator self-heals on the next clean frame (the `0.0` is still dropped by the caller's finite-check, so no spurious sample enters history). Same shape as the calibration NaN bug (ADR-154 §3) — the prior hardening guarded the *history boundary* but not the *filter-state boundary*. Pinned by `breathing::tests::nan_frame_does_not_permanently_poison_filter`, `breathing::tests::inf_mid_stream_does_not_freeze_history`, and `heartrate::tests::nan_frame_does_not_permanently_poison_filter` (all FAIL pre-fix, verified by reverting). The review also de-magicked the HR physiological plausibility band into named `HR_PLAUSIBLE_MIN_BPM`/`HR_PLAUSIBLE_MAX_BPM` consts (value-identical 40/180 BPM) and added a fabricated-vital negative (`pure_noise_is_never_reported_valid` — broadband noise never yields a clinically `Valid` HR; the extractor honestly returns low-confidence `Unreliable`). Clean dimensions confirmed with evidence: flat/silent input → `None`; pure noise → low-confidence `Unreliable`, never `Valid`; harmonic-rich breathing with no cardiac component → low-confidence, not a confident false HR; out-of-band BPM rejected by the plausibility clamp.
1100+
10841101
## References
10851102

10861103
- Ramsauer et al. (2020). "Hopfield Networks is All You Need." ICLR 2021. (ModernHopfield formulation)

api-docs/adr/ADR-046-android-tv-box-armbian-deployment.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ This ADR covers Phase 1 (TV box as aggregator) and Phase 2 (custom WiFi firmware
8383
|---------|--------|-------------|--------------|--------|
8484
| Broadcom BCM43455 | brcmfmac | **Proven** (Nexmon CSI) | Yes | Low — patches exist |
8585
| Realtek RTL8822CS | rtw88 | **Moderate** — driver is open-source, CSI hooks need adding | Yes (patched) | Medium |
86-
| MediaTek MT7661 | mt76 | **Unknown**MediaTek has released CSI tools for some chips | Yes | Medium-High |
86+
| MediaTek MT7661 | mt76 | **Unverified**no supported public CSI capture API was found in upstream `mt76` or public MediaTek SDK material | Yes | Research only |
8787

8888
2. **CSI extraction architecture** (Linux kernel driver modification):
8989

api-docs/adr/ADR-084-rabitq-similarity-sensor.md

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,14 +259,75 @@ Validation runs against:
259259
- **ADR-083** (Proposed) — Per-cluster Pi compute hop. Defines the
260260
device class that hosts the sketch bank.
261261

262+
## Pass 2 — randomized rotation + multi-bit (ADR-156 §8, landed 2026-06)
263+
264+
The "Open question" below ("does `BinaryQuantized` need a randomized
265+
rotation pre-pass?") is now **answered with measured numbers** via
266+
ADR-156 §10. Summary:
267+
268+
- **Pass 2 (randomized rotation) is implemented**
269+
`crates/wifi-densepose-ruvector/src/rotation.rs`: a deterministic
270+
`R = H·D` (Fast Hadamard Transform + seeded ±1 sign flips), `O(d log d)`
271+
/ `O(d)`, norm-preserving, reproducible from a stored `u64` seed. Opt-in
272+
via `Sketch::from_embedding_rotated` / `SketchBank::with_rotation`;
273+
Pass-1 API and wire format unchanged.
274+
- **Measured top-K coverage** (anisotropic planted-cluster fixture,
275+
cosine ground truth, dim=128 N=2048 K=8): rotation lifts coverage
276+
**36.13% → 46.39%** at the strict `candidate_k = K` bar, and Pass-2
277+
reaches the **≥90% acceptance bar at candidate_k = 24 (~3× over-fetch)**.
278+
Multi-bit (≤4-bit) reaches 74% at the strict bar. **Honest verdict:
279+
neither rotation nor ≤4-bit multi-bit clears the strict-K 90% bar on
280+
this distribution; the bar is met via the over-fetch "candidate set"
281+
pattern this ADR specifies** (Decision §"the canonical pattern" — sketch
282+
picks the candidate set, full precision refines). Full numbers and
283+
reproduce commands in ADR-156 §10.
284+
- **Pre-existing `SketchBank::topk` bug fixed** — the `n > k` heap path
285+
returned the k *farthest* sketches (min-heap mistaken for max-heap);
286+
only the `n ≤ k` fast path had test coverage. Fixed + regression-pinned
287+
(`topk_heap_path_returns_nearest`,
288+
`tight_clusters_give_high_coverage_with_overfetch`). This makes every
289+
prior top-K acceptance number in this ADR depend on the fixed path; the
290+
≥90% coverage criterion is only meaningful post-fix.
291+
292+
## Pass 2b — RaBitQ unbiased distance estimator (ADR-156 §11, landed 2026-06)
293+
294+
The **real** RaBitQ contribution (Gao & Long, SIGMOD 2024) — an
295+
**unbiased estimator of the inner product / distance** from the 1-bit
296+
code + per-vector side info, not just sign bits — is now implemented and
297+
**MEASURED against this ADR's ≥90% strict-K bar**:
298+
299+
- **Implemented**`crates/wifi-densepose-ruvector/src/estimator.rs`:
300+
`EstimatorSketch` (Pass-2 sign code + 8 B/vec side info:
301+
`residual_norm` + `x_dot_o = ⟨x̄, o'⟩`), `DistanceEstimator`
302+
(`⟨o',q'⟩ ≈ ⟨x̄,q'⟩ / x_dot_o`, the paper's unbiased rescale), and
303+
`EstimatorBank` reranking candidates by the estimate instead of raw
304+
Hamming. **Zero-centroid simplification** (`c = 0`) documented;
305+
paper-faithful centroid path also built (`with_centroid`). Additive —
306+
Pass-1/Pass-2 and the wire format are unchanged.
307+
- **MEASURED strict-K coverage** (same fixture as §"Pass 2", cosine
308+
ground truth): the estimator lifts the strict `candidate_k = K` bar
309+
**46.39% (Pass-2 sign) → 49.71% (estimator, cosine rerank)** — a real
310+
**+3.3 pp** lift, but **still ~40 pp short of the ≥90% strict bar.**
311+
At over-fetch the estimator does better than sign (95.12% vs 91.60% at
312+
candidate_k = 24). **Honest verdict: the unbiased estimator does NOT
313+
clear the strict-K 90% bar on this distribution** — the binding
314+
constraint is the 1-bit code's information ceiling, not estimator
315+
variance. The ≥90% acceptance bar is still met only via the over-fetch
316+
"candidate set" pattern this ADR's Decision specifies; the estimator
317+
**reduces the over-fetch factor** needed but does not remove it. This
318+
is a **published negative**, reported as such. Full numbers + reproduce
319+
commands in ADR-156 §11.
320+
262321
## Open questions
263322

264323
- **Does `BinaryQuantized` need a randomized rotation pre-pass for
265-
RuView's embedding distributions?** Pure sign quantization assumes
266-
zero-centered, isotropic embeddings. If AETHER / spectrogram
267-
distributions are skewed (likely for spectrogram), add a
268-
`randomized_rotation` pre-pass following the original RaBitQ paper
269-
(Gao & Long, SIGMOD 2024). Decided after pass-1 benchmark.
324+
RuView's embedding distributions?** **ANSWERED (ADR-156 §10):** rotation
325+
is built and measured — it helps (+10pp at strict K) but is not
326+
sufficient alone for strict-K 90% on the tested anisotropic
327+
distribution; the over-fetch candidate-set pattern meets the bar.
328+
Pure sign quantization assumes zero-centered, isotropic embeddings; the
329+
rotation decorrelates anisotropic coords as the RaBitQ paper
330+
(Gao & Long, SIGMOD 2024) prescribes.
270331
- **Sketch dimension target.** Default to the embedding's native
271332
dimension (128 for AETHER, 256 for spectrogram). Higher-dimensional
272333
sketches (Johnson-Lindenstrauss-projected to 512) trade compute for

api-docs/adr/ADR-116-cog-ha-matter-seed.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,57 @@ Ranked by build cost × user impact:
104104
| **P9** | HACS integration repo (`hass-wifi-densepose`) for HA-side install path | pending |
105105
| **P10** | Witness bundle + CSA-style spec compliance check | pending |
106106

107+
## 4.1 Crypto/security review notes (§2.2 witness chain — ADR-262 P2 prerequisite)
108+
109+
Beyond-SOTA crypto+security review of the SHA-256 + Ed25519 witness chain
110+
(`witness.rs` / `witness_signing.rs`) and the manifest signature surface
111+
(`manifest.rs`), because ADR-262 P2 proposes to **reuse this exact signing
112+
chain**. Top priority was the sibling `wifi-densepose-engine` bug class —
113+
unframed boundary-to-boundary concatenation of operator-influenceable strings
114+
into a signed/hashed digest.
115+
116+
- **Engine bug class ABSENT (good result, reported with byte evidence).**
117+
`canonical_bytes` is `DOMAIN_TAG ‖ prev_hash[32] ‖ seq:u64-be ‖ ts:u64-be
118+
kind_len:u32-be ‖ kind ‖ payload_len:u32-be ‖ payload`. The two
119+
variable-length operator-influenceable fields (`kind`, `payload`) are
120+
**length-prefixed**; the fixed-width fields are self-delimiting → the
121+
encoding is injective (no two distinct event tuples share a preimage). The
122+
Ed25519 signature signs the **identical** bytes the SHA-256 chain commits to.
123+
No separate unframed concatenation exists; the manifest `binary_signature`
124+
is signed at build time (Makefile) over a single fixed-length `binary_sha256`
125+
hex value, not in-crate.
126+
127+
- **CHM-WIT-01 (FIXED) — domain-separation tag added.** The engine fix
128+
prescribed *domain-tag + length-prefix*; length-prefix was present, the
129+
domain tag was not. Added a versioned, NUL-terminated
130+
`WITNESS_DOMAIN_TAG = b"cog-ha-matter/witness-event/v1\x00"` prefix so the
131+
witness message can never be replayed as a message for another Ed25519
132+
context that shares key infrastructure (notably the manifest signature).
133+
**Witness bytes change by design** (prior on-disk hashes/signatures
134+
invalidated, as with the engine fix); verified safe because no in-repo crate
135+
consumes cog-ha-matter witness bytes programmatically (doc-mentions only).
136+
137+
- **CHM-WIT-02 (HARDENED) — `verify_signature` now uses `verify_strict`.** For
138+
an audit chain the signature is the attestation, so non-canonical encodings
139+
and small-order keys are rejected (RFC 8032 strict), giving the "one
140+
canonical signature per event" property. Not a forgery fix — the verifying
141+
key is caller-pinned, never read from the event.
142+
143+
- **Confirmed clean (with evidence):** verify-before-trust + key-pinning
144+
(`verify_signature` takes the verifying key as a parameter; `read_jsonl`
145+
re-derives every hash and chain-verifies); key handling (the crate never
146+
generates/stores/logs/serializes a signing key — only a documented test-only
147+
fixed seed; production keys come from the Seed secure store, out of scope);
148+
determinism (positional bytes, deterministic Ed25519, alphabetically-locked
149+
JSONL field order, sorted TXT records — no HashMap/float nondeterminism feeds
150+
any digest); fail-closed parsing (structured errors, no panics; `main.rs`
151+
reads no untrusted files/paths).
152+
153+
Tests: `cog-ha-matter --no-default-features` 64 → **68**, 0 failed (CHM-WIT-01
154+
pinned by 4 fails-on-old tests across `witness.rs`/`witness_signing.rs`;
155+
CHM-WIT-02 guarded by a key-pinning test). Python deterministic proof
156+
unchanged (cog-ha-matter is off the signal proof path).
157+
107158
## 5. References
108159

109160
- ADR-101 — `cog-pose-estimation` packaging precedent (signed binaries on GCS, .cog manifest)

api-docs/adr/ADR-127-homecore-state-machine-rust.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,78 @@ The entity registry is a `RwLock<HashMap<EntityId, EntityEntry>>` backed by an a
190190

191191
- `v2/crates/wifi-densepose-sensing-server/src/main.rs` — Axum + Tokio architecture pattern used throughout the existing server stack
192192
- `docs/adr/ADR-126-ruview-native-ha-port-master.md` — HOMECORE master; §5.5 crate naming; §6 compatibility contract; §5.1 RUVIEW-POLICY
193+
194+
---
195+
196+
## 9. Security & concurrency review (P1 core, beyond-SOTA sweep)
197+
198+
Foundational review of the `homecore` crate — the state store + event bus +
199+
service/entity registries every other HOMECORE module trusts. Same rigor as
200+
the ADR-129/130/132/133/161 sibling reviews. **Three real fixes (one
201+
concurrency, two hardening), each pinned by a fails-on-old test; the bus-lag
202+
and lock-discipline dimensions confirmed clean with evidence.**
203+
204+
- **HC-RACE-01 (state-set TOCTOU — lost / reordered `state_changed`, the
205+
crux). FIXED.** `StateMachine::set` did `get()` (releasing the DashMap
206+
shard lock) → compute the next snapshot + the no-op / `last_changed`
207+
decision → `insert()` (re-acquiring the lock) → `send()`. The
208+
read-modify-write was **not atomic** w.r.t. a concurrent writer on the
209+
same entity, contradicting §2.1's promise that "the writer atomically
210+
replaces the map entry." A writer that read a stale `old` could
211+
mis-classify a genuine transition as a no-op and **drop its
212+
`state_changed` event** (a missed automation trigger) or fire an event
213+
whose `new_state` duplicated the previously delivered one (a spurious
214+
trigger for any automation keyed on `old_state != new_state`). **Fix:**
215+
hold the shard write-lock across the entire read→decide→insert→fire
216+
sequence via `entry()`/`insert_entry()`; `tx.send` is non-blocking,
217+
non-async, and never re-enters the map, so firing under the shard lock
218+
cannot deadlock and keeps global event order in lock-step with global
219+
commit order. Pinned by `concurrent_set_fires_no_duplicate_adjacent_events`
220+
(4 writers toggling one entity A/B; asserts no two consecutive fired
221+
events carry an identical `new_state` — impossible under correct
222+
serialisation; a probe observed ~93k such duplicate-adjacent events across
223+
200 trials on the racy code, zero on the fix).
224+
- **HC-EID-LEN-01 (unbounded `entity_id` — memory-DoS at the REST boundary).
225+
FIXED.** `homecore-api/src/rest.rs` parses untrusted path segments
226+
straight through `EntityId::parse`; with no length cap, an
227+
otherwise-valid id (`a.` + many MB of `[a-z0-9_]`) was accepted and a
228+
`POST /api/states/<giant>` would persist it into the DashMap state store
229+
(permanent growth across distinct ids). **Fix:** reject ids longer than
230+
`MAX_ENTITY_ID_LEN` (255, HA-compatible) up front in `parse()`, before any
231+
per-char scan, with a new `EntityIdError::TooLong`; fail-closed at the
232+
boundary type protects every caller. Pinned by `entity_id_length_boundary`
233+
(exactly-MAX accepted, MAX+1 and a 4 MiB id rejected — fails on old code).
234+
- **HC-SVC-PANIC-01 (service-handler panic not isolated). HARDENED.**
235+
`ServiceRegistry::call` already ran handlers outside the registry lock (no
236+
`RwLock` poisoning, no blocking of other callers — clean), but a
237+
panicking handler unwound through `call()` into the caller's task. **Fix:**
238+
wrap the handler future in `AssertUnwindSafe` + `catch_unwind`, converting
239+
a panic to `ServiceError::HandlerPanicked`; the registry stays fully
240+
usable. Pinned by `panicking_handler_is_isolated_and_registry_survives`.
241+
242+
**Dimensions confirmed clean (with evidence):**
243+
244+
- **Event-bus bounds / lag (same class as the homecore-api WS lag-DoS).**
245+
Both `StateMachine` and `EventBus` use bounded `tokio::sync::broadcast`
246+
(capacity 4,096). A slow subscriber gets a recoverable `Lagged(n)`
247+
(drop-oldest + re-sync); `fire_*` is non-blocking and **never waits on
248+
slow receivers**, so a lagging subscriber cannot block the publisher, grow
249+
the channel without bound, or take down a fast subscriber. Evidenced by
250+
`slow_subscriber_does_not_block_publisher_or_kill_the_bus` (fire 3×
251+
capacity at an idle subscriber; publisher unblocked, bus stays live).
252+
- **Lock ordering / lock-across-await (deadlock).** No code path holds two
253+
of `{state DashMap, registry RwLock, service RwLock}` simultaneously, so
254+
no inconsistent-ordering deadlock can exist. Every `tokio::sync::RwLock`
255+
guard in `registry.rs`/`service.rs` is used in a single synchronous
256+
statement and dropped before any `.await`; `call` explicitly scopes the
257+
read guard out before awaiting the handler. The only guard held across a
258+
send is the DashMap shard lock in `set`, across a synchronous
259+
(non-await) broadcast send — safe.
260+
- **Panic-on-input.** No reachable `unwrap`/`expect`/index in non-test code
261+
beyond the safe `send().unwrap_or(0)` and the dead-but-harmless
262+
`split_once(...).unwrap_or(...)` fallbacks on already-validated ids.
263+
264+
`cargo test -p homecore --no-default-features`: **20 → 24 passed, 0 failed**
265+
(+4 pins). Workspace green; Python deterministic proof unchanged
266+
(`f8e76f21…46f7a`, bit-exact — `homecore` is off the signal proof path).
193267
- `docs/adr/ADR-028-esp32-capability-audit.md` — witness chain pattern (Ed25519 per state transition)

0 commit comments

Comments
 (0)