# Формат челленджа: папка, этапы, вопросы и тесты

Челлендж — папка в репозитории с файлом challenge.md, этапами в stages/, вопросами и тестами в checks/ и заготовкой в starter/. Всё это обычные markdown-файлы и код — их пишут руками или правят после генерации.

## Папка

Челлендж — любая папка репозитория, в которой лежит `challenge.md`. Челленджей в одном
репозитории может быть сколько угодно, корень репозитория — тоже папка. Челленджи читаются с
ветки по умолчанию.

```text
parser/
  challenge.md              название, описание, уровень, теги
  stages/
    01-zachem.md            этап без проверок — теория
    02-voprosy.md           этап с вопросами
    03-kod.md               этап с кодом и тестами
  checks/                   ответы и тесты
    02-voprosy.md           вопросы этапа 02
    03-kod/test_parser.py   тесты этапа 03
  starter/                  с чего начинается репозиторий попытки
    parser.py
```

* **stages/** — этапы. Порядок этапов — порядок имён файлов, поэтому имена начинаются с номера
  с ведущим нулём: `01-…`, `02-…`, `10-…`. Имя без `.md` — адрес этапа:
  `/{владелец}/{репозиторий}/learn/{папка}/stages/02-voprosy`. Челлендж в корне репозитория
  пишется в адресе как `-`.
* **checks/** — файлы вопросов с отмеченными ответами и тесты. На экране этапа они не
  показываются и в репозиторий попытки не копируются. Но это обычные файлы репозитория: в
  публичном репозитории их может открыть любой — как ответы в конце учебника.
* **starter/** — файлы, с которых начинается репозиторий попытки. Нужна, только если в
  челлендже есть этапы с проверкой файлов.

Папка с `challenge.md` внутри `starter/` или `checks/` другого челленджа — его содержимое, а не
отдельный челлендж.

## challenge.md

Шапка плоская — только строки и списки, без вложенных объектов:

| поле | что в нём |
|---|---|
| `title` | название — **обязательно** |
| `summary` | одна строка для списка челленджей |
| `language` | язык текстов: `ru`, `en` |
| `level` | `beginner`, `intermediate` или `advanced` — на экране «для начала», «средний», «продвинутый» |
| `tags` | список `[git, основы]` или одно слово |
| `source` | путь к материалу, из которого сделан челлендж, от папки челленджа: `../git.md`. Файл обязан существовать; на странице челленджа — ссылка «Материал, из которого он сделан» |

Тело — вступление: зачем этот челлендж и что получится в конце. Его показывает страница
челленджа.

## Этап

Этап — файл `stages/NN-имя.md`: шапка и текст задания или теории в markdown. В шапке три поля
описывают этап:

| поле | что в нём |
|---|---|
| `title` | название этапа — **обязательно** |
| `summary` | короткое описание |
| `minutes` | сколько минут займёт, целым числом — в списке этапов «5 мин» |

Все остальные ключи шапки — **проверки**. Незнакомый ключ — ошибка формата.

| ключ | как записать | этап засчитан, когда |
|---|---|---|
| `quiz` | `quiz: checks/02-voprosy.md` | на каждый вопрос файла ответили верно; неверные возвращаются в конце круга |
| `self_check` | `self_check: [пункт, пункт]` или список строк `- пункт` | отмечены все пункты — проверяешь себя сам |
| `files` | `files: [konspekt.md, src/parser.py]` | файлы есть и не пустые |
| `contains` | `contains:` и строки `  путь: текст` или `  путь: ["текст", "текст"]` | в файле есть каждый текст, регистр не важен |
| `min_words` | `min_words:` и строки `  путь: 150` | в файле не меньше стольких слов, шапка файла не считается |
| `numeric` | `numeric:` и строки `  path: answer.md`, `  answer: 42`, `  tolerance: 0.5` | последнее число в файле совпало с ответом в пределах допуска; допуск по умолчанию 0, запятая и точка равноправны |
| `tests` | `tests: checks/03-kod/test_parser.py` | тесты на Python (`.py`) или JavaScript (`.js`) прошли в песочнице |

Как это складывается:

* **Этап без проверок — теория.** Его засчитывает кнопка **Прочитано, дальше**.
* **Проверки сочетаются.** Этап засчитан, когда пройдены все его части: вопросы, самопроверка и
  файлы. На экране этапа это список «Что проверяется».
* **Два рода путей.** `quiz` и `tests` указывают на файлы челленджа — от его папки, и файл обязан
  существовать. `files`, `contains`, `min_words` и `numeric` указывают на файлы в репозитории
  попытки — от его корня. Путь не может выходить за корень через `..`.
* **Проверки по файлам** (`files`, `contains`, `min_words`, `numeric`, `tests`) читают
  репозиторий попытки на последнем коммите. Челлендж с такими этапами получает при старте
  репозиторий попытки, см. [Челленджи](https://skillok.org/docs/ru/learning/challenges).
* **Вид этапа** в списке берётся из проверок: «Задание» — есть проверки по файлам, иначе
  «Вопросы» — есть `quiz`, иначе «Самопроверка», иначе «Теория».

Шапка этапа с проверкой файлов, как в конспекте по методу Корнелла:

```yaml
---
title: Напиши конспект
files: [konspekt.md]
contains:
  konspekt.md: ["## Заметки", "## Вопросы", "## Итог"]
min_words:
  konspekt.md: 150
---
```

Расчёт с ответом числом и самопроверка:

```yaml
---
title: Сколько времени это заняло
files: [skorost.md]
numeric:
  path: skorost.md
  answer: 250
  tolerance: 1
---
```

```yaml
---
title: Проверь конспект сам
self_check:
  - В каждом блоке заметок — мысль своими словами, а не цитата
  - Итог умещается в пять предложений и называет главное
---
```

## Файл вопросов

Вопросы этапа лежат в отдельном markdown-файле в `checks/`, а шапка этапа ссылается на него:
`quiz: checks/02-voprosy.md`. Файл читается в любом редакторе как обычная заметка.

* **Вопрос — заголовок второго уровня** `## …`. Заголовок первого уровня и текст до первого
  вопроса — вступление, вопросом он не считается.
* **В конце заголовка — id в фигурных скобках:** `## Что такое HEAD? {#head}`. id — латиница в
  нижнем регистре, цифры, `-` и `_`, до 64 знаков, уникален на весь челлендж. По id живут
  прогресс попытки и расписание повторений: текст вопроса править можно, смена id начинает
  вопрос с нуля.
* **Вид вопроса** узнаётся по форме ответа. Его можно назвать явно классом рядом с id:
  `{#head .card}`.
* **Тело читается с конца.** Последние строки `> …` — пояснение, которое показывается после
  ответа. Перед ними — блок ответа. Всё выше — текст вопроса, поэтому нумерованный список или
  код в условии с ответом не путаются. В одном блоке ответа — один вид строк: варианты, порядок,
  пары или ответы `= …`.

| вид | как записать | что делает пользователь |
|---|---|---|
| выбор (`single`) | варианты `- [ ] текст`, верный один — `- [x] текст` | выбирает один вариант |
| несколько ответов (`multiple`) | отмечено несколько `- [x]` или класс `.multiple` | отмечает все подходящие; часть верных — «Частично верно» |
| порядок (`order`) | нумерованный список `1.` в верном порядке | расставляет перемешанные пункты |
| пары (`pairs`) | строки `- слева => справа`, не меньше двух | соединяет; правая колонка перемешана |
| пропуски (`blanks`) | `___` в тексте вопроса и по строке `= ответ` на каждый пропуск | вписывает слова |
| число (`numeric`) | `= 42` или `= 2.5 ± 0.1` (можно `+-`) | вводит число; запятая или точка |
| короткий ответ (`text`) | `= ответ \| другой вариант` | пишет ответ; регистр, `ё`/`е`, кавычки и точка в конце не важны |
| карточка (`card`) | класс `.card`, под заголовком — обратная сторона текстом | вспоминает, открывает ответ и сам оценивает себя |

У вариантов их должно быть не меньше двух и хотя бы один отмечен верным; у порядка — не меньше
двух пунктов; у карточки нет блока ответа, только текст обратной стороны.

## Тесты

Этап с кодом ссылается на файл тестов в `checks/`, а файлы, с которых начинается работа,
кладутся в `starter/` и в шапке перечисляются в `files`.

* Тесты — обычный скрипт на Python или JavaScript, язык определяется по расширению. Этап
  засчитан, если скрипт отработал до конца без ошибки: удобно писать `assert` с сообщением. Выход
  раньше конца файла — например, `sys.exit(0)` в файле решения — не засчитывается.
* Файлы попытки подключаются по имени (`from wordcount import count_words`) или по пути
  (`from src.parser import parse`). В песочницу уходят файлы попытки того же языка.
* Прогон идёт в песочнице с ограничением по времени: бесконечный цикл — провал, а не
  зависание. Вывод тестов показывается под заданием.

## Как видны ошибки формата

Челлендж с ошибками не начинается. Во вкладке **Учиться** репозитория у него стоит «В файлах
челленджа ошибок: N», а на странице челленджа — список: файл, строка и что не так. Например:

```text
parser/stages/03-kod.md: в шапке этапа нет `title`
parser/stages/03-kod.md: неизвестная проверка `test`; есть: quiz, self_check, files, contains, min_words, numeric, tests
parser/stages/02-voprosy.md: `quiz` ссылается на `checks/02.md`, а такого файла в челлендже нет
parser/checks/02-voprosy.md:7: у вопроса «Что такое HEAD?» нет id — допиши в конец заголовка `{#короткий-id}`
parser/checks/02-voprosy.md:15: вопрос `merge-blank`: пропусков `___` в тексте 2, а строк ответа `= …` 1
```

Поправь файл и сделай коммит — список обновится. Уже начатые попытки ошибки не задевают: каждая
закреплена на версии челленджа, с которой её начали.

## Пример: челлендж из вопросов

Челлендж про git: две теории, два этапа вопросов и карточки. Репозитория попытки у него нет —
только прогресс.

`git-basics/challenge.md`:

````markdown
---
title: "Git: коммит, ветка, слияние"
summary: Как git хранит историю и почему ветка — это просто указатель
language: ru
level: beginner
tags: [git, основы]
---

Git — это не «папка с копиями», а история снимков, где каждый снимок знает своего родителя. Когда это понятно, остальные команды перестают пугать: `commit` добавляет снимок, `branch` заводит ещё один указатель, `merge` сводит две линии истории в одну.

За этот челлендж ты разберёшь три идеи — снимок, ветку и слияние — и закрепишь их вопросами. Вопросы потом вернутся в режиме «Тренировать», чтобы не забылись.
````

`git-basics/stages/01-snimki.md` — теория, проверок нет:

````markdown
---
title: История — это цепочка снимков
minutes: 5
---

## Рабочая копия, индекс, история

У каждого репозитория три места, где живёт файл:

1. **рабочая копия** — файлы, которые ты правишь в редакторе;
2. **индекс** — черновик следующего снимка: сюда попадает то, что ты отметил командой `git add`;
3. **история** — сохранённые снимки (коммиты).

`git commit` берёт индекс целиком и сохраняет его как новый снимок. У снимка есть автор, время, сообщение и ссылка на **родителя** — предыдущий снимок. Так и получается цепочка.

```text
A ← B ← C      (C знает про B, B — про A)
```

## Хэш — имя снимка

Каждый коммит называется хэшем от своего содержимого, например `3f2a9c1`. Поменяй хоть букву в файле или сообщении — получится другой хэш. Поэтому историю нельзя незаметно подменить: подмена меняет имена всех следующих снимков.
````

`git-basics/stages/02-kommit.md` — этап вопросов:

````markdown
---
title: Проверь себя — снимки
quiz: checks/02-kommit.md
---

Пять вопросов про рабочую копию, индекс и историю. Неверные ответы вернутся в конце круга — этап засчитается, когда на каждый ответишь верно.
````

`git-basics/checks/02-kommit.md` — выбор одного, выбор нескольких и порядок:

````markdown
---
title: Снимки
---

## Какая команда кладёт правку в индекс? {#add-to-index}

- [ ] `git commit`
- [x] `git add`
- [ ] `git push`
- [ ] `git status`

> Индекс — черновик следующего снимка. Наполняет его `git add`, а
> `git commit` сохраняет его целиком.

## Что сохраняет `git commit`? {#commit-saves}

- [ ] Все файлы рабочей копии, даже неотмеченные
- [x] Содержимое индекса
- [ ] Только изменённые строки последнего файла

> Коммит берёт индекс целиком: то, что не отмечено `git add`, в снимок не попадёт.

## Расставь путь правки от редактора до истории {#path-of-change}

1. Правишь файл в рабочей копии
2. `git add` — правка попадает в индекс
3. `git commit` — индекс становится снимком

## Что знает каждый коммит, кроме своих файлов? {#commit-knows}

- [x] Своего родителя
- [x] Автора и время
- [ ] Всех своих потомков
- [x] Сообщение

> Коммит ссылается назад, на родителя. Потомков он не знает — они появляются позже.

## Почему нельзя незаметно подменить старый коммит? {#hash-chain}

- [ ] Git запрещает правку файлов после коммита
- [x] Хэш зависит от содержимого, и подмена меняет имена всех следующих снимков
- [ ] Коммиты хранятся на сервере, а не у тебя

> Хэш — отпечаток содержимого и родителя. Изменил старый снимок — поменялись хэши всей цепочки после него.
````

`git-basics/stages/03-vetki.md` — вторая теория:

````markdown
---
title: Ветка — это указатель
minutes: 5
---

## Ветка ничего не копирует

Ветка в git — **файл с одним хэшем**: она указывает на последний коммит своей линии. Завести ветку — записать ещё один указатель, поэтому это мгновенно при любом размере проекта.

`HEAD` — указатель на то, где ты сейчас: обычно на ветку. Новый коммит сдвигает вперёд ту ветку, на которую смотрит `HEAD`.

```text
      main
       ↓
A ← B ← C
         ↖
          D ← E
              ↑
            feature  ← HEAD
```

## Слияние

`git merge feature`, стоя на `main`, сводит две линии. Если `main` за это время не двигалась, git просто передвигает указатель вперёд (**fast-forward**). Если двигались обе — появляется **коммит слияния** с двумя родителями. Конфликт бывает, только когда обе линии правили одни и те же строки.
````

`git-basics/stages/04-vetki-voprosy.md`:

````markdown
---
title: Проверь себя — ветки и слияние
quiz: checks/04-vetki.md
---

Вопросы про указатели, `HEAD` и слияние.
````

`git-basics/checks/04-vetki.md` — пары, число, короткий ответ и пропуск:

````markdown
---
title: Ветки
---

## Что внутри у ветки git? {#branch-is}

- [ ] Копия всех файлов проекта
- [x] Хэш последнего коммита своей линии
- [ ] Список всех коммитов ветки

> Ветка — указатель. Поэтому она заводится мгновенно и ничего не копирует.

## Соотнеси понятие и его смысл {#pointer-pairs}

- ветка => указатель на последний коммит линии
- HEAD => где ты сейчас
- коммит слияния => снимок с двумя родителями

## Сколько родителей у коммита слияния двух веток? {#merge-parents}

= 2

> Слияние сводит две линии истории, поэтому у такого снимка два родителя.

## Как называется слияние, при котором git просто сдвигает указатель вперёд? {#ff-name}

= fast-forward | fast forward | перемотка вперёд

> Так бывает, когда целевая ветка за это время не двигалась.

## Команда `git ___ feature` на ветке `main` сводит линию `feature` в `main` {#merge-blank}

= merge

## Когда при слиянии возникает конфликт? {#conflict}

- [ ] Всегда, если в ветках разное число коммитов
- [x] Когда обе линии правили одни и те же строки
- [ ] Когда ветки названы по-разному

> Разные файлы и разные места одного файла git сводит сам.
````

`git-basics/stages/05-kartochki.md` и `git-basics/checks/05-kartochki.md` — карточки:

````markdown
---
title: Карточки на повторение
quiz: checks/05-kartochki.md
---

Четыре карточки: вспомни ответ, открой обратную сторону и честно оцени, вспомнил ли. Оценка решает, когда карточка вернётся в «Тренировать».
````

````markdown
---
title: Карточки
---

## Что такое индекс в git? {#card-index .card}

Черновик следующего снимка: сюда `git add` кладёт правки, которые войдут в коммит.

## Что такое HEAD? {#card-head .card}

Указатель на то, где ты сейчас, — обычно на текущую ветку.

## Что такое fast-forward? {#card-ff .card}

Слияние без нового коммита: целевая ветка не двигалась, и git просто передвигает её указатель вперёд.

## Чем коммит отличается от сохранения файла? {#card-commit .card}

Коммит — снимок всего индекса с автором, временем, сообщением и ссылкой на родителя; его имя — хэш содержимого.
````

## Пример: этап с кодом и тестами

Челлендж «Счётчик слов на Python»: пользователь пишет `wordcount.py` в своём репозитории попытки,
тесты проверяют каждый этап.

```text
python-wordcount/
  challenge.md
  stages/
    01-zadacha.md          теория: что считаем словом
    02-count.md            files + tests
    03-top.md              files + tests
    04-voprosy.md          quiz
  checks/
    02-count/test_count.py
    03-top/test_top.py
    04-voprosy.md
  starter/
    wordcount.py
```

`python-wordcount/stages/02-count.md`:

````markdown
---
title: Посчитай слова
files: [wordcount.py]
tests: checks/02-count/test_count.py
---

Напиши `count_words(text)`: сколько слов в тексте по правилу из первого этапа.

```python
count_words("Кот, кот и КОТ!")  # 4
count_words("")                 # 0
count_words("  \n\t ")          # 0
```

Сделай правку и нажми «Проверить» — или сделай `git push`, проверка запустится сама. Если тест упал, внизу будет его вывод.
````

`python-wordcount/checks/02-count/test_count.py`:

```python
from wordcount import count_words

assert count_words("Кот, кот и КОТ!") == 4, "«Кот, кот и КОТ!» — четыре слова"
assert count_words("") == 0, "в пустой строке слов нет"
assert count_words("  \n\t ") == 0, "одни пробелы — это ноль слов"
assert count_words("one-two three") == 3, "дефис — разделитель"
assert count_words("Python 3 вышел в 2008") == 5, "числа — тоже слова"
print("count_words: все проверки прошли")
```

`python-wordcount/starter/wordcount.py` — с этого файла начинается репозиторий попытки:

```python
"""Частотный анализ текста.

Функции появятся по этапам челленджа — начни с `count_words`.
"""


def count_words(text):
    """Сколько слов в тексте."""
    raise NotImplementedError


def top_words(text, n):
    """n самых частых слов: список пар (слово, сколько раз)."""
    raise NotImplementedError
```

На заготовке тест падает на `NotImplementedError`, поэтому этап не засчитывается, пока функция
не написана. Свой челлендж удобно начать с копии этих файлов: создай папку в своём репозитории,
положи в неё `challenge.md` и `stages/`, сделай коммит и открой вкладку **Учиться**.

---

https://skillok.org/docs/ru/learning/challenge-format
