From c6eacb7ff88b37e30fcea0bc9bdf551dab74e08b Mon Sep 17 00:00:00 2001 From: ruv Date: Sat, 13 Jun 2026 00:20:29 -0400 Subject: [PATCH 1/2] feat(wasm-edge): unified EdgePipeline wiring all ~64 edge skills (ADR-160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register every runtime skill module behind one uniform EdgeSkill trait and run them all per CSI frame, aggregating (skill, event_id, value) triples. - src/pipeline_all.rs: CsiFrameView (borrowed per-frame inputs), EdgeSkill trait, EdgePipeline (Box dispatch over all skills), SkillEvent/SkillInfo introspection. Host-only (std); the wasm no_std build keeps the flagship lib.rs pipeline. - src/skill_registry.rs: per-skill adapters (fwd_skill! direct-forward + synth_skill! for non-tuple returns). No skill DSP changed — only call wiring. gesture/coherence/adversarial synthesize one event; sig_sparse_recovery gets an owned mutable amplitude scratch; timer skills driven once per frame. - med_* tier registered only under --features medical-experimental (preserves the ADR-160 safety gate). Default tier = 59 skills; +medical = 64. - tests/pipeline_all.rs: 4 tests — all skills run without panic over 300 deterministic synthetic frames, every emitted id is declared by its skill, introspection well-formed, default tier excludes medical (59) / medical adds 5 (64). - examples/run_all_skills.rs: runnable demo printing per-skill event totals. Full suite: 619 passed default (615 M6 baseline + 4 new), 0 failed. Co-Authored-By: claude-flow --- .../examples/run_all_skills.rs | 108 +++ v2/crates/wifi-densepose-wasm-edge/src/lib.rs | 12 + .../src/pipeline_all.rs | 217 ++++++ .../src/skill_registry.rs | 630 ++++++++++++++++++ .../tests/pipeline_all.rs | 208 ++++++ 5 files changed, 1175 insertions(+) create mode 100644 v2/crates/wifi-densepose-wasm-edge/examples/run_all_skills.rs create mode 100644 v2/crates/wifi-densepose-wasm-edge/src/pipeline_all.rs create mode 100644 v2/crates/wifi-densepose-wasm-edge/src/skill_registry.rs create mode 100644 v2/crates/wifi-densepose-wasm-edge/tests/pipeline_all.rs diff --git a/v2/crates/wifi-densepose-wasm-edge/examples/run_all_skills.rs b/v2/crates/wifi-densepose-wasm-edge/examples/run_all_skills.rs new file mode 100644 index 00000000..9ce92bb0 --- /dev/null +++ b/v2/crates/wifi-densepose-wasm-edge/examples/run_all_skills.rs @@ -0,0 +1,108 @@ +//! Runnable demo of the unified [`EdgePipeline`]: constructs every registered +//! skill, feeds a short deterministic synthetic CSI frame sequence, and prints +//! the per-skill events plus a registration summary. +//! +//! ```bash +//! cd v2/crates/wifi-densepose-wasm-edge +//! cargo run --example run_all_skills --features std +//! cargo run --example run_all_skills --features std,medical-experimental +//! ``` +//! +//! [`EdgePipeline`]: wifi_densepose_wasm_edge::pipeline_all::EdgePipeline + +#[cfg(not(feature = "std"))] +fn main() { + eprintln!("run_all_skills requires --features std"); +} + +#[cfg(feature = "std")] +fn main() { + use std::collections::BTreeMap; + use wifi_densepose_wasm_edge::pipeline_all::{CsiFrameView, EdgePipeline}; + + const N_SC: usize = 32; + let mut pipeline = EdgePipeline::new(); + + println!("=== EdgePipeline registration ==="); + println!("registered skills: {}", pipeline.skill_count()); + let med = pipeline + .skills() + .iter() + .filter(|s| s.medical_experimental) + .count(); + println!( + " default tier: {} medical-experimental tier: {}", + pipeline.skill_count() - med, + med + ); + println!(); + + let mut phases = [0.0f32; N_SC]; + let mut amps = [0.0f32; N_SC]; + let mut vars = [0.0f32; N_SC]; + let mut prev = [0.0f32; N_SC]; + + // Per-skill event counters over the run. + let mut counts: BTreeMap<&'static str, usize> = BTreeMap::new(); + for s in pipeline.skills() { + counts.insert(s.name, 0); + } + + let frames = 300usize; + for t in 0..frames { + let tf = t as f32; + let breath = (tf * 2.0 * std::f32::consts::PI * 0.3 / 20.0).sin(); + let heart = (tf * 2.0 * std::f32::consts::PI * 1.2 / 20.0).sin(); + let mut vmean = 0.0f32; + for i in 0..N_SC { + let sc = i as f32; + phases[i] = (sc * 0.21 + tf * 0.05).sin() + 0.15 * breath; + amps[i] = 1.0 + 0.3 * (sc * 0.11 + tf * 0.03).cos() + 0.1 * heart; + vars[i] = 0.02 + 0.01 * (sc * 0.3).sin().abs() + + if (t / 40) % 2 == 0 { 0.05 } else { 0.0 }; + vmean += vars[i]; + } + vmean /= N_SC as f32; + + let v = CsiFrameView { + phases: &phases, + amplitudes: &s, + variances: &vars, + prev_phases: &prev, + presence: if (t / 30) % 3 == 0 { 0 } else { 1 }, + n_persons: ((t / 50) % 3) as i32, + motion_energy: 0.3 + 0.2 * (tf * 0.07).sin().abs(), + breathing_bpm: 18.0 + 2.0 * (tf * 0.01).sin(), + heartrate_bpm: 72.0 + 5.0 * (tf * 0.02).sin(), + coherence: 0.5 + 0.4 * (tf * 0.03).cos(), + variance_mean: vmean, + }; + + for e in pipeline.on_frame(&v) { + *counts.entry(e.skill).or_insert(0) += 1; + // Print the first few events from the last frame to show liveness. + if t == frames - 1 { + println!( + " frame {} | {:<26} event {:>3} = {:.4}", + t, e.skill, e.event_id, e.value + ); + } + } + prev.copy_from_slice(&phases); + } + + println!(); + println!("=== per-skill event totals over {} synthetic frames ===", frames); + let total: usize = counts.values().sum(); + let active = counts.values().filter(|&&c| c > 0).count(); + for (name, c) in &counts { + println!(" {:<28} {}", name, c); + } + println!(); + println!( + "TOTAL events: {} skills that emitted at least once: {}/{}", + total, + active, + pipeline.skill_count() + ); +} diff --git a/v2/crates/wifi-densepose-wasm-edge/src/lib.rs b/v2/crates/wifi-densepose-wasm-edge/src/lib.rs index b38b1308..2b3b7065 100644 --- a/v2/crates/wifi-densepose-wasm-edge/src/lib.rs +++ b/v2/crates/wifi-densepose-wasm-edge/src/lib.rs @@ -94,6 +94,18 @@ pub mod ind_structural_vibration; pub mod vendor_common; +// ── Unified edge pipeline (ADR-160 deliverable) ────────────────────────────── +// +// `EdgePipeline` registers EVERY runtime skill module behind one uniform +// `EdgeSkill` trait and runs them all per CSI frame. Host-only (`std`): it uses +// Box/Vec for dynamic dispatch; the wasm `no_std` build keeps the small flagship +// pipeline in this file. The `med_*` tier is registered only under +// `medical-experimental` (preserves the ADR-160 safety gate). +#[cfg(feature = "std")] +pub mod pipeline_all; +#[cfg(feature = "std")] +pub mod skill_registry; + // ── Vendor-integrated modules (ADR-041 Category 7) ────────────────────────── // // 24 modules organised into 7 sub-categories. Each module file lives in diff --git a/v2/crates/wifi-densepose-wasm-edge/src/pipeline_all.rs b/v2/crates/wifi-densepose-wasm-edge/src/pipeline_all.rs new file mode 100644 index 00000000..572eb1fc --- /dev/null +++ b/v2/crates/wifi-densepose-wasm-edge/src/pipeline_all.rs @@ -0,0 +1,217 @@ +//! Unified edge pipeline — registers **every** runtime skill module in the crate +//! behind one uniform [`EdgeSkill`] trait and runs them all per CSI frame. +//! +//! # Why this module exists +//! +//! Each skill in `src/*.rs` is an independently-loadable DSP module with its own +//! bespoke `process_frame` / `on_timer` signature (some take `&[f32]` phases, +//! some scalars like `motion_energy`, some `breathing_bpm`/`heartrate_bpm`, etc.). +//! On the wasm target only the flagship `gesture + coherence + adversarial` +//! pipeline (in `lib.rs`) is on the default `on_frame` path. This module wires +//! **all** of them into a single [`EdgePipeline`] so a host can run the whole +//! skill library over one CSI frame stream and collect every emitted event, +//! tagged by its source skill. +//! +//! # Design +//! +//! - [`CsiFrameView`] — a borrowed, host-supplied view of one CSI frame carrying +//! every input any skill needs (phase/amplitude/variance slices + the scalar +//! features the host derives: presence, n_persons, motion_energy, breathing & +//! heart rate, coherence, plus the previous frame's phases for delta skills). +//! - [`EdgeSkill`] — the uniform adapter trait. Each skill gets a small adapter +//! (see `skill_registry`) that pulls the fields it needs out of the view, calls +//! the underlying detector **unchanged**, and returns an aggregated +//! `&[(i32, f32)]` event buffer. **No skill DSP is modified.** +//! - [`EdgePipeline`] — owns one boxed adapter per skill, dispatches `on_frame` +//! to all of them, and aggregates `(skill_name, event_id, value)` triples. +//! +//! # Feature gating (preserves the ADR-160 safety gate) +//! +//! The five `med_*` skills are registered **only** under +//! `--features medical-experimental`. They are NOT pulled into the default +//! pipeline, so they cannot be silently built into a shipping artifact. The +//! medical tier is opt-in; see `EdgePipeline::new` and `skills()`. +//! +//! Requires `std` (uses `Box`/`Vec`); the wasm `no_std` build keeps the small +//! flagship `lib.rs` pipeline instead. + +#![cfg(feature = "std")] + +extern crate std; +use std::boxed::Box; +use std::vec::Vec; + +/// Borrowed view of one CSI frame: every input any registered skill can consume. +/// +/// The host derives these from the Tier-2 DSP output. Slices are +/// per-subcarrier; scalars are frame-level aggregates. A skill adapter reads +/// only the fields it needs and ignores the rest — heterogeneity is absorbed +/// here, not in the skills. +#[derive(Clone, Copy)] +pub struct CsiFrameView<'a> { + /// Per-subcarrier unwrapped phase (radians). + pub phases: &'a [f32], + /// Per-subcarrier amplitude (linear). + pub amplitudes: &'a [f32], + /// Per-subcarrier short-window variance. + pub variances: &'a [f32], + /// Previous frame's phases (for delta/velocity skills like the spiking tracker). + pub prev_phases: &'a [f32], + /// Presence flag from host (0 = empty, 1 = occupied). + pub presence: i32, + /// Estimated person count from host. + pub n_persons: i32, + /// Frame-level motion energy. + pub motion_energy: f32, + /// Breathing rate estimate (breaths/min); 0 if unavailable. + pub breathing_bpm: f32, + /// Heart rate estimate (beats/min); 0 if unavailable. + pub heartrate_bpm: f32, + /// Coherence score [0,1] from the coherence monitor (for gate-style skills). + pub coherence: f32, + /// Mean variance across `variances` (convenience scalar for skills wanting one). + pub variance_mean: f32, +} + +impl<'a> CsiFrameView<'a> { + /// Mean amplitude across the frame (convenience for scalar-input skills). + #[inline] + pub fn amplitude_mean(&self) -> f32 { + if self.amplitudes.is_empty() { + return 0.0; + } + let mut s = 0.0f32; + for &a in self.amplitudes { + s += a; + } + s / self.amplitudes.len() as f32 + } + + /// Mean phase across the frame. + #[inline] + pub fn phase_mean(&self) -> f32 { + if self.phases.is_empty() { + return 0.0; + } + let mut s = 0.0f32; + for &p in self.phases { + s += p; + } + s / self.phases.len() as f32 + } +} + +/// One emitted event, tagged by its source skill. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SkillEvent { + /// Stable name of the skill that produced this event (e.g. `"occupancy"`). + pub skill: &'static str, + /// Event type id (the registry id from `event_types`). + pub event_id: i32, + /// Event payload value. + pub value: f32, +} + +/// Uniform adapter trait over a heterogeneous skill detector. +/// +/// Implementors live in `skill_registry`; each wraps exactly one underlying +/// detector and forwards `on_frame` to its real `process_frame`/`on_timer` +/// without changing the DSP. `event_ids()` is introspection only. +pub trait EdgeSkill { + /// Stable skill name (matches the `src/.rs` module). + fn name(&self) -> &'static str; + /// The event ids this skill can emit (for introspection / docs). + fn event_ids(&self) -> &'static [i32]; + /// Run this skill over one frame, returning its emitted `(event_id, value)` + /// pairs. Returns an empty slice if the skill emitted nothing this frame. + fn on_frame(&mut self, frame: &CsiFrameView) -> &[(i32, f32)]; +} + +/// Introspection record for one registered skill. +#[derive(Clone, Copy, Debug)] +pub struct SkillInfo { + /// Skill name. + pub name: &'static str, + /// Event ids the skill can emit. + pub event_ids: &'static [i32], + /// Whether the skill is part of the gated `medical-experimental` tier. + pub medical_experimental: bool, +} + +/// The unified pipeline: holds one adapter per registered skill and runs them +/// all per frame. +pub struct EdgePipeline { + skills: Vec>, + /// Parallel flag marking which entries are the gated medical tier. + medical_flags: Vec, + frame_count: u64, +} + +impl EdgePipeline { + /// Construct the pipeline with **every** registered skill. + /// + /// The five `med_*` skills are included **only** when the crate is built + /// with `--features medical-experimental`; otherwise the default + /// (non-medical) tier is registered. This preserves the ADR-160 safety gate. + pub fn new() -> Self { + let mut skills: Vec> = Vec::new(); + let mut medical_flags: Vec = Vec::new(); + + crate::skill_registry::register_default(&mut skills, &mut medical_flags); + #[cfg(feature = "medical-experimental")] + crate::skill_registry::register_medical(&mut skills, &mut medical_flags); + + Self { + skills, + medical_flags, + frame_count: 0, + } + } + + /// Number of registered skills (default tier, or +medical if that feature is on). + pub fn skill_count(&self) -> usize { + self.skills.len() + } + + /// Run every registered skill over one frame, aggregating all emitted events + /// tagged by source skill. Order matches registration order. + pub fn on_frame(&mut self, frame: &CsiFrameView) -> Vec { + self.frame_count += 1; + let mut out: Vec = Vec::new(); + for skill in self.skills.iter_mut() { + let name = skill.name(); + for &(event_id, value) in skill.on_frame(frame) { + out.push(SkillEvent { + skill: name, + event_id, + value, + }); + } + } + out + } + + /// Total frames processed so far. + pub fn frame_count(&self) -> u64 { + self.frame_count + } + + /// Introspection: list every registered skill with its event ids and tier. + pub fn skills(&self) -> Vec { + let mut out = Vec::with_capacity(self.skills.len()); + for (i, skill) in self.skills.iter().enumerate() { + out.push(SkillInfo { + name: skill.name(), + event_ids: skill.event_ids(), + medical_experimental: self.medical_flags.get(i).copied().unwrap_or(false), + }); + } + out + } +} + +impl Default for EdgePipeline { + fn default() -> Self { + Self::new() + } +} diff --git a/v2/crates/wifi-densepose-wasm-edge/src/skill_registry.rs b/v2/crates/wifi-densepose-wasm-edge/src/skill_registry.rs new file mode 100644 index 00000000..00a418e0 --- /dev/null +++ b/v2/crates/wifi-densepose-wasm-edge/src/skill_registry.rs @@ -0,0 +1,630 @@ +//! Adapters wiring every runtime skill detector to the uniform [`EdgeSkill`] +//! trait, plus the registration functions consumed by [`EdgePipeline::new`]. +//! +//! [`EdgePipeline::new`]: crate::pipeline_all::EdgePipeline::new +//! [`EdgeSkill`]: crate::pipeline_all::EdgeSkill +//! +//! # How adapters work +//! +//! Each underlying detector keeps its own bespoke `process_frame`/`on_timer` +//! signature and its owned `events: [(i32,f32); N]` buffer (the ADR-160 M6 +//! soundness fix). An adapter holds the detector, implements [`EdgeSkill`], and +//! in `on_frame` simply pulls the needed fields out of [`CsiFrameView`] and +//! forwards the call **unchanged**. The detector returns `&self.events[..n]`; +//! the adapter forwards that borrow directly, so no extra buffer or copy is +//! needed for the common case. +//! +//! Three families need a small owned scratch buffer in the adapter instead of a +//! direct forward, because the underlying entry point does not itself return a +//! `&[(i32,f32)]`: +//! - `gesture` (`-> Option`), `coherence` (`-> f32`), `adversarial` +//! (`-> bool`): the adapter synthesizes a single tagged event. +//! - `sig_sparse_recovery` (`process_frame(&mut [f32])`): the adapter copies the +//! frame amplitudes into an owned scratch slice so the in-place ISTA recovery +//! never mutates the shared frame, then forwards the borrow. +//! - timer-driven skills (`vital_trend`, `lrn_meta_adapt`, `sig_temporal_compress`, +//! `tmp_goap_autonomy`, `tmp_pattern_sequence`): their `on_timer()` is driven +//! once per frame here (a frame *is* the tick at the edge), forwarding the +//! borrow. `tmp_pattern_sequence` additionally calls its `on_frame(...)` +//! accumulator first. +//! +//! **No skill's DSP is changed.** Only the call wiring lives here. + +#![cfg(feature = "std")] + +extern crate std; +use std::boxed::Box; +use std::vec::Vec; + +use crate::pipeline_all::{CsiFrameView, EdgeSkill}; + +// ── Direct-forward adapter macro ───────────────────────────────────────────── +// +// Generates an adapter whose `on_frame` forwards directly to a detector method +// that already returns `&[(i32, f32)]`. `$call` is an expression over `self.0` +// (the detector) and `f` (the `&CsiFrameView`). +macro_rules! fwd_skill { + ($adapter:ident, $detector:path, $name:literal, $ids:expr, |$d:ident, $f:ident| $call:expr) => { + pub struct $adapter($detector); + impl $adapter { + pub fn new() -> Self { + Self(<$detector>::new()) + } + } + impl EdgeSkill for $adapter { + fn name(&self) -> &'static str { + $name + } + fn event_ids(&self) -> &'static [i32] { + &$ids + } + fn on_frame(&mut self, $f: &CsiFrameView) -> &[(i32, f32)] { + let $d = &mut self.0; + $call + } + } + }; +} + +// ── Synthesized-event adapter macro ────────────────────────────────────────── +// +// For detectors whose entry point does NOT return `&[(i32, f32)]`. The adapter +// owns a tiny scratch buffer; `$body` (over `self`, `f`, and `self.buf`/`self.n`) +// fills it and the trait returns the filled prefix. +macro_rules! synth_skill { + ($adapter:ident, $detector:path, $name:literal, $ids:expr, $buf:literal, + |$s:ident, $f:ident| $body:block) => { + pub struct $adapter { + det: $detector, + buf: [(i32, f32); $buf], + n: usize, + } + impl $adapter { + pub fn new() -> Self { + Self { + det: <$detector>::new(), + buf: [(0, 0.0); $buf], + n: 0, + } + } + } + impl EdgeSkill for $adapter { + fn name(&self) -> &'static str { + $name + } + fn event_ids(&self) -> &'static [i32] { + &$ids + } + fn on_frame(&mut self, $f: &CsiFrameView) -> &[(i32, f32)] { + let $s = self; + $s.n = 0; + $body + &$s.buf[..$s.n] + } + } + }; +} + +use crate::event_types as ev; + +// ── Flagship (synthesized) ─────────────────────────────────────────────────── + +synth_skill!(GestureAdapter, crate::gesture::GestureDetector, "gesture", + [ev::GESTURE_DETECTED], 1, |s, f| { + if let Some(id) = s.det.process_frame(f.phases) { + s.buf[0] = (ev::GESTURE_DETECTED, id as f32); + s.n = 1; + } + }); + +synth_skill!(CoherenceAdapter, crate::coherence::CoherenceMonitor, "coherence", + [ev::COHERENCE_SCORE], 1, |s, f| { + let score = s.det.process_frame(f.phases); + s.buf[0] = (ev::COHERENCE_SCORE, score); + s.n = 1; + }); + +synth_skill!(AdversarialAdapter, crate::adversarial::AnomalyDetector, "adversarial", + [ev::ANOMALY_DETECTED], 1, |s, f| { + if s.det.process_frame(f.phases, f.amplitudes) { + s.buf[0] = (ev::ANOMALY_DETECTED, 1.0); + s.n = 1; + } + }); + +// ── sig_sparse_recovery (needs owned mutable amplitude scratch) ─────────────── + +const SPARSE_SC: usize = 64; +pub struct SparseRecoveryAdapter { + det: crate::sig_sparse_recovery::SparseRecovery, + scratch: [f32; SPARSE_SC], +} +impl SparseRecoveryAdapter { + pub fn new() -> Self { + Self { + det: crate::sig_sparse_recovery::SparseRecovery::new(), + scratch: [0.0; SPARSE_SC], + } + } +} +impl EdgeSkill for SparseRecoveryAdapter { + fn name(&self) -> &'static str { + "sig_sparse_recovery" + } + fn event_ids(&self) -> &'static [i32] { + &[ev::RECOVERY_COMPLETE, ev::RECOVERY_ERROR, ev::DROPOUT_RATE] + } + fn on_frame(&mut self, f: &CsiFrameView) -> &[(i32, f32)] { + let n = f.amplitudes.len().min(SPARSE_SC); + self.scratch[..n].copy_from_slice(&f.amplitudes[..n]); + self.det.process_frame(&mut self.scratch[..n]) + } +} + +// ── Standard direct-forward skills (return &[(i32,f32)]) ───────────────────── + +fwd_skill!(AisBehavioralAdapter, crate::ais_behavioral_profiler::BehavioralProfiler, + "ais_behavioral_profiler", + [ev::BEHAVIOR_ANOMALY, ev::PROFILE_DEVIATION, ev::NOVEL_PATTERN, ev::PROFILE_MATURITY], + |d, f| d.process_frame(f.presence != 0, f.motion_energy, f.n_persons.max(0) as u8)); + +fwd_skill!(AisPromptShieldAdapter, crate::ais_prompt_shield::PromptShield, + "ais_prompt_shield", + [ev::REPLAY_ATTACK, ev::INJECTION_DETECTED, ev::JAMMING_DETECTED, ev::SIGNAL_INTEGRITY], + |d, f| d.process_frame(f.phases, f.amplitudes)); + +fwd_skill!(AutPsychoAdapter, crate::aut_psycho_symbolic::PsychoSymbolicEngine, + "aut_psycho_symbolic", + [ev::INFERENCE_RESULT, ev::INFERENCE_CONFIDENCE, ev::RULE_FIRED, ev::CONTRADICTION], + |d, f| d.process_frame(f.presence as f32, f.motion_energy, f.breathing_bpm, + f.heartrate_bpm, f.n_persons as f32, 0.0)); + +fwd_skill!(AutMeshAdapter, crate::aut_self_healing_mesh::SelfHealingMesh, + "aut_self_healing_mesh", + [ev::NODE_DEGRADED, ev::MESH_RECONFIGURE, ev::COVERAGE_SCORE, ev::HEALING_COMPLETE], + |d, f| d.process_frame(f.variances)); + +fwd_skill!(BldElevatorAdapter, crate::bld_elevator_count::ElevatorCounter, + "bld_elevator_count", + [ev::ELEVATOR_COUNT, ev::DOOR_OPEN, ev::DOOR_CLOSE, ev::OVERLOAD_WARNING], + |d, f| d.process_frame(f.amplitudes, f.phases, f.motion_energy, f.n_persons)); + +fwd_skill!(BldEnergyAdapter, crate::bld_energy_audit::EnergyAuditor, + "bld_energy_audit", + [ev::SCHEDULE_SUMMARY, ev::AFTER_HOURS_ALERT, ev::UTILIZATION_RATE], + |d, f| d.process_frame(f.presence, f.n_persons)); + +fwd_skill!(BldHvacAdapter, crate::bld_hvac_presence::HvacPresenceDetector, + "bld_hvac_presence", + [ev::HVAC_OCCUPIED, ev::ACTIVITY_LEVEL, ev::DEPARTURE_COUNTDOWN], + |d, f| d.process_frame(f.presence as f32, f.motion_energy)); + +fwd_skill!(BldLightingAdapter, crate::bld_lighting_zones::LightingZoneController, + "bld_lighting_zones", + [ev::LIGHT_ON, ev::LIGHT_DIM, ev::LIGHT_OFF], + |d, f| d.process_frame(f.amplitudes, f.motion_energy)); + +fwd_skill!(BldMeetingAdapter, crate::bld_meeting_room::MeetingRoomTracker, + "bld_meeting_room", + [ev::MEETING_START, ev::MEETING_END, ev::PEAK_HEADCOUNT, ev::ROOM_AVAILABLE], + |d, f| d.process_frame(f.presence, f.n_persons, f.motion_energy)); + +fwd_skill!(ExoBreathingSyncAdapter, crate::exo_breathing_sync::BreathingSyncDetector, + "exo_breathing_sync", + [ev::SYNC_DETECTED, ev::SYNC_PAIR_COUNT, ev::GROUP_COHERENCE, ev::SYNC_LOST], + |d, f| d.process_frame(f.phases, f.variances, f.breathing_bpm, f.n_persons)); + +fwd_skill!(ExoEmotionAdapter, crate::exo_emotion_detect::EmotionDetector, + "exo_emotion_detect", + [ev::AROUSAL_LEVEL, ev::STRESS_INDEX, ev::CALM_DETECTED, ev::AGITATION_DETECTED], + |d, f| d.process_frame(f.breathing_bpm, f.heartrate_bpm, f.motion_energy, + f.phase_mean(), f.variance_mean)); + +fwd_skill!(ExoDreamAdapter, crate::exo_dream_stage::DreamStageDetector, + "exo_dream_stage", + [ev::SLEEP_STAGE, ev::SLEEP_QUALITY, ev::REM_EPISODE, ev::DEEP_SLEEP_RATIO], + |d, f| d.process_frame(f.breathing_bpm, f.heartrate_bpm, f.motion_energy, + f.phase_mean(), f.variance_mean, f.presence)); + +fwd_skill!(ExoGestureLangAdapter, crate::exo_gesture_language::GestureLanguageDetector, + "exo_gesture_language", + [ev::LETTER_RECOGNIZED, ev::LETTER_CONFIDENCE, ev::WORD_BOUNDARY, ev::GESTURE_REJECTED], + |d, f| d.process_frame(f.phases, f.amplitudes, f.variance_mean, f.motion_energy, f.presence)); + +fwd_skill!(ExoGhostAdapter, crate::exo_ghost_hunter::GhostHunterDetector, + "exo_ghost_hunter", + [ev::EXO_ANOMALY_DETECTED, ev::EXO_ANOMALY_CLASS, ev::HIDDEN_PRESENCE, ev::ENVIRONMENTAL_DRIFT], + |d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.presence, f.motion_energy)); + +fwd_skill!(ExoHappinessAdapter, crate::exo_happiness_score::HappinessScoreDetector, + "exo_happiness_score", + [ev::HAPPINESS_SCORE, ev::GAIT_ENERGY, ev::AFFECT_VALENCE, ev::SOCIAL_ENERGY, ev::TRANSIT_DIRECTION], + |d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.presence, + f.motion_energy, f.breathing_bpm, f.heartrate_bpm)); + +fwd_skill!(ExoHyperbolicAdapter, crate::exo_hyperbolic_space::HyperbolicEmbedder, + "exo_hyperbolic_space", + [ev::HIERARCHY_LEVEL, ev::HYPERBOLIC_RADIUS, ev::LOCATION_LABEL], + |d, f| d.process_frame(f.amplitudes)); + +fwd_skill!(ExoMusicAdapter, crate::exo_music_conductor::MusicConductorDetector, + "exo_music_conductor", + [ev::CONDUCTOR_BPM, ev::BEAT_POSITION, ev::DYNAMIC_LEVEL, ev::GESTURE_CUTOFF, ev::GESTURE_FERMATA], + |d, f| d.process_frame(f.phase_mean(), f.amplitude_mean(), f.motion_energy, f.variance_mean)); + +fwd_skill!(ExoPlantAdapter, crate::exo_plant_growth::PlantGrowthDetector, + "exo_plant_growth", + [ev::GROWTH_RATE, ev::CIRCADIAN_PHASE, ev::WILT_DETECTED, ev::WATERING_EVENT], + |d, f| d.process_frame(f.amplitudes, f.phases, f.variances, f.presence)); + +fwd_skill!(ExoRainAdapter, crate::exo_rain_detect::RainDetector, + "exo_rain_detect", + [ev::RAIN_ONSET, ev::RAIN_INTENSITY, ev::RAIN_CESSATION], + |d, f| d.process_frame(f.phases, f.variances, f.amplitudes, f.presence)); + +fwd_skill!(ExoTimeCrystalAdapter, crate::exo_time_crystal::TimeCrystalDetector, + "exo_time_crystal", + [ev::CRYSTAL_DETECTED, ev::CRYSTAL_STABILITY, ev::COORDINATION_INDEX], + |d, f| d.process_frame(f.motion_energy)); + +fwd_skill!(IndCleanRoomAdapter, crate::ind_clean_room::CleanRoomMonitor, + "ind_clean_room", + [ev::OCCUPANCY_COUNT, ev::OCCUPANCY_VIOLATION, ev::TURBULENT_MOTION, ev::COMPLIANCE_REPORT], + |d, f| d.process_frame(f.n_persons, f.presence, f.motion_energy)); + +fwd_skill!(IndConfinedAdapter, crate::ind_confined_space::ConfinedSpaceMonitor, + "ind_confined_space", + [ev::WORKER_ENTRY, ev::WORKER_EXIT, ev::BREATHING_OK, ev::EXTRACTION_ALERT, ev::IMMOBILE_ALERT], + |d, f| d.process_frame(f.presence, f.breathing_bpm, f.motion_energy, f.variance_mean)); + +fwd_skill!(IndForkliftAdapter, crate::ind_forklift_proximity::ForkliftProximityDetector, + "ind_forklift_proximity", + [ev::PROXIMITY_WARNING, ev::VEHICLE_DETECTED, ev::HUMAN_NEAR_VEHICLE], + |d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.motion_energy, f.presence, f.n_persons)); + +fwd_skill!(IndLivestockAdapter, crate::ind_livestock_monitor::LivestockMonitor, + "ind_livestock_monitor", + [ev::ANIMAL_PRESENT, ev::ABNORMAL_STILLNESS, ev::LABORED_BREATHING, ev::ESCAPE_ALERT], + |d, f| d.process_frame(f.presence, f.breathing_bpm, f.motion_energy, f.variance_mean)); + +fwd_skill!(IndVibrationAdapter, crate::ind_structural_vibration::StructuralVibrationMonitor, + "ind_structural_vibration", + [ev::SEISMIC_DETECTED, ev::MECHANICAL_RESONANCE, ev::STRUCTURAL_DRIFT, ev::VIBRATION_SPECTRUM], + |d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.presence)); + +fwd_skill!(IntrusionAdapter, crate::intrusion::IntrusionDetector, + "intrusion", + [ev::INTRUSION_ALERT, ev::INTRUSION_ZONE, 202], + |d, f| d.process_frame(f.phases, f.amplitudes)); + +fwd_skill!(LrnAttractorAdapter, crate::lrn_anomaly_attractor::AttractorDetector, + "lrn_anomaly_attractor", + [ev::ATTRACTOR_TYPE, ev::LYAPUNOV_EXPONENT, ev::BASIN_DEPARTURE, ev::LEARNING_COMPLETE], + |d, f| d.process_frame(f.phases, f.amplitudes, f.motion_energy)); + +fwd_skill!(LrnDtwAdapter, crate::lrn_dtw_gesture_learn::GestureLearner, + "lrn_dtw_gesture_learn", + [ev::GESTURE_LEARNED, ev::GESTURE_MATCHED, ev::LRN_MATCH_DISTANCE, ev::TEMPLATE_COUNT], + |d, f| d.process_frame(f.phases, f.motion_energy)); + +fwd_skill!(LrnEwcAdapter, crate::lrn_ewc_lifelong::EwcLifelong, + "lrn_ewc_lifelong", + [ev::KNOWLEDGE_RETAINED, ev::NEW_TASK_LEARNED, ev::FISHER_UPDATE, ev::FORGETTING_RISK], + |d, f| d.process_frame(f.variances, f.presence)); + +fwd_skill!(OccupancyAdapter, crate::occupancy::OccupancyDetector, + "occupancy", + [ev::ZONE_OCCUPIED, ev::ZONE_COUNT, ev::ZONE_TRANSITION], + |d, f| d.process_frame(f.phases, f.amplitudes)); + +fwd_skill!(QntInterferenceAdapter, crate::qnt_interference_search::InterferenceSearch, + "qnt_interference_search", + [ev::HYPOTHESIS_WINNER, ev::HYPOTHESIS_AMPLITUDE, ev::SEARCH_ITERATIONS], + |d, f| d.process_frame(f.presence, f.motion_energy, f.n_persons)); + +fwd_skill!(QntCoherenceAdapter, crate::qnt_quantum_coherence::QuantumCoherenceMonitor, + "qnt_quantum_coherence", + [ev::ENTANGLEMENT_ENTROPY, ev::DECOHERENCE_EVENT, ev::BLOCH_DRIFT], + |d, f| d.process_frame(f.phases)); + +fwd_skill!(RetFlowAdapter, crate::ret_customer_flow::CustomerFlowTracker, + "ret_customer_flow", + [ev::INGRESS, ev::EGRESS, ev::NET_OCCUPANCY, ev::HOURLY_TRAFFIC], + |d, f| d.process_frame(f.phases, f.amplitudes, f.variance_mean, f.motion_energy)); + +fwd_skill!(RetDwellAdapter, crate::ret_dwell_heatmap::DwellHeatmapTracker, + "ret_dwell_heatmap", + [ev::DWELL_ZONE_UPDATE, ev::HOT_ZONE, ev::COLD_ZONE, ev::SESSION_SUMMARY], + |d, f| d.process_frame(f.presence, f.variances, f.motion_energy, f.n_persons)); + +fwd_skill!(RetQueueAdapter, crate::ret_queue_length::QueueLengthEstimator, + "ret_queue_length", + [ev::QUEUE_LENGTH, ev::WAIT_TIME_ESTIMATE, ev::SERVICE_RATE, ev::QUEUE_ALERT], + |d, f| d.process_frame(f.presence, f.n_persons, f.variance_mean, f.motion_energy)); + +fwd_skill!(RetShelfAdapter, crate::ret_shelf_engagement::ShelfEngagementDetector, + "ret_shelf_engagement", + [ev::SHELF_BROWSE, ev::SHELF_CONSIDER, ev::SHELF_ENGAGE, ev::REACH_DETECTED], + |d, f| d.process_frame(f.presence, f.motion_energy, f.variance_mean, f.phases)); + +fwd_skill!(RetTableAdapter, crate::ret_table_turnover::TableTurnoverTracker, + "ret_table_turnover", + [ev::TABLE_SEATED, ev::TABLE_VACATED, ev::TABLE_AVAILABLE, ev::TURNOVER_RATE], + |d, f| d.process_frame(f.presence, f.motion_energy, f.n_persons)); + +fwd_skill!(SecLoiteringAdapter, crate::sec_loitering::LoiteringDetector, + "sec_loitering", + [ev::LOITERING_START, ev::LOITERING_ONGOING, ev::LOITERING_END], + |d, f| d.process_frame(f.presence, f.motion_energy)); + +fwd_skill!(SecPanicAdapter, crate::sec_panic_motion::PanicMotionDetector, + "sec_panic_motion", + [ev::PANIC_DETECTED, ev::STRUGGLE_PATTERN, ev::FLEEING_DETECTED], + |d, f| d.process_frame(f.motion_energy, f.variance_mean, f.phase_mean(), f.presence)); + +fwd_skill!(SecPerimeterAdapter, crate::sec_perimeter_breach::PerimeterBreachDetector, + "sec_perimeter_breach", + [ev::PERIMETER_BREACH, ev::APPROACH_DETECTED, ev::DEPARTURE_DETECTED, ev::SEC_ZONE_TRANSITION], + |d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.motion_energy)); + +fwd_skill!(SecTailgateAdapter, crate::sec_tailgating::TailgateDetector, + "sec_tailgating", + [ev::TAILGATE_DETECTED, ev::SINGLE_PASSAGE, ev::MULTI_PASSAGE], + |d, f| d.process_frame(f.motion_energy, f.presence, f.n_persons, f.variance_mean)); + +fwd_skill!(SecWeaponAdapter, crate::sec_weapon_detect::WeaponDetector, + "sec_weapon_detect", + [ev::METAL_ANOMALY, ev::HIGH_METAL_REFLECTIVITY, ev::CALIBRATION_NEEDED], + |d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.motion_energy, f.presence)); + +fwd_skill!(SigCoherenceGateAdapter, crate::sig_coherence_gate::CoherenceGate, + "sig_coherence_gate", + [ev::GATE_DECISION, ev::SIG_COHERENCE_SCORE, ev::RECALIBRATE_NEEDED], + |d, f| d.process_frame(f.phases)); + +fwd_skill!(SigFlashAttnAdapter, crate::sig_flash_attention::FlashAttention, + "sig_flash_attention", + [ev::ATTENTION_PEAK_SC, ev::ATTENTION_SPREAD, ev::SPATIAL_FOCUS_ZONE], + |d, f| d.process_frame(f.phases, f.amplitudes)); + +fwd_skill!(SigMincutAdapter, crate::sig_mincut_person_match::PersonMatcher, + "sig_mincut_person_match", + [ev::PERSON_ID_ASSIGNED, ev::PERSON_ID_SWAP, ev::MATCH_CONFIDENCE], + |d, f| d.process_frame(f.amplitudes, f.variances, f.n_persons.max(0) as usize)); + +fwd_skill!(SigTransportAdapter, crate::sig_optimal_transport::OptimalTransportDetector, + "sig_optimal_transport", + [ev::WASSERSTEIN_DISTANCE, ev::DISTRIBUTION_SHIFT, ev::SUBTLE_MOTION], + |d, f| d.process_frame(f.amplitudes)); + +fwd_skill!(SptHnswAdapter, crate::spt_micro_hnsw::MicroHnsw, + "spt_micro_hnsw", + [ev::NEAREST_MATCH_ID, ev::HNSW_MATCH_DISTANCE, ev::CLASSIFICATION, ev::LIBRARY_SIZE], + |d, f| d.process_frame(f.variances)); + +fwd_skill!(SptPagerankAdapter, crate::spt_pagerank_influence::PageRankInfluence, + "spt_pagerank_influence", + [ev::DOMINANT_PERSON, ev::INFLUENCE_SCORE, ev::INFLUENCE_CHANGE], + |d, f| d.process_frame(f.phases, f.n_persons.max(0) as usize)); + +fwd_skill!(SptSpikingAdapter, crate::spt_spiking_tracker::SpikingTracker, + "spt_spiking_tracker", + [ev::TRACK_UPDATE, ev::TRACK_VELOCITY, ev::SPIKE_RATE, ev::TRACK_LOST], + |d, f| d.process_frame(f.phases, f.prev_phases)); + +fwd_skill!(TmpLogicGuardAdapter, crate::tmp_temporal_logic_guard::TemporalLogicGuard, + "tmp_temporal_logic_guard", + [ev::LTL_VIOLATION, ev::LTL_SATISFACTION, ev::COUNTEREXAMPLE], + |d, f| { + let input = crate::tmp_temporal_logic_guard::FrameInput { + presence: f.presence, + n_persons: f.n_persons, + motion_energy: f.motion_energy, + coherence: f.coherence, + breathing_bpm: f.breathing_bpm, + heartrate_bpm: f.heartrate_bpm, + fall_alert: false, + intrusion_alert: false, + person_id_active: f.n_persons > 0, + vital_signs_active: f.breathing_bpm > 0.0, + seizure_detected: false, + normal_gait: true, + }; + d.on_frame(&input) + }); + +// ── Timer-driven skills (driven once per frame) ────────────────────────────── + +fwd_skill!(VitalTrendAdapter, crate::vital_trend::VitalTrendAnalyzer, + "vital_trend", + // 101-105 = brady/tachypnea, brady/tachycardia, apnea; 110/111 = breathing/heartrate + // moving averages (module-local EVENT_BREATHING_AVG / EVENT_HEARTRATE_AVG). + [ev::BRADYPNEA, ev::TACHYPNEA, ev::BRADYCARDIA, ev::TACHYCARDIA, ev::APNEA, 110, 111], + |d, f| d.on_timer(f.breathing_bpm, f.heartrate_bpm)); + +fwd_skill!(LrnMetaAdapter, crate::lrn_meta_adapt::MetaAdapter, + "lrn_meta_adapt", + [ev::PARAM_ADJUSTED, ev::ADAPTATION_SCORE, ev::ROLLBACK_TRIGGERED, ev::META_LEVEL], + |d, _f| d.on_timer()); + +fwd_skill!(SigTemporalCompressAdapter, crate::sig_temporal_compress::TemporalCompressor, + "sig_temporal_compress", + [ev::COMPRESSION_RATIO, ev::TIER_TRANSITION, ev::HISTORY_DEPTH_HOURS], + |d, _f| d.on_timer()); + +fwd_skill!(TmpGoapAdapter, crate::tmp_goap_autonomy::GoapPlanner, + "tmp_goap_autonomy", + [ev::GOAL_SELECTED, ev::MODULE_ACTIVATED, ev::MODULE_DEACTIVATED, ev::PLAN_COST], + |d, _f| d.on_timer()); + +// tmp_pattern_sequence: accumulate via on_frame, then drive on_timer per frame. +pub struct TmpPatternAdapter(crate::tmp_pattern_sequence::PatternSequenceAnalyzer); +impl TmpPatternAdapter { + pub fn new() -> Self { + Self(crate::tmp_pattern_sequence::PatternSequenceAnalyzer::new()) + } +} +impl EdgeSkill for TmpPatternAdapter { + fn name(&self) -> &'static str { + "tmp_pattern_sequence" + } + fn event_ids(&self) -> &'static [i32] { + &[ev::PATTERN_DETECTED, ev::PATTERN_CONFIDENCE, ev::ROUTINE_DEVIATION, ev::PREDICTION_NEXT] + } + fn on_frame(&mut self, f: &CsiFrameView) -> &[(i32, f32)] { + self.0.on_frame(f.presence, f.motion_energy, f.n_persons); + self.0.on_timer() + } +} + +// ── Medical tier (gated) ───────────────────────────────────────────────────── + +#[cfg(feature = "medical-experimental")] +mod medical { + use super::*; + + // Medical event ids verified against each module's local consts (100-199 block). + fwd_skill!(MedCardiacAdapter, crate::med_cardiac_arrhythmia::CardiacArrhythmiaDetector, + "med_cardiac_arrhythmia", + [110, 111, 112, 113], + |d, f| d.process_frame(f.heartrate_bpm, f.phase_mean())); + + fwd_skill!(MedGaitAdapter, crate::med_gait_analysis::GaitAnalyzer, + "med_gait_analysis", + [130, 131, 132, 133, 134], + |d, f| d.process_frame(f.phase_mean(), f.amplitude_mean(), f.variance_mean, f.motion_energy)); + + fwd_skill!(MedRespiratoryAdapter, crate::med_respiratory_distress::RespiratoryDistressDetector, + "med_respiratory_distress", + [120, 121, 122, 123], + |d, f| d.process_frame(f.breathing_bpm, f.phase_mean(), f.variance_mean)); + + fwd_skill!(MedSeizureAdapter, crate::med_seizure_detect::SeizureDetector, + "med_seizure_detect", + [140, 141, 142, 143], + |d, f| d.process_frame(f.phase_mean(), f.amplitude_mean(), f.motion_energy, f.presence)); + + fwd_skill!(MedApneaAdapter, crate::med_sleep_apnea::SleepApneaDetector, + "med_sleep_apnea", + [100, 101, 102], + |d, f| d.process_frame(f.breathing_bpm, f.presence, f.variance_mean)); + + pub fn register(skills: &mut Vec>, med: &mut Vec) { + macro_rules! push { + ($a:ty) => {{ + skills.push(Box::new(<$a>::new())); + med.push(true); + }}; + } + push!(MedSeizureAdapter); + push!(MedCardiacAdapter); + push!(MedRespiratoryAdapter); + push!(MedApneaAdapter); + push!(MedGaitAdapter); + } +} + +// ── Registration ───────────────────────────────────────────────────────────── + +/// Register every default-tier (non-medical) skill. +pub fn register_default(skills: &mut Vec>, med: &mut Vec) { + macro_rules! push { + ($a:ty) => {{ + skills.push(Box::new(<$a>::new())); + med.push(false); + }}; + } + + // Flagship + synthesized + push!(GestureAdapter); + push!(CoherenceAdapter); + push!(AdversarialAdapter); + push!(OccupancyAdapter); + push!(IntrusionAdapter); + push!(VitalTrendAdapter); + + // Security + push!(SecPerimeterAdapter); + push!(SecWeaponAdapter); + push!(SecTailgateAdapter); + push!(SecLoiteringAdapter); + push!(SecPanicAdapter); + + // Smart building + push!(BldHvacAdapter); + push!(BldLightingAdapter); + push!(BldElevatorAdapter); + push!(BldMeetingAdapter); + push!(BldEnergyAdapter); + + // Retail + push!(RetQueueAdapter); + push!(RetDwellAdapter); + push!(RetFlowAdapter); + push!(RetTableAdapter); + push!(RetShelfAdapter); + + // Industrial + push!(IndForkliftAdapter); + push!(IndConfinedAdapter); + push!(IndCleanRoomAdapter); + push!(IndLivestockAdapter); + push!(IndVibrationAdapter); + + // Exotic / research + push!(ExoTimeCrystalAdapter); + push!(ExoHyperbolicAdapter); + push!(ExoDreamAdapter); + push!(ExoEmotionAdapter); + push!(ExoGestureLangAdapter); + push!(ExoMusicAdapter); + push!(ExoPlantAdapter); + push!(ExoGhostAdapter); + push!(ExoRainAdapter); + push!(ExoBreathingSyncAdapter); + push!(ExoHappinessAdapter); + + // Signal intelligence + push!(SigCoherenceGateAdapter); + push!(SigFlashAttnAdapter); + push!(SigTemporalCompressAdapter); + push!(SparseRecoveryAdapter); + push!(SigMincutAdapter); + push!(SigTransportAdapter); + + // Adaptive learning + push!(LrnDtwAdapter); + push!(LrnAttractorAdapter); + push!(LrnMetaAdapter); + push!(LrnEwcAdapter); + + // Spatial reasoning + push!(SptPagerankAdapter); + push!(SptHnswAdapter); + push!(SptSpikingAdapter); + + // Temporal analysis + push!(TmpPatternAdapter); + push!(TmpLogicGuardAdapter); + push!(TmpGoapAdapter); + + // AI security + push!(AisPromptShieldAdapter); + push!(AisBehavioralAdapter); + + // Quantum-inspired + push!(QntCoherenceAdapter); + push!(QntInterferenceAdapter); + + // Autonomous systems + push!(AutPsychoAdapter); + push!(AutMeshAdapter); + + let _ = (skills.len(), med.len()); +} + +/// Register the gated `medical-experimental` tier (5 `med_*` skills). +#[cfg(feature = "medical-experimental")] +pub fn register_medical(skills: &mut Vec>, med: &mut Vec) { + medical::register(skills, med); +} diff --git a/v2/crates/wifi-densepose-wasm-edge/tests/pipeline_all.rs b/v2/crates/wifi-densepose-wasm-edge/tests/pipeline_all.rs new file mode 100644 index 00000000..8e0c26ec --- /dev/null +++ b/v2/crates/wifi-densepose-wasm-edge/tests/pipeline_all.rs @@ -0,0 +1,208 @@ +//! Integration test for the unified [`EdgePipeline`] (ADR-160 deliverable 1). +//! +//! Proves that EVERY registered skill executes over a deterministic synthetic +//! CSI frame sequence without panicking, that the aggregated event stream is +//! well-formed (each event tagged with a known skill name + a declared event +//! id), and pins the registered-skill count (default vs +medical-experimental). +//! +//! Run: +//! cargo test --features std --test pipeline_all +//! cargo test --features std,medical-experimental --test pipeline_all +//! +//! [`EdgePipeline`]: wifi_densepose_wasm_edge::pipeline_all::EdgePipeline + +#![cfg(feature = "std")] + +use wifi_densepose_wasm_edge::pipeline_all::{CsiFrameView, EdgePipeline}; + +const N_SC: usize = 32; + +/// Deterministic synthetic frame: a moving breathing/heartbeat target plus +/// structured per-subcarrier phase/amplitude. No randomness — fully reproducible. +fn synth_frame(t: usize, phases: &mut [f32], amps: &mut [f32], vars: &mut [f32]) { + let tf = t as f32; + // 0.3 Hz breathing modulation @ 20 Hz frame rate -> period ~66 frames. + let breath = (tf * 2.0 * core::f32::consts::PI * 0.3 / 20.0).sin(); + // 1.2 Hz heartbeat. + let heart = (tf * 2.0 * core::f32::consts::PI * 1.2 / 20.0).sin(); + for i in 0..phases.len() { + let sc = i as f32; + phases[i] = (sc * 0.21 + tf * 0.05).sin() + 0.15 * breath; + amps[i] = 1.0 + 0.3 * (sc * 0.11 + tf * 0.03).cos() + 0.1 * heart; + // motion-correlated variance, with one occasionally-hot zone. + vars[i] = 0.02 + 0.01 * (sc * 0.3).sin().abs() + if (t / 40) % 2 == 0 { 0.05 } else { 0.0 }; + } +} + +/// Build a view over the supplied buffers for frame `t`. +fn view<'a>( + t: usize, + phases: &'a [f32], + amps: &'a [f32], + vars: &'a [f32], + prev_phases: &'a [f32], +) -> CsiFrameView<'a> { + let tf = t as f32; + let motion = 0.3 + 0.2 * (tf * 0.07).sin().abs(); + let mut vmean = 0.0f32; + for &v in vars { + vmean += v; + } + vmean /= vars.len().max(1) as f32; + CsiFrameView { + phases, + amplitudes: amps, + variances: vars, + prev_phases, + presence: if (t / 30) % 3 == 0 { 0 } else { 1 }, + n_persons: ((t / 50) % 3) as i32, + motion_energy: motion, + breathing_bpm: 18.0 + 2.0 * (tf * 0.01).sin(), + heartrate_bpm: 72.0 + 5.0 * (tf * 0.02).sin(), + coherence: 0.5 + 0.4 * (tf * 0.03).cos(), + variance_mean: vmean, + } +} + +#[test] +fn all_skills_execute_without_panic_over_synthetic_stream() { + let mut pipeline = EdgePipeline::new(); + let n_skills = pipeline.skill_count(); + assert!(n_skills > 0, "pipeline must register skills"); + + let mut phases = [0.0f32; N_SC]; + let mut amps = [0.0f32; N_SC]; + let mut vars = [0.0f32; N_SC]; + let mut prev_phases = [0.0f32; N_SC]; + + let known: std::collections::HashSet<&'static str> = + pipeline.skills().iter().map(|s| s.name).collect(); + + // Feed 300 frames (15 s @ 20 Hz) — enough for calibration windows, DTW + // enrollment, periodicity buffers, and timer cadences to fire. + let mut total_events = 0usize; + for t in 0..300 { + synth_frame(t, &mut phases, &mut amps, &mut vars); + let v = view(t, &phases, &s, &vars, &prev_phases); + let events = pipeline.on_frame(&v); + for e in &events { + // Every event must be tagged with a registered skill name. + assert!(known.contains(e.skill), "unknown skill tag: {}", e.skill); + // Value must be finite (no NaN/Inf leaking from the DSP). + assert!(e.value.is_finite(), "non-finite value from {}", e.skill); + } + total_events += events.len(); + prev_phases.copy_from_slice(&phases); + } + + assert_eq!(pipeline.frame_count(), 300); + // A real run over 300 frames must emit *some* events across 59+ skills. + assert!( + total_events > 0, + "expected the skill library to emit events over 300 frames, got 0" + ); + println!( + "pipeline: {} skills, {} aggregated events over 300 synthetic frames", + n_skills, total_events + ); +} + +#[test] +fn every_emitted_event_id_is_declared_by_its_skill() { + // Stronger well-formedness: each event's id must be one the producing skill + // declared in its `event_ids()` introspection list. + let mut pipeline = EdgePipeline::new(); + + // skill name -> its declared event id set + let mut declared: std::collections::HashMap<&'static str, std::collections::HashSet> = + std::collections::HashMap::new(); + for s in pipeline.skills() { + declared.insert(s.name, s.event_ids.iter().copied().collect()); + } + + let mut phases = [0.0f32; N_SC]; + let mut amps = [0.0f32; N_SC]; + let mut vars = [0.0f32; N_SC]; + let mut prev_phases = [0.0f32; N_SC]; + + for t in 0..300 { + synth_frame(t, &mut phases, &mut amps, &mut vars); + let v = view(t, &phases, &s, &vars, &prev_phases); + for e in &pipeline.on_frame(&v) { + let set = declared.get(e.skill).expect("skill declared"); + assert!( + set.contains(&e.event_id), + "{} emitted undeclared event id {}", + e.skill, + e.event_id + ); + } + prev_phases.copy_from_slice(&phases); + } +} + +#[test] +fn introspection_lists_every_skill_with_event_ids() { + let pipeline = EdgePipeline::new(); + let infos = pipeline.skills(); + assert_eq!(infos.len(), pipeline.skill_count()); + for info in &infos { + assert!(!info.name.is_empty()); + assert!( + !info.event_ids.is_empty(), + "skill {} declares no event ids", + info.name + ); + } + // No duplicate skill names. + let names: std::collections::HashSet<_> = infos.iter().map(|i| i.name).collect(); + assert_eq!(names.len(), infos.len(), "duplicate skill registration"); +} + +#[cfg(not(feature = "medical-experimental"))] +#[test] +fn default_tier_count_excludes_medical() { + let pipeline = EdgePipeline::new(); + assert_eq!( + pipeline.skill_count(), + 59, + "default (non-medical) tier must register exactly 59 skills" + ); + // The ADR-160 safety gate: no med_* skill is present in the default build. + for info in pipeline.skills() { + assert!( + !info.medical_experimental, + "medical skill {} leaked into default tier", + info.name + ); + assert!( + !info.name.starts_with("med_"), + "med_* skill {} present without the medical-experimental feature", + info.name + ); + } +} + +#[cfg(feature = "medical-experimental")] +#[test] +fn medical_tier_adds_five_skills() { + let pipeline = EdgePipeline::new(); + assert_eq!( + pipeline.skill_count(), + 64, + "default 59 + 5 medical = 64 skills" + ); + let med: Vec<_> = pipeline + .skills() + .into_iter() + .filter(|s| s.medical_experimental) + .collect(); + assert_eq!(med.len(), 5, "exactly 5 medical-experimental skills"); + for m in &med { + assert!( + m.name.starts_with("med_"), + "medical-flagged skill has non-med_ name: {}", + m.name + ); + } +} From 41665d3de9429c9c104476155aa0e633176ab3b5 Mon Sep 17 00:00:00 2001 From: ruv Date: Sat, 13 Jun 2026 00:33:51 -0400 Subject: [PATCH 2/2] test(wasm-edge): synthetic-ground-truth validation harness for edge skills (ADR-160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plant signals with known answers, run the real detector, MEASURE detection accuracy / precision / recall / rate-error — synthetic-ground-truth ONLY, not field accuracy. MEASURED-on-synthetic (12 tests, all green): - vital_trend, exo_ghost_hunter(hidden breathing), occupancy, intrusion, exo_rain_detect, sig_optimal_transport: acc 1.000 - exo_time_crystal: 1.000 on periodic-vs-aperiodic (its sub-harmonic-vs-clean- period claim is NOT separable by autocorrelation — recorded honestly) - sig_flash_attention: 8/8 peak localization; spt_spiking_tracker: 4/4 zone localization (sparse plant); sig_mincut_person_match: 0 id-swaps/40 frames - lrn_dtw_gesture_learn: enrollment validated (replay-match reported, not asserted) - sig_sparse_recovery: trigger validated; recovery accuracy reported NEGATIVE (-2.2% vs unrecovered baseline) — only its detect/trigger path is validated DATA-GATED (listed, NOT faked): med_seizure/apnea/cardiac/respiratory/gait, sec_weapon_detect, exo_emotion/happiness/dream_stage/gesture_language — each needs real labelled clinical/affect/ASL/metal-object data; no number claimed. benchmarks/edge-skills/RESULTS.md documents every result + reproduce command and the explicit honesty boundary. ADR-160 deferred 'per-skill accuracy validation' item updated to PARTIALLY MEASURED-on-synthetic + DATA-GATED. Suite: 631 passed default / 669 medical, 0 failed. Co-Authored-By: claude-flow --- benchmarks/edge-skills/RESULTS.md | 132 +++ ...-160-edge-skill-library-honest-labeling.md | 31 +- .../tests/synthetic_validation.rs | 762 ++++++++++++++++++ 3 files changed, 921 insertions(+), 4 deletions(-) create mode 100644 benchmarks/edge-skills/RESULTS.md create mode 100644 v2/crates/wifi-densepose-wasm-edge/tests/synthetic_validation.rs diff --git a/benchmarks/edge-skills/RESULTS.md b/benchmarks/edge-skills/RESULTS.md new file mode 100644 index 00000000..bc6b1eeb --- /dev/null +++ b/benchmarks/edge-skills/RESULTS.md @@ -0,0 +1,132 @@ +# Edge-Skill Synthetic-Ground-Truth Validation — RESULTS + +**Crate:** `v2/crates/wifi-densepose-wasm-edge` (workspace-EXCLUDED — build from its own dir) +**Branch:** `feat/edge-skills-synthetic-validation` +**ADR:** [ADR-160](../../docs/adr/ADR-160-edge-skill-library-honest-labeling.md) +**Date:** 2026-06-13 +**Harness:** `tests/synthetic_validation.rs` + +> **HONESTY BOUNDARY — read first.** Everything below is **synthetic-ground-truth +> validation**: a signal is *planted* with a known answer, the **real** detector +> is run, and detection accuracy / precision / recall / rate-error is **measured**. +> This is **NOT field accuracy.** A skill that recovers a planted sinusoid here is +> proven to do the math it claims on a *constructed* signal; it is **NOT** proven +> to work on real CSI in a real room. Skills whose detection target cannot be +> honestly planted (clinical, weapon, affect, sleep-stage, sign-language) are +> **NOT** given a number — they are listed under **DATA-GATED** with the real +> data each would require. + +## Reproduce + +```bash +cd v2/crates/wifi-densepose-wasm-edge # workspace-excluded; build here +cargo test --features std --test synthetic_validation -- --nocapture +# also runs under the medical tier (med_* skills stay DATA-GATED, not validated): +cargo test --features std,medical-experimental --test synthetic_validation -- --nocapture +``` + +Each `MEASURED-on-synthetic | …` line printed by the harness is the source of the +table below. Numbers are deterministic (no RNG; pseudo-noise uses a fixed LCG seed). + +--- + +## MEASURED-on-synthetic (constructible skills) + +| Skill | What was planted (ground truth) | Result | Grade | +|-------|----------------------------------|--------|-------| +| **vital_trend** | BPM held N≥6 calls at each threshold band (brady/tachy-pnea <12 / >25, brady/tachy-cardia <50 / >120, apnea breathing<1.0 for ≥20) vs normal | **acc 1.000, prec 1.000, recall 1.000** (TP5 FP0 TN5 FN0) | MEASURED | +| **exo_time_crystal** | period-2 coordinated motion vs pseudo-noise + flat | **acc 1.000** (TP1 FP0 TN2 FN0) | MEASURED † | +| **exo_ghost_hunter** (hidden breathing) | phase sinusoid at lag-8 (breathing band 5–15) in an empty room vs flat phase | **acc 1.000**; planted score **1.000**, flat **0.000** | MEASURED | +| **occupancy** | 220-frame flat-amplitude calibration, then strong per-zone amplitude variance vs flat | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED | +| **intrusion** | calibrate→arm (330 quiet frames), then per-subcarrier Δphase>1.5 + Δamp≫3σ vs quiet | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED | +| **exo_rain_detect** | empty room, 60-frame baseline, then broadband variance (8/8 groups, ratio≫2.5) for ≥10 frames vs stable-low | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED | +| **sig_flash_attention** | sustained high phase+amplitude in each of the 8 subcarrier groups; assert reported attention peak == planted group | **peak-localization 8/8 = 1.000** | MEASURED | +| **spt_spiking_tracker** | sparse (2-subcarrier) large phase-delta in each of the 4 zones; assert tracked zone == planted zone | **zone-localization 4/4 = 1.000** | MEASURED ‡ | +| **sig_optimal_transport** | sustained large frame-to-frame amplitude-distribution change vs stationary | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED | +| **sig_mincut_person_match** | 2 persons with distinct stable per-region variance signatures over 40 frames | **person ids assigned, 0 id-swaps / 40 frames** | MEASURED | +| **lrn_dtw_gesture_learn** | stillness → 3 identical gesture rehearsals → enrollment | **template enrolled (templates=1)** | MEASURED (enroll) §| +| **sig_sparse_recovery** | 30 clean frames to init, then 8/32 (25%) nulled subcarriers | **dropout-detect + recovery-trigger = PASS** | MEASURED (trigger) ¶| + +### Caveats on individual results + +† **exo_time_crystal — honest discriminative limit.** A *pure* periodic signal +already has autocorrelation peaks at lag L **and** 2L (natural harmonics), so this +"period-doubling" detector cannot separate a true period-2 sub-harmonic from a +plain periodic signal — an earlier plant using a clean sine produced a *false +positive* (recorded during development). The construct it **can** discriminate +with known ground truth is **periodic-coordination vs aperiodic** (noise/flat), +which is what is measured (1.000). The original "sub-harmonic vs clean period" +claim is **NOT** validatable with this algorithm. + +‡ **spt_spiking_tracker — plant must be sparse.** With weights init'd home=1.0 / +cross=0.25, firing all 8 inputs in a zone (8×0.25=2.0 > threshold 1.0) overdrives +*every* output neuron and the tracker collapses to zone 0 (measured 1/4 during +development). Firing only 2 inputs (home 2.0 fires, cross 0.5 silent) yields clean +4/4 zone localization. The validatable claim is *single-zone* localization. + +§ **lrn_dtw_gesture_learn — enrollment validated; replay-match NOT.** The +deterministic, constructible part (stillness → 3 identical rehearsals → a template +is enrolled) is MEASURED. The DTW *replay match* (731) did **not** fire on the +identical replay in this run (`match_same=false`) — replay-recognition accuracy is +**reported, not asserted**, and is not claimed as validated. + +¶ **sig_sparse_recovery — trigger validated; recovery accuracy is NEGATIVE.** +The dropout-detection + ISTA-recovery *trigger* pipeline fires correctly on >10% +planted nulls (asserted). But the **measured recovery accuracy is NOT a win**: +recovered RMSE **1.0045** vs unrecovered-null RMSE **0.9830** (**−2.2%**, i.e. +slightly *worse* than leaving the nulls at zero) on a neighbor-correlated signal. +The tridiagonal correlation model's fixed point does not equal the planted truth. +**The recovery's reconstruction quality is therefore NOT validated as effective on +synthetic data** — only its detection/trigger path is. Reported honestly; no +positive number claimed. + +--- + +## DATA-GATED — NOT validatable on synthetic data + +Planting a "seizure-like" / "weapon-like" / "happy-like" synthetic signal and +claiming the detector "works" validates **nothing real** and is exactly the +AI-slop this project fights. These skills run real DSP (per ADR-160, 0 stubs) and +keep their ADR-160 disclaimers, but get **no accuracy number** here. Each needs +the specific real, labelled data listed: + +| Skill | Why not constructible on synthetic | Real data required | +|-------|------------------------------------|--------------------| +| `med_seizure_detect` | "seizure-like" motion is not a seizure; no ground-truth signature exists synthetically | Clinical EEG-/video-labelled tonic-clonic seizure CSI from instrumented patients | +| `med_sleep_apnea` | a planted breathing-pause is not clinical apnea (AHI scoring, hypopnea, desaturation) | Polysomnography-labelled (PSG) overnight CSI with scored apnea/hypopnea events | +| `med_cardiac_arrhythmia` | a synthetic HR sequence cannot encode true arrhythmia morphology | ECG-labelled CSI (AFib/PVC/etc.) from clinical monitoring | +| `med_respiratory_distress` | distress is a clinical gestalt, not a plantable rate | Clinician-labelled respiratory-distress CSI episodes | +| `med_gait_analysis` | clinical gait metrics need a reference motion-capture standard | Mocap-/force-plate-labelled gait CSI | +| `sec_weapon_detect` | a high variance ratio is RF reflectivity, **not** weapon discrimination (ADR-160 §A3 already renamed the event to `HIGH_METAL_REFLECTIVITY`) | Labelled metal-object-vs-no-object CSI with controlled object classes | +| `exo_emotion_detect` | affect is not recoverable from a planted heuristic; outputs are proxies (ADR-160 §A2) | Validated affect-labelled CSI (self-report / physiological ground truth) | +| `exo_happiness_score` | "happiness" is a gait-energy proxy, not a measured affect (ADR-160 §A2) | Validated affect/valence-labelled CSI | +| `exo_dream_stage` | sleep staging needs PSG reference (EEG/EOG/EMG) | PSG-staged overnight CSI | +| `exo_gesture_language` | coarse gesture clusters ≠ true sign language (ADR-160 §A4) | Labelled ASL letter/word CSI dataset | + +> The above are **not failures** — they are the honest boundary. A smaller set of +> genuinely-measured skills plus this explicit gated list is the deliverable, per +> the prove-everything directive. + +--- + +## Skills not in either list + +The remaining edge skills (smart-building / retail / industrial occupancy-style, +the other `sig_*`/`lrn_*`/`spt_*`/`tmp_*`/`qnt_*`/`aut_*`/`ais_*` algorithm-named +modules) are **wired and exercised live** in the unified pipeline integration test +(`tests/pipeline_all.rs`, all 59 default / 64 medical skills run without panic over +300 synthetic frames) but were **not** given an individual planted-ground-truth +accuracy number here. They are honest REAL-DSP modules (ADR-160) whose physical +observable could be planted with more harness work; that is deferred, not claimed. + +## Test counts (full crate suite) + +``` +DEFAULT (--features std): 631 passed, 0 failed + (lib 504; budget 25; honest_labeling 10; pipeline_all 4; synthetic_validation 12; bench 1; vendor 75) +MEDICAL (--features std,medical-experimental): 669 passed, 0 failed + (lib 542; +16 same new tests; med_* stay DATA-GATED, not validated) +``` + +(M6 baseline was 615 / 653; the new pipeline_all (4) + synthetic_validation (12) +tests add 16 to each tier.) diff --git a/docs/adr/ADR-160-edge-skill-library-honest-labeling.md b/docs/adr/ADR-160-edge-skill-library-honest-labeling.md index 7131a684..6c42956c 100644 --- a/docs/adr/ADR-160-edge-skill-library-honest-labeling.md +++ b/docs/adr/ADR-160-edge-skill-library-honest-labeling.md @@ -178,10 +178,33 @@ 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. +- **Per-skill accuracy validation** — **PARTIALLY MEASURED-on-synthetic** + (2026-06-13). For the subset of skills whose detection target is *constructible* + with known ground truth, a synthetic-ground-truth harness + (`tests/synthetic_validation.rs`, 12 tests) plants signals with known answers, + runs the real detector, and **measures** detection accuracy / rate-error: + `vital_trend`, `exo_time_crystal` (periodic-vs-aperiodic — its sub-harmonic-vs- + clean-period claim is NOT separable, recorded honestly), `exo_ghost_hunter` + (hidden breathing), `occupancy`, `intrusion`, `exo_rain_detect`, + `sig_flash_attention` (8/8 peak localization), `spt_spiking_tracker` (4/4 zone + localization, sparse plant), `sig_optimal_transport`, `sig_mincut_person_match` + (0 id-swaps), `lrn_dtw_gesture_learn` (enrollment) — all 1.000 where claimed; + `sig_sparse_recovery`'s recovery accuracy is reported **negative** (−2.2% vs + unrecovered baseline) — only its trigger path is validated. Full numbers + + reproduce commands in `benchmarks/edge-skills/RESULTS.md`. + The **med_*/affect/sign-language/weapon** claims remain **DATA-GATED**: + validating them requires labelled clinical/affective/ASL/metal-object data and + reference standards that do not exist in this repo. Planting a "seizure-/weapon-/ + happy-like" synthetic signal validates nothing real and is explicitly refused; + RESULTS.md lists each with the real data it needs. The disclaimers + feature gate + are the honest stand-in. Nothing is claimed that is not measured. +- **Unified edge pipeline** — **MEASURED** (2026-06-13). `src/pipeline_all.rs` + (`EdgePipeline`) + `src/skill_registry.rs` register **every** runtime skill + behind one uniform `EdgeSkill` trait and run them all per CSI frame; `med_*` are + registered only under `--features medical-experimental` (preserves the §A1 gate). + `tests/pipeline_all.rs` (4 tests) proves all 59 default / 64 medical skills run + without panic over 300 synthetic frames with a well-formed aggregated event + stream. `examples/run_all_skills.rs` is a runnable demo. No skill DSP changed. - **Criterion benches for `process_frame` budget claims** — **DONE (host)** (ADR-163, 2026-06-12). `benches/process_frame_bench.rs` benches the heaviest hot paths (`exo_time_crystal` 256×128 autocorrelation, `exo_ghost_hunter` diff --git a/v2/crates/wifi-densepose-wasm-edge/tests/synthetic_validation.rs b/v2/crates/wifi-densepose-wasm-edge/tests/synthetic_validation.rs new file mode 100644 index 00000000..c5ab074b --- /dev/null +++ b/v2/crates/wifi-densepose-wasm-edge/tests/synthetic_validation.rs @@ -0,0 +1,762 @@ +//! Synthetic-ground-truth validation harness (ADR-160 deliverable 2). +//! +//! For the subset of edge skills whose detection target can be PLANTED with +//! known ground truth, we generate N signals with known answers, run the real +//! detector, and MEASURE detection rate / precision / recall / rate-error. +//! +//! # Honesty boundary +//! +//! This is **synthetic-ground-truth validation, NOT field accuracy.** A skill +//! that recovers a planted sinusoid here is proven to do the math it claims on +//! a constructed signal; it is NOT proven to work on real CSI in a real room. +//! +//! Skills whose detection target cannot be honestly planted on synthetic data +//! (clinical seizure/apnea/arrhythmia/gait, weapon discrimination, affect/ +//! emotion/happiness, dream stage, sign language) are **NOT** validated here — +//! see RESULTS.md "DATA-GATED" section. Planting a "seizure-like" wiggle and +//! claiming the detector works validates nothing real. +//! +//! Run: +//! cargo test --features std --test synthetic_validation -- --nocapture +//! +//! The printed `MEASURED` lines are the source of `benchmarks/edge-skills/RESULTS.md`. + +#![cfg(feature = "std")] + +use std::f32::consts::PI; + +// ── Confusion-matrix accumulator ───────────────────────────────────────────── + +#[derive(Default, Clone, Copy)] +struct Confusion { + tp: u32, + fp: u32, + tn: u32, + fn_: u32, +} +impl Confusion { + fn observe(&mut self, predicted_positive: bool, actual_positive: bool) { + match (predicted_positive, actual_positive) { + (true, true) => self.tp += 1, + (true, false) => self.fp += 1, + (false, false) => self.tn += 1, + (false, true) => self.fn_ += 1, + } + } + fn precision(&self) -> f32 { + let d = self.tp + self.fp; + if d == 0 { + 1.0 + } else { + self.tp as f32 / d as f32 + } + } + fn recall(&self) -> f32 { + let d = self.tp + self.fn_; + if d == 0 { + 1.0 + } else { + self.tp as f32 / d as f32 + } + } + fn accuracy(&self) -> f32 { + let d = self.tp + self.fp + self.tn + self.fn_; + if d == 0 { + 0.0 + } else { + (self.tp + self.tn) as f32 / d as f32 + } + } + fn report(&self, name: &str) { + println!( + "MEASURED-on-synthetic | {:<34} | acc={:.3} prec={:.3} recall={:.3} | TP={} FP={} TN={} FN={}", + name, + self.accuracy(), + self.precision(), + self.recall(), + self.tp, + self.fp, + self.tn, + self.fn_ + ); + } +} + +// ── 1. vital_trend — rate-threshold detection (directly verified thresholds) ─ +// Thresholds (from src/vital_trend.rs): BRADYPNEA<12, TACHYPNEA>25, +// BRADYCARDIA<50, TACHYCARDIA>120, APNEA at breathing<1.0 for 20 calls; +// ALERT_DEBOUNCE=5. Drive on_timer with known BPM, count event presence. + +#[test] +fn vital_trend_rate_thresholds() { + use wifi_densepose_wasm_edge::vital_trend::VitalTrendAnalyzer; + + // event ids: 101 brady-pnea, 102 tachy-pnea, 103 brady-cardia, 104 tachy-cardia, 105 apnea + fn drive_breathing(bpm: f32, n: u32) -> std::collections::HashSet { + let mut det = VitalTrendAnalyzer::new(); + let mut seen = std::collections::HashSet::new(); + for _ in 0..n { + for &(id, _) in det.on_timer(bpm, 72.0) { + seen.insert(id); + } + } + seen + } + fn drive_heart(bpm: f32, n: u32) -> std::collections::HashSet { + let mut det = VitalTrendAnalyzer::new(); + let mut seen = std::collections::HashSet::new(); + for _ in 0..n { + for &(id, _) in det.on_timer(16.0, bpm) { + seen.insert(id); + } + } + seen + } + + // 6 calls > ALERT_DEBOUNCE(5) so a sustained abnormal value fires. + let mut c = Confusion::default(); + // Bradypnea: <12 positive; normal 16 negative. + c.observe(drive_breathing(8.0, 6).contains(&101), true); + c.observe(drive_breathing(16.0, 6).contains(&101), false); + // Tachypnea: >25 positive; normal negative. + c.observe(drive_breathing(30.0, 6).contains(&102), true); + c.observe(drive_breathing(16.0, 6).contains(&102), false); + // Bradycardia: <50. + c.observe(drive_heart(40.0, 6).contains(&103), true); + c.observe(drive_heart(72.0, 6).contains(&103), false); + // Tachycardia: >120. + c.observe(drive_heart(140.0, 6).contains(&104), true); + c.observe(drive_heart(72.0, 6).contains(&104), false); + // Apnea: breathing < 1.0 for >= 20 calls. + c.observe(drive_breathing(0.0, 20).contains(&105), true); + c.observe(drive_breathing(0.0, 10).contains(&105), false); // only 10 calls -> below APNEA_SECONDS + + c.report("vital_trend (brady/tachy-pnea/cardia, apnea)"); + // All 5 thresholds + their negatives must classify correctly. + assert_eq!(c.accuracy(), 1.0, "vital_trend rate thresholds must be exact"); +} + +// ── 2. exo_time_crystal — period-doubling (sub-harmonic) detection ─────────── +// Detects a peak at lag L AND a peak at lag 2L in motion-energy autocorrelation. +// PLANT positive: period-2 modulation (alternating amplitude on a base period) +// so autocorr has peaks at both L and 2L. +// PLANT negative: a single clean period (peak at L only) or noise. + +fn run_time_crystal(motion: &[f32]) -> bool { + use wifi_densepose_wasm_edge::exo_time_crystal::TimeCrystalDetector; + let mut det = TimeCrystalDetector::new(); + let mut detected = false; + for &m in motion { + for &(id, v) in det.process_frame(m) { + if id == 680 && v >= 2.0 { + detected = true; // CRYSTAL_DETECTED with multiplier 2 + } + } + } + detected +} + +#[test] +fn exo_time_crystal_period_doubling() { + let n = 256usize; + // Positive: period-2 subharmonic. Base period P=16; alternate full periods + // are scaled differently so the waveform only repeats every 2P=32 (peak at + // lag 32) while still correlating at P=16. Plain sine (no abs, which would + // itself fold frequency and fake a sub-harmonic). + let base_p = 16.0f32; + let mut pos = Vec::with_capacity(n); + for t in 0..n { + let phase = (t as f32) * 2.0 * PI / base_p; + let sub = if ((t as f32 / base_p) as i32) % 2 == 0 { 1.0 } else { 0.45 }; + pos.push(0.6 + 0.35 * phase.sin() * sub); + } + // HONEST LIMIT (measured below): a *pure* periodic signal already has + // autocorrelation peaks at L AND 2L (natural harmonics), so this detector + // cannot separate a true period-2 sub-harmonic from a plain periodic signal. + // The construct it CAN discriminate with known ground truth is + // "periodic-with-coordination vs aperiodic". We validate that. + // + // Negative 1: incrementing-seed pseudo-noise (no periodicity). + let mut noise = Vec::with_capacity(n); + let mut s: u32 = 12345; + for _ in 0..n { + s = s.wrapping_mul(1664525).wrapping_add(1013904223); + noise.push(0.3 + 0.4 * ((s >> 8) & 0xffff) as f32 / 65535.0); + } + // Negative 2: near-constant motion (no oscillation at all). + let flat: Vec = (0..n).map(|t| 0.5 + 1e-4 * (t as f32 * 0.01).sin()).collect(); + + let mut c = Confusion::default(); + c.observe(run_time_crystal(&pos), true); // planted period-2 -> detect + c.observe(run_time_crystal(&noise), false); // pseudo-noise -> reject + c.observe(run_time_crystal(&flat), false); // flat -> reject + c.report("exo_time_crystal (periodic-coordination vs aperiodic)"); + assert!( + run_time_crystal(&pos), + "must detect planted period-2 coordinated motion" + ); + assert!( + !run_time_crystal(&noise), + "must NOT fire on pseudo-noise" + ); + assert!(!run_time_crystal(&flat), "must NOT fire on flat motion"); +} + +// ── 3. exo_ghost_hunter — hidden breathing (autocorr at breathing-range lag) ─ +// When presence==0, aggregate phase is autocorrelated at lags 5..=15; a peak +// there above HIDDEN_PRESENCE_THRESHOLD(0.3) emits HIDDEN_PRESENCE(652). +// PLANT positive: phase sinusoid at a lag in [5,15] across an empty room. +// PLANT negative: flat phase (no periodic breathing signature). + +fn run_ghost_hidden_breathing(period: f32, amp: f32, frames: usize) -> f32 { + use wifi_densepose_wasm_edge::exo_ghost_hunter::GhostHunterDetector; + let mut det = GhostHunterDetector::new(); + let n_sc = 32usize; + let mut max_hidden = 0.0f32; + for t in 0..frames { + let breath = if period > 0.0 { + amp * (t as f32 * 2.0 * PI / period).sin() + } else { + 0.0 + }; + let mut phases = [0.0f32; 32]; + let mut amps = [0.0f32; 32]; + let mut vars = [0.0f32; 32]; + for i in 0..n_sc { + // breathing modulates phase uniformly (chest motion -> common phase shift) + phases[i] = 0.1 * (i as f32 * 0.2).sin() + breath; + amps[i] = 1.0; + vars[i] = 0.01; + } + // presence = 0 (empty room) is required for the hidden-breathing path. + for &(id, v) in det.process_frame(&phases, &s, &vars, 0, 0.0) { + if id == 652 { + if v > max_hidden { + max_hidden = v; + } + } + } + } + max_hidden +} + +#[test] +fn exo_ghost_hunter_hidden_breathing() { + // Period 8 frames is within the breathing lag window [5,15]. + let pos = run_ghost_hidden_breathing(8.0, 0.5, 200); + // Flat phase (no breathing) -> no hidden-presence event. + let neg = run_ghost_hidden_breathing(0.0, 0.0, 200); + + let mut c = Confusion::default(); + c.observe(pos > 0.0, true); + c.observe(neg > 0.0, false); + c.report("exo_ghost_hunter (hidden breathing, lag 8)"); + println!( + " detail: planted-breathing hidden-presence score={:.3}, flat-phase score={:.3}", + pos, neg + ); + assert!( + pos > 0.3, + "planted breathing must score above HIDDEN_PRESENCE_THRESHOLD (0.3); got {}", + pos + ); + assert!( + neg <= 0.0, + "flat phase must not emit hidden presence; got {}", + neg + ); +} + +// ── 4. occupancy — calibration + variance-driven zone occupancy ────────────── +// BASELINE_FRAMES=200 of low-variance amplitudes establish baseline; then +// high amplitude-variance per zone (score > ZONE_THRESHOLD=0.02) flips a zone +// to occupied (EVENT_ZONE_OCCUPIED=300). + +#[test] +fn occupancy_variance_detection() { + use wifi_densepose_wasm_edge::occupancy::OccupancyDetector; + + fn run(occupied_signal: bool) -> bool { + let mut det = OccupancyDetector::new(); + let n_sc = 32usize; + let mut phases = [0.0f32; 32]; + // Calibration: 220 frames of near-flat amplitudes (low variance). + for t in 0..220 { + let mut amps = [1.0f32; 32]; + for i in 0..n_sc { + amps[i] = 1.0 + 1e-3 * ((t + i) as f32 * 0.7).sin(); + phases[i] = 0.01 * (i as f32).sin(); + } + det.process_frame(&phases, &s); + } + // Test phase: 60 frames. If occupied, inject strong per-zone amplitude + // variance; else keep flat. + let mut fired = false; + for t in 0..60 { + let mut amps = [1.0f32; 32]; + for i in 0..n_sc { + amps[i] = if occupied_signal { + // strong structured variance within each zone + 1.0 + 2.0 * (((i % 4) as f32) - 1.5) + 0.5 * (t as f32 * 0.3 + i as f32).sin() + } else { + 1.0 + 1e-3 * ((t + i) as f32 * 0.7).sin() + }; + } + for &(id, _) in det.process_frame(&phases, &s) { + if id == 300 { + fired = true; + } + } + } + fired + } + + let mut c = Confusion::default(); + c.observe(run(true), true); + c.observe(run(false), false); + c.report("occupancy (zone variance vs flat baseline)"); + assert!(run(true), "high zone variance after calibration must occupy a zone"); + assert!(!run(false), "flat amplitude must stay unoccupied"); +} + +// ── 5. intrusion — calibrate, arm, then disturbance>=0.8 alerts ────────────── +// disturbance = 0.6*frac(|Δphase|>1.5) + 0.4*frac(|Δamp|>3σ). Calibrate 200 +// quiet frames, monitor 100 quiet frames -> Armed, then 3 frames of large +// phase+amp disturbance -> EVENT_INTRUSION_ALERT(200). + +#[test] +fn intrusion_disturbance_alert() { + use wifi_densepose_wasm_edge::intrusion::IntrusionDetector; + + fn run(intrude: bool) -> bool { + let mut det = IntrusionDetector::new(); + let n_sc = 32usize; + // Calibration (200) + monitoring quiet (120) -> Armed. Quiet = constant. + for _ in 0..330 { + let phases = [0.5f32; 32]; + let amps = [1.0f32; 32]; + det.process_frame(&phases, &s); + } + let mut alerted = false; + // 10 test frames. + for t in 0..10 { + let mut phases = [0.5f32; 32]; + let mut amps = [1.0f32; 32]; + if intrude { + for i in 0..n_sc { + // alternate phase by 3.0 (>1.5) and amplitude far from baseline 1.0. + phases[i] = if t % 2 == 0 { 0.5 } else { 4.0 }; + amps[i] = 1.0 + 8.0; // huge deviation vs ~0 baseline variance + } + } + for &(id, _) in det.process_frame(&phases, &s) { + if id == 200 { + alerted = true; + } + } + } + alerted + } + + let mut c = Confusion::default(); + c.observe(run(true), true); + c.observe(run(false), false); + c.report("intrusion (armed -> disturbance alert vs quiet)"); + assert!(run(true), "large phase+amplitude disturbance must alert when armed"); + assert!(!run(false), "quiet environment must not alert"); +} + +// ── 6. sig_sparse_recovery — ISTA recovery of planted null subcarriers ─────── +// Initialize correlation on clean frames, then null >10% of subcarriers and +// MEASURE how well ISTA recovers them (rate-error style: recovery residual). + +#[test] +fn sig_sparse_recovery_recovers_nulls() { + use wifi_densepose_wasm_edge::sig_sparse_recovery::SparseRecovery; + + let mut det = SparseRecovery::new(); + let n_sc = 32usize; + // Underlying smooth signal (neighbor-correlated) the model can learn. + let truth: Vec = (0..n_sc).map(|i| 1.0 + 0.5 * (i as f32 * 0.4).sin()).collect(); + + // Warm up correlation model with 30 clean frames. + for _ in 0..30 { + let mut amps: Vec = truth.clone(); + det.process_frame(&mut amps); + } + + // Null subcarriers 5..13 (8/32 = 25% > MIN_DROPOUT_RATE 0.10). + let mut amps: Vec = truth.clone(); + let nulled: Vec = (5..13).collect(); + for &i in &nulled { + amps[i] = 0.0; + } + // Baseline error if the nulls were left at 0.0 (unrecovered). + let mut sse0 = 0.0f32; + for &i in &nulled { + sse0 += truth[i] * truth[i]; + } + let baseline_rmse = (sse0 / nulled.len() as f32).sqrt(); + + let mut recovery_seen = false; + for &(id, _) in det.process_frame(&mut amps) { + if id == 715 { + recovery_seen = true; // RECOVERY_COMPLETE + } + } + // Measure recovery error on the nulled positions (now written back in-place). + let mut sse = 0.0f32; + for &i in &nulled { + let d = amps[i] - truth[i]; + sse += d * d; + } + let rmse = (sse / nulled.len() as f32).sqrt(); + println!( + "MEASURED-on-synthetic | {:<34} | dropout-detect+recovery-trigger=PASS | recovered RMSE={:.4} vs unrecovered-null RMSE={:.4} ({:+.1}%) over {} nulled subcarriers", + "sig_sparse_recovery (ISTA)", + rmse, + baseline_rmse, + 100.0 * (1.0 - rmse / baseline_rmse), + nulled.len() + ); + // CONSTRUCTIBLE + MEASURED: the dropout detection and recovery-trigger + // pipeline fires correctly on >10% planted nulls. This is the validatable + // claim and we assert it. + assert!(recovery_seen, "dropout > 10% must trigger ISTA recovery (RECOVERY_COMPLETE)"); + // HONEST MEASURED RESULT (reported, NOT asserted as a win): on this + // neighbor-correlated synthetic signal the tridiagonal-model ISTA recovery + // does NOT beat leaving the nulls at zero (RMSE ~1.00 vs ~0.98). The skill's + // *recovery accuracy* is therefore NOT validated as effective on synthetic + // data — only its dropout-detection/trigger path is. Reported in RESULTS.md. + assert!( + rmse.is_finite() && rmse < 5.0, + "recovered values must be finite and bounded; got {}", + rmse + ); +} + +// ── 7. exo_rain_detect — broadband variance onset (empty room) ─────────────── +// presence=0, MIN_EMPTY_FRAMES=40 baseline, then >=6/8 groups with variance +// ratio > 2.5 for ONSET_FRAMES=10 -> EVENT_RAIN_ONSET(660). + +#[test] +fn exo_rain_detect_broadband_onset() { + use wifi_densepose_wasm_edge::exo_rain_detect::RainDetector; + + fn run(rain: bool) -> bool { + let mut det = RainDetector::new(); + let n_sc = 32usize; + let phases = [0.1f32; 32]; + let amps = [1.0f32; 32]; + // 60 empty baseline frames with low variance. + for _ in 0..60 { + let vars = [0.001f32; 32]; + det.process_frame(&phases, &vars, &s, 0); + } + let mut onset = false; + // 40 frames: broadband-high variance if rain, else stay low. + for _ in 0..40 { + let vars = if rain { [0.5f32; 32] } else { [0.001f32; 32] }; + for &(id, _) in det.process_frame(&phases, &vars, &s, 0) { + if id == 660 { + onset = true; + } + } + } + let _ = n_sc; + onset + } + + let mut c = Confusion::default(); + c.observe(run(true), true); + c.observe(run(false), false); + c.report("exo_rain_detect (broadband variance onset)"); + assert!(run(true), "broadband variance elevation must trigger rain onset"); + assert!(!run(false), "stable low variance must not trigger rain"); +} + +// ── 8. sig_flash_attention — peak-attention subcarrier localization ────────── +// Q=mean(phase) per group, K=mean(prev_phase), score=Q*K/sqrt(8), softmax peak. +// Plant a sustained large phase in a KNOWN group -> assert that group becomes +// the reported attention peak (EVENT_ATTENTION_PEAK_SC=700). + +#[test] +fn sig_flash_attention_peak_localization() { + use wifi_densepose_wasm_edge::sig_flash_attention::FlashAttention; + + fn peak_for_group(target_group: usize) -> i32 { + let mut det = FlashAttention::new(); + let n_sc = 32usize; + let subs_per = n_sc / 8; + let mut last_peak = -1; + // Sustain the spike so both Q (this frame) and K (prev frame) are large + // in the target group -> highest score there. + for _ in 0..20 { + let mut phases = [0.05f32; 32]; + let mut amps = [1.0f32; 32]; + for i in (target_group * subs_per)..((target_group + 1) * subs_per) { + phases[i] = 3.0; + amps[i] = 3.0; + } + for &(id, v) in det.process_frame(&phases, &s) { + if id == 700 { + last_peak = v as i32; + } + } + } + last_peak + } + + let mut correct = 0u32; + let total = 8u32; + for g in 0..8usize { + let got = peak_for_group(g); + if got == g as i32 { + correct += 1; + } + println!(" flash_attention: planted group {} -> reported peak {}", g, got); + } + let acc = correct as f32 / total as f32; + println!( + "MEASURED-on-synthetic | {:<34} | peak-localization accuracy = {}/{} = {:.3}", + "sig_flash_attention", correct, total, acc + ); + assert!(acc >= 0.75, "must localize the planted attention group in >=75% of cases; got {}", acc); +} + +// ── 9. spt_spiking_tracker — phase-delta zone localization ─────────────────── +// LIF neurons fire on |phase - prev_phase|; zone with most spikes is tracked +// (EVENT_TRACK_UPDATE=770 carries zone id). Plant motion in a KNOWN zone. + +#[test] +fn spt_spiking_tracker_zone_localization() { + use wifi_densepose_wasm_edge::spt_spiking_tracker::SpikingTracker; + + fn track_zone(target_zone: usize) -> i32 { + let mut det = SpikingTracker::new(); + let n_sc = 32usize; + let per = n_sc / 4; // 4 zones of 8 subcarriers + let mut prev = [0.0f32; 32]; + let mut last_zone = -1; + // SPARSE plant: each zone's output neuron sums home-weight 1.0 + cross + // 0.25. Firing all 8 inputs (8*0.25=2.0) overdrives EVERY zone, so the + // tracker collapses to zone 0. Firing only 2 inputs in the target zone + // gives potential 2.0 at home (fires) but 0.5 cross (silent) -> only the + // target zone fires. This is the genuinely-constructible localization. + let base = target_zone * per; + for t in 0..60 { + let mut phases = [0.0f32; 32]; + // 2 subcarriers in the target zone get a large alternating delta. + for k in 0..2 { + phases[base + k] = if t % 2 == 0 { 0.0 } else { 3.0 }; + } + for &(id, v) in det.process_frame(&phases, &prev) { + if id == 770 { + last_zone = v as i32; + } + } + prev.copy_from_slice(&phases); + } + last_zone + } + + let mut correct = 0u32; + for z in 0..4usize { + let got = track_zone(z); + if got == z as i32 { + correct += 1; + } + println!(" spiking_tracker: planted zone {} -> tracked zone {}", z, got); + } + let acc = correct as f32 / 4.0; + println!( + "MEASURED-on-synthetic | {:<34} | zone-localization accuracy = {}/4 = {:.3}", + "spt_spiking_tracker", correct, acc + ); + assert!(acc >= 0.75, "must track the planted motion zone in >=75% of cases; got {}", acc); +} + +// ── 10. sig_optimal_transport — distribution-shift detection ───────────────── +// Sliced Wasserstein over amplitudes; sustained shift > WASS_SHIFT(0.25) for +// SHIFT_DEB(3) -> EVENT_DISTRIBUTION_SHIFT(726). Plant a large vs no shift. + +#[test] +fn sig_optimal_transport_distribution_shift() { + use wifi_densepose_wasm_edge::sig_optimal_transport::OptimalTransportDetector; + + fn run(shift: bool) -> bool { + let mut det = OptimalTransportDetector::new(); + let n_sc = 32usize; + // Establish a reference distribution. + let base: Vec = (0..n_sc).map(|i| i as f32 * 0.1).collect(); + for _ in 0..10 { + let mut a = base.clone(); + det.process_frame(&mut a); + } + let mut shifted = false; + // The detector compares each frame to the PREVIOUS frame (prev_amps is + // updated every frame), so a one-time jump decays. To exceed WASS_SHIFT + // (0.25) for SHIFT_DEB(3) consecutive frames we need a sustained large + // frame-to-frame change: alternate between two very different + // distributions each frame. + for t in 0..15 { + let mut a: Vec = if shift { + if t % 2 == 0 { + base.clone() + } else { + base.iter().map(|x| 10.0 - x).collect() // reversed + offset + } + } else { + base.clone() + }; + for &(id, _) in det.process_frame(&mut a) { + if id == 726 { + shifted = true; + } + } + } + shifted + } + + let mut c = Confusion::default(); + c.observe(run(true), true); + c.observe(run(false), false); + c.report("sig_optimal_transport (distribution shift)"); + assert!(run(true), "large amplitude-distribution shift must be detected"); + assert!(!run(false), "stationary distribution must not flag a shift"); +} + +// ── 11. lrn_dtw_gesture_learn — enroll a template, replay match vs reject ──── +// STILLNESS_FRAMES=60 stillness, then 3 rehearsals of the same gesture +// (motion->stillness) -> EVENT_GESTURE_LEARNED(730). Replaying the learned +// gesture later (in Idle) -> EVENT_GESTURE_MATCHED(731); replaying a different +// gesture -> no match. + +#[test] +fn lrn_dtw_gesture_learn_enroll_and_match() { + use wifi_densepose_wasm_edge::lrn_dtw_gesture_learn::GestureLearner; + + // A gesture is a phase trajectory across frames; motion_energy gates the + // enroll state machine (still < 0.05, moving >= 0.05). + fn gesture_frame(kind: u8, step: usize) -> ([f32; 32], f32) { + let mut phases = [0.0f32; 32]; + let s = step as f32; + for i in 0..32 { + phases[i] = match kind { + // distinct trajectories + 0 => (s * 0.4 + i as f32 * 0.1).sin(), + _ => (s * 0.9 + i as f32 * 0.05).cos() * 1.5, + }; + } + (phases, 0.5) // moving + } + + let mut det = GestureLearner::new(); + let still = ([0.0f32; 32], 0.0f32); + + // helper to feed N still frames + let feed_still = |det: &mut GestureLearner, n: usize| { + for _ in 0..n { + det.process_frame(&still.0, still.1); + } + }; + let feed_gesture = |det: &mut GestureLearner, kind: u8, len: usize| -> bool { + let mut learned = false; + for s in 0..len { + let (ph, me) = gesture_frame(kind, s); + for &(id, _) in det.process_frame(&ph, me) { + if id == 730 { + learned = true; + } + } + } + learned + }; + + // Enroll gesture kind 0: stillness, then 3 identical rehearsals (each + // motion burst followed by stillness). + feed_still(&mut det, 70); + let mut any_learned = false; + for _ in 0..3 { + any_learned |= feed_gesture(&mut det, 0, 30); + feed_still(&mut det, 70); + } + + // Replay the SAME gesture during Idle -> expect a match (731). + let mut matched_same = false; + for s in 0..30 { + let (ph, me) = gesture_frame(0, s); + for &(id, _) in det.process_frame(&ph, me) { + if id == 731 { + matched_same = true; + } + } + } + feed_still(&mut det, 70); + // Replay a DIFFERENT gesture -> ideally no match (731) to the learned one. + let mut matched_diff = false; + for s in 0..30 { + let (ph, me) = gesture_frame(1, s); + for &(id, _) in det.process_frame(&ph, me) { + if id == 731 { + matched_diff = true; + } + } + } + + let tmpl_count = det.template_count(); + println!( + "MEASURED-on-synthetic | {:<34} | learned_event={} templates={} match_same={} match_different={}", + "lrn_dtw_gesture_learn", any_learned, tmpl_count, matched_same, matched_diff + ); + // The enroll path must complete (a template is learned from 3 identical + // rehearsals). Whether the precise replay matches is the DTW behavior we + // measure and report; we assert the deterministic enrollment. + assert!( + any_learned || tmpl_count > 0, + "3 identical rehearsals after stillness must enroll a template" + ); +} + +// ── 12. sig_mincut_person_match — stable id assignment for distinct signatures ─ +// Per-person feature = top-FEAT_DIM variances in that person's spatial region. +// Two persons with DISTINCT, stable variance signatures should get stable ids +// (EVENT_PERSON_ID_ASSIGNED=720) with zero swaps across frames. + +#[test] +fn sig_mincut_person_stable_ids() { + use wifi_densepose_wasm_edge::sig_mincut_person_match::PersonMatcher; + + let mut det = PersonMatcher::new(); + let n_sc = 32usize; + let amplitudes = [1.0f32; 32]; + let mut swaps = 0u32; + let mut assigned = false; + + // 40 frames, 2 persons: person 0 region (0..16) high-variance signature, + // person 1 region (16..32) low-variance signature, both stable. + for _ in 0..40 { + let mut variances = [0.0f32; 32]; + for i in 0..n_sc { + variances[i] = if i < 16 { + 2.0 + 0.05 * (i as f32).sin() + } else { + 0.2 + 0.01 * (i as f32).cos() + }; + } + for &(id, _) in det.process_frame(&litudes, &variances, 2) { + if id == 720 { + assigned = true; + } + if id == 721 { + swaps += 1; + } + } + } + println!( + "MEASURED-on-synthetic | {:<34} | assigned={} id_swaps_over_40_frames={}", + "sig_mincut_person_match", assigned, swaps + ); + assert!(assigned, "distinct stable signatures must assign person ids"); + assert!(swaps == 0, "stable distinct signatures must not swap ids; got {} swaps", swaps); +}