refactor!: retire Index, intern and lookup

`Index` was public, `History::intern` and `History::lookup` returned
one, and no public method anywhere accepted one. It was a handle with
nowhere to go — and `key_table.rs` advertised the hot-path story it was
meant to enable ("power users can promote `&K` to `Index` and skip the
lookup"), which was never reachable through the public API.

It also shadowed `std::ops::Index`, which `CompetitorStore` implements,
so `use trueskill_tt::*` alongside `use std::ops::*` collided.

All three are `pub(crate)` now. `intern` stays internal because
ingestion needs it; `lookup` is gone entirely, since `current_skill`,
`rating` and `learning_curve` already answer "does this history know
this key" and all three take a borrowed key.

The three tests that used them asserted things a caller cannot observe.
They now assert what the interning bought:

- `record_winner_creates_two_competitors` compares posteriors instead of
  comparing two opaque indices for inequality.
- `intern_is_idempotent` becomes `a_repeated_key_is_one_competitor` — a
  key appearing in two events gives one competitor with a two-point
  learning curve, which is the observable form of the same claim.
- `lookup_returns_none_for_missing` becomes `an_unknown_key_is_unknown`.

Closes #73.

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 23:18:54 +02:00
co-authored by Claude Opus 5
parent ddbac87744
commit faa25fb3b1
7 changed files with 57 additions and 62 deletions
+6 -6
View File
@@ -41,9 +41,9 @@ fn add_events_bulk_via_iter() {
h.add_events(events).unwrap();
let report = h.converge().unwrap();
assert!(report.converged);
assert!(h.lookup(&"a").is_some());
assert!(h.lookup(&"b").is_some());
assert!(h.lookup(&"c").is_some());
assert!(h.current_skill("a").is_some());
assert!(h.current_skill("b").is_some());
assert!(h.current_skill("c").is_some());
}
#[test]
@@ -103,9 +103,9 @@ fn fluent_event_builder_basic() {
let report = h.converge().unwrap();
assert!(report.converged);
assert!(h.lookup(&"alice").is_some());
assert!(h.lookup(&"bob").is_some());
assert!(h.lookup(&"carol").is_some());
assert!(h.current_skill("alice").is_some());
assert!(h.current_skill("bob").is_some());
assert!(h.current_skill("carol").is_some());
}
#[test]
+8 -7
View File
@@ -77,17 +77,18 @@ fn linear_combinations_take_bare_keys() {
);
}
/// `lookup` is gone with `Index` (#73); the accessors that answer the same
/// question all take a borrowed key.
#[test]
fn lookup_accepts_a_borrowed_key_like_its_neighbours() {
// `lookup` carried `ToOwned<Owned = K>`, copy-pasted from `intern`, which
// genuinely needs it to create the entry. `lookup` never creates.
fn membership_queries_accept_a_borrowed_key() {
let h = owned();
assert!(h.lookup("alice").is_some());
assert!(h.lookup("nobody").is_none());
// Control: its neighbours already accepted this and must still.
assert!(h.current_skill("alice").is_some());
assert!(h.rating("alice").is_some());
assert!(h.learning_curve("alice").is_some());
assert!(h.current_skill("nobody").is_none());
assert!(h.rating("nobody").is_none());
assert!(h.learning_curve("nobody").is_none());
}
#[test]
+20 -12
View File
@@ -17,24 +17,32 @@ fn record_winner_builds_history() {
h.record_winner(&"alice", &"bob", 1).unwrap();
let _ = h.converge().unwrap();
let a_idx = h.lookup(&"alice").unwrap();
let b_idx = h.lookup(&"bob").unwrap();
assert_ne!(a_idx, b_idx);
// `lookup` returned an `Index` that nothing public accepted, so the
// observable claim is the one worth making: two distinct competitors, each
// with their own posterior, and the winner ahead.
assert_eq!(h.competitor_count(), 2);
let alice = h.current_skill("alice").expect("alice played");
let bob = h.current_skill("bob").expect("bob played");
assert!(alice.mu() > bob.mu());
}
/// The same key names the same competitor across events, which is what
/// interning bought and the only part of it a caller can observe.
#[test]
fn intern_is_idempotent() {
fn a_repeated_key_is_one_competitor() {
let mut h: History = History::builder().build();
let a1 = h.intern(&"alice");
let a2 = h.intern(&"alice");
assert_eq!(a1, a2);
h.record_winner(&"alice", &"bob", 1).unwrap();
h.record_winner(&"alice", &"carol", 2).unwrap();
assert_eq!(h.competitor_count(), 3);
assert_eq!(h.learning_curve("alice").expect("known").len(), 2);
}
#[test]
fn lookup_returns_none_for_missing() {
fn an_unknown_key_is_unknown() {
let h: History = History::builder().build();
assert!(h.lookup(&"nobody").is_none());
assert!(h.current_skill("nobody").is_none());
assert!(h.learning_curve("nobody").is_none());
}
#[test]
@@ -50,6 +58,6 @@ fn record_draw_with_p_draw_set() {
h.record_draw(&"alice", &"bob", 1).unwrap();
let _ = h.converge().unwrap();
assert!(h.lookup(&"alice").is_some());
assert!(h.lookup(&"bob").is_some());
assert!(h.current_skill("alice").is_some());
assert!(h.current_skill("bob").is_some());
}