The card you mark as known never comes back
Flashcards has three states and no spaced-repetition scheduler. Marking a card known removes it from revision permanently, and four different screens disagree about which cards are due.
Flashcards has three states. A card is new, known, or difficult. Swipe right and it becomes known and leaves the due pile. Nothing ever puts it back. There is no un-know button, no interval, no ease factor and no lapse count, because there is no spaced-repetition scheduler anywhere in the feature.
What we built instead of a schedule
A counter that looks like one. Every swipe increments reviewCount on the card, and the value is stored and survives a restart. Nothing reads it. Searching the app finds the two writes, the model field, and the storage round trip — and no reader at all. It is a number we kept because keeping it felt like the responsible thing to do, and then never used.
So a student who half-remembers a card on day one and a student who knows it cold six weeks later hold identical records, because there is nothing in the record to tell them apart.
Known is a terminal state
enum FlashcardMastery { newCard, known, difficult }
Marking a card known sets the mastery and clears the review-later flag. Marking it difficult sets the mastery and does not clear that flag. And nothing anywhere sets a card back to newCard after it is created: that value appears in the enum, the constructor default, deserialisation, and the pending filter on the reminder side, and nowhere else.
Known is the de-facto retirement state, which would be perfectly reasonable if it were reachable in both directions. It is not. Tap Known by accident — or tap it on a card you skimmed rather than checked — and that card is out of your revision permanently, with no route back through any screen in the app.
Four definitions of "due"
Because the card model has no schedule, "due" has to be worked out on the fly, and each surface works it out differently:
- the home banner counts decks whose mastery is below 0.5
- the notification scheduler counts individual cards that are new or difficult
- the deck ring is green at 0.8 and above, orange at 0.4 and above
- the analytics screen calls a deck weak below 0.5 and strong at 0.7 or above
A deck with six of ten cards known is, at the same moment: not pending, four cards due, orange, and neither weak nor strong. Four answers to one question, none of them wrong on its own terms, and no single place in the code that decides.
The bug that actually reached students
Learn with Gen AI has a Create Flashcards action. The provider method returns the new deck's key on success and null on failure, because it returns the generation call's result directly. The screen calls that returned value error and puts it straight into a snackbar.
So on success the student sees the raw deck key — something like os·deadlock. On failure — an empty generation, or no AI provider configured at all — they see "Flashcards added". The formula screen carries a comment documenting this exact inversion as a bug already found and fixed there. It is still live in the Learn flow, which means the same mistake was fixed in one place and left in another.
Regenerating a deck throws away what you learned
Generating a deck removes the existing one and re-adds fresh cards with new ids, so the mastery of every card in it goes with it. The background generator guards against this with an explicit "does this deck already exist" check. The three human-triggered paths do not: the new-deck sheet, the formula screen's Generate again button, and Create Flashcards in Learn.
At least the ordering is deliberate. The empty-result check happens before the removal, so a failed regeneration leaves the old deck and its mastery intact. Only a successful regeneration costs you your progress, and nothing warns you before it happens.
Two smaller things
Every swipe re-encodes every card you own. All flashcards live under a single storage key as one JSON string, so an update decodes the whole array, replaces one entry, and encodes the whole array back — on the platform thread, including at cold start, where the provider loads the corpus in its constructor.
And there is a second flashcard type in the app, the cards an AI Notebook generates, which can never reach a deck. There is also no way to write a card by hand. Every study flashcard in the app came out of a model.
Why not just implement SM-2?
Because we did not get to it, and the honest version of that is that we spent the scheduling effort on topics instead — the feature next door, which does have a real interval ladder. Cards got a binary verdict.
A binary verdict is a defensible simplification for a first version. An irreversible one is not, because it is not a simplification of anything — it is a dead end. The smallest fix is to let a known card come back: a third state, or an un-know action in the review screen. The second is to make the four due definitions agree. Both are small. Neither is done.
Keep reading
Related reading
A topic you have not opened in a month still says 'Yesterday'
Daily Revision schedules reviews on a five-rung ladder and then labels them with times that cannot be right. An item untouched for a month still reads "Yesterday".
The Daily Quiz has no daily content, and that is deliberate
Daily Quiz seeds nothing by date and generates nothing when the app opens. The quizzes are the output of a durable background queue, and the only daily thing about the feature is which five of them we remind you about.
Every rule that makes a timetable a timetable is English text in a prompt
We went looking for the scheduler in our AI study timetable and there is not one. No overlap detection, no chronological sort, no feasibility check — the plan is whatever the model returns, and the rules exist only as sentences asking it to behave.