1use crate::types::{ArenaOffset, NodeId, TraceId};
8use std::path::PathBuf;
9use thiserror::Error;
10
11#[derive(Error, Debug)]
13pub enum XervError {
14 #[error("E001: Failed to create arena at {path}: {cause}")]
19 ArenaCreate {
20 path: PathBuf,
22 cause: String,
24 },
25
26 #[error("E002: Failed to mmap arena at {path}: {cause}")]
28 ArenaMmap {
29 path: PathBuf,
31 cause: String,
33 },
34
35 #[error("E003: Arena write failed at offset {offset} for trace {trace_id}: {cause}")]
37 ArenaWrite {
38 trace_id: TraceId,
40 offset: ArenaOffset,
42 cause: String,
44 },
45
46 #[error("E004: Arena read failed at offset {offset}: {cause}")]
48 ArenaRead {
49 offset: ArenaOffset,
51 cause: String,
53 },
54
55 #[error(
57 "E005: Arena capacity exceeded: requested {requested} bytes, available {available} bytes"
58 )]
59 ArenaCapacity {
60 requested: u64,
62 available: u64,
64 },
65
66 #[error("E006: Invalid arena offset {offset}: {cause}")]
68 ArenaInvalidOffset {
69 offset: ArenaOffset,
71 cause: String,
73 },
74
75 #[error("E007: Arena corruption detected at offset {offset}: {cause}")]
77 ArenaCorruption {
78 offset: ArenaOffset,
80 cause: String,
82 },
83
84 #[error("E101: Selector resolution failed for '{selector}' in node {node_id}: {cause}")]
89 SelectorResolution {
90 selector: String,
92 node_id: NodeId,
94 cause: String,
96 },
97
98 #[error("E102: Non-deterministic layout for type '{type_name}': {cause}")]
100 NonDeterministicLayout {
101 type_name: String,
103 cause: String,
105 },
106
107 #[error("E103: Invalid selector syntax '{selector}': {cause}")]
109 SelectorSyntax {
110 selector: String,
112 cause: String,
114 },
115
116 #[error("E104: Selector target '{field}' not found in output of node {node_id}")]
118 SelectorTargetNotFound {
119 field: String,
121 node_id: NodeId,
123 },
124
125 #[error("E105: Type mismatch for selector '{selector}': expected {expected}, got {actual}")]
127 SelectorTypeMismatch {
128 selector: String,
130 expected: String,
132 actual: String,
134 },
135
136 #[error("E110: Schema version '{schema}@{version}' not found")]
141 SchemaVersionNotFound {
142 schema: String,
144 version: String,
146 },
147
148 #[error("E111: Schema incompatible: {from} → {to}, no migration available")]
150 SchemaIncompatible {
151 from: String,
153 to: String,
155 cause: String,
157 },
158
159 #[error("E112: Migration failed from {from} to {to}: {cause}")]
161 MigrationFailed {
162 from: String,
164 to: String,
166 cause: String,
168 },
169
170 #[error("E113: Breaking change in {schema}: {change}")]
172 BreakingSchemaChange {
173 schema: String,
175 change: String,
177 },
178
179 #[error("E114: Migration path from {from} to {to} exceeds max hops ({max_hops})")]
181 MigrationPathTooLong {
182 from: String,
184 to: String,
186 max_hops: u32,
188 },
189
190 #[error("E201: Missing binary '{binary}': {cause}")]
195 MissingBinary {
196 binary: String,
198 cause: String,
200 },
201
202 #[error("E202: Runtime mismatch for '{runtime}': required {required}, found {found}")]
204 RuntimeMismatch {
205 runtime: String,
207 required: String,
209 found: String,
211 },
212
213 #[error("E203: Container image unavailable: {image}")]
215 ContainerImageUnavailable {
216 image: String,
218 },
219
220 #[error("E204: Unsupported execution profile '{profile}' on this host")]
222 UnsupportedProfile {
223 profile: String,
225 },
226
227 #[error("E301: Node {node_id} execution failed in trace {trace_id}: {cause}")]
232 NodeExecution {
233 node_id: NodeId,
235 trace_id: TraceId,
237 cause: String,
239 },
240
241 #[error("E302: Node {node_id} timed out after {timeout_ms}ms in trace {trace_id}")]
243 NodeTimeout {
244 node_id: NodeId,
246 trace_id: TraceId,
248 timeout_ms: u64,
250 },
251
252 #[error("E303: Node {node_id} panicked in trace {trace_id}: {message}")]
254 NodePanic {
255 node_id: NodeId,
257 trace_id: TraceId,
259 message: String,
261 },
262
263 #[error("E304: Invalid configuration for node {node_id}: {cause}")]
265 NodeConfig {
266 node_id: NodeId,
268 cause: String,
270 },
271
272 #[error("E305: Node '{node_name}' not found in flow")]
274 NodeNotFound {
275 node_name: String,
277 },
278
279 #[error("E401: No compatible version found for trace {trace_id}: {cause}")]
284 NoCompatibleVersion {
285 trace_id: TraceId,
287 cause: String,
289 },
290
291 #[error("E402: Invalid flow topology: {cause}")]
293 InvalidTopology {
294 cause: String,
296 },
297
298 #[error("E403: Uncontrolled cycle detected involving nodes: {nodes:?}")]
300 UncontrolledCycle {
301 nodes: Vec<NodeId>,
303 },
304
305 #[error("E404: Invalid port '{port}' on node {node_id}")]
307 InvalidPort {
308 port: String,
310 node_id: NodeId,
312 },
313
314 #[error("E405: Missing required edge from {from_node}.{from_port} to {to_node}.{to_port}")]
316 MissingEdge {
317 from_node: NodeId,
319 from_port: String,
321 to_node: NodeId,
323 to_port: String,
325 },
326
327 #[error("E406: Invalid edge from {from_node}.{from_port} to {to_node}.{to_port}")]
329 InvalidEdge {
330 from_node: NodeId,
332 from_port: String,
334 to_node: NodeId,
336 to_port: String,
338 },
339
340 #[error("E501: Pipeline '{pipeline_id}' not found")]
345 PipelineNotFound {
346 pipeline_id: String,
348 },
349
350 #[error("E502: Pipeline '{pipeline_id}' already exists")]
352 PipelineExists {
353 pipeline_id: String,
355 },
356
357 #[error("E503: Pipeline '{pipeline_id}' concurrency limit reached: {current}/{max}")]
359 ConcurrencyLimit {
360 pipeline_id: String,
362 current: u32,
364 max: u32,
366 },
367
368 #[error("E504: Pipeline '{pipeline_id}' circuit breaker open: error rate {error_rate:.1}%")]
370 CircuitBreakerOpen {
371 pipeline_id: String,
373 error_rate: f64,
375 },
376
377 #[error("E505: Pipeline '{pipeline_id}' drain timeout: {pending_traces} traces still pending")]
379 DrainTimeout {
380 pipeline_id: String,
382 pending_traces: u32,
384 },
385
386 #[error("E601: WAL write failed for trace {trace_id}: {cause}")]
391 WalWrite {
392 trace_id: TraceId,
394 cause: String,
396 },
397
398 #[error("E602: WAL read failed: {cause}")]
400 WalRead {
401 cause: String,
403 },
404
405 #[error("E603: WAL corruption detected at position {position}: {cause}")]
407 WalCorruption {
408 position: u64,
410 cause: String,
412 },
413
414 #[error("E604: WAL replay failed for trace {trace_id}: {cause}")]
416 WalReplay {
417 trace_id: TraceId,
419 cause: String,
421 },
422
423 #[error("E701: Failed to load WASM module '{module}': {cause}")]
428 WasmLoad {
429 module: String,
431 cause: String,
433 },
434
435 #[error("E702: WASM execution failed in node {node_id}: {cause}")]
437 WasmExecution {
438 node_id: NodeId,
440 cause: String,
442 },
443
444 #[error("E703: WASM memory allocation failed: requested {requested} bytes")]
446 WasmMemoryAlloc {
447 requested: u64,
449 },
450
451 #[error("E704: WASM host function '{function}' failed: {cause}")]
453 WasmHostFunction {
454 function: String,
456 cause: String,
458 },
459
460 #[error("E801: Failed to parse YAML at {path}: {cause}")]
465 YamlParse {
466 path: PathBuf,
468 cause: String,
470 },
471
472 #[error("E802: Invalid configuration '{field}': {cause}")]
474 ConfigValue {
475 field: String,
477 cause: String,
479 },
480
481 #[error("E803: Schema validation failed for '{schema}': {cause}")]
483 SchemaValidation {
484 schema: String,
486 cause: String,
488 },
489
490 #[error("E804: Serialization error: {0}")]
492 Serialization(
493 String,
495 ),
496
497 #[error("E901: I/O error at {path}: {cause}")]
502 Io {
503 path: PathBuf,
505 cause: String,
507 },
508
509 #[error("E902: Network error: {cause}")]
511 Network {
512 cause: String,
514 },
515}
516
517impl XervError {
518 #[must_use]
520 pub fn code(&self) -> &'static str {
521 match self {
522 Self::ArenaCreate { .. } => "E001",
523 Self::ArenaMmap { .. } => "E002",
524 Self::ArenaWrite { .. } => "E003",
525 Self::ArenaRead { .. } => "E004",
526 Self::ArenaCapacity { .. } => "E005",
527 Self::ArenaInvalidOffset { .. } => "E006",
528 Self::ArenaCorruption { .. } => "E007",
529 Self::SelectorResolution { .. } => "E101",
530 Self::NonDeterministicLayout { .. } => "E102",
531 Self::SelectorSyntax { .. } => "E103",
532 Self::SelectorTargetNotFound { .. } => "E104",
533 Self::SelectorTypeMismatch { .. } => "E105",
534 Self::SchemaVersionNotFound { .. } => "E110",
535 Self::SchemaIncompatible { .. } => "E111",
536 Self::MigrationFailed { .. } => "E112",
537 Self::BreakingSchemaChange { .. } => "E113",
538 Self::MigrationPathTooLong { .. } => "E114",
539 Self::MissingBinary { .. } => "E201",
540 Self::RuntimeMismatch { .. } => "E202",
541 Self::ContainerImageUnavailable { .. } => "E203",
542 Self::UnsupportedProfile { .. } => "E204",
543 Self::NodeExecution { .. } => "E301",
544 Self::NodeTimeout { .. } => "E302",
545 Self::NodePanic { .. } => "E303",
546 Self::NodeConfig { .. } => "E304",
547 Self::NodeNotFound { .. } => "E305",
548 Self::NoCompatibleVersion { .. } => "E401",
549 Self::InvalidTopology { .. } => "E402",
550 Self::UncontrolledCycle { .. } => "E403",
551 Self::InvalidPort { .. } => "E404",
552 Self::MissingEdge { .. } => "E405",
553 Self::PipelineNotFound { .. } => "E501",
554 Self::PipelineExists { .. } => "E502",
555 Self::ConcurrencyLimit { .. } => "E503",
556 Self::CircuitBreakerOpen { .. } => "E504",
557 Self::DrainTimeout { .. } => "E505",
558 Self::WalWrite { .. } => "E601",
559 Self::WalRead { .. } => "E602",
560 Self::WalCorruption { .. } => "E603",
561 Self::WalReplay { .. } => "E604",
562 Self::WasmLoad { .. } => "E701",
563 Self::WasmExecution { .. } => "E702",
564 Self::WasmMemoryAlloc { .. } => "E703",
565 Self::WasmHostFunction { .. } => "E704",
566 Self::YamlParse { .. } => "E801",
567 Self::ConfigValue { .. } => "E802",
568 Self::SchemaValidation { .. } => "E803",
569 Self::Serialization(_) => "E804",
570 Self::Io { .. } => "E901",
571 Self::Network { .. } => "E902",
572 Self::InvalidEdge { .. } => "E406",
573 }
574 }
575
576 #[must_use]
578 pub fn is_retriable(&self) -> bool {
579 matches!(
580 self,
581 Self::ArenaWrite { .. }
582 | Self::NodeTimeout { .. }
583 | Self::WalWrite { .. }
584 | Self::Network { .. }
585 )
586 }
587
588 #[must_use]
590 pub fn is_config_error(&self) -> bool {
591 matches!(
592 self,
593 Self::SelectorResolution { .. }
594 | Self::NonDeterministicLayout { .. }
595 | Self::SelectorSyntax { .. }
596 | Self::SelectorTargetNotFound { .. }
597 | Self::SelectorTypeMismatch { .. }
598 | Self::SchemaVersionNotFound { .. }
599 | Self::BreakingSchemaChange { .. }
600 | Self::InvalidTopology { .. }
601 | Self::UncontrolledCycle { .. }
602 | Self::InvalidPort { .. }
603 | Self::MissingEdge { .. }
604 | Self::YamlParse { .. }
605 | Self::ConfigValue { .. }
606 | Self::SchemaValidation { .. }
607 )
608 }
609
610 #[must_use]
612 pub fn is_schema_error(&self) -> bool {
613 matches!(
614 self,
615 Self::SchemaVersionNotFound { .. }
616 | Self::SchemaIncompatible { .. }
617 | Self::MigrationFailed { .. }
618 | Self::BreakingSchemaChange { .. }
619 | Self::MigrationPathTooLong { .. }
620 | Self::SchemaValidation { .. }
621 )
622 }
623}
624
625pub type Result<T> = std::result::Result<T, XervError>;
627
628pub trait ResultExt<T> {
630 fn with_trace(self, trace_id: TraceId) -> Result<T>;
632
633 fn with_node(self, node_id: NodeId) -> Result<T>;
635}
636
637impl<T, E: std::fmt::Display> ResultExt<T> for std::result::Result<T, E> {
638 fn with_trace(self, trace_id: TraceId) -> Result<T> {
639 self.map_err(|e| XervError::NodeExecution {
640 node_id: NodeId::new(0),
641 trace_id,
642 cause: e.to_string(),
643 })
644 }
645
646 fn with_node(self, node_id: NodeId) -> Result<T> {
647 self.map_err(|e| XervError::NodeConfig {
648 node_id,
649 cause: e.to_string(),
650 })
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 #[test]
659 fn error_codes_are_correct() {
660 let err = XervError::ArenaCreate {
661 path: PathBuf::from("/tmp/test"),
662 cause: "test".to_string(),
663 };
664 assert_eq!(err.code(), "E001");
665
666 let err = XervError::SelectorResolution {
667 selector: "${test}".to_string(),
668 node_id: NodeId::new(1),
669 cause: "not found".to_string(),
670 };
671 assert_eq!(err.code(), "E101");
672 }
673
674 #[test]
675 fn error_display() {
676 let err = XervError::NodeTimeout {
677 node_id: NodeId::new(5),
678 trace_id: TraceId::new(),
679 timeout_ms: 5000,
680 };
681 let msg = format!("{}", err);
682 assert!(msg.contains("E302"));
683 assert!(msg.contains("node_5"));
684 assert!(msg.contains("5000ms"));
685 }
686
687 #[test]
688 fn retriable_errors() {
689 assert!(
690 XervError::Network {
691 cause: "timeout".to_string()
692 }
693 .is_retriable()
694 );
695
696 assert!(
697 !XervError::InvalidTopology {
698 cause: "cycle".to_string()
699 }
700 .is_retriable()
701 );
702 }
703
704 #[test]
705 fn config_errors() {
706 assert!(
707 XervError::YamlParse {
708 path: PathBuf::from("test.yaml"),
709 cause: "syntax".to_string()
710 }
711 .is_config_error()
712 );
713
714 assert!(
715 !XervError::NodeExecution {
716 node_id: NodeId::new(1),
717 trace_id: TraceId::new(),
718 cause: "failed".to_string()
719 }
720 .is_config_error()
721 );
722 }
723}