Files
trueskill-tt/CLAUDE.md
T
logaritmiskandClaude Opus 5 9e8515b7cd docs: refresh README and CLAUDE.md; add ingest benchmark
The CLAUDE.md architecture section still described the pre-redesign engine:
its data flow named `Batch`, `Agent`, `Player` and `message.rs`, none of
which have existed since T2, and the public API it listed did not match
`lib.rs`. It is the first thing a fresh session reads, so it was actively
misleading. Rewritten against the current module layout, with the invariants
that are easy to violate — ties needing a positive `p_draw`, NaN never being
convergence, log-space evidence, color contiguity, `forbid(unsafe_code)`,
and ingestion-order equivalence — written down.

The README Todo list had five entries that were already done, including
"Time needs to be an enum": `Time` has been a trait since T2, and the
`batch::compute_elapsed()` it pointed at no longer exists. The genuinely
open item — cross-checking `quality()` against sublee/trueskill — stays.

`benches/ingest.rs` measures one-event-per-call against a single batched
call. The rest of the suite only measured batched construction, which is why
the quadratic fixed earlier on this branch went unnoticed for so long.

`TimeSlice::log_evidence` also hashes its target set once instead of
scanning the slice per player per event, so `log_evidence_for` with many
keys is no longer quadratic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnsaJg74eNSva3PJjK2eej
2026-08-04 22:05:49 +02:00

5.1 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

just test                            # Full suite across every feature combination CI checks
just check                           # Fast inner loop: cargo test --features approx
just lint                            # clippy, warnings denied
just fmt                             # ALWAYS nightly — rustfmt.toml uses nightly-only options
just determinism                     # Bit-identical posteriors at RAYON_NUM_THREADS 1/2/4/8
just ci                              # Everything CI runs
cargo test --lib <test_name>         # A single test by name
cargo bench                          # Criterion benchmarks

Run tests in release too. debug_assert! is compiled out there, and that is where several defects have hidden — a debug-only run is not evidence. just test includes a release job.

Feature flags

  • approxapprox::AbsDiffEq etc. for Gaussian. Most numerical goldens need it.
  • rayon — opt-in parallel within-slice sweep and per-slice query passes.

Architecture

A Rust port of TrueSkillThroughTime.py: Bayesian skill rating that infers skill at every point in time, propagating evidence both forward and backward across a history.

Data flow

History  →  TimeSlice[]  →  Event[]  →  Team[] → Item[]
                                  ↓
                     Game (factor graph) → Schedule → BuiltinFactor[]
  • History (history.rs) — top level. Interns keys, groups events into TimeSlices by time, runs the forward/backward sweep in converge(), and answers learning_curves(), current_skill(), log_evidence(), predict_quality(), predict_outcome(). Built via HistoryBuilder.
  • TimeSlice (time_slice.rs) — all events at one time. Owns a SkillStore and a ScratchArena; iteration() sweeps its events, using ColorGroups to partition independent ones.
  • Event (time_slice.rs) — one match. compute() runs inference reading skills immutably; apply() folds the result back. The split is what lets a color group run in parallel with no unsafe.
  • Game (game.rs) — a single match's factor graph. run_chain builds the diff chain between rank-adjacent teams and drives it to convergence.
  • Gaussian (gaussian.rs) — natural parameters (pi = 1/sigma², tau = mu/sigma²). Mul/Div are the EP product/cavity: pure adds and subtracts. Variance-space ops (Add, Sub, exclude, forget) go through from_mv/variance() and take no square root.
  • factor/TeamSumFactor, RankDiffFactor, TruncFactor (ranked), MarginFactor (scored), over a flat VarStore. BuiltinFactor dispatches by enum rather than dyn.
  • Schedule (schedule.rs) — drives factor propagation. EpsilonOrMax is the only implementation.
  • Competitor (competitor.rs) — per-history temporal state (message, last_time). Rating (rating.rs) — static config (prior, beta, drift).
  • storage/SkillStore (per slice) and CompetitorStore (per history), both dense Vecs indexed by Index.
  • KeyTable (key_table.rs) — user key ↔ Index, both directions O(1).
  • Drift (drift.rs) / Time (time.rs) — traits. Time is a trait (i64, Untimed), not an enum.
  • lib.rs — public exports, global defaults (MU, SIGMA, BETA, GAMMA, P_DRAW, EPSILON, ITERATIONS), and the standalone quality(), cdf(), erfc().

Invariants worth knowing

  • A tie needs p_draw > 0. With p_draw == 0.0 the truncation margin is zero and the two-sided tie update evaluates 0/0. Ingestion rejects such events with InferenceError::TieWithoutDrawProbability. This includes Outcome::winner(w, n) for n >= 3, which ties every loser.
  • NaN is never convergence. Comparisons against NaN are all false, so tuple_gt reads NaN as "below epsilon". Use step_converged / step_is_finite, never !tuple_gt(..) alone.
  • Evidence accumulates in log space. A linear product over a long diff chain underflows to zero, and ln(0) is -inf.
  • Colors are contiguous. recompute_color_groups reorders events so each color occupies one range; ColorGroups::groups_are_contiguous asserts it.
  • The crate is #![forbid(unsafe_code)]. Keep it that way.
  • Ingestion order must not change the answer. Events added one at a time must converge to the same fixed point as the same events batched — see tests/ingestion_equivalence.rs.

Testing notes

  • Numerical goldens are cross-validated against the Python/Julia reference. Some are convergence residuals, not exact values; treat a small movement as suspicious but check whether the new value is closer to the analytic truth (symmetric fixtures converge to their prior mean exactly) before assuming a regression.
  • tests/degenerate_inputs.rs covers empty/boundary/error paths, tests/ingestion_equivalence.rs covers batching order, tests/quality.rs covers N-group quality, tests/determinism.rs covers thread counts.