pen-stack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pen_stack/__init__.py +2 -0
- pen_stack/_resources.py +34 -0
- pen_stack/active/__init__.py +20 -0
- pen_stack/active/acquire.py +165 -0
- pen_stack/active/brains.py +74 -0
- pen_stack/active/campaign.py +109 -0
- pen_stack/active/design.py +66 -0
- pen_stack/active/validate.py +104 -0
- pen_stack/adapt/__init__.py +14 -0
- pen_stack/adapt/finetune.py +33 -0
- pen_stack/adapt/ingest.py +86 -0
- pen_stack/adapt/pipeline.py +101 -0
- pen_stack/adapt/recalibrate.py +58 -0
- pen_stack/adapt/report.py +130 -0
- pen_stack/agent/__init__.py +1 -0
- pen_stack/agent/cite.py +175 -0
- pen_stack/agent/co_scientist.py +262 -0
- pen_stack/agent/epistemic.py +102 -0
- pen_stack/agent/guardrails.py +67 -0
- pen_stack/agent/mcp_server.py +215 -0
- pen_stack/agent/orchestrator.py +112 -0
- pen_stack/agent/orchestrator_live.py +56 -0
- pen_stack/agent/pen_agent.py +242 -0
- pen_stack/agent/scope.py +60 -0
- pen_stack/agent/tools.py +130 -0
- pen_stack/api/__init__.py +12 -0
- pen_stack/api/manifest.py +160 -0
- pen_stack/atlas/__init__.py +1 -0
- pen_stack/atlas/atlas.parquet +0 -0
- pen_stack/atlas/build_wtkb.py +80 -0
- pen_stack/atlas/crosslink.py +179 -0
- pen_stack/atlas/expand.py +190 -0
- pen_stack/atlas/guide_design.py +178 -0
- pen_stack/atlas/schema.py +59 -0
- pen_stack/atlas/scorecard.py +134 -0
- pen_stack/atlas/scorecard_v3.parquet +0 -0
- pen_stack/atlas/universe.py +75 -0
- pen_stack/atlas/universe_v3.parquet +0 -0
- pen_stack/atlas/variant_propose.py +155 -0
- pen_stack/atlas/writer_efficiency.py +184 -0
- pen_stack/atlas/writer_predict.py +229 -0
- pen_stack/atlas/writer_recommend.py +170 -0
- pen_stack/atlas/writer_verify.py +167 -0
- pen_stack/atlas/wtkb.parquet +0 -0
- pen_stack/bridge/__init__.py +1 -0
- pen_stack/bridge/activity.py +52 -0
- pen_stack/bridge/cli.py +65 -0
- pen_stack/bridge/fold_qc.py +53 -0
- pen_stack/bridge/guide_qc.py +87 -0
- pen_stack/bridge/ingest.py +139 -0
- pen_stack/bridge/offtarget.py +191 -0
- pen_stack/bridge/offtarget_energetics.py +105 -0
- pen_stack/bridge/ortholog_screen.py +73 -0
- pen_stack/bridge/pipeline.py +83 -0
- pen_stack/build/__init__.py +16 -0
- pen_stack/build/cloudlab.py +74 -0
- pen_stack/build/ingest.py +47 -0
- pen_stack/build/protocol.py +82 -0
- pen_stack/build/simlab.py +30 -0
- pen_stack/cli.py +126 -0
- pen_stack/data/__init__.py +1 -0
- pen_stack/data/encode.py +84 -0
- pen_stack/data/genome.py +71 -0
- pen_stack/data/ingest_chromatin.py +119 -0
- pen_stack/data/ingest_integration.py +112 -0
- pen_stack/data/ingest_safety_annot.py +201 -0
- pen_stack/data/ingest_trip.py +76 -0
- pen_stack/design/__init__.py +14 -0
- pen_stack/design/capsid_generate.py +62 -0
- pen_stack/design/generate.py +70 -0
- pen_stack/design/pareto.py +70 -0
- pen_stack/design/space.py +137 -0
- pen_stack/design/writer_variants.py +121 -0
- pen_stack/env/__init__.py +1 -0
- pen_stack/env/genome_writing_env.py +248 -0
- pen_stack/env/policies.py +94 -0
- pen_stack/graph/__init__.py +21 -0
- pen_stack/graph/build.py +133 -0
- pen_stack/graph/cell_types.py +58 -0
- pen_stack/graph/ingest.py +132 -0
- pen_stack/graph/query.py +148 -0
- pen_stack/graph/schema.py +100 -0
- pen_stack/loop/__init__.py +15 -0
- pen_stack/loop/continual.py +61 -0
- pen_stack/loop/cycle.py +84 -0
- pen_stack/loop/drift.py +41 -0
- pen_stack/mech/__init__.py +1 -0
- pen_stack/mech/classify_atlas.py +71 -0
- pen_stack/mech/pfam_whitelist.yaml +247 -0
- pen_stack/mech/whitelist.py +66 -0
- pen_stack/monitor/__init__.py +1 -0
- pen_stack/monitor/europepmc.py +32 -0
- pen_stack/monitor/run.py +57 -0
- pen_stack/monitor/triage.py +63 -0
- pen_stack/oracles/__init__.py +65 -0
- pen_stack/oracles/affinity.py +116 -0
- pen_stack/oracles/cache.py +53 -0
- pen_stack/oracles/energetics.py +33 -0
- pen_stack/oracles/genome.py +167 -0
- pen_stack/oracles/protein_design.py +136 -0
- pen_stack/oracles/reliability.py +64 -0
- pen_stack/oracles/rna.py +28 -0
- pen_stack/oracles/schema.py +77 -0
- pen_stack/oracles/status.py +123 -0
- pen_stack/oracles/structure.py +42 -0
- pen_stack/oracles/structure_run.py +76 -0
- pen_stack/oracles/vcell.py +74 -0
- pen_stack/planner/__init__.py +1 -0
- pen_stack/planner/ada_risk.py +64 -0
- pen_stack/planner/antipeg_oracle.py +75 -0
- pen_stack/planner/capsid_epitope_oracle.py +135 -0
- pen_stack/planner/cargo.py +56 -0
- pen_stack/planner/cargo_polish.py +146 -0
- pen_stack/planner/chromosome.py +106 -0
- pen_stack/planner/delivery.py +55 -0
- pen_stack/planner/delivery_constraints.py +110 -0
- pen_stack/planner/delivery_immune.py +61 -0
- pen_stack/planner/delivery_immunology.py +222 -0
- pen_stack/planner/delivery_predict.py +196 -0
- pen_stack/planner/delivery_vehicles.py +37 -0
- pen_stack/planner/genotoxicity_oracle.py +112 -0
- pen_stack/planner/immune_mhc2.py +154 -0
- pen_stack/planner/immune_profile.py +292 -0
- pen_stack/planner/innate_sensing.py +135 -0
- pen_stack/planner/multiplex.py +110 -0
- pen_stack/planner/optimize.py +278 -0
- pen_stack/planner/pipeline.py +87 -0
- pen_stack/planner/report.py +26 -0
- pen_stack/planner/router.py +57 -0
- pen_stack/planner/seroprevalence_oracle.py +92 -0
- pen_stack/planner/target_site.py +118 -0
- pen_stack/rag/__init__.py +1 -0
- pen_stack/rag/corpus.py +133 -0
- pen_stack/rag/embed.py +98 -0
- pen_stack/rag/ground.py +131 -0
- pen_stack/rag/index.py +53 -0
- pen_stack/rag/llm.py +215 -0
- pen_stack/rag/qa.py +105 -0
- pen_stack/rag/retrieve.py +48 -0
- pen_stack/rules/__init__.py +9 -0
- pen_stack/rules/evaluators.py +318 -0
- pen_stack/rules/loader.py +31 -0
- pen_stack/rules/schema.py +99 -0
- pen_stack/rules/solver.py +43 -0
- pen_stack/rules/spec.py +78 -0
- pen_stack/safety/__init__.py +21 -0
- pen_stack/safety/audit.py +90 -0
- pen_stack/safety/gate.py +58 -0
- pen_stack/safety/pfam_scan.py +157 -0
- pen_stack/safety/policy.py +69 -0
- pen_stack/safety/redteam.py +71 -0
- pen_stack/safety/registry.py +255 -0
- pen_stack/safety/screen.py +59 -0
- pen_stack/safety/standards.py +141 -0
- pen_stack/score/__init__.py +1 -0
- pen_stack/score/recalibrate.py +77 -0
- pen_stack/score/therapeutic.py +85 -0
- pen_stack/server/__init__.py +1 -0
- pen_stack/server/api.py +647 -0
- pen_stack/spec/__init__.py +18 -0
- pen_stack/spec/clarify.py +42 -0
- pen_stack/spec/extract.py +406 -0
- pen_stack/spec/resolvers/__init__.py +17 -0
- pen_stack/spec/resolvers/cell.py +51 -0
- pen_stack/spec/resolvers/chem.py +32 -0
- pen_stack/spec/resolvers/feature.py +36 -0
- pen_stack/spec/resolvers/gene.py +43 -0
- pen_stack/spec/resolvers/locus.py +29 -0
- pen_stack/spec/resolvers/phenotype.py +37 -0
- pen_stack/spec/satisfy.py +114 -0
- pen_stack/spec/service.py +33 -0
- pen_stack/spec/writespec.py +252 -0
- pen_stack/twin/__init__.py +14 -0
- pen_stack/twin/calibrate.py +61 -0
- pen_stack/twin/data/__init__.py +12 -0
- pen_stack/twin/data/position_effect.py +245 -0
- pen_stack/twin/mechanistic.py +147 -0
- pen_stack/twin/outcome.py +132 -0
- pen_stack/twin/position_effect.py +454 -0
- pen_stack/ui/__init__.py +1 -0
- pen_stack/ui/app.py +713 -0
- pen_stack/validate/__init__.py +1 -0
- pen_stack/validate/adapt_demo.py +69 -0
- pen_stack/validate/agent_eval.py +117 -0
- pen_stack/validate/bench_adversarial_tasks.py +118 -0
- pen_stack/validate/bench_coscientist_tasks.py +60 -0
- pen_stack/validate/bench_graph_tasks.py +64 -0
- pen_stack/validate/bench_rule_tasks.py +84 -0
- pen_stack/validate/bench_trust_tasks.py +92 -0
- pen_stack/validate/bench_writetype_tasks.py +101 -0
- pen_stack/validate/blind_gsh_discovery.py +261 -0
- pen_stack/validate/cargo_directionality.py +57 -0
- pen_stack/validate/closed_loop.py +63 -0
- pen_stack/validate/durability_baselines.py +185 -0
- pen_stack/validate/experiment_design.py +65 -0
- pen_stack/validate/expr_controls.py +39 -0
- pen_stack/validate/forward_hypotheses.py +104 -0
- pen_stack/validate/generative_design.py +62 -0
- pen_stack/validate/guide_qc_demo.py +69 -0
- pen_stack/validate/heldout_celltype_expr.py +32 -0
- pen_stack/validate/immune_calibration.py +133 -0
- pen_stack/validate/intent_specification.py +82 -0
- pen_stack/validate/known_biology_expr.py +38 -0
- pen_stack/validate/offtarget_energetics_eval.py +144 -0
- pen_stack/validate/out_of_scope_refusal.py +82 -0
- pen_stack/validate/outcome_calibration.py +194 -0
- pen_stack/validate/outcome_prediction.py +76 -0
- pen_stack/validate/paper3_benchmark.py +165 -0
- pen_stack/validate/paper4_real_validation.py +144 -0
- pen_stack/validate/paper4_validation.py +82 -0
- pen_stack/validate/protocol_safety.py +62 -0
- pen_stack/validate/safety_screening.py +72 -0
- pen_stack/validate/selective_prediction.py +104 -0
- pen_stack/validate/seq_vs_measured.py +134 -0
- pen_stack/validate/target_site_controls.py +65 -0
- pen_stack/validate/uncertainty_eval.py +244 -0
- pen_stack/validate/ungrounded_baseline.py +234 -0
- pen_stack/validate/within_locus_ranking.py +84 -0
- pen_stack/validate/writer_recovery.py +91 -0
- pen_stack/verify/__init__.py +5 -0
- pen_stack/verify/proof.py +206 -0
- pen_stack/verify/schema.py +53 -0
- pen_stack/verify/service.py +191 -0
- pen_stack/web/__init__.py +18 -0
- pen_stack/web/guide.py +110 -0
- pen_stack/web/llm.py +393 -0
- pen_stack/web/llm_provider.py +119 -0
- pen_stack/web/router.py +119 -0
- pen_stack/web/server.py +96 -0
- pen_stack/web/tools.py +197 -0
- pen_stack/wgenome/__init__.py +1 -0
- pen_stack/wgenome/chromatin_seq.py +83 -0
- pen_stack/wgenome/durability.py +108 -0
- pen_stack/wgenome/export_tracks.py +52 -0
- pen_stack/wgenome/features.py +82 -0
- pen_stack/wgenome/genotoxic_blocklist.py +88 -0
- pen_stack/wgenome/gsh_baseline.py +154 -0
- pen_stack/wgenome/mesh_features.py +61 -0
- pen_stack/wgenome/offtarget_assay.py +80 -0
- pen_stack/wgenome/offtarget_bridge.py +47 -0
- pen_stack/wgenome/offtarget_cast.py +97 -0
- pen_stack/wgenome/offtarget_data.py +148 -0
- pen_stack/wgenome/offtarget_enumerate.py +274 -0
- pen_stack/wgenome/offtarget_integrase.py +155 -0
- pen_stack/wgenome/offtarget_nuclease.py +123 -0
- pen_stack/wgenome/offtarget_paste.py +41 -0
- pen_stack/wgenome/offtarget_predict.py +282 -0
- pen_stack/wgenome/ood.py +135 -0
- pen_stack/wgenome/providers.py +278 -0
- pen_stack/wgenome/safety.py +69 -0
- pen_stack/wgenome/structure3d.py +212 -0
- pen_stack/wgenome/uncertainty.py +250 -0
- pen_stack/wgenome/writability.py +72 -0
- pen_stack-0.1.0.dist-info/METADATA +401 -0
- pen_stack-0.1.0.dist-info/RECORD +259 -0
- pen_stack-0.1.0.dist-info/WHEEL +5 -0
- pen_stack-0.1.0.dist-info/entry_points.txt +3 -0
- pen_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
- pen_stack-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Rule evaluators, the bridge from rules-as-data to the existing validated code.
|
|
2
|
+
|
|
3
|
+
Each evaluator runs ONE rule against a ``Design`` and returns a ``RuleResult``. Crucially, evaluators
|
|
4
|
+
**delegate to the functions that already implement the logic** (``planner.target_site``, ``bridge.fold_qc``,
|
|
5
|
+
``planner.delivery_constraints``, ``planner.multiplex``, and the delivery-vehicle table), so lifting the
|
|
6
|
+
rules into data changes no decision (the parity test proves it). An evaluator whose required inputs are
|
|
7
|
+
absent returns ``not_applicable`` (never a spurious violate), so a partial design is still checkable.
|
|
8
|
+
|
|
9
|
+
Evaluators are registered by name; a rule's ``evaluator`` field names the function. To add a rule you add a
|
|
10
|
+
YAML record + (if new) one evaluator here, never scattered ``if`` checks in the planner.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
|
|
17
|
+
from pen_stack.rules.schema import Design, Rule, RuleResult
|
|
18
|
+
|
|
19
|
+
_REGISTRY: dict[str, Callable[[Design, Rule], RuleResult]] = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def evaluator(name: str):
|
|
23
|
+
def deco(fn):
|
|
24
|
+
_REGISTRY[name] = fn
|
|
25
|
+
return fn
|
|
26
|
+
return deco
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_evaluator(name: str) -> Callable[[Design, Rule], RuleResult]:
|
|
30
|
+
if name not in _REGISTRY:
|
|
31
|
+
raise KeyError(f"no rule evaluator registered as {name!r}")
|
|
32
|
+
return _REGISTRY[name]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def registered_evaluators() -> set[str]:
|
|
36
|
+
"""Names of every registered evaluator (used by the rule-spec export to confirm each rule is executable)."""
|
|
37
|
+
return set(_REGISTRY)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _result(rule: Rule, status: str, reason: str, value=None) -> RuleResult:
|
|
41
|
+
return RuleResult(rule_id=rule.id, kind=rule.kind, category=rule.category, status=status,
|
|
42
|
+
reason=reason, citation=list(rule.provenance.get("doi", [])), value=value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _na(rule: Rule, why: str) -> RuleResult:
|
|
46
|
+
return _result(rule, "not_applicable", f"not applicable: {why}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# writer output form (what the vehicle must carry) when the design does not state it explicitly.
|
|
50
|
+
_WRITER_FORM = {"bridge_is110": "DNA", "seek_is1111": "DNA", "cast_vk": "DNA",
|
|
51
|
+
"serine_integrase": "DNA", "pe_integrase": "DNA",
|
|
52
|
+
"cas9": "RNP", "cas12a": "RNP"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def writer_output_form(design: Design) -> str | None:
|
|
56
|
+
if design.writer_output_form:
|
|
57
|
+
return design.writer_output_form
|
|
58
|
+
return _WRITER_FORM.get(str(design.writer_family or "").lower())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# reachability / target-site (delegates to planner.target_site, the hard filter)
|
|
62
|
+
@evaluator("reachability_target_site")
|
|
63
|
+
def reachability_target_site(design: Design, rule: Rule) -> RuleResult:
|
|
64
|
+
if not design.writer_family or not design.site_seq:
|
|
65
|
+
return _na(rule, "no writer_family + site_seq")
|
|
66
|
+
from pen_stack.planner.target_site import target_site_available
|
|
67
|
+
v = target_site_available(design.writer_family, design.site_seq, installed_att=design.installed_att)
|
|
68
|
+
if not v.get("checked", True):
|
|
69
|
+
return _na(rule, v.get("reason", "family has no target-site rule"))
|
|
70
|
+
return _result(rule, "pass" if v["available"] else "violate", v["reason"])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# fold legality (delegates to bridge.fold_qc cross-loop screen, soft penalty / flag)
|
|
74
|
+
@evaluator("fold_cross_loop")
|
|
75
|
+
def fold_cross_loop(design: Design, rule: Rule) -> RuleResult:
|
|
76
|
+
if not design.target_guide or not design.donor_guide:
|
|
77
|
+
return _na(rule, "no bridge-RNA target_guide + donor_guide")
|
|
78
|
+
from pen_stack.bridge.fold_qc import cross_loop_risk
|
|
79
|
+
thr = float(rule.param.get("cross_loop_threshold", 0.6))
|
|
80
|
+
xl = cross_loop_risk(design.target_guide, design.donor_guide)
|
|
81
|
+
worst = max(xl.values())
|
|
82
|
+
flagged = [k for k, val in xl.items() if val >= thr]
|
|
83
|
+
if flagged:
|
|
84
|
+
return _result(rule, "flag", f"cross-loop complementarity {flagged} >= {thr} "
|
|
85
|
+
"(self/TBL-DBL recombination risk)", value=round(worst, 3))
|
|
86
|
+
return _result(rule, "pass", f"cross-loop complementarity below {thr}", value=round(worst, 3))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# payload capacity (cargo_bp <= vehicle capacity, hard reject) + split-AAV penalty (soft)
|
|
90
|
+
@evaluator("payload_capacity")
|
|
91
|
+
def payload_capacity(design: Design, rule: Rule) -> RuleResult:
|
|
92
|
+
if design.cargo_bp is None or not design.delivery_vehicle:
|
|
93
|
+
return _na(rule, "no cargo_bp + delivery_vehicle")
|
|
94
|
+
from pen_stack.planner.delivery_vehicles import vehicle
|
|
95
|
+
veh = vehicle(design.delivery_vehicle)
|
|
96
|
+
if veh is None:
|
|
97
|
+
return _na(rule, f"unknown vehicle {design.delivery_vehicle!r}")
|
|
98
|
+
cap = veh.get("cargo_capacity_bp")
|
|
99
|
+
if cap is None:
|
|
100
|
+
return _na(rule, "vehicle has no capacity (e.g. physical delivery)")
|
|
101
|
+
if design.cargo_bp > cap:
|
|
102
|
+
return _result(rule, "violate", f"cargo {design.cargo_bp} bp exceeds {design.delivery_vehicle} "
|
|
103
|
+
f"capacity {cap} bp", value=design.cargo_bp)
|
|
104
|
+
return _result(rule, "pass", f"cargo {design.cargo_bp} bp within {design.delivery_vehicle} "
|
|
105
|
+
f"capacity {cap} bp", value=design.cargo_bp)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@evaluator("split_aav_penalty")
|
|
109
|
+
def split_aav_penalty(design: Design, rule: Rule) -> RuleResult:
|
|
110
|
+
if design.cargo_bp is None or not design.delivery_vehicle:
|
|
111
|
+
return _na(rule, "no cargo_bp + delivery_vehicle")
|
|
112
|
+
v = str(design.delivery_vehicle).lower()
|
|
113
|
+
single_cap = int(rule.param.get("single_aav_cap_bp", 4700))
|
|
114
|
+
if "aav" in v and design.cargo_bp > single_cap:
|
|
115
|
+
return _result(rule, "flag", f"cargo {design.cargo_bp} bp needs split/dual AAV "
|
|
116
|
+
"(efficiency drops sharply)", value=design.cargo_bp)
|
|
117
|
+
return _result(rule, "pass", "no split-AAV efficiency penalty")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# delivery compatibility (writer output-form <-> vehicle; integration constraint, hard reject)
|
|
121
|
+
# write types that install a DNA DONOR cassette (need the donor delivered AS DNA, independent of the writer form)
|
|
122
|
+
_DNA_CASSETTE_WRITES = {"insertion", "replacement", "landing_pad", "landing_pad_install"}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@evaluator("delivery_cargo_form")
|
|
126
|
+
def delivery_cargo_form(design: Design, rule: Rule) -> RuleResult:
|
|
127
|
+
if not design.delivery_vehicle:
|
|
128
|
+
return _na(rule, "no delivery_vehicle")
|
|
129
|
+
from pen_stack.planner.delivery_vehicles import vehicle
|
|
130
|
+
veh = vehicle(design.delivery_vehicle)
|
|
131
|
+
if veh is None:
|
|
132
|
+
return _na(rule, f"unknown vehicle {design.delivery_vehicle!r}")
|
|
133
|
+
forms = veh.get("compatible_cargo_form", [])
|
|
134
|
+
form = writer_output_form(design)
|
|
135
|
+
# (1) DNA-donor-cassette install <-> vehicle form. A genomic insertion/replacement/landing-pad by a DNA-writing
|
|
136
|
+
# system (integrase / recombinase / bridge) installs a DNA donor cassette; an mRNA/RNP-only vehicle (e.g.
|
|
137
|
+
# LNP-mRNA) cannot deliver a DNA donor for STABLE integration. Fires for DNA-form OR unspecified writers (the
|
|
138
|
+
# report's gap: "insertion via LNP-mRNA" with no writer named). Skipped for an RNP editor whose RNP delivery is
|
|
139
|
+
# itself valid, that case is governed by the writer-form check below.
|
|
140
|
+
if (str(design.write_type or "").lower() in _DNA_CASSETTE_WRITES and (design.cargo_bp or 0) > 0
|
|
141
|
+
and "DNA" not in forms and form in (None, "DNA")):
|
|
142
|
+
return _result(rule, "violate", f"a genomic {design.write_type} installs a DNA donor cassette "
|
|
143
|
+
f"({design.cargo_bp} bp), but {design.delivery_vehicle} carries {forms}, mRNA is transient "
|
|
144
|
+
"and cannot deliver a DNA donor for stable integration; use a DNA-capable vehicle "
|
|
145
|
+
"(AAV / lentivirus / electroporation)")
|
|
146
|
+
# (2) writer output-form <-> vehicle. The writer machinery must itself be deliverable by the vehicle.
|
|
147
|
+
if form is None:
|
|
148
|
+
return _na(rule, "no writer output-form to check (and no DNA-cassette conflict)")
|
|
149
|
+
if form not in forms:
|
|
150
|
+
return _result(rule, "violate", f"{design.writer_family} delivers {form}, but "
|
|
151
|
+
f"{design.delivery_vehicle} carries {forms}")
|
|
152
|
+
return _result(rule, "pass", f"{form} payload compatible with {design.delivery_vehicle}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@evaluator("delivery_no_integration")
|
|
156
|
+
def delivery_no_integration(design: Design, rule: Rule) -> RuleResult:
|
|
157
|
+
if not design.delivery_vehicle:
|
|
158
|
+
return _na(rule, "no delivery_vehicle")
|
|
159
|
+
if not design.no_integration:
|
|
160
|
+
return _result(rule, "pass", "no non-integration constraint declared")
|
|
161
|
+
from pen_stack.planner.delivery_vehicles import vehicle
|
|
162
|
+
veh = vehicle(design.delivery_vehicle)
|
|
163
|
+
if veh is None:
|
|
164
|
+
return _na(rule, f"unknown vehicle {design.delivery_vehicle!r}")
|
|
165
|
+
if veh.get("integrating"):
|
|
166
|
+
return _result(rule, "violate", f"goal forbids integration but {design.delivery_vehicle} integrates")
|
|
167
|
+
return _result(rule, "pass", f"{design.delivery_vehicle} is non-integrating, as required")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@evaluator("delivery_aav_packaging")
|
|
171
|
+
def delivery_aav_packaging(design: Design, rule: Rule) -> RuleResult:
|
|
172
|
+
"""Delivery-oracle packaging-margin check: AAV packaging EFFICIENCY drops sharply as the cargo approaches the
|
|
173
|
+
capsid limit (a computable property of cargo_bp vs the vehicle capacity), even when still under capacity.
|
|
174
|
+
Soft flag when within the margin; not a titre predictor."""
|
|
175
|
+
if design.cargo_bp is None or not design.delivery_vehicle:
|
|
176
|
+
return _na(rule, "no cargo_bp + delivery_vehicle")
|
|
177
|
+
v = str(design.delivery_vehicle).lower()
|
|
178
|
+
if "aav" not in v:
|
|
179
|
+
return _result(rule, "pass", "not an AAV vehicle; packaging-margin check n/a")
|
|
180
|
+
from pen_stack.planner.delivery_vehicles import vehicle
|
|
181
|
+
veh = vehicle(design.delivery_vehicle) or {}
|
|
182
|
+
cap = veh.get("cargo_capacity_bp")
|
|
183
|
+
if not cap:
|
|
184
|
+
return _na(rule, "vehicle has no capacity")
|
|
185
|
+
margin = float(rule.param.get("margin_frac", 0.9)) # within 90-100% of capacity -> efficiency penalty
|
|
186
|
+
frac = design.cargo_bp / cap
|
|
187
|
+
if frac >= margin:
|
|
188
|
+
return _result(rule, "flag", f"cargo {design.cargo_bp} bp is {frac:.0%} of {design.delivery_vehicle} "
|
|
189
|
+
f"capacity {cap} bp (packaging efficiency / titre drops near the limit)",
|
|
190
|
+
value=round(frac, 3))
|
|
191
|
+
return _result(rule, "pass", f"cargo {frac:.0%} of capacity (comfortable packaging margin)",
|
|
192
|
+
value=round(frac, 3))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@evaluator("delivery_sequence_constraints")
|
|
196
|
+
def delivery_sequence_constraints(design: Design, rule: Rule) -> RuleResult:
|
|
197
|
+
if not design.cargo_seq or not design.delivery_vehicle:
|
|
198
|
+
return _na(rule, "no cargo_seq + delivery_vehicle")
|
|
199
|
+
from pen_stack.planner.delivery_constraints import scan_delivery
|
|
200
|
+
from pen_stack.planner.delivery_vehicles import vehicle
|
|
201
|
+
veh = vehicle(design.delivery_vehicle) or {}
|
|
202
|
+
key = veh.get("constraint_key", design.delivery_vehicle)
|
|
203
|
+
r = scan_delivery(design.cargo_seq, key)
|
|
204
|
+
if r["flags"]:
|
|
205
|
+
return _result(rule, "flag", "; ".join(f["detail"] for f in r["flags"]),
|
|
206
|
+
value=r["delivery_constraint_risk"])
|
|
207
|
+
return _result(rule, "pass", "no vehicle-specific sequence flags", value=r["delivery_constraint_risk"])
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@evaluator("delivery_immunogenicity_scope")
|
|
211
|
+
def delivery_immunogenicity_scope(design: Design, rule: Rule) -> RuleResult:
|
|
212
|
+
if not design.delivery_vehicle:
|
|
213
|
+
return _na(rule, "no delivery_vehicle")
|
|
214
|
+
return _result(rule, "scope", "immunogenicity MAGNITUDE and precise in-vivo tropism are not modeled "
|
|
215
|
+
"(declared out of scope, never predicted)")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# compliance / scope-of-use legality: heritable human germline editing is out of scope + prohibited
|
|
219
|
+
# A term in declared free text counts only if it is NOT inside a NEGATED span, so a somatic design that says
|
|
220
|
+
# "no germline transmission", "not heritable", "somatic, not germline", or "NOT for reproduction or implantation"
|
|
221
|
+
# (a distributed negation over a list) does not false-fire on the negated word. A negator opens a span that runs
|
|
222
|
+
# across list connectors (or / and / ,) until a clause break (. ; but however yet) or ~60 chars.
|
|
223
|
+
_NEGATOR = re.compile(r"\b(not|non|no|never|without|isn'?t|aren'?t|rather than|as opposed to)\b")
|
|
224
|
+
_CLAUSE_BREAK = re.compile(r"[;.]|\bbut\b|\bhowever\b|\byet\b|\bexcept\b")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _negated_spans(text: str) -> list[tuple[int, int]]:
|
|
228
|
+
spans = []
|
|
229
|
+
for m in _NEGATOR.finditer(text):
|
|
230
|
+
rest = text[m.end():m.end() + 60]
|
|
231
|
+
brk = _CLAUSE_BREAK.search(rest)
|
|
232
|
+
spans.append((m.start(), m.end() + (brk.start() if brk else len(rest))))
|
|
233
|
+
return spans
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _intent_present(term: str, text: str, spans: list[tuple[int, int]]) -> bool:
|
|
237
|
+
t = str(term or "").strip().lower()
|
|
238
|
+
if not t:
|
|
239
|
+
return False
|
|
240
|
+
idx = text.find(t)
|
|
241
|
+
while idx != -1:
|
|
242
|
+
if not any(s <= idx < e for s, e in spans):
|
|
243
|
+
return True # a non-negated occurrence
|
|
244
|
+
idx = text.find(t, idx + 1)
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@evaluator("germline_prohibition")
|
|
249
|
+
def germline_prohibition(design: Design, rule: Rule) -> RuleResult:
|
|
250
|
+
"""Hard legality reject for a heritable / germline edit. Fires on (a) a reproductive germline TARGET cell
|
|
251
|
+
(embryo/zygote/oocyte/sperm/gamete - structured, unconditional), (b) a declared REPRODUCTIVE-USE intent
|
|
252
|
+
(gametes / assisted reproduction / implantation / 'for reproduction'), (c) a declared HERITABLE intent
|
|
253
|
+
(germline / heritable / transmitted to offspring), or (d) a germline-COMPETENT cell type (hESC/iPSC) in a
|
|
254
|
+
heritable context (in vivo, or a reproductive/heritable intent). The free-text intent matches are NEGATION-
|
|
255
|
+
aware, so a SOMATIC design that explicitly says 'somatic, not germline' / 'no germline transmission' / 'not
|
|
256
|
+
heritable' is NOT false-flagged, and an ex-vivo somatic use of a germline-competent research line stays in
|
|
257
|
+
scope. The screen reads the declared design artifact only (not any free-text justification)."""
|
|
258
|
+
extra = design.model_extra or {}
|
|
259
|
+
|
|
260
|
+
def _norm(x) -> str:
|
|
261
|
+
return str(x or "").strip().lower()
|
|
262
|
+
|
|
263
|
+
text = " ".join(_norm(x) for x in (design.edit_intent, extra.get("cargo_function"),
|
|
264
|
+
extra.get("goal_function"), extra.get("function_annotation"),
|
|
265
|
+
extra.get("notes")) if x)
|
|
266
|
+
ct = _norm(design.cell_type)
|
|
267
|
+
in_vivo = bool(extra.get("in_vivo")) and _norm(extra.get("in_vivo")) not in ("false", "0", "no")
|
|
268
|
+
p = rule.param
|
|
269
|
+
|
|
270
|
+
spans = _negated_spans(text)
|
|
271
|
+
repro_ct = [c for c in p.get("germline_cell_types", []) if _norm(c) in ct]
|
|
272
|
+
competent_ct = [c for c in p.get("germline_competent_cell_types", []) if _norm(c) in ct]
|
|
273
|
+
repro_intent = [t for t in p.get("reproductive_intent_terms", []) if _intent_present(_norm(t), text, spans)]
|
|
274
|
+
heritable = [t for t in p.get("heritable_terms", []) if _intent_present(_norm(t), text, spans)]
|
|
275
|
+
|
|
276
|
+
triggers = []
|
|
277
|
+
if repro_ct:
|
|
278
|
+
triggers.append(f"a reproductive germline target cell ('{repro_ct[0]}')")
|
|
279
|
+
if repro_intent:
|
|
280
|
+
triggers.append(f"a declared reproductive-use intent ('{repro_intent[0]}')")
|
|
281
|
+
if heritable:
|
|
282
|
+
triggers.append(f"a declared heritable/germline-editing intent ('{heritable[0]}')")
|
|
283
|
+
if competent_ct and (in_vivo or repro_intent or heritable):
|
|
284
|
+
ctx = "in vivo" if in_vivo else (repro_intent or heritable)[0]
|
|
285
|
+
triggers.append(f"a germline-competent cell type ('{competent_ct[0]}') edited in a heritable context "
|
|
286
|
+
f"('{ctx}')")
|
|
287
|
+
if triggers:
|
|
288
|
+
# de-duplicate while preserving order, then report
|
|
289
|
+
seen, uniq = set(), []
|
|
290
|
+
for t in triggers:
|
|
291
|
+
if t not in seen:
|
|
292
|
+
seen.add(t)
|
|
293
|
+
uniq.append(t)
|
|
294
|
+
return _result(rule, "violate",
|
|
295
|
+
"heritable human germline editing is out of scope for this somatic tool and is broadly "
|
|
296
|
+
"prohibited (international moratorium): " + "; ".join(uniq)
|
|
297
|
+
+ ". Restrict to somatic editing (ex vivo, or a somatic cell type / somatic context).")
|
|
298
|
+
note = ""
|
|
299
|
+
if competent_ct:
|
|
300
|
+
note = (f" (cell type '{competent_ct[0]}' is germline-competent; somatic/ex-vivo editing of it is in "
|
|
301
|
+
"scope, but an in-vivo or reproductive heritable edit would not be)")
|
|
302
|
+
return _result(rule, "pass", "no heritable/germline-editing intent or germline target declared" + note)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# multiplex translocation (delegates to planner.multiplex, soft penalty)
|
|
306
|
+
@evaluator("multiplex_translocation")
|
|
307
|
+
def multiplex_translocation(design: Design, rule: Rule) -> RuleResult:
|
|
308
|
+
if not design.edits or len(design.edits) < 2:
|
|
309
|
+
return _na(rule, "fewer than 2 edits")
|
|
310
|
+
from pen_stack.planner.multiplex import translocation_risk
|
|
311
|
+
r = translocation_risk(design.edits)
|
|
312
|
+
risk = r.get("translocation_risk", 0.0)
|
|
313
|
+
thr = float(rule.param.get("risk_threshold", 0.2))
|
|
314
|
+
if risk >= thr:
|
|
315
|
+
return _result(rule, "flag", f"pairwise translocation risk {risk} >= {thr} "
|
|
316
|
+
"(concurrent DSBs may mis-join)", value=risk)
|
|
317
|
+
return _result(rule, "pass", f"translocation risk {risk} < {thr} "
|
|
318
|
+
"(DSB-free writers carry ~zero by construction)", value=risk)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Load the rule base from configs/rules/*.yaml into a validated Ruleset."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from pen_stack._resources import resource
|
|
10
|
+
from pen_stack.rules.schema import Rule, Ruleset
|
|
11
|
+
|
|
12
|
+
RULES_VERSION = "1.0"
|
|
13
|
+
_CATEGORIES = ("reachability", "fold", "payload", "multiplex", "delivery", "compliance")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@lru_cache(maxsize=1)
|
|
17
|
+
def load_ruleset(rules_dir: str | None = None) -> Ruleset:
|
|
18
|
+
base = Path(rules_dir) if rules_dir else resource("configs/rules")
|
|
19
|
+
rules: list[Rule] = []
|
|
20
|
+
for cat in _CATEGORIES:
|
|
21
|
+
f = base / f"{cat}.yaml"
|
|
22
|
+
if not f.exists():
|
|
23
|
+
continue
|
|
24
|
+
data = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
|
|
25
|
+
for rec in data.get("rules", []):
|
|
26
|
+
rules.append(Rule(**rec))
|
|
27
|
+
ids = [r.id for r in rules]
|
|
28
|
+
dupes = {i for i in ids if ids.count(i) > 1}
|
|
29
|
+
if dupes:
|
|
30
|
+
raise ValueError(f"duplicate rule ids: {sorted(dupes)}")
|
|
31
|
+
return Ruleset(version=RULES_VERSION, rules=rules)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Rule / Design / Verdict schema, the laws of genome writing as data.
|
|
2
|
+
|
|
3
|
+
A *rule is data, not code* (Principle 1): every constraint is a versioned record with an id, a kind
|
|
4
|
+
(`hard_reject` | `soft_penalty` | `scope_flag`), the mechanism it encodes, a controlling parameter, a
|
|
5
|
+
provenance/citation, a test reference, and the name of the registered *evaluator* that executes it. Code
|
|
6
|
+
executes rules; it does not contain them. The evaluators (``pen_stack/rules/evaluators.py``) wrap the
|
|
7
|
+
EXISTING validated functions (target_site, fold_qc, delivery, multiplex), so relocation changes nothing about
|
|
8
|
+
the decisions, only makes them enumerable, queryable, and citation-backed (proven by the parity test).
|
|
9
|
+
|
|
10
|
+
Legality and confidence are different axes (Principle 2): ``legal`` = every applicable hard rule passes;
|
|
11
|
+
confidence is the calibrated trust attached separately by the verifier. Never collapse.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Literal
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
18
|
+
|
|
19
|
+
RuleKind = Literal["hard_reject", "soft_penalty", "scope_flag"]
|
|
20
|
+
RuleStatus = Literal["pass", "violate", "flag", "scope", "not_applicable"]
|
|
21
|
+
RuleCategory = Literal["reachability", "fold", "payload", "multiplex", "delivery", "compliance"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Rule(BaseModel):
|
|
25
|
+
"""A single law of genome writing, expressed as data. Frozen: the pinned, citation-backed rules are
|
|
26
|
+
immutable once loaded, so a law cannot be silently edited at runtime."""
|
|
27
|
+
model_config = ConfigDict(frozen=True)
|
|
28
|
+
|
|
29
|
+
id: str
|
|
30
|
+
kind: RuleKind
|
|
31
|
+
category: RuleCategory
|
|
32
|
+
mechanism: str # one-line statement of what physical fact it encodes
|
|
33
|
+
evaluator: str # registered evaluator fn name (rules/evaluators.py)
|
|
34
|
+
param: dict[str, Any] = Field(default_factory=dict)
|
|
35
|
+
provenance: dict[str, Any] = Field(default_factory=dict) # {doi: [...], note: ...}
|
|
36
|
+
test_ref: str | None = None
|
|
37
|
+
scope: str | None = None # limit (e.g. "screen, not activity guarantee")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Ruleset(BaseModel):
|
|
41
|
+
version: str
|
|
42
|
+
rules: list[Rule]
|
|
43
|
+
|
|
44
|
+
def by_category(self, category: str) -> list[Rule]:
|
|
45
|
+
return [r for r in self.rules if r.category == category]
|
|
46
|
+
|
|
47
|
+
def get(self, rule_id: str) -> Rule | None:
|
|
48
|
+
return next((r for r in self.rules if r.id == rule_id), None)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Design(BaseModel):
|
|
52
|
+
"""A proposed genomic write submitted for verification. Permissive: evaluators that lack their inputs
|
|
53
|
+
return ``not_applicable`` rather than failing, so a partial design is still checkable.
|
|
54
|
+
|
|
55
|
+
Biosecurity screening. The safety gate reads hazard content ONLY from these declared fields:
|
|
56
|
+
``cargo_function``, ``function_annotation``, ``goal_function``, ``source_taxon``, ``organism``,
|
|
57
|
+
``host_taxon``, ``cargo_seq``, ``cargo_sequence`` (text) and ``function_tags``, ``pfam_domains``,
|
|
58
|
+
``annotations`` (lists). Of these, only ``cargo_seq`` is a formal field here; the rest are passed as extra
|
|
59
|
+
keys (``model_config`` allows extra) and are screened all the same. Content placed in ``edit_intent`` or
|
|
60
|
+
any other free-text/framing field is deliberately NOT screened, framing is stripped before screening so a
|
|
61
|
+
hazardous design cannot be re-labelled "defensive" to pass. Consequently a verdict with
|
|
62
|
+
``declared_signal=False`` means "no screenable content was declared, so nothing was screened", which is NOT
|
|
63
|
+
the same as "screened and found safe". Put hazard-relevant content in the fields above."""
|
|
64
|
+
write_type: str = "insertion"
|
|
65
|
+
gene: str | None = None
|
|
66
|
+
chrom: str | None = None
|
|
67
|
+
site_seq: str | None = None # sequence window at the target site (reachability)
|
|
68
|
+
writer_family: str | None = None
|
|
69
|
+
writer_output_form: Literal["DNA", "mRNA", "RNP"] | None = None
|
|
70
|
+
installed_att: bool = False # a pre-installed landing pad declared
|
|
71
|
+
cargo_bp: int | None = None
|
|
72
|
+
cargo_seq: str | None = None
|
|
73
|
+
delivery_vehicle: str | None = None
|
|
74
|
+
cell_type: str | None = None
|
|
75
|
+
edit_intent: str | None = None
|
|
76
|
+
no_integration: bool = False # goal forbids genomic integration
|
|
77
|
+
target_guide: str | None = None # bridge-RNA target-binding loop (fold)
|
|
78
|
+
donor_guide: str | None = None # bridge-RNA donor-binding loop (fold)
|
|
79
|
+
edits: list[dict] = Field(default_factory=list) # multi-edit plan (multiplex)
|
|
80
|
+
|
|
81
|
+
model_config = {"extra": "allow"} # forward-compatible with new dimensions
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RuleResult(BaseModel):
|
|
85
|
+
"""Frozen: a rule's outcome cannot be edited after evaluation, so `status`/`kind` (and therefore
|
|
86
|
+
`is_blocking`) cannot be flipped to turn a violation into a pass downstream."""
|
|
87
|
+
model_config = ConfigDict(frozen=True)
|
|
88
|
+
|
|
89
|
+
rule_id: str
|
|
90
|
+
kind: RuleKind
|
|
91
|
+
category: RuleCategory
|
|
92
|
+
status: RuleStatus
|
|
93
|
+
reason: str
|
|
94
|
+
citation: list[str] = Field(default_factory=list)
|
|
95
|
+
value: Any = None # optional numeric (penalty/score), provenance-tagged
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def is_blocking(self) -> bool:
|
|
99
|
+
return self.kind == "hard_reject" and self.status == "violate"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""The rule solver, evaluate a design against the rule base.
|
|
2
|
+
|
|
3
|
+
``evaluate(design, ruleset) -> [RuleResult]`` runs each rule's registered evaluator, deterministically and
|
|
4
|
+
ordered, returning a per-rule verdict with reason + citation. ``legal = every APPLICABLE hard_reject rule
|
|
5
|
+
passes``, a not-applicable hard rule (its inputs absent) never blocks. Legality and confidence are kept
|
|
6
|
+
separate: this module decides *legality* only; the verifier attaches calibrated confidence on top.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pen_stack.rules.evaluators import get_evaluator
|
|
11
|
+
from pen_stack.rules.loader import load_ruleset
|
|
12
|
+
from pen_stack.rules.schema import Design, RuleResult, Ruleset
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def evaluate(design: Design, ruleset: Ruleset | None = None) -> list[RuleResult]:
|
|
16
|
+
rs = ruleset or load_ruleset()
|
|
17
|
+
results: list[RuleResult] = []
|
|
18
|
+
for rule in rs.rules:
|
|
19
|
+
fn = get_evaluator(rule.evaluator)
|
|
20
|
+
results.append(fn(design, rule))
|
|
21
|
+
return results
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_legal(results: list[RuleResult]) -> bool:
|
|
25
|
+
"""Legal = no applicable hard_reject rule is violated."""
|
|
26
|
+
return not any(r.is_blocking for r in results)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def legality_report(design: Design, ruleset: Ruleset | None = None) -> dict:
|
|
30
|
+
"""Structured legality summary: legal bool + violations (named, with reason+citation) + flags + scope."""
|
|
31
|
+
results = evaluate(design, ruleset)
|
|
32
|
+
violations = [r for r in results if r.is_blocking]
|
|
33
|
+
flags = [r for r in results if r.kind == "soft_penalty" and r.status == "flag"]
|
|
34
|
+
scope = [r for r in results if r.kind == "scope_flag" and r.status == "scope"]
|
|
35
|
+
return {
|
|
36
|
+
"legal": is_legal(results),
|
|
37
|
+
"n_rules_evaluated": len(results),
|
|
38
|
+
"n_applicable": sum(1 for r in results if r.status != "not_applicable"),
|
|
39
|
+
"violations": [{"rule_id": r.rule_id, "reason": r.reason, "citation": r.citation} for r in violations],
|
|
40
|
+
"soft_flags": [{"rule_id": r.rule_id, "reason": r.reason, "value": r.value} for r in flags],
|
|
41
|
+
"scope_flags": [{"rule_id": r.rule_id, "reason": r.reason} for r in scope],
|
|
42
|
+
"rule_results": [r.model_dump() for r in results],
|
|
43
|
+
}
|
pen_stack/rules/spec.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Published, machine-readable rule spec.
|
|
2
|
+
|
|
3
|
+
Exports the genome-writing rule base (``configs/rules/*.yaml``) as a single citable, machine-readable document:
|
|
4
|
+
every rule with its id, kind, category, mechanism, the named evaluator that executes it, its parameters, its
|
|
5
|
+
provenance (DOI or note), a test reference, and its scope limit. The spec is a faithful export of the live
|
|
6
|
+
ruleset, not a second copy of the logic: ``spec_parity`` proves the exported records round-trip to the exact
|
|
7
|
+
``Ruleset`` the solver loads (0 mismatches), every rule names a registered evaluator (so the spec is
|
|
8
|
+
executable), and every rule carries a DOI or an explicit note. This is what makes the rules externally
|
|
9
|
+
readable and citable without changing any decision.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from pen_stack.rules.evaluators import registered_evaluators
|
|
17
|
+
from pen_stack.rules.loader import RULES_VERSION, load_ruleset
|
|
18
|
+
from pen_stack.rules.schema import Rule
|
|
19
|
+
|
|
20
|
+
_FIELDS = ("id", "kind", "category", "mechanism", "evaluator", "param", "provenance", "test_ref", "scope")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def export_spec() -> dict[str, Any]:
|
|
24
|
+
"""Return the machine-readable rule spec: a version, the category list, and one record per rule with its
|
|
25
|
+
full provenance, the executing evaluator, and whether it carries a citation."""
|
|
26
|
+
rs = load_ruleset()
|
|
27
|
+
registered = registered_evaluators()
|
|
28
|
+
rules: list[dict[str, Any]] = []
|
|
29
|
+
for r in rs.rules:
|
|
30
|
+
prov = r.provenance or {}
|
|
31
|
+
doi = list(prov.get("doi", []) or [])
|
|
32
|
+
rules.append({
|
|
33
|
+
"id": r.id, "kind": r.kind, "category": r.category, "mechanism": r.mechanism,
|
|
34
|
+
"evaluator": r.evaluator, "param": r.param, "provenance": prov,
|
|
35
|
+
"citation": doi, "note": prov.get("note"), "test_ref": r.test_ref, "scope": r.scope,
|
|
36
|
+
"evaluator_registered": r.evaluator in registered,
|
|
37
|
+
"has_citation": bool(doi or prov.get("note")),
|
|
38
|
+
})
|
|
39
|
+
return {
|
|
40
|
+
"spec": "PEN-STACK genome-writing rule base",
|
|
41
|
+
"version": rs.version,
|
|
42
|
+
"n_rules": len(rules),
|
|
43
|
+
"categories": sorted({r["category"] for r in rules}),
|
|
44
|
+
"kinds": sorted({r["kind"] for r in rules}),
|
|
45
|
+
"note": "A rule is data, not code: each record names the evaluator that executes it against a Design. "
|
|
46
|
+
"Relocating the rules into this spec changes no decision (proven by the parity tests).",
|
|
47
|
+
"rules": rules,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def spec_parity() -> dict[str, Any]:
|
|
52
|
+
"""The exported spec faithfully reproduces the live ruleset. Checks (a) every exported record
|
|
53
|
+
round-trips to the identical ``Rule`` the solver loaded (0 mismatches), (b) every rule names a registered
|
|
54
|
+
evaluator, and (c) every rule carries a DOI or a note."""
|
|
55
|
+
spec = export_spec()
|
|
56
|
+
live = load_ruleset().rules
|
|
57
|
+
reload = [Rule(**{k: rec[k] for k in _FIELDS}) for rec in spec["rules"]]
|
|
58
|
+
mismatches = sorted({a.id for a, b in zip(live, reload, strict=True) if a != b}
|
|
59
|
+
| ({r.id for r in live} ^ {r.id for r in reload}))
|
|
60
|
+
return {
|
|
61
|
+
"n_rules": spec["n_rules"],
|
|
62
|
+
"round_trip_mismatches": mismatches,
|
|
63
|
+
"parity_0_mismatch": len(mismatches) == 0,
|
|
64
|
+
"all_evaluators_registered": all(r["evaluator_registered"] for r in spec["rules"]),
|
|
65
|
+
"uncited_rules": [r["id"] for r in spec["rules"] if not r["has_citation"]],
|
|
66
|
+
"all_rules_cited": all(r["has_citation"] for r in spec["rules"]),
|
|
67
|
+
"rules_version": RULES_VERSION,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def write_spec(path: str = "benchmarks/verify/rule_spec.json") -> str:
|
|
72
|
+
"""Write the machine-readable spec to ``path`` (committed artifact). Returns the path."""
|
|
73
|
+
import os
|
|
74
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
75
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
76
|
+
json.dump(export_spec(), fh, indent=2)
|
|
77
|
+
fh.write("\n")
|
|
78
|
+
return path
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""pen_stack.safety, the Guardian.
|
|
2
|
+
|
|
3
|
+
A defensive biosecurity / dual-use screening gate that every design submitted to `verify()` passes before it
|
|
4
|
+
is evaluated, scored, generated, or exported. It answers a question orthogonal to the immunology subsystem:
|
|
5
|
+
not "will the patient react?" but "is this design itself hazardous / dual-use?". A design that
|
|
6
|
+
matches a select-agent, pandemic-pathogen, or controlled-toxin signature is refused or escalated, by
|
|
7
|
+
construction; legitimate therapeutic designs pass untouched. It is a safeguard, not a guarantee, and not a
|
|
8
|
+
substitute for institutional biosafety review.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pen_stack.safety.audit import audit_log, verify_chain
|
|
13
|
+
from pen_stack.safety.gate import safety_gate
|
|
14
|
+
from pen_stack.safety.policy import SafetyVerdict, decide
|
|
15
|
+
from pen_stack.safety.registry import HazardRegistry
|
|
16
|
+
from pen_stack.safety.screen import ScreenHit, screen_design
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"safety_gate", "SafetyVerdict", "screen_design", "ScreenHit", "decide",
|
|
20
|
+
"HazardRegistry", "audit_log", "verify_chain",
|
|
21
|
+
]
|