1use exo_core::{Did, Hash256};
20use thiserror::Error;
21
22use crate::types::{DecisionClass, SemVer};
23
24#[derive(Error, Debug)]
26pub enum GovernanceError {
27 #[error("Invalid state transition: {from} -> {to}")]
29 InvalidTransition { from: String, to: String },
30
31 #[error("Decision {0} is immutable (terminal status reached) — TNC-08")]
32 DecisionImmutable(Hash256),
33
34 #[error("Decision {0} not found")]
35 DecisionNotFound(Hash256),
36
37 #[error("Authority chain verification failed: {reason}")]
39 AuthorityChainBroken { reason: String },
40
41 #[error("Delegation {0} has expired — TNC-05")]
42 DelegationExpired(Hash256),
43
44 #[error("Delegation {0} has been revoked")]
45 DelegationRevoked(Hash256),
46
47 #[error("Delegation {0} not found")]
48 DelegationNotFound(Hash256),
49
50 #[error("Sub-delegation not permitted by parent delegation {0}")]
51 SubDelegationNotPermitted(Hash256),
52
53 #[error("Authority chain exceeds maximum depth of {0} levels")]
54 ChainTooDeep(usize),
55
56 #[error(
58 "Human gate required for {class} decisions but signer {signer} is an AI agent — TNC-02"
59 )]
60 HumanGateViolation { class: DecisionClass, signer: Did },
61
62 #[error(
64 "AI agent delegation ceiling exceeded: action {action} not permitted for AI agents — TNC-09"
65 )]
66 AiCeilingExceeded { action: String },
67
68 #[error("Constitutional constraint {constraint_id} violated: {reason} — TNC-04")]
70 ConstitutionalViolation {
71 constraint_id: String,
72 reason: String,
73 },
74
75 #[error("Constitution version {required} required but {actual} is active")]
76 ConstitutionVersionMismatch { required: SemVer, actual: SemVer },
77
78 #[error("Constitution not found for tenant")]
79 ConstitutionNotFound,
80
81 #[error("Quorum not met: {present} of {required} required members present — TNC-07")]
83 QuorumNotMet { required: u32, present: u32 },
84
85 #[error("Conflict disclosure required before participation by {0} — TNC-06")]
87 ConflictDisclosureRequired(Did),
88
89 #[error("Challenge {0} not found")]
91 ChallengeNotFound(Hash256),
92
93 #[error("Decision {0} is already contested")]
94 AlreadyContested(Hash256),
95
96 #[error("Emergency action requires ratification — TNC-10")]
98 RatificationRequired,
99
100 #[error("Emergency action frequency threshold exceeded: {count} in current quarter")]
101 EmergencyFrequencyExceeded { count: u32 },
102
103 #[error(
105 "Audit chain integrity violation at sequence {sequence}: expected {expected}, got {actual} — TNC-03"
106 )]
107 AuditChainBroken {
108 sequence: u64,
109 expected: Hash256,
110 actual: Hash256,
111 },
112
113 #[error("Deliberation is not open for votes")]
115 DeliberationNotOpen,
116
117 #[error("Duplicate vote from {0}")]
118 DuplicateVote(String),
119
120 #[error("Action not found: {0}")]
122 ActionNotFound(String),
123
124 #[error("Serialization error: {0}")]
126 Serialization(String),
127
128 #[error("Signature verification failed")]
130 SignatureVerificationFailed,
131
132 #[error("Invalid governance metadata for {field}: {reason}")]
134 InvalidGovernanceMetadata { field: String, reason: String },
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn invalid_transition_display_does_not_debug_quote_labels() {
143 let err = GovernanceError::InvalidTransition {
144 from: "Filed".to_string(),
145 to: "Withdrawn".to_string(),
146 };
147 assert_eq!(
148 err.to_string(),
149 "Invalid state transition: Filed -> Withdrawn"
150 );
151 }
152
153 #[test]
154 fn governance_error_display_does_not_depend_on_debug_formatting() {
155 let source = include_str!("errors.rs")
156 .split("#[cfg(test)]")
157 .next()
158 .expect("production section");
159 for forbidden in [
160 "{from:?}",
161 "{to:?}",
162 "Decision {0:?}",
163 "Delegation {0:?}",
164 "parent delegation {0:?}",
165 "{class:?}",
166 "Challenge {0:?}",
167 "{expected:?}",
168 "{actual:?}",
169 ] {
170 assert!(
171 !source.contains(forbidden),
172 "governance errors must use explicit stable Display labels: {forbidden}"
173 );
174 }
175 }
176}