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, EvalState, StatusView, VerifyAction, VerifyOptions,
9        selection::Selection,
10    },
11    store::{Claim, Execution, ExecutionResult},
12    types::{ArtifactName, EvalId, Fingerprint},
13};
14use std::{
15    collections::{BTreeMap, BTreeSet},
16    path::{Path, PathBuf},
17    sync::Arc,
18};
19use time::OffsetDateTime;
20use tokio_util::sync::CancellationToken;
21
22/// What verify would do now. Declared in precedence order: an Artifact takes the first plan
23/// any of its evals has (one to run before one still being checked before blocked before
24/// waiting before reused).
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum Plan {
27    /// Evaluated again: at least one eval has no result to reuse.
28    Run,
29    /// Not known yet: a fingerprint it needs, or the plan of an eval it waits for, is still
30    /// being computed.
31    Check,
32    /// Cannot be evaluated: an upstream eval is RED.
33    Blocked,
34    /// Waits: for its dependencies' evidence, or a live execution.
35    Wait,
36    /// Every eval's result would be reused without evaluation (an Artifact without evals has
37    /// nothing to evaluate).
38    Reuse,
39}
40
41impl Plan {
42    /// The plan as a Current names it: `reused`, `would run`, `checking`, `waiting` or
43    /// `blocked`.
44    pub fn word(self) -> &'static str {
45        match self {
46            Self::Run => "would run",
47            Self::Check => "checking",
48            Self::Blocked => "blocked",
49            Self::Wait => "waiting",
50            Self::Reuse => "reused",
51        }
52    }
53
54    /// Its one-character mark: reused as a peer's reuse is marked, the others as the statuses
55    /// they lead to.
56    pub fn glyph(self) -> &'static str {
57        match self {
58            Self::Run => "○",
59            Self::Check => "…",
60            Self::Blocked => Status::Blocked.glyph(),
61            Self::Wait => Status::Wait.glyph(),
62            Self::Reuse => "↻",
63        }
64    }
65
66    pub fn tone(self) -> Tone {
67        match self {
68            Self::Run => Tone::Running,
69            Self::Check => Tone::Muted,
70            Self::Blocked => Tone::Blocked,
71            Self::Wait => Tone::Queued,
72            Self::Reuse => Tone::Green,
73        }
74    }
75}
76
77impl CurrentArtifact {
78    /// Its plan as its node and row read: `would run (2 evals)`, `reused`, ….
79    pub fn text(&self) -> String {
80        match (self.plan, self.runs) {
81            (Plan::Run, 1) => "would run (1 eval)".into(),
82            // Without evals of its own, what its parts would do.
83            (plan, 0) => plan.word().into(),
84            (Plan::Run, runs) => format!("would run ({runs} evals)"),
85            (plan, _) => plan.word().into(),
86        }
87    }
88}
89
90impl From<VerifyAction> for Plan {
91    fn from(action: VerifyAction) -> Self {
92        match action {
93            VerifyAction::Execute => Self::Run,
94            VerifyAction::Blocked => Self::Blocked,
95            VerifyAction::Wait => Self::Wait,
96            VerifyAction::Reuse => Self::Reuse,
97        }
98    }
99}
100
101/// Why an eval is still being checked.
102const CHECKING: &str = "Still being checked: a fingerprint its key needs, or the plan of an eval it waits for, is not known yet.";
103
104/// One eval of the scan.
105#[derive(Debug, Clone)]
106pub struct CurrentEval {
107    pub id: EvalId,
108    /// Its id within its Artifact, as a Run's tree names it.
109    pub name: String,
110    pub title: String,
111    pub kind: ProfileKind,
112    pub plan: Plan,
113    pub state: EvalCondition,
114    /// Why it would run, wait, be blocked or be reused.
115    pub reason: String,
116    /// Its instruction and the result it would reuse, as an eval's Summary shows them.
117    pub card: Card,
118    /// Every other fact by name: its profile, fingerprint, reuse key and what changed.
119    pub facts: Vec<(&'static str, String)>,
120}
121
122/// One Artifact of the scan.
123#[derive(Debug, Clone)]
124pub struct CurrentArtifact {
125    pub name: ArtifactName,
126    pub kind: ArtifactKind,
127    pub plan: Plan,
128    /// How many of its evals would run.
129    pub runs: usize,
130    pub path: PathBuf,
131    pub state: ArtifactCondition,
132    pub reason: String,
133    pub evals: Vec<CurrentEval>,
134    /// The Artifacts it is made of directly, in name order.
135    pub parts: Vec<ArtifactName>,
136}
137
138/// `to`'s evals use `from`, lit by `from`'s plan; a part is no edge but drawn in its whole.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct CurrentEdge {
141    pub from: ArtifactName,
142    pub to: ArtifactName,
143    pub plan: Plan,
144}
145
146/// The scan of one worktree.
147#[derive(Debug, Clone)]
148pub struct Current {
149    pub scanned_at: OffsetDateTime,
150    pub fingerprint: Fingerprint,
151    /// Every Artifact, eval criterion and labeled relation as text, shared by a scan's steps.
152    pub structure: Arc<str>,
153    /// In name order.
154    pub artifacts: Vec<CurrentArtifact>,
155    pub edges: Vec<CurrentEdge>,
156}
157
158/// Scan the workspace at `repo` as it is now against the state at `state`, as `artifactize
159/// status` does: it hashes files and may run fingerprint scripts or ask the review store, and
160/// records nothing. `publish` gets the graph as soon as the declarations are read, every
161/// Artifact still being checked, then again each time fingerprints come in.
162pub async fn scan(
163    repo: &Path,
164    state: &Path,
165    cancellation: CancellationToken,
166    publish: impl FnMut(Current) + Send,
167) -> Result<Current, String> {
168    let current = read(repo, state, cancellation, publish).await;
169    // What the scan read is several times what it keeps: give it back, or a long-lived
170    // monitor keeps the largest scan's memory after the scan is gone.
171    crate::platform::release_free_memory();
172    current
173}
174
175async fn read(
176    repo: &Path,
177    state: &Path,
178    cancellation: CancellationToken,
179    mut publish: impl FnMut(Current) + Send,
180) -> Result<Current, String> {
181    // One read of the declarations serves both: the structure depends on them alone.
182    let config = crate::config::read_workspace_config(repo).map_err(|error| error.to_string())?;
183    let structure = graph_structure::extract(&config);
184    // Every step of the scan shows the same declarations: describe them once.
185    let declared = Declared {
186        tree: structure.tree().into(),
187        fingerprint: structure.fingerprint(),
188        structure: &structure,
189    };
190    let (status, cached) = crate::project::scan(
191        config,
192        Some(state),
193        &Selection::All,
194        &VerifyOptions::default(),
195        cancellation,
196        Some(&mut |progress: crate::project::Progress<'_>| {
197            publish(project(
198                &declared,
199                &progress.view,
200                progress.cached,
201                &progress.checking,
202                OffsetDateTime::now_utc(),
203            ));
204        }),
205    )
206    .await?;
207    Ok(project(
208        &declared,
209        &status,
210        &cached,
211        &BTreeSet::new(),
212        OffsetDateTime::now_utc(),
213    ))
214}
215
216/// What running `profile` costs as Execution options name it.
217fn profile(profile: &Profile) -> String {
218    match profile {
219        Profile::Agent { backend, model, .. } => format!("agent · {} · {model}", backend.as_str()),
220        other => other.kind().name().to_owned(),
221    }
222}
223
224fn card(
225    instruction: &str,
226    profile: &Profile,
227    plan: Plan,
228    reused: Option<&Execution>,
229    now: OffsetDateTime,
230) -> Card {
231    let result = reused.and_then(Execution::result);
232    Card {
233        instruction: instruction.to_owned(),
234        result: result.map(model::entries),
235        exit_code: match result {
236            Some(ExecutionResult::Runtime(result)) => result.exit_code.value().copied(),
237            _ => None,
238        },
239        error: None,
240        reused: plan == Plan::Reuse,
241        took: reused
242            .and_then(|execution| model::span(execution.started_at, execution.completed_at(), now)),
243        tokens: reused
244            .and_then(|execution| execution.usage.as_deref())
245            .and_then(model::attempt_tokens),
246        model: match profile {
247            Profile::Agent { model, .. } => Some(model.to_string()),
248            _ => None,
249        },
250    }
251}
252
253/// The declarations a scan read, as every one of its steps shows them.
254struct Declared<'s> {
255    structure: &'s Structure,
256    tree: Arc<str>,
257    fingerprint: Fingerprint,
258}
259
260/// The scan of `declared` as `status` found it, with the claims it found, the evals still
261/// being `checking` marked so.
262fn project(
263    declared: &Declared<'_>,
264    status: &StatusView,
265    cached: &BTreeMap<EvalId, Claim>,
266    checking: &BTreeSet<&EvalId>,
267    now: OffsetDateTime,
268) -> Current {
269    let mut by_target: BTreeMap<&ArtifactName, Vec<&EvalState>> = BTreeMap::new();
270    for eval in &status.evals {
271        by_target.entry(&eval.target).or_default().push(eval);
272    }
273    let mut parts: BTreeMap<&ArtifactName, Vec<ArtifactName>> = BTreeMap::new();
274    for relation in &declared.structure.relations {
275        if relation.kind == graph_structure::Edge::Part {
276            parts
277                .entry(&relation.target)
278                .or_default()
279                .push(relation.source.clone());
280        }
281    }
282    let mut artifacts: Vec<CurrentArtifact> = status
283        .artifacts
284        .iter()
285        .map(|artifact| {
286            let evals: Vec<CurrentEval> =
287                by_target
288                    .get(&artifact.id)
289                    .into_iter()
290                    .flatten()
291                    .map(|eval| {
292                        let plan = if checking.contains(&eval.id) {
293                            Plan::Check
294                        } else {
295                            Plan::from(eval.action)
296                        };
297                        let reused = match cached.get(&eval.id) {
298                            Some(Claim::Reuse(execution)) => Some(&**execution),
299                            _ => None,
300                        };
301                        let instruction = declared
302                            .structure
303                            .evals
304                            .get(&eval.id)
305                            .and_then(|declared| declared.payload.as_ref())
306                            .map_or("", |payload| payload.instruction.as_str());
307                        let mut facts = vec![("Profile", profile(&eval.profile))];
308                        facts.extend(
309                            eval.fingerprint
310                                .as_ref()
311                                .map(|fingerprint| ("Fingerprint", fingerprint.to_string())),
312                        );
313                        facts.push(("Eval", eval.id.to_string()));
314                        facts.push(("Verify would", eval.action.to_string()));
315                        facts.push(("State", eval.state.to_string()));
316                        facts.extend(eval.key.as_ref().map(|key| ("Reuse key", key.to_string())));
317                        facts.push(("Definition hash", eval.eval_def_hash.to_string()));
318                        facts.extend(eval.last.as_ref().map(|last| {
319                            ("Last Run", format!("{} · {}", last.run_id, last.verdict))
320                        }));
321                        if let Some(changes) = &eval.changes {
322                            facts.push(("Changes", changes.summary.clone()));
323                            facts.extend(
324                                changes
325                                    .files
326                                    .as_ref()
327                                    .map(|files| ("Changed files", files.join("\n"))),
328                            );
329                            facts.extend(
330                                changes
331                                    .dependencies
332                                    .as_ref()
333                                    .map(|names| ("Changed dependencies", names.join("\n"))),
334                            );
335                        }
336                        CurrentEval {
337                            id: eval.id.clone(),
338                            name: crate::monitor::model::tree::local(&eval.id).to_owned(),
339                            title: eval.title.clone(),
340                            kind: eval.profile.kind(),
341                            plan,
342                            state: eval.state,
343                            reason: if plan == Plan::Check {
344                                CHECKING.into()
345                            } else {
346                                eval.reason.clone()
347                            },
348                            card: card(instruction, &eval.profile, plan, reused, now),
349                            facts,
350                        }
351                    })
352                    .collect();
353            CurrentArtifact {
354                name: artifact.id.clone(),
355                kind: artifact.kind,
356                plan: evals
357                    .iter()
358                    .map(|eval| eval.plan)
359                    .min()
360                    .unwrap_or(Plan::Reuse),
361                runs: evals.iter().filter(|eval| eval.plan == Plan::Run).count(),
362                path: artifact.path.clone(),
363                state: artifact.state,
364                reason: artifact.reason.clone(),
365                evals,
366                parts: parts.remove(&artifact.id).unwrap_or_default(),
367            }
368        })
369        .collect();
370    let mut plans: BTreeMap<ArtifactName, Plan> = artifacts
371        .iter()
372        .map(|artifact| (artifact.name.clone(), artifact.plan))
373        .collect();
374    // Without evals of its own, an Artifact does what the first of its parts in plan order
375    // does; parts are acyclic, so passes over the wholes settle.
376    let wholes: Vec<usize> = (0..artifacts.len())
377        .filter(|&at| artifacts[at].evals.is_empty() && !artifacts[at].parts.is_empty())
378        .collect();
379    let mut changed = true;
380    while changed {
381        changed = false;
382        for &at in &wholes {
383            let artifact = &mut artifacts[at];
384            let plan = artifact
385                .parts
386                .iter()
387                .filter_map(|part| plans.get(part).copied())
388                .min()
389                .unwrap_or(Plan::Reuse);
390            if plan != artifact.plan {
391                artifact.plan = plan;
392                plans.insert(artifact.name.clone(), plan);
393                changed = true;
394            }
395        }
396    }
397    let pairs: BTreeSet<(&ArtifactName, &ArtifactName)> = declared
398        .structure
399        .relations
400        .iter()
401        .filter(|relation| {
402            relation.source != relation.target && relation.kind != graph_structure::Edge::Part
403        })
404        .map(|relation| (&relation.source, &relation.target))
405        .collect();
406    let edges = pairs
407        .into_iter()
408        .filter_map(|(from, to)| {
409            Some(CurrentEdge {
410                plan: *plans.get(from)?,
411                from: from.clone(),
412                to: plans.contains_key(to).then(|| to.clone())?,
413            })
414        })
415        .collect();
416    Current {
417        scanned_at: now,
418        fingerprint: declared.fingerprint.clone(),
419        structure: declared.tree.clone(),
420        artifacts,
421        edges,
422    }
423}
424
425#[cfg(test)]
426mod tests;