# ADR-117: pip `wifi-densepose` modernization via PyO3 + maturin bindings

| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Date** | 2026-05-24 |
| **Deciders** | ruv |
| **Codename** | **PIP-PHOENIX** — rising from a pure-Python server to Rust-core Python bindings |
| **Relates to** | [ADR-021](ADR-021-esp32-vitals.md) (ESP32 vitals), [ADR-028](ADR-028-esp32-capability-audit.md) (capability audit / witness), [ADR-115](ADR-115-home-assistant-integration.md) (HA-DISCO + HA-MIND MQTT semantics), [ADR-116](ADR-116-cog-ha-matter-seed.md) (HA-COG Seed packaging) |
| **Tracking issue** | TBD — file under RuView issue tracker |

---

## 1. Context

### 1.1 What the pip package is today

`wifi-densepose` v1.1.0 was published to PyPI on **2025-06-07** (two releases the same
day: 1.0.0 at 13:24 UTC, 1.1.0 at 17:02 UTC). Both wheels carry the tag
`py3-none-any` — no compiled extension, no platform-specific code. The package is a
**pure-Python server application** sourced entirely from `archive/v1/`.

The package installs a 40-dependency stack including FastAPI, PyTorch, SQLAlchemy,
Redis, Celery, OpenCV, asyncpg, psycopg2, and Scapy (`archive/v1/setup.py:46–87`).
The declared entry points are:

```
wifi-densepose = src.cli:cli
wdp             = src.cli:cli
```

(`archive/v1/setup.py:178–179`)

The public API surface is centred on a FastAPI HTTP server, a SQLAlchemy/postgres
database layer, and a Redis/Celery task queue — none of which map to the current Rust
architecture. The `__init__.py` exports `app` (FastAPI), `CSIProcessor`,
`PhaseSanitizer`, `PoseEstimator`, `RouterInterface`, `ServiceOrchestrator`,
`HealthCheckService`, and `MetricsService` (`archive/v1/src/__init__.py:54–68`).

### 1.2 Why this matters now

