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