Skip to main content

sloop/flow/
mod.rs

1//! Flow definitions and the pure walk over them. Parsing turns a committed
2//! YAML file into a validated `Flow`; `next_step` then replays the run's
3//! ordered evidence log over that flow to derive the next stage to run or a
4//! terminal reading. Neither half touches a clock, a process, or the store,
5//! so policy can be tested without a daemon.
6//!
7//! The halves live in their own files and meet only here. This file is the
8//! vocabulary: the types a stage is written in, and nothing that reads or
9//! replays them. `parse` holds the YAML grammar and the `Raw*` wire types that
10//! are its only readers; `walk` holds the replay, which reads exactly three
11//! things from the vocabulary and never mentions the rest; `panel` holds the
12//! panel slice — its types, its parse validation, and its aggregation — which
13//! the walk does not know exists.
14
15mod panel;
16mod parse;
17mod walk;
18
19pub use panel::{
20    Confidence, MAX_PANEL_REVIEWERS, MIN_PANEL_REVIEWERS, NO_VERDICT_REPORTED, PANEL_PROMPT_ROOT,
21    Panel, PanelOutcome, Reviewer, ReviewerReport, aggregate,
22};
23pub use parse::parse;
24pub use walk::{
25    HaltReason, Reported, StageEvidence, Step, Verdict, VerdictSource, next_step, resolve_verdict,
26    return_trigger,
27};
28
29use serde::{Deserialize, Serialize};
30
31pub const DEFAULT_FLOW_NAME: &str = "default";
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Flow {
35    pub name: String,
36    pub stages: Vec<Stage>,
37}
38
39/// One stage: an untrusted `action`, an independent `result_check` that
40/// judges it, and what to do when the reading is `Fail`.
41///
42/// The split is the point. The action is whatever produces the work and is
43/// never trusted to grade itself; the check is a separate actor (or the
44/// action's own exit, or a worker's report) that decides. Every judgement a
45/// stage can carry is a configuration of this one shape rather than a policy
46/// of its own.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct Stage {
49    pub name: String,
50    pub action: Actor,
51    pub result_check: Check,
52    pub fail_action: FailAction,
53    /// The merge builtin's fast-forward-only mode, written inside the action
54    /// as `{ builtin: merge, ff_only: true }`. It is refused on every other
55    /// action, so a `true` here always describes a merge stage.
56    ///
57    /// It rides on the stage rather than inside `Actor`, keeping
58    /// `Builtin::Merge`'s unit wire shape: an absent key is simply the mode
59    /// switched off, in the flow file and in the snapshot alike.
60    #[serde(default, skip_serializing_if = "is_false")]
61    pub ff_only: bool,
62}
63
64fn is_false(value: &bool) -> bool {
65    !*value
66}
67
68/// The inclusive upper bound on a `return_to` edge's attempt budget.
69pub const MAX_RETURN_ATTEMPTS: u32 = 3;
70
71/// The worst-case number of stage executions a flow may imply once every
72/// `return_to` budget is spent. A flow that could exceed it is rejected at
73/// parse time rather than discovered at runtime.
74pub const MAX_FLOW_EXECUTIONS: u64 = 32;
75
76/// Something that can be run within a stage. The same vocabulary serves
77/// both positions: an `Actor` is either the stage's action or the
78/// independent judge in its `result_check`.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub enum Actor {
81    /// Spawns the ticket's agent target in the run worktree. The prompt
82    /// comes from the ticket, not from the flow file.
83    Agent,
84    /// Runs an argv (no shell) in the run worktree.
85    Exec { cmd: Vec<String> },
86    /// A judgement Sloop makes itself, with no process to run.
87    Builtin(Builtin),
88}
89
90/// The actors Sloop implements internally.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92pub enum Builtin {
93    /// Applies the run branch to the default branch using Sloop's merge
94    /// policy. Only ever an action, never a check.
95    Merge,
96    /// Passes when Sloop observed at least one new commit on the run
97    /// branch. Only ever a check.
98    Commits,
99    /// Integrates the default branch into the run branch, inside the run
100    /// worktree. Only ever an action, never a check.
101    ///
102    /// It is the merge builtin's mirror image: the merge stage moves the
103    /// default branch and reads the run branch, and this one moves the run
104    /// branch and only ever reads the default branch. Putting it before the
105    /// stages that judge the work is what makes those stages judge the tree
106    /// the merge will actually land.
107    Sync,
108}
109
110/// How a stage's action is judged.
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub enum Check {
113    /// The action's own exit status judges it: 0 passes.
114    None,
115    /// The worker must call `sloop verdict`; silence is a failure.
116    Reported,
117    /// An independent actor runs after the action and decides.
118    Actor(Actor),
119    /// Several independent reviewers each report, and a pure function over
120    /// their reports decides.
121    Panel(Panel),
122}
123
124/// What the walk does when a stage's reading is `Fail`.
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126pub enum FailAction {
127    /// Stop the walk here, leaving the stages after it unrequested.
128    Halt,
129    /// Record the failure and carry on to the next stage.
130    Continue,
131    /// Re-run the span from `stage` through this one, up to `attempts`
132    /// times.
133    ReturnTo { stage: String, attempts: u32 },
134}
135
136pub(crate) fn built_in_default() -> Flow {
137    let stages = vec![
138        Stage {
139            name: "build".into(),
140            action: Actor::Agent,
141            result_check: Check::Actor(Actor::Builtin(Builtin::Commits)),
142            fail_action: FailAction::Halt,
143            ff_only: false,
144        },
145        Stage {
146            name: "merge".into(),
147            action: Actor::Builtin(Builtin::Merge),
148            result_check: Check::None,
149            fail_action: FailAction::Halt,
150            ff_only: false,
151        },
152    ];
153    Flow {
154        name: DEFAULT_FLOW_NAME.into(),
155        stages,
156    }
157}