From 92ed42e5f171a062ab933b2a310c3a7771412356 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 12:57:29 +0200 Subject: [PATCH 01/20] Ignore local agent tooling .agents/ is editor-local configuration, the same class of file as .claude/ and .codex/, so it should not appear as untracked project work. The scenarios comment also misspelled the fixtures directory. --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b578921..8746ae1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,9 @@ env/ # Local Claude/Codex tooling (vendored skills/agents/config β€” not hackathon work) .claude/ .codex/ +.agents/ -# Generator default output (fixtures live in claude-science-artifcats/scenarios/ and ARE committed) +# Generator default output (fixtures live in claude-science-artifacts/scenarios/ and ARE committed) /predictions.csv # Real RSNA-SMBC data β€” the license bans redistribution "in any form". Never commit @@ -33,3 +34,5 @@ predictions_rsna*.csv # OS .DS_Store + +screenshots/ From 5f409a5e1582ab490475411dbe4238adc863979d Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 12:58:58 +0200 Subject: [PATCH 02/20] Validate predictions at the CLI and upload boundary An uploaded CSV is the one input the tool does not control. A missing column, a split with no train or test rows, a label outside {0, 1}, a one-class test set, or a probability that is not a number in [0, 1] used to surface as a KeyError or a scikit-learn traceback. validate_predictions reports those problems in plain language. The CLI prints them on stderr and exits 2, before any check runs. The browser UI shows the same list and stops. --- app.py | 22 +++--------- cli.py | 5 +++ clinval_validators/__init__.py | 2 ++ clinval_validators/checks.py | 49 +++++++++++++++++++++++--- tests/test_app.py | 18 +++------- tests/test_cli.py | 12 +++++++ tests/test_validators.py | 64 ++++++++++++++++++++++++++++++++++ 7 files changed, 137 insertions(+), 35 deletions(-) diff --git a/app.py b/app.py index fbfee48..2864c3d 100644 --- a/app.py +++ b/app.py @@ -18,26 +18,17 @@ import pandas as pd from cli import _assemble_payload +from clinval_validators import validate_predictions from reporter import API_KEY_ENV, render_report, render_reviewer_critique SCENARIOS_DIR = Path(__file__).resolve().parent / "claude-science-artifacts" / "scenarios" -# The columns every validator needs. Checked at the upload boundary: an uploaded -# file is the one input this tool does not control, so a missing column must -# produce a readable message rather than a raw KeyError traceback. -REQUIRED_COLUMNS = ("patient_id", "machine_id", "site_id", "y_true", "y_prob", "split") - def available_scenarios() -> dict[str, Path]: """Map each bundled scenario name to its predictions CSV (excludes the summary).""" return {p.stem: p for p in sorted(SCENARIOS_DIR.glob("*.csv")) if not p.name.startswith("_")} -def missing_columns(df: pd.DataFrame) -> list[str]: - """Return the required columns absent from ``df``; empty when the schema fits.""" - return sorted(set(REQUIRED_COLUMNS) - set(df.columns)) - - def main() -> None: import streamlit as st @@ -72,14 +63,9 @@ def main() -> None: ) generate = st.button("Generate with Claude", type="primary") - absent = missing_columns(df) - if absent: - st.error( - f"This CSV is missing required column(s): {', '.join(absent)}. " - f"A predictions table needs: {', '.join(REQUIRED_COLUMNS)}. " - "See the README for the expected schema.", - icon="🚫", - ) + problems = validate_predictions(df) + if problems: + st.error("\n\n".join(problems), icon="🚫") st.stop() payload = _assemble_payload(df) diff --git a/cli.py b/cli.py index af30f1b..4dd756b 100644 --- a/cli.py +++ b/cli.py @@ -24,6 +24,7 @@ check_patient_leakage, check_subgroup_degradation, compute_pooled_metric, + validate_predictions, ) @@ -66,6 +67,10 @@ def _assemble_payload(df: pd.DataFrame) -> ReportPayload: def main(argv: Sequence[str] | None = None) -> int: args = _parse_args(argv) df = _load_predictions(args.predictions) + problems = validate_predictions(df) + if problems: + print("\n".join(problems), file=sys.stderr) + return 2 payload = _assemble_payload(df) render = render_reviewer_critique if args.mode == "critique" else render_report try: diff --git a/clinval_validators/__init__.py b/clinval_validators/__init__.py index 72bf1ce..23cb94c 100644 --- a/clinval_validators/__init__.py +++ b/clinval_validators/__init__.py @@ -10,6 +10,7 @@ check_patient_leakage, check_subgroup_degradation, compute_pooled_metric, + validate_predictions, ) __all__ = [ @@ -22,4 +23,5 @@ "check_patient_leakage", "check_subgroup_degradation", "compute_pooled_metric", + "validate_predictions", ] diff --git a/clinval_validators/checks.py b/clinval_validators/checks.py index 1ab0f91..5eeb1e4 100644 --- a/clinval_validators/checks.py +++ b/clinval_validators/checks.py @@ -7,26 +7,28 @@ from __future__ import annotations -from typing import TYPE_CHECKING, TypedDict +from typing import TypedDict +import pandas as pd from sklearn.metrics import roc_auc_score from sklearn.model_selection import GroupShuffleSplit -if TYPE_CHECKING: - import pandas as pd - # Subgroup flag thresholds, calibrated against the labeled scenarios: M030 in # subgroup_scanner_degradation sits 0.21 below pooled and must flag, while every # subgroup in clean_patient_grouped is within 0.07 of pooled and must not. _FLAG_DELTA = -0.10 _MIN_SUBGROUP_N = 50 +# Columns the checks read by name. Anything else in the table is ignored. +_REQUIRED_COLUMNS = ("patient_id", "machine_id", "site_id", "y_true", "y_prob", "split") + __all__ = [ "LeakageResult", "SplitAuditResult", "SubgroupResult", "PooledMetric", "ReportPayload", + "validate_predictions", "check_patient_leakage", "audit_split_quality", "check_subgroup_degradation", @@ -76,6 +78,45 @@ class ReportPayload(TypedDict): # --- Checks --------------------------------------------------------------- +def validate_predictions(df: pd.DataFrame) -> list[str]: + """Return readable problems with a predictions table. + + An empty list means the deterministic checks can run. Called at the CLI + and upload boundary, which is the one input this tool does not control. + A missing column stops the remaining checks, because they read columns by + name. + """ + missing = [column for column in _REQUIRED_COLUMNS if column not in df.columns] + if missing: + needed = ", ".join(_REQUIRED_COLUMNS) + return [ + "Missing required column(s): " + + ", ".join(missing) + + f". A predictions table needs {needed}." + ] + + problems: list[str] = [] + if not (df["split"] == "train").any(): + problems.append('No rows with split == "train".') + if not (df["split"] == "test").any(): + problems.append('No rows with split == "test".') + + y_true = pd.to_numeric(df["y_true"], errors="coerce") + if y_true.isna().any() or not y_true.dropna().isin([0, 1]).all(): + problems.append("y_true has values outside {0, 1}.") + + test_y = y_true[df["split"] == "test"] + if len(test_y) and not ((test_y == 0).any() and (test_y == 1).any()): + problems.append("Test rows do not contain both classes (0 and 1).") + + y_prob = pd.to_numeric(df["y_prob"], errors="coerce") + if y_prob.isna().any(): + problems.append("y_prob is not numeric.") + if (y_prob.notna() & ((y_prob < 0) | (y_prob > 1))).any(): + problems.append("y_prob has values outside [0, 1].") + return problems + + def check_patient_leakage(df: pd.DataFrame) -> LeakageResult: """Detect ``patient_id`` overlap between the train and test splits. diff --git a/tests/test_app.py b/tests/test_app.py index 4898a3f..4983c68 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -3,13 +3,15 @@ The Streamlit UI itself is glue over already-tested functions (``_assemble_payload`` and the two reporters), so only the pure helper is unit-tested here. Importing ``app`` must not require Streamlit β€” it is imported lazily inside ``main``. +The upload boundary is ``validate_predictions``, tested in ``test_validators``. """ from __future__ import annotations import pandas as pd -from app import REQUIRED_COLUMNS, available_scenarios, missing_columns +from app import available_scenarios +from clinval_validators import validate_predictions def test_available_scenarios_lists_bundled_csvs() -> None: @@ -25,16 +27,6 @@ def test_available_scenarios_excludes_the_summary_file() -> None: assert not any(name.startswith("_") for name in available_scenarios()) -def test_bundled_scenario_satisfies_the_required_schema() -> None: +def test_bundled_scenario_passes_validation() -> None: df = pd.read_csv(available_scenarios()["clean_patient_grouped"]) - assert missing_columns(df) == [] - - -def test_missing_columns_names_every_absent_required_column() -> None: - df = pd.DataFrame({"patient_id": [1], "y_true": [0], "y_prob": [0.5]}) - assert missing_columns(df) == ["machine_id", "site_id", "split"] - - -def test_missing_columns_ignores_extra_columns() -> None: - df = pd.DataFrame({column: [0] for column in REQUIRED_COLUMNS} | {"laterality": ["L"]}) - assert missing_columns(df) == [] + assert validate_predictions(df) == [] diff --git a/tests/test_cli.py b/tests/test_cli.py index dda2887..0904780 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -16,6 +16,18 @@ DEMO_CSV = REPO_ROOT / "claude-science-artifacts" / "scenarios" / "weak_random_split_leakage.csv" +def test_cli_rejects_an_invalid_table(tmp_path: Path, capsys) -> None: + path = tmp_path / "bad.csv" + path.write_text("patient_id,y_true,y_prob\n1,0,0.2\n", encoding="utf-8") + exit_code = cli.main(["--predictions", str(path)]) + assert exit_code == 2 + captured = capsys.readouterr() + assert captured.out == "" + assert "Missing required column" in captured.err + assert "machine_id" in captured.err + assert "Traceback" not in captured.err + + def test_load_predictions_reads_the_table() -> None: df = _load_predictions(DEMO_CSV) assert len(df) == 8342 diff --git a/tests/test_validators.py b/tests/test_validators.py index ff757da..9532fe0 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -13,6 +13,7 @@ check_patient_leakage, check_subgroup_degradation, compute_pooled_metric, + validate_predictions, ) SCENARIOS_DIR = Path(__file__).resolve().parent.parent / "claude-science-artifacts" / "scenarios" @@ -22,6 +23,69 @@ def _load(scenario: str) -> pd.DataFrame: return pd.read_csv(SCENARIOS_DIR / f"{scenario}.csv") +def _valid_frame() -> pd.DataFrame: + """Small table that satisfies every boundary check.""" + return pd.DataFrame( + { + "patient_id": ["P1", "P1", "P2", "P2"], + "machine_id": ["M010", "M010", "M020", "M020"], + "site_id": ["S01", "S01", "S02", "S02"], + "y_true": [0, 1, 0, 1], + "y_prob": [0.0, 1.0, 0.3, 0.7], + "split": ["train", "train", "test", "test"], + } + ) + + +# --- Input boundary ------------------------------------------------------ + + +def test_validate_predictions_accepts_a_clean_fixture() -> None: + assert validate_predictions(_load("clean_patient_grouped")) == [] + assert validate_predictions(_valid_frame()) == [] + + +def test_validate_predictions_reports_missing_columns() -> None: + df = _valid_frame().drop(columns=["machine_id", "split"]) + problems = validate_predictions(df) + assert len(problems) == 1 + assert "machine_id" in problems[0] + assert "split" in problems[0] + + +def test_validate_predictions_reports_a_missing_train_or_test_split() -> None: + df = _valid_frame() + df["split"] = "val" + problems = validate_predictions(df) + assert any('split == "train"' in problem for problem in problems) + assert any('split == "test"' in problem for problem in problems) + + +def test_validate_predictions_reports_y_true_outside_0_1() -> None: + df = _valid_frame() + df.loc[0, "y_true"] = 2 + assert validate_predictions(df) == ["y_true has values outside {0, 1}."] + + +def test_validate_predictions_reports_test_rows_without_both_classes() -> None: + df = _valid_frame() + df.loc[df["split"] == "test", "y_true"] = 0 + assert validate_predictions(df) == ["Test rows do not contain both classes (0 and 1)."] + + +def test_validate_predictions_reports_non_numeric_y_prob() -> None: + df = _valid_frame() + df["y_prob"] = df["y_prob"].astype(object) + df.loc[0, "y_prob"] = "high" + assert validate_predictions(df) == ["y_prob is not numeric."] + + +def test_validate_predictions_reports_y_prob_outside_0_1() -> None: + df = _valid_frame() + df.loc[0, "y_prob"] = 1.2 + assert validate_predictions(df) == ["y_prob has values outside [0, 1]."] + + # --- Check 1: patient-level leakage -------------------------------------- From bdc7173e8668a571f095d47480ca72ff0716534d Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 12:59:49 +0200 Subject: [PATCH 03/20] Describe the split audit as a rescore, not a grouped retest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The patient sample behind the second AUC is drawn from every row, and about two thirds of those rows are training rows. Calling it a grouped split implied a held-out fold and a measurement of how much leakage inflated the metric. The scores are fixed before the split is assigned, so that comparison cannot show inflation. Rename the two AUCs, keep gap as their difference, and report the 2.5th–97.5th percentile of the sample AUC over seeds 0–199. The same seed-0 sample remains inside that range, and repeating the audit returns the same numbers. --- app.py | 18 +++++++---- clinval_validators/checks.py | 62 +++++++++++++++++++++--------------- tests/test_validators.py | 14 ++++---- 3 files changed, 55 insertions(+), 39 deletions(-) diff --git a/app.py b/app.py index 2864c3d..2a8a71d 100644 --- a/app.py +++ b/app.py @@ -80,16 +80,20 @@ def main() -> None: c1, c2, c3, c4 = st.columns(4) c1.metric("Leaked patients", leakage["overlapping_patient_ids"]) c2.metric("Leakage rate", f"{leakage['leakage_rate']:.1%}") - # The only like-for-like pair: one fixed y_prob, two evaluation populations. - # Labels stay split-agnostic because an uploaded file may already be grouped. - c3.metric("AUC, test split as given", f"{audit['random_split']:.3f}") + # Same fixed predictions. The sample draws patients from every row, + # including training rows, so it is not a second look at the test split. + c3.metric("AUC, test split as given", f"{audit['test_split_as_given']:.3f}") c4.metric( - "AUC, patient-disjoint subset", - f"{audit['grouped_split']:.3f}", - delta=f"{audit['grouped_split'] - audit['random_split']:+.3f}", + "AUC, patient-disjoint sample", + f"{audit['patient_disjoint_sample']:.3f}", + delta=f"{audit['patient_disjoint_sample'] - audit['test_split_as_given']:+.3f}", ) + low, high = audit["sample_range95"] st.caption( - "No retraining β€” only the evaluation population changes. " + "Both AUCs use the same fixed predictions. The patient-disjoint " + "sample is drawn from all rows, including training rows, so this " + "comparison cannot measure how much leakage inflated the metric. " + f"Over seeds 0–199 that sample AUC stays within {low:.3f}–{high:.3f}. " "Pooled AUC and the per-subgroup breakdown are in the JSON below." ) if leakage["overlapping_patient_ids"]: diff --git a/clinval_validators/checks.py b/clinval_validators/checks.py index 5eeb1e4..a2247af 100644 --- a/clinval_validators/checks.py +++ b/clinval_validators/checks.py @@ -9,6 +9,7 @@ from typing import TypedDict +import numpy as np import pandas as pd from sklearn.metrics import roc_auc_score from sklearn.model_selection import GroupShuffleSplit @@ -37,9 +38,7 @@ # --- Report JSON contract ------------------------------------------------- -# Shape mirrors prompts/opus-4-8/reporter_system_prompt.md exactly. The -# validators produce this payload; the reporter consumes it. Do not add fields -# beyond the ones documented there. +# The validators produce this payload; the reporter sends it to Claude. class LeakageResult(TypedDict): @@ -49,8 +48,9 @@ class LeakageResult(TypedDict): class SplitAuditResult(TypedDict): metric: str - random_split: float - grouped_split: float + test_split_as_given: float + patient_disjoint_sample: float + sample_range95: list[float] gap: float @@ -135,30 +135,40 @@ def check_patient_leakage(df: pd.DataFrame) -> LeakageResult: def audit_split_quality(df: pd.DataFrame) -> SplitAuditResult: - """Recompute the headline metric under the file's split vs a grouped split. - - Both AUCs read the same fixed ``y_prob`` on two different test subsets, so - this measures how sensitive the headline metric is to the choice of split β€” - it is a diagnostic, not a retrained proof of split-induced optimism. - ``random_split`` uses the test rows as given in the file; ``grouped_split`` - re-partitions by ``patient_id`` with a fixed seed so no patient spans the - fold boundary. The rigorous leakage signal is :func:`check_patient_leakage`; - ``gap`` can be small or even negative when the leak is structural patient - overlap rather than an inflated ``y_prob``. + """Compare two AUCs computed from the same fixed predictions. + + ``test_split_as_given`` is the AUC on rows with ``split == "test"``. + ``patient_disjoint_sample`` is the AUC on a seed-0 sample of 15% of + patients drawn from every row, so that sample includes training rows. + ``sample_range95`` is the 2.5th and 97.5th percentile of that sample AUC + over seeds 0–199. ``gap`` is ``test_split_as_given`` minus + ``patient_disjoint_sample``. + + The comparison cannot measure how much leakage inflated a metric. Both + numbers rescore predictions that already exist; they do not come from + retraining under a patient-grouped split. The leakage count is + :func:`check_patient_leakage`. """ - test_rows = df[df["split"] == "test"] - random_split = float(roc_auc_score(test_rows["y_true"], test_rows["y_prob"])) - - splitter = GroupShuffleSplit(n_splits=1, test_size=0.15, random_state=0) - _, grouped_test_idx = next(splitter.split(df, groups=df["patient_id"])) - grouped_fold = df.iloc[grouped_test_idx] - grouped_split = float(roc_auc_score(grouped_fold["y_true"], grouped_fold["y_prob"])) - + y_true = df["y_true"].to_numpy() + y_prob = df["y_prob"].to_numpy() + test_rows = df["split"].to_numpy() == "test" + test_split_as_given = float(roc_auc_score(y_true[test_rows], y_prob[test_rows])) + + groups = df["patient_id"] + sample_aucs = np.empty(200, dtype=np.float64) + for seed in range(200): + splitter = GroupShuffleSplit(n_splits=1, test_size=0.15, random_state=seed) + _, sample_idx = next(splitter.split(df, groups=groups)) + sample_aucs[seed] = roc_auc_score(y_true[sample_idx], y_prob[sample_idx]) + + low, high = np.percentile(sample_aucs, [2.5, 97.5]) + patient_disjoint_sample = float(sample_aucs[0]) return { "metric": "auc", - "random_split": random_split, - "grouped_split": grouped_split, - "gap": random_split - grouped_split, + "test_split_as_given": test_split_as_given, + "patient_disjoint_sample": patient_disjoint_sample, + "sample_range95": [float(low), float(high)], + "gap": test_split_as_given - patient_disjoint_sample, } diff --git a/tests/test_validators.py b/tests/test_validators.py index 9532fe0..b39f304 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -164,12 +164,14 @@ def test_audit_split_quality_structure_and_direction() -> None: df = _load("weak_random_split_leakage") audit = audit_split_quality(df) assert audit["metric"] == "auc" - # Both AUCs are finite probabilities computed over the same fixed y_prob. - assert 0.0 < audit["random_split"] < 1.0 - assert 0.0 < audit["grouped_split"] < 1.0 - # gap is exactly the signed difference (may be small or negative β€” the leak - # here is structural patient overlap, not an inflated y_prob). - assert audit["gap"] == pytest.approx(audit["random_split"] - audit["grouped_split"]) + # Both AUCs are computed from the same fixed y_prob. + assert 0.0 < audit["test_split_as_given"] < 1.0 + assert 0.0 < audit["patient_disjoint_sample"] < 1.0 + low, high = audit["sample_range95"] + assert low <= audit["patient_disjoint_sample"] <= high + assert audit["gap"] == pytest.approx( + audit["test_split_as_given"] - audit["patient_disjoint_sample"] + ) # The primary leakage signal for a random split is shared patients. assert check_patient_leakage(df)["overlapping_patient_ids"] > 0 From e6efa7d11346a30a15f84824728ebb09a670159e Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 13:03:51 +0200 Subject: [PATCH 04/20] Flag a subgroup only when it is worse than the rest Comparing a subgroup with the pooled AUC hides the drop, because the degraded rows are inside the pool. On subgroup_density_degradation, density C is 0.670 against 0.792 on the other rows, and the old check flagged nothing. Each subgroup is now compared with the rest of the table. machine_id and site_id are always evaluated; density and decade age bands are evaluated when those columns are present. A subgroup is flagged when its AUC is at least 0.10 below the rest and the upper end of a patient-level 95% interval (200 resamples, seed 0) is below zero. Mean predicted probability and the observed rate are reported and do not flag. The pooled AUC is unchanged, and it assumes every row is a held-out or out-of-fold prediction. --- clinval_validators/checks.py | 198 +++++++++++++++++++++++++++++------ tests/test_validators.py | 93 ++++++++++++++-- 2 files changed, 250 insertions(+), 41 deletions(-) diff --git a/clinval_validators/checks.py b/clinval_validators/checks.py index a2247af..e264279 100644 --- a/clinval_validators/checks.py +++ b/clinval_validators/checks.py @@ -14,11 +14,14 @@ from sklearn.metrics import roc_auc_score from sklearn.model_selection import GroupShuffleSplit -# Subgroup flag thresholds, calibrated against the labeled scenarios: M030 in -# subgroup_scanner_degradation sits 0.21 below pooled and must flag, while every -# subgroup in clean_patient_grouped is within 0.07 of pooled and must not. +# Subgroup flag thresholds, calibrated on the committed fixtures. A subgroup +# flags when its AUC is at least 0.10 below the rest of the table and the +# patient-level 95% interval for that drop lies entirely below zero. M030 in +# subgroup_scanner_degradation must flag; clean_patient_grouped must not. _FLAG_DELTA = -0.10 _MIN_SUBGROUP_N = 50 +_BOOTSTRAP_REPLICATES = 200 +_BOOTSTRAP_SEED = 0 # Columns the checks read by name. Anything else in the table is ignored. _REQUIRED_COLUMNS = ("patient_id", "machine_id", "site_id", "y_true", "y_prob", "split") @@ -59,7 +62,11 @@ class SubgroupResult(TypedDict): value: str n: int auc: float - delta_vs_pooled: float + auc_rest: float + delta_vs_rest: float + delta_ci95: list[float] | None + mean_predicted: float + observed_rate: float flagged: bool @@ -172,43 +179,172 @@ def audit_split_quality(df: pd.DataFrame) -> SplitAuditResult: } -def check_subgroup_degradation(df: pd.DataFrame) -> list[SubgroupResult]: - """Compute per-``machine_id``/``site_id`` AUC and flag degraded subgroups. +def _roc_auc(y_true: np.ndarray, y_score: np.ndarray) -> float: + """ROC AUC with average ranks for ties, the same definition sklearn uses. - Returns one entry per evaluated subgroup, each carrying its AUC, delta - versus the pooled metric, and whether it is flagged as significantly below - pooled performance. A subgroup is skipped (not evaluated) when it has fewer - than ``_MIN_SUBGROUP_N`` rows or lacks both classes, since its AUC would be - undefined or too noisy to trust. + A rank statistic keeps the patient-level bootstrap under a few seconds. """ - pooled_auc = float(roc_auc_score(df["y_true"], df["y_prob"])) - results: list[SubgroupResult] = [] + y_true = np.asarray(y_true) + y_score = np.asarray(y_score, dtype=np.float64) + n_pos = int(np.sum(y_true == 1)) + n = y_true.size + n_neg = n - n_pos + if n_pos == 0 or n_neg == 0: + return float("nan") + order = np.argsort(y_score, kind="mergesort") + y_sorted = y_true[order] + scores = y_score[order] + change = np.empty(n, dtype=bool) + change[0] = True + change[1:] = scores[1:] != scores[:-1] + group = np.cumsum(change) - 1 + counts = np.bincount(group) + ends = np.cumsum(counts) + average_rank = (ends - counts + 1 + ends) / 2.0 + sum_pos = float(average_rank[group][y_sorted == 1].sum()) + return (sum_pos - n_pos * (n_pos + 1) / 2.0) / (n_pos * n_neg) + + +def _both_classes(y_true: np.ndarray) -> bool: + return bool(y_true.size) and bool((y_true == 0).any()) and bool((y_true == 1).any()) + + +def _patient_bootstrap_indices(patient_id: pd.Series) -> list[np.ndarray]: + """Row indices for each patient-level resample of the whole table. + + Patients are drawn with replacement. A patient drawn twice contributes + their rows twice. One draw is reused for every subgroup. + """ + codes, _ = pd.factorize(patient_id, sort=False) + order = np.argsort(codes, kind="mergesort") + sorted_codes = codes[order] + starts = np.flatnonzero(np.r_[True, sorted_codes[1:] != sorted_codes[:-1]]) + counts = np.diff(np.r_[starts, len(patient_id)]) + n_patients = len(counts) + draws = np.random.default_rng(_BOOTSTRAP_SEED).integers( + 0, n_patients, size=(_BOOTSTRAP_REPLICATES, n_patients) + ) + indices: list[np.ndarray] = [] + for draw in draws: + repeated = counts[draw] + total = int(repeated.sum()) + within = np.arange(total) - np.repeat(np.cumsum(repeated) - repeated, repeated) + indices.append(order[np.repeat(starts[draw], repeated) + within]) + return indices + + +def _value_masks(values: np.ndarray) -> list[tuple[str, np.ndarray]]: + masks: list[tuple[str, np.ndarray]] = [] + for value in pd.unique(values): + if pd.isna(value): + continue + masks.append((str(value), values == value)) + return masks + + +def _age_band_masks(age: pd.Series) -> list[tuple[str, np.ndarray]]: + """Decade bands such as ``50-59``. Missing ages are skipped.""" + numeric = pd.to_numeric(age, errors="coerce").to_numpy(dtype=float) + valid = ~np.isnan(numeric) + decade = np.full(numeric.size, -1, dtype=int) + decade[valid] = (np.floor(numeric[valid] / 10.0) * 10).astype(int) + masks: list[tuple[str, np.ndarray]] = [] + for band in sorted(set(decade[valid].tolist())): + masks.append((f"{band}-{band + 9}", decade == band)) + return masks + + +def _subgroup_specs(df: pd.DataFrame) -> list[tuple[str, str, np.ndarray]]: + specs: list[tuple[str, str, np.ndarray]] = [] for key in ("machine_id", "site_id"): - for value, group in df.groupby(key): - if len(group) < _MIN_SUBGROUP_N or group["y_true"].nunique() < 2: + for label, mask in _value_masks(df[key].to_numpy()): + specs.append((key, label, mask)) + if "density" in df.columns: + for label, mask in _value_masks(df["density"].to_numpy()): + specs.append(("density", label, mask)) + if "age" in df.columns: + for label, mask in _age_band_masks(df["age"]): + specs.append(("age", label, mask)) + return specs + + +def check_subgroup_degradation(df: pd.DataFrame) -> list[SubgroupResult]: + """Compare each subgroup's AUC with the rest of the table. + + Evaluates ``machine_id`` and ``site_id``, and ``density`` and decade age + bands when those columns exist. A subgroup is evaluated only when it has + at least ``_MIN_SUBGROUP_N`` rows and both the subgroup and the rest of + the table contain both classes. + + ``delta_vs_rest`` is the subgroup AUC minus the AUC of every other row. + ``delta_ci95`` is the 2.5th and 97.5th percentile of that difference over + a patient-level bootstrap (resample ``patient_id`` with replacement, + 200 replicates, seed 0). Replicates that lack a class in the subgroup or + in the rest are skipped. The subgroup is flagged when ``delta_vs_rest`` + is at or below ``_FLAG_DELTA`` and the upper end of ``delta_ci95`` is + below zero. + + ``mean_predicted`` and ``observed_rate`` are calibration-in-the-large for + the subgroup. They are reported and never used to flag. + """ + y_true = df["y_true"].to_numpy() + y_prob = df["y_prob"].to_numpy(dtype=float) + boots = _patient_bootstrap_indices(df["patient_id"]) + results: list[SubgroupResult] = [] + for key, value, mask in _subgroup_specs(df): + n_sub = int(mask.sum()) + if n_sub < _MIN_SUBGROUP_N: + continue + y_sub = y_true[mask] + y_rest = y_true[~mask] + if not _both_classes(y_sub) or not _both_classes(y_rest): + continue + auc = _roc_auc(y_sub, y_prob[mask]) + auc_rest = _roc_auc(y_rest, y_prob[~mask]) + delta = auc - auc_rest + deltas: list[float] = [] + for idx in boots: + drawn = mask[idx] + if not drawn.any() or not (~drawn).any(): + continue + y_draw = y_true[idx] + p_draw = y_prob[idx] + y_draw_sub = y_draw[drawn] + y_draw_rest = y_draw[~drawn] + if not _both_classes(y_draw_sub) or not _both_classes(y_draw_rest): continue - auc = float(roc_auc_score(group["y_true"], group["y_prob"])) - delta = auc - pooled_auc - results.append( - { - "key": key, - "value": str(value), - "n": len(group), - "auc": auc, - "delta_vs_pooled": delta, - "flagged": delta <= _FLAG_DELTA, - } + deltas.append( + _roc_auc(y_draw_sub, p_draw[drawn]) - _roc_auc(y_draw_rest, p_draw[~drawn]) ) + if deltas: + low, high = (float(bound) for bound in np.percentile(deltas, [2.5, 97.5])) + ci: list[float] | None = [low, high] + flagged = bool(delta <= _FLAG_DELTA and high < 0) + else: + ci = None + flagged = False + results.append( + { + "key": key, + "value": value, + "n": n_sub, + "auc": float(auc), + "auc_rest": float(auc_rest), + "delta_vs_rest": float(delta), + "delta_ci95": ci, + "mean_predicted": float(y_prob[mask].mean()), + "observed_rate": float(np.mean(y_sub)), + "flagged": flagged, + } + ) return results def compute_pooled_metric(df: pd.DataFrame) -> PooledMetric: """Compute the pooled headline metric (AUC) over all rows. - This is the reference value that subgroup deltas are measured against and - the ``pooled_metric`` field of the report payload. AUC is computed over the - full table (not the test split alone) to match each manifest's - ``verification.overall_auc`` and give every subgroup enough positives for a - defined per-subgroup AUC at this dataset's ~1.5% prevalence. + Assumes every row's ``y_prob`` is a held-out or out-of-fold prediction. + AUC is computed over the full table (not the test split alone) to match + each manifest's ``verification.overall_auc``. """ return {"name": "auc", "value": float(roc_auc_score(df["y_true"], df["y_prob"]))} diff --git a/tests/test_validators.py b/tests/test_validators.py index b39f304..ef9ac01 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -2,12 +2,16 @@ from __future__ import annotations +import sys from pathlib import Path import pytest pd = pytest.importorskip("pandas") +_GENERATOR_DIR = Path(__file__).resolve().parent.parent / "claude-science-artifacts" +sys.path.insert(0, str(_GENERATOR_DIR)) + from clinval_validators import ( # noqa: E402 audit_split_quality, check_patient_leakage, @@ -15,6 +19,12 @@ compute_pooled_metric, validate_predictions, ) +from leakage_generator import ( # noqa: E402 + Flaws, + GeneratorConfig, + SubgroupDegradation, + generate, +) SCENARIOS_DIR = Path(__file__).resolve().parent.parent / "claude-science-artifacts" / "scenarios" @@ -136,25 +146,88 @@ def test_pooled_metric_matches_manifest_overall_auc(scenario: str, expected_auc: # --- Check 3: subgroup degradation --------------------------------------- +# make_scenarios.py builds the committed fixtures with seeds 1 through 6. +_FRESH_CLEAN_SEEDS = (11, 12, 13, 14, 15) +_FRESH_SCANNER_SEEDS = (7, 8, 9, 12, 13) + + +@pytest.fixture(scope="module") +def subgroups_by_scenario() -> dict[str, list]: + """Run the subgroup check once per committed scenario.""" + names = ( + "clean_patient_grouped", + "weak_random_split_leakage", + "duplicate_row_leakage", + "subgroup_scanner_degradation", + "subgroup_density_degradation", + "optimistic_site_shortcut", + ) + return {name: check_subgroup_degradation(_load(name)) for name in names} + -def test_subgroup_degradation_is_detectable() -> None: - subgroups = check_subgroup_degradation(_load("subgroup_scanner_degradation")) +def test_subgroup_degradation_is_detectable(subgroups_by_scenario: dict[str, list]) -> None: + subgroups = subgroups_by_scenario["subgroup_scanner_degradation"] assert any(sg["flagged"] for sg in subgroups) m030 = next(sg for sg in subgroups if sg["key"] == "machine_id" and sg["value"] == "M030") assert m030["flagged"] assert m030["auc"] == pytest.approx(0.6243, abs=1e-4) - assert m030["delta_vs_pooled"] < -0.10 - - -def test_no_subgroup_flags_on_clean_scenario() -> None: - subgroups = check_subgroup_degradation(_load("clean_patient_grouped")) + assert m030["delta_vs_rest"] == pytest.approx(m030["auc"] - m030["auc_rest"]) + assert m030["delta_vs_rest"] <= -0.10 + assert m030["delta_ci95"] is not None + assert m030["delta_ci95"][1] < 0 + assert 0.0 <= m030["mean_predicted"] <= 1.0 + assert 0.0 <= m030["observed_rate"] <= 1.0 + + +def test_density_degradation_flags_c_or_d_not_a_or_b(subgroups_by_scenario: dict[str, list]) -> None: + flagged = { + (sg["key"], sg["value"]) + for sg in subgroups_by_scenario["subgroup_density_degradation"] + if sg["flagged"] + } + assert ("density", "C") in flagged or ("density", "D") in flagged + assert ("density", "A") not in flagged + assert ("density", "B") not in flagged + + +def test_no_subgroup_flags_on_clean_scenario(subgroups_by_scenario: dict[str, list]) -> None: + subgroups = subgroups_by_scenario["clean_patient_grouped"] assert subgroups # subgroups were evaluated assert not any(sg["flagged"] for sg in subgroups) -def test_subgroup_results_cover_machine_and_site_keys() -> None: - keys = {sg["key"] for sg in check_subgroup_degradation(_load("clean_patient_grouped"))} - assert keys == {"machine_id", "site_id"} +def test_subgroup_results_cover_machine_site_density_and_age( + subgroups_by_scenario: dict[str, list], +) -> None: + subgroups = subgroups_by_scenario["clean_patient_grouped"] + assert {sg["key"] for sg in subgroups} == {"machine_id", "site_id", "density", "age"} + assert "50-59" in {sg["value"] for sg in subgroups if sg["key"] == "age"} + + +def test_fresh_seeds_flag_scanner_degradation_and_not_clean_tables() -> None: + # Seeds 1–6 are the committed fixtures. These are other draws of the same + # generator configs: five clean tables, five with M030 degraded to AUC 0.62. + used_by_make_scenarios = {1, 2, 3, 4, 5, 6} + assert used_by_make_scenarios.isdisjoint(_FRESH_CLEAN_SEEDS) + assert used_by_make_scenarios.isdisjoint(_FRESH_SCANNER_SEEDS) + + for seed in _FRESH_CLEAN_SEEDS: + df, _manifest = generate(GeneratorConfig(seed=seed, flaws=Flaws(split_strategy="patient"))) + flagged = [sg for sg in check_subgroup_degradation(df) if sg["flagged"]] + assert flagged == [] + + degradation = SubgroupDegradation("machine_id", ["M030"], target_auc=0.62) + for seed in _FRESH_SCANNER_SEEDS: + df, _manifest = generate( + GeneratorConfig( + seed=seed, + flaws=Flaws(split_strategy="patient", subgroup_degradations=[degradation]), + ) + ) + subgroups = check_subgroup_degradation(df) + assert any( + sg["key"] == "machine_id" and sg["value"] == "M030" and sg["flagged"] for sg in subgroups + ) # --- Check 2: split-quality audit ---------------------------------------- From ad2567e84919e1286a9dde6d3d30791dde98ee26 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 13:07:26 +0200 Subject: [PATCH 05/20] Compute the risk level in Python The reporter prompt told Claude to choose RED when leakage was present or the random-versus-grouped gap was "large", and nothing defined large. The level is a pass/fail decision, so it belongs with the other flags. compute_risk returns RED when any patient is in both train and test, YELLOW when a subgroup is flagged, and GREEN otherwise. The split audit stays in the payload as a diagnostic and does not enter the rule. run_checks is the single payload the CLI, the UI, and the tests share. The prompts tell Claude to state that level verbatim, to treat the split audit as a rescore that cannot show inflation, and, when patients overlap, to ask for a retrained patient-grouped evaluation rather than another score of the same predictions. --- app.py | 27 ++++---- cli.py | 28 ++------ clinval_validators/__init__.py | 6 ++ clinval_validators/checks.py | 66 ++++++++++++++++++- prompts/opus-4-8/reporter_system_prompt.md | 42 +++++++++--- .../reviewer_critique_system_prompt.md | 37 +++++++++-- tests/test_app.py | 2 +- tests/test_cli.py | 10 +-- tests/test_reporter.py | 28 +++++++- tests/test_validators.py | 19 ++++++ 10 files changed, 207 insertions(+), 58 deletions(-) diff --git a/app.py b/app.py index 2a8a71d..9458130 100644 --- a/app.py +++ b/app.py @@ -17,8 +17,7 @@ import pandas as pd -from cli import _assemble_payload -from clinval_validators import validate_predictions +from clinval_validators import run_checks, validate_predictions from reporter import API_KEY_ENV, render_report, render_reviewer_critique SCENARIOS_DIR = Path(__file__).resolve().parent / "claude-science-artifacts" / "scenarios" @@ -29,6 +28,11 @@ def available_scenarios() -> dict[str, Path]: return {p.stem: p for p in sorted(SCENARIOS_DIR.glob("*.csv")) if not p.name.startswith("_")} +def _run_checks(df: pd.DataFrame): + """Streamlit-cacheable wrapper. The cache lives on this function, not on a session.""" + return run_checks(df) + + def main() -> None: import streamlit as st @@ -68,9 +72,10 @@ def main() -> None: st.error("\n\n".join(problems), icon="🚫") st.stop() - payload = _assemble_payload(df) + payload = st.cache_data(_run_checks)(df) leakage = payload["leakage"] - flagged = [s for s in payload["subgroups"] if s["flagged"]] + level = payload["risk"]["level"] + reasons = " ".join(payload["risk"]["reasons"]) left, right = st.columns(2) @@ -96,16 +101,12 @@ def main() -> None: f"Over seeds 0–199 that sample AUC stays within {low:.3f}–{high:.3f}. " "Pooled AUC and the per-subgroup breakdown are in the JSON below." ) - if leakage["overlapping_patient_ids"]: - st.error( - f"{leakage['overlapping_patient_ids']} patients appear in both " - "train and test β€” the split is not leakage-safe.", - icon="🚫", - ) - elif flagged: - st.warning(f"{len(flagged)} subgroup(s) flagged below pooled AUC.", icon="⚠️") + if level == "RED": + st.error(f"{level}. {reasons}", icon="🚫") + elif level == "YELLOW": + st.warning(f"{level}. {reasons}", icon="⚠️") else: - st.success("No leakage and no subgroup flags.", icon="βœ…") + st.success(f"{level}. {reasons}", icon="βœ…") st.caption("Payload sent to Claude (every number is computed here, not by the model):") st.json(payload) st.download_button( diff --git a/cli.py b/cli.py index 4dd756b..4a4be53 100644 --- a/cli.py +++ b/cli.py @@ -1,10 +1,9 @@ """Command-line entrypoint: validate a predictions CSV and print the risk report. Wires the two layers: load predictions -> run the deterministic validators -> -assemble the report payload -> hand it to the reporter -> print the report. The -compute logic lives in ``clinval_validators`` and runs offline; ``reporter`` is the -only part that needs the network, so without an API key the validators still run and -their JSON is printed. +hand the payload to the reporter -> print the report. The compute logic lives in +``clinval_validators`` and runs offline; ``reporter`` is the only part that needs +the network, so without an API key the validators still run and their JSON is printed. """ from __future__ import annotations @@ -17,15 +16,8 @@ import pandas as pd +from clinval_validators import run_checks, validate_predictions from reporter import API_KEY_ENV, MissingAPIKeyError, render_report, render_reviewer_critique -from clinval_validators import ( - ReportPayload, - audit_split_quality, - check_patient_leakage, - check_subgroup_degradation, - compute_pooled_metric, - validate_predictions, -) def _parse_args(argv: Sequence[str] | None) -> argparse.Namespace: @@ -54,16 +46,6 @@ def _load_predictions(path: Path) -> pd.DataFrame: return pd.read_csv(path) -def _assemble_payload(df: pd.DataFrame) -> ReportPayload: - """Run every validator and assemble the report JSON contract.""" - return { - "leakage": check_patient_leakage(df), - "split_audit": audit_split_quality(df), - "subgroups": check_subgroup_degradation(df), - "pooled_metric": compute_pooled_metric(df), - } - - def main(argv: Sequence[str] | None = None) -> int: args = _parse_args(argv) df = _load_predictions(args.predictions) @@ -71,7 +53,7 @@ def main(argv: Sequence[str] | None = None) -> int: if problems: print("\n".join(problems), file=sys.stderr) return 2 - payload = _assemble_payload(df) + payload = run_checks(df) render = render_reviewer_critique if args.mode == "critique" else render_report try: print(render(payload)) diff --git a/clinval_validators/__init__.py b/clinval_validators/__init__.py index 23cb94c..f5d90e2 100644 --- a/clinval_validators/__init__.py +++ b/clinval_validators/__init__.py @@ -4,12 +4,15 @@ LeakageResult, PooledMetric, ReportPayload, + RiskResult, SplitAuditResult, SubgroupResult, audit_split_quality, check_patient_leakage, check_subgroup_degradation, compute_pooled_metric, + compute_risk, + run_checks, validate_predictions, ) @@ -17,11 +20,14 @@ "LeakageResult", "PooledMetric", "ReportPayload", + "RiskResult", "SplitAuditResult", "SubgroupResult", "audit_split_quality", "check_patient_leakage", "check_subgroup_degradation", "compute_pooled_metric", + "compute_risk", + "run_checks", "validate_predictions", ] diff --git a/clinval_validators/checks.py b/clinval_validators/checks.py index e264279..00002d6 100644 --- a/clinval_validators/checks.py +++ b/clinval_validators/checks.py @@ -7,7 +7,7 @@ from __future__ import annotations -from typing import TypedDict +from typing import Literal, TypedDict import numpy as np import pandas as pd @@ -37,6 +37,9 @@ "audit_split_quality", "check_subgroup_degradation", "compute_pooled_metric", + "compute_risk", + "run_checks", + "RiskResult", ] @@ -75,11 +78,17 @@ class PooledMetric(TypedDict): value: float +class RiskResult(TypedDict): + level: Literal["RED", "YELLOW", "GREEN"] + reasons: list[str] + + class ReportPayload(TypedDict): leakage: LeakageResult split_audit: SplitAuditResult subgroups: list[SubgroupResult] pooled_metric: PooledMetric + risk: RiskResult # --- Checks --------------------------------------------------------------- @@ -348,3 +357,58 @@ def compute_pooled_metric(df: pd.DataFrame) -> PooledMetric: each manifest's ``verification.overall_auc``. """ return {"name": "auc", "value": float(roc_auc_score(df["y_true"], df["y_prob"]))} + + +def compute_risk(leakage: LeakageResult, subgroups: list[SubgroupResult]) -> RiskResult: + """Choose RED, YELLOW, or GREEN from leakage and subgroup flags. + + RED when any patient is in both train and test. Otherwise YELLOW when + any subgroup is flagged. Otherwise GREEN. The split audit is not an + input: rescoring the same predictions cannot show inflation. + """ + overlap = leakage["overlapping_patient_ids"] + if overlap > 0: + rate = leakage["leakage_rate"] + return { + "level": "RED", + "reasons": [ + f"{overlap} patients appear in both the train and test splits " + f"(leakage rate {rate:.1%})." + ], + } + + flagged = [subgroup for subgroup in subgroups if subgroup["flagged"]] + if flagged: + reasons: list[str] = [] + for subgroup in flagged: + interval = subgroup["delta_ci95"] + if interval is None: + interval_text = "no bootstrap interval was available" + else: + interval_text = f"95% interval {interval[0]:+.3f} to {interval[1]:+.3f}" + reasons.append( + f"{subgroup['key']} {subgroup['value']} has AUC {subgroup['auc']:.3f} " + f"against {subgroup['auc_rest']:.3f} on the rest of the table " + f"(delta {subgroup['delta_vs_rest']:+.3f}, {interval_text})." + ) + return {"level": "YELLOW", "reasons": reasons} + + return { + "level": "GREEN", + "reasons": [ + "No patient appears in both the train and test splits, and no subgroup is flagged." + ], + } + + +def run_checks(df: pd.DataFrame) -> ReportPayload: + """Run every deterministic check and attach the risk level.""" + leakage = check_patient_leakage(df) + subgroups = check_subgroup_degradation(df) + return { + "leakage": leakage, + "split_audit": audit_split_quality(df), + "subgroups": subgroups, + "pooled_metric": compute_pooled_metric(df), + "risk": compute_risk(leakage, subgroups), + } diff --git a/prompts/opus-4-8/reporter_system_prompt.md b/prompts/opus-4-8/reporter_system_prompt.md index 445d9be..e7310c4 100644 --- a/prompts/opus-4-8/reporter_system_prompt.md +++ b/prompts/opus-4-8/reporter_system_prompt.md @@ -4,33 +4,59 @@ This is the system prompt for the LLM step. It receives the deterministic valida --- -You are a senior clinical-AI validation reviewer. You are reviewing the automated validation output for a medical-imaging machine-learning model. A deterministic analysis pipeline has already computed all metrics, statistical tests, and flags; you are given its results as JSON. Your job is to explain what these results mean for the safety of the model's performance claims β€” not to recompute anything. +You are a senior clinical-AI validation reviewer. You are reviewing the automated validation output for a medical-imaging machine-learning model. A deterministic analysis pipeline has already computed all metrics, intervals, flags, and the risk level; you are given its results as JSON. Your job is to explain what these results mean for the safety of the model's performance claims. Do not recompute anything, and do not change the risk level. Rules: - Use only the numbers present in the provided JSON. Never invent, estimate, or recompute a metric. If a value is absent, say the check did not run rather than guessing. +- State risk.level verbatim. Explain each sentence in risk.reasons. Do not raise, lower, or rename the level. +- risk.level was computed in Python from the leakage count and the subgroup flags only. RED means overlapping_patient_ids is greater than zero. YELLOW means there is no patient overlap and at least one subgroup is flagged. GREEN means neither. split_audit did not determine the level. - Do not soften genuine problems and do not inflate them. State the finding, then its consequence for what may be claimed. - Write for a researcher who trained the model, not for a lay audience. Assume they know AUC, calibration, and train/test splits. +- split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, so that sample includes training rows. sample_range95 is the 2.5th and 97.5th percentile of that sample AUC over seeds 0–199. gap is test_split_as_given minus patient_disjoint_sample. The comparison cannot show that leakage inflated a metric. Do not call either AUC a corrected, true, or retrained result. +- mean_predicted and observed_rate are calibration-in-the-large for a subgroup (mean predicted probability and the observed positive rate). Mention them when you discuss calibration. They are not flags. +- pooled_metric assumes every row's y_prob is a held-out or out-of-fold prediction. Input JSON shape (fields may be absent if a check did not run): ```json { "leakage": {"overlapping_patient_ids": int, "leakage_rate": float}, - "split_audit": {"metric": "auc", "random_split": float, "grouped_split": float, "gap": float}, - "subgroups": [{"key": "machine_id", "value": "...", "n": int, "auc": float, "delta_vs_pooled": float, "flagged": bool}], - "pooled_metric": {"name": "auc", "value": float} + "split_audit": { + "metric": "auc", + "test_split_as_given": float, + "patient_disjoint_sample": float, + "sample_range95": [float, float], + "gap": float + }, + "subgroups": [{ + "key": "machine_id | site_id | density | age", + "value": "...", + "n": int, + "auc": float, + "auc_rest": float, + "delta_vs_rest": float, + "delta_ci95": [float, float], + "mean_predicted": float, + "observed_rate": float, + "flagged": bool + }], + "pooled_metric": {"name": "auc", "value": float}, + "risk": {"level": "RED | YELLOW | GREEN", "reasons": [string]} } ``` Produce a report in exactly these sections: -1. **Overall risk** β€” one line: GREEN, YELLOW, or RED, with a one-sentence justification. RED if leakage is present or the random-vs-grouped gap is large; YELLOW if subgroup degradation is flagged without leakage; GREEN only if none of the checks fired. +1. **Overall risk** β€” one line that states risk.level verbatim (GREEN, YELLOW, or RED), then explain risk.reasons. Do not choose a different level. -2. **What the data shows** β€” 3-5 sentences, each tied to a specific number from the JSON, in plain reviewer language. +2. **What the data shows** β€” 3-5 sentences, each tied to a specific number from the JSON, in plain reviewer language. When you mention split_audit, describe it as a rescore of fixed predictions that cannot show inflation. 3. **What can be claimed** β€” the performance statements the results actually support. -4. **What cannot be claimed** β€” the statements the results do NOT support, and why (e.g., a metric measured under a leaking split does not reflect deployment). +4. **What cannot be claimed** β€” the statements the results do not support, and why. A metric measured while the same patients appear in train and test does not estimate performance on unseen patients. -5. **Next validation step** β€” the single most important experiment to run next, given the failure mode found. +5. **Next validation step** β€” the single most important experiment to run next. + - If risk.level is RED because patients overlap train and test: the next step is to retrain and evaluate with a patient-grouped split, so that no test patient was seen in training. Do not offer re-scoring the same predictions as a fix. + - If risk.level is YELLOW: the next step is to examine the flagged subgroup before making a pooled claim. + - If risk.level is GREEN: state that the checks that ran did not fire, and that this is not a clinical endorsement. Keep the whole report under 300 words. No preamble, no restating these instructions β€” start at section 1. diff --git a/prompts/opus-4-8/reviewer_critique_system_prompt.md b/prompts/opus-4-8/reviewer_critique_system_prompt.md index 277c624..3c8bcff 100644 --- a/prompts/opus-4-8/reviewer_critique_system_prompt.md +++ b/prompts/opus-4-8/reviewer_critique_system_prompt.md @@ -4,28 +4,51 @@ This is the system prompt for the reviewer-critique output. It receives the same --- -You are a peer reviewer for a clinical-AI journal, evaluating a manuscript that reports a medical-imaging machine-learning model. A deterministic analysis pipeline has already computed all metrics, statistical tests, and flags; you are given its results as JSON. Your job is to judge what these results actually support and to rewrite the study's evaluation so it is leakage-safe β€” not to recompute anything. +You are a peer reviewer for a clinical-AI journal, evaluating a manuscript that reports a medical-imaging machine-learning model. A deterministic analysis pipeline has already computed all metrics, intervals, flags, and the risk level; you are given its results as JSON. Your job is to judge what these results actually support and to rewrite the study's evaluation so it is leakage-safe. Do not recompute anything, and do not change the risk level. Rules: - Use only the numbers present in the provided JSON. Do not invent, estimate, or recompute a metric. If a value is absent, say the check did not run rather than guessing. +- State risk.level verbatim. Explain risk.reasons. Do not raise, lower, or rename the level. +- risk.level was computed in Python from the leakage count and the subgroup flags only. split_audit did not determine the level. +- Recommend Accept only when risk.level is GREEN. When risk.level is RED, recommend Reject or Major revision. When risk.level is YELLOW, recommend Major revision. - Do not soften genuine problems and do not inflate them. Name the finding, then its consequence for what the manuscript may claim. - Write for the authors, who trained the model, not for a lay audience. Assume they know AUC, calibration, and train/test splits. -- The corrected Methods paragraph must describe an evaluation the JSON's own findings would pass: a patient-grouped (or site-grouped) split with no patient spanning the train/test boundary, and per-subgroup reporting where degradation was flagged. Cite only figures that appear in the JSON; state changes in method, not invented results. +- split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, including training rows. sample_range95 is the spread of that sample AUC. gap is test_split_as_given minus patient_disjoint_sample. The comparison cannot show that leakage inflated a metric. Do not call either AUC a corrected, true, or retrained result. +- mean_predicted and observed_rate are calibration-in-the-large. They are not flags. auc_rest is the AUC on every row outside the subgroup, and delta_vs_rest is auc minus auc_rest. delta_ci95 is the patient-level 95% interval for that difference. +- The corrected Methods paragraph must describe an evaluation the JSON's own findings would pass. If patients overlap train and test, the fix is to retrain and evaluate with a patient-grouped split, so that no test patient was seen in training. Re-scoring the same predictions is not that fix. Where a subgroup is flagged, the paragraph reports that subgroup against the rest of the cohort. Cite only figures that appear in the JSON; state changes in method, not invented results. Input JSON shape (fields may be absent if a check did not run): ```json { "leakage": {"overlapping_patient_ids": int, "leakage_rate": float}, - "split_audit": {"metric": "auc", "random_split": float, "grouped_split": float, "gap": float}, - "subgroups": [{"key": "machine_id", "value": "...", "n": int, "auc": float, "delta_vs_pooled": float, "flagged": bool}], - "pooled_metric": {"name": "auc", "value": float} + "split_audit": { + "metric": "auc", + "test_split_as_given": float, + "patient_disjoint_sample": float, + "sample_range95": [float, float], + "gap": float + }, + "subgroups": [{ + "key": "machine_id | site_id | density | age", + "value": "...", + "n": int, + "auc": float, + "auc_rest": float, + "delta_vs_rest": float, + "delta_ci95": [float, float], + "mean_predicted": float, + "observed_rate": float, + "flagged": bool + }], + "pooled_metric": {"name": "auc", "value": float}, + "risk": {"level": "RED | YELLOW | GREEN", "reasons": [string]} } ``` Produce a critique in exactly these two sections: -1. **Reviewer verdict** β€” Open with a one-line recommendation: Accept, Major revision, or Reject, with a one-sentence reason. Then, in 3-5 sentences of plain reviewer prose, state what the reported numbers support and what they do not, each sentence tied to a specific figure from the JSON (e.g., an AUC measured under patient overlap does not estimate deployment performance). +1. **Reviewer verdict** β€” Open with one line: Accept only when risk.level is GREEN; Major revision when risk.level is YELLOW; Reject or Major revision when risk.level is RED. State risk.level verbatim and one sentence drawn from risk.reasons. Then, in 3-5 sentences of plain reviewer prose, state what the reported numbers support and what they do not, each sentence tied to a specific figure from the JSON. -2. **Corrected Methods paragraph** β€” A single paragraph the authors could paste into their Methods section describing a leakage-safe evaluation that addresses every flaw the JSON found. Write it as publishable Methods prose, not as instructions to the authors. +2. **Corrected Methods paragraph** β€” A single paragraph the authors could paste into their Methods section describing a leakage-safe evaluation that addresses every flaw the JSON found. If the flaw is patient overlap, the paragraph describes retraining and evaluating with a patient-grouped split so that no test patient was seen in training. Write it as publishable Methods prose, not as instructions to the authors. Keep the whole response under 350 words. No preamble, no restating these instructions β€” start at section 1. diff --git a/tests/test_app.py b/tests/test_app.py index 4983c68..d350c47 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,6 +1,6 @@ """Streamlit demo app: the pure scenario-listing helper. -The Streamlit UI itself is glue over already-tested functions (``_assemble_payload`` +The Streamlit UI itself is glue over already-tested functions (``run_checks`` and the two reporters), so only the pure helper is unit-tested here. Importing ``app`` must not require Streamlit β€” it is imported lazily inside ``main``. The upload boundary is ``validate_predictions``, tested in ``test_validators``. diff --git a/tests/test_cli.py b/tests/test_cli.py index 0904780..70b8c16 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -10,7 +10,8 @@ from pathlib import Path import cli -from cli import _assemble_payload, _load_predictions +from clinval_validators import run_checks +from cli import _load_predictions REPO_ROOT = Path(__file__).resolve().parent.parent DEMO_CSV = REPO_ROOT / "claude-science-artifacts" / "scenarios" / "weak_random_split_leakage.csv" @@ -34,10 +35,11 @@ def test_load_predictions_reads_the_table() -> None: assert {"patient_id", "machine_id", "site_id", "y_true", "y_prob", "split"} <= set(df.columns) -def test_assemble_payload_matches_contract_and_is_json_serializable() -> None: - payload = _assemble_payload(_load_predictions(DEMO_CSV)) - assert set(payload) == {"leakage", "split_audit", "subgroups", "pooled_metric"} +def test_run_checks_matches_contract_and_is_json_serializable() -> None: + payload = run_checks(_load_predictions(DEMO_CSV)) + assert set(payload) == {"leakage", "split_audit", "subgroups", "pooled_metric", "risk"} assert payload["leakage"]["overlapping_patient_ids"] == 940 + assert payload["risk"]["level"] == "RED" assert payload["pooled_metric"]["name"] == "auc" assert isinstance(payload["subgroups"], list) # The reporter serializes the payload with json.dumps; guard that boundary. diff --git a/tests/test_reporter.py b/tests/test_reporter.py index 2245680..d0d7f0a 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -8,7 +8,14 @@ import pytest import reporter -from reporter import MODEL, MissingAPIKeyError, render_report, render_reviewer_critique +from reporter import ( + CRITIQUE_SYSTEM_PROMPT_PATH, + MODEL, + SYSTEM_PROMPT_PATH, + MissingAPIKeyError, + render_report, + render_reviewer_critique, +) SAMPLE_PAYLOAD = { "leakage": {"overlapping_patient_ids": 940, "leakage_rate": 0.4702}, @@ -20,6 +27,25 @@ } +def test_prompts_name_the_fields_python_computes() -> None: + field_names = ( + "test_split_as_given", + "patient_disjoint_sample", + "sample_range95", + "auc_rest", + "delta_vs_rest", + "delta_ci95", + "mean_predicted", + "observed_rate", + "risk", + "reasons", + ) + for path in (SYSTEM_PROMPT_PATH, CRITIQUE_SYSTEM_PROMPT_PATH): + text = path.read_text(encoding="utf-8") + missing = [name for name in field_names if name not in text] + assert missing == [], path.name + + def test_render_report_sends_payload_json_to_opus(monkeypatch) -> None: monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") captured: dict = {} diff --git a/tests/test_validators.py b/tests/test_validators.py index ef9ac01..f7bcf8c 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -17,6 +17,7 @@ check_patient_leakage, check_subgroup_degradation, compute_pooled_metric, + compute_risk, validate_predictions, ) from leakage_generator import ( # noqa: E402 @@ -204,6 +205,24 @@ def test_subgroup_results_cover_machine_site_density_and_age( assert "50-59" in {sg["value"] for sg in subgroups if sg["key"] == "age"} +@pytest.mark.parametrize( + ("scenario", "level"), + [ + ("clean_patient_grouped", "GREEN"), + ("weak_random_split_leakage", "RED"), + ("subgroup_scanner_degradation", "YELLOW"), + ("subgroup_density_degradation", "YELLOW"), + ], +) +def test_risk_level_follows_leakage_then_subgroup_flags( + scenario: str, level: str, subgroups_by_scenario: dict[str, list] +) -> None: + risk = compute_risk(check_patient_leakage(_load(scenario)), subgroups_by_scenario[scenario]) + assert risk["level"] == level + assert risk["reasons"] + assert all(isinstance(reason, str) and reason for reason in risk["reasons"]) + + def test_fresh_seeds_flag_scanner_degradation_and_not_clean_tables() -> None: # Seeds 1–6 are the committed fixtures. These are other draws of the same # generator configs: five clean tables, five with M030 degraded to AUC 0.62. From 2b874c9cc0ef953ebe902ec748fc87d4cccfd606 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 13:10:12 +0200 Subject: [PATCH 06/20] Pass the API key into the reporter instead of the process environment app.py wrote the key from the password box into os.environ. That map is process-wide, so one Streamlit session could overwrite the key seen by another session in the same process. render_report and render_reviewer_critique now take an optional api_key and use it in place of ANTHROPIC_API_KEY. The UI passes the typed key and leaves the environment alone. The sample payload in the reporter test also used a leakage rate of 0.4702; on weak_random_split_leakage the rate is 0.9802. --- app.py | 3 +-- reporter/report.py | 32 ++++++++++++++---------- tests/test_reporter.py | 55 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/app.py b/app.py index 9458130..a33bc7e 100644 --- a/app.py +++ b/app.py @@ -126,10 +126,9 @@ def main() -> None: if generate and not api_key: st.warning("Set an Anthropic API key to render the report.", icon="πŸ”‘") elif generate: - os.environ[API_KEY_ENV] = api_key render = render_reviewer_critique if mode == "critique" else render_report with st.spinner(f"Asking claude-opus-4-8 for the {mode} report…"): - cached = (report_key, render(payload)) + cached = (report_key, render(payload, api_key=api_key)) st.session_state["report"] = cached if cached and cached[0] == report_key: diff --git a/reporter/report.py b/reporter/report.py index 8ad677f..1c0bbaa 100644 --- a/reporter/report.py +++ b/reporter/report.py @@ -55,18 +55,24 @@ def _load_system_prompt(path: Path) -> str: return text.split("---", 1)[1].strip() -def _render(payload: ReportPayload, system_prompt_path: Path) -> str: +def _render( + payload: ReportPayload, system_prompt_path: Path, api_key: str | None = None +) -> str: """Send the deterministic payload to ``claude-opus-4-8`` under one prompt. - Reads the Anthropic API key from the ``ANTHROPIC_API_KEY`` environment - variable, loads the operative system prompt from ``system_prompt_path``, sends - the payload as JSON, and returns the model's text. The key is never - hard-coded, and no number is computed here β€” every figure comes from the JSON. + ``api_key`` overrides ``ANTHROPIC_API_KEY`` when it is passed, so a browser + session can use the key typed into that session without writing + ``os.environ``. The key is never hard-coded, and no number is computed here. """ - try: - api_key = os.environ[API_KEY_ENV] - except KeyError as exc: - raise MissingAPIKeyError(f"{API_KEY_ENV} is not set in the environment.") from exc + if api_key is None: + try: + api_key = os.environ[API_KEY_ENV] + except KeyError as exc: + raise MissingAPIKeyError( + f"{API_KEY_ENV} is not set in the environment." + ) from exc + if not api_key: + raise MissingAPIKeyError(f"{API_KEY_ENV} is not set in the environment.") client = Anthropic(api_key=api_key) message = client.messages.create( @@ -78,11 +84,11 @@ def _render(payload: ReportPayload, system_prompt_path: Path) -> str: return message.content[0].text -def render_report(payload: ReportPayload) -> str: +def render_report(payload: ReportPayload, api_key: str | None = None) -> str: """Render the GREEN/YELLOW/RED Validation Risk Report from the payload.""" - return _render(payload, SYSTEM_PROMPT_PATH) + return _render(payload, SYSTEM_PROMPT_PATH, api_key) -def render_reviewer_critique(payload: ReportPayload) -> str: +def render_reviewer_critique(payload: ReportPayload, api_key: str | None = None) -> str: """Render a journal-reviewer critique plus a corrected Methods paragraph.""" - return _render(payload, CRITIQUE_SYSTEM_PROMPT_PATH) + return _render(payload, CRITIQUE_SYSTEM_PROMPT_PATH, api_key) diff --git a/tests/test_reporter.py b/tests/test_reporter.py index d0d7f0a..16e07dd 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import os from types import SimpleNamespace import pytest @@ -17,13 +18,38 @@ render_reviewer_critique, ) +# leakage_rate is 940 / 959 on weak_random_split_leakage. The other fields match +# the current payload contract; this dict is a stub, not a full scenario dump. SAMPLE_PAYLOAD = { - "leakage": {"overlapping_patient_ids": 940, "leakage_rate": 0.4702}, - "split_audit": {"metric": "auc", "random_split": 0.8273, "grouped_split": 0.8994, "gap": -0.0721}, + "leakage": {"overlapping_patient_ids": 940, "leakage_rate": 0.9802}, + "split_audit": { + "metric": "auc", + "test_split_as_given": 0.8273, + "patient_disjoint_sample": 0.8994, + "sample_range95": [0.7523, 0.9336], + "gap": -0.0721, + }, "subgroups": [ - {"key": "machine_id", "value": "M030", "n": 1048, "auc": 0.6243, "delta_vs_pooled": -0.21, "flagged": True} + { + "key": "machine_id", + "value": "M031", + "n": 1020, + "auc": 0.8075, + "auc_rest": 0.8490, + "delta_vs_rest": -0.0415, + "delta_ci95": [-0.1702, 0.0521], + "mean_predicted": 0.5111, + "observed_rate": 0.0216, + "flagged": False, + } ], "pooled_metric": {"name": "auc", "value": 0.8425}, + "risk": { + "level": "RED", + "reasons": [ + "940 patients appear in both the train and test splits (leakage rate 98.0%)." + ], + }, } @@ -78,6 +104,29 @@ def __init__(self, api_key=None): assert "# Reporter system prompt" not in captured["system"] +def test_api_key_argument_reaches_the_client_without_changing_the_environment(monkeypatch) -> None: + monkeypatch.setenv("ANTHROPIC_API_KEY", "env-key") + captured: dict = {} + + class FakeMessages: + def create(self, **kwargs): + captured.update(kwargs) + return SimpleNamespace(content=[SimpleNamespace(text="ok")]) + + class FakeAnthropic: + def __init__(self, api_key=None): + captured["api_key"] = api_key + self.messages = FakeMessages() + + monkeypatch.setattr(reporter.report, "Anthropic", FakeAnthropic) + + assert render_report(SAMPLE_PAYLOAD, api_key="input-key") == "ok" + assert captured["api_key"] == "input-key" + assert render_reviewer_critique(SAMPLE_PAYLOAD, api_key="critique-key") == "ok" + assert captured["api_key"] == "critique-key" + assert os.environ["ANTHROPIC_API_KEY"] == "env-key" + + def test_render_report_raises_a_named_error_when_the_key_is_absent(monkeypatch) -> None: # A missing key is a user-facing condition, not a bug: it must not surface as a # bare KeyError traceback. No network call is attempted. From 0ebec17947dcba2f1fcf40808193f8a8fa6efbc0 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 13:16:54 +0200 Subject: [PATCH 07/20] Document the risk rule the code actually applies The README still told Claude to judge whether a split gap was large, and it still described the second AUC as a grouped retest. Python now chooses RED, YELLOW, or GREEN from patient overlap and subgroup flags, and the split audit only rescores the predictions already in the file. The sample payload is the output of run_checks on weak_random_split_leakage, with the subgroup list marked as truncated. The report excerpt is hand-written and asks for a retrained patient-grouped evaluation. The demo script runs the clean control and then optimistic_site_shortcut, matching the recorded narration. The July 2026 planning notes move to docs/archive. --- README.md | 230 +++++++++--------- claude-science-artifacts/README.md | 38 ++- data/README.md | 22 +- demo/run_demo.sh | 4 +- docs/DEMO.md | 9 + .../archive/PROJECT_PLAN.md | 4 +- TASKS.md => docs/archive/TASKS.md | 4 +- docs/images/app-weak-random-split-leakage.png | Bin 71161 -> 150200 bytes 8 files changed, 163 insertions(+), 148 deletions(-) rename PROJECT_PLAN.md => docs/archive/PROJECT_PLAN.md (96%) rename TASKS.md => docs/archive/TASKS.md (96%) diff --git a/README.md b/README.md index ed83267..b2242da 100644 --- a/README.md +++ b/README.md @@ -1,152 +1,156 @@ # clinval-validator -Flag the failure modes that make a clinical-AI performance claim unsafe β€” -patient-level data leakage, weak evaluation splits, and subgroup degradation β€” -in a model's `predictions.csv`, then render a human-readable Validation Risk -Report. - -![ClinVal Validator browser UI flagging 940 patients shared across train and test in the weak_random_split_leakage scenario](docs/images/app-weak-random-split-leakage.png) - -*Offline audit of the bundled synthetic `weak_random_split_leakage` scenario in the -[browser UI](#browser-ui); no Claude report was requested. The fourth metric is AUC on -the patient-disjoint subset: its +0.072 delta compares two evaluation populations -under unchanged predictions β€” it is not a model improvement and does not mean the -leakage is fixed.* - -Built from scratch for **Built with Claude: Life Sciences** (Builder Track), -started 2026-07-07. - -**Python computes the metrics; Claude explains the risk.** Every metric, -statistical test, and pass/fail flag is computed by deterministic Python in -`clinval_validators/` (runs fully offline). Claude (`claude-opus-4-8`) is used only to -turn that structured JSON into prose in `reporter/` β€” it never computes a number. - -> **Disclaimer.** Synthetic / public-schema data only, modeled on the RSNA -> Screening Mammography schema. No PHI, no real patient data, no RSNA images in -> this repo. Not medical advice. - -**Status:** Phase 1 complete, plus the first Phase-2 feature. The four -deterministic checks (patient leakage, split-quality audit, subgroup -degradation, pooled AUC) and the Claude reporter are implemented and verified -against the labeled scenario manifests β€” `pytest` is green, and -`clinval-validate` runs end-to-end (a real `ANTHROPIC_API_KEY` is needed only for -the report step). A reviewer-critique mode (`--mode critique`) adds a -journal-reviewer critique and a corrected, leakage-safe Methods paragraph over -the same deterministic JSON. See [TASKS.md](TASKS.md) and -[demo/run_demo.sh](demo/run_demo.sh). - -## MVP scope (Phase 1) - -1. **Patient-level leakage check** β€” detect `patient_id` overlap between train - and test; report count and rate. -2. **Split-quality audit** β€” recompute the headline metric under a random split - vs a patient/site-grouped split; surface the gap. -3. **Subgroup degradation** β€” AUC per `machine_id` / `site_id`; flag subgroups - significantly below the pooled metric. -4. **Claude risk report** β€” feed the Phase-1 JSON to `claude-opus-4-8`; get a - GREEN / YELLOW / RED report: what can be claimed, what cannot, and the next - validation step. - -Everything else (bootstrap CIs, calibration plots, TRIPOD+AI / CLAIM mapping) is -Phase 2 β€” see [PROJECT_PLAN.md](PROJECT_PLAN.md). - -## Input schema - -Predictions CSV, modeled on the RSNA Screening Mammography schema: +ClinVal Validator reads a model's `predictions.csv` and reports two failure modes that make a clinical-AI performance claim unsafe: the same patient in both the train and test splits, and a subgroup whose AUC is lower than the rest of the table. Python computes the metrics, the flags, and the risk level. Claude is instructed to turn that JSON into prose and to cite only numbers present in it. Nothing in this repository checks the text Claude returns. + +![ClinVal Validator on weak_random_split_leakage: 940 leaked patients, test-split AUC 0.827, patient-disjoint sample AUC 0.899, and a RED banner](docs/images/app-weak-random-split-leakage.png) + +*Offline view of the bundled synthetic `weak_random_split_leakage` scenario. No API key was entered and no Claude report was requested. The fourth metric is the AUC of a patient sample drawn from all rows, including training rows. The +0.072 figure is that sample minus the test-split AUC. The RED line is the risk level computed by Python.* + +The column names follow the Radiological Society of North America (RSNA) Screening Mammography Breast Cancer Detection dataset. This repository contains synthetic tables and that public schema only. It contains no RSNA images and no protected health information (PHI). It is not medical advice. + +## Risk level + +`risk.level` is one of `RED`, `YELLOW`, or `GREEN`. + +- `RED` when `overlapping_patient_ids` is greater than zero: at least one `patient_id` is in both `split == "train"` and `split == "test"`. +- `YELLOW` when there is no such overlap and at least one subgroup is flagged. +- `GREEN` when neither of those is true. + +`risk.reasons` states the numbers that produced the level. The split audit is not an input to this rule. + +## Subgroup flag + +The check evaluates `machine_id` and `site_id`. When a `density` column is present it evaluates each density value. When an `age` column is present it evaluates decade bands such as `50-59`. Missing density or age values are left out of that grouping. + +A subgroup is evaluated when it has at least 50 rows and both the subgroup and the rest of the table contain class 0 and class 1. `delta_vs_rest` is the subgroup AUC minus the AUC of every other row. `delta_ci95` is the 2.5th and 97.5th percentile of that difference over a patient-level bootstrap: resample `patient_id` with replacement, 200 replicates, seed 0. Replicates that lack a class are skipped. + +A subgroup is flagged when `delta_vs_rest` is at or below βˆ’0.10 and the upper end of `delta_ci95` is below zero. + +`mean_predicted` (mean `y_prob`) and `observed_rate` (mean `y_true`) are calibration-in-the-large for that subgroup. They are reported and they do not flag. On `optimistic_site_shortcut`, mean `y_prob` by site runs from 0.331 to 0.754 while the observed positive rate runs from 0.97% to 2.3%. + +## Split audit + +`test_split_as_given` is the AUC on rows with `split == "test"`. `patient_disjoint_sample` is the AUC on a seed-0 sample of 15% of patients drawn from every row, so the sample includes training rows. `sample_range95` is the 2.5th and 97.5th percentile of that sample AUC over seeds 0–199. `gap` is `test_split_as_given` minus `patient_disjoint_sample`. + +Both AUCs use the predictions already in the file. The audit does not measure how much leakage changed a metric, and it does not set the risk level. + +## Input + +A predictions table needs these columns: ``` -patient_id, image_id, laterality, view, machine_id, site_id, age, density, y_true, y_prob, split +patient_id, machine_id, site_id, y_true, y_prob, split ``` -The validators consume the minimal subset `patient_id, machine_id, site_id, -y_true, y_prob, split` (plus `density` / `age` for subgroup checks). Labeled -fixtures live in -[`claude-science-artifacts/scenarios/`](claude-science-artifacts/scenarios/). +`split` may be `train`, `val`, or `test`. The leakage check compares `train` and `test` only. Rows with `split == "val"` are ignored by that check. `y_true` is 0 or 1. `y_prob` is a number in the closed interval from 0 to 1. The test rows must contain both classes. + +`density` and `age` are optional. The fixtures also carry `image_id`, `laterality`, and `view`. The checks ignore columns they do not name. + +The pooled AUC and the subgroup AUCs treat every row's `y_prob` as a held-out or out-of-fold prediction. A score that was produced on a row the model saw in training is not that quantity, and the file does not record which rows were held out beyond the `split` column. + +Labeled fixtures live in [`claude-science-artifacts/scenarios/`](claude-science-artifacts/scenarios/). + +An uploaded file that fails these checks is rejected before any metric runs. The command-line tool prints the problems on stderr and exits 2. The browser UI shows the same problems and stops. ## Quickstart ```bash -# 1. Install (editable). Requires Python 3.11+. -python -m pip install -e . - -# 2. Set your personal Anthropic API key (never commit it). -export ANTHROPIC_API_KEY=sk-ant-... +# Python 3.11+. Installs the package and pytest. +python -m pip install -e '.[dev]' -# 3. Validate a labeled scenario and print the GREEN / YELLOW / RED risk report. +# ANTHROPIC_API_KEY unset: the JSON payload is printed and the process exits 1. clinval-validate --predictions claude-science-artifacts/scenarios/weak_random_split_leakage.csv +echo "exit code: $?" -# Or ask for a journal-reviewer critique plus a corrected, leakage-safe Methods paragraph. -clinval-validate --predictions claude-science-artifacts/scenarios/weak_random_split_leakage.csv --mode critique +python -m pytest -q +``` -# Run the test suite. -python -m pytest +With `ANTHROPIC_API_KEY` set, the same command prints the risk report instead of the payload and exits 0. `--mode critique` asks for a reviewer critique and a Methods paragraph. The key is read from the environment. It is not stored in the repository. -# Launch the browser UI (no terminal needed after this line). -python -m pip install -e '.[demo]' && streamlit run app.py +```bash +python -m pip install -e '.[demo]' +streamlit run app.py ``` ## Browser UI -`streamlit run app.py` is the path a collaborator can run without you in the room: +`streamlit run app.py` runs the same checks. -- Pick a bundled scenario **or upload your own `predictions.csv`**. A missing - required column is reported by name β€” not as a stack trace. -- The deterministic checks run **offline and without an API key**: leakage count, - split audit, per-subgroup AUC, and the exact JSON that Claude would receive. -- Press **Generate with Claude** for the risk report or the reviewer critique. - Both are optional β€” the validation stands on its own. -- **Download** the payload as JSON and the report as Markdown. +- Choose a bundled scenario or upload a `predictions.csv`. +- The checks run without an API key. The page shows the leakage count, the split audit, and the JSON payload, including `risk.level`. +- The key typed into the sidebar is sent only with that session's request. It is not written into the process environment. +- **Generate with Claude** asks for the risk report or the reviewer critique. Both are optional. +- Download the payload as JSON and, after a report is generated, the report as Markdown. -## Sample output +## Sample payload -Running `--mode risk` on `weak_random_split_leakage`, the deterministic layer -computes this payload (this is what Python hands to Claude β€” no metric is ever -computed by the model): +`run_checks` on `weak_random_split_leakage` produces the payload below. The `subgroups` list is truncated: the command prints all 20 evaluated subgroups, and only the first is shown here. ```json { - "leakage": {"overlapping_patient_ids": 940, "leakage_rate": 0.9802}, - "split_audit": {"metric": "auc", "random_split": 0.8273, "grouped_split": 0.8994, "gap": -0.0721}, - "subgroups": [{"key": "site_id", "value": "S03", "n": 2125, "auc": 0.7896, "delta_vs_pooled": -0.0528, "flagged": false}], - "pooled_metric": {"name": "auc", "value": 0.8425} + "leakage": { + "overlapping_patient_ids": 940, + "leakage_rate": 0.9801876955161627 + }, + "split_audit": { + "metric": "auc", + "test_split_as_given": 0.8272804054054055, + "patient_disjoint_sample": 0.8993770139634801, + "sample_range95": [0.7523126406555013, 0.933559031020775], + "gap": -0.07209660855807465 + }, + "subgroups": [ + { + "key": "machine_id", + "value": "M031", + "n": 1020, + "auc": 0.8075241391874658, + "auc_rest": 0.8490112630891348, + "delta_vs_rest": -0.041487123901668976, + "delta_ci95": [-0.17020941694803463, 0.052104879910572145], + "mean_predicted": 0.511070955882353, + "observed_rate": 0.021568627450980392, + "flagged": false + } + ], + "pooled_metric": {"name": "auc", "value": 0.8424535490580416}, + "risk": { + "level": "RED", + "reasons": [ + "940 patients appear in both the train and test splits (leakage rate 98.0%)." + ] + } } ``` -Claude renders that into a reviewer-style report. An illustrative excerpt (the -wording is model-generated and varies run to run; the numbers are fixed by the -payload above): +The report below is hand-written so that a reader can see the shape. It is not output from Claude. ``` -1. Overall risk β€” RED. 940 patient IDs appear in both train and test, so the - reported metrics do not reflect held-out performance. +1. Overall risk β€” RED. 940 patients appear in both the train and test splits (leakage rate 98.0%). -2. What the data shows β€” The leakage check finds 940 shared patients (98.0% of - the test patients also appear in training). Pooled AUC is 0.84, but that - figure is measured across a contaminated split. No machine_id or site_id - subgroup falls more than 0.10 below pooled. +2. What the data shows β€” The leakage rate is 0.9802, which is 940 divided by the number of distinct test patients. Pooled AUC is 0.8425. The test-split AUC is 0.8273 and the patient-disjoint sample AUC is 0.8994 (gap βˆ’0.0721). The sample range over seeds 0–199 is 0.7523 to 0.9336. No subgroup is flagged. The first subgroup, machine M031, has AUC 0.8075 against 0.8490 on the rest of the table. -5. Next validation step β€” Re-evaluate the same predictions on a patient-grouped - split so no patient spans the train/test boundary, and report the grouped AUC - as the headline number. +3. What can be claimed β€” The file records 940 patients on both sides of the split. + +4. What cannot be claimed β€” An AUC from this file does not estimate performance on patients who were absent from training. + +5. Next validation step β€” Retrain and evaluate with a patient-grouped split, so that no test patient was seen in training. ``` -The full 3-minute demo script β€” synthetic opener, real RSNA cohort, clean -control, and the `--mode critique` close β€” is in [docs/DEMO.md](docs/DEMO.md). +The demo script is [docs/DEMO.md](docs/DEMO.md). -## Demo narrative (3-minute video) +## RSNA adapter -1. Run the validator on `weak_random_split_leakage` β€” the headline pooled AUC - (β‰ˆ 0.84) looks publishable. -2. The leakage check finds **940 patients shared across train and test** and - marks the random split as not leakage-safe; the split-quality audit - re-evaluates the same predictions on a patient-grouped fold. -3. Claude reads the deterministic JSON and explains, in reviewer language, why an - AUC measured under patient overlap does not reflect deployment, and what may - still be claimed. -4. Claude names the single next validation step (a leakage-safe grouped - re-evaluation). For contrast, `optimistic_site_shortcut` shows the same - failure at its most dramatic: AUC 0.9647 under a leaking split. +[`rsna_to_predictions.py`](rsna_to_predictions.py) is a local-only adapter. It reads a `train.csv` from the RSNA Screening Mammography Breast Cancer Detection dataset and writes a predictions table. The RSNA license bans redistribution, so `data/rsna/` and `predictions_rsna*.csv` are gitignored and are not part of this repository. + +```bash +python rsna_to_predictions.py --input data/rsna/train.csv +``` + +The default run writes `predictions_rsna_random.csv` and `predictions_rsna_patient_grouped.csv`. The scores in those files come from a tabular baseline on pre-imaging metadata, not from an imaging model. ## License -MIT β€” see [LICENSE](LICENSE). +MIT. See [LICENSE](LICENSE). + +July 2026 planning notes are in [docs/archive/](docs/archive/). diff --git a/claude-science-artifacts/README.md b/claude-science-artifacts/README.md index 670d8fd..647f4bd 100644 --- a/claude-science-artifacts/README.md +++ b/claude-science-artifacts/README.md @@ -1,17 +1,15 @@ # Synthetic RSNA-like predictions + leakage generator -Fixtures for a medical-imaging ML **validation** tool. Produces -`predictions.csv` tables that look like RSNA screening-mammography outputs and -**inject known, detectable flaws**, so a validator can be tested against ground -truth. +Fixtures for a medical-imaging validation tool. RSNA is the Radiological Society of North America Screening Mammography Breast Cancer Detection dataset. The generator writes `predictions.csv` tables with that column layout and injects known flaws, so a validator can be tested against ground truth. The rows are synthetic. ## Schema `patient_id, image_id, laterality, view, machine_id, site_id, age, density, y_true, y_prob, split` -- Screening exam β‰ˆ 4 images (L/R Γ— CC/MLO), with occasional extra/dropped views β†’ **non-uniform images per patient** (this is what makes image-level random splits leak). -- Machines are nested in sites (`M`); cancer label lives on the affected laterality; density (BI-RADS A–D) modestly raises risk and prevalence is screening-realistic (~1.5% image-level). -- `y_prob` is drawn so overall ROC-AUC hits a target (`AUC = Ξ¦(d/√2)`); AUC survives the sigmoid that maps latent scores to (0,1). +- A screening exam is about 4 images: left and right, each in two views. CC is the craniocaudal view and MLO is the mediolateral oblique view. Occasional extra or dropped views make the image count per patient uneven, which is what lets an image-level random split put one patient in both train and test. +- `machine_id` is the letter `M`, a two-digit site number, and a one-digit machine number. `M030` is site 03, machine 0. The cancer label is on the affected laterality. +- `density` is a Breast Imaging Reporting and Data System (BI-RADS) category, `A` through `D`. Denser categories carry a higher cancer risk in the generator. Image-level prevalence is about 1.5%. +- `d` is the separation, in standard deviations, between the mean latent score of positive rows and the mean latent score of negative rows. Scores are drawn so the ROC AUC is `Ξ¦(d/√2)`. The sigmoid that maps the latent score to a `y_prob` in (0, 1) does not change that AUC. ## Injected failure modes @@ -24,21 +22,32 @@ truth. | Optimistic metric | `optimistic_auc` | implausibly high headline AUC | | Site shortcut / batch effect | `site_shortcut_strength` | site-correlated nuisance signal | -Every table ships a `*.manifest.json` with `injected_flaws`, recomputed -`expected_detections` (overlap counts, target AUCs), and a `verification` -block confirming the flaw landed. +`leakage_generator.py` writes a `*.manifest.json` with `injected_flaws` and +`expected_detections` (overlap counts and target AUCs). The `verification` +block is added only by `make_scenarios.py`, after it recomputes those +quantities on the written table. A manifest from `leakage_generator.py` alone +does not contain `verification`. + +Expectations for the six committed scenarios are in +[`scenarios/_scenario_summary.csv`](scenarios/_scenario_summary.csv). ## Usage +Run these from `claude-science-artifacts/`. They need NumPy, pandas, and +scikit-learn. From the repository root, `python -m pip install -e .` installs +pandas and scikit-learn, and NumPy comes with them. `make_scenarios.py` +imports `leakage_generator` as a local module, so the working directory matters. + CLI: ```bash +cd claude-science-artifacts python leakage_generator.py --out predictions.csv \ --split-strategy random \ --degrade machine_id:M030:0.60 --degrade density:C,D:0.68 \ --optimistic-auc 0.98 ``` -Programmatic: +Programmatic, with `claude-science-artifacts/` on `sys.path`: ```python from leakage_generator import GeneratorConfig, Flaws, SubgroupDegradation, generate cfg = GeneratorConfig(seed=4, flaws=Flaws( @@ -47,8 +56,9 @@ cfg = GeneratorConfig(seed=4, flaws=Flaws( df, manifest = generate(cfg) ``` -Build + self-verify the full labeled suite: +Build the labeled suite and write the `verification` block: ```bash +cd claude-science-artifacts python make_scenarios.py # writes scenarios/*.csv + manifests + _scenario_summary.csv ``` @@ -57,3 +67,7 @@ python make_scenarios.py # writes scenarios/*.csv + manifests + _scenario_sum `clean_patient_grouped` (negative control), `weak_random_split_leakage`, `duplicate_row_leakage`, `subgroup_scanner_degradation`, `subgroup_density_degradation`, `optimistic_site_shortcut`. + +The committed CSVs and manifests are the test oracle. Regenerating them +replaces that oracle. + diff --git a/data/README.md b/data/README.md index 7172a4e..6d97964 100644 --- a/data/README.md +++ b/data/README.md @@ -1,23 +1,7 @@ # data/ -No fixtures are copied or regenerated here. The validators read directly from -the labeled scenario suite that already exists in -[`claude-science-artifacts/scenarios/`](../claude-science-artifacts/scenarios/), -which is the validators' ground-truth test oracle. +This folder contains this README. A local `data/rsna/` directory may hold `train.csv` from the Radiological Society of North America (RSNA) Screening Mammography Breast Cancer Detection dataset. That directory is gitignored. The RSNA license bans redistribution, so the raw table is not in the repository. -Each scenario ships a `*.csv`, a `*.manifest.json` (injected flaws + -`expected_detections`), and is indexed in `_scenario_summary.csv`. +The fixtures the validators read are in [`claude-science-artifacts/scenarios/`](../claude-science-artifacts/scenarios/). Each scenario is a `*.csv` plus a `*.manifest.json`. -## Demo scenario - -The demo runs on **`weak_random_split_leakage`** β€” a random split with a known -940 train/test patient overlap and pooled AUC 0.8425. It exercises the two -headline checks (patient leakage + split-quality gap) in one table. - -Contrast scenarios for the demo narrative: - -- `clean_patient_grouped` β€” negative control; every check should pass (GREEN). -- `optimistic_site_shortcut` β€” inflated AUC 0.9647 under a leaking random split; - the "looks great until you group the split" escalation. - -Synthetic / public-schema data only. No PHI. Never commit real patient data. +The demo in [docs/DEMO.md](../docs/DEMO.md) uses `optimistic_site_shortcut`. diff --git a/demo/run_demo.sh b/demo/run_demo.sh index 4805f4c..c16b240 100755 --- a/demo/run_demo.sh +++ b/demo/run_demo.sh @@ -20,5 +20,5 @@ run() { echo } -run clean_patient_grouped "expect GREEN β€” no leakage, no degraded subgroup" -run weak_random_split_leakage "expect a leakage flag: 940 shared train/test patients" +run clean_patient_grouped "expect GREEN β€” no leakage, no flagged subgroup" +run optimistic_site_shortcut "expect RED β€” 990 patients in both train and test" diff --git a/docs/DEMO.md b/docs/DEMO.md index 19f0ef1..313d0e4 100644 --- a/docs/DEMO.md +++ b/docs/DEMO.md @@ -1,5 +1,14 @@ # ClinVal Validator β€” 3-Minute Demo Script +Note (2026-10-18). The narration below is the script of a recorded video and is unchanged. Corrections since that recording: + +- The 0.9875 β†’ 0.9521 drop lies inside the sample range the tool now reports. +- The "grouped" AUC comes from a patient sample of all rows, not from a subset of the test split. +- The synthetic scenario scores are simulated, and only the RSNA scores come from a tabular baseline. +- The risk level is now computed in Python. +- Install `.[dev,demo]` before running pytest on camera. +- Produce predictions_rsna_random.csv with `python rsna_to_predictions.py --input data/rsna/train.csv`. + **Logline.** A clinical-AI model reports an AUC that looks publishable. ClinVal Validator answers whether that number is real or an artifact of data leakage β€” deterministic Python computes every flag, Claude only explains the risk. diff --git a/PROJECT_PLAN.md b/docs/archive/PROJECT_PLAN.md similarity index 96% rename from PROJECT_PLAN.md rename to docs/archive/PROJECT_PLAN.md index 8695e28..f33c78c 100644 --- a/PROJECT_PLAN.md +++ b/docs/archive/PROJECT_PLAN.md @@ -1,3 +1,5 @@ +Hackathon planning snapshot (July 2026). Not maintained; current status is in README.md. + # PROJECT_PLAN β€” clinval-validator ## Problem statement @@ -91,7 +93,7 @@ All AUCs via scikit-learn `roc_auc_score`. Grouping via pandas. No LLM. The reporter sends the assembled `ReportPayload` (JSON) to `claude-opus-4-8` under the reviewer system prompt at -[`prompts/opus-4-8/reporter_system_prompt.md`](prompts/opus-4-8/reporter_system_prompt.md), +[`prompts/opus-4-8/reporter_system_prompt.md`](../../prompts/opus-4-8/reporter_system_prompt.md), loaded from disk (never duplicated inline). That prompt pins the output: an Overall risk line (GREEN/YELLOW/RED), What the data shows, What can be claimed, What cannot be claimed, and the Next validation step β€” every figure drawn only diff --git a/TASKS.md b/docs/archive/TASKS.md similarity index 96% rename from TASKS.md rename to docs/archive/TASKS.md index 61d8ee6..12fc20e 100644 --- a/TASKS.md +++ b/docs/archive/TASKS.md @@ -1,3 +1,5 @@ +Hackathon planning snapshot (July 2026). Not maintained; current status is in README.md. + # TASKS β€” clinval-validator Every item names a concrete artifact. Phases match CLAUDE.md β€” Phase 1 is @@ -50,7 +52,7 @@ complete; Phase 2 items are optional and started only now that Phase 1 is done. - [x] `demo/run_demo.sh` β€” scripted path over `clean_patient_grouped` then `weak_random_split_leakage`. - [x] `README.md` β€” verified run commands and a sample-output snippet (real deterministic JSON + illustrative report). - [x] Fresh-clone reproducibility: `pip install -e .[dev]` on Python 3.11+ β†’ `python -m pytest` green (28 tests) β†’ `clinval-validate` runs. -- [ ] Record the <3-minute demo video (naive β†’ grouped collapse β†’ Claude explains β†’ next step). Script: [docs/DEMO.md](docs/DEMO.md). **Owner: user.** +- [ ] Record the <3-minute demo video (naive β†’ grouped collapse β†’ Claude explains β†’ next step). Script: [docs/DEMO.md](../DEMO.md). **Owner: user.** ## Phase 2 β€” Stretch (Phase 1 is done; these are optional) diff --git a/docs/images/app-weak-random-split-leakage.png b/docs/images/app-weak-random-split-leakage.png index a2cc0feefac24af16bb226273abcfb451d5949b6..c0f264d760d94818c1f33b3b0b3907a45380bf79 100644 GIT binary patch literal 150200 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 RK6(9d5+bs~MS}Wo{}0ARTb%#^ From b8a66c85cedfd11f368f13acaf6c3e328e2a5866 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 13:19:17 +0200 Subject: [PATCH 08/20] Record the checks and commands that are actually in the tree CLAUDE.md still described the validators, the CLI, and the UI as work left to build, and it still said a subgroup was compared with pooled AUC. Both entry points exist. Python sets the risk level from patient overlap and from subgroup flags against the rest of the table, and the split audit does not enter that rule. The command list is the install, pytest, clinval-validate, Streamlit, and the local RSNA adapter. Three gotchas are easy to miss from the code alone: scores are drawn before the split, the flag thresholds were calibrated on the committed fixtures, and only the prompt text after the first --- is sent. --- CLAUDE.md | 85 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3a52260..81a500f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,39 +14,48 @@ Named user: a clinical-AI researcher / radiology-lab PhD student who trained a m **Python computes. Claude explains.** -- All metrics, statistical tests, and pass/fail flags are computed by deterministic Python. Never ask an LLM to compute AUC, run a bootstrap, or decide a flag. +- All metrics, statistical tests, pass/fail flags, and the risk level are computed by deterministic Python. Never ask an LLM to compute AUC, run a bootstrap, or decide a flag or the risk level. - Claude (Anthropic API, `claude-opus-4-8`) is used **only** to turn the deterministic layer's structured JSON into prose: the risk report, reviewer-style critique, next-step suggestions. See the reporter contract in [prompts/opus-4-8/reporter_system_prompt.md](prompts/opus-4-8/reporter_system_prompt.md) β€” it may cite only numbers present in the JSON. - Layers stay cleanly separable: `clinval_validators/` (pure Python, no network, runs fully offline) and `reporter/` (the only code that touches the network). ## Repo map β€” current state -What exists: -- [claude-science-artifacts/](claude-science-artifacts/) β€” **working** synthetic-data generator + labeled test fixtures (this is the test oracle for the validators). - - `leakage_generator.py` β€” generates one RSNA-like `predictions.csv` with injected, manifest-verified flaws. - - `make_scenarios.py` β€” builds the full labeled suite in `scenarios/`. - - `scenarios/*.csv` + `*.manifest.json` + `_scenario_summary.csv` β€” 6 ground-truth-labeled scenarios. -- [prompts/](prompts/) β€” the two **runtime** system prompts the reporter loads (`reporter_system_prompt.md`, `reviewer_critique_system_prompt.md`); see [prompts/README.md](prompts/README.md). Load-bearing, not historical. +- [clinval_validators/](clinval_validators/) β€” deterministic checks. `run_checks` returns leakage, the split audit, subgroups, the pooled AUC, and `risk`. +- [cli.py](cli.py) β€” `clinval-validate`. Prints the payload and exits 1 when `ANTHROPIC_API_KEY` is unset. Exits 2 when `validate_predictions` reports a problem. +- [app.py](app.py) β€” Streamlit UI over the same checks. The typed API key is passed into the reporter and is not written to `os.environ`. +- [reporter/](reporter/) β€” the only code that calls the network. Loads the two runtime prompts in [prompts/opus-4-8/](prompts/opus-4-8/); see [prompts/README.md](prompts/README.md). +- [claude-science-artifacts/](claude-science-artifacts/) β€” synthetic-data generator and the committed scenario oracle (`scenarios/*.csv`, manifests, `_scenario_summary.csv`). Do not regenerate those fixtures to make a test pass. +- [rsna_to_predictions.py](rsna_to_predictions.py) β€” local-only adapter for an RSNA `train.csv`. Outputs are gitignored. +- [tests/](tests/) β€” unit tests, including fresh generator seeds for the subgroup flag. +- [docs/DEMO.md](docs/DEMO.md) β€” script of the recorded demo video. A dated note under the title lists later corrections. The narration is unchanged. - [AGENTS.md](AGENTS.md) β€” Codex/GPT behavioral rules (also govern Claude). -To build (MVP β€” see Scope): -- `clinval_validators/` β€” deterministic checks (leakage, split audit, subgroup degradation) β†’ structured JSON. -- `reporter/` β€” feeds validator JSON to Claude, renders the risk report. -- `data/` β€” the demo input table (copy/symlink a fixture from `claude-science-artifacts/scenarios/`). -- `tests/` β€” unit tests (see Definition of done). -- CLI **or** thin Streamlit demo β€” pick one, keep minimal. - ## Commands ```bash -# Generate the full labeled scenario suite (writes scenarios/*.csv + manifests + _scenario_summary.csv) -cd claude-science-artifacts && python3 make_scenarios.py +python -m pip install -e '.[dev,demo]' +python -m pytest -q +clinval-validate --predictions claude-science-artifacts/scenarios/optimistic_site_shortcut.csv +clinval-validate --predictions claude-science-artifacts/scenarios/optimistic_site_shortcut.csv --mode critique +streamlit run app.py +python rsna_to_predictions.py --input data/rsna/train.csv +``` + +`clinval-validate` without `ANTHROPIC_API_KEY` prints the JSON payload and exits 1. `--mode critique` asks for the reviewer critique. The RSNA command writes gitignored `predictions_rsna_random.csv` and `predictions_rsna_patient_grouped.csv`. + +New synthetic tables, not replacements for the committed oracle: -# Generate one custom table with specific injected flaws -python3 claude-science-artifacts/leakage_generator.py --out predictions.csv \ +```bash +cd claude-science-artifacts && python make_scenarios.py +python claude-science-artifacts/leakage_generator.py --out predictions.csv \ --split-strategy random --degrade machine_id:M030:0.60 --optimistic-auc 0.98 ``` -Once the MVP exists, document its real commands here (run validators, `pytest`, launch the demo). Do **not** add commands that don't run yet β€” a stale command is worse than none. +## Gotchas + +- `leakage_generator.generate` draws `y_prob` before it assigns splits, so injected leakage does not change the scores. +- `_FLAG_DELTA` (βˆ’0.10) and `_MIN_SUBGROUP_N` (50) were calibrated on the committed fixtures. Do not change them to make a test pass. +- `reporter/report.py` sends only the text after the first `---` of a prompt file. ## Data & schema @@ -62,12 +71,12 @@ Scenario suite (`_scenario_summary.csv`), each with a `*.manifest.json` recordin | Scenario | Flaw | Detectable signal | |---|---|---| -| `clean_patient_grouped` | none (negative control) | all checks pass | -| `weak_random_split_leakage` | patient overlap via random split | 940 train/test patient overlaps | -| `duplicate_row_leakage` | near-duplicate rows trainβ†’test | 113 duplicate-leaked patients | -| `subgroup_scanner_degradation` | one scanner AUC low | subgroup AUC 0.62 vs pooled 0.83 | -| `subgroup_density_degradation` | one density band AUC low | subgroup AUC 0.67 vs pooled 0.73 | -| `optimistic_site_shortcut` | inflated AUC + site shortcut | AUC 0.96 under a leaking random split | +| `clean_patient_grouped` | none (negative control) | risk GREEN; no subgroup flags | +| `weak_random_split_leakage` | patient overlap via random split | 940 train/test patient overlaps; risk RED | +| `duplicate_row_leakage` | near-duplicate rows trainβ†’test | 113 duplicate-leaked patients; risk RED | +| `subgroup_scanner_degradation` | scanner M030 AUC low | M030 about 0.624 vs about 0.873 on the rest; flagged. Site S03 is also flagged. Risk YELLOW | +| `subgroup_density_degradation` | density C and D target AUC 0.70 | density C about 0.670 vs about 0.792 on the rest; flagged. D is not (its delta is above βˆ’0.10). Risk YELLOW | +| `optimistic_site_shortcut` | high AUC, site shortcut, random split | 990 overlapping patients, so risk RED. Mean `y_prob` by site is about 0.33–0.75 against an observed rate of about 1–2%; that gap is reported and not flagged | Never commit real patient data. README must state: synthetic/public-schema only, no PHI, not medical advice. @@ -75,32 +84,30 @@ Never commit real patient data. README must state: synthetic/public-schema only, Judging rewards depth, a working demo, and creative Claude use β€” not feature count. Opus tends to over-engineer; resist it. -### Phase 1 β€” MVP (the submittable product; finish completely before Phase 2) -1. **Patient-level leakage check** β€” detect `patient_id` overlap between train/test; report count + rate. -2. **Split-quality audit** β€” recompute the headline metric under random vs patient/site-grouped split; surface the gap. -3. **Subgroup degradation** β€” AUC (+ calibration) per `machine_id`/`site_id`; flag subgroups significantly below pooled. -4. **Claude risk report** β€” feed Phase-1 JSON to the LLM; get a green/yellow/red report: what can be claimed, what cannot, the next validation step. - -Phase 1 must be demoable end-to-end and reproducible on a fresh clone. That is the bar. +### Phase 1 β€” MVP (in the tree) +1. **Patient-level leakage check** β€” `patient_id` overlap between train and test; count and rate. `val` is ignored. +2. **Split-quality audit** β€” AUC on the test rows as given, and AUC on a patient sample of all rows (that sample includes training rows), plus the seed 0–199 range of the sample AUC. Descriptive only. It does not set the risk level, because rescoring fixed predictions cannot show inflation. +3. **Subgroup degradation** β€” AUC against the rest of the table for `machine_id`, `site_id`, and, when the columns exist, `density` and decade age bands. Flag when the drop is at least 0.10 and the upper end of a patient-level 95% interval is below zero. `mean_predicted` and `observed_rate` are reported and do not flag. Pooled AUC assumes every row's `y_prob` is a held-out or out-of-fold prediction. +4. **Risk level** β€” Python sets `RED` when any patient is in both train and test, otherwise `YELLOW` when a subgroup is flagged, otherwise `GREEN`. Claude is instructed to state `risk.level` verbatim and explain `risk.reasons`. For leakage, the next step it is instructed to name is a retrained patient-grouped evaluation, not another score of the same predictions. -### Phase 2 β€” only if Phase 1 is fully done and time remains (each optional) -5. Bootstrap confidence intervals. 6. Calibration plots per subgroup. 7. Reporting-completeness checklist mapped to TRIPOD+AI and CLAIM. 8. Reviewer-style critique + corrected Methods paragraph. 9. Reproducibility checklist output. +Phase 1 runs offline through the payload. The report step needs `ANTHROPIC_API_KEY`. -Do not start Phase 2 while any Phase 1 item is incomplete. +### Phase 2 β€” not built +Calibration plots per subgroup. A reporting checklist mapped to TRIPOD+AI and CLAIM. A reproducibility checklist. The reviewer critique (`--mode critique`) and the subgroup interval are already in Phase 1. ## Anti-over-engineering - Build only what this file asks for. No speculative configurability, plugin systems, or abstractions "for later." -- No defensive error handling for scenarios that cannot occur. Trust the fixed input schema. +- No defensive error handling for scenarios that cannot occur. `validate_predictions` is the boundary for a CSV the tool did not write. After it accepts a table, the checks trust that schema. - Comment only where logic isn't self-evident; don't document code you didn't write. - A single-file validator beats a five-file framework if it does the job. ## Constraints - Open source, **MIT**. All repo work must be original, started during the hackathon. -- Credits run on a **personal** claude.ai account (institutional accounts ineligible). Never hardcode an API key β€” read from env. -- Stack: Python 3.11+, pandas, scikit-learn for metrics. Streamlit **or** a thin CLI β€” pick one. -- The 3-minute demo video is 30% of judging. Design the demo path first: naive split looks good β†’ grouped split collapses it β†’ Claude explains why the claim is unsafe β†’ corrected next step. Build toward that narrative. +- Credits run on a **personal** claude.ai account (institutional accounts ineligible). Never hardcode an API key. The CLI reads `ANTHROPIC_API_KEY`. The UI passes a typed key into the reporter for that call and does not write `os.environ`. +- Stack: Python 3.11+, pandas, scikit-learn for metrics. Both the CLI and the Streamlit UI are in the tree. +- The 3-minute demo video is 30% of judging. The recorded script is [docs/DEMO.md](docs/DEMO.md): `optimistic_site_shortcut`, then the local RSNA table, then `clean_patient_grouped`, then the critique. The risk level on screen is the Python level. ## Definition of done From 6974de969c72f2f2df913cd7a34846c70043c9f6 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 13:20:38 +0200 Subject: [PATCH 09/20] Run the test suite on Python 3.11 and 3.13 A fresh clone had no check that the validators still pass on the two Python versions the project claims. The workflow installs the dev extra and runs pytest. It does not use secrets, and it does not call the Anthropic API. --- .github/workflows/tests.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..f8c5c22 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,19 @@ +name: tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: pip install -e '.[dev]' + - run: python -m pytest -q From ff52ad946a5d969930b75c236159a7f7c7eb58b6 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 13:32:33 +0200 Subject: [PATCH 10/20] Record measured fresh-seed subgroup outcomes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fresh-seed test asserted that every degraded table flagged M030 and that every clean table was quiet. Its seed lists skipped the misses and the false alarms: M030 is missed on 4 of 30 scanner tables, and 6 of 30 clean tables receive a flag. Seeds 7–11 are contiguous and were fixed before the assertions. The test now records those outcomes: M030 is flagged on 7, 8, and 9 and not on 10 and 11; clean seeds 8 and 10 each flag age 80-89, and 7, 9, and 11 flag nothing. The README states the 30-seed rates from the bundled generator. --- README.md | 2 ++ tests/test_validators.py | 45 +++++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b2242da..bb2632e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ A subgroup is evaluated when it has at least 50 rows and both the subgroup and t A subgroup is flagged when `delta_vs_rest` is at or below βˆ’0.10 and the upper end of `delta_ci95` is below zero. +On 30 tables from the bundled generator (seeds 7–36), 6 of 30 clean patient-grouped tables received a flag, and the injected M030 drop was missed in 4 of 30 scanner tables. These rates come from that generator, not from real data. + `mean_predicted` (mean `y_prob`) and `observed_rate` (mean `y_true`) are calibration-in-the-large for that subgroup. They are reported and they do not flag. On `optimistic_site_shortcut`, mean `y_prob` by site runs from 0.331 to 0.754 while the observed positive rate runs from 0.97% to 2.3%. ## Split audit diff --git a/tests/test_validators.py b/tests/test_validators.py index f7bcf8c..96765a5 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -147,9 +147,22 @@ def test_pooled_metric_matches_manifest_overall_auc(scenario: str, expected_auc: # --- Check 3: subgroup degradation --------------------------------------- -# make_scenarios.py builds the committed fixtures with seeds 1 through 6. -_FRESH_CLEAN_SEEDS = (11, 12, 13, 14, 15) -_FRESH_SCANNER_SEEDS = (7, 8, 9, 12, 13) +# Seeds 7–11 were fixed before these outcomes were recorded. make_scenarios.py +# uses seeds 1–6. On seeds 7–36 (30 tables) this branch misses M030 on 4 +# scanner tables (seeds 10, 11, 21, 22) and flags 6 clean tables (seeds 8, 10, +# 19, 20, 30, 32). The subgroup logic on main misses M030 on 2 of 30 (seeds +# 10, 22) and flags 6 of 30 clean tables (seeds 17, 19, 20, 25, 29, 30). A +# change in the outcomes below means the detection behaviour changed and must +# be reported in the commit message, not fixed by choosing other seeds. +_FRESH_SEEDS = (7, 8, 9, 10, 11) +_CLEAN_FLAGS = { + 7: set(), + 8: {("age", "80-89")}, + 9: set(), + 10: {("age", "80-89")}, + 11: set(), +} +_SCANNER_M030_FLAGGED = {7: True, 8: True, 9: True, 10: False, 11: False} @pytest.fixture(scope="module") @@ -223,30 +236,28 @@ def test_risk_level_follows_leakage_then_subgroup_flags( assert all(isinstance(reason, str) and reason for reason in risk["reasons"]) -def test_fresh_seeds_flag_scanner_degradation_and_not_clean_tables() -> None: - # Seeds 1–6 are the committed fixtures. These are other draws of the same - # generator configs: five clean tables, five with M030 degraded to AUC 0.62. - used_by_make_scenarios = {1, 2, 3, 4, 5, 6} - assert used_by_make_scenarios.isdisjoint(_FRESH_CLEAN_SEEDS) - assert used_by_make_scenarios.isdisjoint(_FRESH_SCANNER_SEEDS) - - for seed in _FRESH_CLEAN_SEEDS: +def test_fresh_seed_subgroup_outcomes_are_recorded() -> None: + """Record per-seed flags. Do not retune the seeds when a flag moves.""" + for seed in _FRESH_SEEDS: df, _manifest = generate(GeneratorConfig(seed=seed, flaws=Flaws(split_strategy="patient"))) - flagged = [sg for sg in check_subgroup_degradation(df) if sg["flagged"]] - assert flagged == [] + flagged = { + (sg["key"], sg["value"]) for sg in check_subgroup_degradation(df) if sg["flagged"] + } + assert flagged == _CLEAN_FLAGS[seed] degradation = SubgroupDegradation("machine_id", ["M030"], target_auc=0.62) - for seed in _FRESH_SCANNER_SEEDS: + for seed in _FRESH_SEEDS: df, _manifest = generate( GeneratorConfig( seed=seed, flaws=Flaws(split_strategy="patient", subgroup_degradations=[degradation]), ) ) - subgroups = check_subgroup_degradation(df) - assert any( - sg["key"] == "machine_id" and sg["value"] == "M030" and sg["flagged"] for sg in subgroups + m030_flagged = any( + sg["key"] == "machine_id" and sg["value"] == "M030" and sg["flagged"] + for sg in check_subgroup_degradation(df) ) + assert m030_flagged is _SCANNER_M030_FLAGGED[seed] # --- Check 2: split-quality audit ---------------------------------------- From ef678dab67936f9abf8c279d71a0b47752dcf3a4 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 13:36:36 +0200 Subject: [PATCH 11/20] Record the density detection rate and why Bonferroni was not adopted The README gave the false-alarm and M030 miss rates but not how often the density drop is caught, and it did not say what a flag means at 2,000 patients. A flag is a candidate drop to confirm on more data, and a missing flag does not clear a subgroup. A one-sided Bonferroni correction across subgroups was measured on seeds 7-36 and rejected: clean-table flags fell from 6 to 3 of 30, but M030 detection fell from 26 to 21 of 30, density detection from 21 to 12 of 30, and the committed density scenario went unflagged. CLAUDE.md records this so the correction is not added again without re-measuring. --- CLAUDE.md | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 81a500f..cc4d891 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,6 +56,7 @@ python claude-science-artifacts/leakage_generator.py --out predictions.csv \ - `leakage_generator.generate` draws `y_prob` before it assigns splits, so injected leakage does not change the scores. - `_FLAG_DELTA` (βˆ’0.10) and `_MIN_SUBGROUP_N` (50) were calibrated on the committed fixtures. Do not change them to make a test pass. - `reporter/report.py` sends only the text after the first `---` of a prompt file. +- A one-sided Bonferroni correction across subgroups was measured on generator seeds 7–36 and not adopted. It cut clean-table flags from 6 to 3 of 30, but M030 detection fell from 26 to 21 of 30 and density detection from 21 to 12 of 30, and `subgroup_density_degradation` was no longer flagged. Re-measure on the same seeds before changing the flag rule. ## Data & schema diff --git a/README.md b/README.md index bb2632e..3acbc27 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ A subgroup is evaluated when it has at least 50 rows and both the subgroup and t A subgroup is flagged when `delta_vs_rest` is at or below βˆ’0.10 and the upper end of `delta_ci95` is below zero. -On 30 tables from the bundled generator (seeds 7–36), 6 of 30 clean patient-grouped tables received a flag, and the injected M030 drop was missed in 4 of 30 scanner tables. These rates come from that generator, not from real data. +On 30 tables from the bundled generator (seeds 7–36; 2,000 patients and about 1.5% positive images per table), 6 of 30 clean patient-grouped tables received a flag, the injected M030 drop was missed in 4 of 30 scanner tables, and the injected density C/D drop was flagged in 21 of 30 tables. These rates come from that generator, not from real data. At this sample size a flag is a candidate drop to confirm on more data, and the absence of a flag does not show that every subgroup performs like the rest. `mean_predicted` (mean `y_prob`) and `observed_rate` (mean `y_true`) are calibration-in-the-large for that subgroup. They are reported and they do not flag. On `optimistic_site_shortcut`, mean `y_prob` by site runs from 0.331 to 0.754 while the observed positive rate runs from 0.97% to 2.3%. From a2d798594c9f1a8eac5287cb3bb1d21a427ce48e Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 14:13:15 +0200 Subject: [PATCH 12/20] Count subgroups and label the pooled AUC in Python A live run of the reporter (24 reports per model) showed Claude counting the subgroup list wrong ("22 subgroups" for 21), claiming every interval spans zero when some did not, and calling the pooled AUC a test-set AUC. Each of these is a number or label Python can supply. The payload gains subgroup_summary: subgroups evaluated, subgroups flagged, and how many delta_ci95 intervals lie entirely below or entirely above zero. pooled_metric gains rows: "all", and both prompts say the pooled AUC covers train, val and test rows and is not the test-split AUC. --- README.md | 10 +++++- clinval_validators/__init__.py | 2 ++ clinval_validators/checks.py | 31 +++++++++++++++++-- prompts/opus-4-8/reporter_system_prompt.md | 6 ++-- .../reviewer_critique_system_prompt.md | 5 ++- tests/test_cli.py | 16 ++++++++-- tests/test_reporter.py | 11 ++++++- tests/test_validators.py | 21 +++++++++++++ 8 files changed, 93 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3acbc27..339a2a9 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ On 30 tables from the bundled generator (seeds 7–36; 2,000 patients and about `mean_predicted` (mean `y_prob`) and `observed_rate` (mean `y_true`) are calibration-in-the-large for that subgroup. They are reported and they do not flag. On `optimistic_site_shortcut`, mean `y_prob` by site runs from 0.331 to 0.754 while the observed positive rate runs from 0.97% to 2.3%. +`subgroup_summary` counts the evaluated and flagged subgroups and the `delta_ci95` intervals that lie entirely below or entirely above zero. `pooled_metric.rows` is `"all"`: the pooled AUC covers train, val and test rows, so it is not the test-split AUC. Python supplies both because live reports from Claude miscounted subgroups and called the pooled AUC a test-set result. + ## Split audit `test_split_as_given` is the AUC on rows with `split == "test"`. `patient_disjoint_sample` is the AUC on a seed-0 sample of 15% of patients drawn from every row, so the sample includes training rows. `sample_range95` is the 2.5th and 97.5th percentile of that sample AUC over seeds 0–199. `gap` is `test_split_as_given` minus `patient_disjoint_sample`. @@ -115,7 +117,13 @@ streamlit run app.py "flagged": false } ], - "pooled_metric": {"name": "auc", "value": 0.8424535490580416}, + "subgroup_summary": { + "evaluated": 20, + "flagged": 0, + "interval_below_zero": 0, + "interval_above_zero": 1 + }, + "pooled_metric": {"name": "auc", "rows": "all", "value": 0.8424535490580416}, "risk": { "level": "RED", "reasons": [ diff --git a/clinval_validators/__init__.py b/clinval_validators/__init__.py index f5d90e2..fb69a09 100644 --- a/clinval_validators/__init__.py +++ b/clinval_validators/__init__.py @@ -7,6 +7,7 @@ RiskResult, SplitAuditResult, SubgroupResult, + SubgroupSummary, audit_split_quality, check_patient_leakage, check_subgroup_degradation, @@ -23,6 +24,7 @@ "RiskResult", "SplitAuditResult", "SubgroupResult", + "SubgroupSummary", "audit_split_quality", "check_patient_leakage", "check_subgroup_degradation", diff --git a/clinval_validators/checks.py b/clinval_validators/checks.py index 00002d6..ca55cb3 100644 --- a/clinval_validators/checks.py +++ b/clinval_validators/checks.py @@ -30,6 +30,7 @@ "LeakageResult", "SplitAuditResult", "SubgroupResult", + "SubgroupSummary", "PooledMetric", "ReportPayload", "validate_predictions", @@ -73,8 +74,16 @@ class SubgroupResult(TypedDict): flagged: bool +class SubgroupSummary(TypedDict): + evaluated: int + flagged: int + interval_below_zero: int + interval_above_zero: int + + class PooledMetric(TypedDict): name: str + rows: str value: float @@ -87,6 +96,7 @@ class ReportPayload(TypedDict): leakage: LeakageResult split_audit: SplitAuditResult subgroups: list[SubgroupResult] + subgroup_summary: SubgroupSummary pooled_metric: PooledMetric risk: RiskResult @@ -354,9 +364,25 @@ def compute_pooled_metric(df: pd.DataFrame) -> PooledMetric: Assumes every row's ``y_prob`` is a held-out or out-of-fold prediction. AUC is computed over the full table (not the test split alone) to match - each manifest's ``verification.overall_auc``. + each manifest's ``verification.overall_auc``. ``rows`` says so in the + payload, so the reporter does not present it as the test-split AUC. """ - return {"name": "auc", "value": float(roc_auc_score(df["y_true"], df["y_prob"]))} + return { + "name": "auc", + "rows": "all", + "value": float(roc_auc_score(df["y_true"], df["y_prob"])), + } + + +def _summarize_subgroups(subgroups: list[SubgroupResult]) -> SubgroupSummary: + """Counts the reporter would otherwise have to tally from the list itself.""" + intervals = [sg["delta_ci95"] for sg in subgroups if sg["delta_ci95"] is not None] + return { + "evaluated": len(subgroups), + "flagged": sum(sg["flagged"] for sg in subgroups), + "interval_below_zero": sum(high < 0 for _, high in intervals), + "interval_above_zero": sum(low > 0 for low, _ in intervals), + } def compute_risk(leakage: LeakageResult, subgroups: list[SubgroupResult]) -> RiskResult: @@ -409,6 +435,7 @@ def run_checks(df: pd.DataFrame) -> ReportPayload: "leakage": leakage, "split_audit": audit_split_quality(df), "subgroups": subgroups, + "subgroup_summary": _summarize_subgroups(subgroups), "pooled_metric": compute_pooled_metric(df), "risk": compute_risk(leakage, subgroups), } diff --git a/prompts/opus-4-8/reporter_system_prompt.md b/prompts/opus-4-8/reporter_system_prompt.md index e7310c4..1dac6d1 100644 --- a/prompts/opus-4-8/reporter_system_prompt.md +++ b/prompts/opus-4-8/reporter_system_prompt.md @@ -14,7 +14,8 @@ Rules: - Write for a researcher who trained the model, not for a lay audience. Assume they know AUC, calibration, and train/test splits. - split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, so that sample includes training rows. sample_range95 is the 2.5th and 97.5th percentile of that sample AUC over seeds 0–199. gap is test_split_as_given minus patient_disjoint_sample. The comparison cannot show that leakage inflated a metric. Do not call either AUC a corrected, true, or retrained result. - mean_predicted and observed_rate are calibration-in-the-large for a subgroup (mean predicted probability and the observed positive rate). Mention them when you discuss calibration. They are not flags. -- pooled_metric assumes every row's y_prob is a held-out or out-of-fold prediction. +- pooled_metric is the AUC over all rows (rows: "all" means train, val and test together) and assumes every row's y_prob is a held-out or out-of-fold prediction. It is not the test-split AUC; that is split_audit.test_split_as_given. +- subgroup_summary gives the counts: subgroups evaluated, subgroups flagged, and how many delta_ci95 intervals lie entirely below zero or entirely above zero. Take counts from it instead of counting the subgroups list. Say that every interval spans zero only when interval_below_zero and interval_above_zero are both 0. Input JSON shape (fields may be absent if a check did not run): ```json @@ -39,7 +40,8 @@ Input JSON shape (fields may be absent if a check did not run): "observed_rate": float, "flagged": bool }], - "pooled_metric": {"name": "auc", "value": float}, + "subgroup_summary": {"evaluated": int, "flagged": int, "interval_below_zero": int, "interval_above_zero": int}, + "pooled_metric": {"name": "auc", "rows": "all", "value": float}, "risk": {"level": "RED | YELLOW | GREEN", "reasons": [string]} } ``` diff --git a/prompts/opus-4-8/reviewer_critique_system_prompt.md b/prompts/opus-4-8/reviewer_critique_system_prompt.md index 3c8bcff..fb259b2 100644 --- a/prompts/opus-4-8/reviewer_critique_system_prompt.md +++ b/prompts/opus-4-8/reviewer_critique_system_prompt.md @@ -15,6 +15,8 @@ Rules: - Write for the authors, who trained the model, not for a lay audience. Assume they know AUC, calibration, and train/test splits. - split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, including training rows. sample_range95 is the spread of that sample AUC. gap is test_split_as_given minus patient_disjoint_sample. The comparison cannot show that leakage inflated a metric. Do not call either AUC a corrected, true, or retrained result. - mean_predicted and observed_rate are calibration-in-the-large. They are not flags. auc_rest is the AUC on every row outside the subgroup, and delta_vs_rest is auc minus auc_rest. delta_ci95 is the patient-level 95% interval for that difference. +- pooled_metric is the AUC over all rows (rows: "all" means train, val and test together) and assumes every row's y_prob is a held-out or out-of-fold prediction. It is not the test-split AUC; that is split_audit.test_split_as_given. +- subgroup_summary gives the counts: subgroups evaluated, subgroups flagged, and how many delta_ci95 intervals lie entirely below zero or entirely above zero. Take counts from it instead of counting the subgroups list. Say that every interval spans zero only when interval_below_zero and interval_above_zero are both 0. - The corrected Methods paragraph must describe an evaluation the JSON's own findings would pass. If patients overlap train and test, the fix is to retrain and evaluate with a patient-grouped split, so that no test patient was seen in training. Re-scoring the same predictions is not that fix. Where a subgroup is flagged, the paragraph reports that subgroup against the rest of the cohort. Cite only figures that appear in the JSON; state changes in method, not invented results. Input JSON shape (fields may be absent if a check did not run): @@ -40,7 +42,8 @@ Input JSON shape (fields may be absent if a check did not run): "observed_rate": float, "flagged": bool }], - "pooled_metric": {"name": "auc", "value": float}, + "subgroup_summary": {"evaluated": int, "flagged": int, "interval_below_zero": int, "interval_above_zero": int}, + "pooled_metric": {"name": "auc", "rows": "all", "value": float}, "risk": {"level": "RED | YELLOW | GREEN", "reasons": [string]} } ``` diff --git a/tests/test_cli.py b/tests/test_cli.py index 70b8c16..72dddce 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -37,10 +37,22 @@ def test_load_predictions_reads_the_table() -> None: def test_run_checks_matches_contract_and_is_json_serializable() -> None: payload = run_checks(_load_predictions(DEMO_CSV)) - assert set(payload) == {"leakage", "split_audit", "subgroups", "pooled_metric", "risk"} + assert set(payload) == { + "leakage", + "split_audit", + "subgroups", + "subgroup_summary", + "pooled_metric", + "risk", + } assert payload["leakage"]["overlapping_patient_ids"] == 940 assert payload["risk"]["level"] == "RED" - assert payload["pooled_metric"]["name"] == "auc" + assert payload["pooled_metric"] == { + "name": "auc", + "rows": "all", + "value": payload["pooled_metric"]["value"], + } + assert payload["subgroup_summary"]["evaluated"] == len(payload["subgroups"]) assert isinstance(payload["subgroups"], list) # The reporter serializes the payload with json.dumps; guard that boundary. assert json.loads(json.dumps(payload)) == payload diff --git a/tests/test_reporter.py b/tests/test_reporter.py index 16e07dd..4b0ff89 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -43,7 +43,13 @@ "flagged": False, } ], - "pooled_metric": {"name": "auc", "value": 0.8425}, + "subgroup_summary": { + "evaluated": 20, + "flagged": 0, + "interval_below_zero": 0, + "interval_above_zero": 1, + }, + "pooled_metric": {"name": "auc", "rows": "all", "value": 0.8425}, "risk": { "level": "RED", "reasons": [ @@ -63,6 +69,9 @@ def test_prompts_name_the_fields_python_computes() -> None: "delta_ci95", "mean_predicted", "observed_rate", + "subgroup_summary", + "interval_below_zero", + "interval_above_zero", "risk", "reasons", ) diff --git a/tests/test_validators.py b/tests/test_validators.py index 96765a5..b7c5e6a 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -18,6 +18,7 @@ check_subgroup_degradation, compute_pooled_metric, compute_risk, + run_checks, validate_predictions, ) from leakage_generator import ( # noqa: E402 @@ -142,9 +143,29 @@ def test_detects_optimistic_site_shortcut_leakage() -> None: def test_pooled_metric_matches_manifest_overall_auc(scenario: str, expected_auc: float) -> None: result = compute_pooled_metric(_load(scenario)) assert result["name"] == "auc" + assert result["rows"] == "all" assert result["value"] == pytest.approx(expected_auc, abs=1e-4) +def test_subgroup_summary_counts_the_subgroup_list() -> None: + # Live reports miscounted subgroups and claimed every interval spans zero, + # so the payload carries these counts instead of leaving them to Claude. + payload = run_checks(_load("subgroup_density_degradation")) + subgroups = payload["subgroups"] + assert payload["subgroup_summary"] == { + "evaluated": len(subgroups), + "flagged": sum(sg["flagged"] for sg in subgroups), + "interval_below_zero": sum(sg["delta_ci95"][1] < 0 for sg in subgroups), + "interval_above_zero": sum(sg["delta_ci95"][0] > 0 for sg in subgroups), + } + assert payload["subgroup_summary"] == { + "evaluated": 20, + "flagged": 1, + "interval_below_zero": 1, + "interval_above_zero": 2, + } + + # --- Check 3: subgroup degradation --------------------------------------- # Seeds 7–11 were fixed before these outcomes were recorded. make_scenarios.py From e4e021f6215e4acf66b46b38a9a47101dbc1e159 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 14:15:12 +0200 Subject: [PATCH 13/20] Read every text block and fail readably on a cut-off or refused report report.py returned message.content[0].text. A model that thinks puts a thinking block first, so a live run with Claude Haiku 5.5 would have raised AttributeError on every report. The reporter now joins the text blocks. It also raises ReportNotRenderedError when the reply stops at max_tokens, is refused, or holds no text. The CLI prints the payload and the stop reason and exits 1; the UI shows the message and keeps the payload. max_tokens rises from 1024 to 4096: the longest live Opus 4.8 report used 998 output tokens, and only generated tokens are billed. The UI spinner names the model from MODEL instead of a hard-coded string. --- CLAUDE.md | 2 +- README.md | 2 +- app.py | 17 ++++++-- cli.py | 16 +++++++- reporter/__init__.py | 4 ++ reporter/report.py | 25 +++++++++++- tests/test_cli.py | 15 +++++++ tests/test_reporter.py | 90 ++++++++++++++++++++++++------------------ 8 files changed, 123 insertions(+), 48 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cc4d891..f2475ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ Named user: a clinical-AI researcher / radiology-lab PhD student who trained a m ## Repo map β€” current state - [clinval_validators/](clinval_validators/) β€” deterministic checks. `run_checks` returns leakage, the split audit, subgroups, the pooled AUC, and `risk`. -- [cli.py](cli.py) β€” `clinval-validate`. Prints the payload and exits 1 when `ANTHROPIC_API_KEY` is unset. Exits 2 when `validate_predictions` reports a problem. +- [cli.py](cli.py) β€” `clinval-validate`. Prints the payload and exits 1 when `ANTHROPIC_API_KEY` is unset or Claude returns no complete report (`ReportNotRenderedError`: cut off at `max_tokens`, refused, or no text). Exits 2 when `validate_predictions` reports a problem. - [app.py](app.py) β€” Streamlit UI over the same checks. The typed API key is passed into the reporter and is not written to `os.environ`. - [reporter/](reporter/) β€” the only code that calls the network. Loads the two runtime prompts in [prompts/opus-4-8/](prompts/opus-4-8/); see [prompts/README.md](prompts/README.md). - [claude-science-artifacts/](claude-science-artifacts/) β€” synthetic-data generator and the committed scenario oracle (`scenarios/*.csv`, manifests, `_scenario_summary.csv`). Do not regenerate those fixtures to make a test pass. diff --git a/README.md b/README.md index 339a2a9..8584e4f 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ echo "exit code: $?" python -m pytest -q ``` -With `ANTHROPIC_API_KEY` set, the same command prints the risk report instead of the payload and exits 0. `--mode critique` asks for a reviewer critique and a Methods paragraph. The key is read from the environment. It is not stored in the repository. +With `ANTHROPIC_API_KEY` set, the same command prints the risk report instead of the payload and exits 0. `--mode critique` asks for a reviewer critique and a Methods paragraph. The key is read from the environment. It is not stored in the repository. If Claude's reply is cut off at the 4,096-token limit, refused, or contains no text, the command prints the payload, names the stop reason on stderr, and exits 1; the browser UI shows the same message. ```bash python -m pip install -e '.[demo]' diff --git a/app.py b/app.py index a33bc7e..7d23286 100644 --- a/app.py +++ b/app.py @@ -18,7 +18,13 @@ import pandas as pd from clinval_validators import run_checks, validate_predictions -from reporter import API_KEY_ENV, render_report, render_reviewer_critique +from reporter import ( + API_KEY_ENV, + MODEL, + ReportNotRenderedError, + render_report, + render_reviewer_critique, +) SCENARIOS_DIR = Path(__file__).resolve().parent / "claude-science-artifacts" / "scenarios" @@ -127,9 +133,12 @@ def main() -> None: st.warning("Set an Anthropic API key to render the report.", icon="πŸ”‘") elif generate: render = render_reviewer_critique if mode == "critique" else render_report - with st.spinner(f"Asking claude-opus-4-8 for the {mode} report…"): - cached = (report_key, render(payload, api_key=api_key)) - st.session_state["report"] = cached + try: + with st.spinner(f"Asking {MODEL} for the {mode} report…"): + cached = (report_key, render(payload, api_key=api_key)) + st.session_state["report"] = cached + except ReportNotRenderedError as exc: + st.error(f"{exc} The deterministic payload on the left is complete.", icon="🚫") if cached and cached[0] == report_key: st.markdown(cached[1]) diff --git a/cli.py b/cli.py index 4a4be53..37c02a9 100644 --- a/cli.py +++ b/cli.py @@ -17,7 +17,13 @@ import pandas as pd from clinval_validators import run_checks, validate_predictions -from reporter import API_KEY_ENV, MissingAPIKeyError, render_report, render_reviewer_critique +from reporter import ( + API_KEY_ENV, + MissingAPIKeyError, + ReportNotRenderedError, + render_report, + render_reviewer_critique, +) def _parse_args(argv: Sequence[str] | None) -> argparse.Namespace: @@ -67,6 +73,14 @@ def main(argv: Sequence[str] | None = None) -> int: file=sys.stderr, ) return 1 + except ReportNotRenderedError as exc: + print(json.dumps(payload, indent=2)) + print( + f"\nThe deterministic checks above ran offline and are complete.\n" + f"{exc} The {args.mode} report was not rendered.", + file=sys.stderr, + ) + return 1 return 0 diff --git a/reporter/__init__.py b/reporter/__init__.py index cef8025..d32e97f 100644 --- a/reporter/__init__.py +++ b/reporter/__init__.py @@ -3,9 +3,11 @@ from .report import ( API_KEY_ENV, CRITIQUE_SYSTEM_PROMPT_PATH, + MAX_TOKENS, MODEL, SYSTEM_PROMPT_PATH, MissingAPIKeyError, + ReportNotRenderedError, render_report, render_reviewer_critique, ) @@ -14,7 +16,9 @@ "render_report", "render_reviewer_critique", "MissingAPIKeyError", + "ReportNotRenderedError", "MODEL", + "MAX_TOKENS", "API_KEY_ENV", "SYSTEM_PROMPT_PATH", "CRITIQUE_SYSTEM_PROMPT_PATH", diff --git a/reporter/report.py b/reporter/report.py index 1c0bbaa..ef40b21 100644 --- a/reporter/report.py +++ b/reporter/report.py @@ -21,6 +21,9 @@ from clinval_validators import ReportPayload MODEL = "claude-opus-4-8" +# The longest live Opus 4.8 report used 998 output tokens, so 1024 left almost no +# margin. Only generated tokens are billed; the cap is a ceiling, not a cost. +MAX_TOKENS = 4096 API_KEY_ENV = "ANTHROPIC_API_KEY" _PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts" / "opus-4-8" SYSTEM_PROMPT_PATH = _PROMPTS_DIR / "reporter_system_prompt.md" @@ -30,7 +33,9 @@ "render_report", "render_reviewer_critique", "MissingAPIKeyError", + "ReportNotRenderedError", "MODEL", + "MAX_TOKENS", "API_KEY_ENV", "SYSTEM_PROMPT_PATH", "CRITIQUE_SYSTEM_PROMPT_PATH", @@ -45,6 +50,15 @@ class MissingAPIKeyError(RuntimeError): """ +class ReportNotRenderedError(RuntimeError): + """Raised when Claude's reply holds no complete report. + + That happens when the reply stops at ``max_tokens``, is refused + (``stop_reason == "refusal"``), or contains no text block. The deterministic + payload is unaffected, so callers can still show it. + """ + + def _load_system_prompt(path: Path) -> str: """Read the operative system prompt from disk. @@ -77,11 +91,18 @@ def _render( client = Anthropic(api_key=api_key) message = client.messages.create( model=MODEL, - max_tokens=1024, + max_tokens=MAX_TOKENS, system=_load_system_prompt(system_prompt_path), messages=[{"role": "user", "content": json.dumps(payload)}], ) - return message.content[0].text + # A model that thinks returns a thinking block before the text, so the first + # block is not always the report. + text = "".join(block.text for block in message.content if block.type == "text") + if message.stop_reason in ("max_tokens", "refusal") or not text: + raise ReportNotRenderedError( + f"Claude returned no complete report (stop_reason: {message.stop_reason})." + ) + return text def render_report(payload: ReportPayload, api_key: str | None = None) -> str: diff --git a/tests/test_cli.py b/tests/test_cli.py index 72dddce..5a30be8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -91,6 +91,21 @@ def test_cli_without_an_api_key_prints_the_payload_and_a_readable_error(monkeypa assert "KeyError" not in captured.err +def test_cli_prints_the_payload_when_claude_returns_no_complete_report(monkeypatch, capsys) -> None: + def cut_off(payload): + raise cli.ReportNotRenderedError("Claude returned no complete report (stop_reason: max_tokens).") + + monkeypatch.setattr(cli, "render_report", cut_off) + + exit_code = cli.main(["--predictions", str(DEMO_CSV)]) + + assert exit_code == 1 + captured = capsys.readouterr() + assert json.loads(captured.out)["risk"]["level"] == "RED" + assert "max_tokens" in captured.err + assert "Traceback" not in captured.err + + def test_cli_critique_mode_uses_reviewer_critique(monkeypatch, capsys) -> None: monkeypatch.setattr(cli, "render_report", lambda payload: "RISK-UNUSED") monkeypatch.setattr( diff --git a/tests/test_reporter.py b/tests/test_reporter.py index 4b0ff89..f413f42 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -11,13 +11,37 @@ import reporter from reporter import ( CRITIQUE_SYSTEM_PROMPT_PATH, + MAX_TOKENS, MODEL, SYSTEM_PROMPT_PATH, MissingAPIKeyError, + ReportNotRenderedError, render_report, render_reviewer_critique, ) + +def _text(text: str) -> SimpleNamespace: + return SimpleNamespace(type="text", text=text) + + +def _fake_client(monkeypatch, content: list, stop_reason: str = "end_turn") -> dict: + """Replace the Anthropic client; return the dict that records each request.""" + captured: dict = {} + + class FakeMessages: + def create(self, **kwargs): + captured.update(kwargs) + return SimpleNamespace(content=content, stop_reason=stop_reason) + + class FakeAnthropic: + def __init__(self, api_key=None): + captured["api_key"] = api_key + self.messages = FakeMessages() + + monkeypatch.setattr(reporter.report, "Anthropic", FakeAnthropic) + return captured + # leakage_rate is 940 / 959 on weak_random_split_leakage. The other fields match # the current payload contract; this dict is a stub, not a full scenario dump. SAMPLE_PAYLOAD = { @@ -83,26 +107,14 @@ def test_prompts_name_the_fields_python_computes() -> None: def test_render_report_sends_payload_json_to_opus(monkeypatch) -> None: monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") - captured: dict = {} - - class FakeMessages: - def create(self, **kwargs): - captured.update(kwargs) - return SimpleNamespace(content=[SimpleNamespace(text="RED: leakage present")]) - - class FakeAnthropic: - def __init__(self, api_key=None): - captured["api_key"] = api_key - self.messages = FakeMessages() - - monkeypatch.setattr(reporter.report, "Anthropic", FakeAnthropic) + captured = _fake_client(monkeypatch, [_text("RED: leakage present")]) report_text = render_report(SAMPLE_PAYLOAD) assert report_text == "RED: leakage present" assert captured["api_key"] == "test-key" assert captured["model"] == MODEL == "claude-opus-4-8" - assert captured["max_tokens"] == 1024 + assert captured["max_tokens"] == MAX_TOKENS == 4096 user_message = captured["messages"][0] assert user_message["role"] == "user" @@ -115,19 +127,7 @@ def __init__(self, api_key=None): def test_api_key_argument_reaches_the_client_without_changing_the_environment(monkeypatch) -> None: monkeypatch.setenv("ANTHROPIC_API_KEY", "env-key") - captured: dict = {} - - class FakeMessages: - def create(self, **kwargs): - captured.update(kwargs) - return SimpleNamespace(content=[SimpleNamespace(text="ok")]) - - class FakeAnthropic: - def __init__(self, api_key=None): - captured["api_key"] = api_key - self.messages = FakeMessages() - - monkeypatch.setattr(reporter.report, "Anthropic", FakeAnthropic) + captured = _fake_client(monkeypatch, [_text("ok")]) assert render_report(SAMPLE_PAYLOAD, api_key="input-key") == "ok" assert captured["api_key"] == "input-key" @@ -150,28 +150,40 @@ def test_render_reviewer_critique_raises_a_named_error_when_the_key_is_absent(mo render_reviewer_critique(SAMPLE_PAYLOAD) -def test_render_reviewer_critique_sends_payload_json_to_opus(monkeypatch) -> None: +def test_text_after_a_thinking_block_is_returned(monkeypatch) -> None: + # Models that think put a thinking block first; reading content[0].text would + # raise AttributeError on it. monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") - captured: dict = {} + thinking = SimpleNamespace(type="thinking", thinking="") + _fake_client(monkeypatch, [thinking, _text("1. Overall risk"), _text(" RED")]) + assert render_report(SAMPLE_PAYLOAD) == "1. Overall risk RED" - class FakeMessages: - def create(self, **kwargs): - captured.update(kwargs) - return SimpleNamespace(content=[SimpleNamespace(text="Reviewer verdict: reject")]) - class FakeAnthropic: - def __init__(self, api_key=None): - captured["api_key"] = api_key - self.messages = FakeMessages() +@pytest.mark.parametrize("stop_reason", ["max_tokens", "refusal"]) +def test_a_cut_off_or_refused_reply_raises_a_named_error(monkeypatch, stop_reason: str) -> None: + monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") + _fake_client(monkeypatch, [_text("1. Overall risk β€” RED. 940 patients")], stop_reason) + with pytest.raises(ReportNotRenderedError, match=stop_reason): + render_report(SAMPLE_PAYLOAD) - monkeypatch.setattr(reporter.report, "Anthropic", FakeAnthropic) + +def test_a_reply_without_text_raises_a_named_error(monkeypatch) -> None: + monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") + _fake_client(monkeypatch, [SimpleNamespace(type="thinking", thinking="")]) + with pytest.raises(ReportNotRenderedError): + render_reviewer_critique(SAMPLE_PAYLOAD) + + +def test_render_reviewer_critique_sends_payload_json_to_opus(monkeypatch) -> None: + monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") + captured = _fake_client(monkeypatch, [_text("Reviewer verdict: reject")]) critique_text = render_reviewer_critique(SAMPLE_PAYLOAD) assert critique_text == "Reviewer verdict: reject" assert captured["api_key"] == "test-key" assert captured["model"] == MODEL == "claude-opus-4-8" - assert captured["max_tokens"] == 1024 + assert captured["max_tokens"] == MAX_TOKENS user_message = captured["messages"][0] assert user_message["role"] == "user" From 0a037f8a2ff5fc30663f702f658c298e3c85884f Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 14:16:58 +0200 Subject: [PATCH 14/20] Tighten the reporter prompts where live reports went wrong A blind review of 48 live reports found the same slips in both models tested: the split audit cited as "no evidence of inflation" or as "consistent with" the leakage story, calibration gaps of 0.50 against 0.02 called small or consistent, risk reports over the 300-word limit (Opus 4.8 exceeded it in 12 of 12), and every RED critique from Haiku 5.5 writing "Reject or Major revision" without choosing. Both prompts now say the audit cannot show inflation or its absence and must not be used as evidence either way, ask for both numbers whenever a calibration gap is described, and name what to cut to stay within the word limit. The critique asks for exactly one recommendation on the first line. --- prompts/opus-4-8/reporter_system_prompt.md | 8 ++++---- prompts/opus-4-8/reviewer_critique_system_prompt.md | 10 +++++----- tests/test_reporter.py | 8 ++++++++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/prompts/opus-4-8/reporter_system_prompt.md b/prompts/opus-4-8/reporter_system_prompt.md index 1dac6d1..55f11a3 100644 --- a/prompts/opus-4-8/reporter_system_prompt.md +++ b/prompts/opus-4-8/reporter_system_prompt.md @@ -12,8 +12,8 @@ Rules: - risk.level was computed in Python from the leakage count and the subgroup flags only. RED means overlapping_patient_ids is greater than zero. YELLOW means there is no patient overlap and at least one subgroup is flagged. GREEN means neither. split_audit did not determine the level. - Do not soften genuine problems and do not inflate them. State the finding, then its consequence for what may be claimed. - Write for a researcher who trained the model, not for a lay audience. Assume they know AUC, calibration, and train/test splits. -- split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, so that sample includes training rows. sample_range95 is the 2.5th and 97.5th percentile of that sample AUC over seeds 0–199. gap is test_split_as_given minus patient_disjoint_sample. The comparison cannot show that leakage inflated a metric. Do not call either AUC a corrected, true, or retrained result. -- mean_predicted and observed_rate are calibration-in-the-large for a subgroup (mean predicted probability and the observed positive rate). Mention them when you discuss calibration. They are not flags. +- split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, so that sample includes training rows. sample_range95 is the 2.5th and 97.5th percentile of that sample AUC over seeds 0–199. gap is test_split_as_given minus patient_disjoint_sample. The comparison cannot show that leakage inflated a metric, and it cannot show that a metric is free of inflation. Do not use it as evidence either way: do not write that it shows, confirms, or is consistent with inflation or with its absence. Do not call either AUC a corrected, true, or retrained result. +- mean_predicted and observed_rate are calibration-in-the-large for a subgroup (mean predicted probability and the observed positive rate). Mention them when you discuss calibration. They are not flags. Give both numbers when you describe a calibration gap. A mean_predicted many times larger than observed_rate is a large miscalibration; do not call it small or consistent. - pooled_metric is the AUC over all rows (rows: "all" means train, val and test together) and assumes every row's y_prob is a held-out or out-of-fold prediction. It is not the test-split AUC; that is split_audit.test_split_as_given. - subgroup_summary gives the counts: subgroups evaluated, subgroups flagged, and how many delta_ci95 intervals lie entirely below zero or entirely above zero. Take counts from it instead of counting the subgroups list. Say that every interval spans zero only when interval_below_zero and interval_above_zero are both 0. @@ -50,7 +50,7 @@ Produce a report in exactly these sections: 1. **Overall risk** β€” one line that states risk.level verbatim (GREEN, YELLOW, or RED), then explain risk.reasons. Do not choose a different level. -2. **What the data shows** β€” 3-5 sentences, each tied to a specific number from the JSON, in plain reviewer language. When you mention split_audit, describe it as a rescore of fixed predictions that cannot show inflation. +2. **What the data shows** β€” 3-5 sentences, each tied to a specific number from the JSON, in plain reviewer language. When you mention split_audit, describe it as a rescore of fixed predictions that says nothing about inflation in either direction. 3. **What can be claimed** β€” the performance statements the results actually support. @@ -61,4 +61,4 @@ Produce a report in exactly these sections: - If risk.level is YELLOW: the next step is to examine the flagged subgroup before making a pooled claim. - If risk.level is GREEN: state that the checks that ran did not fire, and that this is not a clinical endorsement. -Keep the whole report under 300 words. No preamble, no restating these instructions β€” start at section 1. +Keep the whole report under 300 words. To stay within it, do not explain how the level is derived beyond risk.reasons, and name an unflagged subgroup only when its interval lies entirely above or below zero. No preamble, no restating these instructions β€” start at section 1. diff --git a/prompts/opus-4-8/reviewer_critique_system_prompt.md b/prompts/opus-4-8/reviewer_critique_system_prompt.md index fb259b2..105fa62 100644 --- a/prompts/opus-4-8/reviewer_critique_system_prompt.md +++ b/prompts/opus-4-8/reviewer_critique_system_prompt.md @@ -10,11 +10,11 @@ Rules: - Use only the numbers present in the provided JSON. Do not invent, estimate, or recompute a metric. If a value is absent, say the check did not run rather than guessing. - State risk.level verbatim. Explain risk.reasons. Do not raise, lower, or rename the level. - risk.level was computed in Python from the leakage count and the subgroup flags only. split_audit did not determine the level. -- Recommend Accept only when risk.level is GREEN. When risk.level is RED, recommend Reject or Major revision. When risk.level is YELLOW, recommend Major revision. +- Give exactly one recommendation: Accept, Minor revision, Major revision, or Reject. Accept and Minor revision are allowed only when risk.level is GREEN. When risk.level is YELLOW, recommend Major revision. When risk.level is RED, choose Reject or Major revision and write only the one you chose. - Do not soften genuine problems and do not inflate them. Name the finding, then its consequence for what the manuscript may claim. - Write for the authors, who trained the model, not for a lay audience. Assume they know AUC, calibration, and train/test splits. -- split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, including training rows. sample_range95 is the spread of that sample AUC. gap is test_split_as_given minus patient_disjoint_sample. The comparison cannot show that leakage inflated a metric. Do not call either AUC a corrected, true, or retrained result. -- mean_predicted and observed_rate are calibration-in-the-large. They are not flags. auc_rest is the AUC on every row outside the subgroup, and delta_vs_rest is auc minus auc_rest. delta_ci95 is the patient-level 95% interval for that difference. +- split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, including training rows. sample_range95 is the spread of that sample AUC. gap is test_split_as_given minus patient_disjoint_sample. The comparison cannot show that leakage inflated a metric, and it cannot show that a metric is free of inflation. Do not use it as evidence either way: do not write that it shows, confirms, or is consistent with inflation or with its absence. Do not call either AUC a corrected, true, or retrained result. +- mean_predicted and observed_rate are calibration-in-the-large. They are not flags. Give both numbers when you describe a calibration gap. A mean_predicted many times larger than observed_rate is a large miscalibration; do not call it small or consistent. auc_rest is the AUC on every row outside the subgroup, and delta_vs_rest is auc minus auc_rest. delta_ci95 is the patient-level 95% interval for that difference. - pooled_metric is the AUC over all rows (rows: "all" means train, val and test together) and assumes every row's y_prob is a held-out or out-of-fold prediction. It is not the test-split AUC; that is split_audit.test_split_as_given. - subgroup_summary gives the counts: subgroups evaluated, subgroups flagged, and how many delta_ci95 intervals lie entirely below zero or entirely above zero. Take counts from it instead of counting the subgroups list. Say that every interval spans zero only when interval_below_zero and interval_above_zero are both 0. - The corrected Methods paragraph must describe an evaluation the JSON's own findings would pass. If patients overlap train and test, the fix is to retrain and evaluate with a patient-grouped split, so that no test patient was seen in training. Re-scoring the same predictions is not that fix. Where a subgroup is flagged, the paragraph reports that subgroup against the rest of the cohort. Cite only figures that appear in the JSON; state changes in method, not invented results. @@ -50,8 +50,8 @@ Input JSON shape (fields may be absent if a check did not run): Produce a critique in exactly these two sections: -1. **Reviewer verdict** β€” Open with one line: Accept only when risk.level is GREEN; Major revision when risk.level is YELLOW; Reject or Major revision when risk.level is RED. State risk.level verbatim and one sentence drawn from risk.reasons. Then, in 3-5 sentences of plain reviewer prose, state what the reported numbers support and what they do not, each sentence tied to a specific figure from the JSON. +1. **Reviewer verdict** β€” Open with one line that holds only the recommendation you chose. Then state risk.level verbatim and one sentence drawn from risk.reasons. Then, in 3-5 sentences of plain reviewer prose, state what the reported numbers support and what they do not, each sentence tied to a specific figure from the JSON. 2. **Corrected Methods paragraph** β€” A single paragraph the authors could paste into their Methods section describing a leakage-safe evaluation that addresses every flaw the JSON found. If the flaw is patient overlap, the paragraph describes retraining and evaluating with a patient-grouped split so that no test patient was seen in training. Write it as publishable Methods prose, not as instructions to the authors. -Keep the whole response under 350 words. No preamble, no restating these instructions β€” start at section 1. +Keep the whole response under 350 words. To stay within it, do not explain how the level is derived beyond risk.reasons, and name an unflagged subgroup only when its interval lies entirely above or below zero. No preamble, no restating these instructions β€” start at section 1. diff --git a/tests/test_reporter.py b/tests/test_reporter.py index f413f42..d5ee720 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -105,6 +105,14 @@ def test_prompts_name_the_fields_python_computes() -> None: assert missing == [], path.name +def test_prompts_keep_the_rules_live_reports_broke() -> None: + # Live reports read the split audit as evidence of no inflation and left the + # RED critique recommendation as "Reject or Major revision". + for path in (SYSTEM_PROMPT_PATH, CRITIQUE_SYSTEM_PROMPT_PATH): + assert "free of inflation" in path.read_text(encoding="utf-8"), path.name + assert "exactly one recommendation" in CRITIQUE_SYSTEM_PROMPT_PATH.read_text(encoding="utf-8") + + def test_render_report_sends_payload_json_to_opus(monkeypatch) -> None: monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") captured = _fake_client(monkeypatch, [_text("RED: leakage present")]) From ec15a361373a0cfb91a9c13eca5c3205ce17d289 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 14:38:26 +0200 Subject: [PATCH 15/20] Switch the reporter to Claude Opus 5.5 at low effort A blind review of 24 live reports per configuration compared seven setups across four scenarios. Opus 5.5 at low effort made no major error and had the most error-free reports (19 of 24), at $40 per 1,000 reports against $46 for Opus 4.8, which made one major error and had 11 of 24 error-free. Medium effort made no major error either and cost more. Opus 5.5 always thinks, so the request sets output_config.effort and sends no thinking parameter; replies start with thinking blocks, which the reporter already skips. Requests opt into server-side refusal fallback (fallbacks "default" under the server-side-fallback-2026-07-01 beta), so a safety-classifier decline is rerun on the model Anthropic recommends for that category instead of failing the report. The SDK types fallbacks as a list, so the "default" string goes in extra_body. The prompts move from prompts/opus-4-8/ to prompts/, since they no longer belong to one model. CLAUDE.md records the comparison so the choice is not changed without re-running it. --- CLAUDE.md | 6 ++-- README.md | 2 +- demo/run_demo.sh | 2 +- prompts/README.md | 4 +-- .../{opus-4-8 => }/reporter_system_prompt.md | 2 +- .../reviewer_critique_system_prompt.md | 2 +- reporter/__init__.py | 2 ++ reporter/report.py | 33 +++++++++++++------ tests/test_reporter.py | 17 +++++++--- 9 files changed, 48 insertions(+), 22 deletions(-) rename prompts/{opus-4-8 => }/reporter_system_prompt.md (98%) rename prompts/{opus-4-8 => }/reviewer_critique_system_prompt.md (98%) diff --git a/CLAUDE.md b/CLAUDE.md index f2475ff..c8d4aba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Named user: a clinical-AI researcher / radiology-lab PhD student who trained a m **Python computes. Claude explains.** - All metrics, statistical tests, pass/fail flags, and the risk level are computed by deterministic Python. Never ask an LLM to compute AUC, run a bootstrap, or decide a flag or the risk level. -- Claude (Anthropic API, `claude-opus-4-8`) is used **only** to turn the deterministic layer's structured JSON into prose: the risk report, reviewer-style critique, next-step suggestions. See the reporter contract in [prompts/opus-4-8/reporter_system_prompt.md](prompts/opus-4-8/reporter_system_prompt.md) β€” it may cite only numbers present in the JSON. +- Claude (Anthropic API, `claude-opus-5-5` at effort `low`, with server-side refusal fallback) is used **only** to turn the deterministic layer's structured JSON into prose: the risk report, reviewer-style critique, next-step suggestions. See the reporter contract in [prompts/reporter_system_prompt.md](prompts/reporter_system_prompt.md) β€” it may cite only numbers present in the JSON. - Layers stay cleanly separable: `clinval_validators/` (pure Python, no network, runs fully offline) and `reporter/` (the only code that touches the network). ## Repo map β€” current state @@ -23,7 +23,7 @@ Named user: a clinical-AI researcher / radiology-lab PhD student who trained a m - [clinval_validators/](clinval_validators/) β€” deterministic checks. `run_checks` returns leakage, the split audit, subgroups, the pooled AUC, and `risk`. - [cli.py](cli.py) β€” `clinval-validate`. Prints the payload and exits 1 when `ANTHROPIC_API_KEY` is unset or Claude returns no complete report (`ReportNotRenderedError`: cut off at `max_tokens`, refused, or no text). Exits 2 when `validate_predictions` reports a problem. - [app.py](app.py) β€” Streamlit UI over the same checks. The typed API key is passed into the reporter and is not written to `os.environ`. -- [reporter/](reporter/) β€” the only code that calls the network. Loads the two runtime prompts in [prompts/opus-4-8/](prompts/opus-4-8/); see [prompts/README.md](prompts/README.md). +- [reporter/](reporter/) β€” the only code that calls the network. Loads the two runtime prompts in [prompts/](prompts/); see [prompts/README.md](prompts/README.md). - [claude-science-artifacts/](claude-science-artifacts/) β€” synthetic-data generator and the committed scenario oracle (`scenarios/*.csv`, manifests, `_scenario_summary.csv`). Do not regenerate those fixtures to make a test pass. - [rsna_to_predictions.py](rsna_to_predictions.py) β€” local-only adapter for an RSNA `train.csv`. Outputs are gitignored. - [tests/](tests/) β€” unit tests, including fresh generator seeds for the subgroup flag. @@ -57,6 +57,8 @@ python claude-science-artifacts/leakage_generator.py --out predictions.csv \ - `_FLAG_DELTA` (βˆ’0.10) and `_MIN_SUBGROUP_N` (50) were calibrated on the committed fixtures. Do not change them to make a test pass. - `reporter/report.py` sends only the text after the first `---` of a prompt file. - A one-sided Bonferroni correction across subgroups was measured on generator seeds 7–36 and not adopted. It cut clean-table flags from 6 to 3 of 30, but M030 detection fell from 26 to 21 of 30 and density detection from 21 to 12 of 30, and `subgroup_density_degradation` was no longer flagged. Re-measure on the same seeds before changing the flag rule. +- The reporter model was chosen in October 2026 from a blind review of 24 live reports per configuration (4 scenarios Γ— 2 modes Γ— 3 repeats). Major errors: Opus 5.5 low 0, Opus 5.5 medium 0, Sonnet 5.5 with and without thinking 0, Opus 4.8 1, Haiku 5.5 2–3. Opus 5.5 low had the most error-free reports (19 of 24) at $40 per 1,000 reports, against $46 for Opus 4.8; Sonnet 5.5 without thinking is the cheaper option ($18, 14 of 24 error-free). Re-run that comparison before changing `MODEL` or `EFFORT`. +- Opus 5.5 always thinks: `thinking: {"type": "disabled"}` is a 400. Its replies start with thinking blocks, so the reporter joins the `text` blocks, and `MAX_TOKENS` must leave room for the thinking. ## Data & schema diff --git a/README.md b/README.md index 8584e4f..b5e18a1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # clinval-validator -ClinVal Validator reads a model's `predictions.csv` and reports two failure modes that make a clinical-AI performance claim unsafe: the same patient in both the train and test splits, and a subgroup whose AUC is lower than the rest of the table. Python computes the metrics, the flags, and the risk level. Claude is instructed to turn that JSON into prose and to cite only numbers present in it. Nothing in this repository checks the text Claude returns. +ClinVal Validator reads a model's `predictions.csv` and reports two failure modes that make a clinical-AI performance claim unsafe: the same patient in both the train and test splits, and a subgroup whose AUC is lower than the rest of the table. Python computes the metrics, the flags, and the risk level. Claude is instructed to turn that JSON into prose and to cite only numbers present in it. Nothing in this repository checks the text Claude returns. The reporter calls Claude Opus 5.5 (`claude-opus-5-5`) at low effort; if a safety classifier declines a request, the API reruns it on the model Anthropic recommends for that refusal category. ![ClinVal Validator on weak_random_split_leakage: 940 leaked patients, test-split AUC 0.827, patient-disjoint sample AUC 0.899, and a RED banner](docs/images/app-weak-random-split-leakage.png) diff --git a/demo/run_demo.sh b/demo/run_demo.sh index c16b240..12ee4d0 100755 --- a/demo/run_demo.sh +++ b/demo/run_demo.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Spine of the 3-minute demo: a GREEN control, then the leakage scenario. -# Requires ANTHROPIC_API_KEY (the report step calls claude-opus-4-8). +# Requires ANTHROPIC_API_KEY (the report step calls Claude; see MODEL in reporter/report.py). set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" diff --git a/prompts/README.md b/prompts/README.md index 495784a..fc7f0fe 100644 --- a/prompts/README.md +++ b/prompts/README.md @@ -4,10 +4,10 @@ Runtime system prompts for the LLM reporting step. These are **load-bearing**: `reporter/report.py` reads them from disk at call time (see `SYSTEM_PROMPT_PATH` and `CRITIQUE_SYSTEM_PROMPT_PATH`), so renaming or deleting them breaks the tool. -- [`opus-4-8/reporter_system_prompt.md`](opus-4-8/reporter_system_prompt.md) β€” +- [`reporter_system_prompt.md`](reporter_system_prompt.md) β€” turns the validators' JSON into the GREEN/YELLOW/RED Validation Risk Report (`clinval-validate --mode risk`). -- [`opus-4-8/reviewer_critique_system_prompt.md`](opus-4-8/reviewer_critique_system_prompt.md) β€” +- [`reviewer_critique_system_prompt.md`](reviewer_critique_system_prompt.md) β€” turns the same JSON into a journal-reviewer critique plus a corrected, leakage-safe Methods paragraph (`clinval-validate --mode critique`). diff --git a/prompts/opus-4-8/reporter_system_prompt.md b/prompts/reporter_system_prompt.md similarity index 98% rename from prompts/opus-4-8/reporter_system_prompt.md rename to prompts/reporter_system_prompt.md index 55f11a3..149d162 100644 --- a/prompts/opus-4-8/reporter_system_prompt.md +++ b/prompts/reporter_system_prompt.md @@ -1,4 +1,4 @@ -# Reporter system prompt (sent to claude-opus-4-8) +# Reporter system prompt (sent to the model named by MODEL in reporter/report.py) This is the system prompt for the LLM step. It receives the deterministic validators' JSON output and returns the Validation Risk Report. It never computes numbers β€” every figure it cites comes from the JSON. diff --git a/prompts/opus-4-8/reviewer_critique_system_prompt.md b/prompts/reviewer_critique_system_prompt.md similarity index 98% rename from prompts/opus-4-8/reviewer_critique_system_prompt.md rename to prompts/reviewer_critique_system_prompt.md index 105fa62..e41b235 100644 --- a/prompts/opus-4-8/reviewer_critique_system_prompt.md +++ b/prompts/reviewer_critique_system_prompt.md @@ -1,4 +1,4 @@ -# Reviewer critique system prompt (sent to claude-opus-4-8) +# Reviewer critique system prompt (sent to the model named by MODEL in reporter/report.py) This is the system prompt for the reviewer-critique output. It receives the same deterministic validators' JSON as the risk reporter and returns a journal-reviewer-style critique plus a corrected, leakage-safe Methods paragraph. It never computes numbers β€” every figure it cites comes from the JSON. diff --git a/reporter/__init__.py b/reporter/__init__.py index d32e97f..767f4d7 100644 --- a/reporter/__init__.py +++ b/reporter/__init__.py @@ -3,6 +3,7 @@ from .report import ( API_KEY_ENV, CRITIQUE_SYSTEM_PROMPT_PATH, + EFFORT, MAX_TOKENS, MODEL, SYSTEM_PROMPT_PATH, @@ -18,6 +19,7 @@ "MissingAPIKeyError", "ReportNotRenderedError", "MODEL", + "EFFORT", "MAX_TOKENS", "API_KEY_ENV", "SYSTEM_PROMPT_PATH", diff --git a/reporter/report.py b/reporter/report.py index ef40b21..4e13beb 100644 --- a/reporter/report.py +++ b/reporter/report.py @@ -1,8 +1,8 @@ """LLM reporting step: turn the validators' JSON payload into prose. This is the only module in the project that touches the network. It sends the -deterministic report payload to Claude (``claude-opus-4-8``) under one of two -system prompts and returns the rendered text: :func:`render_report` produces the +deterministic report payload to Claude (``claude-opus-5-5`` at low effort) under +one of two system prompts and returns the rendered text: :func:`render_report` produces the GREEN/YELLOW/RED Validation Risk Report, :func:`render_reviewer_critique` produces a journal-reviewer critique with a corrected Methods paragraph. Neither computes a metric β€” every number in the output comes from the payload. @@ -20,12 +20,19 @@ if TYPE_CHECKING: from clinval_validators import ReportPayload -MODEL = "claude-opus-4-8" -# The longest live Opus 4.8 report used 998 output tokens, so 1024 left almost no -# margin. Only generated tokens are billed; the cap is a ceiling, not a cost. +MODEL = "claude-opus-5-5" +# Opus 5.5 always thinks; effort sets how much. In a blind review of 24 live +# reports per setting, neither "low" nor "medium" made a major error, and "low" +# cost less ($40 vs $45 per 1,000 reports). +EFFORT = "low" +# Thinking counts toward max_tokens. The longest live low-effort reply used 1,292 +# output tokens. Only generated tokens are billed; the cap is a ceiling, not a cost. MAX_TOKENS = 4096 +# On a safety-classifier refusal the API reruns the request on the model Anthropic +# recommends for that refusal category, instead of returning the refusal. +FALLBACK_BETA = "server-side-fallback-2026-07-01" API_KEY_ENV = "ANTHROPIC_API_KEY" -_PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts" / "opus-4-8" +_PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts" SYSTEM_PROMPT_PATH = _PROMPTS_DIR / "reporter_system_prompt.md" CRITIQUE_SYSTEM_PROMPT_PATH = _PROMPTS_DIR / "reviewer_critique_system_prompt.md" @@ -35,6 +42,7 @@ "MissingAPIKeyError", "ReportNotRenderedError", "MODEL", + "EFFORT", "MAX_TOKENS", "API_KEY_ENV", "SYSTEM_PROMPT_PATH", @@ -72,7 +80,7 @@ def _load_system_prompt(path: Path) -> str: def _render( payload: ReportPayload, system_prompt_path: Path, api_key: str | None = None ) -> str: - """Send the deterministic payload to ``claude-opus-4-8`` under one prompt. + """Send the deterministic payload to :data:`MODEL` under one prompt. ``api_key`` overrides ``ANTHROPIC_API_KEY`` when it is passed, so a browser session can use the key typed into that session without writing @@ -89,14 +97,19 @@ def _render( raise MissingAPIKeyError(f"{API_KEY_ENV} is not set in the environment.") client = Anthropic(api_key=api_key) - message = client.messages.create( + message = client.beta.messages.create( model=MODEL, max_tokens=MAX_TOKENS, system=_load_system_prompt(system_prompt_path), messages=[{"role": "user", "content": json.dumps(payload)}], + output_config={"effort": EFFORT}, + betas=[FALLBACK_BETA], + # The SDK types `fallbacks` as a list of models; the "default" mode is a + # string the API accepts, so it goes in the raw body. + extra_body={"fallbacks": "default"}, ) - # A model that thinks returns a thinking block before the text, so the first - # block is not always the report. + # Opus 5.5 returns thinking blocks (and, after a fallback, a fallback block) + # before the text, so the first block is not always the report. text = "".join(block.text for block in message.content if block.type == "text") if message.stop_reason in ("max_tokens", "refusal") or not text: raise ReportNotRenderedError( diff --git a/tests/test_reporter.py b/tests/test_reporter.py index d5ee720..c4d8c3f 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -11,6 +11,7 @@ import reporter from reporter import ( CRITIQUE_SYSTEM_PROMPT_PATH, + EFFORT, MAX_TOKENS, MODEL, SYSTEM_PROMPT_PATH, @@ -37,7 +38,7 @@ def create(self, **kwargs): class FakeAnthropic: def __init__(self, api_key=None): captured["api_key"] = api_key - self.messages = FakeMessages() + self.beta = SimpleNamespace(messages=FakeMessages()) monkeypatch.setattr(reporter.report, "Anthropic", FakeAnthropic) return captured @@ -121,8 +122,14 @@ def test_render_report_sends_payload_json_to_opus(monkeypatch) -> None: assert report_text == "RED: leakage present" assert captured["api_key"] == "test-key" - assert captured["model"] == MODEL == "claude-opus-4-8" + assert captured["model"] == MODEL == "claude-opus-5-5" assert captured["max_tokens"] == MAX_TOKENS == 4096 + # Opus 5.5 rejects thinking={"type": "disabled"}; effort is the only control. + assert "thinking" not in captured + assert captured["output_config"] == {"effort": EFFORT} == {"effort": "low"} + # A classifier refusal is retried on Anthropic's recommended model for its category. + assert captured["betas"] == ["server-side-fallback-2026-07-01"] + assert captured["extra_body"] == {"fallbacks": "default"} user_message = captured["messages"][0] assert user_message["role"] == "user" @@ -163,7 +170,8 @@ def test_text_after_a_thinking_block_is_returned(monkeypatch) -> None: # raise AttributeError on it. monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") thinking = SimpleNamespace(type="thinking", thinking="") - _fake_client(monkeypatch, [thinking, _text("1. Overall risk"), _text(" RED")]) + fallback = SimpleNamespace(type="fallback") # present when a refusal was rerun on another model + _fake_client(monkeypatch, [thinking, fallback, _text("1. Overall risk"), _text(" RED")]) assert render_report(SAMPLE_PAYLOAD) == "1. Overall risk RED" @@ -190,8 +198,9 @@ def test_render_reviewer_critique_sends_payload_json_to_opus(monkeypatch) -> Non assert critique_text == "Reviewer verdict: reject" assert captured["api_key"] == "test-key" - assert captured["model"] == MODEL == "claude-opus-4-8" + assert captured["model"] == MODEL assert captured["max_tokens"] == MAX_TOKENS + assert captured["output_config"] == {"effort": EFFORT} user_message = captured["messages"][0] assert user_message["role"] == "user" From bf99252027f92a515d423a04f40b9150ecc808be Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 14:46:43 +0200 Subject: [PATCH 16/20] Stop tracking the local MCP config --- .gitignore | 1 + .mcp.json | 12 ------------ 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 .mcp.json diff --git a/.gitignore b/.gitignore index 8746ae1..459d318 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ predictions_rsna*.csv .DS_Store screenshots/ +.mcp.json diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 30d2a34..0000000 --- a/.mcp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers": { - "playwright": { - "command": "npx", - "args": ["@playwright/mcp@latest"] - }, - "clickup": { - "command": "npx", - "args": ["-y", "mcp-remote", "https://mcp.clickup.com/mcp"] - } - } -} \ No newline at end of file From 27af636cb36e84058c353f8c4c212180a2df6f73 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 17:59:34 +0200 Subject: [PATCH 17/20] Leave one-class samples out of the split audit On a small table a 15% patient sample can contain no positive row. Its AUC is undefined, roc_auc_score returned NaN, and np.percentile then turned sample_range95 into [NaN, NaN]. When the seed-0 sample was the one-class draw, patient_disjoint_sample and gap were NaN as well. json.dumps writes NaN as a bare token, which is not valid JSON, so that payload reached Claude and the JSON download in that form. validate_predictions accepts such tables, because it requires both classes only on the test split. On 200-patient tables from the bundled generator, 7 of seeds 0-19 hit this. Seeds whose sample holds one class are now left out of sample_range95, the same way the subgroup bootstrap skips one-class replicates. patient_disjoint_sample and gap are null when the seed-0 sample holds one class, and sample_range95 is null when every seed's sample does. The UI shows "n/a" for a null sample AUC, and both prompts say a null value is undefined and must not be filled in. The prompts' JSON shape also marks delta_ci95 as nullable, which the code already allowed. The payloads of the six committed scenarios are byte-identical before and after this change. --- README.md | 2 ++ app.py | 16 ++++++--- clinval_validators/checks.py | 38 ++++++++++++++++------ prompts/reporter_system_prompt.md | 10 +++--- prompts/reviewer_critique_system_prompt.md | 10 +++--- tests/test_validators.py | 15 +++++++++ 6 files changed, 67 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index b5e18a1..9dff935 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ On 30 tables from the bundled generator (seeds 7–36; 2,000 patients and about `test_split_as_given` is the AUC on rows with `split == "test"`. `patient_disjoint_sample` is the AUC on a seed-0 sample of 15% of patients drawn from every row, so the sample includes training rows. `sample_range95` is the 2.5th and 97.5th percentile of that sample AUC over seeds 0–199. `gap` is `test_split_as_given` minus `patient_disjoint_sample`. +On a small table a sample can contain one class, where AUC is undefined. Those seeds are left out of `sample_range95`. When the seed-0 sample contains one class, `patient_disjoint_sample` and `gap` are `null`; when every seed's sample does, `sample_range95` is `null`. + Both AUCs use the predictions already in the file. The audit does not measure how much leakage changed a metric, and it does not set the risk level. ## Input diff --git a/app.py b/app.py index 7d23286..74346ff 100644 --- a/app.py +++ b/app.py @@ -94,17 +94,25 @@ def main() -> None: # Same fixed predictions. The sample draws patients from every row, # including training rows, so it is not a second look at the test split. c3.metric("AUC, test split as given", f"{audit['test_split_as_given']:.3f}") + # A small table can draw a one-class sample; the audit then reports None. + sample = audit["patient_disjoint_sample"] c4.metric( "AUC, patient-disjoint sample", - f"{audit['patient_disjoint_sample']:.3f}", - delta=f"{audit['patient_disjoint_sample'] - audit['test_split_as_given']:+.3f}", + "n/a (one class)" if sample is None else f"{sample:.3f}", + delta=None if sample is None else f"{sample - audit['test_split_as_given']:+.3f}", + ) + range95 = audit["sample_range95"] + range_text = ( + "" + if range95 is None + else f"Over the seeds 0–199 whose sample holds both classes, that sample " + f"AUC stays within {range95[0]:.3f}–{range95[1]:.3f}. " ) - low, high = audit["sample_range95"] st.caption( "Both AUCs use the same fixed predictions. The patient-disjoint " "sample is drawn from all rows, including training rows, so this " "comparison cannot measure how much leakage inflated the metric. " - f"Over seeds 0–199 that sample AUC stays within {low:.3f}–{high:.3f}. " + f"{range_text}" "Pooled AUC and the per-subgroup breakdown are in the JSON below." ) if level == "RED": diff --git a/clinval_validators/checks.py b/clinval_validators/checks.py index ca55cb3..81fc630 100644 --- a/clinval_validators/checks.py +++ b/clinval_validators/checks.py @@ -56,9 +56,9 @@ class LeakageResult(TypedDict): class SplitAuditResult(TypedDict): metric: str test_split_as_given: float - patient_disjoint_sample: float - sample_range95: list[float] - gap: float + patient_disjoint_sample: float | None + sample_range95: list[float] | None + gap: float | None class SubgroupResult(TypedDict): @@ -170,6 +170,11 @@ def audit_split_quality(df: pd.DataFrame) -> SplitAuditResult: over seeds 0–199. ``gap`` is ``test_split_as_given`` minus ``patient_disjoint_sample``. + On a small table a sample can hold one class, where AUC is undefined. + Such seeds are left out of ``sample_range95``. When the seed-0 sample + holds one class, ``patient_disjoint_sample`` and ``gap`` are None; when + every seed does, ``sample_range95`` is None. + The comparison cannot measure how much leakage inflated a metric. Both numbers rescore predictions that already exist; they do not come from retraining under a patient-grouped split. The leakage count is @@ -181,20 +186,33 @@ def audit_split_quality(df: pd.DataFrame) -> SplitAuditResult: test_split_as_given = float(roc_auc_score(y_true[test_rows], y_prob[test_rows])) groups = df["patient_id"] - sample_aucs = np.empty(200, dtype=np.float64) + sample_aucs: list[float] = [] + patient_disjoint_sample: float | None = None for seed in range(200): splitter = GroupShuffleSplit(n_splits=1, test_size=0.15, random_state=seed) _, sample_idx = next(splitter.split(df, groups=groups)) - sample_aucs[seed] = roc_auc_score(y_true[sample_idx], y_prob[sample_idx]) - - low, high = np.percentile(sample_aucs, [2.5, 97.5]) - patient_disjoint_sample = float(sample_aucs[0]) + if not _both_classes(y_true[sample_idx]): + continue + auc = float(roc_auc_score(y_true[sample_idx], y_prob[sample_idx])) + if seed == 0: + patient_disjoint_sample = auc + sample_aucs.append(auc) + + sample_range95 = ( + [float(bound) for bound in np.percentile(sample_aucs, [2.5, 97.5])] + if sample_aucs + else None + ) return { "metric": "auc", "test_split_as_given": test_split_as_given, "patient_disjoint_sample": patient_disjoint_sample, - "sample_range95": [float(low), float(high)], - "gap": test_split_as_given - patient_disjoint_sample, + "sample_range95": sample_range95, + "gap": ( + None + if patient_disjoint_sample is None + else test_split_as_given - patient_disjoint_sample + ), } diff --git a/prompts/reporter_system_prompt.md b/prompts/reporter_system_prompt.md index 149d162..ef2ef06 100644 --- a/prompts/reporter_system_prompt.md +++ b/prompts/reporter_system_prompt.md @@ -12,7 +12,7 @@ Rules: - risk.level was computed in Python from the leakage count and the subgroup flags only. RED means overlapping_patient_ids is greater than zero. YELLOW means there is no patient overlap and at least one subgroup is flagged. GREEN means neither. split_audit did not determine the level. - Do not soften genuine problems and do not inflate them. State the finding, then its consequence for what may be claimed. - Write for a researcher who trained the model, not for a lay audience. Assume they know AUC, calibration, and train/test splits. -- split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, so that sample includes training rows. sample_range95 is the 2.5th and 97.5th percentile of that sample AUC over seeds 0–199. gap is test_split_as_given minus patient_disjoint_sample. The comparison cannot show that leakage inflated a metric, and it cannot show that a metric is free of inflation. Do not use it as evidence either way: do not write that it shows, confirms, or is consistent with inflation or with its absence. Do not call either AUC a corrected, true, or retrained result. +- split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, so that sample includes training rows. sample_range95 is the 2.5th and 97.5th percentile of that sample AUC over seeds 0–199. gap is test_split_as_given minus patient_disjoint_sample. On a small table a sample can hold one class, where AUC is undefined: patient_disjoint_sample and gap are null when the seed-0 sample does, and sample_range95 leaves such seeds out and is null when every seed does. Say that the value is undefined; do not supply one. The comparison cannot show that leakage inflated a metric, and it cannot show that a metric is free of inflation. Do not use it as evidence either way: do not write that it shows, confirms, or is consistent with inflation or with its absence. Do not call either AUC a corrected, true, or retrained result. - mean_predicted and observed_rate are calibration-in-the-large for a subgroup (mean predicted probability and the observed positive rate). Mention them when you discuss calibration. They are not flags. Give both numbers when you describe a calibration gap. A mean_predicted many times larger than observed_rate is a large miscalibration; do not call it small or consistent. - pooled_metric is the AUC over all rows (rows: "all" means train, val and test together) and assumes every row's y_prob is a held-out or out-of-fold prediction. It is not the test-split AUC; that is split_audit.test_split_as_given. - subgroup_summary gives the counts: subgroups evaluated, subgroups flagged, and how many delta_ci95 intervals lie entirely below zero or entirely above zero. Take counts from it instead of counting the subgroups list. Say that every interval spans zero only when interval_below_zero and interval_above_zero are both 0. @@ -24,9 +24,9 @@ Input JSON shape (fields may be absent if a check did not run): "split_audit": { "metric": "auc", "test_split_as_given": float, - "patient_disjoint_sample": float, - "sample_range95": [float, float], - "gap": float + "patient_disjoint_sample": float | null, + "sample_range95": [float, float] | null, + "gap": float | null }, "subgroups": [{ "key": "machine_id | site_id | density | age", @@ -35,7 +35,7 @@ Input JSON shape (fields may be absent if a check did not run): "auc": float, "auc_rest": float, "delta_vs_rest": float, - "delta_ci95": [float, float], + "delta_ci95": [float, float] | null, "mean_predicted": float, "observed_rate": float, "flagged": bool diff --git a/prompts/reviewer_critique_system_prompt.md b/prompts/reviewer_critique_system_prompt.md index e41b235..a6e1f6f 100644 --- a/prompts/reviewer_critique_system_prompt.md +++ b/prompts/reviewer_critique_system_prompt.md @@ -13,7 +13,7 @@ Rules: - Give exactly one recommendation: Accept, Minor revision, Major revision, or Reject. Accept and Minor revision are allowed only when risk.level is GREEN. When risk.level is YELLOW, recommend Major revision. When risk.level is RED, choose Reject or Major revision and write only the one you chose. - Do not soften genuine problems and do not inflate them. Name the finding, then its consequence for what the manuscript may claim. - Write for the authors, who trained the model, not for a lay audience. Assume they know AUC, calibration, and train/test splits. -- split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, including training rows. sample_range95 is the spread of that sample AUC. gap is test_split_as_given minus patient_disjoint_sample. The comparison cannot show that leakage inflated a metric, and it cannot show that a metric is free of inflation. Do not use it as evidence either way: do not write that it shows, confirms, or is consistent with inflation or with its absence. Do not call either AUC a corrected, true, or retrained result. +- split_audit is a diagnostic on fixed predictions. test_split_as_given is the AUC on rows with split "test". patient_disjoint_sample is the AUC on a seed-0 sample of patients drawn from all rows, including training rows. sample_range95 is the spread of that sample AUC. gap is test_split_as_given minus patient_disjoint_sample. On a small table a sample can hold one class, where AUC is undefined: patient_disjoint_sample and gap are null when the seed-0 sample does, and sample_range95 leaves such seeds out and is null when every seed does. Say that the value is undefined; do not supply one. The comparison cannot show that leakage inflated a metric, and it cannot show that a metric is free of inflation. Do not use it as evidence either way: do not write that it shows, confirms, or is consistent with inflation or with its absence. Do not call either AUC a corrected, true, or retrained result. - mean_predicted and observed_rate are calibration-in-the-large. They are not flags. Give both numbers when you describe a calibration gap. A mean_predicted many times larger than observed_rate is a large miscalibration; do not call it small or consistent. auc_rest is the AUC on every row outside the subgroup, and delta_vs_rest is auc minus auc_rest. delta_ci95 is the patient-level 95% interval for that difference. - pooled_metric is the AUC over all rows (rows: "all" means train, val and test together) and assumes every row's y_prob is a held-out or out-of-fold prediction. It is not the test-split AUC; that is split_audit.test_split_as_given. - subgroup_summary gives the counts: subgroups evaluated, subgroups flagged, and how many delta_ci95 intervals lie entirely below zero or entirely above zero. Take counts from it instead of counting the subgroups list. Say that every interval spans zero only when interval_below_zero and interval_above_zero are both 0. @@ -26,9 +26,9 @@ Input JSON shape (fields may be absent if a check did not run): "split_audit": { "metric": "auc", "test_split_as_given": float, - "patient_disjoint_sample": float, - "sample_range95": [float, float], - "gap": float + "patient_disjoint_sample": float | null, + "sample_range95": [float, float] | null, + "gap": float | null }, "subgroups": [{ "key": "machine_id | site_id | density | age", @@ -37,7 +37,7 @@ Input JSON shape (fields may be absent if a check did not run): "auc": float, "auc_rest": float, "delta_vs_rest": float, - "delta_ci95": [float, float], + "delta_ci95": [float, float] | null, "mean_predicted": float, "observed_rate": float, "flagged": bool diff --git a/tests/test_validators.py b/tests/test_validators.py index b7c5e6a..4df52ca 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json import sys from pathlib import Path @@ -300,6 +301,20 @@ def test_audit_split_quality_structure_and_direction() -> None: assert check_patient_leakage(df)["overlapping_patient_ids"] > 0 +def test_audit_split_quality_leaves_out_one_class_samples() -> None: + # 200 patients, generator seed 6: the seed-0 sample and some other seeds' + # samples have no positive row, so their AUC is undefined. + df, _ = generate(GeneratorConfig(n_patients=200, seed=6)) + assert validate_predictions(df) == [] + audit = audit_split_quality(df) + assert audit["patient_disjoint_sample"] is None + assert audit["gap"] is None + low, high = audit["sample_range95"] + assert 0.0 <= low <= high <= 1.0 + # NaN would serialize as a bare NaN token, which is not valid JSON. + json.dumps(run_checks(df), allow_nan=False) + + def test_audit_split_quality_is_reproducible() -> None: df = _load("weak_random_split_leakage") assert audit_split_quality(df) == audit_split_quality(df) From 565c17deb62e1fceb8b19c75b56a001a17bd4339 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 18:09:58 +0200 Subject: [PATCH 18/20] Fail readably on API errors and keep the server key off the page The reporter let anthropic.APIError escape. A request that failed with 403 (no credits), 401 (bad key), 429, 529, or a dropped connection printed a traceback from the CLI and no payload, and the browser UI showed the same traceback. _render now raises ReportNotRenderedError from the API error, so both entry points print or show the payload and the reason, as they already did for a cut-off or refused reply, and the CLI exits 1. The UI prefilled the key field with the server's ANTHROPIC_API_KEY. A widget's value is sent to the browser, so anyone who opened a deployed app could reveal that key with the password field's show button. The field now starts empty, and an empty field makes the reporter read ANTHROPIC_API_KEY on the server. --- CLAUDE.md | 4 ++-- README.md | 3 ++- app.py | 11 ++++++++--- reporter/report.py | 35 ++++++++++++++++++++--------------- tests/test_reporter.py | 27 +++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 21 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c8d4aba..31cd97a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,8 +21,8 @@ Named user: a clinical-AI researcher / radiology-lab PhD student who trained a m ## Repo map β€” current state - [clinval_validators/](clinval_validators/) β€” deterministic checks. `run_checks` returns leakage, the split audit, subgroups, the pooled AUC, and `risk`. -- [cli.py](cli.py) β€” `clinval-validate`. Prints the payload and exits 1 when `ANTHROPIC_API_KEY` is unset or Claude returns no complete report (`ReportNotRenderedError`: cut off at `max_tokens`, refused, or no text). Exits 2 when `validate_predictions` reports a problem. -- [app.py](app.py) β€” Streamlit UI over the same checks. The typed API key is passed into the reporter and is not written to `os.environ`. +- [cli.py](cli.py) β€” `clinval-validate`. Prints the payload and exits 1 when `ANTHROPIC_API_KEY` is unset or Claude returns no complete report (`ReportNotRenderedError`: the API request failed, or the reply was cut off at `max_tokens`, refused, or had no text). Exits 2 when `validate_predictions` reports a problem. +- [app.py](app.py) β€” Streamlit UI over the same checks. The typed API key is passed into the reporter and is not written to `os.environ`. An empty field falls back to the server's `ANTHROPIC_API_KEY`; do not prefill the field with it, because widget values reach the browser. - [reporter/](reporter/) β€” the only code that calls the network. Loads the two runtime prompts in [prompts/](prompts/); see [prompts/README.md](prompts/README.md). - [claude-science-artifacts/](claude-science-artifacts/) β€” synthetic-data generator and the committed scenario oracle (`scenarios/*.csv`, manifests, `_scenario_summary.csv`). Do not regenerate those fixtures to make a test pass. - [rsna_to_predictions.py](rsna_to_predictions.py) β€” local-only adapter for an RSNA `train.csv`. Outputs are gitignored. diff --git a/README.md b/README.md index 9dff935..1f880d8 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ echo "exit code: $?" python -m pytest -q ``` -With `ANTHROPIC_API_KEY` set, the same command prints the risk report instead of the payload and exits 0. `--mode critique` asks for a reviewer critique and a Methods paragraph. The key is read from the environment. It is not stored in the repository. If Claude's reply is cut off at the 4,096-token limit, refused, or contains no text, the command prints the payload, names the stop reason on stderr, and exits 1; the browser UI shows the same message. +With `ANTHROPIC_API_KEY` set, the same command prints the risk report instead of the payload and exits 0. `--mode critique` asks for a reviewer critique and a Methods paragraph. The key is read from the environment. It is not stored in the repository. If the API request fails (for example no credits, an invalid key, or no connection), or Claude's reply is cut off at the 4,096-token limit, refused, or contains no text, the command prints the payload, states the reason on stderr, and exits 1; the browser UI shows the same message. ```bash python -m pip install -e '.[demo]' @@ -85,6 +85,7 @@ streamlit run app.py - Choose a bundled scenario or upload a `predictions.csv`. - The checks run without an API key. The page shows the leakage count, the split audit, and the JSON payload, including `risk.level`. - The key typed into the sidebar is sent only with that session's request. It is not written into the process environment. +- When the key field is empty, the app uses `ANTHROPIC_API_KEY` from the server environment. That key stays on the server; the field is never prefilled with it. - **Generate with Claude** asks for the risk report or the reviewer critique. Both are optional. - Download the payload as JSON and, after a report is generated, the report as Markdown. diff --git a/app.py b/app.py index 74346ff..c2e4b44 100644 --- a/app.py +++ b/app.py @@ -68,8 +68,12 @@ def main() -> None: st.header("Claude report") mode = st.radio("Mode", ["risk", "critique"], format_func=str.capitalize) + # The field starts empty: a widget's value is sent to the browser, so + # prefilling it would hand the server's key to every visitor. api_key = st.text_input( - "Anthropic API key", value=os.environ.get(API_KEY_ENV, ""), type="password" + "Anthropic API key", + type="password", + help=f"Leave empty to use {API_KEY_ENV} from the server environment.", ) generate = st.button("Generate with Claude", type="primary") @@ -137,13 +141,14 @@ def main() -> None: report_key = f"{mode}:{json.dumps(payload, sort_keys=True)}" cached = st.session_state.get("report") - if generate and not api_key: + if generate and not (api_key or os.environ.get(API_KEY_ENV)): st.warning("Set an Anthropic API key to render the report.", icon="πŸ”‘") elif generate: render = render_reviewer_critique if mode == "critique" else render_report try: with st.spinner(f"Asking {MODEL} for the {mode} report…"): - cached = (report_key, render(payload, api_key=api_key)) + # None makes the reporter read the server's ANTHROPIC_API_KEY. + cached = (report_key, render(payload, api_key=api_key or None)) st.session_state["report"] = cached except ReportNotRenderedError as exc: st.error(f"{exc} The deterministic payload on the left is complete.", icon="🚫") diff --git a/reporter/report.py b/reporter/report.py index 4e13beb..73b5f32 100644 --- a/reporter/report.py +++ b/reporter/report.py @@ -15,7 +15,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from anthropic import Anthropic +from anthropic import Anthropic, APIError if TYPE_CHECKING: from clinval_validators import ReportPayload @@ -61,9 +61,10 @@ class MissingAPIKeyError(RuntimeError): class ReportNotRenderedError(RuntimeError): """Raised when Claude's reply holds no complete report. - That happens when the reply stops at ``max_tokens``, is refused - (``stop_reason == "refusal"``), or contains no text block. The deterministic - payload is unaffected, so callers can still show it. + That happens when the API request fails (for example no credits, an + invalid key, or no connection), or when the reply stops at ``max_tokens``, + is refused (``stop_reason == "refusal"``), or contains no text block. The + deterministic payload is unaffected, so callers can still show it. """ @@ -97,17 +98,21 @@ def _render( raise MissingAPIKeyError(f"{API_KEY_ENV} is not set in the environment.") client = Anthropic(api_key=api_key) - message = client.beta.messages.create( - model=MODEL, - max_tokens=MAX_TOKENS, - system=_load_system_prompt(system_prompt_path), - messages=[{"role": "user", "content": json.dumps(payload)}], - output_config={"effort": EFFORT}, - betas=[FALLBACK_BETA], - # The SDK types `fallbacks` as a list of models; the "default" mode is a - # string the API accepts, so it goes in the raw body. - extra_body={"fallbacks": "default"}, - ) + try: + message = client.beta.messages.create( + model=MODEL, + max_tokens=MAX_TOKENS, + system=_load_system_prompt(system_prompt_path), + messages=[{"role": "user", "content": json.dumps(payload)}], + output_config={"effort": EFFORT}, + betas=[FALLBACK_BETA], + # The SDK types `fallbacks` as a list of models; the "default" mode is a + # string the API accepts, so it goes in the raw body. + extra_body={"fallbacks": "default"}, + ) + except APIError as exc: + detail = str(exc).rstrip(".") + raise ReportNotRenderedError(f"The Claude API request failed: {detail}.") from exc # Opus 5.5 returns thinking blocks (and, after a fallback, a fallback block) # before the text, so the first block is not always the report. text = "".join(block.text for block in message.content if block.type == "text") diff --git a/tests/test_reporter.py b/tests/test_reporter.py index c4d8c3f..72c74bd 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -6,6 +6,8 @@ import os from types import SimpleNamespace +import anthropic +import httpx import pytest import reporter @@ -183,6 +185,31 @@ def test_a_cut_off_or_refused_reply_raises_a_named_error(monkeypatch, stop_reaso render_report(SAMPLE_PAYLOAD) +def test_an_api_error_raises_a_named_error(monkeypatch) -> None: + # Without this the CLI printed a traceback and no payload, e.g. on a 403 for + # an account that has used its credits. + monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") + request = httpx.Request("POST", "https://api.anthropic.com/v1/messages") + error = anthropic.PermissionDeniedError( + "credit balance is too low", + response=httpx.Response(403, request=request), + body=None, + ) + + class FailingMessages: + def create(self, **kwargs): + raise error + + class FakeAnthropic: + def __init__(self, api_key=None): + self.beta = SimpleNamespace(messages=FailingMessages()) + + monkeypatch.setattr(reporter.report, "Anthropic", FakeAnthropic) + with pytest.raises(ReportNotRenderedError, match="credit balance is too low") as caught: + render_report(SAMPLE_PAYLOAD) + assert caught.value.__cause__ is error + + def test_a_reply_without_text_raises_a_named_error(monkeypatch) -> None: monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") _fake_client(monkeypatch, [SimpleNamespace(type="thinking", thinking="")]) From 08c3ec25e168b3ca290c27550c5eab463f53a7d4 Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 18:09:58 +0200 Subject: [PATCH 19/20] Run CI on the Node 24 releases of checkout and setup-python actions/checkout@v4 and actions/setup-python@v5 run on Node 20, which GitHub has deprecated for actions. Version 7 of both runs on Node 24. Their breaking changes concern pull_request_target, workflow_run, and the removed pip-install input, none of which this workflow uses. --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f8c5c22..7acb07f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,8 +11,8 @@ jobs: matrix: python-version: ["3.11", "3.13"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - run: pip install -e '.[dev]' From c6958df2790e8a060a5ee09da68ca034b02578cc Mon Sep 17 00:00:00 2001 From: vbabenko97 Date: Sun, 18 Oct 2026 18:15:09 +0200 Subject: [PATCH 20/20] Build the API-error test without importing httpx CI installs the newest anthropic release, 1.15.0, which moved its HTTP layer from httpx to httpx2, so the test module failed to import there. The test now hands PermissionDeniedError a stand-in response with the three attributes the error reads. The reporter itself needs no change for 1.x. An offline run against a mock transport sent the same request on anthropic 0.116.0 and 1.15.0 (model, effort, the fallback beta header and fallbacks "default"), and the full suite passes on both. --- tests/test_reporter.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_reporter.py b/tests/test_reporter.py index 72c74bd..e4e201d 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -7,7 +7,6 @@ from types import SimpleNamespace import anthropic -import httpx import pytest import reporter @@ -189,11 +188,11 @@ def test_an_api_error_raises_a_named_error(monkeypatch) -> None: # Without this the CLI printed a traceback and no payload, e.g. on a 403 for # an account that has used its credits. monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") - request = httpx.Request("POST", "https://api.anthropic.com/v1/messages") + # The SDK's HTTP library differs by major version (httpx in 0.x, httpx2 in + # 1.x), so the response is a stand-in with the attributes the error reads. + response = SimpleNamespace(request=None, status_code=403, headers={}) error = anthropic.PermissionDeniedError( - "credit balance is too low", - response=httpx.Response(403, request=request), - body=None, + "credit balance is too low", response=response, body=None ) class FailingMessages: