Skip to content

feat: implement spaced-repetition study loop - #1

Merged
jvsena42 merged 3 commits into
mainfrom
feat/study-loop
Jun 6, 2026
Merged

feat: implement spaced-repetition study loop#1
jvsena42 merged 3 commits into
mainfrom
feat/study-loop

Conversation

@jvsena42

@jvsena42 jvsena42 commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Context

Echo had the SrsState/SrsGrade models and an SrsRepository interface but no study loop: SrsRepositoryImpl didn't exist, HomeViewModel hardcoded dueToday = 0, and DeckDetailViewModel emitted an unwired NavigateStudy effect. This PR implements the full spaced-repetition loop end-to-end on Android, built from the four study screens in design/main/phone-echo.pen (Home/Daily Study, Study Session front/back, Empty State) and the SRSRow component.

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 / Hard 1d / Good 3d / Easy 7d. 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 on CardRepositoryImpl. Interface gains dueForDeck() and review(card, grade) (repo owns grading — no use-case layer).
  • presentation/study/StudySessionViewModel.kt — queue → reveal → grade → next → complete; optional deckId (null = all due across decks, else one deck).
  • platform/Speaker.kt (+ Android TextToSpeech, iOS no-op stub) — DI-bound TTS for the Speak button.
  • Real due-counts wired into HomeViewModel and DeckDetailViewModel.

Android

  • ui/study/StudySessionScreen.kt + study?deckId= route, reached from Home "Start studying" and DeckDetail's NavigateStudy. Tap-to-reveal via Crossfade; grade buttons use srsAgain/Hard/Good/Easy tokens (added missing srsHard/srsEasy).

Decisions (confirmed with author)

  • SRS storage: Pubky-backed — resolves the CLAUDE.md vs Architecture.md §8.3 contradiction in favor of "Pubky is the source of truth."
  • Scope: session handles both all-due and per-deck.
  • Algorithm: simplified SM-2.
  • TTS: real Android implementation now; iOS stubbed.

Verification

  • ./gradlew :composeApp:assembleDebug
  • ./gradlew :shared:testDebugUnitTest ✅ (scheduler + existing tests pass)
  • detekt: new shared code is clean; the only new findings are 2 Compose warnings on the new screen that match every existing screen's convention.

Notes

  • iOS :shared compilation was already broken on main (pre-existing GlobalContext errors; iOS not yet runnable per CLAUDE.md). No iOS regression introduced; iOS Speaker is a deliberate no-op stub.
  • doneToday / masteredPercent stay as placeholders — they need a persisted session-history store not present in v1.

🤖 Generated with Claude Code

jvsena42 and others added 3 commits June 6, 2026 11:02
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>
@jvsena42
jvsena42 merged commit 01cb389 into main Jun 6, 2026
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
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.

1 participant