Skip to main content

trellis_core/
error.rs

1use crate::{DeriveError, NodeId, OutputKey, ScopeId, TransactionId};
2use core::fmt;
3
4/// Result type used by graph metadata operations.
5pub type GraphResult<T> = Result<T, GraphError>;
6
7/// Top-level error category for deterministic failure handling.
8#[derive(Copy, Clone, Debug, Eq, PartialEq)]
9pub enum ErrorCategory {
10    /// Public API misuse or invalid graph references.
11    ProgrammerError,
12    /// User-defined derivation failed.
13    DeriveError,
14    /// User-defined resource planning failed.
15    PlanError,
16    /// User-defined output materialization failed.
17    OutputError,
18    /// Host-reported resource status, modeled as canonical input.
19    HostResourceStatus,
20}
21
22/// Deterministic audit event for a failed transaction.
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct ErrorAuditEvent {
25    /// Error category.
26    pub category: ErrorCategory,
27    /// Stable target involved in the error.
28    pub target: ErrorTarget,
29}
30
31/// Stable graph target involved in an error.
32#[derive(Copy, Clone, Debug, Eq, PartialEq)]
33pub enum ErrorTarget {
34    /// No narrower target exists.
35    Graph,
36    /// A node was involved.
37    Node(NodeId),
38    /// A scope was involved.
39    Scope(ScopeId),
40    /// A transaction was involved.
41    Transaction(TransactionId),
42    /// A materialized output was involved.
43    Output(OutputKey),
44}
45
46/// User-defined resource planning failure.
47#[derive(Clone, Debug, Eq, PartialEq)]
48pub enum PlanError {
49    /// Application-defined planning failure.
50    Message(String),
51}
52
53impl PlanError {
54    /// Creates an application-defined planning failure.
55    pub fn message(message: impl Into<String>) -> Self {
56        Self::Message(message.into())
57    }
58}
59
60/// User-defined output materialization failure.
61#[derive(Clone, Debug, Eq, PartialEq)]
62pub enum OutputError {
63    /// A materializer read failed.
64    Read(DeriveError),
65    /// Application-defined output failure.
66    Message(String),
67}
68
69impl OutputError {
70    /// Creates an application-defined output failure.
71    pub fn message(message: impl Into<String>) -> Self {
72        Self::Message(message.into())
73    }
74}
75
76impl From<DeriveError> for OutputError {
77    fn from(error: DeriveError) -> Self {
78        Self::Read(error)
79    }
80}
81
82/// Errors for graph metadata and input transaction operations.
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum GraphError {
85    /// A node id is not present in the graph.
86    UnknownNode(NodeId),
87    /// A scope id is not present in the graph.
88    UnknownScope(ScopeId),
89    /// A dependency list contains the same node more than once.
90    DuplicateDependency(NodeId),
91    /// A node depends on itself.
92    SelfDependency(NodeId),
93    /// A node already has an owning scope.
94    NodeAlreadyAttached(NodeId),
95    /// A scope is closed and cannot accept new nodes.
96    ScopeAlreadyClosed(ScopeId),
97    /// A scope was already closed.
98    ScopeClosed(ScopeId),
99    /// A transaction is already open.
100    NestedTransaction,
101    /// A transaction was already closed and cannot be reused.
102    TransactionClosed(TransactionId),
103    /// A node is not an input node.
104    NotInputNode(NodeId),
105    /// A node is not a derived node.
106    NotDerivedNode(NodeId),
107    /// A node is not a collection node.
108    NotCollectionNode(NodeId),
109    /// An input write used the wrong value type for the node.
110    WrongInputType(NodeId),
111    /// A derived read used the wrong value type for the node.
112    WrongDerivedType(NodeId),
113    /// A collection read used the wrong key or value type for the node.
114    WrongCollectionType(NodeId),
115    /// An output key is not present in the graph.
116    UnknownOutput(OutputKey),
117    /// A materialized output computation failed.
118    OutputFailed(OutputKey, OutputError),
119    /// A resource planner failed.
120    PlanFailed(ScopeId, PlanError),
121    /// A resource command used a scope outside its registered planner scope.
122    ResourceScopeMismatch(ScopeId),
123    /// A resource command required an existing owned resource.
124    ResourceNotOwned,
125    /// A dependency cycle was detected.
126    CycleDetected(NodeId),
127    /// A scalar derived node declared a collection dependency.
128    CollectionDependencyNotAllowed(NodeId),
129    /// A pure derive function failed.
130    DeriveFailed(NodeId, DeriveError),
131    /// A pure collection function failed.
132    CollectionFailed(NodeId, DeriveError),
133    /// Incremental derived state differs from full recompute.
134    FullRecomputeMismatch(NodeId),
135}
136
137impl fmt::Display for GraphError {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        match self {
140            Self::UnknownNode(id) => write!(f, "unknown node: {id:?}"),
141            Self::UnknownScope(id) => write!(f, "unknown scope: {id:?}"),
142            Self::DuplicateDependency(id) => write!(f, "duplicate dependency: {id:?}"),
143            Self::SelfDependency(id) => write!(f, "self dependency: {id:?}"),
144            Self::NodeAlreadyAttached(id) => write!(f, "node already attached: {id:?}"),
145            Self::ScopeAlreadyClosed(id) => write!(f, "scope already closed: {id:?}"),
146            Self::ScopeClosed(id) => write!(f, "scope already closed: {id:?}"),
147            Self::NestedTransaction => write!(f, "a transaction is already open"),
148            Self::TransactionClosed(id) => write!(f, "transaction already closed: {id:?}"),
149            Self::NotInputNode(id) => write!(f, "node is not an input: {id:?}"),
150            Self::NotDerivedNode(id) => write!(f, "node is not derived: {id:?}"),
151            Self::NotCollectionNode(id) => write!(f, "node is not a collection: {id:?}"),
152            Self::WrongInputType(id) => write!(f, "wrong input value type for node: {id:?}"),
153            Self::WrongDerivedType(id) => write!(f, "wrong derived value type for node: {id:?}"),
154            Self::WrongCollectionType(id) => {
155                write!(f, "wrong collection value type for node: {id:?}")
156            }
157            Self::UnknownOutput(key) => write!(f, "unknown output: {key:?}"),
158            Self::OutputFailed(key, error) => write!(f, "output failed for {key:?}: {error:?}"),
159            Self::PlanFailed(scope, error) => {
160                write!(f, "resource planner failed for {scope:?}: {error:?}")
161            }
162            Self::ResourceScopeMismatch(id) => write!(f, "resource scope mismatch: {id:?}"),
163            Self::ResourceNotOwned => write!(f, "resource is not owned"),
164            Self::CycleDetected(id) => write!(f, "dependency cycle detected at node: {id:?}"),
165            Self::CollectionDependencyNotAllowed(id) => {
166                write!(
167                    f,
168                    "collection dependency is not allowed for derived node: {id:?}"
169                )
170            }
171            Self::DeriveFailed(id, error) => write!(f, "derive failed for {id:?}: {error:?}"),
172            Self::CollectionFailed(id, error) => {
173                write!(f, "collection failed for {id:?}: {error:?}")
174            }
175            Self::FullRecomputeMismatch(id) => {
176                write!(f, "full recompute mismatch for node: {id:?}")
177            }
178        }
179    }
180}
181
182impl GraphError {
183    /// Returns this error's top-level category.
184    pub const fn category(&self) -> ErrorCategory {
185        match self {
186            Self::DeriveFailed(_, _) | Self::CollectionFailed(_, _) => ErrorCategory::DeriveError,
187            Self::PlanFailed(_, _) => ErrorCategory::PlanError,
188            Self::OutputFailed(_, _) => ErrorCategory::OutputError,
189            _ => ErrorCategory::ProgrammerError,
190        }
191    }
192
193    /// Returns a deterministic audit event for this error.
194    pub const fn audit_event(&self) -> ErrorAuditEvent {
195        ErrorAuditEvent {
196            category: self.category(),
197            target: match self {
198                Self::UnknownNode(node)
199                | Self::DuplicateDependency(node)
200                | Self::SelfDependency(node)
201                | Self::NodeAlreadyAttached(node)
202                | Self::NotInputNode(node)
203                | Self::NotDerivedNode(node)
204                | Self::NotCollectionNode(node)
205                | Self::WrongInputType(node)
206                | Self::WrongDerivedType(node)
207                | Self::WrongCollectionType(node)
208                | Self::CycleDetected(node)
209                | Self::CollectionDependencyNotAllowed(node)
210                | Self::DeriveFailed(node, _)
211                | Self::CollectionFailed(node, _)
212                | Self::FullRecomputeMismatch(node) => ErrorTarget::Node(*node),
213                Self::UnknownScope(scope)
214                | Self::ScopeAlreadyClosed(scope)
215                | Self::ScopeClosed(scope)
216                | Self::ResourceScopeMismatch(scope)
217                | Self::PlanFailed(scope, _) => ErrorTarget::Scope(*scope),
218                Self::TransactionClosed(transaction) => ErrorTarget::Transaction(*transaction),
219                Self::UnknownOutput(output) | Self::OutputFailed(output, _) => {
220                    ErrorTarget::Output(*output)
221                }
222                Self::NestedTransaction | Self::ResourceNotOwned => ErrorTarget::Graph,
223            },
224        }
225    }
226}
227
228impl std::error::Error for GraphError {}