# Этап 6. Самопроверка и учебные треки

Дата: 12.10.2026
Статус: принята автономно (владелец 12.10.2026: три параллельных потока, «Все автономно»; пункт «Квизы и учебные треки» из раздела «После MVP» плана)
Связано: спека схемы контента `2026-10-11-stage-3-presets-sources.md`, спека визуала `2026-10-11-article-visuals.md`, редизайн шапки `2026-10-12-home-redesign.md` (PR 5).

## Цель

Читатель может проверить себя после статьи и пройти тему по порядку:

1. **Квиз** в конце статьи: несколько вопросов с вариантами ответа, разбор каждого ответа со ссылкой на источник статьи.
2. **Учебный трек**: упорядоченный список статей по теме («Основы: как устроено сердце», «ЭКГ с нуля»), с отметками прочитанного.

Работает без аккаунтов и сервера: прогресс хранится в браузере.

## Кто что делает

Этот этап делает компонент, схему данных, проверки и страницы треков. Квизы в конкретные статьи вставляет тред «Интерактив для статей»: он пишет файлы вопросов и добавляет `<Quiz />` в MDX. Этот этап статьи не трогает. Демонстрационный квиз есть только в тестовом контенте (`HA_TEST_CONTENT=1`).

### Как подключить квиз к статье

1. Рядом со статьёй `content/<locale>/<type>/<slug>.mdx` создать файл `content/<locale>/<type>/<slug>.quiz.yaml` на том же языке (формат ниже).
2. В MDX статьи поставить `<Quiz />` там, где он должен стоять, обычно перед «Источниками» или в конце. Импорт не нужен, пропсов нет: компонент сам берёт вопросы текущей статьи.
3. `pnpm build` и `pnpm test` проверяют файл: схему, номера источников и то, что `<Quiz />` есть в статье, у которой есть файл вопросов, и наоборот. Поэтому файл вопросов и `<Quiz />` добавляются в одном PR, на обоих языках сразу или по одному языку за раз.

Если у статьи нет перевода и страница показывает запасной язык, квиз берётся из того же языка, что и текст статьи.

## Формат вопросов

```yaml
# content/ru/disease/myocarditis.quiz.yaml
questions:
  - level: patient          # patient | student, как <Patient> и <Student>
    prompt: Что такое миокардит?
    options:
      - text: Воспаление сердечной мышцы
        correct: true
      - text: Сужение аортального клапана
      - text: Тромб в лёгочной артерии
    explanation: Миокардит — воспалительное заболевание миокарда, то есть сердечной мышцы.
    cite: [1]               # номера источников из frontmatter статьи
```

Правила (проверяются схемой и тестом):

- От 1 до 10 вопросов; в каждом от 2 до 5 вариантов, ровно один `correct: true` (вопросы с несколькими правильными ответами не входят в этот этап).
- `explanation` и хотя бы один номер в `cite` обязательны. Номер не больше числа источников статьи. Это то же правило, что у `<Cite>`: факт в разборе должен быть в указанном источнике, новых источников квиз не добавляет.
- Текст вопросов проходит ту же независимую проверку фактов, что и статья (процесс проекта `article-VERIFY.md` для русского, `article-VERIFY-EN.md` для английского): квиз не утверждает ничего, чего нет в статье и её источниках, и не даёт индивидуальных советов по лечению и доз.
- Порядок вариантов на странице фиксированный, как в файле: без перемешивания, чтобы сборка была детерминированной. Автор сам не ставит правильный ответ всегда первым.

## Компонент `<Quiz />`

- Разметка: `<section>` с заголовком «Проверьте себя», каждый вопрос — `<fieldset>` с `<legend>` и радиокнопками. Вопросы уровня `patient` и `student` скрываются и показываются тем же переключателем уровня, что и `<Patient>`/`<Student>`.
- Без JavaScript: под каждым вопросом `<details>` «Показать ответ» с правильным вариантом и разбором.
- С JavaScript: `<details>` скрывается, под вопросом появляется кнопка «Проверить». После ответа варианты блокируются, правильный и выбранный отмечаются не только цветом, но и текстом («Верно», «Неверно»), разбор появляется в области `aria-live="polite"`. Кнопка «Ещё раз» сбрасывает вопрос. В конце строка «Правильных ответов: 3 из 5»; считаются только вопросы, видимые на текущем уровне (при обоих уровнях — все).
- Ссылки в разборе — те же сноски `<Cite>`, ведущие к списку источников статьи.
- Скрипт — обычный модуль Astro без React, загружается только на страницах с квизом. Бюджет JS статьи (`js-budget`) не превышается.
- Подписи интерфейса («Проверьте себя», «Верно», кнопки) на языке страницы, с `lang={locale}`, как в `Level.astro`: статья может быть на запасном языке.
- Компонент находит вопросы по `Astro.locals`: страница статьи кладёт туда язык текста статьи, тип и slug (сейчас там только `locale` и `sourceCount`). Без них компонент роняет сборку.
- Во встраивании (`/embed/<type>/<slug>/`) квиз показывается только в варианте без JavaScript (`<details>`), без скрипта и `localStorage`: хранилище в чужом iframe часто разделено или закрыто.
- Стили на токенах сайта, только логические свойства, работает справа налево. Анимация отметки ответа выключается при `prefers-reduced-motion` и `html[data-motion="off"]`.
- Результат последнего полного прохождения хранится в `localStorage` (`ha:quiz:<locale>/<type>/<slug>`) отдельно для каждого уровня (`{ "patient": {…}, "student": {…} }`), чтобы трек мог отметить квиз пройденным. «Ещё раз» сохранённый результат не стирает. Если `localStorage` недоступен, квиз работает, просто без сохранения.

