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;
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
68impl Light {
69    /// A RED, ERROR or blocked Artifact holds everything downstream.
70    fn holds(self) -> bool {
71        matches!(self, Self::Red | Self::Error | Self::Blocked)
72    }
73}
74
75/// An eval row's light. An eval waiting for an upstream Artifact that is RED, ERROR (which the
76/// tree shows as "waits for … (retry … first)") or itself blocked cannot proceed in this Run:
77/// blocked. `lights` holds the Artifacts already lit, upstream first.
78fn eval_light(eval: &Node, view: &EvalView, lights: &BTreeMap<ArtifactName, Light>) -> Light {
79    let light = Light::from(view);
80    let waits = matches!(
81        view,
82        EvalView::WaitingOn(_) | EvalView::NotRun(NotRun::Dependency(_))
83    );
84    let held = eval.upstream.iter().any(|upstream| {
85        lights
86            .get(&upstream.artifact)
87            .copied()
88            .unwrap_or(upstream.completion.into())
89            .holds()
90    });
91    if waits && held { Light::Blocked } else { light }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct GraphNode {
96    /// The Artifact row's tree id, `a:<artifact>`.
97    pub id: NodeId,
98    pub name: ArtifactName,
99    pub light: Light,
100}
101
102/// `to`'s evals depend on `from`.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct GraphEdge {
105    pub from: ArtifactName,
106    pub to: ArtifactName,
107    /// The most urgent light of `to`'s dependency evals naming `from`, or without one
108    /// `from`'s own light.
109    pub light: Light,
110    /// A dependency eval of `to` names `from`.
111    pub dependency: bool,
112}
113
114#[derive(Debug, Clone, Default, PartialEq, Eq)]
115pub struct Graph {
116    /// In the tree's order: upstream first where the graph allows.
117    pub nodes: Vec<GraphNode>,
118    /// By `to` in node order, then `from` in node order.
119    pub edges: Vec<GraphEdge>,
120}
121
122/// The graph of the Artifact rows of a Run's [`tree`](super::tree), lit upstream first in the
123/// tree's order.
124pub fn graph(tree: &[Node]) -> Graph {
125    let artifacts: Vec<(ArtifactName, &Node, Completion)> = tree
126        .iter()
127        .filter_map(|node| match node.kind {
128            Kind::Artifact { completion, .. } => match Target::from(&node.id) {
129                Target::Artifact(name) => Some((name, node, completion)),
130                _ => None,
131            },
132            Kind::Eval(_) => None,
133        })
134        .collect();
135    let mut lights: BTreeMap<ArtifactName, Light> = BTreeMap::new();
136    for (name, node, completion) in &artifacts {
137        // The first light of its evals in precedence order.
138        let evals = node
139            .children
140            .iter()
141            .filter_map(|eval| match &eval.kind {
142                Kind::Eval(view) => Some(eval_light(eval, view, &lights)),
143                Kind::Artifact { .. } => None,
144            })
145            .min()
146            .unwrap_or(Light::Green);
147        // Every eval done, yet graph.rs still holds them on their own gates: not complete.
148        let light = match (evals, completion) {
149            (Light::Green, Completion::Waiting { .. }) => Light::from(*completion),
150            (light, _) => light,
151        };
152        lights.insert(name.clone(), light);
153    }
154    let position: BTreeMap<&ArtifactName, usize> = artifacts
155        .iter()
156        .enumerate()
157        .map(|(index, (name, _, _))| (name, index))
158        .collect();
159    let mut edges = Vec::new();
160    for (name, node, _) in &artifacts {
161        // Each upstream Artifact, with the most urgent light of the dependency evals naming it.
162        let mut upstream: BTreeMap<&ArtifactName, Option<Light>> = BTreeMap::new();
163        for eval in &node.children {
164            let Kind::Eval(view) = &eval.kind else {
165                continue;
166            };
167            for dependency in &eval.upstream {
168                let lit = upstream.entry(&dependency.artifact).or_default();
169                if eval.dependency {
170                    let light = eval_light(eval, view, &lights);
171                    *lit = Some(lit.map_or(light, |lit| lit.min(light)));
172                }
173            }
174        }
175        for (from, lit) in upstream {
176            let Some(&own) = lights.get(from).filter(|_| from != name) else {
177                continue;
178            };
179            edges.push(GraphEdge {
180                from: from.clone(),
181                to: name.clone(),
182                light: lit.unwrap_or(own),
183                dependency: lit.is_some(),
184            });
185        }
186    }
187    edges.sort_by_key(|edge| (position[&edge.to], position[&edge.from]));
188    Graph {
189        nodes: artifacts
190            .into_iter()
191            .map(|(name, node, _)| GraphNode {
192                id: node.id.clone(),
193                light: lights[&name],
194                name,
195            })
196            .collect(),
197        edges,
198    }
199}