Files
ruvnet--RuView/v2/crates/ruview-gamma/src/program.rs
T
ruv 192b4c6b4a fix(ruview-gamma): safety becomes a property of construction (safety review, 2 critical + 3 major)
- safety stop is now control flow: run_calibration terminates the sweep on
  any stop (partial calibration recorded)
- cross-session terminate-and-lock: persisted latched ParticipantSafetyState;
  locked participants refuse sessions across governor instances until
  unlock_with_acknowledgment writes an audit record; documented class->lock map
- SafetyEnvelope safe-by-construction: private fields, validated try_new/with_*
  constructors, serde routed through the validator, compiled-in absolute bounds
  (floor >=30Hz keeps the whole band above the 15-25Hz photosensitive zone;
  caps <=0.6; <=30min) — an 18-22Hz config fails to deserialize
- dose governance: <=4 sittings/24h + >=60min cooldown from the persisted ledger
- per-tick latched SafetyMonitor wired into run_session (simulator emits ticks;
  mid-session latch truncates delivered duration); clean-session witness
  byte-identical (pinned proof unchanged)
- cohort warm-start k-floor (>=3 profiles)
ADR-250 + crate README updated. 125+13 tests green.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-11 15:26:17 -04:00

375 lines
15 KiB
Rust

//! Neuromodulation programs (ADR-250 extension: adaptive sensory
//! neuromodulation, not just Alzheimer's).
//!
//! The platform thesis: RuView turns the body into the feedback signal,
//! RuVector turns repeated sessions into a personal response map, the device is
//! the actuator, and RuFlo makes the loop governed and auditable. The *real*
//! product is a personal neural-rhythm optimization platform — and each use
//! case is a [`NeuroProgram`] bundling its own safety envelope, starting prior,
//! objective weighting, physiological-state gating, evidence level, and the
//! single claim it is allowed to make.
//!
//! **Claim discipline is structural:** a program's [`NeuroProgram::claim`] is
//! always an *optimization / monitoring* statement, never a disease-treatment
//! claim. The disease context lives only in [`EvidenceLevel`], and a claim is
//! only releasable once the program clears its acceptance gate
//! (`crate::acceptance`).
use crate::objective::ObjectiveWeights;
use crate::response::SleepState;
use crate::stimulus::{DutyCycle, Modality, SafetyEnvelope, StimulusParameters};
/// How well-supported a program's *disease/context* hypothesis is in the
/// literature (the user's opportunity map). This gates nothing by itself — it
/// is metadata a clinician/operator reads alongside the acceptance report.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvidenceLevel {
/// Preclinical + early human (e.g. Alzheimer's gamma work).
MediumPreclinicalEarlyHuman,
/// Early human signals only (post-stroke, sleep, mood).
EarlyHuman,
/// Mixed / protocol-dependent (attention, working memory).
Mixed,
/// Speculative (home neuro-wellness).
Speculative,
/// Strong *infrastructure* opportunity (drug+device trial monitoring) —
/// the strength is in measurement/governance, not a therapeutic claim.
StrongInfrastructure,
}
impl EvidenceLevel {
pub fn tag(self) -> &'static str {
match self {
EvidenceLevel::MediumPreclinicalEarlyHuman => "medium_preclinical_early_human",
EvidenceLevel::EarlyHuman => "early_human",
EvidenceLevel::Mixed => "mixed_protocol_dependent",
EvidenceLevel::Speculative => "speculative",
EvidenceLevel::StrongInfrastructure => "strong_infrastructure",
}
}
}
/// Time-of-day preference for a program (state-dependent entrainment).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimePreference {
Morning,
Evening,
/// Quiet wakefulness (the Alzheimer's/attention default).
QuietWake,
/// Pre-sleep / during sleep (the sleep program).
PreSleepOrSleep,
/// Any time.
Any,
}
/// A named neuromodulation program: everything that distinguishes one use case
/// from another, in one value.
#[derive(Debug, Clone)]
pub struct NeuroProgram {
/// Stable machine id (used in the session witness / provenance).
pub id: &'static str,
/// Human-readable name.
pub display_name: &'static str,
/// Literature support for the *context* hypothesis.
pub evidence_level: EvidenceLevel,
/// The ONLY claim this program may surface (an optimization/monitoring
/// statement — never a disease-treatment claim).
pub claim: &'static str,
/// Per-program safety envelope (optimization happens only inside it).
pub envelope: SafetyEnvelope,
/// Evidence-based starting prior for this program.
pub prior: StimulusParameters,
/// Objective weighting tuned to the program's goal.
pub weights: ObjectiveWeights,
/// Physiological states in which a session is *protocol-eligible* (e.g. the
/// sleep program permits `Asleep`; attention requires wakefulness).
pub eligible_states: &'static [SleepState],
/// When the program prefers to run.
pub time_preference: TimePreference,
}
impl NeuroProgram {
/// Whether a session in `state` fits this program's protocol. This is a
/// *protocol-fit* gate (measured by acceptance), distinct from the hard
/// *safety* gate (`SafetyEnvelope` / `SafetyMonitor`).
pub fn state_eligible(&self, state: SleepState) -> bool {
self.eligible_states.contains(&state)
}
// ---- The opportunity map (one constructor per use case) ----
/// Alzheimer's research — adaptive entrainment + trial monitoring. Matches
/// the original ADR-250 defaults (conservative envelope, 40 Hz prior,
/// default weights), so the `RufloGovernor::enroll` witness is unchanged.
pub fn alzheimers_research() -> Self {
Self {
id: "alzheimers-research",
display_name: "Alzheimer's Research (adaptive entrainment + trial monitoring)",
evidence_level: EvidenceLevel::MediumPreclinicalEarlyHuman,
claim: "personalized entrainment optimization",
envelope: SafetyEnvelope::conservative(),
prior: StimulusParameters::prior(),
weights: ObjectiveWeights::default(),
eligible_states: &[SleepState::QuietWake, SleepState::Drowsy],
time_preference: TimePreference::QuietWake,
}
}
/// Post-stroke cognition — recovery state tracking. Comfort-leaning, gentle
/// onset (ramped), short sessions; recovery populations tolerate less.
pub fn post_stroke_cognition() -> Self {
let mut prior = StimulusParameters::prior();
prior.duty_cycle = DutyCycle::Ramped;
prior.brightness_level = 0.25;
prior.volume_level = 0.24;
prior.duration_minutes = 8.0;
let mut weights = ObjectiveWeights::default();
weights.comfort = 0.20;
weights.breathing_stability = 0.15;
weights.gamma_gain = 0.25;
weights.overstimulation = 0.15;
Self {
id: "post-stroke-cognition",
display_name: "Post-Stroke Cognition (recovery state tracking)",
evidence_level: EvidenceLevel::EarlyHuman,
claim: "personalized entrainment optimization with recovery-state monitoring",
envelope: SafetyEnvelope::conservative()
.with_caps(0.32, 0.32)
.and_then(|e| e.with_max_duration_minutes(12.0))
.expect("post-stroke envelope is within the absolute bounds"),
prior,
weights,
eligible_states: &[SleepState::QuietWake, SleepState::Drowsy],
time_preference: TimePreference::Morning,
}
}
/// Sleep optimization — time stimulation to sleep state. Permits `Drowsy`/
/// `Asleep`, lowest intensity caps (must not degrade sleep), weights calm
/// physiology over raw gamma.
pub fn sleep_optimization() -> Self {
let mut prior = StimulusParameters::prior();
prior.modality = Modality::Audio; // light flicker is disruptive at sleep
prior.brightness_level = 0.0;
prior.volume_level = 0.18;
prior.duty_cycle = DutyCycle::Ramped;
prior.duration_minutes = 15.0;
let mut weights = ObjectiveWeights::default();
weights.gamma_gain = 0.20;
weights.phase_locking = 0.20;
weights.breathing_stability = 0.25; // calm sleep physiology is the point
weights.comfort = 0.15;
weights.overstimulation = 0.20;
Self {
id: "sleep-optimization",
display_name: "Sleep Optimization (state-timed gamma)",
evidence_level: EvidenceLevel::EarlyHuman,
claim: "sleep-state-timed entrainment optimization",
envelope: SafetyEnvelope::conservative()
.with_caps(0.10, 0.25) // near-dark
.and_then(|e| e.with_max_duration_minutes(30.0))
.expect("sleep envelope is within the absolute bounds"),
prior,
weights,
eligible_states: &[
SleepState::Drowsy,
SleepState::Asleep,
SleepState::QuietWake,
],
time_preference: TimePreference::PreSleepOrSleep,
}
}
/// Attention & working memory — personal frequency discovery. Evidence is
/// mixed/protocol-dependent, so this program leans hardest on *entrainment*
/// terms (find the individual's responsive frequency) under wakefulness.
pub fn attention_working_memory() -> Self {
let mut weights = ObjectiveWeights::default();
weights.gamma_gain = 0.35;
weights.phase_locking = 0.30;
weights.comfort = 0.10;
Self {
id: "attention-working-memory",
display_name: "Attention & Working Memory (personal frequency discovery)",
evidence_level: EvidenceLevel::Mixed,
claim: "personalized frequency-response discovery",
envelope: SafetyEnvelope::conservative(),
prior: StimulusParameters::prior(),
weights,
eligible_states: &[SleepState::QuietWake, SleepState::Active],
time_preference: TimePreference::QuietWake,
}
}
/// Mood & arousal regulation — avoid overstimulation, tune the calming
/// response. Lowest gamma weight, highest comfort + overstimulation penalty.
pub fn mood_arousal() -> Self {
let mut prior = StimulusParameters::prior();
prior.brightness_level = 0.22;
prior.volume_level = 0.22;
prior.duty_cycle = DutyCycle::Ramped;
let mut weights = ObjectiveWeights::default();
weights.gamma_gain = 0.20;
weights.phase_locking = 0.15;
weights.comfort = 0.25;
weights.breathing_stability = 0.20;
weights.overstimulation = 0.20;
Self {
id: "mood-arousal",
display_name: "Mood & Arousal Regulation (calming-response tuning)",
evidence_level: EvidenceLevel::EarlyHuman,
claim: "personalized calming-response optimization",
envelope: SafetyEnvelope::conservative()
.with_caps(0.30, 0.30)
.expect("mood-arousal envelope is within the absolute bounds"),
prior,
weights,
eligible_states: &[SleepState::QuietWake, SleepState::Drowsy],
time_preference: TimePreference::Evening,
}
}
/// Home neuro-wellness — safe personalization without treatment claims. The
/// most conservative envelope and the shortest sessions; speculative
/// evidence, so the claim is explicitly wellness-only.
pub fn home_wellness() -> Self {
let mut prior = StimulusParameters::prior();
prior.brightness_level = 0.20;
prior.volume_level = 0.20;
prior.duration_minutes = 6.0;
Self {
id: "home-wellness",
display_name: "Home Neuro-Wellness (no treatment claim)",
evidence_level: EvidenceLevel::Speculative,
claim: "personal neural-rhythm wellness optimization",
envelope: SafetyEnvelope::conservative()
.with_caps(0.28, 0.28)
.and_then(|e| e.with_max_duration_minutes(10.0))
.expect("home-wellness envelope is within the absolute bounds"),
prior,
weights: ObjectiveWeights::default(),
eligible_states: &[SleepState::QuietWake],
time_preference: TimePreference::Any,
}
}
/// Drug-plus-device trial infrastructure — the strongest near-term use. The
/// value is the *governed measurement layer* (RuView state + adherence,
/// RuVector response curve, RuFlo protocol/safety/consent/sham log), so the
/// claim is about a biomarker-correlated protocol layer, not therapy.
pub fn trial_infrastructure() -> Self {
Self {
id: "trial-infrastructure",
display_name: "Drug+Device Trial Infrastructure (governed protocol layer)",
evidence_level: EvidenceLevel::StrongInfrastructure,
claim: "governed, reproducible entrainment-protocol measurement",
envelope: SafetyEnvelope::conservative(),
prior: StimulusParameters::prior(),
weights: ObjectiveWeights::default(),
eligible_states: &[SleepState::QuietWake, SleepState::Drowsy],
time_preference: TimePreference::Any,
}
}
/// Every built-in program — for catalog UIs and the acceptance test matrix.
pub fn catalog() -> Vec<NeuroProgram> {
vec![
Self::alzheimers_research(),
Self::post_stroke_cognition(),
Self::sleep_optimization(),
Self::attention_working_memory(),
Self::mood_arousal(),
Self::home_wellness(),
Self::trial_infrastructure(),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_program_prior_is_inside_its_envelope() {
for p in NeuroProgram::catalog() {
assert!(
p.envelope.contains(&p.prior),
"program {} prior must be inside its envelope",
p.id
);
}
}
#[test]
fn no_program_claim_is_a_disease_treatment_claim() {
let banned = [
"treat",
"cure",
"alzheimer",
"stroke recovery cure",
"therapy for",
];
for p in NeuroProgram::catalog() {
let claim = p.claim.to_lowercase();
for b in banned {
assert!(
!claim.contains(b),
"program {} claim '{}' contains banned term '{}'",
p.id,
p.claim,
b
);
}
}
}
#[test]
fn objective_weights_are_well_formed() {
for p in NeuroProgram::catalog() {
let w = &p.weights;
for v in [
w.gamma_gain,
w.phase_locking,
w.breathing_stability,
w.adherence,
w.comfort,
w.motion_artifact,
w.adverse_event_risk,
w.overstimulation,
] {
assert!((0.0..=1.0).contains(&v));
}
}
}
#[test]
fn sleep_program_permits_asleep_others_do_not() {
assert!(NeuroProgram::sleep_optimization().state_eligible(SleepState::Asleep));
assert!(!NeuroProgram::attention_working_memory().state_eligible(SleepState::Asleep));
assert!(!NeuroProgram::alzheimers_research().state_eligible(SleepState::Asleep));
}
#[test]
fn sleep_program_caps_brightness_near_dark() {
assert!(NeuroProgram::sleep_optimization().envelope.brightness_cap() <= 0.10);
}
#[test]
fn program_ids_are_unique() {
let cat = NeuroProgram::catalog();
let mut ids: Vec<&str> = cat.iter().map(|p| p.id).collect();
ids.sort_unstable();
let n = ids.len();
ids.dedup();
assert_eq!(ids.len(), n);
}
#[test]
fn alzheimers_program_matches_adr250_defaults() {
// The default-enroll path must be unchanged (witness stability).
let p = NeuroProgram::alzheimers_research();
assert_eq!(p.envelope, SafetyEnvelope::conservative());
assert_eq!(p.prior, StimulusParameters::prior());
}
}