Имя описывало устройство, а не предмет: «пайплайн» говорит, что внутри конвейер, — а плагин занят кодом по задачам, и с появлением чекпоинтов он уже не конвейер в чистом виде. Набор имён стал параллельным: docs / tasks / code / git, каждое называет материал. Заодно review-pipeline стал review — слово ушло из плагина целиком, а не наполовину; скиллы выровнялись: resolve / review / openspec. Журнал версий канона переписан вместе со всеми, DECISIONS.md — нет. Разрез по типу высказывания, а не файла: наблюдение и причина неприкосновенны, предписание и адрес обязаны оставаться исполнимыми. Запись версии 10 велит «проверить, что плагин av-dev-pipeline установлен» — проект, дошедший до неё, выполнил бы невыполнимое.
457 lines
32 KiB
Markdown
457 lines
32 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-docs** — документация проекта. Владеет `docs/` и `CLAUDE.md`.
|
||
- `init` — новый проект: интервью по свободному описанию замысла → первичная
|
||
документация;
|
||
- `canon` — привести проект к канону документов: `check` / `adopt` /
|
||
`upgrade`, плюс скрипт `docs.py`. Там же лежит копия языка проектных
|
||
текстов — информационный стиль, англицизмы, жаргон; дом у него общий,
|
||
`shared/language.md`. Смысловую часть, которой скрипт не видит, судят три
|
||
агента: `doc-consistency` (документы между собой и с openspec),
|
||
`doc-code-drift` (документы против кода) и `doc-wording` (язык документов);
|
||
- `docs` — содержимое канона по ходу разработки: ADR из архивного
|
||
`design.md`, промоут конвенций, запись в разведку и журнал ревью, чистка
|
||
архитектуры.
|
||
- **av-dev-tasks** — учёт работ. Владеет каталогом задач.
|
||
- `tasks` — задачи и цели каталогом markdown-файлов, у каждой записи тип
|
||
(`goal`, `feature`, `fix`, `chore`, `research`), и тип задаёт её схему;
|
||
вычитывают их два отдельных прохода: `task-form` (форма записи) и
|
||
`task-wording` (язык записей);
|
||
- `session` — ритуал между спринтами и ведение спринта.
|
||
- **av-dev-code** — код по задачам: решение одной задачи и его проверка.
|
||
Владеет `openspec/`. **Требует OpenSpec и сам его заводит.**
|
||
- `openspec` — завести, настроить и **проверить** `openspec/` в проекте:
|
||
`openspec init`, замена примера в `config.yaml` настройкой канонической
|
||
формы, скрипт `openspec.py` (форма файла + сверка слепка с живой версией
|
||
инструмента). Каталог принадлежит конвейеру, а не канону: без конвейера он
|
||
проекту не нужен, и `docs.py` о нём молчит;
|
||
- `resolve` — одна задача от постановки до закрытия. Обычная идёт циклом SDD
|
||
с **чекпоинтом после ревью дизайна**: объяснение человеческим языком, повод
|
||
скорректировать ход решения. Исследовательская начинается с `opsx:explore` и
|
||
**чекпоинта вариантов** — способы решить, цена каждого, рекомендация; выбор
|
||
оседает по адресу, который назвала сама задача. Между чекпоинтами — без
|
||
согласований;
|
||
- `review` — конвейер ревью **по темам**: документ проекта либо
|
||
заводит тему проверки, либо питает чужую тему источником, либо процессный и в
|
||
ревью не читается вовсе. Разметка идёт **один раз на задачу**, сразу после
|
||
`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-code — исполнение, требует OpenSpec"]
|
||
direction LR
|
||
tp["resolve<br/>2 чекпоинта человеку"] --> rp["review<br/>10 агентов-проходов"]
|
||
osp["openspec<br/>заводит и проверяет openspec/"]
|
||
end
|
||
subgraph docsp["av-dev-docs — документация, владеет docs/"]
|
||
direction LR
|
||
init["init"]
|
||
canon["canon"]
|
||
docs["docs"]
|
||
end
|
||
subgraph tasksp["av-dev-tasks — учёт работ"]
|
||
direction LR
|
||
session["session"] --> tasks["tasks"]
|
||
end
|
||
init --> tasks
|
||
init --> osp
|
||
canon --> tasks
|
||
canon --> osp
|
||
opsx["opsx:* — внешний плагин:<br/>explore, propose, apply, archive"]
|
||
git["av-dev-git: commit"]
|
||
|
||
tp --> opsx
|
||
tp --> git
|
||
tp --> docs
|
||
tp --> tasks
|
||
```
|
||
|
||
Зависимости **односторонние: `av-dev-code` знает про `av-dev-docs` и
|
||
`av-dev-tasks`, обратно — нет.** Между собой эти двое тоже не связаны жёстко:
|
||
каждый работает без другого. Как именно зовут соседа и что делают, когда вызов не
|
||
разрешился, — `shared/plugin-boundary.md`: правило нужно шести скиллам в двух
|
||
плагинах, и ни один им не владеет. То, что нужно нескольким дословно — граница
|
||
плагинов, язык проектных текстов, словарь сопровождения, — живёт домом в
|
||
`shared/` и уезжает в каждый плагин помеченной копией.
|
||
|
||
## Канон документов проекта
|
||
|
||
Все проекты приводятся к одной раскладке — так проще ориентироваться, когда
|
||
проектов много, и рядом OpenSpec тоже держит строгую структуру. `CLAUDE.md` плюс
|
||
`docs/` (паспорт, архитектура, база, безопасность, конвенции, разведка, ADR,
|
||
ревью, задачи) и `openspec/` — **раскладка целиком, роли документов и правило
|
||
единственного дома живут одним домом**:
|
||
[canon.md](av-dev-docs/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-code/skills/review/references/project-facts.md).
|
||
|
||
Прийти в старый проект и перевести его на канон — `/av-dev-docs:canon`. Канон
|
||
версионируется, и проекты повышаются по [журналу
|
||
версий](av-dev-docs/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-docs@av-dev-skills --scope project
|
||
claude plugin install av-dev-tasks@av-dev-skills --scope project
|
||
claude plugin install av-dev-code@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-docs@av-dev-skills": true,
|
||
"av-dev-tasks@av-dev-skills": true,
|
||
"av-dev-code@av-dev-skills": true,
|
||
"av-dev-git@av-dev-skills": true
|
||
}
|
||
}
|
||
```
|
||
|
||
**При установке в проект, где лежали проектные копии** скиллов и агентов
|
||
(`.claude/skills/` — голые `task-pipeline`, `review-pipeline`
|
||
и с префиксом проекта `<проект>-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-docs@av-dev-skills --scope project
|
||
claude plugin update av-dev-tasks@av-dev-skills --scope project
|
||
claude plugin update av-dev-code@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, openspec.py
|
||
<plugin>/agents/ charter'ы сабагентов
|
||
shared/ дома правил, общих для нескольких плагинов
|
||
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/SKILL.md](av-dev-code/skills/review/SKILL.md),
|
||
разделе «Модель по проходу», здесь только её механизация. Держаться вниманием
|
||
правило не может: цвет ставится один раз при заведении charter'а, а модель
|
||
потом меняется калибровкой.
|
||
|
||
## Проверка копий правил
|
||
|
||
«Один факт — один дом» держалось вниманием и трижды не удержалось. Копии всё же
|
||
нужны: скелеты канона уезжают в репозиторий проекта и обязаны там что-то
|
||
говорить. Значит копия допустима, но **дословная и помеченная**:
|
||
|
||
```
|
||
uv run python scripts/copies.py # 0 сошлось, 1 расхождение, 2 разметка, 3 не тот каталог
|
||
```
|
||
|
||
Разметка — HTML-комментарии, невидимые в отрендеренном markdown:
|
||
|
||
```
|
||
<!-- дом: <id> --> …текст… <!-- /дом: <id> -->
|
||
<!-- копия: <id> из <путь к дому> --> …тот же текст… <!-- /копия: <id> -->
|
||
```
|
||
|
||
Идентификатор — буквы, цифры и дефис, и он повторяется в закрывающем маркере.
|
||
Строгость нужна ровно затем, чтобы этот абзац сам не объявил дом: `<id>` под
|
||
шаблон не подходит.
|
||
|
||
Сверяется текст между маркерами; ограда блока кода и пустые строки по краям в
|
||
сверку не входят. Маркер, уехавший в проект вместе со скелетом, там полезен: он
|
||
говорит, что у текста есть дом и правится он там.
|
||
|
||
**Дом правила, общего для нескольких плагинов, лежит в `shared/` и ни одному из
|
||
них не принадлежит.** Так живёт язык проектных текстов: он одинаково нужен
|
||
документам канона и задачам, и хранить его внутри одного плагина значило бы
|
||
отдать общее правило во владение половине. Так же живёт граница плагинов —
|
||
правило обращения к соседу. Плагин везёт копию и потому остаётся
|
||
самодостаточным — `shared/` нужен этому репозиторию, а не установленному
|
||
плагину.
|
||
|
||
**Дом ставится в `shared/` только тогда, когда владельца нет.** У адресов
|
||
владелец есть: раскладку `docs/` держит канон, каталог задач — плагин задач, и
|
||
переносить их наружу значило бы отобрать у владельца его же предмет. Общее без
|
||
владельца едет копией из `shared/`; чужое с владельцем остаётся дома, а
|
||
потребитель на него ссылается.
|
||
|
||
Скрипт ловит четыре вещи: копия разошлась с домом (с диффом), копия указывает не
|
||
на тот файл, дом остался без копий, разметка сломана. Чего он **не** ловит —
|
||
копию, которую забыли пометить: помечать — по-прежнему решение человека.
|
||
|
||
## Проверка адресов документов
|
||
|
||
Адрес документа принадлежит одному плагину, а называют его все: `docs/*` стоит
|
||
примерно в сорока местах конвейера, `tasks/ROADMAP.md` — в четырёх местах канона.
|
||
Переименование в каноне до этих мест само не доходит.
|
||
|
||
```
|
||
python3 scripts/addresses.py # весь репозиторий
|
||
# 0 сошлось, 1 упразднённый адрес или опечатка, 3 перечень владельца недоступен
|
||
```
|
||
|
||
**Зачем машина, а не аккуратность.** Прогон ревью умеет честно деградировать:
|
||
дома темы нет — в границах покрытия появляется строка «документа в проекте нет»
|
||
с названной ценой. Протухший адрес попадает ровно в эту машинерию и выходит
|
||
**правдоподобным отчётом**, а не поломкой. Громкий признак ошибки деградацией
|
||
убран, и здесь он возвращается гейтом.
|
||
|
||
Перечень берётся из **константы владельца** — той, по которой он и так проверяет
|
||
раскладку (`docs.py`, `tasks.py`). Второй перечень прозой был бы вторым домом
|
||
ровно того сорта, против которого написан канон.
|
||
|
||
Судится **упразднённое, а не незнакомое**, и это следует из канона: список тем
|
||
открытый, всё, что проект кладёт в `docs/` сверх закрытых категорий, — законная
|
||
тема, и опровергнуть её нечем. Зато переименование ловится точно: канон, убирая
|
||
слот, кладёт его в карту переездов, и она здесь и есть перечень запрещённого.
|
||
Рядом единственная догадка — имя, **почти** совпавшее с каноническим: `securty`
|
||
это опечатка вероятнее, чем новая тема. Порог замерен по репозиторию: законные
|
||
имена дают до 0.64, опечатки — от 0.91.
|
||
|
||
Не проверяются журналы (они описывают прошлые состояния и задним числом не
|
||
переписываются), адреса `openspec/*` (раскладка чужого инструмента, владельца у
|
||
нас нет) и упоминания в комментариях скриптов — сверяется только markdown. Эти
|
||
границы скрипт печатает сам.
|
||
|
||
## Проверка диаграмм
|
||
|
||
Диаграммы `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` | весь репозиторий | миллисекунды |
|
||
| адреса документов | **каждый коммит** | весь репозиторий | ~0.07 с |
|
||
| диаграммы | правка `*.md` | staged-файлы | ~1 с на файл |
|
||
| `ruff check --fix` | правка `*.py` | staged-файлы | доли секунды |
|
||
| `pyrefly check` | правка `*.py` | staged-файлы | доли секунды |
|
||
|
||
Glob разводит две половины: коммит, трогающий одни скрипты, не платит за рендер
|
||
диаграмм, а коммит в документы не гоняет линтеры.
|
||
|
||
**Судятся staged-файлы, а не рабочее дерево** — гейт обязан проверять то, что
|
||
уедет в историю, а не то, что случайно лежит рядом на диске. Исключений два, и
|
||
три, и все про существо, а не про удобство: `copies.py` сверяет копию с домом, а
|
||
дом лежит в другом файле, которого в индексе может не быть (список staged дал бы
|
||
«копии дословны» ровно там, где правка дома их и разошлась); `frontmatter.py`
|
||
обходит весь репозиторий за сотые доли секунды — экономить тут нечего;
|
||
`addresses.py` идёт **без glob вовсе**, потому что сводит две стороны: перечень
|
||
адресов лежит в `*.py` владельца, а упоминания — в `*.md` соседей, и коммит с
|
||
переименованием документа трогает только первую.
|
||
|
||
**`ruff` чинит безопасное сам, и починка доносится до этого же коммита**
|
||
(`stage_fixed: true`). Иначе исправленный файл остался бы в рабочем дереве, а в
|
||
историю уехал бы невычищенный — худший из исходов: гейт зелёный, коммит грязный.
|
||
|
||
**Обход разовый — `LEFTHOOK=0 git commit …`.** Он законен ровно для случая,
|
||
когда найденное нечем чинить прямо сейчас; молча пропущенная проверка — нет.
|