1mod graph;
3pub use graph::{Graph, GraphEdge, GraphNode, Light, graph};
4mod node_id;
5pub use node_id::NodeId;
6mod states;
7mod status;
8#[cfg(test)]
9mod tests;
10mod tree;
11pub use states::{Activity, Busy, Completion, EvalView, NotRun, Queue, Source, Upstream, Waits};
12pub use status::Status;
13pub use tree::{Clock, Kind, Node, Segment, Tone, Weight, completion, tree, upstream_index};
14
15use serde_json::Value;
16use time::OffsetDateTime;
17
18use crate::{
19 query,
20 store::{RequestView, RunSummary, RunView},
21 types::{ArtifactName, EvalId},
22};
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
26pub enum Target {
27 Run,
29 Artifact(crate::types::ArtifactName),
30 Eval(crate::types::EvalId),
31}
32
33impl Target {
34 pub fn parse(id: &str) -> Option<Self> {
35 let (kind, name) = id.split_once(':')?;
36 match kind {
37 "run" => name.parse::<crate::types::RunId>().ok().map(|_| Self::Run),
38 "a" => name.parse().ok().map(Self::Artifact),
39 "e" => name.parse().ok().map(Self::Eval),
40 _ => None,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
47pub enum Section {
48 Outcome,
49 Waits,
51 What,
52 Provenance,
53 Technical,
54}
55
56impl Section {
57 pub const ALL: [Self; 5] = [
58 Self::Outcome,
59 Self::Waits,
60 Self::What,
61 Self::Provenance,
62 Self::Technical,
63 ];
64
65 pub fn of(key: &str) -> Self {
67 match key {
68 "Status" | "Error" | "Reason" | "Blocked by" | "Result" | "Claim" | "Source"
69 | "At Run end" | "Validation" | "Counts" | "Errors" | "Waiting" | "Running"
70 | "Evals" | "Gates" => Self::Outcome,
71 "Waits for" => Self::Waits,
72 "Timing" | "Usage" | "Budget" | "Work" | "Saved usage" => Self::Provenance,
73 "Execution" | "Fingerprint" | "Key" | "Key covers" | "Path" | "Tags" | "Cycle"
74 | "Run" | "Repository" => Self::Technical,
75 _ => Self::What,
76 }
77 }
78
79 pub fn name(self) -> &'static str {
80 match self {
81 Self::Outcome => "Outcome",
82 Self::Waits => "Waits for",
83 Self::What => "What",
84 Self::Provenance => "Provenance",
85 Self::Technical => "Technical",
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct RunRow {
92 pub id: crate::types::RunId,
93 pub repo: std::path::PathBuf,
94 pub status: crate::types::RunStatus,
95 pub counts: String,
97 pub age: String,
98 pub took: String,
100}
101
102#[derive(Debug, Clone)]
103pub struct Progress {
104 pub status: Status,
105 pub repo: String,
106 pub validation: String,
107 pub timing: String,
108 pub elapsed: String,
110 pub finished: bool,
111 pub satisfied: Option<bool>,
113 pub counts: Vec<(Status, u64)>,
114 pub budget: String,
116 pub work: String,
118 pub usage: String,
120 pub saved: String,
121 pub tokens: Option<u64>,
123 pub running: Vec<(String, String)>,
125 pub waiting: Vec<(String, String)>,
127 pub errors: Vec<(String, String)>,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct Strip {
134 pub status: Status,
135 pub headline: String,
136 pub attention: Vec<(Status, String)>,
138 pub more: usize,
140}
141
142#[derive(Debug, Clone, Default)]
143pub struct Detail {
144 pub title: String,
145 pub summary: String,
147 pub fields: Vec<(&'static str, String)>,
148}
149
150impl Detail {
151 pub fn field(&self, name: &str) -> Option<&str> {
152 self.fields
153 .iter()
154 .find(|(key, _)| *key == name)
155 .map(|(_, value)| value.as_str())
156 }
157
158 fn push(&mut self, key: &'static str, value: impl Into<String>) {
159 let value = value.into();
160 if !value.is_empty() {
161 self.fields.push((key, value));
162 }
163 }
164
165 fn insert_after(&mut self, after: &str, key: &'static str, value: String) {
166 if value.is_empty() {
167 return;
168 }
169 let index = self.fields.iter().position(|(name, _)| *name == after);
170 let index = index.map_or(self.fields.len(), |index| index + 1);
171 self.fields.insert(index, (key, value));
172 }
173}
174
175const ATTENTION_LINES: usize = 3;
177
178const ABSENT: &str = "not in Run";
179
180pub fn glyph(status: Option<Status>) -> &'static str {
182 status.map_or("-", Status::glyph)
183}
184
185fn join<T: AsRef<str>>(items: impl IntoIterator<Item = T>, separator: &str) -> String {
186 let items: Vec<_> = items
187 .into_iter()
188 .map(|item| item.as_ref().to_owned())
189 .collect();
190 items.join(separator)
191}
192
193fn mark(name: &EvalId, status: Option<Status>) -> String {
194 format!(
195 "{} {name} {}",
196 glyph(status),
197 status.map_or(ABSENT, Status::as_str)
198 )
199}
200
201const MINUTE: i64 = 60;
203const HOUR: i64 = 60 * MINUTE;
204const DAY: i64 = 24 * HOUR;
205
206pub fn duration(elapsed: time::Duration) -> String {
207 let seconds = elapsed.whole_seconds().max(0);
208 match seconds {
209 0..MINUTE => format!("{seconds}s"),
210 MINUTE..HOUR => format!("{}m {:02}s", seconds / MINUTE, seconds % MINUTE),
211 HOUR..DAY => format!("{}h {:02}m", seconds / HOUR, seconds % HOUR / MINUTE),
212 _ => format!("{}d {}h", seconds / DAY, seconds % DAY / HOUR),
213 }
214}
215
216#[cfg(test)]
217#[test]
218fn typed_durations_keep_the_display_units_and_clamp_clock_skew() {
219 for (elapsed, expected) in [
220 (time::Duration::seconds(-1), "0s"),
221 (time::Duration::ZERO, "0s"),
222 (time::Duration::milliseconds(59_999), "59s"),
223 (time::Duration::minutes(1), "1m 00s"),
224 (time::Duration::seconds(125), "2m 05s"),
225 (time::Duration::hours(1), "1h 00m"),
226 (time::Duration::minutes(61), "1h 01m"),
227 (time::Duration::days(1), "1d 0h"),
228 (time::Duration::hours(25), "1d 1h"),
229 ] {
230 assert_eq!(duration(elapsed), expected);
231 }
232}
233
234fn span(
236 start: crate::types::Timestamp,
237 end: Option<crate::types::Timestamp>,
238 now: OffsetDateTime,
239) -> Option<String> {
240 let end = end.map_or(now, crate::types::Timestamp::time);
241 Some(duration(end - start.time()))
242}
243
244pub fn counts(counts: impl IntoIterator<Item = (Status, u64)>) -> String {
246 let mut counts: Vec<_> = counts.into_iter().filter(|(_, n)| *n > 0).collect();
247 counts.sort_by_key(|(status, _)| *status);
248 join(
249 counts
250 .into_iter()
251 .map(|(status, n)| format!("{}{n}", status.glyph())),
252 " ",
253 )
254}
255
256pub fn tokens(total: u64) -> String {
258 const THOUSAND: u64 = 1_000;
260 const MILLION: u64 = 1_000_000;
261 match total {
262 0..THOUSAND => total.to_string(),
263 THOUSAND..MILLION => format!("{:.1}k", total as f64 / THOUSAND as f64),
264 _ => format!("{:.1}M", total as f64 / MILLION as f64),
265 }
266}
267
268pub fn run_rows(runs: &[RunSummary], now: OffsetDateTime) -> Vec<RunRow> {
269 runs.iter()
270 .map(|run| RunRow {
271 id: run.id.clone(),
272 repo: run.repo_path.clone(),
273 status: run.status,
274 counts: counts(run.counts.iter().map(|(status, n)| ((*status).into(), *n))),
275 age: span(run.created_at, None, now).unwrap_or_default(),
276 took: run
277 .completed_at
278 .and_then(|end| span(run.created_at, Some(end), now))
279 .unwrap_or_default(),
280 })
281 .collect()
282}
283
284fn reused(view: &RequestView) -> bool {
285 query::reused(&view.request)
286}
287
288fn elapsed(view: &RequestView, now: OffsetDateTime) -> Option<String> {
289 let request = &view.request;
290 if view.request.profile.kind() == crate::config::ProfileKind::Dependency {
291 return Some("derived".into());
292 }
293 if reused(view) {
294 return Some("reused".into());
295 }
296 let active = matches!(
297 request.status(),
298 crate::types::RequestStatus::Running
299 | crate::types::RequestStatus::WaitingHuman
300 | crate::types::RequestStatus::Deferred
301 );
302 let start = request
303 .started_at
304 .or(active.then_some(request.created_at))?;
305 span(start, request.completed_at(), now)
306}
307
308fn claim(view: &RequestView) -> String {
309 view.claim.as_ref().map_or("unclaimed".into(), |claim| {
310 format!("claimed by {} at {}", claim.reviewer, claim.claimed_at)
311 })
312}
313
314fn error(view: &RequestView) -> Option<String> {
315 let request = &view.request;
316 let error = request.error().map(str::to_owned)?;
317 Some(match &request.error_code() {
318 Some(code) => format!("[{code}] {error}"),
319 None => error,
320 })
321}
322
323pub fn progress(view: &RunView, requests: &[RequestView], now: OffsetDateTime) -> Progress {
324 let run = &view.run;
325 let (summary, saved_usage) = query::run_summary(view, now);
326 let with = |status: crate::types::RequestStatus| {
327 requests
328 .iter()
329 .filter(move |view| view.request.status() == status)
330 };
331 let usage = counter_pairs(&summary.usage.usage);
332 let saved = counter_pairs(&saved_usage);
333 let spent = &summary.usage.usage;
334 let tokens = spent.get("totalTokens").copied().or_else(|| {
335 let input = spent.get("inputTokens").copied();
336 let output = spent.get("outputTokens").copied();
337 (input.is_some() || output.is_some())
338 .then(|| input.unwrap_or(0).saturating_add(output.unwrap_or(0)))
339 });
340 let validation = run.validation.snapshot();
341 let unmet = validation
342 .and_then(|v| v.obligations.value())
343 .map(|ids| {
344 ids.iter()
345 .map(ToString::to_string)
346 .collect::<Vec<_>>()
347 .join(", ")
348 })
349 .unwrap_or_default();
350 let wall = span(run.created_at, run.completed_at(), now).unwrap_or_default();
351 Progress {
352 status: run.status().into(),
353 repo: crate::platform::path_text(&run.repo_path),
354 validation: match validation.and_then(|v| v.satisfied.value()) {
356 None => "pending".into(),
357 Some(true) => "SATISFIED at Run end".into(),
358 Some(_) if unmet.is_empty() => "NOT SATISFIED at Run end".into(),
359 Some(_) => format!("NOT SATISFIED at Run end (unmet: {unmet})"),
360 },
361 timing: format!(
362 "started {} · {} {wall}",
363 run.created_at,
364 if run.completed_at().is_some() {
365 "took"
366 } else {
367 "elapsed"
368 },
369 ),
370 elapsed: wall,
371 finished: run.completed_at().is_some(),
372 satisfied: validation.and_then(|v| v.satisfied.value()).copied(),
373 counts: summary
374 .counts
375 .iter()
376 .map(|(status, count)| ((*status).into(), *count))
377 .collect(),
378 budget: format!(
379 "executions {}/{} · jobs {}",
380 run.executions_started,
381 run.max_executions
382 .map_or("unlimited".into(), |max| max.to_string()),
383 run.jobs
384 .map_or("unlimited".into(), |limit| limit.to_string()),
385 ),
386 work: format!(
387 "executed {} · reused {} · derived {}",
388 summary.executed.total, summary.reused.kinds.total, summary.derived,
389 ),
390 usage,
391 saved,
392 tokens,
393 running: with(crate::types::RequestStatus::Running)
394 .map(|view| {
395 (
396 view.request.eval_id.to_string(),
397 elapsed(view, now).unwrap_or_default(),
398 )
399 })
400 .collect(),
401 waiting: requests
402 .iter()
403 .filter(|view| view.request.status().is_human_wait())
404 .map(|view| (view.request.eval_id.to_string(), claim(view)))
405 .collect(),
406 errors: with(crate::types::RequestStatus::Error)
407 .map(|view| {
408 (
409 view.request.eval_id.to_string(),
410 error(view).unwrap_or_default(),
411 )
412 })
413 .chain(
414 run.error()
415 .map(str::to_owned)
416 .map(|error| ("Run".into(), error)),
417 )
418 .chain(
419 view.unreadable
420 .iter()
421 .map(|record| (record.id.clone(), record.to_string())),
422 )
423 .collect(),
424 }
425}
426
427pub fn strip(progress: &Progress) -> Strip {
429 let validation = match progress.satisfied {
430 None => "validation pending".to_owned(),
431 Some(true) => "SATISFIED at Run end".to_owned(),
432 Some(false) => "NOT SATISFIED at Run end".to_owned(),
433 };
434 let counts = counts(progress.counts.iter().copied());
435 let time = format!(
436 "{} {}",
437 if progress.finished { "took" } else { "elapsed" },
438 progress.elapsed
439 );
440 let tokens = progress
441 .tokens
442 .map(|total| format!("tokens {}", tokens(total)));
443 let headline = join(
444 [Some(validation), Some(counts), Some(time), tokens]
445 .into_iter()
446 .flatten()
447 .filter(|part| !part.is_empty()),
448 " · ",
449 );
450 let mut attention: Vec<(Status, String)> = progress
451 .errors
452 .iter()
453 .map(|(eval, error)| (Status::Error, format!("{eval} {error}")))
454 .chain(progress.waiting.iter().map(|(eval, claim)| {
455 (
456 Status::WaitingHuman,
457 format!("{eval} waiting Human · {claim}"),
458 )
459 }))
460 .chain(progress.running.iter().map(|(eval, time)| {
461 (
462 Status::Running,
463 format!("{eval} running {time}").trim_end().to_owned(),
464 )
465 }))
466 .collect();
467 let more = attention.len().saturating_sub(ATTENTION_LINES);
468 attention.truncate(ATTENTION_LINES);
469 Strip {
470 status: progress.status,
471 headline,
472 attention,
473 more,
474 }
475}
476
477pub fn run_node(run: &RunView) -> Node {
479 let status = Status::from(run.run.status());
480 Node {
481 id: NodeId::run(&run.run.id),
482 clock: None,
483 kind: Kind::Artifact {
484 completion: Completion::Complete,
485 basis: false,
486 },
487 glyph: status.glyph(),
488 tone: match status {
489 Status::Green => Tone::Green,
490 Status::Red => Tone::Red,
491 Status::Error => Tone::Error,
492 Status::Running => Tone::Running,
493 Status::Blocked | Status::BudgetExhausted | Status::Incomplete => Tone::Blocked,
494 _ => Tone::Muted,
495 },
496 weight: Weight::Normal,
497 name: "Run".into(),
498 marks: String::new(),
499 text: vec![Segment {
500 text: "Enter: usage, budgets, counts".into(),
501 tone: Some(Tone::Muted),
502 }],
503 right: String::new(),
504 compact: String::new(),
505 changed: false,
506 upstream: Vec::new(),
507 dependency: false,
508 children: Vec::new(),
509 }
510}
511
512fn result_summary(result: &crate::store::ExecutionResult) -> String {
515 let result = result.to_json();
516 let Some(object) = result.as_object() else {
517 return result
518 .to_string()
519 .lines()
520 .next()
521 .unwrap_or_default()
522 .to_owned();
523 };
524 let mut entries: Vec<_> = object
526 .iter()
527 .filter(|(key, _)| !matches!(key.as_str(), "stdout" | "stderr"))
528 .collect();
529 entries.sort_by_key(|(key, _)| *key != "verdict");
530 let parts = entries.into_iter().map(|(key, value)| match value {
531 Value::String(text) => {
532 let line = text.lines().next().unwrap_or_default();
533 if line.len() < text.trim_end().len() {
534 format!("{key} {line} …")
535 } else {
536 format!("{key} {line}")
537 }
538 }
539 Value::Array(items) => format!("{key} [{} items]", items.len()),
540 Value::Object(fields) => format!("{key} {{{} fields}}", fields.len()),
541 other => format!("{key} {other}"),
542 });
543 join(parts, "\n")
544}
545
546struct Saved<'a> {
548 run: &'a RunView,
549 requests: &'a [RequestView],
550}
551
552impl<'a> Saved<'a> {
553 fn definitions(&self) -> Option<&'a crate::store::definitions::Graph> {
554 self.run.run.definitions.graph()
555 }
556 fn artifacts(
557 &self,
558 ) -> impl Iterator<
559 Item = (
560 &'a crate::types::ArtifactName,
561 &'a crate::store::definitions::Artifact,
562 ),
563 > {
564 self.definitions()
565 .into_iter()
566 .flat_map(|graph| graph.artifacts())
567 }
568 fn artifact(&self, id: &ArtifactName) -> Option<&'a crate::store::definitions::Artifact> {
569 self.definitions().and_then(|graph| graph.artifact(id))
570 }
571 fn eval_definitions(&self) -> &'a [crate::store::definitions::Eval] {
572 self.definitions().map_or(&[], |graph| graph.evals())
573 }
574 fn components(&self) -> &'a [crate::store::definitions::Component] {
575 self.definitions().map_or(&[], |graph| graph.components())
576 }
577
578 fn request(&self, eval: &EvalId) -> Option<&'a RequestView> {
579 self.requests
580 .iter()
581 .find(|view| &view.request.eval_id == eval)
582 }
583
584 fn status(&self, eval: &EvalId) -> Option<Status> {
585 self.request(eval).map(|view| view.request.status().into())
586 }
587
588 fn artifact_ids(&self) -> Vec<&'a ArtifactName> {
591 let components = self.components().iter();
592 let saved = components
593 .flat_map(|component| {
594 let mut members: Vec<_> =
595 component.artifacts.value().into_iter().flatten().collect();
596 members.sort_unstable();
597 members
598 })
599 .chain(self.artifacts().map(|(id, _)| id))
600 .chain(self.requests.iter().map(|view| &view.request.target));
601 let mut seen = std::collections::HashSet::new();
602 saved.filter(|id| seen.insert(*id)).collect()
603 }
604
605 fn evals(
607 &self,
608 artifact: &ArtifactName,
609 ) -> Vec<(&'a EvalId, Option<&'a crate::store::definitions::Eval>)> {
610 let mut evals: Vec<_> = self
611 .eval_definitions()
612 .iter()
613 .filter(|eval| &eval.target == artifact)
614 .map(|eval| (&eval.id, Some(eval)))
615 .collect();
616 for request in self.requests.iter().map(|view| &view.request) {
617 if &request.target == artifact && !evals.iter().any(|(id, _)| *id == &request.eval_id) {
618 evals.push((&request.eval_id, None));
619 }
620 }
621 evals
622 }
623
624 fn validation(&self, artifact: &ArtifactName) -> Option<&'a crate::store::ArtifactValidation> {
625 self.run
626 .run
627 .validation
628 .snapshot()?
629 .artifacts
630 .value()?
631 .iter()
632 .find(|a| &a.id == artifact)
633 }
634
635 fn component(&self, id: &ArtifactName) -> Option<&'a crate::store::definitions::Component> {
636 self.components().iter().find(|component| {
637 component
638 .artifacts
639 .value()
640 .is_some_and(|artifacts| artifacts.iter().any(|artifact| artifact == id))
641 })
642 }
643
644 fn relations(
645 &self,
646 artifact: &ArtifactName,
647 input: bool,
648 ) -> Vec<&'a crate::store::definitions::Relation> {
649 self.definitions()
650 .into_iter()
651 .flat_map(|graph| graph.relations())
652 .filter(|relation| {
653 if input {
654 &relation.target == artifact
655 } else {
656 &relation.source == artifact
657 }
658 })
659 .collect()
660 }
661}
662
663fn relation_kind(relation: &crate::store::definitions::Relation) -> String {
664 use crate::store::definitions::RelationKind;
665 let kind = match &relation.kind {
666 RelationKind::Unknown { .. } => serde_json::to_string(relation).unwrap_or_default(),
667 RelationKind::Child { path } => {
668 format!(
669 "child {}",
670 path.value().map_or("", crate::config::ChildPrefix::as_str)
671 )
672 }
673 RelationKind::Mount { alias } => {
674 format!(
675 "mount {}",
676 alias.value().map_or("", crate::config::MountAlias::as_str)
677 )
678 }
679 RelationKind::Dependency { name, eval_id } => format!(
680 "dependency {} in {}",
681 name.value()
682 .map_or("", crate::config::ReferenceName::as_str),
683 eval_id.value().map_or("", crate::types::EvalId::as_str)
684 ),
685 RelationKind::Instruction { name, eval_id } => format!("{{{name}}} in {eval_id}"),
686 RelationKind::Argument {
687 eval_id,
688 index,
689 name,
690 ..
691 } => format!("argv {eval_id}[{index}] {{{name}}}"),
692 };
693 if relation.cyclic.value() == Some(&true) {
694 format!("{kind} ↻")
695 } else {
696 kind
697 }
698}
699
700fn profile(profile: &crate::config::StoredProfile) -> String {
701 use crate::config::{Field, StoredProfile};
702 match profile {
703 StoredProfile::Agent {
704 backend,
705 model,
706 reasoning,
707 ..
708 } => {
709 let reasoning = match reasoning {
710 Field::Value(reasoning) => format!(" reasoning {reasoning}"),
711 _ => String::new(),
712 };
713 format!(
714 "agent {} {model}{reasoning}",
715 serde_json::to_value(backend)
716 .expect("backend is JSON")
717 .as_str()
718 .expect("backend is a string")
719 )
720 }
721 StoredProfile::Runtime { command, args, .. } => {
722 format!("runtime {command} {}", args.join(" "))
723 }
724 StoredProfile::Human {} => "human".into(),
725 StoredProfile::Dependency { depends_on } => format!("dependency {}", depends_on.join(", ")),
726 }
727}
728
729fn options(options: &crate::store::ExecutionOptions) -> String {
731 let value = serde_json::to_value(options).unwrap_or_default();
732 let set: Vec<_> = value
733 .as_object()
734 .into_iter()
735 .flatten()
736 .map(|(key, value)| match value.as_str() {
737 Some(text) => format!("{key} {text}"),
738 None => format!("{key} {value}"),
739 })
740 .collect();
741 if set.is_empty() {
742 "declared defaults".into()
743 } else {
744 set.join(" · ")
745 }
746}
747
748fn request_detail(view: &RequestView, now: OffsetDateTime) -> Detail {
749 let request = &view.request;
750 let summary = query::request_summary(view, now);
751 let deps = join(&request.deps, ", ");
752 let mut detail = Detail {
753 title: format!("{} · {}", request.eval_id, request.title),
754 summary: join(
755 [
756 format!(
757 "{}{}",
758 request.status(),
759 elapsed(view, now).map_or(String::new(), |time| format!(" {time}"))
760 ),
761 profile(&request.profile),
762 if deps.is_empty() {
763 deps
764 } else {
765 format!("← {deps}")
766 },
767 ]
768 .into_iter()
769 .filter(|part| !part.is_empty()),
770 " · ",
771 ),
772 fields: Vec::new(),
773 };
774 detail.push(
775 "Status",
776 format!(
777 "{} — {}",
778 request.status(),
779 Status::from(request.status()).meaning()
780 ),
781 );
782 detail.push("Request", request.id.as_str());
783 detail.push("Reason", request.blocked_reason.clone().unwrap_or_default());
784 if request.profile.kind() == crate::config::ProfileKind::Dependency {
785 detail.push("Source", "derived (no execution)");
786 detail.push(
787 "Blocked by",
788 request
789 .blocked_by
790 .iter()
791 .map(ToString::to_string)
792 .collect::<Vec<_>>()
793 .join(", "),
794 );
795 }
796 detail.push("Error", error(view).unwrap_or_default());
797 detail.push("Instruction", request.payload.instruction());
798 detail.push("Profile", profile(&request.profile));
799 if request.requested_profile != request.profile {
800 detail.push("Requested", profile(&request.requested_profile));
801 }
802 detail.push("Options", options(&request.options));
803 detail.push(
804 "Fingerprint",
805 request.fingerprint.as_deref().unwrap_or("none"),
806 );
807 detail.push("Key", request.key.as_deref().unwrap_or("none (no reuse)"));
808 detail.push(
809 "Key covers",
810 join(
811 request
812 .fingerprints
813 .iter()
814 .map(|(name, fingerprint)| format!("{name} {fingerprint}")),
815 "\n",
816 ),
817 );
818 if let Some(source) = &request.provenance {
819 detail.push(
820 "Source",
821 if reused(view) {
822 format!(
823 "reused from Run {} request {} ({}, {}) completed {}{}",
824 source.run_id,
825 source.request_id,
826 source.eval_id,
827 crate::platform::path_text(&source.repo_path),
828 source
829 .completed_at
830 .map_or_else(|| "?".to_owned(), |time| time.to_string()),
831 request
832 .producer
833 .as_ref()
834 .map_or(String::new(), |producer| format!(" by {}", producer.name))
835 )
836 } else {
837 "executed in this Run".into()
838 },
839 );
840 }
841 if let Some(execution) = &view.execution {
842 let owner = execution
843 .owner
844 .map_or(String::new(), |owner| format!(" · owner pid {}", owner.pid));
845 detail.push(
846 "Execution",
847 format!("{} {}{owner}", execution.id, execution.status()),
848 );
849 }
850 if request.profile.kind() == crate::config::ProfileKind::Human || view.claim.is_some() {
851 detail.push("Claim", claim(view));
852 }
853 detail.push(
854 "Timing",
855 format!(
856 "created {} · started {} · completed {}{}",
857 request.created_at,
858 request
859 .started_at
860 .map_or_else(|| "-".to_owned(), |time| time.to_string()),
861 request
862 .completed_at()
863 .map_or_else(|| "-".to_owned(), |time| time.to_string()),
864 elapsed(view, now).map_or(String::new(), |time| format!(" · {time}"))
865 ),
866 );
867 detail.push("Deps", request.deps.join(", "));
868 detail.push(
869 "Argv",
870 request
871 .argv
872 .as_ref()
873 .map_or(String::new(), |argv| argv.join(" ")),
874 );
875 if let Some(result) = &request.result() {
876 detail.push("Result", result_summary(result));
877 detail.push(
878 "Raw result",
879 serde_json::to_string_pretty(result).unwrap_or_default(),
880 );
881 }
882 let total = counter_pairs(&summary.usage.usage);
883 let attempts = request.usage.as_ref().or(request.reused_usage.as_ref());
884 let attempts = attempts.map_or(&[][..], Vec::as_slice);
885 let original = attempts.len();
886 let attempts = attempts.iter().map(|attempt| {
887 let error = attempt.error.as_deref();
888 let error = error.map_or(String::new(), |error| format!(" error: {error}"));
889 format!(
890 "turn {} attempt {}: {}{error}",
891 attempt.turn,
892 attempt.attempt,
893 counter_pairs(&attempt.usage)
894 )
895 });
896 let state = if reused(view) {
897 format!("reused: spent none · original attempts {original}")
898 } else {
899 format!(
900 "{} · attempts {}{}",
901 summary.usage.usage_state,
902 summary.usage.attempts,
903 if total.is_empty() {
904 total
905 } else {
906 format!(" · total {total}")
907 }
908 )
909 };
910 detail.push("Usage", join(std::iter::once(state).chain(attempts), "\n"));
911 detail
912}
913
914pub fn detail(
915 run: &RunView,
916 requests: &[RequestView],
917 target: &Target,
918 now: OffsetDateTime,
919) -> Detail {
920 let saved = Saved { run, requests };
921 let mut detail = Detail::default();
922 match target {
923 Target::Run => {
924 let progress = progress(run, requests, now);
925 let strip = strip(&progress);
926 detail.title = format!("Run {}", run.run.id);
927 detail.summary = format!("{} · {}", progress.status, strip.headline);
928 detail.push("Status", progress.status.as_str());
929 detail.push("Validation", progress.validation.as_str());
930 detail.push(
931 "Counts",
932 join(
933 progress
934 .counts
935 .iter()
936 .filter(|(_, n)| *n > 0)
937 .map(|(status, n)| format!("{} {status} {n}", status.glyph())),
938 " · ",
939 ),
940 );
941 let lines = |items: &[(String, String)]| {
942 join(
943 items.iter().map(|(eval, text)| format!("{eval} {text}")),
944 "\n",
945 )
946 };
947 detail.push("Errors", lines(&progress.errors));
948 detail.push("Waiting", lines(&progress.waiting));
949 detail.push("Running", lines(&progress.running));
950 detail.push("Timing", progress.timing.as_str());
951 detail.push("Budget", progress.budget.as_str());
952 detail.push("Work", progress.work.as_str());
953 detail.push("Usage", progress.usage.as_str());
954 detail.push("Saved usage", progress.saved.as_str());
955 detail.push("Run", run.run.id.as_str());
956 detail.push("Repository", progress.repo.as_str());
957 }
958 Target::Eval(id) => {
959 let waits = tree::waits_for(&saved, id, now);
960 if let Some(view) = saved.request(id) {
961 let mut detail = request_detail(view, now);
962 detail.insert_after("Status", "Waits for", waits);
963 return detail;
964 }
965 let declaration = saved
966 .eval_definitions()
967 .iter()
968 .find(|eval| &eval.id == id)
969 .and_then(|eval| eval.declaration.value());
970 detail.title = format!(
971 "{id} · {}",
972 declaration
973 .and_then(|declaration| declaration.title.value())
974 .map_or("", String::as_str)
975 );
976 detail.push("Status", "not in this Run (outside the selection)");
977 detail.push("Waits for", waits);
978 detail.push(
979 "Instruction",
980 declaration
981 .and_then(|declaration| declaration.payload.value())
982 .map_or("", |payload| payload.instruction()),
983 );
984 detail.push(
985 "Profile",
986 declaration
987 .and_then(|declaration| declaration.profile.value())
988 .map_or_else(|| "unknown".into(), |saved| profile(&saved.known)),
989 );
990 }
991 Target::Artifact(id) => {
992 let artifact = saved.artifact(id);
993 let basis = if artifact.and_then(|artifact| artifact.basis.value()) == Some(&true) {
994 " [basis]"
995 } else {
996 ""
997 };
998 let kind = if artifact.and_then(|artifact| artifact.kind.value())
999 == Some(&crate::config::ArtifactKind::File)
1000 {
1001 " [file]"
1002 } else {
1003 ""
1004 };
1005 detail.title = format!("Artifact {id}{basis}{kind}");
1006 detail.push(
1007 "Tags",
1008 artifact
1009 .and_then(|artifact| artifact.tags.value())
1010 .map_or_else(String::new, |tags| tags.join(", ")),
1011 );
1012 detail.summary = tree::artifact_status(&saved, id, now);
1013 detail.push("Status", detail.summary.clone());
1014 let snapshot = saved.validation(id);
1015 if let Some(status) = snapshot.and_then(|s| s.status.value()) {
1016 detail.push(
1017 "At Run end",
1018 format!(
1019 "{status} · {}/{} Evals GREEN",
1020 snapshot
1021 .and_then(|s| s.passed.value())
1022 .copied()
1023 .unwrap_or(0),
1024 snapshot.and_then(|s| s.total.value()).copied().unwrap_or(0)
1025 ),
1026 );
1027 }
1028 detail.push(
1029 "Path",
1030 artifact
1031 .and_then(|artifact| artifact.path.value())
1032 .map_or_else(
1033 || "-".into(),
1034 |path| {
1035 if path.as_os_str().is_empty() {
1036 ".".into()
1037 } else {
1038 crate::platform::path_text(path)
1039 }
1040 },
1041 ),
1042 );
1043 detail.push(
1044 "Fingerprint",
1045 saved
1046 .validation(id)
1047 .and_then(|s| s.fingerprint.value())
1048 .map(|f| f.as_str())
1049 .unwrap_or_default(),
1050 );
1051 let component = saved.component(id);
1052 if component.and_then(|component| component.cyclic.value()) == Some(&true) {
1053 detail.push(
1054 "Cycle",
1055 format!(
1056 "↻ {}",
1057 join(
1058 component
1059 .and_then(|component| component.artifacts.value())
1060 .into_iter()
1061 .flatten(),
1062 ", "
1063 )
1064 ),
1065 );
1066 }
1067 let gates = component
1068 .and_then(|component| component.gates.value())
1069 .into_iter()
1070 .flatten()
1071 .map(|gate| mark(gate, saved.status(gate)));
1072 detail.push("Gates", join(gates, "\n"));
1073 let evals = saved.evals(id).into_iter();
1074 detail.push(
1075 "Evals",
1076 join(evals.map(|(eval, _)| mark(eval, saved.status(eval))), "\n"),
1077 );
1078 for (key, input) in [("Inputs", true), ("Used by", false)] {
1079 let relations = saved.relations(id, input).into_iter();
1080 let relations = relations.map(|relation| {
1081 format!(
1082 "{} — {}",
1083 if input {
1084 &relation.source
1085 } else {
1086 &relation.target
1087 },
1088 relation_kind(relation)
1089 )
1090 });
1091 detail.push(key, join(relations, "\n"));
1092 }
1093 }
1094 }
1095 detail
1096}
1097
1098fn counter_pairs(counters: &std::collections::BTreeMap<String, u64>) -> String {
1099 join(
1100 counters
1101 .iter()
1102 .map(|(name, value)| format!("{name} {value}")),
1103 ", ",
1104 )
1105}
1106
1107#[cfg(any(test, feature = "test-support"))]
1108pub fn derivation_count(run: &RunView, requests: &[RequestView]) -> usize {
1109 states::States::new(&Saved { run, requests })
1110 .derivations
1111 .get()
1112}