converge_optimization/packs/shipping_choice/
mod.rs1mod invariants;
24mod solver;
25mod types;
26
27pub use invariants::*;
28pub use solver::*;
29pub use types::*;
30
31use crate::packs::{InvariantDef, InvariantResult, Pack, PackSolveResult, default_gate_evaluation};
32use converge_pack::CONFIDENCE_STEP_MINOR;
33use converge_pack::gate::GateResult as Result;
34use converge_pack::gate::{KernelTraceLink, ProblemSpec, PromotionGate, ProposedPlan};
35
36pub struct ShippingChoicePack;
38
39impl Pack for ShippingChoicePack {
40 fn name(&self) -> &'static str {
41 "shipping-choice"
42 }
43
44 fn version(&self) -> &'static str {
45 "1.0.0"
46 }
47
48 fn validate_inputs(&self, inputs: &serde_json::Value) -> Result<()> {
49 let input: ShippingChoiceInput = serde_json::from_value(inputs.clone()).map_err(|e| {
50 converge_pack::GateError::invalid_input(format!("Invalid input: {}", e))
51 })?;
52 input.validate()
53 }
54
55 fn invariants(&self) -> &[InvariantDef] {
56 INVARIANTS
57 }
58
59 fn solve(&self, spec: &ProblemSpec) -> Result<PackSolveResult> {
60 let input: ShippingChoiceInput = spec.inputs_as()?;
61 input.validate()?;
62
63 let solver = CostMinimizingSolver;
64 let (output, report) = solver.solve_shipping(&input, spec)?;
65
66 let trace = KernelTraceLink::audit_only(format!("trace-{}", spec.problem_id));
67 let confidence = calculate_confidence(&output);
68
69 let plan = ProposedPlan::from_payload(
70 format!("plan-{}", spec.problem_id),
71 self.name(),
72 output.summary(),
73 &output,
74 confidence,
75 trace,
76 )?;
77
78 Ok(PackSolveResult::new(plan, report))
79 }
80
81 fn check_invariants(&self, plan: &ProposedPlan) -> Result<Vec<InvariantResult>> {
82 let output: ShippingChoiceOutput = plan.plan_as()?;
83 Ok(check_all_invariants(&output))
84 }
85
86 fn evaluate_gate(
87 &self,
88 _plan: &ProposedPlan,
89 invariant_results: &[InvariantResult],
90 ) -> PromotionGate {
91 default_gate_evaluation(invariant_results, self.invariants())
92 }
93}
94
95fn calculate_confidence(output: &ShippingChoiceOutput) -> f64 {
96 if output.selected_carrier.is_none() {
97 return 0.0;
98 }
99
100 let mut confidence: f64 = 0.6;
101
102 if output.meets_sla {
103 confidence += 0.3;
104 }
105
106 if !output.alternatives.is_empty() {
108 confidence += CONFIDENCE_STEP_MINOR;
109 }
110
111 confidence.min(1.0)
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use converge_pack::gate::ObjectiveSpec;
118
119 fn create_test_input() -> ShippingChoiceInput {
120 ShippingChoiceInput {
121 order: OrderDetails {
122 order_id: "ORD-001".to_string(),
123 weight_kg: 2.5,
124 dimensions_cm: [20.0, 15.0, 10.0],
125 destination_zip: "10001".to_string(),
126 is_hazmat: false,
127 },
128 carriers: vec![
129 CarrierOption {
130 carrier_id: "ups".to_string(),
131 service_level: "ground".to_string(),
132 cost: 8.99,
133 estimated_days: 5,
134 supports_hazmat: false,
135 },
136 CarrierOption {
137 carrier_id: "fedex".to_string(),
138 service_level: "express".to_string(),
139 cost: 15.99,
140 estimated_days: 2,
141 supports_hazmat: true,
142 },
143 ],
144 sla_days: 5,
145 }
146 }
147
148 #[test]
149 fn test_pack_name() {
150 let pack = ShippingChoicePack;
151 assert_eq!(pack.name(), "shipping-choice");
152 assert_eq!(pack.version(), "1.0.0");
153 }
154
155 #[test]
156 fn test_validate_inputs() {
157 let pack = ShippingChoicePack;
158 let input = create_test_input();
159 let json = serde_json::to_value(&input).unwrap();
160 assert!(pack.validate_inputs(&json).is_ok());
161 }
162
163 #[test]
164 fn test_solve_basic() {
165 let pack = ShippingChoicePack;
166 let input = create_test_input();
167
168 let spec = ProblemSpec::builder("test-001", "test-tenant")
169 .objective(ObjectiveSpec::minimize("cost"))
170 .inputs(&input)
171 .unwrap()
172 .seed(42)
173 .build()
174 .unwrap();
175
176 let result = pack.solve(&spec).unwrap();
177 assert!(result.is_feasible());
178
179 let output: ShippingChoiceOutput = result.plan.plan_as().unwrap();
180 assert!(output.selected_carrier.is_some());
181 assert_eq!(output.selected_carrier.as_deref(), Some("ups"));
182 }
183
184 #[test]
185 fn test_check_invariants() {
186 let pack = ShippingChoicePack;
187 let input = create_test_input();
188
189 let spec = ProblemSpec::builder("test-002", "test-tenant")
190 .objective(ObjectiveSpec::minimize("cost"))
191 .inputs(&input)
192 .unwrap()
193 .seed(42)
194 .build()
195 .unwrap();
196
197 let result = pack.solve(&spec).unwrap();
198 let invariants = pack.check_invariants(&result.plan).unwrap();
199
200 let all_pass = invariants.iter().all(|r| r.passed);
201 assert!(all_pass);
202 }
203
204 #[test]
205 fn test_gate_promotes() {
206 let pack = ShippingChoicePack;
207 let input = create_test_input();
208
209 let spec = ProblemSpec::builder("test-003", "test-tenant")
210 .objective(ObjectiveSpec::minimize("cost"))
211 .inputs(&input)
212 .unwrap()
213 .seed(42)
214 .build()
215 .unwrap();
216
217 let result = pack.solve(&spec).unwrap();
218 let invariants = pack.check_invariants(&result.plan).unwrap();
219 let gate = pack.evaluate_gate(&result.plan, &invariants);
220
221 assert!(gate.is_promoted());
222 }
223
224 #[test]
225 fn test_determinism() {
226 let pack = ShippingChoicePack;
227 let input = create_test_input();
228
229 let spec1 = ProblemSpec::builder("test-a", "tenant")
230 .objective(ObjectiveSpec::minimize("cost"))
231 .inputs(&input)
232 .unwrap()
233 .seed(99999)
234 .build()
235 .unwrap();
236
237 let spec2 = ProblemSpec::builder("test-b", "tenant")
238 .objective(ObjectiveSpec::minimize("cost"))
239 .inputs(&input)
240 .unwrap()
241 .seed(99999)
242 .build()
243 .unwrap();
244
245 let result1 = pack.solve(&spec1).unwrap();
246 let result2 = pack.solve(&spec2).unwrap();
247
248 let output1: ShippingChoiceOutput = result1.plan.plan_as().unwrap();
249 let output2: ShippingChoiceOutput = result2.plan.plan_as().unwrap();
250
251 assert_eq!(output1.selected_carrier, output2.selected_carrier);
252 assert_eq!(output1.cost, output2.cost);
253 }
254}