Канон 5 объявил «каждый документ docs/ — тема ревью». Правило верно ровно наполовину и потому вредно целиком. Паспорт и схему хранилища ревью читает, но темами они не являются: по ним нельзя сказать «в этом изменении сделано не так», они задают границу, по которой судит чужая тема. Журнал решений и журнал наблюдений ревью изменения не нужны вовсе — ADR объясняет прошлое, а не предъявляет требование. Разметчик, применявший правило буквально, обязан был либо завести фантомные темы passport, adr, database, research и продублировать ими работу architecture и operations, либо потерять четыре документа молча; случались обе ветки, и в собственном образце плана docs/passport.md не попадал ни строкой, а обязательная арифметика покрытия при этом не сходилась. Категорий теперь три, разрез проверяемый. Тема — да, прямо: conventions, security, architecture и любой свой документ проекта. Источник темы — нет, но он задаёт границу для чужой: passport, database, CLAUDE.md, openspec/specs. Процессный — нет, он про то, как мы работаем: tasks, review, adr, research, .pm.json. Открыта одна категория из трёх, две другие перечислены поимённо, так что документ вне раскладки — однозначно своя тема. adr и research прогон больше не открывает ни одним проходом; docs/review остаётся читаемым, но как настройка конвейера, а не критерий. Цена записана и стала обязательной строкой границ покрытия: расхождение с записанным решением ловит теперь только сверка документации, а число под находкой обязано быть снято на этом прогоне, с приложенной командой. Классификация выдаёт задаче метку — small, medium, large. Прежние quick, standard и wide назывались ступенью и описывали ревью: как глубоко смотрим. Классифицируется же задача, и пока величина называлась свойством прогона, её естественно было пересчитывать на каждом прогоне — что конвейер и делал. Слово «ступень» удалено, а не оставлено синонимом: два имени одной вещи расходятся. Выводится метка из двух разведённых осей — размер (малое, среднее, крупное) и сложность (знакомое, незнакомое), — и равна максимуму по ним. Метка не синоним размера: малое незнакомое изменение получает large, трогая один узел, поэтому план печатает три строки с обоснованием каждая и выводить одну из другой запрещено. Оси остались русскими словами — это суждение прозой; метка английская — это идентификатор, который проходы сравнивают. Разметка переехала из ревью кода в шаг 4 пайплайна, сразу после propose. Она шла первым проходом каждого ревью кода, а перед ревью дизайна ту же величину называл сам пайплайн — то есть оркестратор, который только что довёл предложение до propose. Одно и то же измерялось дважды, и один из двух раз без разведённости с автором, ровно в той точке, ради которой разметчик заведён. Теперь запуск один на задачу, диффа он не видит, план обслуживает обе стадии, и метка после кода не пересматривается: расхождение факта с разметкой ловит журнал дефектов постфактум, как и всякую другую ошибку выбора. На диск план не пишется — четвёртый артефакт рядом с proposal, tasks и design пережил бы задачу и разошёлся бы с ней молча. Ревью дизайна тоже растёт меткой: small — specs, medium — плюс rubric, large — плюс architecture и вопрос автору о трёх формах решения. Раньше rubric и architecture включались одним условием, и medium получал ровно один проход, то есть не отличался от quick ничем. Разведены они потому, что зарабатывают на разном: рубрика порождает свойства узла и окупается уже на среднем изменении, её выход уезжает приёмочными критериями в tasks.md; архитектура отвечает на вопрос про второй способ, а он на среднем знакомом изменении отвечается «нет» ещё до запуска. small подешевел тремя способами сразу. Составом: приёмник тем не запускается, три темы ядра переходят к code сверкой по записанным инвариантам CLAUDE.md с потолком в одну находку, и это не «глубина ниже», а другой дом темы. Входом: specs читает только дельта-спеку, code — только индекс конвенций. Потолком: он появился у каждого опиниативного прохода, а не у одного basics, и у половин code он раздельный, потому что конвенционных находок больше по построению и в общем списке они вытеснили бы техническую половину. Сработавший потолок обязан быть объявлен строкой — молчащий срез неотличим от «больше не нашлось». Отрицательный тест small от этого стал жёстче, а не мягче: вопросы про обратимость миграции задавал приёмник тем, и на этой метке их не задаст никто. Пайплайн задачи вырос до двенадцати шагов. Тривиальность перестала решать состав ревью — она влияет только на explore; глубину обеих стадий называет метка. Проверено прогоном ревьюверов по готовому результату: девять расхождений найдено и починено — контракт находок печатал старый перечень проходов вместо плана по темам, три ссылки в task-batch указывали на шаг коммита вместо закрытия, запись changelog не переводила вопросы, адресованные passport и database, ops и adversary утверждали, что на нижних метках их вопросы задаёт basics, шаблон покрытия в review-code зашивал потолки small намертво, триггеры метки рассыпались на два списка против трёх, тема из директивы CLAUDE.md могла остаться без запуска исполнителя. Гейт зелёный: фронтматтеры, копии, одиннадцать диаграмм, ruff, pyrefly; docs.py прогнан на живом фикстуре и печатает категорию в отказе. Канон повышен до версии 6 с записью, выполнимой upgrade. Решения — 40–44. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
379 lines
25 KiB
Markdown
379 lines
25 KiB
Markdown
# av-dev-skills
|
||
|
||
Личный маркетплейс плагинов для разработки. Процесс имени не имеет — он и есть
|
||
`av-dev`.
|
||
|
||
Что решено и почему — [DECISIONS.md](DECISIONS.md). Что осталось сделать —
|
||
[TODO.md](TODO.md) и [REMAINING.md](REMAINING.md). Как процесс дошёл до текущей
|
||
формы — [HISTORY.md](HISTORY.md).
|
||
|
||
## Плагины
|
||
|
||
- **av-dev-pm** — управление продуктом. Владеет всем `docs/`.
|
||
- `init` — новый проект: интервью по свободному описанию замысла → первичная
|
||
документация;
|
||
- `canon` — привести проект к канону документов: `check` / `adopt` /
|
||
`upgrade`, плюс скрипт `docs.py`. Там же живёт язык проектных текстов —
|
||
информационный стиль, англицизмы, жаргон. Смысловую часть, которой скрипт
|
||
не видит, судят два агента: `doc-consistency` (документы между собой и с
|
||
openspec) и `doc-code-drift` (документы против кода);
|
||
- `docs` — содержимое канона по ходу разработки: ADR из архивного
|
||
`design.md`, промоут конвенций, запись в разведку и журнал ревью, чистка
|
||
архитектуры;
|
||
- `tasks` — задачи и цели каталогом markdown-файлов, у каждой записи тип
|
||
(`goal`, `feature`, `fix`, `chore`, `research`), и тип задаёт её схему;
|
||
вычитывают их два отдельных прохода: `task-form` (форма записи) и
|
||
`doc-wording` (язык);
|
||
- `session` — ритуал между спринтами и ведение спринта.
|
||
- **av-dev-pipeline** — исполнение. **Требует OpenSpec.**
|
||
- `task-pipeline` — задача через полный цикл SDD, от постановки до коммита;
|
||
- `task-batch` — несколько задач разом, каждая в своём worktree;
|
||
- `review-pipeline` — конвейер ревью **по темам**: документ проекта либо
|
||
заводит тему проверки, либо питает чужую тему источником, либо процессный и в
|
||
ревью не читается вовсе. Разметка идёт **один раз на задачу**, сразу после
|
||
`propose`: агент `review-scope` меряет изменение по двум осям — размер и
|
||
сложность — и берёт метку как максимум по ним. Одна метка правит **обе**
|
||
стадии ревью: дизайна (`small` — только сверка спек; `medium` — плюс
|
||
рубрика; `large` — плюс архитектурный проход) и кода (`small` — гейт, спеки,
|
||
код, триаж; `medium` — плюс приёмник тем; `large` — плюс доказательство:
|
||
запуск, замер, построенный путь, 5–10% задач). Десять агентов-проходов.
|
||
- **av-dev-git** — `commit`: сообщения в личном стиле.
|
||
|
||
Соглашение об именах: имя **плагина** длинное с префиксом `av-dev-`, имена
|
||
**скилов** внутри — короткие. Вызов выходит вида `/av-dev-<плагин>:<скилл>`.
|
||
|
||
Кто кого зовёт (стрелка — вызов через пространство имён, не импорт):
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph pipe["av-dev-pipeline — исполнение, требует OpenSpec"]
|
||
direction LR
|
||
batch["task-batch"] --> tp["task-pipeline"]
|
||
tp --> rp["review-pipeline<br/>10 агентов-проходов"]
|
||
batch --> rp
|
||
end
|
||
subgraph pm["av-dev-pm — управление продуктом, владеет docs/"]
|
||
direction LR
|
||
init["init"] --> tasks["tasks"]
|
||
canon["canon"] --> tasks
|
||
session["session"] --> tasks
|
||
docs["docs"]
|
||
end
|
||
opsx["opsx:* — внешний плагин:<br/>explore, propose, apply, archive"]
|
||
git["av-dev-git: commit"]
|
||
|
||
tp --> opsx
|
||
tp --> git
|
||
tp --> docs
|
||
tp --> tasks
|
||
```
|
||
|
||
Зависимость **односторонняя: `av-dev-pipeline` знает про `av-dev-pm`, обратно —
|
||
нет.** Управление продуктом работает в проекте без конвейера; конвейер без
|
||
канона деградирует поразрядно и говорит об этом строкой.
|
||
|
||
## Канон документов проекта
|
||
|
||
Все проекты приводятся к одной раскладке — так проще ориентироваться, когда
|
||
проектов много, и рядом OpenSpec тоже держит строгую структуру. `CLAUDE.md` плюс
|
||
`docs/` (паспорт, архитектура, база, безопасность, конвенции, разведка, ADR,
|
||
ревью, задачи) и `openspec/` — **раскладка целиком, роли документов и правило
|
||
единственного дома живут одним домом**:
|
||
[canon.md](av-dev-pm/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-pipeline/skills/review-pipeline/references/project-facts.md).
|
||
|
||
Прийти в старый проект и перевести его на канон — `/av-dev-pm:canon`. Канон
|
||
версионируется, и проекты повышаются по [журналу
|
||
версий](av-dev-pm/skills/canon/references/changelog.md).
|
||
|
||
## Подключение
|
||
|
||
Плагин подключается **на уровне проекта**, чтобы был активен у всех, кто
|
||
открывает репозиторий. Из терминала, в каталоге проекта:
|
||
|
||
```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-pm@av-dev-skills --scope project
|
||
claude plugin install av-dev-pipeline@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-pm@av-dev-skills": true,
|
||
"av-dev-pipeline@av-dev-skills": true,
|
||
"av-dev-git@av-dev-skills": true
|
||
}
|
||
}
|
||
```
|
||
|
||
**При установке в проект, где лежали проектные копии** скиллов и агентов
|
||
(`.claude/skills/` — голые `task-pipeline`, `review-pipeline`, `task-batch`
|
||
и с префиксом проекта `<проект>-task-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-pm@av-dev-skills --scope project
|
||
claude plugin update av-dev-pipeline@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 манифест маркетплейса
|
||
<plugin>/.claude-plugin/plugin.json манифест плагина
|
||
<plugin>/skills/<skill>/SKILL.md скилы (авто-обнаружение)
|
||
<plugin>/skills/<skill>/references/ что читается по ссылке из скилла
|
||
<plugin>/skills/<skill>/scripts/ tasks.py, docs.py
|
||
<plugin>/agents/ charter'ы сабагентов
|
||
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` решает, звать ли скилл вообще. **Ошибка здесь не выглядит
|
||
ошибкой** — тем же способом, что и в диаграммах.
|
||
|
||
```
|
||
uv run python scripts/frontmatter.py # 0 в порядке, 1 расхождение, 3 не тот каталог
|
||
```
|
||
|
||
Ловится три класса:
|
||
|
||
- **двоеточие с пробелом в описании без кавычек.** Для YAML `: ` внутри
|
||
простого скаляра начинает вложенное отображение, так что «конвейер ревью: гейт,
|
||
сверка…» — это не текст с двоеточием, а синтаксическая ошибка. Так было
|
||
написано часть описаний плагинов, и читались они правильно — замер и разбор
|
||
в [DECISIONS.md](DECISIONS.md), решение III;
|
||
- **`name`, разошедшееся с именем каталога скилла или файла charter'а.** Вызов
|
||
разрешается по каталогу, а сообщение о промахе говорит «нет такого скилла»,
|
||
а не «имя не то»;
|
||
- **цвет charter'а, не отвечающий его модели.** Цвет кодирует модель, а не роль
|
||
прохода — раскладка живёт в
|
||
[review-pipeline/SKILL.md](av-dev-pipeline/skills/review-pipeline/SKILL.md),
|
||
разделе «Модель по проходу», здесь только её механизация. Держаться вниманием
|
||
правило не может: цвет ставится один раз при заведении charter'а, а модель
|
||
потом меняется калибровкой.
|
||
|
||
## Проверка копий правил
|
||
|
||
«Один факт — один дом» держалось вниманием и трижды не удержалось. Копии всё же
|
||
нужны: скелеты канона уезжают в репозиторий проекта и обязаны там что-то
|
||
говорить. Значит копия допустима, но **дословная и помеченная**:
|
||
|
||
```
|
||
uv run python scripts/copies.py # 0 сошлось, 1 расхождение, 2 разметка, 3 не тот каталог
|
||
```
|
||
|
||
Разметка — HTML-комментарии, невидимые в отрендеренном markdown:
|
||
|
||
```
|
||
<!-- дом: <id> --> …текст… <!-- /дом: <id> -->
|
||
<!-- копия: <id> из <путь к дому> --> …тот же текст… <!-- /копия: <id> -->
|
||
```
|
||
|
||
Идентификатор — буквы, цифры и дефис, и он повторяется в закрывающем маркере.
|
||
Строгость нужна ровно затем, чтобы этот абзац сам не объявил дом: `<id>` под
|
||
шаблон не подходит.
|
||
|
||
Сверяется текст между маркерами; ограда блока кода и пустые строки по краям в
|
||
сверку не входят. Маркер, уехавший в проект вместе со скелетом, там полезен: он
|
||
говорит, что у текста есть дом и правится он там.
|
||
|
||
Скрипт ловит четыре вещи: копия разошлась с домом (с диффом), копия указывает не
|
||
на тот файл, дом остался без копий, разметка сломана. Чего он **не** ловит —
|
||
копию, которую забыли пометить: помечать — по-прежнему решение человека.
|
||
|
||
## Проверка диаграмм
|
||
|
||
Диаграммы `mermaid` живут исходником в markdown — картинок в репозитории нет.
|
||
Синтаксическая ошибка в блоке **не видна при чтении**: текст выглядит
|
||
правдоподобно, диff показывает разумную строку, а рендер падает.
|
||
|
||
```
|
||
uv run python scripts/diagrams.py # весь репозиторий
|
||
uv run python 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` | весь репозиторий | миллисекунды |
|
||
| копии правил | правка `*.md` | весь репозиторий | миллисекунды |
|
||
| диаграммы | правка `*.md` | staged-файлы | ~1 с на файл |
|
||
| `ruff check --fix` | правка `*.py` | staged-файлы | доли секунды |
|
||
| `pyrefly check` | правка `*.py` | staged-файлы | доли секунды |
|
||
|
||
Glob разводит две половины: коммит, трогающий одни скрипты, не платит за рендер
|
||
диаграмм, а коммит в документы не гоняет линтеры.
|
||
|
||
**Судятся staged-файлы, а не рабочее дерево** — гейт обязан проверять то, что
|
||
уедет в историю, а не то, что случайно лежит рядом на диске. Исключений два, и
|
||
оба про существо, а не про удобство: `copies.py` сверяет копию с домом, а дом
|
||
лежит в другом файле, которого в индексе может не быть (список staged дал бы
|
||
«копии дословны» ровно там, где правка дома их и разошлась), а `frontmatter.py`
|
||
обходит весь репозиторий за сотые доли секунды — экономить тут нечего.
|
||
|
||
**`ruff` чинит безопасное сам, и починка доносится до этого же коммита**
|
||
(`stage_fixed: true`). Иначе исправленный файл остался бы в рабочем дереве, а в
|
||
историю уехал бы невычищенный — худший из исходов: гейт зелёный, коммит грязный.
|
||
|
||
**Обход разовый — `LEFTHOOK=0 git commit …`.** Он законен ровно для случая,
|
||
когда найденное нечем чинить прямо сейчас; молча пропущенная проверка — нет.
|