EventBuilder is the crate's headline ingestion ergonomic, and forgetting the terminal .commit() is a silent no-op.
Measured
letmuth=History::builder().build();h.event(1).team(["x"]).team(["y"]).ranking([0,1]);// no .commit()
time_slices_len = 0
current_skill(x) = None
No warning, no error. The event is simply not there, and the next thing the caller does is converge an empty history and read None skills.
ConvergenceReport carries a #[must_use] with an excellent message for exactly this class of mistake. EventBuilder — where the mistake is easier to make and the consequence is silent rather than merely unchecked — has none.
Absent: EventBuilder and every one of its setters; HistoryBuilder and all nine of its setters — so builder.p_draw(0.1); is also a silent no-op; History::log_evidence, log_evidence_for, current_skill, learning_curve(s), filtered_learning_curve(s), lookup, joint, every predict_*, posterior_of*, expected_*; the types Prediction, Gaussian, Joint, OwnedGame; Member::with_*, Team::with_members, Outcome::ranking/scores/try_winner.
filtered_log_evidence has it and log_evidence does not. rating has it and current_skill does not. Rating::with_drift_scale has it and Member::with_drift_scale does not. There is no rule, which is what makes it worth fixing in one pass rather than case by case.
A second, opposite problem: ConvergenceReport's #[must_use] misfires on converge()
Its message reads:
from converge_partial this may describe a fit that stopped at max_iter …
but it fires on converge() too, where that reason is false — converge returns Err(NotConverged) in exactly that case. So every quickstart has to write let _ = h.converge()?;, and the crate's own examples disagree about it: src/lib.rs writes let report = …; assert!(report.converged), the README writes h.converge().unwrap(); (which warns), and several doctests write let _ =.
Fix
#[must_use] on the types EventBuilder, HistoryBuilder, Prediction, Gaussian, Joint, OwnedGame, which covers most method returns at once, plus the individual accessors listed above.
EventBuilder gets a message in ConvergenceReport's style:
#[must_use = "an event is only recorded by `.commit()`; a dropped builder \ silently ingests nothing"]
Move ConvergenceReport's #[must_use] off the type and onto converge_partial's return, so the warning only fires where its reason is true. Then fix the three disagreeing quickstarts to one spelling.
Breaks: nothing at compile time — warnings only. Downstream -D warnings builds will need updating, which is worth a changelog line.
Found by an API audit, 2026-09-09; the no-op reproduced independently.
`EventBuilder` is the crate's headline ingestion ergonomic, and forgetting the terminal `.commit()` is a silent no-op.
## Measured
```rust
let mut h = History::builder().build();
h.event(1).team(["x"]).team(["y"]).ranking([0, 1]); // no .commit()
```
```
time_slices_len = 0
current_skill(x) = None
```
No warning, no error. The event is simply not there, and the next thing the caller does is converge an empty history and read `None` skills.
`ConvergenceReport` carries a `#[must_use]` with an excellent message for exactly this class of mistake. `EventBuilder` — where the mistake is easier to make and the consequence is silent rather than merely unchecked — has none.
## The coverage is inconsistent generally
Present: `ConvergenceReport`, `Gaussian`'s accessors, `Rating`'s getters, `ConstantDrift::new`/`gamma`, `Outcome::winner`/`draw`, `Joint::variables`, `History::builder`/`rating`/`observer`/`into_observer`/`filtered_log_evidence`/`time_slices_len`.
Absent: **`EventBuilder`** and every one of its setters; **`HistoryBuilder`** and all nine of its setters — so `builder.p_draw(0.1);` is also a silent no-op; `History::log_evidence`, `log_evidence_for`, `current_skill`, `learning_curve(s)`, `filtered_learning_curve(s)`, `lookup`, `joint`, every `predict_*`, `posterior_of*`, `expected_*`; the types `Prediction`, `Gaussian`, `Joint`, `OwnedGame`; `Member::with_*`, `Team::with_members`, `Outcome::ranking`/`scores`/`try_winner`.
`filtered_log_evidence` has it and `log_evidence` does not. `rating` has it and `current_skill` does not. `Rating::with_drift_scale` has it and `Member::with_drift_scale` does not. There is no rule, which is what makes it worth fixing in one pass rather than case by case.
## A second, opposite problem: `ConvergenceReport`'s `#[must_use]` misfires on `converge()`
Its message reads:
> from `converge_partial` this may describe a fit that stopped at `max_iter` …
but it fires on `converge()` too, where that reason is **false** — `converge` returns `Err(NotConverged)` in exactly that case. So every quickstart has to write `let _ = h.converge()?;`, and the crate's own examples disagree about it: `src/lib.rs` writes `let report = …; assert!(report.converged)`, the README writes `h.converge().unwrap();` (which warns), and several doctests write `let _ =`.
## Fix
- `#[must_use]` on the types `EventBuilder`, `HistoryBuilder`, `Prediction`, `Gaussian`, `Joint`, `OwnedGame`, which covers most method returns at once, plus the individual accessors listed above.
- `EventBuilder` gets a message in `ConvergenceReport`'s style:
```rust
#[must_use = "an event is only recorded by `.commit()`; a dropped builder \
silently ingests nothing"]
```
- Move `ConvergenceReport`'s `#[must_use]` off the **type** and onto `converge_partial`'s return, so the warning only fires where its reason is true. Then fix the three disagreeing quickstarts to one spelling.
**Breaks:** nothing at compile time — warnings only. Downstream `-D warnings` builds will need updating, which is worth a changelog line.
Found by an API audit, 2026-09-09; the no-op reproduced independently.
The rule the issue asked for: #[must_use] goes on the type, not on each method. That covers every constructor and builder setter in one place, so there is nothing to keep in sync. EventBuilder, HistoryBuilder, Prediction, Gaussian and OwnedGame already had it; this adds Joint, Team, Member and Outcome, plus Prediction::outcomes (an iterator, so the type rule does not reach it). The now-redundant per-method attributes on Team::new, Outcome::winner and Outcome::draw came off — clippy's double_must_use flags them.
Everything else in the "absent" list turned out to be a Result, which is already #[must_use].
Verified by compiling a program that drops each value and reading the warnings back, rather than assuming the attribute took:
warning: unused `EventBuilder` that must be used
warning: unused `Team` that must be used
warning: unused `Member` that must be used
warning: unused `Outcome` that must be used
warning: unused return value of `History::current_skills` that must be used
The ConvergenceReport half was already resolved in the earlier api/cleanup work: the attribute now sits on converge_partial's return with the "may have stopped at max_iter" reason, not on the type, so it no longer fires on converge where that reason is false.
Fixed in e4d6dc4 (merged as c3d1afe).
The rule the issue asked for: `#[must_use]` goes on the **type**, not on each method. That covers every constructor and builder setter in one place, so there is nothing to keep in sync. `EventBuilder`, `HistoryBuilder`, `Prediction`, `Gaussian` and `OwnedGame` already had it; this adds `Joint`, `Team`, `Member` and `Outcome`, plus `Prediction::outcomes` (an iterator, so the type rule does not reach it). The now-redundant per-method attributes on `Team::new`, `Outcome::winner` and `Outcome::draw` came off — clippy's `double_must_use` flags them.
Everything else in the "absent" list turned out to be a `Result`, which is already `#[must_use]`.
Verified by compiling a program that drops each value and reading the warnings back, rather than assuming the attribute took:
```
warning: unused `EventBuilder` that must be used
warning: unused `Team` that must be used
warning: unused `Member` that must be used
warning: unused `Outcome` that must be used
warning: unused return value of `History::current_skills` that must be used
```
The `ConvergenceReport` half was already resolved in the earlier `api/cleanup` work: the attribute now sits on `converge_partial`'s return with the "may have stopped at `max_iter`" reason, not on the type, so it no longer fires on `converge` where that reason is false.
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.
EventBuilderis the crate's headline ingestion ergonomic, and forgetting the terminal.commit()is a silent no-op.Measured
No warning, no error. The event is simply not there, and the next thing the caller does is converge an empty history and read
Noneskills.ConvergenceReportcarries a#[must_use]with an excellent message for exactly this class of mistake.EventBuilder— where the mistake is easier to make and the consequence is silent rather than merely unchecked — has none.The coverage is inconsistent generally
Present:
ConvergenceReport,Gaussian's accessors,Rating's getters,ConstantDrift::new/gamma,Outcome::winner/draw,Joint::variables,History::builder/rating/observer/into_observer/filtered_log_evidence/time_slices_len.Absent:
EventBuilderand every one of its setters;HistoryBuilderand all nine of its setters — sobuilder.p_draw(0.1);is also a silent no-op;History::log_evidence,log_evidence_for,current_skill,learning_curve(s),filtered_learning_curve(s),lookup,joint, everypredict_*,posterior_of*,expected_*; the typesPrediction,Gaussian,Joint,OwnedGame;Member::with_*,Team::with_members,Outcome::ranking/scores/try_winner.filtered_log_evidencehas it andlog_evidencedoes not.ratinghas it andcurrent_skilldoes not.Rating::with_drift_scalehas it andMember::with_drift_scaledoes not. There is no rule, which is what makes it worth fixing in one pass rather than case by case.A second, opposite problem:
ConvergenceReport's#[must_use]misfires onconverge()Its message reads:
but it fires on
converge()too, where that reason is false —convergereturnsErr(NotConverged)in exactly that case. So every quickstart has to writelet _ = h.converge()?;, and the crate's own examples disagree about it:src/lib.rswriteslet report = …; assert!(report.converged), the README writesh.converge().unwrap();(which warns), and several doctests writelet _ =.Fix
#[must_use]on the typesEventBuilder,HistoryBuilder,Prediction,Gaussian,Joint,OwnedGame, which covers most method returns at once, plus the individual accessors listed above.EventBuildergets a message inConvergenceReport's style:ConvergenceReport's#[must_use]off the type and ontoconverge_partial's return, so the warning only fires where its reason is true. Then fix the three disagreeing quickstarts to one spelling.Breaks: nothing at compile time — warnings only. Downstream
-D warningsbuilds will need updating, which is worth a changelog line.Found by an API audit, 2026-09-09; the no-op reproduced independently.
Fixed in
e4d6dc4(merged asc3d1afe).The rule the issue asked for:
#[must_use]goes on the type, not on each method. That covers every constructor and builder setter in one place, so there is nothing to keep in sync.EventBuilder,HistoryBuilder,Prediction,GaussianandOwnedGamealready had it; this addsJoint,Team,MemberandOutcome, plusPrediction::outcomes(an iterator, so the type rule does not reach it). The now-redundant per-method attributes onTeam::new,Outcome::winnerandOutcome::drawcame off — clippy'sdouble_must_useflags them.Everything else in the "absent" list turned out to be a
Result, which is already#[must_use].Verified by compiling a program that drops each value and reading the warnings back, rather than assuming the attribute took:
The
ConvergenceReporthalf was already resolved in the earlierapi/cleanupwork: the attribute now sits onconverge_partial's return with the "may have stopped atmax_iter" reason, not on the type, so it no longer fires onconvergewhere that reason is false.