converge_optimization/gate/
decision.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct PromotionGate {
8 pub required_invariants: Vec<String>,
10 pub required_truths: Vec<String>,
12 pub authority: AuthorityPolicy,
14 pub decision: GateDecision,
16 pub rationale: String,
18}
19
20impl PromotionGate {
21 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 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 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 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 pub fn is_promoted(&self) -> bool {
71 self.decision == GateDecision::Promote
72 }
73
74 pub fn is_rejected(&self) -> bool {
76 self.decision == GateDecision::Reject
77 }
78
79 pub fn requires_escalation(&self) -> bool {
81 self.decision == GateDecision::Escalate
82 }
83
84 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum GateDecision {
99 Promote,
101 Reject,
103 Escalate,
105}
106
107impl GateDecision {
108 pub fn is_terminal(&self) -> bool {
110 matches!(self, Self::Promote | Self::Reject)
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116#[serde(tag = "type", rename_all = "snake_case")]
117pub enum AuthorityPolicy {
118 Automatic,
120 HumanRequired,
122 RoleRequired {
124 roles: Vec<String>,
126 },
127 MultiApproval {
129 count: usize,
131 roles: Vec<String>,
133 },
134}
135
136impl AuthorityPolicy {
137 pub fn role(role: impl Into<String>) -> Self {
139 Self::RoleRequired {
140 roles: vec![role.into()],
141 }
142 }
143
144 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 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 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}