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
@@ -0,0 +1,91 @@
# ADR-251: Clinical Dashboard + Persistent RuVector Store for Adaptive Gamma
| Field | Value |
|-------|-------|
| **Status** | Proposed |
| **Date** | 2026-06-11 |
| **Owner** | RuView, RuVector, RuFlo clinical systems |
| **Decision type** | Architecture, clinical tooling |
| **Relates to** | ADR-250 (Adaptive Gamma Entrainment — platform, programs, acceptance gate) |
| **Codebase target** | `v2/crates/ruview-gamma-clinic` (this ADR's implementation) |
> **Not a medical device.** Read-only research/clinical *instrumentation* over
> the ADR-250 platform. It surfaces only gated claims and witnessed records; it
> can neither start stimulation nor widen any safety envelope.
## 1. Context
ADR-250 shipped the governed adaptive-neuromodulation engine: per-program
safety envelopes, the RuVector self-learning layer (anonymized profiles, kNN
warm-start, drift detection, clustering), witnessed session records, and the
executable acceptance gate. Two operational gaps remain for clinical use:
1. **No durable store.** `ProfileStore` and the governor's audit log are
in-memory; a clinic restart loses cohort memory, and there is no
tamper-evident persistence matching the platform's proof discipline.
2. **No clinician surface.** RuFlo's clinician export exists as a struct
(`ClinicianReport`), but there is no way for a clinician/trial monitor to
*see* a participant's frequency-response curve, session trend, safety
events, drift status, or a program's acceptance verdict.
## 2. Decision
Build `ruview-gamma-clinic`, a separate crate (keeping `ruview-gamma` a
dependency-light deterministic leaf) with two components:
### 2.1 Persistent RuVector store (`store.rs`)
Append-only JSON-lines file per clinic, holding three record kinds —
anonymized profiles, witnessed session summaries, and acceptance reports —
each line **hash-chained** (`entry_hash = SHA-256(prev_hash ‖ canonical_json)`)
so any retroactive edit breaks the chain (`verify_chain()`); the RuVector
in-memory layer (`ProfileStore` kNN, clustering) is rebuilt from the file at
open. Pseudonymity is preserved: records carry only the one-way profile tags
from ADR-250 §10.
### 2.2 Read-only clinical dashboard (`server.rs` + embedded `dashboard.html`)
Axum surface, **strictly read-only** (no POST mutates stimulation state):
| Route | Payload |
|-------|---------|
| `GET /` | embedded single-file HTML dashboard (no build step; SVG charts) |
| `GET /api/clinic/participants` | tag, session count, mean entrainment, safety stops, adverse flag, drift status |
| `GET /api/clinic/participants/{tag}` | response vector, frequency→score curve, session trend |
| `GET /api/clinic/cohort` | deterministic k-means clusters over the stored profiles |
| `GET /api/clinic/acceptance` | per-program acceptance reports with the **gated** claim |
| `GET /api/clinic/integrity` | hash-chain verification result + record count |
**Claim discipline is inherited, not re-implemented:** acceptance payloads
embed `AcceptanceReport::released_claim` (the gate's output), never the
program's raw claim. The dashboard renders what the gate released — nothing
stronger.
### 2.3 Visualization (embedded, dependency-free)
One static HTML file (`include_str!`) with vanilla JS + inline SVG:
participant list → per-participant frequency-response curve (the personal
response map), entrainment/comfort session trend, safety-event markers, cohort
cluster table, and an integrity badge (green only when `verify_chain` passes).
No JS framework, no CDN, no build step — auditable by reading one file.
## 3. Consequences
- Clinic restarts no longer lose cohort memory; warm-start works across runs.
- Tampering with stored records is detectable by one endpoint call.
- A clinician can inspect a trial without shell access; the surface cannot
actuate anything.
- The store is JSONL, not a database server: at research-cohort scale this is
deliberate (greppable, diffable, witness-friendly). An HNSW/ruvector-crate
backend remains the drop-in path past ~10⁵ profiles (ADR-250 §10).
## 4. Acceptance criteria (tested)
| Criterion | Test |
|-----------|------|
| Store round-trips all three record kinds across reopen | `store::tests` |
| Hash chain detects any line edit/deletion/reorder | `tampered_chain_is_detected` |
| kNN warm-start works from a reloaded store | `knn_survives_reload` |
| Every API route serves and is read-only | `server::tests` (oneshot) |
| Acceptance payload carries only the gated claim | `acceptance_payload_uses_gated_claim` |
| Dashboard HTML embeds and serves | `dashboard_html_served` |