# Руководство для ИИ-агента: сайт клуба

Ты — ИИ-агент (Claude Code или аналог), которому владелец сайта спортивного
клуба дал ключ API. Здесь написано, как устроен сайт, что тебе можно
менять, чего нельзя и в каком порядке работать. Адрес сайта ниже —
`https://giga-test.alted.tech`: на живом сайте `/api/agent.md` отдаёт этот текст
уже с его адресом.

Полный контракт — `https://giga-test.alted.tech/api/openapi.json` (OpenAPI 3.1).
Если этот текст и контракт расходятся, прав контракт.

## 1. Ключ

- Ключ выпускает человек в админке: «Настройки → Ключи API»
  (`https://giga-test.alted.tech/admin/`). У ключа есть роль и области; значение
  `cs_…` показывается один раз. Ключ в репозиторий, журналы и чаты не
  копируй.
- Каждый запрос идёт с заголовком `Authorization: Bearer cs_…`. Cookie и
  CSRF-токен не нужны.
- У ключа бывает срок: ключ для чата обычно выпускают на час. Срок —
  поле `expires_at`. Истёкший ключ получает `401 key_expired`: попроси
  человека выпустить новый.
- **Начинай с `GET /api/v1/capabilities`** — что этому ключу можно:
  - `kinds` — какие записи читать, создавать, менять и переводить
    (тарифы с ценами ключу только читать) и как правка попадает на сайт
    (`publishing`): `draft` — черновиком, на сайте после публикации;
    `status` — полем «Состояние» (мероприятия); `immediate` — сразу.
    **Перед правкой вида `immediate` скажи человеку, что она сразу будет
    на сайте;**
  - `layout`, `templates`, `upload` — раскладку и тему, шаблоны блоков,
    загрузку файлов;
  - `blocks` — какие блоки бывают на страницах;
  - `humans_only` — что делает только человек.

  Перескажи это человеку простыми словами, без имён полей и путей, и
  спроси, что он хочет поменять. Если просьба упирается в `humans_only`
  или в то, чего нет в ответе, скажи об этом сразу: «это сделаете вы в
  админке» или «этого ключ не позволяет, выпустите ключ с областью …».
- Имя, роль, области и разрешения ключа — `GET /api/v1/key`.

| Область | Что даёт |
|---|---|
| `read` | читать всё, что видит роль, кроме заявок |
| `content` | материалы, залы, инструкторы, справочники, фото |
| `translate` | писать переводы — со статусом «перевод модели» |
| `schedule` | расписание, замены, мероприятия |
| `layout` | раскладки страниц и тему — черновиком |
| `templates` | шаблоны блоков — черновиком |
| `publish` | публиковать и снимать с публикации то, что ключ может менять: материалы, раскладки, тему, шаблоны блоков — правка сразу на сайте |
| `requests:read` | читать заявки на пробную (персональные данные!) |

### Подключение по MCP

Тот же ключ подключает сайт как MCP-сервер — `https://giga-test.alted.tech/mcp`
(Streamable HTTP). Например, в Claude Code:

```bash
claude mcp add --transport http club https://giga-test.alted.tech/mcp \
  --header "Authorization: Bearer cs_…"
```

После `claude mcp add` перезапусти Claude Code: в уже открытой сессии
новые инструменты не появятся.

Инструменты — те же ручки: `what_can_i_do` (то же, что
`GET /api/v1/capabilities`), `read_guide` (этот текст), `get_schema`,
`list_records`, `get_record`, `create_record`, `update_record`,
`record_history`, `pending_translations`, `get_translation`,
`save_translation`, `list_blocks`, `check_block_template`,
`preview_link`, `get_page_html` (HTML страницы с черновиками и её
CSS-классы), `pending_publication` (что ждёт публикации), `import_photo`
(фото по ссылке), `read_page` (страница другого сайта по ссылке человека),
`publish_record` (с областью `publish`), `revert_record`
(откат к ревизии), `delete_record` (только черновики моделей, раздел 2) и
`call_api` для любой другой ручки из контракта. Права,
комментарий «зачем», сухой прогон (`dry_run`) и ошибки — как у API.

Чат, в котором ключ не вписать в заголовок (Claude в браузере — «Свой
коннектор» с адресом `https://giga-test.alted.tech/mcp`), входит по OAuth: человек
разрешает доступ в админке, выбирает области и срок (по умолчанию час) и
получает ключ, который виден в «Ключах API» и отзывается там же.

## 2. Чего нельзя ни с каким ключом

- **Удалять и отмечать перевод проверенным** — это делает человек в
  админке. На такие запросы ключ получит `403 needs_human`. Если считаешь,
  что что-то нужно удалить, напиши об этом человеку.
- **Исключение — черновики моделей.** Ключ может удалить запись с
  публикацией (материал, раскладку, шаблон блока…), если она ни разу не
  публиковалась и все её правки сделаны ключами того же человека, что
  выпустил твой ключ: например, неудачный опыт этой или прошлой сессии.
  В `capabilities` у таких видов `delete_drafts: true`. Удаление — с
  `?comment=…`, сначала с `?dry_run=1`; восстановит человек в админке.
- **Публиковать** можно только с областью `publish`; без неё — тоже
  `403 needs_human`, и тогда опубликует человек. С ней публикуй, когда
  человек в разговоре этого хочет, после предпросмотра:
  `POST /api/v1/<вид>/{id}/publish` с `comment` (у одиночной темы —
  `POST /api/v1/theme/publish`). Скажи, что выложено и что правку можно
  откатить.
- **Деньги, секреты и пользователи.** Цены, возвраты, ключи терминала,
  токены ботов, SMTP, пользователи, ключи API, настройки рассылки —
  закрыты.
- **Без комментария.** Каждое изменение по ключу несёт поле `comment` —
  зачем правка, одной-двумя фразами. Без него ответ `400 comment_required`.
- **Персональные данные** заявок (имя, телефон, e-mail, возраст ребёнка)
  не выноси за пределы задачи: не пересказывай их в общих отчётах, не
  сохраняй в файлы и не отправляй в сторонние сервисы.

## 3. Как устроен сайт

- **Сущности.** Клуб, залы, города, расписание, группы, мероприятия,
  инструкторы, материалы (`pages`), документы, альбомы, фото, справочники.
  У каждого вида одинаковые ручки: `GET /api/v1/<вид>` — список
  (`?deleted=1` — вместе с удалёнными), `POST` — создать,
  `GET|PATCH /api/v1/<вид>/{id}` — прочитать и изменить, `…/revisions`,
  `…/revert`; у видов с переводимыми полями — `…/translations/{lang}`
  (у раскладок, шаблонов блоков и темы переводов нет). Поля каждого вида:
  `GET /api/v1/schema`.
- **Ревизии.** Каждая запись хранит полные ревизии. Правка несёт `rev` —
  номер ревизии, которую ты прочитал: `{"rev": 7, "data": {…},
  "comment": "…"}`. Если за это время запись изменили, ответ будет
  `409 conflict`: перечитай запись и примени свою правку заново. В `data`
  передавай только те поля, которые меняешь.
- **Черновик и публикация.** У материалов, меню, раскладок, темы и
  шаблонов блоков есть публикация. Сайт показывает `published`, а твоя
  правка ложится в `data` последней ревизии. Публикует человек или ключ с
  областью `publish` (раздел 2). Сухой прогон публикации отвечает записью
  как она есть (`published` — что на сайте сейчас), а что будет после —
  в `after_publish`. Мероприятие выходит на сайт полем `status`
  (`draft` → `published`), остальные виды — сразу после правки.
- **Что ждёт публикации** — `GET /api/v1/pending`: неопубликованные
  черновики (в том числе прошлых сессий) и мероприятия-черновики, с
  последней правкой — когда, кто, зачем. Посмотри его в начале работы.
- **Сухой прогон.** `?dry_run=1` у любой изменяющей ручки возвращает
  результат без записи. Используй его перед каждой настоящей правкой.
- **Текст материалов** — документ TipTap (`{"type":"doc","content":[…]}`)
  с белым списком узлов (схема `RichDoc` в контракте). Проще писать
  Markdown: в поле богатого текста (`body`, `description`, `bio`) пришли
  `{"markdown": "## Заголовок\n\nТекст **жирный**, [ссылка](https://…)\n\n- пункт"}` —
  сервер сам сделает документ. Фото — отдельной строкой
  `![подпись](media:57)` (номер из медиатеки), видео — строкой
  `[видео](youtube:ID)` (`rutube:`, `vk:`), таблицы — `| … |`. Переносишь
  чужой сайт — можно прислать его HTML: `{"html": "…"}`; лишнее (стили,
  скрипты, классы) сервер отбросит. Прочитанный текст (`get_record`)
  приходит документом.
- **Вёрстка.** `GET /api/v1/page-html?path=/&part=header` отдаёт HTML
  страницы с черновиками, как в предпросмотре (`part` — `main`, `header`
  или `full`), и список её CSS-классов. Смотри его перед тем, как писать
  CSS темы или шаблон, и после правки.
- **Предпросмотр.** `POST /api/v1/preview` с `{"path": "/halls/"}` отдаёт
  ссылку на час. По ней сайт открывается в браузере без входа, с
  черновиками раскладок, меню, темы, шаблонов и материалов поверх
  опубликованного. Непроверенные переводы предпросмотр не показывает: их
  проверяет человек в админке. Ссылка ставит cookie на час:
  дальше по сайту можно ходить обычными адресами, предпросмотр остаётся.
  Выйти из него — ссылка «Выйти из предпросмотра» на полосе вверху
  страницы (`?preview=off`); скажи о ней человеку, если он открыл ссылку
  в своём браузере.
  Если сайт закрыт ещё и паролем веб-сервера (тестовый стенд), его
  нужно передать отдельно.
- **Публичный слой** выводится кодом, шаблонами его не сломать:
  `/llms.txt`, `/llms-full.txt`, `/data/*.json`, разметка schema.org,
  `/sitemap.xml`, `/robots.txt`.
- **Помощь для людей** — `GET /api/v1/help` (список) и
  `GET /api/v1/help/{slug}?lang=ru|en` (Markdown): как делать в админке
  обычные дела. Если человек спрашивает «как мне самому…», отвечай по ней
  и давай ссылку `/admin/help/<slug>`.

## 4. Порядок работы

1. **Прочитай.** `GET /api/v1/capabilities`, затем нужные записи, раскладку,
   шаблон, очередь переводов.
2. **Предложи.** Скажи человеку, что собираешься изменить и зачем, если
   задача шире одной очевидной правки.
3. **Сухой прогон.** Отправь ту же правку с `?dry_run=1`; для шаблона блока
   вызови `POST /api/v1/block-templates/check`. Исправь всё, на что
   указали ошибки.
4. **Примени** — тот же запрос без `dry_run`, с `rev` и `comment`.
5. **Проверь предпросмотр.** Возьми ссылку `POST /api/v1/preview` и сними
   экраны на телефоне (ширина 390 px) и на десктопе (1280 px) — каждой
   затронутой страницы, на каждом языке сайта. Проверь, что ничего не
   уехало, текст читается, кнопка записи на месте. Если снять экраны
   нечем (например, в веб-чате), сверь HTML через
   `GET /api/v1/page-html` и попроси человека открыть ссылку на телефоне и
   компьютере — не выдавай вид за проверенный.
6. **Опубликуй или отдай на публикацию.** Опиши человеку, что сделано,
   приложи экраны и ссылку предпросмотра. Если у ключа есть `publish` и
   человек хочет выложить правку — опубликуй сам; иначе опубликует он в
   админке.

**Перенести текущий сайт клуба.** Если человек дал ссылку на свой нынешний
сайт, прочитай его: `POST /api/v1/read-page` с `url` (по MCP — `read_page`).
В ответе — заголовок, текст страницы (заголовки с `#`, пункты с `- `), фото и
ссылки на другие страницы того же сайта. Прочитай главную, потом страницы о
тренерах, залах, расписании и контактах, и заведи всё по шагам выше. Фото
переноси `POST /api/v1/uploads/from-url` по их адресам, с подписью для
незрячих. Цены тебе закрыты (раздел 2): их переносит человек. Чего на старом
сайте нет — не придумывай. Чужие сайты без просьбы человека не читай.

## 5. Как переводить

Сайт показывает перевод только после того, как его проверил человек. Твой
перевод попадает в очередь со статусом «перевод модели».

1. `GET /api/v1/site` — языки сайта (`languages`, `default_language`).
2. `GET /api/v1/translations?lang=en` — что не переведено (`missing`) или
   устарело после правки оригинала (`outdated`). У каждой позиции есть
   `kind`, `id`, `title`. Записанный перевод уходит из этого списка в
   состояние `unverified` — его видно с
   `?lang=en&state=unverified` (поле `status` — `machine`, если
   переводила модель).
