1use crate::types::{ArtifactName, EvalId};
3use serde_json::Value;
4use time::OffsetDateTime;
5
6use super::{
7 Saved,
8 states::{
9 Activity, Busy, Completion, EvalView, NotRun, Queue, Rank, Source, States, Upstream, Waits,
10 },
11};
12use crate::store::{RequestView, RunView};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Tone {
17 Green,
18 Red,
19 Error,
20 Running,
21 Human,
22 Queued,
23 Blocked,
24 Muted,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Weight {
30 Bold,
31 Normal,
32 Dim,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Segment {
38 pub text: String,
39 pub tone: Option<Tone>,
40}
41
42impl Segment {
43 fn plain(text: impl Into<String>) -> Self {
44 Self {
45 text: text.into(),
46 tone: None,
47 }
48 }
49 fn toned(text: impl Into<String>, tone: Tone) -> Self {
50 Self {
51 text: text.into(),
52 tone: Some(tone),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum Kind {
59 Artifact {
61 completion: Completion,
62 basis: bool,
63 },
64 Eval(EvalView),
65}
66
67#[derive(Debug, Clone)]
68pub struct Node {
69 pub id: super::NodeId,
71 pub kind: Kind,
72 pub glyph: &'static str,
73 pub tone: Tone,
74 pub weight: Weight,
75 pub name: String,
76 pub marks: String,
78 pub text: Vec<Segment>,
79 pub right: String,
81 pub compact: String,
83 pub changed: bool,
85 pub upstream: Vec<Upstream>,
87 pub(crate) held: bool,
89 pub parts: Vec<ArtifactName>,
91 pub clock: Option<Clock>,
93 pub children: Vec<Node>,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct Clock {
99 pub start: OffsetDateTime,
100 pub end: Option<OffsetDateTime>,
101}
102
103impl Clock {
104 pub fn elapsed(self, now: OffsetDateTime) -> String {
105 super::duration(self.end.unwrap_or(now) - self.start)
106 }
107}
108
109impl Node {
110 pub fn right_at(&self, now: OffsetDateTime, compact: bool) -> String {
112 match self.clock {
113 Some(clock) => clock.elapsed(now),
114 None if compact => self.compact.clone(),
115 None => self.right.clone(),
116 }
117 }
118
119 pub fn status(&self) -> String {
121 self.text
122 .iter()
123 .map(|segment| segment.text.as_str())
124 .collect()
125 }
126
127 pub fn line(&self) -> String {
129 let mut line = format!("{} {}{}", self.glyph, self.name, self.marks);
130 for part in [self.status(), self.right.clone()] {
131 if !part.is_empty() {
132 line = format!("{line} {part}");
133 }
134 }
135 if self.changed {
136 line.push_str(" *");
137 }
138 line
139 }
140
141 pub fn done(&self) -> bool {
143 matches!(
144 self.kind,
145 Kind::Artifact {
146 completion: Completion::Complete,
147 ..
148 }
149 ) && !self.children.is_empty()
150 }
151
152 pub fn urgent(&self) -> bool {
154 matches!(
155 self.kind,
156 Kind::Eval(EvalView::Failed { .. } | EvalView::InProgress(Activity::Human(_)))
157 )
158 }
159
160 pub fn attention(&self) -> bool {
162 matches!(
163 self.kind,
164 Kind::Eval(EvalView::InProgress(_) | EvalView::Failed { .. })
165 )
166 }
167}
168
169pub fn completion(completion: Completion) -> (&'static str, Tone, &'static str) {
171 match completion {
172 Completion::Complete => ("✓", Tone::Green, "complete"),
173 Completion::InProgress(Busy::Running) => ("◐", Tone::Running, "in progress"),
174 Completion::InProgress(Busy::Human) => ("?", Tone::Human, "in progress"),
175 Completion::InProgress(Busy::Queued) => ("·", Tone::Queued, "in progress"),
176 Completion::Waiting { blocked: false } => ("…", Tone::Muted, "waiting"),
177 Completion::Waiting { blocked: true } => ("⊘", Tone::Blocked, "blocked"),
178 Completion::Failed { verdict: true } => ("✗", Tone::Red, "failed"),
179 Completion::Failed { verdict: false } => ("!", Tone::Error, "failed"),
180 }
181}
182
183fn style(view: &EvalView) -> (&'static str, Tone, Weight) {
185 match view {
186 EvalView::Done(Source::Provisional) => ("○", Tone::Muted, Weight::Normal),
187 EvalView::Done(_) => ("✓", Tone::Green, Weight::Normal),
188 EvalView::Failed { verdict: true } => ("✗", Tone::Red, Weight::Normal),
189 EvalView::Failed { verdict: false } => ("!", Tone::Error, Weight::Normal),
190 EvalView::InProgress(Activity::Running) => ("◐", Tone::Running, Weight::Bold),
191 EvalView::InProgress(Activity::Human(_)) => ("?", Tone::Human, Weight::Bold),
192 EvalView::InProgress(Activity::Queued(_)) => ("·", Tone::Queued, Weight::Normal),
193 EvalView::WaitingOn(_) => ("…", Tone::Muted, Weight::Dim),
194 EvalView::BlockedBy(_) => ("⊘", Tone::Blocked, Weight::Normal),
195 EvalView::NotRun(NotRun::Budget) => ("$", Tone::Blocked, Weight::Normal),
196 EvalView::NotRun(NotRun::Absent) => ("-", Tone::Muted, Weight::Dim),
197 EvalView::NotRun(_) => ("○", Tone::Muted, Weight::Dim),
198 }
199}
200
201const SHOWN: usize = 2;
203const SHOWN_TEXT: usize = 24;
206
207fn tokens(states: &States, x: &[crate::types::ArtifactName], glyphs: bool) -> Vec<Segment> {
209 let mut segments = Vec::new();
210 for (index, id) in x.iter().take(SHOWN).enumerate() {
211 if index > 0 {
212 segments.push(Segment::plain(", "));
213 }
214 let (glyph, tone, word) = completion(states.completion(id));
215 let text = match (glyphs, x.len()) {
216 (false, _) => id.to_string(),
217 (true, 1) => format!("{id} {glyph} {word}"),
218 (true, _) => format!("{id} {glyph}"),
219 };
220 segments.push(Segment::toned(text, tone));
221 }
222 if x.len() > SHOWN {
223 segments.push(Segment::plain(format!(" +{}", x.len() - SHOWN)));
224 }
225 if glyphs
226 && let Some(failed) = x
227 .iter()
228 .find(|id| states.completion(id) == Completion::Failed { verdict: false })
229 {
230 segments.push(Segment::plain(format!(" (retry {failed} first)")));
231 }
232 segments
233}
234
235fn waits(states: &States, prefix: &str, waits: &Waits) -> Vec<Segment> {
236 let mut segments = vec![Segment::plain(prefix)];
237 segments.extend(tokens(states, &waits.artifacts, true));
238 if let Some((via, root)) = &waits.root {
239 segments.push(Segment::plain(format!(" ({via} waits for ")));
240 let (_, tone, _) = completion(states.completion(root));
241 segments.push(Segment::toned(root.clone(), tone));
242 segments.push(Segment::plain(")"));
243 }
244 segments
245}
246
247fn findings(result: Option<&crate::store::ExecutionResult>) -> String {
249 let result = result.map(crate::store::ExecutionResult::to_json);
250 let fields = result
251 .as_ref()
252 .and_then(Value::as_object)
253 .into_iter()
254 .flatten();
255 let fields = fields.filter(|(key, _)| {
256 !matches!(
257 key.as_str(),
258 "verdict" | "stdout" | "stderr" | "truncated" | "durationMs"
259 )
260 });
261 let parts: Vec<_> = fields
262 .take(SHOWN)
263 .map(|(key, value)| match value {
264 Value::Array(items) => format!("{key} {}", items.len()),
265 Value::Object(fields) => format!("{key} {{{}}}", fields.len()),
266 Value::String(text) => {
267 let text = text.lines().next().unwrap_or_default();
268 let mut short: String = text.chars().take(SHOWN_TEXT).collect();
269 if short.len() < text.len() {
270 short.push('…');
271 }
272 format!("{key} {short}")
273 }
274 value => format!("{key} {value}"),
275 })
276 .collect();
277 parts.join(", ")
278}
279
280fn short_profile(profile: &crate::config::StoredProfile) -> String {
281 use crate::config::StoredProfile;
282 match profile {
283 StoredProfile::Agent { backend, model, .. } => format!(
284 "agent {} {model}",
285 serde_json::to_value(backend)
286 .ok()
287 .and_then(|value| value.as_str().map(str::to_owned))
288 .unwrap_or_default()
289 ),
290 StoredProfile::Runtime { command, .. } => format!("runtime {command}"),
291 StoredProfile::Human {} => "human".into(),
292 }
293}
294
295fn text(states: &States, view: &EvalView, request: Option<&RequestView>) -> Vec<Segment> {
296 let plain = |text: &str| vec![Segment::plain(text)];
297 match view {
298 EvalView::Done(Source::Provisional) => plain("PROVISIONAL · sign-offs remain"),
299 EvalView::Done(Source::Executed) => plain("GREEN"),
300 EvalView::Done(Source::Reused) => plain("GREEN · reused"),
301 EvalView::Done(Source::Saved) => plain("GREEN · saved result, not in this Run"),
302 EvalView::Failed { verdict: true } => {
303 let findings = findings(request.and_then(|view| view.request.result()));
304 if request.is_none() {
305 plain("RED · saved result, not in this Run")
306 } else if findings.is_empty() {
307 plain("RED")
308 } else {
309 vec![Segment::plain(format!("RED · {findings}"))]
310 }
311 }
312 EvalView::Failed { verdict: false } => match request.and_then(super::error) {
313 Some(error) => vec![Segment::plain(format!(
314 "ERROR · {}",
315 error.lines().next().unwrap_or_default()
316 ))],
317 None if request.is_none() => plain("ERROR · saved result, not in this Run"),
318 None => plain("ERROR"),
319 },
320 EvalView::InProgress(Activity::Running) => vec![Segment::plain(format!(
321 "running · {}",
322 request.map_or_else(String::new, |view| short_profile(&view.request.profile))
323 ))],
324 EvalView::InProgress(Activity::Human(None)) => plain("Human sign-off · unclaimed"),
325 EvalView::InProgress(Activity::Human(Some(reviewer))) => vec![Segment::plain(format!(
326 "Human sign-off · claimed by {reviewer}"
327 ))],
328 EvalView::InProgress(Activity::Queued(Queue::Ready)) => plain("queued"),
329 EvalView::InProgress(Activity::Queued(Queue::Jobs)) => plain("queued · --jobs slots full"),
330 EvalView::InProgress(Activity::Queued(Queue::Slot(backend))) => {
331 vec![Segment::plain(format!("queued · {backend} slots full"))]
332 }
333 EvalView::InProgress(Activity::Queued(Queue::Joined)) => {
334 plain("queued · joins the running review")
335 }
336 EvalView::WaitingOn(x) => waits(states, "waits for ", x),
337 EvalView::BlockedBy(x) => {
338 let mut segments = vec![Segment::plain(if states.running {
339 "blocked by "
340 } else {
341 "not run: blocked by "
342 })];
343 segments.extend(tokens(states, x, true));
344 segments
345 }
346 EvalView::NotRun(NotRun::Dependency(x)) => waits(states, "not run: waited for ", x),
347 EvalView::NotRun(NotRun::Budget) => plain("not started · maxExecutions reached"),
348 EvalView::NotRun(NotRun::Stale) => plain("not run · no reusable result"),
349 EvalView::NotRun(NotRun::Unreviewed) => plain("not reviewed"),
350 EvalView::NotRun(NotRun::Absent) => plain("not in this Run"),
351 }
352}
353
354fn clock(view: &EvalView, request: Option<&RequestView>) -> Option<Clock> {
356 let timed = matches!(
357 view,
358 EvalView::Done(Source::Executed)
359 | EvalView::Failed { .. }
360 | EvalView::InProgress(Activity::Running | Activity::Human(_))
361 );
362 let request = &request.filter(|_| timed)?.request;
363 let start = request.started_at.unwrap_or(request.created_at).time();
364 let end = request.completed_at().map(crate::types::Timestamp::time);
365 Some(Clock { start, end })
366}
367
368fn compact(view: &EvalView, right: &str) -> String {
369 let x = match view {
370 EvalView::WaitingOn(waits) | EvalView::NotRun(NotRun::Dependency(waits)) => {
371 &waits.artifacts
372 }
373 EvalView::BlockedBy(x) => x,
374 _ => return right.to_owned(),
375 };
376 match x.as_slice() {
377 [one] => format!("←{one}"),
378 many => format!("←{}", many.len()),
379 }
380}
381
382pub(crate) fn local(eval: &EvalId) -> &str {
383 eval.rsplit_once('/')
384 .map_or(eval.as_str(), |(_, local)| local)
385}
386
387fn eval_node<'a>(states: &States<'a>, eval: &'a EvalId, now: OffsetDateTime) -> Node {
388 let view = states.view(eval);
389 let request = states.request(eval);
390 let (glyph, tone, weight) = style(&view);
391 let clock = clock(&view, request);
392 let right = clock.map_or_else(String::new, |clock| clock.elapsed(now));
393 Node {
394 clock,
395 id: super::NodeId::eval(eval.clone()),
396 glyph,
397 tone,
398 weight,
399 name: local(eval).to_owned(),
400 marks: String::new(),
401 text: text(states, &view, request),
402 compact: compact(&view, &right),
403 right,
404 changed: false,
405 upstream: states.upstream_states(eval),
406 held: states.held_for_run(eval),
407 parts: Vec::new(),
408 children: Vec::new(),
409 kind: Kind::Eval(view),
410 }
411}
412
413fn names(nodes: &[&Node]) -> String {
415 let mut names = nodes
416 .iter()
417 .take(SHOWN)
418 .map(|node| node.name.as_str())
419 .collect::<Vec<_>>()
420 .join(", ");
421 if nodes.len() > SHOWN {
422 names = format!("{names} +{}", nodes.len() - SHOWN);
423 }
424 names
425}
426
427fn summary(states: &States, id: &ArtifactName, children: &[Node]) -> Vec<Segment> {
429 let view = |node: &Node| match &node.kind {
430 Kind::Eval(view) => Some(view.clone()),
431 Kind::Artifact { .. } => None,
432 };
433 let Some(top) = children
434 .iter()
435 .filter_map(view)
436 .map(|view| view.rank())
437 .min()
438 else {
439 return Vec::new();
440 };
441 let class = |rank: Rank| -> Vec<&Node> {
442 children
443 .iter()
444 .filter(|node| view(node).is_some_and(|view| view.rank() == rank))
445 .collect()
446 };
447 let x = |ids: Vec<crate::types::ArtifactName>| tokens(states, &ids, false);
448 let mut segments = match top {
449 Rank::Failed => {
450 let failed = class(Rank::Failed);
451 let errors: Vec<_> = failed
452 .iter()
453 .copied()
454 .filter(|node| node.kind == Kind::Eval(EvalView::Failed { verdict: false }))
455 .collect();
456 let (label, nodes) = if errors.is_empty() {
457 ("RED", failed)
458 } else {
459 ("ERROR", errors)
460 };
461 vec![Segment::plain(format!("{label}: {}", names(&nodes)))]
462 }
463 Rank::InProgress => vec![Segment::plain(format!(
464 "in progress: {}",
465 names(&class(Rank::InProgress))
466 ))],
467 Rank::Blocked => {
468 let mut segments = vec![Segment::plain(if states.running {
469 "blocked by "
470 } else {
471 "not run: blocked by "
472 })];
473 segments.extend(x(states.blockers_of(id)));
474 segments
475 }
476 Rank::Waiting => {
477 let mut segments = vec![Segment::plain("waits for ")];
478 segments.extend(x(states.waits_of(id)));
479 segments
480 }
481 Rank::NotRun => {
482 let first = class(Rank::NotRun)[0];
483 match &first.kind {
484 Kind::Eval(EvalView::NotRun(NotRun::Dependency(_))) => {
485 let mut segments = vec![Segment::plain("not run: waited for ")];
486 segments.extend(x(states.waits_of(id)));
487 segments
488 }
489 _ => first.text.clone(),
490 }
491 }
492 Rank::Done => match states.completion(id) {
494 Completion::Waiting { blocked: true } => {
495 let mut segments = vec![Segment::plain("done, but blocked by ")];
496 segments.extend(x(states.blockers_of(id)));
497 segments
498 }
499 Completion::Waiting { blocked: false } => {
500 let mut segments = vec![Segment::plain("done, but waits for ")];
501 segments.extend(x(states.waits_of(id)));
502 segments
503 }
504 _ => Vec::new(),
505 },
506 };
507 if top == Rank::Failed {
508 let busy = class(Rank::InProgress);
509 if !busy.is_empty() {
510 segments.push(Segment::plain(format!(" · in progress: {}", names(&busy))));
511 }
512 }
513 segments
514}
515
516fn artifact_node<'a>(
517 states: &States<'a>,
518 saved: &Saved<'a>,
519 id: &ArtifactName,
520 now: OffsetDateTime,
521) -> Node {
522 let artifact = saved.artifact(id);
523 let basis = artifact.and_then(|artifact| artifact.basis.value()) == Some(&true);
524 let children: Vec<_> = states
525 .evals(id)
526 .iter()
527 .map(|&eval| eval_node(states, eval, now))
528 .collect();
529 let mut marks = String::new();
530 if states.index.cyclic.contains(id) {
531 let peers = states.index.peers.get(id).map_or(&[][..], Vec::as_slice);
532 marks = match peers {
533 [] => " ↻".into(),
534 [peer] => format!(" ↻ {peer}"),
535 [peer, rest @ ..] => format!(" ↻ {peer} +{}", rest.len()),
536 };
537 }
538 if artifact.and_then(|artifact| artifact.kind.value())
539 == Some(&crate::config::ArtifactKind::File)
540 {
541 marks.push_str(" [file]");
542 }
543 let completion = states.completion(id);
544 let passed = states
546 .evals(id)
547 .iter()
548 .filter(|eval| states.effective(eval) == crate::graph::EvalStatus::Green)
549 .count();
550 let right = if children.is_empty() {
551 String::new()
552 } else {
553 format!("{passed}/{}", children.len())
554 };
555 let parts = states.index.parts.get(id).map_or(&[][..], Vec::as_slice);
556 let mut pending: Vec<_> = parts
558 .iter()
559 .filter(|part| states.completion(part) != Completion::Complete)
560 .collect();
561 pending.sort_by_key(|part| states.completion(part).rank());
562 let listed = |label: &str| {
563 let shown: Vec<_> = pending.iter().take(SHOWN).map(|id| id.as_str()).collect();
564 let more = pending.len().saturating_sub(SHOWN);
565 let more = if more > 0 {
566 format!(" +{more}")
567 } else {
568 String::new()
569 };
570 Segment::plain(format!("{label}{}{more}", shown.join(", ")))
571 };
572 let (glyph, tone, weight, mut text) = if basis && pending.is_empty() {
573 ("◇", Tone::Muted, Weight::Dim, vec![Segment::plain("basis")])
574 } else if children.is_empty() && !parts.is_empty() {
575 let (glyph, tone, _) = self::completion(completion);
577 let text = if pending.is_empty() {
578 Segment::plain(format!("made of {}", parts.len()))
579 } else {
580 listed("pending: ")
581 };
582 (glyph, tone, Weight::Normal, vec![text])
583 } else if children.is_empty() {
584 (
585 "○",
586 Tone::Muted,
587 Weight::Dim,
588 vec![Segment::plain("no evals")],
589 )
590 } else {
591 let top = children
593 .iter()
594 .min_by_key(|node| match &node.kind {
595 Kind::Eval(EvalView::Failed { verdict: false }) => (Rank::Failed, false),
596 Kind::Eval(view) => (view.rank(), true),
597 Kind::Artifact { .. } => (Rank::Done, true),
598 })
599 .expect("children");
600 let (glyph, tone, weight) = match (&top.kind, completion) {
601 (_, Completion::InProgress(busy)) => {
602 let (glyph, tone, _) = self::completion(Completion::InProgress(busy));
603 (glyph, tone, Weight::Bold)
604 }
605 (Kind::Eval(EvalView::Done(_)), Completion::Failed { verdict }) => {
607 let (glyph, tone, _) = self::completion(Completion::Failed { verdict });
608 (glyph, tone, Weight::Bold)
609 }
610 (Kind::Eval(EvalView::Done(_)), Completion::Waiting { blocked }) => {
612 let (glyph, tone, _) = self::completion(Completion::Waiting { blocked });
613 let weight = if blocked { Weight::Normal } else { Weight::Dim };
614 (glyph, tone, weight)
615 }
616 _ => (top.glyph, top.tone, top.weight),
617 };
618 (glyph, tone, weight, summary(states, id, &children))
619 };
620 if !children.is_empty() && !pending.is_empty() {
621 text.push(listed(if text.is_empty() {
622 "parts: "
623 } else {
624 " · parts: "
625 }));
626 }
627 Node {
628 id: super::NodeId::artifact(id.clone()),
629 clock: None,
630 kind: Kind::Artifact { completion, basis },
631 glyph,
632 tone,
633 weight,
634 name: id.to_string(),
635 marks,
636 text,
637 compact: right.clone(),
638 right,
639 changed: false,
640 upstream: Vec::new(),
641 held: false,
642 parts: parts.iter().map(|&part| part.clone()).collect(),
643 children,
644 }
645}
646
647pub fn tree(run: &RunView, requests: &[RequestView], now: OffsetDateTime) -> Vec<Node> {
650 let mut nodes = build(run, requests, now);
651 if let Some(ended) = at_end(run, requests) {
654 mark(&mut nodes, &build(run, &ended, now));
655 }
656 nodes
657}
658
659fn build(run: &RunView, requests: &[RequestView], now: OffsetDateTime) -> Vec<Node> {
660 let saved = Saved { run, requests };
661 let states = States::new(&saved);
662 states
663 .index
664 .order
665 .iter()
666 .map(|id| artifact_node(&states, &saved, id, now))
667 .collect()
668}
669
670fn at_end(run: &RunView, requests: &[RequestView]) -> Option<Vec<RequestView>> {
674 let end = run.run.completed_at()?;
675 let late = |time: Option<crate::types::Timestamp>| time.is_some_and(|time| time > end);
676 let changed = requests.iter().any(|view| {
677 late(view.request.completed_at()) || late(view.claim.as_ref().map(|claim| claim.claimed_at))
678 });
679 if !changed {
680 return None;
681 }
682 let mut ended = requests.to_vec();
684 for view in &mut ended {
685 if late(view.claim.as_ref().map(|claim| claim.claimed_at)) {
686 view.claim = None;
687 }
688 if late(view.request.completed_at()) {
689 let request = std::sync::Arc::make_mut(&mut view.request);
690 request.state = if request.profile.kind() == crate::config::ProfileKind::Human {
691 crate::store::RequestState::Deferred
692 } else {
693 crate::store::RequestState::Running
694 };
695 }
696 }
697 Some(ended)
698}
699
700fn state(node: &Node) -> (Kind, &'static str, String) {
702 let kind = match &node.kind {
703 Kind::Eval(EvalView::WaitingOn(waits)) => Kind::Eval(EvalView::WaitingOn(Waits {
704 artifacts: waits.artifacts.clone(),
705 root: None,
706 })),
707 Kind::Eval(EvalView::NotRun(NotRun::Dependency(waits))) => {
708 Kind::Eval(EvalView::NotRun(NotRun::Dependency(Waits {
709 artifacts: waits.artifacts.clone(),
710 root: None,
711 })))
712 }
713 kind => kind.clone(),
714 };
715 let summary = match node.kind {
717 Kind::Artifact { .. } => format!("{} {}", node.status(), node.right),
718 Kind::Eval(_) => String::new(),
719 };
720 (kind, node.glyph, summary)
721}
722
723fn mark(nodes: &mut [Node], ended: &[Node]) {
725 let mut before = std::collections::HashMap::new();
726 for node in ended {
727 before.insert(&node.id, state(node));
728 for child in &node.children {
729 before.insert(&child.id, state(child));
730 }
731 }
732 for node in nodes {
733 node.changed = before.get(&node.id) != Some(&state(node));
734 for child in &mut node.children {
735 child.changed = before.get(&child.id) != Some(&state(child));
736 }
737 }
738}
739
740pub fn upstream_index(run: &RunView, requests: &[RequestView]) -> Vec<(EvalId, Vec<ArtifactName>)> {
742 let saved = Saved { run, requests };
743 let index = super::states::Index::new(&saved);
744 index
745 .upstream
746 .iter()
747 .map(|(eval, artifacts)| {
748 (
749 (*eval).clone(),
750 artifacts.iter().map(|id| (*id).clone()).collect(),
751 )
752 })
753 .collect()
754}
755
756pub(super) fn artifact_status(saved: &Saved, id: &ArtifactName, now: OffsetDateTime) -> String {
758 let states = States::new(saved);
759 let node = artifact_node(&states, saved, id, now);
760 let (_, _, word) = completion(states.completion(id));
761 let status = node.status();
762 let status = if status.is_empty() {
763 word.to_owned()
764 } else {
765 status
766 };
767 if node.right.is_empty() {
768 format!("{} {status}", node.glyph)
769 } else {
770 format!("{} {status} · {} Evals GREEN", node.glyph, node.right)
771 }
772}
773
774pub(super) fn waits_for(saved: &Saved, eval: &EvalId, now: OffsetDateTime) -> String {
777 let states = States::new(saved);
778 format_waits(
779 saved,
780 &states,
781 eval,
782 now,
783 &mut std::collections::HashSet::new(),
784 )
785}
786
787pub(super) fn artifact_waits(
790 saved: &Saved,
791 artifact: &ArtifactName,
792 now: OffsetDateTime,
793) -> String {
794 let states = States::new(saved);
795 let mut seen = std::collections::HashSet::new();
796 let blocks = states
797 .evals(artifact)
798 .iter()
799 .map(|eval| format_waits(saved, &states, eval, now, &mut seen))
800 .filter(|block| !block.is_empty());
801 super::join(blocks, "\n")
802}
803
804fn format_waits(
805 saved: &Saved,
806 states: &States<'_>,
807 eval: &EvalId,
808 now: OffsetDateTime,
809 seen: &mut std::collections::HashSet<ArtifactName>,
810) -> String {
811 let Some((&target, _)) = states
812 .index
813 .evals
814 .iter()
815 .find(|(_, evals)| evals.contains(&eval))
816 else {
817 return String::new();
818 };
819 let peers = |id: &ArtifactName| states.index.peers.get(id).cloned().unwrap_or_default();
820 let mut own = peers(target);
821 own.push(target);
822 let mut lines = Vec::new();
823 for upstream in states.upstream_states(eval) {
824 if !seen.insert(upstream.artifact.clone()) {
825 continue;
826 }
827 let (glyph, _, word) = completion(upstream.completion);
828 let id = &upstream.artifact;
829 let mut origins: Vec<String> = saved
830 .relations(id, false)
831 .into_iter()
832 .filter(|relation| own.contains(&&relation.target))
833 .map(|relation| {
834 let kind = super::relation_kind(relation);
835 if &relation.target == target {
836 kind
837 } else {
838 format!("{kind} → {}", relation.target)
839 }
840 })
841 .collect();
842 if origins.is_empty() && !peers(id).is_empty() {
843 origins.push(format!("↻ peer of {}", super::join(peers(id), ", ")));
844 }
845 let origins = if origins.is_empty() {
846 String::new()
847 } else {
848 format!(" · {}", origins.join(", "))
849 };
850 lines.push(format!("↑ {id} {glyph} {word}{origins}"));
851 if upstream.completion == Completion::Complete {
852 continue;
853 }
854 for &pending in states.evals(id) {
855 let node = eval_node(states, pending, now);
856 if !matches!(node.kind, Kind::Eval(EvalView::Done(_))) {
857 lines.push(format!(" {} {} {}", node.glyph, node.name, node.status()));
858 }
859 }
860 }
861 lines.join("\n")
862}