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