6.9 KiB
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_posesynthesised a geometric placeholder with keypointconfidence = 0.0but plausible-looking coordinates. /api/v1/pose/stats.average_confidencewas the literal0.87hard-coded in the handler./api/v1/pose/zones/summaryinvented four zones (zone_1..4) markedclear, even though no zone configuration exists on this deployment./api/v1/info.features.pose_estimationwas permanentlytrueregardless of whether a model was actually loaded.SignalField(the 20×20 room-heatmap in WS payload) was procedurally generated by mapping subcarrier indexkto angle2π·k/Nand 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
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
{ "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
"pose_estimation": s.model_loaded,
D6 — generate_signal_field returns zero-filled grid
The body is now:
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_fieldstub)
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:
{"persons": [], "total_persons": 0, "model_loaded": false, "source": "esp32"}
/api/v1/info:
{"features": {..., "pose_estimation": false, ...}}
Out of scope (already correct or developer-mode)
--source simulatealready exits with code 2 (parallel agent change).--pretrain/--trainsynthetic-fallback paths are explicit dev-mode CLI flags. They never touch the runtime sensing path and are out of scope for this ADR.vital_signswas already gated:breathing_rate_bpm = Some(_)only when smoothed value > 1.0 BPM; otherwiseNone. No spurious BPM reported.enhanced_motion/enhanced_breathing/bssid_countcome frompipeline.process(&multi_ap_frame)which consumes real CSI. When the multi-BSSID pipeline is inactive they areNone. Left alone.
Open Items
- UI badges for "no model" —
raw.htmlalready renders correctly on empty pose data; the richer Docker UI still tries to render a skeleton frompose_currenteven when the array is empty. Need a small UI patch: hide the pose canvas whenmodel_loaded == false.
Closed
- Honest
enhanced_*fields — bothenhanced_motionandenhanced_breathingnow carry a uniformn_aps_used: u8field alongside the legacycontributing_bssids/bssid_countcounts. Consumers can gate onn_aps_used >= 2before trusting a multi-AP enhancement. (commit598a4b2f) - Real signal_field via multistatic fusion — shipped in ADR-112.
When ≥ 2 ESP32 nodes are active,
MultistaticFuseroutput drives a coverage × activity 20×20 heatmap (isotropic Gaussian per node position, gated bycv²(fused_amplitude) × cross_node_coherence). Single-sensor / fusion-fail paths still return ADR-105's zero grid. Map is honestly framed as coverage, not target position.
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— separate item list for what would replace each of the now-empty outputs with real data.