Skip to main content

converge_optimization/gate/
decision.rs

1//! Promotion gate for plan approval
2
3use serde::{Deserialize, Serialize};
4
5/// Promotion gate for plan approval
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct PromotionGate {
8    /// Invariants that must hold
9    pub required_invariants: Vec<String>,
10    /// Business truths that must be verified
11    pub required_truths: Vec<String>,
12    /// Who can promote this plan
13    pub authority: AuthorityPolicy,
14    /// The gate decision
15    pub decision: GateDecision,
16    /// Rationale for the decision
17    pub rationale: String,
18}
19
20impl PromotionGate {
21    /// Create a gate that promotes automatically
22    pub fn auto_promote(rationale: impl Into<String>) -> Self {
23        Self {
24            required_invariants: Vec::new(),
25            required_truths: Vec::new(),
26            authority: AuthorityPolicy::Automatic,
27            decision: GateDecision::Promote,
28            rationale: rationale.into(),
29        }
30    }
31
32    /// Create a gate that requires human review
33    pub fn requires_review(required_truths: Vec<String>, rationale: impl Into<String>) -> Self {
34        Self {
35            required_invariants: Vec::new(),
36            required_truths,
37            authority: AuthorityPolicy::HumanRequired,
38            decision: GateDecision::Escalate,
39            rationale: rationale.into(),
40        }
41    }
42
43    /// Create a rejection gate
44    pub fn reject(rationale: impl Into<String>) -> Self {
45        Self {
46            required_invariants: Vec::new(),
47            required_truths: Vec::new(),
48            authority: AuthorityPolicy::Automatic,
49            decision: GateDecision::Reject,
50            rationale: rationale.into(),
51        }
52    }
53
54    /// Create a gate with invariant requirements
55    pub fn with_invariants(
56        required_invariants: Vec<String>,
57        decision: GateDecision,
58        rationale: impl Into<String>,
59    ) -> Self {
60        Self {
61            required_invariants,
62            required_truths: Vec::new(),
63            authority: AuthorityPolicy::Automatic,
64            decision,
65            rationale: rationale.into(),
66        }
67    }
68
69    /// Check if this gate promotes the plan
70    pub fn is_promoted(&self) -> bool {
71        self.decision == GateDecision::Promote
72    }
73
74    /// Check if this gate rejects the plan
75    pub fn is_rejected(&self) -> bool {
76        self.decision == GateDecision::Reject
77    }
78
79    /// Check if this gate requires escalation
80    pub fn requires_escalation(&self) -> bool {
81        self.decision == GateDecision::Escalate
82    }
83
84    /// Check if human approval is required
85    pub fn requires_human(&self) -> bool {
86        matches!(
87            self.authority,
88            AuthorityPolicy::HumanRequired
89                | AuthorityPolicy::RoleRequired { .. }
90                | AuthorityPolicy::MultiApproval { .. }
91        )
92    }
93}
94
95/// Gate decision outcome
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum GateDecision {
99    /// Plan is approved for execution
100    Promote,
101    /// Plan is rejected
102    Reject,
103    /// Plan requires escalation to human/higher authority
104    Escalate,
105}
106
107impl GateDecision {
108    /// Check if this is a terminal decision (Promote or Reject)
109    pub fn is_terminal(&self) -> bool {
110        matches!(self, Self::Promote | Self::Reject)
111    }
112}
113
114/// Authority policy for who can make decisions
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116#[serde(tag = "type", rename_all = "snake_case")]
117pub enum AuthorityPolicy {
118    /// System can decide automatically
119    Automatic,
120    /// Human approval required
121    HumanRequired,
122    /// Specific role required
123    RoleRequired {
124        /// Roles that can approve
125        roles: Vec<String>,
126    },
127    /// Multiple approvers required
128    MultiApproval {
129        /// Number of approvals needed
130        count: usize,
131        /// Roles that can approve
132        roles: Vec<String>,
133    },
134}
135
136impl AuthorityPolicy {
137    /// Create role-based authority
138    pub fn role(role: impl Into<String>) -> Self {
139        Self::RoleRequired {
140            roles: vec![role.into()],
141        }
142    }
143
144    /// Create multi-role authority
145    pub fn roles(roles: impl IntoIterator<Item = impl Into<String>>) -> Self {
146        Self::RoleRequired {
147            roles: roles.into_iter().map(Into::into).collect(),
148        }
149    }
150
151    /// Create multi-approval authority
152    pub fn multi_approval(
153        count: usize,
154        roles: impl IntoIterator<Item = impl Into<String>>,
155    ) -> Self {
156        Self::MultiApproval {
157            count,
158            roles: roles.into_iter().map(Into::into).collect(),
159        }
160    }
161
162    /// Check if this policy allows automatic decision
163    pub fn is_automatic(&self) -> bool {
164        matches!(self, Self::Automatic)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_auto_promote() {
174        let gate = PromotionGate::auto_promote("all invariants passed");
175        assert!(gate.is_promoted());
176        assert!(!gate.requires_human());
177        assert!(!gate.requires_escalation());
178    }
179
180    #[test]
181    fn test_reject() {
182        let gate = PromotionGate::reject("constraint violation");
183        assert!(gate.is_rejected());
184        assert!(!gate.is_promoted());
185    }
186
187    #[test]
188    fn test_requires_review() {
189        let gate = PromotionGate::requires_review(
190            vec!["budget_approved".to_string()],
191            "high value transaction",
192        );
193        assert!(gate.requires_escalation());
194        assert!(gate.requires_human());
195        assert!(!gate.is_promoted());
196    }
197
198    #[test]
199    fn test_authority_policy() {
200        let auto = AuthorityPolicy::Automatic;
201        assert!(auto.is_automatic());
202
203        let role = AuthorityPolicy::role("admin");
204        assert!(!role.is_automatic());
205
206        let multi = AuthorityPolicy::multi_approval(2, vec!["manager", "director"]);
207        if let AuthorityPolicy::MultiApproval { count, roles } = multi {
208            assert_eq!(count, 2);
209            assert_eq!(roles.len(), 2);
210        } else {
211            panic!("expected MultiApproval");
212        }
213    }
214
215    #[test]
216    fn test_decision_terminal() {
217        assert!(GateDecision::Promote.is_terminal());
218        assert!(GateDecision::Reject.is_terminal());
219        assert!(!GateDecision::Escalate.is_terminal());
220    }
221
222    #[test]
223    fn test_serde_roundtrip() {
224        let gate = PromotionGate::requires_review(vec!["truth1".to_string()], "needs review");
225        let json = serde_json::to_string(&gate).unwrap();
226        let restored: PromotionGate = serde_json::from_str(&json).unwrap();
227        assert_eq!(restored.required_truths, gate.required_truths);
228    }
229}