Skip to main content

kernel/model/
mode.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The run policy: the four values one run works under (`crates/kernel/spec/Model.lean`
7//! ยง8-77).
8//!
9//! Defined here rather than in `runtime` for the reason
10//! [`DialectKind`](crate::DialectKind) is: the wire carries them and
11//! `runtime` evaluates them, and neither of those crates may name the
12//! other. `runtime::mode` holds what each value admits; this holds only
13//! which values exist and how they are spelled.
14//!
15//! Every enum here is closed and carried in the spelling its `as_str`
16//! writes. A word outside a set fails to deserialize at the process
17//! boundary, which is where the sender can still be told; read as a
18//! default, a misspelled word would become a run nobody asked for.
19
20use serde::{Deserialize, Serialize};
21
22use crate::write_domain::WriteLimit;
23
24/// Whether a run is talking with the person or carrying out work.
25///
26/// It decides how the run's catalog row introduces it and nothing else:
27/// what it may write, what it must prove and whether it lands are the
28/// other three values of [`RunPolicy`].
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32pub enum Mode {
33    /// Talk with the person: answer what they said. First, because a
34    /// sentence typed into a conversation is talk before it is work.
35    Chat,
36    /// Carry out the task towards the goal the person stated.
37    Work,
38}
39
40impl Mode {
41    /// Every mode, in the order a control offers them.
42    pub const ALL: [Mode; 2] = [Mode::Chat, Mode::Work];
43
44    /// The word this mode travels under, everywhere it travels โ€” the
45    /// wire and the ledger.
46    #[must_use]
47    pub const fn as_str(self) -> &'static str {
48        match self {
49            Mode::Chat => "chat",
50            Mode::Work => "work",
51        }
52    }
53}
54
55impl std::fmt::Display for Mode {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.write_str(self.as_str())
58    }
59}
60
61/// The evidence a run's work must carry before a merge lets it into
62/// the building.
63///
64/// Choosing one does not provide the evidence; `runtime::mode::admits`
65/// compares what the run produced against it at the merge.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
69pub enum AdmissionRequirement {
70    /// What the building's own rules already demand, and nothing on
71    /// top. A named value rather than an absence, because an absence
72    /// reads as "no checks" and the building's checks still run.
73    Standing,
74    /// The asset's own tests ran and passed.
75    Tested,
76    /// The observable contract did not move.
77    ContractKept,
78    /// Both halves of the double validation: held-in and held-out.
79    DoubleValidated,
80}
81
82impl AdmissionRequirement {
83    /// Every requirement, from the one that adds nothing to the one
84    /// that asks most.
85    pub const ALL: [AdmissionRequirement; 4] = [
86        AdmissionRequirement::Standing,
87        AdmissionRequirement::Tested,
88        AdmissionRequirement::ContractKept,
89        AdmissionRequirement::DoubleValidated,
90    ];
91
92    /// The word this requirement travels under.
93    #[must_use]
94    pub const fn as_str(self) -> &'static str {
95        match self {
96            AdmissionRequirement::Standing => "standing",
97            AdmissionRequirement::Tested => "tested",
98            AdmissionRequirement::ContractKept => "contract_kept",
99            AdmissionRequirement::DoubleValidated => "double_validated",
100        }
101    }
102}
103
104/// Whether a run's work takes the ordinary road or stays in an
105/// experiment that never lands.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
107#[serde(rename_all = "snake_case")]
108#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
109pub enum LandingPolicy {
110    /// The building's own road: straight into its tree, or through
111    /// review when the building asks for review.
112    Ordinary,
113    /// A worktree of the run's own, even in a building without review,
114    /// from which nothing is ever merged.
115    Experiment,
116}
117
118impl LandingPolicy {
119    /// Every landing policy, the ordinary one first.
120    pub const ALL: [LandingPolicy; 2] = [LandingPolicy::Ordinary, LandingPolicy::Experiment];
121
122    /// The word this landing policy travels under.
123    #[must_use]
124    pub const fn as_str(self) -> &'static str {
125        match self {
126            LandingPolicy::Ordinary => "ordinary",
127            LandingPolicy::Experiment => "experiment",
128        }
129    }
130}
131
132/// The four values one run works under, chosen when it is dispatched
133/// and written into its `run_started` line.
134///
135/// One value because the four always travel together โ€” on the wire, in
136/// the ledger and through the dispatch โ€” and are chosen independently,
137/// so every combination has a meaning.
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
139#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
140pub struct RunPolicy {
141    pub mode: Mode,
142    /// What the run may do to a file that already exists.
143    pub write: WriteLimit,
144    /// The evidence its work must carry to be merged.
145    pub admit: AdmissionRequirement,
146    /// Whether its work takes the ordinary road.
147    pub landing: LandingPolicy,
148}
149
150impl RunPolicy {
151    /// The policy of a dispatch that chose only its mode: no narrowing
152    /// of the write domain, the building's own checks, the ordinary
153    /// road.
154    #[must_use]
155    pub const fn of(mode: Mode) -> RunPolicy {
156        RunPolicy {
157            mode,
158            write: WriteLimit::Full,
159            admit: AdmissionRequirement::Standing,
160            landing: LandingPolicy::Ordinary,
161        }
162    }
163}
164
165#[cfg(test)]
166#[allow(clippy::unwrap_used, clippy::expect_used, reason = "test code")]
167mod tests {
168    use super::*;
169
170    /// One spelling per enum: what `as_str` writes is what serde reads
171    /// back, so a ledger payload and a wire frame cannot name one value
172    /// two ways.
173    #[test]
174    fn every_value_reads_back_from_the_word_it_writes() {
175        fn round_trips<T>(all: &[T], word: fn(T) -> &'static str)
176        where
177            T: Copy + PartialEq + std::fmt::Debug + Serialize + for<'de> Deserialize<'de>,
178        {
179            for value in all {
180                let text = serde_json::to_string(value).unwrap();
181                assert_eq!(text, format!("\"{}\"", word(*value)));
182                assert_eq!(serde_json::from_str::<T>(&text).unwrap(), *value);
183            }
184        }
185        round_trips(&Mode::ALL, Mode::as_str);
186        round_trips(&AdmissionRequirement::ALL, AdmissionRequirement::as_str);
187        round_trips(&LandingPolicy::ALL, LandingPolicy::as_str);
188        round_trips(&WriteLimit::ALL, WriteLimit::as_str);
189    }
190
191    /// An unknown word is refused rather than read as some default,
192    /// and so is a word this version no longer has.
193    #[test]
194    fn a_word_outside_the_set_is_refused() {
195        assert!(serde_json::from_str::<Mode>("\"a-mode-we-have-never-heard-of\"").is_err());
196        assert!(serde_json::from_str::<Mode>("\"plan_goal\"").is_err());
197        assert!(serde_json::from_str::<AdmissionRequirement>("\"none\"").is_err());
198        assert!(serde_json::from_str::<LandingPolicy>("\"merge\"").is_err());
199    }
200
201    /// A policy on the wire names all four values; one left out is a
202    /// refused frame, not a default somebody did not choose.
203    #[test]
204    fn a_policy_missing_a_value_is_refused() {
205        let whole: RunPolicy = serde_json::from_str(
206            r#"{"mode":"work","write":"create","admit":"tested","landing":"experiment"}"#,
207        )
208        .unwrap();
209        assert_eq!(
210            whole,
211            RunPolicy {
212                mode: Mode::Work,
213                write: WriteLimit::Create,
214                admit: AdmissionRequirement::Tested,
215                landing: LandingPolicy::Experiment,
216            }
217        );
218        assert!(
219            serde_json::from_str::<RunPolicy>(r#"{"mode":"work","write":"full","admit":"tested"}"#)
220                .is_err()
221        );
222    }
223}