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

365 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Модуль «Подготовка к экзамену» — план разработки
**Цель:** превратить пассивный браузер 80 вариантов ЦТ-стиля по математике 9 в полноценный модуль подготовки к экзамену с интерактивным тренажёром, аналитикой и пробниками. Архитектура — generic под несколько экзаменов (математика 9, физика 9, химия 9, ЦТ 11 и т.д.).
**Старт:** 2026-05-29.
**Текущая база:** [/exam9](../frontend/exam9.html) — 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` остаются для совместимости с учительским «назначить как ДЗ».
```sql
-- Реестр экзаменов
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`
```sql
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`:
```diff
- ${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 считаем по последней попытке на задачу, история — для виджета «Последние попытки». |
| Иерархия тем | **Двухуровневая**: `algebra``algebra-powers`. На UI группируем по родителю, можно выбрать раздел целиком или конкретную подтему. ~6-8 разделов, ~25-30 подтем. |
---
## Acceptance criteria (для всего модуля)
- [ ] Ученик может выбрать тему и прорешать 10 задач с проверкой ответа.
- [ ] Точность пользователя обновляется в реальном времени на дашборде.
- [ ] Пробник на 180 минут с таймером, балл по сетке ЦТ.
- [ ] Top-3 слабых тем на дашборде с кнопкой «Прокачать».
- [ ] План по дате экзамена показывает «Сегодня N задач».
- [ ] Старая страница вариантов работает без изменений (через новый URL).
- [ ] Учитель может назначить любой из 80 вариантов как ДЗ (не только нечётные).
- [ ] Архитектура позволяет добавить `phys9` без изменений кода — только сид + импорт.