feat(flashcards): learning-steps SR — повторный показ «Снова» в сессии, лимит новых карт/день

Tier-1 апгрейд интервального повторения:
- schedule() с состояниями learning/relearning/review вместо плоского sm2():
  новая карта проходит шаги [1,10] мин, «Снова» возвращает на шаг 0 (минуты),
  «Знаю» продвигает шаг → выпуск (1д), «Легко» выпускает сразу (4д); зрелая
  «Снова» = lapse → relearning (ef−0.2, ×0.5).
- study-сессия: динамическая очередь — недоученная карта (graduated=false)
  возвращается через 3 карты и показывается снова в той же сессии.
- лимит новых карт/день (decks.new_per_day, деф.20) в getStudySession и бейдже.
- превью кнопок fcPreview() показывает минуты/дни, зеркало серверной логики.
- миграция 074: state/learning_step/lapses/created_at + new_per_day + индексы.
- тесты SRS 9/9 (шаги, lapse, лимит новых).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-13 13:10:00 +03:00
parent cbb6edf372
commit 5c01a5c7ed
4 changed files with 389 additions and 85 deletions
@@ -0,0 +1,31 @@
-- 074_flashcard_learning_steps.sql
-- Tier-1 апгрейд интервального повторения: настоящие learning-steps (под-дневные
-- интервалы), повторный показ «Снова» в той же сессии и лимит новых карт в день.
--
-- flashcard_reviews получает состояние карточки:
-- state — 'review' (зрелая, день-интервалы SM-2) | 'learning' (новая в шагах)
-- | 'relearning' (зрелая после провала, снова в шагах).
-- Старые строки = 'review' (они уже были выпущены старым алгоритмом).
-- learning_step — индекс текущего шага обучения (0..N).
-- lapses — сколько раз зрелая карта проваливалась (для статистики).
-- created_at — когда карта впервые введена в оборот (для лимита новых/день).
-- Старым строкам ставим '' (date('')=NULL → не считаются «введёнными
-- сегодня»); новые строки контроллер заполняет datetime('now').
-- ВАЖНО: под-дневные интервалы живут в due_at (TEXT datetime с минутами), поэтому
-- interval_days остаётся INTEGER — менять тип не нужно.
--
-- flashcard_decks.new_per_day — сколько новых карт колоды показывать за день (деф. 20).
--
-- Примечание: ALTER TABLE ADD COLUMN в SQLite запрещает выражение-default
-- (datetime('now')), поэтому created_at = '' и проставляется кодом на INSERT.
ALTER TABLE flashcard_reviews ADD COLUMN state TEXT NOT NULL DEFAULT 'review';
ALTER TABLE flashcard_reviews ADD COLUMN learning_step INTEGER NOT NULL DEFAULT 0;
ALTER TABLE flashcard_reviews ADD COLUMN lapses INTEGER NOT NULL DEFAULT 0;
ALTER TABLE flashcard_reviews ADD COLUMN created_at TEXT NOT NULL DEFAULT '';
ALTER TABLE flashcard_decks ADD COLUMN new_per_day INTEGER NOT NULL DEFAULT 20;
-- Индексы под горячие пути (getCards / getStudySession / listDecks-подсчёты).
CREATE INDEX IF NOT EXISTS idx_fc_cards_deck ON flashcard_cards(deck_id);
CREATE INDEX IF NOT EXISTS idx_fc_reviews_card ON flashcard_reviews(card_id);