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 evaluate(&self, evidence: &BTreeMap<EvalId, Evidence>) -> Evaluation<'_> {
218 self.evaluate_for_run(evidence, false)
219 }
220
221 pub(crate) fn evaluate_for_run(
224 &self,
225 evidence: &BTreeMap<EvalId, Evidence>,
226 errors_block: bool,
227 ) -> Evaluation<'_> {
228 let mut evals: BTreeMap<EvalId, EvalEvaluation<'_>> = BTreeMap::new();
229 for &id in &self.schedule.order {
230 let gates = &self.schedule.gates[id];
231 let unmet_gates: Vec<_> = gates
232 .iter()
233 .copied()
234 .filter(|id| !evals[id.as_str()].status.passes_gate())
235 .collect();
236 let target = self.evals[id.as_str()];
239 let causes: Vec<_> = unmet_gates
241 .iter()
242 .flat_map(|&gate| {
243 let eval = &evals[gate.as_str()];
244 let sibling = self.evals[gate.as_str()] == target;
245 match eval.status {
246 EvalStatus::Red if sibling => vec![Cause::SiblingRed(gate)],
247 EvalStatus::Red => vec![Cause::Red(gate)],
248 EvalStatus::Error if sibling => vec![Cause::SiblingError(gate)],
249 EvalStatus::Error if errors_block => vec![Cause::Error(gate)],
250 EvalStatus::Blocked => {
251 eval.causes.iter().map(|cause| cause.upstream()).collect()
252 }
253 _ => Vec::new(),
254 }
255 })
256 .collect::<BTreeSet<_>>()
257 .into_iter()
258 .collect();
259 let readiness = if !causes.is_empty() {
260 Readiness::Blocked
261 } else if unmet_gates.is_empty() {
262 Readiness::Ready
263 } else {
264 Readiness::Wait
265 };
266 let provisional = gates.iter().any(|id| {
267 matches!(
268 evals[id.as_str()].status,
269 EvalStatus::Deferred | EvalStatus::Provisional
270 )
271 });
272 let evidence = evidence.get(id.as_str()).copied();
273 let status = match readiness {
274 Readiness::Blocked => EvalStatus::Blocked,
275 Readiness::Wait => match evidence {
276 Some(Evidence::Current(Verdict::Red)) if errors_block => EvalStatus::Red,
277 Some(Evidence::OperationalError) if errors_block => EvalStatus::Error,
278 _ => EvalStatus::Wait,
279 },
280 Readiness::Ready => match evidence {
281 Some(Evidence::Deferred) => EvalStatus::Deferred,
282 Some(Evidence::Current(Verdict::Green)) if provisional => {
283 EvalStatus::Provisional
284 }
285 Some(Evidence::Current(Verdict::Green)) => EvalStatus::Green,
286 Some(Evidence::Current(Verdict::Red)) => EvalStatus::Red,
287 Some(Evidence::OperationalError) => EvalStatus::Error,
288 Some(Evidence::Stale) => EvalStatus::Stale,
289 None => EvalStatus::Unreviewed,
290 },
291 };
292 let blocked_by = if causes.is_empty() {
293 unmet_gates
294 .iter()
295 .copied()
296 .map(BlockedReference::Eval)
297 .collect()
298 } else {
299 causes
300 .iter()
301 .map(|cause| BlockedReference::Eval(cause.eval()))
302 .collect()
303 };
304 evals.insert(
305 id.clone(),
306 EvalEvaluation {
307 readiness,
308 status,
309 evidence,
310 unmet_gates,
311 blocked_by,
312 causes,
313 },
314 );
315 }
316 let own_satisfied: BTreeMap<_, _> = self
317 .artifacts
318 .iter()
319 .map(|(id, artifact)| {
320 let satisfied = artifact.basis
321 || (!artifact.evals.is_empty()
322 && artifact
323 .evals
324 .iter()
325 .all(|id| evals[id.as_str()].status == EvalStatus::Green));
326 (id, satisfied)
327 })
328 .collect();
329 let mut satisfied = vec![false; self.components.len()];
330 for (index, component) in self.components.iter().enumerate() {
331 satisfied[index] = component.artifacts.iter().all(|id| own_satisfied[id])
332 && component
333 .dependencies
334 .iter()
335 .all(|&dependency| satisfied[dependency.index()]);
336 }
337 let artifacts = self
338 .artifacts
339 .iter()
340 .map(|(id, artifact)| {
341 let satisfied = satisfied[artifact.component.index()];
342 let status = if satisfied {
343 if artifact.basis {
344 ArtifactStatus::Basis
345 } else {
346 ArtifactStatus::Green
347 }
348 } else {
349 [
350 (EvalStatus::Error, ArtifactStatus::Error),
351 (EvalStatus::Red, ArtifactStatus::Red),
352 (EvalStatus::Blocked, ArtifactStatus::Blocked),
353 (EvalStatus::Wait, ArtifactStatus::Wait),
354 (EvalStatus::Deferred, ArtifactStatus::Deferred),
355 (EvalStatus::Provisional, ArtifactStatus::Provisional),
356 (EvalStatus::Stale, ArtifactStatus::Stale),
357 (EvalStatus::Unreviewed, ArtifactStatus::Unreviewed),
358 ]
359 .into_iter()
360 .find_map(|(eval_status, artifact_status)| {
361 artifact
362 .evals
363 .iter()
364 .any(|id| evals[id.as_str()].status == eval_status)
365 .then_some(artifact_status)
366 })
367 .unwrap_or(if own_satisfied[id] {
368 ArtifactStatus::Incomplete
369 } else {
370 ArtifactStatus::Unreviewed
371 })
372 };
373 (
374 id.clone(),
375 ArtifactEvaluation {
376 status,
377 own_satisfied: own_satisfied[id],
378 satisfied,
379 passed: artifact
380 .evals
381 .iter()
382 .filter(|id| evals[id.as_str()].status == EvalStatus::Green)
383 .count(),
384 total: artifact.evals.len(),
385 },
386 )
387 })
388 .collect();
389 let obligations: Vec<_> = own_satisfied
390 .into_iter()
391 .filter_map(|(id, satisfied)| (!satisfied).then_some(id))
392 .collect();
393 let status = if evals.values().any(|c| c.status == EvalStatus::Error) {
394 FinalStatus::Error
395 } else if evals.values().any(|c| c.status == EvalStatus::Red) {
396 FinalStatus::Red
397 } else if obligations.is_empty() {
398 FinalStatus::Green
399 } else {
400 FinalStatus::Incomplete
401 };
402 Evaluation {
403 evals,
404 artifacts,
405 obligations,
406 status,
407 }
408 }
409
410 pub(crate) fn gates(&self, id: &EvalId) -> Vec<&'a EvalId> {
411 self.schedule.gates[id].clone()
412 }
413
414 pub fn eval_target(&self, id: &str) -> Option<&'a ArtifactName> {
415 self.evals.get(id).copied()
416 }
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub enum Evidence {
423 Current(Verdict),
424 Deferred,
425 OperationalError,
426 Stale,
427}
428
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub enum Readiness {
431 Ready,
432 Wait,
433 Blocked,
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub enum EvalStatus {
438 Deferred,
439 Provisional,
440 Green,
441 Red,
442 Error,
443 Stale,
444 Unreviewed,
445 Wait,
446 Blocked,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq)]
450pub struct EvalEvaluation<'a> {
451 pub readiness: Readiness,
453 pub status: EvalStatus,
454 pub evidence: Option<Evidence>,
456 pub unmet_gates: Vec<&'a EvalId>,
457 pub blocked_by: Vec<BlockedReference<'a>>,
459 pub causes: Vec<Cause<'a>>,
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
468pub enum Cause<'a> {
469 Red(&'a EvalId),
470 Error(&'a EvalId),
471 SiblingRed(&'a EvalId),
472 SiblingError(&'a EvalId),
473}
474
475impl<'a> Cause<'a> {
476 pub fn eval(self) -> &'a EvalId {
477 match self {
478 Self::Red(id) | Self::Error(id) | Self::SiblingRed(id) | Self::SiblingError(id) => id,
479 }
480 }
481
482 fn upstream(self) -> Self {
484 match self {
485 Self::SiblingRed(id) => Self::Red(id),
486 Self::SiblingError(id) => Self::Error(id),
487 cause => cause,
488 }
489 }
490}
491
492#[derive(Debug, Clone, Copy, PartialEq, Eq)]
494pub enum BlockedReference<'a> {
495 Artifact(&'a ArtifactName),
496 Eval(&'a EvalId),
497}
498impl BlockedReference<'_> {
499 pub fn as_str(&self) -> &str {
500 match self {
501 Self::Artifact(id) => id.as_str(),
502 Self::Eval(id) => id.as_str(),
503 }
504 }
505}
506impl std::fmt::Display for BlockedReference<'_> {
507 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508 formatter.write_str(self.as_str())
509 }
510}
511
512impl EvalStatus {
513 pub fn passes_gate(self) -> bool {
514 matches!(self, Self::Green | Self::Deferred | Self::Provisional)
515 }
516}
517
518impl EvalEvaluation<'_> {
519 pub fn can_execute(&self) -> bool {
521 self.readiness == Readiness::Ready && matches!(self.evidence, None | Some(Evidence::Stale))
522 }
523
524 pub fn blocked_reason(&self) -> Option<String> {
527 let names = |kind: fn(&Cause<'_>) -> bool| {
528 self.causes
529 .iter()
530 .filter(|cause| kind(cause))
531 .map(|cause| cause.eval().as_str())
532 .collect::<Vec<_>>()
533 .join(", ")
534 };
535 let parts: Vec<_> = [
536 (
537 "Sibling machine eval RED",
538 names(|cause| matches!(cause, Cause::SiblingRed(_))),
539 ),
540 (
541 "Sibling machine eval ERROR",
542 names(|cause| matches!(cause, Cause::SiblingError(_))),
543 ),
544 (
545 "Dependency verdict RED",
546 names(|cause| matches!(cause, Cause::Red(_))),
547 ),
548 (
549 "Dependency ERROR",
550 names(|cause| matches!(cause, Cause::Error(_))),
551 ),
552 ]
553 .into_iter()
554 .filter(|(_, names)| !names.is_empty())
555 .map(|(label, names)| format!("{label}: {names}"))
556 .collect();
557 (!parts.is_empty()).then(|| parts.join("; "))
558 }
559}
560
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub enum ArtifactStatus {
563 Deferred,
564 Provisional,
565 Basis,
566 Green,
567 Incomplete,
568 Error,
569 Red,
570 Blocked,
571 Wait,
572 Stale,
573 Unreviewed,
574}
575
576impl From<ArtifactStatus> for crate::types::ArtifactValidationStatus {
577 fn from(status: ArtifactStatus) -> Self {
578 match status {
579 ArtifactStatus::Deferred => Self::Deferred,
580 ArtifactStatus::Provisional => Self::Provisional,
581 ArtifactStatus::Basis => Self::Basis,
582 ArtifactStatus::Green => Self::Green,
583 ArtifactStatus::Incomplete => Self::Incomplete,
584 ArtifactStatus::Error => Self::Error,
585 ArtifactStatus::Red => Self::Red,
586 ArtifactStatus::Blocked => Self::Blocked,
587 ArtifactStatus::Wait => Self::Wait,
588 ArtifactStatus::Stale => Self::Stale,
589 ArtifactStatus::Unreviewed => Self::Unreviewed,
590 }
591 }
592}
593
594#[derive(Debug, Clone, PartialEq, Eq)]
595pub struct ArtifactEvaluation {
596 pub status: ArtifactStatus,
597 pub own_satisfied: bool,
598 pub satisfied: bool,
600 pub passed: usize,
601 pub total: usize,
602}
603
604#[derive(Debug, Clone, Copy, PartialEq, Eq)]
606pub enum FinalStatus {
607 Green,
608 Red,
609 Error,
610 Incomplete,
611}
612
613#[derive(Debug, Clone, PartialEq, Eq)]
614pub struct Evaluation<'a> {
615 pub evals: BTreeMap<EvalId, EvalEvaluation<'a>>,
616 pub artifacts: BTreeMap<ArtifactName, ArtifactEvaluation>,
617 pub obligations: Vec<&'a ArtifactName>,
619 pub status: FinalStatus,
620}
621
622#[cfg(test)]
623mod tests;
624
625impl serde::Serialize for BlockedReference<'_> {
626 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
627 serializer.serialize_str(self.as_str())
628 }
629}
630
631impl From<BlockedReference<'_>> for crate::store::Blocker {
632 fn from(reference: BlockedReference<'_>) -> Self {
633 match reference {
634 BlockedReference::Artifact(id) => Self::Artifact(id.clone()),
635 BlockedReference::Eval(id) => Self::Eval(id.clone()),
636 }
637 }
638}
639
640pub(crate) mod schedule;