Documentation has drifted from the code: four README examples no longer compile #35

Closed
opened 2026-09-01 04:39:07 +00:00 by logaritmisk · 0 comments
Owner

The prose documentation has not kept up with the T2 renames and the Drift<T> generalisation. Found while adding Member::with_drift_scale (#34) — the README's Drift section is where that feature belongs, and none of the surrounding code samples still work.

Everything below was verified against the code at b2a7ade (v0.3.0); each item cites the file:line that proves it.

Why CI does not catch this

README.md is not pulled into the crate via #![doc = include_str!("../README.md")], so nothing compiles its code blocks. The doctests in src/lib.rs all pass and are current — the rot is confined to files no test reads.

The single highest-value fix is to add that include_str! line, which turns every README block into a doctest and makes this class of drift a build failure rather than something to be found by reading. Doing so requires fixing the blocks first.

README.md

Four blocks fail to compile as pasted

L38–43 — Player no longer exists

use trueskill_tt::{Player, Gaussian, drift::ConstantDrift};
let player = Player::new(Gaussian::from_ms(0.0, 6.0), 1.0, ConstantDrift(0.1));

The type was renamed Rating in T2 (src/rating.rs:15, exported src/lib.rs:133). No Player symbol exists anywhere in src/ — E0432, unresolved import.

Renaming alone is not enough. Rating<T: Time = i64, D: Drift<T> = ConstantDrift> has new on impl<T: Time, D: Drift<T>> (src/rating.rs:25), and ConstantDrift implements Drift<T> for all T: Time (src/drift.rs:27). With the binding never used afterwards, T is unconstrained and struct-level defaults do not participate in inference — E0283, type annotations needed. Every real call site annotates: tests/game.rs:5, tests/degenerate_inputs.rs:14 and tests/equivalence.rs:12 all use type R = Rating<i64, ConstantDrift>;. The example needs Rating::<i64, _>::new(...) or a typed binding.

L49–64 — the SqrtDrift custom-drift example, three separate errors

impl Drift for SqrtDrift {
    fn variance_delta(&self, elapsed: i64) -> f64 { ... }
}
  • E0107 — the trait takes a type parameter: pub trait Drift<T: Time> (src/drift.rs:9).
  • E0053 — wrong method signature: the real one is variance_delta(&self, from: &T, to: &T) -> f64 (src/drift.rs:13).
  • E0046 — variance_for_elapsed(&self, elapsed: i64) -> f64 (src/drift.rs:17) is not implemented.

The correct shape is impl<T: Time> Drift<T> for SqrtDrift, which also needs use trueskill_tt::Time; (exported src/lib.rs:135) — the block never imports it. L63 then repeats the Player problem and additionally never imports Gaussian.

L68–72 — the .drift() example

The method call itself is correct (HistoryBuilder::drift, src/history.rs:55), but the block has no use trueskill_tt::History; (E0433) and depends on the broken SqrtDrift above.

L112–123 — the scored-outcome example

Compiles. Outcome in the use at L113 is unused — a warning, not an error.

Incorrect statements

L21–24 — the Drift trait declaration is simply wrong

pub trait Drift: Copy + Debug {
    fn variance_delta(&self, elapsed: i64) -> f64;
}

The real trait is generic over T: Time, requires Send + Sync, and has two methods (src/drift.rs:9-18).

L26 — "Gaussian::forget … computes the new sigma: σ_new = sqrt(σ² + variance_delta)"

forget is Self::from_mv(self.mu(), self.variance() + variance_delta) (src/gaussian.rs:144-146) — it works in variance space and takes no square root. Mathematically equivalent, but it contradicts the invariant CLAUDE.md:55-56 states explicitly ("Variance-space ops … take no square root"), which is the kind of thing a reader relies on.

L66 — "use the .drift() builder method instead of .gamma()"

There is no gamma() method on any type — grep -rn "fn gamma" src/ returns nothing. HistoryBuilder's complete surface is mu, sigma, beta, drift, p_draw, score_sigma, convergence, observer, build (src/history.rs:40-139). Only the GAMMA const exists (src/lib.rs:140). The sentence contrasts with something that has no counterpart in the code.

CLAUDE.md

L36 — the data-flow diagram splices the public and internal shapes together

History → TimeSlice[] → Event[] → Team[] → Item[]

There is no Team in that internal chain. The internal Event (src/time_slice.rs:90) holds items: Vec<Item> directly (src/time_slice.rs:85); teams are grouping over Items, not a value. Team (src/event.rs:21) is a public ingestion type that exists only on crate::event::Event<T, K> and is flattened away in History::add_events (src/history.rs:958-985).

L47 — "Event (time_slice.rs) — one match"

Ambiguous now that both exist. The public Event<T, K> is src/event.rs:13, re-exported at src/lib.rs:124; only the private pub(crate) struct Event is in time_slice.rs. A reader looking for the documented type lands in the wrong file. The compute()/apply() description is accurate for the internal one (src/time_slice.rs:133, :164).

L69–71 — "lib.rs — … the standalone quality(), cdf(), erfc()"

Only quality() is public (src/lib.rs:365). cdf is pub(crate) (src/lib.rs:231) and erfc is fully private (src/lib.rs:177). Listing all three implies a public API that does not exist.

L64–65 — "storage/SkillStore … and CompetitorStore …"

SkillStore is not reachable from outside the crate despite storage being a pub mod: src/storage/mod.rs:5 is pub(crate) use skill_store::SkillStore;, against pub use for CompetitorStore on the line above.

Rustdoc on public items

src/event_builder.rs:108-109 — two rustdocs contradict each other

scores_with_sigma claims sigma "debug-asserts otherwise via Outcome::scores_with_sigma". There is no debug_assert! there — src/outcome.rs:71-77 stores the value unvalidated, and its own doc (src/outcome.rs:66-70) says the opposite in as many words: constructing with a non-positive or NaN sigma "is allowed; the value is rejected with InferenceError::InvalidParameter when the event is ingested, so callers get an error rather than a panic". The builder doc describes a debug-assert that was deliberately replaced by an ingestion-time error.

src/event.rs:3-5 — describes a removal that never happened

The module doc calls add_events_with_prior "old" and says Event<T, K> "replaces" the nested vectors it took. It is the current internal ingestion chokepoint, still taking exactly those types (src/history.rs:649-656), and add_events (:1041), record_winner (:884) and record_draw (:908) all route through it.

src/factors.rs:3-4 — advertises an entry point rustdoc does not render

"Power users can construct custom factor graphs via Game::custom". It exists (src/game.rs:566) but carries #[doc(hidden)] (src/game.rs:565), so it does not appear in generated docs at all. Either the hint or the attribute is wrong.

Milestone references left in public rustdocsrc/history.rs:562 ("N-team support lands in T4", still an accurate limitation: assert_eq!(teams.len(), 2, ...) at :563), src/factors.rs:3 ("T4"), src/event.rs:4 ("spec Section 4", which lives only under docs/). No behavioural mismatch; flagging as a cluster, since "T4" and "Section 4" mean nothing to a reader outside this repo.

Verified correct — no action

Recorded so a future pass does not re-derive them: all four src/lib.rs crate-doc examples; README L109-110 (score_sigma, matching src/game.rs:68), L112-123 signatures, and the L129-133 todo list; and every remaining CLAUDE.md architecture bullet — Gaussian natural parameters and the Mul/Div-vs-variance-space split, the four factor types and BuiltinFactor, Competitor/Rating, KeyTable, EpsilonOrMax as the sole Schedule impl, the colour-contiguity invariant, #![forbid(unsafe_code)], and the commands and feature-flag sections.

Suggested order

  1. Fix the four README blocks so they compile.
  2. Add #![doc = include_str!("../README.md")] to src/lib.rs so CI compiles them from then on.
  3. Correct the prose items (README L21-26, L66; CLAUDE.md L36, L47, L64-65, L69-71).
  4. Fix the contradicting rustdoc on event_builder.rs, event.rs and factors.rs, and decide whether the T2/T4/"Section 4" milestone references belong in published docs.
The prose documentation has not kept up with the T2 renames and the `Drift<T>` generalisation. Found while adding `Member::with_drift_scale` (#34) — the README's Drift section is where that feature belongs, and none of the surrounding code samples still work. Everything below was verified against the code at `b2a7ade` (v0.3.0); each item cites the `file:line` that proves it. ## Why CI does not catch this `README.md` is not pulled into the crate via `#![doc = include_str!("../README.md")]`, so nothing compiles its code blocks. The doctests in `src/lib.rs` all pass and are current — the rot is confined to files no test reads. **The single highest-value fix is to add that `include_str!` line**, which turns every README block into a doctest and makes this class of drift a build failure rather than something to be found by reading. Doing so requires fixing the blocks first. ## README.md ### Four blocks fail to compile as pasted **L38–43 — `Player` no longer exists** ```rust use trueskill_tt::{Player, Gaussian, drift::ConstantDrift}; let player = Player::new(Gaussian::from_ms(0.0, 6.0), 1.0, ConstantDrift(0.1)); ``` The type was renamed `Rating` in T2 (`src/rating.rs:15`, exported `src/lib.rs:133`). No `Player` symbol exists anywhere in `src/` — E0432, unresolved import. Renaming alone is not enough. `Rating<T: Time = i64, D: Drift<T> = ConstantDrift>` has `new` on `impl<T: Time, D: Drift<T>>` (`src/rating.rs:25`), and `ConstantDrift` implements `Drift<T>` for *all* `T: Time` (`src/drift.rs:27`). With the binding never used afterwards, `T` is unconstrained and struct-level defaults do not participate in inference — E0283, type annotations needed. Every real call site annotates: `tests/game.rs:5`, `tests/degenerate_inputs.rs:14` and `tests/equivalence.rs:12` all use `type R = Rating<i64, ConstantDrift>;`. The example needs `Rating::<i64, _>::new(...)` or a typed binding. **L49–64 — the `SqrtDrift` custom-drift example, three separate errors** ```rust impl Drift for SqrtDrift { fn variance_delta(&self, elapsed: i64) -> f64 { ... } } ``` - E0107 — the trait takes a type parameter: `pub trait Drift<T: Time>` (`src/drift.rs:9`). - E0053 — wrong method signature: the real one is `variance_delta(&self, from: &T, to: &T) -> f64` (`src/drift.rs:13`). - E0046 — `variance_for_elapsed(&self, elapsed: i64) -> f64` (`src/drift.rs:17`) is not implemented. The correct shape is `impl<T: Time> Drift<T> for SqrtDrift`, which also needs `use trueskill_tt::Time;` (exported `src/lib.rs:135`) — the block never imports it. L63 then repeats the `Player` problem and additionally never imports `Gaussian`. **L68–72 — the `.drift()` example** The method call itself is correct (`HistoryBuilder::drift`, `src/history.rs:55`), but the block has no `use trueskill_tt::History;` (E0433) and depends on the broken `SqrtDrift` above. **L112–123 — the scored-outcome example** Compiles. `Outcome` in the `use` at L113 is unused — a warning, not an error. ### Incorrect statements **L21–24 — the `Drift` trait declaration is simply wrong** ```rust pub trait Drift: Copy + Debug { fn variance_delta(&self, elapsed: i64) -> f64; } ``` The real trait is generic over `T: Time`, requires `Send + Sync`, and has two methods (`src/drift.rs:9-18`). **L26 — "`Gaussian::forget` … computes the new sigma: `σ_new = sqrt(σ² + variance_delta)`"** `forget` is `Self::from_mv(self.mu(), self.variance() + variance_delta)` (`src/gaussian.rs:144-146`) — it works in variance space and takes **no square root**. Mathematically equivalent, but it contradicts the invariant `CLAUDE.md:55-56` states explicitly ("Variance-space ops … take no square root"), which is the kind of thing a reader relies on. **L66 — "use the `.drift()` builder method instead of `.gamma()`"** There is no `gamma()` method on any type — `grep -rn "fn gamma" src/` returns nothing. `HistoryBuilder`'s complete surface is `mu`, `sigma`, `beta`, `drift`, `p_draw`, `score_sigma`, `convergence`, `observer`, `build` (`src/history.rs:40-139`). Only the `GAMMA` const exists (`src/lib.rs:140`). The sentence contrasts with something that has no counterpart in the code. ## CLAUDE.md **L36 — the data-flow diagram splices the public and internal shapes together** ``` History → TimeSlice[] → Event[] → Team[] → Item[] ``` There is no `Team` in that internal chain. The internal `Event` (`src/time_slice.rs:90`) holds `items: Vec<Item>` directly (`src/time_slice.rs:85`); teams are grouping over `Item`s, not a value. `Team` (`src/event.rs:21`) is a *public ingestion* type that exists only on `crate::event::Event<T, K>` and is flattened away in `History::add_events` (`src/history.rs:958-985`). **L47 — "`Event` (`time_slice.rs`) — one match"** Ambiguous now that both exist. The public `Event<T, K>` is `src/event.rs:13`, re-exported at `src/lib.rs:124`; only the private `pub(crate) struct Event` is in `time_slice.rs`. A reader looking for the documented type lands in the wrong file. The `compute()`/`apply()` description is accurate for the internal one (`src/time_slice.rs:133`, `:164`). **L69–71 — "`lib.rs` — … the standalone `quality()`, `cdf()`, `erfc()`"** Only `quality()` is public (`src/lib.rs:365`). `cdf` is `pub(crate)` (`src/lib.rs:231`) and `erfc` is fully private (`src/lib.rs:177`). Listing all three implies a public API that does not exist. **L64–65 — "`storage/` — `SkillStore` … and `CompetitorStore` …"** `SkillStore` is not reachable from outside the crate despite `storage` being a `pub mod`: `src/storage/mod.rs:5` is `pub(crate) use skill_store::SkillStore;`, against `pub use` for `CompetitorStore` on the line above. ## Rustdoc on public items **`src/event_builder.rs:108-109` — two rustdocs contradict each other** `scores_with_sigma` claims sigma "debug-asserts otherwise via `Outcome::scores_with_sigma`". There is no `debug_assert!` there — `src/outcome.rs:71-77` stores the value unvalidated, and its own doc (`src/outcome.rs:66-70`) says the opposite in as many words: constructing with a non-positive or NaN sigma "is allowed; the value is rejected with `InferenceError::InvalidParameter` when the event is ingested, so callers get an error rather than a panic". The builder doc describes a debug-assert that was deliberately replaced by an ingestion-time error. **`src/event.rs:3-5` — describes a removal that never happened** The module doc calls `add_events_with_prior` "old" and says `Event<T, K>` "replaces" the nested vectors it took. It is the current internal ingestion chokepoint, still taking exactly those types (`src/history.rs:649-656`), and `add_events` (`:1041`), `record_winner` (`:884`) and `record_draw` (`:908`) all route through it. **`src/factors.rs:3-4` — advertises an entry point rustdoc does not render** "Power users can construct custom factor graphs via `Game::custom`". It exists (`src/game.rs:566`) but carries `#[doc(hidden)]` (`src/game.rs:565`), so it does not appear in generated docs at all. Either the hint or the attribute is wrong. **Milestone references left in public rustdoc** — `src/history.rs:562` ("N-team support lands in T4", still an accurate limitation: `assert_eq!(teams.len(), 2, ...)` at `:563`), `src/factors.rs:3` ("T4"), `src/event.rs:4` ("spec Section 4", which lives only under `docs/`). No behavioural mismatch; flagging as a cluster, since "T4" and "Section 4" mean nothing to a reader outside this repo. ## Verified correct — no action Recorded so a future pass does not re-derive them: all four `src/lib.rs` crate-doc examples; README L109-110 (`score_sigma`, matching `src/game.rs:68`), L112-123 signatures, and the L129-133 todo list; and every remaining `CLAUDE.md` architecture bullet — `Gaussian` natural parameters and the `Mul`/`Div`-vs-variance-space split, the four factor types and `BuiltinFactor`, `Competitor`/`Rating`, `KeyTable`, `EpsilonOrMax` as the sole `Schedule` impl, the colour-contiguity invariant, `#![forbid(unsafe_code)]`, and the commands and feature-flag sections. ## Suggested order 1. Fix the four README blocks so they compile. 2. Add `#![doc = include_str!("../README.md")]` to `src/lib.rs` so CI compiles them from then on. 3. Correct the prose items (README L21-26, L66; `CLAUDE.md` L36, L47, L64-65, L69-71). 4. Fix the contradicting rustdoc on `event_builder.rs`, `event.rs` and `factors.rs`, and decide whether the T2/T4/"Section 4" milestone references belong in published docs.
logaritmisk added the docs label 2026-09-07 13:53:40 +00:00
Sign in to join this conversation.