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