Spaces:
Running
Running
github-actions[bot] commited on
Commit ·
ea9cb88
1
Parent(s): 3f7edda
Deploy 53117c1
Browse filesValidation corpus, a specificity handoff, and the design record
Source: https://github.com/WINTER4000/turingDNA/commit/53117c103469b28cb1ec444c32e955243a998aa8
- dee/core/compiler.py +180 -1
- dee/core/compiler_validation.py +202 -0
- dee/data/landmark_designs.json +169 -0
- dee/server.py +16 -0
- tests/test_compiler.py +65 -0
- tests/test_compiler_validation.py +97 -0
dee/core/compiler.py
CHANGED
|
@@ -430,6 +430,32 @@ CAPABILITY_NOTES = {
|
|
| 430 |
"GUIDE-seq, CIRCLE-seq or an equivalent empirical assay."),
|
| 431 |
}
|
| 432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
SCOPE = {
|
| 434 |
"application": "Somatic therapeutic design only. Germline and embryo "
|
| 435 |
"editing are out of scope and refused.",
|
|
@@ -563,8 +589,19 @@ def compile_report(wt_allele: str, patient_allele: str, *,
|
|
| 563 |
# ── specificity ─────────────────────────────────────────────────────
|
| 564 |
# Always carries its caveat, even when it runs: a pass that reports "ok"
|
| 565 |
# on a coding-sequence-only index would read as a clean bill of health.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
add("specificity", "warn" if can_check_specificity else "unavailable",
|
| 567 |
-
|
| 568 |
|
| 569 |
# ── emit ────────────────────────────────────────────────────────────
|
| 570 |
add("emit", "ok",
|
|
@@ -887,3 +924,145 @@ def plan_base_edit_strategies(window: str, target_offset: int,
|
|
| 887 |
"choosing — a silent or intronic bystander in a regulatory "
|
| 888 |
"element is exactly the case nothing else checks."))
|
| 889 |
return out[:max_results], diags
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
"GUIDE-seq, CIRCLE-seq or an equivalent empirical assay."),
|
| 431 |
}
|
| 432 |
|
| 433 |
+
# What the specificity pass hands over when it cannot clear a guide itself.
|
| 434 |
+
# A refusal that just stops is a shrug; a refusal that says exactly what to run
|
| 435 |
+
# next is a handoff. These are the searches this deployment is NOT doing, named
|
| 436 |
+
# precisely enough to be executed by someone who has the tools.
|
| 437 |
+
SPECIFICITY_HANDOFF = {
|
| 438 |
+
"in_silico": [
|
| 439 |
+
("Cas-OFFinder / CRISPRme", "genome-wide, mismatch- and bulge-tolerant "
|
| 440 |
+
"search over the WHOLE assembly, not only coding sequence. CRISPRme "
|
| 441 |
+
"additionally accounts for common variants, which matters when the "
|
| 442 |
+
"patient's own genome differs from the reference at an off-target."),
|
| 443 |
+
],
|
| 444 |
+
"empirical": [
|
| 445 |
+
("GUIDE-seq", "unbiased, cell-based detection of double-strand-break "
|
| 446 |
+
"capture sites."),
|
| 447 |
+
("CIRCLE-seq / CHANGE-seq", "in vitro, high-sensitivity nomination of "
|
| 448 |
+
"candidate off-targets from purified genomic DNA."),
|
| 449 |
+
("Targeted amplicon sequencing", "deep sequencing of the nominated "
|
| 450 |
+
"sites in the actual therapeutic cell product."),
|
| 451 |
+
],
|
| 452 |
+
"note": (
|
| 453 |
+
"For a base editor the relevant off-target question is not only where "
|
| 454 |
+
"the nuclease cuts. Cas-independent deamination is not detected by a "
|
| 455 |
+
"DSB-capture assay at all, so a clean GUIDE-seq result does not, on "
|
| 456 |
+
"its own, clear a base editor."),
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
SCOPE = {
|
| 460 |
"application": "Somatic therapeutic design only. Germline and embryo "
|
| 461 |
"editing are out of scope and refused.",
|
|
|
|
| 589 |
# ── specificity ─────────────────────────────────────────────────────
|
| 590 |
# Always carries its caveat, even when it runs: a pass that reports "ok"
|
| 591 |
# on a coding-sequence-only index would read as a clean bill of health.
|
| 592 |
+
#
|
| 593 |
+
# And it now HANDS OFF. A refusal that just stops is a shrug; one that
|
| 594 |
+
# names the exact searches this deployment is not doing is a work order
|
| 595 |
+
# somebody can act on. That difference is most of the value of admitting
|
| 596 |
+
# the gap in the first place.
|
| 597 |
+
spec_detail = CAPABILITY_NOTES["specificity"] + " Not cleared here — run: "
|
| 598 |
+
spec_detail += "; ".join(
|
| 599 |
+
f"{name} ({why.split('.')[0].lower()})"
|
| 600 |
+
for name, why in (SPECIFICITY_HANDOFF["in_silico"]
|
| 601 |
+
+ SPECIFICITY_HANDOFF["empirical"]))
|
| 602 |
+
spec_detail += ". " + SPECIFICITY_HANDOFF["note"]
|
| 603 |
add("specificity", "warn" if can_check_specificity else "unavailable",
|
| 604 |
+
spec_detail)
|
| 605 |
|
| 606 |
# ── emit ────────────────────────────────────────────────────────────
|
| 607 |
add("emit", "ok",
|
|
|
|
| 924 |
"choosing — a silent or intronic bystander in a regulatory "
|
| 925 |
"element is exactly the case nothing else checks."))
|
| 926 |
return out[:max_results], diags
|
| 927 |
+
|
| 928 |
+
|
| 929 |
+
# ═══════════════════════════════════════════════════════════════════════
|
| 930 |
+
# The design record
|
| 931 |
+
# ═══════════════════════════════════════════════════════════════════════
|
| 932 |
+
# In a platform-IND world the deliverable is not a therapy — it is a design
|
| 933 |
+
# and its complete justification, in a form a reviewer can audit and a third
|
| 934 |
+
# party can reproduce. Today that document is assembled by hand, per patient,
|
| 935 |
+
# by scientists. This emits it.
|
| 936 |
+
#
|
| 937 |
+
# Deterministic by construction: same inputs, byte-identical output. Nothing
|
| 938 |
+
# here reads a clock or a random source, because a record that changes between
|
| 939 |
+
# runs cannot be diffed, and a record that cannot be diffed cannot be audited.
|
| 940 |
+
# The caller stamps time and provenance if it wants them.
|
| 941 |
+
|
| 942 |
+
RECORD_VERSION = "1"
|
| 943 |
+
|
| 944 |
+
|
| 945 |
+
def design_record(report: "CompileReport", *, variant: str = "",
|
| 946 |
+
resolved: Optional[Dict[str, object]] = None,
|
| 947 |
+
provenance: Optional[Dict[str, str]] = None) -> str:
|
| 948 |
+
"""Render a compile report as a plain-text design record.
|
| 949 |
+
|
| 950 |
+
Plain text on purpose: it diffs, it pastes into an email or a lab
|
| 951 |
+
notebook, it survives every tool in the chain, and nothing about it can
|
| 952 |
+
silently re-render differently later.
|
| 953 |
+
"""
|
| 954 |
+
L: List[str] = []
|
| 955 |
+
add = L.append
|
| 956 |
+
|
| 957 |
+
add("TURINGDNA THERAPEUTIC DESIGN RECORD")
|
| 958 |
+
add(f"record-version {RECORD_VERSION}")
|
| 959 |
+
add("=" * 72)
|
| 960 |
+
add("")
|
| 961 |
+
add("SCOPE")
|
| 962 |
+
for k in ("application", "status", "silent_on"):
|
| 963 |
+
add(f" {k:<12} {report.scope.get(k, '')}")
|
| 964 |
+
add("")
|
| 965 |
+
|
| 966 |
+
if variant or resolved:
|
| 967 |
+
add("VARIANT")
|
| 968 |
+
if variant:
|
| 969 |
+
add(f" notation {variant}")
|
| 970 |
+
for key, label in (("gene", "gene"), ("transcript", "transcript"),
|
| 971 |
+
("chrom", "chromosome"), ("position", "position"),
|
| 972 |
+
("assembly", "assembly"), ("consequence", "consequence"),
|
| 973 |
+
("orientation", "allele orientation"),
|
| 974 |
+
("source", "resolved by")):
|
| 975 |
+
val = (resolved or {}).get(key)
|
| 976 |
+
if val:
|
| 977 |
+
add(f" {label:<12} {val}")
|
| 978 |
+
if resolved:
|
| 979 |
+
add(f" {'alleles':<12} wild-type {resolved.get('wt_base')} -> "
|
| 980 |
+
f"patient {resolved.get('patient_base')} (forward strand)")
|
| 981 |
+
add("")
|
| 982 |
+
|
| 983 |
+
corr = report.lesion.correction if report.lesion else None
|
| 984 |
+
if report.lesion:
|
| 985 |
+
add("LESION")
|
| 986 |
+
add(f" kind {report.lesion.kind}")
|
| 987 |
+
add(f" size {report.lesion.size}")
|
| 988 |
+
add(f" route {report.lesion.route}")
|
| 989 |
+
if corr:
|
| 990 |
+
add(f" correction {corr.sense_change} on the sense strand")
|
| 991 |
+
add(f" chemistry {corr.editor_family} writes {corr.editor_change} "
|
| 992 |
+
f"on the {corr.strand} strand")
|
| 993 |
+
add("")
|
| 994 |
+
|
| 995 |
+
add("PASSES")
|
| 996 |
+
for p in report.passes:
|
| 997 |
+
add(f" [{p.status:<11}] {p.title}")
|
| 998 |
+
if p.detail:
|
| 999 |
+
for line in _wrap(p.detail, 68):
|
| 1000 |
+
add(f" {line}")
|
| 1001 |
+
for d in p.diagnostics:
|
| 1002 |
+
add(f" - {d.level.upper()} {d.code}: {d.message}")
|
| 1003 |
+
if d.remedy:
|
| 1004 |
+
for line in _wrap(f"remedy: {d.remedy}", 62):
|
| 1005 |
+
add(f" {line}")
|
| 1006 |
+
add("")
|
| 1007 |
+
|
| 1008 |
+
if report.strategies:
|
| 1009 |
+
add("STRATEGIES")
|
| 1010 |
+
for s in report.strategies:
|
| 1011 |
+
add(f" #{s.rank} {s.editor_id} on the "
|
| 1012 |
+
f"{'sense' if s.strand == '+' else 'antisense'} strand")
|
| 1013 |
+
add(f" spacer {s.spacer}")
|
| 1014 |
+
add(f" PAM {s.pam}")
|
| 1015 |
+
add(f" forward position {s.position}")
|
| 1016 |
+
add(f" target at spacer {s.target_spacer_pos} "
|
| 1017 |
+
f"(editor activity {s.target_activity})")
|
| 1018 |
+
add(f" on-target score {s.on_target_score}")
|
| 1019 |
+
if not s.bystanders:
|
| 1020 |
+
add(" bystanders none in this guide's window")
|
| 1021 |
+
else:
|
| 1022 |
+
add(f" bystanders {len(s.bystanders)}")
|
| 1023 |
+
for b in s.bystanders:
|
| 1024 |
+
d = ("not scored" if b.delta_ll is None
|
| 1025 |
+
else f"delta log-likelihood {b.delta_ll:+.4f}")
|
| 1026 |
+
add(f" {b.label or (b.from_base + '>' + b.to_base)}"
|
| 1027 |
+
f" spacer pos {b.spacer_pos} {d}")
|
| 1028 |
+
add("")
|
| 1029 |
+
|
| 1030 |
+
gaps = report.incomplete_because
|
| 1031 |
+
add("COMPLETENESS")
|
| 1032 |
+
add(f" compiled {'yes' if report.compiled else 'NO'}")
|
| 1033 |
+
if gaps:
|
| 1034 |
+
add(" NOT ESTABLISHED by this record:")
|
| 1035 |
+
for g in gaps:
|
| 1036 |
+
add(f" - {g}")
|
| 1037 |
+
else:
|
| 1038 |
+
add(" every pass produced a result")
|
| 1039 |
+
add("")
|
| 1040 |
+
|
| 1041 |
+
if provenance:
|
| 1042 |
+
add("PROVENANCE")
|
| 1043 |
+
for k in sorted(provenance):
|
| 1044 |
+
add(f" {k:<12} {provenance[k]}")
|
| 1045 |
+
add("")
|
| 1046 |
+
|
| 1047 |
+
add("-" * 72)
|
| 1048 |
+
add("This is a design record, not a clinical decision and not an approval.")
|
| 1049 |
+
add("Predicted specificity is not measured specificity. Model-derived")
|
| 1050 |
+
add("judgements are zero-shot and have no validated relationship to")
|
| 1051 |
+
add("clinical outcome. Nothing here substitutes for the preclinical")
|
| 1052 |
+
add("programme, and nothing here should reach a patient on its own.")
|
| 1053 |
+
return "\n".join(L)
|
| 1054 |
+
|
| 1055 |
+
|
| 1056 |
+
def _wrap(text: str, width: int) -> List[str]:
|
| 1057 |
+
"""Tiny greedy wrapper — textwrap would do, but this keeps the record's
|
| 1058 |
+
formatting identical across Python versions."""
|
| 1059 |
+
words, line, out = str(text).split(), "", []
|
| 1060 |
+
for w in words:
|
| 1061 |
+
if line and len(line) + 1 + len(w) > width:
|
| 1062 |
+
out.append(line)
|
| 1063 |
+
line = w
|
| 1064 |
+
else:
|
| 1065 |
+
line = f"{line} {w}".strip()
|
| 1066 |
+
if line:
|
| 1067 |
+
out.append(line)
|
| 1068 |
+
return out
|
dee/core/compiler_validation.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Check the compiler against landmark editing designs.
|
| 2 |
+
|
| 3 |
+
The question anyone serious asks within two minutes is "how do you know it's
|
| 4 |
+
right?", and "it is deterministic and well tested" is an engineering answer to
|
| 5 |
+
a scientific question. This is the scientific answer: run the compiler against
|
| 6 |
+
designs the field has already settled and show it arrives at the same verdict.
|
| 7 |
+
|
| 8 |
+
THE CLAIM THIS SUPPORTS, STATED PRECISELY
|
| 9 |
+
-----------------------------------------
|
| 10 |
+
It reproduces **known-correct decisions**. It does NOT discover new biology,
|
| 11 |
+
and the two must never be blurred — the first is defensible and the second
|
| 12 |
+
would be a lie a reviewer could take apart in one question.
|
| 13 |
+
|
| 14 |
+
TWO KINDS OF CASE, COUNTED SEPARATELY
|
| 15 |
+
-------------------------------------
|
| 16 |
+
`basis="chemistry"` the expected verdict follows from the genetic code and
|
| 17 |
+
the two base-editor chemistries. Anyone can re-derive
|
| 18 |
+
it, so it needs no citation and counts as validation.
|
| 19 |
+
|
| 20 |
+
`basis="literature"` the expectation is a claim about what a real programme
|
| 21 |
+
actually did. It requires a citation. Without one the
|
| 22 |
+
case is reported UNVERIFIED and does **not** count as a
|
| 23 |
+
pass — it is a named gap, which is more useful than a
|
| 24 |
+
silent absence.
|
| 25 |
+
|
| 26 |
+
That split is the honesty mechanism. "12 of 12 validated" means nothing if
|
| 27 |
+
half were asserted from memory; here the report always carries both numbers
|
| 28 |
+
and the unverified ones by name.
|
| 29 |
+
|
| 30 |
+
Pure logic: no network, no GPU, no model. It runs in CI on every commit, so a
|
| 31 |
+
change that breaks a landmark verdict fails the build rather than being
|
| 32 |
+
noticed in a demo.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
from __future__ import annotations
|
| 36 |
+
|
| 37 |
+
import json
|
| 38 |
+
import pathlib
|
| 39 |
+
from dataclasses import dataclass, field
|
| 40 |
+
from typing import Any, Dict, List, Optional
|
| 41 |
+
|
| 42 |
+
from dee.core import compiler as _compiler
|
| 43 |
+
|
| 44 |
+
__all__ = ["CaseResult", "ValidationReport", "load_cases", "run_validation",
|
| 45 |
+
"report_to_dict"]
|
| 46 |
+
|
| 47 |
+
CORPUS_PATH = (pathlib.Path(__file__).resolve().parent.parent
|
| 48 |
+
/ "data" / "landmark_designs.json")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@dataclass
|
| 52 |
+
class CaseResult:
|
| 53 |
+
case_id: str
|
| 54 |
+
name: str
|
| 55 |
+
basis: str
|
| 56 |
+
status: str # "pass" | "fail" | "unverified"
|
| 57 |
+
expected: Dict[str, Any] = field(default_factory=dict)
|
| 58 |
+
observed: Dict[str, Any] = field(default_factory=dict)
|
| 59 |
+
mismatches: List[str] = field(default_factory=list)
|
| 60 |
+
why: str = ""
|
| 61 |
+
citation: str = ""
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclass
|
| 65 |
+
class ValidationReport:
|
| 66 |
+
results: List[CaseResult]
|
| 67 |
+
|
| 68 |
+
@property
|
| 69 |
+
def chemistry_total(self) -> int:
|
| 70 |
+
return sum(1 for r in self.results if r.basis == "chemistry")
|
| 71 |
+
|
| 72 |
+
@property
|
| 73 |
+
def chemistry_passed(self) -> int:
|
| 74 |
+
return sum(1 for r in self.results
|
| 75 |
+
if r.basis == "chemistry" and r.status == "pass")
|
| 76 |
+
|
| 77 |
+
@property
|
| 78 |
+
def unverified(self) -> List[str]:
|
| 79 |
+
return [r.name for r in self.results if r.status == "unverified"]
|
| 80 |
+
|
| 81 |
+
@property
|
| 82 |
+
def failures(self) -> List[CaseResult]:
|
| 83 |
+
return [r for r in self.results if r.status == "fail"]
|
| 84 |
+
|
| 85 |
+
@property
|
| 86 |
+
def headline(self) -> str:
|
| 87 |
+
"""One sentence that cannot be misread as more than it is."""
|
| 88 |
+
n, tot = self.chemistry_passed, self.chemistry_total
|
| 89 |
+
base = (f"{n} of {tot} landmark decisions reproduced "
|
| 90 |
+
"(derivable from the genetic code and base-editor chemistry)")
|
| 91 |
+
if self.unverified:
|
| 92 |
+
base += (f"; {len(self.unverified)} further case(s) awaiting a "
|
| 93 |
+
"citation and not counted")
|
| 94 |
+
return base + "."
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def load_cases(path: Optional[pathlib.Path] = None) -> List[Dict[str, Any]]:
|
| 98 |
+
p = path or CORPUS_PATH
|
| 99 |
+
if not p.exists():
|
| 100 |
+
return []
|
| 101 |
+
data = json.loads(p.read_text())
|
| 102 |
+
return [c for c in (data.get("cases") or []) if isinstance(c, dict)]
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _expand_allele(value: Any) -> str:
|
| 106 |
+
"""Alleles may be a literal string or {"repeat": "A", "times": 400}.
|
| 107 |
+
|
| 108 |
+
The compact form exists because a 400-base deletion is a legitimate case
|
| 109 |
+
and pasting 400 characters into JSON makes the corpus unreadable. It is
|
| 110 |
+
NOT a shorthand the compiler understands — an earlier corpus wrote "A400"
|
| 111 |
+
expecting it to mean 400 A's, and the compiler correctly read it as an
|
| 112 |
+
allele containing a digit and refused. The harness caught that, which is
|
| 113 |
+
the harness working.
|
| 114 |
+
"""
|
| 115 |
+
if isinstance(value, dict):
|
| 116 |
+
base = str(value.get("repeat") or "")
|
| 117 |
+
times = int(value.get("times") or 0)
|
| 118 |
+
return base * times
|
| 119 |
+
return str(value or "")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _observe(case: Dict[str, Any]) -> Dict[str, Any]:
|
| 123 |
+
"""Run the compiler on a case and flatten what it decided."""
|
| 124 |
+
call = _compiler.classify_lesion(_expand_allele(case.get("wt_base")),
|
| 125 |
+
_expand_allele(case.get("patient_base")))
|
| 126 |
+
corr = call.correction
|
| 127 |
+
return {
|
| 128 |
+
"compiles": call.compiles,
|
| 129 |
+
"route": call.route,
|
| 130 |
+
"editor_family": corr.editor_family if corr else None,
|
| 131 |
+
"strand": corr.strand if corr else None,
|
| 132 |
+
"diagnostics": [d.code for d in call.diagnostics],
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _compare(expected: Dict[str, Any], observed: Dict[str, Any]) -> List[str]:
|
| 137 |
+
"""Only the keys the case actually asserts. A case that says nothing about
|
| 138 |
+
strand is not failed for the strand."""
|
| 139 |
+
out: List[str] = []
|
| 140 |
+
for key in ("compiles", "route", "editor_family", "strand"):
|
| 141 |
+
if key in expected and observed.get(key) != expected[key]:
|
| 142 |
+
out.append(f"{key}: expected {expected[key]!r}, "
|
| 143 |
+
f"got {observed.get(key)!r}")
|
| 144 |
+
want_diag = expected.get("diagnostic")
|
| 145 |
+
if want_diag and want_diag not in (observed.get("diagnostics") or []):
|
| 146 |
+
out.append(f"diagnostic {want_diag!r} not raised "
|
| 147 |
+
f"(raised {observed.get('diagnostics')!r})")
|
| 148 |
+
return out
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def run_validation(cases: Optional[List[Dict[str, Any]]] = None
|
| 152 |
+
) -> ValidationReport:
|
| 153 |
+
"""Every case, with literature cases short-circuited unless cited."""
|
| 154 |
+
results: List[CaseResult] = []
|
| 155 |
+
for case in (cases if cases is not None else load_cases()):
|
| 156 |
+
basis = str(case.get("basis") or "literature")
|
| 157 |
+
why = case.get("why")
|
| 158 |
+
why_text = " ".join(why) if isinstance(why, list) else str(why or "")
|
| 159 |
+
citation = str(case.get("citation") or "").strip()
|
| 160 |
+
expected = case.get("expect") or {}
|
| 161 |
+
|
| 162 |
+
# A literature claim without a citation is not evidence, and must not
|
| 163 |
+
# be run as though it were — an uncited expectation is just a guess
|
| 164 |
+
# with a filename.
|
| 165 |
+
if basis == "literature" and not citation:
|
| 166 |
+
results.append(CaseResult(
|
| 167 |
+
case_id=str(case.get("id") or ""), name=str(case.get("name") or ""),
|
| 168 |
+
basis=basis, status="unverified", expected=expected,
|
| 169 |
+
why=why_text, citation=citation))
|
| 170 |
+
continue
|
| 171 |
+
|
| 172 |
+
if not expected:
|
| 173 |
+
results.append(CaseResult(
|
| 174 |
+
case_id=str(case.get("id") or ""), name=str(case.get("name") or ""),
|
| 175 |
+
basis=basis, status="unverified", expected={},
|
| 176 |
+
why=why_text or "No expectation recorded.", citation=citation))
|
| 177 |
+
continue
|
| 178 |
+
|
| 179 |
+
observed = _observe(case)
|
| 180 |
+
mismatches = _compare(expected, observed)
|
| 181 |
+
results.append(CaseResult(
|
| 182 |
+
case_id=str(case.get("id") or ""), name=str(case.get("name") or ""),
|
| 183 |
+
basis=basis, status="pass" if not mismatches else "fail",
|
| 184 |
+
expected=expected, observed=observed, mismatches=mismatches,
|
| 185 |
+
why=why_text, citation=citation))
|
| 186 |
+
return ValidationReport(results)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def report_to_dict(report: ValidationReport) -> Dict[str, Any]:
|
| 190 |
+
return {
|
| 191 |
+
"headline": report.headline,
|
| 192 |
+
"chemistry_passed": report.chemistry_passed,
|
| 193 |
+
"chemistry_total": report.chemistry_total,
|
| 194 |
+
"unverified": report.unverified,
|
| 195 |
+
"all_passed": not report.failures,
|
| 196 |
+
"cases": [
|
| 197 |
+
{"id": r.case_id, "name": r.name, "basis": r.basis,
|
| 198 |
+
"status": r.status, "expected": r.expected, "observed": r.observed,
|
| 199 |
+
"mismatches": r.mismatches, "why": r.why, "citation": r.citation}
|
| 200 |
+
for r in report.results
|
| 201 |
+
],
|
| 202 |
+
}
|
dee/data/landmark_designs.json
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_readme": [
|
| 3 |
+
"Landmark editing designs the compiler is checked against.",
|
| 4 |
+
"",
|
| 5 |
+
"TWO KINDS OF ENTRY, and the difference is the whole point:",
|
| 6 |
+
"",
|
| 7 |
+
" basis = 'chemistry' the expected verdict follows from the genetic code",
|
| 8 |
+
" and the two base-editor chemistries. It can be",
|
| 9 |
+
" asserted without citing anyone, because anyone can",
|
| 10 |
+
" re-derive it. These COUNT as validation.",
|
| 11 |
+
"",
|
| 12 |
+
" basis = 'literature' the expected verdict is a claim about what a real",
|
| 13 |
+
" published or approved programme actually did. It",
|
| 14 |
+
" needs a citation. Until `citation` is filled in,",
|
| 15 |
+
" the runner reports the case as UNVERIFIED and it",
|
| 16 |
+
" does NOT count as a pass.",
|
| 17 |
+
"",
|
| 18 |
+
"An unverified entry is a to-do with a name, not a failure. The report",
|
| 19 |
+
"shows both numbers separately so 'validated' can never quietly include",
|
| 20 |
+
"cases nobody checked.",
|
| 21 |
+
"",
|
| 22 |
+
"Adding a literature case: fill `citation` with something a reader can",
|
| 23 |
+
"look up (DOI, PMID, or the trial/product name), and set the expectation",
|
| 24 |
+
"from the paper — never from memory."
|
| 25 |
+
],
|
| 26 |
+
|
| 27 |
+
"cases": [
|
| 28 |
+
{
|
| 29 |
+
"id": "hbs-sickle-hbb",
|
| 30 |
+
"name": "HbS (sickle cell) — HBB Glu6Val",
|
| 31 |
+
"variant": "NM_000518.5:c.20A>T",
|
| 32 |
+
"locus": "GRCh38 chr11:5227002",
|
| 33 |
+
"wt_base": "T",
|
| 34 |
+
"patient_base": "A",
|
| 35 |
+
"basis": "chemistry",
|
| 36 |
+
"expect": {
|
| 37 |
+
"compiles": false,
|
| 38 |
+
"route": "prime_editing",
|
| 39 |
+
"diagnostic": "transversion_no_base_editor"
|
| 40 |
+
},
|
| 41 |
+
"why": [
|
| 42 |
+
"The HGVS is written on the transcript (minus) strand as A>T. On the",
|
| 43 |
+
"forward strand the reference base is T and the patient carries A.",
|
| 44 |
+
"T>A is a TRANSVERSION, and neither ABE (A>G) nor CBE (C>T) produces a",
|
| 45 |
+
"transversion on either strand — so no base editor can correct HbS.",
|
| 46 |
+
"The compiler must refuse and route to prime editing.",
|
| 47 |
+
"",
|
| 48 |
+
"This is also why the approved sickle-cell editing therapy targets the",
|
| 49 |
+
"BCL11A erythroid enhancer to re-induce fetal haemoglobin rather than",
|
| 50 |
+
"correcting the HBB mutation directly. That clinical fact is context,",
|
| 51 |
+
"not the assertion: the assertion here is purely the chemistry, which",
|
| 52 |
+
"anyone can re-derive from the reference sequence."
|
| 53 |
+
],
|
| 54 |
+
"verified_note": "Coordinate and forward-strand alleles confirmed against Ensembl GRCh38 and the reference sequence at that position on 2026-08-12.",
|
| 55 |
+
"citation": ""
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"id": "transition-abe-sense",
|
| 59 |
+
"name": "Generic G>A transition, sense strand",
|
| 60 |
+
"wt_base": "G",
|
| 61 |
+
"patient_base": "A",
|
| 62 |
+
"basis": "chemistry",
|
| 63 |
+
"expect": {
|
| 64 |
+
"compiles": true,
|
| 65 |
+
"route": "base_editing",
|
| 66 |
+
"editor_family": "ABE",
|
| 67 |
+
"strand": "sense"
|
| 68 |
+
},
|
| 69 |
+
"why": [
|
| 70 |
+
"The patient carries A where wild-type is G. Restoring G means writing",
|
| 71 |
+
"A>G, which is exactly what an adenine base editor does, on the sense",
|
| 72 |
+
"strand. No other chemistry applies."
|
| 73 |
+
],
|
| 74 |
+
"citation": ""
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"id": "transition-cbe-antisense",
|
| 78 |
+
"name": "Generic A>G transition, antisense strand",
|
| 79 |
+
"wt_base": "A",
|
| 80 |
+
"patient_base": "G",
|
| 81 |
+
"basis": "chemistry",
|
| 82 |
+
"expect": {
|
| 83 |
+
"compiles": true,
|
| 84 |
+
"route": "base_editing",
|
| 85 |
+
"editor_family": "CBE",
|
| 86 |
+
"strand": "antisense"
|
| 87 |
+
},
|
| 88 |
+
"why": [
|
| 89 |
+
"Restoring A from G is G>A on the sense strand. On the antisense strand",
|
| 90 |
+
"that position reads C and must become T — a cytosine base editor,",
|
| 91 |
+
"engaging the opposite strand. Getting this backwards designs a guide",
|
| 92 |
+
"against the wrong strand."
|
| 93 |
+
],
|
| 94 |
+
"citation": ""
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"id": "large-deletion-not-editable",
|
| 98 |
+
"name": "Multi-kilobase deletion",
|
| 99 |
+
"wt_base": { "repeat": "A", "times": 400 },
|
| 100 |
+
"patient_base": "",
|
| 101 |
+
"basis": "chemistry",
|
| 102 |
+
"expect": {
|
| 103 |
+
"compiles": false,
|
| 104 |
+
"route": "none",
|
| 105 |
+
"diagnostic": "lesion_too_large"
|
| 106 |
+
},
|
| 107 |
+
"why": [
|
| 108 |
+
"Base editors rewrite one base chemically and prime editing has a",
|
| 109 |
+
"practical size ceiling. A lesion of this size needs a different",
|
| 110 |
+
"modality entirely — integrase or recombinase-mediated insertion, or",
|
| 111 |
+
"gene addition — and the compiler must say so rather than returning a",
|
| 112 |
+
"guide."
|
| 113 |
+
],
|
| 114 |
+
"citation": ""
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"id": "small-insertion-prime-editing",
|
| 118 |
+
"name": "Three-base insertion",
|
| 119 |
+
"wt_base": "",
|
| 120 |
+
"patient_base": "ATG",
|
| 121 |
+
"basis": "chemistry",
|
| 122 |
+
"expect": {
|
| 123 |
+
"compiles": false,
|
| 124 |
+
"route": "prime_editing",
|
| 125 |
+
"diagnostic": "indel_not_base_editable"
|
| 126 |
+
},
|
| 127 |
+
"why": [
|
| 128 |
+
"Base editors do not add or remove bases. An insertion routes to prime",
|
| 129 |
+
"editing, which this compiler does not yet design — so it must refuse",
|
| 130 |
+
"rather than offer a base-editing guide that cannot install the change."
|
| 131 |
+
],
|
| 132 |
+
"citation": ""
|
| 133 |
+
},
|
| 134 |
+
|
| 135 |
+
{
|
| 136 |
+
"id": "kj-cps1-personalised-base-edit",
|
| 137 |
+
"name": "First personalised in-vivo base-editing therapy (CPS1 deficiency)",
|
| 138 |
+
"basis": "literature",
|
| 139 |
+
"expect": {},
|
| 140 |
+
"why": [
|
| 141 |
+
"PENDING. The bespoke n-of-1 base-editing therapy dosed in an infant",
|
| 142 |
+
"with CPS1 deficiency is the single most important case to reproduce,",
|
| 143 |
+
"because it is the template for the whole personalised-editing model.",
|
| 144 |
+
"",
|
| 145 |
+
"Deliberately left unfilled: encoding the variant, the editor and the",
|
| 146 |
+
"guide from memory would be exactly the fabrication this corpus exists",
|
| 147 |
+
"to prevent. Fill `wt_base`/`patient_base`/`expect` from the primary",
|
| 148 |
+
"publication and add the DOI or PMID to `citation`."
|
| 149 |
+
],
|
| 150 |
+
"citation": ""
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"id": "bcl11a-enhancer-hbf-reinduction",
|
| 154 |
+
"name": "BCL11A erythroid enhancer disruption (fetal haemoglobin re-induction)",
|
| 155 |
+
"basis": "literature",
|
| 156 |
+
"expect": {},
|
| 157 |
+
"why": [
|
| 158 |
+
"PENDING. The approved sickle-cell / beta-thalassaemia editing therapy",
|
| 159 |
+
"does not correct a coding variant — it disrupts an enhancer. Worth",
|
| 160 |
+
"encoding because it exercises a different question entirely (does the",
|
| 161 |
+
"compiler reason about a REGULATORY target?) and because it is the",
|
| 162 |
+
"clinical counterpart to the HbS refusal above.",
|
| 163 |
+
"",
|
| 164 |
+
"Needs the enhancer coordinates and the guide from the primary source."
|
| 165 |
+
],
|
| 166 |
+
"citation": ""
|
| 167 |
+
}
|
| 168 |
+
]
|
| 169 |
+
}
|
dee/server.py
CHANGED
|
@@ -3598,8 +3598,23 @@ def create_app() -> Flask:
|
|
| 3598 |
|
| 3599 |
out = _compiler.report_to_dict(report)
|
| 3600 |
out["resolved"] = resolved
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3601 |
return jsonify(out)
|
| 3602 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3603 |
@app.get("/api/benchmarks")
|
| 3604 |
def benchmarks() -> Response:
|
| 3605 |
"""The receipts — how well the engine's zero-shot ranking predicts
|
|
@@ -4318,6 +4333,7 @@ from dee.core import scoring as _scoring
|
|
| 4318 |
# DNA-level tiers (Evo 2, via Modal) — same "/api/models reports what's
|
| 4319 |
# actually runnable" contract, separate id-space (see the models() route).
|
| 4320 |
from dee.core import compiler as _compiler
|
|
|
|
| 4321 |
from dee.core import variant_resolve as _variant_resolve
|
| 4322 |
from dee.core import dna_scoring as _dna_scoring
|
| 4323 |
|
|
|
|
| 3598 |
|
| 3599 |
out = _compiler.report_to_dict(report)
|
| 3600 |
out["resolved"] = resolved
|
| 3601 |
+
# The deliverable: a design and its complete justification, in a form
|
| 3602 |
+
# that diffs and pastes. Deterministic — same inputs, same bytes.
|
| 3603 |
+
out["record"] = _compiler.design_record(
|
| 3604 |
+
report, variant=notation, resolved=resolved,
|
| 3605 |
+
provenance={"engine": "TuringDNA compiler",
|
| 3606 |
+
"record": _compiler.RECORD_VERSION})
|
| 3607 |
return jsonify(out)
|
| 3608 |
|
| 3609 |
+
@app.get("/api/compiler/validation")
|
| 3610 |
+
def compiler_validation() -> Response:
|
| 3611 |
+
"""Landmark designs the compiler is checked against. Public: the whole
|
| 3612 |
+
value is that anyone can look at it, including the gaps."""
|
| 3613 |
+
report = _compiler_validation.run_validation()
|
| 3614 |
+
resp = jsonify(_compiler_validation.report_to_dict(report))
|
| 3615 |
+
resp.headers["Cache-Control"] = "no-store, max-age=0"
|
| 3616 |
+
return resp
|
| 3617 |
+
|
| 3618 |
@app.get("/api/benchmarks")
|
| 3619 |
def benchmarks() -> Response:
|
| 3620 |
"""The receipts — how well the engine's zero-shot ranking predicts
|
|
|
|
| 4333 |
# DNA-level tiers (Evo 2, via Modal) — same "/api/models reports what's
|
| 4334 |
# actually runnable" contract, separate id-space (see the models() route).
|
| 4335 |
from dee.core import compiler as _compiler
|
| 4336 |
+
from dee.core import compiler_validation as _compiler_validation
|
| 4337 |
from dee.core import variant_resolve as _variant_resolve
|
| 4338 |
from dee.core import dna_scoring as _dna_scoring
|
| 4339 |
|
tests/test_compiler.py
CHANGED
|
@@ -512,3 +512,68 @@ def test_scores_attach_by_label():
|
|
| 512 |
assert hit and all(b.delta_ll == -3.75 for b in hit)
|
| 513 |
return
|
| 514 |
pytest.skip("fixture produced no bystanders")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
assert hit and all(b.delta_ll == -3.75 for b in hit)
|
| 513 |
return
|
| 514 |
pytest.skip("fixture produced no bystanders")
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
# ═══════════════════════════════════════════════════════════════════════
|
| 518 |
+
# The design record — the actual deliverable
|
| 519 |
+
# ═══════════════════════════════════════════════════════════════════════
|
| 520 |
+
def test_the_record_is_deterministic():
|
| 521 |
+
"""A record that changes between runs cannot be diffed, and one that
|
| 522 |
+
cannot be diffed cannot be audited."""
|
| 523 |
+
r = C.compile_report("G", "A", window=WINDOW, offset=0)
|
| 524 |
+
a = C.design_record(r, variant="NM_1:c.1G>A")
|
| 525 |
+
b = C.design_record(r, variant="NM_1:c.1G>A")
|
| 526 |
+
assert a == b and len(a) > 400
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
def test_the_record_carries_the_scope_limits():
|
| 530 |
+
r = C.compile_report("G", "A", window=WINDOW, offset=0)
|
| 531 |
+
rec = C.design_record(r)
|
| 532 |
+
assert "Somatic" in rec and "Not IND-ready" in rec
|
| 533 |
+
assert "not a clinical decision" in rec
|
| 534 |
+
assert "Predicted specificity is not measured specificity" in rec
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
def test_the_record_names_what_it_did_not_establish():
|
| 538 |
+
"""The section that makes it a record rather than a certificate."""
|
| 539 |
+
r = C.compile_report("G", "A", window=WINDOW, offset=0)
|
| 540 |
+
rec = C.design_record(r)
|
| 541 |
+
assert "NOT ESTABLISHED by this record" in rec
|
| 542 |
+
assert "Assess specificity" in rec
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
def test_a_refusal_produces_a_record_too():
|
| 546 |
+
"""A refused design still deserves documentation — often more so."""
|
| 547 |
+
r = C.compile_report("A", "C") # transversion
|
| 548 |
+
rec = C.design_record(r, variant="NM_1:c.1C>A")
|
| 549 |
+
assert "compiled NO" in rec
|
| 550 |
+
assert "transversion_no_base_editor" in rec
|
| 551 |
+
assert "remedy:" in rec
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
def test_bystander_scores_appear_in_the_record_and_unscored_says_so():
|
| 555 |
+
corr = C.classify_lesion("G", "A").correction
|
| 556 |
+
for off in range(len(HBB)):
|
| 557 |
+
if HBB[off] != "G":
|
| 558 |
+
continue
|
| 559 |
+
st, dg = C.plan_base_edit_strategies(HBB, off, corr)
|
| 560 |
+
if not any(s.bystanders for s in st):
|
| 561 |
+
continue
|
| 562 |
+
C.label_bystanders(HBB, st)
|
| 563 |
+
r = C.compile_report("G", "A", window=HBB, offset=off,
|
| 564 |
+
strategies=st, enumerate_diags=dg)
|
| 565 |
+
rec = C.design_record(r)
|
| 566 |
+
assert "STRATEGIES" in rec and "bystanders" in rec
|
| 567 |
+
assert "not scored" in rec, "unscored must be stated, not omitted"
|
| 568 |
+
return
|
| 569 |
+
pytest.skip("fixture produced no bystanders")
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def test_the_specificity_pass_hands_off_instead_of_shrugging():
|
| 573 |
+
r = C.compile_report("G", "A", window=WINDOW, offset=0)
|
| 574 |
+
sp = _by_name(r)["specificity"]
|
| 575 |
+
assert "Cas-OFFinder" in sp.detail or "CRISPRme" in sp.detail
|
| 576 |
+
assert "GUIDE-seq" in sp.detail
|
| 577 |
+
assert "Cas-independent deamination" in sp.detail, (
|
| 578 |
+
"a base editor is not cleared by a DSB-capture assay, and the handoff "
|
| 579 |
+
"must say so")
|
tests/test_compiler_validation.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Landmark-design validation. This file guards the honesty mechanism.
|
| 2 |
+
|
| 3 |
+
The number that matters is not "how many passed" but "how many passed that
|
| 4 |
+
were checkable without trusting anyone's memory". A harness that lets an
|
| 5 |
+
uncited claim count as validation is worse than no harness, because it
|
| 6 |
+
launders a guess into a credential.
|
| 7 |
+
"""
|
| 8 |
+
import pytest
|
| 9 |
+
|
| 10 |
+
from dee.core import compiler_validation as V
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_the_bundled_corpus_loads():
|
| 14 |
+
cases = V.load_cases()
|
| 15 |
+
assert cases, "the corpus must ship with the package"
|
| 16 |
+
assert all("id" in c and "basis" in c for c in cases)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def test_every_chemistry_case_reproduces():
|
| 20 |
+
"""If this fails, the compiler changed its verdict on a settled design."""
|
| 21 |
+
report = V.run_validation()
|
| 22 |
+
assert not report.failures, [
|
| 23 |
+
(f.name, f.mismatches) for f in report.failures]
|
| 24 |
+
assert report.chemistry_passed == report.chemistry_total
|
| 25 |
+
assert report.chemistry_total >= 5, "corpus should not silently shrink"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_the_sickle_cell_case_is_a_refusal_and_stays_one():
|
| 29 |
+
"""The anchor case. HbS is T>A on the forward strand — a transversion —
|
| 30 |
+
so no base editor can correct it. If this ever 'passes' as compilable,
|
| 31 |
+
something has broken very badly."""
|
| 32 |
+
report = V.run_validation()
|
| 33 |
+
hbs = next(r for r in report.results if r.case_id == "hbs-sickle-hbb")
|
| 34 |
+
assert hbs.status == "pass"
|
| 35 |
+
assert hbs.observed["compiles"] is False
|
| 36 |
+
assert hbs.observed["route"] == "prime_editing"
|
| 37 |
+
assert "transversion_no_base_editor" in hbs.observed["diagnostics"]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_an_uncited_literature_case_is_unverified_not_passed():
|
| 41 |
+
"""The mechanism. An expectation nobody can look up must never count."""
|
| 42 |
+
cases = [{
|
| 43 |
+
"id": "made-up", "name": "Uncited claim", "basis": "literature",
|
| 44 |
+
"wt_base": "G", "patient_base": "A",
|
| 45 |
+
"expect": {"compiles": True, "route": "base_editing"},
|
| 46 |
+
"citation": "",
|
| 47 |
+
}]
|
| 48 |
+
report = V.run_validation(cases)
|
| 49 |
+
assert report.results[0].status == "unverified"
|
| 50 |
+
assert report.chemistry_passed == 0
|
| 51 |
+
assert "Uncited claim" in report.unverified
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_a_cited_literature_case_is_actually_run():
|
| 55 |
+
cases = [{
|
| 56 |
+
"id": "cited", "name": "Cited claim", "basis": "literature",
|
| 57 |
+
"wt_base": "G", "patient_base": "A",
|
| 58 |
+
"expect": {"compiles": True, "route": "base_editing",
|
| 59 |
+
"editor_family": "ABE"},
|
| 60 |
+
"citation": "PMID:00000000",
|
| 61 |
+
}]
|
| 62 |
+
assert V.run_validation(cases).results[0].status == "pass"
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_a_wrong_expectation_fails_loudly_with_the_delta():
|
| 66 |
+
cases = [{
|
| 67 |
+
"id": "wrong", "name": "Wrong", "basis": "chemistry",
|
| 68 |
+
"wt_base": "G", "patient_base": "A",
|
| 69 |
+
"expect": {"editor_family": "CBE"}, # it is ABE
|
| 70 |
+
}]
|
| 71 |
+
r = V.run_validation(cases).results[0]
|
| 72 |
+
assert r.status == "fail"
|
| 73 |
+
assert any("editor_family" in m and "ABE" in m for m in r.mismatches)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_a_case_is_not_failed_for_something_it_never_asserted():
|
| 77 |
+
"""Partial expectations are legitimate; only stated keys are checked."""
|
| 78 |
+
cases = [{
|
| 79 |
+
"id": "partial", "name": "Partial", "basis": "chemistry",
|
| 80 |
+
"wt_base": "G", "patient_base": "A",
|
| 81 |
+
"expect": {"compiles": True}, # says nothing about strand
|
| 82 |
+
}]
|
| 83 |
+
assert V.run_validation(cases).results[0].status == "pass"
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def test_the_headline_never_reads_as_more_than_it_is():
|
| 87 |
+
report = V.run_validation()
|
| 88 |
+
h = report.headline
|
| 89 |
+
assert "derivable from the genetic code" in h
|
| 90 |
+
if report.unverified:
|
| 91 |
+
assert "awaiting a citation and not counted" in h
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def test_unverified_cases_are_named_not_merely_counted():
|
| 95 |
+
"""A gap with a name is a to-do; a gap as a number is a footnote."""
|
| 96 |
+
report = V.run_validation()
|
| 97 |
+
assert all(isinstance(n, str) and n for n in report.unverified)
|