artifactize-core 0.21.0

The library behind artifactize: Artifacts, evals, reviews, the monitor and review models, and the shared operations every interface calls.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! A branch's worktree as it is now, scanned as `artifactize status` does: the graph of its
//! current declarations, each Artifact marked by what verify would do with it now.
use crate::{
    config::{ArtifactKind, Profile, ProfileKind},
    graph_structure::{self, Structure},
    monitor::model::{self, Card, Status, Tone},
    project::{
        ArtifactCondition, EvalCondition, EvalState, StatusView, VerifyAction, VerifyOptions,
        selection::Selection,
    },
    store::{Claim, Execution, ExecutionResult},
    types::{ArtifactName, EvalId, Fingerprint},
};
use std::{
    collections::{BTreeMap, BTreeSet},
    path::{Path, PathBuf},
    sync::Arc,
};
use time::OffsetDateTime;
use tokio_util::sync::CancellationToken;

/// What verify would do now. Declared in precedence order: an Artifact takes the first plan
/// any of its evals has (one to run before one still being checked before blocked before
/// waiting before reused).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Plan {
    /// Evaluated again: at least one eval has no result to reuse.
    Run,
    /// Not known yet: a fingerprint it needs, or the plan of an eval it waits for, is still
    /// being computed.
    Check,
    /// Cannot be evaluated: an upstream eval is RED.
    Blocked,
    /// Waits: for its dependencies' evidence, or a live execution.
    Wait,
    /// Every eval's result would be reused without evaluation (an Artifact without evals has
    /// nothing to evaluate).
    Reuse,
}

impl Plan {
    /// The plan as a Current names it: `reused`, `would run`, `checking`, `waiting` or
    /// `blocked`.
    pub fn word(self) -> &'static str {
        match self {
            Self::Run => "would run",
            Self::Check => "checking",
            Self::Blocked => "blocked",
            Self::Wait => "waiting",
            Self::Reuse => "reused",
        }
    }

    /// Its one-character mark: reused as a peer's reuse is marked, the others as the statuses
    /// they lead to.
    pub fn glyph(self) -> &'static str {
        match self {
            Self::Run => "○",
            Self::Check => "…",
            Self::Blocked => Status::Blocked.glyph(),
            Self::Wait => Status::Wait.glyph(),
            Self::Reuse => "↻",
        }
    }

    pub fn tone(self) -> Tone {
        match self {
            Self::Run => Tone::Running,
            Self::Check => Tone::Muted,
            Self::Blocked => Tone::Blocked,
            Self::Wait => Tone::Queued,
            Self::Reuse => Tone::Green,
        }
    }
}

impl CurrentArtifact {
    /// Its plan as its node and row read: `would run (2 evals)`, `reused`, ….
    pub fn text(&self) -> String {
        match (self.plan, self.runs) {
            (Plan::Run, 1) => "would run (1 eval)".into(),
            // Without evals of its own, what its parts would do.
            (plan, 0) => plan.word().into(),
            (Plan::Run, runs) => format!("would run ({runs} evals)"),
            (plan, _) => plan.word().into(),
        }
    }
}

impl From<VerifyAction> for Plan {
    fn from(action: VerifyAction) -> Self {
        match action {
            VerifyAction::Execute => Self::Run,
            VerifyAction::Blocked => Self::Blocked,
            VerifyAction::Wait => Self::Wait,
            VerifyAction::Reuse => Self::Reuse,
        }
    }
}

/// Why an eval is still being checked.
const CHECKING: &str = "Still being checked: a fingerprint its key needs, or the plan of an eval it waits for, is not known yet.";

/// One eval of the scan.
#[derive(Debug, Clone)]
pub struct CurrentEval {
    pub id: EvalId,
    /// Its id within its Artifact, as a Run's tree names it.
    pub name: String,
    pub title: String,
    pub kind: ProfileKind,
    pub plan: Plan,
    pub state: EvalCondition,
    /// Why it would run, wait, be blocked or be reused.
    pub reason: String,
    /// Its instruction and the result it would reuse, as an eval's Summary shows them.
    pub card: Card,
    /// Every other fact by name: its profile, fingerprint, reuse key and what changed.
    pub facts: Vec<(&'static str, String)>,
}

/// One Artifact of the scan.
#[derive(Debug, Clone)]
pub struct CurrentArtifact {
    pub name: ArtifactName,
    pub kind: ArtifactKind,
    pub plan: Plan,
    /// How many of its evals would run.
    pub runs: usize,
    pub path: PathBuf,
    pub state: ArtifactCondition,
    pub reason: String,
    pub evals: Vec<CurrentEval>,
    /// The Artifacts it is made of directly, in name order.
    pub parts: Vec<ArtifactName>,
}

/// `to`'s evals use `from`, lit by `from`'s plan; a part is no edge but drawn in its whole.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CurrentEdge {
    pub from: ArtifactName,
    pub to: ArtifactName,
    pub plan: Plan,
}

