docs: complete the public API documentation contract

Closes the last open item in #25. `cargo clippy -W missing_errors_doc
-W missing_panics_doc -W must_use_candidate -W doc_markdown` went from 56
warnings to zero.

The 13 hand-written sections name the actual variants each function returns
rather than gesturing at "an error". Establishing that meant reading the error
paths — `Game::ranked` alone returns four distinct variants, and `record_draw`
can hit TieWithoutDrawProbability where `record_winner` provably cannot, since
a two-team decisive outcome has nothing to tie. Documenting those as
interchangeable would have been worse than leaving them undocumented, because
a reader would trust it.

Two existing doc comments already described panics in prose but not under a
`# Panics` heading, so neither rustdoc nor clippy surfaced them:
`Outcome::winner` and `EventBuilder::weights`. Both now carry the heading, and
`Outcome::winner` gained the note that it ties every loser, so `n >= 3` needs a
positive p_draw — the crate's easiest error to hit by accident.

The 43 mechanical fixes (31 `#[must_use]` on pure accessors, 11 missing
backticks) were applied with `cargo clippy --fix`. `#[must_use]` on Gaussian's
arithmetic and on `posteriors()` matters: discarding those results is always a
bug, and until now nothing said so.

Also documented why `[profile.release] debug = true` exists — cargo-flamegraph
needs the symbols, and library profile settings are ignored downstream, so it
reads as an oversight without the note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T5SYDExxL4vZgvunrcNSMc
This commit is contained in:
2026-08-27 17:43:31 +02:00
co-authored by Claude Opus 5
parent 07285283b6
commit 9b2c2b38c8
17 changed files with 129 additions and 13 deletions
+41 -1
View File
@@ -198,6 +198,7 @@ impl Default for History<i64, ConstantDrift, NullObserver, &'static str> {
}
impl History<i64, ConstantDrift, NullObserver, &'static str> {
#[must_use]
pub fn builder() -> HistoryBuilder<i64, ConstantDrift, NullObserver, &'static str> {
HistoryBuilder::default()
}
@@ -205,6 +206,7 @@ impl History<i64, ConstantDrift, NullObserver, &'static str> {
impl<K: Eq + Hash + Clone> History<i64, ConstantDrift, NullObserver, K> {
/// Like `builder()` but uses a custom key type `K` instead of the default `&'static str`.
#[must_use]
pub fn builder_with_key() -> HistoryBuilder<i64, ConstantDrift, NullObserver, K> {
HistoryBuilder {
mu: MU,
@@ -552,7 +554,11 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
/// 2-team win probability: returns `[P(team0 wins), P(team1 wins)]`.
///
/// Panics if `teams.len() != 2`. N-team support lands in T4.
/// N-team support lands in T4.
///
/// # Panics
///
/// Panics if `teams.len() != 2`.
pub fn predict_outcome(&self, teams: &[&[&K]]) -> Vec<f64> {
assert_eq!(teams.len(), 2, "predict_outcome T2: 2 teams only");
let gather = |team: &[&K]| -> Gaussian {
@@ -574,6 +580,15 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
}
/// Run the full forward+backward convergence loop and return a summary.
///
/// Failing to reach `epsilon` within `max_iter` is not an error: the
/// returned report carries `converged: false` and the final step.
///
/// # Errors
///
/// `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.
pub fn converge(&mut self) -> Result<ConvergenceReport, InferenceError> {
use std::time::Instant;
@@ -830,6 +845,13 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
Ok(())
}
/// Record a single two-competitor event that `winner` won.
///
/// # Errors
///
/// Ingests through the same path as [`History::add_events`], so it returns
/// the same errors. A two-team decisive outcome cannot tie, so
/// `TieWithoutDrawProbability` is not reachable here.
pub fn record_winner<Q>(&mut self, winner: &Q, loser: &Q, time: T) -> Result<(), InferenceError>
where
K: Borrow<Q>,
@@ -847,6 +869,13 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
)
}
/// Record a single two-competitor event that ended level.
///
/// # Errors
///
/// Ingests through the same path as [`History::add_events`]. Note
/// `TieWithoutDrawProbability` *is* reachable here: a draw needs a
/// positive `p_draw`.
pub fn record_draw<Q>(&mut self, a: &Q, b: &Q, time: T) -> Result<(), InferenceError>
where
K: Borrow<Q>,
@@ -870,6 +899,17 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
}
/// Bulk-ingest typed events.
///
/// # 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.
/// - `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.
pub fn add_events<I>(&mut self, events: I) -> Result<(), InferenceError>
where
I: IntoIterator<Item = crate::event::Event<T, K>>,