Files
Learn_System/docs/exam-prep-plan.md

25 KiB
Raw Permalink Blame History

Модуль «Подготовка к экзамену» — план разработки

Цель: превратить пассивный браузер 80 вариантов ЦТ-стиля по математике 9 в полноценный модуль подготовки к экзамену с интерактивным тренажёром, аналитикой и пробниками. Архитектура — generic под несколько экзаменов (математика 9, физика 9, химия 9, ЦТ 11 и т.д.).

Старт: 2026-05-29. Текущая база: /exam9 — 80 вариантов × 30 заданий, KaTeX + SVG, чтение + просмотр решения, учительский «назначить как ДЗ» (только нечётные). Совместимость: старая страница exam9.html сохраняется как «Браузер вариантов» внутри нового модуля (вкладка), URL /exam9 переадресуется на /exam-prep/math9/variants.


0. Generic-архитектура: что значит «несколько экзаменов»

Модуль параметризуется ключом экзамена examKey:

examKey Заголовок Предмет Класс Длит. Вар. Зад/вар.
math9 Экзамен 9 класс — Математика math 9 180 мин 80 30
phys9 Экзамен 9 класс — Физика phys 9 180 мин
chem9 Экзамен 9 класс — Химия chem 9 180 мин
math11ce ЦТ — Математика math 11 180 мин 30

На старте делаем math9. Регистрация остальных — это только заполнение exam_tracks + добавление сидов задач. Никаких кодовых изменений.


1. Структура URL и страниц

Routes (Express)

GET /exam-prep                       → редирект на /exam-prep/math9 (или список, когда экзаменов >1)
GET /exam-prep/:examKey              → exam-prep.html (дашборд)
GET /exam-prep/:examKey/variants     → exam-prep-variants.html (старый /exam9 view)
GET /exam-prep/:examKey/variants/:n  → exam-prep-variants.html?v=:n
GET /exam-prep/:examKey/practice     → exam-prep-practice.html (тренажёр случайных задач)
GET /exam-prep/:examKey/topics       → exam-prep-topics.html (карта тем)
GET /exam-prep/:examKey/topics/:slug → exam-prep-topics.html?t=:slug
GET /exam-prep/:examKey/mock         → exam-prep-mock.html (стартовый экран)
GET /exam-prep/:examKey/mock/:mid    → exam-prep-mock.html?m=:mid (в процессе/результат)
GET /exam9                           → 301 redirect /exam-prep/math9/variants (back-compat)

В backend/src/server.js добавляются 5-6 кастомных роутов, отдающих соответствующий HTML (паттерн как у /exam9).

Файлы фронта

