Skip to main content

converge_optimization/packs/backlog_prioritization/
mod.rs

1//! Backlog Prioritization Pack
2//!
3//! JTBD: "Prioritize work under value-risk-effort tradeoffs."
4//!
5//! ## Problem
6//!
7//! Given:
8//! - Work items with value, risk, effort estimates
9//! - Dependencies between items
10//! - Capacity constraints
11//!
12//! Find:
13//! - Prioritized backlog using WSJF (Weighted Shortest Job First)
14//!
15//! ## Solver
16//!
17//! Uses WSJF scoring:
18//! 1. Calculate WSJF = (Business Value + Time Criticality + Risk Reduction) / Effort
19//! 2. Sort by WSJF descending
20//! 3. Respect dependency ordering
21//! 4. Mark items within capacity as included
22
23mod 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
35/// Backlog Prioritization Pack
36pub 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    // Higher confidence if we included items in capacity
104    if output.included_count > 0 {
105        confidence += 0.2;
106    }
107
108    // Higher confidence if capacity is well utilized
109    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    // Higher confidence if total value is good
119    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}