use crate::core::types::{RiskTier, ValidationTier};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PlanItem {
pub component_id: String,
pub current_version: String,
pub proposed_version: String,
pub validation_tier: ValidationTier,
pub selected: bool,
pub rationale: String,
#[serde(default)]
pub dependencies: Vec<String>,
pub isolation_safe: bool,
pub risk_tier: RiskTier,
}
impl PlanItem {
pub fn new(
component_id: &str,
current_version: &str,
proposed_version: &str,
validation_tier: ValidationTier,
selected: bool,
rationale: &str,
dependencies: Vec<String>,
isolation_safe: bool,
) -> Self {
Self {
component_id: component_id.to_string(),
current_version: current_version.to_string(),
proposed_version: proposed_version.to_string(),
validation_tier,
selected,
rationale: rationale.to_string(),
dependencies,
isolation_safe,
risk_tier: RiskTier::Medium, }
}
}
pub fn topological_sort(items: Vec<PlanItem>) -> Vec<PlanItem> {
let mut remaining_deps: HashMap<String, Vec<String>> = HashMap::new();
let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
let mut all_nodes: HashSet<String> = HashSet::new();
for item in &items {
all_nodes.insert(item.component_id.clone());
remaining_deps.insert(item.component_id.clone(), item.dependencies.clone());
for dep in &item.dependencies {
dependents
.entry(dep.clone())
.or_default()
.push(item.component_id.clone());
}
}
let mut sorted_nodes = Vec::new();
let mut queue: Vec<String> = all_nodes
.iter()
.filter(|id| remaining_deps.get(*id).map_or(true, |d| d.is_empty()))
.cloned()
.collect();
while let Some(node) = queue.pop() {
sorted_nodes.push(node.clone());
if let Some(dep_on_node) = dependents.get(&node) {
for dependent in dep_on_node.clone() {
if let Some(deps) = remaining_deps.get_mut(&dependent) {
deps.retain(|d| d != &node);
if deps.is_empty() {
queue.push(dependent);
}
}
}
}
}
if sorted_nodes.len() != all_nodes.len() {
panic!("Dependency cycle detected in plan items");
}
let mut item_map: HashMap<String, PlanItem> = items
.into_iter()
.map(|item| (item.component_id.clone(), item))
.collect();
sorted_nodes
.into_iter()
.filter_map(|id| item_map.remove(&id))
.collect()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Plan {
pub items: Vec<PlanItem>,
pub summary: PlanSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PlanSummary {
pub total: usize,
pub selected: usize,
pub safe: usize,
pub guarded: usize,
pub candidate: usize,
pub experimental: usize,
pub blocked: usize,
}
impl Plan {
pub fn new(items: Vec<PlanItem>) -> Self {
let items = topological_sort(items);
let summary = PlanSummary::from_items(&items);
Self { items, summary }
}
}
impl PlanSummary {
pub fn from_items(items: &[PlanItem]) -> Self {
Self {
total: items.len(),
selected: items.iter().filter(|i| i.selected).count(),
safe: items
.iter()
.filter(|i| i.risk_tier == RiskTier::Low)
.count(),
guarded: items
.iter()
.filter(|i| i.risk_tier == RiskTier::Medium)
.count(),
candidate: items
.iter()
.filter(|i| i.risk_tier == RiskTier::High)
.count(),
experimental: items
.iter()
.filter(|i| i.validation_tier == ValidationTier::Experimental)
.count(),
blocked: 0, }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::{RiskTier, ValidationTier};
#[test]
fn test_topological_sort_simple_dependency() {
let items = vec![
PlanItem::new(
"pytorch",
"2.4.0",
"2.5.0",
ValidationTier::Validated,
true,
"update pytorch",
vec!["rocm".to_string()],
true,
),
PlanItem::new(
"rocm",
"7.2.0",
"7.3.0",
ValidationTier::Validated,
true,
"update rocm",
vec![],
true,
),
];
let sorted = topological_sort(items);
assert_eq!(sorted[0].component_id, "rocm");
assert_eq!(sorted[1].component_id, "pytorch");
}
#[test]
fn test_topological_sort_complex_dependency_chain() {
let items = vec![
PlanItem::new(
"pytorch",
"2.4.0",
"2.5.0",
ValidationTier::Validated,
true,
"update pytorch",
vec!["rocm".to_string()],
true,
),
PlanItem::new(
"triton",
"3.0.0",
"3.1.0",
ValidationTier::Validated,
true,
"update triton",
vec!["pytorch".to_string()],
true,
),
PlanItem::new(
"rocm",
"7.2.0",
"7.3.0",
ValidationTier::Validated,
true,
"update rocm",
vec![],
true,
),
];
let sorted = topological_sort(items);
assert_eq!(sorted[0].component_id, "rocm");
assert_eq!(sorted[1].component_id, "pytorch");
assert_eq!(sorted[2].component_id, "triton");
}
#[test]
fn test_topological_sort_empty_dependencies() {
let items = vec![
PlanItem::new(
"pytorch",
"2.4.0",
"2.5.0",
ValidationTier::Validated,
true,
"update pytorch",
vec![],
true,
),
PlanItem::new(
"rocm",
"7.2.0",
"7.3.0",
ValidationTier::Validated,
true,
"update rocm",
vec![],
true,
),
];
let sorted = topological_sort(items);
let ids: HashSet<_> = sorted.iter().map(|i| i.component_id.clone()).collect();
assert!(ids.contains("pytorch"));
assert!(ids.contains("rocm"));
}
#[test]
#[should_panic(expected = "Dependency cycle detected")]
fn test_topological_sort_detects_cycle() {
let items = vec![
PlanItem::new(
"pytorch",
"2.4.0",
"2.5.0",
ValidationTier::Validated,
true,
"update pytorch",
vec!["triton".to_string()],
true,
),
PlanItem::new(
"triton",
"3.0.0",
"3.1.0",
ValidationTier::Validated,
true,
"update triton",
vec!["pytorch".to_string()],
true,
),
];
topological_sort(items);
}
#[test]
fn test_plan_creation_applies_topological_sort() {
let items = vec![
PlanItem::new(
"pytorch",
"2.4.0",
"2.5.0",
ValidationTier::Validated,
true,
"update pytorch",
vec!["rocm".to_string()],
true,
),
PlanItem::new(
"rocm",
"7.2.0",
"7.3.0",
ValidationTier::Validated,
true,
"update rocm",
vec![],
true,
),
];
let plan = Plan::new(items);
assert_eq!(plan.items[0].component_id, "rocm");
assert_eq!(plan.items[1].component_id, "pytorch");
}
#[test]
fn test_plan_summary_counts() {
let items = vec![
PlanItem {
component_id: "rocm".to_string(),
current_version: "7.2.0".to_string(),
proposed_version: "7.3.0".to_string(),
validation_tier: ValidationTier::Validated,
selected: true,
rationale: "update rocm".to_string(),
dependencies: vec![],
isolation_safe: true,
risk_tier: RiskTier::Low,
},
PlanItem {
component_id: "pytorch".to_string(),
current_version: "2.4.0".to_string(),
proposed_version: "2.5.0".to_string(),
validation_tier: ValidationTier::Validated,
selected: true,
rationale: "update pytorch".to_string(),
dependencies: vec!["rocm".to_string()],
isolation_safe: true,
risk_tier: RiskTier::Medium,
},
PlanItem {
component_id: "deepspeed".to_string(),
current_version: "".to_string(),
proposed_version: "0.15.0".to_string(),
validation_tier: ValidationTier::Experimental,
selected: false,
rationale: "new experimental".to_string(),
dependencies: vec!["pytorch".to_string()],
isolation_safe: true,
risk_tier: RiskTier::High,
},
];
let plan = Plan::new(items);
assert_eq!(plan.summary.total, 3);
assert_eq!(plan.summary.selected, 2);
assert_eq!(plan.summary.safe, 1);
assert_eq!(plan.summary.guarded, 1);
assert_eq!(plan.summary.candidate, 1);
assert_eq!(plan.summary.experimental, 1);
assert_eq!(plan.summary.blocked, 0);
}
#[test]
fn test_plan_item_serde_roundtrip() {
let item = PlanItem {
component_id: "pytorch".to_string(),
current_version: "2.4.0".to_string(),
proposed_version: "2.5.0".to_string(),
validation_tier: ValidationTier::Validated,
selected: true,
rationale: "update pytorch".to_string(),
dependencies: vec!["rocm".to_string()],
isolation_safe: true,
risk_tier: RiskTier::Medium,
};
let json = serde_json::to_string(&item).unwrap();
let back: PlanItem = serde_json::from_str(&json).unwrap();
assert_eq!(item, back);
}
#[test]
fn test_plan_serde_roundtrip() {
let items = vec![
PlanItem::new(
"rocm",
"7.2.0",
"7.3.0",
ValidationTier::Validated,
true,
"update rocm",
vec![],
true,
),
PlanItem::new(
"pytorch",
"2.4.0",
"2.5.0",
ValidationTier::Validated,
true,
"update pytorch",
vec!["rocm".to_string()],
true,
),
];
let plan = Plan::new(items);
let json = serde_json::to_string(&plan).unwrap();
let back: Plan = serde_json::from_str(&json).unwrap();
assert_eq!(plan.items, back.items);
assert_eq!(plan.summary, back.summary);
}
#[test]
fn test_topological_sort_with_llama_cpp_dependencies() {
let items = vec![
PlanItem::new(
"llama-cpp",
"",
"latest",
ValidationTier::Experimental,
true,
"install llama-cpp",
vec!["rocm".to_string()],
true,
),
PlanItem::new(
"rocm",
"7.2.0",
"7.3.0",
ValidationTier::Validated,
true,
"update rocm",
vec![],
true,
),
];
let sorted = topological_sort(items);
assert_eq!(sorted[0].component_id, "rocm");
assert_eq!(sorted[1].component_id, "llama-cpp");
}
}