Skip to main content

kernel/error/
code.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//! Error codes: the closed AxCode set and its carrier events.
7//! The part `crates/kernel/spec/Error.lean` specifies this module with the
8//! rest of `kernel::error`.
9
10use serde::{Deserialize, Serialize};
11
12use crate::event::EventKind;
13
14/// Where an AxCode surfaces in history: its carrier event, or the loadtime
15/// class (the closed C9 exception: the Ledger is not writable yet).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Carrier {
18    Event(EventKind),
19    Loadtime,
20}
21
22/// Closed set of error codes.
23/// Extension is additive only; the wire spelling lives in [`AxCode::as_str`].
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
25pub enum AxCode {
26    // Base table (15).
27    PathNotFound,
28    ToolUnknown,
29    ToolUnavailable,
30    InvalidArgs,
31    OutsideWriteDomain,
32    VersionConflict,
33    GateDenied,
34    BudgetExhausted,
35    Timeout,
36    Provider,
37    EvidenceMissing,
38    LoopSuspected,
39    LocatorInvalid,
40    SandboxDenied,
41    /// The address or resource this names is taken right now.
42    ///
43    /// Distinct from `WorktreeBusy` and `RepairBusy`, which are
44    /// about those two mechanisms in particular: this one says a
45    /// *run* is working where the caller wants to act, and names the
46    /// run so the caller can stop it and act.
47    Busy,
48    // Collaboration (5). A sixth, `SignalUnknown`, was defined away:
49    // signal payloads are written and read by one module, the kind is
50    // an exhaustive enum, and a kind this version does not know
51    // can only come from a newer binary's ledger — which the version
52    // door already refuses.
53    DraftStale,
54    GoalConflict,
55    TaintedAction,
56    RepairBusy,
57    DelegationDepth,
58    // Governance and facilities (15).
59    ApprovalPending,
60    ApprovalDenied,
61    CrossBuildingDenied,
62    DigestSuspect,
63    CredentialMissing,
64    /// No model is chosen for the tag a caller asked for. Apart from
65    /// `ConfigInvalid` because its recovery differs: a new city starts
66    /// here and is sent to the settings page, while `ConfigInvalid`
67    /// also answers a room whose frozen model changed, which a new
68    /// session recovers.
69    ModelUnchosen,
70    ConfigInvalid,
71    CasCorrupt,
72    StorageFatal,
73    WorktreeBusy,
74    BrowserUnavailable,
75    EndpointDialectUnsupported,
76    WireMismatch,
77    LogVersionUnsupported,
78    /// Another process holds this city's Ledger for writing. Loadtime,
79    /// because the process it refuses is the one that may not write.
80    LedgerHeld,
81    /// A served city is still proving the history it opened from, and
82    /// its writer takes no line until the proof is whole. Loadtime for
83    /// the same reason as `LedgerHeld`: the refused process may not write
84    /// yet (`crates/storage/Spec.lean` §8-30).
85    HistoryUnproven,
86    // Privacy and Discard (2).
87    SecretEgress,
88    DiscardIrreversible,
89    // Backpressure (1). A shed signal is a refused delivery, not a lost
90    // one: the line is on the ledger, the queue never took it, and the
91    // caller learns it here rather than by reading the queue back.
92    BackpressureShed,
93    // Unknown outcome (1).
94    ToolOutcomeUnknown,
95    // Plan (1). A standing goal set on a building with no plan; the
96    // subject is always `<building address>: <goal>`, which is the shape
97    // the client's form recovery reads.
98    PlanMissing,
99}
100
101impl AxCode {
102    /// Every code, in the order the SPEC table lists them. Data face for tests and
103    /// `xtask specalign`.
104    pub const ALL: [AxCode; 41] = [
105        AxCode::PathNotFound,
106        AxCode::ToolUnknown,
107        AxCode::ToolUnavailable,
108        AxCode::InvalidArgs,
109        AxCode::OutsideWriteDomain,
110        AxCode::VersionConflict,
111        AxCode::GateDenied,
112        AxCode::BudgetExhausted,
113        AxCode::Timeout,
114        AxCode::Provider,
115        AxCode::EvidenceMissing,
116        AxCode::LoopSuspected,
117        AxCode::LocatorInvalid,
118        AxCode::SandboxDenied,
119        AxCode::Busy,
120        AxCode::DraftStale,
121        AxCode::GoalConflict,
122        AxCode::TaintedAction,
123        AxCode::RepairBusy,
124        AxCode::DelegationDepth,
125        AxCode::ApprovalPending,
126        AxCode::ApprovalDenied,
127        AxCode::CrossBuildingDenied,
128        AxCode::DigestSuspect,
129        AxCode::CredentialMissing,
130        AxCode::ModelUnchosen,
131        AxCode::ConfigInvalid,
132        AxCode::CasCorrupt,
133        AxCode::StorageFatal,
134        AxCode::WorktreeBusy,
135        AxCode::BrowserUnavailable,
136        AxCode::EndpointDialectUnsupported,
137        AxCode::WireMismatch,
138        AxCode::LogVersionUnsupported,
139        AxCode::LedgerHeld,
140        AxCode::HistoryUnproven,
141        AxCode::SecretEgress,
142        AxCode::DiscardIrreversible,
143        AxCode::BackpressureShed,
144        AxCode::ToolOutcomeUnknown,
145        AxCode::PlanMissing,
146    ];
147
148    /// The wire spelling. Sole spelling authority; serde and Display reuse it.
149    pub fn as_str(&self) -> &'static str {
150        match self {
151            AxCode::PathNotFound => "E_PATH_NOT_FOUND",
152            AxCode::ToolUnknown => "E_TOOL_UNKNOWN",
153            AxCode::ToolUnavailable => "E_TOOL_UNAVAILABLE",
154            AxCode::InvalidArgs => "E_INVALID_ARGS",
155            AxCode::OutsideWriteDomain => "E_OUTSIDE_WRITE_DOMAIN",
156            AxCode::VersionConflict => "E_VERSION_CONFLICT",
157            AxCode::GateDenied => "E_GATE_DENIED",
158            AxCode::BudgetExhausted => "E_BUDGET_EXHAUSTED",
159            AxCode::Timeout => "E_TIMEOUT",
160            AxCode::Provider => "E_PROVIDER",
161            AxCode::EvidenceMissing => "E_EVIDENCE_MISSING",
162            AxCode::LoopSuspected => "E_LOOP_SUSPECTED",
163            AxCode::LocatorInvalid => "E_LOCATOR_INVALID",
164            AxCode::SandboxDenied => "E_SANDBOX_DENIED",
165            AxCode::Busy => "E_BUSY",
166            AxCode::DraftStale => "E_DRAFT_STALE",
167            AxCode::GoalConflict => "E_GOAL_CONFLICT",
168            AxCode::TaintedAction => "E_TAINTED_ACTION",
169            AxCode::RepairBusy => "E_REPAIR_BUSY",
170            AxCode::DelegationDepth => "E_DELEGATION_DEPTH",
171            AxCode::ApprovalPending => "E_APPROVAL_PENDING",
172            AxCode::ApprovalDenied => "E_APPROVAL_DENIED",
173            AxCode::CrossBuildingDenied => "E_CROSS_BUILDING_DENIED",
174            AxCode::DigestSuspect => "E_DIGEST_SUSPECT",
175            AxCode::CredentialMissing => "E_CREDENTIAL_MISSING",
176            AxCode::ModelUnchosen => "E_MODEL_UNCHOSEN",
177            AxCode::ConfigInvalid => "E_CONFIG_INVALID",
178            AxCode::CasCorrupt => "E_CAS_CORRUPT",
179            AxCode::StorageFatal => "E_STORAGE_FATAL",
180            AxCode::WorktreeBusy => "E_WORKTREE_BUSY",
181            AxCode::BrowserUnavailable => "E_BROWSER_UNAVAILABLE",
182            AxCode::EndpointDialectUnsupported => "E_ENDPOINT_DIALECT_UNSUPPORTED",
183            AxCode::WireMismatch => "E_WIRE_MISMATCH",
184            AxCode::LogVersionUnsupported => "E_LOG_VERSION_UNSUPPORTED",
185            AxCode::LedgerHeld => "E_LEDGER_HELD",
186            AxCode::HistoryUnproven => "E_HISTORY_UNPROVEN",
187            AxCode::SecretEgress => "E_SECRET_EGRESS",
188            AxCode::BackpressureShed => "E_BACKPRESSURE_SHED",
189            AxCode::DiscardIrreversible => "E_DISCARD_IRREVERSIBLE",
190            AxCode::ToolOutcomeUnknown => "E_TOOL_OUTCOME_UNKNOWN",
191            AxCode::PlanMissing => "E_PLAN_MISSING",
192        }
193    }
194
195    /// Wire spelling back to code; `None` is the caller's fail-closed branch.
196    pub fn parse(raw: &str) -> Option<AxCode> {
197        AxCode::ALL.into_iter().find(|code| code.as_str() == raw)
198    }
199
200    /// The carrier declaration (C9): which event carries this code into
201    /// history. Sole declaration site, exhaustive on purpose — a new code
202    /// without a carrier decision is a compile error. The loadtime arm is
203    /// the closed whitelist of codes that arise only while this process
204    /// cannot write the Ledger; `crates/kernel/Spec.lean` §12 gives each one's
205    /// reason.
206    pub fn carrier(&self) -> Carrier {
207        match self {
208            AxCode::GateDenied
209            | AxCode::OutsideWriteDomain
210            | AxCode::TaintedAction
211            | AxCode::CrossBuildingDenied
212            | AxCode::DiscardIrreversible
213            | AxCode::SecretEgress
214            | AxCode::DelegationDepth => Carrier::Event(EventKind::GateDenied),
215            AxCode::ApprovalPending => Carrier::Event(EventKind::ApprovalRequested),
216            AxCode::ApprovalDenied => Carrier::Event(EventKind::ApprovalResolved),
217            AxCode::BudgetExhausted => Carrier::Event(EventKind::BudgetLimit),
218            AxCode::LoopSuspected => Carrier::Event(EventKind::WatchdogFired),
219            AxCode::Provider => Carrier::Event(EventKind::ProviderDegraded),
220            AxCode::EndpointDialectUnsupported => Carrier::Event(EventKind::EndpointLost),
221            AxCode::ConfigInvalid
222            | AxCode::CasCorrupt
223            | AxCode::StorageFatal
224            | AxCode::WireMismatch
225            | AxCode::LogVersionUnsupported
226            | AxCode::LedgerHeld
227            | AxCode::HistoryUnproven => Carrier::Loadtime,
228            AxCode::PathNotFound
229            | AxCode::ToolUnknown
230            | AxCode::ToolUnavailable
231            | AxCode::InvalidArgs
232            | AxCode::VersionConflict
233            | AxCode::Timeout
234            | AxCode::EvidenceMissing
235            | AxCode::LocatorInvalid
236            | AxCode::SandboxDenied
237            | AxCode::Busy
238            | AxCode::DraftStale
239            | AxCode::GoalConflict
240            | AxCode::RepairBusy
241            | AxCode::DigestSuspect
242            | AxCode::CredentialMissing
243            | AxCode::ModelUnchosen
244            | AxCode::WorktreeBusy
245            | AxCode::BrowserUnavailable
246            | AxCode::ToolOutcomeUnknown
247            | AxCode::BackpressureShed
248            | AxCode::PlanMissing => Carrier::Event(EventKind::ToolResult),
249        }
250    }
251}
252
253impl std::fmt::Display for AxCode {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        f.write_str(self.as_str())
256    }
257}
258
259impl Serialize for AxCode {
260    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
261        serializer.serialize_str(self.as_str())
262    }
263}
264
265impl<'de> Deserialize<'de> for AxCode {
266    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
267        let raw = String::deserialize(deserializer)?;
268        AxCode::parse(&raw)
269            .ok_or_else(|| serde::de::Error::custom(format_args!("unknown AxCode `{raw}`")))
270    }
271}
272
273#[cfg(test)]
274#[allow(
275    clippy::unwrap_used,
276    clippy::expect_used,
277    clippy::panic,
278    clippy::indexing_slicing,
279    reason = "test code"
280)]
281mod tests {
282    use super::*;
283    use std::collections::BTreeSet;
284
285    #[test]
286    fn axcode_is_the_declared_length_and_spelling_is_bijective() {
287        // The length is the close of the table, so it is stated once:
288        // a code added without a spelling, or two codes sharing one,
289        // fails here rather than at a caller.
290        assert_eq!(AxCode::ALL.len(), 41);
291        let spellings: BTreeSet<&str> = AxCode::ALL.iter().map(AxCode::as_str).collect();
292        assert_eq!(spellings.len(), 41);
293        for s in &spellings {
294            assert!(s.starts_with("E_"));
295        }
296    }
297
298    #[test]
299    fn axcode_parse_roundtrips_every_variant() {
300        for code in AxCode::ALL {
301            assert_eq!(AxCode::parse(code.as_str()), Some(code));
302        }
303        assert_eq!(AxCode::parse("E_NO_SUCH_CODE"), None);
304    }
305
306    #[test]
307    fn axcode_serde_uses_the_wire_spelling() {
308        let json = serde_json::to_string(&AxCode::PathNotFound).unwrap();
309        assert_eq!(json, "\"E_PATH_NOT_FOUND\"");
310        let back: AxCode = serde_json::from_str("\"E_LOCATOR_INVALID\"").unwrap();
311        assert_eq!(back, AxCode::LocatorInvalid);
312        assert!(serde_json::from_str::<AxCode>("\"E_BOGUS\"").is_err());
313    }
314}