feat: implement spaced-repetition study loop - #1
Merged
Conversation
Adds the full SRS study loop end-to-end on Android:
- Simplified SM-2 scheduler (SrsScheduler) with first-review intervals
matching the design's SRSRow (Again <10m / Hard 1d / Good 3d / Easy 7d),
covered by SrsSchedulerTest.
- SrsRepositoryImpl: Pubky-backed (records at /pub/echo/decks/{deckId}/srs/
{cardId}.json) with an in-memory session cache, mirroring CardRepositoryImpl.
Extends SrsRepository with dueForDeck() and review(card, grade).
- StudySessionViewModel (shared): queue -> reveal -> grade -> next -> complete,
with an optional deckId (null = all due cards, else a single deck).
- Real due-counts wired into HomeViewModel and DeckDetailViewModel.
- Speaker (TTS): Android TextToSpeech impl + iOS no-op stub, DI-bound; wired
to the study Speak effect and EditCard's previously-no-op Speak.
- Android StudySessionScreen + study?deckId= route, reachable from Home's
"Start studying" and DeckDetail's NavigateStudy effect.
Resolves the CLAUDE.md vs Architecture.md SRS-storage contradiction in favor
of Pubky as the source of truth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Aug 15, 2026
Closed
jvsena42
added a commit
that referenced
this pull request
Sep 3, 2026
Four inputs the CLI accepted and turned into something the caller did not ask for. **A third TSV column was stored as an image URL without being one.** The image-column format engaged whenever every line had three or more tab fields, so a three-column Anki export — Front / Back / Example sentence, a very common shape — published every card with `MediaRef.Image(url = "una manzana roja")`. Both apps then try to load prose as a picture, the column's real content is lost, and `--json` reports success. Every app-side constructor of a remote image ref takes its URL from a picker; this was the first path where an arbitrary string reached one. Now the format test requires the image columns to hold `http(s)` URLs, and the two entry points differ deliberately: `import` falls through to the text parser (there the third column is content somebody wants imported), while `card add --from-file` errors (there the four-column TSV is what was explicitly asked for). **A malformed `LOOPKY_SESSION` reported `session_expired`.** A parse failure is not an expiry, and exit 4 has a code of its own precisely so an agent can tell a dead session from a wobbly network — teaching it "expired" for a typo'd environment variable is that confusion one layer up. The shape is checked before the FFI sees it. **`--limit twenty` silently became 20**, and `--limit 0` passed straight through, for the one command whose entire output is a ranked list. A silently different N is the class of quiet wrongness the envelope's `indexer` field exists to prevent elsewhere. **A card whose front is `#1 ranked` vanished.** `startsWith("#")` swallowed markdown headings and hashtags, against this file's own contract that a batch producing fewer cards than it has lines is exactly the loss `--json` exists to show. A comment is now `#` followed by whitespace. Also exempts the two test trees this branch added from `MagicNumber` and `TooManyFunctions`, which every other test tree already was — a test class's function count is how much behaviour it pins down. Found in review by jvsena42 on #208. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BXjHov9XSqWtJg8AiqKpBQ
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.
Context
Echo had the
SrsState/SrsGrademodels and anSrsRepositoryinterface but no study loop:SrsRepositoryImpldidn't exist,HomeViewModelhardcodeddueToday = 0, andDeckDetailViewModelemitted an unwiredNavigateStudyeffect. This PR implements the full spaced-repetition loop end-to-end on Android, built from the four study screens indesign/main/phone-echo.pen(Home/Daily Study, Study Session front/back, Empty State) and theSRSRowcomponent.What's included
Shared (
commonMain)domain/model/SrsScheduler.kt— simplified SM-2 (review/previewIntervals/isDue). First-review intervals match the SRSRow design exactly: Again<10m/ Hard1d/ Good3d/ Easy7d. Unit-tested (SrsSchedulerTest).data/pubky/SrsStateDto.kt+PubkyPaths.srs(...)— SRS persists to the user's homeserver at/pub/echo/decks/{deckId}/srs/{cardId}.json.data/repository/impl/SrsRepositoryImpl.kt— Pubky-backed + in-memory session cache, modeled onCardRepositoryImpl. Interface gainsdueForDeck()andreview(card, grade)(repo owns grading — no use-case layer).presentation/study/StudySessionViewModel.kt— queue → reveal → grade → next → complete; optionaldeckId(null = all due across decks, else one deck).platform/Speaker.kt(+ AndroidTextToSpeech, iOS no-op stub) — DI-bound TTS for the Speak button.HomeViewModelandDeckDetailViewModel.Android
ui/study/StudySessionScreen.kt+study?deckId=route, reached from Home "Start studying" and DeckDetail'sNavigateStudy. Tap-to-reveal viaCrossfade; grade buttons usesrsAgain/Hard/Good/Easytokens (added missingsrsHard/srsEasy).Decisions (confirmed with author)
Verification
./gradlew :composeApp:assembleDebug✅./gradlew :shared:testDebugUnitTest✅ (scheduler + existing tests pass)Notes
:sharedcompilation was already broken onmain(pre-existingGlobalContexterrors; iOS not yet runnable per CLAUDE.md). No iOS regression introduced; iOS Speaker is a deliberate no-op stub.doneToday/masteredPercentstay as placeholders — they need a persisted session-history store not present in v1.🤖 Generated with Claude Code