1use std::collections::{BTreeMap, BTreeSet};
4
5use petgraph::{algo::kosaraju_scc, graph::DiGraph};
6use thiserror::Error;
7
8use crate::{
9 config::{ArtifactName, EvalId, RepoConfig},
10 runtime::Verdict,
11};
12
13#[derive(Debug, Error)]
14#[error("{0}")]
15pub struct GraphError(pub String);
16
17#[derive(
21 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
22)]
23#[serde(transparent)]
24pub struct ComponentId(usize);
25
26impl ComponentId {
27 pub(crate) fn from_position(index: usize) -> Self {
30 Self(index)
31 }
32
33 pub(crate) fn index(self) -> usize {
34 self.0
35 }
36}
37
38impl std::fmt::Display for ComponentId {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 self.0.fmt(f)
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ArtifactNode<'a> {
46 pub basis: bool,
47 pub evals: Vec<&'a EvalId>,
48 pub parts: Vec<&'a ArtifactName>,
50 pub component: ComponentId,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct Component<'a> {
55 pub artifacts: Vec<&'a ArtifactName>,
56 pub dependencies: Vec<ComponentId>,
58 pub gates: Vec<&'a EvalId>,
61}
62
63#[derive(Debug)]
64pub struct Graph<'a> {
65 artifacts: BTreeMap<ArtifactName, ArtifactNode<'a>>,
66 evals: BTreeMap<EvalId, &'a ArtifactName>,
67 components: Vec<Component<'a>>,
68 schedule: schedule::Schedule<'a>,
69}
70
71impl<'a> Graph<'a> {
72 pub fn new(config: &'a RepoConfig) -> Result<Self, GraphError> {
74 let mut graph = DiGraph::<&ArtifactName, ()>::new();
75 let nodes: BTreeMap<_, _> = config
76 .artifacts
77 .keys()
78 .map(|id| (id, graph.add_node(id)))
79 .collect();
80 let mut artifacts: BTreeMap<_, _> = config
81 .artifacts
82 .iter()
83 .map(|(id, artifact)| {
84 (
85 id.clone(),
86 ArtifactNode {
87 basis: artifact.basis == Some(true),
88 evals: Vec::new(),
89 parts: Vec::new(),
90 component: ComponentId(0),
91 },
92 )
93 })
94 .collect();
95 let mut evals = BTreeMap::new();
96 for eval in &config.evals {
97 let artifact = artifacts
98 .get_mut(&eval.target)
99 .ok_or_else(|| GraphError(format!("Unknown Eval target: {}", eval.target)))?;
100 if artifact.basis {
101 return Err(GraphError(format!(
102 "Basis Artifact {} cannot own Evals.",
103 eval.target
104 )));
105 }
106 if evals.insert(eval.id.clone(), &eval.target).is_some() {
107 return Err(GraphError(format!("Duplicate Eval: {}", eval.id)));
108 }
109 artifact.evals.push(&eval.id);
110 }
111 for artifact in artifacts.values_mut() {
112 artifact.evals.sort_unstable();
113 }
114 let mut edges = BTreeSet::new();
115 for relation in &config.relations {
116 for id in [&relation.source, &relation.target] {
117 if !nodes.contains_key(id) {
118 return Err(GraphError(format!("Unknown relation Artifact: {id}")));
119 }
120 }
121 edges.insert((&relation.target, &relation.source));
124 if relation.kind == crate::scope::RelationKind::Part {
125 artifacts
126 .get_mut(&relation.target)
127 .unwrap()
128 .parts
129 .push(&relation.source);
130 }
131 }
132 for &(consumer, dependency) in &edges {
134 graph.add_edge(nodes[consumer], nodes[dependency], ());
135 }
136 let mut components: Vec<_> = kosaraju_scc(&graph)
137 .into_iter()
138 .map(|members| {
139 let mut artifacts: Vec<_> = members.into_iter().map(|node| graph[node]).collect();
140 artifacts.sort_unstable();
141 Component {
142 artifacts,
143 dependencies: Vec::new(),
144 gates: Vec::new(),
145 }
146 })
147 .collect();
148 for (index, component) in components.iter().enumerate() {
149 for id in &component.artifacts {
150 artifacts.get_mut(id.as_str()).unwrap().component = ComponentId(index);
151 }
152 }
153 let mut dependencies = vec![BTreeSet::new(); components.len()];
154 for (consumer, dependency) in edges {
155 let consumer = artifacts[consumer.as_str()].component;
156 let dependency = artifacts[dependency.as_str()].component;
157 if consumer != dependency {
158 debug_assert!(dependency < consumer);
159 dependencies[consumer.index()].insert(dependency);
160 }
161 }
162 for (index, dependencies) in dependencies.into_iter().enumerate() {
163 let mut gates: Vec<_> = dependencies
164 .iter()
165 .flat_map(|&dependency| &components[dependency.index()].artifacts)
166 .flat_map(|id| &artifacts[id.as_str()].evals)
167 .copied()
168 .collect();
169 gates.sort_unstable();
170 components[index].dependencies = dependencies.into_iter().collect();
171 components[index].gates = gates;
172 }
173 let schedule = schedule::Schedule::new(config.evals.iter().map(|eval| schedule::Input {
174 id: &eval.id,
175 target: &eval.target,
176 deps: eval.deps.iter().collect(),
177 human: eval.declaration.profile().kind() == crate::config::ProfileKind::Human,
178 }));
179 Ok(Self {
180 artifacts,
181 evals,
182 components,
183 schedule,
184 })
185 }
186
187 pub fn artifacts(&self) -> &BTreeMap<ArtifactName, ArtifactNode<'a>> {
188 &self.artifacts
189 }
190
191 pub fn components(&self) -> &[Component<'a>] {
193 &self.components
194 }
195
196 pub fn dependency_closure(
198 &self,
199 roots: &[&ArtifactName],
200 ) -> Result<Vec<&'a ArtifactName>, GraphError> {
201 let mut pending = Vec::new();
202 for root in roots {
203 let artifact = self
204 .artifacts
205 .get(*root)
206 .ok_or_else(|| GraphError(format!("Unknown Artifact: {root}")))?;
207 pending.push(artifact.component);
208 }
209 let mut visited = BTreeSet::new();
210 let mut required = BTreeSet::new();
211 while let Some(index) = pending.pop() {
212 if !visited.insert(index) {
213 continue;
214 }
215 let component = &self.components[index.index()];
216 required.extend(component.artifacts.iter().copied());
217 pending.extend(&component.dependencies);
218 }
219 Ok(required.into_iter().collect())
220 }
221
222 pub fn cycle_members(
224 &self,
225 roots: &[&ArtifactName],
226 ) -> Result<Vec<&'a ArtifactName>, GraphError> {
227 let components = roots
228 .iter()
229 .map(|root| {
230 self.artifacts
231 .get(*root)
232 .map(|artifact| artifact.component)
233 .ok_or_else(|| GraphError(format!("Unknown Artifact: {root}")))
234 })
235 .collect::<Result<BTreeSet<_>, _>>()?;
236 Ok(components
237 .into_iter()
238 .flat_map(|component| self.components[component.index()].artifacts.iter().copied())
239 .collect::<BTreeSet<_>>()
240 .into_iter()
241 .collect())
242 }
243
244 pub fn evaluate(&self, evidence: &BTreeMap<EvalId, Evidence>) -> Evaluation<'_> {
247 self.evaluate_for_run(evidence, false)
248 }
249
250 pub(crate) fn evaluate_for_run(
253 &self,
254 evidence: &BTreeMap<EvalId, Evidence>,
255 errors_block: bool,
256 ) -> Evaluation<'_> {
257 let mut evals: BTreeMap<EvalId, EvalEvaluation<'_>> = BTreeMap::new();
258 for &id in &self.schedule.order {
259 let gates = &self.schedule.gates[id];
260 let unmet_gates: Vec<_> = gates
261 .iter()
262 .copied()
263 .filter(|id| !evals[id.as_str()].status.passes_gate())
264 .collect();
265 let target = self.evals[id.as_str()];
268 let causes: Vec<_> = unmet_gates
270 .iter()
271 .flat_map(|&gate| {
272 let eval = &evals[gate.as_str()];
273 let sibling = self.evals[gate.as_str()] == target;
274 match eval.status {
275 EvalStatus::Red if sibling => vec![Cause::SiblingRed(gate)],
276 EvalStatus::Red => vec![Cause::Red(gate)],
277 EvalStatus::Error if sibling => vec![Cause::SiblingError(gate)],
278 EvalStatus::Error if errors_block => vec![Cause::Error(gate)],
279 EvalStatus::Blocked => {
280 eval.causes.iter().map(|cause| cause.upstream()).collect()
281 }
282 _ => Vec::new(),
283 }
284 })
285 .collect::<BTreeSet<_>>()
286 .into_iter()
287 .collect();
288 let readiness = if !causes.is_empty() {
289 Readiness::Blocked
290 } else if unmet_gates.is_empty() {
291 Readiness::Ready
292 } else {
293 Readiness::Wait
294 };
295 let provisional = gates.iter().any(|id| {
296 matches!(
297 evals[id.as_str()].status,
298 EvalStatus::Deferred | EvalStatus::Provisional
299 )
300 });
301 let evidence = evidence.get(id.as_str()).copied();
302 let status = match readiness {
303 Readiness::Blocked => EvalStatus::Blocked,
304 Readiness::Wait => match evidence {
305 Some(Evidence::Current(Verdict::Red)) if errors_block => EvalStatus::Red,
306 Some(Evidence::OperationalError) if errors_block => EvalStatus::Error,
307 _ => EvalStatus::Wait,
308 },
309 Readiness::Ready => match evidence {
310 Some(Evidence::Deferred) => EvalStatus::Deferred,
311 Some(Evidence::Current(Verdict::Green)) if provisional => {
312 EvalStatus::Provisional
313 }
314 Some(Evidence::Current(Verdict::Green)) => EvalStatus::Green,
315 Some(Evidence::Current(Verdict::Red)) => EvalStatus::Red,
316 Some(Evidence::OperationalError) => EvalStatus::Error,
317 Some(Evidence::Stale) => EvalStatus::Stale,
318 None => EvalStatus::Unreviewed,
319 },
320 };
321 let blocked_by = if causes.is_empty() {
322 unmet_gates
323 .iter()
324 .copied()
325 .map(BlockedReference::Eval)
326 .collect()
327 } else {
328 causes
329 .iter()
330 .map(|cause| BlockedReference::Eval(cause.eval()))
331 .collect()
332 };
333 evals.insert(
334 id.clone(),
335 EvalEvaluation {
336 readiness,
337 status,
338 evidence,
339 unmet_gates,
340 blocked_by,
341 causes,
342 },
343 );
344 }
345 let own_satisfied: BTreeMap<_, _> = self
346 .artifacts
347 .iter()
348 .map(|(id, artifact)| {
349 let satisfied = artifact.basis
352 || (artifact.evals.is_empty() && !artifact.parts.is_empty())
353 || (!artifact.evals.is_empty()
354 && artifact
355 .evals
356 .iter()
357 .all(|id| evals[id.as_str()].status == EvalStatus::Green));
358 (id, satisfied)
359 })
360 .collect();
361 let mut satisfied = vec![false; self.components.len()];
362 for (index, component) in self.components.iter().enumerate() {
363 satisfied[index] = component.artifacts.iter().all(|id| own_satisfied[id])
364 && component
365 .dependencies
366 .iter()
367 .all(|&dependency| satisfied[dependency.index()]);
368 }
369 let mut order = Vec::with_capacity(self.artifacts.len());
371 let mut placed = BTreeSet::new();
372 for root in self.artifacts.keys() {
373 let mut pending = vec![(root, false)];
374 while let Some((id, expanded)) = pending.pop() {
375 if expanded {
376 order.push((id, &self.artifacts[id]));
377 } else if placed.insert(id) {
378 pending.push((id, true));
379 pending.extend(self.artifacts[id].parts.iter().map(|part| (*part, false)));
380 }
381 }
382 }
383 let mut artifacts: BTreeMap<ArtifactName, ArtifactEvaluation> = BTreeMap::new();
384 for (id, artifact) in order {
385 let satisfied = satisfied[artifact.component.index()];
386 let status = if satisfied {
387 if artifact.basis {
388 ArtifactStatus::Basis
389 } else {
390 ArtifactStatus::Green
391 }
392 } else {
393 [
394 (EvalStatus::Error, ArtifactStatus::Error),
395 (EvalStatus::Red, ArtifactStatus::Red),
396 (EvalStatus::Blocked, ArtifactStatus::Blocked),
397 (EvalStatus::Wait, ArtifactStatus::Wait),
398 (EvalStatus::Deferred, ArtifactStatus::Deferred),
399 (EvalStatus::Provisional, ArtifactStatus::Provisional),
400 (EvalStatus::Stale, ArtifactStatus::Stale),
401 (EvalStatus::Unreviewed, ArtifactStatus::Unreviewed),
402 ]
403 .into_iter()
404 .find_map(|(eval_status, artifact_status)| {
405 (artifact
406 .evals
407 .iter()
408 .any(|id| evals[id.as_str()].status == eval_status)
409 || artifact
410 .parts
411 .iter()
412 .any(|id| artifacts[id.as_str()].status == artifact_status))
413 .then_some(artifact_status)
414 })
415 .unwrap_or(if own_satisfied[id] {
416 ArtifactStatus::Incomplete
417 } else {
418 ArtifactStatus::Unreviewed
419 })
420 };
421 artifacts.insert(
422 id.clone(),
423 ArtifactEvaluation {
424 status,
425 own_satisfied: own_satisfied[id],
426 satisfied,
427 passed: artifact
428 .evals
429 .iter()
430 .filter(|id| evals[id.as_str()].status == EvalStatus::Green)
431 .count(),
432 total: artifact.evals.len(),
433 },
434 );
435 }
436 let obligations: Vec<_> = own_satisfied
437 .into_iter()
438 .filter_map(|(id, satisfied)| (!satisfied).then_some(id))
439 .collect();
440 let status = if evals.values().any(|c| c.status == EvalStatus::Error) {
441 FinalStatus::Error
442 } else if evals.values().any(|c| c.status == EvalStatus::Red) {
443 FinalStatus::Red
444 } else if obligations.is_empty() {
445 FinalStatus::Green
446 } else {
447 FinalStatus::Incomplete
448 };
449 Evaluation {
450 evals,
451 artifacts,
452 obligations,
453 status,
454 }
455 }
456
457 pub(crate) fn gates(&self, id: &EvalId) -> Vec<&'a EvalId> {
458 self.schedule.gates[id].clone()
459 }
460
461 pub fn eval_target(&self, id: &str) -> Option<&'a ArtifactName> {
462 self.evals.get(id).copied()
463 }
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
469pub enum Evidence {
470 Current(Verdict),
471 Deferred,
472 OperationalError,
473 Stale,
474}
475
476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
477pub enum Readiness {
478 Ready,
479 Wait,
480 Blocked,
481}
482
483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
484pub enum EvalStatus {
485 Deferred,
486 Provisional,
487 Green,
488 Red,
489 Error,
490 Stale,
491 Unreviewed,
492 Wait,
493 Blocked,
494}
495
496#[derive(Debug, Clone, PartialEq, Eq)]
497pub struct EvalEvaluation<'a> {
498 pub readiness: Readiness,
500 pub status: EvalStatus,
501 pub evidence: Option<Evidence>,
503 pub unmet_gates: Vec<&'a EvalId>,
504 pub blocked_by: Vec<BlockedReference<'a>>,
506 pub causes: Vec<Cause<'a>>,
509}
510
511#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
515pub enum Cause<'a> {
516 Red(&'a EvalId),
517 Error(&'a EvalId),
518 SiblingRed(&'a EvalId),
519 SiblingError(&'a EvalId),
520}
521
522impl<'a> Cause<'a> {
523 pub fn eval(self) -> &'a EvalId {
524 match self {
525 Self::Red(id) | Self::Error(id) | Self::SiblingRed(id) | Self::SiblingError(id) => id,
526 }
527 }
528
529 fn upstream(self) -> Self {
531 match self {
532 Self::SiblingRed(id) => Self::Red(id),
533 Self::SiblingError(id) => Self::Error(id),
534 cause => cause,
535 }
536 }
537}
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq)]
541pub enum BlockedReference<'a> {
542 Artifact(&'a ArtifactName),
543 Eval(&'a EvalId),
544}
545impl BlockedReference<'_> {
546 pub fn as_str(&self) -> &str {
547 match self {
548 Self::Artifact(id) => id.as_str(),
549 Self::Eval(id) => id.as_str(),
550 }
551 }
552}
553impl std::fmt::Display for BlockedReference<'_> {
554 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555 formatter.write_str(self.as_str())
556 }
557}
558
559impl EvalStatus {
560 pub fn passes_gate(self) -> bool {
561 matches!(self, Self::Green | Self::Deferred | Self::Provisional)
562 }
563}
564
565impl EvalEvaluation<'_> {
566 pub fn can_execute(&self) -> bool {
568 self.readiness == Readiness::Ready && matches!(self.evidence, None | Some(Evidence::Stale))
569 }
570
571 pub fn blocked_reason(&self) -> Option<String> {
574 let names = |kind: fn(&Cause<'_>) -> bool| {
575 self.causes
576 .iter()
577 .filter(|cause| kind(cause))
578 .map(|cause| cause.eval().as_str())
579 .collect::<Vec<_>>()
580 .join(", ")
581 };
582 let parts: Vec<_> = [
583 (
584 "Sibling machine eval RED",
585 names(|cause| matches!(cause, Cause::SiblingRed(_))),
586 ),
587 (
588 "Sibling machine eval ERROR",
589 names(|cause| matches!(cause, Cause::SiblingError(_))),
590 ),
591 (
592 "Dependency verdict RED",
593 names(|cause| matches!(cause, Cause::Red(_))),
594 ),
595 (
596 "Dependency ERROR",
597 names(|cause| matches!(cause, Cause::Error(_))),
598 ),
599 ]
600 .into_iter()
601 .filter(|(_, names)| !names.is_empty())
602 .map(|(label, names)| format!("{label}: {names}"))
603 .collect();
604 (!parts.is_empty()).then(|| parts.join("; "))
605 }
606}
607
608#[derive(Debug, Clone, Copy, PartialEq, Eq)]
609pub enum ArtifactStatus {
610 Deferred,
611 Provisional,
612 Basis,
613 Green,
614 Incomplete,
615 Error,
616 Red,
617 Blocked,
618 Wait,
619 Stale,
620 Unreviewed,
621}
622
623impl From<ArtifactStatus> for crate::types::ArtifactValidationStatus {
624 fn from(status: ArtifactStatus) -> Self {
625 match status {
626 ArtifactStatus::Deferred => Self::Deferred,
627 ArtifactStatus::Provisional => Self::Provisional,
628 ArtifactStatus::Basis => Self::Basis,
629 ArtifactStatus::Green => Self::Green,
630 ArtifactStatus::Incomplete => Self::Incomplete,
631 ArtifactStatus::Error => Self::Error,
632 ArtifactStatus::Red => Self::Red,
633 ArtifactStatus::Blocked => Self::Blocked,
634 ArtifactStatus::Wait => Self::Wait,
635 ArtifactStatus::Stale => Self::Stale,
636 ArtifactStatus::Unreviewed => Self::Unreviewed,
637 }
638 }
639}
640
641#[derive(Debug, Clone, PartialEq, Eq)]
642pub struct ArtifactEvaluation {
643 pub status: ArtifactStatus,
644 pub own_satisfied: bool,
645 pub satisfied: bool,
647 pub passed: usize,
648 pub total: usize,
649}
650
651#[derive(Debug, Clone, Copy, PartialEq, Eq)]
653pub enum FinalStatus {
654 Green,
655 Red,
656 Error,
657 Incomplete,
658}
659
660#[derive(Debug, Clone, PartialEq, Eq)]
661pub struct Evaluation<'a> {
662 pub evals: BTreeMap<EvalId, EvalEvaluation<'a>>,
663 pub artifacts: BTreeMap<ArtifactName, ArtifactEvaluation>,
664 pub obligations: Vec<&'a ArtifactName>,
666 pub status: FinalStatus,
667}
668
669#[cfg(test)]
670mod tests;
671
672impl serde::Serialize for BlockedReference<'_> {
673 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
674 serializer.serialize_str(self.as_str())
675 }
676}
677
678impl From<BlockedReference<'_>> for crate::store::Blocker {
679 fn from(reference: BlockedReference<'_>) -> Self {
680 match reference {
681 BlockedReference::Artifact(id) => Self::Artifact(id.clone()),
682 BlockedReference::Eval(id) => Self::Eval(id.clone()),
683 }
684 }
685}
686
687pub(crate) mod schedule;