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/// Shared resource Open payload conflict found during reconciliation.
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct ResourcePayloadConflict {
109    /// Resource key whose live payload disagreed with the joining payload.
110    pub key: ResourceKey,
111    /// Scope that attempted to join the existing resource.
112    pub joining_scope: ScopeId,
113    /// Scopes that already owned the resource.
114    pub existing_owners: Vec<ScopeId>,
115}
116
117/// Errors for graph metadata and input transaction operations.
118#[derive(Clone, Debug, Eq, PartialEq)]
119pub enum GraphError {
120    /// A node id is not present in the graph.
121    UnknownNode(NodeId),
122    /// A scope id is not present in the graph.
123    UnknownScope(ScopeId),
124    /// A dependency list contains the same node more than once.
125    DuplicateDependency(NodeId),
126    /// A node depends on itself.
127    SelfDependency(NodeId),
128    /// A node already has an owning scope.
129    NodeAlreadyAttached(NodeId),
130    /// A scope is closed and cannot accept new nodes.
131    ScopeAlreadyClosed(ScopeId),
132    /// A scope was already closed.
133    ScopeClosed(ScopeId),
134    /// A transaction is already open.
135    NestedTransaction,
136    /// A transaction was already closed and cannot be reused.
137    TransactionClosed(TransactionId),
138    /// A node is not an input node.
139    NotInputNode(NodeId),
140    /// A node is not a derived node.
141    NotDerivedNode(NodeId),
142    /// A node is not a collection node.
143    NotCollectionNode(NodeId),
144    /// An input write used the wrong value type for the node.
145    WrongInputType(NodeId),
146    /// A derived read used the wrong value type for the node.
147    WrongDerivedType(NodeId),
148    /// A collection read used the wrong key or value type for the node.
149    WrongCollectionType(NodeId),
150    /// An output key is not present in the graph.
151    UnknownOutput(OutputKey),
152    /// A materialized output computation failed.
153    OutputFailed(OutputKey, OutputError),
154    /// A resource planner failed.
155    PlanFailed(ScopeId, PlanError),
156    /// A resource command used a scope outside its registered planner scope.
157    ResourceScopeMismatch(ScopeId),
158    /// A resource command required an existing owned resource.
159    ResourceNotOwned {
160        /// Resource key that was required to be owned.
161        key: ResourceKey,
162        /// Scope that emitted the command.
163        scope: ScopeId,
164        /// Kind of resource command that required ownership.
165        command_kind: ResourceCommandKind,
166    },
167    /// An Open attempted to join a shared resource with a different payload.
168    ResourcePayloadConflict(ResourcePayloadConflict),
169    /// A dependency cycle was detected.
170    CycleDetected(NodeId),
171    /// A scalar derived node declared a collection dependency.
172    CollectionDependencyNotAllowed(NodeId),
173    /// A pure derive function failed.
174    DeriveFailed(NodeId, DeriveError),
175    /// A pure collection function failed.
176    CollectionFailed(NodeId, DeriveError),
177    /// Incremental derived state differs from full recompute.
178    FullRecomputeMismatch(NodeId),
179    /// Incremental resource-owner state differs from full recompute.
180    FullRecomputeResourceMismatch(FullRecomputeResourceMismatch),
181    /// Incremental materialized-output state differs from full recompute.
182    FullRecomputeOutputMismatch(FullRecomputeOutputMismatch),
183}
184
185impl fmt::Display for GraphError {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        match self {
188            Self::UnknownNode(id) => write!(f, "unknown node: {id:?}"),
189            Self::UnknownScope(id) => write!(f, "unknown scope: {id:?}"),
190            Self::DuplicateDependency(id) => write!(f, "duplicate dependency: {id:?}"),
191            Self::SelfDependency(id) => write!(f, "self dependency: {id:?}"),
192            Self::NodeAlreadyAttached(id) => write!(f, "node already attached: {id:?}"),
193            Self::ScopeAlreadyClosed(id) => write!(f, "scope already closed: {id:?}"),
194            Self::ScopeClosed(id) => write!(f, "scope already closed: {id:?}"),
195            Self::NestedTransaction => write!(f, "a transaction is already open"),
196            Self::TransactionClosed(id) => write!(f, "transaction already closed: {id:?}"),
197            Self::NotInputNode(id) => write!(f, "node is not an input: {id:?}"),
198            Self::NotDerivedNode(id) => write!(f, "node is not derived: {id:?}"),
199            Self::NotCollectionNode(id) => write!(f, "node is not a collection: {id:?}"),
200            Self::WrongInputType(id) => write!(f, "wrong input value type for node: {id:?}"),
201            Self::WrongDerivedType(id) => write!(f, "wrong derived value type for node: {id:?}"),
202            Self::WrongCollectionType(id) => {
203                write!(f, "wrong collection value type for node: {id:?}")
204            }
205            Self::UnknownOutput(key) => write!(f, "unknown output: {key:?}"),
206            Self::OutputFailed(key, error) => write!(f, "output failed for {key:?}: {error:?}"),
207            Self::PlanFailed(scope, error) => {
208                write!(f, "resource planner failed for {scope:?}: {error:?}")
209            }
210            Self::ResourceScopeMismatch(id) => write!(f, "resource scope mismatch: {id:?}"),
211            Self::ResourceNotOwned {
212                key,
213                scope,
214                command_kind,
215            } => write!(
216                f,
217                "resource is not owned: key {key:?}, scope {scope:?}, command {command_kind:?}"
218            ),
219            Self::ResourcePayloadConflict(conflict) => write!(
220                f,
221                "resource payload conflict: key {:?}, joining scope {:?}, existing owners {:?}",
222                conflict.key, conflict.joining_scope, conflict.existing_owners
223            ),
224            Self::CycleDetected(id) => write!(f, "dependency cycle detected at node: {id:?}"),
225            Self::CollectionDependencyNotAllowed(id) => {
226                write!(
227                    f,
228                    "collection dependency is not allowed for derived node: {id:?}"
229                )
230            }
231            Self::DeriveFailed(id, error) => write!(f, "derive failed for {id:?}: {error:?}"),
232            Self::CollectionFailed(id, error) => {
233                write!(f, "collection failed for {id:?}: {error:?}")
234            }
235            Self::FullRecomputeMismatch(id) => {
236                write!(f, "full recompute mismatch for node: {id:?}")
237            }
238            Self::FullRecomputeResourceMismatch(mismatch) => write!(
239                f,
240                "full recompute resource mismatch for key: {:?}",
241                mismatch.key
242            ),
243            Self::FullRecomputeOutputMismatch(mismatch) => write!(
244                f,
245                "full recompute output mismatch for key: {:?}",
246                mismatch.key
247            ),
248        }
249    }
250}
251
252impl GraphError {
253    /// Returns this error's top-level category.
254    pub const fn category(&self) -> ErrorCategory {
255        match self {
256            Self::DeriveFailed(_, _) | Self::CollectionFailed(_, _) => ErrorCategory::DeriveError,
257            Self::PlanFailed(_, _) => ErrorCategory::PlanError,
258            Self::OutputFailed(_, _) => ErrorCategory::OutputError,
259            _ => ErrorCategory::ProgrammerError,
260        }
261    }
262
263    /// Returns a deterministic audit event for this error.
264    pub const fn audit_event(&self) -> ErrorAuditEvent {
265        ErrorAuditEvent {
266            category: self.category(),
267            target: match self {
268                Self::UnknownNode(node)
269                | Self::DuplicateDependency(node)
270                | Self::SelfDependency(node)
271                | Self::NodeAlreadyAttached(node)
272                | Self::NotInputNode(node)
273                | Self::NotDerivedNode(node)
274                | Self::NotCollectionNode(node)
275                | Self::WrongInputType(node)
276                | Self::WrongDerivedType(node)
277                | Self::WrongCollectionType(node)
278                | Self::CycleDetected(node)
279                | Self::CollectionDependencyNotAllowed(node)
280                | Self::DeriveFailed(node, _)
281                | Self::CollectionFailed(node, _)
282                | Self::FullRecomputeMismatch(node) => ErrorTarget::Node(*node),
283                Self::UnknownScope(scope)
284                | Self::ScopeAlreadyClosed(scope)
285                | Self::ScopeClosed(scope)
286                | Self::ResourceScopeMismatch(scope)
287                | Self::PlanFailed(scope, _) => ErrorTarget::Scope(*scope),
288                Self::ResourceNotOwned { scope, .. } => ErrorTarget::Scope(*scope),
289                Self::ResourcePayloadConflict(conflict) => {
290                    ErrorTarget::Scope(conflict.joining_scope)
291                }
292                Self::TransactionClosed(transaction) => ErrorTarget::Transaction(*transaction),
293                Self::UnknownOutput(output) | Self::OutputFailed(output, _) => {
294                    ErrorTarget::Output(*output)
295                }
296                Self::FullRecomputeOutputMismatch(mismatch) => ErrorTarget::Output(mismatch.key),
297                Self::NestedTransaction | Self::FullRecomputeResourceMismatch(_) => {
298                    ErrorTarget::Graph
299                }
300            },
301        }
302    }
303}
304
305impl std::error::Error for GraphError {}