use std::collections::{HashMap, HashSet, VecDeque}; use crate::{error::ReflectionError, mode::ActionStep}; /// Full DAG validation: uniqueness + referential integrity + cycle detection. pub fn validate(steps: &[ActionStep]) -> Result<(), ReflectionError> { check_uniqueness(steps)?; check_referential_integrity(steps)?; detect_cycles(steps)?; Ok(()) } /// Kahn's algorithm: returns steps grouped into parallel execution layers. /// Layer 0 has no dependencies; layer N depends only on layers < N. /// Returns `Err(CycleDetected)` if the graph is not a DAG. pub fn topological_layers(steps: &[ActionStep]) -> Result>, ReflectionError> { // in_degree[id] = number of steps id depends on let mut in_degree: HashMap<&str, usize> = steps .iter() .map(|s| (s.id.as_str(), s.depends_on.len())) .collect(); // dependents[id] = steps that depend on id (reverse edges) let mut dependents: HashMap<&str, Vec<&str>> = steps.iter().map(|s| (s.id.as_str(), vec![])).collect(); for step in steps { for dep in &step.depends_on { dependents .entry(dep.step.as_str()) .or_default() .push(step.id.as_str()); } } let mut queue: VecDeque<&str> = in_degree .iter() .filter_map(|(&id, &d)| (d == 0).then_some(id)) .collect(); let mut layers: Vec> = Vec::new(); let mut visited = 0usize; while !queue.is_empty() { let layer: Vec<&str> = queue.drain(..).collect(); visited += layer.len(); let mut next: Vec<&str> = Vec::new(); for &node in &layer { for &dep in dependents.get(node).map(Vec::as_slice).unwrap_or(&[]) { let d = in_degree .get_mut(dep) .expect("all step ids must be in in_degree — check uniqueness first"); *d -= 1; if *d == 0 { next.push(dep); } } } layers.push(layer.iter().map(|&s| s.to_string()).collect()); queue.extend(next); } if visited != steps.len() { Err(ReflectionError::CycleDetected) } else { Ok(layers) } } fn check_uniqueness(steps: &[ActionStep]) -> Result<(), ReflectionError> { let mut seen: HashSet<&str> = HashSet::with_capacity(steps.len()); for step in steps { if !seen.insert(step.id.as_str()) { return Err(ReflectionError::DuplicateStepId(step.id.clone())); } } Ok(()) } fn check_referential_integrity(steps: &[ActionStep]) -> Result<(), ReflectionError> { let ids: HashSet<&str> = steps.iter().map(|s| s.id.as_str()).collect(); let bad: Vec = steps .iter() .flat_map(|step| { step.depends_on.iter().filter_map(|dep| { if ids.contains(dep.step.as_str()) { None } else { Some(format!( "step '{}' depends_on unknown '{}'", step.id, dep.step )) } }) }) .collect(); if bad.is_empty() { Ok(()) } else { Err(ReflectionError::BadDependencyRefs(bad)) } } fn detect_cycles(steps: &[ActionStep]) -> Result<(), ReflectionError> { topological_layers(steps).map(|_| ()) } #[cfg(test)] mod tests { use super::*; use crate::mode::{Dependency, DependencyKind, OnError}; fn step(id: &str, deps: &[&str]) -> ActionStep { ActionStep { id: id.to_string(), action: id.to_string(), actor: crate::mode::Actor::Both, cmd: None, depends_on: deps .iter() .map(|d| Dependency { step: d.to_string(), kind: DependencyKind::Always, condition: None, }) .collect(), on_error: OnError::default(), verify: None, note: None, } } #[test] fn linear_chain_produces_single_layers() { let steps = vec![step("a", &[]), step("b", &["a"]), step("c", &["b"])]; let layers = topological_layers(&steps).unwrap(); assert_eq!(layers.len(), 3); assert_eq!(layers[0], vec!["a"]); assert_eq!(layers[1], vec!["b"]); assert_eq!(layers[2], vec!["c"]); } #[test] fn parallel_deps_form_single_layer() { // a → {b, c} → d let steps = vec![ step("a", &[]), step("b", &["a"]), step("c", &["a"]), step("d", &["b", "c"]), ]; let layers = topological_layers(&steps).unwrap(); assert_eq!(layers.len(), 3); assert_eq!(layers[0], vec!["a"]); assert!(layers[1].contains(&"b".to_string())); assert!(layers[1].contains(&"c".to_string())); assert_eq!(layers[2], vec!["d"]); } #[test] fn cycle_detected() { let steps = vec![step("a", &["b"]), step("b", &["a"])]; assert!(matches!( topological_layers(&steps), Err(ReflectionError::CycleDetected) )); } #[test] fn duplicate_id_rejected() { let steps = vec![step("a", &[]), step("a", &[])]; assert!(matches!( check_uniqueness(&steps), Err(ReflectionError::DuplicateStepId(_)) )); } #[test] fn bad_ref_rejected() { let steps = vec![step("a", &["nonexistent"])]; assert!(matches!( check_referential_integrity(&steps), Err(ReflectionError::BadDependencyRefs(_)) )); } #[test] fn validate_passes_for_valid_dag() { let steps = vec![ step("init_repo", &[]), step("copy_ontology", &["init_repo"]), step("init_kogral", &["init_repo"]), step("publish", &["copy_ontology", "init_kogral"]), ]; assert!(validate(&steps).is_ok()); } }