The per-item documentation is unusually good — InferenceError, ITERATIONS, UnknownKeys, Joint and converge carry prose with measurements in it. The gaps are structural: what a reader meets first, and what is missing entirely.
1. The README has no quickstart, and is stale on everything recent
Heading order: title → Other implementations (a link dump) → Drift (an expert topic, ~130 lines) → Scored outcomes → Prediction → Which match to play next → Todo.
No install line, no "what is this", and no record_winner → converge → current_skill block until line ~226 of 307, buried in a subsection called "Asking about one competitor". The crate publishes to a private registry, so the git host's README is the front door; the good getting-started material exists only in src/lib.rs.
Names appearing zero times in the README: Joint, register, converge_partial, posterior_of, posterior_of_at, expected_variance_reduction, predict_margin, GridTooCoarse, rating(), learning_curve(s), filtered_*, record_draw, ConvergenceOptions, Game, rayon. The strictness of converge — the crate's most opinionated recent decision, with a long rationale in the source — is not mentioned.
rayon appears 0 times; approx appears once, in a ticked Todo box. Feature flags are documented only in lib.rs.
The two canonical examples also disagree.lib.rs writes History::default() and current_skill("alice"); the README writes History::builder().build() and current_skill(&"alice"). Both compile, and nothing says which is idiomatic.
2. #![deny(missing_docs)] is off, and 43.6% of the public surface is undocumented
The worst three are all first-contact:History::current_skill — the method the crate's own first example calls — EventBuilder, the type h.event(t) hands you, and Gaussian::mu().
Cost to turn the lint on: ~62 items are mechanical (the 26 InferenceError field docs already exist as prose inside the Display format strings). ~50 need real writing. Roughly a day and a half. Worth doing after#73 removes six types, which takes the 112 well under 90.
Also: history.rs:1438 links [History::member_skills], which is private → a broken link on the published docs.
3. The only Joint example does not compile downstream
history.rs:1279 opens # use smallvec::smallvec;. smallvec is a dependency of the crate but is not re-exported:
error[E0432]: unresolved import `smallvec`
help: if you wanted to use a crate named `smallvec`, use `cargo add smallvec`
The root cause is broader — four public items expose smallvec types:
You can build an Event without the dependency (vec![..].into() coerces) and iterate the timings via Deref. But naming any of these — a helper returning a teams list, a match arm binding ranks and passing it on — requires adding smallvec and keeping its major version in lockstep. A smallvec 2.0 becomes a breaking change here.
Both examples/scored.rs and examples/atp.rs use smallvec! directly, so the crate's own worked examples teach the leaking path. The README's Event literal dodges it with .into_iter().collect() — a third spelling of the same construction.
Fix:pub use smallvec;, or make the fields private behind accessors. Outcome::as_ranks/as_scores are already written and pub(crate) — promoting those two and making the payloads opaque closes it and costs nothing.
4. Game's three constructors have # Errors as their entire doc
game.rs:456, :520, :598 — no summary line, so rustdoc's index renders the error list as the summary. And two doc comments put # Errorsbefore# Preconditions with the error list stranded after, so rustdoc renders an empty Errors section: History::expected_information_gain and History::predict_ranking. Free to fix.
5. Missing "which entry point?" guidance
Game, record_winner, EventBuilder and add_events are presented as peers. Game is listed in "Core types" beside History with no hint that it does not participate in a history at all. A reader has no basis to choose.
Breaks: nothing in 1, 2, 4, 5. Item 3 breaks struct-literal construction of Event/Team if the fields go private.
Found by an API audit, 2026-09-09; the smallvec failure reproduced from a consumer crate.
The per-item documentation is unusually good — `InferenceError`, `ITERATIONS`, `UnknownKeys`, `Joint` and `converge` carry prose with measurements in it. The gaps are structural: what a reader meets first, and what is missing entirely.
## 1. The README has no quickstart, and is stale on everything recent
Heading order: title → *Other implementations* (a link dump) → *Drift* (an expert topic, ~130 lines) → *Scored outcomes* → *Prediction* → *Which match to play next* → *Todo*.
No install line, no "what is this", and no `record_winner → converge → current_skill` block until **line ~226 of 307**, buried in a subsection called "Asking about one competitor". The crate publishes to a private registry, so the git host's README **is** the front door; the good getting-started material exists only in `src/lib.rs`.
Names appearing **zero times** in the README: `Joint`, `register`, `converge_partial`, `posterior_of`, `posterior_of_at`, `expected_variance_reduction`, `predict_margin`, `GridTooCoarse`, `rating()`, `learning_curve(s)`, `filtered_*`, `record_draw`, `ConvergenceOptions`, `Game`, `rayon`. The strictness of `converge` — the crate's most opinionated recent decision, with a long rationale in the source — is not mentioned.
`rayon` appears 0 times; `approx` appears once, in a ticked Todo box. Feature flags are documented only in `lib.rs`.
**The two canonical examples also disagree.** `lib.rs` writes `History::default()` and `current_skill("alice")`; the README writes `History::builder().build()` and `current_skill(&"alice")`. Both compile, and nothing says which is idiomatic.
## 2. `#![deny(missing_docs)]` is off, and 43.6% of the public surface is undocumented
`cargo rustdoc --all-features -- -W missing_docs` → **112 warnings**. By file:
| File | n |
|---|---|
| `error.rs` | 27 (the enum itself + all 26 variant *fields*; the variants are documented) |
| `lib.rs` | 13 (3 modules; `BETA` `MU` `SIGMA` `GAMMA` `P_DRAW` `EPSILON` `N01` `N00` `N_INF`; `Index`) |
| `event.rs` | 11 |
| `storage/competitor_store.rs` | 10 |
| `time_slice.rs` | 9 |
| `convergence.rs` | 8 |
| `game.rs` | 8 |
| `history.rs` | 8 |
| `key_table.rs` | 7 |
| `gaussian.rs` | 4 (`pi` `tau` `mu` `sigma`) |
| `outcome.rs` | 4 |
| `competitor.rs` | 2 |
| `event_builder.rs` | 1 |
**The worst three are all first-contact:** `History::current_skill` — the method the crate's own first example calls — `EventBuilder`, the type `h.event(t)` hands you, and `Gaussian::mu()`.
Cost to turn the lint on: ~62 items are mechanical (the 26 `InferenceError` field docs already exist as prose inside the `Display` format strings). ~50 need real writing. Roughly a day and a half. Worth doing **after** #73 removes six types, which takes the 112 well under 90.
Also: `history.rs:1438` links `[History::member_skills]`, which is private → a broken link on the published docs.
## 3. The only `Joint` example does not compile downstream
`history.rs:1279` opens `# use smallvec::smallvec;`. `smallvec` is a dependency of the crate but is **not re-exported**:
```
error[E0432]: unresolved import `smallvec`
help: if you wanted to use a crate named `smallvec`, use `cargo add smallvec`
```
The root cause is broader — four public items expose `smallvec` types:
- `Event::teams: SmallVec<[Team<K>; 4]>` (public field)
- `Team::members: SmallVec<[Member<K>; 4]>` (public field)
- `Outcome::Ranked(SmallVec<[u32; 4]>)` and `Outcome::Scored { scores: SmallVec<..> }`
- `ConvergenceReport::per_iteration_time: SmallVec<[Duration; 32]>`
You can *build* an `Event` without the dependency (`vec![..].into()` coerces) and *iterate* the timings via `Deref`. But naming any of these — a helper returning a teams list, a `match` arm binding `ranks` and passing it on — requires adding `smallvec` and keeping its major version in lockstep. **A `smallvec 2.0` becomes a breaking change here.**
Both `examples/scored.rs` and `examples/atp.rs` use `smallvec!` directly, so the crate's own worked examples teach the leaking path. The README's `Event` literal dodges it with `.into_iter().collect()` — a third spelling of the same construction.
**Fix:** `pub use smallvec;`, or make the fields private behind accessors. `Outcome::as_ranks`/`as_scores` are already written and `pub(crate)` — promoting those two and making the payloads opaque closes it and costs nothing.
## 4. `Game`'s three constructors have `# Errors` as their entire doc
`game.rs:456`, `:520`, `:598` — no summary line, so rustdoc's index renders the error list as the summary. And two doc comments put `# Errors` **before** `# Preconditions` with the error list stranded after, so rustdoc renders an empty **Errors** section: `History::expected_information_gain` and `History::predict_ranking`. Free to fix.
## 5. Missing "which entry point?" guidance
`Game`, `record_winner`, `EventBuilder` and `add_events` are presented as peers. `Game` is listed in "Core types" beside `History` with no hint that it does not participate in a history at all. A reader has no basis to choose.
**Breaks:** nothing in 1, 2, 4, 5. Item 3 breaks struct-literal construction of `Event`/`Team` if the fields go private.
Found by an API audit, 2026-09-09; the `smallvec` failure reproduced from a consumer crate.
Restructured: what the crate is → install (with both feature flags) → quickstart (record_winner → converge → current_skill, then learning_curve and current_skills) → teams/rankings/draws → a which entry point? table → the converge-is-strict rationale. The link dump moved to the bottom, and the all-ticked Todo list was retired in favour of a short Status paragraph pointing at the tracker.
The two canonical examples disagreed, as reported. They now use one spelling: History::default() and current_skill("alice").
Five new blocks are doctested, taking the suite from 19 to 25.
2. #![deny(missing_docs)] is on
80 items at the start (112 in the audit; #73's removals accounted for the difference), now zero, and the lint is enabled so the next undocumented item is a build failure rather than a warning nobody reads.
Several of the new docs are measurements, not readings:
Outcome::Ranked — ranks are used ordinally, so [0, 1, 2] and [0, 5, 90] are the same observation. Measured: bit-identical posteriors for both. Contrasted explicitly with Scored, where the literal adjacent difference is the evidence.
OwnedGame::log_evidence — two identically-rated competitors give exactly ln(0.5). Written as a doctest, so the claim runs rather than being asserted from reading ln_sf.
Member::weight — zero and negative are deliberately accepted; only non-finite is InvalidParameter. Measured.
ConvergenceReport::final_step is (|Δmu|, |Δsigma|) in skill units, not natural parameters. That one had to be traced through Gaussian::delta; the surrounding pi/tau vocabulary makes the wrong reading the natural one.
GameOptions::score_sigma rejects non-positive and NaN but accepts +inf — the doc says what the guard says.
Event.time — drift is driven by a competitor's consecutive appearances, not by the gap between slices.
The [History::member_skills] link is on a private method, so it never rendered publicly; changed to plain text anyway.
3. pub use smallvec;
The Joint example's unresolved import reproduced. Re-exported rather than making the fields private: the types are already in the public API, so the major version is already a breaking-change vector — re-exporting only makes it usable, and a consumer takes this crate's version instead of pinning a matching one. examples/scored.rs and examples/atp.rs now import through the re-export, so the crate's own examples teach the path that works downstream.
The opaque-payload alternative (Outcome::as_ranks/as_scores promoted, fields private) is still the cleaner end state and is a breaking change worth its own issue if you want it.
4 and 5
Game's constructors have summary lines; the Game type itself now says plainly that it does not participate in a History and that a hand-chained sequence of Games is a forward-only filter, not the same answer. The README's entry-point table covers the same ground for a reader who never opens the docs.
The two empty # Errors sections had already been fixed by the earlier api/cleanup work — see #78 for the two doc defects in that list that were still real.
Closed by 31564b7 (merged as 251211f). All five items.
## 1. README
Restructured: what the crate is → install (with both feature flags) → quickstart (`record_winner` → `converge` → `current_skill`, then `learning_curve` and `current_skills`) → teams/rankings/draws → a **which entry point?** table → the `converge`-is-strict rationale. The link dump moved to the bottom, and the all-ticked Todo list was retired in favour of a short Status paragraph pointing at the tracker.
The two canonical examples disagreed, as reported. They now use one spelling: `History::default()` and `current_skill("alice")`.
Five new blocks are doctested, taking the suite from 19 to **25**.
## 2. `#![deny(missing_docs)]` is on
80 items at the start (112 in the audit; #73's removals accounted for the difference), now zero, and the lint is enabled so the next undocumented item is a build failure rather than a warning nobody reads.
Several of the new docs are measurements, not readings:
- `Outcome::Ranked` — ranks are used **ordinally**, so `[0, 1, 2]` and `[0, 5, 90]` are the same observation. Measured: bit-identical posteriors for both. Contrasted explicitly with `Scored`, where the literal adjacent difference *is* the evidence.
- `OwnedGame::log_evidence` — two identically-rated competitors give exactly `ln(0.5)`. Written as a doctest, so the claim runs rather than being asserted from reading `ln_sf`.
- `Member::weight` — zero and negative are deliberately accepted; only non-finite is `InvalidParameter`. Measured.
- `ConvergenceReport::final_step` is `(|Δmu|, |Δsigma|)` in **skill units, not natural parameters**. That one had to be traced through `Gaussian::delta`; the surrounding `pi`/`tau` vocabulary makes the wrong reading the natural one.
- `GameOptions::score_sigma` rejects non-positive and NaN but **accepts `+inf`** — the doc says what the guard says.
- `Event.time` — drift is driven by a competitor's *consecutive appearances*, not by the gap between slices.
The `[History::member_skills]` link is on a private method, so it never rendered publicly; changed to plain text anyway.
## 3. `pub use smallvec;`
The `Joint` example's `unresolved import` reproduced. Re-exported rather than making the fields private: the types are already in the public API, so the major version is already a breaking-change vector — re-exporting only makes it usable, and a consumer takes this crate's version instead of pinning a matching one. `examples/scored.rs` and `examples/atp.rs` now import through the re-export, so the crate's own examples teach the path that works downstream.
The opaque-payload alternative (`Outcome::as_ranks`/`as_scores` promoted, fields private) is still the cleaner end state and is a breaking change worth its own issue if you want it.
## 4 and 5
`Game`'s constructors have summary lines; the `Game` type itself now says plainly that it does not participate in a `History` and that a hand-chained sequence of `Game`s is a forward-only filter, not the same answer. The README's entry-point table covers the same ground for a reader who never opens the docs.
The two empty `# Errors` sections had already been fixed by the earlier `api/cleanup` work — see #78 for the two doc defects in that list that were still real.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The per-item documentation is unusually good —
InferenceError,ITERATIONS,UnknownKeys,Jointandconvergecarry prose with measurements in it. The gaps are structural: what a reader meets first, and what is missing entirely.1. The README has no quickstart, and is stale on everything recent
Heading order: title → Other implementations (a link dump) → Drift (an expert topic, ~130 lines) → Scored outcomes → Prediction → Which match to play next → Todo.
No install line, no "what is this", and no
record_winner → converge → current_skillblock until line ~226 of 307, buried in a subsection called "Asking about one competitor". The crate publishes to a private registry, so the git host's README is the front door; the good getting-started material exists only insrc/lib.rs.Names appearing zero times in the README:
Joint,register,converge_partial,posterior_of,posterior_of_at,expected_variance_reduction,predict_margin,GridTooCoarse,rating(),learning_curve(s),filtered_*,record_draw,ConvergenceOptions,Game,rayon. The strictness ofconverge— the crate's most opinionated recent decision, with a long rationale in the source — is not mentioned.rayonappears 0 times;approxappears once, in a ticked Todo box. Feature flags are documented only inlib.rs.The two canonical examples also disagree.
lib.rswritesHistory::default()andcurrent_skill("alice"); the README writesHistory::builder().build()andcurrent_skill(&"alice"). Both compile, and nothing says which is idiomatic.2.
#![deny(missing_docs)]is off, and 43.6% of the public surface is undocumentedcargo rustdoc --all-features -- -W missing_docs→ 112 warnings. By file:error.rslib.rsBETAMUSIGMAGAMMAP_DRAWEPSILONN01N00N_INF;Index)event.rsstorage/competitor_store.rstime_slice.rsconvergence.rsgame.rshistory.rskey_table.rsgaussian.rspitaumusigma)outcome.rscompetitor.rsevent_builder.rsThe worst three are all first-contact:
History::current_skill— the method the crate's own first example calls —EventBuilder, the typeh.event(t)hands you, andGaussian::mu().Cost to turn the lint on: ~62 items are mechanical (the 26
InferenceErrorfield docs already exist as prose inside theDisplayformat strings). ~50 need real writing. Roughly a day and a half. Worth doing after #73 removes six types, which takes the 112 well under 90.Also:
history.rs:1438links[History::member_skills], which is private → a broken link on the published docs.3. The only
Jointexample does not compile downstreamhistory.rs:1279opens# use smallvec::smallvec;.smallvecis a dependency of the crate but is not re-exported:The root cause is broader — four public items expose
smallvectypes:Event::teams: SmallVec<[Team<K>; 4]>(public field)Team::members: SmallVec<[Member<K>; 4]>(public field)Outcome::Ranked(SmallVec<[u32; 4]>)andOutcome::Scored { scores: SmallVec<..> }ConvergenceReport::per_iteration_time: SmallVec<[Duration; 32]>You can build an
Eventwithout the dependency (vec![..].into()coerces) and iterate the timings viaDeref. But naming any of these — a helper returning a teams list, amatcharm bindingranksand passing it on — requires addingsmallvecand keeping its major version in lockstep. Asmallvec 2.0becomes a breaking change here.Both
examples/scored.rsandexamples/atp.rsusesmallvec!directly, so the crate's own worked examples teach the leaking path. The README'sEventliteral dodges it with.into_iter().collect()— a third spelling of the same construction.Fix:
pub use smallvec;, or make the fields private behind accessors.Outcome::as_ranks/as_scoresare already written andpub(crate)— promoting those two and making the payloads opaque closes it and costs nothing.4.
Game's three constructors have# Errorsas their entire docgame.rs:456,:520,:598— no summary line, so rustdoc's index renders the error list as the summary. And two doc comments put# Errorsbefore# Preconditionswith the error list stranded after, so rustdoc renders an empty Errors section:History::expected_information_gainandHistory::predict_ranking. Free to fix.5. Missing "which entry point?" guidance
Game,record_winner,EventBuilderandadd_eventsare presented as peers.Gameis listed in "Core types" besideHistorywith no hint that it does not participate in a history at all. A reader has no basis to choose.Breaks: nothing in 1, 2, 4, 5. Item 3 breaks struct-literal construction of
Event/Teamif the fields go private.Found by an API audit, 2026-09-09; the
smallvecfailure reproduced from a consumer crate.Closed by
31564b7(merged as251211f). All five items.1. README
Restructured: what the crate is → install (with both feature flags) → quickstart (
record_winner→converge→current_skill, thenlearning_curveandcurrent_skills) → teams/rankings/draws → a which entry point? table → theconverge-is-strict rationale. The link dump moved to the bottom, and the all-ticked Todo list was retired in favour of a short Status paragraph pointing at the tracker.The two canonical examples disagreed, as reported. They now use one spelling:
History::default()andcurrent_skill("alice").Five new blocks are doctested, taking the suite from 19 to 25.
2.
#![deny(missing_docs)]is on80 items at the start (112 in the audit; #73's removals accounted for the difference), now zero, and the lint is enabled so the next undocumented item is a build failure rather than a warning nobody reads.
Several of the new docs are measurements, not readings:
Outcome::Ranked— ranks are used ordinally, so[0, 1, 2]and[0, 5, 90]are the same observation. Measured: bit-identical posteriors for both. Contrasted explicitly withScored, where the literal adjacent difference is the evidence.OwnedGame::log_evidence— two identically-rated competitors give exactlyln(0.5). Written as a doctest, so the claim runs rather than being asserted from readingln_sf.Member::weight— zero and negative are deliberately accepted; only non-finite isInvalidParameter. Measured.ConvergenceReport::final_stepis(|Δmu|, |Δsigma|)in skill units, not natural parameters. That one had to be traced throughGaussian::delta; the surroundingpi/tauvocabulary makes the wrong reading the natural one.GameOptions::score_sigmarejects non-positive and NaN but accepts+inf— the doc says what the guard says.Event.time— drift is driven by a competitor's consecutive appearances, not by the gap between slices.The
[History::member_skills]link is on a private method, so it never rendered publicly; changed to plain text anyway.3.
pub use smallvec;The
Jointexample'sunresolved importreproduced. Re-exported rather than making the fields private: the types are already in the public API, so the major version is already a breaking-change vector — re-exporting only makes it usable, and a consumer takes this crate's version instead of pinning a matching one.examples/scored.rsandexamples/atp.rsnow import through the re-export, so the crate's own examples teach the path that works downstream.The opaque-payload alternative (
Outcome::as_ranks/as_scorespromoted, fields private) is still the cleaner end state and is a breaking change worth its own issue if you want it.4 and 5
Game's constructors have summary lines; theGametype itself now says plainly that it does not participate in aHistoryand that a hand-chained sequence ofGames is a forward-only filter, not the same answer. The README's entry-point table covers the same ground for a reader who never opens the docs.The two empty
# Errorssections had already been fixed by the earlierapi/cleanupwork — see #78 for the two doc defects in that list that were still real.