3. `GET /api/v1/<вид>/{id}` — оригинал. `GET
   /api/v1/<вид>/{id}/translations/en` — текущий перевод и его `rev`
   (`0`, если перевода ещё нет).
4. `PUT /api/v1/<вид>/{id}/translations/en?dry_run=1`, затем без
   `dry_run`:

   ```json
   {"rev": 0, "status": "machine", "comment": "Перевод на английский", "data": {"title": "…", "summary": "…", "body": {"type": "doc", "content": […]}}}
   ```

   В `data` идут только переводимые поля вида (в `GET /api/v1/schema` у
   них `translatable: true`, в контракте — `x-translatable`). Сухой
   прогон отвечает переводом, каким он был бы, но без состояния
   проверки: `state` и `updated_at` в нём не окончательные. Имена ручек — во множественном числе через дефис:
   `pages`, `halls`, `events`, `schedule-slots`…
5. В документе TipTap переводи только текст. Структуру, номера фото
   (`media`), ссылки (`href`), ссылки на документы и метки оставляй как
   есть. Имена людей и названия залов пиши так, как принято в языке
   перевода (транслитерация), если в клубе нет устоявшегося написания.
6. Скажи человеку, что перевод ждёт проверки в админке: «Переводы»,
   метка «перевод модели».

## 6. Как править раскладку страницы

- Раскладка — запись вида `layout` на страницу: `page` — это `home` или
  раздел (`halls`, `schedule`, `events`, `about`, `contacts`, `trial`, …).
  Данные раскладки — секции из блоков:

  ```json
  {"page": "home", "sections": [
    {"blocks": [{"type": "hero", "width": 12, "params": {"title": "Айкидо в Москве"}}]},
    {"blocks": [{"type": "next_classes", "width": 8}, {"type": "trial_button", "width": 4, "params": {"label": "Записаться"}}]}
  ]}
  ```

- Ширина считается в долях сетки из 12 и может быть 12, 8, 6, 4 или 3. Типы
  блоков (имя типа — поле `name`), их параметры и встроенные шаблоны
  отдаёт `GET /api/v1/blocks`. На странице раздела должен быть ровно один
  блок `content` — сам раздел.
- **Параметры блока не переводятся**: раскладка одна на все языки. Текст,
  заданный параметром (`title`, `label`…), будет на каждом языке одним и
  тем же. Если параметр оставить пустым, блок возьмёт свою строку
  интерфейса, которая уже переведена (у кнопки записи — «Записаться на
  пробную» / «Book a trial class»). На сайте с несколькими языками задавай
  текстовые параметры только тогда, когда это действительно нужно, и скажи
  об этом человеку.
- Параметр типа `ref` — номер записи вида из `ref` (например материал для
  блока `page`, альбом для `gallery`); `media` — номер фото из медиатеки
  (`GET /api/v1/media`).
- Раздел, в котором нет записей, сайт в меню не показывает, даже если
  он есть в меню. Такие разделы — `hidden_sections` в `GET /api/v1/site`;
  скажи о них человеку.
- **Фото.** Номер фото — из `GET /api/v1/media`. Нового фото в медиатеке
  нет — загрузи его по ссылке: `POST /api/v1/uploads/from-url` с `url`
  (`https` или `http`), `alt` (что на фото) и `comment`. Если ссылки нет,
  попроси человека загрузить фото в админке, в «Медиатеку», и найди его
  в списке.
- `GET /api/v1/layouts` — есть ли уже раскладка у страницы. Если записи
  нет, сайт показывает встроенную раскладку (`defaults` в ответе
  `GET /api/v1/blocks`); чтобы её изменить, начни с неё и создай
  запись: `POST /api/v1/layouts` с `{"data": {…}, "comment": "…"}`. Если
  запись есть, правь её: `PATCH /api/v1/layouts/{id}` с `rev`.
- Затем открой предпросмотр этой страницы (раздел 4, шаг 5).

## 7. Как править шаблон блока

- Шаблон блока — `html/template` Go. `GET /api/v1/blocks` отдаёт
  встроенный шаблон каждого типа (`template`) и его параметры. Начинай с
  встроенного шаблона и меняй его, а не пиши с нуля.
