Compare commits

..

No commits in common. "905b68074766ea8746c9398f9e49972b06ab6fdd" and "5bcb25b2b05a700b43b9d11d37c139db3d3780f3" have entirely different histories.

201 changed files with 276 additions and 4189 deletions

View File

@ -310,27 +310,26 @@ jobs:
runs-on: ubuntu-latest
needs: [code-quality, test, rust-tests, performance-test, docker-build, docs]
if: always()
# GitHub Actions does not allow `secrets.X` directly in step-level `if:`
# expressions — only `env.X`. Promote the secret to env at job scope so
# the gating expression below is parseable.
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
steps:
- name: Notify Slack on success
if: ${{ env.SLACK_WEBHOOK_URL != '' && needs.code-quality.result == 'success' && needs.test.result == 'success' && needs.docker-build.result == 'success' }}
if: ${{ secrets.SLACK_WEBHOOK_URL != '' && needs.code-quality.result == 'success' && needs.test.result == 'success' && needs.docker-build.result == 'success' }}
uses: 8398a7/action-slack@v3
with:
status: success
channel: '#ci-cd'
text: '✅ CI pipeline completed successfully for ${{ github.ref }}'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Notify Slack on failure
if: ${{ env.SLACK_WEBHOOK_URL != '' && (needs.code-quality.result == 'failure' || needs.test.result == 'failure' || needs.docker-build.result == 'failure') }}
if: ${{ secrets.SLACK_WEBHOOK_URL != '' && (needs.code-quality.result == 'failure' || needs.test.result == 'failure' || needs.docker-build.result == 'failure') }}
uses: 8398a7/action-slack@v3
with:
status: failure
channel: '#ci-cd'
text: '❌ CI pipeline failed for ${{ github.ref }}'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Create GitHub Release
if: github.ref == 'refs/heads/main' && needs.docker-build.result == 'success'

View File

