A collection of API-surface problems found while auditing. Individually small, collectively they make the crate awkward to consume.
1. predict_outcome panics, is 2-team-only, and ignores draws
src/history.rs:408-429:
/// Panics if `teams.len() != 2`. N-team support lands in T4.
pubfnpredict_outcome(&self,teams: &[&[&K]])-> Vec<f64>{assert_eq!(teams.len(),2,"predict_outcome T2: 2 teams only");
T4 has shipped; the N-team support did not.
It panics rather than returning InferenceError, in a crate that has one.
It returns Vec<f64> of length 2 where a [f64; 2] or a named struct would be self-describing.
It ignores p_draw entirely: [p_a, 1.0 - p_a] allocates no probability mass to a draw even when the history is configured with p_draw > 0. For a draw-enabled model the numbers are simply wrong.
It silently drops unknown keys via filter_map (src/history.rs:415), same defect as predict_quality in #9 — a team of entirely unknown players yields a confident-looking probability.
2. HistoryBuilder is not exported
src/lib.rs:40 re-exports History but not HistoryBuilder, which is pub in src/history.rs:21. History::builder() works for inline chains, but callers cannot name the type:
so no fn make_builder() -> HistoryBuilder<…>, no storing a partially-configured builder. Same for ConvergenceOptions' companion EpsilonOrMax, which is reachable only via crate::factors.
3. Rating is write-only
Rating's fields are all pub(crate) (src/rating.rs:15-20) with no accessors. A caller can construct one with Rating::new(prior, beta, drift) and hand it to Game::ranked, but cannot read back prior, beta, or drift — so a Rating obtained from anywhere can't be inspected, logged, or serialised. Add pub fn prior(), beta(), drift().
4. Index is opaque with no way back to usize
pub struct Index(usize) (src/lib.rs:64) has From<usize> but no inverse and no accessor. History::intern hands callers an Index and KeyTable is documented as letting "power users promote &K to Index and skip the lookup on the hot path" (src/key_table.rs:12-13) — but the returned handle can't be stored in an external side table keyed by integer, printed meaningfully, or round-tripped. Add pub fn get(self) -> usize (or impl From<Index> for usize).
Note Index is exported at the crate root but is not in the pub use list — it is public only by virtue of being declared pub in lib.rs, which is easy to miss.
5. Game::one_v_one hardcodes default options
src/game.rs:517-525 ignores any caller configuration:
So a 1v1 with a draw cannot set p_draw, and no 1v1 can set convergence options. Given #8, passing Outcome::draw(2) here returns NaN with no way for the caller to avoid it. Take &GameOptions.
6. Observer::on_batch_processed is never called
Declared in the trait (src/observer.rs:20) but grep finds no call site — only on_iteration_end and on_converged are invoked (src/history.rs:446, 450). Implementors get a callback that never fires. Either call it from the slice sweep or remove it. The name is also pre-T2 ("batch" → TimeSlice); on_slice_processed matches the current vocabulary.
7. factor vs factors module naming
pub(crate) mod factor (src/lib.rs:19) and pub mod factors (src/lib.rs:20) sit adjacent, differing by one character — the latter being the public re-export facade of the former. Confusing to read and easy to mistype. Consider factor (private impl) + factors → something like graph or custom.
Acceptance
predict_outcome supports N teams, returns Result, accounts for p_draw, and errors on unknown keys.
HistoryBuilder is exported; Rating and Index have accessors.
one_v_one accepts GameOptions.
No public trait method is unreachable.
CHANGELOG.md records the breaking changes.
A collection of API-surface problems found while auditing. Individually small, collectively they make the crate awkward to consume.
## 1. `predict_outcome` panics, is 2-team-only, and ignores draws
`src/history.rs:408-429`:
```rust
/// Panics if `teams.len() != 2`. N-team support lands in T4.
pub fn predict_outcome(&self, teams: &[&[&K]]) -> Vec<f64> {
assert_eq!(teams.len(), 2, "predict_outcome T2: 2 teams only");
```
- T4 has shipped; the N-team support did not.
- It panics rather than returning `InferenceError`, in a crate that has one.
- It returns `Vec<f64>` of length 2 where a `[f64; 2]` or a named struct would be self-describing.
- It ignores `p_draw` entirely: `[p_a, 1.0 - p_a]` allocates no probability mass to a draw even when the history is configured with `p_draw > 0`. For a draw-enabled model the numbers are simply wrong.
- It silently drops unknown keys via `filter_map` (`src/history.rs:415`), same defect as `predict_quality` in #9 — a team of entirely unknown players yields a confident-looking probability.
## 2. `HistoryBuilder` is not exported
`src/lib.rs:40` re-exports `History` but not `HistoryBuilder`, which is `pub` in `src/history.rs:21`. `History::builder()` works for inline chains, but callers cannot name the type:
```rust
use trueskill_tt::HistoryBuilder; // error[E0432]: unresolved import
```
so no `fn make_builder() -> HistoryBuilder<…>`, no storing a partially-configured builder. Same for `ConvergenceOptions`' companion `EpsilonOrMax`, which is reachable only via `crate::factors`.
## 3. `Rating` is write-only
`Rating`'s fields are all `pub(crate)` (`src/rating.rs:15-20`) with no accessors. A caller can construct one with `Rating::new(prior, beta, drift)` and hand it to `Game::ranked`, but cannot read back `prior`, `beta`, or `drift` — so a `Rating` obtained from anywhere can't be inspected, logged, or serialised. Add `pub fn prior()`, `beta()`, `drift()`.
## 4. `Index` is opaque with no way back to `usize`
`pub struct Index(usize)` (`src/lib.rs:64`) has `From<usize>` but no inverse and no accessor. `History::intern` hands callers an `Index` and `KeyTable` is documented as letting "power users promote `&K` to `Index` and skip the lookup on the hot path" (`src/key_table.rs:12-13`) — but the returned handle can't be stored in an external side table keyed by integer, printed meaningfully, or round-tripped. Add `pub fn get(self) -> usize` (or `impl From<Index> for usize`).
Note `Index` is exported at the crate root but is not in the `pub use` list — it is public only by virtue of being declared `pub` in `lib.rs`, which is easy to miss.
## 5. `Game::one_v_one` hardcodes default options
`src/game.rs:517-525` ignores any caller configuration:
```rust
let game = Self::ranked(&[&[*a], &[*b]], outcome, &GameOptions::default())?;
```
So a 1v1 with a draw cannot set `p_draw`, and no 1v1 can set convergence options. Given #8, passing `Outcome::draw(2)` here returns NaN with no way for the caller to avoid it. Take `&GameOptions`.
## 6. `Observer::on_batch_processed` is never called
Declared in the trait (`src/observer.rs:20`) but `grep` finds no call site — only `on_iteration_end` and `on_converged` are invoked (`src/history.rs:446`, `450`). Implementors get a callback that never fires. Either call it from the slice sweep or remove it. The name is also pre-T2 ("batch" → `TimeSlice`); `on_slice_processed` matches the current vocabulary.
## 7. `factor` vs `factors` module naming
`pub(crate) mod factor` (`src/lib.rs:19`) and `pub mod factors` (`src/lib.rs:20`) sit adjacent, differing by one character — the latter being the public re-export facade of the former. Confusing to read and easy to mistype. Consider `factor` (private impl) + `factors` → something like `graph` or `custom`.
## Acceptance
- `predict_outcome` supports N teams, returns `Result`, accounts for `p_draw`, and errors on unknown keys.
- `HistoryBuilder` is exported; `Rating` and `Index` have accessors.
- `one_v_one` accepts `GameOptions`.
- No public trait method is unreachable.
- `CHANGELOG.md` records the breaking changes.
Partly done — staying open for the items that are breaking or need a design call.
Done (all additive, no breakage):
2.HistoryBuilder is exported. It was pub but unreachable — use trueskill_tt::HistoryBuilder; failed to resolve.
3.Rating::{prior, beta, drift} accessors added.
4.Index::get() plus impl From<Index> for usize, so the handle intern hands out can be read back.
History::time_slices_len() added (small, needed by the new ingest benchmark).
Still open:
1.predict_outcome — still panics on ≠2 teams, still ignores p_draw, still drops unknown keys. All three want a signature change to Result plus a decision on how draw mass is reported for an N-team prediction. That is design work, not cleanup.
5.one_v_one still hardcodes GameOptions::default(). Fixing it means changing the signature. Less urgent than it was: passing Outcome::draw(2) there now returns a clean TieWithoutDrawProbability error rather than NaN (#8), so the trap is visible.
6.Observer::on_batch_processed is still never called, and still carries the pre-T2 "batch" name. Whether to wire it up or delete it is a call about what the observer contract should be.
7.factor vs factors module naming, unchanged.
predict_quality now documents its panics rather than silently dropping unknown keys — a stopgap until it can return Result alongside predict_outcome.
Partly done — **staying open** for the items that are breaking or need a design call.
Done (all additive, no breakage):
- **2.** `HistoryBuilder` is exported. It was `pub` but unreachable — `use trueskill_tt::HistoryBuilder;` failed to resolve.
- **3.** `Rating::{prior, beta, drift}` accessors added.
- **4.** `Index::get()` plus `impl From<Index> for usize`, so the handle `intern` hands out can be read back.
- `History::time_slices_len()` added (small, needed by the new ingest benchmark).
**Still open:**
- **1.** `predict_outcome` — still panics on ≠2 teams, still ignores `p_draw`, still drops unknown keys. All three want a signature change to `Result` plus a decision on how draw mass is reported for an N-team prediction. That is design work, not cleanup.
- **5.** `one_v_one` still hardcodes `GameOptions::default()`. Fixing it means changing the signature. Less urgent than it was: passing `Outcome::draw(2)` there now returns a clean `TieWithoutDrawProbability` error rather than NaN (#8), so the trap is visible.
- **6.** `Observer::on_batch_processed` is still never called, and still carries the pre-T2 "batch" name. Whether to wire it up or delete it is a call about what the observer contract should be.
- **7.** `factor` vs `factors` module naming, unchanged.
`predict_quality` now documents its panics rather than silently dropping unknown keys — a stopgap until it can return `Result` alongside `predict_outcome`.
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.
A collection of API-surface problems found while auditing. Individually small, collectively they make the crate awkward to consume.
1.
predict_outcomepanics, is 2-team-only, and ignores drawssrc/history.rs:408-429:InferenceError, in a crate that has one.Vec<f64>of length 2 where a[f64; 2]or a named struct would be self-describing.p_drawentirely:[p_a, 1.0 - p_a]allocates no probability mass to a draw even when the history is configured withp_draw > 0. For a draw-enabled model the numbers are simply wrong.filter_map(src/history.rs:415), same defect aspredict_qualityin #9 — a team of entirely unknown players yields a confident-looking probability.2.
HistoryBuilderis not exportedsrc/lib.rs:40re-exportsHistorybut notHistoryBuilder, which ispubinsrc/history.rs:21.History::builder()works for inline chains, but callers cannot name the type:so no
fn make_builder() -> HistoryBuilder<…>, no storing a partially-configured builder. Same forConvergenceOptions' companionEpsilonOrMax, which is reachable only viacrate::factors.3.
Ratingis write-onlyRating's fields are allpub(crate)(src/rating.rs:15-20) with no accessors. A caller can construct one withRating::new(prior, beta, drift)and hand it toGame::ranked, but cannot read backprior,beta, ordrift— so aRatingobtained from anywhere can't be inspected, logged, or serialised. Addpub fn prior(),beta(),drift().4.
Indexis opaque with no way back tousizepub struct Index(usize)(src/lib.rs:64) hasFrom<usize>but no inverse and no accessor.History::internhands callers anIndexandKeyTableis documented as letting "power users promote&KtoIndexand skip the lookup on the hot path" (src/key_table.rs:12-13) — but the returned handle can't be stored in an external side table keyed by integer, printed meaningfully, or round-tripped. Addpub fn get(self) -> usize(orimpl From<Index> for usize).Note
Indexis exported at the crate root but is not in thepub uselist — it is public only by virtue of being declaredpubinlib.rs, which is easy to miss.5.
Game::one_v_onehardcodes default optionssrc/game.rs:517-525ignores any caller configuration:So a 1v1 with a draw cannot set
p_draw, and no 1v1 can set convergence options. Given #8, passingOutcome::draw(2)here returns NaN with no way for the caller to avoid it. Take&GameOptions.6.
Observer::on_batch_processedis never calledDeclared in the trait (
src/observer.rs:20) butgrepfinds no call site — onlyon_iteration_endandon_convergedare invoked (src/history.rs:446,450). Implementors get a callback that never fires. Either call it from the slice sweep or remove it. The name is also pre-T2 ("batch" →TimeSlice);on_slice_processedmatches the current vocabulary.7.
factorvsfactorsmodule namingpub(crate) mod factor(src/lib.rs:19) andpub mod factors(src/lib.rs:20) sit adjacent, differing by one character — the latter being the public re-export facade of the former. Confusing to read and easy to mistype. Considerfactor(private impl) +factors→ something likegraphorcustom.Acceptance
predict_outcomesupports N teams, returnsResult, accounts forp_draw, and errors on unknown keys.HistoryBuilderis exported;RatingandIndexhave accessors.one_v_oneacceptsGameOptions.CHANGELOG.mdrecords the breaking changes.Partly done — staying open for the items that are breaking or need a design call.
Done (all additive, no breakage):
HistoryBuilderis exported. It waspubbut unreachable —use trueskill_tt::HistoryBuilder;failed to resolve.Rating::{prior, beta, drift}accessors added.Index::get()plusimpl From<Index> for usize, so the handleinternhands out can be read back.History::time_slices_len()added (small, needed by the new ingest benchmark).Still open:
predict_outcome— still panics on ≠2 teams, still ignoresp_draw, still drops unknown keys. All three want a signature change toResultplus a decision on how draw mass is reported for an N-team prediction. That is design work, not cleanup.one_v_onestill hardcodesGameOptions::default(). Fixing it means changing the signature. Less urgent than it was: passingOutcome::draw(2)there now returns a cleanTieWithoutDrawProbabilityerror rather than NaN (#8), so the trap is visible.Observer::on_batch_processedis still never called, and still carries the pre-T2 "batch" name. Whether to wire it up or delete it is a call about what the observer contract should be.factorvsfactorsmodule naming, unchanged.predict_qualitynow documents its panics rather than silently dropping unknown keys — a stopgap until it can returnResultalongsidepredict_outcome.