av-dev-pipeline стал av-dev-code, review-pipeline — review

Имя описывало устройство, а не предмет: «пайплайн» говорит, что внутри
конвейер, — а плагин занят кодом по задачам, и с появлением чекпоинтов
он уже не конвейер в чистом виде. Набор имён стал параллельным:
docs / tasks / code / git, каждое называет материал.

Заодно review-pipeline стал review — слово ушло из плагина целиком, а
не наполовину; скиллы выровнялись: resolve / review / openspec.

Журнал версий канона переписан вместе со всеми, DECISIONS.md — нет.
Разрез по типу высказывания, а не файла: наблюдение и причина
неприкосновенны, предписание и адрес обязаны оставаться исполнимыми.
Запись версии 10 велит «проверить, что плагин av-dev-pipeline
установлен» — проект, дошедший до неё, выполнил бы невыполнимое.
This commit is contained in:
av
2026-08-09 15:43:03 +03:00
parent c5e6883461
commit 53cf6baedf
40 changed files with 145 additions and 109 deletions
+161
View File
@@ -0,0 +1,161 @@
---
name: openspec
description: "Завести и настроить OpenSpec в проекте — openspec init --tools claude, замена закомментированного примера в openspec/config.yaml на настройку канонической формы (язык, правила именования capability, придирки валидатора, адреса паспорта и CLAUDE.md), проверка формы своим скриптом openspec.py (имя файла, схема, незаменённый пример, адреса документов, ключи rules против артефактов схемы) и сверка слепка с живой версией инструмента. Использовать, когда в проекте нет каталога openspec/, когда config.yaml остался примером из коробки, когда заводят новый проект или переводят чужой и дошли до шага OpenSpec, а также когда конвейер отказался работать без источника требований. Каталог openspec нужен именно конвейеру: без него не работают ни opsx:propose, ни ревью дизайна, ни сверка требований."
---
# OpenSpec в проекте
Каталог `openspec/`**предпосылка конвейера**, а не канона документов. Без него
не работают ни `opsx:propose`, ни ревью дизайна, ни `review-specs`: у требований
не остаётся дома. Поэтому заводит и настраивает его этот плагин — тот, кто по
OpenSpec и работает.
Канон документов о файле не высказывается вовсе: `docs.py` его не открывает и об
его отсутствии молчит. Проект без конвейера живёт без OpenSpec законно, и
проверять там нечего. За каноном остаётся одно — **единственный дом**: не
пересказан ли в `context` документ, у которого есть свой файл. Это суждение, а не
форма, и смотрит его агент.
## Два шага, и второй важнее первого
**1. Завести.**
```
openspec init --tools claude
```
Команда кладёт ещё `.claude/skills/openspec-*` и `.claude/commands/opsx/*` — это
её нормальная работа, не трогай их.
**2. Заменить пример.** `openspec init` кладёт `config.yaml`, где `context` и
`rules` — закомментированный пример на английском. **Файл из коробки хуже
отсутствующего:** он есть, он валиден, имя правильное, — и читается как
настроенный, работая как пустой. Узнаётся это по уже написанному предложению: на
другом языке, с capability по имени пакета, без единого `SHALL`.
Пример **заменяется целиком** по образцу:
[references/config-skeleton.md](references/config-skeleton.md).
## Что туда пишут, а что нет
**Это маршрутизатор, а не второй дом фактов.** Внутрь идёт ровно то, что нужно
**в момент порождения артефакта** и чего в этот момент ещё никто не открыл: язык,
правила именования capability, придирки валидатора и **адреса** документов
проекта.
Сюда же — **требования к форме `proposal` и `design`**, на которых стоит чекпоинт
скилла `av-dev-code:resolve`: объяснение человеку собирается из этих двух
артефактов, и требование к ним обязано применяться в момент, когда их пишут, а не
вспоминаться шагом позже. Образец их содержит.
Пересказ паспорта, инвариантов, конвенций и правил ревью сюда **не переносится**.
Место для второго дома здесь самое частое: `context` читается при порождении
каждого артефакта, туда удобно дописать «чтобы агент знал», и так заводятся копии
инвариантов, состава гейта и правил выбора метки. Расходятся они молча, а
замечают это в уже написанном предложении.
Разрез, по которому отличают одно от другого: **утверждение, которое можно
опровергнуть, открыв другой файл проекта, — пересказ; строка, которая говорит,
какой файл открыть, — ссылка.** Машина этот разрез не проверяет; его смотрит
агент `doc-consistency` из плагина канона, когда тот подключён.
Два адреса обязательны — `docs/passport.md` и `CLAUDE.md`: предложение пишется до
того, как кто-либо откроет `docs/`, и без них его пишут, не зная ни границы
домена, ни инвариантов. Отсутствие адреса к **существующему** документу
`openspec.py check` называет отказом; документа нет в проекте — нет и требования.
## Инструмент
```
os="$CLAUDE_PLUGIN_ROOT/skills/openspec/scripts/openspec.py"
python3 $os check --dir <корень> # форма config.yaml в проекте
python3 $os form # слепок формы против живого OpenSpec
```
**Коды выхода — общий словарь скриптов av-dev:** 0 сошлось, 1 дрейф, 2 ошибка
употребления, 3 окружение, 4 внутренний сбой. Ветвись на коде, а не на тексте.
Различать 1 и 3 обязательно: «форма разошлась» — рабочая ситуация, «openspec не
отвечает» — нерабочая.
`check` проверяет пять вещей, и каждая — про молчащий пробел, а не про вкус:
каталог есть; имя именно `config.yaml` (`config.yml` OpenSpec не читает и об этом
не сообщает); `context` и `rules.specs` не остались примером, а правила называют
`SHALL`; `context` называет паспорт и `CLAUDE.md`; ключи под `rules:` — имена
артефактов схемы, а не свободные слова.
**Адреса требуются только к тем документам, которые в проекте есть.** Канон
документов ставится отдельным плагином и может быть не подключён; требовать
ссылку на несуществующий файл значит требовать битую ссылку. Нет
`docs/passport.md` — проверка по нему идёт строкой «не проверялось», и там же
сказано, что без канона конвейер работает вслепую.
### Форма сверяется с живым инструментом
Схема (`spec-driven`) и перечень артефактов (`proposal`, `specs`, `design`,
`tasks`) — **состояние чужого инструмента**, а не наше решение. OpenSpec
переименует артефакт: правила под прежним именем перестанут применяться, конфиг
останется выглядеть написанным, и молчат при этом все три стороны.
Сторож — сравнение версий. `check` каждым прогоном спрашивает `openspec
--version` (десятые доли секунды) и сравнивает `major.minor` с той версией, на
которой форма сверялась; разошлось — **замечание**, не отказ, с именем команды.
Патч-версия в сравнение не берётся намеренно: формы она не меняет, а нагоняй на
каждый багфикс приучает пролистывать весь блок.
Перепроверяет `openspec.py form`: он спрашивает `openspec templates --json`, то
есть перечень артефактов текущей схемы, и печатает, что разошлось с константами.
Дорогой вызов вынесен из `check` сознательно — он стоит втрое дороже опроса
версии, а ответ меняется только вместе с версией. **Чинится расхождение в
плагине, а не в проекте:** константы скрипта, образец
[references/config-skeleton.md](references/config-skeleton.md) и запись в журнал
версий канона.
## Кто зовёт этот скилл
- `av-dev-docs:init` — шагом заведения нового проекта, до первого документа;
- `av-dev-docs:canon` в режиме `adopt` — если на переводимом проекте каталога нет
или `config.yaml` остался примером;
- человек — когда конвейер отказался работать без источника требований.
**Копия.** Дом правила — `shared/plugin-boundary.md` в репозитории плагинов.
Правится дом, а не этот файл.
<!-- копия: граница-плагинов из shared/plugin-boundary.md -->
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
месте.
**Чужой скилл зовётся полным именем**`av-dev-docs:canon`, `av-dev-tasks:tasks`,
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
прочитает его сам.
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json`
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
<!-- /копия: граница-плагинов -->
Здесь это значит: вызов не разрешился — плагина конвейера в проекте нет, и тогда
OpenSpec заводит человек командой выше.
## Чего этот скилл не делает
- **Не пишет спеки и предложения.** Это `opsx:propose` и пайплайн задачи.
- **Не ведёт документы канона** — их дом плагин `av-dev-docs`, и адреса в
`context` только на них ссылаются.
- **Не чинит расхождение формы с версией OpenSpec в проекте.** Оно чинится в
плагине: константы скрипта, образец здесь, запись в журнал версий канона.
- **Не судит, ссылается `context` на документы или пересказывает их.** Машине
этот разрез не виден; его смотрит агент `doc-consistency` из плагина канона.
Плагина нет — эту проверку не делает никто, и так и скажи.
@@ -0,0 +1,92 @@
# Образец `openspec/config.yaml`
Каталог `openspec/` заводится командой — `openspec init --tools claude`, — и она
кладёт `config.yaml` с закомментированным примером внутри. Пример **заменяется
целиком**: нетронутый файл выглядит настроенным, а работает как пустой.
**Это маршрутизатор, а не второй дом фактов.** Сюда пишут ровно то, что нужно
**в момент порождения артефакта** и чего в этот момент ещё никто не открыл:
язык, правила именования capability, придирки валидатора и **адреса** документов
канона. Пересказ паспорта, инвариантов, конвенций и правил ревью сюда не
переносится: расходится он молча, а замечают это в уже написанном предложении.
```yaml
schema: spec-driven
context: |
Language: Russian
Пиши на русском, но:
- Структурные заголовки оставляй на английском:
## ADDED/MODIFIED/REMOVED Requirements, ### Requirement:, #### Scenario:
- Ключевые слова GIVEN/WHEN/THEN и RFC 2119 (SHALL/MUST/SHOULD) — на английском
- Технические термины, пути и код — на английском
Имена capabilities:
- Capability — это ПОВЕДЕНИЕ или домен системы, а не пакет кода (совпадение с
именем пакета допустимо, но не критерий).
- Существительное, понятное без знания кода: ingest, parsing, storage,
read-api. НЕ store/httpapi — это реализация.
- Гранулярность по принципу «требования меняются вместе». Дробить, когда в
одной спеке смешиваются разные заботы. Переименовать дёшево (RENAMED
Requirements) — не дроби преждевременно в маленьком проекте.
RFC 2119 — требование валидатора, не стиль:
- Каждое ### Requirement ОБЯЗАНО содержать литерал SHALL или MUST, иначе
`openspec validate` падает. Поэтому эти слова и WHEN/THEN не русифицируем.
Что это за проект — читай перед предложением, а не отсюда:
- docs/passport.md — цель, её граница (чем проект НЕ является), потребители,
типовые сценарии, референсы;
- CLAUDE.md — инварианты с severity и семантика гейта;
- docs/architecture.md — устройство; docs/security.md — периметр;
docs/adr/ — почему решено так; docs/research/ — что уже измерено.
Пересказа этих документов здесь нет намеренно: второй дом факта расходится с
первым молча, и заметно это становится в предложении, которое уже написано.
Ревью: правило выбора метки и состав проходов здесь не пересказываем — их дом
скилл av-dev-code:review, проектная настройка — docs/review.md.
Конвенции кода: механизированное проверяет гейт, прозой остаётся
docs/conventions/. Ни состав шагов гейта, ни перечень конвенций здесь не
пересказываем: и то и другое растёт по ходу задач.
Развилка или блокер — сперва prior art. Готовые решения смотрим в референсах
паспорта, отвергаем — с названной причиной, и причина идёт в design.md этого
же изменения.
rules:
proposal:
- Capabilities называй по поведению или домену системы, не по пакету кода
- "Why и What Changes — языком домена из docs/passport.md: без SHALL, без имён модулей и функций там, где вещь называется по-русски"
design:
- "Назови рассмотренные варианты и причину отказа от каждого — из них потом пишется ADR"
- "Решение объясняется через то, что человек увидит иначе, а не через устройство кода"
specs:
# Кавычки обязательны: без них YAML обрежет строку на первом '#'.
- "Каждое ### Requirement обязано содержать SHALL или MUST (иначе валидация падает)"
- "Сценарий — ровно #### (четыре решётки); три или список молча теряются"
- "SHALL/MUST должно стоять в ПЕРВОМ абзаце требования: валидатор смотрит только его"
- "Заголовки и WHEN/THEN/GIVEN — на английском, остальной текст на русском"
```
**Четыре правила для `specs` сняты отказами валидатора, а не выведены из
документации** — потому и записаны дословно: без них каждое второе предложение
узнаёт их падением `openspec validate --strict`.
**Правила для `proposal` и `design` держат чекпоинт скилла
`av-dev-code:resolve`.** Там работа останавливается и человеку объясняют, в
чём проблема и как её решают, — а объяснение **собирается из этих двух
артефактов**, а не сочиняется заново: третий пересказ одного и того же разошёлся
бы с обоими. Требование поэтому стоит здесь, в момент порождения артефакта, а не
в скилле, который спохватится позже. `design.md` при этом ещё и **сырьё для
ADR** — отвергнутый вариант с названной причиной и есть половина будущей записи. Блок `context` проект
дополняет своим (стек, разведка, особенности домена), но **адреса паспорта и
`CLAUDE.md` обязательны** — отсутствие адреса к существующему документу
`openspec.py check` называет отказом.
**Ключи под `rules:` — имена артефактов схемы**, а не свободные слова:
`proposal`, `specs`, `design`, `tasks`. Правило под чужим именем не применяется
и об этом не сообщает, поэтому `rules.spec` вместо `rules.specs` даёт конфиг,
выглядящий написанным и не работающий; `openspec.py check` такой ключ называет.
Перечень артефактов задаёт OpenSpec, а не мы, — за его актуальностью следит
`openspec.py form`.
@@ -0,0 +1,375 @@
#!/usr/bin/env python3
"""Форма `openspec/config.yaml`: проверка проекта и сверка слепка с инструментом.
Каталог `openspec/` — предпосылка **конвейера**, а не канона документов: без него
не работают ни `opsx:propose`, ни ревью дизайна, ни сверка требований. Поэтому и
проверка формы живёт здесь, рядом со скиллом, который каталог заводит. Раньше она
жила в `docs.py` плагина канона, и у файла было два владельца: один заводит,
другой проверяет.
Проверяется то, что **молчит при поломке**. Файл из коробки хуже отсутствующего:
`openspec init` кладёт `config.yaml`, где `context` и `rules` — закомментированный
пример на английском. Он есть, он валиден, имя правильное — и читается как
настроенный, работая как пустой. Узнаётся это по уже написанному предложению.
Разбираем текстом, а не YAML-парсером: у скриптов ноль внешних зависимостей, а
PyYAML в стандартной библиотеке нет. Всё проверяемое различимо построчно,
комментарии отброшены, ключи верхнего уровня стоят в первой колонке.
Коды выхода — общий словарь скриптов av-dev:
0 сошлось
1 дрейф: форма разошлась с ожидаемой
2 ошибка употребления: аргументы
3 окружение: не тот каталог, инструмент не отвечает
4 внутренний сбой
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import NoReturn
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
# Команда заведения. Названа поимённо потому, что её печатает отказ, а отказ без
# команды заставляет искать её в другом месте.
OPENSPEC_INIT = "openspec init --tools claude"
# Адреса, которые обязан назвать блок context. Не пересказ документов, а именно
# ссылки: предложение пишется до того, как кто-либо откроет docs/, и без этих
# двух строк его пишут, не зная ни границы домена, ни инвариантов. Список
# короткий намеренно — длинный превращает context во второй дом фактов.
#
# Третий элемент — путь, по которому проверяется, есть ли документ в проекте
# вообще. Канон документов ставится отдельным плагином и может быть не подключён;
# требовать ссылку на файл, которого нет, значит требовать битую ссылку.
OPENSPEC_POINTERS = [
("passport", "docs/passport.md",
"граница домена и «чем НЕ является» останутся непрочитанными"),
("CLAUDE.md", "CLAUDE.md",
"инварианты и семантика гейта останутся непрочитанными"),
]
# --- Слепок чужого инструмента ----------------------------------------------
#
# Схема, перечень артефактов и версия, на которой это проверено, живут в OpenSpec
# и меняются без нашего участия; здесь они записаны, чтобы проверка шла без
# запуска node на каждом прогоне.
#
# Слепок стареет, и потому есть кто это замечает: `check` сравнивает major.minor
# установленного OpenSpec с OPENSPEC_CHECKED и, если они разошлись, говорит
# замечанием «форма не перепроверена». Перепроверяет команда `form` — она
# спрашивает сам инструмент и печатает, что разошлось. Патч-версия сравнением
# намеренно не берётся: форма конфига в ней не меняется, а замечание на каждый
# багфикс приучило бы пролистывать весь блок.
OPENSPEC_CHECKED = "1.5"
OPENSPEC_SCHEMA = "spec-driven"
# Артефакты схемы. Ключ `rules:` адресуется артефакту, и адресованный
# несуществующему **молча не действует** — ровно тот класс, ради которого вся
# проверка и заведена.
OPENSPEC_ARTIFACTS = ("proposal", "specs", "design", "tasks")
@dataclass
class Report:
errors: list[str] = field(default_factory=list)
notes: list[str] = field(default_factory=list)
skipped: list[str] = field(default_factory=list)
def error(self, msg: str) -> None:
self.errors.append(msg)
def note(self, msg: str) -> None:
self.notes.append(msg)
def skip(self, msg: str) -> None:
self.skipped.append(msg)
def fail(code: int, msg: str) -> NoReturn:
print(msg, file=sys.stderr)
sys.exit(code)
def openspec_cli(args: list[str]) -> str | None:
"""Спросить сам инструмент. None — его нет или он не ответил."""
try:
out = subprocess.run(
["openspec", *args], capture_output=True, text=True, timeout=30
)
except (FileNotFoundError, OSError, subprocess.SubprocessError):
return None
return out.stdout.strip() if out.returncode == 0 else None
def rules_keys(live: str) -> list[str]:
"""Имена артефактов, которым адресованы правила, — и только они.
Идём от строки `rules:` до следующего ключа нулевой колонки, а не ищем
отступ по всему файлу: блок `context: |` — литеральный скаляр, внутри него
строки вида «Language: Russian» и «av-dev-code:review» выглядят
ключами и дали бы находку на ровном месте. Проверено на живом конфиге,
который так и падал.
"""
out: list[str] = []
inside = False
for line in live.splitlines():
if not line.strip():
continue
if not line[0].isspace():
inside = line.startswith("rules:")
continue
if not inside:
continue
m = re.fullmatch(r" ([A-Za-z_-]+):\s*", line)
if m:
out.append(m.group(1))
return out
def check_form(root: Path, rep: Report) -> None:
"""Настройка заведена и не осталась примером из коробки."""
os_dir = root / "openspec"
if not os_dir.is_dir():
# Здесь это отказ, а не «неприменимо»: скрипт принадлежит конвейеру, а
# конвейер без OpenSpec не работает вовсе. Тот же вопрос со стороны
# канона документов звучит иначе, и `docs.py` отвечает на него молчанием.
rep.error(
"нет openspec/ — там дом темы requirements (openspec/specs/) и "
f"настройка генерации артефактов; заводится `{OPENSPEC_INIT}`"
)
return
if (os_dir / "config.yml").is_file():
rep.error(
"openspec/config.yml — читается только config.yaml, и этот файл "
"останется незамеченным: настройка будет пустой, а выглядеть будет "
"заполненной"
)
path = os_dir / "config.yaml"
if not path.is_file():
rep.error(
"нет openspec/config.yaml — язык, правила именования capability и "
"придирки валидатора будут заново угадываться на каждом предложении"
)
return
text = path.read_text(encoding="utf-8")
live = "\n".join(
line for line in text.splitlines() if not line.lstrip().startswith("#")
)
keys = set(re.findall(r"(?m)^([A-Za-z_]+):", live))
schema = re.search(r"(?m)^schema:\s*(\S+)", live)
if schema is None:
rep.error(
f"в openspec/config.yaml нет ключа schema — ожидается {OPENSPEC_SCHEMA}"
)
elif schema.group(1) != OPENSPEC_SCHEMA:
rep.error(
f"schema в openspec/config.yaml — {schema.group(1)}, а форма описана "
f"для {OPENSPEC_SCHEMA}"
)
if "context" not in keys:
rep.error(
"в openspec/config.yaml нет ключа context: файл остался примером из "
"коробки — предложение пишется без языка, правил именования "
"capability и адресов документов проекта"
)
else:
for pointer, where, why in OPENSPEC_POINTERS:
if not (root / where).exists():
rep.skip(
f"{where} в проекте нет — ссылка на него в context не "
f"требуется. Документы канона ведёт отдельный плагин "
f"(av-dev-docs), и без него конвейер работает вслепую"
)
continue
if pointer not in live:
rep.error(f"openspec/config.yaml не называет {pointer}{why}")
if "rules" not in keys or "specs:" not in live:
rep.error(
"в openspec/config.yaml нет rules.specs — придирки валидатора "
"нигде не записаны, и каждое предложение узнаёт их отказом"
)
elif "SHALL" not in live:
rep.error(
"rules.specs в openspec/config.yaml не называет SHALL — "
"требование без этого литерала валидатор отвергает, а правило "
"проекта об этом молчит"
)
# Ключ под rules: — имя артефакта схемы. Опечатка или устаревшее имя не
# ломает ничего видимого: правила просто не применяются, а конфиг выглядит
# написанным.
for name in rules_keys(live):
if name not in OPENSPEC_ARTIFACTS:
rep.error(
f"rules.{name} в openspec/config.yaml — такого артефакта у схемы "
f"{OPENSPEC_SCHEMA} нет ({', '.join(OPENSPEC_ARTIFACTS)}): правила "
f"под ним не применяются и молчат об этом"
)
def check_fresh(rep: Report) -> None:
"""Не устарел ли слепок формы.
Стоит один запуск `openspec --version` — десятые доли секунды. Перечень
артефактов и имя схемы отсюда не спрашиваются намеренно: они стоят втрое
дороже, а меняются только вместе с версией, и потому за ними ходит команда
`form`, а эта проверка говорит, когда её звать.
"""
got = openspec_cli(["--version"])
if got is None:
rep.skip(
"openspec не отвечает (нет на PATH?) — актуальность формы "
"config.yaml не проверялась"
)
return
installed = ".".join(got.split(".")[:2])
if installed != OPENSPEC_CHECKED:
rep.note(
f"форма openspec/config.yaml сверена с OpenSpec {OPENSPEC_CHECKED}, "
f"установлен {got}: перепроверить — `openspec.py form`. Пока не "
f"перепроверено, проверки формы судят по прежней схеме"
)
def report(rep: Report) -> int:
for msg in rep.errors:
print(f"ДРЕЙФ {msg}")
for msg in rep.notes:
print(f"ЗАМЕЧАНИЕ {msg}")
if rep.skipped:
print("\nНЕ ПРОВЕРЯЛОСЬ:")
for msg in rep.skipped:
print(f" {msg}")
print(
"\nМашина проверила форму: имя файла, схему, незаменённый пример, адреса\n"
"документов проекта и ключи rules против артефактов схемы. Чего она не\n"
"видит — **пересказ вместо ссылки**: утверждение, которое можно\n"
"опровергнуть, открыв другой файл проекта, от строки «открой такой-то\n"
"файл» она не отличает. Это суждение агента `doc-consistency` из плагина\n"
"канона документов; нет плагина — нет и этой проверки, и так и скажи."
)
if rep.errors:
print(f"\nИтог: дрейф, {len(rep.errors)} пунктов.")
return DRIFT
print("\nИтог: форма сошлась в механизируемой части.")
return OK
def cmd_check(args: argparse.Namespace) -> int:
root = Path(args.dir).resolve()
if not root.is_dir():
fail(ENV, f"нет каталога {root}")
rep = Report()
check_form(root, rep)
check_fresh(rep)
return report(rep)
def cmd_form(args: argparse.Namespace) -> int:
"""Перепроверить слепок формы по живому OpenSpec.
Ничего не правит и не трогает проект: спрашивает инструмент и печатает, что
разошлось с константами скрипта. Чинит человек — правкой констант, образца в
references/config-skeleton.md и записью в журнал версий канона, если форма
действительно поменялась.
"""
version = openspec_cli(["--version"])
if version is None:
fail(
ENV,
"openspec не отвечает: поставь его или проверь PATH — "
"перепроверять форму нечем",
)
raw = openspec_cli(["templates", "--json"])
if raw is None:
fail(ENV, "`openspec templates --json` не отработал — схему не спросить")
try:
artifacts = tuple(json.loads(raw))
except json.JSONDecodeError as exc:
fail(ENV, f"`openspec templates --json` отдал неразбираемое: {exc}")
print(f"OpenSpec установлен: {version}")
print(f"форма сверена с: {OPENSPEC_CHECKED}")
print(f"артефакты схемы: {', '.join(artifacts)}")
print(f"записано в скрипте: {', '.join(OPENSPEC_ARTIFACTS)}")
diffs: list[str] = []
if ".".join(version.split(".")[:2]) != OPENSPEC_CHECKED:
diffs.append(
f"версия: поднять OPENSPEC_CHECKED до "
f"{'.'.join(version.split('.')[:2])} — но только после того, как "
f"остальные строки этого отчёта сойдутся"
)
for name in artifacts:
if name not in OPENSPEC_ARTIFACTS:
diffs.append(
f"новый артефакт {name}: решить, нужны ли ему правила в rules, "
f"и добавить имя в OPENSPEC_ARTIFACTS"
)
for name in OPENSPEC_ARTIFACTS:
if name not in artifacts:
diffs.append(
f"артефакта {name} у схемы больше нет: правила под ним в конфигах "
f"проектов молчат — убрать из OPENSPEC_ARTIFACTS, из образца и "
f"записать в журнал версий канона"
)
print()
if not diffs:
print("Слепок сходится. Осталось глазами: не изменились ли придирки")
print("валидатора — их скрипт проверить не может, они проявляются только")
print("отказом `openspec validate --strict` на живой спеке.")
return OK
print("Разошлось:")
for line in diffs:
print(f" - {line}")
print()
print("Правится в трёх местах сразу: константы этого скрипта, образец")
print("`references/config-skeleton.md` и запись в журнал версий канона —")
print("иначе проекты останутся на прежней форме молча.")
return DRIFT
def main() -> int:
parser = argparse.ArgumentParser(
prog="openspec.py",
description="форма openspec/config.yaml: проверка проекта и сверка слепка",
)
sub = parser.add_subparsers(dest="cmd", required=True)
p_check = sub.add_parser("check", help="форма config.yaml в проекте")
p_check.add_argument("--dir", default=".", help="корень проекта")
p_check.set_defaults(func=cmd_check)
p_form = sub.add_parser(
"form", help="перепроверить слепок формы по живому OpenSpec"
)
p_form.set_defaults(func=cmd_form)
args = parser.parse_args()
try:
return args.func(args)
except SystemExit:
raise
except Exception as exc: # noqa: BLE001 — последний рубеж, код 4 по словарю
print(f"ВНУТРЕННИЙ СБОЙ: {exc}", file=sys.stderr)
return INTERNAL
if __name__ == "__main__":
sys.exit(main())
+629
View File
@@ -0,0 +1,629 @@
---
name: resolve
description: "Решить одну задачу от постановки до закрытия. На входе путь к файлу задачи, её слаг или просто текст. Обычная задача идёт циклом Spec Driven Development: opsx propose → разметка → ревью дизайна → чекпоинт с объяснением человеческим языком → opsx apply → ревью кода → archive → синк документации → коммит → закрытие. Исследовательская (тип research, сырая идея, мутная постановка) начинается раньше: opsx explore и чекпоинт вариантов — два-четыре способа решить, с ценой каждого и рекомендацией; выбор оседает по адресу, который назвала сама задача. Между чекпоинтами работа идёт без согласований. Использовать, когда просят взять, сделать или решить задачу, довести идею до реализации, разобраться с записью из беклога."
---
# Решение одной задачи
Проводит **одну** задачу от постановки до закрытия. Между плановыми
остановками — без согласований: механику не обсуждаем, делаем.
Тонкая обёртка над каноническими скиллами `opsx:explore` / `opsx:propose` /
`opsx:apply` / `opsx:archive` — зови их через Skill, не переизобретай их шаги.
Ревью — скилл `av-dev-code:review`; он же держит правило выбора
метки, а называет её агент `review-scope` — один раз на задачу, для обеих стадий
ревью.
## Предпосылки
- **OpenSpec и скиллы `opsx:*` — жёсткая предпосылка, а не опция.** На них стоят
шаги 2, 6 и 8, проход `review-specs` и ревью дизайна (они завязаны на
`openspec/changes/<id>/specs/*/spec.md` и на `openspec validate --strict`).
**Проект без OpenSpec этим скиллом не ведётся** — подключай OpenSpec, а не
вырождай цикл: ветка деградации здесь не пишется, потому что непроверенная
ветка деградации хуже честного отказа.
- **Проектные копии этих скиллов и агентов удаляются при установке плагина**
(`.claude/skills/` — и голые имена `resolve`, `task-pipeline`,
`review-pipeline`, и с префиксом проекта: `<проект>-task-pipeline`,
`<проект>-review-pipeline`; `.claude/agents/<проект>-review-*.md`). Две копии
одного скилла расходятся, и побеждает та, что короче названа.
### Обращение к соседним плагинам
**Копия.** Дом — `shared/plugin-boundary.md` в репозитории плагинов: правило
общее для всех, кто зовёт чужое, и ни один плагин им не владеет. Правится дом, а
не этот файл.
<!-- копия: граница-плагинов из shared/plugin-boundary.md -->
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
месте.
**Чужой скилл зовётся полным именем**`av-dev-docs:canon`, `av-dev-tasks:tasks`,
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
прочитает его сам.
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json`
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
<!-- /копия: граница-плагинов -->
Скилл зовёт `av-dev-code:review`, `av-dev-docs:docs` и
`av-dev-tasks:tasks`. Чем оборачивается отсутствие каждого — на самих шагах и в
разделе «Границы».
Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается, если ещё
не в контексте. Проектные факты, нужные ревью — инварианты, семантика гейта,
объёмы, модель угроз, прецеденты, — живут в **документах канона** `av-dev-docs`;
карта «что где» — `references/project-facts.md` конвейера ревью.
**Документов канона нет — проект к нему не приведён.** Скажи это строкой и
предложи скилл `av-dev-docs:canon`: одна операция на проект против поразрядной
деградации на каждой задаче. Работу при этом не останавливай.
## Вход
Задача задаётся **путём к файлу, именем файла, слагом или просто текстом**.
Ничего из этого не задано — попроси у вызывающего и остановись; сам в беклог не
лезь и приоритеты не интерпретируй: что делать дальше, решает не этот скилл.
**Запись, не готовая к работе, в работу не берётся.** У типа `research` это
проверяемо и потому обязательно: нет непустого раздела **«Вопрос»** — это не
разведка, а сырьё, и его сперва доводят до вопроса. Скажи это исходом и назови,
чего не хватает; штурмовать сырьё за автора — не работа этого скилла.
У остальных типов схему держит `av-dev-tasks` (обязательные разделы, не меньше
двух критериев приёмки). Прочитай запись и, если видно, что схема не выполнена,
скажи это строкой — но работу не останавливай: у типов действия пробел лечится
по ходу, а у разведки без вопроса лечить нечего.
## Две ветки
Развилка одна и стоит на входе:
- **обычная задача** — что делать, понятно; спорно только как. Идёт с шага 1;
- **исследовательская** — тип `research`, сырая идея, новое и незнакомое, мутная
постановка. Идёт с шага Р1, и там её ждёт **свой** чекпоинт: варианты решения
обсуждаются **до** того, как написано первое требование.
Признак не в объёме работы, а в том, **есть ли у задачи один очевидный способ
решения**. Его нет — обсуждать варианты после `propose` поздно: предложение уже
воплотило один из них, и разговор пойдёт не о выборе, а о переделке.
```mermaid
flowchart TD
in["вход: файл, слаг или текст"]
fork{"есть очевидный<br/>способ решения?"}
r1["Р1. понять вопрос<br/>сырьё без «Вопроса» — отказ"]
r2["Р2. opsx:explore — груминг"]
r3(["Р3. ЧЕКПОИНТ: варианты<br/>2–4 способа, цена каждого,<br/>рекомендация"])
rout["исход без кода:<br/>ответ записан / отказ / родились задачи"]
s1["1. прочитать задачу<br/>критерии приёмки выписать сразу"]
s2["2. opsx:propose — change, дельта-спеки, tasks.md"]
s3["3. разметка — review-scope:<br/>размер, сложность, метка, план тем"]
s4["4. ревью дизайна, состав по метке<br/>+ отработка замечаний"]
s5(["5. ЧЕКПОИНТ: объяснение<br/>в чём проблема, как решаем,<br/>чем рискуем"])
s6["6. opsx:apply — код, гейт,<br/>поведенческая верификация"]
s7["7. ревью кода, та же метка<br/>+ отработка замечаний"]
s8["8. opsx:archive"]
s9["9. синк документации — av-dev-docs:docs"]
s10["10. коммит работы — av-dev-git:commit"]
s11["11. закрыть задачу — av-dev-tasks:tasks,<br/>вторым коммитом учёта"]
in --> fork
fork -->|"да"| s1
fork -->|"нет"| r1
r1 --> r2 --> r3
r3 -->|"выбран способ"| s1
r3 -.->|"кода не будет"| rout
s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7 --> s8 --> s9 --> s10 --> s11
s3 -.->|"план задачи: та же метка"| s7
s7 -.->|"находка отменяет дизайн"| s5
```
Схема — **сводка**: содержание каждого шага в его разделе ниже, и при
расхождении прав текст.
## Автономность и два плановых стопа
**Между чекпоинтами умолчание прежнее — делать, а не спрашивать.** Чекпоинты не
отменяют автономность, они дают развилкам плановое место, куда копиться.
Разрез простой:
- развилка найдена **до** ближайшего чекпоинта — она его и ждёт. Не спрашивай
отдельно: чекпоинт рядом и стоит дёшево, а три вопроса подряд стоят дороже
одного разговора;
- развилка найдена **после** последнего чекпоинта — старое правило: **запиши
вопрос и доведи остаток**, не останавливаясь.
Запись вопроса устроена так:
1. **Запиши там, где проект держит вопросы** (секция беклога, файл задачи,
трекер — это знает проект). Проект не сказал, куда, — отдельной секцией
`Вопросы` в своём докладе, и это тоже исход. Тело отвечает на три вещи: **что
именно решить**, **какие есть варианты и цена каждого**, **что стоит, пока
решения нет**. Плюс твоя рекомендация — человек чаще соглашается, чем выбирает
заново, и готовое суждение экономит ему весь контекст.
2. **Переформулируй задачу на остаток** — то, что делается без этого решения.
Назови границу: докуда доводим сейчас.
3. Доведи остаток до конца и закоммить. Задача не «висит на вопросе», она сделана
в объявленных границах.
**Что остатком не является — правило живёт не здесь.** Канонический текст с обеими
оговорками — в плагине `av-dev-tasks`, скилл `av-dev-tasks:session`, раздел
`## Вопрос, блокер, необратимое`, подраздел «Отличать вопрос от застревания».
Правило принадлежит управлению задачами, потому что решает **сделана задача или
вышла**, — это исход планирования, а не исполнения. **Ссылайся, не
пересказывай:** копия, заведённая здесь, уже однажды разошлась с оригиналом и
потеряла из перечня самое необратимое — запись **наружу**.
Коротко, чтобы знать, когда идти читать: остаток проверяется двумя порогами —
**материализация нерешённого** (запись состояния, зависящего от неотвеченного
вопроса) и **пол по пользе** (из остатка пропала польза, названная в постановке).
Оба порога — стоп: первый поднимает решение до начала записи, второй даёт исход
«не доведена».
Плагин `av-dev-tasks` не подключён — правило не отменяется, а становится
осторожнее: прежде чем записать зависящее от нерешённого куда бы то ни было —
в хранилище, в журнал, в витрину или наружу, — спрашивай человека.
Нет полезного остатка — задача заканчивается исходом «не доведена», вопрос
записан, ничего не коммитится наполовину.
### Когда спрашивать вне чекпоинтов
По другому основанию — не «сложное решение», а **необратимое действие**:
- деплой, выкладка наружу, смена публичного адреса или токенов;
- удаление или перезапись рабочих данных, включая подрезку архивов;
- всё, что уходит за пределы машины.
Здесь ошибка не откатывается коммитом, поэтому спрашиваем даже когда решение
кажется очевидным.
Стиль правок — заточка под проект и конвенции, right-size, без золочения.
## Границы: чем этот скилл не владеет
- **Беклогом, целями и приоритетами.** Задача приходит извне. Скилл её не
выбирает, не приоритизирует, не заводит и не переоценивает.
- **Форматом задач.** Индексы руками не правятся, путь к скрипту учёта не
выдумывается: этим владеет `av-dev-tasks:tasks` (шаг 11). Закрытие — работа
этого скилла, и это осознанное решение с названной ценой: **приёмщик и
исполнитель совпали**. Закрытие поэтому **не окончательно** — человек на сессии
возвращает задачу `reopen` с причиной, а доклад по критериям приёмки становится
единственным, по чему приёмка вообще возможна.
- **Заведением задач из урожая ревью.** Отложенные находки отдаются **списком**;
превращать их в задачи — работа того, кто ведёт задачи проекта.
- **Определением ценности.** «Нужна ли эта функциональность» — не вопрос этого
скилла ни на одном шаге. Чекпоинт спрашивает «так ли решаем», а не «надо ли».
## Наблюдаемые исходы
Ровно четыре, и каждый обязан быть назван в докладе прямо:
- **сделана** — определение готовности выполнено целиком;
- **не доведена** — с причиной и с записанным вопросом; что именно сделано и до
какой границы, названо явно. Сюда же попадает чекпоинт, на котором человек
решение не одобрил;
- **оказалась крупнее задачи** — распознаётся **до заведения change**, иначе его
придётся выбрасывать. Дальше — декомпозиция, и это не работа этого скилла;
- **знание вместо изменения** — только у исследовательской ветки: ответ записан
по названному адресу, кода задача не потребовала. Это полноправный исход, а не
недоведённая работа.
## Определение готовности
Задача сделана, когда верно всё:
1. гейт проекта зелёный;
2. ревью проведено, **план прогона сверен с исходом по каждой теме**, темы без
отчёта и без дома названы в границах покрытия;
3. решение прошло чекпоинт и не разошлось с одобренным — либо разошлось, и
чекпоинт был пройден заново;
4. change заархивирован, дельты влиты в актуальные спеки;
5. коммит сделан в текущую ветку;
6. **критерии приёмки, если проект их дал, выписаны поимённо, и по каждому назван
оракул и наблюдаемый исход** — «прогнал вот это, увидел вот то». Это **доклад,
а не сертификация: приёмка — не работа этого скилла.** Исполнитель, ставящий
себе галочку «принято», проверяет свою работу своим же взглядом — по границе
это может делать только приёмщик, разведённый с исполнителем. Критерии приходят
снаружи; скилл их не сочиняет и не занижает. Расхождение «по каждому критерию
исход есть, а суть задачи не достигнута» — дефект критериев, и о нём
сообщается, а не молча дорабатывается.
У разведки, кончившейся знанием, определение своё и короткое: **ответ записан по
адресу, который назвала задача**, и в нём есть провенанс у каждого числа.
## Исследовательская ветка
### Р1. Понять вопрос
Прочитай запись. У типа `research` в ней два обязательных раздела, и оба нужны
тебе прямо сейчас: **«Вопрос»** — на что отвечаем, **«Куда ляжет ответ»** — по
какому адресу он ляжет. Адрес назван заранее не из аккуратности: ответ, не
имеющий дома, остаётся в переписке, и через квартал разведку заказывают заново.
Вопроса нет — исход «не доведена» с причиной «запись это сырьё»: назови, что
нужно дописать, и остановись. Адреса нет, а вопрос есть — назначь адрес сам и
скажи об этом строкой: разведка без дома для ответа хуже несделанной.
Задача не из каталога (пришла текстом) — адреса у неё нет по построению. Тогда
дом ответа — `design.md` того change, который родится дальше; кода не будет —
`docs/research/`, и это тоже говорится строкой.
### Р2. Груминг — `opsx:explore`
Вызови Skill `opsx:explore`. Читай документы проекта, а не только запись:
граница домена и «чем проект **не** является» из паспорта отсекают половину
вариантов до того, как их начнут сравнивать. **В explore не пишем код.**
Развилку грумминга **не записывай вопросом** — она и есть предмет следующего
шага. Это отличие от обычной ветки: там развилка уходит в запись, здесь она
копится в чекпоинт.
### Р3. Чекпоинт: варианты
**Остановись и покажи человеку способы решить.** Это первый из двух плановых
стопов, и он существует потому, что после `propose` выбор уже сделан
предложением.
Форма — короткая, экран текста:
- **вопрос**, на который отвечаем, одной фразой (он уже есть в записи);
- **2–4 варианта**, не больше. Больше четырёх — это не выбор, а список: человек
не сравнит, а признает свою неспособность сравнить и попросит рекомендацию.
У каждого варианта: в чём суть простыми словами, **что даёт**, **чего стоит**,
**что становится невозможным** (это ловится хуже всего и стоит дороже всего);
- **рекомендация с причиной**. Человек чаще соглашается, чем выбирает заново;
- что известно **недостоверно** и как это проверить, если проверять дёшево.
Что нельзя: приносить варианты, различающиеся только реализацией; прятать
отвергнутое (отвергнутый вариант с названной причиной — половина будущего ADR);
приносить один вариант и называть это выбором.
**Выбор оседает по адресу.** У `research` это раздел «Куда ляжет ответ». У
остальных — `design.md` change, который родится на шаге 2, разделом
«рассмотренные варианты». Не в переписку: разговор, из которого ничего не
записано, повторяется через месяц целиком.
Три исхода чекпоинта:
- **выбран способ** — идёшь на шаг 1 общей ветки;
- **ответ и есть исход** — работа кончается знанием: запиши ответ по адресу,
доложи исход «знание вместо изменения» и закрой задачу (шаг 11). Провенанс у
каждого числа обязателен: число без источника проход ревью обязан читать как
условие, а не как замер;
- **разведка родила задачи** — исход «оказалась крупнее задачи». Нарезка не твоя
работа: отдай список формулировками и остановись.
## Шаги
### 1. Прочитать задачу
Прочитай запись и связанные спеки и черновики.
Проект даёт задаче **критерии приёмки** — выпиши их сразу: на шаге 2 они уезжают
в `tasks.md` change. Файл задачи может быть удалён до коммита, а критерии обязаны
его пережить.
Здесь же проверка на «крупнее задачи»: видно, что одним заходом это не
мерджится, — объявляй исход **до** заведения change.
### 2. Завести change — `opsx:propose`
Вызови Skill `opsx:propose`. Получаем `proposal.md`, дизайн, дельта-спеки
(`ADDED`/`MODIFIED`/`REMOVED Requirements`), `tasks.md`. Каждое `### Requirement`
содержит `SHALL`/`MUST`; структурные заголовки английские, сценарии
`GIVEN/WHEN/THEN`. Прогони `openspec validate --strict <id>`.
Критерии приёмки задачи, если они были, копируются в `tasks.md` отдельным блоком.
Варианты, разобранные на чекпоинте Р3, — в `design.md`, с причиной отказа по
каждому отвергнутому.
**`proposal.md` пишется так, чтобы его понял человек, не читавший спек.** Это не
стилистическое пожелание: из него собирается чекпоинт шага 5, и переписывать его
там заново значит завести второй дом для одного объяснения. Требование стоит в
`openspec/config.yaml`, `rules.proposal` — то есть применяется в момент
порождения артефакта, а не вспоминается после.
### 3. Разметка задачи — агент `review-scope`
**Один запуск на всю задачу, и он обслуживает оба чекпоинта ревью.** Запусти
агента `review-scope`, дав ему корень проекта, идентификатор change, базу диффа и
запись задачи. Кода на этот момент нет, и это условие его работы, а не помеха.
Он возвращает **план задачи**:
- **размер** (малое / среднее / крупное) и **сложность** (знакомое /
незнакомое), каждое с обоснованием по факту;
- **метку** как максимум по двум осям: `small`, `medium` или `large`;
- **состав ревью дизайна** — что звать на шаге 4;
- **таблицу тем** «тема → дом → глубина → кто закрывает» — для шага 7;
- разнесение документов проекта по трём категориям и строку про директивы.
**Метку выбираешь не ты.** Раньше состав ревью дизайна называл сам оркестратор —
то есть тот, кто только что довёл предложение до `propose`. Разведённости с
автором в этой точке не было вовсе; теперь есть.
**План держи в контексте до конца задачи.** На диск он не пишется: файл-план стал
бы четвёртым артефактом рядом с `proposal.md`, `tasks.md` и `design.md`, пережил
бы задачу и разошёлся бы с ней молча. Прервался прогон — повтори шаг 3, это самый
дешёвый его проход.
**Разметка повторяется ровно в одном случае** — если правки изменили сами
**дельта-спеки**: план выведен из них, и план по отменённым требованиям назовёт
не те темы. Во всех прочих случаях, включая переделку формы кода на шаге 7,
метка остаётся прежней.
### 4. Ревью дизайна — ДО кода, состав по метке
Вызови Skill **`av-dev-code:review`**, дав ссылку на change `<id>`,
**план разметки с шага 3** и указание, что это ревью дизайна.
Состав приходит планом, а не решается здесь:
| Метка | Проходы на предложении |
|---|---|
| `small` | `specs` |
| `medium` | `specs`, `rubric` |
| `large` | `specs`, `rubric`, `architecture` + вопрос автору о трёх формах решения |
`review-specs` в режиме «дизайн ДО кода» идёт **на каждой задаче**: это самый
дешёвый чекпоинт конвейера, и он ловит то, что на готовом коде уже не чинят.
Остальные включаются меткой, потому что стадия стоит на каждой задаче и каждый
лишний проход здесь умножается на число задач.
Смысл стадии: архитектурная находка на готовом коде стоит переписывания и потому
игнорируется — та же находка здесь стоит абзаца обсуждения. Если `review-rubric`
запускался, перенеси его рубрику в `tasks.md` как приёмочные критерии; там же уже
лежат критерии от постановки, если они были.
**Отработка замечаний, и она идёт до чекпоинта, а не после:**
- мелочь и явные улучшения — правь сам в спеках и дизайне;
- развилки (компромисс, scope, инвариант) — **не в запись, а в чекпоинт**: он
следующим шагом, и это ровно то, ради чего он поставлен здесь;
- после правок перепрогони `openspec validate --strict <id>`.
### 5. Чекпоинт: объяснение
**Остановись и объясни человеку, что происходит.** Второй плановый стоп и
единственный обязательный для всех задач.
Он стоит **после** ревью дизайна намеренно. Человек читает объяснение, уже
просеянное машиной: то, что поймал бы `review-specs`, до него не доходит, а
внимание — самый дорогой ресурс процесса, и тратить его на выловимое машиной
нельзя.
**Объяснение не сочиняется заново — оно собирается из артефактов**, `proposal.md`
и `design.md`. Третий пересказ был бы третьим домом одного и того же и разошёлся
бы с обоими. Что показываешь:
- **в чём проблема** — словами домена из паспорта, без имён модулей и функций;
- **как решаем** — суть одним абзацем. Не пересказ дельта-спек: спека нормирует,
а здесь объясняют;
- **что человек увидит иначе**, когда это будет сделано;
- **чего мы намеренно не делаем** и почему — граница scope ловится хуже всего;
- **чем рискуем и что осталось нерешённым** — сюда съезжаются развилки,
накопленные до этого места, и находки ревью с пометкой `развилка`;
- **что дальше**, если возражений нет.
Проверка на «простой язык» одна и механическая: **в тексте нет `SHALL`, нет имён
файлов и функций там, где вещь называется по-русски, и нет слов, которых нет в
паспорте проекта.** Не проходит — переписывай, а не объясняй, почему иначе
нельзя.
Длина — экран. Длинное объяснение не читают, а пролистывают, и чекпоинт
превращается в ритуал одобрения.
Три исхода:
- **согласен** — идёшь на шаг 6;
- **скорректировать** — правишь спеки и дизайн по сказанному. Изменились
**дельта-спеки** — повтори шаг 3 (разметка выведена из них) и ту часть ревью
дизайна, которой касается правка; затем чекпоинт **заново**. Правка внутри
дизайна без спек — повтори только чекпоинт;
- **не одобрено** — исход «не доведена» с причиной. Change остаётся
незаархивированным, задача не закрывается, ничего не коммитится наполовину.
### 6. Написать код — `opsx:apply`
Вызови Skill `opsx:apply` для реализации `tasks.md`. Код — по конвенциям проекта
(каталог `docs/conventions/`). Меняешь схему — обнови её описание в документации
тем же change, если проект этого требует: гейт обычно это проверяет.
Прогони гейт и добейся зелёного — он же гейт следующего шага.
**Поведенческая верификация.** Если задача меняет реальное поведение (новый
эндпоинт, разбор входа, схема, форма ответа) — зелёных юнит-тестов мало. Подними
изменение вживую командой из раздела команд `CLAUDE.md` и прогони сценарий.
Пропусти только для чисто внутренних правок без наблюдаемого рантайма.
**Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца
шага.
### 7. Ревью кода — та же метка
Вызови Skill **`av-dev-code:review`**, дав ссылку на change `<id>`,
базу диффа, **план разметки с шага 3** и режим запуска.
**Метку ты не выбираешь, и это правило, а не упрощение.** Её назвал
`review-scope` ещё на шаге 3 — по размеру и сложности, с обоснованием по каждой
оси. Причина в разведённости: ты только что написал этот код, и решать, насколько
глубоко его проверять, тебе нельзя — под давлением «я почти закончил» решение
известно заранее. Правило выбора живёт в скилле конвейера —
`av-dev-code:review`, `references/review-levels.md`; проектные
триггеры — в `docs/review.*`, подраздел «Триггеры метки».
**Метка не пересматривается по факту диффа.** Дифф может выйти крупнее, чем
ожидалось при разметке, — это не повод её поднимать: пересмотр означал бы второй
запуск разметчика, ровно то, ради устранения чего он и переехал на шаг 3.
**Считаешь метку заниженной — скажи это в докладе строкой, а не переспорь.**
Разметчик вправе и поднять, и понизить; твоё несогласие это факт для человека, а
не команда конвейеру. Место, где такое несогласие превращается в изменение
правил, — журнал дефектов `docs/review.md`, и только постфактум.
**Плана нет — ревью кода не запускается.** Триаж требует план обязательным
входом: без него он не может сверить, все ли размеченные темы вернули отчёт, а
эта сверка — единственная защита от молчащего пропуска. Потерял план (прервалась
сессия, ушёл контекст) — повтори шаг 3, а не гони прогон без него.
**Режим по умолчанию — `по графу`, и обосновывать его не надо.** Конвейер сам
знает свои рёбра: гейт открывает опиниативные проходы, проходы с пометкой «держит
машину» идут цепочкой (иначе замеры портят друг друга и находка выглядит
доказанной), триаж — сток. Просить **`линейно`** нужно только по причине, и она
называется строкой: так сказал оператор; машина занята чем-то ещё; идёт разбор
самого конвейера.
Скилл сам гоняет гейт, нужные проходы и обязательный триаж. Возвращает отчёт с
потолком 7 пунктов, разметкой `Действие: инлайн | развилка` и секцией границ
покрытия.
**Сверь план прогона с исходом, прежде чем коммитить.** Отчёт начинается планом
разметчика — таблицей «тема → дом → глубина → кто закрывает», — и против каждой
темы обязан стоять исход. Тема без отчёта и тема без дома — разные вещи, и обе
должны быть названы. Реестр короткий (шесть тем ядра плюс свои) — сверка стоит
одного взгляда.
#### Отработка, и здесь появляется одно новое правило
Помеченное `инлайн` чини сам и не логируй. `развилка` — вопросом в запись (он уже
сформулирован триажем, его остаётся перенести). После правок — снова гейт.
**Находка, отменяющая одобренный дизайн, отменяет и одобрение.** Признак
проверяемый: **меняются ли дельта-спеки**.
- не меняются — находка внутри дизайна, дожимай сам, это обычная отработка;
- меняются — решение стало другим, а одобрено было прежнее. Повтори шаг 3
(разметка выведена из дельта-спек) и **вернись на чекпоинт шага 5** с тем, что
изменилось и почему.
Тихо переделать утверждённое нельзя. Чекпоинт, который можно обойти находкой
ревью, не значит ничего, а человек при этом уверен, что одобрил именно то, что
уехало в коммит.
**Урожай — списком, не задачами.** Отложенные находки (реальный `major` не для
этого мерджа, развилка, решённая «потом», пачка `nit`) собери в секцию доклада
`Урожай`: формулировка, оракул, провенанс. Задачи из него **заводит не этот
скилл** — у того, кто ведёт задачи проекта, своя нарезка, свой формат и свои
правила дублей. Твоя обязанность — не потерять и передать.
**Границы покрытия из отчёта не выбрасывай** — они уезжают в финальный доклад
сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно»,
превращается в ложное ощущение проверенности.
**Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`; шаг 8
унесёт его в архив вместе с change) — это обязательно, а не «если удобно».** По
нему потом видно, что было найдено и что из этого осталось в урожае. И это
единственный **независимый** артефакт о составе прогона: своей прозе здесь верить
нельзя — она написана тем же, кто мог проход и пропустить.
### 8. Архивировать — `opsx:archive`
Вызови Skill `opsx:archive`: change уезжает в архив, дельты вливаются в
актуальные спеки. Не пропускай `openspec validate --strict` перед этим.
### 9. Синк документации
**Вызови Skill `av-dev-docs:docs`**: он владеет содержимым документов канона и
ведёт чек-лист синка.
**Правило одно и оно жёсткое: принуждённое отрицание.** Доклад обязан назвать
**каждый** документ канона — либо чем он обновлён, либо «не требуется, потому
что…». Нетронутые группируются одной строкой с общей причиной. Список триггеров
прозой уже проверен на живом проекте и дал 6 записей ADR на 43 изменения;
работает только обязательное отрицание.
**Список документов и их триггеров здесь не дублируется** — он в чек-листе скилла
`av-dev-docs:docs`, и копия уже однажды разошлась с оригиналом, потеряв два
триггера.
**Плагина `av-dev-docs` в проекте нет** — путь в его дерево не разрешится ниоткуда,
поэтому за списком иди в **свой** reference:
[references/project-facts.md](../review/references/project-facts.md)
конвейера ревью перечисляет все документы канона с их предметом. Пройди по этому
перечню — каждый документ получает строку, отрицание остаётся обязательным.
Триггеры при этом ты знаешь хуже, и это называется в докладе строкой: «синк
сделан по перечню документов, без списка триггеров — плагина `av-dev-docs` нет».
Канона в проекте тоже нет — назови это исходом и предложи `av-dev-docs:canon`.
### 10. Коммит
Коммить **в текущую ветку** (`git rev-parse --abbrev-ref HEAD`), сам ветку не
создавай и не переключай, ничего не пушь.
Сообщение — по-русски, скиллом `av-dev-git:commit`, если он подключён (первая
строка «что сделано», тело списком 1–3 пункта, без трейлеров). Одна задача — один
осмысленный коммит.
### 11. Закрыть задачу — **после коммита, не раньше**
**Вызови Skill `av-dev-tasks:tasks`** и попроси закрыть задачу как реализованную —
он владеет форматом и двигает строку индекса сам. Путь к его скрипту не выясняй и
индексы руками не правь: мост между плагинами — вызов скилла, а не путь.
**Порядок обязателен.** Закрытие удаляет файл задачи; сделанное до коммита оно
оставило бы задачу закрытой без единого следа работы, если шаг 10 упадёт.
**Закрытие тоже коммитится — вторым коммитом, тут же.** Удаление файла задачи и
правка индексов (их имена знает `av-dev-tasks`, не ты) — это правки в рабочем
дереве, и оставить их незакоммиченными нельзя: закрытие, не доехавшее до основной
ветки, оставит задачу открытой молча, а опора «индексы под git показывают, что и
когда закрыто» без коммита — пустые слова. Сообщение короткое, про учёт, а не про
работу: `закрыта задача <slug>`. Это второй коммит осознанно: правило «одна задача
— один осмысленный коммит» про работу, а учёт — не работа.
Разведка, кончившаяся знанием, закрывается так же — но перед этим убедись, что
ответ **записан по названному адресу и закоммичен**. Закрытая разведка без
записанного ответа не оставляет следа вообще: файл задачи удалён, ответ был в
переписке.
Плагина в проекте нет — вызов не разрешится. Тогда **ничего не выдумывай**: скажи
в докладе, что учёт задач остаётся за владельцем, и назови исход.
## Доклад
Коротко, и в нём обязательно:
- **исход** одним из четырёх слов и, если не «сделана», чем ограничен результат;
- что сделано, какие вопросы записаны и куда;
- **что было одобрено на чекпоинте и совпало ли с тем, что уехало в коммит** —
расхождение здесь называется прямо, даже если оно мелкое;
- ссылка на архивный change и хеш коммита;
- по каждому критерию приёмки, если они были: **оракул и наблюдаемый исход**
это доклад приёмщику, а не отметка «принято»;
- **`Урожай`** — отложенные находки списком (формулировка, оракул, провенанс);
- **одна строка границ покрытия**: какая метка и режим гонялись, какие проходы не
запускались и что проверить было невозможно. Доклад без неё сообщает
«проверено», не сообщая, что именно.
## Тонкости
- **Не завязывайся на основную ветку и корень репозитория.** Скилл работает в
текущем worktree и на текущей ветке: не делай `git checkout`/`switch`, не
создавай веток, не пушь.
- Обычная задача с очевидным решением проходит **один** чекпоинт, и это норма, а
не упрощение. Два чекпоинта — цена незнания, а не признак серьёзности задачи.
- Гейт блокирует: пока он красный, опиниативные проходы не запускаются. Чинить и
перезапускать, а не «посмотреть заодно».
- Держи вызывающего в цикле короткими репликами на переходах фаз, но не проси
подтверждать механику: чекпоинты — единственные места, где ждут ответа.
- **Занизить метку ревью, пропустить тему или проскочить чекпоинт — самый дешёвый
способ «ускориться», и он же самый дорогой по последствиям.** Защита устроена
так, что регулятора у тебя нет: метку выбирает разметчик **до того**, как ты
написал код, план сверяется по темам, непокрытое называется строкой, а
расхождение с одобренным — отдельным пунктом доклада.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,108 @@
# Калибровка проходов
Без измерения набор проходов растёт монотонно и вырождается в театр: каждый
кажется полезным, потому что иногда что-то говорит. Калибровка отвечает на
единственный вопрос — **ловит ли проход дефект своего класса**.
## Процедура (инъекция дефекта)
1. Взять **реальный коммит** из истории (`git log --oneline`), лучше
архивированный change с непустым диффом.
2. Внести в него **один** дефект того класса, который проход обязан ловить по
своему charter'у. Дефект должен быть правдоподобным — таким, какой реально
пишет модель, а не карикатурой (`panic("TODO")` не считается).
3. Прогнать **только этот проход** на подготовленном диффе — **три раза**,
каждый в чистом контексте.
4. Зафиксировать: нашёл `n/3`, число находок всего, число ложных.
5. Вердикт:
| Результат | Вердикт | Что делаем |
|---|---|---|
| нашёл 3/3 или 2/3, ложных немного | `keep` | ничего |
| нашёл 1/3 или 0/3 | `retune` | правим charter — сужаем вход, убираем чек-лист, добавляем оракул |
| `retune` уже был дважды подряд | `drop` | удаляем проход |
| находит, но ложных больше трети от всех находок | `retune` | триаж съедает больше, чем экономит проход |
Вердикты образуют храповик со счётчиком — его-то таблица и не показывает:
```mermaid
stateDiagram-v2
state "проход в составе метки" as live
state "retune №1 — правка charter'а" as r1
state "retune №2 — последняя попытка" as r2
state "проход удалён" as dead
[*] --> live: заведён и откалиброван ДО включения
live --> r1: 1/3, 0/3 или ложных больше трети
r1 --> live: замер keep — счётчик сброшен
r1 --> r2: снова не ловит
r2 --> live: замер keep — счётчик сброшен
r2 --> dead: снова не ловит — это театр
```
Схема — **сводка** к таблице вердиктов выше: она добавляет только счётчик, и при
расхождении прав таблица.
**`retune` не более двух раз подряд.** Проход, не находящий дефект своего класса
в 2 из 3 прогонов после двух правок промпта, — это театр. Удалять, а не
бесконечно править формулировки: каждая итерация правки промпта стоит дороже,
чем отсутствие прохода.
**Существующий проход не удаляется без замера.** Сначала калибровка, потом
решение — иначе удаляется то, что работало, а остаётся то, что громче. Обратный
пример уже был: проход про идиоматичность стоял в списке на удаление как
«вкусовщина», а замер показал, что он зарабатывает **экспериментами против
поведения библиотеки и драйвера**, — и находка, воспроизведённая числом, отменила
решение, принятое по ощущению.
## Состав проходов принадлежит плагину, а не проекту
Проходы общие. Проект не может удалить проход — он может **не звать** его, и
тогда это идёт строкой «не запускался» в границы покрытия, как любой другой
пропуск. Молча сузить состав нельзя: пропуск прохода не отличим от прохода без
находок.
Отсюда два следствия:
- **правка charter'а — правка для всех проектов.** Прежде чем сужать
формулировку под свою боль, проверь, не место ли ей в документах проекта: предмет проверки
живёт там, метод — в charter'е;
- **удаление прохода из плагина требует замера на двух проектах**, а не на одном:
класс, не всплывший здесь, мог быть единственным работающим там.
## Пробы дефектов по проходам
Проба — заготовка инъекции. Список пополняется из журнала проскочивших дефектов
(см. [review-journal.md](review-journal.md)): реальный проскочивший дефект —
лучшая проба, какая вообще возможна, потому что синтетические смещены в сторону
тех, которые уже умеешь придумывать.
| Проход | Класс дефекта для инъекции | Заготовка пробы |
|---|---|---|
| `review-scope` | пропущенная тема | положить в `docs/` новый документ и проверить, попал ли он в план темой |
| `review-autotests` | отсутствующая верификация | убрать тест на изменённую ветку, оставить код рабочим |
| `review-specs` | поведение вне спеки | добавить незаказанный фолбэк-дефолт на пустом входе |
| `review-code` | нарушение прозаической конвенции | увести штатный отказ мимо единой точки трансляции ошибки |
| `review-code` | технический дефект | не проверить возвращённую ошибку в ветке раннего возврата |
| `review-rubric` | нарушенное свойство узла | у клиента внешнего сервиса убрать таймаут и протяжку `context` |
| `review-basics` | отказ, видимый чтением | убрать обработку ошибки записи так, чтобы отказ считался успехом |
| `review-basics` | своя тема проекта | нарушить правило из документа, у которого нет именного прохода |
| `review-architecture` | второй способ | завести вторую точку генерации id мимо единой |
| `review-adversary` | построенный путь | принять внешний идентификатор без разбора до запроса в хранилище |
| `review-ops` | деградация окружения | убрать обработку недоступности внешней зависимости в фоновом цикле |
| `review-triage` | шум | подать 20 находок, из них 15 вкусовщина и 3 дубля — проверить потолок и дедуп |
Метрик сверх этого не заводим. Precision, корреляция между проходами, стоимость
прогона в токенах — всё это красиво звучит и никем не считается вручную; набор
показателей, который не собирают, создаёт впечатление измеряемости и тем вреден.
Работает ровно один механизм: инъекция дефекта и вердикт. Если корреляция двух
проходов действительно бросается в глаза — это видно по полю `Найдено проходом`
в триажированных отчётах и без отдельной метрики.
## Когда калибровать
- при заведении нового прохода — **до** включения в состав метки по умолчанию;
- при правке charter'а существующего — иначе непонятно, правка помогла или нет;
- при появлении записи в журнале проскочивших дефектов — калибруем тот проход,
который должен был поймать;
- планово — нет. Календарная калибровка ради галочки сама превращается в театр.
@@ -0,0 +1,103 @@
# Контракт находок
Единый формат для всех проходов конвейера ревью. Проход, нарушивший контракт,
считается сломанным — триаж вправе выбросить его вывод целиком.
## Форма находки
```
### <краткая формулировка ПОСЛЕДСТВИЯ, не симптома>
- Файл: internal/<пакет>/<файл>.go:120-134
- Severity: critical | major | minor | nit
- Confidence: high | medium | low
- Оракул: <падающий тест / команда с выводом / положение руководства / нет>
- Последствие: <что произойдёт и при каких условиях>
- Предложение: <конкретное изменение>
- Найдено проходом: <имя агента; у проходов с раздельными потолками — имя и половина, например `code/техника`>
```
## Правила
- **Заголовок через последствие.** Не «нет проверки токена», а «читатель без
токена выгрузит всю историю». Не «слияние перезаписывает запись», а «повторная
доставка сотрёт поля у уже сохранённой записи, и восстановить их нечем».
Симптом в заголовке — это заявка на то, что читатель сам достроит последствие;
он не достроит, он просто починит симптом.
- **`critical` без оракула или построенного пути не существует.** Оракул — это
падающий тест, вывод выполненной команды или поимённое положение руководства. Не
«вероятно, здесь гонка», а прогон детектора гонок с его выводом.
- **`confidence: low` — это «так обычно пишут».** Такие находки допустимы, но не
поднимаются выше `minor`. Частотность конструкции в публичном коде — не
аргумент.
- **Находка без поля «Последствие» не выводится вовсе.** Пустое «Последствие:
ухудшает читаемость» равносильно отсутствию поля.
- **`nit` допустим только при нарушении записанной конвенции** — со ссылкой на
файл и раздел конвенций проекта (`docs/conventions/`) либо на
правило линтера. Если правило механизируемо, но не механизировано — это не
находка ревью, это `Promote candidate` (см. [promote.md](promote.md)).
- **`critical` по основанию «нарушен инвариант проекта» требует инвариантов.**
Ссылка идёт на пункт раздела инвариантов `CLAUDE.md` дословно. Без них основание
недоступно — см. [project-facts.md](project-facts.md), поразрядная деградация.
- **Расхождение — не дефект, пока не названо последствие.** Особенно для
архитектурного прохода: «я бы сделал иначе» без последствия не выводится.
## Шкала severity
| Severity | Что это | Пример |
|---|---|---|
| `critical` | нарушение инварианта проекта, потеря или порча данных, утечка секрета, построенный путь к отказу | запись потеряна при слиянии; тело пользовательской выгрузки в поле лога |
| `major` | сломанное требование дельта-спеки, необрабатываемый отказ штатного сценария, флаки-тест, поведение вне спеки, меняющее исход | приём отвечает 200, не записав тело: доставка считается принятой, а данных нет |
| `minor` | отступление от конвенции с реальной ценой, отсутствующая наблюдаемость, дублирование, которое разойдётся | ни одного чекпоинта на пути разбора: молчащая автоматизация неотличима от пустого потока |
| `nit` | нарушение записанной конвенции без последствий за пределами чтения | `msg` с интерполяцией вместо константы |
Шкала привязана к обратимости, а не к громкости: класс «необратимо и молча»
всегда весит больше класса «шумно и лечится повтором». Что здесь необратимо,
говорит `CLAUDE.md` — что в этом проекте необратимо.
## Блок границ покрытия
Каждый проход завершает вывод этим блоком. Он не сокращается и не заменяется
фразой «всё проверено».
```
## Coverage of this pass
- проверено: <что реально прочитано/запущено, с путями и командами>
- не проверялось и почему: <бюджет, недоступный инструмент, вне входа>
- принципиально недоступно этому проходу: <из charter'а агента>
```
## Финальный отчёт триажа
Секции строго в этом порядке, потолок — 7 пунктов в первых двух:
1. `Блокирует мердж` (≤3, каждая с оракулом);
2. `Стоит исправить сейчас` (≤4);
3. `Гипотезы без доказательства` — что понижено и почему;
4. `Promote candidates` — кандидаты в конвенцию или правило линтера;
5. `Границы покрытия` — сводная, обязательная.
Перед секциями — сводка для человека: размер, сложность, метка и режим
прогона, состояние гейта, **план разметки задачи с исходом по каждой теме**,
сколько находок пришло на вход и сколько осталось.
**Реестр сводки — темы, а не проходы, и это не оформление.** Перечень запущенных
проходов отвечает «все, кто должен был, отработали» и молчит о том, что именно
осталось непроверенным: уехавший в старшую метку проход уносит тему с собой
беззвучно. План же называет тему, её дом, глубину и исполнителя — и тема,
оставшаяся без отчёта, видна сразу. Перечень проходов из сводки не исчезает, но
идёт **внутри** плана, колонкой «кто закрывает».
Каждая находка в секциях 1–2 несёт дополнительное поле:
```
- Действие: инлайн | развилка
```
`инлайн` — оркестратор чинит сам, не спрашивая и не логируя. `развилка` — цена
исправления сопоставима с переработкой, либо выбор меняет scope, либо решение
трогает инвариант: уезжает вопросом с вариантами и ценой каждого туда, где
проект держит вопросы, а работа продолжается на остатке.
Потребитель отчёта — оркестратор, который **реализует прочитанное**. Поэтому
потолок в 7 пунктов — не забота о внимании читателя, а защита кодовой базы от
правок, которых никто не заказывал.
@@ -0,0 +1,137 @@
# Откуда проход берёт проектную конкретику
Конвейер общий, находки — проектные. Проход, не знающий, что в этом проекте
нельзя нарушать, чем краснеет гейт и сколько данных реально идёт через узел,
выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать.
Отдельного файла-брифа **нет**. Проектная конкретика живёт в документах канона
`av-dev-docs`, и проход читает их напрямую: пути жёсткие, посредник не нужен, а
второй дом для тех же фактов разошёлся бы и выглядел актуальным.
Определение канона — в плагине `av-dev-docs`,
`skills/canon/references/canon.md`. Здесь только карта «тема → её дом → что
оттуда берётся».
## Карта тем
**Дом бывает файлом или каталогом**`docs/security.md` и `docs/security/`
называют одну и ту же тему. Форму дома называет план разметки задачи; проход её не
угадывает.
| Тема | Дом | Что оттуда берётся |
| --- | --- | --- |
| `requirements` | `openspec/specs/`, `openspec/changes/<id>/specs/` | нормативное поведение и дельты изменения |
| `autotests` | `CLAUDE.md`, семантика гейта | команда гейта, чем краснеет безусловно, чего в нём нет, кто гоняет дорогое |
| `conventions` | `docs/conventions.*` | конвенции прозой и **что уже механизировано** правилом |
| `architecture` | `docs/architecture.*` | компоненты и capability, единые точки проекта |
| | источник `docs/passport.*` | что система делает и **чего не делает**, граница домена |
| `security` | `docs/security.*` | периметр, недоверенный вход, из чего строятся пути и ключи, что вне модели |
| `operations` | `docs/architecture.*`, раздел эксплуатации | окружение, внешние зависимости поимённо, наблюдатель, характер потока |
| | источник `docs/database.*` | чем физически лежит запись, что при чтении и записи, настройки с числовым значением |
| *тема проекта* | её **свой** документ в `docs/` | то, что проект счёл нужным записать |
**`docs/adr.*` и `docs/research.*` в этой карте нет намеренно.** Они процессные
документы: прогон ревью их не открывает. Раньше первый питал тему `architecture`,
второй — `operations` и `requirements`; обе строки убраны, и цена этого названа в
`SKILL.md`, раздел «Честный предел».
**Дом темы зависит ещё и от метки.** На `small` темы `security`, `operations` и
`architecture` смотрятся не против домов из этой таблицы, а против **инвариантов
`CLAUDE.md`**, и закрывает их `code`. Таблица описывает полный дом темы; сколько
из него открыто на этом прогоне, говорит план разметки задачи.
Сквозное, не привязанное к теме:
| Что нужно проходу | Где лежит |
| --- | --- |
| инварианты **с severity рядом с формулировкой** | `CLAUDE.md``AGENTS.md`, если он рядом), раздел инвариантов |
| что запускать запрещено, с путями; `testdata`; куда писать временное; имя основной ветки | `CLAUDE.md` |
| типовые узлы, типовые ложноположительные, **вопросы по темам**, триггеры метки, недоступно проверке | `docs/review.*`, раздел настройки |
| прецеденты: воспроизведённые дефекты с оракулом | `docs/review.*`, журнал |
**Вопросы проекта привязаны к теме, а не к имени прохода.** Раньше блок в
`docs/review.md` адресовался поимённо (`ops: <вопрос>`), и когда проход уехал в
старшую метку, вопрос перестал задаваться молча. Тема переезд прохода
переживает.
## Сшивать обязаны проходы
Раньше эти факты лежали рядом в одном файле, и соседство работало само. Теперь
они разложены по домам, и **проход обязан собрать их сам** — иначе снимет верное
число и честно понизит находку до гипотезы, потому что сравнить будет не с чем.
Два обязательных стыка:
- **замер + настройка.** «Пик 768 МиБ» — аномалия только рядом со строкой
«запись лежит сжатой и распаковывается целиком»; «блокировка удерживалась
5.019 с» — гарантированный отказ соседа только рядом с известным таймаутом
занятости. **Число проход снимает сам, на этом прогоне**, настройки берёт из
`docs/database.md`, и сшивают их `ops` и `adversary`. Раньше числа брались из
`docs/research/`; теперь этот документ процессный, и замер неизвестной свежести
больше не выдаёт себя за оракул.
- **инвариант + обратимость.** severity берётся из `CLAUDE.md`; если её там
нет — она **выводится по обратимости последствия** и помечается «выведена по
обратимости», а не выдаётся за решение проекта.
**У `basics` стыков нет, и это не упущение.** Он не меряет, поэтому сшивать число
с настройкой ему нечего; единственное его основание для `critical` — инвариант из
`CLAUDE.md`, всё остальное он формулирует условиями и оставляет гипотезой. Его
вход намеренно узкий: дома тем из плана плюс инварианты и журнал. Широкий вход —
это метка `large`, и там он есть у `architecture`. Греп по базе ему разрешён
точечный — «есть ли второй вызывающий», — но обход всей базы и инвентарь
концепций не его работа.
**У `scope` стыков нет по другой причине: он не читает содержимого.** Его дело —
найти дома и раздать темы, а не пересказать написанное. Пересказ сделал бы его
посредником между документом и проходом, а посредник расходится с источником и при
этом выглядит актуальным.
## Деградация — поразрядная
Документа нет — деградирует то, что из него читалось, и **только оно**. Каждый
проход пишет **свою** строку в границы покрытия; триаж собирает их в один
список и **не сливает в одну строку**: разные пробелы чинятся разным — периметр
пишется руками за десять минут, а числа требуют замера.
**Кто какой документ читает — из документа не выводится, а назначается планом.**
Документ питает тему (это записано на стороне канона, таблица «Роли документов и
темы ревью»), а тему на этом прогоне закрывает тот, кого назвала разметка задачи; вся
раскладка «тема → проход → глубина» — в `SKILL.md` этого скилла и больше нигде.
**Списка читателей не ведёт никто, и это не пробел.** Он жил бы на стороне
канона, а документ живёт дольше, чем раскладка проходов: список разошёлся бы с
конвейером молча и при этом выглядел актуальным. Однажды уже разошёлся.
Ниже — только **последствие** отсутствия дома, и оно называет самое дорогое, а не
всех пострадавших.
| Нет дома | Что деградирует |
| --- | --- |
| `CLAUDE.md` без инвариантов | `critical` по основанию «нарушен инвариант проекта» не присваивается никем |
| `docs/security.*` | тема `security` остаётся без дома: вопросы задаются по коду, `critical` не ставится, периметр неизвестен |
| `docs/database.*` | замер не с чем сравнить: находка темы `operations` не поднимается выше гипотезы |
| `docs/passport.*` | тема `architecture` теряет границу домена и вырождается в общее мнение |
| `docs/review.*` | `triage` отсеивает вслепую: типовых ложноположительных нет; вопросы проекта по темам не задаются |
| `docs/conventions.*` | вторая половина `code` идёт вхолостую: записанных конвенций нет |
| `docs/architecture.*` | «не появился ли второй способ» не проверяется — единых точек не знает никто; тема `operations` теряет перечень внешних зависимостей |
Строка в границах покрытия обязана называть **причину**: «`docs/security.md` в
проекте нет» читается иначе, чем «есть, но периметр не назван». Без причины
строка неотличима от «мы просто не стали» и перестаёт читаться на третьей задаче.
**Документов канона нет вовсе** — проект не приведён к канону. Это не повод
работать вслепую: скажи об этом строкой и предложи `av-dev-docs:canon`. Одна
операция на проект против деградации на каждой задаче.
## Правило чтения
- **Читай в источнике, не по памяти.** Документы правятся по ходу работы, в том
числе этой же задачей.
- **Число без провенанса — условие, а не утверждение.** Число, чей источник по
ссылке не подтвердился, читается как условие и **называется расходящимся**, а
не подменяется догадкой.
- **Пустое, названное пустым, — это факт.** «Внешних зависимостей нет — смотри
на диск и на СУБД» экономит обязательный вопрос. Отсутствие строки — не факт,
а пробел, и его надо назвать в границах покрытия.
- **Свойство, ставшее правилом линтера, из конвенций удалено** и лежит в
перечне механизированного в `docs/conventions/README.md`. Проверять его
проходом — тратить внимание на уже проверенное.
@@ -0,0 +1,120 @@
# Промоут: находка → конвенция → правило → удаление
Механизм храповика. Без него конвейер выдаёт одни и те же находки бесконечно, а
конвенции не растут — то есть внимание тратится повторно на уже решённое.
Роли уровней:
- **generative-проходы** — механизм *открытия* неявного (дорого, шумно, но
только они достают то, чего нет в списках);
- **конвенции** — дешёвая *регрессионная сетка* на уже открытое;
- **правила линтера** — то же с детерминированным оракулом и нулевой ценой
внимания.
```mermaid
flowchart TD
f["находка ревью"]
cond{"принята и не специфична<br/>для одного места?"}
no["промоуту не подлежит:<br/>место одно — комментарий в коде;<br/>вкусовщина — вон на триаже;<br/>нужен рантайм — в журнал ревью"]
conv["конвенция:<br/>проверяемое свойство + какой проход нашёл"]
rule["правило линтера, запретитель,<br/>тест-сканер или анализатор"]
clean["шаг 3: формулировка удалена из конвенций,<br/>строка — в conventions/README.md"]
f --> cond
cond -->|нет| no
cond -->|да| conv
conv --> rule
rule --> clean
rule -->|"ложных чаще, чем ловит (~треть)"| conv
```
Ребро назад — обратное движение (внизу): правило, дающее ложные срабатывания
чаще, чем ловит, снимается в прозу. Ребро `rule → clean` **обязательное**: без
него первые два шага не окупаются, а именно его и пропускают.
Схема — **сводка**: условия каждого шага в его разделе, и при расхождении прав
текст.
## Шаг 1. Находка → конвенция
Условия: находка **принята** при ревью (не отвергнута, не понижена в гипотезу) и
**не специфична для одного места**.
- Формулируется как **проверяемое свойство**, а не как совет: «уровень доменного
отказа выбирает единственный логирующий чекпоинт», а не «внимательнее с
уровнями логов».
- Записывается источник — какой проход нашёл. Это единственные данные для
калибровки: проход, чьи находки регулярно доезжают до конвенции, оправдан;
проход, чьи находки не доезжают никогда, — кандидат на `drop` (см.
[calibration.md](calibration.md)).
- Место записи — конвенции проекта, файл или нужный файл каталога (путь — в
каталог `docs/conventions/`). Если
тема относится к поведению системы, а не к тому, как мы пишем код, — это не
конвенция, а требование: заводится дельта-спека обычным путём.
Промоут идёт **тем же путём, что change → spec**: правка попадает в тот же
коммит, что и исправление кода, с пометкой в сообщении — история промоутов
остаётся видна в `git log` по файлу конвенций.
## Шаг 2. Конвенция → правило
Как только свойство выражается детерминированно, оно переезжает в инструмент.
Порядок предпочтения — от дешёвого к дорогому:
1. **готовое правило существующего линтера** — включить в конфиг;
2. **запрет идентификатора или импорта** правилом-«запретителем» с собственным
паттерном;
3. **правило с настройкой формы** — когда важно не имя, а конструкция;
4. **тест-сканер исходников** — когда правило про структуру проекта или про
схему: направление зависимостей, форма миграций, матчинг ошибки по тексту,
бизнес-логика в транспорте;
5. **собственный анализатор** — последний рубеж, заводим только если 1–4 не
выражают правило.
Правило обязано быть **зелёным на текущем коде в момент включения**: иначе
хук блокирует любой коммит, и правило снимут первым же раздражённым движением.
Приводить код в соответствие — часть шага 2, отдельным коммитом.
## Шаг 3. Удаление из конвенций и из промптов
**Шаг, который пропускают чаще всего, и единственный, ради которого затевались
первые два.**
Как только правило работает:
- из файла конвенций убирается формулировка правила; остаётся, если нужно, одна
строка «проверяется линтером `<имя>`» — но только там, где без неё раздел
теряет связность;
- правило переезжает в **перечень механизированного в
`docs/conventions/README.md`** — со ссылкой на место механизации: конфиг
линтера, собственный анализатор, тест-сканер исходников. Не названное место
означает, что проход будет добросовестно проверять уже проверенное;
- из контекста инструмента спек убирается дубль, если он там был.
Charter'ы проходов при этом **не правятся**: они общие и живут в плагине, а
предмет проверки приходит из документов проекта. Именно поэтому шаг 3 дешевле,
чем был:
вычеркнуть строку в одном файле проекта, а не в девяти промптах.
Практический критерий: **в прозаических конвенциях остаётся только то, что
принципиально не выражается правилом.** Файл конвенций на несколько сотен строк
размазывает внимание модели по тривиальному — она добросовестно проверит
именование полей лога и не дойдёт до формы решения. Каждая строка конвенций,
которую можно было бы проверить машиной, оплачивается дефектом, который не
поймали где-то ещё.
## Обратное движение
Правило, которое даёт ложные срабатывания чаще, чем ловит (порядка трети от
общего числа), снимается и возвращается в прозу — или удаляется совсем, если
свойство перестало быть важным. Снятие фиксируется там же, где включалось, с
одной строкой «почему».
## Что промоуту не подлежит
- Находка, специфичная для одного места (её лечит комментарий в коде).
- Вкусовщина: не меняет поведения, не влияет на стоимость следующего изменения,
не нарушает записанного. Такое выбрасывается на триаже и не хранится.
- Свойство, требующее знания рантайма (профиль нагрузки, история инцидентов) —
его нельзя проверить ни промптом, ни линтером; место такому — в журнале ревью
как «признано неавтоматизируемым» (см. [review-journal.md](review-journal.md)).
@@ -0,0 +1,106 @@
# Журнал дефектов
Артефакт проекта, а не плагина: файл живёт в репозитории — **`docs/review.md`**,
слот канона `av-dev-docs`. Здесь описано, зачем он и какой формы, потому что без
него конвейер не учится: находки закрываются, а почему их не поймали — забывается,
и один и тот же класс проскакивает второй раз.
Тот же файл держит **настройку конвейера под проект** — типовые узлы, типовые
ложноположительные, вопросы по темам, недоступно проверке. Это не соседство по
случаю: все четыре раздела — производные калибровки, а журнал им источник.
## Что туда попадает
**Воспроизведённый дефект — с пометкой `проскочил` или `пойман ревью`.**
Записывается **сразу**, а не ретроспективно: со временем теряется не сам факт, а то,
почему дефект не поймали, — единственное, ради чего журнал существует.
Пометка делит журнал на две выборки с разным назначением:
- **проскочил** — проверочный набор для калибровки конвейера. Реальный промах сильнее
синтетической пробы: синтетические смещены в сторону тех, которые уже умеешь
придумывать;
- **пойман ревью** — прецеденты с оракулом. Самая сильная опора, какая у прохода
бывает: проектная, воспроизводимая и однажды уже оказавшаяся правдой. Без
журнала они остаются только в отчётах триажа в архиве change, где их никто не
ищет.
Реализованные задачи и принятые решения сюда не пишутся: у них есть коммит, спека
и `docs/adr/`.
Отдельно сюда попадают **решения о составе прогонов**: перестали звать проход,
понизили метку правилом, сузили класс проверяемого. Не потому, что это промах,
а потому, что здесь лежит цена: если что-то теперь проскочит, первый вопрос —
«не тот ли это класс, который мы перестали проверять».
Каждое такое решение обязано получить строку в подразделе **«Перестали проверять
сознательно»** раздела «Недоступно проверке» того же файла. Журнал хранит «почему
тогда так решили», раздел настройки — то, во что смотрит каждый прогон. Решение,
оставшееся только в журнале, в границы покрытия не доедет.
## Форма записи
**Это дом формы, и у него есть копия.** Скелет `docs/review.md`, который кладёт
в проект `av-dev-docs` (`skills/canon/references/skeletons.md`), повторяет её
дословно — он уезжает в репозиторий и обязан там что-то говорить. Правка формы
здесь **обязана** тянуть правку скелета и запись в журнал версий канона; иначе
проекты продолжат писать по старой форме, а конвейер — ждать поля, которого нет.
Дословность сверяет `scripts/copies.py` маркетплейса по маркерам ниже — но
запись в журнал версий он не проверит, это остаётся на человеке.
<!-- дом: журнал-дефектов-форма -->
```
## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман]
- **Где:** путь:строка либо «конвейер, а не код»
- **Симптом:** как обнаружилось, кем и когда
- **Причина:** что на самом деле было не так
- **Чем воспроизведён:** тест, команда, замер — с числами
- **Почему не поймали:** только для проскочивших — какой проход обязан был найти
и что ему помешало
- **Что меняем:** правило прохода, шаг гейта, конвенция, факт в документе
проекта — либо «ничего, цена поимки выше цены дефекта»
```
<!-- /дом: журнал-дефектов-форма -->
Пункт «чем воспроизведён» отличает запись от байки: без него на неё нельзя
сослаться как на оракул. Регрессионный тест, написанный вместе с починкой,
годится наравне с независимым экспериментом — он исполняемый и падает на старом
коде. Слабее он ровно в одном: сформулирован уже зная ответ, и это отмечается
словом.
Последний пункт важнее остальных. Вывод «ничего не меняем» — законный исход: не
всякий дефект стоит того, чтобы усложнять ради него ревью каждой задачи.
## Куда ведёт запись
Три адреса, и выбор между ними — половина ценности журнала:
- **в документ проекта** — если проход не мог знать факта. Адрес зависит от рода
факта, и карта их всех — [project-facts.md](project-facts.md):
настройка хранилища → `docs/database.md`;
что необратимо и какой шаг гейта красит безусловно → `CLAUDE.md`; периметр и
недоверенный вход → `docs/security.*`. **Вопрос по теме**, если промах лечится
не фактом, а заданным вопросом, → раздел «Вопросы по темам» того же
`docs/review.*`; адресуй теме, а не имени прохода — проход уедет между
метками, тема останется. Самый частый адрес и самый дешёвый. Прежде чем
править charter, проверь, не хватит ли факта или вопроса: charter общий для
всех проектов, документ — про этот.
- **в конвенции или в правило линтера** — если свойство выражается
детерминированно (процедура — [promote.md](promote.md)).
- **в charter прохода** — если сломан **метод**, а не знание. Правка charter'а
меняет поведение во всех проектах, поэтому она требует калибровки
([calibration.md](calibration.md)) и обоснования, почему это не лечится фактом
в документе проекта.
## Что журнал даёт конвейеру
- **пробы для калибровки** — выборка по пометке `проскочил`;
- **готовые оракулы** — выборка по пометке `пойман ревью`: находка того же
класса подтверждается ссылкой на запись, а не рассуждением;
- **основание для правил конвейера** — требование называть запущенные проходы
поимённо, отказ от чисел, производных от размера корпуса, и правило очереди для
меряющих проходов выведены из конкретных записей, а не из общих соображений;
- **счётчик обратимости решений** — сузили состав проходов и через месяц поймали
дефект ровно того класса, который перестали проверять: решение пересматривается
фактом, а не спором.
@@ -0,0 +1,165 @@
# Метки задачи — выбор, цена, доли
**Дом правила выбора метки.** Состав проходов по каждой метке, схема процесса и
раздача тем живут в [SKILL.md](../SKILL.md) — там диспетчер, и на готовой задаче
его достаточно. Здесь то, что читают, когда метку **выбирают, оспаривают или
калибруют**.
Применяет правило `review-scope` при разметке задачи — не автор изменения. Его
рабочая выжимка лежит в уставе агента; расходиться она с этим файлом не вправе, а
при расхождении прав этот.
## Правило выбора — две оси, а не один вопрос
**Оси две, они измеряют разное, и метка есть максимум по ним.**
| | **знакомое** — форму решения можно назвать до начала | **незнакомое** — форму предстоит нащупать по ходу |
|---|---|---|
| **малое** — один узел | `small` | `large` |
| **среднее** — несколько узлов одного слоя | `medium` | `large` |
| **крупное** — несколько слоёв, перенос ответственности, большой рефакторинг | `large` | `large` |
**Метка — не синоним размера.** Совпадают они только в левом верхнем углу: малое
**незнакомое** изменение получает `large`, трогая один узел. Поэтому в плане
стоят три строки, а не одна: размер, сложность и метка — каждая со своим
обоснованием. Проход, выведший объём диффа из метки, ошибётся ровно на этом
случае — а он и есть самый опасный: незнакомая форма в одном узле течёт там, где
её никто не ждёт.
**Размер** — про объём: сколько мест трогается. **Сложность** — про
неизвестность: знаем ли мы форму решения заранее. Признак незнакомого простой и
проверяемый: **перед работой нельзя назвать, какие узлы будут тронуты**.
Раньше обе оси были склеены в один вопрос «крупное **или** незнакомое?». Ответ
получался тот же, но две вещи под одним именем не измеришь по отдельности, и
потому разметка не могла сказать «изменение среднее, но совершенно знакомое» —
а именно эта пара и есть рабочее умолчание. Теперь обе оси называются в плане
поимённо, и обе — с обоснованием.
**Оси называются и на стадии дизайна, и на стадии кода — но считаются один
раз.** Это и есть причина, по которой разметка переехала к `propose`: состав
ревью дизайна выводится из той же пары, что и состав ревью кода, а считать её
дважды значит один раз посчитать без разведённости с автором.
**Обратимость — не третья ось, а отрицательный тест.** Она не уточняет размер и
не уточняет сложность: она запрещает нижнюю метку независимо от обеих.
**Отрицательный тест `small`, и он важнее положительного:** изменение, которое
после мерджа **не откатывается обратной правкой**, — не `small`, каким бы
маленьким ни был дифф. Сюда попадают миграция схемы и данных, формат на диске,
публичный контракт, имя, которое разойдётся по кодовой базе. Три строки миграции
— это `medium`, а не `small`: размер диффа и цена ошибки здесь расходятся.
Что здесь считается крупным, что — незнакомым и что — мелким, проект уточняет в
`docs/review.md`, подразделе «Триггеры метки»: **тремя списками** — по одному на
каждую ось вверх и один вниз, поимённо, узлами или capability. Это **уточнение**,
а не отмена: не записано — работает таблица выше.
## Спорный случай решается вниз, и у этого есть цена
Правило асимметрично, потому что асимметрична цена ошибки.
- **Спорно между `medium` и `large` → бери `medium`.** Ошибка в эту сторону
стоит находки, которая всплывёт на следующей задаче или в журнале дефектов.
Ошибка в обратную стоит трёх тяжёлых проходов, двое из которых держат машину и
идут цепочкой, — и платится она **на каждой** задаче, выбранной неверно.
- **Спорно между `small` и `medium` → бери `medium`.** Раньше эта строка
обосновывалась тем, что состав одинаков и ошибка почти бесплатна. Теперь состав
разный, и обоснование стало прямо противоположным: на `small` три темы ядра
смотрятся **только против записанных инвариантов**, а спорный случай — ровно тот,
где неизвестно, покрыт ли он инвариантом. Сомнение здесь стоит дороже, чем
раньше, и потому решается вниз тем более твёрдо.
**Выбор сделан в пользу пропускной способности, и это записано, а не подразумевается.**
Конвейер настроен на поток задач, а не на максимум находок с каждой: поправить в
следующей задаче дешевле, чем держать одну два часа. Отсюда три обязанности,
без которых сделка превращается в незаметную потерю качества:
- **границы покрытия называют темы и их глубину**, а не только запущенные
проходы — иначе `small` выглядит так же, как `large` без находок;
- **журнал дефектов в `docs/review.md` перестаёт быть хорошей практикой и
становится единственной обратной связью**: проскочивший дефект — единственный
сигнал, что метка выбрана слишком низко;
- **возврат в код — повод пересмотреть метку.** Задача, которая приходит в тот
же узел третий раз, уже не мелкая, чем бы ни выглядел её дифф.
## Метка — максимум по поверхности
**Обе оси меряются по всему диффу разом, и максимум по каждой отвечает за весь
дифф.** Метка изменения — не средневзвешенное: одна строка в перечне границ
задачи поднимает метку всему остальному, включая ту часть, которая сама по себе
была бы `small`.
Обратное тоже верно и тоже не бесплатно: у каждой задачи есть **несокращаемый
костяк — гейт, спеки, код, триаж**. Разрезать задачу, обе половины которой
остаются в одной метке, значит заплатить костяк дважды за ту же проверку.
Резать стоит там, где разрез **снимает доказательство с большей части диффа**.
Шов и правило нарезки живут у того, кто ведёт задачи, — скилл `av-dev-tasks:tasks`,
его `references/split.md`. Пути туда конвейер не выносит: за пределы своего
плагина он ходит вызовом скилла, а не файлом.
Разметка в костяк не входит — она платится один раз на задачу, а не один раз на
прогон, и потому **разрез задачи её не удваивает**. Это единственное, что стало
дешевле от переезда разметки к `propose`, и это же снимает прежний довод против
нарезки.
**Размер, сложность, метка и глубина объявляются в отчёте, и все четыре с
обоснованием.** Метка выбирает `review-scope`; он вправе и поднять, и понизить
её — но не молча: строка «метка X, потому что размер Y и сложность Z»
обязательна на каждом прогоне, а не только когда метка отличается от ожидаемой.
## Чем `small` дешевле `medium` и что это стоит
Экономят три рычага — непуск, вход, потолок, — и они общие для всех проходов и
всех меток; их дом и точные числа в [SKILL.md](../SKILL.md), раздел «Модель по
проходу». Здесь только то, что рычаги делают **с этой меткой**:
1. **Составом.** `basics` на `small` не запускается — кроме случая, когда у
проекта есть свои темы; тогда он идёт **только с ними**, ровно как в `large`.
Три темы ядра, которые он держал бы, переходят к `code` сверкой по
инвариантам.
2. **Входом.** На `small` `specs` читает только дельта-спеку, а `code` — только
**индекс** конвенций (перечень родов и что механизировано), не весь их дом. На
`medium` оба читают дома целиком.
3. **Потолком.** На `small` потолки самые жёсткие из трёх меток, и каждый
напечатан в границах покрытия своего прохода.
**Что `small` за это не проверяет, названо поимённо и обязано идти строкой в
границы покрытия:** темы `security`, `operations` и `architecture` смотрятся
только против **записанных инвариантов** `CLAUDE.md`. Свойство, которого в
инвариантах нет, с этой меткой не спросит никто — ни сценарием, ни чтением
дома темы. Это и есть цена метки, и она заметно больше прежней: раньше `small`
отличался от `medium` одним проходом на один вопрос, то есть не экономил
ничего и назывался отдельной меткой зря.
**`large` назван по тому, что он добавляет: вход шире диффа.** Он единственный, где
живут тяжёлые проходы, и единственный, где что-то **запускается**. `basics` в нём
берёт только проектные темы; своих тем у проекта нет — он не запускается вовсе, и
план говорит об этом строкой. **На `small` действует то же правило и по той же
причине** — приёмник запускается только тогда, когда ему есть что принимать.
Совпадение неслучайное: `basics` держит темы ядра ровно при одной метке из трёх,
а приёмником проектных тем работает на всех.
## Доли — не пожелание, а проверка правила, и проверок две
**Сверху: `large` — 510%.** Если туда уходит каждая третья задача, метку
выбирают по ощущению важности. Обратный перекос виден по журналу проскочивших
дефектов: класс, который ловят только меряющие проходы, начинает всплывать после
мерджа.
**Снизу: `small` не должен обгонять `medium`.** Ориентир — до трети задач, но
сравнение важнее числа: **перевес `small` над `medium` значит, что рабочее
умолчание сместилось, а решения об этом никто не принимал.** Проверка нужна
именно теперь: пока две нижние метки совпадали составом, дрейф между ними не
стоил ничего, и проверки не было. Сейчас он стоит трёх тем ядра, которые на
`small` смотрятся только против инвариантов, — то есть ровно того, чем `small` и
дёшев.
Считается это по журналу дефектов и по отчётам, а не по ощущению: метка
напечатана в каждом отчёте, и посчитать её за спринт — работа на минуту.
**У дрейфа вниз есть свой стимул, и его стоит назвать.** `small` дешевле по
времени и по деньгам, а выбирает метку хоть и не автор, но проход, читающий
описание, написанное автором. Занижённое описание даёт занижённую метку без
чьего-либо злого умысла — потому корректор и вынесен в `code`, который смотрит
уже на код, а не на описание.