# av-dev-skills Личный маркетплейс плагинов для разработки. Процесс имени не имеет — он и есть `av-dev`. Что решено и почему — [журнал решений](decisions/README.md). ## Плагины Плагина два: **av-dev** — весь процесс, и **av-dev-git** — сообщения коммитов. Второй работает в любом репозитории, в том числе не приведённом к канону. До 13 августа 2026 процесс жил тремя плагинами (`av-dev-docs`, `av-dev-tasks`, `av-dev-code`); раскол делался под раздельную установку, она не понадобилась ни разу, и плагины слились — [тема 64](decisions/64-three-plugins-merged.md) журнала решений. Имя **скилла** несёт префикс материала, с которым он работает: `doc-`, `task-`, `code-`. Вызов выходит вида `/av-dev:<скилл>`. Префикса нет ровно у одного — `canon`: он работает не с материалом, а с **формой**, общей у всех частей проекта. ### av-dev — форма, документы, учёт, работа **Форма раскладки.** Одна на весь проект, и держит её один скилл. - `canon` — раскладка проекта и её обновление: `check` / `adopt` / `upgrade`, плюс скрипт `docs.py`. `check` сверяет раскладку документов, `adopt` заводит все части сразу и зовёт владельцев каталога задач и `openspec/`, `upgrade` повышает **всю** раскладку по журналу версий — общему, и на документы, и на каталог задач. Содержимого он не ведёт: это соседние скиллы. **Документы проекта.** Владеют **содержимым** `docs/` и `CLAUDE.md`; раскладка — у `canon`. - `doc-init` — новый проект: интервью по свободному описанию замысла → первичная документация; - `doc-healthcheck` — здоровье документации **судом, а не машиной**: не разошлись ли документы между собой и с кодом. Зовёт двух агентов на весь канон разом — `doc-consistency` (документы между собой и с openspec) и `doc-code-drift` (факты против кода) — и разбирает урожай порциями. Дорого, поэтому не на каждой задаче. Язык документов вычитывает отдельный агент `doc-wording`, и зовут его не отсюда, а те, кто только что писал текст: `doc-sync`, `doc-init` и `canon`; - `doc-sync` — содержимое канона по ходу разработки: ADR из архивного `design.md`, промоут конвенций, запись в разведку и журнал ревью, чистка архитектуры. **Учёт работ.** Владеет каталогом задач. - `task-track` — задачи каталогом markdown-файлов: у каждой тип (`feature`, `fix`, `chore`, `research`), задающий её схему, а у проекта — стадия (`build` или `support`), решающая, что значит порядок строк беклога; вычитывают их два отдельных прохода: `task-form` (форма записи) и `task-wording` (язык записей); - `task-groom` — груминг беклога: что сейчас самое важное и что перестало быть важным. Ответ записывается **порядком строк** — приоритет это свойство очереди, и живёт он в индексе. Интерактивный: разбирает вопросы пачкой, переоценивает порциями по 5–8, расставляет верх очереди с доводом на каждое движение. **Работа по задачам.** Владеет `openspec/`. **Сценарий решения требует OpenSpec и заводит его сам** — разведке и обслуживанию он не нужен. - `code-openspec` — завести, настроить и **проверить** `openspec/` в проекте: `openspec init`, замена примера в `config.yaml` настройкой канонической формы, скрипт `openspec.py` (форма файла + сверка слепка с живой версией инструмента). Каталог принадлежит конвейеру, а не канону: без конвейера он проекту не нужен, и `docs.py` о нём молчит; - `code-resolve` — одна задача от постановки до закрытия. **Точка входа одна, а сценария три, и выбирает сценарий сам скилл, прочитав постановку:** классифицировать задачу до вызова человек всё равно не может — «есть ли очевидный способ решения» и «меняется ли спека» видно после чтения записи. **Форм постановки две, и обе полноправны:** запись каталога и просто текст, переданный вызовом, — так же берёт постановку `opsx:propose`. Текстом идут все три сценария; отпадают ровно те шаги, у которых пропал предмет: `ready` гонять нечего, закрывать нечего, а тип, границы и понимание постановки называются вслух первой репликой — человек, написавший текст, рядом и правит одной фразой. Записи в каталог скилл при этом не заводит ни до работы, ни задним числом. **Решение** идёт циклом SDD с чекпоинтом после ревью дизайна: объяснение человеческим языком, повод скорректировать ход. **Обслуживание** (тип `chore`: тулчейн и сборка, зависимости, гит-хуки, перенос, чистка) change не заводит и планового стопа не имеет вовсе: дельта-спек у него нет **по построению**, то есть цикл SDD здесь не урезан, а остаётся без входа. Ревью идёт фиксированным планом без метки и без разметчика — `autotests` и `operations`, плюс `conventions` с техническим разбором, если дифф трогает код; главный шаг сценария — синк документации, потому что обслуживание чаще прочих двигает как раз те факты, которые сверяются с кодом. Правка гейта сверяется по составу проверок, а не по цвету. Нашлась дельта-спека — задача **оказалась шире своего типа**: работа останавливается, тип называется (`fix` или `feature`), человек получает объяснение простым языком и два решения — переформулировать запись и решать её процессом того типа следующим прогоном либо прекратить; «доделать как обслуживание» решением не является. **Разведка** (тип `research`, сырая идея, мутная постановка) кода не пишет вовсе: её чекпоинт — варианты, 2–4 способа решить с ценой каждого, — стоит до первого написанного требования, а исход уезжает в документы канона и в задачи. Обе пачки — документы и записи — **вычитываются перед коммитом** своими проходами: `doc-wording` по документам, `task-form` и `task-wording` по записям. Выбранный способ реализуется **следующим прогоном**, и запускает его человек: смена сценария по ходу — событие с названным исходом, а не тихий поворот. Все три сценария лежат справочниками и одинаково — `references/solve.md`, `references/maintain.md` и `references/research.md`; в самом скилле только вход, развилка и правила, не зависящие от сценария; - `code-review` — конвейер ревью **по темам**: документ проекта либо заводит тему проверки, либо питает чужую тему источником, либо процессный и в ревью не читается вовсе. Разметка идёт **один раз на задачу**, сразу после `propose`: агент `review-scope` меряет изменение по двум осям — размер и сложность — и берёт метку как максимум по ним. Одна метка правит **обе** стадии ревью: дизайна (`small` — только сверка спек; `medium` — плюс рубрика; `large` — плюс архитектурный проход) и кода (`small` — гейт, спеки, код, триаж; `medium` — плюс приёмник тем; `large` — плюс доказательство: запуск, замер, построенный путь, 5–10% задач). Десять агентов-проходов. ### av-dev-git `commit` — сообщения в личном стиле. Отдельным плагином потому, что нужен и в репозитории, который к канону не приведён и никогда не будет. Кто кого зовёт. Сплошная стрелка — вызов скилла через пространство имён, не импорт; пунктир — совет позвать, а не вызов. Агенты-проходы в графе не показаны: их зовут скиллы, названные выше. ```mermaid flowchart TB subgraph avdev["av-dev — один плагин, девять скиллов"] subgraph pipe["работа по задачам; сценарий решения требует OpenSpec"] direction LR tp["code-resolve
3 сценария: разведка,
решение, обслуживание"] --> rp["code-review
10 агентов-проходов"] osp["code-openspec
заводит и проверяет openspec/"] end canon["canon
форма раскладки всего проекта"] subgraph docsp["документы, владеют содержимым docs/"] direction LR init["doc-init"] docs["doc-sync"] hc["doc-healthcheck"] end subgraph tasksp["учёт работ"] direction LR groom["task-groom"] --> tasks["task-track"] end end init --> tasks init --> osp canon --> tasks canon --> osp canon --> hc hc --> tasks docs --> rp rp --> tasks groom -.-> hc opsx["opsx:* — внешний плагин:
explore, propose, apply, archive"] git["av-dev-git: commit"] tp --> opsx tp --> git tp --> docs tp --> tasks ``` **Скиллы зовут друг друга полным именем, а не по пути.** Внутри одного плагина путь бы разрешился, но короткое имя разрешается в устаревшую проектную копию из `.claude/skills/` — молча и без признаков подмены. **Отсутствовать может не плагин, а часть раскладки проекта**: `.av-dev.toml`, `docs/`, каталог задач, `openspec/`. Тогда вызывающий называет строкой, чего теперь не делает никто, и работу не останавливает. Правило целиком — [shared/absence.md](av-dev/shared/absence.md): оно нужно почти каждому скиллу, и ни один им не владеет. Там же, в `shared/`, живут язык проектных текстов, словарь сопровождения и **перечень осей процесса** [axes.md](av-dev/shared/axes.md) — какие закрытые словари правят ходом работы, где дом каждого и чего он **не** решает. Скиллы читают эти дома по ссылке, а дословной копией оттуда уезжает лишь то, что обязано лежать внутри промпта. ## Канон раскладки проекта Все проекты приводятся к одной раскладке — так проще ориентироваться, когда проектов много, и рядом OpenSpec тоже держит строгую структуру. `CLAUDE.md` плюс `docs/` (паспорт, архитектура, база, безопасность, конвенции, разведка, ADR, ревью, задачи) и `openspec/` — **раскладка целиком, роли документов и правило единственного дома живут одним домом**: [canon.md](av-dev/skills/canon/references/canon.md). Здесь она не пересказывается: копия перечня путей уже расходилась с домом, и как раз в обязательных — в ней не хватало путей, чьё отсутствие `docs.py check` считает нарушением. **Документы канона делятся на три категории, и разрез проверяемый: можно ли по документу сказать «в этом изменении сделано не так».** **Тема** — да, прямо (`conventions`, `security`, `architecture` и любой свой документ проекта; список тем открытый — завёл документ, завёл направление проверки). **Источник темы** — нет, но он задаёт границу для чужой темы (`passport`, `database`, `CLAUDE.md`, `openspec/specs/`). **Процессный документ** — нет, он про то, как мы работаем (`tasks/`, `review.*`, `adr.*`, `research.*`); ревью изменения по нему не судит. Форма дома — файл или каталог, на выбор проекта. Отдельного файла-брифа при этом нет — проходы читают документы напрямую; карта «тема → её дом → что оттуда берётся» — [project-facts.md](av-dev/skills/code-review/references/project-facts.md). Прийти в старый проект и перевести его на канон — `/av-dev:canon`. Раскладка версионируется, и проекты повышаются по [журналу версий](av-dev/skills/canon/references/changelog.md). **Версия одна, и живёт она в `.av-dev.toml` в корне репозитория** — вместе с настройками: `[docs] migrations` и секция `[tasks]`, которая говорит, где лежит каталог задач и как названы его части. Версий было две, пока плагинов было три и проект мог взять учёт работ без канона документов; теперь плагин один, и второе число означало бы только вопрос, по какому журналу повышать. Прежние `docs/.docs.json` и `<каталог задач>/.tasks.json` не читаются: увидев их, `docs.py check` называет прежнюю раскладку и зовёт `upgrade` — запись 1 журнала. Формат TOML взят ради комментариев: файл лежит в репозитории проекта, и назначение числа читают из него самого, а скрипты правят строку, а не переписывают файл. Имя служебного файла по-прежнему называет владельца — `.av-dev.toml`, `openspec/config.yaml`. ## Подключение Плагин подключается **на уровне проекта**, чтобы был активен у всех, кто открывает репозиторий. Из терминала, в каталоге проекта: ```bash cd /path/to/project # маркетплейс: один раз на проект. --scope project кладёт его # в extraKnownMarketplaces этого репозитория (см. ниже), без флага — в user claude plugin marketplace add https://git.vakhrushev.me/av/dev-skills.git --scope project # плагины: scope обязателен, умолчание у команды — user, а нам нужен project claude plugin install av-dev@av-dev-skills --scope project claude plugin install av-dev-git@av-dev-skills --scope project ``` Те же команды изнутри Claude Code — со слешем: `/plugin marketplace add …`, `/plugin install … --scope project`. Обе формы дописывают в `.claude/settings.json` проекта то, что можно внести и руками: ```json { "extraKnownMarketplaces": { "av-dev-skills": { "source": { "source": "git", "url": "https://git.vakhrushev.me/av/dev-skills.git" } } }, "enabledPlugins": { "av-dev@av-dev-skills": true, "av-dev-git@av-dev-skills": true } } ``` **При установке в проект, где лежали проектные копии** скиллов и агентов — снеси их. Перечень полный, и он же дом: скиллы носят его помеченной копией, потому что предупреждают о том же в момент работы. Голые имена в `.claude/skills/`: `resolve`, `review`, а у проектов прошлого поколения ещё `task-pipeline`, `review-pipeline`, `task-batch`. С префиксом проекта: `<проект>-task-pipeline`, `<проект>-review-pipeline`. Агенты: `.claude/agents/<проект>-review-*.md`. Две копии одного скилла расходятся, и побеждает та, что **короче названа**: короткое имя разрешится в устаревшую проектную копию молча и без признаков подмены. ## Обновление **Обновление — два шага, и первого мало.** `marketplace update` тянет git-клон маркетплейса, но снимки плагинов лежат отдельно, в `~/.claude/plugins/cache/av-dev-skills/<плагин>/<версия>/`, и обновляются только командой `plugin update`. Один шаг без второго выглядит как «обновил, а ничего не изменилось» — так и было в первый раз. ```bash # 0. отправить свои коммиты: до клона доезжает только то, что на origin git -C /path/to/dev-skills push origin master # 1. клон маркетплейса claude plugin marketplace update av-dev-skills # 2. снимки плагинов — из каталога проекта, где они установлены cd /path/to/project claude plugin update av-dev@av-dev-skills --scope project claude plugin update av-dev-git@av-dev-skills --scope project ``` **`cd` в проект обязателен, и это не педантизм.** Команда правит запись реестра, а записей столько, во скольких проектах плагин установлен; за вызов двигается **одна**. Запущенная не из проекта, она обновит какую-то из них — наблюдалось: `av-dev-git` стоял в шести проектах, один вызов поднял версию ровно в одном, и не в том, из которого звали. Проекты обновляются поштучно. Что стоит и какой версии — одной командой: ```bash python3 - <<'EOF' import json, pathlib d = json.loads(pathlib.Path.home().joinpath(".claude/plugins/installed_plugins.json").read_text()) for name, entries in sorted(d["plugins"].items()): if "av-dev" not in name: continue for e in entries: print(f"{e['version']:14} {name:30} {e.get('projectPath', e.get('scope', ''))}") EOF ``` Версия — первые 12 знаков хеша коммита этого репозитория, так что сверяется глазами с `git rev-parse HEAD | cut -c1-12`. Команда идемпотентна: на уже свежем плагине скажет `already at the latest version`. **Изменения применяются после перезапуска Claude Code** — работающая сессия держит скиллы в контексте и про новый снимок не знает. ## Снятие Действие, обратное подключению. ```bash cd /path/to/project claude plugin uninstall <плагин>@av-dev-skills --scope project ``` Команда правит два места: убирает строку из `enabledPlugins` в `.claude/settings.json` проекта и запись из реестра `~/.claude/plugins/installed_plugins.json`. Снимок в `~/.claude/plugins/cache/av-dev-skills/<плагин>/<версия>/` не трогает — он общий для всех проектов. Маркетплейс тоже остаётся: `extraKnownMarketplaces` нужен остальным плагинам. **Удалять из маркетплейса можно и до снятия с проектов.** `uninstall` идёт по реестру, а не по `marketplace.json`, и снимает плагин, записи о котором в манифесте уже нет. Проверено на `av-dev-backlog`: удалён из маркетплейса, снят с jellybit после — команда отработала штатно. `--scope project` обязателен по той же причине, что и при установке: умолчание у команды — user. `cd` в проект обязателен, но здесь ошибка слышна — вызванная не оттуда, команда откажется словами `is not installed in project scope`, а не снимет плагин наугад, как это делает `plugin update`. **Убрать строку из `settings.json` руками — половина дела:** запись в реестре переживает такую правку, и в инвентаризации проект продолжает числиться. Лечится той же командой из каталога проекта — она отработает и когда в `settings.json` уже пусто. Снялось или нет — видно инвентаризацией из раздела [Обновление](#обновление). **Применяется после перезапуска Claude Code**, как и обновление. ## Структура репозитория ``` .claude-plugin/marketplace.json манифест маркетплейса /.claude-plugin/plugin.json манифест плагина /skills//SKILL.md скилы (авто-обнаружение) /skills//references/ что читается по ссылке из скилла /skills//scripts/ tasks.py, docs.py, openspec.py /agents/ charter'ы сабагентов av-dev/shared/ дома правил и общий читатель .av-dev.toml scripts/ проверки репозитория и пересборка копий pyproject.toml линтеры скриптов, только для этого репозитория lefthook.yml гейт коммита: проверки документов ``` ## Проверка скриптов `tasks.py` и `docs.py` запускаются **где угодно голым `python3` 3.12 без установки чего-либо** — они лежат рядом со скиллами и работают в любом проекте. `pyproject.toml` в корне не меняет этого: он живёт только здесь и держит линтеры, а не зависимости скриптов. ``` uv sync # ставит ruff и pyrefly в .venv, версии прибиты точно uv run ruff check . # правила; --fix для безопасных починок uv run pyrefly check # типы ``` Ноль внешних зависимостей охраняется двумя способами: `banned-api` у ruff ловит частые соблазны по имени, а pyrefly видит окружение, где нет ничего кроме линтеров, и любой сторонний импорт у него не разрешается. Список запретов — не перечень мира, настоящий страж второй. ## Проверка фронтматтеров и описаний плагинов Фронтматтер читает не человек, а загрузчик: по `name` он разрешает вызов, по `description` решает, звать ли скилл вообще. **Ошибка здесь не выглядит ошибкой** — тем же способом, что и в диаграммах. ``` python3 scripts/frontmatter.py # 0 в порядке, 1 расхождение, 3 не тот каталог ``` Ловится четыре класса: - **двоеточие с пробелом в описании без кавычек.** Для YAML `: ` внутри простого скаляра начинает вложенное отображение, так что «конвейер ревью: гейт, сверка…» — это не текст с двоеточием, а синтаксическая ошибка. Так было написано часть описаний плагинов, и читались они правильно — замер и разбор в [решении Р61](decisions/17-notes-tier-work-kind-roadmap.md) журнала; - **`name`, разошедшееся с именем каталога скилла или файла charter'а.** Вызов разрешается по каталогу, а сообщение о промахе говорит «нет такого скилла», а не «имя не то»; - **цвет charter'а, не отвечающий его модели.** Цвет кодирует модель, а не роль прохода — раскладка живёт в [code-review/SKILL.md](av-dev/skills/code-review/SKILL.md), разделе «Модель по проходу», здесь только её механизация. Держаться вниманием правило не может: цвет ставится один раз при заведении charter'а, а модель потом меняется калибровкой; - **описание плагина, разошедшееся между манифестами.** У описания два дома: `<плагин>/.claude-plugin/plugin.json` показывает его установленному плагину, корневой `.claude-plugin/marketplace.json` — тому, кто выбирает, ставить ли. Правят обычно один, и разойтись они успели уже трижды из четырёх. `copies.py` этот класс не берёт: он смотрит markdown, а манифест — json. Отсюда и `*.json` в глобе задачи гейта. ## Проверка копий правил «Один факт — один дом» держалось вниманием и трижды не удержалось. Копии всё же нужны: скелеты канона уезжают в репозиторий проекта и обязаны там что-то говорить. Значит копия допустима, но **дословная и помеченная**: ``` python3 scripts/copies.py # 0 сошлось, 1 расхождение, 2 разметка, 3 не тот каталог ``` Разметка — HTML-комментарии, невидимые в отрендеренном markdown: ``` …текст… …тот же текст… ``` Идентификатор — буквы, цифры и дефис, и он повторяется в закрывающем маркере. Строгость нужна ровно затем, чтобы этот абзац сам не объявил дом: `` под шаблон не подходит. Сверяется текст между маркерами; ограда блока кода и пустые строки по краям в сверку не входят. Маркер, уехавший в проект вместе со скелетом, там полезен: он говорит, что у текста есть дом и правится он там. **Дом правила, общего нескольким скиллам, лежит в `av-dev/shared/` и ни одному из них не принадлежит.** Так живут язык проектных текстов, словарь сопровождения и правило об отсутствующих частях раскладки: каждое нужно многим, и хранить его внутри одного скилла значило бы отдать общее правило во владение части. **Копия при этом делается не всегда.** Пока плагинов было три, копия была единственным способом: путь в дерево соседа не разрешался. Внутри одного дерева справочник читается **по ссылке**, и дословная копия остаётся ровно там, где текст обязан лежать **внутри самого промпта**: в уставе агента, где правило и есть критерий суждения; в `SKILL.md`, который целиком и есть промпт скилла, — за ссылкой скилл пошёл бы отдельным чтением, а правило нужно ему в тот момент, когда он решает; и в скелетах, уезжающих в репозиторий проекта. **Дом ставится в `shared/` только тогда, когда владельца нет.** У адресов владелец есть: раскладку `docs/` держит `canon`, каталог задач — `task-track`, и переносить их наружу значило бы отобрать у владельца его же предмет. Общее без владельца живёт в `shared/`; чужое с владельцем остаётся дома, а потребитель на него ссылается. Скрипт ловит четыре вещи: копия разошлась с домом (с диффом), копия указывает не на тот файл, дом остался без копий, разметка сломана. Чего он **не** ловит — копию, которую забыли пометить: помечать — по-прежнему решение человека. ### Пересборка — `scripts/resync.py` ``` python3 scripts/resync.py # переписать тела всех разошедшихся копий из домов # 0 готово, 2 разметка сломана, 3 не тот каталог ``` Правка дома касается стольких файлов, сколько у него копий, и последний из них забывают — это и есть причина, по которой копии расходятся. Пересборка делает то же машиной и потому дословна по построению. **В гейт коммита скрипт не ставится, и это решение.** Автоматическая пересборка протащила бы правку дома во все копии мимо глаз автора, а правка дома, чья копия уезжает в репозиторий проекта, обязана ещё и попасть в журнал версий канона — этого машина не напишет. Гейт поэтому только **называет** расхождение; согласие с ним остаётся действием человека. Разметку разбирает не он сам: `copies.py` импортируется целиком. Второй разборщик той же разметки разошёлся бы с первым молча — ровно тот класс дефекта, против которого механика копий и заведена. **Ограда блока кода принадлежит месту, а не дому.** Одно и то же тело живёт в доме внутри ```` ``` ````, а в скелете канона — внутри чужой, объемлющей ограды, и своей там иметь не должно. Пересборка берёт тело дома без крайних оград и надевает обратно ту, что была у копии; пустые строки по краям — так же. ## Проверка адресов документов Адрес документа принадлежит одному плагину, а называют его все: `docs/*` стоит примерно в сорока местах конвейера, `tasks/BACKLOG.md` — в нескольких местах канона. Переименование в каноне до этих мест само не доходит. ``` python3 scripts/addresses.py # весь репозиторий # 0 сошлось, 1 упразднённый адрес или опечатка, 3 перечень владельца недоступен ``` **Зачем машина, а не аккуратность.** Прогон ревью умеет честно деградировать: дома темы нет — в границах покрытия появляется строка «документа в проекте нет» с названной ценой. Протухший адрес попадает ровно в эту машинерию и выходит **правдоподобным отчётом**, а не поломкой. Громкий признак ошибки деградацией убран, и здесь он возвращается гейтом. Перечень берётся из **константы владельца** — той, по которой он и так проверяет раскладку (`docs.py`, `tasks.py`). Второй перечень прозой был бы вторым домом ровно того сорта, против которого написан канон. Судится **упразднённое, а не незнакомое**, и это следует из канона: список тем открытый, всё, что проект кладёт в `docs/` сверх закрытых категорий, — законная тема, и опровергнуть её нечем. Зато переименование ловится точно: канон, убирая слот, кладёт его в карту переездов, и она здесь и есть перечень запрещённого. Рядом единственная догадка — имя, **почти** совпавшее с каноническим: `securty` это опечатка вероятнее, чем новая тема. Порог замерен по репозиторию: законные имена дают до 0.64, опечатки — от 0.91. Не проверяются журналы (они описывают прошлые состояния и задним числом не переписываются), адреса `openspec/*` (раскладка чужого инструмента, владельца у нас нет) и упоминания в комментариях скриптов — сверяется только markdown. Эти границы скрипт печатает сам. ## Проверка диаграмм Диаграммы `mermaid` живут исходником в markdown — картинок в репозитории нет. Синтаксическая ошибка в блоке **не видна при чтении**: текст выглядит правдоподобно, диff показывает разумную строку, а рендер падает. ``` python3 scripts/diagrams.py # весь репозиторий python3 scripts/diagrams.py A.md B.md # только названные файлы # 0 рендерятся, 1 нет, 3 нет mermaid-cli ``` Рендерит `mmdc` с PATH или `npx --yes @mermaid-js/mermaid-cli`; ни того ни другого нет — код 3, а не молчаливый успех. Это самая дорогая проверка репозитория: каждый блок — отдельный запуск mermaid-cli со своим chromium, секунда с лишним. Поэтому у неё два рычага, и оба нужны гейту коммита: **блоки собираются все сразу, а рендерятся параллельно** (пул потоков, порядок вывода берётся из порядка сбора), и **проверять можно названные файлы, а не весь репозиторий**. Весь репозиторий — три секунды вместо пятнадцати, один файл — одна. Чего проверка **не** ловит — расхождение диаграммы с прозой вокруг неё. Дословного соответствия между текстом и графом нет, сличать нечего, и держится это правилом старшинства, записанным рядом с каждой диаграммой: **в конвейере ревью старший граф** (он и есть алгоритм планировщика, проза его объясняет), **в остальных местах старшая проза** (диаграмма там сводка). ## Гейт коммита Все семь проверок стоят в `pre-commit` через [lefthook](https://lefthook.dev) — конфиг в [lefthook.yml](lefthook.yml), ставится один раз на клон: ``` lefthook install # пишет .git/hooks/pre-commit lefthook run pre-commit # прогнать руками, не коммитя ``` | Проверка | Когда идёт | Что смотрит | Сколько | | --- | --- | --- | --- | | фронтматтеры | правка `*.md` или `*.json` | весь репозиторий | миллисекунды | | копии правил | правка `*.md` | весь репозиторий | миллисекунды | | адреса документов | **каждый коммит** | весь репозиторий | ~0.07 с | | журнал решений | **каждый коммит** | весь репозиторий | ~0.09 с | | диаграммы | правка `*.md` | staged-файлы | ~1 с на файл | | `ruff check --fix` | правка `*.py` | staged-файлы | доли секунды | | `pyrefly check` | правка `*.py` | staged-файлы | доли секунды | Glob разводит две половины: коммит, трогающий одни скрипты, не платит за рендер диаграмм, а коммит в документы не гоняет линтеры. **Судятся staged-файлы, а не рабочее дерево** — гейт обязан проверять то, что уедет в историю, а не то, что случайно лежит рядом на диске. Исключений три, и все про существо, а не про удобство: `copies.py` сверяет копию с домом, а дом лежит в другом файле, которого в индексе может не быть (список staged дал бы «копии дословны» ровно там, где правка дома их и разошлась); `frontmatter.py` обходит весь репозиторий за сотые доли секунды — экономить тут нечего; `addresses.py` идёт **без glob вовсе**, потому что сводит две стороны: перечень адресов лежит в `*.py` владельца, а упоминания — в `*.md` соседей, и коммит с переименованием документа трогает только первую; `decisions.py` — по той же причине: файл темы и ссылки на него лежат порознь, и переименование темы трогает только одну сторону. **Что судит `decisions.py`.** Номер записи журнала обязан быть один: буквенные метки решений выродились до пятибуквенных и разъехались молча — `АЕАКЛ` означала и тему 53, и тему 65. И подпись ссылки обязана называть свою цель: в `[тема 5](decisions/05-project-start-lifecycle.md)` номер записан дважды, словом и путём, — дубль неизбежный (иначе ссылку не прочитать, не открыв) и потому сверяемый, как всякая копия. **`ruff` чинит безопасное сам, и починка доносится до этого же коммита** (`stage_fixed: true`). Иначе исправленный файл остался бы в рабочем дереве, а в историю уехал бы невычищенный — худший из исходов: гейт зелёный, коммит грязный. **`resync.py` в гейте нет намеренно** — он чинит, а не проверяет, и его правка обязана быть прочитана глазами (см. выше). **Обход разовый — `LEFTHOOK=0 git commit …`.** Он законен ровно для случая, когда найденное нечем чинить прямо сейчас; молча пропущенная проверка — нет.