1use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use converge_pack::{DomainId, GateId, PolicyVersionId, PrincipalId, ResourceId, ResourceKind};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum FlowAction {
20 Propose,
21 Validate,
22 Promote,
23 Commit,
24 AdvancePhase,
25}
26
27impl FlowAction {
28 #[must_use]
29 pub const fn as_str(self) -> &'static str {
30 match self {
31 Self::Propose => "propose",
32 Self::Validate => "validate",
33 Self::Promote => "promote",
34 Self::Commit => "commit",
35 Self::AdvancePhase => "advance_phase",
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum AuthorityLevel {
44 Advisory,
45 Supervisory,
46 Participatory,
47 Sovereign,
48}
49
50impl AuthorityLevel {
51 #[must_use]
52 pub const fn as_str(self) -> &'static str {
53 match self {
54 Self::Advisory => "advisory",
55 Self::Supervisory => "supervisory",
56 Self::Participatory => "participatory",
57 Self::Sovereign => "sovereign",
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum FlowPhase {
66 Intent,
67 Framing,
68 Exploration,
69 Tension,
70 Convergence,
71 Commitment,
72}
73
74impl FlowPhase {
75 #[must_use]
76 pub const fn as_str(self) -> &'static str {
77 match self {
78 Self::Intent => "intent",
79 Self::Framing => "framing",
80 Self::Exploration => "exploration",
81 Self::Tension => "tension",
82 Self::Convergence => "convergence",
83 Self::Commitment => "commitment",
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct FlowGatePrincipal {
91 pub id: PrincipalId,
92 pub authority: AuthorityLevel,
93 pub domains: Vec<DomainId>,
94 pub policy_version: Option<PolicyVersionId>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct FlowGateResource {
100 pub id: ResourceId,
101 pub kind: ResourceKind,
102 pub phase: FlowPhase,
103 pub gates_passed: Vec<GateId>,
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
108pub struct FlowGateContext {
109 pub commitment_type: Option<String>,
110 pub amount: Option<i64>,
111 pub human_approval_present: Option<bool>,
112 pub required_gates_met: Option<bool>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct FlowGateInput {
118 pub principal: FlowGatePrincipal,
119 pub resource: FlowGateResource,
120 pub action: FlowAction,
121 pub context: FlowGateContext,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub enum FlowGateOutcome {
128 Promote,
129 Reject,
130 Escalate,
131}
132
133impl FlowGateOutcome {
134 #[must_use]
135 pub const fn is_allowed(self) -> bool {
136 matches!(self, Self::Promote)
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct FlowGateDecision {
143 pub outcome: FlowGateOutcome,
144 pub reason: Option<String>,
145 pub source: Option<String>,
146}
147
148impl FlowGateDecision {
149 #[must_use]
150 pub fn promote(reason: Option<String>, source: Option<String>) -> Self {
151 Self {
152 outcome: FlowGateOutcome::Promote,
153 reason,
154 source,
155 }
156 }
157
158 #[must_use]
159 pub fn reject(reason: Option<String>, source: Option<String>) -> Self {
160 Self {
161 outcome: FlowGateOutcome::Reject,
162 reason,
163 source,
164 }
165 }
166
167 #[must_use]
168 pub fn escalate(reason: Option<String>, source: Option<String>) -> Self {
169 Self {
170 outcome: FlowGateOutcome::Escalate,
171 reason,
172 source,
173 }
174 }
175}
176
177#[derive(Debug, Error)]
179pub enum FlowGateError {
180 #[error("authorizer failed: {0}")]
181 Authorizer(String),
182 #[error("invalid flow gate input: {0}")]
183 InvalidInput(String),
184}
185
186pub trait FlowGateAuthorizer: Send + Sync {
188 fn decide(&self, input: &FlowGateInput) -> Result<FlowGateDecision, FlowGateError>;
190}
191
192#[derive(Debug, Default, Clone, Copy)]
194pub struct AllowAllFlowGateAuthorizer;
195
196impl FlowGateAuthorizer for AllowAllFlowGateAuthorizer {
197 fn decide(&self, _input: &FlowGateInput) -> Result<FlowGateDecision, FlowGateError> {
198 Ok(FlowGateDecision::promote(
199 Some("allow_all test authorizer".into()),
200 Some("allow_all".into()),
201 ))
202 }
203}
204
205#[derive(Debug, Default, Clone)]
207pub struct RejectAllFlowGateAuthorizer {
208 reason: Option<String>,
209}
210
211impl RejectAllFlowGateAuthorizer {
212 #[must_use]
213 pub fn with_reason(reason: impl Into<String>) -> Self {
214 Self {
215 reason: Some(reason.into()),
216 }
217 }
218}
219
220impl FlowGateAuthorizer for RejectAllFlowGateAuthorizer {
221 fn decide(&self, _input: &FlowGateInput) -> Result<FlowGateDecision, FlowGateError> {
222 Ok(FlowGateDecision::reject(
223 self.reason
224 .clone()
225 .or_else(|| Some("reject_all test authorizer".into())),
226 Some("reject_all".into()),
227 ))
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn sample_input() -> FlowGateInput {
236 FlowGateInput {
237 principal: FlowGatePrincipal {
238 id: "agent:test".into(),
239 authority: AuthorityLevel::Supervisory,
240 domains: vec!["finance".into()],
241 policy_version: Some("v1".into()),
242 },
243 resource: FlowGateResource {
244 id: "expense:1".into(),
245 kind: "expense".into(),
246 phase: FlowPhase::Commitment,
247 gates_passed: vec!["receipt".into()],
248 },
249 action: FlowAction::Validate,
250 context: FlowGateContext {
251 commitment_type: Some("expense".into()),
252 amount: Some(100),
253 human_approval_present: Some(false),
254 required_gates_met: Some(true),
255 },
256 }
257 }
258
259 #[test]
260 fn allow_all_authorizer_promotes() {
261 let decision = AllowAllFlowGateAuthorizer
262 .decide(&sample_input())
263 .expect("allow_all should succeed");
264 assert_eq!(decision.outcome, FlowGateOutcome::Promote);
265 }
266
267 #[test]
268 fn reject_all_authorizer_rejects() {
269 let decision = RejectAllFlowGateAuthorizer::with_reason("blocked")
270 .decide(&sample_input())
271 .expect("reject_all should succeed");
272 assert_eq!(decision.outcome, FlowGateOutcome::Reject);
273 assert_eq!(decision.reason.as_deref(), Some("blocked"));
274 }
275
276 #[test]
279 fn flow_action_as_str_all_variants() {
280 assert_eq!(FlowAction::Propose.as_str(), "propose");
281 assert_eq!(FlowAction::Validate.as_str(), "validate");
282 assert_eq!(FlowAction::Promote.as_str(), "promote");
283 assert_eq!(FlowAction::Commit.as_str(), "commit");
284 assert_eq!(FlowAction::AdvancePhase.as_str(), "advance_phase");
285 }
286
287 #[test]
288 fn flow_action_serde_roundtrip() {
289 let actions = [
290 FlowAction::Propose,
291 FlowAction::Validate,
292 FlowAction::Promote,
293 FlowAction::Commit,
294 FlowAction::AdvancePhase,
295 ];
296 for action in actions {
297 let json = serde_json::to_string(&action).unwrap();
298 let back: FlowAction = serde_json::from_str(&json).unwrap();
299 assert_eq!(back, action);
300 assert_eq!(json, format!("\"{}\"", action.as_str()));
301 }
302 }
303
304 #[test]
307 fn authority_level_as_str_all_variants() {
308 assert_eq!(AuthorityLevel::Advisory.as_str(), "advisory");
309 assert_eq!(AuthorityLevel::Supervisory.as_str(), "supervisory");
310 assert_eq!(AuthorityLevel::Participatory.as_str(), "participatory");
311 assert_eq!(AuthorityLevel::Sovereign.as_str(), "sovereign");
312 }
313
314 #[test]
315 fn authority_level_serde_roundtrip() {
316 let levels = [
317 AuthorityLevel::Advisory,
318 AuthorityLevel::Supervisory,
319 AuthorityLevel::Participatory,
320 AuthorityLevel::Sovereign,
321 ];
322 for level in levels {
323 let json = serde_json::to_string(&level).unwrap();
324 let back: AuthorityLevel = serde_json::from_str(&json).unwrap();
325 assert_eq!(back, level);
326 }
327 }
328
329 #[test]
332 fn flow_phase_as_str_all_variants() {
333 assert_eq!(FlowPhase::Intent.as_str(), "intent");
334 assert_eq!(FlowPhase::Framing.as_str(), "framing");
335 assert_eq!(FlowPhase::Exploration.as_str(), "exploration");
336 assert_eq!(FlowPhase::Tension.as_str(), "tension");
337 assert_eq!(FlowPhase::Convergence.as_str(), "convergence");
338 assert_eq!(FlowPhase::Commitment.as_str(), "commitment");
339 }
340
341 #[test]
342 fn flow_phase_serde_roundtrip() {
343 let phases = [
344 FlowPhase::Intent,
345 FlowPhase::Framing,
346 FlowPhase::Exploration,
347 FlowPhase::Tension,
348 FlowPhase::Convergence,
349 FlowPhase::Commitment,
350 ];
351 for phase in phases {
352 let json = serde_json::to_string(&phase).unwrap();
353 let back: FlowPhase = serde_json::from_str(&json).unwrap();
354 assert_eq!(back, phase);
355 }
356 }
357
358 #[test]
361 fn outcome_is_allowed() {
362 assert!(FlowGateOutcome::Promote.is_allowed());
363 assert!(!FlowGateOutcome::Reject.is_allowed());
364 assert!(!FlowGateOutcome::Escalate.is_allowed());
365 }
366
367 #[test]
370 fn decision_promote_factory() {
371 let d = FlowGateDecision::promote(Some("approved".into()), Some("policy:1".into()));
372 assert_eq!(d.outcome, FlowGateOutcome::Promote);
373 assert_eq!(d.reason.as_deref(), Some("approved"));
374 assert_eq!(d.source.as_deref(), Some("policy:1"));
375 }
376
377 #[test]
378 fn decision_reject_factory() {
379 let d = FlowGateDecision::reject(None, None);
380 assert_eq!(d.outcome, FlowGateOutcome::Reject);
381 assert!(d.reason.is_none());
382 assert!(d.source.is_none());
383 }
384
385 #[test]
386 fn decision_escalate_factory() {
387 let d = FlowGateDecision::escalate(Some("needs human".into()), Some("hitl".into()));
388 assert_eq!(d.outcome, FlowGateOutcome::Escalate);
389 assert_eq!(d.reason.as_deref(), Some("needs human"));
390 }
391
392 #[test]
395 fn flow_gate_error_display() {
396 let e = FlowGateError::Authorizer("connection reset".into());
397 assert!(e.to_string().contains("connection reset"));
398
399 let e = FlowGateError::InvalidInput("missing principal".into());
400 assert!(e.to_string().contains("missing principal"));
401 }
402
403 #[test]
404 fn flow_gate_error_is_std_error() {
405 let e: Box<dyn std::error::Error> = Box::new(FlowGateError::Authorizer("test".into()));
406 assert!(e.to_string().contains("test"));
407 }
408
409 #[test]
412 fn flow_gate_input_serde_roundtrip() {
413 let input = sample_input();
414 let json = serde_json::to_string(&input).unwrap();
415 let back: FlowGateInput = serde_json::from_str(&json).unwrap();
416 assert_eq!(back.action, FlowAction::Validate);
417 assert_eq!(back.principal.authority, AuthorityLevel::Supervisory);
418 assert_eq!(back.resource.phase, FlowPhase::Commitment);
419 assert_eq!(back.context.amount, Some(100));
420 }
421
422 #[test]
425 fn reject_all_default_reason() {
426 let authorizer = RejectAllFlowGateAuthorizer::default();
427 let decision = authorizer.decide(&sample_input()).unwrap();
428 assert_eq!(decision.outcome, FlowGateOutcome::Reject);
429 assert!(decision.reason.as_deref().unwrap().contains("reject_all"));
430 }
431}