Follow-up to the calibration deadlock fix. With the status gate unstuck,
maybe_feed_calibration reached feed_calibration, but a real ESP32 HT40 node
streams 128-wide amplitude frames while the single-link FieldModel is the
canonical 56-tone grid. LinkStats::update returned DimensionMismatch,
feed_calibration bubbled it, and maybe_feed_calibration swallowed it at debug
level — so frame_count stayed pinned at 0 on live hardware (presence/vitals,
which read the global history, were unaffected).
Resample each frame onto the model's canonical 56-tone grid via
HardwareNormalizer::resample_to_canonical before feeding — the same length-only
canonicalization the multistatic fusion path uses (#1170).
Pinned by maybe_feed_calibration_resamples_wide_frames_and_accumulates
(128-wide -> Collecting + count 1). sensing-server bin: 173 passed, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
POST /api/v1/calibration/start created the FieldModel in Uncalibrated, but
field_bridge::maybe_feed_calibration only fed frames while already Collecting
— and the only thing that sets Collecting is feed_calibration on its first
fed frame. The two gates deadlocked: no first frame was ever fed, so
calibration_frame_count stayed 0 and status never left Uncalibrated. Observed
live on a streaming ESP32 node as {"status":"Uncalibrated","frame_count":0}
that never advanced.
- field_bridge::maybe_feed_calibration: feed while Uncalibrated | Collecting
so the first frame flips the model to Collecting and the count advances.
- calibration_stop: return structured {success:false, frame_count,
frames_needed} instead of an opaque 500 when finalized with too few frames.
- FieldModel::min_calibration_frames() accessor for the guard above.
- Regression test: maybe_feed_calibration_advances_uncalibrated_to_collecting.
Presence/motion/vitals were unaffected (separate auto rolling baseline).
Co-Authored-By: claude-flow <ruv@ruv.net>
Lower the STA auth threshold from WPA2_PSK to WPA_PSK so routers running
WPA/WPA2-mixed compatibility mode aren't rejected with
WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD (reason=211), and log the
disconnect reason/rssi instead of retrying blind. Also document the thermal
risk of running this firmware's continuous-radio, tier-2 DSP pipeline on
coin-sized clone boards (ESP32-S3-Zero, SuperMini) with minimal PCB copper
and budget regulators, after a field report of three such boards failing to
power on following a normal session.
Co-Authored-By: claude-flow <ruv@ruv.net>
Published 0.3.4 predates HardwareNormalizer::resample_to_canonical and
MultistaticConfig::for_tdm_schedule, which the sensing-server binary
uses — its publish verify fails against the registry 0.3.4. The in-repo
version had not been bumped since those APIs landed.
Co-Authored-By: claude-flow <ruv@ruv.net>
- wifi-densepose-rufield: add version="0.1.0" to the four rufield path
deps — rufield-core/-provenance/-privacy/-fusion are now published to
crates.io, making this crate (and wifi-densepose-sensing-server 0.3.4)
publishable
- v2/crates/worldgraph -> 4441bc0: wifi-densepose-worldgraph 0.3.2
published (adds prune_semantic_states; unblocks wifi-densepose-engine
0.3.1 publish)
- vendor/rufield -> f3c1492: breaks the fusion<->adapters circular
dev-dependency (path-only dev-dep, stripped at publish)
Closes the crates.io publish blockers in #1334.
Co-Authored-By: claude-flow <ruv@ruv.net>
- sdkconfig.defaults.devkitc: header build command idf v5.2 -> v5.4
(source needs esp_driver_uart, IDF >=5.3 — same reason the PR fixed
the README refs)
- engine/lib.rs: separate doc comments — set_room_adapter's ADR-150
adapter/witness doc had merged into set_multistatic_config's doc
- ui: apply stored bearer token at api.service.js module load instead
of QuickSettings.init — services + dashboard tab dispatch their first
/api/v1/* requests before initializeEnhancements() runs, so the first
requests on every load went out without the Authorization header
- CHANGELOG: Unreleased entries for #1308/#1309/#1310
Co-Authored-By: claude-flow <ruv@ruv.net>
Browsers cannot attach an Authorization header to a WebSocket upgrade,
so with RUVIEW_API_TOKEN set the Live Demo pose stream at
/api/v1/stream/pose always failed with 401 — the same reason
/ws/sensing is already exempted (see bearer_auth module docs). Adds a
narrow EXEMPT_PATHS list plus a regression test that the exemption
does not leak to other /api/v1/* paths. Query-string tokens remain
rejected (CWE-598 test untouched).
Also adds an 'API Access' bearer-token field to the QuickSettings
panel: ui/services/api.service.js had setAuthToken() but nothing ever
called it, so enabling RUVIEW_API_TOKEN broke every /api/v1/* call
from the bundled dashboard. The token is stored in localStorage and
applied before the first request.
Fixes#1310
Co-Authored-By: claude-flow <ruv@ruv.net>
EngineBridge::new built its own StreamingEngine, whose internal
MultistaticFuser was hardcoded to MultistaticConfig::default() (60 ms
guard) — the #1031/#1049 env overrides only reached the sibling
multistatic_fuser field on AppState, so governed trust cycles failed
against the default guard no matter what the deployment configured.
- wifi-densepose-engine: add StreamingEngine::set_multistatic_config()
- engine_bridge: EngineBridge::new takes Option<MultistaticConfig>
- main.rs: thread the same env-derived config into both fusion paths
Verified on a 2-node ESP32-S3 deployment: the failure message now
reflects the configured guard, and with healthy nodes governed cycles
ran 90 s with zero fusion errors.
Fixes#1309
Co-Authored-By: claude-flow <ruv@ruv.net>
The ADR-045 display probe false-positives on display-less
ESP32-S3-DevKitC-1 boards (SH8601 init 'succeeds' against floating
pins), so main.c skips the RuView#893 MGMT+DATA promiscuous upgrade
and CSI yield collapses to 0 pps. sdkconfig.defaults.devkitc compiles
display support out, making has_display constant-false so the #893
upgrade always applies. Hardware-verified on 2x DevKitC-1-N16R8:
0 pps -> steady 40-45 pps.
Also updates the Quick Start build commands and badge from
espressif/idf:v5.2 to v5.4 — current source uses esp_driver_uart
(IDF v5.3+) and no longer builds under v5.2; CI already uses v5.4.
Addresses #1308
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): deep review of the RuView npm surface — ADR-263/264/265 optimization strategies
ADR-263 — @ruvnet/ruview@0.1.0 harness review (O1–O9):
- HIGH: claim-check CLI fails open on empty input (no --text/--file -> PASS exit 0)
- HIGH: MCP stdio server head-of-line blocking (spawnSync verify/calibrate up to 600s)
- MEASURED: optionalDependencies triple the cold npx install (4 pkgs/620kB/71 files
vs 1 pkg/172kB/22 files with --omit=optional) for a path that never imports them
- maxBuffer truncation, python -c port interpolation, version drift, duplicate skills,
guardrail METRIC_TERMS substring false positives ('map'/'F1' — found by dogfooding
claim-check on these very ADRs), zero CI
ADR-264 — @ruvnet/rvagent@0.1.0 + @ruv/ruview-cli review (O1–O9), verified against
the published registry tarball:
- HIGH: exports.require -> dist/index.cjs which is never built nor published
- MEASURED: 44 dead source-map files = 62,698B of the 188kB unpacked payload
- stdio-only server described as dual-transport; mixed dot/underscore tool names;
double Zod validation + hand-duplicated advertised schemas; 2-fd leak per training
job; unbounded body in the unwired HTTP scaffold; dead detectCogBinary candidates;
ruview bin-name collision
ADR-265 — cross-cutting npm distribution strategy: npm-packages.yml CI matrix
(test + pack-content/size gate + tarball-install smoke test), publish-from-CI-only
with npm provenance, version single-sourcing from package.json, bin/namespace
ownership (ruview bin belongs to @ruvnet/ruview), claim-check on package READMEs.
Docs only — no runtime code changed. Index/CHANGELOG/CLAUDE.md/README counts updated.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz
* fix(npm): implement ADR-263/264/265 — harness fail-closed + async MCP, rvagent packaging/transport/naming, npm CI+provenance gate
ADR-263 (@ruvnet/ruview 0.2.0), O1-O9:
- claim-check fails closed on empty input (CLI exit 2, empty_text tool error)
- MCP stdio server dispatches tools/call asynchronously (promise-based spawn);
ping answers while a 3s fake verify runs — pinned by new e2e test
- optionalDependencies dropped: cold npx installs exactly 1 package
(MEASURED: was 4 pkgs/620kB/71 files via npm i in a clean prefix)
- bounded rolling output tails replace spawnSync 1MiB maxBuffer
- node_monitor port passed via sys.argv, never spliced into python -c source
- serverInfo.version read from package.json; resources/prompts stubs
- skills single-sourced: prepack sync script generates .claude/skills/ copies
- which() = memoized dep-free PATH scan
- tools underscore-canonical (ruview_claim_check, ...) + dotted aliases
- guardrail precision: word-boundary map/f1/auc/iou, code-span + F1/O2 label
scrubbing, quantitative-claims-only; packaging reproducer hints
- 30/30 tests (was 17), incl. concurrency e2e + fail-open regression pins
ADR-264 (@ruvnet/rvagent 0.2.0), O1-O9:
- exports fixed: types-first, phantom dist/index.cjs require target removed
- tarball map-free: 127,704B unpacked / 46 files / 0 maps (MEASURED,
npm pack --dry-run; was 188kB incl. 44 maps referencing unshipped src)
- Streamable HTTP actually wired behind RVAGENT_HTTP_PORT: one transport +
one MCP server per session (mcp-session-id routing), 1MiB body cap (413),
port-aware localhost origin gate; dual-transport description now true
- tools renamed underscore-canonical with dotted router-only aliases
- single Zod validation gate; advertised inputSchema generated from the same
Zod source (zod-to-json-schema)
- train_count: parent log fds closed (was leaking 2/job); job records
persisted to <jobsDir>/<id>.json (job_status survives restarts); bounded
log-tail reads
- detectCogBinary probes its candidates instead of dead-coding them
- version from package.json; @types/express dropped; @types/jest -> 29
- README rewritten to match reality (no phantom subcommands/policy layer)
- 99/99 jest tests (incl. new session/body-cap suite + previously-broken
manifest suite); stdio handshake + HTTP session flow smoke-tested live
ADR-265 D1-D4:
- .github/workflows/npm-packages.yml: 3-package x Node 20/22 gate — tests,
version-literal grep (D3), pack-content/size gate, tarball-install smoke
test (catches the ADR-264 F1 class), README claim-check (D4)
- .github/workflows/ruview-npm-release.yml: publish from CI only with
npm publish --provenance
- @ruv/ruview-cli bin renamed ruview-cli (ruview bin belongs to
@ruvnet/ruview); version single-sourced
- ci.yml NODE_VERSION 18 -> 20
ADR statuses updated to Accepted/implemented; harness manifest re-pinned;
ADR-263/264/265 + both package READMEs pass claim-check.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz
* perf(rvagent): lazy-load HTTP transport + memoize generated tool schemas
stdio time-to-first-response ~242ms -> ~189ms (-22%; MEASURED, median of
repeated initialize round-trips against dist/index.js in this container).
- ./http-transport.js now imported lazily inside the RVAGENT_HTTP_PORT
branch: it chain-loads the MCP SDK streamableHttp module (~48ms MEASURED
via per-module import() timing) which the default stdio path never uses
- toolInputJsonSchema memoized per tool: schemas are static for the process
lifetime; under the session-per-server HTTP model every session calls
tools/list, so stop re-walking the Zod tree each time
No behavior change: 99/99 jest tests; HTTP session flow re-smoke-tested
through the lazy import path (initialize -> 200 + mcp-session-id).
Profiled @ruvnet/ruview too and left it alone: 50ms CLI startup vs ~29ms
bare 'node -e ""' floor on the same box (MEASURED) — already near the
interpreter floor with zero dependencies.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz
* ci(ruview-cli): pass jest --passWithNoTests so the private no-test package doesn't fail the npm-packages matrix
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(npm): address 10 verified review findings in harness + rvagent before 0.2.0 publish
harness/ruview (@ruvnet/ruview):
- guardrails: digit gate now sees numbers inside code spans; F1-style
metric tokens followed by ':' or a nearby number are no longer scrubbed
(fail-open regressions in the honesty gate)
- mcp-server: tools/call requests serialize through a FIFO promise chain
(hardware/mutating tools never overlap) while ping/tools/list stay
immediate; stdin close drains in-flight responses before exit
- tools: which() no longer memoizes negative lookups
tools/ruview-mcp (@ruvnet/rvagent):
- index: realpath invoked-directly guard — library import no longer
connects a stdio transport to the consumer's process
- http-transport: explicit allowedOrigins is exact-match only (localhost
any-port convenience applies only with no configured allowlist);
session map gains maxSessions=64 + 5min idle TTL sweep
- train-count: job records persist the child pid and reconcile stale
'running' status after a server restart (exit-code marker or dead pid)
- config: cog binary candidates ordered by process.arch
.github/workflows/ruview-npm-release.yml: port the full ADR-265 D1 gate
(version-literal check, unpacked-size budget, tarball-install smoke test)
from npm-packages.yml so the publish path enforces what the header claims.
Tests: harness 30→36, rvagent 99→112, all passing.
Co-Authored-By: claude-flow <ruv@ruv.net>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The root Makefile still referenced pre-reorg paths that no longer exist, so
the documented build/test/run entrypoints were all broken:
- rust-port/wifi-densepose-rs/ -> v2/ (build-rust, build-wasm[-mat],
test-rust, bench, clean cargo step)
- v1.src.api.main -> archive.v1.src.api.main (run-api, run-api-dev)
test-rust now uses the documented `--no-default-features` invocation (the
proven-passing, GPU-free path). Verified: `cd v2 && cargo test --workspace
--no-default-features --no-run` compiles the full workspace clean.
Surfaced during a metaharness review (the broken root build entrypoint is
why genome reported build:none / publish_readiness 0.40 at the repo root).
Claude-Session: https://claude.ai/code/session_01AgpTcBLRJ32hUsKWxDXf36
While cutting v0.8.3-esp32, an incremental 8MB build reused a leftover
generated `sdkconfig` and silently linked the 4MB dual-OTA partition layout
(no spiffs, ota_1 @ 0x1F0000) — the would-be released `partition-table.bin`
did not match the 8MB `partitions_display.csv` it claimed.
scripts/firmware-release-guard.sh regenerates the expected partition table
from the CSV the named flash-size variant must use and byte-compares it to the
built `partition-table.bin`, and cross-checks flash size in flasher_args.json.
Fails closed so a release pipeline can't ship a mismatched table.
Usage: scripts/firmware-release-guard.sh <8mb|4mb> <build-dir>
Claude-Session: https://claude.ai/code/session_01AgpTcBLRJ32hUsKWxDXf36
#1170 — live multistatic bridge fed raw, un-canonicalized per-node CSI
(64/128/192 bins) to MultistaticFuser, tripping DimensionMismatch every
cycle and silently disabling fusion on mixed HT20/HT40 meshes. Add
HardwareNormalizer::resample_to_canonical (resample-only, no z-score) and
canonicalize every node frame onto the 56-tone grid before fusion.
#1180 — update_csi_fps_ema only rejected dt<=0 or >=1s, so sub-ms UDP-burst
arrivals (36us -> ~27kHz) inflated csi_fps_ema 40-840x. Add a 5ms plausibility
floor and stop re-anchoring observe_csi_frame_arrival on burst deltas.
#1183 — global ENOMEM backoff (CSI flood) starved <=48B/<=1Hz control packets.
Add stream_sender_send_priority() bypassing the backoff gate without touching
the streak; route feature_state/HEALTH/sync through it. Fix the misleading
"HEALTH sent" log that printed even on rv_mesh_send failure.
Verified: signal 501, sensing-server 677 tests (0 failed); firmware builds clean.
Claude-Session: https://claude.ai/code/session_01AgpTcBLRJ32hUsKWxDXf36
Bug #2 (root cause): LD2410 probe-detection matched only the 4-byte head
0xF4F3F2F1, so a floating UART at 256000 baud could phantom-detect a sensor
and spawn a UART task. Now requires a full validated report frame (head +
sane length + tail 0xF8F7F6F5), extracted to mmwave_detect.h and shared with
a host unit test (test_mmwave_detect.c, 8 vectors) so firmware and test can't
diverge. Matches the validate-before-trust approach used for MR60 in #1107.
Bug #1: sendto ENOMEM used a fixed 100 ms backoff too short to drain sustained
lwIP/WiFi buffer pressure, so a node could stay stuck. Now exponential
(100->200->...->2000 ms per consecutive ENOMEM, reset on first successful
send). Removing the phantom LD2410 task (bug #2) also removes the extra load
that tipped the reporter's tier-2 node into the stuck state.
Validated on ESP32-S3 QFN56 rev v0.2 (the reporter's silicon): tier-2 streams
~100 frames/s with no stuck ENOMEM and correctly reports no mmWave (no
phantom). LD2410 predicate truth table proven (head-without-tail REJECTED).
Could not reproduce the reporter's environment-specific floating-pin noise, so
the deterministic proof is the host unit test.
The 40 Hz gamma flicker is now Kconfig-gated (default y, unchanged
behaviour). Set CONFIG_LED_GAMMA_VIZ=n for a dark, lower-power boot (the
LED is simply cleared) — important for photosensitive deployments, no
source edit needed. The colormap saturation point is now operator-tunable
via CONFIG_LED_MOTION_FULLSCALE_MILLI (default 250 = 0.25).
Build + flash confirmed on ESP32-S3 N16R8 (COM8): default y still arms the
gamma timer, CSI flows. ADR-183 updated (gate moved from follow-up to done).
* chore(deps): bump ruv-neural submodule — ColorMap no_std for ESP32
Points to ruvnet/ruv-neural#3 (c9638fa): ruv-neural-viz::ColorMap now
builds no_std, so it can run on the ESP32. Unblocks driving the onboard
WS2812 from the viridis/cool-warm colormap.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(firmware): onboard LED as 40 Hz gamma stimulus, colour from live CSI motion (ADR-183)
The S3 onboard WS2812 (GPIO 48, #962) now runs a GENUS-style 40 Hz gamma
square wave (12.5 ms on/off, 50% duty). The ON-phase colour is live CSI
motion (edge motion_energy) mapped through a 60-step viridis LUT generated
from ruv-neural-viz::ColorMap::viridis() — still=purple, moving=yellow.
Uses the now-no_std ColorMap (ruvnet/ruv-neural#3 / #1126). Hardware-
confirmed on ESP32-S3 N16R8 (COM8): boot log shows the timer armed, CSI
keeps flowing (27-38 pps). Honesty + photosensitivity notes + a Kconfig-gate
follow-up are in ADR-183.
Co-Authored-By: claude-flow <ruv@ruv.net>
Points to ruvnet/ruv-neural#3 (c9638fa): ruv-neural-viz::ColorMap now
builds no_std, so it can run on the ESP32. Unblocks driving the onboard
WS2812 from the viridis/cool-warm colormap.
These are hook/runtime-generated databases (ruvector/intelligence store)
that kept showing as dirty and don't belong in version control. Removed
from the index (files kept on disk) and ignored globally.
A host-portable RuView agent harness minted via MetaHarness and hardened
per ADR-182. Published as @ruvnet/ruview@0.1.0 (bare `ruview` blocked by
npm's typosquat filter → scoped fallback).
What it does:
- 6 fail-closed `ruview.*` tools (onboard, claim_check, verify,
node_monitor, calibrate, node_flash) exposed as CLI verbs + a
dependency-free MCP stdio server.
- The "prove everything" rule made executable: `ruview.claim_check`
flags untagged accuracy claims and the retracted "100%" framing.
- 5 host-neutral skills (onboard/provision-node/calibrate-room/
train-pose/verify) + bundled .claude/ config + provenance manifest.
Validated: 17/17 unit tests, live MCP handshake, `ruview.verify` ran the
real verify.py to VERDICT: PASS, clean `npx @ruvnet/ruview` from registry.
Packs to 16.7 kB / 21 files; kernel+host are optionalDependencies so the
operator tools install lightweight.
README: documented as the portable, multi-host companion to the in-repo
plugins/ruview/ Claude Code plugin (not a replacement).
The SH8601 QSPI panel is write-only, so display_hal_init_panel() 'succeeds' even on a
bare display-less board — display_is_active() then returned true and main.c skipped the
#893/#906 MGMT->MGMT+DATA CSI filter upgrade (yield=0pps). Gate on the FT3168 touch I2C
readback (always present on the Touch-AMOLED board, absent on a bare DevKit): if touch is
absent, the panel 'success' was a false-positive — bail to headless before the display
task starts, so display_is_active() stays false and CSI captures.
Co-authored-by: ruv <ruvnet@gmail.com>
probe_at_baud counted bare 0x01 (SOF) bytes and declared MR60BHA2 on a single one.
A floating UART1 with no sensor reads noise full of 0x01s → false 'Detected MR60BHA2
(caps=0x000f)'. Now a candidate must be a full 8-byte header with a valid header
checksum (bytes 0..6) AND a known frame type (0x0A__ / 0x0F09), and clear the ≥3
threshold; removed the weak single-hit fallback. Real sensors stream valid frames
continuously, so detection of present hardware is unaffected.
Co-authored-by: ruv <ruvnet@gmail.com>
1. record-csi-udp.py stamped LOCAL time with a 'Z' (UTC) suffix → camera/CSI disagreed
by the UTC offset → 0 aligned pairs. Now writes true UTC via datetime.now(timezone.utc).
2. align-ground-truth.js kept empty-keypoint (non-detection) records at confidence 0,
collapsing window avgConf below threshold → all windows rejected. Now skipped at load.
3. extractCsiMatrix silently zero-padded/truncated mixed-subcarrier frames. Now frames
are filtered to the session's modal subcarrier count before windowing — never padded.
4. CSI/feature matrices are filled frame-major but were labeled shape [nSc, nFrames] —
transposed. Labels corrected to [nFrames, nSc] / [nFrames, dim].
Co-Authored-By: claude-flow <ruv@ruv.net>
Advances the vendored ruv-neural submodule from the stale 'Initial' pin (1ece3af) to
current main (81be9e1): the static web console UI, the closed-loop neuromodulation
platform, repositioned README, and the @ruvnet/ruvn companion-tool mention.
ruv-neural is not a v2 workspace member, so this does not affect the workspace build
(cargo metadata resolves clean).
Co-Authored-By: claude-flow <ruv@ruv.net>
The industrial rescope (ruv-drone) removed the itar-unrestricted feature flag —
formation/allocation/raft/flight-control are now default capabilities. Update the
'ruflo+itar' matrix entry to just '--features ruflo' so CI matches the new feature set.
Co-Authored-By: claude-flow <ruv@ruv.net>
- published as github.com/ruvnet/worldgraph (3-crate workspace, history via git-filter-repo)
- replace the 3 in-tree crates with one submodule at v2/crates/worldgraph
- parent workspace: drop the 3 members, exclude the submodule (it is its own workspace),
repoint workspace.dependencies(worldmodel) + engine/sensing-server path-deps into it
- cargo metadata resolves clean (geo/worldgraph/worldmodel consumed from the submodule)
Co-Authored-By: claude-flow <ruv@ruv.net>
- ruview-swarm published as github.com/ruvnet/ruv-drone (history preserved via subtree split)
- replace the in-tree crate with a submodule at v2/crates/ruview-swarm (branch main)
- standalone repo dropped the unused wifi-densepose-core path-dep; export-control NOTICE added there
- workspace member path unchanged; cargo metadata resolves ruview-swarm from the submodule
Co-Authored-By: claude-flow <ruv@ruv.net>
Self-contained Three.js r128 demo at examples/through-wall/ that renders
ONLY genuine data streamed from the running sensing-server over
ws://localhost:8765/ws/sensing. No simulation, no fabricated frames, no
fake skeleton.
Renders, driven by real /ws/sensing frames:
- 20x20 signal_field floor heatmap (real values)
- coarse RF-localization puck from persons[0].position (labeled coarse,
NOT pose; peak signal_field cell as fallback)
- live motion/breathing/variance/rssi bars + motion sparkline
- presence/confidence/estimated_persons/active-node/tick/Hz meters
- 3D room with wall + doorway dividing office (node 9) / hallway (node 13)
- honest mutually-exclusive banner: LIVE (source=esp32) / SIMULATED /
NO SERVER, showing the real source verbatim
- optional webcam tile (ground-truth-when-visible, separate from CSI)
Reuses scene/lights/bloom/CSS + webcam path from
examples/three.js/demos/05-skinned-realtime.html, the floor-heatmap idea
from ui/observatory/js/, and the threaded no-cache server from
examples/three.js/server/serve-demo.py (serve.py on :8080).
Verified against the live server: real frame source=esp32, nodes [9,13],
400 signal_field values, persons[0].position present. Python proof PASS.
Co-Authored-By: claude-flow <ruv@ruv.net>
Two ESP32-S3 nodes on WiFi/ESP-NOW sync drift 10-150 ms (~70 ms typ.), exceeding
the 60 ms default guard → permanent trust demotion to Restricted, all pose output
suppressed, 200k+ errors, no escape but a container restart.
Add a direct WDP_GUARD_INTERVAL_US override (+ optional WDP_SOFT_GUARD_US) to
multistatic_guard_config_from_env. Precedence (most-specific wins): direct
override > WDP_TDM_SLOTS+WDP_TDM_SLOT_US schedule-derived > 60ms/20ms default.
Soft band always clamped strictly below hard; malformed/zero ignored (falls back,
never breaks fusion). Effective guard logged at startup.
Pinned by 6 tests (multistatic_guard_config_tests). sensing-server bin tests
449 -> 455, 0 failed. Python proof PASS, hash unchanged (off signal path).
Closes#1049.
Co-Authored-By: claude-flow <ruv@ruv.net>
Closing beyond-SOTA security review of wifi-densepose-wasm-edge (ADR-040,
~70 edge modules). The two WASM↔host boundaries (lib.rs::on_frame/on_timer
and bin/ghost_hunter.rs::on_frame) read raw IEEE-754 f32 from the csi_get_*
imports with no finiteness check — the crate had zero is_finite/is_nan
guards and its clamp helpers propagate NaN. A single non-finite host value
latches NaN into long-lived per-module accumulators (EMA / Welford / phasor
sums / anomaly baselines), after which detectors fail degraded (stuck gate
state, silently-disabled checks) — silent corruption, not a crash.
Add sanitize_host_f32() (non-finite -> 0.0, core-only for no_std) applied at
every host_get_* float read: one chokepoint covering all downstream modules,
mirroring the existing M-01 negative-n_subcarriers boundary clamp. LOW /
defense-in-depth (the Tier-2 DSP firmware supplies the imports, a semi-trusted
boundary).
Pinned by boundary_tests::{sanitize_passes_finite_values_through,
sanitize_maps_non_finite_to_zero,
coherence_monitor_nan_latches_without_sanitize_but_not_with} — the last
asserts on the current CoherenceMonitor that a raw NaN frame latches the
smoothed score while the sanitized path stays finite.
Other review dimensions attested clean with evidence (see CHANGELOG): no
hot-path panics (all unwrap/expect are test-only or std-gated RVF builder),
all bounds min()-clamped, all index-by-cast const-bounded or guarded, no
leaking closures (no move||/forget/leak), no secrets.
Verified: host `cargo test --features std,medical-experimental` 672 passed /
0 failed (+3 new tests); all three wasm32-unknown-unknown release artifacts
build clean (lib default no_std/panic=abort, ghost_hunter standalone-bin,
medical-experimental); Python proof VERDICT PASS, hash unchanged.
* fix(security): desktop IPC serial-command-injection + over-broad shell capability (ADR-178)
Beyond-SOTA security review of wifi-densepose-desktop (Tauri v2). Two real
findings, each MEASURED on Windows (crate builds + tests under
--no-default-features):
WDP-DESK-01 (MODERATE) — serial command injection via configure_esp32_wifi.
The #[tauri::command] handler concatenated webview-supplied ssid/password into
newline-terminated serial commands with no validation; a \r\n let a compromised
webview inject an arbitrary follow-up firmware command (reboot/erase). Added
validate_wifi_credentials() enforcing WPA2 length bounds and rejecting all
control characters, called fail-closed before any serial write. Pinned by 3
new tests (rejects \r\n / \n / NUL injection, rejects out-of-range, accepts
valid boundaries).
WDP-DESK-02 (MODERATE) — removed unused shell:allow-execute / shell:allow-open
from capabilities/default.json. The Rust backend spawns processes via
std::process::Command (bypassing the allowlist) and the UI only uses
dialog.open; the shell perms were unused privilege granting the webview
arbitrary host command execution on compromise. Regenerated capabilities.json
confirms only core:default + dialog perms remain.
lib tests 18 -> 21 (+3 pins), integration 21 -> 21, 0 failed. Python
deterministic proof unchanged (f8e76f21...46f7a; desktop off the signal path).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): ADR-178 — desktop IPC injection fix + capability least-privilege
Records the 2 MEASURED MODERATE fixes in feddcde9d: WDP-DESK-01 (webview
ssid/password \r\n-injected arbitrary firmware serial commands → validated
fail-closed) and WDP-DESK-02 (unused shell:allow-execute/open capability
granted to the webview → removed). 30-command IPC surface + capability scope
audited; 6 dimensions clean-with-evidence. desktop 18→21.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(ADR-131): HOMECORE-UI operational dashboard + BFF gateway
Complete two-tier Cognitum operator dashboard (ADR-131), served by
homecore-server at /homecore, plus the single-origin BFF gateway that
wires it to real backends.
Front-end (zero-dep vanilla TS/JS + CSS, exact Cognitum design tokens):
- All 10 panels (§4.1-4.10): dashboard, SEED fleet + detail, fleet map,
entities (live WS subscribe_events, never polls), rooms, COGs,
calibration wizard, events + automation builder, witness/audit, settings.
- §6 UX invariants in code: first-class provenance, prominent stale/veto/
fragility, null(not-trained) vs withheld vs error, --mono everywhere,
Hailo vs CPU COG distinction.
- api.js calls the gateway routes in production; mock demoted to a
dev-only ?demo=1 fixture (no mock in prod); typed error states.
- Tests under plain node: import-graph, boot, render-smoke (22),
interaction (3), prod-errors (13) — 5 files green; bundle ~137 KB
(~37x smaller than HA), <2 ms/cold-render.
BFF gateway (homecore-server/src/gateway.rs, compiled + tested on Rust 1.89):
- /api/cal/* reverse-proxy to the calibration API (ADR-151).
- GET /api/homecore/rooms with the RoomState adapter (breathing->breathing_bpm,
heartbeat:null->heart_bpm:null, injected anomaly.threshold/room_id).
- GET /api/homecore/cogs supervisor over /var/lib/cognitum/apps/.
- GET /api/homecore/appliance from /proc + TCP service probes.
- SEED-device/appliance routes return typed 503 upstream_unavailable.
- cargo test -p homecore-server = 12/12; run live (curl-verified);
fixed a real double-v1 proxy-URL bug found during live testing.
Honest scope: W1/W2/W4/W6-appliance functional; W3/W5/W6-Hailo/federation
return typed 503 (depend on services/hardware not in this repo).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-ui): resolve code-review findings — SSRF guard, CORS/trace coverage, §6 honesty, crash guards
Addresses the high-effort review of PR #1082:
- SECURITY: cal_proxy rejects path-traversal/confused-deputy SSRF (`.`/`..`
segments, backslash, %2e%2e/%2f, absolute) on raw+decoded forms → 400,
before attaching the server-side calibration bearer.
- CORRECTNESS: /api/homecore/* + /api/cal/* now covered by the shared CORS
allowlist (build_cors_layer, exported from homecore-api) + TraceLayer —
previously merged outside router()'s layers (no CORS, no tracing).
- §6 HONESTY (no fabricated data): dashboard renders '—' for null metrics
(not "null%"/"null°C"); cogs Hailo pill reflects the REAL appliance probe
(not hardcoded "connected"); room anomaly threshold passed through / null,
not a fabricated 0.5.
- ROBUSTNESS: cogs asArray(hef) guards a non-array manifest field; calibration
progress guards target<=0 (no NaN%/Infinity%); restart clears the poll timer.
- CLEANUP: mock.js is now a cached DYNAMIC import (demo-only) — never bundled
in production (§2.2).
- New ui/tests/unit-fixes.mjs pins the above; ADR-131 + CHANGELOG updated.
Co-Authored-By: claude-flow <ruv@ruv.net>
---------
Co-authored-by: Nick Ruest <127058086+nicholas-ruest@users.noreply.github.com>
* fix(nvsim): guard degenerate input — config-induced panic + NaN-state poisoning
Beyond-SOTA security review of the ADR-089 NV-diamond simulator (milestone #9,
crate 2 of 4). Two real degenerate-input findings, each pinned fails-on-old:
NVSIM-DT-01 (config panic/DoS, pipeline.rs): an external f_s_hz == 0 made
dt == +Inf, dt_us saturated to u64::MAX, and `sample * dt_us` panicked with
"attempt to multiply with overflow" at sample >= 2 (debug/WASM panic=abort;
garbage t_us in release). Fix: sanitise dt (non-finite/non-positive -> 1 µs
fallback), cap the u64 cast, and saturating_mul the timestamp.
NVSIM-NAN-01 (NaN-state poisoning, digitiser.rs): a non-finite scene parameter
(NaN dipole position / Inf moment / NaN loop radius) bypasses the near-field
clamp (NaN < R_MIN_M is false) and yields a NaN field; at the ADC `NaN as i32`
== 0 silently emitted b_pt=[0,0,0] with ADC_SATURATED CLEAR — indistinguishable
from a legit zero-field reading. Fix at the funnel: adc_quantise treats any
non-finite input as out-of-range -> clamps to code 0 AND raises the saturation
flag, so the corruption is visible downstream.
Determinism integrity, panic-free MagFrame deserialisation, and RNG seeding
confirmed clean with evidence. The published cross-machine witness
(cc8de9b0…93b4) is unchanged — guards only affect degenerate inputs.
cargo test -p nvsim --no-default-features: 50 -> 53 passed, 0 failed.
Workspace green; Python deterministic proof unchanged (f8e76f21…46f7a,
nvsim off the signal proof path). Needs ADR slot 177.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): ADR-177 — nvsim degenerate-input hardening
Records the 2 MEASURED MEDIUM fixes in 37764be55 (NVSIM-DT-01 config-induced
overflow panic / WASM-abort DoS; NVSIM-NAN-01 non-finite scene param →
silent fake zero-field reading with saturation flag clear) + 3 pins, and the
clean-with-evidence determinism/deser/div-by-zero verdict. Cross-machine
witness cc8de9b0…93b4 reproduces unchanged.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ruview-swarm): fail-closed on NaN/Inf at swarm-comm trust boundary (ADR-148)
Beyond-SOTA security review of the ADR-148 drone swarm control plane found
four IEEE-754 NaN/Inf fail-open / DoS bugs on data crossing the untrusted
swarm-comm boundary (receive_peer_state / receive_peer_detection accept full
DroneState/CsiDetection whose f64/f32 fields deserialize with no finite-check).
- HIGH: failsafe::tick collision-avoidance + battery checks fail-open on NaN
(NaN < threshold == false silently disabled collision avoidance / kept a
NaN-battery drone Nominal). Now fails closed to EmergencyDiverge / RTH.
- MED: geofence::check NaN-altitude bypass returned Safe through the
point-in-polygon path. Now leading non-finite-coordinate guard -> HardBreach.
- MED/DoS: antijamming FhssRadio panicked with "% 0" on an empty deserialized
channels_mhz. Now len==0 early-returns (benign 0.0 sentinel).
- LOW: multiview::fuse propagated a NaN victim_position into the fused
"confirmed victim" location. Now requires finite confidence + position.
Each fix pinned by a fails-on-old / passes-on-new test (MEASURED: old code
returned Nominal/Safe or panicked). cargo test -p ruview-swarm
--no-default-features: 117 -> 123 passed, 0 failed. Workspace green; Python
deterministic proof unchanged (f8e76f21...46f7a, off the signal path).
Documented-not-fixed (ADR slot 176): Raft AppendEntries lacks Log-Matching
consistency check (topology/raft.rs); MavlinkSigner::verify uses non-constant
-time tag compare + no replay-window rejection (already doc-flagged).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): ADR-176 — ruview-swarm NaN-fail-open safety review
Records the 4 MEASURED fail-open safety bugs fixed in f671000d7 (collision
avoidance, battery RTH, geofence, anti-jamming %0 panic — all NaN/Inf
defeating a safety comparison at the swarm-comm trust boundary) + 6 pins,
5 clean-with-evidence dimensions, and the 2 genuine issues deferred to a
focused follow-up (Raft AppendEntries log-matching; MAVLink signer
constant-time + replay window).
Co-Authored-By: claude-flow <ruv@ruv.net>
Sub-deliverable 8.2 of the benchmark/optimization milestone. Quantizes the
843,834-param "half" WiFlow-STD pose model (half_best.pth) to int8 two ways and
MEASURES the accuracy/size trade-off vs fp32 under ONE locked normalization
(ADR-173 torso-diameter PCK, upstream calculate_pck use_torso_norm=True), on the
same seed-42 file-level 70/15/15 test split that produced the fp32 sweep numbers.
MEASURED on ruvultra (RTX 5080, torch 2.11.0+cu128, fbgemm; clean test, torso-PCK):
fp32 96.62% pck@20 99.47% pck@50 0.008981 mpjpe 3.351 MB
int8 PTQ static 40.98% pck@20 94.98% pck@50 0.038262 mpjpe 1.046 MB (-55.64pp)
int8 QAT (3 ep) 67.48% pck@20 98.69% pck@50 0.026548 mpjpe 1.043 MB (-29.15pp)
Verdict (honest no): int8 is NOT a win at the strict PCK@20 edge target. Static
PTQ collapses; QAT recovers a large share but still loses 29 pp @20 for a 3.2x
size win — keep fp32/fp16 on the edge. Disclosed: QAT fake-quant val pck@20 was
83.45% but converted int8 scores 67.48% (~16pp convert_fx gap, reported honestly).
Deliverables:
- v2/crates/wifi-densepose-train/scripts/quantize_half_int8.py (reproducible:
header carries the exact ssh command + run date; QAT primary, static PTQ fallback)
- docs/adr/ADR-175-int8-quantization-half-pose-model-measured.md (MEASURED table,
locked normalization, QAT-vs-PTQ labeling, verdict, reproduction, limitations)
- CHANGELOG [Unreleased] ### Added entry
No production Rust or signal-pipeline change. Python deterministic proof unchanged
(f8e76f21a0f9852b70b6d9dd5318239f6b20cbcb4cdd995863263cecdc446f7a, bit-exact).
* ci(bench): wire v2 criterion benches into CI as a compile-verify regression gate
Sub-deliverable 8.3 of the benchmark/optimization milestone (needs ADR slot 174).
The v2/ workspace ships 26 criterion benches across 18 crates, but benches are
not part of `cargo test`, so nothing in CI compiled them and they silently rot
when a public API they call changes.
Add `.github/workflows/bench-regression.yml`:
- bench-compile (HARD GATE): `cargo bench --workspace --no-default-features
--no-run` compiles + links every default-feature bench (no measurement) plus
the cir-gated cir_bench — a real, deterministic regression guard against
bench bit-rot.
- bench-fast-run (INFORMATIONAL, continue-on-error, never gates): runs a
curated pure-CPU subset (nvsim, ruvector sketch/fusion) in criterion
quick-mode and uploads logs as an artifact.
No timing-regression gate, by design: wall-clock on shared GitHub runners varies
2-3x run-to-run, so a hard threshold or cross-runner `criterion --baseline`
compare would manufacture false failures. The honest scope is compile-verify +
informational-run; the workflow header documents the self-hosted-runner
condition under which true timing-gating becomes honest. The crv-gated crv_bench
is excluded because its crates.io dep ruvector-crv 0.1.1 fails to build upstream.
Running the gate immediately caught one already-bit-rotted bench:
wifi-densepose-mat/detection_bench failed to compile (E0063: missing field
last_rssi in SensorPosition). Fixed (last_rssi: None) and re-verified.
Validation (MEASURED): mat detection_bench + cir_bench + nvsim + ruvector +
vitals + swarm benches compile under --no-default-features; fast subset runs;
`cargo test -p wifi-densepose-mat --no-default-features` 174 passed / 0 failed;
Python proof PASS, hash f8e76f21...46f7a unchanged.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): ADR-174 — CI bench-regression compile-verify gate
Records sub-deliverable 8.3 (bench-regression.yml, committed c4c59e085):
a hard compile-verify gate over all 26 v2 criterion benches (caught + fixed
one real bit-rotted bench, mat/detection_bench E0063) + an informational
fast-run. Documents the honest scope — no timing-regression gate, since
shared-runner wall-clock varies 2-3x; states the self-hosted-runner condition
under which timing gating becomes honest.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(train): metric-locked PCK/MPJPE accuracy harness — resolve PCK-definition ambiguity
The SOTA brief (docs/research/sota-nn-train-benchmark-brief.md §1/§3.1/§4)
identifies metric ambiguity as the single biggest threat to any beyond-SOTA
claim: three PCK@20 numbers (96.09% WiFlow-STD image-normalized, 81.63%
AetherArena torso-PCK, 61.1% GraphPose-Fi standard PCK) cannot be lined up
because each silently uses a different normalization. The project was retracted
twice over this (a withdrawn 92.9% used absolute pixels, not torso).
New src/accuracy.rs makes the normalizer explicit, selectable, and carried with
every reported number:
- PckNormalization enum: TorsoDiameter (standard MM-Fi/GraphPose-Fi hip↔hip),
BoundingBoxDiagonal (looser WiFlow-STD image-normalized), AbsolutePixels(t)
(retracted convention, reproducible + clearly non-comparable).
- pck_at(pred, gt, vis, k, normalization) — one canonical PCK reusing the
metrics_core geometric primitives (no duplicate kernel).
- mpjpe(pred, gt, vis) — 2D/3D, mm.
- PoseAccuracy { pck_at: BTreeMap<u8,f32>, mpjpe, normalization, n_keypoints,
n_frames } via accuracy_report(frames, ks, normalization) — an unlabeled PCK
number is structurally impossible.
17 hand-computed deterministic tests (no GPU, no datasets) prove the harness
arithmetic, including the key proof that identical predictions score
0.50 / 1.00 / 0.75 under the three normalizations, plus graceful degenerate
handling (zero torso, empty frames, NaN coords — no panic, never false-perfect).
This is measurement infrastructure, NOT an accuracy claim. Public API worth an
ADR — needs ADR slot 173 (parent to write).
wifi-densepose-train lib 191→206, test_metrics 12→14, 0 failed; full workspace
green (exit 0); Python deterministic proof unchanged
(f8e76f21a0f9852b70b6d9dd5318239f6b20cbcb4cdd995863263cecdc446f7a).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): ADR-173 — metric-locked PCK/MPJPE accuracy harness
Documents the accuracy harness (committed 3a8b2ed13) that resolves the
PCK-definition ambiguity flagged as the #1 beyond-SOTA risk in the SOTA brief
(#1090): three historical numbers (96/81.6/61) used three unstated
normalizations. The harness makes normalization explicit + selectable
(PckNormalization enum) and every reported number carries its definition.
Key proof: identical predictions → 0.50/1.00/0.75 under torso/bbox/abs.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(core,cli): pin DoS-resistance of CSI deserialisers (ADR-127 security review)
Beyond-SOTA security review of wifi-densepose-core + wifi-densepose-cli.
Load-bearing-question verdict: the NaN-state-poisoning bug class does NOT
originate in core — core exposes no stateful accumulator (no Welford,
von-Mises, IIR, voxel grid, running mean); each downstream crate rolls its
own, so each fix is correctly local. Both crates confirmed clean on every
reviewed dimension (panic-on-adversarial-input, NaN handling, unbounded
memory, path traversal, secrets) — no production code changed.
Adds 4 regression pins locking in two existing-but-untested DoS guards:
- core: from_canonical_bytes shape guard (Vec::with_capacity bound) — proven
to fail with `capacity overflow` when the saturating-mul guard is removed.
- core: canonical decoder never panics on arbitrary/truncated bytes.
- cli: parse_csi_packet rejects an oversized n_antennas*n_subcarriers claim
before Array2 allocation (33 MB claim in a 2 KB datagram -> None).
- cli: parse_csi_packet never panics on arbitrary UDP bytes.
core: 35 -> 37 lib tests; cli: 24 -> 26 tests; 0 failed. Python proof
unchanged (f8e76f21…46f7a — off the signal path).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): ADR-172 — wifi-densepose-cli + core CSI-deserialiser security review
Records the clean-with-evidence verdict + 4 DoS-resistance regression pins
(test-only, committed in a1051607d). Documents the load-bearing finding:
the NaN-state-poisoning bug class does NOT originate in a shared core
primitive (core exposes no stateful accumulator — MEASURED via grep), so
the 3 prior downstream-local fixes are complete. Gives the wifi-densepose-cli
review its own ADR slot (core portion cross-refs ADR-127 §9).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-migrate): redact secret value from malformed secrets.yaml error (secret-leak)
`read_secrets` wrapped serde_yaml's parse error into `MigrateError::YamlParse {
source }`. serde_yaml's message for a typed-tag coercion failure embeds the
offending scalar verbatim, e.g. `invalid value: string "<the-secret-value>"`.
That error propagates out of `read_secrets`, is `?`-returned by the
`InspectSecrets` CLI path in main.rs, and printed to stderr by anyhow — leaking
a secret value despite the CLI's deliberate `<redacted>` design.
Fix: secrets.yaml parse failures now map to a new redacting variant
`MigrateError::SecretsParse { path, line, column }` that carries only the file
path and a coarse location (from `serde_yaml::Error::location()`), never the
scalar content. Other (non-secret) YAML files keep `YamlParse`.
Pinned by `secrets::tests::malformed_secrets_error_never_contains_secret_value`
(asserts the rendered error AND its full #[source] chain never contain the
secret value; fails on the old `YamlParse` path) plus
`malformed_secrets_error_reports_location` (still fail-closed + locatable).
ADR-165 secret-handling rule: a secret value must never appear in output.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(homecore-migrate): record secret-leak fix in ADR-165 + CHANGELOG
Note the secrets.yaml error-redaction fix and the review's clean dimensions
(read-only source / no traversal / no panic / fail-closed versioning / no
injection) in ADR-165 §2.4, bump the test-evidence count 19→21 in §2.6, and add
an [Unreleased] Security entry to CHANGELOG.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore): atomic state set — close TOCTOU lost/reordered state_changed events
StateMachine::set did get() (release shard lock) → compute next + no-op
decision → insert() (re-acquire lock) → send(). The read-modify-write was
not atomic w.r.t. a concurrent writer on the same entity: a writer that
read a stale `old` could mis-classify a real transition as a no-op and drop
its state_changed event (a missed automation trigger) or fire an event whose
new_state duplicated the previously delivered one (a spurious trigger for any
automation keyed on old_state != new_state). ADR-127 §2.1 promises "writer
atomically replaces the map entry"; the implementation did not.
Fix: hold the DashMap shard write-lock across the whole read→decide→insert→
fire sequence via entry()/insert_entry(). tx.send is non-blocking, non-async,
and never re-enters the map, so firing under the shard lock cannot deadlock
and keeps global event order in lock-step with global commit order.
Pinned by concurrent_set_fires_no_duplicate_adjacent_events: 4 writers
toggling one entity A/B; asserts no two consecutive fired events carry the
same new_state (impossible under correct serialisation). Fails reliably on
the old code (~365-476 duplicate-adjacent events on the first trial), passes
on the fix across repeated runs.
Co-Authored-By: claude-flow <ruv@ruv.net>
* harden(homecore): bound entity_id length — close memory-DoS at the REST boundary
homecore-api/src/rest.rs parses untrusted path segments straight through
EntityId::parse (get/delete/set_state). With no length cap, an otherwise-valid
id like "a." + many MB of [a-z0-9_] was accepted; a POST /api/states/<giant>
would persist it into the DashMap state store, permanently growing memory
(amplification across distinct ids).
Fix: reject ids longer than MAX_ENTITY_ID_LEN (255, HA-compatible) up front in
parse(), before any per-char scan, with a new EntityIdError::TooLong. Fails
closed at the boundary type so every caller (REST, registry deserialize,
automation) is protected.
Pinned by entity_id_length_boundary: exactly-MAX accepted, MAX+1 rejected,
4 MiB id rejected as TooLong. Fails on old code (oversized parses Ok).
Co-Authored-By: claude-flow <ruv@ruv.net>
* harden(homecore): isolate panicking service handlers (catch_unwind)
ServiceRegistry::call already ran handlers outside the registry lock (the
Arc<dyn ServiceHandler> is cloned out of the read guard first), so a panic
could never poison the RwLock or block other callers — good. But a panicking
handler unwound through call() into the caller's task; the task driving the
engine (e.g. an axum request handler invoking a service) could be aborted by
one buggy integration.
Fix: wrap the handler future in AssertUnwindSafe + FutureExt::catch_unwind and
convert a panic into ServiceError::HandlerPanicked. Mirrors HA isolating
service-handler exceptions. The registry stays fully usable afterwards.
Pinned by panicking_handler_is_isolated_and_registry_survives: the panicking
call returns HandlerPanicked (not an unwind), a sibling healthy service still
returns its value, and the bad service remains registered. Fails on old code
(the await point panics instead of returning Err).
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(homecore): pin event-bus lag safety (bounded broadcast, no DoS)
Documents-with-evidence that the core EventBus does NOT have the homecore-api
WS broadcast-lag failure: with EVENT_CHANNEL_CAPACITY=4096, firing 3x capacity
while a subscriber never drains keeps fire_* non-blocking (publisher never
waits on slow receivers), gives the slow receiver a recoverable Lagged(n)
(drop-oldest + re-sync) rather than a closed channel, and leaves the bus live
for a fresh fast subscriber. No code change — pins the clean dimension.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(homecore): record ADR-127 §9 security+concurrency review + CHANGELOG
Documents the three pinned fixes (HC-RACE-01 state-set TOCTOU, HC-EID-LEN-01
entity_id memory-DoS, HC-SVC-PANIC-01 service-handler isolation) and the
clean dimensions (bounded event-bus lag handling, lock discipline / no
lock-across-await, no panic-on-input) with their evidence.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-assist): bound untrusted utterance length, fail closed (ADR-133 security)
The intent recognizers accept utterances from untrusted callers (voice
transcripts, the WebSocket `assist` command). Neither the regex nor the
semantic path bounded utterance length, so a pathological multi-megabyte
utterance forced an unbounded `to_lowercase()` clone plus a per-registered-
pattern scan (and, in the semantic path, full tokenisation + feature-hash
embedding) — an allocation/CPU amplification on attacker-controlled input.
The `regex` crate is linear-time (no catastrophic backtracking), so this was
a throughput/memory DoS rather than a hang, but it was still unbounded.
Fix: introduce MAX_UTTERANCE_BYTES (4 KiB — far above any real spoken
command) and check it at both recognizer boundaries BEFORE any allocation or
scan. An over-length utterance fails closed: Ok(None) (no intent, no action),
identical to an unrecognised phrase. No legitimate command is affected.
Pinned by fails-on-old tests:
- recognizer::over_length_utterance_fails_closed — an over-length utterance
that contains a valid command resolves to None (would have matched before)
- semantic_recognizer::over_length_utterance_fails_closed_semantic
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(homecore-assist): pin clean security dimensions with evidence (ADR-133)
Adds regression tests documenting the dimensions reviewed and found clean,
so the properties cannot silently regress:
- runner: no subprocess surface exists. RufloRunnerOpts.{script_path,env}
are inert and never executed; even a hostile script_path/env spawns
nothing. And the entity_id capture class [a-z0-9_ .] strips every shell
metacharacter, so a resolved slot can never carry ; | & $ ` / etc into a
(future) argv — sanitisation by construction.
(shell_metachars_never_survive_into_a_resolved_slot,
runner_opts_are_inert_no_process_spawned)
- recognizer: the regex crate is a linear-time finite automaton; a classic
catastrophic-backtracking shape (a+)+$ on adversarial input completes in
bounded time — no ReDoS.
(pathological_backtracking_pattern_completes_in_bounded_time)
- embedding: embeddings are structurally finite (FNV feature-hash + guarded
L2 normalise, no external float input, no unguarded division), so a crafted
utterance cannot inject NaN/Inf to poison cosine k-NN; cosine against the
zero vector is a finite 0.0, never NaN.
(embeddings_are_structurally_finite, cosine_with_zero_vector_is_finite_not_nan,
empty_utterance_against_empty_index_no_panic_no_match)
- pipeline: injection-shaped utterances never deliver a metacharacter into a
service call; the worst case resolves to a clean entity token, and an
unrecognised utterance fails closed to not_understood (no action).
(pipeline_injection_shaped_utterance_carries_no_metachars_to_service)
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(homecore-assist): record ADR-133 security review (HC-ASSIST-01 + clean dims)
CHANGELOG [Unreleased] Security entry + ADR-133 section 6 review notes for the
homecore-assist voice/intent pipeline review.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-recorder): bound history query + add transactional purge (memory-DoS + disk-DoS)
Security review of the HA-compat state recorder (ADR-132) found two real
bounding bugs; SQL-injection and NaN-index dimensions confirmed clean.
(1) Memory-DoS: get_state_history carried no LIMIT — a wide [since,until]
window over a high-frequency entity loaded an unbounded row set into a
single in-memory Vec. Added LIMIT MAX_HISTORY_ROWS (1,000,000); the
sibling search paths were already k-bounded.
(2) Disk-DoS / documented-but-missing purge: README advertised
Recorder::purge(older_than) but no retention path existed -> unbounded
disk growth. Added a transactional purge with an EXCLUSIVE cutoff
(idempotent, no off-by-one) that deletes old states+events and
garbage-collects orphaned state_attributes blobs (dedup-shared blobs
are kept until their last referencing state is gone). All three deletes
run in one transaction so a mid-purge failure rolls back cleanly.
Pinning tests (homecore-recorder 19->25 no-default / 25->31 ruvector, 0 failed):
- malicious_entity_id_is_stored_literally_not_executed (SQL injection)
- like_metacharacters_in_query_are_literal_not_wildcards (LIKE escape)
- history_query_carries_a_limit_clause (memory-DoS bound)
- purge_keeps_boundary_row_and_drops_older (exclusive-cutoff, true pin)
- purge_gcs_orphaned_attributes_but_keeps_shared (dedup-safe GC)
- purge_also_removes_old_events
No behaviour change beyond the two fixes. Python deterministic proof
unchanged (recorder is off the signal proof path).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(homecore-recorder): record ADR-132 security review findings
Add a "3a. Security review" section to ADR-132 and a CHANGELOG [Unreleased]
Security entry covering the homecore-recorder review: SQL-injection and
NaN-index dimensions confirmed clean with evidence (every query bound; LIKE
pattern bound+escaped; SHA-256->i32->f32 embeddings always finite, empty
index/k=0 probed no-panic), plus the two fixes (unbounded history LIMIT,
transactional exclusive-cutoff purge with orphan-attribute GC).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-automation): bound template render to stop unbounded-expansion DoS (HC-SEC-01)
A `template:` condition / value_template comes straight from user
automation config and was rendered with MiniJinja's default (no
instruction budget, no output cap). A single condition such as
`{% for i in range(5000) %}{% for j in range(5000) %}xxxx{% endfor %}{% endfor %}`
rendered a 100 MB string over ~11 s on one render call (proven
empirically) — a CPU/memory denial of service, the bfld-class
"unbounded expansion".
Fix:
- Enable MiniJinja's `fuel` feature and set a per-render instruction
budget (`set_fuel(Some(1_000_000))`). A nested loop burns one unit
per iteration, so the budget caps total work regardless of nesting;
the attack now fails fast (~90 ms) with "engine ran out of fuel".
- Reject template sources over 64 KiB before compilation (defense in
depth so a pathological literal can neither compile nor emit verbatim).
Legitimate HA templates (a few dozen instructions) are unaffected.
Tests (fail on old — unbounded render / no rejection):
- nested_loop_template_is_bounded_not_unbounded_dos
- single_huge_repeat_template_is_bounded
- oversized_template_source_is_rejected
- legitimate_template_still_renders_within_fuel (no regression)
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-automation): stop crafted delay/timeout from panicking the run task (HC-SEC-02)
`Action::Delay { seconds }` and `Action::WaitForTrigger { timeout_seconds }`
fed the user-supplied float straight into `Duration::from_secs_f64`, which
PANICS on negative, NaN, infinite, or overflowing inputs. All of those are
reachable from a crafted (or simply typo'd) automation YAML —
`delay: {seconds: -1}`, `.nan`, `.inf`, `1e308` — so one hostile config
aborts the spawned automation task with a panic
("cannot convert float seconds to Duration: value is negative", proven
empirically).
Fix: a `safe_duration_from_secs` guard that saturates instead of panicking,
matching Home Assistant's lenient "non-positive delay = no delay":
- NaN / ±inf / negative -> Duration::ZERO
- absurdly large (would overflow) -> clamped to ~100 years (MAX_DELAY_SECS)
Tests (fail on old — panic = failure):
- delay_negative_seconds_does_not_panic
- delay_nan_seconds_does_not_panic
- delay_infinite_seconds_does_not_panic
- wait_for_trigger_negative_timeout_does_not_panic
- safe_duration_saturates_hostile_values (incl. overflow clamp)
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(homecore-automation): record HC-SEC-01/02 security review (CHANGELOG + ADR-129 §8a)
Document the two DoS findings (template unbounded-expansion HC-SEC-01,
delay panic-on-config HC-SEC-02) and the dimensions probed clean
(condition fail-closed, bounded run-modes, sandboxed read-only templates).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(geo numerical robustness): parse_hgt underflow panic + haversine asin-domain NaN
Targeted numerical-robustness audit of wifi-densepose-geo (ADR-154-class sweep).
Two real bugs, each pinned by a fails-on-old test:
1. terrain.rs parse_hgt — usize underflow panic on degenerate input.
`side = sqrt(n_samples)`; for empty / sub-2x2 buffers side <= 1, so
`1.0 / (side - 1)` underflows `usize` (panic "attempt to subtract with
overflow" in debug; wraps to a huge value in release → garbage/inf
cell_size_deg that poisons every ElevationGrid::get). A truncated HTTP
body or a 404 HTML page reaches parse_hgt. Now bails with a clear error
when side < 2.
2. coord.rs haversine — asin domain overflow → NaN for (near-)antipodal
points. Floating rounding can push `h.sqrt()` to 1.0 + ~4e-16, and
`asin(>1)` is NaN (verified: pair (-44.4994,-178.95722)→(44.49939999,
1.04278001) yields h=1.0000000000000004). A NaN distance silently breaks
all downstream `<`/`>` comparisons. Clamp into [0,1] before asin.
Also pins the ±90° pole-singularity (cos(lat)=0 division) as no-panic; the
ENU transform itself is unchanged (no behavior change for valid inputs).
Tests: wifi-densepose-geo 9→15 lib (6 new), 8 integration unchanged. 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(pointcloud robustness): pin NaN-state-poisoning resistance + degenerate voxel fusion
Numerical-robustness audit of wifi-densepose-pointcloud. No bug found — the
crate is confirmed-robust against the proven NaN-state-poisoning class that bit
calibration/vitals. This adds regression pins documenting why:
1. csi_pipeline.rs — persistent auto-accumulating state (occupancy EMA,
vitals) is provably self-healing. The UDP parser only emits finite
amplitudes/phases (sqrt/atan2 of i8), and even an adversarial hand-built
CsiFrame with NaN/inf amplitudes+phases cannot latch non-finite state:
motion_score = (NaN/100).min(1.0) → 1.0; breathing path → 0 → clamp(5,40)
→ 5.0; tomography EMA uses only integer rssi. The new test injects 40
poisoned frames and asserts occupancy/vitals stay finite AND the pipeline
recovers to an in-range estimate afterward — so a future refactor that drops
a `.min`/`.clamp` self-heal would fail this pin.
2. fusion.rs — fuse_clouds voxel averaging is div-by-zero-safe (per-voxel
count >= 1 by construction). Pins empty / single-point / all-coincident
inputs as no-panic with finite output.
No behavior change. Tests: wifi-densepose-pointcloud 18→22 (4 new), 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(geo/pointcloud robustness): CHANGELOG + ADR-154 sibling-crate sweep note
Record the wifi-densepose-geo + wifi-densepose-pointcloud numerical-robustness
audit under CHANGELOG [Unreleased] → Fixed, and a sibling-crate-extension note
on the ADR-154 horizon ledger (these crates are outside ADR-154's signal scope
but the sweep is the same ADR-154 class).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(vitals): self-heal IIR filters after non-finite CSI frame (ADR-021/ADR-158 §A1)
The 2nd-order resonator bandpass_filter in BreathingExtractor and
HeartRateExtractor latches each output y[n] into the filter state
(y1/y2). A single non-finite amplitude residual from a corrupt CSI
frame produced a NaN output that was written into the state. The
existing extract() is_finite() guard dropped that one sample from the
history buffer but never sanitized the poisoned filter state, so every
subsequent output stayed NaN, was rejected too, and the sliding-window
history never refilled: breathing AND heart-rate extraction went
silently dead (returning None forever) until reset().
On the vitals alert path this is a safety-relevant denial of service —
one bad frame stops monitoring with no error surfaced. Same class as the
calibration NaN bug (ADR-154 §3) and the firmware vitals fixes
(#998/#996/#987): prior hardening guarded the history boundary but not
the filter-state boundary.
Fix: when bandpass_filter computes a non-finite output it resets the IIR
state to default and returns 0.0, so the resonator recovers on the next
clean frame (the 0.0 is still dropped by the caller's finite-check, so no
spurious sample enters history).
Also de-magic the safety-critical HR physiological plausibility band into
named HR_PLAUSIBLE_MIN_BPM/HR_PLAUSIBLE_MAX_BPM consts (value-identical
40/180 BPM).
Pinned by:
- breathing::tests::nan_frame_does_not_permanently_poison_filter (FAILS pre-fix)
- breathing::tests::inf_mid_stream_does_not_freeze_history (FAILS pre-fix)
- heartrate::tests::nan_frame_does_not_permanently_poison_filter (FAILS pre-fix)
- heartrate::tests::pure_noise_is_never_reported_valid (fabricated-vital negative)
- heartrate::tests::plausibility_band_constants_pinned (de-magic value pin)
wifi-densepose-vitals --no-default-features: 55->60 lib tests, 0 failed.
Workspace green (3370 passed, 0 failed). Python proof unchanged (vitals
off the deterministic proof's signal path).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(vitals): record IIR NaN/inf self-heal fix (ADR-021, CHANGELOG)
Document the wifi-densepose-vitals filter-state poisoning fix in ADR-021
Implementation Notes (parallel to the firmware #998/#996/#987 robustness
class) and add a CHANGELOG [Unreleased] Fixed entry. Notes the confirmed
clean dimensions with evidence (flat -> None; noise -> low-confidence
Unreliable, never Valid; harmonic-rich breathing -> not a confident false
HR; out-of-band BPM clamped).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(calibration): drop non-finite samples in Features::from_series (ADR-151)
A single NaN/inf scalar sample (corrupt CSI frame) poisoned mean/variance
into NaN, which — baked into a persisted PresenceSpecialist::threshold —
silently disabled presence detection (every `f.variance > NaN` is false),
no error raised. extract.rs is the live-inference + training feature path,
yet (unlike geometry_embedding.rs) had no non-finite guard.
Fix at the production boundary: filter non-finite samples before computing
any statistic; an all-non-finite series degrades to Features::ZERO, same as
the empty series. Value-identical for all-finite input (full_loop + existing
extract tests unchanged). Pinned by two fails-on-old tests.
Co-Authored-By: claude-flow <ruv@ruv.net>
* refactor(calibration): de-magic specialist thresholds to named consts (ADR-151)
Promote the bare default min-score literals (breathing 0.25, heartbeat 0.3)
and the anomaly score scale / label cutoff (2.0× spread, > 0.5) to documented
named consts. Value-identical — pinned by characterization tests asserting the
consts equal the prior literals and the gate boundary (score >= floor).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(calibration): record ADR-151 review — NaN fix + clean dimensions
CHANGELOG [Unreleased] Security entry and ADR-151 §6.1 review note for the
beyond-SOTA correctness+security review: NaN-poisoning fail-closed fix,
file/path (no I/O in crate), untrusted-load, receipt/hash (absent), and the
clean numerical paths — all with evidence.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-api security): auth-gate GET /api/ (HC-API-AUTH-01, ADR-161)
`rest::api_root` took no headers and unconditionally returned
`200 {"message":"API running."}`, while every sibling REST route gates
on `BearerAuth::from_headers`. HA's `APIStatusView` inherits
`requires_auth = True`, so `/api/` must return 401 for a missing/wrong
bearer — HA clients use it as a token-validation probe, so a 200 told a
bad-token client its token was valid and let an unauthenticated party
confirm a live endpoint. LOW severity (static body, no data leak),
reported at true severity.
Fix: `api_root(headers, State)` validates the bearer like `get_config`.
Pinned by fails-on-old tests (200 -> assert 401):
- api_root_rejects_missing_bearer
- api_root_rejects_wrong_bearer
guarded by api_root_accepts_correct_bearer (still 200 with valid token).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-api security): recover WS subscription on broadcast lag (HC-WS-LAG-01, ADR-161)
`subscribe_events`'s per-subscription task matched `Err(_) => break` on
both broadcast `recv()` arms. `RecvError::Lagged(n)` (a slow consumer
falling >EVENT_CHANNEL_CAPACITY=4,096 events behind) is recoverable —
the bus doc says "Lagged receivers must re-sync" and HA keeps the
subscription alive across a lag. The old code treated the first lag as
fatal, so after an event burst the client's stream went permanently
silent with no error frame — a self-inflicted event-delivery DoS under
load. LOW severity.
Fix: `Lagged(_) => continue` (skip dropped window, re-sync),
`Closed => break`, on both the system and domain arms.
Pinned by subscription_survives_broadcast_lag: subscribes, floods 6,000
filtered events past the 4,096 capacity to force a Lagged, then asserts
a subsequent subscribed event is still delivered (old code: 5s timeout).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(homecore-api security): record HC-API-AUTH-01 + HC-WS-LAG-01 review (ADR-161)
CHANGELOG [Unreleased] Security entry + ADR-161 addendum documenting the
beyond-SOTA network-API review: two LOW bugs fixed (unauthenticated
GET /api/; WS subscription killed on broadcast lag) and the
auth/traversal/injection/info-leak/CORS dimensions confirmed clean with
evidence (no traversal surface — in-memory DashMap + EntityId allowlist;
HashSet token compare, not a byte-== timing oracle).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(bfld): route process_to_frame payload through PrivacyGate (ADR-141 privacy bypass)
BfldPipeline::process_to_frame stamped the frame header with the active
privacy class but serialized the caller-supplied BfldPayload UNCHANGED via
BfldFrame::from_payload. This let a frame labeled Anonymous(2) or
Restricted(3) carry the full identity-leaky compressed_angle_matrix
(+ amplitude/phase proxies, csi_delta) that PrivacyGate::demote is documented
and tested (privacy_gate_demote.rs) to strip at exactly those classes.
A NetworkSink accepts class >= Derived(1), so such a frame would publish the
beamforming angle matrix — the identity surface — across the node boundary
despite its restrictive class byte. The class byte lied about payload content.
Fix: after building the frame at the active class, apply PrivacyGate::demote to
the same class. demote() strips sections by target-class threshold (independent
of any class transition), so a same-class demote performs no class change but
brings the payload into policy compliance. Research classes (Raw/Derived) keep
the full payload — demote is a no-op there.
Pinned by three fails-on-old tests in pipeline_to_frame.rs:
- process_to_frame_at_anonymous_strips_identity_leaky_sections (FAILED pre-fix)
- process_to_frame_in_privacy_mode_strips_amplitude_and_phase (FAILED pre-fix)
- process_to_frame_at_derived_preserves_full_payload (guards against over-strip)
The pre-existing round-trip test is updated to assert the gated payload.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(bfld): JSON-escape zone_id in MQTT state-topic payload
render_events emitted the zone_activity payload as format!("\"{zone}\"") with no
escaping, while ha_discovery.rs already escapes operator-controlled strings via
push_str_field. A zone name containing a double-quote or backslash therefore
produced malformed / injectable JSON on the state topic that Home Assistant
parses (e.g. zone `a"b` -> payload `"a"b"`).
Fix: add json_string_literal() mirroring ha_discovery's escaping (", \, \n, \r,
\t, control chars) and use it for the zone payload. Value-identical for normal
zone names (living_room etc.).
Pinned by zone_payload_escapes_json_metacharacters (FAILED pre-fix); the
existing zone_payload_is_json_string_with_quotes still passes unchanged.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-141): record bfld privacy+security review findings + CHANGELOG
Document the two fixed bugs (process_to_frame privacy-bypass; zone_id JSON
injection) and the dimensions confirmed clean (event-field gating, witness/hash
framing, fail-closed) in ADR-141, plus CHANGELOG [Unreleased] Security/Fixed
entries.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(engine): length-prefix witness fields to close domain-separation collision
The BLAKE3 trust witness concatenated model_version, calibration_version,
and privacy_decision boundary-to-boundary, with the variable-length evidence
list lacking an explicit count. A string straddling a field boundary (e.g. a
per-room adapter id absorbing the leading bytes of the calibration epoch, or a
model_version absorbing a trailing evidence ref) collided with a different
trust decision — silently un-distinguishing two distinct privacy-relevant
inputs and defeating the ADR-137 tamper/drift audit guarantee. model_version
is operator-influenceable via the adapter id (ADR-150 §3.4), so the ambiguity
was reachable.
Fix: domain-tag the hash and length-prefix every field (8-byte LE length),
plus an explicit evidence count. Pinned by two fails-on-old tests:
witness_distinguishes_model_calibration_boundary and
witness_distinguishes_evidence_model_boundary.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(engine): pin privacy monotonicity, fail-closed boundaries; de-magic constants
Review hardening for the governed-trust cycle (no behavior change):
- forced_contradiction_never_relaxes_class: property test over all 5 privacy
modes proving a forced contradiction only ever raises the emitted class byte
(more restrictive) and a clean cycle emits exactly the base class — the
ADR-141/120 information-only-removed invariant.
- empty_cycle_fails_closed: a zero-frame cycle errors (fusion NoFrames),
emits no SemanticState, and does not advance the cycle counter.
- single_node_cycle_is_well_formed: characterizes the n=1 boundary (no mesh,
no directional, base class, witness still emitted) — documents single-node
sensing as a valid non-demoting mode, not a bypass.
- De-magicked the engine-construction literals (coherence accept gate, ADR-143
SLAM discovery + static-anchor thresholds) into named documented consts,
value-identical, pinned by engine_constants_match_prior_values.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(engine-review): record witness domain-separation fix + monotonicity clean bill
CHANGELOG [Unreleased] Security entry and review notes appended to ADR-137
(witness domain-separation fix) and ADR-141 (privacy monotonicity confirmed
clean over all 5 modes, fail-closed boundaries pinned).
Co-Authored-By: claude-flow <ruv@ruv.net>
Bumps vendor/rufield to add --source live --upstream: the dashboard ingests
RuView's /ws/field events, verifies each ed25519 receipt on ingest (forged
events flagged, never fused), and renders real RuView FieldEvents through the
same display path. Honest SYNTHETIC/LIVE/DISCONNECTED banner, mutually
exclusive, never mislabeled (409 on /api/run in live mode). Closes the
RuView↔RuField visual loop (ADR-262 surfaces). 26 tests, 0 failed.
Co-authored-by: ruv <ruvnet@gmail.com>
* feat(ADR-262 P3): live RuField surface — RuView sensing speaks RuField on /api/field + /ws/field
Wire the P1 `wifi-densepose-rufield` bridge into the live
`wifi-densepose-sensing-server` so the governed sensing cycle emits real
signed RuField `FieldEvent`s on two additive endpoints.
- Cargo: add the `wifi-densepose-rufield` path dep (the single coupling
point, ADR-262 §5.4 — no new RuView-internal coupling).
- New `src/rufield_surface.rs` (kept out of the 8k-line main.rs):
`FieldSurface` holds a dedicated ed25519 `Signer` + a bounded ring of
recent events + the `/ws/field` broadcast topic; `GET /api/field` and
`GET /ws/field` handlers; a standalone `router()` for isolated testing.
- Signer (defers the P2 key decision, ADR-262 §8 Q1): a STANDALONE
dev/sensing key from `WDP_RUFIELD_SIGNING_SEED`, else a deterministic
dev default with a logged WARN. Reusing the `cog-ha-matter` Ed25519
key is the deferred P2 call — P3 does not pre-empt it.
- Tap: at the ESP32 governed-trust cycle (`main.rs` ~5886 observe_cycle
/ ~5938 SensingUpdate build), `emit_rufield_event` joins the cycle's
features/classification/signal_field with the engine's
effective_class/demoted trust state into a `SensingSnapshot` and
surfaces it via the bridge. Existing endpoints (`/ws/sensing` etc.)
are unchanged — purely additive.
- Privacy egress: `network_egress_allowed` is fail-closed for an
unattended live surface — only P1/P2 leave the box; P0 raw and
P3/P4/P5 (identity/biometric/aggregate) are held edge-local. A
`Derived` cycle maps to P4/P5 and never surfaces.
- No-phantom: `emit` drops no-presence cycles (no fabricated events).
Gates (tests/rufield_surface_test.rs, tower::oneshot, 4/0): well-formed
signed event (WifiCsi, P2 not P1, is_fusable, real timestamp); empty
cycle → no phantom; Derived trust never surfaces; mixed stream surfaces
only egress-safe events.
Honesty (ADR-262 §0/§6): real plumbing on a live endpoint, NOT accuracy.
Single-link CSI with its existing caveats (no validated room-coordinate
accuracy); dedicated dev signing key pending the P2 ownership decision;
no accuracy claim.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(ADR-262 P3): mark P1+P3 implemented; document /api/field + /ws/field; CHANGELOG
- ADR-262 Status → "P1 + P3 implemented"; add a P3 implementation-status
block (tap site, endpoints, dedicated dev signer deferring the §8 Q1
key decision, fail-closed egress, gates). Keep the honesty framing:
real plumbing on a live endpoint, not accuracy.
- CHANGELOG [Unreleased]: add the ADR-262 P3 entry.
- user-guide: add `/api/field` to the REST table + a "RuField surface
(ADR-262 P3)" section covering `/api/field` + `/ws/field`, the
fail-closed P1/P2-only egress, the WDP_RUFIELD_SIGNING_SEED dev key,
and the no-accuracy honesty note.
Co-Authored-By: claude-flow <ruv@ruv.net>
* ci: checkout submodules everywhere + Dockerfile copies vendor/rufield
Making wifi-densepose-rufield (ADR-262 bridge) a v2 workspace member means
EVERY cargo-on-workspace context must have the vendor/rufield submodule
present (cargo loads all member manifests). P1 only fixed the rust-tests
job; this adds `submodules: recursive` to all workflow checkouts that run
cargo (mqtt-integration was failing on the missing submodule manifest), and
makes Dockerfile.rust COPY vendor/rufield/ to /vendor/rufield (matches the
bridge's ../../../vendor/rufield path-dep under the collapsed Docker layout).
update-submodules.yml left alone (it manages submodules itself).
Co-Authored-By: claude-flow <ruv@ruv.net>
---------
Co-authored-by: ruv <ruvnet@gmail.com>
* feat(rufield): ADR-262 P1 — wifi-densepose-rufield anti-corruption bridge
New v2 workspace member that converts RuView WiFi-CSI sensing output into
signed RuField FieldEvents. Path-deps the vendor/rufield submodule crates
(rufield-core/-provenance/-privacy/-fusion); single coupling point between
RuView and the standalone RuField MFS spec (ADR-262 §5.4).
- SensingSnapshot: owned primitives mirroring SensingUpdate + TrustedOutput
(no dependency on wifi-densepose-sensing-server).
- snapshot_to_field_event(): builds a WifiCsi FieldTensor + Observation,
derives a real position from the signal-field peak (never fabricated),
real sha256 provenance + ed25519 signature (synthetic=false).
- map_privacy() (§3.3 crux): maps by information content, NEVER byte value —
Derived (byte 1) → P4/P5, never P1; fail-closed demotion floor to P2.
P1 gates (tests/p1_gates.rs): round-trip serde, is_fusable verified receipt,
RuFieldFusion::ingest accept + infer runs, privacy-safety (Derived never P1),
full §3.3 table, fail-closed demotion, determinism, no-fabricated-position.
15 tests pass (5 unit + 9 integration + 1 doc), 0 failed.
Honesty: P1 plumbing (tested conversion + safe privacy mapping), NOT wired
into the live server (P3) and NOT an accuracy claim.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-262): mark P1 implemented + CI submodules:recursive + CHANGELOG/CLAUDE
- ADR-262 Status → "Proposed — P1 implemented"; add §0.1 Implementation
status (the bridge crate + the five P1 gates that pass; defers the
provenance-carrier reuse, P3 live wiring, and P4 multi-modality).
- ci.yml: add `submodules: recursive` to the rust-tests checkout so the new
crate's `vendor/rufield` path-deps resolve in CI (they fail otherwise even
though the workspace build passes locally with the submodule present).
- CHANGELOG [Unreleased]: P1 bridge entry (kept alongside the upstream
ADR-262 research entry).
- CLAUDE.md: crate table row for `wifi-densepose-rufield`.
Co-Authored-By: claude-flow <ruv@ruv.net>
Researched integration ADR: thin wifi-densepose-rufield bridge crate
(rvcsi pattern), live SensingServerAdapter emitting signed FieldEvents,
vertical fusion composition (ruvsense within-WiFi → rufield cross-modal),
and ONE canonical privacy/provenance model (RuView effective_class →
RuField P0-P5 at egress; reuse cog-ha-matter SHA-256+Ed25519 receipt).
Key finding: RuView has 2 privacy enums + 3 witness mechanisms; the
Derived(byte=1)<Anonymous(byte=2)-but-carries-identity trap means the
bridge must map by information content, not byte value. Plumbing
architecture, not accuracy (real-CSI is unlabeled replay today).
Co-authored-by: ruv <ruvnet@gmail.com>
* feat(ruvector): real float HNSW + SymphonyQG-style quantized-traversal index (ADR-261)
Adds the graph-ANN index the ruvector retrieval path was missing (ADR-156
§5 #1 noted there was no HNSW baseline to measure SymphonyQG against).
- hnsw.rs: correct float HNSW (Malkov & Yashunin) — multi-layer NSW graph,
ef_construction/ef_search, Algorithm-4 neighbour selection, seeded-
deterministic level assignment (SplitMix64, reused from rotation.rs),
L2 + cosine, brute-force ground truth, full degenerate-case guards.
recall@10 correctness gate >=0.95 vs brute force (L2 + cosine).
- hnsw_quantized.rs: SymphonyQG-style variant — same graph, traversal scored
by cheap 1-bit Hamming over the RaBitQ Pass-2 rotated sign code, final
exact-float rerank.
- ann_measure.rs: shared deterministic planted-cluster fixture + recall/QPS
measurement (ann_bench_report is the ADR source of truth).
Fixes an index-out-of-bounds bug the recall gate caught: insert wired
bidirectional edges before pushing the node's own link row. +20 tests,
ruvector lib 131->151, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* bench(ruvector): criterion ann_bench for HNSW vs quantized vs linear (ADR-261)
Times the same shared ann_measure fixture/indices through criterion so the
bench and the report test can never measure different graphs.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-261): graph-ANN index ADR with MEASURED HNSW vs quantized verdict
ADR-261 (Accepted): float HNSW ~25x QPS over linear scan at recall >=0.99
(the baseline ADR-156 said was missing). Honest negative: the 1-bit
quantized traversal is too coarse to beat float HNSW at equal recall at
N=10k (best recall 0.738, no >=0.90 equal-recall point) — the SymphonyQG
3.5-17x is NOT reproduced by our 1-bit construction; expected crossover at
large N + a multi-bit code. Caveat: our HNSW + our quant, not SymphonyQG's
system — direction tested, not a 1:1 reproduction.
ADR-156 §5 #1 + §8 backlog: CLAIMED -> MEASURED-direction-tested.
CHANGELOG [Unreleased] entry.
Co-Authored-By: claude-flow <ruv@ruv.net>
ADR-260 (Accepted — v0.1 reference stack): RuField, the open specification
for camera-free multimodal field sensing — one FieldEvent/FieldTensor/
FusionGraph/PrivacyClass/ProvenanceReceipt model above WiFi CSI/CIR/BFLD,
UWB, BLE Channel Sounding, mmWave radar, ultrasound, subsonic, infrared,
and quantum sensors.
Published standalone as github.com/ruvnet/rufield and vendored here as the
vendor/rufield submodule (the vendor/rvcsi pattern — not a v2/ workspace
member). v0.1 reference stack: 6 crates, 60 tests/0 failed, clippy-clean.
All benchmark metrics SYNTHETIC (simulator ground truth, no hardware).
Co-authored-by: ruv <ruvnet@gmail.com>
* fix(firmware): gate phantom persons + add presence hysteresis (#998, #996)
Two ESP32 edge-vitals logic bugs in edge_processing.c. Both are
robustness/logic fixes — NOT validated-accuracy claims. True count/PCK
vs labelled ground truth remains hardware/data-gated (COM9 ESP32-S3).
#998 — n_persons over-counted (reported 4 for one person):
update_multi_person_vitals() split top-K subcarriers into top_k_count/2
groups and marked EVERY group active, so one body's multipath always
read the full EDGE_MAX_PERSONS. Added two pure, host-testable helpers:
- count_distinct_persons(): per-group energy gate
(EDGE_PERSON_MIN_ENERGY_RATIO) + spatial dedup
(EDGE_PERSON_MIN_SC_SEP) so weak/adjacent multipath groups don't
count as separate bodies. Strongest group always counts (>=1).
- person_count_debounce(): a gated count must hold
EDGE_PERSON_PERSIST_FRAMES consecutive frames before it's emitted,
so a single noisy frame can't promote a phantom.
The active flags now mark only the strongest stable_count groups.
#996 — presence flag flickered at ~50cm despite high presence_score:
the bare `score > threshold` compare chattered on a noisy score
(field-observed 2.6-26.7 frame-to-frame). Replaced with a Schmitt
trigger + clear-debounce (presence_flag_update): assert above
threshold, hold in the dead band down to threshold *
EDGE_PRESENCE_HYST_RATIO, clear only after EDGE_PRESENCE_CLEAR_FRAMES
consecutive sub-low frames. presence_score itself is unchanged and
still emitted for consumer-side thresholding.
All thresholds are named, documented constants in edge_processing.h.
Firmware builds clean for esp32s3 (idf.py build RC=0).
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(firmware): host C99 tests for vitals count + presence logic (#998, #996)
test/test_vitals_count_presence.c pins the two fixes with deterministic
host-buildable tests (no ESP-IDF needed). 13 cases / 22 assertions, all
passing under gcc 13 -Wall -Wextra:
#998 count gate: single strong signature + multipath -> count==1;
two well-separated -> 2; two strong-but-adjacent -> 1 (dedup);
no signal -> 0; three well-separated -> 3.
#998 debounce: transient spike rejected; sustained change accepted;
flapping count stays stable.
#996 presence: dithering trace -> stable flag (no flicker); brief dips
held by clear-debounce; genuine departure clears within hold window;
dead-band holds state.
The named tuning constants are #include'd from the real
edge_processing.h so the test and firmware can never disagree on
thresholds. `make run_vitals` / `make host_tests` added; binaries
gitignored.
Hardware-gated caveat documented in the test header: these pin the
decision LOGIC; the exact energy/separation/hysteresis values that best
match a real room vs labelled occupancy remain on-device tuning.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs: record ESP32 vitals count/presence fixes (#998, #996)
CHANGELOG [Unreleased] Fixed: root cause + fix + named constants + test
+ explicit hardware/data-gated caveat for both bugs.
ADR-021 Implementation Notes: dated 2026-06 entry noting the edge-path
person-count + presence-flicker fixes are boolean/count emission-logic
fixes, not a validated-accuracy claim; thresholds pending on-device
calibration.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(sensing-server): emit real field-derived person position/motion to /ws/sensing (#1050)
The Observatory 3D figure never animated because the sensing_update WS
frame carried no per-person position/motion_score/pose — only image-space
keypoints. The FigurePool/PoseSystem (and demo-data.js's own contract)
animate each figure from persons[i].position (room-world), .motion_score
(0..100), and .pose; none were on the live stream.
Honest scope (Case 2): the pipeline has no calibrated per-person room
localizer or per-person skeletal pose. New field_localize module extracts
the strongest peak(s) from the real signal_field grid (subcarrier
variances x motion-band power) and maps the peak cell to Observatory world
coords with the exact _buildSignalField transform. motion_score is the
measured motion_band_power passed through; pose is set only from a real
aggregate posture estimate, else None (never a fabricated skeleton).
Empty/below-threshold field -> persons: [] (no phantom); present person
with no resolvable peak keeps position [0,0,0], not invented coords.
attach_field_positions runs after the tracker step at all five broadcast
sites. New position/motion_score/pose fields added to both PersonDetection
structs. No UI change needed — the Observatory already reads these fields.
Tests: field_localize peak/coordinate/empty/separation units +
observatory_persons_field_position_tests (known-peak -> emitted position,
empty-room -> no phantom, pose real-or-None, below-threshold honesty).
sensing-server bin 441->451, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(changelog): record #1050 Observatory persons position/motion fix
Co-Authored-By: claude-flow <ruv@ruv.net>
* perf(signal): hoist FFT planner across subcarriers (ADR-154 §7.4 #20)
compute_multi_subcarrier_spectrogram called compute_spectrogram once per
subcarrier, and each call built a fresh FftPlanner + re-planned the same
length-window_size FFT. Hoist the plan + window out of the per-subcarrier
loop via a new compute_spectrogram_with_plan core that takes a pre-planned
Arc<dyn Fft> and pre-built window. compute_spectrogram delegates to it
(unchanged behaviour); the multi-subcarrier path plans once and reuses.
MEASURED-HOT (dsp_perf_bench, this box): at 56 subcarriers, window 128,
fresh-planner-per-subcarrier 467.88 µs -> hoisted-plan 254.75 µs = 1.84x;
window 256: 627.27 µs -> 448.39 µs = 1.40x. Plan-forward cost alone is
~1.86 µs (w128), x56 subcarriers ~= the removed delta.
Output is bit-identical: multi_subcarrier_hoisted_plan_bit_identical
compares f64::to_bits of every spectrogram value + freq/time resolution
against the per-call fresh-planner path across all 4 window functions x
{power,magnitude} on a 56-subcarrier matrix. The numeric STFT body is the
old loop verbatim; only plan/window construction is lifted.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(signal): boundary/tolerance tests for ADR-154 §7.4 #14#16#19
Three "+ test" backlog gaps closed — pure additions, no behaviour change
(phase_align refactor is internal: estimate_phase_offsets still returns the
identical offset vector; a counted core is split out only to observe the
iteration count).
#14 cir.rs fft_operator — fft_operator_within_tolerance_of_dense_canonical56:
the opt-in FFT Φ/Φᴴ path changes the witness hash, so pin it numerically
CLOSE to the dense path (not silently divergent). Asserts the full Cir
output (every tap within 1e-2·dominant, dominant idx/ratio, active_tap_count,
ranging_valid, rms_delay_spread) on the production canonical-56 config
across τ ∈ {20,50,90} ns. Extends the existing HT20/single-τ test.
#16 phase_align.rs — refinement_terminates_at_iteration_cap_when_not_converging:
forces non-convergence (tolerance=0.0, unreachable) and asserts the loop
runs exactly max_iterations then returns — proving the cap, not convergence,
bounds the loop (no infinite spin). Companion
refinement_converges_before_cap_on_easy_input proves the cap is an upper
bound, not the only exit.
#19 csi_ratio.rs — ratio_finite_at_and_below_1e_12_epsilon: the module
implements the CSI ratio as the conjugate product H_i·conj(H_j) (no
division), so it is finite even at/below the 1e-12 magnitude boundary a
naive H_i/H_j division would need an epsilon to guard. Pins finiteness +
bit-exact conjugate product at the boundary (zero target → zero, never
inf/NaN), through the amplitude/phase extraction.
cargo test -p wifi-densepose-signal --no-default-features --lib: 447 passed,
0 failed; --features cir --lib: 447 passed, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-154): record Milestone-2 P2-perf verdicts + boundary tests (§7.4)
§7.4: #20 MEASURED-HOT (1.40–1.84× spectrogram FFT-plan hoist, bit-identical);
#5/#6/#7 MEASURED-NULL (benched, not hot, left as-is — sub-µs / stack-only /
alloc-once); #8 MEASUREMENT-ONLY (per-call 56×56 eigh cost; eigenvalue/BLAS
backend un-buildable on this Windows host, number deferred to a BLAS box, NOT
fabricated; also corrects the finding — extract_perturbation reuses cached
modes, the recompute is in estimate_occupancy). #14/#16/#19 RESOLVED (tolerance
/ convergence-cap / epsilon-boundary tests). Updated §7.4 intro + Horizon-ledger
(deferred count 41→36). CHANGELOG [Unreleased] entry added.
Co-Authored-By: claude-flow <ruv@ruv.net>
* bench(signal): committed P2 bench-first benches (ADR-154 §7.4 #5/#6/#7/#8/#20)
New dsp_perf_bench.rs backs every Milestone-2 perf verdict with a committed
criterion bench — no speedup claimed without a before/after number here, and
a benched NULL is the proof a micro-opt was unnecessary (the §5.x "already
amortized" pattern). Registered in Cargo.toml [[bench]].
MEASURED (this box, criterion medians):
#20 spectrogram_multi_subcarrier (fresh vs hoisted plan):
MEASURED-HOT — 467.88→254.75 µs (1.84x) @ sc56/w128; 627.27→448.39 µs
(1.40x) @ sc56/w256. Optimized in the prior commit.
#5 multistatic_attention/weights: MEASURED-NULL — 181 ns (2 nodes) ..
848 ns (8 nodes); sub-µs, no hot-path alloc — left as-is.
#6 tomography_reconstruct/solve: MEASURED-NULL — 47.5 µs (16 links) /
60.4 µs (32 links) for a full 50-iter ISTA solve; the 2 per-solve voxel
buffers (~4 KB) are negligible vs O(iters·links·voxels) compute, and
reconstruct(&self) reuses them across iterations already — left as-is.
#7 pose_kalman_update/cycles: MEASURED-NULL — 150 ns (17 kpts) / 2.82 µs
(170); the Kalman "gain matrices" are fixed-size STACK arrays
([[f32;3];6]), zero heap — nothing to reuse — left as-is.
#8 field_model_occupancy (eigenvalue feature): MEASUREMENT-ONLY — quantifies
the per-call n×n eigendecomposition cost; incremental SVD is a sized
future project, not attempted (number recorded in ADR-154 §7.4).
Reproduce:
cargo bench -p wifi-densepose-signal --no-default-features --bench dsp_perf_bench
cargo bench -p wifi-densepose-signal --bench dsp_perf_bench # adds #8
Cargo.lock: dev-dep (criterion/clap) graph + crate version bumps from the
build; no runtime-dependency change.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(hardware): constant-time HMAC sync-beacon tag compare (ADR-157 §B4)
AuthenticatedBeacon::verify compared the 8-byte HMAC-SHA256 tag with
`self.hmac_tag == expected`, which short-circuits on the first differing
byte and leaks, via verification latency, how many leading bytes a forged
tag matched — a byte-by-byte tag-recovery oracle (~256·N trials vs 256^N).
Replace with a hand-rolled branch-free `constant_time_tag_eq`: XOR-accumulate
every byte difference into a single u8 with no early exit, compare to zero
once. `#[inline(never)]` + `core::hint::black_box(diff)` resist the optimizer
reintroducing a short-circuit or a non-constant-time memcmp; length mismatch
returns false without inspecting contents. No new dependency — ADR-157 had
deferred this only to avoid the `subtle` crate; a fixed 8-byte compare needs
none.
Test (hard gate): tag_compare_is_constant_time_shape — equal / first-differ /
last-differ / all-differ / length-mismatch + end-to-end verify() last-byte
tamper. Proven to fail on a last-byte-skipping constant-time bug. A coarse
timing smoke check (tag_compare_timing_invariance_smoke) is #[ignore]d to
avoid CI flakiness. Grade MEASURED (constant-time construction).
ADR-157 §8 §B4 → RESOLVED. wifi-densepose-hardware: 164 passed / 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(wifiscan): MEASURE native wlanapi.dll vs netsh throughput (ADR-157 §5 #4)
ADR-157 §5 #4 recorded the native wlanapi.dll multi-BSSID fast path as
"asserted but NOT implemented; live scanner is the ~2 Hz netsh shim". Audit
finding: that status is stale — wlanapi_native::scan_native already implements
the real WlanOpenHandle → WlanEnumInterfaces → WlanGetNetworkBssList →
WlanFreeMemory/WlanCloseHandle FFI (handle cleanup on all exits, length-bounded
buffer walks, #[cfg(windows)] with typed Unsupported off-Windows), and
WlanApiScanner::scan_instrumented already wires it native-first with a netsh
fallback. The missing piece was an honest MEASUREMENT.
Add benchmark_backend(backend, window): drives one specific backend over a
fixed wall-clock window so netsh is timed independently (the existing
benchmark() picks native-first and so never measures netsh on a box where
native works). Returns None for an unavailable native path (honest negative,
not a fabricated number).
MEASURED on this box (Intel Wi-Fi 7 BE201 320MHz, 2026-06-13), 10 s window:
native 21.42 Hz vs netsh 3.84 Hz = 5.57× (mean 5.0 BSSIDs/scan each).
native-only run: 18.0 Hz. 50/50 back-to-back native scans, no handle leak.
A real positive result — NOT a fabricated 10×. Achieved 21.4 Hz is in the
asserted >2 Hz regime, below the asserted 10–20 Hz upper bound.
Tests (live-WLAN, #[ignore] for CI, RUN here):
measure_native_vs_netsh_throughput, native_scans_dont_leak_handles,
measure_native_scan_rate. Non-ignored pin native_scan_runs_real_ffi_on_windows
(pre-existing) stays green. wifi-densepose-wifiscan: 94 passed / 0 failed.
ADR-157 §5 #4 + §8 → MEASURED (was ACCEPTED-FUTURE / CLAIMED-unmeasured).
Co-Authored-By: claude-flow <ruv@ruv.net>
* refactor(train): hoist canonical PCK/OKS to un-gated metrics_core; fold test_metrics onto production (ADR-155 M1 §8)
ADR-155 §8 deferred item: test_metrics.rs reference kernels validated
production against their OWN reimplementation — a test that cannot catch a
canonical-impl bug (both could be wrong the same way).
- Extract canonical_torso_size / pck_canonical / oks_canonical / sigmas /
bounding_box_diagonal into a new NON-tch-gated `metrics_core` module, so
the single metric definition is reachable under
`cargo test --no-default-features` (the `metrics` module is tch-gated).
`metrics` re-exports every item → still exactly ONE implementation.
- Rewrite tests/test_metrics.rs to assert the PRODUCTION pck_canonical /
oks_canonical equal hand-computed fixtures (not a reimplementation):
canonical_pck_matches_hand_computed_fixture (corr=3/total=4/pck=0.75),
hip↔hip normalizer pin, zero-visible⇒0.0, OKS perfect⇒1.0, fake-Gold pin.
- Keep an INDEPENDENT raw-threshold reference kernel only as a differential
cross-check: test_kernel_agrees_with_canonical asserts it AGREES with
canonical where torso==1.0 (genuine cross-check, not duplication).
Grade: MEASURED. test_metrics 10→12 tests, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(sensing-server): relabel divergent live PCK/OKS so they're never conflated with canonical (ADR-155 M1 §2.1/§8 Goal C)
Goal C named training_api.rs:804 (torso-HEIGHT PCK). Auditing it surfaced
TWO findings the ADR-155 §1 table missed:
1. training_api.rs is an ORPHAN file — not declared `mod` in lib.rs OR main.rs,
so it does NOT compile into the crate. It does not drive the live server.
2. The REAL live `best_pck`/`best_oks` (main.rs training path → RVF metadata
JSON read by model_manager.rs) come from trainer.rs:
- `pck_at_threshold` = RAW-threshold PCK, NO torso normalization (the most
divergent kind), printed/serialized as bare "PCK@0.2".
- `oks_map` calls `oks_single(area=1.0)` = the EXACT fake-Gold pattern
ADR-155 §2.1 claimed closed elsewhere — still live here, inflating best_oks.
Resolution = RELABEL (torso/raw math is load-bearing on different data; the
pub fns can't be renamed without breaking API; sensing-server has no train/
ndarray dep). Honest unify is a tracked §8 backlog item.
- training_api.rs: `compute_pck` → `compute_pck_torso_height` + divergence doc;
val_pck/best_pck/val_oks struct fields documented as torso-HEIGHT proxies;
logs say `pck_torso_h@0.2`. Test torso_pck_is_labelled_distinctly_from_canonical.
- trainer.rs (LIVE): `pck_at_threshold` documented raw-unnormalized; `oks_map`
area=1.0 flagged fake-Gold; test pck_at_threshold_is_raw_unnormalized_not_canonical.
- main.rs: live print relabelled `pck_raw@0.2` / `oks_map(area=1.0 proxy)`.
No wire-format field renames (back-compat); no pub-API rename (no silent break).
Grade: MEASURED (relabel + divergence pinned). sensing-server 450→451 lib tests, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-155): mark §8 metric items RESOLVED + audit map + honest §1 under-count correction (M1b Goals A/D)
- §8.1: full PCK/OKS audit map (every def: file:line, basis, canonical/
legacy/distinct), the two §8 items marked RESOLVED with resolution+why.
- Honest finding: §1's "seven divergent metrics" was an UNDER-count —
sensing-server's LIVE trainer.rs has a raw-unnormalized PCK and an
area=1.0 fake-Gold OKS the table omitted, and the file §8 named
(training_api.rs) is orphaned dead code. §9 honest-limits updated.
- Goal D: metrics.rs *_v2 variants confirmed caller-less + deprecated;
noted for future cleanup, NOT deleted (public API, tch-gated).
- CHANGELOG [Unreleased] Fixed entry.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(ruvector): RaBitQ Pass-2 randomized rotation + topk bugfix (ADR-156 §8)
Implements the deferred "Multi-bit / Extended RaBitQ Pass 2" backlog item
from ADR-156 §8: a deterministic randomized orthogonal rotation applied
before sign-quantization, the published RaBitQ construction (Gao & Long,
SIGMOD 2024).
Rotation construction: Fast Hadamard Transform + seeded ±1 sign flips
("HD" / randomized Hadamard), O(d log d) time and O(d) memory — a dense
d×d rotation is O(d²) and infeasible at the 65,535-d the wire format
provisions for. Pads to the next power of two; SplitMix64 seeds the sign
stream so index-time and query-time rotations are bit-identical.
API is additive and backward-compatible: Pass 1 (`from_embedding`) is
untouched; Pass 2 is opt-in via `Sketch::from_embedding_rotated` and
`SketchBank::with_rotation` (+ `insert_embedding` / `topk_embedding` /
`novelty_embedding` helpers that rotate consistently). Default behaviour
is unchanged.
While building the Pass-2 coverage harness, found and fixed a PRE-EXISTING
correctness bug in `SketchBank::topk`: the n>k heap path used
`BinaryHeap<Reverse<(d,id)>>` (a min-heap) but treated its peek as the
max, so it returned the k FARTHEST sketches as "nearest". The shipped unit
tests only exercised the n≤k fast path, so it went unnoticed. Fixed to a
plain max-heap; pinned by `topk_heap_path_returns_nearest` and
`tight_clusters_give_high_coverage_with_overfetch` (the latter measured
0.072 on the old code).
New tests (+17, 100→117 in the crate): rotation determinism/norm-preservation
(`rotation_is_deterministic_for_seed`, `rotation_preserves_norm`), Pass-2
shape-compatibility, `pass2_coverage_not_worse_than_pass1`, and a
deterministic coverage report.
MEASURED top-K coverage (anisotropic planted-cluster fixture, cosine ground
truth; dim=128 N=2048 K=8 64 clusters noise=0.35 128 queries):
candidate_k=K=8 : Pass1 36.13% -> Pass2 46.39% (both << 90% bar)
candidate_k=24 : Pass1 83.89% -> Pass2 91.60% (Pass2 clears 90%)
candidate_k=32 : Pass1/Pass2 100%
Honest result: rotation consistently helps (+10pp at strict K), but neither
pass clears the ADR-084 90% bar at candidate_k==K on this distribution.
Pass 2 reaches 90% only with ~3x over-fetch (the ADR-084 "candidate set"
deployment pattern). Multi-bit Pass 3 evaluated separately.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(ruvector): multi-bit Pass-3 experiment + ADR-156/084 measured results
Adds the multi-bit half of the ADR-156 §8 "Multi-bit / Extended RaBitQ"
item as a MEASURED experiment (coverage::measure_multibit): rotate, then
b-bit uniform scalar-quantize each coord, rank by L1 over codes — the
natural multi-bit generalization of hamming. Measures the bit/coverage
tradeoff the backlog item asked for.
MEASURED at the strict bar (candidate_k=K=8, anisotropic planted-cluster
fixture, cosine ground truth):
Pass1 (1-bit, no rot) 36.13% 16 B/vec
Pass2 (1-bit, rot) 46.39% 16 B/vec
Pass3 (rot, 2-bit) 54.39% 32 B/vec
Pass3 (rot, 3-bit) 66.70% 48 B/vec
Pass3 (rot, 4-bit) 74.22% 64 B/vec
Honest: multi-bit monotonically helps but even 4-bit (4x memory) reaches
only 74% at the strict bar — neither rotation nor <=4-bit multi-bit clears
the strict-K 90% bar on this distribution. The bar is met via over-fetch
(Pass2 @ candidate_k=24). Tests: multibit_tradeoff_report,
multibit_1bit_matches_pass2_approx (+ sanity that 1-bit ~= Pass-2).
Docs:
- ADR-156 §8 item #2 marked RESOLVED-PARTIAL; §5 #2 grade CLAIMED ->
MEASURED-on-our-hardware; new §10 with full measured tables, the topk
bugfix disclosure, and graded deferred sub-items.
- ADR-084: "Pass 2" section answering the rotation open-question with
measured numbers + the topk bug note.
- CHANGELOG [Unreleased]: Added (Pass-2 milestone) + Fixed (topk heap).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(signal): circular phase variance for ghost-tap guard (ADR-154 §7.4 #1)
`phase_variance` computed a LINEAR sample variance over phase angles that
wrap at ±π, so a tightly-clustered set straddling the branch cut reported
spuriously HIGH dispersion — false-tripping the `> TAU` ghost-tap guard on
real, tightly-clustered CIR taps.
Replace with Mardia's circular variance V = 1 − R̄, bounded [0,1] and
invariant to where the cluster sits on the circle. Re-derive the guard
against the bounded metric via a named const
`GHOST_TAP_CIRCULAR_VARIANCE_MAX` (the old TAU-scaled threshold is
meaningless on [0,1]).
Grade: metric fix MEASURED; threshold value DATA-GATED — a clean single-path
ramp also sweeps the circle, so V alone cannot separate clean from
unsanitized without labelled frames. Conservative default (0.99) errs toward
never false-rejecting, strictly more permissive at the wrap boundary than the
buggy linear guard.
Fails-on-old test: `phase_variance_circular_not_fooled_by_branch_cut` —
inlines the old linear variance to show it exceeds TAU on wrap-straddling
phases while circular V≈0 and the guard no longer trips. Plus
`phase_variance_circular_is_bounded_and_extremal` (V∈[0,1], V≈0 identical,
V≈1 uniform).
cargo test -p wifi-densepose-signal --no-default-features --features cir --lib
→ 432 passed, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(signal): pin Welford n=0/n=1 finiteness guard (ADR-154 §7.4 #10)
The shared `WelfordStats` (field_model.rs, used by longitudinal.rs and others)
relies on `count < 2` guards in `variance`/`sample_variance`/`std_dev`/
`z_score` to stay finite at the boundaries. The guards existed but the n=0
boundary was UNTESTED — exactly the §4 divide-by-(n−1) family the ADR groups
this with.
Add `welford_finite_at_n0_and_n1` asserting every statistic is finite and
returns the documented sentinel (0.0) at n=0 and n=1, plus load-bearing doc
comments on the two guards.
Fails-on-old proof: with the `sample_variance` guard removed, the test FAILS
with "attempt to subtract with overflow" at the `(self.count - 1)` underflow
(0usize − 1); `variance` would similarly yield 0.0/0.0 = NaN. The guard is
restored; the test pins it so a future regression is caught.
Grade: MEASURED (boundary finiteness is asserted; the guard is the §4-family
fix made testable).
cargo test -p wifi-densepose-signal --no-default-features --lib field_model
→ 22 passed, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* refactor(signal): de-magic adversarial thresholds + boundary tests (ADR-154 §7.4 #13)
Lift the bare numeric literals buried in `check`/`check_consistency` into
named, documented module consts (FIELD_MODEL_GINI_VIOLATION=0.8,
ENERGY_RATIO_HIGH_VIOLATION=2.0, ENERGY_RATIO_LOW_VIOLATION=0.1,
CONSISTENCY_ACTIVE_FRACTION_OF_MEAN=0.1, SCORE_W_* weights). VALUES UNCHANGED —
each const equals the original literal; only names + pinning tests are new.
Grade: DATA-GATED. The operating values stay empirical (defensible values need
labelled spoofed/clean CSI — Wi-Spoof, §6.2/§7.3). The de-magicking +
characterization tests are MEASURED: `tuning_consts_unchanged_from_literals`,
`energy_ratio_high_boundary`, `energy_ratio_low_boundary`,
`field_model_gini_boundary`, `consistency_active_fraction_boundary` pin the
decision boundaries at/just-below/just-above each threshold, so a future
data-driven retune is a visible, tested change.
Fails-on-change proof: bumping ENERGY_RATIO_HIGH_VIOLATION 2.0→3.0 makes
`energy_ratio_high_boundary` FAIL (restored). Operating values explicitly
NOT changed.
cargo test -p wifi-densepose-signal --no-default-features --lib ruvsense::adversarial
→ 20 passed, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* refactor(signal): de-magic coherence drift/gate thresholds (ADR-154 §7.4 #9)
Lift the bare detection literals in `coherence.rs::classify_drift`
(DRIFT_STABLE_SCORE=0.85, DRIFT_STEP_CHANGE_MAX_STALE=10) and the
`coherence_gate.rs` Default impl (DEFAULT_ACCEPT_THRESHOLD=0.85,
DEFAULT_REJECT_THRESHOLD=0.5, DEFAULT_MAX_STALE_FRAMES=200,
DEFAULT_PREDICT_ONLY_NOISE=3.0) into named, documented consts. VALUES
UNCHANGED. The gate already exposed these via GatePolicyConfig (config seam);
this names + pins the defaults.
Grade: DATA-GATED. Operating values stay empirical (defensible Z-score
thresholds need labelled stable/drifting coherence traces). De-magicking +
boundary tests are MEASURED: `classify_drift_stable_score_boundary`,
`classify_drift_stale_count_boundary` pin the at/just-below/just-above
decisions; `drift_consts_unchanged_from_literals` /
`gate_default_consts_unchanged_from_literals` pin the values. Operating values
explicitly NOT changed.
cargo test -p wifi-densepose-signal --no-default-features --lib ruvsense::coherence
→ 40 passed, 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-154): mark §7.4 P1 backlog cleared — Milestone-1 (#1,#10 RESOLVED; #9,#13 DATA-GATED)
Update ADR-154 §7.4 backlog rows #1, #9, #10, #13 with commit refs + grades,
the §7.4 intro count (four P1 items cleared, ~41 P2/P3 remain), the
Horizon-ledger one-liner (Milestone-1 DONE), and the §8 honest-limits #1 line
(metric now correct; threshold still DATA-GATED). Add CHANGELOG [Unreleased]
entry.
Grades: #1 RESOLVED (MEASURED metric / DATA-GATED threshold), #10 RESOLVED
(MEASURED), #9 & #13 RESOLVED-PARTIAL (DATA-GATED — de-magicked + boundary
tested, operating values unchanged).
Validation: cargo test --workspace --no-default-features → 2057 passed, 0
failed; wifi-densepose-signal lib → 442 passed (no-default + --features cir);
python archive/v1/data/proof/verify.py → VERDICT: PASS, hash f8e76f21…46f7a
UNCHANGED (CIR ghost-tap guard is not on the deterministic proof path).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(sensing-server): stop leaking internal errors in HTTP responses (ADR-080 #2)
Six handlers in `main.rs` serialized the internal error `Display` straight
into the JSON response body, leaking server internals to any client (ADR-080
finding #2, CWE-209; reframed onto the Rust boundary by ADR-164 G11):
- edge_registry_endpoint: a panicked spawn_blocking `JoinError`
("task … panicked") in a 500, and the raw upstream error in a 503
- delete_model / delete_recording / start_recording: std::io::Error
strings carrying OS detail / filesystem paths
- calibration_start / calibration_stop: the FieldModel error chain
New `error_response` module: `internal_error` / `internal_error_json` /
`upstream_unavailable` log the full detail server-side only (tagged with a
correlation id) and return a generic body
(`{"error":"internal_error","correlation_id":…}`) — no `panicked`, no file
paths, no Debug chain. The correlation id lets an operator join a client
report to the exact server log line without ever shipping the detail.
Pinned by 5 error_response tests, incl. a leak-substring guard
(internal_error_body_does_not_leak_detail) verified to FAIL on the reverted
old body (returns the panic message / path / "os error"). The HOMECORE sweep
(ADR-161) covered homecore-server, not this crate.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(sensing-server): pin XFF-immunity + no-query-token (ADR-080 #1, #3)
Findings #1 (XFF-spoofing bypass) and #3 (JWT-in-URL, CWE-598) were logged
against the Python v1 API but are VERIFIED ABSENT on the current Rust
sensing-server, so they get regression tests rather than redundant fixes:
- #1 XFF: there is no IP-based rate-limiter or IP-allowlist to bypass, and
neither security middleware reads a forwarded header. Added
bearer_auth::xff_header_never_affects_auth_decision (spoofed
X-Forwarded-For never flips a 401<->200 decision) and
host_validation::forwarded_headers_never_bypass_host_allowlist (spoofed
X-Forwarded-Host: localhost never lets Host: evil.com past the allowlist).
- #3 JWT-in-URL: require_bearer reads the token only from the Authorization
header; WS handlers take no query token; the sole Query extractor
(EdgeRegistryParams) is a non-secret refresh flag. Added
bearer_auth::query_string_token_is_never_accepted — ?token= / ?access_token=
in the URL never authenticates (stays 401) while the header path still 200s.
Verified to FAIL when a query-token path is injected into require_bearer.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-080): mark P0 security findings #1-#3 RESOLVED; close ADR-164 G11
- ADR-080: Status note + per-finding closure (#1 XFF and #3 JWT-in-URL
verified absent + regression-pinned; #2 leaked errors fixed via the
error_response module). Records the v1-vs-Rust boundary distinction
explicitly: v1 paths remain archived; this closure governs the shipped
Rust sensing-server.
- ADR-164: Gap Register G11 and the Open/Gated Backlog entry marked
RESOLVED with the fix + branch reference.
- CHANGELOG: [Unreleased] -> ### Security entry covering all three findings.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): renumber 6 displaced ADRs to resolve duplicate-number collisions (ADR-164 G1)
Resolves the 5 duplicate ADR numbers (6 displaced files) flagged by ADR-164
Gap Register item G1. Canonical keeper per number = first file committed at
that number (date tie-broken by inbound cross-reference count / parent-appendix
relationship). Displaced files renumbered to the next free numbers (166-171):
050 keeps provisioning-tool-enhancements (5 refs vs 1)
-> ADR-166-quality-engineering-security-hardening
052 keeps tauri-desktop-frontend (parent ADR)
-> ADR-167-ddd-bounded-contexts (its appendix)
147 keeps nvidia-cosmos/OccWorld (the actual ADR, has Status header)
-> ADR-168-benchmark-proof (proof companion, no Status)
-> ADR-169-adam-mode-light-theme (was untracked)
148 keeps drone-swarm-control-system (committed #862)
-> ADR-170-yoga-mode-pose-system (was untracked)
149 keeps public-community-leaderboard-huggingface (committed 16:47 vs 17:38)
-> ADR-171-swarm-benchmarking-evaluation-methodology
Updates in-file `# ADR-NNN` headers and intra-file self-references (yoga-modes
* docs(adr): repoint inbound cross-references to renumbered ADRs (166-171)
Follow-up to the ADR renumbering (ADR-164 G1). Updates every inbound reference
that pointed at a displaced ADR, disambiguating shared numbers by title/slug so
only references to the DISPLACED topic move and keeper references stay put.
ADR-168 (was 147 benchmark-proof): README, CHANGELOG, user-guide,
proof-of-capabilities, research docs 00/03 — all path/label refs updated.
ADR-169 (was 147 adam-mode) / ADR-170 (was 148 yoga-mode): docs/adr/README index.
ADR-171 (was 149 swarm-benchmarking): all ruview-swarm eval code+docs
(Cargo.toml, evals/, eval_swarm.rs, metrics/mod/report/runner.rs), research
doc 03 (every §-ref matched ADR-171 sections, not AetherArena), 00-system-review,
series README, CHANGELOG, and ADR-148's forward/"open issues" pointers.
ADR-166 (was 050 quality-engineering / security-hardening): disambiguated from the
ADR-050 provisioning KEEPER by topic. The HMAC/secure_tdm, directory-traversal,
bind-address, and OTA-PSK-auth references in code comments
(wifi-densepose-hardware Cargo.toml + secure_tdm.rs, sensing-server main.rs) and
in ADR-052-tauri / ADR-167 all describe the security-hardening ADR -> ADR-166.
ADR-167 (was 052 ddd-appendix): inbound appendix references.
Index/registry updates: docs/adr/README.md, gap-analysis/census.md (rows +
header count), gap-analysis/lens-findings.md (collision table marked RESOLVED),
and ADR-164 Gap Register G1 marked RESOLVED with the full renumber map.
Keeper references deliberately untouched: all ADR-147 OccWorld code, all ADR-148
drone-swarm code/docs, all ADR-149 AetherArena refs (incl. ADR-150's SSL/resampling
refs, which ADR-150 explicitly binds to the AetherArena benchmark), ADR-050
provisioning refs, ADR-052 tauri refs. The frozen GitHub blob URLs in
docs/adr/.issue-177-body.md (pinned to an old branch) are left as historical.
Comment-only code edits; no behavior change. wifi-densepose-hardware compiles
clean; the sensing-server build's sole blocker is the pre-existing upstream
midstreamer-temporal-compare@0.2.1 registry crate, unrelated to these edits.
Co-Authored-By: claude-flow <ruv@ruv.net>
The streaming-engine privacy-demotion test fed a 2 ms timestamp spread, which
demoted under the old 1 ms soft guard. #1031 raised the default soft guard to
20 ms (to accommodate the real TDM slot offset), so 2 ms now fuses cleanly with
no demotion. Bump the test spread to 25 ms (above the 20 ms soft guard, within
the 60 ms hard guard) so it still proves the ADR-137 -> ADR-141 demotion wiring.
Co-Authored-By: claude-flow <ruv@ruv.net>
ProgressiveLoader rejected the published ruvnet/wifi-densepose-pretrained model
with the opaque "invalid magic at offset 0: expected 0x52564653 (RVFS), got
0x77455735", then silently fell back to signal heuristics (the "10 persons for
1" garbage reporters saw). The HF repo ships model.safetensors,
model-q{2,4,8}.bin (magic 0x77455735 = "5WEw"), and model.rvf.jsonl -- none
carry the binary-RVF magic the loader wants.
- New model_format module: auto-detects RVFS / safetensors / HF-quant-bin /
JSONL by magic+name; returns a typed actionable ModelLoadError (lists accepted
formats + the one-command convert path, never the opaque magic); converts
safetensors / model.rvf.jsonl -> RVF in-memory so the published full-precision
model loads via --model.
- load_or_convert_model: native RVF first, else auto-detect+convert+load, else
typed error. The silent heuristics fallback is now a loud, actionable message.
- --convert-model <in> --convert-out <out> CLI subcommand: one-command offline
conversion, verifies the output loads before writing.
- #1031 env seam: WDP_TDM_SLOTS + WDP_TDM_SLOT_US derive the multistatic guard
from a deployment TDM schedule (default 60 ms / 20 ms otherwise).
Honest scope: the converter wires the format/load path (safetensors F32 tensors
-> RVF weight segment, manifest written, Layer A/B/C succeed, weights
round-trip). It does NOT claim end-to-end pose accuracy -- the HF pose-decoder
architecture differs from this crate inference head (data-gated in #894).
Quantized .bin blobs are rejected with a typed error pointing at safetensors.
Tests (fail on the old opaque-magic path):
- model_format::safetensors_converts_and_loads
- model_format::hf_quant_classifies_to_actionable_error
- model_format::{jsonl_converts_and_loads, convert_to_rvf_dispatches_and_rejects_quant, ...}
Co-Authored-By: claude-flow <ruv@ruv.net>
MultistaticConfig::default().guard_interval_us was 5_000 us (5 ms) with a
comment claiming "well within the 50 ms TDMA cycle". That is wrong: on an
N-slot TDM schedule node k transmits in slot k, so two nodes are separated by
the slot offset, not clock jitter. A real 2-node mesh (slots 0/1) measured an
18,194 us spread, so every real frame set exceeded the 5 ms guard and fuse()
silently fell back to per-node sum/dedup -- multistatic fusion never ran on
hardware.
- Raise default hard guard to 60 ms (full 50 ms TDMA cycle + 20% jitter
headroom, derived from the slot model and documented in the field doc).
- Raise soft guard to 20 ms (just above the observed 18.2 ms 2-slot spread).
- Add MultistaticConfig::for_tdm_schedule(total_slots, slot_duration_us).
- Keep the honest per-node fallback for genuinely-mismatched frames.
Tests (fail on the old 5 ms default):
- fuse_real_tdm_spread_18194us_fuses_with_default_guard
- configurable_guard_rejects_too_large_spread
- for_tdm_schedule_invariants
Co-Authored-By: claude-flow <ruv@ruv.net>
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>
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>
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>
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>
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>
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>
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>
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>
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>
ADR-161 implemented RunMode::Single (AtomicBool re-entrancy guard) + Parallel
but honestly left Restart/Queued/max as "ACCEPTED-FUTURE / unbounded parallel" —
every non-Single mode spawned an unbounded task. This makes them real.
New `runmode` module — per-automation RunState owns the machinery:
- Restart: aborts the in-flight action task (tokio::task::AbortHandle) and
starts a fresh one.
- Queued: serializes runs in arrival order via a per-automation async Mutex —
sequential, never concurrent, nothing dropped.
- max: N: caps concurrency at N via a per-automation Semaphore; triggers beyond
N queue (await a permit) rather than running concurrently (HA bounded
semantics). Documented in the module table.
- Single/IgnoreFirst/Parallel preserved.
engine.rs now holds a RunState per registration and calls run_state.dispatch()
at all three trigger sites (event loop, timer, fire_time_for_test); the old
spawn_run is removed. engine.rs trimmed to 433 lines.
Tests (tests/engine_behaviors.rs) — verified to FAIL on the old unbounded-
parallel dispatch (simulated and confirmed each panics), pass on the new:
- restart_mode_cancels_prior_run (old: both runs complete → 2; new: 1)
- queued_mode_runs_sequentially_not_concurrently (old: max concurrency 3; new:
all 3 run, max concurrency 1)
- max_two_caps_concurrency_at_two (old: 4 concurrent; new: all 4 run, max 2)
homecore-automation --no-default-features: 45 passed (lib 37, engine_behaviors
8), 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
ADR-161 honestly relabelled the manifest's wasm_module_hash / wasm_module_sig /
publisher_key as "(P4 — not yet enforced)" and the homecore_permissions claims
as deferred P5 authority isolation. This makes both real and tested.
P4 (signature/integrity verification, SECURITY):
- New `verify` module: SHA-256 module-hash check + Ed25519 signature
verification over the digest against publisher_key, with a PluginPolicy
trust allowlist and an explicit AllowUnsigned dev escape hatch (loud warn).
Secure default rejects unsigned / unknown-publisher / tampered modules.
- Reuses the in-repo cog-ha-matter::witness_signing Ed25519 pattern; sha2 is a
workspace dep, ed25519-dalek/hex/base64 already in the lock — no new external
dep tree (only new edges in homecore-plugins).
- WasmtimeRuntime::load_plugin verifies before instantiation; legacy load_wasm
retained for trusted/test modules.
P5 (authority/capability isolation, SECURITY):
- New `permissions` module: PermissionSet distilled from homecore_permissions
(state:write:<glob> or bare entity glob). hc_state_set now consults it and
returns a typed -3 to the guest on an undeclared write (no host panic).
Tests (fail on old code, which had no load_plugin/verify and an unchecked
hc_state_set): tampered module rejected; valid sig from trusted key loads;
valid sig from untrusted key rejected; unsigned rejected by default and loads
only under AllowUnsigned; light.* plugin writes light.kitchen but is denied
lock.front_door; no-permission plugin can write nothing. Real deterministic
keypair signs real bytes.
Manifest doc updated: P4/P5 now ENFORCED (was "not yet enforced").
homecore-plugins --features wasmtime: 32 passed (lib 23, integration 9), 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
env_override_* and env_empty_* both set_var/remove_var the same process-global
HOMECORE_CORS_ORIGINS; under full-workspace parallelism they raced (one's
remove_var wiped the other's value mid-assert). Serialize via a poison-tolerant
module Mutex. Test-only.
Co-Authored-By: claude-flow <ruv@ruv.net>
Records the Milestone 7 audit: library cores are real (anti-slop positive) but
the network boundary had a CRITICAL WS auth bypass (A1) + reply-theater (A2) +
documented-but-no-op automation (A3-A7) + a network-exposed dev bin (A8), all
fixed and graded MEASURED with failing-on-old tests. Cites the NO-ACTION
security positives (uuid::v4 CSPRNG refuted-suspicion, hardened CORS,
no-traversal migrate, no-secrets-in-logs, honest HAP stub) and the deferred
backlog (plugin authority-isolation P5, sig-verification P4, HAP real pairing
P2, bounded run-modes, YAML load-at-boot).
Co-Authored-By: claude-flow <ruv@ruv.net>
manifest.rs documented wasm_module_hash as 'verified before execution' but
wasm_module_hash/wasm_module_sig/publisher_key are never read for verification
(only set to None in tests). Re-doc'd the three fields as P4-not-yet-enforced
so the doc matches the code. No verification code added (that is P4); no false
capability claimed.
Co-Authored-By: claude-flow <ruv@ruv.net>
A3 (HIGH): homecore-server constructed AutomationEngine then dropped it
immediately while the doc claimed automation was active. Now .start()s the
engine into a long-lived binding (event loop + timer task).
A4 (HIGH): Trigger::Time was hard-coded false with no timer. Added a 1 Hz
wall-clock timer task that fires time: automations when local HH:MM:SS matches
'at' (HH:MM or HH:MM:SS); matches_sync(Time)=false is now correct + documented.
A5 (HIGH): RunMode was documented as AtomicBool-enforced but every trigger
spawned unbounded parallel. Each automation now carries a running AtomicBool;
Single/IgnoreFirst skip re-entrant triggers, Parallel fires every time.
(Bounded Queued/Restart/max → ACCEPTED-FUTURE, honestly stated in the doc.)
A6 (HIGH): Action::Choose discarded choices and always ran default. Now
deserialises each branch's conditions, evaluates them, and runs the first
matching branch; default only if none match.
A7 (MEDIUM): template: conditions were always false in the engine path
(EvalContext built with template_env: None). The engine now builds a
TemplateEnvironment over the state machine and threads it into every
EvalContext (event loop, timer, Choose).
Tests (fail on old source):
- engine_behaviors::time_trigger_fires_via_timer_path (A4)
- engine_behaviors::single_mode_does_not_double_fire_on_rapid_triggers (A5; old fired 2x)
- engine_behaviors::parallel_mode_does_fire_concurrently (A5)
- action::choose_runs_matching_branch_not_default (A6; old ran default)
- engine_behaviors::template_condition_evaluates_true_in_engine (A7; old always false)
engine.rs kept <500 lines; behavioral tests moved to tests/engine_behaviors.rs.
Co-Authored-By: claude-flow <ruv@ruv.net>
A1 (CRITICAL): the /api/websocket handshake accepted any non-empty token,
ignoring the LongLivedTokenStore whitelist the REST path enforces — a full
WS auth bypass. Now validates via state.tokens().is_valid() before auth_ok;
wrong tokens get auth_invalid + close.
A2 (HIGH): WS command replies were pushed into an mpsc whose only consumer
logged and discarded them — no result/pong/event reached the client. Split
the socket with futures StreamExt::split; a dedicated writer task drains the
response channel onto the wire.
A8 (HIGH): the homecore-api dev bin bound 0.0.0.0 with unconditional
allow-any auth and no env path. Wired the HOMECORE_TOKENS env path (dev
fallback warn-logged when unset) and defaulted the bind to 127.0.0.1
(HOMECORE_BIND to opt into LAN).
Tests (fail on old source):
- ws_handshake::wrong_token_is_rejected (old → auth_ok)
- ws_handshake::result_reply_is_received / ping_pong_reply_is_received (old → timeout)
- server_bin_auth::provisioned_bin_rejects_wrong_bearer / from_env_path_enforces_whitelist
Co-Authored-By: claude-flow <ruv@ruv.net>
One-command harness: clone, run scripts/prove.sh, and every headline claim is
either verified on your machine (re-runs the bug-catching tests) or printed as
'CLAIMED — not reproduced here' with the exact prerequisite. Hard gate =
workspace tests + deterministic Python proof; section 3 re-runs 7 anti-slop
assertion tests (each fails on pre-fix code); gated claims (GPU/dataset/hardware/
trained-checkpoint/named-identity) are honestly listed, never faked.
Co-Authored-By: claude-flow <ruv@ruv.net>
checkpoint_round_trip / rvf_test / rvf_pipeline_test shared fixed temp_dir paths
and remove_dir at teardown, so two concurrent/repeated test runs raced (one's
teardown wiped the other's file -> NotFound). Make each dir process-unique.
Test-only; no public API change.
Co-Authored-By: claude-flow <ruv@ruv.net>
- tests/honest_labeling.rs: 10 source-presence tests asserting the A1-A5 claim
invariants (disclaimers present, uncited stat removed, WEAPON_ALERT no longer
exported, med_* feature-gated, no static-mut event buffers). Each is designed to
FAIL on the pre-fix source (ADR-159 A5 manifest-roundtrip style).
- ADR-160: records the headline (0 stubs/0 theater, all real DSP -> claim-surface
honesty debt), the graded A1-A5 fixes, NO-ACTION positives, per-prefix
classification, and the DATA-GATED deferred backlog (criterion benches,
per-skill accuracy validation, wasm32 static_mut_refs CI confirmation).
- ADR-159: its deferred-backlog line "wasm-edge ... honestly labelled, not claimed"
is now actually TRUE.
Validation (all 0 failed, host --features std):
DEFAULT 615 | MEDICAL (+medical-experimental) 653 | NO-DEFAULT 615; 0 warnings.
Co-Authored-By: claude-flow <ruv@ruv.net>
The wasm-edge skill library runs real DSP with 0 stubs / 0 theater; the exposure
is an over-confident claim surface on unvalidated skills plus a latent static-mut
soundness issue. Make the labels TRUE (do not pretend to validate the capability)
and fix the soundness mechanically:
- A1 (HIGH): med_seizure/cardiac/respiratory/sleep_apnea/gait -- add mandatory
"EXPERIMENTAL / NOT VALIDATED AGAINST CLINICAL DATA / NOT A MEDICAL DEVICE"
disclaimers, soften assertive verbs to "flags candidate <X>-like signatures",
and gate all 5 behind a NON-default medical-experimental cargo feature so they
cannot be silently shipped. DSP kept.
- A2 (HIGH): exo_happiness_score/exo_emotion_detect -- delete the uncited
"~12% faster" stat, add "speculative, unvalidated affect heuristic; outputs are
NOT measurements of emotion" disclaimers, reframe HAPPINESS_SCORE as a
gait-energy proxy. Math kept.
- A3 (MEDIUM): sec_weapon_detect -- rename EVENT_WEAPON_ALERT ->
EVENT_HIGH_METAL_REFLECTIVITY and WEAPON_RATIO_THRESH -> HIGH_REFLECTIVITY_THRESH
(a variance ratio measures reflectivity, not weapons). Registry updated.
- A4 (MEDIUM): exo_dream_stage/exo_gesture_language -- add experimental
disclaimers, promote the Exotic/Research tag into the header.
- A5 (MEDIUM, soundness): replace ~61 `static mut EVENTS`/EV/TE/EMPTY per-call
scratch buffers (60 modules) with owned per-instance `events` fields returned as
`&self.events[..n]`. Public signature unchanged; behavior preserved. Only the
two legitimate single-threaded WASM module singletons (lib.rs STATE,
ghost_hunter DETECTOR) remain as static mut. Removes the static_mut_refs source.
NO-ACTION positives (cited, labels untouched): qnt_* (quantum-/Grover-inspired,
disclosed), exo_time_crystal, exo_ghost_hunter, sig_*/lrn_* algorithm-named skills.
Co-Authored-By: claude-flow <ruv@ruv.net>
Matter commissioning is deferred to v0.8 (TlsConfig::Off, LAN-only, per
tls_defaults_to_off_for_v1_lan_only). Soften the Cargo.toml description
from "Home Assistant + Matter integration" to "Home Assistant (MQTT)
integration ... Matter Bridge commissioning is deferred to v0.8 and not
yet implemented" (honest-absence, ADR-158 pattern). No code change.
Co-Authored-By: claude-flow <ruv@ruv.net>
RemoteIdBroadcast::update stored NED metres (state.position.x/.y) into
drone_lat/drone_lon, so the ASTM F3411 broadcast would carry physically
-impossible coordinates ("latitude = 37.5 m"). The module doc claimed a
Location/Vector message but only encode_basic_id() exists.
- Rename drone_lat/drone_lon -> drone_north_m/drone_east_m (NED metres
relative to the operator/takeoff datum), documented as non-geodetic.
operator_lat/lon stay true WGS84.
- Correct the module doc to claim Basic ID only; Location/Vector encoding
is deferred until a datum-anchored NED->WGS84 transform lands.
Never broadcast physically-impossible coordinates.
Failing-on-old test:
security::remote_id::tests::test_ned_offset_stored_as_metres_not_latlon.
Co-Authored-By: claude-flow <ruv@ruv.net>
cmd_manifest emitted a null skeleton (binary_sha256: null) while the
real signed manifest existed on disk at
cog/artifacts/manifests/<arch>/manifest.json.
- New manifest module include_str!-embeds the real signed manifests
(x86_64 + arm), selected by build target arch.
- cmd_manifest parses-then-emits the embedded signed manifest, mirroring
cog-pose-estimation manifest_roundtrips. CLI now reports the real
binary_sha256, weights_sha256, Ed25519 signature, and honest
build_metadata (training_class1_accuracy = 0.343).
Failing-on-old test:
manifest::tests::embedded_manifest_has_non_null_binary_sha256 (+
embedded_manifest_is_signed, embedded_manifest_id_matches_cog).
Verified end-to-end: cog-person-count manifest -> non-null sha256.
Co-Authored-By: claude-flow <ruv@ruv.net>
The count head has 8 classes but count_train_results.json only has
support for classes 0/1 (presence, not multi-occupant counting). An
argmax on classes 2..=7 is out-of-distribution, yet the cog emitted it
as a confident headcount and the crate billed itself a "multi-person
counter".
- Add MAX_TRAINED_CLASS=1, CountPrediction::is_low_confidence() and
clamped_count().
- person.count events now carry low_confidence + raw_count, downgrade to
level "warn" when OOD, and clamp the reported count to the trained
range (no fabricated headcount).
- run.started discloses count_max_trained_class / count_classes.
- Cargo.toml description: "multi-person counter" ->
"presence detector + (data-gated) person count".
Multi-occupant accuracy stays DATA-GATED (not fabricated).
Failing-on-old test: untrained_class_argmax_is_flagged_low_confidence.
Co-Authored-By: claude-flow <ruv@ruv.net>
pose_v1 has no confidence head, so infer() emits a constant 0.185 per
frame. The config default_min_confidence was 0.3 and the runtime gates
on confidence >= min_confidence, so a default install silently emitted
ZERO pose.frame events while health reported healthy.
- Add inference::MODEL_TYPICAL_CONFIDENCE (0.185, the validation PCK@50)
as the single published per-frame confidence.
- Pin default_min_confidence() to MODEL_TYPICAL_CONFIDENCE so a default
install clears its own gate and emits.
- Warn at run.started when min_confidence exceeds the model typical
confidence (disclosed, not silent); document the trade-off in the
config field, the JSON schema, and inference.rs.
Failing-on-old test: default_config_emits_frames_with_real_model
(with old 0.3 it panics: "default install would emit zero pose.frame
events").
Co-Authored-By: claude-flow <ruv@ruv.net>
An external audit correctly found the person-ID/Soul-Signature capability was
spec-only with a no-op oracle. The §3.6 matcher is now real (wifi-densepose-bfld)
but WiFi-only channels are MEASURED not-separable (cardiac+respiratory gap ~0.0005);
named identity is data-gated on enrollment with the decisive AETHER/body-resonance
channel. README now frames person re-id as experimental research, not a shipped feature.
Co-Authored-By: claude-flow <ruv@ruv.net>
The semantic recognizer built a ruvector-core VectorDB at ":memory:"; under
full-workspace feature unification the file-storage backend is enabled and
":memory:" is an invalid Windows filename (os error 123), panicking via
.expect(). Replace the external index with an exact in-memory cosine k-NN over
the enrolled exemplars (embeddings are L2-normalised, so cosine = dot product).
For HOMECORE's small intent vocabularies this is faster, fully deterministic,
and removes the storage backend + cross-crate feature coupling entirely.
ruvector-core dropped from the crate (only used here). Workspace 3122 passed/0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
hardware_adapter read_esp32_csi/read_udp_csi/read_pcap_csi returned 'not yet
implemented'. Wired them to the real CsiParser/PcapCsiReader that already live in
csi_receiver:
- UDP: bind + recv + parse (auto-detect) -> CsiReadings. End-to-end test sends a
real JSON datagram on the wire and parses it.
- PCAP: load + read_next + parse. End-to-end test writes a real little-endian
.pcap with one record and reads it back.
- ESP32: parse CSI_DATA CSV via the real parser; live serial byte I/O behind an
optional feature (native serialport gated off the default/appliance
build) — without it, live reads return a typed UnsupportedAdapter while the
byte parser still works (tested).
Intel5300/Atheros/PicoScenes now return typed HardwareUnavailable/UnsupportedAdapter
(no device/driver/validatable-format here) instead of fake CSI — added
AdapterError::HardwareUnavailable and ::UnsupportedAdapter. Test asserts the gated
adapters error honestly.
Co-Authored-By: claude-flow <ruv@ruv.net>
estimate_gdop returned an average-pair-angle factor merely labelled GDOP (the same
class of defect ADR-156 §2.3 fixed). Replaced with the genuine Geometric Dilution
of Precision computed from the range-measurement Jacobian H (unit target->sensor
bearings): GDOP = sqrt(trace((HtH)^-1)), dimensionless, returning None for singular
(collinear) geometry which the caller treats as factor 1.0. Tests assert a
well-spread array yields lower GDOP than a near-collinear one, cross-check the
closed form, and confirm singular geometry returns None.
Co-Authored-By: claude-flow <ruv@ruv.net>
The comment claimed interpolation but the function returned the bin center,
capping breathing-rate resolution at +/-half a bin. Implemented quadratic
(3-point parabolic) peak interpolation: delta = 0.5*(yL-yR)/(yL-2y0+yR), clamped
to [-0.5,0.5], with an edge fallback to bin center. For a parabola-shaped peak the
recovery is exact (delta=0.4 for a true peak at bin 10.4). Test asserts the result
lands within half a bin of truth and strictly beats the old bin-center estimate.
Co-Authored-By: claude-flow <ruv@ruv.net>
simulate_rssi_measurements always returned vec![], so every survivor got
location: None, which disabled spatial dedup — one person re-detected across N
scan cycles became N survivors, fabricating a mass-casualty event. Two fixes:
1. Real RSSI source: SensorPosition gains an optional last_rssi (populated by the
hardware layer from actual signal-strength readings). collect_rssi_measurements
reads only real per-sensor RSSI and feeds the existing triangulator; it NEVER
fabricates a value. <min_sensors real readings -> None location (honest).
2. Zone + vitals-signature dedup: when no usable location exists, record_detection
matches an existing active, un-located survivor in the same zone whose latest
vital signature (breathing presence + START rate band, heartbeat presence,
movement class) is compatible — collapsing repeat detections of one person while
keeping genuinely distinct survivors (different rate bands) separate.
Tests (fail on old code): 3x identical-vitals/None-location -> 1 survivor (was 3);
distinct vitals stay 2; real-RSSI path yields a position; no-RSSI path yields None.
Co-Authored-By: claude-flow <ruv@ruv.net>
The ensemble gate (EnsembleClassifier::determine_triage) and the survivor
record (Survivor::new -> TriageCalculator::calculate) used two different
START-protocol approximations with different rate bands and movement handling.
The pipeline gated on the ensemble triage then discarded it and recomputed via
TriageCalculator, so a survivor could be admitted as one priority and recorded
as another (e.g. 28 bpm + Tremor: gate said Delayed, record said Immediate).
In a mass-casualty tool that divergence is a life-safety defect.
determine_triage now delegates to TriageCalculator (the single source of truth),
retaining only the ensemble confidence gate (low confidence -> Unknown, except
Immediate which is never suppressed). Updated unit + integration tests to the
canonical expectations and added a divergent-boundary regression asserting
gate triage == survivor-record triage.
Co-Authored-By: claude-flow <ruv@ruv.net>
Realistic depth backprojection is dense (many points per 8 cm voxel). Sweep
points-per-cell {4,16,64,256} at n=50k instead of point-count, so the
measurement reflects where the 9-pass→2-pass reduction actually applies.
Parity guard (old≡new, bit-for-bit) holds at every density.
Co-Authored-By: claude-flow <ruv@ruv.net>
Replace the `Tensor::randn` stubs in occworld-candle's VQVAE encoder
(`encode_occupancy`) and decoder (`decode_to_logits`) with a real,
deterministic, input-dependent convolutional forward pass. Previously
`predict()` emitted trajectory waypoints + confidence that were a function
of RANDOM NOISE, independent of the input and silently presented as model
output — the exact "AI slop" the project must eliminate.
occworld-candle:
- New `cnn.rs`: `Encoder2D` (3× Conv2d + GELU, interpolate2d to pin the
token grid) and `Decoder2D` (upsample_nearest2d + Conv2d + 1×1 head).
Both are deterministic functions of the input — same input → identical
output; different input → different output. No randn in any forward path.
- Deterministic weight init (`det_fill`, seeded xorshift64*) across all
`dummy()` constructors (encoder/decoder, VQ codebook, quant-convs,
transformer), so untrained engines are bit-for-bit reproducible.
- `InferenceOutput.weights_trained: bool` — honest disclosure flag. `false`
for `dummy()` (real but untrained net), `true` only after `load()` reads a
real checkpoint. Priors are always from the real forward pass, never faked.
- VQ codebook + quant/post-quant convs kept and wired encoder→VQ→decoder.
- Centerpiece tests in `tests/predict_honesty.rs` (input-dependence,
run-to-run + cross-engine determinism, untrained flag). All three FAIL on
the old randn stub (verified by temporarily reinstating randn).
pointcloud:
- Optimize `to_gaussian_splats` hot path: 9 separate `.iter().sum()` passes
per voxel → 2 fused accumulation passes. Bit-identical output.
- `benches/splats_bench.rs` (criterion) measures old 9-pass vs new 2-pass
with a parity guard. ~1.3× faster on representative cloud sizes.
- Confirmed: no `randn`/placeholder in any claimed production path. The
remaining synthetic generators (`send_test_frames`, `demo_depth_cloud`)
and honestly-flagged heuristics (`heuristic_pose_from_amplitude`,
luminance pseudo-depth fallback) are explicitly disclosed, not faked output.
DATA-GATED: a trained checkpoint. An untrained-but-real net is the honest
deliverable; accuracy is flagged via `weights_trained`, never claimed.
Tests: occworld 16 unit + 3 integration + 2 doc, pointcloud 18 — all pass
(CPU `Device::Cpu`; CUDA feature is GPU-gated and untouched).
Co-Authored-By: claude-flow <ruv@ruv.net>
Implements the three placeholder paths with real, tested behaviour and an
honest typed result wherever a capability is genuinely data-gated.
homecore-assist:
- runner.rs: add LocalRunner — runs the real IntentRecognizer pipeline and
returns a fully-formed RufloResponse (resolved intent + speech). NoopRunner
is now honest: typed NotStarted before spawn, explicit empty after (never a
silent fabricated response). A live ruflo-agent.js subprocess remains the
data-gated future path.
- recognizer.rs / semantic_recognizer.rs: real SemanticIntentRecognizer — embeds
the utterance (deterministic feature-hash embedding, new embedding.rs) and runs
ruvector-core HNSW nearest-neighbour search over enrolled exemplars, accepting
matches above a configurable cosine-similarity threshold (default 0.75) and
falling back to regex below it. Measured: paraphrase "turn on the kitchen
light" vs exemplar "turn on the light" -> sim 0.855 (match); "schedule a
dentist appointment" -> sim 0.106 (no-match). `semantic` feature on by default.
homecore-recorder:
- db.rs: search_states_by_text — real SQL LIKE query over entity_id/state/attrs
returning real rows (newest-first, k-capped, LIKE-escaped). search_semantic now
falls back to it when the vector index yields no hits, so it is no longer
always-empty under the default NullSemanticIndex.
Tests (real behaviour; each fails on the old always-empty stub, verified):
- homecore-assist: 39 passed / 0 failed
- homecore-recorder (P1, no features): 19 passed / 0 failed
- homecore-recorder (P2, --features ruvector): 25 passed / 0 failed
All files < 500 lines; homecore-server consumer still builds.
Co-Authored-By: claude-flow <ruv@ruv.net>
wifiscan (Tier 2 wlanapi adapter ONLY):
- Real native wlanapi.dll BSS-list FFI (new adapter/wlanapi_native.rs):
WlanOpenHandle -> WlanEnumInterfaces -> WlanGetNetworkBssList ->
WlanFreeMemory/WlanCloseHandle via windows-sys 0.59 (already in lock
tree). Per-BSSID RSSI(dBm)/channel/band/radio-type/SSID + CSI-capable
filter. #[cfg(windows)] real path; #[cfg(not(windows))] returns typed
WifiScanError::Unsupported (honest, never fabricated).
- wlanapi_scanner now native-first with documented netsh fallback,
native_scans metric, scan_native()/scan_native_csi_capable(), and a
benchmark() that MEASURES real Hz (no hardcoded "10x" claim).
- MEASURED 9.74 Hz native on ruvzen (30 iters, Native backend) vs netsh
~2 Hz baseline. Live measurement kept as an #[ignore] test.
- Cargo.toml: unsafe_code forbid->deny so only the audited wlan_ffi
module opts into unsafe; all unsafe confined + null-checked + freed.
sensing-server (Matter commissioning):
- Replaced the lossy modulo placeholder in matter/commissioning.rs with
the real Matter Core Spec 1.3 §5.1.4.1.1 field-packing. Canonical
vector (20202021, 3840) now encodes to the published 34970112332.
- Added ManualPairingCode::decode + DecodedManualCode proving the code
is real/lossless (passcode round-trips bit-for-bit; short
discriminator = top 4 bits) with Verhoeff integrity, incl. proptest.
Tests: wifi-densepose-wifiscan 145 passed (real FFI exercised on
Windows); wifi-densepose-sensing-server 614 passed. 0 failed.
Co-Authored-By: claude-flow <ruv@ruv.net>
Update specification.md §3.6 ONLY with an honest implementation-status note:
the matching algorithm is now implemented and tested in
v2/crates/wifi-densepose-bfld/, weights remain unvalidated design intent, and
named-identity locking is data-gated (cardiac+respiratory alone are not
separable — measured gap ~0.0005). The broader Soul Signature system remains
Pre-Implementation.
Co-Authored-By: claude-flow <ruv@ruv.net>
First running implementation of the spec's §3.6 per-channel weighted-cosine
matcher (docs/research/soul/specification.md). Replaces reliance on NullOracle
(which always returns NotEnrolled) with a real EnrolledMatcher oracle.
- soul_channels.rs: 8-channel SoulChannels container (AETHER reuses
IdentityEmbedding, preserving invariant I2 — no Clone/Serialize, zeroized on
Drop), MatchWeights with the §3.6 default table (unvalidated design intent),
heapless FeatureVector. no_std-compatible.
- soul_match.rs: match_score() implementing the exact formula
Σ w·cos / Σ w·availability, with graceful degradation, zero-norm/NaN safety,
and a typed 'insufficient channels' result (never a default-high score).
EnrolledMatcher (std) satisfies the existing SoulMatchOracle trait, gated on
a score threshold AND a minimum shared-channel count (so a single low-weight
channel can never lock identity). NullOracle retained as the disabled default.
Named-identity locking remains data-gated: it requires real AETHER enrollment +
body-resonance data, which has not been provided.
Co-Authored-By: claude-flow <ruv@ruv.net>
Documents Milestone 3 across the four acquisition crates (vitals, hardware,
wifiscan, calibration). Honest headline: this layer was already well-hardened,
so the real work is small.
- §A1 (perf, MEASURED): Vec::remove(0) O(n^2) sliding windows -> VecDeque.
End-to-end win is NULL within noise at realistic window sizes (DSP dominates);
the win is the algorithmic O(n^2)->O(n) shown in isolation. Claimed nothing
more -- the committed bench proves the null.
- §A2 (correctness): breathing partial-weights scale-mixing -> normalized by
Sigma(effective weights). Pinned by two fail-on-old tests.
- §A3 (stability): IIR resonator divergence. Corrected the research report's
physically-inaccurate trigger (divergence needs |r|>=1, i.e. bw>=4, not "r
negative"); clamp + finite-guard. Pinned by two fail-on-old tests.
- §B1 hardening on an unreachable (already-gated) truncation path -- disclosed.
- §B4 (constant-time HMAC compare) DEFERRED: not worth a new direct `subtle`
dependency for an 8-byte LAN sync-beacon tag.
- MEASURED negative-results section (the centerpiece): esp32_parser length gate,
sync_packet infallible slices, the whole ieee80211bf validate-on-deserialize /
no-panic-FSM / single-role / SBP-single-evaluate model, secure_tdm HMAC+replay,
netsh_scanner fixed-argv + Option parse, geometry_embedding MAX_COORD_M -- each
cited file:line, all NO-ACTION.
- SOTA landscape: deep-CSI vitals (DATA-GATED), 802.11bf conformance (CLAIMED,
non-public suite), per-room calibration (CLAIMED on numbers), native wlanapi
FFI multi-BSSID (CLAIMED-unmeasured -- explicitly NOT claiming the 10x). Mostly
NO-ACTION / ACCEPTED-FUTURE.
- Deferred backlog (§8): nothing silently dropped.
Validation: cargo test --workspace --no-default-features = 3054 passed / 0
failed; python verify.py = VERDICT PASS (hash unchanged, Rust-only changes).
Co-Authored-By: claude-flow <ruv@ruv.net>
OpportunisticCsiBridge::ingest built CsiReportPayload.n_subcarriers via
`self.amp_accum.len() as u16`, which would silently wrap a count above 65_535.
Replace with `u16::try_from(...).ok()?` (drop-instead-of-truncate). Disclosed
honestly as defense-in-depth on an UNREACHABLE path: ingest already gates
subcarrier_count > MAX_REPORT_SUBCARRIERS (484) at entry and report.validate()
rejects oversized counts downstream, so the cast can never wrap in practice.
Correct-by-construction rather than gate-dependent; no behavior change, no new
test (the gate prevents the input that would exercise it).
Co-Authored-By: claude-flow <ruv@ruv.net>
§A2 (correctness): BreathingExtractor weighted fusion was an un-normalized sum.
When `weights` was supplied shorter than n, supplied entries were used raw while
the missing tail defaulted to uniform 1/n -- two scales summed with no
renormalization, silently mis-scaling the breathing signal by a factor of
weights.len(). Extract to fuse_weighted_residuals() and normalize by
Sigma(effective weights), mirroring heartrate::compute_phase_coherence_signal.
Tests: partial_weights_are_renormalized_not_scale_mixed,
partial_weights_fusion_is_weighted_average (both fail on old code).
§A3 (stability): the IIR resonator pole radius r = 1 - bw/2 diverges when the
pole MAGNITUDE |r| >= 1 (i.e. bw >= 4: a very low fs relative to band width) --
NOT merely when r is negative, as the research report stated (a negative r with
|r| < 1 is still stable; the comments/tests are corrected accordingly). On
divergence the filter overflows to +/-inf within ~600 frames, NaN-poisons acf0,
and the extractor stalls permanently. Clamp r to [0, 0.9999] AND finite-guard
the filter output before the history push (defense-in-depth, mirrors ADR-154 §3).
Applied to both heartrate.rs and breathing.rs. Tests:
{heartrate,breathing}::low_sample_rate_filter_stays_finite (fs=0.5, 0.1-0.9 Hz
band, 600-frame unit step -> all-finite; both panic on old code).
These files also carry the §A1 VecDeque window conversion (bit-identical).
Co-Authored-By: claude-flow <ruv@ruv.net>
Replace Vec::remove(0) (O(n) per-sample buffer shift -> O(n^2) full-window
sweep) with VecDeque push_back/pop_front (O(1) eviction) in the fixed-length
sliding/ring buffers of the vital-sign and wifiscan extractors. Where the
autocorrelation / zero-crossing / Pearson loop needs a contiguous slice,
make_contiguous() is called once per extract(), matching the idiom already used
in wifiscan/pipeline/orchestrator.rs. Output is bit-identical.
Sites: anomaly.rs (rr/hr history), store.rs (readings ring; history() now takes
&mut self to hand back a contiguous slice, no external callers), wifiscan
breathing_extractor.rs (filtered history), wifiscan correlator.rs (per-BSSID
histories -> Vec<VecDeque<f32>>). (heartrate.rs/breathing.rs windows land with
the §A2/§A3 fixes in a separate commit.)
New criterion bench crates/wifi-densepose-vitals/benches/vitals_bench.rs drives
each extractor over a full-window fill. Honest MEASURED result: end-to-end win
is NULL within noise at realistic ESP32 window sizes (1500-3000) because the
per-frame DSP dominates the eviction (heartrate 42.8ms->44.4ms, breathing
7.95ms->7.86ms, overlapping CIs). In isolation the eviction collapses O(n^2)
-> O(n) (34.6x at window=3000, 3158x at window=100000); A1 lands as the correct
data structure removing a latent O(n^2), NOT a claimed hot-path speedup.
Reproduce: cargo bench -p wifi-densepose-vitals --bench vitals_bench
Co-Authored-By: claude-flow <ruv@ruv.net>
MultistaticArray::fuse / fuse_ungated cloned every viewpoint embedding twice per
fusion (once into `extracted`, again when building the attention input). Now the
embeddings are MOVED out of `extracted` (one clone per viewpoint instead of two),
capturing geometry/ids by Copy in the same pass. Correctness-neutral — all 100
viewpoint/mat lib tests pass unchanged.
MEASURED (new benches/fusion_bench.rs, embedding_extract A/B, 8 vp x 128-d):
before_double_clone 1.0029 us -> after_single_clone 461.6 ns (~2.17x)
End-to-end fusion_pipeline (8 vp): 202 us — marshalling is <1% of fusion
(n*n attention dominates), so end-to-end win is modest; the A/B isolates the
clone elimination. Reproduce:
cargo bench -p wifi-densepose-ruvector --bench fusion_bench
Co-Authored-By: claude-flow <ruv@ruv.net>
Security fix: two functions on a fusion/localisation path that can carry
network-sourced multistatic frames panicked on crafted input (remote DoS).
- triangulation::solve_triangulation indexed ap_positions[0] (empty table) and
ap_positions[i]/[j] (crafted out-of-range AP index in a TDoA tuple). Now uses
.first()? / .get(i)? / .get(j)? — returns None, never panics.
- heartbeat::band_power computed n_freq_bins-1 (usize underflow on a zero-bin
spectrogram) and did not clamp low_bin. Now guards n_freq_bins==0 and clamps
both bounds into [0,last]; returns 0.0 for empty/inverted ranges.
Tests (each panics on old code, verified by revert):
triangulation_out_of_range_index_returns_none_no_panic,
triangulation_empty_ap_positions_returns_none_no_panic,
heartbeat_band_power_zero_bins_no_panic,
heartbeat_band_power_out_of_range_bounds_no_panic.
Co-Authored-By: claude-flow <ruv@ruv.net>
Two correctness/integrity fixes on the cross-viewpoint fusion geometry path,
each pinned by a regression test that fails on the old code.
- GDOP mislabel (§2.3): CramerRaoBound.gdop was `sqrt(crb_x+crb_y)` — identical
to rmse_lower_bound (metres, noise-dependent), NOT a dimensionless GDOP. Now
computes true GDOP = sqrt(trace(G^-1)) on the unit-variance bearing geometry,
in both estimate() and estimate_regularised(); INFINITY (not NaN) for
degenerate collinear geometry. Test gdop_is_dimensionless_and_noise_independent
asserts GDOP is unchanged under 10x noise while RMSE scales 10x (old code
failed: it scaled with noise, proving it was RMSE).
- Angular wrap (§2.1): GeometricBias::build_matrix used raw |delta-azimuth|
(can exceed pi, mis-states the 0/2pi seam) instead of the wrapped distance.
angular_distance made pub and reused as the single canonical helper. HONEST:
under the current cos() kernel this is a NUMERIC NO-OP (cos is even/periodic,
cos(raw)==cos(wrapped)); landed for contract correctness + single-source-of-
truth + future non-even kernels, not as a behaviour change. Tests pin the
contract (wrapped value in [0,pi], seam symmetry).
ruvector lib tests: 100 passed / 0 failed (+ new tests).
Co-Authored-By: claude-flow <ruv@ruv.net>
Records the integrity-critical fixes (unified canonical metric, leak-free
subject-disjoint split + synthetic-val disclosure, rapid_adapt real gradients,
proof margin + committed-hash rigor), the Tier-2 correctness/security fixes, the
measured Tier-3 perf win, the NN SOTA landscape graded MEASURED/CLAIMED/
THEORETICAL (GraphPose-Fi as top ACCEPTED-future candidate; INT4; CSI-JEPA-vs-MAE
with the honest "no JEPA/MAE-on-WiFi-pose yet" caveat; "Mamba-CSI-pose does not
exist"), and the ~45-finding deferred backlog. Discloses the libtorch/tch-gating
limitation and that the Rust proof is honestly in SKIP until a baseline is
committed.
Co-Authored-By: claude-flow <ruv@ruv.net>
- onnx.rs ORT input: arr.as_slice() single-memcpy fast path with iterator
fallback for strided views. MEASURED [1,256,64,64]: 1.972ms -> 1.336ms
(~1.48x). Repro: cargo bench -p wifi-densepose-nn --no-default-features
--features onnx --bench onnx_bench -- onnx_input_copy
- onnx.rs checked_output_dims: reject ONNX dim <= 0 (incl. unresolved -1) before
allocation (config-OOM class) + test.
- onnx_concurrency bench: empirically proves the per-inference write lock
serializes (throughput drops with more threads). The intended read-lock win is
NOT landable on ort 2.0.0-rc.11 (safe Session::run is &mut self, verified) and
is deferred to the backlog with the upgrade path documented in-code.
New committed fixture tests/fixtures/tiny_conv.onnx (666 B, not gitignored).
Co-Authored-By: claude-flow <ruv@ruv.net>
Each fix ships a test that would have caught the bug:
- ruview_metrics OKS: derive scale from GT extent (no s=1.0 fake-Gold), reject
s<=0, bound the loop to array extents (no panic on short/adversarial input).
- config.validate(): UPPER bounds on window_frames/subcarriers/backbone_channels/
heatmap_size/keypoints/body_parts/batch_size + reject negative gpu_device_id
(closes the config-OOM class); defaults+presets still validate.
- subcarrier.rs: graceful fallback instead of panic on non-contiguous input.
- ablation.rs latency_percentiles: total_cmp + NaN guard (no partial_cmp unwrap).
- tensor.rs softmax(axis): normalize per-lane along the given axis (was whole-
tensor), out-of-range axis -> NnError; fixes densepose per-pixel probs.
- translator.rs apply_attention: real scaled-dot-product attention (was a
uniform 1/seq_len stub that made any "with attention" ablation == without);
mis-shaped checkpoint projections rejected.
Co-Authored-By: claude-flow <ruv@ruv.net>
The deterministic proof self-certified: PASS on any loss decrease (incl. 1e-9
noise) and a missing expected hash defaulted to PASS.
- MIN_LOSS_DECREASE=1e-4: a run counts as learning only above float noise; a
noise-only pipeline now FAILS.
- is_pass() requires hash_matches==Some(true); no-hash -> SKIP (exit 2), never
PASS. verify-training fails fast on a sub-margin loss before the hash compare,
so a missing baseline cannot mask a non-learning pipeline.
Documented honestly: the proof certifies reproducibility/determinism on a
synthetic dataset, NOT that real data produced the weights nor that any accuracy
claim is met. Tests: no_committed_hash_is_skip_not_pass,
submargin_loss_change_fails_even_without_hash,
committed_matching_hash_with_real_decrease_passes.
Co-Authored-By: claude-flow <ruv@ruv.net>
contrastive_step/entropy_step wrote a fake gradient (grad += v*0.01) unrelated
to the stated objective, so any "TTA improves the metric" was unsupported. The
*_loss functions are now pure evaluators of the real objective; adapt() descends
them with a central finite-difference gradient of that exact loss, so "the
adaptation loss decreases" is now a real, reproducible measurement.
Honest scope caveat (documented): this minimizes a self-supervised proxy over a
LoRA bottleneck on raw CSI; it is NOT wired to the pose model and there is NO
measured end-to-end PCK gain on WiFi pose from this path.
Tests: contrastive_loss_decreases, entropy_loss_decreases (real gradient steps
don't increase the loss), reported_loss_is_the_real_objective_not_a_placeholder.
Co-Authored-By: claude-flow <ruv@ruv.net>
MM-Fi windows are stride-1 (~99% overlap), so an index-level split leaks; and
bin/train.rs validated real training against a SYNTHETIC val set, making any
printed PCK meaningless on two counts.
- MmFiDataset::subject_disjoint_split partitions whole subjects -> the two views
share no subject and no window (leak-free by construction, deterministic per
seed). assert_split_leak_free verifies subject- AND window-disjointness and is
called inside the split so a leaky split is never handed out.
- bin/train.rs now prefers the real split; the synthetic path is a labelled
run_smoke_test ("[SMOKE-TEST] DO NOT REPORT") reachable only as a fallback.
- New DatasetError::InvalidSplit.
Tests prove disjointness, determinism, single-subject/bad-fraction rejection,
and that the validator catches an injected subject leak.
Co-Authored-By: claude-flow <ruv@ruv.net>
Collapse the four PCK and three OKS implementations into a single source of
truth — pck_canonical (torso hip↔hip, COCO/ADR-152 convention validated at
~96% PCK@20 in benchmarks/wiflow-std) and oks_canonical (scale from GT pose
extent). MetricsAccumulator, compute_pck/_per_joint/_oks, aggregate_metrics and
the deprecated *_v2 path all route through them, so Trainer::evaluate() and the
bench definition agree.
Fixes two claim-inflating bugs, each pinned by a regression test:
- zero-visible-joint PCK was 1.0 (false-perfect) -> now 0.0
- OKS s=1.0 on normalized coords made OKS~=1.0 for any pose ("fake Gold tier")
-> scale now derived from the pose; a 3x-torso-wrong pose yields OKS<0.2
Divergent local kernels (training_bench raw-threshold, sensing-server
torso-height) annotated "DO NOT USE for reported metrics". Legitimately changed
test expectations (all-coincident "perfect" fixtures are correctly unscoreable;
all-invisible -> 0.0) updated with comments citing the finding.
Co-Authored-By: claude-flow <ruv@ruv.net>
Records Milestone-0 of the signal/DSP beyond-SOTA sweep with full PROOF
discipline (MEASURED vs CLAIMED vs THEORETICAL grading throughout):
- §2 discloses the headline anti-slop finding: the ADR-134 CIR coherence gate
was DEAD in production (canonical-56 frames -> SubcarrierMismatch -> silent
freq-domain fallback for every frame). Documents the canonical56() fix + the
4 committed proof tests.
- §3 NaN/inf adversarial bypass; §4 divide-by-(n-1) window trio.
- §5 the two MEASURED perf wins with before/after medians + reproduce commands.
- §6 per-module SOTA landscape, evidence-graded: deep-unfolded ISTA/LISTA for
CSI->CIR (~3 dB NMSE, MEASURED, arXiv 2211.15440 + 2502.05952), diffusion CIR
prior (public weights, MEASURED), Wi-Spoof adversarial eval (MEASURED, arXiv
2511.20456), Bayesian multi-AP fusion (CLAIMED, no code, 2512.02462),
coherence gating + RF intention-lead (THEORETICAL).
- §7 roadmap: LISTA-for-CIR as the top ACCEPTED-future item (M effort; the ISTA
+ Phi already exist in cir.rs) — proposed, NOT implemented this milestone —
plus the explicit deferred-findings backlog (the ~45 review findings not
fixed here, graded P1/P2/P3) so nothing is silently dropped, with a
horizon-ledger DONE-vs-DEFERRED one-liner.
Co-Authored-By: claude-flow <ruv@ruv.net>
Two measured, bit-equivalent perf wins. Each ships a criterion bench
(benches/features_bench.rs, new) with before/after numbers and a committed
bit-identity test — no perf claim without a measured before/after.
PSD FFT-planner caching (features.rs)
PowerSpectralDensity::from_csi_data re-planned a FftPlanner on EVERY frame,
and FeatureExtractor::extract calls it per frame on the hot path. New
from_csi_data_with_fft(csi, n, &Arc<dyn Fft>) reuses a plan cached in
FeatureExtractor (built once in new()). Bit-identical output
(psd_cached_fft_bit_identical_to_fresh, f64::to_bits over 6 sizes).
MEASURED (median ns/frame, criterion):
fft=64 5.84µs -> 1.89µs (3.09x)
fft=128 9.31µs -> 3.61µs (2.58x)
fft=256 13.77µs -> 6.73µs (2.04x)
DTW Sakoe-Chiba band (gesture.rs)
dtw_distance computed j_start/j_end but iterated the FULL 1..=m row,
continue-ing out-of-band — band constrained the path, not the work (O(n*m)).
Now iterates j_start..=j_end (O(n*band)), resetting only the two boundary
guard cells the recurrence reads, with endpoint reachability (|n-m|<=band)
at the return. Bit-identical across 12 shapes x 8 bands
(dtw_banded_bit_identical_to_fullrow).
MEASURED (median, criterion):
n=m=100 band=5 33.45µs -> 13.77µs (2.43x)
n=m=200 band=5 122.32µs -> 29.55µs (4.14x)
n=m=200 band=10 159.98µs -> 60.19µs (2.66x)
Reproduce:
cd v2 && cargo bench -p wifi-densepose-signal --no-default-features \
--bench features_bench
Co-Authored-By: claude-flow <ruv@ruv.net>
Milestone-0 correctness/security fixes for the beyond-SOTA signal/DSP sweep.
Every fix ships with a committed regression test (proof, not adjectives).
CRITICAL — ADR-134 CIR coherence gate was DEAD in production
MultistaticFuser fuses canonical-56 frames (hardware_norm.rs resamples every
chipset onto a 56-tone grid), but the gate was wired to CirConfig::ht20()
which expects 64/52. Every estimate() returned SubcarrierMismatch and
cir_gate_coherence silently fell back to freq-domain coherence — use_cir_gate
was indistinguishable from false. Fixes:
- new CirConfig::canonical56() (64-bin HT20 framing, 56 active tones, 168 taps)
- new MultistaticFuser::with_cir_canonical56() (correct default); ht20 kept,
now doc-warned
- active_indices() handles (64,56) + length-matched fallback (no silent
fall-through to the 52-index slice)
- SubcarrierMismatch in the gate now debug_assert!s loudly (config error can
no longer hide as a graceful degrade)
- cir_estimate_first() exposes the Ok/Err verdict for tests
PROOF (ruvsense::multistatic::tests): ht20 → 8/8 Err (dead); canonical56 →
8/8 Ok (alive); coherence(gate on) != coherence(gate off).
CRITICAL — adversarial.rs NaN/inf detector bypass
One non-finite link energy bypassed the whole detector (every `e>thresh`
false on NaN; score clamp returns NaN). A non-finite input is itself the
strongest spoof — now short-circuits to a definite anomaly (score 1.0,
affected link reported) and does not poison the temporal-continuity state.
PROOF: nan_link_energy_flags_anomaly, inf_link_energy_flags_anomaly.
CORRECTNESS — divide-by-(n-1) window trio
csi_processor hamming_window (n=0 usize underflow, n=1 div0), bvp Hann,
spectrogram make_window all guarded for n<=1 (empty / constant-1.0 window).
Python deterministic proof still PASS, same pipeline hash (reference uses n>=2).
PROOF: *_degenerate_sizes / *_size_one_is_finite / make_window_size_0_and_1.
CLARITY — calibration.rs subtract_in_place
Removed the vacuous `if active_input {ki} else {ki}` branch that implied a
full-FFT->bin remap that never existed; documented the sequential
active-index convention (matches sibling extract_first_stream). No behavior
change.
Tests: cargo test -p wifi-densepose-signal --no-default-features (+--features cir)
green; full workspace green; verify.py VERDICT: PASS.
Co-Authored-By: claude-flow <ruv@ruv.net>
The 12-crate brain-topology analysis ecosystem (v2/crates/ruv-neural) was a
self-contained nested workspace with no inbound deps from the v2 workspace
(verified: zero path references outside its own tree). Published standalone
at github.com/ruvnet/ruv-neural and re-attached here as a submodule at the
same path, so the build layout is unchanged while the project gets its own
repo/CI/release cadence.
* docs(research): add RuView beyond-SOTA system review (00)
First document of the beyond-SOTA research series: capability audit of
the current RuView engine with role-to-crate maturity matrix, ruvsense
module inventory, gap analysis, and risk register.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* docs(research): add beyond-SOTA architecture design (02, in progress)
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* docs(research): finalize beyond-SOTA architecture (02)
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* docs(research): add benchmark/validation methodology snapshot (03)
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* docs(research): add beyond-SOTA series index with validation results; changelog
README index ties the 5 research docs together with the session's
measured validation evidence: 2,797 workspace tests / 0 failed, Python
proof PASS (bit-exact), and paired pre/post criterion CIR benchmarks.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* perf(signal): precompute CIR warm-start system; hoist tomography solver allocs
Exact, determinism-safe optimizations (bit-identical float results):
- cir.rs: diag(PhiH Phi)+lambda*I and its CSR matrix depend only on Phi
and lambda (fixed at CirEstimator::new) but were rebuilt every frame
(O(K*G) pass + CSR allocation). Now built once in new() via
build_warm_start_system; summation order unchanged.
- tomography.rs: ISTA gradient buffer hoisted out of the 100-iteration
loop (fill(0.0) reset) and the Frobenius Lipschitz bound moved from
per-reconstruct to construction.
Verified: signal 456 tests green; engine 11/11 green including
cycle_is_deterministic and witness-stability tests. Criterion paired
pre/post: cir_estimate/he40 -3.9% (p<0.01), multiband -1.2/-1.4%.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* fix(worldgraph): bound SemanticState growth with deterministic retention
StreamingEngine::process_cycle appended one SemanticState belief per cycle
with no eviction — ~1.7M nodes/day at 20 Hz (beyond-SOTA roadmap finding #6).
Add 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) are never eligible. Wire it into the
engine after each belief append (DEFAULT_SEMANTIC_RETENTION = 7,200, ~6 min
at 20 Hz; set_semantic_retention to tune). The WorldGraph holds current
beliefs; durable history is the recorder's job, so no audit data is lost.
3 new tests: end-to-end bounded growth, oldest-only eviction, deterministic
equal-timestamp tie-break. Workspace gate: 2,865 passed, 0 failed.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* feat(sensing-server): route live frames through the governed StreamingEngine
Closes the live-trust-path gap (ADR-136 section 8, beyond-SOTA system review):
the running server fused live CSI with the bare MultistaticFuser, while the
privacy/provenance/witness control plane (ADR-135..146) only ever ran on
synthetic in-test frames. The privacy control plane was therefore bypassable
on the real path.
New engine_bridge module drives StreamingEngine::process_cycle from the
server's live NodeState map, reusing the existing NodeState -> MultiBandCsiFrame
conversion. It lazily wires each contributing node as a WorldGraph sensor
(idempotent), bounds belief growth via the retention cap, and forwards explicit
timestamps/calibration ids so the path stays deterministic and replayable.
Wired additively into both live ESP32/WiFi fusion sites in main.rs via a
split-borrow off the write guard, so person-count behavior is unchanged; the
latest BLAKE3 witness is stored on AppState. Every published belief now carries
evidence + model + calibration + privacy decision and a deterministic witness.
Adds wifi-densepose-engine/-worldgraph/-bfld/-geo deps. 6 new bridge tests
(witnessed belief with full provenance, cross-run determinism, idempotent node
registration, retention bound, privacy-mode propagation). sensing-server suite
430+128 green; workspace gate 2,904 passed / 0 failed.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* feat(train): falsifiable occupancy benchmark with anti-overfitting gate
Makes the presence/person-count "beyond SOTA" claim falsifiable in code
instead of aspirational (the unfalsifiability gap from the beyond-SOTA system
review). occupancy_bench grades predictions vs ground truth and gates a SOTA
claim behind one claim_allowed invariant requiring ALL of:
- DataProvenance::Measured — synthetic/mock data is scorable for regression
but never claimable (anti-mock-contamination; the CLAUDE.md Kconfig-bug
lesson made structural).
- A leak-free EvalSplit — validate() refuses any split where a subject OR
environment id appears in both train and test (subject leakage /
per-environment overfitting).
- n_test >= min_test_samples (small-N guard).
- Presence F1 whose bootstrap-CI lower bound (deterministic seeded splitmix64)
clears the threshold — not the point estimate.
- Count MAE within threshold.
The claim string is unreadable except through the gate (NO_CLAIM otherwise),
same discipline as the ruview-gamma acceptance gate. What remains is data, not
method: a frozen, SHA-pinned, subject/environment-disjoint measured replay set
turns the claim into a passing/failing test.
Lives in wifi-densepose-train (the eval bounded context, alongside ablation/
eval/metrics). 10 tests cover each refusal path; warning-clean under the
crate's missing_docs lint. Workspace gate 2,914 passed / 0 failed. Doc 03
updated.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* feat(engine): per-room adapter provenance + drift-to-recalibration advisor
Closes the trust-chain gap where an ~11 KB per-room LoRA adapter (ADR-150
section 3.4) could silently change inference without the witness noticing:
provenance carried only "rfenc-v<N>" with no notion of adapter identity.
- StreamingEngine::set_room_adapter(AdapterInfo): pins the adapter's
content-derived id into provenance model_version
("rfenc-v1+adapter:<id>") — and therefore into the BLAKE3 witness — so
swapping or clearing adapter weights always shifts the witness. Engine test
proves base -> adapter -> other-adapter -> cleared all witness differently
and cleared == base.
- RecalibrationAdvisor: recommends re-running the ADR-135 empty-room baseline
/ refitting the room adapter on sustained low fusion coherence (streak
threshold, default 60 cycles ~ 3 s at 20 Hz) or an ADR-142 change-point.
Surfaced as TrustedOutput::recalibration_recommended, stored on the
sensing-server AppState alongside the witness at both live fusion sites.
- Bridge plumbing: EngineBridge::{set_room_adapter, clear_room_adapter} +
live-path test that the adapter id flows into the live witness.
Scope note (honest): this is the deployable provenance/trigger half of the
"retrained model" roadmap item. Fitting the adapter itself runs in the
existing external calibration service (aether-arena/calibration/); a trained
RF-encoder checkpoint still does not exist in-tree.
Engine 15 tests, bridge 7 tests. Workspace gate: 2,918 passed / 0 failed.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* fix(mat): gate api module behind its feature — standalone no-default-features builds
pub mod api was unconditional while its only dependency, serde, is optional
behind the 'api' feature, so any build without default features failed with
101 unresolved-serde errors (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 combos compile: bare --no-default-features (was 101 errors, now 0),
--no-default-features --features api, and full default (177 tests pass).
Workspace gate: 2,918 passed / 0 failed.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* perf(signal): opt-in FFT operator for the CIR ISTA solver (8-14x measured)
Phi is a sub-DFT, so each ISTA mat-vec can run as one length-G FFT
(O(G log G)) instead of a dense O(K*G) product — the dominant-latency-hazard
finding from the beyond-SOTA optimization roadmap.
New CirConfig::fft_operator, default FALSE: the dense path stays the
bit-exact witness default. The FFT evaluates the same sums in a different
order, so enabling it shifts float results in the last bits and requires
regenerating any pinned witness — strictly opt-in per deployment.
FftOperator (rustfft, planned once at CirEstimator::new, scratch buffers
reused across the ISTA loop) dispatches inside ista_solve:
Phi x = scale * forward-FFT(x) sampled at bins (k_idx mod G)
Phi^H v = scale * unnormalised inverse-FFT of v scattered into those bins
Warm-start and Lipschitz estimation stay dense at construction.
Measured (criterion, same run, same machine):
ht20: 2.22 ms -> 265 us (8.4x)
ht40: 10.26 ms -> 717 us (14.3x)
The real HE40 grid (K=484, G=1452) scales further per the O(K*G)/O(G log G)
ratio.
3 new tests: FFT<->dense matvec equivalence to float tolerance on ht20 and
he40 grids; end-to-end dominant-tap agreement on a single-path frame; all
default configs keep FFT off. New cir_estimate_fft bench group.
Workspace gate: 2,921 passed / 0 failed (default path bit-exact, witnesses
unchanged).
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* feat(core): canonical frame decoder — capture-to-claim replay (ADR-136)
The encode half of the ADR-136 frame contract existed (ComplexSample,
to_canonical_bytes, witness_hash) but there was no decoder: a captured
canonical frame could be witnessed but never reconstructed, blocking
replay-from-capture.
CsiFrame::from_canonical_bytes is the exact inverse: same id, metadata,
complex payload, and witness hash (tested as the round-trip law AC7 — the
replayed frame re-encodes byte-identically). Amplitude/phase are recomputed
from the payload (projections, not independent state). Every malformed-input
class fails closed (AC8): header truncation -> Truncated, payload truncation
-> PayloadMismatch, unknown discriminants, non-UTF-8 device id, trailing
bytes. Nil calibration uuid decodes as None per the documented encoding.
Core: 36 tests pass. Workspace gate: 2,937 passed / 0 failed.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* feat(engine): dynamic min-cut mesh partition guard (ruvector-mincut)
Maintains an exact min-cut over the live mesh coupling graph — nodes are
sensing nodes, coupling is the product of fusion attention weights — and
surfaces per cycle, as TrustedOutput::mesh:
- cut value: the global "how close is the array to partitioning" number,
a structural measure per-node heuristics miss;
- weak side: which specific nodes would split off (failure/jamming triage,
feeds ADR-032 posture);
- at-risk flag: counts as a structural event for the drift->recalibration
advisor (alongside ADR-142 change-points).
Degenerate cases fail toward risk: a node with zero coupling is reported as
already partitioned (cut 0, that node as the weak side).
Measured cost policy (criterion, 12-node mesh — the honest part):
- weights quantized (1/64) + change-gated: steady-state cycles do ZERO graph
work and reuse the cached cut (~7.3 us, ~23x cheaper than building);
- on any real change a full exact rebuild (~171 us) is used, because ONE
DynamicMinCut delete+insert measured ~240 us — the subpolynomial machinery
amortizes on much larger graphs, so rebuild-on-change is the measured
optimum at mesh scale (one-edge case -28% after switching policy);
- full process_cycle with the guard: ~33 us for 4 nodes vs the 50 ms budget.
9 mesh_guard tests (weak-node detection, steady-state zero updates,
sub-quantum gating, join/drop rebuild, determinism, disconnection) + an
engine-level wiring test (down-weighted node -> weak side -> recalibration).
Engine 24 tests; workspace gate 2,946 passed / 0 failed.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* feat(engine): mesh partition risk demotes privacy + enters the witness (ADR-032)
Completes the mesh-guard integration: its at_risk signal was advisory-only
(fed the recalibration advisor). It now also contributes to the ADR-141
privacy demotion alongside fusion- and array-level contradictions — a mesh
close to partitioning makes the fused belief less trustworthy, so the cycle
emits at a more restricted class (monotonic; information only removed).
Because effective_class feeds the BLAKE3 witness, a fragmenting array now
shifts the witness: partition risk is auditable, not just logged. The mesh
computation moved ahead of the demotion step in process_cycle; mesh_guard_mut
exposes risk-threshold tuning.
Test: a forced-risk 3-node cycle demotes PrivateHome Anonymous->Restricted
and shifts the witness vs a clean baseline. Engine 25 tests; workspace gate
2,947 passed / 0 failed.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
* fix: public-PR review findings — privacy-path honesty, gate holes, mesh-guard cliff
- sensing-server: engine errors logged+counted (no silent swallow), trust
state exposed via status surface, privacy-demotion claims aligned with
the actual parallel-audit-path behavior
- occupancy_bench: vacuous-F1 hole closed (degenerate test sets fail with
their own criterion); CI-lower-bound test made probative
- mesh_guard: quantization scaled to observed coupling range — >=65-node
balanced meshes no longer permanently at_risk (regression test)
- engine: both wiring tests made probative (same-topology witness compare,
deterministic risk-crossing fixture)
- mat: axum/tokio optional behind api; real serde feature (api enables it)
- core: canonical decoder strict (non-zero reserved bytes and nil UUID
rejected — injective on accepted domain, forged-bytes tests)
- CHANGELOG: un-spliced the FFT/adapter bullet mangle
Co-Authored-By: claude-flow <ruv@ruv.net>
* chore: strip private-track references for public PR
Reword the occupancy-benchmark changelog bullet to drop a cross-reference
to the private research track, and restore the WorldGraph retention bullet
header that was glued onto the preceding MAT bullet.
Co-Authored-By: claude-flow <ruv@ruv.net>
* chore: lockfile refresh for cherry-picked feature set
Co-Authored-By: claude-flow <ruv@ruv.net>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* docs(adr): ADR-151 — Per-Room Calibration & Specialized Model Training
Room-first calibration -> bank of small specialised ruVector models
(breathing, heartbeat, restlessness, posture, presence, anomaly) distilled
from the frozen Hugging-Face-published RF Foundation Encoder (ADR-150).
Four-stage local-first pipeline: baseline (ADR-135 environmental fingerprint)
-> guided enrollment (NEW EnrollmentProtocol, clean anchors not hours) ->
feature extraction (reuse signal_features + ruvsense) -> specialist bank
training (rapid_adapt LoRA heads, RVF storage, HNSW prototypes).
Invariants: specialisation over scale; local heads over a shared public base;
honest STALE degradation on baseline drift. Indexes ADR-149/150/151.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(cli): calibration HTTP API for UI-driven baseline capture (ADR-135/151)
Adds `wifi-densepose calibrate-serve` — an Axum HTTP API that wraps the
ADR-135 CalibrationRecorder so a UI (or any client) can drive an empty-room
baseline capture remotely. Stage 1 ("teach the room") of the ADR-151 room
calibration & training pipeline.
A single background task owns the UDP socket (ESP32 0xC511_0001 frames) and
the optional active recorder; HTTP handlers talk to it over an mpsc command
channel and read a shared status snapshot, keeping the &mut recorder
lock-free. CORS permissive so a browser UI can call it.
Endpoints (/api/v1/calibration/*):
GET /health liveness + UDP ingest stats (frames_seen, streaming)
POST /start { tier?, duration_s?, room_id?, min_frames? }
GET /status live progress (state, frames, progress, z, eta) — poll for UI
POST /stop finalize the current session early
GET /result finalized baseline summary (amp/phase-dispersion averages)
GET /baselines list persisted baseline .bin files
Reuses the existing calibrate.rs ESP32 wire parser (made pub(crate)); honest
abort when <10 frames arrive in the window (e.g. ESP32 not streaming).
Verified end-to-end over loopback: start -> 300 replayed HT20 frames ->
state=complete, 52-subcarrier baseline, phase_dispersion_avg=0.00096
(concentrated/valid), persisted to disk; all 6 endpoints exercised.
CLI: 19 tests pass; crate builds clean.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(cli): firewall-free CSI UDP relay for local Windows ESP32 testing
Windows Defender blocks inbound LAN UDP to a freshly-built binary without an
admin allow-rule; python.exe is already allowed. This relay binds the public
CSI port and forwards each datagram verbatim to a loopback port where
`calibrate-serve --udp-bind 127.0.0.1 --udp-port 5006` listens (loopback is
firewall-exempt). No admin required.
Validated: ESP32-format 0xC5110001 frames -> :5005 -> relay -> :5006 ->
calibrate-serve -> state=complete, 52-subcarrier baseline,
phase_dispersion_avg=0.00098 (clean). Completes the no-admin live-test path.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(changelog): record ADR-151 calibration API (calibrate-serve)
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(calibration): ADR-151 Stages 2–5 — enrollment, extraction, specialist bank, runtime
New crate wifi-densepose-calibration implementing the per-room pipeline beyond
Stage-1 baseline:
- anchor.rs: guided-anchor sequence + event-sourced EnrollmentSession (Stage 2)
- enrollment.rs: AnchorQualityGate + AnchorRecorder — gates anchors against the
ADR-135 baseline deviation (presence/motion), re-prompts bad captures
- extract.rs: Features + AnchorFeature — autocorrelation periodicity (breathing/
HR bands), variance/motion (Stage 3)
- specialist.rs: 6 small room-calibrated models — presence (learned threshold),
posture (nearest-prototype), breathing/heartbeat (band periodicity),
restlessness (calm/active normalization), anomaly (novelty vs anchors) (Stage 4)
- bank.rs: SpecialistBank — train/persist + baseline-drift STALE invalidation
- runtime.rs: MixtureOfSpecialists — presence short-circuit + anomaly veto +
stale flagging (Stage 5)
Statistical heads make the pipeline runnable/validatable today; the ADR-150 HF
RF Foundation Encoder backbone is the documented upgrade path. 29 unit tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(cli): wire ADR-151 enroll / train-room / room-status / room-watch
Integrates the wifi-densepose-calibration crate into the CLI as four
subcommands driving the full Stage 2–5 pipeline against a live ESP32 raw-CSI
stream (edge_tier=0):
- enroll: walks the guided anchor sequence, gates each capture against the
ADR-135 baseline deviation (re-prompts bad anchors), writes labelled features
- train-room: fits the SpecialistBank from the enrollment, persists JSON
- room-status: prints a trained bank's summary
- room-watch: live mixture-of-specialists readout (presence/posture/breathing/
heart/restless) over a rolling window, with anomaly veto + STALE flagging
Per-frame scalar is the mean CSI amplitude (carries presence/motion + breathing
modulation). Validated end-to-end on the live ESP32 (COM8, edge_tier=0): the
real parser → feature extraction → runtime detected breathing (~16–31 BPM) on
hardware. Full multi-anchor enrollment accuracy requires the operator to perform
the poses; phase-based breathing extraction is a noted refinement.
48 tests pass (29 calibration + 19 CLI).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-151): mark Stages 1–5 implemented; expand CHANGELOG
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(cli): keep proven mean-amplitude carrier for room features
The max-variance-subcarrier carrier locked onto motion artifacts (not
breathing) and also had an out-of-bounds bug on variable CSI subcarrier
counts. Reverted to the mean-amplitude carrier, which is validated live to
detect breathing. Phase-based extraction on a stable subcarrier remains the
proper higher-SNR refinement (ADR-151 §4).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(calibration): multistatic fusion of co-located nodes (ADR-029/151)
MultiNodeMixture fuses several co-located nodes (each with its own
room-calibrated SpecialistBank) into one RoomState:
- presence: OR across nodes (any node seeing a person wins)
- posture/breathing/heartbeat: highest-confidence node (best viewpoint)
- restlessness/anomaly: max across nodes
- veto: any node's physically-implausible signal vetoes the room's vitals
(anti-hallucination, same as single-node runtime) + presence short-circuit
- stale: any node's STALE flag propagates
Same-room multistatic only; cross-room is federation (ADR-105), not fusion.
6 unit tests (presence OR, best-confidence breathing, single-node veto,
staleness). 35 calibration tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(cli): multistatic room-watch — fuse co-located nodes (ADR-029/151)
`room-watch --node-bank N:path` (repeatable) groups live CSI frames by node_id
and fuses per-node banks via MultiNodeMixture. Validated live on COM8 (node 9,
edge_tier=0): frames grouped + fused end-to-end. True 2-node fusion is covered
by unit tests; a second raw-CSI node is the hardware blocker. 54 tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(integration): calibration → cognitum-v0 appliance integration overview
Detailed cross-repo integration spec for cognitum-one/v0-appliance: data
contracts (CSI wire format, ADR-135 baseline binary, enrollment/bank/RoomState
JSON schemas), calibrate-serve HTTP API, public crate API, Pi5+Hailo tiering,
and a 5-step appliance integration plan. Grounded in the verified cognitum-v0
inventory (aarch64, cargo 1.96, HAILO10H, ruview-vitals-worker:50054).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(calibration): address PR review — aarch64 decouple, API auth, path traversal, throttle
Resolves the review on #989:
- **Cross-compile (the appliance blocker):** make wifi-densepose-mat optional
and feature-gate it (`mat`), so `cargo build -p wifi-densepose-cli
--no-default-features` excludes the mat→nn→ort(ONNX)→openssl-sys chain.
Verified: `cargo tree --no-default-features` shows 0 ort/openssl deps →
calibration cross-compiles clean for the Pi.
- **Security (must-fix before LAN):**
- `--token` / CALIBRATE_TOKEN bearer-auth middleware on every route; warns if
bound non-loopback without a token.
- sanitize client-supplied `room_id` to [A-Za-z0-9_-] (≤64) before it reaches
the baseline write path — kills the `../` file-write primitive. + test.
- **Perf:** stop locking shared status + cloning SessionStatus on every UDP
frame — counters/snapshot flush on the 200 ms tick instead (no CPU
starvation under flood). finalize write moved to async `tokio::fs::write`.
- **Docs:** ADR-151 STALE wording matches the impl (baseline-id change;
drift-threshold = P6 refinement); integration doc gets the
`--no-default-features` build + auth/sanitize notes.
35 calibration + 15 CLI tests (no-default) / 20 CLI (default) pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(worldgraph,worldmodel): add crates.io READMEs
Plain-language overviews + feature lists, comparison tables (symbolic graph vs
predictive occupancy; graph vs grid vs event-log), usage, and technical
details. Adds readme = "README.md" to both manifests so they render on
crates.io on the next release.
Co-Authored-By: claude-flow <ruv@ruv.net>
* release: worldgraph & worldmodel 0.3.1 (READMEs on crates.io)
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs: precise calibration validation scope (capture+API+auth proven; clean enroll→train→infer not yet on-target)
Aligns ADR-151 §7 + the appliance integration doc with the PR #989 scope
clarification: nothing has run a clean baseline → enroll → train → infer on
live CSI; the live breathing read used the stateless head, not a trained bank.
Adds --source-format adr018v6 to the backlog.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(calibrate-serve): live GET /room/state endpoint (mixture over CSI window)
Adds a live RoomState readout over HTTP — the appliance UI's main need. The
ingest task maintains a rolling per-frame scalar window (flushed on the 200 ms
tick, no per-frame lock); the handler loads a bank (resolved as a sanitized
name under output_dir — same path-traversal defense as room_id), runs the
MixtureOfSpecialists over the window, returns RoomState JSON.
Validated live (ESP32-S3 via relay): breathing 14-19 BPM over HTTP; a
bank=../../etc/passwd query is neutralized to 'etcpasswd' (no traversal).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(calibrate-serve): POST /room/train + fix AnchorLabel JSON to snake_case
- POST /api/v1/room/train: { room_id, baseline_id, anchors[] } → trains a
SpecialistBank and persists it as <output_dir>/<room_id>.json (path-sanitized),
readable via /room/state?bank=<room_id>. Completes the HTTP train→infer loop.
- Fix data-contract bug: AnchorLabel serialized as PascalCase variant names
(serde default) while as_str() + the integration doc used snake_case. Added
#[serde(rename_all = "snake_case")] so the JSON wire format matches the
documented contract (empty/stand_still/…). Locked with a roundtrip test.
Validated live (ESP32-S3): POST train (4 anchors → 6 specialists, persisted) →
GET /room/state returns RoomState with the trained presence/restlessness; the
synthetic-vs-real scale mismatch correctly triggers the anomaly veto. 36
calibration tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(calibrate-serve): live enroll-over-HTTP (POST /enroll/anchor + /enroll/status)
Closes the last HTTP gap — the appliance can now drive the ENTIRE calibration
pipeline over HTTP without the CLI:
baseline (start/stop) -> enroll/anchor x8 -> room/train -> room/state
- POST /enroll/anchor { room_id, baseline, label, duration_s? }: the ingest task
loads the baseline (sanitized name under output_dir), captures the anchor for
the duration against it (AnchorRecorder + per-frame series), runs the quality
gate, and on completion replies with the verdict + accumulates the AnchorFeature
in an in-server enrollment map keyed by room_id. Re-prompts on rejection.
- GET /enroll/status?room=<id>: accepted anchors, next, complete.
- POST /room/train now falls back to the in-server enrollment when anchors[] is
omitted.
Validated live (ESP32-S3): capture baseline -> enroll stand_still (271 frames,
6s) -> gate correctly rejects "no person detected (presence_z 0.90 < 1.50)"
relative to a same-occupancy baseline (a clean empty-room baseline is the
documented on-target prerequisite). Builds clean; CLI tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(calibrate-serve): HTTP integration tests for the room/enroll endpoints
Factor the router into build_router() (shared by execute + tests) and add
tower-oneshot integration tests (no network/ingest needed):
- health + descriptor → 200
- POST /room/train persists the bank; GET /room/state → 200; train with no
anchors/enrollment → 400
- path-traversal: /room/state?bank=../../etc/passwd → 404 (sanitized, never
reads outside output_dir)
- enroll/status empty; /enroll/anchor with an unknown label → 400
CI regression coverage for the endpoints added this session. 18 CLI tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(mat): make serde non-optional — unblocks `cargo test --workspace --no-default-features`
Making wifi-densepose-mat optional in the CLI (for the aarch64/ort decouple)
exposed a latent feature bug: mat's `api` module compiles unconditionally and
uses serde, but `serde` was an optional dep enabled only via the `api`/`serde`
features. Previously the CLI's *unconditional* mat dependency enabled those
features transitively, so `--workspace --no-default-features` still got serde;
once mat became optional+gated, the workspace build lost it →
`error[E0432]: unresolved import serde` across mat's api/* (CI red).
mat already pulls serde_json + axum unconditionally, so making `serde`
non-optional has no real cost and restores the workspace build. Does NOT affect
the aarch64 CLI build (mat isn't built there at all): verified
`cargo tree -p wifi-densepose-cli --no-default-features` still shows 0
ort/openssl deps, and `cargo test --workspace --no-default-features` compiles
clean.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(claude.md): add wifi-densepose-calibration to crate table (pre-merge)
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr): ADR-152 — WiFi-pose SOTA 2026 intake (geometry-conditioned calibration, external benchmarks, encoder recipe)
Records the 2026-06-10 deep-research run (22 sources, 110 claims, 25
adversarially verified: 24 confirmed / 1 refuted) and the decisions it
implies:
- §2.1 ACCEPTED: geometry-condition the ADR-151 calibration system —
NodeGeometry at enrollment, geometry embeddings for future LoRA heads,
PerceptAlign-style two-checkerboard camera↔WiFi alignment for the
ADR-079 supervised path. PerceptAlign (MobiCom'26) names the failure
mode ("coordinate overfitting") that matches our own ADR-150 cross-
subject collapse.
- §2.2 ACCEPTED: benchmark protocol vs external "WiFlow-STD (DY2434)"
(claimed 97.25% PCK@20, Apache-2.0 weights+dataset) with a no-citation
rule until measured on our 17-keypoint ESP32 eval set. Name collision
with our internal WiFlow is disambiguated.
- §2.3 ACCEPTED: amend ADR-150 training recipe per UNSW MAE study —
80% masking, (30,3) patches, data-over-capacity priority (log-linear,
unsaturated at 1.3M samples).
- §2.4 watch items: IEEE 802.11bf-2025 published 2025-09-26;
esp_wifi_sensing as external presence baseline (drop-in claim REFUTED
0-3); ZTECSITool 160MHz/512-subcarrier anchor node (procurement-gated).
- §2.5 NOT adopted: non-WiFi "foundation model" papers; DensePose-UV
(no 2025-2026 work does UV regression from commodity WiFi).
Every number is evidence-graded CLAIMED vs MEASURED in the source
register. Re-check horizon 2026-12.
Co-Authored-By: RuFlo <ruv@ruv.net>
* test(calibration): full-loop integration test — baseline→enroll→train→infer proven in-process (ADR-151 §7 gap, software half)
Closes the software half of PR #989's headline validation gap: the
complete calibration loop had never run end-to-end anywhere, even
in-process. tests/full_loop.rs (412 lines, deterministic xorshift32
room simulator, HT20/52-subcarrier/20Hz, same fingerprint family as
the ADR-135 roundtrip test) now drives the CLI's exact stage order
through the public API:
1. baseline — 600 static frames, zero motion flags post-warmup,
calibration_uuid() exactly as the CLI derives it
2. enroll — all 8 AnchorLabel::SEQUENCE anchors through
AnchorQualityGate::default(), session is_complete()
3. extract — AnchorFeature::from_series recovers injected 0.25Hz
and 0.125Hz breathing within ±0.04Hz
4. train — SpecialistBank::train fits all 6 specialists; JSON
round-trip and the runtime consumes the RELOADED bank
5. infer — positive: never-enrolled 0.30Hz subject reads present,
18±2 BPM; negative: empty window reads absent;
degradation: foreign baseline_id flags STALE
Seed-robust (5 seeds), passes with and without default features:
36 unit + 1 integration green.
Validation docs updated (ADR-151 §7 + integration doc §7 matrix): what
remains is strictly the on-target hardware session (real CSI, physically
empty room, operator performing the guided anchors). Three behavioral
findings from building the test are recorded for pre-session triage:
z-band squeeze between baseline motion flagging (z>2.0) and the still-
anchor gate (presence_z≥1.5) — likeliest on-hardware enroll failure;
variance-only PresenceSpecialist missing motionless-person mean shift;
ungated breathing_hz/heart_hz in noise-window embeddings.
Co-Authored-By: RuFlo <ruv@ruv.net>
* fix(calibration): close all four ADR-152 behavioral findings pre-hardware-session
The full-loop integration test surfaced three findings; fixing the third
exposed a fourth. All four are fixed and regression-guarded:
1. z-band squeeze (enrollment.rs) — anchor motion is now measured from
frame-to-frame deltas of the deviation series (|Δz| > Z_DELTA_MOTION
0.5 ∨ |Δφ| > π/6), not from the absolute motion_flagged, which fires
at amplitude_z_median > 2.0 vs the EMPTY baseline and so conflated
presence strength with motion. A strongly-reflecting still person
(z = 3.0 — every frame flagged by the old heuristic) now enrolls.
The old unit tests mocked (z=3.0, motion=false), a combination the
real deviation() can never emit — which is exactly how the squeeze
hid; tests now derive the flag from z the way the producer does.
2. variance-only presence (specialist.rs) — PresenceSpecialist gains a
mean-shift channel: present when variance > threshold OR
|mean − empty_mean| > mean_dist_threshold (trained at half the
empty→occupied mean distance, None when the means don't separate).
Detects the motionless person whose body raises the scalar mean but
not its variance. Old persisted banks deserialize with the channel
inert (serde default None) — variance-only behavior preserved,
proven by a fixture test against pre-change JSON.
3. ungated hz embedding (extract.rs) — Features::embedding() zeroes
breathing_hz/heart_hz below EMBED_MIN_SCORE (0.25), keeping the
random in-band peaks of noise windows out of the posture/anomaly
prototype space. Raw fields stay ungated (specialists have their
own stricter gates).
4. heart-band lag-floor leakage (extract.rs, found while fixing 3) —
a pure 0.30 Hz breathing signal scored 0.67 in the heart band at
3.33 Hz: out-of-band rhythm leaks as a monotonic slope whose max
sits at the band's lag floor, so score gating alone cannot stop it.
autocorr_dominant now requires the winning lag to be an interior
local maximum; band-edge "peaks" are rejected, true in-band peaks
(interior by definition) are preserved.
full_loop.rs strengthened to drive the fixes end-to-end: the StandStill
anchor is now a z=3.0 strong reflector (unenrollable pre-fix), and a new
motionless-person runtime case proves mean-channel detection at empty-
level variance.
Validation: 41 calibration unit + 1 full-loop integration + 23 CLI tests
green; cargo test --workspace --no-default-features exit 0.
Co-Authored-By: RuFlo <ruv@ruv.net>
* fix(firmware): correct heart-rate estimation — sample-rate + harmonic lock
The edge vitals HR was stuck at ~45 BPM regardless of true heart rate
(Apple Watch ground truth 87 BPM read as ~45) and "dropped a lot" between
frames. Two root causes:
1. Stale fixed sample rate. estimate_bpm_zero_crossing() used a hardcoded
`sample_rate = 10.0f` (and the biquads a separate `fs = 20.0f`). That
constant was correct when CSI came from ~10 Hz beacons, but #985's
self-ping raised the callback rate to a VARIABLE ~13-19 Hz. BPM scales as
(assumed_rate / actual_rate) x true, so a true 87 read ~45, and because
the real rate fluctuates with CSI yield while the code assumed a fixed
value, the reported HR swung frame-to-frame (the "drops").
2. Breathing-harmonic lock. Zero-crossing HR estimation locked onto a
breathing harmonic — a 0.25 Hz breathing fundamental puts its 3rd
harmonic at ~0.74 Hz ~= 44 BPM, right in the HR band — so it parked at
~45 BPM independent of the real heartbeat.
Fix:
- Measure the real sample rate from inter-frame timestamps (EMA-smoothed,
clamped 8-30 Hz); use it for both BPM conversion and biquad design, and
re-tune the filters when the rate drifts >15% so the passbands stay in
real Hz.
- Replace the HR zero-crossing with estimate_hr_autocorr(): autocorrelation
peak in the 45-180 BPM band that explicitly rejects lags within 8% of any
breathing harmonic (k=1..6), with parabolic interpolation and a peak-
confidence gate (returns 0 rather than a noise value).
- Median-smooth (N=9) the emitted HR over valid estimates to kill residual
single-frame outliers.
Validated on hardware (ESP32-S3, COM8/192.168.1.80) vs an unmodified board
(192.168.1.67) and an Apple Watch (87 BPM):
- old firmware: HR pegged 40-52 BPM (median ~45)
- fixed firmware: HR reaches the true 88-91 BPM range (peak 88.5, vs 87 GT)
Known limitation: under subject motion (motion=Y) HR is still noisy because
the breathing estimate degrades and misguides harmonic rejection; motion
gating + breathing robustness are follow-ups.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(firmware): robust HR harmonic rejection via autocorr breathing period (#987)
Follow-up to 332c2a98d. The HR harmonic rejection was fed the noisy
zero-crossing breathing estimate, which under motion notched the wrong
frequencies and let the autocorr lock onto the ~0.75 Hz breathing harmonic
(~45 BPM). Generalize estimate_hr_autocorr -> estimate_periodicity_autocorr
and drive HR harmonic rejection from a robust autocorrelation breathing
period instead; widen the HR median smoother to N=13.
Hardware A/B (fixed .80 vs unmodified control .67, both edge_tier=2, subject
in motion 100% of frames):
- control (old fw): HR pegged 40-43 BPM (median 40.6)
- fixed: HR 60-91 BPM (median 71.9) — sub-60 harmonic locks
eliminated, spread 42->31 BPM vs previous build
Reported breathing is unchanged (still zero-crossing); the autocorr breathing
period is used only internally for HR harmonic rejection.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(changelog): record ESP32 heart-rate fix (#987)
Co-Authored-By: claude-flow <ruv@ruv.net>
The ESP32 CSI engine only produces CSI for received OFDM frames (L-LTF/
HT-LTF). On a quiet network — or on a display-enabled build where the
#893 MGMT->MGMT+DATA promiscuous upgrade is skipped (has_display=true) —
the only CSI-eligible frames are sparse beacons (often non-OFDM DSSS),
so wifi_csi_callback can starve to yield=0pps -> DEGRADED -> motion=0
(#521, #954).
Fix (additive): pin a ~50 Hz OFDM unicast floor by pinging the STA's own
DHCP gateway. The router's ICMP echo replies are OFDM frames destined to
this station and drive the CSI engine regardless of promiscuous filter
state or ambient traffic. Mirrors Espressif's esp-csi csi_recv_router
reference. Promiscuous capture (#396/#893) is left fully intact so
multistatic/multi-node sensing still hears other stations' frames.
Reconciles PR #955 (which removed promiscuous entirely and conflicted
with the already-shipped #893 DATA-capture path) into an additive change
on current main.
Verified on ESP32-S3 (N16R8, COM8), ESP-IDF v5.4:
Promiscuous mode enabled (MGMT-only, RuView#396)
self-ping started -> 192.168.1.1 @50Hz (CSI OFDM source, fix #521/#954)
CSI cb #1: len=128 rssi=-40 ch=5
adaptive_ctrl: state=6 yield=13-19pps motion=1.00 presence>0 (SENSE_ACTIVE)
DEGRADED cleared; CSI yield stable ~15 pps over 60 s.
Co-authored-by: Meraj <merajmehrabi@gmail.com>
Background
Issue #937 in the cognitum-v0 appliance repo flagged that the
`cognitum-csi-capture` systemd unit shipped `--simulate` by default,
silently serving synthetic CSI tagged as production telemetry on
`/api/v1/sensor/stream`. That's a textbook trust-eroding pattern — the
single most-cited "where's the real data?" evidence external reviewers
(#943, #934) point at when they call the project AI-slop.
A grep across THIS tree surfaced the exact same anti-pattern in three
places:
docker/docker-compose.yml:27 # auto (default) — probe ESP32, fall back to simulation
docker/docker-entrypoint.sh:14 # CSI_SOURCE — data source: auto (default), ...
main.rs:6435 info!("No hardware detected, using simulation"); "simulate"
The sensing-server's `auto` source resolver at main.rs:6425-6440
silently fell back to synthetic with only an `info!` log line as the
signal. Downstream consumers calling `/api/v1/sensing/latest` or
`/ws/sensing` had no in-band way to know they were being served fake
data.
Fix
`auto` now refuses to fall back. When neither ESP32 UDP nor host WiFi
is detected, the server logs a clear `error!` explaining the situation
and exits 78 (EX_CONFIG). The error message names the two ways to
proceed: provision real hardware, or set `--source simulated` /
`CSI_SOURCE=simulated` explicitly. Existing operators who already use
`--source simulated` (or its legacy `simulate` alias) are unaffected —
the alias is preserved for back-compat.
Docker entrypoint comment, docker-compose comment, and the Tauri
desktop app's source-default path also updated to reflect the new
posture. The desktop app keeps its `simulated` default because it's
an explicit demo product — the value passed downstream is the
*explicit* `simulated`, not `auto`, so the server tags it correctly
and never lies about its data source.
Validation
cargo build -p wifi-densepose-sensing-server --no-default-features
cargo test -p wifi-densepose-sensing-server --no-default-features
→ 122 / 122 pass, build clean (existing pre-fix warnings unchanged).
Deployment
⚠ Breaking change for unattended deployments that relied on the
`auto → simulated` silent fallback. That is exactly the failure mode
this PR fixes: pretending to serve real sensing data when the source
is fake. Operators who genuinely want demo mode set
`CSI_SOURCE=simulated` explicitly; the error message and the
docker-compose comment both point them there.
* fix(firmware,docker): clear three high-severity bugs in one sweep
Closes#946 — wasm3 fails on Xtensa GCC 15.2.0 (ESP-IDF v6.0.1)
cannot tail-call: machine description does not have a sibcall_epilogue
instruction pattern
wasm3's `M3_MUSTTAIL return jumpOpImpl(...)` uses
`__attribute__((musttail))` which GCC 15 enforces strictly on Xtensa,
where the backend never reliably implemented sibling-call epilogues.
Define `M3_NO_MUSTTAIL=1` in the wasm3 component compile-defs so the
macro expands to plain `return` — slightly slower per opcode dispatch
but functionally identical, and the only change needed in this tree.
Older IDF / GCC builds accept the define as a no-op so the IDF v5.4
CI build is unchanged.
Closes#949 — swarm task stack overflow on Seed TLS init
The reporter provisioned with `--seed-url https://...` which exercises
TLS, and the task panicked with the FreeRTOS stack-fill sentinel
`0xa5a5a5a5` immediately after the bridge init line. `SWARM_TASK_STACK`
was 3 KB ("HTTP client uses ~2.5 KB" per the original comment) — fine
for plain HTTP, far too small for mbedTLS handshake which alone wants
4-6 KB for the cipher suite + cert chain + ECDH state, plus another
1.5-2 KB for esp_http_client. Bumped to 8192 with the why in the
comment. Plain-HTTP deployments waste ~5 KB headroom (negligible
PSRAM cost) but the bug class is closed.
Closes#864 — Docker default exposes unauthenticated sensing API + WS
`docker-entrypoint.sh` started the sensing-server with `--bind-addr
0.0.0.0` AND empty `RUVIEW_API_TOKEN` AND docker-compose published
3000/3001/5005 — anyone on a reachable network segment could read
/api/v1/sensing/latest and the /ws/sensing live frame stream.
Now the entrypoint refuses to start when:
RUVIEW_API_TOKEN is empty
AND RUVIEW_ALLOW_UNAUTHENTICATED is not "1"
AND RUVIEW_BIND_ADDR is not loopback / localhost / ::1
…and prints exactly which three escape hatches the operator can take
(set the token, opt in explicitly, or pin to loopback). Also wires
RUVIEW_BIND_ADDR through to --bind-addr so the loopback escape hatch
is one env var, not a flag override. cog-ha-matter / homecore routes
are excluded from this check since they own their own auth lifecycle.
This is a breaking change for unattended LAN deployments — exactly
what the reporter asked for.
Validation
* `idf.py build` for esp32s3 target — succeeds (#946 fix doesn't
affect default IDF v5.4 build path).
* `idf.py set-target esp32c6 && idf.py build` — succeeds, binary
1015 KB / 45% partition free.
* Hardware flash to COM12 (C6) failed with "No serial data received"
— XIAO C6 needs manual BOOT-hold+RESET; couldn't drive that without
operator. Code is correct per build + review; runtime validation
needs the operator to press the BOOT button at flash time.
* docker-entrypoint.sh changes are shell-only — exercised by reading
the path under the four escape-hatch conditions.
Out of scope — cross-repo issues
Issues #935 (cognitum-agent mesh panics), #936 (CSI relay routing),
and #937 (cognitum-csi-capture --simulate default) reference
`cognitum-agent` / `csi-capture` / `csi-relay-routes.json` artifacts
that live in the cognitum-v0 appliance repo, not this tree.
Issue #954 (CSI callback never fires on S3 v0.6.5/v0.7.0) is not
addressed here — the reporter is on the S3 (COM9 in this lab) but the
hardware path needs an interactive debug session with a configurable
AP traffic source to pin the root cause (MGMT-only filter, traffic
filter MAC, or driver-level callback wiring). Will tackle in a
follow-up.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(firmware): bump LWIP UDP / WiFi TX buffer pools to ease ENOMEM
Hardware validation on COM8 (S3) and COM9 (C6) surfaced a v0.7.0
regression not captured in the existing issue tracker: stock IDF v5.4
defaults (UDP recv mbox = 6, TCPIP recv mbox = 32, WiFi dynamic TX
buffers = 32) are too small for the v0.7.0 packet mix once CSI
promiscuous mode is active. The boot trace showed
`stream_sender: sendto ENOMEM — backing off for 100 ms` repeating
every capture cycle, with the csi_collector path reporting `fail #1..5`
within seconds of associating to an AP.
Modest bumps applied (~3 KB extra heap each):
CONFIG_LWIP_UDP_RECVMBOX_SIZE 6 → 32
CONFIG_LWIP_TCPIP_RECVMBOX_SIZE 32 → 64
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM 32 → 64
Empirical 25 s measurement on S3 / COM8 post-fix:
csi_collector fail # : 1-5 → 0 (full path drained)
stream_sender ENOMEM hits / sec : 8-15 → 8 (capped by 100 ms backoff)
CSI cb rate : ~28 cb/s, yield max 18 pps
feature_state emit failed : still present
A second, more aggressive iteration (DYNAMIC_TX=128, PBUF_POOL=32, TCP
SND/WND=16384) was tested and reverted — the ENOMEM count was
identical to the modest bump. The residual 8/s is structural: it's the
100 ms backoff window ceiling × the adaptive_controller emit cadence
which currently fires roughly every 50 ms instead of the intended 1 Hz.
Bigger buffers don't fix that — only rate-limiting the emitter does.
Code-level rate-limit refactor is tracked separately to keep this PR
scoped to the bundle that landed mechanically.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(firmware): rate-limit feature_state emit from 5 Hz → 1 Hz
Completes the ENOMEM cure that the LWIP/WiFi buffer bumps started.
Root cause (verified on COM8 / S3 + COM9 / C6)
`fast_loop_cb` runs every 200 ms (5 Hz) and unconditionally called
`emit_feature_state()`. Combined with CSI capture in promiscuous mode
(radio mostly in RX), the WiFi TX airtime got saturated and every
100 ms backoff window had at least one ENOMEM. Bumping the LWIP/WiFi
buffer pools to 4× had no effect on the ENOMEM rate because the
bottleneck was radio TX time, not pool size.
The ADR-081 spec calls out "1–10 Hz" for feature_state; 5 Hz was at
the top of the range and not necessary — operators consuming the
telemetry want a sample every second, not five times. Dropping to
1 Hz frees ~80 % of the feature_state TX traffic.
Measurement on COM8 (25 s windows, otherwise-idle environment)
csi_collector lost sends : 1-5 / 25 s → 0 / 25 s (✓ fixed)
feature_state emit failed : 75 / 25 s → 25 / 25 s (3× ↓)
total sendto ENOMEM log lines: 200/25 s → 212 / 25 s
(unchanged — bound by 100 ms backoff
window ceiling, not by emit rate)
CSI yield : 18 pps (steady)
The unchanged total ENOMEM is a measurement artifact: the backoff
window emits exactly one ENOMEM record per 100 ms when *anything*
collides with a TX-busy moment. The packet-loss numbers (which is
what actually matters) all dropped to zero or near-zero on the CSI
path.
Implementation
Pure-static `s_emit_divider` counter in `fast_loop_cb`. Every 5th tick
calls the emit. Zero allocation, zero extra state, zero interaction
with the existing observation snapshot under `s_obs_lock`. Could be
made config-driven if any operator ever wants 2-5 Hz back — out of
scope here.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(firmware): on_send ESP-NOW callback compat for IDF v6.0 (closes#944)
ESP-IDF v6.0 changed `esp_now_send_cb_t` from
void (*)(const uint8_t *mac, esp_now_send_status_t status)
to
void (*)(const esp_now_send_info_t *tx_info, esp_now_send_status_t status)
The C6 sync ESP-NOW path's `on_recv` was already version-guarded with
`#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)` (lines 102-112)
but the `on_send` sibling missed the equivalent guard. CI runs against
IDF v5.4 so the regression slipped through; the reporter on IDF v6.0.1
with xtensa-esp-elf esp-15.2.0_20251204 hit:
c6_sync_espnow.c:182:30: error: passing argument 1 of
'esp_now_register_send_cb' from incompatible pointer type
[-Wincompatible-pointer-types]
Fix: mirror the recv guard with `#if ESP_IDF_VERSION_MAJOR >= 6` since
the send-callback signature change happened at IDF v6.0 (not v5.x like
the recv-callback). Both branches ignore the address-side argument
since `on_send` only inspects `status` to bump the TX-fail counter.
Adds `#include "esp_idf_version.h"` so the macro is in scope.
Closes#944
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(signal): anchor estimate_occupancy noise floor to calibration (closes#942)
`test_estimate_occupancy_noise_only` asserts that 20 noise-only frames
fed through a 50-frame calibrated `FieldModel` yield 0 occupancy.
Failure reported on the upstream Linux + BLAS build.
Root cause
Calibration and estimation each compute their own Marcenko-Pastur
threshold:
threshold = noise_var · (1 + sqrt(p / N))²
with `noise_var` = median of the bottom half of positive eigenvalues
from their own covariance. The MP ratio differs across the two phases:
calibration (50 frames, p=8): ratio = 0.16, factor ≈ 1.96
estimation (20 frames, p=8): ratio = 0.40, factor ≈ 2.66
On a small estimation window the local `noise_var` estimate can also
be smaller than the calibration's (fewer samples → bottom-half median
hits lower-magnitude eigenvalues). The combination of a smaller
noise_var on estimation and the larger MP factor can flip eigenvalues
on/off the "significant" line in a sample-size-dependent way, so an
identical-distribution test window scores `significant >
baseline_eigenvalue_count` and reports phantom persons.
Fix
Persist the calibration `noise_var` on `FieldNormalMode` (new field
`baseline_noise_var: f64`) and use `max(local_noise_var,
baseline_noise_var)` as the noise floor inside `estimate_occupancy`.
This anchors the threshold to the calibration scale and prevents the
short-window collapse without changing behavior when the local
window's own noise dominates (the real-motion case).
`baseline_noise_var` defaults to 0.0 in the diagonal-fallback paths;
the estimation code treats 0.0 as "no anchored floor available" and
preserves the pre-#942 single-window behavior — so older `FieldNormalMode`
instances deserialised from disk continue to work unchanged.
Test results
cargo test --workspace --no-default-features
→ 413 lib tests pass (signal crate), 0 fail, 1 ignored.
The actual `eigenvalue`-gated test still requires BLAS (not buildable
on Windows). Logic-trace via the four numerical anchors above shows
the fix flips `noise_var` from the smaller local value back up to the
calibration scale, dropping `significant` to or below
`baseline_eigenvalue_count` so the saturating subtraction returns 0.
Closes#942
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): SAST actually scans the code + drop deprecated flaky semgrep action
Two real problems in the Static Application Security Testing job:
1. **It scanned a path that no longer exists.** `bandit -r src/` and
`semgrep … src/` pointed at the repo-root `src/`, but the Python code
moved to `archive/v1/src/` (64 .py files) when the runtime was rewritten
in Rust. So the SAST scan matched nothing — a silent no-op (this is also
why `bandit-results.sarif` was "Path does not exist" on recent runs).
Fixed both to `archive/v1/src/`.
2. **Deprecated + redundant + flaky semgrep step.** The
`returntocorp/semgrep-action@v1` step pulled `returntocorp/semgrep-agent:v1`
from Docker Hub every run (intermittently timing out → red check, e.g. on
#929) and is EOL. It was redundant: the pip `semgrep --sarif` step is what
feeds GitHub Security; the action only pushed to the Semgrep cloud app via
SEMGREP_APP_TOKEN. Removed it and folded its `p/docker` + `p/kubernetes`
rulesets into the pip semgrep command, so coverage is preserved with no
Docker pull.
The job stays `continue-on-error: true` (non-gating). YAML validated.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(protocol): resolve 0xC511_0004 magic collision (closes#928)
Background
`0xC511_0004` was assigned to two different packet formats in firmware
— `EDGE_FUSED_MAGIC` (ADR-063, 48-byte `edge_fused_vitals_pkt_t`) and
`WASM_OUTPUT_MAGIC` (ADR-040, variable-length `wasm_output_pkt_t`).
Both were transmitted. The sensing-server only had a WASM parser for
that magic and no fused-vitals parser, so on the ESP32-C6 + MR60BHA2
mmWave configuration the fused-vitals packet was silently misparsed
as a malformed WASM output — `breathing_rate` was read as
`event_count`, mmWave-fused vitals were lost, and spurious WASM events
were emitted to subscribers.
Fix
1. Reassign `WASM_OUTPUT_MAGIC` to `0xC511_0007` (next free slot per
the registry in `rv_feature_state.h`). Smaller blast radius than
moving fused-vitals — the registry already treats `0xC511_0004` as
fused-vitals canonical and several years of deployed feature
tracking depends on that assignment.
2. Add `parse_edge_fused_vitals` + `EdgeFusedVitalsPacket` in
`wifi-densepose-sensing-server::main`. Byte layout taken directly
from `edge_processing.h:129`, mirroring the firmware's
`_Static_assert(sizeof(edge_fused_vitals_pkt_t) == 48)` so future
firmware changes that grow the packet will break this parser
loudly instead of silently.
3. Add a dispatch arm in the UDP receive loop. Fused-vitals is tried
BEFORE WASM so a stale firmware (still emitting 0xC511_0004 with
the WASM payload) fails to parse as fused-vitals (size mismatch),
then fails to parse as WASM (magic mismatch on the new 0x...0007),
and gets dropped — a deliberate "fail loud" outcome rather than the
pre-fix silent garbage.
4. Update the registry comment in `rv_feature_state.h` to add the new
0x...0007 row.
5. Add five tests in a new `issue_928_magic_collision_tests` mod:
- `parse_edge_fused_vitals_extracts_fields_correctly`
- `parse_edge_fused_vitals_rejects_short_buffer`
- `parse_edge_fused_vitals_rejects_wrong_magic`
- `parse_wasm_output_rejects_legacy_0004_magic`
- `parse_wasm_output_accepts_new_0007_magic`
WebSocket payload
Fused-vitals now broadcasts as `{"type": "edge_fused_vitals", ...}`
with the mmWave-specific block nested under `mmwave`. Schema is
additive — existing subscribers that only inspect `type` are
unaffected; subscribers that switch on `type` gain a new branch.
Deployment note
This is a wire-protocol change. Firmware older than this commit that
emits WASM output on 0xC511_0004 will lose its WASM event stream
against an updated host (host expects 0xC511_0007). Per the issue
discussion, "fail loud" is preferred to silent misparsing. Operators
running C6+mmWave should reflash firmware concurrent with the host
upgrade.
Test results
cargo test -p wifi-densepose-sensing-server --no-default-features
--bin sensing-server
→ 122 passed / 0 failed (5 new + 117 existing, unchanged)
Co-Authored-By: claude-flow <ruv@ruv.net>
Per the CLAUDE.md pre-merge checklist (item 5, "Add entry under
[Unreleased]"), several recently-merged PRs landed without CHANGELOG
entries. Backfilling the user/operator-facing ones — most importantly the
MAT triage safety fix:
- #926 (Security/safety): survivor with a heartbeat never triaged Deceased
- #918: per-node HA devices report each node's own presence/motion
- #919: actionable --model load diagnostic (refs #894)
- #920: --export-rvf no longer silently produces a placeholder model
- #929 (Security): bearer scheme matched case-insensitively (RFC 6750)
CI-internal fixes (#925 rust-cache, #930 SAST) are intentionally omitted —
they don't change product behavior. Docs-only.
Two real problems in the Static Application Security Testing job:
1. **It scanned a path that no longer exists.** `bandit -r src/` and
`semgrep … src/` pointed at the repo-root `src/`, but the Python code
moved to `archive/v1/src/` (64 .py files) when the runtime was rewritten
in Rust. So the SAST scan matched nothing — a silent no-op (this is also
why `bandit-results.sarif` was "Path does not exist" on recent runs).
Fixed both to `archive/v1/src/`.
2. **Deprecated + redundant + flaky semgrep step.** The
`returntocorp/semgrep-action@v1` step pulled `returntocorp/semgrep-agent:v1`
from Docker Hub every run (intermittently timing out → red check, e.g. on
#929) and is EOL. It was redundant: the pip `semgrep --sarif` step is what
feeds GitHub Security; the action only pushed to the Semgrep cloud app via
SEMGREP_APP_TOKEN. Removed it and folded its `p/docker` + `p/kubernetes`
rulesets into the pip semgrep command, so coverage is preserved with no
Docker pull.
The job stays `continue-on-error: true` (non-gating). YAML validated.
`require_bearer` parsed the Authorization header with
`strip_prefix("Bearer ")`, which is case-sensitive. Per RFC 6750 §2.1 /
RFC 7235 §2.1 the auth-scheme is case-insensitive, so a correct token sent
as `Authorization: bearer <token>` (or `BEARER`, or with extra whitespace)
was rejected with a confusing "invalid bearer token" 401 — needless friction
when setting up `RUVIEW_API_TOKEN` (the active #864/#924 theme).
Now the scheme is matched with `eq_ignore_ascii_case` and leading token
whitespace trimmed. The token comparison itself is unchanged — still exact
and constant-time (`ct_eq`) — so this does not weaken auth: a wrong token or
a non-Bearer scheme (`Basic …`) still returns 401.
New test `accepts_case_insensitive_bearer_scheme` covers `bearer`/`BEARER`/
extra-space (accept) and wrong-token/`Basic` (still reject). bearer_auth
suite: 9 passed.
Both triage paths in the Mass Casualty Assessment tool classified a
survivor as Deceased (Black) on "no breathing + no movement" while
completely ignoring the heartbeat signal:
- domain `TriageCalculator::calculate` → `combine_assessments(Absent, None)`
returned Deceased. That branch is in fact only reachable *because* a
heartbeat makes `has_vitals()` true (breathing+movement absent alone →
Unknown) — so every "Deceased" was a live person with a pulse.
- detection `EnsembleClassifier::determine_triage` (the path used by
`classify()`) returned Deceased on `!has_breathing && !has_movement`,
also ignoring `reading.heartbeat`.
A survivor with a detectable pulse but no sensed breathing/movement is in
respiratory arrest — the most time-critical *savable* state. Reporting them
Deceased would deprioritize a rescuable person. WiFi-CSI also cannot confirm
death (no airway-repositioning step), so a pulse must override.
Fix: in both paths, if the result would be Deceased but a heartbeat is
present, return Immediate. Total absence of breathing, movement AND heartbeat
is unchanged (domain → Unknown, ensemble → Deceased).
2 safety regression tests added. Full MAT suite: 168 + 6 + 3 passed, 0 failed
(existing test_no_vitals_is_deceased still green — no heartbeat → Deceased).
The Rust Workspace Tests job manually cached the whole `v2/target` via
actions/cache@v4. For a 38-crate workspace that dir is multi-GB, and several
CI runs this cycle intermittently died at the cache/setup step (after
toolchain install, before "Run Rust tests"), each needing a rerun.
Swatinem/rust-cache@v2 is the de-facto standard Rust CI cache: it caches the
cargo registry/git + a pruned target, evicts stale dependencies, and restores
large workspaces far more reliably and faster than a naive whole-target cache.
`workspaces: v2` points it at the v2/ cargo workspace.
Reliability/speed change — verified by observing subsequent main runs.
The --export-rvf handler ran *before* the --train/--pretrain handlers and
unconditionally wrote placeholder sine-wave weights, then returned. So the
documented `--train --dataset … --export-rvf <path>` workflow
(user-guide.md) short-circuited to a PLACEHOLDER model and never trained —
printing "exported successfully" for a non-functional model. Given the
project's anti-"is it fake" stance, silently emitting a fake model is the
wrong default.
Fix:
- Only emit the placeholder container-format demo when --export-rvf is used
*standalone* (new `export_emits_placeholder_demo` guard). With
--train/--pretrain, fall through so the real training pipeline runs and
exports calibrated weights.
- The standalone path now prints a clear WARNING that it writes a
container-format demo with placeholder weights — not a trained model —
pointing to --train / a pretrained encoder (#894).
- Docs: flag --export-rvf as a placeholder demo in the flag table, and fix
the Docker training example to use --save-rvf (consistent with the
from-source example) instead of the placeholder --export-rvf.
3 unit tests for the guard. Full crate unit suite: 429 + 117 passed, 0 failed.
Users who downloaded ruvnet/wifi-densepose-pretrained and passed
model.safetensors / model-q4.bin / model.rvf.jsonl to --model hit a bare
"Progressive loader init failed: invalid magic at offset 0: expected
0x52564653, got 0x77455735" and were stuck — the server then silently fell
back to signal heuristics (which over-count, feeding "is it fake" reports).
The HF files are a different *format* and encoder architecture than the RVF
binary container the progressive loader expects, so they can't load directly.
Now the load-failure path detects the common cases (safetensors header,
JSONL manifest, quantized .bin blob) and emits a plain explanation naming the
format, what --model actually expects (RVF `RVFS` container from
wifi-densepose-train), and that it's continuing with heuristics — with a
pointer to #894.
Pure, testable `diagnose_model_load_error()` + 4 unit tests (run under the
default `--no-default-features` CI). Full crate unit suite: 429 + 114 passed,
0 failed.
The MQTT bridge fanned out one Home-Assistant device per node (#898) but
applied the *room-level aggregate* classification to every node — so in a
multi-node setup a node in an empty corner inherited another node's
"present", and `motion_level: "absent"` was mis-mapped to full motion
(the aggregate match fell through `Some(_) => 1.0`).
Each node in the sensing broadcast's `nodes` array already carries its own
`classification` (`motion_level`/`presence`/`confidence`, see
PerNodeFeatureInfo) and RSSI. Now each per-node snapshot reads that node's
own classification, deferring to the room aggregate only for fields a node
omits. Vitals (breathing/heart rate) and person count stay room-level.
Extracted the JSON→VitalsSnapshot mapping into a pure, testable function
(`vitals_snapshots_from_sensing_json`) and added 4 unit tests covering
per-node divergence, partial-field fallback, the no-nodes aggregate path,
and the absent→zero-motion fix.
Supersedes #899, which targeted the right bug but read non-existent fields
(`node["motion_level"]` / `node["status"]` instead of the nested
`node["classification"]` + `stale`).
Verified: builds with `--features mqtt`; new tests pass; full crate unit
suite 432 + 114 passed, 0 failed.
Since #915 the perf job gates only on test_frame_budget.py, which drives
the CSIProcessor pipeline in-process and makes no HTTP calls. The
"Start application" step (uvicorn + `sleep 10`) was therefore dead weight:
it existed only for the now-excluded api_throughput/inference_speed tests,
wasted ~10-15 s per main-push run, and dumped ~50 misleading
"router requires hardware setup" ERROR lines into every CI log for a
server no test touched. MOCK_POSE_DATA is server-only, unused here.
Removed the step and the vestigial env. The gated test is unchanged and
passes (verified locally, 3/3).
The v1 "100% presence accuracy" headline was already retracted in the
README / user-guide intro / proof-of-capabilities — but 6 secondary
spots still flatly claimed "100% accuracy, never false alarms", which
made proof-of-capabilities.md's "replaced everywhere" assertion untrue.
Completed the retraction in-place with the honest label-free metric
(82.3% held-out temporal-triplet; v1 was a single-class recording where
a constant "yes" scores ~99.98%):
- docs/readme-details.md — 2 benchmark tables + the pre-trained-model row
- docs/user-guide.md — capability table, model-file comment, applications list
- CHANGELOG.md — annotated the historical entry in-place (kept as public
record per built-in-public ethos, not rewritten)
Verified: no remaining flat "100% presence/accuracy" claim lacks a
retraction marker; proof-of-capabilities.md "replaced everywhere" is now
accurate.
After #914 fixed collection, the perf job actually ran the suite and
exposed that test_api_throughput.py / test_inference_speed.py are TDD
red-phase stubs (every test suffixed `_should_fail_initially`) that time
a *mock that sleeps* — not a real perf signal. They carry machine-
dependent wall-clock asserts (actual_rps >= 40, batch_time < individual_time)
that are inherently flaky on shared CI runners, plus a cross-class
fixture-scope bug (`fixture 'standard_model' not found`). Result: 3 failed,
10 errored — by design, not a regression.
Forcing those green would manufacture a false signal. Instead, gate only
on test_frame_budget.py, which times the *real* CSIProcessor pipeline
against the ADR 50 ms per-frame budget (single-frame, p95/100-frames,
+Doppler) — a genuine regression guard. Verified locally: 3 passed.
The stub files remain in-repo for local TDD; they re-enter CI when their
features are implemented and the mock-timing asserts are made deterministic.
The Performance Tests job collected 26 items then aborted with
`ModuleNotFoundError: No module named 'src'` on test_frame_budget.py,
which does `from src.core.csi_processor import CSIProcessor`. The bare
`pytest` console script does not put the cwd (archive/v1) on sys.path;
`python -m pytest` does. pytest aborts the whole session on a collection
error, so this one import masked the entire (otherwise mock-based,
self-contained) perf suite.
Verified locally: bare-script path reproduces the exact error; `-m`
resolves it and test_frame_budget.py passes 3/3. The other two files
(test_api_throughput.py mock server, test_inference_speed.py MockPoseModel
+psutil) are fully self-contained — no test hits the running server.
Closes the last red job in the v1-API CI chain (#910/#911/#913).
Two more latent v1-API CI bugs surfaced once #910/#911 let the jobs reach
their later steps:
- API Documentation: openapi generation now succeeds (psutil fix), but the
gh-pages deploy failed with HTTP 403 — the job had no `permissions` block
and GITHUB_TOKEN is read-only by default. Add `permissions: contents:
write`, and make the deploy `continue-on-error` (the openapi generation is
the real validation; Pages may be disabled).
- Performance Tests: ran `locust -f tests/performance/locustfile.py`, but
there is no locustfile — the suite is pytest (test_api_throughput.py,
test_frame_budget.py, test_inference_speed.py). Run pytest instead, with
working-directory: archive/v1 and MOCK_POSE_DATA=true.
ci.yml validated as well-formed YAML.
The API Documentation job (and any env without locust) failed with
`ModuleNotFoundError: No module named 'psutil'` when importing the app:
psutil is imported by src/api/routers/health.py, services/metrics.py,
commands/status.py, and tasks/monitoring.py, but was never declared as a
dependency — it only happened to be present where locust (Performance
Tests) pulled it in transitively. Declare it explicitly (psutil>=5.9.0).
Verified locally: `from src.api.main import app; app.openapi()` (the exact
docs-job operation) now succeeds.
After the DensePoseHead startup fix (#910), the v1 API starts, but the
Performance Tests load-hit the pose endpoints which error "requires real
CSI data" (no hardware in CI, mock_pose_data defaults False), and the
API-docs job imports the app the same way. Set MOCK_POSE_DATA=true on both
jobs so they exercise the mock path. Verified: the env var maps to
settings.mock_pose_data=True (pydantic, no env_prefix).
(Note: Performance Tests is continue-on-error so this is cleanup, not a
run-blocker; the run-level red on main has been transient Docker Hub pull
timeouts on Tests/docker-build, which are infra flakes that pass on re-run.)
The "Continuous Integration" workflow (Performance Tests + API
Documentation jobs) has failed on every main commit since the API start
path was exercised: pose_service._initialize_models() called
`DensePoseHead()` with no args, but DensePoseHead.__init__ requires a
config dict → "TypeError: DensePoseHead.__init__() missing 1 required
positional argument: 'config'" → uvicorn "Application startup failed".
Pass a config: input_channels=256 (matches the modality translator's
output), num_body_parts=24 (DensePose standard), num_uv_coordinates=2.
Both call sites (with/without pose_model_path) fixed.
Verified locally: DensePoseHead(config) + ModalityTranslationNetwork(config)
both construct + eval, clearing the startup TypeError.
The pre-built binaries in release_bins/ were v0.6.6 (May 21) and shipped
the MGMT-only promiscuous filter, so display-less boards flashed from them
got yield=0pps (#893/#866/#897 — the root cause of the "can't reproduce /
it's fake" reports). Rebuilt every flashable variant from main (which has
the #893 display-gated DATA-frame fix) and refreshed the binaries:
- top-level ESP32-S3 8MB (sdkconfig.defaults) — esp32-csi-node.bin +
bootloader (partition-table/ota_data unchanged — code-only fix)
- esp32-csi-node-4mb.bin (ESP32-S3 4MB, sdkconfig.defaults.4mb)
- c6-adr110/ (ESP32-C6, sdkconfig.defaults.esp32c6) — the exact firmware
hardware-verified on COM6 (CSI yield 0→27 pps, presence/motion alive,
no #396 crash)
- s3-adr110/ (same production S3 8MB config)
Left untouched: s3-fair-adr110/ (a non-production size-comparison build,
features stripped — not a board anyone flashes for sensing).
version.txt → 0.6.7; SHA256SUMS regenerated for the changed variant dirs.
Display boards keep MGMT-only (preserves the #396 crash protection);
display-less boards now capture DATA frames and stream CSI.
Co-Authored-By: claude-flow <ruv@ruv.net>
field_bridge::occupancy_or_fallback returned FieldModel::estimate_occupancy
unbounded (internal ceiling 10), while the perturbation fallback below it
and score_to_person_count both cap at 3 ("1-3 for single ESP32"). On noisy
or under-calibrated CSI the eigenvalue count inflated → "10 persons when 1
present" (#894, seen when --model fails to load → heuristic mode). Bound the
eigenvalue path to a shared MAX_SINGLE_LINK_OCCUPANCY const (3) so every
single-link estimator agrees. Genuine higher counts come from the
multistatic fusion path. Build clean, field_bridge tests pass.
After the per-node discovery change, discovery configs are published the
first time a snapshot for a node_id arrives (not eagerly at startup). The
two discovery integration tests (discovery_topics_appear_on_broker,
privacy_mode_suppresses_biometric_discovery) spawned the publisher with an
empty broadcast channel and never sent a snapshot, so they collected []
and failed ("missing presence discovery topic in []").
Drive snapshots for the test node_id throughout the capture window (same
pattern as state_messages_published_on_snapshot_broadcast) so the per-node
device's discovery lands. Verified against a local mosquitto: 3 passed.
The pre-built binaries set a MGMT-only promiscuous filter
(WIFI_PROMIS_FILTER_MASK_MGMT) as the #396 workaround — DATA-frame
interrupt load races the QSPI display's SPI traffic against the SPI-flash
cache and crashes Core 0 in wDev_ProcessFiq. But MGMT-only fires the CSI
callback only on sparse management frames, so on the common DISPLAY-LESS
boards (DevKitC-1, T7-S3, N8R8) CSI yield collapses to 0 pps under real
traffic (#521) — the node looks dead despite being on the network, which
is the root cause of most "can't reproduce / it's fake" reports (#804/#37).
A board with no AMOLED panel has no QSPI/SPI-flash contention, so it can
safely capture DATA frames. After the boot-time display probe runs:
- display present -> keep MGMT-only (preserve #396 crash protection)
- no display -> upgrade filter to MGMT|DATA (restore CSI yield)
Implementation (runtime-gated, no boot reorder):
- display_task.c: s_display_active flag + display_is_active() accessor,
set true only when the panel is detected and the display task starts.
- csi_collector.c: csi_collector_enable_data_capture() re-sets the
promiscuous filter to MGMT|DATA.
- main.c: after display_task_start(), if !display_is_active() (or display
support not compiled in), upgrade the filter.
Build-verified on BOTH targets: esp32c6 (headless path) and esp32s3
(display path, display_task.c compiled) — Project build complete, RC 0.
Needs on-hardware confirmation that yield recovers and no #396 crash.
After the #872 MQTT wiring, the JSON->VitalsSnapshot bridge hard-coded a
single node_id (the MQTT client id) and the publisher used one
OwnedDiscoveryBuilder, so every physical node collapsed into a single
Home-Assistant device (identifiers:["wifi_densepose_wifi-densepose-1"]),
contradicting the one-device-per-node docs.
- Bridge (main.rs): emit one VitalsSnapshot per node in the sensing
update's nodes[] (each carries its own node_id + RSSI; shared aggregate
presence/vitals), falling back to a single aggregate snapshot when
there is no per-node data (wifi/simulate sources).
- Publisher (publisher.rs): add OwnedDiscoveryBuilder::for_node(), and
publish discovery + availability lazily on first sight of each node_id,
routing state to per-node topics. Heartbeat/refresh/offline-LWT iterate
all known nodes. Result: N distinct HA devices, one per node.
3 new unit tests (distinct nodes -> distinct wifi_densepose_<node>
identifiers); full MQTT suite 71 passed, example builds.
verify.py's published hash is now f8e76f21 (doppler excluded). Document
that the proof reproduces bit-for-bit across Windows / two Linux hosts /
the Azure CI runner, that the peak-normalized Doppler is excluded due to
its cross-microarch argmax instability, and that a relative-tolerance
check against a committed reference vector backs the five stable features.
CI divergence profile was decisive: 6089/36800 elements (≈95% of doppler
values) diverged with O(1) magnitude (ref 0.15 vs CI 1.0), and ALL of it
was the doppler feature — the other 5 features reproduced within tolerance.
Root cause: csi_processor._extract_doppler_features peak-normalizes the
spectrum (`spectrum / max(spectrum)`). When the raw spectrum has near-tied
peaks, the argmax flips under cross-microarchitecture pocketfft/BLAS FP
reordering (Azure CI runner vs dev boxes), renormalizing the whole array —
an O(1) divergence no tolerance can absorb. This is a real *production*
reproducibility bug (models consuming doppler_shift get different values on
different CPUs); it's flagged for a separate, impact-analyzed source fix.
Scoped proof fix: exclude doppler_shift from both the SHA-256 and the
tolerance vector. The remaining five features — amplitude mean/variance,
phase difference, correlation matrix, and the FFT-based PSD (30,400
elements) — reproduce deterministically and provide the proof. Regenerated
hash + reference. Local: VERDICT PASS.
Add a divergence report (count + fraction outside tolerance, per-feature
breakdown, worst offenders) so we can tell a few branch-flip elements
from a pervasive regression. The CI tolerance gate failed with max|d|=0.85
/ maxrel=345 — far beyond FP rounding — so we need to see WHICH feature
elements diverge structurally on the Azure runner.
Definitive root cause of the failing determinism gate: the SHA-256 of
fixed-decimal-rounded features is bit-exact only WITHIN one CPU
microarchitecture. Windows and a second Linux box (ruvultra, identical
numpy 2.4.2/scipy 1.17.1) produce the same hash at every precision
(ca58956c), but the GitHub Azure runner diverges at EVERY precision
including 2 decimals (667eb054) — because pocketfft/BLAS reorders FP
reductions per-microarch and the ~1e-6 *relative* drift lands on
large-magnitude PSD bins as an absolute difference no fixed-decimal grid
can absorb. So no quantization can fix it; the primitive was wrong.
Fix: keep the bit-exact SHA-256 as the strong same-platform proof, and
add a relative-tolerance fallback (np.allclose, rtol=1e-4/atol=1e-6)
against a committed reference feature vector (expected_features_reference.npz,
36,800 float64 values). A run PASSES on either; tolerances sit ~100x over
the observed microarch drift and ~10x under any signal-meaningful change,
so real regressions still fail. Verified locally: bit-exact MATCH -> PASS,
and a corrupted hash falls through to TOLERANCE MATCH -> PASS. CI (Azure,
different hash) now passes via the tolerance path. Removes the temporary
sweep diagnostic.
Co-Authored-By: claude-flow <ruv@ruv.net>
verify.py's HASH_QUANTIZATION_DECIMALS is now overridable via
PROOF_HASH_DECIMALS. Finding: the determinism divergence is NOT
Windows-vs-Linux — Windows and a second Linux box (ruvultra, same
numpy/scipy) produce identical hashes at every precision, including
ca58956c at 6 decimals. Only the GitHub Azure CI runner diverges
(667eb054), i.e. a CPU-microarchitecture pocketfft/BLAS reordering
(the #560 Skylake-vs-Cascade-Lake class).
Temporary diagnostic sweep step prints the CI runner's hash at decimals
6..2 so we can pick the coarsest precision that collapses the
microarch divergence to the common hash. Both the sweep step and the
PROOF_HASH_DECIMALS plumbing are removed/finalized in the follow-up.
Co-Authored-By: claude-flow <ruv@ruv.net>
The determinism gate is path-filtered, but requirements-lock.txt (which
pins the numpy/scipy versions that *produce* the proof hash) was not in
the filter — so a dependency bump could silently drift the hash without
re-running the gate. That's how the 1.26.4 pin diverged from the
published ca58956c hash unnoticed. Add requirements-lock.txt to both the
push and pull_request path filters so this PR (and any future lock
change) actually re-runs verify.py.
Co-Authored-By: claude-flow <ruv@ruv.net>
Verify Pipeline Determinism has been failing (on main too) because
requirements-lock.txt pinned numpy 1.26.4 / scipy 1.14.1 (→ hash
667eb054…) while the committed/published expected_features.sha256
(ca58956c…) was generated with modern numpy 2.x — the version a fresh
`pip install numpy`, the maintainers, and the proof-of-capabilities.md
skeptic path all use today.
Bump the lock to numpy 2.4.2 / scipy 1.17.1 so the determinism gate
matches its own published proof. verify.py prints VERDICT: PASS with
these versions locally. The lock is consumed *only* by
verify-pipeline.yml (the Tests jobs use requirements.txt), so this is
scoped to the determinism gate.
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-31 11:33:42 -04:00
904 changed files with 84348 additions and 43731 deletions
| `vendor/rvcsi` (submodule) | **rvCSI** — edge RF sensing runtime (ADR-095/096): 9 crates (`rvcsi-core`/`-dsp`/`-events`/`-adapter-file`/`-adapter-nexmon`/`-ruvector`/`-runtime`/`-node`/`-cli`). Lives in its own repo ([github.com/ruvnet/rvcsi](https://github.com/ruvnet/rvcsi)), vendored here under `vendor/rvcsi`, published to crates.io as `rvcsi-* 0.3.x` and to npm as `@ruv/rvcsi`. Not a `v2/` workspace member — depend on the published crates (or the submodule's `crates/rvcsi-*` paths). Normalized `CsiFrame`/`CsiWindow`/`CsiEvent` schema, validate-before-FFI, reusable DSP, typed confidence-scored events, the napi-c Nexmon shim (real nexmon_csi `.pcap` from a Raspberry Pi 5 / 4 / 3B+ — BCM43455c0), the napi-rs SDK, the `rvcsi` CLI, a Claude Code plugin. |
| `vendor/rufield` (submodule) | **RuField MFS** — the open spec for camera-free multimodal field sensing (ADR-260). A common `FieldEvent`/`FieldTensor`/`FusionGraph`/`PrivacyClass`/`ProvenanceReceipt` model *above* WiFi CSI/CIR/BFLD, UWB, BLE Channel Sounding, mmWave radar, ultrasound, subsonic, infrared, and quantum sensors. Lives in its own repo ([github.com/ruvnet/rufield](https://github.com/ruvnet/rufield)), vendored here under `vendor/rufield`. Not a `v2/` workspace member. v0.1 reference stack = 7 crates (`rufield-core`/`-provenance`/`-privacy`/`-adapters`/`-fusion`/`-bench`/`-viewer`), 72 tests/0 failed; `rufield-viewer` is an Axum + vanilla-JS read-only dashboard (`cargo run -p rufield-viewer`) completing ADR-260 §27.9. The WiFi-CSI modality is now **real-replay-backed** via `CsiReplayAdapter` (ingests real captured `.csi.jsonl` → fused presence/breathing inferences; replay-from-file, unlabeled CSI-variance proxy, not validated accuracy); mmWave/thermal + all synthetic-bench F1 numbers remain **SYNTHETIC** (no live hardware — live streaming + labeled accuracy are roadmap). |
| `wifi-densepose-rufield` | ADR-262 P1 **anti-corruption bridge** — converts RuView WiFi-CSI sensing output (`SensingSnapshot` mirroring `SensingUpdate` + `TrustedOutput`, owned primitives, no dep on `wifi-densepose-sensing-server`) into **signed RuField `FieldEvent`s** (`Modality::WifiCsi`, real `timestamp_ns`, sha256 + ed25519 provenance, `synthetic=false`). The single coupling point between RuView and the standalone RuField MFS spec (§5.4); path-deps the `vendor/rufield` submodule crates (`rufield-core`/`-provenance`/`-privacy`/`-fusion`). **Critical §3.3 privacy mapping** (`map_privacy`): maps RuView class → RuField P0–P5 by **information content, never byte value**, fail-closed (`Derived → P4/P5`, never P1; `demoted` floors to ≥ P2). 15 tests / 0 failed (round-trip / `is_fusable` / fusion-ingest / privacy-safety / determinism). P1 plumbing — not wired into the live server (P3), no accuracy claim. |
**⚠️ Compact boards (SuperMini, ESP32-S3-Zero, other coin-sized clones) run hot:** the firmware keeps the WiFi radio on continuously (`WIFI_PS_NONE`) and runs a full DSP pipeline (`edge_tier=2`), which is sustained high current draw. Full-size dev boards handle this fine; coin-sized clones with minimal PCB copper and budget regulators can run uncomfortably hot and, per at least one field report, have failed to power on again after a hot session. Give them airflow and check by touch during the first few minutes. See `firmware/esp32-csi-node/README.md` for details.
### Build & Test Commands (this repo)
```bash
# Rust — full workspace tests (1,031+ tests, ~2 min)
## Anti-slop assertion tests (each fails on the pre-fix code)
| Claim | Grade | Test (run via `cargo test -p <crate> <name>`) |
|---|---|---|
| Fusion crafted-input DoS panics are closed (ADR-156 §2.2) | **MEASURED** | `wifi-densepose-ruvector :: triangulation_out_of_range_index_returns_none_no_panic` |
| **The "Soul Signature" identity claim, honestly bounded:** on WiFi-only cardiac+respiratory channels two people are **not separable** (gap ≈ 0.0005) | **MEASURED** | `wifi-densepose-bfld :: cardiac_alone_cannot_separate_identity_matches_audit` |
| OccWorld `predict()` is real (input-dependent), not random noise | **MEASURED** | `wifi-densepose-occworld-candle :: predict_is_deterministic_for_same_input` |
| Pose runtime emits frames under its own default config (ADR-159 A1) | **MEASURED** | `cog-pose-estimation :: default_config_emits_frames_with_real_model` |
| 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)
| Capability | Status |
|---|---|
| **Named person-identity from WiFi** | **NOT achieved, and measured why.** The §3.6 matcher is real, but identity does not lock on WiFi-only channels (gap 0.0005). DATA-GATED on a real enrollment feeding the AETHER/body-resonance channel — never done. No named-identity claim is made. |
| WiFlow-STD ~96% PCK@20 | **CLAIMED-reproduced** on our RTX 5080 (`benchmarks/wiflow-std/RESULTS.md`); HARDWARE-GATED for you (needs an NVIDIA GPU + the MM-Fi dataset). The upstream *shipped checkpoint* was **REFUTED** (0.08% PCK) — we publish that. |
| OccWorld trajectory accuracy | DATA-GATED on a trained checkpoint; `predict()` carries `weights_trained=false` until one is loaded — never silently faked. |
| Edge-skill detection accuracy (seizure, weapon, affect, …) | UNVALIDATED — every such module is now disclaimer-gated as experimental/research; the DSP is real, the accuracy is not claimed. |
| 802.11bf-2025 OTA conformance | No commodity silicon ships a conformant interface as of 2026; ours is a simulation-tested forward-compat protocol model, not a certified implementation. |
## Provenance
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
> | **ESP32 Mesh** | 3-6× ESP32-S3 + WiFi router | ~$54 | Yes | Same capabilities as above without the persistent-memory features |
> | **ESP32-C6 research node** ([ADR-110](docs/adr/ADR-110-esp32-c6-firmware-extension.md), [witness](docs/WITNESS-LOG-110.md), [reviewer guide](docs/ADR-110-REVIEW-GUIDE.md), [firmware v0.7.0](https://github.com/ruvnet/RuView/releases/tag/v0.7.0-esp32)) | ESP32-C6-DevKit ($6–10) | ~$10 | Yes (Wi-Fi 6 capable) | Same CSI pipeline as S3 with the dual-target firmware. **Firmware-side ADR-110 substrate now closed** (v0.7.0): ESP-NOW cross-board mesh quantified at **99.56 % match / 104 µs smoothed offset stdev / 3.95× EMA suppression** over a 5-min two-board soak (witness §A0.10), 32-byte UDP sync packet with operator-tunable cadence (§A0.12), ADR-018 byte 19 bit 4 wire-fix sourced from the working ESP-NOW path (§A0.13). Wire format ready for HE-LTF PPDU tagging in ADR-018 bytes 18-19 (firmware encoder + Rust + Python decoders verified end-to-end across 23 unit tests). LP-core motion-gate RISC-V program and Wi-Fi 6 soft-AP with TWT Responder both ship as opt-in code paths (default off). **Hardware-gated for measurement**: HE-LTF live subcarrier capture needs an 11ax AP (IDF v5.4 doesn't expose AP-side HE config — §A0.6); ~5 µA LP-core hibernation needs an INA meter to capture; 802.15.4 raw RX is broken in IDF v5.4 (workaround: ESP-NOW transport, shipped + measured). See witness log for the empirical / claimed split. |
> | **Research NIC** | Intel 5300 / Atheros AR9580 | ~$50-100 | Yes | Full CSI with 3x3 MIMO |
> | **Qualcomm CSI beta** ([ADR-268](docs/adr/ADR-268-qualcomm-atheros-csi-platform.md)) | QCA9300 now; QCN9074/QCN9274 experimental | ~$30-200 | Simulator now; hardware adapter gated | Rust `QCS1` codec, deterministic replay, UDP/API integration; modern ath11k/ath12k profiles do not claim public CSI export |
> | **Vendor provider beta** ([ADR-270](docs/adr/ADR-270-vendor-rf-sensing-integration-program.md)) | Origin, Plume, Mist, NETGEAR, Electric Imp, RF Solutions, Luma, Nest, Linksys, Wifigarden | Varies | Capability-dependent | Bounded Rust adapters and deterministic fixtures; telemetry/network-only/unsupported states cannot masquerade as CSI |
> | **Any WiFi** | Windows, macOS, or Linux laptop | $0 | No | RSSI-only: coarse presence and motion (see [tutorial #36](https://github.com/ruvnet/RuView/issues/36)) |
>
> No hardware? Verify the signal processing pipeline with the deterministic reference signal: `python archive/v1/data/proof/verify.py`
@@ -194,7 +196,7 @@ The separate **17-keypoint pose-estimation model** is now published at [`ruvnet/
| **Efficiency frontier** | [`docs/benchmarks/wifi-pose-efficiency-frontier.md`](docs/benchmarks/wifi-pose-efficiency-frontier.md) | SOTA-beating WiFi pose in a 20 KB int4 edge model |
@@ -501,7 +503,7 @@ Every WiFi signal that passes through a room creates a unique fingerprint of tha
**What it does in plain terms:**
- Turns any WiFi signal into a 128-number "fingerprint" that uniquely describes what's happening in a room
- Learns entirely on its own from raw WiFi data — no cameras, no labeling, no human supervision needed
- Recognizes rooms, detects intruders, identifies people, and classifies activities using only WiFi
- Recognizes rooms, detects intruders, and classifies activities using only WiFi (named person-identity is an experimental, data-gated research capability — see below, not a shipped feature)
- Runs on an $8 ESP32 chip (the entire model fits in 55 KB of memory)
- Produces both body pose tracking AND environment fingerprints in a single computation
@@ -512,7 +514,7 @@ Every WiFi signal that passes through a room creates a unique fingerprint of tha
| **Self-supervised learning** | The model watches WiFi signals and teaches itself what "similar" and "different" look like, without any human-labeled data | Deploy anywhere — just plug in a WiFi sensor and wait 10 minutes |
| **Room identification** | Each room produces a distinct WiFi fingerprint pattern | Know which room someone is in without GPS or beacons |
| **Anomaly detection** | An unexpected person or event creates a fingerprint that doesn't match anything seen before | Automatic intrusion and fall detection as a free byproduct |
| **Person re-identification**| Each person disturbs WiFi in a slightly different way, creating a personal signature | Track individuals across sessions without cameras |
| **Person re-identification***(experimental, research)* | A real per-channel similarity matcher (Soul Signature §3.6, `wifi-densepose-bfld`); **measured** result: on WiFi-only cardiac+respiratory channels alone two people are *not* separable (gap ~0.0005) | Honest research capability — **named identity is not claimed** and is data-gated on enrollment with the decisive AETHER/body-resonance channel. See [#1021](https://github.com/ruvnet/RuView/issues/1021) |
| **Environment adaptation** | MicroLoRA adapters (1,792 parameters per room) fine-tune the model for each new space | Adapts to a new room with minimal data — 93% less than retraining from scratch |
| **Memory preservation** | EWC++ regularization remembers what was learned during pretraining | Switching to a new task doesn't erase prior knowledge |
| **Hard-negative mining** | Training focuses on the most confusing examples to learn faster | Better accuracy with the same amount of training data |
@@ -601,6 +603,8 @@ claude --plugin-dir ./plugins/ruview
Verify the plugin structure: `bash plugins/ruview/scripts/smoke.sh`. Full details: [`plugins/ruview/README.md`](plugins/ruview/README.md).
**Portable harness — `npx @ruvnet/ruview`:** a lighter, host-portable companion to the in-repo plugin, minted via [MetaHarness](https://www.npmjs.com/package/metaharness) and hardened per [ADR-182](docs/adr/ADR-182-npx-ruview-harness-via-metaharness.md). It runs **without cloning this repo** and on more hosts (Claude Code, Codex, Copilot, opencode, …), exposing the RuView operator tools (`onboard`, `verify`, `node_monitor`, `calibrate`, `node_flash`) over an MCP server — plus the project's **MEASURED-vs-CLAIMED honesty guardrail enforced in code** (`ruview.claim_check` flags untagged or retracted-"100%" accuracy claims). v0.1: the onboarding/verify/claim-check paths are tested (17/17, `verify.py` → PASS); the hardware tools are fail-closed wrappers. Try `npx @ruvnet/ruview` to onboard, or `npx @ruvnet/ruview claim-check --text "…"`. Source: [`harness/ruview/`](harness/ruview/README.md).
---
## 📖 Documentation
@@ -610,11 +614,12 @@ Verify the plugin structure: `bash plugins/ruview/scripts/smoke.sh`. Full detail
| [User Guide](docs/user-guide.md) | Step-by-step guide: installation, first run, API usage, hardware setup, training |
| [Build Guide](docs/build-guide.md) | Building from source (Rust and Python) |
| [**Home Assistant + Matter Integration**](docs/integrations/home-assistant.md) | **Works with Home Assistant** via MQTT auto-discovery + **Works with Matter** (Apple Home / Google Home / Alexa / SmartThings) — full entity catalog, 3 starter blueprints, Lovelace dashboards, privacy mode, threshold tuning ([ADR-115](docs/adr/ADR-115-home-assistant-integration.md)). |
| [**BFLD — Beamforming Feedback Layer for Detection**](v2/crates/wifi-densepose-bfld/README.md) | New privacy-gated WiFi sensing layer that measures + structurally prevents identity leakage from 802.11ac/ax Beamforming Feedback Information. Three type-enforced invariants (raw BFI never exits node, identity embedding is in-RAM-only, cross-site correlation cryptographically impossible via per-site BLAKE3 keyed hash + daily rotation). Ships full operator surface (`BfldPipeline`, `BfldPipelineHandle`, Soul Signature `SoulMatchOracle` integration), MQTT topic router + HA-DISCO + availability + LWT, 3 operator HA blueprints, two runnable examples, eclipse-mosquitto:2 CI service container. 327+ tests. [ADR-118](docs/adr/ADR-118-bfld-beamforming-feedback-layer-for-detection.md) umbrella + sub-ADRs [119](docs/adr/ADR-119-bfld-frame-format-and-wire-protocol.md)/[120](docs/adr/ADR-120-bfld-privacy-class-and-hash-rotation.md)/[121](docs/adr/ADR-121-bfld-identity-risk-scoring.md)/[122](docs/adr/ADR-122-bfld-ruview-ha-matter-exposure.md)/[123](docs/adr/ADR-123-bfld-capture-path-nexmon-and-esp32.md). Research dossier: [`docs/research/BFLD/`](docs/research/BFLD/) (11 files, 13,544 words). |
| [**BFLD — Beamforming Feedback Layer for Detection**](v2/crates/wifi-densepose-bfld/README.md) | New privacy-gated WiFi sensing layer that measures + structurally prevents identity leakage from 802.11ac/ax Beamforming Feedback Information. Three type-enforced invariants (raw BFI never exits node, identity embedding is in-RAM-only, cross-site correlation cryptographically impossible via per-site BLAKE3 keyed hash + daily rotation). Ships full operator surface (`BfldPipeline`, `BfldPipelineHandle`, the Soul Signature §3.6 per-channel matcher `EnrolledMatcher`/`SoulMatchOracle` — experimental; named identity is data-gated, **measured** as not-separable on WiFi-only channels alone), MQTT topic router + HA-DISCO + availability + LWT, 3 operator HA blueprints, two runnable examples, eclipse-mosquitto:2 CI service container. 327+ tests. [ADR-118](docs/adr/ADR-118-bfld-beamforming-feedback-layer-for-detection.md) umbrella + sub-ADRs [119](docs/adr/ADR-119-bfld-frame-format-and-wire-protocol.md)/[120](docs/adr/ADR-120-bfld-privacy-class-and-hash-rotation.md)/[121](docs/adr/ADR-121-bfld-identity-risk-scoring.md)/[122](docs/adr/ADR-122-bfld-ruview-ha-matter-exposure.md)/[123](docs/adr/ADR-123-bfld-capture-path-nexmon-and-esp32.md). Research dossier: [`docs/research/BFLD/`](docs/research/BFLD/) (11 files, 13,544 words). |
| [Semantic Primitives — Precision/Recall](docs/integrations/semantic-primitives-metrics.md) | Per-primitive F1 on the held-out paired-capture set: someone-sleeping, possible-distress, room-active, elderly-inactivity-anomaly, meeting, bathroom, fall-risk, bed-exit, no-movement, multi-room. |
| [Claude Code / Codex Plugin](plugins/ruview/README.md) | The `ruview` plugin + marketplace — skills, `/ruview-*` commands, agents, and the Codex prompt mirror |
| [Architecture Decisions](docs/adr/README.md) | 96 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
| [Portable harness — `npx @ruvnet/ruview`](harness/ruview/README.md) | MetaHarness-minted, host-portable RuView operator harness — `ruview.*` MCP tools + the MEASURED-vs-CLAIMED honesty guardrail enforced in code ([ADR-182](docs/adr/ADR-182-npx-ruview-harness-via-metaharness.md)). A lighter, multi-host companion to the in-repo plugin. |
| [Architecture Decisions](docs/adr/README.md) | 182 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
| [Domain Models](docs/ddd/README.md) | 8 DDD models (RuvSense, Signal Processing, Training Pipeline, Hardware Platform, Sensing Server, WiFi-Mat, CHCI, rvCSI) — bounded contexts, aggregates, domain events, and ubiquitous language |
| [rvCSI — edge RF sensing runtime](https://github.com/ruvnet/rvcsi) | Rust-first / TypeScript-accessible / hardware-abstracted CSI runtime: multi-source ingestion (incl. real nexmon_csi `.pcap` from a **Raspberry Pi 5** / Pi 4 / Pi 3B+ — CYW43455 / BCM43455c0) → validation → DSP → typed events → RuVector RF memory ([ADR-095](docs/adr/ADR-095-rvcsi-edge-rf-sensing-platform.md), [ADR-096](docs/adr/ADR-096-rvcsi-ffi-crate-layout.md), [domain model](docs/ddd/rvcsi-domain-model.md)). Now its own repo — [`ruvnet/rvcsi`](https://github.com/ruvnet/rvcsi) — vendored here under `vendor/rvcsi`; 9 `rvcsi-*` crates on crates.io, `@ruv/rvcsi` on npm, plus a Claude Code plugin. |
| [Desktop App](v2/crates/wifi-densepose-desktop/README.md) | **WIP** — Tauri v2 desktop app for node management, OTA updates, WASM deployment, and mesh visualization |
| **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 |
| `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 |
"note":"3 interleaved repetitions per variant, median ms/window; onnx_fp32 / onnx_int8_ort_dynamic are same-session references",
"onnx_fp32":{
"batch1_reps":[
4.5327999996516155,
2.535649999117595,
2.167549997466267
],
"batch64_reps":[
1.9354515624740998,
2.4948054687854437,
1.9334703125082342
],
"batch1_ms_per_window_median":2.535649999117595,
"batch64_ms_per_window_median":1.9354515624740998
},
"onnx_int8_ort_dynamic":{
"batch1_reps":[
5.698599999959697,
5.721350000385428,
4.805099997611251
],
"batch64_reps":[
4.096601562508795,
4.857628124995017,
4.583800000006022
],
"batch1_ms_per_window_median":5.698599999959697,
"batch64_ms_per_window_median":4.583800000006022
},
"entropy_all":{
"batch1_reps":[
6.444149999879301,
5.038299999796436,
5.713200000172947
],
"batch64_reps":[
4.149468750028973,
3.437125000004926,
4.410960937491382
],
"batch1_ms_per_window_median":5.713200000172947,
"batch64_ms_per_window_median":4.149468750028973
},
"entropy_conv":{
"batch1_reps":[
4.874750000453787,
5.169099998965976,
5.236699998931726
],
"batch64_reps":[
3.010160156236452,
3.1175546875203963,
3.516850781238645
],
"batch1_ms_per_window_median":5.169099998965976,
"batch64_ms_per_window_median":3.1175546875203963
},
"percentile_all":{
"batch1_reps":[
5.184749999898486,
5.2898499998264015,
5.916899999647285
],
"batch64_reps":[
4.305105468745296,
4.460741406262514,
4.184502343747454
],
"batch1_ms_per_window_median":5.2898499998264015,
"batch64_ms_per_window_median":4.305105468745296
},
"percentile_conv":{
"batch1_reps":[
4.916449999655015,
7.150899999032845,
5.284949998895172
],
"batch64_reps":[
3.855813281262499,
4.688969531230214,
5.220103124997877
],
"batch1_ms_per_window_median":5.284949998895172,
"batch64_ms_per_window_median":4.688969531230214
},
"minmax_all":{
"batch1_reps":[
6.463300000177696,
7.149449998905766,
5.3209000016067876
],
"batch64_reps":[
3.9251343750095202,
4.033442187505898,
3.428199218745931
],
"batch1_ms_per_window_median":6.463300000177696,
"batch64_ms_per_window_median":3.9251343750095202
},
"minmax_conv":{
"batch1_reps":[
5.9961499991914025,
5.236549999608542,
4.854399998293957
],
"batch64_reps":[
4.368359375007458,
3.249617187492504,
3.0238906249735464
],
"batch1_ms_per_window_median":5.236549999608542,
"batch64_ms_per_window_median":3.249617187492504
}
},
"accuracy_subset":{
"description":"seed-42 file-level 70/15/15 test split, corrupted windows excluded, seed-42 random subset (same as quantize_bench/eval_ort_accuracy)",
"subset_size":10000
}
},
"tiny_variant":{
"env":{
"torch":"2.12.0+cpu",
"onnxruntime":"1.26.0",
"platform":"Windows-11-10.0.26200-SP0",
"num_threads":16,
"checkpoint":"results\\tiny_best.pth",
"checkpoint_size_bytes":340555,
"params":56290,
"variant_config":{
"tcn":[
68,
56,
44,
32
],
"conv":[
2,
4,
8,
16
],
"attn_groups":2,
"groups_mode":"depthwise",
"input_pw_groups":4
}
},
"export":{
"mode":"dynamic-batch",
"exporter":"torchscript",
"opset":17,
"file":"tiny_fp32_dynamic.onnx",
"size_bytes":295279,
"size_mb":0.295279,
"verified_batches":[
1,
2,
64
],
"note":"AdaptiveAvgPool2d((15,1)) replaced at export by an exact mean(-1) + constant averaging matmul (final_width 16 is not a multiple of 15, which the TorchScript exporter rejects); exactness proven by the parity check vs the original torch model"
},
"parity":{
"fixture":"results/parity_fixture.npz input (batch 2, seed 42); reference output recomputed with the tiny torch model",
"description":"seed-42 file-level 70/15/15 test split, corrupted windows excluded, seed-42 random subset (same as quantize_bench/eval_ort_accuracy/static_ptq_bench)",
@@ -57,7 +57,7 @@ This witness separates what was **empirically observed on real silicon today** f
| # | Claim | Why it's not verified |
|---|---|---|
| **B1** | "Wi-Fi 6 HE-LTF: 242 subcarriers per HE20 frame" | The only AP in range (`ruv.net`) is 11n-only. Every captured frame is 128 bytes = 64 subcarriers (HT-LTF, `ppdu_type=0`). No HE-SU/HE-MU/HE-TB observed. Even if an 11ax AP were available, **whether ESP-IDF v5.4's CSI callback exposes HE-LTF subcarriers via `wifi_csi_info_t.buf` is an open question** — the public API was designed for HT-LTF, and the driver may quietly downconvert. **Validate by capturing CSI against an 11ax AP and comparing `info->len` between HT and HE frames.** |
| **B1** | "Wi-Fi 6 HE-LTF: 242 subcarriers per HE20 frame" | The only AP in range (`ruv.net`) is 11n-only. Every captured frame is 128 bytes = 64 subcarriers (HT-LTF, `ppdu_type=0`). No HE-SU/HE-MU/HE-TB observed. Even if an 11ax AP were available, **whether ESP-IDF v5.4's CSI callback exposes HE-LTF subcarriers via `wifi_csi_info_t.buf` is an open question** — the public API was designed for HT-LTF, and the driver may quietly downconvert. **Validate by capturing CSI against an 11ax AP and comparing `info->len` between HT and HE frames.**<br><br>**RESOLVED WITH MEASUREMENT (2026-06-11, external — issue #1005, production deployment by @stuinfla):** the open question is answered in both directions. **IDF v5.4's driver blob downconverts** (148 B / 64-subcarrier HT frames, PPDU byte 0x00, on a confirmed-HE link); **IDF v5.5.2 delivers true HE-LTF** — 532 B frames = 256 bins (242 active HE20 tones), PPDU byte 0x01 (HE-SU), ~90% of frames, same board/AP/link. Setup: XIAO ESP32-C6 → hostapd on Intel AX210, 2.4 GHz ch 6, `ieee80211ax=1`. No firmware change required (`acquire_csi_su=1` was already set); the gate was purely the IDF driver version. Three C6 nodes ran this mode simultaneously with ADR-110 ESP-NOW sync. Requires the issue-#1005 version-guard fix in `c6_sync_espnow.c` to build on v5.5.x. |<br><br>**REPLICATED IN-HOUSE (2026-06-11):** same source + fix, fresh IDF v5.5.2 toolchain, original COM12 board (`20:6e:f1:17:00:84`), AP `ruv.net` (11ax 2.4 GHz): **84% of 1,525 captured frames at 532 B / PPDU 0x01 (HE-SU)**, HT minority 148 B / 0x00. Evidence grade: MEASURED (two independent rigs). |
| **B2** | "TWT-bounded deterministic CSI cadence (10 ms wake)" | No 11ax AP in range. The TWT setup *call* was exercised live and the graceful fallback path is now correct (A9), but the agreement itself was never accepted. **Validate by associating with an 11ax AP that has TWT Responder=1, then capturing the timestamped CSI cadence vs the wall clock.** |
| **B3** | "±100 µs cross-node alignment over 802.15.4" | 3 boards initialized their radios with correct EUIs (A4/A5), but **none stepped down from candidate-leader to follower** during repeated 35-second multi-board captures. <br><br>**Coex hypothesis REJECTED**: rebuilt + reflashed all 3 boards with `CONFIG_C6_TIMESYNC_CHANNEL=26` (2480 MHz, non-overlapping with WiFi ch 5 at 2432 MHz). Result identical: 3× candidate, 0× "stepping down". So 2.4 GHz radio coex was NOT the cause. <br><br>**Current leading hypothesis**: OpenThread (CONFIG_OPENTHREAD_ENABLED=y) owns the 802.15.4 radio when its stack is initialized — our weak-symbol overrides of `esp_ieee802154_receive_done` / `_transmit_done` may never be called because OpenThread registers strong handlers. Validation in progress: rebuilding with `CONFIG_OPENTHREAD_ENABLED=n` (raw 802.15.4 only, our beacon protocol is private — no need for the Thread stack). If leader election fires under raw-15.4-only, hypothesis confirmed. <br><br>If raw-only also fails, next move is to dump the actual PHY frame bytes via the IEEE 802.15.4 sniffer mode on a 4th board and diagnose at the frame level. |
| **B4** | "~5 µA hibernation for battery seed nodes" | No INA / Joulescope current measurement available on this bench. The shipped code uses `esp_deep_sleep_enable_gpio_wakeup` (ext1 path, ESP-IDF default ~10 µA), not a true LP-core polling program. The 5 µA number is the C6 datasheet figure for ULP-level hibernation, not a measured value. **Validate by hooking an INA219/INA226 between the dev board's 3V3 rail and the regulator output, then averaging current over a 60-second cycle with the LP-core armed.** |
Two robustness bugs were fixed in the on-device edge path (`firmware/esp32-csi-node/main/edge_processing.c`, the ADR-039 packet `0xC5110002`). These touch the *boolean/count emission logic*, not the underlying CSI signal-processing math, and do **not** constitute a validated-accuracy claim — true occupancy-count and presence accuracy vs labelled ground truth remain hardware/data-gated (COM9 ESP32-S3 + labelled capture).
- **#998`n_persons` over-count (reported 4 for one person).** `update_multi_person_vitals()` divided the top-K subcarriers into `top_k_count/2` groups and marked *every* group `active`, so one body's multipath always read the full `EDGE_MAX_PERSONS`. Added an energy gate (`EDGE_PERSON_MIN_ENERGY_RATIO`), spatial dedup (`EDGE_PERSON_MIN_SC_SEP`), and a persistence debounce (`EDGE_PERSON_PERSIST_FRAMES`) via two pure functions `count_distinct_persons()` / `person_count_debounce()`.
- **#996 presence flag flicker at ~50 cm.** Single-threshold compare on a noisy `presence_score` chattered at the boundary. Replaced with a Schmitt trigger + clear-debounce (`presence_flag_update()`, constants `EDGE_PRESENCE_HYST_RATIO` / `EDGE_PRESENCE_CLEAR_FRAMES`); `presence_score` is unchanged and still emitted for consumer-side thresholding.
Both are pinned by host-buildable C99 tests in `firmware/esp32-csi-node/test/test_vitals_count_presence.c` (`make run_vitals`). The exact thresholds are documented constants pending on-device calibration against ground truth.
A correctness/safety review of the Rust extraction crate found a real bug parallel to the firmware robustness class above. The 2nd-order resonator `bandpass_filter` in both `breathing.rs` and `heartrate.rs` latches each output `y[n]` into its filter state (`y1`/`y2`). A single non-finite amplitude residual from a corrupt CSI frame produced a NaN `output` that was written into the state; the existing `extract()``is_finite()` guard dropped that one sample from the history buffer **but never sanitized the poisoned filter state**, so every later output stayed NaN, was rejected too, and the sliding-window history never refilled — breathing **and** heart-rate extraction went silently dead (returning `None` forever) until `reset()`. On the alert path this is a safety-relevant denial of service (one bad frame stops vitals monitoring with no error surfaced).
Fix: when `bandpass_filter` computes a non-finite `output`, it resets the IIR state to default and returns `0.0`, so the resonator self-heals on the next clean frame (the `0.0` is still dropped by the caller's finite-check, so no spurious sample enters history). Same shape as the calibration NaN bug (ADR-154 §3) — the prior hardening guarded the *history boundary* but not the *filter-state boundary*. Pinned by `breathing::tests::nan_frame_does_not_permanently_poison_filter`, `breathing::tests::inf_mid_stream_does_not_freeze_history`, and `heartrate::tests::nan_frame_does_not_permanently_poison_filter` (all FAIL pre-fix, verified by reverting). The review also de-magicked the HR physiological plausibility band into named `HR_PLAUSIBLE_MIN_BPM`/`HR_PLAUSIBLE_MAX_BPM` consts (value-identical 40/180 BPM) and added a fabricated-vital negative (`pure_noise_is_never_reported_valid` — broadband noise never yields a clinically `Valid` HR; the extractor honestly returns low-confidence `Unreliable`). Clean dimensions confirmed with evidence: flat/silent input → `None`; pure noise → low-confidence `Unreliable`, never `Valid`; harmonic-rich breathing with no cardiac component → low-confidence, not a confident false HR; out-of-band BPM rejected by the plausibility clamp.
## References
- Ramsauer et al. (2020). "Hopfield Networks is All You Need." ICLR 2021. (ModernHopfield formulation)
| Realtek RTL8822CS | rtw88 | **Moderate** — driver is open-source, CSI hooks need adding | Yes (patched) | Medium |
| MediaTek MT7661 | mt76 | **Unknown** — MediaTek has released CSI tools for some chips | Yes | Medium-High |
| MediaTek MT7661 | mt76 | **Unverified** — no supported public CSI capture API was found in upstream `mt76` or public MediaTek SDK material | Yes | Research only |
- **Problem:** Trusts `X-Forwarded-For` without validation. Any client bypasses rate limits via header spoofing.
- **Fix:** Validate forwarded headers against trusted proxy list, or use connection IP directly.
- **Rust verification (2026-06-13):** The Rust sensing-server has **no XFF-trusting control to bypass** — there is no IP-based rate-limiter and no IP-allowlist, and neither security middleware reads a forwarded header. `bearer_auth.rs` authenticates on the token alone (`require_bearer` inspects only the `AUTHORIZATION` header); `host_validation.rs` decides on the `Host` header only. A repo-wide grep for `x-forwarded-for|forwarded|peer_addr|client_ip|real-ip` over `wifi-densepose-sensing-server` returns nothing. The only "rate limiter" is the MQTT *sample-rate* gate (`mqtt/state.rs`), a per-entity publish throttle with no IP/header input.
- **Resolution:** No code change needed (no vulnerable surface). Regression tests pin the immunity: `bearer_auth::tests::xff_header_never_affects_auth_decision` (spoofed XFF never flips a 401↔200 decision) and `host_validation::tests::forwarded_headers_never_bypass_host_allowlist` (spoofed `X-Forwarded-Host: localhost` never lets a foreign `Host: evil.com` past the allowlist). Residual: if an IP-based control is ever added, it must derive the peer from the socket (`ConnectInfo<SocketAddr>`) and only honor XFF from an explicit `--trusted-proxy` CIDR — captured as guidance in the test docstrings.
### 2. Exception Details Leaked in Responses (Security HIGH)
- **Problem:**Internal error/stack-trace detail serialized into client responses.
- **Rust finding (2026-06-13):** Six handlers in `wifi-densepose-sensing-server/src/main.rs` serialized the internal error `Display` into the JSON body: `edge_registry_endpoint` returned a panicked `spawn_blocking``JoinError` (`"task … panicked"`) in a `500` and the raw upstream error in a `503`; `delete_model`/`delete_recording`/`start_recording` returned `std::io::Error` strings (OS detail / path); `calibration_start`/`calibration_stop` returned the `FieldModel` error chain.
- **Fix:** New `src/error_response.rs` module — `internal_error` / `internal_error_json` / `upstream_unavailable` log the full detail **server-side only** (tagged with a correlation id) and return a generic body (`{"error":"internal_error","correlation_id":…}`) with no `panicked`, no file paths, no Debug chain. All six call-sites rewired. Pinned by `error_response::tests::internal_error_body_does_not_leak_detail` (leak-substring guard, verified to fail on the reverted old body) + 4 sibling tests.
### 3. WebSocket JWT in URL (Security HIGH, CWE-598)
### 3. WebSocket JWT in URL (Security HIGH, CWE-598) — **RESOLVED (verified absent on Rust boundary)**
- **Problem:** Tokens in query strings visible in logs/proxies/browser history.
- **Fix:** Use WebSocket subprotocol or first-message auth pattern.
- **Rust verification (2026-06-13):** The Rust sensing-server never reads a token from the URL. `require_bearer` (`bearer_auth.rs`) inspects only the `Authorization` header; the WebSocket handlers (`ws_sensing_handler`/`ws_introspection_handler`/`ws_pose_handler`) take a bare `WebSocketUpgrade` with no `Query` extractor; the single `Query` in the crate (`EdgeRegistryParams`) is a non-secret `refresh` flag.
- **Resolution:** No code change needed (no query-token path exists). Regression test `bearer_auth::tests::query_string_token_is_never_accepted` proves `?token=`/`?access_token=` in the URL never authenticates (stays `401`) while the same token in the header succeeds (`200`) — verified to fail if a query-token path is re-introduced.
@@ -19,7 +19,7 @@ The production CSI node firmware (`firmware/esp32-csi-node`) was built around th
| C6 capability | What it enables for sensing | Why we can't get it on S3 |
|---|---|---|
| **802.11ax (Wi-Fi 6) HE-LTF CSI** | 242 subcarriers per HE20 frame (vs 52 for HT-LTF), HE-MU/HE-TB PPDU types, OFDMA-aware channel sounding | S3 radio is HT-only (n) |
| **802.11ax (Wi-Fi 6) HE-LTF CSI** | 242 subcarriers per HE20 frame (vs 52 for HT-LTF), HE-MU/HE-TB PPDU types, OFDMA-aware channel sounding. **Hardware-confirmed 2026-06-11** (issue #1005, external production deployment): requires **ESP-IDF ≥ 5.5** — the v5.4 driver blob silently downconverts to 64-subcarrier HT even on a confirmed-HE link; v5.5.2 delivers 532 B frames = 256 bins (242 active tones), PPDU 0x01 (HE-SU). See WITNESS-LOG-110 §B1 (resolved). | S3 radio is HT-only (n) |
| **802.15.4 (Thread / Zigbee)** | Cross-node time-sync over a separate radio — frees Wi-Fi airtime for CSI, ±100 µs alignment possible without coordination traffic on the sensing channel | S3 has no 802.15.4 |
| **TWT (Target Wake Time)** | Sensor negotiates a deterministic wake slot with the AP; CSI cadence becomes scheduler-bounded instead of opportunistic | Requires 802.11ax — S3 can't speak it |
| **LP-core + hibernation (~5 µA)** | Always-on motion gate runs on a separate RISC-V LP core in deep sleep; HP core stays off until a real event | S3 ULP is FSM-only, ~10 µA floor |
@@ -190,6 +190,23 @@ This is the same Wasmtime host already used for integration plugins (ADR-128)
---
## 8a. Security review (beyond-SOTA sweep, post ADR-154–159)
A focused security review of `homecore-automation` (the execution/eval surface — triggers → conditions → actions, with templates) was run after the ADR-154–159 sweep, applying the same rigor that the sibling engine/bfld/calibration/vitals/geo reviews used. **Two real DoS findings, each pinned by a fails-on-old test; the condition-bypass, fail-closed-parsing, and action-authorization dimensions were probed and found clean.**
- **HC-SEC-01 (template-injection / unbounded-expansion DoS, HIGH) — FIXED.** A `template:` condition / `value_template` is user automation config, and was rendered with MiniJinja's defaults: **no instruction budget, no output cap**. A single condition such as `{% for i in range(5000) %}{% for j in range(5000) %}xxxx{% endfor %}{% endfor %}` rendered a **100 MB string over ~11 s on one render call** (measured) — a CPU/memory denial of service (the bfld-class "unbounded expansion"; MiniJinja's per-call `range()` 10k cap does **not** stop nested loops). **Fix:** enable MiniJinja's `fuel` feature and set a per-render budget (`set_fuel(Some(1_000_000))`) so a nested loop burns one unit per iteration — the attack now fails fast (~90 ms) with "engine ran out of fuel"; plus a 64 KiB source-length cap rejecting pathological sources before compilation. Legitimate HA templates (a few dozen instructions) are unaffected. Pinned by `nested_loop_template_is_bounded_not_unbounded_dos`, `single_huge_repeat_template_is_bounded`, `oversized_template_source_is_rejected` (all fail-on-old: unbounded render / no rejection), and `legitimate_template_still_renders_within_fuel` (no regression).
- **HC-SEC-02 (panic-on-config DoS, MEDIUM) — FIXED.**`Action::Delay { seconds }` and `Action::WaitForTrigger { timeout_seconds }` fed the user-supplied float straight into `Duration::from_secs_f64`, which **panics** on negative, NaN, infinite, or overflowing inputs — all reachable from a crafted (or typo'd) YAML (`delay: {seconds: -1}`, `.nan`, `.inf`, `1e308`). One hostile config aborts the spawned automation run task with a panic (measured: "cannot convert float seconds to Duration: value is negative"). **Fix:** a `safe_duration_from_secs` guard that saturates instead of panicking (NaN/±inf/negative → `Duration::ZERO`, matching HA's lenient "non-positive delay = no delay"; absurdly large → clamped to ~100 years). Pinned by `delay_negative_seconds_does_not_panic`, `delay_nan_seconds_does_not_panic`, `delay_infinite_seconds_does_not_panic`, `wait_for_trigger_negative_timeout_does_not_panic`, `safe_duration_saturates_hostile_values` (incl. overflow clamp).
**Dimensions confirmed clean (with evidence):**
- **Condition bypass / fail-closed eval** — a `Condition::Template` whose render errors evaluates to `false` (`condition.rs``Err(_) => false`), and a `Choose` branch condition that fails to deserialize is treated as **non-matching** (the branch is skipped), not silently passing (`action.rs``ChoiceBranch::matches``Err(_) => return false`). Both fail **closed** (do-not-run), confirmed by the existing `choose_*` tests and template-false-blocks-action behavioral test. No true-by-default-on-parse-error path found.
- **Re-entrancy / livelock (DoS)** — run-mode machinery is bounded and tested: `Single`/`IgnoreFirst` re-entrancy guard, `Restart` cancel-and-replace, `Queued` FIFO serialization, and `max: N` semaphore cap (ADR-162; `restart_mode_cancels_prior_run`, `queued_mode_runs_sequentially_not_concurrently`, `max_two_caps_concurrency_at_two`, `single_mode_does_not_double_fire_on_rapid_triggers`). A self-triggering automation does not livelock the engine — each fire is bounded by its run-mode.
- **Action authorization** — templates are read-only sandboxed (`states`/`state_attr`/`is_state`/`now` globals; no service-call or state-set global is exposed to template scope), so a template cannot escalate into an action. Service authorization itself is enforced at the `homecore` service-registry boundary (out of this crate's scope); no gap found in what the automation crate enforces.
- **Panic-on-config (parse)** — `serde_yaml`/`serde_json` deserialization returns structured `AutomationError` (no `unwrap`/`expect`/index reachable from a crafted config in the eval/exec path); the only remaining panic surface was the `from_secs_f64` path fixed as HC-SEC-02.
Validation: `cargo test -p homecore-automation --no-default-features` → 54 passed / 0 failed (+14 over baseline). Python deterministic proof unchanged (homecore-automation is off the signal-processing proof path).
HOMECORE (ADR-126 through ADR-134) is the native Rust + WASM + TypeScript port of Home Assistant running as the hub on the Cognitum v0 Appliance. As of P2, the state machine ([ADR-127](ADR-127-homecore-state-machine-rust.md)), API ([ADR-130](ADR-130-homecore-rest-websocket-api.md)), and COG runtime ([ADR-128](ADR-128-homecore-integration-plugin-system.md)) are in place. What is missing is a first-class dashboard UI that operators, integrators, and residents can use to manage the full two-tier hardware stack that HOMECORE coordinates.
### 1.1 The two-tier hardware model this UI must represent
This is the most important architectural constraint the UI must carry through every panel:
- **Cognitum SEED** — a Pi Zero 2 W-based edge node. It has its own RVF vector store (8-dim, content-addressed, with kNN queries), Ed25519 witness chain, SHA-256 ingest audit trail, onboard environmental sensors (BME280 temperature/humidity/pressure, PIR motion, reed switch, ADS1115 4-channel ADC, vibration), 13 drift detectors, an MCP proxy (114 tools, JSON-RPC 2.0, default-deny policy), 98 HTTPS API endpoints, and epoch-based swarm sync for multi-SEED deployments. SEEDs sit close to the ESP32 sensing nodes and receive feature vectors from them at 1 Hz. Multiple SEEDs can form a peer mesh. **This is the sensing and memory tier.**
- **Cognitum v0 Appliance** — a Pi 5 + Hailo-10H hub, running at `:9000`. It hosts the COG runtime (`/var/lib/cognitum/apps/`), the HOMECORE state machine and event bus, the calibration service, `ruview-mcp-brain:9876`, `cognitum-rvf-agent:9004`, `ruvector-hailo-worker:50051`, and acts as the fleet coordinator for multi-room correlation and federated training. The Appliance is where HOMECORE runs, and it is what the dashboard user is sitting in front of. **This is the computation and orchestration tier.**
SEEDs are **subordinate nodes that the Appliance supervises** — they are not peers. The UI navigation hierarchy must reflect this: the Appliance is the root, SEEDs are children, ESP32 nodes are leaves.
### 1.2 What the UI is not
HOMECORE-UI is **not** a re-skin of the existing Cognitum Cog Store. It is a full operational dashboard that **extends** the Cognitum platform's shell — the Cog Store, API Explorer, and Guide already exist and must remain intact, with the HOMECORE dashboard added as a first-class navigation section alongside them.
---
## 2. Decision
Build HOMECORE-UI as a **complete** TypeScript + Rust→WASM frontend (per this ADR's §3 and the HOMECORE-127…134 family) that:
1. Lives at `http://cognitum-v0:9000/homecore` (or as a dedicated nav item in the Cognitum Appliance shell).
2. Is visually and stylistically seamless with the existing Cognitum platform — same dark theme, same design tokens, same component patterns as `https://seed.cognitum.one/store`.
3. Drives the HOMECORE REST + WebSocket API ([ADR-130](ADR-130-homecore-rest-websocket-api.md)) and the calibration HTTP API ([ADR-151](ADR-151-room-calibration-specialist-training.md)) for all data.
4. Updates in real-time via the homecore `subscribe_events` WebSocket channel. **The UI must never poll for entity state.**
**This is a decision to deliver the complete operational dashboard — every panel in §4.1 through §4.10, every navigation section in §5, fully wired to live data — not a design-system scaffold or a partial first cut.** A static layout shell with placeholder data is explicitly **out of scope as a deliverable**: the design system (§3) is a means to the complete UI, not an end in itself. The acceptance bar for this ADR is that an operator can drive the full two-tier stack — fleet, entities, rooms, COGs, calibration, events, audit, and settings — from the dashboard, against real APIs, with no panel left as a stub.
### 2.1 `homecore-server` is the single backend-for-frontend (BFF) gateway
The data the dashboard needs is spread across **three backend tiers that are not one process**: (a) `homecore-api` (`/api/*` REST + `/api/websocket`, mounted in `homecore-server`); (b) the **calibration API** (`/api/v1/*`, served by a *separate* binary — `wifi-densepose calibrate-serve` / `wifi-densepose-sensing-server`); and (c) the **SEED device tier + appliance daemons** (RVF vector store, witness chain, onboard sensors, reflex rules, COG supervisor, federation), which are physically separate HTTPS services on the SEED nodes and the appliance.
The browser must talk to **exactly one origin.** Therefore `homecore-server` is promoted to the **single BFF / API gateway** for HOMECORE-UI: it serves the static assets at `/homecore`, serves `homecore-api` at `/api/*`, and **adds a new `/api/homecore/*` namespace** that proxies and aggregates the calibration API and the SEED/appliance tiers server-side. The UI only ever issues same-origin requests; cross-service auth (SEED bearer tokens, calibration tokens) is held by the gateway and **never exposed to the browser**. This collapses the CORS/multi-port problem and gives one place to enforce the long-lived-access-token auth (§4.10).
### 2.2 No mock data in production
The in-browser mock layer that the first UI cut shipped behind DEMO banners (§7.1, prior revision) is **demoted to a dev-only fixture** gated behind an explicit `?demo=1` / `HOMECORE_UI_DEMO=1` flag. The production build wires **every** panel to a real gateway endpoint. The full endpoint contract and the backend work each panel needs are specified in **§11**; the staged path to get there is **§12**. A panel may show an empty/typed-error state when its upstream is down, but it must never silently render fabricated data.
---
## 3. Design system — Cognitum platform conventions
The implementor **must study `https://seed.cognitum.one/store` as the definitive design reference before writing a single line of CSS.** The existing platform's design tokens, extracted from production, are:
- **Nav strip**: `background: var(--bg2)`, text items in `--t2`, active item highlighted in `--cyan` with a bottom underline.
- **Featured card gradient borders**: top-edge linear gradient from `var(--cyan)` to `var(--purple)` — replicate for HOMECORE section headers.
- **Live metric cards** (API Explorer status page): icon + large numeric value in `--cyan` or `--green`, label in `--t2` below, on a `var(--card)` background.
- **Method badge pills** on the API Explorer (`GET` in green, `POST` in amber, `AUTH` in purple) — reuse this same pill system for COG status indicators.
The implementor **must not introduce new colours, typefaces, or border radii.** Every component should feel like it was built by the same team that built the Cog Store and the API Explorer. A user navigating from the Cog Store into the HOMECORE dashboard should not notice a visual seam.
---
## 4. UI sections — required panels
### 4.1 System Dashboard (the "home screen")
The always-visible overview panel. Modelled on the API Explorer's live metric cards. All values update in real-time.
- **v0 Appliance health strip** — reuse the exact metric-card pattern from `seed.cognitum.one/status`: one card each for CPU %, RAM usage, Hailo-10H inference load (% utilisation), Hailo temperature, uptime, and the running services (`ruview-mcp-brain:9876`, `cognitum-rvf-agent:9004`, `ruvector-hailo-worker:50051`). Values in `--cyan`, labels in `--t2`. This strip is always at the top — it represents the machine the user is looking at.
- **SEED Fleet overview** — a grid of SEED node cards (one per paired SEED) on the `var(--card)` surface with `var(--border)`. Each card shows: online/offline status pill (green/red), firmware version, epoch number, current vector count, last ingest timestamp, and witness-chain validity badge. A collapsed row shows the SEED's 5 onboard sensors in summary (PIR: yes/no, door: open/closed, temperature from BME280). Offline SEEDs render the entire card with a `--red-d` background tint. Clicking a SEED card navigates to the SEED Detail view (§4.2).
- **ESP32 Node summary** — count of active ESP32 nodes per SEED, current frame rate (target: 100 Hz CSI + 1 Hz feature vectors), and a compact warning list for nodes with known issues (presence_score normalisation anomaly, stale firmware version).
- **COG Runtime status row** — a horizontal strip of status pills for each installed COG on the v0 Appliance. Pill colours follow the existing badge convention: `--green-d`/`--green` for running, `--red-d`/`--red` for failed, `--t3`/`--t2` for stopped. COG name in `--mono`. Clicking a pill navigates to COG Management (§4.6).
- **Event Bus activity indicator** — a small real-time sparkline showing the homecore broadcast channel event rate (events/sec). Indicate channel lag if a subscriber is falling behind the 4,096-event capacity.
### 4.2 SEED Detail View (per-SEED drill-down)
Accessible from the fleet grid. Full-page panel for a single SEED node, using the card + section-header pattern from the Cog Store's detail views.
- **SEED identity header** — `device_id` in `--mono`, firmware version, paired status in green, USB vs WiFi connection mode. A section-header gradient border (cyan → purple, matching the featured card style) visually separates this from Appliance content.
- **Vector Store panel** — current vector count, dimension (8), last kNN query latency, current epoch number, a small sparkline of ingest rate over the last hour, and a storage budget bar showing usage against the 100K working-set target. A "Compact now" button (`POST /api/v1/store/compact`) in ghost style. When usage exceeds 80%, the bar renders in `--amber`.
- **Witness Chain panel** — chain length (SHA-256 entries), last verification timestamp, a one-click "Verify chain" button (`POST /api/v1/witness/verify`), and an "Export attestation bundle" button for regulated deployments. The Ed25519 custody attestation (device-bound keypair, epoch + vector count + witness head) renders here. Chain length in `--purple`, following the existing epoch/chain colour convention.
- **Onboard Sensors panel** — live readings from all 5 sensors in individual sub-cards: BME280 (temperature °C, humidity %, pressure hPa), PIR (motion boolean with last-triggered timestamp), reed switch (open/closed with last-changed timestamp), ADS1115 (4 analog channels with configurable labels), vibration (boolean with last-triggered). These are ground-truth validators against CSI readings and are critical for diagnosing false positives in the mixture-of-specialists. Sensor values in `--cyan`; sensor names in `--t2`.
- **Reflex Rules panel** — the 3 pre-configured rules with current state: `fragility_alarm` (threshold 0.3 → relay actuator), `drift_cutoff` (threshold 1.0), `hd_anomaly_indicator` (threshold 200 → PWM brightness). Show last-fired time for each. The `fragility_alarm` threshold is the most commonly adjusted field and should be editable inline. Rules that have recently fired render with a `--amber-d` background tint.
- **Cognitive Analysis panel** — boundary fragility score (0.0–1.0, from Stoer-Wagner min-cut on the kNN graph) rendered as a progress bar: green below 0.3, amber 0.3–0.6, red above 0.6. High fragility (>0.3) indicates a regime change in the environment and should be visually prominent. Temporal coherence phase boundaries shown as a labelled timeline of detected environment state transitions. kNN graph rebuild cadence indicator (every 10 s).
- **Ingest pipeline status** — which ESP32 nodes feed this SEED, the packet type each is sending (`0xC5110003` native feature vectors vs `0xC5110002` vitals fallback path — distinguished visually since native is preferred), current ingest batch size, flush interval, and bridge path topology (direct vs host-laptop hop). The bridge-hop warning (known architectural limitation) renders in `--amber` since it adds a network hop.
### 4.3 SEED Fleet Map (multi-SEED topology)
For deployments with more than one SEED, a topology view showing the mesh:
- **Node hierarchy diagram** — v0 Appliance at root, SEEDs as second tier (grouped by room/zone), ESP32 nodes as leaves under each SEED. Lines represent active data flows. ESP-NOW mesh sync links between SEEDs shown as dashed lines. Connection health shown via line colour (green/amber/red). All labels in `--mono`.
- **Cross-SEED event deduplication indicator** — for events that span multiple SEEDs (one fall detected by two rooms; one occupant tracked through room A → hallway → room B), show a fusion badge indicating how many SEEDs contributed to the composite event.
- **Federation config** ([ADR-105](ADR-105-federated-csi-training.md)) — federated-learning round coordinator role (which SEED is the round coordinator), current round number, K healthy nodes selected, delta exchange status. **Model deltas only — never raw CSI** is a design invariant that must be labelled explicitly in the UI.
### 4.4 Entity & State Browser
The homecore state machine (`DashMap<EntityId, Arc<State>>`) is the authoritative source of truth. Every COG running on the v0 Appliance contributes entities.
- **Entity list by domain** — grouped by the `domain.` prefix of `EntityId`, using collapsible section headers. The 21 entities per ESP32 node (11 raw + 10 semantic primitives from `cog-ha-matter`) are the most important set. For each entity: current state string (in `--t1`), last-changed timestamp (in `--t3`), attribute map as collapsible JSON in `--mono`, and the Context (`user_id` + `parent_id` causality chain, critical for care/audit deployments). Entity IDs always in `--mono`.
- **SEED provenance badge** — each entity carries a small badge showing its data lineage: which ESP32 node → which SEED → which COG → homecore state machine. This trace is invaluable for debugging false positives and is a **first-class UI element, not a collapsed detail.**
- **Domain filter + semantic search** — filter by domain prefix and, once [ADR-132](ADR-132-homecore-recorder-history-semantic-search.md) (homecore-recorder) lands, ruvector-backed semantic search: "when did the living room anomaly score last correlate with a door-open event?" A keyword filter across entity IDs and attribute keys ships in the initial release regardless of [ADR-132](ADR-132-homecore-recorder-history-semantic-search.md) status, given entity density; the semantic search layers on top once the recorder lands.
- **Real-time WebSocket feed** — entity states update live via the homecore `subscribe_events` WebSocket command ([ADR-130](ADR-130-homecore-rest-websocket-api.md)). The UI must never poll. Show a broadcast-channel lag indicator; warn visually if the subscriber is falling behind the 4,096-event channel capacity.
- **StateChanged detail panel** — clicking any entity opens a slide-over panel showing the full `StateChangedEvent`: `old_state`, `new_state`, `context.id`, `context.user_id`, and the `context.parent_id` chain rendered as a breadcrumb trail.
### 4.5 RoomState / Sensing Panel
Surfaces the mixture-of-specialists output from the calibration service — the highest-level per-room sensing result. Data comes from `GET /api/v1/room/state?bank=<room_id>` on the v0 Appliance.
- **Per-room cards** — one card per `room_id` on the `var(--card)` surface. Each card shows live `RoomState` JSON fields as sub-rows: presence (occupied/absent chip in green/red with confidence bar), posture (standing/sitting/lying chip with confidence), breathing BPM (numeric in `--cyan` with range indicator 6–30), heart rate BPM (numeric in `--cyan` with range indicator 40–120), restlessness score (0–1 progress bar), and anomaly score (0–1 with normal/anomalous label, bar turns red above a configurable threshold).
- **STALE warning** — when `stale: true` (the specialist bank was trained against a different baseline), render the entire room card with a `--amber-d` background tint and a prominent amber banner reading "Bank stale — baseline has changed" with a direct "Recalibrate room" link into the calibration wizard (§4.7). This is the most common real-world failure mode and **must never be subtle.**
- **VETO indicator** — when `vetoed: true` (anomaly veto suppressed vitals/posture because the window was physically implausible), render the affected specialist slots in `--red` with a "Veto active" label. Values suppressed by veto **must not render as zeros** — they must render as explicitly withheld.
- **Null specialist placeholders** — specialists not yet trained (`null` in the specialist bank) render as "Not trained" placeholders in `--t3` with a small "Calibrate to enable" prompt in ghost style. They are **not** errors.
- **Confidence bars** — each specialist output has a confidence float, shown as a small inline bar (`--cyan` fill) next to the reading. Low confidence (< 0.4) renders the bar in `--amber`.
- **Multi-SEED fusion indicator** — for rooms served by multiple SEEDs, show a small badge indicating how many SEED nodes contributed to the `MultiNodeMixture` for this room's reading.
### 4.6 v0 Appliance COG Management
The v0 Appliance hosts COGs at `/var/lib/cognitum/apps/`. This panel is the operational companion to the existing Cog Store (`seed.cognitum.one/store`). It must match the Cog Store's visual conventions precisely — same card layout, same category pills, same install/detail button pair — because operators will move between the two surfaces.
- **Installed COGs list** — for each COG: `id` and `version` in `--mono`, architecture badge (`arm`/`hailo10` etc., category-pill pattern), status pill (running/stopped/failed/updating in green/grey/red/amber), `binary_sha256` verified badge (Ed25519 signature verification shown as a shield icon in `--green` or `--red`), and PID from the pid file. Actions: start, stop, restart (ghost style), and view `output.log` / `error.log` in a monospace drawer using `--mono`. Edit `config.json` inline with syntax highlighting.
- **COG Store / App Registry** — browsable `app-registry.json` listing. This panel should visually mirror `seed.cognitum.one/store` as closely as possible — same featured-card hero layout, same icon + title + description + category pill + action button structure. One-click install downloads the binary from GCS, verifies `binary_sha256` + `binary_signature`, writes the manifest, and starts the COG. Show which new homecore entities will appear in the state machine after install, as a preview list before confirming.
- **OTA Updates** — a badge count on installed COGs with available updates, matching the "Installed (N)" tab badge convention from the existing Cog Store. Show a diff panel (version change, new entities, config schema changes) before confirming the update.
- **Hailo HEF status** — for COGs with `arch: hailo10`: loaded HEF files on the Hailo-10H, current inference throughput, and `ruvector-hailo-worker:50051` connection status. The RF Foundation Encoder ([ADR-150](ADR-150-rf-foundation-encoder.md)) and neural pose head display here once available.
### 4.7 Calibration Wizard
The full baseline → enroll → train → verify pipeline runs via HTTP against the v0 Appliance ([ADR-151](ADR-151-room-calibration-specialist-training.md)). This is a multi-step guided flow — not a raw API panel. Use a stepped wizard layout with a progress indicator at the top (steps 1–5 as numbered pills, active step in `--cyan`, completed in `--green`, pending in `--t3`).
- **Step 1 — Select room and SEED** — enter a `room_id` name (validated against `[A-Za-z0-9_-]{1,64}`) and select which SEED(s) and ESP32 nodes serve this room from a dropdown populated from the live fleet. Show current CSI ingest health for the selected nodes inline — if frames are not arriving at the expected rate, display an amber warning **before** allowing the operator to proceed. A broken ingest pipeline will silently fail calibration.
- **Step 2 — Baseline capture** — `POST /api/v1/calibration/start`. A large full-width animated progress bar (cyan fill) reads from `GET /api/v1/calibration/status`: frames recorded vs target, ETA in seconds, `z_median` value. If `motion_flagged` is true, overlay an amber banner: "Room must be empty — movement detected." The baseline UUID produced here is the anchor for all future STALE detection for this room — display it in `--mono` once complete so operators can record it.
- **Step 3 — Anchor enrollment** — the 8 anchor labels in enforced order: `empty`, `stand_still`, `sit`, `lie_down`, `breathe_slow`, `breathe_normal`, `small_move`, `sleep_posture`. For each: a human-readable instruction with an illustration, a countdown timer rendered as a circular progress ring in `--cyan`, and an immediate quality-gate result (accepted in green, retry in amber with a reason string). Drive via `POST /api/v1/enroll/anchor` + `GET /api/v1/enroll/status`. After each accepted anchor, show the extracted feature values (mean, variance, breathing_score, heart_score) in a small `--mono` data row so operators can sanity-check the capture. Show overall progress as "N / 8 anchors accepted."
- **Step 4 — Train** — a single `POST /api/v1/room/train` call. Show the 6 specialist results as a checklist: presence (threshold + occupied_var), posture (prototype count), breathing (min_score), heartbeat (min_score), restlessness (calm/active motion values), anomaly (prototype count + scale). Specialists that returned non-null render in `--green`. Null specialists (insufficient anchor data) render in `--amber` with a "Re-enroll missing anchors" prompt linking back to Step 3 for the specific missing labels.
- **Step 5 — Verify live** — display the live `RoomState` for the just-trained room using the same per-room card layout as §4.5. Prompt the operator to stand in the room and verify presence is detected, try sitting/lying to confirm posture, and breathe normally to confirm vitals are in plausible range. A "Confirm and save" button (cyan, primary) closes the wizard; a "Something's wrong — re-enroll" button (ghost) loops back to Step 3.
### 4.8 Event Bus & Automation Feed
- **Live event stream panel** — a virtualized scrolling list of `SystemEvent` variants (`StateChanged`, `EntityRegistered`, `ConfigReloaded`) and notable `DomainEvent`s from the homecore Tokio broadcast channel. Each row shows: event-type pill (coloured by variant), `entity_id` in `--mono`, old state → new state arrow, timestamp, and `context.user_id`. The stream is filterable by entity domain, event type, or source SEED/COG. The filter bar uses the same search-input style as the Cog Store's search field.
- **Context causality breadcrumb** — expanding any event row shows the full Context chain (`context.id` → `parent_id` → `grandparent_id`) as a breadcrumb trail in `--mono`. This is how automation loops become visible without any separate debugging tool.
- **Automation builder** ([ADR-129](ADR-129-homecore-automation-engine.md) scope) — a trigger → condition → action editor on the card surface. The most important RuView-specific trigger types to support are: `state_changed` on `RoomState` entities with a threshold expression (e.g. `anomaly.value > 0.8`), SEED reflex-rule firing events (`fragility_alarm`, `hd_anomaly_indicator`), and custom `domain_event` topics. Actions include calling services in the homecore service registry and firing domain events. The condition expression editor uses `--mono`.
### 4.9 Witness / Audit Log
- **Unified witness timeline** — a chronological merged view of events from both tiers: the SEED's SHA-256 ingest chain (every RVF store write attested) and homecore's Ed25519 state-transition chain (biometric crossings, BFLD identity-risk elevations). Each row: `entity_id` in `--mono`, old/new state, timestamp, source SEED `device_id`, signing key fingerprint (first 8 chars in `--mono`). Pagination uses the same "Showing X–Y of Z" convention from the Cog Store's cog grid.
- **Privacy mode banner** — a persistent top-of-panel banner showing current privacy mode: `--green-d`/green text for full-publish mode; `--amber-d`/amber text for audit-only mode (SHA-256 digests on-SEED only, no MQTT state messages). Show the per-SEED privacy mode state, since SEEDs can be individually configured. Toggling privacy mode is a high-stakes action — require an explicit "Confirm" step with a summary of what will change.
- **Export bundle** — an "Export attestation bundle" button (ghost) that packages the SEED witness chain + homecore Ed25519 chain as a downloadable archive for regulated-deployment (care home, hotel, shared office) compliance handoff.
### 4.10 Settings & Integration Config
- **SEED fleet management** — add, remove, and reprovision SEEDs. Show the USB-only pairing requirement prominently (the pairing window only opens via `169.254.42.1`, not WiFi — a security invariant). Per-SEED: `device_id` in `--mono`, firmware version, bearer token status, and a "Rotate token" action (ghost) that walks the operator through the secure token rotation flow.
- **ESP32 node provisioning** — per-node NVS config display (target IP, target port, node_id), last-seen firmware version, and a link to the provisioning script. The `node_id` → room/zone assignment is editable here and persists to the room calibration system's `room_id` mapping.
- **MQTT / cog-ha-matter config** ([ADR-116](ADR-116-cog-ha-matter-seed.md)) — broker URL, credentials (masked), MQTT topic prefix, mDNS advertisement status (`_ruview-ha._tcp`), and a live connection indicator (green dot for connected, red for unreachable). The 21 HA-DISCO entities per node are listed here with their `via_device` assignments showing which SEED they belong to in HA's device registry.
- **Long-lived access tokens** — for homecore-api companion-app connections (HA 2025.1 wire-compat, [ADR-130](ADR-130-homecore-rest-websocket-api.md)). Token creation, last-used timestamp, and revocation. The HA companion-app pairing QR-code flow surfaces here.
- **Federation config** — for multi-SEED deployments: ESP-NOW mesh sync status, cross-SEED epoch alignment values, and federated-learning round settings (coordinator SEED, round cadence, Krum aggregation parameters per [ADR-105](ADR-105-federated-csi-training.md)). The design invariant **"model deltas only, never raw CSI"** must be labelled explicitly in this panel.
---
## 5. Navigation structure
HOMECORE-UI must integrate into the existing Cognitum Appliance nav shell. The top nav should read:
```
Framework | Guide | Cog Store | HOMECORE | Status
```
— inserting **HOMECORE** as a first-class nav item between the existing "Cog Store" and "Status" entries, using the same nav-item style (text in `--t2`, active state in `--cyan` with bottom underline).
Within the HOMECORE section, a left sidebar (or top sub-nav on narrow viewports) provides section navigation:
The COG Store panel within HOMECORE (§4.6) links out to `seed.cognitum.one/store` for the full catalog view, ensuring the existing Cog Store remains the canonical browsing experience.
---
## 6. Key UX invariants
These must be maintained across every panel:
1. **Always make the tier origin of any data explicit.** A `RoomState` reading traces to an ESP32 node → SEED → COG → v0 Appliance state machine. The provenance badge (§4.4) must appear wherever entity states are displayed.
2. **The `stale` and `vetoed` flags from `RoomState` and the kNN fragility score from SEED cognitive analysis are meaningful diagnostic signals** — they must never be silently hidden, styled grey-on-grey, or collapsed behind an expand toggle. They represent system health operators need to act on.
3. **Values that are `null` because a specialist has not been trained must be visually distinct from values that are unavailable due to an error.** The distinction is operationally important: `null` means "calibrate to enable," unavailable means "investigate."
4. **All entity IDs, hashes, API endpoints, binary signatures, device UUIDs, and JSON payloads must use `--mono` font.** This is already the convention in the API Explorer and must be consistent throughout HOMECORE-UI.
5. **The v0 Appliance Hailo HAT is a separate subsystem from the SEED's edge compute.** Inference results tagged as Hailo-sourced (COGs with `arch: hailo10`) must be visually distinguished from results from CPU-only COGs (`arch: arm`) so operators can triage hardware-specific failures.
---
## 7. Scope — complete UI delivery
The deliverable is the **entire** dashboard. Every panel below ships fully implemented and wired to its live data source — there is no scaffold-only milestone and no panel left as a placeholder. The table records each panel's authoritative backing API so the build can proceed in whatever order best fits the dependency graph; it is a dependency map, **not** a sequence of partial releases.
| Panel | Section | Backing API / source |
|---|---|---|
| System Dashboard | §4.1 | [ADR-130](ADR-130-homecore-rest-websocket-api.md) WebSocket + appliance health endpoints |
| Entity & State Browser | §4.4 | [ADR-127](ADR-127-homecore-state-machine-rust.md) state machine via [ADR-130](ADR-130-homecore-rest-websocket-api.md) `subscribe_events`; semantic search via [ADR-132](ADR-132-homecore-recorder-history-semantic-search.md) |
### 7.1 Build sequencing within the complete deliverable
The complete UI depends on backing services that mature on their own timelines. Each panel is built against the **real gateway endpoint** defined in §11; where the upstream is not yet available the panel renders a typed empty/error state, **not** fabricated data (the dev-only `?demo=1` fixture of §2.2 exists for offline development only and is never the shipped behaviour). Concretely, the hard contract dependencies are: [ADR-130](ADR-130-homecore-rest-websocket-api.md) (REST + WebSocket), [ADR-127](ADR-127-homecore-state-machine-rust.md) (state machine), [ADR-151](ADR-151-room-calibration-specialist-training.md) (calibration), [ADR-128](ADR-128-homecore-integration-plugin-system.md) (plugin runtime), [ADR-129](ADR-129-homecore-automation-engine.md) (automation), [ADR-132](ADR-132-homecore-recorder-history-semantic-search.md) (event history + semantic search), [ADR-116](ADR-116-cog-ha-matter-seed.md) (SEED/Matter), [ADR-069](ADR-069-cognitum-seed-csi-pipeline.md) (SEED ingest), and [ADR-105](ADR-105-federated-csi-training.md) (federation). The keyword entity filter (§4.4) ships immediately; semantic search layers on once [ADR-132](ADR-132-homecore-recorder-history-semantic-search.md) lands. The exact panel→endpoint→upstream map and the new gateway code each requires are §11; the staged delivery is §12.
---
## 8. Consequences
### 8.1 Positive
- Operators, integrators, and residents get a single coherent surface for the full two-tier stack, replacing the need to SSH into SEEDs or hand-craft API calls.
- The dashboard reuses the proven Cognitum design tokens and component patterns verbatim, so it ships visually consistent with no separate design effort and no perceptible seam between surfaces.
- Diagnostic signals that today are invisible (`stale`/`vetoed` flags, kNN fragility, provenance lineage, channel lag) become first-class, surfacing the system's most common real-world failure modes directly to operators.
### 8.2 Negative / risks
- The UI hard-depends on the wire-compat guarantees of ADR-130 and the calibration contract of ADR-151; schema drift in either breaks panels silently. Integration tests against every backing contract in §7 are required.
- Committing to the complete UI in one deliverable is a larger up-front effort and couples the UI's readiness to the maturity of multiple backing services (§7.1, §11). The mitigation is the BFF gateway (§2.1): each panel targets one same-origin endpoint, and the gateway absorbs upstream churn behind a stable contract.
- Promoting `homecore-server` to a gateway means it now **proxies cross-tier traffic** (calibration API, SEED HTTPS, appliance daemons). This adds a network hop, a place for upstream timeouts/partial failures to surface, and a server-side store of SEED bearer tokens that must be protected (§11.10). Each proxied route needs an explicit timeout + typed error mapping so one slow SEED cannot stall the dashboard.
- Several panels depend on data that only exists on **real hardware or new daemons** (SEED device tier, appliance host metrics, COG supervisor). Until those upstreams exist the corresponding gateway routes return `503 upstream_unavailable`; this is honest but means the dashboard is only as "live" as the tiers behind it (§11 classifies every endpoint by what it depends on).
- Faithfully mirroring `seed.cognitum.one/store` couples HOMECORE-UI to the external Cog Store's evolving design; token drift there must be tracked and re-synced.
- The two-tier mental model (Appliance root, SEED children, ESP32 leaves) must be enforced consistently; any panel that flattens or peers the tiers undermines the core architectural constraint.
---
## 9. References
- `https://seed.cognitum.one/store` — primary design reference for all visual conventions.
- `https://seed.cognitum.one/status` — reference for live metric-card layout.
- [ADR-105](ADR-105-federated-csi-training.md) — Federated CSI training (multi-SEED federation).
- [ADR-151](ADR-151-room-calibration-specialist-training.md) — Per-room calibration specialist training (calibration HTTP API).
- `v2/crates/homecore/src/` — state machine, entity, event, registry source.
- `docs/integration/calibration-appliance-integration.md` — calibration API contract and RoomState schema.
---
## 10. Implementation status
Implemented as a zero-dependency, no-build-step vanilla TS/JS + CSS frontend served by `homecore-server` at `/homecore` (the `rufield-viewer` "Axum + vanilla-JS" pattern). The complete deliverable per §2/§7 — all ten panels, fully rendered, wired to live data where the backing service exists and to a contract-conformant DEMO-flagged mock layer (§7.1) where it does not.
**Location:** `v2/crates/homecore-server/ui/` — `css/tokens.css` (the §3.1 palette, verbatim) + `css/app.css` (§3.3 components); `js/{ui,api,ws,mock,app}.js` (shared helpers, REST client, `subscribe_events` WS client, mock layer, shell+router); `js/panels/*.js` (one module per §4 panel). Mounted via `tower-http``ServeDir` in `homecore-server::build_app`, gated by `--ui-dir`/`HOMECORE_UI_DIR`.
**Verification:**
- **Rust** — `#[cfg(test)] mod ui_tests` in `homecore-server/src/main.rs`: 5 integration tests (`tower::oneshot`) covering index, design tokens, all ten panel modules served, API coexistence, and mount-disable. *Written but not compiled in the authoring environment (no Rust toolchain present); run `cargo test -p homecore-server` on a Rust host before merge.*
- **Frontend** — `ui/` test suite under plain `node` (no npm install): `npm test` → import/export graph verifier (15 modules) + render-smoke (executes every panel against a DOM shim; 21 checks) + interaction suite (live WS patch, ws.js handshake/parse, calibration contract; 3 checks). **24/24 green.**
- **Benchmark** — `npm run bench`: total bundle **136.8 KB** uncompressed (**~37× smaller** than HA's ~5 MB Lit bundle, the ADR-126 §1.1 foil); slowest panel **1.5 ms/cold-render**.
**Honest scope — current vs. target.** *Earlier cut:* the front-end was complete but only §4.4 Entities was wired to a real backend; the rest rendered from an in-browser mock. *This revision implements the §11 wiring:*
- **Front-end (§11.11) — DONE and verified.**`api.js` rewritten: all data accessors are async and call the §11.2 gateway routes; the mock layer is demoted to a dev-only fixture reachable **only** under `?demo=1` / `HOMECORE_UI_DEMO` (§2.2); every panel `await`s and renders a typed empty/error state on failure (no mock fallback in production). All ten panels converted (3 by hand, 7 via parallel agents). Verified under Node: 5 test files green — import graph, boot, render-smoke (22), interaction (3), **and a new prod-errors suite (13) that runs with demo OFF + gateway unreachable and asserts every panel renders an error state, never mock, never throws** (it caught and fixed a real unhandled-rejection in the events panel).
- **Gateway (§11.1–§11.6) — IMPLEMENTED, COMPILED, TESTED, RUN.** New `homecore-server/src/gateway.rs` (+`reqwest` dep, +CLI/env flags `--calibration-url`/`--calibration-token`/`--apps-dir`/`--gateway-timeout-ms`, merged into `build_app` via `gateway_router`). Real handlers: `/api/cal/*` reverse-proxy (W2), `GET /api/homecore/rooms` with the §11.3 RoomState adapter (W2), `GET /api/homecore/cogs` supervisor over the apps dir (W4), `GET /api/homecore/appliance` from `/proc` + port probes (W6). SEED-device/appliance-daemon routes (seeds, federation, witness, privacy, settings, automations, events-history, hailo, tokens — W3/W5) return a typed `503 upstream_unavailable` per §11.2. **Verified on Rust 1.89: `cargo test -p homecore-server --no-default-features` = 12/12 pass** (6 gateway + 6 UI mount). **Run live:**`GET /api/homecore/appliance` returns real `/proc` metrics + TCP service probes; unauth → `401`; `cogs` → `[]` with no apps dir; SEED-tier → typed `503`; and against a mock calibration upstream the `/api/cal/*` proxy passes through (`200`) and `GET /api/homecore/rooms` correctly adapts `RoomState` to the UI shape (`breathing`→`breathing_bpm`, `heartbeat:null`→`heart_bpm:null`, injected `anomaly.threshold`/`room_id`, `stale` passthrough). **Live testing caught + fixed one real bug** — a double-`v1` path in the `/api/cal/*` proxy URL.
The endpoint-by-endpoint contract is **§11**; the staged plan and which endpoints depend on real SEED/appliance hardware vs. pure software is **§12**.
---
## 11. Backend wiring — making every panel real
This section is the authoritative contract for full functionality. It removes the mock layer from the production path (§2.2) by routing every panel through the `homecore-server` BFF gateway (§2.1). Each endpoint is classified by what it depends on:
- **EXISTS** — backend code already in this repo; gateway only proxies/adapts.
- **NEW-GW** — pure software the gateway itself implements (filesystem, `/proc`, process control, recorder query) — no new external service.
- **NEW-API** — a small HTTP wrapper to add to an existing in-repo crate (`homecore-api`, `homecore-automation`).
- **SEED-DEV** — depends on a SEED node's on-device HTTPS API (separate hardware/firmware).
- **APPLIANCE** — depends on an appliance daemon / accelerator stat source.
### 11.1 Gateway shape
`homecore-server` already mounts `homecore-api` at `/api/*` and the UI at `/homecore`. It gains a new **`/api/homecore/*`** namespace (the dashboard-specific aggregation surface) plus a **`/api/cal/*`** reverse-proxy to the calibration service. The browser issues only same-origin requests; the gateway fans out server-side, holding all upstream credentials (§11.10). Every proxied route has an explicit timeout and maps upstream failure to a typed body (`503 upstream_unavailable`, `504 upstream_timeout`) so one slow tier never stalls the dashboard.
The calibration service is real but on a different binary/port; the gateway reverse-proxies it under `/api/cal/*` (upstream base from `HOMECORE_CALIBRATION_URL`). Its `RoomState` (`wifi-densepose-calibration/src/runtime.rs`) does **not** match the UI's shape, so the gateway adapts it in `GET /api/homecore/rooms`:
| Real field (`RoomState`) | UI field | Adapter rule |
| `vetoed` / `stale` | `vetoed` / `stale` | pass through (drives the §4.5/§6 banners) |
| *(absent)* | `room_id`, `seeds[]` | injected by the gateway from the **room registry** |
A **room registry** (config or derived from `GET /api/cal/v1/calibration/baselines`) maps each `room_id` → bank name + serving SEED ids, so `GET /api/homecore/rooms` returns one adapted record per room. `Option::None` → JSON `null` keeps the null-vs-withheld distinction (§6 invariant 3) intact end-to-end.
### 11.4 SEED registry & device-API proxy
The gateway holds a **SEED registry** (`device_id` → base URL + bearer token + zone), populated by pairing (§4.10) and persisted server-side. `GET /api/homecore/seeds[/:id]` fans out to each SEED's on-device API and shapes the result to the §4.2 card/detail model. Expected SEED-side endpoints (the contract the SEED firmware must satisfy — a subset of its 98 endpoints): health; vector-store stats (`vector_count`, `dim`, `epoch`, `knn_latency_ms`, ingest rate); witness (`len`, `last_verify`, `valid`) + `POST verify`; onboard sensors (BME280/PIR/reed/ADS1115/vibration); reflex rules + thresholds; cognitive analysis (fragility, coherence phases); ingest feeders (ESP32 node ids + packet type `0xC5110003`/`0xC5110002` + rate). Offline/unreachable SEEDs surface as `online:false` (drives the §4.1 red tint) rather than failing the whole list.
### 11.5 Appliance metrics collector (§4.1)
`GET /api/homecore/appliance`, implemented in the gateway: CPU/RAM/uptime from `/proc`; Hailo load + temperature from the Hailo runtime/sysfs (or `ruvector-hailo-worker` stats); service health by probing `ruview-mcp-brain:9876`, `cognitum-rvf-agent:9004`, `ruvector-hailo-worker:50051`; event-bus rate from the `homecore` broadcast channel + its lag counter (already exposed for §4.1/§4.4).
### 11.6 COG supervisor (§4.6)
`GET /api/homecore/cogs`: read each `/var/lib/cognitum/apps/*/manifest.json` ([ADR-100](ADR-100-cog-packaging-specification.md)), the pid file, and verify `binary_sha256` + `binary_signature` (Ed25519) → status/shield. `POST …/cogs/:id/{start,stop,restart}` performs supervised process control; `GET …/cogs/:id/logs` tails `output.log`/`error.log`; `GET/PUT …/cogs/:id/config` reads/writes `config.json`. Hailo-arch COGs join the §11.5 Hailo stats. The Cog Store/App-Registry **browsing** panel was removed per product decision; this is operational management only.
### 11.7 Witness aggregation + privacy (§4.9)
`GET /api/homecore/witness` merges two chains chronologically: the `homecore` Ed25519 state-transition chain (exposed by a small `homecore-api` route over its witness log) and each paired SEED's SHA-256 ingest chain (proxied via the registry), paginated server-side. `GET/POST /api/homecore/privacy` reads/sets per-SEED privacy mode via the SEED privacy control plane ([ADR-141](ADR-141-bfld-privacy-control-plane-modes-attestation.md)) — the POST is the high-stakes confirmed toggle (§4.9). `GET /api/homecore/witness/export` packages both chains into the downloadable attestation bundle.
### 11.8 Event history + automation CRUD (§4.8)
`homecore-api` adds `GET /api/events?since=…` backed by `homecore-recorder` ([ADR-132](ADR-132-homecore-recorder-history-semantic-search.md)) for history (live updates continue over the existing WS). The automation builder persists through `GET/POST/DELETE /api/homecore/automations`, a thin HTTP wrapper over the `homecore-automation` engine's register/list/remove ([ADR-129](ADR-129-homecore-automation-engine.md)). RuView-specific triggers (RoomState thresholds, SEED reflex events) map onto the engine's trigger types.
### 11.9 Entity provenance convention (§4.4/§6)
The first-class provenance badge requires each entity to carry its lineage. Convention: every integration writes `attributes.source` (and, where known, `attributes.seed` / `attributes.cog`) when it sets state; `cog-ha-matter` ([ADR-116](ADR-116-cog-ha-matter-seed.md)) populates these from the ESP32 node → SEED → COG path and HA `via_device`. The gateway/UI resolves node→seed→cog from these attributes (no fabrication; missing lineage renders as "unknown", not invented).
### 11.10 Auth, credentials, config
- **Browser → gateway:** one long-lived access token (the §4.10 LLAT), sent as `Authorization: Bearer`; validated by `homecore-api`'s `LongLivedTokenStore`. The dev default (`allow_any_non_empty`) stays for local runs; production provisions `HOMECORE_TOKENS`.
- **Gateway → upstreams:** SEED bearer tokens and the calibration token live **only** server-side (SEED registry + `HOMECORE_CALIBRATION_TOKEN`); never sent to the browser. This is the reason the gateway exists.
- **Config:**`HOMECORE_CALIBRATION_URL`, SEED registry store path, per-proxy timeout (default 2 s), `HOMECORE_UI_DEMO` (dev fixture). No browser CORS needed (same origin); gateway→upstream is server-to-server.
### 11.11 Front-end changes
`api.js`: drop the mock fallback from the production path — methods call the §11.2 gateway routes; `this.base` stays same-origin; the mock layer is reachable only under `?demo=1`/`HOMECORE_UI_DEMO`. Every panel renders a **typed empty/error state** (not mock) when its route returns `503/504`. `mock.js` moves to a dev fixture (kept for the offline test harness, excluded from the production bundle). The §10 frontend tests are re-pointed at the gateway contract (and gain contract tests per §11.2 route).
---
## 12. Delivery plan to full functionality
Staged so each wave is independently shippable behind the gateway, lands real data for a coherent set of panels, and has an explicit acceptance gate. "Class" reuses §11's tags.
| Wave | Scope | Class | Acceptance gate |
|---|---|---|---|
| **W1 — Gateway foundation** | `/api/homecore/*` scaffold in `homecore-server`; auth passthrough; per-proxy timeout + typed errors; `api.js` base + remove prod mock (`?demo=1` only); panels get typed empty/error states | NEW-GW | Entities + live WS still green; with no upstreams, every other panel shows "upstream unavailable", **never** mock (unless `?demo=1`); Rust + JS suites pass |
| **W2 — Rooms + Calibration** | `/api/cal/*` reverse-proxy; `GET /api/homecore/rooms` with the §11.3 RoomState adapter + room registry; wire §4.5 + the §4.7 wizard to real endpoints; delete the in-browser calibration stub | EXISTS (proxy+adapter) | Against a running `calibrate-serve` (replayed CSI), the wizard drives a real baseline→enroll→train→verify and §4.5 shows real `RoomState` with correct stale/veto/null mapping; contract test on the adapter |
| **W3 — Events + Automations** | `GET /api/events` over `homecore-recorder`; `/api/homecore/automations` over `homecore-automation` | NEW-API | §4.8 history loads from recorder; an automation created in the UI persists and fires via the engine |
| **W4 — COG management** | `/api/homecore/cogs*` supervisor over `/var/lib/cognitum/apps/` (manifest + pid + sig verify + logs + config) | NEW-GW | §4.6 lists real installed COGs; start/stop/restart works; sha256/signature shield reflects real verification; logs tail |
| **W5 — SEED tier** | SEED registry + pairing; `/api/homecore/seeds*` device proxy; witness merge + privacy control; ESP32 provisioning | SEED-DEV | Against a real or emulated SEED API, §4.2/§4.3/§4.9/§4.10 show real vector-store/witness/sensor/reflex/cognition data; SEED tokens stay server-side; offline SEED → red tint, not a failed page |
| **W6 — Appliance + federation + Hailo** | `/api/homecore/appliance` (host metrics + service probes); `/api/homecore/hailo`; `/api/homecore/federation` ([ADR-105](ADR-105-federated-csi-training.md)) | NEW-GW + APPLIANCE | §4.1 health is real; §4.6 Hailo HEF/throughput real; §4.3 federation round/coordinator/Krum real |
**Definition of done (full functionality):** with W1–W6 merged and the upstream tiers running, loading `/homecore` with **no**`?demo=1` flag shows live data on all ten panels, `api.anyDemo()` is false, and no panel renders fabricated values. Panels whose tier is offline show typed empty/error states. The mock layer is reachable only as the `?demo=1` developer fixture.
### 12.1 Wave status (this revision)
| Wave | Status |
|---|---|
| **W1 — Gateway foundation** | ✅ DONE — `gateway.rs`, auth passthrough, typed `503/504`, merged into `build_app`; front-end mock removed from prod path + `?demo=1` fixture; typed error states. **Compiled + 12/12 Rust tests + JS suite green + run live.** |
| **W2 — Rooms + Calibration** | ✅ DONE — `/api/cal/*` reverse-proxy + `GET /api/homecore/rooms` RoomState adapter; front-end calibration stub deleted (now proxies the real API). **Proven live against a calibration upstream** (proxy 200 + adapted shape); null-preservation unit-tested. |
| **W4 — COG management** | ✅ supervisor DONE — lists `/var/lib/cognitum/apps/` manifests + pid liveness (returns `[]` live with no apps dir); start/stop/log/config control is the remaining follow-up. |
| **W6 — Appliance + federation + Hailo** | ◑ appliance host metrics from `/proc` + port probes DONE (live `/proc` data verified); Hailo stats + federation remain `503` (need the accelerator stat source / coordinator). |
**Status:** the gateway is **compiled and tested on Rust 1.89** (`cargo test -p homecore-server` = 12/12) and was **run live** (curl proof in §10). The one remaining caveat is intrinsic, not an environment limit: **W3/W5/W6-Hailo/federation depend on services/hardware that are not in this repo** (recorder/automation HTTP wrappers, real SEED nodes, the Hailo stat source), so they return honest typed `503`s and the UI shows error states — exactly as §2.2/§11.2 prescribe. W1/W2/W4/W6-appliance are functional now.
### 12.2 Security review (PR #1082)
A high-effort public-PR review of the merged gateway + front-end surfaced the following, all fixed and pinned by tests (`cargo test -p homecore-server` is now **18/18**):
| # | Severity | Finding | Fix |
|---|---|---|---|
| 1 | **HIGH** | **Path-traversal / confused-deputy SSRF** in the `/api/cal/*` reverse-proxy. The wildcard path was interpolated into the upstream URL while `proxy()` attaches the privileged server-side calibration bearer, so `/api/cal/v1/../../x` (or `..%2f`, `%2e%2e`, leading `/`, `\`, double-encoded `%252e`) could escape the `…/api/` scope **with the token**. | `validate_proxy_path()` decode-then-checks and rejects absolute / backslash / dot-segment / encoded-traversal paths with a typed **400 before the URL is built** (GET **and** POST); legit `v1/...` paths still pass. |
| 2 | Correctness | **CORS + tracing didn't cover gateway routes** — `/api/homecore/*` + `/api/cal/*` were `.merge()`d outside `homecore-api::router()`'s layers. | The audited HC-05 `build_cors_layer()` + `TraceLayer` are now applied to the whole merged app in `main.rs`. |
| 3 | Honesty (§6) | **Fabricated data** — hardcoded `anomaly.threshold: 0.5` in the adapter; dashboard rendered `"null%"`/`"null°C"`; COG Hailo pill hardcoded `"connected"`; `rooms.js` defaulted a null threshold to `0.8`. | Threshold passes through the real upstream value or emits `null` (withheld); dashboard renders `—`; the Hailo pill reflects the real appliance probe; the UI treats a null threshold as withheld. |
| 4 | Robustness | A string `hef` (forwarded verbatim) threw on `.forEach`/`.join`; `frames/target` could be `NaN%`/`Infinity%`; calibration Restart leaked the baseline `setTimeout` poll. | `asArray()` coercion; `target > 0` guard; cancellable poll cleared on Restart / panel teardown. |
- **`reqwest` rustls-only is a workspace-wide concern.** `homecore-server` opts into `rustls-tls` only, but cargo feature-unification means any sibling crate enabling the default `native-tls` re-introduces OpenSSL into the final binary. A true "no OpenSSL on the appliance" guarantee requires aligning **every** reqwest-pulling crate on rustls-only — out of scope for this PR; documented at the dependency in `Cargo.toml`.
- **DEV-mode auth.** When `HOMECORE_TOKENS` is unset, the token store falls back to `allow_any_non_empty()` (any non-empty bearer accepted) on `0.0.0.0`. This is pre-existing and intentionally **unchanged** here; the loud boot `warn!` is retained. Provision real tokens (`HOMECORE_TOKENS=…`) before exposing the server to a network.
@@ -495,3 +495,34 @@ Rejected. `ViewpointFusionEvent` (viewpoint/fusion.rs lines 183–219) is an int
**Integration glue -- not yet on the live path:** emission of `CalibrationIdMismatch` / `DriftProfileConflict` / `PhaseAlignmentFailed` once `calibration_id` propagation and the phase-align convergence signal are threaded onto frames; the BFLD witness record emitted on privacy demotion.
**Trust contribution:** sensor *agreement made explicit* -- fusion records the evidence it relied on, and any disagreement automatically tightens the downstream privacy class.
@@ -599,3 +599,53 @@ Per ADR-028/ADR-010, three rows are added to the witness log:
**Integration glue -- not yet on the live path:** wiring the registry into `PrivacyGate` class transitions, the MQTT discovery payload, and a read-only Home Assistant diagnostic entity exposing the active mode + proof hash.
**Trust contribution:** the *policy spine* -- privacy posture is a tamper-evident, auditable chain rather than a checkbox; an operator's mode choice actively governs whether identity data may even exist.
base_class.as_u8()` (strictly greater unless already clamped at `Restricted`)
under a forced contradiction, and `== base` on a clean cycle.
Fail-closed boundaries were also pinned: an empty cycle errors (no degenerate
over-permissive output, `empty_cycle_fails_closed`) and the single-node boundary
is characterized as a valid non-demoting mode (`single_node_cycle_is_well_formed`).
The related witness domain-separation fix from the same review is recorded in
ADR-137 (the witness folds `effective_class`, so the demotion is auditable).
## Security & Privacy Review (2026-06-14)
Beyond-SOTA privacy+security review of `wifi-densepose-bfld` (the crate was not in the ADR-154–159 sweep). Two real bugs fixed (each pinned by a fails-on-old test), several dimensions confirmed clean.
### Findings
| # | Severity | Site | Issue | Fix | Pinned by |
|---|----------|------|-------|-----|-----------|
| 1 | **privacy-bypass (HIGH)** | `pipeline.rs::process_to_frame` | The documented wire-bytes production path stamped the frame header with the active `PrivacyClass` but serialized the caller's `BfldPayload`**unchanged** via `BfldFrame::from_payload` — never routing through `PrivacyGate::demote`. A frame labeled `Anonymous`(2)/`Restricted`(3) carried the full `compressed_angle_matrix` (identity surface) + amplitude/phase + `csi_delta`. A `NetworkSink` accepts class ≥ `Derived`(1), so the identity surface could cross the node boundary despite the restrictive class byte — the byte lied about content. | Apply `PrivacyGate::demote(frame, active_class)` after construction: a same-class transition that strips the sections the class forbids; `Raw`/`Derived` keep the full payload. | `tests/pipeline_to_frame.rs::process_to_frame_at_anonymous_strips_identity_leaky_sections`, `…_in_privacy_mode_strips_amplitude_and_phase` (both FAILED pre-fix); `…_at_derived_preserves_full_payload` (over-strip guard) |
| 2 | **PII/injection (MEDIUM)** | `mqtt_topics.rs::render_events` | `zone_activity` payload built as `format!("\"{zone}\"")` with no JSON escaping (while `ha_discovery.rs` already escapes). A zone name with `"`/`\` produced malformed/injectable JSON on the HA state topic. | `json_string_literal()` escaper mirroring `ha_discovery::push_str_field`. Value-identical for normal zone names. | `tests/mqtt_topic_routing.rs::zone_payload_escapes_json_metacharacters` (FAILED pre-fix) |
### Dimensions confirmed clean (with evidence)
- **Event-field privacy gating** — `BfldEvent::apply_privacy_gating` nulls `identity_risk_score` + `rf_signature_hash` at `Restricted`, and `serde(skip_serializing_if = "Option::is_none")` omits them entirely. `render_events`/`render_discovery_payloads` refuse class < `Anonymous` (stricter than the `sink.rs``NetworkKind``MIN_CLASS = Derived` — defense in depth toward less leakage). Covered by `event_privacy_gating.rs`, `mqtt_topic_routing.rs`, `ha_discovery.rs`.
- **Witness/hash framing (the engine `witness_of` bug class)** — CLEAN. `SignatureHasher::compute` prefixes a **fixed 4-byte**`day_epoch` then a **fixed-width canonical-f32** feature block (`IdentityFeatures`: Embedding = `EMBEDDING_DIM*4`, RiskFactors = 16 B). `PrivacyAttestationProof::compute` hashes a fixed 32-byte `prev_hash` + three fixed 1-byte values. No variable-length operator-influenceable string is concatenated into any digest — no length-prefix-framing collision is possible.
- **Fail-closed** — `payload.rs::from_bytes` rejects truncated/overflowing/trailing-byte sections (`checked_add`, bounds checks); `frame.rs::from_bytes` validates magic/version/length/CRC; `PrivacyClass::try_from` rejects unknown bytes; `identity_risk::score` maps NaN/degenerate factors → 0.0 (privacy-conservative). The `from_score(NaN) → Accept` choice is a documented, deliberate publish-aggregate-only fallback (NaN never reaches it from `score()`); risk-driven NaN cannot leak identity because identity gating is class-byte-driven, not risk-driven.
### Observation (not a bug)
The ADR-141 control plane (`PrivacyMode`/`PrivacyModeRegistry`) is **not yet wired into the emit path** — the emitter/pipeline enforce the raw `PrivacyClass` directly; the registry is exported + unit-tested but advisory. This matches the "Integration glue — not yet on the live path" status above. The class-byte enforcement (emitter + event + renderers + the now-fixed `process_to_frame`) is the live guarantee. Wiring the registry is the documented next step.
### 1.1 The thesis — teach the room before you teach the model
RuView's deployment frontier is not a better generic model. ADR-150 documents the wall directly: an MM-Fi pose head scores **81.63% torso-PCK@20 in-domain but ~11.6% leakage-free cross-subject**, and bigger capacity *hurts* cross-subject (transformer 24.8% < conv 27.3%). A single oversized model that "understands the world" overfits the rooms and bodies it has seen. The lever is the opposite of scale: **a small model that understands *one* room and *one* person**, calibrated in minutes, run locally, and specialised per biological signal.
This positions RuView between the two incumbents in ambient sensing:
- **Wearables** — high fidelity, but people forget to wear them, and they only measure the wearer.
- **Cameras** — powerful, but invasive, store identifiable video, and fail in the dark / under covers.
RuView sits in the middle: it learns the *space*, learns the *person*, and tracks biological rhythm (breathing, heartbeat, restlessness, posture, presence) without seeing skin or storing video. Heartbeat and breathing are not visual problems — they are tiny, repeating disturbances in the RF field. Capturing them well is a *calibration* problem, not a *model-size* problem.
### 1.2 What already exists (and what is missing)
The pieces of a calibration→training pipeline exist as disconnected modules. There is no system that runs them end to end and emits a per-room model bank.
| Capability | Status today | Gap |
|------------|--------------|-----|
| Empty-room baseline (environmental fingerprint) | ADR-135 `BaselineCalibration` (Proposed): per-subcarrier amplitude + circular-phase stats, `ruvcal` NVS namespace | Captures the *room*, but there is no step that captures *guided human anchors* on top of it |
| Field eigenstructure | ADR-030 `field_model.rs` (SVD room eigenmodes) | Consumes calibration; not wired to a training trigger |
| Shared invariant backbone | ADR-150 RF Foundation Encoder (pose-preserving, subject/room/device-invariant) | Defined as a *foundation* embedding; nothing distills it into per-room specialists |
| Few-shot adaptation | `train/src/rapid_adapt.rs` — test-time training → LoRA weight deltas (MERIDIAN P5) | Produces a *single* pose-adaptation delta, not a bank of per-modality specialists |
| Feature extractors | `ruvsense/{bvp,longitudinal,intention,gesture,pose_tracker,adversarial}.rs`, `train/src/signal_features.rs` | Each emits a signal; none is packaged as a labelled training source for enrollment |
| Small-model storage | `wifi-densepose-ruvector` (RVF cognitive containers, HNSW, sketch) | No schema for "a bank of specialist models scoped to a room_id" |
| HF publishing | ADR-149 AetherArena (Hugging Face Space + signed scorer), `sensing-server``from_pretrained` path | Publishes/評価s a *global* model; no notion of a published *base* + private *local* heads |
**The missing system is the connective tissue**: a guided enrollment protocol, a feature-extraction-to-label bridge, a specialist-bank trainer that reuses the frozen HF backbone, and a runtime that fuses the specialists with confidence gating. This ADR defines that system.
### 1.3 The four-step user model (and where each step lands)
The system is deliberately presented to operators as four plain steps. Each maps to existing or new code:
1. **Capture a quiet baseline** — no people, just room/router/reflections/noise/drift → the *environmental fingerprint*. → **Reuse ADR-135**`BaselineCalibration` + **ADR-030** field eigenmodes. No new capture code; the calibration crate calls it.
2. **Capture guided samples** — stand, sit, lie down, slow vs normal breathing, small movement, sleep posture. Clean anchors, not hours of data. → **NEW**`EnrollmentProtocol` (Section 2.2).
3. **Extract the useful signal** — CSI phase, amplitude, Doppler shift, micro-motion, periodicity, variance, timing. → **Reuse**`signal_features.rs` + ruvsense extractors, packaged as labelled `AnchorFeature` records (Section 2.3).
4. **Compress patterns into small ruVector models** — *specialised* per signal: breathing, heartbeat, sleep restlessness, posture, presence, anomaly. → **NEW**`SpecialistBank` trained via `rapid_adapt` LoRA heads over the frozen ADR-150 backbone, stored as RVF (Section 2.4).
---
## 2. Decision
**Build the RuView Per-Room Calibration & Specialized Model Training System: a four-stage, local-first pipeline (`baseline → enroll → extract → train`) that produces a versioned *bank of small specialised ruVector models* scoped to one `room_id`, each a lightweight head distilled/adapted from the frozen, Hugging-Face-published RF Foundation Encoder (ADR-150).** Big model understands the world; small ruVector models understand *your room*.
Two invariants govern every design choice below:
> **(A) Specialisation over scale.** One small model per biological signal, not one large model for all of them. Each specialist is faster, cheaper, more private, and — because it is calibrated to the room's actual fingerprint — often *more accurate* than a general model.
>
> **(B) Local-first, base-shared.** The frozen room/subject/device-invariant backbone is the only artifact published to Hugging Face. Per-room baselines and per-specialist heads never leave the device unless the operator opts into federation (ADR-105).
└───────────────────────────────────────┴────────────────┴──────┤ small models) │
└───────────┬─────────────┘
│ RVF container
▼
RUNTIME: Mixture-of-Specialists
each head emits {value, confidence};
coherence_gate (ADR-135) + anomaly
head veto → fused RoomState
```
The shared backbone is loaded **once per device** and frozen. Every specialist is a small head over its embedding — so the marginal cost of a sixth specialist is kilobytes of LoRA weights, not another full model.
### 2.2 Stage 2 — the guided enrollment protocol (NEW)
`EnrollmentProtocol` is a CLI-driven state machine that walks the operator through a fixed sequence of labelled **anchors**. The design rule from the user vision is explicit: *clean anchors, not hours of data.* Each anchor is a short (default 20 s @ 20 Hz = 400 frames) labelled clip captured against the already-recorded baseline.
The protocol is **adaptive**: an anchor is only accepted when its captured features pass a quality gate (coherence ≥ threshold from `coherence_gate.rs`, sufficient SNR vs baseline, no saturation). A failed anchor is re-prompted rather than silently kept — bad anchors poison small models far more than large ones. Total guided enrollment is ~4 minutes of wall-clock, producing 8 clean anchors. This is intentionally far below the "hours of data" that a from-scratch model needs, because the backbone already carries world knowledge; enrollment only teaches *this* room's offsets.
Anchors are persisted as an append-only `EnrollmentSession` (event-sourced, per CLAUDE.md state rules) under `room_id`, so re-enrollment is incremental and auditable.
### 2.3 Stage 3 — feature extraction to labelled records (REUSE + bridge)
Each accepted anchor clip is run through the existing extractor stack, baseline-subtracted per ADR-135, and packaged into an `AnchorFeature` record. No new DSP is invented — this stage is a *bridge*, not a new algorithm.
| Feature group | Source module | Used by specialists |
`AnchorFeature` = `{ room_id, anchor_label, t_epoch_us, embedding: [f32; D] (backbone output), aux: { resp_hz?, doppler_spread, variance, periodicity_score, eigen_coeffs } }`. The backbone embedding is the *shared* representation; `aux` carries the cheap hand-features that let small heads specialise without re-learning DSP.
### 2.4 Stage 4 — the specialist bank (NEW, the core contribution)
A **`SpecialistBank`** is a versioned collection of small models scoped to one `room_id`, persisted as a single RVF cognitive container (`wifi-densepose-ruvector`). Each specialist is a *head* over the frozen backbone embedding, trained from the labelled `AnchorFeature` records via the existing `rapid_adapt.rs` LoRA machinery (test-time/few-shot training, contrastive + entropy losses), **not** a from-scratch network.
| Specialist | Model type | Params (typ.) | Label source | Output |
- **Independently versioned & swappable.** Re-enrolling breathing does not retrain posture. A specialist carries its own `{trained_at, anchor_set_hash, baseline_hash, backbone_rev}`.
- **HNSW prototype storage for the classifiers.** Posture and presence are nearest-prototype lookups in the RVF index — no inference engine, microsecond latency, and new postures are added by inserting a prototype, not retraining.
- **SONA online adaptation.** Each specialist may carry a SONA/MicroLoRA online-adaptation slot (`ruvllm_sona_*` / `microlora` primitives) so it tracks slow drift (furniture moved, seasonal RF change) between full re-enrollments, gated by ADR-135 baseline drift.
- **Teacher–student distillation (optional, offline).** Where a labelled public corpus exists (MM-Fi, Wi-Pose), the ADR-150 backbone acts as teacher to pre-shape a head before per-room fine-tuning, improving cold-start. The *teacher* is global/HF; the *student head* is local.
**Invalidation contract.** The bank stores the `baseline_id` (the baseline UUID) it was trained against. **As implemented**, the runtime marks the bank `STALE` whenever the *current* baseline id differs from the trained one — a conservative trigger that catches re-calibration (room rearranged, AP moved, band changed) because any of those produces a new baseline. A finer **drift-threshold** trigger (mark STALE when ADR-135's per-subcarrier deviation exceeds τ *without* a full re-baseline) is a planned refinement (P6). Either way the runtime prompts re-enrollment rather than emitting silently wrong vitals — the calibration analogue of the #954`DEGRADED` honesty rule: never report confident numbers from an invalid model.
### 2.5 Runtime — mixture of specialists with confidence gating
At inference, the frozen backbone embeds each CSI window once; every specialist consumes that shared embedding and emits `{value, confidence}`. Fusion rules:
- The **anomaly** specialist holds a **veto**: a high anomaly score (physically-impossible signal per `adversarial.rs`, or a coherence-gate `Reject`) suppresses positive vitals/posture output and raises a flag, rather than propagating a hallucinated reading.
- **presence=0** short-circuits breathing/heartbeat/posture to `null` (you cannot have a respiration rate in an empty room).
- Each emitted reading is tagged with the specialist's confidence and the `baseline_hash`/`backbone_rev` provenance, so downstream consumers (sensing-server, MQTT, Home Assistant) can gate on quality — consistent with ADR-135 coherence-gate semantics.
### 2.6 Crate & module layout
New bounded-context crate `wifi-densepose-calibration` (orchestration only; files < 500 lines, typed public APIs, event-sourced sessions — per CLAUDE.md):
```
wifi-densepose-calibration/
src/
lib.rs # public API: CalibrationSystem facade
enrollment.rs # EnrollmentProtocol state machine (Stage 2)
- **Fidelity through specialisation.** Six small calibrated heads beat one oversized general model on the cross-room/cross-subject frontier that ADR-150 quantified — and each runs in microseconds-to-milliseconds, on-device.
- **Privacy by construction.** Only the room-agnostic backbone is public (HF). The environmental fingerprint and the person-specific heads stay local; no video, no skin, no cloud round-trip. This is the core differentiator vs cameras and the convenience differentiator vs wearables.
- **Minutes, not hours.** Because the backbone carries world knowledge, ~4 minutes of clean anchors calibrates a room. Re-enrollment is incremental.
- **Honest degradation.** The `baseline_hash` invalidation + anomaly veto mean an out-of-calibration room reports `STALE`/flagged rather than confidently wrong — the same honesty principle as the firmware `DEGRADED` flag.
- **Composable & cheap to extend.** A new biological signal = a new small head over the same embedding, not a new model.
### 3.2 Negative / risks
- **Backbone dependency.** Every specialist rides on ADR-150's encoder; its quality and revision compatibility (`backbone_rev`) are a single point of leverage. Mitigation: pin `backbone_rev` in each specialist; distillation cold-start reduces sensitivity.
- **Enrollment burden.** 4 minutes is small but non-zero, and anchor quality depends on the operator following prompts. Mitigation: adaptive re-prompting + quality gates; ship sane defaults so a partial bank (presence+posture) works after just the static anchors.
- **Heartbeat is hard.** Sub-mm chest displacement at HR frequencies is near the ESP32-S3 noise floor; the heartbeat specialist will have lower and more variable confidence than breathing. The confidence-gated runtime surfaces this rather than faking it.
- **Per-room storage proliferation.** A bank per room per person; needs a clear RVF lifecycle (list/prune/export) — handled by `bank.rs` versioning and the `room-status` CLI.
### 3.3 Alternatives considered
| Alternative | Verdict | Reason |
|-------------|---------|--------|
| One large general model for all signals | **Rejected** | The ADR-150 evidence: scale overfits rooms/subjects and collapses cross-domain; also slower, costlier, less private. Directly contradicts invariant (A). |
| Cloud training of per-room models | **Rejected** | Violates invariant (B): would ship raw CSI of a person's home/sleep to a server. Local-first is the privacy promise. Federation (ADR-105) is the *opt-in* path for shared improvement, exchanging gradients/deltas, never raw CSI. |
| Skip the backbone; train each specialist from scratch | **Rejected** | Reintroduces the "hours of data" requirement the user vision explicitly rejects, and loses cross-room priors. |
| Fold this into ADR-135 | **Rejected** | ADR-135 is *room* calibration (no humans). This ADR is *human-anchor* enrollment + model training on top of it. Distinct lifecycles, distinct invalidation; kept as separate bounded contexts. |
**Current status (2026-06-10):** Stages 1–5 implemented with *statistical* specialists (threshold/prototype/autocorrelation). 55 tests (35 unit incl. multistatic + 1 full-loop integration + 19 CLI), all passing under qemu-aarch64. **Validation scope is precise:** baseline capture + HTTP API + auth are proven on real CSI (Pi-5 nexmon, 6,813 frames; and an ESP32-S3). The complete `baseline → enroll → train-room → infer` loop is now **proven in-process** on deterministic synthetic CSI (`tests/full_loop.rs`: clean baseline with zero motion flags, 8/8 anchors through the quality gate, 6 specialists trained, JSON bank round-trip, trained-bank inference 18±2 BPM positive / absent negative / foreign-baseline STALE; seed-robust). The one live runtime signal (breathing ~16–31 BPM via `room-watch`) used the *stateless* breathing head, **not** a trained bank; the clean empty-room loop has **not** yet run on-target — the remaining gap is strictly the hardware session (empty room + operator anchors). The four behavioral findings from the full-loop test (z-band squeeze, variance-only presence, ungated hz embedding, heart-band lag-floor leakage) are FIXED and regression-guarded — see the integration doc §7. SOTA-intake decisions affecting this system (geometry conditioning, checkerboard alignment) are recorded in ADR-152. Open refinements: `--source-format adr018v6` (drive from the Pi's own nexmon), phase-based breathing carrier, RVF/HNSW storage, and the ADR-150 frozen HF backbone the specialists would distill from.
Validation per CLAUDE.md: `cargo test --workspace --no-default-features` green; hardware verification on the ESP32-S3 (currently COM8) before any release; witness bundle regenerated if the proof surface changes.
> Big models understand the world. Small ruVector models understand *your room*.
ADR-151 makes that operational: a local-first `baseline → enroll → extract → train` pipeline that turns ~4 minutes of clean human anchors — layered on ADR-135's empty-room fingerprint and ADR-150's Hugging-Face-published invariant backbone — into a versioned bank of tiny, specialised, privacy-preserving models for breathing, heartbeat, restlessness, posture, presence, and anomaly. Specialisation over scale; local heads over a shared base; honest `STALE` degradation over confident error.
A structured survey of the 2025–2026 WiFi human-sensing state of the art was run on 2026-06-10 to answer: *what should RuView integrate next, and does anything published invalidate our current direction?* Every claim below was verified against the primary source by independent adversarial reviewers; **evidence grades distinguish what the papers measured from what they merely claim**. Almost all performance numbers are author-self-reported preprint results — treated here as CLAIMED until reproduced on our hardware.
### 1.1 The five verified findings
**(F1) "Coordinate overfitting" is a named, diagnosed failure mode of camera-supervised WiFi pose — and our ADR-079 pipeline has the exact shape of it.**
PerceptAlign (arXiv [2601.12252](https://arxiv.org/abs/2601.12252), accepted ACM MobiCom 2026) shows that models regressing CSI directly to camera-frame coordinates memorize the deployment-specific transceiver layout; SOTA baselines degrade to >600 mm MPJPE in unseen scenes. Their fix is cheap: a <5-minute calibration using two checkerboards and a few photos to align WiFi and vision in one shared 3D frame, plus **fusing transceiver-position embeddings with CSI features**. Claimed: −12.3% in-domain error, −60%+ cross-domain error. They release the claimed-largest cross-domain 3D WiFi pose dataset (21 subjects, 5 scenes, 18 actions, **7 device layouts**). *Evidence: improvements CLAIMED (preprint w/ MobiCom acceptance); the failure mode itself is corroborated across the cross-domain literature — and independently by our own ADR-150 data (81.63% in-domain vs ~11.6% leakage-free cross-subject torso-PCK).*
**(F2) An external model named "WiFlow" claims 97.25% PCK@20 with 2.23M params and ships everything.**
arXiv [2602.08661](https://arxiv.org/abs/2602.08661) (Apr 2026) — spatio-temporal-decoupled CSI pose, 97.25% PCK@20 / 99.48% PCK@50 / 0.007 m MPJPE, 2.23M parameters (~2.2 MB int8). Code, pretrained weights, and a 360k-sample CSI-pose dataset are public under Apache-2.0 ([repo](https://github.com/DY2434/WiFlow-WiFi-Pose-Estimation-with-Spatio-Temporal-Decoupling), Kaggle dataset). *Evidence: artifact availability MEASURED (verified by direct repo inspection); PCK numbers CLAIMED (5-subject, in-domain, self-collected dataset; hardware unspecified; 15 keypoints vs our 17).* ⚠️ **Name collision:** this is unrelated to RuView's internal WiFlow model. In all RuView docs the external model is referred to as **WiFlow-STD (DY2434)**.
**(F3) For CSI foundation encoders, data scale — not model capacity — is the bottleneck, and the tokenization recipe is now known.**
UNSW's MAE pretraining study (arXiv [2511.18792](https://arxiv.org/abs/2511.18792), Nov 2025) — the largest heterogeneous CSI pretraining run to date (1,320,892 samples, 14 public datasets incl. MM-Fi, Widar 3.0, Person-in-WiFi 3D; 4 devices; 2.4/5/6 GHz; 20–160 MHz) — reports zero-shot cross-domain gains of 2.2–15.7% over supervised baselines, with unseen-domain performance scaling **log-linearly with pretraining data, unsaturated at 1.3M samples**, while ViT-Base adds only 0.4–0.9% over ViT-Small. Optimal recipe: **80% masking ratio, small (30,3) patches** (+4.7% over (40,5) by preserving fine temporal dynamics). *Evidence: MEASURED within-study (ablations verified in body text) but preprint; downstream tasks are classification, NOT pose — pose transfer is a hypothesis. Independently corroborates ADR-150's finding that capacity hurts cross-subject.*
**(F4) Hardware/standards: 802.11bf is finished; Espressif ships official sensing; Wi-Fi 6 AP CSI is reachable.**
- **IEEE 802.11bf-2025** published **2025-09-26** (verified against the IEEE SA record) — sensing standardization is complete for both sub-7 GHz and >45 GHz, with formal sensing setup/feedback procedures. No ESP32 silicon implements it yet. *Evidence: MEASURED (standards-body record).*
- **Espressif `esp_wifi_sensing`** (Apache-2.0, v0.1.x, ESP Component Registry): official CSI presence/motion FSM; esp-csi actively maintained (commit 2026-04-22, verified), CSI confirmed across ESP32/S2/C3/S3/C5/C6/C61. *Evidence: MEASURED (vendor pages + commit log).* ⚠️ A stronger "drop-in compatible with RuView nodes" claim was **REFUTED 0-3** — WiFi-6 parts use a different CSI acquisition config struct.
- **ZTECSITool** (arXiv [2506.16957](https://arxiv.org/abs/2506.16957), [code](https://github.com/WiFiZTE2025/ZTE_WiFi_Sensing)): CSI from commercial Wi-Fi 6 APs at up to 160 MHz / 512 subcarriers (~5–10× ESP32 subcarrier count; the gain is aperture, not per-Hz granularity). Firmware is gated behind a ZTE serial-number approval. *Evidence: capability CLAIMED by the vendor-authored tool paper; code artifact MEASURED.*
**(F5) Nothing in 2025–2026 does full DensePose UV regression from commodity WiFi.** Keypoint pose remains the field's frontier. Three "wireless foundation model" papers were screened out by full-text inspection (HeterCSI = simulated cellular channels only; the NeurIPS-2025 FMCW pilot = mmWave radar, presence-only; arXiv 2509.15258 = survey, no artifacts). *Evidence: MEASURED (absence verified by full-text inspection of the candidates that surfaced; absence of evidence across the whole literature is necessarily weaker).*
### 1.2 What this means for the ADR-151 calibration system
ADR-151's enrollment protocol captures guided human anchors but does **not** record or condition on transceiver geometry. F1 says that omission is precisely the thing that makes camera-supervised (and, plausibly, anchor-supervised) heads layout-brittle. ADR-151's per-room thesis ("teach the room before you teach the model") is *strengthened* by F1 — PerceptAlign is independent evidence that layout must be modeled explicitly — and the fix composes naturally with our Stage-2 enrollment.
ADR-150's masked-CSI-encoder design is *validated* by F3, which also hands us the hyperparameters and the priority call: **collect/aggregate more heterogeneous CSI before scaling the encoder.**
## 2. Decision
Adopt four changes, ordered by effort-vs-gain:
### 2.1 Geometry-condition the calibration system (extends ADR-151 Stage 2) — ACCEPTED
1. **Record transceiver geometry at enrollment.**`EnrollmentProtocol` gains an optional `NodeGeometry` record per node (position estimate, antenna orientation, inter-node distances where known). Stored alongside the room baseline in the bank; schema-versioned so existing banks remain readable.
2. **Fuse geometry embeddings into specialist training.** Where a specialist head consumes the (future, ADR-150) backbone embedding, concatenate a small learned embedding of `NodeGeometry` — the PerceptAlign mechanism, transplanted to our per-room banks. Statistical specialists (current) ignore it; LoRA heads (ADR-151 P6) consume it.
3. **Adopt the two-checkerboard alignment for the camera-supervised path (ADR-079).** When MediaPipe supervision is used, calibrate camera↔WiFi into one shared 3D frame before regression (<5 min, two checkerboards, a few photos). This is the direct defense against F1 for our camera-supervised pipeline. ~~92.9%-PCK@20~~ — *that figure was retracted during measurement (b) (2026-06-10): the surviving holdout shows a constant-output model under an absolute (non-torso) threshold on 69 near-static frames; mean predictor scores 100% under the same protocol. The §2.2 no-citation rule now applies to it.*
4. **Evaluate on the PerceptAlign cross-domain dataset** (21 subjects / 7 layouts) as the MERIDIAN cross-layout benchmark — *gated on confirming its license and downloadability* (open question; repo per paper: github.com/Trymore-lab/PerceptAlign).
> **Gate resolved (2026-06-10, MEASURED by repo inspection):** repo exists, **MIT license**, dataset downloadable from HuggingFace (5 per-scene repos, raw CSI + separate vision keypoints; Intel 5300, 1TX×3RX×3 ant, 57 subcarriers — same order as ESP32 subcarrier counts; Scene3 ships 3 distinct layouts). Code present, no pretrained weights. Benchmark adoption unblocked; dataset-side license terms inherit HF dataset terms (not separately stated — check at download time).
### 2.2 Benchmark against WiFlow-STD (DY2434) — ACCEPTED
Pull the Apache-2.0 weights + 360k-sample dataset; run three measurements: (a) their model on their data (reproduce 97.25% claim), (b) their model fine-tuned on our ESP32 17-keypoint eval set, (c) our internal WiFlow on their dataset (15-keypoint subset mapping). Until (a)–(c) are measured, **no RuView doc may cite 97.25% as a comparable number** — different dataset, subjects, keypoints.
> **Status (2026-06-10, measurement (a) complete — `benchmarks/wiflow-std/RESULTS.md`):** shipped checkpoint REFUTED (0.08% PCK@20 — wrong keypoint normalization, predates published code); released code does not run as published (6 defects, incl. broken package import and an unreachable test phase); released dataset's last 13 files are corrupted (9,072 windows: NaN + float32-max garbage, diverges fp16 training via BatchNorm poisoning). After repairing both, retraining with upstream defaults reproduced **96.09% PCK@20 full-test / 96.61% corruption-free / MPJPE 0.0094–0.0098** (published: 97.25% / 0.007) on an RTX 5080. Accuracy claims graded MEASURED-EQUIVALENT; params (2.23M) and FLOPs (~0.055G) verified. (b)/(c) remain open.
### 2.3 Apply the UNSW recipe to the ADR-150 encoder — ACCEPTED (amends ADR-150 §2.3)
- Pretraining corpus: start from the same 14 public datasets (1.3M samples) + our home/MM-Fi frames; data aggregation takes priority over architecture work.
- Tokenization: 80% masking, (30,3)-class small patches; encoder stays ViT-Small-class (~15M params) — F3 and our own DANN/transformer results agree that capacity does not pay.
- The published log-linear scaling (unsaturated) sets the expectation: more heterogeneous CSI in, better zero-shot out.
### 2.4 Hardware watch items — ACCEPTED (no code now)
- **802.11bf**: track silicon/certification; OTA binding remains deferred until commodity chipsets expose standardized sensing measurements. **Amended by ADR-153** (2026-06-10): implement a pure Rust forward-compatibility protocol layer now — typed procedure models, a deterministic session FSM, a transport abstraction, simulation tests, and an `OpportunisticCsiBridge` that maps today's ESP32 CSI batches into standardized sensing-report shape.
- **esp_wifi_sensing**: benchmark our presence pipeline against the vendor FSM (one afternoon; useful external baseline). Do **not** treat as drop-in (refuted claim).
- **ZTECSITool AP**: optional high-resolution anchor node for the ADR-029 multistatic mesh — procurement-gated; only pursue if a 160 MHz anchor materially helps tomography.
### 2.5 Explicitly NOT adopted
- No pivot toward "wireless foundation model" papers that don't ship WiFi-CSI artifacts (HeterCSI, FMCW pilot, surveys).
- No DensePose-UV work item: the field has not demonstrated UV regression from commodity WiFi; keypoints remain our supervised target (F5).
**Vendor sync record.** `vendor/ruvector` moved from pin `e38347601` (2026-05-07) to `a083bd77f` (origin/main, 3 commits past tag `ruvector-v0.2.28`; vendored workspace version 2.2.3). 111 commits in the range, roughly half NAPI-binary/lint chores. Substantive: graph condensation + differentiable min-cut (#547), core HNSW correctness fixes v2.2.3 (#502), RUSTSEC/clippy hardening (#504), ONNX embedder API-contract fix (#523/#525 — npm/TypeScript package only), dead parallel-worker import removal (#532). *Evidence: MEASURED (git range + commit-stat inspection).*
**Opportunity table.** Workspace policy is crates.io versions only, so unpublished crates are WATCH by definition regardless of fit.
| Crate | What it offers | wifi-densepose target | crates.io | Verdict |
|---|---|---|---|---|
| `ruvector-graph-condense` (new, #547) | Training-free min-cut graph condensation + **differentiable normalized-cut loss** (`DiffCutCondenser`, analytic MinCutPool-style gradients, gradient-checked tests; provenance-retaining super-nodes) | `subcarrier_selection.rs` (condense 114 subcarriers into cut-preserving regions instead of raw min-cut); auxiliary clustering regularizer for `wifi-densepose-train`; `DynamicPersonMatcher` region structure | **Not published** | **WATCH** — strongest technical fit in the sync; adopt when published. README's "no published method uses graph-cut condensation" is CLAIMED; the diffcut implementation + tests are MEASURED |
| `ruvector-attention` 2.1.0 | #304 SOTA modules: MLA, KV-cache, SSM, sparse/MoE, hybrid search, Graph RAG (publish date 2026-03-27 matches the #304 commit — MEASURED) | Supersedes pinned 2.0.4 used by `model.rs` spatial attention + `bvp.rs`; SSM/MLA are candidate pure-Rust edge-inference primitives for the ADR-150 encoder | 2.1.0 (pinned **2.0.4**) | **ADOPT** (minor bump; API-compat check first) |
| `ruvector-cnn` 2.0.6 | Pure-Rust SIMD conv kernels (AVX2/NEON/WASM), MobileNetV3, INT8 quantization, contrastive losses (InfoNCE/triplet, #252) | **Not** the WiFlow-STD training port — `wiflow_std/model.rs` is tch/libtorch (MEASURED). Relevant to the *edge inference* path of the trained ~2.2 MB int8 model, and InfoNCE/triplet overlaps AETHER (ADR-024) | 2.0.6 | **EVALUATE** — only if/when we commit to a no-libtorch edge runtime for WiFlow-STD-class models |
| `ruvector-acorn` (new-ish) | ACORN predicate-agnostic filtered HNSW (SIGMOD'24 algorithm; γ·M denser graphs for low-selectivity filters) | Metadata-filtered pattern search over ADR-151 calibration banks — speculative; bank sizes are far below where filtered-ANN recall collapse matters | **Not published** | **WATCH** |
| `ruvector-cluster` 2.0.6 | Distributed sharding, gossip discovery, DAG consensus | No current need; ADR-029 mesh coordination is ESP32-side, not vector-DB-side | 2.0.6 | **WATCH** |
| ONNX embedder fix (#523/#525) | API-contract + packaging fixes in `npm/packages/ruvector` (TypeScript) | None — `wifi-densepose-nn`'s ONNX backend is Rust (ort/tract), untouched by this change (MEASURED: commit touches npm/ only) | n/a | No action |
| `ruvector-perception` (new, #547) | "Physical perception substrate" (hypothesis/topology/witness modules) — agent-perception oriented, not RF | None identified | Not published | WATCH (name-overlap only) |
**Security note (RUSTSEC #504).** The substantive fixes target `ruvllm`, `ruvector-dag`, `prime-radiant`, `rvagent-*`, and the `ruvector-server` HTTP endpoint (NaN-safe `partial_cmp`, input-validation guards, env-allowlisted exec) — **none of which we pin**. The commit states `cargo audit` returns clean across the workspace. *Evidence: MEASURED (commit message + file list). Conclusion: no pinned version has an outstanding advisory; no urgent bump required.* The NaN-sort hardening is panic-robustness hygiene our pinned 2.0.4-era crates predate, which is one more reason for the routine bumps below.
**Version-bump recommendations (follow-up PR — no Cargo.toml change in this ADR):** `ruvector-mincut` 2.0.4→2.0.6, `ruvector-solver` 2.0.4→2.0.6, `ruvector-attention` 2.0.4→2.1.0, `ruvector-gnn` 2.0.5→2.2.0. Current: `ruvector-core` 2.2.0, `ruvector-attn-mincut` 2.0.4, `ruvector-temporal-tensor` 2.0.6, `ruvector-crv` 0.1.1 — all at latest published. Nothing in the sync changes §2.1.2 geometry conditioning (our `viewpoint/attention.rs``GeometricBias` already implements the fusion mechanism) or the ADR-150 MAE recipe (training stays in tch).
## 3. Consequences
**Positive:** the calibration system gains the one mechanism (geometry conditioning) the 2026 literature identifies as the difference between layout-brittle and layout-robust supervised WiFi pose; ADR-150 gets a measured training recipe instead of a guessed one; we acquire two external benchmarks (WiFlow-STD, PerceptAlign dataset) to keep our claims honest.
**Negative / risks:** geometry records add schema surface to banks (mitigated: optional + versioned); every adopted number is preprint-grade until our own benchmark runs land (mitigated by §2.2's no-citation rule); PerceptAlign dataset license is unconfirmed (gated); name collision risk in docs (mitigated: "WiFlow-STD (DY2434)" naming rule).
**Re-check by 2026-12:** 802.11bf silicon, esp_wifi_sensing maturity (v0.1.x today), and the preprint field (newest source Apr 2026).
## 4. Open questions (carried from the research run)
1. Does WiFlow-STD retain accuracy when fine-tuned on ESP32-S3/C6 CSI (fewer subcarriers, lower SNR), scored on our 17-keypoint set? (§2.2 answers this.)
> **Partial answer (MEASURED 2026-06-11, measurement (b) on 2,046 single-room windows — `benchmarks/wiflow-std/RESULTS.md`):** pretrained init shows strong *optimization* transfer (65% PCK@20 vs scratch's 0% collapse under the same budget) but **no feature transfer** (frozen-trunk + linear adapter ≈ 0%). And no run beat the mean-pose baseline (95.9% PCK@20 — single subject, near-static normalized coords), so no CSI→pose capability is citable from this data. A definitive answer needs multi-subject/multi-position data where the mean pose is weak.
2. Is the PerceptAlign dataset downloadable under a usable license, and does the two-checkerboard procedure work with ESP32 transceiver geometry? (§2.1.4 gate.)
| **Scope** | Milestone 0 of the beyond-SOTA signal/DSP sweep: high-leverage **correctness/security fixes**, two **measured** perf wins, the per-module SOTA landscape with evidence grades, and a prioritized roadmap. **45 review findings were explicitly deferred** (§7 backlog) — **now all addressed across Milestones 0–3** (§7.4 backlog cleared 2026-06-13); nothing was silently dropped. |
---
## 0. PROOF discipline (this ADR's contract)
This project has been publicly accused of "AI slop." This ADR answers that with **evidence, not adjectives**:
- Every claimed code improvement ships with a **committed regression test** (correctness) or a **committed criterion bench** (performance).
- Every perf number below is **MEASURED before/after** with the exact reproduce command. A perf claim without a measured before/after is **UNPROVEN** and is not made here.
- Every external SOTA reference is graded **MEASURED** / **CLAIMED** / **THEORETICAL**, distinguishing what a paper *measured* from what it *asserts* and from what is merely *plausible*.
- The headline finding — a **dead CIR coherence gate that silently fell back in production for every canonical frame** — is disclosed in full (§2), not buried.
Test machine for the perf numbers: Windows 11, `cargo bench --release`, criterion 0.5. Numbers are wall-clock medians on this box; they are about **ratios** (before/after), which are stable across machines, not absolute ns.
---
## 1. Context
The RuvSense signal stack (16 `ruvsense/` modules + the classic `features.rs`/`csi_processor.rs`/`spectrogram.rs`/`bvp.rs` pipeline) grew quickly across ADR-014/029/030/134/135. A beyond-SOTA review surfaced ~50 findings ranging from two **critical correctness/security defects** to micro-optimizations and SOTA-gap research items. Milestone 0 closes the **provable, high-leverage subset**: the two criticals, a divide-by-zero trio, two measured perf wins, and the research landscape. The remaining ~45 are catalogued in §7 so the backlog is explicit and auditable.
---
## 2. The headline finding — the ADR-134 CIR coherence gate was DEAD in production (CRITICAL, FIXED)
### 2.1 What was wrong
`MultistaticFuser` fuses **canonical CSI frames**: `hardware_norm.rs` resamples every chipset onto a uniform **56-tone canonical grid** before fusion (`HardwareNormalizer`, default `canonical_subcarriers = 56`). The ADR-134 CIR coherence gate (`cir_gate_coherence`, multistatic.rs) is supposed to blend a CIR dominant-tap ratio into the cross-node coherence — `coherence = 0.7·freq + 0.3·dominant_tap_ratio`.
But the gate was wired to `CirEstimator::new(CirConfig::ht20())` (`with_cir_ht20`), and `ht20()` expects **64 FFT bins or 52 active tones**. A canonical-56 frame matches *neither*, so every call returned `CirError::SubcarrierMismatch` and `cir_gate_coherence` hit its **silent `Err(_) => freq_coherence` fallback** (multistatic.rs). Net effect: **the CIR gate never ran on a single production frame** — `use_cir_gate = true` was indistinguishable from `false`. This is the exact shape of "AI slop": a feature that compiles, has tests on the *estimator*, and is dead at the *integration seam*.
### 2.2 The fix (the gate now actually runs)
- New `CirConfig::canonical56()` (cir.rs): 64-bin HT20 framing, **56 active tones**, 168 delay taps, Φ built over a contiguous −28..+28 active-tone grid (also the native Atheros-56 layout). `bandwidth_hz`/`tap_spacing` stay physically correct for a 20 MHz HT20 channel; only the active-tone count differs from `ht20()`.
- New `MultistaticFuser::with_cir_canonical56()` — the **correct default** for the RuvSense pipeline. `with_cir_ht20()` is retained for genuine raw-64/52 feeds and now carries a loud doc-warning.
- `active_indices()` handles `(64, 56)` explicitly and the fallback now selects the slice whose length matches `num_active` (so Φ's column count is always self-consistent — no silent fall-through to the 52-index slice).
- The remaining silent fallback is made **LOUD**: a `SubcarrierMismatch` inside `cir_gate_coherence` now fires a `debug_assert!` naming the misconfiguration ("CIR gate DEAD … build it with `CirConfig::canonical56()`"). A *config* error can no longer hide as a graceful runtime degrade.
- `cir_estimate_first()` exposes the raw `estimate()` verdict so a test can **count Ok vs Err** on a canonical-56 stream.
### 2.3 The PROOF (committed regression tests, `ruvsense::multistatic::tests`)
| Test | Asserts | Result |
|------|---------|--------|
| `cir_gate_ht20_is_dead_on_canonical56` | old ht20 estimator on 8 canonical-56 frames → **0 Ok, 8 `SubcarrierMismatch`** | the dead gate, measured |
| `cir_gate_canonical56_is_alive` | new canonical56 estimator on the same 8 frames → **8 Ok, 0 Err** | the gate runs |
| `cir_gate_on_changes_coherence_vs_off` | `coherence(gate on)` ≠ `coherence(gate off)` (\|Δ\| > 1e-6) | the CIR term is actually applied |
cd v2 && cargo test -p wifi-densepose-signal --no-default-features --lib \
ruvsense::multistatic::tests::cir
# 3 passed (the 4th is #[cfg(not(debug_assertions))], add --release to run it)
```
**Resolution: FIXED** (not merely loud-fail-documented). The gate now decodes 100% of canonical-56 frames where it previously decoded 0%.
---
## 3. The second critical — NaN/inf adversarial-detector bypass (CRITICAL, FIXED)
### 3.1 What was wrong
`AdversarialDetector::check` (adversarial.rs) takes per-link `link_energies: &[f64]`. A single **NaN/inf** entry bypassed the whole detector: every `e > threshold` test is `false` on NaN, the Gini sort used `partial_cmp().unwrap_or(Equal)`, and the final `anomaly_score.clamp(0,1)` returns NaN on a NaN input. A real RF link can never have NaN/inf energy, so a non-finite input is *itself* the strongest possible spoof — yet it could slip through as "clean."
### 3.2 The fix
Finite-validate at the boundary: the first non-finite `link_energies` entry now **short-circuits to a definite anomaly** (`anomaly_detected = true`, `anomaly_score = 1.0`, `affected_links = [bad_idx]`, `FieldModelViolation`), and the poisoned frame is **not** seeded into the temporal-continuity state.
### 3.3 The PROOF
| Test | Asserts |
|------|---------|
| `nan_link_energy_flags_anomaly` | a NaN link energy → `anomaly_detected`, score 1.0, affected link reported, `anomaly_count == 1` |
| `inf_link_energy_flags_anomaly` | both `+inf` and `−inf` → anomaly, score 1.0 |
```bash
cd v2 && cargo test -p wifi-densepose-signal --no-default-features --lib \
| `bvp.rs` Hann window | `window_size=1` divided by 0 → NaN BVP | length-1 guard → constant `[1.0]` |
| `spectrogram.rs``make_window` | `size=1` divided by 0 for Hann/Hamming/Blackman | `size <= 1` short-circuit → `vec![1.0; size]` |
The standard convention for a length-1 window is the constant `1.0`; length-0 is empty.
**PROOF:** `test_hamming_window_degenerate_sizes` (csi_processor), `bvp_window_size_one_is_finite` (bvp), `make_window_size_0_and_1_are_safe` (spectrogram) — each asserts finiteness at sizes 0/1/2.
The Python deterministic proof (`archive/v1/data/proof/verify.py`) still prints **VERDICT: PASS** with the **same** pipeline hash `f8e76f21…46f7a` — the reference path uses `n ≥ 2`, so the guard is bit-transparent there.
Both changes are **bit-equivalent** (asserted by a committed test) — they only remove wasted work. New criterion benches in `benches/features_bench.rs` (registered in `Cargo.toml`).
**Reproduce both:**
```bash
cd v2 && cargo bench -p wifi-densepose-signal --no-default-features --bench features_bench
# compile-only: append --no-run
```
### 5.1 FFT-planner caching for PSD (features.rs)
`PowerSpectralDensity::from_csi_data` constructed a fresh `FftPlanner` and re-planned the FFT **on every frame** — and `FeatureExtractor::extract` calls it per frame on the hot path. New `from_csi_data_with_fft(csi, fft_size, &Arc<dyn Fft>)` reuses a plan cached in `FeatureExtractor` (built once in `new()`). Output is **bit-identical** (`psd_cached_fft_bit_identical_to_fresh` compares `f64::to_bits` of values + all summary stats across 6 FFT sizes).
Bench group `psd_fft_planner` — `fresh_planner` (before) vs `cached_planner` (after), per frame:
| fft_size | before (fresh plan), median | after (cached), median | speedup |
The win is the re-planned `FftPlanner` construction the cache hoists out of the per-frame loop; it grows in *relative* terms at small FFTs (planning is a larger fraction of a cheap transform) and stays a flat ~2× at 256.
### 5.2 DTW Sakoe-Chiba band honored (gesture.rs)
`dtw_distance` computed the band bounds `j_start/j_end` but still iterated the **full**`1..=m` row, `continue`-ing on out-of-band cells — so the band constrained the *path* but not the *work* (still O(n·m)). The fix iterates only `j_start..=j_end` (O(n·band)), resetting just the two boundary-guard cells the recurrence can read, and computes the endpoint reachability (`|n−m| ≤ band`) at the return site. Result is **bit-identical** to the full-row version across 12 shapes × 8 band widths (`dtw_banded_bit_identical_to_fullrow`).
Bench group `dtw_sakoe_chiba` — `full_row` (before) vs `banded` (after):
| case | before (full row), median | after (banded), median | speedup |
The speedup tracks the inner-loop cell-count ratio `m / (2·band+1)` — n=m=200, band=5 → 200/11 ≈ 18× fewer cells, but euclidean-distance cost and loop overhead dominate at these sizes so the wall-clock win is ~4× (still the **largest at the longest sequence / narrowest band**, exactly as the algorithm predicts). It shrinks toward 1× as the band widens to cover the whole matrix (band=10 → 2.66×), and grows with sequence length (band=5: 2.43× at n=100 → 4.14× at n=200).
> **Note on the other re-plan sites.**`spectrogram.rs`/`bvp.rs` plan their FFT **once per call** and reuse it across all frames/subcarriers (already amortized), so caching there is marginal — deferred (§7). The PSD site was the only one re-planning *per frame*.
---
## 6. Per-module SOTA landscape (evidence-graded)
Grades: **MEASURED** (the source measured it, ideally with public method/code), **CLAIMED** (asserted, no reproducible artifact), **THEORETICAL** (plausible, no published target).
- **Deep-unfolded ISTA / LISTA for CSI→CIR — MEASURED.** Learned ISTA unrolling reports ~**3 dB NMSE** improvement over classical OMP/FISTA for channel/CIR estimation (arXiv [2211.15440](https://arxiv.org/abs/2211.15440); survey [2502.05952](https://arxiv.org/abs/2502.05952)). Public methods; numbers measured in-paper. **This is our #1 future item (§7) — our `cir.rs` already builds the sub-DFT Φ that LISTA would make trainable.**
- **Diffusion CIR prior — MEASURED (artifact).** [github.com/benediktfesl/Diffusion_channel_est](https://github.com/benediktfesl/Diffusion_channel_est) ships **public weights** for a diffusion-model channel-estimation prior. Heavier than our edge budget; tracked, not adopted.
- **Coherence gating (the §2 gate) — THEORETICAL.** Our 0.7/0.3 freq/CIR blend is an engineering heuristic with no published accuracy target; now that it *runs*, it can finally be A/B-measured.
### 6.2 Adversarial robustness (adversarial.rs)
- **Adversarial-robustness eval for WiFi sensing — MEASURED.** arXiv [2511.20456](https://arxiv.org/abs/2511.20456) + the **Wi-Spoof** benchmark provide a measured evaluation protocol for spoofed/injected CSI. Our detector's physical-plausibility checks (consistency/Gini/temporal/energy) are in the same spirit; adopting Wi-Spoof as an external benchmark is a §7 item. (The §3 NaN fix is a precondition: a detector that NaN-bypasses can't be benchmarked honestly.)
- **Bayesian multi-AP fusion — CLAIMED.** arXiv [2512.02462](https://arxiv.org/abs/2512.02462) proposes a Bayesian fusion across APs; **no code released**, numbers self-reported. Our attention-weighted fusion is a different (cheaper) mechanism; tracked as a comparison target, not adopted.
The 200–500 ms pre-movement "lead signal" framing has **no published commodity-WiFi target** we can grade. Honestly THEORETICAL; no work item.
---
## 7. Decision, roadmap, and the deferred-findings backlog
### 7.1 Accepted now (this milestone)
The §2–§5 fixes are **ACCEPTED and committed**: dead CIR gate fixed, NaN bypass fixed, window trio fixed, calibration dead-branch de-misled, two measured perf wins. All `cargo test -p wifi-densepose-signal --no-default-features` (and `--features cir`) green; Python proof PASS.
### 7.2 Top accepted-future item — LISTA-for-CIR (NOT implemented here)
**Unroll the existing ISTA in `cir.rs` into trainable layers (LISTA).** Effort: **M**. The sensing matrix Φ and the ISTA recurrence already exist; LISTA replaces the fixed step size / threshold with per-layer learned parameters over a fixed unroll depth. Measured target to beat: **~3 dB NMSE over OMP/FISTA** (arXiv 2211.15440 — MEASURED). Proposed, not built in Milestone 0.
### 7.3 Other graded-future items
- Adopt **Wi-Spoof** (arXiv 2511.20456, MEASURED) as the external adversarial benchmark for `adversarial.rs`.
- Evaluate the **diffusion CIR prior** (public weights, MEASURED) as an offline quality ceiling — *not* an edge target.
Catalogued so nothing is silently dropped. Priority: **P1** correctness-adjacent, **P2** perf, **P3** clarity/style.
**Milestone-1 update (2026-06-13):** the **four P1 backlog items** (#1, #9, #10, #13) are now cleared — #1 and #10**RESOLVED (MEASURED)**, #9 and #13**RESOLVED-PARTIAL (DATA-GATED:** de-magicked + boundary-tested, operating values unchanged**)**. Each fix is pinned by a regression test that fails on the old behaviour (commits `fd32f094a`, `4a9f2bcf4`, `d672fa602`, `5193f6369`); workspace `--no-default-features` green, Python proof unchanged (bit-exact).
**Milestone-2 update (2026-06-13):** the **bench-first P2 perf subset** (#5, #6, #7, #8, #20) and the **three missing boundary tests** (#14, #16, #19) are now cleared — ~36 P2/P3 items remained deferred *(now cleared — see the Milestone-3 update)*. PROOF discipline (§0): every perf item was **benched before being touched** — committed in `benches/dsp_perf_bench.rs` (criterion, this Windows box). Only **#20** proved hot and was optimized; **#5/#6/#7** are committed **MEASURED-NULLs** (benched, not hot, left as-is for clarity — exactly the §5.1 "already amortized" pattern); **#8** is **MEASUREMENT-ONLY** but its `eigenvalue`/BLAS backend won't build on this Windows host, so its µs cost must come from a Linux/BLAS box (recorded, not fabricated). Commits `e839fa8f1` (#20 fix), `02e5dd13a` (#14/#16/#19 tests), `aad9464f0` (benches). Workspace `--no-default-features` green; Python proof unchanged (#20 is bit-identical, off the proof path).
**Milestone-3 update (2026-06-13):** the lumped **row #21–45** P3 backlog — *"remaining clarity/doc/magic-constant/missing-boundary-test findings across `ruvsense/*`, `features.rs`, `motion.rs`"* — is now **cleared, and with it the residual P3 items #2/#12/#17/#18.** Honest enumeration first (`grep`, not the ADR's "21–45" estimate — that was a count, not 25 distinct findings): after M0–M2 the genuinely-bare in-function literals resolved to **22 de-magicked constants across 11 modules** (each → a named, documented **EMPIRICAL-DEFAULT** const that **equals the prior literal exactly**), **6 added boundary/characterization tests**, **~4 doc-only fixes** (no-behaviour-change), and **a handful of agent-flagged "findings" that were NOT real** and are reported as skipped (below). **No operating value or behaviour changed** — every module carries a `*_consts_unchanged_from_literals` pin test and every boundary test pins *current* behaviour, so a future retune is a visible, tested change. Resolution by module: `motion.rs` (**#18** — fusion weights / Doppler+variance+phase scales / confidence weights / adaptive-threshold clamp; 5 tests), `gesture.rs` (**#12** — `euclidean_distance` length-mismatch `debug_assert` documenting the silent-`zip`-truncation caller contract, behaviour-preserving in release; + confidence epsilon; + DTW n=0/m=0 boundary), `longitudinal.rs` (7-day/2σ/3-day/7-day drift thresholds + EMA-α + cosine epsilon; day-6/7 + zero-vector boundaries; the duplicated `>=7` deduped), `cross_room.rs`/`multiband.rs`/`intention.rs`/`hampel.rs` (**#17** — division-guard epsilons `1e-9`/`1e-12`/`1e-10`/`1e-15` + zero-norm/zero-variance/zero-MAD boundaries + the previously-untested `hampel half_window==0` error path + `# Errors` doc), `rf_slam.rs` (`NS_PER_DAY` + `MIGRATION_MIN_SPAN_DAYS` + fixed-map defaults; single-sighting zero-span guard), `attractor_drift.rs` (`METRIC_BUFFER_CAPACITY`/`STABLE_CENTER_WINDOW`; **documented** the implicit `recent.len()>=1` divide-safety; `min_observations` off-by-one boundary), `coherence.rs` (**#9 completion** — the residual bare `1e-6` variance-floor ×4 + default `0.95` decay; floor-effect test), `calibration.rs` (**#2 completion** — `DEFAULT_MIN_FRAMES` deduped across all 4 tier constructors + `AMP_STD_FLOOR`/`MOTION_AMP_Z_THRESHOLD`/`MOTION_PHASE_DRIFT_THRESHOLD`/`SUBTRACT_MIN_NORM`), `fusion_quality.rs` (`CONTRADICTION_PENALTY` 0.8 / bound-halfwidth 0.1; n=0 identity boundary), `temporal_gesture.rs` (confidence epsilon + L2-norm quantization scale). **NOT-REAL / skipped (reported honestly, no churn manufactured):** an agent-flagged `attractor_drift.rs:301` "divide-by-zero" is **unreachable** — the `count < min_observations` guard guarantees `recent.len()>=1` before the `PointAttractor` branch (documented + boundary-tested, **not** guarded, per the no-behaviour-change rule); agent-flagged `gesture.rs` `2.0`/`π·6` motion thresholds **do not exist** in that file (a confusion with `calibration.rs::deviation`); **`features.rs` was deliberately left untouched** (it is on the deterministic Python-proof PSD/Doppler path — its `1e-10` guards already exist and are already correct; doc-only-skipped to protect the bit-exact hash). Commits `c794d1a0c` (motion #18), `adf9ed8e4` (gesture #12), `19f5b6335` (longitudinal), `19e0373c8` (epsilon helpers #17), `c6a09b69a` (rf_slam + attractor_drift), `5a1839f33` (coherence #9 completion), `df25a303e` (calibration #2 completion), `0f931ff2f` (fusion_quality + temporal_gesture). Signal crate lib `--no-default-features` **476 passed / 0 failed / 1 ignored**; `--no-default-features --features cir` **476 / 0**; workspace `--no-default-features` **3,275 / 0 failed** (single clean run); Python proof **VERDICT: PASS**, hash `f8e76f21…46f7a` **UNCHANGED (bit-exact)**. **§7.4 backlog is now fully cleared — ADR-154's deferred findings are addressed across M0–M3 with nothing silently dropped.**
| # | Module | Finding | Pri | Why deferred |
|---|--------|---------|-----|--------------|
| 1 | cir.rs ~937 | `phase_variance` uses **linear** variance on **wrapped** angles (doc says "variance of phase angles") — spuriously inflates near ±π | P1 | **RESOLVED (`fd32f094a`) — metric MEASURED, threshold DATA-GATED.** Replaced with Mardia's circular variance V = 1 − R̄ ∈ **[0,1]**, invariant to the cluster's position on the circle (branch-cut artefact gone). Guard re-derived against the bounded metric via named const `GHOST_TAP_CIRCULAR_VARIANCE_MAX = 0.99` (fires only when R̄ ≤ 0.01 — essentially uniform phase). The **threshold value is DATA-GATED**: a clean single-path ramp also sweeps the circle, so V alone can't separate clean from unsanitized without labelled frames — the default is deliberately conservative (strictly more permissive at the wrap boundary than the buggy linear guard). Fails-on-old: `phase_variance_circular_not_fooled_by_branch_cut` (old linear variance > TAU on wrap-straddling phases while circular V≈0, guard no longer trips), `phase_variance_circular_is_bounded_and_extremal`. |
| 2 | calibration.rs ~311 | `subtract_in_place` had a vacuous `if active_input {ki} else {ki}` branch implying a full-FFT→bin remap that didn't exist | P3 | **Resolved (M0 + M3 `df25a303e`).** Branch removed in M0 (sequential-convention documented). M3 completed the de-magic: `DEFAULT_MIN_FRAMES=600` deduped across all four tier constructors, plus `AMP_STD_FLOOR`/`MOTION_AMP_Z_THRESHOLD`/`MOTION_PHASE_DRIFT_THRESHOLD`/`SUBTRACT_MIN_NORM` named + `calibration_consts_unchanged_from_literals`. Behaviour unchanged. |
| 3 | spectrogram.rs / bvp.rs | FFT planner built once-per-call (already amortized across frames) | P2 | Marginal vs the per-frame PSD site; cache if these become hot. |
| 4 | features.rs ~347 | Doppler FFT planner planned once per call, reused across subcarriers | P2 | Already amortized within the call. |
| 5 | multistatic.rs | `node_attention_weights` recomputes consensus/softmax each call; no SIMD | P2 | **MEASURED-NULL (`aad9464f0`) — benched, not hot, left as-is.**`multistatic_attention/weights`: **181 ns** (2 nodes) … **848 ns** (8 nodes) @ 56 subcarriers — sub-µs, no hot-path allocation. A precompute/SIMD rewrite buys nothing measurable at the realistic 2–8 node fan-in; the cosine/softmax cost is dwarfed by the surrounding fusion + per-frame FFT. Bench `multistatic_attention` in `dsp_perf_bench.rs`. |
| 6 | tomography.rs | ISTA L1 solver re-allocates voxel buffers per solve | P2 | **MEASURED-NULL (`aad9464f0`) — benched, not hot, left as-is.** A full 50-iteration `reconstruct` (256 voxels): **47.5 µs** (16 links) / **60.4 µs** (32 links). The two voxel buffers (`x`, `gradient`; ~4 KB) are already allocated *once* per `reconstruct()` and `.fill`-reused across iterations — the per-solve alloc is a negligible fraction of the O(iters·links·voxels) inner product. Reusing scratch across *calls* would force `reconstruct(&self)`→`&mut self` (API break) for no measurable gain. Bench `tomography_reconstruct`. |
| 7 | pose_tracker.rs | Kalman gain matrices reallocated per update | P2 | **MEASURED-NULL (`aad9464f0`) — benched, not hot, left as-is.** A Kalman predict+update cycle: **150 ns** (17 keypoints) / **2.82 µs** (170). The "gain matrices" (`s:[f32;3]`, `k:[[f32;3];6]`) are fixed-size **stack** arrays, *not* heap — there is no per-update allocation to reuse; the compiler keeps them in registers/stack. Bench `pose_kalman_update`. |
| 8 | field_model.rs | SVD recomputed on every perturbation extract | P2 | **MEASUREMENT-ONLY (`aad9464f0`) — BLAS-gated, not measurable on this host.** Correction: `extract_perturbation` does **not** recompute the SVD — it projects against the cached `modes` from `finalize_calibration`. The real per-call eigendecomposition is in the `eigenvalue`-feature `estimate_occupancy` (`cov.eigh()` on a 56×56 covariance, an O(n³)≈175k-flop symmetric eigensolve + O(n²·frames) covariance build, run per call). The bench (`dsp_perf_bench`'s `eig` module) is committed, but `openblas-src`**fails to build on this Windows box** ("Non-vcpkg builds are not supported on Windows" — the very reason the project gate runs `--no-default-features`), so a measured µs number must come from a Linux/BLAS host; **not estimated/fabricated here.** Incremental SVD remains a sized future project, not a micro-fix. |
| 9 | coherence.rs / coherence_gate.rs | Z-score thresholds are magic constants, untested at boundaries | P1 | **RESOLVED-PARTIAL (`5193f6369`) — DATA-GATED.** De-magicked `classify_drift` (`DRIFT_STABLE_SCORE=0.85`, `DRIFT_STEP_CHANGE_MAX_STALE=10`) and the `coherence_gate.rs` defaults (`DEFAULT_ACCEPT_THRESHOLD`/`…REJECT…`/`…MAX_STALE_FRAMES`/`…PREDICT_ONLY_NOISE`) into named, documented consts marked EMPIRICAL DEFAULT; added at/just-below/just-above boundary tests (`classify_drift_*_boundary`) + `*_consts_unchanged_from_literals`. **Operating values explicitly NOT changed** — defensible values still need labelled stable/drifting traces. The gate already exposed these via `GatePolicyConfig` (config seam). |
| 10 | longitudinal.rs | Welford update not numerically guarded for n=0 | P1 | **RESOLVED (`4a9f2bcf4`) — MEASURED.** The shared `WelfordStats` (`field_model.rs`, consumed by longitudinal.rs) `count < 2` guards already prevent the n=0 NaN / n=1 div0 / `(count−1)` underflow, but the boundary was untested. Added `welford_finite_at_n0_and_n1` (finite + documented 0.0 sentinel at n=0/n=1). Fails-on-old proof: removing the `sample_variance` guard makes the test panic with "attempt to subtract with overflow" at the `(count − 1)` underflow. |
| 12 | gesture.rs | `euclidean_distance` no length-mismatch guard | P3 | **RESOLVED (M3 `adf9ed8e4`).** Added a `debug_assert_eq!` on the two slice lengths + a doc block stating the same-`feature_dim` caller contract and that `zip()` silently truncates on a mismatch. Behaviour-preserving (no-op in release, the operating path). Also de-magicked the confidence `1e-10` epsilon and pinned the DTW `n=0`/`m=0` boundary (`dtw_empty_sequence_is_infinite`). |
| 13 | adversarial.rs | Gini/consistency thresholds are magic constants | P1 | **RESOLVED-PARTIAL (`d672fa602`) — DATA-GATED.** Lifted the bare literals in `check`/`check_consistency` (`FIELD_MODEL_GINI_VIOLATION=0.8`, `ENERGY_RATIO_HIGH_VIOLATION=2.0`, `ENERGY_RATIO_LOW_VIOLATION=0.1`, `CONSISTENCY_ACTIVE_FRACTION_OF_MEAN=0.1`, `SCORE_W_*`) into named, documented consts marked EMPIRICAL DEFAULT; added at/just-below/just-above boundary tests (`energy_ratio_high_boundary`, `energy_ratio_low_boundary`, `field_model_gini_boundary`, `consistency_active_fraction_boundary`) + `tuning_consts_unchanged_from_literals`. **Operating values explicitly NOT changed** — defensible values still need labelled spoofed/clean CSI (Wi-Spoof, §6.2/§7.3). Bumping a const fails a boundary test (verified). |
| 14 | cir.rs | `fft_operator` path changes the witness hash (documented) — no test that it's *numerically close* to dense | P2 | **RESOLVED (`02e5dd13a`) — tolerance test added.**`fft_operator_within_tolerance_of_dense_canonical56` pins the **full `Cir` output** of the FFT path within a *documented* relative tolerance of the dense path on the production **canonical-56** config across τ ∈ {20,50,90} ns: every tap within `1e-2·|dominant|`, identical `dominant_tap_idx`, `active_tap_count`, `ranging_valid`, `dominant_tap_ratio` within `1e-2`, `rms_delay_spread` within `1e-2` rel. A regression that lets the FFT path drift (scaling/Φ-column bug) now fails here instead of silently corrupting a downstream witness. Extends the existing HT20/single-τ `fft_estimate_matches_dense_dominant_tap`. |
| 15 | multistatic.rs | `cir_gate_coherence` only estimates the **first** node/channel; multi-node CIR consensus unused | P2 | Design item (which node's CIR is authoritative?). |
| 16 | phase_align.rs | Iterative LO offset estimation has no convergence cap test | P2 | **RESOLVED (`02e5dd13a`) — cap test added.**`refinement_terminates_at_iteration_cap_when_not_converging` forces non-convergence (`tolerance = 0.0`, unreachable since `max_update ≥ 0`) and asserts the loop runs **exactly `max_iterations`** then returns — proving the cap (not convergence) bounds the loop, so a non-converging input can never spin forever. Companion `refinement_converges_before_cap_on_easy_input` proves the cap is an upper bound, not the only exit. Internal-only refactor: `estimate_phase_offsets` still returns the identical offset vector; a `…_counted` core surfaces the iteration count for the test. |
| 17 | hampel.rs | Window edge handling at series boundaries | P3 | **RESOLVED (M3 `19e0373c8`).** De-magicked the zero-MAD `1e-15` epsilon (`ZERO_MAD_EPSILON`), documented `hampel_filter`'s `# Errors`, and added the previously-untested `half_window == 0` error-path boundary (`test_zero_half_window_error`) + a zero-MAD constant-window characterization (`test_zero_mad_constant_window`). Window-edge handling itself is correct (`saturating_sub`/`.min(n)`); it is now pinned. |
| 19 | csi_ratio.rs | Division guard relies on `1e-12` epsilon; no test | P2 | **RESOLVED (`02e5dd13a`) — boundary test added.** Finding clarification: `csi_ratio.rs` implements the CSI *ratio model* as the **conjugate product**`H_i·conj(H_j)` (SpotFi/IndoTrack) — there is **no division**, hence no literal `1e-12` epsilon; the classic `H_i/H_j` ratio (which a `1e-12` guard protects) is deliberately avoided. `ratio_finite_at_and_below_1e_12_epsilon` pins the property the finding cares about: at and below the `1e-12` target magnitude (and at exact zero — where a division ratio is ±inf/NaN) the conjugate-product output is **finite**, exactly the conjugate product (bit-exact), collapses toward zero (the physically correct "no path" answer), and stays finite through `ratio_to_amplitude_phase`. |
| 20 | spectrogram.rs | `compute_multi_subcarrier_spectrogram` re-plans per subcarrier via `compute_spectrogram` | P2 | **MEASURED-HOT (`e839fa8f1`) — optimized, bit-identical.** Hoisted the FFT plan + window out of the per-subcarrier loop (new `compute_spectrogram_with_plan` core). **56-subcarrier** multi-spectrogram: **467.88 µs → 254.75 µs = 1.84×** (window 128); **627.27 µs → 448.39 µs = 1.40×** (window 256). The removed cost is the per-subcarrier `FftPlanner` re-plan (~1.86 µs/plan @ w128 × 56). Bit-identical (`multi_subcarrier_hoisted_plan_bit_identical`, `f64::to_bits` across all 4 windows × {power,magnitude}). The most likely real win predicted by the §7.4 intro — confirmed. (Relates to #3, which stays deferred: `spectrogram.rs`/`bvp.rs` single-signal callers already plan once-per-call.) |
| 21–45 | (assorted) | Remaining clarity/doc/magic-constant/missing-boundary-test findings across `ruvsense/*`, `features.rs`, `motion.rs` | P3 | **RESOLVED (Milestone-3, 2026-06-13).** Enumerated honestly (the "21–45" was an estimate, not 25 distinct findings): **22 bare in-function literals de-magicked → named EMPIRICAL-DEFAULT consts (each == prior literal, pinned)**, **6 boundary/characterization tests added**, **~4 doc-only fixes**, across 11 modules (`motion`, `gesture`, `longitudinal`, `cross_room`, `multiband`, `intention`, `hampel`, `rf_slam`, `attractor_drift`, `coherence`, `calibration`, `fusion_quality`, `temporal_gesture`). **No operating value changed.****Skipped-as-not-real (reported, no churn):**`attractor_drift.rs:301` "divide-by-zero" is unreachable (guarded by `count < min_observations`) → documented + boundary-tested, not guarded; agent-flagged `gesture.rs``2.0`/`π·6` motion thresholds don't exist there (confusion with `calibration::deviation`); **`features.rs` left untouched** (on the deterministic Python-proof path; its `1e-10` guards already exist & are correct — doc-only-skipped to keep the `f8e76f21…` hash bit-exact). See the Milestone-3 update note above and the per-row #2/#12/#17/#18 entries. |
> **Horizon-ledger one-liner.** Milestone-0 DONE: dead CIR gate (FIXED+proved), NaN/inf adversarial bypass (FIXED+proved), divide-by-(n−1) window trio (FIXED+proved), calibration dead-branch (FIXED), PSD FFT-planner cache (MEASURED), DTW band (MEASURED). **Milestone-1 DONE (2026-06-13): all four P1 backlog items cleared — circular phase variance #1 (RESOLVED/MEASURED metric, DATA-GATED threshold), Welford n=0 guard #10 (RESOLVED/MEASURED), threshold magic-constants #9 & #13 (RESOLVED-PARTIAL/DATA-GATED — de-magicked + boundary-tested, values unchanged).****Milestone-2 DONE (2026-06-13): bench-first P2 perf subset + missing boundary tests cleared — spectrogram per-subcarrier FFT re-plan #20 (MEASURED-HOT, 1.40–1.84×, bit-identical); attention/tomography/Kalman #5/#6/#7 (MEASURED-NULL — benched, not hot, left as-is); field_model eigendecompose #8 (MEASUREMENT-ONLY, BLAS un-buildable on this Windows host, number deferred to a BLAS box, NOT fabricated); fft_operator tolerance #14, phase-align convergence-cap #16, csi-ratio epsilon #19 (RESOLVED, tests added).****Milestone-3 DONE (2026-06-13): the lumped §7.4 row #21–45 P3 backlog cleared, and with it residual P3 items #2/#12/#17/#18 — 22 magic constants de-magicked into named EMPIRICAL-DEFAULT consts (each pinned == prior literal) + 6 boundary/characterization tests across 11 modules; ~4 doc-only; not-real findings (unreachable attractor_drift div0, non-existent gesture thresholds, proof-path features.rs) reported + skipped, no churn; no operating value changed; workspace 3,275/0, Python proof bit-exact `f8e76f21…`.****§7.4 deferred backlog is now FULLY CLEARED across M0–M3 — nothing silently dropped.**
> **Sibling-crate sweep extension (2026-06-14) — `wifi-densepose-geo` + `wifi-densepose-pointcloud`.** The ADR-154-class numerical-robustness sweep (non-finite-input-poisons-persistent-state + divide-by-zero / asin-domain / degenerate-geometry) was extended to two crates *outside* this ADR's signal scope. **Two real `geo` bugs FIXED, each fails-on-old-pinned:**`terrain.rs::parse_hgt` usize-underflow panic on empty/sub-2x2 SRTM data (`1.0/(side-1)` → panic in debug / inf `cell_size_deg` poisoning `ElevationGrid::get` in release — a truncated download / 404 HTML body reaches it; now `bail!`s when `side < 2`); `coord.rs::haversine``asin(>1)→NaN` for near-antipodal points (`h` rounds to `1.0+4e-16`; clamped to `[0,1]`). The ±90° pole `cos(lat)=0` ENU singularity is pinned no-panic without changing the transform. **`pointcloud` is confirmed-robust (no manufactured finding):** its only persistent auto-accumulating state (`occupancy` EMA + vitals) is fed solely by the integer-rssi/`sqrt`/`atan2` parser (always finite) and is provably self-healing even under an adversarial NaN/inf `CsiFrame` (`motion_score=(NaN/100).min(1.0)→1.0`; breathing `→0→clamp(5,40)→5.0`) — pinned by `nonfinite_frame_does_not_poison_persistent_state` + degenerate-voxel-fusion no-panic tests. `geo` 9→15 lib / 8 integration; `pointcloud` 18→22; 0 failed; workspace green; Python proof bit-exact `f8e76f21…`. See CHANGELOG `[Unreleased] → Fixed`.
---
## 8. Consequences
- **Positive:** the ADR-134 CIR gate is alive for the first time in production; the adversarial detector can no longer be NaN-bypassed; three latent divide-by-zero NaN sources are gone; the per-frame PSD path and gesture DTW are measurably faster with bit-identical output; the SOTA landscape and a concrete LISTA-for-CIR roadmap are graded and recorded.
- **Negative / honest limits:**`canonical56()` models the canonical grid as a contiguous 56-tone band — a reasonable physical interpretation of a *resampled* grid, but not a literal hardware tone map; the CIR gate still uses only the first node's CIR (#15). The `phase_variance`**metric** is now correct (Mardia circular variance, Milestone-1 #1), so the branch-cut false-trip is gone — but its ghost-tap **threshold** (`GHOST_TAP_CIRCULAR_VARIANCE_MAX = 0.99`) is a conservative DATA-GATED default, not a calibrated operating point, and still awaits labelled sanitized/unsanitized frames to tune. Likewise the de-magicked coherence/adversarial thresholds (#9/#13) keep their pre-existing empirical values pending labelled calibration.
- **Neutral:** no public API removed; `with_cir_ht20()` kept (warned); files stay scoped; new bench is additive.
| **Scope** | Milestone 1 of the beyond-SOTA NN/training sweep: the **integrity-critical** fixes that let the training/metrics subsystem substantiate a clean accuracy claim (the unified metric, leak-free validation, honest TTA, rigorous proof), a focused set of **correctness/security** fixes, two **measured** perf wins, the NN SOTA landscape with evidence grades, and a prioritized backlog. **~45 review findings are explicitly deferred (§8)** — nothing is silently dropped. |
---
## 0. PROOF discipline (this ADR's contract)
This project has been publicly accused of "AI slop." Milestone 1 is the **most integrity-critical** of the sweep because a gap review found the training/metrics subsystem **could not substantiate a clean accuracy claim**: there were four divergent PCK implementations and three divergent OKS implementations, a model trained on real data was validated against a *synthetic* set, the dataset had no leak-free split, the test-time-adaptation path descended a *fake* gradient, and the deterministic proof self-certified on any loss decrease (including float noise) with no committed baseline.
We answer that with **evidence, not adjectives**:
- Every integrity fix ships with a **committed regression test that would have caught the bug**.
- Every perf number is **MEASURED before/after** with the exact reproduce command. A perf claim without a measured before/after is **UNPROVEN** and is not made here.
- Every external SOTA reference is graded **MEASURED** / **CLAIMED** / **THEORETICAL**.
- We disclose, in full, what the proof does **not** prove and what remains unmeasured.
### Build/test constraint (disclosed)
The reportable-metric code (`metrics.rs`, `trainer.rs`, `proof.rs`, `model.rs`, `losses.rs`) is gated behind the `tch-backend` Cargo feature (libtorch FFI). libtorch is **not installed on the development host**, so the project's standard gate is `cargo test --workspace --no-default-features` (no tch). The canonical-metric *logic* is therefore validated two ways: (1) the non-tch reachable surface (`compute_pck`/`compute_oks` free functions, `dataset.rs` split, `rapid_adapt.rs`, `ruview_metrics.rs`) runs under the workspace test suite with new regression tests; (2) the `tch`-gated accumulator/trainer/proof changes are routed through those same canonical functions, so the metric definition is identical whether or not tch is present. This limitation is disclosed rather than hidden.
---
## 1. Context — the seven divergent metric definitions
The gap review found **four** PCK and **three** OKS implementations that disagreed on normalization, on the zero-visible-joint case, and on the OKS scale:
Two of these are not merely inconsistent, they are **wrong in a claim-inflating direction**:
- **The `MetricsAccumulator` zero-visible-joint bug** scored a sample with *no visible joints* as PCK = 1.0 ("no errors to measure"). An empty or garbage prediction could thus *inflate* the reported metric.
- **The OKS `s = 1.0`-on-normalized-coordinates bug** ("fake Gold tier"): with keypoints in `[0,1]` and the scale fixed at `1.0`, every squared distance is ≈0 and the exponential kernel returns ≈1.0 for *any* pose. OKS looked near-perfect regardless of prediction quality.
This is the same metric-bug class ADR-152 flagged. Milestone 1 closes it for real.
### 2.1 Unify the metrics — ONE canonical definition — ACCEPTED & IMPLEMENTED
There is now exactly **one** PCK and one OKS that may be used for any *reported* number, in the `canonical` region of `metrics.rs`:
- **`pck_canonical(pred, gt, vis, k)` — torso-normalized PCK@k.** A keypoint `j` is correct iff `‖pred_j − gt_j‖₂ ≤ k · torso`, where `torso = ‖left_hip(11) − right_hip(12)‖₂` in the keypoint coordinate space, with a **bounding-box-diagonal fallback** when the hips are not both visible. This is the COCO / ADR-152 convention validated in `benchmarks/wiflow-std/RESULTS.md` (the ~96% PCK@20 reproduction — hip↔hip torso, COCO Setting). **Zero visible joints ⇒ `(0, 0, 0.0)`** — a sample with no measurable evidence scores 0, never 1.
- **`oks_canonical(pred, gt, vis)` — COCO OKS.** `s = sqrt(area)` is derived from the **GT pose extent** (the canonical torso size as a robust, always-positive scale proxy), never a fixed `1.0`. There is no escape hatch that makes OKS ≈ 1.0 for any pose; a degenerate (zero-extent) pose returns 0.0.
**Single source of truth, enforced.** `MetricsAccumulator::update` (the trainer's), `compute_pck`, `compute_per_joint_pck`, `compute_oks`, `aggregate_metrics`, and the deprecated `compute_pck_v2`/`compute_oks_v2`/`MetricsAccumulatorV2`**all route through**`pck_canonical`/`oks_canonical`. So `Trainer::evaluate()` → `MetricsAccumulator` → canonical; the WiFlow-STD bench definition (RESULTS.md) is the reference the canonical *matches*. `eval.rs` reports MPJPE (a distinct, non-divergent error metric, unchanged). The `v2` functions and the `training_bench.rs` raw-threshold kernel are annotated **`#[deprecated]` / "DO NOT USE for reported metrics"**.
**The two claim-inflating bugs are fixed and pinned by regression tests:**
- `canonical_oks_not_one_for_wrong_pose_on_normalized_coords` — a pose off by 3× the torso on `[0,1]` coords yields OKS < 0.2 (the old `s=1.0` path returned ≈1.0).
- `canonical_pck_uses_hip_to_hip_torso`, `canonical_torso_falls_back_to_bbox_when_hips_hidden` — pin the normalizer.
- `all_invisible_gives_zero_pck` (renamed from `all_invisible_gives_trivial_pck`, comment cites this ADR) — the trainer accumulator now scores no-visible as 0.
**Legitimately changed test expectations** (each updated with a comment citing this finding): the historical "perfect on an all-coincident pose" fixtures used keypoints at a single point, which is *correctly unscoreable* under canonical (zero extent ⇒ no scale). Test fixtures were given a real ±0.05 hip span so the canonical normalizer is positive; `all_invisible_*` flipped from 1.0 → 0.0.
**The leak.** MM-Fi windows are extracted with **stride 1** (`MmFiEntry::num_windows = num_frames − window_frames + 1`), so adjacent windows overlap by `window_frames − 1` frames (~99% at the default 100-frame window). And `bin/train.rs` validated a *real* MM-Fi training run against a **synthetic** val set "for pipeline verification" — any PCK it printed was meaningless on two counts.
**The fix (mirroring the leak-free discipline of `occupancy_bench::EvalSplit`):**
- `MmFiDataset::subject_disjoint_split(test_subject_fraction, seed) → (train_view, test_view)` partitions **whole subjects** to one side. Because every window of a subject travels with that subject, the two views share **no subject and no window** — leak-free by construction, deterministic per seed. Returns `DatasetError::InvalidSplit` on <2 subjects, bad fraction, or an empty side.
- `assert_split_leak_free(train, test)` independently verifies subject-disjointness **and** window-index-disjointness, and is called inside the split so a leaky split can never be handed out.
- `bin/train.rs` now **prefers the real split**; the synthetic path is reachable only as a labelled fallback (single-subject data) and is routed through a new `run_smoke_test` that prefixes every metric `[SMOKE-TEST] (DO NOT REPORT)`. `--dry-run` is likewise relabelled. A synthetic-val PCK can no longer be mistaken for a measurement.
**Leak-free proof (tests):** `subject_split_is_subject_and_window_disjoint` (no shared subject, no shared window index, partition covers every window once), `subject_split_is_deterministic_for_seed`, `subject_split_rejects_single_subject`, `subject_split_rejects_bad_fraction`, `assert_leak_free_detects_injected_subject_leak` (the validator catches a deliberately-injected subject overlap — a guard against future partitioner bugs).
`rapid_adapt.rs`'s `contrastive_step`/`entropy_step` wrote a **fake gradient** (`grad += v * 0.01`) unrelated to the stated triplet / entropy objective — so any "TTA improves the metric" was unsupported by the code.
**Resolution: real gradients (not removal).** The two `*_loss` functions are now **pure evaluators** of the real objective; `RapidAdaptation::adapt` descends them with a **central finite-difference gradient** of that exact loss (`∂L/∂wᵢ ≈ (L(w+εeᵢ) − L(w−εeᵢ))/2ε`). Finite differences genuinely minimize the stated objective (to O(ε²) truncation), so "the adaptation loss decreases" is now a **real, reproducible** measurement rather than an artefact of a hand-tuned step. The returned `final_loss` is the *actual* objective at the produced weights.
**Honest scope caveat (recorded in the module and here):** this minimizes a *self-supervised proxy* (temporal-contrastive + prediction entropy) over a tiny LoRA bottleneck on raw CSI. It is **NOT** wired to the pose model, and **there is no measured end-to-end PCK gain on WiFi pose from this path.** TTA-on-pose is a future, **not-yet-measured** capability — no PCK improvement may be cited from this module.
**Tests:** `contrastive_loss_decreases` and `entropy_loss_decreases` (20/30 real gradient steps do not increase the loss vs 0 steps), `reported_loss_is_the_real_objective_not_a_placeholder` (the returned `final_loss` equals an independent recomputation of the objective at the output weights — i.e. it is the real loss, not a fabricated number).
The deterministic proof self-certified: `generate_expected_hash` blessed whatever the pipeline emitted, PASS counted *any* loss decrease (including 1e-9 float noise), and a *missing* expected hash defaulted to PASS.
**Two hardenings:**
1. **Minimum-decrease margin.**`MIN_LOSS_DECREASE = 1e-4`. A run counts as "learning" only when `initial − final ≥ MIN_LOSS_DECREASE` — well above float noise, far below a real step's decrease. A pipeline that only wanders by noise now **FAILS**.
2. **No-hash is a SKIP, never a PASS.**`ProofResult::is_pass()` requires `hash_matches == Some(true)` (a *committed*`expected_proof.sha256`). An absent baseline yields SKIP (exit 2). The `verify-training` binary additionally **fails fast** on a sub-margin loss *before* the hash comparison, so a missing baseline can never downgrade a non-learning pipeline to SKIP.
**What this proves — and what it does NOT (disclosed):** the proof certifies **reproducibility and determinism** (same seed ⇒ same weights ⇒ same hash) and that the optimiser *measurably* reduces a loss. It runs on a deterministic *synthetic* dataset by construction, so it does **not** prove the shipped weights came from real MM-Fi data, nor that any accuracy claim is met. Accuracy is substantiated separately (`benchmarks/wiflow-std/RESULTS.md`). There is currently **no committed `expected_proof.sha256` for the Rust proof**, so it is honestly in the SKIP state until a baseline is committed on a libtorch-enabled host — and SKIP is now reported as SKIP, not green.
Each fix ships a test that would have caught the bug (all in the non-tch, workspace-tested surface).
| Finding | File | Fix | Test |
|---------|------|-----|------|
| `softmax(axis)` ignored the axis (whole-tensor normalize — breaks densepose per-pixel probs) | `nn/tensor.rs` | softmax along the given axis per lane; out-of-range axis ⇒ `NnError` (no panic) | (tier-2 suite) |
| `apply_attention` identity/uniform stub (any "with attention" ablation == without) | `nn/translator.rs` | **implemented real single-head scaled-dot-product attention** (`softmax(QKᵀ/√d)V` with Q/K/V/output projections); mis-shaped checkpoint projections rejected so a bad checkpoint can't silently become a no-op | `test_attention_is_not_uniform_stub`, `test_attention_rejects_wrong_weight_shape` |
| `config.validate()` had no UPPER bounds (config-OOM class still open) | `train/config.rs` | upper bounds on `window_frames`/subcarriers/`backbone_channels`/`heatmap_size`/keypoints/parts/`batch_size`; reject negative `gpu_device_id` | rejection tests; defaults+presets still validate |
| `subcarrier.rs` panic on non-contiguous input | `train/subcarrier.rs` | graceful path / typed error on strided input | non-contiguous-input test |
| `ablation.rs``latency_percentiles``partial_cmp().unwrap()` NaN panic | `train/ablation.rs` | `total_cmp` / NaN-guarded compare | NaN-input no-panic test |
| `onnx.rs` unchecked `-1` dim cast | `nn/onnx.rs` | reject negative/zero output dims with `NnError` | guarded-dim test |
| `ruview_metrics``compute_single_oks``s=1.0` fake-Gold + unguarded `[j]<17` | `train/ruview_metrics.rs` | derive scale from GT extent when none supplied; reject `s≤0`; bound the loop to array extents | `oks_rejects_nonpositive_scale`, `oks_does_not_panic_on_short_arrays`, `oks_not_perfect_for_wrong_pose_with_derived_scale` |
`rf_encoder.rs` was inspected and found to contain **no checkpoint-deserialization assert**: its `assert_eq!`s in `LinearHead::new` / `ContrastiveBatcher::new` are documented construction-time API contracts on *programmer-supplied* vector lengths, not adversarial-input panics — the described bug does not exist there. Any genuine checkpoint-load assert lives in the tch-gated `proof.rs`/`trainer.rs` path and is deferred (§8) as unverifiable without libtorch. Test pass counts: nn `--no-default-features`**35 passed**, nn `--features onnx onnx::tests`**3 passed**, train `--no-default-features` lib **176 passed**.
All numbers MEASURED on the Windows dev host with the `onnx` feature (`ort 2.0.0-rc.11`, runtime auto-downloaded), committed in `nn/benches/onnx_bench.rs`.
### 4.1 Zero-copy ORT input — LANDED, MEASURED
`onnx.rs` built the ORT input via `arr.iter().cloned().collect::<Vec<f32>>()` — a full element-wise copy. Replaced with a contiguous fast path (`arr.as_slice() ⇒ single memcpy`, iterator fallback only for strided views).
### 4.2 ONNX per-inference write-lock — DIAGNOSED, NOT LANDABLE (honest)
`OnnxBackend::run` takes a `parking_lot::RwLock`**write** lock per inference, serializing concurrency. The intended fix was a read-lock. **It is not landable on `ort 2.0.0-rc.11`:** the safe `Session::run` is `&mut self` (verified against the vendored source) — there is no `&self` run path, so a read-lock fails the borrow checker. The underlying C++ `OrtSession::Run` is thread-safe, but exploiting that would require an `unsafe` interior-mutability bypass; we did **not** introduce that soundness risk. The write lock was kept, with a doc comment recording the upgrade path (a future `ort` with `&self` run ⇒ flip to `read()`).
- **Harness landed anyway**, empirically proving the serialization: `cargo bench -p wifi-densepose-nn --no-default-features --features onnx --bench onnx_bench -- onnx_concurrency` → throughput **drops** with more threads (1 thr 19.4 Kelem/s → 2 thr 16.9K → 4 thr 14.0K → 8 thr 14.3K). When `ort` exposes `&self` run, the one-line lock change will show the speedup on this same bench.
The native-conv naive-loop rewrite was **deferred** (§8) as out of scope for a measured milestone.
---
## 5. The NN / training SOTA landscape (graded)
| Candidate | What | Grade | Verdict |
|-----------|------|-------|---------|
| **GraphPose-Fi** (arXiv 2511.19105, code github.com/Cirrick/GraphPose-Fi) | Graph/skeleton pose **decoder** for cross-environment WiFi pose; MM-Fi, 17 joints — matches our setup. ADR-150 §2.2 named a graph decoder but never built it. | **CLAIMED** (preprint; cross-env gains author-reported) | **Top beyond-SOTA candidate. Propose as ACCEPTED-future — NOT built here.** Best fit because the decoder is a drop-in on our 17-joint MM-Fi backbone and directly targets the cross-environment brittleness ADR-150/ADR-027 fight. |
| **ONNX INT4** | Extend our **measured** INT8 ONNX quantization to INT4 for edge. | **THEORETICAL** for our pipeline (INT8 is MEASURED; INT4 untested here) | #2 priority — natural extension of a measured capability. |
| **CSI-JEPA vs MAE A/B** | Joint-embedding predictive pretraining vs the ADR-152 §2.3 MAE recipe. | **CLAIMED** (JEPA strong elsewhere) — **honest caveat: no JEPA *or* MAE result exists on WiFi POSE yet** (ADR-152 F3: UNSW MAE downstream tasks are classification, not pose). | #3 — run as a measured A/B, do not pre-announce a winner. |
| **"Mamba-CSI-pose"** | A state-space-model CSI pose backbone. | — | **Does NOT exist. Do not propose it.** No such artifact in the 2025–2026 literature; naming it would be exactly the kind of unfounded claim this sweep exists to prevent. |
---
## 6. Validation
- `cargo test --workspace --no-default-features` — green (the metric unification legitimately changed a handful of test expectations; each was updated with a comment citing the finding, and the trainer/eval/proof now all route through the one canonical metric).
- `python archive/v1/data/proof/verify.py` — `VERDICT: PASS` (Python pipeline proof, independent of the Rust changes).
- New criterion benches compile and run under the `onnx` feature.
---
## 7. What changed, file by file
- `metrics.rs` — `canonical_torso_size`, `pck_canonical`, `oks_canonical` (single source of truth); `MetricsAccumulator`/`compute_pck`/`compute_per_joint_pck`/`compute_oks`/`aggregate_metrics` route through them; `compute_pck_v2`/`compute_oks_v2`/`MetricsAccumulatorV2` deprecated → canonical; zero-visible and `s=1.0` bugs fixed; canonical bug-catching tests.
- `training_bench.rs`, `sensing-server/training_api.rs` — divergent local PCK kernels annotated "DO NOT USE for reported metrics"; the sensing-server torso-height PCK unification is a **deferred** backlog item (separate service + tch boundary).
---
## 8. Deferred backlog (NOT silently dropped)
The gap review surfaced ~60 findings; this milestone scoped to the provable integrity-critical subset plus two measured perf wins. The remainder are tracked here for a future ADR-155 milestone:
- **GraphPose-Fi graph decoder** — build the §5 top candidate (ACCEPTED-future, not built).
- **ONNX INT4** quantization; **CSI-JEPA vs MAE** A/B; the rest of the §5 roadmap.
- **ONNX read-lock concurrency win** — blocked on an `ort` release exposing `&self``Session::run` (§4.2); harness already committed.
- ~~**native-conv naive-loop** perf rewrite (§4).~~ — **RESOLVED in Milestone-2 (see §8.2): bench-first → MEASURED-INCONCLUSIVE, no perf change shipped.**
- ~~**`rf_encoder.rs` `assert_eq!`-on-checkpoint**~~ — **RESOLVED in Milestone-2 (see §8.2): a pure-Rust fallible `LinearHead::try_new` guard was added.** Any genuine **tch-gated** panic-on-input sites remain deferred — they require a libtorch host to compile/verify (`model.rs``amp_fc1` unbounded alloc is *indirectly* guarded by the new `config.validate()` upper bounds, but a direct guard + test is deferred).
- ~~**`sensing-server/training_api.rs` PCK**~~ — **RESOLVED in Milestone-1b (see §8.1, Goal C).** Relabelled (not unified) — and the audit found the *real* live divergence is in `trainer.rs`, not the orphaned `training_api.rs`.
- ~~**`test_metrics.rs` reference kernels**~~ — **RESOLVED in Milestone-1b (see §8.1, Goal B).** Canonical core hoisted to an un-gated module; the integration test now validates the production functions against hand-computed fixtures + a differential cross-check.
- **`metrics.rs``compute_pck_v2`/`compute_oks_v2`/`MetricsAccumulatorV2`/`evaluate_dataset_v2`/`hungarian_assignment_v2`** — confirmed to have **zero external callers** (only `evaluate_dataset_v2`→`MetricsAccumulatorV2` internally). They are already `#[deprecated]` and route through canonical, so they are not a *divergent-definition* risk, only dead weight. Left in place this pass (public API in a tch-gated module; deleting needs a deprecation-cycle + tch host to verify) — flagged here for a future cleanup, NOT deleted silently.
- **`sensing-server/trainer.rs``pck_at_threshold` (raw) + `oks_map(area=1.0)` and the `training_bench.rs` raw kernel** — relabelled in Milestone-1b (§8.1); true unification onto `pck_canonical`/`oks_canonical` (needs a torso scale + the train crate as a sensing-server dep) remains deferred.
- ~~The remaining ~40 lower-severity review findings (style, micro-opt, doc).~~ — **RESOLVED in Milestone-2 (§8.2): the host-verifiable subset is cleared.** The "~40" was an estimate; the actual host-verifiable (non-tch) train/nn surface is smaller. Enumerated resolution below.
Mirroring the ADR-154 M3 cleanup discipline, M2 closed the **host-verifiable (non-tch) subset** of the §8 backlog in `wifi-densepose-train` (+ the pure-Rust `rf_encoder.rs`/`densepose.rs` in `wifi-densepose-nn` that the §3/§4 items named). Everything behind `#[cfg(feature = "tch-backend")]` (`metrics.rs`, `model.rs`, `losses.rs`, `proof.rs`, `trainer.rs`, `wiflow_std/{layers,model}.rs`) is **out of host-verifiable scope** — it cannot be compiled/verified without libtorch and stays genuinely deferred (not dropped).
**PROOF discipline held:** every de-magicked constant is pinned `== prior literal` by a `*_consts_unchanged_from_literals` test; every boundary test characterizes CURRENT behaviour; no operating-value or behaviour change; the Python proof stays bit-exact at `f8e76f21…46f7a` (the metrics path is off the signal proof path — asserted, not assumed). A smaller-but-true count was reported rather than inventing 40 fixes.
**Skipped honestly (flagged-but-not-real):** `ablation.rs` (NaN sort + boundary already fixed/tested in M1 — clean), `signal_features.rs` (consts already named, n=0 boundary already tested), `mae.rs` (no bare guard literals found), `metrics_core` already had thorough zero-visible/hip-normalizer coverage from M1. No churn was manufactured to hit a count.
**Genuinely data-gated / tch-gated — remaining backlog (blocked, not dropped):** GraphPose-Fi graph decoder, ONNX INT4, CSI-JEPA vs MAE A/B (all **data/model-gated** — need a training run + datasets); ONNX read-lock concurrency win (**upstream-gated** on `ort`); the tch-gated panic-on-input sites in `proof.rs`/`trainer.rs`/`model.rs` and the `metrics.rs``*_v2` dead-code deletion (**tch-gated** — need a libtorch host to compile/verify). **The non-tch-verifiable subset of §8 is now cleared.**
**Findings the §1 seven-definition table under-counted (honest correction):** the live sensing-server claim surface is `trainer.rs` (in `lib.rs`), **not** the named `training_api.rs` — which is an **orphan file, never `mod`-declared, so it does not compile into the crate**. The live `best_pck` is a **raw, unnormalized** PCK and the live `best_oks` still uses the **`area=1.0` fake-Gold** path ADR-155 §2.1 reported as closed elsewhere. So the true metric landscape is **messier than §1 documented**: ≥3 PCK and ≥1 OKS live in `sensing-server`, two of them on the inflating side, and the file the ADR named for the fix was dead code. This is a finding, not a failure — recorded here rather than hidden.
**Goal B (`test_metrics.rs`) — RESOLVED, MEASURED.** The canonical core (`pck_canonical`/`oks_canonical`/`canonical_torso_size`/sigmas/`bounding_box_diagonal`) was hoisted into a new **un-gated**`metrics_core` module (the full `metrics` module is `tch-backend`-gated, so the canonical definition was previously unreachable from the workspace test gate; `metrics` now re-exports it → still ONE implementation). `tests/test_metrics.rs` now asserts the **production** functions against hand-computed fixtures — `canonical_pck_matches_hand_computed_fixture` (3/4 correct ⇒ 0.75, hand-derived), zero-visible⇒0.0, hip↔hip normalizer pin, OKS perfect⇒1.0, the fake-Gold pin — plus `test_kernel_agrees_with_canonical`, a differential test where an independent raw-threshold reference must AGREE with canonical in the torso=1.0 regime. (10→12 tests.)
**Goal C (`training_api.rs` PCK) — RESOLVED by RELABEL, MEASURED.** Torso-height is **load-bearing** (pixel-space, vertical nose→hip scale, `[17×3]` layout, no `ndarray`/train dep), so unifying would silently change the live numbers' meaning — exactly what to avoid. Resolution: relabel everywhere the metric surfaces so it is never read as canonical, in both the named `training_api.rs` (now `compute_pck_torso_height`, struct/JSON-field docs, `pck_torso_h@0.2` logs) **and** — the real fix — the LIVE `trainer.rs` path (`pck_at_threshold` documented raw-unnormalized; `oks_map``area=1.0` flagged fake-Gold; `main.rs` prints `pck_raw@0.2` / `oks_map(area=1.0 proxy)`). No wire-format field or `pub`-fn renames (no silent API break). Pinned by `torso_pck_is_labelled_distinctly_from_canonical` (training_api) and `pck_at_threshold_is_raw_unnormalized_not_canonical` (the live kernel). True unification (route the live server through `pck_canonical`/`oks_canonical`) remains a deferred §8 item — it needs a torso scale on the live data and the train crate as a dep.
---
## 9. Consequences
**Positive.** The training/metrics subsystem can now substantiate a clean accuracy claim: one documented metric used everywhere, a leak-free split, an honest TTA path, a proof that fails on noise and refuses to bless an unbaselined run, and two of the most claim-inflating bugs (false-perfect PCK, fake-Gold OKS) closed and pinned by regression tests. The unmeasured/unprovable parts are **disclosed**, not hidden.
**Negative / honest.** The reportable-metric tch-gated code cannot be compiled on the dev host (libtorch absent), so its validation rests on routing through the workspace-tested canonical functions plus review; the Rust deterministic proof is in SKIP until a baseline is committed on a tch host; the ONNX concurrency win is blocked upstream; and ~45 findings are deferred. None of these is presented as done.
**Picture changed by Milestone-1b (§8.1) — corrected, not hidden.** The §1 "seven divergent metrics" count was an **under-count**. The metric-unification audit (Goal A) found the live `wifi-densepose-sensing-server` carries additional, divergent definitions the §1 table omitted: a **raw, unnormalized**`pck_at_threshold` and an **`area=1.0` fake-Gold** `oks_map` in `trainer.rs` — and these, not the orphaned `training_api.rs` the backlog named, are what actually drive the live-reported `best_pck`/`best_oks`. Milestone-1b **relabelled** them (load-bearing math on different data; relabel beats false unification) and pinned the divergence with tests; full unification onto the canonical definition stays deferred. So the canonical *train/nn* metric is unified and test-validated end-to-end, but the *sensing-server* still computes (now clearly-labelled, non-canonical) progress proxies — disclosed here as the honest current state.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.