converge_optimization/packs/vendor_shortlist/
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::gate::GateResult as Result;
33use converge_pack::gate::{KernelTraceLink, ProblemSpec, PromotionGate, ProposedPlan};
34use converge_pack::{CONFIDENCE_STEP_MAJOR, CONFIDENCE_STEP_MINOR};
35
36pub struct VendorShortlistPack;
38
39impl Pack for VendorShortlistPack {
40 fn name(&self) -> &'static str {
41 "vendor-shortlist"
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: VendorShortlistInput = 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: VendorShortlistInput = spec.inputs_as()?;
61 input.validate()?;
62
63 let solver = ScoreRankingSolver;
64 let (output, report) = solver.solve_shortlist(&input, spec)?;
65
66 let trace = KernelTraceLink::audit_only(format!("trace-{}", spec.problem_id));
67 let confidence = calculate_confidence(&output, &input);
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: VendorShortlistOutput = 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: &VendorShortlistOutput, input: &VendorShortlistInput) -> f64 {
96 if output.shortlist.is_empty() {
97 return 0.0;
98 }
99
100 let mut confidence: f64 = 0.5;
101
102 if output.shortlist.len() >= 2 {
104 confidence += CONFIDENCE_STEP_MAJOR;
105 }
106
107 if output.shortlist.len() == input.requirements.max_vendors {
109 confidence += CONFIDENCE_STEP_MINOR;
110 }
111
112 if output.stats.average_score >= 80.0 {
114 confidence += CONFIDENCE_STEP_MAJOR;
115 } else if output.stats.average_score >= 70.0 {
116 confidence += CONFIDENCE_STEP_MINOR;
117 }
118
119 confidence.min(1.0)
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use converge_pack::gate::ObjectiveSpec;
126
127 fn create_test_input() -> VendorShortlistInput {
128 VendorShortlistInput {
129 vendors: vec![
130 Vendor {
131 id: "v1".to_string(),
132 name: "Acme Corp".to_string(),
133 score: 85.0,
134 risk_score: 20.0,
135 compliance_status: "compliant".to_string(),
136 certifications: vec!["ISO9001".to_string()],
137 },
138 Vendor {
139 id: "v2".to_string(),
140 name: "BetaCo".to_string(),
141 score: 75.0,
142 risk_score: 15.0,
143 compliance_status: "compliant".to_string(),
144 certifications: vec!["ISO9001".to_string()],
145 },
146 ],
147 requirements: ShortlistRequirements {
148 max_vendors: 3,
149 min_score: 50.0,
150 max_risk_score: 50.0,
151 required_certifications: vec![],
152 },
153 }
154 }
155
156 #[test]
157 fn test_pack_name() {
158 let pack = VendorShortlistPack;
159 assert_eq!(pack.name(), "vendor-shortlist");
160 assert_eq!(pack.version(), "1.0.0");
161 }
162
163 #[test]
164 fn test_validate_inputs() {
165 let pack = VendorShortlistPack;
166 let input = create_test_input();
167 let json = serde_json::to_value(&input).unwrap();
168 assert!(pack.validate_inputs(&json).is_ok());
169 }
170
171 #[test]
172 fn test_solve_basic() {
173 let pack = VendorShortlistPack;
174 let input = create_test_input();
175
176 let spec = ProblemSpec::builder("test-001", "test-tenant")
177 .objective(ObjectiveSpec::maximize("score"))
178 .inputs(&input)
179 .unwrap()
180 .seed(42)
181 .build()
182 .unwrap();
183
184 let result = pack.solve(&spec).unwrap();
185 assert!(result.is_feasible());
186
187 let output: VendorShortlistOutput = result.plan.plan_as().unwrap();
188 assert_eq!(output.shortlist.len(), 2);
189 }
190
191 #[test]
192 fn test_check_invariants() {
193 let pack = VendorShortlistPack;
194 let input = create_test_input();
195
196 let spec = ProblemSpec::builder("test-002", "test-tenant")
197 .objective(ObjectiveSpec::maximize("score"))
198 .inputs(&input)
199 .unwrap()
200 .seed(42)
201 .build()
202 .unwrap();
203
204 let result = pack.solve(&spec).unwrap();
205 let invariants = pack.check_invariants(&result.plan).unwrap();
206
207 let all_pass = invariants.iter().all(|r| r.passed);
208 assert!(all_pass);
209 }
210
211 #[test]
212 fn test_gate_promotes() {
213 let pack = VendorShortlistPack;
214 let input = create_test_input();
215
216 let spec = ProblemSpec::builder("test-003", "test-tenant")
217 .objective(ObjectiveSpec::maximize("score"))
218 .inputs(&input)
219 .unwrap()
220 .seed(42)
221 .build()
222 .unwrap();
223
224 let result = pack.solve(&spec).unwrap();
225 let invariants = pack.check_invariants(&result.plan).unwrap();
226 let gate = pack.evaluate_gate(&result.plan, &invariants);
227
228 assert!(gate.is_promoted());
229 }
230
231 #[test]
232 fn test_determinism() {
233 let pack = VendorShortlistPack;
234 let input = create_test_input();
235
236 let spec1 = ProblemSpec::builder("test-a", "tenant")
237 .objective(ObjectiveSpec::maximize("score"))
238 .inputs(&input)
239 .unwrap()
240 .seed(99999)
241 .build()
242 .unwrap();
243
244 let spec2 = ProblemSpec::builder("test-b", "tenant")
245 .objective(ObjectiveSpec::maximize("score"))
246 .inputs(&input)
247 .unwrap()
248 .seed(99999)
249 .build()
250 .unwrap();
251
252 let result1 = pack.solve(&spec1).unwrap();
253 let result2 = pack.solve(&spec2).unwrap();
254
255 let output1: VendorShortlistOutput = result1.plan.plan_as().unwrap();
256 let output2: VendorShortlistOutput = result2.plan.plan_as().unwrap();
257
258 assert_eq!(output1.shortlist.len(), output2.shortlist.len());
259 for (a, b) in output1.shortlist.iter().zip(output2.shortlist.iter()) {
260 assert_eq!(a.vendor_id, b.vendor_id);
261 }
262 }
263}