Skip to main content

trellis_core/
error.rs

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