Skip to main content

converge_optimization/packs/
traits.rs

1//! Pack traits and core abstractions
2
3use crate::Result;
4use crate::gate::{ProblemSpec, PromotionGate, ProposedPlan, SolverReport, Violation};
5use serde::{Serialize, de::DeserializeOwned};
6
7/// A domain pack for the solver gate
8///
9/// Packs define the contract for a specific optimization domain,
10/// including input validation, solving, invariant checking, and
11/// gate evaluation.
12pub trait Pack: Send + Sync {
13    /// Pack name (e.g., "meeting-scheduler")
14    fn name(&self) -> &'static str;
15
16    /// Pack version
17    fn version(&self) -> &'static str;
18
19    /// Validate and deserialize the input payload
20    fn validate_inputs(&self, inputs: &serde_json::Value) -> Result<()>;
21
22    /// Get invariant definitions for this pack
23    fn invariants(&self) -> &[InvariantDef];
24
25    /// Solve the problem and return a proposed plan
26    fn solve(&self, spec: &ProblemSpec) -> Result<PackSolveResult>;
27
28    /// Check invariants against a proposed plan
29    fn check_invariants(&self, plan: &ProposedPlan) -> Result<Vec<InvariantResult>>;
30
31    /// Evaluate promotion gate based on plan and invariant results
32    fn evaluate_gate(
33        &self,
34        plan: &ProposedPlan,
35        invariant_results: &[InvariantResult],
36    ) -> PromotionGate;
37}
38
39/// A solver within a pack
40///
41/// Each pack can have multiple solvers (e.g., greedy, exact, heuristic).
42pub trait PackSolver: Send + Sync {
43    /// Solver identifier (e.g., "greedy-v1")
44    fn id(&self) -> &'static str;
45
46    /// Solve and produce plan payload + report
47    fn solve(&self, spec: &ProblemSpec) -> Result<(serde_json::Value, SolverReport)>;
48
49    /// Whether this solver guarantees optimality
50    fn is_exact(&self) -> bool;
51}
52
53/// Result of pack solving
54#[derive(Debug)]
55pub struct PackSolveResult {
56    /// The proposed plan
57    pub plan: ProposedPlan,
58    /// Solver reports (may have tried multiple solvers)
59    pub reports: Vec<SolverReport>,
60}
61
62impl PackSolveResult {
63    /// Create a new solve result
64    pub fn new(plan: ProposedPlan, report: SolverReport) -> Self {
65        Self {
66            plan,
67            reports: vec![report],
68        }
69    }
70
71    /// Create with multiple reports
72    pub fn with_reports(plan: ProposedPlan, reports: Vec<SolverReport>) -> Self {
73        Self { plan, reports }
74    }
75
76    /// Get the primary (first) report
77    pub fn primary_report(&self) -> Option<&SolverReport> {
78        self.reports.first()
79    }
80
81    /// Check if any solver found a feasible solution
82    pub fn is_feasible(&self) -> bool {
83        self.reports.iter().any(|r| r.feasible)
84    }
85}
86
87/// Definition of an invariant
88#[derive(Debug, Clone)]
89pub struct InvariantDef {
90    /// Invariant name
91    pub name: String,
92    /// Human-readable description
93    pub description: String,
94    /// Whether this is critical (blocks promotion if violated)
95    pub critical: bool,
96}
97
98impl InvariantDef {
99    /// Create a critical invariant (blocks promotion if violated)
100    pub fn critical(name: impl Into<String>, description: impl Into<String>) -> Self {
101        Self {
102            name: name.into(),
103            description: description.into(),
104            critical: true,
105        }
106    }
107
108    /// Create a non-critical (advisory) invariant
109    pub fn advisory(name: impl Into<String>, description: impl Into<String>) -> Self {
110        Self {
111            name: name.into(),
112            description: description.into(),
113            critical: false,
114        }
115    }
116}
117
118/// Result of checking an invariant
119#[derive(Debug, Clone)]
120pub struct InvariantResult {
121    /// Which invariant was checked
122    pub invariant: String,
123    /// Whether it passed
124    pub passed: bool,
125    /// Violation details if failed
126    pub violation: Option<Violation>,
127}
128
129impl InvariantResult {
130    /// Create a passing result
131    pub fn pass(invariant: impl Into<String>) -> Self {
132        Self {
133            invariant: invariant.into(),
134            passed: true,
135            violation: None,
136        }
137    }
138
139    /// Create a failing result
140    pub fn fail(invariant: impl Into<String>, violation: Violation) -> Self {
141        Self {
142            invariant: invariant.into(),
143            passed: false,
144            violation: Some(violation),
145        }
146    }
147
148    /// Check if this is a critical failure
149    pub fn is_critical_failure(&self, invariants: &[InvariantDef]) -> bool {
150        if self.passed {
151            return false;
152        }
153        invariants
154            .iter()
155            .find(|i| i.name == self.invariant)
156            .map(|i| i.critical)
157            .unwrap_or(false)
158    }
159}
160
161/// Schema trait for typed pack inputs/outputs
162///
163/// Implement this for your pack's input and output types to get
164/// automatic validation and JSON conversion.
165pub trait PackSchema: Sized + Serialize + DeserializeOwned {
166    /// Validate the schema
167    fn validate(&self) -> Result<()>;
168
169    /// Convert to JSON value
170    fn to_json(&self) -> Result<serde_json::Value> {
171        serde_json::to_value(self).map_err(|e| crate::Error::invalid_input(e.to_string()))
172    }
173
174    /// Parse from JSON value
175    fn from_json(value: &serde_json::Value) -> Result<Self> {
176        serde_json::from_value(value.clone())
177            .map_err(|e| crate::Error::invalid_input(e.to_string()))
178    }
179}
180
181/// Helper to evaluate gate based on invariant results
182pub fn default_gate_evaluation(
183    invariant_results: &[InvariantResult],
184    invariant_defs: &[InvariantDef],
185) -> PromotionGate {
186    // Check for critical failures
187    let critical_failures: Vec<_> = invariant_results
188        .iter()
189        .filter(|r| r.is_critical_failure(invariant_defs))
190        .collect();
191
192    if !critical_failures.is_empty() {
193        let failed_names: Vec<_> = critical_failures
194            .iter()
195            .map(|r| r.invariant.as_str())
196            .collect();
197        return PromotionGate::reject(format!(
198            "Critical invariant(s) violated: {}",
199            failed_names.join(", ")
200        ));
201    }
202
203    // Check for any failures (advisory)
204    let advisory_failures: Vec<_> = invariant_results.iter().filter(|r| !r.passed).collect();
205
206    if !advisory_failures.is_empty() {
207        let failed_names: Vec<_> = advisory_failures
208            .iter()
209            .map(|r| r.invariant.as_str())
210            .collect();
211        return PromotionGate::requires_review(
212            failed_names.iter().map(|s| s.to_string()).collect(),
213            format!(
214                "Advisory invariant(s) violated: {}",
215                failed_names.join(", ")
216            ),
217        );
218    }
219
220    // All passed
221    PromotionGate::auto_promote("All invariants passed")
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_invariant_def() {
230        let critical = InvariantDef::critical("cap", "capacity check");
231        assert!(critical.critical);
232
233        let advisory = InvariantDef::advisory("pref", "preference check");
234        assert!(!advisory.critical);
235    }
236
237    #[test]
238    fn test_invariant_result() {
239        let pass = InvariantResult::pass("test");
240        assert!(pass.passed);
241        assert!(pass.violation.is_none());
242
243        let fail = InvariantResult::fail("test", Violation::new("test", 1.0, "failed"));
244        assert!(!fail.passed);
245        assert!(fail.violation.is_some());
246    }
247
248    #[test]
249    fn test_critical_failure_detection() {
250        let invariants = vec![
251            InvariantDef::critical("critical_one", "must pass"),
252            InvariantDef::advisory("advisory_one", "nice to have"),
253        ];
254
255        let critical_fail = InvariantResult::fail(
256            "critical_one",
257            Violation::new("critical_one", 1.0, "failed"),
258        );
259        assert!(critical_fail.is_critical_failure(&invariants));
260
261        let advisory_fail = InvariantResult::fail(
262            "advisory_one",
263            Violation::new("advisory_one", 0.5, "failed"),
264        );
265        assert!(!advisory_fail.is_critical_failure(&invariants));
266    }
267
268    #[test]
269    fn test_default_gate_evaluation() {
270        let invariants = vec![
271            InvariantDef::critical("must_pass", "critical"),
272            InvariantDef::advisory("nice_to_have", "advisory"),
273        ];
274
275        // All pass
276        let results = vec![
277            InvariantResult::pass("must_pass"),
278            InvariantResult::pass("nice_to_have"),
279        ];
280        let gate = default_gate_evaluation(&results, &invariants);
281        assert!(gate.is_promoted());
282
283        // Critical failure
284        let results = vec![
285            InvariantResult::fail("must_pass", Violation::new("must_pass", 1.0, "failed")),
286            InvariantResult::pass("nice_to_have"),
287        ];
288        let gate = default_gate_evaluation(&results, &invariants);
289        assert!(gate.is_rejected());
290
291        // Advisory failure only
292        let results = vec![
293            InvariantResult::pass("must_pass"),
294            InvariantResult::fail(
295                "nice_to_have",
296                Violation::new("nice_to_have", 0.5, "failed"),
297            ),
298        ];
299        let gate = default_gate_evaluation(&results, &invariants);
300        assert!(gate.requires_escalation());
301    }
302}