Three of the four ingestion routes cannot express competitor configuration. Only the typed add_events path can.
Noticed while documenting Member::with_drift_scale (#34) for the README (#35): the natural place to show the feature is the fluent builder, and the fluent builder cannot do it.
The gap
EventBuilder::team constructs members through Member::new and never exposes the rest of the builder (src/event_builder.rs:53-54):
weights is the only per-member setter it offers (src/event_builder.rs:70). There is no priors or drift_scales counterpart, so this cannot be written:
h.event(0).team(["player"]).team(["layout_7"])// want drift_scale 0.0 here
.ranking([0,1]).commit()?;
record_winner and record_draw are more absolute — both hand add_events_with_prior an empty map (src/history.rs:890, :914), so a competitor whose first appearance arrives through either one is permanently stuck on the history defaults.
Combined with first-appearance capture (#34), that is worse than an ergonomic wart: which route a competitor happens to arrive through decides whether they can ever be configured. See #38 for the lifecycle half of this.
Shape
Mirroring weights, which already records a length mismatch as MismatchedShape rather than panicking:
Worth considering instead: a members(...) escape hatch taking Member<K> values directly, which covers every present and future per-member field in one method rather than growing a parallel setter each time Member gains one. team stays the common case.
record_winner / record_draw are deliberately the two-argument convenience path; extending them is probably wrong. If #38 lands a pre-registration API, that covers them without touching their signatures — which is an argument for doing #38 first and keeping this issue to EventBuilder.
Tests
Each new setter reaches the same posterior as the equivalent typed add_events call.
A length mismatch returns MismatchedShape from commit, applies nothing, and cannot reach the history — matching the guarantee weights documents at src/event_builder.rs:60-65.
An invalid drift_scale surfaces as InvalidParameter from commit.
Three of the four ingestion routes cannot express competitor configuration. Only the typed `add_events` path can.
Noticed while documenting `Member::with_drift_scale` (#34) for the README (#35): the natural place to show the feature is the fluent builder, and the fluent builder cannot do it.
## The gap
`EventBuilder::team` constructs members through `Member::new` and never exposes the rest of the builder (`src/event_builder.rs:53-54`):
```rust
pub fn team<I: IntoIterator<Item = K>>(mut self, keys: I) -> Self {
let members: SmallVec<[Member<K>; 4]> = keys.into_iter().map(Member::new).collect();
...
}
```
`weights` is the only per-member setter it offers (`src/event_builder.rs:70`). There is no `priors` or `drift_scales` counterpart, so this cannot be written:
```rust
h.event(0)
.team(["player"])
.team(["layout_7"]) // want drift_scale 0.0 here
.ranking([0, 1])
.commit()?;
```
`record_winner` and `record_draw` are more absolute — both hand `add_events_with_prior` an empty map (`src/history.rs:890`, `:914`), so a competitor whose first appearance arrives through either one is permanently stuck on the history defaults.
Combined with first-appearance capture (#34), that is worse than an ergonomic wart: which route a competitor happens to arrive through decides whether they can ever be configured. See #38 for the lifecycle half of this.
## Shape
Mirroring `weights`, which already records a length mismatch as `MismatchedShape` rather than panicking:
```rust
h.event(0)
.team(["player", "layout_7"])
.drift_scales([1.0, 0.0])
.priors([None, Some(Gaussian::from_ms(0.0, 1.0))])
.ranking([0, 1])
.commit()?;
```
Worth considering instead: a `members(...)` escape hatch taking `Member<K>` values directly, which covers every present and future per-member field in one method rather than growing a parallel setter each time `Member` gains one. `team` stays the common case.
`record_winner` / `record_draw` are deliberately the two-argument convenience path; extending them is probably wrong. If #38 lands a pre-registration API, that covers them without touching their signatures — which is an argument for doing #38 first and keeping this issue to `EventBuilder`.
## Tests
- Each new setter reaches the same posterior as the equivalent typed `add_events` call.
- A length mismatch returns `MismatchedShape` from `commit`, applies nothing, and cannot reach the history — matching the guarantee `weights` documents at `src/event_builder.rs:60-65`.
- An invalid `drift_scale` surfaces as `InvalidParameter` from `commit`.
team stays the common case. One method rather than priors and drift_scales beside weights, because a parallel array per field means a parallel length check per field, and each one is a new way to get the lengths wrong. Member already has a builder; this just lets the fluent path reach it, and it covers whatever Member gains next for free.
Your three tests, in tests/event_builder_members.rs: members reaches the typed path's posterior bit for bit; weights still records MismatchedShape on a members team and applies nothing; an invalid drift_scale surfaces as InvalidParameter from commit for -1.0, NaN and infinity, with nothing reaching the history.
There is a fourth, because the first three would all pass if members recorded the configuration and inference ignored it. A competitor pinned at drift_scale = 0.0 is checked not to move across slices, against an unpinned control in the same fit. Worth saying how that one nearly went wrong: I first asserted the pinned marginals were bit-identical across slices. They are not — each slice combines its own forward and backward messages, so the last bit moves. "Pinned" promises that no drift variance accumulates, not that the arithmetic is identical, and asserting the stronger thing would have pinned a property the model does not have.
record_winner / record_draw
You wrote that extending them is probably wrong, and that #38's pre-registration would cover them without touching their signatures. Both still hold — they are pub and positional, so adding parameters is breaking.
But the premise underneath has weakened. This issue says a competitor whose first appearance arrives through either one is "permanently stuck on the history defaults". That stopped being true at 8c087ad in 0.4.0, which made competitor configuration apply whenever it is supplied, including on a key the history already knows, with a forward refresh from the earliest affected slice. Measured on exactly the route this issue names:
first seen via record_winner, configured later : mu = 40.000000000
configured from the start : mu = 40.000000000
never configured : mu = 4.027250930
So it is repairable, and the gap is ergonomic rather than a trap. That removes the urgency argument for touching those two signatures. The remaining case for pre-registration is #38's own — declaring configuration before any event exists — which is a better-shaped feature than overloading the convenience path.
Closing this as the EventBuilder half, which is what your own reasoning scoped it to.
Done for `EventBuilder`, in `7c6965c`. `record_winner`/`record_draw` left alone, for the reason you gave and one you could not have known yet.
## `EventBuilder::members`
Your "worth considering instead" won:
```rust
h.event(0)
.team(["player"])
.members([Member::new("layout_7")
.with_drift_scale(0.0)
.with_prior(Gaussian::from_ms(0.0, 1.0))])
.ranking([0, 1])
.commit()?;
```
`team` stays the common case. One method rather than `priors` and `drift_scales` beside `weights`, because a parallel array per field means a parallel length check per field, and each one is a new way to get the lengths wrong. `Member` already has a builder; this just lets the fluent path reach it, and it covers whatever `Member` gains next for free.
Your three tests, in `tests/event_builder_members.rs`: `members` reaches the typed path's posterior bit for bit; `weights` still records `MismatchedShape` on a `members` team and applies nothing; an invalid `drift_scale` surfaces as `InvalidParameter` from `commit` for `-1.0`, NaN and infinity, with nothing reaching the history.
There is a fourth, because the first three would all pass if `members` recorded the configuration and inference ignored it. A competitor pinned at `drift_scale = 0.0` is checked not to move across slices, against an unpinned control in the same fit. Worth saying how that one nearly went wrong: I first asserted the pinned marginals were bit-identical across slices. They are not — each slice combines its own forward and backward messages, so the last bit moves. "Pinned" promises that no drift variance accumulates, not that the arithmetic is identical, and asserting the stronger thing would have pinned a property the model does not have.
## `record_winner` / `record_draw`
You wrote that extending them is probably wrong, and that #38's pre-registration would cover them without touching their signatures. Both still hold — they are `pub` and positional, so adding parameters is breaking.
But the premise underneath has weakened. This issue says a competitor whose first appearance arrives through either one is **"permanently stuck on the history defaults"**. That stopped being true at `8c087ad` in 0.4.0, which made competitor configuration apply whenever it is supplied, including on a key the history already knows, with a forward refresh from the earliest affected slice. Measured on exactly the route this issue names:
```
first seen via record_winner, configured later : mu = 40.000000000
configured from the start : mu = 40.000000000
never configured : mu = 4.027250930
```
So it is repairable, and the gap is ergonomic rather than a trap. That removes the urgency argument for touching those two signatures. The remaining case for pre-registration is #38's own — declaring configuration before any event exists — which is a better-shaped feature than overloading the convenience path.
Closing this as the `EventBuilder` half, which is what your own reasoning scoped it to.
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.
Three of the four ingestion routes cannot express competitor configuration. Only the typed
add_eventspath can.Noticed while documenting
Member::with_drift_scale(#34) for the README (#35): the natural place to show the feature is the fluent builder, and the fluent builder cannot do it.The gap
EventBuilder::teamconstructs members throughMember::newand never exposes the rest of the builder (src/event_builder.rs:53-54):weightsis the only per-member setter it offers (src/event_builder.rs:70). There is nopriorsordrift_scalescounterpart, so this cannot be written:record_winnerandrecord_draware more absolute — both handadd_events_with_prioran empty map (src/history.rs:890,:914), so a competitor whose first appearance arrives through either one is permanently stuck on the history defaults.Combined with first-appearance capture (#34), that is worse than an ergonomic wart: which route a competitor happens to arrive through decides whether they can ever be configured. See #38 for the lifecycle half of this.
Shape
Mirroring
weights, which already records a length mismatch asMismatchedShaperather than panicking:Worth considering instead: a
members(...)escape hatch takingMember<K>values directly, which covers every present and future per-member field in one method rather than growing a parallel setter each timeMembergains one.teamstays the common case.record_winner/record_draware deliberately the two-argument convenience path; extending them is probably wrong. If #38 lands a pre-registration API, that covers them without touching their signatures — which is an argument for doing #38 first and keeping this issue toEventBuilder.Tests
add_eventscall.MismatchedShapefromcommit, applies nothing, and cannot reach the history — matching the guaranteeweightsdocuments atsrc/event_builder.rs:60-65.drift_scalesurfaces asInvalidParameterfromcommit.Done for
EventBuilder, in7c6965c.record_winner/record_drawleft alone, for the reason you gave and one you could not have known yet.EventBuilder::membersYour "worth considering instead" won:
teamstays the common case. One method rather thanpriorsanddrift_scalesbesideweights, because a parallel array per field means a parallel length check per field, and each one is a new way to get the lengths wrong.Memberalready has a builder; this just lets the fluent path reach it, and it covers whateverMembergains next for free.Your three tests, in
tests/event_builder_members.rs:membersreaches the typed path's posterior bit for bit;weightsstill recordsMismatchedShapeon amembersteam and applies nothing; an invaliddrift_scalesurfaces asInvalidParameterfromcommitfor-1.0, NaN and infinity, with nothing reaching the history.There is a fourth, because the first three would all pass if
membersrecorded the configuration and inference ignored it. A competitor pinned atdrift_scale = 0.0is checked not to move across slices, against an unpinned control in the same fit. Worth saying how that one nearly went wrong: I first asserted the pinned marginals were bit-identical across slices. They are not — each slice combines its own forward and backward messages, so the last bit moves. "Pinned" promises that no drift variance accumulates, not that the arithmetic is identical, and asserting the stronger thing would have pinned a property the model does not have.record_winner/record_drawYou wrote that extending them is probably wrong, and that #38's pre-registration would cover them without touching their signatures. Both still hold — they are
puband positional, so adding parameters is breaking.But the premise underneath has weakened. This issue says a competitor whose first appearance arrives through either one is "permanently stuck on the history defaults". That stopped being true at
8c087adin 0.4.0, which made competitor configuration apply whenever it is supplied, including on a key the history already knows, with a forward refresh from the earliest affected slice. Measured on exactly the route this issue names:So it is repairable, and the gap is ergonomic rather than a trap. That removes the urgency argument for touching those two signatures. The remaining case for pre-registration is #38's own — declaring configuration before any event exists — which is a better-shaped feature than overloading the convenience path.
Closing this as the
EventBuilderhalf, which is what your own reasoning scoped it to.