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
107impl Graph {
108    /// Whether `other` has the same Artifacts and edges in the same order, whatever their
109    /// lights: a view can keep where it placed them and only relight them.
110    pub fn same_shape(&self, other: &Self) -> bool {
111        self.nodes.len() == other.nodes.len()
112            && self.edges.len() == other.edges.len()
113            && self
114                .nodes
115                .iter()
116                .zip(&other.nodes)
117                .all(|(one, other)| one.id == other.id)
118            && self
119                .edges
120                .iter()
121                .zip(&other.edges)
122                .all(|(one, other)| one.from == other.from && one.to == other.to)
123    }
124}
125
126/// The graph of the Artifact rows of a Run's [`tree`](super::tree), lit upstream first in the
127/// tree's order.
128pub fn graph(tree: &[Node]) -> Graph {
129    let artifacts: Vec<(ArtifactName, &Node, Completion)> = tree
130        .iter()
131        .filter_map(|node| match node.kind {
132            Kind::Artifact { completion, .. } => match Target::from(&node.id) {
133                Target::Artifact(name) => Some((name, node, completion)),
134                _ => None,
135            },
136            Kind::Eval(_) => None,
137        })
138        .collect();
139    let mut lights: BTreeMap<ArtifactName, Light> = BTreeMap::new();
140    for (name, node, completion) in &artifacts {
141        // The first light of its evals in precedence order.
142        let evals = node
143            .children
144            .iter()
145            .filter_map(|eval| match &eval.kind {
146                Kind::Eval(view) => Some(eval_light(eval, view)),
147                Kind::Artifact { .. } => None,
148            })
149            .min()
150            .unwrap_or(Light::Green);
151        // Every eval done, yet graph.rs still holds them on their own gates: not complete.
152        let light = match (evals, completion) {
153            (Light::Green, Completion::Waiting { .. }) => Light::from(*completion),
154            (light, _) => light,
155        };
156        lights.insert(name.clone(), light);
157    }
158    let position: BTreeMap<&ArtifactName, usize> = artifacts
159        .iter()
160        .enumerate()
161        .map(|(index, (name, _, _))| (name, index))
162        .collect();
163    let mut edges = Vec::new();
164    for (name, node, _) in &artifacts {
165        let upstream: BTreeSet<_> = node
166            .children
167            .iter()
168            .flat_map(|eval| eval.upstream.iter().map(|dependency| &dependency.artifact))
169            .collect();
170        for from in upstream {
171            let Some(&own) = lights.get(from).filter(|_| from != name) else {
172                continue;
173            };
174            edges.push(GraphEdge {
175                from: from.clone(),
176                to: name.clone(),
177                light: own,
178            });
179        }
180    }
181    edges.sort_by_key(|edge| (position[&edge.to], position[&edge.from]));
182    Graph {
183        nodes: artifacts
184            .into_iter()
185            .map(|(name, node, _)| GraphNode {
186                id: node.id.clone(),
187                light: lights[&name],
188                name,
189            })
190            .collect(),
191        edges,
192    }
193}