- `decisions.py` судит четыре вещи: уникальность номеров Т/Р/С, раскладку тем, указатель и ссылки — цель существует, подпись называет именно её; - проверка идёт без glob, как адреса: файл темы и ссылки на него лежат порознь, и переименование темы трогает только одну сторону, а ломает обе; - ссылка внутри блока кода ссылкой не считается — в скелетах канона она адресована дереву проекта.
578 lines
43 KiB
Markdown
578 lines
43 KiB
Markdown
# av-dev-skills
|
||
|
||
Личный маркетплейс плагинов для разработки. Процесс имени не имеет — он и есть
|
||
`av-dev`.
|
||
|
||
Что решено и почему — [журнал решений](decisions/README.md). Что осталось сделать —
|
||
[TODO.md](TODO.md) и [REMAINING.md](REMAINING.md). Как процесс дошёл до текущей
|
||
формы — [HISTORY.md](HISTORY.md).
|
||
|
||
## Плагины
|
||
|
||
Плагина два: **av-dev** — весь процесс, и **av-dev-git** — сообщения коммитов.
|
||
Второй работает в любом репозитории, в том числе не приведённом к канону. До 13 августа 2026 процесс жил тремя плагинами
|
||
(`av-dev-docs`, `av-dev-tasks`, `av-dev-code`); раскол делался под раздельную
|
||
установку, она не понадобилась ни разу, и плагины слились —
|
||
[тема 64](decisions/64-three-plugins-merged.md) журнала решений.
|
||
|
||
Имя **скилла** несёт префикс прежнего плагина: `doc-`, `task-`, `code-`. Вызов
|
||
выходит вида `/av-dev:<скилл>`.
|
||
|
||
### av-dev — документы, учёт, работа
|
||
|
||
**Документы проекта.** Владеют `docs/` и `CLAUDE.md`.
|
||
|
||
- `doc-init` — новый проект: интервью по свободному описанию замысла →
|
||
первичная документация;
|
||
- `doc-canon` — привести проект к канону документов: `check` / `adopt` /
|
||
`upgrade`, плюс скрипт `docs.py`. Он же ведёт журнал версий раскладки —
|
||
общий, и на документы, и на каталог задач;
|
||
- `doc-healthcheck` — здоровье документации **судом, а не машиной**: не
|
||
разошлись ли документы между собой и с кодом. Зовёт двух агентов на весь канон
|
||
разом — `doc-consistency` (документы между собой и с openspec) и
|
||
`doc-code-drift` (факты против кода) — и разбирает урожай порциями. Дорого,
|
||
поэтому не на каждой задаче. Язык документов вычитывает отдельный агент
|
||
`doc-wording`, и зовут его не отсюда, а те, кто только что писал текст:
|
||
`doc-sync`, `doc-init` и `doc-canon`;
|
||
- `doc-sync` — содержимое канона по ходу разработки: ADR из архивного
|
||
`design.md`, промоут конвенций, запись в разведку и журнал ревью, чистка
|
||
архитектуры.
|
||
|
||
**Учёт работ.** Владеет каталогом задач.
|
||
|
||
- `task-track` — задачи и цели каталогом markdown-файлов, у каждой записи тип
|
||
(`goal`, `feature`, `fix`, `chore`, `research`), и тип задаёт её схему;
|
||
вычитывают их два отдельных прохода: `task-form` (форма записи) и
|
||
`task-wording` (язык записей);
|
||
- `task-groom` — груминг беклога: что сейчас самое важное и что перестало быть
|
||
важным. Ответ записывается **порядком строк** — приоритет это свойство
|
||
очереди, и живёт он в индексе. Интерактивный: разбирает вопросы пачкой,
|
||
переоценивает порциями по 5–8, расставляет верх очереди с доводом на каждое
|
||
движение.
|
||
|
||
**Работа по задачам.** Владеет `openspec/`. **Сценарий решения требует OpenSpec
|
||
и заводит его сам** — разведке и обслуживанию он не нужен.
|
||
|
||
- `code-openspec` — завести, настроить и **проверить** `openspec/` в проекте:
|
||
`openspec init`, замена примера в `config.yaml` настройкой канонической формы,
|
||
скрипт `openspec.py` (форма файла + сверка слепка с живой версией
|
||
инструмента). Каталог принадлежит конвейеру, а не канону: без конвейера он
|
||
проекту не нужен, и `docs.py` о нём молчит;
|
||
- `code-resolve` — одна задача от постановки до закрытия. **Точка входа одна, а
|
||
сценария три, и выбирает сценарий сам скилл, прочитав постановку:**
|
||
классифицировать задачу до вызова человек всё равно не может — «есть ли
|
||
очевидный способ решения» и «меняется ли спека» видно после чтения записи.
|
||
**Решение** идёт циклом SDD с чекпоинтом после ревью дизайна: объяснение
|
||
человеческим языком, повод скорректировать ход.
|
||
**Обслуживание** (тип `chore`: тулчейн и сборка, зависимости, гит-хуки,
|
||
перенос, чистка) change не заводит и планового стопа не имеет вовсе:
|
||
дельта-спек у него нет **по построению**, то есть цикл SDD здесь не урезан, а
|
||
остаётся без входа. Ревью идёт фиксированным планом без метки и без
|
||
разметчика — `autotests` и `operations`, плюс `conventions` с техническим
|
||
разбором, если дифф трогает код; главный шаг сценария — синк документации,
|
||
потому что обслуживание чаще прочих двигает как раз те факты, которые
|
||
сверяются с кодом. Правка гейта сверяется по составу проверок, а не по цвету.
|
||
Нашлась дельта-спека — задача **оказалась шире своего типа**: работа
|
||
останавливается, тип называется (`fix` или `feature`), человек получает
|
||
объяснение простым языком и два решения — переформулировать запись и решать её
|
||
процессом того типа следующим прогоном либо прекратить; «доделать как
|
||
обслуживание» решением не является.
|
||
**Разведка** (тип `research`, сырая идея, мутная постановка) кода не пишет
|
||
вовсе: её чекпоинт — варианты, 2–4 способа решить с ценой каждого, — стоит до
|
||
первого написанного требования, а исход уезжает в документы канона и в задачи.
|
||
Обе пачки — документы и записи — **вычитываются перед коммитом** своими
|
||
проходами: `doc-wording` по документам, `task-form` и `task-wording` по
|
||
записям. Выбранный способ реализуется **следующим прогоном**, и запускает его
|
||
человек: смена сценария по ходу — событие с названным исходом, а не тихий
|
||
поворот. Все три сценария лежат справочниками и одинаково —
|
||
`references/solve.md`, `references/maintain.md` и `references/research.md`; в
|
||
самом скилле только вход, развилка и правила, не зависящие от сценария;
|
||
- `code-review` — конвейер ревью **по темам**: документ проекта либо заводит
|
||
тему проверки, либо питает чужую тему источником, либо процессный и в ревью не
|
||
читается вовсе. Разметка идёт **один раз на задачу**, сразу после `propose`:
|
||
агент `review-scope` меряет изменение по двум осям — размер и сложность — и
|
||
берёт метку как максимум по ним. Одна метка правит **обе** стадии ревью:
|
||
дизайна (`small` — только сверка спек; `medium` — плюс рубрика; `large` — плюс
|
||
архитектурный проход) и кода (`small` — гейт, спеки, код, триаж; `medium` —
|
||
плюс приёмник тем; `large` — плюс доказательство: запуск, замер, построенный
|
||
путь, 5–10% задач). Десять агентов-проходов.
|
||
|
||
### av-dev-git
|
||
|
||
`commit` — сообщения в личном стиле. Отдельным плагином потому, что нужен и в
|
||
репозитории, который к канону не приведён и никогда не будет.
|
||
|
||
Кто кого зовёт. Сплошная стрелка — вызов скилла через пространство имён, не
|
||
импорт; пунктир — совет позвать, а не вызов. Агенты-проходы в графе не показаны:
|
||
их зовут скиллы, названные выше.
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph avdev["av-dev — один плагин, девять скиллов"]
|
||
subgraph pipe["работа по задачам; сценарий решения требует OpenSpec"]
|
||
direction LR
|
||
tp["code-resolve<br/>3 сценария: разведка,<br/>решение, обслуживание"] --> rp["code-review<br/>10 агентов-проходов"]
|
||
osp["code-openspec<br/>заводит и проверяет openspec/"]
|
||
end
|
||
subgraph docsp["документы, владеют docs/"]
|
||
direction LR
|
||
init["doc-init"]
|
||
canon["doc-canon"]
|
||
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](av-dev/shared/absence.md): оно нужно почти каждому скиллу, и
|
||
ни один им не владеет. Там же, в `shared/`, живут язык проектных текстов,
|
||
словарь сопровождения и **перечень осей процесса**
|
||
[axes.md](av-dev/shared/axes.md) — какие закрытые словари правят ходом работы,
|
||
где дом каждого и чего он **не** решает. Скиллы читают эти дома по ссылке, а
|
||
дословной копией оттуда уезжает лишь то, что обязано лежать внутри промпта.
|
||
|
||
## Канон документов проекта
|
||
|
||
Все проекты приводятся к одной раскладке — так проще ориентироваться, когда
|
||
проектов много, и рядом OpenSpec тоже держит строгую структуру. `CLAUDE.md` плюс
|
||
`docs/` (паспорт, архитектура, база, безопасность, конвенции, разведка, ADR,
|
||
ревью, задачи) и `openspec/` — **раскладка целиком, роли документов и правило
|
||
единственного дома живут одним домом**:
|
||
[canon.md](av-dev/skills/doc-canon/references/canon.md). Здесь она не
|
||
пересказывается: копия перечня путей уже расходилась с домом, и как раз в
|
||
обязательных — в ней не хватало путей, чьё отсутствие `docs.py check` считает
|
||
нарушением.
|
||
|
||
**Документы канона делятся на три категории, и разрез проверяемый: можно ли по
|
||
документу сказать «в этом изменении сделано не так».** **Тема** — да, прямо
|
||
(`conventions`, `security`, `architecture` и любой свой документ проекта; список
|
||
тем открытый — завёл документ, завёл направление проверки). **Источник темы** —
|
||
нет, но он задаёт границу для чужой темы (`passport`, `database`, `CLAUDE.md`,
|
||
`openspec/specs/`). **Процессный документ** — нет, он про то, как мы работаем
|
||
(`tasks/`, `review.*`, `adr.*`, `research.*`); ревью изменения по нему не судит.
|
||
Форма дома — файл или каталог, на выбор проекта.
|
||
|
||
Отдельного файла-брифа при этом нет — проходы читают документы напрямую; карта
|
||
«тема → её дом → что оттуда берётся» —
|
||
[project-facts.md](av-dev/skills/code-review/references/project-facts.md).
|
||
|
||
Прийти в старый проект и перевести его на канон — `/av-dev:doc-canon`.
|
||
Раскладка версионируется, и проекты повышаются по [журналу
|
||
версий](av-dev/skills/doc-canon/references/changelog.md).
|
||
|
||
**Версия одна, и живёт она в `.av-dev.toml` в корне репозитория** — вместе с
|
||
настройками: `[docs] migrations` и секция `[tasks]`, которая говорит, где лежит
|
||
каталог задач и как названы его части. Версий было две, пока плагинов было три и проект мог
|
||
взять учёт работ без канона документов; теперь плагин один, и второе число
|
||
означало бы только вопрос, по какому журналу повышать. Прежние
|
||
`docs/.docs.json` и `<каталог задач>/.tasks.json` не читаются: увидев их,
|
||
`docs.py check` называет прежнюю раскладку и зовёт `upgrade` — запись 1
|
||
журнала. Формат TOML взят ради комментариев: файл лежит в репозитории проекта,
|
||
и назначение числа читают из него самого, а скрипты правят строку, а не
|
||
переписывают файл. Имя служебного файла по-прежнему называет владельца —
|
||
`.av-dev.toml`, `openspec/config.yaml`.
|
||
|
||
## Подключение
|
||
|
||
Плагин подключается **на уровне проекта**, чтобы был активен у всех, кто
|
||
открывает репозиторий. Из терминала, в каталоге проекта:
|
||
|
||
```bash
|
||
cd /path/to/project
|
||
|
||
# маркетплейс: один раз на проект. --scope project кладёт его
|
||
# в extraKnownMarketplaces этого репозитория (см. ниже), без флага — в user
|
||
claude plugin marketplace add https://git.vakhrushev.me/av/dev-skills.git --scope project
|
||
|
||
# плагины: scope обязателен, умолчание у команды — user, а нам нужен project
|
||
claude plugin install av-dev@av-dev-skills --scope project
|
||
claude plugin install av-dev-git@av-dev-skills --scope project
|
||
```
|
||
|
||
Те же команды изнутри Claude Code — со слешем: `/plugin marketplace add …`,
|
||
`/plugin install … --scope project`. Обе формы дописывают в
|
||
`.claude/settings.json` проекта то, что можно внести и руками:
|
||
|
||
```json
|
||
{
|
||
"extraKnownMarketplaces": {
|
||
"av-dev-skills": {
|
||
"source": { "source": "git", "url": "https://git.vakhrushev.me/av/dev-skills.git" }
|
||
}
|
||
},
|
||
"enabledPlugins": {
|
||
"av-dev@av-dev-skills": true,
|
||
"av-dev-git@av-dev-skills": true
|
||
}
|
||
}
|
||
```
|
||
|
||
**При установке в проект, где лежали проектные копии** скиллов и агентов
|
||
(`.claude/skills/` — голые `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@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'ы сабагентов
|
||
av-dev/shared/ дома правил и общий читатель .av-dev.toml
|
||
scripts/ проверки репозитория и пересборка копий
|
||
pyproject.toml линтеры скриптов, только для этого репозитория
|
||
lefthook.yml гейт коммита: проверки документов
|
||
```
|
||
|
||
## Проверка скриптов
|
||
|
||
`tasks.py` и `docs.py` запускаются **где угодно голым `python3` 3.12 без
|
||
установки чего-либо** — они лежат рядом со скиллами и работают в любом проекте.
|
||
`pyproject.toml` в корне не меняет этого: он живёт только здесь и держит
|
||
линтеры, а не зависимости скриптов.
|
||
|
||
```
|
||
uv sync # ставит ruff и pyrefly в .venv, версии прибиты точно
|
||
uv run ruff check . # правила; --fix для безопасных починок
|
||
uv run pyrefly check # типы
|
||
```
|
||
|
||
Ноль внешних зависимостей охраняется двумя способами: `banned-api` у ruff ловит
|
||
частые соблазны по имени, а pyrefly видит окружение, где нет ничего кроме
|
||
линтеров, и любой сторонний импорт у него не разрешается. Список запретов —
|
||
не перечень мира, настоящий страж второй.
|
||
|
||
## Проверка фронтматтеров и описаний плагинов
|
||
|
||
Фронтматтер читает не человек, а загрузчик: по `name` он разрешает вызов, по
|
||
`description` решает, звать ли скилл вообще. **Ошибка здесь не выглядит
|
||
ошибкой** — тем же способом, что и в диаграммах.
|
||
|
||
```
|
||
python3 scripts/frontmatter.py # 0 в порядке, 1 расхождение, 3 не тот каталог
|
||
```
|
||
|
||
Ловится четыре класса:
|
||
|
||
- **двоеточие с пробелом в описании без кавычек.** Для YAML `: ` внутри
|
||
простого скаляра начинает вложенное отображение, так что «конвейер ревью: гейт,
|
||
сверка…» — это не текст с двоеточием, а синтаксическая ошибка. Так было
|
||
написано часть описаний плагинов, и читались они правильно — замер и разбор
|
||
в [решении Р61](decisions/17-notes-tier-work-kind-roadmap.md) журнала;
|
||
- **`name`, разошедшееся с именем каталога скилла или файла charter'а.** Вызов
|
||
разрешается по каталогу, а сообщение о промахе говорит «нет такого скилла»,
|
||
а не «имя не то»;
|
||
- **цвет charter'а, не отвечающий его модели.** Цвет кодирует модель, а не роль
|
||
прохода — раскладка живёт в
|
||
[code-review/SKILL.md](av-dev/skills/code-review/SKILL.md),
|
||
разделе «Модель по проходу», здесь только её механизация. Держаться вниманием
|
||
правило не может: цвет ставится один раз при заведении charter'а, а модель
|
||
потом меняется калибровкой;
|
||
- **описание плагина, разошедшееся между манифестами.** У описания два дома:
|
||
`<плагин>/.claude-plugin/plugin.json` показывает его установленному плагину,
|
||
корневой `.claude-plugin/marketplace.json` — тому, кто выбирает, ставить ли.
|
||
Правят обычно один, и разойтись они успели уже трижды из четырёх. `copies.py`
|
||
этот класс не берёт: он смотрит markdown, а манифест — json. Отсюда и `*.json`
|
||
в глобе задачи гейта.
|
||
|
||
## Проверка копий правил
|
||
|
||
«Один факт — один дом» держалось вниманием и трижды не удержалось. Копии всё же
|
||
нужны: скелеты канона уезжают в репозиторий проекта и обязаны там что-то
|
||
говорить. Значит копия допустима, но **дословная и помеченная**:
|
||
|
||
```
|
||
python3 scripts/copies.py # 0 сошлось, 1 расхождение, 2 разметка, 3 не тот каталог
|
||
```
|
||
|
||
Разметка — HTML-комментарии, невидимые в отрендеренном markdown:
|
||
|
||
```
|
||
<!-- дом: <id> --> …текст… <!-- /дом: <id> -->
|
||
<!-- копия: <id> из <путь к дому> --> …тот же текст… <!-- /копия: <id> -->
|
||
```
|
||
|
||
Идентификатор — буквы, цифры и дефис, и он повторяется в закрывающем маркере.
|
||
Строгость нужна ровно затем, чтобы этот абзац сам не объявил дом: `<id>` под
|
||
шаблон не подходит.
|
||
|
||
Сверяется текст между маркерами; ограда блока кода и пустые строки по краям в
|
||
сверку не входят. Маркер, уехавший в проект вместе со скелетом, там полезен: он
|
||
говорит, что у текста есть дом и правится он там.
|
||
|
||
**Дом правила, общего нескольким скиллам, лежит в `av-dev/shared/` и ни одному
|
||
из них не принадлежит.** Так живут язык проектных текстов, словарь
|
||
сопровождения и правило об отсутствующих частях раскладки: каждое нужно
|
||
многим, и хранить его внутри одного скилла значило бы отдать общее правило во
|
||
владение части.
|
||
|
||
**Копия при этом делается не всегда.** Пока плагинов было три, копия была
|
||
единственным способом: путь в дерево соседа не разрешался. Внутри одного дерева
|
||
справочник читается **по ссылке**, и дословная копия остаётся ровно там, где
|
||
текст обязан лежать **внутри самого промпта**: в уставе агента, где правило и
|
||
есть критерий суждения; в `SKILL.md`, который целиком и есть промпт скилла, — за
|
||
ссылкой скилл пошёл бы отдельным чтением, а правило нужно ему в тот момент,
|
||
когда он решает; и в скелетах, уезжающих в репозиторий проекта.
|
||
|
||
**Дом ставится в `shared/` только тогда, когда владельца нет.** У адресов
|
||
владелец есть: раскладку `docs/` держит `doc-canon`, каталог задач —
|
||
`task-track`, и переносить их наружу значило бы отобрать у владельца его же
|
||
предмет. Общее без владельца живёт в `shared/`; чужое с владельцем остаётся
|
||
дома, а потребитель на него ссылается.
|
||
|
||
Скрипт ловит четыре вещи: копия разошлась с домом (с диффом), копия указывает не
|
||
на тот файл, дом остался без копий, разметка сломана. Чего он **не** ловит —
|
||
копию, которую забыли пометить: помечать — по-прежнему решение человека.
|
||
|
||
### Пересборка — `scripts/resync.py`
|
||
|
||
```
|
||
python3 scripts/resync.py # переписать тела всех разошедшихся копий из домов
|
||
# 0 готово, 2 разметка сломана, 3 не тот каталог
|
||
```
|
||
|
||
Правка дома касается стольких файлов, сколько у него копий, и последний из них
|
||
забывают — это и есть причина, по которой копии расходятся. Пересборка делает то
|
||
же машиной и потому дословна по построению.
|
||
|
||
**В гейт коммита скрипт не ставится, и это решение.** Автоматическая пересборка
|
||
протащила бы правку дома во все копии мимо глаз автора, а правка дома, чья копия
|
||
уезжает в репозиторий проекта, обязана ещё и попасть в журнал версий канона —
|
||
этого машина не напишет. Гейт поэтому только **называет** расхождение; согласие с
|
||
ним остаётся действием человека.
|
||
|
||
Разметку разбирает не он сам: `copies.py` импортируется целиком. Второй
|
||
разборщик той же разметки разошёлся бы с первым молча — ровно тот класс дефекта,
|
||
против которого механика копий и заведена.
|
||
|
||
**Ограда блока кода принадлежит месту, а не дому.** Одно и то же тело живёт в
|
||
доме внутри ```` ``` ````, а в скелете канона — внутри чужой, объемлющей ограды,
|
||
и своей там иметь не должно. Пересборка берёт тело дома без крайних оград и
|
||
надевает обратно ту, что была у копии; пустые строки по краям — так же.
|
||
|
||
## Проверка адресов документов
|
||
|
||
Адрес документа принадлежит одному плагину, а называют его все: `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 показывает разумную строку, а рендер падает.
|
||
|
||
```
|
||
python3 scripts/diagrams.py # весь репозиторий
|
||
python3 scripts/diagrams.py A.md B.md # только названные файлы
|
||
# 0 рендерятся, 1 нет, 3 нет mermaid-cli
|
||
```
|
||
|
||
Рендерит `mmdc` с PATH или `npx --yes @mermaid-js/mermaid-cli`; ни того ни
|
||
другого нет — код 3, а не молчаливый успех. Это самая дорогая проверка
|
||
репозитория: каждый блок — отдельный запуск mermaid-cli со своим chromium,
|
||
секунда с лишним. Поэтому у неё два рычага, и оба нужны гейту коммита: **блоки
|
||
собираются все сразу, а рендерятся параллельно** (пул потоков, порядок вывода
|
||
берётся из порядка сбора), и **проверять можно названные файлы, а не весь
|
||
репозиторий**. Весь репозиторий — три секунды вместо пятнадцати, один
|
||
файл — одна.
|
||
|
||
Чего проверка **не** ловит — расхождение диаграммы с прозой вокруг неё. Дословного
|
||
соответствия между текстом и графом нет, сличать нечего, и держится это
|
||
правилом старшинства, записанным рядом с каждой диаграммой: **в конвейере ревью
|
||
старший граф** (он и есть алгоритм планировщика, проза его объясняет), **в
|
||
остальных местах старшая проза** (диаграмма там сводка).
|
||
|
||
## Гейт коммита
|
||
|
||
Все семь проверок стоят в `pre-commit` через [lefthook](https://lefthook.dev) —
|
||
конфиг в [lefthook.yml](lefthook.yml), ставится один раз на клон:
|
||
|
||
```
|
||
lefthook install # пишет .git/hooks/pre-commit
|
||
lefthook run pre-commit # прогнать руками, не коммитя
|
||
```
|
||
|
||
| Проверка | Когда идёт | Что смотрит | Сколько |
|
||
| --- | --- | --- | --- |
|
||
| фронтматтеры | правка `*.md` или `*.json` | весь репозиторий | миллисекунды |
|
||
| копии правил | правка `*.md` | весь репозиторий | миллисекунды |
|
||
| адреса документов | **каждый коммит** | весь репозиторий | ~0.07 с |
|
||
| журнал решений | **каждый коммит** | весь репозиторий | ~0.09 с |
|
||
| диаграммы | правка `*.md` | staged-файлы | ~1 с на файл |
|
||
| `ruff check --fix` | правка `*.py` | staged-файлы | доли секунды |
|
||
| `pyrefly check` | правка `*.py` | staged-файлы | доли секунды |
|
||
|
||
Glob разводит две половины: коммит, трогающий одни скрипты, не платит за рендер
|
||
диаграмм, а коммит в документы не гоняет линтеры.
|
||
|
||
**Судятся staged-файлы, а не рабочее дерево** — гейт обязан проверять то, что
|
||
уедет в историю, а не то, что случайно лежит рядом на диске. Исключений три, и
|
||
все про существо, а не про удобство: `copies.py` сверяет копию с домом, а
|
||
дом лежит в другом файле, которого в индексе может не быть (список staged дал бы
|
||
«копии дословны» ровно там, где правка дома их и разошлась); `frontmatter.py`
|
||
обходит весь репозиторий за сотые доли секунды — экономить тут нечего;
|
||
`addresses.py` идёт **без glob вовсе**, потому что сводит две стороны: перечень
|
||
адресов лежит в `*.py` владельца, а упоминания — в `*.md` соседей, и коммит с
|
||
переименованием документа трогает только первую; `decisions.py` — по той же
|
||
причине: файл темы и ссылки на него лежат порознь, и переименование темы трогает
|
||
только одну сторону.
|
||
|
||
**Что судит `decisions.py`.** Номер записи журнала обязан быть один: буквенные
|
||
метки решений выродились до пятибуквенных и разъехались молча — `АЕАКЛ` означала
|
||
и тему 53, и тему 65. И подпись ссылки обязана называть свою цель: в
|
||
`[тема 5](05-project-start-lifecycle.md)` номер записан дважды, словом и путём, —
|
||
дубль неизбежный (иначе ссылку не прочитать, не открыв) и потому сверяемый, как
|
||
всякая копия.
|
||
|
||
**`ruff` чинит безопасное сам, и починка доносится до этого же коммита**
|
||
(`stage_fixed: true`). Иначе исправленный файл остался бы в рабочем дереве, а в
|
||
историю уехал бы невычищенный — худший из исходов: гейт зелёный, коммит грязный.
|
||
|
||
**`resync.py` в гейте нет намеренно** — он чинит, а не проверяет, и его правка
|
||
обязана быть прочитана глазами (см. выше).
|
||
|
||
**Обход разовый — `LEFTHOOK=0 git commit …`.** Он законен ровно для случая,
|
||
когда найденное нечем чинить прямо сейчас; молча пропущенная проверка — нет.
|