docs: ADR-105 — no synthetic data in production runtime

Records the cleanup of five fake outputs the rich Docker UI exposed
when pointed at our backend without a trained pose model loaded:

  D1  derive_pose_from_sensing  → Vec::new()
  D2  pose_current              → gated on s.model_loaded
  D3  pose_stats                → drop hard-coded average_confidence 0.87
  D4  pose_zones_summary        → drop fabricated zones, report real presence
  D5  api_info.pose_estimation  → reflects s.model_loaded
  D6  generate_signal_field     → returns zero-filled grid (was procedural)

Two implementation commits already on the branch: 9aa027e9 and 30244d27.

Audit table confirms /api/v1/sensing/latest now carries only real
ESP32-derived state. Out-of-scope items (--source simulate already
disabled; --pretrain/--train synthetic fallbacks are explicit dev
flags; vital_signs already gated on real detection) are documented
so the next reader doesn't re-audit them.
This commit is contained in:
arsen 2026-05-17 11:36:30 +07:00
parent 30244d274b
commit 45c759d207
1 changed files with 184 additions and 0 deletions

View File

@ -0,0 +1,184 @@
# ADR-105 — No Synthetic Data in Production Runtime
**Status**: Accepted
**Date**: 2026-05-17
**Scope**: `v2/crates/wifi-densepose-sensing-server/src/main.rs`
(REST handlers under `/api/v1/pose/*`, `/api/v1/info`,
`derive_pose_from_sensing`, `generate_signal_field`).
## Context
After we pulled the upstream Docker UI (`ruvnet/wifi-densepose:latest`)
and pointed it at our backend via `--ui-path /tmp/wdp_ui/ui`, the
operator inspected the rich SPA and noticed several panels showing
data we have no business showing:
* **Pose dashboard rendered a 17-keypoint skeleton** even though no
DensePose model is loaded. Trace: `derive_pose_from_sensing`
`derive_single_person_pose` synthesised a geometric placeholder
with keypoint `confidence = 0.0` but plausible-looking coordinates.
* **`/api/v1/pose/stats.average_confidence` was the literal `0.87`**
hard-coded in the handler.
* **`/api/v1/pose/zones/summary` invented four zones** (`zone_1..4`)
marked `clear`, even though no zone configuration exists on this
deployment.
* **`/api/v1/info.features.pose_estimation` was permanently `true`**
regardless of whether a model was actually loaded.
* **`SignalField` (the 20×20 room-heatmap in WS payload) was
procedurally generated** by mapping subcarrier index `k` to angle
`2π·k/N` and dropping Gaussian hotspots at radius proportional to
variance. A single sensor has no directional information — the
resulting heatmap had no correspondence to where anything actually
was in the room. UI rendered a believable spatial visual that was
entirely a fiction.
All five were cosmetic noise hiding the real capability gap. Operator
asked for boots-on-the-ground honesty: surface real ESP32-derived
state and nothing else.
## Decisions
### D1 — `derive_pose_from_sensing` returns empty
The function body is now `Vec::new()`. The legacy heuristic
(`derive_single_person_pose` + bone-length tables) is unreachable
from production paths but left in the source for the day a real
trained pose model is wired in. All call sites compile unchanged
and just get an empty vector when there is no model.
### D2 — `/api/v1/pose/current` gated on `model_loaded`
```rust
let persons = if s.model_loaded {
s.latest_update.as_ref().and_then(|u| u.persons.clone()).unwrap_or_default()
} else {
Vec::new()
};
```
Response now includes `"model_loaded": false` so the UI can decide
whether to render a placeholder ("No pose model loaded") or hide the
panel entirely.
### D3 — `/api/v1/pose/stats` drops the fake confidence
The hard-coded `"average_confidence": 0.87` is removed. Only
counters that come from real frame ingest remain
(`total_detections`, `frames_processed`) plus `model_loaded`.
### D4 — `/api/v1/pose/zones/summary` reports actual zone state
```json
{ "presence": <real>, "zones_configured": 0, "zones": {} }
```
No more invented `zone_1..4`. When the operator configures real
zones (open work), they get added here.
### D5 — `/api/v1/info.features.pose_estimation` reflects reality
```rust
"pose_estimation": s.model_loaded,
```
### D6 — `generate_signal_field` returns zero-filled grid
The body is now:
```rust
let grid = 20usize;
return SignalField {
grid_size: [grid, 1, grid],
values: vec![0.0; grid * grid],
};
```
UI renders blank instead of a synthesised spatial map. This is the
truthful state until a real multistatic localizer is wired (per
ADR-008 multi-AP attention or the `MultistaticFuser` already in
state). 77 lines of procedural-art code deleted.
## Files Touched
```
v2/crates/wifi-densepose-sensing-server/src/main.rs
- fn api_info (D5)
- fn pose_current (D2)
- fn pose_stats (D3)
- fn pose_zones_summary (D4)
- fn derive_pose_from_sensing (D1)
- fn generate_signal_field (D6)
docs/adr/ADR-105-no-synthetic-data-in-production-runtime.md (this)
```
Two commits:
* `9aa027e9` — D1..D5 (REST handlers + `derive_pose_from_sensing`)
* `30244d27` — D6 (`generate_signal_field` stub)
## Verified Acceptance
`/api/v1/sensing/latest` snapshot, deployment idle:
```
signal_field grid=[20,1,20], 400 values, 0 non-zero (was: random hotspots)
pose_keypoints null (was: 17-point heuristic)
persons null (was: synthesised array)
posture null (was: heuristic string)
signal_quality_score null
enhanced_motion null
vital_signs.br_bpm null (smoothed_br ≤ 1.0)
vital_signs.hr_bpm null
— still real —
features.mean_rssi -59 dBm ✓
features.variance 8.64 ✓
classification absent / present_still / present_moving / active per ADR-101
```
`/api/v1/pose/current`:
```json
{"persons": [], "total_persons": 0, "model_loaded": false, "source": "esp32"}
```
`/api/v1/info`:
```json
{"features": {..., "pose_estimation": false, ...}}
```
## Out of scope (already correct or developer-mode)
* `--source simulate` already exits with code 2 (parallel agent change).
* `--pretrain` / `--train` synthetic-fallback paths are explicit
dev-mode CLI flags. They never touch the runtime sensing path and
are out of scope for this ADR.
* `vital_signs` was already gated: `breathing_rate_bpm = Some(_)` only
when smoothed value > 1.0 BPM; otherwise `None`. No spurious BPM
reported.
* `enhanced_motion` / `enhanced_breathing` / `bssid_count` come from
`pipeline.process(&multi_ap_frame)` which consumes real CSI. When
the multi-BSSID pipeline is inactive they are `None`. Left alone.
## Open Items
* **UI badges for "no model"**`raw.html` already renders correctly
on empty pose data; the richer Docker UI still tries to render a
skeleton from `pose_current` even when the array is empty. Need
a small UI patch: hide the pose canvas when `model_loaded == false`.
* **Real signal_field** via multistatic fusion — when ≥ 2 nodes are
active, `MultistaticFuser` can produce a physically meaningful
spatial map. ADR-104 will cover wiring it through.
* **Honest `enhanced_*` fields** — when the multi-AP pipeline runs
on a single sensor it still emits scores. Should add a
`n_aps_used` field so consumers know whether to trust them.
## References
* ADR-101 — classifier (only emits real-derived `motion_level`).
* ADR-103 — persistent baseline (only emits real-derived
baseline/threshold).
* [`docs/references/espectre-gap-analysis.md`](../references/espectre-gap-analysis.md)
— separate item list for what would replace each of the now-empty
outputs with real data.