- Что доступно в шаблоне: `.Data` — данные блока; что в них лежит, пишет
  `GET /api/v1/blocks` у каждого типа в поле `data`: `kind` — `list`
  (`{{range .Data}}`), `one` или `none`, `fields` — поля записи, например
  `Name string`, `Slug string`, `Instructor.Label string` (вложенное поле
  может отсутствовать — оборачивай в `{{with}}`). Поля самого значения
  (`.Width`, `.Lang`, `.Club.Name`…) — в `value_fields`. `.Param "имя"` —
  параметр блока.
- Помощники — методы значения: `.T "ключ"` (строки интерфейса),
  `.URL "/путь/"` (адрес на текущем языке), `.Date`, `.DateTime`,
  `.DayMonth`, `.Weekday`, `.WeekdayOf`, `.Rich` (текст материала),
  `.Picture id "sizes" "class"`, `.Thumb id ширина`, `.Alt id`, `.Full id`,
  `.Lines`, `.Tel`. Номер фото можно передавать прямо из параметра:
  `{{$.Picture ($.Param "photo") "100vw" "hero-photo"}}`. Фото блоков
  первой секции страницы грузятся сразу, остальные — по мере прокрутки.
  `.Page "имя"` — материал из параметра-ссылки (`URL`, `Title`,
  `Summary`…), `.Album "имя"` — фото альбома из параметра (`Photo`,
  `Caption`, `Alt`…); пусто, если ссылки нет или запись не на сайте. Внутри `{{range}}` до них дотянись через `$`: `{{$.URL "/trial/"}}`.
  Кроме них работают встроенные функции шаблонов Go: `eq`, `ne`, `lt`,
  `le`, `gt`, `ge`, `and`, `or`, `not`, `len`, `index`, `slice`, `printf`.
- **Свой текст на нескольких языках.** Строки интерфейса сайта берутся
  через `.T "ключ"`; проверка шаблона сообщает о неизвестном ключе (сайт
  показал бы сам ключ). Своих строк шаблон добавить не может — текст,
  которого нет среди строк сайта, пиши по языкам:
  `{{if eq $.Lang "en"}}Book a class here{{else}}Записаться в этот зал{{end}}`,
  и скажи человеку, что на новом языке сайта текст нужно будет дописать.
- Ссылка на запись в конкретный зал: `{{$.URL "/trial/"}}?hall={{.Slug}}`
  (у занятий — `.HallSlug` и `.DisciplineSlug`).
- Запрещено: `<script`, `<style`, `<iframe`, обработчики `on…=`,
  `javascript:`. Оформление — классами; свой CSS идёт в поле `css` темы
  (`PATCH /api/v1/theme`).
- **Классы сайта.** Шапка — `.site-header` (внутри `.site-header-row`,
  `.site-name`, меню `.site-nav`, `.site-langs`, `.site-city`, на телефоне
  кнопка `.menu-toggle`), содержимое — `.site-main`, подвал —
  `.site-footer`. Строка раскладки — `.layout-row`, блок в ней —
  `.block .w-12` (ширина). Блоки: `.hero` (`.hero-tagline`,
  `.hero-actions`, `.hero-photo`), `.home-block`, карточки `.cards` →
  `.card` (`.card-media`, `.card-body`, `.card-meta`), кнопки `.button` и
  `.button-secondary`, текст материала `.rich`. У `<body>` есть класс
  страницы — `page-home`, `page-halls`, `page-events`… (у страницы
  записи раздела — `page-halls-item`): им CSS темы ограничивается одной
  страницей (`.page-home .hero {…}`). Точный HTML —
  `GET /api/v1/page-html`. Стили сайта сильнее простых селекторов:
  проверь, что твои правила действительно применились.
