Skip to main content

areev_loop/
error.rs

1//! Areev Loop error type. Follows the Areev error convention: every variant's
2//! `Display` string leads with a stable `LOP-Ennn` code, and `code()` returns
3//! it. Codes are **append-only** — never renumber or reuse (see
4//! `ERROR_CODES.md`). The engine has zero areev dependencies, so it owns its
5//! own domain (`LOP`); REVIEW/APPLY *syntax* errors belong to the substrate's
6//! CAL domain, not here.
7
8use std::fmt;
9
10pub type Result<T> = std::result::Result<T, Error>;
11
12/// All errors produced by the Areev Loop engine.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Error {
15    /// A substrate call (grain read/write, CAL execution) failed.
16    Substrate(String),
17    /// The substrate cannot execute the given CAL (e.g. the reference
18    /// substrate's minimal subset). Distinct from a substrate fault.
19    CalUnsupported(String),
20    /// A `target_ref` did not parse to a known scheme.
21    InvalidTargetRef(String),
22    /// A proposal payload failed structural validation.
23    InvalidProposal(String),
24    /// A recommendation draft is missing a MUST field or is malformed.
25    InvalidRecommendation(String),
26    /// An attempted lifecycle transition is not allowed from the current state.
27    LifecycleViolation(String),
28    /// Self-approval blocked: the approving actor authored the recommendation.
29    SelfApproval(String),
30    /// The caller lacks a scope required for the operation.
31    ScopeDenied(String),
32    /// A destructive apply was attempted without the required gates.
33    DestructiveGated(String),
34    /// One analyzer's run failed; its findings are dropped, the run continues.
35    AnalyzerFailed { id: String, message: String },
36    /// An analyzer parameter is outside its declared `ParamSpec`.
37    ParamInvalid(String),
38    /// A required substrate capability (forks, telemetry, embeddings) is absent.
39    CapabilityMissing(String),
40    /// The optional LLM enrichment backend (`--llm-cmd`) is misconfigured or
41    /// failed. Never fatal to a run — the LLM contribution is dropped.
42    LlmBackend(String),
43    /// The optional decision backend (`Engine::with_decider`) failed or
44    /// returned a malformed answer. Never fatal to a run — that stage's
45    /// decision contribution is dropped and today's rule applies.
46    DecideBackend(String),
47    /// No recommendation exists at the given hash.
48    NotFound(String),
49    /// An unexpected internal fault (should not happen — file a bug).
50    Internal(String),
51}
52
53impl Error {
54    /// Stable machine-readable code in `LOP-Ennn` form.
55    pub fn code(&self) -> &'static str {
56        match self {
57            Error::Substrate(_) => "LOP-E001",
58            Error::CalUnsupported(_) => "LOP-E002",
59            Error::InvalidTargetRef(_) => "LOP-E010",
60            Error::InvalidProposal(_) => "LOP-E011",
61            Error::InvalidRecommendation(_) => "LOP-E012",
62            Error::LifecycleViolation(_) => "LOP-E020",
63            Error::SelfApproval(_) => "LOP-E021",
64            Error::ScopeDenied(_) => "LOP-E022",
65            Error::DestructiveGated(_) => "LOP-E023",
66            Error::AnalyzerFailed { .. } => "LOP-E030",
67            Error::ParamInvalid(_) => "LOP-E031",
68            Error::CapabilityMissing(_) => "LOP-E032",
69            Error::LlmBackend(_) => "LOP-E050",
70            Error::DecideBackend(_) => "LOP-E051",
71            Error::NotFound(_) => "LOP-E040",
72            Error::Internal(_) => "LOP-E099",
73        }
74    }
75}
76
77impl fmt::Display for Error {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        let code = self.code();
80        match self {
81            Error::Substrate(m) => write!(f, "{code} substrate call failed: {m}"),
82            Error::CalUnsupported(m) => write!(f, "{code} CAL not supported by substrate: {m}"),
83            Error::InvalidTargetRef(m) => write!(f, "{code} invalid target_ref: {m}"),
84            Error::InvalidProposal(m) => write!(f, "{code} invalid proposal: {m}"),
85            Error::InvalidRecommendation(m) => write!(f, "{code} invalid recommendation: {m}"),
86            Error::LifecycleViolation(m) => write!(f, "{code} illegal lifecycle transition: {m}"),
87            Error::SelfApproval(m) => write!(f, "{code} self-approval blocked: {m}"),
88            Error::ScopeDenied(m) => write!(f, "{code} scope denied: {m}"),
89            Error::DestructiveGated(m) => write!(f, "{code} destructive apply gated: {m}"),
90            Error::AnalyzerFailed { id, message } => {
91                write!(f, "{code} analyzer {id} failed: {message}")
92            }
93            Error::ParamInvalid(m) => write!(f, "{code} analyzer parameter invalid: {m}"),
94            Error::CapabilityMissing(m) => write!(f, "{code} required capability missing: {m}"),
95            Error::LlmBackend(m) => write!(f, "{code} LLM backend error: {m}"),
96            Error::DecideBackend(m) => write!(f, "{code} decision backend error: {m}"),
97            Error::NotFound(m) => write!(f, "{code} recommendation not found: {m}"),
98            Error::Internal(m) => write!(f, "{code} internal error: {m}"),
99        }
100    }
101}
102
103impl std::error::Error for Error {}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn display_leads_with_code() {
111        let e = Error::InvalidTargetRef("nope".into());
112        assert!(e.to_string().starts_with("LOP-E010 "));
113        assert_eq!(e.code(), "LOP-E010");
114    }
115
116    #[test]
117    fn codes_are_unique_and_well_formed() {
118        // One representative per variant; guards against copy-paste collisions.
119        let all = [
120            Error::Substrate(String::new()),
121            Error::CalUnsupported(String::new()),
122            Error::InvalidTargetRef(String::new()),
123            Error::InvalidProposal(String::new()),
124            Error::InvalidRecommendation(String::new()),
125            Error::LifecycleViolation(String::new()),
126            Error::SelfApproval(String::new()),
127            Error::ScopeDenied(String::new()),
128            Error::DestructiveGated(String::new()),
129            Error::AnalyzerFailed {
130                id: String::new(),
131                message: String::new(),
132            },
133            Error::ParamInvalid(String::new()),
134            Error::CapabilityMissing(String::new()),
135            Error::LlmBackend(String::new()),
136            Error::DecideBackend(String::new()),
137            Error::NotFound(String::new()),
138            Error::Internal(String::new()),
139        ];
140        let mut seen = std::collections::BTreeSet::new();
141        for e in &all {
142            let c = e.code();
143            assert!(c.starts_with("LOP-E"), "bad domain: {c}");
144            assert_eq!(c.len(), 8, "bad code length: {c}");
145            assert!(seen.insert(c), "duplicate code: {c}");
146        }
147    }
148}