ADR-115 (PR #778, merged 2026-05-23) shipped 21 Home Assistant entities, 10 semantic
primitives, mTLS, privacy mode, and a full witness bundle from the Rust crate
`wifi-densepose-sensing-server`. ADR-116 is packaging this as a Cognitum Seed cog.
Neither surface is reachable from `pip install wifi-densepose` — the pip package cannot
import a CsiFrame, decode an edge-vitals packet, call a DSP stage, verify a witness
bundle, or subscribe to the sensing server's MQTT or WebSocket endpoints. The ecosystem
split is now wide enough that the pip package actively misleads new users about what
the project does.

Three concrete customer pain points:

1. A Python user who `pip install wifi-densepose` expecting to consume live pose/vitals
   data gets a FastAPI server that requires postgres + redis, not a library they can
   script against.
2. Integrators writing HA automations or Node-RED flows in Python have no idiomatic
   Python API for the v0.7 telemetry surface (ADR-115 entities, semantic primitives).
3. The ADR-028 witness chain (deterministic pipeline proof) is Python-based and
   exercised via `archive/v1/data/proof/verify.py`, but it imports from the v1 stack —
   it cannot witness the Rust pipeline that is now the production implementation.

### 1.3 What this ADR is *not*

- Not a removal of `archive/v1/` from the repository. The v1 codebase stays as a
  research archive and its proof bundle stays in `archive/v1/data/proof/`.
- Not a port of the Rust crates to Python. The Rust workspace (`v2/`) is authoritative
  and unmodified by this ADR.
- Not a replacement of the `wifi-densepose-sensing-server` Rust binary. The pip
  package wraps or clients the binary; it does not reimplement it.
- Not an overlap with ADR-116 (Seed cog packaging). ADR-116 ships a Seed-installable
  artifact; ADR-117 ships a Python developer library for scripting, automation, and
  prototyping against the Rust stack.

---

## 2. Current state — evidence

| Artifact | Value | Source |
|---|---|---|
| Latest PyPI version | **1.1.0** | `pypi.org/pypi/wifi-densepose/json` |
| First release date | 2025-06-07T13:24:53Z | PyPI JSON metadata |
| Latest release date | 2025-06-07T17:02:40Z | PyPI JSON metadata |
| Months since last release | **~11.5 months** | as of 2026-05-24 |
| Wheel tag | `py3-none-any` | PyPI simple index |
| Hard dependencies | 40 (torch, fastapi, sqlalchemy, redis, celery, …) | `setup.py:46–87` |
| Entry point | `src.cli:cli` | `setup.py:178` |
| Python requires | `>=3.9` | `setup.py:108` |
| Classifiers Python versions | 3.9, 3.10, 3.11, 3.12 | PyPI JSON classifiers |
| Classifiers status | Beta (4) | PyPI JSON classifiers |
| Current Rust workspace version | **0.3.0** | `v2/Cargo.toml:version` |
| Rust crates in workspace | 20+ | `v2/Cargo.toml` members |
| ADR-115 shipped | 2026-05-23 | PR #778 |

The v1 source package (`archive/v1/setup.py:112–215`) was clearly designed as an
all-in-one server application, not a reusable library. The `find_packages` call at
line 134 searches from `"."` (the archive root), meaning the wheel ships `src.*` as the
importable namespace. The proof bundle (`archive/v1/data/proof/verify.py:56–57`) imports
`src.hardware.csi_extractor.CSIData` and `src.core.csi_processor.CSIProcessor` — v1 pure
Python only.

**PyPI org presence check:** a search for other `ruvnet`-published PyPI packages
(`ruvector`, `claude-flow`) returned no matches in the PyPI simple index as of this
writing. The `wifi-densepose` package is currently the only Python entry point for this
project's ecosystem.

---

## 3. Gap analysis

| Capability | Rust crate(s) | pip v1.1.0 status | Gap severity |
|---|---|---|---|
| `CsiFrame` / `CsiMetadata` core types | `wifi-densepose-core` (`types.rs`) | Not present — v1 uses `CSIData` Python class | **Critical** |
| HR/BR extraction from CSI buffer | `wifi-densepose-vitals` (4-stage pipeline: preprocessor → breathing → heartrate → anomaly) | Stub Python (`src/hardware/csi_extractor.py`) with no DSP | **Critical** |
| Phase sanitization / noise removal | `wifi-densepose-signal` (`phase_sanitizer`, `csi_processor`, `hampel`) | Python stubs in `src/core/phase_sanitizer.py` | **Critical** |
| Motion detection + presence scoring | `wifi-densepose-signal` (`motion.rs`, `MotionDetector`) | Not present | **Critical** |
| RuvSense multistatic sensing (13 modules) | `wifi-densepose-signal/src/ruvsense/` | Not present — ADR-029 post-dates v1 | **Critical** |
| 17-keypoint pose estimation | `wifi-densepose-nn`, `wifi-densepose-mat` | Stub `PoseEstimator` wrapping a `torch.nn.Module` that requires model weights | **High** |
| MQTT publisher (21 HA entities) | `wifi-densepose-sensing-server/src/mqtt/` | Not present — ADR-115 post-dates v1 | **High** |
| Semantic primitives (10 types) | `wifi-densepose-sensing-server/src/semantic/` | Not present | **High** |
| Matter bridge | `wifi-densepose-sensing-server/src/matter/` | Not present | **High** |
| WS/REST client for sensing-server | `wifi-densepose-sensing-server` (Axum) | v1 has a separate FastAPI server; no client | **High** |
| Witness bundle verification | ADR-028 / `scripts/generate-witness-bundle.sh` | `archive/v1/data/proof/verify.py` — proves v1 pipeline only | **High** |
| ESP32-C6 firmware telemetry (ADR-110) | `wifi-densepose-hardware` + `wifi-densepose-sensing-server` | Not present | **Medium** |
| Cross-viewpoint fusion (RuVector) | `wifi-densepose-ruvector/src/viewpoint/` | Not present | **Medium** |
| Semantic-primitive MQTT payload | `wifi-densepose-sensing-server/src/semantic/bus.rs` | Not present | **Medium** |
| PostgreSQL + Redis server mode | `archive/v1/` | Present (v1 only) | Low (not SOTA) |
| FastAPI HTTP REST server | `archive/v1/src/app.py` | Present (v1 only) | Low (not SOTA) |

---

## 4. Decision

Adopt **PyO3 + maturin Python extension bindings** as the primary modernization path,
shipping the pip package as a platform-native wheel (`manylinux`, `macosx`, `win-amd64`)
with compiled Rust extension modules, plus a pure-Python WS/MQTT client layer that talks
to a running `wifi-densepose-sensing-server` instance.

This path is called **PIP-PHOENIX**.

### 4.1 Why PyO3 + maturin over the three rejected alternatives

| Criterion | **PyO3 + maturin** (chosen) | Subprocess wrapper | REST/WS client only | Pure Python reimpl |
|---|---|---|---|---|
| Performance for DSP | Native Rust speed, zero copy | IPC overhead per call | N/A — no local DSP | Python bottleneck |
| Binary size in wheel | Core + vitals + signal only: ~2 MB stripped | Full sensing-server binary: ~15–30 MB | Minimal (~50 kB) | Minimal (~100 kB) |
| Works offline / no server | Yes | Yes (binary bundled) | No — server required | Partial |
| Proof bundle can cover Rust pipeline | Yes — bindings call the same Rust code the server uses | Partial — server is a black box | No | No |
| Install experience | `pip install wifi-densepose` — wheel has no system deps | `pip install` downloads 25 MB binary | `pip install` — pure Python | `pip install` — pure Python |
| Maintenance surface | Python bindings + Rust workspace | Python thin shim | Python client | Python reimpl must track Rust |
| Async / tokio support | PyO3 0.28 `pyo3-asyncio` or `pyo3-async-runtimes` for async export; sync entry points for the DSP hot path | N/A | Native asyncio on client | N/A |
| GIL concern | DSP-heavy calls release GIL via `py.allow_threads`; tokio runtime per module | N/A | None | N/A |
| Fits existing architecture | Core + vitals + signal already have clean public APIs (`lib.rs` re-exports) | Requires sensing-server to be running | Requires sensing-server | Forks the domain model |

**Subprocess wrapper** is rejected because shipping a 25 MB pre-built server binary
inside every pip wheel is an unacceptably heavy install, and it makes offline scripting
impossible without starting the server.

**REST/WS client only** is rejected because it provides zero DSP utility offline and
cannot close the witness gap — the proof bundle must exercise the same pipeline code.

**Pure Python reimplementation** is the root cause of the current drift and is
explicitly rejected.

The chosen path starts small: **bind only the three crates with the highest Python
utility** (`wifi-densepose-core`, `wifi-densepose-vitals`, `wifi-densepose-signal`),
ship a `py3-none-any` pure-Python WS/MQTT client layer as a separate sub-module, and
grow from there.

---

## 5. Detailed design

### 5.1 Rust crates bound in v2.0 (first wheel)

Three crates are in scope for the initial binding. They were chosen because they have
no heavy system dependencies (no libtorch, no ONNX runtime), have stable `pub` re-export
surfaces in `lib.rs`, and directly address the three most-requested missing capabilities.

| Crate | Exported Python types / functions | Binding rationale |
|---|---|---|
| `wifi-densepose-core` | `CsiFrame`, `CsiMetadata`, `Keypoint`, `KeypointType`, `PersonPose`, `PoseEstimate`, `Confidence`, `BoundingBox` | Foundation types shared by all other crates; without these users can't even describe a frame |
| `wifi-densepose-vitals` | `CsiVitalPreprocessor`, `BreathingExtractor`, `HeartRateExtractor`, `VitalAnomalyDetector`, `VitalSignStore`, `VitalReading`, `VitalEstimate`, `AnomalyAlert` | The most-asked-for surface: HR/BR from a CSI buffer in 4 lines of Python |
| `wifi-densepose-signal` | `CsiProcessor`, `CsiProcessorConfig`, `PhaseSanitizer`, `MotionDetector`, `MotionScore`, `FeatureExtractor`, `HardwareNormalizer` | DSP pipeline that produces the features vitals and pose estimation consume |

Crates **deferred to P6+**: `wifi-densepose-nn` (requires libtorch or candle — wheel
size risk), `wifi-densepose-mat` (depends on nn), `wifi-densepose-ruvector` (RuVector
GNN types — high value but adds ruvector-gnn 2.0.5 link dependency),
`wifi-densepose-hardware` (ESP32 HAL — not Python-scripting friendly).

### 5.2 New workspace member: `python/`

A new crate `python/` is added as a workspace member at `v2/crates/wifi-densepose-py/`.
It is a `cdylib` that re-exports the three bound crates behind a single maturin module
named `wifi_densepose._core`.

```toml
# v2/crates/wifi-densepose-py/Cargo.toml (sketch)
[package]
name = "wifi-densepose-py"
version.workspace = true
edition.workspace = true

[lib]
name = "_core"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.28", features = ["extension-module", "abi3-py310"] }
wifi-densepose-core   = { path = "../wifi-densepose-core", features = ["serde"] }
wifi-densepose-vitals = { path = "../wifi-densepose-vitals" }
wifi-densepose-signal = { path = "../wifi-densepose-signal" }
```

The `abi3-py310` feature locks the stable ABI to CPython 3.10+, so one wheel binary
works across 3.10, 3.11, 3.12, and 3.13 without recompilation.

PyO3 bindings pattern (example for `CsiFrame`):

```rust
// v2/crates/wifi-densepose-py/src/core_types.rs
use pyo3::prelude::*;
use wifi_densepose_core::CsiFrame as RustCsiFrame;

#[pyclass(name = "CsiFrame")]
#[derive(Clone)]
pub struct PyCsiFrame {
    inner: RustCsiFrame,
}

#[pymethods]
impl PyCsiFrame {
    #[new]
    fn new(amplitudes: Vec<f32>, phases: Vec<f32>, n_subcarriers: usize,
           sample_index: u64, sample_rate_hz: f32) -> Self {
        Self { inner: RustCsiFrame { amplitudes, phases, n_subcarriers,
                                     sample_index, sample_rate_hz } }
    }

    #[getter] fn amplitudes(&self) -> Vec<f32> { self.inner.amplitudes.clone() }
    #[getter] fn phases(&self) -> Vec<f32> { self.inner.phases.clone() }
    #[getter] fn n_subcarriers(&self) -> usize { self.inner.n_subcarriers }
}
```

DSP calls that execute >1 ms release the GIL:

```rust
#[pymethods]
impl PyCsiProcessor {
    fn process<'py>(&mut self, py: Python<'py>, frame: &PyCsiFrame)
        -> PyResult<Option<PyProcessedSignal>>
    {
        py.allow_threads(|| self.inner.process(&frame.inner))
            .map(|opt| opt.map(PyProcessedSignal::from))
            .map_err(|e| PyRuntimeError::new_err(e.to_string()))
    }
}
```

### 5.3 pip package layout

```
wifi-densepose/                  ← PyPI package name (unchanged)
  wifi_densepose/                ← importable namespace
    __init__.py                  ← re-exports core types + version
    _core.pyd / _core.so         ← compiled PyO3 extension (maturin build output)
    vitals.py                    ← thin Python wrapper + docstrings over _core vitals types
    signal.py                    ← thin Python wrapper over _core signal types
    client/
      __init__.py
      ws.py                      ← asyncio WebSocket client for sensing-server /ws/sensing
      mqtt.py                    ← paho-mqtt wrapper for ruview/<node_id>/raw/* topics
      ha.py                      ← helpers for HA-DISCO payloads (read-only, mirrors ADR-115 §3.2)
    witness/
      __init__.py
      verify.py                  ← Python-callable witness verifier (re-creates ADR-028 proof
                                     over the Rust pipeline via PyO3 bindings, not archive/v1/)
    compat/
      v1.py                      ← import shim that raises MigrationError (see §9)
    py.typed                     ← PEP 561 marker
```

The import path intentionally maps to Rust crate names:

```python
from wifi_densepose import CsiFrame           # core types
from wifi_densepose.vitals import BreathingExtractor, HeartRateExtractor
from wifi_densepose.signal import CsiProcessor, MotionDetector
from wifi_densepose.client.ws import SensingClient
from wifi_densepose.witness import verify_bundle
```

### 5.4 PyPI distribution — wheel matrix

Published as `wifi-densepose==2.0.0` using **cibuildwheel** driven by GitHub Actions.

| Platform | Arch | CPython | Tag (stable ABI) |
|---|---|---|---|
| `manylinux_2_28` | x86_64 | 3.10+ | `cp310-abi3-manylinux_2_28_x86_64` |
| `manylinux_2_28` | aarch64 | 3.10+ | `cp310-abi3-manylinux_2_28_aarch64` |
| `macosx_11_0` | x86_64 | 3.10+ | `cp310-abi3-macosx_11_0_x86_64` |
| `macosx_11_0` | arm64 | 3.10+ | `cp310-abi3-macosx_11_0_arm64` |
| `win` | amd64 | 3.10+ | `cp310-abi3-win_amd64` |
| sdist | — | — | source fallback |

The `abi3-py310` flag means **one binary per OS/arch** covers all supported Python
versions — 5 wheels total plus an sdist, compared to the 20-wheel matrix that would be
needed without stable ABI.

```yaml
# .github/workflows/pip-release.yml (sketch)
- uses: pypa/cibuildwheel@v2
  with:
    package-dir: v2/crates/wifi-densepose-py
    output-dir: dist
  env:
    CIBW_BUILD: "cp310-*"
    CIBW_ARCHS_LINUX: "x86_64 aarch64"
    CIBW_ARCHS_MACOS: "x86_64 arm64"
    CIBW_ARCHS_WINDOWS: "AMD64"
    CIBW_BEFORE_BUILD: "pip install maturin"
    CIBW_BUILD_FRONTEND: "build[uv]"
```

### 5.5 CLI parity

The pip wheel installs a `wifi-densepose` console script. In v2 this script is a thin
Python shim that:

1. Checks whether `wifi-densepose-sensing-server` binary is on `PATH` (installed
   separately via a platform-specific binary distribution or `cargo install`).
2. If found: proxies `wifi-densepose serve`, `wifi-densepose stream`, etc. to the Rust
   binary via `subprocess.run`.
3. If not found: falls back to the PyO3 module for offline DSP commands
   (`wifi-densepose vitals --file recording.jsonl`).

This is explicitly **not** a reimplementation of the CLI — the Rust binary
(`wifi-densepose-cli/src/main.rs`, currently exposes `mat` and `version` subcommands)
is the authoritative CLI. The pip shim is a discovery/convenience layer.

### 5.6 WS/MQTT client layer

`wifi_densepose.client.ws.SensingClient` is a pure-Python asyncio client wrapping the
sensing-server WebSocket at `/ws/sensing`:

```python
async with SensingClient("ws://localhost:8765/ws/sensing") as client:
    async for msg in client.stream():
        if msg.type == "edge_vitals":
            print(msg.breathing_rate_bpm, msg.heartrate_bpm)
```

`wifi_densepose.client.mqtt.RuViewMqttClient` wraps paho-mqtt and subscribes to
`ruview/<node_id>/raw/+` as defined in ADR-115 §3.2.

Both clients are **pure Python** (no PyO3) and are optional dependencies (`pip install
wifi-densepose[client]`). They depend on `websockets>=12` and `paho-mqtt>=2` respectively.

### 5.7a Beamforming Feedback Loop Data (BFLD) support — new binding target

**Added 2026-05-24 per maintainer feedback during P3 implementation.**

BFLD is the transmitter-side, AP-station-loop view of the WiFi channel
— compressed beamforming feedback frames that 802.11ac/ax/be stations
send to the AP per sounding cycle. From a sensing perspective it
complements receiver-side CSI:

| | Receiver-side CSI (current) | BFLD (this addition) |
|---|---|---|
| Source | RX side of the radio (e.g. Nexmon CSI on Pi 5, ESP32 promisc cb) | Sniffed BFR frames in the air or `mac80211` ACK trace |
| Subcarriers (HE20) | 52 (HT-LTF) or 242 (HE-LTF) | Up to 996 (HE160 compressed BFR) — denser |
| Hardware requirements | Patched Broadcom/Cypress or ESP32 specifically | **Any** 802.11ac+ station-AP pair — no patched firmware |
| Privacy model | Captures everyone in radio range | Same |
| Maturity in repo | Production (ADR-014, ADR-018, ADR-039) | Research; no Rust crate yet |
| Suitable use case | Through-wall pose + vitals | Dense subcarrier reflection profile for AETHER-class biometric (ADR-024) and the soul-signature spec (`docs/research/soul/`) |

#### Binding strategy

Because the Rust workspace has no `wifi-densepose-bfld` crate yet, P3
ships a **forward-compatible Python trait surface** that the future
Rust crate plugs into without changing the Python API:

```python
from wifi_densepose import BfldFrame, BfldReport

# Today (P3): construct from a parsed BFR feedback matrix (the bring-
# your-own-parser path). Users on Pi 5 + Wireshark BFR dissector
# pipe frames in directly.
frame = BfldFrame.from_compressed_feedback(
    timestamp_ms=…,
    sounding_index=…,
    sta_mac="aa:bb:cc:…",
    bandwidth_mhz=80,
    n_subcarriers=996,
    feedback_matrix=…,  # numpy ndarray complex64 [Nr × Nc × Nsc]
)

# P3 also ships a stub `BfldReport` aggregator that mirrors how
# `VitalEstimate` aggregates `VitalReading`s. Users who have BFR
# pipelines feeding RuView can use this today via the
# bring-your-own-parser path.

# Tomorrow (post-v2.0): the `wifi-densepose-bfld` Rust crate (TBD —
# separate ADR-1xx) provides ingestion from Nexmon `nl80211` traces +
# kernel `mac80211` debugfs hooks, and the pip wheel transparently
# binds it without changing this Python surface.
```

#### Why this matters

Three reasons BFLD belongs in v2.0 rather than waiting for the Rust
core:

1. **Customer pull**. Several integrators reading the ADR-115 release
   notes asked about WiFi-6 dense-subcarrier capture; the answer is
   BFLD, and we want the API stable before they build pipelines.
2. **Soul-signature dependency**. The soul-signature research spec
   (`docs/research/soul/specification.md`) lists "Subcarrier Reflection
   Profile" as one of seven biometric channels. At HE20/HE80 the
   dense BFR subcarriers are the right input — exposing `BfldFrame`
   now lets researchers prototype the channel without waiting on a
   Rust ingestion crate.
3. **Cross-vendor portability**. CSI ingestion needs patched
   firmware. BFR ingestion works on stock 802.11ac/ax hardware
   (capture via `tcpdump`/Wireshark + a BFR dissector). Shipping the
   Python data structures first gives the community a way to feed
   RuView from gear we don't directly support.

#### Implementation surface in P3

Lands as a new module `bindings/bfld.rs` (~150 lines, three
`#[pyclass]` types):

- `BfldFrame` (frozen) — one compressed feedback matrix snapshot.
  Constructors: `from_compressed_feedback(...)` and
  `from_uncompressed_v(...)` (the 802.11n V-matrix form).
  Properties: `timestamp_ms`, `sounding_index`, `sta_mac`,
  `bandwidth_mhz`, `n_subcarriers`, `n_rows` (Nr), `n_cols` (Nc),
  `feedback_matrix` (numpy ndarray complex64).
- `BfldReport` (frozen) — aggregator over a window of `BfldFrame`s.
  Properties: `n_frames`, `timestamp_first`, `timestamp_last`,
  `mean_amplitude_per_subcarrier`, `coherence_score`. The Python
  side gives users a stable handle for "all BFR data in this 60-s
  scan" without leaking the storage representation.
- `BfldKind` (`#[pyclass(eq, eq_int, hash, frozen)]`) — enum
  enumerating the BFR variants we support: `CompressedHE20`,
  `CompressedHE40`, `CompressedHE80`, `CompressedHE160`,
  `UncompressedHT20`, `UncompressedHT40`.

Stub Rust implementation lives in `python/src/bfld_stub.rs` until
the proper Rust crate exists; it's intentionally not in v2/crates/.
A new ADR-1xx will own the Rust ingestion crate when we commit to it.

#### Open questions added

- §9.11 — Should BFLD ingestion live in a new `wifi-densepose-bfld`
  crate or in `wifi-densepose-signal` extended?
- §9.12 — Per-vendor BFR variant compatibility (Broadcom vs Intel vs
  Qualcomm encode the compressed angles slightly differently) — how
  much normalisation belongs in the Python binding vs. the future
  Rust crate?

### 5.7 Witness chain (re-rooted to the Rust pipeline)

`wifi_densepose.witness.verify_bundle(path)` replaces the v1 proof verification with a
new chain that exercises the Rust pipeline via PyO3:

```python
from wifi_densepose.witness import verify_bundle

result = verify_bundle("dist/witness-bundle-ADR028-*/")
assert result.verdict == "PASS", result.detail
```

Internally it:
1. Loads the 1,000-frame reference JSON from the bundle.
2. Feeds each frame through `PyCsiProcessor` (PyO3 binding of the Rust `CsiProcessor`).
3. Hashes the output using the same SHA-256 scheme as `archive/v1/data/proof/verify.py`.
4. Compares against the published hash in `expected_features.sha256`.

The v1 proof (`archive/v1/data/proof/verify.py`) is **preserved unchanged** — it
continues to prove the v1 pipeline. The new `witness.py` proves the v2/Rust pipeline.
Both can coexist; the ADR-028 witness bundle ships with both.

---

## 6. Migration path (phased)

```
P1  ──►  P2  ──►  P3  ──►  P4  ──►  P5  ──►  P6+
scaffold  core   vitals+   client   publish  deferred
          types  signal    layer    v2.0.0
```

### P1 — Scaffold (1 week)

- [ ] Add `v2/crates/wifi-densepose-py/` as workspace member.
- [ ] `Cargo.toml`: `crate-type = ["cdylib"]`, pyo3 0.28 + `abi3-py310`, no
  workspace deps yet (empty module compiles and imports).
- [ ] `pyproject.toml` at repo root `python/` with `[build-system] requires =
  ["maturin>=1.8"]` and `[tool.maturin] features = ["pyo3/extension-module"]`.
- [ ] CI job: `maturin develop` on ubuntu-latest in a Python 3.12 venv; import
  `wifi_densepose._core` succeeds.
- [ ] Publish `wifi-densepose==1.99.0` to PyPI with a migration notice in the
  module body (see §9 — no new features, just the tombstone release).

### P2 — Core type bindings (1 week)

- [ ] Bind `CsiFrame`, `CsiMetadata`, `Confidence`, `Keypoint`, `KeypointType`,
  `BoundingBox`, `PoseEstimate`, `PersonPose` from `wifi-densepose-core`.
- [ ] All types: `__repr__`, `__eq__`, `__hash__` where meaningful; serde JSON
  round-trip via `pyo3-serde` or manual `to_dict()` / `from_dict()`.
- [ ] Add `py.typed` + stub `.pyi` file generated by `pyo3-stub-gen`.
- [ ] Unit tests: `tests/test_core.py` — construct each type, round-trip JSON.

### P3 — Vitals + signal DSP bindings (2 weeks)

- [ ] Bind the full 4-stage vitals pipeline:
  `CsiVitalPreprocessor`, `BreathingExtractor`, `HeartRateExtractor`,
  `VitalAnomalyDetector`, `VitalSignStore`, `VitalReading`, `VitalEstimate`,
  `AnomalyAlert`.
- [ ] Bind signal DSP entry points: `CsiProcessor`, `CsiProcessorConfig`,
  `PhaseSanitizer`, `MotionDetector`, `HardwareNormalizer`.
- [ ] GIL release (`py.allow_threads`) on all calls >0.5 ms (measured in bench).
- [ ] Integration test: feed 1,000 frames from `archive/v1/data/proof/sample_csi_data.json`
  through the PyO3 vitals pipeline; assert output is deterministic across runs.
- [ ] Re-implement `witness/verify.py` using P3 bindings; compare SHA-256 against the
  v1 expected hash. **Note:** the hash will differ because the Rust and Python
  processors are not identical — generate and publish a new `expected_features_v2.sha256`.

### P4 — WS/MQTT client layer (1 week)

- [ ] Implement `wifi_densepose.client.ws.SensingClient` (asyncio, `websockets>=12`).
- [ ] Implement `wifi_densepose.client.mqtt.RuViewMqttClient` (paho-mqtt 2.x).
- [ ] Add `wifi_densepose.client.ha` helpers that parse ADR-115 MQTT discovery payloads
  into Python dataclasses.
- [ ] Integration test: spin up `sensing-server` in Docker with `--mock-frames`;
  assert `SensingClient` receives `edge_vitals` messages.

### P5 — First cibuildwheel publish as v2.0.0 (1 week)

- [ ] `.github/workflows/pip-release.yml` — cibuildwheel matrix (5 wheels + sdist).
- [ ] `python_requires = ">=3.10"` (stable ABI base).
- [ ] Populate `pyproject.toml` with minimal `install_requires`: `pyo3` is a build dep,
  not a runtime dep. Runtime extras: `[client]` adds `websockets>=12,paho-mqtt>=2`.
- [ ] `pip install wifi-densepose==2.0.0` and smoke-test on each CI platform.
- [ ] PyPI publish via Trusted Publisher (OIDC, no API token in secrets).
- [ ] Announce: `wifi-densepose==1.99.0` tombstone already on PyPI; `v2.0.0` replaces
  it in search results.

### P3.5 — BFLD binding surface (concurrent with P3)

**Added 2026-05-24 per maintainer feedback.** See §5.7a for the rationale.

- [ ] `python/src/bindings/bfld.rs` — `BfldFrame`, `BfldReport`,
  `BfldKind` `#[pyclass]` wrappers backed by a stub Rust impl
  pending the v3 `wifi-densepose-bfld` crate.
- [ ] `python/src/bfld_stub.rs` — minimal in-crate stub storage
  (vec of compressed feedback matrices) so the Python API is
  fully usable today even before the Rust ingestion crate lands.
- [ ] Numpy bridge for `feedback_matrix` (Complex64 ndarray) — same
  approach as `CsiFrame.amplitude` from P3.
- [ ] Tests covering: per-bandwidth constructor paths
  (HE20/HE40/HE80/HE160 + HT20/HT40), n_subcarriers contract,
  coherence_score sanity, BfldKind hashability + equality.
- [ ] Forward-compat contract test: `BfldFrame` constructed today
  from a numpy ndarray must round-trip through (de)serialisation
  identically once the Rust crate exists.
- [ ] §9.11 + §9.12 open questions raised so the eventual Rust crate
  has clear decisions waiting for it.

P3.5 is concurrent with P3 (no new schedule cushion needed) because
the Python surface is independent of the rest of the v2/ workspace.
Land in the same wheel as P3.

### P6+ — Deferred

- [ ] `wifi-densepose-bfld` Rust crate — proper ingestion from
  Nexmon BFR pcaps + `mac80211` debugfs. Replaces the P3.5 stub
  storage without changing the Python API. Owns its own ADR-1xx.
- [ ] `wifi-densepose-nn` bindings (libtorch / candle wheel size TBD — see Open
  Questions §13.3).
- [ ] `wifi-densepose-ruvector` bindings (RuVector attention types).
- [ ] MQTT/Matter integration helpers (`wifi_densepose.client.matter`).
- [ ] Deprecation notice on `wifi-densepose==1.x` releases (PyPI yank — see §9).
- [ ] `wifi-densepose-sensing-server` binary distribution via pip extra
  (`pip install wifi-densepose[server]` fetches pre-built binary for the platform).
- [ ] HACS Python integration built on top of the pip client layer (follow-on to
  ADR-115 §6.A).

---

## 7. Compatibility and deprecation

### 7.1 Version bump strategy

`wifi-densepose==2.0.0` is a **hard major-version break**. The 1.x import namespace
`src.*` is incompatible with the 2.x namespace `wifi_densepose.*`. There is no shim
that can bridge them transparently.

### 7.2 Tombstone release: v1.99.0

Before publishing v2.0.0, publish `wifi-densepose==1.99.0` as a pure-Python sdist/wheel
whose sole content is:

```python
# wifi_densepose/__init__.py  (v1.99.0)
raise ImportError(
    "wifi-densepose 1.x has been superseded by v2.0.0 which wraps "
    "the Rust-based stack. Run:\n\n"
    "    pip install wifi-densepose==2.0.0\n\n"
    "Migration guide: https://github.com/ruvnet/RuView/blob/main/docs/pip-migration.md\n"
    "Legacy v1 source: archive/v1/ in the repository"
)
```

This ensures any project pinned to `wifi-densepose>=1` that upgrades to 1.99.0 gets a
clear error rather than a silent broken import.

### 7.3 PyPI yank strategy

After v2.0.0 is stable (90-day observation window):

- Yank `wifi-densepose==1.0.0` — never had a separate stable release period; was
  superseded 4 hours after publication.
- Leave `wifi-densepose==1.1.0` un-yanked but deprecated in the description.
- Publish `wifi-densepose==1.99.0` as the canonical 1.x landing page (raise error).

Yanked versions remain installable with `pip install wifi-densepose==1.1.0 --force`
so users with reproducible builds pinned to exact versions are not broken silently.

### 7.4 Semver

| Version | Content |
|---|---|
| 1.0.0 – 1.1.0 | Legacy Python server (archive/v1/) |
| **1.99.0** | Tombstone: ImportError migration notice |
| **2.0.0** | PyO3 Rust bindings + WS/MQTT client |
| 2.x.y | Additive bindings + client improvements |
| 3.0.0 | If/when nn bindings added (libtorch wheel size may force a separate package) |

---

## 8. Alternatives considered and rejected

### Alt-A: Subprocess wrapper

Package the pre-built `wifi-densepose-sensing-server` Rust binary inside the pip wheel.
Python calls it via `subprocess`. **Rejected** because: the binary is 15–30 MB stripped;
the install footprint is prohibitive; offline DSP scripting still requires the server to
be running; the witness chain cannot exercise Rust code through a black-box binary.

### Alt-B: REST/WS client only

Ship a pure-Python package that is purely a client to a running `sensing-server`
instance. **Rejected** because: it provides zero offline utility; it cannot host the
witness chain over the Rust pipeline; it solves the "Python access to telemetry" problem
but not the "Python DSP / prototyping" problem that academic and embedded users need.

### Alt-C: Pure Python reimplementation

Rewrite the DSP pipeline in pure Python/NumPy to reach parity with the Rust
implementation. **Rejected explicitly** — this is the root cause of the current 11-month
drift and the pattern this ADR is designed to exit. Any Python reimplementation will
immediately begin drifting again as the Rust stack evolves.

---

## 9. Risks

| Risk | Likelihood | Severity | Mitigation |
|---|---|---|---|
| **Build matrix complexity** — 5 target triples × cibuildwheel setup; CI time; QEMU for aarch64 cross-compile | High | Medium | Use `abi3-py310` (5 wheels not 20); QEMU aarch64 emulation available in GitHub Actions; maturin handles auditwheel automatically |
| **Binary size** — future nn/ONNX bindings may push wheel past 50 MB | Medium | High | Keep nn bindings in a separate `wifi-densepose-nn` PyPI package; keep core+vitals+signal wheel lean (~2 MB stripped) |
| **GIL / async issues** — PyO3 wrapping tokio crates requires careful runtime management; `py.allow_threads` must be used around all blocking Rust calls | High | High | Restrict initial bindings to synchronous Rust APIs (vitals, signal, core are all sync); async sensing-server client stays in pure-Python `client/ws.py` |
| **Maintainer overhead** — two languages, two build systems, one PyPI package | Medium | Medium | maturin unifies the build; CI handles publishing; start with 3 bound crates only |
| **1.x user breakage** — users pinned to `wifi-densepose>=1,<2` will get the tombstone | Low | Medium | 1.99.0 tombstone gives a clear error; maintain 1.1.0 on PyPI un-yanked for 90 days post-v2 |
| **Windows Rust toolchain in CI** — linking PyO3 on Windows requires MSVC or mingw; extra CI complexity | Medium | Medium | GitHub Actions `windows-latest` has MSVC; maturin + cibuildwheel handle this natively |
| **Stable ABI limitations** — `abi3` precludes some advanced PyO3 features (e.g. `Buffer` protocol) | Low | Low | Core/vitals/signal types are scalar/Vec<f32> — no need for buffer protocol in P2–P3 |
| **PyPI name ownership** — we own `wifi-densepose` on PyPI (confirmed via rUv author field) | Low | Low | Confirm with `pypi.org/user/ruvnet` before publishing |

---

## 10. Acceptance criteria

The following checks must all pass before ADR-117 is considered Accepted:

- [ ] `pip install wifi-densepose==2.0.0` succeeds on Python 3.10, 3.11, 3.12, 3.13
  on linux/x86_64, macos/arm64, and windows/amd64 in a clean venv with no extra build tools.
- [ ] `python -c "import wifi_densepose; print(wifi_densepose.__version__)"` prints `2.0.0`.
- [ ] `python -c "from wifi_densepose import CsiFrame; f = CsiFrame([1.0]*56, [0.0]*56, 56, 0, 100.0); print(f)"` produces a non-error repr.
- [ ] The 4-stage vitals pipeline processes 1,000 frames in under 500 ms on a
  reference machine (CPython 3.12, linux x86_64, no GPU).
- [ ] `wifi_densepose.witness.verify_bundle(path)` returns `verdict="PASS"` for a
  freshly generated witness bundle from `scripts/generate-witness-bundle.sh`.
- [ ] `wifi_densepose.client.ws.SensingClient` receives at least one `edge_vitals`
  message from a `sensing-server --mock-frames` instance within 5 seconds.
- [ ] `pip install wifi-densepose==1.99.0` raises `ImportError` with the migration URL.
- [ ] The compiled `_core` extension has no unresolved dynamic library dependencies
  beyond libc/msvcrt (verified by `auditwheel show` on Linux, `delocate-listdeps` on macOS).
- [ ] Type stubs (`wifi_densepose/*.pyi`) are present; `mypy --strict` passes on the
  example code in `examples/vitals_from_buffer.py`.
- [ ] Total wheel size for core+vitals+signal: `≤ 5 MB` per platform.

---

## 11. Open questions

1. **Stable ABI base version**: `abi3-py310` drops support for Python 3.9, which v1.1.0
   declared. Is Python 3.9 EOL-enough (EOL 2025-10-05) to drop cleanly? *Tentative: yes,
   drop 3.9. Use abi3-py310.*

2. **Package name for nn bindings**: if `wifi-densepose-nn` bindings require a 30 MB
   libtorch wheel, should they live at `wifi-densepose-nn` (separate PyPI package) or
   as an optional heavy extra of `wifi-densepose[nn]`? *Tentative: separate package to
   avoid polluting the lean wheel.*

3. **Witness hash continuity**: the Rust pipeline will produce a different SHA-256 than
   the v1 Python pipeline for the same input frames. The new `expected_features_v2.sha256`
   must be generated and committed before v2.0.0 ships. Who generates it, and how is
   the generation process itself witnessed? *Tentative: generate in CI, commit hash to
   `archive/v1/data/proof/`, include in ADR-028 matrix.*

4. **`ruv-neural` crate**: `v2/crates/ruv-neural/` exists in the workspace. Is it a
   candidate for early Python bindings (useful for training-loop scripting), or should
   it wait for the nn/train tier? *Tentative: defer — it depends on training backends.*

5. **Tokio runtime**: `wifi-densepose-sensing-server` is tokio-based, but the three
   crates bound in P2–P3 (`core`, `vitals`, `signal`) are synchronous. Are there any
   hidden tokio dependencies that would force a runtime into the extension module?
   *Tentative: inspect each crate's Cargo.toml for tokio deps before P1 scaffold.*

6. **`pyo3-stub-gen` vs manual stubs**: automated stub generation from PyO3 has rough
   edges for generics and newtype patterns. Should we hand-write `.pyi` stubs for the
   first release? *Tentative: use `pyo3-stub-gen` for scaffolding, hand-tune for public
   API.*

7. **`wifi_densepose` vs `wifi-densepose` namespace**: the pip package name uses a dash
   (`wifi-densepose`) but Python imports use underscores (`wifi_densepose`). The v1
   package shipped under `src.*`, not `wifi_densepose.*`. Is there any tooling that
   hardcodes the `src` namespace? *Tentative: the `src.*` namespace was specific to
   `archive/v1/` and is cleanly dropped.*

8. **cibuildwheel version**: the current stable is cibuildwheel v2.x. Does the
   project's existing GitHub Actions config need updates for maturin builds vs
   the current `cargo build` / `build.py` patterns? *Tentative: yes, add a separate
   `pip-release.yml` workflow; do not modify existing Rust CI.*

9. **RuVector bindings timeline**: the `wifi-densepose-ruvector` crate (`v2/crates/`)
   depends on `ruvector-gnn = "2.0.5"`. Does ruvector-gnn ship as a pre-built static
   lib or require linking at build time? This directly affects the P6+ wheel size.
   *Tentative: investigate ruvector-gnn link strategy before committing to a timeline.*

10. **`wifi_densepose.client.ha` conflict with ADR-115/116**: the `ha.py` helper module
    should not duplicate the ADR-115 MQTT discovery logic in Python. Should it be read-only
    (parse HA discovery JSON → Python dataclasses) or also write (publish discovery JSON)?
    *Tentative: read-only for v2.0. Write path deferred to the HACS integration follow-on
    (ADR-115 §6.A).*

11. **BFLD Rust crate ownership** (added 2026-05-24): the P3.5 BFLD bindings ship with a
    stub Rust impl in `python/src/bfld_stub.rs`. The proper Rust crate (Nexmon BFR pcap
    parser + `mac80211` debugfs ingestor) will land later. Should it be a new
    `wifi-densepose-bfld` workspace member, or should it extend `wifi-densepose-signal`?
    *Tentative: new dedicated crate. Reasons: (a) the BFR parser is significant code
    (Wireshark's dissector is ~2k lines) and bloats `-signal`; (b) BFLD ingestion is
    optional — many deployments will only use CSI; gating behind a separate crate keeps
    the default `-signal` lean. Decide before committing to the crate name in any
    `pyproject.toml` extras.*

12. **BFLD per-vendor compressed-angle variants** (added 2026-05-24): 802.11 standardizes
    the compressed beamforming feedback format but vendors (Broadcom, Intel, Qualcomm,
    MediaTek) differ in psi/phi quantization step + ordering of consecutive matrix
    entries. How much normalisation belongs in the Python `BfldFrame.from_compressed_feedback`
    binding vs. the future Rust crate? *Tentative: Python binding is dumb (numpy ndarray
    in, numpy ndarray out — no decoding); the future Rust crate owns per-vendor
    normalisation, exposed via a `Vendor` enum on the binding constructor. Confirm via
    a per-vendor test fixture before P3.5 ships.*

---

## 12. References

### BFLD references (added 2026-05-24 for §5.7a + §11.11 + §11.12)

- Hernandez & Bulut, *"Wi-Fi Sensing With Compressed Beamforming Feedback"*, ACM TOSN 2024 — first systematic survey of BFR-as-sensing
- Yousefi, Soltanaghaei & Bharadia, *"Just-In-Time Wi-Fi Sensing Using Compressed Beamforming Feedback"*, MobiSys 2023 — practical pipeline for breath + heart-rate extraction from sniffed BFR
- IEEE 802.11ax-2021 §27.3.10 — Compressed Beamforming Feedback frame format
- Wireshark BFR dissector — `packet-ieee80211.c` reference implementation
- AX210 Linux mac80211 debugfs BFR capture path (kernel 6.10+)
- Sample BFR-vs-CSI parity dataset — TBD; we'll publish one alongside the
  `wifi-densepose-bfld` crate when it lands

### Original references

- **PyPI package (current)**: https://pypi.org/project/wifi-densepose/ — v1.1.0, released 2025-06-07
- **PyPI JSON metadata**: https://pypi.org/pypi/wifi-densepose/json
- **Local source**: `archive/v1/setup.py`, `archive/v1/src/__init__.py`, `archive/v1/data/proof/verify.py`
- **Rust workspace**: `v2/Cargo.toml`, `v2/crates/wifi-densepose-core/src/lib.rs`,
  `v2/crates/wifi-densepose-vitals/src/lib.rs`, `v2/crates/wifi-densepose-signal/src/lib.rs`,
  `v2/crates/wifi-densepose-sensing-server/src/lib.rs`
- **PyO3 docs**: https://pyo3.rs/ — v0.28.3 stable, Rust ≥1.83 required
- **maturin docs**: https://maturin.rs/ — supports Python 3.8+ on Linux/macOS/Windows/FreeBSD
- **cibuildwheel docs**: https://cibuildwheel.pypa.io/
- **ADR-021**: ESP32 vitals — defines the HR/BR extraction pipeline this ADR exposes in Python
- **ADR-028**: ESP32 capability audit — defines the witness bundle format `witness/verify.py` must re-verify
- **ADR-115**: HA-DISCO + HA-MIND + HA-FABRIC — defines the MQTT topic structure the `client/mqtt.py` helper consumes
- **ADR-116**: HA-COG cog packaging — parallel effort; ADR-117 pip library is the developer-facing Python surface; ADR-116 is the Seed-installable artifact
