docs: add a migration guide for 0.9.0, and drop merge noise from the changelog
Twenty-one breaking changes in one release, with a live consumer. The
changelog lists them; `MIGRATING.md` says what to do about them, leading
with the three that change what an existing, *compiling* call returns —
unknown keys, predictions from a broken fit, and `Gaussian`'s operators
— since those are the ones the compiler will not find for you.
Every "after" snippet was compiled, not written from memory, and doing
so caught three errors in my own guide:
- `log_evidence_for(&[&"alice"])` does not compile at `K = String`. The
right spelling is `&["alice"]`, which works at *both* key types —
checked, because a guide that is right for half its readers is worse
than no guide.
- the same for `filtered_log_evidence_for`
- the `Analysis<'h> { joint: Joint<'h> }` example needs a history at the
default key type; pairing it with a `History<String>` does not compile
git-cliff skips merge commits now. Every branch lands with `--no-ff`, so
a release's merges outnumber its real commits and say nothing the merged
ones do not — 0.9.0's changelog had fourteen lines of them under "Other
(unconventional)". `ci:` commits get a group instead of falling through
to that catch-all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
This commit is contained in:
+204
@@ -0,0 +1,204 @@
|
||||
# Migrating
|
||||
|
||||
## 0.8.0 → 0.9.0
|
||||
|
||||
Twenty-one breaking changes. Nearly all of them are mechanical, and the
|
||||
compiler finds every one — nothing here changes behaviour silently.
|
||||
|
||||
Three exceptions are worth reading before you start, because they change
|
||||
what an existing, compiling call *returns*: [unknown keys](#unknown-keys-are-reported-not-skipped),
|
||||
[predictions from a broken fit](#predictions-refuse-a-fit-they-cannot-answer-from),
|
||||
and [`Gaussian`'s operators](#gaussians-operators-are-gone).
|
||||
|
||||
### Type parameters: `K` comes first
|
||||
|
||||
`K` was last, so naming a history meant writing all four parameters to change
|
||||
the one that matters.
|
||||
|
||||
```rust
|
||||
// before
|
||||
struct Ladder { history: History<i64, ConstantDrift, NullObserver, String> }
|
||||
struct Analysis<'h> { joint: Joint<'h, i64, ConstantDrift, NullObserver, &'static str> }
|
||||
|
||||
// after
|
||||
struct Ladder { history: History<String> }
|
||||
struct Analysis<'h> { joint: Joint<'h> }
|
||||
```
|
||||
|
||||
`History<K, T, D, O, R>` — key, time, drift, observer, rating rule — all
|
||||
defaulted. `HistoryBuilder` matches. There is a fifth parameter now (`R`), and
|
||||
you will never write it unless you use `default_rating_for`.
|
||||
|
||||
`HistoryBuilder::<Untimed, _, _, String>::new()` becomes
|
||||
`HistoryBuilder::<String, Untimed>::new()`.
|
||||
|
||||
### Predictions and joint queries take borrowed keys
|
||||
|
||||
At `K = String` a string literal used to be impossible, and asking "who wins"
|
||||
cost four allocations of temporaries that all had to outlive the call.
|
||||
|
||||
```rust
|
||||
// before, at K = String
|
||||
let ta = vec![a.to_string()];
|
||||
let ra: Vec<&String> = ta.iter().collect();
|
||||
let tb = vec![b.to_string()];
|
||||
let rb: Vec<&String> = tb.iter().collect();
|
||||
let teams: Vec<&[&String]> = vec![&ra, &rb];
|
||||
h.predict_win_probabilities(&teams)?;
|
||||
|
||||
// after, at either key type
|
||||
h.predict_win_probabilities(&[&["alice"], &["bob"]])?;
|
||||
h.posterior_of(&[("alice", 1.0), ("bob", -1.0)])?;
|
||||
```
|
||||
|
||||
At `K = &'static str` the old `&[&[&"a"]]` spelling still compiles — `Q` infers
|
||||
to `&str` and the two shapes coincide — so this is only a break for owned keys,
|
||||
where nothing compiled before.
|
||||
|
||||
One cost: `predict_outcome(&[])` can no longer infer the key type. Annotate it,
|
||||
`let none: &[&[&str]] = &[];`. It bites only on that degenerate call.
|
||||
|
||||
### Unknown keys are reported, not skipped
|
||||
|
||||
**Read this one.** `log_evidence_for` used to `filter_map` unknown keys away,
|
||||
and an empty target list means *no restriction* downstream — so a list of
|
||||
entirely unknown keys returned the **whole-history** value. Measured:
|
||||
`log_evidence_for(["typo"])` returned exactly `log_evidence()`. On the one
|
||||
workload it is documented for, leave-one-out cross-validation, that is the
|
||||
un-held-out score.
|
||||
|
||||
```rust
|
||||
let e = h.log_evidence_for(&["alice"])?; // now Result
|
||||
let curve = h.learning_curve("alice"); // now Option
|
||||
```
|
||||
|
||||
Note `&["alice"]`, not `&[&"alice"]`. These take borrowed keys like the
|
||||
prediction methods, so one spelling works at both key types.
|
||||
|
||||
`learning_curve` and `filtered_learning_curve` return `Option`: `None` is "never
|
||||
heard of this key", `Some(vec![])` is "known, has not played". They used to be
|
||||
the same empty `Vec`.
|
||||
|
||||
### Predictions refuse a fit they cannot answer from
|
||||
|
||||
**Read this one too.** `converge` already refused to report a NaN fit, but
|
||||
nothing stopped a caller ignoring that error and predicting anyway. On a
|
||||
NaN-poisoned fit, `quality` returned `Ok(NaN)`, `predict_outcome().total()` was
|
||||
`NaN`, and `predict_win_probabilities` returned `Ok([0.0, 0.0])` — finite,
|
||||
plausible, and summing to zero against a doc promising one.
|
||||
|
||||
Every `predict_*` path now returns `Err(NonFiniteSkill { .. })` there, and
|
||||
`Err(NoPerformanceVariance)` when `beta` is zero and every skill is a point
|
||||
mass. If you were ignoring `converge`'s error, you will start seeing these.
|
||||
|
||||
### `Gaussian`'s operators are gone
|
||||
|
||||
`Mul`, `Div`, `Add` and `Sub` were the EP product, cavity and variance-space
|
||||
convolutions, not arithmetic — `N(10,2) * N(4,3)` is `N(8.15, 1.66)`, and
|
||||
`a / c` could leave a negative precision whose `mu()` printed a confident `0`.
|
||||
|
||||
They are `pub(crate)` inherent methods now. The public surface is `from_ms`,
|
||||
`from_mv`, `mu`, `sigma`, `variance`, `probability_below`, `probability_above`;
|
||||
`pi()` and `tau()` are internal. If you compared fits bit-for-bit on
|
||||
`(pi, tau)`, compare `(mu, variance)` — same information, still exact.
|
||||
|
||||
### `Game` is the type you get
|
||||
|
||||
`Game::ranked` returned an `OwnedGame`, so `let g: Game = Game::ranked(..)?` did
|
||||
not compile. Names swapped: `Game<T, D>` is public, `OwnedGame` is gone.
|
||||
|
||||
`one_v_one` returns a `Game` rather than `(Gaussian, Gaussian)`, so it can be
|
||||
asked for `log_evidence()` like its siblings. For the old shape:
|
||||
|
||||
```rust
|
||||
let post = Game::one_v_one(&a, &b, outcome, &opts)?.posteriors();
|
||||
let (a_post, b_post) = (post[0][0], post[1][0]);
|
||||
```
|
||||
|
||||
### The joint is reached through `Joint`
|
||||
|
||||
`History::posterior_of`, `posterior_of_at` and `expected_variance_reduction`
|
||||
were one-shot wrappers that re-factorised on every call. They are gone.
|
||||
|
||||
```rust
|
||||
// before — pays for the factorisation twice
|
||||
let a = h.posterior_of(&terms)?;
|
||||
let b = h.posterior_of(&other)?;
|
||||
|
||||
// after — pays once, and the borrow says so
|
||||
let joint = h.joint()?;
|
||||
let a = joint.posterior_of(&terms)?;
|
||||
let b = joint.posterior_of(&other)?;
|
||||
```
|
||||
|
||||
### `InferenceError` is typed
|
||||
|
||||
Six variants carried `&'static str` discriminators. Four enums replace them:
|
||||
`Parameter`, `Shape`, `OutcomeKind`, `CompetitorField`.
|
||||
|
||||
```rust
|
||||
// before
|
||||
InferenceError::InvalidParameter { name: "drift_scale", value }
|
||||
InferenceError::MismatchedShape { kind: "ranks vs teams", .. }
|
||||
InferenceError::WrongOutcomeKind { context, expected, got } // three &str
|
||||
|
||||
// after
|
||||
InferenceError::InvalidParameter { parameter: Parameter::DriftScale, value }
|
||||
InferenceError::MismatchedShape { shape: Shape::OutcomeVsTeams, .. }
|
||||
InferenceError::WrongOutcomeKind { expected: OutcomeKind::Ranked, got }
|
||||
```
|
||||
|
||||
Variants that split or merged:
|
||||
|
||||
| before | after |
|
||||
|---|---|
|
||||
| `InvalidProbability { value }` | `InvalidParameter { parameter: Parameter::PDraw, value }` |
|
||||
| `JointUnavailable { reason }` | `EmptyHistory`, `JointRequiresScoredEvents`, `NotPositiveDefinite` |
|
||||
| `NonFiniteResult { context, step }` | `NonFiniteStep { context, step }` (convergence), `NonFiniteSkill { mu, sigma }` (prediction) |
|
||||
|
||||
Every struct variant is `#[non_exhaustive]`, so `match` with a `..` and
|
||||
construct through the library.
|
||||
|
||||
### Renames
|
||||
|
||||
| before | after |
|
||||
|---|---|
|
||||
| `History::predict_quality` | `History::quality` |
|
||||
| `Outcome::scores_with_sigma` | `Outcome::scores_with_noise` |
|
||||
| `EventBuilder::scores_with_sigma` | `EventBuilder::scores_with_noise` |
|
||||
| `Outcome::Scored { sigma }` | `Outcome::Scored { score_sigma }` |
|
||||
| `OwnedGame` | `Game` |
|
||||
|
||||
### Removed
|
||||
|
||||
`History::intern`, `History::lookup` and `Index`. Nothing public ever accepted
|
||||
an `Index`, so there was nothing to do with one. `current_skill`, `rating` and
|
||||
`learning_curve` answer "does this history know this key" and all take a
|
||||
borrowed key.
|
||||
|
||||
`TimeSlice`, `EventKind`, `KeyTable`, `CompetitorStore`, `Competitor` and `N01`
|
||||
are no longer exported. None was obtainable from a `History`.
|
||||
|
||||
### Warnings, not errors
|
||||
|
||||
`#[must_use]` now sits on the value types, so a dropped `EventBuilder` — an
|
||||
event you forgot to `.commit()`, previously a silent no-op — warns. So do
|
||||
dropped `Team`, `Member`, `Outcome` and `Joint` values. A `-D warnings` build
|
||||
will need updating.
|
||||
|
||||
### Nothing to do, but worth knowing
|
||||
|
||||
The joint factorisation is sparse with an AMD fill-reducing ordering:
|
||||
**745 ms → 1.11 ms** on a 1976-appearance fixture, and near-linear scaling where
|
||||
it was cubic. Results are unchanged; `feral-amd` is a new dependency (two
|
||||
crates, both `#![forbid(unsafe_code)]`).
|
||||
|
||||
`HistoryBuilder::gamma(f64)` is shorthand for
|
||||
`.drift(ConstantDrift::new(gamma))`.
|
||||
|
||||
`History::current_skills()` is the leaderboard query — every competitor's latest
|
||||
posterior in one pass, rather than a full smoothed curve each.
|
||||
|
||||
`History::filtered_log_evidence_for(&["alice"])` completes the evidence matrix:
|
||||
forward-only *and* key-restricted, which is what per-competitor prequential
|
||||
scoring needs.
|
||||
Reference in New Issue
Block a user