Compare commits

..

21 Commits

Author SHA1 Message Date
rUv 29e937ef52 Merge pull request #1044 from ruvnet/feat/edge-skills-synthetic-validation
feat(wasm-edge): unified EdgePipeline (all ~64 skills) + honest synthetic validation harness
2026-06-13 00:46:29 -04:00
ruv 41665d3de9 test(wasm-edge): synthetic-ground-truth validation harness for edge skills (ADR-160)
Plant signals with known answers, run the real detector, MEASURE detection
accuracy / precision / recall / rate-error — synthetic-ground-truth ONLY, not
field accuracy.

MEASURED-on-synthetic (12 tests, all green):
- vital_trend, exo_ghost_hunter(hidden breathing), occupancy, intrusion,
  exo_rain_detect, sig_optimal_transport: acc 1.000
- exo_time_crystal: 1.000 on periodic-vs-aperiodic (its sub-harmonic-vs-clean-
  period claim is NOT separable by autocorrelation — recorded honestly)
- sig_flash_attention: 8/8 peak localization; spt_spiking_tracker: 4/4 zone
  localization (sparse plant); sig_mincut_person_match: 0 id-swaps/40 frames
- lrn_dtw_gesture_learn: enrollment validated (replay-match reported, not asserted)
- sig_sparse_recovery: trigger validated; recovery accuracy reported NEGATIVE
  (-2.2% vs unrecovered baseline) — only its detect/trigger path is validated

DATA-GATED (listed, NOT faked): med_seizure/apnea/cardiac/respiratory/gait,
sec_weapon_detect, exo_emotion/happiness/dream_stage/gesture_language — each
needs real labelled clinical/affect/ASL/metal-object data; no number claimed.

benchmarks/edge-skills/RESULTS.md documents every result + reproduce command and
the explicit honesty boundary. ADR-160 deferred 'per-skill accuracy validation'
item updated to PARTIALLY MEASURED-on-synthetic + DATA-GATED.

Suite: 631 passed default / 669 medical, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 00:33:51 -04:00
ruv c6eacb7ff8 feat(wasm-edge): unified EdgePipeline wiring all ~64 edge skills (ADR-160)
Register every runtime skill module behind one uniform EdgeSkill trait and
run them all per CSI frame, aggregating (skill, event_id, value) triples.

- src/pipeline_all.rs: CsiFrameView (borrowed per-frame inputs), EdgeSkill
  trait, EdgePipeline (Box<dyn> dispatch over all skills), SkillEvent/SkillInfo
  introspection. Host-only (std); the wasm no_std build keeps the flagship
  lib.rs pipeline.
- src/skill_registry.rs: per-skill adapters (fwd_skill! direct-forward +
  synth_skill! for non-tuple returns). No skill DSP changed — only call wiring.
  gesture/coherence/adversarial synthesize one event; sig_sparse_recovery gets
  an owned mutable amplitude scratch; timer skills driven once per frame.
- med_* tier registered only under --features medical-experimental (preserves
  the ADR-160 safety gate). Default tier = 59 skills; +medical = 64.
- tests/pipeline_all.rs: 4 tests — all skills run without panic over 300
  deterministic synthetic frames, every emitted id is declared by its skill,
  introspection well-formed, default tier excludes medical (59) / medical adds 5 (64).
- examples/run_all_skills.rs: runnable demo printing per-skill event totals.

Full suite: 619 passed default (615 M6 baseline + 4 new), 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 00:20:29 -04:00
rUv 153bc0595b Merge pull request #1043 from ruvnet/docs/adr-gap-remediation-1
docs(adr): Gap Register remediation — write phantom ADR-132/165, fix ADR-134 collision, correct statuses
2026-06-12 23:11:10 -04:00
ruv 8fd4ee917d docs(adr): mark ADR-164 Gap Register items resolved (G3, G5) + correct G2
Records the remediation done in this branch:
- G3 (homecore-recorder/migrate phantom ADRs) → RESOLVED: ADR-132 + ADR-165 written.
- G5 (10 streaming-engine Proposed-while-built) → RESOLVED: 136-145 flipped to
  "Accepted — partial", with the honest caveat that the notes describe building
  blocks built+tested, not live-path integration.
- G2 (missing Status headers) → corrected: ADR-134-CIR was mislabeled as missing
  (it has a Status row); the 2 genuine misses (147-benchmark-proof, 052-ddd) are
  both inside owner-gated duplicate-number collisions, so left untouched. Early
  ADRs using "| Status |" vs "| **Status** |" are different-format-but-present.
  Net: 0 status headers added.
- Updated Coverage-Gaps bullets for recorder/migrate.

Renumbering/dedup of the 6 collisions left owner-gated, as instructed.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 23:01:10 -04:00
ruv 5c5112db0e docs(adr): correct streaming-engine statuses 136-145 Proposed→Accepted — ADR-164 G5
All 10 streaming-engine ADRs (136-145) carried Status: Proposed while each has a
concrete commit-pinned "Built -- tested building block" Implementation-Status note
(136: 11f89727f; 137: 4fa3847ac; 138: fc7674bde; 139: 521a012d8; 140: 169a355bd;
141: 7d88eb84c; 142: 1f8e180d6; 143: 2d4f3dea5; 144: b10bc2e9a; 145: 0f336b7d3),
each with a test count.

Flipped each to "Accepted — partial (built + tested building block; integration
glue pending — see Implementation Status, commit <hash>)". Honest "partial", not
full Accepted: the notes themselves state the blocks are tested+compiling but
"mostly not yet on the live 20 Hz path". 143 (v2 dataset-gated) and 144 (no UWB
radio in fleet) carry their specific residual gates inline.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 23:00:54 -04:00
ruv e3696da8d8 docs(adr): write ADR-165 (HOMECORE-MIGRATE), repoint migrate 134→165 — ADR-164 G3
homecore-migrate cited "ADR-134 (HOMECORE-MIGRATE)", but on-disk ADR-134 is
"First-Class CIR Support" — a different decision. The migrate crate was governed
by a phantom identity (ADR-164 Gap G3).

- New ADR-165-homecore-migrate-from-home-assistant.md (next free number),
  reverse-documented from the shipped P1 scaffold: HA .storage reader, versioned
  format gate (unknown minor_version = hard error), per-artifact parsers, inspect
  CLI, structured errors. Status: Accepted — P1 scaffold (full conversion P2).
  Trust-boundary rationale for the untrusted .storage import is the centerpiece.
- Repointed every ADR-134 governing reference in v2/crates/homecore-migrate/
  (Cargo.toml, README.md, src/lib.rs, src/config_entries.rs,
  src/storage_format/mod.rs) → ADR-165. Left the ADR-132 (recorder-feature)
  refs intact. Explanatory renumber notes retained.
- On-disk ADR-134 (CIR) untouched. ADR-126 series-map registry row owner-gated.

Docs/comments only — cargo build -p homecore-migrate --no-default-features
still compiles.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 23:00:33 -04:00
ruv 9457d441b2 docs(adr): write missing ADR-132 (HOMECORE-RECORDER) — resolves ADR-164 G3
homecore-recorder cites "ADR-132" in Cargo.toml/README/lib.rs/schema.rs/
semantic.rs, but no ADR-132 file existed — the durable-state backbone was
ungoverned (ADR-164 Gap G3 / Coverage-Gaps Lens A).

Reverse-documented from the shipped, tested crate (not invented): SQLite
HA-compatible recorder schema v48 (P1, 14 tests), ruvector HNSW semantic
index (P2, feature-gated, 20 tests), hash-embedding honesty note, P3 real
embeddings planned. Status: Accepted (shipped). Filename matches the link
the crate README already pointed at. Documented retroactively; honest about
hash-embedding limits and unbenchmarked latency targets.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 23:00:15 -04:00
rUv 626b4b2e97 Merge pull request #1042 from ruvnet/docs/adr-164-gap-analysis
docs(adr): ADR-164 — ADR corpus gap analysis & remediation backlog (162 ADRs)
2026-06-12 22:47:21 -04:00
ruv 260fceefe9 docs(adr): ADR-164 corpus gap analysis + research notes (162 ADRs)
Parallel gap analysis of all 162 ADRs (14-agent workflow): status distribution,
prioritized Gap Register, supersession integrity, contradictions/retractions
(anti-slop centerpiece), coverage gaps, and the honestly-gated backlog.

