109 lines
3.2 KiB
Rust
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(())
|
|
}
|