feat(trainer): P2 — умная тренировка, интервальное повторение, итог сессии

- adaptive.js (TrainerAdaptive): nextSkill (in-session повтор → серверный due → прогрессия → удержание), onWrong/onCorrect (очередь повторения), sessionStats
- умная тренировка на странице (тумблер, по умолч. вкл): авто-подбор навыка от простого к сложному, возврат ошибок
- сессия из 10 задач + экран «Итог сессии» (верно/точность/навыки/стоит повторить); неверный ответ авто-показывает решение
- сервер: SR-поля box+due_at на practice_progress (мигр.082, Leitner 0/1/3/7/16/30 дн), listProgress отдаёт box/due_at/due
- смоуки: adaptive 12/12, страница 23/23, practice.test.js 11/11 (+SR box/due); план P2 → DONE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-25 13:46:29 +03:00
parent 20b8ce2c5b
commit 48a73d9f8e
6 changed files with 281 additions and 29 deletions
+12
View File
@@ -79,6 +79,18 @@ describe('/api/practice progress', () => {
assert.equal(miss.body.progress.mastered, 1, 'mastered is sticky');
});
it('SR: box растёт на верный ответ и сбрасывается на ошибку; due отражает срок', async () => {
const sk = 'sr-skill';
const c1 = await inject('POST', '/api/practice/attempt', { skill: sk, correct: true }, token);
assert.equal(c1.body.progress.box, 1, 'box=1 после первого верного');
assert.equal(c1.body.progress.due, 0, 'свежий навык не просрочен (срок в будущем)');
const c2 = await inject('POST', '/api/practice/attempt', { skill: sk, correct: true }, token);
assert.equal(c2.body.progress.box, 2, 'box растёт на следующем верном');
const w = await inject('POST', '/api/practice/attempt', { skill: sk, correct: false }, token);
assert.equal(w.body.progress.box, 0, 'ошибка сбрасывает box в 0');
assert.equal(w.body.progress.due, 1, 'после ошибки навык сразу к повторению (due=1)');
});
it('progress is per-user (другой ученик начинает с нуля)', async () => {
const other = (await getToken('student')).token;
const res = await inject('POST', '/api/practice/attempt', { skill: SKILL, correct: true }, other);