Skip to content

fix(practice): draw the first Practice problem at the unlocked level (#21) - #25

Merged
tsenoner merged 2 commits into
mainfrom
fix/21-first-problem-level
Jun 23, 2026
Merged

fix(practice): draw the first Practice problem at the unlocked level (#21)#25
tsenoner merged 2 commits into
mainfrom
fix/21-first-problem-level

Conversation

@tsenoner

Copy link
Copy Markdown
Owner

Closes #21.

First issue off the post-Levels roadmap — picked first because it's the only live bug and it directly protects the just-shipped Levels feature.

The bug

useUnlockedLevel() resolved meta.unlockedLevel into a ref asynchronously, but useDrill/useGuided seed problem #1 synchronously in their useState initializer — when the ref is still 0. Now that level tests write meta.unlockedLevel, a user who unlocked a wider range got a level-0 first problem every session (only problem #2 onward respected the unlocked level). Latent until #13 shipped; live now.

The fix

  • useUnlockedLevel() now returns { ref, level, loaded } — the live ref for imperative next-problem reads, plus the resolved level + a loaded flag.
  • useDrill/useGuided keep the lazy seed for first paint and add a once-guarded effect that regenerates problem fix: address baseline code-review findings #1 when the level resolves — only if it differs from the seed (skips level === 0, so default users never see a swap) and only while the first problem is still untouched. The draw happens outside the state updater (StrictMode-safe).
  • useSpeedrun only consumes the ref (its first problem is generated in the user-triggered start(), after the read resolves) — left unchanged.

Tests

  • New useDrill/useGuided specs assert the redraw at the unlocked level and the no-swap guard at level 0 — strengthened to assert call count + problem-object identity (a weaker first cut false-passed; an adversarial review caught it and a mutation test now confirms removing the level === 0 guard makes the no-swap test fail).
  • useUnlockedLevel test updated for the new { ref, level, loaded } shape.

Verification

  • typecheck ✓ · lint --max-warnings 0 ✓ · 290 tests ✓ · build ✓
  • Adversarial review of the diff: 7 candidates → all the same test-quality finding (now fixed) → re-verified clean via mutation testing.

🤖 Generated with Claude Code

…21)

useUnlockedLevel() resolved meta.unlockedLevel into a ref asynchronously, but
useDrill/useGuided seed problem #1 synchronously in their useState initializer —
when the ref is still 0. Now that level tests write meta.unlockedLevel, a user who
unlocked a wider range got a level-0 first problem every session.

- useUnlockedLevel() now also returns the resolved `level` + a `loaded` flag
  (alongside the live `ref` for imperative next-problem reads).
- useDrill/useGuided keep the lazy seed for first paint and add a once-guarded
  effect that regenerates problem #1 when the level resolves — only if it differs
  from the seed (skips 0, so default users see no swap) and only while the first
  problem is still untouched. StrictMode-safe (draw outside the updater).
- useSpeedrun only consumes the ref (its first problem is generated in the
  user-triggered start(), after the read resolves) — left unchanged.

Tests: new useDrill/useGuided specs assert the redraw at the unlocked level and
the no-swap guard at level 0; useUnlockedLevel test updated for the new shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daycipher Ready Ready Preview, Comment Jun 23, 2026 10:16am

Pull two copy-pasted effects out of the practice runners into shared hooks:
- useFirstProblemAtLevel: redraw the lazily-seeded problem #1 at the unlocked
  level once it resolves (was duplicated in useDrill/useGuided).
- useAttemptsRef: load the attempt history into a ref on mount (was duplicated
  in useDrill/useGuided/useSpeedrun).

Behavior unchanged; removes the per-hook duplication.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tsenoner
tsenoner merged commit 380fc70 into main Jun 23, 2026
3 checks passed
@tsenoner
tsenoner deleted the fix/21-first-problem-level branch June 23, 2026 10:18
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.

Practice: first problem after mount ignores the unlocked level (uses level 0)

1 participant