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
Open
feat(v0.3.0): per-key heatmap, per-mode PB, streak, 7/30-day rolling WPM#6withrvr wants to merge 8 commits into
withrvr wants to merge 8 commits into
Conversation
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.
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.
Storage layer:
backward compat — old sessions without the fields deserialise cleanly)
App state:
Session save (main.rs):
Results screen:
Stats screen (major rework):
to best WPM, avg accuracy, session count, last WPM)
and colour-coded accuracy (red < 80%, amber < 93%, green otherwise)
so the total still fits comfortably in a 24-row terminal
Tests: 76 passing (+28 from v0.2.0 baseline)
key_accuracy (5 cases), backward-compat deserialisation
https://claude.ai/code/session_01KUdVbjq8X6vm5TEv5Szysf