Iter 9. Lands ADR-120 §2.4 — the only operation that can lower a frame's
information content. Demote is monotonic by construction (Result::Err
on non-monotone target), strips payload sections per the target class
table, and re-syncs header.privacy_class + CRC32.
Added:
- src/privacy_gate.rs (gated on `feature = "std"`):
* PrivacyGate unit struct (+ Default impl)
* PrivacyGate::demote(BfldFrame, target: PrivacyClass) -> Result<BfldFrame>
* Stripping policy:
target >= Anonymous (2): zeros + clears compressed_angle_matrix and
csi_delta; sets csi_delta = None so from_payload clears HAS_CSI_DELTA
target >= Restricted (3): also zeros + clears amplitude_proxy and phase_proxy
* zeroize_then_clear helper — overwrite with 0 then black_box then truncate
- BfldError::InvalidDemote { from: u8, to: u8 } variant
- pub use PrivacyGate from lib.rs
Note: demote does NOT zero the original Vec capacity that the heap allocator
may still hold — the buffers we own are zeroed and cleared, but the
intermediate Vec passed back to BfldFrame::from_payload reallocates anew.
For strict heap zeroization in regulated deployments, a follow-up iter can
substitute zeroize::Zeroizing<Vec<u8>>.
tests/privacy_gate_demote.rs (7 named tests, all green):
demote_to_same_class_is_identity
demote_derived_to_anonymous_strips_compressed_angle_matrix
(also asserts csi_delta dropped, snr_vector and amplitude_proxy preserved)
demote_derived_to_restricted_strips_amplitude_and_phase_too
(snr_vector and vendor_extension survive at class 3)
demote_anonymous_to_derived_is_rejected
(asserts InvalidDemote { from: 2, to: 1 })
demote_to_raw_is_rejected_from_any_higher_class
(parameterized over Derived, Anonymous, Restricted as sources)
demote_preserves_frame_crc_consistency_through_wire_roundtrip
(post-demote frame survives to_bytes -> from_bytes with no CRC error)
demote_clears_has_csi_delta_flag_bit
ACs progressed:
- AC5 ↑ — privacy_mode enforcement at the frame-class boundary now works
through PrivacyGate, not just the BfldEvent emitter (deferred). When the
active class is Anonymous (2) or Restricted (3), the angle matrix /
csi_delta / amplitude / phase sections that carry identity information
are zeroed before any downstream code sees them.
- AC4 ↑ — demoted frames retain valid CRC; the round-trip-through-bytes
test proves bit-correctness after the class transition.
Test config:
- cargo test --no-default-features → 31 passed (privacy_gate cfg-out)
- cargo test → 60 passed (53 + 7)
Out of scope (next iter target):
- SoulMatchOracle stub trait + no-op default impl (ADR-121 §2.6) so the
Recalibrate exemption hook is wireable from `--features soul-signature`.
- IdentityRiskEngine — multiplicative formula on (sep, stab, consist, conf)
with the coherence-gate GateAction enum (ADR-121 §2.2 + §2.4).
Co-Authored-By: claude-flow <ruv@ruv.net>
Iter 8. Lands the lifecycle half of ADR-120 §2.5: a bounded, in-place,
no_std-compatible ring of IdentityEmbeddings. Insertion is O(1); when
full, push evicts the oldest entry, whose Drop runs and zeroizes the
f32 storage. drain() clears the ring on the coherence-gate Recalibrate
action (ADR-121 §2.4).
Added:
- src/embedding_ring.rs (no_std-compatible; no heap):
* EmbeddingRing struct with [Option<IdentityEmbedding>; RING_CAPACITY=64]
backing array, head cursor, count
* EmbeddingRing::new() / Default impl
* push(emb) -> Option<IdentityEmbedding> (evicted oldest when full)
* len / is_empty / capacity / is_full / iter
* iter() returns occupied slots in insertion order (oldest first)
* drain() -> usize (empties the ring, returns count drained)
- pub use EmbeddingRing, RING_CAPACITY from lib.rs
Uses `[const { None }; RING_CAPACITY]` (stable since 1.79) to initialize
the slot array for a non-Copy element type.
tests/embedding_ring.rs (9 named tests, all green):
new_ring_is_empty
default_constructor_matches_new
push_below_capacity_returns_none
iter_yields_in_insertion_order
push_at_capacity_evicts_oldest_and_returns_it
(verifies eviction reports the FIRST pushed value, not the last)
push_beyond_capacity_keeps_last_n_entries
(after 74 pushes into a 64-slot ring, the surviving 64 are positions 10..74)
drain_empties_the_ring_and_returns_count
drain_on_empty_ring_returns_zero
ring_can_be_refilled_after_drain
(post-drain push lands cleanly at index 0; iter yields exactly that entry)
ACs progressed:
- I2 ↑ — ring eviction and explicit drain both drop IdentityEmbeddings,
which the iter-7 Drop impl zeroizes. The "in-RAM-only" lifecycle is now
end-to-end: bounded buffer in, FIFO out, drain on Recalibrate.
Test config:
- cargo test --no-default-features → 31 passed (22 + 9)
- cargo test → 53 passed (44 + 9)
Out of scope (next iter target):
- PrivacyGate::demote(frame, target_class) — ADR-120 §2.4 monotonic class
transition with field zeroization, refusing demote-to-Raw (compile-fail).
- SoulMatchOracle stub trait + no-op default impl (ADR-121 §2.6) so the
Recalibrate exemption hook is wireable from `--features soul-signature`.
Co-Authored-By: claude-flow <ruv@ruv.net>
Iter 7. First structural enforcement of ADR-118 invariant I2 — the
identity embedding is in-RAM-only and cannot be serialized, cloned,
or copied. Lands the type itself; ring-buffer lifecycle is next.
Added:
- src/embedding.rs (no_std-compatible; lives in the lib regardless of features):
* IdentityEmbedding wrapping [f32; EMBEDDING_DIM=128]
* from_raw(values), as_slice() -> &[f32], l2_norm(), len(), is_empty()
* NO Serialize, NO Clone, NO Copy impl
* Custom Debug emits only dim + L2 norm + "<redacted>" — never raw values
* Drop overwrites storage with 0.0 then core::hint::black_box(...) to defeat
dead-store elimination (DSE would otherwise let the compiler skip the write)
- Compile-time structural guards via static_assertions:
assert_impl_all!(IdentityEmbedding: Drop)
assert_not_impl_any!(IdentityEmbedding: Copy, Clone)
- pub use IdentityEmbedding, EMBEDDING_DIM from lib.rs
tests/identity_embedding.rs (5 named tests, all green):
from_raw_preserves_values_through_as_slice
l2_norm_is_correct
debug_output_redacts_raw_values
(asserts the formatted output does NOT contain decimal text of values)
embedding_is_not_clonable
(runtime witness; compile-time assertion lives in src/embedding.rs)
drop_overwrites_storage_with_zeros
(Drop runs without panic; bit-level zeroization is asserted by the
black_box-guarded loop. Unsafe peek-after-free is intentionally avoided.)
ACs progressed:
- AC5 ↑ — even in `privacy_mode`, the IdentityEmbedding type can't be reached
from any serialization path because the type system rejects the impl.
- I2 ↑ — Drop, no Clone, no Copy, redacted Debug are all in place as
compile-time guarantees.
Test config:
- cargo test --no-default-features → 22 passed
- cargo test → 44 passed (3 + 6 + 7 + 8 + 8 + 7 + 5)
Out of scope (next iter target):
- EmbeddingRing — 64-entry FIFO ring buffer holding IdentityEmbeddings,
drained on coherence-gate Recalibrate (ADR-121 §2.4).
- PrivacyGate::demote(frame, target_class) transformer (ADR-120 §2.4).
Co-Authored-By: claude-flow <ruv@ruv.net>
Iter 6. Connects the typed payload parser (iter 5) to the framed
wire format (iter 4): the CRC32 now covers the section-prefixed
payload bytes per ADR-119 §2.2 ("CRC32 covers all section bytes
including length prefixes").
Added:
- BfldFrame::from_payload(header, &BfldPayload) -> Self
Auto-syncs header.flags HAS_CSI_DELTA bit from payload.csi_delta.is_some(),
serializes payload via to_bytes(), feeds BfldFrame::new() which computes
payload_len + payload_crc32 over the section-prefixed bytes.
- BfldFrame::parse_payload(&self) -> Result<BfldPayload, BfldError>
Reads HAS_CSI_DELTA bit from header.flags and dispatches to
BfldPayload::from_bytes(&self.payload, expect_csi_delta).
tests/frame_payload_integration.rs (7 named tests, all green):
from_payload_then_parse_payload_is_identity
from_payload_autosets_has_csi_delta_flag
from_payload_clears_has_csi_delta_flag_when_csi_absent
(verifies the flag is cleared when csi_delta is None even if caller
pre-set the bit; other flag bits like PRIVACY_MODE are preserved)
frame_crc_covers_section_prefixed_bytes
(mutating a byte inside section body trips CRC, not magic/length)
frame_crc_covers_section_length_prefixes
(mutating a section length-prefix byte trips CRC before parser ever runs)
empty_typed_payload_roundtrips
end_to_end_wire_roundtrip_via_bytes
(BfldPayload -> from_payload -> to_bytes -> from_bytes -> parse_payload
is the identity function modulo flag auto-set)
ACs progressed:
- AC5 ↑ — full payload round-trip through the framed bytes (closes
the round-trip leg from BfldPayload through wire and back).
- AC6 ↑ — same input produces same bytes through both layers.
- AC4 ↑ — CRC mismatch on tampered section bodies and tampered section
length prefixes both surface as BfldError::Crc, not as silent acceptance
or as a deeper parser error.
Test config:
- cargo test --no-default-features → 17 passed (integration tests cfg-out)
- cargo test → 39 passed (3 + 6 + 7 + 8 + 8 + 7)
Out of scope (next iter target):
- PrivacyGate::demote(frame, target_class) — ADR-120 §2.4 class transition
transformer with subtle::Zeroize on dropped fields.
- IdentityEmbedding newtype with no Serialize impl (ADR-120 §2.5 / I2).
Co-Authored-By: claude-flow <ruv@ruv.net>
Iter 2 of the BFLD rollout. Adds the canonical little-endian wire form for
BfldFrameHeader with safe (no unsafe) encoders/decoders. Covers ADR-119 AC5
(round-trip preservation), AC6 (deterministic serialization), and partial
AC1 (constant wire size) / AC4 (rejects bad magic + bad version).
Added:
- BfldFrameHeader::empty() — convenience constructor with magic/version set
- BfldFrameHeader::to_le_bytes() -> [u8; 86]
- BfldFrameHeader::from_le_bytes(&[u8; 86]) -> Result<Self, BfldError>
- Field-level doc strings on every header field (clears all 21 missing-docs
warnings the iter 1 commit logged)
- tests/header_roundtrip.rs — 6 named tests:
header_roundtrip_preserves_all_fields
header_serialization_is_deterministic
header_magic_is_at_offset_zero_little_endian (LE byte order proof)
parsing_rejects_invalid_magic
parsing_rejects_unsupported_version
wire_size_is_constant
Implementation notes:
- Used #[derive(Default)] on BfldFrameHeader so empty() can build cleanly.
- to_le_bytes copies packed fields into locals first to dodge unaligned-
borrow lints; from_le_bytes uses try_into() on byte slices.
- All field reads/writes are #[forbid(unsafe_code)] compliant.
Out of scope (next iter targets):
- BfldFrame (header + payload sections + section-length prefixes + CRC32
computation over payload bytes only) — needs the `crc` crate dependency.
- PrivacyGate::demote(...) skeleton (ADR-120 §2.4).
- SinkMarker traits (LocalSink / NetworkSink / MatterSink) — ADR-120 §2.2.
cargo test -p wifi-densepose-bfld --no-default-features → 9 passed, 0 failed
Co-Authored-By: claude-flow <ruv@ruv.net>
Land P1 of the BFLD rollout — the wire-format primitives:
- New workspace member: v2/crates/wifi-densepose-bfld
- PrivacyClass enum (Raw/Derived/Anonymous/Restricted) with allows_network()
and allows_matter() const helpers reflecting ADR-120 §2.2 and ADR-122 §2.4
- BfldFrameHeader (#[repr(C, packed)]) per ADR-119 §2.1
- BFLD_MAGIC = 0xBF1D_0001, BFLD_VERSION = 1
- BfldError variants for InvalidMagic / UnsupportedVersion / Crc / PrivacyViolation
- soul-signature cargo feature (gated, default OFF) per ADR-118 §1.4
- Compile-time size assertion via static_assertions::const_assert_eq!
- 3 acceptance tests in tests/frame_header_size.rs (all pass)
Bug fix:
- ADR-119 AC1 claimed BfldFrameHeader is 40 bytes. Actual packed layout sums
to 86 bytes. Updated AC1 and §2.1 prose to match. const_assert in frame.rs
pins the value structurally — a future field addition that breaks the size
fails to compile.
Out of scope for this iter (deferred to later P1 commits):
- Field-level missing-docs warnings (21) — addressed alongside accessor helpers
- Payload section parsing — needs the section-length prefix tests
- Round-trip serialize/parse — covered by a fixture-based test in the next iter
cargo test -p wifi-densepose-bfld --no-default-features → 3 passed, 0 failed
Co-Authored-By: claude-flow <ruv@ruv.net>
Both packages are now live on PyPI; bring the in-repo docs up to
match. Keep both updates brief — the canonical surface
documentation lives on the PyPI project pages themselves.
Root README (Option 4 block):
- Switch the default `pip install` example to `ruview` (the brand
name) and note `wifi-densepose` is equivalent.
- Add live PyPI version badges for both packages.
docs/user-guide.md (§Python wheel):
- Replace the single-install example with a table showing both
PyPI projects and their import names so users see the choice
immediately.
- Add three short usage snippets (vitals, live sensing-server WS,
HA-MIND semantic-primitive MQTT listener) so the guide doubles
as a "what does this thing do?" reference for someone landing
via pip.
- Note the cibuildwheel matrix for multi-arch wheels.
- Add the `pytest tests/` + `pytest bench/` source-build verify
steps.
No code or test changes.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #786
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-117): seed branch — ADR-117 pip-modernization spec + soul-signature research bundle
Two artifacts landing together on this new branch as the prerequisite
documentation for the v2.0.0 Python wheel modernization work:
1. **docs/adr/ADR-117-pip-wifi-densepose-modernization.md** (644 lines)
— Plan to bring the 2025-published `wifi-densepose` PyPI package
(last release v1.1.0, 2025-06-07, 11.5 months out of sync) up to
the current Rust v2/ workspace SOTA. Recommends PyO3 + maturin
with abi3-py310 (one binary covers Python 3.10–3.13 per OS/arch),
first-wheel scope = core + vitals + signal crates (~5 MB), v1.99.0
tombstone + 90-day un-yank window for v1.1.0, v2.0.0 hard break.
Open questions catalogued; phases P1–P6+ laid out with concrete
acceptance criteria.
2. **docs/research/soul/** (5 files, ~1,450 lines) — Soul Signature
research spec: 7-channel electromagnetic biometric fingerprint
(AETHER 128-dim + cardiac HR/HRV + cardiac waveform morphology +
respiratory pattern + gait timing + skeletal proportions +
subcarrier reflection profile), fused into one RVF graph file.
Includes 60s scanning protocol, 5-layer security model,
threat-model + mitigations, references to existing ADRs (014,
021, 024, 027, 030, 039, 079, 106, 108, 109, 110, 115). Marked
"Research Specification (Pre-Implementation)". Explicit "what
this is NOT" disclaimers preempt pseudoscience drift; every
discriminative-power claim either cites a measurement or is
marked "open research; baseline TBD".
Branch off main at HEAD; ready for /loop 10m implementation
iterations.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p1): scaffold python/ workspace — PyO3 + maturin + smoke tests (refs #785)
ADR-117 P1 — the python/ directory is now a working maturin-buildable
crate that produces the v2.x replacement for the legacy pure-Python
wifi-densepose==1.1.0 PyPI wheel.
## What lands
- `python/Cargo.toml` — PyO3 0.22 with `extension-module` + `abi3-py310`
(one binary covers Python 3.10–3.13 per OS/arch — keeps the
cibuildwheel matrix to 5 wheels per release, not 20). Depends on
`wifi-densepose-core` from the existing v2/ workspace via relative
path.
- `python/pyproject.toml` — maturin>=1.7 build backend with
`python-source = "python"` and `module-name = "wifi_densepose._native"`
so the compiled module loads as an internal underscore-private
submodule of the user-facing `wifi_densepose` package. PEP 621
metadata + classifiers + project URLs. Optional-deps:
`wifi-densepose[client]` for the P4 WS/MQTT pure-Python layer,
`wifi-densepose[dev]` for the test toolchain (pytest, ruff, mypy).
- `python/src/lib.rs` — minimal `#[pymodule] wifi_densepose_native`
exporting `__rust_version__`, `__rust_build_tag__`,
`__build_features__`, and a `hello()` smoke function. P2 will land
the core type bindings here.
- `python/wifi_densepose/__init__.py` — pure-Python facade re-exporting
the compiled module's symbols under their stable user-facing names.
Docstring teaches the v1→v2 migration story up-front.
- `python/wifi_densepose/py.typed` — PEP 561 marker so `mypy --strict`
in user code treats the wheel as fully typed (real stubs land in P2).
- `python/tests/test_smoke.py` — 6 P1 acceptance tests:
1. package imports without error
2. version string is PEP 440-compliant
3. `__rust_version__` is reachable from Python (the diagnostic
surface ADR-117 §5.2 promised)
4. `__build_features__` lists `p1-scaffold` marker
5. `wifi_densepose.hello()` returns "ok" (FFI round-trip)
6. `wifi_densepose._native` is reachable but the leading underscore
conveys "private; users should import the parent package"
- `python/README.md` — phase ledger, local build instructions
(`maturin develop`), layout diagram.
## What's deferred to P2+
- Core type bindings (`CsiFrame`, `Keypoint`, `PoseEstimate`) — P2
- Vitals + signal DSP bindings + witness v2 — P3
- Pure-Python WS/MQTT client layer (`wifi_densepose[client]`) — P4
- cibuildwheel + PyPI publish — P5
- v1.99.0 tombstone — concurrent with P5
The new `python/` crate is intentionally OUTSIDE the v2/ Cargo
workspace — it has its own Cargo.toml with `[package]` not
`[workspace.package]` inheritance — to keep maturin's `python-source`
+ `module-name` config self-contained and to avoid forcing every
`cargo test --workspace` invocation in v2/ to compile pyo3.
Refs ADR-117 §5 (Detailed design) and §6 (Phased migration).
Refs #785 (tracking issue).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p1): standalone Cargo.toml + python-source=. + #[pyo3(name=_native)] (P1 GREEN)
Three fixes to make maturin develop actually work locally:
1. `python/Cargo.toml` removed `*.workspace = true` inheritance —
the python/ crate is intentionally outside the v2/ workspace
(ADR-117 §5.2) so it needs every `[package]` field local.
2. `python/pyproject.toml` `python-source = "python"` was wrong
because pyproject.toml lives at python/ — maturin was looking for
python/python/. Changed to `python-source = "."` so the
`wifi_densepose/` package directory sibling-to-pyproject is found.
3. `python/src/lib.rs` `#[pymodule] fn wifi_densepose_native` →
`#[pymodule] #[pyo3(name = "_native")] fn wifi_densepose_native`.
PyO3 generates `PyInit__native` from the pyo3-name attribute, which
must match the `module-name` in pyproject.toml's [tool.maturin]
block ("wifi_densepose._native"). Without this attribute the wheel
builds but `import wifi_densepose._native` fails with
ModuleNotFoundError.
## Local validation (P1 acceptance gate)
```
$ python -m venv .venv && .venv/Scripts/python -m pip install maturin pytest
$ VIRTUAL_ENV=… maturin develop --release
…
Finished `release` profile [optimized] target(s)
📦 Built wheel for abi3 Python ≥ 3.10
🛠 Installed wifi-densepose-2.0.0a1
$ .venv/Scripts/python -c 'import wifi_densepose; print(wifi_densepose.__version__, wifi_densepose.__rust_version__, wifi_densepose.hello())'
2.0.0a1 2.0.0-alpha.1 ok
$ .venv/Scripts/python -m pytest tests/ -v
tests/test_smoke.py::test_package_imports PASSED
tests/test_smoke.py::test_version_string_well_formed PASSED
tests/test_smoke.py::test_rust_version_surfaced PASSED
tests/test_smoke.py::test_build_features_listed PASSED
tests/test_smoke.py::test_hello_returns_ok PASSED
tests/test_smoke.py::test_native_module_private PASSED
======================== 6 passed in 0.05s =========================
```
P1 closed. Moving to P2 (core type bindings).
Refs #785, ADR-117 §6.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p2): Keypoint + KeypointType bindings — 23 new tests (29/29 GREEN)
Lands the first chunk of P2: PyO3 bindings for `Keypoint` and
`KeypointType` from `wifi_densepose_core`. Bound types surface to
Python as `wifi_densepose.Keypoint` / `wifi_densepose.KeypointType`.
## Design choices that affect the API surface
1. **`Confidence` is NOT bound as a separate class.** Users hate
wrapping a float in a constructor. Python-side, confidence is just
a `float in [0.0, 1.0]`; the binding validates on construction
(`ValueError` for out-of-range, matching the Rust core error).
2. **`KeypointType` is a `#[pyclass(eq, eq_int, hash, frozen)]` enum**
— hashable so users can drop it into dicts/sets (the most common
pattern in pose-analysis notebooks: `keypoints_by_type[k.type] = k`).
3. **`Keypoint.__init__` keyword-only `z`** so 2D users don't have to
write `None` and 3D users get a clear named arg:
`Keypoint(KeypointType.LeftWrist, 0.2, 0.4, 0.8, z=0.1)`.
4. **`Keypoint` is `#[pyclass(frozen)]`** — no in-place mutation. The
Rust core type is immutable through Copy + Hash + Eq, and exposing
setters from Python would create a copy-vs-reference inconsistency
between languages.
## Files
- `python/src/bindings/keypoint.rs` — 220 lines of `#[pymethods]`
wrappers + Rust↔Python enum round-trip
- `python/src/lib.rs` — `mod bindings { pub mod keypoint; }` +
`bindings::keypoint::register(m)?` call from `#[pymodule]`
- `python/wifi_densepose/__init__.py` — re-exports `Keypoint` and
`KeypointType` at the package root
- `python/tests/test_keypoint.py` — 23 tests covering:
- 17-element COCO ordering of `KeypointType.all()`
- index→type mapping for every variant
- snake_name matches COCO spec
- `is_face()` / `is_upper_body()` predicates
- hashability (the bug I caught when I added the set-based face
test — fixed by adding `hash` to the `#[pyclass]` attribute)
- 2D + 3D constructor variants
- position_2d / position_3d tuples
- is_visible threshold
- confidence validation (Err on out-of-range)
- distance_to (2D Euclidean, 3D Euclidean, fallback when one is 2D
and the other is 3D)
- __repr__ + __eq__
- the new `p2-keypoint-bindings` feature marker landed
## Local validation
\`\`\`
$ cd python && .venv/Scripts/python -m pytest tests/ -v
tests/test_smoke.py::test_package_imports PASSED
tests/test_smoke.py::test_version_string_well_formed PASSED
tests/test_smoke.py::test_rust_version_surfaced PASSED
tests/test_smoke.py::test_build_features_listed PASSED
tests/test_smoke.py::test_hello_returns_ok PASSED
tests/test_smoke.py::test_native_module_private PASSED
tests/test_keypoint.py::test_keypoint_type_all_returns_17 PASSED
…
======================== 29 passed in 0.06s =========================
\`\`\`
Wheel size after both bindings: still well under the 5 MB ADR §5.4
budget (release build with --strip on Windows: ~340 KB).
Also adds `python/.gitignore` to prevent the `.venv/` + `target/` +
`_native.abi3.pyd` artifacts from getting committed.
## What's left in P2
CsiFrame + PoseEstimate bindings land in the next iteration. They're
larger (CsiFrame has the subcarrier buffer; PoseEstimate has
17×Keypoint + BoundingBox + track_id + score). Pattern is now proven
so they go faster.
Refs #785, ADR-117 §6.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p2): BoundingBox + PersonPose + PoseEstimate — P2 COMPLETE (57/57 tests GREEN)
Lands the second + third chunks of P2: PyO3 bindings for `BoundingBox`,
`PersonPose`, `PoseEstimate` from `wifi_densepose_core`. Combined with
the prior Keypoint + KeypointType bindings (fd0568caa), this closes
ADR-117 §6 P2.
## Coverage
| Type | Bound | Tests | Mutability |
|---|---|---|---|
| Confidence | exposed as `float` with validation | (covered in keypoint tests) | n/a |
| KeypointType | `#[pyclass(eq, eq_int, hash, frozen)]` | 7 tests | immutable |
| Keypoint | `#[pyclass(frozen)]` | 16 tests | immutable |
| BoundingBox | `#[pyclass(frozen)]` | 8 tests | immutable |
| PersonPose | `#[pyclass]` (mutable, builder-style) | 12 tests | mutable |
| PoseEstimate | `#[pyclass(frozen)]` | 8 tests | immutable |
Smoke (P1) + new tests: **57/57 PASS** locally on Windows.
## What's deferred to P3
CsiFrame intentionally NOT bound in P2 because it uses
`Array2<Complex64>` (ndarray) — the natural Python surface is via the
`numpy` pyo3 bridge, which lands in P3 alongside the vitals + signal
DSP bindings. Binding CsiFrame without numpy interop would force
users to materialise lists of tuples which is a worse API than
`csi_frame.amplitude_array()` returning an ndarray.
## Design choices that affect the API surface
1. **PersonPose.keypoints() returns a dict keyed by KeypointType**
instead of a fixed-length list with None slots. Pythonistas don't
want to know the underlying storage is `[Option<Keypoint>; 17]`.
2. **PoseEstimate.id and .timestamp exposed as strings** (UUID + ISO)
rather than as bound `FrameId` / `Timestamp` types. Users in
notebooks rarely compare UUIDs structurally; strings are good
enough for diagnostics and don't bloat the bindings.
3. **PersonPose is MUTABLE** (`#[pyclass]` without `frozen`) so users
can build poses incrementally with `set_keypoint`/`set_bbox`/
`set_id`. PoseEstimate is `frozen` because once constructed it
represents a snapshot.
## Three PyO3 0.22 gotchas surfaced this iteration
1. `#[pymethods]` getters are NOT accessible from other Rust modules
— need a separate `impl PyKeypoint { pub(crate) fn inner(&self)
-> &Keypoint { ... } }` block for cross-module use.
2. `PyDict::new(py)` was removed in PyO3 0.21 → 0.22 in favour of
`PyDict::new_bound(py)`. (Confusing because `Bound<'py, PyDict>`
is the return type either way.)
3. `dict.set_item(K, V)` requires both K and V to impl
`ToPyObject`. `#[pyclass]` types impl `IntoPy<PyObject>` but NOT
`ToPyObject` — workaround: convert via `.into_py(py)` first, then
`set_item(py_object_k, py_object_v)`.
Saved as PyO3 0.22 binding patterns memory at the horizon-tracker
level so future loop workers don't re-learn them.
## Local validation
\`\`\`
$ cd python && .venv/Scripts/python -m pytest tests/ -v
…
======================== 57 passed in 0.24s =========================
\`\`\`
Wheel size: still ~340 KB on Windows release build.
Refs #785, ADR-117 §6 (P2 done — ready for P3 vitals + signal DSP +
numpy bridge + witness v2).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-117): add BFLD support (§5.7a + P3.5 phase + §11.11/12 open questions)
Per maintainer feedback during P3 implementation, expand ADR-117 to
include Beamforming Feedback Loop Data (BFLD) as a first-class binding
target alongside CSI. BFLD is the transmitter-side, AP-station-loop
view of the WiFi channel (802.11ac/ax/be compressed beamforming feedback
frames) — complementary to receiver-side CSI, with three properties
that make it strategically important for the pip wheel:
1. **Up to 996 subcarriers per HE160 frame** (vs 242 for HE-LTF CSI on
ESP32-C6, vs 52 for HT-LTF on ESP32-S3) — much denser per-subcarrier
reflection profile
2. **Works on stock 802.11ac+ hardware** — no Nexmon patch, no ESP32
monitor mode, no firmware drift. Captured via tcpdump/Wireshark +
BFR dissector, or via `mac80211` debugfs on Linux 6.10+
3. **Direct input for the soul-signature spec** (`docs/research/soul/`)
— the seven-channel biometric needs dense subcarrier reflection;
BFLD provides it without specialized hardware
## Three additions to ADR-117
### §5.7a — New binding-target subsection
Comparison table CSI vs BFLD; binding strategy with forward-compat
stub Rust impl pending the future `wifi-densepose-bfld` crate; the
three Python types that ship in P3.5:
- `BfldFrame` (frozen) — one compressed feedback matrix snapshot
- `BfldReport` (frozen) — aggregator over a 60-s scan window
- `BfldKind` enum — `CompressedHE20/40/80/160`, `UncompressedHT20/40`
### §6 P3.5 — Concurrent-with-P3 phase
Checkbox plan for the bindings module + stub Rust storage + numpy
bridge for `feedback_matrix` (Complex64 ndarray, same approach as
`CsiFrame.amplitude` from P3). Lands in the same wheel as P3, no
schedule cushion needed.
### §11.11/12 — Two new open questions
- **§11.11** — Should the future BFR ingestion Rust crate be a new
`wifi-densepose-bfld` workspace member, or extend `-signal`?
*Tentative: new dedicated crate. Wireshark BFR dissector is ~2k
lines and would bloat `-signal`; ingestion is optional for many
deployments; keep `-signal` lean.*
- **§11.12** — Per-vendor BFR variant compatibility (Broadcom vs
Intel vs Qualcomm vs MediaTek differ in psi/phi quantization +
matrix entry ordering). How much normalisation in the Python
binding vs. the future Rust crate? *Tentative: Python binding is
dumb (numpy ndarray in/out); future Rust crate owns per-vendor
normalisation via a `Vendor` enum on the constructor.*
### §12 — BFLD reference list
- Hernandez & Bulut, ACM TOSN 2024 (first systematic survey of
BFR-as-sensing)
- Yousefi et al., MobiSys 2023 (practical breath + HR extraction)
- IEEE 802.11ax-2021 §27.3.10 (frame format)
- Wireshark `packet-ieee80211.c` dissector
- AX210 Linux mac80211 debugfs path (kernel 6.10+)
ADR line count: 644 → 807 (+163). Refs #785 (tracking issue).
The implementation work for P3.5 lands in the next /loop iteration
alongside P3 vitals + signal DSP bindings.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p3+p3.5): vitals + BFLD bindings
P3 — Vital sign extraction bindings (wifi-densepose-vitals):
- VitalStatus enum (eq, eq_int, hash, frozen) — Valid/Degraded/Unreliable/Unavailable
- VitalEstimate (frozen) — value_bpm + confidence + status
- VitalReading (frozen) — HR + BR + signal quality composite
- BreathingExtractor — 0.1–0.5 Hz bandpass + zero-crossing
- HeartRateExtractor — 0.8–2.0 Hz bandpass + autocorrelation
- py.allow_threads on extract() hot loops (Q5 audit confirmed
core/vitals/signal are pure-sync — zero tokio deps, safe to release
GIL with no embedded runtime needed)
- 17 tests covering construction, getters, frozen immutability,
esp32_default + explicit ctors, synthetic-signal end-to-end
P3.5 — BFLD bindings (forward-compat surface, stub Rust):
- BfldKind enum — CompressedHE20/40/80/160 + UncompressedHT20/40
with n_subcarriers, bandwidth_mhz, is_he metadata getters
- BfldFrame (frozen) — from_compressed_feedback() accepts numpy
Complex64 ndarray [Nr x Nc x Nsc], validates dims against kind,
feedback_matrix() returns lossless roundtrip ndarray
- BfldReport — aggregates frames, rejects mismatched kinds,
computes inverse-CV coherence score
- 19 tests covering all 6 PHY variants + numpy roundtrip +
dim-mismatch error + aggregation
- Real Rust ingestion (wifi-densepose-bfld crate) lands post-v2.0
per ADR-117 §11.11/12 — Python API will not change
Total Python test count: 93 (was 57, +36 P3+P3.5). All passing.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p4): pure-Python WS/MQTT client layer
New sub-package `wifi_densepose.client` (no PyO3, no Rust deps):
- ws.SensingClient — asyncio websockets>=12 wrapper for the Rust
sensing-server /ws/sensing endpoint. Yields typed dataclasses
(ConnectionEstablishedMessage, EdgeVitalsMessage, PoseDataMessage)
with raw-payload fallback for forward-compat with unknown types.
Malformed frames log+drop without breaking the stream.
- mqtt.RuViewMqttClient — paho-mqtt v2 wrapper using the explicit
CallbackAPIVersion.VERSION2 API. Per-instance unique client_id by
default (rumqttc memory lesson). MQTT v5-spec-correct topic
wildcard matcher: + as whole-level wildcard, # matches the prefix
itself plus all sub-levels. Auto-resubscribes on reconnect.
Handler exceptions are caught and logged so a misbehaving callback
can't crash the network loop.
- primitives.SemanticPrimitiveListener — typed router for the 10
HA-MIND fused inference outputs from ADR-115 §3.12
(SomeoneSleeping, PossibleDistress, RoomActive, ElderlyInactivity-
Anomaly, MeetingInProgress, BathroomOccupied, FallRiskElevated,
BedExit, NoMovementSafety, MultiRoomTransition). Decodes both
JSON payloads with confidence+explanation AND plain HA state
strings ("ON"/"OFF"/numeric). Pluggable into RuViewMqttClient.
- ha.HABlueprintHelper — read-only parser for the
homeassistant/<kind>/wifi_densepose_<node>/<id>/config payload
family. Aggregator queries: entities_for_node, by_device_class,
nodes. Useful for blueprint authors + dashboard introspection.
Test coverage (63 new tests, 156 total in Python suite):
- test_client_ha — 18 tests (topic+payload parsing, aggregator)
- test_client_primitives — 13 tests (enum coverage, listener routing)
- test_client_mqtt — 17 tests (matcher parametrize, dispatch path,
on_connect, exception isolation) — no broker needed
- test_client_ws — 6 tests including end-to-end against an in-process
websockets.serve() fixture exercising all 4 message types plus a
malformed-frame survival check
Post-bridge wheel size: 238 KB (well under ADR §5.4 5 MB budget).
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md §5.6
Refs: docs/adr/ADR-115-home-assistant-integration.md §3.12
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p5+p-tomb): pip-release workflow + v1.99.0 tombstone wheel
P5 — `.github/workflows/pip-release.yml`:
- cibuildwheel matrix per ADR §5.4: manylinux x86_64 + aarch64,
macos x86_64 + arm64, win amd64 (5 wheels via abi3-py310 stable
ABI — one binary per OS/arch covers Python 3.10–3.13)
- Linux aarch64 cross-builds via QEMU; rustup 1.82 pinned in
CIBW_BEFORE_ALL_LINUX for reproducibility
- Per-wheel smoke test: import wifi_densepose, assert hello()=="ok"
- sdist via `maturin sdist`
- Trigger: workflow_dispatch + push to `v*-pip` tags ONLY (never
on regular commits — won't accidentally publish)
- TestPyPI dry-run gate via `repository-url: https://test.pypi.org/legacy/`
- Production PyPI publish via Trusted Publisher OIDC (no API tokens
in GH secrets per ADR §9). Requires one-time PyPI Trusted Publisher
registration before the first publish can fire.
- Q3 (witness hash v2 — ADR-117 §11.3) flagged in workflow comments
as a hard gate before the first tag.
P-tomb — `python/tombstone/`:
- Separate `wifi-densepose==1.99.0` sdist+wheel using setuptools
backend (NOT maturin — tombstone is pure Python, no Rust).
- `src/wifi_densepose/__init__.py` raises ImportError with the
migration URL on import. Verified locally: 2.7 KB wheel,
`pip install` then `import wifi_densepose` raises ImportError
with `pip install wifi-densepose==2.0.0` hint + repo URL.
- 5 unit tests (`tests/test_tombstone.py`) lock the file content
down: must `raise ImportError`, must contain v2 install hint
and migration URL, must NOT contain any `def`/`class`/`import`
beyond the bare `raise` — so a well-intentioned refactor can't
accidentally bloat the tombstone into a real module that loads
partway before failing.
Both wheels are published by the same pip-release.yml workflow:
- `v1.99.0-pip` tag → publishes tombstone (or via workflow_dispatch
with `target: v1-99-tombstone`)
- `v2.X.Y-pip` tag → publishes the v2 wheel matrix
Per ADR-117 §7.3: tag and publish 1.99.0-pip FIRST so the tombstone
claims the "current" slot in pip's resolver, THEN publish 2.0.0-pip.
Test count unchanged in main python/ suite (156/156). Tombstone
sub-suite: 5 passing.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md §5.4, §7
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* hardening(adr-117): benchmarks + security/robustness test suite
Benchmarks (`python/bench/`, pytest-benchmark — opt-in via --benchmark-only):
| Hot path | Mean | Ops/sec | % of 100 Hz budget |
|---|---|---|---|
| BfldFrame HT20 1×1×52 | 800 ns | 1.25 Mops | 0.008% |
| BfldFrame HE20 2×1×242 | 1.3 μs | 750 kops | 0.013% |
| BfldFrame HE80 2×1×996 | 4.2 μs | 236 kops | 0.042% |
| BfldFrame HE160 2×2×1992 | 14 μs | 71 kops | 0.14% |
| BfldFrame.feedback_matrix() | 2.8 μs | 352 kops | — |
| WS edge_vitals decode | 7.4 μs | 134 kops | 0.074% |
| WS pose_data decode (3 persons) | 23 μs | 42 kops | 0.24% |
| BreathingExtractor.extract() 56sc | 28 μs | 35 kops | 0.28% |
| BreathingExtractor.extract() 114sc | 44 μs | 23 kops | 0.44% |
| BreathingExtractor.extract() 242sc | 79 μs | 13 kops | 0.79% |
| HeartRateExtractor.extract() 56sc | 105 μs | 9.5 kops | 1.05% |
All hot paths well under the 100 Hz ESP32 frame budget (10 ms).
Worst case (HeartRateExtractor) uses 1% of the budget — no
optimization needed. Scaling on n_subcarriers is sub-quadratic
(56→242 = 4.3× input, 2.8× time) — catches future O(n²)
regressions.
Security & robustness tests (`tests/test_security.py`, +27 tests):
- WS decoder: rejects non-object roots cleanly, survives 1 MB string
values, handles non-ASCII node IDs, survives deeply-nested JSON
(Python's json.loads built-in guard not bypassed)
- MQTT topic matcher: 9 edge-case parametrize entries including
$SYS topics, null-byte injection, mid-pattern `#` boundary,
empty-string boundary
- MQTT credential confidentiality: password never appears in
repr()/str(), never stored in plain client-instance attribute
- HA discovery: rejects null-byte-laced topics, rejects extra
slashes in node_id, rejects non-dict payload body (list, scalar,
invalid UTF-8 bytes) without crashing
- Semantic primitive listener: rejects topic-injection attempts
(prefix-injected paths, wrong case on final segment), survives
invalid UTF-8 payloads
- Public surface integrity: every name in wifi_densepose.__all__
AND wifi_densepose.client.__all__ resolves — catches accidental
re-export breakage between phases
- Multi-handler MQTT exception isolation: a crashing handler in
the middle of the registered list doesn't stop later handlers
from firing
Test count: 156 → 183 (+27). All passing.
Bench results steady-state confirm no Rust-binding-layer
optimization is needed before the v2.0.0 publish.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p5): switch publish workflow to PYPI_API_TOKEN + user-facing README
- Workflow rewired from OIDC Trusted Publisher to token-based publish
via the `PYPI_API_TOKEN` GitHub Actions secret. Both publish jobs
(v2 wheels + tombstone) pass `password: ${{ secrets.PYPI_API_TOKEN }}`
to `pypa/gh-action-pypi-publish@release/v1`. Workflow comments now
document the GCP → GH secret-refresh command.
- Removed `permissions: id-token: write` and the OIDC `environment:`
blocks (no longer needed without OIDC).
- Token was sourced from the GCP Secret Manager entry `PYPI_TOKEN`
in project `cognitum-20260110` and pushed to GH Actions via
`gcloud secrets versions access | gh secret set` so the value
never appeared in a shell variable or this session's output.
- Rewrote `python/README.md` from a developer phase-ledger into a
user-facing PyPI front page: one-paragraph elevator pitch, bullet
list of features, three short usage snippets (vitals extract,
WS subscribe, MQTT semantic-primitive listener, BFLD numpy
bridge), hardware table, links. The README is the FIRST thing
pip users see at https://pypi.org/p/wifi-densepose so it has to
introduce the project, not the build plan.
Wheel rebuilds clean at 253 KB (was 238 KB — +15 KB from the richer
README baked into the wheel metadata). Test suite unchanged at 183/183.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-117): point root README + user-guide at the v2 pip wheel
- Root README — add Option 4 alongside the existing Docker / ESP32 /
Cognitum Seed installs: `pip install "wifi-densepose[client]"` with
a two-line import preview.
- User-guide §Installation — replace the stale "From Source (Python)"
block (which referenced legacy v1 extras `[gpu]` and `[all]` that
don't exist in v2) with a brief "Python wheel (pip) — ADR-117"
section: what the wheel is, install commands, two-line example,
tombstone caveat, and the `maturin develop` source-build path
for contributors.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p5): pin Python 3.12 + isolated venv for tombstone smoke-test
First v1.99.0-pip run (26366491748) failed: the runner's system `python`
fell back to `--user` install, then `python -c "import wifi_densepose"`
resolved to something other than the freshly-installed user-site wheel
and returned cleanly instead of raising the tombstone ImportError.
Fixes:
- `actions/setup-python@v5` with explicit 3.12 — owns its own site-
packages so pip won't fall back to --user.
- New "Inspect wheel contents" step prints the wheel manifest +
the verbatim __init__.py inside it. If a future regression ships
an empty __init__.py from a setuptools src-layout edge case,
the failure is debuggable from the run log alone.
- Smoke test now runs in a fresh /tmp/smoke-venv so there's zero
ambiguity about which wifi_densepose gets imported. Also uses
importlib.util.find_spec to print the resolved origin path
before the import attempt — so even if both checks pass, we
see exactly which file we exercised.
No code changes to the tombstone source itself.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p5): smoke-test must cd out of repo root before importing
Root cause from run 26366579422 diagnostics: the wheel built correctly
(872 bytes, valid ImportError) but `import wifi_densepose` resolved to
the legacy `./wifi_densepose/__init__.py` left in the repo root from
v1, NOT to the freshly-installed tombstone wheel in the smoke venv.
Python places the cwd at sys.path[0] for `python -c "..."`, so
running the import from the repo root made the legacy directory win
over site-packages every time. The "isolated venv" was not the
problem — the cwd was.
Fix: copy the wheel to /tmp, cd /tmp before the import. Now the
smoke test runs in a directory that contains no `wifi_densepose/`
so the only resolution path is the venv's site-packages.
The repo-root `./wifi_densepose/__init__.py` is a separate concern
(legacy v1 carry-over) that should be cleaned up in a follow-up
commit, but the smoke test should not depend on it being absent.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117): publish wifi-densepose 2.0.0a1 + ruview 2.0.0a1 to PyPI
Three PyPI artifacts now live (published from .env-sourced PYPI_TOKEN
via twine from the maintainer box — direct upload bypassed the GH
Actions workflow auth churn):
1. wifi-densepose==1.99.0 — tombstone (raises ImportError with migration URL)
https://pypi.org/project/wifi-densepose/1.99.0/
2. wifi-densepose==2.0.0a1 — PyO3 wheel (win_amd64 cp310-abi3) + sdist
https://pypi.org/project/wifi-densepose/2.0.0a1/
3. ruview==2.0.0a1 — meta-package re-exporting wifi_densepose
https://pypi.org/project/ruview/2.0.0a1/
New `python/ruview-meta/` subdirectory:
- pyproject.toml — name="ruview", version="2.0.0a1", setuptools backend,
dependencies = ["wifi-densepose==2.0.0a1"]
- src/ruview/__init__.py — re-exports every name from
`wifi_densepose.__all__` so `from ruview import BreathingExtractor`
is equivalent to `from wifi_densepose import BreathingExtractor`.
Also re-exports `__version__`, `__rust_version__`,
`__rust_build_tag__`, `__build_features__`. Aliases the `client`
sub-package transparently when wifi-densepose[client] extras are
installed.
- README.md — explains why two PyPI names ship the same code (brand
vs technical name) and shows install commands for both.
End-to-end verified: fresh venv, `pip install ruview`,
`import ruview` + `import wifi_densepose` both succeed,
`ruview.BreathingExtractor is wifi_densepose.BreathingExtractor` → True.
Multi-platform wheels (manylinux x86_64+aarch64, macos x86_64+arm64)
still pending — the cibuildwheel workflow path remains for that.
Linux/macOS users today install via the sdist (requires rustup +
maturin locally).
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* ci(adr-117): kics-compatible workflow comments + fix-marker guards
- KICS error fix (.github/workflows/pip-release.yml:20): the inline
`gcloud secrets versions access --secret=PYPI_TOKEN ...` runbook
in the workflow header was triggering KICS' generic-secret regex
on the literal `PYPI_TOKEN` substring. Moved the refresh runbook
to docs/integrations/pypi-release.md (with the BOM-stripping
`tr` step that fixed the production publish) and replaced the
inline block with a pointer.
- Three new fix-marker guards in scripts/fix-markers.json so the
next person to touch this code can't silently regress what
PR #786 just shipped:
* RuView#786-tombstone-import — the tombstone __init__.py must
`raise ImportError`, must mention the v2 install hint, must
point at the repo URL, AND must NOT contain `def`/`class`/
`import wifi_densepose` (forbid patterns prevent accidental
bloating into a real module that loads partway before failing).
* RuView#786-tombstone-smoke-cwd — pip-release.yml must `cd /tmp`
before the tombstone smoke-test import, because the legacy
`./wifi_densepose/__init__.py` at repo root would otherwise
shadow the venv install. This was the root cause of run
26366648768; locking it in.
* RuView#786-pypi-token-auth — the workflow must use
`password: ${{ secrets.PYPI_API_TOKEN }}` and must NOT carry
`id-token: write`. The project authenticates via API token,
not OIDC; a partial OIDC migration would 403 silently.
Local check: all 25 markers pass.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #786
Co-Authored-By: claude-flow <ruv@ruv.net>
Wire the Soul Signature research (docs/research/soul/) into BFLD as a
consent-based opt-in that runs at privacy_class = 1 (derived). BFLD becomes
the policy-enforcement and compliance layer for Soul Signature; the two
share the AETHER encoder, the witness chain, the RVF container, and
cross_room.rs.
ADR-118 §1.4 (new): comparison table of intents, consent models, ID spaces,
and shared assets. Explains why the two systems are complementary, not
antagonistic.
ADR-120 §2.7 (new): dual-ID-space contract.
- Default BFLD: class 2, daily-rotated rf_signature_hash for all.
- Soul Signature opt-in: class 1, rotating hash for unenrolled + stable
opaque person_id for enrolled. No collision.
- Class 3 (restricted): Soul Signature disabled.
Static enforcement via --features soul-signature feature gate.
ADR-121 §2.6 (new): Soul Signature Recalibrate exemption + enrollment-
quality gate.
- SoulMatchOracle suppresses Recalibrate when high score traces to an
enrolled person_id (matched outcome is intended, not an attack).
- identity_risk_score doubles as enrollment-quality signal: Soul Signature
enrollment requires score >= 0.65 sustained over the 60s window.
- Exemption is asymmetric: unknown high-separability clusters still
trigger Recalibrate.
ADR-122 §2.7 (new): three Soul Signature HA entities exposed at class 1
only, structurally rejected at the Matter boundary. Fourth blueprint
(enrolled-person arrival notification) ships under feature flag, default
off, per-person opt-in.
Co-Authored-By: claude-flow <ruv@ruv.net>
Two closing P8 deliverables that complete the local-side publishing
scaffolding. The remaining work is all credential-bearing user
action.
1. `cog/app-registry-entry.json` — the exact JSON payload to paste
into cognitum-one's `app-registry.json`. Schema discovered by
fetching the live registry (105 cogs, 11 categories) and
matching the existing `ruview-densepose` entry verbatim. Keys:
id, name, category, version, size_kb, difficulty, description,
featured, config[], sha256, binary_size
cog-ha-matter slots in under `category: "building"` (smart home
/ building automation — the natural HA / Matter category, vs
`network` which is more about transport bridges).
7 config[] entries mirror our CLI surface:
sensing_url, mqtt_host, mqtt_port, privacy_mode,
mdns_hostname, mdns_ipv4, no_mdns
Two post-build fields left as `<FILL_IN_...>` markers:
sha256 (paste from the workflow artifact's .sha256)
binary_size (wc -c < the binary)
Schema validated: all 10 required keys present, parses as JSON.
2. `cog/RELEASE-CHECKLIST.md` — one-page mechanical playbook with
four explicit "🔑 USER ACTION" gates. Each gate names exactly
what the user (or org admin) has to do that the pipeline cannot:
a) provision GCP_CREDENTIALS + HAS_GCP_CREDENTIALS org var
b) provision COGNITUM_OWNER_SIGNING_KEY GH secret
c) gcloud auth login (only if uploading locally)
d) PR app-registry.json into cognitum-one
Plus pre-release test gate, tag-push command, post-release
verification curl, and a rollback procedure using GCS object
versioning (per ADR-100 §"GCS misconfiguration risks").
Stop-condition check (cron's predicate: "ALL local-side publishing
scaffolding is complete and the only remaining work requires user
action"):
✅ cog/manifest.template.json
✅ cog/Makefile (build / sign / upload / verify / clean)
✅ cog/README.md
✅ cog/app-registry-entry.json (this commit)
✅ cog/RELEASE-CHECKLIST.md (this commit)
✅ .github/workflows/cog-ha-matter-release.yml (3 jobs, gated)
✅ dist/ handling (gitignored, created by make)
🔑 4 user-action gates explicitly enumerated in the checklist
The cron should STOP after this iter — the local-side scaffolding
is complete and the remaining work is the four named credential
gates that the pipeline cannot self-serve.
Co-Authored-By: claude-flow <ruv@ruv.net>
New `.github/workflows/cog-ha-matter-release.yml`:
* Triggers on `cog-ha-matter-v*` tag-push + manual dispatch
* Three jobs: build-x86_64, build-arm, publish-gcs
* x86_64: native ubuntu-latest cargo build
* arm: aarch64-unknown-linux-gnu via apt-installed gcc-aarch64-linux-gnu
linker (no `cross` dep needed — keeps workflow self-contained)
* Each build job runs make build-{arch} + make sign-{arch} +
gated Ed25519 sign step (skipped when COGNITUM_OWNER_SIGNING_KEY
secret is unset — workflow still produces unsigned artifacts so
we get build coverage now and signing later without re-merging)
* publish-gcs job gated on `vars.HAS_GCP_CREDENTIALS == 'true'`
so the workflow is safe to merge before credentials land —
no-op until the org admin sets the variable
* Uploads binary + sha256 + (optional) sig to
`gs://cognitum-apps/cogs/{arch}/cog-ha-matter-{arch}`
* Prints the app-registry.json snippet for the cognitum-one PR
(so the publish step's output is the exact JSON the user pastes)
Fixed a bug inherited from cog-pose-estimation's Makefile: the
precedent produces `dist/cog-cog-pose-estimation-arm` (double
`cog-` prefix because CRATE name already starts with `cog-`) but
the manifest URL has single prefix `cog-pose-estimation-arm`. The
upload path doesn't match the binary_url — a latent bug in the
pose cog's pipeline.
My copy now produces `dist/cog-ha-matter-arm` matching the
manifest URL `cog-ha-matter-{{ARCH}}`. Changed: Makefile (build /
sign / upload / verify / clean targets), workflow (artifact names
+ gsutil paths), README (local dry-run instructions). The
cog-pose-estimation precedent is unchanged — separate fix if/when
the user wants to align it.
What this iter does NOT do (P8 remaining):
* provision GCP_CREDENTIALS / COGNITUM_OWNER_SIGNING_KEY secrets
(user action — needs org admin access)
* actually run the workflow (needs a `cog-ha-matter-v0.1.0` tag
push, or workflow_dispatch from the Actions tab)
* append to app-registry.json in cognitum-one (separate repo PR)
Next iter: tag a v0.0.1-dev (so the workflow runs once + we see
any build-time errors on real CI runners) OR scaffold the
app-registry.json patch payload as a check-in doc.
Co-Authored-By: claude-flow <ruv@ruv.net>
Mirrors v2/crates/cog-pose-estimation/cog/ so the Seed runtime
treats cog-ha-matter identically — `cognitum cog install ha-matter`
behaves like `cognitum cog install pose-estimation`.
Files:
* cog/manifest.template.json — 9-field manifest with {{VERSION}}
+ {{ARCH}} slots, hand-edited by the Makefile signer
* cog/Makefile — same target set as cog-pose-estimation:
build / build-arm / build-x86_64
sign / sign-arm / sign-x86_64 (Ed25519 step is TODO,
blocked on COGNITUM_OWNER_SIGNING_KEY provisioning —
same blocker as cog-pose-estimation)
upload / upload-arm / upload-x86_64
manifest (delegates to `cargo run -- --print-manifest`)
release (= build + sign + upload + manifest)
verify (sha256sum vs sidecar)
clean
Adds `mkdir -p dist` to build steps so the gitignored dist/
folder is created on first build.
* cog/README.md — what this cog does, layout map, local dry-run
instructions, gcloud auth requirements, the JSON snippet to
paste into app-registry.json (in the separate cognitum-one
repo, not this one)
Local dist/ is intentionally not committed: top-level .gitignore
matches `dist/` globally, the Makefile creates it on demand.
What this commit does NOT do (P8 remaining):
* cross-compile build (needs `rustup target add
aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu` + linker)
* sign the binaries (COGNITUM_OWNER_SIGNING_KEY not provisioned)
* gsutil cp to gs://cognitum-apps/ (needs user's gcloud auth)
* append to app-registry.json (lives in cognitum-one repo —
separate PR there)
Next iter: a CI workflow that runs `make build sign verify` on
tag-push, so the local-side pipeline is fully exercised even
without the production credentials.
Co-Authored-By: claude-flow <ruv@ruv.net>
Two landings that flip P4 to shipped:
1. main.rs now actually registers the mDNS responder. New CLI:
--mdns-hostname (default: cog-ha-matter.local.)
--mdns-ipv4 (default: 127.0.0.1)
--no-mdns (skip for restrictive CI / multi-instance)
Responder boots after the publisher; failure logs WARN + falls
back to manual HA config instead of killing the cog. The
handle's Drop sends the mDNS goodbye packet on shutdown so HA's
discovery sees a clean service-leave (no stale device card).
2. Embedded rumqttd broker DEFERRED to v0.7 per dossier §8 ranking.
The dossier's prioritised v1 scope is:
1. --privacy-mode audit-only
2. cog manifest + Ed25519 signing + store listing
3. local SONA fine-tuning loop
4. HACS gold-tier integration
5. Matter Bridge (v0.8)
Embedded broker is not in that list. Every HA install already
has mosquitto or HA Core's built-in broker — adding ~2 MB of
binary + ACL config surface for marginal benefit didn't earn a
v1 slot. Documented as row 6 of §4 v1 scope table with explicit
v0.7 target.
P4 row updated to ✅: mDNS half complete (record-builder +
ServiceInfo + live responder + main.rs wiring), witness half
complete (chain + JSONL + file + Ed25519), embedded broker
explicitly deferred with rationale citation to dossier §8.
Stop-condition check:
* dossier has "Recommended scope" section ✅ (§8, folded into
ADR §4)
* P2 (cog scaffold) ✅
* P3 (MQTT publisher wrap) ✅
* P4 (Seed-native enhancements) ✅
Cron's stop predicate evaluates: P2-P4 shipped AND dossier has
the recommended-scope section → STOP. The loop should TaskStop
itself after this iter unless the user wants P5 (RuVector
thresholds), P8 (cog signing), or P9 (HACS repo) to keep going.
64/64 tests green.
Co-Authored-By: claude-flow <ruv@ruv.net>
Closes the mDNS half of P4. `runtime::start_mdns_responder` binds
multicast via `mdns_sd::ServiceDaemon::new`, builds the
ServiceInfo from `MdnsService::to_service_info` (iter 9), and
registers — returning a typed handle that owns both daemon and
fullname.
Handle shape:
pub struct MdnsResponderHandle {
daemon: ServiceDaemon,
fullname: String,
}
impl MdnsResponderHandle {
pub fn fullname(&self) -> &str;
pub fn shutdown(self) -> Result<(), mdns_sd::Error>;
}
impl Drop for MdnsResponderHandle { /* best-effort */ }
Why explicit `shutdown` + best-effort `Drop`: a clean shutdown
sends a goodbye packet so HA's discovery integration sees the
service leave (good UX — no stale device card). `Drop` is the
fallback for panics / process termination but swallows errors
since panicking-in-Drop would mask the real failure.
1 new live-I/O test:
* mdns_responder_fullname_concatenates_instance_and_service_type
— actually binds multicast on the loopback adapter, registers,
asserts the fullname contains `_ruview-ha._tcp`, then
shutdown()s. Confirmed working on Windows; CI environments
where multicast bind is filtered will hit the gracefully-
skipping early return rather than failing the suite.
64/64 cog tests green (63 → 64).
ADR-116 P4: mDNS half ✅ (record-builder + ServiceInfo + live
responder), witness half ✅ (chain + JSONL + file + Ed25519).
Last piece is the embedded rumqttd broker so external mosquitto
becomes optional.
Co-Authored-By: claude-flow <ruv@ruv.net>
Pure conversion from our wire-format `MdnsService` to the
`mdns_sd::ServiceInfo` shape the responder daemon consumes. No
socket binding, no daemon registration yet — that lands next iter
as a `runtime::spawn_mdns_responder(info)` JoinHandle returning
helper, same shape as `runtime::spawn_publisher`.
* `MdnsService::to_service_info(hostname, ipv4) ->
Result<ServiceInfo, mdns_sd::Error>`
* `mdns-sd = "0.11"` added — aligned with the workspace pin from
wifi-densepose-desktop so the lockfile doesn't fork dalek-like
surfaces.
3 new tests:
* to_service_info_carries_service_type_and_port — locks that
`_ruview-ha._tcp` (with or without mdns-sd's trailing-dot
normalisation) and the control port round-trip through the
conversion
* to_service_info_propagates_txt_records — every locked TXT
key from iter 4 (cog_id, mqtt_port, privacy, proto, node_id,
cog_version) reachable via `get_property_val_str` on the
converted ServiceInfo
* to_service_info_does_not_silently_drop_caller_hostname —
locks the caller-side responsibility for the .local. suffix.
mdns-sd 0.11 accepts bare hostnames (verified empirically by
initial test expecting it to reject — it didn't), so the
wrapper layer must do the trailing-dot dance. Documenting
that via a named test catches future bumps where the lib
starts mutating the value.
63/63 cog tests green (60 → 63).
ADR-116 P4 now ⁶⁄₇: ✅ mDNS record-builder, ✅ chain, ✅ JSONL, ✅
file persistence, ✅ Ed25519 signing, ✅ ServiceInfo conversion;
⏳ daemon register + embedded broker.
Co-Authored-By: claude-flow <ruv@ruv.net>
Closes the cryptographic-attestation gap in ADR-116 §2.2: every
witness event can now be signed by the Seed's Ed25519 key, with
verify available to any auditor holding the public key.
Module shape (`src/witness_signing.rs`, kept separate from
`witness::` so the hash chain stays usable without dalek linked
in — important for the wasm32 audit-verifier variant we'll ship
later):
* sign_event(event, &SigningKey) -> Signature
* verify_signature(event, &Signature, &VerifyingKey)
-> Result<(), SignatureVerifyError>
* signature_to_hex / signature_from_hex (128-char lowercase,
matches the witness hex convention)
* SignatureVerifyError::Invalid
* SignatureParseError::{Length, Hex}
Key design point: signature covers the SAME canonical bytes
witness::hash_event hashes. That means:
1. A signed event commits to the entire event content (kind,
payload, timestamp, seq, prev_hash) — no field can be
retroactively changed without invalidating both the hash AND
the signature.
2. The signature implicitly commits to the event's *chain
position* via prev_hash — splicing a signed event into a
different chain breaks verification.
Adds `ed25519-dalek = "2.1"` to cog-ha-matter (already in
workspace via ruv-neural, version kept aligned).
9 new tests:
* sign_and_verify_round_trip
* verify_rejects_signature_under_wrong_key
* verify_rejects_tampered_event (mutate payload after sign)
* verify_rejects_event_with_wrong_prev_hash (splice attack)
* signature_hex_round_trip
* signature_from_hex_rejects_wrong_length
* signature_from_hex_rejects_non_hex
* signature_is_deterministic_for_same_event_and_key
(locks Ed25519's determinism — catches future accidental
swap to a randomized scheme)
* different_events_produce_different_signatures
60/60 cog tests green (51 → 60). Key management is intentionally
out of scope here — the cog runtime reads the Seed's key from the
Cognitum control plane's secure store (separate concern).
ADR-116 P4 now ⁵⁄₆: ✅ mDNS record, ✅ chain, ✅ JSONL, ✅ file
persistence, ✅ Ed25519 signing; ⏳ responder + embedded broker.
Co-Authored-By: claude-flow <ruv@ruv.net>
Closes the witness audit-bundle surface. The hash-chain primitive
+ JSONL serializer from earlier iters only handled one event at a
time; this lands the file-stream surface that operations actually
need:
* `WitnessChain::write_jsonl(&mut impl Write) -> io::Result<()>`
— streams every event as one line + `\n`, empty chain writes
zero bytes
* `WitnessChain::read_jsonl(impl BufRead) -> Result<WitnessChain,
WitnessReadError>` — parses event-by-event AND runs chain-level
`verify()` on the loaded chain, catching reordered or replayed
prefixes that per-event hashing alone misses
Critical security property: `read_jsonl` calls `WitnessChain::verify`
on the loaded chain BEFORE returning Ok. A forged bundle assembled
from two valid chains pasted together would slip past the
per-event hash check (each event's `this_hash` is internally
consistent) but the cross-event `prev_hash` linkage detects the
seam. Test `read_jsonl_chain_verify_catches_reordered_events`
locks this — swap two events in a 2-event bundle, see Verify error.
Error surface (new `WitnessReadError` enum):
* `Io { line_no, msg }` — read failure mid-stream
* `Parse { line_no, source }` — per-event from_jsonl_line failure
* `Verify { source }` — chain-level verify failure
`line_no` is 1-indexed so an auditor sees the same number their
text editor shows. Blank lines tolerated for hand-edited bundles.
7 new tests:
* empty chain writes zero bytes
* write→read round-trips a 3-event chain
* exactly N newlines for N events; trailing newline present
* blank lines / leading newline tolerated
* parse error surfaces with correct line_no
* reordered events caught by chain-level verify
* no-trailing-newline still loads the final event
51/51 cog tests green (44 → 51).
Co-Authored-By: claude-flow <ruv@ruv.net>
Third P4 sub-unit: serialize/parse for the witness hash chain so
audit bundles can be written to disk and replayed.
Wire shape (one record per line, alphabetical field order locked):
{"kind":"...","payload_hex":"...","prev_hash":"...","seq":N,
"this_hash":"...","timestamp_unix_s":N}
Why alphabetical field order: auditors archive whole bundles and
hash them. A rebuild that reordered fields would silently
invalidate every archival hash — locking the order is what makes
the JSONL stable across compiler / serde-json upgrades.
Why hex everywhere: human-greppable, monospace-friendly, no base64
ambiguity, no Vec<u8> JSON-array ugliness. Same convention as
ADR-101's `binary_sha256`.
Critically, `from_jsonl_line` RE-VERIFIES `this_hash` against
the canonical bytes derived from the parsed fields. A tampered
bundle fires `WitnessParseError::HashMismatch` BEFORE the event
loads — the parser is itself an auditor.
New surfaces:
* `WitnessHash::from_hex` (with structured length/parse errors)
* `WitnessEvent::to_jsonl_line`, `from_jsonl_line`
* `WitnessParseError` enum: Json | MissingField | WrongType |
HashLength | HashHex | PayloadHex | PayloadLength | HashMismatch
* private `hex_encode` / `hex_decode` helpers (no `hex` crate dep)
10 new tests:
* jsonl round-trip preserves all fields
* jsonl line has no embedded \n / \r (one record per line)
* jsonl field order is alphabetical (byte-stable archival)
* parser rejects tampered payload via HashMismatch
* parser rejects non-hex characters in hash
* parser rejects missing field
* hex encode/decode round-trip across empty / single byte / 0xff /
UTF-8 / arbitrary bytes
* hex decode rejects odd-length input
* WitnessHash::from_hex round-trip
* WitnessHash::from_hex rejects wrong length
44/44 cog tests green (34 → 44).
ADR-116 P4 row enumerates 4 sub-units now: ✅ mDNS record-builder,
✅ witness chain primitive, ✅ witness JSONL persistence,
⏳ responder + embedded broker + Ed25519 signing.
Co-Authored-By: claude-flow <ruv@ruv.net>
Second P4 unit: an append-only SHA-256 hash chain for tamper-evident
audit logging. ADR-116 §2.2 promised this for healthcare /
education / shared-housing deployments — this lands the primitive
with no key dependency so the next iter can layer Ed25519 signing
on top without touching the chain itself.
Module shape:
* `WitnessHash([u8; 32])` newtype + `WitnessHash::GENESIS` sentinel
* `WitnessEvent { seq, prev_hash, ts, kind, payload, this_hash }`
— once committed, every field is immutable
* `WitnessChain` — `append`, `tip`, `verify`, `events`
* `canonical_bytes` — length-prefixed serialization that prevents
the classic concatenation forgery
(`abc|def` ≠ `ab|cdef`)
* `WitnessVerifyError` — auditor-friendly error with `at: usize`
on every variant (SeqGap, PrevHashMismatch, HashMismatch)
13 new tests covering both happy path and active tampering:
* genesis hash all-zeros
* empty chain tip is genesis
* canonical bytes length-prefixed (anti-forgery)
* canonical bytes start with prev_hash (wire-format lock)
* append links to prev_hash
* seq monotonic from 0
* verify passes on clean chain
* verify catches tampered payload (fires HashMismatch)
* verify catches broken prev_hash link
* verify catches seq gap
* hash hex is 64 lowercase chars
* first event prev_hash == GENESIS (auditor anchor)
* different payloads → different hashes
Hash-chain over Merkle is the right tradeoff for the cog's event
rate (a few/min steady, dozens during a fall) — linear scan is
fine and we save the Merkle complexity for a future tier when
chains span days.
34/34 cog tests green (21 → 34).
ADR-116 P4 row updated to enumerate the three P4 sub-units shipped /
pending: (a) mDNS record-builder ✅, (b) witness hash-chain ✅, (c)
responder + embedded broker + Ed25519 signing pending.
Co-Authored-By: claude-flow <ruv@ruv.net>
Opens P4 with the smallest extractable unit: a pure builder that
produces the wire-format `MdnsService` the responder will publish
next iter. Splitting the record-builder from the responder lets
us:
* lock the TXT-record surface with named unit tests so drift
between the cog and the HA-side YAML auto-discovery binding
fires a test instead of silently breaking deployments,
* swap the responder library (mdns-sd / zeroconf / pnet) without
touching content,
* include the advertisement in `--print-manifest` for Seed
integration tests that can't boot tokio.
TXT surface (sorted, RFC 6763):
| cog_id | "ha-matter" |
| cog_version | CARGO_PKG_VERSION |
| node_id | identity.node_id |
| mqtt_port | u16 stringified |
| privacy | "1" | "0" |
| proto | "ruview-ha/1" |
9 new tests:
* service_type locked to `_ruview-ha._tcp`
* instance_name carries node_id
* control_port advertises the *control plane*, not MQTT
* privacy flag is "1"/"0" (HA config flow reads it byte-stable)
* proto version locked to ruview-ha/1 (bump is deliberate)
* cog_id in TXT matches crate constant
* txt_records sorted for byte-stable mDNS responses
* **PII leak guard**: TXT must NOT carry hr_bpm, br_bpm, pose_*,
keypoint, ssid, lat, lon, mac, rssi — broadcasts in cleartext
so a future "let's add hr_bpm for convenience" patch fires
here, not in a privacy incident.
* required-keys lock — adding is fine, removing/renaming breaks
every deployed Seed.
21/21 cog tests green (12 → 21).
ADR-116 P4 flipped pending → in progress, with the responder /
embedded broker / witness chain enumerated as the remaining P4
sub-units.
Co-Authored-By: claude-flow <ruv@ruv.net>
P3 closes the publisher wiring loop. `main.rs` now:
1. builds `PublisherInputs` from CLI args via the pure helper
extracted last iter,
2. opens a `broadcast::channel::<VitalsSnapshot>(256)`,
3. calls `runtime::spawn_publisher(inputs, rx)` — a thin
wrapper around ADR-115's `publisher::spawn` that owns the
`Arc<MqttConfig>` wrap,
4. holds the tx side so the channel stays open until P3.5
wires the sensing-server bridge,
5. awaits Ctrl-C or unexpected publisher exit (logged at WARN).
Two new tests:
* `spawn_publisher_returns_live_handle_without_broker` — proves
the wiring compiles and the rumqttc event loop survives an
unreachable broker (it retries internally; we abort the handle
inside 100 ms). Catches breakage from a future refactor that
accidentally pre-validates host reachability.
* `default_state_channel_capacity_is_reasonable` — locks the
`DEFAULT_STATE_CHANNEL_CAPACITY = 256` default; a regression to
e.g. 1 would surface here instead of as a dropped frame in
production under bursty multi-Seed federation.
12/12 cog-ha-matter tests green (10 → 12).
ADR-116 phase table: P3 flipped from "in progress" to ✅ wiring done,
with the P3.5 follow-up (sensing-server `/v1/snapshot` WS bridge)
explicitly named.
Co-Authored-By: claude-flow <ruv@ruv.net>
Adds `runtime::build_publisher_inputs(host, port, privacy, identity)` —
the side-effect-free helper that turns the cog's CLI surface into the
`(MqttConfig, OwnedDiscoveryBuilder)` pair ADR-115's `publisher::spawn`
consumes. Keeps the tokio runtime wiring out of the pure unit so the
mDNS responder + Seed control plane (P4) can build the same inputs
from different sources without going through clap.
8 new tests lock the wire-format invariants:
* host/port round-trip into MqttConfig
* privacy_mode propagation (P1 dossier item 7, FDA Jan 2026)
* discovery_prefix defaults to "homeassistant"
* discovery carries node_id + sw_version + friendly_name
* via_device advertises COG_ID (ADR-101/102 device-registry shape)
* client_id includes node_id (lesson from ADR-115 iter 45-48 session
takeover post-mortem — two publishers sharing a client_id loop)
* tls defaults to Off for v1 LAN-only (lock against silent enablement)
* default_identity carries CARGO_PKG_VERSION + PID for uniqueness
Plus the existing 2 manifest tests → 10/10 green
(`cargo test -p cog-ha-matter --no-default-features --lib`).
Also lands the deep-researcher dossier (`docs/research/ADR-116-ha-...`)
that the ADR §3+§4 reference — it was produced last iter but only the
ADR was committed; this puts the source-of-truth into the tree so the
ADR's "8 sections, 30+ citations" claim is actually verifiable.
P3 status in the ADR phase table flipped from "pending" to "in progress"
with the helper named; next iter tokio::spawns publisher::run(...) in
main.rs and registers the mDNS responder.
Co-Authored-By: claude-flow <ruv@ruv.net>
Proposes `cog-ha-matter` as a Cognitum Seed cog packaging the
ADR-115 HA-DISCO + HA-MIND surfaces as a first-class Seed-installable
artifact, rather than configuration of an external sensing-server.
P1 — research dossier in progress (deep-researcher agent), output at
`docs/research/ADR-116-ha-matter-cog-research.md`.
Seed-native enhancements vs the ADR-115 sensing-server flag:
- Embedded mosquitto (optional, for Seeds without external broker)
- mDNS service advertisement (_ruview-ha._tcp)
- RuVector-backed semantic-primitive thresholds (SONA adaptation,
per-home learning rather than static YAML)
- Ed25519 witness chain for state transitions (regulated deployments)
- OTA firmware coordination for the mesh's ESP32-C6 nodes
- Multi-Seed federation via ADR-110 ESP-NOW substrate (≤100 µs
sync enables cross-Seed dedup of events like falls in shared rooms)
7 open questions tracked for the research dossier to answer:
Matter Bridge vs Matter Root, Thread Border Router feasibility,
HACS value-add, CSA cert cost/timeline, cog binary RAM budget,
ruvllm latency, HIPAA/FDA classification.
10 implementation phases scaffolded. Tracking issue to file once
research lands. PR for the cog binary in P2.
Co-Authored-By: claude-flow <ruv@ruv.net>
Tighten the ADR-079 camera-supervised limitation line and remove the
prominent iter-50 'What's new (2026-05-23)' callout block — both
preferred local edits.
Co-Authored-By: claude-flow <ruv@ruv.net>
Iter 50 — both ADRs merged today (PR #764 + PR #778). README's
beta-software warning block was the natural location for a release
callout above the main pitch; users hitting the README see today's
shipped work first.
Two-bullet block:
- ADR-110 ESP32-C6 firmware substrate at v0.7.0-esp32 with the
headline measured numbers (99.56 % match / 104 µs stdev / 3.95x
EMA suppression) and the host-side surface (decoders + REST +
Prometheus + WebSocket).
- ADR-115 HA+Matter integration with the entity-count / blueprint
/ Lovelace count and the privacy-mode architectural win.
Both link to their ADRs + PRs so reviewers can follow back.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ui): unbreak viz.html — OrbitControls importmap, WS URL, toast NPE (#760)
Three independent bugs were stacking to make ui/viz.html unusable from `main`:
1. Three.js r160 removed `examples/js/OrbitControls.js`, so the script-tag
load 404'd and `new THREE.OrbitControls(...)` threw. Switch to an
importmap that pulls the ES module build, then re-expose
`window.THREE` and `THREE.OrbitControls` so the existing component
modules (scene.js, body-model.js, …) keep working without a wider
refactor.
2. The WebSocket client was hardcoded to `ws://localhost:8000/ws/pose`,
but the sensing-server listens on `--ws-port` (8765 default, 3001 in
the Docker image) at `/ws/sensing`. Reuse the existing
`buildSensingWsUrl()` helper from `sensing.service.js` so port
pairings are handled centrally, and add a `?ws=…` query-string
override for non-standard setups. The websocket-client.js default is
also updated to derive from `window.location` instead of the dead
`:8000/ws/pose` literal.
3. `ToastManager.show()` called `this.container.appendChild(...)` even
when `init()` had never been called, throwing a TypeError that
killed the rest of page initialization. Auto-init the container
lazily on first show (patch from issue reporter).
Closes#760.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ui): single module script + mutable THREE — OrbitControls validated
Browser validation against the previous commit caught two stacked issues:
1. `import * as THREE from 'three'` returns a frozen Module Namespace
Object — assignment `THREE.OrbitControls = OrbitControls` silently
no-ops, so the global never gets the OrbitControls reference.
2. Two separate `<script type="module">` blocks (one installing the
THREE global, one consuming it via Scene) are independently
async-resolved. The second can finish dependency loading first and
call `new THREE.OrbitControls(...)` before the first script has run.
Fixed by spreading the namespace into a plain mutable object and merging
all initialization into a single module script with `await import()` for
component modules. Order is now strictly: import THREE → install
window.THREE → import components → run init().
Validated via agent-browser: page logs `[VIZ] Initialization complete`,
WebSocket targets the correct `ws://127.0.0.1:3001/ws/sensing` endpoint
(derived from buildSensingWsUrl), toast lazy-init confirmed via eval.
Co-Authored-By: claude-flow <ruv@ruv.net>
PR #744 moved the files into 9 thematic folders via git mv but missed
the READMEs due to a working-directory issue with git add. This PR
adds the actual READMEs:
- examples/research-sota/README.md (main overview)
- examples/research-sota/01-physics-floor/README.md
- examples/research-sota/02-placement/README.md
- examples/research-sota/03-spatial-intelligence/README.md
- examples/research-sota/04-rssi/README.md
- examples/research-sota/05-cross-room-reid/README.md
- examples/research-sota/06-structure-detection/README.md
- examples/research-sota/07-negative-results/README.md
- examples/research-sota/08-verticals/README.md
- examples/research-sota/09-quantum-fusion/README.md
Each sub-README documents:
- Scripts + headlines table
- Why this folder bounds/composes with others
- Sample output / honest scope
- Cross-references to related loop notes + ADRs
Main README covers:
- Folder map with thread numbers
- Cross-folder dependency graph
- 8-entry headline findings table
- Reading order for newcomers (4 scripts in suggested order)
- Honest scope (synthetic-physics caveats)
Eighth exotic vertical. Recovers what R13 NEGATIVE physically excluded.
Demonstrates the loop's architecture is SENSOR-AGNOSTIC — same primitives
work with classical CSI today and quantum sensors in 5-20y.
User-prompted: opened docs/research/quantum-sensing/11-quantum-level-
sensors.md indicating quantum-integration interest. Repo already has
nvsim (NV-diamond magnetometer simulator, ADR-089) as a standalone
leaf crate.
Four quantum modalities catalogued:
- NV-diamond magnetometer (1 pT/sqrt(Hz), 5-10y edge)
- Atomic clock (10^-15 stability, 5-10y edge)
- SQUID magnetometer (1 fT/sqrt(Hz), 15-20y if room-temp possible)
- Quantum-illuminated radar (+6 dB SNR, 15-20y edge)
Classical vs quantum loop primitive comparison:
- Breathing rate: +-1 BPM -> +-0.1 BPM (10x)
- HR rate: +-5 BPM -> +-0.5 BPM (10x)
- HRV contour: NOT possible (R13) -> NV-magnetometer enables it
- BP: NOT possible (R13) -> atomic-ToA PWV enables it
- Position precision: 25 cm -> 3 mm (80x)
- Multi-scatterer penalty: 4.7 dB -> 1 dB (3.7 dB recovery)
- Through-rubble: 2 m -> 5 m+ (2.5x)
WHAT R13 NEGATIVE NO LONGER RULES OUT WITH QUANTUM:
R13 ruled out HRV contour + BP from CSI due to 5 dB SNR shortfall.
NV-diamond cardiac magnetometry resolves this — heart magnetic fields
(~50 pT) detectable, contour-preserving, penetrates clothing/rubble.
The 5 dB R13 shortfall was SENSOR-BOUND, not PHYSICS-BOUND-period.
Different sensor recovers it. R20 identifies this categorisation
explicitly.
Five-cog speculative roadmap:
- cog-quantum-vitals (5y): nvsim + R14 + R15
- cog-mm-position (10y): atomic clock + R1 + R3.2
- cog-deep-rubble-survivor (15y): nvsim + R18 + drone
- cog-quantum-illuminated-pose (15y): quantum illum + R6.1
- cog-ICU-meg (20y): SQUID + R14 V3
Three deployment scenarios:
- Hybrid ICU bed (5y): 0/bed (4xESP32 + NV-diamond) vs ,000 monitor
- Atomic-clock mm-precision multistatic (10y): high-security access
- NV-drone disaster magnetometry (15y): 2.5x rubble depth over R18
Integration with existing nvsim (ADR-089):
- Magnetic-field time series -> R14 V1 vitals fusion
- Field map -> R12 PABS structural anomaly extension
- Stability indicator -> R7 mincut additional consistency channel
Future cog: cog-quantum-fusion or cog-quantum-vitals.
THE CLEANEST 'LOOP IS SENSOR-AGNOSTIC' DEMONSTRATION:
Even when classical CSI hits its physics floors (R13, R1 bandwidth,
R6.1 penalty), the ARCHITECTURE STAYS THE SAME; only the sensor swaps.
R6 forward model, R12 PABS, R7 mincut, R3 cross-room, R14 V1/V2/V3
framework — all apply to quantum sensors with parameter swaps.
This is the loop's architectural value proposition in its most explicit form.
Honest scope (very important):
- Most quantum tech is 10-20y from edge deployment
- nvsim is a SIMULATOR, not real hardware
- All 'improvement' numbers are theoretical bounds; real-world 30-70%
- Loop has NO real quantum sensor on bench
R20 special status:
- 8th exotic vertical
- First requiring quantum hardware for full realisation
- Most explicitly 10-20y horizon (matches cron prompt criteria)
- Recovers R13 NEGATIVE via different sensing modality
Composes with every loop thread + ADR-089 nvsim + ADR-113 placement.
Coordination: ticks/tick-37.md, no PROGRESS.md edit.
Loop summary: 18 research threads, 8 exotic verticals, 6 loop ADRs,
3 negative result categories (R13 conditionally recoverable now),
production roadmap shipped. 00-summary.md to follow at 12:00 UTC stop.
Terminal output of the SOTA research loop. Maps every research finding
to owner, LOC estimate, dependency, and priority across 6 tiers.
Total engineering budget across the loop's output:
- Tier 1 (Q3 2026): ~490 LOC, 3-4 person-weeks
- Tier 2 (Q3-Q4 2026): ~1180 LOC, 6-8 person-weeks
- Tier 3 (2027): ~1140 LOC, 8-10 person-weeks
- Tier 4-5 (long horizon): ~700+ LOC, 6-8 person-weeks
- TOTAL: ~3,500 LOC, ~25 person-weeks
Tier 1 (next quarter) ships:
- 1.1 wifi-densepose plan-antennas CLI tool (360 LOC) -- 93x placement lift
- 1.2 R12.1 pose-PABS in vital_signs cog (80 LOC) -- 9.36x intruder lift
- 1.3 cog-person-count v0.0.3 chest-centric (50 LOC)
- 1.4 ADR-029 amendment w/ ADR-113 matrix (0 LOC)
Critical-path graph:
1.1 + 1.2 -> 1.3 -> 2.1 ruview-fed -> 2.2 DP-vital-signs -> 3.1 cross-install -> 3.2 PQC
+-> 3.3 real-AETHER -> 3.4 fall-detect
+-> 4.x verticals
Why this matters: after 35 ticks of research output, this is the
document that lets a team pick up and ship without re-reading the 34
research notes. Priority alignment, estimate-anchoring, critical-path
visibility — all in one place.
R-thread mapping:
- R5/R6/R6.2 family/R6.1 -> Tier 1
- R12/R12.1 PABS -> Tier 1.2
- R3/R3.1/R3.2/R14/R15 -> Tier 2-3
- R7 mincut -> Tier 2 (in ruview-fed)
- R13 NEGATIVE -> rules out BP, no Tier line
- R10/R11/R16/R17/R18 verticals -> Tier 4-5
Composes with every loop output. Every thread, ADR, vertical sketch
has a line in some Tier. The TERMINAL output that needs the synthesis
power of a research loop to produce.
Honest scope:
- Estimates synthetic-data-based; may shift after bench validation
- Critical-path may have hidden dependencies (e.g. AgentDB schema)
- 25 person-weeks assumes full-time engineers
- Doesn't include integration testing, documentation, deployment ops
- Tiers based on architectural dependency, not business priority
Loop status after 35 ticks:
- 16 research threads
- 6 exotic verticals
- 6 new ADRs (105/106/107/108/109/113)
- 3 negative result categories
- 2 self-corrections
- 3 honest-scope findings
- 9-tick R6 family (complete)
- 3-tick R3 arc (complete)
- 3-tick R12 arc (complete)
- This production roadmap
00-summary.md will follow at 12:00 UTC / 08:00 ET cron stop.
Coordination: ticks/tick-35.md, no PROGRESS.md edit.