1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use thiserror::Error;
/// Errors produced while parsing, validating, or building a prior book.
///
/// Every variant that originates from a JSONL file carries the 1-indexed
/// line number so callers can point users at the offending record.
#[derive(Debug, Error)]
pub enum Error {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("line {line}: invalid JSON: {source}")]
Json {
line: usize,
#[source]
source: serde_json::Error,
},
#[error("line {line}: missing required field `{field}`")]
MissingField { line: usize, field: &'static str },
#[error("line {line}: state must not be empty")]
EmptyState { line: usize },
#[error("line {line}: action must not be empty")]
EmptyAction { line: usize },
#[error("line {line}: weight must not be negative, got {value}")]
NegativeWeight { line: usize, value: f64 },
#[error("line {line}: score must not be NaN")]
NanScore { line: usize },
#[error("line {line}: unsupported outcome value `{value}`")]
UnsupportedOutcome { line: usize, value: String },
#[error("no observations remain after parsing and filtering")]
NoObservations,
#[error(
"prior book was built with a different config than expected \
(expected fingerprint {expected}, found {found})"
)]
BuildConfigMismatch { expected: u64, found: u64 },
#[error("invalid build config: {message}")]
InvalidConfig { message: String },
/// Raised only when `BuildConfig::context_order > 0`: deriving a
/// sequence's recent-action window while streaming requires that
/// sequence's own rows be contiguous and in increasing `step` order.
/// Identified by `sequence_id`/`step` rather than a line number --
/// unlike the JSONL-parse errors above, this is checked after parsing,
/// against the observation stream itself (shared by both the eager and
/// streaming build paths, only one of which has line numbers at all).
/// Unconditional -- not gated by `--strict`, since this is a stream-wide
/// structural precondition, not a single bad record.
#[error(
"sequence `{sequence_id}`: step {step} does not follow step {last_step} \
-- input must be sorted by (sequence_id, step) when context_order > 0"
)]
SequenceNotSorted {
sequence_id: String,
step: u32,
last_step: u32,
},
/// Raised by [`crate::gate::GateModel::fit`]: fewer than 2 distinct
/// `group_id`s means there is no way to hold out a group for
/// cross-validation without training on 100% of the data.
#[error("gate fit requires at least 2 distinct group_ids, found {num_groups}")]
InsufficientGateGroups { num_groups: usize },
/// Raised by [`crate::gate::GateModel::fit`]: too few observations to fit
/// `num_features` coefficients without massively overfitting. `required`
/// is `max(6, num_features + 2)`.
#[error(
"gate fit requires at least {required} observations for {num_features} features, found {num_observations}"
)]
InsufficientGateObservations {
num_observations: usize,
num_features: usize,
required: usize,
},
/// Raised by [`crate::gate::GateModel::fit`]: every [`crate::gate::GateObservation`]
/// must carry the same set of feature names -- a per-row-varying feature
/// set would silently change what each coefficient means, unlike
/// `predict`'s query-time missing/unknown-feature handling, which is
/// explicit and reported rather than baked into training.
#[error(
"gate observation `{candidate_id}` has a different feature set than the first observation"
)]
InconsistentGateFeatures { candidate_id: String },
/// Raised by [`crate::gate::GateModel::fit`]: a `gate_elo_delta`,
/// `gate_games_played`, or feature value was NaN or infinite.
#[error("gate observation `{candidate_id}` has a non-finite value in field `{field}`")]
NonFiniteGateValue { candidate_id: String, field: String },
/// Raised by [`crate::gate::GateModel::fit`]: `gate_games_played` must be
/// a positive, finite reliability weight for the label.
#[error("gate observation `{candidate_id}` has gate_games_played <= 0: {value}")]
NonPositiveGateWeight { candidate_id: String, value: f64 },
}
pub type Result<T> = std::result::Result<T, Error>;
/// A non-fatal issue skipped in non-strict mode. Carries enough detail to
/// report to the user without aborting the whole run.
#[derive(Debug, Clone, PartialEq)]
pub struct Warning {
pub line: usize,
pub message: String,
}
impl std::fmt::Display for Warning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// `message` is the source error's own Display, which already
// includes "line N: ..." -- don't prepend it again here.
write!(f, "{}", self.message)
}
}