A typo and a deliberately registered competitor are indistinguishable. That matters precisely because register was added to make configuration explicit — and the accessor for "how has this competitor moved" cannot tell you whether it has heard of them.
This is the crate's own stated failure class, from UnknownKey's doc:
Reported rather than skipped: dropping unknown keys turns a team of strangers into a confident-looking probability about nobody.
None = never heard of them. Some(vec![]) = known, no appearances. That also aligns the singular accessors on one convention: Option for "is this competitor known", errors reserved for the calls that compute something.
Breaks: both return types.
Related gaps in the same family
You cannot enumerate a history's competitors. There is no competitors(), keys(), len() or is_empty() on History. KeyTable::keys() exists and KeyTable is exported, but History exposes no way to obtain one — so KeyTable::keys/len/is_empty have zero callers in the entire repo. Answering "who is best", the first question a stats consumer asks, currently requires materialising every competitor's full smoothed curve just to read the last point of each:
The evidence matrix is 3/4 complete, and the missing cell is the one cross-validation needs:
log_evidence() smoothed, all keys
log_evidence_for(keys) smoothed, key-restricted
filtered_log_evidence() forward-only, all keys
filtered_log_evidence_for(..) MISSING
log_evidence_for is documented as "useful for leave-one-out cross-validation" and filtered_log_evidence as "the right quantity for prequential scoring" — per-competitor prequential scoring is exactly the intersection, and it is the one combination you cannot express. The plumbing already takes both axes: log_evidence_internal(forward: bool, targets: &[Index]). Only the (true, targets) corner has no public caller.
Suggested additions:
pubfncompetitors(&self)-> implExactSizeIterator<Item=&K>+'_;pubfncompetitor_count(&self)-> usize;pubfnevent_count(&self)-> usize;// `size` has no accessor
pubfncurrent_skills(&self)-> HashMap<K,Gaussian>;// plural of current_skill
pubfnfiltered_log_evidence_for<Q>(&self,keys: &[&Q])-> f64;
The learning-curve 2×2 itself (learning_curves / learning_curve / filtered_*) is complete and worth keeping as the model the other families should follow.
Breaks: the two return types above; the rest is additive.
Found by an API audit, 2026-09-09; the ambiguity reproduced independently.
| Method | Unknown key |
|---|---|
| `lookup` (`history.rs:382`) | `None` |
| `rating` (`:613`) | `None` |
| `current_skill` (`:622`) | `None` |
| `learning_curve` (`:635`) | **empty `Vec`** |
| `filtered_learning_curve` (`:680`) | **empty `Vec`** |
| `posterior_of` | `Err(UnknownKey)` |
| `log_evidence_for` | silently whole-history (see #66) |
Four different answers to the same question.
## The `learning_curve` case is worse than inconsistent — it is ambiguous
An empty `Vec` is *also* what a **known** competitor with no appearances returns, and `History::register` creates exactly that state by design. Measured:
```
learning_curve("typo") = []
learning_curve("registered_never_played") = []
```
A typo and a deliberately registered competitor are indistinguishable. That matters precisely because `register` was added to make configuration explicit — and the accessor for "how has this competitor moved" cannot tell you whether it has heard of them.
This is the crate's own stated failure class, from `UnknownKey`'s doc:
> Reported rather than skipped: dropping unknown keys turns a team of strangers into a confident-looking probability about nobody.
## Fix
```rust
pub fn learning_curve<Q>(&self, key: &Q) -> Option<Vec<(T, Gaussian)>>;
pub fn filtered_learning_curve<Q>(&self, key: &Q) -> Option<Vec<(T, Gaussian)>>;
```
`None` = never heard of them. `Some(vec![])` = known, no appearances. That also aligns the singular accessors on one convention: `Option` for "is this competitor known", errors reserved for the calls that compute something.
**Breaks:** both return types.
## Related gaps in the same family
**You cannot enumerate a history's competitors.** There is no `competitors()`, `keys()`, `len()` or `is_empty()` on `History`. `KeyTable::keys()` exists and `KeyTable` is exported, but `History` exposes no way to obtain one — so `KeyTable::keys`/`len`/`is_empty` have zero callers in the entire repo. Answering "who is best", the first question a stats consumer asks, currently requires materialising every competitor's full smoothed curve just to read the last point of each:
```rust
let mut board: Vec<_> = h.learning_curves().iter()
.map(|(k, c)| (*k, c.last().unwrap().1.mu())).collect();
```
**The evidence matrix is 3/4 complete**, and the missing cell is the one cross-validation needs:
```
log_evidence() smoothed, all keys
log_evidence_for(keys) smoothed, key-restricted
filtered_log_evidence() forward-only, all keys
filtered_log_evidence_for(..) MISSING
```
`log_evidence_for` is documented as *"useful for leave-one-out cross-validation"* and `filtered_log_evidence` as *"the right quantity for prequential scoring"* — per-competitor prequential scoring is exactly the intersection, and it is the one combination you cannot express. The plumbing already takes both axes: `log_evidence_internal(forward: bool, targets: &[Index])`. Only the `(true, targets)` corner has no public caller.
Suggested additions:
```rust
pub fn competitors(&self) -> impl ExactSizeIterator<Item = &K> + '_;
pub fn competitor_count(&self) -> usize;
pub fn event_count(&self) -> usize; // `size` has no accessor
pub fn current_skills(&self) -> HashMap<K, Gaussian>; // plural of current_skill
pub fn filtered_log_evidence_for<Q>(&self, keys: &[&Q]) -> f64;
```
The learning-curve 2×2 itself (`learning_curves` / `learning_curve` / `filtered_*`) is complete and worth keeping as the model the other families should follow.
**Breaks:** the two return types above; the rest is additive.
Found by an API audit, 2026-09-09; the ambiguity reproduced independently.
Done in e4a68ba (merged as 60fc3e9), for the two curve accessors.
learning_curve and filtered_learning_curve now return Option<Vec<(T, Gaussian)>>: None for a key the history has never interned, Some(vec![]) for a competitor that is registered but has no appearances yet. The two were previously the same empty Vec.
tests/honest_accessors.rs asserts both halves against a control (a key with four appearances), so the test cannot pass by everything collapsing to one answer. tests/properties.rs also needed the distinction — a generated schedule need not touch every key, and that is now an explicit None branch rather than an empty vector that read as a passing assertion.
log_evidence_for was the other member of this family and is covered by #66.
Done in e4a68ba (merged as 60fc3e9), for the two curve accessors.
`learning_curve` and `filtered_learning_curve` now return `Option<Vec<(T, Gaussian)>>`: `None` for a key the history has never interned, `Some(vec![])` for a competitor that is registered but has no appearances yet. The two were previously the same empty `Vec`.
`tests/honest_accessors.rs` asserts both halves against a control (a key with four appearances), so the test cannot pass by everything collapsing to one answer. `tests/properties.rs` also needed the distinction — a generated schedule need not touch every key, and that is now an explicit `None` branch rather than an empty vector that read as a passing assertion.
`log_evidence_for` was the other member of this family and is covered by #66.
The additive half of this issue is now done too, in 86e1521 (merged as cc601c0).
competitors(), competitor_count(), event_count() — landed earlier in the api/cleanup work.
current_skills() -> HashMap<K, Gaussian> — the leaderboard case. A registered-but-unplayed competitor is absent from the map, matching current_skill returning None.
filtered_log_evidence_for — the missing corner.
One correction to this issue's analysis: the missing corner is notlog_evidence_internal(true, targets). That path selects skill.forward as each event's prior, which is only a filtering quantity on a history that has never been converged — iteration alternates sweeps, so from the second iteration the forward message has already absorbed backward information. log_evidence_internal's own doc says so. The new accessor goes through filtered_pass, like filtered_log_evidence, with the restriction applied to which events are scored rather than which are run: a held-out score under the real history, not a score under a counterfactual one where nobody else played.
tests/evidence_matrix.rs pins all four corners as distinct finite log probabilities, and carries controls both ways — naming every competitor must recover the unrestricted value, and the restricted forward-only value must differ from the restricted smoothed one.
The additive half of this issue is now done too, in 86e1521 (merged as cc601c0).
- `competitors()`, `competitor_count()`, `event_count()` — landed earlier in the `api/cleanup` work.
- `current_skills() -> HashMap<K, Gaussian>` — the leaderboard case. A registered-but-unplayed competitor is absent from the map, matching `current_skill` returning `None`.
- `filtered_log_evidence_for` — the missing corner.
One correction to this issue's analysis: the missing corner is **not** `log_evidence_internal(true, targets)`. That path selects `skill.forward` as each event's prior, which is only a filtering quantity on a history that has never been converged — `iteration` alternates sweeps, so from the second iteration the forward message has already absorbed backward information. `log_evidence_internal`'s own doc says so. The new accessor goes through `filtered_pass`, like `filtered_log_evidence`, with the restriction applied to which events are *scored* rather than which are *run*: a held-out score under the real history, not a score under a counterfactual one where nobody else played.
`tests/evidence_matrix.rs` pins all four corners as distinct finite log probabilities, and carries controls both ways — naming every competitor must recover the unrestricted value, and the restricted forward-only value must differ from the restricted smoothed one.
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.
lookup(history.rs:382)Nonerating(:613)Nonecurrent_skill(:622)Nonelearning_curve(:635)Vecfiltered_learning_curve(:680)Vecposterior_ofErr(UnknownKey)log_evidence_forFour different answers to the same question.
The
learning_curvecase is worse than inconsistent — it is ambiguousAn empty
Vecis also what a known competitor with no appearances returns, andHistory::registercreates exactly that state by design. Measured:A typo and a deliberately registered competitor are indistinguishable. That matters precisely because
registerwas added to make configuration explicit — and the accessor for "how has this competitor moved" cannot tell you whether it has heard of them.This is the crate's own stated failure class, from
UnknownKey's doc:Fix
None= never heard of them.Some(vec![])= known, no appearances. That also aligns the singular accessors on one convention:Optionfor "is this competitor known", errors reserved for the calls that compute something.Breaks: both return types.
Related gaps in the same family
You cannot enumerate a history's competitors. There is no
competitors(),keys(),len()oris_empty()onHistory.KeyTable::keys()exists andKeyTableis exported, butHistoryexposes no way to obtain one — soKeyTable::keys/len/is_emptyhave zero callers in the entire repo. Answering "who is best", the first question a stats consumer asks, currently requires materialising every competitor's full smoothed curve just to read the last point of each:The evidence matrix is 3/4 complete, and the missing cell is the one cross-validation needs:
log_evidence_foris documented as "useful for leave-one-out cross-validation" andfiltered_log_evidenceas "the right quantity for prequential scoring" — per-competitor prequential scoring is exactly the intersection, and it is the one combination you cannot express. The plumbing already takes both axes:log_evidence_internal(forward: bool, targets: &[Index]). Only the(true, targets)corner has no public caller.Suggested additions:
The learning-curve 2×2 itself (
learning_curves/learning_curve/filtered_*) is complete and worth keeping as the model the other families should follow.Breaks: the two return types above; the rest is additive.
Found by an API audit, 2026-09-09; the ambiguity reproduced independently.
Done in
e4a68ba(merged as60fc3e9), for the two curve accessors.learning_curveandfiltered_learning_curvenow returnOption<Vec<(T, Gaussian)>>:Nonefor a key the history has never interned,Some(vec![])for a competitor that is registered but has no appearances yet. The two were previously the same emptyVec.tests/honest_accessors.rsasserts both halves against a control (a key with four appearances), so the test cannot pass by everything collapsing to one answer.tests/properties.rsalso needed the distinction — a generated schedule need not touch every key, and that is now an explicitNonebranch rather than an empty vector that read as a passing assertion.log_evidence_forwas the other member of this family and is covered by #66.The additive half of this issue is now done too, in
86e1521(merged ascc601c0).competitors(),competitor_count(),event_count()— landed earlier in theapi/cleanupwork.current_skills() -> HashMap<K, Gaussian>— the leaderboard case. A registered-but-unplayed competitor is absent from the map, matchingcurrent_skillreturningNone.filtered_log_evidence_for— the missing corner.One correction to this issue's analysis: the missing corner is not
log_evidence_internal(true, targets). That path selectsskill.forwardas each event's prior, which is only a filtering quantity on a history that has never been converged —iterationalternates sweeps, so from the second iteration the forward message has already absorbed backward information.log_evidence_internal's own doc says so. The new accessor goes throughfiltered_pass, likefiltered_log_evidence, with the restriction applied to which events are scored rather than which are run: a held-out score under the real history, not a score under a counterfactual one where nobody else played.tests/evidence_matrix.rspins all four corners as distinct finite log probabilities, and carries controls both ways — naming every competitor must recover the unrestricted value, and the restricted forward-only value must differ from the restricted smoothed one.