Skip to main content

lean_ctx_protocol/
experiment.rs

1use crate::MoneyV1;
2use serde::{Deserialize, Serialize};
3
4/// Experiment arm assignment — signed by proprietary platform.
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub enum ExperimentArm {
7    Control,
8    Optimized,
9    Shadow,
10}
11
12/// Signed assignment from sidecar. Runtime executes, never decides.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct ExperimentAssignmentV1 {
15    pub experiment_id: String,
16    pub subject_id: String,
17    pub arm: ExperimentArm,
18    pub configuration_ref: String,
19    pub expires_at: String,
20    pub max_incremental_cost: MoneyV1,
21    pub allowed_providers: Vec<String>,
22    pub allowed_models: Vec<String>,
23    pub data_classification: DataClassification,
24    pub side_effect_policy: SideEffectPolicy,
25    pub kill_switch_ref: String,
26    pub signature: String,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub enum DataClassification {
31    Public,
32    Internal,
33    Confidential,
34    Restricted,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub enum SideEffectPolicy {
39    NoSideEffects,
40    ReadOnly,
41    AllowWrites,
42}
43
44#[cfg(test)]
45mod tests {
46    use crate::{
47        DataClassification, ExperimentArm, ExperimentAssignmentV1, MoneyV1, SideEffectPolicy,
48    };
49
50    #[test]
51    fn serialization_round_trip() {
52        let assignment = ExperimentAssignmentV1 {
53            experiment_id: "exp-1".to_owned(),
54            subject_id: "subject-1".to_owned(),
55            arm: ExperimentArm::Optimized,
56            configuration_ref: "config:1".to_owned(),
57            expires_at: "2026-01-01T00:00:00Z".to_owned(),
58            max_incremental_cost: MoneyV1 {
59                currency: "USD".to_owned(),
60                coefficient: 25,
61                scale: 4,
62            },
63            allowed_providers: vec!["provider".to_owned()],
64            allowed_models: vec!["model".to_owned()],
65            data_classification: DataClassification::Internal,
66            side_effect_policy: SideEffectPolicy::ReadOnly,
67            kill_switch_ref: "kill:1".to_owned(),
68            signature: "signature".to_owned(),
69        };
70        let json = serde_json::to_string(&assignment).expect("assignment should serialize");
71        let decoded = serde_json::from_str(&json).expect("assignment should deserialize");
72        assert_eq!(assignment, decoded);
73    }
74}