Skip to main content

converge_core/
truth.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! Truth catalog primitives.
5//!
6//! Truths describe jobs, policies, and invariants above domain packs.
7//! Applications provide the catalog content; the runtime consumes a common
8//! shape for intent construction, guardrails, and pack participation.
9
10use serde::{Deserialize, Serialize};
11
12use crate::{ApprovalPointId, Context, Criterion, FactId, PackId, TruthId, TypesIntentConstraint};
13
14/// What class of truth is being described.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum TruthKind {
17    /// A job-to-be-done spanning multiple packs.
18    Job,
19    /// A cross-cutting policy or guardrail.
20    Policy,
21    /// A module-local or pack-local invariant.
22    Invariant,
23}
24
25/// Portable truth definition.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct TruthDefinition {
28    /// Stable truth identifier.
29    pub key: TruthId,
30    /// Truth class.
31    pub kind: TruthKind,
32    /// Human-readable summary.
33    pub summary: String,
34    /// Required or optional success criteria.
35    pub success_criteria: Vec<Criterion>,
36    /// Hard and soft constraints derived from the truth.
37    pub constraints: Vec<TypesIntentConstraint>,
38    /// Human approval points that the runtime must respect.
39    pub approval_points: Vec<ApprovalPointId>,
40    /// Which packs should participate when this truth is active.
41    pub participating_packs: Vec<PackId>,
42}
43
44/// Machine-evaluable outcome for a single criterion.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub enum CriterionResult {
47    /// The criterion was satisfied, with fact IDs that justify the result.
48    Met { evidence: Vec<FactId> },
49    /// The criterion is currently blocked on human intervention.
50    Blocked {
51        /// Why the criterion is blocked.
52        reason: String,
53        /// Optional approval or workflow reference the host can surface.
54        approval_ref: Option<ApprovalPointId>,
55    },
56    /// The criterion was evaluated and is not satisfied.
57    Unmet { reason: String },
58    /// The runtime could not determine whether the criterion was satisfied.
59    Indeterminate,
60}
61
62/// Evaluated outcome for a specific criterion.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct CriterionOutcome {
65    /// The criterion that was evaluated.
66    pub criterion: Criterion,
67    /// The result of the evaluation.
68    pub result: CriterionResult,
69}
70
71/// Application-provided boundary for evaluating success criteria.
72pub trait CriterionEvaluator: Send + Sync {
73    /// Evaluate a criterion against the converged context.
74    fn evaluate(&self, criterion: &Criterion, context: &dyn Context) -> CriterionResult;
75}
76
77/// Application-provided truth catalog boundary.
78pub trait TruthCatalog: Send + Sync {
79    /// List all truths known to the application.
80    fn list_truths(&self) -> Vec<TruthDefinition>;
81
82    /// Resolve a truth by key.
83    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}