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}