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.
Files changed (259) hide show
  1. pen_stack/__init__.py +2 -0
  2. pen_stack/_resources.py +34 -0
  3. pen_stack/active/__init__.py +20 -0
  4. pen_stack/active/acquire.py +165 -0
  5. pen_stack/active/brains.py +74 -0
  6. pen_stack/active/campaign.py +109 -0
  7. pen_stack/active/design.py +66 -0
  8. pen_stack/active/validate.py +104 -0
  9. pen_stack/adapt/__init__.py +14 -0
  10. pen_stack/adapt/finetune.py +33 -0
  11. pen_stack/adapt/ingest.py +86 -0
  12. pen_stack/adapt/pipeline.py +101 -0
  13. pen_stack/adapt/recalibrate.py +58 -0
  14. pen_stack/adapt/report.py +130 -0
  15. pen_stack/agent/__init__.py +1 -0
  16. pen_stack/agent/cite.py +175 -0
  17. pen_stack/agent/co_scientist.py +262 -0
  18. pen_stack/agent/epistemic.py +102 -0
  19. pen_stack/agent/guardrails.py +67 -0
  20. pen_stack/agent/mcp_server.py +215 -0
  21. pen_stack/agent/orchestrator.py +112 -0
  22. pen_stack/agent/orchestrator_live.py +56 -0
  23. pen_stack/agent/pen_agent.py +242 -0
  24. pen_stack/agent/scope.py +60 -0
  25. pen_stack/agent/tools.py +130 -0
  26. pen_stack/api/__init__.py +12 -0
  27. pen_stack/api/manifest.py +160 -0
  28. pen_stack/atlas/__init__.py +1 -0
  29. pen_stack/atlas/atlas.parquet +0 -0
  30. pen_stack/atlas/build_wtkb.py +80 -0
  31. pen_stack/atlas/crosslink.py +179 -0
  32. pen_stack/atlas/expand.py +190 -0
  33. pen_stack/atlas/guide_design.py +178 -0
  34. pen_stack/atlas/schema.py +59 -0
  35. pen_stack/atlas/scorecard.py +134 -0
  36. pen_stack/atlas/scorecard_v3.parquet +0 -0
  37. pen_stack/atlas/universe.py +75 -0
  38. pen_stack/atlas/universe_v3.parquet +0 -0
  39. pen_stack/atlas/variant_propose.py +155 -0
  40. pen_stack/atlas/writer_efficiency.py +184 -0
  41. pen_stack/atlas/writer_predict.py +229 -0
  42. pen_stack/atlas/writer_recommend.py +170 -0
  43. pen_stack/atlas/writer_verify.py +167 -0
  44. pen_stack/atlas/wtkb.parquet +0 -0
  45. pen_stack/bridge/__init__.py +1 -0
  46. pen_stack/bridge/activity.py +52 -0
  47. pen_stack/bridge/cli.py +65 -0
  48. pen_stack/bridge/fold_qc.py +53 -0
  49. pen_stack/bridge/guide_qc.py +87 -0
  50. pen_stack/bridge/ingest.py +139 -0
  51. pen_stack/bridge/offtarget.py +191 -0
  52. pen_stack/bridge/offtarget_energetics.py +105 -0
  53. pen_stack/bridge/ortholog_screen.py +73 -0
  54. pen_stack/bridge/pipeline.py +83 -0
  55. pen_stack/build/__init__.py +16 -0
  56. pen_stack/build/cloudlab.py +74 -0
  57. pen_stack/build/ingest.py +47 -0
  58. pen_stack/build/protocol.py +82 -0
  59. pen_stack/build/simlab.py +30 -0
  60. pen_stack/cli.py +126 -0
  61. pen_stack/data/__init__.py +1 -0
  62. pen_stack/data/encode.py +84 -0
  63. pen_stack/data/genome.py +71 -0
  64. pen_stack/data/ingest_chromatin.py +119 -0
  65. pen_stack/data/ingest_integration.py +112 -0
  66. pen_stack/data/ingest_safety_annot.py +201 -0
  67. pen_stack/data/ingest_trip.py +76 -0
  68. pen_stack/design/__init__.py +14 -0
  69. pen_stack/design/capsid_generate.py +62 -0
  70. pen_stack/design/generate.py +70 -0
  71. pen_stack/design/pareto.py +70 -0
  72. pen_stack/design/space.py +137 -0
  73. pen_stack/design/writer_variants.py +121 -0
  74. pen_stack/env/__init__.py +1 -0
  75. pen_stack/env/genome_writing_env.py +248 -0
  76. pen_stack/env/policies.py +94 -0
  77. pen_stack/graph/__init__.py +21 -0
  78. pen_stack/graph/build.py +133 -0
  79. pen_stack/graph/cell_types.py +58 -0
  80. pen_stack/graph/ingest.py +132 -0
  81. pen_stack/graph/query.py +148 -0
  82. pen_stack/graph/schema.py +100 -0
  83. pen_stack/loop/__init__.py +15 -0
  84. pen_stack/loop/continual.py +61 -0
  85. pen_stack/loop/cycle.py +84 -0
  86. pen_stack/loop/drift.py +41 -0
  87. pen_stack/mech/__init__.py +1 -0
  88. pen_stack/mech/classify_atlas.py +71 -0
  89. pen_stack/mech/pfam_whitelist.yaml +247 -0
  90. pen_stack/mech/whitelist.py +66 -0
  91. pen_stack/monitor/__init__.py +1 -0
  92. pen_stack/monitor/europepmc.py +32 -0
  93. pen_stack/monitor/run.py +57 -0
  94. pen_stack/monitor/triage.py +63 -0
  95. pen_stack/oracles/__init__.py +65 -0
  96. pen_stack/oracles/affinity.py +116 -0
  97. pen_stack/oracles/cache.py +53 -0
  98. pen_stack/oracles/energetics.py +33 -0
  99. pen_stack/oracles/genome.py +167 -0
  100. pen_stack/oracles/protein_design.py +136 -0
  101. pen_stack/oracles/reliability.py +64 -0
  102. pen_stack/oracles/rna.py +28 -0
  103. pen_stack/oracles/schema.py +77 -0
  104. pen_stack/oracles/status.py +123 -0
  105. pen_stack/oracles/structure.py +42 -0
  106. pen_stack/oracles/structure_run.py +76 -0
  107. pen_stack/oracles/vcell.py +74 -0
  108. pen_stack/planner/__init__.py +1 -0
  109. pen_stack/planner/ada_risk.py +64 -0
  110. pen_stack/planner/antipeg_oracle.py +75 -0
  111. pen_stack/planner/capsid_epitope_oracle.py +135 -0
  112. pen_stack/planner/cargo.py +56 -0
  113. pen_stack/planner/cargo_polish.py +146 -0
  114. pen_stack/planner/chromosome.py +106 -0
  115. pen_stack/planner/delivery.py +55 -0
  116. pen_stack/planner/delivery_constraints.py +110 -0
  117. pen_stack/planner/delivery_immune.py +61 -0
  118. pen_stack/planner/delivery_immunology.py +222 -0
  119. pen_stack/planner/delivery_predict.py +196 -0
  120. pen_stack/planner/delivery_vehicles.py +37 -0
  121. pen_stack/planner/genotoxicity_oracle.py +112 -0
  122. pen_stack/planner/immune_mhc2.py +154 -0
  123. pen_stack/planner/immune_profile.py +292 -0
  124. pen_stack/planner/innate_sensing.py +135 -0
  125. pen_stack/planner/multiplex.py +110 -0
  126. pen_stack/planner/optimize.py +278 -0
  127. pen_stack/planner/pipeline.py +87 -0
  128. pen_stack/planner/report.py +26 -0
  129. pen_stack/planner/router.py +57 -0
  130. pen_stack/planner/seroprevalence_oracle.py +92 -0
  131. pen_stack/planner/target_site.py +118 -0
  132. pen_stack/rag/__init__.py +1 -0
  133. pen_stack/rag/corpus.py +133 -0
  134. pen_stack/rag/embed.py +98 -0
  135. pen_stack/rag/ground.py +131 -0
  136. pen_stack/rag/index.py +53 -0
  137. pen_stack/rag/llm.py +215 -0
  138. pen_stack/rag/qa.py +105 -0
  139. pen_stack/rag/retrieve.py +48 -0
  140. pen_stack/rules/__init__.py +9 -0
  141. pen_stack/rules/evaluators.py +318 -0
  142. pen_stack/rules/loader.py +31 -0
  143. pen_stack/rules/schema.py +99 -0
  144. pen_stack/rules/solver.py +43 -0
  145. pen_stack/rules/spec.py +78 -0
  146. pen_stack/safety/__init__.py +21 -0
  147. pen_stack/safety/audit.py +90 -0
  148. pen_stack/safety/gate.py +58 -0
  149. pen_stack/safety/pfam_scan.py +157 -0
  150. pen_stack/safety/policy.py +69 -0
  151. pen_stack/safety/redteam.py +71 -0
  152. pen_stack/safety/registry.py +255 -0
  153. pen_stack/safety/screen.py +59 -0
  154. pen_stack/safety/standards.py +141 -0
  155. pen_stack/score/__init__.py +1 -0
  156. pen_stack/score/recalibrate.py +77 -0
  157. pen_stack/score/therapeutic.py +85 -0
  158. pen_stack/server/__init__.py +1 -0
  159. pen_stack/server/api.py +647 -0
  160. pen_stack/spec/__init__.py +18 -0
  161. pen_stack/spec/clarify.py +42 -0
  162. pen_stack/spec/extract.py +406 -0
  163. pen_stack/spec/resolvers/__init__.py +17 -0
  164. pen_stack/spec/resolvers/cell.py +51 -0
  165. pen_stack/spec/resolvers/chem.py +32 -0
  166. pen_stack/spec/resolvers/feature.py +36 -0
  167. pen_stack/spec/resolvers/gene.py +43 -0
  168. pen_stack/spec/resolvers/locus.py +29 -0
  169. pen_stack/spec/resolvers/phenotype.py +37 -0
  170. pen_stack/spec/satisfy.py +114 -0
  171. pen_stack/spec/service.py +33 -0
  172. pen_stack/spec/writespec.py +252 -0
  173. pen_stack/twin/__init__.py +14 -0
  174. pen_stack/twin/calibrate.py +61 -0
  175. pen_stack/twin/data/__init__.py +12 -0
  176. pen_stack/twin/data/position_effect.py +245 -0
  177. pen_stack/twin/mechanistic.py +147 -0
  178. pen_stack/twin/outcome.py +132 -0
  179. pen_stack/twin/position_effect.py +454 -0
  180. pen_stack/ui/__init__.py +1 -0
  181. pen_stack/ui/app.py +713 -0
  182. pen_stack/validate/__init__.py +1 -0
  183. pen_stack/validate/adapt_demo.py +69 -0
  184. pen_stack/validate/agent_eval.py +117 -0
  185. pen_stack/validate/bench_adversarial_tasks.py +118 -0
  186. pen_stack/validate/bench_coscientist_tasks.py +60 -0
  187. pen_stack/validate/bench_graph_tasks.py +64 -0
  188. pen_stack/validate/bench_rule_tasks.py +84 -0
  189. pen_stack/validate/bench_trust_tasks.py +92 -0
  190. pen_stack/validate/bench_writetype_tasks.py +101 -0
  191. pen_stack/validate/blind_gsh_discovery.py +261 -0
  192. pen_stack/validate/cargo_directionality.py +57 -0
  193. pen_stack/validate/closed_loop.py +63 -0
  194. pen_stack/validate/durability_baselines.py +185 -0
  195. pen_stack/validate/experiment_design.py +65 -0
  196. pen_stack/validate/expr_controls.py +39 -0
  197. pen_stack/validate/forward_hypotheses.py +104 -0
  198. pen_stack/validate/generative_design.py +62 -0
  199. pen_stack/validate/guide_qc_demo.py +69 -0
  200. pen_stack/validate/heldout_celltype_expr.py +32 -0
  201. pen_stack/validate/immune_calibration.py +133 -0
  202. pen_stack/validate/intent_specification.py +82 -0
  203. pen_stack/validate/known_biology_expr.py +38 -0
  204. pen_stack/validate/offtarget_energetics_eval.py +144 -0
  205. pen_stack/validate/out_of_scope_refusal.py +82 -0
  206. pen_stack/validate/outcome_calibration.py +194 -0
  207. pen_stack/validate/outcome_prediction.py +76 -0
  208. pen_stack/validate/paper3_benchmark.py +165 -0
  209. pen_stack/validate/paper4_real_validation.py +144 -0
  210. pen_stack/validate/paper4_validation.py +82 -0
  211. pen_stack/validate/protocol_safety.py +62 -0
  212. pen_stack/validate/safety_screening.py +72 -0
  213. pen_stack/validate/selective_prediction.py +104 -0
  214. pen_stack/validate/seq_vs_measured.py +134 -0
  215. pen_stack/validate/target_site_controls.py +65 -0
  216. pen_stack/validate/uncertainty_eval.py +244 -0
  217. pen_stack/validate/ungrounded_baseline.py +234 -0
  218. pen_stack/validate/within_locus_ranking.py +84 -0
  219. pen_stack/validate/writer_recovery.py +91 -0
  220. pen_stack/verify/__init__.py +5 -0
  221. pen_stack/verify/proof.py +206 -0
  222. pen_stack/verify/schema.py +53 -0
  223. pen_stack/verify/service.py +191 -0
  224. pen_stack/web/__init__.py +18 -0
  225. pen_stack/web/guide.py +110 -0
  226. pen_stack/web/llm.py +393 -0
  227. pen_stack/web/llm_provider.py +119 -0
  228. pen_stack/web/router.py +119 -0
  229. pen_stack/web/server.py +96 -0
  230. pen_stack/web/tools.py +197 -0
  231. pen_stack/wgenome/__init__.py +1 -0
  232. pen_stack/wgenome/chromatin_seq.py +83 -0
  233. pen_stack/wgenome/durability.py +108 -0
  234. pen_stack/wgenome/export_tracks.py +52 -0
  235. pen_stack/wgenome/features.py +82 -0
  236. pen_stack/wgenome/genotoxic_blocklist.py +88 -0
  237. pen_stack/wgenome/gsh_baseline.py +154 -0
  238. pen_stack/wgenome/mesh_features.py +61 -0
  239. pen_stack/wgenome/offtarget_assay.py +80 -0
  240. pen_stack/wgenome/offtarget_bridge.py +47 -0
  241. pen_stack/wgenome/offtarget_cast.py +97 -0
  242. pen_stack/wgenome/offtarget_data.py +148 -0
  243. pen_stack/wgenome/offtarget_enumerate.py +274 -0
  244. pen_stack/wgenome/offtarget_integrase.py +155 -0
  245. pen_stack/wgenome/offtarget_nuclease.py +123 -0
  246. pen_stack/wgenome/offtarget_paste.py +41 -0
  247. pen_stack/wgenome/offtarget_predict.py +282 -0
  248. pen_stack/wgenome/ood.py +135 -0
  249. pen_stack/wgenome/providers.py +278 -0
  250. pen_stack/wgenome/safety.py +69 -0
  251. pen_stack/wgenome/structure3d.py +212 -0
  252. pen_stack/wgenome/uncertainty.py +250 -0
  253. pen_stack/wgenome/writability.py +72 -0
  254. pen_stack-0.1.0.dist-info/METADATA +401 -0
  255. pen_stack-0.1.0.dist-info/RECORD +259 -0
  256. pen_stack-0.1.0.dist-info/WHEEL +5 -0
  257. pen_stack-0.1.0.dist-info/entry_points.txt +3 -0
  258. pen_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
  259. pen_stack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,647 @@
