1use serde::{Deserialize, Serialize};
11
12use crate::{ApprovalPointId, Context, Criterion, FactId, PackId, TruthId, TypesIntentConstraint};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum TruthKind {
17 Job,
19 Policy,
21 Invariant,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct TruthDefinition {
28 pub key: TruthId,
30 pub kind: TruthKind,
32 pub summary: String,
34 pub success_criteria: Vec<Criterion>,
36 pub constraints: Vec<TypesIntentConstraint>,
38 pub approval_points: Vec<ApprovalPointId>,
40 pub participating_packs: Vec<PackId>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub enum CriterionResult {
47 Met { evidence: Vec<FactId> },
49 Blocked {
51 reason: String,
53 approval_ref: Option<ApprovalPointId>,
55 },
56 Unmet { reason: String },
58 Indeterminate,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct CriterionOutcome {
65 pub criterion: Criterion,
67 pub result: CriterionResult,
69}
70
71pub trait CriterionEvaluator: Send + Sync {
73 fn evaluate(&self, criterion: &Criterion, context: &dyn Context) -> CriterionResult;
75}
76
77pub trait TruthCatalog: Send + Sync {
79 fn list_truths(&self) -> Vec<TruthDefinition>;
81
82 fn find_truth(&self, key: &TruthId) -> Option<TruthDefinition> {
84 self.list_truths()
85 .into_iter()
86 .find(|truth| truth.key.as_str() == key.as_str())
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn truth_kind_equality() {
96 assert_eq!(TruthKind::Job, TruthKind::Job);
97 assert_ne!(TruthKind::Job, TruthKind::Policy);
98 assert_ne!(TruthKind::Policy, TruthKind::Invariant);
99 }
100
101 #[test]
102 fn truth_kind_serde_roundtrip() {
103 for kind in [TruthKind::Job, TruthKind::Policy, TruthKind::Invariant] {
104 let json = serde_json::to_string(&kind).unwrap();
105 let back: TruthKind = serde_json::from_str(&json).unwrap();
106 assert_eq!(kind, back);
107 }
108 }
109
110 #[test]
111 fn criterion_result_met_with_evidence() {
112 let result = CriterionResult::Met {
113 evidence: vec!["fact-1".into(), "fact-2".into()],
114 };
115 assert!(matches!(result, CriterionResult::Met { evidence } if evidence.len() == 2));
116 }
117
118 #[test]
119 fn criterion_result_blocked() {
120 let result = CriterionResult::Blocked {
121 reason: "needs approval".into(),
122 approval_ref: Some("approval:top-up".into()),
123 };
124 assert!(matches!(result, CriterionResult::Blocked { .. }));
125 }
126
127 #[test]
128 fn criterion_result_unmet() {
129 let result = CriterionResult::Unmet {
130 reason: "insufficient funds".into(),
131 };
132 assert!(matches!(result, CriterionResult::Unmet { reason } if reason.contains("funds")));
133 }
134
135 #[test]
136 fn criterion_result_indeterminate() {
137 let result = CriterionResult::Indeterminate;
138 assert!(matches!(result, CriterionResult::Indeterminate));
139 }
140
141 #[test]
142 fn criterion_result_serde_roundtrip() {
143 let variants = vec![
144 CriterionResult::Met {
145 evidence: vec!["e1".into()],
146 },
147 CriterionResult::Blocked {
148 reason: "wait".into(),
149 approval_ref: None,
150 },
151 CriterionResult::Unmet {
152 reason: "fail".into(),
153 },
154 CriterionResult::Indeterminate,
155 ];
156 for variant in variants {
157 let json = serde_json::to_string(&variant).unwrap();
158 let back: CriterionResult = serde_json::from_str(&json).unwrap();
159 assert_eq!(variant, back);
160 }
161 }
162
163 #[test]
164 fn truth_catalog_find_truth_default_impl() {
165 struct TestCatalog;
166 impl TruthCatalog for TestCatalog {
167 fn list_truths(&self) -> Vec<TruthDefinition> {
168 vec![
169 TruthDefinition {
170 key: "job:onboard".into(),
171 kind: TruthKind::Job,
172 summary: "Onboard a new employee".into(),
173 success_criteria: vec![],
174 constraints: vec![],
175 approval_points: vec![],
176 participating_packs: vec!["hr".into()],
177 },
178 TruthDefinition {
179 key: "policy:expense".into(),
180 kind: TruthKind::Policy,
181 summary: "Expense policy".into(),
182 success_criteria: vec![],
183 constraints: vec![],
184 approval_points: vec![],
185 participating_packs: vec![],
186 },
187 ]
188 }
189 }
190
191 let catalog = TestCatalog;
192 let found = catalog.find_truth(&TruthId::new("job:onboard"));
193 assert!(found.is_some());
194 assert_eq!(found.unwrap().kind, TruthKind::Job);
195
196 let not_found = catalog.find_truth(&TruthId::new("nonexistent"));
197 assert!(not_found.is_none());
198 }
199}