Задачу часто нужно решить прямо по описанию в разговоре, без файла в каталоге — так её берёт и opsx:propose. Скилл вход текстом объявлял, но прорабатывала его одна разведка: у решения и обслуживания шаг «прочитать задачу» читал разделы записи, шаг закрытия закрывал запись, признак обслуживания опирался на объявленный автором тип, а критерии приходили «от проекта». Форм постановки теперь две, и они равноправны. Отпадают ровно те шаги, у которых пропал предмет: ready гонять нечего, закрывать нечего. Ни один шаг с предметом не выпал — гейт, ревью, синк, чекпоинт и коммит идут как обычно, а сценарий, метку и глубину форма не выбирает. Взамен пропавшего — названное вслух первой репликой: как понята постановка, каким типом её считаешь и где проводишь границу. Человек, написавший текст, сидит в этом же разговоре и поправляет одной фразой; названный после работы тип не признак, а объяснение готового диффа. По сценариям: решение добирает недостающие критерии приёмки на чекпоинте и считает их данными только после ответа; обслуживание объявляет их отсутствие строкой (чекпоинта у него нет) и само называет границы, которых текст не дал; разведка увязана с общим правилом, а её шаг закрытия отпал с оговоркой про единственный след — записанный ответ. Записи в каталог скилл по-прежнему не заводит: ни перед работой, ни задним числом ради закрытия. Похожую строку беклога не разыскивает. Перечень осей пополнен формой постановки: по ней ветвятся готовность, источник типа и наличие закрытия.
45 KiB
av-dev-skills
Личный маркетплейс плагинов для разработки. Процесс имени не имеет — он и есть
av-dev.
Что решено и почему — журнал решений.
Плагины
Плагина два: av-dev — весь процесс, и av-dev-git — сообщения коммитов.
Второй работает в любом репозитории, в том числе не приведённом к канону. До 13 августа 2026 процесс жил тремя плагинами
(av-dev-docs, av-dev-tasks, av-dev-code); раскол делался под раздельную
установку, она не понадобилась ни разу, и плагины слились —
тема 64 журнала решений.
Имя скилла несёт префикс материала, с которым он работает: 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 — сообщения в личном стиле. Отдельным плагином потому, что нужен и в
репозитории, который к канону не приведён и никогда не будет.
Кто кого зовёт. Сплошная стрелка — вызов скилла через пространство имён, не импорт; пунктир — совет позвать, а не вызов. Агенты-проходы в графе не показаны: их зовут скиллы, названные выше.
flowchart TB
subgraph avdev["av-dev — один плагин, девять скиллов"]
subgraph pipe["работа по задачам; сценарий решения требует OpenSpec"]
direction LR
tp["code-resolve<br/>3 сценария: разведка,<br/>решение, обслуживание"] --> rp["code-review<br/>10 агентов-проходов"]
osp["code-openspec<br/>заводит и проверяет openspec/"]
end
canon["canon<br/>форма раскладки всего проекта"]
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:* — внешний плагин:<br/>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: оно нужно почти каждому скиллу, и
ни один им не владеет. Там же, в shared/, живут язык проектных текстов,
словарь сопровождения и перечень осей процесса
axes.md — какие закрытые словари правят ходом работы,
где дом каждого и чего он не решает. Скиллы читают эти дома по ссылке, а
дословной копией оттуда уезжает лишь то, что обязано лежать внутри промпта.
Канон раскладки проекта
Все проекты приводятся к одной раскладке — так проще ориентироваться, когда
проектов много, и рядом OpenSpec тоже держит строгую структуру. CLAUDE.md плюс
docs/ (паспорт, архитектура, база, безопасность, конвенции, разведка, ADR,
ревью, задачи) и openspec/ — раскладка целиком, роли документов и правило
единственного дома живут одним домом:
canon.md. Здесь она не
пересказывается: копия перечня путей уже расходилась с домом, и как раз в
обязательных — в ней не хватало путей, чьё отсутствие docs.py check считает
нарушением.
Документы канона делятся на три категории, и разрез проверяемый: можно ли по
документу сказать «в этом изменении сделано не так». Тема — да, прямо
(conventions, security, architecture и любой свой документ проекта; список
тем открытый — завёл документ, завёл направление проверки). Источник темы —
нет, но он задаёт границу для чужой темы (passport, database, CLAUDE.md,
openspec/specs/). Процессный документ — нет, он про то, как мы работаем
(tasks/, review.*, adr.*, research.*); ревью изменения по нему не судит.
Форма дома — файл или каталог, на выбор проекта.
Отдельного файла-брифа при этом нет — проходы читают документы напрямую; карта «тема → её дом → что оттуда берётся» — project-facts.md.
Прийти в старый проект и перевести его на канон — /av-dev:canon.
Раскладка версионируется, и проекты повышаются по журналу
версий.
Версия одна, и живёт она в .av-dev.toml в корне репозитория — вместе с
настройками: [docs] migrations и секция [tasks], которая говорит, где лежит
каталог задач и как названы его части. Версий было две, пока плагинов было три и проект мог
взять учёт работ без канона документов; теперь плагин один, и второе число
означало бы только вопрос, по какому журналу повышать. Прежние
docs/.docs.json и <каталог задач>/.tasks.json не читаются: увидев их,
docs.py check называет прежнюю раскладку и зовёт upgrade — запись 1
журнала. Формат TOML взят ради комментариев: файл лежит в репозитории проекта,
и назначение числа читают из него самого, а скрипты правят строку, а не
переписывают файл. Имя служебного файла по-прежнему называет владельца —
.av-dev.toml, openspec/config.yaml.
Подключение
Плагин подключается на уровне проекта, чтобы был активен у всех, кто открывает репозиторий. Из терминала, в каталоге проекта:
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 проекта то, что можно внести и руками:
{
"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. Один шаг без второго выглядит как «обновил, а ничего не
изменилось» — так и было в первый раз.
# 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 стоял в шести проектах, один вызов поднял версию ровно в одном, и не
в том, из которого звали. Проекты обновляются поштучно.
Что стоит и какой версии — одной командой:
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 — работающая сессия держит скиллы в контексте и про новый снимок не знает.
Снятие
Действие, обратное подключению.
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'ы сабагентов
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 журнала; name, разошедшееся с именем каталога скилла или файла charter'а. Вызов разрешается по каталогу, а сообщение о промахе говорит «нет такого скилла», а не «имя не то»;- цвет charter'а, не отвечающий его модели. Цвет кодирует модель, а не роль прохода — раскладка живёт в 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:
<!-- дом: <id> --> …текст… <!-- /дом: <id> -->
<!-- копия: <id> из <путь к дому> --> …тот же текст… <!-- /копия: <id> -->
Идентификатор — буквы, цифры и дефис, и он повторяется в закрывающем маркере.
Строгость нужна ровно затем, чтобы этот абзац сам не объявил дом: <id> под
шаблон не подходит.
Сверяется текст между маркерами; ограда блока кода и пустые строки по краям в сверку не входят. Маркер, уехавший в проект вместе со скелетом, там полезен: он говорит, что у текста есть дом и правится он там.
Дом правила, общего нескольким скиллам, лежит в 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 —
конфиг в 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 …. Он законен ровно для случая,
когда найденное нечем чинить прямо сейчас; молча пропущенная проверка — нет.