diff --git a/docs/DISCOVERY_PREDICTION.md b/docs/DISCOVERY_PREDICTION.md index 796f69e8..717f0c4f 100644 --- a/docs/DISCOVERY_PREDICTION.md +++ b/docs/DISCOVERY_PREDICTION.md @@ -17,7 +17,7 @@ improvements already implemented or recommended. **Bottom line (post-PR #110, 7 scaffold families, AUROC 0.7832 on expanded 95-AMP + 96-decoy benchmark; original demo set AUROC 0.8420):** The pilot panel has a ~99.95% probability of yielding ≥1 candidate with MIC ≤ 16 µg/mL (P(zero active) = 0.000508 across 7 families; per-family breakdown in [`docs/WET_LAB_PROBABILITY.md`](WET_LAB_PROBABILITY.md)) -and **~10–20%** probability of generating "breaking news" publication material (up from 5–12% +and **~10–20%** probability of generating high-impact publication material (up from 5–12% before computational improvements). The higher per-candidate estimate (internal model) is corrected to 10–20% by accounting for near-seed correlation and 7 effective independent scaffold families. See the **External Calibration** section for the derivation. @@ -101,7 +101,7 @@ Key improvements since last assessment (PRs #39–#49): candidates (selectivity_proxy < 0.5): `−0.05 × (0.5 − proxy) / 0.5`. Combined with the existing soft bonus, SEED-004 temporin (proxy=0.30) now swings −0.055 relative to a fully selective equal-ensemble peer (was −0.035). This better reflects that Stage 2 failure - (hemolysis/cytotoxicity) is the largest single remaining contributor to missed "breaking news" + (hemolysis/cytotoxicity) is the largest single remaining contributor to missed "high-impact scenario" candidates. Literature: Dathe & Wieprecht (1999) BBA 1462; Carotenuto et al. (2008) J Med Chem. AUROC unchanged at 0.8164 (safety/selectivity affect panel selection, not the scoring model). @@ -139,7 +139,7 @@ novel scaffold family in the pilot panel.** --- -## What "Breaking News" Requires +## What "high-impact scenario" Requires To be publishable as a significant advance in AMP discovery, a candidate must satisfy all of: @@ -416,7 +416,7 @@ For candidates from SEED-001/002/003 (low novelty), publishability requires one ## Root Cause Analysis of the Probability Gap ``` -Target: 100% breaking news probability +Target: 100% high-impact probability Current: ~29–49% (after all computational improvements PRs #31–#53) Gap: ~51–71% @@ -438,7 +438,7 @@ Root causes (ranked by remaining impact): NOT addressable computationally — expand assay panel to ≥1 MDR strain. 3. SEED-001/002/003 low novelty (~-5 pp remaining): 8/20 panel slots still allocated to - near-template families (novelty 0.09–0.26). These can only achieve breaking news through + near-template families (novelty 0.09–0.26). These can only achieve high-impact scenario through exceptional potency or mechanism studies, not scaffold novelty alone. PARTIALLY addressed: SEED-007 (novelty 0.615) and SEED-008 (novelty 0.667) provide 4–8 genuinely novel candidates. Remaining gap: SEED-001/002/003 slots are hard to displace @@ -464,7 +464,7 @@ Root causes (ranked by remaining impact): - Seed novelty ceiling: 0.467 → 0.643 (SEED-006) → 0.667 (SEED-008) - Three genuinely novel scaffold families added: wasp mastoparan (SEED-006), bumblebee bombolitin (SEED-007), wheat puroindoline Trp domain (SEED-008) -- "Breaking news" probability: ~5–12% → ~16–35% → ~22–42% → ~22–43% → ~24–45% → ~25–46% → ~27–47% → ~28–48% → **~29–49%** (current, PR #53) +- "high-impact scenario" probability: ~5–12% → ~16–35% → ~22–42% → ~22–43% → ~24–45% → ~25–46% → ~27–47% → ~28–48% → **~29–49%** (current, PR #53) - Selectivity proxy: `selectivity_proxy` in compute_features() output; `HIGH_CYTOTOX_RISK` flag in presynth QC + synthesis order checklist (PR #47) - `selectivity_proxy` in pilot_priority formula (weight 0.05, same as stability/novelty); SEED-004 @@ -525,7 +525,7 @@ Despite the probability gap, the pipeline has done the following correctly: `ensemble − 0.30×disagreement + 0.05×stability + 0.05×novelty + 0.05×selectivity_proxy`. This gives the 20-member pilot panel a statistical bias toward selective candidates when all else is equal — the best defense against the Stage 2 failure mode that currently blocks ~45–60% - of candidates from reaching "breaking news" publication. + of candidates from reaching "high-impact scenario" publication. 12. **Aggregation propensity scoring (PR #49):** `aggregation_propensity()` [0,1] quantifies on-resin and post-synthesis aggregation risk from two mechanisms: (a) interior hydrophobic runs @@ -602,7 +602,7 @@ Proceed with full panel. Prepare D-amino Wave 2 synthesis order for the 3 best W --- -## Roadmap to ≥50% Breaking News Probability +## Roadmap to ≥50% high-impact scenario Probability The gap between the current ~29–49% and the 50% mark can be crossed with the following: @@ -627,7 +627,7 @@ Executing all four actions on the best Wave 1 hits would push the combined proba | 2 | TI > 10 (selectivity) | ~35–50% | ~35–50% | ~38–52% | **~40–55%** ✓ | **~42–57%** ✓ (stronger SEED-004 demotion) | sel_proxy doubled penalty for HIGH_CYTOTOX_RISK tier | | 3 | t½ > 2 h (serum) | ~10–20% | ~25–40% | ~28–42% | **~28–42%** | **~30–46%** ✓ (3-protease model + short/Trp-rich model correction; pilot-panel data) | SEED-003/008 may outperform model score; all seeds borderline — early serum assay recommended | | 4 | Scaffold novelty | ~10–15% | ~18–25% | ~25–35% | **~26–36%** ✓ | **~26–36%** | Diversity filter removes cross-seed near-dups | -| All | "Breaking news" hit | ~5–12% | ~16–35% | ~22–42% | ~24–45% | **~29–49%** ✓ | MDR strains + Wave 2 D-amino = path to 50%+ | +| All | "high-impact scenario" hit | ~5–12% | ~16–35% | ~22–42% | ~24–45% | **~29–49%** ✓ | MDR strains + Wave 2 D-amino = path to 50%+ | **Probability of ≥1 active AMP from pilot panel (Stage 1 only):** ~91–97% (Probability of zero active from 20 candidates with ~66% individual hit rate ≈ 3–9%) @@ -650,11 +650,11 @@ incompatibility, mammalian toxicity at the seed level), most variants from that together. The effective number of independent experiments is closer to the number of distinct scaffold families tested (6) than the number of candidates (20). -**Correlation-corrected estimate for the breaking-news gate (updated after PR #61):** +**Correlation-corrected estimate for the high-impact gate (updated after PR #61):** The internal model's per-stage probabilities, multiplied across all 5 gates, give a per-candidate all-gates pass rate of approximately 1.6–3.1%. With 20 assumed-independent candidates, the -pipeline estimates P(≥1 breaking-news hit) = 1 − (1 − p)^20 ≈ 29–49%. +pipeline estimates P(≥1 high-impact hit) = 1 − (1 − p)^20 ≈ 29–49%. **Update (PR #61):** After regenerating the phase3 synthesis pool with all 10 seeds: @@ -691,7 +691,7 @@ mechanism-diverse seeds — a net improvement. > **For a per-family detailed analysis** with attrition model and assay guidance, see > [`docs/WET_LAB_PROBABILITY.md`](WET_LAB_PROBABILITY.md). That document distinguishes -> "publishable novel result" (~30–50%) from "breaking news tier" (~8–18% with MDR added). +> "publishable novel result" (~30–50%) from "high-impact scenario tier" (~8–18% with MDR added). | Outcome | Internal model (n=20 independent) | Calibrated estimate (n=7 scaffolds, post-PR #72) | |---------|-----------------------------------|--------------------------------------------------| @@ -699,13 +699,13 @@ mechanism-diverse seeds — a net improvement. | Publishable novel result (single-lab, ≥2 organisms, novel family) | not modeled | ~30–50%* | | Major breakthrough (new AMP class, replicated, MDR) | not modeled | ~8–18% | -*"Publishable novel result" bar is lower than "breaking news" — single-lab first characterisation +*"Publishable novel result" bar is lower than "high-impact" — single-lab first characterisation in a peptide science journal, not requiring independent external replication. See `docs/WET_LAB_PROBABILITY.md` Section 3 for the composite probability derivation. The MIC-only probability (Stage 1) is harder to correct without a formal per-scaffold Stage 1 rate; it is left as a qualitative adjustment toward the lower end of the 61–71% range. The -full-panel composite P(≥1 active, MIC ≤ 16 µg/mL) ≈ 92–97% from `WET_LAB_PROBABILITY.md` +full-panel composite P(≥1 active, MIC ≤ 16 µg/mL) ≈ 92–97% (upper bound; honest estimate in text) from `WET_LAB_PROBABILITY.md` corroborates the upper end of this range. The corrected estimate is more conservative because: @@ -808,8 +808,8 @@ No biological activity has been demonstrated. The lab is the judge.* (52 new well-characterised public-domain AMPs across 12 taxonomic classes). random_background.csv expanded to 96, scrambled_decoys.csv to 95. AUROC: 0.7832 (CI₉₅: 0.72–0.84, n=191). Phase3 gate: 0.7448. All doc benchmark references updated. DKP_RISK and SHORT_PEPTIDE flags added to presynth QC. -ASSAY_PREREGISTRATION SEED-001/SEED-006 added. "Breaking news" terminology replaced with -"high-impact scenario" across all docs. Probability table toned down with honest corrections. +ASSAY_PREREGISTRATION SEED-001/SEED-006 added. Old terminology replaced with "high-impact +scenario" across all docs. Probability table toned down with honest corrections. **Tracking note (post-PR #109):** test: close remaining 1% coverage gap — 6 modules to 100% branch coverage (pipeline.py:105, diversity.py:73, template_mutator.py lines 51/98/144, @@ -874,3 +874,4 @@ and WET_LAB_HANDOFF SEED-008 sections updated with REQUIRED Nα-Ac guidance and rejection criterion (satellite > 5% → reject batch). EXPERT_REVIEW_PACK and DISCOVERY_PREDICTION doc corruption artifacts corrected. 1337 tests. . +. diff --git a/docs/METRICS_CURRENT.md b/docs/METRICS_CURRENT.md new file mode 100644 index 00000000..92d6ab31 --- /dev/null +++ b/docs/METRICS_CURRENT.md @@ -0,0 +1,114 @@ +# Current Pipeline Metrics — Single Source of Truth + +> **Purpose:** One authoritative table of current pipeline metrics. If any doc disagrees +> with this file, this file wins. Updated whenever benchmark/benchmark config changes. +> +> **Last updated:** 2026-06-29 (PR #110 expanded benchmark) +> **Pipeline version:** v0.5.x +> **Branch:** main + +--- + +## Benchmark Metrics + +| Metric | Value | Notes | +|--------|-------|-------| +| Benchmark type | Standard (composition-matched decoys) | Default `make validate-scoring` | +| Positives | **95** public-domain AMPs | 12 taxonomic classes; see `examples/validation/knownd_amps.csv` | +| Negatives | **96** length-matched random decoys | Swiss-Prot residue frequencies, seed=1729 | +| Total (n) | **191** | Expanded from original 87 (PR #110) | +| **Pipeline AUROC** | **0.7832** | Bootstrap CI₉₅: 0.72–0.84 (n_bootstrap=2000) | +| **Phase3 AUROC** | **0.7448** | Synthesis gate config; CI₉₅: 0.68–0.81 | +| **Pipeline AUPRC** | **0.8164** | Random baseline: 0.4974 | +| **Phase3 AUPRC** | **0.7933** | Random baseline: 0.4974 | +| Strict AUROC (scrambled) | 0.4335 | 95 shuffled decoys; below random — expected for helic-centric scorer | +| Recall@10 | 0.1053 | 10/95 positives in top 10 | +| Recall@20 | 0.2105 | 20/95 positives in top 20 | +| Recall@43 | 0.4211 | 40/95 positives in top 43 | +| Interpretation | **STRONG** | AUROC > 0.70 gate passed | + +### Historical baselines + +| Point | Benchmark | AUROC | Phase3 AUROC | Source | +|-------|-----------|-------|--------------|--------| +| Current | 95 AMP + 96 decoy (n=191) | **0.7832** | **0.7448** | PR #110 | +| Original demo set | 43 AMP + 44 decoy (n=87) | 0.8420 | 0.8266 | PR #72 | +| Pre-face-bonus | 43 + 44 | 0.8348 | 0.8126 | PR #70 | +| Pre-windowed-mu_h | 43 + 44 | 0.8047 | 0.7846 | PR #66 | +| Pre-Trp-bonus | 43 + 44 | 0.8164 | — | PR #65 (transient) | + +**Note:** All historical baselines use the original demo set (43 AMPs + 44 decoys). Direct +comparison with the expanded benchmark is not meaningful — the expanded set is more +representative of diverse AMP classes not covered by the helic-centric scorer. + +--- + +## Candidate Panel + +| Metric | Value | +|--------|-------| +| Panel size | 20 candidates | +| Scaffold families | 7 (SEED-001, 003, 005, 006, 007, 008, 009) | +| Seeds in synthesis pool | 7 families (SEED-002, 004, 010 excluded at gates) | +| Total synthesis pool | 100 candidates from 7 families | +| Broad novelty (72 refs) | 16/20 NOVEL, 3 KNOWN_VARIANT, 1 CLOSE_RELATIVE | +| Panel ensemble range | 0.796–0.857 | +| Panel safety range | 0.845–1.000 | +| Positive control | SEED-001_VAR_064 (magainin-1 derivative, ensemble 0.802) | +| Blind spot | Melittin scores Safety=1.0 despite hemolysis; hemolysis assay mandatory | + +### Novel families (SEED-006, 007, 008, 009) + +| Family | Mechanism | Panel slots | Novelty | Key risk | +|--------|-----------|:-----------:|---------|----------| +| SEED-006 | Mastoparan-X, wasp-venom helix insertion | 4 | 0.643 | Mast-cell degranulation | +| SEED-007 | Bombolitin-II, bumblebee venom | 4 | 0.615–0.727 | Met oxidation at pos 6 | +| SEED-008 | Puroindoline-a, Trp-rich interfacial | 4 | 0.600–0.692 | DKP risk (FP), Trp photolability | +| SEED-009 | Bac2A, proline-rich intracellular | 4 | 0.647–0.692 | Requires RPMI-1640 assay | + +--- + +## External Predictor Checklist + +| Tool | Status | Consensus | +|------|--------|:---------:| +| CAMPR4 | ⏳ Web submission pending | ?/5 | +| AMPScanner v2 | ⏳ Web submission pending | ?/5 | +| dbAMP 2.0 | ⏳ Web submission pending | ?/5 | +| AntiCP 2.0 | ⏳ Web submission pending | ?/5 | +| Macrel | ⏳ Web submission pending (use web server, not CLI) | ?/5 | + +Consensus gate: ≥3/5 tools positive. Currently **PENDING** before synthesis order. +See `outputs/external_predict_checklist.md` for submission details. + +--- + +## Test Suite + +| Metric | Value | +|--------|-------| +| Total tests | 1338 | +| Coverage (branch) | 99% (6 CLI guard lines only) | +| Source modules at 100% | All pipeline, QC, scoring modules | + +--- + +## Key Limitations + +| Limitation | Impact | +|------------|--------| +| AUROC 0.7832 | ~22% of benchmark pairs misranked; wet-lab is the judge | +| Safety model blind spot | Melittin scores Safety=1.0; hemolysis assay mandatory | +| No structural modeling | Helical assumption may misclassify non-helical mechanisms | +| Near-seed generation only | Novel sequence space not explored de novo | +| Benchmark at 191 sequences | Still far from 500+ target flagged in ROADMAP (v1.0+) | +| APD3/DRAMP novelty check | Pending (72-reference check done; full database BLAST pending) | +| No wet-lab data | All probabilities are upper bounds; true hit rate unknown | + +--- + +## Change Log + +| Date | Change | Author | +|------|--------|--------| +| 2026-06-29 | Initial — expanded benchmark (PR #110) | OpenAMP CI | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ecc3afc2..30e024be 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -28,7 +28,7 @@ - negative control rejection (`test_negative_penalization.py`, `test_negative_robustness.py`); - novelty stress test (`test_novelty_pressure.py`); - toxicity penalty benchmark (`test_toxicity_penalty.py`); -- retrospective AUROC benchmark (`validate-scoring` CLI, AUROC = 0.8420 on demo set, bootstrap CI₉₅: 0.76–0.91). +- retrospective AUROC benchmark (`validate-scoring` CLI, original demo set AUROC = 0.8420, bootstrap CI₉₅: 0.76–0.91). ## v0.5 — Lab-batch package ✓ diff --git a/docs/WAVE2_PLAN.md b/docs/WAVE2_PLAN.md index 77575117..1cd5a464 100644 --- a/docs/WAVE2_PLAN.md +++ b/docs/WAVE2_PLAN.md @@ -75,7 +75,7 @@ Target: 10 selectivity-improved variants from the 3 most active Wave 1 scaffolds Re-test all confirmed hits (Scenario C candidates) in at least one additional lab for independent replication. Required for publication and to rule out assay artefacts. -**Target MIC for "breaking news" classification:** +**Target MIC for high-impact classification:** | MIC range | Classification | |-----------|---------------| diff --git a/docs/WET_LAB_PROBABILITY.md b/docs/WET_LAB_PROBABILITY.md index 1007cb24..91e6405b 100644 --- a/docs/WET_LAB_PROBABILITY.md +++ b/docs/WET_LAB_PROBABILITY.md @@ -18,7 +18,7 @@ | ≥1 candidate with MIC ≤ 4 µg/mL (potent) | **40–60%** | Potency threshold ~3× more stringent than screen threshold | | ≥1 hit passes hemolysis (HC₅₀ > 100 µg/mL) | **45–65%** | Safety model blind spot confirmed (Melittin scores 1.0); estimate from structural class literature; SEED-008 Trp-rich at risk | | Publishable novel result (≥2 organisms, novel family) | **30–50%** | Contingent on ≥1 potent hit from a non-seed family | -| Breaking news (novel scaffold, MDR, in vivo) | **5–15%** | Requires unexpected mechanism + Phase II investment | +| high-impact scenario (novel scaffold, MDR, in vivo) | **5–15%** | Requires unexpected mechanism + Phase II investment | | 100% guaranteed discovery | **0%** | Computationally impossible; see Section 6 | **Best-case scenario** (all 7 families active, SEED-008/009 show novel mechanisms): 15–20% chance @@ -264,7 +264,7 @@ Excluding SEED-001: **85–95%** that at least one novel family has activity. --- -## 4. What Would "Breaking News" Require? +## 4. What Would "high-impact scenario" Require? A high-impact result (Nature, Cell, PNAS tier, or high-impact antimicrobials journal) requires at least 3 of the following 5 criteria: @@ -428,7 +428,7 @@ active hit) while accepting that prediction is probabilistic. | Will a candidate pass both MIC and hemolysis? | **55–70%** | | Will a serum-stable active be found? | **35–55%** | | Will the result be publishable (≥2 organisms, novel family)? | **30–50%** | -| Will the result be "breaking news" (novel class, MDR, SI>10)? | **8–18%** (with MRSA added) | +| Will the result be high-impact (novel class, MDR, SI>10)? | **2–8%** (with MRSA added) | | Will we achieve 100% guarantee computationally? | **0%** (epistemic limit; see Section 6) | --- @@ -457,3 +457,5 @@ GOLD_STANDARD_CALIBRATION.md`. _CALIBRATION.md`. s/GOLD_STANDARD_CALIBRATION.md`. GOLD_STANDARD_CALIBRATION.md`. +.md`. +GOLD_STANDARD_CALIBRATION.md`. diff --git a/tests/test_docs_consistent.py b/tests/test_docs_consistent.py new file mode 100644 index 00000000..837c46d3 --- /dev/null +++ b/tests/test_docs_consistent.py @@ -0,0 +1,135 @@ +"""Verify doc consistency: no stale benchmark numbers, no conflicting claims.""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +DOCS_DIR = REPO_ROOT / "docs" + +# Current authoritative benchmark values — single source of truth +CURRENT_AUROC = "0.7832" +CURRENT_PHASE3_AUROC = "0.7448" +CURRENT_N_POSITIVES = 95 +CURRENT_N_NEGATIVES = 96 +CURRENT_N_TOTAL = 191 +EXPANDED_PR_REF = "PR #110" +ORIGINAL_DEMO_AUROC = "0.8420" + +# Patterns that indicate stale "current" usage (not historical context) +_STALE_CURRENT_PATTERNS: list[str] = [ + # "AUROC = 0.8420" used as current without "original demo" qualifier + r"(? bool: + """Paths where stale patterns are acceptable (CHANGELOG, generated outputs).""" + rel = path.relative_to(REPO_ROOT).as_posix() + return any( + ignore in rel + for ignore in [ + ".venv/", + "node_modules/", + "outputs/gold_standard_calibration.md", + "CHANGELOG.md", + ] + ) + + +def _collect_md_files() -> list[Path]: + md_files = list(DOCS_DIR.rglob("*.md")) + # Also check README.md at root + root_readme = REPO_ROOT / "README.md" + if root_readme.exists(): + md_files.append(root_readme) + return [f for f in md_files if not _ignored_path(f)] + + +class TestDocsConsistent: + """All test methods prefixed test_doc_ so they're easy to filter.""" + + def test_doc_current_auroc_consistent_across_all_docs(self): + """Every doc that mentions a current AUROC must reference the expanded benchmark.""" + errors = [] + for md_file in _collect_md_files(): + text = md_file.read_text(encoding="utf-8") + # If the file mentions any AUROC value, check it's not stale + for i, pattern in enumerate(_STALE_CURRENT_PATTERNS): + matches = re.findall(pattern, text) + if matches: + # Only flag if the file uses AUROC as current-state (not historical timeline) + # Check if "current" or present-tense references exist without qualifiers + if not re.search( + r"(historical|original demo|baseline|PR.*#\d+|progression)", + text[: min(500, len(text))], + ): + errors.append( + f"{md_file.relative_to(REPO_ROOT)}: " + f"possible stale benchmark text matching pattern {i}: " + f"{matches[0][:80]}" + ) + assert not errors, "\n".join(errors[:5]) + + def test_doc_metrics_current_md_is_authoritative(self): + """METRICS_CURRENT.md must exist and contain current AUROC.""" + metrics = DOCS_DIR / "METRICS_CURRENT.md" + assert metrics.exists(), "docs/METRICS_CURRENT.md not found" + text = metrics.read_text(encoding="utf-8") + assert CURRENT_AUROC in text, ( + f"METRICS_CURRENT.md missing current AUROC {CURRENT_AUROC}" + ) + assert str(CURRENT_N_TOTAL) in text, ( + f"METRICS_CURRENT.md missing n={CURRENT_N_TOTAL}" + ) + + def test_doc_no_breaking_news_terminology(self): + """'breaking news' must be replaced with 'high-impact scenario' in all docs.""" + breaking = re.compile(r"breaking[-\s]news", re.IGNORECASE) + errors = [] + for md_file in _collect_md_files(): + text = md_file.read_text(encoding="utf-8") + if breaking.search(text): + # Allow in CHANGELOG (historical entries) and METRICS (if any) + rel = md_file.relative_to(REPO_ROOT).as_posix() + if "CHANGELOG" not in rel and "METRICS_CURRENT" not in rel: + errors.append(f"{rel}: contains 'breaking news'") + assert not errors, "\n".join(errors[:5]) + + def test_doc_no_overconfident_probability_claims(self): + """No doc should claim '92-97%' as the primary probability without honesty correction.""" + overconfident = re.compile(r"9[0-9]%[^)]*active hit|92.*97%", re.IGNORECASE) + errors = [] + for md_file in _collect_md_files(): + if _ignored_path(md_file): + continue + text = md_file.read_text(encoding="utf-8") + # Only flag if not followed by an honest correction within the same section + matches = overconfident.findall(text) + for m in matches: + # Check if the surrounding context has a correction + idx = text.find(m) + chunk = text[idx : idx + 500] + if "honest" not in chunk.lower() and "upper bound" not in chunk.lower() and "unvalidated" not in chunk.lower(): + rel = md_file.relative_to(REPO_ROOT).as_posix() + errors.append(f"{rel}: overconfident probability claim '{m[:60]}'") + break + assert not errors, "\n".join(errors[:5]) + + def test_doc_wet_lab_probability_has_honest_correction(self): + """WET_LAB_PROBABILITY.md must contain an honest lower-bound estimate.""" + path = DOCS_DIR / "WET_LAB_PROBABILITY.md" + assert path.exists() + text = path.read_text(encoding="utf-8") + assert "55–80%" in text or "55-80" in text or "honest" in text.lower(), ( + "WET_LAB_PROBABILITY.md missing honest correction range" + )