feat(ruview-gamma): centralize claim-gate invariant + add HIL acceptance contract

Two of the "what to optimize next" items, as enforced code:

1. Hard claim gate: extract the release rule into a single
   acceptance::claim_allowed(entrainment, safety, adherence, repeatability)
   = strict AND of all four, used by every path. A test proves every 3-of-4
   subset is denied, so no path can silently weaken the gate to an OR/subset.

2. Hardware-in-the-loop contract (new hil module): verify_hil grades a
   captured actuator bench measurement against fixed targets — LED frequency
   +/-0.1 Hz, audio-visual sync < 5 ms, stop-signal -> actuator-off < 100 ms,
   session-hash reproducibility 100%, EEG entrainment lift >= 20% over fixed
   40 Hz. Every failure mode fails closed (missing/NaN stop measurement, no
   replay, any hash mismatch). The crate stays a deterministic leaf: firmware
   records the measurements, this module grades them.

README: benchmark table (safety tick ~8 ns, recommend ~15 us, cohort kNN/500
~15 us, calibration ~115 us, acceptance grading ~425 us enrollment-only) and
the positioning line — "a governed personalization engine that refuses to
overpromise."

9 new tests; crate now 97 + 1 doctest; pinned witness 13cb164c... preserved.
Workspace gate: 2,898 passed / 0 failed.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
This commit is contained in:
Claude
2026-06-10 04:31:28 +00:00
parent d55e3659be
commit 46b4d63dad
5 changed files with 309 additions and 3 deletions
+1
View File
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- **`ruview-gamma` claim-gate invariant + hardware-in-the-loop contract.** Centralized the claim release rule into a single `acceptance::claim_allowed(entrainment, safety, adherence, repeatability)` (strict AND of all four) used by every path, with a test proving every 3-of-4 subset is denied — no path can weaken the gate. New `hil` module: `verify_hil` grades a captured actuator bench measurement against fixed targets (LED frequency ±0.1 Hz, audio-visual sync < 5 ms, stop-signal→actuator-off < 100 ms, session-hash reproducibility 100%, EEG entrainment lift ≥ 20% over fixed 40 Hz) — the next acceptance milestone for a real LED+speaker (e.g. ESP32) actuator; all failure modes fail closed (missing stop measurement, no replay, any hash mismatch). README gains the benchmark table and the "governed personalization engine that refuses to overpromise" positioning. 9 new tests; crate now 97 + 1 doctest; pinned witness preserved.
- **`ruview-gamma` generalized to an adaptive sensory neuromodulation platform (ADR-250 §23).** 40 Hz is now one prior in one program, not the product. New `program` module: `NeuroProgram` catalog of 7 use cases (Alzheimer's research, post-stroke cognition, sleep optimization, attention/working-memory, mood/arousal, home wellness, drug+device trial infrastructure), each with its own `SafetyEnvelope`, starting prior, `ObjectiveWeights`, physiological-state gating (sleep permits `Asleep` + near-dark brightness cap; attention requires wakefulness), `EvidenceLevel`, and a single non-disease claim. New `acceptance` module makes the acceptance sentence executable: `AcceptanceHarness` grades a program over ≥3 repeats on entrainment gain, safety-stop rate, adherence, and optimal-frequency repeatability, exposing a `ClaimGate` that returns the program's claim **only if all four criteria pass** — the marketing claim is otherwise unreadable (`NO_CLAIM`). Governor wiring: `enroll_program` (per-program envelope/objective; `enroll` stays the bare Alzheimer's-defaults path so the pinned witness `13cb164c…` is preserved), `program()`, `prior()`, `state_eligible()`. 13 new module tests + 2 platform integration tests (per-program envelope enforced end-to-end — a stimulus valid for Alzheimer's is refused by the sleep program; acceptance gates every catalog program's claim); crate now 88 tests + 1 doctest. Bench: full 3-repeat program grading ~425 µs.
- **`ruview-gamma` RuVector self-learning layer (ADR-250 §10 items 36).** New `ruvector` module: anonymized `ProfileStore` (one-way SHA-256 hashed tags, never `person_id`; safe-session scores only), deterministic exact kNN (fixed-range normalization, index tie-break), **cohort warm-start** — a new person's optimizer is seeded from the k nearest responders as down-weighted GP pseudo-observations (`BayesianOptimizer::observe_prior`, ≥25× real-observation noise, excluded from the EI incumbent / audit / clinician report), **physiological drift detection** (Welford centroid with stimulus-input fields masked out of the distance; `Drifted` recommends re-calibration), and deterministic k-means response clustering (farthest-point seeding, no RNG). Wired into `RufloGovernor` (`seed_from_cohort`, `export_anonymized_profile`, per-session `drift_status`). The GP gains per-observation noise (real path unchanged — pinned witness `13cb164c…` preserved). 11 new module tests + 2 integration tests (cohort warm-start beats the cold 40 Hz prior for a detuned subject; collapsed physiology flags drift); crate now 75 tests + 1 doctest. Benches: kNN over 500 profiles ~15 µs, full warm-start ~16 µs; no regression on existing paths.
- **`ruview-gamma` crate (ADR-250) — Adaptive Gamma Entrainment.** Governed, deterministic, safety-constrained personalization of 40 Hz-prior light+sound stimulation, treating 40 Hz as the evidence-based *starting prior* and learning each person's safe entrainment response curve. Eleven modules: `stimulus` (params + `SafetyEnvelope` validate/clamp), `safety` (exclusion screen + latched `SafetyMonitor` with hard-stop reasons), `response` (`RuViewState`, optional `EegMeasurement`, 20-field `PersonResponseVector` with sticky adverse flag), `objective` (safe-entrainment score; safety is a hard gate, not a weight), `simulator` (deterministic ChaCha20 `frequency_response_curve`), `optimizer` (Phase-1 calibration sweep + Phase-2 GP/Expected-Improvement + Phase-4 closed-loop control), `bandit` (Phase-3 LinUCB over envelope-safe arms), `session` (reproducible SHA-256 `session_hash`), `ruflo` (consent→exclusion→envelope→run→monitor→score→update→witnessed audit, trial/sham mode, clinician export, claim discipline), `proof` (deterministic bundle witness), `math` (dependency-light numerics). **Safety invariant** (asserted in tests): no recommendation, calibration step, bandit arm, or closed-loop nudge can ever emit a stimulus outside the `SafetyEnvelope`; non-finite inputs clamp to the conservative floor. **Claim discipline**: the only product claim is `PRODUCT_CLAIM` = "personalized entrainment optimization" — never Alzheimer's treatment (ADR-250 §19). Standalone leaf crate (no internal RuView deps), `publish = false` pending safety sign-off. 64 unit/integration tests + 1 doctest pass; deterministic witness pinned (`13cb164c…`); criterion benches (safety-stop tick ~9.3 ns vs the ADR §17 500 ms bound, Bayesian recommend ~105 µs, full 9-session governed sweep ~486 µs). See [ADR-250](docs/adr/ADR-250-adaptive-gamma-entrainment.md).
+64 -2
View File
@@ -1,4 +1,65 @@
# ruview-gamma — Adaptive Gamma Entrainment (ADR-250)
# ruview-gamma — Adaptive Sensory Neuromodulation (ADR-250)
> **The most valuable thing here is not 40 Hz. It is a governed personalization
> engine that refuses to overpromise.**
The control brain for an adaptive light-and-sound neuromodulation device. The
device plays stimulation; **RuView** reads the body as feedback; **RuVector**
learns the personal response map; **RuFlo** governs the safety, audit trail, and
claim boundary. The breakthrough is not speed alone — it is **safe adaptive
personalization with proof discipline**.
It starts from 40 Hz as the research prior, then learns whether a person
responds better at 38.5, 40, 41.2, or another safe setting — watching breathing,
stillness, restlessness, adherence, and sensor confidence. If something goes
wrong, the session locks. If a program has not proven entrainment, safety,
adherence, and repeatability, it cannot advertise a benefit — it returns
*research use only*.
## Benchmarks (this container — indicative)
| Path | Current | Role |
|------|---------|------|
| Safety tick | ~8 ns | real-time stop path |
| Recommendation | ~15 µs | per-session decision |
| Cohort kNN (500 profiles) | ~15 µs | warm-start matching |
| Calibration sweep | ~115 µs | setup and tuning |
| Full acceptance grading | ~425 µs | enrollment-only (offline) |
The per-session control loop is microseconds; the heavier acceptance grading is
enrollment-time work, not on the loop. No regression across the optimization
passes.
## The hard claim gate
A program's benefit claim is releasable through exactly one invariant
(`acceptance::claim_allowed`), used everywhere:
```text
claim_allowed = entrainment_pass AND safety_pass
AND adherence_pass AND repeatability_pass
```
Anything short of all four returns `research use only — … no claim`
(`acceptance::NO_CLAIM`). The marketing claim is unreadable except through the
gate.
## Next milestone — hardware in the loop (`hil`)
The software core is proven against a deterministic simulator; the next
acceptance bar is a real LED + speaker actuator (e.g. ESP32-driven) plus the
stop path. `hil::verify_hil` grades a captured bench measurement against fixed
targets:
| Test | Target |
|------|--------|
| LED frequency accuracy | ±0.1 Hz |
| Audio-visual sync drift | < 5 ms |
| Stop signal → actuator off | < 100 ms |
| Session-hash reproducibility | 100% |
| EEG entrainment lift vs fixed 40 Hz | ≥ 20% |
---
Governed, deterministic, **safety-constrained** personalization of 40 Hz-prior
multisensory (light + sound) stimulation. Treats 40 Hz as the evidence-based
@@ -44,7 +105,8 @@ conservative floor, never the cap.
| `bandit` | §8 P3 | LinUCB contextual bandit over envelope-safe arms |
| `ruvector` | §10 items 36 | anonymized `ProfileStore` (one-way hashed tags), deterministic kNN, cohort warm-start priors (down-weighted pseudo-observations), `DriftDetector` over the physiological sub-vector, deterministic k-means clustering |
| `program` | §23 | `NeuroProgram` catalog (7 use cases) — per-program envelope, prior, objective, state-gating, evidence level, and gated claim |
| `acceptance` | §18/§23.1 | `AcceptanceHarness` + `ClaimGate` — entrainment/safety/adherence/repeatability gate; a program's claim is unreadable until all four pass |
| `acceptance` | §18/§23.1 | `AcceptanceHarness` + `ClaimGate` + the `claim_allowed` invariant — entrainment/safety/adherence/repeatability gate; a program's claim is unreadable until all four pass |
| `hil` | §17/§21 M2 | hardware-in-the-loop contract: `verify_hil` grades a captured actuator measurement (LED ±0.1 Hz, A/V sync < 5 ms, stop < 100 ms, hash 100%, EEG lift ≥ 20%) |
| `session` | §11, §13 | hashable `SessionRecord`, reproducible `session_hash` (SHA-256, quantized canonical form) |
| `ruflo` | §11 | consent → exclusion → envelope → run → monitor → score → update → witnessed audit; trial/sham mode; clinician export; claim discipline |
| `proof` | — | deterministic bundle witness (mirrors `nvsim` / `verify.py`) |
+40 -1
View File
@@ -20,6 +20,26 @@ use crate::stimulus::StimulusParameters;
/// claim, no disease claim — only a statement that evidence is insufficient.
pub const NO_CLAIM: &str = "research use only — acceptance criteria not yet met; no claim";
/// **The hard claim-gate invariant** (ADR-250 §23.1). The single source of
/// truth used everywhere a claim could be released:
///
/// ```text
/// claim_allowed = entrainment_pass AND safety_pass
/// AND adherence_pass AND repeatability_pass
/// ```
///
/// Anything short of all four returns the research-only string. Centralizing it
/// here means no path can accidentally weaken the gate to an OR or a subset.
#[inline]
pub fn claim_allowed(
entrainment_pass: bool,
safety_pass: bool,
adherence_pass: bool,
repeatability_pass: bool,
) -> bool {
entrainment_pass && safety_pass && adherence_pass && repeatability_pass
}
/// Thresholds a program must clear (ADR-250 §18 generalized). Defaults mirror
/// the ADR's published targets; programs may tighten them.
#[derive(Debug, Clone, Copy)]
@@ -183,7 +203,8 @@ impl AcceptanceHarness {
let safety_pass = safety_stop_rate <= c.max_safety_stop_rate;
let adherence_pass = mean_adherence >= c.min_adherence;
let repeatability_pass = repeatability_band_hz <= c.max_repeatability_band_hz;
let overall_pass = entrainment_pass && safety_pass && adherence_pass && repeatability_pass;
let overall_pass =
claim_allowed(entrainment_pass, safety_pass, adherence_pass, repeatability_pass);
AcceptanceReport {
program_id: program.id.to_string(),
@@ -243,6 +264,24 @@ mod tests {
use super::*;
use crate::response::{RuViewState, SleepState};
#[test]
fn claim_allowed_requires_all_four_and_rejects_every_subset() {
// All four → allowed.
assert!(claim_allowed(true, true, true, true));
// Every 3-of-4 subset (one false) → denied. This is the AND, not OR,
// guarantee the whole gate rests on.
let one_false = [
(false, true, true, true),
(true, false, true, true),
(true, true, false, true),
(true, true, true, false),
];
for (e, s, a, r) in one_false {
assert!(!claim_allowed(e, s, a, r), "subset {e}{s}{a}{r} must be denied");
}
assert!(!claim_allowed(false, false, false, false));
}
fn detuned_subject() -> (String, LatentPerson) {
// A subject whose latent peak is clearly off the prior frequency, so an
// adaptive program has real gain to find.
+203
View File
@@ -0,0 +1,203 @@
//! Hardware-in-the-loop (HIL) acceptance contract — Milestone "device harness"
//! (ADR-250 §17, §21 M2).
//!
//! The software core is proven against the deterministic simulator; the next
//! acceptance milestone is **real hardware**: an LED + speaker actuator (e.g.
//! driven by an ESP32) plus the safety stop path. This module defines the
//! *contract* every actuator integration must satisfy and a verifier that
//! grades a captured [`HilMeasurement`] against fixed targets. It does **not**
//! talk to hardware — the firmware/driver records the measurements and submits
//! them here, keeping this crate a deterministic, dependency-light leaf.
//!
//! | Test | Target |
//! |------|--------|
//! | LED frequency accuracy | ±0.1 Hz |
//! | Audio-visual sync drift | < 5 ms |
//! | Stop signal → actuator off | < 100 ms |
//! | Session-hash reproducibility | 100% |
//! | EEG entrainment lift vs fixed 40 Hz | ≥ 20% |
use serde::{Deserialize, Serialize};
/// Fixed HIL targets (ADR-250 §17 acceptance + §18). Constants, not config:
/// these are the bar a device must clear to be called validated.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct HilTargets {
/// Max |measured commanded| LED/flicker frequency, Hz.
pub max_frequency_error_hz: f64,
/// Max audio-visual onset skew, milliseconds.
pub max_av_sync_drift_ms: f64,
/// Max latency from stop assertion to actuator-off, milliseconds.
pub max_stop_latency_ms: f64,
/// Min fraction of replayed sessions whose witness hash reproduced.
pub min_hash_reproducibility: f64,
/// Min EEG entrainment lift over fixed 40 Hz, as a fraction.
pub min_eeg_lift: f64,
}
impl Default for HilTargets {
fn default() -> Self {
Self {
max_frequency_error_hz: 0.1,
max_av_sync_drift_ms: 5.0,
max_stop_latency_ms: 100.0,
min_hash_reproducibility: 1.0,
min_eeg_lift: 0.20,
}
}
}
/// A captured bench measurement from a real actuator run. Populated by the
/// firmware/driver test harness (e.g. measuring LED frequency with a photodiode
/// and a logic analyzer, sync with a dual-channel capture, stop latency from
/// GPIO assert to PWM-off).
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct HilMeasurement {
/// Commanded frequency the controller asked for (Hz).
pub commanded_frequency_hz: f64,
/// Frequency actually measured at the LED (Hz).
pub measured_frequency_hz: f64,
/// Measured audio-visual onset skew (ms).
pub av_sync_drift_ms: f64,
/// Measured stop-assert → actuator-off latency (ms).
pub stop_latency_ms: f64,
/// Replayed sessions whose witness hash matched / total replayed.
pub hashes_reproduced: u32,
pub hashes_total: u32,
/// Mean EEG entrainment under the adaptive protocol.
pub eeg_entrainment_adaptive: f64,
/// Mean EEG entrainment under fixed 40 Hz (the control arm).
pub eeg_entrainment_fixed_40hz: f64,
}
/// Per-criterion verdict for a HIL run.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct HilReport {
pub frequency_error_hz: f64,
pub frequency_pass: bool,
pub av_sync_pass: bool,
pub stop_latency_pass: bool,
pub hash_reproducibility: f64,
pub hash_pass: bool,
pub eeg_lift: f64,
pub eeg_lift_pass: bool,
/// True only if every HIL criterion passes — the device is bench-validated.
pub overall_pass: bool,
}
/// Grade a [`HilMeasurement`] against [`HilTargets`].
pub fn verify_hil(m: &HilMeasurement, t: &HilTargets) -> HilReport {
let frequency_error_hz = (m.measured_frequency_hz - m.commanded_frequency_hz).abs();
let frequency_pass = frequency_error_hz <= t.max_frequency_error_hz;
let av_sync_pass = m.av_sync_drift_ms.abs() <= t.max_av_sync_drift_ms;
// Stop latency must be finite and within bound (a missing/NaN measurement
// fails closed).
let stop_latency_pass =
m.stop_latency_ms.is_finite() && m.stop_latency_ms <= t.max_stop_latency_ms;
let hash_reproducibility = if m.hashes_total > 0 {
m.hashes_reproduced as f64 / m.hashes_total as f64
} else {
0.0 // nothing replayed ⇒ unproven ⇒ fail closed
};
let hash_pass = hash_reproducibility >= t.min_hash_reproducibility;
let baseline = m.eeg_entrainment_fixed_40hz.max(1e-6);
let eeg_lift = (m.eeg_entrainment_adaptive - baseline) / baseline;
let eeg_lift_pass = eeg_lift >= t.min_eeg_lift;
let overall_pass =
frequency_pass && av_sync_pass && stop_latency_pass && hash_pass && eeg_lift_pass;
HilReport {
frequency_error_hz,
frequency_pass,
av_sync_pass,
stop_latency_pass,
hash_reproducibility,
hash_pass,
eeg_lift,
eeg_lift_pass,
overall_pass,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn passing() -> HilMeasurement {
HilMeasurement {
commanded_frequency_hz: 40.0,
measured_frequency_hz: 40.05, // within ±0.1 Hz
av_sync_drift_ms: 2.0, // < 5 ms
stop_latency_ms: 40.0, // < 100 ms
hashes_reproduced: 100,
hashes_total: 100, // 100%
eeg_entrainment_adaptive: 0.36,
eeg_entrainment_fixed_40hz: 0.30, // +20%
}
}
#[test]
fn a_good_bench_run_passes_all_criteria() {
let r = verify_hil(&passing(), &HilTargets::default());
assert!(r.overall_pass);
assert!(r.frequency_error_hz <= 0.1);
assert!((r.eeg_lift - 0.20).abs() < 1e-9);
}
#[test]
fn frequency_drift_beyond_tenth_hz_fails() {
let mut m = passing();
m.measured_frequency_hz = 40.3; // 0.3 Hz error
let r = verify_hil(&m, &HilTargets::default());
assert!(!r.frequency_pass);
assert!(!r.overall_pass);
}
#[test]
fn slow_stop_fails() {
let mut m = passing();
m.stop_latency_ms = 250.0;
assert!(!verify_hil(&m, &HilTargets::default()).stop_latency_pass);
}
#[test]
fn missing_stop_measurement_fails_closed() {
let mut m = passing();
m.stop_latency_ms = f64::NAN;
assert!(!verify_hil(&m, &HilTargets::default()).stop_latency_pass);
}
#[test]
fn any_hash_mismatch_fails_reproducibility() {
let mut m = passing();
m.hashes_reproduced = 99; // one of 100 drifted
let r = verify_hil(&m, &HilTargets::default());
assert!(!r.hash_pass);
assert!(!r.overall_pass);
}
#[test]
fn no_replay_fails_closed() {
let mut m = passing();
m.hashes_reproduced = 0;
m.hashes_total = 0;
assert!(!verify_hil(&m, &HilTargets::default()).hash_pass);
}
#[test]
fn insufficient_eeg_lift_fails() {
let mut m = passing();
m.eeg_entrainment_adaptive = 0.32; // only +6.7%
let r = verify_hil(&m, &HilTargets::default());
assert!(!r.eeg_lift_pass);
assert!(!r.overall_pass);
}
#[test]
fn sync_drift_beyond_5ms_fails() {
let mut m = passing();
m.av_sync_drift_ms = 7.5;
assert!(!verify_hil(&m, &HilTargets::default()).av_sync_pass);
}
}
+1
View File
@@ -70,6 +70,7 @@
pub mod acceptance;
pub mod bandit;
pub mod hil;
pub mod math;
pub mod objective;
pub mod optimizer;