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_with_policy(evidence, false)
257 }
258
259 pub fn evaluate_with_policy(
261 &self,
262 evidence: &BTreeMap<EvalId, Evidence>,
263 ignore_gates: bool,
264 ) -> Evaluation<'_> {
265 let mut evals: BTreeMap<EvalId, EvalEvaluation<'_>> = BTreeMap::new();
266 for component in &self.components {
267 let unmet_gates: Vec<_> = component
268 .gates
269 .iter()
270 .copied()
271 .filter(|id| !ignore_gates && evals[id.as_str()].status != EvalStatus::Green)
272 .collect();
273 let readiness = if unmet_gates.iter().any(|id| {
274 matches!(
275 evals[id.as_str()].status,
276 EvalStatus::Red | EvalStatus::Blocked
277 )
278 }) {
279 Readiness::Blocked
280 } else if unmet_gates.is_empty() {
281 Readiness::Ready
282 } else {
283 Readiness::Wait
284 };
285 let ordinary = component
286 .artifacts
287 .iter()
288 .flat_map(|id| &self.artifacts[id.as_str()].evals)
289 .copied()
290 .filter(|id| !self.derived.contains_key(id.as_str()));
291 let derived = self
292 .derived_order
293 .iter()
294 .copied()
295 .filter(|id| component.artifacts.contains(&self.evals[id.as_str()]));
296 for id in ordinary.chain(derived) {
297 if let Some(targets) = self.derived.get(id.as_str()) {
298 if evidence.get(id.as_str()) == Some(&Evidence::OperationalError) {
299 evals.insert(
300 id.clone(),
301 EvalEvaluation {
302 status: EvalStatus::Error,
303 readiness: Readiness::Ready,
304 evidence: Some(Evidence::OperationalError),
305 unmet_gates: Vec::new(),
306 blocked_by: Vec::new(),
307 derived: true,
308 },
309 );
310 continue;
311 }
312 let unmet: Vec<_> = targets
313 .iter()
314 .flat_map(|target| &self.artifacts[target.as_str()].evals)
315 .copied()
316 .filter(|id| evals[id.as_str()].status != EvalStatus::Green)
317 .collect();
318 let mut blocked_by = Vec::new();
319 for target in targets {
320 let pending: Vec<_> = self.artifacts[target.as_str()]
321 .evals
322 .iter()
323 .copied()
324 .filter(|id| evals[id.as_str()].status != EvalStatus::Green)
325 .collect();
326 if !pending.is_empty() {
327 blocked_by.push(BlockedReference::Artifact(target));
328 blocked_by.extend(pending.into_iter().map(BlockedReference::Eval));
329 }
330 }
331 let readiness = if unmet.iter().any(|id| {
332 matches!(
333 evals[id.as_str()].status,
334 EvalStatus::Red | EvalStatus::Blocked
335 )
336 }) {
337 Readiness::Blocked
338 } else if unmet.is_empty() {
339 Readiness::Ready
340 } else {
341 Readiness::Wait
342 };
343 evals.insert(
344 id.clone(),
345 EvalEvaluation {
346 status: match readiness {
347 Readiness::Ready => EvalStatus::Green,
348 Readiness::Blocked => EvalStatus::Blocked,
349 Readiness::Wait => EvalStatus::Wait,
350 },
351 readiness,
352 evidence: None,
353 unmet_gates: unmet,
354 blocked_by,
355 derived: true,
356 },
357 );
358 continue;
359 }
360 let evidence = evidence.get(id.as_str()).copied();
361 let status = match readiness {
362 Readiness::Blocked => EvalStatus::Blocked,
363 Readiness::Wait => EvalStatus::Wait,
364 Readiness::Ready => match evidence {
365 Some(Evidence::Current(Verdict::Green)) => EvalStatus::Green,
366 Some(Evidence::Current(Verdict::Red)) => EvalStatus::Red,
367 Some(Evidence::OperationalError) => EvalStatus::Error,
368 Some(Evidence::Stale) => EvalStatus::Stale,
369 None => EvalStatus::Unreviewed,
370 },
371 };
372 evals.insert(
373 id.clone(),
374 EvalEvaluation {
375 readiness,
376 status,
377 evidence,
378 unmet_gates: unmet_gates.clone(),
379 blocked_by: unmet_gates
380 .iter()
381 .copied()
382 .map(BlockedReference::Eval)
383 .collect(),
384 derived: false,
385 },
386 );
387 }
388 }
389 let own_satisfied: BTreeMap<_, _> = self
390 .artifacts
391 .iter()
392 .map(|(id, artifact)| {
393 let satisfied = artifact.basis
394 || (!artifact.evals.is_empty()
395 && artifact
396 .evals
397 .iter()
398 .all(|id| evals[id.as_str()].status == EvalStatus::Green));
399 (id, satisfied)
400 })
401 .collect();
402 let mut satisfied = vec![false; self.components.len()];
403 for (index, component) in self.components.iter().enumerate() {
404 satisfied[index] = component.artifacts.iter().all(|id| own_satisfied[id])
405 && component
406 .dependencies
407 .iter()
408 .all(|&dependency| satisfied[dependency.index()]);
409 }
410 let artifacts = self
411 .artifacts
412 .iter()
413 .map(|(id, artifact)| {
414 let satisfied = satisfied[artifact.component.index()];
415 let status = if satisfied {
416 if artifact.basis {
417 ArtifactStatus::Basis
418 } else {
419 ArtifactStatus::Green
420 }
421 } else {
422 [
423 (EvalStatus::Error, ArtifactStatus::Error),
424 (EvalStatus::Red, ArtifactStatus::Red),
425 (EvalStatus::Blocked, ArtifactStatus::Blocked),
426 (EvalStatus::Wait, ArtifactStatus::Wait),
427 (EvalStatus::Stale, ArtifactStatus::Stale),
428 (EvalStatus::Unreviewed, ArtifactStatus::Unreviewed),
429 ]
430 .into_iter()
431 .find_map(|(eval_status, artifact_status)| {
432 artifact
433 .evals
434 .iter()
435 .any(|id| evals[id.as_str()].status == eval_status)
436 .then_some(artifact_status)
437 })
438 .unwrap_or(if own_satisfied[id] {
439 ArtifactStatus::Incomplete
440 } else {
441 ArtifactStatus::Unreviewed
442 })
443 };
444 (
445 id.clone(),
446 ArtifactEvaluation {
447 status,
448 own_satisfied: own_satisfied[id],
449 satisfied,
450 passed: artifact
451 .evals
452 .iter()
453 .filter(|id| evals[id.as_str()].status == EvalStatus::Green)
454 .count(),
455 total: artifact.evals.len(),
456 },
457 )
458 })
459 .collect();
460 let obligations: Vec<_> = own_satisfied
461 .into_iter()
462 .filter_map(|(id, satisfied)| (!satisfied).then_some(id))
463 .collect();
464 let status = if evals.values().any(|c| c.status == EvalStatus::Error) {
465 FinalStatus::Error
466 } else if evals.values().any(|c| c.status == EvalStatus::Red) {
467 FinalStatus::Red
468 } else if obligations.is_empty() {
469 FinalStatus::Green
470 } else {
471 FinalStatus::Incomplete
472 };
473 Evaluation {
474 evals,
475 artifacts,
476 obligations,
477 status,
478 }
479 }
480
481 pub fn eval_target(&self, id: &str) -> Option<&'a ArtifactName> {
482 self.evals.get(id).copied()
483 }
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub enum Evidence {
490 Current(Verdict),
491 OperationalError,
492 Stale,
493}
494
495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
496pub enum Readiness {
497 Ready,
498 Wait,
499 Blocked,
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503pub enum EvalStatus {
504 Green,
505 Red,
506 Error,
507 Stale,
508 Unreviewed,
509 Wait,
510 Blocked,
511}
512
513#[derive(Debug, Clone, PartialEq, Eq)]
514pub struct EvalEvaluation<'a> {
515 pub readiness: Readiness,
517 pub status: EvalStatus,
518 pub evidence: Option<Evidence>,
520 pub unmet_gates: Vec<&'a EvalId>,
521 pub blocked_by: Vec<BlockedReference<'a>>,
522 pub derived: bool,
523}
524
525#[derive(Debug, Clone, Copy, PartialEq, Eq)]
527pub enum BlockedReference<'a> {
528 Artifact(&'a ArtifactName),
529 Eval(&'a EvalId),
530}
531impl BlockedReference<'_> {
532 pub fn as_str(&self) -> &str {
533 match self {
534 Self::Artifact(id) => id.as_str(),
535 Self::Eval(id) => id.as_str(),
536 }
537 }
538}
539impl std::fmt::Display for BlockedReference<'_> {
540 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541 formatter.write_str(self.as_str())
542 }
543}
544
545impl EvalEvaluation<'_> {
546 pub fn can_execute(&self) -> bool {
548 !self.derived
549 && self.readiness == Readiness::Ready
550 && matches!(self.evidence, None | Some(Evidence::Stale))
551 }
552}
553
554#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555pub enum ArtifactStatus {
556 Basis,
557 Green,
558 Incomplete,
559 Error,
560 Red,
561 Blocked,
562 Wait,
563 Stale,
564 Unreviewed,
565}
566
567#[derive(Debug, Clone, PartialEq, Eq)]
568pub struct ArtifactEvaluation {
569 pub status: ArtifactStatus,
570 pub own_satisfied: bool,
571 pub satisfied: bool,
573 pub passed: usize,
574 pub total: usize,
575}
576
577#[derive(Debug, Clone, Copy, PartialEq, Eq)]
579pub enum FinalStatus {
580 Green,
581 Red,
582 Error,
583 Incomplete,
584}
585
586#[derive(Debug, Clone, PartialEq, Eq)]
587pub struct Evaluation<'a> {
588 pub evals: BTreeMap<EvalId, EvalEvaluation<'a>>,
589 pub artifacts: BTreeMap<ArtifactName, ArtifactEvaluation>,
590 pub obligations: Vec<&'a ArtifactName>,
592 pub status: FinalStatus,
593}
594
595#[cfg(test)]
596mod tests;
597
598impl serde::Serialize for BlockedReference<'_> {
599 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
600 serializer.serialize_str(self.as_str())
601 }
602}
603
604impl From<BlockedReference<'_>> for crate::store::Blocker {
605 fn from(reference: BlockedReference<'_>) -> Self {
606 match reference {
607 BlockedReference::Artifact(id) => Self::Artifact(id.clone()),
608 BlockedReference::Eval(id) => Self::Eval(id.clone()),
609 }
610 }
611}