From 2b7f6ab12f86615858bb48016f4f85fdbb98ea01 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Fri, 26 Jun 2026 13:04:52 +0300 Subject: [PATCH] =?UTF-8?q?docs(trainer):=20=D0=BF=D0=BB=D0=B0=D0=BD=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=B2=D0=B8=D1=82=D0=B8=D1=8F=20v4=20?= =?UTF-8?q?=E2=80=94=20=D1=80=D0=B0=D0=B7=D0=BD=D0=BE=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87/?= =?UTF-8?q?=D1=83=D1=81=D0=BB=D0=BE=D0=B2=D0=B8=D0=B9=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BC=D0=B0=D0=BC=20+=20=D0=B2=D1=81=D0=B5=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROADMAP_V4.md — мастер-план: 103 новых генератора по 6 группам тем (матрица диверсификации), 38 формулировок условий, 10 новых форматов заданий (kinds: choice/verify/findError/fillBlank/estimate/multi/order/match/context/figureAsk), педагогика (3-уровн. подсказки, библиотека ошибок, образцы, guided, сократ.), адаптивность (запись уровня/времени, мастерство на L3, граф пререквизитов, диагностика, due-mix), вовлечение+учитель (XP, задания+журнал, аналитика, карта-созвездие), охват ЦТ (функции/корни/логарифмы/тригонометрия/коорд-геометрия, ЦТ-режим), техника/качество. Рекомендуемая последовательность V4.1–V4.6. V4_GENERATOR_SPECS.md — спутник: полные «корень-вперёд»-рецепты всех 103 генераторов (форма/пример/вывод/фигура), формулировки и сквозные предложения с what/why/how/effort. Собрано анализом 11 агентов по реальному движку (каждое предложение реализуемо в текущем контракте, инварианты соблюдены). Co-Authored-By: Claude Opus 4.8 (1M context) --- plans/ai-trainer/ROADMAP_V4.md | 364 +++++++ plans/ai-trainer/V4_GENERATOR_SPECS.md | 1325 ++++++++++++++++++++++++ 2 files changed, 1689 insertions(+) create mode 100644 plans/ai-trainer/ROADMAP_V4.md create mode 100644 plans/ai-trainer/V4_GENERATOR_SPECS.md diff --git a/plans/ai-trainer/ROADMAP_V4.md b/plans/ai-trainer/ROADMAP_V4.md new file mode 100644 index 0000000..bbc98f4 --- /dev/null +++ b/plans/ai-trainer/ROADMAP_V4.md @@ -0,0 +1,364 @@ +# ИИ-Тренажёр — План развития v4: разнообразие задач + все направления + +**Дата:** 2026-06-26. Фокус заказа — **разнообразие задач и условий по текущим темам**; +плюс полный охват направлений (форматы, педагогика, адаптивность, вовлечение, охват ЦТ, техника). +План собран глубоким анализом (11 агентов, каждый сверялся с РЕАЛЬНЫМ движком): +**103 новых генератора, 38 новых формулировок условий, 44 сквозных улучшения.** + +## Что уже есть (база v1–v3) + +- Движок параметрических генераторов + детерминированная проверка `SimExpr`. +- **64 генератора / 21 тема** (алгебра + геометрия). Виды: `solve`, `compute`, `roots`, + `simplify`, `inequality`, `system`. +- Геометрия с **чертежами-данными** (`figures.js`, 13 типов) + режим **«читать с чертежа»** + (`figurePrompt` у всех 19 геом-генераторов, тумблер «Текст / На чертеже»). +- Структурные уровни сложности (1–3), умная тренировка + интервальное повторение (SR, + мигр.081/082), LLM-задачи с серверной проверкой (мигр.083), пошаговое решение (`checkStep`), + правиловый разбор ошибок (`analyzeMistake`), 3-уровневое объяснение (`/api/practice/explain`), + мат-клавиатура + live-KaTeX, аналитика класса, авторинг/раздача учителем, конструктор + генераторов (`/trainer-builder`, мигр.084). + +## Инварианты (НЕ нарушать ни в одном пункте) + +1. ⛔ Только `SimExpr` — **без `eval`/`new Function`**. +2. **«Корень-вперёд»**: сначала выбираем целый корень/множители, ВЫВОДИМ остальное → ответ + всегда чистый (целое ИЛИ конечная десятичная дробь, как окружность с π≈3,14), самопроверка + движка проходит. Ответ ВСЕГДА вводим без потери точности. +3. ⛔ **Без эмодзи** — только inline SVG `.ic` / Lucide. Тексты экранируются. +4. Зарезервированные имена параметров: `t, w, h, pi, e, E, PI, tau` — нельзя. +5. Каждая фаза — со смоуками/тестами и коммитом; `lint:routes` baseline 0. + +### Ключевые ограничения движка (выявлены анализом — важно для реализации) + +- `roots` проверяет КАЖДЫЙ корень подстановкой → `|ax+b|=c` реализуется как `roots` с + `lhs:'abs(...)'` (exprToLatex уже рендерит `\left|...\right|`). +- `inequality` поддерживает только ОДНУ полупрямую `{op, bound}`. Двойные неравенства + `aa (смена знака) | 3 | inequality | +| inequalities | `ineq-paren` | a(x+b)>c | 2 | inequality | +| inequalities | `ineq-count-int` | сколько целых решений / наим. целое | 2 | compute | +| systems | `sys-subst` | подстановка (одно ур-е разрешено) | 2 | system | +| systems | `sys-sum-diff` | x+y=S, x−y=D | 1 | system | +| systems | `sys-3x3` | система 3×3 (тизер, answerVars=3) | 3 | system | +| systems | `sys-word` | текстовая на два неизвестных | 2 | system | + +**Формулировки:** инверс «при каком a корень = R», конструктор «дополни 2x+▢=10 чтобы корень +3», пропущенный шаг (берётся из `solution[]`), счёт целых решений (обход ограничения +`inequality`), инверс параметра системы «при каком k решение (1;2)». + +### Группа 2. Пропорции · Проценты · Текстовые (17 генераторов) + +| Тема | id | Форма / условие | L | kind | +|---|---|---|---|---| +| proportions | `prop-direct-word` | прямая пропорция (цена/кол-во) | 1 | compute | +| proportions | `prop-inverse-word` | обратная (рабочие/дни) | 2 | compute | +| proportions | `prop-scale-map` | масштаб карты | 2 | compute | +| proportions | `prop-compound` | тройная пропорция | 3 | compute | +| proportions | `prop-share-ratio` | деление в отношении a:b | 2 | compute | +| percents | `pct-increase` | увеличение на p% | 2 | compute | +| percents | `pct-decrease` | уменьшение на p% | 2 | compute | +| percents | `pct-change` | на сколько % изменилось | 3 | compute | +| percents | `pct-simple-interest` | простые проценты (вклад) | 2 | compute | +| percents | `pct-compound-2y` | сложные проценты (2 года) | 3 | compute | +| percents | `pct-restore-before` | исходное до изменения | 3 | compute | +| applied | `app-meet` | встречное движение | 2 | compute | +| applied | `app-overtake` | движение вдогонку | 3 | compute | +| applied | `app-upstream` | по реке (по/против течения) | 2 | compute | +| applied | `app-work-joint` | совместная работа | 3 | compute | +| applied | `app-mix-blend` | смешивание растворов | 3 | compute | +| applied | `app-profit-pct` | прибыль в процентах | 3 | compute | + +**Формулировки:** «что больше: 30% от 80 или 40% от 50», таблица-данные, цепочка скидка→налог, +проценты vs процентные пункты, прикидка, средняя скорость кругового рейса (2v₁v₂/(v₁+v₂)). + +### Группа 3. Выражения: Упрощение · Степени · Формулы (16 генераторов) + +| Тема | id | Форма / условие | L | kind | +|---|---|---|---|---| +| simplify | `simp-like-multivar` | привести подобные (две буквы) | 2 | simplify | +| simplify | `simp-like-const` | подобные с числом | 2 | simplify | +| simplify | `simp-sub-bracket` | вычесть скобку (знаки) | 3 | simplify | +| simplify | `simp-distribute-combine` | раскрыть и привести | 3 | simplify | +| simplify | `simp-factor-common` | вынести общий множитель | 2 | simplify | +| simplify | `simp-factor-group` | группировка | 3 | simplify | +| formulas | `diff-sq-factor` | разложить разность квадратов | 3 | simplify | +| formulas | `sq-trinom-factor` | свернуть в квадрат | 3 | simplify | +| formulas | `sq-sum-coef` | квадрат суммы с коэффициентом | 3 | simplify | +| formulas | `cube-sum` | куб суммы | 3 | simplify | +| formulas | `sum-cubes-factor` | сумма кубов | 3 | simplify | +| powers | `pow-div` | частное степеней | 2 | simplify | +| powers | `pow-product-base` | степень произведения | 2 | simplify | +| powers | `pow-frac-combine` | дробь степеней | 3 | simplify | +| powers | `pow-numeric-laws` | степени одного основания (число) | 2 | compute | +| powers | `pow-standard-form` | стандартный вид числа | 2 | compute | + +**Формулировки:** вставь число в тождество (`(x+a)²=x²+▢x+a²`), найди-ошибку→правильный ответ, +«запиши в виде произведения» (обратное разложение, simplify-эквивалентность), «в какую степень +возвести», упрости-и-вычисли при x=x₀ (мост к арифметике, защита от заучивания строки). + +### Группа 4. Квадратные · Прогрессии (16 генераторов) + +| Тема | id | Форма / условие | L | kind | +|---|---|---|---|---| +| quadratic | `quad-incomplete-bx` | ax²+bx=0 (вынесение x) | 2 | roots | +| quadratic | `quad-incomplete-c` | ax²=c (корень) | 2 | roots | +| quadratic | `quad-disc-clean` | ax²+bx+c=0 (дискриминант, чистый D) | 3 | roots | +| quadratic | `quad-trinomial-factor` | разложить трёхчлен | 2 | simplify | +| quadratic | `quad-find-b` | найти b по корню | 3 | compute | +| quadratic | `quad-count-roots` | сколько корней (знак D) | 2 | compute | +| quadratic | `quad-vertex-x` | вершина x₀=−b/2a | 2 | compute | +| quadratic | `quad-complete-square` | выделить полный квадрат | 3 | simplify | +| progressions | `prog-arith-sum` | сумма n членов (арифм.) | 2 | compute | +| progressions | `prog-arith-find-d` | найти d по двум членам | 2 | compute | +| progressions | `prog-arith-find-n` | каким по счёту идёт член | 3 | compute | +| progressions | `prog-arith-mean` | вставить среднее арифм. | 2 | compute | +| progressions | `prog-geom-find-q` | найти знаменатель q | 2 | compute | +| progressions | `prog-geom-mean` | геометрическое среднее | 3 | compute | +| progressions | `prog-geom-sum` | сумма n членов (геом.) | 3 | compute | +| progressions | `prog-arith-word` | ряды кресел / зарплата | 2 | compute | + +**Формулировки:** корни-как-пара (сумма/произведение по Виета), «составь приведённое ур-е по +корням» (simplify), классификация по знаку D, кратный корень (полный квадрат, 1 корень), числовой +сюжет (произведение последовательных), инверс a₁/n/q, сумма 1..n и 1+3+..+(2n−1), реальный геом. +сюжет (мяч/бактерии, q∈{2,3}). + +### Группа 5. Арифметика 5–6: НОД/НОК · Дроби · Десятичные · Отрицательные (19 генераторов) + +| Тема | id | Форма / условие | L | kind | +|---|---|---|---|---| +| gcd-lcm | `gcd-triple` | НОД трёх чисел | 2 | compute | +| gcd-lcm | `lcm-triple` | НОК трёх чисел | 3 | compute | +| gcd-lcm | `coprime-check` | взаимно простые? (1/0) | 2 | compute | +| gcd-lcm | `lcm-buses` | «снова вместе» (НОК-задача) | 2 | compute | +| fractions | `frac-reduce` | сократить дробь | 2 | compute | +| fractions | `frac-add-unlike` | сложение (разные знаменатели) | 3 | compute | +| fractions | `frac-mult` | умножение дробей | 2 | compute | +| fractions | `frac-compare` | сравнить дроби (код 1/2/0) | 2 | compute | +| fractions | `frac-of-whole-inverse` | число по его части | 3 | compute | +| fractions | `frac-to-decimal` | дробь → десятичная | 2 | compute | +| decimals | `dec-div` | деление десятичных | 3 | compute | +| decimals | `dec-round` | округление | 2 | compute | +| decimals | `dec-times-pow10` | ×/÷ на 10/100/1000 | 1 | compute | +| decimals | `dec-compare` | сравнить десятичные | 1 | compute | +| negatives | `neg-div` | деление (отрицательные) | 2 | compute | +| negatives | `neg-order-ops` | порядок действий со знаками | 3 | compute | +| negatives | `neg-abs` | модуль числа/выражения | 2 | compute | +| negatives | `neg-compare-line` | сравнение на коорд. прямой | 1 | compute | +| negatives | `neg-square` | квадрат отрицательного | 2 | compute | + +**Формулировки:** истина/ложь (1/0), «что больше» кодом (1/2/0), «вставь знак» (1/2/0), словесный +контекст (НОК-автобусы, остаток пирога), прикидка с округлением, смешанное число ↔ неправильная дробь. + +### Группа 6. Геометрия: Углы · Пифагор · Площади · Многоугольники · Подобие · Окружность (20 генераторов) + +Все используют систему **фигур-данных** (`figures.js`); где нужно — новый тип фигуры. + +| Тема | id | Форма / условие | L | Нужна фигура | +|---|---|---|---|---| +| g-angles | `ang-parallel-transversal` | параллельные + секущая | 2 | новый тип `parallel-lines` | +| g-angles | `ang-isosceles-base` | углы равнобедренного | 2 | `triangle-angles` (расш.) | +| g-angles | `ang-vertical-bisector` | вертикальные / биссектриса | 1 | новый `crossing-lines` | +| g-pyth | `pyth-perimeter` | периметр прям. треугольника | 3 | `right-triangle` | +| g-pyth | `pyth-distance` | расстояние между точками | 3 | новый `coord-points` | +| g-pyth | `pyth-rect-diagonal` | диагональ прямоугольника | 2 | `rectangle`+диагональ | +| g-pyth | `pyth-space-diagonal` | диагональ параллелепипеда | 3 | новый `box-3d` | +| g-area | `area-rect-inverse` | сторона по площади | 2 | `rectangle` (▢) | +| g-area | `area-l-shape` | площадь L-фигуры | 3 | новый `l-shape` | +| g-area | `area-sector` | площадь сектора | 3 | `circle-arc`+заливка | +| g-poly | `poly-diagonals` | число диагоналей | 2 | `regular-polygon`+диаг. | +| g-poly | `poly-find-n` | число сторон по углу | 3 | `regular-polygon` | +| g-poly | `poly-exterior-sum` | внешний угол правильного | 2 | `regular-polygon`+метка | +| g-sim | `sim-scale-factor` | коэффициент по сторонам | 2 | `two-similar` | +| g-sim | `sim-area-ratio` | отношение площадей (k²) | 3 | `two-similar` | +| g-sim | `sim-thales` | отрезок по т. Фалеса | 3 | новый `thales` | +| g-sim | `sim-map-scale` | масштаб карты | 2 | (без фигуры) | +| g-circle | `circ-inscribed-angle` | вписанный/центральный угол | 3 | `circle`+две хорды | +| g-circle | `circ-chord-pyth` | длина хорды через радиус | 3 | `circle`+хорда | +| g-circle | `circ-tangent-len` | длина касательной | 3 | `circle`+касательная | + +**Формулировки:** только-с-чертежа (L3-вариант любого углового), двухшаговая «погоня за углом» +(вертикальный→смежный; накрест→сумма треугольника), обратная «прямоугольный ли?» (1/0, +`rhs:'(a*a+b*b==c*c)'`), закрашенная область (круг в квадрате, π≈3,14), инверс «сколько сторон +по сумме углов», подобие через тени (дерево/столб), угол в полуокружности (Фалес). + +--- + +# ЧАСТЬ II — НОВЫЕ ФОРМАТЫ ЗАДАНИЙ (kinds) + +Дают наибольший прирост разнообразия условий — применимы ко всем темам. Каждый сохраняет +SimExpr-проверку. + +| # | Формат | Усилие | Суть реализации | +|---|---|---|---| +| P1 | `choice` (выбор) | M | 4 варианта; дистракторы из `analyzeMistake` (типовые ошибки) + случайные; правильный — текущий ответ | +| P2 | `verify` (верно/неверно) | M | утверждение → булева SimExpr (1/0); часть истинных, часть ложных | +| P3 | `findError` (найди ошибку) | M | портим ОДИН шаг `solution[]`; ученик указывает номер неверного шага | +| P4 | `fillBlank` (вставь пропуск) | M | равенство с ▢; ответ — число/выражение, проверка подстановкой/эквивалентностью | +| P5 | `estimate` (оценка) | S | ответ в допуске (band ±ε); для прикидки/π | +| P6 | `multi` (многошаговая) | M | несколько gated под-ответов в одной задаче (путь→время→…) | +| P7 | `order` (упорядочи) | M | расставить числа/шаги; идеально для отрицательных/дробей/десятичных | +| P8 | `match` (сопоставь) | L | два столбца: выражение↔форма, фигура↔площадь | +| P9 | `context` (бытовой сюжет) | S | тонкая обёртка существующих compute/solve в случайные сюжеты (данными) | +| P10 | `figureAsk` (с чертежа/таблицы) | M | ответ ЧИТАЕТСЯ с фигуры; + новый тип фигуры `table` | + +**Старт:** P9 (контекст-обёртки — почти бесплатно), P5 (estimate), затем P1/P4/P2 (высокая +ценность, средняя цена). P8 (match) — позже. + +--- + +# ЧАСТЬ III — ПЕДАГОГИКА / РЕПЕТИТОР + +| # | Что | Усилие | +|---|---|---| +| C1 | 3-уровневые подсказки (намёк → первый шаг → полное решение) из `solution[]` | S | +| C2 | Библиотека типовых ошибок по темам (`MISTAKES` data, расширяет `analyzeMistake`) | M | +| C3 | Режим «сначала образец» (worked example) → задача-близнец | M | +| C4 | Направляемые пропуски в шагах (валидатор — `checkStep`) | M | +| C5 | Мастерство с учётом помощи (флаг `assisted` → «освоено» = без подсказок) | M | +| C6 | Сократический «объясни мою ошибку» (вопрос вместо ответа, анти-чит) | M | +| C7 | Интервальное повторение МЕТОДА (мини-карточки «вспомни формулу») | L | + +--- + +# ЧАСТЬ IV — АДАПТИВНОСТЬ / МАСТЕРСТВО / ДИАГНОСТИКА + +| # | Что | Усилие | +|---|---|---| +| D5 | **Фундамент:** писать структурный `level` и `time_ms` в попытку (POST attempt) | S | +| D3 | Мастерство = серия верных на уровне 3 (а не на 1) | M | +| D7 | Авто-калибровка уровня по точности/времени последних попыток | M | +| D1 | Граф пререквизитов навыков + гейтинг (как `unlockStars` в Квантике) | M | +| D2 | Входная диагностика → персональный стартовый план | L | +| D6 | Подмешивание due-навыков (кривая забывания) в КАЖДУЮ сессию | M | +| D4 | «Повторить всё слабое» — кросс-темовый режим слабых навыков | M | +| D8 | Полоса мастерства на чипе навыка + аналитика глубины учителю | S | + +> Начинать с **D5** — без записи уровня/времени остальные пункты слепы. + +--- + +# ЧАСТЬ V — ВОВЛЕЧЕНИЕ + УЧИТЕЛЬ + +| # | Что | Усилие | +|---|---|---| +| E1 | XP/монеты за практику (хук в `submitAttempt`, reuse геймификации) | S | +| E2 | Практика в Дневной цели + календарь серий на странице | M | +| E3 | Достижения/бейджи (объёмы, мастерство, идеальная сессия) | M | +| E4 | Задания учителя + журнал (`practice_assignments`: темы/цель/дедлайн, трекинг) | L | +| E5 | Глубже аналитика класса (помастерство, слабые места, время, динамика) | M | +| E6 | Карта-созвездие навыков (reuse `QuantikMap`) с пререквизитами | L | +| E7 | Очередь ревью LLM-пула (одобрить/править/удалить черновики) | M | +| E8 | Конструктор генераторов на ВСЕ виды + шаринг/клон | L | + +> Учитывать **kill-switch геймификации** (`body.no-gamification`) для E1–E3/E6. + +--- + +# ЧАСТЬ VI — ОХВАТ ЦТ/ЦЭ (новые темы) + ТЕХНИКА/КАЧЕСТВО + +**Новые темы (к программе 9–11 и ЦТ):** + +| id | Тема | Усилие | Заметка | +|---|---|---|---| +| `g-func` | Функции: чтение графика, значение, область | M | новые kinds `graph-read`/`domain`; линейная/квадратичная | +| `roots` | Корни и иррациональности (7–9) | S | √ из точных квадратов; √(a²b)→a√b (simplify) | +| `logs` | Логарифмы и показательные (10–11) | M | log_a(aᵏ)=k, aˣ=aᵏ; целые логи | +| `trig` | Тригонометрия (9–11) | M | значения в особых углах (0/30/45/60/90) | +| `g-coord` | Координатная геометрия (9) | M | середина, расстояние (пифагоровы пары), прямая | + +**ЦТ-режим (B5, L):** формат А1–А10 / В1–В20, бланк ответов, таймер, тег `ct_code` со связью +с таксономией exam-prep (как `tag-exam-textbook`). + +**Техника/качество (сквозное):** + +| # | Что | Усилие | +|---|---|---| +| H1 | Ленивая загрузка KaTeX/Lucide, defer модулей тренажёра (first-paint) | S | +| H4 | **Закоммиченный** смоук: инстанс КАЖДОГО генератора × сидов + покрытие всех kinds | M | +| H3 | Телеметрия: труднейшие навыки, эффективность подсказок | M | +| A11y | ARIA, клавиатура, `prefers-reduced-motion`, озвучка формул | M | +| H2 | Офлайн/PWA для параметрики + очередь синка прогресса | L | + +--- + +# ЧАСТЬ VII — РЕКОМЕНДУЕМАЯ ПОСЛЕДОВАТЕЛЬНОСТЬ + +Сначала — то, что даёт максимум разнообразия при минимуме риска (чистые данные), затем форматы, +затем педагогика/адаптивность/вовлечение, затем охват ЦТ. + +1. **V4.1 — Контент-разнообразие волнами (★ главное по запросу).** Выкатить 103 генератора + группами (1→6) + формулировки, требующие ТОЛЬКО данных (инверс, пропущенный шаг, словесные, + «что больше», 1/0, счёт целых решений) + контекст-обёртки P9. Геометрия — с расширением + `figures.js` (новые типы: `parallel-lines`, `crossing-lines`, `coord-points`, `box-3d`, + `l-shape`, `thales`, таблица). Риск низкий — движок не меняется (кроме фигур). +2. **V4.2 — Новые форматы условий (kinds):** P9→P5→P1→P4→P2→P3→P6→P7→P10→P8 (движок + страница + смоуки). +3. **V4.3 — Педагогика:** C1→C2→C5→C3→C4→C6→C7. +4. **V4.4 — Адаптивность:** D5 (фундамент)→D3→D7→D6→D4→D8→D1→D2. +5. **V4.5 — Вовлечение + учитель:** E1→E2→E3→E5→E7→E4→E6→E8. +6. **V4.6 — Охват ЦТ:** `roots`→`g-coord`→`g-func`→`logs`→`trig`→ЦТ-режим (B5) + `ct_code`. +7. **Сквозное (каждую фазу):** H4 (закоммиченный смоук всех генераторов/kinds), затем H1/H3/A11y/H2. + +**Каждая фаза:** смоук движка (инстанс всех генераторов + самопроверка) + смоук страницы + +бэкенд-тесты (где есть API) + коммит/пуш; эмодзи/eval = 0; `lint:routes` baseline 0. + +--- + +## Приложение — где что в коде + +- Генераторы (данные): `frontend/js/trainer/generators.js` +- Движок/проверка/kinds: `frontend/js/trainer/_trainer_engine.js` +- Чертежи-данные: `frontend/js/trainer/figures.js` +- Умная сессия: `frontend/js/trainer/adaptive.js` +- Страница: `frontend/trainer.html` +- Прогресс/SR/пул: `backend/src/controllers/practiceController.js`, `routes/practice.js`, + мигр.081/082/083; конструктор — `customGeneratorController` + мигр.084. +- Предыдущие планы: `plans/ai-trainer/PLAN.md`, `ROADMAP_V2.md`, `ROADMAP_V3.md`. diff --git a/plans/ai-trainer/V4_GENERATOR_SPECS.md b/plans/ai-trainer/V4_GENERATOR_SPECS.md new file mode 100644 index 0000000..3933a7c --- /dev/null +++ b/plans/ai-trainer/V4_GENERATOR_SPECS.md @@ -0,0 +1,1325 @@ +# ИИ-Тренажёр v4 — детальные спецификации (build spec) + +Спутник к `ROADMAP_V4.md`: полные «корень-вперёд»-рецепты для 103 генераторов, формулировки и сквозные предложения. Сгенерировано анализом 11 агентов по реальному движку. + +> Инварианты: только SimExpr (без eval), без эмодзи, чистый ответ, зарезерв. имена t/w/h/pi/e/E/PI/tau. + +--- + +# Группа: linear-family + +## Текущий инвентарь и пробелы + +**linear-eq** +- lin-basic (L1): ax + b = c — one-step solve, root-forward via c=a*root+b +- lin-paren (L2): a(x + b) = c — divide by a then isolate; c=a*(root+b) +- lin-both-sides (L2): ax + b = cx + d — x on both sides; d=(a-c)*root+b, constraint c0, sign kept; bound=root, relOp '<' +- ineq-ge (L1): ax + b >= c — a>0, sign kept; relOp '>=' +- ineq-flip (L3): -ax + b < c — negative coefficient, sign FLIPS to '>' + +_Пробелы:_ Only single-step half-line inequalities. Missing: variable-on-both-sides inequality (ax+b < cx+d); inequality with a fraction/divide by constant ((ax+b)/c <= d); parentheses inequality a(x+b) > c; the OTHER two relation symbols at L2 (>, <= without flip), plus a flip with >= ; double inequality a < bx+c < d and |x|=2, elimination + +_Пробелы:_ Both are 2x2 by elimination with the SAME structural template. Missing: a system meant to be solved by SUBSTITUTION (one eq already y=mx+k); a system where one equation is x+y=S and the other x-y=D (sum/difference, classic); a 2x2 with a coefficient of 1 on one variable (cleaner substitution); a word-problem system (two unknowns from a sentence); a 3-unknown teaser (engine supports answerVars length 3). No 'find the parameter so the system has a given solution' inverse framing. + +## Новые генераторы + +### `lin-both-frac` — ax + b = cx + d, дробный ответ → целый (тема linear-eq, L2, solve) +- **Форма:** Variable on both sides combined with a fraction step: (a/k)x + b = (c/k)x + d, but kept clean. Simpler concrete: ax + b = cx + d where after collecting you DIVIDE — extend lin-both-sides to also require a brackets-free fraction division producing integer. +- **Пример условия:** 7x − 5 = 3x + 11 → 4x = 16 → x = 4 +- **Корень-вперёд:** pick root (nonzero int) and a>c>=1; derive d=(a-c)*root+b so RHS const is exact; answer=root is integer by construction. To force a genuine DIVISION (not trivial), require (a-c)>1 via constraint 'a - c >= 2'. Distinct from lin-both-sides only by that constraint guaranteeing a non-unit divisor. + +### `lin-x-denom` — a/(x + b) = c (x в знаменателе) (тема linear-eq, L3, solve) +- **Форма:** x in the denominator: a/(x+b) = c. lhs:'{a}/(x + {b})', rhs:'{c}'. +- **Пример условия:** 12/(x + 1) = 3 → x + 1 = 4 → x = 3 +- **Корень-вперёд:** pick root (root != -b so denom nonzero), pick c (2..6) and b; derive a = c*(root + b). Then a/(root+b) = c exactly. answer=root integer. require 'root + b != 0'. Self-check substitutes root into lhs=rhs and matches (engine verifyRoot). + +### `lin-k-over-x` — k/x = c (обратная пропорция → линейный) (тема linear-eq, L2, solve) +- **Форма:** k/x = c. lhs:'{k}/x', rhs:'{c}'. +- **Пример условия:** 20/x = 5 → x = 4 +- **Корень-вперёд:** pick root (nonzero int) and c (nonzero int), derive k = c*root. Then k/root = c exactly, answer=root integer. require 'root != 0'. Trivial-clean because k is the product. + +### `lin-abs` — |ax + b| = c (два корня) (тема linear-eq, L3, roots) +- **Форма:** Absolute value: |ax+b|=c. lhs:'abs({a}*x + {b})', rhs:'{c}'. Two roots from ax+b=c and ax+b=-c. +- **Пример условия:** |2x − 3| = 5 → 2x−3=5 (x=4) or 2x−3=−5 (x=−1) +- **Корень-вперёд:** ROOT-FORWARD on BOTH roots: pick the inner root rin (so ax+b=c at x=rin) and a, b → c = abs(a*rin + b); the two answers are r1=rin and r2=(-c - b)/a. To keep BOTH integer: pick a, then pick r1 and r2 of OPPOSITE parity-safe values directly and derive b,c: choose a and two roots r1 0'. answers:['r1','r2'] both integer; engine roots-kind verifies each by substitution into abs(...)=c. exprToLatex renders abs as \left|...\right|. + +### `lin-frac-eq-frac` — (ax + b)/c = (dx + e)/f (дробь = дробь) (тема linear-eq, L3, solve) +- **Форма:** Two linear fractions equal: cross-multiply. lhs:'({a}*x + {b})/{c}', rhs:'({d}*x + {e})/{f}'. +- **Пример условия:** (2x + 1)/3 = (x + 4)/2 → 2(2x+1)=3(x+4) → 4x+2=3x+12 → x=10 +- **Корень-вперёд:** pick root, a,c,d,f and b; derive e so that f*(a*root+b) = c*(d*root+e), i.e. e = (f*(a*root+b) - c*d*root)/c. require 'mod(f*(a*root+b) - c*d*root, c) == 0' (e integer) and constraint a*f != c*d (genuine linear, unique root) and root != 0. Self-check: substitution gives equal fractions exactly. + +### `lin-nested-paren` — a(b(x + c) + d) = e (вложенные скобки) (тема linear-eq, L3, solve) +- **Форма:** Nested brackets. lhs:'{a}*({b}*(x + {c}) + {d})', rhs:'{e}'. +- **Пример условия:** 2(3(x + 1) + 4) = 26 → 3(x+1)+4 = 13 → 3(x+1)=9 → x+1=3 → x=2 +- **Корень-вперёд:** pick root, a,b,c,d; derive e = a*(b*(root+c)+d). Everything integer, e exact. answer=root. require root != 0. Solution steps unwrap outer-then-inner bracket. + +### `lin-literal` — ax = b·k − c·x с буквенным коэффициентом (параметрический) (тема linear-eq, L3, solve) +- **Форма:** Literal/parametric: solve for x with a LETTER coefficient shown but a numeric instance generated. Concrete generated instance: a·x + p·m = m·x (m is a 'parameter' shown as a number each round but framed as 'выразите x'). Render: '{a}x + {p}*{m} = {m}*x'. +- **Пример условия:** 5x + 2·3 = 3·x → wait keep clean: 3x + 12 = 7x → x = 3 (parameter m=7 shown) +- **Корень-вперёд:** Treat as ax + b = mx with m the 'parameter'. pick root, a, m with constraint 'm != a' and 'm - a divides b cleanly': pick root and (m-a), set b=(m-a)*root, then m=a+(m-a). derive b=(m-a)*root. answer=root integer. The didactic frame ('коэффициент m задан') teaches isolating x with a symbolic-looking coefficient while staying numerically clean. + +### `ineq-both-sides` — ax + b < cx + d (переменная с двух сторон) (тема inequalities, L2, inequality) +- **Форма:** Variable both sides, sign kept (a>c). lhs:'{a}*x + {b}', rhs:'{c}*x + {d}', dispOp '<', relOp '<', bound 'root'. +- **Пример условия:** 5x + 1 < 2x + 10 → 3x < 9 → x < 3 +- **Корень-вперёд:** pick a>c (constraint 'a - c >= 1'), pick the boundary root and b, derive d = (a-c)*root + b. Then (a-c)x < (d-b) i.e. x < root. answerRel {op:'<', bound:root}. Self-check _origIneqHolds passes (single bound). Choose a-c>0 so sign is kept; bound=root integer. + +### `ineq-both-flip` — ax + b >= cx + d, c>a (смена знака при переносе) (тема inequalities, L3, inequality) +- **Форма:** Variable both sides where collecting x gives a NEGATIVE coefficient → flip. lhs:'{a}*x + {b}', rhs:'{c}*x + {d}', dispOp '>=', relOp '<=', bound 'root'. +- **Пример условия:** 2x + 9 >= 5x + 3 → −3x >= −6 → x <= 2 +- **Корень-вперёд:** pick c>a (constraint 'c - a >= 1'), boundary root, b; derive d = (a-c)*root + b (a-c<0). Original a x + b >= c x + d ⇔ (a-c)x >= d-b ⇔ x <= root after dividing by negative (a-c). relOp flips to '<='. answerRel {op:'<=', bound:root}. Single-bound self-check valid. + +### `ineq-paren` — a(x + b) > c (скобка в неравенстве) (тема inequalities, L2, inequality) +- **Форма:** Parentheses inequality. lhs:'{a}*(x + {b})', rhs:'{c}', dispOp '>', relOp '>', bound 'root'. +- **Пример условия:** 3(x + 2) > 15 → x + 2 > 5 → x > 3 +- **Корень-вперёд:** pick a>0, boundary root, b; derive c = a*(root + b). a(x+b)>c ⇔ x+b > c/a = root+b ⇔ x > root. answerRel {op:'>', bound:root}. a positive ⇒ no flip; bound integer. + +### `ineq-count-int` — Сколько целых решений у a < x <= b? / наименьшее целое (тема inequalities, L2, compute) +- **Форма:** Double-bound framing routed through COMPUTE (self-checkable) instead of relation-answer: prompt gives a compound condition, answer is an integer count or the smallest/largest integer. lhs:'x', rhs:, display:text. +- **Пример условия:** Сколько целых x удовлетворяют −3 < x <= 4 ? Ответ: 7. (или: наименьшее целое x с 2x+1 > 9 → x>4 → ответ 5) +- **Корень-вперёд:** ROOT-FORWARD on the answer: pick integer endpoints lo0, derive c=a*root+ (a) so x > root ⇒ smallest int = root+1, answer:'root + 1'. Avoids the single-{op,bound} limitation of inequality-kind while still teaching double-bound / sign reasoning. Fully integer, fully self-checkable as compute. + +### `sys-subst` — Система: одно уравнение уже разрешено (метод подстановки) (тема systems, L2, system) +- **Форма:** Substitution-ready: first equation is y = m*x + k, second is a2 x + b2 y = c2. eqs:[{lhs:'y', rhs:'{m}*x + {k}'},{lhs:'{a2}*x + {b2}*y', rhs:'{c2}'}]. +- **Пример условия:** y = 2x − 1 ; 3x + y = 9 → 3x + 2x − 1 = 9 → x = 2, y = 3 +- **Корень-вперёд:** pick solution (sx, sy) and m, then k = sy - m*sx (so first eq holds exactly); pick a2,b2 (det check: a2 + b2*m != 0), derive c2 = a2*sx + b2*sy. answers {x:'sx', y:'sy'} integer by construction. Distinct pedagogy (substitution) vs existing elimination templates. Self-check: pair satisfies both eqs. + +### `sys-sum-diff` — Система x + y = S, x − y = D (сумма и разность) (тема systems, L1, system) +- **Форма:** Classic sum/difference. eqs:[{lhs:'x + y', rhs:'{S}'},{lhs:'x - y', rhs:'{D}'}]. +- **Пример условия:** x + y = 10 ; x − y = 4 → x = 7, y = 3 +- **Корень-вперёд:** pick sx, sy; derive S = sx + sy, D = sx - sy. answers {x:'sx', y:'sy'}. Always integer; teaches add/subtract-equations elimination in its purest form. Determinant always 2 (nonzero). + +### `sys-3x3` — Система 3×3 (вводный тизер) (тема systems, L3, system) +- **Форма:** Three unknowns, engine supports answerVars length 3. eqs: three linear eqs in x,y,z; answers {x,y,z}; answerVars ['x','y','z']. +- **Пример условия:** x + y + z = 6 ; x − y = 1 ; y + z = 5 → x=3? choose clean: x=1,y=2,z=3 +- **Корень-вперёд:** pick sx,sy,sz; build three eqs with simple coefficients (e.g. x+y+z=S, x+y=Sxy, y+z=Syz) and derive RHS by substituting (sx,sy,sz): S=sx+sy+sz, Sxy=sx+sy, Syz=sy+sz. Coefficient matrix chosen invertible (constraint or fixed template). answers all integer. Engine builds \begin{cases} with 3 rows via exprToLatex and self-checks the triple against every eq — no engine change needed (answerVars already generic). + +### `sys-word` — Текстовая задача на систему (два неизвестных) (тема systems, L2, system) +- **Форма:** Word problem yielding a 2x2 system; framed in display, solved as system. e.g. 'В классе мальчиков и девочек всего N, девочек на D больше'. eqs:[{lhs:'x + y', rhs:'{N}'},{lhs:'y - x', rhs:'{D}'}], with figurePrompt/display text. +- **Пример условия:** Всего 30 учеников, девочек на 6 больше мальчиков. Сколько каждого? x=12 (мальчики), y=18 (девочки) +- **Корень-вперёд:** pick sx (boys), sy (girls) with sy>sx; derive N=sx+sy, D=sy-sx. answers {x:'sx', y:'sy'} integer. The display sentence is the new framing; underlying system self-checks identically to sys-sum-diff. Connects linear systems to the 'applied' framing missing from this topic. + +## Новые формулировки условий + +- **Inverse — 'find the coefficient given the root' (восстанови коэффициент). Show an equation with ONE blank/unknown coefficient and a stated root; student finds the coefficient. kind:'compute' (answer is the coefficient, not x).** + - Пример: При каком a корень уравнения a·x − 6 = 9 равен x = 5? Ответ: a = 3. + - Заметка: Root-forward: pick a and root, derive the shown constants (here c=a*root-? ) so the coefficient is the integer answer. answer:'a'. Self-checkable as compute (lhs:'x' is wrong here; instead lhs:'a', rhs:numeric formula). Teaches that the parameter is recoverable from a known root — a genuine ЦТ inverse skill absent today. + +- **Construct — 'build an equation with root R' (составь уравнение). Give a target root and a required FORM, ask the student to produce constants; auto-graded by checking their equation has that root (uses checkStudentAnswer/verifyRoot machinery). Could ship first as a guided/compute variant: 'дополни уравнение 2x + ? = 10 так, чтобы корень был 3'.** + - Пример: Дополни: 2x + ▢ = 10, чтобы корнем было x = 3. Ответ: ▢ = 4. + - Заметка: Root-forward trivially: the blank = c - a*root. answer is the blank integer. compute-kind. Inverse of every existing solve generator — high pedagogical value, no engine change. + +- **Missing-step / guided — present a partially worked solution with ONE step blanked; student fills the result of that step (e.g. the value after moving b across). Leverages existing solution[] steps as data.** + - Пример: Шаг решения 4x + 7 = 19: после переноса 7 вправо получаем 4x = ▢. Ответ: 12. + - Заметка: compute-kind; answer is the intermediate value (c - b), derived root-forward from the same picks as lin-basic. Reuses the step texts already authored; teaches the procedure, not just the final root. + +- **Integer-solution counting / extremal integer (double-bound and |x| reasoning WITHOUT needing relation-answer support). Routes the unsupported double-inequality and |x| 13 → x > 4 → 5. + - Заметка: The current answerRel is a single {op,bound}; double inequalities and unions cannot be expressed as inequality-kind without an engine change. Compute-kind keeps the rich condition in the prompt and an integer answer — same skill, fully verifiable today (see sim id ineq-count-int). + +- **Parameter-of-system inverse — 'find the value of the parameter so the system has the given solution / so it has no solution'. Ship the 'given solution' variant as compute (answer = parameter); 'no solution' as a teaser noting the determinant condition.** + - Пример: При каком значении k система { x + 2y = 5 ; 3x + k·y = 9 } имеет решение x = 1, y = 2? Найди k. (подставить: 3 + 2k = 9 → k = 3). + - Заметка: Root-forward: pick (sx,sy) and the missing parameter k, derive the constant that makes the second equation hold (c2 = a2*sx + k*sy). answer:'k' integer. compute-kind, self-checkable. Inverse framing absent from systems today; connects to determinant/uniqueness intuition for ЦТ. + +## Заметки реализации + +Verified against the REAL engine: roots-kind verifies EACH listed root by substituting into lhs=rhs (lines 420-421, 659-669), so |ax+b|=c becomes a clean 2-root generator by writing lhs:'abs({a}*x+{b})' and answers:[r1,r2] (root-forward: pick both roots, derive c). exprToLatex already renders abs() as \left|...\right| and sqrt{} (lines 205-206). inequality-kind self-checks via _origIneqHolds with a single point inside/outside the bound (lines 407-412) — so it only supports a SINGLE half-line answerRel {op,bound}; double inequalities a0. val=abase*(100-p) clean integer. rhs:'{a}*(100-{p})/100'. Distinct from app-discount by abstract framing ('число', not 'товар/скидка') and round a. + +### `pct-change` — На сколько процентов изменилось (тема percents, L3, compute) +- **Форма:** Было a, стало b — на сколько % выросло/упало? p=100*(b-a)/a +- **Пример условия:** Цена выросла с {a} руб до {b} руб. На сколько процентов выросла цена? +- **Корень-вперёд:** Pick base a=abase*10 (abase=[2..12]) and target percent p=pidx*5 (pidx=[1..10]) FIRST; derive b=a*(100+p)/100 = abase*(100+p)/10 — require 'mod(abase*(100+p),10)==0' (always true since (100+p) multiple of 5 and abase*5... ensure b integer; simplest: a=abase*20 → b=abase*(100+p)/5 integer). Answer p is the picked integer. rhs:'100*({b}-{a})/{a}' returns p exactly. Clean because p chosen first. +- **Фигура:** two bars a vs b + +### `pct-simple-interest` — Простые проценты (вклад) (тема percents, L2, compute) +- **Форма:** Вклад S под p% годовых на n лет — итог? val=S*(100+p*n)/100 +- **Пример условия:** В банк положили {S} руб под {p}% годовых (простые проценты). Какая сумма будет на счёте через {n} года/лет? +- **Корень-вперёд:** Pick S=sbase*100 (sbase=[1..9]), p=pidx*5 (pidx=[1..6]), n=[1..4] FIRST. Interest = S*p*n/100 = sbase*p*n (clean), total=S+interest = sbase*(100+p*n) clean integer (S divisible by 100). rhs:'{S}*(100+{p}*{n})/100'. No rounding. + +### `pct-compound-2y` — Сложные проценты (2 года) (тема percents, L3, compute) +- **Форма:** S под p% годовых, капитализация, 2 года — итог; val=S*(100+p)^2/10000 +- **Пример условия:** Вклад {S} руб под {p}% годовых с ежегодной капитализацией. Какая сумма будет через 2 года? +- **Корень-вперёд:** Pick p=pidx*10 (pidx=[1..4] → 10,20,30,40 so (100+p)/100 = 1.1,1.2,… and square is exact: 1.21,1.44,1.69,1.96) and S=sbase*10000 (sbase=[1..3]) FIRST. val=S*(100+p)^2/10000 = sbase*(100+p)^2 — clean integer because S divisible by 10000 and (100+p)^2 integer. rhs:'{S}*(100+{p})^2/10000'. Root-forward picks p so the squared factor stays exact. + +### `pct-restore-before` — Найти исходное до изменения (тема percents, L3, compute) +- **Форма:** После роста на p% стало b — найди исходное a; a=b*100/(100+p) +- **Пример условия:** После повышения на {p}% цена стала {b} руб. Какой была цена до повышения (в рублях)? +- **Корень-вперёд:** Pick original a=abase*100, p=pidx*5 FIRST; derive b=a*(100+p)/100=abase*(100+p) (clean), answer a is the picked integer. rhs:'{b}*100/(100+{p})' returns a exactly. The classic reverse-percent trap (students wrongly subtract p% of b); root-forward keeps a clean. + +### `app-meet` — Встречное движение (тема applied, L2, compute) +- **Форма:** Два тела навстречу, расстояние S, скорости u и v — время до встречи; t=S/(u+v) +- **Пример условия:** Из двух городов навстречу выехали машины со скоростями {u} и {v} км/ч. Расстояние между городами {S} км. Через сколько часов они встретятся? +- **Корень-вперёд:** Pick u=[20..70], v=[20..70], and meeting time t=[2..6] FIRST; derive S=(u+v)*t (clean integer). Answer=t. rhs:'{S}/({u}+{v})' = t exactly. Root-forward via t guarantees S divisible by (u+v). +- **Фигура:** two arrows pointing toward each other along a segment '{S} км' + +### `app-overtake` — Движение вдогонку (тема applied, L3, compute) +- **Форма:** Отрыв d, скорости ut' constraint + 'mod(a*t,a-t)==0'). Then val=a*b/(a+b)=t by construction. rhs:'{a}*{b}/({a}+{b})' returns t exactly. Resampling finds clean (a,b,t) triples (e.g. a=6,b=12,t=4; a=4,b=12,t=3). + +### `app-mix-blend` — Смешивание двух растворов (тема applied, L3, compute) +- **Форма:** m1 кг p1% + m2 кг p2% — какова концентрация смеси? p=(m1*p1+m2*p2)/(m1+m2) +- **Пример условия:** Смешали {m1} кг раствора {p1}% и {m2} кг раствора {p2}%. Какова концентрация (в %) полученной смеси? +- **Корень-вперёд:** Pick target concentration p=[10..60] FIRST, then masses m1,m2=[1..9] and one component p1=[5..p-1]; derive p2 so weighted average = p: p2=(p*(m1+m2)-m1*p1)/m2 with require 'mod(p*(m1+m2)-m1*p1,m2)==0' and 'p2>0 && p2<=100'. Answer p is the picked clean integer. rhs:'({m1}*{p1}+{m2}*{p2})/({m1}+{m2})' = p exactly. +- **Фигура:** two beakers → one beaker, labels p1%, p2%, ?% + +### `app-profit-pct` — Прибыль в процентах (тема applied, L3, compute) +- **Форма:** Купили за c, продали за s — прибыль в %? p=100*(s-c)/c +- **Пример условия:** Товар купили за {c} руб, а продали за {s} руб. Сколько процентов прибыли получили? +- **Корень-вперёд:** Pick cost c=cbase*20 and profit-percent p=pidx*5 FIRST; derive s=c*(100+p)/100=cbase*(100+p)/5 → with c=cbase*20, s=cbase*4*(100+p)/... ensure integer: use c=cbase*100 → s=cbase*(100+p) clean. Answer p picked. rhs:'100*({s}-{c})/{c}' = p exactly. Clean by choosing p and c=multiple of 100. + +## Новые формулировки условий + +- **«Что больше?» — сравнение двух процентных операций** + - Пример: Что больше: 30% от 80 или 40% от 50? (compute, lhs:'x' rhs:'(30*80/100) - (40*50/100)'; answer is the signed difference, or kind:compute asking 'на сколько больше'). Root-forward: pick both products as clean integers (24 vs 20 → diff 4). + - Заметка: Forces conceptual comparison, not just one calc. Keep both terms clean integers via multiples-of-5 percents and multiples-of-10 bases. + +- **Табличные данные («прочитай таблицу»)** + - Пример: Display a 2-row mini-table (магазин А: 6 ручек 18 руб; магазин Б: 4 ручки 16 руб) and ask 'где ручка дешевле и на сколько копеек?'. Reuse figure-as-data: a new figure type 'table' in figures.js, or render table inside display text. Root-forward: pick unit prices first so per-unit cost is clean, totals = unit*count. + - Заметка: New 'which is cheaper per unit' family. Needs either a tiny table figure spec or plain-text rows; answer = integer difference in price-per-unit. + +- **Многошаговая цепочка (скидка → налог)** + - Пример: Товар {price} руб. Сначала скидка {d}%, затем к новой цене добавили {t}% (например доставка). Итоговая цена? rhs:'{price}*(100-{d})/100*(100+{t})/100'. Root-forward: price=pbase*100, d and t multiples of 10 → after-discount divisible by 100, final clean integer. + - Заметка: Teaches that successive percents don't add. Two stacked factors; pick all bases as multiples of 100/percents as multiples of 10 to keep every intermediate clean. + +- **Процентные пункты vs проценты** + - Пример: Доля выросла с {a}% до {b}% — на сколько процентных ПУНКТОВ выросла? (answer b-a) versus на сколько ПРОЦЕНТОВ (answer 100*(b-a)/a). Two linked compute generators with explicit wording. + - Заметка: Common exam trap. Root-forward trivial: pick a,b as integers; пункты answer=b-a (integer); процентов variant choose a|‌(b-a)*100 to stay integer (a divides 100*(b-a)). + +- **Оценка/прикидка («примерно сколько»)** + - Пример: Estimation framing on direct proportion: 'За 3 кг яблок 12 руб. Примерно сколько за 7 кг?' but with values engineered so the EXACT answer is still clean (28), wording just adds estimation context. Could accept a small tolerance band via existing numeric tolerance. + - Заметка: Adds real-world 'прикидка' phrasing without breaking exact self-check; the engine already tolerates near-canonical numeric answers, but keep the true answer an exact clean integer so grading stays deterministic. + +- **Средняя скорость кругового рейса** + - Пример: Туда {S} км со скоростью {v1}, обратно с {v2}. Средняя скорость всего пути? v_avg=2*v1*v2/(v1+v2). Root-forward: pick v_avg and a clean (v1,v2) harmonic-mean pair (e.g. 40 и 60 → 48; 30 и 60 → 40) via require 'mod(2*v1*v2,v1+v2)==0'. + - Заметка: Classic 'why not just average' trap. The harmonic-mean require gates to clean integer triples; S cancels out so it can be any value. + +## Заметки реализации + +Read all 4 real files. Engine confirms: kind:'compute' self-checks the student answer numerically against lhs='x'/rhs=formula via verifyRoot (tolerance EPS 1e-7 + nearCanonical 1e-6), and integerAnswer:true RESAMPLES until the answer is integer — so every 'derive answer first, build inputs around it' (root-forward) proposal above is guaranteed clean and deterministically gradable. Decimal answers ARE accepted (SimExpr.compile of the typed string, numeric compare), so compound-interest (1.21× etc.) works, but I engineered S as a multiple of 100/10000 so even those land on clean INTEGERS — safest for typing. RESERVED param names t,w,h,pi,e,E,PI,tau: I deliberately avoided them as new pick names EXCEPT where the existing convention already uses t as a free multiplier in proportions/motion (prop-x-right, app-move-* already use 't' in pick) — note that 't' is allowed as a PICK name here because the engine samples pick into a fresh env and only SimExpr's CONSTANTS clash is for the literal identifier; existing generators prove 't' as a pick works. For brand-new generators I used u,v,c,d,e,g,m1,m2,p,n,k etc. (avoid t/w/h to be safe; app-meet/app-overtake use t as the picked ROOT time, matching existing app-move-time precedent). LEVELS map: each new id must get a 1..3 entry (difficulty field gives the intended level). 'require'/'constraint' are SimExpr booleans evaluated post-derive/pre-derive respectively; I used 'mod(...,k)==0' divisibility gates (mod is in SimExpr FUNCTIONS) for the work/mixture/harmonic-mean generators where cleanliness needs a resample gate. solution[] steps and display text must use {param} placeholders and be escaped; figures are optional data specs (figures.js currently has 13 geometry types — a 'table'/'bar' figure would be a small addition for the table-data framing, otherwise render rows in display text). No eval, no emoji (inline SVG .ic only) in any proposal. Implementation order suggestion: percents increase/decrease/change/simple-interest + proportions direct/inverse/scale/share are L1-L2 quick wins; compound-interest, work-joint, mix-blend, overtake, harmonic average-speed are the L3 'real ЦТ' set needing the divisibility require gates. + +--- + +# Группа: expressions + +## Текущий инвентарь и пробелы + +**simplify** +- simp-like (lvl1): a*x + b*x → (a+b)x [collect like terms, one var] +- simp-expand (lvl2): a*(x + b) → ax + ab [single distribute] + +_Пробелы:_ Only 2 generators. Missing: like-terms with MULTIPLE variables, like-terms with constant term, distribute-then-combine (two brackets / bracket + free term), subtraction of a bracket (sign trap), factor out a common multiplier (reverse of expand), factor by grouping, negative leading coefficient, fraction-of-powers simplification belongs to powers but a/b·x style also missing. No equivalence-checked multi-var item at all. No 'fill-the-blank', 'find-the-error', 'recognize-formula' framings anywhere in the group. + +**powers** +- pow-eval (lvl1, compute): compute a^n (n=2..3), numeric answer +- pow-mult (lvl2, simplify): x^a · x^b → x^(a+b) [product rule] +- pow-pow (lvl3, simplify): (x^a)^b → x^(ab) [power of a power] + +_Пробелы:_ No quotient rule x^a/x^b → x^(a−b). No power-of-product (xy)^n or (a·x)^n → a^n·x^n. No negative/zero exponent. No standard (scientific) form a·10^n. No simplify a FRACTION of powers (x^a·x^b)/x^c. No mixed-base numeric power-law evaluation (e.g. 2^3·2^2). No 'find-the-error' on a wrong exponent-law application. No recognize-which-law framing. + +**formulas** +- sq-sum (lvl2, simplify): (x+a)^2 → x^2 + 2ax + a^2 +- sq-diff (lvl2, simplify): (x−a)^2 → x^2 − 2ax + a^2 +- diff-sq (lvl3, simplify): (x−a)(x+a) → x^2 − a^2 + +_Пробелы:_ All three only FORWARD (expand). No BACKWARD/recognize direction (factor x^2−a^2 → (x−a)(x+a); recognize a perfect-square trinomial → (x±a)^2). No leading-coefficient variant ((bx+a)^2, (bx−a)(bx+a)). No (a±b)^3 cube formulas. No sum/difference of cubes a^3±b^3. No two-variable squares (x+y)^2. No 'fill-the-blank in identity' (… = x^2 + 6x + 9). No 'find-the-error' on a sloppy expansion (e.g. (x+a)^2 = x^2 + a^2). No evaluate-by-substitution use of a formula. + +## Новые генераторы + +### `simp-like-multivar` — Привести подобные (две буквы) (тема simplify, L2, simplify) +- **Форма:** a*x + b*y + c*x + d*y → (a+c)x + (b+d)y +- **Пример условия:** pick:{a:[2,7],b:[2,7],c:[1,6],d:[1,6]}; derive:{sx:'a+c', sy:'b+d'}; srcExpr:'{a}*x + {b}*y + {c}*x + {d}*y'; answerExpr:'{sx}*x + {sy}*y'; answerVars:['x','y'] +- **Корень-вперёд:** No root needed — sums sx=a+c, sy=b+d are integers by construction, so the canonical answerExpr has clean integer coefficients. Equivalence self-check over x,y always passes because answerExpr is literally the sum of like terms. + +### `simp-like-const` — Привести подобные с числом (тема simplify, L2, simplify) +- **Форма:** a*x + b + c*x + d → (a+c)x + (b+d) +- **Пример условия:** pick:{a:[2,8],c:[1,7],b:[1,9],d:[1,9]}; derive:{sx:'a+c', sc:'b+d'}; srcExpr:'{a}*x + {b} + {c}*x + {d}'; answerExpr:'{sx}*x + {sc}'; answerVars:['x'] +- **Корень-вперёд:** Coefficient sum sx=a+c and constant sum sc=b+d are integers, so answerExpr is clean. Self-check samples x and confirms src ≡ answer. + +### `simp-sub-bracket` — Вычесть скобку (знаки) (тема simplify, L3, simplify) +- **Форма:** a*x + b − (c*x + d) → (a−c)x + (b−d) +- **Пример условия:** pick:{a:[4,9],c:[1,3],b:[5,12],d:[1,4]}; constraint:'c < a && d < b'; derive:{sx:'a-c', sc:'b-d'}; srcExpr:'{a}*x + {b} - ({c}*x + {d})'; answerExpr:'{sx}*x + {sc}'; answerVars:['x'] +- **Корень-вперёд:** Choosing c b'; derive:{d:'a-b'}; srcExpr:'x^{a}/x^{b}'; answerExpr:'x^{d}'; answerVars:['x'] +- **Корень-вперёд:** a>b guarantees a positive integer exponent d=a−b, so the simplified power x^d is clean (no negative/fractional exponent). Equivalence sampling over nonzero x confirms the quotient law. + +### `pow-product-base` — Степень произведения (тема powers, L2, simplify) +- **Форма:** (a*x)^n → a^n * x^n +- **Пример условия:** pick:{a:[2,4],n:[2,3]}; derive:{an:'a^n'}; srcExpr:'({a}*x)^{n}'; answerExpr:'{an}*x^{n}'; answerVars:['x'] +- **Корень-вперёд:** an=a^n is precomputed to an exact integer (a≤4,n≤3 → ≤64), so the distributed answer a^n·x^n is clean. Equivalence over x validates the power-of-product law. + +### `pow-frac-combine` — Дробь степеней (тема powers, L3, simplify) +- **Форма:** (x^a * x^b) / x^c → x^(a+b−c) (combine then reduce) +- **Пример условия:** pick:{a:[2,5],b:[2,5],c:[1,4]}; require:'a+b-c >= 1'; derive:{d:'a+b-c'}; srcExpr:'(x^{a}*x^{b})/x^{c}'; answerExpr:'x^{d}'; answerVars:['x'] +- **Корень-вперёд:** require a+b−c≥1 forces a positive integer result exponent d, so x^d is clean (no negative/zero exponent). Two-step law application (multiply then divide) raises structural level; equivalence over nonzero x passes by construction. + +### `pow-numeric-laws` — Степени одного основания (число) (тема powers, L2, compute) +- **Форма:** b^a · b^c → b^(a+c), give the NUMBER +- **Пример условия:** pick:{b:[2,3],a:[2,3],c:[1,2]}; derive:{val:'b^(a+c)'}; lhs:'x'; rhs:'{b}^{a}*{b}^{c}'; display:'Вычислите {b}^{a} · {b}^{c} (запишите числом).'; answer:'val'; integerAnswer:true +- **Корень-вперёд:** Small base/exponents keep b^(a+c) ≤ 3^5=243, a clean integer; the compute self-check substitutes and matches. Teaches that same-base product adds exponents while the final value is a typable integer. + +### `pow-standard-form` — Стандартный вид числа (тема powers, L2, compute) +- **Форма:** a*10^n given as plain number → student writes the number (or reverse) +- **Пример условия:** pick:{a:[2,9],n:[2,4]}; derive:{val:'a*10^n'}; lhs:'x'; rhs:'{a}*10^{n}'; display:'Запишите число {a}·10^{n} в обычном виде.'; answer:'val'; integerAnswer:true +- **Корень-вперёд:** a·10^n is exactly an integer (a≤9, n≤4 → ≤90000), so the plain-number answer is clean and typable. Compute substitution check passes. Introduces scientific/standard form, a ЦТ staple, without fractional results. + +## Новые формулировки условий + +- **Fill-the-blank in an identity (kind:compute, numeric blank). Show a partial identity with a missing coefficient or constant and ask the student for the missing NUMBER. lhs:'x'/rhs: verifies by substitution, so the answer is always a clean integer.** + - Пример: id:'fmt-blank-square' — display:'Вставьте число: (x + {a})² = x² + {a2x}x + ?'; pick:{a:[1,9]}; derive:{a2:'a*a', a2x:'2*a', val:'a*a'}; lhs:'x'; rhs:'{val}'; answer:'val'; integerAnswer:true. Root-forward: val=a² is the genuine constant term, an integer. + - Заметка: Reuses existing compute machinery — no engine change. The 'identity' is purely in the display string; correctness is the single numeric blank. + +- **Find-the-error → type the CORRECT value/expression. Present a deliberately wrong line in the display, ask the student to give the corrected result. As compute (numeric) it is bullet-proof; as simplify it accepts any equivalent corrected form.** + - Пример: id:'fmt-fix-square' (compute) — display:'Ученик записал (x + {a})² = x² + {a2}. Это неверно. Чему равен пропущенный средний коэффициент при x?'; pick:{a:[2,9]}; derive:{a2:'a*a', mid:'2*a'}; lhs:'x'; rhs:'{mid}'; answer:'mid'; integerAnswer:true. Root-forward: mid=2a is an integer; the 'wrong' line is just narrative text. + - Заметка: Texts must be escaped (engine does not escape display — page renderer does). Keep the wrong statement as plain prose so it cannot be mis-parsed as math. + +- **Recognize-the-formula (kind:simplify, BACKWARD). Give an expanded/expandable expression and ask the student to write it in FACTORED form; equivalence sampling accepts any algebraically equal factorization. Already embodied by diff-sq-factor / sq-trinom-factor / sum-cubes-factor above — generalize as a framing.** + - Пример: display:'Запишите в виде произведения (разложите на множители): x² − {a2}'; srcExpr:'x^2 - {a2}'; answerExpr:'(x - {a})*(x + {a})'; answerVars:['x']. Root-forward: a2=a² ensures an exact factorization exists. + - Заметка: The engine's _sampleEquiv makes 'recognize/factor' robust — the student is NOT forced into one canonical spelling, only into an equivalent expression. + +- **Recognize-which-law as evaluate-by-substitution (kind:compute). Instead of asking for the symbolic rule, ask for the resulting NUMERIC exponent or value, forcing the student to apply the correct law. Clean because the exponent/value is integer by construction.** + - Пример: id:'pow-which-law' — display:'На какое число надо возвести x, чтобы x^{a}·x^{b} = x^?'; pick:{a:[2,6],b:[2,6]}; derive:{val:'a+b'}; lhs:'x'; rhs:'{val}'; answer:'val'; integerAnswer:true. Root-forward: val=a+b integer. + - Заметка: Lets us test exponent-law understanding with a single typable integer, avoiding symbolic-answer ambiguity. Same pattern works for quotient (a−b, require a>b) and power-of-power (a*b). + +- **Evaluate-by-substitution of a simplified expression (kind:compute). Ask the student to first simplify mentally, then evaluate at a given x; checks one integer. Bridges simplify-skill with arithmetic and prevents 'memorize the canonical string' gaming.** + - Пример: id:'simp-eval-at' — display:'Упростите {a}x + {b}x и найдите значение при x = {x0}.'; pick:{a:[2,7],b:[2,7],x0:[1,5]}; derive:{s:'a+b', val:'(a+b)*x0'}; lhs:'x'; rhs:'{val}'; answer:'val'; integerAnswer:true. Root-forward: val=(a+b)*x0 integer. + - Заметка: Note the answerVar must NOT collide with the substitution number; use a distinct param name like x0 (x is the engine's unknown). Confirms simplify+substitute in one numeric check. + +## Заметки реализации + +Read all four target files (generators.js full, _trainer_engine.js full, figures.js scope via task brief, ROADMAP_V3). Engine facts that constrain proposals: (1) `simplify` kind shows `srcExpr`, checks student input ≡ `answerExpr` via `_sampleEquiv` over `answerVars` at fixed points _EQUIV_PTS=[-3.7,-1.3,0.5,2.1,4.9,-0.9,3.3,1.7] — so MULTI-VARIABLE (x,y) and any equivalent factored/expanded spelling are accepted (great for collect-like-terms-multivar, factor, and BACKWARD recognize generators). (2) `compute` shows the `display` prompt only and verifies a numeric answer by substitution — ideal for fill-blank / find-error / standard-form / evaluate-by-substitution framings. (3) There is NO dedicated 'fill-blank' or 'find-error' kind; both are implemented as compute (numeric blank) or simplify (corrected expression) with the framing living in the `display` text — zero engine changes needed. (4) `_sampleEquiv` samples NONZERO points, so quotient/fraction-of-powers (x in denominator) are safe. (5) Reserved param names t,w,h,pi,e,E,PI,tau — none of my proposals use them; I used x0 (not x/h) for the substitution example. (6) Every proposal is root-forward: outputs are precomputed integers (sums a+c, products a*b, squares a*a, cubes a*a*a, exponent arithmetic a+b / a-b / a*b) so both the engine self-check and the student-typed answer are exact and clean. Each new generator also needs a LEVELS entry (suggested difficulty given). FILE: G:\\Dev\\Тесты\\BQ-System\\frontend\\js\\trainer\\generators.js (add objects + LEVELS keys); no change to _trainer_engine.js required. 13 new generators (simplify 6, formulas 5, powers 5 — pow appears 5) plus 5 reusable condition framings across the group. All forward-direction gaps closed: collect-like-terms (multivar + const), distribute&combine, subtract-bracket sign trap, factor common multiplier, factor by grouping, difference of squares BOTH ways, perfect-square recognition (backward), (bx+a)^2 leading-coef, (a+b)^3 cube, sum-of-cubes, powers quotient/power-of-product/fraction-of-powers/numeric-laws/standard-form, plus fill-blank, find-error, recognize-formula, evaluate-by-substitution framings. + +--- + +# Группа: quadratic-progressions + +## Текущий инвентарь и пробелы + +**quadratic** +- quad-factored (order 1, kind:roots, level 3) — x²+bx+c=0 solved by Vieta: pick roots r1,r2; derive b=-(r1+r2), c=r1*r2; answers:['r1','r2'] +- quad-diff (order 2, kind:roots, level 2) — x²−a²=0 difference of squares; pick a; derive a2=a*a; answers:['a','-a'] + +_Пробелы:_ Only 2 generators, both reduced/monic forms with leading coeff 1. MISSING: full discriminant (clean D), incomplete forms ax²+bx=0 and ax²=c, leading coefficient a≠1, complete-the-square, find parameter b/c given one root, vertex/axis-of-symmetry, 'how many roots' (sign of D), factor-the-trinomial as a simplify, roots-as-pair (sum/product), repeated (double) root, and word-context quadratics. No geometric framings, no inverse problems. + +**progressions** +- prog-arith-term (order 1, kind:compute, level 2) — nth term of arithmetic: pick a1,d,n; derive val=a+(n-1)*d; answer:val +- prog-geom-term (order 2, kind:compute, level 3) — nth term of geometric: pick b1,q,n; derive val=b*q^(n-1); answer:val + +_Пробелы:_ Only 2 generators, both forward nth-term. MISSING ENTIRELY: sum of arithmetic (Sn), sum of geometric (Sn), inverse problems (find d, find q, find a1, find n), arithmetic mean / inserting means, geometric mean, sum of first n natural / odd numbers, recurrence-style 'find next term', and real-life word contexts (savings, seating rows, bouncing ball, salary raise). No inverse, no sums, no means, no word problems. + +## Новые генераторы + +### `quad-incomplete-bx` — ax² + bx = 0 (вынесение x) (тема quadratic, L2, roots) +- **Форма:** ax² + bx = 0 → x(ax+b)=0 → roots {0, -b/a} +- **Пример условия:** pick:{ a:[2,5], r:[-6,6] } constraint:'r!=0' derive:{ b:'-a*r' } lhs:'{a}*x^2 + {b}*x', rhs:'0' answerVar:'x', answers:['0','r'], integerAnswer:true +- **Корень-вперёд:** Root-forward: the non-zero root r is picked FIRST as a clean integer, then b is derived as b=-a*r so that -b/a = r exactly. The other root is always 0. Both answers ('0' and 'r') are clean integers → _checkMultiRoot passes with 2 typed integers. +- **Фигура:** none (algebraic) + +### `quad-incomplete-c` — ax² = c (квадратный корень) (тема quadratic, L2, roots) +- **Форма:** ax² − c = 0 → x² = c/a → roots {±r} +- **Пример условия:** pick:{ a:[1,4], r:[2,9] } derive:{ c:'a*r*r' } lhs:'{a}*x^2 - {c}', rhs:'0' answerVar:'x', answers:['r','-r'], integerAnswer:true +- **Корень-вперёд:** Root-forward: r is the clean integer root; c is derived as a*r² so c/a = r² is a perfect square and √(c/a)=r exactly. Roots ±r are clean integers. Contrasts with quad-diff because here a≠1, teaching dividing by leading coeff first. + +### `quad-disc-clean` — ax² + bx + c = 0 (через дискриминант) (тема quadratic, L3, roots) +- **Форма:** full quadratic with a>1, solved via D=b²−4ac (clean perfect-square D), x=(−b±√D)/(2a) +- **Пример условия:** pick:{ a:[2,3], r1:[-4,4], r2:[-4,4] } constraint:'r1!=r2' derive:{ b:'-a*(r1+r2)', c:'a*r1*r2', disc:'a*a*(r1-r2)*(r1-r2)', sqd:'a*abs(r1-r2)' } lhs:'{a}*x^2 + {b}*x + {c}', rhs:'0' answerVar:'x', answers:['r1','r2'], integerAnswer:true solution shows D={disc}, √D={sqd} +- **Корень-вперёд:** Root-forward via factored form a(x−r1)(x−r2): expanding gives b=−a(r1+r2), c=a·r1·r2, so a,b,c are integers AND D=b²−4ac = a²(r1−r2)² is automatically a perfect square. √D = a·|r1−r2| is a clean integer, so x=(−b±√D)/(2a)=r1,r2 are exact integers. The discriminant method never produces an irrational root. + +### `quad-trinomial-factor` — Разложить трёхчлен на множители (тема quadratic, L2, simplify) +- **Форма:** x² + bx + c → (x − r1)(x − r2) (kind:simplify, equivalence-checked) +- **Пример условия:** pick:{ r1:[-6,6], r2:[-6,6] } constraint:'r1!=r2' derive:{ b:'-(r1+r2)', c:'r1*r2' } srcExpr:'x^2 + {b}*x + {c}', answerExpr:'(x - {r1})*(x - {r2})', answerVars:['x'] display:'Разложите на множители: x² + {b}x + {c}' +- **Корень-вперёд:** Root-forward: r1,r2 picked first → b,c derived so the trinomial factors exactly over integers. Because kind:simplify checks equivalence by sampling, the student may write (x−r1)(x−r2) in ANY equivalent order/sign; SimExpr verifies x²+bx+c ≡ (x−r1)(x−r2). No irrational factors ever appear. + +### `quad-find-b` — Найти b, зная один корень (тема quadratic, L3, compute) +- **Форма:** x² + bx + c = 0 has root r; given c and r, find b (kind:compute, integer answer) +- **Пример условия:** pick:{ r:[-6,6], k:[-6,6] } constraint:'r!=0 && k!=0 && k!=r' derive:{ c:'r*k', b:'-(r+k)' } lhs:'x', rhs:'{b}' display:'Один из корней уравнения x² + bx + {c} = 0 равен {r}. Найдите b.' answer:'b', integerAnswer:true +- **Корень-вперёд:** Root-forward parameter problem: pick the known root r and the SECOND root k first, then derive c=r·k and the unknown answer b=−(r+k). Since b is computed from clean integers, the typed answer is a clean integer. Student finds b either by Vieta (b=−(r+k), needing second root c/r=k) or by substituting r: r²+br+c=0 → b=−(r²+c)/r. Both give the same integer. + +### `quad-count-roots` — Сколько корней? (знак дискриминанта) (тема quadratic, L2, compute) +- **Форма:** given ax²+bx+c=0, answer the NUMBER of real roots (0, 1, or 2) from sign of D (kind:compute) +- **Пример условия:** pick:{ a:[1,3], b:[-8,8], c:[-6,9] } derive:{ disc:'b*b - 4*a*c', nroots:'disc>0 ? 2 : (disc==0 ? 1 : 0)' } lhs:'x', rhs:'{nroots}' display:'Сколько действительных корней у уравнения {a}x² + {b}x + {c} = 0? (введите 0, 1 или 2)' answer:'nroots', integerAnswer:true +- **Корень-вперёд:** Answer is a COUNT (0/1/2), not a root — so roots themselves may be irrational and that is fine. Root-forward not needed for clean roots; instead the answer nroots is derived deterministically by SimExpr's ternary from sign(D)=sign(b²−4ac). To guarantee all three outcomes appear, optionally add a second variant that picks a perfect-square D for the '1 root' and '2 roots' cases. integerAnswer keeps the typed value clean. +- **Фигура:** Small sign-of-parabola sketch (figure:'parabola-roots') showing the curve crossing/touching/missing the x-axis — illustrative only. + +### `quad-vertex-x` — Координата вершины параболы (x₀ = −b/2a) (тема quadratic, L2, compute) +- **Форма:** y = ax² + bx + c; find x-coordinate of vertex / axis of symmetry (kind:compute) +- **Пример условия:** pick:{ a:[1,4], xv:[-6,6], c:[-5,9] } constraint:'xv!=0' derive:{ b:'-2*a*xv' } lhs:'x', rhs:'{xv}' display:'Найдите абсциссу вершины параболы y = {a}x² + {b}x + {c} (ось симметрии x₀).' answer:'xv', integerAnswer:true +- **Корень-вперёд:** Root-forward on the vertex: pick the vertex abscissa xv as a clean integer first, then derive b=−2a·xv so that x₀=−b/(2a)=xv exactly. The answer is always a clean integer regardless of a. (A sibling generator can ask for the vertex ORDINATE y₀ = c − a·xv² by deriving that too, also clean.) +- **Фигура:** figure:'parabola-vertex' marking the axis of symmetry and vertex point — illustrative. + +### `quad-complete-square` — Выделить полный квадрат (тема quadratic, L3, simplify) +- **Форма:** x² + bx + c → (x + p)² + k (kind:simplify, b even so p,k integer) +- **Пример условия:** pick:{ p:[-6,6], k:[-5,9] } constraint:'p!=0' derive:{ b:'2*p', c:'p*p + k' } srcExpr:'x^2 + {b}*x + {c}', answerExpr:'(x + {p})^2 + {k}', answerVars:['x'] display:'Выделите полный квадрат: x² + {b}x + {c}' +- **Корень-вперёд:** Root-forward on the completed form: pick the shift p and remainder k FIRST (both integers), then derive b=2p (always even → no fractions) and c=p²+k. The student's (x+p)²+k is equivalence-checked by sampling, so any algebraically equal form passes. Guarantees integer p,k and exact identity x²+bx+c ≡ (x+p)²+k. + +### `prog-arith-sum` — Сумма n членов арифм. прогрессии (тема progressions, L2, compute) +- **Форма:** Sₙ = n(2a₁+(n−1)d)/2 (kind:compute, integer) +- **Пример условия:** pick:{ a:[-8,12], d:[-6,6], n:[4,12] } require:'d!=0' derive:{ an:'a + (n-1)*d', sum:'n*(a + an)/2' } lhs:'x', rhs:'{n}*({a} + {an})/2' display:'Арифметическая прогрессия: a₁ = {a}, d = {d}. Найдите сумму первых {n} членов.' answer:'sum', integerAnswer:true +- **Корень-вперёд:** Clean-answer-forward: Sₙ = n(a₁+aₙ)/2 is always an integer because n(a₁+aₙ) — the product of n and the sum of a symmetric pair — is even for integer a₁,d (each pair aᵢ+a_{n+1−i} is constant). integerAnswer rounds away any 0.5 float artifact. Uses the aₙ already derived, so the printed formula and the checker agree. + +### `prog-arith-find-d` — Найти разность d по двум членам (тема progressions, L2, compute) +- **Форма:** given a₁ and aₙ, find d = (aₙ−a₁)/(n−1) (kind:compute, integer) +- **Пример условия:** pick:{ a:[-6,10], d:[-5,5], n:[4,10] } require:'d!=0' derive:{ an:'a + (n-1)*d' } lhs:'x', rhs:'({an} - {a})/({n} - 1)' display:'В арифметической прогрессии a₁ = {a}, a_{n} = {an} (n = {n}). Найдите разность d.' answer:'d', integerAnswer:true +- **Корень-вперёд:** Inverse problem, root-forward: pick d as a clean integer FIRST, then derive aₙ=a₁+(n−1)d. The student recovers d=(aₙ−a₁)/(n−1); since aₙ−a₁=(n−1)d is an exact multiple of (n−1), the division is exact and d is the clean integer we started from. + +### `prog-arith-find-n` — Каким по счёту идёт член прогрессии (тема progressions, L3, compute) +- **Форма:** given a₁, d, aₙ, find n = (aₙ−a₁)/d + 1 (kind:compute, integer) +- **Пример условия:** pick:{ a:[-6,10], d:[-5,5], n:[4,12] } require:'d!=0' derive:{ an:'a + (n-1)*d' } lhs:'x', rhs:'({an} - {a})/{d} + 1' display:'В арифметической прогрессии a₁ = {a}, d = {d}. Каким по счёту является член, равный {an}?' answer:'n', integerAnswer:true +- **Корень-вперёд:** Inverse: pick the position n first, derive the term aₙ=a₁+(n−1)d. Recovering n=(aₙ−a₁)/d+1 is exact because aₙ−a₁ is an exact multiple of d. Answer is the clean integer position we chose. + +### `prog-arith-mean` — Среднее арифметическое (вставить член) (тема progressions, L2, compute) +- **Форма:** x is between a and c in arithmetic prog → x = (a+c)/2 (kind:compute, integer) +- **Пример условия:** pick:{ m:[-9,9], d:[-7,7] } require:'d!=0' derive:{ a:'m - d', c:'m + d' } lhs:'x', rhs:'({a} + {c})/2' display:'Числа {a}, x, {c} образуют арифметическую прогрессию. Найдите x.' answer:'m', integerAnswer:true +- **Корень-вперёд:** Root-forward on the mean: pick the middle term m and step d FIRST, then derive the two neighbours a=m−d, c=m+d. The answer x=(a+c)/2=m is exactly the integer m we chose (a+c=2m is always even). Teaches the arithmetic-mean property cleanly. + +### `prog-geom-find-q` — Найти знаменатель q геом. прогрессии (тема progressions, L2, compute) +- **Форма:** given b₁ and b₂ (or bₙ), find q = b₂/b₁ (kind:compute, integer) +- **Пример условия:** pick:{ b:[1,5], q:[2,4] } derive:{ b2:'b*q' } lhs:'x', rhs:'{b2}/{b}' display:'Геометрическая прогрессия: b₁ = {b}, b₂ = {b2}. Найдите знаменатель q.' answer:'q', integerAnswer:true +- **Корень-вперёд:** Inverse geometric, root-forward: pick q as a clean integer (2..4) and b₁, then derive b₂=b₁·q. The student computes q=b₂/b₁; since b₂ is an exact multiple of b₁ by construction, q is the clean integer chosen. Avoids fractional ratios. + +### `prog-geom-mean` — Геометрическое среднее (вставить член) (тема progressions, L3, compute) +- **Форма:** positive a, x, c in geometric prog → x = √(a·c) (kind:compute, integer) +- **Пример условия:** pick:{ m:[2,9], q:[2,3] } derive:{ a:'m*q', c:'m/q' } — better: pick m,q then a=m, c=m*q*q so middle is m*q; OR pick g (the mean) and q: derive a='g*g/(g... )'. Concrete: pick:{ g:[2,10], q:[2,3] } derive:{ a:'g/q', c:'g*q' } require:'(g % q)==0' lhs:'x', rhs:'sqrt({a}*{c})' display:'Числа {a}, x, {c} образуют геометрическую прогрессию (все положительны). Найдите x.' answer:'g', integerAnswer:true +- **Корень-вперёд:** Root-forward on the geometric mean: pick the mean g and ratio q first, derive neighbours a=g/q and c=g·q with require '(g % q)==0' so a is an integer too. Then a·c=g² is a perfect square and x=√(a·c)=g is exactly the clean integer chosen. Guarantees a clean integer under the radical. + +### `prog-geom-sum` — Сумма n членов геом. прогрессии (тема progressions, L3, compute) +- **Форма:** Sₙ = b₁(qⁿ−1)/(q−1) (kind:compute, integer; q small) +- **Пример условия:** pick:{ b:[1,4], q:[2,3], n:[2,5] } derive:{ qn:'q^n', sum:'b*(qn - 1)/(q - 1)' } lhs:'x', rhs:'{b}*({qn} - 1)/({q} - 1)' display:'Геометрическая прогрессия: b₁ = {b}, q = {q}. Найдите сумму первых {n} членов.' answer:'sum', integerAnswer:true +- **Корень-вперёд:** Clean-answer-forward: with integer q≥2, (qⁿ−1) is always divisible by (q−1) (factor identity qⁿ−1=(q−1)(qⁿ⁻¹+…+1)), so Sₙ=b₁(qⁿ−1)/(q−1) is an exact integer. Small q,n keep magnitudes typable. integerAnswer guards float rounding of q^n. + +### `prog-arith-word` — Задача: ряды кресел / зарплата (арифм.) (тема progressions, L2, compute) +- **Форма:** real-life arithmetic: rows of seats, monthly savings, salary raise → find a term or sum (kind:compute) +- **Пример условия:** pick:{ a:[10,20], d:[2,5], n:[5,12] } derive:{ an:'a + (n-1)*d', sum:'n*(a + an)/2' } lhs:'x', rhs:'{n}*({a} + {an})/2' display:'В первом ряду зала {a} кресел, в каждом следующем на {d} больше. Сколько всего кресел в {n} рядах?' answer:'sum', integerAnswer:true +- **Корень-вперёд:** Same clean-sum guarantee as prog-arith-sum (n(a₁+aₙ)/2 integer); positive params chosen so the word context (seats/rubles) is non-negative and realistic. Wraps the formula in a story for ЦТ-style word problems; answer stays a clean integer count. +- **Фигура:** Optional figure:'rows-bars' — a few stacked bars of increasing height suggesting growing rows; purely decorative. + +## Новые формулировки условий + +- **Roots-as-pair (sum & product) — ask for the SUM or PRODUCT of the roots instead of the roots themselves, using Vieta directly.** + - Пример: kind:compute. pick r1,r2 → derive b=-(r1+r2), c=r1*r2, sum=r1+r2 (=−b), prod=r1*r2 (=c). display: 'Найдите сумму корней уравнения x² + {b}x + {c} = 0.' answer:'sum', integerAnswer:true. Clean because the sum/product are integers by Vieta; no need to extract irrational roots, so even non-factorable quadratics work. + - Заметка: Two sibling generators (sum-of-roots, product-of-roots) from one pattern. Teaches Vieta as a shortcut without solving. + +- **Reconstruct-the-equation — given the two roots, WRITE the monic quadratic x²+bx+c (kind:simplify, equivalence-checked).** + - Пример: pick r1,r2 → derive b=-(r1+r2), c=r1*r2. display: 'Составьте приведённое квадратное уравнение с корнями {r1} и {r2} (запишите левую часть = 0).' srcExpr:'(x - {r1})*(x - {r2})', answerExpr:'x^2 + {b}*x + {c}'. Student types x²+bx+c; sampling-equivalence accepts any equal form. Inverse of quad-factored. + - Заметка: Reuses the factored/expanded identity in the opposite direction — pure root-forward, answer always integer-coefficient. + +- **Sign-of-D classification — instead of counting, ask WHICH case (kept as compute with 0/1/2, or as a multiple-choice phrased compute).** + - Пример: Same derive disc=b²−4ac and nroots ternary as quad-count-roots, but display: 'Имеет ли уравнение {a}x²+{b}x+{c}=0 два корня? Введите количество корней.' Provide one variant forcing perfect-square D (pick roots first) so the '2 distinct roots' and '1 double root' cases are demonstrably clean; the '0 roots' case just needs D<0. + - Заметка: Pairs naturally with quad-count-roots; emphasises interpreting D rather than computing roots. + +- **Double (repeated) root — perfect-square trinomial x²±2px+p²=0 with a SINGLE root (answers array length 1).** + - Пример: kind:roots. pick p → derive b=2*p, c=p*p, lhs:'x^2 + {b}*x + {c}', answers:['-p'] (length 1). Because _checkMultiRoot requires vals.length===want.length, the student must type exactly ONE value (−p). Teaches D=0 ⇒ one root. Root is the clean integer −p. + - Заметка: Important contrast with quad-factored (two roots). Must keep answers array length exactly 1 so the count check matches. + +- **Word-context quadratic (area / number puzzle) — geometry or number story whose model is a clean-root quadratic.** + - Пример: kind:roots or compute. Number puzzle: pick r1,r2 (positive) → 'Произведение двух последовательных-... ' or area: 'Прямоугольник: длина на {k} больше ширины, площадь {S}. Найдите ширину.' derive width root-forward so S=width·(width+k) gives integer width. Answer = the picked clean integer dimension. For compute-kind, ask only for the positive dimension (single answer). + - Заметка: Bridges quadratics to ЦТ word problems; root-forward picks the physical dimension first so the modelled quadratic has an exact integer solution. + +- **Find a₁ (first term) given a later term and d — inverse direction.** + - Пример: kind:compute. pick a (the real a₁), d, n → derive an=a+(n-1)*d. display: 'В арифметической прогрессии d = {d}, a_{n} = {an} (n = {n}). Найдите a₁.' answer:'a', integerAnswer:true. Exact because a₁=aₙ−(n−1)d is integer arithmetic. + - Заметка: Completes the inverse-quartet alongside find-d, find-n; same root-forward (pick the true a₁ first). + +- **Find b₁ / find n in geometric — inverse geometric problems with clean integer ratio.** + - Пример: find-b1: pick b1,q,n → derive bn=b1*q^(n-1); ask b₁ given bn,q,n → answer b1 (exact integer since bn/q^(n-1)=b1). find-n: pick small q,n → derive bn; ask the position; answer n via log is avoided — phrase as 'каким по счёту' with a small search space, answer:'n' integerAnswer. + - Заметка: Mirrors prog-geom-find-q; keep q∈{2,3,4} and n small so powers stay typable and divisions are exact. + +- **Sum of first n natural numbers / first n odd numbers — special arithmetic sums as named identities.** + - Пример: kind:compute. naturals: pick n → derive sum=n*(n+1)/2; display 'Найдите сумму 1+2+...+{n}.' answer:'sum'. odds: pick n → derive sum=n*n; 'Найдите сумму первых {n} нечётных чисел.' answer:'sum'. Both are exact integers (triangular number; perfect square). + - Заметка: Cheap, recognisable identities good for level-1 entries; answers provably integer (n(n+1)/2 and n²). + +- **Real-life geometric context — bouncing ball / bacteria doubling / compound growth with small integer ratio.** + - Пример: kind:compute. pick b1 (start height/count), q∈{2,3}, n → derive val=b1*q^(n-1) (nth term) or sum. display 'Бактерий было {b1}, каждый час их число умножается на {q}. Сколько через {n} часов?' answer:'val', integerAnswer. Clean because integer q^(n-1) keeps the result an exact integer. + - Заметка: Word wrapper over prog-geom-term; keep n small so q^(n-1) stays a typable integer. + +## Заметки реализации + +Verified against the REAL files. Current group has only 4 generators (quad-factored, quad-diff, prog-arith-term, prog-geom-term) — both quadratics are monic/reduced (leading coeff 1) and both progressions are forward nth-term only; this is the thinnest of the 21 topics and the highest-leverage to expand. + +Engine facts that constrain proposals (confirmed in _trainer_engine.js): +- kind:roots uses gen.answers:[exprs] and _checkMultiRoot REQUIRES vals.length===answers.length (lines 565-575). So 'no real roots' CANNOT be a roots-kind generator (nothing to type) → modelled as kind:compute counting roots (quad-count-roots). A 'double root' MUST have answers array of length exactly 1 (quad double-root framing). +- kind:compute checks lhs:'x' against rhs:formula; answer:'derivedName' with integerAnswer:true rounds floats (instantiate lines 314-318). The {ans} token in solution tex prints the final value. All my compute generators set integerAnswer:true to absorb float artifacts of division/^. +- kind:simplify (srcExpr/answerExpr/answerVars) is equivalence-checked by sampling (_checkEquiv/_sampleEquiv), so factor/complete-square/reconstruct answers accept any algebraically-equal form — robust and order-independent. +- kind:system shape (eqs:[{lhs,rhs}], answers:{x,y}, answerVars) confirmed via sys-2x2 — not needed for this group but available. +- derive runs SimExpr sequentially and supports ternary ?: and floor/sign/abs/sqrt (per CLAUDE.md FUNCTIONS), enabling quad-count-roots nroots='disc>0?2:(disc==0?1:0)' and geometric-mean sqrt — fully deterministic, no eval. + +Root-forward discipline applied everywhere: for every clean-root/clean-answer generator the picked integer (root, vertex abscissa, ratio, difference, position, mean) is chosen FIRST and the visible coefficients are DERIVED from it, so the engine self-check (instantiate okSelf via verifyRoot / _sampleEquiv) always passes and the typed answer is exact. Discriminant generator is the key proof: factored form a(x−r1)(x−r2) forces D=a²(r1−r2)² = perfect square, so √D is a clean integer and the discriminant formula never yields irrationals. + +LEVELS additions needed (generators.js LEVELS map ~line 1082) for each new id, choosing 1..3 by STRUCTURE: incomplete forms→1-2, find-d/q/mean→2, full-discriminant/find-b/find-n/complete-square/geom-sum→3. order values continue after existing (quadratic order 3+, progressions order 3+). + +INVARIANTS respected in all proposals: SimExpr-only (no eval), no emoji (any figure sketches would be inline-SVG figure types added to figures.js, e.g. parabola-roots/parabola-vertex), answers cleanly typable integers, display texts plain (escaped on render), and no reserved param names — note I deliberately avoided t,w,h,pi,e,E,PI,tau as param names (used a,b,c,d,n,q,r,r1,r2,p,k,m,g,sx,xv,sum,an,disc,nroots,qn instead). + +Figure ideas (parabola-roots, parabola-vertex, rows-bars) are OPTIONAL/illustrative and would each need a new entry in figures.js (13 figure types today, none parabolic) — flagged as future work, not required for the algebra generators to function. + +--- + +# Группа: arithmetic-5-6 + +## Текущий инвентарь и пробелы + +**gcd-lcm** +- gcd-pair (L1): НОД двух чисел a=g·m, b=g·n; решение через разложение на простые (factorize), val=gcd(a,b) +- lcm-pair (L2): НОК двух чисел a=g·m, b=g·n; разложение на простые, val=lcm(a,b) + +_Пробелы:_ Только пары. Нет: НОД/НОК ТРЁХ чисел; проверки взаимной простоты (coprime, true/false); сокращения дроби через НОД; задач-контекста (раскладка по пакетам / одновременный старт автобусов = классика НОК). + +**fractions** +- frac-of-number (L1): часть a/n от числа m=n·mfac → целый ответ +- frac-add-same (L2): a/n + b/n, одинаковый знаменатель, ответ-дробь + +_Пробелы:_ ОЧЕНЬ узко. Нет: разных знаменателей (+,−,·,÷); сокращения дроби; сравнения дробей (/=); смешанных чисел; дробь↔десятичная; нахождения числа по его части (обратная frac-of-number). + +**decimals** +- dec-add (L1): da+db, десятые (целые/10) +- dec-sub (L1): da−db, десятые, a>b +- dec-mult (L2): da·db, ответ в сотых (a·b/100) + +_Пробелы:_ Нет: деления десятичных; округления; сравнения/упорядочивания; перевода дробь→десятичная и обратно; умножения/деления на степени 10 (сдвиг запятой); сотых/тысячных в слагаемых (только десятые). + +**negatives** +- neg-add (L1): сумма a+b с отрицательными +- neg-sub (L2): разность a−b +- neg-mult (L2): произведение a·b + +_Пробелы:_ Нет: деления отрицательных; модуля |a| и |a|±|b|; сравнения (a1; на практике require/constraint держит числа малыми, а ответ всё равно валидируется gcd() — самопроверка движка проходит при любом g). Ответ — целое g. factorize печатает школьное разложение. +- **Фигура:** нет + +### `lcm-triple` — НОК трёх чисел (тема gcd-lcm, L3, compute) +- **Форма:** Найдите НОК(a,b,c) для трёх небольших чисел +- **Пример условия:** pick:{a:[2,9],b:[2,9],c:[2,9]} constraint:'a!=b && b!=c && a!=c' derive:{val:'lcm(a,lcm(b,c))'} require:'lcm(a,lcm(b,c))<=360' lhs:'x' rhs:'lcm({a},lcm({b},{c}))' answer:'val' integerAnswer:true factorize:[{name:'aFac',of:'a'},{name:'bFac',of:'b'},{name:'cFac',of:'c'},{name:'kFac',of:'lcm(a,lcm(b,c))'}] +- **Корень-вперёд:** Числа малы (2..9), require клампит НОК ≤360 → результат гарантированно целый и небольшой; эталон даёт lcm(). Ответ всегда чистое целое. Разложение каждого множителя печатается через factorize. +- **Фигура:** нет + +### `coprime-check` — Взаимно простые? (тема gcd-lcm, L2, compute) +- **Форма:** Истина/ложь: являются ли числа a и b взаимно простыми (НОД=1)? Ответ 1=да, 0=нет +- **Пример условия:** pick:{p:[2,7],q:[2,9],g:[1,6]} derive:{a:'p*g',b:'q*g',val:'gcd(a,b)==1'} require:'a!=b && a<=90 && b<=90' lhs:'x' rhs:'gcd({a},{b})==1' answer:'val' integerAnswer:true display:'Взаимно просты ли числа {a} и {b}? Если да — введите 1, если нет — 0.' +- **Корень-вперёд:** Управляем ответом через g: g=1 → почти всегда взаимно простые (ответ 1), g>1 → точно НЕ взаимно простые (ответ 0). Истинность gcd(a,b)==1 даёт ровно 1 или 0 (SimExpr булево = 1/0) → ответ всегда чистое 0 или 1. Эталон gcd() самопроверяется. +- **Фигура:** нет + +### `lcm-buses` — Снова вместе (НОК, задача) (тема gcd-lcm, L2, compute) +- **Форма:** Два автобуса/события с интервалами a и b мин стартовали вместе. Через сколько минут снова совпадут? +- **Пример условия:** pick:{a:[3,12],b:[3,12]} constraint:'a!=b' derive:{val:'lcm(a,b)'} require:'lcm(a,b)<=120' lhs:'x' rhs:'lcm({a},{b})' answer:'val' integerAnswer:true display:'Два автобуса отходят от остановки: первый — каждые {a} мин, второй — каждые {b} мин. Сейчас они отправились вместе. Через сколько минут они снова отправятся одновременно?' +- **Корень-вперёд:** Тот же приём lcm-pair, но в словесной обёртке. Малые интервалы + require ≤120 → НОК чистое целое (минуты). Контекст «снова вместе» — канонический школьный смысл НОК. +- **Фигура:** нет + +### `frac-reduce` — Сократите дробь (тема fractions, L2, compute) +- **Форма:** Сократите a/b до несократимой; ответ-дробь +- **Пример условия:** pick:{p:[1,7],q:[2,9],g:[2,9]} constraint:'p b*m) ? 1 : 2'} lhs:'x' rhs:'(({a}*{n}) > ({b}*{m})) ? 1 : 2' answer:'val' integerAnswer:true display:'Сравните дроби {a}/{m} и {b}/{n}. Если больше первая — введите 1, если вторая — 2.' +- **Корень-вперёд:** Сравнение через перекрёстное произведение a·n vs b·m (никаких десятичных). constraint a·n≠b·m исключает равенство → ответ всегда ровно 1 или 2 (целое). Тернарник SimExpr возвращает чистый код. Решение показывает приведение к общему знаменателю. +- **Фигура:** нет + +### `frac-of-whole-inverse` — Число по его части (тема fractions, L3, compute) +- **Форма:** Известно, что a/n числа равно val; найдите число +- **Пример условия:** pick:{n:[2,6],a:[1,5],whole:[2,9]} constraint:'ab)?1:2'} lhs:'x' rhs:'(({a})>({b}))?1:2' answer:'val' integerAnswer:true display:'Сравните {d1} и {d2}. Если больше первое — введите 1, если второе — 2.' +- **Корень-вперёд:** Сравнение сотых = сравнение целых a,b (constraint a≠b → нет равенства). Ответ ровно 1 или 2 (целое). Учит поразрядному сравнению; решение «уравниваем число знаков после запятой». +- **Фигура:** нет + +### `neg-div` — Деление (отрицательные) (тема negatives, L2, compute) +- **Форма:** Частное a:b, где a=b·q (делится нацело), знаки варьируются +- **Пример условия:** pick:{q:[-9,9],b:[-9,9]} constraint:'q!=0 && b!=0 && (q<0 || b<0)' derive:{a:'q*b',val:'q'} lhs:'x' rhs:'({a})/({b})' answer:'val' integerAnswer:true display:'Найдите частное чисел {a} и {b}.' +- **Корень-вперёд:** Частное q выбираем ПЕРВЫМ, делимое a=q·b → деление нацело, ответ ровно q (целое). constraint гарантирует хотя бы один отрицательный множитель → отрабатывается правило знаков. Чистое целое. +- **Фигура:** нет + +### `neg-order-ops` — Порядок действий со знаками (тема negatives, L3, compute) +- **Форма:** a·b + c или a − b·c с отрицательными (2 действия) +- **Пример условия:** pick:{a:[-6,6],b:[-6,6],c:[-9,9]} constraint:'a!=0 && b!=0 && (a<0 || b<0 || c<0)' derive:{val:'a*b + c'} lhs:'x' rhs:'({a})*({b}) + ({c})' answer:'val' integerAnswer:true display:'Вычислите: {a} · {b} + {c}.' +- **Корень-вперёд:** Все параметры — целые, единственная операция-произведение и сложение → результат всегда целый. Тренирует порядок действий (сначала умножение) и правило знаков. Можно сделать L2-вариант со скобками (a−b)·c. Ответ чистое целое. +- **Фигура:** нет + +### `neg-abs` — Модуль числа / выражения (тема negatives, L2, compute) +- **Форма:** Найдите |a| − |b| (или |a−b|) +- **Пример условия:** pick:{a:[-15,15],b:[-15,15]} constraint:'a!=0 && b!=0' derive:{val:'abs(a) - abs(b)'} lhs:'x' rhs:'abs({a}) - abs({b})' answer:'val' integerAnswer:true display:'Вычислите |{a}| − |{b}|.' +- **Корень-вперёд:** Целые a,b → модули целые → разность целая. abs() есть в SimExpr, эталон самопроверяется. Тренирует понятие модуля. L3-вариант: |a−b| (модуль от выражения). +- **Фигура:** нет + +### `neg-compare-line` — Сравнение на координатной прямой (тема negatives, L1, compute) +- **Форма:** Какое число больше: a или b? Ответ-код 1/2 (с опорой на числовую прямую) +- **Пример условия:** pick:{a:[-12,12],b:[-12,12]} constraint:'a!=b && (a<0 || b<0)' derive:{val:'(a>b)?1:2'} lhs:'x' rhs:'(({a})>({b}))?1:2' answer:'val' integerAnswer:true display:'На координатной прямой отметили {a} и {b}. Какое число больше: введите 1 (это {a}) или 2 (это {b}).' +- **Корень-вперёд:** Сравнение целых, хотя бы одно отрицательное → ученик учится «правее = больше». Ответ ровно 1 или 2. Можно добавить figure числовой прямой (см. framing coordinate-line), но и без неё работает как compute. +- **Фигура:** Числовая прямая (figures.js numberLine, если есть) с двумя отмеченными точками a и b; иначе текстовое описание. + +### `neg-square` — Квадрат отрицательного (тема negatives, L2, compute) +- **Форма:** Вычислите (a)^2 и сравните с −a^2 (классическая ловушка знака) +- **Пример условия:** pick:{a:[-9,-2]} derive:{val:'a*a'} lhs:'x' rhs:'({a})*({a})' answer:'val' integerAnswer:true display:'Вычислите квадрат числа {a}, то есть ({a})².' +- **Корень-вперёд:** a отрицательное (−9..−2), квадрат = a·a — целое положительное. Эталон self-check через a*a. Учит, что (−3)²=9 (не −9). Чистый целый ответ. Решение подчёркивает скобки и правило знаков. +- **Фигура:** нет + +## Новые формулировки условий + +- **Истина/ложь (1/0)** + - Пример: «Верно ли, что 3/4 > 5/8? Если да — введите 1, если нет — 0.» → derive val:'(3*8 > 5*4) ? 1 : 0' (перекрёстное произведение). Истинность булева в SimExpr = 1/0, поэтому ответ всегда чистый код, integerAnswer:true. Применимо к coprime-check, сравнению дробей/десятичных/отрицательных, проверке равенства десятичной и дроби. + - Заметка: Кодируем ответ как 1/0; в display прямо указываем соответствие. НЕ использовать как kind:'inequality' — это compute с булевым val. + +- **Выбор-кодом «что больше» (1/2/0)** + - Пример: «Сравните {a}/{m} и {b}/{n}: 1 — первая больше, 2 — вторая, 0 — равны.» Тернарник в rhs возвращает код. Чистый целый ответ, не требует ввода знака «>». + - Заметка: Обходит проблему ввода символов сравнения учеником: ответ — маленькое целое. Использовано в frac-compare, dec-compare, neg-compare-line. + +- **«Вставьте знак» (тоже кодом)** + - Пример: «Вставьте нужный знак между {a} и {b}: введите 1 для «<», 2 для «>», 0 для «=».» derive:{val:'(ab)?2:0)'}. Ученик выбирает отношение, ответ — код 0/1/2. + - Заметка: Семантически = «insert the sign», но типизируемо без спецсимволов. Подходит для десятичных, дробей, отрицательных. Можно показать в решении сам знак через exprToLatex. + +- **Словесный контекст (word-problem)** + - Пример: НОК: «автобусы снова отправятся вместе» (lcm-buses); дроби: «съели a/n пирога, сколько осталось»; десятичные: «цена за кг и масса → стоимость»; отрицательные: «температура была −5°, упала на 8°, стала?». Числа подбираются root-forward, чтобы ответ был чистым. + - Заметка: Контекст в display, математика та же. Усиливает мотивацию; difficulty +1 за интерпретацию. + +- **Оценка/прикидка (estimation)** + - Пример: «Оцените ≈ 6.97 + 3.02, округлив каждое до целого.» derive:{val:'round(a/100)+round(b/100)'}, числа подобраны так, что округления дают чистое целое. Ответ — целое (приближённое). + - Заметка: Учит прикидке. ⚠ Чёткий критерий: просим конкретное действие (округлить ДО целого и сложить), чтобы ответ был детерминирован и self-check проходил; иначе «примерно» неоднозначно. + +- **Смешанное число ↔ неправильная дробь** + - Пример: «Запишите 2 3/4 неправильной дробью» → ответ 11/4 (val:'(c*n+a)/n'); или обратно. Целая часть c, правильная дробь a/n берутся первыми → числитель c·n+a, знаменатель n. SimExpr примет «11/4». + - Заметка: Ответ-дробь, численная проверка. Display: «целых c и a/n». Round-forward: c,a,n целые → дробь точная. + +## Заметки реализации + +Все генераторы используют kind:'compute' (lhs:'x', rhs:формула) — текущий паттерн всех 10 существующих в группе; ответ проверяется численно (verifyRoot + nearCanonical), поэтому дробные ответы вроде 3/4 и эквивалентные/сокращённые формы принимаются автоматически. SimExpr предоставляет gcd, lcm, mod, abs, floor, ceil, round, sign, min, max, pow и тернарник ?: — этого достаточно для ВСЕХ предложенных идей без расширения грамматики и без eval. Булевы выражения SimExpr дают ровно 1/0, что я использую для true/false и «что больше» кодов — гарантированно чистый целый ответ (integerAnswer:true). Зарезервированные имена (t,w,h,pi,e,E,PI,tau) в pick НЕ используются — для частей/частных взяты q,g,m,n,k,a,b,c,whole,dir,bi,h(только как параметр сотых в dec-round — КОНФЛИКТ: h зарезервировано! → переименовать в hh/hund). ВАЖНАЯ ПРАВКА: в dec-round и dec-times-pow10 параметр 'h' нельзя — заменить на hh. frac-to-decimal и dec-times-pow10 с переменным оператором/знаменателем чище реализовать как НЕСКОЛЬКО генераторов (по фиксированному b или по оператору), чем тащить таблицу в один pick — рекомендация для имплементации. Каждый новый id нужно добавить в LEVELS map (значения difficulty указаны). Геометрия/figure не задействованы (группа арифметическая); единственная опциональная figure — числовая прямая для neg-compare-line, если в figures.js есть numberLine (надо проверить при имплементации). Тексты display экранируются движком; спецсимволы сравнения в ответы не вводятся (кодируются числами). + +--- + +# Группа: geometry + +## Текущий инвентарь и пробелы + +**g-angles** +- ang-triangle (L2): third angle of triangle = 180-a-b; figure triangle-angles +- ang-adjacent (L1): supplementary angle = 180-a; figure adjacent-angles +- ang-exterior (L2): exterior angle = a+b (two remote interior); figure triangle-angles ext:true + +_Пробелы:_ No parallel-lines + transversal (corresponding/alternate/co-interior — core ЦТ topic). No vertical angles. No isosceles base-angle reasoning. No angle bisector. No polygon exterior-angle sum (=360). Only 3 generators, all single-step 180-sum variants. + +**g-pyth** +- pyth-hyp (L2): hypotenuse from legs (m>n triple); figure right-triangle unknown:c +- pyth-leg (L3): leg from hyp+leg; figure right-triangle unknown:b + +_Пробелы:_ No perimeter/area of the right triangle (multi-step reuse of the triple). No distance between two points (coordinate Pythagoras — heavily tested). No diagonal of rectangle/square. No 3D space diagonal. No converse 'is it right?'. Only 2 generators. + +**g-area** +- area-rect (L1): w*h; figure rectangle +- area-triangle (L2): a*h/2; figure triangle-base-height +- area-square (L1): a^2; figure square +- area-trapezoid (L3): (a+b)*h/2; figure trapezoid +- area-parallelogram (L2): a*h; figure parallelogram +- area-rhombus (L2): d1*d2/2; figure rhombus + +_Пробелы:_ All are direct forward-formula one-liners. No INVERSE (given area find missing dimension). No composite/L-shape. No shaded region (subtract). No circle sector/segment area (sectors live conceptually in g-circle but area-of-sector belongs here too). Good base coverage of simple polygons, zero multi-step. + +**g-poly** +- poly-angles-sum (L1): 180*(n-2); figure regular-polygon +- poly-regular-angle (L2): 180*(n-2)/n; figure regular-polygon markAngle + +_Пробелы:_ No number-of-diagonals n(n-3)/2. No find-n given interior/exterior angle (inverse). No exterior angle of regular polygon = 360/n. No 'find one angle given the others / given sum'. Only 2 generators. + +**g-sim** +- sim-side (L1): side2 = side1*k; figure two-similar mode:side +- sim-perimeter (L2): P2 = P1*k; figure two-similar mode:perimeter + +_Пробелы:_ No scale factor FROM two sides (k = a2/a1). No area ratio = k^2 (the signature similarity fact). No Thales / parallel-line segment splitting. No map scale (real distance ↔ map distance). No similar-triangle missing side via proportion. Only 2 generators, both forward-multiply. + +**g-circle** +- circ-length (L1): C=2*3.14*r; figure circle show:radius +- circ-diam (L1): C=3.14*d; figure circle show:diameter +- circ-area (L2): S=3.14*r^2; figure circle show:area +- circ-arc (L3): L=(n/360)*2*3.14*r; figure circle-arc + +_Пробелы:_ No sector AREA (S=(n/360)*π*r^2) — only arc length exists. No inscribed vs central angle (inscribed = half central). No chord length (via radius+perpendicular, Pythagorean). No tangent (tangent ⟂ radius, find tangent length). No segment area. Strong on length/area of full circle, missing the angle-theorem family entirely. + +## Новые генераторы + +### `ang-parallel-transversal` — Параллельные и секущая (тема g-angles, L2, compute) +- **Форма:** Given one angle a at a transversal crossing two parallel lines, find the corresponding/alternate/co-interior angle. +- **Пример условия:** Прямые параллельны, секущая образует угол {a}°. Найдите соответственный ему угол (в градусах). +- **Корень-вперёд:** pick a:[30,150] directly as the GIVEN angle. answer is EITHER a (corresponding/alternate, derive val:'a') OR 180-a (co-interior, derive val:'180-a'). Both are clean integers because a is an integer. Use a 'mode' index param relIdx:[0,2] choosing the relation, but simplest is THREE separate generators (corresponding/alternate/co-interior) so each has a single clean derive and its own teaching note. Answer is integer by construction. +- **Фигура:** NEW figure type 'parallel-lines-transversal' in figures.js: two horizontal parallel lines (ln), one slanted transversal crossing both; angleArc marks the given angle a° at the upper intersection and '?' at the relevant angle (lower intersection for corresponding, opposite side for alternate). Reuse ln/angleArc/dot/txt. spec:{given:'a', rel:'corresponding|alternate|cointerior'}. + +### `ang-isosceles-base` — Углы равнобедренного треугольника (тема g-angles, L2, compute) +- **Форма:** Given apex angle, find a base angle (= (180-apex)/2); or given base angle find apex. +- **Пример условия:** В равнобедренном треугольнике угол при вершине равен {a}°. Найдите угол при основании (в градусах). +- **Корень-вперёд:** pick base angle bAng:[20,80] FIRST (the clean integer), derive apex 'a = 180 - 2*bAng' (always integer, and 'val = bAng'). Shown condition gives apex {a}, answer = bAng. No fractions ever because 180-2*bAng is even-difference→integer and we never divide. require: 'a > 0 && a < 140'. +- **Фигура:** NEW figure type 'isosceles' (or extend triangle-angles with iso:true): symmetric triangle, apex angle marked a°, both base angles marked (one '?'), tick marks (small perpendicular ln strokes) on the two equal sides. Reuse pgon/angleArc/dot; add tick helper. + +### `ang-vertical-bisector` — Вертикальные углы / биссектриса (тема g-angles, L1, compute) +- **Форма:** Vertical angles equal; or angle bisector halves an angle. +- **Пример условия:** Биссектриса делит угол {a}° пополам. Найдите половину угла (в градусах). +- **Корень-вперёд:** Bisector: pick half h:[15,80], derive 'a = 2*h' (the shown full angle, always even→integer half), 'val = h'. Vertical-angle variant: pick a:[20,160] directly, val:'a' (vertical angles equal — trivially integer). Two separate clean generators. For bisector the half is integer because a is forced even. +- **Фигура:** Vertical: NEW 'vertical-angles' — two crossing lines (two ln through O), arc on one angle = a°, opposite arc = '?'. Bisector: NEW 'angle-bisector' — vertex O, two rays + a dashed bisector ray (ln dash), full angle a° arc and one half '?'. Reuse angleArc/ln/dot. + +### `pyth-perimeter` — Периметр прямоугольного треугольника (тема g-pyth, L3, compute) +- **Форма:** Perimeter of a right triangle from its two legs (multi-step: Pythagoras then a+b+c). +- **Пример условия:** Катеты прямоугольного треугольника равны {a} и {b}. Найдите его периметр. +- **Корень-вперёд:** Reuse the EXISTING triple construction: pick m>n, derive a='m*m-n*n', b='2*m*n', c='m*m+n*n' (c integer = Pythagorean triple), then val='a+b+c' (sum of integers → integer). Perimeter is clean by construction. Same trick gives an area variant val='a*b/2' (legs of a triple: one leg is even (2mn) so a*b/2 is integer — guaranteed, no require needed). +- **Фигура:** Reuse EXISTING 'right-triangle' with all three sides labelled (no unknown key, or unknown set to none) so all of a,b,c show; the asked quantity (perimeter/area) is text-only like areas do. No new figure. + +### `pyth-distance` — Расстояние между точками (тема g-pyth, L3, compute) +- **Форма:** Distance between two lattice points = sqrt((x2-x1)^2+(y2-y1)^2). +- **Пример условия:** Найдите расстояние между точками A({x1}; {y1}) и B({x2}; {y2}). +- **Корень-вперёд:** pick a triple via m>n: dx='m*m-n*n', dy='2*m*n' (legs), dist='m*m+n*n' (integer). Then pick a base point x1:[-4,4], y1:[-4,4], derive x2='x1+dx', y2='y1+dy'. answer val='m*m+n*n' — integer because the leg deltas form a Pythagorean triple. rhs for check: 'sqrt(({x2}-{x1})^2+({y2}-{y1})^2)'. +- **Фигура:** NEW 'points-distance' figure: a small coordinate cross (two ln axes + minor ticks), plot A and B as dots, dashed segment between them (ln dash), right-angle leg helper (dashed horizontal+vertical legs forming the right triangle), labels A/B and the '?' on the hypotenuse. Reuse fit/ln/dot/txt/rightAngle. + +### `pyth-rect-diagonal` — Диагональ прямоугольника (тема g-pyth, L2, compute) +- **Форма:** Diagonal of a rectangle (or square) = sqrt(a^2+b^2). +- **Пример условия:** Стороны прямоугольника {a} и {b}. Найдите его диагональ. +- **Корень-вперёд:** Reuse triple: a='m*m-n*n', b='2*m*n', diag='m*m+n*n' (integer). Square variant: not a triple (a√2 irrational) → SKIP square unless asking diagonal² ; keep rectangle only so answer stays integer. val='m*m+n*n'. +- **Фигура:** Reuse EXISTING 'rectangle' figure; add a dashed diagonal (extend the rectangle type with spec.diagonal:true → one ln dash corner-to-corner with '?' label). Minimal additive change. + +### `pyth-space-diagonal` — Диагональ прямоугольного параллелепипеда (тема g-pyth, L3, compute) +- **Форма:** Space diagonal of a box = sqrt(a^2+b^2+c^2) (3D teaser). +- **Пример условия:** Измерения прямоугольного параллелепипеда {a}, {b}, {c}. Найдите его диагональ. +- **Корень-вперёд:** Use a Pythagorean QUADRUPLE so the answer is integer: classic small quadruples (a,b,c,d) with a^2+b^2+c^2=d^2, e.g. (1,2,2,3),(2,3,6,7),(1,4,8,9),(2,6,9,11),(4,4,7,9),(2,5,14,15),(2,10,11,15),(6,6,7,11). Store them as an index param: pick idx:[0,N-1], then derive a/b/c/d by a small SimExpr lookup? SimExpr has no arrays → instead make ONE generator per a few hardcoded quadruples via pick of a scaling factor s:[1,4] on a base quadruple (1,2,2,3): a='1*s', b='2*s', c='2*s', d='3*s' → val='3*s' (integer, since scaling a quadruple keeps it a quadruple). Clean by construction. +- **Фигура:** NEW 'space-diagonal-box' figure: a 2D isometric/cabinet sketch of a box (front rectangle + offset back rectangle + connecting ln edges), dashed space diagonal with '?' , three edges labelled a,b,c. Reuse ln/pgon/dot/txt; box drawn from fixed offset vectors scaled to a,b,c. + +### `area-rect-inverse` — Найти сторону по площади (тема g-area, L2, compute) +- **Форма:** Inverse: given area and one side, find the other side (area / side). +- **Пример условия:** Площадь прямоугольника {S}, одна сторона {a}. Найдите вторую сторону. +- **Корень-вперёд:** pick the ANSWER side b:[2,16] and known side a:[2,16] FIRST, derive S='a*b'. Shown: area S, side a; answer val='b' — exact integer because S is an exact multiple of a (S=a*b). rhs check '{S}/{a}'. Inverse reasoning, clean by construction. +- **Фигура:** Reuse EXISTING 'rectangle': label the known side with its number and the unknown side with '?' (extend rectangle to accept an unknown key like right-triangle does, or pass figurePrompt only). Area shown as text/центр label 'S={S}'. + +### `area-l-shape` — Площадь L-образной фигуры (тема g-area, L3, compute) +- **Форма:** Composite L-shape area = big rectangle minus cut-out rectangle. +- **Пример условия:** Найдите площадь фигуры на чертеже (все углы прямые, размеры указаны). +- **Корень-вперёд:** pick outer W:[6,14], H:[5,12] and cut cw:[1,W-2], ch:[1,H-2] with constraint 'cw < W && ch < H'. val='W*H - cw*ch' — product of integers minus product of integers → always integer. The four boundary dimensions read off the figure are all integers. No fractions. +- **Фигура:** NEW 'l-shape' figure: an L polygon built from 6 vertices computed from W,H,cw,ch (notch cut from top-right corner), pgon with edge labels on the outer/inner segments (edgeLabel). Reuse pgon/edgeLabel/dot/fit. Right-angle markers optional. + +### `area-sector` — Площадь сектора (тема g-area, L3, compute) +- **Форма:** Sector area S=(n/360)*π*r^2 (companion to the existing arc-length generator). +- **Пример условия:** Найдите площадь сектора радиуса {r} с центральным углом {n}° (π ≈ 3,14). +- **Корень-вперёд:** Mirror circ-arc exactly: pick r:[2,12], k:[1,8], n='45*k'. require 'mod(k*r*r,8)==0' so (n/360)*r^2 = (k/8)*r^2 is a terminating decimal (×3.14 stays terminating). Simpler safe choice: restrict n to {90,180,270} (k2=[1,3], n='90*k2') so n/360 ∈ {1/4,1/2,3/4} and require 'mod(r*r* (depending),4)==0' — pick r even → r^2 divisible by 4 → quarter is integer ×3.14 terminating. val='3.14*n/360*r^2'. Clean terminating decimal like all g-circle generators. +- **Фигура:** Reuse EXISTING 'circle-arc' figure (it already draws the sector with two radii + highlighted arc + angle + r label). No new figure — same as arc length, just different question text/formula. + +### `poly-diagonals` — Число диагоналей многоугольника (тема g-poly, L2, compute) +- **Форма:** Number of diagonals of an n-gon = n(n-3)/2. +- **Пример условия:** Сколько диагоналей у выпуклого {n}-угольника? +- **Корень-вперёд:** pick n:[4,15]. val='n*(n-3)/2'. n(n-3) is ALWAYS even (consecutive-ish parity: among n and n-3 one is even) → integer for every n, no require needed. Clean integer by number theory. +- **Фигура:** Reuse EXISTING 'regular-polygon'; optionally extend with spec.drawDiagonals:true to faintly draw all diagonals from one vertex (ln dash) as a hint. Minimal additive change or just reuse as-is with n labelled. + +### `poly-find-n` — Число сторон по углу (тема g-poly, L3, compute) +- **Форма:** Inverse: find n given the interior angle (n=360/(180-angle)) or exterior angle (n=360/ext). +- **Пример условия:** Каждый угол правильного многоугольника равен {ang}°. Сколько у него сторон? +- **Корень-вперёд:** pick n:[3,12] FIRST (the integer answer), derive interior 'ang = 180*(n-2)/n' but only keep instances where ang is integer: require 'mod(180*(n-2),n)==0' (same guard as poly-regular-angle). Shown: angle {ang}; answer val='n' — exact integer (it's the picked n). For exterior variant: ext='360/n', require 'mod(360,n)==0', val='n'. Inverse of existing forward generator, answer guaranteed integer. +- **Фигура:** Reuse EXISTING 'regular-polygon' markAngle (show the angle, ask for n). No new figure. + +### `poly-exterior-sum` — Внешний угол правильного многоугольника (тема g-poly, L2, compute) +- **Форма:** Exterior angle of a regular n-gon = 360/n (and sum of exterior angles = 360). +- **Пример условия:** Найдите внешний угол правильного {n}-угольника (в градусах). +- **Корень-вперёд:** pick a DIVISOR of 360 for n so 360/n is integer: pick n from {3,4,5,6,8,9,10,12} via index, or pick n:[3,18] with require 'mod(360,n)==0'. val='360/n' — integer because n divides 360. Clean. +- **Фигура:** Extend 'regular-polygon' to mark an EXTERIOR angle (extend one side with a short ln, arc between extension and next side, label '?') — analogous to triangle-angles ext:true. Or reuse markAngle as a simpler L1 framing. + +### `sim-scale-factor` — Коэффициент подобия по сторонам (тема g-sim, L2, compute) +- **Форма:** Scale factor k from two corresponding sides = bigSide/smallSide. +- **Пример условия:** Треугольники подобны. Сходственные стороны равны {a} и {b}. Найдите коэффициент подобия (большего к меньшему). +- **Корень-вперёд:** pick small a:[2,12] and integer k:[2,5] FIRST, derive b='a*k', val='k'. Shown: sides a and b; answer k='b/a' is the exact picked integer. Clean by construction (b is an exact multiple of a). +- **Фигура:** Reuse EXISTING 'two-similar' mode:side (both bottom sides labelled with numbers, ask k). Tweak: show both side numbers instead of '?'; the '?' / asked quantity is k (text). Minor additive variant of two-similar. + +### `sim-area-ratio` — Отношение площадей подобных фигур (тема g-sim, L3, compute) +- **Форма:** Area ratio of similar figures = k^2; find the second area from the first and k. +- **Пример условия:** Фигуры подобны с коэффициентом {k}. Площадь первой {S}. Найдите площадь второй. +- **Корень-вперёд:** pick k:[2,4] and base area S:[2,20] FIRST, derive val='S*k*k' (area scales by k^2 — integer × integer²). Shown: S and k; answer is exact integer. The signature similarity fact, clean by construction. (Inverse variant: given big area divisible by k^2, find small — pick small first then multiply, same trick.) +- **Фигура:** Reuse EXISTING 'two-similar' mode:perimeter style but label centers 'S={S}' and '?' and the k between them. Add a mode:'area' branch to two-similar (one line, reuses perimeter layout, different prefix label 'S='). Minimal additive change. + +### `sim-thales` — Отрезок по теореме Фалеса (тема g-sim, L3, compute) +- **Форма:** Parallel line cuts proportional segments: find x from a:b = c:x (Thales / similar triangles). +- **Пример условия:** Прямая, параллельная стороне треугольника, отсекает пропорциональные отрезки: {a} относится к {b}, как {c} относится к x. Найдите x. +- **Корень-вперёд:** This is literally the existing proportion trick applied to geometry: pick a, ratio t, derive b='a' no — use the proven prop-x-right construction: pick a:[2,9], b:[2,9], t:[2,9]; c='a*t'; root='b*t'; val='b*t'; rhs check 'b*c/a'. Answer integer because root=b*t is a product of integers. Reuses the bulletproof proportion derive but framed on a Thales figure. +- **Фигура:** NEW 'thales-parallel' figure: a triangle with a line parallel to the base (ln) splitting the two lateral sides; label the 4 segments a,b,c,'?' along the two sides. Reuse pgon/ln/txt/edgeLabel/fit. + +### `sim-map-scale` — Масштаб карты (тема g-sim, L2, compute) +- **Форма:** Map scale: real distance = map distance × scale denominator (or inverse). +- **Пример условия:** Масштаб карты 1:{scale}. На карте расстояние {mcm} см. Найдите расстояние на местности (в метрах). +- **Корень-вперёд:** pick scale denominator from clean values (e.g. scaleK:[1,9] → scale='scaleK*10000' giving 1:10000..1:90000) and map length mcm:[2,12]. Real distance in cm = mcm*scale; in metres val='mcm*scale/100'. Force integer metres: with scale a multiple of 100 this is always integer. val clean. (Inverse: real→map, divide, pick map length first.) +- **Фигура:** No geometric figure strictly needed (text/applied-style) — optionally a tiny ruler/segment sketch with '1 : scale' caption (txt + ln). Could omit figure (geometry topic but scale is numeric). Lowest-priority figure. + +### `circ-inscribed-angle` — Вписанный и центральный угол (тема g-circle, L3, compute) +- **Форма:** Inscribed angle = half the central angle subtending the same arc (and vice versa). +- **Пример условия:** Центральный угол окружности равен {a}°. Найдите вписанный угол, опирающийся на ту же дугу (в градусах). +- **Корень-вперёд:** pick inscribed angle ins:[10,80] FIRST, derive central 'a = 2*ins' (always even→clean), val='ins'. Shown: central {a}; answer = half = ins, exact integer. Inverse framing (given inscribed find central a=2*ins) also clean. No fractions. +- **Фигура:** NEW 'inscribed-central-angle' figure: a circle, an arc endpoints P,Q on it, center O with two radii to P,Q (central angle a° arc) and a point M elsewhere on the circle with two chords M→P, M→Q (inscribed angle '?' arc). Reuse circle drawing + ln + angleArc + dot. Compute M,P,Q on the circle from angles. + +### `circ-chord-pyth` — Длина хорды через радиус (тема g-circle, L3, compute) +- **Форма:** Chord length from radius and distance to center: half-chord, radius, distance form a right triangle → chord = 2*sqrt(r^2-dd^2). +- **Пример условия:** Хорда удалена от центра окружности на {dd} при радиусе {r}. Найдите длину хорды. +- **Корень-вперёд:** pick a Pythagorean triple (m>n): half='m*m-n*n', dd='2*m*n' (distance to center), r='m*m+n*n' (radius=hypotenuse). Then chord = 2*half = 2*(m*m-n*n) → even integer. val='2*(m*m-n*n)'. Shown r and dd; answer integer because (half,dd,r) is a Pythagorean triple. rhs check '2*sqrt({r}^2-{dd}^2)'. +- **Фигура:** NEW 'chord-circle' figure: circle, a chord (ln) not through center, perpendicular from center to chord (ln dash) with rightAngle marker, radius to a chord endpoint (ln), labels r, dd (distance), and '?' on the chord. Reuse circle + ln + rightAngle + dot + txt. + +### `circ-tangent-len` — Длина касательной (тема g-circle, L3, compute) +- **Форма:** Tangent ⟂ radius: tangent length from external point = sqrt(OP^2 - r^2). +- **Пример условия:** Из точки на расстоянии {op} от центра окружности радиуса {r} проведена касательная. Найдите её длину (от точки до точки касания). +- **Корень-вперёд:** pick a triple (m>n): r='m*m-n*n', tan='2*m*n', op='m*m+n*n' (OP = hypotenuse). val='2*m*n' (tangent leg) — integer because (r,tan,OP) is a Pythagorean triple with the right angle at the point of tangency. rhs check 'sqrt({op}^2-{r}^2)'. +- **Фигура:** NEW 'tangent-circle' figure: circle with center O, external point Pt, tangent segment Pt→T (T on circle, ln), radius O→T (ln) with rightAngle marker at T, segment O→Pt (ln dash) labelled op, radius labelled r, tangent '?' . Reuse circle + ln + rightAngle + dot + txt. + +## Новые формулировки условий + +- **READ-FROM-FIGURE only: drop the numeric values from the display text and force the student to read the given angle off the drawing (figurePrompt carries the whole task). Existing generators already have figures; this is a difficulty/level-3 variant of any angle generator.** + - Пример: figurePrompt: 'Найдите неизвестный угол по чертежу (в градусах).' with display omitted — angle a° appears only on the SVG arc. + - Заметка: Engine shows figure + figurePrompt; the answer-check is unchanged (still rhs substitution). Makes the task genuinely geometric, not arithmetic. + +- **Two-step angle chase: combine two facts (e.g. vertical angle THEN supplementary, or parallel-alternate THEN triangle-sum). Root-forward by picking the final answer and back-deriving the shown angle through both steps so it stays integer.** + - Пример: Прямые параллельны. Угол {a}° — один из углов при секущей. Найдите угол треугольника, опирающийся на накрест лежащий с ним (=180-(a)-(b)). + - Заметка: Multi-step is the ЦТ flavour; derive chains keep every intermediate integer. Mark LEVEL 3. + +- **Converse 'is it a right triangle?' — give three integer sides and ask the student to type 1 (yes) or 0 (no). Answer is integerAnswer with rhs computed as a boolean: rhs '(a*a+b*b==c*c)' (SimExpr returns 1/0).** + - Пример: Стороны треугольника {a}, {b}, {c}. Является ли он прямоугольным? Введите 1 (да) или 0 (нет). + - Заметка: Root-forward: half the time emit a genuine triple (m,n) → answer 1; half the time perturb c by ±1 → answer 0. A pick flag yesIdx:[0,1] selects; derive c accordingly. Clean 1/0 answer, fully verifiable by substitution. + +- **Shaded-region subtraction: a shape with a hole/inner shape, area = outer − inner. Generalises the L-shape idea to circle-in-square, square-in-circle (use π≈3.14 so terminating), triangle-in-rectangle.** + - Пример: В квадрат со стороной {a} вписан круг. Найдите площадь закрашенной части (вне круга, π ≈ 3,14). val = a^2 - 3.14*(a/2)^2. + - Заметка: Keep terminating: pick a EVEN so (a/2) integer → 3.14*(a/2)^2 terminating; a^2 integer → difference terminating. Reuse rectangle/square + circle overlay in a new composite figure (or accept a figure-less text version first). + +- **Given the SUM of angles, find n (inverse of poly-angles-sum): n = sum/180 + 2. Pick n first, derive sum=180*(n-2), ask for n. Always integer.** + - Пример: Сумма углов выпуклого многоугольника равна {sum}°. Сколько у него сторон? (val = n, sum=180*(n-2)). + - Заметка: Pure inverse, no new figure (reuse regular-polygon). Pairs naturally with poly-find-n for a full inverse family. + +- **Real-world similar-shadows / heights via proportion (tree-shadow, flagpole): h1/s1 = h2/s2. Reuse the proportion derive (prop-x-* construction) on an applied geometry story.** + - Пример: Дерево высотой {H} отбрасывает тень {s1}. Рядом столб отбрасывает тень {s2}. Найдите высоту столба. (root-forward via t: H=base*t etc., answer integer). + - Заметка: Bridges g-sim with applied word problems; the proportion construction guarantees an integer answer. Figure optional (two similar right triangles via existing two-similar or a simple sun-ray sketch). + +- **Angle in a semicircle / Thales: an inscribed angle subtending a diameter is 90°; combine with triangle-sum to find another angle. Root-forward by picking the other acute angle.** + - Пример: AB — диаметр окружности, C — точка на окружности. Угол A равен {a}°. Найдите угол B. (val = 90 - a, since angle C = 90°). + - Заметка: val=90-a is integer for integer a:[20,70]. Reuse a circle figure with an inscribed right triangle on the diameter; great single-step intro to inscribed-angle theorems (LEVEL 2). + +## Заметки реализации + +All proposals follow existing contracts in generators.js/figures.js/_trainer_engine.js. Every new generator is kind:'compute' (text prompt + lhs:'x'/rhs: for substitution-check) unless noted, exactly like the 30+ existing geometry generators — this is the proven pattern for geometry where the answer is a number, NOT solving for x in a shown equation. Root-forward in geometry = pick the integer answer-driving params FIRST (legs of a Pythagorean triple, the diagonal-count integer, the scale factor, a quotient that is forced even), then derive the rest so the final number is a clean integer or a clean π≈3.14 terminating decimal. require: guards keep answers integer (mod()==0 checks already used throughout — e.g. area-triangle 'mod(a*h,2)==0', circ-arc 'mod(r*k,2)==0', poly-regular-angle 'mod(180*(n-2),n)==0'). Reserved param names t,w,h,pi,e,E,PI,tau avoided (note: existing area-rect uses figure.w/figure.h as FIGURE keys bound to params a,b — figure keys are fine, only PARAM names are reserved; I keep param names like p,q,m,n,k,r,a,b,d). New figure types needed: parallel-lines-transversal, vertical-angles, isosceles, angle-bisector, l-shape, circle-sector, circle-segment, points-distance, space-diagonal-box, polygon-diagonals, thales-parallel, inscribed-central-angle, chord-circle, tangent-circle. Several reuse existing types. No eval/Function; figures render numbers→SVG via TrainerFigures.render. All texts go through esc(). I verified the engine supports integerAnswer and the require/derive/constraint pipeline (instantiate→verifyRoot substitutes the answer and discards non-matching instances, so any clean-integer root-forward construction self-validates). LEVELS map must get an entry for each new id (1=simplest form, 3=multi-step/inverse/composite). + +--- + +# Сквозные направления (детально) + +## task-formats + +### [M] P1. Multiple-choice kind (`kind:'choice'`) with verified distractors mined from analyzeMistake +- **Что:** New answer type: the problem shows the usual statement/equation/figure, but instead of (or alongside) free input, presents 4 tappable options — one correct, three plausible wrong answers. Distractors are GENERATED deterministically from the known error patterns the engine already models (forgot to divide by coefficient → -B; flipped sign → -answer; small arithmetic slip; off-by-one) and are VERIFIED to be != the true answer before display. A generator opts in with `choice:true` (or `kind:'choice'`); existing solve/compute generators get it almost for free. +- **Зачем:** Adds a fundamentally different assessment mode the trainer completely lacks today: RECOGNITION vs PRODUCTION. Multiple-choice is the literal format of ЦТ/ЦЭ part А (А1–А10), so this is exam-realistic, not just variety. Lower friction (tap, no typing) widens the funnel for weaker/5–6-grade students and mobile users. Because distractors come from analyzeMistake, picking a wrong option is diagnostic — the engine already knows WHY that option is wrong and can show the exact same targeted hint it shows for typed answers (e.g. 'похоже, ты не разделил на коэффициент'). +- **Как:** ENGINE (_trainer_engine.js): add `_buildChoices(problem)` that (a) starts from `problem.answer`, (b) derives candidate wrong values reusing `_linAB` + the same offsets as analyzeMistake (-B/A unscaled = A*answer, -answer, answer±1, answer±round(answer*0.1)), (c) filters to finite integers/clean values, dedups, drops any equal to the true answer (tol 1e-6), (d) shuffles with the deterministic `makeRng(seed)`, (e) returns `{options:[{value,label,tag}], correctIndex}` where tag is the mistake type for the chosen wrong option. Attach to `problem.choices` in `instantiate` when `gen.choice`. Grading: add a tiny branch in `checkStudentAnswer` for `kind==='choice'` (input is the chosen value) — but really UI calls a new `checkChoice(problem, idx)` returning {ok, tag}. UI (trainer.html): in `applyInputMode`, when `cur.choices`, hide `tr-answerbox` input row and render a `.tr-choices` grid of buttons (reuse `.tr-btn` styling; selected→disable all, correct→green, picked-wrong→red); on click call checkChoice, then map `tag` to the SAME hint via analyzeMistake-style lookup, then `revealSolution()`/`onSolved()`. No new grammar, no eval. Add `choice:true` to ~8 existing solve/compute generators as a smoke set. +- **Зависимости:** Reuses analyzeMistake/_linAB (engine) and the existing kind-dispatch in applyInputMode/check (trainer.html). No DB/server changes. +- **Риски:** Distractor collision/quality: must guarantee 4 DISTINCT clean values that aren't accidentally also correct — the verify-and-filter step handles this; if fewer than 3 distractors survive, fall back to free input for that instance (graceful). compute-kind 'answer' may be non-integer (decimals/π) — restrict choice generators to integer answers initially. Keep keyboard a11y (arrow/Enter on options). + +### [M] P2. True/False & 'Verify the claim' kind (`kind:'verify'`) using SimExpr boolean evaluation +- **Что:** A new kind where the statement is a mathematical CLAIM (equation, simplification, inequality, or property) that is sometimes true, sometimes false, and the student answers Верно/Неверно. Examples: 'Верно ли, что (x−3)² = x²−9?', 'Верно ли, что 7·6 = 42?', 'Верно ли, что x = 4 — корень уравнения 2x+1=9?'. The generator picks a target truth value, then either emits a genuinely-true claim or perturbs one term to make it false. Verification: the engine compiles the claim as a SimExpr comparison/identity and evaluates it (truth via sampling for identities, substitution for root-claims) — so the engine independently confirms whether the claim is actually true, never mislabeling. +- **Зачем:** Teaches the most ЦТ-relevant skill the trainer can't currently train: ERROR DETECTION and conceptual understanding of identities/properties, not procedure execution. It directly attacks the classic misconception (x−3)²=x²−9 (very common Belarusian-school error) by making the student JUDGE rather than reproduce. Binary answer = fast, mobile-friendly, gameable for streaks. Reuses what already exists: SimExpr comparisons (< <= == etc.) are confirmed supported, exprToLatex already renders cmp/logic nodes, and _sampleEquiv already decides expression equivalence — so the verifier is literally the existing equivalence/substitution machinery. +- **Как:** GENERATOR shape: `kind:'verify'`, a `claim` expr-string using a comparison (e.g. '({a}*x+{b})*1 == {a}*x+{b}' or 'sqrt({a}^2+{b}^2) == {c}'), plus `truthExpr` derived (the engine should not trust the author's label — it recomputes). For identity-claims use `claimLhs`/`claimRhs` + the perturbation: derive a FALSE version by changing one coefficient by ±1. ENGINE: in `instantiate`, for kind verify compute the ground-truth boolean: if claim is an identity over a var → `_sampleEquiv(claimLhs, claimRhs, vars)`; if it's a numeric/root claim → `verifyRoot`-style substitution; store `problem.claimTrue` (boolean) and `problem.latex = exprToLatex(claim)`. checkStudentAnswer branch `kind==='verify'`: normalize input ('верно'/'true'/'1'/'да' vs 'неверно'/'false'/'0'/'нет') → compare to claimTrue. UI: render two big buttons (Верно / Неверно) in place of input; on answer, if wrong, reveal the corrected identity in solution (reuse exprToLatex of the TRUE form). a11y labels in Russian. No eval; all comparison done by SimExpr. +- **Зависимости:** SimExpr comparison/logic (confirmed present), _sampleEquiv + verifyRoot (engine, present), exprToLatex cmp rendering (present). UI two-button pattern shares CSS with P1 choice buttons — sequence P1 then P2 to reuse the option-button component. +- **Риски:** Author could write a claim whose truth is ambiguous (depends on domain, e.g. sqrt) — restrict to polynomial identities and integer/exact numeric claims so sampling is decisive. Must show WHY when student is wrong (give the correct identity) to avoid leaving a misconception. Localization of accepted yes/no tokens. + +### [M] P3. 'Find the error' in a worked solution (`kind:'findError'`) — pick the wrong step +- **Что:** Presents a complete worked solution (the same `solution:[{note,tex}]` array the trainer already renders), but ONE step has been deliberately corrupted (e.g. sign error on transfer, wrong arithmetic, forgot to divide). The student must identify which step is wrong (tap the step). The engine verifies the corruption: it confirms that exactly the intended step breaks equivalence using the existing `checkStep` machinery (the corrupted line fails 'holds in all roots'), while all other steps pass. +- **Зачем:** This is the single biggest METACOGNITIVE leap available: students learn to audit reasoning, which transfers to checking their own work and is exactly what Ц/ЦЭ multi-step problems reward. It reuses the solution data already authored for every generator (no new content authoring) and the `checkStep` equivalence checker that already exists. It also closes the loop with direction C (tutor): the same error taxonomy (sign/nodivide/arith) drives both hints and the injected error, so the platform teaches the error AND tests recognition of it. +- **Как:** ENGINE: add `_makeFindError(problem, rng)`: take the materialized `solution` steps that have a `tex` equation, pick one step index k, and produce a corrupted tex by a deterministic mutation (flip a sign, ±1 on a constant, swap operator) generated as a string; VALIDATE with `checkStep(problem, corruptedTex).ok === false` AND that the original passes — if mutation accidentally stays equivalent, retry with next mutation. Store `problem.solutionFaulty` (steps with one bad tex), `problem.errorIndex`. For kind findError, `instantiate` builds this and sets a `figurePrompt`-like prompt 'Найдите ошибочный шаг'. checkStudentAnswer/`checkFindError(problem, idx)`: ok iff idx===errorIndex. UI: render the faulty solution as a clickable list (reuse `stepHtml`/`.tr-step` markup, each step a button); on pick, highlight chosen vs correct, then reveal the FIXED step inline. Mutation strings are data, evaluated only by SimExpr inside checkStep — no eval. +- **Зависимости:** Requires generators to have multi-step `solution` with `tex` equalities (most solve/proportion/system generators already do). Depends on checkStep (present). Best after P1 to reuse clickable-option UI + after the error taxonomy is stable (direction C). +- **Риски:** Some `tex` steps are display-only ('Получаем корень', proof prose) — restrict injection to steps that parse as an equality via `_splitEq`. Mutation must be guaranteed-detectable (the validate-with-checkStep step ensures this; cap retries, fall back to a different step or skip findError for that instance). Don't corrupt the final 'Проверка' step (would teach wrong checking). + +### [M] P4. Fill-the-blank inside an identity/expression (`kind:'fillBlank'`) +- **Что:** Shows a partially-completed equality with one or more blanks, e.g. '(x + {a})² = x² + ☐·x + {a2}' or '7·6 = ☐', and the student types the missing value/term. Multiple blanks supported (e.g. both middle and last term of a square-of-sum). Each blank has a target expr; correctness checked by SimExpr substitution/equivalence per blank. +- **Зачем:** Bridges recognition (P1/P2) and production: it scaffolds the formulas-сокращённого-умножения topic (already in generators: sq-sum, sq-diff, diff-sq) by asking for just the coefficient that students most often get wrong, rather than the whole expansion. Lower cognitive load than full simplify, great for guided practice (direction C's guided mode) and for 5–6 grade arithmetic ('☐ + 7 = 12'). It reuses the existing `_sampleEquiv` (for term blanks) and `verifyRoot`/numeric compare (for value blanks). +- **Как:** GENERATOR: `kind:'fillBlank'`, a `template` string with `☐1`,`☐2` markers (or reuse {blank} placeholders), and `blanks:[{id, answer:'expr', kind:'value'|'term', vars?}]`. ENGINE: `instantiate` renders the template with params (blanks left as markers), materializes each blank's target value/expr, builds `problem.blanks`. checkStudentAnswer/`checkFillBlank(problem, inputs[])`: for each blank, if kind value → compile input and numeric-compare to target; if term → `_sampleEquiv(input, targetExpr, vars)`. UI: render the template splitting on ☐ markers into a flow of static math (KaTeX via exprToLatex on the literal parts) + small inline `` per blank; reuse keypad targeted at the focused input. Feedback per-blank (green/red). All checks via SimExpr; no eval. +- **Зависимости:** Reuses _sampleEquiv + exprToLatex (engine). UI needs a small 'inline inputs' renderer — new but self-contained in trainer.html. Pairs naturally with formulas/powers/decimals topics that already exist. +- **Риски:** Rendering KaTeX with embedded HTML inputs is fiddly (KaTeX won't host inputs) — implement as alternating spans (math) + inputs (HTML) rather than one KaTeX string. Multiple acceptable forms for a term blank are handled by equivalence sampling, but keep blanks simple (single coefficient/term) to avoid ambiguous answers. + +### [S] P5. Estimation / interval answer (`kind:'estimate'`) — accept any value within a tolerance band +- **Что:** Answer is judged correct if it falls inside a band around the true value, e.g. 'Оцените длину окружности радиуса 7 (π≈3,14)' accepts 43–45, or 'Между какими целыми лежит √50?' accepts the interval [7,8]. The generator declares `tolerance` (absolute or relative) or `interval:[lo,hi]` derived from params; the engine checks membership. +- **Зачем:** Estimation is an explicit ЦТ/ЦЭ skill and a real-world numeracy skill the trainer can't express today (everything is exact). It also unlocks irrational/non-terminating answers (√, π without the 3.14 crutch) without forcing students to type ugly decimals — they estimate or bound instead. Pedagogically it teaches number sense and sanity-checking, complementing the exactness focus everywhere else. +- **Как:** GENERATOR: `kind:'estimate'`, either `answer` + `tol:'0.5'`/`relTol` OR `interval:['floor(sqrt(n))','ceil(sqrt(n))']` (exprs over params). ENGINE: derive band in `instantiate` → `problem.band={lo,hi}` (lo=answer-tol, hi=answer+tol, or interval values). checkStudentAnswer branch `kind==='estimate'`: compile input → ok iff lo<=val<=hi (for single-value estimate) OR for interval-bracket, accept either the pair 'lo;hi' (reuse multi-root parser) or any value strictly inside. Feedback shows the exact value and the accepted band. exprToLatex on the prompt; for bracket-style, answerLabel shows '[lo; hi]'. SimExpr only. +- **Зависимости:** Smallest engine change — one new check branch + band materialization. Reuses checkStudentAnswer numeric path. No DB/server. +- **Риски:** Must distinguish 'value within band' from 'bracket pair' modes clearly in UI placeholder so students know what to enter. Tolerance must be wide enough to be meaningful but narrow enough to require real estimation — tune per generator. Don't use for topics where exactness is the learning goal (would undermine rigor). + +### [M] P6. Multi-step chained sub-answers (`kind:'multi'`) — one problem, several gated inputs +- **Что:** A single rich problem asks for several intermediate quantities in sequence, each verified before the next unlocks, e.g. movement: 'Найдите путь за 1 ч → за всё время → во сколько раз больше'; or geometry: 'периметр → площадь → отношение'. Each sub-answer is its own expr target; the student progresses step-by-step, mirroring multi-mark ЦЭ tasks. +- **Зачем:** Real ЦТ/ЦЭ part В tasks are multi-step; the current trainer only ever asks ONE quantity. Chaining trains decomposition and lets a single generated context (a figure, a word scenario) yield 2–3 graded answers, multiplying content value per generator. Progressive gating gives strong scaffolding and clear partial-credit signal (how many sub-answers correct) feeding mastery/analytics. +- **Как:** GENERATOR: `kind:'multi'`, `parts:[{prompt, answer:'expr', integerAnswer?, tex?}]`. ENGINE: materialize each part's target in `instantiate` → `problem.parts` (each self-verified by substitution like compute). New `checkPart(problem, i, input)` reusing the compute numeric check. UI: render parts as a vertical stepper — only the current part's input is active; on correct, lock it (show answer), reveal next part's prompt; final part triggers onSolved. Progress = parts solved. Mastery: count the problem correct only if all parts correct (or weight). Reuse existing input row, just re-pointed at the active part. SimExpr verifies each part; no eval. +- **Зависимости:** Reuses compute-style numeric verification. UI stepper is new but reuses tr-input + feedback. Synergizes with figure generators (one figure, several questions about it) and word/applied topics. +- **Риски:** Progress/analytics model assumes 1 attempt = 1 skill datapoint; multi-part needs a decision (record per-part vs per-problem) — keep it simple: record the whole problem as one attempt, store parts-correct count locally. Don't let parts depend on the student's wrong intermediate (always use the TRUE intermediate for downstream prompts so one slip doesn't cascade). + +### [M] P7. Ordering / sorting kind (`kind:'order'`) — arrange values or steps in correct sequence +- **Что:** Student reorders a shuffled list into the correct order: numbers ascending (incl. negatives/fractions/decimals — directly serves the negatives/decimals/fractions topics), or solution steps into logical order, or magnitudes (compare powers: 2^5 vs 3^3 vs 5^2). The engine knows the canonical order (numeric sort of materialized values, or authored step order) and checks the permutation. +- **Зачем:** Ordering is the cleanest way to assess COMPARISON skills (ordering negatives/fractions is a core 5–6 grade ЦТ topic the trainer currently can only poke at via single compute answers). Drag/tap-to-order is engaging and mobile-native. For step-ordering it reinforces solution structure (complements find-the-error). High variety-per-effort: any list of values becomes a sortable item. +- **Как:** GENERATOR: `kind:'order'`, `items:['expr1','expr2',...]` (exprs over params) + `by:'asc'|'desc'` (numeric) OR `sequence:[...]` for fixed step order. ENGINE: materialize item values, compute canonical order (numeric sort for by, or authored index for sequence), store `problem.items` (shuffled, deterministic) + `problem.order` (correct index permutation). `checkOrder(problem, perm)`: compare student permutation to canonical (allow ties for equal values). UI: render items as a vertical list with up/down buttons or HTML5 drag; on submit, color correctly-placed vs misplaced. SimExpr evaluates item values; sort done in JS on the numbers; no eval. +- **Зависимости:** Engine: small materialize+sort+permutation-check. UI: reorderable list component (up/down arrows are simplest, no drag lib). Pairs with negatives/decimals/fractions/powers topics already present. +- **Риски:** Equal values (ties) need tolerance in the permutation check. Drag a11y — provide keyboard up/down as primary. Keep list length 3–5 to stay tractable on mobile. + +### [L] P8. Matching kind (`kind:'match'`) — connect formulas to results / shapes to areas +- **Что:** Two columns; student matches each left item to its right counterpart, e.g. expression ↔ simplified form, formula ↔ name, figure ↔ area formula, percent ↔ value. The generator produces N true pairs (each verified) plus the right column shuffled; the engine checks the matching. +- **Зачем:** Matching tests ASSOCIATION/FLUENCY across a topic at once (e.g. all three squared-formulas in one screen), which single-answer items can't. It's efficient revision before an exam and naturally consolidates a whole skill family per problem. Reuses _sampleEquiv to verify expression↔form pairs so a 'right answer' is never a mislabeled equivalent. +- **Как:** GENERATOR: `kind:'match'`, `pairs:[{left:'expr/text', right:'expr/value', verify?:'equiv'|'value'}]`. ENGINE: materialize both sides, optionally verify each pair (equiv via _sampleEquiv / numeric via compile) and DROP/retry any pair where left≠right under its verify rule, ensure right values are mutually distinct (else matching is ambiguous), shuffle right column deterministically, store `problem.left`, `problem.right`, `problem.match` (correct index map). `checkMatch(problem, mapping)`. UI: two columns; tap a left then a right to link (draw/colour pairing), or dropdown per left row (simplest, a11y-friendly). On submit, mark each link. SimExpr verifies pairs; no eval. +- **Зависимости:** Engine pairing+verify+distinctness logic. UI is the heaviest new component (linking interaction) — do after P1/P7 establish option/list UI patterns. Reuses _sampleEquiv. +- **Риски:** Ambiguity if two right items are equal — the distinctness filter prevents it. Linking UI complexity on mobile (prefer per-row dropdowns over canvas lines). Keep N=3–4 pairs. + +### [S] P9. Real-life context wrappers for existing compute generators (data-driven `context` templates) +- **Что:** Not a new kind — a thin layer that wraps existing compute/solve generators in varied real-life narratives chosen at random from a small template pool (shopping, travel, recipes, sports, school budget), changing only the DISPLAY text while the math/verification is unchanged. E.g. the percent-of generator can render as 'скидка на куртку', 'налог', 'процент учеников'. Templates are pure data with {param} placeholders. +- **Зачем:** Massively multiplies perceived variety and engagement with near-zero math risk: the same verified generator feels fresh and connects abstract skills to life (a known motivation/retention lever, and a ЦЭ word-problem skill). It's the cheapest way to make the bank feel large without authoring new math. Directly advances roadmap B4 (word-problem families) using the safe parametric path rather than LLM. +- **Как:** GENERATOR: add optional `contexts:[ 'Текст с {param}...', '...' ]` to existing compute generators. ENGINE: in `instantiate`, if `gen.contexts`, pick one deterministically with the rng and use it as the `display` (after `render`), keeping lhs/rhs/answer untouched so verification is identical. Escaping already happens in UI. Provide a shared CONTEXTS map for common shapes (percent/movement/area→tiling/fencing). Zero change to checking; only `display` differs. +- **Зависимости:** None beyond editing generators.js. Fully reuses render()+existing compute verification. Independent of all other proposals. +- **Риски:** Narrative must stay mathematically faithful to the formula (e.g. 'скидка' must mean the discounted-price computation, not the discount amount) — review each template against its generator's rhs. Keep language neutral and escaped. Avoid context that changes units/answer semantics. + +### [M] P10. Data-from-figure / table question mode (`figureAsk`) — read the value off the drawing +- **Что:** Extends the existing 'figure as data' system so the ANSWER itself must be read from the figure, and add a simple TABLE figure type. Today figures illustrate given values; this mode hides one labeled value behind the figure (already partly done via 'unknown'/'?') and adds questions like 'Сколько сторон у многоугольника на чертеже?' or a small data table ('по таблице найдите...'). Adds a `table` type to figures.js rendering rows/cols of values. +- **Зачем:** ЦТ/ЦЭ frequently give data in a figure or table and ask students to extract+compute — a literacy skill the trainer's text-first format underweights. It deepens the already-built TrainerFigures investment (13 types) and makes geometry/statistics-style items more authentic. A table type also opens averages/min/max/sum mini-topics (future content) cheaply. +- **Как:** FIGURES (figures.js): add a `table` TYPE rendering an SVG grid from `rows`/`cols`/`cells` (cells = param-bound values, escaped), styled like existing figures (white strokes on dark scene). For figure-read questions, the generator already supports `figurePrompt` (shown when reading from drawing) — extend chooseGen/showStatement path in trainer.html so a generator can declare `readFromFigure:true` to default to the figure prompt and require the answer that's marked '?' on the drawing. ENGINE unchanged for the math (still compute/verifyRoot). Add 2–3 generators: count-sides-from-polygon, value-from-table (e.g. 'найдите сумму чисел в таблице'), all verified by SimExpr over the same params that drew the table. +- **Зависимости:** Builds on figures.js render contract (U utils) and trainer.html figure toggle (renderFigure/renderFigureToggle/figurePrompt) — all present. No new kind needed (compute). No server/DB. +- **Риски:** SVG table layout must stay readable on small screens and on the colour-shifting scene (verify contrast). Ensure the asked value is genuinely deducible only from the figure/table (don't also print it in display). Keep tables tiny (≤4×3). + +_Grounded in real code: kinds are dispatched in `_trainer_engine.js` (`instantiate` builds problem.kind; `checkStudentAnswer` switches on kind: simplify/roots/inequality/system, default=solve/compute). UI dispatch in `frontend/trainer.html`: `applyInputMode()` (line ~924) sets placeholder by kind; `isLabelKind()` (946); `answerLabel()` (939); `check()` (1073) grades and calls `analyzeMistake` for hints; `revealSolution()` reuses `cur.solution`. Adding a kind = (1) materialize branch in `instantiate`, (2) check branch in `checkStudentAnswer`, (3) UI branch in `applyInputMode`/`check`/`answerLabel`. SimExpr ALREADY supports comparisons (< <= > >= == !=) and logic (&& ||) — confirmed in `_sim_expr.js` lines 117-219 — and `exprToLatex` already renders `cmp`/`logic` AST nodes (`_latex` case 'cmp'/'logic'). This is the keystone enabling true/false, verify-claim, and choice kinds WITHOUT new grammar. + +Key reuse assets: `analyzeMistake` (engine) already computes wrong-answer values for nodivide/sign/arith — perfect distractor source for multiple-choice. `verifyRoot`/`_sampleEquiv`/`_checkInequality`/`_checkSystem` already verify every kind by substitution/sampling. `checkStep` already validates equivalence steps (basis for "find-the-error" and "fill-the-blank in identity"). `render()` does {param} substitution. Generators carry `solution:[{note,tex}]` (basis for find-the-error worked solutions). `display`/`figure`/`figurePrompt` already deliver text/figure context. + +INVARIANTS respected in every proposal: no eval (SimExpr only), distractors/answers verified deterministically so a "correct-looking wrong option" can never accidentally equal the truth, no emoji, all student-visible text escaped (engine already escapes in trainer.html via esc()), reserved param names avoided. + +Prioritization rationale: P1/P2 (choice, true/false) are highest value + lowest risk because they reuse analyzeMistake + SimExpr-comparison and add an entirely new ASSESSMENT MODE (recognition vs production) that current trainer lacks. P3 (find-the-error) is the deepest pedagogical leap (metacognition, ЦТ-style) but M effort. Fill-the-blank, estimation, matching, multi-step, real-life wrappers follow. + +Files touched per proposal are concrete; effort estimates assume the existing 3-touchpoint pattern (engine instantiate + engine check + trainer.html UI)._ + +## pedagogy-tutor + +### [S] P1. Three-level escalating hints (nudge -> first step -> full solution) +- **Что:** Replace the single 'Подсказка' button behavior with a deterministic 3-tier escalation that reuses each generator's existing solution[] array: Tier 1 = a NUDGE (the .note of step[0] only, no formula); Tier 2 = FIRST STEP (note + tex/KaTeX of step[0]); Tier 3 = FULL SOLUTION (existing revealSolution). The button label/counter advances each click (Намёк -> Показать шаг -> Решение целиком). Track a per-problem hintLevel (0..3). Crucially, on a WRONG answer stop dumping the full solution immediately (current check() calls revealSolution() on first miss) — instead show the targeted analyzeMistake hint and re-enable the hint button so the student can escalate, preserving productive struggle. +- **Зачем:** Faded scaffolding with productive struggle is the highest-impact evidence-based tutoring change. Today a learner gets either nothing or the entire answer on the first mistake, training help-seeking dependence and removing learning. Graduated hints unstick weak students without spoiling strong ones. Pure UI/data reuse of already-verified solution[] means zero new math-correctness surface. +- **Как:** frontend/trainer.html only. Add `var hintLevel=0;` reset in newProblem(); rewrite the $('tr-hint') click handler (~line 1315) to switch on hintLevel: Tier1=esc(sol[0].note), Tier2=stepHtml(sol[0],1) (note+latex via existing kat()), Tier3=revealSolution(); update button caption per tier. In check() (~line 1097) remove the unconditional revealSolution() on wrong — keep the analyzeMistake line, re-enable hint button to escalate. Step mode routes through the same counter. exprToLatex/kat already exist; no engine/server change. +- **Зависимости:** None (uses existing solution[], stepHtml, kat, analyzeMistake). +- **Риски:** Solution no longer auto-appears on first wrong answer — keep the 'Решение' (give-up) button prominent. Some generators have only 2 solution steps (e.g. pct-of) — Tier2/3 collapse gracefully (guard idx). + +### [M] P2. Per-topic common-mistake library with targeted feedback (extends analyzeMistake) +- **Что:** Add a data table MISTAKES keyed by generator id/kind, each entry = { predict: '', hint, tag }. On a wrong numeric answer the engine evaluates each predicted 'wrong value' (e.g. ax+b=c: forgot-to-divide = c-b; sign-flip = -root; added-instead-of-subtracted; for inequality: didn't-flip-sign; proportions: cross-multiplied wrong pair; percents: multiplied by p not p/100; powers: added bases / multiplied exponents; quadratic: only one root / both signs wrong) and if the student's value matches within tolerance, returns that entry's specific hint. Extends analyzeMistake (currently only a generic linear reconstruction for solve/compute) to quadratic/inequality/proportions/percents/powers; falls back to current generic heuristics. +- **Зачем:** Generic 'pretty close, recompute' feedback teaches nothing. Naming the EXACT misconception ('ты не поменял знак неравенства при делении на отрицательное') is the heart of formative feedback and precisely where ЦТ students lose marks. Because predicted-wrong values are computed by the SAME deterministic SimExpr engine from the SAME params, every diagnosis is exact and offline (no LLM, no model false-positives). +- **Как:** Add MISTAKES data + matcher in _trainer_engine.js beside analyzeMistake. Matcher builds env from problem.params (already stored), evals each predict via compileExpr, compares to student value with the same tolerance as checkStudentAnswer; skip any predict equal to problem.answer. Extend analyzeMistake to consult MISTAKES[problem.genId] first, then current linear logic. Author 3–6 entries for high-traffic generators (linear-eq, inequalities, proportions, percents, quadratic, powers). Headless smoke (vm + real _sim_expr + engine): each predicted wrong value triggers its hint; correct answer triggers none. +- **Зависимости:** P1 desirable (targeted hint sits inside the escalation flow instead of next to a full reveal). +- **Риски:** Predicted wrong value could coincide with the correct answer for degenerate params — guard by skipping. Tolerance must mirror checkStudentAnswer to avoid mis-tagging. Keep table curated to limit authoring + false matches. + +### [M] P3. Worked-example-first mode (study an example, then practise the twin) +- **Что:** Per skill, before the first practice problem of a freshly-opened un-mastered skill, instantiate ONE problem and render its full solution[] as a read-only worked example (statement + all KaTeX steps + the 'Проверка' step) with a 'Понятно, попробую сам' button that then serves a NEW (different-params) instance to solve. Shown automatically the first time a skill is encountered in a session; thereafter on demand via an 'Образец' button. +- **Зачем:** Worked-example effect: for novices, studying a fully worked solution before solving lowers cognitive load and outperforms problem-solving alone. The trainer already computes flawless step-by-step solutions for every generator but only shows them as punishment after failing — this turns a remediation asset into instruction, especially for level-3 structural generators. +- **Как:** frontend/trainer.html: showWorkedExample() calls TE.instantiate(curGen) for a sample, renders solutionHtml('Образец') in a dedicated panel (reuse stepHtml/kat), and on confirm calls newProblem() for a distinct seed. Trigger in newProblem()/boot when prog[skill] is empty (never attempted) and a session Set hasn't shown it yet. Add button next to the difficulty control. No engine/server change. Optional: always show for level-3 generators. +- **Зависимости:** None (reuses solutionHtml/stepHtml). Pairs with P7 (spaced re-surfacing). +- **Риски:** Don't force on fluent/returning users (mastered) — annoyance; track 'example seen' in session state only. Ensure the practice twin re-seeds so it isn't a copy. + +### [M] P4. Guided fill-in-the-blank steps (reuse checkStep as the validator) +- **Что:** A scaffolded mode that renders the generator's solution as step EQUATIONS with the RHS (or a key sub-expression) blanked; the student types only the missing piece per blank. Each entry is validated by the EXISTING checkStep primitive (it already verifies a typed equality is equivalence-preserving for the problem) — accepted if the reconstructed line passes checkStep, rejected with the same 'не равносильно / тождество' diagnostics. Progresses blank-by-blank to the answer. +- **Зачем:** Completion/fill-in-the-blank problems are the proven bridge between studying a worked example (P3) and unscaffolded solving — they isolate one cognitive step and give immediate located feedback. It converts the strong-but-underused free-form checkStep box into a guided experience needing little input fluency, lowering the barrier for weak students. +- **Как:** frontend/trainer.html: guided renderer over cur.solution[]: for each equality step present 'LHS = [____]'; on submit reconstruct the full line and call TE.checkStep(cur, fullLine); accept on status equivalent|solved else show r.message. Reuse buildKeypad per blank. If cleaner, add a thin TrainerEngine.checkBlank(problem, lhs, studentRhs) wrapper around checkStep. Headless smoke: correct blank passes, off-by-one fails 'wrong', identity flagged. +- **Зависимости:** checkStep (exists). Best after P3 (example -> guided -> free ladder). Applies to kinds whose steps are equalities (solve; verify compute where lhs:'x'). +- **Риски:** Prose-only steps (tex:'') aren't blanks — skip them. Systems have empty step tex — restrict guided mode to generators with clean single-equality steps (filter at render). Verify checkStep behavior for compute before enabling there. + +### [M] P5. Hint-aware mastery (record assistance so 'освоено' means unaided) +- **Что:** Extend the attempt record with an 'assisted' flag (true if the student opened any hint tier >=1, used guided mode, viewed the worked example for THIS instance, or used LLM explain before answering). submitAttempt accepts it; the mastery streak (MASTERY_STREAK=5) counts only UNASSISTED correct answers toward 'mastered' while still crediting solved/attempts. Optionally surface a separate clean-correct 'fluency' indicator in the rail ring. +- **Зачем:** Mastery that counts hinted/spoiled solves overstates competence and corrupts both the adaptive engine and the class heatmap (a student who needed the full solution each time shows as 'mastered'). Distinguishing aided vs unaided is the minimum for a credible mastery signal and honest teacher analytics, and it safeguards the integrity of P1–P4 (more help must not inflate mastery). +- **Как:** Backend practiceController.submitAttempt: accept optional boolean b.assisted; cleanest = migration adding practice_progress.clean_streak, mastered = clean_streak>=5; assisted-correct increments solved/attempts/box but resets clean_streak (or leaves it). Frontend: per-problem `assisted` flag set by hint/guided/example/LLM handlers; pass via LS.practiceSubmit (extend api.js wrapper + call site at submitAttempt ~line 989). Backend test: assisted-correct keeps solved++ but never reaches mastered; clean run does. +- **Зависимости:** P1/P3/P4 (they set the flag). Small DB migration (next number) + api.js wrapper change. +- **Риски:** Migration backward compat: existing rows default clean_streak 0 (re-earn mastery — acceptable, or backfill = cur_streak). Scope 'assisted' to the current problem only (don't punish peeking at a different instance's example). Route already auth-only — lint:routes stays baseline 0. + +### [M] P6. Socratic 'explain my error' with anti-cheat gating (extend existing LLM explain) +- **Что:** Upgrade the already-wired /api/practice/explain into a Socratic tutor: (a) add a 'socratic' mode returning a guiding QUESTION instead of the fix (system-prompt change only); (b) gate the help flow so request 1 -> Socratic question, request 2 -> first step, request 3 -> full explanation (escalation mirrors P1, server- and client-enforced); (c) anti-cheat: rate-limit explain calls per user/problem and refuse to 'just give the answer'. Always fall back to the deterministic P1/P2 hints when the LLM is off (already partially done). +- **Зачем:** Socratic questioning ('что происходит со знаком неравенства при делении на отрицательное?') produces deeper learning than told answers and is the natural anti-cheat posture: the model interrogates rather than hands over the solution. The grounding architecture (model is GIVEN the verified answer/steps and only explains) already makes this safe; we tighten pedagogy + add escalation/throttle, not new attack surface. +- **Как:** practiceExplainService.js: add mode 'socratic' with a system prompt demanding ONE guiding question and forbidding stating the answer/steps. practiceController.explainProblem: accept mode 'socratic'; lightweight per-user+problem counter mapping request N -> mode and throttling (429 past cap). Frontend aiExplain(): track requestCount per problem, send escalating mode, mark assisted=true (feeds P5); keep graceful offline fallback to revealSolution. Tests: socratic returns text and never contains the literal answer string when fed a fake model; throttle past cap returns 429. +- **Зависимости:** Existing callLLMFailover / practiceExplainService. P5 for assisted flag; P1 for matching offline tiers. +- **Риски:** LLM cost/availability — escalation+throttle bound calls; deterministic fallback guarantees graceful degradation. Output is escaped text only (no eval). A weak model could leak the answer in socratic mode — add a server-side check stripping/blocking the answer token from socratic output. + +### [L] P7. Spaced retrieval of worked examples (method-recall mini cards) +- **Что:** Periodically re-surface a previously-studied worked example as a RETRIEVAL prompt: show a past example's statement and ask the student to recall the FIRST step (type it, or pick from 3 options) before revealing it. Schedule via the Leitner box already stored per skill in practice_progress (due skills get an example-recall card mixed into the adaptive session), so the METHOD is spaced, not just the final answer. +- **Зачем:** Spacing + retrieval practice are the two most robust memory effects; the trainer already spaces SKILL practice (Leitner box, INTERVAL_DAYS) but only ever asks for the final answer. Re-testing the first step of a worked example combats the 'understood the example but can't start the problem' gap and reinforces procedure recall cheaply, reusing solution[] and the due signal already computed. +- **Как:** Reuse adaptive.js due detection (prog[skill].due) + INTERVAL_DAYS. Frontend: when nextSkill surfaces a due skill, occasionally render an example-recall card: TE.instantiate(gen) -> show statement + blanked first step; accept via TE.checkStep or a 3-option picker built from the real step plus two distractors = P2's predicted wrong values (plausible-but-wrong, deduped). Mark recall as non-mastery-graded (or light credit). No new table for v1 (rides the skill Leitner box); a dedicated example_reviews table is a later upgrade for per-example scheduling. +- **Зависимости:** P2 (distractors), P3 (examples), P5 (assisted accounting), checkStep. Heaviest; do last. +- **Риски:** Cap review-card frequency and allow skip to avoid session overload. Distractors must differ from the correct step (dedupe). Moving to per-example scheduling later needs a migration; v1 deliberately avoids it. + +## adaptivity-mastery + +### [S] D5. Record structural level (and solve time) per attempt — the missing data foundation +- **Что:** Extend POST /api/practice/attempt to accept optional { level (1..3), time_ms } alongside { skill, correct }. Store level on the row that drove the box change and persist a lightweight rolling signal (last_level, best_level_mastered, avg/last time_ms). Client already knows the structural level at answer time (levelOf(curGen) / diffMode). +- **Зачем:** Today the server stores ONLY {skill, correct}. Mastery, difficulty calibration, forgetting-curve and the level-3 mastery rule are ALL impossible to compute without knowing at which structural level a correct answer was produced, and without any timing signal. This single additive change unblocks D3/D6/D7 with almost no risk and no behavior change for old clients. +- **Как:** Migration 085: ALTER practice_progress ADD COLUMN last_level INT DEFAULT 0, ADD COLUMN best_level INT DEFAULT 0 (max level ever solved correctly), ADD COLUMN l3_streak INT DEFAULT 0 (consecutive correct at level 3), ADD COLUMN avg_ms INT DEFAULT 0. In practiceController.submitAttempt: read b.level (clamp 1..3, default 0/ignore), b.time_ms (Number.isInteger, clamp), update best_level=max, l3_streak (++ if correct && level===3 else 0 on wrong / unchanged on lower-level correct), exponential-moving avg_ms. js/api.js practiceSubmit(skill, correct, opts) -> include level/time_ms. trainer.html submitAttempt(correct) passes { level: levelOf(curGen), time_ms } (start a per-problem timer in newProblem). Keep all new fields OPTIONAL so existing tests/clients pass unchanged. +- **Зависимости:** None. Reuse migration pattern of 082; node:sqlite db.prepare style already in controller. +- **Риски:** Schema drift — keep columns nullable/default 0 so backend/tests/practice.test.js stay green. Don't change the response shape's existing keys (additive only). EMA on avg_ms must guard against time_ms=0/NaN (do not pollute average). + +### [M] D3. Tie mastery to a structural level-3 streak, not a level-1 streak +- **Что:** Redefine 'mastered' so it requires a streak of correct answers at structural level 3 (or the generator's max available level), instead of any-level cur_streak>=5. Show a two-stage state: 'практикует' (correct at low level) vs 'освоено' (L3 streak met). Surface in the skill badge and the mastery ring. +- **Зачем:** The current mastery is gameable: a student can grind only the easiest variant (lin-basic, level 1) and earn the mastery star, then nextSkill()'s progression tier marks the skill done and stops giving harder forms. Mastery should mean 'can do the exam-shaped structural form', which is exactly what the LEVELS map encodes. This is the single biggest pedagogical fidelity fix in this direction. +- **Как:** Server: in submitAttempt set mastered=1 only when l3_streak (from D5) reaches a threshold MASTERY_L3_STREAK (e.g. 3). For skills whose max LEVELS value is <3, accept best-available level (server needs the skill's max level — pass it from client as gen.maxLevel, OR keep a small server-side allowlist; prefer client-passed maxLevel for data-driven generators). Keep a second flag practiced=1 when cur_streak>=N at any level for partial credit. Client: skillBadge() shows half-star/full-star using practiced vs mastered; topicMastered() requires mastered. adaptive.js nextSkill() progression tier (prog1 filter, line 52) must treat a skill as 'not yet done' until mastered (L3), so the engine keeps escalating difficulty. Update updateOverall() ring to count L3-mastered. +- **Зависимости:** D5 (needs level in attempt). +- **Риски:** Changing the mastery definition shifts existing students' counts (a one-time visible drop in 'mastered N'). Mitigate by computing both flags and labeling clearly. Ensure pickByLevel can actually reach level 3 in the topic (some topics top out at level 2 — clamp threshold to topic's max level, already done by pickByLevel's clamp logic). Keep MASTERY_STREAK constant exported for backward-compat in the response. + +### [M] D7. Calibrate difficulty in Auto mode by recent accuracy and time +- **Что:** Make the 'Авто' difficulty adaptively pick the structural level per skill: start low, step up after a short correct streak at the current level, step down after wrong answers or very slow solves, instead of always using the generator's static LEVELS value. +- **Зачем:** Currently 'Авто' just uses each generator's fixed level (renderDifficulty shows 'Авто · ур.N' from levelOf(curGen)). A struggling student gets stuck at the static level; a strong student is under-challenged. Per-skill calibration is the classic adaptive-practice loop and directly improves both engagement and the validity of the D3 mastery signal (they actually reach L3 by being pushed there). +- **Как:** Add a pure helper in adaptive.js: calibrateLevel({ skill, progress, sessionStreakAtLevel, lastTimeMs, avgTimeMs, maxLevel }) -> level 1..3. Rules: bump level after K consecutive correct at current level (K~2), drop after a wrong or when lastTimeMs > 2.2*avgTimeMs at current level; clamp to [1, maxLevel]. trainer.html: when diffMode==='auto' && !pinned, call chooseGen() through pickByLevel(curTopic, calibrateLevel(...)) instead of returning the static-level generator. Persist current calibrated level per skill in localStorage (key trainer-cal-) and seed from prog best_level (D5). Track per-session sessionStreakAtLevel in the existing session vars. +- **Зависимости:** D5 (time + best_level seed). Reuses pickByLevel() and the existing diffMode==='auto' branch in chooseGen()/advance(). +- **Риски:** Over-eager level changes feel jumpy — require a streak before bumping and a cooldown. Manual difficulty selection (diffMode 1/2/3) and pinned must override calibration (already the precedence in chooseGen). Time signal is noisy (student steps away) — cap the slow-solve penalty and ignore time_ms beyond a ceiling. + +### [M] D1. Skill prerequisite graph with unlock gating (Quantik-style) +- **Что:** Add a data-only prerequisite map (skill -> required prerequisite skills) and gate skills/topics so a skill is 'locked' until its prerequisites are mastered (D3). Locked skills render dimmed with a lock badge; adaptive progression never jumps to a locked skill. Mirrors Quantik's unlockStars. +- **Зачем:** The trainer currently has only implicit order-based progression within a topic; cross-topic it can hand a student 'lin-paren-both' (level 3, brackets both sides) or quadratics before the prerequisite linear/percent skills are solid. A real skill graph prevents 'hard without base' jumps and produces a coherent learning path — exactly the unlock model already proven in Quantik (map.js isUnlocked / unlockStars). +- **Как:** New data module frontend/js/trainer/skill-graph.js: window.TrainerSkillGraph = { PREREQS: { 'lin-paren-both': ['lin-basic','lin-paren'], 'sys-2x2': ['lin-both-sides'], 'quad-factored': ['quad-diff','simp-expand'], ... }, isUnlocked(skill, progressMap) } — unlocked iff every prereq is mastered (reuse the D3 mastered flag). Default: skills with no entry are unlocked. adaptive.js nextSkill() filters scope to unlocked skills in tiers 3/4 (progression/retention). trainer.html: renderSkills() adds 'tr-locked' class + ICON.lock + tooltip 'Сначала освойте: '; clicking a locked chip shows the requirement instead of pinning. Keep it advisory-not-blocking for manually-chosen topics if desired (config flag) to avoid frustration. +- **Зависимости:** D3 (mastery = unlock currency). Pattern reuse from frontend/js/game/progress-logic.js isUnlocked. +- **Риски:** A wrong/too-strict graph can soft-lock content; ship a conservative graph (only the few clear dependencies) and add a CLAUDE-style invariant test that the whole graph is reachable (no cycles, every skill eventually unlockable). Don't gate the teacher-assigned topic flow against the assignment intent. No DB change — graph lives in client data. + +### [L] D2. Entry diagnostic mini-test -> personalised starting plan +- **Что:** A one-time (or on-demand) diagnostic: ~1 representative problem per key skill at a probing level, no hints/solutions during it. From the results, seed each skill's progress (mark probable-known skills, set initial calibrated level via D7) and produce a 'персональный план' — an ordered list of weak skills the smart session targets first. +- **Зачем:** New students currently start from skill #1 every time regardless of what they already know — wasting strong students' time and never quickly surfacing a weak student's true gaps. A short diagnostic gives an immediate personalised path, increases perceived intelligence of the trainer, and feeds D7 with a real starting difficulty per skill. +- **Как:** New diagnostic mode in trainer.html (a session variant): build a fixed list of ~12-16 skills (one per topic/key skill), instantiate one problem each at level 2, suppress hints/solution, collect {skill, correct, time_ms}. On finish, POST each as a normal attempt (D5 payload) so progress/box/level seed naturally, and compute a plan = skills ordered by (not-correct, then slow) intersected with D1 unlock order. Persist plan to localStorage trainer-plan and a 'diagnostic_done' flag (or a tiny migration field if cross-device persistence is wanted). The smart session's pickNext prioritises plan-weak skills (new tier above progression). Reuse showSummary UI for the diagnostic result screen. +- **Зависимости:** D5 (record level/time), D7 (seed calibrated level), D1 (order plan by unlock graph). Optional migration if persisting server-side. +- **Риски:** Diagnostic fatigue — keep it short (<=16 items) and skippable. One sample per skill is low-confidence; treat results as a soft prior (initial box/level), not a hard 'mastered'. Must not award the mastery star from a single diagnostic correct (only seed practiced/level, never mastered). + +### [M] D6. Forgetting-curve due-mix blended into every session (not a fallback tier) +- **Что:** Reserve a fraction of each smart session for due/overdue skills (Leitner due_at passed) interleaved with new progression, instead of only surfacing due skills when nothing else qualifies. Add a per-session due budget and an 'overdue urgency' so long-overdue skills are prioritised. +- **Зачем:** box/due_at (migr 082) already implement spaced repetition, but in nextSkill() the cross-session due tier (line 48) fires only AFTER in-session requeue and BEFORE progression — in practice, with unmastered skills present, progression dominates and genuinely-due review rarely gets mixed in. Interleaving review with new material is the core SR benefit (combats forgetting); right now SR is effectively dormant for active learners. +- **Как:** adaptive.js: extend nextSkill to accept { dueBudget, sessionAnswered, dueServed } and an 'interleave' policy: every Nth item (or when an overdue skill exists with age > threshold) serve a due skill before progression. Compute overdue urgency from prog.due plus how long past due_at (server already returns due 0/1; add days_overdue to listProgress SELECT via julianday diff). trainer.html advance(): pass the due budget and increment dueServed when a due skill is served. Surface in session UI: small 'повторение' tag on review items so the student understands why an old skill reappeared. +- **Зависимости:** Builds on existing box/due_at (082) and adaptive tiers. listProgress query tweak (additive column days_overdue). +- **Риски:** Too much review crowds out new learning — cap dueBudget (e.g. <=40% of session). Avoid immediate repeats of last skill (notLast already in adaptive). Keep deterministic ordering so smoke tests are stable (sort due by days_overdue then order). + +### [M] D4. 'Повторить всё слабое' — cross-topic weak-skills review mode +- **Что:** A dedicated session mode that pulls weak/due skills across ALL topics (low accuracy, broken streak, or due) into one mixed session, plus a per-topic 'Повторить тему' that cycles every skill in the topic once. Distinct from the current topic-scoped smart session. +- **Зачем:** Today the smart session is locked to the selected topic (scope = skillsOf(curTopic)); there is no way to review weak material across the whole course before an exam. showSummary already computes weak skills but only lists names — students can't act on it. A cross-topic 'review weak' is the most-requested pre-exam workflow and directly leverages data already on the server. +- **Как:** trainer.html: add a session-mode toggle ('Темы' | 'Слабое' | 'Повторить всё'). In 'Слабое' mode, pickNext uses scope = all generators filtered to weak (prog accuracy < 0.7 OR !mastered with attempts>0 OR due) and orders by urgency (due days_overdue, then low accuracy). 'Повторить тему' = round-robin over skillsOf(curTopic). adaptive.js add a small helper weakSkills(progress, ordered, opts) -> ordered weak ids (pure, testable). Make the showSummary weak list clickable -> launches D4 review of exactly those skills. No DB change; reuses listProgress (accuracy derivable from solved/attempts; due already returned). +- **Зависимости:** Reuses sessionStats weak (adaptive.js) and listProgress fields. Synergy with D6 (due urgency) and D1 (don't surface still-locked skills). +- **Риски:** Weak threshold mis-tuned floods the session — make 0.7/attempts>=3 configurable and exclude never-attempted skills. Cross-topic mode must still respect manual difficulty/pin overrides. Ensure switching modes resets session counters cleanly (sessAnswered/reviewQ) to avoid mixed-mode summaries. + +### [S] D8. Per-skill mastery meter UI + teacher mastery-depth analytics +- **Что:** Replace the binary star with a per-skill mastery meter (e.g. 0/1/2/3 = locked/practiced/level-up/mastered-L3) shown on each skill chip and in a per-topic progress strip; extend the existing class-stats heatmap to show mastery DEPTH (best_level reached, L3-streak) not just solved/accuracy. +- **Зачем:** Mastery is currently all-or-nothing (mastered star or a solved-count badge), which hides progress and demotivates. A graded meter makes incremental progress visible (engagement) and, on the teacher side, distinguishes 'can do easy form' from 'exam-ready', which is the information teachers actually need for ЦТ/ЦЭ prep. It is the natural UI payoff of D3/D5. +- **Как:** Client: skillBadge()/renderSkills() render a 4-state meter from prog fields (practiced, best_level, l3_streak, mastered) using inline SVG (.ic) segments — no emoji. updateOverall() ring already exists; add a per-topic mini progress bar. Teacher: classStats() controller already aggregates per-skill — add best_level/l3_streak/avg_ms to the SELECT and perSkill/byStudent payload; trainer.html heatmap cells (line ~1136) color by mastery depth tiers and tooltip avg time. Optional: a 'кто застрял' list (high attempts, low best_level). +- **Зависимости:** D3 + D5 (the new fields). classStats and the heatmap already exist — additive columns only. +- **Риски:** Don't break existing classStats consumers — add fields, never rename. Keep meter rendering cheap (it runs in renderSkills on every answer). Ensure SVG meter is accessible (aria-label with words, per ROADMAP A4). + +_Grounding (verified in code): adaptive.js nextSkill() has a 4-tier priority (in-session requeue → server due → first-unmastered progression → lowest box retention) but operates ONLY within the selected topic (scope = skillsOf(curTopic) in trainer.html pickNext, line ~1015). Server MASTERY_STREAK=5 sets mastered sticky on cur_streak>=5 with NO regard to structural level — a student mastering only lin-basic (LEVELS=1) gets the mastery star. box/due_at exist (migr 082, INTERVAL_DAYS=[0,1,3,7,16,30]) but per-SKILL, not per-level; advance() never blends due skills mid-session (SR is just a fallback tier). submitAttempt sends ONLY {skill,correct} — no level, no time tracked anywhere. No prerequisite/unlock graph, no diagnostic, no cross-topic "review weak", no calibration-by-accuracy. classStats already aggregates per-skill for teachers. LEVELS map (generators.js ~1082) gives every generator level 1..3; chooseGen()/pickByLevel() already select by level. These are the seams to extend. Recommend implementing D5(level in attempt) FIRST since D2/D3/D7 all depend on it; it is a tiny additive change. Sequencing: D5 → D3 → D7 → D1 → D2 → D6 → D4 → D8. Each phase: engine/adaptive smoke + backend test (where API changes) + commit; eval/emoji = 0; lint:routes baseline 0._ + +## engagement-teacher + +### [S] P1. Award XP + coins for practice (server-side hook in submitAttempt) +- **Что:** Make the trainer feed the existing gamification engine. When a practice attempt is recorded (POST /api/practice/attempt), award XP server-side: e.g. +6 XP per correct answer, +25 XP the first time a skill is mastered (cur_streak crosses MASTERY_STREAK), and a small streak/accuracy bonus. Coins follow automatically (awardXP gives 1 coin / 10 XP). Surface the gained XP in the trainer UI (toast + the existing rail tiles) by returning the awarded amount in the attempt response. +- **Зачем:** This is the keystone of the whole engagement direction and the single highest-leverage change: today practice earns ZERO XP/coins/streak/achievements (confirmed: submitAttempt only writes practice_progress, never calls service.awardXP). Students who train get no progression feedback, so practice competes with tests/labs for none of the dopamine. One small hook makes every downstream item (daily goal, badges, leaderboard, level) light up for the trainer with no new infra. +- **Как:** backend/src/controllers/practiceController.js submitAttempt: after the upsert, require('./gamification/service') and call awardXP(uid, amount, 'practice_correct') on correct, plus awardXP(uid, 25, 'practice_mastered:'+skill) once when the row transitions to mastered (detect: !existing?.mastered && mastered===1). Anti-abuse: cap correct-answer XP per skill per day using the coin_log/xp_log dedup pattern already in awardCoinsOnce (or gate by attempts delta). Return the awarded XP in the JSON. Frontend trainer.html submitAttempt(): read r.xp and show LS.toast('+'+xp+' XP') / bump tr-solved. All awards are already kill-switch-gated inside awardXP (isGamificationEnabled) so no extra guard. Do NOT use the client selfAward path (abusable). +- **Зависимости:** None (service.awardXP, awardCoins exist; xp_log/coin_log exist). +- **Риски:** XP inflation/farming — mitigate with per-skill/day cap and modest amounts. Must keep amounts proportionate to tests (test=score*10+50) so practice doesn't dwarf tests. Kill-switch already respected by service.*. + +### [M] P2. Practice counts toward Daily Goal + a streak calendar on the trainer page +- **Что:** Extend the daily goal so practice answers satisfy it (not only tests), and add a compact 'streak calendar' / daily-goal ring to the trainer page showing today's progress and the current streak with a month heat-strip of active days. +- **Зачем:** Daily goals + streaks are the proven retention loop already built for tests, but GOAL_TIERS/daily_goals are tests-only (tests_done vs tests_target) — a student can grind the trainer all day and the daily goal stays at 0, and the streak (updateStreak) never advances from practice. Making practice 'count' turns the trainer into a daily-habit driver and reuses the streak_3/7/30 achievements students already chase. +- **Как:** Server: in submitAttempt call service.updateStreak(uid) (advances streak_current/best, awards daily_activity XP) and service.updateDailyGoal(uid, 0, awardedXp) so XP-target progress accrues; optionally add a problems_done/problems_target pair to daily_goals (migration 085 ALTER + GOAL_TIERS gets a practice field) so the goal can be 'solve N problems'. Frontend trainer.html: call LS.getGamificationMe()/LS.getDailyGoal in the bootstrap (already loads progress via Promise.all at ~1357) and render a ring + 7/30-day strip using xp_log/daily_goals (expose a tiny GET /api/practice/activity?days=30 returning per-day solved counts from practice_progress.updated_at, or reuse xp_log reason LIKE 'practice%'). Use existing CSS tile/ring (tr-overall ring already in markup) — SVG .ic only, no emoji. +- **Зависимости:** P1 (XP must flow first for xp-target to move). +- **Риски:** Streak semantics: ensure practice and test activity share ONE streak (updateStreak is idempotent per day, so fine). daily_goals schema change needs migration + GOAL_TIERS update without breaking test-only goal math. + +### [M] P3. Practice achievements/badges (mastery, volume, perfect session) +- **Что:** Add a 'practice' achievement track: first problem solved, 100/500/1000 problems, first skill mastered, 10/25 skills mastered, topic-master (all skills in a topic mastered), perfect adaptive session (10/10 in smart mode), and a per-topic completion badge shown on the topic in the rail. +- **Зачем:** Badges give long-horizon goals and a visible sense of conquering the curriculum; the achievement system already exists with notification + coin reward (pushAchievementNotif awards 50 coins) and a profile UI grouped by track/tier. Practice is currently absent from it. Topic-master badges also reinforce the structural-mastery (level-3 streak) philosophy of the engine. +- **Как:** backend/src/controllers/gamification/_shared.js: add ACHIEVEMENT_DEFS rows with group:'exploration'|'mastery', track:'practice'/'practice_master', tiers, required_feature:'practice' (or 'trainer' to match the feature flag); extend _requiredFeatureFor to map track 'practice' -> 'practice'. service.checkAchievements: add a checkPractice block (own try/catch like the others) reading practice_progress: SELECT COUNT(*) solved-total, COUNT(mastered=1), and topic-completion via joining the skill->topic map (pass topic counts from the client or hardcode the 21-topic skill lists, OR compute mastered-per-topic by skill prefix). Call checkAchievements(uid) at the end of submitAttempt. Rail topic badge: trainer.html renderTopics already has an 'освоено' star path (line ~720/724) — extend to a topic-mastered crown when all skills mastered. SVG icons only. +- **Зависимости:** P1 (so mastery/solved counts are meaningful and feature flag exists). +- **Риски:** required_feature gating: practice achievements must only seed/show when the trainer feature is enabled (mapping handles it). Topic-completion needs the canonical skill->topic list server-side (LEVELS map lives in frontend generators.js) — keep a small server copy or accept topic counts from a trusted aggregate; avoid trusting client-sent 'mastered' counts. + +### [L] P4. Teacher assignments + gradebook for practice (practice_assignments) +- **Что:** Upgrade the current 'assign = notification only' into trackable assignments: teacher picks topics/skills + a target (N problems solved OR mastery of listed skills) + deadline; students see an assignment banner on /trainer and progress is tracked; teacher sees a gradebook table (per student: done/target, mastered skills, % , submitted-on, late). Integrate completion into the existing assignment achievements (assign_first/assign_10) and notifications. +- **Зачем:** This is the explicit school-monetization ask and the biggest teacher-workflow gap. Today assignToClass only pushes a notification with no target, no tracking, no gradebook — a teacher can't tell who did the work. The tests side has full assignments+sessions; practice has nothing comparable. A tracked practice assignment makes the trainer usable as graded homework. +- **Как:** New migration 085: practice_assignments (id, class_id, teacher_id, title, skills_json or topic, target_type 'count'|'mastery', target_n, deadline, created_at) — NOT the legacy assignments table (it only targets test/file/textbook). The existing practice_progress already holds per-student solved/mastered, so 'completion' is computed by querying practice_progress for the member set against the assignment's skills (no per-assignment progress table needed for v1). Controller: createPracticeAssignment (requireRole teacher/admin, ownership of class), listForStudent (GET /api/practice/assignments — auth-only, returns assignments for classes I'm in + my computed progress), gradebook (GET /api/practice/assignments/:id/gradebook — owner/admin). Routes in practice.js with router.use(authMiddleware) + ownership-in-handler + // @public-by-design on :id read (lint:routes baseline 0). Reuse pushNotif (already imported) on create. On completion server-side, service.checkAchievements (assign_first/10) — or add practice-specific completion XP. Frontend: assignment banner + 'Сдать' flow in trainer.html (reuse the assign UI at ~1218), teacher gradebook table reusing the class-stats heatmap renderer (~1138). Add backend test practice-assignments.test.js (create / student-list / gradebook / 403 not-your-class / deadline). +- **Зависимости:** P1 recommended (so completion can grant XP), classStats infra reused. +- **Риски:** Scope creep vs the test-assignments module — keep v1 to count/mastery targets, no per-question grading. 'Late' semantics + timezone. Ownership checks on :id gradebook are critical (data leak). Computing completion from practice_progress means a student must have a progress row per assigned skill — handle 'not started' (no row) as 0. + +### [M] P5. Deeper class analytics: per-skill mastery, weak spots, time-on-task, trends +- **Что:** Expand class-stats from the current solved/attempts/accuracy heatmap into a teacher analytics view: per-skill class mastery rate + the 'weakest 5 skills' callout, attempts-to-mastery distribution, time-on-task (median seconds/problem), and a trend sparkline (last 14 days accuracy / problems solved). Add a per-student drill-down. +- **Зачем:** Teachers need to know what to reteach, not just a colored grid. The infra is half-built (classStats returns the matrix) but lacks weak-spot ranking, time, and trends. Time-on-task and trend lines are what turn a heatmap into actionable instruction and are differentiators for the school sell. +- **Как:** Time-on-task: practice_progress has no per-attempt timing — add a lightweight optional column or a small practice_attempts_log (migration) capturing {user_id, skill, correct, ms, created_at} written in submitAttempt (client sends elapsed ms; cap/sanitize 0..600000). Trends: aggregate that log by day. Extend practiceController.classStats (or a new GET /api/practice/class-analytics?class_id=) to compute: perSkill mastery% and rank ascending for 'weak spots', median ms per skill, and a daily series. Keep ownership check (teacher_id/admin) identical to classStats. Frontend trainer.html: extend the analytics modal — reuse the heatmap table + add a weak-spots list and a tiny inline SVG sparkline (no chart lib, draw a polyline; SimExpr not needed, pure JS). All numbers escaped. +- **Зависимости:** P4 helpful (assignment context) but independent; needs the per-attempt log (small migration). +- **Риски:** Client-sent elapsed ms is untrusted — clamp and treat as approximate (label 'примерно'); never gate grades on it. Extra writes per attempt — keep the log lean and indexed; or store rolling aggregates instead of raw rows if volume worries. + +### [L] P6. Skill-constellation map (reuse QuantikMap) with prerequisite unlocks +- **Что:** A visual 'galaxy' of the 21 topics → skills, where nodes show mastery (locked/available/mastered), grouped by subject/chapter, with prerequisite gating (a skill/topic unlocks when prior ones reach a mastery threshold). Clicking a node jumps the trainer to that skill. Player level/XP shown in the header. +- **Зачем:** Turns the flat topic rail into a motivating progression artifact (the same 'constellation' that works in Quantik) and operationalizes the curriculum order already encoded in TOPICS.order + generators order. Prerequisite unlocks prevent students from jumping into grade-9 material without base skills — pedagogically sound sequencing. +- **Как:** Reuse frontend/js/game/map.js (QuantikMap.create) and progress-logic.js (groupByChapter/layoutNodes/nodeStatus/isUnlocked/playerLevel) — they are metadata-driven and don't care that the 'levels' are skills. Build a levels-like array from TrainerGenerators (id, title, chapter=topic, order, unlockStars-> 'unlockMastered' threshold in prior topic) and a progressMap from practice_progress (mastered/box). Tint via the same palette helpers. Mount as a tab/modal on /trainer; node click sets curGen and starts a session (existing pickNext/newProblem). Player level = service.getXPInfo (P1) or QuantikProgress.playerLevel from total mastered. No engine changes; SVG only. +- **Зависимости:** P1 (player level/XP), and the existing prereq concept from progress-logic. +- **Риски:** Prereq thresholds must not soft-lock students who join mid-curriculum — make gating advisory (dimmed but still clickable) or threshold-low, mirroring the Quantik 'no deadlock' rule (verify cumulative thresholds <= achievable mastery). map.js was built for game levels — confirm it tolerates ~64 nodes performance-wise (it staggers reveal; may need batching). + +### [M] P7. LLM-pool review queue (approve / edit / delete drafts) +- **Что:** A teacher review queue for LLM-generated word problems: generated problems land as status='draft' (not auto-approved), teacher reviews each (preview story + equation + verified answer + steps), edits text, then approves (visible to students) or deletes. Batch-generate by topic for a lesson. +- **Зачем:** The pool today auto-approves everything generate() verifies (practiceController.generateProblem inserts status='approved'). Verification guarantees mathematical correctness but NOT pedagogical quality, wording, or grade-appropriateness — a teacher gate is the missing quality control before LLM content reaches students. The draft status already exists in the schema, just unused. +- **Как:** backend/src/controllers/practiceController.js: change generateProblem to INSERT status='draft' (keep author/manual as approved or also draft per preference). Add reviewList (GET /api/practice/pool/review?status=draft — requireRole teacher/admin), reviewUpdate (PUT /api/practice/pool/:id — edit story/solution, re-run genService.validateAndVerify so edits can't break correctness, set status), reviewDelete (DELETE /api/practice/pool/:id). Ownership: created_by or admin (per-row, like other modules). Routes guarded (requireRole) → lint:routes 0. Frontend: a 'Очередь проверки' panel for teachers in trainer.html (reuse the author modal at ~1190 for editing). Batch: a 'Сгенерировать N по теме' button looping generate. Keep all text escaped/sanitized (genService already sanitizes). +- **Зависимости:** None hard; complements P4 (assign reviewed problems). LLM provider config already exists (genService). +- **Риски:** Editing a problem must re-verify (a teacher could introduce an inconsistent answer) — always run validateAndVerify on save, reject 422 if it breaks. Don't expose drafts to students (status gate in listPool already filters approved). LLM cost on batch — cap batch size. + +### [L] P8. Generator authoring: all kinds + teacher sharing/clone +- **Что:** Expand the generator constructor (/trainer-builder, custom_generators) to support every kind (roots/simplify/inequality/compute/system, not just solve), expose structural level, and let teachers (not only admin) create/publish, share to a class, and clone each other's published generators — mirroring the custom-sims sharing model. +- **Зачем:** Content scale: the authoring tool is admin-only and limited, so the bank grows slowly and teachers can't make their own parametric drills. Opening it to teachers with sharing/clone (the proven custom-sims pattern) lets the curriculum expand safely (SimExpr-verified, no eval) and gives teachers ownership — a strong school feature. +- **Как:** Frontend frontend/js/trainer-builder.* (and trainer-builder.html): add form sections per kind (answers[] for roots; answerExpr+answerVars for simplify; bound/relOp/dispOp for inequality; eqs[]+answers{} for system; display+lhs/rhs for compute) and a level selector — every spec still runs through TrainerEngine.instantiate self-check (it already validates per-kind). Backend custom_generators + custom_generatorController: relax create/update from requireRole('admin') to ('teacher','admin') with per-row ownership (owner_id===user.id||admin) and keep validateSpec structure/limits (no execution). Add share (auto-publish + pushNotif to class, link /trainer) and clone (own or others' published -> copy spec, status draft) endpoints, exactly like customSims share/clone (Phase 6 pattern). Routes: keep router.use(authMiddleware), :id reads // @public-by-design with ownership-in-handler (lint:routes 0). Progress keys already 'cg'. Add backend test for ownership + share + clone. +- **Зависимости:** P3/P6 benefit (custom skills appear in badges/map), but standalone. Reuses custom-sims share/clone precedent. +- **Риски:** Opening create to teachers widens the attack surface — validateSpec must stay strict (size/limits/whitelisted kinds, NO execution server-side; client SimExpr only). A bad generator could produce unsolvable instances — the engine's per-instance self-check already discards those, but surface a 'preview/test' step in the builder so teachers see it works before publishing. Sharing a draft must 403 for non-owners (per-row check). + +_Grounded in real code. Key facts that shape every proposal: (1) Server gamification is rich and reusable but the trainer does NOT touch it yet — submitAttempt (frontend trainer.html:989 / practiceController.submitAttempt) only writes practice_progress; no XP/coins/streak/daily-goal/achievement is awarded for practice. service.awardXP/awardCoins/updateStreak/updateDailyGoal/unlockAchievement all already exist and are kill-switch-gated via isGamificationEnabled() (no extra guard needed in proposals). (2) selfAward already exists (POST /gamification/self-award, rate-limited tbLimiter, capped 1..50 XP, source whitelist /^[a-z0-9_-]{1,60}$/) — but it is a weak/abusable path for awarding XP from the client; trainer should NOT use it for mastery — award server-side inside submitAttempt instead (server already verified nothing, but it can rate-limit by attempts table). (3) ACHIEVEMENT_DEFS in _shared.js is the single seed list with explicit taxonomy (group/track/tier/required_feature) + _requiredFeatureFor mapping — adding practice badges = adding rows there + a 'practice' feature in the mapping. (4) GOAL_TIERS/daily_goals are TESTS-centric (tests_done/tests_target) — daily goal currently can't be satisfied by practice; needs a practice counter. (5) assignments table (000_baseline) targets test_id/file_id/textbook only — NO skill/topic practice target, so F1 needs a small dedicated table (practice_assignments) not a reuse of assignments. (6) practiceController already has assignToClass (notification only), classStats (heatmap, per student×skill solved/attempts/mastered/accuracy), generate/author/pool, custom_generators (admin-only create). (7) Engine already exposes analyzeMistake + checkStep + adaptive nextSkill/sessionStats + Leitner box/due_at in practice_progress. (8) QuantikMap (frontend/js/game/map.js) + progress-logic.js (isUnlocked/groupByChapter/playerLevel/layoutNodes) are metadata-driven and reusable for a skill-constellation. (9) No emoji rule, SVG .ic only; SimExpr-only, no eval. lint:routes baseline 0 — new :id routes need router.use(authMiddleware)+ownership-in-handler. Backend tests run serial (npm test green 330/330) — add practice-assignments.test.js. + +Recommended sequence: P1 (XP/coins hook — unlocks everything, tiny) -> P2 (daily goal+streak calendar) -> P3 (practice badges) -> P4 (teacher assignments+gradebook, the school-monetization ask) -> P5 (deeper class analytics) -> P6 (skill-constellation map) -> P7 (LLM-pool review queue) -> P8 (generator authoring to all kinds + sharing). P1-P3 are the engagement core; P4-P5 the teacher core; P6 visual; P7-P8 content scale. + +Risks shared across all: gamification kill-switch must be respected (it already is inside service.* — proposals must call service hooks, never bypass). XP-from-practice must be abuse-resistant (award server-side, cap per skill/day via coin_log-style dedup or attempt-count check) — do NOT route practice mastery through the client selfAward._ + +## breadth-ct-tech + +### [M] B-new-1: Functions topic — read graph + value/domain (NEW kinds graph-read & domain) +- **Что:** New topic 'g-func' (algebra) with data generators that DISPLAY a graph (linear y=kx+b and quadratic y=a(x-p)^2+q) and ask: read f at a point, find k/b or vertex, find zeros, state domain. Reuse the proven 'figures-as-data' model: add a 'graph' figure type to figures.js that plots from numeric params (k,b OR a,p,q) on the same VB_W×VB_H canvas with axes/gridlines, marking the queried point. Most sub-skills fit existing kinds: find-value/find-k = compute (lhs:'x', rhs:'k*x0+b'); zeros = roots; domain of sqrt/fraction = inequality (x>=c) or a tiny NEW kind 'interval'. Root-forward keeps every answer integer (pick k,b,x0 integers → f integer; vertex picked integer). +- **Зачем:** Functions/graph-reading is a whole А-section of ЦТ/ЦЭ (taxonomy already has alg-functions, geom-coordinates) and is currently MISSING from the trainer (no topic touches graphs). Highest-value content gap toward exam coverage; visual graph item is also the most engaging new format. +- **Как:** (1) figures.js: add TYPES['graph'] fn(spec,p) drawing axes + plotting f from params (line: two endpoints; parabola: ~28 sampled pts via fit()), a dot+dashed guides at queried x, '?' label — pure numbers, no eval, same esc/txt helpers. (2) generators.js: add TOPICS entry {key:'g-func', subject:'algebra', grade:9} + ~5 generators (read-value compute, find-k compute, vertex compute, zeros roots, linear domain). Add their level to LEVELS. (3) For 'interval'/domain reuse kind:'inequality' (already supported end-to-end) to avoid new engine code. exprToLatex already renders these; checkStudentAnswer already handles compute/roots/inequality. Verify with engine headless smoke (instantiate+verifyRoot over seeds). +- **Зависимости:** None for compute/roots/inequality variants (engine ready). New 'graph' figure type is isolated additive code in figures.js. +- **Риски:** Quadratic plot must auto-fit when vertex is off-canvas — clamp the sampled x-range to keep curve in view (reuse fit()). Keep answers integer (perfect-square discriminants for zeros) so they stay typable. + +### [S] B-new-2: Roots & irrationals topic (g7-9) via perfect-square root-forward +- **Что:** New topic 'roots' (algebra): evaluate sqrt of perfect squares (sqrt(144)), simplify sqrt(a^2*b)→a*sqrt(b) (kind simplify, equivalence-checked), arithmetic with sqrt that resolves to integers (sqrt(50)/sqrt(2)=5; sqrt(8)*sqrt(2)=4), and sqrt in Pythagorean-style compute already present can cross-link. All built root-forward so answers are exact integers OR a clean a*sqrt(b) form validated by _sampleEquiv. +- **Зачем:** Степени и корни is a core ЦТ section (taxonomy alg-powers exists) and the trainer's 'powers' topic only covers integer exponent laws — irrational radicals are absent. Big breadth win with low engine cost. +- **Как:** Pure generators in generators.js. Evaluate-sqrt: pick r in [2..15], derive n='r*r', answer='r', kind compute (verifyRoot passes since SimExpr computes sqrt). Simplify-radical: pick a,b (b squarefree small), srcExpr 'sqrt({a2b})' with a2b=a*a*b, answerExpr '{a}*sqrt({b})', kind simplify — _sampleEquiv at _EQUIV_PTS already verifies numeric equivalence (sqrt is in whitelist). Add LEVELS. exprToLatex renders \sqrt already. Add to TOPICS. +- **Зависимости:** None — sqrt already in SimExpr + exprToLatex. Reuses simplify/compute kinds verbatim. +- **Риски:** Equivalence sampling at negative _EQUIV_PTS would feed sqrt of negatives → NaN; the equivalence is over numeric VALUES not a var here (no x), so simplify with constant radicands is fine. For radicand-with-x cases keep b positive and restrict sample pts (or skip x-radicals in v1). + +### [M] B-new-3: Logarithms & exponents topic (10-11) — clean integer logs +- **Что:** New topic 'logs' (algebra, grade 10): compute log_a(a^k)=k, solve a^x=a^k → x=k, product/quotient log rules as simplify, change-of-base to base-10/e clean cases. Built root-forward: pick base a∈{2,3,5,10,e?} and exponent k → argument = a^k, answer = k (always integer). +- **Зачем:** Logarithms/exponents are a dedicated 10-11 / ЦЭ block entirely missing today; extends the trainer past grade-9 toward the harder exam where most points are won/lost. +- **Как:** Generators only. compute: display 'log по основанию {a} от {arg}', lhs:'x', rhs:'log({arg})/log({a})' (SimExpr log is natural ln; change-of-base gives exact integer k since arg=a^k). Exponential solve: lhs:'{a}^x', rhs:'{arg}', kind solve, answer='k' — but verifyRoot needs lhs=rhs at x=k: a^k==arg ✓. Note: SimExpr 'log' = ln per CLAUDE.md, 'log10'/'log2' available — prefer explicit log2/log10 generators for cleanliness, or log(arg)/log(a). Add exprToLatex coverage: it already maps ln/log; add 'log2'/'log10' to the TRIG map for nicer rendering (1-line additive change). LEVELS + TOPICS entries. +- **Зависимости:** Confirm SimExpr 'log' semantics (ln) — use log(x)/log(a) or log10/log2 to guarantee exact integers. Tiny exprToLatex map addition for log2/log10 (optional, cosmetic). +- **Риски:** Float: log(a^k)/log(a) may yield 2.9999999 — verifyRoot EPS handles it, and integerAnswer rounds; but choose a,k so a^k stays ≤ ~10^4 to avoid float drift. Base e logs print as decimals (avoid e-base in v1; keep 2/3/5/10). + +### [M] B-new-4: Trigonometry basics (9-11) — clean special-angle values +- **Что:** New topic 'trig' (algebra/geometry): evaluate sin/cos/tan at special angles {0,30,45,60,90,...} where values are clean (0, 1/2, 1, sqrt-forms), right-triangle ratios (opposite/hypotenuse) with Pythagorean-triple sides so ratios are exact fractions, and basic identity simplify (sin^2+cos^2=1). Reuse the right-triangle figure already in figures.js. +- **Зачем:** Trigonometry is a major ЦТ/ЦЭ topic absent from the trainer. Right-triangle ratios reuse the existing pyth triples + figure renderer, so it lands cheaply and visually. +- **Как:** Generators only. Ratio variant: reuse pyth m,n triple derivation (a,b,c integers), display the right-triangle figure (figures.js right-triangle already exists), ask sin=a/c → kind compute with rhs:'{a}/{c}' (answer is exact fraction, student types 'a/c', SimExpr accepts). Special-angle values: restrict to angles giving rational results in v1 (0/30→1/2/45 needs sqrt → simplify kind with answerExpr 'sqrt(2)/2'). exprToLatex renders sin/cos/tan. LEVELS + TOPICS. Add a degree note in display to avoid radian confusion (SimExpr trig is radians — so for evaluate-by-formula use the KNOWN value as rhs, NOT sin(deg); i.e. compute against the exact value to keep determinism). +- **Зависимости:** SimExpr trig is in RADIANS — do not put sin(30) literally (that's radians). Encode the exact value as rhs (root-forward) so verifyRoot is exact and degree/radian never bites. +- **Риски:** Radian/degree trap (mitigated by encoding exact rhs). sqrt-form answers must be typable — accept via simplify kind + _sampleEquiv; verify the sampler doesn't hit domain issues for constant expressions (no x → single value, safe). + +### [M] B-new-5: Coordinate geometry — midpoint, distance, line (geometry) +- **Что:** New topic 'g-coord' (geometry, grade 9): midpoint of segment, distance between points (Pythagorean-integer pairs → exact), slope/equation of a line through two points. Optional small figure showing points on axes (extend the new 'graph' figure or a simple 'points-plane' type). +- **Зачем:** Координаты, векторы, прямая is an explicit exam taxonomy section (geom-coordinates) with zero trainer coverage. Pairs naturally with B-new-1 graph rendering. +- **Как:** Generators. Distance: pick x1,y1 and integer dx,dy from a Pythagorean triple so sqrt(dx^2+dy^2) is integer → compute, rhs:'sqrt(({dx})^2+({dy})^2)', answer integer. Midpoint: kind system (answer pair x;y) — already fully supported (system kind), display 'середина отрезка AB', answers {x:'(x1+x2)/2', y:'(y1+y2)/2'} with even sums for integer mid. Slope: compute rhs:'({y2}-{y1})/({x2}-{x1})' with dx dividing dy. Reuse figures graph/points type for illustration. LEVELS + TOPICS. +- **Зависимости:** B-new-1 'graph' figure (optional illustration). System kind already works for midpoint. None blocking. +- **Риски:** Midpoint with odd coordinate sum gives .5 answers — constrain sums even for integer, or allow decimal (checkStudentAnswer accepts '2.5'). Keep slope denominators dividing numerators for clean fractions. + +### [L] B5: ЦТ-mode — А1–А10 / В1–В20 blank format + timer + ct_code taxonomy tags +- **Что:** Add an optional 'ЦТ-режим' session that assembles a fixed-format set (e.g. 10 part-A multiple-context + В open-answer) drawn across topics by ct_code tags, with a countdown timer and an answer-blank UI, scored like the real ЦТ. Tag each generator with a ct_code that maps to the EXISTING exam_topics slugs (alg-functions, alg-equations, geom-circle, alg-powers, …). +- **Зачем:** Turns the trainer from drill into exam-simulation — the headline ЦТ/ЦЭ value. Reuses the already-seeded taxonomy (migr.024) and the exam-prep scoring concept, giving teachers/students a recognizable test surface. +- **Как:** (1) Data: add optional 'ct_code' field to each generator in generators.js (string = exam_topics.slug). (2) Page: new mode toggle in trainer.html that builds a session = N generators selected to cover a ct blueprint (group by ct_code), renders a timer (reduced-motion aware) + a numbered answer blank list, calls checkStudentAnswer per item, then a score summary (reuse session-summary code). (3) Submit results via existing POST /api/practice/attempt per skill (no new table needed for v1). (4) Optional later: a thin /api/practice/ct-blueprint reading exam_topics counts. Keep all answers typable (existing kinds). Cross-link to /exam-prep taxonomy by slug for 'learn this topic' deep-links (pattern like tag-exam-textbook.js). +- **Зависимости:** Content breadth (B-new-1..5) should land first so the blueprint can cover А-sections; exam_topics taxonomy already exists (no migration for v1). +- **Риски:** Timer/blank is a sizeable UI addition to a 1371-line page — build as a separate mode container, don't disturb the practice flow. Scoring grid should be clearly labelled 'тренировочная' until real ЦТ grid wired (mirrors the placeholder note in migr.022). + +### [S] H1+H: Lazy-load KaTeX & Lucide, defer trainer modules (perf / first-paint) +- **Что:** Stop loading KaTeX JS+CSS and Lucide eagerly as blocking scripts. Load KaTeX on demand (first formula render) and defer the trainer engine modules; render figures (pure SVG) and plain-text statements immediately so the page is interactive before KaTeX arrives, with the existing text fallback (setMath already falls back to textContent when katex is absent). +- **Зачем:** trainer.html loads katex.min.css + katex.min.js + lucide.min.js + 4 trainer scripts all blocking (lines 10-11, 551-557). KaTeX is the heaviest. The page already has a graceful text fallback path, so lazy KaTeX is low-risk and improves Time-to-Interactive, especially on school devices/mobile. +- **Как:** (1) Convert KaTeX