1use std::fmt;
9
10pub type Result<T> = std::result::Result<T, Error>;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Error {
15 Substrate(String),
17 CalUnsupported(String),
20 InvalidTargetRef(String),
22 InvalidProposal(String),
24 InvalidRecommendation(String),
26 LifecycleViolation(String),
28 SelfApproval(String),
30 ScopeDenied(String),
32 DestructiveGated(String),
34 AnalyzerFailed { id: String, message: String },
36 ParamInvalid(String),
38 CapabilityMissing(String),
40 LlmBackend(String),
43 DecideBackend(String),
47 NotFound(String),
49 Internal(String),
51}
52
53impl Error {
54 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 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}