# Challenge format: folder, stages, questions and tests

A challenge is a repository folder with a challenge.md file, stages in stages/, questions and tests in checks/ and starting files in starter/. All of it is ordinary markdown and code — written by hand or edited after generation.

## The folder

A challenge is any repository folder that contains `challenge.md`. A repository can have any number
of challenges, and the repository root is a folder too. Challenges are read from the default branch.

```text
parser/
  challenge.md              title, description, level, tags
  stages/
    01-why.md               a stage without checks — theory
    02-questions.md         a stage with questions
    03-code.md              a stage with code and tests
  checks/                   answers and tests
    02-questions.md         questions of stage 02
    03-code/test_parser.py  tests of stage 03
  starter/                  what the attempt repository starts with
    parser.py
```

* **stages/** — the stages. Their order is the order of the file names, so names start with a number
  with a leading zero: `01-…`, `02-…`, `10-…`. The name without `.md` is the stage address:
  `/{owner}/{repository}/learn/{folder}/stages/02-questions`. A challenge at the repository root is
  written as `-` in the address.
* **checks/** — question files with marked answers, and tests. They are not shown on the stage screen
  and are not copied to the attempt repository. But they are ordinary repository files: in a public
  repository anyone can open them — like the answers at the back of a textbook.
* **starter/** — the files the attempt repository starts with. Needed only if the challenge has stages
  with file checks.

A folder with `challenge.md` inside another challenge's `starter/` or `checks/` is part of that
challenge's content, not a separate challenge.

## challenge.md

The header is flat — only strings and lists, no nested objects:

| field | what it holds |
|---|---|
| `title` | the title — **required** |
| `summary` | one line for the list of challenges |
| `language` | the language of the texts: `ru`, `en` |
| `level` | `beginner`, `intermediate` or `advanced` |
| `tags` | a list `[git, basics]` or a single word |
| `source` | the path to the material the challenge was made from, relative to the challenge folder: `../git.md`. The file must exist; the challenge page shows a «The material it was made from» link |

The body is the introduction: why this challenge and what you will have at the end. The challenge page
shows it.

## A stage

A stage is a `stages/NN-name.md` file: a header and the text of the task or theory in markdown. Three
header fields describe the stage:

| field | what it holds |
|---|---|
| `title` | the stage title — **required** |
| `summary` | a short description |
| `minutes` | how many minutes it takes, as a whole number — «5 min» in the list of stages |

Every other header key is a **check**. An unknown key is a format error.

| key | how to write it | the stage counts when |
|---|---|---|
| `quiz` | `quiz: checks/02-questions.md` | every question of the file has been answered correctly; wrong ones come back at the end of the round |
| `self_check` | `self_check: [item, item]` or a list of `- item` lines | all items are ticked — you check yourself |
| `files` | `files: [notes.md, src/parser.py]` | the files exist and are not empty |
| `contains` | `contains:` and lines `  path: text` or `  path: ["text", "text"]` | the file contains every text, case-insensitive |
| `min_words` | `min_words:` and lines `  path: 150` | the file has at least that many words; the file's header does not count |
| `numeric` | `numeric:` and lines `  path: answer.md`, `  answer: 42`, `  tolerance: 0.5` | the last number in the file matches the answer within the tolerance; the default tolerance is 0, comma and dot are equal |
| `tests` | `tests: checks/03-code/test_parser.py` | Python (`.py`) or JavaScript (`.js`) tests passed in the sandbox |

How it adds up:

* **A stage without checks is theory.** The **Read, next** button counts it.
* **Checks combine.** A stage counts when all its parts pass: questions, self-check and files. On the
  stage screen this is the «What is checked» list.
* **Two kinds of paths.** `quiz` and `tests` point at challenge files — relative to its folder, and the
  file must exist. `files`, `contains`, `min_words` and `numeric` point at files in the attempt
  repository — relative to its root. A path cannot leave the root through `..`.
* **File checks** (`files`, `contains`, `min_words`, `numeric`, `tests`) read the attempt repository at
  its latest commit. A challenge with such stages gets an attempt repository at start, see
  [Challenges](https://skillok.org/docs/en/learning/challenges).
* **The stage kind** in the list comes from its checks: «Task» — there are file checks, otherwise
  «Questions» — there is a `quiz`, otherwise «Self-check», otherwise «Theory».

A stage header with a file check, like notes taken with the Cornell method:

```yaml
---
title: Write your notes
files: [notes.md]
contains:
  notes.md: ["## Notes", "## Questions", "## Summary"]
min_words:
  notes.md: 150
---
```

A calculation with a numeric answer, and a self-check:

```yaml
---
title: How long did it take
files: [speed.md]
numeric:
  path: speed.md
  answer: 250
  tolerance: 1
---
```

```yaml
---
title: Check your notes yourself
self_check:
  - Every notes block holds a thought in your own words, not a quote
  - The summary fits into five sentences and names the main point
---
```

## The question file

A stage's questions live in a separate markdown file in `checks/`, and the stage header refers to it:
`quiz: checks/02-questions.md`. The file reads like an ordinary note in any editor.

* **A question is a second-level heading** `## …`. A first-level heading and the text before the first
  question are an introduction, not a question.
* **The heading ends with an id in curly braces:** `## What is HEAD? {#head}`. An id is lowercase Latin
  letters, digits, `-` and `_`, up to 64 characters, unique across the whole challenge. The attempt's
  progress and the review schedule live by the id: you can edit the question text, but changing the id
  starts the question from zero.
* **The question kind** is recognized by the shape of the answer. It can be named explicitly with a
  class next to the id: `{#head .card}`.
* **The body is read from the end.** The last `> …` lines are the explanation shown after answering.
  Before them is the answer block. Everything above is the question text, so a numbered list or code in
  the question is not confused with the answer. One answer block holds one kind of lines: options,
  order, pairs or `= …` answers.

| kind | how to write it | what the user does |
|---|---|---|
| choice (`single`) | options `- [ ] text`, one right one `- [x] text` | picks one option |
| several answers (`multiple`) | several `- [x]` marked, or the `.multiple` class | ticks all that apply; part of the right ones gives «Partially correct» |
| order (`order`) | a numbered `1.` list in the right order | arranges the shuffled items |
| pairs (`pairs`) | lines `- left => right`, at least two | matches them; the right column is shuffled |
| gaps (`blanks`) | `___` in the question text and one `= answer` line per gap | fills in the words |
| number (`numeric`) | `= 42` or `= 2.5 ± 0.1` (`+-` works too) | enters a number; comma or dot |
| short answer (`text`) | `= answer \| another variant` | types an answer; case, `ё`/`е`, quotes and a final period do not matter |
| card (`card`) | the `.card` class, the back side as text under the heading | recalls, opens the answer and rates themselves |

Options need at least two, and at least one marked right; an order needs at least two items; a card has
no answer block, only the text of its back side.

## Tests

A code stage refers to a test file in `checks/`, and the files the work starts with go into `starter/`
and are listed in the header under `files`.

* Tests are an ordinary Python or JavaScript script; the language comes from the extension. The stage
  counts if the script runs to the end without an error: `assert` with a message is handy. Exiting
  before the end of the file — for example, `sys.exit(0)` in the solution file — does not count.
* Attempt files are imported by name (`from wordcount import count_words`) or by path
  (`from src.parser import parse`). Attempt files of the same language go into the sandbox.
* The run happens in a sandbox with a time limit: an infinite loop is a failure, not a hang. The test
  output is shown under the task.

## How format errors are shown

A challenge with errors cannot be started. The repository's **Learn** tab shows how many errors the
challenge files have, and the challenge page lists them: file, line and what is wrong. For example:

```text
parser/stages/03-code.md: the stage header has no `title`
parser/stages/03-code.md: unknown check `test`; available: quiz, self_check, files, contains, min_words, numeric, tests
parser/stages/02-questions.md: `quiz` refers to `checks/02.md`, and the challenge has no such file
parser/checks/02-questions.md:7: the question «What is HEAD?» has no id — add `{#short-id}` to the end of the heading
parser/checks/02-questions.md:15: question `merge-blank`: the text has 2 `___` gaps but 1 `= …` answer line
```

Fix the file and commit — the list updates. Attempts already started are not affected: each is pinned
to the version of the challenge it started with.

## Example: a challenge of questions

A challenge about git: two theory stages, two question stages and cards. It has no attempt repository —
only progress.

`git-basics/challenge.md`:

````markdown
---
title: "Git: commit, branch, merge"
summary: How git keeps history and why a branch is just a pointer
language: en
level: beginner
tags: [git, basics]
---

Git is not a «folder of copies» but a history of snapshots, where every snapshot knows its parent. Once that is clear, the other commands stop being scary: `commit` adds a snapshot, `branch` creates one more pointer, `merge` joins two lines of history into one.

In this challenge you will work through three ideas — snapshot, branch and merge — and reinforce them with questions. The questions will come back later in «Train» mode so you do not forget them.
````

`git-basics/stages/01-snapshots.md` — theory, no checks:

````markdown
---
title: History is a chain of snapshots
minutes: 5
---

## Working copy, index, history

Every repository has three places where a file lives:

1. **the working copy** — the files you edit in your editor;
2. **the index** — a draft of the next snapshot: what you marked with `git add` goes here;
3. **history** — the saved snapshots (commits).

`git commit` takes the whole index and saves it as a new snapshot. A snapshot has an author, a time, a message and a link to its **parent** — the previous snapshot. That is how the chain forms.

```text
A ← B ← C      (C knows about B, B about A)
```

## A hash is the name of a snapshot

Every commit is named by a hash of its content, for example `3f2a9c1`. Change even one letter in a file or the message and you get a different hash. That is why history cannot be swapped unnoticed: a swap changes the names of all the following snapshots.
````

`git-basics/stages/02-commit.md` — a question stage:

````markdown
---
title: Check yourself — snapshots
quiz: checks/02-commit.md
---

Five questions about the working copy, the index and history. Wrong answers come back at the end of the round — the stage counts once you answer each one correctly.
````

`git-basics/checks/02-commit.md` — single choice, multiple choice and order:

````markdown
---
title: Snapshots
---

## Which command puts a change into the index? {#add-to-index}

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

> The index is a draft of the next snapshot. `git add` fills it, and
> `git commit` saves it whole.

## What does `git commit` save? {#commit-saves}

- [ ] All files of the working copy, even unmarked ones
- [x] The content of the index
- [ ] Only the changed lines of the last file

> A commit takes the whole index: whatever was not marked with `git add` does not get into the snapshot.

## Put the path of a change from the editor to history in order {#path-of-change}

1. You edit a file in the working copy
2. `git add` — the change goes into the index
3. `git commit` — the index becomes a snapshot

## What does every commit know besides its files? {#commit-knows}

- [x] Its parent
- [x] Its author and time
- [ ] All its descendants
- [x] Its message

> A commit points back, to its parent. It does not know its descendants — they appear later.

## Why can an old commit not be swapped unnoticed? {#hash-chain}

- [ ] Git forbids editing files after a commit
- [x] The hash depends on the content, and a swap changes the names of all following snapshots
- [ ] Commits are stored on the server, not on your machine

> A hash is a fingerprint of the content and the parent. Change an old snapshot and the hashes of the whole chain after it change.
````

`git-basics/stages/03-branches.md` — the second theory stage:

````markdown
---
title: A branch is a pointer
minutes: 5
---

## A branch copies nothing

A branch in git is **a file with one hash**: it points at the last commit of its line. Creating a branch means writing one more pointer, so it is instant whatever the size of the project.

`HEAD` is a pointer to where you are now: usually to a branch. A new commit moves forward the branch `HEAD` points at.

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

## Merging

`git merge feature`, standing on `main`, joins two lines. If `main` has not moved in the meantime, git simply moves the pointer forward (**fast-forward**). If both moved, a **merge commit** with two parents appears. A conflict happens only when both lines edited the same lines.
````

`git-basics/stages/04-branch-questions.md`:

````markdown
---
title: Check yourself — branches and merging
quiz: checks/04-branches.md
---

Questions about pointers, `HEAD` and merging.
````

`git-basics/checks/04-branches.md` — pairs, a number, a short answer and a gap:

````markdown
---
title: Branches
---

## What is inside a git branch? {#branch-is}

- [ ] A copy of all project files
- [x] The hash of the last commit of its line
- [ ] A list of all the branch's commits

> A branch is a pointer. That is why it is created instantly and copies nothing.

## Match the term and its meaning {#pointer-pairs}

- branch => a pointer to the last commit of a line
- HEAD => where you are now
- merge commit => a snapshot with two parents

## How many parents does a merge commit of two branches have? {#merge-parents}

= 2

> A merge joins two lines of history, so such a snapshot has two parents.

## What is a merge called when git just moves the pointer forward? {#ff-name}

= fast-forward | fast forward

> It happens when the target branch has not moved in the meantime.

## The command `git ___ feature` on the `main` branch joins the `feature` line into `main` {#merge-blank}

= merge

## When does a merge conflict happen? {#conflict}

- [ ] Always, if the branches have a different number of commits
- [x] When both lines edited the same lines
- [ ] When the branches have different names

> Git joins different files and different places in one file by itself.
````

`git-basics/stages/05-cards.md` and `git-basics/checks/05-cards.md` — cards:

````markdown
---
title: Review cards
quiz: checks/05-cards.md
---

Four cards: recall the answer, open the back side and honestly rate whether you remembered. The rating decides when the card comes back in «Train».
````

````markdown
---
title: Cards
---

## What is the index in git? {#card-index .card}

A draft of the next snapshot: `git add` puts the changes that will go into the commit here.

## What is HEAD? {#card-head .card}

A pointer to where you are now — usually to the current branch.

## What is a fast-forward? {#card-ff .card}

A merge without a new commit: the target branch did not move, and git just moves its pointer forward.

## How is a commit different from saving a file? {#card-commit .card}

A commit is a snapshot of the whole index with an author, a time, a message and a link to its parent; its name is the hash of its content.
````

## Example: a stage with code and tests

The «Word counter in Python» challenge: the user writes `wordcount.py` in their attempt repository, and
tests check each stage.

```text
python-wordcount/
  challenge.md
  stages/
    01-task.md             theory: what counts as a word
    02-count.md            files + tests
    03-top.md              files + tests
    04-questions.md        quiz
  checks/
    02-count/test_count.py
    03-top/test_top.py
    04-questions.md
  starter/
    wordcount.py
```

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

````markdown
---
title: Count the words
files: [wordcount.py]
tests: checks/02-count/test_count.py
---

Write `count_words(text)`: how many words the text has, by the rule from the first stage.

```python
count_words("Cat, cat and CAT!")  # 4
count_words("")                   # 0
count_words("  \n\t ")            # 0
```

Make a change and press «Check» — or `git push`, and the check starts by itself. If a test fails, its output is shown below.
````

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

```python
from wordcount import count_words

assert count_words("Cat, cat and CAT!") == 4, "«Cat, cat and CAT!» is four words"
assert count_words("") == 0, "an empty string has no words"
assert count_words("  \n\t ") == 0, "only whitespace is zero words"
assert count_words("one-two three") == 3, "a hyphen is a separator"
assert count_words("Python 3 came out in 2008") == 6, "numbers are words too"
print("count_words: all checks passed")
```

`python-wordcount/starter/wordcount.py` — the attempt repository starts with this file:

```python
"""Word frequency analysis.

The functions appear stage by stage — start with `count_words`.
"""


def count_words(text):
    """How many words the text has."""
    raise NotImplementedError


def top_words(text, n):
    """The n most frequent words: a list of (word, count) pairs."""
    raise NotImplementedError
```

On the starter the test fails with `NotImplementedError`, so the stage does not count until the function
is written. It is handy to start your own challenge from a copy of these files: create a folder in your
repository, put `challenge.md` and `stages/` into it, commit and open the **Learn** tab.

---

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