Skip to main content

artifactize_core/monitor/model/
graph.rs

1//! A Run as a graph of Artifacts: one node per Artifact of the tree, one edge per Artifact an
2//! Artifact's evals depend on, each with one of six lights taken from the tree's own states.
3use std::collections::{BTreeMap, BTreeSet};
4
5use super::{
6    NodeId, Target,
7    states::{Activity, Busy, Completion, EvalView, NotRun},
8    tree::{Kind, Node},
9};
10use crate::types::ArtifactName;
11
12/// How a graph node or edge is lit. Declared in precedence order: an Artifact takes the first
13/// light any of its evals has (RED before ERROR before in progress before blocked before
14/// waiting before GREEN).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum Light {
17    /// An eval's verdict is RED.
18    Red,
19    /// An eval failed operationally: ERROR, not a verdict.
20    Error,
21    /// Running, or waiting for its Human.
22    Active,
23    /// Cannot proceed: an upstream Artifact is RED, ERROR or itself blocked.
24    Blocked,
25    /// Runnable but still waiting: queued, for upstream Artifacts that are not done yet, or
26    /// not run in a finished Run.
27    Waiting,
28    /// Every eval is GREEN (an Artifact without evals is complete too).
29    Green,
30}
31
32impl From<Completion> for Light {
33    /// An upstream Artifact's light where the graph has not lit it (a cycle peer lit later).
34    fn from(completion: Completion) -> Self {
35        match completion {
36            Completion::Complete => Self::Green,
37            Completion::Failed { verdict: true } => Self::Red,
38            Completion::Failed { verdict: false } => Self::Error,
39            Completion::InProgress(Busy::Running | Busy::Human) => Self::Active,
40            Completion::InProgress(Busy::Queued) => Self::Waiting,
41            Completion::Waiting { blocked: true } => Self::Blocked,
42            Completion::Waiting { blocked: false } => Self::Waiting,
43        }
44    }
45}
46
47impl From<&EvalView> for Light {
48    fn from(view: &EvalView) -> Self {
49        match view {
50            EvalView::Done(_) => Self::Green,
51            EvalView::Failed { verdict: true } => Self::Red,
52            EvalView::Failed { verdict: false } => Self::Error,
53            EvalView::InProgress(Activity::Running | Activity::Human(_)) => Self::Active,
54            EvalView::BlockedBy(_) => Self::Blocked,
55            EvalView::InProgress(Activity::Queued(_))
56            | EvalView::WaitingOn(_)
57            | EvalView::NotRun(
58                NotRun::Dependency(_)
59                | NotRun::Budget
60                | NotRun::Stale
61                | NotRun::Unreviewed
62                | NotRun::Absent,
63            ) => Self::Waiting,
64        }
65    }
66}
67
68/// Gate failures come from the Eval-level schedule, never an Artifact-wide inference.
69fn eval_light(eval: &Node, view: &EvalView) -> Light {
70    if eval.held
71        && matches!(
72            view,
73            EvalView::WaitingOn(_) | EvalView::NotRun(NotRun::Dependency(_))
74        )
75    {
76        Light::Blocked
77    } else {
78        Light::from(view)
79    }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct GraphNode {
84    /// The Artifact row's tree id, `a:<artifact>`.
85    pub id: NodeId,
86    pub name: ArtifactName,
87    pub light: Light,
88}
89
90/// `to`'s evals depend on `from`.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct GraphEdge {
93    pub from: ArtifactName,
94    pub to: ArtifactName,
95    /// The input Artifact's current light.
96    pub light: Light,
97}
98
99#[derive(Debug, Clone, Default, PartialEq, Eq)]
100pub struct Graph {
101    /// In the tree's order: upstream first where the graph allows.
102    pub nodes: Vec<GraphNode>,
103    /// By `to` in node order, then `from` in node order.
104    pub edges: Vec<GraphEdge>,
105}
106
107/// The graph of the Artifact rows of a Run's [`tree`](super::tree), lit upstream first in the
108/// tree's order.
109pub fn graph(tree: &[Node]) -> Graph {
110    let artifacts: Vec<(ArtifactName, &Node, Completion)> = tree
111        .iter()
112        .filter_map(|node| match node.kind {
113            Kind::Artifact { completion, .. } => match Target::from(&node.id) {
114                Target::Artifact(name) => Some((name, node, completion)),
115                _ => None,
116            },
117            Kind::Eval(_) => None,
118        })
119        .collect();
120    let mut lights: BTreeMap<ArtifactName, Light> = BTreeMap::new();
121    for (name, node, completion) in &artifacts {
122        // The first light of its evals in precedence order.
123        let evals = node
124            .children
125            .iter()
126            .filter_map(|eval| match &eval.kind {
127                Kind::Eval(view) => Some(eval_light(eval, view)),
128                Kind::Artifact { .. } => None,
129            })
130            .min()
131            .unwrap_or(Light::Green);
132        // Every eval done, yet graph.rs still holds them on their own gates: not complete.
133        let light = match (evals, completion) {
134            (Light::Green, Completion::Waiting { .. }) => Light::from(*completion),
135            (light, _) => light,
136        };
137        lights.insert(name.clone(), light);
138    }
139    let position: BTreeMap<&ArtifactName, usize> = artifacts
140        .iter()
141        .enumerate()
142        .map(|(index, (name, _, _))| (name, index))
143        .collect();
144    let mut edges = Vec::new();
145    for (name, node, _) in &artifacts {
146        let upstream: BTreeSet<_> = node
147            .children
148            .iter()
149            .flat_map(|eval| eval.upstream.iter().map(|dependency| &dependency.artifact))
150            .collect();
151        for from in upstream {
152            let Some(&own) = lights.get(from).filter(|_| from != name) else {
153                continue;
154            };
155            edges.push(GraphEdge {
156                from: from.clone(),
157                to: name.clone(),
158                light: own,
159            });
160        }
161    }
162    edges.sort_by_key(|edge| (position[&edge.to], position[&edge.from]));
163    Graph {
164        nodes: artifacts
165            .into_iter()
166            .map(|(name, node, _)| GraphNode {
167                id: node.id.clone(),
168                light: lights[&name],
169                name,
170            })
171            .collect(),
172        edges,
173    }
174}