Skip to content

feat(v0.3.0): per-key heatmap, per-mode PB, streak, 7/30-day rolling WPM#6

Open
withrvr wants to merge 8 commits into
mainfrom
feature/v0.3.0
Open

feat(v0.3.0): per-key heatmap, per-mode PB, streak, 7/30-day rolling WPM#6
withrvr wants to merge 8 commits into
mainfrom
feature/v0.3.0

Conversation

@withrvr

@withrvr withrvr commented Jun 1, 2026

Copy link
Copy Markdown
Owner

Storage layer:

  • Add key_hits / key_misses fields to SessionRecord (serde(default) for
    backward compat — old sessions without the fields deserialise cleanly)
  • personal_best_for_mode(): per-mode PB lookup
  • streak(): consecutive-day streak ending on today/yesterday
  • avg_wpm_last_n_days(): rolling WPM average for last N days
  • key_accuracy(): aggregate per-key stats sorted worst-first
  • KeyAccuracyStat struct for the heatmap UI

App state:

  • key_hits / key_misses HashMaps in App, cleared on start_game
  • handle_char now records hit (typed == expected) or miss (wrong / extra)

Session save (main.rs):

  • Include key_hits + key_misses when constructing SessionRecord

Results screen:

  • Replace all-time "best ever" with per-mode personal best
  • Show "★ new best!" badge when the session sets a new mode PB

Stats screen (major rework):

  • Expanded summary: streak, 7-day avg WPM, 30-day avg WPM (in addition
    to best WPM, avg accuracy, session count, last WPM)
  • Two-column layout: summary (55%) + key-accuracy heatmap (45%)
  • Key heatmap shows up to 5 worst keys (≥3 presses) with ok/total counts
    and colour-coded accuracy (red < 80%, amber < 93%, green otherwise)
  • "no key data yet (keep typing!)" message when heatmap has no data
  • Sparkline reduced to 5 rows; sessions table uses Constraint::Min(4)
    so the total still fits comfortably in a 24-row terminal

Tests: 76 passing (+28 from v0.2.0 baseline)

  • storage: personal_best, mode_pb, streak (7 cases), avg_wpm (4 cases),
    key_accuracy (5 cases), backward-compat deserialisation
  • app: correct/wrong/extra key tracking, accumulation, clear on restart

https://claude.ai/code/session_01KUdVbjq8X6vm5TEv5Szysf

claude and others added 8 commits May 19, 2026 03:41
Storage layer:
- Add key_hits / key_misses fields to SessionRecord (serde(default) for
  backward compat — old sessions without the fields deserialise cleanly)
- personal_best_for_mode(): per-mode PB lookup
- streak(): consecutive-day streak ending on today/yesterday
- avg_wpm_last_n_days(): rolling WPM average for last N days
- key_accuracy(): aggregate per-key stats sorted worst-first
- KeyAccuracyStat struct for the heatmap UI

App state:
- key_hits / key_misses HashMaps in App, cleared on start_game
- handle_char now records hit (typed == expected) or miss (wrong / extra)

Session save (main.rs):
- Include key_hits + key_misses when constructing SessionRecord

Results screen:
- Replace all-time "best ever" with per-mode personal best
- Show "★ new best!" badge when the session sets a new mode PB

Stats screen (major rework):
- Expanded summary: streak, 7-day avg WPM, 30-day avg WPM (in addition
  to best WPM, avg accuracy, session count, last WPM)
- Two-column layout: summary (55%) + key-accuracy heatmap (45%)
- Key heatmap shows up to 5 worst keys (≥3 presses) with ok/total counts
  and colour-coded accuracy (red < 80%, amber < 93%, green otherwise)
- "no key data yet (keep typing!)" message when heatmap has no data
- Sparkline reduced to 5 rows; sessions table uses Constraint::Min(4)
  so the total still fits comfortably in a 24-row terminal

Tests: 76 passing (+28 from v0.2.0 baseline)
  - storage: personal_best, mode_pb, streak (7 cases), avg_wpm (4 cases),
    key_accuracy (5 cases), backward-compat deserialisation
  - app: correct/wrong/extra key tracking, accumulation, clear on restart

https://claude.ai/code/session_01KUdVbjq8X6vm5TEv5Szysf
- Cargo.toml: version 0.2.0 → 0.3.0
- CHANGELOG.md: full 0.3.0 Added / Changed / Compatibility section
- docs/ROADMAP.md: mark all four v0.3.0 items ✅
- docs/USAGE.md: document Stats screen panels, per-mode PB, key heatmap
- docs/ARCHITECTURE.md: document new storage helpers and key_hits/key_misses
  fields; update module responsibilities table
- src/ui/results.rs: fix Zen mode — show "— (zen not saved)" placeholder
  instead of always showing "★ new best!" (Zen sessions are never persisted
  so there is no meaningful mode PB to track)

