Skip to main content

nika_engine/
error_domains.rs

1//! Domain-specific error types for ARCH-3 incremental migration.
2//!
3//! These sub-enums group NikaError variants by domain. Each implements
4//! `From<SubEnum> for NikaError` so new code can use domain errors
5//! while existing code keeps working unchanged.
6//!
7//! # Migration Path
8//!
9//! 1. (THIS COMMIT) Create domain enums + From impls
10//! 2. New code uses `ProviderError::NotConfigured` instead of
11//!    `NikaError::ProviderNotConfigured`
12//! 3. Gradually migrate existing call sites per-module
13//! 4. Eventually make NikaError variants delegate to domain enums
14//!
15//! # Domain Groups
16//!
17//! | Range | Domain | Enum |
18//! |-------|--------|------|
19//! | 001-009 | Workflow | `WorkflowError` |
20//! | 010-019 | Schema | `SchemaError` |
21//! | 020-029 | DAG | `DagError` |
22//! | 030-039 | Provider | `ProviderError` |
23//! | 040-049 | Binding/Template | `BindingError` |
24//! | 050-059 | Path/Security | `SecurityError` |
25//! | 060-069 | Output | `OutputError` |
26//! | 090-099 | Execution | `ExecutionError` |
27//! | 100-109 | MCP | `McpError` |
28//! | 110-119 | Agent | `AgentError` |
29//! | 200-219 | File/Builtin tools | `ToolError` |
30//! | 250-299 | Media | `MediaError` |
31//! | 300-319 | Structured output | `StructuredOutputError` |
32
33use crate::error::NikaError;
34
35// ═══════════════════════════════════════════════════════════════════════════
36// PROVIDER ERRORS (030-039)
37// ═══════════════════════════════════════════════════════════════════════════
38
39/// Provider-related errors (API keys, connections, configuration).
40#[derive(Debug, thiserror::Error)]
41pub enum ProviderError {
42    #[error("[NIKA-030] Provider '{provider}' not configured")]
43    NotConfigured { provider: String },
44
45    #[error("[NIKA-031] Provider API error: {message}")]
46    ApiError { message: String },
47
48    #[error("[NIKA-032] Missing API key for provider '{provider}'")]
49    MissingApiKey { provider: String },
50
51    #[error("[NIKA-033] Invalid configuration: {message}")]
52    InvalidConfig { message: String },
53}
54
55impl From<ProviderError> for NikaError {
56    fn from(e: ProviderError) -> Self {
57        match e {
58            ProviderError::NotConfigured { provider } => {
59                NikaError::ProviderNotConfigured { provider }
60            }
61            ProviderError::ApiError { message } => NikaError::ProviderApiError { message },
62            ProviderError::MissingApiKey { provider } => {
63                NikaError::MissingApiKey { provider }
64            }
65            ProviderError::InvalidConfig { message } => NikaError::InvalidConfig { message },
66        }
67    }
68}
69
70// ═══════════════════════════════════════════════════════════════════════════
71// DAG ERRORS (020-029)
72// ═══════════════════════════════════════════════════════════════════════════
73
74/// DAG structure errors (cycles, missing deps, duplicates).
75#[derive(Debug, thiserror::Error)]
76pub enum DagError {
77    #[error("[NIKA-020] Cycle detected in DAG: {cycle}")]
78    CycleDetected { cycle: String },
79
80    #[error("[NIKA-021] Missing dependency: task '{task_id}' depends on unknown '{dep_id}'")]
81    MissingDependency { task_id: String, dep_id: String },
82
83    #[error("[NIKA-022] Duplicate task ID: '{task_id}' appears multiple times")]
84    DuplicateTaskId { task_id: String },
85}
86
87impl From<DagError> for NikaError {
88    fn from(e: DagError) -> Self {
89        match e {
90            DagError::CycleDetected { cycle } => NikaError::CycleDetected { cycle },
91            DagError::MissingDependency { task_id, dep_id } => {
92                NikaError::MissingDependency { task_id, dep_id }
93            }
94            DagError::DuplicateTaskId { task_id } => NikaError::DuplicateTaskId { task_id },
95        }
96    }
97}
98
99// ═══════════════════════════════════════════════════════════════════════════
100// EXECUTION ERRORS (090-099)
101// ═══════════════════════════════════════════════════════════════════════════
102
103/// Runtime execution errors.
104#[derive(Debug, thiserror::Error)]
105pub enum ExecutionError {
106    #[error("[NIKA-044] Exec error: {reason}")]
107    ExecFailed { reason: String },
108
109    #[error("[NIKA-045] Fetch error: {reason}")]
110    FetchFailed { reason: String },
111
112    #[error("[NIKA-046] Extract error: {reason}")]
113    ExtractFailed { reason: String },
114
115    #[error("[NIKA-096] Execution error: {0}")]
116    General(String),
117
118    #[error("[NIKA-097] Workflow cancelled: {phase}")]
119    Cancelled { phase: String },
120
121    #[error("[NIKA-098] Task panicked: {reason}")]
122    Panicked { reason: String },
123}
124
125impl From<ExecutionError> for NikaError {
126    fn from(e: ExecutionError) -> Self {
127        match e {
128            ExecutionError::ExecFailed { reason } => NikaError::ExecError { reason },
129            ExecutionError::FetchFailed { reason } => NikaError::FetchError { reason },
130            ExecutionError::ExtractFailed { reason } => NikaError::ExtractError { reason },
131            ExecutionError::General(msg) => NikaError::Execution(msg),
132            ExecutionError::Cancelled { phase } => NikaError::WorkflowCancelled { phase },
133            ExecutionError::Panicked { reason } => NikaError::TaskPanicked { reason },
134        }
135    }
136}
137
138// ═══════════════════════════════════════════════════════════════════════════
139// BINDING ERRORS (040-049)
140// ═══════════════════════════════════════════════════════════════════════════
141
142/// Template and binding errors.
143#[derive(Debug, thiserror::Error)]
144pub enum BindingError {
145    #[error("[NIKA-041] Template error in '{template}': {reason}")]
146    TemplateError { template: String, reason: String },
147
148    #[error("[NIKA-042] Binding '{alias}' not found")]
149    NotFound { alias: String },
150
151    #[error("[NIKA-043] Binding type mismatch at '{path}': expected {expected}, got {actual}")]
152    TypeMismatch {
153        path: String,
154        expected: String,
155        actual: String,
156    },
157}
158
159impl From<BindingError> for NikaError {
160    fn from(e: BindingError) -> Self {
161        match e {
162            BindingError::TemplateError { template, reason } => {
163                NikaError::TemplateError { template, reason }
164            }
165            BindingError::NotFound { alias } => NikaError::BindingNotFound { alias },
166            BindingError::TypeMismatch {
167                path,
168                expected,
169                actual,
170            } => NikaError::BindingTypeMismatch {
171                path,
172                expected,
173                actual,
174            },
175        }
176    }
177}
178
179// ═══════════════════════════════════════════════════════════════════════════
180// TESTS
181// ═══════════════════════════════════════════════════════════════════════════
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn provider_error_converts_to_nika_error() {
189        let err: NikaError = ProviderError::NotConfigured {
190            provider: "anthropic".into(),
191        }
192        .into();
193        assert!(err.to_string().contains("NIKA-030"));
194        assert!(err.to_string().contains("anthropic"));
195    }
196
197    #[test]
198    fn dag_error_converts_to_nika_error() {
199        let err: NikaError = DagError::CycleDetected {
200            cycle: "a → b → a".into(),
201        }
202        .into();
203        assert!(err.to_string().contains("NIKA-020"));
204    }
205
206    #[test]
207    fn execution_error_converts_to_nika_error() {
208        let err: NikaError = ExecutionError::General("test error".into()).into();
209        assert!(err.to_string().contains("NIKA-096"));
210    }
211
212    #[test]
213    fn binding_error_converts_to_nika_error() {
214        let err: NikaError = BindingError::NotFound {
215            alias: "data".into(),
216        }
217        .into();
218        assert!(err.to_string().contains("NIKA-042"));
219    }
220
221    #[test]
222    fn domain_errors_are_send_sync() {
223        fn assert_send_sync<T: Send + Sync>() {}
224        assert_send_sync::<ProviderError>();
225        assert_send_sync::<DagError>();
226        assert_send_sync::<ExecutionError>();
227        assert_send_sync::<BindingError>();
228    }
229}