use std::collections::{BTreeMap, BTreeSet};
use petgraph::{algo::kosaraju_scc, graph::DiGraph};
use thiserror::Error;
use crate::{
config::{ArtifactName, EvalId, RepoConfig},
runtime::Verdict,
};
#[derive(Debug, Error)]
#[error("{0}")]
pub struct GraphError(pub String);
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
#[serde(transparent)]
pub struct ComponentId(usize);
impl ComponentId {
pub(crate) fn from_position(index: usize) -> Self {
Self(index)
}
pub(crate) fn index(self) -> usize {
self.0
}
}
impl std::fmt::Display for ComponentId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArtifactNode<'a> {
pub basis: bool,
pub evals: Vec<&'a EvalId>,
pub component: ComponentId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Component<'a> {
pub artifacts: Vec<&'a ArtifactName>,
pub dependencies: Vec<ComponentId>,
pub gates: Vec<&'a EvalId>,
}
#[derive(Debug)]
pub struct Graph<'a> {
artifacts: BTreeMap<ArtifactName, ArtifactNode<'a>>,
evals: BTreeMap<EvalId, &'a ArtifactName>,
components: Vec<Component<'a>>,
schedule: schedule::Schedule<'a>,
}
impl<'a> Graph<'a> {
pub fn new(config: &'a RepoConfig) -> Result<Self, GraphError> {
let mut graph = DiGraph::<&ArtifactName, ()>::new();
let nodes: BTreeMap<_, _> = config
.artifacts
.keys()
.map(|id| (id, graph.add_node(id)))
.collect();
let mut artifacts: BTreeMap<_, _> = config
.artifacts
.iter()
.map(|(id, artifact)| {
(
id.clone(),
ArtifactNode {
basis: artifact.basis == Some(true),
evals: Vec::new(),
component: ComponentId(0),
},
)
})
.collect();
let mut evals = BTreeMap::new();
for eval in &config.evals {
let artifact = artifacts
.get_mut(&eval.target)
.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.clone(), &eval.target).is_some() {
return Err(GraphError(format!("Duplicate Eval: {}", eval.id)));
}
artifact.evals.push(&eval.id);
}
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) {
return Err(GraphError(format!("Unknown relation Artifact: {id}")));
}
}
edges.insert((&relation.target, &relation.source));
}
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.as_str()).unwrap().component = ComponentId(index);
}
}
let mut dependencies = vec![BTreeSet::new(); components.len()];
for (consumer, dependency) in edges {
let consumer = artifacts[consumer.as_str()].component;
let dependency = artifacts[dependency.as_str()].component;
if consumer != dependency {
debug_assert!(dependency < consumer);
dependencies[consumer.index()].insert(dependency);
}
}
for (index, dependencies) in dependencies.into_iter().enumerate() {
let mut gates: Vec<_> = dependencies
.iter()
.flat_map(|&dependency| &components[dependency.index()].artifacts)
.flat_map(|id| &artifacts[id.as_str()].evals)
.copied()
.collect();
gates.sort_unstable();
components[index].dependencies = dependencies.into_iter().collect();
components[index].gates = gates;
}
let schedule = schedule::Schedule::new(
config.evals.iter().map(|eval| schedule::Input {
id: &eval.id,
target: &eval.target,
deps: eval.deps.iter().collect(),
human: eval.declaration.profile().kind() == crate::config::ProfileKind::Human,
}),
config.artifacts.iter().flat_map(|(parent, artifact)| {
artifact.children.values().map(move |child| (parent, child))
}),
);
Ok(Self {
artifacts,
evals,
components,
schedule,
})
}
pub fn artifacts(&self) -> &BTreeMap<ArtifactName, ArtifactNode<'a>> {
&self.artifacts
}
pub fn components(&self) -> &[Component<'a>] {
&self.components
}
pub fn dependency_closure(
&self,
roots: &[&ArtifactName],
) -> Result<Vec<&'a ArtifactName>, 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.index()];
required.extend(component.artifacts.iter().copied());
pending.extend(&component.dependencies);
}
Ok(required.into_iter().collect())
}
pub fn evaluate(&self, evidence: &BTreeMap<EvalId, Evidence>) -> Evaluation<'_> {
self.evaluate_for_run(evidence, false)
}
pub(crate) fn evaluate_for_run(
&self,
evidence: &BTreeMap<EvalId, Evidence>,
errors_block: bool,
) -> Evaluation<'_> {
let mut evals: BTreeMap<EvalId, EvalEvaluation<'_>> = BTreeMap::new();
for &id in &self.schedule.order {
let gates = &self.schedule.gates[id];
let unmet_gates: Vec<_> = gates
.iter()
.copied()
.filter(|id| !evals[id.as_str()].status.passes_gate())
.collect();
let target = self.evals[id.as_str()];
let causes: Vec<_> = unmet_gates
.iter()
.flat_map(|&gate| {
let eval = &evals[gate.as_str()];
let sibling = self.evals[gate.as_str()] == target;
match eval.status {
EvalStatus::Red if sibling => vec![Cause::SiblingRed(gate)],
EvalStatus::Red => vec![Cause::Red(gate)],
EvalStatus::Error if sibling => vec![Cause::SiblingError(gate)],
EvalStatus::Error if errors_block => vec![Cause::Error(gate)],
EvalStatus::Blocked => {
eval.causes.iter().map(|cause| cause.upstream()).collect()
}
_ => Vec::new(),
}
})
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let readiness = if !causes.is_empty() {
Readiness::Blocked
} else if unmet_gates.is_empty() {
Readiness::Ready
} else {
Readiness::Wait
};
let provisional = gates.iter().any(|id| {
matches!(
evals[id.as_str()].status,
EvalStatus::Deferred | EvalStatus::Provisional
)
});
let evidence = evidence.get(id.as_str()).copied();
let status = match readiness {
Readiness::Blocked => EvalStatus::Blocked,
Readiness::Wait => match evidence {
Some(Evidence::Current(Verdict::Red)) if errors_block => EvalStatus::Red,
Some(Evidence::OperationalError) if errors_block => EvalStatus::Error,
_ => EvalStatus::Wait,
},
Readiness::Ready => match evidence {
Some(Evidence::Deferred) => EvalStatus::Deferred,
Some(Evidence::Current(Verdict::Green)) if provisional => {
EvalStatus::Provisional
}
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,
},
};
let blocked_by = if causes.is_empty() {
unmet_gates
.iter()
.copied()
.map(BlockedReference::Eval)
.collect()
} else {
causes
.iter()
.map(|cause| BlockedReference::Eval(cause.eval()))
.collect()
};
evals.insert(
id.clone(),
EvalEvaluation {
readiness,
status,
evidence,
unmet_gates,
blocked_by,
causes,
},
);
}
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.as_str()].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(|&dependency| satisfied[dependency.index()]);
}
let artifacts = self
.artifacts
.iter()
.map(|(id, artifact)| {
let satisfied = satisfied[artifact.component.index()];
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::Deferred, ArtifactStatus::Deferred),
(EvalStatus::Provisional, ArtifactStatus::Provisional),
(EvalStatus::Stale, ArtifactStatus::Stale),
(EvalStatus::Unreviewed, ArtifactStatus::Unreviewed),
]
.into_iter()
.find_map(|(eval_status, artifact_status)| {
artifact
.evals
.iter()
.any(|id| evals[id.as_str()].status == eval_status)
.then_some(artifact_status)
})
.unwrap_or(if own_satisfied[id] {
ArtifactStatus::Incomplete
} else {
ArtifactStatus::Unreviewed
})
};
(
id.clone(),
ArtifactEvaluation {
status,
own_satisfied: own_satisfied[id],
satisfied,
passed: artifact
.evals
.iter()
.filter(|id| evals[id.as_str()].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(crate) fn gates(&self, id: &EvalId) -> Vec<&'a EvalId> {
self.schedule.gates[id].clone()
}
pub fn eval_target(&self, id: &str) -> Option<&'a ArtifactName> {
self.evals.get(id).copied()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Evidence {
Current(Verdict),
Deferred,
OperationalError,
Stale,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Readiness {
Ready,
Wait,
Blocked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvalStatus {
Deferred,
Provisional,
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 EvalId>,
pub blocked_by: Vec<BlockedReference<'a>>,
pub causes: Vec<Cause<'a>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Cause<'a> {
Red(&'a EvalId),
Error(&'a EvalId),
SiblingRed(&'a EvalId),
SiblingError(&'a EvalId),
}
impl<'a> Cause<'a> {
pub fn eval(self) -> &'a EvalId {
match self {
Self::Red(id) | Self::Error(id) | Self::SiblingRed(id) | Self::SiblingError(id) => id,
}
}
fn upstream(self) -> Self {
match self {
Self::SiblingRed(id) => Self::Red(id),
Self::SiblingError(id) => Self::Error(id),
cause => cause,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockedReference<'a> {
Artifact(&'a ArtifactName),
Eval(&'a EvalId),
}
impl BlockedReference<'_> {
pub fn as_str(&self) -> &str {
match self {
Self::Artifact(id) => id.as_str(),
Self::Eval(id) => id.as_str(),
}
}
}
impl std::fmt::Display for BlockedReference<'_> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
impl EvalStatus {
pub fn passes_gate(self) -> bool {
matches!(self, Self::Green | Self::Deferred | Self::Provisional)
}
}
impl EvalEvaluation<'_> {
pub fn can_execute(&self) -> bool {
self.readiness == Readiness::Ready && matches!(self.evidence, None | Some(Evidence::Stale))
}
pub fn blocked_reason(&self) -> Option<String> {
let names = |kind: fn(&Cause<'_>) -> bool| {
self.causes
.iter()
.filter(|cause| kind(cause))
.map(|cause| cause.eval().as_str())
.collect::<Vec<_>>()
.join(", ")
};
let parts: Vec<_> = [
(
"Sibling machine eval RED",
names(|cause| matches!(cause, Cause::SiblingRed(_))),
),
(
"Sibling machine eval ERROR",
names(|cause| matches!(cause, Cause::SiblingError(_))),
),
(
"Dependency verdict RED",
names(|cause| matches!(cause, Cause::Red(_))),
),
(
"Dependency ERROR",
names(|cause| matches!(cause, Cause::Error(_))),
),
]
.into_iter()
.filter(|(_, names)| !names.is_empty())
.map(|(label, names)| format!("{label}: {names}"))
.collect();
(!parts.is_empty()).then(|| parts.join("; "))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactStatus {
Deferred,
Provisional,
Basis,
Green,
Incomplete,
Error,
Red,
Blocked,
Wait,
Stale,
Unreviewed,
}
impl From<ArtifactStatus> for crate::types::ArtifactValidationStatus {
fn from(status: ArtifactStatus) -> Self {
match status {
ArtifactStatus::Deferred => Self::Deferred,
ArtifactStatus::Provisional => Self::Provisional,
ArtifactStatus::Basis => Self::Basis,
ArtifactStatus::Green => Self::Green,
ArtifactStatus::Incomplete => Self::Incomplete,
ArtifactStatus::Error => Self::Error,
ArtifactStatus::Red => Self::Red,
ArtifactStatus::Blocked => Self::Blocked,
ArtifactStatus::Wait => Self::Wait,
ArtifactStatus::Stale => Self::Stale,
ArtifactStatus::Unreviewed => Self::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<EvalId, EvalEvaluation<'a>>,
pub artifacts: BTreeMap<ArtifactName, ArtifactEvaluation>,
pub obligations: Vec<&'a ArtifactName>,
pub status: FinalStatus,
}
#[cfg(test)]
mod tests;
impl serde::Serialize for BlockedReference<'_> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl From<BlockedReference<'_>> for crate::store::Blocker {
fn from(reference: BlockedReference<'_>) -> Self {
match reference {
BlockedReference::Artifact(id) => Self::Artifact(id.clone()),
BlockedReference::Eval(id) => Self::Eval(id.clone()),
}
}
}
pub(crate) mod schedule;