## Учебные треки

Файлы `content/tracks/<locale>/<id>.yaml`:

```yaml
title: Как устроено сердце
description: Шесть статей об анатомии сердца по порядку, от строения до сердечного цикла.
level: student            # patient | student | all
steps:
  - anatomy/heart-structure
  - anatomy/heart-chambers
  - anatomy/heart-valves
```

- Шаг — `<type>/<slug>` статьи. Сборка падает, если такой статьи нет ни на языке трека, ни на запасном языке.
- Треки не утверждают медицинских фактов (это порядок чтения и короткие описания), поэтому источники им не нужны.
- Страницы: `/<locale>/tracks/` (список треков) и `/<locale>/tracks/<id>/` (шаги трека: название и описание статьи, отметка «прочитано», отметка «квиз пройден», если у статьи есть квиз, ссылка «Продолжить» на первый непрочитанный шаг).
- «Прочитано» отмечается флажком «Прочитано» на странице трека или под статьёй, открытой из трека (флажок, а не кнопка: отметку можно снять, и состояние читается скринридером). Хранится в `localStorage` (`ha:read:<locale>/<type>/<slug>`). Без JavaScript страница трека — обычный нумерованный список ссылок.
- Если статья открыта из трека (`?track=<id>`), под статьёй появляется блок «Трек: <название>, шаг 3 из 6» с флажком «Прочитано» и ссылками на предыдущий и следующий шаги. Данные трека берутся из маленького JSON (`/<locale>/tracks/<id>/steps.json`), который сборка пишет рядом со страницей трека, а не встраиваются в каждую статью. На странице статьи остаётся только короткая проверка параметра, которая загружает модуль трека, когда `?track=` есть; она входит в бюджет JS статьи. Без параметра блок не показывается и модуль не грузится.
- Ссылка на треки — пункт «Треки» в меню разделов шапки и подвала (`siteNav`), у языков, которым есть что показать. Редизайн шапки (PR 5 треда «Дизайн главной страницы», #139) уже влит, поэтому пункт добавляется в этом этапе.
- Описания треков говорят только о том, что в треке (темы, порядок), без медицинских фактов; это проверяется на ревью.
- Первые треки (русский и английский): «Как устроено сердце» (анатомия), «ЭКГ и обследования» (диагностика), «Пациенту о болезнях сердца» (уровень patient, ключевые статьи о болезнях). Только из уже опубликованных статей.
- Коллекция `tracks` берёт тот же каталог контента, что и статьи: при `HA_TEST_CONTENT=1` — тестовый. В тестовом контенте есть трек на основном языке и трек, который на другом языке показывается через запасной. Тестовая локаль справа налево (`qps-plocm`) — только строки, треки у неё всегда запасные.

## Строки интерфейса

Все тексты интерфейса в `apps/web/src/i18n/messages/<locale>.json` (`quiz.*`, `tracks.*`), эталон `en.json`, плюс тестовая локаль `qps-plocm`. Названия и описания треков — в файлах треков, по одному файлу на язык. Язык без файла трека показывает трек запасного языка с пометкой, как у статей.

## План PR

1. **Спека** (этот файл).
2. **Схема и проверки квиза.** Коллекция `quizzes` (`content/**/*.quiz.yaml`) со своим `generateId`, который даёт id `<locale>/<type>/<slug>` (стандартный превращает `myocarditis.quiz` в `myocarditisquiz`), zod-схема, поля `Astro.locals` для статьи на обеих страницах статьи (обычной и встраивания), тест: файл квиза лежит рядом со статьёй того же языка, номера `cite` в пределах источников статьи, `<Quiz />` стоит ровно в тех статьях, у которых есть файл. Демонстрационный квиз в тестовом контенте.
3. **Компонент `<Quiz />`.** Разметка, скрипт, стили, строки, регистрация в `article-components.ts`. e2e: ответ без JS через `<details>`, проверка ответа и счёт с JS, переключатель уровня, вариант без скрипта во встраивании, axe, справа налево на тестовой локали.
4. **Треки: схема и страницы.** Коллекция `tracks`, страницы списка и трека, строки, тесты схемы (шаги существуют), e2e и axe.
5. **Прогресс.** Отметки «прочитано» (флажком) и «квиз пройден», «Продолжить», блок шага трека под статьёй при `?track=`. e2e на прогресс и на то, что без параметра нет ни блока, ни загрузки модуля; `js-budget` проходит.
6. **Первые треки** на русском и английском, ссылки на треки, README и CHANGELOG.

## Что не входит

- Сами квизы к опубликованным статьям (их делает тред «Интерактив для статей» по разделу «Как подключить квиз к статье»).
- Аккаунты, синхронизация прогресса между устройствами, рейтинги.
- Вопросы с несколькими ответами, свободным вводом, картинками или 3D.
- Изменение шапки сайта.

## Готово, когда

- Квиз из тестового контента работает с JavaScript и без него, проходит axe и e2e на обеих сборках (обычной и с тестовыми локалями).
- Три трека опубликованы на русском и английском, прогресс сохраняется между визитами.
- CI зелёный, Lighthouse не ниже порогов, бюджет JS статьи не превышен.
