Files
ruvnet--RuView/v2/crates/wifi-densepose-sensing-server/src/semantic/mod.rs
T
ruv b2a692369e feat(adr-115): P4.5b — 6 remaining semantic primitives — all 10 HA-MIND v1 done (66 tests)
Lands the remaining six §3.12 v1 primitives:

- `distress` (PossibleDistress) — EWMA baseline HR + 1.5× multiplier
  + agitated motion + no-fall + 60 s dwell → ON. Refractory 5 min
  after exit. Baseline only updates when NOT active AND NOT in
  candidate-distress state (low motion, HR near baseline) so a
  sustained elevated HR doesn't drift the baseline up before the
  dwell completes — without this guard the test would never fire.
- `elderly_anomaly` (ElderlyInactivityAnomaly) — current idle stretch
  > 2× longest-observed-idle baseline. Baseline floor at 30 min so
  the first day doesn't fire spuriously. 24 h refractory per resident.
- `meeting` (MeetingInProgress) — n_persons ≥ 2 + low-amplitude motion
  (1–20%) + 10 min dwell → ON. 2 min exit dwell on count drop.
- `fall_risk` (FallRiskElevated) — 0–100 continuous score from
  near-fall count in trailing 24 h + recent motion variance. Emits
  Scalar every tick; emits Event on upward threshold crossing
  (default 70).
- `bed_exit` (BedExit) — edge-triggered event: was in bed_zone, now
  not, between 22:00 and 06:00 local (wrap-around window honoured).
- `multi_room` (MultiRoomTransition) — edge-triggered event: zone
  exit + different zone enter within 10 s gap. Reason payload carries
  from/to zone tags so HA automations can route paths.

Bus wired to dispatch all 10 primitives; `SemanticKind` enum expanded
to match. `tick()` returns up to 10 events per snapshot.

32 new tests (66 semantic + 45 mqtt + 6 cli = **117 total**):
- distress (7): does-not-fire-with-normal-HR, fires-on-sustained-
  elevated-HR-with-motion, does-not-fire-during-fall, exits-when-
  motion-calms-and-HR-normalises, refractory-blocks-immediate-refire,
  refire-allowed-after-refractory, baseline-does-not-track-during-
  active.
- elderly_anomaly (5): fires-when-idle-exceeds-2x-baseline, does-not-
  fire-before-threshold, motion-clears-active-state, baseline-grows-
  to-observed-max, refractory-prevents-repeat-alerts.
- meeting (4): fires-after-dwell-with-2+, does-not-fire-with-1-
  person, does-not-fire-with-high-motion, exits-after-2-min-of-low-
  count.
- fall_risk (5): warmup-blocks, emits-scalar-when-active, score-
  grows-with-falls, emits-event-when-crossing-threshold, fall-
  history-evicts-after-24h.
- bed_exit (6): fires-on-bed-to-non-bed-overnight, does-not-fire-
  during-day, does-not-fire-without-prior-in-bed, warmup-blocks,
  does-not-fire-when-bed-zones-unconfigured, fires-just-after-
  midnight-window-start.
- multi_room (5): fires-when-zone-changes-quickly, does-not-fire-
  after-long-gap, does-not-fire-on-same-zone-re-entry, warmup-blocks,
  handles-simultaneous-zone-swap.

ADR-115 §3.12 inference layer now complete. Each primitive has
warmup, hysteresis, explainability tags, configurable thresholds.
Adding a v2 primitive is one file + one bus entry.

Refs #776.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-23 14:07:21 -04:00

64 lines
3.0 KiB
Rust

//! ADR-115 §3.12 — Semantic Automation Primitives (HA-MIND).
//!
//! Raw signals are not the product. Customers want first-class entities
//! like `binary_sensor.bedroom_someone_sleeping`, not a Node-RED flow
//! that thresholds breathing rate at night. This module owns the
//! inference layer that turns the `sensing-server` broadcast (raw
//! `edge_vitals` / `pose_data` / `sensing_update`) into the 10 v1
//! semantic primitives published as HA entities, Matter events, and
//! Apple Home scene triggers.
//!
//! ## Architectural contract
//!
//! - **Server-side inference.** All primitives run inside this process.
//! Only the inferred *state* (true/false, scalar, event) crosses the
//! wire. This is what makes `--privacy-mode` compatible with
//! semantic primitives — biometric *values* can be stripped at the
//! integration boundary while the inferred *states* still publish.
//! - **One source of truth.** Each primitive's FSM lives in one file
//! alongside its tests. The `SemanticBus` aggregates output and
//! broadcasts to MQTT + Matter consumers. Adding a new primitive is
//! one file change — no new MQTT discovery schema, no new Matter
//! cluster.
//! - **Explainability.** Every state change carries a `reason`
//! payload so HA users can debug *why* a primitive fired.
//! - **Hysteresis everywhere.** Each primitive has explicit enter /
//! exit thresholds + minimum dwell time so a single noisy frame
//! never toggles state. Refractory periods prevent alert spam.
//! - **Warmup suppression.** No primitive fires during the first 60 s
//! after start (per §3.12.4 — sensors are still settling).
//!
//! ## Primitives (v1)
//!
//! | Primitive | Module | Output |
//! |-------------------------|-----------------------|------------------|
//! | someone_sleeping | [`sleeping`] | binary_sensor |
//! | possible_distress | [`distress`] | binary_sensor + event |
//! | room_active | [`room_active`] | binary_sensor |
//! | elderly_inactivity_… | [`elderly_anomaly`] | binary_sensor + event |
//! | meeting_in_progress | [`meeting`] | binary_sensor |
//! | bathroom_occupied | [`bathroom`] | binary_sensor |
//! | fall_risk_elevated | [`fall_risk`] | sensor (0-100) |
//! | bed_exit | [`bed_exit`] | event |
//! | no_movement | [`no_movement`] | binary_sensor |
//! | multi_room_transition | [`multi_room`] | event |
//!
//! Each module exports a struct implementing [`Primitive`] and a `new`
//! constructor that takes a [`PrimitiveConfig`].
mod bathroom;
mod bed_exit;
mod bus;
mod common;
mod distress;
mod elderly_anomaly;
mod fall_risk;
mod meeting;
mod multi_room;
mod no_movement;
mod room_active;
mod sleeping;
pub use bus::{SemanticBus, SemanticEvent, SemanticKind};
pub use common::{PrimitiveConfig, PrimitiveState, RawSnapshot, Reason};