feat(clinic): ADR-251 clinical dashboard + hash-chained RuVector store

Read-only research/clinical instrumentation over the ADR-250 adaptive-gamma
platform, closing two operational gaps: no durable cohort memory, no
clinician surface.

Store (store.rs): append-only JSONL holding anonymized profiles, witnessed
session summaries, and acceptance verdicts. Every line is hash-chained
(entry_hash = SHA-256(prev_hash || raw record bytes)), so any retroactive
edit, deletion, or reorder breaks the chain — the store fails closed (refuses
to open tampered data) and rebuilds the RuVector kNN/clustering layer on open
so cohort warm-start survives restarts. The chain hashes the exact on-disk
bytes via serde_json RawValue, because serde_json's default float parse is
lossy and re-serialization is not byte-stable (this bit the first cut: a
9-session ingest self-corrupted on reopen).

Dashboard (server.rs + embedded dependency-free dashboard.html): Axum surface
with GET routes for participants, per-participant frequency-response map +
session trend with safety-stop markers, cohort clusters, per-program
acceptance verdicts, and a live chain-integrity badge. Strictly read-only — a
test asserts no route accepts POST. Claim discipline inherited: acceptance
payloads carry AcceptanceReport::released_claim (NO_CLAIM on failure), never a
raw program claim.

gamma-clinic binary; ingest_governor bridges the live ADR-250 loop into the
store (pseudonymous, dedup by witness hash). Pseudonymity asserted: the
person_id never reaches disk.

20 tests (13 store/lib + 7 server) + 1 doctest; live binary smoke-tested.
Workspace gate: 2,935 passed / 0 failed.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
This commit is contained in:
Claude
2026-06-11 00:32:38 +00:00
parent b273fac719
commit 41d52311bd
11 changed files with 1209 additions and 1 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-clinic` crate (ADR-251) — clinical dashboard + persistent hash-chained RuVector store.** Read-only research/clinical instrumentation over the ADR-250 platform, closing two operational gaps (no durable cohort memory; no clinician surface). **Store** (`store.rs`): append-only JSONL holding profiles, witnessed session summaries, and acceptance verdicts, each line hash-chained `entry_hash = SHA-256(prev ‖ raw record bytes)` so any retroactive edit/deletion/reorder breaks the chain — the store *fails closed* (refuses to open tampered data) and rebuilds the RuVector kNN/clustering layer on open so warm-start survives restarts. (Hashes the exact on-disk bytes via `RawValue`, since serde_json's default float parse is lossy and re-serialization isn't byte-stable.) **Dashboard** (`server.rs` + embedded dependency-free `dashboard.html`): Axum surface with `GET` routes for participants, per-participant frequency-response map + session trend (safety-stop markers), cohort clusters, per-program acceptance verdicts, and a live chain-integrity badge — **strictly read-only** (a test asserts no route accepts POST). Claim discipline inherited: acceptance payloads carry `AcceptanceReport::released_claim` (the gate's output, `NO_CLAIM` on failure), never a raw program claim. `gamma-clinic` binary; `ingest_governor` bridges the live ADR-250 loop into the store (pseudonymous, dedup by witness hash). 20 tests (13 store/lib + 7 server) + 1 doctest; pseudonymity asserted (the `person_id` never reaches disk).
- **Opt-in FFT operator for the CIR ISTA solver (814× measured).** Φ is a sub-DFT, so each ISTA mat-vec can run as one length-G FFT (O(G log G)) instead of a dense O(K·G) product. New `CirConfig::fft_operator` (default **false** — the dense path stays the bit-exact witness default; the FFT evaluates the same sums in a different order, so enabling it shifts float results and requires regenerating any pinned witness). `FftOperator` (rustfft, planned once at construction, scratch reused across the ISTA loop) dispatches inside `ista_solve`; warm-start/Lipschitz stay dense at construction. Measured (criterion, same run): ht20 2.22 ms → 265 µs (**8.4×**), ht40 10.26 ms → 717 µs (**14.3×**); the real HE40 grid (K=484, G=1452) scales further. 3 new tests: FFT↔dense matvec equivalence to float tolerance (ht20 + he40 grids), end-to-end dominant-tap agreement on a single-path frame, and all default configs keep FFT off. New `cir_estimate_fft` bench group. Closes the trust-chain gap where an ~11 KB per-room LoRA adapter (ADR-150 §3.4) could silently change inference without the witness noticing. `StreamingEngine::set_room_adapter(AdapterInfo)` pins the adapter's content-derived id into provenance `model_version` (`rfenc-v1+adapter:<id>`) — and therefore into the BLAKE3 witness — so swapping or clearing adapter weights always shifts the witness (engine test proves base → adapter → other-adapter → cleared all witness differently, and cleared == base). New `RecalibrationAdvisor` recommends re-running the ADR-135 baseline / refitting the adapter on sustained low fusion coherence (streak threshold, default 60 cycles ≈ 3 s at 20 Hz) or an ADR-142 change-point; surfaced as `TrustedOutput::recalibration_recommended` and stored on the sensing-server `AppState` alongside the witness. Bridge plumbing: `EngineBridge::{set_room_adapter, clear_room_adapter}` + live-path test that the adapter id flows into the live witness. Engine 15 tests, bridge 7 tests. *Scope note: this is the deployable provenance/trigger half of the "retrained model" roadmap item — fitting the adapter itself runs in the existing external calibration service (`aether-arena/calibration/`), and a trained RF-encoder checkpoint still does not exist in-tree.*
- **`esp32-gamma-stim` firmware — ESP32 gamma stimulation actuator (ADR-250 §21 M2 device harness).** The hardware side of `ruview-gamma`: an ESP32 driving an LED + audio flicker at a commanded 3644 Hz envelope with a hardware emergency stop. Split into a **pure, host-tested safety core** (`main/stim_core.{h,c}` — envelope validation mirroring `SafetyEnvelope::conservative()`, a latched START/STOP/e-stop state machine, exact integer timing math in millihertz so the ±0.1 Hz HIL target is exact, and a line-protocol parser; **15 host tests pass under gcc, no ESP-IDF needed**) and a thin **ESP-IDF binding** (`main/main.c` — GPTimer ISR, LEDC PWM for LED+audio, sync-out GPIO for logic-analyzer capture, e-stop GPIO ISR that kills outputs in microseconds, USB-CDC console). Defense in depth: the device re-enforces the safety envelope independently of the Rust host, so a buggy/compromised host still cannot command an out-of-envelope output. Emits a canonical integer `SESSION {...}` record per run for witness-hash reproduction. Maps 1:1 to the five `hil::verify_hil` targets. Kconfig pin config, 4 MB single-app, radio-off deterministic actuator profile.
- **`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.