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            (Plan::Run, runs) => format!("would run ({runs} evals)"),
83            (plan, _) => plan.word().into(),
84        }
85    }
86}
87
88impl From<VerifyAction> for Plan {
89    fn from(action: VerifyAction) -> Self {
90        match action {
91            VerifyAction::Execute => Self::Run,
92            VerifyAction::Blocked => Self::Blocked,
93            VerifyAction::Wait => Self::Wait,
94            VerifyAction::Reuse => Self::Reuse,
95        }
96    }
97}
98
99/// Why an eval is still being checked.
100const CHECKING: &str = "Still being checked: a fingerprint its key needs, or the plan of an eval it waits for, is not known yet.";
101
102/// One eval of the scan.
103#[derive(Debug, Clone)]
104pub struct CurrentEval {
105    pub id: EvalId,
106    /// Its id within its Artifact, as a Run's tree names it.
107    pub name: String,
108    pub title: String,
109    pub kind: ProfileKind,
110    pub plan: Plan,
111    pub state: EvalCondition,
112    /// Why it would run, wait, be blocked or be reused.
113    pub reason: String,
114    /// Its instruction and the result it would reuse, as an eval's Summary shows them.
115    pub card: Card,
116    /// Every other fact by name: its profile, fingerprint, reuse key and what changed.
117    pub facts: Vec<(&'static str, String)>,
118}
119
120/// One Artifact of the scan.
121#[derive(Debug, Clone)]
122pub struct CurrentArtifact {
123    pub name: ArtifactName,
124    pub kind: ArtifactKind,
125    pub plan: Plan,
126    /// How many of its evals would run.
127    pub runs: usize,
128    pub path: PathBuf,
129    pub state: ArtifactCondition,
130    pub reason: String,
131    pub evals: Vec<CurrentEval>,
132}
133
134/// `to`'s evals use `from`, lit by `from`'s plan.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct CurrentEdge {
137    pub from: ArtifactName,
138    pub to: ArtifactName,
139    pub plan: Plan,
140}
141
142/// The scan of one worktree.
143#[derive(Debug, Clone)]
144pub struct Current {
145    pub scanned_at: OffsetDateTime,
146    pub fingerprint: Fingerprint,
147    /// Every Artifact, eval criterion and labeled relation as text, shared by a scan's steps.
148    pub structure: Arc<str>,
149    /// In name order.
150    pub artifacts: Vec<CurrentArtifact>,
151    pub edges: Vec<CurrentEdge>,
152}
153
154/// Scan the workspace at `repo` as it is now against the state at `state`, as `artifactize
155/// status` does: it hashes files and may run fingerprint scripts or ask the review store, and
156/// records nothing. `publish` gets the graph as soon as the declarations are read, every
157/// Artifact still being checked, then again each time fingerprints come in.
158pub async fn scan(
159    repo: &Path,
160    state: &Path,
161    cancellation: CancellationToken,
162    publish: impl FnMut(Current) + Send,
163) -> Result<Current, String> {
164    let current = read(repo, state, cancellation, publish).await;
165    // What the scan read is several times what it keeps: give it back, or a long-lived
166    // monitor keeps the largest scan's memory after the scan is gone.
167    crate::platform::release_free_memory();
168    current
169}
170
171async fn read(
172    repo: &Path,
173    state: &Path,
174    cancellation: CancellationToken,
175    mut publish: impl FnMut(Current) + Send,
176) -> Result<Current, String> {
177    // One read of the declarations serves both: the structure depends on them alone.
178    let config = crate::config::read_workspace_config(repo).map_err(|error| error.to_string())?;
179    let structure = graph_structure::extract(&config);
180    // Every step of the scan shows the same declarations: describe them once.
181    let declared = Declared {
182        tree: structure.tree().into(),
183        fingerprint: structure.fingerprint(),
184        structure: &structure,
185    };
186    let (status, cached) = crate::project::scan(
187        config,
188        Some(state),
189        &Selection::All,
190        &VerifyOptions::default(),
191        cancellation,
192        Some(&mut |progress: crate::project::Progress<'_>| {
193            publish(project(
194                &declared,
195                &progress.view,
196                progress.cached,
197                &progress.checking,
198                OffsetDateTime::now_utc(),
199            ));
200        }),
201    )
202    .await?;
203    Ok(project(
204        &declared,
205        &status,
206        &cached,
207        &BTreeSet::new(),
208        OffsetDateTime::now_utc(),
209    ))
210}
211
212/// What running `profile` costs as Execution options name it.
213fn profile(profile: &Profile) -> String {
214    match profile {
215        Profile::Agent { backend, model, .. } => format!("agent · {} · {model}", backend.as_str()),
216        other => other.kind().name().to_owned(),
217    }
218}
219
220fn card(
221    instruction: &str,
222    profile: &Profile,
223    plan: Plan,
224    reused: Option<&Execution>,
225    now: OffsetDateTime,
226) -> Card {
227    let result = reused.and_then(Execution::result);
228    Card {
229        instruction: instruction.to_owned(),
230        result: result.map(model::entries),
231        exit_code: match result {
232            Some(ExecutionResult::Runtime(result)) => result.exit_code.value().copied(),
233            _ => None,
234        },
235        error: None,
236        reused: plan == Plan::Reuse,
237        took: reused
238            .and_then(|execution| model::span(execution.started_at, execution.completed_at(), now)),
239        tokens: reused
240            .and_then(|execution| execution.usage.as_deref())
241            .and_then(model::attempt_tokens),
242        model: match profile {
243            Profile::Agent { model, .. } => Some(model.to_string()),
244            _ => None,
245        },
246    }
247}
248
249/// The declarations a scan read, as every one of its steps shows them.
250struct Declared<'s> {
251    structure: &'s Structure,
252    tree: Arc<str>,
253    fingerprint: Fingerprint,
254}
255
256/// The scan of `declared` as `status` found it, with the claims it found, the evals still
257/// being `checking` marked so.
258fn project(
259    declared: &Declared<'_>,
260    status: &StatusView,
261    cached: &BTreeMap<EvalId, Claim>,
262    checking: &BTreeSet<&EvalId>,
263    now: OffsetDateTime,
264) -> Current {
265    let mut by_target: BTreeMap<&ArtifactName, Vec<&EvalState>> = BTreeMap::new();
266    for eval in &status.evals {
267        by_target.entry(&eval.target).or_default().push(eval);
268    }
269    let artifacts: Vec<CurrentArtifact> = status
270        .artifacts
271        .iter()
272        .map(|artifact| {
273            let evals: Vec<CurrentEval> =
274                by_target
275                    .get(&artifact.id)
276                    .into_iter()
277                    .flatten()
278                    .map(|eval| {
279                        let plan = if checking.contains(&eval.id) {
280                            Plan::Check
281                        } else {
282                            Plan::from(eval.action)
283                        };
284                        let reused = match cached.get(&eval.id) {
285                            Some(Claim::Reuse(execution)) => Some(&**execution),
286                            _ => None,
287                        };
288                        let instruction = declared
289                            .structure
290                            .evals
291                            .get(&eval.id)
292                            .and_then(|declared| declared.payload.as_ref())
293                            .map_or("", |payload| payload.instruction.as_str());
294                        let mut facts = vec![("Profile", profile(&eval.profile))];
295                        facts.extend(
296                            eval.fingerprint
297                                .as_ref()
298                                .map(|fingerprint| ("Fingerprint", fingerprint.to_string())),
299                        );
300                        facts.push(("Eval", eval.id.to_string()));
301                        facts.push(("Verify would", eval.action.to_string()));
302                        facts.push(("State", eval.state.to_string()));
303                        facts.extend(eval.key.as_ref().map(|key| ("Reuse key", key.to_string())));
304                        facts.push(("Definition hash", eval.eval_def_hash.to_string()));
305                        facts.extend(eval.last.as_ref().map(|last| {
306                            ("Last Run", format!("{} · {}", last.run_id, last.verdict))
307                        }));
308                        if let Some(changes) = &eval.changes {
309                            facts.push(("Changes", changes.summary.clone()));
310                            facts.extend(
311                                changes
312                                    .files
313                                    .as_ref()
314                                    .map(|files| ("Changed files", files.join("\n"))),
315                            );
316                            facts.extend(
317                                changes
318                                    .dependencies
319                                    .as_ref()
320                                    .map(|names| ("Changed dependencies", names.join("\n"))),
321                            );
322                        }
323                        CurrentEval {
324                            id: eval.id.clone(),
325                            name: crate::monitor::model::tree::local(&eval.id).to_owned(),
326                            title: eval.title.clone(),
327                            kind: eval.profile.kind(),
328                            plan,
329                            state: eval.state,
330                            reason: if plan == Plan::Check {
331                                CHECKING.into()
332                            } else {
333                                eval.reason.clone()
334                            },
335                            card: card(instruction, &eval.profile, plan, reused, now),
336                            facts,
337                        }
338                    })
339                    .collect();
340            CurrentArtifact {
341                name: artifact.id.clone(),
342                kind: artifact.kind,
343                plan: evals
344                    .iter()
345                    .map(|eval| eval.plan)
346                    .min()
347                    .unwrap_or(Plan::Reuse),
348                runs: evals.iter().filter(|eval| eval.plan == Plan::Run).count(),
349                path: artifact.path.clone(),
350                state: artifact.state,
351                reason: artifact.reason.clone(),
352                evals,
353            }
354        })
355        .collect();
356    let plans: BTreeMap<&ArtifactName, Plan> = artifacts
357        .iter()
358        .map(|artifact| (&artifact.name, artifact.plan))
359        .collect();
360    let pairs: BTreeSet<(&ArtifactName, &ArtifactName)> = declared
361        .structure
362        .relations
363        .iter()
364        .filter(|relation| relation.source != relation.target)
365        .map(|relation| (&relation.source, &relation.target))
366        .collect();
367    let edges = pairs
368        .into_iter()
369        .filter_map(|(from, to)| {
370            Some(CurrentEdge {
371                plan: *plans.get(from)?,
372                from: from.clone(),
373                to: plans.contains_key(to).then(|| to.clone())?,
374            })
375        })
376        .collect();
377    Current {
378        scanned_at: now,
379        fingerprint: declared.fingerprint.clone(),
380        structure: declared.tree.clone(),
381        artifacts,
382        edges,
383    }
384}
385
386#[cfg(test)]
387mod tests;