From 726896a2baaddf6449f8c3863ed09e14aa8ceaa8 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Fri, 24 Apr 2026 12:16:25 +0200 Subject: [PATCH] feat(api): add Observer trait and NullObserver default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Observer replaces verbose: bool with structured progress callbacks: on_iteration_end, on_batch_processed, on_converged — all no-op default impls so users override only what they need. NullObserver is a ZST default. Send + Sync bounds deferred to T3 (Rayon support). Fully additive — wired into History::converge in Task 12. Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md. --- src/lib.rs | 2 ++ src/observer.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/observer.rs diff --git a/src/lib.rs b/src/lib.rs index c007764..d4efa2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ pub mod gaussian; mod history; mod key_table; mod matrix; +mod observer; mod outcome; mod rating; pub(crate) mod schedule; @@ -33,6 +34,7 @@ pub use gaussian::Gaussian; pub use history::History; pub use key_table::KeyTable; use matrix::Matrix; +pub use observer::{NullObserver, Observer}; pub use outcome::Outcome; pub use rating::Rating; pub use schedule::ScheduleReport; diff --git a/src/observer.rs b/src/observer.rs new file mode 100644 index 0000000..223948b --- /dev/null +++ b/src/observer.rs @@ -0,0 +1,48 @@ +//! Observer trait for progress reporting during convergence. +//! +//! Replaces the old `verbose: bool` + `println!` path. Callers wire in any +//! observer that implements the trait; default methods are no-ops so users +//! override only what they need. + +use crate::time::Time; + +/// Receives progress callbacks during `History::converge`. +/// +/// All methods have default no-op implementations; implement only what's +/// interesting. Send/Sync is NOT required in T2 (added in T3 along with +/// Rayon support). +pub trait Observer { + /// Called after each convergence iteration across the whole history. + fn on_iteration_end(&self, _iter: usize, _max_step: (f64, f64)) {} + + /// Called after each time slice is processed within an iteration. + fn on_batch_processed(&self, _time: &T, _slice_idx: usize, _n_events: usize) {} + + /// Called once when convergence completes (or max iters is reached). + fn on_converged(&self, _iters: usize, _final_step: (f64, f64), _converged: bool) {} +} + +/// ZST no-op observer; the default when none is configured. +#[derive(Copy, Clone, Debug, Default)] +pub struct NullObserver; + +impl Observer for NullObserver {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn null_observer_compiles_for_i64() { + let o = NullObserver; + >::on_iteration_end(&o, 1, (0.0, 0.0)); + >::on_converged(&o, 5, (1e-6, 1e-6), true); + } + + #[test] + fn null_observer_compiles_for_untimed() { + use crate::Untimed; + let o = NullObserver; + >::on_iteration_end(&o, 1, (0.0, 0.0)); + } +}