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