2026-03-24 13:42:13 +00:00

109 lines
3.2 KiB
Rust

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<ProjectId> = 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(())
}