1
+ """PEN-STACK REST API - atlas + cross-link endpoints over FastAPI.
2
+
3
+ Extends the base atlas with the Writer Atlas and the writer<->locus cross-link. Every quantitative
4
+ result is computed by the validated library functions (never guessed); the ``/ask`` route defers numeric
5
+ claims to those tools. Heavy data is loaded lazily so the app boots without the base atlas.
6
+
7
+ Run: ``uvicorn pen_stack.server.api:app --host 0.0.0.0 --port 8000`` (needs the ``server`` extra).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+
14
+ import pandas as pd
15
+
16
+ try:
17
+ from fastapi import FastAPI, HTTPException, Query, Request
18
+ except ImportError as e: # pragma: no cover - server extra optional
19
+ raise ImportError("FastAPI not installed: pip install 'pen-stack[server]'") from e
20
+
21
+ from pen_stack import __version__
22
+
23
+ _ATLAS = Path(__file__).resolve().parents[1] / "atlas" / "atlas.parquet"
24
+
25
+ app = FastAPI(title="PEN-STACK API", version=__version__,
26
+ description="Open infrastructure for genome writing: Writer Atlas + Writable Genome cross-link.")
27
+
28
+ _DISCLAIMER = ("Decision-support only - predictions are calibrated risk/durability estimates, not "
29
+ "clinical directives. Tier-2/3 reachability is candidate and requires experimental validation.")
30
+
31
+
32
+ def _resolve_ct(request: Request, ct: str, cell_type: str | None, allowed_params: set[str]) -> str:
33
+ """Resolve the cell type from `ct` OR its `cell_type` alias, and REJECT any unrecognized query parameter.
34
+
35
+ Without this, a misnamed cell-type argument (e.g. `?cell_type=hspc` when the endpoint reads `ct`) is silently
36
+ ignored by FastAPI and `ct` falls back to its "k562" default, so the caller gets confidently-wrong K562 data
37
+ labelled coverage=full. We instead accept both names and 422 on any unknown parameter, so the fallback can
38
+ never happen silently. The resolved cell type is validated against the known universe.
39
+ """
40
+ extra = set(request.query_params) - allowed_params
41
+ if extra:
42
+ raise HTTPException(422, f"unknown query parameter(s) {sorted(extra)}; for the cell type use 'ct' or its "
43
+ f"alias 'cell_type'. Valid parameters: {sorted(allowed_params)}.")
44
+ resolved = (cell_type or ct or "k562").lower()
45
+ known = {c["id"] for c in _CELLTYPES}
46
+ if resolved not in known:
47
+ raise HTTPException(422, f"unknown cell type '{resolved}'; valid cell types: {sorted(known)} "
48
+ f"(only k562/hepg2/hspc have a measured atlas; the others are a data-gated roadmap).")
49
+ return resolved
50
+
51
+
52
+ def _atlas_df() -> pd.DataFrame:
53
+ if not _ATLAS.exists():
54
+ raise HTTPException(503, "atlas.parquet not built")
55
+ return pd.read_parquet(_ATLAS)
56
+
57
+
58
+ def _records(df: pd.DataFrame) -> list[dict]:
59
+ """JSON-safe records from a DataFrame: NaN/inf -> null and numpy scalars -> native (pandas `to_json`
60
+ handles both). Raw `to_dict('records')` leaks non-finite floats, which the JSON encoder rejects (500)."""
61
+ return json.loads(df.to_json(orient="records"))
62
+
63
+
64
+ @app.get("/health")
65
+ def health():
66
+ """Liveness + a DATA-PRESENCE check. `atlas_present` is the writer atlas (bundled); `writability_atlas`
67
+ reports the per-cell-type writable-genome atlases that Site Finder / the cell-type dropdown depend on and
68
+ that are provided at runtime (mounted), NOT bundled. Reporting only the writer atlas here once masked a
69
+ deploy that dropped the writability-atlas mount: health stayed green while Site Finder was 100% broken and
70
+ every cell type read 'no atlas'. Surfacing measured_count makes that failure visible (0 = misconfigured)."""
71
+ from pen_stack.atlas.crosslink import writability_path
72
+ measured = []
73
+ for ct in _CELLTYPES:
74
+ try:
75
+ writability_path(ct["id"])
76
+ measured.append(ct["id"])
77
+ except Exception: # noqa: BLE001 - absent atlas is the very thing we are reporting
78
+ pass
79
+ return {"status": "ok", "version": __version__, "atlas_present": _ATLAS.exists(),
80
+ "writability_atlas": {"measured_count": len(measured), "measured": measured,
81
+ "ok": len(measured) > 0}}
82
+
83
+
84
+ @app.get("/atlas/coverage")
85
+ def atlas_coverage():
86
+ df = _atlas_df()
87
+ cov = (df.groupby("family")
88
+ .agg(n=("representative_system", "size"),
89
+ measured=("confidence", lambda s: int((s == "measured").sum())),
90
+ reachability_tier=("reachability_tier", "first"),
91
+ mechanism=("mechanism_bucket", "first"))
92
+ .reset_index())
93
+ return {"families": int(df["family"].nunique()), "systems": int(len(df)),
94
+ "coverage": cov.to_dict("records"), "disclaimer": _DISCLAIMER}
95
+
96
+
97
+ @app.get("/atlas")
98
+ def atlas(family: str | None = None, limit: int = Query(50, ge=1, le=500)):
99
+ df = _atlas_df()
100
+ if family:
101
+ df = df[df["family"] == family]
102
+ cols = [c for c in ["representative_system", "family", "confidence", "mechanism_bucket",
103
+ "deliv_class", "readiness", "cargo_capacity_bp", "reachability_tier",
104
+ "human_cell_activity"] if c in df.columns]
105
+ return {"n": int(len(df)), "rows": _records(df[cols].head(limit)), "disclaimer": _DISCLAIMER}
106
+
107
+
108
+ @app.get("/crosslink/writers")
109
+ def crosslink_writers(request: Request, chrom: str, bin: int, ct: str = "k562", cell_type: str | None = None):
110
+ from pen_stack.atlas import crosslink as cl
111
+ ct = _resolve_ct(request, ct, cell_type, {"chrom", "bin", "ct", "cell_type"})
112
+ try:
113
+ w = cl.writers_for_locus(chrom, bin, ct)
114
+ except FileNotFoundError as e:
115
+ raise HTTPException(503, str(e)) from e
116
+ if w.empty:
117
+ return {"locus": f"{chrom}:bin{bin}", "writers": [], "disclaimer": _DISCLAIMER}
118
+ fams = w.groupby("family").size().to_dict()
119
+ return {"locus": f"{chrom}:bin{bin}", "ct": ct,
120
+ "locus_writability": float(w["locus_writability"].iloc[0]),
121
+ "families": {k: int(v) for k, v in fams.items()},
122
+ "n_systems": int(len(w)), "disclaimer": _DISCLAIMER}
123
+
124
+
125
+ @app.get("/crosslink/loci")
126
+ def crosslink_loci(request: Request, family: str, ct: str = "k562", cell_type: str | None = None,
127
+ top: int = Query(20, ge=1, le=200)):
128
+ from pen_stack.atlas import crosslink as cl
129
+ ct = _resolve_ct(request, ct, cell_type, {"family", "ct", "cell_type", "top"})
130
+ try:
131
+ loci = cl.loci_for_writer(family, ct, top=top)
132
+ except FileNotFoundError as e:
133
+ raise HTTPException(503, str(e)) from e
134
+ return {"family": family, "ct": ct, "loci": _records(loci), "disclaimer": _DISCLAIMER}
135
+
136
+
137
+ @app.get("/gene/location")
138
+ def gene_location(gene: str):
139
+ """The canonical chromosome of a gene (or safe-harbour locus nickname), for the UI's gene/chromosome
140
+ concordance check. found=False when the gene is not in the coordinate table (no fabrication)."""
141
+ from pen_stack.planner.chromosome import canonical_chromosome
142
+ from pen_stack.planner.optimize import gene_region, resolve_gene
143
+ try:
144
+ reg = gene_region(gene)
145
+ except Exception: # noqa: BLE001 - coords table absent -> cannot resolve
146
+ reg = None
147
+ resolved = resolve_gene(gene)
148
+ if reg is None:
149
+ return {"gene": gene, "resolved": resolved, "found": False, "chrom": None}
150
+ return {"gene": gene, "resolved": resolved, "found": True, "chrom": canonical_chromosome(reg[0]),
151
+ "start": int(reg[1]), "end": int(reg[2])}
152
+
153
+
154
+ @app.get("/writable")
155
+ def writable(request: Request, gene: str, ct: str = "k562", cell_type: str | None = None,
156
+ top: int = Query(20, ge=1, le=200)):
157
+ from pen_stack.atlas.crosslink import loci_for_gene
158
+ ct = _resolve_ct(request, ct, cell_type, {"gene", "ct", "cell_type", "top"})
159
+ try:
160
+ g = loci_for_gene(gene, ct)
161
+ except FileNotFoundError as e:
162
+ raise HTTPException(503, str(e)) from e
163
+ cov = next((c["coverage"] for c in _CELLTYPES if c["id"] == ct), "unknown")
164
+ # writability = 0.5*safety + 0.5*p_durable (an additive, decomposable mean -- NOT a product, and there is no
165
+ # separate accessibility axis; chromatin enters as input features to the safety + durability models).
166
+ meta = {"gene": gene, "ct": ct, "coverage": cov, "writability_formula": "0.5*safety + 0.5*p_durable",
167
+ "coverage_note": ("partial chromatin panel for this cell type: durability degrades gracefully over the "
168
+ "missing tracks (still measured, not extrapolated)") if cov == "partial" else None,
169
+ "disclaimer": _DISCLAIMER}
170
+ if g.empty:
171
+ return {**meta, "loci": []}
172
+ cols = ["chrom", "bin", "safety", "p_durable", "writability"]
173
+ return {**meta, "loci": _records(g[cols].head(top))}
174
+
175
+
176
+ # the cell types the Site Finder offers, with their actual coverage. A cell type returns writable loci only when
177
+ # its measured writability atlas (atlas_<ct>.parquet) is actually present; the rest are a data-gated roadmap,
178
+ # never a silently-failing dropdown option.
179
+ _CELLTYPES = [
180
+ {"id": "k562", "label": "K562", "description": "chronic myelogenous leukemia line",
181
+ "coverage": "full", "tracks": "ATAC + histones + TRIP durability + safety"},
182
+ {"id": "hepg2", "label": "HepG2", "description": "hepatocellular carcinoma line",
183
+ "coverage": "full", "tracks": "ATAC + histones + safety (partial TRIP)"},
184
+ {"id": "hspc", "label": "HSPC", "description": "hematopoietic stem and progenitor cells",
185
+ "coverage": "partial", "tracks": "ATAC + expression + genotoxicity; partial histone panel (graceful degradation)"},
186
+ {"id": "h1_hesc", "label": "H1 hESC", "description": "H1 human embryonic stem cells", "coverage": "none", "tracks": ""},
187
+ {"id": "ipsc", "label": "iPSC", "description": "induced pluripotent stem cells", "coverage": "none", "tracks": ""},
188
+ {"id": "cd8_t", "label": "CD8 T", "description": "cytotoxic T lymphocytes", "coverage": "none", "tracks": ""},
189
+ {"id": "pbmc", "label": "PBMC", "description": "peripheral blood mononuclear cells", "coverage": "none", "tracks": ""},
190
+ ]
191
+
192
+
193
+ @app.get("/celltypes", tags=["site finder"])
194
+ def celltypes_endpoint():
195
+ """Per cell type: whether a MEASURED writability atlas exists (so Site Finder returns real loci) and its
196
+ coverage. Cell types without an atlas are a documented, data-gated roadmap, never a silently-failing option."""
197
+ from pen_stack.atlas.crosslink import writability_path
198
+ out = []
199
+ for ct in _CELLTYPES:
200
+ try:
201
+ writability_path(ct["id"])
202
+ measured = True
203
+ except Exception: # noqa: BLE001
204
+ measured = False
205
+ cov = ct["coverage"] if measured else "none"
206
+ out.append({**ct, "coverage": cov, "measured": measured,
207
+ "note": ct["tracks"] if measured else "no writability atlas built yet (data-gated roadmap)"})
208
+ return {"cell_types": out, "measured_count": sum(c["measured"] for c in out),
209
+ "disclaimer": "Only cell types with a measured writability atlas return loci; the rest are a "
210
+ "data-gated roadmap, never a fabricated or silently-empty result."}
211
+
212
+
213
+ @app.get("/recommend", tags=["writer atlas"])
214
+ def recommend_endpoint(write_type: str = "insertion", cargo_bp: int = Query(2000, ge=1, le=300000), cell_type: str = "K562",
215
+ target_seq: str | None = None, donor_seq: str | None = None,
216
+ top_k: int = Query(8, ge=1, le=30)):
217
+ """Rank writer families for a write request. KB readiness is the GROUNDED primary ranking;
218
+ each family also carries a CANDIDATE learned efficiency with a trained split-conformal interval, and a
219
+ dependency-free guide / att design when target/donor sequences are supplied. No efficiency is ever
220
+ fabricated for a family the curated dataset never saw (KB-only for those)."""
221
+ from pen_stack.atlas.writer_recommend import WRITE_TYPES, recommend_writers
222
+ if write_type not in WRITE_TYPES:
223
+ raise HTTPException(422, f"unknown write_type {write_type!r}; expected one of {sorted(WRITE_TYPES)}")
224
+ req = {"write_type": write_type, "cargo_bp": cargo_bp, "cell_type": cell_type,
225
+ "target_seq": (target_seq or "").strip() or None, "donor_seq": (donor_seq or "").strip() or None}
226
+ return recommend_writers(req, top_k=top_k)
227
+
228
+
229
+ @app.get("/guide_design", tags=["writer atlas"])
230
+ def guide_design_endpoint(writer_family: str, target_seq: str | None = None,
231
+ donor_seq: str | None = None, integrase: str = "Bxb1"):
232
+ """Design the targeting component a writer family needs from the published reprogramming rules: the
233
+ IS110/IS621 bridge-RNA loops for a (target, donor) pair, or a prime-editing pegRNA that writes a serine-
234
+ integrase attB at the target. Returned sequences are DESIGN CANDIDATES requiring empirical validation -- no
235
+ activity is claimed, and a documented att is written verbatim or not at all (the site is never fabricated)."""
236
+ from pen_stack.atlas.guide_design import design_guide_for_writer
237
+ return design_guide_for_writer(writer_family, (target_seq or "").strip() or None,
238
+ (donor_seq or "").strip() or None, integrase=integrase)
239
+
240
+
241
+ @app.get("/writer/efficiency", tags=["writer atlas"])
242
+ def writer_efficiency_endpoint():
243
+ """The curated Writer-Efficiency dataset (real measured integration efficiencies, one row per condition
244
+ with a DOI + verbatim quote) and the held-out Writer-Efficiency-Bench result (validation). The
245
+ pre-registered outcome: the learned predictor beats the KB family-mean baseline on held-out LOCUS (CI excludes
246
+ 0) but NOT on held-out FAMILY at this N, so the KB ranking is retained as primary and the predictor ships as a
247
+ candidate advisory."""
248
+ from pen_stack.atlas import writer_efficiency as we
249
+ df = we.human_cell()
250
+ cols = [c for c in ["system", "family", "variant", "cargo_bp", "locus", "cell_type",
251
+ "efficiency_pct", "specificity_pct", "doi", "quote"] if c in df.columns]
252
+ bench = None
253
+ p = _ATLAS.parents[2] / "benchmarks" / "writer_efficiency" / "result.json"
254
+ if p.exists():
255
+ bench = json.loads(p.read_text(encoding="utf-8"))
256
+ return {"dataset_summary": we.provenance_summary(), "records": _records(df[cols]), "benchmark": bench,
257
+ "note": "Measured, DOI-backed integration efficiencies. The bench is the contribution; the "
258
+ "learned predictor is a candidate advisory, not the authoritative ranking."}
259
+
260
+
261
+ @app.get("/writer/variants", tags=["writer atlas"])
262
+ def writer_variants_endpoint(integrase: str | None = None, system: str | None = None):
263
+ """Variant critique: retrospective recovery of known serine-integrase hyperactive mutants over a frozen
264
+ DOI'd panel (NOT a blind sequence-only predictor), plus the deferral of the blind protein-LM recovery
265
+ (no per-variant fitness endpoint exists, so it abstains rather than fabricate a positive). `system` is accepted
266
+ as an alias for `integrase`; omit both to get the full panel."""
267
+ from pen_stack.design import writer_variants as wv
268
+ target = integrase or system # accept either name; the panel is keyed by serine-integrase
269
+ return {"hyperactive_recovery": wv.hyperactive_recovery(target),
270
+ "blind_lm_recovery": wv.lm_recovery(),
271
+ "panel": wv.hyperactive_panel(target), # honour the integrase filter (was: always the full panel)
272
+ "note": "Retrospective catalogue recovery is real and DOI-backed; the blind LM predictor is deferred "
273
+ "(reported as a known limitation, never a manufactured positive)."}
274
+
275
+
276
+ @app.get("/writer/immune", tags=["writer atlas"])
277
+ def writer_immune_endpoint():
278
+ """The writer enzyme's immunogenicity as an antigen (writer-as-antigen, surfaced in the Writer Atlas):
279
+ per genome-writer family, the real NetMHCIIpan-4.0 MHC-II/CD4 epitope load + the ADA-risk axis
280
+ (MHC-II density x foreignness, self-tolerance filtered against the human proteome). Read from the committed
281
+ cache - NOT recomputed. Population-level proxy, never a patient-specific magnitude (a known-unknown). The Cas9
282
+ nuclease (an editor, not a large-cargo writer) and the human self control are excluded."""
283
+ from pen_stack.planner.immune_profile import writer_immunogenicity_table
284
+ return {"writers": writer_immunogenicity_table(),
285
+ "method": "NetMHCIIpan-4.0 MHC-II epitope load + ADA risk (MHC-II density x foreignness, self-tolerance "
286
+ "filtered against the human proteome). Population-level proxy from the committed cache; the "
287
+ "realized CD4 response / ADA titer is a known-unknown.",
288
+ "scale": "0-1, higher = lower risk (1 = least presentable / least ADA-driving)",
289
+ "no_fabrication": True}
290
+
291
+
292
+ @app.get("/bridge/design")
293
+ def bridge_design(target: str, donor: str, scaffold: str = "ISCro4_enhanced",
294
+ ct: str | None = None, scan: bool = False):
295
+ """Bridge-recombinase design + off-target/QC. scan=false by default (genome scan is heavy)."""
296
+ from pen_stack.bridge.pipeline import design_and_assess
297
+ res = design_and_assess(target, donor, scaffold, ct=ct, scan=scan)
298
+ off = res["offtargets"]
299
+ if off.get("scanned") and "table" in off:
300
+ t = off["table"]
301
+ off = {"scanned": True, "n_candidates": off["n_candidates"], "n_exact": off["n_exact"],
302
+ "top": t.head(20).to_dict("records")}
303
+ return {"brna": {k: v for k, v in res["brna"].items() if k != "bridge_sequence"} |
304
+ ({"bridge_sequence_len": len(res["brna"]["bridge_sequence"])} if res["brna"].get("available") else {}),
305
+ "qc": res["qc"], "offtargets": off, "disclaimer": res["disclaimer"]}
306
+
307
+
308
+ @app.get("/ask")
309
+ def ask(q: str):
310
+ """Grounded, cited Q&A. Numeric claims are resolved by tool calls, never guessed."""
311
+ from pen_stack.rag.qa import answer
312
+ return answer(q)
313
+
314
+
315
+ @app.get("/plan")
316
+ def plan(request: Request, gene: str, intent: str,
317
+ cargo_bp: int = Query(2000, ge=1, le=300000, description="donor payload in bp (1..300000)"),
318
+ ct: str = "k562", cell_type: str | None = None, k: int = Query(5, ge=1, le=20)):
319
+ """Write Planner: goal + edit_intent -> ranked, traceable plans. cargo_bp is bounded to a real
320
+ payload range (1..300 kb); out-of-range values are a 422, never a silently-'valid' plan."""
321
+ from pen_stack.planner.optimize import EditIntent
322
+ from pen_stack.planner.pipeline import plan_write
323
+ ct = _resolve_ct(request, ct, cell_type, {"gene", "intent", "cargo_bp", "ct", "cell_type", "k"})
324
+ try:
325
+ intent_e = EditIntent(intent)
326
+ except ValueError as e:
327
+ raise HTTPException(422, f"unknown edit_intent: {intent}") from e
328
+ try:
329
+ plans = plan_write(gene, intent_e, cargo_bp, ct, k=k)
330
+ except FileNotFoundError as e:
331
+ raise HTTPException(503, str(e)) from e
332
+ cov = next((c["coverage"] for c in _CELLTYPES if c["id"] == ct), "unknown")
333
+ return {"gene": gene, "intent": intent, "ct": ct, "coverage": cov, "n": len(plans), "plans": plans,
334
+ "disclaimer": _DISCLAIMER}
335
+
336
+
337
+ @app.post("/verify")
338
+ def verify_endpoint(design: dict):
339
+ """Verification service: submit a proposed genomic write, get back a structured Verdict,
340
+ legality + named rejections + calibrated confidence + epistemic status + scope flags. Legality and
341
+ confidence are distinct axes. A `question` key (optional) is checked against the known-unknowns registry."""
342
+ from pen_stack.verify import verify
343
+ return verify(design).model_dump()
344
+
345
+
346
+ @app.post("/verify/proof")
347
+ def verify_proof_endpoint(design: dict):
348
+ """Verification service: the repair-oriented proof object. Returns the three axes
349
+ (legality, confidence, biosecurity) reported separately, each with a status, the rule or signature that
350
+ fired, evidence, and a repair hint; the collapsed verdict is None. An agent repairs a failed design from
351
+ the legality axis's repair hint. A `question` key (optional) is checked against the known-unknowns."""
352
+ from pen_stack.verify.proof import verify_proof
353
+ return verify_proof(design).model_dump()
354
+
355
+
356
+ @app.post("/graph/query")
357
+ def graph_query_endpoint(q: dict):
358
+ """World-model graph: multi-hop query. Body: {locus, cargo_form?}. Returns writers that
359
+ reach the locus AND are deliverable by a cargo-form-compatible vehicle, each with its provenanced path."""
360
+ from pen_stack.graph import writers_reaching_and_deliverable
361
+ return writers_reaching_and_deliverable(q.get("locus"), cargo_form=q.get("cargo_form"))
362
+
363
+
364
+ # ======================================================================================
365
+ # The AI Integration Surface: the self-describing contract + the engine tool routes.
366
+ # An external agent fetches /capabilities + /scope and ROUTES on them, then calls the tools.
367
+ # ======================================================================================
368
+ @app.get("/capabilities", tags=["AI surface"])
369
+ def capabilities_endpoint():
370
+ """Machine-readable: WHAT PEN-STACK can do (tools, inputs, outputs, stability). Route on this, not prose."""
371
+ from pen_stack.api.manifest import capability_manifest
372
+ return capability_manifest()
373
+
374
+
375
+ @app.get("/scope", tags=["AI surface"])
376
+ def scope_endpoint():
377
+ """Machine-readable: WHAT PEN-STACK REFUSES to answer (known-unknowns + oracle scope cards). The contract
378
+ that makes depending on PEN-STACK safe: outputs outside scope are out_of_scope/extrapolating, never asserted."""
379
+ from pen_stack.api.manifest import scope_manifest
380
+ return scope_manifest()
381
+
382
+
383
+ @app.get("/oracles", tags=["live oracles"])
384
+ def oracles_endpoint(probe: bool = False):
385
+ """Per-foundation-model EXECUTION + LATENCY CLASS + live status (the 'tell the user the cost up front'
386
+ surface). `?probe=true` pings the local GPU model servers. Live oracles answer in seconds, ~2 min; held cloud
387
+ jobs (AF3/Boltz/Chai/Protenix) run separately and never block; deferred outcomes are never fabricated."""
388
+ from pen_stack.oracles.status import oracle_status, summary
389
+ return {"summary": summary(), "oracles": oracle_status(probe=bool(probe))}
390
+
391
+
392
+ @app.post("/safety", tags=["AI surface"])
393
+ def safety_endpoint(design: dict):
394
+ """Guardian: biosecurity / dual-use screen -> SafetyVerdict (clear/flag/escalate/refuse) + reason.
395
+ Additive: also carries ``standards``, the SafetyVerdict expressed in the community-standard
396
+ vocabulary (Common Mechanism ScreenStatus, SecureDNA outcome) for this specific decision, an in-design
397
+ concordance, not a certification (see /safety/concordance for the labelled-probe-set benchmark)."""
398
+ from pen_stack.safety import safety_gate
399
+ from pen_stack.safety.standards import align_to_common_mechanism
400
+ sv = safety_gate(design, actor=str(design.get("actor", "api")))
401
+ return {**sv.model_dump(), "standards": align_to_common_mechanism(sv)}
402
+
403
+
404
+ @app.get("/safety/concordance", tags=["AI surface"])
405
+ def safety_concordance_endpoint():
406
+ """Standards concordance: run the Guardian over the committed labelled probe set
407
+ (configs/safety/probes.yaml) and report, verbatim, its concordance with the Common Mechanism's expected
408
+ ScreenStatus (Pass for benign, Warning/Flag for a hazard). A concordance, not a certification."""
409
+ from pen_stack.safety.standards import concordance_report
410
+ return concordance_report()
411
+
412
+
413
+ @app.post("/immune", tags=["AI surface"])
414
+ def immune_endpoint(design: dict):
415
+ """Immune-risk profile: per-axis screen (never collapsed; collapsed_score is None)."""
416
+ from pen_stack.planner.immune_profile import immune_profile
417
+ return immune_profile(design)
418
+
419
+
420
+ @app.post("/offtarget", tags=["off-target"])
421
+ def offtarget_endpoint(req: dict):
422
+ """Cross-family off-target NOMINATION (NOT a clearance). Body:
423
+ {writer_family, guide?, enzyme?, max_mismatch?, candidate_sites?, sequence?, accessibility?, assay?}.
424
+
425
+ (finder): for a nuclease guide with NO ``candidate_sites``, enumerates the genome-wide off-target set
426
+ over GRCh38 (Cas-OFFinder, replayed from the committed cache) and ranks it by the real CRISOT-Score +
427
+ mismatch-calibrated risk + chromatin annotation - the default. Supplying ``candidate_sites`` keeps the
428
+ score-my-candidates path. Abstains (never fabricates) for a novel guide with no VM scan."""
429
+ from pen_stack.wgenome.offtarget_predict import nominate_offtargets
430
+ return nominate_offtargets(
431
+ req.get("writer_family", ""), guide=req.get("guide"), candidate_sites=req.get("candidate_sites"),
432
+ sequence=req.get("sequence"), accessibility=req.get("accessibility"),
433
+ target_core=req.get("target_core"), assay=req.get("assay", "guideseq"),
434
+ enzyme=req.get("enzyme") or req.get("system"), max_mismatch=int(req.get("max_mismatch", 5)))
435
+
436
+
437
+ @app.get("/offtarget/assay", tags=["off-target"])
438
+ def offtarget_assay_endpoint(writer_family: str):
439
+ """Validation-assay recommendation for a writer family (the assay that would confirm a nomination)."""
440
+ from pen_stack.wgenome.offtarget_assay import recommend_assay
441
+ return recommend_assay(writer_family)
442
+
443
+
444
+ @app.get("/offtarget/enumerated", tags=["off-target"])
445
+ def offtarget_enumerated_endpoint():
446
+ """The guides whose genome-wide off-target enumeration is CACHED (so the finder works here without a VM
447
+ scan). A novel guide abstains (its scan runs on the VM). Enumerated coordinates are public-genome facts."""
448
+ from pen_stack.wgenome.offtarget_data import CANONICAL_GUIDES
449
+ from pen_stack.wgenome.offtarget_enumerate import enumerated_guides
450
+ cached = enumerated_guides()
451
+ seq2name = {v: k for k, v in CANONICAL_GUIDES.items()}
452
+ return {"enzyme": "SpCas9",
453
+ "guides": [{"guide": g, "name": seq2name.get(g, g), "enzyme": e} for e, g in cached],
454
+ "note": ("genome-wide enumeration for these guides is replayed from the committed GRCh38 Cas-OFFinder "
455
+ "cache; a novel guide requires an on-VM scan (the finder abstains rather than fabricate).")}
456
+
457
+
458
+ @app.get("/campaign", tags=["closed-loop"])
459
+ def campaign_endpoint():
460
+ """The validation-campaign engine. Returns the expression-validation campaign: the next batch of
461
+ (cassette x locus x cell type) measurements ordered by expected information gain, the calibrate_axis gate it
462
+ targets, and the active-vs-random result (reported verbatim). Cloud-lab-executable; Level 3, human in control;
463
+ the experiments are candidates, the wet run is the standing bottleneck."""
464
+ from pen_stack.active.campaign import design_campaign
465
+ return design_campaign()
466
+
467
+
468
+ @app.post("/cloudlab", tags=["closed-loop"])
469
+ def cloudlab_endpoint(req: dict):
470
+ """Safety-gated cloud-lab submission. Body: {design, experiment?, provider?, actor?}. The
471
+ biosecurity gate runs BEFORE submission; a flagged design returns a structured refusal (blocked=True) and NO
472
+ protocol is emitted. A cleared design returns a mock / dry-run job receipt (a real run needs a partner).
473
+
474
+ An unknown provider is a 422 (not a 500): `submit()` raises CloudLabError for a provider outside the
475
+ recognised set, which submit_gated does NOT catch (it only wraps the safety refusal), so validate it here.
476
+ A null/empty provider falls back to the only wired provider, `mock`."""
477
+ from pen_stack.build.cloudlab import CloudLabError, submit_gated
478
+ provider = req.get("provider") or "mock" # null / "" -> the wired mock/dry-run path
479
+ try:
480
+ return submit_gated(req.get("design", {}), req.get("experiment", {}),
481
+ provider=provider, actor=str(req.get("actor", "api")))
482
+ except CloudLabError as e:
483
+ raise HTTPException(422, str(e)) from e
484
+
485
+
486
+ @app.get("/brains", tags=["closed-loop"])
487
+ def brains_endpoint():
488
+ """Benchmark the EIG/VOI experiment designer against the public SDL optimizers (BayBE / Atlas),
489
+ reported verbatim with both cited (a win is not required; the result is falsifiable)."""
490
+ from pen_stack.active.brains import benchmark
491
+ return benchmark()
492
+
493
+
494
+ @app.post("/writespec", tags=["writespec"])
495
+ def writespec_endpoint(req: dict):
496
+ """Parse a plain-language genome-writing request into a typed, ontology-backed WriteSpec.
497
+ Body: {prose, overrides?, check_feasibility?}. Returns the typed spec (with per-field provenance), the
498
+ assumptions behind every inferred field, clarifying questions for anything underspecified, the unresolved
499
+ terms (kept null, never invented), the downstream design adapter, and the feasibility verdict. A WriteSpec is
500
+ a REQUEST, not a claim; the extractor never fabricates intent."""
501
+ from pen_stack.spec.service import parse_request
502
+ return parse_request(req.get("prose", ""), overrides=req.get("overrides"),
503
+ check_feasibility=bool(req.get("check_feasibility", True)))
504
+
505
+
506
+ @app.post("/oracle/affinity", tags=["oracle"])
507
+ def oracle_affinity_endpoint(req: dict):
508
+ """Protein-ligand binding-affinity (Boltz-2 head) under the oracle contract. Body:
509
+ {protein_seq, ligand_smiles, pair_type?, ligand_name?}. Returns a CANDIDATE affinity (binder probability +
510
+ predicted value) with native uncertainty, cache-or-abstain; protein-protein/protein-DNA pair types are
511
+ flagged extrapolating (the head is protein-ligand only). Never runs the long job on the request path."""
512
+ from pen_stack.oracles.affinity import predict_affinity
513
+ r = predict_affinity(req.get("protein_seq", ""), req.get("ligand_smiles", ""),
514
+ pair_type=req.get("pair_type", "ligand"), ligand_name=req.get("ligand_name"))
515
+ return r.model_dump()
516
+
517
+
518
+ @app.post("/delivery", tags=["delivery"])
519
+ def delivery_endpoint(req: dict):
520
+ """Cross-modality delivery recommender. Body: {cargo_form, cargo_bp?, target_tissue?,
521
+ safety_weight?, in_vivo?}. Returns ranked vehicles + a grounded serotype->tissue tropism prior (approved
522
+ therapies; known-unknown for novel capsids) + the learned capsid-fitness bench. Never fabricates tropism."""
523
+ from pen_stack.planner.delivery_predict import recommend_delivery_plus
524
+ return recommend_delivery_plus(req.get("cargo_form", ""), req.get("cargo_bp"), req.get("target_tissue"),
525
+ safety_weight=float(req.get("safety_weight", 0.5)), in_vivo=req.get("in_vivo"),
526
+ serotype=req.get("serotype"))
527
+
528
+
529
+ @app.post("/capsid_fitness", tags=["delivery"])
530
+ def capsid_fitness_endpoint(req: dict):
531
+ """Learned AAV capsid packaging-fitness for a VP1 sequence (FLIP-AAV-trained). Body: {vp1_sequence}.
532
+ A CANDIDATE for the measured packaging axis, not an in-vivo tropism claim; abstains if the model is absent."""
533
+ from pen_stack.planner.delivery_predict import capsid_fitness
534
+ return capsid_fitness(req.get("vp1_sequence", ""), req.get("vector", "AAV"))
535
+
536
+
537
+ @app.post("/capsid/generate", tags=["delivery"])
538
+ def capsid_generate_endpoint(req: dict):
539
+ """Verify-gated generative AAV capsid candidates: propose VP1 555-595 variants of a WT, score each
540
+ with the learned FLIP-AAV fitness model, and keep only survivors with fitness >= WT (verifier-as-discriminator).
541
+ Body: {wt_vp1, n?, max_mut?, top?}. Candidates are labelled -- packaging fitness ONLY; assembly, in-vivo tropism
542
+ and immunogenicity are NOT claimed (immunogenicity is deferred to the immune profile, run before
543
+ synthesis). Abstains without the model; never fabricates a capsid."""
544
+ from pen_stack.design.capsid_generate import generate_capsid_candidates
545
+ n = max(1, min(int(req.get("n", 200)), 500))
546
+ max_mut = max(1, min(int(req.get("max_mut", 4)), 8))
547
+ top = max(1, min(int(req.get("top", 20)), 50))
548
+ return generate_capsid_candidates(req.get("wt_vp1", ""), n=n, max_mut=max_mut, top=top)
549
+
550
+
551
+ @app.get("/delivery/tropism", tags=["delivery"])
552
+ def delivery_tropism_endpoint(serotype: str | None = None, target_tissue: str | None = None):
553
+ """Grounded AAV serotype<->tissue tropism priors (approved therapies). Pass a `serotype` for the tissue(s)
554
+ it reaches (with the approved product + DOI), or a `target_tissue` for the approved serotypes that reach it; a
555
+ known-unknown when there is no approved precedent. Never fabricates a tissue."""
556
+ from pen_stack.planner.delivery_predict import serotype_tropism, serotypes_for_tissue
557
+ if serotype:
558
+ return serotype_tropism(serotype)
559
+ if target_tissue:
560
+ return serotypes_for_tissue(target_tissue)
561
+ raise HTTPException(422, "provide a serotype (-> tissue) or a target_tissue (-> serotypes)")
562
+
563
+
564
+ @app.post("/generate", tags=["AI surface"])
565
+ def generate_endpoint(req: dict):
566
+ """Generative designer: verifier-as-discriminator. Body: {goal?, candidates?, keep?}. Hazardous/illegal
567
+ candidates are discarded; survivors are calibrated + immune-profiled candidates (never asserted to work).
568
+
569
+ The GOAL'S cargo function is screened by the Guardian FIRST, before the vehicle x cargo sweep. A
570
+ hazardous goal (e.g. a furin-cleavage tropism-enhancement or a dominant-negative tumor-suppressor ablation) is
571
+ REFUSED up front and the response carries the explicit safety verdict, so an empty result is correctly
572
+ attributed to a biosecurity refusal (not a silent 'no candidates'). The per-candidate Guardian still runs in
573
+ verify() as defence in depth."""
574
+ from pen_stack.design import generate_designs
575
+ goal = req.get("goal")
576
+ # Guardian pre-screen on the goal's declared cargo function (the artifact, not any free-text justification).
577
+ if isinstance(goal, dict) and any(goal.get(f) for f in ("cargo_function", "cargo_seq")):
578
+ from pen_stack.safety.gate import safety_gate
579
+ screen = {f: goal[f] for f in ("cargo_function", "cargo_seq", "gene", "delivery_vehicle", "in_vivo",
580
+ "delivery_tropism", "replication_competent") if goal.get(f) is not None}
581
+ gv = safety_gate(screen, actor=str(req.get("actor", "api")))
582
+ if gv.decision in ("refuse", "escalate"):
583
+ return {"survivors": [], "refused": True,
584
+ "safety": {"decision": gv.decision, "reason": gv.reason,
585
+ "hits": [{"detail": h.detail, "severity": h.severity, "kind": h.kind}
586
+ for h in gv.hits]},
587
+ "disclaimer": _DISCLAIMER}
588
+ return {"survivors": generate_designs(goal, candidates=req.get("candidates"),
589
+ keep=int(req.get("keep", 25)), actor=str(req.get("actor", "api"))),
590
+ "refused": False, "disclaimer": _DISCLAIMER}
591
+
592
+
593
+ @app.post("/predict", tags=["AI surface"])
594
+ def predict_endpoint(req: dict):
595
+ """Digital twin: calibrated, OOD-gated, phenotype-bounded outcome. Body: {design, cell_state}."""
596
+ from pen_stack.twin import predict_outcome
597
+ return predict_outcome(req["design"], req.get("cell_state", "k562"))
598
+
599
+
600
+ @app.get("/twin/promoters", tags=["AI surface"])
601
+ def promoters_endpoint():
602
+ """The selectable promoter palette (constitutive + tissue-specific, literature strength/context/DOI).
603
+ The Twin's relative-expression estimate reacts to the chosen promoter (design.promoter)."""
604
+ from pen_stack.twin.mechanistic import promoter_palette
605
+ return {"promoters": promoter_palette()}
606
+
607
+
608
+ @app.post("/suggest", tags=["AI surface"])
609
+ def suggest_endpoint(req: dict):
610
+ """Experiment designer: a diverse, informative next-experiment batch. Body: {candidates, cell_state, k?}."""
611
+ from pen_stack.active import select_batch
612
+ return {"batch": select_batch(req["candidates"], req.get("cell_state", "k562"), {},
613
+ k=int(req.get("k", 8))), "disclaimer": _DISCLAIMER}
614
+
615
+
616
+ @app.post("/session", tags=["AI surface"])
617
+ def session_endpoint(req: dict):
618
+ """Co-scientist: drive the full loop. Body: {goal, cell_state, candidates?}. Returns strategies +
619
+ predicted outcomes + per-axis immune profiles + suggested experiments + citations + scope ledger + safety."""
620
+ from pen_stack.agent.co_scientist import co_scientist_session
621
+ return co_scientist_session(req["goal"], req.get("cell_state", "k562"), candidates=req.get("candidates"))
622
+
623
+
624
+ # ======================================================================================
625
+ # The Genome-Writing Challenge (read-only surface for the web Challenge page).
626
+ # Public tasks (NO labels) + the PEN-STACK reference submission that anchors the leaderboard. Submissions
627
+ # are Python `predict_fn`s scored offline (`benchmarks/genome_writing_challenge/run.py`), never accepted
628
+ # over HTTP, so these routes only EXPOSE the held-out round and the anchor score.
629
+ # ======================================================================================
630
+ @app.get("/challenge/tasks", tags=["challenge"])
631
+ def challenge_tasks(round_id: str = "2026R1"):
632
+ """The public inputs of the current held-out round (family + design + instructions; NEVER the label)."""
633
+ from benchmarks.genome_writing_challenge.harness import _round_tasks
634
+ tasks = _round_tasks(round_id)
635
+ return {"round": round_id, "n_tasks": len(tasks),
636
+ "tasks": [{"id": t.id, "family": t.family, "public_input": t.public_input} for t in tasks]}
637
+
638
+
639
+ @app.get("/challenge/leaderboard", tags=["challenge"])
640
+ def challenge_leaderboard(round_id: str = "2026R1"):
641
+ """The leaderboard anchored by the PEN-STACK reference submission (deterministic, non-circular labels,
642
+ no-fabrication audited). External submissions are scored offline and appended here after a round."""
643
+ from benchmarks.genome_writing_challenge.harness import evaluate, reference_submission
644
+ ref = evaluate(reference_submission(), round_id)
645
+ return {"round": round_id, "leaderboard": [ref],
646
+ "rules": {"no_circular_labels": ref["no_circular_labels"],
647
+ "no_fabrication_audited": True, "labels": "validated PEN-STACK verifier / oracles"}}