I build Pianory, a browser-based piano learning app. AI helped draft this article; I checked the behavior against the current implementation and its focused tests.
A course step can be hard without being finished or abandoned. Our course journey models that case with a recovery branch attached to the original step. The important invariant is that help changes the active activity, while the learner's step identity remains stable.
The branch is deliberately narrow. A partial attempt with a self-report rating of stuck can enter recovery only when the authored step has a recovery path and the cursor is in the original or retest phase. A failed quiz alone does not trigger it. When the condition matches, the enrollment cursor keeps its stepId and preset, adds the recovery branchId, and changes phase to explanation.
courseActiveStep resolves the explanation phase to an authored lesson and the drill phase to an authored drill. Both are temporary activities under the original step. Passing the explanation moves to the drill; passing the drill moves to a retest of the original task. A failed recovery quiz keeps the learner in its current phase. The original completion policy, rather than the helper quiz, decides whether the course step is complete.
This is a useful state-machine boundary: the learner can ask for help and return to the same passage rather than losing their place or silently advancing. In course-journey.spec.ts, the recovery case checks preservation of step and preset, a failed explanation quiz, and the return to the original passage. The focused suite passed 11 tests on 28 October 2026. A separate browser test describes recovery surviving a reload; I did not rerun that end-to-end test for this article.
The limitation is as important as the path. This behavior applies to steps with an authored recovery path and an explicit stuck self-report. It is not a general promise that every failed attempt automatically selects a lesson.
Implementation: src/lib/domain/course-journey.ts (courseActiveStep and attempt transition), with the focused test in src/lib/domain/course-journey.spec.ts. The design lesson for me is to make help a reversible route through the original task, with the transition conditions visible in tests.
Top comments (0)