Key findings: 6 duplicate ADR numbers + 3 missing Status headers (breaks the
index); shipped crates citing phantom governing ADRs (homecore-recorder->ADR-132
nonexistent, homecore-migrate->ADR-134 mis-identified); streaming-engine ADRs
136-145 marked Proposed but actually Built; open ADR-080 sensing-server security
findings never closed; ~64 proposed-only ADRs; pre-ADR-155 accuracy claims are
CLAIMED not MEASURED. Detail in docs/adr/gap-analysis/{census,lens-findings}.md.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 22:40:32 -04:00
rUv e063de5970 Merge pull request #1039 from ruvnet/release/patch-1009-1004
release: patch-bump signal/sensing-server/cli for #1009+#1004 fixes (+ first-publish calibration)
2026-06-12 17:09:29 -04:00
ruv 53b327e649 release: bump signal 0.3.4 / sensing-server 0.3.3 / cli 0.3.1 (fixes #1009, #1004)
HE20 calibration baseline fix (signal), sensing-server --source auto simulate-latch
fix (sensing-server), HE20 calibrate parser/asserts (cli). See PR #1038.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 16:55:27 -04:00
rUv ad3908bd9e Merge pull request #1038 from ruvnet/fix/issues-1009-1004-real-csi-ingest
fix: real CSI-ingest bugs — HE20 baseline corruption (#1009) + sensing-server simulate-latch (#1004)
2026-06-12 16:47:25 -04:00
ruv a27ee6f6cd fix(csi-ingest): real HE20 CSI no longer dropped or replaced with simulated data (#1009, #1004)
Two ingest bugs caused real ESP32-C6 HE20 CSI to be silently discarded or
never received — the "real data silently lost" failure class. Each fix is
pinned by a test that fails on the old code.

#1009 §1b — HE20 baseline recorder trimmed 256->242 bins by sequential index.
ESP-IDF v5.5.2 delivers all 256 FFT bins for an HE20 frame, but
CalibrationConfig::he20() carried num_active: 242, so the recorder (no HE20
tone map — extract_first_stream takes the first num_active columns
sequentially) kept bins 0..242 = the lower guard band + DC, NOT the 242 active
tones, silently corrupting the empty-room baseline. Now num_active: 256 records
every delivered bin, aligned 1:1 with the live deviation() path. The exact-242
tone map stays only in cir.rs (HE20_ACTIVE), where the Phi sensing matrix needs
it. HE20 synthetic/bench fixtures updated to feed 256-bin frames.

#1009 §1a/§1c — u8->u16 n_subcarriers truncation, regression-pinned.
The ADR-018 wire format carries n_subcarriers as u16 LE at bytes 6-7; a 256-bin
HE20 frame (byte6=0x00) read as one byte decodes to 0 subcarriers -> every
frame skipped. The CLI parser and the sensing-server parse_esp32_frame were
already corrected to u16 under #1005/ADR-110; added regression tests that fail
on the old single-byte read so the truncation cannot silently return.

#1004 — --source auto latched on simulate forever, never binding UDP :5005.
A one-shot boot probe resolved the source once; with no CSI flowing at boot
(the normal firmware/server startup race) it served simulated poses for the
whole process and ignored real CSI arriving seconds later (the prior #937 fix
hard-exited instead — equally wrong). New plan_source() state machine: in auto
mode ALWAYS bind the UDP receiver and serve simulated only until the first real
frame, then udp_receiver_task promotes source -> esp32 (mirroring the existing
esp32 -> esp32:offline reversion). simulated_data_task self-suspends once
promoted. Explicit --source simulated stays a hard, UDP-free offline override.

Validation: 3-crate tests 1118 passed / 0 failed; workspace 3166 passed /
0 failed; Python proof VERDICT: PASS (bit-exact, unaffected). cir.rs untouched.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 16:37:55 -04:00
rUv 3d7530f08d Merge pull request #1033 from ruvnet/feat/v2-zero-warnings-hygiene
chore: zero-warnings hygiene — clear 13 build warnings across v2/crates
2026-06-12 09:09:18 -04:00
ruv d4170ad159 fix: revert config-dependent cargo-fix changes (kept only always-safe edits)
cargo fix ran under --no-default-features and removed an import/mut that are
'unused' ONLY in the minimal build but genuinely USED in CI's full build
(error[E0596]: cannot borrow result as mutable in desktop discovery.rs). Those
are false-positive warnings in the minimal config. Reverted bridge.rs/
commissioning.rs/discovery.rs to origin/main; kept the always-safe edits
(dead-code #[allow] notes + ClockGateDecision doc fields + camera macOS-only
allow). Full-features build of all four crates: Finished, 0 errors.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 08:56:26 -04:00
ruv 0d6c20c278 chore(v2): zero-warnings hygiene — clear 13 build warnings across 4 crates
Removed unused Matter imports (sensing-server bridge/commissioning), dropped
needless mut (bridge, desktop discovery), documented ClockGateDecision variant
fields (ruvector coherence), and marked deferred-P2/platform-only helpers
#[allow(dead_code)] with honest notes (entity_on_matter/next_endpoint =
Matter-publisher API deferred per ADR-159 §A5; decode_jpeg_to_rgb = macOS-only).
Behavior-neutral; touched-crate tests green. Remaining 1 warning is a benign
Windows .pdb filename collision inherent to the Tauri lib+bin desktop crate
(renaming the bin would break Tauri bundling — won't-fix for a cosmetic warning).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 08:44:42 -04:00
rUv 3fb40a9deb Merge pull request #1030 from ruvnet/feat/v2-beyond-sota-sweep-m9
Beyond-SOTA sweep M9 (ADR-163): edge-latency measurement debt → MEASURED-on-host benches
2026-06-12 08:14:57 -04:00
ruv 1a17cc5b06 docs(ADR-163): edge-latency RESULTS + PROOF/prove.sh wiring (T3)
Adds benchmarks/edge-latency/RESULTS.md (wiflow-std RESULTS style: each
measured number with reproduce command, machine, MEASURED-on-host grade,
and the honest host-vs-ESP32 / steady-state-vs-cold-start caveats) and
ADR-163 (HEADLINE: CLAIMED latency budgets -> MEASURED-on-host, closing
M5/M6 measurement debt; ESP32-on-hardware still pending).

- ADR-160 deferred 'criterion benches for process_frame budget claims'
  line updated to DONE (host) with the ESP32-pending note.
- PROOF.md performance table gains the two edge-latency reproduce rows;
  provenance ADR range extended to ADR-163.
- prove.sh gated section gains the edge-latency bench note (host proxy
  only; not asserted, never claims the ESP32 figure).

Benches/docs only; no crate republishes.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 08:02:07 -04:00
ruv 7c13ec6a00 bench(cogs): steady-state CPU infer latency benches (ADR-163 T2)
Criterion benches over InferenceEngine::infer for cog-person-count and
cog-pose-estimation, on Device::Cpu with the real shipped safetensors
weights (asserts candle backend so the stub is never silently benched),
over a fixed CSI window after a warm-up forward.

HOST-MEASURED steady-state medians (idle box): ~305us each. This is the
recurring per-frame cost and is explicitly NOT the pose manifest's
cold_start_ms_avg=5.4 (a different measurement, weight-load included, taken
on ruvultra/RTX 5080) -- the two are labelled and not conflated.

Closes the ADR-159/160 deferred cog inference-latency item. No production-
code behavior change.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 08:01:50 -04:00
ruv d3606d51a7 bench(wasm-edge): host process_frame latency benches (ADR-163 T1)
Criterion benches over the M6-audit-named heaviest hot paths:
exo_time_crystal 256x128 autocorrelation, exo_ghost_hunter periodicity,
sec_weapon_detect per-subcarrier Welford, med_seizure_detect clonic rhythm
(medical-experimental-gated). Drives each through the public process_frame
on a fixed synthetic CSI frame after warming the relevant buffers.

Crate is workspace-excluded: run from the crate dir with --features std.
Set lib bench=false so libtest does not intercept criterion CLI flags.

HOST-MEASURED medians (Intel Core Ultra 9 285H, native --release), NOT the
ESP32/WASM3 doc budget (that needs hardware): time_crystal 17.3us,
ghost_hunter 1.44us, weapon 0.42us, seizure 0.10us.

Closes the ADR-160 deferred 'criterion benches for process_frame budget
claims' item on host. No production-code behavior change.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 08:01:29 -04:00
52 changed files with 4712 additions and 91 deletions
+4
View File
@@ -42,6 +42,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Live trust path: sensing-server routes real frames through the governed `StreamingEngine` (parallel governed path with partial output gating).** Previously the live server ran only the *bare* `MultistaticFuser` (fused amplitudes, no trust control plane), while the privacy/provenance/witness engine (ADR-135..146) ran only on synthetic in-test frames — the gap called out in ADR-136 §8 and the beyond-SOTA system review. New `engine_bridge` module drives `StreamingEngine::process_cycle` from the server's live `NodeState` map (reusing the existing `NodeState → MultiBandCsiFrame` conversion), lazily wiring each node as a WorldGraph sensor and bounding belief growth via the retention cap; every *governed belief* carries evidence + model + calibration + privacy decision and a deterministic witness. **Honest scope:** the engine runs alongside (not instead of) the bare fusion path that feeds the live `SensingUpdate`. What its decision gates on the wire today: a cycle emitted at class `Restricted` (base mode or contradiction/mesh-risk demotion) suppresses the per-node raw amplitude vectors from the live publish — the same field mapping `wifi-densepose-bfld`'s privacy gate applies at `Restricted`; gating the remaining derived outputs (person count, classification, signal field) is tracked as a follow-up. Trust state is no longer write-only: the latest witness, effective privacy class, demotion flag, recalibration recommendation, and an engine-error counter are readable on `GET /api/v1/status`, and engine errors are counted + rate-limit logged instead of silently swallowed (`EngineBridge::observe_cycle`). Adds `wifi-densepose-engine/-worldgraph/-bfld/-geo` deps. Bridge tests cover witnessed belief with provenance, determinism, idempotent node registration, retention bound, privacy-mode propagation, trust-state recording, the error-counter path, and Restricted-class raw-output suppression.
### Fixed
- **Real HE20 CSI no longer silently dropped or replaced with simulated data (fixes #1009, #1004).** Two ingest bugs caused real ESP32-C6 HE20 frames to be discarded or never received — the exact "real data silently lost" failure class the project fights. Each fix is pinned by a test that fails on the old code.
- **#1009 §1b — HE20 baseline recorder trimmed 256 → 242 bins by sequential index (`wifi-densepose-signal/src/ruvsense/calibration.rs`).** ESP-IDF v5.5.2 delivers all 256 FFT bins for an HE20 frame; `CalibrationConfig::he20()` carried `num_active: 242`, so the recorder (which has no HE20 tone map — `extract_first_stream` takes the first `num_active` columns *sequentially*) kept bins 0..242 of the 256-bin grid. Those are the lower guard band + DC, **not** the 242 active tones, silently corrupting the empty-room baseline. Now `num_active: 256` records every delivered bin, staying aligned 1:1 with the live `deviation()` path. The exact-242 tone map deliberately stays only in `cir.rs` (`HE20_ACTIVE`), where the Φ sensing matrix genuinely needs it. Test `he20_records_all_256_bins_not_trimmed_to_242` asserts the finalized baseline covers all 256 bins (was 242). HE20 synthetic/bench fixtures updated to feed 256-bin frames (the real wire format).
- **#1009 §1a/§1c — already-fixed u8→u16 `n_subcarriers` truncation, now regression-pinned.** The ADR-018 wire format carries `n_subcarriers` as u16 LE at bytes 67. A 256-bin HE20 frame (byte6=0x00, byte7=0x01) read as a single byte decodes to **0 subcarriers** → every frame skipped (invisible until HE20: ESP32-S3's ≤192 bins fit in one byte). The CLI parser (`wifi-densepose-cli/calibrate.rs`) and the sensing-server template parser (`wifi-densepose-sensing-server` `parse_esp32_frame`) were already corrected to u16 under #1005/ADR-110; added regression tests (`parse_esp32_frame_he20_256_bins_not_truncated`, CLI `test_parse_csi_packet_he_su_256_bins`) that fail on the old single-byte read so the truncation cannot silently return.
- **#1004`--source auto` latched on `simulate` forever, never binding UDP :5005 (`wifi-densepose-sensing-server/src/main.rs`).** A one-shot boot probe resolved the source once; with no CSI flowing at boot (the normal firmware/server startup race) it served simulated poses for the whole process and ignored real CSI that arrived seconds later (the prior #937 fix hard-exited instead — equally wrong, the server could never pick up late-starting CSI). New `plan_source()` state machine: in `auto` mode **always bind the UDP receiver** and serve simulated data only until the first real frame, at which point `udp_receiver_task` promotes `source``esp32` (mirroring the existing `esp32 → esp32:offline` reversion in `effective_source()`); `simulated_data_task` self-suspends once promoted so it never clobbers live CSI. Explicit `--source simulated` stays a hard, UDP-free override for offline demos. 6 unit tests pin the resolution/promotion machine (`auto_with_no_boot_source_still_binds_udp_and_simulates`, etc.); the auto-binds-UDP assertion fails on the old behavior.
- **`wifi-densepose-mat` standalone `--no-default-features` build (101 errors → 0).** `pub mod api` was unconditional while its only dependency, serde, is optional behind the `api` feature — so any build without default features failed with unresolved serde imports (masked in `--workspace` runs by feature unification). The `api` module and its `create_router`/`AppState` re-export are now `#[cfg(feature = "api")]`-gated (with docsrs annotations). All feature combos compile: bare `--no-default-features`, `--no-default-features --features api`, and full default (177 tests pass).
- **WorldGraph no longer grows unboundedly under the live loop.** `StreamingEngine::process_cycle` appended one `SemanticState` belief per cycle with no eviction — ~1.7M nodes/day at 20 Hz (identified in `docs/research/ruview-beyond-sota/04-optimization-roadmap.md`). Added `WorldGraph::prune_semantic_states(max)` — deterministic eviction of the oldest beliefs by `(valid_from_unix_ms, id)`, structural nodes (rooms/zones/sensors/anchors/tracks/events) never eligible — and wired it into the engine after each belief append (`StreamingEngine::DEFAULT_SEMANTIC_RETENTION` = 7,200 ≈ 6 min at 20 Hz; tunable via `set_semantic_retention`). The WorldGraph holds *current* beliefs; durable history is the recorder's job, so no audit data is lost. 3 new tests (bounded growth end-to-end, oldest-only eviction, deterministic tie-break).
- **ESP32 edge heart rate no longer stuck at ~45 BPM / dropping wildly — #987.** The on-device HR estimator (`edge_processing.c`, `0xC5110002`) reported ~45 BPM regardless of true heart rate (Apple-Watch ground truth 87 BPM read as ~45) and swung frame-to-frame. Two root causes: (1) a hardcoded `sample_rate = 10.0f` that became wrong after #985's self-ping raised the CSI callback rate to a variable ~1319 Hz — BPM scales as `assumed/actual × true`, so 87 read ~45 and the reading swung as CSI yield fluctuated; (2) the zero-crossing estimator locked onto a breathing harmonic (a 0.25 Hz breathing fundamental puts its 3rd harmonic at ~0.74 Hz ≈ 44 BPM inside the HR band). Fix: measure the real sample rate from inter-frame timestamps (used for BPM conversion + biquad re-tuning on >15% drift); replace the HR zero-crossing with an autocorrelation estimator that rejects breathing harmonics (driven by a robust autocorr breathing period); median-13 smooth the output. Hardware A/B (fixed vs unmodified control board, both `edge_tier=2`): control pegged 4049 BPM; fixed reaches the true 8891 BPM (vs 87 GT) and holds a stable physiological value (spread 59→0 for a steady subject). Known limitation: heavy subject motion still degrades the estimate (motion gating is a follow-up).
+5 -2
View File
@@ -55,6 +55,8 @@ trained checkpoint) so you can reproduce them yourself.
| zero-copy ORT input ~1.48× (ADR-155) | **MEASURED** | `cd v2 && cargo bench -p wifi-densepose-nn --features onnx --bench onnx_bench` |
| pointcloud splats 9→2 passes ~1.24× (ADR-160 research) | **MEASURED** | `cd v2 && cargo bench -p wifi-densepose-pointcloud --bench splats_bench` |
| native wlanapi multi-BSSID scan 9.74 Hz (vs netsh ~2 Hz) | **MEASURED (Windows)** | `cd v2 && cargo test -p wifi-densepose-wifiscan -- --ignored measure_native_scan_rate` |
| wasm-edge `process_frame` hot-path latency (host proxy, ADR-163) | **MEASURED-on-host** (NOT the ESP32/WASM3 budget — needs hardware) | `cd v2/crates/wifi-densepose-wasm-edge && cargo bench --features std` |
| cog steady-state CPU infer latency ~305 µs (ADR-163; NOT the manifest cold-start) | **MEASURED-on-host** | `cd v2 && cargo bench -p cog-person-count -p cog-pose-estimation --no-default-features --bench infer_bench` |
## What we do NOT claim (the honest negatives — the strongest anti-slop signal)
@@ -68,8 +70,9 @@ trained checkpoint) so you can reproduce them yourself.
## Provenance
Every claim above traces to a committed ADR (`docs/adr/ADR-154``ADR-160`), a
test, a criterion bench, or `benchmarks/wiflow-std/RESULTS.md`. The history
Every claim above traces to a committed ADR (`docs/adr/ADR-154``ADR-163`), a
test, a criterion bench, `benchmarks/wiflow-std/RESULTS.md`, or
`benchmarks/edge-latency/RESULTS.md`. The history
includes published **retractions** (the 92.9% PCK retraction; the WiFlow-STD
shipped-checkpoint refutation; the NV-diamond BOM reality check) — a faker hides
failures; we commit them.
+137
View File
@@ -0,0 +1,137 @@
# Edge-Latency Benchmark Results — ADR-163
Converting **CLAIMED** edge latency budgets into **MEASURED-on-host** numbers,
closing the measurement debt flagged by Milestones 5/6 (ADR-159 / ADR-160).
Benches + docs only — **no production-code behavior changed**.
## The honest caveat, up front (read before citing any number)
Two distinct gaps separate every number below from the figure it is converting:
1. **Host ≠ ESP32.** The wasm-edge skill modules document budgets *"on ESP32-S3
WASM3"* (e.g. `exo_time_crystal`: "H (<10 ms)"). These benches run **native
x86_64 on a development laptop**, not the Xtensa/WASM3 target. A native host
median is an **upper bound on the algorithm's work**, not the ESP32 number.
WASM3 interpretation on a ~240 MHz Xtensa core is typically 12 orders of
magnitude slower than native `-O` host code, so a host median far under the
budget **does NOT prove the ESP32 meets it.** *The ESP32 figure is NOT
reproduced here — it needs hardware.*
2. **Bench ≠ the doc-claimed measurement.** For the cogs, the manifest cites a
**cold-start** number (`cold_start_ms_avg`, weight-load included); these
benches measure **steady-state** per-frame `infer` (warm, weights resident).
Different measurements; we report both, labelled.
Grades (per `benchmarks/wiflow-std/RESULTS.md` / ADR-152 vocabulary):
- **MEASURED-on-host** — reproduced in this repo on the machine below, exact
command recorded. NOT the ESP32 / NOT the cold-start figure.
- **CLAIMED (ESP32)** — the doc budget; UNMEASURED on hardware here.
## Machine
| | |
|---|---|
| Host | `ruvzen` (Windows 11, this dev box) |
| CPU | Intel Core Ultra 9 285H |
| Toolchain | `cargo 1.91.1`, `--release` (opt-level per crate profile) |
| Bench harness | criterion 0.5 (`time: [low **median** high]` reported below) |
| Date | 2026-06-12 |
Run-to-run spread on this box is non-trivial (criterion's low/high bracket the
median by a few %); the medians below are single-session captures with the smoke
settings `--warm-up-time 1 --measurement-time 2` (wasm-edge) / `3` (cogs). Re-run
for your own machine — the absolute numbers are host-specific.
---
## T1 — wasm-edge `process_frame` hot paths (ADR-160 deferred item → DONE host)
The crate is **excluded from the v2 workspace**; bench from the crate dir.
```bash
cd v2/crates/wifi-densepose-wasm-edge
cargo bench --features std -- --warm-up-time 1 --measurement-time 2
# med_seizure_detect is medical-experimental-gated:
cargo bench --features std,medical-experimental -- --warm-up-time 1 --measurement-time 2 med_seizure
```
| Hot path (M6-audit-named) | Bench id | Host median | Grade | Doc budget (CLAIMED, ESP32) |
|---|---|---|---|---|
| `exo_time_crystal` 256-pt × 128-lag autocorrelation (full buffer) | `exo_time_crystal::process_frame[autocorr_256x128]` | **17.3 µs** | MEASURED-on-host | "H (<10 ms) on ESP32-S3 WASM3" — **NOT reproduced here (needs hardware)** |
| `exo_ghost_hunter` empty-room periodicity + hidden-breathing | `exo_ghost_hunter::process_frame[empty_room_periodicity]` | **1.44 µs** | MEASURED-on-host | research/exotic; no firm ESP32 figure — host proxy only |
| `sec_weapon_detect` per-subcarrier Welford (MAX_SC=32) | `sec_weapon_detect::process_frame[per_sc_welford]` | **0.42 µs** (420 ns) | MEASURED-on-host | research-grade; calibration-gated — host proxy only |
| `med_seizure_detect` clonic-phase rhythm path (steady-state frame) | `med_seizure_detect::process_frame[clonic_rhythm]` | **0.10 µs** (105 ns) | MEASURED-on-host (feature-gated) | doc budget "S (<5 ms) on ESP32"; **NOT reproduced here** |
Reading these honestly:
- `exo_time_crystal` at **17.3 µs host** is the only one whose host cost is even
in the same *thousandths* of its 10 ms ESP32 budget — it does the most work
(~32K MACs/frame). 17.3 µs native says the algorithm is cheap; it says
**nothing** about whether WASM3-on-Xtensa lands under 10 ms. A naïve
host→ESP32 extrapolation (assume 100× interpreter+clock penalty) would put it
near ~1.7 ms, comfortably under — **but that is an extrapolation, not a
measurement**, and is recorded here only to show the host number is not
obviously in tension with the budget. ESP32 figure: **UNMEASURED**.
- `med_seizure_detect`'s 105 ns is the **steady-state** per-frame cost; the
expensive clonic autocorrelation only fires when the state machine is in the
clonic phase, so this is a lower-bound on the heavy path, not the worst case.
It is still a real, committed host datapoint.
- The pre-existing `tests/budget_compliance.rs` already asserts the L/S/H
wall-clock tiers (25 passing tests); these criterion benches add the
regression-grade, reproducible median that ADR-160 deferred.
---
## T2 — cog steady-state inference latency (ADR-159/160 deferred item → DONE)
Cog crates are normal workspace members; bench from `v2/`. Real weights
(`count_v1.safetensors` / `pose_v1.safetensors`) ship in-repo under each cog's
`cog/artifacts/`, so the bench measures the **real Candle CPU forward**, not the
stub (the bench `assert!`s `backend().starts_with("candle-")`).
```bash
cd v2
cargo bench -p cog-person-count --no-default-features --bench infer_bench -- --warm-up-time 1 --measurement-time 3
cargo bench -p cog-pose-estimation --no-default-features --bench infer_bench -- --warm-up-time 1 --measurement-time 3
```
| Cog | Bench id | Host median (steady-state infer, CPU) | Grade | Manifest cold-start (CLAIMED, different measurement + machine) |
|---|---|---|---|---|
| cog-person-count | `cog_person_count::infer[cpu_real_weights_steady_state]` | **305 µs** (idle box) | MEASURED-on-host | — (person-count manifest carries comparable provenance) |
| cog-pose-estimation | `cog_pose_estimation::infer[cpu_real_weights_steady_state]` | **305 µs** (idle box) | MEASURED-on-host | `cold_start_ms_avg: 5.4` (30 invocations, **ruvultra/RTX 5080 host**, candle 0.9 cpu) — **cold-start, NOT steady-state; NOT this machine** |
> Spread caveat (observed, honest): both medians above were captured with the box
> otherwise idle. A re-run of the validate-form command *while a second cargo job
> was loading the same cores* gave 385 µs (person-count) / 973 µs (pose) —
> the criterion low/high bracket widens to ~0.341.18 ms under contention. The
> 305 µs figures are the idle-box datapoints; the absolute number is host- and
> load-dependent (the ~10× pose swing is core contention, not a code change).
Reading these honestly:
- **Steady-state ≠ cold-start.** The pose manifest's `5.4 ms` folds in one-time
weight load / mmap / first-forward allocation. This bench warms the engine
first and times only the recurring per-frame forward, on a *different
machine*. The two numbers are not comparable and we do not claim this bench
reproduces the 5.4 ms manifest figure.
- Both cogs share the same conv encoder; person-count adds a count head +
confidence head, pose adds a 256-wide MLP head. The host steady-state cost is
dominated by the three dilated Conv1d layers (56→64→128→128) shared by both —
which is why both land at ~305 µs.
- **Empirical confirmation of the steady-state/cold-start gap:** pose
steady-state (305 µs host) is ~18× *under* the manifest's 5.4 ms cold-start.
Even accounting for the different machine, this is the expected shape — the
bulk of cold-start is one-time setup, not the forward pass — and it is exactly
why conflating the two would be dishonest.
---
## Status vs the deferred items
| Deferred item | Was | Now |
|---|---|---|
| ADR-160 "Criterion benches for `process_frame` budget claims" | ACCEPTED-FUTURE | **DONE (host)**; ESP32-on-hardware still **PENDING** (needs the wasm32 target + a flashed ESP32-S3) |
| ADR-159/160 cog inference latency (`cold_start_ms_avg` uncommitted-benched) | CLAIMED | **MEASURED-on-host (steady-state)**; cold-start-on-ruvultra remains the manifest's separate claim |
Nothing here changes runtime behavior — these are benches + this results file
only. No crate needs republishing.
+132
View File
@@ -0,0 +1,132 @@
# Edge-Skill Synthetic-Ground-Truth Validation — RESULTS
**Crate:** `v2/crates/wifi-densepose-wasm-edge` (workspace-EXCLUDED — build from its own dir)
**Branch:** `feat/edge-skills-synthetic-validation`
**ADR:** [ADR-160](../../docs/adr/ADR-160-edge-skill-library-honest-labeling.md)
**Date:** 2026-06-13
**Harness:** `tests/synthetic_validation.rs`
> **HONESTY BOUNDARY — read first.** Everything below is **synthetic-ground-truth
> validation**: a signal is *planted* with a known answer, the **real** detector
> is run, and detection accuracy / precision / recall / rate-error is **measured**.
> This is **NOT field accuracy.** A skill that recovers a planted sinusoid here is
> proven to do the math it claims on a *constructed* signal; it is **NOT** proven
> to work on real CSI in a real room. Skills whose detection target cannot be
> honestly planted (clinical, weapon, affect, sleep-stage, sign-language) are
> **NOT** given a number — they are listed under **DATA-GATED** with the real
> data each would require.
## Reproduce
```bash
cd v2/crates/wifi-densepose-wasm-edge # workspace-excluded; build here
cargo test --features std --test synthetic_validation -- --nocapture
# also runs under the medical tier (med_* skills stay DATA-GATED, not validated):
cargo test --features std,medical-experimental --test synthetic_validation -- --nocapture
```
Each `MEASURED-on-synthetic | …` line printed by the harness is the source of the
table below. Numbers are deterministic (no RNG; pseudo-noise uses a fixed LCG seed).
---
## MEASURED-on-synthetic (constructible skills)
| Skill | What was planted (ground truth) | Result | Grade |
|-------|----------------------------------|--------|-------|
| **vital_trend** | BPM held N≥6 calls at each threshold band (brady/tachy-pnea <12 / >25, brady/tachy-cardia <50 / >120, apnea breathing<1.0 for ≥20) vs normal | **acc 1.000, prec 1.000, recall 1.000** (TP5 FP0 TN5 FN0) | MEASURED |
| **exo_time_crystal** | period-2 coordinated motion vs pseudo-noise + flat | **acc 1.000** (TP1 FP0 TN2 FN0) | MEASURED † |
| **exo_ghost_hunter** (hidden breathing) | phase sinusoid at lag-8 (breathing band 515) in an empty room vs flat phase | **acc 1.000**; planted score **1.000**, flat **0.000** | MEASURED |
| **occupancy** | 220-frame flat-amplitude calibration, then strong per-zone amplitude variance vs flat | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED |
| **intrusion** | calibrate→arm (330 quiet frames), then per-subcarrier Δphase>1.5 + Δamp≫3σ vs quiet | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED |
| **exo_rain_detect** | empty room, 60-frame baseline, then broadband variance (8/8 groups, ratio≫2.5) for ≥10 frames vs stable-low | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED |
| **sig_flash_attention** | sustained high phase+amplitude in each of the 8 subcarrier groups; assert reported attention peak == planted group | **peak-localization 8/8 = 1.000** | MEASURED |
| **spt_spiking_tracker** | sparse (2-subcarrier) large phase-delta in each of the 4 zones; assert tracked zone == planted zone | **zone-localization 4/4 = 1.000** | MEASURED ‡ |
| **sig_optimal_transport** | sustained large frame-to-frame amplitude-distribution change vs stationary | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED |
| **sig_mincut_person_match** | 2 persons with distinct stable per-region variance signatures over 40 frames | **person ids assigned, 0 id-swaps / 40 frames** | MEASURED |
| **lrn_dtw_gesture_learn** | stillness → 3 identical gesture rehearsals → enrollment | **template enrolled (templates=1)** | MEASURED (enroll) §|
| **sig_sparse_recovery** | 30 clean frames to init, then 8/32 (25%) nulled subcarriers | **dropout-detect + recovery-trigger = PASS** | MEASURED (trigger) ¶|
### Caveats on individual results
**exo_time_crystal — honest discriminative limit.** A *pure* periodic signal
already has autocorrelation peaks at lag L **and** 2L (natural harmonics), so this
"period-doubling" detector cannot separate a true period-2 sub-harmonic from a
plain periodic signal — an earlier plant using a clean sine produced a *false
positive* (recorded during development). The construct it **can** discriminate
with known ground truth is **periodic-coordination vs aperiodic** (noise/flat),
which is what is measured (1.000). The original "sub-harmonic vs clean period"
claim is **NOT** validatable with this algorithm.
**spt_spiking_tracker — plant must be sparse.** With weights init'd home=1.0 /
cross=0.25, firing all 8 inputs in a zone (8×0.25=2.0 > threshold 1.0) overdrives
*every* output neuron and the tracker collapses to zone 0 (measured 1/4 during
development). Firing only 2 inputs (home 2.0 fires, cross 0.5 silent) yields clean
4/4 zone localization. The validatable claim is *single-zone* localization.
§ **lrn_dtw_gesture_learn — enrollment validated; replay-match NOT.** The
deterministic, constructible part (stillness → 3 identical rehearsals → a template
is enrolled) is MEASURED. The DTW *replay match* (731) did **not** fire on the
identical replay in this run (`match_same=false`) — replay-recognition accuracy is
**reported, not asserted**, and is not claimed as validated.
**sig_sparse_recovery — trigger validated; recovery accuracy is NEGATIVE.**
The dropout-detection + ISTA-recovery *trigger* pipeline fires correctly on >10%
planted nulls (asserted). But the **measured recovery accuracy is NOT a win**:
recovered RMSE **1.0045** vs unrecovered-null RMSE **0.9830** (**2.2%**, i.e.
slightly *worse* than leaving the nulls at zero) on a neighbor-correlated signal.
The tridiagonal correlation model's fixed point does not equal the planted truth.
**The recovery's reconstruction quality is therefore NOT validated as effective on
synthetic data** — only its detection/trigger path is. Reported honestly; no
positive number claimed.
---
## DATA-GATED — NOT validatable on synthetic data
Planting a "seizure-like" / "weapon-like" / "happy-like" synthetic signal and
claiming the detector "works" validates **nothing real** and is exactly the
AI-slop this project fights. These skills run real DSP (per ADR-160, 0 stubs) and
keep their ADR-160 disclaimers, but get **no accuracy number** here. Each needs
the specific real, labelled data listed:
| Skill | Why not constructible on synthetic | Real data required |
|-------|------------------------------------|--------------------|
| `med_seizure_detect` | "seizure-like" motion is not a seizure; no ground-truth signature exists synthetically | Clinical EEG-/video-labelled tonic-clonic seizure CSI from instrumented patients |
| `med_sleep_apnea` | a planted breathing-pause is not clinical apnea (AHI scoring, hypopnea, desaturation) | Polysomnography-labelled (PSG) overnight CSI with scored apnea/hypopnea events |
| `med_cardiac_arrhythmia` | a synthetic HR sequence cannot encode true arrhythmia morphology | ECG-labelled CSI (AFib/PVC/etc.) from clinical monitoring |
| `med_respiratory_distress` | distress is a clinical gestalt, not a plantable rate | Clinician-labelled respiratory-distress CSI episodes |
| `med_gait_analysis` | clinical gait metrics need a reference motion-capture standard | Mocap-/force-plate-labelled gait CSI |
| `sec_weapon_detect` | a high variance ratio is RF reflectivity, **not** weapon discrimination (ADR-160 §A3 already renamed the event to `HIGH_METAL_REFLECTIVITY`) | Labelled metal-object-vs-no-object CSI with controlled object classes |
| `exo_emotion_detect` | affect is not recoverable from a planted heuristic; outputs are proxies (ADR-160 §A2) | Validated affect-labelled CSI (self-report / physiological ground truth) |
| `exo_happiness_score` | "happiness" is a gait-energy proxy, not a measured affect (ADR-160 §A2) | Validated affect/valence-labelled CSI |
| `exo_dream_stage` | sleep staging needs PSG reference (EEG/EOG/EMG) | PSG-staged overnight CSI |
| `exo_gesture_language` | coarse gesture clusters ≠ true sign language (ADR-160 §A4) | Labelled ASL letter/word CSI dataset |
> The above are **not failures** — they are the honest boundary. A smaller set of
> genuinely-measured skills plus this explicit gated list is the deliverable, per
> the prove-everything directive.
---
## Skills not in either list
The remaining edge skills (smart-building / retail / industrial occupancy-style,
the other `sig_*`/`lrn_*`/`spt_*`/`tmp_*`/`qnt_*`/`aut_*`/`ais_*` algorithm-named
modules) are **wired and exercised live** in the unified pipeline integration test
(`tests/pipeline_all.rs`, all 59 default / 64 medical skills run without panic over
300 synthetic frames) but were **not** given an individual planted-ground-truth
accuracy number here. They are honest REAL-DSP modules (ADR-160) whose physical
observable could be planted with more harness work; that is deferred, not claimed.
## Test counts (full crate suite)
```
DEFAULT (--features std): 631 passed, 0 failed
(lib 504; budget 25; honest_labeling 10; pipeline_all 4; synthetic_validation 12; bench 1; vendor 75)
MEDICAL (--features std,medical-experimental): 669 passed, 0 failed
(lib 542; +16 same new tests; med_* stay DATA-GATED, not validated)
```
(M6 baseline was 615 / 653; the new pipeline_all (4) + synthetic_validation (12)
tests add 16 to each tier.)
@@ -0,0 +1,130 @@
# ADR-132: HOMECORE-RECORDER — State History + Semantic Search
| Field | Value |
|-------|-------|
| **Status** | Accepted |
| **Date** | 2026-05-25 |
| **Deciders** | ruv |
| **Codename** | **HOMECORE-RECORDER** |
| **Crate** | `v2/crates/homecore-recorder` |
| **Relates to** | [ADR-126](ADR-126-ruview-native-ha-port-master.md) (HOMECORE master — series map row ADR-132), [ADR-127](ADR-127-homecore-state-machine-rust.md) (HOMECORE-CORE state machine), [ADR-124](ADR-124-rvagent-mcp-ruvector-npm-integration.md) (ruvector/SENSE-BRIDGE), [ADR-130](ADR-130-homecore-rest-websocket-api.md) (HOMECORE-API query surface, downstream) |
| **Tracking issue** | [#800](https://github.com/ruvnet/RuView/pull/800) (HOMECORE intake) |
> **Documented retroactively (2026-06-12).** The `homecore-recorder` crate shipped under
> the ADR-126 series map (which planned an "ADR-132 HOMECORE-RECORDER") but the standalone
> ADR file was never written; the crate's `Cargo.toml`, `README.md`, `lib.rs`, `schema.rs`,
> and `semantic.rs` all cite "ADR-132". This ADR reverse-documents the decision that the
> shipped, tested code already embodies (ADR-164 Gap G3 / Coverage-Gaps Lens §A). It does
> **not** introduce new design; it records what is built. Date reflects the crate's intake
> era (first commit `e96ebaea8`, 2026-05-25); real-impl pass landed in `7c8071145`
> (2026-06-11).
---
## 1. Context
ADR-126 (the HOMECORE master) decided to reimplement Home Assistant (HA) natively in Rust.
HA persists every state change to a SQLite *recorder* database; downstream features
(history graphs, the logbook, long-term statistics, automation conditions that reference
past state) all read that store. HOMECORE therefore needs a durable state-history backbone.
Two forces shape the decision:
1. **Migration / coexistence.** Users adopting HOMECORE will have an existing HA
`recorder` database. Reusing HA's on-disk schema (rather than inventing a new one) lets
HOMECORE read an existing HA `home-assistant_v2.db` directly and lets HA-aware tooling
read HOMECORE's store. This is the same trust boundary that `homecore-migrate`
(ADR-165) handles for `.storage/*.json`.
2. **Semantic queries.** HA history is queried with SQL `BETWEEN`/`WHERE` clauses. The
HOMECORE platform already carries ruvector (ADR-124) for vector search, so the recorder
can additionally embed state changes and answer natural-language queries
("which kitchen devices were warm at 3 PM?") via k-NN — a capability HA does not have.
The recorder is the **durable-state surface**: if it is wrong, history, logbook, and
historical-condition automations are all wrong. ADR-164 flagged it as a CRITICAL coverage
gap precisely because such a load-bearing crate had no governing ADR.
## 2. Decision
Ship `homecore-recorder` as a SQLite state-history recorder with an HA-compatible schema
and an optional ruvector-backed semantic index, in three phases. P1 and P2 are built and
tested; P3 is planned.
### 2.1 Storage — SQLite with the HA recorder schema (P1, shipped)
- Persist via `sqlx` with the SQLite backend only (no Postgres, no TLS feature set).
- Mirror HA recorder **schema v48** so the store is bidirectionally readable
(`src/schema.rs`):
- `state_attributes` — shared attribute JSON blobs, deduped by an FNV-1a 64-bit hash
stored as a signed `i64` (matches HA's dedup key);
- `states` — one row per state write (`entity_id`, `state`, `attributes_id` FK,
`last_changed_ts`/`last_updated_ts` as REAL Unix seconds, `context_id` UUID);
- `events` — domain events (`event_type`, `event_data` JSON, `time_fired_ts`);
- `recorder_runs` — boot/shutdown bookends for history-gap detection.
- All DDL uses `CREATE TABLE IF NOT EXISTS`, so schema application is idempotent and safe
on every startup.
- Default persistence path `.homecore/home.db` (configurable).
### 2.2 Capture — listener on the HOMECORE event bus (P1, shipped)
- `RecorderListener` subscribes to the HOMECORE event bus (ADR-127) and captures
`StateChanged` events, writing snapshots through `Recorder` (`src/listener.rs`,
`src/db.rs`).
- A `DedupEngine` (`src/dedup.rs`) skips redundant writes when the state hash is unchanged,
matching HA's stateful-listener behaviour.
### 2.3 Semantic search — ruvector HNSW (P2, shipped, feature-gated)
- Behind the `ruvector` Cargo feature, the `Recorder` additionally calls a `SemanticIndex`
implementation (`src/semantic.rs`) that embeds state attributes and stores vectors in a
`ruvector-core` HNSW index for k-NN search.
- P2 embeddings are **hash-based** (sha2) — a deliberate, honest placeholder. They give a
working HNSW surface without claiming sentence-level semantic quality.
- When the feature is off, `NullSemanticIndex` satisfies the `SemanticIndex` trait bound
with no allocation, so the structural recorder ships independently of ruvector.
### 2.4 Real sentence embeddings (P3, planned — not yet built)
- Replace the hash embeddings with ruvector-attention sentence embeddings (dim → 384). Not
implemented; tracked as a follow-up. The README and `Cargo.toml` label this P3 explicitly.
### 2.5 Test evidence (as shipped)
- P1: 14 tests (`cargo test -p homecore-recorder --no-default-features`).
- P2: 20 tests (`cargo test -p homecore-recorder --features ruvector`).
## 3. Consequences
**Positive.**
- HA-schema compatibility makes migration (ADR-165) and coexistence cheap: HOMECORE can
read an existing HA `recorder.db`, and any SQLite tool can read HOMECORE's history.
- The semantic index is **additive** and feature-gated: the durable structural recorder has
no hard dependency on ruvector, so the storage backbone ships first.
- Standard SQLite means no proprietary export format; history is directly queryable.
**Negative / honest limits.**
- P2 semantic search uses **hash embeddings**, not real sentence embeddings — query quality
is limited until P3. This is disclosed in the crate docs and here; it must not be cited as
semantic-quality-validated.
- No per-crate benchmarks exist yet; the latency figures in the README
(state-write p50 < 2 ms, semantic search < 10 ms on 1 M records) are design targets /
estimates, **needs verification** with a criterion baseline.
- Pinning to HA schema v48 couples HOMECORE to a specific HA recorder schema generation;
future HA schema bumps require an explicit migration step.
**Neutral.**
- This ADR governs the recorder crate only. The query/REST surface over recorder data is
HOMECORE-API (ADR-130, P3); automation conditions on historical state are
HOMECORE-automation (ADR-129, P3).
## 4. Links
- Crate: `v2/crates/homecore-recorder/``Cargo.toml`, `README.md`, `src/lib.rs`,
`src/db.rs`, `src/schema.rs`, `src/dedup.rs`, `src/listener.rs`, `src/semantic.rs`.
- [ADR-126](ADR-126-ruview-native-ha-port-master.md) — HOMECORE master (series map: ADR-132 = HOMECORE-RECORDER).
- [ADR-165](ADR-165-homecore-migrate-from-home-assistant.md) — HOMECORE-MIGRATE (reads HA `.storage`; P2 exports a side-by-side recorder DB).
- [ADR-164](ADR-164-adr-corpus-gap-analysis.md) — gap analysis that surfaced this missing ADR (Gap G3).
- [Home Assistant Recorder integration](https://www.home-assistant.io/integrations/recorder/).
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see §8 Implementation Status, commit `11f89727f`) |
| **Date** | 2026-05-28 |
| **Deciders** | ruv |
| **Codebase target** | `wifi-densepose-core` (`types.rs`: `CsiFrame`/`CsiMetadata`); `wifi-densepose-signal/src/ruvsense/mod.rs` (`RuvSensePipeline`, six-stage flow); `v2/Cargo.toml` (workspace topology) |
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `4fa3847ac`) |
| **Date** | 2026-05-28 |
| **Deciders** | ruv |
| **Codebase target** | `wifi-densepose-signal` (`ruvsense/multistatic.rs``fuse`, `attention_weighted_fusion`); `wifi-densepose-ruvector` (`viewpoint/fusion.rs``MultistaticArray`); `wifi-densepose-bfld` (`event.rs`) |
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `fc7674bde`) |
| **Date** | 2026-05-28 |
| **Deciders** | ruv |
| **Codebase target** | `wifi-densepose-signal` (`ruvsense/multiband.rs`, `ruvsense/multistatic.rs`); `wifi-densepose-ruvector` (`viewpoint/geometry.rs`, `viewpoint/coherence.rs`, `viewpoint/attention.rs`, `viewpoint/fusion.rs`) |
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `521a012d8`) |
| **Date** | 2026-05-28 |
| **Deciders** | ruv |
| **Codebase target** | New module/crate `wifi-densepose-worldgraph` alongside `v2/crates/wifi-densepose-geo` and `v2/crates/homecore`; petgraph bridge pattern from `v2/crates/ruv-neural/ruv-neural-graph/src/petgraph_bridge.rs`; integrates `homecore/src/registry.rs` `area_id` and `wifi-densepose-mat/src/domain/scan_zone.rs` |
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `169a355bd`) |
| **Date** | 2026-05-28 |
| **Deciders** | ruv |
| **Codebase target** | `wifi-densepose-sensing-server/src/semantic/` (`bus.rs`, `common.rs`); `homecore/src/state.rs` + `event.rs`; `homecore-assist` |
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `7d88eb84c`) |
| **Date** | 2026-05-28 |
| **Deciders** | ruv |
| **Codebase target** | `wifi-densepose-bfld` (new module `mode.rs` + `attestation.rs`; extends `lib.rs` `PrivacyClass`, `sink.rs`, `privacy_gate.rs`, `identity_risk.rs`, `emitter.rs`, `ha_discovery.rs`) |
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `1f8e180d6`) |
| **Date** | 2026-05-28 |
| **Deciders** | ruv |
| **Codebase target** | `wifi-densepose-signal` (`ruvsense/longitudinal.rs`, `ruvsense/attractor_drift.rs`, `ruvsense/calibration.rs`, `ruvsense/field_model.rs`, `ruvsense/tomography.rs`); `wifi-densepose-bfld` (`privacy_gate.rs`) |
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Status** | Accepted — partial (built + tested building block, v1 fixed-map default; v2 dataset-gated — see Implementation Status, commit `2d4f3dea5`) |
| **Date** | 2026-05-28 |
| **Deciders** | ruv |
| **Codebase target** | `wifi-densepose-signal` (`ruvsense/field_model.rs`, new `ruvsense/rf_slam.rs`); `wifi-densepose-mat` (`tracking/kalman.rs`, `localization/triangulation.rs`); `wifi-densepose-geo`; `wifi-densepose-ruvector` (`mat/triangulation.rs`) |
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Status** | Accepted — partial (built + tested building block; no UWB radio in fleet — see Implementation Status, commit `b10bc2e9a`) |
| **Date** | 2026-05-28 |
| **Deciders** | ruv |
| **Codebase target** | `wifi-densepose-hardware` (new UWB driver/parser/auto-detect in `src/`); `wifi-densepose-signal` (`ruvsense/pose_tracker.rs` constraint-aware Kalman update); `wifi-densepose-mat` (`localization/fusion.rs` constraint integration) |
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `0f336b7d3`) |
| **Date** | 2026-05-28 |
| **Deciders** | ruv |
| **Codebase target** | `wifi-densepose-train` (`src/eval.rs`, `src/metrics.rs`, `src/ruview_metrics.rs`, `src/proof.rs`); `wifi-densepose-signal` (`src/bin/*_proof_runner.rs`); `wifi-densepose-cli` |
@@ -178,13 +178,42 @@ label or behavior change, consistent with leaving their claim surface intact.)
## Deferred Backlog (Nothing Dropped)
- **Per-skill accuracy validation** — **DATA-GATED**. Validating any med_*/affect/
sign-language claim requires labelled clinical/affective/ASL data and reference
standards that do not exist in this repo. The disclaimers + feature gate are the
honest stand-in. Nothing is claimed that is not measured.
- **Criterion benches for `process_frame` budget claims** — **ACCEPTED-FUTURE**.
`tests/budget_compliance.rs` asserts L/S/H tier wall-clock budgets (25 tests,
passing), but a regression-grade criterion bench is not yet wired.
- **Per-skill accuracy validation** — **PARTIALLY MEASURED-on-synthetic**
(2026-06-13). For the subset of skills whose detection target is *constructible*
with known ground truth, a synthetic-ground-truth harness
(`tests/synthetic_validation.rs`, 12 tests) plants signals with known answers,
runs the real detector, and **measures** detection accuracy / rate-error:
`vital_trend`, `exo_time_crystal` (periodic-vs-aperiodic — its sub-harmonic-vs-
clean-period claim is NOT separable, recorded honestly), `exo_ghost_hunter`
(hidden breathing), `occupancy`, `intrusion`, `exo_rain_detect`,
`sig_flash_attention` (8/8 peak localization), `spt_spiking_tracker` (4/4 zone
localization, sparse plant), `sig_optimal_transport`, `sig_mincut_person_match`
(0 id-swaps), `lrn_dtw_gesture_learn` (enrollment) — all 1.000 where claimed;
`sig_sparse_recovery`'s recovery accuracy is reported **negative** (2.2% vs
unrecovered baseline) — only its trigger path is validated. Full numbers +
reproduce commands in `benchmarks/edge-skills/RESULTS.md`.
The **med_*/affect/sign-language/weapon** claims remain **DATA-GATED**:
validating them requires labelled clinical/affective/ASL/metal-object data and
reference standards that do not exist in this repo. Planting a "seizure-/weapon-/
happy-like" synthetic signal validates nothing real and is explicitly refused;
RESULTS.md lists each with the real data it needs. The disclaimers + feature gate
are the honest stand-in. Nothing is claimed that is not measured.
- **Unified edge pipeline** — **MEASURED** (2026-06-13). `src/pipeline_all.rs`
(`EdgePipeline`) + `src/skill_registry.rs` register **every** runtime skill
behind one uniform `EdgeSkill` trait and run them all per CSI frame; `med_*` are
registered only under `--features medical-experimental` (preserves the §A1 gate).
`tests/pipeline_all.rs` (4 tests) proves all 59 default / 64 medical skills run
without panic over 300 synthetic frames with a well-formed aggregated event
stream. `examples/run_all_skills.rs` is a runnable demo. No skill DSP changed.
- **Criterion benches for `process_frame` budget claims** — **DONE (host)**
(ADR-163, 2026-06-12). `benches/process_frame_bench.rs` benches the heaviest
hot paths (`exo_time_crystal` 256×128 autocorrelation, `exo_ghost_hunter`
periodicity, `sec_weapon_detect` per-subcarrier Welford, `med_seizure_detect`
clonic rhythm) and reports committed **host** medians
(`benchmarks/edge-latency/RESULTS.md`). `tests/budget_compliance.rs` continues
to assert the L/S/H tier wall-clock budgets (25 tests, passing). **ESP32-on-
hardware (Xtensa/WASM3) latency remains PENDING** — the host bench is an
upper-bound algorithm-cost proxy, NOT the ESP32 figure (needs hardware).
- **`wasm32-unknown-unknown` `static_mut_refs` confirmation** — **ACCEPTED-FUTURE**
(toolchain): the source pattern is eliminated; a CI job on the wasm target should
assert zero `static_mut_refs` once the target is added to the build image.
@@ -0,0 +1,123 @@
# ADR-163: Edge-Latency Measurement — CLAIMED budgets → MEASURED-on-host
- **Status**: accepted
- **Date**: 2026-06-12
- **Deciders**: ruv
- **Tags**: edge-latency, wasm-edge, esp32, cog-inference, criterion, prove-everything, measurement-debt
- **Amends**: ADR-160 (deferred "criterion benches for process_frame budget claims" line now DONE-on-host); ADR-159 (cog inference latency)
## Context — Milestone 9 of the beyond-SOTA sweep
Prior milestones (M5/M6, ADR-159/ADR-160) flagged **measurement debt**: edge
latency budgets asserted in doc-comments and manifests but **never reproduced by
a committed benchmark**. Specifically:
- Many `wifi-densepose-wasm-edge` skill modules document a timing budget *"on
ESP32-S3 WASM3"* (e.g. `exo_time_crystal`: "H (heavy, <10 ms)"). These were
**CLAIMED**, not benchmarked. ADR-160's deferred backlog named exactly this:
*"Criterion benches for `process_frame` budget claims — ACCEPTED-FUTURE."*
- `cog-pose-estimation`'s manifest cites `cold_start_ms_avg: 5.4`, but neither
cog had a `benches/` directory or any committed inference-latency number.
Under the project's **prove-everything / anti-"AI-slop"** directive, a CLAIMED
latency budget that a skeptic cannot reproduce is debt. M9 pays it down — benches
and docs only, **no production-code behavior change** (so nothing republishes).
## Headline
**Converted the CLAIMED edge-latency budgets into MEASURED-on-host numbers, with
the honest host-vs-ESP32 caveat stated everywhere.** Added committed criterion
benches over the heaviest hot paths and a results file a skeptic can re-run. The
ESP32-on-hardware figure remains explicitly **UNMEASURED** — this milestone does
not pretend a laptop reproduces an Xtensa/WASM3 budget.
## Decision — benches landed
### T1 — wasm-edge `process_frame` budget benches
`v2/crates/wifi-densepose-wasm-edge/benches/process_frame_bench.rs` (criterion,
`harness = false`, `required-features = ["std"]`). The crate is **excluded from
the v2 workspace**, so it runs from the crate dir. Benches the M6-audit-named
heaviest hot paths over a **fixed synthetic CSI frame**, each driven through the
public `process_frame` after warming the relevant ring/phase buffers so the
expensive path actually executes:
- `exo_time_crystal::process_frame` — full 256-pt × 128-lag autocorrelation.
- `exo_ghost_hunter::process_frame` — empty-room periodicity / hidden-breathing.
- `sec_weapon_detect::process_frame` — per-subcarrier (MAX_SC=32) Welford.
- `med_seizure_detect::process_frame` — clonic-rhythm path (`#[cfg(feature =
"medical-experimental")]`, only built/run with that gate).
The lib's `bench = false` was set so the libtest harness does not intercept
criterion CLI flags; the `ghost_hunter` bin is already `standalone-bin`-gated and
not built under `--features std`.
**Measured host medians** (Intel Core Ultra 9 285H, native `--release`):
`exo_time_crystal` **17.3 µs** · `exo_ghost_hunter` **1.44 µs** ·
`sec_weapon_detect` **0.42 µs** · `med_seizure_detect` **0.10 µs**.
### T2 — cog inference latency benches
`v2/crates/cog-person-count/benches/infer_bench.rs` and
`v2/crates/cog-pose-estimation/benches/infer_bench.rs` (criterion,
`harness = false`). Each loads the **real** shipped weights from the in-repo
`cog/artifacts/`, asserts the Candle CPU backend (so the stub can never be
silently benched), warms one forward, then times steady-state
`InferenceEngine::infer` over a fixed CSI window on `Device::Cpu`.
**Measured host medians:** cog-person-count **305 µs** · cog-pose-estimation
**305 µs** (steady-state, CPU, real weights).
### T3 — results file
`benchmarks/edge-latency/RESULTS.md`, in the `benchmarks/wiflow-std/RESULTS.md`
style: each number with its exact reproduce command, the machine, the
MEASURED-on-host grade, and the honest caveat.
## The honest caveat (recorded, non-negotiable)
1. **Host ≠ ESP32.** The wasm-edge benches run native x86_64, not Xtensa/WASM3.
A host median is an **upper bound on algorithm work**, not the ESP32 number;
WASM3 interpretation on a ~240 MHz core is 12 orders of magnitude slower than
native `-O`. A host median under budget does **not** prove the ESP32 meets it.
**The ESP32 figure is NOT reproduced here — it needs hardware.**
2. **Bench ≠ the doc-claimed measurement.** The cogs' manifest cites a
**cold-start** number (weight-load included); these benches measure
**steady-state** per-frame `infer`. We report both, labelled, and do not
conflate them. Empirically, pose steady-state (305 µs host) is ~18× under the
5.4 ms cold-start — the expected shape, and exactly why conflating would lie.
## Deferred / still-pending (nothing dropped)
- **ESP32-on-hardware `process_frame` latency** — **PENDING (hardware)**. Needs
the `wasm32-unknown-unknown` target built + flashed to an ESP32-S3 and timed
under WASM3. The host bench is the algorithm-cost proxy until then.
- **Per-skill *accuracy*** remains **DATA-GATED** (unchanged from ADR-160) —
this ADR measures latency only, never claims detection accuracy.
## Reproduction (MEASURED)
```bash
# T1 — wasm-edge (workspace-excluded → run from the crate dir)
cd v2/crates/wifi-densepose-wasm-edge
cargo bench --features std -- --warm-up-time 1 --measurement-time 2
cargo bench --features std,medical-experimental -- --warm-up-time 1 --measurement-time 2 med_seizure
# T2 — cogs (workspace members)
cd v2
cargo bench -p cog-person-count --no-default-features --bench infer_bench
cargo bench -p cog-pose-estimation --no-default-features --bench infer_bench
# existing tests still green (behavior unchanged)
cargo test -p cog-person-count -p cog-pose-estimation --no-default-features
```
## Consequences
- ADR-160's deferred *"Criterion benches for `process_frame` budget claims"* line
is now **DONE (host)**; the ESP32-on-hardware confirmation is explicitly the
one remaining pending item.
- The cogs now ship committed, reproducible steady-state inference-latency
numbers, cleanly distinguished from the manifest's cold-start claim.
- No runtime behavior changed; no crate republishes. `PROOF.md`'s performance
table and `scripts/prove.sh`'s gated section reference the new benches.
+125
View File
@@ -0,0 +1,125 @@
# ADR-164: ADR Corpus Gap Analysis & Remediation Backlog
- **Status:** proposed
- **Date:** 2026-06-12
- **Deciders:** ruv
- **Tags:** governance, meta
## 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.
Two forces motivate a corpus-wide gap analysis *now*:
1. **The beyond-SOTA / anti-AI-slop sweep (ADR-154163) just landed.** That sweep is itself a structured retraction layer: each ADR exists *because* an earlier accepted-or-shipped claim was found false (a dead CIR coherence gate, a fake-gradient TTA path, a self-certifying proof, a WebSocket auth bypass, an inflated survivor count). The sweep hardened five subsystems but was narrowly scoped — it never touched the two largest capability gaps (camera-teacher training validation; federation/BFLD privacy chains). A ledger is needed to record what the sweep retracted and what it left open.
2. **The status field can no longer be trusted as a source of truth.** A five-lens audit (status-distribution, supersession-chains, contradictions, coverage-gaps, data-hardware-gated) found ~24 ADRs mislabeled `Proposed` while their own commit-pinned Implementation-Status notes report them built and tested; 6 ADR numbers collide; 3 files have no Status header at all. An auditor reading headers would conclude "not built" for landed code, and "built/Accepted" for unvalidated capability.
The detailed lens outputs and the full per-ADR census live in `docs/adr/gap-analysis/` (`lens-findings.md`, `census.md`). This ADR is the authoritative summary and remediation backlog.
## Decision
**This ADR is the authoritative gap ledger and remediation backlog for the ADR corpus as of 2026-06-12.** It does not change any subsystem behavior. It records, with cited ADR ids:
- the status/impl distribution and the bookkeeping-drift problem;
- a prioritized Gap Register with a recommended action per gap;
- supersession-integrity defects;
- the contradiction/retraction list (the anti-slop centerpiece);
- shipped capabilities with no governing ADR;
- the genuinely open data/hardware-gated backlog.
Until the Gap Register items are worked, **treat the ADR Status header as advisory, not authoritative**, and treat any accuracy number authored before ADR-155 landed as CLAIMED (not MEASURED) until re-derived through the post-155 leak-free validation split.
## Status Distribution
Counts are approximate (`~`) where a status string is non-canonical or dual-valued; the per-ADR breakdown is in `census.md`.
| Status bucket | Count | impl_state | Count |
|---|---|---|---|
| Accepted (incl. partial/in-progress/Phase-1 variants) | ~56 | implemented | ~36 |
| 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/…) |
| 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.
## Gap Register
Severity: CRITICAL (corpus integrity / tooling-breaking / life-safety / security) · HIGH · MEDIUM · LOW. Action vocabulary: *implement · supersede · mark-stale · write-missing-ADR · close-as-gated · renumber · reconcile-docs*.
| 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.** |
| 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 | 136145 | ~~mark-stale → "Accepted — partial (integration glue pending)" (one batch)~~ DONE: all 10 (136145) 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" |
| G6 | Stale `Proposed` headers on built+published code | HIGH | 029/030/031, 095/096, 152, 154157, 024/027/072, 150 | mark-stale; reconcile with downstream/CLAUDE.md evidence |
| G7 | Status-graph inversion: Accepted ADR depends on Proposed parent | HIGH | 032→029/030/031; 053→052; 048→045; 077→075/076; 104→103 | promote parents to match built reality, or downgrade dependents |
| 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 135146) | 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) |
| 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) |
| G15 | ~50 Proposed-forever chains pollute every gap analysis | MEDIUM | 003/007010, 105109, 118125, HOMECORE 124133, 033/046/049/067/074/085 | close-as-gated or mark Deferred/Rejected + open tracking issues |
| G16 | De-facto supersessions never recorded (lifecycle graph incomplete) | MEDIUM | 098/099, 063/064, 042/153, 050/060, 035/023, 100/109, 117 retracts PyPI v1.1.0 | reconcile (add supersedes/superseded_by fields) |
| G17 | Accepted but no implementation evidence ("unverified done") | MEDIUM | 034 (FieldView app — no crate); 044 (wifi-densepose-geo — bare Accepted, no Date/Deciders) | implement or downgrade to Proposed |
| G18 | Workspace has ~38 crates; CLAUDE.md publishing list (12-step) and crate table (15) are stale | MEDIUM | corpus-wide (crate-graph topology) | write-missing-ADR (crate-graph / publish boundaries) + reconcile CLAUDE.md |
## Supersession Integrity
Only **3 formal supersession edges** exist; all three are defective (see G8/G12; full detail in `lens-findings.md` Lens 2):
- **ADR-002 → ADR-016 / ADR-017** is one-directional. ADR-016 never mentions ADR-002 (its References list only 014/015); ADR-017 only *corrects* ADR-002's "fictional crate names" and never says "supersede." The census `supersedes:["ADR-002"]` on 016/017 is **file-unsupported** — the superseded ADR points up at two successors that do not point back.
- **ADR-002 is an umbrella** whose children 003/007/008/009/010 are still `Proposed`. ADR-016/017 realize only the training/signal/MAT integration points; the RVF-container (003), PQ-crypto (007), Raft (008), WASM-edge-runtime (009), and witness-chains (010) decisions are **neither implemented nor formally superseded**. Marking the parent fully "Superseded" silently buries 5 live-but-abandoned child decisions. Recommended: split ADR-002 to "partially superseded."
- **ADR-052-tauri → ADR-054** is declared by the predecessor but ADR-054 contains zero references to ADR-052. ADR-054 ("Full Implementation", in progress) is the impl plan *for* 052, not a replacement — likely a mis-modeled edge.
- **No cycles** detected. The graph is clean structurally; the defect is missing reciprocity and ~7 unrecorded de-facto supersessions (G16).
## Contradictions & Retractions (anti-slop centerpiece)
The four CRITICAL items are the corpus's load-bearing AI-slop admissions — each an accepted-or-shipped surface whose stated accuracy/security/function was provably false until the sweep landed. **Every accuracy number predating ADR-155 should be treated as CLAIMED until re-derived through the post-155 leak-free split.** Source-cited evidence is in `lens-findings.md` Lens 3.
- **[CRITICAL] ADR-155** retracts every prior NN accuracy/TTA/proof claim: real MM-Fi training validated against a *synthetic* val set with stride-1 (~99%) window leakage (§2.2); a *fake gradient* `grad += v*0.01` in the TTA path (§2.3); a *self-certifying* proof that blessed whatever the pipeline emitted and PASSed on 1e-9 float noise (§2.4).
- **[CRITICAL] ADR-154** proves the ADR-134 CIR coherence gate was **dead in production for every canonical 56-tone frame** (`SubcarrierMismatch`, 0 Ok / 8 mismatch), silently degrading coherence to freq-only. Any "CIR-enhanced coherence/ToF" claim before this fix overstated reality.
- **[CRITICAL] ADR-079** carries three mutually inconsistent values for its own central metric: proxy PCK@20 = 2.5% (prose) vs 35.3% (baseline table — equal to the *target*) vs 0% upper-body joints; #640 measured 0% on real local data. An Accepted ADR whose headline 1020x improvement is self-refuting.
- **[CRITICAL] ADR-161** fixes a HOMECORE WebSocket **auth bypass** (any non-empty token accepted) + reply-theater + no-op automation; **ADR-162** then enforces plugin Ed25519 signature verification, capability isolation, and bounded RunModes — retracting ADR-128/129/130's implied security guarantees.
- **[HIGH]** ADR-152 self-refutes 1 of 25 claims (ESP WiFi-6 "drop-in" REFUTED 0-3); CLAUDE.md's "WiFlow-STD MEASURED-EQUIVALENT ~96% PCK" contradicts §F1's own gating (97.25% is CLAIMED until measurements (a)(c) run). ADR-150 retracts the implied cross-subject capability (81.63% in-domain vs ~11.6% leakage-free cross-subject; DANN ~0 gain). ADR-159 ships real models but discloses person-count `training_class1_accuracy = 0.343` and renames "learned multi-person counter" → "presence detector," gutting ADR-103/104's claim.
- **[MEDIUM]** ADR-163 leaves the ESP32/Xtensa on-hardware latency figure UNMEASURED; ADR-098↔099 partial reversal on midstream; ADR-147 self-retracts Cosmos for OccWorld.
## Coverage Gaps (shipped capability, no/broken governing ADR)
- ~~**CRITICAL — `homecore-recorder`** (SQLite state history + semantic search) cites "ADR-132", which **does not exist**. The durable-state backbone is ungoverned. → write HOMECORE-RECORDER ADR.~~ **RESOLVED in `docs/adr-gap-remediation-1`:** ADR-132 written (`ADR-132-homecore-recorder-history-semantic-search.md`, Status: Accepted — reverse-documented from the shipped crate).
- ~~**CRITICAL — `homecore-migrate`** (reads untrusted Python-HA `.storage/*.json`) cites "ADR-134", but on-disk ADR-134 is CIR. A data-integrity-sensitive importer governed by a phantom identity. → resolve 134 collision + write HOMECORE-MIGRATE ADR (trust boundary).~~ **RESOLVED in `docs/adr-gap-remediation-1`:** ADR-165 written (`ADR-165-homecore-migrate-from-home-assistant.md`, Status: Accepted — P1 scaffold); crate's `ADR-134` refs repointed → ADR-165; on-disk ADR-134 (CIR) left intact. ADR-126's series-map row (which labels the *role* "ADR-134 HOMECORE-MIGRATE") is owner-gated and unchanged.
- **HIGH — `wifi-densepose-engine`** composes ADR-135..146 onto the live 20 Hz path but **no ADR governs the integrator contract** (ordering, back-pressure, "one pipeline cycle" boundary).
- **MEDIUM — `wasm-edge`** (~70 skills) governed only by remediation ADRs 160/163 — no creation/taxonomy/ABI ADR. **`occworld-candle`** is a Rust-native backend swap ADR-147 explicitly deferred. **`pointcloud`** has only a viewer-deploy ADR (094), no data-format contract.
- **MEDIUM — workspace topology:** ~38 crates exist; the CLAUDE.md 15-crate table and 12-step publishing order are stale, and no ADR governs crate-graph/publish boundaries at this scale.
- Verified-governed (scoped out): worldmodel→147, worldgraph→139, cog-*→101/103/116, ruview-swarm→148, nvsim→089/092, bfld→118-123/141, calibration→151, homecore-hap→125, geo→044, desktop→052/054.
## Open / Gated Backlog (genuinely unresolved, honestly labeled)
The ADR-154163 sweep was narrowly scoped. The two largest **capability** gaps it did not touch:
- **CRITICAL — Camera-teacher training validation (ADR-079 / 072 / 150).** P7P9 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-105109, 118125).** 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.
- **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.
## Consequences
**Positive.** One authoritative ledger replaces scattered, drifting status fields. The anti-slop retractions are recorded in a citable place, so the "AI slop" accusation is met with a structured admission + fix-trail rather than denial. The Gap Register is a concrete, severity-ordered work queue. Batch-fixing G5 (10 streaming-engine headers) and G1/G2 (numbering + missing headers) is high-ROI and unblocks ADR tooling.
**Negative.** This ADR is a snapshot; it goes stale the moment the next ADR lands. Counts marked `~` are approximate and a few impl_state values are *needs verification* (downstream-prose-derived, not file-confirmed). Acting on the register (renumbering, status flips, supersession edits) touches ~30 files and risks transient cross-reference breakage if not done atomically.
**Neutral.** No subsystem behavior changes. Renumbering decisions (which of the colliding files keeps each number) are deferred to the follow-up remediation PR — this ADR records the collision, not the resolution. Whether to close abandoned chains as `Rejected` vs `Deferred` is a judgment call left to the deciders per chain.
## Links
- `docs/adr/gap-analysis/census.md` — full per-ADR census (162 entries).
- `docs/adr/gap-analysis/lens-findings.md` — five-lens findings (status-distribution, supersession-chains, contradictions, coverage-gaps, data-hardware-gated), verbatim.
- Anti-slop sweep: ADR-154, ADR-155, ADR-156, ADR-157, ADR-158, ADR-159, ADR-160, ADR-161, ADR-162, ADR-163.
- Most-cited defects: ADR-079, ADR-134, ADR-002, ADR-136145, ADR-152.
- Governance: CLAUDE.md (crate table + publishing order — stale per G18); ADR-038 (prior roadmap census, now stale).
@@ -0,0 +1,129 @@
# ADR-165: HOMECORE-MIGRATE — Migration Tooling from Python Home Assistant
| Field | Value |
|-------|-------|
| **Status** | Accepted — P1 scaffold (full conversion deferred to P2) |
| **Date** | 2026-05-25 |
| **Deciders** | ruv |
| **Codename** | **HOMECORE-MIGRATE** |
| **Crate** | `v2/crates/homecore-migrate` |
| **Relates to** | [ADR-126](ADR-126-ruview-native-ha-port-master.md) (HOMECORE master — series map row "ADR-134 HOMECORE-MIGRATE"), [ADR-127](ADR-127-homecore-state-machine-rust.md) (HOMECORE-CORE), [ADR-132](ADR-132-homecore-recorder-history-semantic-search.md) (HOMECORE-RECORDER — P2 side-by-side export target) |
| **Tracking issue** | [#800](https://github.com/ruvnet/RuView/pull/800) (HOMECORE intake) |
> **Number-collision resolution (2026-06-12).** The HOMECORE series in ADR-126 §4 planned
> "ADR-134 = HOMECORE-MIGRATE", and the `homecore-migrate` crate cites "ADR-134" throughout.
> But the on-disk `ADR-134-csi-to-cir-time-domain-multipath.md` is a **different, unrelated
> decision** (First-Class CIR Support, a signal-processing tier). The migrate crate was
> therefore governed by a phantom identity (ADR-164 Gap G3 / Coverage-Gaps Lens §A). This
> ADR takes the next free number (**165**) and becomes the real governing record for
> HOMECORE-MIGRATE; the `ADR-134` references inside `v2/crates/homecore-migrate/` are
> repointed to ADR-165. The real ADR-134 (CIR) is untouched. ADR-126's series-map row still
> labels the *role* "ADR-134 HOMECORE-MIGRATE" for historical traceability; that registry
> renumber is owner-gated and left for the follow-up. This ADR reverse-documents the shipped
> P1 scaffold; it introduces no new design.
---
## 1. Context
ADR-126 decided to reimplement Home Assistant (HA) natively in Rust. A user adopting
HOMECORE has an existing HA install whose configuration lives in two places on disk:
- `.storage/*.json` — versioned JSON envelopes (`{ version, minor_version, data }`) holding
the entity registry, device registry, and config entries;
- top-level YAML — `secrets.yaml`, `automations.yaml`.
To migrate, HOMECORE must read this foreign, **untrusted** on-disk state. It is untrusted in
the security sense: the schema can drift between HA releases, and silently mis-parsing a
registry would corrupt the imported home. ADR-164 flagged this as a CRITICAL coverage gap —
a data-integrity-sensitive importer governed by a non-existent ADR identity.
The decision an ADR must pin here is the **trust boundary and import contract**: which HA
files are read, how schema versions are validated, and what happens on an unknown version.
## 2. Decision
Ship `homecore-migrate` as a CLI + library that reads an existing HA filesystem and imports
its configuration into HOMECORE. P1 is a **scaffold**: it parses and inspects everything and
converts the entity registry; full conversion of the remaining artifacts is deferred to P2.
### 2.1 Storage reader + versioned format gate (P1, shipped)
- `HaStorageDir` / `HaStorageEnvelope` read HA's `.storage/` directory; `read_envelope(path)`
deserializes a `.storage/*.json` envelope (`src/storage.rs`).
- Versioned parsers live under `storage_format::v<N>` (e.g. `v13` for the entity registry)
(`src/storage_format/`).
- **Schema-version validation is the load-bearing safety rule (§6 Q5 of this ADR):** an
unknown `minor_version` is a **hard error** (`MigrateError::UnsupportedSchemaVersion`),
never a silent best-effort parse. Better to refuse than to corrupt.
### 2.2 Per-artifact parsers (P1, shipped)
- `entity_registry::load()``core.entity_registry``Vec<homecore::EntityEntry>`
(ready for import).
- `device_registry::load()``core.device_registry``Vec<DeviceImport>` (P1 diagnostic;
full conversion P2).
- `config_entries::load()``core.config_entries` → domain counts + integration names
(the format is undocumented per §6 Q5; treated diagnostically).
- `secrets::load_secrets()``secrets.yaml``HashMap<String, String>` (resolution P2).
- `automations::load()``automations.yaml` → count + ID/alias list (conversion P2).
### 2.3 CLI (P1, shipped)
- `homecore-migrate inspect <ha-dir>` previews what will be migrated (entity/device/config
counts, redacted secret/automation lists) (`src/cli.rs`, `src/main.rs`).
- `import-entities` and `export-for-sidecar` are declared but their full behaviour is P2.
### 2.4 Structured errors (P1, shipped)
- `MigrateError` carries context (`path`, line/field) for I/O, JSON, YAML, missing-field,
unsupported-schema-version, and entity-id parse failures (`src/lib.rs`).
### 2.5 Deferred to P2+ (NOT built — honestly labelled)
- Convert `config_entries` → HOMECORE plugin manifests.
- Convert `automations.yaml``homecore-automation` YAML.
- Side-by-side runtime mode (requires `homecore-recorder`, ADR-132; behind the `recorder`
Cargo feature, currently a no-op stub).
- `!secret` reference resolution in non-secrets YAML files.
### 2.6 Test evidence (as shipped)
- 19 tests (`cargo test -p homecore-migrate`), per the crate README badge.
## 3. Consequences
**Positive.**
- The trust boundary is explicit: unknown HA schema versions are rejected, not guessed, so a
schema drift fails loudly instead of corrupting an imported home.
- Reusing HA's own `.storage` and YAML formats means no intermediate export step; the tool
reads a live HA install directly.
- P1 `inspect` gives users a no-risk dry run before any write.
**Negative / honest limits.**
- P1 is a **scaffold**: only the entity registry is conversion-ready. Device registry,
config-entry→plugin, automation, and secret-resolution conversions are P2 and **not yet
built** — the Status field and crate docs say so.
- The side-by-side recorder export depends on ADR-132 and is currently a feature-gated
no-op.
- Performance figures in the README (envelope parse < 5 ms, 1 000-entity load < 50 ms) are
estimates, **needs verification** with a benchmark.
**Neutral.**
- This resolves only the *identity* of the migrate decision (134→165). The broader 6-way
duplicate-number cleanup (incl. ADR-126's series-map registry row) is owner-gated.
## 4. Links
- Crate: `v2/crates/homecore-migrate/``Cargo.toml`, `README.md`, `src/lib.rs`,
`src/storage.rs`, `src/storage_format/`, `src/entity_registry.rs`,
`src/device_registry.rs`, `src/config_entries.rs`, `src/secrets.rs`,
`src/automations.rs`, `src/cli.rs`, `src/main.rs`.
- [ADR-126](ADR-126-ruview-native-ha-port-master.md) — HOMECORE master (series map: HOMECORE-MIGRATE).
- [ADR-132](ADR-132-homecore-recorder-history-semantic-search.md) — HOMECORE-RECORDER (P2 side-by-side export target).
- [ADR-134](ADR-134-csi-to-cir-time-domain-multipath.md) — First-Class CIR Support (the *unrelated* decision the crate was mistakenly citing).
- [ADR-164](ADR-164-adr-corpus-gap-analysis.md) — gap analysis that surfaced this collision (Gap G3).
- [Home Assistant `.storage` format](https://developers.home-assistant.io/docs/storage/).
+168
View File
@@ -0,0 +1,168 @@
# 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*.
| ADR | Title | Status | impl_state | Flags |
|-----|-------|--------|-----------|-------|
| ADR-001 | WiFi-Mat Disaster Detection Architecture | Accepted | implemented | data/hardware-gated (rubble-penetration unproven without field hardware) |
| ADR-002 | RuVector RVF Integration Strategy | Superseded by ADR-016 + ADR-017 | superseded | umbrella ADR; child ADRs 003/007/008/009/010 still Proposed |
| ADR-003 | RVF Cognitive Containers for CSI Data | Proposed | proposed-only | proposed-but-looks-abandoned (parent 002 superseded, never advanced) |
| ADR-004 | HNSW Vector Search for Signal Fingerprinting | Partially realized by ADR-024; extended by ADR-027 | partial | realized indirectly via downstream ADRs, not directly |
| ADR-005 | SONA Self-Learning Pose Estimation | Partially realized in ADR-023; extended by ADR-027 | partial | realized indirectly via ADR-023 (MicroLoRA/EWC++) |
| ADR-006 | GNN-Enhanced CSI Pattern Recognition | Partially realized in ADR-023; extended by ADR-027 | partial | realized indirectly via ADR-023 (2-layer GCN), scope narrowed |
| ADR-007 | Post-Quantum Cryptography for Secure Sensing | Proposed | proposed-only | proposed-but-looks-abandoned (parent 002 superseded) |
| ADR-008 | Distributed Consensus for Multi-AP | Proposed | proposed-only | proposed-but-looks-abandoned (parent 002 superseded) |
| ADR-009 | RVF WASM Runtime for Edge Deployment | Proposed | proposed-only | contradicts shipped wifi-densepose-wasm crate it proposes to replace |
| ADR-010 | Witness Chains for Audit-Trail Integrity | Proposed | proposed-only | witness-bundle (ADR-028) fills this role instead |
| ADR-011 | Python Proof-of-Reality / Mock Elimination | Proposed (URGENT) | partial | proof pipeline (verify.py/ADR-028) live despite Proposed status; credibility-gated |
| ADR-012 | ESP32 CSI Sensor Mesh | Accepted — Partially Implemented | partial | hardware-gated; mesh partial, single-node firmware working per ADR-018 |
| ADR-013 | Feature-Level Sensing on Commodity Gear | Accepted — Implemented (36/36 tests) | implemented | — |
| ADR-014 | SOTA Signal Processing | Accepted | implemented | — |
| ADR-015 | Public Dataset Training Strategy | Accepted | implemented | data-gated (MM-Fi/Wi-Pose availability/licensing) |
| ADR-016 | RuVector Training-Pipeline Integration | Accepted | implemented | supersedes ADR-002 (but file never mentions 002 — unsupported claim) |
| ADR-017 | RuVector Signal + MAT Integration | Accepted | implemented | CLAUDE.md still lists as Proposed; supersedes 002 only via "Correction" prose |
| ADR-018 | ESP32 Dev Implementation | Proposed | partial | status stale — ADR-012 cites it as working firmware/aggregator |
| ADR-019 | Sensing-Only UI Mode with Gaussian Splat Viz | Accepted | implemented | status in table format not ## header |
| ADR-020 | Migrate AI/Model Inference to Rust (RuVector + ONNX) | Accepted | partial | table-format status; overlaps ADR-019 backend-decoupling scope |
| ADR-021 | Vital Sign Detection via rvdna Pipeline | Partially Implemented | partial | wifi-densepose-vitals crate exists |
| ADR-022 | Enhanced Windows WiFi Fidelity via Multi-BSSID | Partially Implemented | partial | wifi-densepose-wifiscan crate exists |
| ADR-023 | Trained DensePose Model w/ RuVector Signal Intelligence | Proposed | proposed-only | data/hardware-gated; scaffold w/ random weights |
| ADR-024 | Project AETHER — Contrastive CSI Embedding | Proposed | proposed-only | CLAUDE.md lists Accepted; pose_tracker.rs uses AETHER re-ID — contradiction |
| ADR-025 | macOS CoreWLAN WiFi Sensing (ORCA) | Proposed | proposed-only | hardware-gated (Mac Mini M2 Pro); RSSI-only |
| ADR-026 | Survivor Track Lifecycle Management (MAT) | Accepted | implemented | explicit Supersedes: None |
| ADR-027 | Project MERIDIAN — Cross-Env Domain Generalization | Proposed | proposed-only | CLAUDE.md lists Accepted — contradiction; data-gated |
| ADR-028 | ESP32 Capability Audit & Witness Record | Accepted | implemented | audit/witness record; pins commit 96b01008 |
| ADR-029 | RuvSense — Sensing-First RF Multistatic Mode | Proposed | stale-or-contradicted | repo has ruvsense/ (16 modules); ADR-032 hardens it |
| ADR-030 | RuvSense Persistent Field Model | Proposed | stale-or-contradicted | field_model/longitudinal/cross_room modules exist; ADR-032 secures |
| ADR-031 | RuView — Cross-Viewpoint Fusion | Proposed | stale-or-contradicted | ruvector/src/viewpoint/ exists; near-duplicate of ADR-029 |
| ADR-032 | Multistatic Mesh Security Hardening | Accepted | implemented | hardens Proposed 029/030/031 — status-graph inversion |
| ADR-033 | CRV Signal Line Sensing (Coordinate Remote Viewing) | Proposed | proposed-only | speculative/metaphor-driven; abandonment risk |
| ADR-034 | Expo React Native Mobile App (FieldView) | Accepted | unknown | no mobile-app crate/dir in CLAUDE.md — unverified |
| ADR-035 | Live Sensing UI Accuracy & Data Source Transparency | Accepted | implemented | bug-fix; heuristic pose superseded in spirit by 023/036 |
| ADR-036 | RVF Model Training Pipeline & UI Integration | Proposed | proposed-only | overlaps ADR-023 scope |
| ADR-037 | Multi-Person Pose from Single ESP32 CSI Stream | Proposed | proposed-only | explicit Supersedes: None; HW limitation noted |
| ADR-038 | Sublinear GOAP for Roadmap Optimization | Proposed | proposed-only | meta/process ADR; own corpus census may be stale |
| ADR-039 | ESP32-S3 Edge Intelligence Pipeline | Accepted (hardware-validated) | implemented | hardware-validated |
| ADR-040 | WASM Programmable Sensing (Tier 3) | Accepted | implemented | depends on ADR-039; WASM3 optional |
| ADR-041 | WASM Module Collection — Sensing Registry | Accepted (Phase 1) | partial | ~57 modules catalog/proposed; exotic modules speculative |
| ADR-042 | Coherent Human Channel Imaging (CHCI) | Proposed | proposed-only | hardware-gated (custom PCB/TCXO); superseded-in-intent by ADR-153 |
| ADR-043 | Sensing Server UI API Completion | Accepted | implemented | internal route count contradiction (14 vs 17) |
| ADR-044 | Geospatial Satellite Integration | Accepted | unknown | no Date/Deciders; wifi-densepose-geo crate not in CLAUDE.md table |
| ADR-045 | AMOLED Display Support for ESP32-S3 | Proposed | proposed-only | hardware-gated (LilyGO T-Display-S3); ADR-048 depends on it |
| ADR-046 | Android TV Box / Armbian Deployment Target | Proposed | proposed-only | proposed-but-looks-abandoned; Phase 2 speculative |
| 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-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 | — |
| ADR-056 | RuView Desktop Complete Capabilities Reference | Accepted | partial | reference doc; "complete" overstates impl state |
| ADR-057 | Firmware CSI Build Guard & sdkconfig.defaults | Accepted | implemented | minor C6 CSI matrix tension vs CLAUDE.md |
| ADR-058 | Dual-Modal WASM Browser Pose (Video + CSI) | Proposed | partial | data-gated; ships placeholder weights |
| ADR-059 | Live ESP32 CSI Pipeline Integration | Accepted | implemented | hardware-gated (physical ESP32-S3 + UDP:5005) |
| ADR-060 | Provision Channel Override & MAC Filtering | Accepted | implemented | fulfills part of Proposed ADR-050(prov) without superseding |
| ADR-061 | QEMU ESP32-S3 Emulation for Firmware Testing | Accepted | implemented | RF-PHY paths untestable in QEMU |
| ADR-062 | QEMU ESP32-S3 Swarm Configurator | Accepted | implemented | — |
| ADR-063 | 60 GHz mmWave Sensor Fusion with WiFi CSI | Proposed | proposed-only | hardware-gated (ESP32-C6+MR60BHA2); superseded-in-scope by 064 |
| ADR-064 | Multimodal Ambient Intelligence (CSI+mmWave+env) | Proposed | proposed-only | hardware-gated; mixes build-now + speculative tiers |
| ADR-065 | Hotel Guest Happiness Scoring | Proposed | proposed-only | hardware-gated (Cognitum Seed Pi Zero 2 W) |
| ADR-066 | ESP32 CSI Swarm with Cognitum Seed Coordinator | Proposed | proposed-only | hardware-gated; overlaps 068/069 |
| ADR-067 | RuVector v2.0.4→v2.0.5 Upgrade | Proposed | proposed-only | CLAUDE.md still v2.0.4 — not adopted |
| ADR-068 | Per-Node State Pipeline for Multi-Node Sensing | Accepted | implemented | — |
| ADR-069 | ESP32 CSI → Cognitum Seed RVF Ingest Pipeline | Accepted | implemented | hardware-gated (live Cognitum Seed fw v0.8.1) |
| ADR-070 | Self-Supervised Pretraining from Live CSI + Seed | Accepted | partial | hardware-gated (live 2-node + Seed capture) |
| ADR-071 | ruvllm Training Pipeline for CSI Models | Proposed | proposed-only | overlaps 072/079 + libtorch pipeline |
| ADR-072 | WiFlow Pose Estimation Architecture | Proposed | partial | data-gated; referenced as implemented in CLAUDE.md (WiFlow-STD) — stale header |
| ADR-073 | Multi-Frequency Mesh Scanning | Proposed | proposed-only | hardware-gated (2-node multi-AP) |
| ADR-074 | Spiking Neural Network for CSI Sensing | Proposed | proposed-only | proposed-but-looks-abandoned (no in-repo SNN signal) |
| ADR-075 | Min-Cut Person Separation from Subcarrier Corr | Proposed | proposed-only | fixes #348; 077/078 depend on it though Proposed |
| ADR-076 | CSI Spectrogram Embeddings via CNN + Graph Transformer | Proposed | proposed-only | — |
| ADR-077 | Novel RF Sensing Applications | Accepted | partial | depends on Proposed 075/076; data-gated |
| ADR-078 | Multi-Frequency Mesh Sensing Applications | Proposed | proposed-only | hardware-gated; depends on Proposed 073 |
| ADR-079 | Camera Ground-Truth Training Pipeline | Accepted | partial | P7-P9 Pending; internal PCK contradiction (2.5% vs 35.3% vs 0%); #640 = 0% |
| ADR-080 | QE Analysis Remediation Plan | Proposed | proposed-only | unfixed security HIGH findings (XFF bypass, stack traces, JWT-in-URL) |
| ADR-081 | Adaptive CSI Mesh Firmware Kernel | Accepted — L1-5 host-tested | partial | mesh RX + Ed25519 signing deferred to Phase 3.5 |
| ADR-082 | Pose Tracker Confirmed-Track Output Filter | Accepted — implemented | implemented | fixes #420 |
| ADR-083 | Per-Cluster Pi Compute Hop | Proposed — pending field evidence | proposed-only | hardware-gated (status explicitly pending field evidence) |
| ADR-084 | RaBitQ Similarity Sensor (4 pipeline points) | Accepted — merged PR #435 | implemented | acceptance on synthetic data; <1pp regression deferred to soak |
| ADR-085 | RaBitQ Similarity Sensor — Pipeline Expansion (7 sites) | Proposed | proposed-only | proposed-but-looks-abandoned (refines 084, never advanced) |
| ADR-086 | Edge Novelty Gate — RaBitQ on Sensor MCU | Proposed | proposed-only | hardware-gated (no_std port + real-deployment suppression rates) |
| ADR-089 | nvsim — NV-Diamond Magnetometer Simulator | Accepted — Passes 1-5 merged | partial | Pass 6 (proof bundle + bench) pending |
| ADR-090 | nvsim — Full Hamiltonian/Lindblad Solver | Proposed — conditional | proposed-only | explicitly deferred decision-to-defer |
| ADR-091 | Stand-off Radar — 77 GHz / sub-THz Research | Proposed — research only | proposed-only | hardware-gated (COTS sub-THz); ITAR/dual-use |
| ADR-092 | nvsim Dashboard — Vite + Dual-Transport | Implemented (2026-04-27) | implemented | 4/12 gates need external infra; PR #436 open |
| ADR-093 | nvsim Dashboard Gap Analysis | Implemented (2026-04-27) | implemented | P2.7/P2.8 polish deferred |
| ADR-094 | Live 3D Point Cloud Viewer — GH Pages | Proposed (2026-04-29) | proposed-only | governs viewer deploy only, not crate data contract |
| ADR-095 | rvCSI — Edge RF Sensing Runtime Platform | Proposed | implemented | header stale — ADR-097 confirms built, published 0.3.1 |
| ADR-096 | rvCSI — Crate Topology, napi-c Shim, napi-rs | Proposed | implemented | header stale — 9 crates published 0.3.1 |
| ADR-097 | Adopt rvCSI as RuView's primary CSI runtime | Proposed | proposed-only | RuView vendors but does not yet consume — adoption open |
| ADR-098 | Evaluate ruvnet/midstream | Rejected (with carve-outs) | proposed-only | rejection; carve-outs revived by ADR-099 |
| ADR-099 | Adopt midstream — introspection + low-latency tap | Proposed | proposed-only | tension with ADR-098 (which rejected midstream) |
| ADR-100 | Cognitum Cog Packaging Specification | Accepted | implemented | first cog shipped 2026-05-19 (ADR-101) |
| ADR-101 | Pose Estimation Cog (WiFi-DensePose side) | Accepted — v0.0.1 shipped | implemented | hardware-gated; signed binaries on GCS |
| ADR-102 | Edge Module Registry Integration | Accepted | implemented | serves 105-cog catalog |
| ADR-103 | Learned Multi-Person Counter (cog-person-count) | Proposed | proposed-only | data/hardware-gated; claim gutted by ADR-159 |
| ADR-104 | RuView MCP Server + CLI Distribution | Accepted | partial | depends on Proposed ADR-103 for count tool |
| ADR-105 | Federated learning for RuView CSI personalization | Proposed | proposed-only | head of 105-108 chain, none implemented |
| ADR-106 | Differential privacy + biometric isolation | Proposed | proposed-only | extends Proposed 105 |
| ADR-107 | Cross-installation federation w/ secure aggregation | Proposed | proposed-only | classical DH later superseded by 108 |
| ADR-108 | Kyber PQ key exchange for federation | Proposed | proposed-only | extends Proposed 107 (parent unimplemented) |
| ADR-109 | Dilithium PQ signatures for cog distribution | Proposed | proposed-only | extends ADR-100; sister of 108 |
| ADR-110 | ESP32-C6 firmware extension (Wi-Fi 6 CSI, 802.15.4, TWT, LP) | Accepted — P1-P10 complete v0.7.0 | implemented | HE-CSI needs ESP-IDF ≥5.5 (v5.4 downconverts to HT) |
| ADR-113 | Multistatic anchor placement strategy | Proposed | proposed-only | amends ADR-029; simulation-derived not HW-validated |
| ADR-114 | cog-quantum-vitals | Proposed | proposed-only | hardware-gated (nvsim today, real NV-diamond in prod); R13 NEGATIVE |
| ADR-115 | Home Assistant via MQTT auto-discovery + Matter bridge | Accepted (MQTT) / Proposed (Matter) | partial | mixed status; Matter deferred to v0.7.1 |
| ADR-116 | HA + Matter as a Cognitum Seed cog (cog-ha-matter) | Proposed — P2 scaffold compiles | partial | provisional; Matter deferred to v0.8 |
| ADR-117 | pip wifi-densepose via PyO3 + maturin | Proposed | proposed-only | current PyPI v1.1.0 stale; tracking issue TBD |
| ADR-118 | BFLD — Beamforming Feedback Layer for Detection | Proposed | proposed-only | umbrella; sub-ADRs 119-123 |
| ADR-119 | BFLD Frame Format and Wire Protocol | Proposed | proposed-only | child of Proposed 118 |
| ADR-120 | BFLD Privacy Class and Hash Rotation | Proposed | proposed-only | child of Proposed 118 |
| ADR-121 | BFLD Identity Risk Scoring and Coherence Gate | Proposed | proposed-only | abandonment risk; data-gated (KIT BFId dataset) |
| ADR-122 | BFLD RuView Surface — HA/Matter/MQTT | Proposed | proposed-only | abandonment risk; depends on Soul Signature + cog-ha-matter |
| ADR-123 | BFLD Capture Path — Pi5/Nexmon, ESP32 feasibility | Proposed | proposed-only | hardware-gated (ESP32 cannot sniff CBFR) |
| ADR-124 | rvagent — MCP + ruvector npm lib (SENSE-BRIDGE) | Proposed | proposed-only | abandonment risk; not published; open questions |
| ADR-125 | RuView ↔ Apple Home native HAP bridge | Proposed | proposed-only | abandonment risk; hardware-gated (same-L2 pairing) |
| ADR-126 | HOMECORE — Rust+WASM+TS port of Home Assistant | Proposed | proposed-only | multi-quarter; series map cites missing 131/132 + mis-numbered 134 |
| ADR-127 | HOMECORE-CORE — state machine, registries, event bus | Proposed | proposed-only | future-dated Q3 2026 |
| ADR-128 | HOMECORE-PLUGINS — WASM integration plugin system | Proposed | proposed-only | future-dated; depends on 127 ABI freeze |
| ADR-129 | HOMECORE-AUTO — automation engine + template eval | Proposed | proposed-only | future-dated; broken cross-ref to ADR-134 |
| ADR-130 | HOMECORE-API — wire-compatible REST + WS | Proposed | proposed-only | future-dated; wire-compat needs HA companion-app suite |
| ADR-133 | HOMECORE-ASSIST — voice/intent + Ruflo bridge | Proposed | partial | missing tracking issue; P1 partial build, P2 deferred |
| ADR-134 | First-Class Channel Impulse Response (CIR) Support | Proposed | proposed-only | DUPLICATE IDENTITY (126/129 cite 134 as HOMECORE-MIGRATE); hardware-gated |
| ADR-135 | Empty-Room Baseline Calibration | Proposed | proposed-only | hardware-gated (COM9/COM12 + 802.15.4 sync) |
| ADR-136 | RuView Rust Streaming Engine — Architecture/Contracts | Proposed | partial | status-contradiction: §8 says Built (commit 11f89727f, 9 tests) |
| ADR-137 | Fusion Engine Quality Scoring | Proposed | partial | status-contradiction: Built (commit 4fa3847ac, 6 tests) |
| ADR-138 | WiFi-7 MLO LinkGroup + ArrayCoordinator gating | Proposed | partial | status-contradiction: Built (commit fc7674bde, 8 tests) |
| ADR-139 | WorldGraph — Environmental Digital Twin | Proposed | partial | status-contradiction: Built (commit 521a012d8, 7 tests) |
| ADR-140 | Semantic State Record + Ruflo Agent Bridge | Proposed | partial | status-contradiction: Built (commit 169a355bd, 4 tests); Rest kind not built |
| ADR-141 | BFLD Privacy Control Plane | Proposed | partial | header stale vs Implementation note (commit 7d88eb84c, 6 tests) |
| ADR-142 | Evolution Tracker + Temporal VoxelMap | Proposed | partial | header stale vs note (commit 1f8e180d6, 6 tests) |
| ADR-143 | RF SLAM v2 — Reflector Discovery + Anchor Learning | Proposed | partial | header stale (commit 2d4f3dea5); v2 dormant behind 7-day validation |
| 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-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 |
| ADR-153 | IEEE 802.11bf-2025 Forward-Compat Protocol Model | accepted | implemented | amends ADR-152 §2.4; OTA/silicon binding deferred |
| ADR-154 | Signal/DSP Beyond-SOTA Sweep — M0 | Proposed | partial | header likely stale; discloses dead CIR coherence gate; ~45 deferred |
| ADR-155 | NN/Training Beyond-SOTA Sweep — M1 | Proposed | partial | header likely stale; retracts synthetic-val/fake-gradient/self-cert proof |
| ADR-156 | RuVector/Cross-Viewpoint Fusion Sweep — M2 | Proposed | partial | header likely stale; one staged finding is numeric no-op |
| ADR-157 | Hardware/Sensing-Acquisition Sweep — M3 | Proposed | partial | header likely stale; headline negative result (layer already hardened) |
| ADR-158 | MAT/World-Model Cluster Sweep — Anti-AI-Slop | accepted | implemented | life-safety; fixes triage inflation; some paths DATA-GATED |
| ADR-159 | Cognitum Appliance Cluster Sweep — Anti-AI-Slop | accepted | implemented | person-count training_class1_accuracy = 0.343; description renamed |
| ADR-160 | Edge Skill Library (wasm-edge) — Honest Labeling | accepted | implemented | medical/affect/weapon NOT validated — relabelled |
| ADR-161 | HOMECORE Server — WS Auth Bypass, Reply-Theater | accepted | implemented | CRITICAL WS auth bypass fix; amends 130/129/128 |
| ADR-162 | HOMECORE Plugin Security + Bounded RunModes | accepted | implemented | security-critical; enforces ADR-161 deferrals |
| ADR-163 | Edge-Latency Measurement — CLAIMED→MEASURED | accepted | implemented | ESP32/Xtensa figure remains UNMEASURED (hardware-gated) |
+316
View File
@@ -0,0 +1,316 @@
# ADR Corpus Gap Analysis — Five-Lens Findings
Research notes backing ADR-164. Each lens output is reproduced verbatim. Census: 162 ADR entries across 156 distinct files (6 duplicate-number collisions verified on disk).
---
## 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.
### ADR Corpus — Status & Implementation Distribution
Census: **162 ADR entries** across **156 distinct files** (6 duplicate-number collisions verified on disk).
#### Status tally
| Status bucket | Count |
|---|---|
| Accepted (incl. "Accepted — partial/in-progress/Phase 1" variants) | ~56 |
| 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) |
| Mixed/dual status in one ADR | 3 (ADR-115, ADR-149-AetherArena vs swarm, ADR-133) |
#### impl_state tally
| impl_state | Count |
|---|---|
| implemented | ~36 |
| partial | ~50 |
| proposed-only | ~64 |
| stale-or-contradicted | 3 (ADR-029, 030, 031) |
| unknown | 5 (ADR-034, 044, 052-ddd, 147-proof, …) |
| 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-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)".**
#### 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 |
These break the ADR index and `/adr` tooling — two ADRs answering to one number is a corpus-integrity defect, not cosmetics.
#### SEVERITY: HIGH — Status header stale vs. shipped reality (Proposed header on landed code)
These are the most dangerous: an auditor reading the header concludes "not built" when code + tests exist. Ranked by blast radius:
1. **ADR-136 → ADR-145** (streaming-engine series, 10 ADRs) — every header says `Proposed` but each `§ Implementation Status` reports **"Built" with pinned commits + passing tests** (136: 11f89727f; 137: 4fa3847ac; 138: fc7674bde; 139: 521a012d8; 140: 169a355bd; 141: 7d88eb84c; 142: 1f8e180d6; 143: 2d4f3dea5; 144: b10bc2e9a; 145 referenced as landed by 149/150/151). **Bulk action: flip headers to "Accepted — partial (integration glue pending)".**
2. **ADR-029 / 030 / 031** (RuvSense/field-model/cross-viewpoint) — `Proposed` but repo has `signal/src/ruvsense/` (16 modules) and `ruvector/src/viewpoint/`, and **Accepted ADR-032 hardens them** — an Accepted ADR depending on Proposed parents (status-graph inversion).
3. **ADR-095 / 096** (rvCSI) — `Proposed` but ADR-097 confirms built, extracted to own repo, published 0.3.1 to crates.io/npm.
4. **ADR-152**`Proposed` but CLAUDE.md + recent commits report §2.12.3/2.6 implemented, WiFlow-STD MEASURED-EQUIVALENT ~96% PCK.
5. **ADR-154/155/156/157** (beyond-SOTA sweeps) — `Proposed` but each describes fixes **already landed with revert-verified regression tests**.
6. **ADR-024 (AETHER) / 027 (MERIDIAN) / 072 (WiFlow)**`Proposed` but CLAUDE.md lists them Accepted and code references them as implemented.
7. **ADR-017** — header Accepted but CLAUDE.md still calls it "Proposed" (inverse drift).
8. **ADR-018**`Proposed` but ADR-012 cites it as the working firmware/aggregator impl.
#### SEVERITY: HIGH — Status ahead of its dependencies (Accepted depends on Proposed)
- **ADR-032** Accepted → depends on Proposed 029/030/031.
- **ADR-053** Accepted → depends on Proposed ADR-052.
- **ADR-048** Accepted → depends on Proposed ADR-045.
- **ADR-077** Accepted → depends on Proposed ADR-075/076.
#### SEVERITY: MEDIUM — Proposed-but-looks-abandoned (decisions that will likely never land)
Cluster heads where the whole chain is Proposed with zero implementation evidence:
- **ADR-003/007/008/009/010** — RuVector child ADRs orphaned after parent ADR-002 was superseded by 016/017.
- **ADR-105/106/107/108** — entire federation chain, none implemented.
- **ADR-118/119/120/121/122/123** — entire BFLD chain, all ACs unchecked, tracking issues TBD.
- **ADR-124/125/126/127/128/129/130/133** — HOMECORE/bridge chain, multi-quarter future-dated, all TBD.
- **ADR-033** (remote-viewing), **ADR-042** (CHCI, superseded-in-intent by 153), **ADR-046** (Android TV), **ADR-049** (Python v1 legacy), **ADR-067** (RuVector v2.0.5 upgrade not adopted), **ADR-074** (SNN), **ADR-085** (RaBitQ expansion), **ADR-011** (Proposed-URGENT despite proof pipeline live).
#### SEVERITY: MEDIUM — Accepted but no implementation evidence (unverified "done")
- **ADR-034** (FieldView mobile app) — Accepted, no crate/dir in CLAUDE.md.
- **ADR-044** (wifi-densepose-geo) — bare Accepted, no Date/Deciders, crate not in CLAUDE.md table.
#### 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.
2. **Bulk-flip the 10 streaming-engine headers (ADR-136145)** 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).
5. **Triage the abandoned chains** (003/007010, 105108, 118123, HOMECORE 124133) — either mark `Rejected`/`Deferred` or open tracking issues; leaving ~50 Proposed-forever ADRs pollutes every gap analysis.
6. **Verify or downgrade unsubstantiated Accepteds** (034, 044).
**Bottom line:** the corpus's #1 defect is not abandoned work — it's **bookkeeping drift**. ~24 ADRs are mislabeled Proposed while code+tests exist, 6 numbers collide, and 3 files lack Status headers entirely. The status field cannot currently be trusted as a source of truth for what is built.
---
## Lens 2: supersession-chains
I have all evidence needed. ADR-016 doesn't mention ADR-002 at all (only 014/015); ADR-017 discusses ADR-002 as a "correction" but never uses the word "supersede." This confirms the broken reciprocity. Final analysis below.
### Supersession & Lifecycle Integrity — ADR Corpus
Total declared supersession edges in census: **3** (ADR-002→016, ADR-002→017, ADR-052-tauri→054). All three were file-verified. Findings below; severity graded **CRITICAL / HIGH / MEDIUM / LOW**.
#### Reconstructed chains
- **ADR-002** (RuVector RVF Integration Strategy) → superseded-by **ADR-016 + ADR-017** (dual realization). Self-declared `supersedes` on 016/017.
- **ADR-052-tauri** (Tauri Desktop Frontend) → superseded-by **ADR-054** (declared in 052's `superseded_by`).
- No other formal `supersedes`/`superseded_by` links exist. No cycles detected (the only multi-node graph, ADR-002→{016,017}, is a DAG; ADR-052→054 is a single edge). **No cycles — clean.**
#### Broken / asymmetric links
**1. ADR-002 → ADR-016 / ADR-017: one-directional, never reciprocated. (HIGH)**
ADR-002 header declares "Superseded by [ADR-016] and [ADR-017]" (`docs/adr/ADR-002-ruvector-rvf-integration-strategy.md:4`). But neither successor claims it:
- **ADR-016** (`ADR-016-ruvector-integration.md`) never mentions ADR-002 anywhere — its `## References` lists only ADR-014/015. It does not assert supersession; the census `supersedes:["ADR-002"]` for ADR-016 is **unsupported by the file**.
- **ADR-017** (`ADR-017-ruvector-signal-mat-integration.md`) discusses ADR-002 only as a `## Correction to ADR-002 Dependency Strategy` (line 532) — corrects "fictional crate names" — but **never uses the word "supersede."** Census `supersedes:["ADR-002"]` is again file-unsupported.
- Net: ADR-002 points up at two ADRs that don't point back. The supersession is asserted by the superseded ADR alone — backwards from convention, and unverifiable from the successors.
**2. ADR-002 partial-supersession leaves 5 orphaned children stranded. (HIGH)**
ADR-002 is an umbrella whose children ADR-003, 007, 008, 009, 010 are still `Proposed`. ADR-016/017 only realize the *training/signal/MAT* integration points (mincut, attention, solver, etc.). The RVF-container (003), PQ-crypto (007), Raft consensus (008), WASM edge runtime (009), and witness-chains (010) decisions are **neither implemented nor formally superseded** — ADR-017:555 explicitly acknowledges 008/009 "described in ADR-002" are not carried forward. Marking the parent fully "Superseded" silently buries 5 live-but-abandoned child decisions. ADR-010's role is additionally filled de facto by ADR-028's witness-bundle without any supersession link.
**3. ADR-052-tauri → ADR-054: declared by predecessor, not acknowledged by successor. (HIGH)**
Census records ADR-052-tauri `superseded_by:["ADR-054"]`. **ADR-054 (`ADR-054-desktop-full-implementation.md`) contains zero references to ADR-052** (grep for `ADR-052|replac|supersed` returns nothing). ADR-054 is titled "RuView Desktop **Full Implementation**" and is "in progress" — functionally it's the implementation plan *for* 052, not a replacement. The supersession edge is unconfirmed by the successor and arguably mis-modeled (an in-progress impl doesn't supersede its own design ADR).
#### Orphaned superseded ADRs still marked accepted/active
**4. No classic orphan (superseded ADR still `Accepted`), but two soft variants: (MEDIUM)**
- **ADR-052-tauri** is `Proposed` *and* `superseded_by ADR-054`, yet downstream ADR-053/055/056 (all `Accepted`) build on it and treat the desktop app as shipped (v0.3.0). A Proposed-and-superseded ADR anchoring three Accepted descendants is a lifecycle inconsistency: the live decision-of-record is ambiguous (052? 054? 056?).
- **ADR-002** is correctly `Superseded`, so not an orphan — but ADR-038's roadmap census still counts it among 37 active ADRs, so stale references persist downstream.
#### De-facto supersessions never recorded (missing links) — MEDIUM
These pairs behave as supersession in the corpus but carry **no** `supersedes`/`superseded_by` fields, so the chain graph understates reality:
- **ADR-098 ⇄ ADR-099** (`MEDIUM`): ADR-098 **Rejected** midstream; ADR-099 revives its carve-outs ("Adopt midstream…"). A rejection partially reversed by a later Proposed ADR — neither links the other via supersession fields (only prose tension).
- **ADR-063 → ADR-064**, **ADR-035 → ADR-023/036**, **ADR-042 → ADR-153**, **ADR-050-provisioning → ADR-060**, **ADR-117 retracts PyPI v1.1.0**, PQ chain **ADR-100 → ADR-109 / ADR-107→108**: each is "superseded in scope/intent" per flags but has empty supersession fields. Not broken links (none falsely asserted), but **missing** ones — the lifecycle graph is incomplete.
#### Cycles
**None.** ADR-002→{016,017} and ADR-052→054 are acyclic; no ADR is reachable from its own successor.
#### Severity summary
| # | Issue | Severity |
|---|-------|----------|
| 1 | ADR-002 supersession not reciprocated by ADR-016/017 (016 never mentions 002; 017 only "corrects") | HIGH |
| 2 | ADR-002 children 003/007/008/009/010 stranded under a "fully superseded" parent | HIGH |
| 3 | ADR-052→054 edge unacknowledged by ADR-054 (no reference); likely mis-modeled (impl, not replacement) | HIGH |
| 4 | ADR-052-tauri Proposed+superseded yet anchors Accepted 053/055/056 | MEDIUM |
| 5 | De-facto supersessions (098/099, 063/064, 042/153, 050/060, 035/023, 100/109) unrecorded | MEDIUM |
| — | Cycles | none (clean) |
Highest-priority fix: add reciprocal `supersedes: ADR-002` language to ADR-016/017 (or downgrade the claim), and split ADR-002's status to "partially superseded" so children 003/007/008/009/010 aren't silently closed.
---
## Lens 3: contradictions
All findings confirmed against source. Here is the contradictions lens.
### Contradictions & Retractions Lens — ADR Corpus
The sweep (ADR-154163) is itself a structured retraction layer: each "Beyond-SOTA / anti-AI-slop" ADR exists *because* an earlier accepted claim was found false. Findings graded **CRITICAL** (life-safety, security, or a published accuracy number that was meaningless) / **HIGH** (a capability/number retracted or directly contradicted) / **MEDIUM** (status or scope conflict) / **LOW** (cosmetic/doc drift).
#### A. Accepted/published claims later RETRACTED or REFUTED
**[CRITICAL] ADR-155 retracts every prior NN accuracy/TTA/proof claim.** ADR-155 §2.2 discloses `bin/train.rs` validated a *real* MM-Fi training run against a **synthetic** val set, and windows leak at stride-1 (~99% overlap) — *"any PCK it printed was meaningless on two counts."* §2.3: `rapid_adapt.rs` `contrastive_step`/`entropy_step` wrote a **fake gradient** (`grad += v * 0.01`) unrelated to the objective — every "TTA improves the metric" result was unsupported. §2.4: the deterministic proof **self-certified** (`generate_expected_hash` blessed whatever the pipeline emitted; PASS counted any loss decrease incl. 1e-9 float noise; missing hash defaulted to PASS). This retroactively voids accuracy claims made anywhere in the corpus that depended on the training/proof path prior to commit landing ADR-155.
**[CRITICAL] ADR-154 retracts the ADR-134 CIR coherence gate as live.** ADR-152/CLAUDE.md present CIR (ADR-134) as a contributing signal in the multistatic coherence gate. ADR-154 §2 proves it was **DEAD in production for every canonical frame**: the HT20 CIR estimator returns `SubcarrierMismatch` on all 56-tone canonical frames (`cir_gate_ht20_is_dead_on_canonical56`: 0 Ok / 8 mismatch), so `coherence = 0.7·freq + 0.3·dominant_tap_ratio` silently degraded to freq-only (`cir_gate_dead_ht20_equals_gate_off`, |Δ|<1e-9). Any ADR claiming CIR-enhanced coherence/ToF before this fix overstated reality.
**[CRITICAL] ADR-079 internal accuracy contradiction (self-flagged in census, confirmed).** Context states proxy PCK@20 = **2.5%** (lines 11, 25) and "10-20x improvement: 2.5% → 35%+". The baseline table (line 497) reports proxy PCK@20 = **35.3%** — i.e. the *baseline already equals the stated target* — while per-joint upper body (nose/shoulders/wrists) is **0%** (line 503). The headline 1020x improvement number is therefore self-refuting against its own baseline table. CLAUDE.local.md adds the local-Windows attempt (#640) measured **0% PCK**. An Accepted ADR with three mutually inconsistent values for its own central metric.
**[HIGH] ADR-152 self-refutes one verified research claim (F4).** ADR-152 grades 25 claims 3-vote; §F4 records the "Espressif `esp_wifi_sensing` is **drop-in compatible with RuView nodes**" claim **REFUTED 0-3** (WiFi-6 parts use a different CSI acquisition config struct). ADR-110 ("ESP32-C6 Wi-Fi 6 CSI") and the CLAUDE.md hardware table treat C6/Wi-Fi-6 CSI as a smooth extension; ADR-152 also notes HE-CSI needs ESP-IDF ≥5.5 (v5.4 silently downconverts to HT). The "WiFlow-STD MEASURED-EQUIVALENT ~96% PCK@20" line in CLAUDE.md is *not* yet supported: §2.2/§F1 mark external pose numbers (incl. the 97.25% WiFlow-STD figure) **CLAIMED**, and §F1 explicitly forbids citing 97.25% as comparable until measurements (a)(c) are run. CLAUDE.md asserting "MEASURED-EQUIVALENT" contradicts the ADR's own gating.
**[HIGH] ADR-150 retracts the implied cross-subject capability of the encoder line.** AETHER/MERIDIAN ADRs (024/027) and the foundation-encoder framing imply subject-invariant embeddings work. ADR-150 measures **81.63% in-domain vs ~11.6% leakage-free cross-subject** torso-PCK, and reports DANN **failed** (27.26%→27.54%, empirically ~0 gain) and bigger capacity *hurt* (transformer 24.8% < conv 27.3%). §1.1/§4 conclude the cross-subject acceptance gate "is **unlikely to be met without new multi-subject** data" — a direct retraction of the "more capacity / adversarial alignment solves cross-environment loss" premise underlying ADR-027.
**[HIGH] ADR-159 refutes the "never identified anyone" accusation but simultaneously retracts cog-person-count's marketing.** ADR-159 ships real SHA-pinned Candle models, but discloses person-count `training_class1_accuracy = 0.343` (presence-only, classes 0/1), and **renames** the Cargo description from "learned multi-person counter" → "presence detector + (data-gated) person count," clamping/`low_confidence`-flagging multi-occupant counts. This retracts ADR-103's "learned multi-person counter (SOTA WiFi CSI counting)" claim and ADR-104's count tool, which depended on it.
**[HIGH] ADR-161 retracts HOMECORE server security + functionality claims.** ADR-130 (HOMECORE-API, wire-compatible, Ed25519-JWT) implied a secured server. ADR-161 fixes a **CRITICAL WebSocket auth bypass** (any non-empty token accepted), "reply-theater" (WS responses computed then discarded), and documented-but-no-op automation — then ADR-162 enforces the ADR-161 deferrals (plugin Ed25519 sig verification, capability isolation, bounded RunModes that were "parsed-but-unenforced/unbounded-parallel"), retracting ADR-128/129's implied plugin-signing and automation guarantees.
**[MEDIUM] ADR-163 converts CLAIMED latency budgets to MEASURED — retracting prior budget citations.** ADR-160/159 cited wasm-edge/cog latency *budgets*. ADR-163 adds host benches and explicitly states the **ESP32/Xtensa-on-hardware figure remains UNMEASURED** — so any doc citing the device latency budget as achieved is unsupported.
**[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.
#### B. Pairs making CONFLICTING decisions on the same topic
**[HIGH] RVF-WASM edge runtime — ADR-009 vs shipped `wifi-densepose-wasm`.** ADR-009 (Proposed) decides to **replace** the existing wifi-densepose-wasm approach with an `.rvf.edge` container runtime. The crate it proposes to replace is shipped and in the CLAUDE.md crate table (and is the dependency base for ADR-058/059 browser pose). ADR-009 is an unrealized decision directly contradicting shipped architecture.
**[HIGH] Witness/audit mechanism — ADR-010 vs ADR-028.** ADR-010 (Proposed) decides RuVector witness *chains* as "the primary tamper-evident audit mechanism." ADR-028 (Accepted, implemented) established a different **witness-bundle** mechanism (verify.py / SHA-256 / VERIFY.sh) that fills this role. Two competing "primary audit" decisions; ADR-010 is stranded.
**[HIGH] Multistatic "sensing-first RF mode" — ADR-029 vs ADR-031 near-duplicate scope.** Both decide a "sensing-first RF mode for multistatic fidelity": ADR-029 (RuvSense, signal/src/ruvsense/) and ADR-031 (RuView cross-viewpoint fusion, ruvector/src/viewpoint/). Overlapping problem statements (occlusion/depth/multi-person via multistatic attention+geometry), separate crate homes, both still nominally "Proposed" while both are implemented. Unreconciled dual ownership of the multistatic-fusion decision.
**[MEDIUM] Person-counting decision conflict — ADR-037 vs ADR-075 vs ADR-103.** Three different decisions to replace the same fixed-threshold counter: ADR-037 (4-phase neural decomposition), ADR-075 (spectral min-cut over subcarrier-correlation graph, fixes #348), ADR-103 (learned Cog `cog-person-count`). ADR-075's bug (#348) overlaps ADR-069's driver. None supersedes the others; ADR-159 then guts ADR-103's claim (above).
**[MEDIUM] PQ-crypto signing — ADR-007 vs ADR-109.** ADR-007 (Proposed) decides Ed25519 + ML-DSA-65 hybrid for sensing-data signing; ADR-109 (Proposed) decides Ed25519 + **Dilithium-3** hybrid for cog signing (Dilithium = ML-DSA family but a different parameter pick/scope). Two PQ-signature decisions over adjacent surfaces with non-identical algorithm choices, neither reconciled.
**[MEDIUM] Federation key-exchange self-supersession — ADR-107 vs ADR-108.** ADR-107 adopts classical Diffie-Hellman in secure-aggregation Layer 4; ADR-108 replaces it with Kyber-768 because the DH choice is "quantum-vulnerable." ADR-108 supersedes a core element of ADR-107 while ADR-107 is still only Proposed — a decision corrected before it was ever accepted.
**[MEDIUM] Provisioning path forked three ways — ADR-050(prov) vs ADR-060 vs ADR-052/054.** ADR-050 (provisioning-tool-enhancements, Proposed) scopes channel+MAC-filter flags; ADR-060 (Accepted) actually implements them; ADR-052/054 move provisioning into a Rust-native Tauri desktop path. Three live decisions for "how RuView provisions nodes," with ADR-060 partially fulfilling ADR-050 without superseding it.
#### C. Status-graph contradictions (Accepted depending on / contradicting Proposed)
**[MEDIUM] Accepted ADRs hardening/depending on Proposed ones.** ADR-032 (Accepted, security hardening) hardens ADR-029/030/031 which remain "Proposed" — an accepted decision presupposing un-accepted ones exist. Same pattern: ADR-048 (Accepted) depends on ADR-045 (Proposed); ADR-053 (Accepted) depends on ADR-052 (Proposed); ADR-077 (Accepted) depends on ADR-075/076 (Proposed); ADR-104 (Accepted) depends on ADR-103 (Proposed). These are status contradictions, not capability retractions, but they signal the same "header lags reality" hygiene problem the sweep is correcting.
**[LOW] Header-stale-vs-implementation (pervasive).** ADR-029/030/031, 072, 095/096, 136145, 150, 152, 154157 all carry `Status: Proposed` while their own appended Implementation-Status notes (or downstream ADRs / CLAUDE.md) report them built+tested with commits. ADR-024/027 say Proposed; CLAUDE.md lists them Accepted; pose_tracker.rs already uses AETHER re-ID. Cosmetic but corpus-wide; it is the mechanism by which retracted/overstated claims survive (a green "built" note under a "Proposed" header is exactly where ADR-155's self-certifying proof hid).
#### Cited source files (absolute)
- C:\Users\ruv\Projects\wifi-densepose\docs\adr\ADR-079-camera-ground-truth-training.md (lines 11/25/497/503 — 2.5% vs 35.3% vs 0%)
- C:\Users\ruv\Projects\wifi-densepose\docs\adr\ADR-150-rf-foundation-encoder.md (81.63% vs 11.6%; DANN ~0)
- C:\Users\ruv\Projects\wifi-densepose\docs\adr\ADR-152-wifi-pose-sota-2026-intake.md (F4 REFUTED 0-3; 97.25% CLAIMED-not-MEASURED)
- C:\Users\ruv\Projects\wifi-densepose\docs\adr\ADR-154-signal-dsp-beyond-sota.md (§2 dead CIR gate)
- C:\Users\ruv\Projects\wifi-densepose\docs\adr\ADR-155-nn-training-beyond-sota.md (§2.22.4 synthetic-val / fake gradient / self-certifying proof)
- C:\Users\ruv\Projects\wifi-densepose\docs\adr\ADR-159-cognitum-appliance-beyond-sota.md (person-count 0.343; description renamed)
**Top-severity summary:** the four CRITICAL items (ADR-155 fake-gradient+synthetic-val+self-certifying proof; ADR-154 dead CIR gate; ADR-079 self-inconsistent PCK; ADR-161 WS auth bypass) are the corpus's load-bearing "AI-slop" admissions — each is an *accepted-or-shipped* surface whose stated accuracy/security/function was provably false until the sweep landed. Every accuracy number predating ADR-155 in any other ADR should be treated as CLAIMED, not MEASURED, until re-derived through the post-155 leak-free split.
---
## Lens 4: coverage-gaps
Confirmed — ADR-094 governs the pointcloud *viewer deployment* (proposed-only), not the crate's sensing-data-production contract. I have all evidence needed.
### Coverage Gaps — Crates/Capabilities vs Governing ADRs
Severity: **CRITICAL** (shipped code with no/broken governing ADR), **HIGH** (architect would expect an ADR, none exists), **MEDIUM** (governed only by a remediation/deploy ADR, no creation/architecture ADR), **LOW** (minor).
#### A. Shipped crates whose cited ADR does not exist (CRITICAL)
Two crates are built and in-tree but reference ADR numbers that point to *different* on-disk ADRs or to files that never existed (confirmed: no `ADR-131*.md` or `ADR-132*.md` exists; `ADR-134` on disk is CIR, not HOMECORE-MIGRATE):
- **`v2/crates/homecore-recorder`** — Cargo.toml header: *"SQLite state history + semantic search (ADR-132)"*. **No ADR-132 exists.** The HOMECORE series map (ADR-126 §4) lists ADR-132 HOMECORE-RECORDER as planned, but it was never written. A shipped persistence/history crate has zero governing decision record. **CRITICAL** — this is the recorder, the durable-state surface, ungoverned.
- **`v2/crates/homecore-migrate`** — Cargo.toml header: *"Implements ADR-134 (HOMECORE-MIGRATE)"*. **On-disk ADR-134 is "First-Class CIR Support"** (census + glob confirm). ADR-129/126 also cite ADR-134 as HOMECORE-MIGRATE. The crate implements a migration tool from Python HA reading `.storage/*.json` — a data-integrity-sensitive importer — governed by a phantom ADR identity. **CRITICAL** (compounds the documented ADR-134 duplicate-number collision).
These are not stale-header issues like the ADR-136..146 cluster (where the ADR exists and is just marked Proposed); here the cited governing ADR **is absent or is a different decision**.
#### B. Shipped crates with NO governing ADR at all (HIGH)
- **`v2/crates/wifi-densepose-engine`** — *"streaming-engine integration layer — composes the ADR-135..146 building blocks into one trust-traceable pipeline cycle."* It composes ~12 ADRs' outputs into the live pipeline-cycle aggregate, but **no ADR governs the composition/orchestration contract itself** (ordering, back-pressure, the "one pipeline cycle" boundary). ADR-136 defines frame contracts/stages but not the integrator crate. An architect would expect an ADR for the seam that wires 135146 onto the live 20 Hz path — exactly the "integration glue not yet on live path" caveat repeated across ADR-136..146. **HIGH.**
#### C. Capabilities governed only by a remediation/deploy ADR — no creation/architecture ADR (MEDIUM)
- **`v2/crates/wifi-densepose-wasm-edge` (~70 edge skills)** — The only ADRs touching it are **ADR-160** (honest *relabeling*/soundness cleanup) and **ADR-163** (latency *measurement*). Both are anti-slop remediation ADRs that presuppose ~70 skills already shipped. There is **no creation/architecture ADR** defining the skill taxonomy, ABI, event-ID allocation, or budget tiers for this crate. (Contrast ADR-041, which *does* catalog the 60-module registry — but for the ESP32/WASM3 on-device path of ADR-040, a different artifact.) A whole ~70-module crate's design rationale lives nowhere. **MEDIUM-HIGH.**
- **`v2/crates/wifi-densepose-occworld-candle`** — *"OccWorld TransVQVAE inference ported to Candle (Rust-native, no Python IPC)."* ADR-147 (OccWorld) decided a **Python-subprocess** thin client and explicitly deferred a Rust backend swap to "Phase B / RoboOccWorld." A native Candle reimplementation is a material architecture change (new dep surface, no IPC, weight-loading path) that **no ADR records the decision to build now**. **MEDIUM.**
- **`v2/crates/wifi-densepose-pointcloud`** — ADR-094 governs only the *GitHub-Pages viewer deployment* (Proposed). The crate as a **point-cloud data-production/format contract** (what it emits, schema, real-data-stream toggle wiring) has no governing decision beyond the demo-deploy doc. **MEDIUM.**
- **`v2/crates/homecore-hap`** — header cites ADR-125 P1 scaffold; ADR-125 (Apple Home HAP bridge) exists and covers it. **Governed — no gap.** (Listed to scope out the false positive.)
- **`v2/crates/wifi-densepose-geo`** — governed by ADR-044 (geospatial). Governed, but ADR-044 is a bare "Accepted" with no implementation evidence and is cross-referenced incorrectly by ADR-052 (cites ADR-044 for provisioning). **LOW** (governed but the ADR itself is thin).
#### D. Decision areas an architect would expect an ADR for, but none exists (HIGH)
1. **Persistence/storage strategy for HOMECORE state history**`homecore-recorder` ships SQLite with an "HA-compat schema," but no ADR decides SQLite-vs-alternatives, retention, or the semantic-search index. Recorder is the durability backbone; an unrecorded storage choice is a classic missing-ADR. **HIGH** (ties to gap A).
2. **Python-HA → HOMECORE migration/import contract**`homecore-migrate` reads foreign `.storage` JSON (untrusted input, schema-drift risk) with no governing ADR (the cited one is CIR). Migration correctness and trust boundary are exactly what an ADR should pin. **HIGH** (ties to gap A).
3. **The streaming-engine *integrator* contract** (`wifi-densepose-engine`) — see B. **HIGH.**
4. **Cross-crate workspace dependency/publishing ADR** — CLAUDE.md lists a hand-maintained 12-step publishing order and a 15-crate table, but the workspace now has **38 crates** (glob count) including ungoverned ones (engine, worldmodel, worldgraph, occworld-candle, geo, wasm-edge, homecore-*, cog-*, ruview-swarm, pointcloud, nvsim-server, desktop). No ADR governs crate-graph topology / publish boundaries at this scale — the publishing list in CLAUDE.md is already stale against reality. **MEDIUM-HIGH.**
5. **No ADR ties the streaming-engine (`engine`) to the cog/appliance deploy surface** — ADR-101/102/159 govern cogs; ADR-136..146 govern the engine; nothing decides how the trust-traceable engine output becomes a deployed cog. The seam between the two largest subsystems is ungoverned. **MEDIUM.**
#### E. Scoped-out false positives (verified governed)
- `wifi-densepose-worldmodel` → ADR-147 (OccWorld bridge). Governed.
- `wifi-densepose-worldgraph` → ADR-139. Governed.
- `cog-ha-matter` → ADR-116; `cog-person-count` → ADR-103; `cog-pose-estimation` → ADR-101. Governed.
- `ruview-swarm` → ADR-148. `nvsim`/`nvsim-server` → ADR-089/092. `wifi-densepose-bfld` → ADR-118123/141. `wifi-densepose-calibration` → ADR-151. All governed.
- `wifi-densepose-desktop` → ADR-052/054 (contested status, but an ADR exists). Not a coverage gap (it's a status-drift issue, out of this lens's scope).
#### Top-priority remediation (concrete)
1. Write **ADR-132 HOMECORE-RECORDER** (or renumber) so the shipped `homecore-recorder` SQLite/history crate has a real governing decision — **CRITICAL**.
2. Resolve the **ADR-134 identity collision** and create a real **HOMECORE-MIGRATE** ADR for `homecore-migrate` (untrusted-import trust boundary) — **CRITICAL**.
3. Write a **streaming-engine integrator ADR** for `wifi-densepose-engine` (the 135146 composition contract / live-path seam) — **HIGH**.
4. Write a **creation/architecture ADR for `wifi-densepose-wasm-edge`** (taxonomy/ABI/event-IDs) — currently only remediation ADRs (160/163) exist — **MEDIUM-HIGH**.
5. Add an ADR for the **`occworld-candle` Rust-native backend swap** (ADR-147 only blessed the Python-subprocess path) — **MEDIUM**.
Evidence files: `v2/crates/homecore-recorder/Cargo.toml`, `v2/crates/homecore-migrate/Cargo.toml`, `v2/crates/wifi-densepose-engine/Cargo.toml`, `v2/crates/wifi-densepose-occworld-candle/Cargo.toml`, `docs/adr/ADR-134-csi-to-cir-time-domain-multipath.md` (CIR, not MIGRATE), `docs/adr/ADR-160-edge-skill-library-honest-labeling.md`, `docs/adr/ADR-094-pointcloud-github-pages-deployment.md`; absence confirmed via glob: no `ADR-131*`, no `ADR-132*`.
---
## Lens 5: data-hardware-gated
Evidence confirmed. I have enough to write the section.
### Open / Gated Decisions — The Real Backlog (data/hardware-gated, in-progress, or unresolved)
Severity = how load-bearing the unresolved decision is to a shipped claim. Sweep coverage = whether ADR-154163 touched it.
#### CRITICAL — life-safety or shipped-claim surface, still gated
**ADR-079 — Camera Ground-Truth Training Pipeline.** *Accepted, but core decision unvalidated.* P7P9 (real paired-data collection, training, cross-room LoRA) are **Pending** (file lines 476478). Blocker: a real synchronized camera+ESP32 paired-capture session and GPU training run — neither done. The ADR's own baseline table is self-contradictory: text says proxy PCK@20=2.5% (lines 11, 25) yet line 497 reports 35.3% (the *target*) with line 503 confessing **upper-body joints at 0%** — the proxy has no real spatial signal. CLAUDE.local.md records the local-Windows attempt (#640) at 0% PCK. The fleet (ruvultra RTX 5080, cognitum-seed-1) is the unblock, but the decision is accepted-on-paper, not proven. **Sweep: NOT addressed** — 154163 never touch the camera-teacher path. Real open backlog item.
**ADR-158 — MAT/World-Model sweep (life-safety).** *Accepted/implemented for the correctness fixes, but capability remains DATA-GATED.* The sweep honestly fixed the dangerous bugs (unified the two divergent triage engines so survivor count can't inflate from repeat detection — lines 4656, 184186), but explicitly grades the actual capabilities as unproven: **RF-through-rubble survivor detection = DATA-GATED** (needs instrumented rubble trials, line 37); **learned multi-person counter = DATA-GATED** on labelled multi-occupant CSI (lines 41, 173); PicoScenes/Intel-5300/Atheros live capture DATA-GATED on NIC/driver hardware (lines 177179). **Sweep: addressed the slop, honestly deferred the capability.** This is the model the rest should follow — code is real, accuracy claim is withheld pending absent hardware. Severity CRITICAL because it is the life-safety surface; the residual gate is acceptable and labeled.
#### HIGH — shipped/benchmarked claim with an explicit residual gate
**ADR-152 — WiFi-Pose SOTA 2026 Intake.** Status header stale (says Proposed; commits + line 58 report §2.12.3/2.6 implemented and WiFlow-STD **MEASURED-EQUIVALENT 96.09% PCK@20** on RTX 5080). Residual gates are real and disclosed: (1) **1 of 25 verified claims REFUTED 0-3** — "ESP WiFi-6 drop-in compatible with RuView nodes" is false (WiFi-6 parts use a different CSI acquisition struct, lines 31, 123); (2) external pose numbers (PerceptAlign 60% cross-domain; UNSW MAE pose transfer) remain **CLAIMED until reproduced on our hardware** (lines 21, 27, 119122); (3) measurement (b)/(c) open — line 111 confirms pretrained init gives optimization transfer but **no feature transfer**, and no run beat a mean-pose baseline on single-subject data, so **no CSI→pose capability is citable** until multi-subject/multi-position data exists. Blocker: heterogeneous multi-subject CSI dataset (data-gated, per ADR-150 §F3). **Sweep: this ADR *is* the prove-everything discipline applied to research intake** — gates labeled, not buried.
**ADR-072 / ADR-150 — WiFlow pose + RF foundation encoder.** ADR-072 >80% PCK@20 target unverifiable without camera labels (resolved-path via ADR-079, itself gated above). ADR-150 cites measured 81.63% in-domain vs **~11.6% leakage-free cross-subject** — the cross-subject collapse is real and the stated lever (ADR-152 F3) is *more heterogeneous data*, not capacity. Blocker: multi-subject/room dataset + libtorch GPU training. **Sweep: NOT directly addressed** (155 fixed PCK/OKS metric-integrity plumbing, which makes these numbers *trustworthy* but doesn't close the data gap).
#### HIGH — security/privacy decisions still Proposed-only (no sweep touched the gate itself)
**ADR-080 — QE Remediation.** Tracks unfixed security HIGH findings (X-Forwarded-For bypass, leaked stack traces, JWT-in-URL CWE-598), gate FAILED, status Proposed, no done-marking. The HOMECORE sweep (ADR-161/162) fixed *HOMECORE*'s WS-auth bypass and plugin signing — a **different** server boundary. **Sweep: did NOT cover ADR-080's sensing-server findings.** Genuine open security backlog.
**ADR-105→109, ADR-118125 (BFLD/federation/fabric chains).** Entire federation chain (105109) and BFLD surface (118125) are Proposed-only, all ACs unchecked, several "tracking issue TBD." Blockers: KIT BFId dataset (ADR-121 calibration), Pi5/Nexmon CBFR capture hardware (ADR-123 — ESP32 *structurally cannot* sniff CBFR), Soul-Signature + cog-ha-matter dependencies (ADR-122/125). **Sweep: NOT addressed** — 154163 stop at HOMECORE/MAT/cog/edge; the privacy control *plane* (ADR-141, built) exists but the BFLD *capture/scoring* chain it would gate does not. Backlog, honestly gated by absent hardware.
#### MEDIUM — hardware-gated, honestly deferred BY the sweep (lowest risk)
**ADR-163 — Edge-latency measurement.** *Accepted/implemented* for host benches, but the **ESP32/Xtensa on-hardware `process_frame` figure is explicitly UNMEASURED / PENDING (hardware)** (lines 3132, 7983, 9293). Blocker: `wasm32-unknown-unknown` built + flashed to ESP32-S3 and timed on-device; host x86_64 median is "an upper bound on algorithm work, not the ESP32 number." This is the **gold-standard deferral**: the gate is stated everywhere, no claim overreaches. **Sweep: this *is* a sweep ADR honestly deferring its own residual.**
**ADR-160 — wasm-edge skill labeling.** Medical/affect/weapon capabilities explicitly **NOT validated** — relabelled/disclaimed/feature-gated rather than implemented, reference-standard-gated. **Sweep: addressed by relabeling, capability honestly deferred.**
**ADR-110 — ESP32-C6 firmware.** Implemented, but HE-CSI requires ESP-IDF ≥5.5 (v5.4 silently downconverts to HT) — capability hardware/toolchain-gated per WITNESS §B1. Not a sweep target; gate is a noted hardware constraint, not slop.
**Other purely hardware/data-gated Proposed decisions (no sweep involvement, no overreach):** ADR-023 (paired data+GPU), ADR-027/MERIDIAN (multi-env data), ADR-042 CHCI (custom PCB/TCXO — largely superseded by 153), ADR-063/064 (ESP32-C6+MR60BHA2 mmWave), ADR-065/066 (live Cognitum Seed deploy), ADR-070 (live 2-node+Seed capture), ADR-073/078 (multi-AP mesh deployment), ADR-083 (pending field evidence), ADR-086 (real-deployment suppression rates), ADR-091 (COTS sub-THz + ITAR-clear use case), ADR-103 (labelled count data), ADR-113 (Fresnel-sim, not hardware-validated), ADR-114 (real NV-diamond device), ADR-134/135 (COM9/COM12 hardware-test feature), ADR-143 v2 (7-day fleet validation campaign, dead-code until then), ADR-144 (no UWB radio in fleet).
#### Cross-cutting finding
The sweep (ADR-154163) is **narrowly scoped**: it hardened MAT (158), Cognitum cogs (159), wasm-edge (160), HOMECORE server+plugins (161/162), and latency debt (163) — converting CLAIMED→MEASURED or DATA-GATED with honest labels. It **did not** touch the two largest *capability* gaps: the **camera-teacher training validation (ADR-079/072/150)** and the **federation/BFLD privacy chains (105109, 118125)** — both remain data/hardware-gated and Proposed-only. The single hard contradiction worth flagging to a human: **ADR-079's baseline table reports the target (35.3%) as if achieved while the prose and #640 evidence say 2.5%/0%** — that is the one place a reader could mistake an aspiration for a measurement.
+1
View File
@@ -131,6 +131,7 @@ else
SKIP "named person-identity — DATA-GATED: needs a real enrollment feeding the AETHER/body-resonance channel (see docs/research/soul/)"
SKIP "OccWorld trained accuracy — needs a trained checkpoint (predict() carries weights_trained=false until then)"
SKIP "native wlanapi 9.74 Hz scan — Windows-only; run: cargo test -p wifi-densepose-wifiscan -- --ignored measure_native_scan_rate"
SKIP "edge-latency benches (ADR-163) — host medians, not asserted here: (cd v2/crates/wifi-densepose-wasm-edge && cargo bench --features std) and (cd v2 && cargo bench -p cog-person-count -p cog-pose-estimation --no-default-features --bench infer_bench). HOST proxy only — the ESP32/WASM3 budget is NOT reproduced on a laptop; see benchmarks/edge-latency/RESULTS.md"
echo " (re-run with --full to attempt the feature-gated subset where prereqs exist)"
fi
hr
Generated
+2
View File
@@ -1015,6 +1015,7 @@ dependencies = [
"candle-core 0.9.2",
"candle-nn 0.9.2",
"clap",
"criterion",
"safetensors 0.4.5",
"serde",
"serde_json",
@@ -1034,6 +1035,7 @@ dependencies = [
"candle-core 0.9.2",
"candle-nn 0.9.2",
"clap",
"criterion",
"hex",
"safetensors 0.4.5",
"serde",
+6
View File
@@ -34,6 +34,12 @@ safetensors = "0.4"
[dev-dependencies]
tempfile = "3"
approx = "0.5"
# ADR-163: steady-state infer latency bench (real count_v1 weights, Device::Cpu).
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "infer_bench"
harness = false
[features]
default = []
@@ -0,0 +1,95 @@
//! Criterion bench for `cog-person-count` steady-state inference latency
//! (ADR-163, closing the ADR-159/160 deferred "cog inference latency bench" item).
//!
//! ## What this measures — and what the manifest's `cold_start_ms` does NOT
//!
//! This benches **steady-state** `InferenceEngine::infer` over a FIXED CSI
//! window on `Device::Cpu` with the **real** shipped `count_v1.safetensors`
//! weights — i.e. the per-frame cost once the model is loaded and warm.
//!
//! The cog manifest's `build_metadata.cold_start_ms_avg` (in the pose cog;
//! person-count's manifest carries comparable provenance) is a **DIFFERENT
//! measurement**: it includes one-time weight load / mmap / first-forward
//! allocation. Cold-start is a startup cost paid once; steady-state infer is the
//! recurring per-frame cost. They are not comparable and we do not conflate them.
//! `cold_start` was measured on ruvultra (RTX 5080 host, candle 0.9 cpu); this
//! bench runs on whatever machine you run it on — see `benchmarks/edge-latency/RESULTS.md`
//! for the host the committed numbers were taken on.
//!
//! If the weights file is absent the engine falls back to the zero-confidence
//! stub; we skip the bench in that case rather than benchmark the stub (which
//! would be a meaningless number) — the bench prints a notice and measures a
//! no-op so criterion still produces a (clearly-labelled) datapoint.
//!
//! Run (cog crates are normal workspace members):
//! cd v2 && cargo bench -p cog-person-count --no-default-features
//! cd v2 && cargo bench -p cog-person-count --no-default-features -- --warm-up-time 1 --measurement-time 2
use std::hint::black_box;
use std::path::Path;
use criterion::{criterion_group, criterion_main, Criterion};
use cog_person_count::inference::{CsiWindow, InferenceEngine, INPUT_SUBCARRIERS, INPUT_TIMESTEPS};
/// Deterministic fixed CSI window (seed-stable LCG), normalised-ish amplitudes.
fn fixed_window() -> CsiWindow {
let mut s = 0x00C0_FFEEu32;
let data: Vec<f32> = (0..INPUT_SUBCARRIERS * INPUT_TIMESTEPS)
.map(|_| {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
(s >> 16) as f32 / 32768.0 // [0, 1)
})
.collect();
CsiWindow { data }
}
/// Locate the real weights from the crate dir or the repo root.
fn real_weights() -> Option<std::path::PathBuf> {
let candidates = [
"cog/artifacts/count_v1.safetensors",
"v2/crates/cog-person-count/cog/artifacts/count_v1.safetensors",
"crates/cog-person-count/cog/artifacts/count_v1.safetensors",
];
candidates
.iter()
.map(Path::new)
.find(|p| p.exists())
.map(|p| p.to_path_buf())
}
fn bench_infer(c: &mut Criterion) {
let window = fixed_window();
match real_weights() {
Some(path) => {
let engine =
InferenceEngine::with_weights(Some(&path)).expect("load real count_v1 weights");
assert!(
engine.backend().starts_with("candle-"),
"expected real Candle backend, got {} — bench would measure the stub",
engine.backend()
);
// Sanity: one real inference before timing.
let _ = engine.infer(&window).expect("warmup infer");
c.bench_function("cog_person_count::infer[cpu_real_weights_steady_state]", |b| {
b.iter(|| {
black_box(engine.infer(black_box(&window)).expect("infer"));
});
});
}
None => {
eprintln!(
"NOTE: count_v1.safetensors not found — skipping the real-weights infer bench. \
(The committed RESULTS.md numbers require the in-repo weights.)"
);
c.bench_function("cog_person_count::infer[SKIPPED_no_weights]", |b| {
b.iter(|| black_box(1 + 1));
});
}
}
}
criterion_group!(benches, bench_infer);
criterion_main!(benches);
+6
View File
@@ -39,6 +39,12 @@ wifi-densepose-train = { version = "0.3.1", path = "../wifi-densepose-train", de
[dev-dependencies]
tempfile = "3"
# ADR-163: steady-state infer latency bench (real pose_v1 weights, Device::Cpu).
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "infer_bench"
harness = false
[features]
default = []
@@ -0,0 +1,89 @@
//! Criterion bench for `cog-pose-estimation` steady-state inference latency
//! (ADR-163, closing the ADR-159/160 deferred "cog inference latency bench" item).
//!
//! ## What this measures — and what the manifest's `cold_start_ms_avg` does NOT
//!
//! The pose cog's manifest (`cog/artifacts/manifests/x86_64/manifest.json`)
//! cites `build_metadata.cold_start_ms_avg: 5.4` (30 invocations, measured on
//! ruvultra / RTX 5080 host, candle 0.9 cpu). **That is a cold-start number** —
//! it folds in one-time weight load / mmap / first-forward allocation.
//!
//! This bench measures the **steady-state** per-frame cost instead:
//! `InferenceEngine::infer` over a FIXED CSI window on `Device::Cpu` with the
//! **real** shipped `pose_v1.safetensors`, after a warm-up forward. Steady-state
//! and cold-start are different measurements; we label both honestly and do not
//! claim this reproduces the 5.4 ms manifest figure (different machine, different
//! measurement). See `benchmarks/edge-latency/RESULTS.md`.
//!
//! Run (cog crates are normal workspace members):
//! cd v2 && cargo bench -p cog-pose-estimation --no-default-features
//! cd v2 && cargo bench -p cog-pose-estimation --no-default-features -- --warm-up-time 1 --measurement-time 2
use std::hint::black_box;
use std::path::Path;
use criterion::{criterion_group, criterion_main, Criterion};
use cog_pose_estimation::inference::{
CsiWindow, InferenceEngine, INPUT_SUBCARRIERS, INPUT_TIMESTEPS,
};
/// Deterministic fixed CSI window (seed-stable LCG).
fn fixed_window() -> CsiWindow {
let mut s = 0x00C0_FFEEu32;
let data: Vec<f32> = (0..INPUT_SUBCARRIERS * INPUT_TIMESTEPS)
.map(|_| {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
(s >> 16) as f32 / 32768.0 // [0, 1)
})
.collect();
CsiWindow { data }
}
fn real_weights() -> Option<std::path::PathBuf> {
let candidates = [
"cog/artifacts/pose_v1.safetensors",
"v2/crates/cog-pose-estimation/cog/artifacts/pose_v1.safetensors",
"crates/cog-pose-estimation/cog/artifacts/pose_v1.safetensors",
];
candidates
.iter()
.map(Path::new)
.find(|p| p.exists())
.map(|p| p.to_path_buf())
}
fn bench_infer(c: &mut Criterion) {
let window = fixed_window();
match real_weights() {
Some(path) => {
let engine =
InferenceEngine::with_weights(Some(&path)).expect("load real pose_v1 weights");
assert!(
engine.backend().starts_with("candle-"),
"expected real Candle backend, got {} — bench would measure the stub",
engine.backend()
);
let _ = engine.infer(&window).expect("warmup infer");
c.bench_function("cog_pose_estimation::infer[cpu_real_weights_steady_state]", |b| {
b.iter(|| {
black_box(engine.infer(black_box(&window)).expect("infer"));
});
});
}
None => {
eprintln!(
"NOTE: pose_v1.safetensors not found — skipping the real-weights infer bench. \
(The committed RESULTS.md numbers require the in-repo weights.)"
);
c.bench_function("cog_pose_estimation::infer[SKIPPED_no_weights]", |b| {
b.iter(|| black_box(1 + 1));
});
}
}
}
criterion_group!(benches, bench_infer);
criterion_main!(benches);
+3 -2
View File
@@ -1,5 +1,6 @@
# homecore-migrate — Migration tooling from Python Home Assistant.
# Implements ADR-134 (HOMECORE-MIGRATE), P1 scaffold:
# Implements ADR-165 (HOMECORE-MIGRATE), P1 scaffold:
# (was cited as "ADR-134"; renumbered to ADR-165 — on-disk ADR-134 is CIR. See ADR-164/ADR-165.)
# - HaStorageDir + HaStorageEnvelope: reads `.storage/*.json` files
# - Versioned format parsers under `storage_format::v<N>`
# - entity_registry, device_registry, config_entries parsers
@@ -14,7 +15,7 @@ version = "0.1.0-alpha.0"
edition = "2021"
license = "MIT"
authors = ["rUv <ruv@ruv.net>", "HOMECORE Contributors"]
description = "Migration tooling from Python Home Assistant to HOMECORE (ADR-134 P1 scaffold)"
description = "Migration tooling from Python Home Assistant to HOMECORE (ADR-165 P1 scaffold)"
repository = "https://github.com/ruvnet/RuView"
[[bin]]
+3 -3
View File
@@ -6,7 +6,7 @@ Migration tooling for importing Home Assistant configuration, entities, and secr
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![MSRV: 1.89+](https://img.shields.io/badge/MSRV-1.89%2B-purple.svg)
[![Tests](https://img.shields.io/badge/tests-19%20passing-brightgreen.svg)](https://github.com/ruvnet/RuView)
[![ADR-134](https://img.shields.io/badge/ADR-134-orange.svg)](../../docs/adr/ADR-134-homecore-migration-from-python-ha.md)
[![ADR-165](https://img.shields.io/badge/ADR-165-orange.svg)](../../docs/adr/ADR-165-homecore-migrate-from-home-assistant.md)
Parse and inspect Home Assistant's `.storage/` directory, entity registry, device registry, secrets, and automations. Convert existing HA configurations for import into HOMECORE (full conversion in P2).
@@ -22,7 +22,7 @@ Parse and inspect Home Assistant's `.storage/` directory, entity registry, devic
- **Automations parser** — reads `automations.yaml` and counts/lists automations (full conversion in P2)
- **CLI binary** — `homecore-migrate inspect` to preview what will be migrated
The tool enforces version schema compatibility: unknown HA schema versions are rejected (hard error per ADR-134 §6 Q5) rather than silently corrupting data.
The tool enforces version schema compatibility: unknown HA schema versions are rejected (hard error per ADR-165 §6 Q5) rather than silently corrupting data.
## Features
@@ -136,7 +136,7 @@ homecore-migrate (import from HA)
## References
- [ADR-134: HOMECORE Migration from Python Home Assistant](../../docs/adr/ADR-134-homecore-migration-from-python-ha.md)
- [ADR-165: HOMECORE Migration from Python Home Assistant](../../docs/adr/ADR-165-homecore-migrate-from-home-assistant.md)
- [ADR-126: HOMECORE Home Assistant Port (master)](../../docs/adr/ADR-126-homecore-home-assistant-port.md)
- [Home Assistant .storage/ format](https://developers.home-assistant.io/docs/storage/)
- [homecore-migrate CLI source](src/main.rs)
@@ -1,6 +1,6 @@
//! Parser for `core.config_entries` (HA storage schema v1, minor_version varies).
//!
//! Per ADR-134 §6 Q5, `.storage/core.config_entries` format is undocumented
//! Per ADR-165 §6 Q5, `.storage/core.config_entries` format is undocumented
//! and version-gated. P1 reads the envelope and emits:
//! - count of config entries
//! - list of integration domains represented
+4 -3
View File
@@ -1,7 +1,8 @@
//! homecore-migrate — Migration tooling from Python Home Assistant.
//!
//! Implements [ADR-134](../../docs/adr/ADR-134-homecore-migration-from-python-ha.md)
//! (referenced via ADR-126 §4, series map row ADR-134 HOMECORE-MIGRATE).
//! Implements [ADR-165](../../docs/adr/ADR-165-homecore-migrate-from-home-assistant.md)
//! (HOMECORE-MIGRATE; ADR-126 §4 series map labels the role "ADR-134 HOMECORE-MIGRATE",
//! but on-disk ADR-134 is CIR — the migrate decision was renumbered to ADR-165. See ADR-164).
//!
//! ## P1 scope
//!
@@ -56,7 +57,7 @@ pub enum MigrateError {
/// Fired when the outer `{version, minor_version}` envelope version is
/// known but the `minor_version` is not supported by any compiled parser.
/// Per ADR-134 §6 Q5: hard error on unknown minor_version.
/// Per ADR-165 §6 Q5: hard error on unknown minor_version.
#[error(
"unsupported schema version in {file}: \
version={version} minor_version={minor_version}. \
@@ -5,7 +5,7 @@
//! adding a new `v<N>.rs` module; the dispatch function in each parser module
//! routes to the right implementation.
//!
//! Per ADR-134 §6 Q5: unknown `minor_version` values produce a hard
//! Per ADR-165 §6 Q5: unknown `minor_version` values produce a hard
//! `MigrateError::UnsupportedSchemaVersion` — we do NOT silently fall back
//! to an older parser, because schema changes can be load-bearing (new fields,
//! renamed keys, semantic reinterpretations).
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "wifi-densepose-cli"
version.workspace = true
version = "0.3.1"
edition.workspace = true
description = "CLI for WiFi-DensePose"
authors.workspace = true
@@ -405,7 +405,9 @@ mod tests {
#[test]
fn test_tier_config_he20() {
let cfg = tier_config("he20");
assert_eq!(cfg.num_active, 242);
// Issue #1009 §1b: HE20 baseline records all 256 delivered bins
// (no tone map in the recorder), not the 242 active tones.
assert_eq!(cfg.num_active, 256);
}
#[test]
@@ -188,6 +188,8 @@ Thread.sleep(forTimeInterval: 3)"#,
bail!("macOS camera capture requires GUI session with camera permission")
}
// Used only by the macOS capture path above; dead on other targets.
#[allow(dead_code)]
fn decode_jpeg_to_rgb(path: &PathBuf, _width: u32, _height: u32) -> Result<Frame> {
let data = std::fs::read(path)?;
let _ = std::fs::remove_file(path);
@@ -261,9 +261,15 @@ pub enum ClockGateDecision {
/// Both terms pass: node admitted at full weight.
Admit,
/// Phase OK but clock degraded: evidence-only, NO environment/model update.
MonitorOnly { clock_quality: f32 },
MonitorOnly {
/// Combined clock-quality score in [0, 1] (dispersion × age terms).
clock_quality: f32,
},
/// Either term fails hard: node excluded this cycle.
Reject { reason: ClockRejectReason },
Reject {
/// Which hard term failed (phase, dispersion, or age).
reason: ClockRejectReason,
},
}
/// Clock-quality gate: combines the phase [`CoherenceGate`] with clock
@@ -1,6 +1,6 @@
[package]
name = "wifi-densepose-sensing-server"
version = "0.3.2"
version = "0.3.3"
edition.workspace = true
description = "Lightweight Axum server for WiFi sensing UI with RuVector signal processing"
license.workspace = true
@@ -1483,6 +1483,65 @@ fn parse_esp32_frame(buf: &[u8]) -> Option<Esp32Frame> {
})
}
#[cfg(test)]
mod issue_1009_n_subcarriers_u16_tests {
//! Issue #1009 §1c — `parse_esp32_frame` must read `n_subcarriers` as a
//! u16 LE at bytes 6..7 (ADR-018 wire format), not a single byte at 6.
//!
//! An ESP32-C6 HE20 frame carries 256 subcarriers → byte 6 = 0x00,
//! byte 7 = 0x01. The pre-#1005 single-byte read decoded this as 0
//! subcarriers, silently dropping every real HE20 frame. This was the same
//! truncation as the CLI parser (`wifi-densepose-cli` calibrate.rs); this
//! module pins that the sensing-server template stays u16-correct.
use super::*;
/// Build an ADR-018 CSI frame (magic 0xC511_0001, 20-byte header).
fn build_csi_frame(n_subcarriers: u16) -> Vec<u8> {
let mut buf = vec![0u8; 20 + n_subcarriers as usize * 2];
buf[0..4].copy_from_slice(&0xC511_0001u32.to_le_bytes());
buf[4] = 7; // node_id
buf[5] = 1; // n_antennas
buf[6..8].copy_from_slice(&n_subcarriers.to_le_bytes()); // u16 LE
buf[8..12].copy_from_slice(&5180u32.to_le_bytes()); // freq_mhz (5 GHz HE)
buf[12..16].copy_from_slice(&42u32.to_le_bytes()); // sequence
buf[16] = (-40i8) as u8; // rssi
buf[17] = (-90i8) as u8; // noise_floor
buf[18] = 0; // ppdu_type
buf[19] = 0;
for k in 0..n_subcarriers as usize {
buf[20 + k * 2] = (5 + (k % 40) as i8) as u8; // i
buf[20 + k * 2 + 1] = (k % 30) as u8; // q
}
buf
}
#[test]
fn parse_esp32_frame_he20_256_bins_not_truncated() {
// 256 = 0x0100 LE: byte6 = 0x00, byte7 = 0x01. A u8 read of byte 6
// would see 0 subcarriers; a u16 read sees 256.
let buf = build_csi_frame(256);
assert_eq!(buf.len(), 532, "256-bin frame wire size = 20 + 256*2");
let frame = parse_esp32_frame(&buf).expect("256-bin HE20 frame must parse");
assert_eq!(
frame.n_subcarriers, 256,
"n_subcarriers must read as u16 (256), not the byte-6-only 0"
);
assert_eq!(frame.amplitudes.len(), 256);
assert_eq!(frame.node_id, 7);
assert_eq!(frame.rssi, -40);
assert_eq!(frame.sequence, 42);
}
#[test]
fn parse_esp32_frame_ht20_64_bins_still_parses() {
// Regression guard for the common single-byte (≤255) case.
let buf = build_csi_frame(64);
let frame = parse_esp32_frame(&buf).expect("64-bin HT20 frame must parse");
assert_eq!(frame.n_subcarriers, 64);
assert_eq!(frame.amplitudes.len(), 64);
}
}
// ── Signal field generation ──────────────────────────────────────────────────
/// Generate a signal field that reflects where motion and signal changes are occurring.
@@ -2694,6 +2753,203 @@ async fn probe_esp32(port: u16) -> bool {
}
}
// ── Source resolution state machine (issue #1004) ────────────────────────────
/// What background tasks to start, derived from `--source` and the boot probes.
///
/// Issue #1004: a one-shot startup probe latched `auto` to `simulate` forever
/// when no CSI happened to be flowing at boot (the normal case — the firmware
/// and the server race to come up). The UDP :5005 receiver was then never
/// bound, so real CSI arriving seconds later was silently ignored and the
/// server served simulated poses for the rest of the process. The UI looked
/// live; the data was fake. This is the exact "where's the real data?" failure
/// class the project fights.
///
/// The robust resolution: in `auto` mode **always bind the UDP receiver**
/// regardless of the boot probe. If no real source is up yet, serve simulated
/// data *and* keep the UDP receiver listening; the receiver promotes
/// `source` → `esp32` the instant the first real frame lands (see
/// `udp_receiver_task`, which sets `s.source = "esp32"`), mirroring the inverse
/// `esp32 → esp32:offline` reversion already in `effective_source()`.
///
/// Explicit `--source simulated` is a hard override for offline demos: it does
/// NOT bind UDP, so no promotion ever happens.
#[derive(Debug, Clone, PartialEq, Eq)]
struct SourcePlan {
/// The `AppStateInner.source` value to start with.
initial_source: String,
/// Bind the UDP :5005 receiver (and thus allow simulate→esp32 promotion).
bind_udp: bool,
/// Run the simulated-data generator (serves poses until a real frame arrives).
run_simulator: bool,
/// Run the Windows WiFi capture task.
run_wifi: bool,
}
/// Pure decision function — fully unit-testable without binding sockets.
///
/// `requested` is the normalized `--source` value. `esp32_detected` /
/// `wifi_detected` are the boot-probe results (only consulted in `auto` mode).
/// Returns `None` for an unknown source that names neither a real source nor a
/// simulate alias (the caller maps that to its own pass-through/exit policy).
fn plan_source(requested: &str, esp32_detected: bool, wifi_detected: bool) -> SourcePlan {
match requested {
"auto" => {
if esp32_detected {
// Real CSI already flowing — bind UDP, no simulator.
SourcePlan {
initial_source: "esp32".to_string(),
bind_udp: true,
run_simulator: false,
run_wifi: false,
}
} else if wifi_detected {
SourcePlan {
initial_source: "wifi".to_string(),
bind_udp: false,
run_simulator: false,
run_wifi: true,
}
} else {
// No real source *yet*. Serve simulated data, but ALSO bind UDP
// so the receiver can promote to esp32 when the first real
// frame arrives (issue #1004). Never latch on simulate.
SourcePlan {
initial_source: "simulated".to_string(),
bind_udp: true,
run_simulator: true,
run_wifi: false,
}
}
}
// Explicit overrides. "simulate" is a back-compat alias for "simulated".
"simulate" | "simulated" => SourcePlan {
initial_source: "simulated".to_string(),
bind_udp: false, // hard override: offline demo, no live promotion
run_simulator: true,
run_wifi: false,
},
"esp32" => SourcePlan {
initial_source: "esp32".to_string(),
bind_udp: true,
run_simulator: false,
run_wifi: false,
},
"wifi" => SourcePlan {
initial_source: "wifi".to_string(),
bind_udp: false,
run_simulator: false,
run_wifi: true,
},
// Unknown source — preserve it verbatim, no tasks (caller's policy).
other => SourcePlan {
initial_source: other.to_string(),
bind_udp: false,
run_simulator: false,
run_wifi: false,
},
}
}
#[cfg(test)]
mod issue_1004_source_plan_tests {
//! Issue #1004 — `--source auto` must NOT latch on `simulate` forever.
//!
//! Old behavior: a one-shot boot probe resolved the source once. With no CSI
//! flowing at boot (the normal case), the server either latched on simulate
//! (never binding UDP :5005, so later real CSI was silently ignored) or
//! hard-exited (#937), never picking up CSI that started after launch.
//!
//! New behavior (`plan_source`): in `auto` the UDP receiver is ALWAYS bound,
//! simulated data is served only until the first real frame, then
//! `udp_receiver_task` promotes `source` → "esp32". These tests pin the
//! resolution/promotion state machine directly (no sockets bound).
use super::*;
// FAILS ON OLD CODE: the old `auto`-with-no-source path bound no UDP
// receiver (it spawned only `simulated_data_task`, or exited). This asserts
// UDP IS bound even when the boot probe finds no source.
#[test]
fn auto_with_no_boot_source_still_binds_udp_and_simulates() {
let plan = plan_source("auto", false, false);
assert!(plan.bind_udp, "auto must bind UDP :5005 even with no boot source (#1004)");
assert!(plan.run_simulator, "auto must serve simulated data until real CSI arrives");
assert!(!plan.run_wifi);
assert_eq!(plan.initial_source, "simulated");
}
#[test]
fn auto_with_esp32_detected_binds_udp_no_simulator() {
let plan = plan_source("auto", true, false);
assert!(plan.bind_udp);
assert!(!plan.run_simulator, "real CSI present → no synthetic frames");
assert_eq!(plan.initial_source, "esp32");
}
#[test]
fn auto_with_wifi_detected_runs_wifi_no_udp() {
let plan = plan_source("auto", false, true);
assert!(plan.run_wifi);
assert!(!plan.bind_udp);
assert!(!plan.run_simulator);
assert_eq!(plan.initial_source, "wifi");
}
// Explicit `--source simulated` is a hard offline override: it must NOT bind
// UDP (so it can never be promoted to live), distinguishing it from
// auto-mode simulate.
#[test]
fn explicit_simulated_is_offline_override_no_udp() {
for s in ["simulated", "simulate"] {
let plan = plan_source(s, false, false);
assert!(!plan.bind_udp, "{s}: explicit simulate must not bind UDP (offline demo)");
assert!(plan.run_simulator);
assert_eq!(plan.initial_source, "simulated");
}
}
#[test]
fn explicit_esp32_binds_udp() {
let plan = plan_source("esp32", false, false);
assert!(plan.bind_udp);
assert!(!plan.run_simulator);
assert_eq!(plan.initial_source, "esp32");
}
// Promotion check: the runtime promotes by setting `AppStateInner.source`
// to "esp32" on the first real frame; `effective_source()` then reports it
// (and reverts to "esp32:offline" after a 5 s gap). This asserts the
// promotion direction the simulator/receiver rely on, without binding a
// socket — it exercises the same `source` field the UDP task writes.
#[test]
fn effective_source_promotes_from_simulated_to_esp32_on_real_frame() {
// Start as the auto/simulate plan would: source = "simulated".
let mut src = "simulated".to_string();
// effective_source() logic for the simulate state: stays "simulated".
assert_eq!(promote_view(&src, None), "simulated");
// First real frame arrives → udp_receiver_task sets source = "esp32".
src = "esp32".to_string();
let fresh = Some(std::time::Duration::from_millis(10));
assert_eq!(promote_view(&src, fresh), "esp32", "fresh esp32 frame ⇒ live");
// After a >5 s gap it reverts to offline (inverse machinery, #1004).
let stale = Some(ESP32_OFFLINE_TIMEOUT + std::time::Duration::from_secs(1));
assert_eq!(promote_view(&src, stale), "esp32:offline");
}
/// Mirror of `AppStateInner::effective_source` over just (source, age) so the
/// promotion/reversion logic is testable without constructing full state.
fn promote_view(source: &str, last_frame_age: Option<std::time::Duration>) -> String {
if source == "esp32" {
if let Some(age) = last_frame_age {
if age > ESP32_OFFLINE_TIMEOUT {
return "esp32:offline".to_string();
}
}
}
source.to_string()
}
}
// ── Simulated data generator ─────────────────────────────────────────────────
fn generate_simulated_frame(tick: u64) -> Esp32Frame {
@@ -5699,6 +5955,18 @@ async fn simulated_data_task(state: SharedState, tick_ms: u64) {
interval.tick().await;
let mut s = state.write().await;
// Issue #1004: in `auto` mode this task runs alongside `udp_receiver_task`.
// Once a real frame promotes `source` → "esp32", stop emitting synthetic
// frames so we never clobber live CSI with simulated poses. (For an
// explicit `--source simulated` demo, `source` stays "simulated" and the
// simulator keeps running — that path never binds UDP, so it is never
// promoted.) The task stays alive so it can resume serving if the real
// source later ages out to "esp32:offline".
if s.effective_source() == "esp32" {
continue;
}
s.tick += 1;
let tick = s.tick;
@@ -6584,48 +6852,48 @@ async fn main() {
info!(" UI path: {}", args.ui_path.display());
info!(" Source: {}", args.source);
// Auto-detect data source.
// Resolve the data source into a concrete task plan (issue #1004).
//
// Issue #937 / sibling fix: previously `auto` silently fell back to the
// synthetic data source when no ESP32 or Windows WiFi was reachable, with
// only an `info!` log line as the signal. Downstream API consumers
// (`/api/v1/sensing/latest`, `/ws/sensing`) had no in-band way to know they
// were being served fake CSI tagged as production telemetry. That is the
// exact "where's the real data?" pattern external reviewers (#943, #934)
// cited as the most damaging evidence of the project misrepresenting its
// posture. Synthetic-data is now opt-in only — operators who want demo
// mode must explicitly set `--source simulated` or `CSI_SOURCE=simulated`.
let source = match args.source.as_str() {
"auto" => {
info!("Auto-detecting data source...");
if probe_esp32(args.udp_port).await {
info!(" ESP32 CSI detected on UDP :{}", args.udp_port);
"esp32"
} else if probe_windows_wifi().await {
info!(" Windows WiFi detected");
"wifi"
} else {
error!(
"No real CSI source detected. Auto-detection refuses to silently \
fall back to synthetic data because that would expose downstream \
consumers (/api/v1/sensing/latest, /ws/sensing) to fake telemetry \
tagged as production. To run with synthetic data, set the source \
explicitly: --source simulated (or CSI_SOURCE=simulated in Docker). \
To use real hardware: provision an ESP32 to emit CSI on UDP :{} or \
install the Windows WiFi capture driver. See \
https://github.com/ruvnet/RuView/issues/937 for context.",
args.udp_port
);
std::process::exit(78); // EX_CONFIG
}
// Issue #937 (prior fix): `auto` must never serve fake CSI *tagged as
// production telemetry*. We keep that guarantee — in the gap before real
// CSI arrives, `source` is the honest string "simulated" (downstream
// `/api/v1/sensing/latest`, `/ws/sensing` see `source: "simulated"`, not a
// production tag). What #937's hard-exit got wrong: at boot the firmware and
// server race, so CSI usually is NOT flowing during the 2 s probe. Exiting
// (or latching on simulate) meant the server could never pick up CSI that
// started seconds later. The robust resolution (see `plan_source`): in
// `auto` always bind the UDP :5005 receiver; serve simulated until the first
// real frame; then `udp_receiver_task` promotes `source` → "esp32". Explicit
// `--source simulated` stays a hard, UDP-free override for offline demos.
let normalized = if args.source == "simulate" { "simulated" } else { args.source.as_str() };
let plan = if normalized == "auto" {
info!("Auto-detecting data source (UDP :{} bound either way)...", args.udp_port);
let esp32 = probe_esp32(args.udp_port).await;
let wifi = if esp32 { false } else { probe_windows_wifi().await };
if esp32 {
info!(" ESP32 CSI detected on UDP :{}", args.udp_port);
} else if wifi {
info!(" Windows WiFi detected");
} else {
warn!(
"No real CSI source at boot — serving SIMULATED data (tagged as \
'simulated', not production) while the UDP :{} receiver stays bound. \
The server promotes to live the instant a real frame arrives (issue \
#1004). For an offline demo with no live promotion, pass \
--source simulated explicitly.",
args.udp_port
);
}
// "simulate" is a synonym for "simulated" (back-compat alias kept so
// existing operators who already opted in don't get broken by this fix).
"simulate" => "simulated",
other => other,
plan_source("auto", esp32, wifi)
} else {
plan_source(normalized, false, false)
};
let source: &str = plan.initial_source.as_str();
info!("Data source: {source}");
info!(
"Data source: {source} (udp_receiver={}, simulator={}, wifi={})",
plan.bind_udp, plan.run_simulator, plan.run_wifi
);
// Shared state
// Vital sign sample rate derives from tick interval (e.g. 500ms tick => 2 Hz)
@@ -6905,18 +7173,22 @@ async fn main() {
data_dir: data_dir.clone(),
}));
// Start background tasks based on source
match source {
"esp32" => {
tokio::spawn(udp_receiver_task(state.clone(), args.udp_port));
tokio::spawn(broadcast_tick_task(state.clone(), args.tick_ms));
}
"wifi" => {
tokio::spawn(windows_wifi_task(state.clone(), args.tick_ms));
}
_ => {
tokio::spawn(simulated_data_task(state.clone(), args.tick_ms));
}
// Start background tasks from the resolved plan (issue #1004).
//
// In `auto` mode with no boot source, `bind_udp` AND `run_simulator` are
// both true: the UDP receiver is bound so real CSI can promote the source,
// and the simulator serves poses in the meantime (it self-suspends once
// promoted — see `simulated_data_task`). Explicit `--source simulated` has
// `bind_udp = false`, so it serves simulated data only, with no live binding.
if plan.bind_udp {
tokio::spawn(udp_receiver_task(state.clone(), args.udp_port));
tokio::spawn(broadcast_tick_task(state.clone(), args.tick_ms));
}
if plan.run_wifi {
tokio::spawn(windows_wifi_task(state.clone(), args.tick_ms));
}
if plan.run_simulator {
tokio::spawn(simulated_data_task(state.clone(), args.tick_ms));
}
// ADR-050: Parse bind address once, use for all listeners
@@ -145,6 +145,8 @@ pub fn matter_mapping(entity: EntityKind) -> Option<MatterClusterMapping> {
}
/// True iff the entity has a Matter exposure on a current spec cluster.
// P2 Matter-publisher API surface; real Matter exposure is deferred (ADR-159 §A5).
#[allow(dead_code)]
pub fn entity_on_matter(entity: EntityKind) -> bool {
matter_mapping(entity).is_some()
}
@@ -152,6 +154,8 @@ pub fn entity_on_matter(entity: EntityKind) -> bool {
/// Compute the next available endpoint ID for a node-scoped entity,
/// given a starting offset (the bridge's first child endpoint). Used
/// by the publisher to assign per-primitive endpoints deterministically.
// P2 Matter-publisher API surface; real Matter exposure is deferred (ADR-159 §A5).
#[allow(dead_code)]
pub fn next_endpoint(base: u16, primitive_index: u16) -> u16 {
base.saturating_add(primitive_index)
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "wifi-densepose-signal"
version = "0.3.3"
version = "0.3.4"
edition.workspace = true
description = "WiFi CSI signal processing for DensePose estimation"
license.workspace = true
@@ -1,7 +1,7 @@
//! Criterion benchmarks for the empty-room baseline calibration module (ADR-135).
//!
//! Measures per-call throughput of CalibrationRecorder and BaselineCalibration
//! across HT20 (K=52), HT40 (K=114), HE20 (K=242), and HE40 (K=484).
//! across HT20 (K=52), HT40 (K=114), HE20 (K=256, all bins; #1009), and HE40 (K=484).
//!
//! Run (compile-only — no execution):
//! cargo bench -p wifi-densepose-signal --no-default-features --bench calibration_bench --no-run
@@ -63,7 +63,8 @@ fn tiers() -> Vec<TierSpec> {
vec![
TierSpec { label: "ht20", n_active: 52, bandwidth_mhz: 20, config: CalibrationConfig::ht20() },
TierSpec { label: "ht40", n_active: 114, bandwidth_mhz: 40, config: CalibrationConfig::ht40() },
TierSpec { label: "he20", n_active: 242, bandwidth_mhz: 20, config: CalibrationConfig::he20() },
// Issue #1009 §1b: HE20 records all 256 delivered bins (he20().num_active == 256).
TierSpec { label: "he20", n_active: 256, bandwidth_mhz: 20, config: CalibrationConfig::he20() },
TierSpec { label: "he40", n_active: 484, bandwidth_mhz: 40, config: CalibrationConfig::he40() },
]
}
@@ -109,9 +109,26 @@ impl CalibrationConfig {
pub fn ht40() -> Self {
Self { tier: PhyTier::Ht40, num_subcarriers: 128, num_active: 114, min_frames: 600, max_phase_variance: 0.3 }
}
/// HE20 defaults: 256 FFT, 242 active.
/// HE20 defaults: 256 FFT, **256 active** (record all delivered bins).
///
/// Issue #1009: the ESP-IDF v5.5.2 driver delivers all 256 FFT bins on the
/// wire for an HE20 frame (242 data tones + pilots + guards + DC; n_subc =
/// 0x0100 LE, wire-verified on ESP32-C6). We set `num_active: 256` so the
/// recorder accumulates statistics over **every** delivered bin rather than
/// trimming to the first 242 columns.
///
/// Why not 242? `CalibrationRecorder` has no HE20 tone map — `extract_first_stream`
/// takes the first `num_active` columns *sequentially*. With 242 it would
/// keep bins 0..242 of the 256-bin grid, which are NOT the 242 active tones
/// (they include the lower guard band and DC) — silently corrupting the
/// empty-room baseline. Recording all 256 bins keeps amplitude/phase stats
/// aligned 1:1 with the live `deviation()` path (which also sees 256 bins),
/// so guard/DC bins simply carry near-zero, stable statistics and never
/// generate false occupancy alarms. The exact-242 tone map lives only in
/// `cir.rs` (`HE20_ACTIVE`), where the Φ sensing matrix genuinely needs it;
/// the baseline recorder does not.
pub fn he20() -> Self {
Self { tier: PhyTier::He20, num_subcarriers: 256, num_active: 242, min_frames: 600, max_phase_variance: 0.3 }
Self { tier: PhyTier::He20, num_subcarriers: 256, num_active: 256, min_frames: 600, max_phase_variance: 0.3 }
}
/// HE40 defaults: 512 FFT, 484 active.
pub fn he40() -> Self {
@@ -674,13 +691,38 @@ mod tests {
let he20 = CalibrationConfig::he20();
assert_eq!(he20.num_subcarriers, 256);
assert_eq!(he20.num_active, 242);
// Issue #1009: HE20 records all 256 delivered bins (no tone map in the
// baseline recorder), not the 242 active tones — see he20() rationale.
assert_eq!(he20.num_active, 256);
let he40 = CalibrationConfig::he40();
assert_eq!(he40.num_subcarriers, 512);
assert_eq!(he40.num_active, 484);
}
// Issue #1009 §1b: a real HE20 frame carries all 256 FFT bins. The recorder
// must accept it AND build the baseline over all 256 bins — not silently
// trim to the first 242 columns (which are guards/DC, not active tones).
//
// FAILS ON OLD CODE: with `he20().num_active == 242` the finalised baseline
// had only 242 subcarriers (256 → 242 sequential trim). This asserts 256.
#[test]
fn he20_records_all_256_bins_not_trimmed_to_242() {
let mut cfg = CalibrationConfig::he20();
cfg.min_frames = 1;
let mut rec = CalibrationRecorder::new(cfg);
// Feed a 256-bin frame exactly as ESP-IDF v5.5.2 delivers it.
let frame = constant_frame(256, 1.0, 0.0);
rec.record(&frame).expect("256-bin HE20 frame must be accepted");
let baseline = rec.finalize().expect("finalize after 1 frame (min_frames=1)");
assert_eq!(
baseline.subcarriers.len(),
256,
"HE20 baseline must cover all 256 delivered bins, not a 242-trim"
);
assert_eq!(baseline.tier, PhyTier::He20);
}
// Additional: insufficient frames → error.
#[test]
fn finalize_requires_min_frames() {
@@ -67,7 +67,10 @@ fn ht40_spec() -> TierSpec {
TierSpec { label: "HT40", n_active: 114, bandwidth_mhz: 40, config: CalibrationConfig::ht40() }
}
fn he20_spec() -> TierSpec {
TierSpec { label: "HE20", n_active: 242, bandwidth_mhz: 20, config: CalibrationConfig::he20() }
// Issue #1009 §1b: real HE20 frames carry all 256 FFT bins (242 data +
// pilots/guards/DC), and the recorder now records all 256 (he20().num_active
// == 256). Feed 256-bin frames to match the wire format.
TierSpec { label: "HE20", n_active: 256, bandwidth_mhz: 20, config: CalibrationConfig::he20() }
}
// ---------------------------------------------------------------------------
+577
View File
@@ -2,6 +2,33 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -11,12 +38,76 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstyle",
"clap_lex",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -26,6 +117,73 @@ dependencies = [
"libc",
]
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -46,6 +204,36 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -56,6 +244,60 @@ dependencies = [
"version_check",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.182"
@@ -68,6 +310,192 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha2"
version = "0.10.9"
@@ -79,22 +507,171 @@ dependencies = [
"digest",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "wifi-densepose-wasm-edge"
version = "0.3.0"
dependencies = [
"criterion",
"libm",
"sha2",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zerocopy"
version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
@@ -11,6 +11,20 @@ categories = ["embedded", "wasm", "science"]
[lib]
crate-type = ["cdylib", "rlib"]
# The lib's libtest harness does not understand criterion CLI flags
# (`--warm-up-time` etc.), so exclude it from `cargo bench` — only the criterion
# bench target below should receive bench args (ADR-163).
bench = false
# ADR-163: host-measured process_frame latency benches (closes the ADR-160
# "criterion benches for process_frame budget claims" deferred item — HOST only;
# the ESP32-S3 WASM3 budget remains unmeasured, see the bench header).
# `std` is required (criterion is a host crate); the crate is workspace-EXCLUDED
# so run from the crate dir: `cargo bench --features std`.
[[bench]]
name = "process_frame_bench"
harness = false
required-features = ["std"]
[dependencies]
# no_std math
@@ -18,6 +32,11 @@ libm = "0.2"
# SHA-256 for RVF build hash (optional, used by builder)
sha2 = { version = "0.10", optional = true, default-features = false }
[dev-dependencies]
# Host-only latency regression benches (ADR-163). Pinned to match the rest of
# the workspace's bench crates.
criterion = { version = "0.5", features = ["html_reports"] }
[features]
default = ["default-pipeline"]
# Enable std for testing on host + RVF builder
@@ -0,0 +1,259 @@
//! Criterion benches for the heaviest `process_frame` hot paths in the edge
//! skill library (ADR-163, closing the ADR-160 §"Deferred Backlog" item
//! "Criterion benches for process_frame budget claims").
//!
//! ## HONEST SCOPE — read this before citing any number here
//!
//! These benches measure **HOST** wall-clock latency on a development laptop.
//! The per-module doc budgets (e.g. `exo_time_crystal` "H (heavy, <10ms) on
//! ESP32-S3 WASM3") are **for a different target**: an Xtensa ESP32-S3 running
//! the WASM3 interpreter. A native x86_64 host with `-O` is an **upper-bound
//! proxy for the ALGORITHM cost only**; it is NOT the ESP32 number and does NOT
//! reproduce the ESP32 budget. WASM3 interpretation on a ~240 MHz Xtensa core is
//! typically 1-2 orders of magnitude slower than native host code, so a host
//! median well under the budget does NOT prove the ESP32 meets it — it only
//! bounds the work. The ESP32 figure remains UNMEASURED (needs hardware).
//!
//! What these benches DO prove (MEASURED-on-host):
//! * the hot paths run, on a fixed synthetic CSI frame, with a real median;
//! * a regression guard exists so a future change that 10×'s the host cost
//! is caught in CI/dev even before anyone reflashes an ESP32.
//!
//! Run (the crate is EXCLUDED from the v2 workspace — bench from the crate dir):
//! cd v2/crates/wifi-densepose-wasm-edge
//! cargo bench --features std
//! # quick smoke:
//! cargo bench --features std -- --warm-up-time 1 --measurement-time 2
//!
//! `med_seizure_detect` is gated behind `medical-experimental`; its bench is
//! `#[cfg(feature = "medical-experimental")]` and only runs when that feature is
//! also enabled:
//! cargo bench --features std,medical-experimental
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use std::hint::black_box;
use wifi_densepose_wasm_edge::exo_ghost_hunter::GhostHunterDetector;
use wifi_densepose_wasm_edge::exo_time_crystal::TimeCrystalDetector;
use wifi_densepose_wasm_edge::sec_weapon_detect::WeaponDetector;
// ── Fixed synthetic CSI fixtures (deterministic LCG, seed-stable) ────────────
/// Deterministic pseudo-random in [lo, hi) from a 32-bit LCG, matching the
/// generator style used by `tests/budget_compliance.rs`.
fn lcg(seed: &mut u32) -> f32 {
*seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
(*seed >> 16) as f32 / 32768.0
}
fn synthetic_phases(n: usize, seed: u32) -> Vec<f32> {
let mut s = seed;
(0..n).map(|_| lcg(&mut s) * 6.2832 - 3.1416).collect()
}
fn synthetic_amplitudes(n: usize, seed: u32) -> Vec<f32> {
let mut s = seed;
(0..n).map(|_| lcg(&mut s) * 10.0 + 0.1).collect()
}
fn synthetic_variance(n: usize, seed: u32) -> Vec<f32> {
let mut s = seed;
(0..n).map(|_| lcg(&mut s) * 2.0 + 0.05).collect()
}
const N_SC: usize = 32; // per-subcarrier width (matches both modules' MAX_SC)
// ── exo_time_crystal: compute_autocorrelation 256×128 hot path ───────────────
//
// `compute_autocorrelation` is private, so we drive it through the public
// `process_frame`. To hit the full 256-point × 128-lag autocorrelation the
// circular buffer must be FULL (≥256 samples) and the signal must be
// non-constant (the module early-outs on `buf_var < 1e-8`). We pre-fill once
// with a periodic-plus-noise motion-energy stream, then bench a single
// `process_frame` (each call recomputes the full 256×128 autocorrelation =
// ~32K multiply-accumulates, the M6-audit-named hot path).
fn prefilled_time_crystal() -> TimeCrystalDetector {
let mut d = TimeCrystalDetector::new();
let mut s = 0xC0FFEEu32;
// 300 frames (> BUF_LEN=256) so the buffer is full and statistics are warm.
for i in 0..300 {
// period-10 square wave + small noise → guarantees buf_var > 0 and a
// genuine autocorrelation structure (the expensive path runs).
let base = if (i % 10) < 5 { 1.0 } else { 0.0 };
let me = base + lcg(&mut s) * 0.05;
black_box(d.process_frame(black_box(me)));
}
d
}
fn bench_exo_time_crystal(c: &mut Criterion) {
c.bench_function("exo_time_crystal::process_frame[autocorr_256x128]", |b| {
let mut s = 0x1357_9BDFu32;
b.iter_batched(
prefilled_time_crystal,
|mut d| {
// One frame = one full 256×128 autocorrelation pass.
let me = if (d.frame_count() % 10) < 5 { 1.0 } else { 0.0 } + lcg(&mut s) * 0.05;
black_box(d.process_frame(black_box(me)));
},
BatchSize::SmallInput,
);
});
}
// ── exo_ghost_hunter: periodicity + hidden-breathing hot path ────────────────
//
// Heaviest path runs only when the room is reported EMPTY (presence == 0):
// per-group anomaly accumulation + aggregate-phase autocorrelation for hidden
// periodic (breathing) signatures. We warm the noise floor + phase buffer first,
// then bench one empty-room frame.
fn prefilled_ghost_hunter() -> GhostHunterDetector {
let mut d = GhostHunterDetector::new();
let mut s = 0xBADC0DEu32;
// Warm the per-group EWMA noise floors + fill the phase buffer (PHASE_BUF_LEN=64)
// with a periodic phase signal so the periodicity autocorrelation has structure.
for i in 0..120u32 {
let phases: Vec<f32> = (0..N_SC)
.map(|k| libm::sinf(i as f32 * 0.4 + k as f32 * 0.1) * 0.3 + lcg(&mut s) * 0.02)
.collect();
let amps = synthetic_amplitudes(N_SC, 4000 + i);
let var = synthetic_variance(N_SC, 4500 + i);
black_box(d.process_frame(&phases, &amps, &var, 0, 0.05));
}
d
}
fn bench_exo_ghost_hunter(c: &mut Criterion) {
let amps = synthetic_amplitudes(N_SC, 9000);
let var = synthetic_variance(N_SC, 9500);
c.bench_function("exo_ghost_hunter::process_frame[empty_room_periodicity]", |b| {
let mut s = 0x2468_ACE0u32;
b.iter_batched(
prefilled_ghost_hunter,
|mut d| {
let i = d.frame_count();
let phases: Vec<f32> = (0..N_SC)
.map(|k| libm::sinf(i as f32 * 0.4 + k as f32 * 0.1) * 0.3 + lcg(&mut s) * 0.02)
.collect();
black_box(d.process_frame(
black_box(&phases),
black_box(&amps),
black_box(&var),
black_box(0),
black_box(0.05),
));
},
BatchSize::SmallInput,
);
});
}
// ── sec_weapon_detect: per-subcarrier Welford hot path ───────────────────────
//
// After calibration the detector runs a per-subcarrier online Welford update
// over MAX_SC=32 subcarriers each frame (the M6-audit-named hot path). We
// calibrate first (the early frames just accumulate baseline stats), then bench
// one steady-state frame.
fn calibrated_weapon_detector() -> WeaponDetector {
let mut d = WeaponDetector::new();
// Drive enough empty-room frames to complete calibration + warm the running
// Welford state. Calibration window is internal; 200 frames is comfortably
// past it for MAX_SC=32.
for i in 0..200u32 {
let phases = synthetic_phases(N_SC, 6000 + i);
let amps = synthetic_amplitudes(N_SC, 6500 + i);
let var = synthetic_variance(N_SC, 7000 + i);
black_box(d.process_frame(&phases, &amps, &var, 0.05, 0));
}
d
}
fn bench_sec_weapon_detect(c: &mut Criterion) {
c.bench_function("sec_weapon_detect::process_frame[per_sc_welford]", |b| {
let mut seed = 8000u32;
b.iter_batched(
calibrated_weapon_detector,
|mut d| {
seed = seed.wrapping_add(1);
let phases = synthetic_phases(N_SC, seed);
let amps = synthetic_amplitudes(N_SC, seed.wrapping_add(500));
let var = synthetic_variance(N_SC, seed.wrapping_add(1000));
black_box(d.process_frame(
black_box(&phases),
black_box(&amps),
black_box(&var),
black_box(0.3),
black_box(1),
));
},
BatchSize::SmallInput,
);
});
}
// ── med_seizure_detect: detect_rhythm / clonic autocorrelation hot path ──────
//
// Gated behind `medical-experimental` (ADR-160 §A1). The clonic-phase rhythm
// detection autocorrelates the amplitude ring buffer (PHASE_WINDOW=100); we warm
// the buffers with a high-energy rhythmic signal, then bench one frame.
#[cfg(feature = "medical-experimental")]
mod med {
use super::*;
use wifi_densepose_wasm_edge::med_seizure_detect::SeizureDetector;
fn warmed_seizure_detector() -> SeizureDetector {
let mut d = SeizureDetector::new();
let mut s = 0x5EE_D00Du32;
// High-energy ~4 Hz rhythmic (period ~5 frames at 20 Hz) → exercises the
// clonic-phase rhythm/autocorrelation path, with presence asserted.
for i in 0..150u32 {
let me = 2.5 + libm::sinf(i as f32 * 1.25) * 1.5;
let amp = 1.0 + lcg(&mut s) * 0.2;
black_box(d.process_frame(0.0, amp, me, 1));
}
d
}
pub fn bench_med_seizure_detect(c: &mut Criterion) {
c.bench_function("med_seizure_detect::process_frame[clonic_rhythm]", |b| {
let mut s = 0x9A_BCDE_F0u32;
b.iter_batched(
warmed_seizure_detector,
|mut d| {
let i = d.frame_count();
let me = 2.5 + libm::sinf(i as f32 * 1.25) * 1.5;
let amp = 1.0 + lcg(&mut s) * 0.2;
black_box(d.process_frame(
black_box(0.0),
black_box(amp),
black_box(me),
black_box(1),
));
},
BatchSize::SmallInput,
);
});
}
}
#[cfg(feature = "medical-experimental")]
criterion_group!(
benches,
bench_exo_time_crystal,
bench_exo_ghost_hunter,
bench_sec_weapon_detect,
med::bench_med_seizure_detect,
);
#[cfg(not(feature = "medical-experimental"))]
criterion_group!(
benches,
bench_exo_time_crystal,
bench_exo_ghost_hunter,
bench_sec_weapon_detect,
);
criterion_main!(benches);
@@ -0,0 +1,108 @@
//! Runnable demo of the unified [`EdgePipeline`]: constructs every registered
//! skill, feeds a short deterministic synthetic CSI frame sequence, and prints
//! the per-skill events plus a registration summary.
//!
//! ```bash
//! cd v2/crates/wifi-densepose-wasm-edge
//! cargo run --example run_all_skills --features std
//! cargo run --example run_all_skills --features std,medical-experimental
//! ```
//!
//! [`EdgePipeline`]: wifi_densepose_wasm_edge::pipeline_all::EdgePipeline
#[cfg(not(feature = "std"))]
fn main() {
eprintln!("run_all_skills requires --features std");
}
#[cfg(feature = "std")]
fn main() {
use std::collections::BTreeMap;
use wifi_densepose_wasm_edge::pipeline_all::{CsiFrameView, EdgePipeline};
const N_SC: usize = 32;
let mut pipeline = EdgePipeline::new();
println!("=== EdgePipeline registration ===");
println!("registered skills: {}", pipeline.skill_count());
let med = pipeline
.skills()
.iter()
.filter(|s| s.medical_experimental)
.count();
println!(
" default tier: {} medical-experimental tier: {}",
pipeline.skill_count() - med,
med
);
println!();
let mut phases = [0.0f32; N_SC];
let mut amps = [0.0f32; N_SC];
let mut vars = [0.0f32; N_SC];
let mut prev = [0.0f32; N_SC];
// Per-skill event counters over the run.
let mut counts: BTreeMap<&'static str, usize> = BTreeMap::new();
for s in pipeline.skills() {
counts.insert(s.name, 0);
}
let frames = 300usize;
for t in 0..frames {
let tf = t as f32;
let breath = (tf * 2.0 * std::f32::consts::PI * 0.3 / 20.0).sin();
let heart = (tf * 2.0 * std::f32::consts::PI * 1.2 / 20.0).sin();
let mut vmean = 0.0f32;
for i in 0..N_SC {
let sc = i as f32;
phases[i] = (sc * 0.21 + tf * 0.05).sin() + 0.15 * breath;
amps[i] = 1.0 + 0.3 * (sc * 0.11 + tf * 0.03).cos() + 0.1 * heart;
vars[i] = 0.02 + 0.01 * (sc * 0.3).sin().abs()
+ if (t / 40) % 2 == 0 { 0.05 } else { 0.0 };
vmean += vars[i];
}
vmean /= N_SC as f32;
let v = CsiFrameView {
phases: &phases,
amplitudes: &amps,
variances: &vars,
prev_phases: &prev,
presence: if (t / 30) % 3 == 0 { 0 } else { 1 },
n_persons: ((t / 50) % 3) as i32,
motion_energy: 0.3 + 0.2 * (tf * 0.07).sin().abs(),
breathing_bpm: 18.0 + 2.0 * (tf * 0.01).sin(),
heartrate_bpm: 72.0 + 5.0 * (tf * 0.02).sin(),
coherence: 0.5 + 0.4 * (tf * 0.03).cos(),
variance_mean: vmean,
};
for e in pipeline.on_frame(&v) {
*counts.entry(e.skill).or_insert(0) += 1;
// Print the first few events from the last frame to show liveness.
if t == frames - 1 {
println!(
" frame {} | {:<26} event {:>3} = {:.4}",
t, e.skill, e.event_id, e.value
);
}
}
prev.copy_from_slice(&phases);
}
println!();
println!("=== per-skill event totals over {} synthetic frames ===", frames);
let total: usize = counts.values().sum();
let active = counts.values().filter(|&&c| c > 0).count();
for (name, c) in &counts {
println!(" {:<28} {}", name, c);
}
println!();
println!(
"TOTAL events: {} skills that emitted at least once: {}/{}",
total,
active,
pipeline.skill_count()
);
}
@@ -94,6 +94,18 @@ pub mod ind_structural_vibration;
pub mod vendor_common;
// ── Unified edge pipeline (ADR-160 deliverable) ──────────────────────────────
//
// `EdgePipeline` registers EVERY runtime skill module behind one uniform
// `EdgeSkill` trait and runs them all per CSI frame. Host-only (`std`): it uses
// Box/Vec for dynamic dispatch; the wasm `no_std` build keeps the small flagship
// pipeline in this file. The `med_*` tier is registered only under
// `medical-experimental` (preserves the ADR-160 safety gate).
#[cfg(feature = "std")]
pub mod pipeline_all;
#[cfg(feature = "std")]
pub mod skill_registry;
// ── Vendor-integrated modules (ADR-041 Category 7) ──────────────────────────
//
// 24 modules organised into 7 sub-categories. Each module file lives in
@@ -0,0 +1,217 @@
//! Unified edge pipeline — registers **every** runtime skill module in the crate
//! behind one uniform [`EdgeSkill`] trait and runs them all per CSI frame.
//!
//! # Why this module exists
//!
//! Each skill in `src/*.rs` is an independently-loadable DSP module with its own
//! bespoke `process_frame` / `on_timer` signature (some take `&[f32]` phases,
//! some scalars like `motion_energy`, some `breathing_bpm`/`heartrate_bpm`, etc.).
//! On the wasm target only the flagship `gesture + coherence + adversarial`
//! pipeline (in `lib.rs`) is on the default `on_frame` path. This module wires
//! **all** of them into a single [`EdgePipeline`] so a host can run the whole
//! skill library over one CSI frame stream and collect every emitted event,
//! tagged by its source skill.
//!
//! # Design
//!
//! - [`CsiFrameView`] — a borrowed, host-supplied view of one CSI frame carrying
//! every input any skill needs (phase/amplitude/variance slices + the scalar
//! features the host derives: presence, n_persons, motion_energy, breathing &
//! heart rate, coherence, plus the previous frame's phases for delta skills).
//! - [`EdgeSkill`] — the uniform adapter trait. Each skill gets a small adapter
//! (see `skill_registry`) that pulls the fields it needs out of the view, calls
//! the underlying detector **unchanged**, and returns an aggregated
//! `&[(i32, f32)]` event buffer. **No skill DSP is modified.**
//! - [`EdgePipeline`] — owns one boxed adapter per skill, dispatches `on_frame`
//! to all of them, and aggregates `(skill_name, event_id, value)` triples.
//!
//! # Feature gating (preserves the ADR-160 safety gate)
//!
//! The five `med_*` skills are registered **only** under
//! `--features medical-experimental`. They are NOT pulled into the default
//! pipeline, so they cannot be silently built into a shipping artifact. The
//! medical tier is opt-in; see `EdgePipeline::new` and `skills()`.
//!
//! Requires `std` (uses `Box`/`Vec`); the wasm `no_std` build keeps the small
//! flagship `lib.rs` pipeline instead.
#![cfg(feature = "std")]
extern crate std;
use std::boxed::Box;
use std::vec::Vec;
/// Borrowed view of one CSI frame: every input any registered skill can consume.
///
/// The host derives these from the Tier-2 DSP output. Slices are
/// per-subcarrier; scalars are frame-level aggregates. A skill adapter reads
/// only the fields it needs and ignores the rest — heterogeneity is absorbed
/// here, not in the skills.
#[derive(Clone, Copy)]
pub struct CsiFrameView<'a> {
/// Per-subcarrier unwrapped phase (radians).
pub phases: &'a [f32],
/// Per-subcarrier amplitude (linear).
pub amplitudes: &'a [f32],
/// Per-subcarrier short-window variance.
pub variances: &'a [f32],
/// Previous frame's phases (for delta/velocity skills like the spiking tracker).
pub prev_phases: &'a [f32],
/// Presence flag from host (0 = empty, 1 = occupied).
pub presence: i32,
/// Estimated person count from host.
pub n_persons: i32,
/// Frame-level motion energy.
pub motion_energy: f32,
/// Breathing rate estimate (breaths/min); 0 if unavailable.
pub breathing_bpm: f32,
/// Heart rate estimate (beats/min); 0 if unavailable.
pub heartrate_bpm: f32,
/// Coherence score [0,1] from the coherence monitor (for gate-style skills).
pub coherence: f32,
/// Mean variance across `variances` (convenience scalar for skills wanting one).
pub variance_mean: f32,
}
impl<'a> CsiFrameView<'a> {
/// Mean amplitude across the frame (convenience for scalar-input skills).
#[inline]
pub fn amplitude_mean(&self) -> f32 {
if self.amplitudes.is_empty() {
return 0.0;
}
let mut s = 0.0f32;
for &a in self.amplitudes {
s += a;
}
s / self.amplitudes.len() as f32
}
/// Mean phase across the frame.
#[inline]
pub fn phase_mean(&self) -> f32 {
if self.phases.is_empty() {
return 0.0;
}
let mut s = 0.0f32;
for &p in self.phases {
s += p;
}
s / self.phases.len() as f32
}
}
/// One emitted event, tagged by its source skill.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SkillEvent {
/// Stable name of the skill that produced this event (e.g. `"occupancy"`).
pub skill: &'static str,
/// Event type id (the registry id from `event_types`).
pub event_id: i32,
/// Event payload value.
pub value: f32,
}
/// Uniform adapter trait over a heterogeneous skill detector.
///
/// Implementors live in `skill_registry`; each wraps exactly one underlying
/// detector and forwards `on_frame` to its real `process_frame`/`on_timer`
/// without changing the DSP. `event_ids()` is introspection only.
pub trait EdgeSkill {
/// Stable skill name (matches the `src/<name>.rs` module).
fn name(&self) -> &'static str;
/// The event ids this skill can emit (for introspection / docs).
fn event_ids(&self) -> &'static [i32];
/// Run this skill over one frame, returning its emitted `(event_id, value)`
/// pairs. Returns an empty slice if the skill emitted nothing this frame.
fn on_frame(&mut self, frame: &CsiFrameView) -> &[(i32, f32)];
}
/// Introspection record for one registered skill.
#[derive(Clone, Copy, Debug)]
pub struct SkillInfo {
/// Skill name.
pub name: &'static str,
/// Event ids the skill can emit.
pub event_ids: &'static [i32],
/// Whether the skill is part of the gated `medical-experimental` tier.
pub medical_experimental: bool,
}
/// The unified pipeline: holds one adapter per registered skill and runs them
/// all per frame.
pub struct EdgePipeline {
skills: Vec<Box<dyn EdgeSkill>>,
/// Parallel flag marking which entries are the gated medical tier.
medical_flags: Vec<bool>,
frame_count: u64,
}
impl EdgePipeline {
/// Construct the pipeline with **every** registered skill.
///
/// The five `med_*` skills are included **only** when the crate is built
/// with `--features medical-experimental`; otherwise the default
/// (non-medical) tier is registered. This preserves the ADR-160 safety gate.
pub fn new() -> Self {
let mut skills: Vec<Box<dyn EdgeSkill>> = Vec::new();
let mut medical_flags: Vec<bool> = Vec::new();
crate::skill_registry::register_default(&mut skills, &mut medical_flags);
#[cfg(feature = "medical-experimental")]
crate::skill_registry::register_medical(&mut skills, &mut medical_flags);
Self {
skills,
medical_flags,
frame_count: 0,
}
}
/// Number of registered skills (default tier, or +medical if that feature is on).
pub fn skill_count(&self) -> usize {
self.skills.len()
}
/// Run every registered skill over one frame, aggregating all emitted events
/// tagged by source skill. Order matches registration order.
pub fn on_frame(&mut self, frame: &CsiFrameView) -> Vec<SkillEvent> {
self.frame_count += 1;
let mut out: Vec<SkillEvent> = Vec::new();
for skill in self.skills.iter_mut() {
let name = skill.name();
for &(event_id, value) in skill.on_frame(frame) {
out.push(SkillEvent {
skill: name,
event_id,
value,
});
}
}
out
}
/// Total frames processed so far.
pub fn frame_count(&self) -> u64 {
self.frame_count
}
/// Introspection: list every registered skill with its event ids and tier.
pub fn skills(&self) -> Vec<SkillInfo> {
let mut out = Vec::with_capacity(self.skills.len());
for (i, skill) in self.skills.iter().enumerate() {
out.push(SkillInfo {
name: skill.name(),
event_ids: skill.event_ids(),
medical_experimental: self.medical_flags.get(i).copied().unwrap_or(false),
});
}
out
}
}
impl Default for EdgePipeline {
fn default() -> Self {
Self::new()
}
}
@@ -0,0 +1,630 @@
//! Adapters wiring every runtime skill detector to the uniform [`EdgeSkill`]
//! trait, plus the registration functions consumed by [`EdgePipeline::new`].
//!
//! [`EdgePipeline::new`]: crate::pipeline_all::EdgePipeline::new
//! [`EdgeSkill`]: crate::pipeline_all::EdgeSkill
//!
//! # How adapters work
//!
//! Each underlying detector keeps its own bespoke `process_frame`/`on_timer`
//! signature and its owned `events: [(i32,f32); N]` buffer (the ADR-160 M6
//! soundness fix). An adapter holds the detector, implements [`EdgeSkill`], and
//! in `on_frame` simply pulls the needed fields out of [`CsiFrameView`] and
//! forwards the call **unchanged**. The detector returns `&self.events[..n]`;
//! the adapter forwards that borrow directly, so no extra buffer or copy is
//! needed for the common case.
//!
//! Three families need a small owned scratch buffer in the adapter instead of a
//! direct forward, because the underlying entry point does not itself return a
//! `&[(i32,f32)]`:
//! - `gesture` (`-> Option<u8>`), `coherence` (`-> f32`), `adversarial`
//! (`-> bool`): the adapter synthesizes a single tagged event.
//! - `sig_sparse_recovery` (`process_frame(&mut [f32])`): the adapter copies the
//! frame amplitudes into an owned scratch slice so the in-place ISTA recovery
//! never mutates the shared frame, then forwards the borrow.
//! - timer-driven skills (`vital_trend`, `lrn_meta_adapt`, `sig_temporal_compress`,
//! `tmp_goap_autonomy`, `tmp_pattern_sequence`): their `on_timer()` is driven
//! once per frame here (a frame *is* the tick at the edge), forwarding the
//! borrow. `tmp_pattern_sequence` additionally calls its `on_frame(...)`
//! accumulator first.
//!
//! **No skill's DSP is changed.** Only the call wiring lives here.
#![cfg(feature = "std")]
extern crate std;
use std::boxed::Box;
use std::vec::Vec;
use crate::pipeline_all::{CsiFrameView, EdgeSkill};
// ── Direct-forward adapter macro ─────────────────────────────────────────────
//
// Generates an adapter whose `on_frame` forwards directly to a detector method
// that already returns `&[(i32, f32)]`. `$call` is an expression over `self.0`
// (the detector) and `f` (the `&CsiFrameView`).
macro_rules! fwd_skill {
($adapter:ident, $detector:path, $name:literal, $ids:expr, |$d:ident, $f:ident| $call:expr) => {
pub struct $adapter($detector);
impl $adapter {
pub fn new() -> Self {
Self(<$detector>::new())
}
}
impl EdgeSkill for $adapter {
fn name(&self) -> &'static str {
$name
}
fn event_ids(&self) -> &'static [i32] {
&$ids
}
fn on_frame(&mut self, $f: &CsiFrameView) -> &[(i32, f32)] {
let $d = &mut self.0;
$call
}
}
};
}
// ── Synthesized-event adapter macro ──────────────────────────────────────────
//
// For detectors whose entry point does NOT return `&[(i32, f32)]`. The adapter
// owns a tiny scratch buffer; `$body` (over `self`, `f`, and `self.buf`/`self.n`)
// fills it and the trait returns the filled prefix.
macro_rules! synth_skill {
($adapter:ident, $detector:path, $name:literal, $ids:expr, $buf:literal,
|$s:ident, $f:ident| $body:block) => {
pub struct $adapter {
det: $detector,
buf: [(i32, f32); $buf],
n: usize,
}
impl $adapter {
pub fn new() -> Self {
Self {
det: <$detector>::new(),
buf: [(0, 0.0); $buf],
n: 0,
}
}
}
impl EdgeSkill for $adapter {
fn name(&self) -> &'static str {
$name
}
fn event_ids(&self) -> &'static [i32] {
&$ids
}
fn on_frame(&mut self, $f: &CsiFrameView) -> &[(i32, f32)] {
let $s = self;
$s.n = 0;
$body
&$s.buf[..$s.n]
}
}
};
}
use crate::event_types as ev;
// ── Flagship (synthesized) ───────────────────────────────────────────────────
synth_skill!(GestureAdapter, crate::gesture::GestureDetector, "gesture",
[ev::GESTURE_DETECTED], 1, |s, f| {
if let Some(id) = s.det.process_frame(f.phases) {
s.buf[0] = (ev::GESTURE_DETECTED, id as f32);
s.n = 1;
}
});
synth_skill!(CoherenceAdapter, crate::coherence::CoherenceMonitor, "coherence",
[ev::COHERENCE_SCORE], 1, |s, f| {
let score = s.det.process_frame(f.phases);
s.buf[0] = (ev::COHERENCE_SCORE, score);
s.n = 1;
});
synth_skill!(AdversarialAdapter, crate::adversarial::AnomalyDetector, "adversarial",
[ev::ANOMALY_DETECTED], 1, |s, f| {
if s.det.process_frame(f.phases, f.amplitudes) {
s.buf[0] = (ev::ANOMALY_DETECTED, 1.0);
s.n = 1;
}
});
// ── sig_sparse_recovery (needs owned mutable amplitude scratch) ───────────────
const SPARSE_SC: usize = 64;
pub struct SparseRecoveryAdapter {
det: crate::sig_sparse_recovery::SparseRecovery,
scratch: [f32; SPARSE_SC],
}
impl SparseRecoveryAdapter {
pub fn new() -> Self {
Self {
det: crate::sig_sparse_recovery::SparseRecovery::new(),
scratch: [0.0; SPARSE_SC],
}
}
}
impl EdgeSkill for SparseRecoveryAdapter {
fn name(&self) -> &'static str {
"sig_sparse_recovery"
}
fn event_ids(&self) -> &'static [i32] {
&[ev::RECOVERY_COMPLETE, ev::RECOVERY_ERROR, ev::DROPOUT_RATE]
}
fn on_frame(&mut self, f: &CsiFrameView) -> &[(i32, f32)] {
let n = f.amplitudes.len().min(SPARSE_SC);
self.scratch[..n].copy_from_slice(&f.amplitudes[..n]);
self.det.process_frame(&mut self.scratch[..n])
}
}
// ── Standard direct-forward skills (return &[(i32,f32)]) ─────────────────────
fwd_skill!(AisBehavioralAdapter, crate::ais_behavioral_profiler::BehavioralProfiler,
"ais_behavioral_profiler",
[ev::BEHAVIOR_ANOMALY, ev::PROFILE_DEVIATION, ev::NOVEL_PATTERN, ev::PROFILE_MATURITY],
|d, f| d.process_frame(f.presence != 0, f.motion_energy, f.n_persons.max(0) as u8));
fwd_skill!(AisPromptShieldAdapter, crate::ais_prompt_shield::PromptShield,
"ais_prompt_shield",
[ev::REPLAY_ATTACK, ev::INJECTION_DETECTED, ev::JAMMING_DETECTED, ev::SIGNAL_INTEGRITY],
|d, f| d.process_frame(f.phases, f.amplitudes));
fwd_skill!(AutPsychoAdapter, crate::aut_psycho_symbolic::PsychoSymbolicEngine,
"aut_psycho_symbolic",
[ev::INFERENCE_RESULT, ev::INFERENCE_CONFIDENCE, ev::RULE_FIRED, ev::CONTRADICTION],
|d, f| d.process_frame(f.presence as f32, f.motion_energy, f.breathing_bpm,
f.heartrate_bpm, f.n_persons as f32, 0.0));
fwd_skill!(AutMeshAdapter, crate::aut_self_healing_mesh::SelfHealingMesh,
"aut_self_healing_mesh",
[ev::NODE_DEGRADED, ev::MESH_RECONFIGURE, ev::COVERAGE_SCORE, ev::HEALING_COMPLETE],
|d, f| d.process_frame(f.variances));
fwd_skill!(BldElevatorAdapter, crate::bld_elevator_count::ElevatorCounter,
"bld_elevator_count",
[ev::ELEVATOR_COUNT, ev::DOOR_OPEN, ev::DOOR_CLOSE, ev::OVERLOAD_WARNING],
|d, f| d.process_frame(f.amplitudes, f.phases, f.motion_energy, f.n_persons));
fwd_skill!(BldEnergyAdapter, crate::bld_energy_audit::EnergyAuditor,
"bld_energy_audit",
[ev::SCHEDULE_SUMMARY, ev::AFTER_HOURS_ALERT, ev::UTILIZATION_RATE],
|d, f| d.process_frame(f.presence, f.n_persons));
fwd_skill!(BldHvacAdapter, crate::bld_hvac_presence::HvacPresenceDetector,
"bld_hvac_presence",
[ev::HVAC_OCCUPIED, ev::ACTIVITY_LEVEL, ev::DEPARTURE_COUNTDOWN],
|d, f| d.process_frame(f.presence as f32, f.motion_energy));
fwd_skill!(BldLightingAdapter, crate::bld_lighting_zones::LightingZoneController,
"bld_lighting_zones",
[ev::LIGHT_ON, ev::LIGHT_DIM, ev::LIGHT_OFF],
|d, f| d.process_frame(f.amplitudes, f.motion_energy));
fwd_skill!(BldMeetingAdapter, crate::bld_meeting_room::MeetingRoomTracker,
"bld_meeting_room",
[ev::MEETING_START, ev::MEETING_END, ev::PEAK_HEADCOUNT, ev::ROOM_AVAILABLE],
|d, f| d.process_frame(f.presence, f.n_persons, f.motion_energy));
fwd_skill!(ExoBreathingSyncAdapter, crate::exo_breathing_sync::BreathingSyncDetector,
"exo_breathing_sync",
[ev::SYNC_DETECTED, ev::SYNC_PAIR_COUNT, ev::GROUP_COHERENCE, ev::SYNC_LOST],
|d, f| d.process_frame(f.phases, f.variances, f.breathing_bpm, f.n_persons));
fwd_skill!(ExoEmotionAdapter, crate::exo_emotion_detect::EmotionDetector,
"exo_emotion_detect",
[ev::AROUSAL_LEVEL, ev::STRESS_INDEX, ev::CALM_DETECTED, ev::AGITATION_DETECTED],
|d, f| d.process_frame(f.breathing_bpm, f.heartrate_bpm, f.motion_energy,
f.phase_mean(), f.variance_mean));
fwd_skill!(ExoDreamAdapter, crate::exo_dream_stage::DreamStageDetector,
"exo_dream_stage",
[ev::SLEEP_STAGE, ev::SLEEP_QUALITY, ev::REM_EPISODE, ev::DEEP_SLEEP_RATIO],
|d, f| d.process_frame(f.breathing_bpm, f.heartrate_bpm, f.motion_energy,
f.phase_mean(), f.variance_mean, f.presence));
fwd_skill!(ExoGestureLangAdapter, crate::exo_gesture_language::GestureLanguageDetector,
"exo_gesture_language",
[ev::LETTER_RECOGNIZED, ev::LETTER_CONFIDENCE, ev::WORD_BOUNDARY, ev::GESTURE_REJECTED],
|d, f| d.process_frame(f.phases, f.amplitudes, f.variance_mean, f.motion_energy, f.presence));
fwd_skill!(ExoGhostAdapter, crate::exo_ghost_hunter::GhostHunterDetector,
"exo_ghost_hunter",
[ev::EXO_ANOMALY_DETECTED, ev::EXO_ANOMALY_CLASS, ev::HIDDEN_PRESENCE, ev::ENVIRONMENTAL_DRIFT],
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.presence, f.motion_energy));
fwd_skill!(ExoHappinessAdapter, crate::exo_happiness_score::HappinessScoreDetector,
"exo_happiness_score",
[ev::HAPPINESS_SCORE, ev::GAIT_ENERGY, ev::AFFECT_VALENCE, ev::SOCIAL_ENERGY, ev::TRANSIT_DIRECTION],
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.presence,
f.motion_energy, f.breathing_bpm, f.heartrate_bpm));
fwd_skill!(ExoHyperbolicAdapter, crate::exo_hyperbolic_space::HyperbolicEmbedder,
"exo_hyperbolic_space",
[ev::HIERARCHY_LEVEL, ev::HYPERBOLIC_RADIUS, ev::LOCATION_LABEL],
|d, f| d.process_frame(f.amplitudes));
fwd_skill!(ExoMusicAdapter, crate::exo_music_conductor::MusicConductorDetector,
"exo_music_conductor",
[ev::CONDUCTOR_BPM, ev::BEAT_POSITION, ev::DYNAMIC_LEVEL, ev::GESTURE_CUTOFF, ev::GESTURE_FERMATA],
|d, f| d.process_frame(f.phase_mean(), f.amplitude_mean(), f.motion_energy, f.variance_mean));
fwd_skill!(ExoPlantAdapter, crate::exo_plant_growth::PlantGrowthDetector,
"exo_plant_growth",
[ev::GROWTH_RATE, ev::CIRCADIAN_PHASE, ev::WILT_DETECTED, ev::WATERING_EVENT],
|d, f| d.process_frame(f.amplitudes, f.phases, f.variances, f.presence));
fwd_skill!(ExoRainAdapter, crate::exo_rain_detect::RainDetector,
"exo_rain_detect",
[ev::RAIN_ONSET, ev::RAIN_INTENSITY, ev::RAIN_CESSATION],
|d, f| d.process_frame(f.phases, f.variances, f.amplitudes, f.presence));
fwd_skill!(ExoTimeCrystalAdapter, crate::exo_time_crystal::TimeCrystalDetector,
"exo_time_crystal",
[ev::CRYSTAL_DETECTED, ev::CRYSTAL_STABILITY, ev::COORDINATION_INDEX],
|d, f| d.process_frame(f.motion_energy));
fwd_skill!(IndCleanRoomAdapter, crate::ind_clean_room::CleanRoomMonitor,
"ind_clean_room",
[ev::OCCUPANCY_COUNT, ev::OCCUPANCY_VIOLATION, ev::TURBULENT_MOTION, ev::COMPLIANCE_REPORT],
|d, f| d.process_frame(f.n_persons, f.presence, f.motion_energy));
fwd_skill!(IndConfinedAdapter, crate::ind_confined_space::ConfinedSpaceMonitor,
"ind_confined_space",
[ev::WORKER_ENTRY, ev::WORKER_EXIT, ev::BREATHING_OK, ev::EXTRACTION_ALERT, ev::IMMOBILE_ALERT],
|d, f| d.process_frame(f.presence, f.breathing_bpm, f.motion_energy, f.variance_mean));
fwd_skill!(IndForkliftAdapter, crate::ind_forklift_proximity::ForkliftProximityDetector,
"ind_forklift_proximity",
[ev::PROXIMITY_WARNING, ev::VEHICLE_DETECTED, ev::HUMAN_NEAR_VEHICLE],
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.motion_energy, f.presence, f.n_persons));
fwd_skill!(IndLivestockAdapter, crate::ind_livestock_monitor::LivestockMonitor,
"ind_livestock_monitor",
[ev::ANIMAL_PRESENT, ev::ABNORMAL_STILLNESS, ev::LABORED_BREATHING, ev::ESCAPE_ALERT],
|d, f| d.process_frame(f.presence, f.breathing_bpm, f.motion_energy, f.variance_mean));
fwd_skill!(IndVibrationAdapter, crate::ind_structural_vibration::StructuralVibrationMonitor,
"ind_structural_vibration",
[ev::SEISMIC_DETECTED, ev::MECHANICAL_RESONANCE, ev::STRUCTURAL_DRIFT, ev::VIBRATION_SPECTRUM],
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.presence));
fwd_skill!(IntrusionAdapter, crate::intrusion::IntrusionDetector,
"intrusion",
[ev::INTRUSION_ALERT, ev::INTRUSION_ZONE, 202],
|d, f| d.process_frame(f.phases, f.amplitudes));
fwd_skill!(LrnAttractorAdapter, crate::lrn_anomaly_attractor::AttractorDetector,
"lrn_anomaly_attractor",
[ev::ATTRACTOR_TYPE, ev::LYAPUNOV_EXPONENT, ev::BASIN_DEPARTURE, ev::LEARNING_COMPLETE],
|d, f| d.process_frame(f.phases, f.amplitudes, f.motion_energy));
fwd_skill!(LrnDtwAdapter, crate::lrn_dtw_gesture_learn::GestureLearner,
"lrn_dtw_gesture_learn",
[ev::GESTURE_LEARNED, ev::GESTURE_MATCHED, ev::LRN_MATCH_DISTANCE, ev::TEMPLATE_COUNT],
|d, f| d.process_frame(f.phases, f.motion_energy));
fwd_skill!(LrnEwcAdapter, crate::lrn_ewc_lifelong::EwcLifelong,
"lrn_ewc_lifelong",
[ev::KNOWLEDGE_RETAINED, ev::NEW_TASK_LEARNED, ev::FISHER_UPDATE, ev::FORGETTING_RISK],
|d, f| d.process_frame(f.variances, f.presence));
fwd_skill!(OccupancyAdapter, crate::occupancy::OccupancyDetector,
"occupancy",
[ev::ZONE_OCCUPIED, ev::ZONE_COUNT, ev::ZONE_TRANSITION],
|d, f| d.process_frame(f.phases, f.amplitudes));
fwd_skill!(QntInterferenceAdapter, crate::qnt_interference_search::InterferenceSearch,
"qnt_interference_search",
[ev::HYPOTHESIS_WINNER, ev::HYPOTHESIS_AMPLITUDE, ev::SEARCH_ITERATIONS],
|d, f| d.process_frame(f.presence, f.motion_energy, f.n_persons));
fwd_skill!(QntCoherenceAdapter, crate::qnt_quantum_coherence::QuantumCoherenceMonitor,
"qnt_quantum_coherence",
[ev::ENTANGLEMENT_ENTROPY, ev::DECOHERENCE_EVENT, ev::BLOCH_DRIFT],
|d, f| d.process_frame(f.phases));
fwd_skill!(RetFlowAdapter, crate::ret_customer_flow::CustomerFlowTracker,
"ret_customer_flow",
[ev::INGRESS, ev::EGRESS, ev::NET_OCCUPANCY, ev::HOURLY_TRAFFIC],
|d, f| d.process_frame(f.phases, f.amplitudes, f.variance_mean, f.motion_energy));
fwd_skill!(RetDwellAdapter, crate::ret_dwell_heatmap::DwellHeatmapTracker,
"ret_dwell_heatmap",
[ev::DWELL_ZONE_UPDATE, ev::HOT_ZONE, ev::COLD_ZONE, ev::SESSION_SUMMARY],
|d, f| d.process_frame(f.presence, f.variances, f.motion_energy, f.n_persons));
fwd_skill!(RetQueueAdapter, crate::ret_queue_length::QueueLengthEstimator,
"ret_queue_length",
[ev::QUEUE_LENGTH, ev::WAIT_TIME_ESTIMATE, ev::SERVICE_RATE, ev::QUEUE_ALERT],
|d, f| d.process_frame(f.presence, f.n_persons, f.variance_mean, f.motion_energy));
fwd_skill!(RetShelfAdapter, crate::ret_shelf_engagement::ShelfEngagementDetector,
"ret_shelf_engagement",
[ev::SHELF_BROWSE, ev::SHELF_CONSIDER, ev::SHELF_ENGAGE, ev::REACH_DETECTED],
|d, f| d.process_frame(f.presence, f.motion_energy, f.variance_mean, f.phases));
fwd_skill!(RetTableAdapter, crate::ret_table_turnover::TableTurnoverTracker,
"ret_table_turnover",
[ev::TABLE_SEATED, ev::TABLE_VACATED, ev::TABLE_AVAILABLE, ev::TURNOVER_RATE],
|d, f| d.process_frame(f.presence, f.motion_energy, f.n_persons));
fwd_skill!(SecLoiteringAdapter, crate::sec_loitering::LoiteringDetector,
"sec_loitering",
[ev::LOITERING_START, ev::LOITERING_ONGOING, ev::LOITERING_END],
|d, f| d.process_frame(f.presence, f.motion_energy));
fwd_skill!(SecPanicAdapter, crate::sec_panic_motion::PanicMotionDetector,
"sec_panic_motion",
[ev::PANIC_DETECTED, ev::STRUGGLE_PATTERN, ev::FLEEING_DETECTED],
|d, f| d.process_frame(f.motion_energy, f.variance_mean, f.phase_mean(), f.presence));
fwd_skill!(SecPerimeterAdapter, crate::sec_perimeter_breach::PerimeterBreachDetector,
"sec_perimeter_breach",
[ev::PERIMETER_BREACH, ev::APPROACH_DETECTED, ev::DEPARTURE_DETECTED, ev::SEC_ZONE_TRANSITION],
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.motion_energy));
fwd_skill!(SecTailgateAdapter, crate::sec_tailgating::TailgateDetector,
"sec_tailgating",
[ev::TAILGATE_DETECTED, ev::SINGLE_PASSAGE, ev::MULTI_PASSAGE],
|d, f| d.process_frame(f.motion_energy, f.presence, f.n_persons, f.variance_mean));
fwd_skill!(SecWeaponAdapter, crate::sec_weapon_detect::WeaponDetector,
"sec_weapon_detect",
[ev::METAL_ANOMALY, ev::HIGH_METAL_REFLECTIVITY, ev::CALIBRATION_NEEDED],
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.motion_energy, f.presence));
fwd_skill!(SigCoherenceGateAdapter, crate::sig_coherence_gate::CoherenceGate,
"sig_coherence_gate",
[ev::GATE_DECISION, ev::SIG_COHERENCE_SCORE, ev::RECALIBRATE_NEEDED],
|d, f| d.process_frame(f.phases));
fwd_skill!(SigFlashAttnAdapter, crate::sig_flash_attention::FlashAttention,
"sig_flash_attention",
[ev::ATTENTION_PEAK_SC, ev::ATTENTION_SPREAD, ev::SPATIAL_FOCUS_ZONE],
|d, f| d.process_frame(f.phases, f.amplitudes));
fwd_skill!(SigMincutAdapter, crate::sig_mincut_person_match::PersonMatcher,
"sig_mincut_person_match",
[ev::PERSON_ID_ASSIGNED, ev::PERSON_ID_SWAP, ev::MATCH_CONFIDENCE],
|d, f| d.process_frame(f.amplitudes, f.variances, f.n_persons.max(0) as usize));
fwd_skill!(SigTransportAdapter, crate::sig_optimal_transport::OptimalTransportDetector,
"sig_optimal_transport",
[ev::WASSERSTEIN_DISTANCE, ev::DISTRIBUTION_SHIFT, ev::SUBTLE_MOTION],
|d, f| d.process_frame(f.amplitudes));
fwd_skill!(SptHnswAdapter, crate::spt_micro_hnsw::MicroHnsw,
"spt_micro_hnsw",
[ev::NEAREST_MATCH_ID, ev::HNSW_MATCH_DISTANCE, ev::CLASSIFICATION, ev::LIBRARY_SIZE],
|d, f| d.process_frame(f.variances));
fwd_skill!(SptPagerankAdapter, crate::spt_pagerank_influence::PageRankInfluence,
"spt_pagerank_influence",
[ev::DOMINANT_PERSON, ev::INFLUENCE_SCORE, ev::INFLUENCE_CHANGE],
|d, f| d.process_frame(f.phases, f.n_persons.max(0) as usize));
fwd_skill!(SptSpikingAdapter, crate::spt_spiking_tracker::SpikingTracker,
"spt_spiking_tracker",
[ev::TRACK_UPDATE, ev::TRACK_VELOCITY, ev::SPIKE_RATE, ev::TRACK_LOST],
|d, f| d.process_frame(f.phases, f.prev_phases));
fwd_skill!(TmpLogicGuardAdapter, crate::tmp_temporal_logic_guard::TemporalLogicGuard,
"tmp_temporal_logic_guard",
[ev::LTL_VIOLATION, ev::LTL_SATISFACTION, ev::COUNTEREXAMPLE],
|d, f| {
let input = crate::tmp_temporal_logic_guard::FrameInput {
presence: f.presence,
n_persons: f.n_persons,
motion_energy: f.motion_energy,
coherence: f.coherence,
breathing_bpm: f.breathing_bpm,
heartrate_bpm: f.heartrate_bpm,
fall_alert: false,
intrusion_alert: false,
person_id_active: f.n_persons > 0,
vital_signs_active: f.breathing_bpm > 0.0,
seizure_detected: false,
normal_gait: true,
};
d.on_frame(&input)
});
// ── Timer-driven skills (driven once per frame) ──────────────────────────────
fwd_skill!(VitalTrendAdapter, crate::vital_trend::VitalTrendAnalyzer,
"vital_trend",
// 101-105 = brady/tachypnea, brady/tachycardia, apnea; 110/111 = breathing/heartrate
// moving averages (module-local EVENT_BREATHING_AVG / EVENT_HEARTRATE_AVG).
[ev::BRADYPNEA, ev::TACHYPNEA, ev::BRADYCARDIA, ev::TACHYCARDIA, ev::APNEA, 110, 111],
|d, f| d.on_timer(f.breathing_bpm, f.heartrate_bpm));
fwd_skill!(LrnMetaAdapter, crate::lrn_meta_adapt::MetaAdapter,
"lrn_meta_adapt",
[ev::PARAM_ADJUSTED, ev::ADAPTATION_SCORE, ev::ROLLBACK_TRIGGERED, ev::META_LEVEL],
|d, _f| d.on_timer());
fwd_skill!(SigTemporalCompressAdapter, crate::sig_temporal_compress::TemporalCompressor,
"sig_temporal_compress",
[ev::COMPRESSION_RATIO, ev::TIER_TRANSITION, ev::HISTORY_DEPTH_HOURS],
|d, _f| d.on_timer());
fwd_skill!(TmpGoapAdapter, crate::tmp_goap_autonomy::GoapPlanner,
"tmp_goap_autonomy",
[ev::GOAL_SELECTED, ev::MODULE_ACTIVATED, ev::MODULE_DEACTIVATED, ev::PLAN_COST],
|d, _f| d.on_timer());
// tmp_pattern_sequence: accumulate via on_frame, then drive on_timer per frame.
pub struct TmpPatternAdapter(crate::tmp_pattern_sequence::PatternSequenceAnalyzer);
impl TmpPatternAdapter {
pub fn new() -> Self {
Self(crate::tmp_pattern_sequence::PatternSequenceAnalyzer::new())
}
}
impl EdgeSkill for TmpPatternAdapter {
fn name(&self) -> &'static str {
"tmp_pattern_sequence"
}
fn event_ids(&self) -> &'static [i32] {
&[ev::PATTERN_DETECTED, ev::PATTERN_CONFIDENCE, ev::ROUTINE_DEVIATION, ev::PREDICTION_NEXT]
}
fn on_frame(&mut self, f: &CsiFrameView) -> &[(i32, f32)] {
self.0.on_frame(f.presence, f.motion_energy, f.n_persons);
self.0.on_timer()
}
}
// ── Medical tier (gated) ─────────────────────────────────────────────────────
#[cfg(feature = "medical-experimental")]
mod medical {
use super::*;
// Medical event ids verified against each module's local consts (100-199 block).
fwd_skill!(MedCardiacAdapter, crate::med_cardiac_arrhythmia::CardiacArrhythmiaDetector,
"med_cardiac_arrhythmia",
[110, 111, 112, 113],
|d, f| d.process_frame(f.heartrate_bpm, f.phase_mean()));
fwd_skill!(MedGaitAdapter, crate::med_gait_analysis::GaitAnalyzer,
"med_gait_analysis",
[130, 131, 132, 133, 134],
|d, f| d.process_frame(f.phase_mean(), f.amplitude_mean(), f.variance_mean, f.motion_energy));
fwd_skill!(MedRespiratoryAdapter, crate::med_respiratory_distress::RespiratoryDistressDetector,
"med_respiratory_distress",
[120, 121, 122, 123],
|d, f| d.process_frame(f.breathing_bpm, f.phase_mean(), f.variance_mean));
fwd_skill!(MedSeizureAdapter, crate::med_seizure_detect::SeizureDetector,
"med_seizure_detect",
[140, 141, 142, 143],
|d, f| d.process_frame(f.phase_mean(), f.amplitude_mean(), f.motion_energy, f.presence));
fwd_skill!(MedApneaAdapter, crate::med_sleep_apnea::SleepApneaDetector,
"med_sleep_apnea",
[100, 101, 102],
|d, f| d.process_frame(f.breathing_bpm, f.presence, f.variance_mean));
pub fn register(skills: &mut Vec<Box<dyn EdgeSkill>>, med: &mut Vec<bool>) {
macro_rules! push {
($a:ty) => {{
skills.push(Box::new(<$a>::new()));
med.push(true);
}};
}
push!(MedSeizureAdapter);
push!(MedCardiacAdapter);
push!(MedRespiratoryAdapter);
push!(MedApneaAdapter);
push!(MedGaitAdapter);
}
}
// ── Registration ─────────────────────────────────────────────────────────────
/// Register every default-tier (non-medical) skill.
pub fn register_default(skills: &mut Vec<Box<dyn EdgeSkill>>, med: &mut Vec<bool>) {
macro_rules! push {
($a:ty) => {{
skills.push(Box::new(<$a>::new()));
med.push(false);
}};
}
// Flagship + synthesized
push!(GestureAdapter);
push!(CoherenceAdapter);
push!(AdversarialAdapter);
push!(OccupancyAdapter);
push!(IntrusionAdapter);
push!(VitalTrendAdapter);
// Security
push!(SecPerimeterAdapter);
push!(SecWeaponAdapter);
push!(SecTailgateAdapter);
push!(SecLoiteringAdapter);
push!(SecPanicAdapter);
// Smart building
push!(BldHvacAdapter);
push!(BldLightingAdapter);
push!(BldElevatorAdapter);
push!(BldMeetingAdapter);
push!(BldEnergyAdapter);
// Retail
push!(RetQueueAdapter);
push!(RetDwellAdapter);
push!(RetFlowAdapter);
push!(RetTableAdapter);
push!(RetShelfAdapter);
// Industrial
push!(IndForkliftAdapter);
push!(IndConfinedAdapter);
push!(IndCleanRoomAdapter);
push!(IndLivestockAdapter);
push!(IndVibrationAdapter);
// Exotic / research
push!(ExoTimeCrystalAdapter);
push!(ExoHyperbolicAdapter);
push!(ExoDreamAdapter);
push!(ExoEmotionAdapter);
push!(ExoGestureLangAdapter);
push!(ExoMusicAdapter);
push!(ExoPlantAdapter);
push!(ExoGhostAdapter);
push!(ExoRainAdapter);
push!(ExoBreathingSyncAdapter);
push!(ExoHappinessAdapter);
// Signal intelligence
push!(SigCoherenceGateAdapter);
push!(SigFlashAttnAdapter);
push!(SigTemporalCompressAdapter);
push!(SparseRecoveryAdapter);
push!(SigMincutAdapter);
push!(SigTransportAdapter);
// Adaptive learning
push!(LrnDtwAdapter);
push!(LrnAttractorAdapter);
push!(LrnMetaAdapter);
push!(LrnEwcAdapter);
// Spatial reasoning
push!(SptPagerankAdapter);
push!(SptHnswAdapter);
push!(SptSpikingAdapter);
// Temporal analysis
push!(TmpPatternAdapter);
push!(TmpLogicGuardAdapter);
push!(TmpGoapAdapter);
// AI security
push!(AisPromptShieldAdapter);
push!(AisBehavioralAdapter);
// Quantum-inspired
push!(QntCoherenceAdapter);
push!(QntInterferenceAdapter);
// Autonomous systems
push!(AutPsychoAdapter);
push!(AutMeshAdapter);
let _ = (skills.len(), med.len());
}
/// Register the gated `medical-experimental` tier (5 `med_*` skills).
#[cfg(feature = "medical-experimental")]
pub fn register_medical(skills: &mut Vec<Box<dyn EdgeSkill>>, med: &mut Vec<bool>) {
medical::register(skills, med);
}
@@ -0,0 +1,208 @@
//! Integration test for the unified [`EdgePipeline`] (ADR-160 deliverable 1).
//!
//! Proves that EVERY registered skill executes over a deterministic synthetic
//! CSI frame sequence without panicking, that the aggregated event stream is
//! well-formed (each event tagged with a known skill name + a declared event
//! id), and pins the registered-skill count (default vs +medical-experimental).
//!
//! Run:
//! cargo test --features std --test pipeline_all
//! cargo test --features std,medical-experimental --test pipeline_all
//!
//! [`EdgePipeline`]: wifi_densepose_wasm_edge::pipeline_all::EdgePipeline
#![cfg(feature = "std")]
use wifi_densepose_wasm_edge::pipeline_all::{CsiFrameView, EdgePipeline};
const N_SC: usize = 32;
/// Deterministic synthetic frame: a moving breathing/heartbeat target plus
/// structured per-subcarrier phase/amplitude. No randomness — fully reproducible.
fn synth_frame(t: usize, phases: &mut [f32], amps: &mut [f32], vars: &mut [f32]) {
let tf = t as f32;
// 0.3 Hz breathing modulation @ 20 Hz frame rate -> period ~66 frames.
let breath = (tf * 2.0 * core::f32::consts::PI * 0.3 / 20.0).sin();
// 1.2 Hz heartbeat.
let heart = (tf * 2.0 * core::f32::consts::PI * 1.2 / 20.0).sin();
for i in 0..phases.len() {
let sc = i as f32;
phases[i] = (sc * 0.21 + tf * 0.05).sin() + 0.15 * breath;
amps[i] = 1.0 + 0.3 * (sc * 0.11 + tf * 0.03).cos() + 0.1 * heart;
// motion-correlated variance, with one occasionally-hot zone.
vars[i] = 0.02 + 0.01 * (sc * 0.3).sin().abs() + if (t / 40) % 2 == 0 { 0.05 } else { 0.0 };
}
}
/// Build a view over the supplied buffers for frame `t`.
fn view<'a>(
t: usize,
phases: &'a [f32],
amps: &'a [f32],
vars: &'a [f32],
prev_phases: &'a [f32],
) -> CsiFrameView<'a> {
let tf = t as f32;
let motion = 0.3 + 0.2 * (tf * 0.07).sin().abs();
let mut vmean = 0.0f32;
for &v in vars {
vmean += v;
}
vmean /= vars.len().max(1) as f32;
CsiFrameView {
phases,
amplitudes: amps,
variances: vars,
prev_phases,
presence: if (t / 30) % 3 == 0 { 0 } else { 1 },
n_persons: ((t / 50) % 3) as i32,
motion_energy: motion,
breathing_bpm: 18.0 + 2.0 * (tf * 0.01).sin(),
heartrate_bpm: 72.0 + 5.0 * (tf * 0.02).sin(),
coherence: 0.5 + 0.4 * (tf * 0.03).cos(),
variance_mean: vmean,
}
}
#[test]
fn all_skills_execute_without_panic_over_synthetic_stream() {
let mut pipeline = EdgePipeline::new();
let n_skills = pipeline.skill_count();
assert!(n_skills > 0, "pipeline must register skills");
let mut phases = [0.0f32; N_SC];
let mut amps = [0.0f32; N_SC];
let mut vars = [0.0f32; N_SC];
let mut prev_phases = [0.0f32; N_SC];
let known: std::collections::HashSet<&'static str> =
pipeline.skills().iter().map(|s| s.name).collect();
// Feed 300 frames (15 s @ 20 Hz) — enough for calibration windows, DTW
// enrollment, periodicity buffers, and timer cadences to fire.
let mut total_events = 0usize;
for t in 0..300 {
synth_frame(t, &mut phases, &mut amps, &mut vars);
let v = view(t, &phases, &amps, &vars, &prev_phases);
let events = pipeline.on_frame(&v);
for e in &events {
// Every event must be tagged with a registered skill name.
assert!(known.contains(e.skill), "unknown skill tag: {}", e.skill);
// Value must be finite (no NaN/Inf leaking from the DSP).
assert!(e.value.is_finite(), "non-finite value from {}", e.skill);
}
total_events += events.len();
prev_phases.copy_from_slice(&phases);
}
assert_eq!(pipeline.frame_count(), 300);
// A real run over 300 frames must emit *some* events across 59+ skills.
assert!(
total_events > 0,
"expected the skill library to emit events over 300 frames, got 0"
);
println!(
"pipeline: {} skills, {} aggregated events over 300 synthetic frames",
n_skills, total_events
);
}
#[test]
fn every_emitted_event_id_is_declared_by_its_skill() {
// Stronger well-formedness: each event's id must be one the producing skill
// declared in its `event_ids()` introspection list.
let mut pipeline = EdgePipeline::new();
// skill name -> its declared event id set
let mut declared: std::collections::HashMap<&'static str, std::collections::HashSet<i32>> =
std::collections::HashMap::new();
for s in pipeline.skills() {
declared.insert(s.name, s.event_ids.iter().copied().collect());
}
let mut phases = [0.0f32; N_SC];
let mut amps = [0.0f32; N_SC];
let mut vars = [0.0f32; N_SC];
let mut prev_phases = [0.0f32; N_SC];
for t in 0..300 {
synth_frame(t, &mut phases, &mut amps, &mut vars);
let v = view(t, &phases, &amps, &vars, &prev_phases);
for e in &pipeline.on_frame(&v) {
let set = declared.get(e.skill).expect("skill declared");
assert!(
set.contains(&e.event_id),
"{} emitted undeclared event id {}",
e.skill,
e.event_id
);
}
prev_phases.copy_from_slice(&phases);
}
}
#[test]
fn introspection_lists_every_skill_with_event_ids() {
let pipeline = EdgePipeline::new();
let infos = pipeline.skills();
assert_eq!(infos.len(), pipeline.skill_count());
for info in &infos {
assert!(!info.name.is_empty());
assert!(
!info.event_ids.is_empty(),
"skill {} declares no event ids",
info.name
);
}
// No duplicate skill names.
let names: std::collections::HashSet<_> = infos.iter().map(|i| i.name).collect();
assert_eq!(names.len(), infos.len(), "duplicate skill registration");
}
#[cfg(not(feature = "medical-experimental"))]
#[test]
fn default_tier_count_excludes_medical() {
let pipeline = EdgePipeline::new();
assert_eq!(
pipeline.skill_count(),
59,
"default (non-medical) tier must register exactly 59 skills"
);
// The ADR-160 safety gate: no med_* skill is present in the default build.
for info in pipeline.skills() {
assert!(
!info.medical_experimental,
"medical skill {} leaked into default tier",
info.name
);
assert!(
!info.name.starts_with("med_"),
"med_* skill {} present without the medical-experimental feature",
info.name
);
}
}
#[cfg(feature = "medical-experimental")]
#[test]
fn medical_tier_adds_five_skills() {
let pipeline = EdgePipeline::new();
assert_eq!(
pipeline.skill_count(),
64,
"default 59 + 5 medical = 64 skills"
);
let med: Vec<_> = pipeline
.skills()
.into_iter()
.filter(|s| s.medical_experimental)
.collect();
assert_eq!(med.len(), 5, "exactly 5 medical-experimental skills");
for m in &med {
assert!(
m.name.starts_with("med_"),
"medical-flagged skill has non-med_ name: {}",
m.name
);
}
}
@@ -0,0 +1,762 @@
//! Synthetic-ground-truth validation harness (ADR-160 deliverable 2).
//!
//! For the subset of edge skills whose detection target can be PLANTED with
//! known ground truth, we generate N signals with known answers, run the real
//! detector, and MEASURE detection rate / precision / recall / rate-error.
//!
//! # Honesty boundary
//!
//! This is **synthetic-ground-truth validation, NOT field accuracy.** A skill
//! that recovers a planted sinusoid here is proven to do the math it claims on
//! a constructed signal; it is NOT proven to work on real CSI in a real room.
//!
//! Skills whose detection target cannot be honestly planted on synthetic data
//! (clinical seizure/apnea/arrhythmia/gait, weapon discrimination, affect/
//! emotion/happiness, dream stage, sign language) are **NOT** validated here —
//! see RESULTS.md "DATA-GATED" section. Planting a "seizure-like" wiggle and
//! claiming the detector works validates nothing real.
//!
//! Run:
//! cargo test --features std --test synthetic_validation -- --nocapture
//!
//! The printed `MEASURED` lines are the source of `benchmarks/edge-skills/RESULTS.md`.
#![cfg(feature = "std")]
use std::f32::consts::PI;
// ── Confusion-matrix accumulator ─────────────────────────────────────────────
#[derive(Default, Clone, Copy)]
struct Confusion {
tp: u32,
fp: u32,
tn: u32,
fn_: u32,
}
impl Confusion {
fn observe(&mut self, predicted_positive: bool, actual_positive: bool) {
match (predicted_positive, actual_positive) {
(true, true) => self.tp += 1,
(true, false) => self.fp += 1,
(false, false) => self.tn += 1,
(false, true) => self.fn_ += 1,
}
}
fn precision(&self) -> f32 {
let d = self.tp + self.fp;
if d == 0 {
1.0
} else {
self.tp as f32 / d as f32
}
}
fn recall(&self) -> f32 {
let d = self.tp + self.fn_;
if d == 0 {
1.0
} else {
self.tp as f32 / d as f32
}
}
fn accuracy(&self) -> f32 {
let d = self.tp + self.fp + self.tn + self.fn_;
if d == 0 {
0.0
} else {
(self.tp + self.tn) as f32 / d as f32
}
}
fn report(&self, name: &str) {
println!(
"MEASURED-on-synthetic | {:<34} | acc={:.3} prec={:.3} recall={:.3} | TP={} FP={} TN={} FN={}",
name,
self.accuracy(),
self.precision(),
self.recall(),
self.tp,
self.fp,
self.tn,
self.fn_
);
}
}
// ── 1. vital_trend — rate-threshold detection (directly verified thresholds) ─
// Thresholds (from src/vital_trend.rs): BRADYPNEA<12, TACHYPNEA>25,
// BRADYCARDIA<50, TACHYCARDIA>120, APNEA at breathing<1.0 for 20 calls;
// ALERT_DEBOUNCE=5. Drive on_timer with known BPM, count event presence.
#[test]
fn vital_trend_rate_thresholds() {
use wifi_densepose_wasm_edge::vital_trend::VitalTrendAnalyzer;
// event ids: 101 brady-pnea, 102 tachy-pnea, 103 brady-cardia, 104 tachy-cardia, 105 apnea
fn drive_breathing(bpm: f32, n: u32) -> std::collections::HashSet<i32> {
let mut det = VitalTrendAnalyzer::new();
let mut seen = std::collections::HashSet::new();
for _ in 0..n {
for &(id, _) in det.on_timer(bpm, 72.0) {
seen.insert(id);
}
}
seen
}
fn drive_heart(bpm: f32, n: u32) -> std::collections::HashSet<i32> {
let mut det = VitalTrendAnalyzer::new();
let mut seen = std::collections::HashSet::new();
for _ in 0..n {
for &(id, _) in det.on_timer(16.0, bpm) {
seen.insert(id);
}
}
seen
}
// 6 calls > ALERT_DEBOUNCE(5) so a sustained abnormal value fires.
let mut c = Confusion::default();
// Bradypnea: <12 positive; normal 16 negative.
c.observe(drive_breathing(8.0, 6).contains(&101), true);
c.observe(drive_breathing(16.0, 6).contains(&101), false);
// Tachypnea: >25 positive; normal negative.
c.observe(drive_breathing(30.0, 6).contains(&102), true);
c.observe(drive_breathing(16.0, 6).contains(&102), false);
// Bradycardia: <50.
c.observe(drive_heart(40.0, 6).contains(&103), true);
c.observe(drive_heart(72.0, 6).contains(&103), false);
// Tachycardia: >120.
c.observe(drive_heart(140.0, 6).contains(&104), true);
c.observe(drive_heart(72.0, 6).contains(&104), false);
// Apnea: breathing < 1.0 for >= 20 calls.
c.observe(drive_breathing(0.0, 20).contains(&105), true);
c.observe(drive_breathing(0.0, 10).contains(&105), false); // only 10 calls -> below APNEA_SECONDS
c.report("vital_trend (brady/tachy-pnea/cardia, apnea)");
// All 5 thresholds + their negatives must classify correctly.
assert_eq!(c.accuracy(), 1.0, "vital_trend rate thresholds must be exact");
}
// ── 2. exo_time_crystal — period-doubling (sub-harmonic) detection ───────────
// Detects a peak at lag L AND a peak at lag 2L in motion-energy autocorrelation.
// PLANT positive: period-2 modulation (alternating amplitude on a base period)
// so autocorr has peaks at both L and 2L.
// PLANT negative: a single clean period (peak at L only) or noise.
fn run_time_crystal(motion: &[f32]) -> bool {
use wifi_densepose_wasm_edge::exo_time_crystal::TimeCrystalDetector;
let mut det = TimeCrystalDetector::new();
let mut detected = false;
for &m in motion {
for &(id, v) in det.process_frame(m) {
if id == 680 && v >= 2.0 {
detected = true; // CRYSTAL_DETECTED with multiplier 2
}
}
}
detected
}
#[test]
fn exo_time_crystal_period_doubling() {
let n = 256usize;
// Positive: period-2 subharmonic. Base period P=16; alternate full periods
// are scaled differently so the waveform only repeats every 2P=32 (peak at
// lag 32) while still correlating at P=16. Plain sine (no abs, which would
// itself fold frequency and fake a sub-harmonic).
let base_p = 16.0f32;
let mut pos = Vec::with_capacity(n);
for t in 0..n {
let phase = (t as f32) * 2.0 * PI / base_p;
let sub = if ((t as f32 / base_p) as i32) % 2 == 0 { 1.0 } else { 0.45 };
pos.push(0.6 + 0.35 * phase.sin() * sub);
}
// HONEST LIMIT (measured below): a *pure* periodic signal already has
// autocorrelation peaks at L AND 2L (natural harmonics), so this detector
// cannot separate a true period-2 sub-harmonic from a plain periodic signal.
// The construct it CAN discriminate with known ground truth is
// "periodic-with-coordination vs aperiodic". We validate that.
//
// Negative 1: incrementing-seed pseudo-noise (no periodicity).
let mut noise = Vec::with_capacity(n);
let mut s: u32 = 12345;
for _ in 0..n {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
noise.push(0.3 + 0.4 * ((s >> 8) & 0xffff) as f32 / 65535.0);
}
// Negative 2: near-constant motion (no oscillation at all).
let flat: Vec<f32> = (0..n).map(|t| 0.5 + 1e-4 * (t as f32 * 0.01).sin()).collect();
let mut c = Confusion::default();
c.observe(run_time_crystal(&pos), true); // planted period-2 -> detect
c.observe(run_time_crystal(&noise), false); // pseudo-noise -> reject
c.observe(run_time_crystal(&flat), false); // flat -> reject
c.report("exo_time_crystal (periodic-coordination vs aperiodic)");
assert!(
run_time_crystal(&pos),
"must detect planted period-2 coordinated motion"
);
assert!(
!run_time_crystal(&noise),
"must NOT fire on pseudo-noise"
);
assert!(!run_time_crystal(&flat), "must NOT fire on flat motion");
}
// ── 3. exo_ghost_hunter — hidden breathing (autocorr at breathing-range lag) ─
// When presence==0, aggregate phase is autocorrelated at lags 5..=15; a peak
// there above HIDDEN_PRESENCE_THRESHOLD(0.3) emits HIDDEN_PRESENCE(652).
// PLANT positive: phase sinusoid at a lag in [5,15] across an empty room.
// PLANT negative: flat phase (no periodic breathing signature).
fn run_ghost_hidden_breathing(period: f32, amp: f32, frames: usize) -> f32 {
use wifi_densepose_wasm_edge::exo_ghost_hunter::GhostHunterDetector;
let mut det = GhostHunterDetector::new();
let n_sc = 32usize;
let mut max_hidden = 0.0f32;
for t in 0..frames {
let breath = if period > 0.0 {
amp * (t as f32 * 2.0 * PI / period).sin()
} else {
0.0
};
let mut phases = [0.0f32; 32];
let mut amps = [0.0f32; 32];
let mut vars = [0.0f32; 32];
for i in 0..n_sc {
// breathing modulates phase uniformly (chest motion -> common phase shift)
phases[i] = 0.1 * (i as f32 * 0.2).sin() + breath;
amps[i] = 1.0;
vars[i] = 0.01;
}
// presence = 0 (empty room) is required for the hidden-breathing path.
for &(id, v) in det.process_frame(&phases, &amps, &vars, 0, 0.0) {
if id == 652 {
if v > max_hidden {
max_hidden = v;
}
}
}
}
max_hidden
}
#[test]
fn exo_ghost_hunter_hidden_breathing() {
// Period 8 frames is within the breathing lag window [5,15].
let pos = run_ghost_hidden_breathing(8.0, 0.5, 200);
// Flat phase (no breathing) -> no hidden-presence event.
let neg = run_ghost_hidden_breathing(0.0, 0.0, 200);
let mut c = Confusion::default();
c.observe(pos > 0.0, true);
c.observe(neg > 0.0, false);
c.report("exo_ghost_hunter (hidden breathing, lag 8)");
println!(
" detail: planted-breathing hidden-presence score={:.3}, flat-phase score={:.3}",
pos, neg
);
assert!(
pos > 0.3,
"planted breathing must score above HIDDEN_PRESENCE_THRESHOLD (0.3); got {}",
pos
);
assert!(
neg <= 0.0,
"flat phase must not emit hidden presence; got {}",
neg
);
}
// ── 4. occupancy — calibration + variance-driven zone occupancy ──────────────
// BASELINE_FRAMES=200 of low-variance amplitudes establish baseline; then
// high amplitude-variance per zone (score > ZONE_THRESHOLD=0.02) flips a zone
// to occupied (EVENT_ZONE_OCCUPIED=300).
#[test]
fn occupancy_variance_detection() {
use wifi_densepose_wasm_edge::occupancy::OccupancyDetector;
fn run(occupied_signal: bool) -> bool {
let mut det = OccupancyDetector::new();
let n_sc = 32usize;
let mut phases = [0.0f32; 32];
// Calibration: 220 frames of near-flat amplitudes (low variance).
for t in 0..220 {
let mut amps = [1.0f32; 32];
for i in 0..n_sc {
amps[i] = 1.0 + 1e-3 * ((t + i) as f32 * 0.7).sin();
phases[i] = 0.01 * (i as f32).sin();
}
det.process_frame(&phases, &amps);
}
// Test phase: 60 frames. If occupied, inject strong per-zone amplitude
// variance; else keep flat.
let mut fired = false;
for t in 0..60 {
let mut amps = [1.0f32; 32];
for i in 0..n_sc {
amps[i] = if occupied_signal {
// strong structured variance within each zone
1.0 + 2.0 * (((i % 4) as f32) - 1.5) + 0.5 * (t as f32 * 0.3 + i as f32).sin()
} else {
1.0 + 1e-3 * ((t + i) as f32 * 0.7).sin()
};
}
for &(id, _) in det.process_frame(&phases, &amps) {
if id == 300 {
fired = true;
}
}
}
fired
}
let mut c = Confusion::default();
c.observe(run(true), true);
c.observe(run(false), false);
c.report("occupancy (zone variance vs flat baseline)");
assert!(run(true), "high zone variance after calibration must occupy a zone");
assert!(!run(false), "flat amplitude must stay unoccupied");
}
// ── 5. intrusion — calibrate, arm, then disturbance>=0.8 alerts ──────────────
// disturbance = 0.6*frac(|Δphase|>1.5) + 0.4*frac(|Δamp|>3σ). Calibrate 200
// quiet frames, monitor 100 quiet frames -> Armed, then 3 frames of large
// phase+amp disturbance -> EVENT_INTRUSION_ALERT(200).
#[test]
fn intrusion_disturbance_alert() {
use wifi_densepose_wasm_edge::intrusion::IntrusionDetector;
fn run(intrude: bool) -> bool {
let mut det = IntrusionDetector::new();
let n_sc = 32usize;
// Calibration (200) + monitoring quiet (120) -> Armed. Quiet = constant.
for _ in 0..330 {
let phases = [0.5f32; 32];
let amps = [1.0f32; 32];
det.process_frame(&phases, &amps);
}
let mut alerted = false;
// 10 test frames.
for t in 0..10 {
let mut phases = [0.5f32; 32];
let mut amps = [1.0f32; 32];
if intrude {
for i in 0..n_sc {
// alternate phase by 3.0 (>1.5) and amplitude far from baseline 1.0.
phases[i] = if t % 2 == 0 { 0.5 } else { 4.0 };
amps[i] = 1.0 + 8.0; // huge deviation vs ~0 baseline variance
}
}
for &(id, _) in det.process_frame(&phases, &amps) {
if id == 200 {
alerted = true;
}
}
}
alerted
}
let mut c = Confusion::default();
c.observe(run(true), true);
c.observe(run(false), false);
c.report("intrusion (armed -> disturbance alert vs quiet)");
assert!(run(true), "large phase+amplitude disturbance must alert when armed");
assert!(!run(false), "quiet environment must not alert");
}
// ── 6. sig_sparse_recovery — ISTA recovery of planted null subcarriers ───────
// Initialize correlation on clean frames, then null >10% of subcarriers and
// MEASURE how well ISTA recovers them (rate-error style: recovery residual).
#[test]
fn sig_sparse_recovery_recovers_nulls() {
use wifi_densepose_wasm_edge::sig_sparse_recovery::SparseRecovery;
let mut det = SparseRecovery::new();
let n_sc = 32usize;
// Underlying smooth signal (neighbor-correlated) the model can learn.
let truth: Vec<f32> = (0..n_sc).map(|i| 1.0 + 0.5 * (i as f32 * 0.4).sin()).collect();
// Warm up correlation model with 30 clean frames.
for _ in 0..30 {
let mut amps: Vec<f32> = truth.clone();
det.process_frame(&mut amps);
}
// Null subcarriers 5..13 (8/32 = 25% > MIN_DROPOUT_RATE 0.10).
let mut amps: Vec<f32> = truth.clone();
let nulled: Vec<usize> = (5..13).collect();
for &i in &nulled {
amps[i] = 0.0;
}
// Baseline error if the nulls were left at 0.0 (unrecovered).
let mut sse0 = 0.0f32;
for &i in &nulled {
sse0 += truth[i] * truth[i];
}
let baseline_rmse = (sse0 / nulled.len() as f32).sqrt();
let mut recovery_seen = false;
for &(id, _) in det.process_frame(&mut amps) {
if id == 715 {
recovery_seen = true; // RECOVERY_COMPLETE
}
}
// Measure recovery error on the nulled positions (now written back in-place).
let mut sse = 0.0f32;
for &i in &nulled {
let d = amps[i] - truth[i];
sse += d * d;
}
let rmse = (sse / nulled.len() as f32).sqrt();
println!(
"MEASURED-on-synthetic | {:<34} | dropout-detect+recovery-trigger=PASS | recovered RMSE={:.4} vs unrecovered-null RMSE={:.4} ({:+.1}%) over {} nulled subcarriers",
"sig_sparse_recovery (ISTA)",
rmse,
baseline_rmse,
100.0 * (1.0 - rmse / baseline_rmse),
nulled.len()
);
// CONSTRUCTIBLE + MEASURED: the dropout detection and recovery-trigger
// pipeline fires correctly on >10% planted nulls. This is the validatable
// claim and we assert it.
assert!(recovery_seen, "dropout > 10% must trigger ISTA recovery (RECOVERY_COMPLETE)");
// HONEST MEASURED RESULT (reported, NOT asserted as a win): on this
// neighbor-correlated synthetic signal the tridiagonal-model ISTA recovery
// does NOT beat leaving the nulls at zero (RMSE ~1.00 vs ~0.98). The skill's
// *recovery accuracy* is therefore NOT validated as effective on synthetic
// data — only its dropout-detection/trigger path is. Reported in RESULTS.md.
assert!(
rmse.is_finite() && rmse < 5.0,
"recovered values must be finite and bounded; got {}",
rmse
);
}
// ── 7. exo_rain_detect — broadband variance onset (empty room) ───────────────
// presence=0, MIN_EMPTY_FRAMES=40 baseline, then >=6/8 groups with variance
// ratio > 2.5 for ONSET_FRAMES=10 -> EVENT_RAIN_ONSET(660).
#[test]
fn exo_rain_detect_broadband_onset() {
use wifi_densepose_wasm_edge::exo_rain_detect::RainDetector;
fn run(rain: bool) -> bool {
let mut det = RainDetector::new();
let n_sc = 32usize;
let phases = [0.1f32; 32];
let amps = [1.0f32; 32];
// 60 empty baseline frames with low variance.
for _ in 0..60 {
let vars = [0.001f32; 32];
det.process_frame(&phases, &vars, &amps, 0);
}
let mut onset = false;
// 40 frames: broadband-high variance if rain, else stay low.
for _ in 0..40 {
let vars = if rain { [0.5f32; 32] } else { [0.001f32; 32] };
for &(id, _) in det.process_frame(&phases, &vars, &amps, 0) {
if id == 660 {
onset = true;
}
}
}
let _ = n_sc;
onset
}
let mut c = Confusion::default();
c.observe(run(true), true);
c.observe(run(false), false);
c.report("exo_rain_detect (broadband variance onset)");
assert!(run(true), "broadband variance elevation must trigger rain onset");
assert!(!run(false), "stable low variance must not trigger rain");
}
// ── 8. sig_flash_attention — peak-attention subcarrier localization ──────────
// Q=mean(phase) per group, K=mean(prev_phase), score=Q*K/sqrt(8), softmax peak.
// Plant a sustained large phase in a KNOWN group -> assert that group becomes
// the reported attention peak (EVENT_ATTENTION_PEAK_SC=700).
#[test]
fn sig_flash_attention_peak_localization() {
use wifi_densepose_wasm_edge::sig_flash_attention::FlashAttention;
fn peak_for_group(target_group: usize) -> i32 {
let mut det = FlashAttention::new();
let n_sc = 32usize;
let subs_per = n_sc / 8;
let mut last_peak = -1;
// Sustain the spike so both Q (this frame) and K (prev frame) are large
// in the target group -> highest score there.
for _ in 0..20 {
let mut phases = [0.05f32; 32];
let mut amps = [1.0f32; 32];
for i in (target_group * subs_per)..((target_group + 1) * subs_per) {
phases[i] = 3.0;
amps[i] = 3.0;
}
for &(id, v) in det.process_frame(&phases, &amps) {
if id == 700 {
last_peak = v as i32;
}
}
}
last_peak
}
let mut correct = 0u32;
let total = 8u32;
for g in 0..8usize {
let got = peak_for_group(g);
if got == g as i32 {
correct += 1;
}
println!(" flash_attention: planted group {} -> reported peak {}", g, got);
}
let acc = correct as f32 / total as f32;
println!(
"MEASURED-on-synthetic | {:<34} | peak-localization accuracy = {}/{} = {:.3}",
"sig_flash_attention", correct, total, acc
);
assert!(acc >= 0.75, "must localize the planted attention group in >=75% of cases; got {}", acc);
}
// ── 9. spt_spiking_tracker — phase-delta zone localization ───────────────────
// LIF neurons fire on |phase - prev_phase|; zone with most spikes is tracked
// (EVENT_TRACK_UPDATE=770 carries zone id). Plant motion in a KNOWN zone.
#[test]
fn spt_spiking_tracker_zone_localization() {
use wifi_densepose_wasm_edge::spt_spiking_tracker::SpikingTracker;
fn track_zone(target_zone: usize) -> i32 {
let mut det = SpikingTracker::new();
let n_sc = 32usize;
let per = n_sc / 4; // 4 zones of 8 subcarriers
let mut prev = [0.0f32; 32];
let mut last_zone = -1;
// SPARSE plant: each zone's output neuron sums home-weight 1.0 + cross
// 0.25. Firing all 8 inputs (8*0.25=2.0) overdrives EVERY zone, so the
// tracker collapses to zone 0. Firing only 2 inputs in the target zone
// gives potential 2.0 at home (fires) but 0.5 cross (silent) -> only the
// target zone fires. This is the genuinely-constructible localization.
let base = target_zone * per;
for t in 0..60 {
let mut phases = [0.0f32; 32];
// 2 subcarriers in the target zone get a large alternating delta.
for k in 0..2 {
phases[base + k] = if t % 2 == 0 { 0.0 } else { 3.0 };
}
for &(id, v) in det.process_frame(&phases, &prev) {
if id == 770 {
last_zone = v as i32;
}
}
prev.copy_from_slice(&phases);
}
last_zone
}
let mut correct = 0u32;
for z in 0..4usize {
let got = track_zone(z);
if got == z as i32 {
correct += 1;
}
println!(" spiking_tracker: planted zone {} -> tracked zone {}", z, got);
}
let acc = correct as f32 / 4.0;
println!(
"MEASURED-on-synthetic | {:<34} | zone-localization accuracy = {}/4 = {:.3}",
"spt_spiking_tracker", correct, acc
);
assert!(acc >= 0.75, "must track the planted motion zone in >=75% of cases; got {}", acc);
}
// ── 10. sig_optimal_transport — distribution-shift detection ─────────────────
// Sliced Wasserstein over amplitudes; sustained shift > WASS_SHIFT(0.25) for
// SHIFT_DEB(3) -> EVENT_DISTRIBUTION_SHIFT(726). Plant a large vs no shift.
#[test]
fn sig_optimal_transport_distribution_shift() {
use wifi_densepose_wasm_edge::sig_optimal_transport::OptimalTransportDetector;
fn run(shift: bool) -> bool {
let mut det = OptimalTransportDetector::new();
let n_sc = 32usize;
// Establish a reference distribution.
let base: Vec<f32> = (0..n_sc).map(|i| i as f32 * 0.1).collect();
for _ in 0..10 {
let mut a = base.clone();
det.process_frame(&mut a);
}
let mut shifted = false;
// The detector compares each frame to the PREVIOUS frame (prev_amps is
// updated every frame), so a one-time jump decays. To exceed WASS_SHIFT
// (0.25) for SHIFT_DEB(3) consecutive frames we need a sustained large
// frame-to-frame change: alternate between two very different
// distributions each frame.
for t in 0..15 {
let mut a: Vec<f32> = if shift {
if t % 2 == 0 {
base.clone()
} else {
base.iter().map(|x| 10.0 - x).collect() // reversed + offset
}
} else {
base.clone()
};
for &(id, _) in det.process_frame(&mut a) {
if id == 726 {
shifted = true;
}
}
}
shifted
}
let mut c = Confusion::default();
c.observe(run(true), true);
c.observe(run(false), false);
c.report("sig_optimal_transport (distribution shift)");
assert!(run(true), "large amplitude-distribution shift must be detected");
assert!(!run(false), "stationary distribution must not flag a shift");
}
// ── 11. lrn_dtw_gesture_learn — enroll a template, replay match vs reject ────
// STILLNESS_FRAMES=60 stillness, then 3 rehearsals of the same gesture
// (motion->stillness) -> EVENT_GESTURE_LEARNED(730). Replaying the learned
// gesture later (in Idle) -> EVENT_GESTURE_MATCHED(731); replaying a different
// gesture -> no match.
#[test]
fn lrn_dtw_gesture_learn_enroll_and_match() {
use wifi_densepose_wasm_edge::lrn_dtw_gesture_learn::GestureLearner;
// A gesture is a phase trajectory across frames; motion_energy gates the
// enroll state machine (still < 0.05, moving >= 0.05).
fn gesture_frame(kind: u8, step: usize) -> ([f32; 32], f32) {
let mut phases = [0.0f32; 32];
let s = step as f32;
for i in 0..32 {
phases[i] = match kind {
// distinct trajectories
0 => (s * 0.4 + i as f32 * 0.1).sin(),
_ => (s * 0.9 + i as f32 * 0.05).cos() * 1.5,
};
}
(phases, 0.5) // moving
}
let mut det = GestureLearner::new();
let still = ([0.0f32; 32], 0.0f32);
// helper to feed N still frames
let feed_still = |det: &mut GestureLearner, n: usize| {
for _ in 0..n {
det.process_frame(&still.0, still.1);
}
};
let feed_gesture = |det: &mut GestureLearner, kind: u8, len: usize| -> bool {
let mut learned = false;
for s in 0..len {
let (ph, me) = gesture_frame(kind, s);
for &(id, _) in det.process_frame(&ph, me) {
if id == 730 {
learned = true;
}
}
}
learned
};
// Enroll gesture kind 0: stillness, then 3 identical rehearsals (each
// motion burst followed by stillness).
feed_still(&mut det, 70);
let mut any_learned = false;
for _ in 0..3 {
any_learned |= feed_gesture(&mut det, 0, 30);
feed_still(&mut det, 70);
}
// Replay the SAME gesture during Idle -> expect a match (731).
let mut matched_same = false;
for s in 0..30 {
let (ph, me) = gesture_frame(0, s);
for &(id, _) in det.process_frame(&ph, me) {
if id == 731 {
matched_same = true;
}
}
}
feed_still(&mut det, 70);
// Replay a DIFFERENT gesture -> ideally no match (731) to the learned one.
let mut matched_diff = false;
for s in 0..30 {
let (ph, me) = gesture_frame(1, s);
for &(id, _) in det.process_frame(&ph, me) {
if id == 731 {
matched_diff = true;
}
}
}
let tmpl_count = det.template_count();
println!(
"MEASURED-on-synthetic | {:<34} | learned_event={} templates={} match_same={} match_different={}",
"lrn_dtw_gesture_learn", any_learned, tmpl_count, matched_same, matched_diff
);
// The enroll path must complete (a template is learned from 3 identical
// rehearsals). Whether the precise replay matches is the DTW behavior we
// measure and report; we assert the deterministic enrollment.
assert!(
any_learned || tmpl_count > 0,
"3 identical rehearsals after stillness must enroll a template"
);
}
// ── 12. sig_mincut_person_match — stable id assignment for distinct signatures ─
// Per-person feature = top-FEAT_DIM variances in that person's spatial region.
// Two persons with DISTINCT, stable variance signatures should get stable ids
// (EVENT_PERSON_ID_ASSIGNED=720) with zero swaps across frames.
#[test]
fn sig_mincut_person_stable_ids() {
use wifi_densepose_wasm_edge::sig_mincut_person_match::PersonMatcher;
let mut det = PersonMatcher::new();
let n_sc = 32usize;
let amplitudes = [1.0f32; 32];
let mut swaps = 0u32;
let mut assigned = false;
// 40 frames, 2 persons: person 0 region (0..16) high-variance signature,
// person 1 region (16..32) low-variance signature, both stable.
for _ in 0..40 {
let mut variances = [0.0f32; 32];
for i in 0..n_sc {
variances[i] = if i < 16 {
2.0 + 0.05 * (i as f32).sin()
} else {
0.2 + 0.01 * (i as f32).cos()
};
}
for &(id, _) in det.process_frame(&amplitudes, &variances, 2) {
if id == 720 {
assigned = true;
}
if id == 721 {
swaps += 1;
}
}
}
println!(
"MEASURED-on-synthetic | {:<34} | assigned={} id_swaps_over_40_frames={}",
"sig_mincut_person_match", assigned, swaps
);
assert!(assigned, "distinct stable signatures must assign person ids");
assert!(swaps == 0, "stable distinct signatures must not swap ids; got {} swaps", swaps);
}