https://claude.ai/code/session_01KUdVbjq8X6vm5TEv5Szysf
Adds a comprehensive stats-analysis test suite covering both new v0.3.0
helpers and the existing v0.1/v0.2 ones. Designed to prove zero regressions
in legacy behaviour while exercising the new code over many shapes of data.

Refactor (test-only):
- Extract pure-path helpers `save_session_to_path` and `load_sessions_from_path`
  so disk I/O can be exercised against a tempdir without touching ~/.typerush.
- The original `save_session` / `load_sessions` public API is unchanged — they
  now thin-wrap the path-based helpers using `stats_path()`.
- Add `tempfile = "3"` as a [dev-dependencies] entry (test-only, no production
  impact).

25 new scenarios in storage::tests::scenario_*:
  1.  Empty history — every helper returns the safe-default (None/0/empty)
  2.  Ten varied sessions — PB, mode-PB, average accuracy, key-heatmap order
  3.  Single session — all six helpers
  4.  Large history (10,000 sessions) — correctness + perf budget (<5s)
  5.  Large history with multi-segment session timeline — streak picks current run
  6.  Pure legacy JSON (v0.1/v0.2 format, no key_hits/key_misses) loads cleanly
      and every analysis helper aggregates correctly over it
  7.  Mixed legacy + new records in one file — both load, aggregation correct
  8.  Full JSON round-trip — every SessionRecord field preserved exactly
  9.  Corrupt JSON file — falls back to empty Vec, no panic
  10. Empty file — falls back to empty Vec
  11. Missing file — falls back to empty Vec
  12. save_session_to_path → load_sessions_from_path single-record round-trip
  13. 50 sequential saves — file grows correctly, append order preserved
  14. Save into existing legacy file — preserves old records, appends new one
  15. 100 sessions on the same day → streak = 1
  16. 7-day vs 30-day windows return correctly different averages
  17. Full a-z heatmap — monotonic ordering verified, worst-first sort holds
  18. Unicode key support (é, 中) in the heatmap
  19. Realistic 14-day-streak user with 3 sessions/day across 5 modes
  20. Zero-WPM session — personal_best returns Some(0.0), not None
  21. Session exactly 2 days ago → streak = 0 (correctly broken)
  22. Today + 2-days-ago (gap on yesterday) → streak = 1 (no bridging)
  23. Mode-PB does NOT leak across modes
  24. min_presses filter excludes keys below threshold
  25. data_dir() and stats_path() resolve correctly

Test count: 76 → 104 (+28 incl. mod-helper coverage).
All existing v0.1/v0.2 tests continue to pass — clippy -D warnings clean,
cargo fmt --check clean, cargo build --release clean.

https://claude.ai/code/session_01KUdVbjq8X6vm5TEv5Szysf
Keys tied at the same accuracy percentage (e.g. 'r' and 'n' both at
80%) were sorted non-deterministically because HashMap iteration order
is random between renders, causing visible flicker in the key accuracy
panel. Added a secondary alphabetical tiebreaker so equal-accuracy keys
always appear in a fixed order. Also adds a regression test covering
this case.
Previously the Stats screen called load_sessions() + key_accuracy() on
every frame (10x/sec), reading and re-scanning the full stats.json each
time — O(n_sessions) disk I/O per render.

Two complementary fixes:

1. AggregateStats (aggregate.json) — cumulative per-key hit/miss totals
   updated incrementally on each session save (O(keys_in_session)). The
   key-accuracy panel now reads from App::aggregate via the new
   key_accuracy_from_aggregate(), keeping it O(distinct_keys) regardless
   of history size. On first startup after upgrade, the aggregate is
   rebuilt from existing sessions (one-time O(n) cost) and then cached.

2. stats_cache — full session list loaded once when entering the Stats
   screen and stored in App::stats_cache. All other Stats panels
   (sparkline, summary, table) read from the cache. Invalidated on each
   session save so the next Stats visit picks up fresh data.

At 10k sessions: previously ~10 MB disk reads/sec while viewing Stats;
now a single load on screen entry + O(50) aggregate lookup per frame.
Code review pass ahead of the v0.3.0 release.

- Results screen no longer reads stats.json on every frame — it now uses
  the same App::stats_cache as the Stats screen (populated on screen
  entry, after the save). This was the last per-frame disk read in the
  render path.
- Fix stats.rs module doc: heatmap shows 5 worst keys, not 8.
- CHANGELOG: add release date (2026-06-01) and document the read-path
  performance work (session cache + aggregate.json) and the stable
  key-ordering fix.
- README: advertise the new v0.3 stats (streak, 7/30-day averages,
  per-key heatmap, per-mode PBs).
- ARCHITECTURE / USAGE: document AggregateStats, aggregate.json, and the
  stats_cache read-path design.
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