use super::{Edge, Node, TopoOrder, identity_key};
use crate::reference::CardRef;
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum GraphError {
#[error("dependency cycle among submissions: {cycle:?}")]
Cycle {
cycle: Vec<CardRef>,
},
#[error("multiple roots detected: {candidates:?}")]
MultipleRoots {
candidates: Vec<CardRef>,
},
#[error("empty submission set")]
Empty,
#[error("submission metadata.space is required for graph operations")]
MissingSpace,
#[error("duplicate submission identity: {candidates:?}")]
DuplicateIdentity {
candidates: Vec<CardRef>,
},
#[error(
"Service {service} component {alias} cannot reference peer-only card {component} at {field}"
)]
InvalidServiceComponentKind {
service: Box<CardRef>,
alias: String,
component: Box<CardRef>,
field: String,
},
#[error("Service-root bundle {root} contains unbound Verifier peer {peer}")]
UnboundVerifierPeer {
root: Box<CardRef>,
peer: Box<CardRef>,
},
#[error("invalid submission spec: {message}")]
InvalidSpec {
message: String,
},
}
pub fn topo_sort(nodes: &[Node], edges: &[Edge]) -> Result<TopoOrder, GraphError> {
if nodes.is_empty() {
return Err(GraphError::Empty);
}
let index_by_identity = nodes
.iter()
.enumerate()
.map(|(index, node)| (identity_key(&node.card_ref), index))
.collect::<std::collections::BTreeMap<_, _>>();
let mut remaining_children = vec![0usize; nodes.len()];
let mut parent_edges = vec![Vec::new(); nodes.len()];
for edge in edges {
let (Some(&from), Some(&to)) = (
index_by_identity.get(&identity_key(&edge.from)),
index_by_identity.get(&identity_key(&edge.to)),
) else {
continue;
};
remaining_children[from] += 1;
parent_edges[to].push(from);
}
let mut ready = nodes
.iter()
.enumerate()
.filter_map(|(index, _)| (remaining_children[index] == 0).then_some(index))
.collect::<Vec<_>>();
let mut ordered = Vec::with_capacity(nodes.len());
while let Some(index) = pop_canonical(&mut ready, nodes) {
ordered.push(nodes[index].clone());
for parent in parent_edges[index].drain(..) {
remaining_children[parent] -= 1;
if remaining_children[parent] == 0 {
ready.push(parent);
}
}
}
if ordered.len() != nodes.len() {
let mut cycle = nodes
.iter()
.enumerate()
.filter_map(|(index, node)| {
(remaining_children[index] != 0).then_some(node.card_ref.clone())
})
.collect::<Vec<_>>();
cycle.sort_by_key(identity_key);
return Err(GraphError::Cycle { cycle });
}
Ok(TopoOrder {
nodes: ordered,
edges: edges.to_vec(),
})
}
fn pop_canonical(ready: &mut Vec<usize>, nodes: &[Node]) -> Option<usize> {
let position = ready
.iter()
.enumerate()
.min_by_key(|(_, index)| identity_key(&nodes[**index].card_ref))
.map(|(position, _)| position)?;
Some(ready.swap_remove(position))
}
#[cfg(test)]
mod tests {
use super::{GraphError, topo_sort};
use crate::envelope::CardKind;
use crate::graph::{Edge, Node};
use crate::reference::CardRef;
use wyrd_semver::VersionBlock;
fn card_ref(kind: CardKind, name: &str) -> CardRef {
CardRef {
kind,
name: name.parse().expect("test card name is valid"),
version: VersionBlock::parse("1.0.0").expect("test version is valid"),
space: Some("default".parse().expect("test space is valid")),
uid: None,
}
}
fn node(kind: CardKind, name: &str) -> Node {
Node {
card_ref: card_ref(kind, name),
}
}
fn edge(from: &Node, to: &Node) -> Edge {
Edge {
from: from.card_ref.clone(),
to: to.card_ref.clone(),
}
}
#[test]
fn topo_sort_single_root_leaf_first() {
let prompt = node(CardKind::Prompt, "prompt");
let agent = node(CardKind::Agent, "agent");
let service = node(CardKind::Service, "service");
let edges = vec![edge(&service, &agent), edge(&agent, &prompt)];
let order = topo_sort(&[service.clone(), prompt.clone(), agent.clone()], &edges)
.expect("chain is acyclic");
assert_eq!(
order.nodes,
vec![prompt.clone(), agent.clone(), service.clone()]
);
assert_eq!(order.edges, edges);
}
#[test]
fn topo_sort_tie_break_canonical() {
let agent = node(CardKind::Agent, "agent");
let prompt_b = node(CardKind::Prompt, "prompt-b");
let prompt_a = node(CardKind::Prompt, "prompt-a");
let service = node(CardKind::Service, "service");
let edges = vec![
edge(&agent, &prompt_b),
edge(&agent, &prompt_a),
edge(&service, &agent),
];
let nodes = [
service.clone(),
prompt_b.clone(),
agent.clone(),
prompt_a.clone(),
];
let first = topo_sort(&nodes, &edges).expect("graph is acyclic");
let second = topo_sort(&nodes, &edges).expect("graph is acyclic");
assert_eq!(first, second);
assert_eq!(first.nodes, vec![prompt_a, prompt_b, agent, service]);
}
#[test]
fn topo_sort_cycle_returns_cycle_error() {
let a = node(CardKind::Agent, "agent-a");
let b = node(CardKind::Agent, "agent-b");
let c = node(CardKind::Agent, "agent-c");
let error = topo_sort(
&[a.clone(), b.clone(), c.clone()],
&[edge(&a, &b), edge(&b, &c), edge(&c, &a)],
)
.expect_err("cycle must be rejected");
let GraphError::Cycle { cycle } = error else {
panic!("expected cycle error");
};
assert_eq!(cycle.len(), 3);
assert!(cycle.contains(&a.card_ref));
assert!(cycle.contains(&b.card_ref));
assert!(cycle.contains(&c.card_ref));
}
}