use anyhow::{bail, Context, Result}; use prometeu_core::ProjectId; use std::collections::{HashMap, VecDeque}; use crate::workspace::phases::state::ResolverState; /// Phase: Validate workspace graph & invariants (v0). /// /// Checks: /// - root present /// - edges are in-range /// - no cycles /// - no version conflicts for same project name pub fn validate(cfg: &crate::DepsConfig, state: &ResolverState) -> Result<()> { // 1) root present let root = state.root.context("deps: internal error: root ProjectId not set")?; if cfg.explain { eprintln!("[deps][validate] root={:?}", root); } // 2) edges sanity let n = state.nodes.len(); for (from_idx, outs) in state.edges.iter().enumerate() { for &to in outs { let to_idx = to.0 as usize; if to_idx >= n { bail!( "deps: invalid edge: from {:?} -> {:?} (to out of range; nodes={})", ProjectId(from_idx as u32), to, n ); } } } // 3) version conflicts by name // name -> (version -> ProjectId) let mut by_name: HashMap<&str, HashMap<&str, ProjectId>> = HashMap::new(); for node in &state.nodes { let vmap = by_name.entry(node.name.as_str()).or_default(); vmap.entry(node.version.as_str()).or_insert(node.id); } for (name, versions) in &by_name { if versions.len() > 1 { // create deterministic message let mut vs: Vec<(&str, ProjectId)> = versions.iter().map(|(v, id)| (*v, *id)).collect(); vs.sort_by(|a, b| a.0.cmp(b.0)); let mut msg = format!("deps: version conflict for project '{}':", name); for (v, id) in vs { let dir = &state.nodes[id.0 as usize].dir; msg.push_str(&format!("\n - {} at {:?} (id={:?})", v, dir, id)); } bail!(msg); } } // 4) cycle detection (Kahn + leftover nodes) // Build indegree let mut indeg = vec![0usize; n]; for outs in &state.edges { for &to in outs { indeg[to.0 as usize] += 1; } } let mut q = VecDeque::new(); for i in 0..n { if indeg[i] == 0 { q.push_back(i); } } let mut visited = 0usize; while let Some(i) = q.pop_front() { visited += 1; for &to in &state.edges[i] { let j = to.0 as usize; indeg[j] -= 1; if indeg[j] == 0 { q.push_back(j); } } } if visited != n { // Nodes with indeg>0 are part of cycles (or downstream of them) let mut cyclic: Vec = Vec::new(); for i in 0..n { if indeg[i] > 0 { cyclic.push(ProjectId(i as u32)); } } // Deterministic error output cyclic.sort_by_key(|id| id.0); let mut msg = "deps: dependency cycle detected among:".to_string(); for id in cyclic { let node = &state.nodes[id.0 as usize]; msg.push_str(&format!("\n - {:?} {}@{} dir={:?}", id, node.name, node.version, node.dir)); } bail!(msg); } Ok(()) }