feat(trainer): P3 — текстовые задачи от LLM с серверной проверкой подстановкой

- practiceVerify.js: грузит SimExpr в Node (require), verifyRoot подстановкой корня
- practiceGenService.js: LLM (инъектируемый ask) → parse → validateAndVerify (SimExpr + подстановка + санитизация) → авторетрай по фидбэку; дефолт ask = assistantController.callLLMFailover
- пул practice_problems (мигр.083); POST /api/practice/generate (учитель/админ) + GET /api/practice/pool
- инвариант: невалидная/неверная задача в БД НЕ пишется → ученику не попадёт
- клиент: LS.practicePool/Generate, тема «Текстовые задачи» (из пула; учителю кнопка «Сгенерировать»)
- тесты practice-gen.test.js 13/13 (verify, ретраи, off→503, 403 ученику, пул); смоуки страница 26/26; план P3 → DONE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-25 14:00:39 +03:00
parent 48a73d9f8e
commit 8c4c9bf04c
9 changed files with 445 additions and 9 deletions
@@ -0,0 +1,28 @@
-- ═══════════════════════════════════════════════════════════════
-- 083: Пул текстовых задач тренажёра (Уровень 1, Фаза 3).
--
-- Кэш сгенерированных LLM и ПРОВЕРЕННЫХ задач: модель предлагает условие +
-- уравнение (lhs/rhs) + корень, сервер подтверждает подстановкой (practiceVerify)
-- и только тогда пишет сюда. Ученик берёт готовые задачи из пула (не платим за
-- генерацию на каждый показ). story и заметки решения уже санитизированы.
-- status: approved (видна ученикам) | draft (на ревью учителю).
-- created_by ON DELETE SET NULL — задача переживает удаление автора.
-- ═══════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS practice_problems (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic TEXT NOT NULL, -- word-linear | word-proportion | word-percent
skill TEXT NOT NULL, -- ключ навыка (для прогресса)
difficulty INTEGER NOT NULL DEFAULT 1,
story TEXT NOT NULL, -- условие словами (экранировано)
lhs TEXT NOT NULL, -- левая часть уравнения (выражение от x)
rhs TEXT NOT NULL, -- правая часть
answer_var TEXT NOT NULL DEFAULT 'x',
answer REAL NOT NULL, -- проверенный корень
solution_json TEXT, -- шаги [{note,tex}] (JSON)
status TEXT NOT NULL DEFAULT 'approved', -- approved | draft
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_practice_problems_skill ON practice_problems (skill, status);