mod graph;
pub use graph::{Graph, GraphEdge, GraphNode, Light, graph};
mod node_id;
pub use node_id::NodeId;
mod states;
mod status;
#[cfg(test)]
mod tests;
mod tree;
pub use states::{Activity, Busy, Completion, EvalView, NotRun, Queue, Source, Upstream, Waits};
pub use status::Status;
pub use tree::{Clock, Kind, Node, Segment, Tone, Weight, completion, tree, upstream_index};
use serde_json::Value;
use time::OffsetDateTime;
use crate::{
query,
store::{RequestView, RunSummary, RunView},
types::{ArtifactName, EvalId},
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Target {
Run,
Artifact(crate::types::ArtifactName),
Eval(crate::types::EvalId),
}
impl Target {
pub fn parse(id: &str) -> Option<Self> {
let (kind, name) = id.split_once(':')?;
match kind {
"run" => name.parse::<crate::types::RunId>().ok().map(|_| Self::Run),
"a" => name.parse().ok().map(Self::Artifact),
"e" => name.parse().ok().map(Self::Eval),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Section {
Outcome,
Waits,
What,
Provenance,
Technical,
}
impl Section {
pub const ALL: [Self; 5] = [
Self::Outcome,
Self::Waits,
Self::What,
Self::Provenance,
Self::Technical,
];
pub fn of(key: &str) -> Self {
match key {
"Status" | "Error" | "Reason" | "Blocked by" | "Result" | "Claim" | "Source"
| "At Run end" | "Validation" | "Counts" | "Errors" | "Waiting" | "Running"
| "Evals" | "Gates" => Self::Outcome,
"Waits for" => Self::Waits,
"Timing" | "Usage" | "Budget" | "Work" | "Saved usage" => Self::Provenance,
"Execution" | "Fingerprint" | "Key" | "Key covers" | "Path" | "Tags" | "Cycle"
| "Run" | "Repository" => Self::Technical,
_ => Self::What,
}
}
pub fn name(self) -> &'static str {
match self {
Self::Outcome => "Outcome",
Self::Waits => "Waits for",
Self::What => "What",
Self::Provenance => "Provenance",
Self::Technical => "Technical",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RunRow {
pub id: crate::types::RunId,
pub repo: std::path::PathBuf,
pub status: crate::types::RunStatus,
pub counts: String,
pub age: String,
pub took: String,
}
#[derive(Debug, Clone)]
pub struct Progress {
pub status: Status,
pub repo: String,
pub validation: String,
pub timing: String,
pub elapsed: String,
pub finished: bool,
pub satisfied: Option<bool>,
pub counts: Vec<(Status, u64)>,
pub budget: String,
pub work: String,
pub usage: String,
pub saved: String,
pub tokens: Option<u64>,
pub running: Vec<(String, String)>,
pub waiting: Vec<(String, String)>,
pub errors: Vec<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Strip {
pub status: Status,
pub headline: String,
pub attention: Vec<(Status, String)>,
pub more: usize,
}
#[derive(Debug, Clone, Default)]
pub struct Detail {
pub title: String,
pub summary: String,
pub fields: Vec<(&'static str, String)>,
}
impl Detail {
pub fn field(&self, name: &str) -> Option<&str> {
self.fields
.iter()
.find(|(key, _)| *key == name)
.map(|(_, value)| value.as_str())
}
fn push(&mut self, key: &'static str, value: impl Into<String>) {
let value = value.into();
if !value.is_empty() {
self.fields.push((key, value));
}
}
fn insert_after(&mut self, after: &str, key: &'static str, value: String) {
if value.is_empty() {
return;
}
let index = self.fields.iter().position(|(name, _)| *name == after);
let index = index.map_or(self.fields.len(), |index| index + 1);
self.fields.insert(index, (key, value));
}
}
const ATTENTION_LINES: usize = 3;
const ABSENT: &str = "not in Run";
pub fn glyph(status: Option<Status>) -> &'static str {
status.map_or("-", Status::glyph)
}
fn join<T: AsRef<str>>(items: impl IntoIterator<Item = T>, separator: &str) -> String {
let items: Vec<_> = items
.into_iter()
.map(|item| item.as_ref().to_owned())
.collect();
items.join(separator)
}
fn mark(name: &EvalId, status: Option<Status>) -> String {
format!(
"{} {name} {}",
glyph(status),
status.map_or(ABSENT, Status::as_str)
)
}
const MINUTE: i64 = 60;
const HOUR: i64 = 60 * MINUTE;
const DAY: i64 = 24 * HOUR;
pub fn duration(elapsed: time::Duration) -> String {
let seconds = elapsed.whole_seconds().max(0);
match seconds {
0..MINUTE => format!("{seconds}s"),
MINUTE..HOUR => format!("{}m {:02}s", seconds / MINUTE, seconds % MINUTE),
HOUR..DAY => format!("{}h {:02}m", seconds / HOUR, seconds % HOUR / MINUTE),
_ => format!("{}d {}h", seconds / DAY, seconds % DAY / HOUR),
}
}
#[cfg(test)]
#[test]
fn typed_durations_keep_the_display_units_and_clamp_clock_skew() {
for (elapsed, expected) in [
(time::Duration::seconds(-1), "0s"),
(time::Duration::ZERO, "0s"),
(time::Duration::milliseconds(59_999), "59s"),
(time::Duration::minutes(1), "1m 00s"),
(time::Duration::seconds(125), "2m 05s"),
(time::Duration::hours(1), "1h 00m"),
(time::Duration::minutes(61), "1h 01m"),
(time::Duration::days(1), "1d 0h"),
(time::Duration::hours(25), "1d 1h"),
] {
assert_eq!(duration(elapsed), expected);
}
}
fn span(
start: crate::types::Timestamp,
end: Option<crate::types::Timestamp>,
now: OffsetDateTime,
) -> Option<String> {
let end = end.map_or(now, crate::types::Timestamp::time);
Some(duration(end - start.time()))
}
pub fn counts(counts: impl IntoIterator<Item = (Status, u64)>) -> String {
let mut counts: Vec<_> = counts.into_iter().filter(|(_, n)| *n > 0).collect();
counts.sort_by_key(|(status, _)| *status);
join(
counts
.into_iter()
.map(|(status, n)| format!("{}{n}", status.glyph())),
" ",
)
}
pub fn tokens(total: u64) -> String {
const THOUSAND: u64 = 1_000;
const MILLION: u64 = 1_000_000;
match total {
0..THOUSAND => total.to_string(),
THOUSAND..MILLION => format!("{:.1}k", total as f64 / THOUSAND as f64),
_ => format!("{:.1}M", total as f64 / MILLION as f64),
}
}
pub fn run_rows(runs: &[RunSummary], now: OffsetDateTime) -> Vec<RunRow> {
runs.iter()
.map(|run| RunRow {
id: run.id.clone(),
repo: run.repo_path.clone(),
status: run.status,
counts: counts(run.counts.iter().map(|(status, n)| ((*status).into(), *n))),
age: span(run.created_at, None, now).unwrap_or_default(),
took: run
.completed_at
.and_then(|end| span(run.created_at, Some(end), now))
.unwrap_or_default(),
})
.collect()
}
fn reused(view: &RequestView) -> bool {
query::reused(&view.request)
}
fn elapsed(view: &RequestView, now: OffsetDateTime) -> Option<String> {
let request = &view.request;
if reused(view) {
return Some("reused".into());
}
let active = matches!(
request.status(),
crate::types::RequestStatus::Running
| crate::types::RequestStatus::WaitingHuman
| crate::types::RequestStatus::Deferred
);
let start = request
.started_at
.or(active.then_some(request.created_at))?;
span(start, request.completed_at(), now)
}
fn claim(view: &RequestView) -> String {
view.claim.as_ref().map_or("unclaimed".into(), |claim| {
format!("claimed by {} at {}", claim.reviewer, claim.claimed_at)
})
}
fn error(view: &RequestView) -> Option<String> {
let request = &view.request;
let error = request.error().map(str::to_owned)?;
Some(match &request.error_code() {
Some(code) => format!("[{code}] {error}"),
None => error,
})
}
pub fn progress(view: &RunView, requests: &[RequestView], now: OffsetDateTime) -> Progress {
let run = &view.run;
let (summary, saved_usage) = query::run_summary(view, now);
let with = |status: crate::types::RequestStatus| {
requests
.iter()
.filter(move |view| view.request.status() == status)
};
let usage = counter_pairs(&summary.usage.usage);
let saved = counter_pairs(&saved_usage);
let spent = &summary.usage.usage;
let tokens = spent.get("totalTokens").copied().or_else(|| {
let input = spent.get("inputTokens").copied();
let output = spent.get("outputTokens").copied();
(input.is_some() || output.is_some())
.then(|| input.unwrap_or(0).saturating_add(output.unwrap_or(0)))
});
let validation = run.validation.snapshot();
let unmet = validation
.and_then(|v| v.obligations.value())
.map(|ids| {
ids.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default();
let wall = span(run.created_at, run.completed_at(), now).unwrap_or_default();
Progress {
status: run.status().into(),
repo: crate::platform::path_text(&run.repo_path),
validation: match validation.and_then(|v| v.satisfied.value()) {
None => "pending".into(),
Some(true) => "SATISFIED at Run end".into(),
Some(_) if unmet.is_empty() => "NOT SATISFIED at Run end".into(),
Some(_) => format!("NOT SATISFIED at Run end (unmet: {unmet})"),
},
timing: format!(
"started {} · {} {wall}",
run.created_at,
if run.completed_at().is_some() {
"took"
} else {
"elapsed"
},
),
elapsed: wall,
finished: run.completed_at().is_some(),
satisfied: validation.and_then(|v| v.satisfied.value()).copied(),
counts: summary
.counts
.iter()
.map(|(status, count)| ((*status).into(), *count))
.collect(),
budget: format!(
"executions {}/{} · jobs {}",
run.executions_started,
run.max_executions
.map_or("unlimited".into(), |max| max.to_string()),
run.jobs
.map_or("unlimited".into(), |limit| limit.to_string()),
),
work: format!(
"executed {} · reused {}",
summary.executed.total, summary.reused.kinds.total,
),
usage,
saved,
tokens,
running: with(crate::types::RequestStatus::Running)
.map(|view| {
(
view.request.eval_id.to_string(),
elapsed(view, now).unwrap_or_default(),
)
})
.collect(),
waiting: requests
.iter()
.filter(|view| view.request.status().is_human_wait())
.map(|view| (view.request.eval_id.to_string(), claim(view)))
.collect(),
errors: with(crate::types::RequestStatus::Error)
.map(|view| {
(
view.request.eval_id.to_string(),
error(view).unwrap_or_default(),
)
})
.chain(
run.error()
.map(str::to_owned)
.map(|error| ("Run".into(), error)),
)
.chain(
view.unreadable
.iter()
.map(|record| (record.id.clone(), record.to_string())),
)
.collect(),
}
}
pub fn strip(progress: &Progress) -> Strip {
let validation = match progress.satisfied {
None => "validation pending".to_owned(),
Some(true) => "SATISFIED at Run end".to_owned(),
Some(false) => "NOT SATISFIED at Run end".to_owned(),
};
let counts = counts(progress.counts.iter().copied());
let time = format!(
"{} {}",
if progress.finished { "took" } else { "elapsed" },
progress.elapsed
);
let tokens = progress
.tokens
.map(|total| format!("tokens {}", tokens(total)));
let headline = join(
[Some(validation), Some(counts), Some(time), tokens]
.into_iter()
.flatten()
.filter(|part| !part.is_empty()),
" · ",
);
let mut attention: Vec<(Status, String)> = progress
.errors
.iter()
.map(|(eval, error)| (Status::Error, format!("{eval} {error}")))
.chain(progress.waiting.iter().map(|(eval, claim)| {
(
Status::WaitingHuman,
format!("{eval} waiting Human · {claim}"),
)
}))
.chain(progress.running.iter().map(|(eval, time)| {
(
Status::Running,
format!("{eval} running {time}").trim_end().to_owned(),
)
}))
.collect();
let more = attention.len().saturating_sub(ATTENTION_LINES);
attention.truncate(ATTENTION_LINES);
Strip {
status: progress.status,
headline,
attention,
more,
}
}
pub fn run_node(run: &RunView) -> Node {
let status = Status::from(run.run.status());
Node {
id: NodeId::run(&run.run.id),
clock: None,
kind: Kind::Artifact {
completion: Completion::Complete,
basis: false,
},
glyph: status.glyph(),
tone: match status {
Status::Green => Tone::Green,
Status::Red => Tone::Red,
Status::Error => Tone::Error,
Status::Running => Tone::Running,
Status::Blocked | Status::BudgetExhausted | Status::Incomplete => Tone::Blocked,
_ => Tone::Muted,
},
weight: Weight::Normal,
name: "Run".into(),
marks: String::new(),
text: vec![Segment {
text: "Enter: usage, budgets, counts".into(),
tone: Some(Tone::Muted),
}],
right: String::new(),
compact: String::new(),
changed: false,
upstream: Vec::new(),
held: false,
children: Vec::new(),
}
}
fn result_summary(result: &crate::store::ExecutionResult) -> String {
let result = result.to_json();
let Some(object) = result.as_object() else {
return result
.to_string()
.lines()
.next()
.unwrap_or_default()
.to_owned();
};
let mut entries: Vec<_> = object
.iter()
.filter(|(key, _)| !matches!(key.as_str(), "stdout" | "stderr"))
.collect();
entries.sort_by_key(|(key, _)| *key != "verdict");
let parts = entries.into_iter().map(|(key, value)| match value {
Value::String(text) => {
let line = text.lines().next().unwrap_or_default();
if line.len() < text.trim_end().len() {
format!("{key} {line} …")
} else {
format!("{key} {line}")
}
}
Value::Array(items) => format!("{key} [{} items]", items.len()),
Value::Object(fields) => format!("{key} {{{} fields}}", fields.len()),
other => format!("{key} {other}"),
});
join(parts, "\n")
}
struct Saved<'a> {
run: &'a RunView,
requests: &'a [RequestView],
}
impl<'a> Saved<'a> {
fn definitions(&self) -> Option<&'a crate::store::definitions::Graph> {
self.run.run.definitions.graph()
}
fn artifacts(
&self,
) -> impl Iterator<
Item = (
&'a crate::types::ArtifactName,
&'a crate::store::definitions::Artifact,
),
> {
self.definitions()
.into_iter()
.flat_map(|graph| graph.artifacts())
}
fn artifact(&self, id: &ArtifactName) -> Option<&'a crate::store::definitions::Artifact> {
self.definitions().and_then(|graph| graph.artifact(id))
}
fn eval_definitions(&self) -> &'a [crate::store::definitions::Eval] {
self.definitions().map_or(&[], |graph| graph.evals())
}
fn components(&self) -> &'a [crate::store::definitions::Component] {
self.definitions().map_or(&[], |graph| graph.components())
}
fn request(&self, eval: &EvalId) -> Option<&'a RequestView> {
self.requests
.iter()
.find(|view| &view.request.eval_id == eval)
}
fn status(&self, eval: &EvalId) -> Option<Status> {
self.request(eval).map(|view| view.request.status().into())
}
fn artifact_ids(&self) -> Vec<&'a ArtifactName> {
let components = self.components().iter();
let saved = components
.flat_map(|component| {
let mut members: Vec<_> =
component.artifacts.value().into_iter().flatten().collect();
members.sort_unstable();
members
})
.chain(self.artifacts().map(|(id, _)| id))
.chain(self.requests.iter().map(|view| &view.request.target));
let mut seen = std::collections::HashSet::new();
saved.filter(|id| seen.insert(*id)).collect()
}
fn evals(
&self,
artifact: &ArtifactName,
) -> Vec<(&'a EvalId, Option<&'a crate::store::definitions::Eval>)> {
let mut evals: Vec<_> = self
.eval_definitions()
.iter()
.filter(|eval| &eval.target == artifact)
.map(|eval| (&eval.id, Some(eval)))
.collect();
for request in self.requests.iter().map(|view| &view.request) {
if &request.target == artifact && !evals.iter().any(|(id, _)| *id == &request.eval_id) {
evals.push((&request.eval_id, None));
}
}
evals
}
fn validation(&self, artifact: &ArtifactName) -> Option<&'a crate::store::ArtifactValidation> {
self.run
.run
.validation
.snapshot()?
.artifacts
.value()?
.iter()
.find(|a| &a.id == artifact)
}
fn component(&self, id: &ArtifactName) -> Option<&'a crate::store::definitions::Component> {
self.components().iter().find(|component| {
component
.artifacts
.value()
.is_some_and(|artifacts| artifacts.iter().any(|artifact| artifact == id))
})
}
fn relations(
&self,
artifact: &ArtifactName,
input: bool,
) -> Vec<&'a crate::store::definitions::Relation> {
self.definitions()
.into_iter()
.flat_map(|graph| graph.relations())
.filter(|relation| {
if input {
&relation.target == artifact
} else {
&relation.source == artifact
}
})
.collect()
}
}
fn relation_kind(relation: &crate::store::definitions::Relation) -> String {
use crate::store::definitions::RelationKind;
let kind = match &relation.kind {
RelationKind::Unknown { .. } => serde_json::to_string(relation).unwrap_or_default(),
RelationKind::Child { path } => {
format!(
"child {}",
path.value().map_or("", crate::config::ChildPrefix::as_str)
)
}
RelationKind::Instruction { name, eval_id } => format!("{{{name}}} in {eval_id}"),
RelationKind::Argument {
eval_id,
index,
name,
..
} => format!("argv {eval_id}[{index}] {{{name}}}"),
};
if relation.cyclic.value() == Some(&true) {
format!("{kind} ↻")
} else {
kind
}
}
fn profile(profile: &crate::config::StoredProfile) -> String {
use crate::config::{Field, StoredProfile};
match profile {
StoredProfile::Agent {
backend,
model,
reasoning,
..
} => {
let reasoning = match reasoning {
Field::Value(reasoning) => format!(" reasoning {reasoning}"),
_ => String::new(),
};
format!(
"agent {} {model}{reasoning}",
serde_json::to_value(backend)
.expect("backend is JSON")
.as_str()
.expect("backend is a string")
)
}
StoredProfile::Runtime { command, args, .. } => {
format!("runtime {command} {}", args.join(" "))
}
StoredProfile::Human {} => "human".into(),
}
}
fn options(options: &crate::store::ExecutionOptions) -> String {
let value = serde_json::to_value(options).unwrap_or_default();
let set: Vec<_> = value
.as_object()
.into_iter()
.flatten()
.map(|(key, value)| match value.as_str() {
Some(text) => format!("{key} {text}"),
None => format!("{key} {value}"),
})
.collect();
if set.is_empty() {
"declared defaults".into()
} else {
set.join(" · ")
}
}
fn request_detail(view: &RequestView, now: OffsetDateTime) -> Detail {
let request = &view.request;
let summary = query::request_summary(view, now);
let deps = join(&request.deps, ", ");
let mut detail = Detail {
title: format!("{} · {}", request.eval_id, request.title),
summary: join(
[
format!(
"{}{}",
request.status(),
elapsed(view, now).map_or(String::new(), |time| format!(" {time}"))
),
profile(&request.profile),
if deps.is_empty() {
deps
} else {
format!("← {deps}")
},
]
.into_iter()
.filter(|part| !part.is_empty()),
" · ",
),
fields: Vec::new(),
};
detail.push(
"Status",
format!(
"{} — {}",
request.status(),
Status::from(request.status()).meaning()
),
);
detail.push("Request", request.id.as_str());
detail.push("Reason", request.blocked_reason.clone().unwrap_or_default());
detail.push("Error", error(view).unwrap_or_default());
detail.push("Instruction", request.payload.instruction());
detail.push("Profile", profile(&request.profile));
if request.requested_profile != request.profile {
detail.push("Requested", profile(&request.requested_profile));
}
detail.push("Options", options(&request.options));
detail.push(
"Fingerprint",
request.fingerprint.as_deref().unwrap_or("none"),
);
detail.push("Key", request.key.as_deref().unwrap_or("none (no reuse)"));
detail.push(
"Key covers",
join(
request
.fingerprints
.iter()
.map(|(name, fingerprint)| format!("{name} {fingerprint}")),
"\n",
),
);
if let Some(source) = &request.provenance {
detail.push(
"Source",
if reused(view) {
format!(
"reused from Run {} request {} ({}, {}) completed {}{}",
source.run_id,
source.request_id,
source.eval_id,
crate::platform::path_text(&source.repo_path),
source
.completed_at
.map_or_else(|| "?".to_owned(), |time| time.to_string()),
request
.producer
.as_ref()
.map_or(String::new(), |producer| format!(" by {}", producer.name))
)
} else {
"executed in this Run".into()
},
);
}
if let Some(execution) = &view.execution {
let owner = execution
.owner
.map_or(String::new(), |owner| format!(" · owner pid {}", owner.pid));
detail.push(
"Execution",
format!("{} {}{owner}", execution.id, execution.status()),
);
}
if request.profile.kind() == crate::config::ProfileKind::Human || view.claim.is_some() {
detail.push("Claim", claim(view));
}
detail.push(
"Timing",
format!(
"created {} · started {} · completed {}{}",
request.created_at,
request
.started_at
.map_or_else(|| "-".to_owned(), |time| time.to_string()),
request
.completed_at()
.map_or_else(|| "-".to_owned(), |time| time.to_string()),
elapsed(view, now).map_or(String::new(), |time| format!(" · {time}"))
),
);
detail.push("Deps", request.deps.join(", "));
detail.push(
"Argv",
request
.argv
.as_ref()
.map_or(String::new(), |argv| argv.join(" ")),
);
if let Some(result) = &request.result() {
detail.push("Result", result_summary(result));
detail.push(
"Raw result",
serde_json::to_string_pretty(result).unwrap_or_default(),
);
}
let total = counter_pairs(&summary.usage.usage);
let attempts = request.usage.as_ref().or(request.reused_usage.as_ref());
let attempts = attempts.map_or(&[][..], Vec::as_slice);
let original = attempts.len();
let attempts = attempts.iter().map(|attempt| {
let error = attempt.error.as_deref();
let error = error.map_or(String::new(), |error| format!(" error: {error}"));
format!(
"turn {} attempt {}: {}{error}",
attempt.turn,
attempt.attempt,
counter_pairs(&attempt.usage)
)
});
let state = if reused(view) {
format!("reused: spent none · original attempts {original}")
} else {
format!(
"{} · attempts {}{}",
summary.usage.usage_state,
summary.usage.attempts,
if total.is_empty() {
total
} else {
format!(" · total {total}")
}
)
};
detail.push("Usage", join(std::iter::once(state).chain(attempts), "\n"));
detail
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Card {
pub instruction: String,
pub result: Option<serde_json::Map<String, Value>>,
pub exit_code: Option<i32>,
pub error: Option<String>,
pub reused: bool,
pub took: Option<String>,
pub tokens: Option<u64>,
pub model: Option<String>,
}
pub fn card(view: &RequestView, now: OffsetDateTime) -> Card {
use crate::config::StoredProfile;
use crate::store::ExecutionResult;
let request = &view.request;
let reused = reused(view);
let result = request.result();
let attempts = if reused {
request.reused_usage.as_ref()
} else {
request.usage.as_ref()
};
let tokens = attempts
.map(|attempts| {
attempts
.iter()
.filter_map(|attempt| attempt.usage.get("totalTokens"))
.sum::<u64>()
})
.filter(|tokens| *tokens > 0);
Card {
instruction: request.payload.instruction().to_owned(),
result: result.map(|result| {
let mut entries = result.to_json().as_object().cloned().unwrap_or_default();
for key in [
"verdict",
"stdout",
"stderr",
"exitCode",
"durationMs",
"truncated",
] {
entries.remove(key);
}
entries
}),
exit_code: match result {
Some(ExecutionResult::Runtime(result)) => result.exit_code.value().copied(),
_ => None,
},
error: error(view),
reused,
took: view
.execution
.as_ref()
.and_then(|execution| span(execution.started_at, execution.completed_at(), now)),
tokens,
model: match &request.profile {
StoredProfile::Agent { model, .. } => Some(model.to_string()),
_ => None,
},
}
}
pub fn detail(
run: &RunView,
requests: &[RequestView],
target: &Target,
now: OffsetDateTime,
) -> Detail {
let saved = Saved { run, requests };
let mut detail = Detail::default();
match target {
Target::Run => {
let progress = progress(run, requests, now);
let strip = strip(&progress);
detail.title = format!("Run {}", run.run.id);
detail.summary = format!("{} · {}", progress.status, strip.headline);
detail.push("Status", progress.status.as_str());
detail.push("Validation", progress.validation.as_str());
detail.push(
"Counts",
join(
progress
.counts
.iter()
.filter(|(_, n)| *n > 0)
.map(|(status, n)| format!("{} {status} {n}", status.glyph())),
" · ",
),
);
let lines = |items: &[(String, String)]| {
join(
items.iter().map(|(eval, text)| format!("{eval} {text}")),
"\n",
)
};
detail.push("Errors", lines(&progress.errors));
detail.push("Waiting", lines(&progress.waiting));
detail.push("Running", lines(&progress.running));
detail.push("Timing", progress.timing.as_str());
detail.push("Budget", progress.budget.as_str());
detail.push("Work", progress.work.as_str());
detail.push("Usage", progress.usage.as_str());
detail.push("Saved usage", progress.saved.as_str());
detail.push("Run", run.run.id.as_str());
detail.push("Repository", progress.repo.as_str());
}
Target::Eval(id) => {
let waits = tree::waits_for(&saved, id, now);
if let Some(view) = saved.request(id) {
let mut detail = request_detail(view, now);
detail.insert_after("Status", "Waits for", waits);
return detail;
}
let declaration = saved
.eval_definitions()
.iter()
.find(|eval| &eval.id == id)
.and_then(|eval| eval.declaration.value());
detail.title = format!(
"{id} · {}",
declaration
.and_then(|declaration| declaration.title.value())
.map_or("", String::as_str)
);
detail.push("Status", "not in this Run (outside the selection)");
detail.push("Waits for", waits);
detail.push(
"Instruction",
declaration
.and_then(|declaration| declaration.payload.value())
.map_or("", |payload| payload.instruction()),
);
detail.push(
"Profile",
declaration
.and_then(|declaration| declaration.profile.value())
.map_or_else(|| "unknown".into(), |saved| profile(&saved.known)),
);
}
Target::Artifact(id) => {
let artifact = saved.artifact(id);
let basis = if artifact.and_then(|artifact| artifact.basis.value()) == Some(&true) {
" [basis]"
} else {
""
};
let kind = if artifact.and_then(|artifact| artifact.kind.value())
== Some(&crate::config::ArtifactKind::File)
{
" [file]"
} else {
""
};
detail.title = format!("Artifact {id}{basis}{kind}");
detail.push(
"Tags",
artifact
.and_then(|artifact| artifact.tags.value())
.map_or_else(String::new, |tags| tags.join(", ")),
);
detail.summary = tree::artifact_status(&saved, id, now);
detail.push("Status", detail.summary.clone());
detail.push("Waits for", tree::artifact_waits(&saved, id, now));
let snapshot = saved.validation(id);
if let Some(status) = snapshot.and_then(|s| s.status.value()) {
detail.push(
"At Run end",
format!(
"{status} · {}/{} Evals GREEN",
snapshot
.and_then(|s| s.passed.value())
.copied()
.unwrap_or(0),
snapshot.and_then(|s| s.total.value()).copied().unwrap_or(0)
),
);
}
detail.push(
"Path",
artifact
.and_then(|artifact| artifact.path.value())
.map_or_else(
|| "-".into(),
|path| {
if path.as_os_str().is_empty() {
".".into()
} else {
crate::platform::path_text(path)
}
},
),
);
detail.push(
"Fingerprint",
saved
.validation(id)
.and_then(|s| s.fingerprint.value())
.map(|f| f.as_str())
.unwrap_or_default(),
);
let component = saved.component(id);
if component.and_then(|component| component.cyclic.value()) == Some(&true) {
detail.push(
"Cycle",
format!(
"↻ {}",
join(
component
.and_then(|component| component.artifacts.value())
.into_iter()
.flatten(),
", "
)
),
);
}
let gates = component
.and_then(|component| component.gates.value())
.into_iter()
.flatten()
.map(|gate| mark(gate, saved.status(gate)));
detail.push("Gates", join(gates, "\n"));
let evals = saved.evals(id).into_iter();
detail.push(
"Evals",
join(evals.map(|(eval, _)| mark(eval, saved.status(eval))), "\n"),
);
for (key, input) in [("Inputs", true), ("Used by", false)] {
let relations = saved.relations(id, input).into_iter();
let relations = relations.map(|relation| {
format!(
"{} — {}",
if input {
&relation.source
} else {
&relation.target
},
relation_kind(relation)
)
});
detail.push(key, join(relations, "\n"));
}
}
}
detail
}
fn counter_pairs(counters: &std::collections::BTreeMap<String, u64>) -> String {
join(
counters
.iter()
.map(|(name, value)| format!("{name} {value}")),
", ",
)
}
#[cfg(any(test, feature = "test-support"))]
pub fn derivation_count(run: &RunView, requests: &[RequestView]) -> usize {
states::States::new(&Saved { run, requests })
.derivations
.get()
}