Каталог openspec/ был предпосылкой, о которой канон говорил, но за которой не следил. openspec/specs/ объявлен домом темы requirements, config.yaml описан абзацем — а заводилось всё руками, и не проверялось ничего. Новый проект выходил из init с полным каноном документов и без каталога, без которого не работают ни opsx:propose, ни ревью дизайна, ни сверка требований. Теперь init делает openspec init --tools claude шагом 3, до первого документа, а adopt заводит его тем же способом, если на переводимом проекте его нет. Команда названа поимённо в трёх местах — скилле, каноне и отказе docs.py: отказ без команды заставляет искать её в другом месте. Файл из коробки оказался хуже отсутствующего, и потому проверяется машиной. openspec init кладёт config.yaml, где context и rules — закомментированный пример на английском. Такой файл читается как настроенный: он есть, он валиден, имя правильное. Работает он как пустой, и узнаётся это по уже написанному предложению — на другом языке, с capability по имени пакета, без единого SHALL. docs.py проверяет четыре вещи, каждая про молчащий пробел: каталог есть; имя именно config.yaml (config.yml OpenSpec не читает и об этом не сообщает); context и rules.specs не остались примером, а правила называют SHALL; context называет passport и CLAUDE.md. Последние два обязательны по порядку работы: предложение пишется до того, как кто-либо откроет docs/, и без этих строк его пишут, не зная ни границы домена, ни инвариантов. Форма конфига записана скелетом и сформулирована разрезом: утверждение, которое можно опровергнуть, открыв другой файл проекта, — пересказ; строка, которая говорит, какой файл открыть, — ссылка. Машина этот разрез не проверяет, отличить одно от другого она не умеет; он отдан doc-consistency отдельным абзацем правила «один факт — один дом», и config.yaml добавлен ему во вход. Место второго дома там самое частое: context читается при порождении каждого артефакта, туда удобно дописать «чтобы агент знал», и так заводятся копии инвариантов, конвенций, состава гейта и правил ревью. Образец лёг в канон, а не в конвейер, как планировало решение C: форма документа принадлежит владельцу канона документов, конвейер её читатель. Иначе av-dev-pipeline завёл бы описание файла, который заводит и проверяет av-dev-pm. Канон повышен до версии 7 с записью, выполнимой upgrade: завести openspec, привести config.yaml к скелету, вычистить из context пересказ, проверить имя файла, поднять номер в .pm.json. Проверка прогнана на четырёх фикстурах — свежий openspec init, два живых проекта и пустой каталог; отличает все четыре случая. Решение — 47. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
505 lines
27 KiB
Markdown
505 lines
27 KiB
Markdown
# Скелеты документов канона
|
||
|
||
Что кладут `init` и `canon adopt` в незаполненный слот. Правило одно:
|
||
**честная информативная строка вместо заглушки**. Проход читает строку как факт;
|
||
`<!-- заполнить: … -->` он читает как пробел, и `docs.py check` о таком
|
||
плейсхолдере напоминает.
|
||
|
||
Плейсхолдер ставится только там, где ответ **обязан** быть и его не спросили.
|
||
Всё, чего в проекте пока просто нет, описывается словами, а не плейсхолдером.
|
||
|
||
**Шаблоны — единственное место, где правило канона копируется намеренно.**
|
||
`adr/README.md` и `review.md` уезжают в репозиторий проекта и обязаны там что-то
|
||
говорить; определение при этом остаётся в [canon.md](canon.md). Отсюда
|
||
обязанность: **правка такого правила в каноне тянет запись в
|
||
[changelog.md](changelog.md)** — и запись называет, какой файл проекта поднимает
|
||
`upgrade`. Без этого копия в проекте останется на старой версии молча.
|
||
|
||
**Каждая такая копия помечена и сверяется машиной.** Дом обрамляется
|
||
`<!-- дом: <id> -->` … `<!-- /дом: <id> -->`, копия —
|
||
`<!-- копия: <id> из <путь> -->` … `<!-- /копия: <id> -->`;
|
||
`scripts/copies.py` маркетплейса требует дословного
|
||
совпадения. Комментарии невидимы в отрендеренном markdown и уезжают в проект
|
||
вместе со скелетом — там они говорят читателю, что у текста есть дом. Правишь
|
||
текст внутри маркеров — правь дом, а не копию.
|
||
|
||
## `docs/passport.md`
|
||
|
||
```markdown
|
||
# Паспорт проекта
|
||
|
||
Зачем это и для кого. [architecture.md](architecture.md) отвечает «как
|
||
устроено», [tasks/ROADMAP.md](tasks/ROADMAP.md) — «в каком порядке», паспорт —
|
||
«зачем и для кого».
|
||
|
||
## Цель
|
||
|
||
<!-- заполнить: одна фраза без технических деталей -->
|
||
|
||
**Потребители** — список закрытый: он определяет, что считать нужным, а что
|
||
интересным.
|
||
|
||
| Кто | Что ему нужно от нас |
|
||
| --- | --- |
|
||
|
||
Цель достигнута, когда:
|
||
|
||
## Что целью не является
|
||
|
||
Граница домена. По ней в теме `architecture` судят, не перенесено ли понятие
|
||
через границу.
|
||
|
||
## Типовые сценарии
|
||
|
||
## Референсы
|
||
|
||
Где смотреть prior art, когда упёрлись.
|
||
```
|
||
|
||
## `docs/architecture.md`
|
||
|
||
```markdown
|
||
# Архитектура
|
||
|
||
Обзор: как сложено и где что работает. **Поведение системы здесь не
|
||
описывается** — его нормативный дом `openspec/specs/`.
|
||
|
||
## Принципы
|
||
|
||
## Компоненты
|
||
|
||
Каждый — строкой со ссылкой на capability, а не пересказом её требований.
|
||
|
||
## Внешние границы и форматы
|
||
|
||
## Эксплуатация
|
||
|
||
- Где работает, что рядом, кто перезапускает:
|
||
- Внешние зависимости поимённо и чем каждая отказывает (падает, отвечает
|
||
медленно, молчит, отдаёт мусор):
|
||
- Кто заметит отказ и когда:
|
||
- Характер потока (непрерывный, по запросу, по расписанию):
|
||
|
||
## Единые точки проекта
|
||
|
||
Где генерируются идентификаторы и время; где единственный парсер входного
|
||
формата; где маппинг доменной ошибки в код ответа; где общий путь приёма.
|
||
Материал для вопроса «не появился ли второй способ делать то, что уже делается».
|
||
|
||
## Деплой
|
||
|
||
## Открытые вопросы
|
||
```
|
||
|
||
Пустой проект: «Архитектуры пока нет: кода нет. Наполняется первой задачей.»
|
||
Нет внешних зависимостей: «Внешних зависимостей нет — смотри на диск и на СУБД.»
|
||
|
||
## `docs/database.md`
|
||
|
||
```markdown
|
||
# Схема хранилища
|
||
|
||
СУБД, миграции, правило времени и идентификаторов.
|
||
|
||
## Таблицы
|
||
|
||
## Представление данных
|
||
|
||
Чем физически лежит запись и что происходит при чтении и записи.
|
||
|
||
## Настройки с числовым значением
|
||
|
||
Таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен.
|
||
Без них замер не превращается в находку: пик памяти — аномалия только рядом
|
||
со строкой «запись лежит сжатой и распаковывается целиком».
|
||
```
|
||
|
||
Нет БД — файла нет, и в `docs/.pm.json` нет ключа `migrations`.
|
||
|
||
## `docs/security.md`
|
||
|
||
```markdown
|
||
# Модель угроз
|
||
|
||
## Периметр
|
||
|
||
<!-- заполнить: первой строкой, против кого защищаемся -->
|
||
|
||
Контур не развёрнут — назови оба периметра, целевой и сегодняшний, и скажи
|
||
прямо, против какого строятся находки.
|
||
|
||
## Недоверенный вход
|
||
|
||
Что приходит извне и каким каналом: тело запроса, файл, аргумент команды,
|
||
ответ внешней системы, содержимое архива.
|
||
|
||
## Из чего строятся пути и ключи
|
||
|
||
Раскладка файлов на диске, состав координатного ключа записи, имя каталога.
|
||
Отсюда строится выход за пределы песочницы.
|
||
|
||
## Что разграничивает доступ
|
||
|
||
## Что чувствительнее чего
|
||
|
||
## Что вне модели
|
||
|
||
Перечислить явно. Пустой пункт означает, что в теме `security` угрозу выдумают
|
||
за тебя, и находка никогда не будет исправлена.
|
||
```
|
||
|
||
## `docs/conventions/README.md`
|
||
|
||
```markdown
|
||
# Конвенции кода
|
||
|
||
Как мы пишем код — в отличие от `openspec/specs/`, который описывает, что
|
||
система делает.
|
||
|
||
**Прозой остаётся только то, что не выражается правилом.** Свойство, ставшее
|
||
правилом линтера, отсюда удаляется и переезжает в перечень ниже.
|
||
|
||
## Записи
|
||
|
||
## Механизировано
|
||
|
||
| Правило | Где механизировано |
|
||
| --- | --- |
|
||
|
||
Не названное здесь место механизации означает, что проход по конвенциям будет
|
||
добросовестно проверять уже проверенное.
|
||
```
|
||
|
||
Пустой проект: «Конвенций пока нет: код не написан. Наполняется по мере
|
||
реального трения, а не вперёд.»
|
||
|
||
## `docs/research/README.md`
|
||
|
||
```markdown
|
||
# Разведка
|
||
|
||
Наблюдения за внешним миром: что реально шлёт источник, чем документация
|
||
формата расходится с практикой. Источник истины — этот каталог, а не чужая
|
||
документация.
|
||
|
||
**Каждый вывод — с числами и командой, которой получен**, чтобы его можно было
|
||
перепроверить.
|
||
|
||
## Как снималось
|
||
|
||
## Записи
|
||
```
|
||
|
||
Нет внешних источников: «Внешних источников данных нет — разведка неприменима.»
|
||
|
||
## `docs/adr/README.md`
|
||
|
||
```markdown
|
||
# Журнал решений
|
||
|
||
Одна запись — одно решение. **ADR это промоут поверх архивного `design.md`**,
|
||
а не второе сочинение: запись цитирует решение и ссылается на
|
||
`openspec/changes/archive/<id>/design.md`.
|
||
|
||
## Когда заводить
|
||
|
||
Верно одно из трёх:
|
||
|
||
<!-- копия: adr-когда-заводить из av-dev-pm/skills/canon/references/canon.md -->
|
||
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
|
||
- **намеренный отказ** от очевидного подхода;
|
||
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
|
||
«заменено на».
|
||
<!-- /копия: adr-когда-заводить -->
|
||
|
||
Не заводить для рутины и для того, что видно из кода и `git log`.
|
||
|
||
## Соглашения
|
||
|
||
- Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, дата — когда решение реально принято.
|
||
Слаг **английский по сути, а не транслитом**: `queue-as-table`, не
|
||
`ochered-tablicej`. Форму имени и слаг проверяет `docs.py check`.
|
||
- Записи неизменяемы: передумали — новая запись, старой ставится статус.
|
||
- Активная запись статуса не имеет. Значений два: `заменено на ADR-…` и
|
||
`устарело`; ставятся полем меты записи — `- **Статус:** …` рядом с датой и
|
||
источником, а не абзацем в теле.
|
||
|
||
## Записи
|
||
|
||
Новые сверху.
|
||
|
||
| Дата | Запись | Статус |
|
||
| --- | --- | --- |
|
||
```
|
||
|
||
## `docs/adr/template.md`
|
||
|
||
```markdown
|
||
# Краткий заголовок решения
|
||
|
||
- **Дата:** ГГГГ-ММ-ДД
|
||
- **Источник:** openspec/changes/archive/<id>/design.md
|
||
|
||
Статус ставится тем же полем и только при пересмотре:
|
||
`- **Статус:** заменено на ADR-…` либо `- **Статус:** устарело`.
|
||
У активной записи поля нет.
|
||
|
||
## Решение
|
||
|
||
Что именно решено — одной фразой.
|
||
|
||
## Почему
|
||
|
||
Намерение и причина. Цитата из источника, а не пересказ. Пиши так, чтобы через
|
||
год было понятно без чтения переписки.
|
||
|
||
## Последствия
|
||
|
||
- `+` что стало лучше.
|
||
- `−` чем платим: ограничения, риски, нагрузка на поддержку.
|
||
```
|
||
|
||
## `docs/review.md`
|
||
|
||
```markdown
|
||
# Ревью: настройка и журнал
|
||
|
||
## Как настроен конвейер
|
||
|
||
### Типовые узлы
|
||
|
||
Рода узлов проекта и 3–5 проверяемых свойств к каждому. Рода, а не инвентарь
|
||
пакетов: род, который проект задумал, но ещё не написал, включать полезно.
|
||
|
||
### Типовые ложноположительные
|
||
|
||
Находки, которые здесь выглядят убедительно и всегда неверны. Каждая — с одной
|
||
строкой «почему здесь это не дефект».
|
||
|
||
### Вопросы по темам
|
||
|
||
Форма: `<тема>: <вопрос> (<провенанс>)`. Главный источник — журнал ниже. Вопрос
|
||
задаёт тот проход, который закрывает эту тему на текущем прогоне, дополнительно
|
||
к обязательным.
|
||
|
||
**Адресуй теме, а не имени прохода.** Проходы переезжают между метками и
|
||
упраздняются; вопрос, адресованный проходу, перестанет задаваться в тот день,
|
||
когда тот уедет в старшую метку, — и заметить это будет нечем. Тема переезд
|
||
переживает.
|
||
|
||
Темы ядра: `requirements`, `autotests`, `conventions`, `architecture`,
|
||
`security`, `operations`. Плюс любая своя — та, под которую проект завёл в
|
||
`docs/` **свой** документ. Документы категорий `источник` и `процессный` тем не
|
||
порождают, и адресовать вопрос `passport`, `database`, `adr`, `research` или
|
||
`review` нельзя — таких тем нет. Вопрос про границу домена адресуй
|
||
`architecture`, вопрос про хранилище и числа — `operations`.
|
||
|
||
### Триггеры метки
|
||
|
||
Проектная конкретизация правила выбора метки. **Списка три: по одному на
|
||
каждую ось вверх и один вниз** — поимённо, узлами или capability.
|
||
|
||
**Крупное здесь** — про объём: что трогает несколько узлов или слоёв, переносит
|
||
ответственность между ними, перекладывает существующий код в новую форму.
|
||
|
||
**Незнакомое здесь** — про форму решения: то, чего в проекте ещё не было и чью
|
||
форму предстоит нащупать по ходу. Признак простой: перед работой нельзя назвать,
|
||
какие узлы будут тронуты.
|
||
|
||
Любая из двух осей поднимает прогон до `large`, старшей метки: там `security`,
|
||
`operations` и `architecture` проверяют запуском, и там же единственные замеры.
|
||
Метка рассчитана на **5–10% задач**; если сюда попадает каждая третья, списки
|
||
написаны слишком широко.
|
||
|
||
**Мелкое здесь** — опускает до `small`. Ориентир по доле — до трети задач, и в
|
||
любом случае меньше, чем `medium`: перевес `small` значит, что рабочее умолчание
|
||
сместилось само. Помни отрицательный тест конвейера: что
|
||
после мерджа не откатывается обратной правкой (миграция, формат на диске,
|
||
публичный контракт, имя), — не `small`, каким бы маленьким ни был дифф.
|
||
|
||
Уточняет умолчания конвейера, не отменяет их; рабочее умолчание — `medium`.
|
||
|
||
### Недоступно проверке
|
||
|
||
Оба подраздела — **по темам**: «в теме `operations` не проверяется X» читается,
|
||
а «не проверяется X» через месяц не найдёт ни один проход.
|
||
|
||
**Не проверит ни один проход** — принципиальная граница; по факту промаха не
|
||
пересматривается.
|
||
|
||
**Перестали проверять сознательно** — что, когда и почему, со ссылкой на запись
|
||
журнала. Пересматривается **первым**, как только что-то проскочило.
|
||
|
||
Тему, у которой в проекте нет дома, сюда писать не надо: её называет план
|
||
каждого прогона, и это честнее разовой записи.
|
||
|
||
## Журнал дефектов
|
||
|
||
Запись на каждый воспроизведённый дефект, сразу, а не ретроспективно: со
|
||
временем теряется не факт, а то, почему дефект не поймали.
|
||
|
||
Форма:
|
||
|
||
<!-- копия: журнал-дефектов-форма из av-dev-pipeline/skills/review-pipeline/references/review-journal.md -->
|
||
## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман]
|
||
|
||
- **Где:** путь:строка либо «конвейер, а не код»
|
||
- **Симптом:** как обнаружилось, кем и когда
|
||
- **Причина:** что на самом деле было не так
|
||
- **Чем воспроизведён:** тест, команда, замер — с числами
|
||
- **Почему не поймали:** только для проскочивших — какой проход обязан был найти
|
||
и что ему помешало
|
||
- **Что меняем:** правило прохода, шаг гейта, конвенция, факт в документе
|
||
проекта — либо «ничего, цена поимки выше цены дефекта»
|
||
<!-- /копия: журнал-дефектов-форма -->
|
||
```
|
||
|
||
Новый проект: «Дефектов пока не было. Настройка конвейера появится с первым
|
||
ревью.»
|
||
|
||
## `CLAUDE.md`
|
||
|
||
Лежит в корне, не в `docs/`. Единственный файл канона, который агент читает
|
||
**всегда**, поэтому в нём то, без чего нельзя сделать ни шага.
|
||
|
||
```markdown
|
||
# CLAUDE.md
|
||
|
||
Памятка для работы над <проект>. Перед задачей прочитай также
|
||
[docs/passport.md](docs/passport.md), [docs/architecture.md](docs/architecture.md)
|
||
и [docs/conventions/](docs/conventions/README.md).
|
||
|
||
## Что это
|
||
|
||
Абзац: что делает и чего **не** делает.
|
||
|
||
## Стек
|
||
|
||
## Инварианты
|
||
|
||
Что нарушать нельзя. Каждый пункт — три вещи: формулировка **как проверяемое
|
||
свойство**, а не лозунг; последствие нарушения и его обратимость; **severity**
|
||
рядом. По этим формулировкам проходы ревью присваивают `critical`, поэтому
|
||
severity стоит здесь, а не выводится каждым проходом заново.
|
||
|
||
## Команды
|
||
|
||
## Гейт
|
||
|
||
- Команда целиком и как определяется база диффа:
|
||
- Где логи шагов:
|
||
- Что означает каждый исход:
|
||
- **Что красит безусловно и почему:**
|
||
- Чего в гейте намеренно нет и **кто тогда обязан это гонять:**
|
||
|
||
## Запреты
|
||
|
||
Что запускать нельзя, **с путями**: рабочая БД, боевой каталог данных, внешние
|
||
сервисы. Плюс где `testdata` и куда писать временное.
|
||
|
||
## Работа
|
||
|
||
- **Основная ветка:** <имя>
|
||
- **Необратимое** (спрашивается у человека всегда):
|
||
- **Общий станок** — какая проверка, покраснев, врывается в замороженный спринт:
|
||
- **Ориентир по размеру спринта:** 5–8 задач, ориентир а не закон
|
||
- **Что такое «сделана»:** пайплайн проекта пройден + критерии приёмки проверены
|
||
поимённо
|
||
|
||
## Язык
|
||
|
||
- Документация, комментарии, сообщения коммитов — русский.
|
||
- Код и идентификаторы — английский.
|
||
```
|
||
|
||
Имя основной ветки, запреты с путями и «что необратимо» — не украшение: без
|
||
первого падают git-операции батча и расчёт базы диффа, без второго проход может
|
||
тронуть рабочие данные, без третьего вся шкала ранжирования триажа держится на
|
||
догадке.
|
||
|
||
## `openspec/config.yaml`
|
||
|
||
Каталог `openspec/` заводится командой — `openspec init --tools claude`, — и она
|
||
кладёт `config.yaml` с закомментированным примером внутри. Пример **заменяется
|
||
целиком**: нетронутый файл выглядит настроенным, а работает как пустой.
|
||
|
||
**Это маршрутизатор, а не второй дом фактов.** Сюда пишут ровно то, что нужно
|
||
**в момент порождения артефакта** и чего в этот момент ещё никто не открыл:
|
||
язык, правила именования capability, придирки валидатора и **адреса** документов
|
||
канона. Пересказ паспорта, инвариантов, конвенций и правил ревью сюда не
|
||
переносится: расходится он молча, а замечают это в уже написанном предложении.
|
||
|
||
```yaml
|
||
schema: spec-driven
|
||
|
||
context: |
|
||
Language: Russian
|
||
Пиши на русском, но:
|
||
- Структурные заголовки оставляй на английском:
|
||
## ADDED/MODIFIED/REMOVED Requirements, ### Requirement:, #### Scenario:
|
||
- Ключевые слова GIVEN/WHEN/THEN и RFC 2119 (SHALL/MUST/SHOULD) — на английском
|
||
- Технические термины, пути и код — на английском
|
||
|
||
Имена capabilities:
|
||
- Capability — это ПОВЕДЕНИЕ или домен системы, а не пакет кода (совпадение с
|
||
именем пакета допустимо, но не критерий).
|
||
- Существительное, понятное без знания кода: ingest, parsing, storage,
|
||
read-api. НЕ store/httpapi — это реализация.
|
||
- Гранулярность по принципу «требования меняются вместе». Дробить, когда в
|
||
одной спеке смешиваются разные заботы. Переименовать дёшево (RENAMED
|
||
Requirements) — не дроби преждевременно в маленьком проекте.
|
||
|
||
RFC 2119 — требование валидатора, не стиль:
|
||
- Каждое ### Requirement ОБЯЗАНО содержать литерал SHALL или MUST, иначе
|
||
`openspec validate` падает. Поэтому эти слова и WHEN/THEN не русифицируем.
|
||
|
||
Что это за проект — читай перед предложением, а не отсюда:
|
||
- docs/passport.md — цель, её граница (чем проект НЕ является), потребители,
|
||
типовые сценарии, референсы;
|
||
- CLAUDE.md — инварианты с severity и семантика гейта;
|
||
- docs/architecture.md — устройство; docs/security.md — периметр;
|
||
docs/adr/ — почему решено так; docs/research/ — что уже измерено.
|
||
Пересказа этих документов здесь нет намеренно: второй дом факта расходится с
|
||
первым молча, и заметно это становится в предложении, которое уже написано.
|
||
|
||
Ревью: правило выбора метки и состав проходов здесь не пересказываем — их дом
|
||
скилл av-dev-pipeline:review-pipeline, проектная настройка — docs/review.md.
|
||
|
||
Конвенции кода: механизированное проверяет гейт, прозой остаётся
|
||
docs/conventions/. Ни состав шагов гейта, ни перечень конвенций здесь не
|
||
пересказываем: и то и другое растёт по ходу задач.
|
||
|
||
Развилка или блокер — сперва prior art. Готовые решения смотрим в референсах
|
||
паспорта, отвергаем — с названной причиной, и причина идёт в design.md этого
|
||
же изменения.
|
||
|
||
rules:
|
||
proposal:
|
||
- Capabilities называй по поведению или домену системы, не по пакету кода
|
||
specs:
|
||
# Кавычки обязательны: без них YAML обрежет строку на первом '#'.
|
||
- "Каждое ### Requirement обязано содержать SHALL или MUST (иначе валидация падает)"
|
||
- "Сценарий — ровно #### (четыре решётки); три или список молча теряются"
|
||
- "SHALL/MUST должно стоять в ПЕРВОМ абзаце требования: валидатор смотрит только его"
|
||
- "Заголовки и WHEN/THEN/GIVEN — на английском, остальной текст на русском"
|
||
```
|
||
|
||
**Четыре правила для `specs` сняты отказами валидатора, а не выведены из
|
||
документации** — потому и записаны дословно: без них каждое второе предложение
|
||
узнаёт их падением `openspec validate --strict`. Блок `context` проект
|
||
дополняет своим (стек, разведка, особенности домена), но **адреса паспорта и
|
||
`CLAUDE.md` обязательны** — их отсутствие `docs.py check` называет отказом.
|
||
|
||
## `docs/.pm.json`
|
||
|
||
```json
|
||
{
|
||
"canon": 7
|
||
}
|
||
```
|
||
|
||
Плюс `"migrations": "<путь>"`, если есть БД. Ключ `"tasks"` заводится **только**
|
||
когда имя файла или заголовка отличается от умолчания (`{"backlog":
|
||
"INDEX.md"}`); секций беклога в нём нет — их дом заголовки `##` индекса. Состав
|
||
ключей — [canon.md](canon.md).
|