Skip to main content

exo_governance/
errors.rs

1// Copyright 2026 Exochain Foundation
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at:
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! Governance error types (canonical, TNC-aware).
18
19use exo_core::{Did, Hash256};
20use thiserror::Error;
21
22use crate::types::{DecisionClass, SemVer};
23
24/// Canonical error type for all governance operations, covering TNC compliance violations.
25#[derive(Error, Debug)]
26pub enum GovernanceError {
27    // --- Decision lifecycle errors ---
28    #[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    // --- Authority chain errors ---
38    #[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    // --- Human gate errors (TNC-02) ---
57    #[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    // --- AI ceiling errors (TNC-09) ---
63    #[error(
64        "AI agent delegation ceiling exceeded: action {action} not permitted for AI agents — TNC-09"
65    )]
66    AiCeilingExceeded { action: String },
67
68    // --- Constitutional errors ---
69    #[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    // --- Quorum errors (TNC-07) ---
82    #[error("Quorum not met: {present} of {required} required members present — TNC-07")]
83    QuorumNotMet { required: u32, present: u32 },
84
85    // --- Conflict disclosure errors (TNC-06) ---
86    #[error("Conflict disclosure required before participation by {0} — TNC-06")]
87    ConflictDisclosureRequired(Did),
88
89    // --- Challenge errors ---
90    #[error("Challenge {0} not found")]
91    ChallengeNotFound(Hash256),
92
93    #[error("Decision {0} is already contested")]
94    AlreadyContested(Hash256),
95
96    // --- Emergency errors (TNC-10) ---
97    #[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    // --- Audit errors (TNC-03) ---
104    #[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    // --- Deliberation errors ---
114    #[error("Deliberation is not open for votes")]
115    DeliberationNotOpen,
116
117    #[error("Duplicate vote from {0}")]
118    DuplicateVote(String),
119
120    // --- Succession / action errors ---
121    #[error("Action not found: {0}")]
122    ActionNotFound(String),
123
124    // --- Serialization errors ---
125    #[error("Serialization error: {0}")]
126    Serialization(String),
127
128    // --- Crypto errors ---
129    #[error("Signature verification failed")]
130    SignatureVerificationFailed,
131
132    // --- Deterministic metadata errors ---
133    #[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}