converge_optimization/packs/
traits.rs1use crate::Result;
4use crate::gate::{ProblemSpec, PromotionGate, ProposedPlan, SolverReport, Violation};
5use serde::{Serialize, de::DeserializeOwned};
6
7pub trait Pack: Send + Sync {
13 fn name(&self) -> &'static str;
15
16 fn version(&self) -> &'static str;
18
19 fn validate_inputs(&self, inputs: &serde_json::Value) -> Result<()>;
21
22 fn invariants(&self) -> &[InvariantDef];
24
25 fn solve(&self, spec: &ProblemSpec) -> Result<PackSolveResult>;
27
28 fn check_invariants(&self, plan: &ProposedPlan) -> Result<Vec<InvariantResult>>;
30
31 fn evaluate_gate(
33 &self,
34 plan: &ProposedPlan,
35 invariant_results: &[InvariantResult],
36 ) -> PromotionGate;
37}
38
39pub trait PackSolver: Send + Sync {
43 fn id(&self) -> &'static str;
45
46 fn solve(&self, spec: &ProblemSpec) -> Result<(serde_json::Value, SolverReport)>;
48
49 fn is_exact(&self) -> bool;
51}
52
53#[derive(Debug)]
55pub struct PackSolveResult {
56 pub plan: ProposedPlan,
58 pub reports: Vec<SolverReport>,
60}
61
62impl PackSolveResult {
63 pub fn new(plan: ProposedPlan, report: SolverReport) -> Self {
65 Self {
66 plan,
67 reports: vec![report],
68 }
69 }
70
71 pub fn with_reports(plan: ProposedPlan, reports: Vec<SolverReport>) -> Self {
73 Self { plan, reports }
74 }
75
76 pub fn primary_report(&self) -> Option<&SolverReport> {
78 self.reports.first()
79 }
80
81 pub fn is_feasible(&self) -> bool {
83 self.reports.iter().any(|r| r.feasible)
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct InvariantDef {
90 pub name: String,
92 pub description: String,
94 pub critical: bool,
96}
97
98impl InvariantDef {
99 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 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#[derive(Debug, Clone)]
120pub struct InvariantResult {
121 pub invariant: String,
123 pub passed: bool,
125 pub violation: Option<Violation>,
127}
128
129impl InvariantResult {
130 pub fn pass(invariant: impl Into<String>) -> Self {
132 Self {
133 invariant: invariant.into(),
134 passed: true,
135 violation: None,
136 }
137 }
138
139 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 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
161pub trait PackSchema: Sized + Serialize + DeserializeOwned {
166 fn validate(&self) -> Result<()>;
168
169 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 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
181pub fn default_gate_evaluation(
183 invariant_results: &[InvariantResult],
184 invariant_defs: &[InvariantDef],
185) -> PromotionGate {
186 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 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 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 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 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 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}