From 2e4461d64d56f8be323c87e5e384eef34942f7cb Mon Sep 17 00:00:00 2001 From: ruv Date: Thu, 11 Jun 2026 22:41:21 -0400 Subject: [PATCH 01/11] release: bump 9 crates changed in the beyond-SOTA sweep for crates.io vitals/wifiscan/hardware/nn 0.3.0->0.3.1, ruvector 0.3.1->0.3.2, signal 0.3.2->0.3.3, train 0.3.1->0.3.2, mat 0.3.0->0.3.1, sensing-server 0.3.1->0.3.2. Co-Authored-By: claude-flow --- v2/crates/wifi-densepose-hardware/Cargo.toml | 2 +- v2/crates/wifi-densepose-mat/Cargo.toml | 2 +- v2/crates/wifi-densepose-nn/Cargo.toml | 2 +- v2/crates/wifi-densepose-ruvector/Cargo.toml | 2 +- v2/crates/wifi-densepose-sensing-server/Cargo.toml | 2 +- v2/crates/wifi-densepose-signal/Cargo.toml | 2 +- v2/crates/wifi-densepose-train/Cargo.toml | 2 +- v2/crates/wifi-densepose-vitals/Cargo.toml | 2 +- v2/crates/wifi-densepose-wifiscan/Cargo.toml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/v2/crates/wifi-densepose-hardware/Cargo.toml b/v2/crates/wifi-densepose-hardware/Cargo.toml index 6af0ac93..694ea372 100644 --- a/v2/crates/wifi-densepose-hardware/Cargo.toml +++ b/v2/crates/wifi-densepose-hardware/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wifi-densepose-hardware" -version.workspace = true +version = "0.3.1" edition.workspace = true description = "Hardware interface abstractions for WiFi CSI sensors (ESP32, Intel 5300, Atheros)" license = "MIT OR Apache-2.0" diff --git a/v2/crates/wifi-densepose-mat/Cargo.toml b/v2/crates/wifi-densepose-mat/Cargo.toml index e11a331e..937a3d77 100644 --- a/v2/crates/wifi-densepose-mat/Cargo.toml +++ b/v2/crates/wifi-densepose-mat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wifi-densepose-mat" -version = "0.3.0" +version = "0.3.1" edition = "2021" authors = ["rUv ", "WiFi-DensePose Contributors"] description = "Mass Casualty Assessment Tool - WiFi-based disaster survivor detection" diff --git a/v2/crates/wifi-densepose-nn/Cargo.toml b/v2/crates/wifi-densepose-nn/Cargo.toml index fe221ea1..54b0bfd5 100644 --- a/v2/crates/wifi-densepose-nn/Cargo.toml +++ b/v2/crates/wifi-densepose-nn/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wifi-densepose-nn" -version.workspace = true +version = "0.3.1" edition.workspace = true authors.workspace = true license.workspace = true diff --git a/v2/crates/wifi-densepose-ruvector/Cargo.toml b/v2/crates/wifi-densepose-ruvector/Cargo.toml index 34ba107a..5b3a1b0c 100644 --- a/v2/crates/wifi-densepose-ruvector/Cargo.toml +++ b/v2/crates/wifi-densepose-ruvector/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wifi-densepose-ruvector" -version = "0.3.1" # ADR-138: ClockQualityGate / clock-quality coherence gate +version = "0.3.2" edition.workspace = true authors.workspace = true license.workspace = true diff --git a/v2/crates/wifi-densepose-sensing-server/Cargo.toml b/v2/crates/wifi-densepose-sensing-server/Cargo.toml index 2ca98f3e..266704a8 100644 --- a/v2/crates/wifi-densepose-sensing-server/Cargo.toml +++ b/v2/crates/wifi-densepose-sensing-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wifi-densepose-sensing-server" -version = "0.3.1" +version = "0.3.2" edition.workspace = true description = "Lightweight Axum server for WiFi sensing UI with RuVector signal processing" license.workspace = true diff --git a/v2/crates/wifi-densepose-signal/Cargo.toml b/v2/crates/wifi-densepose-signal/Cargo.toml index 147568ad..9ed130d1 100644 --- a/v2/crates/wifi-densepose-signal/Cargo.toml +++ b/v2/crates/wifi-densepose-signal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wifi-densepose-signal" -version = "0.3.2" # ADR-137/138/142/143: fuse_scored_calibrated, ArrayCoordinator, evolution, rf_slam, calibration apply +version = "0.3.3" edition.workspace = true description = "WiFi CSI signal processing for DensePose estimation" license.workspace = true diff --git a/v2/crates/wifi-densepose-train/Cargo.toml b/v2/crates/wifi-densepose-train/Cargo.toml index 6c677add..133026f5 100644 --- a/v2/crates/wifi-densepose-train/Cargo.toml +++ b/v2/crates/wifi-densepose-train/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wifi-densepose-train" -version = "0.3.1" +version = "0.3.2" edition = "2021" authors = ["rUv ", "WiFi-DensePose Contributors"] license = "MIT OR Apache-2.0" diff --git a/v2/crates/wifi-densepose-vitals/Cargo.toml b/v2/crates/wifi-densepose-vitals/Cargo.toml index ccf177e9..756c1471 100644 --- a/v2/crates/wifi-densepose-vitals/Cargo.toml +++ b/v2/crates/wifi-densepose-vitals/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wifi-densepose-vitals" -version.workspace = true +version = "0.3.1" edition.workspace = true description = "ESP32 CSI-grade vital sign extraction (ADR-021): heart rate and respiratory rate from WiFi Channel State Information" license.workspace = true diff --git a/v2/crates/wifi-densepose-wifiscan/Cargo.toml b/v2/crates/wifi-densepose-wifiscan/Cargo.toml index f10c8c24..5d7c6aba 100644 --- a/v2/crates/wifi-densepose-wifiscan/Cargo.toml +++ b/v2/crates/wifi-densepose-wifiscan/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wifi-densepose-wifiscan" -version.workspace = true +version = "0.3.1" edition.workspace = true description = "Multi-BSSID WiFi scanning domain layer for enhanced Windows WiFi DensePose sensing (ADR-022)" license.workspace = true From 98bf8c4726d05e37c1e09ed35e68ccf06ff0b25b Mon Sep 17 00:00:00 2001 From: ruv Date: Thu, 11 Jun 2026 23:10:00 -0400 Subject: [PATCH 02/11] fix(cog-pose-estimation): emit frames under default config (ADR-159 A1) pose_v1 has no confidence head, so infer() emits a constant 0.185 per frame. The config default_min_confidence was 0.3 and the runtime gates on confidence >= min_confidence, so a default install silently emitted ZERO pose.frame events while health reported healthy. - Add inference::MODEL_TYPICAL_CONFIDENCE (0.185, the validation PCK@50) as the single published per-frame confidence. - Pin default_min_confidence() to MODEL_TYPICAL_CONFIDENCE so a default install clears its own gate and emits. - Warn at run.started when min_confidence exceeds the model typical confidence (disclosed, not silent); document the trade-off in the config field, the JSON schema, and inference.rs. Failing-on-old test: default_config_emits_frames_with_real_model (with old 0.3 it panics: "default install would emit zero pose.frame events"). Co-Authored-By: claude-flow --- .../cog/config.schema.json | 4 +- v2/crates/cog-pose-estimation/src/config.rs | 11 +++- .../cog-pose-estimation/src/inference.rs | 21 ++++++-- v2/crates/cog-pose-estimation/src/main.rs | 12 +++++ v2/crates/cog-pose-estimation/tests/smoke.rs | 53 +++++++++++++++++++ 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/v2/crates/cog-pose-estimation/cog/config.schema.json b/v2/crates/cog-pose-estimation/cog/config.schema.json index 023ebaec..a5ebf85b 100644 --- a/v2/crates/cog-pose-estimation/cog/config.schema.json +++ b/v2/crates/cog-pose-estimation/cog/config.schema.json @@ -26,8 +26,8 @@ "type": "number", "minimum": 0, "maximum": 1, - "default": 0.3, - "description": "Drop frames where the inferred pose confidence is below this threshold." + "default": 0.185, + "description": "Drop frames where the inferred pose confidence is below this threshold. pose_v1 has no confidence head, so every frame carries the model's published per-frame confidence (0.185 = validation PCK@50); the default is pinned to that value so a default install actually emits frames. Raising it above 0.185 suppresses ALL pose.frame events (the runtime warns when this happens)." } }, "required": ["model_path"] diff --git a/v2/crates/cog-pose-estimation/src/config.rs b/v2/crates/cog-pose-estimation/src/config.rs index d5976f9c..605cc5ae 100644 --- a/v2/crates/cog-pose-estimation/src/config.rs +++ b/v2/crates/cog-pose-estimation/src/config.rs @@ -23,6 +23,13 @@ pub struct CogConfig { pub poll_ms: u64, /// Confidence threshold below which a frame's keypoints are not emitted. + /// + /// Defaults to [`crate::inference::MODEL_TYPICAL_CONFIDENCE`] (0.185) — the + /// model's published per-frame confidence. `pose_v1` has no confidence head, + /// so every frame carries this same value; a default above it would silently + /// suppress *all* `pose.frame` events while health still reports healthy. + /// The runtime warns at `run.started` if this is raised above the model's + /// typical confidence rather than dropping frames quietly. #[serde(default = "default_min_confidence")] pub min_confidence: f32, } @@ -36,7 +43,9 @@ fn default_poll_ms() -> u64 { } fn default_min_confidence() -> f32 { - 0.3 + // Pinned to the model's typical/published confidence so a default install + // actually emits frames. See `min_confidence` doc and ADR-159 §A1. + crate::inference::MODEL_TYPICAL_CONFIDENCE } impl CogConfig { diff --git a/v2/crates/cog-pose-estimation/src/inference.rs b/v2/crates/cog-pose-estimation/src/inference.rs index fc675e2c..314d75e5 100644 --- a/v2/crates/cog-pose-estimation/src/inference.rs +++ b/v2/crates/cog-pose-estimation/src/inference.rs @@ -27,6 +27,16 @@ pub const INPUT_SUBCARRIERS: usize = 56; pub const INPUT_TIMESTEPS: usize = 20; pub const OUTPUT_KEYPOINTS: usize = 17; +/// The model's typical self-reported confidence. `pose_v1` has **no confidence +/// head** (the head emits 34 keypoint coordinates only), so per-frame confidence +/// is not available from the network. This is the validation-set PCK@50 (18.5%) +/// the training run reported, used as the published per-frame confidence floor. +/// +/// Surfaced as a public constant so the runtime can warn when a configured +/// `min_confidence` threshold exceeds it — otherwise a default install would +/// silently emit zero `pose.frame` events while health reports healthy. +pub const MODEL_TYPICAL_CONFIDENCE: f32 = 0.185; + #[derive(Debug, Clone)] pub struct CsiWindow { pub data: Vec, // length INPUT_SUBCARRIERS * INPUT_TIMESTEPS @@ -283,12 +293,15 @@ impl InferenceEngine { let out = model.net.forward(&t)?; // [1, 34] let flat: Vec = out.flatten_all()?.to_vec1()?; // Confidence from pose_v1 is a published constant rather than per-frame — - // the trained model didn't emit a confidence head. Use the validation-set - // PCK@50 (18.5%) as the published self-reported confidence so downstream - // consumers can gate display decisions on it. + // the trained model has no confidence head (the head emits 34 keypoint + // coordinates only), so a real per-frame value is genuinely unavailable. + // We surface the validation-set PCK@50 (`MODEL_TYPICAL_CONFIDENCE`) as the + // honest self-reported confidence. The runtime's `min_confidence` default + // is pinned at or below this so a default install actually emits frames + // (and warns if an operator raises the threshold above the model's reach). Ok(PoseOutput { keypoints: flat, - confidence: 0.185, + confidence: MODEL_TYPICAL_CONFIDENCE, }) } } diff --git a/v2/crates/cog-pose-estimation/src/main.rs b/v2/crates/cog-pose-estimation/src/main.rs index 99e12b85..669eedfd 100644 --- a/v2/crates/cog-pose-estimation/src/main.rs +++ b/v2/crates/cog-pose-estimation/src/main.rs @@ -113,6 +113,18 @@ fn cmd_run( let cfg = CogConfig::load(&config_path)?; emit_event(&Event::run_started(COG_ID, &cfg)); + // Disclosure: pose_v1 has no confidence head, so every frame carries the + // same `MODEL_TYPICAL_CONFIDENCE`. A `min_confidence` above that silently + // suppresses *all* pose.frame events. Warn loudly rather than drop quietly. + if cfg.min_confidence > cog_pose_estimation::inference::MODEL_TYPICAL_CONFIDENCE { + tracing::warn!( + min_confidence = cfg.min_confidence, + model_typical_confidence = cog_pose_estimation::inference::MODEL_TYPICAL_CONFIDENCE, + "configured min_confidence exceeds the model's typical confidence; \ + no pose.frame events will be emitted until this is lowered" + ); + } + let engine = InferenceEngine::with_adapter(adapter.as_deref())?; if engine.is_calibrated() { tracing::info!("per-room calibration adapter loaded"); diff --git a/v2/crates/cog-pose-estimation/tests/smoke.rs b/v2/crates/cog-pose-estimation/tests/smoke.rs index 395e51c3..c79a559c 100644 --- a/v2/crates/cog-pose-estimation/tests/smoke.rs +++ b/v2/crates/cog-pose-estimation/tests/smoke.rs @@ -172,3 +172,56 @@ fn manifest_roundtrips() { assert_eq!(back.id, "pose-estimation"); assert_eq!(back.version, "0.0.1"); } + +/// ADR-159 §A1 — the default-config min_confidence threshold must not silently +/// suppress every `pose.frame`. With the old `default_min_confidence()=0.3` and +/// the model's per-frame confidence pinned at 0.185, the runtime gate +/// (`out.confidence >= cfg.min_confidence`) never fired, so a default install +/// emitted ZERO frames while health reported healthy. This asserts the default +/// install actually clears its own gate. +#[test] +fn default_config_emits_frames_with_real_model() { + use cog_pose_estimation::config::CogConfig; + + // A minimal config (only the required model_path) exercises every + // `#[serde(default)]` path — i.e. the *default* install threshold. + let cfg: CogConfig = + serde_json::from_value(serde_json::json!({ "model_path": "pose_v1.safetensors" })) + .expect("default config parse"); + + // Real model when present; stub otherwise. Either way the per-frame + // confidence the runtime gates on must clear the default threshold, + // OR (stub case) the gate must still let the model's typical confidence + // through. We assert against the same value the runtime emits. + let weights = std::path::Path::new("cog/artifacts/pose_v1.safetensors"); + let engine = if weights.exists() { + InferenceEngine::with_weights(Some(weights)).expect("load real weights") + } else { + InferenceEngine::new().expect("engine init") + }; + + // Core regression assertion (fails on the old `default_min_confidence()=0.3`): + // the default threshold must not exceed the model's published per-frame + // confidence (0.185), which is the exact value `infer()` emits for the real + // model. With 0.3 the runtime gate `out.confidence >= min_confidence` never + // fired → zero pose.frame events on a default install. + assert!( + cfg.min_confidence <= cog_pose_estimation::inference::MODEL_TYPICAL_CONFIDENCE, + "default min_confidence {} exceeds model typical confidence {} — \ + a default install would emit zero pose.frame events", + cfg.min_confidence, + cog_pose_estimation::inference::MODEL_TYPICAL_CONFIDENCE + ); + + // End-to-end: when the real model is loaded, the value it actually emits + // must clear the default gate (i.e. the runtime would emit this frame). + if engine.backend().starts_with("candle-") { + let out = engine.infer(&SyntheticInput.as_window()).expect("infer"); + assert!( + out.confidence >= cfg.min_confidence, + "default install must emit: infer confidence {} < default min_confidence {}", + out.confidence, + cfg.min_confidence + ); + } +} From 240021692018972cb4e8d5c131ed6b40b7b6cb79 Mon Sep 17 00:00:00 2001 From: ruv Date: Thu, 11 Jun 2026 23:10:01 -0400 Subject: [PATCH 03/11] fix(cog-person-count): flag untrained-class counts low_confidence (ADR-159 A2) The count head has 8 classes but count_train_results.json only has support for classes 0/1 (presence, not multi-occupant counting). An argmax on classes 2..=7 is out-of-distribution, yet the cog emitted it as a confident headcount and the crate billed itself a "multi-person counter". - Add MAX_TRAINED_CLASS=1, CountPrediction::is_low_confidence() and clamped_count(). - person.count events now carry low_confidence + raw_count, downgrade to level "warn" when OOD, and clamp the reported count to the trained range (no fabricated headcount). - run.started discloses count_max_trained_class / count_classes. - Cargo.toml description: "multi-person counter" -> "presence detector + (data-gated) person count". Multi-occupant accuracy stays DATA-GATED (not fabricated). Failing-on-old test: untrained_class_argmax_is_flagged_low_confidence. Co-Authored-By: claude-flow --- v2/crates/cog-person-count/Cargo.toml | 2 +- v2/crates/cog-person-count/src/inference.rs | 28 ++++++++++++ v2/crates/cog-person-count/src/publisher.rs | 19 ++++++++- v2/crates/cog-person-count/tests/smoke.rs | 47 ++++++++++++++++++++- 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/v2/crates/cog-person-count/Cargo.toml b/v2/crates/cog-person-count/Cargo.toml index 64526f60..2b3a65ea 100644 --- a/v2/crates/cog-person-count/Cargo.toml +++ b/v2/crates/cog-person-count/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true authors.workspace = true license.workspace = true repository.workspace = true -description = "Cognitum Cog: learned multi-person counter from WiFi CSI (ADR-103). Replaces the PR #491 slot heuristic with a Candle-based count head + Stoer-Wagner multi-node fusion." +description = "Cognitum Cog: WiFi-CSI presence detector + (data-gated) person count (ADR-103). Candle-based head trained on classes 0/1 (presence); the 8-class count head ships but counts above the trained range are flagged low_confidence. Stoer-Wagner multi-node fusion." [[bin]] name = "cog-person-count" diff --git a/v2/crates/cog-person-count/src/inference.rs b/v2/crates/cog-person-count/src/inference.rs index 96f82e89..fc810398 100644 --- a/v2/crates/cog-person-count/src/inference.rs +++ b/v2/crates/cog-person-count/src/inference.rs @@ -24,6 +24,17 @@ pub const INPUT_TIMESTEPS: usize = 20; /// Count classification over {0, 1, ..., 7} persons. pub const COUNT_CLASSES: usize = 8; +/// Highest class the shipped `count_v1` weights were actually **trained** on. +/// +/// The count head has 8 logits, but `count_train_results.json` only has support +/// for classes 0 and 1 (`per_class_accuracy` keys are `"0"` and `"1"`). The model +/// is a presence detector (0 vs ≥1 person), **not** a calibrated multi-occupant +/// counter. An argmax landing on classes 2..=7 is out-of-distribution: the logits +/// there were never supervised against labelled data. We flag such outputs +/// `low_confidence` so downstream consumers don't trust a fabricated headcount. +/// (Multi-occupant *accuracy* is DATA-GATED — not fabricated here.) +pub const MAX_TRAINED_CLASS: usize = 1; + #[derive(Debug, Clone)] pub struct CsiWindow { pub data: Vec, @@ -45,6 +56,23 @@ impl CountPrediction { self.probs.iter().all(|v| v.is_finite()) && self.confidence.is_finite() } + /// True when the maximum-likelihood class is beyond what the shipped weights + /// were trained on ([`MAX_TRAINED_CLASS`]). Such a prediction is out-of- + /// distribution — the count head's logits for classes 2..=7 were never + /// supervised, so the headcount is not trustworthy. Surfaced as the + /// `low_confidence` field on the `person.count` event (honest-clip pattern). + pub fn is_low_confidence(&self) -> bool { + self.argmax() > MAX_TRAINED_CLASS + } + + /// Argmax clamped to [`MAX_TRAINED_CLASS`]. When the raw argmax is an + /// untrained class we clamp the *reported* count to the highest trained + /// class rather than emit a fabricated multi-occupant headcount. The raw + /// distribution is still available in `probs` for diagnostics. + pub fn clamped_count(&self) -> usize { + self.argmax().min(MAX_TRAINED_CLASS) + } + /// Maximum-likelihood class. pub fn argmax(&self) -> usize { let mut best_i = 0; diff --git a/v2/crates/cog-person-count/src/publisher.rs b/v2/crates/cog-person-count/src/publisher.rs index 2287a775..677788af 100644 --- a/v2/crates/cog-person-count/src/publisher.rs +++ b/v2/crates/cog-person-count/src/publisher.rs @@ -45,20 +45,35 @@ pub fn run_started(cog_id: &str, sensing_url: &str, poll_ms: u64, model_path: &s "sensing_url": sensing_url, "poll_ms": poll_ms, "model_path": model_path, + // Honest disclosure: the count head has 8 classes but the shipped + // weights were only trained on classes 0..=MAX_TRAINED_CLASS + // (presence, not multi-occupant counting). Counts above this are + // flagged `low_confidence` on each person.count event. + "count_max_trained_class": crate::inference::MAX_TRAINED_CLASS, + "count_classes": crate::inference::COUNT_CLASSES, }), }); } pub fn person_count(tick: u64, fused: &CountPrediction, n_nodes: usize) { let (lo, hi) = fused.p95_range(); + let low_confidence = fused.is_low_confidence(); emit_event(&Event { ts: now_secs(), - level: "info", + // An out-of-distribution count (argmax beyond the trained classes) is + // a warning, not a clean info reading. + level: if low_confidence { "warn" } else { "info" }, event: "person.count", fields: json!({ "tick": tick, - "count": fused.argmax(), + // Reported count is clamped to the trained range — we never emit a + // fabricated multi-occupant headcount the weights can't back. + "count": fused.clamped_count(), + // Raw argmax kept for diagnostics/audit. + "raw_count": fused.argmax(), "confidence": fused.confidence, + // True when argmax > MAX_TRAINED_CLASS (untrained class). + "low_confidence": low_confidence, "count_p95_low": lo, "count_p95_high": hi, "n_nodes": n_nodes, diff --git a/v2/crates/cog-person-count/tests/smoke.rs b/v2/crates/cog-person-count/tests/smoke.rs index 433c7155..2447cca1 100644 --- a/v2/crates/cog-person-count/tests/smoke.rs +++ b/v2/crates/cog-person-count/tests/smoke.rs @@ -4,7 +4,7 @@ use cog_person_count::{ fusion::{fuse_confidence_weighted, fuse_with_mincut_clip}, inference::{ CountPrediction, CsiWindow, InferenceEngine, SyntheticInput, COUNT_CLASSES, - INPUT_SUBCARRIERS, INPUT_TIMESTEPS, + INPUT_SUBCARRIERS, INPUT_TIMESTEPS, MAX_TRAINED_CLASS, }, }; @@ -83,6 +83,51 @@ fn fusion_passes_through_single_node() { assert!((out.confidence - 0.6).abs() < 1e-6); } +/// ADR-159 §A2 — the 8-class count head ships, but the weights were only +/// trained on classes 0/1 (presence). A prediction whose argmax lands on an +/// UNTRAINED class (2..=7) must be flagged `low_confidence` and the reported +/// count clamped to the trained range, so we never emit a fabricated +/// multi-occupant headcount. Fails on old code (no such flag/clamp existed). +#[test] +fn untrained_class_argmax_is_flagged_low_confidence() { + // Sanity: the trained ceiling is below the head width. + assert!(MAX_TRAINED_CLASS < COUNT_CLASSES - 1); + + // Mass on an untrained class (5 persons) — out-of-distribution. + let mut probs = [0.0_f32; COUNT_CLASSES]; + probs[5] = 0.9; + probs[1] = 0.1; + let oodp = CountPrediction { + probs, + confidence: 0.95, // even a "confident" softmax must be flagged + }; + assert_eq!(oodp.argmax(), 5); + assert!( + oodp.is_low_confidence(), + "argmax beyond MAX_TRAINED_CLASS must be flagged low_confidence" + ); + assert_eq!( + oodp.clamped_count(), + MAX_TRAINED_CLASS, + "reported count must clamp to the trained ceiling, not fabricate a headcount" + ); + + // A trained-range prediction (1 person) is NOT flagged. + let mut probs2 = [0.0_f32; COUNT_CLASSES]; + probs2[1] = 0.8; + probs2[0] = 0.2; + let inp = CountPrediction { + probs: probs2, + confidence: 0.8, + }; + assert_eq!(inp.argmax(), 1); + assert!( + !inp.is_low_confidence(), + "a trained-range count must not be flagged" + ); + assert_eq!(inp.clamped_count(), 1); +} + #[test] fn mincut_clip_with_high_cap_is_noop() { let mut probs = [0.0_f32; COUNT_CLASSES]; From 6b5fd3cf253a79fa306f4568808a15706e753a75 Mon Sep 17 00:00:00 2001 From: ruv Date: Thu, 11 Jun 2026 23:10:01 -0400 Subject: [PATCH 04/11] fix(cog-person-count): emit real signed manifest from CLI (ADR-159 A4) cmd_manifest emitted a null skeleton (binary_sha256: null) while the real signed manifest existed on disk at cog/artifacts/manifests//manifest.json. - New manifest module include_str!-embeds the real signed manifests (x86_64 + arm), selected by build target arch. - cmd_manifest parses-then-emits the embedded signed manifest, mirroring cog-pose-estimation manifest_roundtrips. CLI now reports the real binary_sha256, weights_sha256, Ed25519 signature, and honest build_metadata (training_class1_accuracy = 0.343). Failing-on-old test: manifest::tests::embedded_manifest_has_non_null_binary_sha256 (+ embedded_manifest_is_signed, embedded_manifest_id_matches_cog). Verified end-to-end: cog-person-count manifest -> non-null sha256. Co-Authored-By: claude-flow --- v2/crates/cog-person-count/src/lib.rs | 1 + v2/crates/cog-person-count/src/main.rs | 19 ++---- v2/crates/cog-person-count/src/manifest.rs | 77 ++++++++++++++++++++++ 3 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 v2/crates/cog-person-count/src/manifest.rs diff --git a/v2/crates/cog-person-count/src/lib.rs b/v2/crates/cog-person-count/src/lib.rs index 168696cc..375773d3 100644 --- a/v2/crates/cog-person-count/src/lib.rs +++ b/v2/crates/cog-person-count/src/lib.rs @@ -9,6 +9,7 @@ pub mod fusion; pub mod inference; +pub mod manifest; pub mod publisher; pub mod runtime; diff --git a/v2/crates/cog-person-count/src/main.rs b/v2/crates/cog-person-count/src/main.rs index e5697440..5ff76046 100644 --- a/v2/crates/cog-person-count/src/main.rs +++ b/v2/crates/cog-person-count/src/main.rs @@ -12,7 +12,6 @@ use cog_person_count::{ publisher, COG_ID, COG_VERSION, }; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; use std::path::PathBuf; #[derive(Parser)] @@ -83,19 +82,11 @@ fn cmd_version() -> Result<(), Box> { } fn cmd_manifest() -> Result<(), Box> { - println!( - "{}", - serde_json::to_string_pretty(&json!({ - "id": COG_ID, - "version": COG_VERSION, - "binary_url": Value::Null, - "binary_bytes": Value::Null, - "binary_sha256": Value::Null, - "binary_signature": Value::Null, - "installed_at": Value::Null, - "status": Value::Null, - }))? - ); + // Emit the real, signed manifest embedded at compile time (ADR-159 §A4) — + // not the old hollow null skeleton. Parse-then-emit so a malformed embedded + // artifact fails loudly and the output is canonical JSON. + let spec = cog_person_count::manifest::embedded_manifest_value()?; + println!("{}", serde_json::to_string_pretty(&spec)?); Ok(()) } diff --git a/v2/crates/cog-person-count/src/manifest.rs b/v2/crates/cog-person-count/src/manifest.rs new file mode 100644 index 00000000..2f7a28f8 --- /dev/null +++ b/v2/crates/cog-person-count/src/manifest.rs @@ -0,0 +1,77 @@ +//! Embedded signed cog manifest (ADR-100 §"manifest.json", ADR-159 §A4). +//! +//! The `cog-person-count manifest` subcommand emits the **real, signed** +//! manifest the release pipeline produced — byte-for-byte the artifact served +//! from GCS, with a real `binary_sha256`, `weights_sha256`, Ed25519 +//! `binary_signature`, and honest `build_metadata` (e.g. `training_class1_accuracy +//! = 0.343`, not inflated). The previous implementation printed a hollow +//! skeleton with `binary_sha256: null`, which made the CLI look unsigned even +//! though the signed manifest existed on disk. +//! +//! The matching manifest for the build's target arch is selected via `cfg!`. + +/// Real signed manifest for `x86_64-unknown-linux-gnu`. +pub const MANIFEST_X86_64: &str = + include_str!("../cog/artifacts/manifests/x86_64/manifest.json"); + +/// Real signed manifest for `aarch64`/`arm` (the Seed appliance). +pub const MANIFEST_ARM: &str = include_str!("../cog/artifacts/manifests/arm/manifest.json"); + +/// The embedded signed manifest matching the build's target arch. +pub fn embedded_manifest_str() -> &'static str { + if cfg!(any(target_arch = "aarch64", target_arch = "arm")) { + MANIFEST_ARM + } else { + MANIFEST_X86_64 + } +} + +/// Parse the embedded manifest into canonical JSON. Returns an error if the +/// embedded artifact is malformed (so the CLI fails loudly rather than printing +/// garbage). +pub fn embedded_manifest_value() -> Result { + serde_json::from_str(embedded_manifest_str()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// ADR-159 §A4 — the embedded manifest the CLI emits must carry a real + /// `binary_sha256` (the field the old hollow `cmd_manifest` left null). + #[test] + fn embedded_manifest_has_non_null_binary_sha256() { + let v = embedded_manifest_value().expect("embedded manifest parses"); + let sha = v.get("binary_sha256").and_then(|s| s.as_str()); + assert!( + sha.is_some(), + "embedded manifest must have a non-null binary_sha256 (got {:?})", + v.get("binary_sha256") + ); + let sha = sha.unwrap(); + assert_eq!(sha.len(), 64, "binary_sha256 must be a 32-byte hex digest"); + assert!( + sha.chars().all(|c| c.is_ascii_hexdigit()), + "binary_sha256 must be hex" + ); + } + + #[test] + fn embedded_manifest_is_signed() { + let v = embedded_manifest_value().expect("parse"); + assert!( + v.get("binary_signature").and_then(|s| s.as_str()).is_some(), + "embedded manifest must carry an Ed25519 binary_signature" + ); + assert_eq!( + v.get("sig_algo").and_then(|s| s.as_str()), + Some("Ed25519") + ); + } + + #[test] + fn embedded_manifest_id_matches_cog() { + let v = embedded_manifest_value().expect("parse"); + assert_eq!(v.get("id").and_then(|s| s.as_str()), Some(crate::COG_ID)); + } +} From 8d9c5994dbfa3fea8a2b6d8a17a4412338059685 Mon Sep 17 00:00:00 2001 From: ruv Date: Thu, 11 Jun 2026 23:10:02 -0400 Subject: [PATCH 05/11] fix(ruview-swarm): honest NED metres in Remote ID, not WGS84 (ADR-159 A3) RemoteIdBroadcast::update stored NED metres (state.position.x/.y) into drone_lat/drone_lon, so the ASTM F3411 broadcast would carry physically -impossible coordinates ("latitude = 37.5 m"). The module doc claimed a Location/Vector message but only encode_basic_id() exists. - Rename drone_lat/drone_lon -> drone_north_m/drone_east_m (NED metres relative to the operator/takeoff datum), documented as non-geodetic. operator_lat/lon stay true WGS84. - Correct the module doc to claim Basic ID only; Location/Vector encoding is deferred until a datum-anchored NED->WGS84 transform lands. Never broadcast physically-impossible coordinates. Failing-on-old test: security::remote_id::tests::test_ned_offset_stored_as_metres_not_latlon. Co-Authored-By: claude-flow --- .../ruview-swarm/src/security/remote_id.rs | 78 ++++++++++++++++--- 1 file changed, 69 insertions(+), 9 deletions(-) diff --git a/v2/crates/ruview-swarm/src/security/remote_id.rs b/v2/crates/ruview-swarm/src/security/remote_id.rs index 1aaa63aa..a4289222 100644 --- a/v2/crates/ruview-swarm/src/security/remote_id.rs +++ b/v2/crates/ruview-swarm/src/security/remote_id.rs @@ -1,16 +1,38 @@ -//! ASTM F3411 Remote ID broadcast (Basic ID + Location/Vector message). +//! ASTM F3411 Remote ID — **Basic ID message only** (ADR-159 §A3). +//! +//! Only the Basic ID message (`encode_basic_id`) is implemented. The +//! Location/Vector message is **not** encoded yet because the drone position is +//! tracked in a local NED frame (north/east metres relative to a takeoff datum), +//! and a compliant Location/Vector message requires WGS84 latitude/longitude. +//! Broadcasting NED metres in lat/lon fields would emit physically-impossible +//! coordinates (e.g. "latitude = 12.4 metres"), so we deliberately keep the +//! drone position in honest `drone_north_m` / `drone_east_m` fields until a real +//! local-tangent-plane NED→WGS84 transform (with an operator datum) lands. See +//! the `ACCEPTED-FUTURE` note in ADR-159 §A3. use crate::types::DroneState; use serde::{Deserialize, Serialize}; /// Remote ID broadcast state for one drone. +/// +/// Drone position is stored as **NED metres** (`drone_north_m` / `drone_east_m`) +/// relative to the operator/takeoff datum — *not* WGS84 lat/lon — because no +/// datum-anchored geodetic transform is wired yet. The operator position is true +/// WGS84 (it comes from the operator's GNSS, not the local frame). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RemoteIdBroadcast { pub uas_id: [u8; 20], // 20-byte UAS ID (ANSI/CTA-2063-A) + /// Operator latitude (WGS84 degrees) — real geodetic position. pub operator_lat: f64, + /// Operator longitude (WGS84 degrees) — real geodetic position. pub operator_lon: f64, - pub drone_lat: f64, - pub drone_lon: f64, + /// Drone north offset in **metres** from the operator/takeoff datum (NED x). + /// NOT a latitude. See module docs — Location/Vector encoding is deferred + /// until a real NED→WGS84 transform exists. + pub drone_north_m: f64, + /// Drone east offset in **metres** from the operator/takeoff datum (NED y). + /// NOT a longitude. + pub drone_east_m: f64, pub altitude_msl_m: f32, pub speed_ms: f32, pub heading_deg: f32, @@ -24,8 +46,8 @@ impl RemoteIdBroadcast { uas_id, operator_lat: 0.0, operator_lon: 0.0, - drone_lat: 0.0, - drone_lon: 0.0, + drone_north_m: 0.0, + drone_east_m: 0.0, altitude_msl_m: 0.0, speed_ms: 0.0, heading_deg: 0.0, @@ -35,11 +57,15 @@ impl RemoteIdBroadcast { } /// Update from a drone state and operator position. + /// + /// The drone position is stored as honest NED metres — we do **not** fake a + /// lat/lon from a local-frame offset. The operator position is true WGS84. pub fn update(&mut self, state: &DroneState, operator_pos: (f64, f64)) { - // Convert NED position to approximate lat/lon (placeholder — real impl uses WGS84). - // We store the NED metres as placeholder values here. - self.drone_lat = state.position.x; // placeholder: x ≈ north offset - self.drone_lon = state.position.y; // placeholder: y ≈ east offset + // NED metres, stored as-is in metre-typed fields (no fabricated geodetic + // coordinates). A future Location/Vector encoder must transform these + // through a datum-anchored NED→WGS84 projection before broadcast. + self.drone_north_m = state.position.x; // NED x = north offset, metres + self.drone_east_m = state.position.y; // NED y = east offset, metres self.altitude_msl_m = state.altitude_agl_m as f32; self.speed_ms = state.velocity.magnitude() as f32; self.heading_deg = state.heading_rad.to_degrees() as f32; @@ -80,4 +106,38 @@ mod tests { let buf = rid.encode_basic_id(); assert_eq!(buf[2], 0xFF); } + + /// ADR-159 §A3 — a known NED offset must land in honest **metre** fields, + /// never in WGS84 lat/lon fields (which would broadcast physically-impossible + /// coordinates like "latitude = 37.5 m"). Fails on old code, where the same + /// values were stored into `drone_lat`/`drone_lon`. + #[test] + fn test_ned_offset_stored_as_metres_not_latlon() { + use crate::types::{DroneState, NodeId, Position3D}; + + let mut state = DroneState::default_at_origin(NodeId(7)); + // 37.5 m north, -12.0 m east of the takeoff datum. + state.position = Position3D { + x: 37.5, + y: -12.0, + z: 5.0, + }; + let mut rid = RemoteIdBroadcast::new([0x41u8; 20]); + // Operator at a real WGS84 fix (San Francisco-ish). + rid.update(&state, (37.7749, -122.4194)); + + // Drone offset is honest NED metres. + assert_eq!(rid.drone_north_m, 37.5); + assert_eq!(rid.drone_east_m, -12.0); + + // Operator position is the real geodetic fix and is plausibly a lat/lon. + assert!((-90.0..=90.0).contains(&rid.operator_lat)); + assert!((-180.0..=180.0).contains(&rid.operator_lon)); + assert!((rid.operator_lat - 37.7749).abs() < 1e-9); + + // The drone NED metres would have been an out-of-range "latitude" only + // if a value happened to exceed 90 — but the contract is the field name + // itself: these are metres, not degrees. A future Location/Vector + // encoder must project them through a real NED→WGS84 transform. + } } From 48b002fa7e05f98506c3694a2fede4c56a216594 Mon Sep 17 00:00:00 2001 From: ruv Date: Thu, 11 Jun 2026 23:10:02 -0400 Subject: [PATCH 06/11] docs(cog-ha-matter): stop claiming Matter until it exists (ADR-159 A5) Matter commissioning is deferred to v0.8 (TlsConfig::Off, LAN-only, per tls_defaults_to_off_for_v1_lan_only). Soften the Cargo.toml description from "Home Assistant + Matter integration" to "Home Assistant (MQTT) integration ... Matter Bridge commissioning is deferred to v0.8 and not yet implemented" (honest-absence, ADR-158 pattern). No code change. Co-Authored-By: claude-flow --- v2/crates/cog-ha-matter/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/crates/cog-ha-matter/Cargo.toml b/v2/crates/cog-ha-matter/Cargo.toml index 629a60aa..4ebdeebc 100644 --- a/v2/crates/cog-ha-matter/Cargo.toml +++ b/v2/crates/cog-ha-matter/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true authors.workspace = true license.workspace = true repository.workspace = true -description = "Cognitum Cog: Home Assistant + Matter integration for the Seed (ADR-116). Wraps ADR-115's HA-DISCO + HA-MIND publisher as a Seed-installable artifact with mDNS, embedded broker, RuVector-backed thresholds, and Ed25519 witness." +description = "Cognitum Cog: Home Assistant (MQTT) integration for the Seed (ADR-116). Wraps ADR-115's HA-DISCO + HA-MIND publisher as a Seed-installable artifact with mDNS, embedded broker, RuVector-backed thresholds, and Ed25519 witness. LAN-only (no TLS); Matter Bridge commissioning is deferred to v0.8 and not yet implemented." [[bin]] name = "cog-ha-matter" From 772ece4568cbea58ce52e0b38c01d0e1916ea102 Mon Sep 17 00:00:00 2001 From: ruv Date: Thu, 11 Jun 2026 23:10:03 -0400 Subject: [PATCH 07/11] docs(adr): ADR-159 Cognitum appliance beyond-SOTA sweep Records the anti-AI-slop sweep over cog-person-count, cog-pose-estimation, cog-ha-matter, ruview-swarm. HEADLINE: the "never identified anyone" accusation is REFUTED (real SHA-pinned Ed25519-signed trained Candle models, honest 34%/3% accuracy in manifests). Documents claim-surface fixes A1-A5 (MEASURED), NO-ACTION positives (witness chain, fusion, PPO + randn audit), graded SOTA landscape (counting/pose DATA-GATED, swarm MARL untrained-at-runtime by design), and the deferred backlog (benches, Location/Vector, Matter v0.8, wasm-edge accuracy). Co-Authored-By: claude-flow --- .../ADR-159-cognitum-appliance-beyond-sota.md | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 docs/adr/ADR-159-cognitum-appliance-beyond-sota.md diff --git a/docs/adr/ADR-159-cognitum-appliance-beyond-sota.md b/docs/adr/ADR-159-cognitum-appliance-beyond-sota.md new file mode 100644 index 00000000..9c01ab41 --- /dev/null +++ b/docs/adr/ADR-159-cognitum-appliance-beyond-sota.md @@ -0,0 +1,240 @@ +# ADR-159: Cognitum Appliance Cluster — Beyond-SOTA Sweep, Anti-"AI-Slop" Hardening + +- **Status**: accepted +- **Date**: 2026-06-11 +- **Deciders**: ruv +- **Tags**: cognitum, cogs, person-count, pose-estimation, ha-matter, drone-swarm, remote-id, manifest, prove-everything + +## Context + +This ADR records the beyond-SOTA sweep over the Cognitum appliance cluster +(`cog-person-count`, `cog-pose-estimation`, `cog-ha-matter`, `ruview-swarm`), +executed under the project's **prove-everything / anti-"AI-slop"** directive: the +claim surface every cog presents (manifests, descriptions, runtime events, +broadcast fields) must match what the code and the shipped weights actually do. + +### Headline — the "never identified anyone" accusation is REFUTED + +A read-only audit raised the worst-class accusation: that these cogs are slop that +"never identified anyone." That accusation is **refuted by byte-level evidence**: + +- `cog-pose-estimation` and `cog-person-count` ship **real, trained Candle models** + (`pose_v1.safetensors`, `count_v1.safetensors`), not placeholders. The forward + passes (`PoseNet`, `CountNet`) mirror the training scripts exactly and run on + real CSI bytes. +- The artifacts are **SHA-pinned and Ed25519-signed**: the on-disk + `manifests/x86_64/manifest.json` carries a real `binary_sha256` + (`051614ce…388b3` for person-count, `a434739a…71fa` for pose), a real + `weights_sha256`, and a `binary_signature` over `sig_algo: Ed25519`. +- The manifests are **brutally honest about accuracy**: person-count's + `build_metadata` ships `training_class1_accuracy = 0.343` and a candid + `training_caveat`; pose ships `training_pck20 = 3.0` / `training_pck50 = 18.5`. + Nothing is inflated. That honesty *is* the anti-slop win — the models are weak + in the field, and the manifests say so. + +So the cogs **do** run real trained inference and **do** disclose how weak it is. +What the audit correctly found were not fabrications but **claim-surface +overclaims** — four places where the surface said more than the weights deliver. +This ADR tightens those four (A1–A4) and cites the already-correct subsystems as +NO-ACTION positives. + +Grading vocabulary follows ADR-152 / ADR-158: +- **MEASURED** — reproduced in this worktree, command + failing-on-old test recorded. +- **DATA-GATED** — real code path present; honestly flagged where data/hardware is absent. +- **NO-ACTION (already-SOTA)** — audited, found correct, cited as a positive. +- **ACCEPTED-FUTURE** — deliberately deferred, nothing dropped. + +## Graded SOTA Landscape + +| Capability | Grade | Note | +|------------|-------|------| +| CSI person counting (`cog-person-count`) | **DATA-GATED** | Real Candle count head + Bayesian fusion; weights trained only on classes 0/1 (presence). Multi-occupant accuracy is genuinely unproven and is **not fabricated** — counts above the trained range are now flagged `low_confidence` and clamped. | +| CSI pose estimation (`cog-pose-estimation`) | **DATA-GATED** | Real Candle encoder + 17-keypoint head; field accuracy honestly weak (PCK@50 = 18.5%, disclosed in the manifest). The default-install gate bug (A1) is fixed so it actually emits frames. | +| Signed cog manifests (Ed25519 + SHA-256) | **NO-ACTION (already-SOTA)** | On-disk manifests are real, signed, SHA-pinned, and honest about accuracy. The CLI now emits them verbatim (A4). | +| HA bridge (`cog-ha-matter`) MQTT + witness | **NO-ACTION (already-SOTA)** | Real Ed25519 hash-chain witness, mDNS, embedded broker. Matter commissioning is honestly deferred to v0.8 (TLS off, LAN-only) — description softened to stop claiming Matter (honest-absence). | +| Drone-swarm MARL (`ruview-swarm`) | **DATA-GATED / honest** | `candle_ppo.rs` is real autodiff PPO; it is **untrained at runtime** (random init) by design — the swarm must be trained before deploy, which the code does not hide. | +| ASTM F3411 Remote ID | **MEASURED (A3)** | Basic ID message is real; the Location/Vector message is honestly *not* implemented (NED metres are no longer mislabelled as WGS84 lat/lon). | + +## Decision — Fixes Landed (MEASURED) + +### §A1 Pose runtime emitted ZERO frames under default config (HIGH) + +**Overclaim (silent correctness bug):** `inference.rs` hardcoded +`confidence: 0.185` for every inference, `config.rs default_min_confidence()` +returned `0.3`, and `runtime.rs` gated emission on `confidence >= min_confidence`. +A default install therefore **never emitted a single `pose.frame`** while +`health` reported healthy — the cog *claimed* to be a running pose estimator but +silently produced nothing. + +**Real fix:** `pose_v1` has **no confidence head** (the head emits 34 keypoint +coordinates only), so a real per-frame confidence is genuinely unavailable. We +took the disclosed "ok" path rather than silently lowering the threshold: +- Introduced `inference::MODEL_TYPICAL_CONFIDENCE = 0.185` (the validation PCK@50) + as the single published per-frame confidence, used by both `infer()` and the + config default. +- Pinned `default_min_confidence()` to `MODEL_TYPICAL_CONFIDENCE` so a default + install clears its own gate and emits. +- Documented the trade-off in the config field doc, the JSON schema + (`default` 0.3 → 0.185, with a description), **and** added a `run.started` + warning in `main.rs` that fires when an operator raises `min_confidence` above + the model's typical confidence — so a deliberately-high threshold is loud, not + silent. + +**Failing-on-old test:** `cog_pose_estimation` smoke +`default_config_emits_frames_with_real_model` — parses a default config and +asserts `min_confidence <= MODEL_TYPICAL_CONFIDENCE` (and, with the real model +loaded, that `infer().confidence >= min_confidence`). **Proven to fail** on the +old `default_min_confidence()=0.3`: +`default min_confidence 0.3 exceeds model typical confidence 0.185 — a default +install would emit zero pose.frame events`. + +**Grade: MEASURED.** + +### §A2 8-class count head on a 2-class-trained model (MEDIUM) + +**Overclaim:** `inference.rs COUNT_CLASSES = 8` with argmax over {0..7}, but +`count_train_results.json` has support only for classes 0 and 1 (`per_class_accuracy` +keys `"0"`/`"1"`). The model is a **presence detector**, not a calibrated +multi-occupant counter; an argmax on classes 2..=7 is out-of-distribution, yet the +cog would emit it as a confident headcount. The Cargo.toml billed it as a +"learned multi-person counter." + +**Real fix (no network change — DATA-GATED, accuracy not fabricated):** +- Added `inference::MAX_TRAINED_CLASS = 1`, plus `CountPrediction::is_low_confidence()` + (argmax beyond the trained ceiling) and `clamped_count()` (report clamped to the + trained range, raw argmax kept for audit). +- `person.count` events now carry `low_confidence` + `raw_count`, and downgrade to + `level: "warn"` when out-of-distribution; the reported `count` is clamped so we + never emit a fabricated headcount the weights can't back. +- `run.started` discloses `count_max_trained_class` and `count_classes`. +- Cargo.toml description changed from "learned multi-person counter" to + "presence detector + (data-gated) person count". + +**Failing-on-old test:** `cog_person_count` smoke +`untrained_class_argmax_is_flagged_low_confidence` — a prediction whose argmax is +class 5 is asserted `is_low_confidence() == true` and `clamped_count() == +MAX_TRAINED_CLASS`; a class-1 prediction is asserted *not* flagged. Fails on old +code (no such methods/flag existed). + +**Grade: MEASURED (mechanism); multi-occupant accuracy DATA-GATED.** + +### §A3 Remote ID broadcast NED metres as WGS84 lat/lon (MEDIUM — safety/compliance) + +**Overclaim (compliance hazard):** `security/remote_id.rs update()` stored +`state.position.x/.y` (NED **metres**) into `drone_lat`/`drone_lon`, so the Remote +ID broadcast would carry physically-impossible coordinates (e.g. "latitude = +37.5 m"). The module doc claimed a "Basic ID + Location/Vector message," but only +`encode_basic_id()` exists. + +**Real fix (honest naming — never broadcast impossible coordinates):** +- Renamed `drone_lat`/`drone_lon` → `drone_north_m`/`drone_east_m` (NED metres + relative to the operator/takeoff datum), with field docs stating they are *not* + geodetic. `operator_lat`/`operator_lon` remain true WGS84 (from the operator's + GNSS). +- Corrected the module doc to claim **Basic ID only**; the Location/Vector encoder + is explicitly deferred until a datum-anchored NED→WGS84 transform lands + (ACCEPTED-FUTURE), rather than removing a real feature. + +**Failing-on-old test:** `security::remote_id::tests::test_ned_offset_stored_as_metres_not_latlon` +— a 37.5 m north / −12.0 m east NED offset is asserted to land in +`drone_north_m`/`drone_east_m`; the operator's real WGS84 fix stays in range. Fails +on old code, where these values were stored into `drone_lat`/`drone_lon`. + +**Grade: MEASURED.** + +### §A4 Hollow CLI manifest (LOW) + +**Overclaim:** `cog-person-count main.rs cmd_manifest` emitted a null skeleton +(`binary_sha256: null`, no training metadata), making the CLI look unsigned even +though the **real signed manifest** existed at +`cog/artifacts/manifests/x86_64/manifest.json`. + +**Real fix:** new `cog_person_count::manifest` module `include_str!`-embeds the +real signed manifests (x86_64 + arm), selected by build target arch. +`cmd_manifest` now parses-then-emits the embedded signed manifest — exactly the +pattern `cog-pose-estimation`'s `manifest_roundtrips` test demonstrates. The CLI +now reports the real `binary_sha256`, `weights_sha256`, Ed25519 signature, and +honest `build_metadata` (`training_class1_accuracy = 0.343`). + +**Failing-on-old test:** `manifest::tests::embedded_manifest_has_non_null_binary_sha256` +asserts a 64-hex-char `binary_sha256`; companions assert the embedded manifest is +signed (`sig_algo == Ed25519`) and `id == COG_ID`. End-to-end verified: +`cog-person-count manifest` prints `binary_sha256: +051614ce6ba63df704fae848a67ad095df4bb88862fdff05ef3c0419cc8388b3`. + +**Grade: MEASURED.** + +### §A5 cog-ha-matter description claimed Matter before it exists (LOW — honest-labeling) + +**Overclaim:** the Cargo.toml description said "Home Assistant + Matter +integration," but Matter commissioning is deferred to v0.8 (`TlsConfig::Off`, +LAN-only, asserted by `runtime.rs tls_defaults_to_off_for_v1_lan_only`). + +**Real fix (no code change):** softened the description to "Home Assistant (MQTT) +integration … LAN-only (no TLS); Matter Bridge commissioning is deferred to v0.8 +and not yet implemented." Mirrors ADR-158 §6 honest-absence: state what isn't +there rather than implying it is. + +**Grade: MEASURED (label).** + +## Negative Results (Confirmed — NO-ACTION positives) + +Audited and found genuinely correct; cited as positives, not edited: + +- **`cog-ha-matter` witness chain** (`witness.rs` / `witness_signing.rs`) — real + Ed25519 hash-chained witness log. Already-SOTA. +- **`cog-person-count` fusion** (`fusion.rs`) — real Bayesian product-of-experts + multi-node fusion (Stoer-Wagner-bounded clip), not a heuristic. Already-SOTA. +- **`ruview-swarm` PPO** (`marl/candle_ppo.rs`) — real Candle autodiff PPO with a + genuine policy-gradient update; its `randn` uses (init, action sampling, + exploration) are all legitimate, not fake-output substitutes. Untrained at + runtime by design (the swarm must be trained before deploy), which the code + does not hide. Already-SOTA / honest. + +## Deferred Backlog (Nothing Dropped) + +- **Multi-occupant count accuracy** — DATA-GATED on labelled multi-occupant CSI. + The `low_confidence` flag + clamp (§A2) is the honest stand-in until then. +- **Remote ID Location/Vector message** — ACCEPTED-FUTURE; requires a + datum-anchored local-tangent-plane NED→WGS84 transform with an operator datum. + Basic ID ships today. +- **Matter Bridge commissioning** — ACCEPTED-FUTURE (v0.8); LAN-only MQTT ships today. +- **Criterion benches** for cog inference latency and `mesh_guard` — ACCEPTED-FUTURE + (cold-start timings are recorded in the manifests' `build_metadata`, not yet a + regression bench). +- **`wasm-edge` 70-skill accuracy** — unvalidated; honestly labelled, not claimed. + +## Consequences + +- A default pose-estimation install now actually emits `pose.frame` events; + raising the threshold above the model's reach is a loud `run.started` warning, + not a silent dropout. +- A person-count reading on an untrained class is flagged `low_confidence`, + clamped, and downgraded to `warn` — no fabricated headcounts. +- The Remote ID broadcast can never carry physically-impossible coordinates; NED + metres live in honestly-named metre fields. +- `cog-person-count manifest` now reports the real signed manifest instead of a + hollow null skeleton. +- No cog Cargo.toml description claims a capability (multi-person counting, Matter) + the code/weights don't yet deliver. + +## Reproduction (MEASURED) + +```bash +cd v2 +cargo test -p cog-person-count -p cog-pose-estimation -p cog-ha-matter -p ruview-swarm \ + --no-default-features +# ruview-swarm train path compiles (PPO autodiff) +cargo check -p ruview-swarm --features train +# A4 end-to-end — real signed manifest, non-null binary_sha256 +cargo run -q -p cog-person-count --no-default-features -- manifest +``` + +Result at time of writing (all 0 failed): +- `cog-person-count` — **19 passed** (lib 10 incl. 3 manifest; smoke 9) +- `cog-pose-estimation` — **8 passed** (smoke) +- `cog-ha-matter` — **64 passed** (unchanged; description-only edit) +- `ruview-swarm` — **117 passed** (default features); `--features train` compiles clean. + +Scope was limited to the four named crates. NO-ACTION positives (witness chain, +fusion, PPO + randn audit) were verified by inspection and left untouched. From 36af09a4a8c4057e668996a330435a964b945ad2 Mon Sep 17 00:00:00 2001 From: ruv Date: Thu, 11 Jun 2026 23:59:08 -0400 Subject: [PATCH 08/11] feat(wasm-edge): honest labeling + static-mut soundness for edge skills (ADR-160) The wasm-edge skill library runs real DSP with 0 stubs / 0 theater; the exposure is an over-confident claim surface on unvalidated skills plus a latent static-mut soundness issue. Make the labels TRUE (do not pretend to validate the capability) and fix the soundness mechanically: - A1 (HIGH): med_seizure/cardiac/respiratory/sleep_apnea/gait -- add mandatory "EXPERIMENTAL / NOT VALIDATED AGAINST CLINICAL DATA / NOT A MEDICAL DEVICE" disclaimers, soften assertive verbs to "flags candidate -like signatures", and gate all 5 behind a NON-default medical-experimental cargo feature so they cannot be silently shipped. DSP kept. - A2 (HIGH): exo_happiness_score/exo_emotion_detect -- delete the uncited "~12% faster" stat, add "speculative, unvalidated affect heuristic; outputs are NOT measurements of emotion" disclaimers, reframe HAPPINESS_SCORE as a gait-energy proxy. Math kept. - A3 (MEDIUM): sec_weapon_detect -- rename EVENT_WEAPON_ALERT -> EVENT_HIGH_METAL_REFLECTIVITY and WEAPON_RATIO_THRESH -> HIGH_REFLECTIVITY_THRESH (a variance ratio measures reflectivity, not weapons). Registry updated. - A4 (MEDIUM): exo_dream_stage/exo_gesture_language -- add experimental disclaimers, promote the Exotic/Research tag into the header. - A5 (MEDIUM, soundness): replace ~61 `static mut EVENTS`/EV/TE/EMPTY per-call scratch buffers (60 modules) with owned per-instance `events` fields returned as `&self.events[..n]`. Public signature unchanged; behavior preserved. Only the two legitimate single-threaded WASM module singletons (lib.rs STATE, ghost_hunter DETECTOR) remain as static mut. Removes the static_mut_refs source. NO-ACTION positives (cited, labels untouched): qnt_* (quantum-/Grover-inspired, disclosed), exo_time_crystal, exo_ghost_hunter, sig_*/lrn_* algorithm-named skills. Co-Authored-By: claude-flow --- v2/crates/wifi-densepose-wasm-edge/Cargo.toml | 9 +++ .../src/ais_behavioral_profiler.rs | 16 +++-- .../src/ais_prompt_shield.rs | 16 +++-- .../src/aut_psycho_symbolic.rs | 14 ++-- .../src/aut_self_healing_mesh.rs | 16 +++-- .../src/bld_elevator_count.rs | 18 ++--- .../src/bld_energy_audit.rs | 18 ++--- .../src/bld_hvac_presence.rs | 18 ++--- .../src/bld_lighting_zones.rs | 14 ++-- .../src/bld_meeting_room.rs | 26 +++---- .../src/exo_breathing_sync.rs | 28 +++----- .../src/exo_dream_stage.rs | 32 +++++---- .../src/exo_emotion_detect.rs | 33 +++++---- .../src/exo_gesture_language.rs | 33 +++++---- .../src/exo_ghost_hunter.rs | 22 +++--- .../src/exo_happiness_score.rs | 52 +++++++------- .../src/exo_hyperbolic_space.rs | 18 ++--- .../src/exo_music_conductor.rs | 26 +++---- .../src/exo_plant_growth.rs | 22 +++--- .../src/exo_rain_detect.rs | 18 ++--- .../src/exo_time_crystal.rs | 18 ++--- .../src/ind_clean_room.rs | 15 ++-- .../src/ind_confined_space.rs | 16 +++-- .../src/ind_forklift_proximity.rs | 20 +++--- .../src/ind_livestock_monitor.rs | 15 ++-- .../src/ind_structural_vibration.rs | 18 ++--- .../wifi-densepose-wasm-edge/src/intrusion.rs | 18 ++--- v2/crates/wifi-densepose-wasm-edge/src/lib.rs | 16 ++++- .../src/lrn_anomaly_attractor.rs | 38 +++++----- .../src/lrn_dtw_gesture_learn.rs | 26 ++++--- .../src/lrn_ewc_lifelong.rs | 22 +++--- .../src/lrn_meta_adapt.rs | 36 +++++----- .../src/med_cardiac_arrhythmia.rs | 38 ++++++---- .../src/med_gait_analysis.rs | 31 +++++--- .../src/med_respiratory_distress.rs | 38 ++++++---- .../src/med_seizure_detect.rs | 43 +++++++---- .../src/med_sleep_apnea.rs | 39 ++++++---- .../wifi-densepose-wasm-edge/src/occupancy.rs | 18 ++--- .../src/qnt_interference_search.rs | 18 ++--- .../src/qnt_quantum_coherence.rs | 18 ++--- .../src/ret_customer_flow.rs | 26 +++---- .../src/ret_dwell_heatmap.rs | 22 +++--- .../src/ret_queue_length.rs | 22 +++--- .../src/ret_shelf_engagement.rs | 18 +++-- .../src/ret_table_turnover.rs | 30 +++----- .../src/sec_loitering.rs | 18 ++--- .../src/sec_panic_motion.rs | 18 ++--- .../src/sec_perimeter_breach.rs | 18 ++--- .../src/sec_tailgating.rs | 20 +++--- .../src/sec_weapon_detect.rs | 72 +++++++++++-------- .../src/sig_coherence_gate.rs | 12 ++-- .../src/sig_flash_attention.rs | 14 ++-- .../src/sig_mincut_person_match.rs | 18 ++--- .../src/sig_optimal_transport.rs | 14 ++-- .../src/sig_sparse_recovery.rs | 20 +++--- .../src/sig_temporal_compress.rs | 22 +++--- .../src/spt_micro_hnsw.rs | 21 +++--- .../src/spt_pagerank_influence.rs | 22 +++--- .../src/spt_spiking_tracker.rs | 18 ++--- .../src/tmp_goap_autonomy.rs | 14 ++-- .../src/tmp_pattern_sequence.rs | 14 ++-- .../src/tmp_temporal_logic_guard.rs | 41 +++++------ .../src/vital_trend.rs | 34 +++------ 63 files changed, 727 insertions(+), 751 deletions(-) diff --git a/v2/crates/wifi-densepose-wasm-edge/Cargo.toml b/v2/crates/wifi-densepose-wasm-edge/Cargo.toml index 02cade21..2dee4614 100644 --- a/v2/crates/wifi-densepose-wasm-edge/Cargo.toml +++ b/v2/crates/wifi-densepose-wasm-edge/Cargo.toml @@ -22,6 +22,15 @@ sha2 = { version = "0.10", optional = true, default-features = false } default = ["default-pipeline"] # Enable std for testing on host + RVF builder std = ["sha2/std"] +# Experimental medical skills (med_seizure_detect, med_cardiac_arrhythmia, +# med_respiratory_distress, med_sleep_apnea, med_gait_analysis). +# +# ⚠️ NON-DEFAULT BY DESIGN. These modules run real DSP but are NOT validated +# against clinical data and are NOT medical devices (ADR-160 §A1). They are +# gated behind this feature so they cannot be silently built into a shipping +# artifact. Build/test with: +# cargo test -p wifi-densepose-wasm-edge --features std,medical-experimental +medical-experimental = [] # Include the default combined pipeline (gesture+coherence+adversarial) entry points. # Disable this when building standalone module binaries (ghost_hunter, etc.) default-pipeline = [] diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ais_behavioral_profiler.rs b/v2/crates/wifi-densepose-wasm-edge/src/ais_behavioral_profiler.rs index 04c15ed2..61db4481 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ais_behavioral_profiler.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ais_behavioral_profiler.rs @@ -111,6 +111,8 @@ pub struct BehavioralProfiler { obs_cycles: u32, cooldown: u16, anomaly_count: u32, + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], } impl BehavioralProfiler { @@ -118,6 +120,7 @@ impl BehavioralProfiler { Self { stats: [Welford::new(); N_DIM], obs: ObsWindow::new(), mature: false, frame_count: 0, obs_cycles: 0, cooldown: 0, anomaly_count: 0, + events: [(0, 0.0); 4], } } @@ -127,7 +130,6 @@ impl BehavioralProfiler { self.cooldown = self.cooldown.saturating_sub(1); self.obs.push(present, motion, n_persons); - static mut EV: [(i32, f32); 4] = [(0, 0.0); 4]; let mut ne = 0usize; if self.frame_count % (OBS_WIN as u32) == 0 && self.obs.len == OBS_WIN { @@ -139,7 +141,7 @@ impl BehavioralProfiler { if self.obs_cycles >= LEARNING_FRAMES / (OBS_WIN as u32) { self.mature = true; let days = self.frame_count as f32 / (20.0 * 86400.0); - unsafe { EV[ne] = (EVENT_PROFILE_MATURITY, days); } + self.events[ne] = (EVENT_PROFILE_MATURITY, days); ne += 1; } } else { @@ -159,12 +161,12 @@ impl BehavioralProfiler { if self.cooldown == 0 { if cz > ANOMALY_Z { self.anomaly_count += 1; - unsafe { EV[ne] = (EVENT_BEHAVIOR_ANOMALY, cz); } ne += 1; - if ne < 4 { unsafe { EV[ne] = (EVENT_PROFILE_DEVIATION, max_d as f32); } ne += 1; } + self.events[ne] = (EVENT_BEHAVIOR_ANOMALY, cz); ne += 1; + if ne < 4 { self.events[ne] = (EVENT_PROFILE_DEVIATION, max_d as f32); ne += 1; } self.cooldown = COOLDOWN; } if hi_z >= NOVEL_MIN && ne < 4 { - unsafe { EV[ne] = (EVENT_NOVEL_PATTERN, hi_z as f32); } ne += 1; + self.events[ne] = (EVENT_NOVEL_PATTERN, hi_z as f32); ne += 1; if self.cooldown == 0 { self.cooldown = COOLDOWN; } } } @@ -173,10 +175,10 @@ impl BehavioralProfiler { // Periodic maturity report. if self.mature && self.frame_count % MATURITY_INTERVAL == 0 && ne < 4 { - unsafe { EV[ne] = (EVENT_PROFILE_MATURITY, self.frame_count as f32 / (20.0 * 86400.0)); } + self.events[ne] = (EVENT_PROFILE_MATURITY, self.frame_count as f32 / (20.0 * 86400.0)); ne += 1; } - unsafe { &EV[..ne] } + &self.events[..ne] } pub fn is_mature(&self) -> bool { self.mature } diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ais_prompt_shield.rs b/v2/crates/wifi-densepose-wasm-edge/src/ais_prompt_shield.rs index 5e8ae8e0..d807c514 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ais_prompt_shield.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ais_prompt_shield.rs @@ -48,6 +48,8 @@ pub struct PromptShield { cd_replay: u16, cd_inject: u16, cd_jam: u16, + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], } impl PromptShield { @@ -58,6 +60,7 @@ impl PromptShield { baseline_snr: 0.0, cal_amp: 0.0, cal_var: 0.0, cal_n: 0, calibrated: false, low_snr_run: 0, frame_count: 0, cd_replay: 0, cd_inject: 0, cd_jam: 0, + events: [(0, 0.0); 4], } } @@ -70,7 +73,6 @@ impl PromptShield { self.cd_inject = self.cd_inject.saturating_sub(1); self.cd_jam = self.cd_jam.saturating_sub(1); - static mut EV: [(i32, f32); 4] = [(0, 0.0); 4]; let mut ne = 0usize; // Frame features: mean phase, mean amp, amp variance. @@ -98,7 +100,7 @@ impl PromptShield { } let h = self.fnv1a(m_ph, m_a, a_var); self.push_hash(h); - return unsafe { &EV[..0] }; + return &self.events[..0]; } // ── 1. Replay ─────────────────────────────────────────────────── @@ -106,7 +108,7 @@ impl PromptShield { let replay = self.has_hash(h); self.push_hash(h); if replay && self.cd_replay == 0 { - unsafe { EV[ne] = (EVENT_REPLAY_ATTACK, 1.0); } + self.events[ne] = (EVENT_REPLAY_ATTACK, 1.0); ne += 1; self.cd_replay = COOLDOWN; } @@ -121,7 +123,7 @@ impl PromptShield { jc as f32 / n as f32 } else { 0.0 }; if inj_f >= INJECTION_FRAC && self.cd_inject == 0 && ne < 4 { - unsafe { EV[ne] = (EVENT_INJECTION_DETECTED, inj_f); } + self.events[ne] = (EVENT_INJECTION_DETECTED, inj_f); ne += 1; self.cd_inject = COOLDOWN; } @@ -133,7 +135,7 @@ impl PromptShield { } else { self.low_snr_run = 0; } if self.low_snr_run >= JAMMING_CONSEC && self.cd_jam == 0 && ne < 4 { let r = if cur_snr > 0.0001 { self.baseline_snr / cur_snr } else { 1000.0 }; - unsafe { EV[ne] = (EVENT_JAMMING_DETECTED, 10.0 * log10f(r)); } + self.events[ne] = (EVENT_JAMMING_DETECTED, 10.0 * log10f(r)); ne += 1; self.cd_jam = COOLDOWN; } @@ -146,12 +148,12 @@ impl PromptShield { let r = cur_snr / self.baseline_snr; if r < 0.5 { s -= (1.0 - r * 2.0).min(0.3); } } - unsafe { EV[ne] = (EVENT_SIGNAL_INTEGRITY, if s < 0.0 { 0.0 } else { s }); } + self.events[ne] = (EVENT_SIGNAL_INTEGRITY, if s < 0.0 { 0.0 } else { s }); ne += 1; } for i in 0..n { self.prev_amps[i] = amps[i]; } - unsafe { &EV[..ne] } + &self.events[..ne] } fn fnv1a(&self, ph: f32, amp: f32, var: f32) -> u32 { diff --git a/v2/crates/wifi-densepose-wasm-edge/src/aut_psycho_symbolic.rs b/v2/crates/wifi-densepose-wasm-edge/src/aut_psycho_symbolic.rs index a5a3088c..af8dc3e9 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/aut_psycho_symbolic.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/aut_psycho_symbolic.rs @@ -290,6 +290,8 @@ static KNOWLEDGE_BASE: [Rule; MAX_RULES] = build_knowledge_base(); /// Psycho-symbolic inference engine. pub struct PsychoSymbolicEngine { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); MAX_EVENTS], /// Bitmap of rules that fired in the current frame. fired_rules: u16, /// Previous frame's winning conclusion ID. @@ -307,6 +309,7 @@ pub struct PsychoSymbolicEngine { impl PsychoSymbolicEngine { pub const fn new() -> Self { Self { + events: [(0, 0.0); MAX_EVENTS], fired_rules: 0, prev_conclusion: 0, contradiction_count: 0, @@ -340,7 +343,6 @@ impl PsychoSymbolicEngine { n_persons: f32, time_bucket: f32, ) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS]; let mut n_events = 0usize; self.frame_count += 1; @@ -372,7 +374,7 @@ impl PsychoSymbolicEngine { // Emit RULE_FIRED event (up to budget). if n_events < MAX_EVENTS { - unsafe { EVENTS[n_events] = (EVENT_RULE_FIRED, i as f32); } + self.events[n_events] = (EVENT_RULE_FIRED, i as f32); n_events += 1; } @@ -394,7 +396,7 @@ impl PsychoSymbolicEngine { self.contradiction_count += 1; if n_events < MAX_EVENTS { let encoded = (a as f32) * 100.0 + (b as f32); - unsafe { EVENTS[n_events] = (EVENT_CONTRADICTION, encoded); } + self.events[n_events] = (EVENT_CONTRADICTION, encoded); n_events += 1; } // Suppress the weaker conclusion. @@ -414,10 +416,10 @@ impl PsychoSymbolicEngine { // Emit winning inference. if best_confidence > 0.0 && n_events < MAX_EVENTS { - unsafe { EVENTS[n_events] = (EVENT_INFERENCE_RESULT, best_conclusion as f32); } + self.events[n_events] = (EVENT_INFERENCE_RESULT, best_conclusion as f32); n_events += 1; if n_events < MAX_EVENTS { - unsafe { EVENTS[n_events] = (EVENT_INFERENCE_CONFIDENCE, best_confidence); } + self.events[n_events] = (EVENT_INFERENCE_CONFIDENCE, best_confidence); n_events += 1; } } @@ -426,7 +428,7 @@ impl PsychoSymbolicEngine { self.prev_motion = motion; self.prev_conclusion = best_conclusion; - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Get the bitmap of rules that fired in the last frame. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/aut_self_healing_mesh.rs b/v2/crates/wifi-densepose-wasm-edge/src/aut_self_healing_mesh.rs index b8b475d7..3aab4930 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/aut_self_healing_mesh.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/aut_self_healing_mesh.rs @@ -28,6 +28,8 @@ pub const EVENT_HEALING_COMPLETE: i32 = 888; /// Self-healing mesh monitor with Stoer-Wagner min-cut analysis. pub struct SelfHealingMesh { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); MAX_EVENTS], /// EMA-smoothed quality score per node [0, 1]. node_quality: [f32; MAX_NODES], /// Whether each node quality has received its first sample. @@ -49,6 +51,7 @@ pub struct SelfHealingMesh { impl SelfHealingMesh { pub const fn new() -> Self { Self { + events: [(0, 0.0); MAX_EVENTS], node_quality: [0.0; MAX_NODES], node_init: [false; MAX_NODES], adj: [[0.0; MAX_NODES]; MAX_NODES], @@ -76,7 +79,6 @@ impl SelfHealingMesh { /// per active node (length clamped to 8). /// Returns a slice of (event_id, value) pairs. pub fn process_frame(&mut self, node_qualities: &[f32]) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS]; let mut ne = 0usize; self.frame_count += 1; @@ -84,7 +86,7 @@ impl SelfHealingMesh { self.n_active = n; for i in 0..n { self.update_node_quality(i, node_qualities[i]); } - if n < 2 { return unsafe { &EVENTS[..0] }; } + if n < 2 { return &self.events[..0]; } // Build adjacency: edge weight = min(quality_i, quality_j). for i in 0..n { @@ -101,7 +103,7 @@ impl SelfHealingMesh { for i in 0..n { sum += self.node_quality[i]; } let coverage = sum / (n as f32); if ne < MAX_EVENTS { - unsafe { EVENTS[ne] = (EVENT_COVERAGE_SCORE, coverage); } + self.events[ne] = (EVENT_COVERAGE_SCORE, coverage); ne += 1; } @@ -112,24 +114,24 @@ impl SelfHealingMesh { if !self.healing { self.healing = true; } self.weakest = cut_node; if ne < MAX_EVENTS { - unsafe { EVENTS[ne] = (EVENT_NODE_DEGRADED, cut_node as f32); } + self.events[ne] = (EVENT_NODE_DEGRADED, cut_node as f32); ne += 1; } if ne < MAX_EVENTS { - unsafe { EVENTS[ne] = (EVENT_MESH_RECONFIGURE, mincut); } + self.events[ne] = (EVENT_MESH_RECONFIGURE, mincut); ne += 1; } } else if self.healing && mincut >= MINCUT_HEALTHY { self.healing = false; self.weakest = NO_NODE; if ne < MAX_EVENTS { - unsafe { EVENTS[ne] = (EVENT_HEALING_COMPLETE, mincut); } + self.events[ne] = (EVENT_HEALING_COMPLETE, mincut); ne += 1; } } self.prev_mincut = mincut; - unsafe { &EVENTS[..ne] } + &self.events[..ne] } /// Simplified Stoer-Wagner min-cut for n <= 8 nodes. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/bld_elevator_count.rs b/v2/crates/wifi-densepose-wasm-edge/src/bld_elevator_count.rs index b84df980..74323099 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/bld_elevator_count.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/bld_elevator_count.rs @@ -59,6 +59,8 @@ pub enum DoorState { /// Elevator occupancy counter. pub struct ElevatorCounter { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Baseline amplitude per subcarrier (empty cabin). baseline_amp: [f32; MAX_SC], /// Baseline variance per subcarrier. @@ -93,6 +95,7 @@ pub struct ElevatorCounter { impl ElevatorCounter { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], baseline_amp: [0.0; MAX_SC], baseline_var: [0.0; MAX_SC], prev_amp: [0.0; MAX_SC], @@ -268,15 +271,12 @@ impl ElevatorCounter { } // ── Build events ──────────────────────────────────────────────── - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_events = 0usize; // Door events (immediate). if let Some(evt) = door_event { if n_events < 4 { - unsafe { - EVENTS[n_events] = (evt, self.count as f32); - } + self.events[n_events] = (evt, self.count as f32); n_events += 1; } } @@ -284,22 +284,18 @@ impl ElevatorCounter { // Periodic count and overload. if self.frame_count % EMIT_INTERVAL == 0 { if n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_ELEVATOR_COUNT, self.count as f32); - } + self.events[n_events] = (EVENT_ELEVATOR_COUNT, self.count as f32); n_events += 1; } // Overload warning. if self.count >= self.overload_thresh && n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_OVERLOAD_WARNING, self.count as f32); - } + self.events[n_events] = (EVENT_OVERLOAD_WARNING, self.count as f32); n_events += 1; } } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Get current occupant count estimate. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/bld_energy_audit.rs b/v2/crates/wifi-densepose-wasm-edge/src/bld_energy_audit.rs index c2d36f13..94c12561 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/bld_energy_audit.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/bld_energy_audit.rs @@ -77,6 +77,8 @@ impl HourBin { /// Energy audit analyzer. pub struct EnergyAuditor { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], /// Weekly histogram: [day][hour]. histogram: [[HourBin; HOURS_PER_DAY]; DAYS_PER_WEEK], /// Current simulated hour (0-23). In production, derived from host timestamp. @@ -98,6 +100,7 @@ impl EnergyAuditor { const BIN_INIT: HourBin = HourBin::new(); const DAY_INIT: [HourBin; HOURS_PER_DAY] = [BIN_INIT; HOURS_PER_DAY]; Self { + events: [(0, 0.0); 3], histogram: [DAY_INIT; DAYS_PER_WEEK], current_hour: 8, // Default start: 8 AM. current_day: 0, // Monday. @@ -161,14 +164,11 @@ impl EnergyAuditor { } // Build events. - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut n_events = 0usize; // After-hours alert. if self.after_hours_presence >= AFTER_HOURS_ALERT_FRAMES && n_events < 3 { - unsafe { - EVENTS[n_events] = (EVENT_AFTER_HOURS_ALERT, self.current_hour as f32); - } + self.events[n_events] = (EVENT_AFTER_HOURS_ALERT, self.current_hour as f32); n_events += 1; } @@ -177,23 +177,19 @@ impl EnergyAuditor { // Emit current hour's occupancy rate. let rate = self.histogram[d][h].occupancy_rate(); if n_events < 3 { - unsafe { - EVENTS[n_events] = (EVENT_SCHEDULE_SUMMARY, rate); - } + self.events[n_events] = (EVENT_SCHEDULE_SUMMARY, rate); n_events += 1; } // Emit overall utilization rate. if n_events < 3 { let util = self.utilization_rate(); - unsafe { - EVENTS[n_events] = (EVENT_UTILIZATION_RATE, util); - } + self.events[n_events] = (EVENT_UTILIZATION_RATE, util); n_events += 1; } } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Check if a given hour is after-hours. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/bld_hvac_presence.rs b/v2/crates/wifi-densepose-wasm-edge/src/bld_hvac_presence.rs index 4f47d505..c85d629c 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/bld_hvac_presence.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/bld_hvac_presence.rs @@ -57,6 +57,8 @@ pub enum ActivityLevel { /// HVAC-optimized presence detector. pub struct HvacPresenceDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], state: HvacState, /// Smoothed motion energy (EMA). motion_ema: f32, @@ -73,6 +75,7 @@ pub struct HvacPresenceDetector { impl HvacPresenceDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 3], state: HvacState::Vacant, motion_ema: 0.0, activity: ActivityLevel::Sedentary, @@ -159,7 +162,6 @@ impl HvacPresenceDetector { } // Build output events. - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut n = 0usize; if self.frame_count % EMIT_INTERVAL == 0 { @@ -168,9 +170,7 @@ impl HvacPresenceDetector { HvacState::Occupied | HvacState::DeparturePending => 1.0, _ => 0.0, }; - unsafe { - EVENTS[n] = (EVENT_HVAC_OCCUPIED, occupied_val); - } + self.events[n] = (EVENT_HVAC_OCCUPIED, occupied_val); n += 1; // Activity level: 0.0 = sedentary, 1.0 = active, plus raw EMA. @@ -178,9 +178,7 @@ impl HvacPresenceDetector { ActivityLevel::Sedentary => 0.0 + self.motion_ema.min(0.99), ActivityLevel::Active => 1.0, }; - unsafe { - EVENTS[n] = (EVENT_ACTIVITY_LEVEL, activity_val); - } + self.events[n] = (EVENT_ACTIVITY_LEVEL, activity_val); n += 1; } @@ -191,13 +189,11 @@ impl HvacPresenceDetector { { let remaining = DEPARTURE_TIMEOUT.saturating_sub(self.absence_frames); let fraction = remaining as f32 / DEPARTURE_TIMEOUT as f32; - unsafe { - EVENTS[n] = (EVENT_DEPARTURE_COUNTDOWN, fraction); - } + self.events[n] = (EVENT_DEPARTURE_COUNTDOWN, fraction); n += 1; } - unsafe { &EVENTS[..n] } + &self.events[..n] } /// Get current HVAC state. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/bld_lighting_zones.rs b/v2/crates/wifi-densepose-wasm-edge/src/bld_lighting_zones.rs index 3501e463..c1321cfd 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/bld_lighting_zones.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/bld_lighting_zones.rs @@ -76,6 +76,8 @@ struct ZoneLight { /// Lighting zone controller. pub struct LightingZoneController { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 8], zones: [ZoneLight; MAX_ZONES], n_zones: usize, /// Calibration accumulators. @@ -99,6 +101,7 @@ impl LightingZoneController { vacant_frames: 0, }; Self { + events: [(0, 0.0); 8], zones: [ZONE_INIT; MAX_ZONES], n_zones: 0, calib_sum: [0.0; MAX_ZONES], @@ -230,7 +233,6 @@ impl LightingZoneController { } // Build output events. - static mut EVENTS: [(i32, f32); 8] = [(0, 0.0); 8]; let mut n_events = 0usize; // Emit transitions immediately. @@ -241,9 +243,7 @@ impl LightingZoneController { LightState::Dim => EVENT_LIGHT_DIM, LightState::Off => EVENT_LIGHT_OFF, }; - unsafe { - EVENTS[n_events] = (event_id, z as f32); - } + self.events[n_events] = (event_id, z as f32); n_events += 1; } } @@ -259,15 +259,13 @@ impl LightingZoneController { }; // Encode zone_id + confidence in value. let val = z as f32 + self.zones[z].score.min(0.99); - unsafe { - EVENTS[n_events] = (event_id, val); - } + self.events[n_events] = (event_id, val); n_events += 1; } } } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Get the lighting state of a specific zone. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/bld_meeting_room.rs b/v2/crates/wifi-densepose-wasm-edge/src/bld_meeting_room.rs index 1a6ebe40..8075a323 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/bld_meeting_room.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/bld_meeting_room.rs @@ -54,6 +54,8 @@ pub enum MeetingState { /// Meeting room tracker. pub struct MeetingRoomTracker { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], state: MeetingState, /// Frames in current state. state_frames: u32, @@ -76,6 +78,7 @@ pub struct MeetingRoomTracker { impl MeetingRoomTracker { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], state: MeetingState::Empty, state_frames: 0, n_persons: 0, @@ -116,7 +119,6 @@ impl MeetingRoomTracker { self.multi_person_frames += 1; } - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_events = 0usize; let _prev_state = self.state; @@ -146,9 +148,7 @@ impl MeetingRoomTracker { self.meeting_count += 1; if n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_MEETING_START, self.n_persons as f32); - } + self.events[n_events] = (EVENT_MEETING_START, self.n_persons as f32); n_events += 1; } } else if self.state_frames >= PRE_MEETING_TIMEOUT { @@ -175,17 +175,13 @@ impl MeetingRoomTracker { // Emit meeting end with duration. let duration_mins = self.total_meeting_frames as f32 / (20.0 * 60.0); if n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_MEETING_END, duration_mins); - } + self.events[n_events] = (EVENT_MEETING_END, duration_mins); n_events += 1; } // Emit peak headcount. if n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_PEAK_HEADCOUNT, self.peak_headcount as f32); - } + self.events[n_events] = (EVENT_PEAK_HEADCOUNT, self.peak_headcount as f32); n_events += 1; } } @@ -204,9 +200,7 @@ impl MeetingRoomTracker { self.multi_person_frames = 0; if n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_ROOM_AVAILABLE, 1.0); - } + self.events[n_events] = (EVENT_ROOM_AVAILABLE, 1.0); n_events += 1; } } @@ -216,14 +210,12 @@ impl MeetingRoomTracker { // Periodic status emission. if self.frame_count % EMIT_INTERVAL == 0 && self.state == MeetingState::Active { if n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_PEAK_HEADCOUNT, self.peak_headcount as f32); - } + self.events[n_events] = (EVENT_PEAK_HEADCOUNT, self.peak_headcount as f32); n_events += 1; } } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Get current meeting room state. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/exo_breathing_sync.rs b/v2/crates/wifi-densepose-wasm-edge/src/exo_breathing_sync.rs index b22fe739..995cfbd0 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/exo_breathing_sync.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/exo_breathing_sync.rs @@ -151,6 +151,8 @@ impl PairState { /// group assignment, then computes pairwise cross-correlation to detect /// phase-locked breathing. pub struct BreathingSyncDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Per-person breathing channels (max 4). channels: [BreathingChannel; MAX_PERSONS], /// Pairwise synchronization states (max 6). @@ -170,6 +172,7 @@ pub struct BreathingSyncDetector { impl BreathingSyncDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], channels: [ BreathingChannel::new(), BreathingChannel::new(), BreathingChannel::new(), BreathingChannel::new(), @@ -201,7 +204,6 @@ impl BreathingSyncDetector { _breathing_bpm: f32, n_persons: i32, ) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_ev = 0usize; self.frame_count += 1; @@ -214,14 +216,12 @@ impl BreathingSyncDetector { if n_pers < 2 { // Reset pair states when fewer than 2 persons. if self.any_synced { - unsafe { - EVENTS[n_ev] = (EVENT_SYNC_LOST, 1.0); - } + self.events[n_ev] = (EVENT_SYNC_LOST, 1.0); n_ev += 1; self.any_synced = false; self.prev_sync_count = 0; } - return unsafe { &EVENTS[..n_ev] }; + return &self.events[..n_ev]; } let n_sc = core::cmp::min(phases.len(), MAX_SC); @@ -331,36 +331,28 @@ impl BreathingSyncDetector { // Emit events. if self.any_synced && !was_any_synced { - unsafe { - EVENTS[n_ev] = (EVENT_SYNC_DETECTED, 1.0); - } + self.events[n_ev] = (EVENT_SYNC_DETECTED, 1.0); n_ev += 1; } if was_any_synced && !self.any_synced { - unsafe { - EVENTS[n_ev] = (EVENT_SYNC_LOST, 1.0); - } + self.events[n_ev] = (EVENT_SYNC_LOST, 1.0); n_ev += 1; } if sync_count != self.prev_sync_count && sync_count > 0 { - unsafe { - EVENTS[n_ev] = (EVENT_SYNC_PAIR_COUNT, sync_count as f32); - } + self.events[n_ev] = (EVENT_SYNC_PAIR_COUNT, sync_count as f32); n_ev += 1; } self.prev_sync_count = sync_count; // Emit coherence periodically (every 10 frames). if self.frame_count % 10 == 0 { - unsafe { - EVENTS[n_ev] = (EVENT_GROUP_COHERENCE, self.group_coherence); - } + self.events[n_ev] = (EVENT_GROUP_COHERENCE, self.group_coherence); n_ev += 1; } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Compute normalized cross-correlation between two person channels diff --git a/v2/crates/wifi-densepose-wasm-edge/src/exo_dream_stage.rs b/v2/crates/wifi-densepose-wasm-edge/src/exo_dream_stage.rs index 6c0b712e..26e82c8c 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/exo_dream_stage.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/exo_dream_stage.rs @@ -1,4 +1,12 @@ -//! Non-contact sleep stage classification — ADR-041 exotic module. +//! Non-contact sleep-stage-like classification — ADR-041 exotic / research module. +//! +//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED. Quasi-medical sleep-stage +//! ⚠️ classification here is a *candidate* heuristic only: it has never been +//! ⚠️ compared against polysomnography or any sleep-staging reference standard, +//! ⚠️ and its accuracy is unproven (see ADR-160 §A4). NOT a medical device. Do +//! ⚠️ NOT use for sleep diagnosis or any clinical decision. (Registry tag: +//! ⚠️ Exotic / Research.) The DSP is real; the sleep-stage labels are not +//! ⚠️ validated. //! //! # Algorithm //! @@ -113,6 +121,8 @@ pub enum SleepStage { /// Non-contact sleep stage classifier using WiFi CSI physiological signatures. pub struct DreamStageDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Rolling breathing BPM values. breath_hist: CircularBuffer, /// Rolling heart rate BPM values. @@ -152,6 +162,7 @@ pub struct DreamStageDetector { impl DreamStageDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], breath_hist: CircularBuffer::new(), hr_hist: CircularBuffer::new(), phase_buf: CircularBuffer::new(), @@ -192,7 +203,6 @@ impl DreamStageDetector { _variance: f32, presence: i32, ) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_ev = 0usize; self.frame_count += 1; @@ -282,33 +292,25 @@ impl DreamStageDetector { }; // Emit events. - unsafe { - EVENTS[n_ev] = (EVENT_SLEEP_STAGE, self.current_stage as u8 as f32); - } + self.events[n_ev] = (EVENT_SLEEP_STAGE, self.current_stage as u8 as f32); n_ev += 1; // Emit quality periodically (every 20 frames). if self.frame_count % 20 == 0 { - unsafe { - EVENTS[n_ev] = (EVENT_SLEEP_QUALITY, efficiency); - } + self.events[n_ev] = (EVENT_SLEEP_QUALITY, efficiency); n_ev += 1; - unsafe { - EVENTS[n_ev] = (EVENT_DEEP_SLEEP_RATIO, deep_ratio); - } + self.events[n_ev] = (EVENT_DEEP_SLEEP_RATIO, deep_ratio); n_ev += 1; } // Emit REM episode when in REM or just exited. if rem_ep > 0 { - unsafe { - EVENTS[n_ev] = (EVENT_REM_EPISODE, rem_ep as f32); - } + self.events[n_ev] = (EVENT_REM_EPISODE, rem_ep as f32); n_ev += 1; } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Classify the sleep stage from physiological features. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/exo_emotion_detect.rs b/v2/crates/wifi-densepose-wasm-edge/src/exo_emotion_detect.rs index f8e7454e..492aa74c 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/exo_emotion_detect.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/exo_emotion_detect.rs @@ -1,4 +1,13 @@ -//! Affect computing from physiological CSI signatures — ADR-041 exotic module. +//! Affect-proxy heuristic from physiological CSI signatures — ADR-041 exotic module. +//! +//! ⚠️ SPECULATIVE, UNVALIDATED AFFECT HEURISTIC. The outputs of this module +//! ⚠️ (`AROUSAL_LEVEL`, `STRESS_INDEX`, `CALM_DETECTED`, `AGITATION_DETECTED`) +//! ⚠️ are NOT measurements of emotion. They are threshold-based proxies over +//! ⚠️ breathing/motion/heart-rate estimates that have never been correlated +//! ⚠️ against self-report, physiological ground truth, or any reference standard +//! ⚠️ (see ADR-160 §A2). Do NOT use for affect inference, stress screening, or +//! ⚠️ any decision about a person's emotional state. The DSP (rolling statistics +//! ⚠️ + weighted scoring) is real; the affect interpretation of its output is not. //! //! # Algorithm //! @@ -153,6 +162,8 @@ pub struct EmotionDetector { agitation_detected: bool, /// Total frames processed. frame_count: u32, + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], } impl EmotionDetector { @@ -171,6 +182,7 @@ impl EmotionDetector { calm_detected: false, agitation_detected: false, frame_count: 0, + events: [(0, 0.0); 4], } } @@ -192,7 +204,6 @@ impl EmotionDetector { _phase: f32, variance: f32, ) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_ev = 0usize; self.frame_count += 1; @@ -251,31 +262,23 @@ impl EmotionDetector { || breath_cv > STRESS_BREATH_CV_THRESH); // ── Emit events ── - unsafe { - EVENTS[n_ev] = (EVENT_AROUSAL_LEVEL, self.arousal); - } + self.events[n_ev] = (EVENT_AROUSAL_LEVEL, self.arousal); n_ev += 1; - unsafe { - EVENTS[n_ev] = (EVENT_STRESS_INDEX, self.stress_index); - } + self.events[n_ev] = (EVENT_STRESS_INDEX, self.stress_index); n_ev += 1; if self.calm_detected { - unsafe { - EVENTS[n_ev] = (EVENT_CALM_DETECTED, 1.0); - } + self.events[n_ev] = (EVENT_CALM_DETECTED, 1.0); n_ev += 1; } if self.agitation_detected { - unsafe { - EVENTS[n_ev] = (EVENT_AGITATION_DETECTED, 1.0); - } + self.events[n_ev] = (EVENT_AGITATION_DETECTED, 1.0); n_ev += 1; } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Compute breathing rate score [0, 1]. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/exo_gesture_language.rs b/v2/crates/wifi-densepose-wasm-edge/src/exo_gesture_language.rs index c9942b96..3a30ef0f 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/exo_gesture_language.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/exo_gesture_language.rs @@ -1,4 +1,13 @@ -//! Sign language letter recognition from CSI signatures — ADR-041 exotic module. +//! Sign-language-letter-like recognition from CSI signatures — ADR-041 exotic / research module. +//! +//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED. This is a *candidate* +//! ⚠️ coarse gesture-cluster classifier, NOT a validated sign-language +//! ⚠️ recognizer: it has never been evaluated against a labelled ASL (or any +//! ⚠️ sign-language) dataset, accuracy is unproven, and it does not recognize +//! ⚠️ true sign language (see ADR-160 §A4). Do NOT rely on its letter labels +//! ⚠️ for communication or accessibility. (Registry tag: Exotic / Research.) +//! ⚠️ The DSP (feature extraction + template matching) is real; the +//! ⚠️ sign-language interpretation is not validated. //! //! # Algorithm //! @@ -87,6 +96,8 @@ pub const EVENT_GESTURE_REJECTED: i32 = 623; /// Supports up to 26 letter templates loaded via `set_template()`. /// Uses DTW matching on compact feature sequences. pub struct GestureLanguageDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Template feature sequences: [template_idx][frame][feature]. templates: [[[f32; FEAT_DIM]; GESTURE_WIN_LEN]; MAX_TEMPLATES], /// Length of each template (0 = not loaded). @@ -118,6 +129,7 @@ pub struct GestureLanguageDetector { impl GestureLanguageDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], templates: [[[0.0; FEAT_DIM]; GESTURE_WIN_LEN]; MAX_TEMPLATES], template_lens: [0; MAX_TEMPLATES], n_templates: 0, @@ -201,7 +213,6 @@ impl GestureLanguageDetector { motion_energy: f32, presence: i32, ) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_ev = 0usize; self.frame_count += 1; @@ -223,29 +234,21 @@ impl GestureLanguageDetector { if self.gesture_fill >= MIN_GESTURE_FILL && self.gesture_active { let (letter, confidence) = self.match_gesture(); if letter < MAX_TEMPLATES as u8 && self.since_last_letter >= DEBOUNCE_FRAMES { - unsafe { - EVENTS[n_ev] = (EVENT_LETTER_RECOGNIZED, letter as f32); - } + self.events[n_ev] = (EVENT_LETTER_RECOGNIZED, letter as f32); n_ev += 1; - unsafe { - EVENTS[n_ev] = (EVENT_LETTER_CONFIDENCE, confidence); - } + self.events[n_ev] = (EVENT_LETTER_CONFIDENCE, confidence); n_ev += 1; self.last_letter = letter; self.last_confidence = confidence; self.since_last_letter = 0; } else { - unsafe { - EVENTS[n_ev] = (EVENT_GESTURE_REJECTED, 1.0); - } + self.events[n_ev] = (EVENT_GESTURE_REJECTED, 1.0); n_ev += 1; } } // Emit word boundary. - unsafe { - EVENTS[n_ev] = (EVENT_WORD_BOUNDARY, 1.0); - } + self.events[n_ev] = (EVENT_WORD_BOUNDARY, 1.0); n_ev += 1; self.word_boundary_emitted = true; self.reset_gesture(); @@ -264,7 +267,7 @@ impl GestureLanguageDetector { } } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Match the current gesture buffer against all loaded templates. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/exo_ghost_hunter.rs b/v2/crates/wifi-densepose-wasm-edge/src/exo_ghost_hunter.rs index c36e7c13..bd072e02 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/exo_ghost_hunter.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/exo_ghost_hunter.rs @@ -123,6 +123,8 @@ pub enum AnomalyClass { /// Environmental anomaly detector for empty-room CSI monitoring. pub struct GhostHunterDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Noise floor per subcarrier group (slow EWMA of variance). noise_floor: [Ema; N_GROUPS], /// Anomaly energy buffer per group. @@ -158,6 +160,7 @@ pub struct GhostHunterDetector { impl GhostHunterDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], noise_floor: [ Ema::new(NOISE_ALPHA), Ema::new(NOISE_ALPHA), Ema::new(NOISE_ALPHA), Ema::new(NOISE_ALPHA), @@ -203,7 +206,6 @@ impl GhostHunterDetector { presence: i32, motion_energy: f32, ) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_ev = 0usize; self.frame_count += 1; @@ -336,35 +338,27 @@ impl GhostHunterDetector { let norm_energy = if energy > 1.0 { 1.0 } else { energy }; if anomaly_active { - unsafe { - EVENTS[n_ev] = (EVENT_ANOMALY_DETECTED, norm_energy); - } + self.events[n_ev] = (EVENT_ANOMALY_DETECTED, norm_energy); n_ev += 1; if self.current_class != AnomalyClass::None { - unsafe { - EVENTS[n_ev] = (EVENT_ANOMALY_CLASS, self.current_class as u8 as f32); - } + self.events[n_ev] = (EVENT_ANOMALY_CLASS, self.current_class as u8 as f32); n_ev += 1; } } if self.hidden_presence_score > HIDDEN_PRESENCE_THRESHOLD { - unsafe { - EVENTS[n_ev] = (EVENT_HIDDEN_PRESENCE, self.hidden_presence_score); - } + self.events[n_ev] = (EVENT_HIDDEN_PRESENCE, self.hidden_presence_score); n_ev += 1; } if self.drift_frames >= DRIFT_MIN_FRAMES { let drift_mag = fabsf(amp_delta) * self.drift_frames as f32; - unsafe { - EVENTS[n_ev] = (EVENT_ENVIRONMENTAL_DRIFT, drift_mag); - } + self.events[n_ev] = (EVENT_ENVIRONMENTAL_DRIFT, drift_mag); n_ev += 1; } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Check periodicity in the phase buffer via short autocorrelation. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/exo_happiness_score.rs b/v2/crates/wifi-densepose-wasm-edge/src/exo_happiness_score.rs index d4486a47..2fdcff04 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/exo_happiness_score.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/exo_happiness_score.rs @@ -1,12 +1,21 @@ -//! Happiness score from WiFi CSI physiological proxies -- ADR-041 exotic module. +//! Gait-energy / affect-proxy scoring from WiFi CSI -- ADR-041 exotic module. +//! +//! ⚠️ SPECULATIVE, UNVALIDATED AFFECT HEURISTIC. The outputs of this module are +//! ⚠️ NOT measurements of emotion. `HAPPINESS_SCORE` is a gait-energy / movement +//! ⚠️ proxy, not a validated affect measure; it has never been correlated +//! ⚠️ against self-report, facial-affect, or any reference standard, and its +//! ⚠️ relationship to actual mood is unproven (see ADR-160 §A2). Do NOT use for +//! ⚠️ affect inference, screening, or any decision about a person's emotional +//! ⚠️ state. The DSP (rolling statistics + weighted scoring) is real; the affect +//! ⚠️ interpretation of its output is not. //! //! # Algorithm //! -//! Combines six physiological proxies extracted from CSI into a composite -//! happiness score [0, 1]: +//! Combines six movement/physiology proxies extracted from CSI into a composite +//! gait-energy score [0, 1] (labelled `HAPPINESS_SCORE` for the event registry, +//! but it is a proxy, not an affect measurement): //! -//! 1. **Gait speed** -- Doppler proxy from phase rate-of-change. Happy people -//! walk approximately 12% faster than neutral baseline. +//! 1. **Gait speed** -- Doppler proxy from phase rate-of-change. //! //! 2. **Stride regularity** -- Variance of step intervals from successive phase //! differences. Regular strides correlate with confidence and positive affect. @@ -31,7 +40,9 @@ //! //! # Events (690-694: Exotic / Research) //! -//! - `HAPPINESS_SCORE` (690): Composite happiness [0.0 = sad, 0.5 = neutral, 1.0 = happy]. +//! - `HAPPINESS_SCORE` (690): Composite **gait-energy proxy** [0, 1], NOT a +//! validated affect measure. Higher = more energetic/fluid movement, which is +//! only speculatively (unvalidated) associated with positive affect. //! - `GAIT_ENERGY` (691): Normalized gait speed/stride score [0, 1]. //! - `AFFECT_VALENCE` (692): Emotional valence from breathing + motion [0, 1]. //! - `SOCIAL_ENERGY` (693): Group animation/interaction level [0, 1]. @@ -97,7 +108,7 @@ const MAX_SC: usize = 32; const EVENT_DECIMATION: u32 = 4; /// Baseline gait speed (phase rate-of-change, arbitrary units). -/// Happy gait is ~12% above this. +/// Used only as a normalization reference for the gait-energy proxy. const BASELINE_GAIT_SPEED: f32 = 0.5; /// Maximum expected gait speed for normalization. @@ -184,6 +195,9 @@ pub struct HappinessScoreDetector { /// Total frames processed. frame_count: u32, + + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 5], } impl HappinessScoreDetector { @@ -209,6 +223,7 @@ impl HappinessScoreDetector { happiness_vector: [0.0; HAPPINESS_VECTOR_DIM], frame_count: 0, + events: [(0, 0.0); 5], } } @@ -234,7 +249,6 @@ impl HappinessScoreDetector { breathing_bpm: f32, heart_rate_bpm: f32, ) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 5] = [(0, 0.0); 5]; let mut n_ev = 0usize; self.frame_count += 1; @@ -341,34 +355,24 @@ impl HappinessScoreDetector { // ── Emit events (decimated for ESP32 bandwidth) ── // Always emit happiness score; other events only every Nth frame. - unsafe { - EVENTS[n_ev] = (EVENT_HAPPINESS_SCORE, self.happiness); - } + self.events[n_ev] = (EVENT_HAPPINESS_SCORE, self.happiness); n_ev += 1; if self.frame_count % EVENT_DECIMATION == 0 { - unsafe { - EVENTS[n_ev] = (EVENT_GAIT_ENERGY, gait_energy); - } + self.events[n_ev] = (EVENT_GAIT_ENERGY, gait_energy); n_ev += 1; - unsafe { - EVENTS[n_ev] = (EVENT_AFFECT_VALENCE, affect_valence); - } + self.events[n_ev] = (EVENT_AFFECT_VALENCE, affect_valence); n_ev += 1; - unsafe { - EVENTS[n_ev] = (EVENT_SOCIAL_ENERGY, social_energy); - } + self.events[n_ev] = (EVENT_SOCIAL_ENERGY, social_energy); n_ev += 1; - unsafe { - EVENTS[n_ev] = (EVENT_TRANSIT_DIRECTION, transit); - } + self.events[n_ev] = (EVENT_TRANSIT_DIRECTION, transit); n_ev += 1; } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Average phase rate-of-change over the rolling window. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/exo_hyperbolic_space.rs b/v2/crates/wifi-densepose-wasm-edge/src/exo_hyperbolic_space.rs index 9a67e332..a557510e 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/exo_hyperbolic_space.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/exo_hyperbolic_space.rs @@ -88,6 +88,8 @@ pub const EVENT_LOCATION_LABEL: i32 = 687; /// Pre-configured with 16 reference points (4 rooms, 12 zones) and a /// linear projection from 8D CSI features to 2D Poincare disk. pub struct HyperbolicEmbedder { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], /// Reference embeddings on the Poincare disk [N_REFS][DIM]. references: [[f32; DIM]; N_REFS], /// Linear projection matrix W: [DIM][FEAT_DIM] (2x8). @@ -111,6 +113,7 @@ pub struct HyperbolicEmbedder { impl HyperbolicEmbedder { pub const fn new() -> Self { Self { + events: [(0, 0.0); 3], references: Self::default_references(), projection_w: Self::default_projection(), prev_label: 0, @@ -166,7 +169,6 @@ impl HyperbolicEmbedder { /// /// Returns events as `(event_id, value)` pairs. pub fn process_frame(&mut self, amplitudes: &[f32]) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut n_ev = 0usize; if amplitudes.len() < FEAT_DIM { @@ -250,22 +252,16 @@ impl HyperbolicEmbedder { let level: u8 = if radius < LEVEL_RADIUS_THRESHOLD { 0 } else { 1 }; // Emit events. - unsafe { - EVENTS[n_ev] = (EVENT_HIERARCHY_LEVEL, level as f32); - } + self.events[n_ev] = (EVENT_HIERARCHY_LEVEL, level as f32); n_ev += 1; - unsafe { - EVENTS[n_ev] = (EVENT_HYPERBOLIC_RADIUS, radius); - } + self.events[n_ev] = (EVENT_HYPERBOLIC_RADIUS, radius); n_ev += 1; - unsafe { - EVENTS[n_ev] = (EVENT_LOCATION_LABEL, best_label as f32); - } + self.events[n_ev] = (EVENT_LOCATION_LABEL, best_label as f32); n_ev += 1; - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Set a reference embedding. `index` must be < N_REFS. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/exo_music_conductor.rs b/v2/crates/wifi-densepose-wasm-edge/src/exo_music_conductor.rs index 3c5f5add..f79079c1 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/exo_music_conductor.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/exo_music_conductor.rs @@ -99,6 +99,8 @@ pub const EVENT_GESTURE_FERMATA: i32 = 634; /// Extracts tempo, beat position, dynamics, and special gestures from /// WiFi CSI motion patterns. pub struct MusicConductorDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 5], /// Circular buffer of motion energy samples. motion_buf: CircularBuffer, /// Autocorrelation values at lags MIN_LAG..MAX_LAG. @@ -132,6 +134,7 @@ pub struct MusicConductorDetector { impl MusicConductorDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 5], motion_buf: CircularBuffer::new(), autocorr: [0.0; MAX_LAG], tempo_ema: Ema::new(TEMPO_ALPHA), @@ -165,7 +168,6 @@ impl MusicConductorDetector { motion_energy: f32, _variance: f32, ) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 5] = [(0, 0.0); 5]; let mut n_ev = 0usize; self.frame_count += 1; @@ -277,37 +279,27 @@ impl MusicConductorDetector { // ── Emit events ── if self.tempo_ema.is_initialized() { - unsafe { - EVENTS[n_ev] = (EVENT_CONDUCTOR_BPM, self.tempo_ema.value); - } + self.events[n_ev] = (EVENT_CONDUCTOR_BPM, self.tempo_ema.value); n_ev += 1; - unsafe { - EVENTS[n_ev] = (EVENT_BEAT_POSITION, beat_position as f32); - } + self.events[n_ev] = (EVENT_BEAT_POSITION, beat_position as f32); n_ev += 1; } - unsafe { - EVENTS[n_ev] = (EVENT_DYNAMIC_LEVEL, dynamic_level); - } + self.events[n_ev] = (EVENT_DYNAMIC_LEVEL, dynamic_level); n_ev += 1; if self.cutoff_detected { - unsafe { - EVENTS[n_ev] = (EVENT_GESTURE_CUTOFF, 1.0); - } + self.events[n_ev] = (EVENT_GESTURE_CUTOFF, 1.0); n_ev += 1; } if self.fermata_active { - unsafe { - EVENTS[n_ev] = (EVENT_GESTURE_FERMATA, 1.0); - } + self.events[n_ev] = (EVENT_GESTURE_FERMATA, 1.0); n_ev += 1; } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Compute buffer mean and variance (single-pass). diff --git a/v2/crates/wifi-densepose-wasm-edge/src/exo_plant_growth.rs b/v2/crates/wifi-densepose-wasm-edge/src/exo_plant_growth.rs index acbe3be8..d3828e9e 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/exo_plant_growth.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/exo_plant_growth.rs @@ -95,6 +95,8 @@ pub const EVENT_WATERING_EVENT: i32 = 643; /// and phase to detect growth drift, circadian oscillation, wilting, /// and watering events. pub struct PlantGrowthDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Slow EWMA of amplitude per subcarrier group. amp_baseline: [Ema; N_GROUPS], /// Fast EWMA of amplitude per subcarrier group. @@ -124,6 +126,7 @@ pub struct PlantGrowthDetector { impl PlantGrowthDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], amp_baseline: [ Ema::new(BASELINE_ALPHA), Ema::new(BASELINE_ALPHA), Ema::new(BASELINE_ALPHA), Ema::new(BASELINE_ALPHA), @@ -174,7 +177,6 @@ impl PlantGrowthDetector { variance: &[f32], presence: i32, ) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_ev = 0usize; self.frame_count += 1; @@ -264,9 +266,7 @@ impl PlantGrowthDetector { self.drift_interval_count = 0; if fabsf(avg_drift) > GROWTH_THRESHOLD { - unsafe { - EVENTS[n_ev] = (EVENT_GROWTH_RATE, avg_drift); - } + self.events[n_ev] = (EVENT_GROWTH_RATE, avg_drift); n_ev += 1; } } @@ -288,9 +288,7 @@ impl PlantGrowthDetector { if avg_osc > CIRCADIAN_MIN_MAGNITUDE { // Normalize to [0, 1] range (cap at 1.0). let normalized = if avg_osc > 1.0 { 1.0 } else { avg_osc }; - unsafe { - EVENTS[n_ev] = (EVENT_CIRCADIAN_PHASE, normalized); - } + self.events[n_ev] = (EVENT_CIRCADIAN_PHASE, normalized); n_ev += 1; } } @@ -315,9 +313,7 @@ impl PlantGrowthDetector { } // Need majority of groups to agree. if amp_rise_count >= (N_GROUPS / 2) as u8 && var_drop_count >= 2 { - unsafe { - EVENTS[n_ev] = (EVENT_WILT_DETECTED, 1.0); - } + self.events[n_ev] = (EVENT_WILT_DETECTED, 1.0); n_ev += 1; } } @@ -333,14 +329,12 @@ impl PlantGrowthDetector { } } if drop_count >= (N_GROUPS / 2) as u8 { - unsafe { - EVENTS[n_ev] = (EVENT_WATERING_EVENT, 1.0); - } + self.events[n_ev] = (EVENT_WATERING_EVENT, 1.0); n_ev += 1; } } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Get the number of empty-room frames accumulated. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/exo_rain_detect.rs b/v2/crates/wifi-densepose-wasm-edge/src/exo_rain_detect.rs index 79f3b577..8830723f 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/exo_rain_detect.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/exo_rain_detect.rs @@ -99,6 +99,8 @@ pub enum RainIntensity { /// Detects rain from broadband CSI phase variance perturbations. pub struct RainDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], /// Baseline variance per subcarrier group (slow EWMA). baseline_var: [Ema; N_GROUPS], /// Short-term variance per subcarrier group (fast EWMA). @@ -122,6 +124,7 @@ pub struct RainDetector { impl RainDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 3], baseline_var: [ Ema::new(BASELINE_ALPHA), Ema::new(BASELINE_ALPHA), Ema::new(BASELINE_ALPHA), Ema::new(BASELINE_ALPHA), @@ -159,7 +162,6 @@ impl RainDetector { amplitudes: &[f32], presence: i32, ) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut n_ev = 0usize; self.frame_count += 1; @@ -250,9 +252,7 @@ impl RainDetector { // Onset: was not raining, now have enough consecutive rain frames. if !self.raining && self.rain_frames >= ONSET_FRAMES { self.raining = true; - unsafe { - EVENTS[n_ev] = (EVENT_RAIN_ONSET, 1.0); - } + self.events[n_ev] = (EVENT_RAIN_ONSET, 1.0); n_ev += 1; } @@ -260,9 +260,7 @@ impl RainDetector { if was_raining && self.quiet_frames >= CESSATION_FRAMES { self.raining = false; self.intensity = RainIntensity::None; - unsafe { - EVENTS[n_ev] = (EVENT_RAIN_CESSATION, 1.0); - } + self.events[n_ev] = (EVENT_RAIN_CESSATION, 1.0); n_ev += 1; } @@ -277,13 +275,11 @@ impl RainDetector { RainIntensity::Heavy }; - unsafe { - EVENTS[n_ev] = (EVENT_RAIN_INTENSITY, self.intensity as u8 as f32); - } + self.events[n_ev] = (EVENT_RAIN_INTENSITY, self.intensity as u8 as f32); n_ev += 1; } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Whether rain is currently detected. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/exo_time_crystal.rs b/v2/crates/wifi-densepose-wasm-edge/src/exo_time_crystal.rs index b900388a..895bab85 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/exo_time_crystal.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/exo_time_crystal.rs @@ -74,6 +74,8 @@ pub const EVENT_COORDINATION_INDEX: i32 = 682; /// Samples `motion_energy` into a circular buffer and runs autocorrelation /// to detect period doubling and multi-person temporal coordination. pub struct TimeCrystalDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], /// Circular buffer of motion energy samples. motion_buf: CircularBuffer, /// Autocorrelation values at lags 1..MAX_LAG. @@ -101,6 +103,7 @@ pub struct TimeCrystalDetector { impl TimeCrystalDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 3], motion_buf: CircularBuffer::new(), autocorr: [0.0; MAX_LAG], last_multiplier: 0, @@ -119,7 +122,6 @@ impl TimeCrystalDetector { /// /// Returns events as `(event_id, value)` pairs in a static buffer. pub fn process_frame(&mut self, motion_energy: f32) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut n_ev = 0usize; // Push sample into circular buffer. @@ -216,25 +218,19 @@ impl TimeCrystalDetector { // Emit events. if detected_multiplier > 0 { - unsafe { - EVENTS[n_ev] = (EVENT_CRYSTAL_DETECTED, detected_multiplier as f32); - } + self.events[n_ev] = (EVENT_CRYSTAL_DETECTED, detected_multiplier as f32); n_ev += 1; } - unsafe { - EVENTS[n_ev] = (EVENT_CRYSTAL_STABILITY, self.stability_ema.value); - } + self.events[n_ev] = (EVENT_CRYSTAL_STABILITY, self.stability_ema.value); n_ev += 1; if coordination > 0 { - unsafe { - EVENTS[n_ev] = (EVENT_COORDINATION_INDEX, coordination as f32); - } + self.events[n_ev] = (EVENT_COORDINATION_INDEX, coordination as f32); n_ev += 1; } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Compute mean and variance of the circular buffer contents. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ind_clean_room.rs b/v2/crates/wifi-densepose-wasm-edge/src/ind_clean_room.rs index 8688950a..8963d0b4 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ind_clean_room.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ind_clean_room.rs @@ -41,6 +41,8 @@ pub const EVENT_COMPLIANCE_REPORT: i32 = 523; /// Clean room monitor. pub struct CleanRoomMonitor { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Maximum allowed occupancy. max_occupancy: u8, /// Current smoothed person count. @@ -70,6 +72,7 @@ pub struct CleanRoomMonitor { impl CleanRoomMonitor { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], max_occupancy: DEFAULT_MAX_OCCUPANCY, current_count: 0, prev_count: 0, @@ -88,6 +91,7 @@ impl CleanRoomMonitor { /// Create with custom maximum occupancy. pub const fn with_max_occupancy(max: u8) -> Self { Self { + events: [(0, 0.0); 4], max_occupancy: max, current_count: 0, prev_count: 0, @@ -146,12 +150,11 @@ impl CleanRoomMonitor { } } - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_events = 0usize; // --- Step 1: Emit count changes --- if count != self.prev_count && n_events < 4 { - unsafe { EVENTS[n_events] = (EVENT_OCCUPANCY_COUNT, count as f32); } + self.events[n_events] = (EVENT_OCCUPANCY_COUNT, count as f32); n_events += 1; } @@ -166,7 +169,7 @@ impl CleanRoomMonitor { self.violation_cooldown = VIOLATION_COOLDOWN; // Value encodes: count * 10 + max_allowed. let val = count as f32; - unsafe { EVENTS[n_events] = (EVENT_OCCUPANCY_VIOLATION, val); } + self.events[n_events] = (EVENT_OCCUPANCY_VIOLATION, val); n_events += 1; } } else { @@ -182,7 +185,7 @@ impl CleanRoomMonitor { { self.total_turbulent += 1; self.turbulent_cooldown = TURBULENT_COOLDOWN; - unsafe { EVENTS[n_events] = (EVENT_TURBULENT_MOTION, motion_energy); } + self.events[n_events] = (EVENT_TURBULENT_MOTION, motion_energy); n_events += 1; } } else { @@ -196,11 +199,11 @@ impl CleanRoomMonitor { } else { 100.0 }; - unsafe { EVENTS[n_events] = (EVENT_COMPLIANCE_REPORT, compliance_pct); } + self.events[n_events] = (EVENT_COMPLIANCE_REPORT, compliance_pct); n_events += 1; } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Current occupancy count. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ind_confined_space.rs b/v2/crates/wifi-densepose-wasm-edge/src/ind_confined_space.rs index 34bdc7c8..17bc8cf6 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ind_confined_space.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ind_confined_space.rs @@ -55,6 +55,8 @@ pub enum WorkerState { /// Confined space monitor. pub struct ConfinedSpaceMonitor { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Current worker state. state: WorkerState, /// Presence debounce counters. @@ -79,6 +81,7 @@ pub struct ConfinedSpaceMonitor { impl ConfinedSpaceMonitor { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], state: WorkerState::Empty, present_count: 0, absent_count: 0, @@ -110,7 +113,6 @@ impl ConfinedSpaceMonitor { ) -> &[(i32, f32)] { self.frame_count += 1; - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_events = 0usize; // --- Step 1: Debounced presence detection --- @@ -141,7 +143,7 @@ impl ConfinedSpaceMonitor { self.extraction_alerted = false; self.immobile_alerted = false; if n_events < 4 { - unsafe { EVENTS[n_events] = (EVENT_WORKER_ENTRY, 1.0); } + self.events[n_events] = (EVENT_WORKER_ENTRY, 1.0); n_events += 1; } } @@ -150,7 +152,7 @@ impl ConfinedSpaceMonitor { if !self.worker_inside && was_inside { self.state = WorkerState::Empty; if n_events < 4 { - unsafe { EVENTS[n_events] = (EVENT_WORKER_EXIT, 1.0); } + self.events[n_events] = (EVENT_WORKER_EXIT, 1.0); n_events += 1; } } @@ -169,7 +171,7 @@ impl ConfinedSpaceMonitor { // Periodic breathing confirmation. if self.frame_count % BREATHING_REPORT_INTERVAL == 0 && n_events < 4 { - unsafe { EVENTS[n_events] = (EVENT_BREATHING_OK, breathing_bpm); } + self.events[n_events] = (EVENT_BREATHING_OK, breathing_bpm); n_events += 1; } } else { @@ -197,7 +199,7 @@ impl ConfinedSpaceMonitor { self.state = WorkerState::BreathingCeased; self.extraction_alerted = true; let seconds = self.no_breathing_frames as f32 / 20.0; - unsafe { EVENTS[n_events] = (EVENT_EXTRACTION_ALERT, seconds); } + self.events[n_events] = (EVENT_EXTRACTION_ALERT, seconds); n_events += 1; } @@ -209,12 +211,12 @@ impl ConfinedSpaceMonitor { self.state = WorkerState::Immobile; self.immobile_alerted = true; let seconds = self.no_motion_frames as f32 / 20.0; - unsafe { EVENTS[n_events] = (EVENT_IMMOBILE_ALERT, seconds); } + self.events[n_events] = (EVENT_IMMOBILE_ALERT, seconds); n_events += 1; } } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Current worker state. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ind_forklift_proximity.rs b/v2/crates/wifi-densepose-wasm-edge/src/ind_forklift_proximity.rs index 8786afc3..1761727c 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ind_forklift_proximity.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ind_forklift_proximity.rs @@ -59,6 +59,8 @@ pub const EVENT_HUMAN_NEAR_VEHICLE: i32 = 502; /// Forklift proximity detector. pub struct ForkliftProximityDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Per-subcarrier baseline amplitude (calibrated). baseline_amp: [f32; MAX_SC], /// Phase history ring buffer for frequency analysis. @@ -83,6 +85,7 @@ pub struct ForkliftProximityDetector { impl ForkliftProximityDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], baseline_amp: [0.0; MAX_SC], phase_history: [[0.0; MAX_SC]; PHASE_HISTORY], phase_hist_idx: 0, @@ -139,7 +142,6 @@ impl ForkliftProximityDetector { self.phase_hist_len += 1; } - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_events = 0usize; // Calibration phase: 100 frames (~5 seconds). @@ -158,7 +160,7 @@ impl ForkliftProximityDetector { } self.calibrated = true; } - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } // --- Step 1: Detect forklift/AGV signature --- @@ -182,9 +184,7 @@ impl ForkliftProximityDetector { // Emit vehicle detected on transition. if self.vehicle_present && !was_vehicle && n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_VEHICLE_DETECTED, amp_ratio); - } + self.events[n_events] = (EVENT_VEHICLE_DETECTED, amp_ratio); n_events += 1; } @@ -197,9 +197,7 @@ impl ForkliftProximityDetector { // Emit human-near-vehicle event on transition (debounce threshold reached). if self.proximity_debounce == PROXIMITY_DEBOUNCE && n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_HUMAN_NEAR_VEHICLE, motion_energy); - } + self.events[n_events] = (EVENT_HUMAN_NEAR_VEHICLE, motion_energy); n_events += 1; } @@ -215,9 +213,7 @@ impl ForkliftProximityDetector { } else { 2.0 // caution }; - unsafe { - EVENTS[n_events] = (EVENT_PROXIMITY_WARNING, dist_cat); - } + self.events[n_events] = (EVENT_PROXIMITY_WARNING, dist_cat); n_events += 1; self.cooldown = ALERT_COOLDOWN; } @@ -225,7 +221,7 @@ impl ForkliftProximityDetector { self.proximity_debounce = 0; } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Compute mean amplitude ratio vs baseline across subcarriers. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ind_livestock_monitor.rs b/v2/crates/wifi-densepose-wasm-edge/src/ind_livestock_monitor.rs index 48fa6e75..7d4d265f 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ind_livestock_monitor.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ind_livestock_monitor.rs @@ -72,6 +72,8 @@ impl Species { /// Livestock monitor. pub struct LivestockMonitor { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Configured species. species: Species, /// Whether animal is currently detected (debounced). @@ -97,6 +99,7 @@ pub struct LivestockMonitor { impl LivestockMonitor { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], species: Species::Cattle, animal_present: false, presence_frames: 0, @@ -113,6 +116,7 @@ impl LivestockMonitor { /// Create with a specific species. pub const fn with_species(species: Species) -> Self { Self { + events: [(0, 0.0); 4], species, animal_present: false, presence_frames: 0, @@ -148,7 +152,6 @@ impl LivestockMonitor { self.escape_cooldown -= 1; } - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_events = 0usize; let raw_present = presence > 0 || motion_energy > MIN_MOTION_ACTIVE; @@ -177,7 +180,7 @@ impl LivestockMonitor { { self.escape_cooldown = ESCAPE_COOLDOWN; let minutes_present = self.presence_frames as f32 / (20.0 * 60.0); - unsafe { EVENTS[n_events] = (EVENT_ESCAPE_ALERT, minutes_present); } + self.events[n_events] = (EVENT_ESCAPE_ALERT, minutes_present); n_events += 1; } @@ -190,7 +193,7 @@ impl LivestockMonitor { && self.frame_count % PRESENCE_REPORT_INTERVAL == 0 && n_events < 4 { - unsafe { EVENTS[n_events] = (EVENT_ANIMAL_PRESENT, breathing_bpm); } + self.events[n_events] = (EVENT_ANIMAL_PRESENT, breathing_bpm); n_events += 1; } @@ -209,7 +212,7 @@ impl LivestockMonitor { { self.stillness_alerted = true; let minutes_still = self.still_frames as f32 / (20.0 * 60.0); - unsafe { EVENTS[n_events] = (EVENT_ABNORMAL_STILLNESS, minutes_still); } + self.events[n_events] = (EVENT_ABNORMAL_STILLNESS, minutes_still); n_events += 1; } } @@ -226,7 +229,7 @@ impl LivestockMonitor { if is_labored { self.labored_debounce = self.labored_debounce.saturating_add(1); if self.labored_debounce >= LABORED_DEBOUNCE && n_events < 4 { - unsafe { EVENTS[n_events] = (EVENT_LABORED_BREATHING, breathing_bpm); } + self.events[n_events] = (EVENT_LABORED_BREATHING, breathing_bpm); n_events += 1; self.labored_debounce = 0; // Reset to allow repeated alerts. } @@ -235,7 +238,7 @@ impl LivestockMonitor { } } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Whether an animal is currently detected. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ind_structural_vibration.rs b/v2/crates/wifi-densepose-wasm-edge/src/ind_structural_vibration.rs index 25317bca..acdf44fa 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ind_structural_vibration.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ind_structural_vibration.rs @@ -72,6 +72,8 @@ pub const EVENT_VIBRATION_SPECTRUM: i32 = 543; /// Structural vibration monitor. pub struct StructuralVibrationMonitor { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Phase history ring buffer [time][subcarrier]. phase_history: [[f32; MAX_SC]; PHASE_HISTORY_LEN], hist_idx: usize, @@ -104,6 +106,7 @@ pub struct StructuralVibrationMonitor { impl StructuralVibrationMonitor { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], phase_history: [[0.0; MAX_SC]; PHASE_HISTORY_LEN], hist_idx: 0, hist_len: 0, @@ -162,7 +165,6 @@ impl StructuralVibrationMonitor { self.hist_len += 1; } - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_events = 0usize; // --- Calibration: establish baseline when space is empty --- @@ -180,7 +182,7 @@ impl StructuralVibrationMonitor { self.baseline_set = true; } } - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } // Only analyze when unoccupied (human presence masks structural signals). @@ -191,7 +193,7 @@ impl StructuralVibrationMonitor { self.drift_direction[i] = 0; self.drift_accumulator[i] = 0.0; } - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } // --- Step 1: Compute phase deviation RMS --- @@ -209,7 +211,7 @@ impl StructuralVibrationMonitor { && n_events < 4 { self.seismic_cooldown = SEISMIC_COOLDOWN; - unsafe { EVENTS[n_events] = (EVENT_SEISMIC_DETECTED, rms); } + self.events[n_events] = (EVENT_SEISMIC_DETECTED, rms); n_events += 1; } } @@ -235,7 +237,7 @@ impl StructuralVibrationMonitor { } else { 0.0 }; - unsafe { EVENTS[n_events] = (EVENT_MECHANICAL_RESONANCE, freq); } + self.events[n_events] = (EVENT_MECHANICAL_RESONANCE, freq); n_events += 1; } } else { @@ -253,7 +255,7 @@ impl StructuralVibrationMonitor { if fabsf(avg_drift) > DRIFT_RATE_THRESH { self.drift_cooldown = DRIFT_COOLDOWN; // Value is drift rate in rad/second. - unsafe { EVENTS[n_events] = (EVENT_STRUCTURAL_DRIFT, avg_drift * 20.0); } + self.events[n_events] = (EVENT_STRUCTURAL_DRIFT, avg_drift * 20.0); n_events += 1; } } @@ -263,11 +265,11 @@ impl StructuralVibrationMonitor { && self.hist_len >= MAX_LAGS + 1 && n_events < 4 { - unsafe { EVENTS[n_events] = (EVENT_VIBRATION_SPECTRUM, rms); } + self.events[n_events] = (EVENT_VIBRATION_SPECTRUM, rms); n_events += 1; } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Compute RMS phase deviation from baseline. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/intrusion.rs b/v2/crates/wifi-densepose-wasm-edge/src/intrusion.rs index f706c661..f8e4b282 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/intrusion.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/intrusion.rs @@ -57,6 +57,8 @@ pub enum DetectorState { /// Intrusion detector. pub struct IntrusionDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Per-subcarrier baseline amplitude. baseline_amp: [f32; MAX_SC], /// Per-subcarrier baseline variance. @@ -86,6 +88,7 @@ pub struct IntrusionDetector { impl IntrusionDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], baseline_amp: [0.0; MAX_SC], baseline_var: [0.0; MAX_SC], prev_phases: [0.0; MAX_SC], @@ -119,7 +122,6 @@ impl IntrusionDetector { self.cooldown -= 1; } - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_events = 0usize; match self.state { @@ -165,9 +167,7 @@ impl IntrusionDetector { if self.quiet_frames >= ARM_FRAMES { self.state = DetectorState::Armed; if n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_INTRUSION_ARMED, 1.0); - } + self.events[n_events] = (EVENT_INTRUSION_ARMED, 1.0); n_events += 1; } } @@ -190,18 +190,14 @@ impl IntrusionDetector { self.cooldown = ALERT_COOLDOWN; if n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_INTRUSION_ALERT, disturbance); - } + self.events[n_events] = (EVENT_INTRUSION_ALERT, disturbance); n_events += 1; } // Find the most disturbed zone. let zone = self.find_disturbed_zone(amplitudes, n_sc); if n_events < 4 { - unsafe { - EVENTS[n_events] = (EVENT_INTRUSION_ZONE, zone as f32); - } + self.events[n_events] = (EVENT_INTRUSION_ZONE, zone as f32); n_events += 1; } } @@ -235,7 +231,7 @@ impl IntrusionDetector { } } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Compute overall disturbance score. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/lib.rs b/v2/crates/wifi-densepose-wasm-edge/src/lib.rs index f06cd1ee..b38b1308 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/lib.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/lib.rs @@ -46,10 +46,20 @@ pub mod vital_trend; pub mod intrusion; // ── Category 1: Medical & Health (ADR-041, event IDs 100-199) ─────────────── +// +// ⚠️ EXPERIMENTAL — NOT clinically validated, NOT medical devices (ADR-160 §A1). +// Gated behind the non-default `medical-experimental` feature so they cannot be +// silently built into a shipping artifact. The DSP is real; the clinical claim +// surface is not. See each module's header disclaimer. +#[cfg(feature = "medical-experimental")] pub mod med_sleep_apnea; +#[cfg(feature = "medical-experimental")] pub mod med_cardiac_arrhythmia; +#[cfg(feature = "medical-experimental")] pub mod med_respiratory_distress; +#[cfg(feature = "medical-experimental")] pub mod med_gait_analysis; +#[cfg(feature = "medical-experimental")] pub mod med_seizure_detect; // ── Category 2: Security & Safety (ADR-041, event IDs 200-299) ────────────── @@ -228,9 +238,11 @@ pub mod event_types { pub const DEPARTURE_DETECTED: i32 = 212; pub const SEC_ZONE_TRANSITION: i32 = 213; - // sec_weapon_detect (220-222) + // sec_weapon_detect (220-222) — ADR-160 §A3: honest physical-quantity names. + // `WEAPON_ALERT` was renamed to `HIGH_METAL_REFLECTIVITY`: a variance ratio + // measures RF reflectivity, not weapon-grade discrimination. pub const METAL_ANOMALY: i32 = 220; - pub const WEAPON_ALERT: i32 = 221; + pub const HIGH_METAL_REFLECTIVITY: i32 = 221; pub const CALIBRATION_NEEDED: i32 = 222; // sec_tailgating (230-232) diff --git a/v2/crates/wifi-densepose-wasm-edge/src/lrn_anomaly_attractor.rs b/v2/crates/wifi-densepose-wasm-edge/src/lrn_anomaly_attractor.rs index 2ccbd62f..80634a2f 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/lrn_anomaly_attractor.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/lrn_anomaly_attractor.rs @@ -71,6 +71,8 @@ type StateVec = [f32; STATE_DIM]; /// Attractor-based anomaly detector. pub struct AttractorDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Circular trajectory buffer. trajectory: [StateVec; TRAJ_LEN], /// Write index into trajectory buffer. @@ -108,6 +110,7 @@ pub struct AttractorDetector { impl AttractorDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], trajectory: [[0.0; STATE_DIM]; TRAJ_LEN], traj_idx: 0, traj_len: 0, @@ -137,7 +140,6 @@ impl AttractorDetector { amplitudes: &[f32], motion_energy: f32, ) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_ev = 0usize; let n_sc = phases.len().min(amplitudes.len()); @@ -200,16 +202,14 @@ impl AttractorDetector { self.radius = 0.01; } - unsafe { - EVENTS[n_ev] = (EVENT_LEARNING_COMPLETE, 1.0); - n_ev += 1; - EVENTS[n_ev] = (EVENT_ATTRACTOR_TYPE, self.attractor_type as u8 as f32); - n_ev += 1; - EVENTS[n_ev] = (EVENT_LYAPUNOV_EXPONENT, lambda); - n_ev += 1; - } + self.events[n_ev] = (EVENT_LEARNING_COMPLETE, 1.0); + n_ev += 1; + self.events[n_ev] = (EVENT_ATTRACTOR_TYPE, self.attractor_type as u8 as f32); + n_ev += 1; + self.events[n_ev] = (EVENT_LYAPUNOV_EXPONENT, lambda); + n_ev += 1; - return unsafe { &EVENTS[..n_ev] }; + return &self.events[..n_ev]; } return &[]; @@ -221,10 +221,8 @@ impl AttractorDetector { if dist > departure_threshold && self.cooldown == 0 { self.cooldown = DEPARTURE_COOLDOWN; - unsafe { - EVENTS[n_ev] = (EVENT_BASIN_DEPARTURE, dist / self.radius); - n_ev += 1; - } + self.events[n_ev] = (EVENT_BASIN_DEPARTURE, dist / self.radius); + n_ev += 1; } // ── Periodic attractor update (every 200 frames) ──────────────── @@ -234,16 +232,14 @@ impl AttractorDetector { if new_type != self.attractor_type && n_ev < 3 { self.attractor_type = new_type; - unsafe { - EVENTS[n_ev] = (EVENT_ATTRACTOR_TYPE, new_type as u8 as f32); - n_ev += 1; - EVENTS[n_ev] = (EVENT_LYAPUNOV_EXPONENT, lambda); - n_ev += 1; - } + self.events[n_ev] = (EVENT_ATTRACTOR_TYPE, new_type as u8 as f32); + n_ev += 1; + self.events[n_ev] = (EVENT_LYAPUNOV_EXPONENT, lambda); + n_ev += 1; } } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Compute the current largest Lyapunov exponent estimate. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/lrn_dtw_gesture_learn.rs b/v2/crates/wifi-densepose-wasm-edge/src/lrn_dtw_gesture_learn.rs index 6c02c654..26c172df 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/lrn_dtw_gesture_learn.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/lrn_dtw_gesture_learn.rs @@ -85,6 +85,8 @@ impl Template { /// User-teachable gesture learner and recognizer. pub struct GestureLearner { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], // ── Stored templates ───────────────────────────────────────────────── templates: [Template; MAX_TEMPLATES], template_count: usize, @@ -117,6 +119,7 @@ pub struct GestureLearner { impl GestureLearner { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], templates: [Template::empty(); MAX_TEMPLATES], template_count: 0, learn_phase: LearnPhase::Idle, @@ -143,7 +146,6 @@ impl GestureLearner { /// /// Returns events as `(event_id, value)` pairs in a static buffer. pub fn process_frame(&mut self, phases: &[f32], motion_energy: f32) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_ev = 0usize; if phases.is_empty() { @@ -228,12 +230,10 @@ impl GestureLearner { // Check if all 3 rehearsals are mutually similar. if self.rehearsals_are_similar() { if let Some(id) = self.commit_template() { - unsafe { - EVENTS[n_ev] = (EVENT_GESTURE_LEARNED, id as f32); - n_ev += 1; - EVENTS[n_ev] = (EVENT_TEMPLATE_COUNT, self.template_count as f32); - n_ev += 1; - } + self.events[n_ev] = (EVENT_GESTURE_LEARNED, id as f32); + n_ev += 1; + self.events[n_ev] = (EVENT_TEMPLATE_COUNT, self.template_count as f32); + n_ev += 1; } } // Reset learning state regardless. @@ -284,18 +284,16 @@ impl GestureLearner { if let Some(id) = best_id { self.cooldown = MATCH_COOLDOWN; - unsafe { - EVENTS[n_ev] = (EVENT_GESTURE_MATCHED, id as f32); + self.events[n_ev] = (EVENT_GESTURE_MATCHED, id as f32); + n_ev += 1; + if n_ev < 4 { + self.events[n_ev] = (EVENT_MATCH_DISTANCE, best_dist); n_ev += 1; - if n_ev < 4 { - EVENTS[n_ev] = (EVENT_MATCH_DISTANCE, best_dist); - n_ev += 1; - } } } } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Check if all rehearsals are pairwise similar (DTW distance < threshold). diff --git a/v2/crates/wifi-densepose-wasm-edge/src/lrn_ewc_lifelong.rs b/v2/crates/wifi-densepose-wasm-edge/src/lrn_ewc_lifelong.rs index c7758324..f248ba68 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/lrn_ewc_lifelong.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/lrn_ewc_lifelong.rs @@ -99,6 +99,8 @@ pub const EVENT_FORGETTING_RISK: i32 = 748; /// Elastic Weight Consolidation lifelong on-device learner. pub struct EwcLifelong { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Current learnable parameters [N_PARAMS] (flattened [N_OUTPUT][N_INPUT]). params: [f32; N_PARAMS], /// Fisher Information diagonal [N_PARAMS]. @@ -128,6 +130,7 @@ pub struct EwcLifelong { impl EwcLifelong { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], params: Self::default_params(), fisher: [0.0; N_PARAMS], theta_star: [0.0; N_PARAMS], @@ -169,7 +172,6 @@ impl EwcLifelong { /// /// Returns events as `(event_id, value)` pairs. pub fn process_frame(&mut self, features: &[f32], target_zone: i32) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_ev = 0usize; if features.len() < N_INPUT { @@ -217,17 +219,13 @@ impl EwcLifelong { && self.task_count < MAX_TASKS { self.commit_task(); - unsafe { - EVENTS[n_ev] = (EVENT_NEW_TASK_LEARNED, self.task_count as f32); - } + self.events[n_ev] = (EVENT_NEW_TASK_LEARNED, self.task_count as f32); n_ev += 1; // Emit mean Fisher value. let mean_fisher = self.mean_fisher(); if n_ev < 4 { - unsafe { - EVENTS[n_ev] = (EVENT_FISHER_UPDATE, mean_fisher); - } + self.events[n_ev] = (EVENT_FISHER_UPDATE, mean_fisher); n_ev += 1; } } @@ -235,9 +233,7 @@ impl EwcLifelong { // Periodic reporting. if self.frame_count % REPORT_INTERVAL == 0 { if n_ev < 4 { - unsafe { - EVENTS[n_ev] = (EVENT_KNOWLEDGE_RETAINED, ewc_penalty); - } + self.events[n_ev] = (EVENT_KNOWLEDGE_RETAINED, ewc_penalty); n_ev += 1; } @@ -248,15 +244,13 @@ impl EwcLifelong { 0.0 }; if n_ev < 4 { - unsafe { - EVENTS[n_ev] = (EVENT_FORGETTING_RISK, risk); - } + self.events[n_ev] = (EVENT_FORGETTING_RISK, risk); n_ev += 1; } } } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Forward pass: linear classifier `output = params * features`. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/lrn_meta_adapt.rs b/v2/crates/wifi-densepose-wasm-edge/src/lrn_meta_adapt.rs index 3c15db52..bf975f02 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/lrn_meta_adapt.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/lrn_meta_adapt.rs @@ -85,6 +85,8 @@ enum OptPhase { /// Meta-learning parameter optimizer. pub struct MetaAdapter { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Tunable parameters. params: [TunableParam; NUM_PARAMS], @@ -140,6 +142,7 @@ impl MetaAdapter { /// 7: intrusion_sensitivity (0.30, range 0.05-0.9) pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], params: [ TunableParam::new(0.05, 0.01, 0.50, 0.01), TunableParam::new(0.10, 0.02, 1.00, 0.02), @@ -198,7 +201,6 @@ impl MetaAdapter { /// /// Returns events as `(event_id, value)` pairs. pub fn on_timer(&mut self) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n_ev = 0usize; self.eval_ticks += 1; @@ -228,16 +230,14 @@ impl MetaAdapter { self.consecutive_failures = 0; self.success_count += 1; - unsafe { - EVENTS[n_ev] = ( - EVENT_PARAM_ADJUSTED, - self.current_param as f32 - + self.params[self.current_param].value / 1000.0, - ); - n_ev += 1; - EVENTS[n_ev] = (EVENT_ADAPTATION_SCORE, score); - n_ev += 1; - } + self.events[n_ev] = ( + EVENT_PARAM_ADJUSTED, + self.current_param as f32 + + self.params[self.current_param].value / 1000.0, + ); + n_ev += 1; + self.events[n_ev] = (EVENT_ADAPTATION_SCORE, score); + n_ev += 1; } else { // Revert the perturbation. self.params[self.current_param].value = @@ -248,10 +248,8 @@ impl MetaAdapter { // ── Safety rollback ────────────────────────────────── if self.consecutive_failures >= MAX_CONSECUTIVE_FAILURES { self.safety_rollback(); - unsafe { - EVENTS[n_ev] = (EVENT_ROLLBACK_TRIGGERED, self.meta_level as f32); - n_ev += 1; - } + self.events[n_ev] = (EVENT_ROLLBACK_TRIGGERED, self.meta_level as f32); + n_ev += 1; } // ── Advance to next parameter ──────────────────────── @@ -261,16 +259,14 @@ impl MetaAdapter { // ── Emit meta level periodically ───────────────────── if self.sweep_idx == 0 && n_ev < 4 { - unsafe { - EVENTS[n_ev] = (EVENT_META_LEVEL, self.meta_level as f32); - n_ev += 1; - } + self.events[n_ev] = (EVENT_META_LEVEL, self.meta_level as f32); + n_ev += 1; } } } } - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } /// Compute the performance score from accumulated feedback. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/med_cardiac_arrhythmia.rs b/v2/crates/wifi-densepose-wasm-edge/src/med_cardiac_arrhythmia.rs index eb58aaec..0e89c004 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/med_cardiac_arrhythmia.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/med_cardiac_arrhythmia.rs @@ -1,10 +1,20 @@ -//! Cardiac arrhythmia detection — ADR-041 Category 1 Medical module. +//! Cardiac-rhythm anomaly flagging — ADR-041 Category 1 Medical module. //! -//! Monitors heart rate from host CSI pipeline and detects: -//! - Tachycardia: sustained HR > 100 BPM -//! - Bradycardia: sustained HR < 50 BPM -//! - Missed beats: sudden HR dips > 30% below running average -//! - HRV anomaly: RMSSD outside normal range over 30-second window +//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED AGAINST CLINICAL DATA. +//! ⚠️ NOT A MEDICAL DEVICE. Do NOT use for diagnosis or patient monitoring. +//! ⚠️ This module flags *candidate* arrhythmia-like heart-rate signatures only +//! ⚠️ (sustained high/low rate estimates, abrupt drops, variability proxies); +//! ⚠️ it has never been compared against ECG or any reference standard, and its +//! ⚠️ accuracy is unproven (see ADR-160 §A1). Gated behind the non-default +//! ⚠️ `medical-experimental` cargo feature. +//! +//! Monitors a heart-rate estimate from the host CSI pipeline and flags: +//! - Tachycardia-like: sustained rate estimate > 100 BPM +//! - Bradycardia-like: sustained rate estimate < 50 BPM +//! - Missed-beat-like: sudden rate dips > 30% below running average +//! - HRV-like anomaly: RMSSD proxy outside a coarse band over 30 seconds +//! +//! These are experimental signal proxies, NOT clinical measurements. //! //! Events: //! TACHYCARDIA (110) — sustained high heart rate @@ -87,6 +97,8 @@ pub struct CardiacArrhythmiaDetector { cd_hrv: u16, /// Frame counter. frame_count: u32, + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], } impl CardiacArrhythmiaDetector { @@ -106,6 +118,7 @@ impl CardiacArrhythmiaDetector { cd_missed: 0, cd_hrv: 0, frame_count: 0, + events: [(0, 0.0); 4], } } @@ -122,14 +135,13 @@ impl CardiacArrhythmiaDetector { self.cd_missed = self.cd_missed.saturating_sub(1); self.cd_hrv = self.cd_hrv.saturating_sub(1); - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n = 0usize; // Ignore invalid / zero / NaN readings. // NaN comparisons return false, so we must check explicitly to prevent // NaN from contaminating the EMA and RMSSD calculations. if !(hr_bpm >= 1.0) { - return unsafe { &EVENTS[..n] }; + return &self.events[..n]; } // ── EMA update ────────────────────────────────────────────────── @@ -156,7 +168,7 @@ impl CardiacArrhythmiaDetector { if hr_bpm > TACHY_THRESH { self.tachy_count = self.tachy_count.saturating_add(1); if self.tachy_count >= SUSTAINED_SECS && self.cd_tachy == 0 && n < 4 { - unsafe { EVENTS[n] = (EVENT_TACHYCARDIA, hr_bpm); } + self.events[n] = (EVENT_TACHYCARDIA, hr_bpm); n += 1; self.cd_tachy = COOLDOWN_SECS; } @@ -168,7 +180,7 @@ impl CardiacArrhythmiaDetector { if hr_bpm < BRADY_THRESH { self.brady_count = self.brady_count.saturating_add(1); if self.brady_count >= SUSTAINED_SECS && self.cd_brady == 0 && n < 4 { - unsafe { EVENTS[n] = (EVENT_BRADYCARDIA, hr_bpm); } + self.events[n] = (EVENT_BRADYCARDIA, hr_bpm); n += 1; self.cd_brady = COOLDOWN_SECS; } @@ -180,7 +192,7 @@ impl CardiacArrhythmiaDetector { if self.ema_init && self.hr_ema > 1.0 { let drop_frac = (self.hr_ema - hr_bpm) / self.hr_ema; if drop_frac > MISSED_BEAT_DROP && self.cd_missed == 0 && n < 4 { - unsafe { EVENTS[n] = (EVENT_MISSED_BEAT, hr_bpm); } + self.events[n] = (EVENT_MISSED_BEAT, hr_bpm); n += 1; self.cd_missed = COOLDOWN_SECS; } @@ -190,13 +202,13 @@ impl CardiacArrhythmiaDetector { if self.rr_len >= HRV_WINDOW && n < 4 { let rmssd = self.compute_rmssd(); if (rmssd < RMSSD_LOW || rmssd > RMSSD_HIGH) && self.cd_hrv == 0 { - unsafe { EVENTS[n] = (EVENT_HRV_ANOMALY, rmssd); } + self.events[n] = (EVENT_HRV_ANOMALY, rmssd); n += 1; self.cd_hrv = COOLDOWN_SECS; } } - unsafe { &EVENTS[..n] } + &self.events[..n] } /// Compute RMSSD from the RR-diff ring buffer. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/med_gait_analysis.rs b/v2/crates/wifi-densepose-wasm-edge/src/med_gait_analysis.rs index ab19bf6a..d85c7b28 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/med_gait_analysis.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/med_gait_analysis.rs @@ -1,7 +1,15 @@ -//! Gait analysis — ADR-041 Category 1 Medical module. +//! Gait-parameter proxies & fall-risk-like scoring — ADR-041 Category 1 Medical module. //! -//! Extracts gait parameters from CSI phase variance periodicity to assess -//! mobility and fall risk: +//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED AGAINST CLINICAL DATA. +//! ⚠️ NOT A MEDICAL DEVICE. Do NOT use for diagnosis, fall-risk assessment, or +//! ⚠️ any clinical decision. This module computes *candidate* gait-parameter +//! ⚠️ proxies and a fall-risk-like score only; it has never been compared +//! ⚠️ against gait labs, clinical fall-risk instruments, or any reference +//! ⚠️ standard, and its accuracy is unproven (see ADR-160 §A1). Gated behind +//! ⚠️ the non-default `medical-experimental` cargo feature. +//! +//! Extracts candidate gait-parameter proxies from CSI phase-variance +//! periodicity (experimental, NOT clinical measurements): //! - Step cadence (steps/min) from dominant phase variance frequency //! - Gait asymmetry from left/right step interval ratio //! - Stride variability (coefficient of variation) @@ -109,6 +117,9 @@ pub struct GaitAnalyzer { /// Frame counter. frame_count: u32, + + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 5], } impl GaitAnalyzer { @@ -132,6 +143,7 @@ impl GaitAnalyzer { last_asymmetry: 0.0, last_fall_risk: 0.0, frame_count: 0, + events: [(0, 0.0); 5], } } @@ -162,7 +174,6 @@ impl GaitAnalyzer { self.var_idx = (self.var_idx + 1) % GAIT_WINDOW; if self.var_len < GAIT_WINDOW { self.var_len += 1; } - static mut EVENTS: [(i32, f32); 5] = [(0, 0.0); 5]; let mut n = 0usize; // ── Step detection (peak in variance) ─────────────────────────── @@ -201,13 +212,13 @@ impl GaitAnalyzer { // Emit cadence. if n < 5 { - unsafe { EVENTS[n] = (EVENT_STEP_CADENCE, cadence); } + self.events[n] = (EVENT_STEP_CADENCE, cadence); n += 1; } // Emit asymmetry if above threshold. if fabsf(asymmetry - 1.0) > ASYMMETRY_THRESH && n < 5 { - unsafe { EVENTS[n] = (EVENT_GAIT_ASYMMETRY, asymmetry); } + self.events[n] = (EVENT_GAIT_ASYMMETRY, asymmetry); n += 1; } @@ -215,7 +226,7 @@ impl GaitAnalyzer { if cadence > SHUFFLE_CADENCE_HIGH && avg_energy < SHUFFLE_ENERGY_LOW && self.cd_shuffle == 0 && n < 5 { - unsafe { EVENTS[n] = (EVENT_SHUFFLING_DETECTED, cadence); } + self.events[n] = (EVENT_SHUFFLING_DETECTED, cadence); n += 1; self.cd_shuffle = COOLDOWN_SECS; } @@ -223,7 +234,7 @@ impl GaitAnalyzer { // Festination: accelerating cadence. if self.cadence_len >= 3 && self.cd_festination == 0 && n < 5 { if self.detect_festination() { - unsafe { EVENTS[n] = (EVENT_FESTINATION, cadence); } + self.events[n] = (EVENT_FESTINATION, cadence); n += 1; self.cd_festination = COOLDOWN_SECS; } @@ -233,7 +244,7 @@ impl GaitAnalyzer { let risk = self.compute_fall_risk(cadence, asymmetry, variability, avg_energy); self.last_fall_risk = risk; if n < 5 { - unsafe { EVENTS[n] = (EVENT_FALL_RISK_SCORE, risk); } + self.events[n] = (EVENT_FALL_RISK_SCORE, risk); n += 1; } @@ -241,7 +252,7 @@ impl GaitAnalyzer { self.step_count = 0; } - unsafe { &EVENTS[..n] } + &self.events[..n] } /// Compute cadence in steps/min from step intervals. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/med_respiratory_distress.rs b/v2/crates/wifi-densepose-wasm-edge/src/med_respiratory_distress.rs index bd1dfd20..1add7d85 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/med_respiratory_distress.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/med_respiratory_distress.rs @@ -1,11 +1,20 @@ -//! Respiratory distress detection — ADR-041 Category 1 Medical module. +//! Respiratory-distress-like pattern flagging — ADR-041 Category 1 Medical module. //! -//! Detects pathological breathing patterns from host CSI pipeline: -//! - Tachypnea: sustained breathing rate > 25 BPM -//! - Labored breathing: high amplitude variance relative to baseline -//! - Cheyne-Stokes respiration: crescendo-decrescendo periodicity (30-90 s) -//! detected via autocorrelation of the breathing amplitude envelope -//! - Overall respiratory distress level: composite severity score 0-100 +//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED AGAINST CLINICAL DATA. +//! ⚠️ NOT A MEDICAL DEVICE. Do NOT use for diagnosis or patient monitoring. +//! ⚠️ This module flags *candidate* respiratory-distress-like breathing +//! ⚠️ signatures only; it has never been compared against capnography, +//! ⚠️ spirometry, or any reference standard, and its accuracy is unproven +//! ⚠️ (see ADR-160 §A1). Gated behind the non-default `medical-experimental` +//! ⚠️ cargo feature. +//! +//! Flags candidate pathological-breathing-like patterns from the host CSI +//! pipeline (experimental proxies, NOT clinical measurements): +//! - Tachypnea-like: sustained breathing-rate estimate > 25 BPM +//! - Labored-breathing-like: high amplitude variance relative to baseline +//! - Cheyne-Stokes-like: crescendo-decrescendo periodicity (30-90 s) +//! flagged via autocorrelation of the breathing-rate envelope +//! - Composite distress-level proxy: severity score 0-100 //! //! Events: //! TACHYPNEA (120) — sustained high respiratory rate @@ -97,6 +106,9 @@ pub struct RespiratoryDistressDetector { /// Frame counter. frame_count: u32, + + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], } impl RespiratoryDistressDetector { @@ -116,6 +128,7 @@ impl RespiratoryDistressDetector { cd_cs: 0, last_distress: 0.0, frame_count: 0, + events: [(0, 0.0); 4], } } @@ -163,14 +176,13 @@ impl RespiratoryDistressDetector { self.var_mean += d / self.var_count as f32; } - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n = 0usize; // ── Tachypnea ─────────────────────────────────────────────────── if breathing_bpm > TACHYPNEA_THRESH { self.tachy_count = self.tachy_count.saturating_add(1); if self.tachy_count >= SUSTAINED_SECS && self.cd_tachy == 0 && n < 4 { - unsafe { EVENTS[n] = (EVENT_TACHYPNEA, breathing_bpm); } + self.events[n] = (EVENT_TACHYPNEA, breathing_bpm); n += 1; self.cd_tachy = COOLDOWN_SECS; } @@ -183,7 +195,7 @@ impl RespiratoryDistressDetector { let current_var = self.recent_var_mean(); let ratio = current_var / self.var_mean; if ratio > LABORED_VAR_RATIO && self.cd_labored == 0 && n < 4 { - unsafe { EVENTS[n] = (EVENT_LABORED_BREATHING, ratio); } + self.events[n] = (EVENT_LABORED_BREATHING, ratio); n += 1; self.cd_labored = COOLDOWN_SECS; } @@ -192,7 +204,7 @@ impl RespiratoryDistressDetector { // ── Cheyne-Stokes (autocorrelation) ───────────────────────────── if self.bpm_len >= AC_WINDOW && self.cd_cs == 0 && n < 4 { if let Some(period) = self.detect_cheyne_stokes() { - unsafe { EVENTS[n] = (EVENT_CHEYNE_STOKES, period as f32); } + self.events[n] = (EVENT_CHEYNE_STOKES, period as f32); n += 1; self.cd_cs = COOLDOWN_SECS; } @@ -202,11 +214,11 @@ impl RespiratoryDistressDetector { if self.frame_count % DISTRESS_REPORT_INTERVAL == 0 && n < 4 { let score = self.compute_distress_score(breathing_bpm, variance); self.last_distress = score; - unsafe { EVENTS[n] = (EVENT_RESP_DISTRESS_LEVEL, score); } + self.events[n] = (EVENT_RESP_DISTRESS_LEVEL, score); n += 1; } - unsafe { &EVENTS[..n] } + &self.events[..n] } /// Mean of recent variance samples. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/med_seizure_detect.rs b/v2/crates/wifi-densepose-wasm-edge/src/med_seizure_detect.rs index 0ff76a0d..e5ec4dc4 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/med_seizure_detect.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/med_seizure_detect.rs @@ -1,7 +1,17 @@ -//! Seizure detection — ADR-041 Category 1 Medical module. +//! Seizure-like motion-signature flagging — ADR-041 Category 1 Medical module. //! -//! Detects tonic-clonic seizures via high-energy rhythmic motion in the -//! 3-8 Hz band, discriminating from: +//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED AGAINST CLINICAL DATA. +//! ⚠️ NOT A MEDICAL DEVICE. Do NOT use for diagnosis, seizure monitoring, or any +//! ⚠️ clinical decision. This module flags *candidate* seizure-like motion +//! ⚠️ signatures (high-energy rhythmic 3-8 Hz motion) only; it has never been +//! ⚠️ validated against EEG/video-EEG or any reference standard, and its +//! ⚠️ accuracy is unproven (see ADR-160 §A1). Seizure detection cannot be +//! ⚠️ validated without clinical data — this module does not claim to do so. +//! ⚠️ Gated behind the non-default `medical-experimental` cargo feature. +//! +//! Flags candidate tonic-clonic-seizure-like motion signatures (experimental) +//! via high-energy rhythmic motion in the 3-8 Hz band, attempting to +//! discriminate from: //! - Falls: single impulse followed by stillness //! - Tremor: lower amplitude, higher regularity //! @@ -125,6 +135,9 @@ pub struct SeizureDetector { /// Frame counter. frame_count: u32, + + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], } impl SeizureDetector { @@ -143,6 +156,7 @@ impl SeizureDetector { cooldown: 0, seizure_count: 0, frame_count: 0, + events: [(0, 0.0); 4], } } @@ -172,7 +186,6 @@ impl SeizureDetector { self.amp_idx = (self.amp_idx + 1) % PHASE_WINDOW; if self.amp_len < PHASE_WINDOW { self.amp_len += 1; } - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n = 0usize; // No detection without presence. @@ -182,7 +195,7 @@ impl SeizureDetector { self.state_frames = 0; self.high_energy_frames = 0; } - return unsafe { &EVENTS[..n] }; + return &self.events[..n]; } // Tick cooldown. @@ -192,7 +205,7 @@ impl SeizureDetector { self.phase = SeizurePhase::Monitoring; self.state_frames = 0; } - return unsafe { &EVENTS[..n] }; + return &self.events[..n]; } // ── State machine ─────────────────────────────────────────────── @@ -222,7 +235,7 @@ impl SeizureDetector { self.phase = SeizurePhase::Monitoring; self.state_frames = 0; self.high_energy_frames = 0; - return unsafe { &EVENTS[..n] }; + return &self.events[..n]; } } @@ -232,7 +245,7 @@ impl SeizureDetector { self.phase = SeizurePhase::Tonic; self.state_frames = 0; self.seizure_count += 1; - unsafe { EVENTS[n] = (EVENT_SEIZURE_ONSET, motion_energy); } + self.events[n] = (EVENT_SEIZURE_ONSET, motion_energy); n += 1; } @@ -244,10 +257,10 @@ impl SeizureDetector { self.phase = SeizurePhase::Clonic; self.state_frames = 0; self.seizure_count += 1; - unsafe { EVENTS[n] = (EVENT_SEIZURE_ONSET, motion_energy); } + self.events[n] = (EVENT_SEIZURE_ONSET, motion_energy); n += 1; if n < 4 { - unsafe { EVENTS[n] = (EVENT_SEIZURE_CLONIC, period as f32); } + self.events[n] = (EVENT_SEIZURE_CLONIC, period as f32); n += 1; } } @@ -271,13 +284,13 @@ impl SeizureDetector { if energy_var > TONIC_VAR_CEIL { if let Some(period) = self.detect_rhythm() { if self.state_frames >= TONIC_MIN_FRAMES && n < 4 { - unsafe { EVENTS[n] = (EVENT_SEIZURE_TONIC, self.state_frames as f32); } + self.events[n] = (EVENT_SEIZURE_TONIC, self.state_frames as f32); n += 1; } self.phase = SeizurePhase::Clonic; self.state_frames = 0; if n < 4 { - unsafe { EVENTS[n] = (EVENT_SEIZURE_CLONIC, period as f32); } + self.events[n] = (EVENT_SEIZURE_CLONIC, period as f32); n += 1; } } @@ -289,7 +302,7 @@ impl SeizureDetector { self.low_energy_frames += 1; if self.low_energy_frames >= POST_ICTAL_MIN_FRAMES { if self.state_frames >= TONIC_MIN_FRAMES && n < 4 { - unsafe { EVENTS[n] = (EVENT_SEIZURE_TONIC, self.state_frames as f32); } + self.events[n] = (EVENT_SEIZURE_TONIC, self.state_frames as f32); n += 1; } self.phase = SeizurePhase::PostIctal; @@ -318,7 +331,7 @@ impl SeizureDetector { SeizurePhase::PostIctal => { self.state_frames += 1; if self.state_frames == 1 && n < 4 { - unsafe { EVENTS[n] = (EVENT_POST_ICTAL, 1.0); } + self.events[n] = (EVENT_POST_ICTAL, 1.0); n += 1; } @@ -337,7 +350,7 @@ impl SeizureDetector { } } - unsafe { &EVENTS[..n] } + &self.events[..n] } /// Compute variance of recent motion energy. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/med_sleep_apnea.rs b/v2/crates/wifi-densepose-wasm-edge/src/med_sleep_apnea.rs index e49f34f4..797c0ec3 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/med_sleep_apnea.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/med_sleep_apnea.rs @@ -1,10 +1,19 @@ -//! Sleep apnea detection — ADR-041 Category 1 Medical module. +//! Apnea-like breathing-pause flagging — ADR-041 Category 1 Medical module. //! -//! Detects obstructive and central sleep apnea by monitoring breathing BPM -//! from the host CSI pipeline. When breathing drops below 4 BPM for more -//! than 10 seconds the detector flags an apnea event. It also tracks the -//! Apnea-Hypopnea Index (AHI) — the number of apnea events per hour of -//! monitored sleep time. +//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED AGAINST CLINICAL DATA. +//! ⚠️ NOT A MEDICAL DEVICE. Do NOT use for diagnosis, monitoring of patients, +//! ⚠️ or any clinical decision. This module flags *candidate* apnea-like +//! ⚠️ breathing-pause signatures (sustained low breathing-rate estimates) +//! ⚠️ only; it has never been compared against polysomnography or any +//! ⚠️ reference standard, and its accuracy is unproven (see ADR-160 §A1). +//! ⚠️ Gated behind the non-default `medical-experimental` cargo feature so it +//! ⚠️ cannot be silently built into a shipping artifact. +//! +//! Monitors breathing-rate estimates from the host CSI pipeline. When the +//! estimate drops below 4 BPM for more than 10 seconds the detector flags a +//! candidate apnea-like event. It also tracks a candidate Apnea-Hypopnea +//! Index (AHI) proxy — the number of flagged events per hour of monitored +//! time. These are experimental proxies, NOT clinical measurements. //! //! Events: //! APNEA_START (100) — breathing ceased or fell below threshold @@ -77,6 +86,8 @@ pub struct SleepApneaDetector { timer_count: u32, /// Most recently computed AHI. last_ahi: f32, + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], } impl SleepApneaDetector { @@ -90,6 +101,7 @@ impl SleepApneaDetector { monitoring_secs: 0, timer_count: 0, last_ahi: 0.0, + events: [(0, 0.0); 4], } } @@ -104,7 +116,6 @@ impl SleepApneaDetector { ) -> &[(i32, f32)] { self.timer_count += 1; - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n = 0usize; // Only monitor when subject is present. @@ -115,11 +126,11 @@ impl SleepApneaDetector { self.record_episode(self.current_start, dur); self.in_apnea = false; self.low_breath_secs = 0; - unsafe { EVENTS[n] = (EVENT_APNEA_END, dur as f32); } + self.events[n] = (EVENT_APNEA_END, dur as f32); n += 1; } self.low_breath_secs = 0; - return unsafe { &EVENTS[..n] }; + return &self.events[..n]; } self.monitoring_secs += 1; @@ -129,7 +140,7 @@ impl SleepApneaDetector { // Treat NaN as invalid — skip detection for this frame. if breathing_bpm != breathing_bpm { // NaN: f32::NAN != f32::NAN is true. - return unsafe { &EVENTS[..n] }; + return &self.events[..n]; } // ── Apnea detection ───────────────────────────────────────────── @@ -140,7 +151,7 @@ impl SleepApneaDetector { // Apnea onset — backdate start to when breathing first dropped. self.in_apnea = true; self.current_start = self.timer_count.saturating_sub(self.low_breath_secs); - unsafe { EVENTS[n] = (EVENT_APNEA_START, breathing_bpm); } + self.events[n] = (EVENT_APNEA_START, breathing_bpm); n += 1; } } else { @@ -149,7 +160,7 @@ impl SleepApneaDetector { let dur = self.timer_count.saturating_sub(self.current_start); self.record_episode(self.current_start, dur); self.in_apnea = false; - unsafe { EVENTS[n] = (EVENT_APNEA_END, dur as f32); } + self.events[n] = (EVENT_APNEA_END, dur as f32); n += 1; } self.low_breath_secs = 0; @@ -163,11 +174,11 @@ impl SleepApneaDetector { } else { 0.0 }; - unsafe { EVENTS[n] = (EVENT_AHI_UPDATE, self.last_ahi); } + self.events[n] = (EVENT_AHI_UPDATE, self.last_ahi); n += 1; } - unsafe { &EVENTS[..n] } + &self.events[..n] } fn record_episode(&mut self, start: u32, duration: u32) { diff --git a/v2/crates/wifi-densepose-wasm-edge/src/occupancy.rs b/v2/crates/wifi-densepose-wasm-edge/src/occupancy.rs index f7075d57..a09f9594 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/occupancy.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/occupancy.rs @@ -42,6 +42,8 @@ struct ZoneState { /// Occupancy zone detector. pub struct OccupancyDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 12], zones: [ZoneState; MAX_ZONES], n_zones: usize, /// Calibration accumulators. @@ -61,6 +63,7 @@ impl OccupancyDetector { prev_occupied: false, }; Self { + events: [(0, 0.0); 12], zones: [ZONE_INIT; MAX_ZONES], n_zones: 0, calib_sum: [0.0; MAX_ZONES], @@ -163,7 +166,6 @@ impl OccupancyDetector { // Build output events in a static buffer. // We re-use a static to avoid allocation in no_std. - static mut EVENTS: [(i32, f32); 12] = [(0, 0.0); 12]; let mut n_events = 0usize; // Emit per-zone occupancy (every 10 frames to limit bandwidth). @@ -172,18 +174,14 @@ impl OccupancyDetector { if self.zones[z].occupied && n_events < 10 { // Encode zone_id in integer part, confidence in fractional. let val = z as f32 + self.zones[z].score.min(0.99); - unsafe { - EVENTS[n_events] = (EVENT_ZONE_OCCUPIED, val); - } + self.events[n_events] = (EVENT_ZONE_OCCUPIED, val); n_events += 1; } } // Emit total occupied zone count. if n_events < 11 { - unsafe { - EVENTS[n_events] = (EVENT_ZONE_COUNT, total_occupied as f32); - } + self.events[n_events] = (EVENT_ZONE_COUNT, total_occupied as f32); n_events += 1; } } @@ -192,14 +190,12 @@ impl OccupancyDetector { for z in 0..zone_count { if self.zones[z].occupied != self.zones[z].prev_occupied && n_events < 12 { let val = z as f32 + if self.zones[z].occupied { 0.5 } else { 0.0 }; - unsafe { - EVENTS[n_events] = (EVENT_ZONE_TRANSITION, val); - } + self.events[n_events] = (EVENT_ZONE_TRANSITION, val); n_events += 1; } } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Get the number of currently occupied zones. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/qnt_interference_search.rs b/v2/crates/wifi-densepose-wasm-edge/src/qnt_interference_search.rs index 4c0e803e..42ee15ff 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/qnt_interference_search.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/qnt_interference_search.rs @@ -112,6 +112,8 @@ impl Hypothesis { /// Grover-inspired room state search engine. pub struct InterferenceSearch { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], /// Amplitude for each of the 16 hypotheses. amplitudes: [f32; N_HYPO], /// Total Grover iterations applied. @@ -130,6 +132,7 @@ impl InterferenceSearch { pub const fn new() -> Self { // 1/sqrt(16) = 0.25 Self { + events: [(0, 0.0); 3], amplitudes: [0.25; N_HYPO], iteration_count: 0, converged: false, @@ -178,37 +181,30 @@ impl InterferenceSearch { self.converged = winner_prob > CONVERGENCE_PROB; // ── Build output events ── - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut n_events = 0usize; // Emit winner periodically or on change. let winner_changed = winner_idx as u8 != self.prev_winner; if winner_changed || self.frame_count % WINNER_EMIT_INTERVAL == 0 { - unsafe { - EVENTS[n_events] = (EVENT_HYPOTHESIS_WINNER, winner_idx as f32); - } + self.events[n_events] = (EVENT_HYPOTHESIS_WINNER, winner_idx as f32); n_events += 1; } // Emit amplitude periodically. if self.frame_count % AMPLITUDE_EMIT_INTERVAL == 0 { - unsafe { - EVENTS[n_events] = (EVENT_HYPOTHESIS_AMPLITUDE, winner_prob); - } + self.events[n_events] = (EVENT_HYPOTHESIS_AMPLITUDE, winner_prob); n_events += 1; } // Emit iteration count periodically. if self.frame_count % ITERATION_EMIT_INTERVAL == 0 { - unsafe { - EVENTS[n_events] = (EVENT_SEARCH_ITERATIONS, self.iteration_count as f32); - } + self.events[n_events] = (EVENT_SEARCH_ITERATIONS, self.iteration_count as f32); n_events += 1; } self.prev_winner = winner_idx as u8; - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Apply the oracle: set boost/dampen factors based on CSI evidence. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/qnt_quantum_coherence.rs b/v2/crates/wifi-densepose-wasm-edge/src/qnt_quantum_coherence.rs index a5860438..994e53d1 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/qnt_quantum_coherence.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/qnt_quantum_coherence.rs @@ -58,6 +58,8 @@ pub const EVENT_BLOCH_DRIFT: i32 = 852; /// Quantum-inspired coherence monitor using Bloch sphere representation. pub struct QuantumCoherenceMonitor { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], /// Previous aggregate Bloch vector [x, y, z]. prev_bloch: [f32; 3], /// EMA-smoothed Von Neumann entropy. @@ -74,6 +76,7 @@ impl QuantumCoherenceMonitor { /// Create a new monitor. Const-evaluable for static initialization. pub const fn new() -> Self { Self { + events: [(0, 0.0); 3], prev_bloch: [0.0, 0.0, 1.0], smoothed_entropy: 0.0, prev_entropy: 0.0, @@ -129,34 +132,27 @@ impl QuantumCoherenceMonitor { self.prev_bloch = bloch; // ── Build output events ── - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut n_events = 0usize; // Entropy (periodic). if self.frame_count % ENTROPY_EMIT_INTERVAL == 0 { - unsafe { - EVENTS[n_events] = (EVENT_ENTANGLEMENT_ENTROPY, self.smoothed_entropy); - } + self.events[n_events] = (EVENT_ENTANGLEMENT_ENTROPY, self.smoothed_entropy); n_events += 1; } // Decoherence event (immediate). if entropy_jump > DECOHERENCE_THRESHOLD { - unsafe { - EVENTS[n_events] = (EVENT_DECOHERENCE_EVENT, entropy_jump); - } + self.events[n_events] = (EVENT_DECOHERENCE_EVENT, entropy_jump); n_events += 1; } // Bloch drift (periodic). if self.frame_count % DRIFT_EMIT_INTERVAL == 0 { - unsafe { - EVENTS[n_events] = (EVENT_BLOCH_DRIFT, drift); - } + self.events[n_events] = (EVENT_BLOCH_DRIFT, drift); n_events += 1; } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Compute the mean Bloch vector from subcarrier phases. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ret_customer_flow.rs b/v2/crates/wifi-densepose-wasm-edge/src/ret_customer_flow.rs index ccf69fea..9d00c2ae 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ret_customer_flow.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ret_customer_flow.rs @@ -72,6 +72,8 @@ const MAX_EVENTS: usize = 4; /// Tracks directional foot traffic using phase gradient analysis. pub struct CustomerFlowTracker { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); MAX_EVENTS], /// Previous phase values per subcarrier. prev_phases: [f32; MAX_SC], /// Previous amplitude values per subcarrier. @@ -101,6 +103,7 @@ pub struct CustomerFlowTracker { impl CustomerFlowTracker { pub const fn new() -> Self { Self { + events: [(0, 0.0); MAX_EVENTS], prev_phases: [0.0; MAX_SC], prev_amplitudes: [0.0; MAX_SC], gradient_ema: Ema::new(GRADIENT_EMA_ALPHA), @@ -200,7 +203,6 @@ impl CustomerFlowTracker { } // Build events. - static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS]; let mut ne = 0usize; // Crossing detection: look for gradient peak + motion + amplitude spike. @@ -218,9 +220,7 @@ impl CustomerFlowTracker { self.ingress_count += 1; self.hourly_ingress += 1; if ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_INGRESS, self.ingress_count as f32); - } + self.events[ne] = (EVENT_INGRESS, self.ingress_count as f32); ne += 1; } } else { @@ -228,9 +228,7 @@ impl CustomerFlowTracker { self.egress_count += 1; self.hourly_egress += 1; if ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_EGRESS, self.egress_count as f32); - } + self.events[ne] = (EVENT_EGRESS, self.egress_count as f32); ne += 1; } } @@ -238,9 +236,7 @@ impl CustomerFlowTracker { // Emit net occupancy on each crossing. let net = self.net_occupancy(); if ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_NET_OCCUPANCY, net as f32); - } + self.events[ne] = (EVENT_NET_OCCUPANCY, net as f32); ne += 1; } } @@ -248,9 +244,7 @@ impl CustomerFlowTracker { // Periodic net occupancy report. if self.frame_count % OCCUPANCY_REPORT_INTERVAL == 0 && ne < MAX_EVENTS { let net = self.net_occupancy(); - unsafe { - EVENTS[ne] = (EVENT_NET_OCCUPANCY, net as f32); - } + self.events[ne] = (EVENT_NET_OCCUPANCY, net as f32); ne += 1; } @@ -259,16 +253,14 @@ impl CustomerFlowTracker { // Encode: ingress * 1000 + egress. let summary = self.hourly_ingress as f32 * 1000.0 + self.hourly_egress as f32; if ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_HOURLY_TRAFFIC, summary); - } + self.events[ne] = (EVENT_HOURLY_TRAFFIC, summary); ne += 1; } self.hourly_ingress = 0; self.hourly_egress = 0; } - unsafe { &EVENTS[..ne] } + &self.events[..ne] } /// Get net occupancy (ingress - egress), clamped to 0. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ret_dwell_heatmap.rs b/v2/crates/wifi-densepose-wasm-edge/src/ret_dwell_heatmap.rs index 526d0e53..108039c4 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ret_dwell_heatmap.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ret_dwell_heatmap.rs @@ -80,6 +80,8 @@ const ZONE_INIT: ZoneState = ZoneState { /// Tracks dwell time across a 3x3 spatial zone grid. pub struct DwellHeatmapTracker { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); MAX_EVENTS], zones: [ZoneState; NUM_ZONES], /// Frame counter. frame_count: u32, @@ -96,6 +98,7 @@ pub struct DwellHeatmapTracker { impl DwellHeatmapTracker { pub const fn new() -> Self { Self { + events: [(0, 0.0); MAX_EVENTS], zones: [ZONE_INIT; NUM_ZONES], frame_count: 0, any_present: false, @@ -176,7 +179,6 @@ impl DwellHeatmapTracker { self.any_present = is_present || any_zone_occupied; // Build events. - static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS]; let mut ne = 0usize; // Periodic zone updates. @@ -186,9 +188,7 @@ impl DwellHeatmapTracker { if self.zones[z].dwell_seconds > 0.0 && ne < MAX_EVENTS - 3 { // Encode zone_id in integer part, dwell seconds in value. let val = z as f32 * 1000.0 + self.zones[z].dwell_seconds; - unsafe { - EVENTS[ne] = (EVENT_DWELL_ZONE_UPDATE, val); - } + self.events[ne] = (EVENT_DWELL_ZONE_UPDATE, val); ne += 1; } } @@ -211,16 +211,12 @@ impl DwellHeatmapTracker { } if hot_dwell > 0.0 && ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_HOT_ZONE, hot_zone as f32 + hot_dwell / 1000.0); - } + self.events[ne] = (EVENT_HOT_ZONE, hot_zone as f32 + hot_dwell / 1000.0); ne += 1; } if cold_dwell < f32::MAX && ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_COLD_ZONE, cold_zone as f32 + cold_dwell / 1000.0); - } + self.events[ne] = (EVENT_COLD_ZONE, cold_zone as f32 + cold_dwell / 1000.0); ne += 1; } } @@ -230,14 +226,12 @@ impl DwellHeatmapTracker { self.session_active = false; let session_duration = (self.frame_count - self.session_start_frame) as f32 / FRAME_RATE; if ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_SESSION_SUMMARY, session_duration); - } + self.events[ne] = (EVENT_SESSION_SUMMARY, session_duration); ne += 1; } } - unsafe { &EVENTS[..ne] } + &self.events[..ne] } /// Get dwell time (seconds) for a specific zone in the current session. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ret_queue_length.rs b/v2/crates/wifi-densepose-wasm-edge/src/ret_queue_length.rs index 00bbc434..bc9226a6 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ret_queue_length.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ret_queue_length.rs @@ -62,6 +62,8 @@ const RATE_HISTORY: usize = 1200; /// Estimates queue length from CSI presence and person-count data. pub struct QueueLengthEstimator { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Smoothed queue length estimate. queue_ema: Ema, /// Smoothed arrival rate (persons/minute). @@ -91,6 +93,7 @@ pub struct QueueLengthEstimator { impl QueueLengthEstimator { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], queue_ema: Ema::new(QUEUE_EMA_ALPHA), arrival_rate_ema: Ema::new(RATE_EMA_ALPHA), service_rate_ema: Ema::new(RATE_EMA_ALPHA), @@ -161,14 +164,11 @@ impl QueueLengthEstimator { } // Build events. - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut ne = 0usize; // Periodic queue length report. if self.frame_count % REPORT_INTERVAL == 0 { - unsafe { - EVENTS[ne] = (EVENT_QUEUE_LENGTH, self.current_queue as f32); - } + self.events[ne] = (EVENT_QUEUE_LENGTH, self.current_queue as f32); ne += 1; } @@ -184,9 +184,7 @@ impl QueueLengthEstimator { // Service rate event. if ne < 4 { - unsafe { - EVENTS[ne] = (EVENT_SERVICE_RATE, self.service_rate_ema.value); - } + self.events[ne] = (EVENT_SERVICE_RATE, self.service_rate_ema.value); ne += 1; } @@ -199,9 +197,7 @@ impl QueueLengthEstimator { }; if ne < 4 { - unsafe { - EVENTS[ne] = (EVENT_WAIT_TIME_ESTIMATE, wait_time); - } + self.events[ne] = (EVENT_WAIT_TIME_ESTIMATE, wait_time); ne += 1; } } @@ -216,16 +212,14 @@ impl QueueLengthEstimator { if self.current_queue as f32 >= QUEUE_ALERT_THRESH && !self.alert_active { self.alert_active = true; if ne < 4 { - unsafe { - EVENTS[ne] = (EVENT_QUEUE_ALERT, self.current_queue as f32); - } + self.events[ne] = (EVENT_QUEUE_ALERT, self.current_queue as f32); ne += 1; } } else if (self.current_queue as f32) < QUEUE_ALERT_THRESH - 1.0 { self.alert_active = false; } - unsafe { &EVENTS[..ne] } + &self.events[..ne] } /// Get the current smoothed queue length. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ret_shelf_engagement.rs b/v2/crates/wifi-densepose-wasm-edge/src/ret_shelf_engagement.rs index d4cc182f..d8e8fd68 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ret_shelf_engagement.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ret_shelf_engagement.rs @@ -96,6 +96,8 @@ pub enum EngagementLevel { /// Detects and classifies customer shelf engagement from CSI data. pub struct ShelfEngagementDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); MAX_EVENTS], /// Previous phase values for perturbation calculation. prev_phases: [f32; MAX_SC], /// Phase perturbation EMA (high-frequency component). @@ -133,6 +135,7 @@ pub struct ShelfEngagementDetector { impl ShelfEngagementDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); MAX_EVENTS], prev_phases: [0.0; MAX_SC], perturbation_ema: Ema::new(PERTURBATION_EMA_ALPHA), motion_ema: Ema::new(MOTION_EMA_ALPHA), @@ -221,7 +224,6 @@ impl ShelfEngagementDetector { self.phase_diff_history.push(perturbation); // Build events. - static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS]; let mut ne = 0usize; if !is_present { @@ -234,7 +236,7 @@ impl ShelfEngagementDetector { self.still_frames = 0; self.level = EngagementLevel::None; self.prev_emitted_level = EngagementLevel::None; - unsafe { return &EVENTS[..ne]; } + return &self.events[..ne]; } // Detect stillness (low translational motion). @@ -249,7 +251,7 @@ impl ShelfEngagementDetector { self.engagement_frames = 0; self.level = EngagementLevel::None; self.prev_emitted_level = EngagementLevel::None; - unsafe { return &EVENTS[..ne]; } + return &self.events[..ne]; } // Only start engagement counting after debounce. @@ -284,9 +286,7 @@ impl ShelfEngagementDetector { }; if event_id != 0 && ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (event_id, duration); - } + self.events[ne] = (event_id, duration); ne += 1; self.prev_emitted_level = self.level; self.cooldown = ENGAGEMENT_COOLDOWN; @@ -297,13 +297,11 @@ impl ShelfEngagementDetector { // Reach detection: sudden high-frequency phase burst while still. if self.still_frames > STILL_DEBOUNCE && perturbation > REACH_BURST_THRESH && ne < MAX_EVENTS { self.total_reaches += 1; - unsafe { - EVENTS[ne] = (EVENT_REACH_DETECTED, perturbation); - } + self.events[ne] = (EVENT_REACH_DETECTED, perturbation); ne += 1; } - unsafe { &EVENTS[..ne] } + &self.events[..ne] } /// Emit engagement end event based on current level. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/ret_table_turnover.rs b/v2/crates/wifi-densepose-wasm-edge/src/ret_table_turnover.rs index 82c2041c..f753024a 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/ret_table_turnover.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/ret_table_turnover.rs @@ -80,6 +80,8 @@ pub enum TableState { /// Tracks table occupancy state transitions and turnover metrics. pub struct TableTurnoverTracker { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); MAX_EVENTS], /// Current table state. state: TableState, /// Smoothed motion energy. @@ -109,6 +111,7 @@ pub struct TableTurnoverTracker { impl TableTurnoverTracker { pub const fn new() -> Self { Self { + events: [(0, 0.0); MAX_EVENTS], state: TableState::Empty, motion_ema: Ema::new(MOTION_EMA_ALPHA), presence_frames: 0, @@ -143,7 +146,6 @@ impl TableTurnoverTracker { let smoothed_motion = self.motion_ema.update(motion_energy); let n = if n_persons < 0 { 0 } else { n_persons }; - static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS]; let mut ne = 0usize; match self.state { @@ -158,9 +160,7 @@ impl TableTurnoverTracker { self.absence_frames = 0; if ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_TABLE_SEATED, n as f32); - } + self.events[ne] = (EVENT_TABLE_SEATED, n as f32); ne += 1; } } @@ -202,9 +202,7 @@ impl TableTurnoverTracker { let duration_s = self.session_frames as f32 / FRAME_RATE; if ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_TABLE_VACATED, duration_s); - } + self.events[ne] = (EVENT_TABLE_VACATED, duration_s); ne += 1; } @@ -241,9 +239,7 @@ impl TableTurnoverTracker { let duration_s = self.session_frames as f32 / FRAME_RATE; if ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_TABLE_VACATED, duration_s); - } + self.events[ne] = (EVENT_TABLE_VACATED, duration_s); ne += 1; } @@ -270,9 +266,7 @@ impl TableTurnoverTracker { self.peak_persons = 0; if ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_TABLE_AVAILABLE, 1.0); - } + self.events[ne] = (EVENT_TABLE_AVAILABLE, 1.0); ne += 1; } } else if is_present { @@ -285,9 +279,7 @@ impl TableTurnoverTracker { self.presence_frames = 0; if ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_TABLE_SEATED, n as f32); - } + self.events[ne] = (EVENT_TABLE_SEATED, n as f32); ne += 1; } } @@ -301,14 +293,12 @@ impl TableTurnoverTracker { if self.frame_count % TURNOVER_REPORT_INTERVAL == 0 && self.frame_count > 0 { let rate = self.turnover_rate(); if ne < MAX_EVENTS { - unsafe { - EVENTS[ne] = (EVENT_TURNOVER_RATE, rate); - } + self.events[ne] = (EVENT_TURNOVER_RATE, rate); ne += 1; } } - unsafe { &EVENTS[..ne] } + &self.events[..ne] } /// Compute turnovers per hour (rolling window). diff --git a/v2/crates/wifi-densepose-wasm-edge/src/sec_loitering.rs b/v2/crates/wifi-densepose-wasm-edge/src/sec_loitering.rs index 2abd0475..f4dbede1 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/sec_loitering.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/sec_loitering.rs @@ -46,6 +46,8 @@ pub enum LoiterState { /// Loitering detector. pub struct LoiteringDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 2], state: LoiterState, /// Consecutive frames with presence detected. presence_frames: u32, @@ -65,6 +67,7 @@ pub struct LoiteringDetector { impl LoiteringDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 2], state: LoiterState::Absent, presence_frames: 0, dwell_frames: 0, @@ -88,7 +91,6 @@ impl LoiteringDetector { self.frame_count += 1; self.post_end_cd = self.post_end_cd.saturating_sub(1); - static mut EVENTS: [(i32, f32); 2] = [(0, 0.0); 2]; let mut ne = 0usize; // Determine if someone is present and roughly stationary. @@ -133,9 +135,7 @@ impl LoiteringDetector { if ne < 2 { let dwell_seconds = self.dwell_frames as f32 / 20.0; - unsafe { - EVENTS[ne] = (EVENT_LOITERING_START, dwell_seconds); - } + self.events[ne] = (EVENT_LOITERING_START, dwell_seconds); ne += 1; } } @@ -161,9 +161,7 @@ impl LoiteringDetector { self.ongoing_timer = 0; if ne < 2 { let total_seconds = self.dwell_frames as f32 / 20.0; - unsafe { - EVENTS[ne] = (EVENT_LOITERING_ONGOING, total_seconds); - } + self.events[ne] = (EVENT_LOITERING_ONGOING, total_seconds); ne += 1; } } @@ -177,9 +175,7 @@ impl LoiteringDetector { if ne < 2 { let total_seconds = self.dwell_frames as f32 / 20.0; - unsafe { - EVENTS[ne] = (EVENT_LOITERING_END, total_seconds); - } + self.events[ne] = (EVENT_LOITERING_END, total_seconds); ne += 1; } @@ -191,7 +187,7 @@ impl LoiteringDetector { } } - unsafe { &EVENTS[..ne] } + &self.events[..ne] } pub fn state(&self) -> LoiterState { self.state } diff --git a/v2/crates/wifi-densepose-wasm-edge/src/sec_panic_motion.rs b/v2/crates/wifi-densepose-wasm-edge/src/sec_panic_motion.rs index 33e2115f..7b6b9dc4 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/sec_panic_motion.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/sec_panic_motion.rs @@ -54,6 +54,8 @@ pub const EVENT_FLEEING_DETECTED: i32 = 252; /// Panic/erratic motion detector. pub struct PanicMotionDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], /// Circular buffer of motion energy values. energy_buf: [f32; WINDOW], /// Circular buffer of phase variance values (for direction estimation). @@ -75,6 +77,7 @@ pub struct PanicMotionDetector { impl PanicMotionDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 3], energy_buf: [0.0; WINDOW], variance_buf: [0.0; WINDOW], buf_idx: 0, @@ -102,7 +105,6 @@ impl PanicMotionDetector { self.cd_struggle = self.cd_struggle.saturating_sub(1); self.cd_fleeing = self.cd_fleeing.saturating_sub(1); - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut ne = 0usize; // Store in circular buffer. @@ -117,13 +119,13 @@ impl PanicMotionDetector { if !self.buf_filled { self.prev_energy = motion_energy; self.prev_energy_init = true; - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } // Require presence. if presence < MIN_PRESENCE { self.prev_energy = motion_energy; - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } // Compute jerk (absolute rate of change of motion energy). @@ -142,7 +144,7 @@ impl PanicMotionDetector { // Skip if not enough motion. if mean_energy < MIN_MOTION { - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } // Panic detection: high jerk AND high entropy over threshold fraction of window. @@ -152,7 +154,7 @@ impl PanicMotionDetector { if is_panic && self.cd_panic == 0 && ne < 3 { let severity = (mean_jerk / JERK_THRESH) * (entropy / ENTROPY_THRESH); - unsafe { EVENTS[ne] = (EVENT_PANIC_DETECTED, severity.min(10.0)); } + self.events[ne] = (EVENT_PANIC_DETECTED, severity.min(10.0)); ne += 1; self.cd_panic = COOLDOWN; self.panic_count += 1; @@ -167,7 +169,7 @@ impl PanicMotionDetector { && entropy > ENTROPY_THRESH * 0.5; if is_struggle && !is_panic && self.cd_struggle == 0 && ne < 3 { - unsafe { EVENTS[ne] = (EVENT_STRUGGLE_PATTERN, mean_jerk); } + self.events[ne] = (EVENT_STRUGGLE_PATTERN, mean_jerk); ne += 1; self.cd_struggle = COOLDOWN; } @@ -179,12 +181,12 @@ impl PanicMotionDetector { && entropy < FLEE_MAX_ENTROPY; if is_fleeing && !is_panic && self.cd_fleeing == 0 && ne < 3 { - unsafe { EVENTS[ne] = (EVENT_FLEEING_DETECTED, mean_energy); } + self.events[ne] = (EVENT_FLEEING_DETECTED, mean_energy); ne += 1; self.cd_fleeing = COOLDOWN; } - unsafe { &EVENTS[..ne] } + &self.events[..ne] } /// Compute window-level statistics. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/sec_perimeter_breach.rs b/v2/crates/wifi-densepose-wasm-edge/src/sec_perimeter_breach.rs index 17834b87..3540ff35 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/sec_perimeter_breach.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/sec_perimeter_breach.rs @@ -92,6 +92,8 @@ impl ZoneState { /// Multi-zone perimeter breach detector. pub struct PerimeterBreachDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], zones: [ZoneState; MAX_ZONES], /// Calibration accumulators per zone: sum of gradient magnitudes. cal_grad_sum: [f32; MAX_ZONES], @@ -118,6 +120,7 @@ pub struct PerimeterBreachDetector { impl PerimeterBreachDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], zones: [ZoneState::new(); MAX_ZONES], cal_grad_sum: [0.0; MAX_ZONES], cal_var_sum: [0.0; MAX_ZONES], @@ -155,7 +158,6 @@ impl PerimeterBreachDetector { self.cd_departure = self.cd_departure.saturating_sub(1); self.cd_transition = self.cd_transition.saturating_sub(1); - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut ne = 0usize; let subs_per_zone = n_sc / MAX_ZONES; @@ -196,7 +198,7 @@ impl PerimeterBreachDetector { } if !self.phase_init { self.phase_init = true; - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } // Calibration phase. @@ -214,7 +216,7 @@ impl PerimeterBreachDetector { } self.calibrated = true; } - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } // Detect breaches and direction per zone. @@ -262,7 +264,7 @@ impl PerimeterBreachDetector { if self.approach_run[z] >= DIRECTION_DEBOUNCE && is_breach && self.cd_approach == 0 && ne < 4 { - unsafe { EVENTS[ne] = (EVENT_APPROACH_DETECTED, z as f32); } + self.events[ne] = (EVENT_APPROACH_DETECTED, z as f32); ne += 1; self.cd_approach = COOLDOWN; self.approach_run[z] = 0; @@ -272,7 +274,7 @@ impl PerimeterBreachDetector { if self.departure_run[z] >= DIRECTION_DEBOUNCE && self.cd_departure == 0 && ne < 4 { - unsafe { EVENTS[ne] = (EVENT_DEPARTURE_DETECTED, z as f32); } + self.events[ne] = (EVENT_DEPARTURE_DETECTED, z as f32); ne += 1; self.cd_departure = COOLDOWN; self.departure_run[z] = 0; @@ -281,7 +283,7 @@ impl PerimeterBreachDetector { // Perimeter breach event. if most_disturbed_zone >= 0 && self.cd_breach == 0 && ne < 4 { - unsafe { EVENTS[ne] = (EVENT_PERIMETER_BREACH, max_energy); } + self.events[ne] = (EVENT_PERIMETER_BREACH, max_energy); ne += 1; self.cd_breach = COOLDOWN; } @@ -296,7 +298,7 @@ impl PerimeterBreachDetector { // Encode as from*10 + to. let transition_code = self.last_active_zone as f32 * 10.0 + most_disturbed_zone as f32; - unsafe { EVENTS[ne] = (EVENT_ZONE_TRANSITION, transition_code); } + self.events[ne] = (EVENT_ZONE_TRANSITION, transition_code); ne += 1; self.cd_transition = COOLDOWN; } @@ -305,7 +307,7 @@ impl PerimeterBreachDetector { self.last_active_zone = most_disturbed_zone; } - unsafe { &EVENTS[..ne] } + &self.events[..ne] } pub fn is_calibrated(&self) -> bool { self.calibrated } diff --git a/v2/crates/wifi-densepose-wasm-edge/src/sec_tailgating.rs b/v2/crates/wifi-densepose-wasm-edge/src/sec_tailgating.rs index 7fdeee3c..e5f46bd1 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/sec_tailgating.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/sec_tailgating.rs @@ -50,6 +50,8 @@ enum PeakState { /// Tailgating detector. pub struct TailgateDetector { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], state: PeakState, /// Current peak's maximum energy. peak_max: f32, @@ -80,6 +82,7 @@ pub struct TailgateDetector { impl TailgateDetector { pub const fn new() -> Self { Self { + events: [(0, 0.0); 3], state: PeakState::Idle, peak_max: 0.0, peak_frames: 0, @@ -110,7 +113,6 @@ impl TailgateDetector { self.cd_tailgate = self.cd_tailgate.saturating_sub(1); self.cd_passage = self.cd_passage.saturating_sub(1); - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut ne = 0usize; // Update noise floor estimate (exponential moving average of variance). @@ -168,7 +170,7 @@ impl TailgateDetector { self.state = PeakState::InPeak; self.peak_max = motion_energy; self.peak_frames = 1; - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } // Window expired — evaluate passage. @@ -176,9 +178,7 @@ impl TailgateDetector { if self.peaks_in_window >= 2 { // Multiple peaks detected = tailgating. if self.cd_tailgate == 0 && ne < 3 { - unsafe { - EVENTS[ne] = (EVENT_TAILGATE_DETECTED, self.peaks_in_window as f32); - } + self.events[ne] = (EVENT_TAILGATE_DETECTED, self.peaks_in_window as f32); ne += 1; self.cd_tailgate = COOLDOWN; self.tailgate_count += 1; @@ -186,18 +186,14 @@ impl TailgateDetector { // Also emit multi-passage. if self.cd_passage == 0 && ne < 3 { - unsafe { - EVENTS[ne] = (EVENT_MULTI_PASSAGE, self.peaks_in_window as f32); - } + self.events[ne] = (EVENT_MULTI_PASSAGE, self.peaks_in_window as f32); ne += 1; self.cd_passage = COOLDOWN; } } else if self.peaks_in_window == 1 { // Single passage. if self.cd_passage == 0 && ne < 3 { - unsafe { - EVENTS[ne] = (EVENT_SINGLE_PASSAGE, self.peak_energies[0]); - } + self.events[ne] = (EVENT_SINGLE_PASSAGE, self.peak_energies[0]); ne += 1; self.cd_passage = COOLDOWN; self.single_passages += 1; @@ -212,7 +208,7 @@ impl TailgateDetector { } self.prev_energy = motion_energy; - unsafe { &EVENTS[..ne] } + &self.events[..ne] } pub fn frame_count(&self) -> u32 { self.frame_count } diff --git a/v2/crates/wifi-densepose-wasm-edge/src/sec_weapon_detect.rs b/v2/crates/wifi-densepose-wasm-edge/src/sec_weapon_detect.rs index 640b3b0a..41e17ee4 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/sec_weapon_detect.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/sec_weapon_detect.rs @@ -11,7 +11,14 @@ //! variance ratio compared to a person without metal, because metal strongly //! reflects RF energy while producing less phase dispersion than diffuse tissue. //! -//! Events: METAL_ANOMALY(220), WEAPON_ALERT(221), CALIBRATION_NEEDED(222). +//! ⚠️ HONEST-NAMING NOTE (ADR-160 §A3): this module measures RF **reflectivity** +//! ⚠️ (an amplitude-variance / phase-variance ratio), not weapons. A variance +//! ⚠️ ratio cannot discriminate a weapon from any other highly-reflective metal +//! ⚠️ object (keys, laptop, belt buckle). The high-ratio event is therefore named +//! ⚠️ `HIGH_METAL_REFLECTIVITY`, NOT a weapon alert — the physical quantity the +//! ⚠️ code can actually back. +//! +//! Events: METAL_ANOMALY(220), HIGH_METAL_REFLECTIVITY(221), CALIBRATION_NEEDED(222). //! Budget: S (<5 ms). #[cfg(not(feature = "std"))] @@ -26,16 +33,17 @@ const MAX_SC: usize = 32; const BASELINE_FRAMES: u32 = 100; /// Amplitude variance / phase variance ratio threshold for metal detection. const METAL_RATIO_THRESH: f32 = 4.0; -/// Elevated ratio for weapon-grade alert (very high reflectivity). -const WEAPON_RATIO_THRESH: f32 = 8.0; +/// Elevated reflectivity-ratio threshold (very high RF reflectivity). +/// NOTE (ADR-160 §A3): a variance ratio measures reflectivity, not weapons. +const HIGH_REFLECTIVITY_THRESH: f32 = 8.0; /// Minimum motion energy to consider detection valid (ignore static scenes). const MIN_MOTION_ENERGY: f32 = 0.5; /// Minimum presence required (person must be present). const MIN_PRESENCE: i32 = 1; /// Consecutive frames for metal anomaly debounce. const METAL_DEBOUNCE: u8 = 4; -/// Consecutive frames for weapon alert debounce. -const WEAPON_DEBOUNCE: u8 = 6; +/// Consecutive frames for high-reflectivity debounce. +const HIGH_REFLECTIVITY_DEBOUNCE: u8 = 6; /// Cooldown frames after event emission. const COOLDOWN: u16 = 60; /// Re-calibration trigger: if baseline drift exceeds this ratio. @@ -44,7 +52,9 @@ const RECALIB_DRIFT_THRESH: f32 = 3.0; const VAR_WINDOW: usize = 16; pub const EVENT_METAL_ANOMALY: i32 = 220; -pub const EVENT_WEAPON_ALERT: i32 = 221; +/// High RF reflectivity (formerly mislabelled `EVENT_WEAPON_ALERT`, ADR-160 §A3). +/// A variance ratio measures reflectivity, not weapon-grade discrimination. +pub const EVENT_HIGH_METAL_REFLECTIVITY: i32 = 221; pub const EVENT_CALIBRATION_NEEDED: i32 = 222; /// Concealed metallic object detector. @@ -74,12 +84,14 @@ pub struct WeaponDetector { run_count: u32, /// Debounce counters. metal_run: u8, - weapon_run: u8, + high_refl_run: u8, /// Cooldowns. cd_metal: u16, - cd_weapon: u16, + cd_high_refl: u16, cd_recalib: u16, frame_count: u32, + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], } impl WeaponDetector { @@ -101,11 +113,12 @@ impl WeaponDetector { run_phase_m2: [0.0; MAX_SC], run_count: 0, metal_run: 0, - weapon_run: 0, + high_refl_run: 0, cd_metal: 0, - cd_weapon: 0, + cd_high_refl: 0, cd_recalib: 0, frame_count: 0, + events: [(0, 0.0); 3], } } @@ -125,10 +138,9 @@ impl WeaponDetector { self.frame_count += 1; self.cd_metal = self.cd_metal.saturating_sub(1); - self.cd_weapon = self.cd_weapon.saturating_sub(1); + self.cd_high_refl = self.cd_high_refl.saturating_sub(1); self.cd_recalib = self.cd_recalib.saturating_sub(1); - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut ne = 0usize; // Calibration phase: collect baseline statistics in empty room. @@ -153,7 +165,7 @@ impl WeaponDetector { } self.calibrated = true; } - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } // Update running Welford statistics. @@ -176,7 +188,7 @@ impl WeaponDetector { // Only detect when someone is present and moving. if presence < MIN_PRESENCE || motion_energy < MIN_MOTION_ENERGY { self.metal_run = 0; - self.weapon_run = 0; + self.high_refl_run = 0; // Reset running stats periodically when no one is present. if self.run_count > 200 { self.run_count = 0; @@ -187,12 +199,12 @@ impl WeaponDetector { self.run_phase_m2[i] = 0.0; } } - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } // Compute current amplitude variance / phase variance ratio. if self.run_count < 4 { - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } let mut ratio_sum = 0.0f32; @@ -221,14 +233,14 @@ impl WeaponDetector { } if valid_sc < 2 { - return unsafe { &EVENTS[..0] }; + return &self.events[..0]; } let mean_ratio = ratio_sum / valid_sc as f32; // Check for re-calibration need. if max_drift > RECALIB_DRIFT_THRESH && self.cd_recalib == 0 && ne < 3 { - unsafe { EVENTS[ne] = (EVENT_CALIBRATION_NEEDED, max_drift); } + self.events[ne] = (EVENT_CALIBRATION_NEEDED, max_drift); ne += 1; self.cd_recalib = COOLDOWN * 5; // Less frequent recalibration alerts. } @@ -240,28 +252,28 @@ impl WeaponDetector { self.metal_run = self.metal_run.saturating_sub(1); } - // Weapon-grade detection (higher threshold). - if mean_ratio > WEAPON_RATIO_THRESH { - self.weapon_run = self.weapon_run.saturating_add(1); + // High-reflectivity detection (higher threshold). NOT weapon discrimination. + if mean_ratio > HIGH_REFLECTIVITY_THRESH { + self.high_refl_run = self.high_refl_run.saturating_add(1); } else { - self.weapon_run = self.weapon_run.saturating_sub(1); + self.high_refl_run = self.high_refl_run.saturating_sub(1); } // Emit metal anomaly. if self.metal_run >= METAL_DEBOUNCE && self.cd_metal == 0 && ne < 3 { - unsafe { EVENTS[ne] = (EVENT_METAL_ANOMALY, mean_ratio); } + self.events[ne] = (EVENT_METAL_ANOMALY, mean_ratio); ne += 1; self.cd_metal = COOLDOWN; } - // Emit weapon alert (supersedes metal anomaly in severity). - if self.weapon_run >= WEAPON_DEBOUNCE && self.cd_weapon == 0 && ne < 3 { - unsafe { EVENTS[ne] = (EVENT_WEAPON_ALERT, mean_ratio); } + // Emit high-reflectivity event (supersedes metal anomaly in severity). + if self.high_refl_run >= HIGH_REFLECTIVITY_DEBOUNCE && self.cd_high_refl == 0 && ne < 3 { + self.events[ne] = (EVENT_HIGH_METAL_REFLECTIVITY, mean_ratio); ne += 1; - self.cd_weapon = COOLDOWN; + self.cd_high_refl = COOLDOWN; } - unsafe { &EVENTS[..ne] } + &self.events[..ne] } pub fn is_calibrated(&self) -> bool { self.calibrated } @@ -311,7 +323,7 @@ mod tests { let ev = det.process_frame(&p, &[20.0; 16], &[0.01; 16], 0.0, 0); for &(et, _) in ev { assert_ne!(et, EVENT_METAL_ANOMALY); - assert_ne!(et, EVENT_WEAPON_ALERT); + assert_ne!(et, EVENT_HIGH_METAL_REFLECTIVITY); } } } @@ -369,7 +381,7 @@ mod tests { } let ev = det.process_frame(&p, &a, &[0.01; 16], 1.0, 1); for &(et, _) in ev { - assert_ne!(et, EVENT_WEAPON_ALERT, "normal person should not trigger weapon alert"); + assert_ne!(et, EVENT_HIGH_METAL_REFLECTIVITY, "normal person should not trigger weapon alert"); } } } diff --git a/v2/crates/wifi-densepose-wasm-edge/src/sig_coherence_gate.rs b/v2/crates/wifi-densepose-wasm-edge/src/sig_coherence_gate.rs index 2ccfc556..185e9531 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/sig_coherence_gate.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/sig_coherence_gate.rs @@ -73,6 +73,8 @@ impl WelfordStats { /// Coherence-gated frame filter. pub struct CoherenceGate { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], prev_phases: [f32; MAX_SC], stats: WelfordStats, initial_variance: f32, @@ -89,6 +91,7 @@ pub struct CoherenceGate { impl CoherenceGate { pub const fn new() -> Self { Self { + events: [(0, 0.0); 3], prev_phases: [0.0; MAX_SC], stats: WelfordStats::new(), initial_variance: 0.0, @@ -105,7 +108,6 @@ impl CoherenceGate { let n_sc = if phases.len() > MAX_SC { MAX_SC } else { phases.len() }; if n_sc < 2 { return &[]; } - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut n_ev = 0usize; if !self.initialized { @@ -146,7 +148,7 @@ impl CoherenceGate { self.gate = GateDecision::Recalibrate; self.low_count = 0; self.high_count = 0; - unsafe { EVENTS[n_ev] = (EVENT_RECALIBRATE_NEEDED, variance); } + self.events[n_ev] = (EVENT_RECALIBRATE_NEEDED, variance); n_ev += 1; } else { let below = coherence < LOW_THRESHOLD; @@ -178,11 +180,11 @@ impl CoherenceGate { }; } - unsafe { EVENTS[n_ev] = (EVENT_GATE_DECISION, self.gate.as_f32()); } + self.events[n_ev] = (EVENT_GATE_DECISION, self.gate.as_f32()); n_ev += 1; - unsafe { EVENTS[n_ev] = (EVENT_COHERENCE_SCORE, coherence); } + self.events[n_ev] = (EVENT_COHERENCE_SCORE, coherence); n_ev += 1; - unsafe { &EVENTS[..n_ev] } + &self.events[..n_ev] } pub fn gate(&self) -> GateDecision { self.gate } diff --git a/v2/crates/wifi-densepose-wasm-edge/src/sig_flash_attention.rs b/v2/crates/wifi-densepose-wasm-edge/src/sig_flash_attention.rs index e9d1fdbc..d0693086 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/sig_flash_attention.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/sig_flash_attention.rs @@ -25,6 +25,8 @@ pub const EVENT_SPATIAL_FOCUS_ZONE: i32 = 702; /// Flash Attention spatial focus estimator. pub struct FlashAttention { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], prev_group_phases: [f32; N_GROUPS], attention_weights: [f32; N_GROUPS], smoothed_entropy: f32, @@ -37,6 +39,7 @@ pub struct FlashAttention { impl FlashAttention { pub const fn new() -> Self { Self { + events: [(0, 0.0); 3], prev_group_phases: [0.0; N_GROUPS], attention_weights: [0.0; N_GROUPS], smoothed_entropy: MAX_ENTROPY, @@ -50,7 +53,6 @@ impl FlashAttention { let n_sc = phases.len().min(amplitudes.len()).min(MAX_SC); if n_sc < N_GROUPS { return &[]; } - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; // Per-group means for Q and V. let subs_per = n_sc / N_GROUPS; @@ -117,12 +119,10 @@ impl FlashAttention { for g in 0..N_GROUPS { self.prev_group_phases[g] = q[g]; } // Emit events. - unsafe { - EVENTS[0] = (EVENT_ATTENTION_PEAK_SC, peak_idx as f32); - EVENTS[1] = (EVENT_ATTENTION_SPREAD, self.smoothed_entropy); - EVENTS[2] = (EVENT_SPATIAL_FOCUS_ZONE, centroid); - &EVENTS[..3] - } + self.events[0] = (EVENT_ATTENTION_PEAK_SC, peak_idx as f32); + self.events[1] = (EVENT_ATTENTION_SPREAD, self.smoothed_entropy); + self.events[2] = (EVENT_SPATIAL_FOCUS_ZONE, centroid); + &self.events[..3] } pub fn weights(&self) -> &[f32; N_GROUPS] { &self.attention_weights } diff --git a/v2/crates/wifi-densepose-wasm-edge/src/sig_mincut_person_match.rs b/v2/crates/wifi-densepose-wasm-edge/src/sig_mincut_person_match.rs index 8c05a5b5..b102ad31 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/sig_mincut_person_match.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/sig_mincut_person_match.rs @@ -59,6 +59,8 @@ impl PersonSlot { /// Min-cut person identity matcher. pub struct PersonMatcher { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 8], slots: [PersonSlot; MAX_PERSONS], active_count: u8, prev_assignment: [u8; MAX_PERSONS], @@ -69,6 +71,7 @@ pub struct PersonMatcher { impl PersonMatcher { pub const fn new() -> Self { Self { + events: [(0, 0.0); 8], slots: [ PersonSlot::new(0), PersonSlot::new(1), @@ -98,7 +101,6 @@ impl PersonMatcher { self.frame_count += 1; let n_det = n_persons.min(MAX_PERSONS); - static mut EVENTS: [(i32, f32); 8] = [(0, 0.0); 8]; let mut n_events = 0usize; // Extract per-person feature vectors (spatial region -> top-8 variances). @@ -134,9 +136,7 @@ impl PersonMatcher { self.swap_count += 1; if n_events < 7 { let swap_val = (prev as f32) * 16.0 + (curr as f32); - unsafe { - EVENTS[n_events] = (EVENT_PERSON_ID_SWAP, swap_val); - } + self.events[n_events] = (EVENT_PERSON_ID_SWAP, swap_val); n_events += 1; } } @@ -177,9 +177,7 @@ impl PersonMatcher { 0.0 }; let val = slot.person_id as f32 + confidence.min(0.99) * 0.01; - unsafe { - EVENTS[n_events] = (EVENT_PERSON_ID_ASSIGNED, val); - } + self.events[n_events] = (EVENT_PERSON_ID_ASSIGNED, val); n_events += 1; } } @@ -213,9 +211,7 @@ impl PersonMatcher { avg_conf /= n_det as f32; if n_events < 8 { - unsafe { - EVENTS[n_events] = (EVENT_MATCH_CONFIDENCE, avg_conf); - } + self.events[n_events] = (EVENT_MATCH_CONFIDENCE, avg_conf); n_events += 1; } } @@ -223,7 +219,7 @@ impl PersonMatcher { // Save current assignment for next-frame swap detection. self.prev_assignment = assignment; - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Extract top-FEAT_DIM variance values (descending) from a subcarrier range. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/sig_optimal_transport.rs b/v2/crates/wifi-densepose-wasm-edge/src/sig_optimal_transport.rs index 4ac1e997..e0a2fed1 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/sig_optimal_transport.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/sig_optimal_transport.rs @@ -84,12 +84,15 @@ pub struct OptimalTransportDetector { frame_count: u32, shift_streak: u8, subtle_streak: u8, + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], } impl OptimalTransportDetector { pub const fn new() -> Self { Self { prev_amps: [0.0; MAX_SC], smoothed_dist: 0.0, smoothed_var: 0.0, prev_var: 0.0, - initialized: false, frame_count: 0, shift_streak: 0, subtle_streak: 0 } + initialized: false, frame_count: 0, shift_streak: 0, subtle_streak: 0, + events: [(0, 0.0); 4] } } fn w1_sorted(a: &[f32], b: &[f32], n: usize) -> f32 { @@ -150,16 +153,15 @@ impl OptimalTransportDetector { i = 0; while i < n { self.prev_amps[i] = cur[i]; i += 1; } - static mut EV: [(i32, f32); 4] = [(0, 0.0); 4]; let mut ne = 0usize; if self.frame_count % 5 == 0 && ne < 4 { - unsafe { EV[ne] = (EVENT_WASSERSTEIN_DISTANCE, self.smoothed_dist); } ne += 1; + self.events[ne] = (EVENT_WASSERSTEIN_DISTANCE, self.smoothed_dist); ne += 1; } if self.smoothed_dist > WASS_SHIFT { self.shift_streak = self.shift_streak.saturating_add(1); if self.shift_streak >= SHIFT_DEB && ne < 4 { - unsafe { EV[ne] = (EVENT_DISTRIBUTION_SHIFT, self.smoothed_dist); } ne += 1; + self.events[ne] = (EVENT_DISTRIBUTION_SHIFT, self.smoothed_dist); ne += 1; self.shift_streak = 0; } } else { self.shift_streak = 0; } @@ -167,12 +169,12 @@ impl OptimalTransportDetector { if self.smoothed_dist > WASS_SUBTLE && vc < VAR_STABLE { self.subtle_streak = self.subtle_streak.saturating_add(1); if self.subtle_streak >= SUBTLE_DEB && ne < 4 { - unsafe { EV[ne] = (EVENT_SUBTLE_MOTION, self.smoothed_dist); } ne += 1; + self.events[ne] = (EVENT_SUBTLE_MOTION, self.smoothed_dist); ne += 1; self.subtle_streak = 0; } } else { self.subtle_streak = 0; } - unsafe { &EV[..ne] } + &self.events[..ne] } pub fn distance(&self) -> f32 { self.smoothed_dist } diff --git a/v2/crates/wifi-densepose-wasm-edge/src/sig_sparse_recovery.rs b/v2/crates/wifi-densepose-wasm-edge/src/sig_sparse_recovery.rs index c03168a7..948b2777 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/sig_sparse_recovery.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/sig_sparse_recovery.rs @@ -64,6 +64,8 @@ fn soft_threshold(x: f32, t: f32) -> f32 { /// Sparse subcarrier recovery engine. pub struct SparseRecovery { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 3], /// Compact correlation estimate: [MAX_SC][NEIGHBORS]. /// For subcarrier i: [corr(i,i-1), corr(i,i), corr(i,i+1)]. /// Edge entries (i=0 left neighbor, i=31 right neighbor) are zero. @@ -87,6 +89,7 @@ pub struct SparseRecovery { impl SparseRecovery { pub const fn new() -> Self { Self { + events: [(0, 0.0); 3], correlation: [[0.0; NEIGHBORS]; MAX_SC], recent_valid: [0.0; MAX_SC], initialized: false, @@ -135,20 +138,17 @@ impl SparseRecovery { } // -- Build event output ----------------------------------------------- - static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3]; let mut n_events = 0usize; // Always emit dropout rate periodically (every 20 frames). if self.frame_count % 20 == 0 { - unsafe { - EVENTS[n_events] = (EVENT_DROPOUT_RATE, dropout_rate); - } + self.events[n_events] = (EVENT_DROPOUT_RATE, dropout_rate); n_events += 1; } // -- Skip recovery if dropout too low or model not ready --------------- if dropout_rate < MIN_DROPOUT_RATE || !self.initialized { - unsafe { return &EVENTS[..n_events]; } + return &self.events[..n_events]; } // -- ISTA recovery ---------------------------------------------------- @@ -158,19 +158,15 @@ impl SparseRecovery { // Emit recovery results. if n_events < 3 { - unsafe { - EVENTS[n_events] = (EVENT_RECOVERY_COMPLETE, recovered as f32); - } + self.events[n_events] = (EVENT_RECOVERY_COMPLETE, recovered as f32); n_events += 1; } if n_events < 3 { - unsafe { - EVENTS[n_events] = (EVENT_RECOVERY_ERROR, residual); - } + self.events[n_events] = (EVENT_RECOVERY_ERROR, residual); n_events += 1; } - unsafe { &EVENTS[..n_events] } + &self.events[..n_events] } /// Update the compact correlation model from a fully valid frame. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/sig_temporal_compress.rs b/v2/crates/wifi-densepose-wasm-edge/src/sig_temporal_compress.rs index c6cf49e1..05b9e7df 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/sig_temporal_compress.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/sig_temporal_compress.rs @@ -54,12 +54,16 @@ pub struct TemporalCompressor { prev_ts: u32, has_ts: bool, ratio: f32, + /// Per-call event scratch buffers (owned; replace former `static mut`). + events: [(i32, f32); 4], + timer_events: [(i32, f32); 2], } impl TemporalCompressor { pub const fn new() -> Self { const E: Snap = Snap::empty(); - Self { buf: [E; CAP], w_idx: 0, total: 0, frame_rate: 20.0, prev_ts: 0, has_ts: false, ratio: 1.0 } + Self { buf: [E; CAP], w_idx: 0, total: 0, frame_rate: 20.0, prev_ts: 0, has_ts: false, ratio: 1.0, + events: [(0, 0.0); 4], timer_events: [(0, 0.0); 2] } } fn occ(&self) -> usize { if (self.total as usize) < CAP { self.total as usize } else { CAP } } @@ -97,7 +101,6 @@ impl TemporalCompressor { } self.prev_ts = ts_ms; self.has_ts = true; - static mut EV: [(i32, f32); 4] = [(0, 0.0); 4]; let mut ne = 0usize; let occ = self.occ(); @@ -113,23 +116,22 @@ impl TemporalCompressor { let mut j = 0; while j < VALS { let d = dequantize(self.buf[slot].data[j], s, old_l); self.buf[slot].data[j] = quantize(d, s, new_l); j += 1; } self.buf[slot].tier = new_t; - if ne < 4 { unsafe { EV[ne] = (EVENT_TIER_TRANSITION, new_t as i32 as f32); } ne += 1; } + if ne < 4 { self.events[ne] = (EVENT_TIER_TRANSITION, new_t as i32 as f32); ne += 1; } } } } self.ratio = self.calc_ratio(occ); - if self.total % 64 == 0 && ne < 4 { unsafe { EV[ne] = (EVENT_COMPRESSION_RATIO, self.ratio); } ne += 1; } - unsafe { &EV[..ne] } + if self.total % 64 == 0 && ne < 4 { self.events[ne] = (EVENT_COMPRESSION_RATIO, self.ratio); ne += 1; } + &self.events[..ne] } /// Periodic timer events. - pub fn on_timer(&self) -> &[(i32, f32)] { - static mut TE: [(i32, f32); 2] = [(0, 0.0); 2]; + pub fn on_timer(&mut self) -> &[(i32, f32)] { let mut n = 0; let h = self.history_hours(); - if h > 0.0 { unsafe { TE[n] = (EVENT_HISTORY_DEPTH_HOURS, h); } n += 1; } - unsafe { TE[n] = (EVENT_COMPRESSION_RATIO, self.ratio); } n += 1; - unsafe { &TE[..n] } + if h > 0.0 { self.timer_events[n] = (EVENT_HISTORY_DEPTH_HOURS, h); n += 1; } + self.timer_events[n] = (EVENT_COMPRESSION_RATIO, self.ratio); n += 1; + &self.timer_events[..n] } fn calc_ratio(&self, occ: usize) -> f32 { diff --git a/v2/crates/wifi-densepose-wasm-edge/src/spt_micro_hnsw.rs b/v2/crates/wifi-densepose-wasm-edge/src/spt_micro_hnsw.rs index 6f563aab..d40e6fa1 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/spt_micro_hnsw.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/spt_micro_hnsw.rs @@ -56,6 +56,8 @@ fn l2_query(stored: &[f32; DIM], query: &[f32]) -> f32 { /// Micro-HNSW on-device vector index. pub struct MicroHnsw { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], nodes: [HnswNode; MAX_VECTORS], n_vectors: usize, entry_point: usize, @@ -68,6 +70,7 @@ impl MicroHnsw { pub const fn new() -> Self { const EMPTY: HnswNode = HnswNode::empty(); Self { + events: [(0, 0.0); 4], nodes: [EMPTY; MAX_VECTORS], n_vectors: 0, entry_point: usize::MAX, frame_count: 0, last_nearest: 0, last_distance: f32::MAX, } @@ -194,9 +197,8 @@ impl MicroHnsw { pub fn process_frame(&mut self, features: &[f32]) -> &[(i32, f32)] { self.frame_count += 1; if self.n_vectors == 0 { - static mut EMPTY: [(i32, f32); 1] = [(0, 0.0); 1]; - unsafe { EMPTY[0] = (EVENT_LIBRARY_SIZE, 0.0); } - return unsafe { &EMPTY[..1] }; + self.events[0] = (EVENT_LIBRARY_SIZE, 0.0); + return &self.events[..1]; } let (nearest_id, distance) = self.search(features); self.last_nearest = nearest_id; @@ -205,14 +207,11 @@ impl MicroHnsw { self.nodes[nearest_id].label } else { CLASS_UNKNOWN }; - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; - unsafe { - EVENTS[0] = (EVENT_NEAREST_MATCH_ID, nearest_id as f32); - EVENTS[1] = (EVENT_MATCH_DISTANCE, distance); - EVENTS[2] = (EVENT_CLASSIFICATION, label as f32); - EVENTS[3] = (EVENT_LIBRARY_SIZE, self.n_vectors as f32); - } - unsafe { &EVENTS[..4] } + self.events[0] = (EVENT_NEAREST_MATCH_ID, nearest_id as f32); + self.events[1] = (EVENT_MATCH_DISTANCE, distance); + self.events[2] = (EVENT_CLASSIFICATION, label as f32); + self.events[3] = (EVENT_LIBRARY_SIZE, self.n_vectors as f32); + &self.events[..4] } pub fn size(&self) -> usize { self.n_vectors } diff --git a/v2/crates/wifi-densepose-wasm-edge/src/spt_pagerank_influence.rs b/v2/crates/wifi-densepose-wasm-edge/src/spt_pagerank_influence.rs index d608883c..41ee6c1d 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/spt_pagerank_influence.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/spt_pagerank_influence.rs @@ -48,6 +48,8 @@ pub const EVENT_INFLUENCE_CHANGE: i32 = 762; /// PageRank influence tracker. pub struct PageRankInfluence { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 8], /// Weighted adjacency matrix (row-major, adj[i][j] = correlation i<->j). adj: [[f32; MAX_PERSONS]; MAX_PERSONS], /// Current PageRank vector. @@ -63,6 +65,7 @@ pub struct PageRankInfluence { impl PageRankInfluence { pub const fn new() -> Self { Self { + events: [(0, 0.0); 8], adj: [[0.0; MAX_PERSONS]; MAX_PERSONS], rank: [0.25; MAX_PERSONS], prev_rank: [0.25; MAX_PERSONS], @@ -190,9 +193,8 @@ impl PageRankInfluence { } } - /// Build output events into a static buffer. - fn build_events(&self, np: usize) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 8] = [(0, 0.0); 8]; + /// Build output events into the owned per-call buffer. + fn build_events(&mut self, np: usize) -> &[(i32, f32)] { let mut n = 0usize; // Find dominant person. @@ -206,15 +208,11 @@ impl PageRankInfluence { } // Emit dominant person every frame. - unsafe { - EVENTS[n] = (EVENT_DOMINANT_PERSON, best_idx as f32); - } + self.events[n] = (EVENT_DOMINANT_PERSON, best_idx as f32); n += 1; // Emit influence score every frame. - unsafe { - EVENTS[n] = (EVENT_INFLUENCE_SCORE, best_rank); - } + self.events[n] = (EVENT_INFLUENCE_SCORE, best_rank); n += 1; // Emit change events for persons whose rank shifted significantly. @@ -223,14 +221,12 @@ impl PageRankInfluence { if fabsf(delta) > CHANGE_THRESHOLD && n < 8 { // Encode: integer part = person_id, fractional = clamped delta. let encoded = i as f32 + delta.clamp(-0.49, 0.49); - unsafe { - EVENTS[n] = (EVENT_INFLUENCE_CHANGE, encoded); - } + self.events[n] = (EVENT_INFLUENCE_CHANGE, encoded); n += 1; } } - unsafe { &EVENTS[..n] } + &self.events[..n] } /// Get the current PageRank score for a person. diff --git a/v2/crates/wifi-densepose-wasm-edge/src/spt_spiking_tracker.rs b/v2/crates/wifi-densepose-wasm-edge/src/spt_spiking_tracker.rs index 668b9fd8..f41b65b2 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/spt_spiking_tracker.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/spt_spiking_tracker.rs @@ -69,6 +69,8 @@ pub const EVENT_TRACK_LOST: i32 = 773; /// Spiking neural network person tracker. pub struct SpikingTracker { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Membrane potential of each input neuron. membrane: [f32; N_INPUT], /// Synaptic weights from input to output neurons. @@ -109,6 +111,7 @@ impl SpikingTracker { } Self { + events: [(0, 0.0); 4], membrane: [0.0; N_INPUT], weights, input_spike_time: [0; N_INPUT], @@ -242,8 +245,7 @@ impl SpikingTracker { } /// Construct event output. - fn build_events(&self, zone: i8, was_active: bool) -> &[(i32, f32)] { - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; + fn build_events(&mut self, zone: i8, was_active: bool) -> &[(i32, f32)] { let mut n = 0usize; // Mean spike rate across all zones. @@ -255,29 +257,29 @@ impl SpikingTracker { if zone >= 0 { // TRACK_UPDATE with zone ID. - unsafe { EVENTS[n] = (EVENT_TRACK_UPDATE, zone as f32); } + self.events[n] = (EVENT_TRACK_UPDATE, zone as f32); n += 1; // TRACK_VELOCITY. - unsafe { EVENTS[n] = (EVENT_TRACK_VELOCITY, self.velocity_ema); } + self.events[n] = (EVENT_TRACK_VELOCITY, self.velocity_ema); n += 1; // SPIKE_RATE. - unsafe { EVENTS[n] = (EVENT_SPIKE_RATE, mean_rate); } + self.events[n] = (EVENT_SPIKE_RATE, mean_rate); n += 1; } else { // SPIKE_RATE even when no track. - unsafe { EVENTS[n] = (EVENT_SPIKE_RATE, mean_rate); } + self.events[n] = (EVENT_SPIKE_RATE, mean_rate); n += 1; // TRACK_LOST if we had a track before. if was_active { - unsafe { EVENTS[n] = (EVENT_TRACK_LOST, self.prev_zone as f32); } + self.events[n] = (EVENT_TRACK_LOST, self.prev_zone as f32); n += 1; } } - unsafe { &EVENTS[..n] } + &self.events[..n] } /// Get the current tracked zone (-1 if lost). diff --git a/v2/crates/wifi-densepose-wasm-edge/src/tmp_goap_autonomy.rs b/v2/crates/wifi-densepose-wasm-edge/src/tmp_goap_autonomy.rs index 2b6b95b5..be3932ba 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/tmp_goap_autonomy.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/tmp_goap_autonomy.rs @@ -73,6 +73,8 @@ impl PlanNode { /// GOAP autonomy planner. pub struct GoapPlanner { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], world_state: WorldState, current_goal: u8, plan: [u8; MAX_PLAN_DEPTH], @@ -89,6 +91,7 @@ impl GoapPlanner { let mut p = [0.0f32; NUM_GOALS]; p[0]=0.9; p[1]=0.8; p[2]=0.7; p[3]=0.5; p[4]=0.3; p[5]=0.1; Self { + events: [(0, 0.0); 4], world_state: 0, current_goal: 0xFF, plan: [0xFF; MAX_PLAN_DEPTH], plan_len: 0, plan_step: 0, goal_priorities: p, timer_count: 0, replan_interval: 60, @@ -112,17 +115,16 @@ impl GoapPlanner { /// Called at ~1 Hz. Replans periodically and executes plan steps. pub fn on_timer(&mut self) -> &[(i32, f32)] { self.timer_count += 1; - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n = 0usize; // Replan at interval. if self.timer_count % self.replan_interval == 0 { let g = self.select_goal(); if g < NUM_GOALS as u8 { self.current_goal = g; - if n < 4 { unsafe { EVENTS[n] = (EVENT_GOAL_SELECTED, g as f32); } n += 1; } + if n < 4 { self.events[n] = (EVENT_GOAL_SELECTED, g as f32); n += 1; } let cost = self.plan_for_goal(g as usize); if cost < 255 && n < 4 { - unsafe { EVENTS[n] = (EVENT_PLAN_COST, cost as f32); } n += 1; + self.events[n] = (EVENT_PLAN_COST, cost as f32); n += 1; } } } @@ -135,16 +137,16 @@ impl GoapPlanner { let old = self.world_state; self.world_state = action.apply(self.world_state); if (self.world_state & !old) != 0 && n < 4 { - unsafe { EVENTS[n] = (EVENT_MODULE_ACTIVATED, aid as f32); } n += 1; + self.events[n] = (EVENT_MODULE_ACTIVATED, aid as f32); n += 1; } if (old & !self.world_state) != 0 && n < 4 { - unsafe { EVENTS[n] = (EVENT_MODULE_DEACTIVATED, aid as f32); } n += 1; + self.events[n] = (EVENT_MODULE_DEACTIVATED, aid as f32); n += 1; } } } self.plan_step += 1; } - unsafe { &EVENTS[..n] } + &self.events[..n] } fn select_goal(&self) -> u8 { diff --git a/v2/crates/wifi-densepose-wasm-edge/src/tmp_pattern_sequence.rs b/v2/crates/wifi-densepose-wasm-edge/src/tmp_pattern_sequence.rs index 118e681e..344f20ec 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/tmp_pattern_sequence.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/tmp_pattern_sequence.rs @@ -38,6 +38,8 @@ impl PatternEntry { const fn empty() -> Self { Self { symbols: [0; PATTERN_LEN], /// Temporal pattern sequence analyzer. pub struct PatternSequenceAnalyzer { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 4], /// Two-day history: [0..DAY_LEN)=yesterday, [DAY_LEN..2*DAY_LEN)=today. history: [u8; DAY_LEN * 2], minute_counter: u16, @@ -55,6 +57,7 @@ pub struct PatternSequenceAnalyzer { impl PatternSequenceAnalyzer { pub const fn new() -> Self { Self { + events: [(0, 0.0); 4], history: [0; DAY_LEN * 2], minute_counter: 0, day_offset: 0, pattern_lib: [PatternEntry::empty(); MAX_PATTERNS], n_patterns: 0, routine_confidence: 0.0, frame_votes: [0; 5], frames_in_minute: 0, @@ -72,7 +75,6 @@ impl PatternSequenceAnalyzer { /// Called at ~1 Hz. Commits symbols and runs hourly LCS comparison. pub fn on_timer(&mut self) -> &[(i32, f32)] { self.timer_count += 1; - static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4]; let mut n = 0usize; if self.timer_count % 60 == 0 && self.frames_in_minute > 0 { @@ -83,12 +85,12 @@ impl PatternSequenceAnalyzer { if self.day_offset > 0 { let predicted = self.history[self.minute_counter as usize]; if sym as u8 != predicted && n < 4 { - unsafe { EVENTS[n] = (EVENT_ROUTINE_DEVIATION, self.minute_counter as f32); } + self.events[n] = (EVENT_ROUTINE_DEVIATION, self.minute_counter as f32); n += 1; } let next_min = (self.minute_counter + 1) % DAY_LEN as u16; if n < 4 { - unsafe { EVENTS[n] = (EVENT_PREDICTION_NEXT, self.history[next_min as usize] as f32); } + self.events[n] = (EVENT_PREDICTION_NEXT, self.history[next_min as usize] as f32); n += 1; } } @@ -104,14 +106,14 @@ impl PatternSequenceAnalyzer { if wlen >= MIN_PATTERN_LEN { let lcs = self.compute_lcs(start, wlen); self.routine_confidence = if wlen > 0 { lcs as f32 / wlen as f32 } else { 0.0 }; - if n < 4 { unsafe { EVENTS[n] = (EVENT_PATTERN_CONFIDENCE, self.routine_confidence); } n += 1; } + if n < 4 { self.events[n] = (EVENT_PATTERN_CONFIDENCE, self.routine_confidence); n += 1; } if lcs >= MIN_PATTERN_LEN { self.store_pattern(start, wlen); - if n < 4 { unsafe { EVENTS[n] = (EVENT_PATTERN_DETECTED, lcs as f32); } n += 1; } + if n < 4 { self.events[n] = (EVENT_PATTERN_DETECTED, lcs as f32); n += 1; } } } } - unsafe { &EVENTS[..n] } + &self.events[..n] } fn majority_symbol(&self) -> Symbol { diff --git a/v2/crates/wifi-densepose-wasm-edge/src/tmp_temporal_logic_guard.rs b/v2/crates/wifi-densepose-wasm-edge/src/tmp_temporal_logic_guard.rs index 8b9431bf..fc17f10e 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/tmp_temporal_logic_guard.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/tmp_temporal_logic_guard.rs @@ -45,18 +45,19 @@ pub struct TemporalLogicGuard { vio_counts: [u32; NUM_RULES], frame_idx: u32, report_interval: u32, + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 12], } impl TemporalLogicGuard { pub const fn new() -> Self { Self { rules: [Rule::new(); NUM_RULES], vio_counts: [0; NUM_RULES], - frame_idx: 0, report_interval: 200 } + frame_idx: 0, report_interval: 200, events: [(0, 0.0); 12] } } /// Process one frame. Returns events to emit. pub fn on_frame(&mut self, input: &FrameInput) -> &[(i32, f32)] { self.frame_idx += 1; - static mut EV: [(i32, f32); 12] = [(0, 0.0); 12]; let mut n = 0usize; // G-rules (0-3, 6): violated when condition holds on any frame. @@ -75,10 +76,10 @@ impl TemporalLogicGuard { self.rules[rid].state = RuleState::Violated; self.rules[rid].vio_frame = self.frame_idx; self.vio_counts[rid] += 1; - if n + 1 < 12 { unsafe { - EV[n] = (EVENT_LTL_VIOLATION, rid as f32); - EV[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32); - } n += 2; } + if n + 1 < 12 { + self.events[n] = (EVENT_LTL_VIOLATION, rid as f32); + self.events[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32); + n += 2; } } } else { self.rules[rid].state = RuleState::Satisfied; } g += 1; @@ -86,18 +87,18 @@ impl TemporalLogicGuard { // Rule 4: F(motion_start -> motion_end within 300s). if self.check_deadline_rule(4, input.motion_energy > 0.1, MOTION_STOP_DEADLINE) { - if n + 1 < 12 { unsafe { - EV[n] = (EVENT_LTL_VIOLATION, 4.0); - EV[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32); - } n += 2; } + if n + 1 < 12 { + self.events[n] = (EVENT_LTL_VIOLATION, 4.0); + self.events[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32); + n += 2; } } // Rule 5: G(breathing>40 -> alert within 5s). if self.check_deadline_rule(5, input.breathing_bpm > 40.0, FAST_BREATH_DEADLINE) { - if n + 1 < 12 { unsafe { - EV[n] = (EVENT_LTL_VIOLATION, 5.0); - EV[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32); - } n += 2; } + if n + 1 < 12 { + self.events[n] = (EVENT_LTL_VIOLATION, 5.0); + self.events[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32); + n += 2; } } // Rule 7: G(seizure -> !normal_gait within 60s). @@ -113,10 +114,10 @@ impl TemporalLogicGuard { self.rules[7].state = RuleState::Violated; self.rules[7].vio_frame = self.frame_idx; self.vio_counts[7] += 1; - if n + 1 < 12 { unsafe { - EV[n] = (EVENT_LTL_VIOLATION, 7.0); - EV[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32); - } n += 2; } + if n + 1 < 12 { + self.events[n] = (EVENT_LTL_VIOLATION, 7.0); + self.events[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32); + n += 2; } } else if self.frame_idx >= self.rules[7].deadline { self.rules[7].state = RuleState::Satisfied; } @@ -129,10 +130,10 @@ impl TemporalLogicGuard { } if self.frame_idx % self.report_interval == 0 && n < 12 { - unsafe { EV[n] = (EVENT_LTL_SATISFACTION, self.satisfied_count() as f32); } + self.events[n] = (EVENT_LTL_SATISFACTION, self.satisfied_count() as f32); n += 1; } - unsafe { &EV[..n] } + &self.events[..n] } /// Generic deadline rule: condition triggers pending, expiry = violation, diff --git a/v2/crates/wifi-densepose-wasm-edge/src/vital_trend.rs b/v2/crates/wifi-densepose-wasm-edge/src/vital_trend.rs index 227f7547..8e7d5a94 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/vital_trend.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/vital_trend.rs @@ -137,6 +137,8 @@ impl VitalHistory { /// Vital trend analyzer. pub struct VitalTrendAnalyzer { + /// Per-call event scratch buffer (owned; replaces former `static mut`). + events: [(i32, f32); 8], breathing: VitalHistory, heartrate: VitalHistory, /// Debounce counters for each alert type. @@ -153,6 +155,7 @@ pub struct VitalTrendAnalyzer { impl VitalTrendAnalyzer { pub const fn new() -> Self { Self { + events: [(0, 0.0); 8], breathing: VitalHistory::new(), heartrate: VitalHistory::new(), bradypnea_count: 0, @@ -172,16 +175,13 @@ impl VitalTrendAnalyzer { self.breathing.push(breathing_bpm); self.heartrate.push(heartrate_bpm); - static mut EVENTS: [(i32, f32); 8] = [(0, 0.0); 8]; let mut n = 0usize; // ── Apnea detection (highest priority) ────────────────────────── if breathing_bpm < 1.0 { self.apnea_counter += 1; if self.apnea_counter >= APNEA_SECONDS { - unsafe { - EVENTS[n] = (EVENT_APNEA, self.apnea_counter as f32); - } + self.events[n] = (EVENT_APNEA, self.apnea_counter as f32); n += 1; } } else { @@ -192,9 +192,7 @@ impl VitalTrendAnalyzer { if breathing_bpm > 0.0 && breathing_bpm < BRADYPNEA_THRESH { self.bradypnea_count = self.bradypnea_count.saturating_add(1); if self.bradypnea_count >= ALERT_DEBOUNCE && n < 7 { - unsafe { - EVENTS[n] = (EVENT_BRADYPNEA, breathing_bpm); - } + self.events[n] = (EVENT_BRADYPNEA, breathing_bpm); n += 1; } } else { @@ -205,9 +203,7 @@ impl VitalTrendAnalyzer { if breathing_bpm > TACHYPNEA_THRESH { self.tachypnea_count = self.tachypnea_count.saturating_add(1); if self.tachypnea_count >= ALERT_DEBOUNCE && n < 7 { - unsafe { - EVENTS[n] = (EVENT_TACHYPNEA, breathing_bpm); - } + self.events[n] = (EVENT_TACHYPNEA, breathing_bpm); n += 1; } } else { @@ -218,9 +214,7 @@ impl VitalTrendAnalyzer { if heartrate_bpm > 0.0 && heartrate_bpm < BRADYCARDIA_THRESH { self.bradycardia_count = self.bradycardia_count.saturating_add(1); if self.bradycardia_count >= ALERT_DEBOUNCE && n < 7 { - unsafe { - EVENTS[n] = (EVENT_BRADYCARDIA, heartrate_bpm); - } + self.events[n] = (EVENT_BRADYCARDIA, heartrate_bpm); n += 1; } } else { @@ -231,9 +225,7 @@ impl VitalTrendAnalyzer { if heartrate_bpm > TACHYCARDIA_THRESH { self.tachycardia_count = self.tachycardia_count.saturating_add(1); if self.tachycardia_count >= ALERT_DEBOUNCE && n < 7 { - unsafe { - EVENTS[n] = (EVENT_TACHYCARDIA, heartrate_bpm); - } + self.events[n] = (EVENT_TACHYCARDIA, heartrate_bpm); n += 1; } } else { @@ -245,20 +237,16 @@ impl VitalTrendAnalyzer { let br_avg = self.breathing.mean_last(WINDOW_1M); let hr_avg = self.heartrate.mean_last(WINDOW_1M); if n < 7 { - unsafe { - EVENTS[n] = (EVENT_BREATHING_AVG, br_avg); - } + self.events[n] = (EVENT_BREATHING_AVG, br_avg); n += 1; } if n < 8 { - unsafe { - EVENTS[n] = (EVENT_HEARTRATE_AVG, hr_avg); - } + self.events[n] = (EVENT_HEARTRATE_AVG, hr_avg); n += 1; } } - unsafe { &EVENTS[..n] } + &self.events[..n] } /// Get the 1-minute breathing average. From 8ad0d0f91cc1c1b204cbca7c687f7026ecc07aed Mon Sep 17 00:00:00 2001 From: ruv Date: Fri, 12 Jun 2026 00:01:22 -0400 Subject: [PATCH 09/11] test+docs(wasm-edge): honest-labeling presence tests + ADR-160 (ADR-159 backlog now TRUE) - tests/honest_labeling.rs: 10 source-presence tests asserting the A1-A5 claim invariants (disclaimers present, uncited stat removed, WEAPON_ALERT no longer exported, med_* feature-gated, no static-mut event buffers). Each is designed to FAIL on the pre-fix source (ADR-159 A5 manifest-roundtrip style). - ADR-160: records the headline (0 stubs/0 theater, all real DSP -> claim-surface honesty debt), the graded A1-A5 fixes, NO-ACTION positives, per-prefix classification, and the DATA-GATED deferred backlog (criterion benches, per-skill accuracy validation, wasm32 static_mut_refs CI confirmation). - ADR-159: its deferred-backlog line "wasm-edge ... honestly labelled, not claimed" is now actually TRUE. Validation (all 0 failed, host --features std): DEFAULT 615 | MEDICAL (+medical-experimental) 653 | NO-DEFAULT 615; 0 warnings. Co-Authored-By: claude-flow --- .../ADR-159-cognitum-appliance-beyond-sota.md | 4 +- ...-160-edge-skill-library-honest-labeling.md | 228 +++++++++++++++ .../tests/honest_labeling.rs | 259 ++++++++++++++++++ 3 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 docs/adr/ADR-160-edge-skill-library-honest-labeling.md create mode 100644 v2/crates/wifi-densepose-wasm-edge/tests/honest_labeling.rs diff --git a/docs/adr/ADR-159-cognitum-appliance-beyond-sota.md b/docs/adr/ADR-159-cognitum-appliance-beyond-sota.md index 9c01ab41..2f9b2cc7 100644 --- a/docs/adr/ADR-159-cognitum-appliance-beyond-sota.md +++ b/docs/adr/ADR-159-cognitum-appliance-beyond-sota.md @@ -202,7 +202,9 @@ Audited and found genuinely correct; cited as positives, not edited: - **Criterion benches** for cog inference latency and `mesh_guard` — ACCEPTED-FUTURE (cold-start timings are recorded in the manifests' `build_metadata`, not yet a regression bench). -- **`wasm-edge` 70-skill accuracy** — unvalidated; honestly labelled, not claimed. +- **`wasm-edge` skill accuracy** — unvalidated; **now honestly labelled, not + claimed** (done in ADR-160: medical/affect/security/exotic claim surfaces + disclaimed, renamed, and feature-gated; per-skill accuracy remains DATA-GATED). ## Consequences diff --git a/docs/adr/ADR-160-edge-skill-library-honest-labeling.md b/docs/adr/ADR-160-edge-skill-library-honest-labeling.md new file mode 100644 index 00000000..90672aa7 --- /dev/null +++ b/docs/adr/ADR-160-edge-skill-library-honest-labeling.md @@ -0,0 +1,228 @@ +# ADR-160: Edge Skill Library (`wifi-densepose-wasm-edge`) — Honest Labeling & Soundness Cleanup + +- **Status**: accepted +- **Date**: 2026-06-11 +- **Deciders**: ruv +- **Tags**: wasm-edge, esp32, edge-skills, claim-surface, medical-overclaim, affect, prove-everything, soundness, static-mut +- **Amends**: ADR-159 (deferred-backlog line for wasm-edge now TRUE) + +## Context + +Beyond-SOTA sweep Milestone 6, over `v2/crates/wifi-densepose-wasm-edge` only, +executed under the project's **prove-everything / anti-"AI-slop"** directive. + +### Headline — 0 stubs, 0 theater, all real DSP (REFUTES the slop accusation) + +A read-only audit found this crate has **zero stubs and zero fake-output theater: +every one of the ~70 edge skills runs real DSP** (Welford statistics, +autocorrelation, DTW, sliced-Wasserstein, ISTA-style recovery, Kalman/HNSW, etc.). +The forward paths are genuine signal processing on real CSI-derived inputs. That +is the anti-slop win and it is cited here as a positive, not a fabrication. + +What the audit correctly found was **not fake code but an over-confident claim +surface**: skill *names* and doc-comments asserting clinical/affective/security +capabilities that the **unvalidated** code cannot back, concentrated in the +medical (`med_*`) and affect (`exo_happiness`/`exo_emotion`) skills. The fix is +**honest labeling — making the labels TRUE — NOT making the claimed capability +real.** You cannot validate seizure detection, affect inference, or weapon +discrimination without clinical/labelled data and reference standards; this ADR +does not pretend to. It disclaims, renames, softens, and feature-gates so the +surface matches what the DSP actually delivers. + +Grading vocabulary follows ADR-152 / ADR-158 / ADR-159: +- **MEASURED** — reproduced in this worktree, command + failing-on-old test recorded. +- **DATA-GATED** — real code path present; honestly flagged where data is absent. +- **NO-ACTION (already-honest)** — audited, found correct, cited as a positive. +- **ACCEPTED-FUTURE** — deliberately deferred, nothing dropped. + +## Per-prefix classification + +| Prefix | Class | Note | +|--------|-------|------| +| `sig_*` (signal intelligence) | **REAL-DSP, honest** | Algorithm-named (flash-attention, sparse-recovery, optimal-transport, temporal-compress, mincut). Names describe the math, not an overclaimed outcome. NO-ACTION on labels; A5 soundness applied. | +| `lrn_*` (adaptive learning) | **REAL-DSP, honest** | DTW/EWC/meta-adapt/attractor — algorithm-named. NO-ACTION on labels; A5 applied. | +| `spt_*` / `tmp_*` | **REAL-DSP, honest** | PageRank/HNSW/spiking-tracker; LTL-guard/GOAP/pattern-sequence. Algorithm-named. NO-ACTION on labels; A5 applied. | +| `qnt_*` | **REAL-DSP, honest (disclosed analogy)** | "quantum-**inspired**" / Grover-**inspired** are already disclosed analogies. NO-ACTION (DO-NOT-touch); A5 applied (mechanical, no label/behavior change). | +| `bld_*` / `ret_*` / `ind_*` / `occupancy`/`intrusion` | **REAL-DSP, honest** | Occupancy/queue/forklift/clean-room etc. describe physical observables. NO-ACTION on labels; A5 applied. | +| `sec_weapon_detect` | **REAL-DSP, overclaiming NAME** → fixed (A3) | Variance-ratio reflectivity renamed off "weapon". | +| `med_*` (5) | **REAL-DSP, overclaiming NAME/DOC** → fixed (A1) | Clinical detection asserted as fact; now disclaimed + softened + feature-gated. | +| `exo_happiness` / `exo_emotion` | **REAL-DSP, overclaiming NAME/DOC** → fixed (A2) | Affect outputs reframed as proxies; uncited stat removed. | +| `exo_dream_stage` / `exo_gesture_language` | **REAL-DSP, quasi-medical/over-named** → fixed (A4) | Disclaimers added; Research tag promoted to header. | +| `exo_time_crystal` / `exo_ghost_hunter` | **REAL-DSP, honest novelty** | Disclosed exploratory/novelty skills. NO-ACTION (DO-NOT-touch); A5 applied. | +| `nvsim` | out of scope | Disclaimer gold standard; copied its tone. | + +## Decision — Fixes Landed + +### §A1 Medical overclaim (HIGH) — MEASURED + +The five `med_*` modules (`med_seizure_detect`, `med_cardiac_arrhythmia`, +`med_respiratory_distress`, `med_sleep_apnea`, `med_gait_analysis`) stated clinical +detection as fact with no disclaimer ("Detects tonic-clonic seizures…"). + +**Real fix (honest labeling — the DSP is kept, untouched):** +- **(a)** Every module's `//!` header now carries a mandatory disclaimer block, + modelled on `sec_weapon_detect.rs` and `nvsim/src/lib.rs`: *"EXPERIMENTAL + RESEARCH MODULE — NOT VALIDATED AGAINST CLINICAL DATA. NOT A MEDICAL DEVICE. + Flags candidate -like signatures only,"* citing ADR-160. +- **(b)** Doc verbs softened: *"Detects tonic-clonic seizures"* → + *"Flags candidate tonic-clonic-seizure-like motion signatures (experimental)"*; + similarly for cardiac/respiratory/apnea/gait. +- **(c)** All five gated behind a new **non-default** cargo feature + `medical-experimental` (`#[cfg(feature = "medical-experimental")]` in `lib.rs`, + `medical-experimental = []` in `Cargo.toml`, **not** in `default`) so they cannot + be silently built into a shipping artifact. + +**Failing-on-old tests** (`tests/honest_labeling.rs`): +`a1_med_modules_have_clinical_disclaimer`, +`a1_med_modules_gated_behind_medical_experimental`, +`a1_seizure_verbs_softened`. All fail on the old, undisclaimed, ungated source. +**Grade: MEASURED (label); per-skill clinical accuracy DATA-GATED.** + +### §A2 Affect overclaim (HIGH) — MEASURED + +`exo_happiness_score.rs` carried an **uncited** "Happy people walk ~12% faster" +statistic and emits `HAPPINESS_SCORE`; `exo_emotion_detect.rs` emits +`STRESS_INDEX`/`CALM_DETECTED`/`AGITATION_DETECTED`. + +**Real fix (honest labeling — math kept):** +- Deleted the uncited "12% faster" / "~12% above" / "Happy people walk" statements. +- Added a prominent *"speculative, unvalidated affect heuristic; outputs are NOT + measurements of emotion"* disclaimer to both `//!` headers, citing ADR-160. +- Reframed `HAPPINESS_SCORE` in the docs as a **"gait-energy proxy, not a validated + affect measure."** + +**Failing-on-old tests:** `a2_affect_modules_have_unvalidated_disclaimer`, +`a2_uncited_12_percent_stat_removed`, `a2_happiness_reframed_as_proxy`. +**Grade: MEASURED (label); affect validity DATA-GATED.** + +### §A3 Security event-name overclaim (MEDIUM) — MEASURED + +`sec_weapon_detect.rs`'s module doc was already honest (research-grade, +calibration-required), but the event/const names claimed weapon-grade +discrimination a variance ratio cannot deliver. + +**Real fix (honest physical-quantity naming — behavior unchanged):** +- `EVENT_WEAPON_ALERT` → `EVENT_HIGH_METAL_REFLECTIVITY` (event id 221 unchanged). +- `WEAPON_RATIO_THRESH` → `HIGH_REFLECTIVITY_THRESH`. +- Internal fields/consts renamed (`weapon_run`→`high_refl_run`, + `cd_weapon`→`cd_high_refl`, `WEAPON_DEBOUNCE`→`HIGH_REFLECTIVITY_DEBOUNCE`). +- `lib.rs` `event_types` registry: `WEAPON_ALERT` → `HIGH_METAL_REFLECTIVITY`. +- A reflectivity-vs-weapons honest-naming note added to the header. +The detector still flags a high amplitude-variance/phase-variance ratio (real RF +reflectivity); it just no longer *names* that "weapon". + +**Failing-on-old tests:** `a3_weapon_names_renamed_to_reflectivity`, +`a3_registry_no_longer_exports_weapon_alert` (registry no longer exports a +`WEAPON_ALERT` name). **Grade: MEASURED.** + +### §A4 Quasi-medical / sign-language exotic modules (MEDIUM) — MEASURED + +`exo_dream_stage.rs` ("sleep stage classification", quasi-medical) and +`exo_gesture_language.rs` ("sign language letter recognition"). + +**Real fix (honest labeling — DSP kept):** added an experimental "NOT VALIDATED" +disclaimer to each `//!` header (citing ADR-160) and promoted the +**Exotic/Research** registry tag into the header where a reader sees it. +`exo_gesture_language` additionally states it is a coarse gesture-cluster +classifier that **does not recognize true sign language** (never evaluated on a +labelled ASL set). + +**Failing-on-old test:** `a4_exotic_modules_have_experimental_disclaimer`. +**Grade: MEASURED (label); accuracy DATA-GATED.** + +### §A5 `static mut` event-buffer soundness (MEDIUM) — the one real code fix — MEASURED + +~61 per-call event scratch buffers across the crate used a module-level +`static mut EVENTS: [(i32,f32); N]` (a handful named `EV`/`TE`/`EMPTY`) and returned +`&EVENTS[..n]`. On a `cdylib`+`rlib` linkable into multithreaded/reentrant host +code this is latent aliasing UB, and `static_mut_refs` is deny-by-default on newer +Rust. + +**Real fix (mechanical, behavior-preserving):** moved each scratch buffer off +`static mut` into an **owned per-instance field** (`events: [(i32,f32); N]` on the +detector struct, written via `&mut self` and returned as `&self.events[..n]`). The +public `-> &[(i32, f32)]` signature is **unchanged**, so no caller (in-module +tests, `ghost_hunter` bin, `budget_compliance`) needed editing. Two helper methods +that built events under `&self` (`spt_pagerank_influence::build_events`, +`spt_spiking_tracker::build_events`) and `sig_temporal_compress::on_timer` were +promoted to `&mut self`. Leftover now-redundant `unsafe { }` wrappers were removed. + +**Count: 61 scratch buffers across 60 module files fixed** (the only `static mut` +left in `src/` are the two **legitimate WASM module singletons** — `lib.rs STATE` +and `bin/ghost_hunter.rs DETECTOR` — `#[cfg(target_arch="wasm32")]`, +`#[no_mangle]`, accessed via `core::ptr::addr_of_mut!`, single-threaded by the +wasm runtime contract; these are *not* the aliasing-UB scratch pattern and are +left as-is). + +**Verification:** the full host build (`--features std` and +`std,medical-experimental`) compiles with **0 warnings** — there is no longer any +`static mut ` + `&` source for `static_mut_refs` to fire on in the 60 +fixed modules. (The pure-`wasm32-unknown-unknown` build, where the lint is +deny-by-default, could not be run in this worktree because the `wasm32` target is +not installed on the build toolchain; the source-level elimination is the +evidence, asserted per-module by `a5_claim_bearing_modules_have_no_static_mut_event_buffer`.) +**Grade: MEASURED (source-eliminated; residual = 2 legitimate singletons).** + +## Negative Results (NO-ACTION positives — cited, not edited for labels) + +Audited and found genuinely honest; cited as positives: +- **`qnt_quantum_coherence.rs`** — discloses "quantum-**inspired**" analogy. +- **`exo_time_crystal.rs`**, **`exo_ghost_hunter.rs`** — disclosed exploratory/novelty. +- **`qnt_interference_search.rs`** — disclosed "Grover-**inspired**". +- **`sig_*` / `lrn_*`** algorithm-named skills — names describe the DSP, not an outcome. +- **`nvsim`** — out of scope; the project's disclaimer gold standard (its tone was + copied into the A1/A2/A4 disclaimers). + +(These were A5-soundness-fixed mechanically where they used `static mut`, with no +label or behavior change, consistent with leaving their claim surface intact.) + +## Deferred Backlog (Nothing Dropped) + +- **Per-skill accuracy validation** — **DATA-GATED**. Validating any med_*/affect/ + sign-language claim requires labelled clinical/affective/ASL data and reference + standards that do not exist in this repo. The disclaimers + feature gate are the + honest stand-in. Nothing is claimed that is not measured. +- **Criterion benches for `process_frame` budget claims** — **ACCEPTED-FUTURE**. + `tests/budget_compliance.rs` asserts L/S/H tier wall-clock budgets (25 tests, + passing), but a regression-grade criterion bench is not yet wired. +- **`wasm32-unknown-unknown` `static_mut_refs` confirmation** — **ACCEPTED-FUTURE** + (toolchain): the source pattern is eliminated; a CI job on the wasm target should + assert zero `static_mut_refs` once the target is added to the build image. +- **The 2 residual `static mut` singletons** (`lib.rs STATE`, `ghost_hunter DETECTOR`) + — **ACCEPTED-FUTURE**: these are the canonical wasm module-state pattern; migrating + them to a safe cell is a separate, larger change with no current UB (single-threaded + wasm runtime, `addr_of_mut!` access). + +## Reproduction (MEASURED) + +```bash +cd v2/crates/wifi-densepose-wasm-edge # excluded from the v2 workspace; build here +cargo test --features std # default +cargo test --features std,medical-experimental # med_* skills enabled +cargo test --no-default-features --features std # no default-pipeline +cargo test --features std --test honest_labeling # A1–A5 label invariants +``` + +(`std` is required for host tests — the crate is `no_std` for `wasm32`; pure +`--no-default-features` builds only on `wasm32-unknown-unknown`, where it +intentionally has no panic handler on the host.) + +Result at time of writing (all 0 failed): +- **DEFAULT** (`--features std`) — **615 passed** (lib 504; budget 25; honest_labeling 10; bench 1; vendor 75) +- **MEDICAL** (`--features std,medical-experimental`) — **653 passed** (lib 542; +38 med_* tests; others unchanged) +- **NO-DEFAULT** (`--no-default-features --features std`) — **615 passed** +- Full host build emits **0 warnings**; **61** `static mut` scratch buffers eliminated, **2** legitimate wasm singletons remain. + +## Consequences + +- No edge skill's name or doc-comment claims a clinical, affective, security, or + sign-language capability the unvalidated DSP cannot back. +- The five medical skills cannot be silently compiled into a shipping artifact + (non-default `medical-experimental` gate). +- The security skill can never emit a "weapon alert" — it reports + `HIGH_METAL_REFLECTIVITY`, the physical quantity it actually measures. +- The latent `static mut` aliasing-UB / `static_mut_refs` exposure is removed from + 60 modules; the public API and all runtime behavior are unchanged (615/653 tests + prove behavior preservation). +- ADR-159's deferred-backlog statement *"wasm-edge … honestly labelled, not + claimed"* is now actually TRUE. diff --git a/v2/crates/wifi-densepose-wasm-edge/tests/honest_labeling.rs b/v2/crates/wifi-densepose-wasm-edge/tests/honest_labeling.rs new file mode 100644 index 00000000..20c86307 --- /dev/null +++ b/v2/crates/wifi-densepose-wasm-edge/tests/honest_labeling.rs @@ -0,0 +1,259 @@ +//! Honest-labeling source-presence tests (ADR-160). +//! +//! These tests assert that the claim-surface fixes A1–A4 are physically present +//! in the source (disclaimers added, uncited stats removed, overclaiming names +//! renamed). They are deliberately source-text assertions (`include_str!`), +//! mirroring ADR-159 §A5 / `cog-pose-estimation`'s `manifest_roundtrips` pattern: +//! the win here is making the *labels* true, which is a documentation invariant, +//! not a runtime capability. Each test is designed to FAIL on the pre-fix source. + +// ── A1: medical modules carry the mandatory disclaimer + feature gate ───────── + +const MED_SEIZURE: &str = include_str!("../src/med_seizure_detect.rs"); +const MED_CARDIAC: &str = include_str!("../src/med_cardiac_arrhythmia.rs"); +const MED_RESP: &str = include_str!("../src/med_respiratory_distress.rs"); +const MED_APNEA: &str = include_str!("../src/med_sleep_apnea.rs"); +const MED_GAIT: &str = include_str!("../src/med_gait_analysis.rs"); + +const MED_MODULES: &[(&str, &str)] = &[ + ("med_seizure_detect", MED_SEIZURE), + ("med_cardiac_arrhythmia", MED_CARDIAC), + ("med_respiratory_distress", MED_RESP), + ("med_sleep_apnea", MED_APNEA), + ("med_gait_analysis", MED_GAIT), +]; + + +/// Char-boundary-safe prefix of up to `max` bytes (module headers are ASCII-ish +/// but contain box-drawing chars, so a naive byte slice can split a UTF-8 char). +fn char_safe_prefix(s: &str, max: usize) -> &str { + let mut end = s.len().min(max); + while end > 0 && !s.is_char_boundary(end) { end -= 1; } + &s[..end] +} + +/// A1(a): every med_* module's `//!` header must carry the mandatory disclaimer +/// stating it is experimental, not clinically validated, and not a medical device. +#[test] +fn a1_med_modules_have_clinical_disclaimer() { + for (name, src) in MED_MODULES { + // Search the whole module doc-comment region (first ~2KB) for robustness. + let scan = char_safe_prefix(src, 2048); + assert!( + scan.contains("NOT VALIDATED AGAINST CLINICAL DATA"), + "{name}: missing 'NOT VALIDATED AGAINST CLINICAL DATA' disclaimer" + ); + assert!( + scan.contains("NOT A MEDICAL DEVICE"), + "{name}: missing 'NOT A MEDICAL DEVICE' disclaimer" + ); + assert!( + scan.contains("EXPERIMENTAL"), + "{name}: missing 'EXPERIMENTAL' marker" + ); + // ADR cross-reference so the disclaimer is traceable. + assert!( + scan.contains("ADR-160"), + "{name}: disclaimer should cite ADR-160" + ); + } +} + +/// A1(c): all five med_* modules must be gated behind the non-default +/// `medical-experimental` cargo feature in lib.rs (cannot be silently shipped). +#[test] +fn a1_med_modules_gated_behind_medical_experimental() { + const LIB: &str = include_str!("../src/lib.rs"); + for (name, _) in MED_MODULES { + // Each module declaration must be immediately preceded by the cfg gate. + let decl = format!("pub mod {name};"); + let idx = LIB + .find(&decl) + .unwrap_or_else(|| panic!("{name}: `{decl}` not found in lib.rs")); + let preceding = &LIB[idx.saturating_sub(80)..idx]; + assert!( + preceding.contains("#[cfg(feature = \"medical-experimental\")]"), + "{name}: `{decl}` not gated behind medical-experimental in lib.rs" + ); + } + // The feature itself must exist in Cargo.toml. + const CARGO: &str = include_str!("../Cargo.toml"); + assert!( + CARGO.contains("medical-experimental = []"), + "Cargo.toml missing `medical-experimental` feature definition" + ); + // And it must NOT be in the default feature set. + let default_line = CARGO + .lines() + .find(|l| l.trim_start().starts_with("default = [")) + .expect("Cargo.toml missing default features"); + assert!( + !default_line.contains("medical-experimental"), + "medical-experimental must be NON-default; found in: {default_line}" + ); +} + +/// A1(b): the seizure module must no longer assert detection as fact +/// ("Detects tonic-clonic seizures") and must use softened "candidate"/"flags" +/// language instead. +#[test] +fn a1_seizure_verbs_softened() { + assert!( + !MED_SEIZURE.contains("Detects tonic-clonic seizures"), + "med_seizure_detect still asserts 'Detects tonic-clonic seizures' as fact" + ); + assert!( + MED_SEIZURE.contains("candidate") && MED_SEIZURE.contains("signature"), + "med_seizure_detect should describe 'candidate ... signatures' (experimental)" + ); +} + +// ── A2: affect modules carry the speculative/unvalidated disclaimer ─────────── + +const EXO_HAPPINESS: &str = include_str!("../src/exo_happiness_score.rs"); +const EXO_EMOTION: &str = include_str!("../src/exo_emotion_detect.rs"); + +/// A2: both affect modules must declare outputs are NOT measurements of emotion +/// and cite ADR-160. +#[test] +fn a2_affect_modules_have_unvalidated_disclaimer() { + for (name, src) in [("exo_happiness_score", EXO_HAPPINESS), ("exo_emotion_detect", EXO_EMOTION)] { + let scan = char_safe_prefix(src, 2048); + assert!( + scan.contains("NOT measurements of emotion") || scan.contains("NOT a") + && scan.contains("affect"), + "{name}: missing 'NOT measurements of emotion' style disclaimer" + ); + assert!( + scan.to_lowercase().contains("speculative") + || scan.to_lowercase().contains("unvalidated"), + "{name}: missing speculative/unvalidated qualifier" + ); + assert!(scan.contains("ADR-160"), "{name}: disclaimer should cite ADR-160"); + } +} + +/// A2: the uncited "Happy people walk ~12% faster" statistic must be deleted. +#[test] +fn a2_uncited_12_percent_stat_removed() { + assert!( + !EXO_HAPPINESS.contains("12% faster"), + "exo_happiness_score still contains the uncited '12% faster' claim" + ); + assert!( + !EXO_HAPPINESS.contains("~12% above"), + "exo_happiness_score still contains the uncited '~12% above' claim" + ); + assert!( + !EXO_HAPPINESS.contains("Happy people walk"), + "exo_happiness_score still contains the uncited 'Happy people walk' claim" + ); +} + +/// A2: HAPPINESS_SCORE must be documented as a gait-energy proxy, not an affect +/// measurement. +#[test] +fn a2_happiness_reframed_as_proxy() { + assert!( + EXO_HAPPINESS.contains("gait-energy proxy"), + "exo_happiness_score should document HAPPINESS_SCORE as a 'gait-energy proxy'" + ); +} + +// ── A3: weapon-detect renamed to honest physical quantities ─────────────────── + +const SEC_WEAPON: &str = include_str!("../src/sec_weapon_detect.rs"); +const LIB_RS: &str = include_str!("../src/lib.rs"); + +/// A3: the weapon-grade overclaim must be gone from the event/const names. +#[test] +fn a3_weapon_names_renamed_to_reflectivity() { + // The module must no longer *define* a WEAPON_ALERT event or WEAPON_RATIO_THRESH + // const. (A doc-comment may still reference the old name historically, e.g. + // "formerly `EVENT_WEAPON_ALERT`" — we assert on the definitions, not mentions.) + assert!( + !SEC_WEAPON.contains("pub const EVENT_WEAPON_ALERT"), + "sec_weapon_detect still defines/exports EVENT_WEAPON_ALERT" + ); + assert!( + !SEC_WEAPON.contains("const WEAPON_RATIO_THRESH"), + "sec_weapon_detect still defines WEAPON_RATIO_THRESH" + ); + // Honest replacements must be present. + assert!( + SEC_WEAPON.contains("EVENT_HIGH_METAL_REFLECTIVITY"), + "sec_weapon_detect missing renamed EVENT_HIGH_METAL_REFLECTIVITY" + ); + assert!( + SEC_WEAPON.contains("HIGH_REFLECTIVITY_THRESH"), + "sec_weapon_detect missing renamed HIGH_REFLECTIVITY_THRESH" + ); +} + +/// A3: the lib.rs event registry must no longer export a `WEAPON_ALERT` name. +#[test] +fn a3_registry_no_longer_exports_weapon_alert() { + assert!( + !LIB_RS.contains("pub const WEAPON_ALERT"), + "event_types registry still exports WEAPON_ALERT" + ); + assert!( + LIB_RS.contains("pub const HIGH_METAL_REFLECTIVITY"), + "event_types registry missing HIGH_METAL_REFLECTIVITY (id 221)" + ); +} + +// ── A4: quasi-medical / sign-language exotic modules carry the disclaimer ────── + +const EXO_DREAM: &str = include_str!("../src/exo_dream_stage.rs"); +const EXO_SIGN: &str = include_str!("../src/exo_gesture_language.rs"); + +/// A4: dream-stage and gesture-language modules promote the Exotic/Research tag +/// into a header disclaimer and state they are not validated. +#[test] +fn a4_exotic_modules_have_experimental_disclaimer() { + for (name, src) in [("exo_dream_stage", EXO_DREAM), ("exo_gesture_language", EXO_SIGN)] { + let scan = char_safe_prefix(src, 2048); + assert!( + scan.contains("EXPERIMENTAL") && scan.contains("NOT VALIDATED"), + "{name}: missing EXPERIMENTAL / NOT VALIDATED disclaimer" + ); + assert!(scan.contains("ADR-160"), "{name}: disclaimer should cite ADR-160"); + assert!( + scan.contains("Research"), + "{name}: should surface the Exotic/Research registry tag in the header" + ); + } +} + +// ── A5: the static_mut soundness fix is present (no per-call static mut bufs) ── + +/// A5: claim-bearing modules must no longer use a `static mut` event scratch +/// buffer (latent aliasing UB). They now own a per-instance `events` field. +#[test] +fn a5_claim_bearing_modules_have_no_static_mut_event_buffer() { + let modules: &[(&str, &str)] = &[ + ("med_seizure_detect", MED_SEIZURE), + ("med_cardiac_arrhythmia", MED_CARDIAC), + ("med_respiratory_distress", MED_RESP), + ("med_sleep_apnea", MED_APNEA), + ("med_gait_analysis", MED_GAIT), + ("exo_happiness_score", EXO_HAPPINESS), + ("exo_emotion_detect", EXO_EMOTION), + ("sec_weapon_detect", SEC_WEAPON), + ("exo_dream_stage", EXO_DREAM), + ("exo_gesture_language", EXO_SIGN), + ]; + for (name, src) in modules { + assert!( + !src.contains("static mut EVENTS") + && !src.contains("static mut EV:") + && !src.contains("static mut EMPTY"), + "{name}: still uses a `static mut` event scratch buffer (A5 not applied)" + ); + assert!( + src.contains("events: [(i32, f32);"), + "{name}: missing owned `events` scratch buffer field (A5)" + ); + } +} From d120cc227810b704219e4c48a13447cd906d0a2e Mon Sep 17 00:00:00 2001 From: ruv Date: Fri, 12 Jun 2026 00:11:24 -0400 Subject: [PATCH 10/11] test(sensing-server): unique per-process temp dirs (deterministic under concurrent runs) checkpoint_round_trip / rvf_test / rvf_pipeline_test shared fixed temp_dir paths and remove_dir at teardown, so two concurrent/repeated test runs raced (one's teardown wiped the other's file -> NotFound). Make each dir process-unique. Test-only; no public API change. Co-Authored-By: claude-flow --- v2/crates/wifi-densepose-sensing-server/src/rvf_container.rs | 2 +- v2/crates/wifi-densepose-sensing-server/src/rvf_pipeline.rs | 2 +- v2/crates/wifi-densepose-sensing-server/src/trainer.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v2/crates/wifi-densepose-sensing-server/src/rvf_container.rs b/v2/crates/wifi-densepose-sensing-server/src/rvf_container.rs index 78e251bd..e3d4d29a 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/rvf_container.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/rvf_container.rs @@ -894,7 +894,7 @@ mod tests { #[test] fn file_round_trip() { - let dir = std::env::temp_dir().join("rvf_test"); + let dir = std::env::temp_dir().join(format!("rvf_test_{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("test_model.rvf"); diff --git a/v2/crates/wifi-densepose-sensing-server/src/rvf_pipeline.rs b/v2/crates/wifi-densepose-sensing-server/src/rvf_pipeline.rs index 5c0c3f7c..55b40220 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/rvf_pipeline.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/rvf_pipeline.rs @@ -1002,7 +1002,7 @@ mod tests { #[test] fn rvf_model_file_round_trip() { - let dir = std::env::temp_dir().join("rvf_pipeline_test"); + let dir = std::env::temp_dir().join(format!("rvf_pipeline_test_{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("pipeline_model.rvf"); diff --git a/v2/crates/wifi-densepose-sensing-server/src/trainer.rs b/v2/crates/wifi-densepose-sensing-server/src/trainer.rs index b77fe674..66588563 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/trainer.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/trainer.rs @@ -1318,7 +1318,7 @@ mod tests { let mut t = Trainer::new(TrainerConfig::default()); t.train_epoch(&[sample()]); let ckpt = t.checkpoint(); - let dir = std::env::temp_dir().join("trainer_ckpt_test"); + let dir = std::env::temp_dir().join(format!("trainer_ckpt_test_{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("ckpt.json"); ckpt.save_to_file(&path).unwrap(); From 8487192d0fc0ad3b20147378488ac0cbbd1fcb43 Mon Sep 17 00:00:00 2001 From: ruv Date: Fri, 12 Jun 2026 00:19:43 -0400 Subject: [PATCH 11/11] docs(proof): PROOF.md capstone + scripts/prove.sh reproduction harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One-command harness: clone, run scripts/prove.sh, and every headline claim is either verified on your machine (re-runs the bug-catching tests) or printed as 'CLAIMED — not reproduced here' with the exact prerequisite. Hard gate = workspace tests + deterministic Python proof; section 3 re-runs 7 anti-slop assertion tests (each fails on pre-fix code); gated claims (GPU/dataset/hardware/ trained-checkpoint/named-identity) are honestly listed, never faked. Co-Authored-By: claude-flow --- PROOF.md | 75 ++++++++++++++++++++++++ scripts/prove.sh | 146 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 PROOF.md create mode 100644 scripts/prove.sh diff --git a/PROOF.md b/PROOF.md new file mode 100644 index 00000000..db651c80 --- /dev/null +++ b/PROOF.md @@ -0,0 +1,75 @@ +# PROOF — reproduce every claim, or find the one we can't yet + +This project (RuView / wifi-densepose) has been publicly called "AI slop" and +"fake." This document is the answer: **a skeptic can clone the repo, run one +script, and have every headline claim either verified on their own machine or +shown — explicitly — as "CLAIMED, not yet reproduced (here's exactly what it +needs)."** Nothing below is asserted without a command you can run. + +```bash +git clone https://github.com/ruvnet/RuView && cd RuView +bash scripts/prove.sh # core gate + the anti-slop assertion tests +bash scripts/prove.sh --full # also attempt the feature-gated subset +``` + +`prove.sh` exits 0 only if every **non-gated** claim passes. Gated claims never +fail the run; they print the prerequisite (a GPU, a dataset, real hardware, a +trained checkpoint) so you can reproduce them yourself. + +## Grading + +- **MEASURED** — reproduced on our hardware, with the exact command recorded, and + pinned by a test that *fails on the pre-fix code*. `prove.sh` re-runs these. +- **CLAIMED** — cited from a source, or measured by the source, but not + reproduced in this repo's automated harness. +- **DATA-GATED / HARDWARE-GATED** — the *code path* is real and tested, but the + *accuracy/throughput claim* needs data or hardware we don't ship. We never + fabricate the number; the code carries a typed error or a `weights_trained`/ + provenance flag instead. + +## The hard gate (run on any machine with Rust + Python) + +| Claim | Grade | Reproduce | +|---|---|---| +| Rust workspace: 3,128 tests, 0 failed | **MEASURED** | `cd v2 && cargo test --workspace --no-default-features` | +| Deterministic CSI pipeline proof (bit-exact SHA-256) | **MEASURED** | `python archive/v1/data/proof/verify.py` → `VERDICT: PASS` | + +## Anti-slop assertion tests (each fails on the pre-fix code) + +| Claim | Grade | Test (run via `cargo test -p `) | +|---|---|---| +| Fusion crafted-input DoS panics are closed (ADR-156 §2.2) | **MEASURED** | `wifi-densepose-ruvector :: triangulation_out_of_range_index_returns_none_no_panic` | +| **The "Soul Signature" identity claim, honestly bounded:** on WiFi-only cardiac+respiratory channels two people are **not separable** (gap ≈ 0.0005) | **MEASURED** | `wifi-densepose-bfld :: cardiac_alone_cannot_separate_identity_matches_audit` | +| OccWorld `predict()` is real (input-dependent), not random noise | **MEASURED** | `wifi-densepose-occworld-candle :: predict_is_deterministic_for_same_input` | +| Pose runtime emits frames under its own default config (ADR-159 A1) | **MEASURED** | `cog-pose-estimation :: default_config_emits_frames_with_real_model` | +| Person-count flags untrained classes — no count inflation (ADR-159 A2) | **MEASURED** | `cog-person-count :: untrained_class_argmax_is_flagged_low_confidence` | +| Medical edge skills carry a "not a medical device" disclaimer (ADR-160 A1) | **MEASURED** | `wifi-densepose-wasm-edge :: a1_med_modules_have_clinical_disclaimer` (`--features std`) | +| Survivor dedup 3→1, count-inflation killed (ADR-158 §2) | **MEASURED** | `wifi-densepose-mat :: test_identical_vitals_no_location_dedup_to_one` (`--features mat`) | + +## Measured performance (criterion; reproduce on your machine) + +| Claim | Grade | Reproduce | +|---|---|---| +| PSD FFT-planner cache 2.0–3.1×, DTW band 2.4–4.1× (ADR-154) | **MEASURED** | `cd v2 && cargo bench -p wifi-densepose-signal` | +| fuse() double-clone removed ~2.17× marshalling (ADR-156) | **MEASURED** | `cd v2 && cargo bench -p wifi-densepose-ruvector --bench fusion_bench` | +| zero-copy ORT input ~1.48× (ADR-155) | **MEASURED** | `cd v2 && cargo bench -p wifi-densepose-nn --features onnx --bench onnx_bench` | +| pointcloud splats 9→2 passes ~1.24× (ADR-160 research) | **MEASURED** | `cd v2 && cargo bench -p wifi-densepose-pointcloud --bench splats_bench` | +| native wlanapi multi-BSSID scan 9.74 Hz (vs netsh ~2 Hz) | **MEASURED (Windows)** | `cd v2 && cargo test -p wifi-densepose-wifiscan -- --ignored measure_native_scan_rate` | + +## What we do NOT claim (the honest negatives — the strongest anti-slop signal) + +| Capability | Status | +|---|---| +| **Named person-identity from WiFi** | **NOT achieved, and measured why.** The §3.6 matcher is real, but identity does not lock on WiFi-only channels (gap 0.0005). DATA-GATED on a real enrollment feeding the AETHER/body-resonance channel — never done. No named-identity claim is made. | +| WiFlow-STD ~96% PCK@20 | **CLAIMED-reproduced** on our RTX 5080 (`benchmarks/wiflow-std/RESULTS.md`); HARDWARE-GATED for you (needs an NVIDIA GPU + the MM-Fi dataset). The upstream *shipped checkpoint* was **REFUTED** (0.08% PCK) — we publish that. | +| OccWorld trajectory accuracy | DATA-GATED on a trained checkpoint; `predict()` carries `weights_trained=false` until one is loaded — never silently faked. | +| Edge-skill detection accuracy (seizure, weapon, affect, …) | UNVALIDATED — every such module is now disclaimer-gated as experimental/research; the DSP is real, the accuracy is not claimed. | +| 802.11bf-2025 OTA conformance | No commodity silicon ships a conformant interface as of 2026; ours is a simulation-tested forward-compat protocol model, not a certified implementation. | + +## Provenance + +Every claim above traces to a committed ADR (`docs/adr/ADR-154`…`ADR-160`), a +test, a criterion bench, or `benchmarks/wiflow-std/RESULTS.md`. The history +includes published **retractions** (the 92.9% PCK retraction; the WiFlow-STD +shipped-checkpoint refutation; the NV-diamond BOM reality check) — a faker hides +failures; we commit them. diff --git a/scripts/prove.sh b/scripts/prove.sh new file mode 100644 index 00000000..737223f1 --- /dev/null +++ b/scripts/prove.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# prove.sh — one-command reproduction harness for RuView / wifi-densepose. +# +# Mission: this project has been publicly accused of being "AI slop / fake." +# The answer is reproducibility. Clone the repo, run THIS script, and every +# headline claim is either VERIFIED on your machine (MEASURED) or printed as +# "CLAIMED — not reproduced here (why)". Nothing is asserted without a command. +# +# Usage: +# bash scripts/prove.sh # core gate + anti-slop assertion tests +# bash scripts/prove.sh --full # also run the tch/GPU/dataset-gated claims +# +# Exit code 0 only if every NON-gated claim passes. Gated claims never fail the +# run; they print exactly what they need (libtorch, a GPU, a dataset) so you can +# reproduce them yourself. +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" +FULL=0; [ "${1:-}" = "--full" ] && FULL=1 + +pass=0; fail=0; skip=0 +PASS(){ echo " [PASS] $1"; pass=$((pass+1)); } +FAIL(){ echo " [FAIL] $1"; fail=$((fail+1)); } +SKIP(){ echo " [CLAIMED — not reproduced here] $1"; skip=$((skip+1)); } +hr(){ echo "------------------------------------------------------------"; } + +echo "RuView / wifi-densepose — PROOF harness" +echo "repo: $ROOT" +echo "date: $(date -u +%Y-%m-%dT%H:%M:%SZ)" +hr + +# ── 1. HARD GATE: Rust workspace tests (no native libs required) ──────────── +echo "[1] Rust workspace tests (cargo test --workspace --no-default-features)" +if command -v cargo >/dev/null 2>&1; then + if ( cd v2 && cargo test --workspace --no-default-features ) > /tmp/prove_ws.log 2>&1; then + n=$(grep -oE "result: ok\. [0-9]+ passed" /tmp/prove_ws.log | grep -oE "[0-9]+" | awk '{s+=$1} END {print s}') + PASS "workspace tests green — ${n:-?} passed, 0 failed (CARGO exit 0)" + else + FAIL "workspace tests — see /tmp/prove_ws.log (grep 'test result: FAILED')" + fi +else + SKIP "cargo not installed — install Rust to run the workspace gate" +fi +hr + +# ── 2. HARD GATE: deterministic Python pipeline proof (SHA-256) ───────────── +echo "[2] Deterministic CSI pipeline proof (archive/v1/data/proof/verify.py)" +if command -v python >/dev/null 2>&1; then + if python archive/v1/data/proof/verify.py > /tmp/prove_py.log 2>&1 && grep -q "VERDICT: PASS" /tmp/prove_py.log; then + PASS "Python proof VERDICT: PASS (bit-exact SHA-256 of reference features)" + else + FAIL "Python proof — see /tmp/prove_py.log" + fi +else + SKIP "python not installed — install Python 3.10+ to run the deterministic proof" +fi +hr + +# ── 3. ANTI-SLOP ASSERTION TESTS — each encodes a headline MEASURED claim ──── +# Format: claim_test [extra cargo args] +claim_test(){ + local crate="$1" filt="$2" desc="$3"; shift 3 + if ! command -v cargo >/dev/null 2>&1; then SKIP "$desc (cargo missing)"; return; fi + if ( cd v2 && cargo test -p "$crate" "$@" "$filt" ) > /tmp/prove_claim.log 2>&1 \ + && grep -qE "test result: ok\. [1-9]" /tmp/prove_claim.log; then + PASS "$desc" + else + # distinguish "didn't run" (feature/lib gated) from real failure + if grep -qE "0 passed|filtered out;? finished|error: no test target" /tmp/prove_claim.log \ + && ! grep -q "test result: FAILED" /tmp/prove_claim.log; then + SKIP "$desc (test gated/absent in this build — see /tmp/prove_claim.log)" + else + FAIL "$desc — see /tmp/prove_claim.log" + fi + fi +} + +# Variant for workspace-excluded crates (e.g. wasm-edge): run from the crate dir. +claim_test_indir(){ + local dir="$1" filt="$2" desc="$3"; shift 3 + if ! command -v cargo >/dev/null 2>&1; then SKIP "$desc (cargo missing)"; return; fi + if ( cd "$dir" && cargo test "$@" "$filt" ) > /tmp/prove_claim.log 2>&1 \ + && grep -qE "test result: ok\. [1-9]" /tmp/prove_claim.log; then + PASS "$desc" + else + if grep -qE "0 passed|error: no test target" /tmp/prove_claim.log \ + && ! grep -q "test result: FAILED" /tmp/prove_claim.log; then + SKIP "$desc (test gated/absent — see /tmp/prove_claim.log)" + else + FAIL "$desc — see /tmp/prove_claim.log" + fi + fi +} + +echo "[3] Anti-slop assertion tests (each fails on the pre-fix code)" +echo " ADR-156 §2.2 — fusion crafted-input DoS panics are closed:" +claim_test wifi-densepose-ruvector triangulation_out_of_range_index_returns_none_no_panic \ + "crafted out-of-range index returns None, no panic" --no-default-features + +echo " Soul Signature §3.6 — the audit's 'identity does not lock' claim, MEASURED:" +claim_test wifi-densepose-bfld cardiac_alone_cannot_separate_identity_matches_audit \ + "WiFi-only cardiac+respiratory channels CANNOT separate two people (gap ~0.0005)" + +echo " OccWorld — predict() is real (input-dependent), not random:" +claim_test wifi-densepose-occworld-candle predict_is_deterministic_for_same_input \ + "same occupancy input -> identical prediction (no randn stub)" + +echo " ADR-159 A1 — pose runtime actually emits under its own default config:" +claim_test cog-pose-estimation default_config_emits_frames_with_real_model \ + "default install emits pose frames (confidence >= min_confidence)" --no-default-features + +echo " ADR-159 A2 — person-count flags untrained classes (no count inflation):" +claim_test cog-person-count untrained_class_argmax_is_flagged_low_confidence \ + "argmax on an untrained class is flagged low_confidence" --no-default-features + +echo " ADR-160 A1 — medical edge skills carry a not-a-medical-device disclaimer:" +# wasm-edge is a workspace-excluded crate → run from its own directory. +claim_test_indir v2/crates/wifi-densepose-wasm-edge a1_med_modules_have_clinical_disclaimer \ + "every med_* module carries the experimental/non-clinical disclaimer" --features std +hr + +# ── 4. DATA/HARDWARE-GATED claims — honestly NOT reproduced by this script ─── +echo "[4] DATA/HARDWARE-GATED claims (reproduce instructions, not asserted here)" +if [ "$FULL" = "1" ]; then + echo " (--full) attempting the gated claims; missing prereqs are reported, not failed:" + claim_test wifi-densepose-mat test_identical_vitals_no_location_dedup_to_one \ + "ADR-158 §2 survivor dedup 3->1 (count-inflation fix)" --features mat +else + SKIP "WiFlow-STD ~96% PCK@20 reproduction — needs an NVIDIA GPU + MM-Fi dataset; see benchmarks/wiflow-std/RESULTS.md" + SKIP "named person-identity — DATA-GATED: needs a real enrollment feeding the AETHER/body-resonance channel (see docs/research/soul/)" + SKIP "OccWorld trained accuracy — needs a trained checkpoint (predict() carries weights_trained=false until then)" + SKIP "native wlanapi 9.74 Hz scan — Windows-only; run: cargo test -p wifi-densepose-wifiscan -- --ignored measure_native_scan_rate" + echo " (re-run with --full to attempt the feature-gated subset where prereqs exist)" +fi +hr + +# ── verdict ────────────────────────────────────────────────────────────────── +echo "VERDICT: $pass verified · $fail failed · $skip claimed-not-reproduced-here" +if [ "$fail" -eq 0 ]; then + echo "RESULT: PASS — every reproducible claim verified on this machine." + exit 0 +else + echo "RESULT: FAIL — $fail claim(s) did not reproduce. See the /tmp/prove_*.log files." + exit 1 +fi