use std::collections::{BTreeMap, BTreeSet};
use petgraph::{algo::kosaraju_scc, graph::DiGraph};
use thiserror::Error;
use crate::{config::RepoConfig, runtime::Verdict};
#[derive(Debug, Error)]
#[error("{0}")]
pub struct GraphError(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArtifactNode<'a> {
pub basis: bool,
pub evals: Vec<&'a str>,
pub component: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Component<'a> {
pub artifacts: Vec<&'a str>,
pub dependencies: Vec<usize>,
pub gates: Vec<&'a str>,
}
#[derive(Debug)]
pub struct Graph<'a> {
artifacts: BTreeMap<&'a str, ArtifactNode<'a>>,
evals: BTreeMap<&'a str, &'a str>,
components: Vec<Component<'a>>,
}
impl<'a> Graph<'a> {
pub fn new(config: &'a RepoConfig) -> Result<Self, GraphError> {
let mut graph = DiGraph::<&str, ()>::new();
let nodes: BTreeMap<_, _> = config
.artifacts
.keys()
.map(|id| (id.as_str(), graph.add_node(id.as_str())))
.collect();
let mut artifacts: BTreeMap<_, _> = config
.artifacts
.iter()
.map(|(id, artifact)| {
(
id.as_str(),
ArtifactNode {
basis: artifact.basis == Some(true),
evals: Vec::new(),
component: 0,
},
)
})
.collect();
let mut evals = BTreeMap::new();
for eval in &config.evals {
let artifact = artifacts
.get_mut(eval.target.as_str())
.ok_or_else(|| GraphError(format!("Unknown Eval target: {}", eval.target)))?;
if artifact.basis {
return Err(GraphError(format!(
"Basis Artifact {} cannot own Evals.",
eval.target
)));
}
if evals
.insert(eval.id.as_str(), eval.target.as_str())
.is_some()
{
return Err(GraphError(format!("Duplicate Eval: {}", eval.id)));
}
artifact.evals.push(eval.id.as_str());
}
for artifact in artifacts.values_mut() {
artifact.evals.sort_unstable();
}
let mut edges = BTreeSet::new();
for relation in &config.relations {
for id in [&relation.source, &relation.target] {
if !nodes.contains_key(id.as_str()) {
return Err(GraphError(format!("Unknown relation Artifact: {id}")));
}
}
edges.insert((relation.target.as_str(), relation.source.as_str()));
}
for &(consumer, dependency) in &edges {
graph.add_edge(nodes[consumer], nodes[dependency], ());
}
let mut components: Vec<_> = kosaraju_scc(&graph)
.into_iter()
.map(|members| {
let mut artifacts: Vec<_> = members.into_iter().map(|node| graph[node]).collect();
artifacts.sort_unstable();
Component {
artifacts,
dependencies: Vec::new(),
gates: Vec::new(),
}
})
.collect();
for (index, component) in components.iter().enumerate() {
for id in &component.artifacts {
artifacts.get_mut(id).unwrap().component = index;
}
}
let mut dependencies = vec![BTreeSet::new(); components.len()];
for (consumer, dependency) in edges {
let consumer = artifacts[consumer].component;
let dependency = artifacts[dependency].component;
if consumer != dependency {
debug_assert!(dependency < consumer);
dependencies[consumer].insert(dependency);
}
}
for (index, dependencies) in dependencies.into_iter().enumerate() {
let mut gates: Vec<_> = dependencies
.iter()
.flat_map(|&dependency| &components[dependency].artifacts)
.flat_map(|id| &artifacts[id].evals)
.copied()
.collect();
gates.sort_unstable();
components[index].dependencies = dependencies.into_iter().collect();
components[index].gates = gates;
}
Ok(Self {
artifacts,
evals,
components,
})
}
pub fn artifacts(&self) -> &BTreeMap<&'a str, ArtifactNode<'a>> {
&self.artifacts
}
pub fn components(&self) -> &[Component<'a>] {
&self.components
}
pub fn dependency_closure(&self, roots: &[&str]) -> Result<Vec<&'a str>, GraphError> {
let mut pending = Vec::new();
for root in roots {
let artifact = self
.artifacts
.get(root)
.ok_or_else(|| GraphError(format!("Unknown Artifact: {root}")))?;
pending.push(artifact.component);
}
let mut visited = BTreeSet::new();
let mut required = BTreeSet::new();
while let Some(index) = pending.pop() {
if !visited.insert(index) {
continue;
}
let component = &self.components[index];
required.extend(component.artifacts.iter().copied());
pending.extend(&component.dependencies);
}
Ok(required.into_iter().collect())
}
pub fn evaluate(&self, evidence: &BTreeMap<String, Evidence>) -> Evaluation<'a> {
self.evaluate_with_policy(evidence, false)
}
pub fn evaluate_with_policy(
&self,
evidence: &BTreeMap<String, Evidence>,
ignore_gates: bool,
) -> Evaluation<'a> {
let mut evals: BTreeMap<&str, EvalEvaluation<'_>> = BTreeMap::new();
for component in &self.components {
let unmet_gates: Vec<_> = component
.gates
.iter()
.copied()
.filter(|id| !ignore_gates && evals[id].status != EvalStatus::Green)
.collect();
let readiness = if unmet_gates
.iter()
.any(|id| matches!(evals[id].status, EvalStatus::Red | EvalStatus::Blocked))
{
Readiness::Blocked
} else if unmet_gates.is_empty() {
Readiness::Ready
} else {
Readiness::Wait
};
for id in component
.artifacts
.iter()
.flat_map(|id| &self.artifacts[id].evals)
{
let evidence = evidence.get(*id).copied();
let status = match readiness {
Readiness::Blocked => EvalStatus::Blocked,
Readiness::Wait => EvalStatus::Wait,
Readiness::Ready => match evidence {
Some(Evidence::Current(Verdict::Green)) => EvalStatus::Green,
Some(Evidence::Current(Verdict::Red)) => EvalStatus::Red,
Some(Evidence::OperationalError) => EvalStatus::Error,
Some(Evidence::Stale) => EvalStatus::Stale,
None => EvalStatus::Unreviewed,
},
};
evals.insert(
*id,
EvalEvaluation {
readiness,
status,
evidence,
unmet_gates: unmet_gates.clone(),
},
);
}
}
let own_satisfied: BTreeMap<_, _> = self
.artifacts
.iter()
.map(|(&id, artifact)| {
let satisfied = artifact.basis
|| (!artifact.evals.is_empty()
&& artifact
.evals
.iter()
.all(|id| evals[id].status == EvalStatus::Green));
(id, satisfied)
})
.collect();
let mut satisfied = vec![false; self.components.len()];
for (index, component) in self.components.iter().enumerate() {
satisfied[index] = component.artifacts.iter().all(|id| own_satisfied[id])
&& component.dependencies.iter().all(|&index| satisfied[index]);
}
let artifacts = self
.artifacts
.iter()
.map(|(&id, artifact)| {
let satisfied = satisfied[artifact.component];
let status = if satisfied {
if artifact.basis {
ArtifactStatus::Basis
} else {
ArtifactStatus::Green
}
} else {
[
(EvalStatus::Error, ArtifactStatus::Error),
(EvalStatus::Red, ArtifactStatus::Red),
(EvalStatus::Blocked, ArtifactStatus::Blocked),
(EvalStatus::Wait, ArtifactStatus::Wait),
(EvalStatus::Stale, ArtifactStatus::Stale),
(EvalStatus::Unreviewed, ArtifactStatus::Unreviewed),
]
.into_iter()
.find_map(|(eval_status, artifact_status)| {
artifact
.evals
.iter()
.any(|id| evals[id].status == eval_status)
.then_some(artifact_status)
})
.unwrap_or(if own_satisfied[id] {
ArtifactStatus::Incomplete
} else {
ArtifactStatus::Unreviewed
})
};
(
id,
ArtifactEvaluation {
status,
own_satisfied: own_satisfied[id],
satisfied,
passed: artifact
.evals
.iter()
.filter(|id| evals[*id].status == EvalStatus::Green)
.count(),
total: artifact.evals.len(),
},
)
})
.collect();
let obligations: Vec<_> = own_satisfied
.into_iter()
.filter_map(|(id, satisfied)| (!satisfied).then_some(id))
.collect();
let status = if evals.values().any(|c| c.status == EvalStatus::Error) {
FinalStatus::Error
} else if evals.values().any(|c| c.status == EvalStatus::Red) {
FinalStatus::Red
} else if obligations.is_empty() {
FinalStatus::Green
} else {
FinalStatus::Incomplete
};
Evaluation {
evals,
artifacts,
obligations,
status,
}
}
pub fn eval_target(&self, id: &str) -> Option<&'a str> {
self.evals.get(id).copied()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Evidence {
Current(Verdict),
OperationalError,
Stale,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Readiness {
Ready,
Wait,
Blocked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvalStatus {
Green,
Red,
Error,
Stale,
Unreviewed,
Wait,
Blocked,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EvalEvaluation<'a> {
pub readiness: Readiness,
pub status: EvalStatus,
pub evidence: Option<Evidence>,
pub unmet_gates: Vec<&'a str>,
}
impl EvalEvaluation<'_> {
pub fn can_execute(&self) -> bool {
self.readiness == Readiness::Ready && matches!(self.evidence, None | Some(Evidence::Stale))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactStatus {
Basis,
Green,
Incomplete,
Error,
Red,
Blocked,
Wait,
Stale,
Unreviewed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArtifactEvaluation {
pub status: ArtifactStatus,
pub own_satisfied: bool,
pub satisfied: bool,
pub passed: usize,
pub total: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FinalStatus {
Green,
Red,
Error,
Incomplete,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Evaluation<'a> {
pub evals: BTreeMap<&'a str, EvalEvaluation<'a>>,
pub artifacts: BTreeMap<&'a str, ArtifactEvaluation>,
pub obligations: Vec<&'a str>,
pub status: FinalStatus,
}
#[cfg(test)]
mod tests;