1use crate::gate::{
4 GateError, GateResult, ProblemSpec, PromotionGate, ProposedPlan, SolverReport, Violation,
5};
6use serde::{Serialize, de::DeserializeOwned};
7
8pub trait Pack: Send + Sync {
14 fn name(&self) -> &'static str;
16
17 fn version(&self) -> &'static str;
19
20 fn validate_inputs(&self, inputs: &serde_json::Value) -> GateResult<()>;
22
23 fn invariants(&self) -> &[InvariantDef];
25
26 fn solve(&self, spec: &ProblemSpec) -> GateResult<PackSolveResult>;
28
29 fn check_invariants(&self, plan: &ProposedPlan) -> GateResult<Vec<InvariantResult>>;
31
32 fn evaluate_gate(
34 &self,
35 plan: &ProposedPlan,
36 invariant_results: &[InvariantResult],
37 ) -> PromotionGate;
38}
39
40pub trait PackSolver: Send + Sync {
44 fn id(&self) -> &'static str;
46
47 fn solve(&self, spec: &ProblemSpec) -> GateResult<(serde_json::Value, SolverReport)>;
49
50 fn is_exact(&self) -> bool;
52}
53
54#[derive(Debug)]
56pub struct PackSolveResult {
57 pub plan: ProposedPlan,
59 pub reports: Vec<SolverReport>,
61}
62
63impl PackSolveResult {
64 pub fn new(plan: ProposedPlan, report: SolverReport) -> Self {
66 Self {
67 plan,
68 reports: vec![report],
69 }
70 }
71
72 pub fn with_reports(plan: ProposedPlan, reports: Vec<SolverReport>) -> Self {
74 Self { plan, reports }
75 }
76
77 pub fn primary_report(&self) -> Option<&SolverReport> {
79 self.reports.first()
80 }
81
82 pub fn is_feasible(&self) -> bool {
84 self.reports.iter().any(|r| r.feasible)
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct InvariantDef {
91 pub name: String,
93 pub description: String,
95 pub critical: bool,
97}
98
99impl InvariantDef {
100 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 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#[derive(Debug, Clone)]
121pub struct InvariantResult {
122 pub invariant: String,
124 pub passed: bool,
126 pub violation: Option<Violation>,
128}
129
130impl InvariantResult {
131 pub fn pass(invariant: impl Into<String>) -> Self {
133 Self {
134 invariant: invariant.into(),
135 passed: true,
136 violation: None,
137 }
138 }
139
140 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 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
162pub trait PackSchema: Sized + Serialize + DeserializeOwned {
167 fn validate(&self) -> GateResult<()>;
169
170 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 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
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}