zeph_experiments/error.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Error types for the experiments module.
5
6/// Errors that can occur during experiment evaluation, benchmark loading, or persistence.
7///
8/// Most variants carry structured context (file paths, token counts, parameter names)
9/// so that callers can surface actionable diagnostics to the user.
10///
11/// # Examples
12///
13/// ```rust
14/// use zeph_experiments::EvalError;
15///
16/// let err = EvalError::BudgetExceeded { used: 1_500, budget: 1_000 };
17/// assert!(err.to_string().contains("1500"));
18///
19/// let err = EvalError::InvalidRadius { radius: -1.0 };
20/// assert!(err.to_string().contains("finite and positive"));
21/// ```
22#[derive(Debug, thiserror::Error)]
23pub enum EvalError {
24 /// The benchmark TOML file could not be opened or read.
25 #[error("failed to load benchmark file {0}: {1}")]
26 BenchmarkLoad(String, #[source] std::io::Error),
27
28 /// The benchmark TOML file could not be parsed.
29 #[error("failed to parse benchmark file {0}: {1}")]
30 BenchmarkParse(String, String),
31
32 /// [`BenchmarkSet::validate`] was called on an empty `cases` vec.
33 ///
34 /// [`BenchmarkSet::validate`]: crate::BenchmarkSet::validate
35 #[error("benchmark set is empty")]
36 EmptyBenchmarkSet,
37
38 /// The cumulative token budget for judge calls was exhausted.
39 ///
40 /// When this error is returned from [`Evaluator::evaluate`], the report will
41 /// have `is_partial = true` and only include cases scored before the budget was hit.
42 ///
43 /// [`Evaluator::evaluate`]: crate::Evaluator::evaluate
44 #[error("evaluation budget exceeded: used {used} of {budget} tokens")]
45 BudgetExceeded { used: u64, budget: u64 },
46
47 /// An LLM call failed (network, auth, timeout, or API error).
48 #[error("LLM error during evaluation: {0}")]
49 Llm(#[from] zeph_llm::LlmError),
50
51 /// The judge model returned a non-finite or structurally invalid score.
52 #[error("judge output parse failed for case {case_index}: {detail}")]
53 JudgeParse {
54 /// Zero-based index of the benchmark case that produced the invalid output.
55 case_index: usize,
56 /// Description of the parse failure (e.g., `"non-finite score: NaN"`).
57 detail: String,
58 },
59
60 /// The internal tokio semaphore used for concurrency control was closed.
61 ///
62 /// This is an internal invariant violation and should never occur in normal usage.
63 #[error("semaphore acquire failed: {0}")]
64 Semaphore(String),
65
66 /// The benchmark file exceeds the 10 MiB size limit.
67 #[error("benchmark file exceeds size limit ({size} bytes > {limit} bytes): {path}")]
68 BenchmarkTooLarge {
69 /// Canonicalized path of the file.
70 path: String,
71 /// Actual file size in bytes.
72 size: u64,
73 /// Maximum allowed size in bytes (currently 10 MiB).
74 limit: u64,
75 },
76
77 /// The benchmark file's canonical path escaped the expected parent directory.
78 ///
79 /// This indicates a symlink traversal attack and is rejected before any file I/O.
80 #[error("benchmark file path escapes allowed directory: {0}")]
81 PathTraversal(String),
82
83 /// A parameter value was outside its declared `[min, max]` range.
84 #[error("parameter out of range: {kind} value {value} not in [{min}, {max}]")]
85 OutOfRange {
86 /// Parameter name (e.g., `"temperature"`).
87 kind: String,
88 /// The value that was rejected.
89 value: f64,
90 /// Minimum allowed value (inclusive).
91 min: f64,
92 /// Maximum allowed value (inclusive).
93 max: f64,
94 },
95
96 /// All variations in the generator's search space have been visited.
97 #[error("search space exhausted: all variations in {strategy} have been visited")]
98 SearchSpaceExhausted {
99 /// Name of the strategy that exhausted (e.g., `"grid"`).
100 strategy: &'static str,
101 },
102
103 /// The [`Neighborhood`] radius was not finite and positive.
104 ///
105 /// [`Neighborhood`]: crate::Neighborhood
106 #[error("invalid neighborhood radius {radius}: must be finite and positive")]
107 InvalidRadius {
108 /// The invalid radius value that was rejected.
109 radius: f64,
110 },
111
112 /// An experiment result could not be persisted to SQLite.
113 #[error("experiment storage error: {0}")]
114 Storage(String),
115}