Club Rating Unification — 2026-09-24
Status: Active Last reviewed: 2026-09-24
Owner decision (2026-09-24, on the CLUB-ELO-POOL question in open-issues-plan-2026-09-24.md §5): retire the per-club rating pool. A player has one rating; a club's ranking is that rating filtered to the club's members.
Why
public.club_elo_ratingsapplied the SAME global match delta to a club total starting at 1000, so clubmates never started equal in any meaningful sense, and a player carried two numbers that disagreed.- It had no history table of its own (the "7-day trend" already fell back to the global
elo_history). get_session_rating_impactranked a player's GLOBAL rating against other members' CLUB ratings — two scales in one comparison.- CLUB-ELO-LABEL existed only to keep the two numbers apart.
- Chess.com, Lichess, UTR, ITF WTN and Matchi all keep one rating and show club ranks as a filter of it.
Contract (every lane builds to this)
Server (migration 00670):
| Piece | Change |
|---|---|
private.club_player_match_counts (user_id, club_id, discipline, matches_played) | NEW, server-only (private schema, no data-API exposure): the one per-club signal still needed — private.get_home_club's calibration (rating-system-v2 §11). Backfilled from club_elo_ratings.elo_matches_played. |
private.match_rating_upsert_club_elo | REPLACED by private.match_rating_bump_club_match_count(...): increments the count only. Its caller (private.match_rating_write_player_result) is re-pointed (reproduce the LIVE body with \sf). |
private.apply_match_confidence_bonus | Drops the club-pool half; the global bonus is unchanged. |
private.recompute_all_ratings, dev_reconcile_seeded_world | Rebuild club match counts instead of club ratings. |
private.get_home_club | Reads the counts table. |
public.get_club_analytics | avgElo = average global rating (player_ratings, doubles) over active members with matches_played > 0. |
public.get_session_rating_impact | Club rank before/after = the participant's rating_before/rating_after ranked among the OTHER active members' current global ratings (same scale — the mixing bug is gone). |
public.get_club_rating_ranks(p_club_id uuid, p_discipline text DEFAULT 'doubles') | NEW, SECURITY INVOKER (RLS on club_members and player_ratings already allows a member to read both): RETURNS TABLE(user_id uuid, rating int, matches_played int, rank int) for the club's ACTIVE members; rank is rank() OVER (ORDER BY rating DESC) among members with matches_played > 0, NULL for unrated members. |
public.club_elo_ratings | DROPPED after the backfill (the DB is not live; the QA bed loses nothing it needs). |
Client:
- The entity
ClubEloRating, its mapper, port, adapter anduseClubEloLeaderboardare replaced byClubMemberRating { userId, rating, matchesPlayed, rank | null }throughClubRatingRepository.findByClub(clubId, discipline)→get_club_rating_ranks, and auseClubRatingRanks(clubId)hook. club-groups.rules.ts: the rank-partition input is the member's GLOBAL rating (rating,matchesPlayed) — eligibility and ordering rules unchanged.- Every surface that said 클럽 ELO says ELO;
ratingScopeLabeland itsclubscope are retired (one rating, one word). Surfaces: club ranking tab + head row, records leaderboard club tab + podium, regroup preview, club groups section, the session rating-impact card, club member stats. - Seeds and the activity simulator stop writing club ratings (the global rating pipeline is the source); truncation lists name the new private table.
Lanes
- M — migration 00670 + pgTAP + types (server contract above).
- K — client domain/port/adapter/hooks/rules + test-kit (contract above; types regenerated after M).
- U — surfaces and copy (after K's hook exists).
- S — seeds + simulate-activity (after M's schema).
- Docs — canon (clubs.md CLUB-ELO-LABEL → one rating), rating-system-v2 §11 (home club reads match counts), blueprints, decision log.
Verification
pgTAP for each redefined function (club rank on one scale; home club from counts; analytics average of global ratings; the new RPC's rank/NULL rules and RLS as a member vs. a non-member); the full suite; yarn check; device check of the club ranking tab, records club tab and regroup preview.
Status (2026-09-24)
Built and reviewed:
- M (00670 + pgTAP): 21 assertions; the RPC's order is pinned; types regenerated.
- K (data path): the entity, port, adapter, mapper, hook and query keys; the dead
clubEloEnabledentitlement retired. - U (surfaces): ranks from the server, unrated members out of rankings, every caption ELO.
- S (seeds): no club-rating writes; the local seed ranks 24 members of a showcase club (901–1158) from real matches.
Two adversarial reviewers checked every replaced function against its previous body (no drift) and found one P1: ranks can tie now, and three lists keyed rows by rank. All three key by the person, and the test kit's list mocks refuse duplicate keys. Found alongside: the post counters bug (00671, see the decision log).