Skip to content

feat(#1996): add time management rating for game uploads#2007

Open
Dblike wants to merge 8 commits intojackstenglein:devfrom
Dblike:feat/1996-time-management-rating
Open

feat(#1996): add time management rating for game uploads#2007
Dblike wants to merge 8 commits intojackstenglein:devfrom
Dblike:feat/1996-time-management-rating

Conversation

@Dblike
Copy link
Contributor

@Dblike Dblike commented Feb 23, 2026

ticket: #1996

summary

overview

Adds a per-game "time management rating" (0–3000) computed from clock annotations in the PGN, persisted on game records, and aggregated into a user-level rating on the profile. Only classical games (30+ min) with real {[%clk ...]} data and 5+ moves are scored. Includes a backfill script for existing games.

before + after

before after
Profile shows no time management rating "Time Management" row in Dojo Score Card with rating + provisional indicator
Game records have no TM fields Games store timeManagementRatingWhite / Black
No user-level TM aggregate Dedicated User.timeManagementRating field (not in RatingSystem enum)

key changes

  • Moved clock rating calculation from frontend to common/src/ratings/clockRating.ts (single source of truth)
  • Per-game TM ratings calculated and persisted on game create/update
  • Incremental user aggregation: running average (provisional, < 10 games) → USCF Elo draw adjustment (K=32)
  • TM rating stored as a dedicated top-level User.timeManagementRating field — not in the RatingSystem enum, since it's a computed internal metric, not a user-entered external rating
  • Profile display with provisional indicator in DojoScoreCard
  • Backfill script (backfillTimeManagement.ts) — idempotent, single-pass scan
Design decisions

Dedicated field, not a RatingSystem memberRatingSystem represents external, user-entered rating systems (Chess.com, Lichess, FIDE, etc.). TimeManagement is a computed aggregate with no username, no graduation boundary, and shouldn't appear in rating selectors or search fields. A top-level User.timeManagementRating field avoids needing filters everywhere the codebase iterates Object.values(RatingSystem).

Store both white and black per-game ratings — cheap to compute in one pass, avoids ambiguity about which side the owner played, enables future comparison features.

Incremental aggregation (no history array) — DynamoDB User records are under size pressure (~400KB limit). A gameRatings[] array has unbounded growth and race conditions under concurrent Lambdas. Instead we store currentRating + numGames and update incrementally. Per-game ratings on Game records are the durable source of truth — the backfill script doubles as repair.

Skip games without clock annotations — without real clk data, the fallback produces a flat line and a meaningless 0 rating. The hasClockData guard prevents pollution.

No aggregate update on game edit — PGN edits are rare, correction is complex, drift is at most one game. Backfill script corrects accumulated drift.

Open questions

Q1: How should time management be displayed on the profile?

Currently rendered as a simple icon + text + provisional chip in DojoScoreCard. Every other row in the card uses ScoreboardProgress with a progress bar. Options: keep as-is, add progress bar (e.g. 3/10 toward established), use rating boundaries like the main rating system, or extract to its own component. Happy to implement whichever fits.

Time Management Rating

Q2: Is incremental aggregation acceptable, or should we store a full history array?

Currently we store only currentRating + numGames on the User record and update incrementally. This avoids unbounded array growth and race conditions under concurrent Lambdas, but means the aggregate can't be recalculated from the User record alone — per-game ratings on Game records are the source of truth, and the backfill script serves as the repair path. If a full history array is preferred (e.g. for charting rating over time), we'd need a cap or pagination strategy to stay within DynamoDB's 400KB item limit.

@Dblike Dblike changed the base branch from main to dev February 23, 2026 22:48
- Add TIME_MANAGEMENT variant to TS and Go RatingSystem enums
- Add -1 rating boundary entries for all cohorts
- Add clock icon to RatingSystemIcons
- Add formatRatingSystem display name
- Add switch cases in RatingCard, PreferredRatingSystemForm
- Include lock file updates from dependency resolution
- Create common/src/ratings/clockRating.ts with calculateTimeRating(),
  getPerfectLineSeconds(), ClockDatum interface, and constants
- Replace frontend clockRating.ts with re-exports from common
- Remove unused _side parameter from calculateTimeRating calls
- Add timeManagementRatingWhite/Black fields to Game schema (TS + Go)
- Add numGames field to Rating interface for incremental aggregation
- Create timeManagement.ts to extract clock data and calculate ratings
- Skip games without real clock annotations (hasClockData guard)
- Integrate into getGame() for creation and getGameUpdate() for updates
- Add updateUserTimeManagementRating() with incremental aggregation
- Guard against missing ratings map on user record
- Create common/src/ratings/timeManagement.ts with incremental model:
  updateTimeManagementAggregate() stores only currentRating + numGames
- Provisional (< MIN_GAMES_FOR_ELO): running average
- Established (>= MIN_GAMES_FOR_ELO): USCF Elo draw adjustment (K=32)
- Wire DojoScoreCard to read real user.ratings[TimeManagement]
- Derive provisional chip from numGames using named constant
- timeManagement.test.ts: 7 tests for PGN clock extraction
  (classical with clocks, blitz, short game, no clocks, no TimeControl,
  getGame integration for both positive and negative cases)
- timeManagementAggregation.test.ts: 11 tests for pure calculation
  (clockRating edge cases, incremental aggregation provisional/Elo
  transition, draw adjustment direction and equality)
Single-pass script that scans all games, calculates per-game TM ratings
from PGN clock annotations, writes them to game records, and rebuilds
user-level aggregates.

- Idempotent: skips games that already have TM ratings
- User aggregates rebuilt from scratch (not incremental)
- Run via: stage=dev npx tsx pgnService/game/backfillTimeManagement.ts
@Dblike Dblike force-pushed the feat/1996-time-management-rating branch 2 times, most recently from 8d7eb9a to bba2703 Compare February 23, 2026 22:53
Summary of changes:
- Extract 0.5 magic number as DRAW_SCORE constant in timeManagement.ts
- Replace directional Elo tests with hand-computed exact-value assertions
- Add 500-point gap test cases for larger rating differences
@Dblike Dblike force-pushed the feat/1996-time-management-rating branch from bba2703 to f39c14e Compare February 23, 2026 23:03
TimeManagement is a computed internal aggregate, not a user-entered
external rating system. Storing it in the RatingSystem enum caused it
to leak into search fields, preferred rating dropdowns, and rating
boundary tables.

Summary of changes:
- Remove TimeManagement from RatingSystem enum (TS + Go)
- Add dedicated User.timeManagementRating field (TimeManagementAggregate)
- Remove numGames from Rating interface (was TM-only)
- Remove 22 boundary table entries (all were -1 placeholders)
- Simplify DynamoDB writes to a single top-level field SET
- Clean up 7 frontend files that had TM switch cases or filters
@Dblike Dblike marked this pull request as ready for review February 24, 2026 00:41
@Dblike Dblike force-pushed the feat/1996-time-management-rating branch from a0a8325 to 1d4d0d3 Compare February 24, 2026 08:47
@jackstenglein
Copy link
Owner

@Dblike Thanks for the PR! I haven't reviewed the code yet (planning to get to it tomorrow) but this line stood out to me in the description:

No aggregate update on game edit — PGN edits are rare, correction is complex, drift is at most one game. Backfill script corrects accumulated drift.

Does this mean if I import my game, then edit it later, my overall time management rating isn't updated? I think that presents a big problem if so. Imagine I start entering my OTB game, and only enter clock data for half the game. Then a day later, I come back and enter the rest of the clock data. My time management rating for that game will be drastically different, so my overall time management rating should also change.

Your open questions:

Q1: How should time management be displayed on the profile?

Similar to what you have, but let's have the rating right-aligned so there is space between it and the label. Also if provisional, let's add a ? after the rating instead of the chip.

Q2: Is incremental aggregation acceptable, or should we store a full history array?

I don't think we should store a full history array. The team had previously discussed a moving average or exponential moving average of the most recent X games, where the value of X had not yet been figured out. What does incremental aggregation mean in this case? Is it just a simple average?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants