скиллы: doc-canon стал canon, версия раскладки поднята до 2

- каталог скилла и все вызовы переименованы: префикс `doc-` называл материал,
  а скилл занят формой — раскладкой всех частей проекта и общим повышением
  версии, включая каталог задач;
- README перестроен: `canon` вынесен из семейства документов отдельным блоком
  и отдельным узлом графа, правило префиксов переформулировано, у документов
  уточнено владение — содержимым, а не раскладкой;
- заведена запись 2 журнала версий: в проекте ничего не переехало, но путь к
  `docs.py` и имя вызова живут в гейте и в `CLAUDE.md` проекта и сломаются
  молча;
- прежние адреса в записи 1 и в журнале решений оставлены как есть: журнал
  описывает состояния, которые были, и задним числом не переписывается.
This commit is contained in:
av
2026-08-13 12:53:12 +03:00
parent 3529cd8425
commit dff05ad097
34 changed files with 149 additions and 102 deletions
+356
View File
@@ -0,0 +1,356 @@
---
name: canon
description: Форма раскладки проекта под av-dev и её обновление — три операции одной машиной сравнения. check — что разошлось с текущей версией раскладки; adopt — перевод проекта из любой прежней раскладки (docs/specs, drafts, backlog, BRIEF.md, review-brief) в канон с переносом файлов и вызовом владельцев каталога задач и openspec/; upgrade — повышение проекта с версии N до текущей по журналу версий, и повышается им вся раскладка, включая каталог задач. Использовать, когда просят проверить документацию проекта, перевести проект на канон, обновить его под новую версию раскладки или когда пришли в старый проект и надо понять, что в нём не так. Имя без префикса намеренно — скилл держит форму всех артефактов проекта, а не один их вид. Содержимое документов ведёт av-dev:doc-sync, форму записей задач — av-dev:task-track, заведение проекта с нуля — av-dev:doc-init.
---
# Форма раскладки проекта
Три операции, одна машина сравнения с разными исходами:
| Операция | Когда | Исход |
| --- | --- | --- |
| `check` | начало сессии, шаг синка, гейт | что разошлось |
| `adopt` | проект в чужой раскладке | перенос в канон |
| `upgrade` | канон вырос, проект отстал | по журналу версий |
**Имя без префикса, и это не случайность.** Остальные скиллы названы по
материалу, с которым работают, — `doc-`, `task-`, `code-`; этот работает не с
материалом, а с **формой**, и она у всех частей проекта одна. `check` сверяет
раскладку документов, `adopt` заводит все части сразу и зовёт владельцев каталога
задач и `openspec/`, `upgrade` повышает **всю** раскладку одним журналом версий —
и документы, и каталог задач. Содержимое при этом не его: документы ведёт
`av-dev:doc-sync`, записи задач — `av-dev:task-track`.
**Определение канона — [references/canon.md](references/canon.md).** Здесь оно не
пересказывается: два описания одной раскладки разъедутся, и работать будет то,
которое прочитали последним. Прочитай его **до** первой правки.
- [references/skeletons.md](references/skeletons.md) — **что именно класть** в
каждый незаполненный слот. Не выдумывай заглушку своей формы: `docs.py`
узнаёт только плейсхолдер `<!-- заполнить: … -->` из шаблонов.
- [shared/language.md](../../shared/language.md) — **как это написано словами**:
информационный стиль, применённый к проектным текстам, таблицы англицизмов и
жаргона. Раскладка отвечает, где текст лежит, — этот файл отвечает, каким он
должен быть. Правила общие для документов канона, задач, решений ADR и
записок разведки, и это их **дом**. Вычитывают их два прохода по охвату:
документы — `doc-wording`, записи каталога задач — `task-wording`.
- [references/changelog.md](references/changelog.md) — журнал версий раскладки;
закрытые журналы до слияния плагинов лежат рядом.
## Три правила, из которых всё следует
1. **Сперва карта, потом файлы.** Человеку показывается, что найдено, как
разложилось и **что не разложилось**, — и только после подтверждения
переносится хоть один файл. Массовый перенос без подтверждения разгребать
дороже, чем согласовать.
2. **Ничего не терять.** Содержимое переезжает целиком; ссылки чинятся тем же
проходом, что и перенос. Старый файл удаляется **только** после того, как
всё его содержимое нашло дом, и это названо поимённо.
3. **Что не классифицировалось — назвать.** Проглоченный абзац выглядит как
«всё перенеслось». Список «не разложилось» идёт в доклад целиком, с причиной
по каждому пункту.
## Инструмент
```
ds="$CLAUDE_PLUGIN_ROOT/skills/canon/scripts/docs.py"
python3 $ds check --dir <корень> [--base <rev>] # раскладка, ссылки, версия, сверки
python3 $ds version --dir <корень> # версия раскладки: скрипта и проекта
python3 $ds bump --dir <корень> # поднять версию проекта до версии скрипта
```
**Формы `openspec/config.yaml` здесь больше нет.** Каталог принадлежит конвейеру,
и форму смотрит его скрипт — скилл `av-dev:code-openspec`, команда
`openspec.py check`. Проект работает по OpenSpec, а каталога `openspec/` нет — форму
не проверяет никто, и это надо сказать строкой доклада, а не считать, что она
верна.
**Копия.** Дом словаря — `shared/axes.md` в репозитории плагина: словарь общий
для всех скриптов, и ни один скилл им не владеет. Правится дом, а не этот файл.
<!-- копия: коды-выхода из av-dev/shared/axes.md -->
**Коды выхода — общий словарь всех скриптов `av-dev`. Ветвись на коде, а не на
тексте вывода.**
| Код | Что случилось |
| --- | --- |
| 0 | сошлось |
| 1 | дрейф: рабочая ситуация, чинится |
| 2 | ошибка употребления: аргументы или нарушенное правило |
| 3 | окружение: не тот каталог, битый конфиг, нет инструмента |
| 4 | внутренний сбой — дефект скрипта, доложить |
**Различать 1 и 3 обязательно.** «Дрейф» — рабочая ситуация, и чинится она
правкой предмета; «окружение» — нерабочая, и повтор той же командой не поможет.
Одинаковая реакция на них неверна в обоих случаях.
<!-- /копия: коды-выхода -->
Здесь это значит: «дрейф раскладки» — рабочая ситуация, «это не корень проекта» —
нерабочая.
### Граница механизируемого — объявляется вслух
Скрипт печатает её сам последним абзацем, и **эту строку из доклада выбрасывать
нельзя**. `check`, отчитавшийся «канон соблюдён» на проекте, где из шести файлов
три лишние, хуже отсутствующего.
Машина **дрейфом** считает: отсутствующий путь канона, файл вне канона, битую
ссылку, отставшую версию, capability без упоминания в обзоре, миграцию без правки
`database.md`. **Замечанием** — незаполненный плейсхолдер и слабое упоминание
capability: незаполненный канон это переходное состояние, а не отказ. Маркеры
долга просто считает числом.
Того, чего она не умеет, **ты не судишь сам** — для этого есть два агента, и
разведены они по глубине:
| Агент | Что смотрит | Читает |
| --- | --- | --- |
| `doc-consistency` | смысловой дубль, прямое противоречие между документами, поведение в `architecture.md` вместо спек, ADR без ссылки и парного статуса, число без провенанса, заглушка вместо честной строки | `docs/`, `openspec/` |
| `doc-code-drift` | протухший факт: имя ветки, команды, пути, зависимости поимённо, настройки с числом, единые точки проекта, capability | весь репозиторий |
Судит **не тот, кто писал**: самопроверка документа слабее всего ровно там, где
формулировка казалась удачной при написании. Ни один из них ничего не правит —
оба возвращают готовые формулировки, подставляешь ты.
## Чего может не быть
`adopt` зовёт двоих: `av-dev:code-openspec` (шаг 4, пункт 3) и
`av-dev:task-track` (шаг 4, пункт 5). Каталоги `openspec/` и `tasks/` каноном не
ведутся, и трогать их этому скиллу нечем, кроме вызова.
**Копия.** Дом правила — `shared/absence.md` в репозитории плагина.
Правится дом, а не этот файл.
<!-- копия: отсутствие из av-dev/shared/absence.md -->
**Скилл не вправе считать раскладку проекта полной.** Части заводятся порознь и
живут порознь; каждая узнаётся своим следом:
| Чего нет | Как видно | Чего теперь не делает никто |
| --- | --- | --- |
| настройки av-dev | нет `.av-dev.toml` в корне | проект под процесс не заводился; версии нет, настроек нет |
| документы канона | нет `docs/` | проектную конкретику брать неоткуда — темы, инварианты, прецеденты |
| учёт работ | нет каталога задач | запись остаётся владельцу: назови её текстом в докладе |
| источник требований | нет `openspec/config.yaml` | цикл SDD не запускается: спеки не с чем сверять |
**Свой скилл зовётся полным именем**`av-dev:canon`, `av-dev:task-track`,
`av-dev:code-review`. Короткое имя может разрешиться в устаревшую проектную
копию из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в
поведении.
**Внешний плагин может не стоять.** Их два: `opsx:*` — цикл SDD, и
`av-dev-git:commit` — сообщения коммитов. Путь в дерево чужого плагина не
пишется никогда: `$CLAUDE_PLUGIN_ROOT` ведёт только в своё дерево, а
вычисленный от него путь к соседу либо не откроется, либо откроет чужую
установку. Нужен чужой справочник — зови владеющий им скилл, он прочитает его
сам.
**Отсутствие — исход, а не поломка.** Назови строкой доклада, чего теперь не
делает никто, и продолжай работу. Молчать нельзя: пропуск неотличим от
сделанного. Выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся следом в проекте, а не объявлением.** Перечня того, что
здесь заведено, проект не ведёт — он разошёлся бы с действительностью молча.
<!-- /копия: отсутствие -->
Чем оборачивается отсутствие каждого — на самих пунктах шага 4. `adopt` из-за
этого не останавливается ни в одном из двух случаев.
## `check`
1. `docs.py check`, при наличии базы диффа — с `--base`.
2. **Судей документов на каждом `check` не зови.** Ими владеет отдельный скилл —
`av-dev:doc-healthcheck`, — и там же записано, когда его звать: он дорог, и
прогон по каждому `check` не окупается. `check` отвечает на «сходится ли
форма», `doc-healthcheck` — на «не разошлись ли утверждения».
3. Доклад: вывод скрипта строкой исхода и **граница покрытия** — что смотрели и
чего не смотрели. Если суждение здесь нужно, скажи это строкой и предложи
`doc-healthcheck`, а не зови агентов сам.
Дрейф раскладки чинится переносом; смысловые находки — это либо правка
документа, либо задача, если работы больше чем на абзац.
## `adopt` — проект в чужой раскладке
### 1. Осмотрись
`docs.py check` — он уже назовёт упразднённые слоты с адресом, куда каждый
уезжает. **Но смотрит он только верхний уровень `docs/`:** упразднённое в корне
репозитория (`BRIEF.md`) и во вложенных каталогах он не назовёт никогда, поэтому
корневые `*.md` читай глазами. Плюс: `CLAUDE.md`, `openspec/specs/` (список
capability), `openspec/config.yaml`.
### 2. Составь карту
Каждый найденный файл получает строку: **куда едет, целиком или разбирается, что
делать с оригиналом**. Разбор `docs/specs/` — самое дорогое место, и он делается
поимённо по capability:
| Что в файле | Куда |
| --- | --- |
| требования, сценарии, поведение | `openspec/specs/<capability>/spec.md`**или уже там**, тогда файл дубль |
| компоненты, транспорты, раскладка, деплой | `docs/architecture.md` |
| конвенции чужой системы, формат чужих данных | `docs/research/` |
| обоснование принятого решения | `docs/adr/` |
**Дубль удаляется только после поимённой сверки**: открыть спеку capability,
открыть файл, убедиться, что в файле нет ничего сверх спеки. Нашлось сверх —
сперва переезжает в спеку дельтой, потом файл удаляется.
### 3. Покажи карту человеку
`AskUserQuestion`, **не больше трёх вопросов за итерацию**, рекомендация первым
вариантом. Показывается: сколько файлов, куда каждый, спорные отнесения, список
«не разложилось». Механику (порядок строк, имена файлов внутри `research/`) не
выноси — это не развилка.
### 4. Перенеси
Порядок важен — он минимизирует окно, в котором ссылки битые:
1. `.av-dev.toml` в корне: `version = <текущая версия>` и путь миграций в
`[docs]`, если БД есть;
2. каталоги канона и скелет **по [references/skeletons.md](references/skeletons.md)**:
незаполненное — одной честной информативной строкой, а не «TBD»;
3. **OpenSpec, если его нет или `config.yaml` остался примером** — **вызови
Skill `av-dev:code-openspec`**. Каталог принадлежит конвейеру, и команда
заведения с формой файла живут там. Пересказ инвариантов, конвенций и правил
ревью из `context` вычисти ссылкой на дом — на переводимом проекте он там
почти наверняка есть. Проект решил жить без OpenSpec — `docs.py` о каталоге
тогда тоже молчит, и форму `config.yaml` не проверяет никто; скажи это
строкой;
4. переносы содержимого;
5. каталог задач — **вызови скилл `av-dev:task-track`**, сценарий адаптации: он
владеет форматом задач. Он же переименует транслитные слаги в английские и
тем же проходом починит перекрёстные ссылки;
6. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`,
`CLAUDE.md`, `README.md`;
7. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**;
8. **шаг `docs.py check` в гейт проекта.** Путь к скрипту — переменной с
умолчанием на канонический путь маркетплейса, чтобы переустановка плагина не
меняла `Taskfile`; шаг обязан **краснеть внятно**, если скрипт не найден, а не
пропускаться. Передай ему базу диффа (`--base`) той же переменной, что и
остальным шагам гейта: без неё сверка миграций со схемой не гоняется вовсе.
Пример строки покажи человеку — гейт принадлежит проекту, и правит его он.
**Шагов в гейте три, и они независимы.** `docs.py check` не тянет за собой
ни задачи, ни конвейер: без своих строк дрейф каталога задач и формы
`openspec/config.yaml` перестаёт ловиться совсем. Ставь соседние шаги по
следу присутствия — каталог задач с индексом на месте, значит ставится
`tasks.py check --dir <каталог задач>`; `openspec/config.yaml` есть, значит
ставится `openspec.py check`. Следа нет — этой части в проекте нет, шаг не
ставится, и это **строка доклада**, а не поломка: назови, чего теперь не
проверяет никто. У каждого шага своя переменная пути с тем же умолчанием на
канонический путь маркетплейса; `$CLAUDE_PLUGIN_ROOT` в гейт не подставляй —
он ведёт только в свой плагин;
9. `docs.py check` — до **отсутствия дрейфа раскладки**. Замечания
(незаполненные плейсхолдеры, слабое упоминание capability) остаются:
незаполненный канон это объявленное переходное состояние из шага 5, а не
отказ. Пересчитай эти пункты в докладе переходного состояния — не выдавай
их за поломку и не молчи о них.
**Задачи `docs.py` не проверяет** — их ведёт другой скилл, и согласованность
каталога показывает только `tasks.py check`. Позвал на шаге 5 скилл задач —
его отчёт идёт в доклад отдельной строкой, и пункт «задачи без цели» в нём
зелёным не станет: цели не сочиняются адаптацией (запрет записан у того, кто
ведёт задачи), их проставляет человек порциями переоценки на первом груминге —
скилл `av-dev:task-groom`.
### 5. Объяви переходное состояние
Сразу после переноса канон **заполнен не весь**, и это нормально, но обязано
быть названо, иначе следующий агент примет скелет за поломку.
Печатается по факту: сколько документов стоят честной строкой вместо
содержания, сколько маркеров долга в `architecture.md`, сколько задач без
критериев приёмки. Закрывается порциями по ходу работы, а не одним заходом.
### 6. Позови обоих судей
`docs.py` увидел раскладку, а не смысл: перенос растащил один факт по двум домам,
оставил в `architecture.md` поведение, которому место в спеке, и оторвал ADR от
его `design.md`. Ничего из этого скрипт не видит, и первый прогон на живом
проекте обычно самый урожайный — правило единственного дома до адаптации никто не
проверял.
Вызови Skill **`av-dev:doc-healthcheck`** — он зовёт обоих судей на весь канон
разом и держит разбор урожая порциями.
**Передай им объявленное переходное состояние из шага 5** — иначе честная строка
в незаполненном слоте вернётся находкой, а это не поломка, а объявленный долг.
### 7. Вычитай написанное — агент `doc-wording`
Судьи смотрят утверждения, а `adopt` только что **писал текст**: честные строки
в пустые слоты, переписанные при переносе абзацы, шапки перенесённых документов.
Язык этого текста не проверяет никто другой, а зовущий здесь по определению тот,
кто его и написал.
Позови агента **по названной пачке** — документы, которые ты завёл или правил,
плюс перенесённые целиком. Весь канон ему не нужен: он работает по списку, и
список же служит ему словарём терминов. Находки — готовые формулировки,
подставляешь их ты.
## `upgrade` — канон вырос
1. `docs.py version` — версия проекта и версия скрипта.
2. Проект новее скрипта — **обнови маркетплейс**, а не проект: это отстал
плагин.
3. Иначе иди по [changelog.md](references/changelog.md) снизу вверх от версии
проекта до текущей и делай названное в каждой записи. Записи независимы и
применяются по порядку.
4. Подними версию — `docs.py bump`. Он правит **строку**, а не переписывает
файл: комментарии в нём принадлежат проекту. Последним шагом, потому что
число объявляет пройденными записи журнала.
5. `docs.py check`.
6. **Позови судей** — Skill `av-dev:doc-healthcheck`.
7. **Позови вычитку** — агент `doc-wording`, но **только по тем документам,
которых записи журнала коснулись**, и только если правка была текстовой, а не
переименованием файла. Записи журнала пишутся руками в проектной прозе, и
дописанный по журналу раздел — такой же свежий текст, как на синке.
Записи журнала описывают **что сделать проекту**. Если запись этого не говорит —
это дефект журнала, и о нём надо сказать, а не догадываться.
**Каталог задач повышается этим же журналом.** Версия одна на всю раскладку —
`version` в `.av-dev.toml`, — и записи журнала говорят про обе половины: и про
документы, и про каталог задач. Порознь версии жили, пока плагинов было три и
проект мог взять одну половину без другой; с одним плагином два числа означали
бы только вопрос, по какому журналу повышать. Что каталог задач отстал, скажет
`tasks.py check` своей строкой гейта — той же версией, что и `docs.py`.
**Шаг 6 обязателен, и вот почему.** `check` сверяет **число** в `.av-dev.toml`
с версией скрипта — и только его. Применена ли запись журнала **по существу**,
он не знает: проект несёт `version` текущей версии и может не иметь того, чего требовала любая
из пройденных версий. Записи применяются руками (переименовать секцию, проставить
типы, дописать раздел каждому `fix`), а ручной проход по нескольким записям
подряд — ровно то место, где половина шага делается и забывается. Судьи и есть
проверка, которой у `upgrade` иначе нет: `doc-consistency` увидит, что документы
разошлись после переименований, `doc-code-drift` — что переехавший факт
разошёлся с кодом.
## Чего этот скилл не делает
- **Не сочиняет содержание.** Пустой слот получает честную строку о том, что его
наполнить пока нечем, а не выдуманный абзац. Придуманный периметр модели угроз
хуже отсутствующего: по нему будут строиться находки.
- **Не удаляет то, чьё содержимое не нашло дом.** Оригинал живёт, пока не
названо поимённо, куда переехал каждый его кусок.
- **Не ведёт содержимое канона** — это скилл `doc-sync`. Здесь только раскладка.
- **Не заводит проект с нуля** — это скилл `doc-init`.
- **Не правит историю.** В старых коммитах старые пути остаются, и это нормально.
## Доклад
- Что нашёл `docs.py`: код выхода и число пунктов дрейфа.
- Что перенесено: файл → дом, числом и поимённо для спорного.
- **Удалённые дубли** — с указанием, против какой спеки сверялся каждый.
- **Не разложилось** — поимённо, с причиной.
- Переходное состояние числами: честных строк, маркеров долга, задач без
критериев.
- **Граница покрытия**: что проверила машина, что судил ты, чего не смотрел
никто.
+582
View File
@@ -0,0 +1,582 @@
# Канон документов проекта
**Номер версии здесь не стоит намеренно.** Этот файл описывает канон таким, какой
он сейчас, а число живёт в двух домах, которые не расходятся: константа в
`docs.py` (её печатает `docs.py version`) и верхняя запись
[журнала](changelog.md). Литерал в шапке был третьим и отстал на первом же
повышении — версию 13 он пережил, объявляя канон двенадцатым.
Это **единственный дом определения канона**. Скиллы `doc-init`, `canon` и `doc-sync`
читают его, а не пересказывают: три описания одной раскладки разъедутся, и
работать будет то, которое прочитали последним. Меняется канон — меняется этот
файл и появляется запись в [changelog.md](changelog.md).
## Зачем канон жёсткий
Пути фиксированы, и проект под них подгоняется, а не наоборот. Причина не
техническая: проектов много, все малого и среднего размера, и ориентироваться в
слегка похожих, но разных раскладках дороже, чем один раз привести их к общей.
Рядом лежит OpenSpec, у которого структура тоже строгая.
Цена принята сознательно: плагин не переносится на чужой репозиторий как есть —
чужой репозиторий **приводится** к канону скиллом `canon`.
Раскладка отвечает, **где** текст лежит и на какой вопрос отвечает. Каким он
должен быть **словами** — общий для всех документов канона файл
[shared/language.md](../../../shared/language.md): информационный стиль, англицизмы, жаргон. Он
относится и к задачам, и к решениям ADR, и к запискам разведки.
## Сопровождение и эксплуатация — целое и часть
Словарь этой темы — [shared/operations.md](../../../shared/operations.md):
целое и часть, три места одной темы (секция роадмапа, раздел архитектуры, тема
ревью `operations`) и граница с возможностями проекта. Здесь он не
пересказывается: копия жила рядом с домом в одном дереве и была ровно тем
вторым домом, против которого правило и написано.
## Раскладка
**Документ канона живёт файлом или каталогом.** `docs/security.md` и
`docs/security/` — одно и то же; форму выбирает проект по объёму написанного, и
переход между формами не меняет ни канон, ни версию. Обе формы сразу — ошибка:
два дома для одного факта расходятся молча.
```
CLAUDE.md памятка агенту: что это, стек, инварианты с
severity, команды, семантика гейта, запреты
AGENTS.md необязателен, лежит рядом; читается теми же
.av-dev.toml версия раскладки и настройки проверок; лежит
в корне, потому что нужен и без docs/
docs/
passport.md | passport/ зачем и для кого; чем НЕ является; сценарии
architecture.md | architecture/ как сложено — обзор; окружение и эксплуатация
database.md | database/ схема хранилища; представление данных и настройки
security.md | security/ периметр; недоверенный вход; что вне модели
conventions.md | conventions/ как пишем код; что механизировано
research.md | research/ наблюдения и числа с провенансом
adr.md | adr/ почему решено так; статусы, правило замены
review.md | review/ настройка конвейера под проект + журнал дефектов
<своя тема>.md | <своя тема>/ всё, что проект счёл нужным проверять
tasks/ каталог задач — скилл task-track, не канон;
лежит в корне, вне docs/, и канон его не требует
openspec/
config.yaml только нужды генерации артефактов + ссылки
specs/<capability>/spec.md что система делает — нормативно
changes/archive/ архив изменений с design.md — сырьё для ADR
```
**У документа-каталога обязателен `README.md`** — вход, по которому его читают агенты.
`adr/` в форме каталога держит ещё и `template.md`, а записи именуются
`ADR-ГГГГ-ММ-ДД-slug.md`.
## Три категории документов
Категория документа — ось процесса; перечень осей и того, чего каждая **не**
решает, — [shared/axes.md](../../../shared/axes.md).
Раньше здесь стояло плоское правило «каждый документ `docs/` — тема ревью». Оно
неверно ровно наполовину: паспорт и схема хранилища ревью нужны, но темами не
являются, а журнал решений и журнал наблюдений ревью изменения не нужны вовсе.
Плоское правило заставляло разметчика либо плодить фантомные темы, либо терять
документы молча — а молчащая потеря и есть то, против чего канон написан.
**Разрез один и проверяемый: можно ли по документу сказать «в этом изменении
сделано не так»?**
| Категория | Ответ на разрез | Что с ней делает ревью |
| --- | --- | --- |
| **тема** | да, прямо | заводит направление проверки и требует исполнителя |
| **источник темы** | нет, но он задаёт границу, по которой судит чужая тема | читается как материал, своей темы не порождает |
| **процессный документ** | нет: он про то, как мы работаем, а не про изменение | не судит по нему изменение |
| Документ | Категория | Куда питает |
| --- | --- | --- |
| `conventions.*` | тема | `conventions` |
| `security.*` | тема | `security` |
| `architecture.*` | тема | `architecture`; раздел эксплуатации — `operations` |
| *свой документ проекта* | тема | своя тема, именем документа |
| `passport.*` | источник | `architecture` — граница домена, «чем **не** является» |
| `database.*` | источник | `operations` — схема и настройки с числами |
| `CLAUDE.md`, `AGENTS.md` | источник | `autotests` (семантика гейта); инварианты — сквозные |
| `openspec/specs/` | источник | `requirements` |
| `openspec/config.yaml` | процессный | — (настройка порождения артефактов, слой **до** тем; заводит конвейер) |
| `tasks/` | процессный | — (чужое владение: скилл `task-track`) |
| `review.*` | процессный | — (настройка самого конвейера, слой **над** темами) |
| `adr.*` | процессный | — |
| `research.*` | процессный | — |
| `.av-dev.toml` | процессный | — (служебный файл, не документ) |
**Список тем открытый, и это не послабление, а механизм.** Категории
`источник` и `процессный` **закрыты** — они перечислены здесь поимённо и
проектом не пополняются. Всё остальное, что проект кладёт в `docs/`, — тема: у
конвейера есть приёмник для темы, к которой нет именной оптики, и заведён он
ровно за этим. Завёл `docs/accessibility.md` — появилась тема `accessibility`, и
она попадает в план каждого прогона.
Отсюда следствие, ради которого правило и заведено: **`docs/` — это конфигурация
ревью.** Проект настраивает проверку тем, что пишет о себе, а не отдельным файлом
настроек, который разошёлся бы с документами.
**«Не судит по нему» и «не открывает» — не одно и то же, и разница существенна.**
`docs/review.*` проходы читают на каждом прогоне: там лежат вопросы по темам,
журнал дефектов, типовые узлы и типовые ложноположительные. Это чтение конвейером
**собственной настройки**, а не суждение об изменении, и потому оно законно.
`adr/`, `research/` и `tasks/` не открывает никто: по ним изменение не судят, и
настройкой конвейера они не являются.
**Процессный документ — не документ второго сорта.** `adr/` и `research/`
проверяются наравне с остальными, но **сверкой документации**, а не прогоном
ревью: ADR без ссылки на источник, замена без парного статуса, число
без провенанса — это работа агентов `doc-consistency` и `doc-code-drift`, и она
осталась там же, где была. Изменилось одно: прогон ревью не открывает их как
критерий и не судит по ним изменение.
Цена этого решения записана, а не подразумевается: **расхождение изменения с
записанным решением прогоном больше не ловится.** Раньше архитектурный проход
читал `adr/` и мог сказать «здесь отменено решение ADR-2026-03-11, а парного
статуса нет»; теперь это скажет только `doc-consistency`, а зовёт его скилл
`healthcheck`. Сделка сознательная — ADR объясняет прошлое, а не предъявляет
требование к изменению, и чтение всего каталога решений на каждой задаче
оплачивалось на каждой, а срабатывало на единицах.
### Имена файлов английские, текст русский
**Текст документов русский; имена файлов, capability и задач — английские,
kebab-case.** Причина не эстетическая: имя файла стоит в ссылках из других
документов, в коммитах и в путях, которые набирают руками, — а кириллица в пути
ломается по-разному в разных местах и не набирается на английской раскладке.
**Транслита не заводим.** Слаг именуется английским словом **по сути**, а не
записью русского латиницей: `queue-as-table`, а не `ochered-tablicej`. Транслит
нечитаем тому, кто ищет по смыслу, и не сокращается.
У ADR имя вдобавок несёт форму — `ADR-ГГГГ-ММ-ДД-slug.md`: по ней записи
сортируются, и по ней же ищется дата решения.
`docs.py check` проверяет кириллицу и kebab-case **жёстко**, форму имени ADR —
тоже, а транслит **эвристикой**, то есть замечанием: английское слово от
транслита машина не отличает. Слаги каталога задач ведёт `tasks.py` — там та же
проверка и тот же разрез.
**Переименование — не правка, а перенос ссылок**: делается одним проходом по
всем местам, где имя упомянуто, иначе останутся битые ссылки. Для задач это
умеет `tasks.py adopt`; для документов канона правит человек, а `docs.py` потом
показывает, что ссылки целы.
## Роли документов и темы ревью
Одна строка на каждый — на какой вопрос он отвечает, в какой он категории и
какую тему питает. **Кто именно закрывает тему, здесь не указано намеренно**: это
зависит от метки прогона и меняется вместе с конвейером, а документ живёт
дольше. Раскладку «тема → проход → глубина» держит скилл
`av-dev:code-review`.
**Общего словаря у канона с конвейером ровно три вида имён: имена категорий,
имена тем и имена меток.** Категорий три — `тема`, `источник`, `процессный`;
**меток тоже три, и они закрыты: `small`, `medium`, `large`.** Метка это итог
классификации задачи, и по ней конвейер выбирает исполнителей на обеих стадиях
ревью; проект её не выдумывает, а только уточняет триггеры. Ими проект и
настраивает ревью — вопросами по темам и триггерами метки. **Имён проходов канон не называет нигде**, включая вывод `docs.py`:
проход переименовывается и переезжает между метками, и канон, назвавший его, в
этот день соврёт молча. Обратное направление законно — конвейер называет
документы канона поимённо, потому что он их читатель.
| Документ | Вопрос | Категория и тема |
| --- | --- | --- |
| `CLAUDE.md`, `AGENTS.md` | что нельзя нарушать, чем краснеет гейт | источник: `autotests`; инварианты — сквозные, во все темы |
| `passport.*` | зачем и для кого, чем это **не** является | источник: `architecture` |
| `architecture.*` | как сложено и где что работает | тема `architecture`; раздел эксплуатации — `operations` |
| `database.*` | что лежит в хранилище и какими настройками | источник: `operations` |
| `security.*` | против кого защищаемся и что вне модели | тема `security` |
| `conventions.*` | как мы пишем код | тема `conventions` |
| `openspec/specs/` | что система делает — нормативно | источник: `requirements` |
| `research.*` | что показала реальность, а не документация | процессный |
| `adr.*` | почему решено именно так | процессный |
| `review.*` | как настроен конвейер и что уже проскакивало | процессный: слой **над** темами |
| `tasks/` | что делаем и в каком порядке | процессный |
| *свой документ проекта* | что проект счёл нужным проверять | **своя тема**, именем документа |
### `passport.md`
Цель; закрытый список потребителей и что каждому нужно; **чем целью не
является** — это граница домена, по которой архитектурный проход судит о
переносе понятия; типовые сценарии; мера, по которой проект считается удавшимся;
референсы, у кого подсматривать.
### `architecture.md` — **обзор, не поведение**
Принципы; компоненты **со ссылками на capability**, а не с пересказом их
требований; **единые точки проекта** — где генерируются идентификаторы и время,
где единственный парсер входного формата, где маппинг доменной ошибки в код
ответа, где общий путь приёма (это материал для вопроса «не появился ли второй
способ»); внешние границы и форматы чужих систем; окружение — где работает, что
рядом, кто перезапускает; **внешние зависимости поимённо** и чем каждая
отказывает (не только «падает», но и «отвечает медленно», «молчит», «отдаёт
мусор»); кто заметит отказ и когда; характер потока — непрерывный, по запросу,
по расписанию; деплой; открытые вопросы.
**Обратимости здесь нет** — её единственный дом `CLAUDE.md`: туда ходят пять
проходов, и раздвоение адреса означало бы, что проект написал ответ, а ревью его
не прочитало.
**Поведение системы сюда не пишется.** Его нормативный дом — `openspec/specs/`,
куда `opsx:archive` вливает дельты; второй дом синхронизировать руками
невозможно, и он разойдётся.
Раздел, ещё не разнесённый при переезде, помечается маркером долга:
```
<!-- канон: поведение → openspec/specs/<capability> -->
```
`docs.py` считает маркеры и печатает остаток числом. Гейт от них **не краснеет**:
это долг, а не отказ, иначе постепенный переезд стал бы невозможен.
### `database.md`
Схема: таблицы, ключи, связи, правило времени и идентификаторов. Плюс то, чего
нет в схеме, но без чего замер не превращается в находку: **чем физически лежит
запись** (сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи
(распаковка целиком, read-modify-write), и **настройки с числовым значением**
таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен.
Конвенции идентификаторов и именования — не схема, они в `conventions/`.
### `security.md`
**Периметр первой строкой.** «Сервис открыт наружу» и «контур доверенный,
публичного интернета здесь нет, не выдумывай его» — противоположные постановки
под одним заголовком, и разбор темы `security` между ними сам не выберет. Контур ещё
не развёрнут — назови **оба** периметра, целевой и сегодняшний, и скажи прямо,
против какого строятся находки.
Дальше: что недоверенное и каким каналом приходит; **из чего строятся пути и
ключи** (раскладка файлов, состав координатного ключа, имя каталога) — отсюда
строится выход за пределы песочницы; что разграничивает доступ; что
чувствительнее чего; **что вне модели** — перечислить явно.
### `conventions/`
Прозой остаётся **только то, что не выражается правилом**. `README.md` держит
индекс, правило промоута и **перечень уже механизированного** со ссылкой на
место механизации — конфиг линтера, собственный анализатор, тест-сканер
исходников. Не названное место механизации означает, что проход добросовестно
проверит уже проверенное.
### `research/`
Наблюдения за внешним миром: что реально шлёт источник, чем документация формата
расходится с практикой, какие числа сняты с живого потока. **Числа — с
провенансом**, то есть с командой или условиями, которыми получены.
`README.md` — как снималось и индекс тем.
Число без источника проход обязан читать как условие, а не как замер. Число, чей
источник по ссылке не подтвердился, не выбрасывается и не переписывается по
догадке — остаётся с пометкой «расходится с источником: там <что нашли>».
### `adr/`
**ADR продвигает уже написанное решение, а не сочиняет его заново.** Запись
цитирует решение и ссылается на источник. Источников два, и оба законны:
- **архивный `design.md`** — решение принято по ходу изменения:
`openspec/changes/archive/<id>/design.md`. Обычный случай;
- **записка разведки** — решение принято разведкой, и change по нему не будет
никогда: намеренный отказ, выбор подхода, «проверили и не делаем». У такой
работы нет `design.md` по построению, и без второго источника её решение либо
не попадало в `adr/` вовсе, либо попадало сочинённым заново.
Источник называется в записи всегда — по нему видно, чем решение подтверждено.
Заводится, когда верно одно из трёх:
<!-- дом: adr-когда-заводить -->
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
- **намеренный отказ** от очевидного подхода;
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
«заменено на».
<!-- /дом: adr-когда-заводить -->
Не заводится для рутины и для того, что видно из кода и `git log`.
Записи неизменяемы: передумали — заводится новая, старая получает статус.
Активная запись статуса не имеет.
**Статус живёт полем меты записи**, там же, где дата и источник:
`- **Статус:** заменено на ADR-…` либо `- **Статус:** устарело`. Места ему в
шаблоне не отводилось, и каждая запись изобретала своё — то абзацем, то
заголовком; в таблице `adr/README.md` статус при этом обязан быть, а брать его
оттуда, где он у каждого свой, нельзя.
### `review.md`
Два раздела с разными сроками жизни.
**Настройка конвейера под проект**, пять подразделов с точными именами — по ним
проходы находят свой кусок:
- **Типовые узлы** — рода узлов проекта и 3–5 проверяемых свойств к каждому;
- **Типовые ложноположительные** — находки, которые здесь выглядят убедительно и
всегда неверны, каждая со строкой «почему здесь это не дефект»;
- **Вопросы по темам** — в форме `<тема>: <вопрос> (<провенанс>)`. **Не по именам
проходов**: проход уезжает между метками, а тема остаётся, и вопрос,
адресованный проходу, перестал бы задаваться молча в тот день, когда тот уехал
в старшую метку. Задаёт вопрос тот, кто закрывает тему на этом прогоне.
Адресовать можно только теме: `passport`, `database`, `adr`, `research` и
`review` — не темы, и вопрос, адресованный им, не задаст никто;
- **Триггеры метки** — проектная конкретизация правила выбора метки ревью,
**тремя списками**. Два поднимают, по одному на ось: что в этом проекте считается
**крупным** (объём: сколько узлов и слоёв трогает) и что считается
**незнакомым** (форма решения: известна до начала или нащупывается по ходу).
Любая из двух осей поднимает прогон до `large`, старшей метки, — а она
рассчитана на 5–10% задач. Третий список — что считается **мелким** (опускает
до `small`); он один, потому что вниз метку опускает только совпадение обеих
осей сразу. Перечнем мест, узлами или capability, а не вторым определением
класса. Уточняет умолчания, а не отменяет их. Рабочее умолчание — `medium`:
миграция схемы и публичный контракт метку **не** поднимают, их проверяют
проходы, которые в `medium` и так есть;
- **Недоступно проверке** — два подраздела, оба **по темам**: «не проверит ни
один проход» (принципиальная граница, по факту промаха не пересматривается) и
«перестали проверять сознательно» (пересматривается первым). Тема, у которой в
проекте нет дома, сюда не пишется: её и так называет план каждого прогона.
**Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой
**проскочил / пойман ревью**. Проскочившие — проверочный набор для калибровки конвейера,
выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные,
воспроизводимые, однажды оказавшиеся правдой.
### `tasks/`
**Каталог задач канону не принадлежит.** Его ведёт скилл `task-track` — своим
скриптом и своими проверками; где каталог лежит и как названы его части, говорит
секция `[tasks]` в `.av-dev.toml`. Версия там одна на всю раскладку: канон и
каталог задач двигаются вместе, потому что ведёт их один плагин.
Канон **резервирует место** в `docs/` и внутрь не смотрит:
`docs.py` каталог не открывает, его отсутствия не считает дрейфом и согласованность
задач не проверяет. Проект, не заведший каталог задач, их не ведёт
вовсе, и отказом это быть не может.
Раскладку, форму записи и команды держит скилл `av-dev:task-track`. Ниже — то,
от чего зависит, читается ли проект как продукт: канон высказывается об этом
потому, что роадмап отвечает на вопрос о **системе**, а не о работах.
**`ROADMAP.md` отвечает на «что приложение уже умеет и чего ещё не умеет».** Это
не очередь работ: цель — **возможность приложения**, задача — шаг к ней.
Достигнутая цель из роадмапа **не исчезает** — строка с датой переезжает в
секцию достигнутого, потому что «что умеет» и есть половина вопроса, ради
которого документ открывают. Вторым домом поведения роадмап при этом не
становится: нормативное поведение живёт в `openspec/specs/`, роадмап отвечает,
**когда и в каком порядке** оно появилось.
**У каждой записи есть тип, и тип решает, что с ней можно делать.** Дом типа —
поле меты `Тип` первой строкой; эмодзи в заголовке от него производна. Словарь
закрыт:
| Тип | Что это |
| --- | --- |
| 🎯 `goal` | возможность приложения |
| ✨ `feature` | снаружи появляется то, чего не было |
| 🐞 `fix` | поведение расходится с заявленным |
| 🧹 `chore` | обслуживание, поведение не меняется |
| 🔬 `research` | исход — знание, а не изменение |
**Схемы записи здесь нет намеренно.** Какие разделы тип требует, нужна ли ему
цель и берётся ли он в работу — скилл `av-dev:task-track`, раздел «Тип
записи», подробно — по файлу на тип в его `references/task-<тип>.md`. Канон
фиксирует **словарь**, потому что
от него зависит, читается ли проект как продукт; схема — механика ведения задач,
и второй её экземпляр разошёлся бы с первым (он и разошёлся: канон успел
объявить цель у `fix` запрещённой, хотя она там необязательна).
Схема требуется **к взятию в работу**, а не к заведению: беклог пополняется чаще,
чем разбирается, и требование на входе выгоняло бы в заметки то, что должно
лежать задачей. Запись, не собравшая разделы своего типа, — законное состояние
беклога; невзятой её делает `tasks.py ready`.
Отдельного типа для незаполненной записи нет: «ещё не описано» — состояние, а не
род работы, и называется оно **`research` без раздела «Вопрос»**. Такая запись в
работу не берётся и лежит в конце своей категории.
Раскладку, форму записи и алгоритм работы над каждым типом держит скилл
`av-dev:task-track`.
### `CLAUDE.md`
Что это и стек; **инварианты с severity рядом с формулировкой** — по ним проходы
присваивают `critical`, поэтому severity стоит здесь, а не выводится каждым
проходом заново; команды; **семантика гейта** — чем краснеет безусловно и почему,
где логи, что означает исход, чего в гейте намеренно нет, **кто и когда обязан
гонять дорогое вне гейта**.
Плюс то, что нужно git-операциям и проходам и не выводится ниоткуда:
- **имя основной ветки** — от неё считается база диффа
(`git merge-base HEAD <ветка>`), в неё коммитит работу конвейер.
Угадывание между `master` и `main` ломает интеграцию целиком;
- **что запускать запрещено, с путями** — рабочая БД, боевой каталог данных,
внешние сервисы. Запретом с путями, а не «будь осторожен»;
- **где `testdata`** и что в них лежит; **куда писать временное**;
- **что считается необратимым** — единственный дом: от обратимости зависит вся
шкала ранжирования триажа и право проходов на `critical`;
- **что считается сломанным** — красная проверка, обгоняющая развитие;
**ориентир по размеру порции**, если он замерялся. Оба слота читает скилл
`av-dev:task-groom`, и имена их — его; названы они здесь потому, что дом
содержимого `CLAUDE.md` один и он тут.
### `openspec/config.yaml`
**Файл канону не принадлежит, и проверяет его тоже не канон.** Каталог
`openspec/` — предпосылка конвейера: без него не работают ни `opsx:propose`, ни
ревью дизайна, ни сверка требований. Заводит его, настраивает и **проверяет
форму** скилл `av-dev:code-openspec`: там образец файла, там же скрипт
`openspec.py check`. `docs.py` о файле не говорит ничего.
Канон называет его здесь по одной причине: `openspec/specs/` — **дом темы
`requirements`**, и без этой строки карта тем неполна. На форму самого
`config.yaml` канон не высказывается.
**Одно за каноном всё же остаётся, и это не форма, а единственный дом.** Блок
`context` — самое частое место для второго дома: он читается при порождении
каждого артефакта, туда удобно дописать «чтобы агент знал», и так заводятся копии
инвариантов, конвенций, состава гейта и правил ревью. Расходятся они молча.
Разрез: **утверждение, которое можно опровергнуть, открыв другой файл проекта, —
пересказ; строка, которая говорит, какой файл открыть, — ссылка.** Машина этого
не различает; судит агент `doc-consistency`, и `config.yaml` у него во входе.
## Правило единственного дома
Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора:
<!-- дом: карта-домов -->
| Факт | Дом |
| --- | --- |
| поведение системы | `openspec/specs/<capability>/spec.md` |
| почему решено так | `adr/`, источник — архивный `design.md` либо записка разведки |
| граница домена, «чем не является» | `passport.md` |
| инвариант и его severity | `CLAUDE.md` |
| что приложение умеет и чего не умеет; порядок работ | `tasks/ROADMAP.md` |
| измеренное число | `research/` |
| настройка с числовым значением | `database.md` |
| периметр и модель угроз | `security.md` |
| что необратимо | `CLAUDE.md`**не** `architecture.md` |
| единые точки проекта | `architecture.md` |
| имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` |
| что уже механизировано правилом | `conventions.*`, раздел «Механизировано» |
<!-- /дом: карта-домов -->
## Пустое называется пустым
Скелет канона заводится **целиком** с первого дня. Незаполненный документ держит
**одну честную информативную строку**, а не заглушку:
- «внешних зависимостей нет — смотри на диск и на СУБД»;
- «наблюдений на живых данных нет: внешний источник один, формат документирован»;
- «прецедентов не накоплено»;
- «сознательно ничего не отключали»;
- «архитектуры пока нет: кода нет, заводится первой задачей».
Проход читает такую строку **как факт** и не тратит на неё обязательный вопрос.
Отсутствие файла он не может прочитать никак, а «TBD» читает как пробел —
поэтому `docs.py check` отличает честную строку от нетронутого плейсхолдера
шаблона и напоминает о втором.
## Слотов нет
Файлы и каталоги, которых в каноне **нет**, и куда уезжает их содержимое:
| Было | Куда |
| --- | --- |
| `docs/review-brief.md` | документы канона и есть бриф; остаток — в `review.md` |
| `docs/specs/` | `openspec/specs/` (поведение) и `architecture.md` (обзор) |
| `docs/drafts/` | идея → запись `research`; отказ → ADR; порядок → `ROADMAP.md`; размышление → `opsx:explore` |
| `docs/plan.md` | `tasks/ROADMAP.md` |
| `BRIEF.md` | `passport.md` |
| `docs/backlog/` | `tasks/` в корне репозитория |
| `docs/review-journal.md`, `docs/review/journal.md` | `docs/review.md` |
## Что проверяет машина, а что человек
Граница объявляется вслух в каждом отчёте: `check`, отчитавшийся «канон
соблюдён» на проекте, где из шести файлов три лишние, хуже отсутствующего.
| Проверяет `docs.py` | Судит агент | Какой |
| --- | --- | --- |
| отсутствующие пути канона | смысловой дубль документа и capability | `doc-consistency` |
| файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` | `doc-consistency` |
| имя файла не kebab-case латиницей; форма имени ADR | транслит в имени — сверх эвристики | `doc-wording` |
| битые относительные ссылки | прямое противоречие между документами | `doc-consistency` |
| версия канона и её отставание | достаточность честной строки в пустом слоте | `doc-consistency` |
| нетронутый плейсхолдер шаблона | ADR без ссылки на источник, замена без парного статуса | `doc-consistency` |
| маркеры долга — числом | **протухший факт, разошедшийся с кодом** | `doc-code-drift` |
| миграция изменена, а `database.md` нет | зависимость в манифесте, не названная в обзоре | `doc-code-drift` |
| capability без упоминания в `architecture.md` | второй способ там, где обзор обещал единственный | `doc-code-drift` |
| | **пересказ документа канона в `context` вместо ссылки** | `doc-consistency` |
| | придирки валидатора: сменились ли они | никакой — проявляются отказом `openspec validate --strict` |
| | связность и читаемость | `doc-wording` |
**Форма `openspec/config.yaml` в левой колонке отсутствует не по забывчивости.**
С канона 10 `docs.py` о файле не говорит ничего: имя, `schema`, незаменённый
пример, адреса паспорта и `CLAUDE.md`, ключи `rules` и сторож версии OpenSpec —
всё это смотрит `openspec.py check` скилла `av-dev:code-openspec`. Плагина
конвейера в проекте может не быть; тогда форму не проверяет никто, и это строка
доклада.
**Агентов двое, и разведены они по глубине, а не по охвату.** `doc-consistency`
читает только `docs/` и `openspec/`, `doc-code-drift` — весь репозиторий и гоняет
читающие команды. Слитый агент делал бы одну половину поверхностной; тот же
разрез, что между `task-form` и `task-wording`.
**Зовутся оба одинаково и одним скиллом — `av-dev:doc-healthcheck`, на весь
канон разом; шагом `adopt` и шагом `upgrade` его зовёт `canon`.** Не на синке
документации: `doc-consistency` на
`opus` по каждой сделанной задаче не окупается, а расхождение между двумя документами по
определению требует двух, и на большинстве задач синк правит один. Пачка,
отбираемая работой, вдобавок не видит того, чего работа не касалась, — а именно
там расхождение и живёт: правка отменяет решение в одном документе, парный статус
нужен в другом.
**Перечень фактов, которые `doc-code-drift` сверяет с кодом, закрыт** — имя
основной ветки, команды, пути, зависимости поимённо, настройки с числовым
значением, единые точки проекта, capability, проверяемые инварианты. «Сверить
архитектуру с кодом» задача без дна, и агент, которому её поставили, выдаёт
правдоподобную труху вместо находок.
## `.av-dev.toml`
```toml
# Раскладка av-dev в этом проекте: версия и настройки проверок.
version = 1 # версия раскладки
[docs]
migrations = "internal/store/migrations" # если БД есть
[tasks]
dir = "tasks" # каталог задач от корня репозитория
```
`version` — версия раскладки, под которую проект приведён, целым числом:
обратной совместимости нет, есть «приведён» и «не приведён». Число подставляет
`init`, `adopt` или `upgrade`, и берётся оно из `docs.py version`, а не из
образца: литерал в образце протухает на первом же повышении.
`[docs] migrations` — путь каталога миграций, если БД есть; по нему `docs.py`
делает сверку с `database.md`. `[tasks]` — где лежит каталог задач и как названы
его части; состав ключей описывает скилл `task-track`.
**Формат TOML взят ради комментариев.** Файл лежит в репозитории проекта, и
человек, открывший его через полгода, обязан прочитать в нём, что означает
число. JSON комментариев не знает, и объяснение приходилось держать в другом
файле. Отсюда же правило записи: скрипты правят **строку**, а не переписывают
файл — перезапись стёрла бы то, ради чего формат и выбран.
**Файл один, и лежит он в корне.** До слияния плагинов их было два —
`docs/.docs.json` с версией канона и `<каталог задач>/.tasks.json` с версией
формата задач, — и версии двигались порознь, потому что плагины ставились
порознь. Плагин теперь один, версия одна, а корень выбран потому, что он есть и
у проекта без `docs/`, и у проекта без каталога задач. Прежние имена не
читаются: два дома для одной версии расходятся молча. Увидев их, `docs.py` и
`tasks.py` говорят «прежняя раскладка» и зовут `upgrade` — версия 1 журнала.
Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py`
игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом
строкой, а не молчит.
@@ -0,0 +1,784 @@
# Журнал версий канона до слияния плагинов
**Журнал закрыт.** Он описывает версии канона документов 1–14 — время, когда
плагинов было три и у канона была своя нумерация. Действующий журнал —
[changelog.md](changelog.md), и его версия 1 идёт **после** записи 14 отсюда.
Записи не переписаны под нынешние имена: адрес и имя скилла, верные на день
записи, остаются там как свидетельство. Записи ниже версии 13 зовут служебный
файл `docs/.pm.json` — так и было; переименование делает запись 13, а переезд в
`.av-dev.toml` — запись 1 действующего журнала.
Проект, отставший от канона 14, идёт сперва по этим записям снизу вверх от своей
версии до 14, и только потом переходит в действующий журнал.
---
## Версия 14 — 2026-08-11
У ADR стало два законных источника. Прежде запись цитировала только архивный
`design.md`, то есть решение, принятое по ходу изменения. Решение, принятое
**разведкой** — намеренный отказ, выбор подхода, «проверили и не делаем», — не
имеет `design.md` по построению: change по нему не заводится никогда. Триггер
канона такое решение ловит («намеренный отказ от очевидного подхода»), а дома у
него не было, и оно оседало в записке разведки или в переписке.
**Что изменилось.** `adr/` принимает второй источник — записку разведки. Правило
«промоут, а не второе сочинение» не тронуто: запись по-прежнему цитирует уже
написанное и **называет источник**, изменилось только то, что источников два.
Следом сказали то же самое: карта домов, разрез проверки `doc-consistency`, вход
и устав самого агента, скелеты `docs/adr/README.md` и `docs/adr/template.md`.
**Почему это версия, а не правка текста.** Два следствия уезжают в репозиторий
проекта. По карте домов судит агент согласованности — прежняя редакция читала ADR
со ссылкой на записку разведки как нарушение; а скелеты `adr/` лежат в проекте
файлами и говорят там от имени канона.
**Что сделать проекту.**
1. Ничего с существующими записями: прежние ADR ссылаются на `design.md`, и это
по-прежнему верно.
2. **Поднять шапку `docs/adr/README.md`**: «промоут поверх архивного `design.md`»
→ «промоут поверх уже написанного», с обоими источниками. Точный текст — в
[skeletons.md](skeletons.md), раздел `docs/adr/README.md`.
3. **Поднять `docs/adr/template.md`**: строка `- **Источник:**` называет два
возможных источника.
4. `docs/.docs.json`: `"canon": 14`.
**Чего делать не надо.** Заводить ADR задним числом по старым разведкам. Запись
заводится, когда решение принимается, а не когда о нём вспомнили: сочинённое
через полгода обоснование — ровно то «второе сочинение», против которого правило
и написано.
---
## Версия 13 — 2026-08-11
Служебный файл канона переименован: `docs/.pm.json``docs/.docs.json`. Имя
досталось от плагина `av-dev-pm`, который распался на четыре и которого больше
нет: файл пережил владельца и указывал в пустоту. Правило простое и теперь
соблюдается всеми тремя: **имя служебного файла — имя плагина, который его
завёл**, `.docs.json` — канон, `.tasks.json` — задачи, `openspec/config.yaml`
конвейер.
**Что изменилось.** `docs.py` читает только новое имя. Прежнее он не читает
намеренно: два дома для одной версии канона расходятся молча, а тут расхождение
стоило бы дорого — по этому числу `upgrade` решает, какие записи применять.
Файл под старым именем `check` узнаёт и называет отдельной строкой с готовой
командой, а не жалуется на пропажу.
**Что появилось у соседа.** У каталога задач теперь есть **своя версия
формата** — ключ `tasks` в `<каталог задач>/.tasks.json`, — и свой журнал версий
в скилле `av-dev-tasks:tasks`. До сих пор её не было вовсе: формат задач менялся
записями этого журнала (8, 11, 12), хотя каталог принадлежит другому плагину и
ставится без канона документов. Канон это число не двигает.
**Что сделать проекту.**
1. `git mv docs/.pm.json docs/.docs.json` — одним коммитом с шагом 2. Содержимое
не меняется: ключи те же.
2. **Поправить упоминания прежнего имени** в своих файлах — `CLAUDE.md`, гейт,
`README.md`, `docs/**`. Битой ссылкой это чаще всего не выглядит (файл
служебный, на него ссылаются прозой), поэтому `docs.py check` таких упоминаний
не ловит: ищи `grep -rn '\.pm\.json'` по репозиторию.
3. **Объявить версию формата задач**, если каталог задач в проекте есть:
`<каталог задач>/.tasks.json` с ключом `"tasks": <версия>`. Файла нет вовсе —
заведи, он теперь обязателен: версия не настройка, от которой можно
отказаться. Какое число ставить и что сделать перед этим, говорит журнал
владельца — **позови скилл `av-dev-tasks:tasks`**, здесь этих шагов нет
намеренно: второй перечень чужих шагов разошёлся бы с первым.
4. Гейт не меняется: шаги те же, версию задач сторожит `tasks.py check`, который
в нём уже стоит.
5. `docs/.docs.json`: `"canon": 13`.
**Чего делать не надо.** Ключи в файле не трогаются, документы не переезжают,
записи задач не меняются: версия 13 — про имена служебных файлов и про то, кто
чью версию двигает.
---
## Версия 12 — 2026-08-09
Спринты отменены. Работа идёт задача за задачей, и замороженный набор перестал
что-либо удерживать: он отвечал на вопрос «что делать дальше», а между наборами
на этот вопрос не отвечал никто.
**Что изменилось.** Индексов задач два вместо трёх: `SPRINT.md` упразднён.
Приоритет стал тем, чем он и является, — **порядком строк в `BACKLOG.md`**:
первая строка секции это то, что делают следующим. Назначает порядок человек,
машина его не выводит; двигают его `move --after` и `move --first` с причиной.
Гейт готовности записи стоял на взятии задачи в спринт — единственном месте, где
её судили целиком. Момент нужен и без спринта: теперь это команда
`tasks.py ready <слаг>`, и зовёт её тот, кто берёт задачу в работу.
Ритуал между спринтами (`av-dev-tasks:session`) стал скиллом груминга
(`av-dev-tasks:groom`): два вопроса — что сейчас самое важное и что перестало
быть важным.
**Что сделать проекту.**
1. **Вернуть задачи из набора в беклог и снести `SPRINT.md`.** Порядок такой:
`git rm tasks/SPRINT.md`, затем `tasks.py check --dir tasks --fix`. Строки
набора после удаления файла становятся бездомными, и `--fix` возвращает их в
беклог **в конец своей секции** — с пометкой, что позицию назначает человек.
Наоборот делать нельзя: `check` без удалённого файла увидит третий индекс и
станет ругаться на него, а не чинить.
2. **Снять теги `sprint:<слаг>`** с записей — `tasks.py edit <слаг> --rm-tag
sprint:<слаг>`. Тег больше никем не читается, а `check` о нём молчит: он
законный свободный тег. Пропущенный вреда не сделает, но и пользы не несёт.
3. **Расставить порядок** — первый груминг: `av-dev-tasks:groom`. После шага 1
очередь состоит из того, что машина поставила в конец, то есть очереди нет
вовсе. Пока порядок не назначен, «что делать дальше» по-прежнему без ответа.
4. Поправить упоминания спринта в `CLAUDE.md` проекта, если они были: слот
«общий станок» переехал в груминг под именем «что считается сломанным»,
ориентир «5–8 задач в спринте» стал ориентиром размера порции разбора.
5. `docs/.pm.json`: `"canon": 12`.
**Чего делать не надо.** `REJECTED.md`, `ROADMAP.md` и файлы `items/` не
меняются: спринт жил только в собственном индексе и в тегах.
---
## Версия 11 — 2026-08-09
Каталог задач уехал из `docs/` в корень репозитория. Версия 8 отпустила его из
канона — перестала требовать, перестала проверять, — но место он занимал всё то
же, `docs/tasks/`. Полдела: каталог, принадлежащий одному плагину, лежал внутри
дерева, которым владеет другой. Проекту, поставившему учёт работ без канона
документов, приходилось заводить `docs/` ради одной вложенной папки.
**Что изменилось.** Дом задач — `tasks/` в корне репозитория. `tasks.py` ищет его
там первым; `docs/tasks/` и `doc/tasks/` остаются в списке поиска для
непереехавших проектов, а `init` заводит только в корне. Настройки — там же,
`tasks/.tasks.json`.
**Что осталось терпимым.** `docs.py` по-прежнему не считает `docs/tasks/` файлом
вне канона: непереехавший проект не должен получать выдуманную ошибку вдобавок к
этой записи, которая и так велит ему переехать.
**Что сделать проекту.**
1. `git mv docs/tasks tasks` — одним коммитом вместе с шагом 2, чтобы ссылки не
жили битыми между коммитами.
2. **Починить относительные ссылки внутри записей.** Файл `tasks/items/x.md`
стал на уровень ближе к корню: `../../passport.md` в теле записи теперь
`../docs/passport.md`. Тот же сдвиг у ссылок из индексов. Это самая тихая
часть переезда: битая относительная ссылка не мешает `tasks.py check`, её
ловит только `docs.py check` и только у документов канона.
3. Проверить ссылки **на** задачи снаружи: `CLAUDE.md`, `README.md`, гейт,
`docs/review.md`. Путь `docs/tasks/...` в них теперь ведёт в никуда.
4. Поправить путь в гейте: `tasks.py check --dir tasks`.
5. `docs/.pm.json`: `"canon": 11`.
## Версия 10 — 2026-08-09
Проверка формы `config.yaml` ушла к тому, кто файл заводит. Версия 9 перенесла в
конвейер настройку OpenSpec и честно назвала остаток: форма и сторож версии
остались в `docs.py`, то есть у файла было два плагина — один заводит, другой
проверяет. Остаток закрыт.
**Что появилось.** Скрипт `openspec.py` в скилле `av-dev-code:openspec`, две
команды: `check --dir <корень>` — форма в проекте, `form` — сверка слепка с живым
OpenSpec. Коды выхода те же, что у `docs.py` и `tasks.py`.
**Что удалено из `docs.py`.** Константы `OPENSPEC_*`, проверка формы, сторож
версии и подкоманда `openspec-form` — 252 строки. Скрипт канона про
`openspec/config.yaml` не говорит теперь ничего; `openspec/specs/` он по-прежнему
знает, потому что это дом темы `requirements` и часть карты тем.
**Что стало лучше по дороге.** Адреса `docs/passport.md` и `CLAUDE.md` требуются
теперь **только к тем документам, которые в проекте есть**. Прежняя проверка
требовала их безусловно, то есть на проекте без канона документов требовала
битую ссылку. Теперь отсутствие документа — строка «не проверялось» с указанием,
что без канона конвейер работает вслепую.
**Что осталось за каноном.** Один вопрос, и это не форма: не пересказан ли в
`context` документ, у которого есть свой дом. Разрез — утверждение, опровергаемое
открытием другого файла, против строки «открой такой-то файл»; машине он не
виден, судит агент `doc-consistency`, и `config.yaml` у него во входе.
**Что сделать проекту.**
1. Заменить в гейте и в скриптах `docs.py openspec-form` на `openspec.py form`.
Подкоманды больше нет: прежний вызов упадёт ошибкой употребления (код 2), а не
промолчит.
2. **Добавить в гейт шаг `openspec.py check`, если проект работает по OpenSpec.**
Форму раньше проверял `docs.py check` заодно; теперь он о ней молчит, и без
отдельного шага незаменённый пример в `config.yaml` перестанет ловиться. Это
главная потеря этого повышения, и она тихая.
3. Проект по OpenSpec без установленного `av-dev-code` — форму не проверяет
никто. Либо поставить плагин, либо назвать это принятым риском вслух.
4. `docs/.pm.json`: `"canon": 10`.
## Версия 9 — 2026-08-09
OpenSpec уехал в конвейер. Каталог `openspec/` версией 7 был объявлен слотом
канона: `init` его заводил, `adopt` тоже, образец `config.yaml` лежал в скелетах,
а отсутствие каталога `docs.py` считал отказом. Разрез был проведён не там. По
OpenSpec работает конвейер — без каталога не запускаются ни `opsx:propose`, ни
ревью дизайна, ни сверка требований, — а канон документов о нём только
высказывался. Проект, которому конвейер не нужен, получал отказ за отсутствие
того, чем не пользуется.
**Что появилось.** Скилл `av-dev-code:openspec`: заводит каталог, заменяет
закомментированный пример в `config.yaml` настройкой, объясняет разрез между
ссылкой и пересказом. Образец файла переехал туда же — в
`references/config-skeleton.md` того скилла.
**Что изменилось.** `init` и `canon adopt` OpenSpec больше не заводят, а **зовут
скилл конвейера**; вызов не разрешился — плагина конвейера нет, и это строка
доклада, а не поломка. Отсутствие `openspec/` для `docs.py check` стало
неприменимостью вместо отказа: остальные четыре проверки формы идут только при
живом каталоге.
**Что осталось на месте и почему.** Проверка формы `config.yaml` и сторож версии
(`docs.py openspec-form`) пока живут в скрипте канона — переносить их значит
заводить в конвейере свой скрипт, а этого у него нет ни одного. Разрез названного
это не отменяет, но и не завершает: **у файла сейчас два плагина — один заводит,
другой проверяет**, и это временное состояние, а не задуманное.
**Что сделать проекту.**
1. Ничего не переносить: файлы проекта эта версия не двигает. Меняется только то,
кто их заводит.
2. Проверить, что плагин `av-dev-code` установлен, если проект работает по
OpenSpec. Без него `docs.py check` про каталог промолчит — и молчание это
законное, так что отсутствие настройки перестанет ловиться само.
3. Проект **не** работает по OpenSpec: убедиться, что `openspec/` нет, и
перестать держать его пустым ради проверки. Она больше не требует каталога.
4. `docs/.pm.json`: `"canon": 9`.
## Версия 8 — 2026-08-09
Канон отпустил каталог задач. Плагин `av-dev-pm` расколот на `av-dev-docs`
(документы) и `av-dev-tasks` (учёт работ), и каждый теперь ставится сам по себе.
Пока владелец был один, `docs/tasks/` числился слотом канона: `docs.py` требовал
каталог, звал внутрь чужой скрипт и выдавал его дрейф за свой, а настройки задач
жили ключом `tasks` в `docs/.pm.json`. Для проекта, поставившего только документы,
всё это — отказ на ровном месте: задач он не ведёт, и требовать их не за что.
**Что изменилось.** Каталог задач канону не принадлежит; канон резервирует ему
место в `docs/` и внутрь не смотрит. `docs.py` больше не проверяет согласованность
задач вовсе — это делает `tasks.py` сам, командой своего плагина. Дом настроек
каталога задач — `<каталог задач>/.tasks.json`; ключ `tasks` в `docs/.pm.json`
читается, только пока своего файла нет, и об этом говорится замечанием.
**Что удалено.** Проверка `check_tasks` из `docs.py` и ключ `"tasks"` из скелета
`docs/.pm.json`.
**Что сделать проекту.**
1. Перенести настройки задач: содержимое ключа `"tasks"` из `docs/.pm.json` — в
`docs/tasks/.tasks.json` тем же объектом. Ключа в проекте нет (имена файлов
и заголовков умолчательные) — переносить нечего, шаг пропускается.
2. Удалить ключ `"tasks"` из `docs/.pm.json` после переноса. Оставленный он не
читается, и `tasks.py` скажет об этом замечанием на каждом прогоне.
3. Проверить, что согласованность задач по-прежнему кто-то гоняет: раньше её
тянул за собой `docs.py check`, теперь — только `tasks.py check`. **Если в
гейте проекта стоял один `docs.py`, добавить туда второй шаг** — иначе дрейф
индексов перестанет ловиться молча, и это самая вероятная потеря на этом
повышении.
4. Установить оба плагина, если нужны оба: `av-dev-docs` и `av-dev-tasks`
вместо прежнего `av-dev-pm`. Прежний из `enabledPlugins` убрать.
5. `docs/.pm.json`: `"canon": 8`.
## Версия 7 — 2026-08-07
`openspec/` был предпосылкой, о которой канон говорил, но за которой не следил.
Каталог назван в раскладке, `openspec/specs/` объявлен домом темы `requirements`,
`config.yaml` описан абзацем — а заводил всё это человек руками, и проверялось
из перечисленного ничего. Заведение нового проекта проходило мимо: `init`
собирал документы канона и оставлял проект без каталога, без которого не работают
ни `opsx:propose`, ни ревью дизайна, ни сверка требований.
Хуже отсутствия оказался файл из коробки. `openspec init` кладёт `config.yaml`,
где `context` и `rules` — закомментированный пример на английском. Такой файл
читается как настроенный: он есть, он валиден, имя правильное. Работает он как
пустой, и узнаётся это по предложению, написанному на другом языке, с
capability по имени пакета и без единого `SHALL`.
**Что изменилось:**
1. **`init` заводит OpenSpec сам** — `openspec init --tools claude`, до первого
документа канона. Команда названа в каноне поимённо, потому что её печатает
отказ `docs.py`.
2. **У `openspec/config.yaml` появилась каноническая форма** и скелет в
`skeletons.md`. Содержание — только то, что нужно **в момент порождения
артефакта**: язык, правила именования capability, придирки валидатора и
**адреса** документов канона. Пересказ паспорта, инвариантов, конвенций и
правил ревью в него не переносится.
3. **`docs.py check` проверяет пять вещей:** каталог `openspec/` есть; файл
называется `config.yaml` (`config.yml` OpenSpec читать не станет и об этом не
сообщит); `context` и `rules.specs` не остались примером, а правила для
`specs` называют `SHALL`; `context` называет `passport` и `CLAUDE.md`; ключи
под `rules:` — имена артефактов схемы, а не опечатки.
4. **За свежестью формы следит машина, а не память.** Схема и перечень
артефактов — слепок чужого инструмента; `check` сравнивает `major.minor`
установленного OpenSpec с версией, на которой форма сверялась, и при
расхождении даёт замечание. Перепроверяет `docs.py openspec-form`, и чинится
расхождение **в плагине, а не в проекте**.
5. **Шестое проверяет агент.** Отличить ссылку на документ от пересказа документа
машина не умеет — это работа `doc-consistency`, и в таблице «Что проверяет
машина, а что человек» она стоит строкой.
**Что переехало:** ничего в раскладке `docs/`. Ни один файл не переименовывается
и не перемещается.
**Что сделать проекту:**
1. Нет `openspec/` — завести: `openspec init --tools claude`. Команда кладёт ещё
и `.claude/skills/openspec-*` с `.claude/commands/opsx/*`; это её нормальная
работа, удалять их не надо.
2. Открыть `openspec/config.yaml` и привести к скелету из
[skeletons.md](skeletons.md): блок `context` с языком, правилами именования
capability, требованием `SHALL` и **адресами** `docs/passport.md` и
`CLAUDE.md`; блок `rules` с четырьмя правилами для `specs`.
3. **Вычистить из `context` пересказ.** Инварианты, перечень конвенций, состав
шагов гейта, правило выбора метки и состав проходов ревью — заменить ссылкой
на дом. Признак пересказа простой: строку можно опровергнуть, открыв другой
файл проекта.
4. Проверить имя файла: `config.yml` переименовать в `config.yaml`. Если жили оба
— содержимое `.yml` до сих пор не читалось никем, и переносить из него нужно
именно то, чего нет в `.yaml`.
5. `docs/.pm.json`: `"canon": 7`.
---
## Версия 6 — 2026-08-07
Версия 5 объявила: **каждый документ `docs/` — тема ревью**. Правило оказалось
верным ровно наполовину и потому вредным целиком. Паспорт и схему хранилища
ревью читает, но темами они не являются — они задают границу, по которой судит
чужая тема. Журнал решений и журнал наблюдений ревью изменения не нужны вовсе:
ADR объясняет прошлое решение, а не предъявляет требование к изменению.
Разметчик, применявший плоское правило буквально, обязан был либо завести
фантомные темы `passport`, `adr`, `database`, `research` и продублировать ими
работу тем `architecture` и `operations`, либо потерять четыре документа молча —
а молчащая потеря и есть то, против чего канон написан.
**Что изменилось:**
1. **Три категории документов вместо одной.** Разрез проверяемый: можно ли по
документу сказать «в этом изменении сделано не так»? **Тема** — да, прямо
(`conventions`, `security`, `architecture`, свои документы проекта).
**Источник темы** — нет, но он задаёт границу для чужой темы (`passport.*` →
`architecture`, `database.*` → `operations`, `CLAUDE.md` → `autotests`,
`openspec/specs/` → `requirements`). **Процессный документ** — нет, он про то,
как мы работаем (`tasks/`, `review.*`, `adr.*`, `research.*`, `.pm.json`).
2. **Категории `источник` и `процессный` закрыты, категория `тема` открыта.**
Прежде открытым был весь список, и «не темы ровно две» противоречило
собственной раскладке канона. Теперь пополняется только одно множество, и
документ, которого нет в раскладке, — однозначно своя тема проекта.
3. **`adr/` и `research/` уходят из входа ревью изменения.** Прогон их больше не
открывает. Проверяться они не перестали: ADR без ссылки на архивный
`design.md`, замена без парного статуса, число без провенанса — это по-прежнему
работа `doc-consistency` и `doc-code-drift`, на сессии между спринтами.
4. **`docs.py` печатает категорию в отказе.** «Нет источника passport» читается
иначе, чем «нет темы security». Обязательность при этом не изменилась:
заводятся все документы одинаково и с первого дня.
5. **У задачи появилась метка — `small`, `medium` или `large`.** Это итог
классификации и **единственный вход, по которому конвейер выбирает
исполнителей** на обеих стадиях ревью. Прежние имена `quick`, `standard` и
`wide` описывали глубину прогона, то есть свойство ревью; метка описывает
**задачу** — а выбирают по ней одно и то же. Слово «ступень» уходит:
у одной вещи одно имя.
6. **Метка выводится из двух осей и не равна ни одной из них.** Размер (малое,
среднее, крупное) и сложность (знакомое, незнакомое); метка — максимум по
ним. Малое **незнакомое** изменение получает `large`, трогая один узел, —
поэтому размер и метка пишутся отдельными строками, и выводить одно из
другого нельзя.
**Цена, записанная явно:** расхождение изменения с записанным решением прогоном
больше не ловится. Раньше архитектурный проход мог сказать «здесь отменено
решение ADR-2026-03-11, парного статуса нет»; теперь это скажет только сверка
документации. Сделка сознательная: чтение всего каталога решений оплачивалось на
каждой задаче, а срабатывало на единицах.
**Что переехало:** ничего в раскладке. Ни один файл не переименовывается и не
перемещается.
**Что сделать проекту:**
1. `docs/review.*`, подраздел «Вопросы по темам»: убрать вопросы, адресованные
`passport`, `database`, `adr`, `research` и `review` — **ни одно из этих имён
больше не тема**. Под каноном 5 темой был каждый документ `docs/`, поэтому
такие вопросы там законны и почти наверняка есть. Переадресовать:
про границу домена и про решение → `architecture`; про хранилище, настройку и
измеренное число → `operations`. Вопрос, который никуда не переадресовывается,
удалить, а не оставить висеть: адресованный несуществующей теме, он не
задаётся никем и молча.
2. Там же, «Недоступно проверке»: те же пять имён убрать из разнесения по темам,
переразнеся содержимое по оставшимся.
3. Там же: подраздел **«Триггеры профиля» → «Триггеры метки»**, и разнести его
на **три** списка вместо двух — «крупное здесь» (про объём), «незнакомое
здесь» (про форму решения) и «мелкое здесь» (опускает до `small`). Раньше
первые две оси были склеены в один список, и потому объём в правило по факту
не входил.
4. **Переименовать метки прогона везде, где проект их называет** — в «Триггерах
метки», в «Недоступно проверке», в журнале дефектов: `quick` → **`small`**,
`standard` → **`medium`**, `wide` → **`large`**. Метка это итог классификации
задачи, и три её значения — часть общего словаря канона и конвейера. Слово
«ступень» из документов уходит: у одной вещи одно имя.
5. Проверить, что свои темы проекта не совпадают именем с закрытыми категориями:
`docs/passport/`, `docs/adr/`, `docs/research/`, `docs/database/`,
`docs/review/` — это слоты канона, а не свои темы, и своим смыслом их
наполнять нельзя.
6. Ничего не заводить и не удалять: раскладка канона 6 совпадает с раскладкой
канона 5 файл в файл.
7. `docs/.pm.json`: `"canon": 6`.
---
## Версия 5 — 2026-08-06
Канон перестал быть списком файлов и стал **списком тем ревью**. Раскладка та же,
но читается иначе: документ в `docs/` — это направление проверки, а не просто
текст. Отсюда три правки, и все три развязывают то, что раньше было жёстко
сцеплено.
**Что изменилось:**
1. **Тема живёт файлом или каталогом, на выбор проекта.** `docs/security.md` и
`docs/security/` — одно и то же; тема разрослась, стала каталогом с
`README.md` — канон не сменился и версия не двинулась. Прежде форма была
задана поимённо: `conventions`, `research` и `adr` обязаны были быть
каталогами, остальные — файлами, и обосновать это было нечем. Обе формы сразу
— ошибка: два дома для одного факта расходятся молча.
2. **Список тем открытый.** Всё, что проект кладёт в `docs/`, становится темой
ревью и попадает в план каждого прогона; именной оптики у такой темы нет, её
разбирает общий проход конвейера, заведённый ровно за этим.
Прежде `docs.py` называл незнакомый файл «вне канона» — теперь называет своей
темой проекта и перечисляет их в отчёте. Не темы ровно две: `docs/tasks/` и
`docs/review.*`.
3. **`AGENTS.md` рядом с `CLAUDE.md` — законно.** Он почти стандарт; обязателен
по-прежнему только `CLAUDE.md`, но если лежат оба, читаются оба, и проверки
канона смотрят на второй так же, как на первый.
**Что переехало:**
- в `docs/review.*`: **«Вопросы к проходам» → «Вопросы по темам»**, форма
`<тема>: <вопрос> (<провенанс>)`. Причина не косметическая: вопрос,
адресованный проходу, перестал задаваться молча в тот день, когда тот уехал в
верхнюю ступень ревью. Тема переезд прохода переживает, имя прохода — нет;
- там же **«Недоступно проверке» — по темам**, оба подраздела.
**Что сделать проекту:**
1. Ничего не переименовывать, если всё уже разложено по канону 4: обе формы
дома законны, и текущая — одна из них.
2. `docs/review.*`, подраздел «Вопросы к проходам»: переименовать в «Вопросы по
темам» и переадресовать каждый вопрос теме вместо имени прохода. Темы ядра —
`requirements`, `autotests`, `conventions`, `architecture`, `security`,
`operations`.
3. Там же «Недоступно проверке»: разнести обе половины по темам.
4. Проверить, не лежит ли в `docs/` документ, который раньше считался лишним и
потому не заводился. Теперь он законен и станет темой ревью — это и есть
способ добавить проверку, которой в конвейере нет.
5. `docs/.pm.json`: `"canon": 5`.
6. Позвать судей `doc-consistency` и `doc-code-drift` — шагом 6 `upgrade`.
## Версия 4 — 2026-08-05
Две правки, обе про то, как читается каталог задач. Первая — секция роадмапа
переименована, и вместе с именем расширен её смысл; достигнутое переехало вниз.
Вторая — **у каждой записи появился тип, и тип определяет, что с записью можно
делать**. Раскладка не меняется, файлов канона не прибавляется.
**Что переехало:**
- секция роадмапа `Разработка` → **`Сопровождение`** (англ. `Tooling` →
**`Operations`**). Прежнее имя называло слишком много: роадмап **весь** про
разработку, и секция с таким именем не отличалась от остальных ничем;
- **тип записи** — из префикса заголовка (`[goal]`/`[idea]`) и тега
`kind:<род>` в **поле меты `Тип`** первой строкой. Эмодзи в заголовке от него
производна;
- **поле места** у задачи: `Секция` → **`Категория`**. У цели остаётся `Секция`:
у задачи поле называет полку домена, в которую она вернётся из спринта, у цели
— часть роадмапа, то есть состояние очереди. Одно имя на два смысла их и
смешивало.
**Что добавилось:**
1. **Смысл секции расширен.** Было «инструмент и процесс», стало «чем держат
проект: инструмент, процесс, эксплуатация». Метрики, логи, инфраструктура,
выкладка и дежурство — сюда же. Расширение не косметическое: английское
`Operations` при узком смысле обещало бы эксплуатацию, а внутри лежал бы
линтер.
2. **Общий словарь трёх мест** — [canon.md](canon.md), раздел «Сопровождение и
эксплуатация». Сопровождение — всё, чем держат проект; эксплуатация — его
часть, работа системы на проде. `ROADMAP.md`, секция `Сопровождение` — план
работ; `architecture.md`, раздел «Эксплуатация» — как устроено сейчас;
эксплуатационный проход ревью — оптика проверки. Слить их в одно слово
нельзя: они отвечают на разные вопросы. Слово **«поддержка» не употребляется
вовсе** — в нём слышится помощь пользователю.
3. **Граница с возможностями проходит по тому, кто наблюдает.** «Приложение
сообщает о своём состоянии» — возможность приложения, её место среди прочих
целей. «Дежурный видит состояние на одном экране» — сопровождение. Одни и те
же метрики попадают в разные секции роадмапа, и это верно.
4. **Порядок секций стал каноническим**, и `Готово` переехало **вниз**:
`Запланировано` | `Направления` | `Сопровождение` | `Готово`. Достигнутое
копится — через год этой секции больше, чем всех остальных вместе, — и стоя
первой она отодвигает за экран то, ради чего роадмап открывают чаще всего.
Порядок проверяет `tasks.py check`, переставляет `check --fix`.
5. **Заголовок секции отбивается пустой строкой с обеих сторон.** Прежде
проверялась только строка после заголовка; перестановка секций двигает целые
блоки, и два заголовка оказываются вплотную. Правит `check --fix`.
6. **Тип — единственная ось записи, закрытый словарь из пяти значений:**
`goal` | `feature` | `fix` | `chore` | `research`. Осей было две — тип записи
(`goal`/`idea`/`task`) и род работы (`kind:` тегом), — но из двенадцати
клеток произведения законны были шесть, а алгоритм работы крепится к роду, а
не к типу. Оси схлопнуты.
7. **Тип задаёт схему тела:** какие разделы обязательны, какие допустимы, нужна
ли цель, берётся ли запись в спринт. Проверяет `sprint take`, замечания даёт
`check`. Два раздела новые: **`Воспроизведение`** у `fix` (не
воспроизводится — это `research`, а не `fix`; правило было записано и не
проверялось) и **`Вопрос` + `Куда ляжет ответ`** у `research` вместо
критериев приёмки (приёмка разведки — записанный ответ, и критерии в форме
«оракул: тест» ей натянуты).
8. **Тип `idea` упразднён.** Он значил не род работы, а состояние
незаполненности, а состояние типом быть не может. Теперь оно называется
честно: `research` без раздела «Вопрос» — **сырьё**. В спринт не берётся, как
и прежняя идея, лежит **в конце своей категории** (проверяет `check`,
переставляет `--fix`) и отбирается `list --raw`. Порядка «по важности» в
беклоге по-прежнему нет: этот порядок производен от типа, а не назначен
человеком.
9. **Алгоритм работы над каждым типом** — отдельным файлом,
`skills/tasks/references/task-<тип>.md`: схема, что проверяет машина, что
человек, и порядок шагов.
10. **Имена файлов проверяются.** Правило «текст русский, имена английские»
стояло в каноне и не было подкреплено ничем: `docs.py` имён не смотрел вовсе.
Теперь смотрит — кириллица и не-kebab-case **жёстко**, форма имени
`ADR-ГГГГ-ММ-ДД-slug.md` жёстко, транслит **эвристикой**, то есть
замечанием. Заодно из раскладки канона убраны плейсхолдеры `<тема>.md`,
приглашавшие называть файлы по-русски.
11. **Два агента вместо обещания.** В каноне была таблица «Что проверяет машина,
а что человек», и её правая колонка три версии описывала судью, которого не
существовало. Судьи заведены и разведены по глубине: **`doc-consistency`**
(документ ↔ документ ↔ openspec: факт в двух домах, прямое противоречие,
поведение в `architecture.md` вместо спек, ADR без ссылки и парного статуса,
число без провенанса, заглушка вместо честной строки); **`doc-code-drift`**
(документ ↔ код по закрытому перечню фактов). Оба зовутся раз в спринт на
сессии, а также после adopt и после upgrade, на весь канон разом.
**Что сделать проекту:**
1. Переименовать заголовок секции в `docs/tasks/ROADMAP.md`: `## Разработка` →
`## Сопровождение` (или `## Tooling` → `## Operations`, если индекс
английский). **`check --fix` этого не сделает**: регистр канонической секции
он правит сам, а чужую секцию только называет ошибкой — смысл за человеком.
2. Поправить поле `- **Секция:**` в файлах целей, которые в ней лежат. Порядок
именно такой: сперва заголовок, потом `python3 tasks.py check --dir
docs/tasks` покажет расхождение поимённо.
3. Перечитать состав секции: цели про выкладку, метрики, логи и инфраструктуру,
если они лежали в `Направлениях` за неимением места, переезжают сюда.
4. Прогнать `python3 tasks.py check --dir docs/tasks --fix`. За один проход он
переставит секции роадмапа в канонический порядок (`Готово` уедет вниз вместе
со всем содержимым), поправит отбивку заголовков и **переведёт записи на
типы**: перенесёт значение из тега `kind:` и префикса `[goal]`/`[idea]` в поле
`Тип`, снимет тег, поставит эмодзи в заголовок, переименует `Секция` →
`Категория` у задач и снесёт сырьё в конец категорий.
5. Разобрать то, что `--fix` вернул пометкой `НЕОДНОЗНАЧНО`. Главный случай —
**записи без типа**: заведённые до появления рода работы, они не несут ни
тега, ни префикса, и машина их не угадывает (`feature` от `chore` не
отличает). Проставить руками: `edit <слаг> --type …`.
6. Дописать новые обязательные разделы у задач, которые собираются в спринт:
`Воспроизведение` у каждого `fix`, `Вопрос` и `Куда ляжет ответ` у каждого
`research`. Не «заодно по всему беклогу», а порциями переоценки: `check`
ошибкой это не считает, отказывает только `sprint take`. Сколько задач готово
к взятию, печатает блок здоровья `check`.
7. Прогнать `python3 docs.py check`: он назовёт имена файлов не по правилу.
Кириллицу и не-kebab-case править обязательно, транслит — по решению
человека. **Переименование ADR это перенос ссылок**: слаг стоит в
`adr/README.md`, в `architecture.md` и в чужих документах, и делается одним
проходом, иначе останутся битые ссылки (их `docs.py` потом и покажет).
8. `docs/review.md`, подраздел «Триггеры профиля» — переписать целиком, он
отстал дважды. Снести перечень мест для `deep`: профиль упразднён вместе с
проходом независимой реализации, и перечень стал указателем в пустоту.
Оставшийся перечень перевести на новое правило: `wide` теперь означает не
«новое понятие», а **крупное или незнакомое** изменение и рассчитан на 5–10%
задач; отдельным списком назвать, что здесь считается **мелким** (это `quick`).
Форма подраздела — в [skeletons.md](skeletons.md). Там же проверить журнал
дефектов и «Недоступно проверке» на упоминания независимой реализации: класс
«форма решения, где спека выбора не сделала» переезжает в подраздел «перестали
проверять сознательно», а рядом с ним встаёт вторая честная строка — на
`quick` и `standard` не проверяется ничего, что требует запуска.
9. `docs/.pm.json`: `"canon": 4`.
10. Позвать **обоих судей** — `doc-consistency` и `doc-code-drift`, шагом 6
`upgrade`. Пунктов выше десять, половина из них ручная, и именно здесь видно,
какие сделаны только наполовину: переименования секций и полей разводят
документы, а `check` сверяет число версии, а не существо. Первый прогон на
живом проекте вдобавок самый урожайный — правило единственного дома до сих пор
никто не проверял. Разбирать порциями, а не одним заходом.
## Версия 3 — 2026-08-04
Роадмап стал **состоянием проекта**, а не очередью работ: цель — возможность
приложения, задача — шаг к ней, достигнутое из роадмапа не исчезает. Плюс род
работы, раздел «Затрагивает» и новое умолчание профиля ревью. Раскладка меняется
в одном файле, но переименование и смена секций тянут за собой ссылки, поэтому
шаги делаются одним заходом.
**Что добавилось:**
1. **Род работы** — тег `kind:<род>` в мете задачи, словарь закрыт:
`feature` | `fix` | `chore` | `research`. Обязателен у задачи, у цели
запрещён. `sprint take` без него отказывает, `check` о пропаже напоминает
замечанием. Определение — [canon.md](canon.md), раздел `tasks/`; смысл и
причина, почему тегом, — в SKILL.md скилла `tasks`, раздел «Род работы».
2. **Раздел «Затрагивает»** в теле задачи — перечень границ, которых изменение
касается (эндпоинт, таблица и миграция, формат на диске, публичный тип). Как
и критерии приёмки, требуется к взятию в спринт, а не к заведению.
3. **Секции роадмапа** — четыре вместо двух и **канонические**, в отличие от
секций беклога: `Готово` (достигнутые цели строкой с датой, без ссылки на
файл), `Запланировано` (очередь значима), `Направления` (очереди нет),
`Разработка` (инструмент и процесс, не возможности приложения). Английский
вариант — `Done` | `Planned` | `Directions` | `Tooling`, один язык на весь
индекс. Переименованию проектом не подлежат: у каждой свой смысл, и в первую
пишет сам `close`; `tasks.py check` проверяет состав.
4. **Форма заголовка записи** — по типу: задача отвечает на «что нужно сделать»
и пишется глаголом в неопределённой форме («Не отбрасывать молча лишние
символы»), цель — на «что приложение будет уметь», идея просто называет, о
чём она. `check` считает заголовки не в форме действия и печатает число в
блоке здоровья. Годность формулировки — не машине: её смотрит новый агент
`task-form` (форма записи, только чтение), а язык текста — `doc-wording`.
5. **Заголовки секций — с прописной, после заголовка пустая строка**, во всех
индексах. Написание канонических секций и отбивку правит `check --fix`; он
же сводит написание секции в мете файла с заголовком индекса.
6. **Язык проектных текстов** — [language.md](../../../shared/language.md), общий дом для
документов канона, задач, решений ADR и записок разведки: информационный
стиль (глагол вместо отглагольного существительного, активный залог, факт
вместо оценки, стоп-слова, параллельность), таблицы англицизмов и жаргона и
то, что из стиля отброшено намеренно. Проектных файлов не добавляет и
раскладку не меняет — это правила письма, а не новый слот.
7. **Умолчание профиля ревью сменилось** — это не раскладка, но проектный текст
под него уже написан. `standard` стал рабочим умолчанием: миграция схемы,
публичный контракт и инвариант ступень больше **не** поднимают, `wide`
означает новое понятие или структурную единицу. Подраздел «Триггеры профиля»
в `docs/review.md` остаётся на месте, но его содержимое надо перечитать.
**Что переехало:** `docs/tasks/PLAN.md` → `docs/tasks/ROADMAP.md`; достигнутая
цель — из небытия в секцию `Готово`: `close <цель> --implemented` удаляет файл, но
**оставляет строку с датой**. Прежде роадмап отвечал только «что осталось», и
половину его вопроса вели прозой руками. Вместе с
файлом переименован ключ конфига `tasks.plan` → `tasks.roadmap` и токены
команд: `--index plan` → `--index roadmap`, `init --plan-sections` →
`--roadmap-sections`, `init --plan` → `--roadmap`. Старый ключ в
`docs/.pm.json` не игнорируется молча — `tasks.py` останавливается и называет
переименование.
**Что удалено:** тип `[epic]`. Он был зонтиком между целью и задачами; зонтиком
стала цель, а слишком крупный шаг дробится на шаги помельче под ней. Ноль
употреблений на 97 записей двух живых проектов.
**Что сделать проекту:**
1. `git mv docs/tasks/PLAN.md docs/tasks/ROADMAP.md`.
2. Починить ссылки на прежнее имя: `grep -rn 'PLAN\.md' docs/ CLAUDE.md` —
заголовок самого файла («# План» → «# Роадмап»), строка в `docs/tasks/BACKLOG.md`,
упоминания в `docs/passport.md` и в телах задач.
3. `docs/.pm.json`: ключ `tasks.plan`, если он там был, — в `tasks.roadmap`.
4. Проставить род работы живым задачам: `python3 tasks.py check --dir docs/tasks`
перечислит те, у кого его нет. Задним числом весь беклог не переоформляется —
род нужен к взятию, так что порядок такой: сперва то, что берётся в ближайший
спринт, остальное по ходу переоценки.
5. Дописать раздел «Затрагивает» — тем же порядком и по той же причине: сперва
набор спринта, остальное по мере того, как задача попадает в работу.
6. Перечитать «Триггеры профиля» в `docs/review.md`: строки вида «миграция →
`deep`» теперь дублируют умолчание с обратным знаком. Оставить там только то,
что для этого проекта считается **новым понятием** и **правилом
идентичности**, — и убрать остальное, иначе проект возвращает себе прежнюю
частоту полного набора уточнением.
7. Переименовать секции роадмапа: `порядок` → `Запланировано`, `темы` →
`Направления`; завести `Готово` **первой** и `Разработка` последней
(порядок секций поменялся в версии 4 — если едешь сразу на неё, заводи
`Готово` последней и не переставляй дважды).
Прозаические разделы вроде «Что уже пройдено», которые велись руками,
разложить: звенья — строками в `Готово` (дата, слаг, что стало возможно),
обоснование очереди оставить прозой в `Запланировано`. Любой `##` в индексе
проверка считает секцией, и теперь `check` называет чужую секцию ошибкой.
8. Переформулировать цели ответом на **«что приложение будет уметь»**: не
«Работа со слиянием», а «Исход слияния не зависит от порядка доставки».
Свойство поведения — законная цель. Цель, которая не про приложение
(процесс, инструмент), переезжает в `Разработка`.
9. `[epic]`, если он в проекте заводился: это либо цель, либо набор задач под
общей целью. `check` назовёт его неизвестным типом.
10. Прогнать `python3 tasks.py check --dir docs/tasks --fix`: он поднимет
написание канонических секций, поставит отбивку после заголовков и сведёт
секцию в мете файлов с заголовками индексов. Секции беклога проект
переименовывает сам — их имена он выбирал, и трогать их скрипт не вправе.
11. Переписать заголовки задач в форму действия — по мере того, как задача
попадает в работу, а не «заодно»: `check` печатает их число, а `task-form`
предложит формулировки на замену пачкой.
12. Прочитать [language.md](../../../shared/language.md) — и **ничего не переписывать задним
числом**. Правила языка применяются к тому, что пишется и правится сейчас;
сплошная вычитка старых документов стоит дороже, чем даёт.
13. `docs/.pm.json`: `"canon": 3`.
## Версия 2 — 2026-08-03
Шапка записи ADR — мета-блоком общей формы, и у статуса появился объявленный
дом. Раскладка не менялась: правка касается одного шаблона.
**Что добавилось:** поле `- **Статус:**` в шапке `docs/adr/template.md` —
`заменено на ADR-…` либо `устарело`, у активной записи поля нет. Правило
«старая запись получает статус» было и раньше ([canon.md](canon.md), `adr/`),
но места под него шаблон не отводил: каждая запись изобретала своё, а колонка
«Статус» таблицы `adr/README.md` брала его оттуда, где он у каждого свой.
**Что переехало:** поля `Дата` и `Источник` в шаблоне стали жирными
(`- **Дата:**`, `- **Источник:**`) — та же форма, что у меты задачи и у записи
журнала дефектов: поле на строку, имя жирным.
**Что удалено:** ничего.
**Что сделать проекту:**
1. Привести `docs/adr/template.md` к скелету версии 2
([skeletons.md](skeletons.md), раздел `docs/adr/template.md`).
2. В существующих записях `docs/adr/ADR-*.md`: жирным поля шапки; если статус
записан прозой или заголовком — перенести его полем `- **Статус:**` в шапку
и сверить с колонкой «Статус» таблицы в `docs/adr/README.md`.
3. `docs/.pm.json`: `"canon": 2`.
## Версия 1 — 2026-08-03
Первая версия. Проект любой прежней раскладки приводится к ней скиллом `canon`
в режиме `adopt`, а не `upgrade`.
**Что вводится:** раскладка целиком — см. [canon.md](canon.md).
**Что сделать проекту, который приходит из свободной раскладки:**
1. `docs/.pm.json` с `{"canon": 1}` и путём миграций, если БД есть.
2. Скелет канона целиком; незаполненное — одной честной строкой.
3. `docs/specs/` разобрать: поведение — в `openspec/specs/`, обзор — в
`docs/architecture.md`, знание о чужих системах — в `docs/research/`.
Дубли capability удалить, сверив поимённо.
4. `docs/plan.md` → `docs/tasks/PLAN.md`, шаги плана — целями в «порядок».
5. `BRIEF.md` → `docs/passport.md`.
6. `docs/backlog/` → `docs/tasks/`.
7. `docs/review-journal.md` или `docs/review/journal.md` → `docs/review.md`,
плюс раздел настройки конвейера.
8. `docs/drafts/` растворить: идея → задача `[idea]`, намеренный отказ → ADR,
порядок работ → `PLAN.md`.
9. `docs/review-brief.md`, если заводился, удалить: его разделы разошлись по
документам канона.
10. `conventions.md` → `conventions/`, `local-research.md` → `research/`.
11. Завести `docs/security.md` с периметром первой строкой и `docs/adr/`.
12. В `CLAUDE.md`: severity рядом с каждым инвариантом; семантика гейта (чем
краснеет безусловно, где логи, чего в нём нет и кто тогда гоняет дорогое);
**имя основной ветки**; запреты с путями; где `testdata` и куда писать
временное; **что считается необратимым**; общий станок; ориентир по размеру
спринта. Убрать раздел «Процесс», если он пересказывает пайплайн.
13. В `openspec/config.yaml` оставить только нужды генерации и ссылки.
14. Добавить шаг `docs.py check` в гейт проекта.
**Копии правил в шаблонах, которые версия 1 уносит в проект** — их правка в
каноне обязана появляться здесь отдельной версией:
| Что копируется | Дом определения |
| --- | --- |
| форма записи журнала дефектов в `docs/review.md` | `av-dev-code/skills/review/references/review-journal.md` |
| правило заведения ADR в `docs/adr/README.md` | [canon.md](canon.md), раздел `adr/` |
@@ -0,0 +1,52 @@
# Журнал версий формата задач до слияния плагинов
**Журнал закрыт.** У каталога задач была своя версия, пока его вёл отдельный
плагин `av-dev-tasks`. Версия теперь одна на всю раскладку —
действующий журнал [changelog.md](changelog.md), и переезд числа описан его
записью 1.
Запись ниже не переписана под нынешние имена: она описывает состояние, которое
было.
---
## Версия 1 — 2026-08-11
Первая объявленная версия формата. До неё каталог задач версии не имел вовсе:
формат менялся, а сказать, к какому его состоянию приведён конкретный проект,
было нечем — `tasks.py` о расхождении молчал, и отставший каталог выглядел
здоровым ровно до первой команды, которая об него спотыкалась.
**Что появилось.** Ключ `tasks` в `<каталог задач>/.tasks.json` — целое число,
версия формата. Сам файл стал **обязательным**: до сих пор он заводился только
ради имён, отличных от умолчания, и проект с умолчаниями жил без него. Версия —
не настройка, от которой можно отказаться, поэтому `init` и `adopt apply` теперь
пишут файл всегда, а `check` требует числа и сверяет его со своим.
**Что версия значит, а что нет.** Она отвечает на один вопрос — «по какой записи
журнала повышать каталог». Что записи применены **по существу**, из числа не
следует: двигают его руками, и соврать им так же легко, как любой другой
строкой. `check --fix` недостающее число не приписывает намеренно — это было бы
объявлением каталога приведённым к формату, шагов которого никто не делал.
**Чего в этой записи нет.** Переезды, случившиеся до появления числа, — каталог
из `docs/` в корень (канон 11) и отмена спринтов (канон 12) — задним числом сюда
не переписаны. Они уже названы журналом канона, и второй перечень тех же шагов
разошёлся бы с первым. Версия 1 — это формат на день её появления, что бы
проекту ни пришлось пройти до неё.
**Что сделать проекту.**
1. **Догнать формат по журналу канона, если каталог отстал.** Признаки известны
поимённо: каталог лежит в `docs/tasks/` (канон 11 велит `git mv docs/tasks
tasks` и починку относительных ссылок внутри записей), в нём есть `SPRINT.md`
или теги `sprint:<слаг>` (канон 12 велит снести файл, вернуть строки в беклог
через `check --fix` и расставить порядок грумингом). Ничего из этого нет —
каталог уже в сегодняшнем формате, и шаг пропускается.
2. **Завести `<каталог задач>/.tasks.json`**, если его нет. Имена частей в него
не переписываются: там только то, что отличается от умолчания.
3. **Записать версию**: `"tasks": 1` первым ключом.
4. `tasks.py check --dir <каталог задач>` — до отсутствия расхождений.
**Что при этом не трогается.** Записи в `items/`, индексы и `REJECTED.md` не
меняются ни строкой: версия 1 объявляет то, что уже есть, а не переделывает его.
+122
View File
@@ -0,0 +1,122 @@
# Журнал версий раскладки
Одна запись на версию. Проект знает свою версию из ключа `version` в
`.av-dev.toml`; операция `upgrade` скилла `av-dev:canon` идёт по записям
снизу вверх от версии проекта до текущей и делает то, что в них названо.
Правило записи: **что добавилось, что переехало, что удалено, что сделать
проекту**. Без последнего пункта запись бесполезна — по ней и работает
`upgrade`.
Версия — целое число. Обратной совместимости нет: есть «приведён» и «не
приведён». Версия **одна на всю раскладку** — и на документы канона, и на
каталог задач: ведёт их один плагин, и второе число означало бы только вопрос,
по какому журналу повышать.
**До слияния журналов было два**, и нумерация в них своя:
[changelog-before-merge.md](changelog-before-merge.md) — канон документов,
версии 114; [changelog-tasks-before-merge.md](changelog-tasks-before-merge.md) —
формат задач, версия 1. Оба **закрыты и не переписаны**: адрес, верный на день
записи, там и остался. Проект, отставший от канона 14, идёт сперва по ним, а
потом по этому журналу — порядок назван в записи 1.
---
## Версия 2 — 2026-08-13
Скилл `doc-canon` стал `canon`: префикс называл материал (`doc-`), а скилл
занят не материалом, а **формой** — раскладкой всех частей проекта и общим
повышением версии. Ни один файл проекта от этого не переехал; сменились **путь к
скрипту** и **имя вызова**, а оба живут в проекте: первый — строкой гейта, второй
— в `CLAUDE.md` и в записях задач.
**Что переехало в вызовах.** `av-dev:doc-canon``av-dev:canon`. Прочие имена не
тронуты.
**Что сделать проекту.**
1. **Поправить шаг гейта.** Путь к `docs.py` сменился вместе с именем каталога
скилла: `skills/doc-canon/scripts/docs.py`
`skills/canon/scripts/docs.py`. Шаг, который не нашёл скрипт, обязан
краснеть, а не пропускаться, — проверь, что он краснеет.
2. **Поправить свои вызовы скилла**`grep -rn "doc-canon" --exclude-dir=.git .`
по проекту целиком: имя встречается в `CLAUDE.md`, в `Taskfile`, в записях
задач и в документах канона. Прежнее полное имя не разрешится вовсе.
3. **Поднять версию**`docs.py bump`. Последним шагом.
4. `docs.py check` и `tasks.py check --dir <каталог задач>` — до отсутствия
дрейфа.
**Проект, не прошедший запись 1, переименовывает дважды подряд**`skills/canon/`
`skills/doc-canon/` записью 1 и обратно этой. Порядок записей от этого не
меняется: каждая исполняется на том состоянии, которое оставила предыдущая, и
прошлая запись под новое имя не переписывается.
---
## Версия 1 — 2026-08-13
Три плагина — `av-dev-docs`, `av-dev-tasks` и `av-dev-code` — слились в один,
`av-dev`. Раскол делался под раздельную установку: проект мог взять учёт работ
без документов канона или конвейер без обоих. Практика посылку не подтвердила —
подмножество не понадобилось ни разу, — а платился раскол помеченными копиями
общих правил и веткой «плагина нет» на каждый вызов соседа.
**Что переехало в проекте.** Служебных файла было два, стал один:
| Было | Стало |
| --- | --- |
| `docs/.docs.json`, ключ `canon` | `.av-dev.toml` в корне, ключ `version` |
| `docs/.docs.json`, ключ `migrations` | `.av-dev.toml`, секция `[docs]` |
| `<каталог задач>/.tasks.json`, ключ `tasks` | тот же `version`: версия теперь одна |
| `<каталог задач>/.tasks.json`, имена частей | `.av-dev.toml`, секция `[tasks]` |
Корень выбран потому, что он есть у обоих: и у проекта без `docs/`, и у проекта
без каталога задач. Формат TOML — ради комментариев: файл лежит в репозитории
проекта, и назначение числа читают из него самого, а не из документации плагина.
**Что переехало в вызовах.** Имена скиллов сменили пространство имён и получили
префикс по прежнему плагину: `av-dev-docs:canon``av-dev:doc-canon`,
`av-dev-docs:init``av-dev:doc-init`, `av-dev-docs:docs``av-dev:doc-sync`,
`av-dev-docs:healthcheck``av-dev:doc-healthcheck`, `av-dev-tasks:tasks`
`av-dev:task-track`, `av-dev-tasks:groom``av-dev:task-groom`,
`av-dev-code:openspec``av-dev:code-openspec`, `av-dev-code:resolve`
`av-dev:code-resolve`, `av-dev-code:review``av-dev:code-review`.
**Что сделать проекту.**
1. **Отставшим сперва прежние журналы.** Версия канона в `docs/.docs.json`
меньше 14 — пройди записи до 14 по
[changelog-before-merge.md](changelog-before-merge.md), и только потом эту.
Иначе повышение объявит приведённым то, чего никто не делал.
2. **Завести `.av-dev.toml`** в корне репозитория: `version = 1`, секция
`[docs]` с `migrations`, если ключ был, секция `[tasks]` с `dir` и теми
именами частей, которые в `.tasks.json` отличались от умолчаний. Комментарии
пиши свои — файл читает человек.
3. **Удалить `docs/.docs.json` и `<каталог задач>/.tasks.json`.** Прежние имена
не читаются: два дома для одной версии расходятся молча. Пока старые файлы на
месте, `docs.py check` и `tasks.py check` называют это прежней раскладкой.
4. **Переставить плагины.** `av-dev-docs`, `av-dev-tasks` и `av-dev-code`
удалить, `av-dev` поставить — команды в README репозитория плагинов.
5. **Поправить гейт проекта.** Пути к `docs.py`, `tasks.py` и `openspec.py`
сменились вместе с именами каталогов скиллов: `skills/canon/`
`skills/doc-canon/`, `skills/tasks/``skills/task-track/`,
`skills/openspec/``skills/code-openspec/`. Шаг, который не нашёл скрипт,
обязан краснеть, а не пропускаться, — проверь, что он краснеет.
6. **Поправить свои вызовы скиллов** — в `CLAUDE.md`, в `Taskfile`, в записях
задач: короткое имя разрешится в проектную копию, а прежнее полное не
разрешится вовсе.
7. **Найти, где проект читает служебный файл сам.** Шаг гейта, скрипт, шаблон —
что угодно, что брало значение из `docs/.docs.json`, чтобы не заводить факту
второй дом. Такое чтение переезжает на `.av-dev.toml` и на `tomllib` вместо
`json`: `python3 -c 'import tomllib; print(tomllib.load(open(".av-dev.toml","rb"))["docs"]["migrations"])'`.
Ищется командой `grep -rn "\.docs\.json\|\.tasks\.json" --exclude-dir=.git .`
— по проекту целиком, а не по документам: на первом же живом переезде это
нашлось в `Taskfile.yml`, и нашёл это гейт, а не человек.
8. **Поднять версию**`docs.py bump`. Последним шагом: число объявляет
пройденными шаги журнала, и раньше времени поднятое врёт.
9. `docs.py check` и `tasks.py check --dir <каталог задач>` — до отсутствия
дрейфа.
**Чего делать не надо.** Переписывать прошлые записи журналов под новые имена.
Они описывают состояния, которые были, и адрес, верный на день записи, остаётся
верным как свидетельство.
+472
View File
@@ -0,0 +1,472 @@
# Скелеты документов канона
Что кладут `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` маркетплейса требует дословного
совпадения. Правишь текст внутри маркеров — правь дом, а не копию.
**Сама пара маркеров в проект не переносится.** Это машинерия маркетплейса:
путь в ней ведёт в дерево плагина, и в репозитории проекта он не разрешится ни
во что. Кладя скелет, копируй содержимое между маркерами, а строки
`<!-- копия: … -->` и `<!-- /копия: … -->` оставляй здесь.
## `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
# Схема хранилища
СУБД, миграции, правило времени и идентификаторов.
## Таблицы
## Представление данных
Чем физически лежит запись и что происходит при чтении и записи.
## Настройки с числовым значением
Таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен.
Без них замер не превращается в находку: пик памяти — аномалия только рядом
со строкой «запись лежит сжатой и распаковывается целиком».
```
Нет БД — файла нет, и в `.av-dev.toml` нет ключа `[docs] migrations`.
## `docs/security.md`
```markdown
# Модель угроз
## Периметр
<!-- заполнить: первой строкой, против кого защищаемся -->
Контур не развёрнут — назови оба периметра, целевой и сегодняшний, и скажи
прямо, против какого строятся находки.
## Недоверенный вход
Что приходит извне и каким каналом: тело запроса, файл, аргумент команды,
ответ внешней системы, содержимое архива.
## Из чего строятся пути и ключи
Раскладка файлов на диске, состав координатного ключа записи, имя каталога.
Отсюда строится выход за пределы песочницы.
## Что разграничивает доступ
## Что чувствительнее чего
## Что вне модели
Перечислить явно. Пустой пункт означает, что в теме `security` угрозу выдумают
за тебя, и находка никогда не будет исправлена.
```
## `docs/conventions/README.md`
```markdown
# Конвенции кода
Как мы пишем код — в отличие от `openspec/specs/`, который описывает, что
система делает.
**Прозой остаётся только то, что не выражается правилом.** Свойство, ставшее
правилом линтера, отсюда удаляется и переезжает в перечень ниже.
## Записи
## Механизировано
| Правило | Где механизировано |
| --- | --- |
Не названное здесь место механизации означает, что проход по конвенциям будет
добросовестно проверять уже проверенное.
```
Пустой проект: «Конвенций пока нет: код не написан. Наполняется по мере
реального трения, а не вперёд.»
## `docs/research/README.md`
```markdown
# Разведка
Наблюдения за внешним миром: что реально шлёт источник, чем документация
формата расходится с практикой. Источник истины — этот каталог, а не чужая
документация.
**Каждый вывод — с числами и командой, которой получен**, чтобы его можно было
перепроверить.
## Как снималось
## Записи
```
Нет внешних источников: «Внешних источников данных нет — разведка неприменима.»
## `docs/adr/README.md`
```markdown
# Журнал решений
Одна запись — одно решение. **ADR продвигает уже написанное решение, а не
сочиняет его заново**: запись цитирует решение и ссылается на источник —
`openspec/changes/archive/<id>/design.md`, а у решения, принятого разведкой без
изменения, на её записку.
## Когда заводить
Верно одно из трёх:
<!-- копия: adr-когда-заводить из av-dev/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` проверяют запуском, и там же единственные замеры.
Метка рассчитана на **510% задач**; если сюда попадает каждая третья, списки
написаны слишком широко.
**Мелкое здесь** — опускает до `small`. Ориентир по доле — до трети задач, и в
любом случае меньше, чем `medium`: перевес `small` значит, что рабочее умолчание
сместилось само. Помни отрицательный тест конвейера: что
после мерджа не откатывается обратной правкой (миграция, формат на диске,
публичный контракт, имя), — не `small`, каким бы маленьким ни был дифф.
Уточняет умолчания конвейера, не отменяет их; рабочее умолчание — `medium`.
### Недоступно проверке
Оба подраздела — **по темам**: «в теме `operations` не проверяется X» читается,
а «не проверяется X» через месяц не найдёт ни один проход.
**Не проверит ни один проход** — принципиальная граница; по факту промаха не
пересматривается.
**Перестали проверять сознательно** — что, когда и почему, со ссылкой на запись
журнала. Пересматривается **первым**, как только что-то проскочило.
Тему, у которой в проекте нет дома, сюда писать не надо: её называет план
каждого прогона, и это честнее разовой записи.
## Журнал дефектов
Запись на каждый воспроизведённый дефект, сразу, а не ретроспективно: со
временем теряется не факт, а то, почему дефект не поймали.
Форма:
<!-- копия: журнал-дефектов-форма из av-dev/skills/code-review/references/review-journal.md -->
## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман]
- **Где:** путь:строка либо «конвейер, а не код»
- **Симптом:** как обнаружилось, кем и когда
- **Причина:** что на самом деле было не так
- **Чем воспроизведён:** тест, команда, замер — с числами
- **Почему не поймали:** только для проскочивших — какой проход обязан был найти
и что ему помешало
- **Что меняем:** правило прохода, шаг гейта, конвенция, факт в документе
проекта — либо «ничего, цена поимки выше цены дефекта»
<!-- /копия: журнал-дефектов-форма -->
```
Пара маркеров `копия:` внутри — машинерия маркетплейса; в `docs/review.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` и куда писать временное.
## Работа
- **Основная ветка:** <имя>
- **Необратимое** (спрашивается у человека всегда):
- **Что считается сломанным** — какая красная проверка обгоняет развитие,
то есть останавливает текущую работу:
- **Ориентир по размеру порции:** своё число, если замерялось
- **Что такое «сделана»:** конвейер проекта пройден + критерии приёмки проверены
поимённо
## Язык
- Документация, комментарии, сообщения коммитов — русский.
- Код и идентификаторы — английский.
```
Имя основной ветки, запреты с путями и «что необратимо» — не украшение: без
первого падают git-операции батча и расчёт базы диффа, без второго проход может
тронуть рабочие данные, без третьего вся шкала ранжирования триажа держится на
догадке.
## `openspec/config.yaml`
**Образец переехал.** Файл заводит и заполняет скилл
`av-dev:code-openspec`, — потому что по OpenSpec работает он, а не канон
документов. Проект, не ведущий задачи циклом SDD, каталога `openspec/` не имеет
вовсе, и образец
файла, которого у него нет, в скелетах канона лежал бы мёртвым грузом.
**Форму не проверяет и `docs.py`** — с канона 10 он о файле молчит вовсе.
Проверяет её тот же владелец: скилл `av-dev:code-openspec`, команда
`openspec.py check`. Плагина конвейера в проекте может не быть — тогда форму не
смотрит никто, и это строка доклада, а не поломка. Что канон о файле всё же
говорит (единственный дом, а не форма) — [canon.md](canon.md), раздел
`openspec/config.yaml`.
## `.av-dev.toml`
```toml
# Раскладка av-dev в этом проекте: версия и настройки проверок.
version = <текущая версия>
[docs]
# migrations = "<путь>" — появится, когда появится БД
[tasks]
dir = "tasks"
```
`<текущая версия>` подставляет `init` или `adopt`, целым числом; берётся она из
`docs.py version` (строка «версия раскладки, скрипт»), а не из памяти. Литерал здесь
протухает при каждом повышении, поэтому его тут и нет: незамещённый плейсхолдер
ломает разбор TOML громко, а отставшее число дало бы дрейф молча.
Комментарии в файле — не украшение, а причина, по которой взят TOML: файл живёт
в чужом репозитории, и назначение числа читают из него самого. Скрипты это
учитывают и правят строку, а не переписывают файл. Состав ключей —
[canon.md](canon.md), раздел `.av-dev.toml`.
Файл лежит **в корне репозитория**, а не в `docs/`: версия одна на всю
раскладку, и нужна она в том числе проекту, который канон документов ещё не
завёл. Прежние `docs/.docs.json` и `<каталог задач>/.tasks.json` остались от
трёх плагинов, слившихся в один; увидев их, `docs.py check` называет это прежней
раскладкой и зовёт `upgrade`.
+742
View File
@@ -0,0 +1,742 @@
#!/usr/bin/env python3
"""Проверка раскладки документов проекта против канона av-dev.
Определение канона — references/canon.md рядом со скриптом. Здесь только
механизируемая часть: пути, лишние файлы, битые ссылки, версия, плейсхолдеры,
маркеры долга и две сверки с кодом. Смысловые дубли и оставшееся в архитектуре
поведение судит агент — скрипт об этом говорит вслух в конце отчёта.
Коды выхода — общий словарь скриптов av-dev; дом словаря и разбор «дрейф
против окружения» — av-dev/shared/axes.md. Значения — в константах ниже.
"""
from __future__ import annotations
import argparse
import importlib.util
import re
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
from types import ModuleType
from typing import NoReturn
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
def _load_shared() -> ModuleType:
"""Общий читатель `.av-dev.toml` — `shared/config.py` этого же плагина.
Путь считается от файла скрипта, а не от рабочего каталога: скрипт зовут из
репозитория проекта, где ни плагина, ни его дерева в текущем каталоге нет.
Своё дерево — единственное, куда ходить можно; в чужое не ходим никогда.
"""
path = Path(__file__).resolve().parents[3] / "shared" / "config.py"
# Проверка именно файлом: `spec_from_file_location` на отсутствующем пути
# возвращает исправный спек, и падает уже `exec_module` — трейсбеком и кодом
# 1, то есть «найден дрейф, чинится». Битая установка дрейфом не является.
spec = importlib.util.spec_from_file_location("avdev_config", path)
if not path.is_file() or spec is None or spec.loader is None:
print(f"ОТКАЗ: не читается {path} — общий читатель настроек;"
f" переустанови плагин av-dev", file=sys.stderr)
sys.exit(ENV)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
conf = _load_shared()
# Версия раскладки одна на плагин и живёт в `shared/config.py`: её знают оба
# скрипта, и второе число здесь было бы вторым домом.
LAYOUT_VERSION = conf.VERSION
# Дом версии и путей, нужных проверкам, — `.av-dev.toml` в корне репозитория.
# До слияния плагинов файлов было два, `docs/.docs.json` и `.tasks.json`, и
# версии двигались порознь; теперь дом один, и лежит он в корне, потому что
# настройки нужны и проекту без `docs/`.
CONFIG = conf.CONFIG_NAME
# --- Раскладка канона -------------------------------------------------------
# Документ канона: имя → (категория, на какой вопрос отвечает).
#
# Категории — из canon.md, раздел «Три категории документов». Разрез один: можно
# ли по документу сказать «в этом изменении сделано не так»?
# тема — да, прямо: документ заводит направление проверки изменения;
# источник — нет, но он задаёт границу, по которой судит чужая тема;
# процессный — нет: он про то, как мы работаем, а не про изменение.
#
# **Категория не меняет обязательности документа** — заводятся все три
# одинаково и с первого дня. Она меняет только то, что с документом делает
# конвейер ревью, и потому печатается в отказе: «нет источника passport»
# читается иначе, чем «нет темы security», и чинится теми же руками, но с
# другим приоритетом.
#
# **Документ живёт файлом `docs/<имя>.md` либо каталогом `docs/<имя>/` с
# README.md внутри.** Форму выбирает проект: документ разросся — стал каталогом,
# и это не смена канона и не повод править скрипт. Обе формы сразу — ошибка: это
# два дома для одного факта, ровно то, от чего канон и защищает.
DOCS = {
"passport": ("источник", "зачем и для кого, чем НЕ является"),
"architecture": ("тема", "как сложено — обзор, окружение, эксплуатация"),
"security": ("тема", "периметр, недоверенный вход, что вне модели"),
"conventions": ("тема", "как мы пишем код; индекс, промоут, что механизировано"),
"research": ("процессный", "что показала реальность: наблюдения и числа"),
"adr": ("процессный", "почему решено так; индекс, статусы, правило замены"),
"review": ("процессный", "настройка конвейера + журнал дефектов"),
}
# Документ, обязательный только при условии: имя → (ключ .docs.json, категория,
# пояснение).
CONDITIONAL_DOCS = {
"database": ("migrations", "источник", "схема хранилища и настройки"),
}
# Обязательные файлы вне раскладки docs/.
REQUIRED = {
"CLAUDE.md": "памятка агенту: инварианты с severity, команды, семантика гейта",
CONFIG: "версия раскладки av-dev и пути, нужные проверкам",
}
# Файлы, которые документ-каталог обязан держать сверх README.md.
DOC_EXTRA = {
"adr": {"template.md": "шаблон записи ADR"},
}
# Служебное в docs/ и каталог задач, оставшийся там от прежней раскладки. Формы
# у них скрипт не проверяет, и по разным причинам: `.docs.json` не markdown (и
# сам он теперь след прежней раскладки, о котором говорит `check_required`), а
# задачи ведёт **другой скилл** — `task-track`, со своим скриптом и своими
# проверками.
#
# Дом задач с версии 11 — `tasks/` в корне репозитория, то есть вне `docs/`
# вовсе. `docs/tasks/` здесь терпится потому, что непереехавший проект не должен
# получать «файл вне канона» вдобавок к записи журнала, которая и так велит ему
# переехать. Внутрь скрипт не смотрит ни в том, ни в другом случае.
#
# Прежнее имя конфига терпится ровно за тем же: про переименование проект
# слышит одну строку — от `check_required`, — а не две, из которых вторая ещё и
# зовёт файл лишним.
NOT_DOCS = {".docs.json", ".pm.json", "tasks"}
# Слоты, которых в каноне нет, — с адресом, куда уезжает содержимое. Имена,
# совпадающие с темой, отсюда убраны намеренно: `docs/conventions.md` и
# `docs/review/` теперь законные формы своих тем.
RETIRED = {
"review-brief.md": "документы канона и есть бриф; остаток — в review",
"review-journal.md": "→ документ review",
"plan.md": "→ tasks/ROADMAP.md (ведёт скилл task-track)",
"local-research.md": "→ документ research",
"specs": "поведение → openspec/specs/, обзор → тема architecture",
"drafts": "идея → запись research, отказ → ADR, порядок → ROADMAP.md",
"backlog": "→ tasks/ в корне репозитория (ведёт скилл task-track)",
}
# --- Слаги в именах файлов --------------------------------------------------
# Текст документов русский, а **имена файлов английские, kebab-case**. Причина
# не в эстетике: имя файла стоит в ссылках из других документов, в коммитах и в
# путях, которые люди набирают руками, — а кириллица в пути ломается по-разному
# в разных местах и не набирается на английской раскладке.
SLUG = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*")
ADR_NAME = re.compile(r"ADR-(\d{4})-(\d{2})-(\d{2})-(.+)")
CYRILLIC = re.compile(r"[а-яёА-ЯЁ]")
# Признаки транслита — и только они. Отличить английское слово от транслита
# машина не умеет, поэтому находка идёт **замечанием**: кластеры, которых в
# английском практически не бывает, плюс окончания русских падежей.
#
# Слабые маркеры выброшены намеренно, каждый по своему ложному срабатыванию:
# `ost` ловит `post` и `cost`, `sch` — `schema`, `ya` — `yaml`, `nost` —
# `nostalgia`, хвост `ii` — `radii`. Набор подобран так, чтобы ложных
# срабатываний не было вовсе: правило, краснеющее на правде, приучает
# пролистывать весь блок. Цена известна и принята — `sostoyanie-partii`
# проходит мимо.
#
# Тот же приём, что `translit_ish` в tasks.py; скрипты независимы намеренно —
# каждый уезжает в чужой проект в одиночку.
TRANSLIT_CLUSTER = re.compile(r"zh|kh|shch|tsy|iya|ovanie|enie|stvo")
TRANSLIT_TAIL = re.compile(r"(?:ej|oj|ij|yj|yy|aya)$")
def translit_ish(slug: str) -> bool:
if TRANSLIT_CLUSTER.search(slug):
return True
return any(TRANSLIT_TAIL.search(part) for part in slug.split("-"))
def check_slugs(root: Path, rep: Report) -> None:
"""Имена файлов канона: латиница kebab-case, у ADR — ещё и форма имени.
Каталог задач не трогаем: его слаги ведёт и проверяет tasks.py, и вторая
проверка того же места разошлась бы с первой.
"""
docs = root / "docs"
if not docs.is_dir():
return
# Имена, выбранные каноном, а не проектом: их форма задана здесь же.
fixed = {"README.md", "template.md"} | {f"{name}.md" for name in DOCS}
# Все документы-каталоги, включая свои темы проекта: правило имён общее, а
# перечислять их поимённо значило бы закрыть открытый список.
for folder in sorted(docs.iterdir()):
if not folder.is_dir() or folder.name in NOT_DOCS:
continue
sub = folder.name
for path in sorted(folder.rglob("*.md")):
name = path.name
rel = path.relative_to(root)
if name in fixed:
continue
stem = path.stem
if sub == "adr":
m = ADR_NAME.fullmatch(stem)
if not m:
rep.error(
f"{rel}: имя не по форме ADR-ГГГГ-ММ-ДД-slug.md — "
f"по имени сортируются записи и ищется дата решения"
)
continue
stem = m.group(4)
if CYRILLIC.search(stem):
rep.error(
f"{rel}: кириллица в имени файла — слаги английские, "
f"kebab-case (текст документа при этом русский)"
)
continue
if not SLUG.fullmatch(stem):
rep.error(
f"{rel}: имя не kebab-case латиницей — только строчные "
f"буквы, цифры и одиночные дефисы"
)
continue
if translit_ish(stem):
rep.note(
f"{rel}: имя похоже на транслит («{stem}») — слаг именуется "
f"английским словом по сути, а не записью русского латиницей: "
f"транслит нечитаем тому, кто ищет по смыслу. Проверено "
f"эвристикой: английское слово от транслита машина не отличает"
)
check_capability_slugs(root, rep)
def check_capability_slugs(root: Path, rep: Report) -> None:
specs = root / "openspec" / "specs"
if not specs.is_dir():
return
for folder in sorted(specs.iterdir()):
if not folder.is_dir():
continue
if CYRILLIC.search(folder.name) or not SLUG.fullmatch(folder.name):
rep.error(
f"openspec/specs/{folder.name}/: имя capability — латиница "
f"kebab-case; оно стоит в ссылках из architecture.md и в спеках"
)
DEBT_MARKER = re.compile(r"<!--\s*канон:\s*(.+?)\s*-->")
PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->")
MD_LINK = re.compile(r"\[[^\]]*\]\(\s*<?([^)>\s]+)>?(?:\s+[\"'(][^)]*)?\)")
FENCE = re.compile(r"^\s*(```|~~~)")
INLINE_CODE = re.compile(r"`[^`\n]*`")
def strip_code(text: str) -> str:
"""Выкинуть блоки кода и вставки в обратных кавычках.
Путь в примере или в шаблоне — не ссылка, и краснеть на нём значит краснеть
на каждом образце документа. Инлайн-код тоже: `[docs/backlog](tasks/…)`
в тексте про подписи ссылок — иллюстрация, а не ссылка."""
out, inside = [], False
for line in text.splitlines():
if FENCE.match(line):
inside = not inside
continue
out.append("" if inside else INLINE_CODE.sub("", line))
return "\n".join(out)
@dataclass
class Report:
errors: list[str] = field(default_factory=list)
notes: list[str] = field(default_factory=list)
debts: list[str] = field(default_factory=list)
skipped: list[str] = field(default_factory=list)
def error(self, msg: str) -> None:
self.errors.append(msg)
def note(self, msg: str) -> None:
self.notes.append(msg)
def debt(self, msg: str) -> None:
self.debts.append(msg)
def skip(self, msg: str) -> None:
self.skipped.append(msg)
def fail(code: int, msg: str) -> NoReturn:
print(f"ОТКАЗ: {msg}", file=sys.stderr)
sys.exit(code)
def read_config(root: Path, rep: Report) -> dict:
"""Настройки проекта целиком; проверкам канона нужна секция `[docs]`."""
try:
cfg = conf.read(root)
conf.check_keys(docs_cfg(cfg), DOCS_KEYS, "в секции [docs]")
except conf.ConfigError as exc:
fail(ENV, str(exc))
return cfg
# Ключи секции `[docs]`. Секцию знает этот скрипт, а не общий читатель: ключ
# заводится вместе с проверкой, которая его читает.
DOCS_KEYS = ("migrations",)
def docs_cfg(cfg: dict) -> dict:
return conf.section(cfg, "docs")
# --- Проверки ---------------------------------------------------------------
def check_version(root: Path, cfg: dict, rep: Report) -> None:
if not (root / CONFIG).exists():
return # об отсутствии файла скажет check_required, второй раз не нужно
got = conf.version(cfg)
if got is None:
rep.error(f"в {CONFIG} нет ключа version — версия раскладки не объявлена")
return
if got < LAYOUT_VERSION:
rep.error(
f"проект приведён к раскладке версии {got}, текущая — {LAYOUT_VERSION}:"
f" нужно повышение (скилл av-dev:canon, операция upgrade)"
)
elif got > LAYOUT_VERSION:
rep.error(
f"проект приведён к раскладке версии {got}, а скрипт знает"
f" {LAYOUT_VERSION}: устарел плагин, обнови маркетплейс"
)
def doc_home(root: Path, name: str) -> tuple[Path | None, str | None]:
"""Дом документа: файл `docs/<имя>.md` или каталог `docs/<имя>/`.
Возвращает путь и жалобу. Обе формы сразу — это два дома для одного факта, и
расходятся они молча: правят одну, читают другую.
"""
docs = root / "docs"
as_file = docs / f"{name}.md"
as_dir = docs / name
if as_file.is_file() and as_dir.is_dir():
return as_file, (
f"{name} живёт сразу двумя домами — docs/{name}.md и docs/{name}/:"
f" оставить один, иначе правят один, а читают другой"
)
if as_file.is_file():
return as_file, None
if as_dir.is_dir():
if not (as_dir / "README.md").is_file():
return as_dir, (
f"docs/{name}/ без README.md — у документа-каталога вход"
f" обязателен: по нему его читают агенты"
)
return as_dir, None
return None, None
def check_legacy(root: Path, rep: Report) -> None:
"""Следы прежней раскладки — отдельная проверка, а не ветка отсутствия.
Пока она жила внутри «нового файла нет», половина переезда проходила молча:
завели `.av-dev.toml`, старые файлы удалить забыли — и оба скрипта считали
проект здоровым. Это ровно тот второй дом, против которого переезд и
делался, и увидеть его можно только тогда, когда новый файл уже есть.
"""
legacy = conf.legacy_files(root)
if not legacy:
return
if (root / CONFIG).is_file():
rep.error(
f"прежняя раскладка не убрана: {', '.join(legacy)} рядом с {CONFIG}."
f" Эти файлы не читаются, и версия в них своя — второй дом для того"
f" же числа. Удали их: переезд не закончен (журнал, версия 1, шаг 3)"
)
return
rep.error(
f"нет {CONFIG}, а настройки лежат по прежней раскладке"
f" ({', '.join(legacy)}) — она осталась от трёх плагинов, которые"
f" слились в один: перенеси значения и удали старые файлы операцией"
f" upgrade скилла av-dev:canon (журнал, версия 1). Прежние имена не"
f" читаются, поэтому в этом прогоне всё остальное проверено так, будто"
f" настроек нет вовсе"
)
def check_required(root: Path, cfg: dict, rep: Report) -> None:
for rel, what in REQUIRED.items():
if (root / rel).exists():
continue
if rel == CONFIG and conf.legacy_files(root):
continue # об этом уже сказала check_legacy, и подробнее
rep.error(f"нет {rel}{what}")
for name, (kind, what) in DOCS.items():
home, complaint = doc_home(root, name)
if home is None:
rep.error(
f"нет документа {name} (docs/{name}.md или docs/{name}/),"
f" категория «{kind}» — {what}"
)
continue
if complaint:
rep.error(complaint)
if home.is_dir():
for extra, why in DOC_EXTRA.get(name, {}).items():
if not (home / extra).is_file():
rep.error(f"нет docs/{name}/{extra}{why}")
docs = docs_cfg(cfg)
for name, (key, kind, what) in CONDITIONAL_DOCS.items():
home, complaint = doc_home(root, name)
if complaint:
rep.error(complaint)
if key in docs and home is None:
rep.error(
f"нет документа {name} (docs/{name}.md или docs/{name}/),"
f" категория «{kind}» — {what}"
f" (обязателен: в {CONFIG} объявлен [docs] {key})"
)
elif key not in docs and home is None:
rep.skip(f"{name} — в {CONFIG} нет ключа [docs] {key},"
f" проверка неприменима")
def check_stray(root: Path, rep: Report) -> None:
"""Лишнего в docs/ больше нет — есть свои темы проекта.
Категории `источник` и `процессный` **закрыты**: они перечислены в каноне
поимённо и проектом не пополняются. Открыта только категория `тема` —
поэтому любой документ в docs/, которого нет в раскладке, и есть заявка на
свою тему, и запретить её нельзя. Проверяются только слоты, у которых дом в
другом месте, — иначе переехавшее содержимое вернулось бы темой и выглядело
законным.
"""
docs = root / "docs"
if not docs.is_dir():
rep.error("нет каталога docs/")
return
known = set(DOCS) | set(CONDITIONAL_DOCS)
own: list[str] = []
for entry in sorted(docs.iterdir()):
name = entry.name
if name in RETIRED:
rep.error(f"docs/{name} — слота нет в каноне: {RETIRED[name]}")
continue
if name in NOT_DOCS:
continue
topic = name[:-3] if entry.is_file() and name.endswith(".md") else name
if topic in known:
continue
if entry.is_file() and not name.endswith(".md"):
rep.error(f"docs/{name} — не markdown: тема ревью читается как текст")
continue
if entry.is_dir() and not (entry / "README.md").is_file():
rep.error(
f"docs/{name}/ без README.md — у темы-каталога вход обязателен:"
f" по нему её читают агенты"
)
continue
own.append(topic)
if own:
rep.note(
f"свои темы проекта: {', '.join(own)} — именной оптики у них нет,"
f" их разбирает общий проход конвейера"
)
def canon_docs(root: Path) -> list[Path]:
"""Документы канона. Каталог задач ведёт tasks.py; упразднённые каталоги
уже названы отдельной строкой, и их внутренние ссылки не наша забота —
они переезжают целиком."""
out = []
docs = root / "docs"
skip = {"tasks"} | {name for name in RETIRED if not name.endswith(".md")}
if docs.is_dir():
for path in sorted(docs.rglob("*.md")):
head = path.relative_to(docs).parts[0]
if head in skip or head in RETIRED:
continue
out.append(path)
# AGENTS.md лежит рядом с CLAUDE.md и читается теми же агентами: он почти
# стандарт, и проект вправе держать оба. Обязателен по-прежнему только
# первый.
for name in ("CLAUDE.md", "AGENTS.md"):
path = root / name
if path.exists():
out.append(path)
return out
def check_links(root: Path, rep: Report) -> None:
for path in canon_docs(root):
try:
text = path.read_text(encoding="utf-8")
except OSError as exc:
rep.error(f"{path.relative_to(root)} не читается: {exc}")
continue
for target in MD_LINK.findall(strip_code(text)):
target = target.strip()
if not target or target.startswith(("http://", "https://", "#", "mailto:")):
continue
clean = target.split("#", 1)[0]
if not clean:
continue
if (path.parent / clean).exists():
continue
rep.error(f"{path.relative_to(root)}: битая ссылка на {target}")
def check_placeholders_and_debt(root: Path, rep: Report) -> None:
for path in canon_docs(root):
text = strip_code(path.read_text(encoding="utf-8", errors="replace"))
rel = path.relative_to(root)
for what in PLACEHOLDER.findall(text):
# Замечание, а не дрейф: незаполненный канон — объявленное переходное
# состояние, и краснеть на нём значит требовать выдумать содержание.
rep.note(f"{rel}: плейсхолдер шаблона не заполнен — {what}")
for what in DEBT_MARKER.findall(text):
rep.debt(f"{rel}: {what}")
def doc_text(root: Path, name: str) -> str | None:
"""Текст документа целиком: файл или все markdown каталога, склеенные.
Проверке всё равно, одним файлом написан документ или десятью: она ищет
упоминание, а упоминание живёт в любом из них.
"""
home, _ = doc_home(root, name)
if home is None:
return None
if home.is_file():
return home.read_text(encoding="utf-8", errors="replace")
return "\n".join(
path.read_text(encoding="utf-8", errors="replace")
for path in sorted(home.rglob("*.md"))
)
def check_capabilities(root: Path, rep: Report) -> None:
specs = root / "openspec" / "specs"
text = doc_text(root, "architecture")
if not specs.is_dir():
rep.skip("openspec/specs/ нет — сверка capability с архитектурой неприменима")
return
if text is None:
rep.skip(
"темы architecture нет — capability не сверены с обзором "
"(об отсутствии сказано отдельной строкой)"
)
return
for d in sorted(specs.iterdir()):
if not d.is_dir():
continue
name = d.name
# Засчитываем только явное упоминание: ссылку на спеку или имя в обратных
# кавычках. Голая подстрока совпадает с именем пакета или CLI-команды и
# даёт ложное «упомянуто» — то есть проверку, проходящую не по той причине.
explicit = f"openspec/specs/{name}" in text or f"`{name}`" in text
loose = re.search(rf"\b{re.escape(name)}\b", text) is not None
if explicit:
continue
if loose:
rep.note(
f"capability {name}: в теме architecture есть слово «{name}», но "
f"нет ни ссылки на openspec/specs/{name}, ни имени в обратных "
f"кавычках — проверь, это про capability или про пакет"
)
else:
rep.error(
f"capability {name} есть в openspec/specs/, но не упомянута в "
f"теме architecture — обзор отстал от нормативных спек"
)
def changed_files(root: Path, base: str, rep: Report) -> list[str] | None:
"""Объединение закоммиченного, рабочего дерева и untracked.
Гейт гоняют ДО коммита, поэтому `base...HEAD` не видит ровно ту правку, ради
которой проверка и заводилась: миграция уже лежит в дереве, но ещё не в
истории. Пропущенная правка выглядела бы как зелёный шаг."""
cmds = [
["diff", "--name-only", base],
["ls-files", "--others", "--exclude-standard"],
]
seen: list[str] = []
for cmd in cmds:
try:
out = subprocess.run(
["git", "-C", str(root), *cmd],
capture_output=True,
text=True,
check=True,
)
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
rep.skip(f"сверка миграций пропущена: git не отдал дифф ({exc})")
return None
seen.extend(line for line in out.stdout.splitlines() if line)
return sorted(set(seen))
def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> None:
migrations = docs_cfg(cfg).get("migrations")
if not migrations:
rep.skip(f"в {CONFIG} нет ключа [docs] migrations —"
f" сверка со схемой неприменима")
return
if not base:
rep.skip("база диффа не названа (--base) — сверка миграций со схемой не гонялась")
return
changed = changed_files(root, base, rep)
if changed is None:
return
touched = [f for f in changed if f.startswith(migrations.rstrip("/") + "/")]
if not touched:
return
# Тема database бывает файлом и каталогом — правкой считается любой её файл.
if not any(
f == "docs/database.md" or f.startswith("docs/database/") for f in changed
):
rep.error(
f"миграции изменены ({len(touched)} файлов), а тема database — нет: "
f"схема в документации отстала"
)
# --- Отчёт ------------------------------------------------------------------
def report(rep: Report) -> int:
for msg in rep.errors:
print(f"ДРЕЙФ {msg}")
for msg in rep.notes:
print(f"ЗАМЕЧАНИЕ {msg}")
if rep.debts:
print(f"\nДОЛГ ({len(rep.debts)} маркеров, гейт от них не краснеет):")
for msg in rep.debts:
print(f" {msg}")
if rep.skipped:
print("\nНЕ ПРОВЕРЯЛОСЬ:")
for msg in rep.skipped:
print(f" {msg}")
print(
"\nМашина проверила раскладку, имена файлов, ссылки, версию и две\n"
"сверки с кодом. Форму openspec/config.yaml она не проверяет: каталог\n"
"принадлежит конвейеру, и форму смотрит его скрипт\n"
"(`av-dev:code-openspec`, команда `openspec.py check`). Согласованность\n"
"документов между собой и с кодом — тоже не её: это суждение агентов\n"
"`doc-consistency` (документ ↔ документ ↔ openspec) и `doc-code-drift`\n"
"(документ ↔ код)."
)
if rep.errors:
print(f"\nИтог: дрейф, {len(rep.errors)} пунктов.")
return DRIFT
print("\nИтог: канон соблюдён в механизируемой части.")
return OK
def cmd_check(args: argparse.Namespace) -> int:
root = Path(args.dir).resolve()
if not root.is_dir():
fail(ENV, f"каталог {root} не найден")
if not (root / "docs").exists() and not (root / "CLAUDE.md").exists():
fail(ENV, f"{root} не похож на корень проекта: нет ни docs/, ни CLAUDE.md")
rep = Report()
cfg = read_config(root, rep)
check_version(root, cfg, rep)
check_legacy(root, rep)
check_required(root, cfg, rep)
check_stray(root, rep)
check_slugs(root, rep)
check_links(root, rep)
check_placeholders_and_debt(root, rep)
check_capabilities(root, rep)
check_migrations(root, cfg, args.base, rep)
return report(rep)
def cmd_version(args: argparse.Namespace) -> int:
root = Path(args.dir).resolve()
if not root.is_dir():
fail(ENV, f"нет каталога {root}")
cfg = read_config(root, Report())
got = conf.version(cfg)
print(f"версия раскладки, скрипт: {LAYOUT_VERSION}")
print(f"версия раскладки, проект: {got if got is not None else 'не объявлена'}")
return OK
def cmd_bump(args: argparse.Namespace) -> int:
"""Поднять версию проекта до той, что знает скрипт. Последний шаг повышения.
Двигается **строка**, а не файл: комментарии в нём принадлежат проекту.
Поднять раньше времени нельзя не потому, что скрипт не даст, а потому что
число объявляет пройденными шаги журнала, которых никто не делал, — поэтому
команда отдельная и зовётся руками, а `check --fix` этого не пишет.
"""
root = Path(args.dir).resolve()
if not (root / CONFIG).is_file():
fail(ENV, f"нет {root / CONFIG} — сперва заведи раскладку (adopt)")
was = conf.version(read_config(root, Report()))
if was == LAYOUT_VERSION:
print(f"версия уже {LAYOUT_VERSION}, файл не тронут")
return OK
if was is not None and was > LAYOUT_VERSION:
fail(ENV, f"проект на версии {was}, скрипт знает {LAYOUT_VERSION}:"
f" устарел плагин, обнови маркетплейс")
conf.set_version(root, LAYOUT_VERSION)
print(f"версия раскладки: {was if was is not None else 'не была объявлена'}"
f"{LAYOUT_VERSION} в {CONFIG}")
return OK
def main() -> int:
parser = argparse.ArgumentParser(
prog="docs.py",
description="механическая проверка канона документов проекта",
)
sub = parser.add_subparsers(dest="cmd", required=True)
p_check = sub.add_parser("check", help="раскладка, ссылки, версия, сверки с кодом")
p_check.add_argument("--dir", default=".", help="корень проекта (по умолчанию текущий)")
p_check.add_argument("--base", default=None, help="база диффа для сверки миграций")
p_check.set_defaults(func=cmd_check)
p_ver = sub.add_parser("version", help="версия раскладки: скрипта и проекта")
p_ver.add_argument("--dir", default=".", help="корень проекта")
p_ver.set_defaults(func=cmd_version)
p_bump = sub.add_parser("bump", help="поднять версию проекта до версии скрипта")
p_bump.add_argument("--dir", default=".", help="корень проекта")
p_bump.set_defaults(func=cmd_bump)
args = parser.parse_args()
try:
return args.func(args)
except SystemExit:
raise
except Exception as exc: # noqa: BLE001 — последний рубеж, код 4 по словарю
print(f"ВНУТРЕННИЙ СБОЙ: {exc}", file=sys.stderr)
return INTERNAL
if __name__ == "__main__":
sys.exit(main())