Skip to main content

artifactize_core/
graph.rs

1//! Dependency edges, strongly connected components, gates, and obligations.
2
3use 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/// A strongly-connected component's position in `Graph::components`, guarding it from
18/// arbitrary `usize` arithmetic: dependency-first, so a lower id is never a later
19/// component's dependency.
20#[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    /// Live graph and legacy saved components derive their identity from a list position.
28    /// Saved explicit IDs deserialize as the same unsigned integer, never arithmetic indices.
29    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    /// The Artifacts it is directly composed of, by name.
49    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    /// Direct condensation dependencies, always earlier in the component list.
57    pub dependencies: Vec<ComponentId>,
58    /// Aggregate external obligations for the material projection, not execution gates.
59    /// Each Eval’s actual gates come from the Eval-level schedule.
60    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    /// Build from scope's resolved relations without reading files or running code.
73    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            // A parent needs its parts as a consumer needs its inputs, for obligations
122            // only: execution gates come from references alone.
123            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        // Consumer -> input makes iterative SCC traversal dependency-first.
133        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    /// Components are dependency-first; members and gates are lexically ordered.
192    pub fn components(&self) -> &[Component<'a>] {
193        &self.components
194    }
195
196    /// Required material and verification, including roots and all cycle peers.
197    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    /// The roots and every Artifact sharing a cycle with one of them, lexically ordered.
223    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    /// Evaluate the whole graph. Evidence freshness is supplied by the caller;
245    /// absent entries are unreviewed, and unrelated entries are ignored.
246    pub fn evaluate(&self, evidence: &BTreeMap<EvalId, Evidence>) -> Evaluation<'_> {
247        self.evaluate_for_run(evidence, false)
248    }
249
250    /// Speculation turns operational failures into immediate blockers, without changing
251    /// the default read/verification policy of waiting for replacement GREEN evidence.
252    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            // Only a Human eval's sibling machine evals share its Artifact; their ERROR blocks
266            // it too, since a person signs off only on passing machine evals.
267            let target = self.evals[id.as_str()];
268            // A BLOCKED gate passes on its own causes: the root is always a RED upstream.
269            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                // An Artifact without evals that is composed of others takes its verdict from
350                // them, which the components below require.
351                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        // A part's status is part of its parent's, so parts go first; parts never form a cycle.
370        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/// Current means usable for this input/run, not merely the latest stored result.
467/// Stale evidence carries no current verdict; operational failure is not RED.
468#[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    /// Gate readiness only; READY never requests re-execution of existing evidence.
499    pub readiness: Readiness,
500    pub status: EvalStatus,
501    /// Retained for audit even when gates mask its effective status.
502    pub evidence: Option<Evidence>,
503    pub unmet_gates: Vec<&'a EvalId>,
504    /// BLOCKED: the root causes; otherwise the unmet gates it waits for.
505    pub blocked_by: Vec<BlockedReference<'a>>,
506    /// Why the eval is BLOCKED, followed through BLOCKED gates to the failed evals upstream;
507    /// empty unless BLOCKED.
508    pub causes: Vec<Cause<'a>>,
509}
510
511/// The failed eval at the root of a block: a RED verdict, or in a Run that treats
512/// operational failures as blockers (`--speculate`), an ERROR. A Human eval is also blocked
513/// by a RED or ERROR machine eval of its own Artifact, its sibling.
514#[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    /// The same failure as seen from downstream, where it is an upstream dependency.
530    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/// A blocked dependency names either material or an Eval, never an unclassified string.
540#[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    /// Operational errors require an explicit retry, not automatic redispatch.
567    pub fn can_execute(&self) -> bool {
568        self.readiness == Readiness::Ready && matches!(self.evidence, None | Some(Evidence::Stale))
569    }
570
571    /// Names the failed evals that block this one, RED verdicts first; `None` unless BLOCKED.
572    /// Each cause is final within a Run, but more can follow, so settlement saves it again.
573    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    /// Includes all required dependencies and SCC peers, even without Evals.
646    pub satisfied: bool,
647    pub passed: usize,
648    pub total: usize,
649}
650
651/// Aggregate validation status, never a fabricated Eval verdict.
652#[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    /// Artifacts whose own obligations are unmet, not execution gates.
665    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;