mod invariants;
mod solver;
mod types;
pub use invariants::*;
pub use solver::*;
pub use types::*;
use crate::gate::{KernelTraceLink, ProblemSpec, PromotionGate, ProposedPlan};
use crate::packs::{default_gate_evaluation, InvariantDef, InvariantResult, Pack, PackSolveResult};
use crate::Result;
pub struct CapacityPlanningPack;
impl Pack for CapacityPlanningPack {
fn name(&self) -> &'static str {
"capacity-planning"
}
fn version(&self) -> &'static str {
"1.0.0"
}
fn validate_inputs(&self, inputs: &serde_json::Value) -> Result<()> {
let input: CapacityPlanningInput = serde_json::from_value(inputs.clone())
.map_err(|e| crate::Error::invalid_input(format!("Invalid input: {}", e)))?;
input.validate()
}
fn invariants(&self) -> &[InvariantDef] {
INVARIANTS
}
fn solve(&self, spec: &ProblemSpec) -> Result<PackSolveResult> {
let input: CapacityPlanningInput = spec.inputs_as()?;
input.validate()?;
let solver = MatchAllocationSolver;
let (output, report) = solver.solve_capacity(&input, spec)?;
let trace = KernelTraceLink::audit_only(format!("trace-{}", spec.problem_id));
let confidence = calculate_confidence(&output, &input);
let plan = ProposedPlan::from_payload(
format!("plan-{}", spec.problem_id),
self.name(),
output.summary(),
&output,
confidence,
trace,
)?;
Ok(PackSolveResult::new(plan, report))
}
fn check_invariants(&self, plan: &ProposedPlan) -> Result<Vec<InvariantResult>> {
let output: CapacityPlanningOutput = plan.plan_as()?;
Ok(check_all_invariants(&output))
}
fn evaluate_gate(
&self,
_plan: &ProposedPlan,
invariant_results: &[InvariantResult],
) -> PromotionGate {
default_gate_evaluation(invariant_results, self.invariants())
}
}
fn calculate_confidence(output: &CapacityPlanningOutput, input: &CapacityPlanningInput) -> f64 {
if output.assignments.is_empty() {
return 0.0;
}
let mut confidence: f64 = 0.4;
if output.summary.overall_fulfillment_ratio >= 0.95 {
confidence += 0.3;
} else if output.summary.overall_fulfillment_ratio >= 0.8 {
confidence += 0.2;
} else if output.summary.overall_fulfillment_ratio >= 0.6 {
confidence += 0.1;
}
if output.summary.teams_over_capacity == 0 {
confidence += 0.15;
}
if !output.team_utilization.is_empty() {
let utils: Vec<f64> = output
.team_utilization
.iter()
.filter(|t| t.total_capacity > 0.0)
.map(|t| t.utilization_ratio)
.collect();
if !utils.is_empty() {
let mean = utils.iter().sum::<f64>() / utils.len() as f64;
let variance = utils.iter().map(|u| (u - mean).powi(2)).sum::<f64>() / utils.len() as f64;
let std_dev = variance.sqrt();
if std_dev < 0.15 {
confidence += 0.1;
}
}
}
if let Some(budget) = input.constraints.max_budget {
if output.summary.total_cost <= budget {
confidence += 0.05;
}
}
confidence.min(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gate::ObjectiveSpec;
fn create_test_input() -> CapacityPlanningInput {
CapacityPlanningInput {
demand_forecasts: vec![
DemandForecast {
period_id: "Q1-2024".to_string(),
resource_type: "engineering".to_string(),
required_skill: "backend".to_string(),
demand_units: 100.0,
priority: 1,
min_fulfillment_ratio: 0.8,
},
DemandForecast {
period_id: "Q1-2024".to_string(),
resource_type: "engineering".to_string(),
required_skill: "frontend".to_string(),
demand_units: 50.0,
priority: 2,
min_fulfillment_ratio: 0.7,
},
],
resource_types: vec![ResourceType {
id: "engineering".to_string(),
name: "Engineering Hours".to_string(),
unit: "hours".to_string(),
cost_per_unit: 100.0,
}],
teams: vec![
Team {
id: "team-a".to_string(),
name: "Backend Team".to_string(),
skills: vec!["backend".to_string()],
resource_types: vec!["engineering".to_string()],
available_capacity: 120.0,
max_utilization: 0.85,
headcount: 6,
},
Team {
id: "team-b".to_string(),
name: "Frontend Team".to_string(),
skills: vec!["frontend".to_string()],
resource_types: vec!["engineering".to_string()],
available_capacity: 80.0,
max_utilization: 0.85,
headcount: 4,
},
],
constraints: PlanningConstraints {
target_utilization: 0.75,
max_budget: Some(20000.0),
min_overall_fulfillment: 0.8,
allow_cross_team: false,
strict_skill_matching: true,
},
}
}
#[test]
fn test_pack_name() {
let pack = CapacityPlanningPack;
assert_eq!(pack.name(), "capacity-planning");
assert_eq!(pack.version(), "1.0.0");
}
#[test]
fn test_validate_inputs() {
let pack = CapacityPlanningPack;
let input = create_test_input();
let json = serde_json::to_value(&input).unwrap();
assert!(pack.validate_inputs(&json).is_ok());
}
#[test]
fn test_validate_inputs_empty_demands() {
let pack = CapacityPlanningPack;
let mut input = create_test_input();
input.demand_forecasts = vec![];
let json = serde_json::to_value(&input).unwrap();
assert!(pack.validate_inputs(&json).is_err());
}
#[test]
fn test_solve_basic() {
let pack = CapacityPlanningPack;
let input = create_test_input();
let spec = ProblemSpec::builder("test-001", "test-tenant")
.objective(ObjectiveSpec::maximize("fulfillment"))
.inputs(&input)
.unwrap()
.seed(42)
.build()
.unwrap();
let result = pack.solve(&spec).unwrap();
assert!(result.is_feasible());
let output: CapacityPlanningOutput = result.plan.plan_as().unwrap();
assert!(!output.assignments.is_empty());
assert!(output.summary.overall_fulfillment_ratio > 0.0);
}
#[test]
fn test_solve_with_skill_matching() {
let pack = CapacityPlanningPack;
let input = create_test_input();
let spec = ProblemSpec::builder("test-002", "test-tenant")
.objective(ObjectiveSpec::maximize("fulfillment"))
.inputs(&input)
.unwrap()
.seed(42)
.build()
.unwrap();
let result = pack.solve(&spec).unwrap();
let output: CapacityPlanningOutput = result.plan.plan_as().unwrap();
let backend_assignments: Vec<_> = output
.assignments
.iter()
.filter(|a| a.demand_id.contains("backend"))
.collect();
assert!(backend_assignments.iter().all(|a| a.team_id == "team-a"));
let frontend_assignments: Vec<_> = output
.assignments
.iter()
.filter(|a| a.demand_id.contains("frontend"))
.collect();
assert!(frontend_assignments.iter().all(|a| a.team_id == "team-b"));
}
#[test]
fn test_check_invariants() {
let pack = CapacityPlanningPack;
let input = create_test_input();
let spec = ProblemSpec::builder("test-003", "test-tenant")
.objective(ObjectiveSpec::maximize("fulfillment"))
.inputs(&input)
.unwrap()
.seed(42)
.build()
.unwrap();
let result = pack.solve(&spec).unwrap();
let invariants = pack.check_invariants(&result.plan).unwrap();
let all_pass = invariants.iter().all(|r| r.passed);
assert!(all_pass);
}
#[test]
fn test_gate_promotes() {
let pack = CapacityPlanningPack;
let input = create_test_input();
let spec = ProblemSpec::builder("test-004", "test-tenant")
.objective(ObjectiveSpec::maximize("fulfillment"))
.inputs(&input)
.unwrap()
.seed(42)
.build()
.unwrap();
let result = pack.solve(&spec).unwrap();
let invariants = pack.check_invariants(&result.plan).unwrap();
let gate = pack.evaluate_gate(&result.plan, &invariants);
assert!(gate.is_promoted());
}
#[test]
fn test_determinism() {
let pack = CapacityPlanningPack;
let input = create_test_input();
let spec1 = ProblemSpec::builder("test-a", "tenant")
.objective(ObjectiveSpec::maximize("fulfillment"))
.inputs(&input)
.unwrap()
.seed(99999)
.build()
.unwrap();
let spec2 = ProblemSpec::builder("test-b", "tenant")
.objective(ObjectiveSpec::maximize("fulfillment"))
.inputs(&input)
.unwrap()
.seed(99999)
.build()
.unwrap();
let result1 = pack.solve(&spec1).unwrap();
let result2 = pack.solve(&spec2).unwrap();
let output1: CapacityPlanningOutput = result1.plan.plan_as().unwrap();
let output2: CapacityPlanningOutput = result2.plan.plan_as().unwrap();
assert_eq!(output1.assignments.len(), output2.assignments.len());
assert!(
(output1.summary.total_allocated - output2.summary.total_allocated).abs() < 0.01
);
}
#[test]
fn test_insufficient_capacity() {
let pack = CapacityPlanningPack;
let mut input = create_test_input();
input.demand_forecasts[0].demand_units = 500.0;
input.constraints.min_overall_fulfillment = 0.95;
let spec = ProblemSpec::builder("test-005", "test-tenant")
.objective(ObjectiveSpec::maximize("fulfillment"))
.inputs(&input)
.unwrap()
.seed(42)
.build()
.unwrap();
let result = pack.solve(&spec).unwrap();
assert!(!result.is_feasible());
}
#[test]
fn test_utilization_metrics() {
let pack = CapacityPlanningPack;
let input = create_test_input();
let spec = ProblemSpec::builder("test-006", "test-tenant")
.objective(ObjectiveSpec::maximize("fulfillment"))
.inputs(&input)
.unwrap()
.seed(42)
.build()
.unwrap();
let result = pack.solve(&spec).unwrap();
let output: CapacityPlanningOutput = result.plan.plan_as().unwrap();
assert_eq!(output.team_utilization.len(), 2);
assert!(output.team_utilization.iter().all(|t| !t.is_over_utilized));
assert!(output.summary.average_utilization > 0.0);
assert!(output.summary.average_utilization <= 1.0);
}
#[test]
fn test_period_fulfillment() {
let pack = CapacityPlanningPack;
let input = create_test_input();
let spec = ProblemSpec::builder("test-007", "test-tenant")
.objective(ObjectiveSpec::maximize("fulfillment"))
.inputs(&input)
.unwrap()
.seed(42)
.build()
.unwrap();
let result = pack.solve(&spec).unwrap();
let output: CapacityPlanningOutput = result.plan.plan_as().unwrap();
assert!(!output.period_fulfillment.is_empty());
let q1 = output
.period_fulfillment
.iter()
.find(|p| p.period_id == "Q1-2024");
assert!(q1.is_some());
let q1 = q1.unwrap();
assert!(q1.fulfillment_ratio > 0.8);
}
#[test]
fn test_cost_calculation() {
let pack = CapacityPlanningPack;
let input = create_test_input();
let spec = ProblemSpec::builder("test-008", "test-tenant")
.objective(ObjectiveSpec::maximize("fulfillment"))
.inputs(&input)
.unwrap()
.seed(42)
.build()
.unwrap();
let result = pack.solve(&spec).unwrap();
let output: CapacityPlanningOutput = result.plan.plan_as().unwrap();
let sum_costs: f64 = output.assignments.iter().map(|a| a.cost).sum();
assert!((output.summary.total_cost - sum_costs).abs() < 0.01);
for assignment in &output.assignments {
assert!((assignment.cost - assignment.allocated_units * 100.0).abs() < 0.01);
}
}
}