diff --git a/README.md b/README.md index 0c4bce5..798f859 100644 --- a/README.md +++ b/README.md @@ -330,7 +330,7 @@ stay available at any size: Unknown keys are an error by default, not a silent omission: a team the history has never seen cannot produce a confident-looking probability. The error names -the key, and every key must already be known — pre-filter with `lookup` or +the key, and every key must already be known — pre-filter with `current_skill` if your caller cannot guarantee that. If predicting for competitors you have never seen is the point rather than a diff --git a/src/error.rs b/src/error.rs index 5ee07e7..2273032 100644 --- a/src/error.rs +++ b/src/error.rs @@ -155,7 +155,7 @@ pub enum InferenceError { /// when a competitor's configuration is a property of the domain. #[non_exhaustive] ConflictingCompetitorConfig { - /// The competitor's interned [`Index`](crate::Index) as a raw `usize`, + /// The competitor's interned slot as a raw `usize`, /// not the user key — the batch is already flattened to indices by the /// time the conflict is detectable. competitor: usize, @@ -323,7 +323,7 @@ impl fmt::Display for InferenceError { f, "team {team}, member {member}: no skill recorded for key {key} \ (every key must already be known to the history; pre-filter \ - with `lookup` or `current_skill` if that is not guaranteed)" + with `current_skill` if that is not guaranteed)" ) } Self::AlreadyRegistered { key } => { diff --git a/src/history.rs b/src/history.rs index e313653..3b88cf3 100644 --- a/src/history.rs +++ b/src/history.rs @@ -538,29 +538,16 @@ impl HistoryBuilder, O: Observer, K: Eq + Hash + Clone> History { /// Promote a key to its [`Index`], creating the entry if it is new. /// - /// Interning a key does not register a competitor or give them a rating — - /// it only reserves the slot. Use [`History::register`] to declare a - /// competitor's configuration up front. - pub fn intern(&mut self, key: &Q) -> Index + /// Crate-internal since #73: interning reserves a storage slot and nothing + /// more, and no public method ever accepted the handle it returned. + /// [`History::register`] is the public way to declare a competitor. + pub(crate) fn intern(&mut self, key: &Q) -> Index where K: Borrow, Q: Hash + Eq + ToOwned + ?Sized, { self.keys.get_or_create(key) } - - /// Resolve an existing key to its [`Index`], or `None` if the history has - /// never seen it. - /// - /// The read-only counterpart of [`History::intern`]: it never creates. - #[must_use] - pub fn lookup(&self, key: &Q) -> Option - where - K: Borrow, - Q: Hash + Eq + ?Sized, - { - self.keys.get(key) - } } impl, O: Observer, K: Eq + Hash + Clone> History { @@ -1328,7 +1315,7 @@ impl, O: Observer, K: Eq + Hash + Clone> History, O: Observer, K: Eq + Hash + Clone> History, O: Observer, K: Eq + Hash + Clone> History, O: Observer, K: Eq + Hash + Clone> History, O: Observer, K: Eq + Hash + Clone> History usize { + pub(crate) fn get(self) -> usize { self.0 } } diff --git a/tests/api_shape.rs b/tests/api_shape.rs index 6466b2f..5e9fb01 100644 --- a/tests/api_shape.rs +++ b/tests/api_shape.rs @@ -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] diff --git a/tests/key_ergonomics.rs b/tests/key_ergonomics.rs index 1078bd6..2baea2f 100644 --- a/tests/key_ergonomics.rs +++ b/tests/key_ergonomics.rs @@ -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`, 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] diff --git a/tests/record_winner.rs b/tests/record_winner.rs index 2e2b84b..0762c28 100644 --- a/tests/record_winner.rs +++ b/tests/record_winner.rs @@ -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()); }