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, RunStatus},
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    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    pub cyclic: BTreeSet<&'a ArtifactName>,
165}
166
167type Component = crate::store::definitions::Component;
168
169fn members(component: &Component) -> Vec<&ArtifactName> {
170    component.artifacts.value().into_iter().flatten().collect()
171}
172
173impl<'a> Index<'a> {
174    pub fn new(saved: &Saved<'a>) -> Self {
175        let order = saved.artifact_ids();
176        let position: HashMap<_, _> = order
177            .iter()
178            .enumerate()
179            .map(|(index, id)| (*id, index))
180            .collect();
181        let mut requests = HashMap::new();
182        for view in saved.requests {
183            requests.entry(&view.request.eval_id).or_insert(view);
184        }
185        let definitions: HashMap<_, _> = saved
186            .eval_definitions()
187            .iter()
188            .map(|eval| (&eval.id, eval))
189            .collect();
190        // Saved Eval definitions targeting each Artifact, then requests without one.
191        let mut targets: HashMap<&ArtifactName, Vec<&EvalId>> = HashMap::new();
192        for eval in saved.eval_definitions() {
193            targets.entry(&eval.target).or_default().push(&eval.id);
194        }
195        for view in saved.requests {
196            let request = &view.request;
197            if !definitions.contains_key(&request.eval_id) {
198                let evals = targets.entry(&request.target).or_default();
199                if !evals.contains(&&request.eval_id) {
200                    evals.push(&request.eval_id);
201                }
202            }
203        }
204        let evals: BTreeMap<_, _> = order
205            .iter()
206            .map(|&artifact| {
207                let mut ids = targets.remove(artifact).unwrap_or_default();
208                ids.sort_unstable();
209                (artifact, ids)
210            })
211            .collect();
212        let components = saved.components();
213        let mut peers = BTreeMap::new();
214        let mut cyclic = BTreeSet::new();
215        for component in components {
216            let mut names = members(component);
217            names.sort_unstable();
218            for &artifact in &names {
219                peers.insert(
220                    artifact,
221                    names.iter().copied().filter(|id| *id != artifact).collect(),
222                );
223                if component.cyclic.value() == Some(&true) {
224                    cyclic.insert(artifact);
225                }
226            }
227        }
228        let targets: BTreeMap<_, _> = evals
229            .iter()
230            .flat_map(|(&target, ids)| ids.iter().map(move |&id| (id, target)))
231            .collect();
232        let children: BTreeMap<_, Vec<_>> = saved
233            .artifacts()
234            .map(|(parent, artifact)| {
235                let children = artifact.children.value().into_iter().flatten();
236                (parent, children.map(|(_, child)| child).collect())
237            })
238            .collect();
239        // An eval depends on what it references and, like every eval of its target, on the
240        // target's children.
241        let inputs: BTreeMap<_, Vec<_>> = targets
242            .iter()
243            .map(|(&eval, &target)| {
244                let mut deps: Vec<_> = definitions
245                    .get(eval)
246                    .and_then(|definition| definition.deps.value())
247                    .map(|deps| deps.iter().collect())
248                    .unwrap_or_else(|| {
249                        requests
250                            .get(eval)
251                            .map_or(Vec::new(), |view| view.request.deps.iter().collect())
252                    });
253                deps.extend(children.get(target).into_iter().flatten().copied());
254                (eval, deps)
255            })
256            .collect();
257        let human = |eval: &EvalId| match requests.get(eval) {
258            Some(view) => view.request.profile.kind() == crate::config::ProfileKind::Human,
259            None => definitions
260                .get(eval)
261                .and_then(|definition| definition.declaration.value())
262                .and_then(|declaration| declaration.profile.value())
263                .is_some_and(|profile| {
264                    matches!(profile.known, crate::config::StoredProfile::Human { .. })
265                }),
266        };
267        let schedule = crate::graph::schedule::Schedule::new(
268            targets
269                .iter()
270                .map(|(&eval, &target)| crate::graph::schedule::Input {
271                    id: eval,
272                    target,
273                    deps: inputs[eval].clone(),
274                    human: human(eval),
275                }),
276            children
277                .iter()
278                .flat_map(|(&parent, children)| children.iter().map(move |&child| (parent, child))),
279        );
280        let upstream = inputs
281            .into_iter()
282            .map(|(eval, mut deps)| {
283                deps.extend(schedule.gates[eval].iter().map(|gate| targets[gate]));
284                let mut deps: Vec<_> = deps
285                    .into_iter()
286                    .collect::<BTreeSet<_>>()
287                    .into_iter()
288                    .collect();
289                deps.sort_by_key(|id| position.get(id).copied().unwrap_or(usize::MAX));
290                (eval, deps)
291            })
292            .collect();
293        let execution_order = schedule.order;
294        Self {
295            order,
296            position,
297            requests,
298            evals,
299            upstream,
300            gates: schedule.gates,
301            execution_order,
302            targets,
303            peers,
304            cyclic,
305        }
306    }
307}
308
309/// Lazily derived views over one Run, using the same Eval gates as live scheduling.
310pub(super) struct States<'a> {
311    run: &'a crate::store::Run,
312    #[cfg(any(test, feature = "test-support"))]
313    pub derivations: std::cell::Cell<usize>,
314    pub index: Index<'a>,
315    pub running: bool,
316    views: RefCell<HashMap<&'a EvalId, Option<EvalView>>>,
317    effective: RefCell<HashMap<&'a EvalId, Option<EvalStatus>>>,
318    completions: RefCell<HashMap<&'a ArtifactName, Completion>>,
319    waits: RefCell<HashMap<&'a ArtifactName, Vec<ArtifactName>>>,
320    held: HashMap<&'a EvalId, bool>,
321}
322
323impl<'a> States<'a> {
324    /// Derive every Artifact once, upstream before downstream, so each lookup afterwards
325    /// is memoized and no derivation recurses along a long dependency chain.
326    pub fn new(saved: &Saved<'a>) -> Self {
327        let run = &saved.run.run;
328        let mut states = Self {
329            run,
330            #[cfg(any(test, feature = "test-support"))]
331            derivations: std::cell::Cell::new(0),
332            index: Index::new(saved),
333            running: run.status() == RunStatus::Running,
334            views: RefCell::new(HashMap::new()),
335            effective: RefCell::new(HashMap::new()),
336            completions: RefCell::new(HashMap::new()),
337            waits: RefCell::new(HashMap::new()),
338            held: HashMap::new(),
339        };
340        for &eval in &states.index.execution_order {
341            states.effective(eval);
342            let held = states.index.gates[eval].iter().any(|&gate| {
343                matches!(
344                    states.effective(gate),
345                    EvalStatus::Red | EvalStatus::Blocked | EvalStatus::Error
346                ) || states.held[gate]
347            });
348            states.held.insert(eval, held);
349        }
350        for artifact in states.upstream_first() {
351            states.completion(artifact);
352            states.waits_of(artifact);
353        }
354        states
355    }
356
357    /// Every Artifact after the Artifacts its evals depend on (an iterative depth-first
358    /// post-order; projected Artifact cycles may come either way).
359    fn upstream_first(&self) -> Vec<&'a ArtifactName> {
360        let mut done = HashSet::new();
361        let mut seen = HashSet::new();
362        let mut ordered = Vec::with_capacity(self.index.order.len());
363        for &start in &self.index.order {
364            if !seen.insert(start) {
365                continue;
366            }
367            let mut stack = vec![(start, self.dependencies(start), 0)];
368            while let Some((artifact, dependencies, next)) = stack.last_mut() {
369                if let Some(&dependency) = dependencies.get(*next) {
370                    *next += 1;
371                    if seen.insert(dependency) {
372                        let dependencies = self.dependencies(dependency);
373                        stack.push((dependency, dependencies, 0));
374                    }
375                } else {
376                    if done.insert(*artifact) {
377                        ordered.push(*artifact);
378                    }
379                    stack.pop();
380                }
381            }
382        }
383        ordered
384    }
385
386    fn dependencies(&self, artifact: &ArtifactName) -> Vec<&'a ArtifactName> {
387        let mut dependencies: Vec<&'a ArtifactName> = Vec::new();
388        for eval in self.evals(artifact) {
389            for &upstream in self.upstream(eval) {
390                if !dependencies.contains(&upstream) {
391                    dependencies.push(upstream);
392                }
393            }
394        }
395        dependencies
396    }
397
398    /// The Artifact id as borrowed from the index, for memo keys.
399    fn key(&self, artifact: &ArtifactName) -> Option<&'a ArtifactName> {
400        self.index
401            .position
402            .get_key_value(artifact)
403            .map(|(&id, _)| id)
404    }
405
406    pub fn request(&self, eval: &EvalId) -> Option<&'a RequestView> {
407        self.index.requests.get(eval).copied()
408    }
409
410    /// Evidence the graph used for an eval without a request in this Run: the Run's
411    /// record of saved results, or for older Runs the validation saved at their end.
412    fn outside(&self, eval: &EvalId) -> Option<RequestStatus> {
413        if let Some(status) = self.run.evidence.get(eval) {
414            return Some(*status);
415        }
416        let saved = self.run.validation.snapshot()?.evals.value()?;
417        let status = *saved
418            .iter()
419            .find(|saved| &saved.id == eval)?
420            .status
421            .value()?;
422        matches!(
423            status,
424            RequestStatus::Green | RequestStatus::Red | RequestStatus::Error | RequestStatus::Stale
425        )
426        .then_some(status)
427    }
428
429    pub fn evals(&self, artifact: &ArtifactName) -> &[&'a EvalId] {
430        self.index.evals.get(artifact).map_or(&[], Vec::as_slice)
431    }
432
433    pub fn upstream(&self, eval: &EvalId) -> &[&'a ArtifactName] {
434        self.index.upstream.get(eval).map_or(&[], Vec::as_slice)
435    }
436
437    fn position(&self, artifact: &ArtifactName) -> usize {
438        self.index
439            .position
440            .get(artifact)
441            .copied()
442            .unwrap_or(usize::MAX)
443    }
444
445    pub fn view(&self, eval: &'a EvalId) -> EvalView {
446        if let Some(view) = self.views.borrow().get(eval) {
447            // Artifact-level presentation can revisit an Eval through a projected cycle.
448            return view.clone().unwrap_or(EvalView::NotRun(NotRun::Unreviewed));
449        }
450        self.views.borrow_mut().insert(eval, None);
451        #[cfg(any(test, feature = "test-support"))]
452        self.derivations.set(self.derivations.get() + 1);
453        let view = self.derive(eval);
454        self.views.borrow_mut().insert(eval, Some(view.clone()));
455        view
456    }
457
458    /// The eval's status as `graph.rs` evaluates it for gates: a GREEN result whose own
459    /// gates are unmet still waits or is blocked, and never fulfils a downstream gate.
460    pub fn effective(&self, eval: &'a EvalId) -> EvalStatus {
461        if let Some(status) = self.effective.borrow().get(eval) {
462            // Eval gates are acyclic; a revisit cannot supply a verdict.
463            return status.unwrap_or(EvalStatus::Wait);
464        }
465        self.effective.borrow_mut().insert(eval, None);
466        #[cfg(any(test, feature = "test-support"))]
467        self.derivations.set(self.derivations.get() + 1);
468        let status = self.evaluate(eval);
469        self.effective.borrow_mut().insert(eval, Some(status));
470        status
471    }
472
473    /// `Graph::evaluate` over the saved snapshot: gate readiness first, then
474    /// the eval's own evidence.
475    fn evaluate(&self, eval: &'a EvalId) -> EvalStatus {
476        let request = self.request(eval).map(|view| &view.request);
477        let status = request
478            .map(|request| request.status())
479            .or_else(|| self.outside(eval));
480        let unmet: Vec<_> = self
481            .index
482            .gates
483            .get(eval)
484            .into_iter()
485            .flatten()
486            .map(|&gate| (gate, self.effective(gate)))
487            .filter(|(_, status)| !status.passes_gate())
488            .collect();
489        if unmet
490            .iter()
491            .any(|&(gate, status)| self.status_blocks(eval, gate, status))
492        {
493            return EvalStatus::Blocked;
494        }
495        if !unmet.is_empty() {
496            return EvalStatus::Wait;
497        }
498        match status {
499            Some(RequestStatus::Deferred) => EvalStatus::Deferred,
500            Some(RequestStatus::Provisional) => EvalStatus::Provisional,
501            Some(RequestStatus::Green) => EvalStatus::Green,
502            Some(RequestStatus::Red) => EvalStatus::Red,
503            Some(RequestStatus::Error) => EvalStatus::Error,
504            Some(RequestStatus::Stale) => EvalStatus::Stale,
505            _ => EvalStatus::Unreviewed,
506        }
507    }
508
509    fn met(&self, artifact: &ArtifactName) -> bool {
510        self.evals(artifact)
511            .iter()
512            .all(|eval| self.effective(eval) == EvalStatus::Green)
513    }
514
515    fn derive(&self, eval: &'a EvalId) -> EvalView {
516        let Some(view) = self.request(eval) else {
517            // A saved result the Run used without a request of its own.
518            return match self.outside(eval) {
519                Some(RequestStatus::Green) => EvalView::Done(Source::Saved),
520                Some(RequestStatus::Red) => EvalView::Failed { verdict: true },
521                Some(RequestStatus::Error) => EvalView::Failed { verdict: false },
522                Some(RequestStatus::Stale) => EvalView::NotRun(NotRun::Stale),
523                _ => EvalView::NotRun(NotRun::Absent),
524            };
525        };
526        let request = &view.request;
527        match request.status() {
528            RequestStatus::Provisional => EvalView::Done(Source::Provisional),
529            RequestStatus::Green => EvalView::Done(if crate::query::reused(request) {
530                Source::Reused
531            } else {
532                Source::Executed
533            }),
534            RequestStatus::Red => EvalView::Failed { verdict: true },
535            RequestStatus::Error => EvalView::Failed { verdict: false },
536            RequestStatus::Running => EvalView::InProgress(Activity::Running),
537            RequestStatus::WaitingHuman | RequestStatus::Deferred => EvalView::InProgress(
538                Activity::Human(view.claim.as_ref().map(|claim| claim.reviewer.clone())),
539            ),
540            RequestStatus::BudgetExhausted => EvalView::NotRun(NotRun::Budget),
541            RequestStatus::Stale => EvalView::NotRun(NotRun::Stale),
542            RequestStatus::Queued
543            | RequestStatus::WaitDependency
544            | RequestStatus::Unreviewed
545            | RequestStatus::Blocked => {
546                let unmet = self.unmet(eval);
547                let blocking: Vec<_> = unmet
548                    .iter()
549                    .filter(|artifact| self.gate_blocks(eval, artifact))
550                    .cloned()
551                    .collect();
552                if !blocking.is_empty() {
553                    return EvalView::BlockedBy(blocking);
554                }
555                if self.run.modes.speculate {
556                    return self.ready(view);
557                }
558                if !unmet.is_empty() {
559                    let waits = Waits {
560                        root: self.root(&unmet),
561                        artifacts: unmet,
562                    };
563                    return if self.running {
564                        EvalView::WaitingOn(waits)
565                    } else {
566                        EvalView::NotRun(NotRun::Dependency(waits))
567                    };
568                }
569                self.ready(view)
570            }
571        }
572    }
573
574    /// Nothing upstream holds the eval.
575    fn ready(&self, view: &RequestView) -> EvalView {
576        let request = &view.request;
577        if !self.running || request.status() != RequestStatus::Queued {
578            return EvalView::NotRun(NotRun::Unreviewed);
579        }
580        EvalView::InProgress(Activity::Queued(
581            if let Some(crate::store::QueueReason::Slot { backend }) = request.queue {
582                Queue::Slot(backend)
583            } else if request.execution_id.is_some() {
584                Queue::Joined
585            } else if self.run.jobs.is_some() {
586                Queue::Jobs
587            } else {
588                Queue::Ready
589            },
590        ))
591    }
592
593    /// Only this Eval's gates contribute waits; its own Artifact and cycle peers never do.
594    fn unmet(&self, eval: &EvalId) -> Vec<ArtifactName> {
595        let mut artifacts: Vec<_> = self
596            .index
597            .gates
598            .get(eval)
599            .into_iter()
600            .flatten()
601            .filter(|&&gate| !self.effective(gate).passes_gate())
602            .map(|gate| self.index.targets[gate])
603            .collect::<BTreeSet<_>>()
604            .into_iter()
605            .collect();
606        artifacts
607            .sort_by_key(|artifact| (self.completion(artifact).rank(), self.position(artifact)));
608        artifacts.into_iter().cloned().collect()
609    }
610
611    fn views_of(&self, artifact: &ArtifactName) -> Vec<EvalView> {
612        self.evals(artifact)
613            .iter()
614            .map(|eval| self.view(eval))
615            .collect()
616    }
617
618    /// The Artifact's completion from its eval rows; done rows whose own gates are unmet
619    /// do not complete it.
620    pub fn completion(&self, artifact: &ArtifactName) -> Completion {
621        if let Some(completion) = self.completions.borrow().get(artifact) {
622            return *completion;
623        }
624        let views = self.views_of(artifact);
625        let busy = views
626            .iter()
627            .filter_map(|view| match view {
628                EvalView::InProgress(Activity::Running) => Some(Busy::Running),
629                EvalView::InProgress(Activity::Human(_)) => Some(Busy::Human),
630                EvalView::InProgress(Activity::Queued(_)) => Some(Busy::Queued),
631                _ => None,
632            })
633            .min();
634        let completion = if views
635            .iter()
636            .any(|view| matches!(view, EvalView::Failed { .. }))
637        {
638            // ERROR before RED, like the Artifact's own row; blocking follows `blocks`.
639            Completion::Failed {
640                verdict: !views.contains(&EvalView::Failed { verdict: false }),
641            }
642        } else if let Some(busy) = busy {
643            Completion::InProgress(busy)
644        } else if views.iter().any(|view| !matches!(view, EvalView::Done(_))) || !self.met(artifact)
645        {
646            Completion::Waiting {
647                blocked: self.blocks(artifact)
648                    || views
649                        .iter()
650                        .any(|view| matches!(view, EvalView::BlockedBy(_))),
651            }
652        } else {
653            Completion::Complete
654        };
655        if let Some(id) = self.key(artifact) {
656            self.completions.borrow_mut().insert(id, completion);
657        }
658        completion
659    }
660
661    /// Group only this Eval's actual gates, never unrelated Evals on the same material.
662    fn gate_blocks(&self, eval: &EvalId, artifact: &ArtifactName) -> bool {
663        self.index
664            .gates
665            .get(eval)
666            .into_iter()
667            .flatten()
668            .filter(|gate| self.index.targets[*gate] == artifact)
669            .any(|&gate| self.status_blocks(eval, gate, self.effective(gate)))
670    }
671
672    /// A RED or blocked gate blocks; an ERROR blocks under `--speculate`, and always when the
673    /// gate is a machine eval of a Human eval's own Artifact.
674    fn status_blocks(&self, eval: &EvalId, gate: &EvalId, status: EvalStatus) -> bool {
675        match status {
676            EvalStatus::Red | EvalStatus::Blocked => true,
677            EvalStatus::Error => {
678                self.run.modes.speculate
679                    || self.index.targets.get(gate) == self.index.targets.get(eval)
680            }
681            _ => false,
682        }
683    }
684
685    /// Graph lights treat an external ERROR as held too, without treating cycle peers
686    /// or unrelated sibling Evals as gates.
687    pub fn held_for_run(&self, eval: &EvalId) -> bool {
688        self.held.get(eval).copied().unwrap_or(false)
689    }
690
691    /// The graph's Blocked readiness downstream: an effective RED or blocked eval.
692    fn blocks(&self, artifact: &ArtifactName) -> bool {
693        self.evals(artifact).iter().any(|eval| {
694            matches!(self.effective(eval), EvalStatus::Red | EvalStatus::Blocked)
695                || (self.run.modes.speculate && self.effective(eval) == EvalStatus::Error)
696        })
697    }
698
699    /// Done evals whose own gates are unmet: the Artifacts holding them back.
700    fn held(&self, artifact: &ArtifactName, blocked: bool) -> Vec<crate::types::ArtifactName> {
701        let mut x: Vec<crate::types::ArtifactName> = Vec::new();
702        for &eval in self.evals(artifact) {
703            let wanted = if blocked {
704                EvalStatus::Blocked
705            } else {
706                EvalStatus::Wait
707            };
708            if matches!(self.view(eval), EvalView::Done(_)) && self.effective(eval) == wanted {
709                for id in self.unmet(eval) {
710                    if (!blocked || self.gate_blocks(eval, &id)) && !x.contains(&id) {
711                        x.push(id);
712                    }
713                }
714            }
715        }
716        x
717    }
718
719    /// The Artifacts this Artifact's evals wait for, as one X list.
720    pub fn waits_of(&self, artifact: &ArtifactName) -> Vec<crate::types::ArtifactName> {
721        if let Some(x) = self.waits.borrow().get(artifact) {
722            return x.clone();
723        }
724        let mut x = self.held(artifact, false);
725        for view in self.views_of(artifact) {
726            if let EvalView::WaitingOn(waits) | EvalView::NotRun(NotRun::Dependency(waits)) = view {
727                for id in waits.artifacts {
728                    if !x.contains(&id) {
729                        x.push(id);
730                    }
731                }
732            }
733        }
734        x.sort_by_key(|id| (self.completion(id).rank(), self.position(id)));
735        if let Some(id) = self.key(artifact) {
736            self.waits.borrow_mut().insert(id, x.clone());
737        }
738        x
739    }
740
741    /// The Artifacts this Artifact's blocked evals are blocked by.
742    pub fn blockers_of(&self, artifact: &ArtifactName) -> Vec<crate::types::ArtifactName> {
743        let mut x = self.held(artifact, true);
744        for view in self.views_of(artifact) {
745            if let EvalView::BlockedBy(blockers) = view {
746                for id in blockers {
747                    if !x.contains(&id) {
748                        x.push(id);
749                    }
750                }
751            }
752        }
753        x
754    }
755
756    /// When every X is itself waiting, exactly one level of cause: what the first X waits for.
757    fn root(
758        &self,
759        x: &[crate::types::ArtifactName],
760    ) -> Option<(crate::types::ArtifactName, crate::types::ArtifactName)> {
761        if x.iter()
762            .any(|id| !matches!(self.completion(id), Completion::Waiting { blocked: false }))
763        {
764            return None;
765        }
766        let via = x.first()?.clone();
767        let cause = self.waits_of(&via).into_iter().next()?;
768        Some((via, cause))
769    }
770
771    /// Every dependency Artifact of the eval, unmet ones first in X order.
772    pub fn upstream_states(&self, eval: &EvalId) -> Vec<Upstream> {
773        let mut upstream: Vec<_> = self
774            .upstream(eval)
775            .iter()
776            .map(|artifact| Upstream {
777                artifact: (*artifact).clone(),
778                completion: self.completion(artifact),
779            })
780            .collect();
781        upstream.sort_by_key(|upstream| {
782            (
783                upstream.completion.rank(),
784                self.position(&upstream.artifact),
785            )
786        });
787        upstream
788    }
789}