refactor!: un-export six types that no caller could reach

`TimeSlice`, `EventKind`, `KeyTable`, `CompetitorStore`, `Competitor` and
the `storage` module were all public and none was obtainable from a
`History` — `time_slices`, `agents` and `keys` are all private or
`pub(crate)`. `TimeSlice` was the worst: `new`, `add_events`, `iteration`,
`get_composition` and `get_results` were `pub` on a type you could only
build standalone and never feed back into anything.

Their sole consumer outside `src/` was `benches/batch.rs`, so a benchmark
was dictating six public types. It is rewritten against the public API: a
single-slice history's `converge` calls exactly the same per-slice sweep,
so capping at one iteration measures the same code path.

`N01` had zero references in the entire repository, including inside the
crate; removed. `N00` and `N_INF` are EP identities (`Add` and `Mul`) and
are now `pub(crate)` — a user reaching for `N_INF` as "an unknown
competitor's prior" would get an improper distribution whose `mu()`
silently reports 0.0.

Adds the accessors their absence forced people around, from #70:
`competitors()`, `competitor_count()` and `event_count()` (`size` had no
accessor at all). Answering "who is best" previously meant materialising
every competitor's full smoothed curve to read the last point of each.

`KeyTable::keys` now iterates the dense reverse table rather than the
forward `HashMap`, so `competitors()` yields insertion order rather than
per-process hash order — the same hazard as #62, caught before it could
reach a caller building a standings table.

Two `CompetitorStore` methods (`is_empty`, `iter_mut`) had no callers
anywhere and are gone; four more are now `#[cfg(test)]`, which is what
they always were in practice.

Worth recording a mistake: I first deleted `get_composition`/`get_results`
on the strength of a "never used" warning, and the build broke — the
warning came from the plain-lib target, where `#[cfg(test)]` callers in
history.rs are not compiled. A dead-code warning from one target is not
evidence about the others.

BREAKING CHANGE: `TimeSlice`, `EventKind`, `KeyTable`, `CompetitorStore`,
`Competitor`, the `storage` module, `N01`, `N00` and `N_INF` are no longer
public.

Refs #73, #70

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
This commit is contained in:
2026-09-09 20:34:18 +02:00
co-authored by Claude Opus 5
parent 5f5a37090a
commit 4472d98b56
6 changed files with 99 additions and 64 deletions
+45 -37
View File
@@ -1,49 +1,57 @@
//! One slice's event sweep.
//!
//! Written against the public API rather than against `TimeSlice` directly.
//! It used to reach for `TimeSlice`, `KeyTable`, `CompetitorStore`,
//! `Competitor` and `EventKind`, and was the *only* thing outside `src/`
//! that did — so a benchmark was dictating five public types that no test,
//! example or consumer could otherwise obtain.
//!
//! A single-slice history's `converge` calls exactly the same per-slice sweep,
//! so capping at one iteration measures the same code path.
use criterion::{Criterion, criterion_group, criterion_main};
use smallvec::smallvec;
use trueskill_tt::{
BETA, Competitor, ConvergenceOptions, EventKind, GAMMA, KeyTable, MU, P_DRAW, Rating, SIGMA,
TimeSlice, drift::ConstantDrift, gaussian::Gaussian, storage::CompetitorStore,
ConstantDrift, ConvergenceOptions, Event, History, Member, Outcome, Team,
};
fn criterion_benchmark(criterion: &mut Criterion) {
let mut index_map = KeyTable::new();
let build = || {
let mut h = History::builder()
.convergence(ConvergenceOptions {
max_iter: 1,
epsilon: 0.0,
alpha: 1.0,
})
.drift(ConstantDrift::new(0.0))
.build();
let a = index_map.get_or_create("a");
let b = index_map.get_or_create("b");
let c = index_map.get_or_create("c");
// 100 events, all at one time, so the history has a single slice.
let events: Vec<Event<i64, &'static str>> = (0..100)
.map(|_| Event {
time: 1,
teams: smallvec![
Team::with_members([Member::new("a")]),
Team::with_members([Member::new("b")]),
],
outcome: Outcome::winner(0, 2),
})
.collect();
h.add_events(events).expect("fixture ingests");
h
};
let mut agents: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
for agent in [a, b, c] {
agents.insert(
agent,
Competitor {
rating: Rating::new(
Gaussian::from_ms(MU, SIGMA),
BETA,
ConstantDrift::new(GAMMA),
),
..Default::default()
criterion.bench_function("slice_sweep_100_events", |b| {
b.iter_batched(
build,
|mut h| {
// `converge_partial`, not `converge`: one iteration is
// deliberately short of convergence and `converge` reports that
// as an error.
let _ = h.converge_partial();
},
criterion::BatchSize::SmallInput,
);
}
let mut composition = Vec::new();
let mut results = Vec::new();
let mut weights = Vec::new();
for _ in 0..100 {
composition.push(vec![vec![a], vec![b]]);
results.push(vec![1.0, 0.0]);
weights.push(vec![vec![1.0], vec![1.0]]);
}
let kinds = vec![EventKind::Ranked; composition.len()];
let mut time_slice = TimeSlice::new(1, P_DRAW, ConvergenceOptions::default());
time_slice.add_events(composition, Some(results), Some(weights), kinds, &agents);
criterion.bench_function("Batch::iteration", |b| {
b.iter(|| time_slice.iteration(0, &agents))
});
}