/// The scan of one worktree.
#[derive(Debug, Clone)]
pub struct Current {
    pub scanned_at: OffsetDateTime,
    pub fingerprint: Fingerprint,
    /// Every Artifact, eval criterion and labeled relation as text, shared by a scan's steps.
    pub structure: Arc<str>,
    /// In name order.
    pub artifacts: Vec<CurrentArtifact>,
    pub edges: Vec<CurrentEdge>,
}

/// Scan the workspace at `repo` as it is now against the state at `state`, as `artifactize
/// status` does: it hashes files and may run fingerprint scripts or ask the review store, and
/// records nothing. `publish` gets the graph as soon as the declarations are read, every
/// Artifact still being checked, then again each time fingerprints come in.
pub async fn scan(
    repo: &Path,
    state: &Path,
    cancellation: CancellationToken,
    publish: impl FnMut(Current) + Send,
) -> Result<Current, String> {
    let current = read(repo, state, cancellation, publish).await;
    // What the scan read is several times what it keeps: give it back, or a long-lived
    // monitor keeps the largest scan's memory after the scan is gone.
    crate::platform::release_free_memory();
    current
}

async fn read(
    repo: &Path,
    state: &Path,
    cancellation: CancellationToken,
    mut publish: impl FnMut(Current) + Send,
) -> Result<Current, String> {
    // One read of the declarations serves both: the structure depends on them alone.
    let config = crate::config::read_workspace_config(repo).map_err(|error| error.to_string())?;
    let structure = graph_structure::extract(&config);
    // Every step of the scan shows the same declarations: describe them once.
    let declared = Declared {
        tree: structure.tree().into(),
        fingerprint: structure.fingerprint(),
        structure: &structure,
    };
    let (status, cached) = crate::project::scan(
        config,
        Some(state),
        &Selection::All,
        &VerifyOptions::default(),
        cancellation,
        Some(&mut |progress: crate::project::Progress<'_>| {
            publish(project(
                &declared,
                &progress.view,
                progress.cached,
                &progress.checking,
                OffsetDateTime::now_utc(),
            ));
        }),
    )
    .await?;
    Ok(project(
        &declared,
        &status,
        &cached,
        &BTreeSet::new(),
        OffsetDateTime::now_utc(),
    ))
}

/// What running `profile` costs as Execution options name it.
fn profile(profile: &Profile) -> String {
    match profile {
        Profile::Agent { backend, model, .. } => format!("agent · {} · {model}", backend.as_str()),
        other => other.kind().name().to_owned(),
    }
}

fn card(
    instruction: &str,
    profile: &Profile,
    plan: Plan,
    reused: Option<&Execution>,
    now: OffsetDateTime,
) -> Card {
    let result = reused.and_then(Execution::result);
    Card {
        instruction: instruction.to_owned(),
        result: result.map(model::entries),
        exit_code: match result {
            Some(ExecutionResult::Runtime(result)) => result.exit_code.value().copied(),
            _ => None,
        },
        error: None,
        reused: plan == Plan::Reuse,
        took: reused
            .and_then(|execution| model::span(execution.started_at, execution.completed_at(), now)),
        tokens: reused
            .and_then(|execution| execution.usage.as_deref())
            .and_then(model::attempt_tokens),
        model: match profile {
            Profile::Agent { model, .. } => Some(model.to_string()),
            _ => None,
        },
    }
}

/// The declarations a scan read, as every one of its steps shows them.
struct Declared<'s> {
    structure: &'s Structure,
    tree: Arc<str>,
    fingerprint: Fingerprint,
}

