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    /// No recommendation exists at the given hash.
44    NotFound(String),
45    /// An unexpected internal fault (should not happen — file a bug).
46    Internal(String),
47}
48
49impl Error {
50    /// Stable machine-readable code in `LOP-Ennn` form.
51    pub fn code(&self) -> &'static str {
52        match self {
53            Error::Substrate(_) => "LOP-E001",
54            Error::CalUnsupported(_) => "LOP-E002",
55            Error::InvalidTargetRef(_) => "LOP-E010",
56            Error::InvalidProposal(_) => "LOP-E011",
57            Error::InvalidRecommendation(_) => "LOP-E012",
58            Error::LifecycleViolation(_) => "LOP-E020",
59            Error::SelfApproval(_) => "LOP-E021",
60            Error::ScopeDenied(_) => "LOP-E022",
61            Error::DestructiveGated(_) => "LOP-E023",
62            Error::AnalyzerFailed { .. } => "LOP-E030",
63            Error::ParamInvalid(_) => "LOP-E031",
64            Error::CapabilityMissing(_) => "LOP-E032",
65            Error::LlmBackend(_) => "LOP-E050",
66            Error::NotFound(_) => "LOP-E040",
67            Error::Internal(_) => "LOP-E099",
68        }
69    }
70}
71
72impl fmt::Display for Error {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        let code = self.code();
75        match self {
76            Error::Substrate(m) => write!(f, "{code} substrate call failed: {m}"),
77            Error::CalUnsupported(m) => write!(f, "{code} CAL not supported by substrate: {m}"),
78            Error::InvalidTargetRef(m) => write!(f, "{code} invalid target_ref: {m}"),
79            Error::InvalidProposal(m) => write!(f, "{code} invalid proposal: {m}"),
80            Error::InvalidRecommendation(m) => write!(f, "{code} invalid recommendation: {m}"),
81            Error::LifecycleViolation(m) => write!(f, "{code} illegal lifecycle transition: {m}"),
82            Error::SelfApproval(m) => write!(f, "{code} self-approval blocked: {m}"),
83            Error::ScopeDenied(m) => write!(f, "{code} scope denied: {m}"),
84            Error::DestructiveGated(m) => write!(f, "{code} destructive apply gated: {m}"),
85            Error::AnalyzerFailed { id, message } => {
86                write!(f, "{code} analyzer {id} failed: {message}")
87            }
88            Error::ParamInvalid(m) => write!(f, "{code} analyzer parameter invalid: {m}"),
89            Error::CapabilityMissing(m) => write!(f, "{code} required capability missing: {m}"),
90            Error::LlmBackend(m) => write!(f, "{code} LLM backend error: {m}"),
91            Error::NotFound(m) => write!(f, "{code} recommendation not found: {m}"),
92            Error::Internal(m) => write!(f, "{code} internal error: {m}"),
93        }
94    }
95}
96
97impl std::error::Error for Error {}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn display_leads_with_code() {
105        let e = Error::InvalidTargetRef("nope".into());
106        assert!(e.to_string().starts_with("LOP-E010 "));
107        assert_eq!(e.code(), "LOP-E010");
108    }
109
110    #[test]
111    fn codes_are_unique_and_well_formed() {
112        // One representative per variant; guards against copy-paste collisions.
113        let all = [
114            Error::Substrate(String::new()),
115            Error::CalUnsupported(String::new()),
116            Error::InvalidTargetRef(String::new()),
117            Error::InvalidProposal(String::new()),
118            Error::InvalidRecommendation(String::new()),
119            Error::LifecycleViolation(String::new()),
120            Error::SelfApproval(String::new()),
121            Error::ScopeDenied(String::new()),
122            Error::DestructiveGated(String::new()),
123            Error::AnalyzerFailed {
124                id: String::new(),
125                message: String::new(),
126            },
127            Error::ParamInvalid(String::new()),
128            Error::CapabilityMissing(String::new()),
129            Error::LlmBackend(String::new()),
130            Error::NotFound(String::new()),
131            Error::Internal(String::new()),
132        ];
133        let mut seen = std::collections::BTreeSet::new();
134        for e in &all {
135            let c = e.code();
136            assert!(c.starts_with("LOP-E"), "bad domain: {c}");
137            assert_eq!(c.len(), 8, "bad code length: {c}");
138            assert!(seen.insert(c), "duplicate code: {c}");
139        }
140    }
141}