use crate::{ValidationError, ValidationResult};
use agileplus_domain::intent_graph::*;
use std::collections::{HashMap, HashSet};
pub fn validate_dag_acyclicity(graph: &IntentGraph) -> ValidationResult {
let mut errors = Vec::new();
let mut adjacency: HashMap<&NodeId, Vec<&NodeId>> = HashMap::new();
for edge in &graph.edges {
adjacency.entry(&edge.source).or_default().push(&edge.target);
}
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
fn dfs<'a>(
node: &'a NodeId,
adjacency: &HashMap<&'a NodeId, Vec<&'a NodeId>>,
visited: &mut HashSet<&'a NodeId>,
rec_stack: &mut HashSet<&'a NodeId>,
path: &mut Vec<&'a NodeId>,
errors: &mut Vec<ValidationError>,
) {
visited.insert(node);
rec_stack.insert(node);
path.push(node);
if let Some(neighbors) = adjacency.get(node) {
for neighbor in neighbors {
if !visited.contains(neighbor) {
dfs(neighbor, adjacency, visited, rec_stack, path, errors);
} else if rec_stack.contains(neighbor) {
let cycle_start = path.iter().position(|&n| n == *neighbor).unwrap_or(0);
let cycle_nodes: Vec<String> = path[cycle_start..]
.iter()
.map(|&n| n.to_string())
.collect();
let cycle_str = cycle_nodes.join(" -> ");
errors.push(ValidationError::CycleDetected(format!(
"Cycle detected: {} -> {}",
cycle_str, neighbor
)));
}
}
}
path.pop();
rec_stack.remove(node);
}
let mut path = Vec::new();
for node in &graph.nodes {
if !visited.contains(&node.id) {
dfs(
&node.id,
&adjacency,
&mut visited,
&mut rec_stack,
&mut path,
&mut errors,
);
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn validate_dag_flow(graph: &IntentGraph) -> ValidationResult {
let mut errors = Vec::new();
let ontology = Ontology::default();
let rules = &ontology.dag_flow.validation_rules;
let mut has_incoming: HashSet<&NodeId> = HashSet::new();
for edge in &graph.edges {
has_incoming.insert(&edge.target);
}
let roots: Vec<&Node> = graph
.nodes
.iter()
.filter(|n| !has_incoming.contains(&n.id))
.collect();
if rules.no_cycles && !rules.root_nodes_must_be.is_empty() {
for root in &roots {
if !rules.root_nodes_must_be.contains(&root.node_type) {
errors.push(ValidationError::DagFlowViolation(format!(
"Root node {} has type {:?}, but must be one of {:?}",
root.id, root.node_type, rules.root_nodes_must_be
)));
}
}
}
let mut has_outgoing: HashSet<&NodeId> = HashSet::new();
for edge in &graph.edges {
has_outgoing.insert(&edge.source);
}
let leaves: Vec<&Node> = graph
.nodes
.iter()
.filter(|n| !has_outgoing.contains(&n.id))
.collect();
if !rules.leaf_nodes_prefer.is_empty() {
for leaf in &leaves {
if !rules.leaf_nodes_prefer.contains(&leaf.node_type) {
errors.push(ValidationError::DagFlowViolation(format!(
"Leaf node {} has type {:?}, but preferred types are {:?}",
leaf.id, leaf.node_type, rules.leaf_nodes_prefer
)));
}
}
}
if !graph.nodes.is_empty() && !graph.edges.is_empty() && roots.is_empty() {
errors.push(ValidationError::DagFlowViolation(
"Graph has edges but no root nodes (no nodes without incoming edges)"
.to_string(),
));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}