av 7ab759ae4a старший долг: развилка тремя основаниями, вопросы тем, версия раскладки 5
Третий заход по находкам ревью — то, что старше темы 78 и тянулось с тем 74–77.

Оснований у развилки три во всех местах: конвейер называл два, а устав триажа,
контракт находок, сценарий решения и журнал — три. Там же сказано, чем третье
отличается: по первым двум оркестратор урезает изменение до остатка, третье
отменяет одобрение и возвращает на чекпоинт.

Вопросы проекта по темам достались проходам, которые эти темы закрывают:
review-code, review-specs и review-autotests получили обязанность отвечать
дословно и строку в блоке покрытия. Прежде конвейер обещал их каждому проходу,
а знал о них только приёмник тем.

Глубокое ревью приведено к уставам, которые зовёт: глубина у проходов разная —
доказательство у тех двоих, что держат машину, разбор у architecture и code;
у триажа три вызывающих, а не два режима, и потолка в 7 пунктов там нет.

Версия раскладки поднята до 5 с записью журнала: скелет docs/review.md потерял
подраздел «Триггеры метки» ещё темой 77, а миграции проектам никто не дал.
Сняты остатки меток в task-track и в config-skeleton, уезжающем в чужой проект.
Перечень осей досчитал три оси: глубина темы, разметка действия, род правки.

Журнал — тема 81.
2026-08-23 19:51:47 +03:00

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 здесь не урезан, а остаётся без входа. Ревью идёт фиксированным планом без change — 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 — конвейер ревью по темам: документ проекта либо заводит тему проверки, либо питает чужую тему источником, либо процессный и в ревью не читается вовсе. Состав постоянный, метки у прогона нет: гейт, сверка со спекой, разбор кода, триаж; приёмник тем идёт, когда у проекта есть свои темы. Цикл задачи проверяет корректность и механику против записанного критерия — дельта-спеки, конвенции, инварианты CLAUDE.md, вывод инструментов; темы security, operations и architecture закрыты в нём сверкой с записанными инвариантами, и только. Находки по умолчанию чинятся инлайн и молча, человеку уходит необратимое и то, что меняет дельта-спеки, а задачи из урожая заводятся по его слову. Каждый проход — свой агент, перечень держит сам скилл;
  • code-deep-reviewглубокое ревью области, а не задачи: модуля, слоя, сервиса целиком. Здесь живут тяжёлые проходы, которых в цикле задачи нет, — review-adversary строит путь и прогоняет падающий тест, review-ops снимает числа замером, architecture судит форму решения на широком входе; рядом идёт code по коду целиком. Исход — не правки, а разговор: находки разбираются с человеком по одной, и согласованное уезжает задачами через task-track. Дорого — не на задаче и не по расписанию; вход копит сам цикл строками «отложено» в границах покрытия.

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/>агенты-проходы"]
            osp["code-openspec<br/>заводит и проверяет openspec/"]
            deep["code-deep-review<br/>область, а не задача:<br/>тяжёлые проходы"]
        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 -.->|"строки «отложено»"| deep
    deep --> 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 …. Он законен ровно для случая, когда найденное нечем чинить прямо сейчас; молча пропущенная проверка — нет.

S
Description
No description provided
Readme
3.5 MiB
Languages
Python 100%