converge_optimization/packs/backlog_prioritization/
mod.rs1mod invariants;
24mod solver;
25mod types;
26
27pub use invariants::*;
28pub use solver::*;
29pub use types::*;
30
31use crate::Result;
32use crate::gate::{KernelTraceLink, ProblemSpec, PromotionGate, ProposedPlan};
33use crate::packs::{InvariantDef, InvariantResult, Pack, PackSolveResult, default_gate_evaluation};
34
35pub struct BacklogPrioritizationPack;
37
38impl Pack for BacklogPrioritizationPack {
39 fn name(&self) -> &'static str {
40 "backlog-prioritization"
41 }
42
43 fn version(&self) -> &'static str {
44 "1.0.0"
45 }
46
47 fn validate_inputs(&self, inputs: &serde_json::Value) -> Result<()> {
48 let input: BacklogPrioritizationInput = serde_json::from_value(inputs.clone())
49 .map_err(|e| crate::Error::invalid_input(format!("Invalid input: {}", e)))?;
50 input.validate()
51 }
52
53 fn invariants(&self) -> &[InvariantDef] {
54 INVARIANTS
55 }
56
57 fn solve(&self, spec: &ProblemSpec) -> Result<PackSolveResult> {
58 let input: BacklogPrioritizationInput = spec.inputs_as()?;
59 input.validate()?;
60
61 let solver = WsjfSolver;
62 let (output, report) = solver.solve_backlog(&input, spec)?;
63
64 let trace = KernelTraceLink::audit_only(format!("trace-{}", spec.problem_id));
65 let confidence = calculate_confidence(&output, &input);
66
67 let plan = ProposedPlan::from_payload(
68 format!("plan-{}", spec.problem_id),
69 self.name(),
70 output.summary(),
71 &output,
72 confidence,
73 trace,
74 )?;
75
76 Ok(PackSolveResult::new(plan, report))
77 }
78
79 fn check_invariants(&self, plan: &ProposedPlan) -> Result<Vec<InvariantResult>> {
80 let output: BacklogPrioritizationOutput = plan.plan_as()?;
81 Ok(check_all_invariants(&output))
82 }
83
84 fn evaluate_gate(
85 &self,
86 _plan: &ProposedPlan,
87 invariant_results: &[InvariantResult],
88 ) -> PromotionGate {
89 default_gate_evaluation(invariant_results, self.invariants())
90 }
91}
92
93fn calculate_confidence(
94 output: &BacklogPrioritizationOutput,
95 input: &BacklogPrioritizationInput,
96) -> f64 {
97 if output.ranked_items.is_empty() {
98 return 0.0;
99 }
100
101 let mut confidence: f64 = 0.5;
102
103 if output.included_count > 0 {
105 confidence += 0.2;
106 }
107
108 if output.total_effort > 0 {
110 let utilization = output.total_effort as f64 / input.capacity_points as f64;
111 if utilization >= 0.7 {
112 confidence += 0.2;
113 } else if utilization >= 0.5 {
114 confidence += 0.1;
115 }
116 }
117
118 if output.total_value >= 100.0 {
120 confidence += 0.1;
121 }
122
123 confidence.min(1.0)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::gate::ObjectiveSpec;
130
131 fn create_test_input() -> BacklogPrioritizationInput {
132 BacklogPrioritizationInput {
133 items: vec![
134 BacklogItem {
135 id: "feat-1".to_string(),
136 title: "Feature 1".to_string(),
137 business_value: 80.0,
138 time_criticality: 60.0,
139 risk_reduction: 40.0,
140 effort_points: 5,
141 dependencies: vec![],
142 },
143 BacklogItem {
144 id: "feat-2".to_string(),
145 title: "Feature 2".to_string(),
146 business_value: 40.0,
147 time_criticality: 80.0,
148 risk_reduction: 30.0,
149 effort_points: 2,
150 dependencies: vec![],
151 },
152 ],
153 capacity_points: 10,
154 }
155 }
156
157 #[test]
158 fn test_pack_name() {
159 let pack = BacklogPrioritizationPack;
160 assert_eq!(pack.name(), "backlog-prioritization");
161 assert_eq!(pack.version(), "1.0.0");
162 }
163
164 #[test]
165 fn test_validate_inputs() {
166 let pack = BacklogPrioritizationPack;
167 let input = create_test_input();
168 let json = serde_json::to_value(&input).unwrap();
169 assert!(pack.validate_inputs(&json).is_ok());
170 }
171
172 #[test]
173 fn test_solve_basic() {
174 let pack = BacklogPrioritizationPack;
175 let input = create_test_input();
176
177 let spec = ProblemSpec::builder("test-001", "test-tenant")
178 .objective(ObjectiveSpec::maximize("value"))
179 .inputs(&input)
180 .unwrap()
181 .seed(42)
182 .build()
183 .unwrap();
184
185 let result = pack.solve(&spec).unwrap();
186 assert!(result.is_feasible());
187
188 let output: BacklogPrioritizationOutput = result.plan.plan_as().unwrap();
189 assert_eq!(output.ranked_items.len(), 2);
190 }
191
192 #[test]
193 fn test_check_invariants() {
194 let pack = BacklogPrioritizationPack;
195 let input = create_test_input();
196
197 let spec = ProblemSpec::builder("test-002", "test-tenant")
198 .objective(ObjectiveSpec::maximize("value"))
199 .inputs(&input)
200 .unwrap()
201 .seed(42)
202 .build()
203 .unwrap();
204
205 let result = pack.solve(&spec).unwrap();
206 let invariants = pack.check_invariants(&result.plan).unwrap();
207
208 let all_pass = invariants.iter().all(|r| r.passed);
209 assert!(all_pass);
210 }
211
212 #[test]
213 fn test_gate_promotes() {
214 let pack = BacklogPrioritizationPack;
215 let input = create_test_input();
216
217 let spec = ProblemSpec::builder("test-003", "test-tenant")
218 .objective(ObjectiveSpec::maximize("value"))
219 .inputs(&input)
220 .unwrap()
221 .seed(42)
222 .build()
223 .unwrap();
224
225 let result = pack.solve(&spec).unwrap();
226 let invariants = pack.check_invariants(&result.plan).unwrap();
227 let gate = pack.evaluate_gate(&result.plan, &invariants);
228
229 assert!(gate.is_promoted());
230 }
231
232 #[test]
233 fn test_determinism() {
234 let pack = BacklogPrioritizationPack;
235 let input = create_test_input();
236
237 let spec1 = ProblemSpec::builder("test-a", "tenant")
238 .objective(ObjectiveSpec::maximize("value"))
239 .inputs(&input)
240 .unwrap()
241 .seed(99999)
242 .build()
243 .unwrap();
244
245 let spec2 = ProblemSpec::builder("test-b", "tenant")
246 .objective(ObjectiveSpec::maximize("value"))
247 .inputs(&input)
248 .unwrap()
249 .seed(99999)
250 .build()
251 .unwrap();
252
253 let result1 = pack.solve(&spec1).unwrap();
254 let result2 = pack.solve(&spec2).unwrap();
255
256 let output1: BacklogPrioritizationOutput = result1.plan.plan_as().unwrap();
257 let output2: BacklogPrioritizationOutput = result2.plan.plan_as().unwrap();
258
259 for (a, b) in output1.ranked_items.iter().zip(output2.ranked_items.iter()) {
260 assert_eq!(a.item_id, b.item_id);
261 assert_eq!(a.rank, b.rank);
262 }
263 }
264}