Spaces:
Running
Running
| """The therapeutic compiler — lower a pathogenic variant to an editing strategy. | |
| A compiler is not a pipeline with a nicer name. What makes this one is that it | |
| **refuses to compile** and says why. A tool that always returns a strategy for | |
| a patient's variant is a plausible-answer generator, and in this domain a | |
| plausible answer is worse than none: it is the kind of output that gets built | |
| on. "This is a 4 kb deletion; no base or prime editor addresses it" is the | |
| useful answer, and it is the one nobody ships. | |
| So the diagnostics below are the product. The strategies are what falls out | |
| when there are no errors. | |
| Same discipline as dee/core/edits.py ("REFUSE ON MISMATCH... a silent | |
| off-by-one here is not a bug report, it is a scientist ordering the wrong | |
| DNA"), applied one level up: here a silent wrong answer is a scientist | |
| designing the wrong therapy. | |
| SCOPE, enforced in code and not only in copy | |
| -------------------------------------------- | |
| * **Somatic only.** Germline and embryo applications are refused outright | |
| (:func:`compile_correction` raises on ``germline=True``). This is the line | |
| the field draws and this module does not sit on it. | |
| * **Design and assessment, not a clinical decision.** The output is a design | |
| record for humans to evaluate. It is not IND-ready, it does not clear a | |
| strategy for use, and nothing here should reach a patient without the | |
| ordinary preclinical program. | |
| * **Predicted specificity is not measured specificity.** Off-target search | |
| narrows where to look. It does not replace GUIDE-seq / CIRCLE-seq or any | |
| other empirical assay. | |
| * **Out of scope entirely:** immunogenicity, pharmacokinetics, dosing, | |
| manufacturing, and delivery efficacy. The compiler is silent on all of | |
| them rather than guessing. | |
| This module is PURE LOGIC — no network, no GPU, no model. Everything here is | |
| a deterministic consequence of the two alleles and the genetic code, which is | |
| why it can be tested exhaustively. Model-derived judgements (what a bystander | |
| edit *does*) live outside it and are attached later, clearly labelled as | |
| predictions. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from typing import Dict, List, Optional, Tuple | |
| __all__ = [ | |
| "Diagnostic", "Correction", "LesionCall", | |
| "classify_lesion", "restores_wildtype", "compile_correction", | |
| "GermlineRefused", | |
| ] | |
| _COMPLEMENT = {"A": "T", "T": "A", "C": "G", "G": "C"} | |
| _BASES = frozenset("ACGT") | |
| # Base editors make exactly two chemistries. Everything downstream follows | |
| # from this and nothing else: | |
| # ABE adenine base editor A -> G | |
| # CBE cytosine base editor C -> T | |
| _ABE_CHANGE = ("A", "G") | |
| _CBE_CHANGE = ("C", "T") | |
| # Routing bounds for prime editing, NOT capability guarantees. Published PE | |
| # work spans a range that depends on the construct, the locus and the cell | |
| # type, so a hard biological limit would be a fiction. These are deliberately | |
| # conservative and are used only to decide "route to PE" vs "refuse" — a | |
| # lesion near the boundary gets a warning saying the call is marginal, not a | |
| # promise that it will work. | |
| PRIME_EDIT_INSERT_BOUND = 44 | |
| PRIME_EDIT_DELETE_BOUND = 80 | |
| class GermlineRefused(Exception): | |
| """Raised for any germline or embryo request. Not a routing decision.""" | |
| class Diagnostic: | |
| """One compiler message. `code` is stable; `message` is for humans. | |
| `remedy` is the field that makes a refusal useful instead of merely | |
| correct — it says what would have to change for this to compile. | |
| """ | |
| level: str # "error" | "warning" | "note" | |
| code: str | |
| message: str | |
| remedy: str = "" | |
| def blocking(self) -> bool: | |
| return self.level == "error" | |
| class Correction: | |
| """The change that restores wild-type, and which chemistry can make it. | |
| `strand` matters and is the part most easily got wrong. A base editor | |
| only ever writes A->G (ABE) or C->T (CBE) on the strand it engages. A | |
| sense-strand T->C is therefore an ABE edit — on the ANTISENSE strand, | |
| where that position reads A and must become G. Getting this backwards | |
| designs a guide against the wrong strand, which fails silently in | |
| silico and expensively at the bench. | |
| """ | |
| wt_base: str | |
| patient_base: str | |
| sense_change: str # what must happen on the sense strand, "T>C" | |
| strand: str # "sense" | "antisense" — where the editor works | |
| editor_change: str # what the editor actually writes, "A>G" | |
| editor_family: str # "ABE" | "CBE" | |
| class LesionCall: | |
| """What kind of lesion this is and what could address it.""" | |
| kind: str # substitution | insertion | deletion | delins | identity | |
| size: int # bases changed (max of ref/alt length for delins) | |
| is_transition: bool | |
| correction: Optional[Correction] | |
| route: str # "base_editing" | "prime_editing" | "none" | |
| diagnostics: List[Diagnostic] = field(default_factory=list) | |
| def compiles(self) -> bool: | |
| return not any(d.blocking for d in self.diagnostics) | |
| def errors(self) -> List[Diagnostic]: | |
| return [d for d in self.diagnostics if d.blocking] | |
| def _clean_allele(value: str) -> str: | |
| """Alleles arrive as '-', '', 'del', or real bases. Normalise to bases.""" | |
| v = "".join(str(value or "").split()).upper() | |
| if v in ("-", ".", "DEL", "NONE", "NULL"): | |
| return "" | |
| return v | |
| def _is_transition(a: str, b: str) -> bool: | |
| """A<->G or C<->T. Everything else is a transversion.""" | |
| return {a, b} in ({"A", "G"}, {"C", "T"}) | |
| def _base_editor_for(wt: str, patient: str) -> Optional[Correction]: | |
| """Which base editor, if any, restores `wt` from `patient`. | |
| The whole derivation, because it is short and worth being able to check: | |
| sense A->G ABE reads A, writes G -> ABE, sense | |
| sense C->T CBE reads C, writes T -> CBE, sense | |
| sense T->C antisense reads A, writes G -> ABE, antisense | |
| sense G->A antisense reads C, writes T -> CBE, antisense | |
| Every remaining substitution is a transversion (A<->C, A<->T, C<->G, | |
| G<->T), and neither chemistry produces one on either strand. There is no | |
| base-editing route to a transversion — that is a fact about the enzymes, | |
| not a gap in this function. | |
| """ | |
| change = (patient, wt) # from the patient's base, back to wild-type | |
| if change == _ABE_CHANGE: | |
| return Correction(wt, patient, f"{patient}>{wt}", "sense", "A>G", "ABE") | |
| if change == _CBE_CHANGE: | |
| return Correction(wt, patient, f"{patient}>{wt}", "sense", "C>T", "CBE") | |
| anti = (_COMPLEMENT[patient], _COMPLEMENT[wt]) | |
| if anti == _ABE_CHANGE: | |
| return Correction(wt, patient, f"{patient}>{wt}", "antisense", "A>G", "ABE") | |
| if anti == _CBE_CHANGE: | |
| return Correction(wt, patient, f"{patient}>{wt}", "antisense", "C>T", "CBE") | |
| return None | |
| def classify_lesion(wt_allele: str, patient_allele: str) -> LesionCall: | |
| """Route a lesion to an editing chemistry, or refuse with a reason. | |
| `wt_allele` is the reference/wild-type allele and `patient_allele` is what | |
| the patient carries. Correction runs patient -> wild-type; passing them | |
| the wrong way round designs an editor that installs the disease, so the | |
| argument names are deliberately not `ref`/`alt` (which flip meaning | |
| depending on whether you are reading a VCF or thinking about a therapy). | |
| """ | |
| wt = _clean_allele(wt_allele) | |
| pt = _clean_allele(patient_allele) | |
| diags: List[Diagnostic] = [] | |
| bad = [b for b in (wt + pt) if b not in _BASES] | |
| if bad: | |
| diags.append(Diagnostic( | |
| "error", "non_dna_allele", | |
| f"Alleles must be A/C/G/T; got {sorted(set(bad))!r}.", | |
| "Supply unambiguous bases. IUPAC ambiguity codes and amino-acid " | |
| "letters are not alleles and cannot be routed.")) | |
| return LesionCall("invalid", 0, False, None, "none", diags) | |
| if not wt and not pt: | |
| diags.append(Diagnostic( | |
| "error", "empty_alleles", "Both alleles are empty.", | |
| "Give the wild-type and patient alleles for the position.")) | |
| return LesionCall("invalid", 0, False, None, "none", diags) | |
| if wt == pt: | |
| diags.append(Diagnostic( | |
| "error", "no_lesion", | |
| "Wild-type and patient alleles are identical — there is nothing " | |
| "to correct.", | |
| "Check the alleles are not swapped, and that the variant call is " | |
| "against the intended reference.")) | |
| return LesionCall("identity", 0, False, None, "none", diags) | |
| # ── substitution ──────────────────────────────────────────────────── | |
| if len(wt) == 1 and len(pt) == 1: | |
| transition = _is_transition(wt, pt) | |
| corr = _base_editor_for(wt, pt) | |
| if corr is not None: | |
| return LesionCall("substitution", 1, transition, corr, | |
| "base_editing", diags) | |
| diags.append(Diagnostic( | |
| "warning", "transversion_no_base_editor", | |
| f"{pt}>{wt} is a transversion. No base editor makes this change " | |
| "on either strand — ABE writes A>G and CBE writes C>T, and " | |
| "neither produces a transversion.", | |
| "Prime editing is the route for transversions, and it is designed " | |
| "here — see the pegRNAs. Base editing is ruled out by chemistry, " | |
| "not by a threshold.")) | |
| return LesionCall("substitution", 1, transition, None, | |
| "prime_editing", diags) | |
| # ── indels and delins ─────────────────────────────────────────────── | |
| if not pt: | |
| kind, size = "deletion", len(wt) | |
| bound, what = PRIME_EDIT_DELETE_BOUND, "deletion" | |
| elif not wt: | |
| kind, size = "insertion", len(pt) | |
| bound, what = PRIME_EDIT_INSERT_BOUND, "insertion" | |
| else: | |
| kind, size = "delins", max(len(wt), len(pt)) | |
| bound, what = PRIME_EDIT_INSERT_BOUND, "replacement" | |
| diags.append(Diagnostic( | |
| "note", "indel_not_base_editable", | |
| f"A {size}-base {kind} cannot be corrected by base editing — base " | |
| "editors rewrite one base chemically and do not add or remove any.", | |
| "Prime editing is the route for indels of this size.")) | |
| if size > bound: | |
| diags.append(Diagnostic( | |
| "error", "lesion_too_large", | |
| f"A {size}-base {what} is beyond what this compiler will route " | |
| f"to prime editing (bound {bound}).", | |
| "Larger lesions need a different modality — integrase or " | |
| "recombinase-based insertion, or gene addition. Those are not " | |
| "editing strategies and are outside this compiler.")) | |
| return LesionCall(kind, size, False, None, "none", diags) | |
| if size > bound // 2: | |
| diags.append(Diagnostic( | |
| "warning", "lesion_near_bound", | |
| f"A {size}-base {what} is large for prime editing; efficiency " | |
| "falls off with edit size and varies by locus and cell type.", | |
| "Treat the route as marginal and plan an empirical check early.")) | |
| return LesionCall(kind, size, False, None, "prime_editing", diags) | |
| def restores_wildtype(window: str, offset: int, corr: Correction) -> bool: | |
| """The compiler's type-check: does applying `corr` actually give wild-type? | |
| `window` is reference (wild-type) sequence and `offset` is the 0-based | |
| index of the variant position within it. The patient's sequence is the | |
| window with the patient's base substituted in; applying the correction | |
| must return it to the reference exactly. | |
| This exists because every other check in this module reasons about | |
| ALLELES, and a position that has drifted by one still type-checks at the | |
| allele level while pointing at the wrong base. Comparing whole sequences | |
| catches that. | |
| """ | |
| if not window or not (0 <= offset < len(window)): | |
| return False | |
| if window[offset] != corr.wt_base: | |
| return False | |
| patient_seq = window[:offset] + corr.patient_base + window[offset + 1:] | |
| corrected = patient_seq[:offset] + corr.wt_base + patient_seq[offset + 1:] | |
| return corrected == window | |
| def compile_correction(wt_allele: str, patient_allele: str, *, | |
| window: str = "", offset: int = -1, | |
| germline: bool = False) -> LesionCall: | |
| """Front door. Classifies, then verifies against real sequence if given. | |
| `germline=True` is refused rather than routed — see the module docstring. | |
| It raises instead of returning a diagnostic because a refusal that a | |
| caller can read past and keep going is not a refusal. | |
| """ | |
| if germline: | |
| raise GermlineRefused( | |
| "This compiler designs somatic therapeutic edits only. Germline " | |
| "and embryo editing are out of scope and are not routed here.") | |
| call = classify_lesion(wt_allele, patient_allele) | |
| if not window: | |
| return call | |
| if call.correction is None: | |
| # No single-base correction — a transversion or an indel, which routes | |
| # to prime editing. There is still exactly one thing worth checking | |
| # against real sequence, and it is the one that matters most: does the | |
| # reference actually carry the wild-type allele where it is claimed to? | |
| # Skipping this for the PE route would mean every pegRNA in the module | |
| # was designed against an unverified coordinate. | |
| if call.route == "none": | |
| return call | |
| wt = _clean_allele(wt_allele) | |
| limit = len(window) if not wt else len(window) - len(wt) | |
| if offset < 0 or offset > limit: | |
| call.diagnostics.append(Diagnostic( | |
| "error", "offset_outside_window", | |
| f"The wild-type allele ({len(wt)} nt) does not fit in the " | |
| f"{len(window)}-base reference window at offset {offset}.", | |
| "Give the 0-based index at which the wild-type allele starts " | |
| "inside the window you supplied.")) | |
| return call | |
| observed = window[offset:offset + len(wt)] | |
| if wt and observed != wt: | |
| call.diagnostics.append(Diagnostic( | |
| "error", "reference_mismatch", | |
| f"The reference window has {observed!r} at offset {offset}, " | |
| f"but the wild-type allele was given as {wt!r}.", | |
| "Refusing rather than templating a correction toward bases the " | |
| "reference disagrees about. Check the coordinate, the " | |
| "transcript, and whether the variant was called on the " | |
| "opposite strand.")) | |
| return call | |
| if offset < 0 or offset >= len(window): | |
| call.diagnostics.append(Diagnostic( | |
| "error", "offset_outside_window", | |
| f"Variant offset {offset} is outside the {len(window)}-base " | |
| "reference window.", | |
| "Give the 0-based index of the variant within the window you " | |
| "supplied.")) | |
| return call | |
| observed = window[offset] | |
| if observed != call.correction.wt_base: | |
| # The single most valuable refusal in the module. Everything else is | |
| # arithmetic on alleles; this is the check that catches a coordinate | |
| # that is right by one, or a variant called on the other strand. | |
| call.diagnostics.append(Diagnostic( | |
| "error", "reference_mismatch", | |
| f"The reference window has {observed!r} at offset {offset}, but " | |
| f"the wild-type allele was given as {call.correction.wt_base!r}.", | |
| "Refusing rather than editing a position the reference disagrees " | |
| "about. Check the coordinate, the transcript, and whether the " | |
| "variant was called on the opposite strand.")) | |
| return call | |
| if not restores_wildtype(window, offset, call.correction): | |
| call.diagnostics.append(Diagnostic( | |
| "error", "correction_does_not_restore", | |
| "Applying the correction does not reproduce the reference " | |
| "sequence.", | |
| "This is a compiler bug or a malformed window; do not proceed.")) | |
| return call | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # The pass pipeline | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # A compiler shows its passes. This one shows the passes it CANNOT run and | |
| # why, which is the part that matters here: a therapeutic design tool that | |
| # quietly skips specificity analysis and prints a strategy is worse than one | |
| # that stops and says "the human off-target index does not cover intronic or | |
| # intergenic space, so I did not clear this guide". | |
| # | |
| # Every pass reports one of: | |
| # ok ran, produced a result, nothing blocking | |
| # warn ran, produced a result, with a caveat the designer must read | |
| # error ran, and refused | |
| # failed was ATTEMPTED and produced no result | |
| # unavailable did NOT run, because this deployment cannot — with the reason | |
| # skipped did not run because an earlier pass already refused | |
| # | |
| # `failed` is separate from `warn` on purpose. Reporting a model call that | |
| # errored as "passed with caveat" is a soft version of the same lie as | |
| # reporting an unrun pass as ok: in both cases nothing was assessed. | |
| # | |
| # "unavailable" is deliberately distinct from "ok". Conflating them is how a | |
| # tool ends up implying it checked something it never looked at. | |
| PASS_ORDER: Tuple[Tuple[str, str], ...] = ( | |
| ("resolve", "Resolve variant"), | |
| ("classify", "Classify lesion"), | |
| ("verify", "Verify against reference"), | |
| ("enumerate", "Enumerate strategies"), | |
| ("consequence", "Assess edit consequence"), | |
| ("specificity", "Assess specificity"), | |
| ("emit", "Emit design record"), | |
| ) | |
| class Pass: | |
| name: str | |
| title: str | |
| status: str # ok | warn | error | unavailable | skipped | |
| detail: str = "" | |
| diagnostics: List[Diagnostic] = field(default_factory=list) | |
| class CompileReport: | |
| lesion: Optional[LesionCall] | |
| passes: List[Pass] | |
| scope: Dict[str, str] | |
| strategies: List["Strategy"] = field(default_factory=list) | |
| # Prime-editing designs. Kept in their own list rather than coerced into | |
| # `Strategy`: a pegRNA is not a guide with extra fields, and flattening the | |
| # two would invite a UI that renders an RT template as if it were a spacer. | |
| pegrnas: List[object] = field(default_factory=list) | |
| def compiled(self) -> bool: | |
| """True only if every pass that RAN succeeded and none was skipped | |
| for an upstream refusal. An 'unavailable' pass does not fail the | |
| build, but it does mean the record is explicitly incomplete.""" | |
| return not any(p.status in ("error", "skipped") for p in self.passes) | |
| def incomplete_because(self) -> List[str]: | |
| """Passes that produced no assessment — whether they were never run | |
| or were attempted and failed. Both leave the same hole in the record.""" | |
| return [p.title for p in self.passes | |
| if p.status in ("unavailable", "failed")] | |
| # What this deployment can and cannot do, stated once so the passes and the | |
| # UI cannot drift apart. Each entry is the honest reason a pass will report | |
| # `unavailable` — not a TODO, a disclosure. | |
| CAPABILITY_NOTES = { | |
| "enumerate": ( | |
| "Base-editing guides and prime-editing pegRNAs are both designed here, " | |
| "but both are read off the locus — without a reference window there is " | |
| "nothing to design against. Lesions beyond the prime-editing size " | |
| "bound are not designed at all: those need integrase, recombinase or " | |
| "gene addition, which are different modalities and outside this " | |
| "compiler."), | |
| "consequence": ( | |
| "Bystander consequence scoring uses a zero-shot genome model. It " | |
| "ranks hypotheses about what an edit does; it has no validated " | |
| "relationship to clinical outcome and does not substitute for a " | |
| "functional assay."), | |
| "specificity": ( | |
| "The human and mouse off-target index covers CODING SEQUENCE ONLY. " | |
| "Intronic and intergenic off-targets sit outside it and are NOT " | |
| "cleared here. For therapeutic work this pass does not replace " | |
| "GUIDE-seq, CIRCLE-seq or an equivalent empirical assay."), | |
| } | |
| # What the specificity pass hands over when it cannot clear a guide itself. | |
| # A refusal that just stops is a shrug; a refusal that says exactly what to run | |
| # next is a handoff. These are the searches this deployment is NOT doing, named | |
| # precisely enough to be executed by someone who has the tools. | |
| SPECIFICITY_HANDOFF = { | |
| "in_silico": [ | |
| ("Cas-OFFinder / CRISPRme", "genome-wide, mismatch- and bulge-tolerant " | |
| "search over the WHOLE assembly, not only coding sequence. CRISPRme " | |
| "additionally accounts for common variants, which matters when the " | |
| "patient's own genome differs from the reference at an off-target."), | |
| ], | |
| "empirical": [ | |
| ("GUIDE-seq", "unbiased, cell-based detection of double-strand-break " | |
| "capture sites."), | |
| ("CIRCLE-seq / CHANGE-seq", "in vitro, high-sensitivity nomination of " | |
| "candidate off-targets from purified genomic DNA."), | |
| ("Targeted amplicon sequencing", "deep sequencing of the nominated " | |
| "sites in the actual therapeutic cell product."), | |
| ], | |
| "note": ( | |
| "For a base editor the relevant off-target question is not only where " | |
| "the nuclease cuts. Cas-independent deamination is not detected by a " | |
| "DSB-capture assay at all, so a clean GUIDE-seq result does not, on " | |
| "its own, clear a base editor."), | |
| } | |
| SCOPE = { | |
| "application": "Somatic therapeutic design only. Germline and embryo " | |
| "editing are out of scope and refused.", | |
| "status": "Design and assessment. Not IND-ready, not a clinical " | |
| "decision, not a clearance of any strategy for use.", | |
| "silent_on": "Immunogenicity, pharmacokinetics, dosing, manufacturing " | |
| "and delivery efficacy are not modelled and not reported.", | |
| } | |
| def compile_report(wt_allele: str, patient_allele: str, *, | |
| window: str = "", offset: int = -1, | |
| germline: bool = False, | |
| strategies: Optional[List["Strategy"]] = None, | |
| pegrnas: Optional[List[object]] = None, | |
| enumerate_diags: Optional[List[Diagnostic]] = None, | |
| consequence: Optional[Dict[str, object]] = None, | |
| can_check_specificity: bool = False) -> CompileReport: | |
| """Run the passes and report every one, including those that could not run. | |
| `consequence` is the ACTUAL RESULT of scoring the variant, or None. It is | |
| deliberately not a capability flag: the first version of this function | |
| took `can_score_consequence: bool` and reported the pass as "ok" whenever | |
| the model was merely *reachable*, so the UI printed "Assess edit | |
| consequence — passed" while nothing had been assessed. That is the same | |
| "configured means done" lie that dee/core/modal_client.reachable exists to | |
| prevent, rebuilt one layer up. A pass reports success only when it holds | |
| the output of work that happened. | |
| """ | |
| if germline: | |
| raise GermlineRefused( | |
| "This compiler designs somatic therapeutic edits only. Germline " | |
| "and embryo editing are out of scope and are not routed here.") | |
| passes: List[Pass] = [] | |
| titles = dict(PASS_ORDER) | |
| def add(name, status, detail="", diags=None): | |
| passes.append(Pass(name, titles[name], status, detail, diags or [])) | |
| # ── resolve ───────────────────────────────────────────────────────── | |
| if window: | |
| add("resolve", "ok", | |
| f"{len(window)} nt of reference supplied; variant at offset {offset}.") | |
| else: | |
| add("resolve", "warn", | |
| "No reference window supplied — the lesion can be classified from " | |
| "alleles alone, but nothing can be checked against real sequence.") | |
| # ── classify ──────────────────────────────────────────────────────── | |
| lesion = classify_lesion(wt_allele, patient_allele) | |
| if lesion.errors(): | |
| add("classify", "error", | |
| f"{lesion.kind} — no route.", lesion.diagnostics) | |
| for name, _ in PASS_ORDER[2:]: | |
| add(name, "skipped", "An earlier pass refused.") | |
| return CompileReport(lesion, passes, dict(SCOPE), strategies or [], | |
| list(pegrnas or [])) | |
| corr = lesion.correction | |
| is_pe = lesion.route == "prime_editing" | |
| if corr: | |
| classify_detail = (f"{lesion.kind}: {corr.sense_change} corrected by " | |
| f"{corr.editor_family} on the {corr.strand} strand.") | |
| elif is_pe: | |
| classify_detail = (f"{lesion.kind} ({lesion.size} nt) — not base " | |
| "editable; routed to prime editing.") | |
| else: | |
| classify_detail = lesion.kind | |
| add("classify", | |
| "warn" if any(d.level == "warning" for d in lesion.diagnostics) else "ok", | |
| classify_detail, lesion.diagnostics) | |
| # ── verify ────────────────────────────────────────────────────────── | |
| if window and (corr or is_pe): | |
| verified = compile_correction(wt_allele, patient_allele, | |
| window=window, offset=offset) | |
| new = [d for d in verified.diagnostics if d not in lesion.diagnostics] | |
| if verified.errors(): | |
| add("verify", "error", "Reference disagrees with the alleles.", new) | |
| for name, _ in PASS_ORDER[3:]: | |
| add(name, "skipped", "An earlier pass refused.") | |
| return CompileReport(verified, passes, dict(SCOPE), | |
| strategies or [], list(pegrnas or [])) | |
| if corr: | |
| detail = (f"Reference has {corr.wt_base} at offset {offset}; the " | |
| "correction reproduces it exactly.") | |
| else: | |
| wtc = _clean_allele(wt_allele) | |
| detail = (f"Reference carries the wild-type allele " | |
| f"{wtc or '(none — the patient has extra bases)'} at " | |
| f"offset {offset}." if wtc else | |
| f"Nothing to match at offset {offset}: the patient " | |
| "carries extra bases the reference does not have.") | |
| add("verify", "ok", detail, new) | |
| lesion = verified | |
| else: | |
| add("verify", "unavailable", | |
| "No reference window, so the coordinate could not be checked. An " | |
| "off-by-one still type-checks at the allele level.") | |
| # ── enumerate ─────────────────────────────────────────────────────── | |
| # Reports on DESIGNS, not on the ability to look for them. The first | |
| # version said "guides can be enumerated" and enumerated none, which made | |
| # this a classifier wearing a compiler's clothes. | |
| ediags = list(enumerate_diags or []) | |
| if is_pe: | |
| # Prime editing. Every transversion and every small indel arrives here. | |
| if pegrnas is None: | |
| add("enumerate", "unavailable", CAPABILITY_NOTES["enumerate"], ediags) | |
| elif not pegrnas: | |
| add("enumerate", "error", | |
| "No pegRNA reaches this lesion — no NGG PAM puts a nick on the " | |
| "correct side of it within reach.", ediags) | |
| for name, _ in PASS_ORDER[4:]: | |
| add(name, "skipped", "No strategy to assess.") | |
| return CompileReport(lesion, passes, dict(SCOPE), strategies or [], | |
| []) | |
| else: | |
| from dee.core import prime_editor as _pe | |
| worst = max((1 for p in pegrnas | |
| for w in p.warnings if w.level == "warning"), | |
| default=0) | |
| add("enumerate", "warn" if worst else "ok", | |
| _pe.summarise(pegrnas), ediags) | |
| elif strategies is None: | |
| add("enumerate", "unavailable", CAPABILITY_NOTES["enumerate"], ediags) | |
| elif not strategies: | |
| add("enumerate", "error", | |
| "No guide places this base inside an editing window.", ediags) | |
| for name, _ in PASS_ORDER[4:]: | |
| add(name, "skipped", "No strategy to assess.") | |
| return CompileReport(lesion, passes, dict(SCOPE), strategies or [], | |
| list(pegrnas or [])) | |
| else: | |
| n_by = sum(len(s.bystanders) for s in strategies) | |
| clean = sum(1 for s in strategies if s.clean) | |
| add("enumerate", | |
| "warn" if clean == 0 else "ok", | |
| f"{len(strategies)} {corr.editor_family} guide(s) reach this base " | |
| f"on the {corr.strand} strand; {clean} with no bystander, " | |
| f"{n_by} bystander edit(s) across the set.", ediags) | |
| # ── consequence ───────────────────────────────────────────────────── | |
| # Reports on the RESULT, never on the ability to have produced one. | |
| if consequence is None: | |
| add("consequence", "unavailable", CAPABILITY_NOTES["consequence"]) | |
| elif not consequence.get("ok"): | |
| add("consequence", "failed", | |
| "Scoring was attempted and did not return a result: " | |
| f"{consequence.get('error') or 'no detail'}. Nothing is reported " | |
| "for this pass rather than an assumed-benign default. " | |
| + CAPABILITY_NOTES["consequence"]) | |
| else: | |
| dl = consequence.get("delta_ll") | |
| label = consequence.get("label") or "the variant" | |
| # State the direction in words. A bare signed number invites the | |
| # reader to supply their own convention, and half of them will | |
| # supply the wrong one. | |
| if isinstance(dl, (int, float)): | |
| direction = ("less likely than wild-type" if dl < 0 | |
| else "more likely than wild-type" if dl > 0 | |
| else "indistinguishable from wild-type") | |
| detail = (f"{label}: delta log-likelihood {dl:+.4f} — the model " | |
| f"finds the patient sequence {direction}. ") | |
| else: | |
| detail = f"{label}: scored, no delta returned. " | |
| add("consequence", "ok", detail + CAPABILITY_NOTES["consequence"]) | |
| # ── specificity ───────────────────────────────────────────────────── | |
| # Always carries its caveat, even when it runs: a pass that reports "ok" | |
| # on a coding-sequence-only index would read as a clean bill of health. | |
| # | |
| # And it now HANDS OFF. A refusal that just stops is a shrug; one that | |
| # names the exact searches this deployment is not doing is a work order | |
| # somebody can act on. That difference is most of the value of admitting | |
| # the gap in the first place. | |
| spec_detail = CAPABILITY_NOTES["specificity"] + " Not cleared here — run: " | |
| spec_detail += "; ".join( | |
| f"{name} ({why.split('.')[0].lower()})" | |
| for name, why in (SPECIFICITY_HANDOFF["in_silico"] | |
| + SPECIFICITY_HANDOFF["empirical"])) | |
| spec_detail += ". " + SPECIFICITY_HANDOFF["note"] | |
| add("specificity", "warn" if can_check_specificity else "unavailable", | |
| spec_detail) | |
| # ── emit ──────────────────────────────────────────────────────────── | |
| add("emit", "ok", | |
| "Design record assembled with every pass, its status, and the " | |
| "reasons for anything not run.") | |
| return CompileReport(lesion, passes, dict(SCOPE), strategies or [], | |
| list(pegrnas or [])) | |
| def _pegrnas_to_dicts(pegrnas) -> List[Dict[str, object]]: | |
| """Serialise pegRNAs without importing prime_editor at module scope. | |
| The lazy import is what keeps the dependency one-way: prime_editor needs | |
| Diagnostic from here, so this module must not need it back at import time. | |
| """ | |
| if not pegrnas: | |
| return [] | |
| from dee.core import prime_editor as _pe | |
| return [_pe.pegrna_to_dict(p) for p in pegrnas] | |
| def report_to_dict(report: CompileReport) -> Dict[str, object]: | |
| """JSON shape for the API. Deliberately verbose: the record is the point, | |
| so nothing is elided to make the payload tidy.""" | |
| def diag(d: Diagnostic): | |
| return {"level": d.level, "code": d.code, | |
| "message": d.message, "remedy": d.remedy} | |
| lesion = report.lesion | |
| corr = lesion.correction if lesion else None | |
| return { | |
| "strategies": [ | |
| {"rank": s.rank, "editor_id": s.editor_id, | |
| "editor_family": s.editor_family, "strand": s.strand, | |
| "position": s.position, "spacer": s.spacer, "pam": s.pam, | |
| "target_spacer_pos": s.target_spacer_pos, | |
| "target_activity": s.target_activity, | |
| "on_target_score": s.on_target_score, | |
| "composite_score": s.composite_score, | |
| "clean": s.clean, | |
| "bystanders": [ | |
| {"spacer_pos": b.spacer_pos, "offset": b.offset, | |
| "from_base": b.from_base, "to_base": b.to_base, | |
| "activity": b.activity, "label": b.label, | |
| "delta_ll": b.delta_ll} for b in s.bystanders]} | |
| for s in (report.strategies or []) | |
| ], | |
| "pegrnas": _pegrnas_to_dicts(report.pegrnas), | |
| "compiled": report.compiled, | |
| "incomplete_because": report.incomplete_because, | |
| "scope": report.scope, | |
| "lesion": { | |
| "kind": lesion.kind, | |
| "size": lesion.size, | |
| "is_transition": lesion.is_transition, | |
| "route": lesion.route, | |
| } if lesion else None, | |
| "correction": { | |
| "wt_base": corr.wt_base, | |
| "patient_base": corr.patient_base, | |
| "sense_change": corr.sense_change, | |
| "strand": corr.strand, | |
| "editor_change": corr.editor_change, | |
| "editor_family": corr.editor_family, | |
| } if corr else None, | |
| "passes": [ | |
| {"name": p.name, "title": p.title, "status": p.status, | |
| "detail": p.detail, "diagnostics": [diag(d) for d in p.diagnostics]} | |
| for p in report.passes | |
| ], | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # Enumerate: actual guides, not a promise of guides | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # The first version of the enumerate pass reported "base-editing guides CAN | |
| # be enumerated" and enumerated none, which made the whole compiler a | |
| # classifier with ceremony. This composes crispr.find_guides and | |
| # base_editor.predict_base_edits into real strategies. | |
| # | |
| # Still deterministic — no network, no GPU, no model — but no longer | |
| # dependency-free, hence the lazy imports. | |
| # | |
| # The coordinate convention was established EMPIRICALLY, not assumed: the | |
| # spacer footprint is always window[position-1 : position-1+len] on the | |
| # forward strand, read directly for a '+' guide and reverse-complemented for | |
| # a '-' guide. Verified over 160 spacer positions across both strands with | |
| # zero mismatches before anything was built on it, because guessing here | |
| # designs a guide against the wrong strand. | |
| class Bystander: | |
| """A base the editor will also change, because it sits in the window. | |
| `from_base`/`to_base` are on the strand the EDITOR engages, which is not | |
| the forward strand for a '-' guide. `label` and `delta_ll` are the | |
| forward-strand view — what a genome model has to be asked. | |
| """ | |
| spacer_pos: int # 1-based, 5'->3' along the spacer | |
| offset: int # 0-based on the forward strand — what Evo 2 needs | |
| from_base: str # on the EDITED strand | |
| to_base: str | |
| activity: float | |
| label: str = "" # forward-strand, e.g. "A1234G" | |
| delta_ll: Optional[float] = None # None = not scored, NOT "harmless" | |
| class Strategy: | |
| rank: int | |
| editor_id: str | |
| editor_family: str | |
| strand: str # '+' sense | '-' antisense | |
| position: int # 1-based forward start of the spacer footprint | |
| spacer: str | |
| pam: str | |
| target_spacer_pos: int | |
| target_activity: float | |
| on_target_score: float | |
| composite_score: float | |
| bystanders: List[Bystander] = field(default_factory=list) | |
| def clean(self) -> bool: | |
| return not self.bystanders | |
| def spacer_pos_to_offset(position: int, strand: str, spacer_len: int, | |
| spacer_pos: int) -> int: | |
| """1-based spacer index -> 0-based forward-strand offset. | |
| Separate and named because it is the single most dangerous line here. | |
| A '-' guide's spacer runs antiparallel: its 5' base is the LAST base of | |
| the forward footprint, so the index has to be mirrored. | |
| """ | |
| start = position - 1 | |
| if strand == "+": | |
| return start + (spacer_pos - 1) | |
| return start + (spacer_len - spacer_pos) | |
| def bystander_forward_label(window: str, offset: int, from_base: str, | |
| to_base: str, strand: str) -> str: | |
| """Forward-strand variant label for a bystander, e.g. "A1234G". | |
| The reference base is READ FROM THE WINDOW rather than derived from | |
| `from_base`. Same trick as variant_resolve.reconcile_strand: the genome | |
| is authoritative, so there is no strand arithmetic to get wrong on the | |
| half that matters. Only the ALT needs complementing, and that is checked | |
| against the reference before it is trusted. | |
| Raises ValueError when the editor's view of the base disagrees with the | |
| genome — which would mean the spacer/offset mapping is broken, and | |
| scoring a label built on it would produce a confident wrong number. | |
| """ | |
| if not window or not (0 <= offset < len(window)): | |
| raise ValueError(f"bystander offset {offset} outside the window") | |
| fwd_ref = window[offset].upper() | |
| expect = from_base.upper() if strand == "+" else _COMPLEMENT[from_base.upper()] | |
| if fwd_ref != expect: | |
| raise ValueError( | |
| f"bystander at offset {offset}: genome has {fwd_ref}, the editor " | |
| f"sees {from_base} on the {strand} strand (expected {expect}). " | |
| "The spacer-to-genome mapping is wrong; refusing to build a label.") | |
| fwd_alt = to_base.upper() if strand == "+" else _COMPLEMENT[to_base.upper()] | |
| return f"{fwd_ref}{offset + 1}{fwd_alt}" | |
| def label_bystanders(window: str, strategies: List["Strategy"]) -> List[str]: | |
| """Attach forward-strand labels to every bystander; return the unique set. | |
| One flat list so the caller can score them all in ONE model call rather | |
| than one per bystander — a 7B GPU round trip per base would make the | |
| feature unusable. | |
| """ | |
| seen: List[str] = [] | |
| for st in strategies or []: | |
| for b in st.bystanders: | |
| b.label = bystander_forward_label( | |
| window, b.offset, b.from_base, b.to_base, st.strand) | |
| if b.label not in seen: | |
| seen.append(b.label) | |
| return seen | |
| def attach_bystander_scores(strategies: List["Strategy"], | |
| scores: Dict[str, float]) -> None: | |
| """Fill in delta_ll from a label->score map. Unscored stays None. | |
| None must never be coerced to 0.0: "not scored" and "predicted neutral" | |
| are different claims, and a therapeutic reader will act on them | |
| differently. | |
| """ | |
| for st in strategies or []: | |
| for b in st.bystanders: | |
| if b.label in scores: | |
| b.delta_ll = scores[b.label] | |
| def plan_base_edit_strategies(window: str, target_offset: int, | |
| corr: Correction, *, editor_id: str = "", | |
| max_results: int = 6 | |
| ) -> Tuple[List[Strategy], List[Diagnostic]]: | |
| """Guides that put the target base in the editing window, right strand. | |
| `window` is WILD-TYPE reference. Guides are designed against the | |
| PATIENT'S sequence, which this function derives — and that distinction is | |
| the whole ballgame. An ABE has to find an A to convert; if you search the | |
| wild-type reference for an ABE correction you are looking at the G that is | |
| already correct, and every search returns nothing. The first version of | |
| this function did exactly that and reported "no guide places this base in | |
| an editing window" for lesions that are perfectly editable. | |
| Returns (strategies, diagnostics). An empty list with a diagnostic is a | |
| real answer — "no guide places this base in any editing window" is the | |
| single most common reason a base-editable lesion is still not treatable, | |
| and it is caused by PAM availability, which nothing can argue with. | |
| """ | |
| from dee.core import base_editor as _be | |
| from dee.core import crispr as _crispr | |
| diags: List[Diagnostic] = [] | |
| want_strand = "+" if corr.strand == "sense" else "-" | |
| if not window or not (0 <= target_offset < len(window)): | |
| diags.append(Diagnostic( | |
| "error", "target_outside_window", | |
| f"Target offset {target_offset} is outside the " | |
| f"{len(window)}-base window.", "Check the coordinate.")) | |
| return [], diags | |
| if window[target_offset].upper() != corr.wt_base: | |
| diags.append(Diagnostic( | |
| "error", "window_is_not_wildtype", | |
| f"The window has {window[target_offset]!r} at the target, but the " | |
| f"wild-type allele is {corr.wt_base!r}.", | |
| "Pass the WILD-TYPE reference; the patient sequence is derived " | |
| "from it here.")) | |
| return [], diags | |
| # The sequence the editor actually sees. | |
| patient_seq = (window[:target_offset] + corr.patient_base | |
| + window[target_offset + 1:]) | |
| if not editor_id: | |
| # Default to the first editor of the required family. Not an | |
| # allow-list: the caller can name any editor the catalogue knows. | |
| fam = [e for e in _be.list_base_editors(corr.editor_family)] | |
| if not fam: | |
| diags.append(Diagnostic( | |
| "error", "no_editor_for_family", | |
| f"No {corr.editor_family} is registered in the editor catalogue.", | |
| "Add one to dee/core/base_editor.py.")) | |
| return [], diags | |
| editor_id = fam[0].id | |
| ed = _be.get_base_editor(editor_id) | |
| if ed is None: | |
| diags.append(Diagnostic("error", "unknown_editor", | |
| f"Unknown base editor {editor_id!r}.", | |
| "Pick one from the catalogue.")) | |
| return [], diags | |
| if ed.kind != corr.editor_family: | |
| diags.append(Diagnostic( | |
| "error", "editor_family_mismatch", | |
| f"{editor_id} is a {ed.kind}; this correction needs a " | |
| f"{corr.editor_family}.", | |
| f"{ed.kind} writes {ed.target_base}>{ed.result_base}, which does " | |
| "not make this change.")) | |
| return [], diags | |
| try: | |
| guides = _crispr.find_guides(patient_seq, mode="base_edit", | |
| base_editor=editor_id, max_results=200) | |
| except ValueError as exc: | |
| diags.append(Diagnostic( | |
| "error", "no_guides_possible", str(exc), | |
| "A base editor needs a PAM at a fixed distance from the target. " | |
| "Supply a longer reference window so more PAMs are in range.")) | |
| return [], diags | |
| out: List[Strategy] = [] | |
| for g in guides: | |
| if g.strand != want_strand: | |
| continue | |
| pred = _be.predict_base_edits(g.spacer, editor_id) | |
| if not pred.edits: | |
| continue | |
| hit = None | |
| rest: List[Bystander] = [] | |
| for p, fb, tb, act in pred.edits: | |
| off = spacer_pos_to_offset(g.position, g.strand, len(g.spacer), p) | |
| if off == target_offset: | |
| hit = (p, act) | |
| else: | |
| rest.append(Bystander(p, off, fb, tb, round(act, 3))) | |
| if hit is None: | |
| continue # this guide edits, but not the base we care about | |
| out.append(Strategy( | |
| rank=0, editor_id=editor_id, editor_family=ed.kind, | |
| strand=g.strand, position=g.position, spacer=g.spacer, pam=g.pam, | |
| target_spacer_pos=hit[0], target_activity=round(hit[1], 3), | |
| on_target_score=round(g.on_target_score, 3), | |
| composite_score=round(g.composite_score, 3), | |
| bystanders=rest)) | |
| if not out: | |
| diags.append(Diagnostic( | |
| "error", "no_guide_places_target_in_window", | |
| f"No {editor_id} guide on the {corr.strand} strand puts this base " | |
| f"inside the editing window (positions {ed.window[0]}-{ed.window[1]}).", | |
| "This is a PAM-availability limit, not a scoring threshold: a base " | |
| "editor can only reach bases at a fixed distance from an NGG. Try " | |
| "another editor whose window sits differently, a wider reference " | |
| "window, or a different Cas variant.")) | |
| return [], diags | |
| # Rank by the editor's activity at the TARGET base first. A guide that | |
| # edits the right base weakly is worse than one that edits it strongly, | |
| # regardless of how the generic on-target heuristic scores the spacer. | |
| out.sort(key=lambda s: (-s.target_activity, len(s.bystanders), | |
| -s.composite_score)) | |
| for i, s in enumerate(out[:max_results], 1): | |
| s.rank = i | |
| n_clean = sum(1 for s in out[:max_results] if s.clean) | |
| if n_clean == 0: | |
| diags.append(Diagnostic( | |
| "warning", "every_guide_has_bystanders", | |
| "Every guide that reaches this base also edits at least one other " | |
| "base in its window.", | |
| "Bystanders are not automatically benign. Score them before " | |
| "choosing — a silent or intronic bystander in a regulatory " | |
| "element is exactly the case nothing else checks.")) | |
| return out[:max_results], diags | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # The design record | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # In a platform-IND world the deliverable is not a therapy — it is a design | |
| # and its complete justification, in a form a reviewer can audit and a third | |
| # party can reproduce. Today that document is assembled by hand, per patient, | |
| # by scientists. This emits it. | |
| # | |
| # Deterministic by construction: same inputs, byte-identical output. Nothing | |
| # here reads a clock or a random source, because a record that changes between | |
| # runs cannot be diffed, and a record that cannot be diffed cannot be audited. | |
| # The caller stamps time and provenance if it wants them. | |
| RECORD_VERSION = "1" | |
| def design_record(report: "CompileReport", *, variant: str = "", | |
| resolved: Optional[Dict[str, object]] = None, | |
| provenance: Optional[Dict[str, str]] = None) -> str: | |
| """Render a compile report as a plain-text design record. | |
| Plain text on purpose: it diffs, it pastes into an email or a lab | |
| notebook, it survives every tool in the chain, and nothing about it can | |
| silently re-render differently later. | |
| """ | |
| L: List[str] = [] | |
| add = L.append | |
| add("TURINGDNA THERAPEUTIC DESIGN RECORD") | |
| add(f"record-version {RECORD_VERSION}") | |
| add("=" * 72) | |
| add("") | |
| add("SCOPE") | |
| for k in ("application", "status", "silent_on"): | |
| add(f" {k:<12} {report.scope.get(k, '')}") | |
| add("") | |
| if variant or resolved: | |
| add("VARIANT") | |
| if variant: | |
| add(f" notation {variant}") | |
| for key, label in (("gene", "gene"), ("transcript", "transcript"), | |
| ("chrom", "chromosome"), ("position", "position"), | |
| ("assembly", "assembly"), ("consequence", "consequence"), | |
| ("orientation", "allele orientation"), | |
| ("source", "resolved by")): | |
| val = (resolved or {}).get(key) | |
| if val: | |
| add(f" {label:<12} {val}") | |
| if resolved: | |
| add(f" {'alleles':<12} wild-type {resolved.get('wt_base')} -> " | |
| f"patient {resolved.get('patient_base')} (forward strand)") | |
| add("") | |
| corr = report.lesion.correction if report.lesion else None | |
| if report.lesion: | |
| add("LESION") | |
| add(f" kind {report.lesion.kind}") | |
| add(f" size {report.lesion.size}") | |
| add(f" route {report.lesion.route}") | |
| if corr: | |
| add(f" correction {corr.sense_change} on the sense strand") | |
| add(f" chemistry {corr.editor_family} writes {corr.editor_change} " | |
| f"on the {corr.strand} strand") | |
| add("") | |
| add("PASSES") | |
| for p in report.passes: | |
| add(f" [{p.status:<11}] {p.title}") | |
| if p.detail: | |
| for line in _wrap(p.detail, 68): | |
| add(f" {line}") | |
| for d in p.diagnostics: | |
| add(f" - {d.level.upper()} {d.code}: {d.message}") | |
| if d.remedy: | |
| for line in _wrap(f"remedy: {d.remedy}", 62): | |
| add(f" {line}") | |
| add("") | |
| if report.strategies: | |
| add("STRATEGIES") | |
| for s in report.strategies: | |
| add(f" #{s.rank} {s.editor_id} on the " | |
| f"{'sense' if s.strand == '+' else 'antisense'} strand") | |
| add(f" spacer {s.spacer}") | |
| add(f" PAM {s.pam}") | |
| add(f" forward position {s.position}") | |
| add(f" target at spacer {s.target_spacer_pos} " | |
| f"(editor activity {s.target_activity})") | |
| add(f" on-target score {s.on_target_score}") | |
| if not s.bystanders: | |
| add(" bystanders none in this guide's window") | |
| else: | |
| add(f" bystanders {len(s.bystanders)}") | |
| for b in s.bystanders: | |
| d = ("not scored" if b.delta_ll is None | |
| else f"delta log-likelihood {b.delta_ll:+.4f}") | |
| add(f" {b.label or (b.from_base + '>' + b.to_base)}" | |
| f" spacer pos {b.spacer_pos} {d}") | |
| add("") | |
| if report.pegrnas: | |
| from dee.core import prime_editor as _pe | |
| add("PRIME EDITING — pegRNAs") | |
| add(" Each is emitted as three parts. The scaffold is a construct") | |
| add(" decision and is deliberately not supplied here.") | |
| add("") | |
| for p in report.pegrnas: | |
| add(f" #{p.rank} nicking the " | |
| f"{'sense' if p.strand == '+' else 'antisense'} strand") | |
| add(f" spacer {p.spacer} (PAM {p.pam})") | |
| add(f" nick at forward index {p.nick_offset}; the " | |
| f"edit is {p.nick_to_edit} nt into the new strand") | |
| add(f" PBS {p.pbs_len:>2} nt {p.pbs}" | |
| f" GC {p.pbs_gc}%" | |
| + (f", Tm {p.pbs_tm} C" if p.pbs_tm is not None else "")) | |
| add(f" RTT {p.rtt_len:>2} nt {p.rtt}") | |
| add(f" 3' extension {p.extension}") | |
| add(f" synthesised flap {p.flap}") | |
| add(f" homology past edit {p.homology} nt") | |
| ok = "reproduced the reference exactly" if p.reconstructed else "FAILED" | |
| add(f" reconstruction {ok}") | |
| add(f" corrected allele re-targeted by this spacer: " | |
| f"{'yes' if p.re_engages_edited_allele else 'no'}") | |
| for w in p.warnings: | |
| add(f" - {w.level.upper()} {w.code}: {w.message}") | |
| if p.nicks: | |
| add(" second nick options (PE3 / PE3b):") | |
| for n in p.nicks: | |
| kind = "PE3b" if n.pe3b else "PE3 " | |
| band = " [in the 40-90 nt band]" if n.in_optimal_band else "" | |
| add(f" {kind} {n.spacer} {n.strand} " | |
| f"{n.signed_offset:+d} nt{band}") | |
| else: | |
| add(" second nick none found in this window; PE2 " | |
| "(pegRNA alone) needs none") | |
| if p.screen: | |
| add(f" lengths to screen ({len(p.screen)} combinations):") | |
| for v in p.screen: | |
| flag = " <- starts with C, avoid" if v.first_base_is_c else "" | |
| add(f" PBS {v.pbs_len:>2} / RTT {v.rtt_len:>2} " | |
| f"{v.extension}{flag}") | |
| add("") | |
| add(" Heuristics used, with their sources:") | |
| for key in ("pbs_length", "rtt_length", "no_c_at_extension_start", | |
| "pe3_nick_window", "pe3b"): | |
| h = _pe.HEURISTICS[key] | |
| for line in _wrap(f"{key}: {h['rule']}", 64): | |
| add(f" {line}") | |
| for line in _wrap(f"source: {h['source']}", 60): | |
| add(f" {line}") | |
| add("") | |
| gaps = report.incomplete_because | |
| add("COMPLETENESS") | |
| add(f" compiled {'yes' if report.compiled else 'NO'}") | |
| if gaps: | |
| add(" NOT ESTABLISHED by this record:") | |
| for g in gaps: | |
| add(f" - {g}") | |
| else: | |
| add(" every pass produced a result") | |
| add("") | |
| if provenance: | |
| add("PROVENANCE") | |
| for k in sorted(provenance): | |
| add(f" {k:<12} {provenance[k]}") | |
| add("") | |
| add("-" * 72) | |
| add("This is a design record, not a clinical decision and not an approval.") | |
| add("Predicted specificity is not measured specificity. Model-derived") | |
| add("judgements are zero-shot and have no validated relationship to") | |
| add("clinical outcome. Nothing here substitutes for the preclinical") | |
| add("programme, and nothing here should reach a patient on its own.") | |
| return "\n".join(L) | |
| def _wrap(text: str, width: int) -> List[str]: | |
| """Tiny greedy wrapper — textwrap would do, but this keeps the record's | |
| formatting identical across Python versions.""" | |
| words, line, out = str(text).split(), "", [] | |
| for w in words: | |
| if line and len(line) + 1 + len(w) > width: | |
| out.append(line) | |
| line = w | |
| else: | |
| line = f"{line} {w}".strip() | |
| if line: | |
| out.append(line) | |
| return out | |