1mod states;
2pub use states::{ArtifactCondition, EvalCondition, VerifyAction};
3#[cfg(test)]
4mod tests;
5
6use std::{
7 collections::{BTreeMap, BTreeSet},
8 path::Path,
9};
10
11use serde::Serialize;
12use tokio_util::sync::CancellationToken;
13
14use crate::{
15 cache,
16 config::{Profile, read_workspace_config},
17 graph::{Evidence, Graph, Readiness},
18 project::{VerifyOptions, selection::select_profiles},
19 remote::Session,
20 store::{self, Claim, LastRequest},
21};
22
23use super::selection::Selection;
24
25#[derive(Debug, Serialize)]
26#[serde(rename_all = "camelCase")]
27pub struct StatusView {
28 pub selection: Selection,
29 pub recursive: bool,
30 pub force: bool,
31 pub selected_eval_ids: Vec<crate::config::EvalId>,
32 pub included_eval_ids: Vec<crate::config::EvalId>,
33 pub satisfied: bool,
34 pub obligations: Vec<crate::config::ArtifactName>,
35 pub artifacts: Vec<ArtifactState>,
36 pub evals: Vec<EvalState>,
37 pub counts: Counts,
38 #[serde(skip_serializing_if = "Vec::is_empty")]
39 pub unreadable: Vec<store::Unreadable>,
40}
41
42#[derive(Debug, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct ArtifactState {
45 pub id: crate::config::ArtifactName,
46 pub kind: crate::config::ArtifactKind,
47 #[serde(with = "crate::platform::path_serde")]
48 pub path: std::path::PathBuf,
49 pub tags: Vec<String>,
50 pub state: ArtifactCondition,
51 pub reason: String,
52 pub eval_ids: Vec<crate::config::EvalId>,
53 pub passed: usize,
54 pub total: usize,
55 pub satisfied: bool,
56 pub obligations: Vec<crate::config::ArtifactName>,
57}
58
59#[derive(Debug, Serialize)]
60#[serde(rename_all = "camelCase")]
61pub struct EvalState {
62 pub id: crate::config::EvalId,
63 pub target: crate::config::ArtifactName,
64 pub title: String,
65 pub profile: Profile,
66 pub selected: bool,
67 pub included: bool,
68 pub force: bool,
69 pub state: EvalCondition,
70 pub action: VerifyAction,
71 pub reason: String,
72 pub blocked_by: Vec<crate::store::Blocker>,
73 pub obligations: Vec<crate::config::ArtifactName>,
74 pub eval_def_hash: crate::types::DefinitionHash,
76 pub fingerprint: Option<crate::types::Fingerprint>,
78 pub fingerprints: BTreeMap<crate::config::ArtifactName, Option<crate::types::Fingerprint>>,
80 pub key: Option<crate::types::ReuseKey>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub last: Option<LastRequest>,
84 #[serde(skip_serializing_if = "Option::is_none")]
87 pub changes: Option<cache::Changes>,
88}
89
90#[derive(Debug, Default, Serialize)]
91pub struct Counts {
92 pub artifacts: BTreeMap<ArtifactCondition, usize>,
93 pub evals: BTreeMap<EvalCondition, usize>,
94 pub execute: usize,
95 pub derive: usize,
96 pub reuse: usize,
97 pub wait: usize,
98 pub blocked: usize,
99}
100
101pub async fn status(
103 repo: &Path,
104 state_dir: Option<&Path>,
105 selection: &Selection,
106 options: &VerifyOptions,
107 cancellation: CancellationToken,
108) -> Result<StatusView, String> {
109 let parallelism = super::fingerprint_parallelism(options)?;
110 let config = read_workspace_config(repo).map_err(|error| error.to_string())?;
111 let config = select_profiles(
112 config,
113 selection,
114 options.profile.as_ref(),
115 options.recursive,
116 )?;
117 let graph = Graph::new(&config).map_err(|error| error.to_string())?;
118 let selected = selection.resolve(&config)?;
119 let required: BTreeSet<_> = graph
120 .dependency_closure(&selected.roots)
121 .map_err(|error| error.to_string())?
122 .into_iter()
123 .collect();
124 let selected_eval_ids: Vec<_> = selected.evals.iter().map(|eval| eval.id.clone()).collect();
125 let included_eval_ids: Vec<_> = selection
126 .included_evals(&config, options.recursive)?
127 .iter()
128 .map(|eval| eval.id.clone())
129 .collect();
130 let state = store::state_dir(state_dir)?;
131 let mut latest = store::read_latest_requests(&state, &config.root).await?;
132 let mut evidence: BTreeMap<_, _> = latest
133 .keys()
134 .map(|id| (id.clone(), Evidence::Stale))
135 .collect();
136 if options.force {
137 evidence.extend(
138 selected_eval_ids
139 .iter()
140 .map(|id| (id.clone(), Evidence::Stale)),
141 );
142 }
143 let selected_ids: BTreeSet<_> = selected_eval_ids.iter().collect();
144 let included_ids: BTreeSet<_> = included_eval_ids.iter().collect();
145 let fingerprints = cache::prepare(
146 &config,
147 cache::fingerprint_targets(&config, &required),
148 &state,
149 ¶llelism,
150 cancellation.clone(),
151 )
152 .await?;
153 let all_keys = cache::eval_keys(&config, &fingerprints);
154 let keys: BTreeMap<_, _> = all_keys
155 .iter()
156 .filter(|(id, _)| !(options.force && selected_ids.contains(*id)))
157 .map(|(id, key)| (*id, key))
158 .collect();
159 let mut cached = store::read_keyed_executions(
160 &state,
161 &keys
162 .values()
163 .map(|key| key.value.clone())
164 .collect::<BTreeSet<_>>()
165 .into_iter()
166 .collect::<Vec<_>>(),
167 )
168 .await?;
169 if !options.force
173 && let Some(remote) = Session::open(Some(&state), Some(&config.root))?
174 {
175 let all: Vec<_> = keys
176 .values()
177 .map(|key| key.value.clone())
178 .collect::<BTreeSet<_>>()
179 .into_iter()
180 .collect();
181 for execution in remote.lookup(&all).await? {
182 let key = execution.key.clone().expect("remote key");
183 let newer = match cached.get(&key) {
184 Some(Claim::Reuse(local)) => execution.completed_after(local),
185 _ => true,
186 };
187 if newer {
188 cached.insert(key, Claim::Reuse(Box::new(execution)));
189 }
190 }
191 }
192 let claim = |id: &crate::types::EvalId| keys.get(id).and_then(|key| cached.get(&key.value));
193 let stale: Vec<_> = keys
195 .iter()
196 .filter(|(id, _)| !matches!(claim(id), Some(Claim::Reuse(_))))
197 .map(|(id, key)| ((*id).clone(), key.eval_def_hash.clone()))
198 .collect();
199 let previous = store::read_latest_cached(&state, &stale).await?;
200 for eval in &config.evals {
201 if let Some(Claim::Reuse(execution)) = claim(&eval.id) {
202 evidence.insert(
203 eval.id.clone(),
204 Evidence::Current(execution.verdict().expect("completed cache entry")),
205 );
206 }
207 }
208 if cancellation.is_cancelled() {
209 return Err("Project preparation was cancelled.".into());
210 }
211 let evaluation = graph.evaluate(&evidence);
212 let deferred: BTreeSet<_> = store::read_waiting(&state, Some(&config.root))
213 .await?
214 .into_iter()
215 .filter(|view| view.request.status() == crate::types::RequestStatus::Deferred)
216 .filter(|view| {
217 matches!(
218 claim(&view.request.eval_id),
219 Some(Claim::WaitHuman(id)) if Some(id) == view.request.execution_id.as_ref()
220 )
221 })
222 .map(|view| view.request.eval_id)
223 .collect();
224 let mut shown_evidence = evidence.clone();
225 for id in &deferred {
226 shown_evidence.insert(id.clone(), Evidence::Deferred);
227 }
228 let shown = graph.evaluate(&shown_evidence);
229 let obligations: Vec<_> = evaluation
230 .obligations
231 .iter()
232 .filter(|id| required.contains(*id))
233 .map(|id| (*id).clone())
234 .collect();
235 let mut counts = Counts::default();
236 let mut artifacts = Vec::new();
237 for id in &required {
238 let artifact = &evaluation.artifacts[id.as_str()];
239 let status = ArtifactCondition::from(shown.artifacts[id.as_str()].status);
240 let own = &graph.artifacts()[id.as_str()];
241 let unmet = graph
242 .dependency_closure(&[*id])
243 .map_err(|error| error.to_string())?
244 .into_iter()
245 .filter(|id| !evaluation.artifacts[id.as_str()].own_satisfied)
246 .map(|id| (*id).clone())
247 .collect::<Vec<_>>();
248 let reason = if artifact.satisfied {
249 "All required obligations are satisfied.".into()
250 } else if own.evals.is_empty() && !own.basis {
251 "No Evals are declared and this Artifact is not a basis.".into()
252 } else {
253 format!(
254 "Unmet obligations: {}",
255 unmet
256 .iter()
257 .map(ToString::to_string)
258 .collect::<Vec<_>>()
259 .join(", ")
260 )
261 };
262 *counts.artifacts.entry(status).or_default() += 1;
263 artifacts.push(ArtifactState {
264 kind: config.artifacts[*id].kind,
265 path: config.artifacts[*id].path.clone(),
266 id: (*id).clone(),
267 tags: config.artifacts[*id].tags.clone(),
268 state: status,
269 reason,
270 eval_ids: own.evals.iter().map(|id| (*id).clone()).collect(),
271 passed: artifact.passed,
272 total: artifact.total,
273 satisfied: artifact.satisfied,
274 obligations: unmet,
275 });
276 }
277 let obligations_by_artifact: BTreeMap<_, _> = artifacts
278 .iter()
279 .map(|artifact| (artifact.id.as_str(), &artifact.obligations))
280 .collect();
281 let mut evals = Vec::new();
282 for eval in config
283 .evals
284 .iter()
285 .filter(|eval| required.contains(&eval.target))
286 {
287 let current = &evaluation.evals[eval.id.as_str()];
288 let selected = selected_ids.contains(&eval.id);
289 let included = included_ids.contains(&eval.id);
290 let derived = matches!(eval.declaration.profile(), Profile::Dependency { .. });
291 let force = options.force && selected && !derived;
292 let (action, reason) = match current.readiness {
293 _ if derived => (
294 VerifyAction::Derive,
295 if current.blocked_by.is_empty() {
296 "Derived from current GREEN dependency evidence; no execution or reuse.".into()
297 } else {
298 format!(
299 "Derived dependency verdict: waiting for current GREEN evidence from {}.",
300 current
301 .blocked_by
302 .iter()
303 .map(ToString::to_string)
304 .collect::<Vec<_>>()
305 .join(", ")
306 )
307 },
308 ),
309 _ if matches!(current.evidence, Some(Evidence::Current(_))) => (
312 VerifyAction::Reuse,
313 format!(
314 "The current eval and fingerprints have a completed {}{}{}",
315 match claim(&eval.id) {
316 Some(Claim::Reuse(execution)) if execution.origin.is_some() => format!(
317 "result in the remote review store, from {}",
318 execution
319 .producer
320 .as_ref()
321 .map_or("an unknown producer", |producer| producer.name.as_str())
322 ),
323 _ => "cached result".into(),
324 },
325 match claim(&eval.id) {
326 Some(Claim::Reuse(execution))
327 if execution.profile
328 != crate::config::StoredProfile::from(
329 eval.declaration.profile()
330 ) =>
331 format!(
332 " produced by profile {}",
333 crate::query::profile_name(&execution.profile, &execution.options)
334 ),
335 _ => String::new(),
336 },
337 match current.readiness {
338 Readiness::Ready => ".".into(),
339 Readiness::Wait => format!(
340 "; its gates still wait for: {}",
341 current
342 .unmet_gates
343 .iter()
344 .map(ToString::to_string)
345 .collect::<Vec<_>>()
346 .join(", ")
347 ),
348 Readiness::Blocked => format!(
349 "; its gates are blocked by RED: {}",
350 current
351 .unmet_gates
352 .iter()
353 .map(ToString::to_string)
354 .collect::<Vec<_>>()
355 .join(", ")
356 ),
357 }
358 ),
359 ),
360 Readiness::Blocked => (
361 VerifyAction::Blocked,
362 format!(
363 "Dependency verdict RED: {}",
364 current
365 .unmet_gates
366 .iter()
367 .map(ToString::to_string)
368 .collect::<Vec<_>>()
369 .join(", ")
370 ),
371 ),
372 Readiness::Wait => (
373 VerifyAction::Wait,
374 format!(
375 "Waiting for current GREEN dependency evidence: {}",
376 current
377 .unmet_gates
378 .iter()
379 .map(ToString::to_string)
380 .collect::<Vec<_>>()
381 .join(", ")
382 ),
383 ),
384 Readiness::Ready
385 if !force
386 && matches!(claim(&eval.id), Some(Claim::Wait(_) | Claim::WaitHuman(_))) =>
387 {
388 (
389 VerifyAction::Wait,
390 "The current eval and fingerprints have a live execution.".into(),
391 )
392 }
393 Readiness::Ready => match eval.declaration.profile() {
394 Profile::Dependency { .. } => unreachable!("derived above"),
395 Profile::Human { .. } => (
396 VerifyAction::Execute,
397 "Record a request awaiting a Human claim and submission.".into(),
398 ),
399 Profile::Runtime { .. } | Profile::Agent { .. } => (
400 VerifyAction::Execute,
401 if force {
402 "An explicitly forced Eval requires a new execution.".into()
403 } else {
404 match cache::eval_key(&config, eval, &fingerprints) {
405 Ok(_) => {
406 "The current eval and fingerprints have no completed cached result."
407 .into()
408 }
409 Err(cache::Unkeyed::Derived) => unreachable!("derived above"),
410 Err(cache::Unkeyed::Target) => {
411 "The Artifact declares fingerprint: false; saved noncached results satisfy only their own Run."
412 .into()
413 }
414 Err(cache::Unkeyed::Dependency(id)) => format!(
415 "Dependency {id} declares fingerprint: false, so this eval has no reuse key; saved noncached results satisfy only their own Run."
416 ),
417 }
418 },
419 ),
420 },
421 };
422 let status = if !force && matches!(claim(&eval.id), Some(Claim::WaitHuman(_))) {
423 if deferred.contains(&eval.id) {
424 EvalCondition::Deferred
425 } else {
426 EvalCondition::WaitingHuman
427 }
428 } else {
429 EvalCondition::from(shown.evals[eval.id.as_str()].status)
430 };
431 *counts.evals.entry(status).or_default() += 1;
432 if included {
433 counts.action(action);
434 }
435 evals.push(EvalState {
436 id: eval.id.clone(),
437 target: eval.target.clone(),
438 title: eval.declaration.title.clone(),
439 profile: eval.declaration.profile().clone(),
440 selected,
441 included,
442 force,
443 state: status,
444 action,
445 reason,
446 blocked_by: current.blocked_by.iter().copied().map(Into::into).collect(),
447 obligations: obligations_by_artifact[eval.target.as_str()].clone(),
448 eval_def_hash: cache::eval_definition_hash(&eval.declaration),
449 fingerprint: fingerprints
450 .get(&eval.target)
451 .map(|fingerprint| fingerprint.value.clone()),
452 fingerprints: cache::dependencies(&config, eval)
453 .into_iter()
454 .map(|id| {
455 (
456 config.artifacts[id].name.clone(),
457 fingerprints
458 .get(id)
459 .map(|fingerprint| fingerprint.value.clone()),
460 )
461 })
462 .collect(),
463 key: all_keys.get(&eval.id).map(|key| key.value.clone()),
464 last: latest.remove(eval.id.as_str()),
465 changes: keys.get(&eval.id).and_then(|key| {
466 previous
467 .get(&(eval.id.clone(), key.eval_def_hash.clone()))
468 .map(|execution| {
469 cache::changes(
470 execution,
471 &eval.target,
472 key,
473 fingerprints[&eval.target].manifest.as_ref(),
474 )
475 })
476 }),
477 });
478 }
479 let unreadable = store::read_unreadable(&state, Some(&config.root)).await?;
480 Ok(StatusView {
481 unreadable,
482 selection: selection.clone(),
483 recursive: options.recursive,
484 force: options.force,
485 selected_eval_ids,
486 included_eval_ids,
487 satisfied: obligations.is_empty(),
488 obligations,
489 artifacts,
490 evals,
491 counts,
492 })
493}
494
495impl Counts {
496 fn action(&mut self, action: VerifyAction) {
497 match action {
498 VerifyAction::Execute => self.execute += 1,
499 VerifyAction::Derive => self.derive += 1,
500 VerifyAction::Reuse => self.reuse += 1,
501 VerifyAction::Wait => self.wait += 1,
502 VerifyAction::Blocked => self.blocked += 1,
503 }
504 }
505}