Skip to main content

artifactize_core/monitor/live/
current.rs

1//! A branch's worktree as it is now, scanned as `artifactize status` does: the graph of its
2//! current declarations, each Artifact marked by what verify would do with it now.
3use crate::{
4    config::{ArtifactKind, Profile, ProfileKind},
5    graph_structure::{self, Structure},
6    monitor::model::{self, Card, Status, Tone},
7    project::{
8        ArtifactCondition, EvalCondition, StatusView, VerifyAction, VerifyOptions,
9        selection::Selection,
10    },
11    store::{Execution, ExecutionResult},
12    types::{ArtifactName, EvalId, Fingerprint},
13};
14use std::{
15    collections::{BTreeMap, BTreeSet},
16    path::{Path, PathBuf},
17};
18use time::OffsetDateTime;
19use tokio_util::sync::CancellationToken;
20
21/// What verify would do now. Declared in precedence order: an Artifact takes the first plan
22/// any of its evals has (one to run before blocked before waiting before reused).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub enum Plan {
25    /// Evaluated again: at least one eval has no result to reuse.
26    Run,
27    /// Cannot be evaluated: an upstream eval is RED.
28    Blocked,
29    /// Waits: for its dependencies' evidence, or a live execution.
30    Wait,
31    /// Every eval's result would be reused without evaluation (an Artifact without evals has
32    /// nothing to evaluate).
33    Reuse,
34}
35
36impl Plan {
37    /// The plan as a Current names it: `reused`, `would run`, `waiting` or `blocked`.
38    pub fn word(self) -> &'static str {
39        match self {
40            Self::Run => "would run",
41            Self::Blocked => "blocked",
42            Self::Wait => "waiting",
43            Self::Reuse => "reused",
44        }
45    }
46
47    /// Its one-character mark: reused as a peer's reuse is marked, the others as the statuses
48    /// they lead to.
49    pub fn glyph(self) -> &'static str {
50        match self {
51            Self::Run => "○",
52            Self::Blocked => Status::Blocked.glyph(),
53            Self::Wait => Status::Wait.glyph(),
54            Self::Reuse => "↻",
55        }
56    }
57
58    pub fn tone(self) -> Tone {
59        match self {
60            Self::Run => Tone::Running,
61            Self::Blocked => Tone::Blocked,
62            Self::Wait => Tone::Queued,
63            Self::Reuse => Tone::Green,
64        }
65    }
66}
67
68impl CurrentArtifact {
69    /// Its plan as its node and row read: `would run (2 evals)`, `reused`, ….
70    pub fn text(&self) -> String {
71        match (self.plan, self.runs) {
72            (Plan::Run, 1) => "would run (1 eval)".into(),
73            (Plan::Run, runs) => format!("would run ({runs} evals)"),
74            (plan, _) => plan.word().into(),
75        }
76    }
77}
78
79impl From<VerifyAction> for Plan {
80    fn from(action: VerifyAction) -> Self {
81        match action {
82            VerifyAction::Execute => Self::Run,
83            VerifyAction::Blocked => Self::Blocked,
84            VerifyAction::Wait => Self::Wait,
85            VerifyAction::Reuse => Self::Reuse,
86        }
87    }
88}
89
90/// One eval of the scan.
91#[derive(Debug, Clone)]
92pub struct CurrentEval {
93    pub id: EvalId,
94    /// Its id within its Artifact, as a Run's tree names it.
95    pub name: String,
96    pub title: String,
97    pub kind: ProfileKind,
98    pub plan: Plan,
99    pub state: EvalCondition,
100    /// Why it would run, wait, be blocked or be reused.
101    pub reason: String,
102    /// Its instruction and the result it would reuse, as an eval's Summary shows them.
103    pub card: Card,
104    /// Every other fact by name: its profile, fingerprint, reuse key and what changed.
105    pub facts: Vec<(&'static str, String)>,
106}
107
108/// One Artifact of the scan.
109#[derive(Debug, Clone)]
110pub struct CurrentArtifact {
111    pub name: ArtifactName,
112    pub kind: ArtifactKind,
113    pub plan: Plan,
114    /// How many of its evals would run.
115    pub runs: usize,
116    pub path: PathBuf,
117    pub state: ArtifactCondition,
118    pub reason: String,
119    pub evals: Vec<CurrentEval>,
120}
121
122/// `to`'s evals use `from`, lit by `from`'s plan.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct CurrentEdge {
125    pub from: ArtifactName,
126    pub to: ArtifactName,
127    pub plan: Plan,
128}
129
130/// The scan of one worktree.
131#[derive(Debug, Clone)]
132pub struct Current {
133    pub scanned_at: OffsetDateTime,
134    pub fingerprint: Fingerprint,
135    /// Every Artifact, eval criterion and labeled relation as text.
136    pub structure: String,
137    /// In name order.
138    pub artifacts: Vec<CurrentArtifact>,
139    pub edges: Vec<CurrentEdge>,
140}
141
142/// Scan the workspace at `repo` as it is now against the state at `state`, as `artifactize
143/// status` does: it hashes files and may run fingerprint scripts or ask the review store, and
144/// records nothing.
145pub async fn scan(
146    repo: &Path,
147    state: &Path,
148    cancellation: CancellationToken,
149) -> Result<Current, String> {
150    let structure = graph_structure::read(repo)?;
151    let (status, reused) = crate::project::scan(
152        repo,
153        Some(state),
154        &Selection::All,
155        &VerifyOptions::default(),
156        cancellation,
157    )
158    .await?;
159    Ok(project(
160        &structure,
161        &status,
162        &reused,
163        OffsetDateTime::now_utc(),
164    ))
165}
166
167/// What running `profile` costs as Execution options name it.
168fn profile(profile: &Profile) -> String {
169    match profile {
170        Profile::Agent { backend, model, .. } => format!("agent · {} · {model}", backend.as_str()),
171        other => other.kind().name().to_owned(),
172    }
173}
174
175fn card(
176    instruction: &str,
177    profile: &Profile,
178    plan: Plan,
179    reused: Option<&Execution>,
180    now: OffsetDateTime,
181) -> Card {
182    let result = reused.and_then(Execution::result);
183    Card {
184        instruction: instruction.to_owned(),
185        result: result.map(model::entries),
186        exit_code: match result {
187            Some(ExecutionResult::Runtime(result)) => result.exit_code.value().copied(),
188            _ => None,
189        },
190        error: None,
191        reused: plan == Plan::Reuse,
192        took: reused
193            .and_then(|execution| model::span(execution.started_at, execution.completed_at(), now)),
194        tokens: reused
195            .and_then(|execution| execution.usage.as_ref())
196            .map(|attempts| {
197                attempts
198                    .iter()
199                    .filter_map(|attempt| attempt.usage.get("totalTokens"))
200                    .sum::<u64>()
201            })
202            .filter(|tokens| *tokens > 0),
203        model: match profile {
204            Profile::Agent { model, .. } => Some(model.to_string()),
205            _ => None,
206        },
207    }
208}
209
210/// The scan of `structure` as `status` found it, with the results it would `reuse`.
211pub(crate) fn project(
212    structure: &Structure,
213    status: &StatusView,
214    reused: &BTreeMap<EvalId, Execution>,
215    now: OffsetDateTime,
216) -> Current {
217    let artifacts: Vec<CurrentArtifact> = status
218        .artifacts
219        .iter()
220        .map(|artifact| {
221            let evals: Vec<CurrentEval> =
222                status
223                    .evals
224                    .iter()
225                    .filter(|eval| eval.target == artifact.id)
226                    .map(|eval| {
227                        let plan = Plan::from(eval.action);
228                        let declared = structure.evals.get(&eval.id);
229                        let instruction = declared
230                            .and_then(|declared| declared.payload.as_ref())
231                            .map_or("", |payload| payload.instruction.as_str());
232                        let mut facts = vec![("Profile", profile(&eval.profile))];
233                        facts.extend(
234                            eval.fingerprint
235                                .as_ref()
236                                .map(|fingerprint| ("Fingerprint", fingerprint.to_string())),
237                        );
238                        facts.push(("Eval", eval.id.to_string()));
239                        facts.push(("Verify would", eval.action.to_string()));
240                        facts.push(("State", eval.state.to_string()));
241                        facts.extend(eval.key.as_ref().map(|key| ("Reuse key", key.to_string())));
242                        facts.push(("Definition hash", eval.eval_def_hash.to_string()));
243                        facts.extend(eval.last.as_ref().map(|last| {
244                            ("Last Run", format!("{} · {}", last.run_id, last.verdict))
245                        }));
246                        if let Some(changes) = &eval.changes {
247                            facts.push(("Changes", changes.summary.clone()));
248                            facts.extend(
249                                changes
250                                    .files
251                                    .as_ref()
252                                    .map(|files| ("Changed files", files.join("\n"))),
253                            );
254                            facts.extend(
255                                changes
256                                    .dependencies
257                                    .as_ref()
258                                    .map(|names| ("Changed dependencies", names.join("\n"))),
259                            );
260                        }
261                        CurrentEval {
262                            id: eval.id.clone(),
263                            name: crate::monitor::model::tree::local(&eval.id).to_owned(),
264                            title: eval.title.clone(),
265                            kind: eval.profile.kind(),
266                            plan,
267                            state: eval.state,
268                            reason: eval.reason.clone(),
269                            card: card(instruction, &eval.profile, plan, reused.get(&eval.id), now),
270                            facts,
271                        }
272                    })
273                    .collect();
274            CurrentArtifact {
275                name: artifact.id.clone(),
276                kind: artifact.kind,
277                plan: evals
278                    .iter()
279                    .map(|eval| eval.plan)
280                    .min()
281                    .unwrap_or(Plan::Reuse),
282                runs: evals.iter().filter(|eval| eval.plan == Plan::Run).count(),
283                path: artifact.path.clone(),
284                state: artifact.state,
285                reason: artifact.reason.clone(),
286                evals,
287            }
288        })
289        .collect();
290    let plans: BTreeMap<&ArtifactName, Plan> = artifacts
291        .iter()
292        .map(|artifact| (&artifact.name, artifact.plan))
293        .collect();
294    let pairs: BTreeSet<(&ArtifactName, &ArtifactName)> = structure
295        .relations
296        .iter()
297        .filter(|relation| relation.source != relation.target)
298        .map(|relation| (&relation.source, &relation.target))
299        .collect();
300    let edges = pairs
301        .into_iter()
302        .filter_map(|(from, to)| {
303            Some(CurrentEdge {
304                plan: *plans.get(from)?,
305                from: from.clone(),
306                to: plans.contains_key(to).then(|| to.clone())?,
307            })
308        })
309        .collect();
310    Current {
311        scanned_at: now,
312        fingerprint: structure.fingerprint(),
313        structure: structure.tree(),
314        artifacts,
315        edges,
316    }
317}