feat(adr-117): pip wifi-densepose modernization (PIP-PHOENIX) + ruview sibling release - #786
Merged
Conversation
…gnature 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>
… 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>
…e=_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>
…/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>
…TE (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 (fd0568c), 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>
…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>
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>
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>
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>
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>
…ing 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>
- 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>
…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>
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>
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>
| # Linux aarch64 needs QEMU for cross-build on x86_64 runners. | ||
| - name: Set up QEMU | ||
| if: matrix.os == 'ubuntu-latest' && matrix.arch == 'aarch64' | ||
| uses: docker/setup-qemu-action@v3 |
| # ADR-117 §5.4: abi3-py310 — one binary per OS/arch covers all | ||
| # Python minor versions ≥ 3.10. Build only cp310 wheels. | ||
| - name: Build wheels (cibuildwheel) | ||
| uses: pypa/cibuildwheel@v2.21 |
| ls -lh dist/ | ||
| - name: Publish to TestPyPI (dry-run target) | ||
| if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi' | ||
| uses: pypa/gh-action-pypi-publish@release/v1 |
| if: | | ||
| startsWith(github.ref, 'refs/tags/v2.') || | ||
| (github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi') | ||
| uses: pypa/gh-action-pypi-publish@release/v1 |
| path: dist | ||
| - name: Publish to TestPyPI (dry-run target) | ||
| if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi' | ||
| uses: pypa/gh-action-pypi-publish@release/v1 |
| if: | | ||
| startsWith(github.ref, 'refs/tags/v1.99') || | ||
| (github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi') | ||
| uses: pypa/gh-action-pypi-publish@release/v1 |
- 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>
ruvnet
added a commit
that referenced
this pull request
May 24, 2026
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>
ruvnet
added a commit
that referenced
this pull request
May 24, 2026
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>
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements ADR-117 — the PyO3+maturin replacement for the legacy
wifi-densepose==1.1.0pure-Python FastAPI server. Also ships the brand-facing sibling releaseruviewas a thin meta-package.Three PyPI artifacts are live as of this PR:
wifi-denseposewifi-denseposeruviewpip install ruview+pip install wifi-denseposeboth work; both expose the same compiled DSP under their respective namespaces.What ships
python/— maturin-built PyO3 wheel. Surfaces fromwifi_densepose:Keypoint,KeypointType,BoundingBox,PersonPose,PoseEstimateVitalStatus,VitalEstimate,VitalReading,BreathingExtractor,HeartRateExtractor(GIL-releasing extract hot loops)BfldKind,BfldFrame,BfldReport(forward-compat surface for 802.11ax beamforming feedback)wifi_densepose.client.{ws,mqtt,primitives,ha}opt-in client extrapython/tombstone/— pure-Pythonwifi-densepose==1.99.0wheel raising ImportError with migration URLpython/ruview-meta/— pure-Pythonruview==2.0.0a1meta-package re-exporting everything fromwifi_denseposepython/bench/— pytest-benchmark suite for the hot paths (all extract loops under 1% of the 100 Hz ESP32 frame budget).github/workflows/pip-release.yml— cibuildwheel matrix (manylinux/macOS/Windows × abi3-py310) wired toPYPI_API_TOKEN; triggers onv*-piptags orworkflow_dispatchREADME.md+docs/user-guide.md— updated with briefpip installquickstart pointing at the v2 wheelTest counts
ruviewwheel verified end-to-end from a fresh venvCI / release plumbing
secrets.PYPI_API_TOKENworkflow_dispatchor tags matchingv*-pipwifi_densepose/shadowing the venv install)workflow_dispatchwithtarget: v2-wheels, publish_to: pypi(currently only the Windows wheel + sdist are on PyPI)Test plan
pip install ruviewin a fresh venv worksimport ruview+import wifi_denseposeboth succeed and share the same compiled typespip install wifi-densepose==1.99.0raises ImportError with migration URLRefs
docs/adr/ADR-117-pip-wifi-densepose-modernization.md🤖 Generated with claude-flow