frontend/
  exam-prep.html                 ← дашборд
  exam-prep-variants.html        ← старый exam9.html, перенесённый сюда
  exam-prep-practice.html        ← тренажёр (random)
  exam-prep-topics.html          ← темы (список + по теме)
  exam-prep-mock.html            ← пробный (старт + run + результат)
  css/exam-prep.css              ← общий CSS модуля
  js/exam-prep/
    common.js                    ← общая загрузка examKey, sidebar, helpers
    api.js                       ← обёртки LS.api для /api/exam-prep/*
    task-card.js                 ← рендер задания с проверкой ответа
    answer-input.js              ← компонент ввода (radio для MC, text для open)
    katex.js                     ← обёртка над KaTeX bootstrap
    progress.js                  ← вычисление прогресса/streak
    dashboard.js                 ← логика exam-prep.html
    practice.js                  ← логика exam-prep-practice.html
    topics.js                    ← логика exam-prep-topics.html
    mock.js                      ← логика exam-prep-mock.html
    variants.js                  ← логика exam-prep-variants.html (по сути старый app.js)

2. База данных (новые таблицы)

Все таблицы создаются миграцией 004_exam_prep.sql. Существующие exam9_variant_tests и feature_exam9_enabled остаются для совместимости с учительским «назначить как ДЗ».

-- Реестр экзаменов
CREATE TABLE exam_tracks (
  exam_key          TEXT PRIMARY KEY,        -- 'math9'
  title             TEXT NOT NULL,           -- 'Экзамен 9 класс — Математика'
  subject_slug      TEXT NOT NULL,           -- 'math'
  grade             INTEGER NOT NULL,        -- 9
  duration_min      INTEGER NOT NULL,        -- 180
  tasks_per_variant INTEGER NOT NULL,        -- 30
  variants_count    INTEGER NOT NULL,        -- 80
  scoring_json      TEXT,                    -- JSON: [{correct:30, score:100}, ...] сетка
  intro_html        TEXT,                    -- описание для лендинга
  enabled           INTEGER NOT NULL DEFAULT 1,
  sort_order        INTEGER NOT NULL DEFAULT 0
);

-- Банк задач (импорт из v*.js)
CREATE TABLE exam_tasks (
  id          INTEGER PRIMARY KEY,
  exam_key    TEXT NOT NULL REFERENCES exam_tracks(exam_key) ON DELETE CASCADE,
  variant     INTEGER NOT NULL,              -- 1..80
  task_idx    INTEGER NOT NULL,              -- 1..30
  task_type   TEXT NOT NULL,                 -- 'mc' | 'open' | 'long'
  text_html   TEXT NOT NULL,                 -- условие (HTML с $...$ для KaTeX)
  figure_html TEXT,                          -- опциональный SVG/HTML
  opts_json   TEXT,                          -- JSON: [["а","..."],...] для mc, NULL для остальных
  answer      TEXT,                          -- 'г' для mc, '-2' / '7500' для open, NULL для long
  solution_html TEXT NOT NULL,               -- полное решение (HTML)
  topic       TEXT,                          -- 'algebra/powers' (заполнится в фазе 4)
  subtopic    TEXT,
  difficulty  INTEGER,                       -- 1-5
  UNIQUE(exam_key, variant, task_idx)
);
CREATE INDEX idx_exam_tasks_topic     ON exam_tasks(exam_key, topic);
CREATE INDEX idx_exam_tasks_variant   ON exam_tasks(exam_key, variant);

-- Попытки пользователя
CREATE TABLE exam_attempts (
  id           INTEGER PRIMARY KEY,
  user_id      INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  exam_task_id INTEGER NOT NULL REFERENCES exam_tasks(id) ON DELETE CASCADE,
  user_answer  TEXT,
  is_correct   INTEGER,                      -- 0/1, NULL = долгое задание без автопроверки
  time_ms      INTEGER,                      -- сколько потратил на эту задачу
  mode         TEXT NOT NULL,                -- 'practice'|'variant'|'topic'|'mock'
  session_id   INTEGER,                      -- группировка попыток в пробнике/тем.сессии
  hint_used    INTEGER NOT NULL DEFAULT 0,   -- 0/1, открывал ли подсказку
  solution_viewed INTEGER NOT NULL DEFAULT 0,-- 0/1, открывал ли решение
  created_at   INTEGER NOT NULL              -- unix ms
);
CREATE INDEX idx_exam_attempts_user_time ON exam_attempts(user_id, created_at DESC);
CREATE INDEX idx_exam_attempts_task      ON exam_attempts(exam_task_id);
CREATE INDEX idx_exam_attempts_session   ON exam_attempts(session_id) WHERE session_id IS NOT NULL;

-- Пробные экзамены
CREATE TABLE exam_mock_sessions (
  id                INTEGER PRIMARY KEY,
  user_id           INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  exam_key          TEXT NOT NULL,
  variant           INTEGER,                 -- если из готового варианта
  source            TEXT NOT NULL,           -- 'variant' | 'random' | 'weak-topics'
  task_ids_json     TEXT NOT NULL,           -- JSON [id,id,...] — порядок задач
  started_at        INTEGER NOT NULL,
  finished_at       INTEGER,
  duration_planned_min INTEGER NOT NULL,     -- 180
  score             INTEGER,                 -- балл по сетке
  total_correct     INTEGER,
  total_tasks       INTEGER,
  status            TEXT NOT NULL DEFAULT 'active' -- 'active'|'finished'|'abandoned'
);
CREATE INDEX idx_mock_user ON exam_mock_sessions(user_id, started_at DESC);

-- План подготовки (по дате экзамена)
CREATE TABLE exam_user_plan (
  user_id      INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  exam_key     TEXT NOT NULL,
  exam_date    TEXT NOT NULL,                -- YYYY-MM-DD
  daily_target INTEGER NOT NULL DEFAULT 10,  -- задач в день
  weak_focus   INTEGER NOT NULL DEFAULT 1,   -- приоритезировать слабые темы
  created_at   INTEGER NOT NULL,
  updated_at   INTEGER NOT NULL,
  PRIMARY KEY (user_id, exam_key)
);

-- Темы (справочник, заполняется в фазе 4)
CREATE TABLE exam_topics (
  slug         TEXT PRIMARY KEY,             -- 'algebra-powers'
  exam_key     TEXT NOT NULL,
  parent_slug  TEXT,                         -- 'algebra' для вложенности
  title        TEXT NOT NULL,                -- 'Степени и корни'
  description  TEXT,
  sort_order   INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_topics_exam ON exam_topics(exam_key, parent_slug);

Seed: exam_tracks

INSERT INTO exam_tracks (exam_key,title,subject_slug,grade,duration_min,tasks_per_variant,variants_count,scoring_json,sort_order)
VALUES ('math9','Экзамен 9 класс — Математика','math',9,180,30,80,
  '[{"correct":30,"score":100},{"correct":28,"score":95},...]',  -- сетка перевода
  10);

Импорт-скрипт

backend/scripts/import-exam-tasks.js — переносит frontend/js/exam9/variants/v*.js в таблицу exam_tasks:

  1. Загружает каждый v*.js в изолированный VM-контекст с подставным VARIANTS = {}.
  2. Для каждой задачи парсит:
    • text_html, figure_html, solution_html, opts_json — напрямую.
    • task_type — из наличия opts: если есть → mc, иначе если в sol есть <div class="sol-ans">Ответ: $число$open, иначе → long.
    • answer — регулярка по <div class="sol-ans">Ответ:\s*([абвгд]) для MC; Ответ:\s*\$?(-?[\d,\.]+)\$? для open; NULL для long.
  3. Идемпотентно: DELETE FROM exam_tasks WHERE exam_key='math9' перед вставкой.
  4. Запуск: node backend/scripts/import-exam-tasks.js math9. Без аргумента — все ключи.

Качество автопарса: ожидаемо ~80-90% задач получат корректный answer. Остальные требуют:

  • ручной правки v*.js (добавить answer: "г" явным полем в task → парсер берёт его в первую очередь),
  • либо помечаем task_type='long' (не идут в тренажёр, только в просмотре варианта).

Поле answer в v*.js — необязательное расширение текущего формата. Старый код exam9.html его игнорирует.


3. Backend API

Новый роут: backend/src/routes/exam-prep.js, монтируется как /api/exam-prep.

Метод Путь Что делает
GET /tracks список включённых треков (для лендинга)
GET /:examKey/info трек + агрегаты пользователя (всего задач, решено, точность)
GET /:examKey/variants список вариантов с прогрессом пользователя (для browse-вкладки)
GET /:examKey/variants/:n/tasks задачи варианта N (text/figure/opts/answer-shape)
GET /:examKey/topics дерево тем + кол-во задач + точность пользователя по теме
GET /:examKey/topics/:slug/tasks?limit=20&exclude_solved=1 задачи по теме для тренажёра
GET /:examKey/practice/next?count=10&strategy=random|weak|unsolved выборка задач для тренажёра
GET /:examKey/dashboard агрегат: точность, последние попытки, streak, top-3 слабых тем, прогресс по плану
GET /:examKey/plan план пользователя
PUT /:examKey/plan сохранить план (exam_date, daily_target, weak_focus)
POST /attempts сохранить попытку {exam_task_id, user_answer, is_correct, time_ms, mode, session_id, hint_used, solution_viewed}
GET /attempts?task_id=X история попыток по задаче (для повторного открытия)
POST /:examKey/mock/start создать пробник {source, variant?, count?, weak_topics?}{id, task_ids, duration_min}
GET /mock/:id состояние пробника + задачи (без answer/solution пока active)
POST /mock/:id/answer сохранить ответ (без проверки) {task_id, user_answer, time_ms}
POST /mock/:id/finish завершить → вернуть результат с is_correct/solution
GET /mock/:id/result результат завершённого пробника (балл + разбор)
GET /admin/tasks/:id (admin) редактор задачи (для исправлений)
PUT /admin/tasks/:id (admin) сохранить правки задачи

Ответы безопасны: при mode=mock и статусе active — answer и solution_html не отдаются. После finish отдаются.

Производительность: агрегаты дашборда (точность, слабые темы) считаются одним запросом с GROUP BY, без N+1.


4. Расчёты и формулы

Точность

accuracy = correct_attempts / total_attempts

Где correct_attempts — последняя попытка по каждой задаче (не «все попытки за всё время»). Это даёт реальный текущий уровень: если ученик ошибся, потом решил — считаем решённой.

Слабая тема

Тема со ≥3 попыток и точностью < 60%, отсортированы по (accuracy ASC, attempts DESC). Top-3 показываем на дашборде.

Streak

Подряд идущие календарные дни с ≥1 правильной попыткой. Сегодня обнуляет/сохраняет.

Готовность к экзамену (gauge 0-100)

readiness = 0.6 * (coverage) + 0.4 * (accuracy_weighted)
  coverage          = topics_with_3plus_attempts / total_topics
  accuracy_weighted = avg(accuracy_per_topic) только по тем с ≥3 попытками

Метрика — для дашборда (виджет «Готовность: 73%»). Грубая, мотивационная.

План «Сегодня»

days_left      = max(1, exam_date - today)
tasks_left     = total_tasks - distinct_solved_tasks
daily_target   = clamp(ceil(tasks_left / days_left), 5, 50)
weights        = если weak_focus=1: 70% задач из top-3 слабых тем, 30% случайных нерешённых

Балл пробника

По scoring_json из трека (сетка перевода кол-ва правильных в балл). Если NULL — просто процент.


5. Фазы реализации

# Что Фронт Бэк Оценка
F0 Миграция 004_exam_prep.sql + seed math9 + импорт-скрипт + проверка качества автопарса ответов DB schema, import script 0.5 дня
F1 Скелет страниц exam-prep.html + sidebar (замена /exam9), общий CSS, common.js, базовый дашборд (статичный) 5 HTML, общий CSS, sidebar route handlers (отдача HTML), /api/exam-prep/tracks, /info 1 день
F2 Браузер вариантов (перенос текущего /exam9 в exam-prep-variants.html, читает из API а не из v*.js) variants.js + рефактор /variants, /variants/:n/tasks 0.5 дня
F3 Компонент task-card с вводом ответа + проверкой + POST /attempts + просмотром решения после ответа task-card.js, answer-input.js /attempts (POST/GET) 1 день
F4 Дашборд live: точность, последние попытки, streak, прогресс-бар. Реальные данные. dashboard.js, progress.js /dashboard 0.5 дня
F5 Тренажёр «случайные задачи» — выборка через /practice/next?strategy=random|unsolved practice.js /practice/next 0.5 дня
F6 Тегирование тем: LLM-скрипт классификации + ручная проверка + exam_topics справочник tag-tasks.js скрипт 1 день (включая ручную верификацию)
F7 Карта тем + тренажёр по теме topics.js, topic-card.js /topics, /topics/:slug/tasks 0.5 дня
F8 Слабые места на дашборде + кнопка «Прокачать тему» + strategy=weak + изменения в dashboard.js/practice.js расширение /dashboard, /practice/next 0.5 дня
F9 Пробный экзамен: старт, таймер, ответы без проверки, финиш, результат с разбором mock.js (3 экрана) /mock/start, /mock/:id/answer, /mock/:id/finish, /mock/:id/result 1.5 дня
F10 План по дате экзамена: форма даты + виджет «Сегодня N задач» на дашборде дополнение dashboard.js /plan (GET/PUT), интеграция в /dashboard 0.5 дня
F11 Учительская часть: расширить import-exam9.js → все 80 вариантов; отчёт класса по темам таб «Класс» на learners-странице расширение /api/classes/:id/exam-stats 1 день
F12 Полировка: skeleton-loaders, empty states, mobile UI, achievements (интеграция с gamification) косметика 0.5 дня

Итого: ~10 дней работы (одна большая сессия = 1 фаза). Опционально F11+F12 после внутренней приёмки.

Параллелизация

  • F0 → F1 → F2 — последовательно (база).
  • F3 — после F0.
  • F4 — после F3.
  • F5 — после F3.
  • F6 — независим, можно параллельно с F1-F5.
  • F7-F8 — после F6.
  • F9 — после F3.
  • F10 — после F4.

6. Sidebar — точечные изменения

В js/sidebar.js, группа content:

- ${L('/exam9', 'clipboard-check', 'Экзамен 9 класс')}
+ ${L('/exam-prep/math9', 'clipboard-check', 'Подготовка к экзамену 9')}

Внутри страницы /exam-prep/math9 — табы или подменю на 4 вкладки: Дашборд · Варианты · Тренажёр · Темы · Пробник.

Когда будут другие экзамены — родительский пункт Подготовка к экзаменам (расскрывающаяся группа) с детьми.


7. Подсчёт ответа: алгоритм проверки

В task-card.js при клике «Проверить»:

task_type Ввод Эталон Проверка
mc radio выбран 'г' answer='г' строгое равенство
open text input '7500' answer='7500' нормализация: запятая→точка, обрезка пробелов, парсинг как Number, сравнение с 1e-6 погрешностью
long нет ввода (только «Я решил / Показать решение») NULL пользователь сам отмечает правильность

Ответы хранятся в user_answer строкой как ввёл, is_correct — 0/1/NULL.


8. Совместимость и миграция

  1. URL /exam9 → 301 на /exam-prep/math9/variants (server.js).
  2. localStorage exam9_progress_v1 — оставляем, не мигрируем. После запуска прогресс пишется в БД, старый кэш не мешает.
  3. localStorage exam9_last_variant — переименовываем в exam_prep_math9_last_variant.
  4. exam9_variant_tests — не трогаем, учительский «назначить как ДЗ» продолжает работать. В F11 расширяем импорт на 80 вариантов.
  5. v*.js файлы — остаются как авторский источник. Импорт-скрипт перегружает таблицу exam_tasks. Любые правки в v55.js → npm run import-exam-tasks math9.
  6. KaTeX — общий bootstrap через js/exam-prep/katex.js.

9. Что не входит в этот план (future)

  • Видео-разборы решений.
  • Конструктор кастомного варианта учителем.
  • Лидерборд между учениками.
  • Adaptive difficulty (карта Лейтнера для задач).
  • Push в Telegram по плану.
  • Экспорт пробника в PDF.

10. Зафиксированные решения (2026-05-29)

Вопрос Решение
Откуда брать правильный ответ Гибрид: если в task есть явное поле answer: — берём его; иначе парсим из <div class="sol-ans"> в sol регуляркой. Неуспешные парсы логируются — фиксим явным полем.
Long-задачи (без автопроверки) В режимах practice/topicфильтр task_type != 'long'. В variant/mock — присутствуют, ученик после решения сам отмечает «решил/не решил».
Когда задача «решена» ≥1 правильная попытка за всю историю. Прогресс не откатывается. Для long — если пользователь отметил «решил».
История попыток Все. Accuracy считаем по последней попытке на задачу, история — для виджета «Последние попытки».
Иерархия тем Двухуровневая: algebraalgebra-powers. На UI группируем по родителю, можно выбрать раздел целиком или конкретную подтему. ~6-8 разделов, ~25-30 подтем.

Acceptance criteria (для всего модуля)

  • Ученик может выбрать тему и прорешать 10 задач с проверкой ответа.
  • Точность пользователя обновляется в реальном времени на дашборде.
  • Пробник на 180 минут с таймером, балл по сетке ЦТ.
  • Top-3 слабых тем на дашборде с кнопкой «Прокачать».
  • План по дате экзамена показывает «Сегодня N задач».
  • Старая страница вариантов работает без изменений (через новый URL).
  • Учитель может назначить любой из 80 вариантов как ДЗ (не только нечётные).
  • Архитектура позволяет добавить phys9 без изменений кода — только сид + импорт.