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 NotFound(String),
45 Internal(String),
47}
48
49impl Error {
50 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 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}