Skip to main content

converge_pack/
pack.rs

1//! Pack traits and core abstractions
2
3use crate::gate::{
4    GateError, GateResult, ProblemSpec, PromotionGate, ProposedPlan, SolverReport, Violation,
5};
6use serde::{Serialize, de::DeserializeOwned};
7
8/// A domain pack for the solver gate
9///
10/// Packs define the contract for a specific optimization domain,
11/// including input validation, solving, invariant checking, and
12/// gate evaluation.
13pub trait Pack: Send + Sync {
14    /// Pack name (e.g., "meeting-scheduler")
15    fn name(&self) -> &'static str;
16
17    /// Pack version
18    fn version(&self) -> &'static str;
19
20    /// Validate and deserialize the input payload
21    fn validate_inputs(&self, inputs: &serde_json::Value) -> GateResult<()>;
22
23    /// Get invariant definitions for this pack
24    fn invariants(&self) -> &[InvariantDef];
25
26    /// Solve the problem and return a proposed plan
27    fn solve(&self, spec: &ProblemSpec) -> GateResult<PackSolveResult>;
28
29    /// Check invariants against a proposed plan
30    fn check_invariants(&self, plan: &ProposedPlan) -> GateResult<Vec<InvariantResult>>;
31
32    /// Evaluate promotion gate based on plan and invariant results
33    fn evaluate_gate(
34        &self,
35        plan: &ProposedPlan,
36        invariant_results: &[InvariantResult],
37    ) -> PromotionGate;
38}
39
40/// A solver within a pack
41///
42/// Each pack can have multiple solvers (e.g., greedy, exact, heuristic).
43pub trait PackSolver: Send + Sync {
44    /// Solver identifier (e.g., "greedy-v1")
45    fn id(&self) -> &'static str;
46
47    /// Solve and produce plan payload + report
48    fn solve(&self, spec: &ProblemSpec) -> GateResult<(serde_json::Value, SolverReport)>;
49
50    /// Whether this solver guarantees optimality
51    fn is_exact(&self) -> bool;
52}
53
54/// Result of pack solving
55#[derive(Debug)]
56pub struct PackSolveResult {
57    /// The proposed plan
58    pub plan: ProposedPlan,
59    /// Solver reports (may have tried multiple solvers)
60    pub reports: Vec<SolverReport>,
61}
62
63impl PackSolveResult {
64    /// Create a new solve result
65    pub fn new(plan: ProposedPlan, report: SolverReport) -> Self {
66        Self {
67            plan,
68            reports: vec![report],
69        }
70    }
71
72    /// Create with multiple reports
73    pub fn with_reports(plan: ProposedPlan, reports: Vec<SolverReport>) -> Self {
74        Self { plan, reports }
75    }
76
77    /// Get the primary (first) report
78    pub fn primary_report(&self) -> Option<&SolverReport> {
79        self.reports.first()
80    }
81
82    /// Check if any solver found a feasible solution
83    pub fn is_feasible(&self) -> bool {
84        self.reports.iter().any(|r| r.feasible)
85    }
86}
87
88/// Definition of an invariant
89#[derive(Debug, Clone)]
90pub struct InvariantDef {
91    /// Invariant name
92    pub name: String,
93    /// Human-readable description
94    pub description: String,
95    /// Whether this is critical (blocks promotion if violated)
96    pub critical: bool,
97}
98
99impl InvariantDef {
100    /// Create a critical invariant (blocks promotion if violated)
101    pub fn critical(name: impl Into<String>, description: impl Into<String>) -> Self {
102        Self {
103            name: name.into(),
104            description: description.into(),
105            critical: true,
106        }
107    }
108
109    /// Create a non-critical (advisory) invariant
110    pub fn advisory(name: impl Into<String>, description: impl Into<String>) -> Self {
111        Self {
112            name: name.into(),
113            description: description.into(),
114            critical: false,
115        }
116    }
117}
118
119/// Result of checking an invariant
120#[derive(Debug, Clone)]
121pub struct InvariantResult {
122    /// Which invariant was checked
123    pub invariant: String,
124    /// Whether it passed
125    pub passed: bool,
126    /// Violation details if failed
127    pub violation: Option<Violation>,
128}
129
130impl InvariantResult {
131    /// Create a passing result
132    pub fn pass(invariant: impl Into<String>) -> Self {
133        Self {
134            invariant: invariant.into(),
135            passed: true,
136            violation: None,
137        }
138    }
139
140    /// Create a failing result
141    pub fn fail(invariant: impl Into<String>, violation: Violation) -> Self {
142        Self {
143            invariant: invariant.into(),
144            passed: false,
145            violation: Some(violation),
146        }
147    }
148
149    /// Check if this is a critical failure
150    pub fn is_critical_failure(&self, invariants: &[InvariantDef]) -> bool {
151        if self.passed {
152            return false;
153        }
154        invariants
155            .iter()
156            .find(|i| i.name == self.invariant)
157            .map(|i| i.critical)
158            .unwrap_or(false)
159    }
160}
161
162/// Schema trait for typed pack inputs/outputs
163///
164/// Implement this for your pack's input and output types to get
165/// automatic validation and JSON conversion.
166pub trait PackSchema: Sized + Serialize + DeserializeOwned {
167    /// Validate the schema
168    fn validate(&self) -> GateResult<()>;
169
170    /// Convert to JSON value
171    fn to_json(&self) -> GateResult<serde_json::Value> {
172        serde_json::to_value(self).map_err(|e| GateError::invalid_input(e.to_string()))
173    }
174
175    /// Parse from JSON value
176    fn from_json(value: &serde_json::Value) -> GateResult<Self> {
177        serde_json::from_value(value.clone()).map_err(|e| GateError::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}