Per-tournament scoring config: rank_by (peak_rating | current_rating) - #304
Merged
Conversation
amrtgaber
force-pushed
the
rank-by-config
branch
from
July 25, 2026 21:20
d8a7d7d to
8fcaa02
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #290: the ranking metric becomes per-tournament configuration instead of platform behavior.
rank_by = peak_rating(server_default — every existing row and the launch event's behavior, unchanged) orcurrent_rating.Model + contract (all additive; consumers regenerate orval clients):
Tournament.rank_by— non-native Enum stored as the StrEnum value, mirroringMatchState. Expand-only migration7f07220ac5cb(NOT NULL + server_default → safe under revision rollover; validated on a throwaway Postgres 16 together with its parent).rank_byonTournamentRead/Create/Update(explicit null → 422, likeleaderboard_id).TournamentSummary.highest_current_rating— the rating card for current-mode tournaments; exactly one of the two rating cards is ever populated,highest_peak_ratingstays the peak-mode card, pre-existing consumers seenullin the new field.Read paths threaded (
app/routers/tournaments.py):/standings: SQL ORDER BY ranks on the configured metric (other rating + name as tie-breaks); post-window re-rank and the surfaced value of the ranked metric freeze, the other rating stays live./teams/standings: member order,combined_rating_sum/combined_rating_average, and team order all follow the metric; field names deliberately unchanged (same documented-semantics approach as Team standings: rank teams by combined PEAK rating, not current; add per-member max_rating #158)./summary: the rating card ranks by the configured metric via the generalized_rank_metric_by_profile(keeps the Mirror FE stat cards on /summary; resolve displayName into name #244 correction in peak mode)./standings/history: peak mode only — 501 for current-mode tournaments. The sweep's carried-in baselines, ratchets, and live clamp are peak-specific machinery; an honest refusal beats a chart whose order contradicts the live table. Tracked as a follow-up issue;/progressioncovers rating-over-time meanwhile.Freeze semantics for current mode (the issue's open design point, resolved): new
_frozen_current_by_profilebehind a_frozen_metric_by_profiledispatcher. Current is a last-value metric, not a ratchet, so the fold differs from peak: (1) thenew_ratingof the last game started by the bound — authoritative, includes the buzzer-beater that settles after the bound, durable in ourmatchestable for any in-window game; (2) fallback, the last recorded snapshot at-or-before the bound; (3) live current. No clamp — clamping exists for rebased-away peak noise only.Recorder widened (
insert_rating_snapshots): diffs the(max_rating, current_rating)pair instead of max alone, so current-mode freezes have observations to fall back on. Volume stays bounded by play (≤ one row per rated game per tracked pair); self-seeding and rebase-down behavior preserved.CLAUDE.md's three convention bullets (sort order, team aggregates, freeze) updated to the config-aware wording.
Non-goals (per #290)
No points/W-L/bracket modes — this is the config seam plus the two modes with known semantics; the platform stays a ladder-standings API.
Tests
581 passed. New coverage: rank_by round-trip/default/invalid on create + PATCH (incl. explicit-null 422); current-mode standings order with peak surfaced untouched; frozen-current via match log (beats a stale snapshot, post-window play ignored), snapshot fallback, live fallback; team sums/order on current; summary card routing both modes; history 501; recorder current-only-change append.
Merge order: after #303 (stacked — shares the migration lineage and tournament-write test context; #299's one-line CLAUDE.md fix also lands first and is a trivial rebase here).
Closes #290