▶ Live demo: rule-shadow.kareemghazal.com
Pick a baseline ruleset and a proposed change, and watch the shadow engine compute exactly what the change would do to a synthetic cohort — before it ever touches a patient. (First run ~10–20s.)
How it works — input → shadow engine → gates → narrated report, with the eval harness that measures it:
Demo / educational — synthetic data only. NOT a medical device. Not for clinical use. Monitoring intervals are illustrative, paraphrased from public UK guidance (BNF / NICE CKS).
A small, safe demonstration of the core idea behind shipping a clinical rule change safely: run the proposed ruleset in the shadow of the live one — both versions over the same cohort — and measure the difference, the way a UKCA-regulated clinical AI has to handle change control. No A/B testing on patients; the proposal earns its deployment by surviving a shadow run, a golden regression cohort and volume-delta alarms.
The point isn't the LLM. It's the split:
- A deterministic engine COMPUTES. "What does this rule change actually do?" is answered by running v1 and v2 over the same 120 synthetic patients and diffing the results: per-test flagged-volume deltas, newly / no-longer flagged patients, golden-cohort regressions, and threshold alerts when any test's volume moves more than 25%. No model involved — testable, explainable, reproducible.
- The LLM only NARRATES. It's handed the computed diff and drafts the change-impact report: summary, risk callouts, ship/hold recommendation. It is told, and deterministically checked, to invent nothing — every number in the narrative must exist in the computed diff, or the grounding gate fails it.
- Everything is human-in-the-loop. The output is a draft impact report for a reviewer; the tool never approves its own change.
Why build it this way? Because the scariest rule-change bug is the silent one. One of the bundled proposals "just renames a trigger" — and CKD monitoring quietly stops matching anyone. No exception, no failing request, nothing in the logs. The only thing that catches it is measuring the two versions against the same cohort and alarming on the volume drop. That's the difference between vibing a change and measuring it.
baseline ruleset (v1) proposed ruleset (v2)
│ │
└──────────┬─────────────────┘
▼
┌────────────────────┐
│ shadow engine │ runs BOTH over the SAME
│ (deterministic) │ synthetic cohort (120 pts)
└────────────────────┘
│
▼
ShadowDiff — volumes, deltas, newly / no-longer
flagged, rule changes, >25% volume ALERTS
│ │
decides the numbers ▼
│ ┌────────────────────┐
│ │ LLM reporter │ drafts the impact report
│ │ (narrates only) │ from the diff — nothing else
│ └────────────────────┘
│ │
▼ ▼
golden-cohort regression number-grounding gate:
(hand-written expecteds) every figure in the text
must exist in the diff
pip install -e .
cp .env.example .env # add ANTHROPIC_API_KEY (only needed for the narrative step)
# engine only — deterministic, no API key:
rule-shadow rulesets
rule-shadow shadow --baseline v1_baseline --proposed v2_trigger_typo
# full run — computed diff + narrated impact report:
rule-shadow shadow --baseline v1_baseline --proposed v2_trigger_typo --narrative
rule-shadow demoThe two bundled proposals:
v2_interval_tightened— the intended change: ACE-inhibitor U&E monitoring 365d → 180d. The shadow run shows the workload cost (U&E flagged volume rises, alert fires) before anyone commits to it.v2_trigger_typo— the silent regression: the CKD trigger is "renamed" fromchronic kidney diseasetockd, which matches no patient record. CKD monitoring vanishes without an error anywhere; the −100% ACR volume alert is the tripwire. (This mirrors a real bug class: a matcher change that silently empties a cohort.)
The deterministic gates need no API key:
python evals/run_evals.py --engine-only # diff correctness / golden regression / alerts
python evals/run_evals.py # + narrative number-grounding (needs API key)
python evals/run_evals.py --judge # + an opus faithfulness judge- Diff correctness — exact per-test volumes, newly/no-longer flagged and alerts on the golden cohort match the hand-computed expectations, for both bundled rule-change pairs.
- Golden regression — ten hand-picked patients with hand-written expected findings per ruleset version; any engine drift from the expecteds fails the gate.
- Alert correctness — the typo pair MUST fire the volume-drop alert; the tightened pair MUST fire the volume-rise alert; no phantom alerts.
- Number-grounding — every integer/percentage in the narrated report exists in the computed diff (deterministic check; fabricated numbers fail the gate).
- Judge — opus confirms the narrative is faithful to the diff, flags the risk clearly, and gives no clinical advice.
Each run writes an evals/results/latest.json artifact (metrics, per-case results, models,
timestamp), so eval claims are reproducible instead of vibes.
Latest run (claude-sonnet-4-6 reporter, claude-opus-4-8 judge — full results in
evals/results/latest.json): all gates pass — golden-cohort
regression 0 mismatches on all three rulesets, diff and alert correctness exact, both
narratives fully number-grounded, and the judge rates both faithful, risk-flagging, free of
clinical advice, clarity 5/5. On the committed 120-patient cohort the two bundled changes land
exactly as designed: the interval tightening raises U&E flagged volume 14 → 26 (+85.7%) and
fires a volume-rise alert; the CKD trigger typo drops ACR flagged volume 8 → 0 (−100%) and
fires the "this monitoring has silently vanished" alert — the exact class of silent regression
this tool exists to catch (it reproduces a real bug found in a sibling tool, where the trigger
"ckd" stopped matching "Chronic kidney disease").
pytest -q # offline: engine + diff math + alerts + golden regression + grounding gate; no API key, no networkweb/ — a Next.js UI: pick baseline + proposed rulesets, run the shadow, and read the diff
table, alerts and the drafted impact report, with the safety banner on every screen.
Run it locally in two terminals:
# terminal 1 — the API
pip install -e .
cp .env.example .env # add ANTHROPIC_API_KEY
python -m uvicorn rule_shadow.api:app --port 8000
# terminal 2 — the UI
cd web
npm install
echo "NEXT_PUBLIC_API_URL=http://localhost:8000" > .env.local
npm run dev # open http://localhost:3000See DEPLOY.md.
- The cohort is synthetic and small. 120 seeded patients exercise the mechanics; they don't reproduce real-world prevalence, comorbidity structure or coding noise. A real shadow run uses the real (de-identified) population, at scale.
- Volume deltas are a tripwire, not a proof. A change can be wrong without moving any volume by 25%, and a legitimate change can move one by more. The alerts prioritise review; they don't replace it.
- Rule matching is simplified. Case-insensitive substring matching stands in for real terminology binding (SNOMED CT codes, dose forms, problem-list semantics) — which is exactly where the bundled typo bug class comes from in real systems.
- The grounding gate checks numbers, not meaning. A narrative could use only grounded figures and still frame them misleadingly; that's what the opus judge and the human reviewer are for.
- One cohort, one run. Real change control would also shadow over time (seasonality, new registrations) and gate on more than volumes (e.g. time-to-recall distributions).
This is a teaching demonstration built on made-up patients and simplified public guidance. It is not a medical device and must never be used for real clinical decisions or real change control.
MIT — see LICENSE.