- Сначала проверь шаблон:

  ```bash
  curl -s -X POST https://giga-test.alted.tech/api/v1/block-templates/check \
    -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
    -d '{"type": "halls", "template": "<ul class=\"halls-list\">{{range .Data}}<li><a href=\"{{.URL}}\">{{.Name}}</a></li>{{end}}</ul>"}'
  ```

  В ответе:
  - `problems` — ошибки; `line` — строка шаблона, если её удалось
    определить;
  - `used_on` — страницы (с языком), где этот тип стоит в черновике или
    опубликованной раскладке. Пусто — блок нигде не стоит;
  - `pages` — разница с опубликованным сайтом, только по страницам, где
    HTML меняется. Пустые `pages` при непустых `used_on` значат, что сайт
    не изменится;
  - `draft_pages` — разница с черновиками, если на этих страницах лежат
    неопубликованные раскладки или шаблоны;
  - `samples` — HTML блока на эталонных данных (`empty` и `long`) на
    каждом языке, с адресами и словами этого языка: так видно, как блок
    рисуется, даже на пустом сайте.

  Пока `problems` не пуст, не сохраняй.
- Где стоит блок, видно по раскладкам: `GET /api/v1/layouts` и
  `defaults` в `GET /api/v1/blocks`. Во встроенных раскладках блоки
  (`halls`, `next_classes`, `events`…) стоят только на главной, а разделы
  (`/halls/`, `/schedule/`…) рисуются блоком `content` — его шаблон не
  меняется. Предпросмотр делай для тех страниц, где блок стоит.
- Сохрани черновик: `POST /api/v1/block-templates` с `{"data": {"type":
  "halls", "template": "…"}, "comment": "…"}` (переопределение
  встроенного) или `PATCH /api/v1/block-templates/{id}`. При сохранении
  проверка повторяется, отказ приходит как `400 template_check`.
- Новый тип блока — та же запись с новым `type` и `params`: `[{"name":
  "text", "type": "string"}]`. Типы параметров — `string`, `text`,
  `integer`, `boolean`, `enum` (с `enum` — список значений), `media`
  (фото) и `ref` с `"ref": "page"` или `"album"`. Новый тип появится на
  странице, только когда его поставят в раскладку.
- У переопределения встроенного типа `params` не задаётся: он сохраняет
  свои параметры. Проверка ответит на это так же, как сохранение.
- Если на работающем сайте шаблон всё-таки сломается, сайт пропустит этот
  блок, а не упадёт. Предпросмотр всё равно обязателен.

## 8. Запись на пробную из чата

Если ты помогаешь человеку записаться (а не правишь сайт), ключ не нужен:

- `GET /llms.txt` — клуб, залы, направления (их `slug`), как записаться.
  `GET /data/halls.json`, `/data/schedule.json` — залы и расписание.
- `POST /api/trial` создаёт черновик заявки (`hall` и `discipline` — их
  `slug` с этого сайта; ниже — пример):

  ```json
  {"name": "Анна", "age": 28, "phone": "+7 900 111-22-33", "hall": "zal-na-arbate", "discipline": "aikido-adults", "preferred": "вторник вечером"}
  ```

  Ответ — `confirm_url` и шестизначный `code`. Отдай ссылку человеку.
  Клуб получит заявку только после того, как человек сам проверит
  данные, даст согласие на обработку персональных данных и нажмёт
  «Записаться». За человека согласие не даётся. Черновик живёт 24 часа;
  с одного адреса — не больше 5 черновиков в час.
- Если делать запросы ты не можешь, дай ссылку с заполненной формой:
  `https://giga-test.alted.tech/trial/?name=Анна&age=28&phone=…&hall=zal-na-arbate&preferred=…`.
- Денег ты не принимаешь и оплату не начинаешь.

## 9. Ошибки

Тело ошибки: `{"error": {"code": "…", "message": "…", "fields": {…}, "details": {…}}}`.
`fields` — путь поля → код, `details` — тот же путь → пояснение
по-английски там, где кода мало (схема параметров, параметры блока).
У `taken` пояснение называет запись, которая уже занимает значение, и её
адрес: раскладка страницы, шаблон типа блока, материал раздела, адрес
(`slug`) — правь её (`PATCH`), а не создавай другую.

| Код | Что делать |
|---|---|
| `400 validation` | поправь поля из `fields`; что именно не так — в `details` |
| `400 comment_required` | добавь `comment` |
| `400 template_check` | смотри `problems`, исправь шаблон |
| `401 unauthorized` | ключ неверный или отозван — скажи человеку |
| `401 key_expired` | срок ключа вышел — попроси выпустить новый |
| `403 forbidden` | у ключа нет области — попроси человека её добавить |
| `403 needs_human` | это делает человек в админке |
| `409 conflict` | запись изменили: перечитай и примени правку заново |
| `429 rate_limited` | подожди |
