Skip to main content

artifactize_core/monitor/model/
states.rs

1//! Eval view states: what one tree row shows, derived from the saved definitions and the
2//! current request states with the readiness rules of [`crate::graph`].
3//!
4//! Each Eval has its own references and gates, and its target's children count as references;
5//! its own Artifact and that Artifact's cycle peers never gate it.
6use std::{
7    cell::RefCell,
8    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
9};
10
11use super::Saved;
12use crate::{
13    graph::EvalStatus,
14    store::RequestView,
15    types::{ArtifactName, EvalId, RequestStatus},
16};
17
18/// Where a done eval's GREEN came from.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Source {
21    Executed,
22    Provisional,
23    Reused,
24    /// A saved result the Run used for an eval it has no request for.
25    Saved,
26}
27
28/// Why a queued eval has not started although nothing upstream holds it.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum Queue {
31    /// Ready, waiting for scheduler admission without a Run cap.
32    Ready,
33    /// Waiting for the Run's explicitly chosen `--jobs` cap.
34    Jobs,
35    /// Waiting for a backend slot (limits.json); holds the backend name.
36    Slot(crate::config::Backend),
37    /// Waiting for the running execution of the same reuse key.
38    Joined,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum Activity {
43    Running,
44    /// The claimant, when someone has claimed it.
45    Human(Option<crate::types::ReviewerId>),
46    Queued(Queue),
47}
48
49/// The Artifacts an eval waits for, most actionable first, and one level of root cause when
50/// every one of them is itself waiting: `(via, root)` reads "via waits for root".
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Waits {
53    pub artifacts: Vec<crate::types::ArtifactName>,
54    pub root: Option<(crate::types::ArtifactName, crate::types::ArtifactName)>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum NotRun {
59    /// A finished Run ended while the eval still waited for these Artifacts.
60    Dependency(Waits),
61    Budget,
62    Stale,
63    Unreviewed,
64    /// The eval has no request in this Run.
65    Absent,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum EvalView {
70    Done(Source),
71    /// `verdict` is RED; otherwise an operational ERROR.
72    Failed {
73        verdict: bool,
74    },
75    InProgress(Activity),
76    /// Only while the Run is running.
77    WaitingOn(Waits),
78    /// Upstream failed with a RED verdict or is itself blocked.
79    BlockedBy(Vec<crate::types::ArtifactName>),
80    NotRun(NotRun),
81}
82
83/// Roll-up urgency, ordered from failed to done for eval rows and Artifact completions.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
85pub enum Rank {
86    Failed,
87    InProgress,
88    Blocked,
89    Waiting,
90    NotRun,
91    Done,
92}
93
94impl EvalView {
95    pub fn rank(&self) -> Rank {
96        match self {
97            Self::Failed { .. } => Rank::Failed,
98            Self::InProgress(_) => Rank::InProgress,
99            Self::BlockedBy(_) => Rank::Blocked,
100            Self::WaitingOn(_) => Rank::Waiting,
101            Self::NotRun(_) => Rank::NotRun,
102            Self::Done(_) => Rank::Done,
103        }
104    }
105}
106
107/// The most urgent kind of activity in an in-progress Artifact.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
109pub enum Busy {
110    Running,
111    Human,
112    Queued,
113}
114
115/// An Artifact's evaluation as a whole, as shown where another eval waits for it.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum Completion {
118    /// Every eval is done; also basis Artifacts and Artifacts without evals.
119    Complete,
120    InProgress(Busy),
121    /// Some eval waits, is blocked or did not run.
122    Waiting {
123        blocked: bool,
124    },
125    /// `verdict` when an eval is RED, otherwise only operational ERRORs.
126    Failed {
127        verdict: bool,
128    },
129}
130
131impl Completion {
132    /// Order of X: failed, in progress, waiting, complete; blocked completions still wait.
133    pub(super) fn rank(self) -> Rank {
134        match self {
135            Self::Failed { .. } => Rank::Failed,
136            Self::InProgress(_) => Rank::InProgress,
137            Self::Waiting { .. } => Rank::Waiting,
138            Self::Complete => Rank::Done,
139        }
140    }
141}
142
143/// One Artifact an eval depends on, with its current completion.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct Upstream {
146    pub artifact: crate::types::ArtifactName,
147    pub completion: Completion,
148}
149
150/// Per-Run index from saved definitions: display order, evals per Artifact and the
151/// Artifacts each eval depends on. Built in one pass over the definitions and requests.
152pub(super) struct Index<'a> {
153    pub order: Vec<&'a ArtifactName>,
154    position: HashMap<&'a ArtifactName, usize>,
155    requests: HashMap<&'a EvalId, &'a RequestView>,
156    pub evals: BTreeMap<&'a ArtifactName, Vec<&'a EvalId>>,
157    /// Eval → direct dependency Artifacts, in display order.
158    pub upstream: BTreeMap<&'a EvalId, Vec<&'a ArtifactName>>,
159    gates: BTreeMap<&'a EvalId, Vec<&'a EvalId>>,
160    execution_order: Vec<&'a EvalId>,
161    targets: BTreeMap<&'a EvalId, &'a ArtifactName>,
162    /// Artifact → its cycle peers (same component), by name.
163    pub peers: BTreeMap<&'a ArtifactName, Vec<&'a ArtifactName>>,
164    /// Artifact → the Artifacts it is directly composed of.
165    pub parts: BTreeMap<&'a ArtifactName, Vec<&'a ArtifactName>>,
166    pub cyclic: BTreeSet<&'a ArtifactName>,
167}
168
169type Component = crate::store::definitions::Component;
170
171fn members(component: &Component) -> Vec<&ArtifactName> {
172    component.artifacts.value().into_iter().flatten().collect()
173}
174
175impl<'a> Index<'a> {
176    pub fn new(saved: &Saved<'a>) -> Self {
177        let order = saved.artifact_ids();
178        let position: HashMap<_, _> = order
179            .iter()
180            .enumerate()
181            .map(|(index, id)| (*id, index))
182            .collect();
183        let mut requests = HashMap::new();
184        for view in saved.requests {
185            requests.entry(&view.request.eval_id).or_insert(view);
186        }
187        let definitions: HashMap<_, _> = saved
188            .eval_definitions()
189            .iter()
190            .map(|eval| (&eval.id, eval))
191            .collect();
192        // Saved Eval definitions targeting each Artifact, then requests without one.
193        let mut targets: HashMap<&ArtifactName, Vec<&EvalId>> = HashMap::new();
194        for eval in saved.eval_definitions() {
195            targets.entry(&eval.target).or_default().push(&eval.id);
196        }
197        for view in saved.requests {
198            let request = &view.request;
199            if !definitions.contains_key(&request.eval_id) {
200                let evals = targets.entry(&request.target).or_default();
201                if !evals.contains(&&request.eval_id) {
202                    evals.push(&request.eval_id);
203                }
204            }
205        }
206        let evals: BTreeMap<_, _> = order
207            .iter()
208            .map(|&artifact| {
209                let mut ids = targets.remove(artifact).unwrap_or_default();
210                ids.sort_unstable();
211                (artifact, ids)
212            })
213            .collect();
214        let components = saved.components();
215        let mut peers = BTreeMap::new();
216        let mut cyclic = BTreeSet::new();
217        for component in components {
218            let mut names = members(component);
219            names.sort_unstable();
220            for &artifact in &names {
221                peers.insert(
222                    artifact,
223                    names.iter().copied().filter(|id| *id != artifact).collect(),
224                );
225                if component.cyclic.value() == Some(&true) {
226                    cyclic.insert(artifact);
227                }
228            }
229        }
230        let targets: BTreeMap<_, _> = evals
231            .iter()
232            .flat_map(|(&target, ids)| ids.iter().map(move |&id| (id, target)))
233            .collect();
234        // An eval depends on what it references.
235        let inputs: BTreeMap<_, Vec<_>> = targets
236            .iter()
237            .map(|(&eval, _)| {
238                let deps: Vec<_> = definitions
239                    .get(eval)
240                    .and_then(|definition| definition.deps.value())
241                    .map(|deps| deps.iter().collect())
242                    .unwrap_or_else(|| {
243                        requests
244                            .get(eval)
245                            .map_or(Vec::new(), |view| view.request.deps.iter().collect())
246                    });
247                (eval, deps)
248            })
249            .collect();
250        let human = |eval: &EvalId| match requests.get(eval) {
251            Some(view) => view.request.profile.kind() == crate::config::ProfileKind::Human,
252            None => definitions
253                .get(eval)
254                .and_then(|definition| definition.declaration.value())
255                .and_then(|declaration| declaration.profile.value())
256                .is_some_and(|profile| {
257                    matches!(profile.known, crate::config::StoredProfile::Human { .. })
258                }),
259        };
260        let schedule =
261            crate::graph::schedule::Schedule::new(targets.iter().map(|(&eval, &target)| {
262                crate::graph::schedule::Input {
263                    id: eval,
264                    target,
265                    deps: inputs[eval].clone(),
266                    human: human(eval),
267                }
268            }));
269        let upstream = inputs
270            .into_iter()
271            .map(|(eval, mut deps)| {
272                deps.extend(schedule.gates[eval].iter().map(|gate| targets[gate]));
273                let mut deps: Vec<_> = deps
274                    .into_iter()
275                    .collect::<BTreeSet<_>>()
276                    .into_iter()
277                    .collect();
278                deps.sort_by_key(|id| position.get(id).copied().unwrap_or(usize::MAX));
279                (eval, deps)
280            })
281            .collect();
282        let execution_order = schedule.order;
283        Self {
284            order,
285            position,
286            requests,
287            evals,
288            upstream,
289            gates: schedule.gates,
290            execution_order,
291            targets,
292            peers,
293            parts: saved.parts(),
294            cyclic,
295        }
296    }
297}
298
299/// Lazily derived views over one Run, using the same Eval gates as live scheduling.
300pub(super) struct States<'a> {
301    run: &'a crate::store::Run,
302    #[cfg(any(test, feature = "test-support"))]
303    pub derivations: std::cell::Cell<usize>,
304    pub index: Index<'a>,
305    pub running: bool,
306    views: RefCell<HashMap<&'a EvalId, Option<EvalView>>>,
307    effective: RefCell<HashMap<&'a EvalId, Option<EvalStatus>>>,
308    completions: RefCell<HashMap<&'a ArtifactName, Completion>>,
309    waits: RefCell<HashMap<&'a ArtifactName, Vec<ArtifactName>>>,
310    held: HashMap<&'a EvalId, bool>,
311}
312
313impl<'a> States<'a> {
314    /// Derive every Artifact once, upstream before downstream, so each lookup afterwards
315    /// is memoized and no derivation recurses along a long dependency chain.
316    pub fn new(saved: &Saved<'a>) -> Self {
317        let run = &saved.run.run;
318        let mut states = Self {
319            run,
320            #[cfg(any(test, feature = "test-support"))]
321            derivations: std::cell::Cell::new(0),
322            index: Index::new(saved),
323            running: run.status().is_open(),
324            views: RefCell::new(HashMap::new()),
325            effective: RefCell::new(HashMap::new()),
326            completions: RefCell::new(HashMap::new()),
327            waits: RefCell::new(HashMap::new()),
328            held: HashMap::new(),
329        };
330        for &eval in &states.index.execution_order {
331            states.effective(eval);
332            let held = states.index.gates[eval].iter().any(|&gate| {
333                matches!(
334                    states.effective(gate),
335                    EvalStatus::Red | EvalStatus::Blocked | EvalStatus::Error
336                ) || states.held[gate]
337            });
338            states.held.insert(eval, held);
339        }
340        for artifact in states.upstream_first() {
341            states.completion(artifact);
342            states.waits_of(artifact);
343        }
344        states
345    }
346
347    /// Every Artifact after the Artifacts its evals depend on (an iterative depth-first
348    /// post-order; projected Artifact cycles may come either way).
349    fn upstream_first(&self) -> Vec<&'a ArtifactName> {
350        let mut done = HashSet::new();
351        let mut seen = HashSet::new();
352        let mut ordered = Vec::with_capacity(self.index.order.len());
353        for &start in &self.index.order {
354            if !seen.insert(start) {
355                continue;
356            }
357            let mut stack = vec![(start, self.dependencies(start), 0)];
358            while let Some((artifact, dependencies, next)) = stack.last_mut() {
359                if let Some(&dependency) = dependencies.get(*next) {
360                    *next += 1;
361                    if seen.insert(dependency) {
362                        let dependencies = self.dependencies(dependency);
363                        stack.push((dependency, dependencies, 0));
364                    }
365                } else {
366                    if done.insert(*artifact) {
367                        ordered.push(*artifact);
368                    }
369                    stack.pop();
370                }
371            }
372        }
373        ordered
374    }
375
376    fn dependencies(&self, artifact: &ArtifactName) -> Vec<&'a ArtifactName> {
377        let mut dependencies: Vec<&'a ArtifactName> = Vec::new();
378        for eval in self.evals(artifact) {
379            for &upstream in self.upstream(eval) {
380                if !dependencies.contains(&upstream) {
381                    dependencies.push(upstream);
382                }
383            }
384        }
385        dependencies
386    }
387
388    /// The Artifact id as borrowed from the index, for memo keys.
389    fn key(&self, artifact: &ArtifactName) -> Option<&'a ArtifactName> {
390        self.index
391            .position
392            .get_key_value(artifact)
393            .map(|(&id, _)| id)
394    }
395
396    pub fn request(&self, eval: &EvalId) -> Option<&'a RequestView> {
397        self.index.requests.get(eval).copied()
398    }
399
400    /// Evidence the graph used for an eval without a request in this Run: the Run's
401    /// record of saved results, or for older Runs the validation saved at their end.
402    fn outside(&self, eval: &EvalId) -> Option<RequestStatus> {
403        if let Some(status) = self.run.evidence.get(eval) {
404            return Some(*status);
405        }
406        let saved = self.run.validation.snapshot()?.evals.value()?;
407        let status = *saved
408            .iter()
409            .find(|saved| &saved.id == eval)?
410            .status
411            .value()?;
412        matches!(
413            status,
414            RequestStatus::Green | RequestStatus::Red | RequestStatus::Error | RequestStatus::Stale
415        )
416        .then_some(status)
417    }
418
419    pub fn evals(&self, artifact: &ArtifactName) -> &[&'a EvalId] {
420        self.index.evals.get(artifact).map_or(&[], Vec::as_slice)
421    }
422
423    pub fn upstream(&self, eval: &EvalId) -> &[&'a ArtifactName] {
424        self.index.upstream.get(eval).map_or(&[], Vec::as_slice)
425    }
426
427    fn position(&self, artifact: &ArtifactName) -> usize {
428        self.index
429            .position
430            .get(artifact)
431            .copied()
432            .unwrap_or(usize::MAX)
433    }
434
435    pub fn view(&self, eval: &'a EvalId) -> EvalView {
436        if let Some(view) = self.views.borrow().get(eval) {
437            // Artifact-level presentation can revisit an Eval through a projected cycle.
438            return view.clone().unwrap_or(EvalView::NotRun(NotRun::Unreviewed));
439        }
440        self.views.borrow_mut().insert(eval, None);
441        #[cfg(any(test, feature = "test-support"))]
442        self.derivations.set(self.derivations.get() + 1);
443        let view = self.derive(eval);
444        self.views.borrow_mut().insert(eval, Some(view.clone()));
445        view
446    }
447
448    /// The eval's status as `graph.rs` evaluates it for gates: a GREEN result whose own
449    /// gates are unmet still waits or is blocked, and never fulfils a downstream gate.
450    pub fn effective(&self, eval: &'a EvalId) -> EvalStatus {
451        if let Some(status) = self.effective.borrow().get(eval) {
452            // Eval gates are acyclic; a revisit cannot supply a verdict.
453            return status.unwrap_or(EvalStatus::Wait);
454        }
455        self.effective.borrow_mut().insert(eval, None);
456        #[cfg(any(test, feature = "test-support"))]
457        self.derivations.set(self.derivations.get() + 1);
458        let status = self.evaluate(eval);
459        self.effective.borrow_mut().insert(eval, Some(status));
460        status
461    }
462
463    /// `Graph::evaluate` over the saved snapshot: gate readiness first, then
464    /// the eval's own evidence.
465    fn evaluate(&self, eval: &'a EvalId) -> EvalStatus {
466        let request = self.request(eval).map(|view| &view.request);
467        let status = request
468            .map(|request| request.status())
469            .or_else(|| self.outside(eval));
470        let unmet: Vec<_> = self
471            .index
472            .gates
473            .get(eval)
474            .into_iter()
475            .flatten()
476            .map(|&gate| (gate, self.effective(gate)))
477            .filter(|(_, status)| !status.passes_gate())
478            .collect();
479        if unmet
480            .iter()
481            .any(|&(gate, status)| self.status_blocks(eval, gate, status))
482        {
483            return EvalStatus::Blocked;
484        }
485        if !unmet.is_empty() {
486            return EvalStatus::Wait;
487        }
488        match status {
489            Some(RequestStatus::Deferred) => EvalStatus::Deferred,
490            // A provisional result stands once nothing it relies on is pending any more.
491            Some(RequestStatus::Provisional) if self.pending(eval).is_empty() => EvalStatus::Green,
492            Some(RequestStatus::Provisional) => EvalStatus::Provisional,
493            Some(RequestStatus::Green) => EvalStatus::Green,
494            Some(RequestStatus::Red) => EvalStatus::Red,
495            Some(RequestStatus::Error) => EvalStatus::Error,
496            Some(RequestStatus::Stale) => EvalStatus::Stale,
497            _ => EvalStatus::Unreviewed,
498        }
499    }
500
501    fn met(&self, artifact: &ArtifactName) -> bool {
502        self.evals(artifact)
503            .iter()
504            .all(|eval| self.effective(eval) == EvalStatus::Green)
505    }
506
507    fn derive(&self, eval: &'a EvalId) -> EvalView {
508        let Some(view) = self.request(eval) else {
509            // A saved result the Run used without a request of its own.
510            return match self.outside(eval) {
511                Some(RequestStatus::Green) => EvalView::Done(Source::Saved),
512                Some(RequestStatus::Red) => EvalView::Failed { verdict: true },
513                Some(RequestStatus::Error) => EvalView::Failed { verdict: false },
514                Some(RequestStatus::Stale) => EvalView::NotRun(NotRun::Stale),
515                _ => EvalView::NotRun(NotRun::Absent),
516            };
517        };
518        let request = &view.request;
519        match request.status() {
520            RequestStatus::Provisional if self.effective(eval) == EvalStatus::Provisional => {
521                EvalView::Done(Source::Provisional)
522            }
523            RequestStatus::Provisional | RequestStatus::Green => {
524                EvalView::Done(if crate::query::reused(request) {
525                    Source::Reused
526                } else {
527                    Source::Executed
528                })
529            }
530            RequestStatus::Red => EvalView::Failed { verdict: true },
531            RequestStatus::Error => EvalView::Failed { verdict: false },
532            RequestStatus::Running => EvalView::InProgress(Activity::Running),
533            RequestStatus::WaitingHuman | RequestStatus::Deferred => EvalView::InProgress(
534                Activity::Human(view.claim.as_ref().map(|claim| claim.reviewer.clone())),
535            ),
536            RequestStatus::BudgetExhausted => EvalView::NotRun(NotRun::Budget),
537            RequestStatus::Stale => EvalView::NotRun(NotRun::Stale),
538            RequestStatus::Queued
539            | RequestStatus::WaitDependency
540            | RequestStatus::Unreviewed
541            | RequestStatus::Blocked => {
542                let unmet = self.unmet(eval);
543                let blocking: Vec<_> = unmet
544                    .iter()
545                    .filter(|artifact| self.gate_blocks(eval, artifact))
546                    .cloned()
547                    .collect();
548                if !blocking.is_empty() {
549                    return EvalView::BlockedBy(blocking);
550                }
551                if self.run.modes.speculate {
552                    return self.ready(view);
553                }
554                if !unmet.is_empty() {
555                    let waits = Waits {
556                        root: self.root(&unmet),
557                        artifacts: unmet,
558                    };
559                    return if self.running {
560                        EvalView::WaitingOn(waits)
561                    } else {
562                        EvalView::NotRun(NotRun::Dependency(waits))
563                    };
564                }
565                self.ready(view)
566            }
567        }
568    }
569
570    /// Nothing upstream holds the eval.
571    fn ready(&self, view: &RequestView) -> EvalView {
572        let request = &view.request;
573        if !self.running || request.status() != RequestStatus::Queued {
574            return EvalView::NotRun(NotRun::Unreviewed);
575        }
576        EvalView::InProgress(Activity::Queued(
577            if let Some(crate::store::QueueReason::Slot { backend }) = request.queue.clone() {
578                Queue::Slot(backend)
579            } else if request.execution_id.is_some() {
580                Queue::Joined
581            } else if self.run.jobs.is_some() {
582                Queue::Jobs
583            } else {
584                Queue::Ready
585            },
586        ))
587    }
588
589    /// The Artifacts of this Eval's gates that pass only provisionally: a sign-off not yet
590    /// submitted, or a result that relies on one.
591    fn pending(&self, eval: &EvalId) -> Vec<ArtifactName> {
592        let mut artifacts: Vec<_> = self
593            .index
594            .gates
595            .get(eval)
596            .into_iter()
597            .flatten()
598            .filter(|&&gate| {
599                matches!(
600                    self.effective(gate),
601                    EvalStatus::Deferred | EvalStatus::Provisional
602                )
603            })
604            .map(|gate| self.index.targets[gate])
605            .collect::<BTreeSet<_>>()
606            .into_iter()
607            .collect();
608        artifacts
609            .sort_by_key(|artifact| (self.completion(artifact).rank(), self.position(artifact)));
610        artifacts.into_iter().cloned().collect()
611    }
612
613    /// Only this Eval's gates contribute waits; its own Artifact and cycle peers never do.
614    fn unmet(&self, eval: &EvalId) -> Vec<ArtifactName> {
615        let mut artifacts: Vec<_> = self
616            .index
617            .gates
618            .get(eval)
619            .into_iter()
620            .flatten()
621            .filter(|&&gate| !self.effective(gate).passes_gate())
622            .map(|gate| self.index.targets[gate])
623            .collect::<BTreeSet<_>>()
624            .into_iter()
625            .collect();
626        artifacts
627            .sort_by_key(|artifact| (self.completion(artifact).rank(), self.position(artifact)));
628        artifacts.into_iter().cloned().collect()
629    }
630
631    fn views_of(&self, artifact: &ArtifactName) -> Vec<EvalView> {
632        self.evals(artifact)
633            .iter()
634            .map(|eval| self.view(eval))
635            .collect()
636    }
637
638    /// The Artifact's completion from its eval rows; done rows whose own gates are unmet
639    /// do not complete it. A part that is not complete keeps its parent from it too.
640    pub fn completion(&self, artifact: &ArtifactName) -> Completion {
641        if let Some(completion) = self.completions.borrow().get(artifact) {
642            return *completion;
643        }
644        let own = self.own_completion(artifact);
645        let completion = self
646            .index
647            .parts
648            .get(artifact)
649            .into_iter()
650            .flatten()
651            .map(|part| self.completion(part))
652            .chain([own])
653            .min_by_key(|completion| match completion {
654                Completion::Failed { verdict } => (completion.rank(), *verdict),
655                _ => (completion.rank(), true),
656            })
657            .expect("its own completion");
658        if let Some(id) = self.key(artifact) {
659            self.completions.borrow_mut().insert(id, completion);
660        }
661        completion
662    }
663
664    fn own_completion(&self, artifact: &ArtifactName) -> Completion {
665        let views = self.views_of(artifact);
666        let busy = views
667            .iter()
668            .filter_map(|view| match view {
669                EvalView::InProgress(Activity::Running) => Some(Busy::Running),
670                EvalView::InProgress(Activity::Human(_)) => Some(Busy::Human),
671                EvalView::InProgress(Activity::Queued(_)) => Some(Busy::Queued),
672                _ => None,
673            })
674            .min();
675        if views
676            .iter()
677            .any(|view| matches!(view, EvalView::Failed { .. }))
678        {
679            // ERROR before RED, like the Artifact's own row; blocking follows `blocks`.
680            Completion::Failed {
681                verdict: !views.contains(&EvalView::Failed { verdict: false }),
682            }
683        } else if let Some(busy) = busy {
684            Completion::InProgress(busy)
685        } else if views.iter().any(|view| !matches!(view, EvalView::Done(_))) || !self.met(artifact)
686        {
687            Completion::Waiting {
688                blocked: self.blocks(artifact)
689                    || views
690                        .iter()
691                        .any(|view| matches!(view, EvalView::BlockedBy(_))),
692            }
693        } else {
694            Completion::Complete
695        }
696    }
697
698    /// Group only this Eval's actual gates, never unrelated Evals on the same material.
699    fn gate_blocks(&self, eval: &EvalId, artifact: &ArtifactName) -> bool {
700        self.index
701            .gates
702            .get(eval)
703            .into_iter()
704            .flatten()
705            .filter(|gate| self.index.targets[*gate] == artifact)
706            .any(|&gate| self.status_blocks(eval, gate, self.effective(gate)))
707    }
708
709    /// A RED or blocked gate blocks; an ERROR blocks under `--speculate`, and always when the
710    /// gate is a machine eval of a Human eval's own Artifact.
711    fn status_blocks(&self, eval: &EvalId, gate: &EvalId, status: EvalStatus) -> bool {
712        match status {
713            EvalStatus::Red | EvalStatus::Blocked => true,
714            EvalStatus::Error => {
715                self.run.modes.speculate
716                    || self.index.targets.get(gate) == self.index.targets.get(eval)
717            }
718            _ => false,
719        }
720    }
721
722    /// Graph lights treat an external ERROR as held too, without treating cycle peers
723    /// or unrelated sibling Evals as gates.
724    pub fn held_for_run(&self, eval: &EvalId) -> bool {
725        self.held.get(eval).copied().unwrap_or(false)
726    }
727
728    /// The graph's Blocked readiness downstream: an effective RED or blocked eval.
729    fn blocks(&self, artifact: &ArtifactName) -> bool {
730        self.evals(artifact).iter().any(|eval| {
731            matches!(self.effective(eval), EvalStatus::Red | EvalStatus::Blocked)
732                || (self.run.modes.speculate && self.effective(eval) == EvalStatus::Error)
733        })
734    }
735
736    /// Done evals whose own gates are unmet: the Artifacts holding them back.
737    fn held(&self, artifact: &ArtifactName, blocked: bool) -> Vec<crate::types::ArtifactName> {
738        let mut x: Vec<crate::types::ArtifactName> = Vec::new();
739        for &eval in self.evals(artifact) {
740            let wanted = if blocked {
741                EvalStatus::Blocked
742            } else {
743                EvalStatus::Wait
744            };
745            if matches!(self.view(eval), EvalView::Done(_)) && self.effective(eval) == wanted {
746                for id in self.unmet(eval) {
747                    if (!blocked || self.gate_blocks(eval, &id)) && !x.contains(&id) {
748                        x.push(id);
749                    }
750                }
751            }
752            // A provisional result waits for the sign-offs it relies on.
753            if !blocked && self.effective(eval) == EvalStatus::Provisional {
754                for id in self.pending(eval) {
755                    if !x.contains(&id) {
756                        x.push(id);
757                    }
758                }
759            }
760        }
761        x
762    }
763
764    /// The Artifacts this Artifact's evals wait for, as one X list.
765    pub fn waits_of(&self, artifact: &ArtifactName) -> Vec<crate::types::ArtifactName> {
766        if let Some(x) = self.waits.borrow().get(artifact) {
767            return x.clone();
768        }
769        let mut x = self.held(artifact, false);
770        for view in self.views_of(artifact) {
771            if let EvalView::WaitingOn(waits) | EvalView::NotRun(NotRun::Dependency(waits)) = view {
772                for id in waits.artifacts {
773                    if !x.contains(&id) {
774                        x.push(id);
775                    }
776                }
777            }
778        }
779        x.sort_by_key(|id| (self.completion(id).rank(), self.position(id)));
780        if let Some(id) = self.key(artifact) {
781            self.waits.borrow_mut().insert(id, x.clone());
782        }
783        x
784    }
785
786    /// The Artifacts this Artifact's blocked evals are blocked by.
787    pub fn blockers_of(&self, artifact: &ArtifactName) -> Vec<crate::types::ArtifactName> {
788        let mut x = self.held(artifact, true);
789        for view in self.views_of(artifact) {
790            if let EvalView::BlockedBy(blockers) = view {
791                for id in blockers {
792                    if !x.contains(&id) {
793                        x.push(id);
794                    }
795                }
796            }
797        }
798        x
799    }
800
801    /// When every X is itself waiting, exactly one level of cause: what the first X waits for.
802    fn root(
803        &self,
804        x: &[crate::types::ArtifactName],
805    ) -> Option<(crate::types::ArtifactName, crate::types::ArtifactName)> {
806        if x.iter()
807            .any(|id| !matches!(self.completion(id), Completion::Waiting { blocked: false }))
808        {
809            return None;
810        }
811        let via = x.first()?.clone();
812        let cause = self.waits_of(&via).into_iter().next()?;
813        Some((via, cause))
814    }
815
816    /// Every dependency Artifact of the eval, unmet ones first in X order.
817    pub fn upstream_states(&self, eval: &EvalId) -> Vec<Upstream> {
818        let mut upstream: Vec<_> = self
819            .upstream(eval)
820            .iter()
821            .map(|artifact| Upstream {
822                artifact: (*artifact).clone(),
823                completion: self.completion(artifact),
824            })
825            .collect();
826        upstream.sort_by_key(|upstream| {
827            (
828                upstream.completion.rank(),
829                self.position(&upstream.artifact),
830            )
831        });
832        upstream
833    }
834}