From 3bf993275e40bab39ce08055827a5af3b13f2d91 Mon Sep 17 00:00:00 2001 From: Mathew Beane Date: Fri, 2 Oct 2026 22:07:09 -0400 Subject: [PATCH] fix(sensing-server): give the governed fuser the configured node positions EngineBridge built its StreamingEngine without ever calling set_node_positions, so the governed fuser placed every node at the origin even when --node-positions was set. Startup now passes the same keyed map the sibling multistatic_fuser gets, via EngineBridge::with_node_positions. The fuser resolves positions by node_id per frame, so nodes discovered later need no extra call. /api/v1/status (and /health/ready) now reports the map under trust.node_positions so the configuration can be checked at runtime. Co-Authored-By: claude-flow --- docs/trust-and-engine-errors.md | 6 +++ v2/crates/wifi-densepose-engine/src/lib.rs | 8 ++++ .../src/engine_bridge.rs | 42 +++++++++++++++++++ .../wifi-densepose-sensing-server/src/main.rs | 34 ++++++++++++++- 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/docs/trust-and-engine-errors.md b/docs/trust-and-engine-errors.md index 1ba6ca18e..e284334a0 100644 --- a/docs/trust-and-engine-errors.md +++ b/docs/trust-and-engine-errors.md @@ -112,6 +112,7 @@ handler (`health_ready`, `main.rs:8128,8133`) and return a `trust` block { "status": "ready", "trust": { + "node_positions": { "1": [0.5, 0.0, 1.0], "2": [4.0, 3.0, 1.0] }, "last_witness": "…64 hex chars or null…", "effective_class": "Anonymous | Restricted | …", "demoted": false, @@ -122,6 +123,11 @@ handler (`health_ready`, `main.rs:8128,8133`) and return a `trust` block } ``` +`node_positions` is the `--node-positions` map the governed fuser holds, +keyed by node id. It is `{}` when no positions are configured, and then every +node fuses at the origin. A node the map does not name also fuses at the +origin. + **This is a real, currently-shipped diagnostic surface — but it is honestly limited.** It tells you *that* errors are occurring and *that* the current class is demoted, and the total count, but not *why* for your specific run: diff --git a/v2/crates/wifi-densepose-engine/src/lib.rs b/v2/crates/wifi-densepose-engine/src/lib.rs index 3ba767297..adbd8d56e 100644 --- a/v2/crates/wifi-densepose-engine/src/lib.rs +++ b/v2/crates/wifi-densepose-engine/src/lib.rs @@ -260,6 +260,14 @@ impl StreamingEngine { self.fuser.set_node_positions_by_id(positions); } + /// Node positions the governed fuser currently holds, keyed by node id. + /// Empty until [`Self::set_node_positions`] is called, in which case every + /// node fuses at the origin. + #[must_use] + pub fn node_positions(&self) -> &std::collections::HashMap { + self.fuser.node_positions_by_id() + } + /// Activate a per-room calibration adapter (ADR-150 §3.4). From the next /// cycle on, the adapter id is part of provenance `model_version` — and /// therefore of the witness — so the exact weights shaping inference are diff --git a/v2/crates/wifi-densepose-sensing-server/src/engine_bridge.rs b/v2/crates/wifi-densepose-sensing-server/src/engine_bridge.rs index 25ab95b1c..0daa4b93b 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/engine_bridge.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/engine_bridge.rs @@ -132,6 +132,21 @@ impl EngineBridge { self.engine.set_semantic_retention(max_states); } + /// Give the governed fuser the `--node-positions` map, keyed by node id. + /// The fuser resolves each frame's position by its `node_id`, so nodes + /// discovered later pick up their entry without another call; nodes with + /// no entry fuse at the origin. + #[must_use] + pub fn with_node_positions(mut self, positions: HashMap) -> Self { + self.engine.set_node_positions(positions); + self + } + + /// Node positions the governed fuser is using, keyed by node id. + pub fn node_positions(&self) -> &HashMap { + self.engine.node_positions() + } + /// Switch the active privacy mode (operator/control-plane action). pub fn set_privacy_mode(&mut self, mode: PrivacyMode) { self.engine.set_privacy_mode(mode); @@ -299,6 +314,33 @@ mod tests { m } + fn configured_positions() -> HashMap { + [(0u8, [0.5, 0.0, 1.0]), (1u8, [4.0, 3.0, 1.0])] + .into_iter() + .collect() + } + + #[test] + fn with_node_positions_reaches_the_governed_fuser() { + let bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", None) + .with_node_positions(configured_positions()); + assert_eq!(bridge.node_positions(), &configured_positions()); + } + + #[test] + fn node_positions_survive_a_guard_config() { + let cfg = MultistaticConfig { + guard_interval_us: 200_000, + min_nodes: 1, + ..MultistaticConfig::default() + }; + let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R", Some(cfg)) + .with_node_positions(configured_positions()); + assert_eq!(bridge.node_positions(), &configured_positions()); + assert!(bridge.observe_cycle(&two_node_states(), 1_000).is_some()); + assert_eq!(bridge.node_positions(), &configured_positions()); + } + #[test] fn empty_states_produce_no_belief() { let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "living_room", "Living Room", None); diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 180ae6f53..a6675e727 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -6524,6 +6524,31 @@ async fn health_live(State(state): State) -> Json) -> serde_json::Value { + let sorted: BTreeMap = positions + .iter() + .map(|(id, p)| (id.to_string(), *p)) + .collect(); + serde_json::json!(sorted) +} + +#[cfg(test)] +mod engine_node_positions_json_tests { + use super::*; + + #[test] + fn engine_node_positions_json_is_keyed_by_id() { + let configured: HashMap = [(12, [4.0, 3.0, 1.0]), (11, [0.5, 0.0, 1.0])] + .into_iter() + .collect(); + assert_eq!( + engine_node_positions_json(&configured), + serde_json::json!({"11": [0.5, 0.0, 1.0], "12": [4.0, 3.0, 1.0]}) + ); + } +} + /// Lowercase hex of a 32-byte witness for JSON exposure. fn witness_hex(w: [u8; 32]) -> String { use std::fmt::Write; @@ -6545,6 +6570,8 @@ async fn health_ready(State(state): State) -> Json = None; let mut node_positions_config: HashMap = HashMap::new(); + // The same map for the governed fuser behind `engine_bridge`, which + // otherwise places every node at the origin. + let mut engine_bridge_node_positions: HashMap = HashMap::new(); let state: SharedState = Arc::new(RwLock::new(AppStateInner { latest_update: None, rssi_history: VecDeque::new(), @@ -12221,6 +12251,7 @@ async fn main() { // no `node_id:` prefix takes its list index as its id), so // an existing `--node-positions` string keeps its meaning. fuser.set_node_positions_by_id(node_positions_config.clone()); + engine_bridge_node_positions = node_positions_config.clone(); } } engine_bridge_multistatic_cfg = Some(MultistaticConfig { @@ -12236,7 +12267,8 @@ async fn main() { "default", "Default Room", engine_bridge_multistatic_cfg, - ), + ) + .with_node_positions(engine_bridge_node_positions), field_model: bootstrap_field_model, installation_id: args.installation_id.clone(), bootstrap_baseline: bootstrap_metadata.clone(),