/// The scan of `declared` as `status` found it, with the claims it found, the evals still
/// being `checking` marked so.
fn project(
    declared: &Declared<'_>,
    status: &StatusView,
    cached: &BTreeMap<EvalId, Claim>,
    checking: &BTreeSet<&EvalId>,
    now: OffsetDateTime,
) -> Current {
    let mut by_target: BTreeMap<&ArtifactName, Vec<&EvalState>> = BTreeMap::new();
    for eval in &status.evals {
        by_target.entry(&eval.target).or_default().push(eval);
    }
    let mut parts: BTreeMap<&ArtifactName, Vec<ArtifactName>> = BTreeMap::new();
    for relation in &declared.structure.relations {
        if relation.kind == graph_structure::Edge::Part {
            parts
                .entry(&relation.target)
                .or_default()
                .push(relation.source.clone());
        }
    }
    let mut artifacts: Vec<CurrentArtifact> = status
        .artifacts
        .iter()
        .map(|artifact| {
            let evals: Vec<CurrentEval> =
                by_target
                    .get(&artifact.id)
                    .into_iter()
                    .flatten()
                    .map(|eval| {
                        let plan = if checking.contains(&eval.id) {
                            Plan::Check
                        } else {
                            Plan::from(eval.action)
                        };
                        let reused = match cached.get(&eval.id) {
                            Some(Claim::Reuse(execution)) => Some(&**execution),
                            _ => None,
                        };
                        let instruction = declared
                            .structure
                            .evals
                            .get(&eval.id)
                            .and_then(|declared| declared.payload.as_ref())
                            .map_or("", |payload| payload.instruction.as_str());
                        let mut facts = vec![("Profile", profile(&eval.profile))];
                        facts.extend(
                            eval.fingerprint
                                .as_ref()
                                .map(|fingerprint| ("Fingerprint", fingerprint.to_string())),
                        );
                        facts.push(("Eval", eval.id.to_string()));
                        facts.push(("Verify would", eval.action.to_string()));
                        facts.push(("State", eval.state.to_string()));
                        facts.extend(eval.key.as_ref().map(|key| ("Reuse key", key.to_string())));
                        facts.push(("Definition hash", eval.eval_def_hash.to_string()));
                        facts.extend(eval.last.as_ref().map(|last| {
                            ("Last Run", format!("{} · {}", last.run_id, last.verdict))
                        }));
                        if let Some(changes) = &eval.changes {
                            facts.push(("Changes", changes.summary.clone()));
                            facts.extend(
                                changes
                                    .files
                                    .as_ref()
                                    .map(|files| ("Changed files", files.join("\n"))),
                            );
                            facts.extend(
                                changes
                                    .dependencies
                                    .as_ref()
                                    .map(|names| ("Changed dependencies", names.join("\n"))),
                            );
                        }
                        CurrentEval {
                            id: eval.id.clone(),
                            name: crate::monitor::model::tree::local(&eval.id).to_owned(),
                            title: eval.title.clone(),
                            kind: eval.profile.kind(),
                            plan,
                            state: eval.state,
                            reason: if plan == Plan::Check {
                                CHECKING.into()
                            } else {
                                eval.reason.clone()
                            },
                            card: card(instruction, &eval.profile, plan, reused, now),
                            facts,
                        }
                    })
                    .collect();
            CurrentArtifact {
                name: artifact.id.clone(),
                kind: artifact.kind,
                plan: evals
                    .iter()
                    .map(|eval| eval.plan)
                    .min()
                    .unwrap_or(Plan::Reuse),
                runs: evals.iter().filter(|eval| eval.plan == Plan::Run).count(),
                path: artifact.path.clone(),
                state: artifact.state,
                reason: artifact.reason.clone(),
                evals,
                parts: parts.remove(&artifact.id).unwrap_or_default(),
            }
        })
        .collect();
    let mut plans: BTreeMap<ArtifactName, Plan> = artifacts
        .iter()
        .map(|artifact| (artifact.name.clone(), artifact.plan))
        .collect();
    // Without evals of its own, an Artifact does what the first of its parts in plan order
    // does; parts are acyclic, so passes over the wholes settle.
    let wholes: Vec<usize> = (0..artifacts.len())
        .filter(|&at| artifacts[at].evals.is_empty() && !artifacts[at].parts.is_empty())
        .collect();
    let mut changed = true;
    while changed {
        changed = false;
        for &at in &wholes {
            let artifact = &mut artifacts[at];
            let plan = artifact
                .parts
                .iter()
                .filter_map(|part| plans.get(part).copied())
                .min()
                .unwrap_or(Plan::Reuse);
            if plan != artifact.plan {
                artifact.plan = plan;
                plans.insert(artifact.name.clone(), plan);
                changed = true;
            }
        }
    }
    let pairs: BTreeSet<(&ArtifactName, &ArtifactName)> = declared
        .structure
        .relations
        .iter()
        .filter(|relation| {
            relation.source != relation.target && relation.kind != graph_structure::Edge::Part
        })
        .map(|relation| (&relation.source, &relation.target))
        .collect();
    let edges = pairs
        .into_iter()
        .filter_map(|(from, to)| {
            Some(CurrentEdge {
                plan: *plans.get(from)?,
                from: from.clone(),
                to: plans.contains_key(to).then(|| to.clone())?,
            })
        })
        .collect();
    Current {
        scanned_at: now,
        fingerprint: declared.fingerprint.clone(),
        structure: declared.tree.clone(),
        artifacts,
        edges,
    }
}

#[cfg(test)]
mod tests;