@ -377,11 +377,6 @@ jobs:
runs-on: ubuntu-latest
needs: [sast, dependency-scan, container-scan, iac-scan, secret-scan, license-scan, compliance-check]
if: always()
# Promote secret to env-scope so the gating `if:` on the Slack-notify
# step below is parseable (GitHub Actions rejects `secrets.X` in
# step-level `if:` expressions).
env:
SECURITY_SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
@ -407,11 +402,8 @@ jobs:
name: security-summary
path: security-summary.md
# GitHub Actions does not allow `secrets.X` in step-level `if:` —
# use env.X instead. Inherits SECURITY_SLACK_WEBHOOK_URL from the
# job-level env block (added below).
- name: Notify security team on critical findings
if: ${{ env.SECURITY_SLACK_WEBHOOK_URL != '' && (needs.sast.result == 'failure' || needs.dependency-scan.result == 'failure' || needs.container-scan.result == 'failure') }}
if: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL != '' && (needs.sast.result == 'failure' || needs.dependency-scan.result == 'failure' || needs.container-scan.result == 'failure') }}
uses: 8398a7/action-slack@v3
with:
status: failure
@ -423,7 +415,7 @@ jobs:
Workflow: ${{ github.workflow }}
Please review the security scan results immediately.
env:
SLACK_WEBHOOK_URL: ${{ env.SECURITY_SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
- name: Create security issue on critical findings
if: needs.sast.result == 'failure' || needs.dependency-scan.result == 'failure'

View File

@ -4,16 +4,16 @@ on:
push:
branches: [ main, master, 'claude/**' ]
paths:
- 'archive/v1/src/core/**'
- 'archive/v1/src/hardware/**'
- 'archive/v1/data/proof/**'
- 'v1/src/core/**'
- 'v1/src/hardware/**'
- 'v1/data/proof/**'
- '.github/workflows/verify-pipeline.yml'
pull_request:
branches: [ main, master ]
paths:
- 'archive/v1/src/core/**'
- 'archive/v1/src/hardware/**'
- 'archive/v1/data/proof/**'
- 'v1/src/core/**'
- 'v1/src/hardware/**'
- 'v1/data/proof/**'
- '.github/workflows/verify-pipeline.yml'
workflow_dispatch:
@ -37,19 +37,19 @@ jobs:
- name: Install pinned dependencies
run: |
python -m pip install --upgrade pip
pip install -r archive/v1/requirements-lock.txt
pip install -r v1/requirements-lock.txt
- name: Verify reference signal is reproducible
run: |
echo "=== Regenerating reference signal ==="
python archive/v1/data/proof/generate_reference_signal.py
python v1/data/proof/generate_reference_signal.py
echo ""
echo "=== Checking data file matches committed version ==="
# The regenerated file should be identical to the committed one
# (We compare the metadata file since data file is large)
python -c "
import json, hashlib
with open('archive/v1/data/proof/sample_csi_meta.json') as f:
with open('v1/data/proof/sample_csi_meta.json') as f:
meta = json.load(f)
assert meta['is_synthetic'] == True, 'Metadata must mark signal as synthetic'
assert meta['numpy_seed'] == 42, 'Seed must be 42'
@ -76,7 +76,7 @@ jobs:
echo "=== Scanning for unseeded np.random usage in production code ==="
# Search for np.random calls without a seed in production code
# Exclude test files, proof data generators, and known parser placeholders
VIOLATIONS=$(grep -rn "np\.random\." archive/v1/src/ \
VIOLATIONS=$(grep -rn "np\.random\." v1/src/ \
--include="*.py" \
--exclude-dir="__pycache__" \
| grep -v "np\.random\.RandomState" \

View File

@ -520,7 +520,7 @@ Major release: complete Rust sensing server, full DensePose training pipeline, R
- `PresenceClassifier` — rule-based 3-state classification (ABSENT / PRESENT_STILL / ACTIVE)
- Cross-receiver agreement scoring for multi-AP confidence boosting
- WebSocket sensing server (`ws_server.py`) broadcasting JSON at 2 Hz
- Deterministic CSI proof bundles for reproducible verification (`archive/v1/data/proof/`)
- Deterministic CSI proof bundles for reproducible verification (`v1/data/proof/`)
- Commodity sensing unit tests (`b391638`)
### Changed
@ -528,7 +528,7 @@ Major release: complete Rust sensing server, full DensePose training pipeline, R
### Fixed
- Review fixes for end-to-end training pipeline (`45f0304`)
- Dockerfile paths updated from `src/` to `archive/v1/src/` (`7872987`)
- Dockerfile paths updated from `src/` to `v1/src/` (`7872987`)
- IoT profile installer instructions updated for aggregator CLI (`f460097`)
- `process.env` reference removed from browser ES module (`e320bc9`)

View File

@ -91,10 +91,10 @@ cargo test --workspace --no-default-features
cargo check -p wifi-densepose-train --no-default-features
# Python — deterministic proof verification (SHA-256)
python archive/v1/data/proof/verify.py
python v1/data/proof/verify.py
# Python — test suite
cd archive/v1 && python -m pytest tests/ -x -q
cd v1 && python -m pytest tests/ -x -q
```
### ESP32 Firmware Build (Windows — Python subprocess required)
@ -156,7 +156,7 @@ cargo test --workspace --no-default-features
# 2. Python proof — must print VERDICT: PASS
cd ..
python archive/v1/data/proof/verify.py
python v1/data/proof/verify.py
# 3. Generate witness bundle (includes both above + firmware hashes)
bash scripts/generate-witness-bundle.sh
@ -169,8 +169,8 @@ bash VERIFY.sh
**If the Python proof hash changes** (e.g., numpy/scipy version update):
```bash
# Regenerate the expected hash, then verify it passes
python archive/v1/data/proof/verify.py --generate-hash
python archive/v1/data/proof/verify.py
python v1/data/proof/verify.py --generate-hash
python v1/data/proof/verify.py
```
**Witness bundle contents** (`dist/witness-bundle-ADR028-<sha>.tar.gz`):
@ -183,9 +183,9 @@ python archive/v1/data/proof/verify.py
- `VERIFY.sh` — One-command self-verification for recipients
**Key proof artifacts:**
- `archive/v1/data/proof/verify.py` — Trust Kill Switch: feeds reference signal through production pipeline, hashes output
- `archive/v1/data/proof/expected_features.sha256` — Published expected hash
- `archive/v1/data/proof/sample_csi_data.json` — 1,000 synthetic CSI frames (seed=42)
- `v1/data/proof/verify.py` — Trust Kill Switch: feeds reference signal through production pipeline, hashes output
- `v1/data/proof/expected_features.sha256` — Published expected hash
- `v1/data/proof/sample_csi_data.json` — 1,000 synthetic CSI frames (seed=42)
- `docs/WITNESS-LOG-028.md` — 11-step reproducible verification procedure
- `docs/adr/ADR-028-esp32-capability-audit.md` — Complete audit record
@ -216,8 +216,8 @@ Active feature branch: `ruvsense-full-implementation` (PR #77)
- `v2/crates/wifi-densepose-ruvector/src/viewpoint/` — Cross-viewpoint fusion (5 files)
- `v2/crates/wifi-densepose-hardware/src/esp32/` — ESP32 TDM protocol
- `firmware/esp32-csi-node/main/` — ESP32 C firmware (channel hopping, NVS config, TDM)
- `archive/v1/src/` — Python source (core, hardware, services, api)
- `archive/v1/data/proof/` — Deterministic CSI proof bundles
- `v1/src/` — Python source (core, hardware, services, api)
- `v1/data/proof/` — Deterministic CSI proof bundles
- `.claude-flow/` — Claude Flow coordination state (committed for team sharing)
- `.claude/` — Claude Code settings, agents, memory (committed for team sharing)
@ -243,7 +243,7 @@ Active feature branch: `ruvsense-full-implementation` (PR #77)
Before merging any PR, verify each item applies and is addressed:
1. **Rust tests pass**`cargo test --workspace --no-default-features` (1,031+ passed, 0 failed)
2. **Python proof passes**`python archive/v1/data/proof/verify.py` (VERDICT: PASS)
2. **Python proof passes**`python v1/data/proof/verify.py` (VERDICT: PASS)
3. **README.md** — Update platform tables, crate descriptions, hardware tables, feature summaries if scope changed
4. **CLAUDE.md** — Update crate table, ADR list, module tables, version if scope changed
5. **CHANGELOG.md** — Add entry under `[Unreleased]` with what was added/fixed/changed

View File

@ -92,7 +92,7 @@ node scripts/mincut-person-counter.js --port 5006 # Correct person counting
> | **Research NIC** | Intel 5300 / Atheros AR9580 | ~$50-100 | Yes | Full CSI with 3x3 MIMO |
> | **Any WiFi** | Windows, macOS, or Linux laptop | $0 | No | RSSI-only: coarse presence and motion |
>
> No hardware? Verify the signal processing pipeline with the deterministic reference signal: `python archive/v1/data/proof/verify.py`
> No hardware? Verify the signal processing pipeline with the deterministic reference signal: `python v1/data/proof/verify.py`
>
---
@ -1171,7 +1171,7 @@ Bundle verify: 7/7 checks PASS
cd v2 && cargo test --workspace --no-default-features
# Run the deterministic proof
python archive/v1/data/proof/verify.py
python v1/data/proof/verify.py
# Generate + verify the witness bundle
bash scripts/generate-witness-bundle.sh
@ -2164,7 +2164,7 @@ cargo test -p wifi-densepose-sensing-server
./target/release/sensing-server --benchmark
# Python tests
python -m pytest archive/v1/tests/ -v
python -m pytest v1/tests/ -v
# Pipeline verification (no hardware needed)
./verify

View File

@ -1,74 +0,0 @@
# Archive
Frozen, no-longer-active components of RuView preserved for historical
reference, reproducibility, and load-bearing legacy paths the active
codebase still depends on.
## What lives here
| Path | What it is | Why it's archived | Still load-bearing? |
|------|------------|-------------------|---------------------|
| `v1/` | Original Python implementation of RuView (CSI processing, hardware adapters, services, FastAPI) | Superseded by the Rust workspace at `v2/`; ~810× slower in benchmarks. Kept rather than deleted because the deterministic proof bundle (`v1/data/proof/`) is part of the pre-merge witness verification process per ADR-011 / ADR-028. | **Yes — for the proof bundle only.** Active code lives in `v2/`. |
## What "archived" means
- **Do not add new features here.** New work goes in `v2/`.
- **Do not refactor or modernize the archived code beyond what is
strictly necessary** to keep the load-bearing paths working. The
Python proof bundle is intentionally frozen so that its SHA-256
reproducibility holds across releases (per ADR-028's witness
verification requirement).
- **Bug fixes inside archived code are allowed** when the bug affects a
still-load-bearing path (currently: only the Python proof). All
other "bugs" in archived code are out-of-scope — they are part of
the historical record and any fix would unnecessarily churn the
witness hashes.
- **CI continues to verify the load-bearing paths.**
`.github/workflows/verify-pipeline.yml` runs the Python proof on
every push and PR; if you change anything inside `archive/v1/src/`
or `archive/v1/data/proof/`, expect the determinism check to flag
it.
## Quick reference for the load-bearing paths
```bash
# Run the deterministic Python proof (must print VERDICT: PASS)
python archive/v1/data/proof/verify.py
# Regenerate the expected hash (only if numpy/scipy version legitimately changed)
python archive/v1/data/proof/verify.py --generate-hash
# Run the full Python test suite (legacy, still maintained)
cd archive/v1&& python -m pytest tests/ -x -q
```
## Why we keep `v1/` rather than delete it
1. **Trust kill-switch.** The proof at `v1/data/proof/verify.py` feeds
a known reference signal through the full pipeline and hashes the
output. If the active code's behavior drifts, the hash changes and
CI fails. This is what stops accidental regression in the science
layer of the codebase.
2. **Witness verification.** ADR-028's witness-bundle process bundles
the proof, the rust workspace test results, and firmware hashes
into a tarball recipients can self-verify. Removing v1 would break
that chain.
3. **Historical reference.** ADR-011 documents the "no mocks in
production code" decision; the original violations and their fixes
live in this Python codebase. The ADRs reference these paths.
If the time comes to retire the proof bundle (e.g., a Rust port of
the proof exists and the Python version is no longer canonical), the
right move is a single follow-up that simultaneously: ports the
witness-bundle process, updates `verify-pipeline.yml`, and either
deletes `archive/v1/` or moves it to a separate read-only repository.
That decision belongs in its own ADR.
## See also
- `docs/adr/ADR-011-python-proof-of-reality-mock-elimination.md`
- `docs/adr/ADR-028-esp32-capability-audit.md`
- `archive/v1/data/proof/README.md` (if present)
- `docs/WITNESS-LOG-028.md`

View File

@ -10,16 +10,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY archive/v1/requirements-lock.txt /app/requirements.txt
COPY v1/requirements-lock.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt \
&& pip install --no-cache-dir websockets uvicorn fastapi
# Copy application code
COPY archive/v1/ /app/v1/
COPY v1/ /app/v1/
COPY ui/ /app/ui/
# Copy sensing modules
COPY archive/v1/src/sensing/ /app/v1/src/sensing/
COPY v1/src/sensing/ /app/v1/src/sensing/
EXPOSE 8765
EXPOSE 8080

View File

@ -133,7 +133,7 @@ cargo test -p wifi-densepose-train --no-default-features
### Step 9: Verify Python Proof System
```bash
python archive/v1/data/proof/verify.py
python v1/data/proof/verify.py
```
**Expected:** PASS (hash `8c0680d7...` matches `expected_features.sha256`).

View File

@ -20,31 +20,31 @@ The following code paths produce fake data **in the default configuration** or a
| File | Line | Issue | Impact |
|------|------|-------|--------|
| `archive/v1/src/core/csi_processor.py` | 390 | `doppler_shift = np.random.rand(10) # Placeholder` | **Real feature extractor returns random Doppler** - kills credibility of entire feature pipeline |
| `archive/v1/src/hardware/csi_extractor.py` | 83-84 | `amplitude = np.random.rand(...)` in CSI extraction fallback | Random data silently substituted when parsing fails |
| `archive/v1/src/hardware/csi_extractor.py` | 129-135 | `_parse_atheros()` returns `np.random.rand()` with comment "placeholder implementation" | Named as if it parses real data, actually random |
| `archive/v1/src/hardware/router_interface.py` | 211-212 | `np.random.rand(3, 56)` in fallback path | Silent random fallback |
| `archive/v1/src/services/pose_service.py` | 431 | `mock_csi = np.random.randn(64, 56, 3) # Mock CSI data` | Mock CSI in production code path |
| `archive/v1/src/services/pose_service.py` | 293-356 | `_generate_mock_poses()` with `random.randint` throughout | Entire mock pose generator in service layer |
| `archive/v1/src/services/pose_service.py` | 489-607 | Multiple `random.randint` for occupancy, historical data | Fake statistics that look real in API responses |
| `archive/v1/src/api/dependencies.py` | 82, 408 | "return a mock user for development" | Auth bypass in default path |
| `v1/src/core/csi_processor.py` | 390 | `doppler_shift = np.random.rand(10) # Placeholder` | **Real feature extractor returns random Doppler** - kills credibility of entire feature pipeline |
| `v1/src/hardware/csi_extractor.py` | 83-84 | `amplitude = np.random.rand(...)` in CSI extraction fallback | Random data silently substituted when parsing fails |
| `v1/src/hardware/csi_extractor.py` | 129-135 | `_parse_atheros()` returns `np.random.rand()` with comment "placeholder implementation" | Named as if it parses real data, actually random |
| `v1/src/hardware/router_interface.py` | 211-212 | `np.random.rand(3, 56)` in fallback path | Silent random fallback |
| `v1/src/services/pose_service.py` | 431 | `mock_csi = np.random.randn(64, 56, 3) # Mock CSI data` | Mock CSI in production code path |
| `v1/src/services/pose_service.py` | 293-356 | `_generate_mock_poses()` with `random.randint` throughout | Entire mock pose generator in service layer |
| `v1/src/services/pose_service.py` | 489-607 | Multiple `random.randint` for occupancy, historical data | Fake statistics that look real in API responses |
| `v1/src/api/dependencies.py` | 82, 408 | "return a mock user for development" | Auth bypass in default path |
#### Moderate Severity (mock gated behind flags but confusing)
| File | Line | Issue |
|------|------|-------|
| `archive/v1/src/config/settings.py` | 144-145 | `mock_hardware=False`, `mock_pose_data=False` defaults - correct, but mock infrastructure exists |
| `archive/v1/src/core/router_interface.py` | 27-300 | 270+ lines of mock data generation infrastructure in production code |
| `archive/v1/src/services/pose_service.py` | 84-88 | Silent conditional: `if not self.settings.mock_pose_data` with no logging of real-mode |
| `archive/v1/src/services/hardware_service.py` | 72-375 | Interleaved mock/real paths throughout |
| `v1/src/config/settings.py` | 144-145 | `mock_hardware=False`, `mock_pose_data=False` defaults - correct, but mock infrastructure exists |
| `v1/src/core/router_interface.py` | 27-300 | 270+ lines of mock data generation infrastructure in production code |
| `v1/src/services/pose_service.py` | 84-88 | Silent conditional: `if not self.settings.mock_pose_data` with no logging of real-mode |
| `v1/src/services/hardware_service.py` | 72-375 | Interleaved mock/real paths throughout |
#### Low Severity (placeholders/TODOs)
| File | Line | Issue |
|------|------|-------|
| `archive/v1/src/core/router_interface.py` | 198 | "Collect real CSI data from router (placeholder implementation)" |
| `archive/v1/src/api/routers/health.py` | 170-171 | `uptime_seconds = 0.0 # TODO` |
| `archive/v1/src/services/pose_service.py` | 739 | `"uptime_seconds": 0.0 # TODO` |
| `v1/src/core/router_interface.py` | 198 | "Collect real CSI data from router (placeholder implementation)" |
| `v1/src/api/routers/health.py` | 170-171 | `uptime_seconds = 0.0 # TODO` |
| `v1/src/services/pose_service.py` | 739 | `"uptime_seconds": 0.0 # TODO` |
### Root Cause Analysis
@ -119,7 +119,7 @@ def _parse_atheros(self, raw_data: bytes) -> CSIData:
**All mock code moves to a dedicated module. Default execution NEVER touches mock paths.**
```
archive/v1/src/
v1/src/
├── core/
│ ├── csi_processor.py # Real processing only
│ └── router_interface.py # Real hardware interface only
@ -157,7 +157,7 @@ if MOCK_MODE:
A small real CSI capture file + one-command verification pipeline:
```
archive/v1/data/proof/
v1/data/proof/
├── README.md # How to verify
├── sample_csi_capture.bin # Real CSI data (1 second, ~50 KB)
├── sample_csi_capture_meta.json # Capture metadata (hardware, env)
@ -172,7 +172,7 @@ archive/v1/data/proof/
"""Verify WiFi-DensePose pipeline produces deterministic output from real CSI data.
Usage:
python archive/v1/data/proof/verify.py
python v1/data/proof/verify.py
Expected output:
PASS: Pipeline output matches expected hash
@ -265,13 +265,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
WORKDIR /app
# Pinned requirements (not a reference to missing file)
COPY archive/v1/requirements-lock.txt ./requirements.txt
COPY v1/requirements-lock.txt ./requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY archive/v1/ ./v1/
COPY v1/ ./v1/
# Proof of reality: verify pipeline on build
RUN cd archive/v1 && python data/proof/verify.py
RUN cd v1 && python data/proof/verify.py
EXPOSE 8000
# Default: REAL mode (mock requires explicit opt-in)
@ -281,7 +281,7 @@ CMD ["uvicorn", "v1.src.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
**Key change**: `RUN python data/proof/verify.py` **during build** means the Docker image cannot be created unless the pipeline produces correct output from real CSI data.
**Requirements lockfile** (`archive/v1/requirements-lock.txt`):
**Requirements lockfile** (`v1/requirements-lock.txt`):
```
# Core (required)
fastapi==0.115.6
@ -307,9 +307,9 @@ name: Verify Signal Pipeline
on:
push:
paths: ['archive/v1/src/**', 'archive/v1/data/proof/**']
paths: ['v1/src/**', 'v1/data/proof/**']
pull_request:
paths: ['archive/v1/src/**']
paths: ['v1/src/**']
jobs:
verify:
@ -322,11 +322,11 @@ jobs:
- name: Install minimal deps
run: pip install numpy scipy pydantic pydantic-settings
- name: Verify pipeline determinism
run: python archive/v1/data/proof/verify.py
run: python v1/data/proof/verify.py
- name: Verify no random in production paths
run: |
# Fail if np.random appears in production code (not in testing/)
! grep -r "np\.random\.\(rand\|randn\|randint\)" archive/v1/src/ \
! grep -r "np\.random\.\(rand\|randn\|randint\)" v1/src/ \
--include="*.py" \
--exclude-dir=testing \
|| (echo "FAIL: np.random found in production code" && exit 1)
@ -336,23 +336,23 @@ jobs:
| File | Action | Description |
|------|--------|-------------|
| `archive/v1/src/core/csi_processor.py:390` | **Replace** | Real Doppler extraction from temporal CSI history |
| `archive/v1/src/hardware/csi_extractor.py:83-84` | **Replace** | Hard error with descriptive message when parsing fails |
| `archive/v1/src/hardware/csi_extractor.py:129-135` | **Replace** | Real Atheros CSI parser or hard error with hardware instructions |
| `archive/v1/src/hardware/router_interface.py:198-212` | **Replace** | Hard error for unimplemented hardware, or real `iwconfig` + CSI tool integration |
| `archive/v1/src/services/pose_service.py:293-356` | **Move** | Move `_generate_mock_poses()` to `archive/v1/src/testing/mock_pose_generator.py` |
| `archive/v1/src/services/pose_service.py:430-431` | **Remove** | Remove mock CSI generation from production path |
| `archive/v1/src/services/pose_service.py:489-607` | **Replace** | Real statistics from database, or explicit "no data" response |
| `archive/v1/src/core/router_interface.py:60-300` | **Move** | Move mock generator to `archive/v1/src/testing/mock_csi_generator.py` |
| `archive/v1/src/api/dependencies.py:82,408` | **Replace** | Real auth check or explicit dev-mode bypass with logging |
| `archive/v1/data/proof/` | **Create** | Proof bundle (sample capture + expected hash + verify script) |
| `archive/v1/requirements-lock.txt` | **Create** | Pinned minimal dependencies |
| `v1/src/core/csi_processor.py:390` | **Replace** | Real Doppler extraction from temporal CSI history |
| `v1/src/hardware/csi_extractor.py:83-84` | **Replace** | Hard error with descriptive message when parsing fails |
| `v1/src/hardware/csi_extractor.py:129-135` | **Replace** | Real Atheros CSI parser or hard error with hardware instructions |
| `v1/src/hardware/router_interface.py:198-212` | **Replace** | Hard error for unimplemented hardware, or real `iwconfig` + CSI tool integration |
| `v1/src/services/pose_service.py:293-356` | **Move** | Move `_generate_mock_poses()` to `v1/src/testing/mock_pose_generator.py` |
| `v1/src/services/pose_service.py:430-431` | **Remove** | Remove mock CSI generation from production path |
| `v1/src/services/pose_service.py:489-607` | **Replace** | Real statistics from database, or explicit "no data" response |
| `v1/src/core/router_interface.py:60-300` | **Move** | Move mock generator to `v1/src/testing/mock_csi_generator.py` |
| `v1/src/api/dependencies.py:82,408` | **Replace** | Real auth check or explicit dev-mode bypass with logging |
| `v1/data/proof/` | **Create** | Proof bundle (sample capture + expected hash + verify script) |
| `v1/requirements-lock.txt` | **Create** | Pinned minimal dependencies |
| `.github/workflows/verify-pipeline.yml` | **Create** | CI verification |
### Hardware Documentation
```
archive/v1/docs/hardware-setup.md (to be created)
v1/docs/hardware-setup.md (to be created)
# Supported Hardware Matrix
@ -368,17 +368,17 @@ archive/v1/docs/hardware-setup.md (to be created)
2. Capture 10 seconds of empty-room baseline
3. Have one person walk through at normal pace
4. Capture 10 seconds during walk-through
5. Run calibration: `python archive/v1/scripts/calibrate.py --baseline empty.dat --activity walk.dat`
5. Run calibration: `python v1/scripts/calibrate.py --baseline empty.dat --activity walk.dat`
```
## Consequences
### Positive
- **"Clone, build, verify" in one command**: `docker build . && docker run --rm wifi-densepose python archive/v1/data/proof/verify.py` produces a deterministic PASS
- **"Clone, build, verify" in one command**: `docker build . && docker run --rm wifi-densepose python v1/data/proof/verify.py` produces a deterministic PASS
- **No silent fakes**: Random data never appears in production output
- **CI enforcement**: PRs that introduce `np.random` in production paths fail automatically
- **Credibility anchor**: SHA-256 verified output from real CSI capture is unchallengeable proof
- **Clear mock boundary**: Mock code exists only in `archive/v1/src/testing/`, never imported by production modules
- **Clear mock boundary**: Mock code exists only in `v1/src/testing/`, never imported by production modules
### Negative
- **Requires real CSI capture**: Someone must capture and commit a real CSI sample (one-time effort)
@ -390,7 +390,7 @@ archive/v1/docs/hardware-setup.md (to be created)
A stranger can:
1. `git clone` the repository
2. Run ONE command (`docker build .` or `python archive/v1/data/proof/verify.py`)
2. Run ONE command (`docker build .` or `python v1/data/proof/verify.py`)
3. See `PASS: Pipeline output matches expected hash` with a specific SHA-256
4. Confirm no `np.random` in any non-test file via CI badge

View File

@ -1,7 +1,7 @@
# ADR-013: Feature-Level Sensing on Commodity Gear (Option 3)
## Status
Accepted — Implemented (36/36 unit tests pass, see `archive/v1/src/sensing/` and `archive/v1/tests/unit/test_sensing.py`)
Accepted — Implemented (36/36 unit tests pass, see `v1/src/sensing/` and `v1/tests/unit/test_sensing.py`)
## Date
2026-02-28
@ -323,7 +323,7 @@ class PresenceClassifier:
### Proof Bundle for Commodity Sensing
```
archive/v1/data/proof/commodity/
v1/data/proof/commodity/
├── rssi_capture_30sec.json # 30 seconds of RSSI from 3 receivers
├── rssi_capture_meta.json # Hardware: Intel AX200, Router: TP-Link AX1800
├── scenario.txt # "Person walks through room at t=10s, sits at t=20s"
@ -375,7 +375,7 @@ class CommodityBackend(SensingBackend):
### Implementation Status
The full commodity sensing pipeline is implemented in `archive/v1/src/sensing/`:
The full commodity sensing pipeline is implemented in `v1/src/sensing/`:
| Module | File | Description |
|--------|------|-------------|
@ -384,7 +384,7 @@ The full commodity sensing pipeline is implemented in `archive/v1/src/sensing/`:
| Classifier | `classifier.py` | `PresenceClassifier` with ABSENT/PRESENT_STILL/ACTIVE levels, confidence scoring |
| Backend | `backend.py` | `CommodityBackend` wiring collector → extractor → classifier, reports PRESENCE + MOTION capabilities |
**Test coverage**: 36 tests in `archive/v1/tests/unit/test_sensing.py` — all passing:
**Test coverage**: 36 tests in `v1/tests/unit/test_sensing.py` — all passing:
- `TestRingBuffer` (4), `TestSimulatedCollector` (5), `TestFeatureExtractor` (8), `TestCusum` (4), `TestPresenceClassifier` (7), `TestCommodityBackend` (6), `TestBandPower` (2)
**Dependencies**: `numpy`, `scipy` (for FFT and spectral analysis)

View File

@ -22,8 +22,8 @@ This ADR answers *how* to build it — the concrete development sequence, the sp
| Frame types | `wifi-densepose-hardware/src/csi_frame.rs` | Complete — `CsiFrame`, `CsiMetadata`, `SubcarrierData`, `to_amplitude_phase()` |
| Parse error types | `wifi-densepose-hardware/src/error.rs` | Complete — `ParseError` enum with 6 variants |
| Signal processing pipeline | `wifi-densepose-signal` crate | Complete — Hampel, Fresnel, BVP, Doppler, spectrogram |
| CSI extractor (Python) | `archive/v1/src/hardware/csi_extractor.py` | Stub — `_read_raw_data()` raises `NotImplementedError` |
| Router interface (Python) | `archive/v1/src/hardware/router_interface.py` | Stub — `_parse_csi_response()` raises `RouterConnectionError` |
| CSI extractor (Python) | `v1/src/hardware/csi_extractor.py` | Stub — `_read_raw_data()` raises `NotImplementedError` |
| Router interface (Python) | `v1/src/hardware/router_interface.py` | Stub — `_parse_csi_response()` raises `RouterConnectionError` |
**Not yet implemented:**
@ -211,10 +211,10 @@ The bridge test: parse a known binary frame, convert to `CsiData`, assert `ampli
### Layer 4 — Python `_read_raw_data()` Real Implementation
Replace the `NotImplementedError` stub in `archive/v1/src/hardware/csi_extractor.py` with a UDP socket reader. This allows the Python pipeline to receive real CSI from the aggregator while the Rust pipeline is being integrated.
Replace the `NotImplementedError` stub in `v1/src/hardware/csi_extractor.py` with a UDP socket reader. This allows the Python pipeline to receive real CSI from the aggregator while the Rust pipeline is being integrated.
```python
# archive/v1/src/hardware/csi_extractor.py
# v1/src/hardware/csi_extractor.py
# Replace _read_raw_data() stub:
import socket as _socket

View File

@ -34,7 +34,7 @@ Implement a **sensing-only UI mode** that:
- Breathing ring modulation when breathing-band power detected
- Side panel with RSSI sparkline, feature meters, and classification badge
4. **Python WebSocket bridge** (`archive/v1/src/sensing/ws_server.py`) that:
4. **Python WebSocket bridge** (`v1/src/sensing/ws_server.py`) that:
- Auto-detects ESP32 UDP CSI stream on port 5005 (ADR-018 binary frames)
- Falls back to `WindowsWifiCollector``SimulatedCollector`
- Runs `RssiFeatureExtractor``PresenceClassifier` pipeline
@ -80,7 +80,7 @@ Windows WiFi RSSI ───┘ │ │
### Created
| File | Purpose |
|------|---------|
| `archive/v1/src/sensing/ws_server.py` | Python asyncio WebSocket server with auto-detect collectors |
| `v1/src/sensing/ws_server.py` | Python asyncio WebSocket server with auto-detect collectors |
| `ui/components/SensingTab.js` | Sensing tab UI with Three.js integration |
| `ui/components/gaussian-splats.js` | Custom GLSL Gaussian splat renderer |
| `ui/services/sensing.service.js` | WebSocket client with reconnect + simulation fallback |

View File

@ -40,8 +40,8 @@ Use the `wifi-densepose-nn` crate with `default-features = ["onnx"]` only. This
| Component | Rust Crate | Replaces Python |
|-----------|-----------|-----------------|
| CSI processing | `wifi-densepose-signal::csi_processor` | `archive/v1/src/sensing/feature_extractor.py` |
| Motion detection | `wifi-densepose-signal::motion` | `archive/v1/src/sensing/classifier.py` |
| CSI processing | `wifi-densepose-signal::csi_processor` | `v1/src/sensing/feature_extractor.py` |
| Motion detection | `wifi-densepose-signal::motion` | `v1/src/sensing/classifier.py` |
| BVP extraction | `wifi-densepose-signal::bvp` | N/A (new capability) |
| Fresnel geometry | `wifi-densepose-signal::fresnel` | N/A (new capability) |
| Subcarrier selection | `wifi-densepose-signal::subcarrier_selection` | N/A (new capability) |

View File

@ -232,10 +232,10 @@ python scripts/provision.py --port COM7 \
| Component | File | Purpose |
|-----------|------|---------|
| Reference signal | `archive/v1/data/proof/sample_csi_data.json` | 1,000 synthetic CSI frames, seed=42 |
| Generator | `archive/v1/data/proof/generate_reference_signal.py` | Deterministic multipath model |
| Verifier | `archive/v1/data/proof/verify.py` | SHA-256 hash comparison |
| Expected hash | `archive/v1/data/proof/expected_features.sha256` | `0b82bd45...` |
| Reference signal | `v1/data/proof/sample_csi_data.json` | 1,000 synthetic CSI frames, seed=42 |
| Generator | `v1/data/proof/generate_reference_signal.py` | Deterministic multipath model |
| Verifier | `v1/data/proof/verify.py` | SHA-256 hash comparison |
| Expected hash | `v1/data/proof/expected_features.sha256` | `0b82bd45...` |
**Audit-time result:** PASS. Hash regenerated with numpy 2.4.2 + scipy 1.17.1. Pipeline hash: `8c0680d7d285739ea9597715e84959d9c356c87ee3ad35b5f1e69a4ca41151c6`.

View File

@ -108,7 +108,7 @@ Remove duplicated platform-detection logic from `ws_server.py` and `install.sh`.
## Implementation Notes
1. Add `create_collector()` and `BaseCollector.is_available()` to `archive/v1/src/sensing/rssi_collector.py`
1. Add `create_collector()` and `BaseCollector.is_available()` to `v1/src/sensing/rssi_collector.py`
2. Refactor `ws_server.py` `_init_collector()` to call `create_collector()`
3. Update `install.sh` `detect_wifi_hardware()` to use shared detection logic
4. Add unit tests for each platform path (mock `/proc/net/wireless` presence/absence)

View File

@ -17,19 +17,19 @@ Address the 15 prioritized issues from the QE analysis in three waves: P0 (immed
### 1. Rate Limiter Bypass (Security HIGH)
- **Location:** `archive/v1/src/middleware/rate_limit.py:200-206`
- **Location:** `v1/src/middleware/rate_limit.py:200-206`
- **Problem:** Trusts `X-Forwarded-For` without validation. Any client bypasses rate limits via header spoofing.
- **Fix:** Validate forwarded headers against trusted proxy list, or use connection IP directly.
### 2. Exception Details Leaked in Responses (Security HIGH)
- **Location:** `archive/v1/src/api/routers/pose.py:140`, `stream.py:297`, +5 endpoints
- **Location:** `v1/src/api/routers/pose.py:140`, `stream.py:297`, +5 endpoints
- **Problem:** Stack traces visible regardless of environment.
- **Fix:** Wrap with generic error responses in production; log details server-side only.
### 3. WebSocket JWT in URL (Security HIGH, CWE-598)
- **Location:** `archive/v1/src/api/routers/stream.py:74`, `archive/v1/src/middleware/auth.py:243`
- **Location:** `v1/src/api/routers/stream.py:74`, `v1/src/middleware/auth.py:243`
- **Problem:** Tokens in query strings visible in logs/proxies/browser history.
- **Fix:** Use WebSocket subprotocol or first-message auth pattern.

View File

@ -1,245 +0,0 @@
# ADR-083: Per-Cluster Pi Compute Hop
| Field | Value |
|----------------|--------------------------------------------------------------------------------------|
| **Status** | Proposed — pending field evidence on three-tier proposal scope |
| **Date** | 2026-04-26 |
| **Authors** | ruv |
| **Supersedes** | — |
| **Refines** | ADR-028 (capability audit), ADR-081 (5-layer kernel), ADR-066 (swarm bridge) |
| **Companion** | `docs/research/architecture/three-tier-rust-node.md`, `docs/research/architecture/decision-tree.md`, `docs/research/sota/2026-Q2-rf-sensing-and-edge-rust.md` |
## Context
ADR-028 established the per-node BOM at ~$9 (ESP32-S3 8MB) — ~$15 with a
mmWave sensor — and ADR-081 framed the firmware as a 5-layer adaptive
kernel running entirely on a single ESP32-S3 die. Both decisions are
correct for the **per-node** dimension; deployments that fit the
"sensor talks UDP to a server somewhere" shape work fine on this stack.
The three-tier-node research exploration
(`docs/research/architecture/three-tier-rust-node.md`) raised a separate
question: **what changes when a deployment scales past one or two rooms,
and where should the heavy compute live?** The exploration's answer
("dual ESP32-S3 + Pi Zero 2W per node") is one shape, but the
companion decision-tree (`decision-tree.md` §1, §3 L3, §5) identifies a
materially cheaper path: keep today's single-S3 sensor node unchanged
and add **one Pi per cluster of 36 sensor nodes**. The 2026-Q2 SOTA
survey (`sota/2026-Q2-rf-sensing-and-edge-rust.md`) confirms that the
load this path needs to carry — model inference, QUIC backhaul, and a
real secure-boot story — fits comfortably on a Pi-class SoC, while the
load it doesn't need to carry — CSI capture, ISR-precise wake control —
is exactly what the ESP32-S3 already does well.
The three things this ADR is about, all of which the current single-S3
deployment shape pushes onto the cloud or onto every individual node:
1. **Per-deployment ML inference.** WiFlow / DT-Pose / GraphPose-Fi
class models (410M params, 0.51.5 GFLOPs) want a Cortex-A53-class
target. The ESP32-S3 cannot host these; the cloud can but only at
the cost of round-trip latency. A per-cluster Pi inference hop is
the natural home.
2. **QUIC backhaul.** `quinn` + `rustls` is mature on Linux but does
not run on ESP32-class hardware in any production-grade form
(SOTA §5). A Pi terminating QUIC for a cluster gives every sensor
node QUIC's loss/handoff/multiplex properties without porting QUIC
to the MCU.
3. **Secure-boot anchor for OTA.** ESP-IDF Secure Boot V2 covers each
sensor node, but cluster-wide policy (which model is current, which
sensor MCU image is canary, what is the rollout ring) needs a
higher-trust local store. A Pi running buildroot + dm-verity +
signed FIT is a defensible anchor without the BOM hit of CM4 / Pi 5
(the latter is its own decision; see ADR-085 sketch below and
decision-tree.md L6).
The cluster-Pi shape does **not** require any change to ADR-028 or
ADR-081. The sensor node continues to be a single-MCU ESP32-S3 running
the 5-layer kernel. Everything new lives at the cluster boundary.
## Decision
Adopt **a per-cluster Pi hop** as the canonical RuView mid-scale
deployment shape. A "cluster" is **36 ESP32-S3 sensor nodes within
WiFi mesh range of one Pi**.
Specifically:
1. **Sensor nodes are unchanged.** They continue to run the ADR-081
5-layer kernel on a single ESP32-S3, emit `rv_feature_state_t`
packets (60 byte, ~5 Hz, ~300 B/s) over UDP, and connect via
ESP-WIFI-MESH or direct WiFi to the cluster Pi.
2. **Each cluster has exactly one Pi** acting as:
- **Sensor aggregator**: ingests UDP from all cluster sensor
nodes, runs feature-level fusion (multistatic + viewpoint
attention from the existing `wifi-densepose-ruvector` crate).
- **ML inference target**: hosts the WiFi-pose model and runs
inference at the cluster boundary, not on each sensor MCU.
- **QUIC client to the cloud / gateway**: terminates QUIC mTLS,
batches cluster-level events.
- **OTA + secure-boot anchor for its sensor nodes**: holds signed
manifests, stages canary rollouts, owns provisioning state.
3. **Cluster Pi SoC choice is deferred** to a future ADR (sketched
below as ADR-085). The acceptable candidates are Pi Zero 2W, Pi 4,
Pi 5, and CM4. The decision tree's L6 distinguishes these by
secure-boot threat model; this ADR does not pre-commit.
4. **The single-node deployment shape is not deprecated.** A
home-lab / single-room / development deployment can still run a
single ESP32-S3 talking UDP directly to the existing
`wifi-densepose-sensing-server`, no Pi required. The cluster Pi
becomes the recommended shape for fleets ≥ 3 sensor nodes.
### Boundary contract
The cluster Pi exposes two interfaces:
| Interface | Direction | Schema |
|------------------------|-------------------|-----------------------------------------------------------------------|
| **UDP `rv_feature_state_t` ingest** | sensor → Pi | Existing 60-byte packed struct from ADR-081 (magic `0xC5110006`) |
| **QUIC mTLS uplink** | Pi → gateway/cloud | New: cluster-level event envelope (CBOR), batched, ~10 KB/min upper bound |
Sensor → Pi is **the same wire as today's sensor → server**. Cluster Pi
uplink is **new** and is what the existing `wifi-densepose-sensing-server`
becomes — relocated from the user's laptop / container to the cluster
node. Concretely: the sensing server already exists in
`crates/wifi-densepose-sensing-server`; it cross-compiles to ARMv7 /
AArch64 today via `cargo build --target aarch64-unknown-linux-gnu`. The
relocation is a deployment change, not a re-implementation.
### Three-tier vs cluster hop
This ADR's cluster-Pi shape is the L3-hybrid path in
`decision-tree.md` §2 — **not** the full three-tier (dual-MCU + per-node
Pi) shape. It captures most of the value (ML, QUIC, secure-boot anchor)
at minimal BOM impact. The full three-tier shape remains the long-term
exploration target, blocked behind L4 (no_std CSI maturity) and L2
(per-node ISR-jitter evidence).
## Consequences
### Positive
- **Pose-grade ML on edge becomes deployable**, not just possible. A
Pi (any of the eligible SoCs) hosts WiFlow-class models with
≤ 100 ms latency per cluster, vs ≥ 1 s round-trip if pose runs in the
cloud (SOTA §1, §3).
- **QUIC arrives without an MCU port.** `quinn` + `rustls` runs on the
Pi as it does on a server (SOTA §5). The sensor MCU keeps UDP — the
cheapest, highest-tested wire it already speaks.
- **Cluster-level secure boot becomes coherent.** Per-sensor Secure
Boot V2 + flash encryption (ADR-028 baseline) is unchanged. The Pi
buildroot + dm-verity image is the cluster trust anchor and signs
the OTA manifests for its sensors. The cluster-level threat model is
expressible without per-sensor BOM regression.
- **No PCB respin.** Sensor nodes are bit-for-bit identical to today's
ADR-028 baseline. The cluster Pi is a separate device on the cluster
WiFi (and / or Ethernet, if available).
- **Deployment cost scales sub-linearly with sensor count.** One
$25$60 Pi per 36 sensor nodes adds ~$5$20 per sensor amortized,
vs ~$25$50 per sensor for the per-node-Pi shape.
### Negative
- **The cluster Pi is a new piece of infrastructure to provision,
monitor, and update.** It is the right place for cluster-level
responsibilities, but it is not free; it adds a Linux box to every
multi-room deployment. Mitigated by buildroot images and the
existing OTA tooling story (see Implementation §4).
- **Cluster Pi failure takes the cluster offline** (sensor nodes
cannot uplink without a working aggregator on the WiFi LAN). For
high-availability deployments, this ADR is the floor; an HA-pair
cluster Pi would be a follow-up.
- **One more network hop on the sensing path.** Sensor → Pi → cloud
adds ~520 ms over Sensor → cloud (depending on link quality).
Pose latency budgets are 100s of ms, so this is well inside spec.
### Neutral
- ADR-028 (capability audit), ADR-081 (5-layer kernel), and ADR-066
(swarm bridge) are unchanged. This ADR adds a new device class above
the sensor; it does not modify the sensor itself.
- The home-lab single-node shape continues to work; this ADR adds a
recommended path for fleets, it does not deprecate the existing one.
## Implementation
The implementation is intentionally light because most of the pieces
already exist; the ADR is largely about formalizing where they live.
1. **Cluster-Pi cross-compile target.** Add to
`rust-port/wifi-densepose-rs/.cargo/config.toml` (or the equivalent
per-crate target spec) an `aarch64-unknown-linux-gnu` target so
`wifi-densepose-sensing-server` builds for Pi 4 / 5 / CM4 by
default. Also retain `armv7-unknown-linux-gnueabihf` for Pi Zero 2W
compatibility while the Pi-SoC decision (ADR-085 sketch) is open.
2. **Cluster-Pi service unit.** Add a systemd unit file under
`firmware/cluster-pi/` (new directory) that runs
`wifi-densepose-sensing-server` with the cluster's UDP/QUIC ports
and drops privileges. Buildroot integration is a separate ADR if
the SoC choice goes to Pi Zero 2W (where there's no RPi-OS path).
3. **QUIC uplink module.** Add `wifi-densepose-sensing-server` a
feature-gated `quic-uplink` module using `quinn` + `rustls`. The
feature is **off by default** in the home-lab shape and on for the
cluster Pi.
4. **OTA + signed-manifest flow.** Out of scope for this ADR; tracked
as I4 in `decision-tree.md` §4. The cluster Pi's role is to *hold*
the manifest store, not to define the manifest format. Use the
existing ADR-066 swarm bridge channel for OTA staging.
5. **Documentation update.** README's hardware-table gains a
"Cluster compute" row. CLAUDE.md gets a one-paragraph cluster-Pi
section under Architecture. User-guide gets a cluster-deployment
section.
6. **Validation.** A 3-sensor cluster + 1 Pi fixture in the lab.
Pass criteria: end-to-end CSI → cluster fusion → cloud ingest;
measured latency under 100 ms per cluster; cluster Pi reboot
without sensor data loss > 5 s; OTA staging round-trip across all
sensors in the cluster.
## Validation
This ADR is **proposed**, not accepted. Acceptance requires:
1. The cluster-Pi `wifi-densepose-sensing-server` cross-compiles
cleanly on `aarch64-unknown-linux-gnu` and `armv7-unknown-linux-gnueabihf`
targets with the existing workspace tests passing.
2. A 3-sensor + 1-Pi field test demonstrates ≥ 4 hours stable
end-to-end CSI → fusion → cloud round-trip with latency
≤ 100 ms per cluster and zero phantom-skeleton regressions
(ADR-082 holds across the new uplink).
3. The cluster-Pi ↔ sensor secure-boot story is approved alongside
ADR-085's SoC choice.
When the above pass, this ADR moves from **Proposed** → **Accepted**
and the README + CLAUDE.md are updated to reflect cluster-Pi as the
recommended fleet-shape.
## Related ADRs (current and proposed)
- **ADR-028** (Accepted) — ESP32 capability audit. Single-node BOM
baseline. Unchanged by this ADR.
- **ADR-029** (Proposed) — RuvSense multistatic sensing mode. Pairs
naturally with cluster-Pi: cluster Pi is the natural home for
multi-sensor fusion.
- **ADR-066** — Swarm bridge to coordinator. The cluster-Pi is the
per-cluster swarm coordinator endpoint.
- **ADR-081** (Accepted) — 5-layer adaptive CSI mesh firmware kernel.
Unchanged by this ADR.
- **ADR-082** (Accepted) — Pose tracker confirmed-track output filter.
Holds across UDP and QUIC uplinks identically.
- **Future ADR (sketched in `decision-tree.md` L4)**`no_std` CSI
capture maturity benchmark. Gates the dual-MCU shape; not required
for the cluster-Pi shape proposed here.
- **Future ADR (sketched in `decision-tree.md` L6)** — Cluster-Pi SoC
choice (Pi Zero 2W vs CM4 vs Pi 5). Pure secure-boot decision.
## Open questions
- **Cluster size sweet spot.** "36 nodes" is a planning estimate. The
3-sensor lab fixture in §Implementation will inform whether the
upper bound is closer to 4, 6, or 8 in practice.
- **Cluster-Pi failure semantics.** Default behavior: sensor MCUs hold
the last 60 s of feature packets in RAM and replay on reconnect.
HA-pair cluster Pi is a separate ADR if needed.
- **Mesh control-plane interaction.** If the deployment moves to
Thread (decision-tree.md L5), the cluster Pi may need a Thread
Border Router role. This ADR doesn't pre-commit; it's compatible
with both ESP-WIFI-MESH and Thread futures.

View File

@ -1,276 +0,0 @@
# ADR-084: RaBitQ Similarity Sensor for CSI / Pose / Memory Routing
| Field | Value |
|----------------|-----------------------------------------------------------------------------------------|
| **Status** | Accepted — Passes 15 + L1L4 hardening implemented and merged via PR #435 (commit `d71ef9a`); acceptance numbers in §"Acceptance test" all measured and passing on synthetic AETHER-shape data; the `< 1 pp end-to-end accuracy regression` criterion is tracked as a post-merge soak test |
| **Date** | 2026-04-26 |
| **Authors** | ruv |
| **Refines** | ADR-024 (AETHER re-ID embeddings), ADR-027 (cross-environment domain generalization), ADR-076 (CSI spectrogram embeddings), ADR-081 (5-layer firmware kernel) |
| **Companion** | ADR-083 (per-cluster Pi compute hop) |
| **Implements** | `vendor/ruvector/crates/ruvector-core/src/quantization.rs::BinaryQuantized` |
## Context
RuView's signal pipeline already produces several **dense float
embeddings** at different layers:
- AETHER 128-d re-ID embeddings on each `PoseTrack` (ADR-024)
- 64256-d CSI spectrogram embeddings (ADR-076)
- per-room field-model eigenmode vectors (ADR-030)
- per-frame multistatic fused vectors (ADR-029)
Every one of these eventually answers the same shape of question:
**"have I seen something like this before?"** Today the answer is
computed by full float dot-product / Mahalanobis comparisons against a
candidate set. That cost grows linearly with stored vectors and
quadratically when used inside dynamic-mincut graph maintenance,
re-identification re-scoring, and cross-environment domain detection.
The vendored `ruvector-core` crate already ships a 1-bit quantization
(`BinaryQuantized`, 32× compression, SIMD popcnt + hamming distance)
that is functionally equivalent to the **RaBitQ** family of binary
sketches: a vector is reduced to one bit per dimension, compared via
hamming distance, and used as a coarse pre-filter before full
precision refinement. The same module also exposes `ScalarQuantized`
(int8, 4×) and `ProductQuantized` (PQ, 816×), so the tiered
quantization story is already implemented; the *deployment pattern* is
not.
The user observation that motivates this ADR: **RaBitQ-style sketches
are not just a vector compression trick — they are a cheap similarity
sensor.** Used as a sensor, they unlock:
- always-on novelty / anomaly gating that wakes heavy CNNs only on
meaningful change
- cluster-Pi memory routing (which shard / room / model to query first)
- cross-node mesh exchange of compressed sketches instead of raw vectors
- privacy-preserving event logs (sketches, not reconstructable signals)
This ADR formalizes the deployment pattern across the RuView stack and
commits to `ruvector::quantization::BinaryQuantized` as the canonical
implementation.
## Decision
Adopt **RaBitQ-style binary sketches as a first-class, cheap
similarity sensor** at four points in the RuView pipeline:
1. **CSI / pose embedding hot-cache filter** at the cluster Pi.
2. **Drift / novelty sensor** between live observation and a
per-room normal-state bank.
3. **Mesh-exchange compression** between sensor nodes when reporting
cross-cluster events.
4. **Privacy-preserving event log** at the cluster Pi and gateway.
The canonical pattern at every point is:
```text
dense embedding ──► RaBitQ sketch ──► hamming/popcnt compare
├──► candidate set (top-K)
└──► novelty score (0..1)
┌── below threshold ──► emit summary, no escalation
└── above threshold ──► full-precision refinement
├──► ruvector mincut / HNSW
├──► AETHER re-ID rescoring
└──► pose model / CNN wake
```
### Implementation home
- **Sketch type and SIMD primitives**:
`vendor/ruvector/crates/ruvector-core/src/quantization.rs::BinaryQuantized`
— already implemented, already SIMD-accelerated (NEON on aarch64,
POPCNT on x86_64). Re-export through a new
`crates/wifi-densepose-ruvector/src/sketch.rs` module so consumers in
`signal`, `train`, `mat`, and `sensing-server` see a stable
RuView-flavored API and don't bind directly to the vendor crate.
- **Per-room normal-state bank**: lives at the cluster Pi (ADR-083),
not on the sensor MCU. Sensor MCUs continue to emit dense embeddings
in the existing `rv_feature_state_t` packet shape; sketching happens
on the Pi where the candidate bank is.
- **Sketch versioning**: each sketch carries a 16-bit `sketch_version`
field so the Pi can tell incompatible sketches apart when an
embedding model upgrades. Bumped on every embedding-model change.
### Where the sensor sits in the pipeline
| Pipeline stage | Today (full float) | With RaBitQ similarity sensor |
|---|---|---|
| AETHER re-ID match | full 128-d cosine on every active track × candidate | hamming pre-filter to top-K, then full cosine on K |
| Mincut subcarrier selection | full graph re-evaluation | sketch-flagged "likely-changed" boundary edges, full mincut on those |
| CSI room fingerprint | trained classifier on full embedding | sketch hamming to per-room sketch, classifier on miss |
| Field-model novelty (ADR-030) | residual-energy threshold | sketch novelty as second gate before SVD redo |
| Mesh / inter-cluster sync | dense embedding broadcast | sketch broadcast; full vector only on miss |
| Event log retention | full embedding stored | sketch + witness hash stored; raw embedding ephemeral |
In every row, the **decision boundary is unchanged** — full precision
still owns the final answer. The sketch is a sensor that only gates
which comparisons run, not what they decide.
### Acceptance criterion (per the source proposal)
The system-level acceptance test is:
> RaBitQ should reduce compare cost by **8× to 30×** while preserving
> top-k decisions well enough that full refinement changes **fewer
> than 10%** of final results.
Concretely, this means:
- Sketch compare must be measurably **8× cheaper** than the float
comparison it replaces (criterion-bench in `signal/`).
- Top-K candidate set chosen by sketch must contain ≥ 90% of the
candidates the full-float pass would have picked (offline replay
against recorded CSI).
- End-to-end pose / re-ID accuracy must regress by **less than 1
percentage point** vs the full-float baseline on the existing
evaluation set.
If any of these three fail, the sensor is rolled back at that point in
the pipeline and the failing site reverts to full float; the rest of
the pipeline keeps using sketches. This is point-by-point, not
all-or-nothing.
## Consequences
### Positive
- **Cheaper hot path everywhere a "have I seen this" question lives.**
AETHER re-ID, mincut maintenance, room fingerprinting, novelty
detection, mesh sync, and event-log retention all run a 32×-smaller,
popcnt-friendly comparison first.
- **Always-on anomaly gating becomes affordable.** The CNN / pose
model only wakes when sketch novelty crosses a threshold. Energy
budget per node drops materially in steady-state quiet rooms.
- **Privacy story improves.** Event logs and inter-cluster mesh
traffic carry sketches and witness hashes, not reconstructable
embeddings. The 1-bit quantization is *not* invertible to the
original CSI.
- **Composes cleanly with ADR-083.** The cluster Pi is the natural
home for the sketch bank; sensor MCUs remain unchanged.
- **No new dependency.** `BinaryQuantized` is already in the vendored
`ruvector-core` and already SIMD-accelerated.
### Negative / risks
- **Sketch quality depends on embedding distribution.** Pure 1-bit
sign quantization (which `BinaryQuantized` implements) works best
when the embedding space is roughly zero-centered and isotropic.
AETHER and CSI spectrogram embeddings need to be benchmarked for
this assumption; if either fails, a randomized rotation
(Johnson-Lindenstrauss / RaBitQ-paper-style) must be added before
sketching. Out-of-scope for this ADR; tracked as a follow-up if
the acceptance test fails.
- **Top-K coverage degrades for small candidate sets.** With < 16
candidates, the sketch compare can pick the wrong K. Site-by-site
fallback to full float is part of the rollout plan.
- **Sketch-version skew during model upgrades.** A model change
invalidates all stored sketches; the cluster Pi must re-sketch the
candidate bank when `sketch_version` bumps. Cost is bounded but
non-zero.
### Neutral
- ADR-024, ADR-027, ADR-029, ADR-030, ADR-076 are unchanged in
*what* they compute. They gain a sketch pre-filter at the comparison
step.
- ADR-082's confirmed-track output filter is upstream of the sketch
layer; it stays correct.
## Implementation
The implementation lands in five passes, each independently testable.
Every pass is gated by the acceptance criterion above; if any fail,
that site rolls back and the rest continue.
1. **`wifi-densepose-ruvector::sketch` module.** Re-export
`BinaryQuantized` plus a thin RuView-flavored API
(`Sketch::from_embedding`, `Sketch::distance`, `SketchBank::topk`).
Add `sketch_version: u16` and `embedding_dim: u16` fields to the
public type. Criterion benches: sketch ↔ float compare-cost ratio.
2. **AETHER re-ID pre-filter.** In
`wifi-densepose-signal/src/ruvsense/pose_tracker.rs`, before
computing the full 128-d cosine across active tracks × candidates,
sketch both sides and reduce to top-K via hamming. Bench: re-ID
pass time per frame, ID-stability under cross-room transitions.
3. **Cluster-Pi novelty sensor.** In
`wifi-densepose-sensing-server`, maintain a per-room
`SketchBank` of "normal-state" sketches; on each incoming
`rv_feature_state_t`, compute embedding sketch, score novelty
against the bank, and emit `novelty_score` as a new field on the
WebSocket update envelope. Heavy CNN wake gate uses this score.
4. **Mesh-exchange compression.** Inter-cluster broadcasts (the
ADR-066 swarm-bridge channel) carry sketch + witness instead of
the full embedding when novelty is low. Full embedding only
exchanged when novelty crosses threshold.
5. **Privacy-preserving event log.** Event log table on the cluster
Pi stores `(sketch_bytes, sketch_version, novelty_score,
witness_sha256)` instead of raw embeddings. Existing log readers
are unchanged in API; only the storage layer rewrites.
Each pass adds tests: a property test (sketch ↔ float top-K agreement
≥ 90%), a criterion bench (≥ 8× compare cost reduction), and an
end-to-end accuracy regression test (< 1 pp drop).
## Validation
This ADR is **proposed**, not accepted. Acceptance requires the three
acceptance numbers above to hold on **at least three of the five
implementation passes** (the sites where the bulk of the load sits:
AETHER re-ID, cluster-Pi novelty, and event log). The mesh-exchange
and mincut prefilter passes are nice-to-haves; they can ship
afterward if their per-site numbers hold.
Validation runs against:
- the existing 1,539-test workspace suite (must stay green)
- a new `tests/integration/rabitq_sketch_pipeline.rs` integration test
driving recorded CSI through the full pipeline with and without
sketches, comparing top-K decisions and end-to-end pose accuracy
- ESP32-S3 on COM7 — sensor MCU unchanged; sketch happens at the
cluster Pi, so this validation is a smoke test that the
sensor → Pi UDP path still works after the cluster Pi gains the
sketch bank
## Related
- **ADR-024** (Accepted) — AETHER re-ID embeddings. Primary consumer
of the sketch pre-filter.
- **ADR-027** (Accepted) — Cross-environment domain generalization
(MERIDIAN). Per-room sketch bank is the natural data structure for
domain detection.
- **ADR-030** (Proposed) — RuvSense persistent field model. Sketch
novelty is the cheap second gate before SVD recompute.
- **ADR-066** — Swarm bridge to coordinator. Inter-cluster sketch
exchange.
- **ADR-076** (Accepted) — CSI spectrogram embeddings. Sketch
consumer; embedding source.
- **ADR-081** (Accepted) — 5-layer adaptive CSI mesh firmware kernel.
Sensor MCU unchanged by this ADR; sketches happen at the cluster Pi.
- **ADR-083** (Proposed) — Per-cluster Pi compute hop. Defines the
device class that hosts the sketch bank.
## Open questions
- **Does `BinaryQuantized` need a randomized rotation pre-pass for
RuView's embedding distributions?** Pure sign quantization assumes
zero-centered, isotropic embeddings. If AETHER / spectrogram
distributions are skewed (likely for spectrogram), add a
`randomized_rotation` pre-pass following the original RaBitQ paper
(Gao & Long, SIGMOD 2024). Decided after pass-1 benchmark.
- **Sketch dimension target.** Default to the embedding's native
dimension (128 for AETHER, 256 for spectrogram). Higher-dimensional
sketches (Johnson-Lindenstrauss-projected to 512) trade compute for
recall; benchmark before committing.
- **Per-room vs per-deployment sketch banks.** Defaulting to per-room
for novelty detection. Cross-room re-ID may want a shared bank;
decide once cross-room AETHER traces are available.

View File

@ -1,452 +0,0 @@
# ADR-085: RaBitQ Similarity Sensor — Pipeline Expansion (Seven Additional Sites)
| Field | Value |
|----------------|------------------------------------------------------------------------------------------------------------------------------------------------|
| **Status** | Proposed |
| **Date** | 2026-04-25 |
| **Authors** | ruv |
| **Refines** | ADR-084 (RaBitQ similarity sensor, five-site baseline) |
| **Touches** | ADR-027 (cross-environment generalization), ADR-028 (capability audit / witness bundle), ADR-066 (swarm-bridge to coordinator), ADR-073 (multifrequency mesh scan), ADR-076 (CSI spectrogram embeddings), ADR-081 (5-layer firmware kernel), ADR-082 (confirmed-track filter), ADR-083 (per-cluster Pi compute hop) |
| **Companion** | `v2/crates/wifi-densepose-ruvector/src/sketch.rs` (ADR-084 Pass 1 — `Sketch`, `SketchBank`, `SketchError`; on branch `feat/adr-084-pass-1-sketch-module`, commits `6fd5b7d` + `1df9d5f7d`) |
## Context
ADR-084 committed RuView to **RaBitQ-style binary sketches as a cheap
similarity sensor** (Gao & Long, SIGMOD 2024 — arxiv 2405.12497) at
five pipeline sites: AETHER re-ID pre-filter, cluster-Pi novelty,
mincut subcarrier maintenance, mesh-exchange compression, and the
privacy-preserving event log. Pass 1 of that work landed the
`wifi-densepose-ruvector::sketch` module and benched at **4351×
compare speedup at d=512** and **7.5× top-K speedup at k=8 over 1024
sketches** — comfortably above the ADR-084 acceptance threshold of
8×. The sketch primitive is no longer an open question; the question
is where else in the pipeline the same sensor pattern earns its keep.
Seven additional sites have been identified, all outside the ADR-084
five but matching the same shape — code that asks "is this familiar?"
against a stored set, today by way of a full float compare or model
invocation. The unifying rule articulated alongside ADR-084 — *sketch
first, refine on miss, store the witness hash instead of the raw
embedding* — applies to all seven.
This ADR formalizes those seven sites in one document rather than
seven small ADRs because (a) they share one primitive and one
acceptance shape, so evaluating in isolation hides the pattern;
(b) most involve modest code surgery (< 200 LOC at the call site)
and an ADR-per-site would inflate the ledger without buying
decision-resolution; (c) the few sites that *do* raise novel
questions (Mahalanobis pre-filtering, REST similarity API shape,
witness-hash format for non-vector data) are flagged under Open
Questions and may spin out as follow-ups if their answers prove
load-bearing. ADR-084 owns the primitive; ADR-085 owns the
*deployment surface*.
## Decision
Apply the ADR-084 sketch sensor pattern at seven additional sites,
listed in the order they will be implemented (cheapest-first /
lowest-risk-first). Each entry states (a) **what is sketched**,
(b) **what triggers the comparison**, (c) **what the refinement step
on a miss is**, and (d) **what artifact stands in for the raw
embedding** — i.e., the witness hash.
### Site 1 — Per-room adaptive classifier short-circuit
**Crate:** `wifi-densepose-sensing-server`
`src/adaptive_classifier.rs::classify` (per-class centroids and spread,
Mahalanobis-like distance per frame).
- **Sketched:** Each per-class centroid `µ_k` (already a fixed-dim
feature vector). Sketches live in a `SketchBank` keyed by class id,
rebuilt whenever a class is re-trained.
- **Trigger:** Every classification call, before the float Mahalanobis
distance loop runs.
- **Refinement on miss / first cut:** Hamming top-K (K = 3) selects
candidate classes; full Mahalanobis runs only on those K. If the
hamming top-1 disagrees with the eventual Mahalanobis winner, log
the disagreement and fall back to full evaluation against all
classes for that frame.
- **Witness hash:** `sha256(centroid_bytes || spread_bytes ||
sketch_version)` per class, recorded once at classifier-train time
and stored alongside the sketch.
The sketch only narrows; Mahalanobis still decides on the K
candidates, preserving the original distance-to-class semantics.
Substituting Mahalanobis for the standard RaBitQ exact-distance
re-rank step (Gao & Long 2024) is, to our knowledge, novel — Open Q1.
### Site 2 — Recording-search REST endpoint
**Crate:** `wifi-densepose-sensing-server`
`src/recording.rs` plus a new HTTP handler in `src/main.rs`.
- **Sketched:** Each recording's pooled CSI/embedding signature (mean
AETHER embedding over the recording, or mean spectrogram embedding
per ADR-076). One sketch per recording, stored next to the recording
metadata.
- **Trigger:** `GET /api/v1/recordings/similar?to=<id>&k=N` request.
- **Refinement on miss:** Hamming top-K returns a candidate list of
recording ids. Full embedding refinement is **opt-in** via a
`&refine=true` query param that loads the candidate recordings'
full embeddings (if stored) and re-ranks. Default behavior is
sketch-only — the endpoint trades exact ranking for the ability to
ship without storing full embeddings server-side.
- **Witness hash:** `sha256(sketch_bytes || recording_id ||
sketch_version)` returned in the response payload as the result row
identifier. The raw embedding is **not retained** by default; the
hash is the artifact a client can use to assert which sketch
produced the match.
Delivers "find recordings that look like this one" without
long-term embedding storage. The shape is closer to SimHash dedup
APIs than to Qdrant's `/collections/{name}/points/search` (the
closest Rust-native vector-DB endpoint, which returns full vectors)
— deliberate; see Open Q4.
### Site 3 — WiFi BSSID fingerprinting (channel-hop scheduler input)
**Crate:** `wifi-densepose-wifiscan`
new `bssid_sketch` module beside the existing scan/result types.
- **Sketched:** A short per-BSSID time-series feature vector — recent
RSSI, SNR, channel, beacon interval, capability flags — pooled over
a rolling window (e.g., last 60 s). One sketch per (BSSID, window).
- **Trigger:** Each scan tick, after the multi-BSSID scan completes.
The current window's sketch is compared against the prior window's
bank.
- **Refinement on miss:** A sketch whose nearest neighbor's hamming
distance exceeds a threshold flags the BSSID as **novel** (newly
appeared, or known-AP-changed-beyond-recognition). The hop scheduler
(ADR-073) reads novelty as a hint to give the affected channel
more dwell time on the next rotation.
- **Witness hash:** `sha256(bssid || pooled_features || sketch_version
|| window_end_unix)` stored in the per-AP novelty log; raw
per-BSSID time series is dropped after the sketch is taken.
Anomaly detection over a heterogeneous low-dim vector; acceptance
is **false-positive rate on stable deployments**, not top-K
coverage. IEEE 802.11bf-2025 (published March 2025) standardizes
sensing measurement frames but not BSSID-novelty heuristics, so
this site does not duplicate the standard's scope.
### Site 4 — mmWave radar signature memory
**Crate:** `wifi-densepose-vitals`
`src/preprocessor.rs` and `src/anomaly.rs` (LD2410 / MR60BHA2 input
path).
- **Sketched:** A per-frame radar signature vector — range bins,
Doppler bins, peak frequencies — sketched at the same cadence as
the radar input (~10 Hz).
- **Trigger:** Every incoming radar frame, before the heavy vital
signs DSP runs. The current sketch is compared against a small
per-room "have we seen this kind of frame before" bank.
- **Refinement on miss:** A sketch within hamming distance of a known
signature short-circuits to "no new event"; vital signs DSP stays
asleep. A sketch beyond threshold wakes the full breathing/heart
pipeline (`vitals::breathing`, `vitals::heartrate`) for one or more
frames, then re-sleeps once the bank update settles.
- **Witness hash:** `sha256(signature_bytes || sensor_kind ||
sketch_version)` stored in the vitals event log; the raw radar
frame is not retained beyond the rolling preprocessor buffer.
Energy is the headline: vital signs DSP (band-pass + phase-fusion +
heart/breath FFT) is the most expensive cluster-Pi operation per
minute of quiet-room time. Published FMCW pipelines treat the DSP
stage as always-on after presence; **no primary source** found for
"binary-sketch wake-gate over a per-room radar signature bank" —
this is a direct extension of ADR-084's novelty sensor.
### Site 5 — Witness bundle similarity (ADR-028 release-CI signal)
**Crate:** Out-of-tree — addition to `scripts/generate-witness-bundle.sh`
plus a new `scripts/witness_drift_check.py`.
- **Sketched:** Each release's witness bundle "fingerprint" — a fixed
vector built from per-component SHA-256 prefixes plus numeric
attestation values (test count, proof hash byte-segments,
per-firmware sizes). One sketch per release.
- **Trigger:** Run during the CI release job, after the witness
bundle is generated and before publication.
- **Refinement on miss:** A sketch whose hamming distance to the prior
release exceeds threshold flags the release as **drifted** and
surfaces the changed components in the CI summary. The release is
not blocked; the signal is a ratchet that says "these components
changed by more than the recent baseline, take a second look."
- **Witness hash:** `sha256(sketch_bytes || release_tag ||
sketch_version)` published alongside the witness bundle as
`WITNESS-LOG-<sha>.sketch`. The full bundle is the existing artifact;
the sketch hash is a 32-byte add-on.
Conservative use of the sensor — drift detection over a *very*
small candidate set (last 510 releases). Existing CI drift prior
art is autoencoder/SHAP-based commit-anomaly detection plus
PKI-signed artifact integrity; **no primary source** for
"binary-sketch over release-bundle fingerprint" as a CI signal.
Acceptance: "useful ratchet without false-firing on every
dependency bump." If no, the sketch step drops from the release
script — most readily revertible of the seven.
### Site 6 — Agent / swarm memory routing
**Crate:** `wifi-densepose-sensing-server`
`src/multistatic_bridge.rs` (ADR-066 swarm-bridge channel) and the
peer Cognitum Seed registration metadata.
- **Sketched:** Each Cognitum Seed's accumulated **historical bank**
signature — a pooled mean of the sketches it has stored over a
rolling horizon. One sketch per peer Seed; refreshed at peer
heartbeat cadence.
- **Trigger:** A sensor node escalates an event to the swarm. Before
broadcasting to all peer Seeds, the cluster Pi computes the event's
sketch and routes it to the **closest peer** by hamming distance.
- **Refinement on miss:** No nearby peer (all hammings above threshold)
→ broadcast to all. Nearby peer hits → unicast to that Seed first;
only escalate to broadcast if the routed Seed cannot resolve.
- **Witness hash:** `sha256(event_sketch || origin_seed_id ||
routed_seed_id || sketch_version || event_unix)` recorded in the
swarm-bridge audit log. The full event sketch is exchanged; the
hash is the routing-decision attestation.
A 12-Seed swarm broadcasting every event is O(n) message storm per
event; sketch-routing turns the common case into O(1) with O(n)
fallback. Closest published comparator: **MasRouter** (ACL 2025),
which routes LLM queries via a learned DeBERTa router; ADR-085's
variant is structurally similar but uses unlearned hamming compare
against each peer's pooled bank — cheaper, and resilient to peer
churn.
### Site 7 — Log / event-stream pattern detection
**Crate:** `wifi-densepose-sensing-server`
new `src/event_anomaly.rs` module reading the cluster Pi's
existing event stream.
- **Sketched:** A pooled feature vector over the recent-events window
(last hour by default) — counts per event type, mean inter-event
interval, sources distribution. One sketch per cluster, refreshed
every 5 minutes.
- **Trigger:** Every refresh tick. The current-hour sketch is compared
against the historical bank (last 24 hours of hourly sketches).
- **Refinement on miss:** Hamming distance above threshold flags the
hour as **anomalous behavior**; the cluster Pi raises a single
cluster-level alert with a pointer to the witness hash, **not** to
the raw events. No raw events leave the Pi as part of the alert
payload.
- **Witness hash:** `sha256(hourly_sketch || cluster_id || hour_unix
|| sketch_version)` recorded as the alert body. Raw events stay on
the cluster Pi behind the existing privacy boundary.
The most genuinely "anomaly detection" of the seven, and most
exposed to the non-vector witness-hash open question (event
features are mixed counts and rates needing normalization before
sketching). Closest published comparator: **LogAI** (Salesforce,
Drain parser → counter vectors → unsupervised detection); ADR-085's
variant sketches the counter vector, trading recall for constant
memory and sub-ms compare on the cluster Pi.
### Witness-hash discipline
In every site above, the witness hash replaces the raw embedding /
feature vector at the storage boundary — the same privacy posture
ADR-084 introduced for the cluster-Pi event log, generalized across
seven new contexts. The format is uniform:
`sha256(sketch_bytes || stable_metadata || sketch_version)`. Where
the input is not natively a dense vector (Sites 5 and 7), the
encoding into a sketchable shape is itself a design choice — see
Open Questions.
## Consequences
### Positive
- **The "is this familiar?" pattern becomes a first-class deployment
primitive across REST APIs, scanning subsystems, mmWave gating,
CI, swarm routing, and event analytics.** Each site is a modest
win individually; together they remove the last excuses to keep
full embeddings on every storage and exchange path.
- **Energy and bandwidth wins compound at the cluster boundary.**
Site 4 cuts vital signs DSP duty cycle; Site 6 cuts cross-cluster
broadcast load. Both are at the cluster Pi, where wattage matters.
- **Privacy story strengthens.** Every site stores a witness hash,
not raw data. Sites 2 and 7 are explicitly designed to ship
without retaining the embeddings or event payloads they index.
- **Reuses ADR-084 Pass 1 with no new dependency.** The
`wifi-densepose-ruvector::sketch` module already exposes
`Sketch`, `SketchBank`, `SketchError` at 4351× compare speedup.
- **Each site is independently testable and revertible.** The seven
passes share no data paths; failure at any one rolls back without
touching the others.
### Negative / risks
- **Mahalanobis distributional assumption (Site 1).** Pure 1-bit
sign quantization performs best on zero-centered, isotropic
embeddings; Mahalanobis explicitly encodes covariance structure
hamming distance is insensitive to. The sketch is used **only**
as a candidate-narrower; the Mahalanobis re-score preserves
semantics. But if hamming top-K systematically excludes the true
winner, the short-circuit is worse than no short-circuit. The
Validation acceptance test guards this; randomized rotation
pre-pass (RaBitQ-paper-style) may be needed — see Open Q1.
- **REST endpoint shape (Site 2) is an API surface commitment.**
A `GET /api/v1/recordings/similar` with a sketch-only default
is a contract; clients expect approximate-recall behavior.
Documenting "sketch-only by default, `&refine=true` for full
re-ranking" is part of the acceptance bar.
- **False-positive risk on Site 3 (BSSID novelty)** in dynamic
environments. Coffee-shop / co-working deployments see BSSIDs
rotate constantly; the signal must flag *unexpected* change,
not background churn — acceptance is framed accordingly.
- **Witness-hash format for non-vector inputs (Sites 5 and 7).**
Witness bundles and event streams are not natively dense-vector
data; the encoding into sketchable form (numeric SHA-prefix
segments; normalized event-type histograms) is itself a design
choice future model changes can break. `sketch_version` bumps
invalidate banks everywhere, but only Sites 5 and 7 must
re-encode raw inputs.
- **Operational surface area.** Seven banks each with their own
persistence, version-skew, and refresh story. The cluster Pi
gains non-trivial state. ADR-083's secure-boot / OTA story
holds, but state-rebuild cost on `sketch_version` bump is now
seven banks, not one.
### Neutral
- The five ADR-084 sites and the seven sites here are independent.
Acceptance or rollback at any one site does not propagate.
- ADR-082 (confirmed-track filter) remains upstream of every sketch
call. ADR-081 (5-layer firmware kernel) is unchanged — every new
bank lives at the cluster Pi or higher.
- ADR-027 (cross-environment generalization, MERIDIAN) interacts
cleanly: Site 1's per-class sketches are *per environment* by
construction, which is the same shape MERIDIAN already assumes.
## Implementation
Seven passes, ordered cheapest-first / lowest-risk-first. Each is
independently shippable; each has a single-line acceptance test that
must pass before the next pass starts.
| # | Pass | Target crate | Acceptance test (one line) |
|---|------|--------------|----------------------------|
| 1 | **Witness bundle drift sketch** (Site 5) | `scripts/witness_drift_check.py` | CI run on the last 5 releases produces ≥ 1 drift flag on a known dependency-bump release and 0 flags on a known no-op release. |
| 2 | **BSSID fingerprint novelty** (Site 3) | `wifi-densepose-wifiscan::bssid_sketch` | 24-hour soak in a stable office: novelty rate ≤ 5 events / hour; controlled new-AP injection: novelty fires within 2 scan cycles. |
| 3 | **mmWave signature gate** (Site 4) | `wifi-densepose-vitals::preprocessor` | Vitals DSP CPU time / hour ≥ 4× lower in steady-state empty-room compared to no-gate baseline; missed-detection regression ≤ 1 pp on the existing breathing/heart fixtures. |
| 4 | **Adaptive classifier short-circuit** (Site 1) | `wifi-densepose-sensing-server::adaptive_classifier` | Per-frame `classify` time reduced ≥ 2× at K = 3 candidates; classification accuracy regression ≤ 1 pp on the held-out test set. |
| 5 | **Event-stream anomaly sketch** (Site 7) | `wifi-densepose-sensing-server::event_anomaly` | 7-day rolling deployment: ≤ 1 false anomaly / day; injection of a synthetic anomalous hour fires within one refresh tick. |
| 6 | **Swarm memory routing** (Site 6) | `wifi-densepose-sensing-server::multistatic_bridge` | 12-Seed simulated swarm: per-event broadcast-message count drops ≥ 5× vs. unrouted baseline; routed-Seed-resolution rate ≥ 80%. |
| 7 | **Recording-search REST endpoint** (Site 2) | `wifi-densepose-sensing-server::recording` + HTTP route | `GET /api/v1/recordings/similar` returns a top-K with ≥ 90% candidate-set agreement vs. full-embedding re-rank on the recorded dataset; response time < 50 ms at K = 10 over 1000 recordings. |
ADR-084's general acceptance numbers — **830× compare cost
reduction, ≥ 90% top-K coverage, < 1 pp accuracy regression**
apply unchanged to Sites 1 (classifier) and 2 (recording search),
where the candidate set is large and top-K coverage is the right
framing. Sites 3, 4, 5, 6 are gating / anomaly / routing problems
measured against site-specific criteria above (false-positive rate,
DSP duty cycle, broadcast count, drift-flag precision). Each pass
adds three tests under `v2/crates/<target>/tests/`: property test
(sketch ↔ float top-K where applicable), criterion bench
(compare-cost ratio), end-to-end regression against recorded data.
Benches reuse the ADR-084 Pass 1 harness.
## Validation
This ADR is **Proposed**. Acceptance requires **at least four of
seven passes** to meet their per-row acceptance test. The four
must-haves are: **Site 1** (per-frame cost; Mahalanobis assumption
load-bearing), **Site 4** (cluster-Pi energy), **Site 6**
(cross-cluster bandwidth), **Site 7** (privacy-preserving anomaly).
Sites 2, 3, 5 are nice-to-haves and may ship or revert
independently.
Validation runs against:
- existing workspace tests (must stay green at
`cargo test --workspace --no-default-features` on `v2/`);
- a 7-day cluster-Pi soak at the lab fixture (3 sensor nodes + 1 Pi
per ADR-083) with recordings, mmWave, and BSSID scans active —
per-site logs graded against the Implementation table;
- Python proof harness unchanged (`archive/v1/data/proof/verify.py`
must still print `VERDICT: PASS`);
- regenerated witness bundle (ADR-028) including the Site 5 sketch.
When the four must-haves pass and the soak holds, ADR moves
**Proposed → Accepted** and README hardware/feature tables gain a
sketch-bank row.
## Open questions
1. **Does Mahalanobis pre-filtering survive sign-quantization bias
on Site 1?** Pure 1-bit sketches discard the covariance
structure Mahalanobis uses. The pass-1 framing — sketch narrows,
Mahalanobis decides — preserves correctness in expectation, but
adversarial centroid geometries can let the hamming top-K
systematically exclude the true winner. **No primary source
found** for "binary-sketch + Mahalanobis-refine" as a published
pipeline; marked as conjecture, gated by the Site-1 acceptance
test. If it fails, the next experiment is the randomized
rotation pre-pass from Gao & Long (SIGMOD 2024, arxiv
2405.12497), which ADR-084 also flagged for AETHER /
spectrogram embeddings. A standalone follow-up ADR is the
likely outcome if rotation is needed.
2. **Witness-hash format for non-vector data (Sites 5, 7).** The
release bundle (Site 5) and event stream (Site 7) are not
natively dense-vector inputs. The proposed encodings — numeric
SHA-256-prefix segments plus attestation values for Site 5;
normalized event-type histograms for Site 7 — are plausible
but unvalidated against drift in the underlying distributions.
A small follow-up ADR formalizing the "non-vector → sketchable"
canonical path is plausible if the two sites diverge.
3. **Cross-environment domain generalization interaction
(ADR-027).** Per-class sketches in Site 1 and per-room banks at
Sites 4 and 7 are implicitly per-environment artifacts; ADR-027
(MERIDIAN) handles cross-environment generalization at the model
layer. When MERIDIAN's domain detector flags an environment
shift, do banks rebuild, swap, or merge? Default here is
**rebuild on shift**; a merge story may be cheaper and is open
for the eventual MERIDIAN-aware deployment.
4. **REST API shape for Site 2.** The choice between
Qdrant/Pinecone/Weaviate-style endpoints (Qdrant being the
closest Rust-native comparator with HTTP `/points/search`) and
a thin sketch-only response is intentionally opinionated
toward the thin shape. **No Rust-idiom primary source** was
located for "sketch-only similarity search over recordings"
specifically; closest analog is SimHash-over-documents
deduplication, which lacks time-series-recording prior art.
If a clean Rust crate emerges owning this idiom, Site 2 may
delegate rather than ship bespoke.
5. **BSSID novelty and 802.11bf-2025 interaction.** IEEE 802.11bf
was published in March 2025 and standardizes WLAN sensing
measurement frames; Site 3's novelty sketch operates above the
measurement layer (on RSSI/SNR/channel time-series) and should
not duplicate what 802.11bf eventually exposes natively. **No
primary source found** for "RSSI-fingerprint anomaly + 802.11bf"
— marked as conjecture; revisit when client/AP support arrives.
## Related
- **ADR-027** (Proposed) — MERIDIAN cross-environment generalization.
Per-environment sketch banks (Sites 1, 4, 7) need an explicit
swap/rebuild story under MERIDIAN-detected domain shifts.
- **ADR-028** (Accepted) — ESP32 capability audit / witness bundle.
Site 5 adds a sketch ratchet to the existing release artifact.
- **ADR-066** (Proposed) — Swarm bridge to coordinator. Site 6 routes
over the bridge channel ADR-066 defines.
- **ADR-073** (Proposed) — Multifrequency mesh scan. Site 3's
BSSID novelty feeds the hop scheduler ADR-073 owns.
- **ADR-076** (Proposed) — CSI spectrogram embeddings. Site 2's
recording-search sketch can pool over spectrogram embeddings
when present, or fall back to AETHER means.
- **ADR-081** (Accepted) — 5-layer adaptive CSI mesh firmware kernel.
No firmware change; every new sketch bank is at the cluster Pi
or higher.
- **ADR-082** (Accepted) — Pose tracker confirmed-track filter.
Upstream of every sketch call; unchanged.
- **ADR-083** (Proposed) — Per-cluster Pi compute hop. The Pi is
the host for all seven new banks; ADR-083's deployment story is
the prerequisite.
- **ADR-084** (Proposed) — RaBitQ similarity sensor (five-site
baseline). This ADR refines and extends; it does not duplicate
ADR-084's compare-cost / top-K / accuracy acceptance numbers
where unchanged.

View File

@ -1,423 +0,0 @@
# ADR-086: Edge Novelty Gate — Push the RaBitQ Sensor Down to the Sensor MCU
| Field | Value |
|----------------|----------------------------------------------------------------------------------------------------------------------------------------------|
| **Status** | Proposed |
| **Date** | 2026-04-26 |
| **Authors** | ruv |
| **Refines** | ADR-081 (5-layer adaptive CSI mesh firmware kernel — Layer 4 / On-device feature extraction), ADR-084 (RaBitQ similarity sensor) |
| **Touches** | ADR-018 (binary CSI frame magic discipline), ADR-028 (capability audit / witness verification), ADR-082 (confirmed-track output filter), ADR-085 (RaBitQ pipeline expansion) |
| **Companion** | `firmware/esp32-csi-node/main/rv_feature_state.h` (current `0xC5110006` v6 wire format), `docs/research/architecture/three-tier-rust-node.md` (BQ24074 power budget context), `vendor/ruvector/crates/ruvector-core/src/quantization.rs::BinaryQuantized` (std reference implementation that this ADR will not directly reuse on-MCU) |
## Context
ADR-081's 5-layer firmware kernel today emits one `rv_feature_state_t`
packet per node every 1001000 ms (110 Hz, default 5 Hz on COM7),
60 bytes payload, magic `0xC5110006`, regardless of how interesting
the underlying CSI window was. At a 5 Hz baseline the per-node steady-
state load is ~300 B/s of UDP plus the radio TX duty that emits it.
Across a 12-node deployment the cluster Pi sees ~3.6 kB/s of
feature-state — not a bandwidth crisis on its own, but every one of
those packets also costs sensor-MCU radio TX energy, every one
contends for ESP-WIFI-MESH airtime per ADR-081 Layer 3, and every one
runs through the cluster-Pi novelty bank ADR-084 Pass 3 only to be
classified as "nothing new" most of the time in a quiet room.
ADR-084 made novelty cheap on the cluster-Pi side. The same novelty
sensor is structurally local: a sketch, a small ring of recent
sketches, and a hamming-distance compare. Pushing that gate down into
the sensor MCU's Layer 4 (On-device feature extraction) lets the node
*not transmit* a frame the cluster-Pi would have filed under
"familiar" anyway. Bandwidth, sensor-MCU TX energy, and RF airtime
all win, and the cluster-Pi novelty path stops re-doing work the edge
already proved pointless. This is the natural ADR-085 follow-up
flagged but deliberately left out of the ADR-085 scope because it
requires a `no_std` sketch port, a Kconfig-gated rollout, a wire-
format bump, and a fresh witness regeneration — none of which are
appropriate inside an in-flight cluster-Pi work loop.
The crux of the decision is whether the cost of (a) hand-porting the
sketch primitive to `no_std` Xtensa LX7, (b) sizing the in-IRAM ring
without disturbing the existing Layer 4 budget, (c) bumping the
`rv_feature_state_t` magic and teaching the cluster-Pi a graceful
v6/v7 fallback, and (d) re-cutting the ADR-028 witness bundle is
justified by the suppression rate the gate actually achieves on real
deployments. The answer should be obvious in stable rooms (≥50 %
suppression looks easy) and ambiguous in active rooms (suppression
should drop sharply, which is exactly what we want). This ADR commits
to numbers up front so the decision is falsifiable.
## Decision
Adopt an **edge novelty gate** in the sensor MCU's Layer 4 of
ADR-081's 5-layer kernel. The gate sits between feature extraction
and the existing UDP send path; when novelty is below a configurable
threshold the frame is **not transmitted**, and the node accumulates
a per-source `suppressed_since_last` counter that is folded into the
next non-suppressed packet. This keeps the cluster-Pi's books
honest — the edge can suppress *bandwidth*, but it can never
silently suppress the *fact of suppression*.
### Components
The implementation is two pieces, both new in
`firmware/esp32-csi-node/main/`:
1. **`rv_sketch.{h,c}`** — a `no_std`-equivalent (plain C, ESP-IDF)
1-bit sketch primitive. Sign-quantize a feature vector, pack into
bytes (`(dim + 7) / 8` bytes), hamming distance via 8-bit
table-lookup popcount. Xtensa LX7 has no hardware POPCNT
instruction (no primary source consulted; conjecture based on the
ESP32-S3 TRM not advertising one — to be confirmed by checking
the [TRM](https://www.espressif.com/sites/default/files/documentation/esp32-s3_technical_reference_manual_en.pdf)
under bit-manipulation extensions); the table-lookup scalar
baseline is the right starting point and is already what
`BinaryQuantized` falls back to on architectures without a SIMD
POPCNT path (`vendor/ruvector/crates/ruvector-core/src/quantization.rs`,
lines 332340).
2. **An IRAM-resident sketch ring.** Fixed size at compile time:
`RV_EDGE_BANK_SIZE` slots × `RV_EDGE_VECTOR_DIM_BYTES` bytes.
For the default Layer 4 feature dimension of 56 (matching the
subcarrier-selection / interpolation target widely used in this
codebase), the ring at the default 32 slots costs
`32 × 7 = 224 bytes`. A 64-slot ring at 56 d costs 448 bytes — both
sit comfortably inside the existing static-memory budget on either
the 4 MB or 8 MB Waveshare AMOLED ESP32-S3 board, well clear of
ADR-081 Layer 4's existing window buffers. Eviction is FIFO; on
each new sketch the oldest is overwritten.
### Gating policy
For each completed Layer 4 feature window:
```text
1. compute feature vector (existing)
2. sketch = sign_quantize(feature_vector) // new
3. nearest_hamming = ring_min_distance(sketch) // new
4. novelty = nearest_hamming / dim // 0..1, new
5. if novelty >= CONFIG_RV_EDGE_NOVELTY_THRESHOLD
OR suppressed_since_last >= CONFIG_RV_EDGE_MAX_CONSEC_SUPPRESS
OR CONFIG_RV_EDGE_FORCE_SEND:
ring_insert(sketch)
emit rv_feature_state_t v7 with suppressed_since_last
suppressed_since_last = 0
else:
suppressed_since_last += 1
// do not insert into ring — only confirmed-emitted sketches anchor the bank
```
Threshold default: `CONFIG_RV_EDGE_NOVELTY_THRESHOLD = 500`
basis-points (= 5.0 % of dimension). Kconfig does not accept floats
without contortion (the standard Espressif practice in our codebase
is to express thresholds as `int` basis-points or scaled fixed-point);
this preserves the Kconfig-as-truth discipline ADR-081 already
follows.
Suppression cap default:
`CONFIG_RV_EDGE_MAX_CONSEC_SUPPRESS = 50`. At 5 Hz that is 10 s of
forced silence at most before a "stuck gate" self-heals into a
forced send — comparable to ADR-081's slow-loop 30 s recalibration
cadence and well below any user-visible UI staleness threshold.
Default-off gate: `CONFIG_RV_EDGE_NOVELTY_GATE_ENABLE = n`. Existing
deployments behave identically until they opt in.
### Wire format — v7
Bump the `rv_feature_state_t` magic to `0xC5110007` and add three
bytes by reusing the existing 2-byte `reserved` field plus one byte
borrowed from the 16-bit `quality_flags` budget (only 8 of 16 flags
are defined today; we narrow to `uint8_t quality_flags`):
| Offset (v7) | Field | Notes |
|-------------|-----------------------------|--------------------------------------|
| 0..3 | `magic = 0xC5110007` | new; differentiates from `0xC5110006` |
| 4 | `node_id` | unchanged |
| 5 | `mode` | unchanged |
| 6..7 | `seq` | unchanged |
| 8..15 | `ts_us` | unchanged |
| 16..51 | nine `float` features | unchanged |
| 52 | `quality_flags` (`uint8_t`) | narrowed from u16 — see Open Q3 |
| 53 | `gate_version` (`uint8_t`) | new |
| 54..55 | `suppressed_since_last` | new (`uint16_t` LE) |
| 56..59 | `crc32` | unchanged, computed over [0..56) |
Total size: still 60 bytes, **wire-compatible at packet length but
not at field semantics** — magic is the discriminator. Cluster-Pi
receivers that recognize `0xC5110007` interpret the new fields;
receivers that recognize `0xC5110006` continue to work but do not
see the suppression count. The receiver gracefully falls back when
it sees the v6 magic; this is the explicit graceful-fallback contract
ADR-081 already established for Layer 5 stream parsing.
The choice to narrow `quality_flags` from 16 to 8 bits relies on the
fact that `rv_feature_state.h` defines exactly 8 `RV_QFLAG_*` bits
today (lines 3340); future flag growth is a separate ADR slot, and
the alternative — adding a 4th `uint8_t` and growing the packet to
64 bytes — costs a recompute of every Layer 5 parser and is more
intrusive than the magic bump.
## Consequences
### Positive
- **Sensor-MCU UDP TX duty cycle drops by the suppression rate.** A
back-of-envelope at 5 Hz: at 50 % suppression, ~150 B/s and
~2.5 packets/s per node instead of ~300 B/s and 5; at 90 %
suppression, ~30 B/s and 0.5 packets/s. ESP32-S3 TX energy at
+20 dBm is the dominant per-packet cost on the BQ24074-class node
(`docs/research/architecture/three-tier-rust-node.md` §3.3 power
budget shows ~80 mA active-CSI baseline with TX-burst spikes at
~150 mA peak; the gate primarily cuts the burst-frequency rather
than the baseline). ≥30 % TX-energy reduction in steady-state quiet
rooms is the validation target.
- **Cluster-Pi novelty path runs on a smaller stream.** ADR-084
Pass 3 is unchanged in code, but the input rate it processes drops
by the suppression rate. The Pi-side bank stops accumulating
redundant "stable" anchors and concentrates its bank slots on
actually-different frames. This is a quality win, not just a cost
win.
- **Mesh airtime contention drops, which improves ADR-081 Layer 3
for everyone else.** Less feature-state traffic frees airtime for
TIME_SYNC, ROLE_ASSIGN, FEATURE_DELTA, HEALTH, and ANOMALY_ALERT
— the high-priority mesh-control traffic that today competes with
routine feature-state in the same channel.
- **`suppressed_since_last` is observable.** The cluster-Pi can
detect a node that has been suppressing for too long, a node
whose suppression rate suddenly drops (occupant entered the
room — the right behaviour), and a node whose suppression cap is
triggering frequently (gate is mistuned). All three are useful
signals and all three live in fields the receiver already parses.
### Negative / risks
- **The cluster-Pi-side novelty sensor sees fewer data points.** This
is the load-bearing negative consequence and the most likely
source of regression. ADR-084 Pass 3's bank ages out anchors based
on insertion time; if the edge gate suppresses 70 % of frames in
a quiet room, the Pi bank receives 30 % of its expected anchor
rate and may take 3× longer to converge to a useful steady state
on a freshly-rebooted Pi. Mitigation: the validation acceptance
test runs the Pi-side novelty top-K coverage against an
unsuppressed baseline and budgets ≤5 percentage points regression.
If the cluster-Pi cold-start convergence becomes a real problem
the simplest patch is to force-send the first
`CONFIG_RV_EDGE_FORCE_SEND_BURST` (default 32) frames per
Layer 2 slow-loop recalibration window — but this lives outside
the ADR-086 baseline and is called out as a follow-up if needed.
- **Witness chain.** Per ADR-028, every change to firmware
invalidates the witness bundle. Edge novelty gate is a non-trivial
firmware change: it touches Layer 4, adds a wire-format magic,
and ships a Kconfig surface. The witness bundle must be re-cut
and the SHA-256 of the proof bundle is **expected** to change
(which is the whole point of the witness — the change must be
visible). The post-change validation step is to run
`bash scripts/generate-witness-bundle.sh` and confirm 7/7 PASS
via `dist/witness-bundle-ADR028-*/VERIFY.sh`.
- **Two wire-format magics in the field at once.** During rollout
some nodes emit v6 and some v7. The cluster-Pi receiver must
handle both, and the WebSocket "latest snapshot" path must not
accidentally null-out the new fields when re-encoding for v6
consumers. The graceful-fallback contract is small (~30 LOC on
the Pi), but it is a contract and breaking it loses observability
for the v7 nodes. Validation includes a mixed-version soak.
- **Pose-tracker interaction (Open Q4).** ADR-082 added a confirmed-
track output filter that already drops single-frame phantom poses
before they reach the WebSocket. The edge gate could *suppress
the very frames* that would have promoted a pose track from
Tentative to Active — i.e., a person walks through a quiet room
and the first 12 frames look "low novelty" because the gate
hasn't seen them yet, then the gate suddenly fires and emits the
third frame. ADR-082's three-frame minimum could miss a real pose.
Mitigation candidates: (a) lower the threshold during ADR-082
Tentative-state minutes; (b) treat motion_score above a fixed
floor as a force-send signal regardless of sketch novelty;
(c) accept the regression as part of the "novelty is precisely
what we wanted to gate on" framing. Decision deferred — Open Q4.
- **Operator debuggability.** A development-time
`CONFIG_RV_EDGE_FORCE_SEND` Kconfig flag bypasses the gate
entirely and is the right tool for diffing
with-gate vs without-gate behaviour during a deployment. Required.
### Neutral
- ADR-018's binary CSI frame stream is unchanged; the gate operates
on Layer 4 feature state, not on the debug raw-CSI path.
- ADR-085's seven cluster-Pi-side sketch sites that consume
`rv_feature_state_t` see *fewer* inputs but the same shape;
Sites 6 (swarm routing) and 7 (event-stream anomaly) will be
slightly less sensitive under v7. Re-measurement is recommended
but is not a blocker for ADR-086.
## Implementation
Six numbered passes, ordered cheapest-first / lowest-risk-first.
Each is independently shippable, each has a one-line acceptance
criterion that must pass before the next pass starts. Default-off
Kconfig means none of these passes can break a deployment that has
not opted in.
| # | Pass | Target | Acceptance |
|---|------|--------|------------|
| 1 | **`no_std` sketch primitive port** (`firmware/esp32-csi-node/main/rv_sketch.{h,c}`) | sensor-MCU C | QEMU unit test: 56-d sign-quantize of a fixed seed produces the bit-pattern matching the host-side reference; hamming distance round-trips. |
| 2 | **IRAM ring + insert/min-distance API** | sensor-MCU C | On-target benchmark on COM7: insert + ring-min on 32 slots ≤ 200 µs at 240 MHz. |
| 3 | **Kconfig flags** (`CONFIG_RV_EDGE_NOVELTY_GATE_ENABLE`, `_THRESHOLD`, `_MAX_CONSEC_SUPPRESS`, `_FORCE_SEND`) | `firmware/esp32-csi-node/main/Kconfig.projbuild` | Build with each flag toggled produces the expected `sdkconfig.defaults` merge; unit test asserts threshold of 500 bps maps to 5.0 % decision boundary. |
| 4 | **`rv_feature_state_t` v7 wire format + finalize() update** | `firmware/esp32-csi-node/main/rv_feature_state.{h,c}` | `_Static_assert(sizeof == 60)` still holds; CRC32 over the new layout round-trips; v6 receiver test reads a v7 packet without panic and ignores the new fields. |
| 5 | **Cluster-Pi reconciliation** | `crates/wifi-densepose-sensing-server/` UDP intake + ADR-084 Pass 3 novelty bank | A v7 packet with `suppressed_since_last = N` causes the Pi-side bank to interpret the gap as low-novelty stable-baseline contribution rather than as missing data; integration test on a synthetic v7 stream. |
| 6 | **QEMU + COM7 hardware-in-loop validation** | end-to-end | Stable-room recording: ≥50 % suppression rate; cluster-Pi novelty top-K coverage regression ≤ 5 pp vs unsuppressed baseline; stuck-gate self-heal exercised in a unit test. |
Pass 1 deliberately does not depend on
`vendor/ruvector/crates/ruvector-core::BinaryQuantized`. That crate
is `std`-bound (`Vec<u8>`, `is_x86_feature_detected!`, NEON
intrinsics — `quantization.rs` lines 289340) and porting it to
`no_std` Xtensa LX7 is not a one-line `#![no_std]` flip. The clean
path is a fresh minimal C primitive that matches the
`BinaryQuantized` *behaviour* (sign quantization, byte-table popcount
fallback, `(dim+7)/8` packed bytes); the host-side reference becomes
a **spec**, not a dependency. A future `no_std`-clean Rust port may
unify both once `esp-radio` / `esp-csi-rs` matures (three-tier node
research §7.3) — out of scope here.
## Validation
This ADR is **Proposed**. Acceptance requires every numbered Pass to
meet its acceptance criterion *and* the following system-level
numbers to hold on the COM7 hardware-in-loop run:
- **Computation budget**: sketch insert + ring-min ≤ 200 µs;
total per-frame Layer 4 overhead (existing feature extraction +
new gate) ≤ 500 µs at 240 MHz Xtensa LX7.
- **Energy**: ≥ 30 % UDP TX-energy reduction in stable-room
scenarios, measured by packets-per-second × per-packet TX duty
against an unsuppressed baseline. Direct mA-level measurement is
out of scope for this ADR; the proxy metric is sufficient.
- **Cluster-Pi accuracy**: ≤ 5 percentage-point drop on the
ADR-084 Pass 3 novelty top-K coverage metric vs an unsuppressed
baseline run on the same recorded CSI.
- **Bandwidth**: ≥ 50 % reduction in steady-state quiet-room UDP
byte rate per node.
- **Stuck-gate self-heal**: a unit test that pins the sketch
primitive output to "always low novelty" must observe a forced
send within ≤ 10 s (≤ 50 frames at 5 Hz).
- **Existing test gates**: `cargo test --workspace
--no-default-features` stays green; `python v1/data/proof/verify.py`
stays green (the proof harness sees no firmware-side change and
the SHA-256 should not move because the proof exercises Python
pipeline math, not firmware behaviour); the witness bundle
(`scripts/generate-witness-bundle.sh`) runs and the resulting
`VERIFY.sh` reports 7/7 PASS — **the bundle's own SHA-256 will
differ**, which is the witness-chain signal that firmware
changed.
If any system-level number fails, the gate ships behind
`CONFIG_RV_EDGE_NOVELTY_GATE_ENABLE = n` (default-off) and the ADR
moves to **Rejected** for that hardware target while the wire-format
v7 changes are kept (they cost nothing dormant). If only the cluster-
Pi accuracy number fails, the gate is allowed to ship at a more
conservative `CONFIG_RV_EDGE_NOVELTY_THRESHOLD` until the cluster-
Pi-side reconciliation logic catches up.
## Open questions
1. **Does Xtensa LX7's lack of POPCNT make the table-lookup scalar
baseline fast enough at 5 Hz?** **No primary-source confirmation
performed — conjecture** (the ESP32-S3 TRM is the primary
source). At 7 bytes/sketch × 32 slots = 224 bytes of popcount
per frame, even a pessimistic 100-cycles-per-byte estimate sits
well under 200 µs at 240 MHz; Pass 2 bench resolves it.
2. **Should the IRAM ring be replaced by PSRAM-backed storage when
the board has it?** The 8 MB-flash Waveshare AMOLED ESP32-S3
ships with 8 MB PSRAM (CLAUDE.md hardware table; not a primary
source — the board datasheet is); the ring at 32 slots × 7 bytes
does not need PSRAM. A larger ring (1024 slots × 7 bytes ≈ 7 kB)
to keep a longer history would benefit from PSRAM. The default
IRAM-only sizing is the correct ship-now choice; PSRAM-backed
is an open follow-up if the cluster-Pi reconciliation logic
needs more history than 32 slots provides.
3. **Where does `gate_version: u8` come from?** Three options:
(a) Kconfig-pinned at firmware build time;
(b) NVS-stored and bumped at provision time;
(c) embedded as a build-id byte derived from the firmware
manifest. Default: option (a), Kconfig-pinned. Rationale: the
gate version is part of the firmware contract, not the per-
deployment configuration. NVS is the wrong namespace; the build-
id approach is more robust to provisioning slips but harder to
compare across deployments. The decision is reversible — the
field width is fixed at 8 bits regardless of source.
4. **Interaction with ADR-082 (pose-tracker confirmed-track
filter).** The gate could legitimately suppress the very frames
that would have promoted a Tentative track to Active in
ADR-082's three-frame minimum. The risk is asymmetric: false-
positive ghost poses are filtered by ADR-082 (correct), but
false-negative-real poses are *enabled* by the edge gate
suppressing real-but-quiet first frames. Mitigations are listed
in Consequences; the ADR commits to (a) Tentative-state-aware
threshold tuning if the validation regression on the pose
recall metric exceeds 2 percentage points, and (b) keeping
`motion_score >= 0.05` as an unconditional force-send override
inside the gate. Open Q because the right mitigation depends on
the measured regression.
## Related
- **ADR-018** (Accepted) — Binary CSI frame magic discipline. The
v7 wire format follows the same magic-bump pattern.
- **ADR-028** (Accepted) — Capability audit / witness verification.
Re-cut the bundle after this ADR ships; the SHA is *expected* to
change.
- **ADR-081** (Accepted) — 5-layer adaptive CSI mesh firmware
kernel. ADR-086 is a Layer 4 refinement.
- **ADR-082** (Accepted) — Pose-tracker confirmed-track filter.
Open Q4 above.
- **ADR-084** (Proposed) — RaBitQ similarity sensor. The cluster-
Pi reference for the same gate this ADR pushes to the edge.
- **ADR-085** (Proposed) — RaBitQ pipeline expansion. Seven
cluster-Pi-side sites; ADR-086 is the deliberately-out-of-scope
edge follow-up flagged at ADR-085 publication time.
## Related ADR slots
The user prompt that produced this ADR identified two further
follow-ups that should land as their own ADRs *if and when* the
triggering condition occurs. They are recorded here as pointer-stubs
rather than full ADRs because each is a one-paragraph commitment, not
a structured decision; opening a full ADR for either prematurely
would inflate the ledger without buying decision resolution.
### ADR-087 (prospective) — Pass-4 mesh-exchange scope clarification
ADR-084 §"Decision" lists "mesh-exchange compression" between sensor
nodes when reporting cross-cluster events as the fourth of its five
sites. The binding intent of that text is **cluster-Pi to cluster-Pi
exchange** — i.e., the ADR-066 swarm-bridge channel between peer
Cognitum Seeds — not sensor-MCU to cluster-Pi UDP traffic. The two
are different problems: cluster-to-cluster is std Rust on Linux/Mac
and reuses `BinaryQuantized` directly; sensor-to-Pi is what ADR-086
addresses. If the team later reinterprets Pass 4 as
sensor→cluster-Pi UDP compression, that would be ADR-086's twin and
should land as **ADR-087** with its own firmware release, distinct
from ADR-086's release. The clarification is one paragraph because
the only decision is "which interpretation does ADR-084's Pass 4
mean", and the answer is currently the cluster-to-cluster reading.
ADR-087 only opens if that reading is contested.
### ADR-088 (prospective) — Firmware-release coordination policy
Issues #386 and #396 (firmware-only fixes — the MGMT-only
promiscuous filter and the 50 Hz callback-rate gate) demonstrate
that the firmware can need a release independent of any cluster-Pi
ADR work. ADR-086 is itself an example: it requires a firmware
release that is not driven by ADR-084 or ADR-085, both of which are
cluster-Pi-only. Today the implicit policy is "firmware releases
when something firmware-only ships." That works but is undocumented.
**ADR-088** would formalize *when* a firmware release is required vs
deferred, with concrete examples: a Kconfig flag flip (#386 / #396)
must release; a Pi-side parser-only addition (ADR-085 Sites 17)
must not; a wire-format magic bump (ADR-086) must release and must
re-cut the witness bundle; a feature-flag-default flip on a shipped
v7 firmware should release a config bundle but not a firmware
binary. ADR-088 opens when the next firmware-only change after
ADR-086 lands and forces the decision; it is recorded here as a
slot rather than written speculatively because the actual release-
gating questions only become concrete in the presence of a real
shipping change.

View File

@ -29,7 +29,7 @@ This runs three phases:
1. **Environment checks** -- confirms Python, numpy, scipy, and proof files are present.
2. **Proof pipeline replay** -- feeds a published reference signal through the full signal processing chain (noise filtering, Hamming windowing, amplitude normalization, FFT-based Doppler extraction, power spectral density via scipy.fft) and computes a SHA-256 hash of the output.
3. **Production code integrity scan** -- scans `archive/v1/src/` for `np.random.rand` / `np.random.randn` calls in production code (test helpers are excluded).
3. **Production code integrity scan** -- scans `v1/src/` for `np.random.rand` / `np.random.randn` calls in production code (test helpers are excluded).
Exit codes:
- `0` PASS -- pipeline hash matches the published expected hash
@ -51,7 +51,7 @@ make verify-audit
If the expected hash file is missing, regenerate it:
```bash
python3 archive/v1/data/proof/verify.py --generate-hash
python3 v1/data/proof/verify.py --generate-hash
```
### Minimal dependencies for verification only
@ -63,7 +63,7 @@ pip install numpy==1.26.4 scipy==1.14.1
Or install the pinned set that guarantees hash reproducibility:
```bash
pip install -r archive/v1/requirements-lock.txt
pip install -r v1/requirements-lock.txt
```
The lock file pins: `numpy==1.26.4`, `scipy==1.14.1`, `pydantic==2.10.4`, `pydantic-settings==2.7.1`.
@ -82,7 +82,7 @@ The Python pipeline lives under `v1/` and provides the full API server, signal p
### Install (verification-only -- lightweight)
```bash
pip install -r archive/v1/requirements-lock.txt
pip install -r v1/requirements-lock.txt
```
This installs only the four packages needed for deterministic pipeline verification.
@ -98,7 +98,7 @@ This pulls in FastAPI, uvicorn, torch, OpenCV, SQLAlchemy, Redis client, and all
### Verify the pipeline
```bash
python3 archive/v1/data/proof/verify.py
python3 v1/data/proof/verify.py
```
Same as `./verify` but calls the Python script directly, skipping the bash wrapper's codebase scan phase.
@ -124,7 +124,7 @@ uvicorn v1.src.api.main:app --host 0.0.0.0 --port 8000 --reload
### Run with commodity WiFi (RSSI sensing -- no custom hardware)
The commodity sensing module (`archive/v1/src/sensing/`) extracts presence and motion features from standard Linux WiFi metrics (RSSI, noise floor, link quality) without any hardware modification. See [ADR-013](adr/ADR-013-feature-level-sensing-commodity-gear.md) for full design details.
The commodity sensing module (`v1/src/sensing/`) extracts presence and motion features from standard Linux WiFi metrics (RSSI, noise floor, link quality) without any hardware modification. See [ADR-013](adr/ADR-013-feature-level-sensing-commodity-gear.md) for full design details.
Requirements:
- Any Linux machine with a WiFi interface (laptop, Raspberry Pi, etc.)
@ -667,13 +667,13 @@ python3 -m http.server 3000 --directory ui
|------|---------|
| `./verify` | Trust kill switch -- one-command pipeline proof |
| `Makefile` | `make verify`, `make verify-verbose`, `make verify-audit` |
| `archive/v1/requirements-lock.txt` | Pinned Python deps for hash reproducibility |
| `v1/requirements-lock.txt` | Pinned Python deps for hash reproducibility |
| `requirements.txt` | Full Python deps (API server, torch, etc.) |
| `archive/v1/data/proof/verify.py` | Python verification script |
| `archive/v1/data/proof/sample_csi_data.json` | Deterministic reference signal |
| `archive/v1/data/proof/expected_features.sha256` | Published expected hash |
| `archive/v1/src/api/main.py` | FastAPI application entry point |
| `archive/v1/src/sensing/` | Commodity WiFi sensing module (RSSI) |
| `v1/data/proof/verify.py` | Python verification script |
| `v1/data/proof/sample_csi_data.json` | Deterministic reference signal |
| `v1/data/proof/expected_features.sha256` | Published expected hash |
| `v1/src/api/main.py` | FastAPI application entry point |
| `v1/src/sensing/` | Commodity WiFi sensing module (RSSI) |
| `v2/Cargo.toml` | Rust workspace root |
| `ui/viz.html` | Three.js 3D visualization |
| `Dockerfile` | Multi-stage Docker build (dev/prod/test/security) |

View File

@ -38,7 +38,7 @@ The project implements WiFi-based human pose estimation using Channel State Info
| Architecture Decision Records | Strong | 79 ADRs documented in `docs/adr/` |
| CI/CD pipelines | Strong | 8 GitHub Actions workflows (CI, CD, security scan, firmware CI, QEMU, desktop release, verify pipeline, submodules) |
| Security scanning | Strong | Dedicated `security-scan.yml` with Bandit, Semgrep, Safety; runs daily on schedule |
| Deterministic verification | Strong | SHA-256 proof pipeline (`archive/v1/data/proof/verify.py`) with witness bundles (ADR-028) |
| Deterministic verification | Strong | SHA-256 proof pipeline (`v1/data/proof/verify.py`) with witness bundles (ADR-028) |
| Code formatting | Moderate | Black/Flake8 enforced for Python in CI; no `rustfmt.toml` found for Rust |
| Type checking | Moderate | MyPy configured in CI for Python; Rust has native type safety |
| Dependency management | Strong | Workspace-level Cargo.toml with pinned versions; `requirements.txt` for Python |

View File

@ -368,7 +368,7 @@ or macro-based approach would reduce this to a fraction of the code.
| wifi-densepose-wifiscan | 75/100 | EASY | Platform-specific but well-abstracted |
| wifi-densepose-sensing-server | 32/100 | VERY DIFFICULT | God object, coupled state, async |
| wifi-densepose-wasm-edge | 55/100 | MODERATE | Repetitive but self-contained |
| archive/v1/src (Python) | 70/100 | MODERATE | Good DI, some tight coupling |
| v1/src (Python) | 70/100 | MODERATE | Good DI, some tight coupling |
| firmware (C) | 40/100 | DIFFICULT | Hardware deps, global state |
| ui/mobile (TypeScript) | 72/100 | MODERATE | Component isolation is good |

View File

@ -35,20 +35,20 @@ This security review examined all security-sensitive code across the wifi-densep
**Severity:** HIGH
**OWASP:** A07:2021 -- Identification and Authentication Failures
**Files:**
- `archive/v1/src/api/routers/stream.py:74` (WebSocket `token` query parameter)
- `archive/v1/src/middleware/auth.py:243` (fallback to `request.query_params.get("token")`)
- `archive/v1/src/api/middleware/auth.py:173` (`request.query_params.get("token")`)
- `v1/src/api/routers/stream.py:74` (WebSocket `token` query parameter)
- `v1/src/middleware/auth.py:243` (fallback to `request.query_params.get("token")`)
- `v1/src/api/middleware/auth.py:173` (`request.query_params.get("token")`)
**Description:**
JWT tokens are accepted via URL query parameters for WebSocket connections. URL parameters are logged in web server access logs, browser history, proxy logs, and HTTP Referer headers. This creates multiple credential leakage vectors.
```python
# archive/v1/src/api/routers/stream.py:74
# v1/src/api/routers/stream.py:74
token: Optional[str] = Query(None, description="Authentication token")
```
```python
# archive/v1/src/middleware/auth.py:243
# v1/src/middleware/auth.py:243
if request.url.path.startswith("/ws"):
token = request.query_params.get("token")
```
@ -66,13 +66,13 @@ if request.url.path.startswith("/ws"):
**Severity:** HIGH
**OWASP:** A05:2021 -- Security Misconfiguration
**File:** `archive/v1/src/middleware/rate_limit.py:200-206`
**File:** `v1/src/middleware/rate_limit.py:200-206`
**Description:**
The `_get_client_ip` method trusts the `X-Forwarded-For` header without any validation. An attacker can spoof this header to bypass IP-based rate limiting entirely by rotating forged IP addresses on each request.
```python
# archive/v1/src/middleware/rate_limit.py:200-206
# v1/src/middleware/rate_limit.py:200-206
def _get_client_ip(self, request: Request) -> str:
forwarded_for = request.headers.get("X-Forwarded-For")
if forwarded_for:
@ -99,17 +99,17 @@ def _get_client_ip(self, request: Request) -> str:
**Severity:** HIGH
**OWASP:** A09:2021 -- Security Logging and Monitoring Failures
**Files:**
- `archive/v1/src/api/routers/pose.py:140-141` -- `detail=f"Pose estimation failed: {str(e)}"`
- `archive/v1/src/api/routers/pose.py:176-177` -- `detail=f"Pose analysis failed: {str(e)}"`
- `archive/v1/src/api/routers/stream.py:297` -- `detail=f"Failed to get stream status: {str(e)}"`
- All exception handlers in `archive/v1/src/api/routers/stream.py` (lines 326, 351, 404, 442, 463)
- `archive/v1/src/middleware/error_handler.py:101-104` -- traceback in development mode
- `v1/src/api/routers/pose.py:140-141` -- `detail=f"Pose estimation failed: {str(e)}"`
- `v1/src/api/routers/pose.py:176-177` -- `detail=f"Pose analysis failed: {str(e)}"`
- `v1/src/api/routers/stream.py:297` -- `detail=f"Failed to get stream status: {str(e)}"`
- All exception handlers in `v1/src/api/routers/stream.py` (lines 326, 351, 404, 442, 463)
- `v1/src/middleware/error_handler.py:101-104` -- traceback in development mode
**Description:**
Multiple API endpoints directly interpolate Python exception messages into HTTP error responses. While the global error handler in `error_handler.py` correctly suppresses details in production, the per-endpoint `HTTPException` handlers bypass this and always expose `str(e)` regardless of environment.
```python
# archive/v1/src/api/routers/pose.py:140-141
# v1/src/api/routers/pose.py:140-141
raise HTTPException(
status_code=500,
detail=f"Pose estimation failed: {str(e)}"
@ -130,14 +130,14 @@ raise HTTPException(
**Severity:** MEDIUM
**OWASP:** A05:2021 -- Security Misconfiguration
**Files:**
- `archive/v1/src/config/settings.py:33-34` -- defaults: `cors_origins=["*"]`, `cors_allow_credentials=True`
- `archive/v1/src/middleware/cors.py:255-256` -- development config combines `allow_origins=["*"]` + `allow_credentials=True`
- `v1/src/config/settings.py:33-34` -- defaults: `cors_origins=["*"]`, `cors_allow_credentials=True`
- `v1/src/middleware/cors.py:255-256` -- development config combines `allow_origins=["*"]` + `allow_credentials=True`
**Description:**
The default settings allow CORS from all origins (`*`) with credentials (`allow_credentials=True`). Per the CORS specification, `Access-Control-Allow-Origin: *` cannot be used with `Access-Control-Allow-Credentials: true`. However, the `CORSMiddleware` implementation echoes the requesting origin header verbatim, effectively granting credentialed access from any origin.
```python
# archive/v1/src/middleware/cors.py:255-256 (development_config)
# v1/src/middleware/cors.py:255-256 (development_config)
"allow_origins": ["*"],
"allow_credentials": True,
```
@ -158,8 +158,8 @@ The `validate_cors_config` function at line 354 correctly flags this combination
**Severity:** MEDIUM
**OWASP:** A04:2021 -- Insecure Design
**Files:**
- `archive/v1/src/api/routers/stream.py:127-128` -- `message = await websocket.receive_text()` with no size limit
- `archive/v1/src/api/websocket/connection_manager.py` -- no `max_size` configuration
- `v1/src/api/routers/stream.py:127-128` -- `message = await websocket.receive_text()` with no size limit
- `v1/src/api/websocket/connection_manager.py` -- no `max_size` configuration
**Description:**
WebSocket endpoints accept incoming messages of arbitrary size. The `receive_text()` call at `stream.py:127` has no size limit, allowing a client to send extremely large messages that consume server memory.
@ -179,7 +179,7 @@ Additionally, the `ConnectionManager` does not enforce a maximum number of conne
**Severity:** MEDIUM
**OWASP:** A07:2021 -- Identification and Authentication Failures
**File:** `archive/v1/src/api/middleware/auth.py:246-252`
**File:** `v1/src/api/middleware/auth.py:246-252`
**Description:**
The `TokenBlacklist` class clears all blacklisted tokens every hour, regardless of their actual expiry time. This means:
@ -187,7 +187,7 @@ The `TokenBlacklist` class clears all blacklisted tokens every hour, regardless
2. Tokens revoked just before a clear cycle have nearly zero effective blacklist time.
```python
# archive/v1/src/api/middleware/auth.py:246-252
# v1/src/api/middleware/auth.py:246-252
def _cleanup_if_needed(self):
now = datetime.utcnow()
if (now - self._last_cleanup).total_seconds() > self._cleanup_interval:
@ -306,8 +306,8 @@ if (s_cfg.seed_token[0] != '\0') {
**Severity:** MEDIUM
**OWASP:** A04:2021 -- Insecure Design
**Files:**
- `archive/v1/src/api/middleware/rate_limit.py:28-29` -- `self.request_counts = defaultdict(lambda: deque())`
- `archive/v1/src/middleware/rate_limit.py:132` -- `self._sliding_windows: Dict[str, SlidingWindowCounter] = {}`
- `v1/src/api/middleware/rate_limit.py:28-29` -- `self.request_counts = defaultdict(lambda: deque())`
- `v1/src/middleware/rate_limit.py:132` -- `self._sliding_windows: Dict[str, SlidingWindowCounter] = {}`
**Description:**
Both rate limiter implementations store per-client sliding window data in unbounded in-memory dictionaries. An attacker sending requests from many spoofed IPs (see HIGH-002) can create millions of entries, each containing a `deque` of timestamps. The cleanup tasks run only periodically (every 5 minutes or on-demand) and cannot keep pace with a high-rate attack.
@ -349,8 +349,8 @@ While marked with a comment indicating it should be changed, this file is checke
**Severity:** LOW
**OWASP:** A01:2021 -- Broken Access Control
**Files:**
- `archive/v1/src/middleware/auth.py:298-299` -- `response.headers["X-User"] = user_info["username"]` and `response.headers["X-User-Roles"] = ",".join(user_info["roles"])`
- `archive/v1/src/api/middleware/auth.py:111` -- `response.headers["X-User-ID"] = request.state.user.get("id", "")`
- `v1/src/middleware/auth.py:298-299` -- `response.headers["X-User"] = user_info["username"]` and `response.headers["X-User-Roles"] = ",".join(user_info["roles"])`
- `v1/src/api/middleware/auth.py:111` -- `response.headers["X-User-ID"] = request.state.user.get("id", "")`
**Description:**
Authenticated user information (username, roles, user ID) is included in HTTP response headers. These headers are visible to any intermediary (CDN, reverse proxy, browser extensions) and in browser developer tools.
@ -380,7 +380,7 @@ Replace all instances of `datetime.utcnow()` with `datetime.now(datetime.timezon
**Severity:** LOW
**OWASP:** A02:2021 -- Cryptographic Failures
**File:** `archive/v1/src/config/settings.py:30` -- `jwt_algorithm: str = Field(default="HS256")`
**File:** `v1/src/config/settings.py:30` -- `jwt_algorithm: str = Field(default="HS256")`
**Description:**
The default JWT algorithm is HS256 (HMAC-SHA256), a symmetric algorithm. This means the same secret is used for both signing and verification, requiring the secret to be distributed to every service that needs to verify tokens. For multi-service architectures, asymmetric algorithms (RS256, ES256) are preferred.
@ -398,7 +398,7 @@ Additionally, the `jwt_algorithm` setting is not validated against a safe algori
**Severity:** LOW
**OWASP:** A07:2021 -- Identification and Authentication Failures
**File:** `archive/v1/src/middleware/auth.py:115` -- `create_user()` method
**File:** `v1/src/middleware/auth.py:115` -- `create_user()` method
**Description:**
The `create_user()` method accepts any password without minimum length, complexity, or entropy requirements. Test credentials in `v1/test_auth_rate_limit.py:21-23` demonstrate weak passwords ("admin123", "user123").
@ -460,7 +460,7 @@ This is a positive finding reflecting good security practices.
| `paramiko>=3.0.0` | LOW -- SSH library. Ensure latest minor version for CVE patches. |
| `fastapi>=0.95.0` | LOW -- Version floor is old. Pin to latest stable for security patches. |
**Recommendation:** Run `pip audit` or `safety check` against the locked dependency file (`archive/v1/requirements-lock.txt`) to identify known CVEs.
**Recommendation:** Run `pip audit` or `safety check` against the locked dependency file (`v1/requirements-lock.txt`) to identify known CVEs.
### Rust Dependencies (`Cargo.toml`)
@ -484,11 +484,11 @@ The following areas demonstrate security-conscious design:
3. **RVF build hash validation** (`firmware/esp32-csi-node/main/rvf_parser.c:126-137`): SHA-256 hash of the WASM payload is verified against the manifest before loading, preventing tampered module execution.
4. **Password hashing with bcrypt** (`archive/v1/src/middleware/auth.py:21`): Proper use of `passlib` with `bcrypt` scheme.
4. **Password hashing with bcrypt** (`v1/src/middleware/auth.py:21`): Proper use of `passlib` with `bcrypt` scheme.
5. **Protected user fields** (`archive/v1/src/middleware/auth.py:139`): `update_user()` prevents modification of `username`, `created_at`, and `hashed_password`.
5. **Protected user fields** (`v1/src/middleware/auth.py:139`): `update_user()` prevents modification of `username`, `created_at`, and `hashed_password`.
6. **Production error suppression** (`archive/v1/src/middleware/error_handler.py:214-218`): The centralized error handler correctly suppresses internal details in production mode.
6. **Production error suppression** (`v1/src/middleware/error_handler.py:214-218`): The centralized error handler correctly suppresses internal details in production mode.
7. **No hardcoded secrets in source** (verified via entropy-based search across entire repository): No API keys, passwords, or tokens found in source files (the test script placeholder at `test_auth_rate_limit.py:26` is marked as requiring replacement).
@ -502,20 +502,20 @@ The following areas demonstrate security-conscious design:
## Files Examined
### Python (archive/v1/src/)
- `archive/v1/src/middleware/auth.py` (457 lines) -- JWT auth, user management, middleware
- `archive/v1/src/middleware/rate_limit.py` (465 lines) -- Rate limiting with sliding window
- `archive/v1/src/middleware/cors.py` (375 lines) -- CORS middleware and validation
- `archive/v1/src/middleware/error_handler.py` (505 lines) -- Error handling middleware
- `archive/v1/src/api/middleware/auth.py` (303 lines) -- API-layer JWT auth
- `archive/v1/src/api/middleware/rate_limit.py` (326 lines) -- API-layer rate limiting
- `archive/v1/src/api/websocket/connection_manager.py` (461 lines) -- WebSocket manager
- `archive/v1/src/api/websocket/pose_stream.py` (384 lines) -- Pose streaming handler
- `archive/v1/src/api/routers/pose.py` (420 lines) -- Pose API endpoints
- `archive/v1/src/api/routers/stream.py` (465 lines) -- Streaming API endpoints
- `archive/v1/src/config/settings.py` (436 lines) -- Application settings
- `archive/v1/src/sensing/rssi_collector.py` (partial) -- Subprocess usage review
- `archive/v1/src/tasks/backup.py` (partial) -- Subprocess command construction
### Python (v1/src/)
- `v1/src/middleware/auth.py` (457 lines) -- JWT auth, user management, middleware
- `v1/src/middleware/rate_limit.py` (465 lines) -- Rate limiting with sliding window
- `v1/src/middleware/cors.py` (375 lines) -- CORS middleware and validation
- `v1/src/middleware/error_handler.py` (505 lines) -- Error handling middleware
- `v1/src/api/middleware/auth.py` (303 lines) -- API-layer JWT auth
- `v1/src/api/middleware/rate_limit.py` (326 lines) -- API-layer rate limiting
- `v1/src/api/websocket/connection_manager.py` (461 lines) -- WebSocket manager
- `v1/src/api/websocket/pose_stream.py` (384 lines) -- Pose streaming handler
- `v1/src/api/routers/pose.py` (420 lines) -- Pose API endpoints
- `v1/src/api/routers/stream.py` (465 lines) -- Streaming API endpoints
- `v1/src/config/settings.py` (436 lines) -- Application settings
- `v1/src/sensing/rssi_collector.py` (partial) -- Subprocess usage review
- `v1/src/tasks/backup.py` (partial) -- Subprocess command construction
- `v1/test_auth_rate_limit.py` (partial) -- Test credentials review
### Rust (v2/)

View File

@ -352,16 +352,16 @@ pub fn run(&self, csi_input: &Tensor) -> NnResult<DensePoseOutput> {
| File | Lines | Role |
|------|-------|------|
| `archive/v1/src/core/csi_processor.py` | 467 | CSI processing pipeline |
| `archive/v1/src/services/pose_service.py` | 200+ | Pose estimation service |
| `archive/v1/src/api/websocket/connection_manager.py` | 461 | WebSocket management |
| `archive/v1/src/sensing/feature_extractor.py` | 150+ | RSSI feature extraction |
| `v1/src/core/csi_processor.py` | 467 | CSI processing pipeline |
| `v1/src/services/pose_service.py` | 200+ | Pose estimation service |
| `v1/src/api/websocket/connection_manager.py` | 461 | WebSocket management |
| `v1/src/sensing/feature_extractor.py` | 150+ | RSSI feature extraction |
---
### FINDING PERF-PY01: Doppler Feature Extraction -- list() Conversion of deque [CRITICAL]
**File**: `archive/v1/src/core/csi_processor.py`
**File**: `v1/src/core/csi_processor.py`
**Lines**: 412-414
```python
@ -391,7 +391,7 @@ class CircularBuffer:
### FINDING PERF-PY02: CSI Preprocessing Creates 3 New CSIData Objects per Frame [HIGH]
**File**: `archive/v1/src/core/csi_processor.py`
**File**: `v1/src/core/csi_processor.py`
**Lines**: 118-377
The preprocessing pipeline creates a new CSIData object at each step:
@ -417,7 +417,7 @@ Each CSIData construction copies metadata via `{**csi_data.metadata, 'key': True
### FINDING PERF-PY03: Correlation Matrix -- Full np.corrcoef on Every Frame [MEDIUM]
**File**: `archive/v1/src/core/csi_processor.py`
**File**: `v1/src/core/csi_processor.py`
**Lines**: 391-395
```python
@ -436,7 +436,7 @@ def _extract_correlation_features(self, csi_data: CSIData) -> np.ndarray:
### FINDING PERF-PY04: WebSocket Broadcast -- Sequential Send to All Clients [MEDIUM]
**File**: `archive/v1/src/api/websocket/connection_manager.py`
**File**: `v1/src/api/websocket/connection_manager.py`
**Lines**: 230-264
```python
@ -461,7 +461,7 @@ results = await asyncio.gather(*tasks, return_exceptions=True)
### FINDING PERF-PY05: get_recent_history -- Copies Entire History [LOW]
**File**: `archive/v1/src/core/csi_processor.py`
**File**: `v1/src/core/csi_processor.py`
**Lines**: 284-297
```python

View File

@ -14,7 +14,7 @@ The wifi-densepose project contains **3,353 total test functions** across three
| Stack | Test Functions | Files | Frameworks |
|-------|---------------|-------|------------|
| Rust (inline + integration) | 2,658 | 292 source files + 16 integration test files | `#[test]`, Rust built-in |
| Python (archive/v1/tests/) | 491 | 30 test files | pytest, pytest-asyncio |
| Python (v1/tests/) | 491 | 30 test files | pytest, pytest-asyncio |
| Mobile (ui/mobile) | 204 | 25 test files | Jest, React Testing Library |
| **Total** | **3,353** | **363** | |
@ -26,7 +26,7 @@ The wifi-densepose project contains **3,353 total test functions** across three
---
## 1. Python Test Suite Analysis (archive/v1/tests/)
## 1. Python Test Suite Analysis (v1/tests/)
### 1.1 Test Distribution
@ -229,7 +229,7 @@ All 14 tests use `MockPoseModel` with `asyncio.sleep()` simulating inference tim
### 1.10 Test Infrastructure Quality
**Fixtures (`archive/v1/tests/fixtures/csi_data.py`):**
**Fixtures (`v1/tests/fixtures/csi_data.py`):**
Well-designed `CSIDataGenerator` class (487 lines) with:
- Multiple scenario generators (empty room, single person, multi-person)
@ -238,7 +238,7 @@ Well-designed `CSIDataGenerator` class (487 lines) with:
- Time series generation
- Validation utilities (`validate_csi_sample`)
**Mocks (`archive/v1/tests/mocks/hardware_mocks.py`):**
**Mocks (`v1/tests/mocks/hardware_mocks.py`):**
Comprehensive mock infrastructure (716 lines) including:
- `MockWiFiRouter` with realistic CSI streaming
@ -448,9 +448,9 @@ This is the best-tested service in the mobile suite.
**High maintenance cost files:**
1. `archive/v1/tests/mocks/hardware_mocks.py` (716 lines) -- Complex mock infrastructure that must evolve with the production code. Any hardware interface change requires updating this file.
1. `v1/tests/mocks/hardware_mocks.py` (716 lines) -- Complex mock infrastructure that must evolve with the production code. Any hardware interface change requires updating this file.
2. `archive/v1/tests/fixtures/csi_data.py` (487 lines) -- Rich data generation but duplicates some logic from the production `SimulatedCollector`.
2. `v1/tests/fixtures/csi_data.py` (487 lines) -- Rich data generation but duplicates some logic from the production `SimulatedCollector`.
3. The 5 CSI extractor test files collectively contain ~3,000 lines of test code for a single module. Merging to one file would reduce this to ~600 lines.
@ -468,8 +468,8 @@ This is the best-tested service in the mobile suite.
| File | Why It's Good |
|------|---------------|
| `archive/v1/tests/unit/test_sensing.py` | 45 tests with mathematical rigor, known-signal validation, domain-specific edge cases, cross-receiver agreement, band isolation. No mocks for core logic. |
| `archive/v1/tests/unit/test_esp32_binary_parser.py` | Real UDP socket testing, struct-level binary validation, ADR-018 compliance. Tests actual I/Q to amplitude/phase math. |
| `v1/tests/unit/test_sensing.py` | 45 tests with mathematical rigor, known-signal validation, domain-specific edge cases, cross-receiver agreement, band isolation. No mocks for core logic. |
| `v1/tests/unit/test_esp32_binary_parser.py` | Real UDP socket testing, struct-level binary validation, ADR-018 compliance. Tests actual I/Q to amplitude/phase math. |
| `v2/.../tests/validation_test.rs` | Physics-based validation (Doppler, phase unwrapping, spectral analysis). Tests prove algorithm correctness, not just non-failure. |
| `v2/.../tests/test_losses.rs` | Deterministic data, feature-gated, tests mathematical properties (zero loss for identical inputs, non-zero for mismatched). |
| `ui/mobile/.../utils/ringBuffer.test.ts` | Comprehensive boundary testing (NaN, Infinity, 0, negative, overflow). Tests copy semantics. |
@ -478,10 +478,10 @@ This is the best-tested service in the mobile suite.
| File | Issues |
|------|--------|
| `archive/v1/tests/performance/test_inference_speed.py` | Tests `asyncio.sleep()` accuracy, not model performance. `MockPoseModel` simulates inference with sleep. |
| `archive/v1/tests/e2e/test_healthcare_scenario.py` | Not a real E2E test -- defines its own mock classes. Test names contain stale "should_fail_initially" text. |
| `archive/v1/tests/unit/test_csi_processor_tdd.py` | 14/25 tests mock the SUT's own private methods. Tests verify mock calls, not behavior. |
| `archive/v1/tests/unit/test_phase_sanitizer_tdd.py` | 12/31 tests mock internal methods. Same anti-pattern as csi_processor_tdd. |
| `v1/tests/performance/test_inference_speed.py` | Tests `asyncio.sleep()` accuracy, not model performance. `MockPoseModel` simulates inference with sleep. |
| `v1/tests/e2e/test_healthcare_scenario.py` | Not a real E2E test -- defines its own mock classes. Test names contain stale "should_fail_initially" text. |
| `v1/tests/unit/test_csi_processor_tdd.py` | 14/25 tests mock the SUT's own private methods. Tests verify mock calls, not behavior. |
| `v1/tests/unit/test_phase_sanitizer_tdd.py` | 12/31 tests mock internal methods. Same anti-pattern as csi_processor_tdd. |
| `ui/mobile/.../components/GaugeArc.test.tsx` | All 4 tests are `expect(toJSON()).not.toBeNull()` -- smoke tests with no behavioral verification. |
---

View File

@ -31,18 +31,18 @@ The WiFi-DensePose system demonstrates strong architectural foundations with a w
### Key Findings
**Strengths:**
- Comprehensive error handling middleware with structured error responses, request IDs, and environment-aware detail levels (`archive/v1/src/middleware/error_handler.py`)
- Comprehensive error handling middleware with structured error responses, request IDs, and environment-aware detail levels (`v1/src/middleware/error_handler.py`)
- Robust WebSocket reconnection with exponential backoff and automatic simulation fallback in the mobile app (`ui/mobile/src/services/ws.service.ts`)
- Well-designed health check architecture with component-level status, readiness probes, and liveness endpoints (`archive/v1/src/api/routers/health.py`)
- Strong input validation on API models with Pydantic, including range constraints and clear field descriptions (`archive/v1/src/api/routers/pose.py`)
- Well-designed health check architecture with component-level status, readiness probes, and liveness endpoints (`v1/src/api/routers/health.py`)
- Strong input validation on API models with Pydantic, including range constraints and clear field descriptions (`v1/src/api/routers/pose.py`)
- Persistent settings with AsyncStorage in the mobile app, surviving app restarts (`ui/mobile/src/stores/settingsStore.ts`)
- Server URL validation with test-before-save workflow in mobile settings (`ui/mobile/src/screens/SettingsScreen/ServerUrlInput.tsx`)
**Critical Issues:**
- API documentation is disabled in production (`docs_url=None`, `redoc_url=None` when `is_production=True`), leaving production API consumers without discoverability (in `archive/v1/src/api/main.py` line 146-148)
- No user-facing progress indicator during calibration -- the calibration endpoint returns an estimated duration but there is no polling endpoint progress beyond percentage (`archive/v1/src/api/routers/pose.py` lines 320-361)
- Rate limit responses lack a human-readable `Retry-After` message body; the client receives a bare `"Rate limit exceeded"` string with retry information only in HTTP headers (`archive/v1/src/middleware/rate_limit.py` line 323)
- CLI `status` command uses emoji/Unicode characters that break in terminals without UTF-8 support (`archive/v1/src/commands/status.py` lines 360-474)
- API documentation is disabled in production (`docs_url=None`, `redoc_url=None` when `is_production=True`), leaving production API consumers without discoverability (in `v1/src/api/main.py` line 146-148)
- No user-facing progress indicator during calibration -- the calibration endpoint returns an estimated duration but there is no polling endpoint progress beyond percentage (`v1/src/api/routers/pose.py` lines 320-361)
- Rate limit responses lack a human-readable `Retry-After` message body; the client receives a bare `"Rate limit exceeded"` string with retry information only in HTTP headers (`v1/src/middleware/rate_limit.py` line 323)
- CLI `status` command uses emoji/Unicode characters that break in terminals without UTF-8 support (`v1/src/commands/status.py` lines 360-474)
- Mobile app `MainTabs.tsx` passes an inline arrow function as the `component` prop to `Tab.Screen` (line 130), causing unnecessary re-renders on every parent render cycle
**Top 3 Recommendations:**
@ -166,7 +166,7 @@ WS /api/v1/stream/events - Event stream
### 4.2 Error Handling (Score: 85/100)
The `ErrorHandler` class in `archive/v1/src/middleware/error_handler.py` is well-designed:
The `ErrorHandler` class in `v1/src/middleware/error_handler.py` is well-designed:
**Strengths:**
- Structured error responses with consistent format: `{ "error": { "code": "...", "message": "...", "timestamp": "...", "request_id": "..." } }`
@ -401,7 +401,7 @@ The `ServerUrlInput` component in the Settings screen provides:
**Strengths:**
- Rust workspace has 1,031+ tests with a single command: `cargo test --workspace --no-default-features`
- Deterministic proof verification via `python archive/v1/data/proof/verify.py` with SHA-256 hash checking
- Deterministic proof verification via `python v1/data/proof/verify.py` with SHA-256 hash checking
- Mobile app has comprehensive test coverage with tests for components, hooks, screens, services, stores, and utilities
- Witness bundle verification with `VERIFY.sh` providing 7/7 pass/fail attestation
@ -706,20 +706,20 @@ The `provision.py` script in `firmware/esp32-csi-node/` handles WiFi credential
This Quality Experience analysis was performed by examining source code across all touchpoints of the WiFi-DensePose system. Files analyzed include:
**API Layer (9 files):**
- `archive/v1/src/api/main.py` -- FastAPI application setup, middleware configuration, exception handlers
- `archive/v1/src/api/routers/health.py` -- Health check endpoints
- `archive/v1/src/api/routers/pose.py` -- Pose estimation endpoints
- `archive/v1/src/api/routers/stream.py` -- WebSocket streaming endpoints
- `archive/v1/src/api/websocket/connection_manager.py` -- WebSocket connection lifecycle
- `archive/v1/src/api/dependencies.py` -- Dependency injection, authentication, authorization
- `archive/v1/src/middleware/error_handler.py` -- Error handling middleware
- `archive/v1/src/middleware/rate_limit.py` -- Rate limiting middleware
- `v1/src/api/main.py` -- FastAPI application setup, middleware configuration, exception handlers
- `v1/src/api/routers/health.py` -- Health check endpoints
- `v1/src/api/routers/pose.py` -- Pose estimation endpoints
- `v1/src/api/routers/stream.py` -- WebSocket streaming endpoints
- `v1/src/api/websocket/connection_manager.py` -- WebSocket connection lifecycle
- `v1/src/api/dependencies.py` -- Dependency injection, authentication, authorization
- `v1/src/middleware/error_handler.py` -- Error handling middleware
- `v1/src/middleware/rate_limit.py` -- Rate limiting middleware
**CLI Layer (4 files):**
- `archive/v1/src/cli.py` -- Click CLI entry point
- `archive/v1/src/commands/start.py` -- Server start command
- `archive/v1/src/commands/stop.py` -- Server stop command
- `archive/v1/src/commands/status.py` -- Server status command
- `v1/src/cli.py` -- Click CLI entry point
- `v1/src/commands/start.py` -- Server start command
- `v1/src/commands/stop.py` -- Server stop command
- `v1/src/commands/status.py` -- Server status command
**Mobile Layer (15 files):**
- `ui/mobile/src/screens/LiveScreen/index.tsx` -- Live visualization screen

View File

@ -75,7 +75,7 @@ The wifi-densepose project is an ambitious WiFi-based human pose estimation syst
**Test Ideas:**
| # | Priority | Test Idea | Automation |
|---|----------|-----------|------------|
| S-08 | P0 | Run `python archive/v1/data/proof/verify.py` in CI on every PR that touches `archive/v1/src/core/` or `archive/v1/src/hardware/` to catch proof-breaking changes | CI |
| S-08 | P0 | Run `python v1/data/proof/verify.py` in CI on every PR that touches `v1/src/core/` or `v1/src/hardware/` to catch proof-breaking changes | CI |
| S-09 | P2 | Pin numpy/scipy versions in requirements.txt and confirm `verify.py --generate-hash` produces the same hash across Python 3.10, 3.11, and 3.12 | Integration |
---
@ -222,7 +222,7 @@ The Rust `Esp32CsiParser::parse_frame` takes raw bytes and returns structured `C
#### D3: Proof Data Integrity
**Finding:** The proof-of-reality system (`archive/v1/data/proof/verify.py`) is a deterministic pipeline verification tool. It feeds 1,000 synthetic CSI frames through the production CSI processor, hashes the output with SHA-256, and compares against a published hash. This is a strong engineering practice.
**Finding:** The proof-of-reality system (`v1/data/proof/verify.py`) is a deterministic pipeline verification tool. It feeds 1,000 synthetic CSI frames through the production CSI processor, hashes the output with SHA-256, and compares against a published hash. This is a strong engineering practice.
**Risk: LOW**
- The proof only exercises the Python v1 pipeline. The Rust port has no equivalent proof-of-reality check.
@ -448,7 +448,7 @@ The ESP32-S3 is the primary sensing node. The mmWave sensors are auxiliary.
**Test Ideas:**
| # | Priority | Test Idea | Automation |
|---|----------|-----------|------------|
| O-06 | P0 | Run the complete developer setup workflow from a clean Ubuntu 22.04 VM: clone, install deps, `cargo test --workspace --no-default-features`, `python archive/v1/data/proof/verify.py` -- measure total setup time and document any manual steps | Human Exploration |
| O-06 | P0 | Run the complete developer setup workflow from a clean Ubuntu 22.04 VM: clone, install deps, `cargo test --workspace --no-default-features`, `python v1/data/proof/verify.py` -- measure total setup time and document any manual steps | Human Exploration |
| O-07 | P1 | Simulate a MAT scan with 5 survivors at varying signal strengths (strong, weak, borderline) and confirm the triage classification matches expected START protocol categories | Integration |
#### O4: Extreme Use

View File

@ -287,22 +287,22 @@
| 1 | `firmware/main/wasm_runtime.c` | Firmware | 867 | **Critical** | 0.98 | WASM execution on embedded device, untested attack surface |
| 2 | `firmware/main/ota_update.c` | Firmware | 266 | **Critical** | 0.97 | OTA firmware update -- integrity/authentication critical |
| 3 | `firmware/main/swarm_bridge.c` | Firmware | 327 | **Critical** | 0.96 | Multi-node mesh networking, untested protocol |
| 4 | `archive/v1/src/services/pose_service.py` | Python | 855 | **Critical** | 0.95 | Core production path, highest complexity, no unit tests |
| 5 | `archive/v1/src/middleware/auth.py` | Python | 456 | **Critical** | 0.94 | Authentication -- security-critical, no unit tests |
| 6 | `archive/v1/src/api/websocket/connection_manager.py` | Python | 460 | **Critical** | 0.93 | WebSocket lifecycle, connection state, no tests |
| 4 | `v1/src/services/pose_service.py` | Python | 855 | **Critical** | 0.95 | Core production path, highest complexity, no unit tests |
| 5 | `v1/src/middleware/auth.py` | Python | 456 | **Critical** | 0.94 | Authentication -- security-critical, no unit tests |
| 6 | `v1/src/api/websocket/connection_manager.py` | Python | 460 | **Critical** | 0.93 | WebSocket lifecycle, connection state, no tests |
| 7 | `firmware/main/mmwave_sensor.c` | Firmware | 571 | **Critical** | 0.92 | 60GHz FMCW sensor driver, hardware-critical |
| 8 | `firmware/main/wasm_upload.c` | Firmware | 432 | **Critical** | 0.91 | OTA WASM upload, code injection risk |
| 9 | `archive/v1/src/services/orchestrator.py` | Python | 394 | **Critical** | 0.90 | Service lifecycle management, no tests |
| 10 | `archive/v1/src/database/connection.py` | Python | 639 | **Critical** | 0.89 | DB + Redis connection management, pooling |
| 11 | `archive/v1/src/middleware/error_handler.py` | Python | 504 | **High** | 0.87 | Global error handler, affects all requests |
| 12 | `archive/v1/src/tasks/monitoring.py` | Python | 771 | **High** | 0.86 | System monitoring, DB queries, async tasks |
| 13 | `archive/v1/src/services/hardware_service.py` | Python | 481 | **High** | 0.85 | Hardware abstraction, device management |
| 14 | `archive/v1/src/middleware/rate_limit.py` | Python | 464 | **High** | 0.84 | Rate limiting -- DoS protection |
| 15 | `archive/v1/src/services/health_check.py` | Python | 464 | **High** | 0.83 | Health monitoring, dependency checks |
| 16 | `archive/v1/src/tasks/backup.py` | Python | 609 | **High** | 0.82 | Data backup operations |
| 17 | `archive/v1/src/tasks/cleanup.py` | Python | 597 | **High** | 0.81 | Data retention, cleanup logic |
| 9 | `v1/src/services/orchestrator.py` | Python | 394 | **Critical** | 0.90 | Service lifecycle management, no tests |
| 10 | `v1/src/database/connection.py` | Python | 639 | **Critical** | 0.89 | DB + Redis connection management, pooling |
| 11 | `v1/src/middleware/error_handler.py` | Python | 504 | **High** | 0.87 | Global error handler, affects all requests |
| 12 | `v1/src/tasks/monitoring.py` | Python | 771 | **High** | 0.86 | System monitoring, DB queries, async tasks |
| 13 | `v1/src/services/hardware_service.py` | Python | 481 | **High** | 0.85 | Hardware abstraction, device management |
| 14 | `v1/src/middleware/rate_limit.py` | Python | 464 | **High** | 0.84 | Rate limiting -- DoS protection |
| 15 | `v1/src/services/health_check.py` | Python | 464 | **High** | 0.83 | Health monitoring, dependency checks |
| 16 | `v1/src/tasks/backup.py` | Python | 609 | **High** | 0.82 | Data backup operations |
| 17 | `v1/src/tasks/cleanup.py` | Python | 597 | **High** | 0.81 | Data retention, cleanup logic |
| 18 | `firmware/main/rvf_parser.c` | Firmware | 239 | **High** | 0.80 | Binary format parsing -- buffer overflow risk |
| 19 | `archive/v1/src/api/routers/pose.py` | Python | 419 | **High** | 0.79 | Pose API endpoint handlers |
| 19 | `v1/src/api/routers/pose.py` | Python | 419 | **High** | 0.79 | Pose API endpoint handlers |
| 20 | `mobile/hooks/useWebViewBridge.ts` | Mobile | 30 | **High** | 0.78 | Native-WebView IPC bridge |
---

View File

@ -25,9 +25,9 @@
| # | Issue | File(s) | Impact |
|---|-------|---------|--------|
| 1 | **Rate limiter bypass** -- trusts `X-Forwarded-For` without validation | `archive/v1/src/middleware/rate_limit.py:200-206` | Any client can bypass rate limits via header spoofing |
| 2 | **Exception details leaked** in HTTP responses regardless of environment | `archive/v1/src/api/routers/pose.py:140`, `stream.py:297`, +5 others | Stack traces visible to attackers |
| 3 | **WebSocket JWT in URL** -- tokens visible in logs, browser history, proxies | `archive/v1/src/api/routers/stream.py:74`, `archive/v1/src/middleware/auth.py:243` | Token exposure (CWE-598) |
| 1 | **Rate limiter bypass** -- trusts `X-Forwarded-For` without validation | `v1/src/middleware/rate_limit.py:200-206` | Any client can bypass rate limits via header spoofing |
| 2 | **Exception details leaked** in HTTP responses regardless of environment | `v1/src/api/routers/pose.py:140`, `stream.py:297`, +5 others | Stack traces visible to attackers |
| 3 | **WebSocket JWT in URL** -- tokens visible in logs, browser history, proxies | `v1/src/api/routers/stream.py:74`, `v1/src/middleware/auth.py:243` | Token exposure (CWE-598) |
| 4 | **Rust tests not in CI** -- 2,618 tests in largest codebase never run in pipeline | No `cargo test` in any GitHub Actions workflow | Regressions ship undetected |
| 5 | **WebSocket path mismatch** -- mobile app sends to wrong endpoint | `ui/mobile/src/services/ws.service.ts:104` vs `constants/websocket.ts:1` | Mobile WebSocket connections fail silently |
@ -39,16 +39,16 @@
| 7 | **O(L*V) tomography voxel scan** per frame | `ruvsense/tomography.rs:345-383` | ~10ms wasted per frame; use DDA ray march for 5-10x speedup |
| 8 | **Sequential neural inference** -- defeats GPU batching | `wifi-densepose-nn inference.rs:334-336` | 2-4x latency penalty |
| 9 | **720 `.unwrap()` calls** in Rust production code | Across entire Rust workspace | Each is a potential panic in real-time/safety-critical paths |
| 10 | **Python Doppler: 112KB alloc per frame** at 20Hz | `archive/v1/src/core/csi_processor.py:412-414` | Converts deque -> list -> numpy every frame |
| 10 | **Python Doppler: 112KB alloc per frame** at 20Hz | `v1/src/core/csi_processor.py:412-414` | Converts deque -> list -> numpy every frame |
## P2 -- Fix This Quarter (Coverage + Safety)
| # | Issue | File(s) | Impact |
|---|-------|---------|--------|
| 11 | **11/12 Python modules untested** -- only CSI extraction has unit tests | `archive/v1/src/services/`, `middleware/`, `database/`, `tasks/` | 12,280 LOC with zero unit tests |
| 11 | **11/12 Python modules untested** -- only CSI extraction has unit tests | `v1/src/services/`, `middleware/`, `database/`, `tasks/` | 12,280 LOC with zero unit tests |
| 12 | **Firmware at 19% coverage** -- WASM runtime, OTA, swarm bridge untested | `firmware/esp32-csi-node/main/wasm_runtime.c` (867 LOC) | Security-critical code with no tests |
| 13 | **MAT simulation fallback** -- disaster tool auto-falls back to simulated data | `ui/mobile/src/screens/MATScreen/index.tsx` | Risk of operators monitoring fake data during real incidents |
| 14 | **Token blacklist never consulted** during auth | `archive/v1/src/api/middleware/auth.py:246-252` | Revoked tokens remain valid |
| 14 | **Token blacklist never consulted** during auth | `v1/src/api/middleware/auth.py:246-252` | Revoked tokens remain valid |
| 15 | **50ms frame budget never benchmarked** -- no latency CI gate | No benchmark harness exists | Real-time requirement is aspirational, not verified |
## P3 -- Technical Debt

View File

@ -279,7 +279,7 @@ Uses CoreWLAN via a Swift helper binary. macOS Sonoma 14.4+ redacts real BSSIDs;
```bash
# Compile the Swift helper (once)
swiftc -O archive/v1/src/sensing/mac_wifi.swift -o mac_wifi
swiftc -O v1/src/sensing/mac_wifi.swift -o mac_wifi
# Run natively
./target/release/sensing-server --source macos --http-port 3000 --ws-port 3001 --tick-ms 500

Some files were not shown because too many files have changed in this diff Show More