feat(trainer): P13 — конструктор параметрических генераторов

- custom_generators (мигр.084, spec_json + draft/published); customGeneratorController: validateGenSpec без исполнения (лимиты/типы), CRUD own+published + ownership
- /api/practice/generators[/:id]; клиент LS.practiceGen*
- страница /trainer-builder (учитель): форма (pick/derive/lhs/rhs/display/answer/solution) + живое превью через TE.instantiate(strict) (материализация + проверка ответа подстановкой) + список своих (правка/удаление/публикация)
- тренажёр грузит свои+опубликованные генераторы в тему «Мои генераторы» (пошаговый режим работает); пункт сайдбара /trainer-builder (teacher-only)
- тесты custom-generators.test.js 12/12; смоук движка 402/402 (T17 кастомный спек + strict-валидация); страница 33/33; ROADMAP_V2 P13 → DONE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-25 15:30:08 +03:00
parent 47d4f71eac
commit 6d600ad576
9 changed files with 694 additions and 7 deletions
@@ -0,0 +1,24 @@
-- ═══════════════════════════════════════════════════════════════
-- 084: Пользовательские генераторы тренажёра (конструктор, Roadmap P13).
--
-- Учитель создаёт ПАРАМЕТРИЧЕСКИЙ генератор задач — это ДАННЫЕ (spec_json):
-- диапазоны pick, формулы derive, шаблоны lhs/rhs, ответ, шаги решения. На
-- клиенте спек исполняет БЕЗОПАСНЫЙ SimExpr (⛔ без eval), на сервере он только
-- хранится и валидируется по структуре/лимитам (НЕ исполняется). Прогресс по
-- такому навыку ключуется как 'cg<id>'.
-- status: draft (видит только автор) | published (видят и ученики).
-- owner_id ON DELETE CASCADE — генераторы удаляются вместе с автором.
-- ═══════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS custom_generators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
topic TEXT NOT NULL DEFAULT 'custom',
spec_json TEXT NOT NULL, -- полный спек генератора (данные)
status TEXT NOT NULL DEFAULT 'draft', -- draft | published
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_custom_generators_owner ON custom_generators (owner_id, status);