use std::collections::BTreeSet;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::models::bundle::SynthesisBundle;
use crate::models::profile::BioProfile;
use crate::screening::HazardHit;
pub mod chemical;
pub mod dna;
pub mod homology;
pub mod molecule;
pub mod peptide;
pub mod protocol;
pub mod stateful;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
#[allow(missing_docs)]
pub enum InvariantId {
D1,
D2,
D3,
D4,
D5,
D6,
D7,
D8,
D9,
D10,
P1,
P2,
P3,
P4,
P5,
P6,
P7,
P8,
P9,
P10,
C1,
C2,
C3,
C4,
C5,
C6,
C7,
C8,
C9,
C10,
Pr1,
Pr2,
Pr3,
Pr4,
}
impl InvariantId {
pub fn as_str(&self) -> &'static str {
match self {
InvariantId::D1 => "D1",
InvariantId::D2 => "D2",
InvariantId::D3 => "D3",
InvariantId::D4 => "D4",
InvariantId::D5 => "D5",
InvariantId::D6 => "D6",
InvariantId::D7 => "D7",
InvariantId::D8 => "D8",
InvariantId::D9 => "D9",
InvariantId::D10 => "D10",
InvariantId::P1 => "P1",
InvariantId::P2 => "P2",
InvariantId::P3 => "P3",
InvariantId::P4 => "P4",
InvariantId::P5 => "P5",
InvariantId::P6 => "P6",
InvariantId::P7 => "P7",
InvariantId::P8 => "P8",
InvariantId::P9 => "P9",
InvariantId::P10 => "P10",
InvariantId::C1 => "C1",
InvariantId::C2 => "C2",
InvariantId::C3 => "C3",
InvariantId::C4 => "C4",
InvariantId::C5 => "C5",
InvariantId::C6 => "C6",
InvariantId::C7 => "C7",
InvariantId::C8 => "C8",
InvariantId::C9 => "C9",
InvariantId::C10 => "C10",
InvariantId::Pr1 => "PR1",
InvariantId::Pr2 => "PR2",
InvariantId::Pr3 => "PR3",
InvariantId::Pr4 => "PR4",
}
}
pub fn family(&self) -> InvariantFamily {
match self {
InvariantId::D1
| InvariantId::D2
| InvariantId::D3
| InvariantId::D4
| InvariantId::D5
| InvariantId::D6
| InvariantId::D7
| InvariantId::D8
| InvariantId::D9
| InvariantId::D10 => InvariantFamily::Dna,
InvariantId::P1
| InvariantId::P2
| InvariantId::P3
| InvariantId::P4
| InvariantId::P5
| InvariantId::P6
| InvariantId::P7
| InvariantId::P8
| InvariantId::P9
| InvariantId::P10 => InvariantFamily::Peptide,
InvariantId::C1
| InvariantId::C2
| InvariantId::C3
| InvariantId::C4
| InvariantId::C5
| InvariantId::C6
| InvariantId::C7
| InvariantId::C8
| InvariantId::C9
| InvariantId::C10 => InvariantFamily::Chemical,
InvariantId::Pr1 | InvariantId::Pr2 | InvariantId::Pr3 | InvariantId::Pr4 => {
InvariantFamily::Protocol
}
}
}
pub fn all() -> &'static [InvariantId] {
&[
InvariantId::D1,
InvariantId::D2,
InvariantId::D3,
InvariantId::D4,
InvariantId::D5,
InvariantId::D6,
InvariantId::D7,
InvariantId::D8,
InvariantId::D9,
InvariantId::D10,
InvariantId::P1,
InvariantId::P2,
InvariantId::P3,
InvariantId::P4,
InvariantId::P5,
InvariantId::P6,
InvariantId::P7,
InvariantId::P8,
InvariantId::P9,
InvariantId::P10,
InvariantId::C1,
InvariantId::C2,
InvariantId::C3,
InvariantId::C4,
InvariantId::C5,
InvariantId::C6,
InvariantId::C7,
InvariantId::C8,
InvariantId::C9,
InvariantId::C10,
InvariantId::Pr1,
InvariantId::Pr2,
InvariantId::Pr3,
InvariantId::Pr4,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InvariantFamily {
Dna,
Peptide,
Chemical,
Protocol,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum InvariantStatus {
Pass,
Fail {
reason: String,
},
Advisory {
note: String,
},
Unimplemented,
DbStale {
reason: String,
},
}
impl InvariantStatus {
pub fn is_pass(&self) -> bool {
matches!(self, InvariantStatus::Pass)
}
pub fn is_fail(&self) -> bool {
matches!(
self,
InvariantStatus::Fail { .. } | InvariantStatus::DbStale { .. }
)
}
pub fn is_advisory(&self) -> bool {
matches!(self, InvariantStatus::Advisory { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InvariantResult {
pub id: InvariantId,
pub name: String,
pub family: InvariantFamily,
pub status: InvariantStatus,
}
pub trait HazardDatabase: Send + Sync {
fn freshness(&self) -> Duration;
fn version(&self) -> u64;
fn freshness_window(&self) -> Duration {
Duration::from_secs(30 * 24 * 60 * 60)
}
fn is_stale(&self) -> bool {
self.freshness() > self.freshness_window()
}
}
#[derive(Debug, Clone, Copy)]
pub struct InvariantContext<'a> {
pub screening_hits: &'a [HazardHit],
pub profile: &'a BioProfile,
}
pub trait Invariant: Send + Sync {
fn id(&self) -> InvariantId;
fn name(&self) -> &'static str;
fn evaluate(&self, bundle: &SynthesisBundle) -> InvariantStatus;
fn evaluate_with(
&self,
bundle: &SynthesisBundle,
_ctx: &InvariantContext<'_>,
) -> InvariantStatus {
self.evaluate(bundle)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct OperatorState {
pub principal: String,
#[serde(default)]
pub approved_count: u64,
#[serde(default)]
pub rejected_count: u64,
#[serde(default)]
pub cumulative_dna_bases: u64,
#[serde(default)]
pub cumulative_peptide_residues: u64,
#[serde(default)]
pub recent_kmers: Vec<String>,
}
pub trait StatefulInvariant: Send + Sync {
fn id(&self) -> InvariantId;
fn name(&self) -> &'static str;
fn evaluate(&self, bundle: &SynthesisBundle, state: &OperatorState) -> InvariantStatus;
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct InvariantSelection {
#[serde(default)]
pub disabled: BTreeSet<InvariantId>,
}
impl InvariantSelection {
pub fn includes(&self, id: InvariantId) -> bool {
!self.disabled.contains(&id)
}
}
pub fn run_all(
bundle: &SynthesisBundle,
selection: &InvariantSelection,
ctx: &InvariantContext<'_>,
) -> Vec<InvariantResult> {
let mut out = Vec::with_capacity(34);
for id in InvariantId::all() {
if !selection.includes(*id) {
continue;
}
let (name, family, status) = evaluate_by_id(*id, bundle, ctx);
out.push(InvariantResult {
id: *id,
name: name.into(),
family,
status,
});
}
out
}
fn evaluate_by_id(
id: InvariantId,
bundle: &SynthesisBundle,
ctx: &InvariantContext<'_>,
) -> (&'static str, InvariantFamily, InvariantStatus) {
use chemical::*;
use dna::*;
use peptide::*;
use protocol::*;
match id {
InvariantId::D1 => {
let inv = SelectAgentScreen;
(
inv.name(),
InvariantFamily::Dna,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::D2 => {
let inv = PandemicPathogenScreen;
(
inv.name(),
InvariantFamily::Dna,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::D3 => {
let inv = ToxinGeneScreen;
(
inv.name(),
InvariantFamily::Dna,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::D4 => {
let inv = VirulenceFactorScreen;
(
inv.name(),
InvariantFamily::Dna,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::D5 => {
let inv = AntibioticResistanceScreen;
(
inv.name(),
InvariantFamily::Dna,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::D6 => {
let inv = SynbioPartScreen;
(
inv.name(),
InvariantFamily::Dna,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::D7 => {
let inv = CodonEntropyScreen;
(
inv.name(),
InvariantFamily::Dna,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::D8 => {
let inv = GcContentScreen;
(
inv.name(),
InvariantFamily::Dna,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::D9 => {
let inv = SecondaryStructureScreen;
(
inv.name(),
InvariantFamily::Dna,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::D10 => {
let inv = AssemblyCompatibilityScreen;
(
inv.name(),
InvariantFamily::Dna,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::P1 => {
let inv = AntimicrobialPeptideScreen;
(
inv.name(),
InvariantFamily::Peptide,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::P2 => {
let inv = CellPenetratingPeptideScreen;
(
inv.name(),
InvariantFamily::Peptide,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::P3 => {
let inv = MembraneDisruptingScreen;
(
inv.name(),
InvariantFamily::Peptide,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::P4 => {
let inv = PpiInhibitorScreen;
(
inv.name(),
InvariantFamily::Peptide,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::P5 => {
let inv = EnzymeActiveSiteMimicScreen;
(
inv.name(),
InvariantFamily::Peptide,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::P6 => {
let inv = ImmunogenicEpitopeScreen;
(
inv.name(),
InvariantFamily::Peptide,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::P7 => {
let inv = StabilityScreen;
(
inv.name(),
InvariantFamily::Peptide,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::P8 => {
let inv = SolubilityScreen;
(
inv.name(),
InvariantFamily::Peptide,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::P9 => {
let inv = PtmSiteScreen;
(
inv.name(),
InvariantFamily::Peptide,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::P10 => {
let inv = DeliveryCompatScreen;
(
inv.name(),
InvariantFamily::Peptide,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::C1 => {
let inv = CwcScreen;
(
inv.name(),
InvariantFamily::Chemical,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::C2 => {
let inv = ExplosiveScreen;
(
inv.name(),
InvariantFamily::Chemical,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::C3 => {
let inv = NarcoticScreen;
(
inv.name(),
InvariantFamily::Chemical,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::C4 => {
let inv = EnvToxinScreen;
(
inv.name(),
InvariantFamily::Chemical,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::C5 => {
let inv = CarcinogenMutagenScreen;
(
inv.name(),
InvariantFamily::Chemical,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::C6 => {
let inv = EndocrineDisruptorScreen;
(
inv.name(),
InvariantFamily::Chemical,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::C7 => {
let inv = BioaccumulationScreen;
(
inv.name(),
InvariantFamily::Chemical,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::C8 => {
let inv = PathwayFeasibilityScreen;
(
inv.name(),
InvariantFamily::Chemical,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::C9 => {
let inv = ReactionSafetyScreen;
(
inv.name(),
InvariantFamily::Chemical,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::C10 => {
let inv = WasteToxicityScreen;
(
inv.name(),
InvariantFamily::Chemical,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::Pr1 => {
let inv = ProtocolStepCount;
(
inv.name(),
InvariantFamily::Protocol,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::Pr2 => {
let inv = ProtocolAllowedVocabulary;
(
inv.name(),
InvariantFamily::Protocol,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::Pr3 => {
let inv = ProtocolNoNested;
(
inv.name(),
InvariantFamily::Protocol,
inv.evaluate_with(bundle, ctx),
)
}
InvariantId::Pr4 => {
let inv = ProtocolAggregateVolume;
(
inv.name(),
InvariantFamily::Protocol,
inv.evaluate_with(bundle, ctx),
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invariant_id_all_has_thirty_four_unique_entries() {
let all = InvariantId::all();
assert_eq!(all.len(), 34);
let unique: BTreeSet<_> = all.iter().collect();
assert_eq!(unique.len(), 34);
}
#[test]
fn invariant_id_family_classifies_correctly() {
assert_eq!(InvariantId::D5.family(), InvariantFamily::Dna);
assert_eq!(InvariantId::P5.family(), InvariantFamily::Peptide);
assert_eq!(InvariantId::C5.family(), InvariantFamily::Chemical);
}
#[test]
fn invariant_status_classifications() {
assert!(InvariantStatus::Pass.is_pass());
assert!(InvariantStatus::Fail { reason: "x".into() }.is_fail());
assert!(InvariantStatus::DbStale { reason: "x".into() }.is_fail());
assert!(!InvariantStatus::Unimplemented.is_pass());
assert!(!InvariantStatus::Unimplemented.is_fail());
assert!(InvariantStatus::Advisory { note: "x".into() }.is_advisory());
assert!(!InvariantStatus::Advisory { note: "x".into() }.is_pass());
assert!(!InvariantStatus::Advisory { note: "x".into() }.is_fail());
}
#[test]
fn selection_round_trips_disabled() {
let mut sel = InvariantSelection::default();
sel.disabled.insert(InvariantId::D7);
assert!(!sel.includes(InvariantId::D7));
assert!(sel.includes(InvariantId::D1));
let json = serde_json::to_string(&sel).unwrap();
let back: InvariantSelection = serde_json::from_str(&json).unwrap();
assert_eq!(back, sel);
}
#[test]
fn selection_rejects_unknown_fields() {
let bad = r#"{"disabled":["D1"],"unknown":42}"#;
assert!(serde_json::from_str::<InvariantSelection>(bad).is_err());
}
#[test]
fn run_all_with_dna_bundle_returns_thirty_four_results() {
let bundle = sample_dna_bundle();
let profile = sample_profile();
let ctx = InvariantContext {
screening_hits: &[],
profile: &profile,
};
let results = run_all(&bundle, &InvariantSelection::default(), &ctx);
assert_eq!(results.len(), 34);
}
#[test]
fn run_all_honours_selection() {
let mut sel = InvariantSelection::default();
sel.disabled.insert(InvariantId::D1);
sel.disabled.insert(InvariantId::C10);
sel.disabled.insert(InvariantId::Pr1);
let profile = sample_profile();
let ctx = InvariantContext {
screening_hits: &[],
profile: &profile,
};
let results = run_all(&sample_dna_bundle(), &sel, &ctx);
assert_eq!(results.len(), 31);
assert!(!results.iter().any(|r| r.id == InvariantId::D1));
assert!(!results.iter().any(|r| r.id == InvariantId::C10));
assert!(!results.iter().any(|r| r.id == InvariantId::Pr1));
}
#[test]
fn protocol_bundle_runs_pr_pipeline() {
use crate::models::bundle::{BundleAuthority, SynthesisPayload};
let bundle = SynthesisBundle {
timestamp: chrono::Utc::now(),
source: "t".into(),
sequence: 0,
payload: SynthesisPayload::Protocol {
steps: vec!["aspirate 10uL".into(), "dispense 10uL".into()],
},
delta_time: 0.0,
authority: BundleAuthority {
pca_chain: String::new(),
required_ops: vec![],
},
metadata: Default::default(),
};
let profile = sample_profile();
let ctx = InvariantContext {
screening_hits: &[],
profile: &profile,
};
let results = run_all(&bundle, &InvariantSelection::default(), &ctx);
assert_eq!(results.len(), 34);
let pr1 = results.iter().find(|r| r.id == InvariantId::Pr1).unwrap();
assert!(matches!(pr1.status, InvariantStatus::Pass));
}
fn sample_dna_bundle() -> SynthesisBundle {
use crate::models::bundle::{BundleAuthority, SynthesisPayload};
SynthesisBundle {
timestamp: chrono::Utc::now(),
source: "test".into(),
sequence: 1,
payload: SynthesisPayload::Dna {
sequence: "ATGAAAGCTGGCGTTTTTTGCCTG".into(),
},
delta_time: 0.0,
authority: BundleAuthority {
pca_chain: String::new(),
required_ops: Vec::new(),
},
metadata: Default::default(),
}
}
pub(crate) fn sample_profile() -> BioProfile {
BioProfile {
name: "test".into(),
version: "0.1.0".into(),
bsl_level: 2,
allowed_substrates: vec!["dna".into()],
max_synthesis_volume_ml: 5.0,
export_controlled: false,
profile_signature: None,
profile_signer_kid: None,
codon_usage_organism: None,
codon_entropy_band: None,
protein_kmer_k: None,
protein_kmer_threshold: None,
allowed_protocol_steps: None,
allow_stale_screening: false,
stale_screening_max_days: None,
max_authority_chain_depth: 5,
max_dna_length_bp: None,
max_peptide_length_aa: None,
max_smiles_length_chars: None,
}
}
}