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/// Host-observed resource status that can be written as canonical input.
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum HostResourceStatus {
85    /// The host has not reported a resource outcome.
86    Unknown,
87    /// The resource is live according to the host.
88    Open,
89    /// The resource failed outside graph propagation.
90    Failed(String),
91    /// The resource is closed according to the host.
92    Closed,
93}
94
95impl HostResourceStatus {
96    /// Returns the model category for host-reported resource status.
97    pub const fn category(&self) -> ErrorCategory {
98        ErrorCategory::HostResourceStatus
99    }
100}
101
102/// Errors for graph metadata and input transaction operations.
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub enum GraphError {
105    /// A node id is not present in the graph.
106    UnknownNode(NodeId),
107    /// A scope id is not present in the graph.
108    UnknownScope(ScopeId),
109    /// A dependency list contains the same node more than once.
110    DuplicateDependency(NodeId),
111    /// A node depends on itself.
112    SelfDependency(NodeId),
113    /// A node already has an owning scope.
114    NodeAlreadyAttached(NodeId),
115    /// A scope is closed and cannot accept new nodes.
116    ScopeAlreadyClosed(ScopeId),
117    /// A scope was already closed.
118    ScopeClosed(ScopeId),
119    /// A transaction is already open.
120    NestedTransaction,
121    /// A transaction was already closed and cannot be reused.
122    TransactionClosed(TransactionId),
123    /// A node is not an input node.
124    NotInputNode(NodeId),
125    /// A node is not a derived node.
126    NotDerivedNode(NodeId),
127    /// A node is not a collection node.
128    NotCollectionNode(NodeId),
129    /// An input write used the wrong value type for the node.
130    WrongInputType(NodeId),
131    /// A derived read used the wrong value type for the node.
132    WrongDerivedType(NodeId),
133    /// A collection read used the wrong key or value type for the node.
134    WrongCollectionType(NodeId),
135    /// An output key is not present in the graph.
136    UnknownOutput(OutputKey),
137    /// A materialized output computation failed.
138    OutputFailed(OutputKey, OutputError),
139    /// A resource planner failed.
140    PlanFailed(ScopeId, PlanError),
141    /// A resource command used a scope outside its registered planner scope.
142    ResourceScopeMismatch(ScopeId),
143    /// A resource command required an existing owned resource.
144    ResourceNotOwned,
145    /// A dependency cycle was detected.
146    CycleDetected(NodeId),
147    /// A scalar derived node declared a collection dependency.
148    CollectionDependencyNotAllowed(NodeId),
149    /// A pure derive function failed.
150    DeriveFailed(NodeId, DeriveError),
151    /// A pure collection function failed.
152    CollectionFailed(NodeId, DeriveError),
153    /// Incremental derived state differs from full recompute.
154    FullRecomputeMismatch(NodeId),
155}
156
157impl fmt::Display for GraphError {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        match self {
160            Self::UnknownNode(id) => write!(f, "unknown node: {id:?}"),
161            Self::UnknownScope(id) => write!(f, "unknown scope: {id:?}"),
162            Self::DuplicateDependency(id) => write!(f, "duplicate dependency: {id:?}"),
163            Self::SelfDependency(id) => write!(f, "self dependency: {id:?}"),
164            Self::NodeAlreadyAttached(id) => write!(f, "node already attached: {id:?}"),
165            Self::ScopeAlreadyClosed(id) => write!(f, "scope already closed: {id:?}"),
166            Self::ScopeClosed(id) => write!(f, "scope already closed: {id:?}"),
167            Self::NestedTransaction => write!(f, "a transaction is already open"),
168            Self::TransactionClosed(id) => write!(f, "transaction already closed: {id:?}"),
169            Self::NotInputNode(id) => write!(f, "node is not an input: {id:?}"),
170            Self::NotDerivedNode(id) => write!(f, "node is not derived: {id:?}"),
171            Self::NotCollectionNode(id) => write!(f, "node is not a collection: {id:?}"),
172            Self::WrongInputType(id) => write!(f, "wrong input value type for node: {id:?}"),
173            Self::WrongDerivedType(id) => write!(f, "wrong derived value type for node: {id:?}"),
174            Self::WrongCollectionType(id) => {
175                write!(f, "wrong collection value type for node: {id:?}")
176            }
177            Self::UnknownOutput(key) => write!(f, "unknown output: {key:?}"),
178            Self::OutputFailed(key, error) => write!(f, "output failed for {key:?}: {error:?}"),
179            Self::PlanFailed(scope, error) => {
180                write!(f, "resource planner failed for {scope:?}: {error:?}")
181            }
182            Self::ResourceScopeMismatch(id) => write!(f, "resource scope mismatch: {id:?}"),
183            Self::ResourceNotOwned => write!(f, "resource is not owned"),
184            Self::CycleDetected(id) => write!(f, "dependency cycle detected at node: {id:?}"),
185            Self::CollectionDependencyNotAllowed(id) => {
186                write!(
187                    f,
188                    "collection dependency is not allowed for derived node: {id:?}"
189                )
190            }
191            Self::DeriveFailed(id, error) => write!(f, "derive failed for {id:?}: {error:?}"),
192            Self::CollectionFailed(id, error) => {
193                write!(f, "collection failed for {id:?}: {error:?}")
194            }
195            Self::FullRecomputeMismatch(id) => {
196                write!(f, "full recompute mismatch for node: {id:?}")
197            }
198        }
199    }
200}
201
202impl GraphError {
203    /// Returns this error's top-level category.
204    pub const fn category(&self) -> ErrorCategory {
205        match self {
206            Self::DeriveFailed(_, _) | Self::CollectionFailed(_, _) => ErrorCategory::DeriveError,
207            Self::PlanFailed(_, _) => ErrorCategory::PlanError,
208            Self::OutputFailed(_, _) => ErrorCategory::OutputError,
209            _ => ErrorCategory::ProgrammerError,
210        }
211    }
212
213    /// Returns a deterministic audit event for this error.
214    pub const fn audit_event(&self) -> ErrorAuditEvent {
215        ErrorAuditEvent {
216            category: self.category(),
217            target: match self {
218                Self::UnknownNode(node)
219                | Self::DuplicateDependency(node)
220                | Self::SelfDependency(node)
221                | Self::NodeAlreadyAttached(node)
222                | Self::NotInputNode(node)
223                | Self::NotDerivedNode(node)
224                | Self::NotCollectionNode(node)
225                | Self::WrongInputType(node)
226                | Self::WrongDerivedType(node)
227                | Self::WrongCollectionType(node)
228                | Self::CycleDetected(node)
229                | Self::CollectionDependencyNotAllowed(node)
230                | Self::DeriveFailed(node, _)
231                | Self::CollectionFailed(node, _)
232                | Self::FullRecomputeMismatch(node) => ErrorTarget::Node(*node),
233                Self::UnknownScope(scope)
234                | Self::ScopeAlreadyClosed(scope)
235                | Self::ScopeClosed(scope)
236                | Self::ResourceScopeMismatch(scope)
237                | Self::PlanFailed(scope, _) => ErrorTarget::Scope(*scope),
238                Self::TransactionClosed(transaction) => ErrorTarget::Transaction(*transaction),
239                Self::UnknownOutput(output) | Self::OutputFailed(output, _) => {
240                    ErrorTarget::Output(*output)
241                }
242                Self::NestedTransaction | Self::ResourceNotOwned => ErrorTarget::Graph,
243            },
244        }
245    }
246}
247
248impl std::error::Error for GraphError {}