Skip to content

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_ratings applied 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_impact ranked 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):

PieceChange
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_eloREPLACED 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_bonusDrops the club-pool half; the global bonus is unchanged.
private.recompute_all_ratings, dev_reconcile_seeded_worldRebuild club match counts instead of club ratings.
private.get_home_clubReads the counts table.
public.get_club_analyticsavgElo = average global rating (player_ratings, doubles) over active members with matches_played > 0.
public.get_session_rating_impactClub 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_ratingsDROPPED after the backfill (the DB is not live; the QA bed loses nothing it needs).

Client:

  • The entity ClubEloRating, its mapper, port, adapter and useClubEloLeaderboard are replaced by ClubMemberRating { userId, rating, matchesPlayed, rank | null } through ClubRatingRepository.findByClub(clubId, discipline) → get_club_rating_ranks, and a useClubRatingRanks(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; ratingScopeLabel and its club scope 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 ​

  1. M — migration 00670 + pgTAP + types (server contract above).
  2. K — client domain/port/adapter/hooks/rules + test-kit (contract above; types regenerated after M).
  3. U — surfaces and copy (after K's hook exists).
  4. S — seeds + simulate-activity (after M's schema).
  5. 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 clubEloEnabled entitlement 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).

Markdown remains the source of truth. Run yarn docs:check before handoff.