fix!: correct eight wrong # Errors sections and seal the error variants
Documentation (#78). Every item below was measured against the code rather than read: - `expected_information_gain` and `predict_ranking` had `# Errors` immediately followed by `# Preconditions`, with the error list stranded at the bottom of the latter — rustdoc rendered a BLANK Errors section on both. The heading now sits with its content. - `predict_outcome`, `predict_ranking` and the free `expected_information_gain` all omitted `GridTooCoarse`. - `predict_margin` claimed `JointUnavailable` "if the LATEST slice holds ranked events". Measured with an early ranked slice and a late scored one: it fails. The condition is *any* slice. - `add_events` documented three errors and can return five more; it also claimed a weights `MismatchedShape` that is unreachable through it, since weights arrive one-per-`Member`. That check belongs to `EventBuilder::weights`, and the doc now says so. - `converge` and `converge_partial` both omitted the drift-variance `InvalidParameter`. `History` gains a hand-written `Debug` (#76). Summarising, not exhaustive — a derived one would print every competitor's skill at every slice. It exists because without it a consumer cannot `#[derive(Debug)]` on any struct holding a `History`, which is how both known consumers store one. `#[non_exhaustive]` on all 17 `InferenceError` struct variants and on `Outcome::Scored` (#74). The enum carried the attribute; no variant did, so adding a field to any of them — and downstream construction of any of them — were both in the public contract. This crate added two variants in two days. The options structs are deliberately NOT sealed. `ConvergenceOptions` and `GameOptions` are constructed by struct literal at 65 sites of which only 8 use `..default()`, and specifying all three convergence fields is a natural complete statement rather than a partial one. That is a real trade-off rather than an oversight, and it is left as a decision on #74. Also spells `UnknownKeys::Reject` explicitly at both sites that wildcarded it. `#[non_exhaustive]` on your own enum gives no exhaustiveness safety net if you then match `_`. Sealing the variants pushed ten test sites from constructing errors to `matches!`, which is the better assertion anyway — an `assert_eq!` against a constructed error breaks whenever a field is added, which is the exact fragility the attribute exists to prevent. BREAKING CHANGE: `InferenceError`'s struct variants and `Outcome::Scored` are `#[non_exhaustive]` — downstream patterns need `..` and downstream construction is no longer possible. Refs #78, #76, #74 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
This commit is contained in:
+59
-14
@@ -963,7 +963,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
Some(skill) => skill,
|
||||
None => match self.unknown_keys {
|
||||
crate::UnknownKeys::Prior => Gaussian::from_ms(self.mu, self.sigma),
|
||||
_ => {
|
||||
crate::UnknownKeys::Reject => {
|
||||
return Err(InferenceError::UnknownKey {
|
||||
team: team_idx,
|
||||
member: member_idx,
|
||||
@@ -1220,7 +1220,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
mean += coefficient * self.mu;
|
||||
*unseen.entry(format!("{key:?}")).or_insert(0.0) += coefficient;
|
||||
}
|
||||
_ => {
|
||||
crate::UnknownKeys::Reject => {
|
||||
return Err(InferenceError::UnknownKey {
|
||||
team: 0,
|
||||
member,
|
||||
@@ -1482,7 +1482,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
///
|
||||
/// `MismatchedShape` unless exactly two teams are supplied, `EmptyTeam`,
|
||||
/// `UnknownKey` under [`UnknownKeys::Reject`](crate::UnknownKeys::Reject),
|
||||
/// and `JointUnavailable` if the latest slice holds ranked events.
|
||||
/// and `JointUnavailable` if the history is empty or holds ranked events in
|
||||
/// *any* slice — not merely the latest one.
|
||||
pub fn predict_margin(&self, teams: &[&[&K]]) -> Result<Gaussian, InferenceError>
|
||||
where
|
||||
K: std::fmt::Debug,
|
||||
@@ -1538,8 +1539,6 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
/// elsewhere. See [`expected_information_gain`](crate::expected_information_gain)
|
||||
/// for the scale, the analytic `ln k` ceiling, and the cost.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// # Preconditions
|
||||
///
|
||||
/// Every key must already be known to the history — that is, must have
|
||||
@@ -1550,8 +1549,12 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
/// whole-team miss into a plausible constant, which is invisible to any
|
||||
/// test that does not assert on variation.
|
||||
///
|
||||
/// As [`History::member_skills`], plus `TooManyTeams` and anything
|
||||
/// inference returns for a hypothetical outcome.
|
||||
/// # Errors
|
||||
///
|
||||
/// `NotEnoughTeams`, `EmptyTeam`, `UnknownKey` and `TooManyTeams` for the
|
||||
/// shape of the request, `GridTooCoarse` when the performance sigmas are
|
||||
/// too far apart to integrate on one grid, and anything inference returns
|
||||
/// for a hypothetical outcome.
|
||||
pub fn expected_information_gain(&self, teams: &[&[&K]]) -> Result<f64, InferenceError>
|
||||
where
|
||||
K: std::fmt::Debug,
|
||||
@@ -1652,6 +1655,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
/// # Errors
|
||||
///
|
||||
/// `NotEnoughTeams`, `EmptyTeam`, `UnknownKey`, or `TooManyTeams`.
|
||||
/// `GridTooCoarse` when the performance sigmas are too far apart to
|
||||
/// integrate on one grid.
|
||||
pub fn predict_outcome(&self, teams: &[&[&K]]) -> Result<Prediction, InferenceError>
|
||||
where
|
||||
K: std::fmt::Debug,
|
||||
@@ -1680,8 +1685,6 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
/// space, so it stays cheap at any team count — use it when you know which
|
||||
/// orderings you care about.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// # Preconditions
|
||||
///
|
||||
/// Every key must already be known to the history — that is, must have
|
||||
@@ -1692,8 +1695,11 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
/// whole-team miss into a plausible constant, which is invisible to any
|
||||
/// test that does not assert on variation.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `NotEnoughTeams`, `EmptyTeam`, `UnknownKey`, or `MismatchedShape` if
|
||||
/// `ranks` does not have one entry per team.
|
||||
/// `ranks` does not have one entry per team. `GridTooCoarse` when the
|
||||
/// performance sigmas are too far apart to integrate on one grid.
|
||||
pub fn predict_ranking(&self, teams: &[&[&K]], ranks: &[u32]) -> Result<f64, InferenceError>
|
||||
where
|
||||
K: std::fmt::Debug,
|
||||
@@ -1739,6 +1745,10 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
/// `NonFiniteResult` if a sweep produces a NaN or infinite step. EP has
|
||||
/// broken down at that point and further iterations cannot recover, so the
|
||||
/// loop stops rather than reporting a NaN step as convergence.
|
||||
///
|
||||
/// `InvalidParameter` if a competitor's drift model yields a negative or
|
||||
/// non-finite variance — which also covers a custom [`Drift`]
|
||||
/// implementation, the one case no constructor can check.
|
||||
pub fn converge(&mut self) -> Result<ConvergenceReport, InferenceError> {
|
||||
let report = self.converge_partial()?;
|
||||
|
||||
@@ -2289,10 +2299,18 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
/// # Errors
|
||||
///
|
||||
/// - `MismatchedShape` if an event's outcome does not describe the same
|
||||
/// number of teams the event has, or if per-member weights do not match
|
||||
/// the team's membership.
|
||||
/// - `InvalidParameter` if a per-event `score_sigma` override is not
|
||||
/// strictly positive.
|
||||
/// number of teams the event has. (Weights cannot mismatch here — they
|
||||
/// come one-per-`Member`; that check belongs to
|
||||
/// [`EventBuilder::weights`](crate::EventBuilder::weights), which builds
|
||||
/// them from a separate list.)
|
||||
/// - `NotEnoughTeams` for an event with fewer than two teams, and
|
||||
/// `EmptyTeam` for a team with no members.
|
||||
/// - `InvalidParameter` for a per-event `score_sigma` override that is not
|
||||
/// strictly positive, a non-finite score, rank or weight, or a
|
||||
/// `drift_scale` that is negative or non-finite.
|
||||
/// - `ConflictingCompetitorConfig` if one competitor is given two different
|
||||
/// values for `prior` or `drift_scale`, whether within one batch or
|
||||
/// across batches.
|
||||
/// - `TieWithoutDrawProbability` if an event ties two teams while the
|
||||
/// history's `p_draw` is zero. This includes `Outcome::winner(w, n)` for
|
||||
/// `n >= 3`, which ties every loser.
|
||||
@@ -2420,6 +2438,33 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
}
|
||||
}
|
||||
|
||||
/// Summarising rather than exhaustive.
|
||||
///
|
||||
/// A `History` owns every competitor's skill at every time slice, so a derived
|
||||
/// `Debug` would print the entire fit — megabytes for a real history, and
|
||||
/// useless in a log. This prints the shape instead. Same reasoning as `Joint`'s,
|
||||
/// which omits its `n^2` factorisation.
|
||||
///
|
||||
/// It exists at all because without it a consumer cannot `#[derive(Debug)]` on
|
||||
/// any struct holding a `History`, which is how both known consumers store it.
|
||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> std::fmt::Debug
|
||||
for History<T, D, O, K>
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("History")
|
||||
.field("competitors", &self.keys.len())
|
||||
.field("events", &self.size)
|
||||
.field("time_slices", &self.time_slices.len())
|
||||
.field("mu", &self.mu)
|
||||
.field("sigma", &self.sigma)
|
||||
.field("beta", &self.beta)
|
||||
.field("p_draw", &self.p_draw)
|
||||
.field("score_sigma", &self.score_sigma)
|
||||
.field("unknown_keys", &self.unknown_keys)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// A factorised joint posterior, reusable across many queries.
|
||||
///
|
||||
/// Built by [`History::joint`]. Every question the joint answers — the width of
|
||||
|
||||
Reference in New Issue
Block a user