layover_core/diagram/mod.rs
1//! Drawing a factory.
2//!
3//! Two renderers, deliberately, because they answer different questions.
4//!
5//! - [`mermaid`] emits Mermaid source. It is for *portability*: paste it into a README, an issue
6//! or a chat window and something will draw it. `layover graph` prints this.
7//! - [`layout`] and [`svg`] draw the graph directly. That is for the *dashboard*, where the
8//! diagram has to carry live state, respond to a pointer, and load instantly.
9//!
10//! The alternative was to use Mermaid for both, and it was rejected on size: the Mermaid runtime
11//! is 2.5 MB of JavaScript, which would have to be vendored into the repository and embedded in
12//! the binary to keep the dashboard working offline. That is a poor trade for laying out twenty
13//! nodes, and a layered layout for a graph this small is a few hundred lines that can actually be
14//! unit-tested — which asserting on a JavaScript library's output could not be.
15
16pub mod layout;
17pub mod mermaid;
18pub mod svg;
19
20use std::collections::BTreeMap;
21
22use crate::agent::AgentName;
23
24pub use layout::{Edge, EdgeStyle, Layout, Node, NodeKind, Shape};
25pub use mermaid::route_map;
26pub use svg::render as render_svg;
27
28/// Which workflow to draw.
29///
30/// A factory holds several pipelines and they are genuinely separate workflows: a nightly sweep
31/// has nothing to do with taking a work item to a pull request. Drawing them together produces one
32/// tangle that reads as a single, very confused process — which is what a reader concludes.
33#[derive(Debug, Clone, Default, PartialEq, Eq)]
34pub enum Scope {
35 /// Every pipeline and every route in the factory.
36 #[default]
37 Everything,
38 /// Only what this pipeline sets in motion.
39 Pipeline(crate::pipeline::PipelineName),
40}
41
42impl Scope {
43 /// The pipeline being drawn, or `None` for the whole factory.
44 #[must_use]
45 pub fn pipeline(&self) -> Option<&crate::pipeline::PipelineName> {
46 match self {
47 Self::Everything => None,
48 Self::Pipeline(name) => Some(name),
49 }
50 }
51}
52
53/// What an agent is doing right now, for colouring a diagram.
54///
55/// Absent from the map means idle. Idle is the overwhelmingly common state, so storing it would
56/// be storing mostly nothing.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub enum Activity {
59 /// At least one run of this agent is in flight.
60 Running,
61 /// Flights are parked at this agent's barrier, waiting for the rest.
62 Waiting,
63 /// This agent's last run ended badly.
64 Failed,
65}
66
67impl Activity {
68 /// The class name used to colour a node in this state.
69 #[must_use]
70 pub fn class(self) -> &'static str {
71 match self {
72 Self::Running => "running",
73 Self::Waiting => "waiting",
74 Self::Failed => "failed",
75 }
76 }
77}
78
79/// How the factory is currently behaving, overlaid on the static route map.
80///
81/// Empty by default, which renders the plain topology. That matters because the diagram has to
82/// work before the Tower has ever run anything.
83#[derive(Debug, Clone, Default)]
84pub struct Live {
85 /// What each busy agent is doing.
86 pub activity: BTreeMap<AgentName, Activity>,
87}
88
89impl Live {
90 /// Marks an agent as being in a state.
91 #[must_use]
92 pub fn with(mut self, agent: impl Into<AgentName>, activity: Activity) -> Self {
93 self.activity.insert(agent.into(), activity);
94 self
95 }
96
97 /// Returns `true` when nothing is happening.
98 #[must_use]
99 pub fn is_idle(&self) -> bool {
100 self.activity.is_empty()
101 }
102}