mirror of
https://github.com/ruvnet/RuView
synced 2026-06-09 10:13:17 +00:00
0bffe27288
* 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>
307 lines
14 KiB
Markdown
307 lines
14 KiB
Markdown
# Soul Signature — Scanning Process
|
||
|
||
**Status:** Research Specification (Pre-Implementation)
|
||
**Date:** 2026-05-24
|
||
**Author:** ruv
|
||
|
||
---
|
||
|
||
## 1. Hardware Prerequisites
|
||
|
||
### 1.1 Full Protocol (N ≥ 3 Nodes)
|
||
|
||
| Component | Minimum | Recommended | Notes |
|
||
|---|---|---|---|
|
||
| Sensing nodes | 3 × ESP32-S3 (ADR-028) | 5+ nodes | Multi-node triangulation reduces angle-dependent blind spots; ADR-029 multistatic mesh |
|
||
| Compute appliance | Cognitum Seed (Pi 5 + Hailo) | Same | Runs the field model, AETHER inference, vitals pipeline |
|
||
| Network link | 2.4 GHz or 5 GHz AP | Dedicated sensing AP | Shared AP with user traffic degrades CSI frame rate |
|
||
| Firmware version | ADR-110 v0.7.0+ | Same | Ed25519 witness chain required for attestation |
|
||
| Clock sync | 802.15.4 time-sync (ESP32-C6) or NTP fallback | 802.15.4 preferred | ±100 µs alignment per ADR-110; NTP gives ±5 ms |
|
||
|
||
### 1.2 Degraded Mode (1 Node)
|
||
|
||
A single-node enrollment produces an incomplete signature:
|
||
- Skeletal proportions: degraded (single-angle view)
|
||
- Subcarrier reflection profile: single orientation only (3-orientation protocol collapses to 1)
|
||
- AETHER embedding: usable but lower confidence
|
||
- Cardiac / respiratory: unaffected (single-node sufficient)
|
||
- Gait timing: usable if node placement allows bidirectional walk
|
||
|
||
Single-node signatures MUST be tagged `degraded_mode: true` in the manifest. The
|
||
match score uses only the channels that met minimum confidence thresholds. The
|
||
soul signature is technically valid but should be re-enrolled with multi-node
|
||
hardware when possible.
|
||
|
||
### 1.3 ESP32-C6 Uplift (Wi-Fi 6 HE-LTF)
|
||
|
||
When at least one ESP32-C6 node is present (ADR-110), the subcarrier count
|
||
expands from 52 (HT-LTF, S3) to up to 242 (HE-LTF, C6). The MERIDIAN
|
||
HardwareNormalizer (ADR-027) maps all nodes to a canonical 56-subcarrier
|
||
representation for the AETHER backbone. The full 242-subcarrier profile is
|
||
preserved in the SubcarrierReflectionProfile node for higher-fidelity matching
|
||
when available. The C6's 802.15.4 time-sync (±100 µs) also improves multistatic
|
||
coherence relative to NTP-only S3 meshes.
|
||
|
||
---
|
||
|
||
## 2. Structured 60-Second Enrollment Protocol
|
||
|
||
The enrollment protocol produces exactly one `.rvf` soul signature file. The
|
||
protocol is structured into five phases with exact timing. A human-readable
|
||
prompt sequence should be delivered to the subject via audio or display.
|
||
|
||
### Phase 0 — Empty-Room Field Recalibration (T+0 to T+10)
|
||
|
||
Before the subject enters the sensing zone, the room must be empty and the
|
||
ADR-030 field model must be current.
|
||
|
||
```
|
||
T+0s : System checks field model age. Maximum age: 4 hours.
|
||
If stale or absent → run field recalibration:
|
||
Collect 1,200 CSI frames at 20 Hz (60 seconds of empty room)
|
||
Compute per-link Welford mean and covariance
|
||
Run SVD on covariance matrix → top-K=8 eigenmode vectors
|
||
Store in field_model.rs::FieldNormalMode
|
||
|
||
T+0–10s: Quiet sampling of empty-room field state. No subject present.
|
||
Operator prompt: "Please ensure the room is empty."
|
||
System: verifies presence score < 0.1 (ADR-039 Tier 2 presence detection).
|
||
Failure: if presence score ≥ 0.1, abort and report FAIL_ROOM_NOT_EMPTY.
|
||
```
|
||
|
||
This phase is skipped (not aborted) if the field model was updated within the
|
||
last 4 hours AND the current empty-room sampling confirms presence score < 0.05.
|
||
|
||
### Phase 1 — Deep Breathing Baseline (T+10 to T+25)
|
||
|
||
Subject enters the sensing zone and performs five deep breathing cycles.
|
||
|
||
```
|
||
T+10s : Subject enters scan zone. System detects presence.
|
||
Operator prompt: "Please stand still and breathe slowly and deeply."
|
||
|
||
T+10–25s: Subject stands at zone center, facing node cluster.
|
||
Five complete breath cycles, each ≥ 4 seconds.
|
||
System collects:
|
||
- ADR-021 BreathingExtractor: baseline_bpm, depth_amplitude,
|
||
inspiration_expiration_ratio, HRV_RSA
|
||
- ADR-021 HeartRateExtractor: initial HR, HRV_SDNN (partial)
|
||
- AETHER embedding: accumulates over 300 CSI frames (20 Hz × 15s)
|
||
Quality gate: BreathingExtractor VitalCoherenceGate must emit
|
||
PERMIT for ≥ 10 of the 15 seconds. Failure → FAIL_POOR_BREATHING_SIGNAL.
|
||
```
|
||
|
||
### Phase 2 — Seated Rest (T+25 to T+35)
|
||
|
||
Subject sits to minimize motion and allow cardiac signal isolation.
|
||
|
||
```
|
||
T+25s : Operator prompt: "Please sit down and rest quietly."
|
||
|
||
T+25–35s: Subject seated, minimal movement.
|
||
System collects:
|
||
- HeartRateExtractor: HR baseline, HRV_SDNN, HRV_RMSSD,
|
||
LF/HF ratio, sinus rhythm classification
|
||
- Cardiac_Waveform_Morphology: 64-coefficient wavelet decomposition
|
||
of bandpass-filtered cardiac phase signal (0.8–2.0 Hz)
|
||
Quality gate: HR confidence ≥ 0.6 for ≥ 7 of 10 seconds.
|
||
Failure → FAIL_POOR_CARDIAC_SIGNAL (soft failure: cardiac nodes
|
||
marked low-confidence; signature proceeds without them if AETHER
|
||
and gait nodes pass their own thresholds).
|
||
```
|
||
|
||
### Phase 3 — Gait Walk (T+35 to T+50)
|
||
|
||
Subject walks a 2-meter line twice in each direction.
|
||
|
||
```
|
||
T+35s : Operator prompt: "Please walk a straight line of 2 meters back and
|
||
forth twice at your natural pace."
|
||
|
||
T+35–50s: Subject walks: A→B, B→A, A→B, B→A (four transits, ≥ 8 strides total).
|
||
System collects (via pose_tracker.rs, ADR-029 Sect 2.7):
|
||
- GaitTimingNode: cadence, stride_period_variance,
|
||
double_support_pct, asymmetry_index, step_width_m
|
||
- SkeletalProportionsNode: torso/limb ratios from 17-keypoint
|
||
trajectory accumulated over ≥ 8 strides
|
||
- AETHER embedding: continues accumulating (300 more frames)
|
||
Quality gate: ≥ 8 strides detected with confidence ≥ 0.7 per stride.
|
||
Failure → FAIL_INSUFFICIENT_GAIT_DATA.
|
||
Note: the ruvector-mincut DynamicPersonMatcher must confirm only one
|
||
person is tracked. If two tracks are active → FAIL_MULTIPLE_SUBJECTS.
|
||
```
|
||
|
||
### Phase 4 — Standing Orientation Scan (T+50 to T+60)
|
||
|
||
Subject stands at three orientations to capture the subcarrier reflection profile.
|
||
|
||
```
|
||
T+50s : Operator prompt: "Please stand facing the wall. I will ask you to
|
||
rotate in place twice."
|
||
|
||
T+50–53s: Orientation 0° (subject faces primary node cluster).
|
||
System collects: SubcarrierReflectionProfile at 0°
|
||
(ADR-030 field-subtracted, 56 subcarriers, amplitude + phase).
|
||
|
||
T+53s : Operator prompt: "Please turn 90 degrees to your right."
|
||
|
||
T+53–56s: Orientation 90°.
|
||
System collects: SubcarrierReflectionProfile at 90°.
|
||
|
||
T+56s : Operator prompt: "Please turn 90 degrees to your right again."
|
||
|
||
T+56–60s: Orientation 180°.
|
||
System collects: SubcarrierReflectionProfile at 180°.
|
||
Body_Field_Coupling: computed from AETHER attention map weighted
|
||
by ADR-030 top-K=8 eigenvectors (final computation at T=60s).
|
||
|
||
T+60s : Enrollment window closes.
|
||
AETHER embedding finalized: mean pool over all ~1,200 accumulated frames.
|
||
All node confidence values computed.
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Quality Gates
|
||
|
||
The enrollment FAILS and emits a structured error code if any of the following
|
||
conditions are met. Failed enrollments do not produce a stored `.rvf` file.
|
||
|
||
| Gate | Condition for FAIL | Error code |
|
||
|---|---|---|
|
||
| Room occupied | Presence score ≥ 0.1 at Phase 0 end | `FAIL_ROOM_NOT_EMPTY` |
|
||
| Multiple subjects | ≥ 2 active pose tracks during Phases 1–4 | `FAIL_MULTIPLE_SUBJECTS` |
|
||
| Intermittent presence | Subject exits sensing zone for > 3 consecutive seconds | `FAIL_SUBJECT_LEFT_ZONE` |
|
||
| AETHER confidence low | Final embedding confidence < 0.6 (HNSW search confidence) | `FAIL_AETHER_LOW_CONFIDENCE` |
|
||
| Breathing signal absent | VitalCoherenceGate PERMIT rate < 67% during Phase 1 | `FAIL_POOR_BREATHING_SIGNAL` |
|
||
| Gait data insufficient | Fewer than 8 strides detected with confidence ≥ 0.7 | `FAIL_INSUFFICIENT_GAIT_DATA` |
|
||
| Field model dirty | Field model age > 4 hours and recalibration refused | `FAIL_STALE_FIELD_MODEL` |
|
||
| Adversarial detection | RuvSense adversarial.rs flags physically impossible signal | `FAIL_ADVERSARIAL_SIGNAL` |
|
||
| Node count below minimum | Fewer than 2 nodes online during Phases 3–4 | `WARN_DEGRADED_MODE` (not a hard fail; produces degraded signature) |
|
||
|
||
Soft failures (cardiac signal only) do not abort the enrollment; they mark those
|
||
nodes as low-confidence and reduce the match weight for those channels at
|
||
recognition time.
|
||
|
||
---
|
||
|
||
## 4. Fast Scan (10-Second Degraded Identification)
|
||
|
||
A fast scan produces a partial query embedding, not a stored profile. It is used
|
||
for recognition of already-enrolled subjects, not for new enrollment.
|
||
|
||
```
|
||
T+0s : System checks whether field model is current (age < 4 hours).
|
||
If stale: recognition accuracy degraded; warn operator.
|
||
|
||
T+0–10s: Subject stands still at zone center, natural breathing.
|
||
System collects: AETHER embedding (200 frames, 10s at 20 Hz).
|
||
Cardiac HR: partial (confidence typically < 0.5).
|
||
Gait: not available.
|
||
Subcarrier reflection: 1 orientation only.
|
||
|
||
T+10s : Query issued against all stored profiles in HNSW index.
|
||
Match score computed using available channels only.
|
||
Cardiac, gait, and skeletal proportions excluded from denominator
|
||
(availability factor = 0 for absent channels).
|
||
```
|
||
|
||
Fast scan is acceptable for:
|
||
- Returning resident recognition (already enrolled, low-friction use case)
|
||
- Home automation triggers (occupancy attribution per ADR-115 HA-MIND)
|
||
|
||
Fast scan is NOT acceptable for:
|
||
- Initial enrollment
|
||
- High-assurance access control
|
||
- Healthcare identification
|
||
|
||
---
|
||
|
||
## 5. Continuous Mode — Implicit Signature Refinement
|
||
|
||
In continuous operating mode, the system incrementally updates the online
|
||
aggregator for enrolled persons as they go about their normal activities. The
|
||
stored profile is re-published from the aggregator every 90 days (or on the
|
||
re-scan cadence, whichever comes first). This means a deployed system becomes
|
||
more accurate over time, not less.
|
||
|
||
Convergence property: the Welford online statistics in the aggregator are
|
||
numerically stable and converge to the true population mean/variance as
|
||
observation count increases. The AETHER embedding accumulated over thousands
|
||
of natural-activity windows is more representative than a single 60-second
|
||
enrollment. The stored profile is replaced (not amended) on each re-publish; the
|
||
old profile is archived (not deleted) per the forward-secrecy requirements in
|
||
`security.md`.
|
||
|
||
The continuous mode raises a consent concern: a person is effectively being
|
||
re-enrolled continuously without explicit action. This is addressed in
|
||
`security.md §4` (Consent Architecture).
|
||
|
||
---
|
||
|
||
## 6. Multi-Room Enrollment
|
||
|
||
When a person moves across multiple sensing zones (e.g., living room and bedroom
|
||
each with a Cognitum Seed node cluster), the cross-room signature works as follows:
|
||
|
||
1. Full 60-second enrollment is performed in the primary room. This produces the
|
||
initial stored profile with `environment_normalized: false` in the manifest.
|
||
|
||
2. When the MERIDIAN domain generalization layer (ADR-027) is active, the
|
||
HardwareNormalizer maps the enrollment embedding to the environment-invariant
|
||
subspace. The stored profile is updated to `environment_normalized: true`.
|
||
|
||
3. In subsequent rooms, a fast scan (10s) is sufficient to attribute identity. The
|
||
MERIDIAN-normalized AETHER embedding handles the room shift.
|
||
|
||
4. For healthcare deployments requiring room-by-room re-enrollment for regulatory
|
||
reasons, a per-room enrollment protocol runs in each room and the signatures
|
||
are linked by the opaque `person_id` field (never by raw PII).
|
||
|
||
---
|
||
|
||
## 7. Re-Scan Cadence
|
||
|
||
| Deployment context | Re-scan interval | Rationale |
|
||
|---|---|---|
|
||
| Healthy adult (residential) | 90 days | Anatomy stable; continuous mode refines continuously |
|
||
| Child (growing skeleton) | 30 days | Skeletal proportions change; gait timing changes |
|
||
| Healthcare / clinical | Per clinical event | Post-surgery, post-illness, post-significant weight change |
|
||
| Post-exercise monitoring | 7 days during active programs | Body composition changes affect RF backscatter |
|
||
| Any | On drift alert from longitudinal.rs (ADR-030 Tier 4) | System-initiated; shown to user as "calibration recommended" |
|
||
|
||
The `longitudinal.rs` module monitors five drift metrics (GaitSymmetry,
|
||
StabilityIndex, BreathingRegularity, MicroTremor, ActivityLevel) using Welford
|
||
statistics over daily observations. When any metric exceeds 2-sigma deviation
|
||
sustained for 3 consecutive days, a `DriftAlert` is emitted. The system
|
||
displays this as "signature drift detected — re-scan recommended," not as a
|
||
health diagnosis.
|
||
|
||
---
|
||
|
||
## 8. Output Artifact
|
||
|
||
On successful completion, the enrollment pipeline produces:
|
||
|
||
1. `signature-<sha256>.rvf` — the binary soul signature container. Content-addressed.
|
||
Encrypted with the person's key (see `security.md §5`) before writing to disk.
|
||
|
||
2. `signature-<sha256>.json` — the JSON-LD sidecar for human inspection and audit.
|
||
Does not contain raw vector data. Safe to log.
|
||
|
||
3. A row in the local HNSW index (`ruvector-core::VectorIndex`, `person_track`
|
||
subindex per ADR-024 §2.4) linking the person_id to the AETHER embedding.
|
||
This index is used for O(log n) recognition queries.
|
||
|
||
4. An Ed25519 witness entry per ADR-110, signing
|
||
`(rvf_sha256 || timestamp_ns || enrolled_by_device_id)`. Stored in the
|
||
RVF SEG_WITNESS segment AND in the node's local audit log.
|
||
|
||
The enrollment process does NOT:
|
||
- Transmit raw CSI or raw biometrics to any external server.
|
||
- Publish the soul signature to MQTT or Matter unless explicitly configured with
|
||
`--privacy-mode disabled` (see `security.md §6`).
|
||
- Store PII (name, email, account linkage) in the `.rvf` file. The `person_id`
|
||
field is an opaque u64. PII linkage, if any, lives in the application layer
|
||
and is governed by separate access control.
|