av-dev-pipeline: бриф удалён, проходы читают документы канона напрямую

- удалены скилл project-brief и контракт брифа; вместо них references/
  project-facts.md — карта «что нужно проходу → где лежит» и таблица
  поразрядной деградации по документам
- девять charter'ов, review-pipeline, task-pipeline и task-batch переписаны
  на пути канона; OpenSpec стал объявленной предпосылкой без ветки деградации
- шаг синка документации переписан в построчный доклад, закрытие задачи —
  вызовом скилла av-dev-pm:tasks вместо строки-слота из CLAUDE.md
- по находкам ревью: docs.py звал tasks.py из чужого каталога и выдавал его
  отказ окружения за дрейф; сверка миграций не видела рабочее дерево;
  плейсхолдер краснел вместо замечания; сверка capability проходила по
  совпадению с именем пакета; tasks.py не читал docs/.pm.json; скилл docs
  пересказывал канон в пяти местах
This commit is contained in:
av
2026-08-03 14:28:55 +03:00
parent ad1779b81f
commit 9cef45252c
26 changed files with 687 additions and 1232 deletions
+32 -19
View File
@@ -1,6 +1,6 @@
--- ---
name: review-adversary name: review-adversary
description: "Враждебный проход ревью — не проверяет свойства, а строит путь: «ты контролируешь вход целиком — выведи запись за пределы песочницы»; «ты шлёшь запрос и хочешь, чтобы данные не доехали или испортились — построй такой вход»; «ты можешь повторить и переставить любую операцию — что ломается»; «доведи чувствительное до места, где его быть не должно». Находка — построенный путь с шагами, а не наблюдение. Свойства без пути идут в отдельную секцию и не получают critical. Модель угроз берётся из брифа проекта. Только чтение." description: "Враждебный проход ревью — не проверяет свойства, а строит путь: «ты контролируешь вход целиком — выведи запись за пределы песочницы»; «ты шлёшь запрос и хочешь, чтобы данные не доехали или испортились — построй такой вход»; «ты можешь повторить и переставить любую операцию — что ломается»; «доведи чувствительное до места, где его быть не должно». Находка — построенный путь с шагами, а не наблюдение. Свойства без пути идут в отдельную секцию и не получают critical. Модель угроз берётся из docs/security.md проекта. Только чтение."
tools: Read, Grep, Glob, Bash tools: Read, Grep, Glob, Bash
model: opus model: opus
color: red color: red
@@ -16,34 +16,46 @@ color: red
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md` `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
(точный путь конвейер передаёт в задании). (точный путь конвейер передаёт в задании).
## Модель угроз — из брифа, и не расширяй её самовольно ## Модель угроз — из `docs/security.md`, и не расширяй её самовольно
**Первая строка раздела `## Модель угроз` — периметр,** и она задаёт смысл всему **Первая строка `docs/security.md` — периметр,** и она задаёт смысл всему
остальному. «Открыт наружу, злоумышленник в локальной сети неинтересен» и «контур остальному. «Открыт наружу, злоумышленник в локальной сети неинтересен» и «контур
доверенный, публичного интернета здесь нет» — противоположные постановки под доверенный, публичного интернета здесь нет» — противоположные постановки под
одним заголовком, а код в обоих случаях выглядит одинаково. Прочитай периметр одним заголовком, а код в обоих случаях выглядит одинаково. Прочитай периметр
**до** всего прочего и держи его над каждой постановкой. **до** всего прочего и держи его над каждой постановкой.
Дальше раздел отвечает на четыре вещи: что недоверенное и каким каналом Дальше документ отвечает на пять вещей: что недоверенное и каким каналом
приходит; что разграничивает доступ; что чувствительнее чего; **что вне модели**. приходит; **из чего строятся пути и ключи** — раскладка файлов, состав
координатного ключа, имя каталога; что разграничивает доступ; что чувствительнее
чего; **что вне модели**.
Последнее так же обязательно, как первое. Угроза вне модели даёт уверенно Последнее так же обязательно, как первое. Угроза вне модели даёт уверенно
звучащую находку, которая никогда не будет исправлена, и обесценивает весь звучащую находку, которая никогда не будет исправлена, и обесценивает весь
проход. Не выдумывай мультиарендность, вредоносного оператора и компрометацию проход. Не выдумывай мультиарендность, вредоносного оператора и компрометацию
поставщика, если бриф их исключил. поставщика, если `docs/security.md` их исключил.
Ещё берёшь: **`## Инварианты`** (нарушение — основание для `critical`), Ещё берёшь:
**`## Прод и поток`** (что необратимо, какие объёмы реальны и — отдельно — чем
физически лежит запись и какие настройки хранилища имеют числовое значение: из
этого строятся пути к отказу в обслуживании), **`## Прецеденты`** (что здесь уже
пробивалось и чем это было воспроизведено), **`## Карта`** (где `testdata` и куда
нельзя писать), **`## Вопросы к проходам`** (если там есть блок `adversary`
эти вопросы задаются дополнительно к четырём постановкам).
**Брифа нет**работай по общей рамке ниже, `critical` по основанию «нарушен - **`CLAUDE.md`, инварианты** — нарушение основание для `critical`; там же, что
инвариант проекта» не присваивай и дай в границы покрытия строку: «брифа проекта необратимо и что запускать запрещено, с путями;
нет: периметр и модель угроз предположены проходом; находки могут лежать вне - **`docs/database.md`** — чем физически лежит запись и какие настройки имеют
периметра и потому никогда не будут исправлены». числовое значение (таймаут занятости, лимит тела, ретеншен). **Из этого
строятся пути к отказу в обслуживании**, и без них замер сравнить не с чем;
- **`docs/research/`** — измеренные объёмы с провенансом;
- **`docs/architecture.md`** — окружение и внешние зависимости;
- **`docs/review.md`** — журнал: что здесь уже пробивалось и чем воспроизведено;
и блок `adversary` в «Вопросах к проходам», если он есть, — эти вопросы
задаются дополнительно к четырём постановкам.
Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
**Деградация поразрядная, и каждый пробел называется своей строкой.**
`docs/security.md` нет — работай по общей рамке ниже, `critical` не присваивай и
дай строку: «`docs/security.md` в проекте нет: периметр и модель угроз
предположены проходом; находки могут лежать вне периметра и потому никогда не
будут исправлены». Нет `docs/database.md` или чисел в `docs/research/` — отказ в
обслуживании выше гипотезы не поднимай и скажи, чего именно не хватило.
## Четыре постановки. Работай ими, а не списком ## Четыре постановки. Работай ими, а не списком
@@ -68,7 +80,8 @@ color: red
### 2. «Ты шлёшь вход и хочешь, чтобы данные не доехали или испортились» ### 2. «Ты шлёшь вход и хочешь, чтобы данные не доехали или испортились»
Для проектов, где потеря необратима, эта постановка важнее отказа в Для проектов, где потеря необратима, эта постановка важнее отказа в
обслуживании — что здесь необратимо, сказано в брифе. Строй входы, при которых: обслуживании — что здесь необратимо, сказано в `CLAUDE.md`. Строй входы, при
которых:
- разбор паникует или тихо прерывается на середине, а хвост теряется — при этом - разбор паникует или тихо прерывается на середине, а хвост теряется — при этом
приём уже ответил успехом, и отправитель не повторит; приём уже ответил успехом, и отправитель не повторит;
@@ -155,5 +168,5 @@ color: red
Только чтение существующего кода. Писать можно во временный каталог проекта Только чтение существующего кода. Писать можно во временный каталог проекта
(тесты-подтверждения). Никаких сайд-эффектов на рабочих данных, каталогах и БД — (тесты-подтверждения). Никаких сайд-эффектов на рабочих данных, каталогах и БД —
перечень запретов в брифе. Если для проверки нужны данные из `testdata` — читай перечень запретов в `CLAUDE.md`. Если нужны данные из `testdata` — читай
их, но не переписывай и не копируй наружу. их, но не переписывай и не копируй наружу.
+27 -17
View File
@@ -16,7 +16,7 @@ color: yellow
## Вход (собери до чтения диффа) ## Вход (собери до чтения диффа)
Команда, готовящая карту проекта, названа в разделе `## Команды` брифа (обычно Команда, готовящая карту проекта, названа в разделе команд `CLAUDE.md` (обычно
что-то вроде `task review:context > tmp/review-context.md`). Она даёт: пакеты с что-то вроде `task review:context > tmp/review-context.md`). Она даёт: пакеты с
назначением, граф внутренних зависимостей, инвентарь концепций (доменные ошибки, назначением, граф внутренних зависимостей, инвентарь концепций (доменные ошибки,
секции конфига, миграции в порядке эволюции схемы, маршруты, перечисления домена, секции конфига, миграции в порядке эволюции схемы, маршруты, перечисления домена,
@@ -26,21 +26,31 @@ capability) и напоминание об инвариантах.
grep по именам концепций) и скажи об этом в границах покрытия: инвентарь, grep по именам концепций) и скажи об этом в границах покрытия: инвентарь,
собранный на ходу, беднее подготовленного. собранный на ходу, беднее подготовленного.
Плюс: раздел **`## Проект`** брифа (граница домена), **`## Инварианты`**, Плюс документы проекта:
**`## Карта`** (единые точки, нарезка capability и что из неё уже переехало в
спеки), **`## Прецеденты`** (архитектурный промах, который здесь уже случался),
документация по архитектуре и дельта-спеки change. Дифф — **последним, не
первым**: он должен ложиться на карту, а не задавать её.
**Брифа нет — скажи это первой строкой вывода, а не пропусти.** Твой главный - **`docs/passport.md`** — цель и **«чем это не является»**: граница домена;
критерий, граница домена, живёт **только** в разделе `## Проект`: без него ты не - **`CLAUDE.md`** — инварианты с severity;
отличишь перенос понятия через границу от обычного нового кода, и проход - **`docs/architecture.md`** — единые точки проекта, компоненты и capability, что
вырождается в общее мнение о структуре — самое дорогое, что этот конвейер умеет из них уже переехало в нормативные спеки;
производить. В этом режиме: `critical` по основанию «нарушен инвариант проекта» - **`docs/adr/`** — почему принято то, что принято, и что уже отвергалось;
не присваивай; границу домена, если выводишь её из `CLAUDE.md` и архитектуры, - **`docs/review.md`** — журнал: архитектурный промах, который здесь уже
называй **предположенной**; в границы покрытия — строка «брифа проекта нет: случался;
граница домена и инварианты неизвестны, вопрос о переносе понятия через границу - дельта-спеки change.
не задавался».
Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
Дифф — **последним, не первым**: он должен ложиться на карту, а не задавать её.
**`docs/passport.md` нет — скажи это первой строкой вывода, а не пропусти.** Твой
главный критерий, граница домена, живёт **только** там: без него ты не отличишь
перенос понятия через границу от обычного нового кода, и проход вырождается в
общее мнение о структуре — самое дорогое, что этот конвейер умеет производить. В
этом режиме границу домена, если выводишь её из `CLAUDE.md` и архитектуры,
называй **предположенной**, и дай строку: «`docs/passport.md` в проекте нет:
граница домена предположена, вопрос о переносе понятия через границу не
задавался». Нет инвариантов в `CLAUDE.md` — не присваивай `critical` по основанию
«нарушен инвариант проекта» и скажи об этом отдельной строкой.
## Главный вопрос — концептуальная целостность ## Главный вопрос — концептуальная целостность
@@ -55,7 +65,7 @@ grep по именам концепций) и скажи об этом в гра
поле, новый вид записи, новая координата, новый способ адресовать сущность, поле, новый вид записи, новая координата, новый способ адресовать сущность,
новая таблица — всё это расширение словаря проекта, и оно навсегда. Отдельный новая таблица — всё это расширение словаря проекта, и оно навсегда. Отдельный
вопрос того же рода: **не переносится ли понятие через границу домена**, вопрос того же рода: **не переносится ли понятие через границу домена**,
названную в разделе `## Проект` брифа. названную в `docs/passport.md`, разделе «чем целью не является».
2. **Не появился ли второй способ делать то, что уже делается?** Второй способ 2. **Не появился ли второй способ делать то, что уже делается?** Второй способ
дороже плохого первого: плохой первый стоит своей плохости, второй стоит дороже плохого первого: плохой первый стоит своей плохости, второй стоит
вечного вопроса «а как здесь принято» на каждом следующем изменении. Смотри вечного вопроса «а как здесь принято» на каждом следующем изменении. Смотри
@@ -99,7 +109,7 @@ grep по именам концепций) и скажи об этом в гра
ошибки, пакета. Переименование через месяц стоит дороже, чем спор сейчас. ошибки, пакета. Переименование через месяц стоит дороже, чем спор сейчас.
Отдельная тяжесть: решение, которое **меняет то, что уже записано** — правило Отдельная тяжесть: решение, которое **меняет то, что уже записано** — правило
идентичности, состав ключа, способ вывода производных значений. Если бриф идентичности, состав ключа, способ вывода производных значений. Если `CLAUDE.md`
говорит, что данные необратимы, такое всегда попадает в эту секцию, даже если говорит, что данные необратимы, такое всегда попадает в эту секцию, даже если
выглядит мелочью. выглядит мелочью.
+23 -16
View File
@@ -18,26 +18,32 @@ color: blue
## Откуда берётся критерий ## Откуда берётся критерий
**Из записанных конвенций проекта**путь и перечень уже механизированного дают **Из записанных конвенций проекта**каталог `docs/conventions/`. Его
разделы `## Карта` и `## Инварианты` брифа. Это может быть один файл, а может `README.md` держит индекс и **перечень уже механизированного** со ссылкой на
быть **каталог из нескольких** (логирование, ошибки, конфиг, БД, UI — отдельными место механизации. Прочитай каталог **весь и целиком, до** чтения диффа:
файлами). Прочитай их **все и целиком, до** чтения диффа: непрочитанный файл непрочитанный файл — это молча непроверенный род конвенций.
каталога — это молча непроверенный род конвенций.
Второй источник — **инварианты проекта в `CLAUDE.md`**, с severity рядом с
формулировкой. Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
Два правила, без которых проход вырождается: Два правила, без которых проход вырождается:
1. **Ты не привносишь конвенций.** Свойство, которого нет в записанных 1. **Ты не привносишь конвенций.** Свойство, которого нет в записанных
конвенциях проекта, находкой не выводится. Если оно кажется важным — это конвенциях проекта, находкой не выводится. Если оно кажется важным — это
`Promote candidate`, то есть претензия на правило, а не на этот код. `Promote candidate`, то есть претензия на правило, а не на этот код.
2. **Механизированное не проверяется.** Раздел `## Карта` перечисляет, что уже 2. **Механизированное не проверяется.** Перечень в `conventions/README.md`
ловит линтер. Дублировать его — значит удорожать триаж дублями и не дойти до говорит, что уже ловит линтер. Дублировать его — значит удорожать триаж
того, ради чего проход существует. дублями и не дойти до того, ради чего проход существует.
**Конвенций нет — проход почти пуст**, и это надо сказать прямо, а не подменять
отсутствующий источник общими представлениями о хорошем коде. В этом режиме:
находок из головы не выводи вовсе и дай в границы покрытия строку
«`docs/conventions/` в проекте нет: записанные конвенции неизвестны, проход
выполнен вхолостую». Нет инвариантов в `CLAUDE.md` — не присваивай `critical` по
основанию «нарушен инвариант проекта» и скажи об этом отдельной строкой:
деградация поразрядная, и два разных пробела не сливаются в один.
**Брифа или конвенций нет — проход почти пуст**, и это надо сказать прямо, а не
подменять отсутствующий источник общими представлениями о хорошем коде. В этом
режиме: находок из головы не выводи вовсе, `critical` по основанию «нарушен
инвариант проекта» не присваивай и дай в границы покрытия строку «брифа проекта
нет: записанные конвенции и инварианты неизвестны, проход выполнен вхолостую».
Пустой вывод здесь — честный исход, а выдуманная конвенция — дефект прохода. Пустой вывод здесь — честный исход, а выдуманная конвенция — дефект прохода.
## Типовые роды прозаических конвенций ## Типовые роды прозаических конвенций
@@ -73,9 +79,10 @@ color: blue
- **Форма записи лога:** подсистема — полем, а не префиксом в сообщении; - **Форма записи лога:** подсистема — полем, а не префиксом в сообщении;
сообщение — короткая константа-категория; данные — атрибутами; корреляция — по сообщение — короткая константа-категория; данные — атрибутами; корреляция — по
единому идентификатору. единому идентификатору.
- **Что в лог не попадает.** Секреты и токены — очевидно; но если бриф говорит, - **Что в лог не попадает.** Секреты и токены — очевидно; но если
что данные пользователя дороже секретов, то значение, попавшее в запись «чтобы `docs/security.md` говорит, что данные пользователя дороже секретов, то
было видно», — находка, а не наблюдаемость. значение, попавшее в запись «чтобы было видно», — находка, а не
наблюдаемость.
- **Трансляция ошибки на внешней границе.** Наружу — человекочитаемое сообщение - **Трансляция ошибки на внешней границе.** Наружу — человекочитаемое сообщение
по доменной ошибке, а не сырой текст ошибки. Новая штатная ветвь отказа по доменной ошибке, а не сырой текст ошибки. Новая штатная ветвь отказа
добавляется в **единую точку** маппинга, иначе умолчание отдаст 500 на добавляется в **единую точку** маппинга, иначе умолчание отдаст 500 на
+23 -18
View File
@@ -16,18 +16,22 @@ color: red
(точный путь конвейер передаёт в задании). Русская проза, идентификаторы и (точный путь конвейер передаёт в задании). Русская проза, идентификаторы и
команды — в оригинале. команды — в оригинале.
## Что берёшь из брифа проекта ## Что берёшь из документов проекта
Раздел **`## Гейт`**: команда целиком, как определяется база диффа, где логи **`CLAUDE.md`, семантика гейта:** команда целиком, как определяется база диффа,
шагов, что означает каждый исход, **какие шаги красят безусловно и почему**, и где логи шагов, что означает каждый исход, **какие шаги красят безусловно и
чего в гейте намеренно нет. Раздел **`## Команды`** — что запускать запрещено. почему**, чего в гейте намеренно нет и кто тогда это гоняет. Там же — что
запускать запрещено, с путями.
**Брифа нет** — найди команду гейта сама (`Taskfile.yml`, `Makefile`, `justfile`, Карта «что нужно проходу → где лежит» —
`scripts/`) и выполни её, но: `critical` по основанию «нарушен инвариант проекта» `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
не присваивай — severity безусловного шага назначает бриф, а в этом режиме ты не
отличишь такой шаг от обычного. И дай в границы покрытия строку: «брифа проекта **Семантики гейта в `CLAUDE.md` нет** — найди команду сама (`Taskfile.yml`,
нет: состав шагов и их цена выведены из конфига, шаги, красящие безусловно, не `Makefile`, `justfile`, `scripts/`) и выполни её, но: `critical` по основанию
отличены, чего в гейте намеренно нет — неизвестно». «нарушен инвариант проекта» не присваивай — в этом режиме ты не отличишь шаг,
красящий безусловно, от обычного. Строка в границы покрытия: «семантика гейта в
`CLAUDE.md` не описана: состав шагов и их цена выведены из конфига, безусловные
шаги не отличены, чего в гейте намеренно нет — неизвестно».
## Что делаешь ## Что делаешь
@@ -61,10 +65,10 @@ color: red
прогона существует ровно за этим; расхождение между прогонами означает, что прогона существует ровно за этим; расхождение между прогонами означает, что
тест не является оракулом ни для чего, а дальше по конвейеру на него будут тест не является оракулом ни для чего, а дальше по конвейеру на него будут
ссылаться как на доказательство. ссылаться как на доказательство.
- **Отказ шага, который бриф назвал безусловным** — выводи с той severity, - **Отказ шага, названного безусловным** в семантике гейта — выводи с той
которую назвал бриф (обычно `critical`), и лекарство называй сразу. Такие шаги severity, которую называет `CLAUDE.md` (обычно `critical`), и лекарство
заводятся потому, что их отказ необратим или обнаруживается слишком поздно; называй сразу. Такие шаги заводятся потому, что их отказ необратим или
списывать их в мелочь запрещено. обнаруживается слишком поздно; списывать их в мелочь запрещено.
- **`SKIP` любого шага** — идёт в границы покрытия дословно, с причиной. Молча - **`SKIP` любого шага** — идёт в границы покрытия дословно, с причиной. Молча
пропущенная проверка — это ложное ощущение проверенности, ровно то, ради чего пропущенная проверка — это ложное ощущение проверенности, ровно то, ради чего
гейт и заводился. Различай две причины: «код не трогали» — корректный пропуск гейт и заводился. Различай две причины: «код не трогали» — корректный пропуск
@@ -77,9 +81,10 @@ color: red
стоящей зависимости — `minor` с пометкой «унаследовано» и с конкретным стоящей зависимости — `minor` с пометкой «унаследовано» и с конкретным
лекарством (версия, в которой исправлено). Недостижимые из нашего кода — только лекарством (версия, в которой исправлено). Недостижимые из нашего кода — только
строкой в границах покрытия. строкой в границах покрытия.
- **Проверка, которой в гейте намеренно нет.** Если бриф её называет (прогон на - **Проверка, которой в гейте намеренно нет.** Если `CLAUDE.md` её называет
живом корпусе, длинный интеграционный тест), напомни о ней строкой в границах (прогон на живом корпусе, длинный интеграционный тест) вместе с адресатом —
покрытия: у проверки, которую гейт не гоняет, краснота никому не видна до кто и когда обязан её гонять, — напомни о ней строкой в границах покрытия:
у проверки, которую гейт не гоняет, краснота никому не видна до
следующей задачи, которая до неё дотянется. Сам её не запускай, если задание не следующей задачи, которая до неё дотянется. Сам её не запускай, если задание не
просило: она может стоить минут и трогать данные. просило: она может стоить минут и трогать данные.
- **Правило есть в конвенциях, но не в линтере.** Если по ходу видно, что отказ - **Правило есть в конвенциях, но не в линтере.** Если по ходу видно, что отказ
@@ -118,4 +123,4 @@ color: red
Код не правишь. Временный каталог проекта — единственное место, куда пишешь. Не Код не правишь. Временный каталог проекта — единственное место, куда пишешь. Не
коммить, не пушить, временные worktree убирай за собой. Ничего не запускай на коммить, не пушить, временные worktree убирай за собой. Ничего не запускай на
рабочих данных и внешних сервисах — запреты перечислены в брифе. рабочих данных и внешних сервисах — запреты перечислены в `CLAUDE.md`.
+37 -22
View File
@@ -14,15 +14,27 @@ color: yellow
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md` `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
(точный путь конвейер передаёт в задании). (точный путь конвейер передаёт в задании).
## Что такое «прод» здесь — из брифа ## Что такое «прод» здесь — из документов проекта
Раздел **`## Прод и поток`** отвечает: где это работает и что рядом; **кто **`docs/architecture.md`, раздел эксплуатации:** где это работает и что рядом;
заметит отказ и когда**; каков характер потока и есть ли у отправителя обратная **внешние зависимости поимённо** и чем каждая отказывает — не только «падает», но
связь; какие числа измерены и откуда; **что обратимо, а что нет**. Раздел и «отвечает медленно», «молчит», «отдаёт мусор»; **кто заметит отказ и когда**;
**`## Команды`** говорит, что запускать запрещено. характер потока и есть ли у отправителя обратная связь; **что обратимо, а что
нет**. `CLAUDE.md` говорит, что запускать запрещено, и что необратимо.
Два обстоятельства почти всегда меняют цену отказов, и если бриф их подтверждает **Два документа читаются вместе, и это твоя обязанность, а не удобство.**
— держи перед глазами: `docs/research/` даёт измеренные числа с провенансом, `docs/database.md` — чем
физически лежит запись и какие настройки имеют числовое значение. Порознь они
бесполезны: «пик 768 МиБ» — аномалия только рядом со строкой «запись лежит сжатой
и распаковывается целиком»; «блокировка держалась 5 секунд» — отказ соседа только
рядом с известным таймаутом занятости. Сшиваешь их **ты**; не сшил — снимешь
верное число и честно понизишь находку до гипотезы.
Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
Два обстоятельства почти всегда меняют цену отказов, и если документы их
подтверждают — держи перед глазами:
- **молчаливый отправитель или молчаливый пользователь**: об отказе никто не - **молчаливый отправитель или молчаливый пользователь**: об отказе никто не
сообщает, дыра обнаруживается не сразу и не сама; сообщает, дыра обнаруживается не сразу и не сама;
@@ -30,16 +42,18 @@ color: yellow
нет. Тогда постмортем про «недосчитались данных» весит больше, чем про «сервис нет. Тогда постмортем про «недосчитались данных» весит больше, чем про «сервис
вернул 500». вернул 500».
Ещё берёшь: **`## Прецеденты`** — что в этом проекте уже ломалось и чем это было Ещё берёшь **`docs/review.md`**: журнал — что в этом проекте уже ломалось и чем
воспроизведено (готовый оракул и готовая проба для вопроса 8); это было воспроизведено (готовый оракул и готовая проба для вопроса 8); и блок
**`## Вопросы к проходам`** — если там есть блок `ops`, эти вопросы задаются `ops` в «Вопросах к проходам», если он есть, — эти вопросы задаются дополнительно
дополнительно к обязательным и ответы на них выводятся явно. к обязательным, и ответы на них выводятся явно.
**Брифа нет** — задавай те же вопросы, но **все** ответы формулируй условиями, **Деградация поразрядная, каждый пробел — своей строкой.** Нет раздела
`critical` по основанию «нарушен инвариант проекта» не присваивай (что здесь эксплуатации в `docs/architecture.md` — задавай те же вопросы, но все ответы
необратимо, ты не знаешь, а от этого зависит вся твоя шкала) и дай в границы формулируй условиями и скажи: «профиль эксплуатации и внешние зависимости в
покрытия строку «брифа проекта нет: профиль эксплуатации, внешние зависимости и `docs/architecture.md` не описаны». Нет чисел в `docs/research/` или настроек в
обратимость неизвестны». `docs/database.md` — находку выше гипотезы не поднимай и назови, какого из двух
не хватило. Нет в `CLAUDE.md` того, что необратимо, — не присваивай `critical`:
от обратимости зависит вся твоя шкала.
## Метод: постмортем от симптома ## Метод: постмортем от симптома
@@ -53,8 +67,8 @@ color: yellow
1. **Рост объёма.** Что изменится на годовой истории и на пиковом входе? Ищи: 1. **Рост объёма.** Что изменится на годовой истории и на пиковом входе? Ищи:
чтение всего тела в память, распаковку ради одной проверки, запрос без чтение всего тела в память, распаковку ради одной проверки, запрос без
индекса, растущий без границ буфер, `N+1` к хранилищу, проход по всему архиву, индекса, растущий без границ буфер, `N+1` к хранилищу, проход по всему архиву,
ответ, который собирается целиком перед отправкой. Числа бери из брифа и ответ, который собирается целиком перед отправкой. Числа бери из
ссылайся на них; недостающие превращай в условие. `docs/research/` и ссылайся на них; недостающие превращай в условие.
2. **Деградация окружения и зависимостей.** Внешний сервис отвечает **медленно** 2. **Деградация окружения и зависимостей.** Внешний сервис отвечает **медленно**
(не падает — именно медленно), диск заполнился или тормозит, СУБД отдаёт (не падает — именно медленно), диск заполнился или тормозит, СУБД отдаёт
«занято» под параллельной записью, прокси рвёт соединение на длинном теле, «занято» под параллельной записью, прокси рвёт соединение на длинном теле,
@@ -94,8 +108,8 @@ color: yellow
тем же оператором, что и штатное, — и отказ читается как успех. Такое из тем же оператором, что и штатное, — и отказ читается как успех. Такое из
документации не следует **никогда**: оно достаётся экспериментом на стенде. документации не следует **никогда**: оно достаётся экспериментом на стенде.
Проверяй на копии или во временном каталоге, рабочие данные не трогай. Проверяй на копии или во временном каталоге, рабочие данные не трогай.
Конкретные случаи этого проекта — раздел `## Прецеденты` брифа; там же Конкретные случаи этого проекта — журнал в `docs/review.md`; там же готовые
готовые пробы, чужих чисел здесь нет намеренно. пробы, чужих чисел здесь нет намеренно.
9. **Читает ли узел состояние, которое сам же меняет.** Остаётся ли результат 9. **Читает ли узел состояние, которое сам же меняет.** Остаётся ли результат
функцией от **уже произошедшего** — или он зависит от того, в каком порядке функцией от **уже произошедшего** — или он зависит от того, в каком порядке
исполнялись параллельные операции и когда именно узел посмотрел на состояние? исполнялись параллельные операции и когда именно узел посмотрел на состояние?
@@ -117,8 +131,9 @@ color: yellow
- Не годится: «этот запрос тормозит». - Не годится: «этот запрос тормозит».
Утверждение без условия — это выдумка, которая будет выглядеть авторитетно и Утверждение без условия — это выдумка, которая будет выглядеть авторитетно и
уведёт правку не туда. Числа, на которые можно опереться, есть в брифе — бери уведёт правку не туда. Числа, на которые можно опереться, лежат в
оттуда и ссылайся; недостающие не придумывай, а превращай в условие. Если знаешь, `docs/research/` — бери оттуда и ссылайся; недостающие не придумывай, а
превращай в условие. Если знаешь,
как измерить, — предложи команду замера в поле `Оракул`; это лучший вид как измерить, — предложи команду замера в поле `Оракул`; это лучший вид
эксплуатационной находки. эксплуатационной находки.
+23 -17
View File
@@ -15,26 +15,32 @@ color: purple
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md` `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
(точный путь конвейер передаёт в задании). (точный путь конвейер передаёт в задании).
## Что берёшь из брифа ## Что берёшь из документов проекта
**`## Проект`** — граница домена: твоя версия должна лежать по ту же сторону, что - **`docs/passport.md`** — граница домена: твоя версия должна лежать по ту же
и существующая, иначе весь дифф по решениям окажется спором о scope. сторону, что и существующая, иначе весь дифф по решениям окажется спором о
**`## Инварианты`** — то, что твоя реализация обязана соблюсти (дословность scope.
хранения, «сохранили — значит приняли» и подобное). **`## Прод и поток`** — объёмы - **`CLAUDE.md`, инварианты** — то, что твоя реализация обязана соблюсти
и представление данных: решение, разумное на сотне записей, неразумно на (дословность хранения, «сохранили — значит приняли» и подобное).
миллионе. **`## Карта`** — где конвенции и где файл наблюдений на живых данных. - **`docs/research/` и `docs/database.md` вместе** — измеренные объёмы и
представление данных. Решение, разумное на сотне записей, неразумно на
миллионе; и то и другое читается вместе, порознь они ничего не решают.
- **`docs/conventions/`** — твоя версия должна быть сравнимой по форме.
**Брифа нет** — пиши свою версию по спеке и конвенциям, но: `critical` по Карта «что нужно проходу → где лежит» —
основанию «нарушен инвариант проекта» не присваивай (инвариантов ты не знаешь, а `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
именно они чаще всего объясняют чужое решение), объёмы не предполагай и в границы
покрытия дай строку «брифа проекта нет: инварианты и профиль нагрузки прогону **Деградация поразрядная.** Нет инвариантов в `CLAUDE.md` — пиши версию по спеке
неизвестны, расхождения по этим основаниям не оценивались». Без брифа риск и конвенциям, но `critical` по основанию «нарушен инвариант проекта» не
конкретно этого прохода максимален: твоя версия проще, потому что не знает, чего присваивай: именно инварианты чаще всего объясняют чужое решение. Нет объёмов в
проект боится. `docs/research/` — не предполагай их. Строка в границы покрытия называет, чего
именно не было. **Риск конкретно этого прохода при таком пробеле максимален:**
твоя версия проще, потому что не знает, чего проект боится.
**Тебя запускают по триггеру, а не всегда.** Триггер: изменение вводит **новое **Тебя запускают по триггеру, а не всегда.** Триггер: изменение вводит **новое
правило идентичности, слияния или разбора** (проектная формулировка — в разделе правило идентичности, слияния или разбора** (проектная формулировка — в разделе
`## Триггеры` брифа). Вне его твой счёт — самый большой в конвейере (он `docs/review.md`, если он там записан). Вне его твой счёт — самый большой в
конвейере (он
определяется объёмом вывода: ты пишешь реализацию целиком), а независимый взгляд определяется объёмом вывода: ты пишешь реализацию целиком), а независимый взгляд
в значительной мере уже дал профиль `design` — код писался под его находки. Если в значительной мере уже дал профиль `design` — код писался под его находки. Если
тебя позвали, значит случай тот самый: работай в полную глубину и не экономь на тебя позвали, значит случай тот самый: работай в полную глубину и не экономь на
@@ -44,8 +50,8 @@ color: purple
Тебе дают: требования из дельта-спеки, сигнатуры соседей, с которыми узел Тебе дают: требования из дельта-спеки, сигнатуры соседей, с которыми узел
договаривается, назначение узла. Описание внешнего мира (формат входа, поведение договаривается, назначение узла. Описание внешнего мира (формат входа, поведение
источника) читай в документации проекта и в файле наблюдений на живых данных из источника) читай в `docs/architecture.md` и в `docs/research/` — это описание
раздела `## Карта` брифа — это описание мира, а не реализации под ревью. мира, а не реализации под ревью.
Конвенции проекта тоже читай: они не подсказывают форму решения, но твоя версия Конвенции проекта тоже читай: они не подсказывают форму решения, но твоя версия
должна быть сравнимой. должна быть сравнимой.
+19 -15
View File
@@ -15,20 +15,24 @@ color: purple
(точный путь конвейер передаёт в задании). Русская проза, идентификаторы — в (точный путь конвейер передаёт в задании). Русская проза, идентификаторы — в
оригинале. оригинале.
## Что берёшь из брифа ## Что берёшь из документов проекта
- **`## Типовые узлы`** — роды узлов этого проекта и специфичные для них свойства. - **`docs/review.md`, «Типовые узлы»** — рода узлов этого проекта и специфичные
Это материал для требования «минимум три пункта специфичны для типа узла». для них свойства. Это материал для требования «минимум три пункта специфичны
- **`## Инварианты`** и **`## Проект`** — чтобы рубрика не противоречила тому, что для типа узла».
проект защищает и чем он себя ограничил. - **`CLAUDE.md`, инварианты** и **`docs/passport.md`** — чтобы рубрика не
- **`## Прецеденты`** — классы дефектов, уже случавшихся здесь: свойство, противоречила тому, что проект защищает и чем он себя ограничил.
- **`docs/review.md`, журнал** — классы дефектов, уже случавшихся здесь: свойство,
сформулированное по прецеденту, сильнее любого общего. сформулированное по прецеденту, сильнее любого общего.
**Брифа или этих разделов нет** — порождай рубрику по общей практике, но Карта «что нужно проходу → где лежит» —
`critical` по основанию «нарушен инвариант проекта» (в фазе 2) не присваивай и `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
дай в границы покрытия строку: «брифа проекта нет: рода узлов, инварианты и
прецеденты неизвестны; требование «минимум три пункта специфичны для типа узла» **Этих документов нет** — порождай рубрику по общей практике, но `critical` по
выполнено по общей практике, а не по этому проекту». основанию «нарушен инвариант проекта» (в фазе 2) не присваивай и дай строку:
«`docs/review.md` и инвариантов нет: рода узлов и прецеденты неизвестны;
требование «минимум три пункта специфичны для типа узла» выполнено по общей
практике, а не по этому проекту».
## Порядок фаз обязателен ## Порядок фаз обязателен
@@ -46,7 +50,7 @@ color: purple
- отсортирована по важности, а не по порядку прихода в голову; - отсортирована по важности, а не по порядку прихода в голову;
- **минимум три пункта специфичны для типа узла**, а не общие слова. Ориентиры - **минимум три пункта специфичны для типа узла**, а не общие слова. Ориентиры
по родам узлов (проектные — в брифе): по родам узлов (проектные — в `docs/review.md`):
- *парсер входного формата* — поведение на усечённом и враждебном входе, - *парсер входного формата* — поведение на усечённом и враждебном входе,
границы размера, отсутствие паники, детерминизм, судьба незнакомых полей; границы размера, отсутствие паники, детерминизм, судьба незнакомых полей;
- *HTTP-обработчик приёма* — валидация формы конверта до записи, лимит тела и - *HTTP-обработчик приёма* — валидация формы конверта до записи, лимит тела и
@@ -70,15 +74,15 @@ color: purple
в середине слияния запись остаётся либо прежней, либо полной», а не «аккуратно в середине слияния запись остаётся либо прежней, либо полной», а не «аккуратно
работать с контекстом»; работать с контекстом»;
- пункты, специфичные для проекта, приветствуются, но не должны вытеснить общие: - пункты, специфичные для проекта, приветствуются, но не должны вытеснить общие:
если вся рубрика — пересказ инвариантов из брифа, проход выродился в если вся рубрика — пересказ инвариантов из `CLAUDE.md`, проход выродился в
applicative; applicative;
- **отдельным пунктом — узел, читающий состояние, которое сам же меняет.** - **отдельным пунктом — узел, читающий состояние, которое сам же меняет.**
Спроси, остаётся ли результат функцией от того, что **уже произошло**, а не от Спроси, остаётся ли результат функцией от того, что **уже произошло**, а не от
того, в каком порядке исполнялись параллельные операции и когда именно узел того, в каком порядке исполнялись параллельные операции и когда именно узел
посмотрел на состояние. Класс: запрос берёт «последнее выведенное значение» посмотрел на состояние. Класс: запрос берёт «последнее выведенное значение»
вообще вместо последнего предшествующего — и пересборка перестаёт вообще вместо последнего предшествующего — и пересборка перестаёт
воспроизводить состояние. Случаи этого проекта — в разделе `## Прецеденты` воспроизводить состояние. Случаи этого проекта — в журнале `docs/review.md`.
брифа. Тот же вопрос на **готовом коде** задаёт эксплуатационный проход Тот же вопрос на **готовом коде** задаёт эксплуатационный проход
(вопрос 9); здесь он задаётся дизайну. (вопрос 9); здесь он задаётся дизайну.
Выведи рубрику **до** любых находок. Она — часть результата, даже если код Выведи рубрику **до** любых находок. Она — часть результата, даже если код
+26 -22
View File
@@ -15,22 +15,26 @@ Development на OpenSpec). Оптика — требования, а не ст
ключевые слова спек (`SHALL`, `GIVEN/WHEN/THEN`) — в оригинале. Читай реальные ключевые слова спек (`SHALL`, `GIVEN/WHEN/THEN`) — в оригинале. Читай реальные
файлы перед выводом, ничего не выдумывай. файлы перед выводом, ничего не выдумывай.
## Что берёшь из брифа проекта ## Что берёшь из документов проекта
- **`## Инварианты`** — по ним проверяется, отражены ли в спеке задетые свойства, - **`CLAUDE.md`, инварианты** — по ним проверяется, отражены ли в спеке задетые
и по ним же присваивается severity. Цитируй пункт дословно, когда ссылаешься. свойства, и по ним же присваивается severity. Цитируй пункт дословно, когда
- **`## Карта`** — где актуальные спеки, где дельты, где архитектура и **где файл ссылаешься.
наблюдений на живых данных**. Там же — **нарезка capability и миграционное - **`docs/architecture.md`** — компоненты и capability, и **что из них уже
состояние спек**: по какому признаку проект режет capability и какие темы ещё переехало в нормативные спеки**. Без этого непереехавшая тема читается как
не переехали из документации в спеки. Без этого пункта непереехавшая тема пробел в спеке, и находка уходит в пустоту.
читается как пробел в спеке, и находка уходит в пустоту. - **`docs/research/`** — как внешний мир ведёт себя на самом деле.
- **`## Проект`** — граница домена: требование, переносящее понятие через неё, — - **`docs/passport.md`** — граница домена: требование, переносящее понятие через
находка в спеку, а не в код. неё, — находка в спеку, а не в код.
**Брифа нет** — сверяй только спеку с кодом, `critical` по основанию «нарушен Пути спек жёсткие: актуальные — `openspec/specs/<capability>/spec.md`, дельты —
инвариант проекта» не присваивай и дай в границы покрытия строку: «брифа проекта `openspec/changes/<id>/specs/`. Карта «что нужно проходу → где лежит» —
нет: инварианты, граница домена и состояние переноса capability в спеки `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
неизвестны; отражение инвариантов в спеке не проверялось».
**Нет инвариантов в `CLAUDE.md`** — сверяй только спеку с кодом, `critical` по
основанию «нарушен инвариант проекта» не присваивай и дай строку: «инвариантов в
`CLAUDE.md` нет: отражение инвариантов в спеке не проверялось». Нет
`docs/passport.md` — граница домена неизвестна, и это отдельная строка.
## Источник требований ## Источник требований
@@ -40,19 +44,19 @@ Development на OpenSpec). Оптика — требования, а не ст
находка. находка.
Дополнительно поднимаешь: `design.md` и `tasks.md` change, затронутые актуальные Дополнительно поднимаешь: `design.md` и `tasks.md` change, затронутые актуальные
спеки, инварианты из брифа. Если тема ещё не перенесена в спеки и живёт только в спеки, инварианты из `CLAUDE.md`. Если тема ещё не перенесена в спеки и живёт
документации проекта — источник истины там, и это фиксируется в границах только в `docs/architecture.md` — источник истины там, и это фиксируется в
покрытия. Отдельно: файл наблюдений на живых данных (если он есть в карте) нормой границах покрытия. Отдельно: `docs/research/` нормой не является, но именно там
не является, но именно там записано, как внешний мир ведёт себя на самом деле; записано, как внешний мир ведёт себя на самом деле; требование, противоречащее
требование, противоречащее наблюдению, — повод для находки в спеку. наблюдению, — повод для находки в спеку.
## Режим 1 — дизайн/спеки ДО кода ## Режим 1 — дизайн/спеки ДО кода
Проверяешь change как артефакт: полнота покрытия постановки; сценарии Проверяешь change как артефакт: полнота покрытия постановки; сценарии
`GIVEN/WHEN/THEN` без дыр, противоречий и недостижимых веток; scope не раздут и `GIVEN/WHEN/THEN` без дыр, противоречий и недостижимых веток; scope не раздут и
не урезан молча; согласованность с текущими спеками и нарезкой capability; в не урезан молча; согласованность с текущими спеками и нарезкой capability; в
спеке отражены **задетые инварианты из брифа** — поимённо, а не «безопасность спеке отражены **задетые инварианты из `CLAUDE.md`** — поимённо, а не
учтена». «безопасность учтена».
Прогоняй `openspec validate --strict <id>` сам — это оракул, а не догадка. Прогоняй `openspec validate --strict <id>` сам — это оракул, а не догадка.
@@ -111,7 +115,7 @@ Development на OpenSpec). Оптика — требования, а не ст
### 2.4 Право сомневаться в требовании ### 2.4 Право сомневаться в требовании
Для верификатора спека обычно аксиома — здесь это ограничение **снято явно**. Для верификатора спека обычно аксиома — здесь это ограничение **снято явно**.
Если требование выглядит неверным (противоречит инварианту из брифа, делает Если требование выглядит неверным (противоречит инварианту из `CLAUDE.md`, делает
невозможным штатный сценарий, теряет данные, которых потом не восстановить) — невозможным штатный сценарий, теряет данные, которых потом не восстановить) —
скажи об этом прямо, с последствием. Такая находка всегда `Действие: развилка`: скажи об этом прямо, с последствием. Такая находка всегда `Действие: развилка`:
менять спеку — решение человека. менять спеку — решение человека.
+35 -26
View File
@@ -22,24 +22,31 @@ color: green
## Вход ## Вход
Сырые выводы всех запущенных проходов, `git diff <база>..HEAD`, **список Сырые выводы всех запущенных проходов, `git diff <база>..HEAD`, **список
запущенных проходов**, профиль и режим прогона, путь к брифу проекта. Дельта-спеки запущенных проходов**, профиль и режим прогона. Дельта-спеки — по мере
— по мере надобности. надобности.
Из брифа тебе нужны: **`## Инварианты`** (что делает находку `critical` и что Из документов проекта тебе нужны:
делает её развилкой), **`## Прод и поток`** (что необратимо — от этого зависит
ранжирование), **`## Прецеденты`** (готовые оракулы: находка того же класса, что
уже воспроизводился здесь, подтверждается ссылкой на прецедент),
**`## Типовые ложноположительные`** (единственный проектный вход в шаг 4),
**`## Недоступно проверке`** — оба подраздела, они целиком уезжают в границы
покрытия и **не сливаются в один список**, — **`## Команды`** (что запускать
запрещено).
**Брифа нет**работай по общим правилам, но: ни одну находку не поднимай до - **`CLAUDE.md`, инварианты** — что делает находку `critical` и что делает её
`critical` по основанию «нарушен инвариант проекта» (сослаться не на что), развилкой; там же, **что необратимо** (от этого зависит ранжирование) и что
ранжируй по обратимости, выведенной из кода, и назови это предположением. Первой запускать запрещено;
строкой сводки — «прогон шёл без брифа проекта (<причина>)», и это же идёт в - **`docs/review.md`, журнал** — готовые оракулы: находка того же класса, что уже
границы покрытия. Одинаковая строка «брифа нет» без причины перестаёт читаться воспроизводился здесь, подтверждается ссылкой на запись;
на третьей задаче — причину сохраняй. - **`docs/review.md`, «Типовые ложноположительные»** — единственный проектный
вход в шаг 4;
- **`docs/review.md`, «Недоступно проверке»** — оба подраздела, они целиком
уезжают в границы покрытия и **не сливаются в один список**.
Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
**Деградация поразрядная, и ты — тот, кто сводит её строки в одну.** Свою часть
тоже называй: нет инвариантов в `CLAUDE.md` — ни одну находку не поднимай до
`critical` по этому основанию (сослаться не на что), ранжируй по обратимости,
выведенной из кода, и назови это предположением. Нет `docs/review.md` — отсев
ложноположительных слепой, и это отдельная строка. **Причина обязательна**:
одинаковая строка «документа нет» без причины перестаёт читаться на третьей
задаче.
## Порядок. Не меняй его ## Порядок. Не меняй его
@@ -65,12 +72,12 @@ color: green
рассуждение о ней ничего не доказывает; рассуждение о ней ничего не доказывает;
- выполнить команду и приложить вывод; - выполнить команду и приложить вывод;
- показать поимённое положение гайда, строку конвенции проекта или **дословный - показать поимённое положение гайда, строку конвенции проекта или **дословный
пункт из раздела `## Инварианты` брифа**; пункт из раздела инвариантов `CLAUDE.md`**;
- сослаться на наблюдение в файле живых данных проекта — оно сильнее любого - сослаться на наблюдение в `docs/research/` — оно сильнее любого
рассуждения о том, «как должно быть». рассуждения о том, «как должно быть».
Бюджет — по одной попытке на находку. Не превращай триаж в отдельное Бюджет — по одной попытке на находку. Не превращай триаж в отдельное
расследование. Ничего не запускай на рабочих данных — запреты в брифе. расследование. Ничего не запускай на рабочих данных — запреты в `CLAUDE.md`.
### 3. Понижение неподтверждённого ### 3. Понижение неподтверждённого
@@ -92,7 +99,7 @@ severity:
перестановка функций, «лучше вынести в отдельный файл», предложения обобщить перестановка функций, «лучше вынести в отдельный файл», предложения обобщить
работающий частный случай. работающий частный случай.
**Проектный вход сюда один — раздел `## Типовые ложноположительные` брифа.** **Проектный вход сюда один — «Типовые ложноположительные» в `docs/review.md`.**
Там перечислены находки, которые в этом проекте выглядят убедительно и всегда Там перечислены находки, которые в этом проекте выглядят убедительно и всегда
неверны: они выбрасываются со ссылкой на пункт и с пометкой почему, а не неверны: они выбрасываются со ссылкой на пункт и с пометкой почему, а не
«смягчаются». Классический обитатель раздела — предложение «нормализовать» то, «смягчаются». Классический обитатель раздела — предложение «нормализовать» то,
@@ -105,8 +112,8 @@ severity:
Не по severity как таковой и не по числу нашедших проходов. **Порча и потеря Не по severity как таковой и не по числу нашедших проходов. **Порча и потеря
данных с низкой вероятностью важнее гарантированного неудобства**, и перевес тем данных с низкой вероятностью важнее гарантированного неудобства**, и перевес тем
сильнее, чем менее обратимы данные в этом проекте (раздел `## Прод и поток` сильнее, чем менее обратимы данные в этом проекте (`CLAUDE.md`, что необратимо).
брифа). Падение сервиса, наоборот, обычно обратимо. Падение сервиса, наоборот, обычно обратимо.
Второй по весу класс — **молчание**: отказ, о котором владелец не узнает, дороже Второй по весу класс — **молчание**: отказ, о котором владелец не узнает, дороже
отказа, который виден сразу. отказа, который виден сразу.
@@ -128,7 +135,7 @@ severity:
- **инлайн** — оркестратор чинит сам, не спрашивая и не логируя. Правка локальна, - **инлайн** — оркестратор чинит сам, не спрашивая и не логируя. Правка локальна,
решение однозначно, объём right-size. решение однозначно, объём right-size.
- **развилка** — цена сопоставима с переработкой, либо меняется scope, либо - **развилка** — цена сопоставима с переработкой, либо меняется scope, либо
трогается инвариант из брифа, либо надо менять спеку. Формулируй готовым трогается инвариант из `CLAUDE.md`, либо надо менять спеку. Формулируй готовым
вопросом с 2–3 вариантами: оркестратор перенесёт его почти дословно. вопросом с 2–3 вариантами: оркестратор перенесёт его почти дословно.
Сомневаешься — ставь `развилка`. Ошибка в сторону лишнего вопроса дешевле Сомневаешься — ставь `развилка`. Ошибка в сторону лишнего вопроса дешевле
@@ -152,7 +159,7 @@ severity:
- какие **не** запускались и почему (профиль, бюджет, недоступный инструмент, - какие **не** запускались и почему (профиль, бюджет, недоступный инструмент,
остановленный прогон); остановленный прогон);
- что каждый запущенный проход **не мог проверить в принципе** — из его charter'а; - что каждый запущенный проход **не мог проверить в принципе** — из его charter'а;
- **что осталось целиком на человеке** — раздел `## Недоступно проверке` брифа, - **что осталось целиком на человеке** — «Недоступно проверке» из `docs/review.md`,
**двумя отдельными списками**: «не проверит ни один проход» и «перестали **двумя отдельными списками**: «не проверит ни один проход» и «перестали
проверять сознательно». Слитый список бесполезен: при следующем промахе первый проверять сознательно». Слитый список бесполезен: при следующем промахе первый
вопрос — «не тот ли это класс, который мы перестали проверять», и ответить на вопрос — «не тот ли это класс, который мы перестали проверять», и ответить на
@@ -160,8 +167,10 @@ severity:
инцидентов, поведение под реальным потоком, поведение внешних систем в их инцидентов, поведение под реальным потоком, поведение внешних систем в их
версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта
функциональность вообще»; функциональность вообще»;
- если брифа не было — строку об этом **с причиной**: инварианты, модель угроз и - **каких документов проекта не хватило** — строкой на каждый, **с причиной**:
профиль нагрузки прогону были неизвестны, потому что <причина>. «`docs/security.md` в проекте нет», «есть, но периметр не назван». Строки
приходят из проходов; слить их в одну «документации не было» нельзя —
деградация поразрядная, и разные пробелы чинятся разным.
Формулировка «критичных проблем не обнаружено» **запрещена** без этой секции: она Формулировка «критичных проблем не обнаружено» **запрещена** без этой секции: она
потребляет ощущение проверенности, ничего не гарантируя, и это хуже, чем потребляет ощущение проверенности, ничего не гарантируя, и это хуже, чем
@@ -1,127 +0,0 @@
---
name: project-brief
description: Заводит или обновляет бриф ревью проекта (docs/review-brief.md) — файл, откуда конвейер ревью берёт инварианты, команду гейта, модель угроз, объёмы, прецеденты и карту проекта. Вызывать, когда брифа нет (это обнаруживают review-pipeline, task-pipeline и task-batch на старте), когда сменился гейт или появилась новая зависимость, и по прямой просьбе завести или обновить бриф.
---
# Заведение брифа проекта
Бриф — **предмет** ревью: что здесь нельзя нарушать, чем краснеет гейт, сколько
данных реально проходит, что необратимо. Без него конвейер работает в
деградированном режиме: `critical` по основанию «нарушен инвариант проекта»
недоступен ни одному проходу, числа объёма не используются, архитектурный проход
теряет свой главный критерий (граница домена) и вырождается в общее мнение.
Поэтому заведение брифа — **шаг, а не документ**. Этот скилл его выполняет.
- Контракт разделов — [контракт брифа](../review-pipeline/references/project-brief.md).
- Форма и образцы заполнения — [шаблон](../review-pipeline/references/brief-template.md).
## Когда вызывается
- **Автоматически**, без спроса: `av-dev-pipeline:review-pipeline`,
`av-dev-pipeline:task-pipeline` и `av-dev-pipeline:task-batch` разрешают путь к
брифу на старте и, не найдя его ни по одному пути, зовут этот скилл. Это не
развилка и не повод остановиться — заведение брифа делается молча, как любая
другая механика.
- **По событию:** сменился гейт; появился новый контур, зависимость или источник
входа; в журнал ревью попала запись вида «проход не мог этого знать»; свойство
промоутнулось в правило линтера (тогда пункт из брифа **вычёркивается**).
- **По просьбе человека.**
Планового пересмотра нет.
## Шаг 1. Убедиться, что брифа действительно нет
Порядок разрешения пути — тот же, что у конвейера:
1. путь, названный в задании;
2. `docs/review-brief.md`;
3. `.claude/review-brief.md`.
Файл есть, но неполон (нет обязательного раздела, раздел пуст, числа без
провенанса) — это **не** заведение с нуля: дозаполняй недостающее и не переписывай
то, что уже выверено. Разошедшийся бриф хуже отсутствующего, но переписанный
поверх выверенного — хуже разошедшегося.
## Шаг 2. Собрать материал из проекта
Бриф **выводится из проекта, а не сочиняется**. Источники по убыванию плотности:
| Раздел брифа | Откуда берётся |
|---|---|
| `## Проект` | `CLAUDE.md` / `AGENTS.md`, паспорт или README — абзац «что это и чего оно не делает» |
| `## Инварианты` | раздел инвариантов `CLAUDE.md`, архитектура, журнал решений; **цитируются формулировкой** |
| `## Гейт` | `Taskfile.yml` / `Makefile` / `justfile` / CI — сама цель гейта, состав её шагов, коды и логи |
| `## Команды` | тот же файл задач: карта проекта, поднять вживую, тесты, дорогое вне гейта, запрещённое |
| `## Прод и поток` | документация по деплою и архитектуре, конфиг и его образец, схема БД, файл наблюдений на живых данных |
| `## Модель угроз` | конфиг (токены, права), раскладка файлов на диске, схема ключей, места приёма недоверенного входа |
| `## Карта` | дерево репозитория: спеки, конвенции, архитектура, журнал ревью, миграции, `testdata`, основная ветка |
| `## Типовые узлы` | дерево пакетов: какие рода узлов реально есть |
| `## Прецеденты` | журнал ревью, архивные отчёты триажа, `git log` по починкам |
| `## Недоступно проверке` | журнал ревью (что решили не проверять) плюс общий список из контракта |
Прочитай `CLAUDE.md` и всё, на что он ссылается, **до** того, как писать первую
строку. Бриф, собранный из одного файла, повторяет его и потому бесполезен.
## Шаг 3. Заполнить
Идёшь по контракту раздел за разделом. Четыре правила ведения, из-за которых
брифы портятся чаще всего:
1. **Не пересказывай документацию.** Факт, записанный в `CLAUDE.md` или в
архитектуре, попадает сюда ссылкой и одной строкой сути. Исключение — раздел
инвариантов: он цитируется дословно, потому что по нему присваивается severity.
2. **Числа — с провенансом.** «Тела доходили до 42 МБ (замер,
`docs/local-research.md`)». Число без источника проход обязан превратить в
условие, то есть оно бесполезно.
3. **Пустой пункт называется пустым.** «Внешних зависимостей нет — смотри на диск
и на СУБД» стоит целого прохода: без этой строки эксплуатационный проход
потратит обязательный вопрос впустую или выдумает зависимость. То же про
угрозы вне модели, про отсутствующие прецеденты, про отсутствие наблюдателя.
4. **Не выдумывай четыре вещи.** Измеренные числа; периметр модели угроз; то, что
в этом проекте необратимо; и **кто обязан гонять дорогую проверку вне гейта**
— всё это из кода не выводится. Не нашёл в документации — **спроси человека на
шаге 4**, а до ответа напиши пункт словом «неизвестно» с пометкой, что он ждёт
ответа. Придуманное число здесь дороже отсутствующего: проход сошлётся на него
как на замер.
5. **Что выведено, а не прочитано, — помечай.** Чаще всего это severity у
инвариантов: проекты редко пишут её рядом с формулировкой, и её приходится
выводить по обратимости последствия. Пометка «выведена по обратимости» стоит
трёх слов и сообщает проходу, чьё это суждение, — а он по ней ставит
`critical`. То же для периметра, восстановленного из конфига, и для чисел, чей
источник по ссылке не подтвердился.
## Шаг 4. Показать человеку
Бриф — единственный файл, который конвейер **читает как истину**, поэтому он
показывается, а не заводится молча:
- покажи готовый файл (или дифф, если это обновление);
- отдельным коротким списком назови, **что выведено из проекта**, а что
**предположено или осталось неизвестным** — по этим строкам человек и правит;
- если на шаге 3 остались вопросы из класса «не выдумывай три вещи», задай их
здесь, разом и с вариантами.
Ответа ждать не обязательно: работа продолжается по заведённому брифу, а
неизвестные пункты честно стоят словом «неизвестно» — проход прочитает его как
деградацию по этому пункту, а не как факт.
**Бриф ведёт проект.** Файл кладётся в репозиторий проекта и коммитится вместе с
той работой, в ходе которой заведён. Плагин его больше не правит — он только
читает.
## Шаг 5. Вернуться в вызвавший шаг
Скажи вызвавшему скиллу путь к брифу — дальше конвейер передаёт его каждому
проходу готовым, и деградированный режим не включается.
## Если завести нельзя
Заведение отменяется ровно в трёх случаях: репозиторий доступен только на чтение;
человек прямо сказал брифа не заводить; проект настолько чужой, что вывести
инварианты неоткуда. Тогда — деградированный режим по контракту: строка в границы
покрытия и запрет на `critical` по основанию «нарушен инвариант проекта».
Во всех остальных случаях бриф заводится. «Задача маленькая, брифа не надо» —
не основание: бриф заводится один раз на проект, а деградированный режим платит
на каждой задаче.
+51 -52
View File
@@ -1,6 +1,6 @@
--- ---
name: review-pipeline name: review-pipeline
description: Конвейер ревью изменения — детерминированный гейт, сверка с дельта-спеками в обе стороны, враждебные постановки и эксплуатационный постмортем, независимая реализация по триггеру, архитектура и обязательный триаж. Проходы гонятся последовательно; параллельно — только по явной просьбе и с явно названным набором. Проектная специфика приходит из файла-брифа. Вызывается из task-pipeline (чекпоинты ревью), из task-batch (финальная сверка) и отдельно — профилем design на предложении ДО кода. description: Конвейер ревью изменения — детерминированный гейт, сверка с дельта-спеками в обе стороны, враждебные постановки и эксплуатационный постмортем, независимая реализация по триггеру, архитектура и обязательный триаж. Проходы гонятся последовательно; параллельно — только по явной просьбе и с явно названным набором. Проектная специфика приходит из документов канона av-dev-pm. Вызывается из task-pipeline (чекпоинты ревью), из task-batch (финальная сверка) и отдельно — профилем design на предложении ДО кода.
--- ---
# Конвейер ревью # Конвейер ревью
@@ -34,16 +34,17 @@ description: Конвейер ревью изменения — детермин
Конвейер опирается на внешнюю обвязку и без неё работает не целиком. Проверь это Конвейер опирается на внешнюю обвязку и без неё работает не целиком. Проверь это
один раз, при установке плагина в проект: один раз, при установке плагина в проект:
- **OpenSpec и скиллы `opsx:*`.** Профиль `design`, проход `review-specs` и - **OpenSpec — жёсткая предпосылка, а не опция.** Профиль `design`, проход
`review-specs` и
вызывающий пайплайн задачи завязаны на дельта-спеки вызывающий пайплайн задачи завязаны на дельта-спеки
(`openspec/changes/<id>/specs/*/spec.md`), на актуальные спеки (`openspec/changes/<id>/specs/*/spec.md`), на актуальные спеки
(`openspec/specs/`) и на `openspec validate --strict`. В проекте без OpenSpec (`openspec/specs/`) и на `openspec validate --strict`. В проекте без OpenSpec
шаги, зовущие `opsx:explore` / `opsx:propose` / `opsx:apply` / `opsx:archive`, шаги, зовущие `opsx:explore` / `opsx:propose` / `opsx:apply` / `opsx:archive`,
упадут на «нет такого скилла», а `review-specs` останется без источника упадут на «нет такого скилла», а `review-specs` останется без источника
требований. Такой проект либо подключает OpenSpec, либо сознательно не зовёт требований. **Проект без OpenSpec этим конвейером не проверяется** — подключай
`review-specs` и профиль `design` — и тогда это идёт строкой «не запускался» в OpenSpec, а не понижай прогон: ветка деградации здесь не пишется, потому что
границы покрытия, как любой другой пропуск. непроверенная ветка деградации хуже честного отказа.
- **Бриф проекта** — см. следующий раздел. Заводится скиллом, а не руками. - **Документы канона** — см. следующий раздел.
- **Проектные копии этих скиллов и агентов удаляются при установке.** Если в - **Проектные копии этих скиллов и агентов удаляются при установке.** Если в
проекте уже лежат свои `.claude/skills/review-pipeline`, проекте уже лежат свои `.claude/skills/review-pipeline`,
`.claude/skills/task-pipeline`, `.claude/skills/task-batch` или `.claude/skills/task-pipeline`, `.claude/skills/task-batch` или
@@ -51,39 +52,35 @@ description: Конвейер ревью изменения — детермин
в устаревшую проектную копию, молча и без признаков подмены. По той же причине в устаревшую проектную копию, молча и без признаков подмены. По той же причине
**скиллы этого плагина зовутся с пространством имён**: **скиллы этого плагина зовутся с пространством имён**:
`av-dev-pipeline:review-pipeline`, `av-dev-pipeline:task-pipeline`, `av-dev-pipeline:review-pipeline`, `av-dev-pipeline:task-pipeline`,
`av-dev-pipeline:task-batch`, `av-dev-pipeline:project-brief`. `av-dev-pipeline:task-batch`.
## Что конвейер защищает — приходит из брифа ## Что конвейер защищает — приходит из документов проекта
Проходы общие, а нарушать нельзя проектное. Список инвариантов, команду гейта, Проходы общие, а нарушать нельзя проектное. Инварианты, команду гейта, объёмы,
объёмы, прецеденты и модель угроз конвейер **не знает** — он читает их в брифе прецеденты и модель угроз конвейер **не знает** — он читает их в документах
проекта: [references/project-brief.md](references/project-brief.md) описывает канона `av-dev-pm`, **напрямую и по жёстким путям**. Отдельного файла-брифа нет:
контракт, [references/brief-template.md](references/brief-template.md) — образец пути известны, посредник не нужен, а второй дом для тех же фактов разошёлся бы и
заполнения. выглядел актуальным.
Разреши путь к брифу один раз, в начале прогона: путь из задания → Карта «что нужно проходу → где лежит» —
`docs/review-brief.md``.claude/review-brief.md`. Дальше передавай готовым. [references/project-facts.md](references/project-facts.md). Прочитай её до
раздачи заданий; там же таблица поразрядной деградации.
**Брифа нет по всем трём путям — заведи его, а не понижай прогон.** Вызови Skill **Деградация поразрядная, а не всё-или-ничего.** Документа нет — деградирует то,
**`av-dev-pipeline:project-brief`**: он соберёт бриф из `CLAUDE.md`, архитектуры, что из него читалось, и только оно: нет `docs/security.md` — слабеет
файла задач и конвенций, покажет человеку и вернёт путь. Это механика, а не `adversary`; нет `docs/research/` — числа неизвестны трём проходам; нет
развилка: спрашивать разрешения не нужно, и остановка прогона тут не инвариантов в `CLAUDE.md``critical` по основанию «нарушен инвариант проекта»
предусмотрена. Заведение стоит одного шага один раз на проект — деградированный не присваивается никем. Каждый проход пишет **свою** строку в границы покрытия, с
режим платит на каждой задаче. **причиной**; триаж сводит их и не сливает в одну.
**Деградированный режим — исход, а не умолчание.** Он включается ровно тогда, **Документов канона нет вовсе** — проект не приведён к канону. Скажи это строкой
когда бриф завести не удалось (репозиторий на чтение, человек прямо запретил, и предложи скилл `av-dev-pm:canon`: одна операция на проект против деградации на
инварианты вывести неоткуда): `critical` по основанию «нарушен инвариант каждой задаче. Прогон при этом не останавливается.
проекта» никем не присваивается, числа объёма не используются, и в границы
покрытия уезжает строка «брифа проекта нет, завести не удалось: <причина>».
Причина обязательна — без неё строка неотличима от «мы просто не стали».
## Что получает каждый проход ## Что получает каждый проход
Задание любому проходу состоит из шести вещей, и первая — главная: без брифа Задание любому проходу состоит из пяти вещей:
проход теряет предмет проверки и уходит в деградированный режим.
- **бриф** — путь (разрешён или заведён на старте, см. выше);
- **контракт находок** — путь к - **контракт находок** — путь к
[references/finding-contract.md](references/finding-contract.md) (в [references/finding-contract.md](references/finding-contract.md) (в
установленном плагине — `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/`); установленном плагине — `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/`);
@@ -146,7 +143,7 @@ description: Конвейер ревью изменения — детермин
|---|---|---|---| |---|---|---|---|
| `quick` | багфикс, локальная правка, доки | 0, 1, 5 | 4 | | `quick` | багфикс, локальная правка, доки | 0, 1, 5 | 4 |
| `standard` | новая функциональность в существующем пакете | 0, 1, 2, 5 | 6 | | `standard` | новая функциональность в существующем пакете | 0, 1, 2, 5 | 6 |
| `deep` | новый пакет, изменение публичного контракта, миграция схемы, трогает инварианты брифа | 0, 1, 2, 3, 4, 5 | 78 | | `deep` | новый пакет, изменение публичного контракта, миграция схемы, трогает инварианты проекта | 0, 1, 2, 3, 4, 5 | 78 |
| `design` | **до кода**, на предложении | specs + rubric + architecture (см. ниже) | 3 | | `design` | **до кода**, на предложении | specs + rubric + architecture (см. ниже) | 3 |
**Состав сверяется по этой таблице до коммита.** Реестр из трёх-восьми пунктов **Состав сверяется по этой таблице до коммита.** Реестр из трёх-восьми пунктов
@@ -168,8 +165,8 @@ description: Конвейер ревью изменения — детермин
- иначе → `quick`. - иначе → `quick`.
Что именно в этом проекте считается публичным контрактом и какие пути означают Что именно в этом проекте считается публичным контрактом и какие пути означают
`deep` — раздел `## Триггеры` брифа. Он **уточняет** правило, а не отменяет его: `deep`, проект может уточнить в `docs/review.md`, разделе настройки конвейера. Это
если триггеров в брифе нет, работает список выше. **уточнение**, а не отмена: не записано — работает список выше.
Профиль объявляется в отчёте. Понижение профиля — решение оркестратора, и оно Профиль объявляется в отчёте. Понижение профиля — решение оркестратора, и оно
попадает в границы покрытия строкой «профиль понижен до X, потому что …». попадает в границы покрытия строкой «профиль понижен до X, потому что …».
@@ -207,8 +204,8 @@ description: Конвейер ревью изменения — детермин
роста файлов журнала, длительность транзакции. Два меряющих прохода на одной роста файлов журнала, длительность транзакции. Два меряющих прохода на одной
машине соревнуются за диск, CPU и за саму СУБД и выдают числа, которые не машине соревнуются за диск, CPU и за саму СУБД и выдают числа, которые не
воспроизведутся. Это не гипотеза: правило выведено из находок, целиком воспроизведутся. Это не гипотеза: правило выведено из находок, целиком
державшихся на таких замерах, — у каждого проекта они свои и лежат в разделе державшихся на таких замерах, — у каждого проекта они свои и лежат в журнале
`## Прецеденты` его брифа. Число, снятое под конкурентную нагрузку от соседнего `docs/review.md`. Число, снятое под конкурентную нагрузку от соседнего
прохода, — это находка с испорченным оракулом, а её опровержение стоит дороже прохода, — это находка с испорченным оракулом, а её опровержение стоит дороже
всего выигрыша от параллельности. всего выигрыша от параллельности.
- **Машина одна.** Рядом идёт задача, поднят сервис, гоняется гейт или дорогая - **Машина одна.** Рядом идёт задача, поднят сервис, гоняется гейт или дорогая
@@ -251,7 +248,7 @@ description: Конвейер ревью изменения — детермин
## Стадия 0 — Gate (обязательна во всех профилях) ## Стадия 0 — Gate (обязательна во всех профилях)
Агент `review-gate`. Запускает команду гейта из раздела `## Гейт` брифа и Агент `review-gate`. Запускает команду гейта из семантики гейта в `CLAUDE.md` и
интерпретирует вывод. интерпретирует вывод.
**Пока гейт красный — опиниативные проходы не запускаются.** Оркестратор чинит и **Пока гейт красный — опиниативные проходы не запускаются.** Оркестратор чинит и
@@ -267,7 +264,7 @@ description: Конвейер ревью изменения — детермин
линтеры и детектор гонок. Пропуск при этом не молчит — он виден в сводке с линтеры и детектор гонок. Пропуск при этом не молчит — он виден в сводке с
причиной и уезжает в границы покрытия, как и любой другой `SKIP`. причиной и уезжает в границы покрытия, как и любой другой `SKIP`.
Шаги, которые красят гейт безусловно, перечислены в брифе с причиной. Проходу Шаги, которые красят гейт безусловно, перечислены в `CLAUDE.md` с причиной. Проходу
запрещено списывать такой отказ в мелочь. запрещено списывать такой отказ в мелочь.
## Стадия 1 — Conformance (обязательна во всех профилях) ## Стадия 1 — Conformance (обязательна во всех профилях)
@@ -279,11 +276,10 @@ description: Конвейер ревью изменения — детермин
- `review-specs` — критерий взят из **дельта-спек предлагаемого изменения**, а не - `review-specs` — критерий взят из **дельта-спек предлагаемого изменения**, а не
из proposal, сообщения коммита или описания задачи. Сверка двунаправленная; из proposal, сообщения коммита или описания задачи. Сверка двунаправленная;
направление `code → spec` важнее. направление `code → spec` важнее.
- `review-code` — критерий взят из конвенций проекта: файла или каталога файлов, - `review-code` — критерий взят из конвенций проекта, каталог
путь — раздел `## Карта` брифа. Берётся только та их часть, которая **не `docs/conventions/`. Берётся только та их часть, которая **не выражается
выражается правилом**: правилом**: механизируемое уже проверила стадия 0. Что именно механизировано,
механизируемое уже проверила стадия 0. Что именно механизировано, тот же раздел перечисляет `conventions/README.md` повторять это проходом вредно.
брифа перечисляет — повторять это проходом вредно.
Recall обоих равен длине их источника — это и есть предел applicative-проходов, Recall обоих равен длине их источника — это и есть предел applicative-проходов,
ради которого существует стадия 2. ради которого существует стадия 2.
@@ -309,15 +305,17 @@ Recall обоих равен длине их источника — это и е
**прогнать**, второй смотрит ось времени и эксплуатации, которую не смотрит **прогнать**, второй смотрит ось времени и эксплуатации, которую не смотрит
никто другой. никто другой.
Материал обоим даёт бриф: `## Модель угроз` — враждебному, `## Прод и поток` Материал берётся из документов: `docs/security.md` — враждебному,
эксплуатационному. Без этих разделов стадия вырождается в общие места. `docs/architecture.md` плюс **`docs/research/` и `docs/database.md` вместе** —
эксплуатационному. Последние два сшивает сам проход: число без настройки не с чем
сравнить. Без этих документов стадия вырождается в общие места.
## Стадия 3 — Independent reimplementation (`deep`, по триггеру) ## Стадия 3 — Independent reimplementation (`deep`, по триггеру)
- `review-reimpl` — пишет свою реализацию, не открывая существующую, затем - `review-reimpl` — пишет свою реализацию, не открывая существующую, затем
диффит по решениям. **Запускается по триггеру, а не всегда:** изменение вводит диффит по решениям. **Запускается по триггеру, а не всегда:** изменение вводит
новое правило идентичности, слияния или разбора (проектная формулировка новое правило идентичности, слияния или разбора (проектная формулировка
триггера — в разделе `## Триггеры` брифа). Это самый дорогой проход конвейера триггера — в `docs/review.md`, если записана). Это самый дорогой проход конвейера
(его счёт определяется объёмом вывода — он пишет реализацию целиком), а вне (его счёт определяется объёмом вывода — он пишет реализацию целиком), а вне
этого триггера независимый взгляд в значительной мере уже дал профиль `design`: этого триггера независимый взгляд в значительной мере уже дал профиль `design`:
код писался под его находки. Триггер выбран по факту: единственный раз, когда код писался под его находки. Триггер выбран по факту: единственный раз, когда
@@ -328,7 +326,7 @@ Recall обоих равен длине их источника — это и е
Агент `review-architecture`. Получает **вход шире диффа**: дерево пакетов с Агент `review-architecture`. Получает **вход шире диффа**: дерево пакетов с
назначением, граф внутренних зависимостей, инвентарь существующих концепций. назначением, граф внутренних зависимостей, инвентарь существующих концепций.
Команду, которая это готовит, даёт раздел `## Команды` брифа; нет команды — Команду, которая это готовит, даёт раздел команд `CLAUDE.md`; нет команды —
проход собирает карту сам и говорит об этом в границах покрытия. проход собирает карту сам и говорит об этом в границах покрытия.
Главный вопрос — концептуальная целостность и **второй способ** делать то, что Главный вопрос — концептуальная целостность и **второй способ** делать то, что
@@ -400,7 +398,7 @@ Recall обоих равен длине их источника — это и е
у него свой формат, своя нарезка и свои правила дублей. Мелочь класса `nit` у него свой формат, своя нарезка и свои правила дублей. Мелочь класса `nit`
идёт в урожай одной пачкой, а не записью на находку. идёт в урожай одной пачкой, а не записью на находку.
- `Promote candidates` — по процедуре [references/promote.md](references/promote.md): - `Promote candidates` — по процедуре [references/promote.md](references/promote.md):
находка → конвенция → правило линтера → **удаление из конвенций и из брифа**. находка → конвенция → правило линтера → **удаление формулировки из конвенций**.
Третий шаг обязателен. Третий шаг обязателен.
- Дефект, проскочивший ревью и всплывший позже, идёт в журнал проекта - Дефект, проскочивший ревью и всплывший позже, идёт в журнал проекта
([references/review-journal.md](references/review-journal.md)) — сразу, не ([references/review-journal.md](references/review-journal.md)) — сразу, не
@@ -421,8 +419,9 @@ Recall обоих равен длине их источника — это и е
Согласие нескольких проходов — **не подтверждение**: это один источник, Согласие нескольких проходов — **не подтверждение**: это один источник,
высказавшийся несколько раз. Совпадение повышает приоритет, но не `confidence`. высказавшийся несколько раз. Совпадение повышает приоритет, но не `confidence`.
Что недоступно **этому** проекту принципиально — перечисляет раздел Что недоступно **этому** проекту принципиально — перечисляет «Недоступно
`## Недоступно проверке` брифа, и он целиком уезжает в границы покрытия. проверке» в `docs/review.md`, и оба его подраздела целиком уезжают в границы
покрытия.
Независимо от проекта недоступно: Независимо от проекта недоступно:
- поведение внешних систем в их будущих версиях; - поведение внешних систем в их будущих версиях;
@@ -442,9 +441,9 @@ Recall обоих равен длине их источника — это и е
## Ссылки ## Ссылки
- Skill `av-dev-pipeline:project-brief` — заведение и обновление брифа. - [references/project-facts.md](references/project-facts.md) — что нужно проходу
- [references/project-brief.md](references/project-brief.md) — контракт брифа проекта. и где это лежит в документах проекта; таблица поразрядной деградации.
- [references/brief-template.md](references/brief-template.md) — шаблон брифа. - Skill `av-dev-pm:canon` — приведение проекта к канону документов.
- [references/finding-contract.md](references/finding-contract.md) — контракт находок. - [references/finding-contract.md](references/finding-contract.md) — контракт находок.
- [references/promote.md](references/promote.md) — промоут находка → конвенция → правило → удаление. - [references/promote.md](references/promote.md) — промоут находка → конвенция → правило → удаление.
- [references/calibration.md](references/calibration.md) — калибровка инъекцией, вердикты keep/retune/drop. - [references/calibration.md](references/calibration.md) — калибровка инъекцией, вердикты keep/retune/drop.
@@ -1,289 +0,0 @@
# Шаблон брифа проекта
Образец заполнения. Контракт разделов — в
[project-brief.md](project-brief.md); заводит бриф по этому образцу скилл
`av-dev-pipeline:project-brief` — руками копировать не надо, но читать полезно.
Курсивом даны пояснения — их из готового брифа убирают. Примеры взяты из двух
разных проектов (коллектор данных с непрерывным потоком и связующий сервис вокруг
внешних демонов), чтобы было видно, как один и тот же раздел выглядит при разной
природе проекта.
---
## Проект
*Абзац: что делает — и чего не делает.*
> Коллектор выгрузок с телефона. Принимает доставки, хранит их и отдаёт другим
> сервисам. Это **хранилище, а не аналитика**: принять, дедуплицировать,
> сохранить, отдать. Не переименовывать поля источника, не интерпретировать
> значения; свёртка считается только в ответе на запрос.
> Связующий сервис между качалкой и медиасервером: принимает задание, качает,
> распознаёт содержимое, раскладывает файлы ссылками. **Не медиатека и не
> плеер** — ничего не хранит сверх метаданных о раскладке.
## Инварианты
*Проверяемое свойство + последствие + severity по умолчанию. Цитируются
формулировкой. Severity проект обычно не пишет — тогда она выводится по
обратимости и помечается: «по умолчанию `critical` (выведена по обратимости)».*
- **Точка сохраняется дословно.** Незнакомое поле не отбрасывается, число не
округляется при записи. Нарушение — необратимая потеря: сырой архив живёт
14 дней, дальше истина только в свёртке. По умолчанию `critical`.
- **Источник неприкосновенен.** Только `mkdir`/`link(2)`/`unlink` собственных
ссылок; файлы под каталогом загрузок не трогаются никогда. Нарушение —
повреждение чужих данных, необратимое. По умолчанию `critical`.
- **Сохранили — значит приняли.** Код ответа отражает доставку, а не разбор:
непонятое содержимое — `200`, тело уже на диске. Нарушение стоит доставки,
которую отправитель не повторит. По умолчанию `critical`.
- **Секреты и данные пользователя не в логах.** Тело запроса — только на `DEBUG`
и с обрезкой. По умолчанию `critical`.
- **Агрегации при записи нет.** Нарушение искажает историю молча и
диагностируется только сверкой с внешним источником, то есть месяцами позже.
По умолчанию `major`, `critical` — если испорченное невосстановимо.
## Гейт
- **Команда:** `task gate BASE=<база>`; база по умолчанию —
`git merge-base HEAD master`, на `master``HEAD~1`.
- **Логи шагов:** `tmp/gate/<шаг>.log`. Сводка печатает `OK`/`FAIL`/`WARN`/`SKIP`;
краснит гейт только `FAIL`.
- **Шаги:** сборка, `vet`, линтеры, форматирование, тесты, повторный прогон на
флаки, `-race`, покрытие изменённых строк, накат миграций с нуля, поиск
секретов, `govulncheck`.
- **Красят безусловно** *(перечислить с причиной — это главная часть раздела)*:
- `no-user-data` — файл из каталога данных попал под контроль версий: убрать
обычным коммитом уже нельзя;
- `config-samples` — структура конфига изменилась, а образец нет: забытое поле
обнаруживается не тестом, а тем, что через полгода о нём никто не знает;
- `migrations` — миграции не накатываются с нуля: восстановление перестаёт
работать ровно тогда, когда оно нужно;
- `er-schema` — миграция тронута, а схема в документации не обновлена.
- **Чего в гейте намеренно нет:** прогон на живом корпусе (`task verify:archive`)
— минута работы и данные, которых нет ни на какой другой машине. У этой
проверки краснота не видна никому до следующей задачи, которая до неё
дотянется, — говори об этом в границах покрытия.
## Команды
- Карта проекта для архитектурного прохода: `task review:context > tmp/review-context.md`
- Поднять изменение вживую: `task restart`, логи — `task logs`
- Тесты и линт: `task test`, `task lint`
- **Дорогое вне гейта, с адресатом:** `task verify:archive` (минута, живые
данные). **Кто и когда обязан:** пайплайн задачи — после любого изменения
разбора входного формата или правила слияния, до архивации change; вручную —
человек перед выкладкой. Не прогонялась — строка в границы покрытия, а не
молчание.
- **Запускать запрещено:** ничего, что пишет в `./data`, в рабочую БД и в боевой
каталог архива. Замеры — только на копиях в `./tmp`.
## Прод и поток
*Первая строка — главный вопрос эксплуатации этого проекта.*
> **Главный вопрос:** поток идёт непрерывно и молча, отправитель об отказе не
> узнает и не повторит — значит, дороже всего тихо потерянная доставка, а не
> упавший сервис.
> *(В сервисе, который сам опрашивает чужих демонов, первая строка была бы
> противоположной: «главный вопрос — что происходит, когда внешний сервис
> отвечает медленно, а не когда он упал».)*
- **Где:** один статический бинарь в контейнере на домашнем сервере, перед ним
обратный прокси с TLS, SQLite на диске. Ни оркестратора, ни реплик, ни дежурной
смены.
- **Внешние зависимости и как каждая отказывает:** прокси — рвёт соединение на
длинном теле; диск — заполняется и тормозит; СУБД — отдаёт «занято» под
параллельной записью; приложение-источник на телефоне — молча перестаёт слать.
*(В другом проекте здесь были бы качалка, медиасервер, LLM и база метаданных, и
каждая — со своим «отвечает медленно», а не только «упала».)*
*(Если зависимостей нет — так и пишут: «внешних зависимостей нет, смотри на
диск и на СУБД». Пустой пункт называется пустым.)*
- **Кто заметит отказ:** один пользователь-владелец, в лучшем случае вечером, а
скорее не заметит вовсе.
- **Характер потока:** телефон шлёт непрерывно и молча; обратной связи у
отправителя нет, об отказах он не сообщает, расписание плавает. Тихо
сломавшаяся доставка — главный эксплуатационный риск.
- **Представление данных и настройки хранилища:** запись — сжатый BLOB, читается
и пересобирается целиком на каждой операции (`internal/store`); журнал СУБД —
WAL; таймаут занятости — 5000 мс (`config.example.toml`); лимит тела приёма —
64 МБ; ретеншен сырого архива — 14 дней.
- **Числа (с провенансом):** нижний слой — порядка 135 тыс. точек в сутки
(замер, `docs/local-research.md`); тела доходили до 42 МБ (там же); запись —
read-modify-write под конкурентными доставками (`docs/architecture.md`).
- **Обратимость:** падение сервиса обратимо — отправитель дошлёт широким
проходом. Потеря или порча точки необратима. Поэтому тихая порча весит больше,
чем «сервис вернул 500».
## Модель угроз
*Первая строка — периметр.*
> **Периметр:** сервис открыт наружу через обратный прокси, недоверенным считается
> всё, что приходит по HTTP. Злоумышленник в локальной сети — вне периметра.
> *(У сервиса в доверенном контуре первая строка противоположна: «контур
> доверенный, публичного интернета здесь нет — не выдумывай его; недоверенное
> здесь — то, что отдают внешние демоны и трекеры».)*
> *(Контур ещё не развёрнут — тогда периметров два: «целевой — за прокси с TLS;
> сегодняшний — только локальная машина, токены пусты осознанно. **Находки
> строятся против целевого**, отсутствие TLS сегодня находкой не является».)*
- **Недоверенное:** тело доставки целиком (имена метрик, единицы, формы точек,
метки времени, глубина вложенности, размер); заголовки доставки, часть которых
участвует в решениях; содержимое архива внешнего экспорта (имена файлов внутри
zip мы не формировали); параметры читающего API.
- **Из чего строятся пути и ключи:** файл сырого архива —
`raw/ГГГГ/ММ/ДД/<ulid>.json.gz`, дата берётся из времени приёма, имя — из
генератора идентификаторов; ключ записи — `метрика + слой + начало + конец`,
источник в ключ не входит.
- **Разграничение:** статические токены в `Authorization: Bearer`, раздельные на
запись и на чтение; конфиг под `0600`.
- **Что дороже:** данные пользователя дороже токена. Путь, по которому значение
доезжает до лога выше `DEBUG`, до ответа с ошибкой или до `testdata` в git, —
полноценная находка, а не замечание по гигиене.
- **Вне модели:** злоумышленник в локальной сети; вредоносный оператор;
компрометация поставщика данных; мультиарендность. Находки этих классов не
выводятся — они никогда не будут исправлены.
## Карта
- **Основная ветка:** `master`. От неё берутся ветки задач, в неё вливается батч,
база диффа по умолчанию — `git merge-base HEAD master` (на самой ветке `HEAD~1`).
- Актуальные спеки: `openspec/specs/<capability>/spec.md`
- Дельта-спеки изменения: `openspec/changes/<id>/specs/*/spec.md`
- **Нарезка capability и что из неё переехало в спеки:** режем по домену
(`ingest`, `storage`, `read-api`, `mcp`), а не по транспорту. В актуальные
спеки перенесены `ingest` и `storage`; `read-api` описан только в
`docs/architecture.md`, `mcp` — пока только в коде. Пробел в спеке по этим двум
темам — не находка, а известное состояние.
- Конвенции прозой: `docs/conventions.md` *(в другом проекте это каталог из
нескольких файлов — тогда перечисляют все:
`docs/conventions/{logging,errors,config,database,web-ui}.md`)*. Механизировано
и потому **не проверяется проходом по конвенциям**: форма логов,
`fmt.Print*`/`os.Getenv`/`time.Now` мимо единых точек, сравнение ошибок,
сторонние пакеты ошибок — всё это правила в `.golangci.yml`.
- Архитектура и решения: `docs/architecture.md`
- **Наблюдения на живых данных:** `docs/local-research.md` — что реально шлёт
источник и чем это расходится с его документацией. *(Не ведём — так и пишут:
«наблюдений на живых данных не ведём».)*
- Журнал проскочивших дефектов: `docs/review-journal.md`
- **Единые точки:** идентификаторы — `internal/ident`; время — `store.Now()`;
разбор дат входного формата — один парсер в `internal/parse`; маппинг доменной
ошибки в код ответа — одна точка в `internal/httpapi`; путь приёма — `ingest`,
общий для HTTP и CLI. Инвентарь целиком выгружает `task review:context`.
- **Нумерованные артефакты:** миграции — `internal/store/migrations/NNNN_*.sql`,
номер монотонный, следующий свободный смотреть там же.
- Задачи: `docs/backlog/` *(пайплайн только читает и сообщает исход)*
- Реальные пакеты для тестов разбора: `internal/parse/testdata` — там данные
пользователя с вычищенными токенами, наружу не копировать
- Временное: `./tmp` (не системный `/tmp`)
- **Не трогать:** `./data` — боевой архив и БД
## Типовые узлы
*Род узла + 3–5 специфичных проверяемых свойств.*
- **Разбор входного формата** — поведение на усечённом и враждебном входе,
границы размера, отсутствие паники, детерминизм, судьба незнакомых полей.
- **HTTP-обработчик приёма** — валидация формы конверта до записи, лимит тела и
архивная бомба, что попадает в ответ, а что в лог, отсутствие доменной логики
в транспорте.
- **Обработчик читающего API** — предсказуемость размера ответа, поведение при
пустом диапазоне, коды ответа на невозможный запрос.
- **Репозиторий** — границы транзакции, конкурентная запись того же ключа,
откуда берутся время и id, что возвращается при отсутствии записи,
идемпотентность повторной записи.
- **Файловое хранилище с ретеншеном** — атомарность записи, поведение при
неполной записи и нехватке места, что удаляется и по какому критерию, можно ли
удалить лишнее.
- **CLI-команда пересборки** — идемпотентность повторного прогона, поведение при
отмене на середине, что остаётся после падения, отчёт для человека.
- **Клиент внешнего сервиса** — таймаут, протяжка `context`, поведение при
«медленно» против «упало», ретраи и их граница.
## Прецеденты
*Воспроизведённые случаи этого проекта: класс — симптом — чем воспроизведён —
чем закончилось. Прецедентов нет — так и пишут: «прецедентов не накоплено».*
- **Вырожденный ответ библиотеки, неотличимый от штатного.** Симптом: пересборка
докладывала «журнал разобран целиком», а часть записей не доезжала. Причина:
контрольная точка журнала СУБД под занятой блокировкой возвращала `-1` вместо
пары чисел, и сравнение `-1 >= -1` читалось как успех — 1492 тика из 5502.
Воспроизведено экспериментом на стенде (`tmp/probe-checkpoint/`), из
документации драйвера не следовало. Закончилось: явная проверка вырожденного
значения + вопрос 8 в эксплуатационном проходе.
- **Канонизация внутри транзакции.** Симптом: соседняя доставка получала «база
занята». Причина: пересборка держала блокировку записи 5.019 с при таймауте
занятости 5000 мс — канонизация и хеширование шли внутри транзакции.
Воспроизведено замером на копии БД. Закончилось: вынос канонизации из
транзакции; числа — в раздел `## Прод и поток`.
- **Пик памяти на распаковке.** Симптом: контейнер убивался по памяти на крупных
доставках. Причина: сжатая запись распаковывалась целиком, пик 768 МиБ на теле
40 МБ. Воспроизведено прогоном на реальном пакете из `testdata`. Закончилось:
потоковая обработка; факт «запись — сжатый BLOB» вынесен в бриф, потому что без
него замер не читается как аномалия.
## Типовые ложноположительные
*Находки, которые здесь выглядят убедительно и всегда неверны. Пусто — так и
пишут.*
- «Значения из входа надо нормализовать перед записью» — инвариант требует
дословного хранения; нормализация тут порча, а не улучшение.
- «Приём должен отвечать ошибкой на непонятое содержимое» — инвариант «сохранили
— значит приняли»; отправитель доставку не повторит.
- «Порядок ключей в JSON стабилен, канонизация избыточна» — наблюдение на живых
данных говорит обратное.
- «Вынести в конфиг» про значения, заданные внешним форматом.
## Вопросы к проходам
*Производные от журнала: вопрос конкретному проходу плюс ссылка на запись, из
которой он взялся. Пусто — так и пишут.*
- `ops`: что произойдёт при откате бинаря поверх уже накатившейся миграции —
стартует ли старая версия молча (журнал, запись 2026-05-12).
- `adversary`: имена файлов внутри архива внешнего экспорта мы не формировали —
проверь путь от имени в архиве до операции с файловой системой (журнал, запись
2026-06-03).
## Триггеры
- `deep`: миграция в `internal/store/migrations/`, новый пакет `internal/*`,
изменение контракта читающего API, правило слияния или вывод слоя.
- «Видимое снаружи» (то есть `standard`): эндпоинт, форма ответа, код ответа
приёма, формат лога.
- `reimpl` запускается, когда изменение вводит **новое правило слияния,
идентичности или разбора**.
## Недоступно проверке
### Не проверит ни один проход
*Принципиальные границы. По факту промаха не пересматриваются.*
- Поведение внешнего приложения-источника на следующем его обновлении.
- Что реально лежит в системе-источнике: сверить можно только ручным экспортом,
а он делается раз в 2–3 месяца.
- Поведение таблицы под объёмом нескольких лет истории и реальный профиль
нагрузки.
- Завязка внешних потребителей на текущую форму ответа.
- Суждение «этой функциональности не должно существовать».
### Перестали проверять сознательно
*Что, когда, почему и где записано. Пересматривается первым, как только что-то
проскочило. Пусто — так и пишут: «сознательно ничего не отключали».*
- **Поимённая сверка со стайлгайдами языка** — с 2026-05, вместе с упразднением
прохода про идиоматичность (журнал ревью, запись 2026-05-04). Класс обратимый:
портит форму кода, не данные.
- **Правило линтера про длину функции** — снято 2026-06-18: ложных срабатываний
больше трети (журнал, там же). Вернуть, если проскочит дефект «функция делает
три вещи».
@@ -45,7 +45,7 @@
Отсюда два следствия: Отсюда два следствия:
- **правка charter'а — правка для всех проектов.** Прежде чем сужать - **правка charter'а — правка для всех проектов.** Прежде чем сужать
формулировку под свою боль, проверь, не место ли ей в брифе: предмет проверки формулировку под свою боль, проверь, не место ли ей в документах проекта: предмет проверки
живёт там, метод — в charter'е; живёт там, метод — в charter'е;
- **удаление прохода из плагина требует замера на двух проектах**, а не на одном: - **удаление прохода из плагина требует замера на двух проектах**, а не на одном:
класс, не всплывший здесь, мог быть единственным работающим там. класс, не всплывший здесь, мог быть единственным работающим там.
@@ -32,12 +32,12 @@
- **Находка без поля «Последствие» не выводится вовсе.** Пустое «Последствие: - **Находка без поля «Последствие» не выводится вовсе.** Пустое «Последствие:
ухудшает читаемость» равносильно отсутствию поля. ухудшает читаемость» равносильно отсутствию поля.
- **`nit` допустим только при нарушении записанной конвенции** — со ссылкой на - **`nit` допустим только при нарушении записанной конвенции** — со ссылкой на
файл и раздел конвенций проекта (путь — из раздела `## Карта` брифа) либо на файл и раздел конвенций проекта (`docs/conventions/`) либо на
правило линтера. Если правило механизируемо, но не механизировано — это не правило линтера. Если правило механизируемо, но не механизировано — это не
находка ревью, это `Promote candidate` (см. [promote.md](promote.md)). находка ревью, это `Promote candidate` (см. [promote.md](promote.md)).
- **`critical` по основанию «нарушен инвариант проекта» требует брифа.** Ссылка - **`critical` по основанию «нарушен инвариант проекта» требует инвариантов.**
идёт на пункт раздела `## Инварианты` дословно. Без брифа такое основание Ссылка идёт на пункт раздела инвариантов `CLAUDE.md` дословно. Без них основание
недоступно — см. [project-brief.md](project-brief.md), деградированный режим. недоступно — см. [project-facts.md](project-facts.md), поразрядная деградация.
- **Расхождение — не дефект, пока не названо последствие.** Особенно для прохода - **Расхождение — не дефект, пока не названо последствие.** Особенно для прохода
независимой реализации: «я бы сделал иначе» без последствия не выводится. независимой реализации: «я бы сделал иначе» без последствия не выводится.
@@ -52,7 +52,7 @@
Шкала привязана к обратимости, а не к громкости: класс «необратимо и молча» Шкала привязана к обратимости, а не к громкости: класс «необратимо и молча»
всегда весит больше класса «шумно и лечится повтором». Что здесь необратимо, всегда весит больше класса «шумно и лечится повтором». Что здесь необратимо,
говорит раздел `## Прод и поток` брифа. говорит `CLAUDE.md` — что в этом проекте необратимо.
## Блок границ покрытия ## Блок границ покрытия
@@ -1,413 +0,0 @@
# Бриф проекта — контракт
Конвейер общий, а находки — проектные. Проход, не знающий, что в этом проекте
нельзя нарушать, чем краснеет гейт и сколько данных реально проходит через узел,
выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать.
Поэтому проектная специфика живёт **в одном файле проекта**, а не в charter'ах
агентов. Charter описывает **метод** прохода (что он делает и почему именно так),
бриф — **предмет** (что здесь дорого, чем это меряется, где лежит).
Шаблон для заполнения — [brief-template.md](brief-template.md).
## Где лежит и как находится
Порядок разрешения пути, одинаковый для скилла и для каждого агента:
1. путь, названный в задании конвейера (`бриф: <путь>`) — конвейер обязан его
передавать каждому проходу;
2. `docs/review-brief.md`;
3. `.claude/review-brief.md`;
4. брифа нет ни по одному пути — **он заводится**, скиллом
`av-dev-pipeline:project-brief`, и прогон продолжается по заведённому.
Разрешает путь конвейер, один раз, и дальше передаёт готовым. Агент, получивший
путь в задании, сам ничего не ищет.
## Деградированный режим — исход, а не умолчание
Он включается ровно тогда, когда бриф **завести не удалось**: репозиторий
доступен только на чтение, человек прямо запретил, инварианты вывести неоткуда.
Во всех остальных случаях брифа быть обязано.
Каждый проход в этом режиме:
- не присваивает `critical` по основанию «нарушен инвариант проекта» — инвариантов
он не знает;
- не оперирует числами объёма и потока — формулирует условиями;
- пишет в границы покрытия строку: «брифа проекта нет (<причина>): инварианты,
модель угроз и профиль нагрузки неизвестны; находки этих классов не искались».
Причина обязательна: без неё строка неотличима от «мы просто не стали», и
одинаковая строка в каждом отчёте перестаёт читаться на третьей задаче.
Триаж сводит эти строки в одну и выносит в финальный отчёт. Отсутствие брифа —
дыра покрытия, а не нейтральное умолчание.
## Форма
Markdown. Разделы — заголовки второго уровня с **точными именами** из списка
ниже: по ним агенты находят свой кусок. Порядок разделов свободен, лишние разделы
допустимы и игнорируются, отсутствующий раздел работает как деградированный режим
для тех проходов, которые его читают.
## Разделы
### `## Проект` — обязателен
Абзац: что система делает — и, что важнее, **чего она не делает**. Граница домена
нужна архитектурному проходу как критерий: «хранилище, а не аналитика», «единое
ядро, тонкие транспорты», «связующий сервис, а не медиатека». Без неё перенос
понятия через границу выглядит просто новым кодом.
Читают: `architecture`, `rubric`, `reimpl`, `specs`.
### `## Инварианты` — обязателен
Список того, что нарушать нельзя. Каждый пункт — три вещи:
- формулировка **как проверяемое свойство**, а не как лозунг: «точка сохраняется
дословно: незнакомое поле не отбрасывается», а не «бережно относимся к данным»;
- **последствие нарушения** и его обратимость;
- **severity по умолчанию** — если это не `critical`, скажи прямо.
Это единственный раздел, который **цитируется формулировкой**, а не пересказывается
ссылкой: по нему присваивается severity, и пересказ здесь стоит неверной оценки.
**Оговорка про severity, потому что она единственная не цитируется.** Проекты
почти никогда не пишут severity рядом с инвариантом — её приходится выводить, и
правило вывода одно: **по обратимости последствия**. Необратимо и молча —
`critical`; лечится повтором, видно сразу — ниже. Выведенная severity помечается
словом «выведена по обратимости», а не выдаётся за решение проекта: проход ставит
по ней `critical`, и он вправе знать, чьё это суждение. Лучший исход — дописать
severity туда, откуда цитируется формулировка, и тогда пометка снимается.
Читают: `specs` (режим 1 — отражены ли задетые инварианты в спеке), `code`,
`adversary`, `architecture`, `triage` (ранжирование и разметка «развилка»).
### `## Гейт` — обязателен
- **Команда** целиком, включая передачу базы диффа (`task gate BASE=<база>`), и
как база определяется по умолчанию.
- **Где логи** отдельных шагов.
- **Что означает каждый исход**: чем гейт краснеет, что предупреждает, что
пропускается по составу диффа.
- **Шаги, которые красят безусловно, и почему.** Это самая ценная часть раздела:
«данные под контролем версий», «структура конфига изменилась, а образец нет»,
«миграции не накатываются с нуля» — проход обязан знать, что здесь не бывает
«ну это мелочь».
- **Чего в гейте намеренно нет** и почему — прогон на живом корпусе, длинный
интеграционный тест. У проверки, которую гейт не гоняет, краснота никому не
видна; это уезжает в границы покрытия.
Читает: `gate`.
### `## Команды` — обязателен
Что проход имеет право выполнить и чем:
- **карта проекта для архитектуры** — команда, отдающая пакеты, граф зависимостей
и инвентарь концепций (`task review:context`);
- **запуск изменения вживую** — чем поднять и как проверить поведение (нужно
пайплайну задачи на шаге поведенческой верификации);
- **тесты, линт, дополнительные проверки** — и какие из них дорогие;
- **дорогие проверки вне гейта — с адресатом.** Мало сказать «`verify:archive`
идёт минуту»: назови, **кто и когда обязан** её гонять — какой класс изменения
её требует, кто её запускает (проход, пайплайн, человек) и что делать, если она
не прогонялась. Без адресата дорогая проверка не гоняется никогда, а её
краснота не видна никому. **Адресат в проекте не записан нигде — тогда бриф его
назначает**, и назначение помечается: «адресат назначен брифом, владельцем не
подтверждён». Это тот же класс, что выведенная severity у инварианта: слот
честнее заполнить назначением с пометкой, чем оставить пустым;
- **что запускать запрещено**: рабочая БД, боевой каталог данных, внешние
сервисы. Формулируй запретом с путями, а не «будь осторожен».
Читают: `architecture`, `gate`, `ops`, `triage`, пайплайн задачи.
### `## Прод и поток` — обязателен
Материал для эксплуатационного прохода, и он же — половина ранжирования триажа.
**Первой строкой — главный вопрос эксплуатации этого проекта.** Один заголовок
покрывает противоположные постановки: «поток идёт непрерывно и молча, отправитель
об отказе не узнает» и «мы опрашиваем чужие сервисы, и главный вопрос — что
делать, когда сосед отвечает медленно». От того, какая из них здесь главная,
зависит порядок находок в отчёте, а вывести её проход не может — он видит
одинаковый код.
Дальше:
- где это работает: машина, окружение, что рядом, кто перезапускает;
- **внешние зависимости поимённо** и чем каждая отказывает: не только «падает», но
и «отвечает медленно», «молчит», «отдаёт мусор». Эксплуатационный проход
спрашивает про каждую отдельно, и список зависимостей он взять больше неоткуда.
**Зависимостей почти нет — так и напиши**: «внешних зависимостей нет, смотри на
диск и на СУБД». Пустой пункт, не названный пустым, проход тратит впустую или
заполняет выдумкой;
- **кто заметит отказ и когда** — есть ли вообще наблюдатель;
- **характер потока**: непрерывный и молчаливый, по запросу, по расписанию; есть
ли обратная связь у отправителя;
- **представление данных и настройки хранилища.** Чем физически лежит запись
(сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи
(распаковка целиком, read-modify-write), и **настройки, у которых есть
числовое значение**: таймаут занятости СУБД, режим журналирования, лимит тела,
размер пула, ретеншен. Это не украшение раздела: ровно эти два факта
превращают **замер** в находку. Замеренный пик памяти — аномалия только если
известно, что запись лежит сжатой и распаковывается целиком; замеренная
длительность удержания блокировки — гарантированный отказ соседа только если
известно, чему равен таймаут занятости. Без этих фактов проход снимет верное
число и честно понизит находку до гипотезы, потому что сравнить его будет не с
чем. Цена пропущенного пункта здесь не «не найдём», а **«найдём и не
починим»**. Числа с провенансом — в следующем пункте, воспроизведённые случаи —
в разделе `## Прецеденты`;
- **измеренные числа с провенансом**: объёмы, размеры тел, темп, размеры таблиц.
Число без источника проход обязан превратить в условие — так и напиши, откуда
оно. **Замер и настройка — разные пункты, и путать их нельзя:** настройка
(`busy_timeout`, лимит тела, размер пула) живёт пунктом выше и говорит, чему
равен порог; замер говорит, что происходит на самом деле. Проекту без
наблюдаемой нагрузки нечего писать во втором пункте — **так и напиши**:
«измеренных чисел нагрузки нет, всё, что ниже, — настройки». Тогда проход
формулирует условиями осознанно, а не потому, что не нашёл;
- **что обратимо, а что нет.** Падение, которое лечится повтором, и тихая потеря,
которую нечем восстановить, — разные классы, и порядок находок в отчёте зависит
от того, какой из них здесь главный.
Читают: `ops`, `adversary`, `triage`, `reimpl`.
### `## Модель угроз` — обязателен
**Первой строкой — периметр.** «Сервис открыт наружу; злоумышленник в локальной
сети неинтересен» и «контур доверенный, публичного интернета здесь нет, не
выдумывай его» — это один и тот же заголовок при противоположной постановке, и
враждебный проход не может выбрать между ними сам. Периметр, объявленный первой
строкой, задаёт смысл всему остальному разделу.
**Периметров может быть два — целевой и сегодняшний**, если контур ещё не
развёрнут: «целевой — открыт наружу за прокси с TLS; сегодняшний — только
локальная машина, токены пусты осознанно». Тогда назови оба и скажи прямо,
**против какого строятся находки**. Иначе враждебный проход либо завалит отчёт
находками «нет TLS» по сегодняшнему состоянию, либо не станет искать дефекты,
спящие до выкладки, — оба исхода стоят прохода целиком.
Дальше:
- **что недоверенное** и каким каналом приходит: тело запроса, файл, аргумент
команды, ответ внешней системы, содержимое архива;
- **из чего строятся пути и ключи** — раскладка файлов на диске, состав
координатного ключа записи, имя каталога. Враждебный проход выводит запись за
пределы песочницы именно отсюда, и без этого пункта он ищет вслепую;
- **что разграничивает доступ** — токены, контуры, права файлов;
- **что чувствительнее чего**: если данные дороже секретов, скажи это прямо;
- **что вне модели** — перечислить явно. Пустой пункт «вне модели» означает, что
враждебный проход выдумает угрозу сам, и находка никогда не будет исправлена.
Читает: `adversary`.
### `## Карта` — обязателен
Где что лежит, путями:
- **основная ветка** — её имя. Отсюда берутся ветки задач, в неё вливается батч,
от неё считается база диффа по умолчанию (`git merge-base HEAD <основная>`).
Батч подставляет это имя в каждую команду git; взять его больше неоткуда, а
угадывание между `master` и `main` ломает интеграцию целиком;
- актуальные спеки и дельта-спеки предлагаемого изменения;
- **нарезка capability и миграционное состояние спек** — по какому признаку
проект режет capability (по домену, по транспорту, по подсистеме), какие из них
уже перенесены в актуальные спеки, а какие ещё живут только в документации или
в коде. Проход по спекам иначе примет непереехавшую тему за пробел в спеке, а
архитектурный — за отсутствие понятия;
- конвенции прозой — **файл или каталог файлов**, путями; и **какая их часть уже
механизирована** правилом. Механизация бывает **в нескольких местах сразу**:
конфиг линтера, собственный анализатор и — чаще всего незамеченное —
**тест-сканер исходников** (правило про направление зависимостей, форму
миграций, логику в транспорте), который внешне неотличим от обычного теста.
Перечисли все места: непойманное место механизации означает, что проход по
конвенциям будет добросовестно проверять уже проверенное;
- **наблюдения на живых данных** — где записано, как внешний мир ведёт себя на
самом деле (что реально шлёт источник, чем документация формата расходится с
практикой, какие числа сняты с живого потока). Их спрашивают `specs`, `reimpl`
и `ops`, и все трое — «из раздела `## Карта`». Отдельного файла нет — **так и
напиши**, и перечисли суррогаты: спеки, где наблюдения рассыпаны, комментарии в
адаптерах, `testdata`. Отдельный файл — лучшая форма, потому что при нескольких
внешних источниках наблюдения иначе не сойдутся в одном месте; но честный
перечень суррогатов лучше молчания, от которого три прохода ищут
несуществующий путь;
- архитектура и решения; журнал проскочивших дефектов;
- **единые точки проекта** — где генерируются идентификаторы и время, где
единственный парсер входного формата, где маппинг доменной ошибки в код ответа,
где общий путь приёма. Это материал для вопроса «не появился ли второй способ»;
если команда карты проекта их выгружает, здесь хватит ссылки на неё;
- **нумерованные артефакты** — путь миграций и правило нумерации: батч раздаёт
номера заранее, чтобы параллельные задачи не столкнулись файлами;
- где ведутся задачи (пайплайн только читает и сообщает исход);
- `testdata` и что в них лежит; куда можно писать временное;
- **каталоги, которые не трогают вовсе**.
Читают: все проходы.
### `## Типовые узлы` — необязателен, но без него рубрика беднеет
Роды узлов, из которых состоит проект (парсер входного формата, HTTP-обработчик,
репозиторий, воркер, клиент внешнего API, CLI-команда, файловое хранилище), и по
35 **специфичных для рода** проверяемых свойств к каждому.
**Рода, а не инвентарь того, что сейчас лежит в пакетах.** Список пишется по
природе проекта: род, который проект уже задумал, но ещё не написал, включать
полезно (рубрика на него понадобится ровно на той задаче, где его заводят); а
род, случайно оказавшийся в коде в одном экземпляре, — нет. Иначе раздел
протухает на каждой задаче и требует пересмотра, которого никто не делает.
Читает: `rubric`. Без раздела рубрика выродится в общие слова и повторит
конвенции — то есть станет applicative-проходом, ради отсутствия которого она и
существует.
### `## Прецеденты` — обязателен, хотя бы строкой «пусто»
**Воспроизведённые случаи этого проекта, с оракулом.** Не «здесь бывают гонки», а
«такой дефект здесь уже был, вот чем он воспроизведён»: что оказалось не так,
каким экспериментом или тестом это показано, какими числами, где это записано.
Каждый пункт — четыре вещи:
- **класс дефекта** — так, чтобы проход узнал его в другом месте;
- **как проявился** — симптом, который увидел человек;
- **чем воспроизведён** — команда, тест, стенд, замер. Без этого пункт
превращается в байку. **Регрессионный тест, написанный вместе с починкой,
годится** наравне с независимым экспериментом: он исполняемый и падает на
старом коде, а это всё, что требуется от оракула. Слабее он ровно в одном —
сформулирован уже зная ответ; это отмечается словом, а не служит поводом
выбросить пункт;
- **чем закончилось** — починка, правило линтера, пункт брифа, «ничего».
Зачем раздел существует. Прецедент — самая сильная опора, какая у прохода вообще
бывает: он проектный, воспроизводимый и уже однажды оказался правдой. Пока слота
не было, прецеденты вмерзали в charter'ы проходов — то есть каждый проект читал
про чужую контрольную точку в чужой СУБД и искал её у себя. Charter описывает
**форму класса**, бриф — **случай**.
Источники: журнал проскочивших дефектов, архивные отчёты триажа, `git log` по
починкам. Прецедентов нет — так и напиши: «прецедентов не накоплено», и это
честнее пустого раздела.
Читают: все проходы — свой класс; `triage` — как готовый оракул.
### `## Типовые ложноположительные` — необязателен, но без него отсев слепой
Находки, которые в **этом** проекте выглядят убедительно и всегда неверны. Это
единственный проектный вход в шаг триажа «отсев вкусовщины»: общие критерии
(«не меняет поведения, не влияет на стоимость следующего изменения, не нарушает
записанного») ловят вкусовщину, но не ловят находку, которая нарушает общее
правило **осознанно**.
Каждый пункт — формулировка находки, какой её выдаёт проход, плюс одна строка
«почему здесь это не дефект». Типичные обитатели: «дословное хранение надо
нормализовать» там, где дословность — инвариант; «повтор надо сделать
идемпотентным» там, где повтор невозможен по построению; «это надо вынести в
конфиг» там, где значение задано внешним протоколом.
Читает: `triage`.
### `## Вопросы к проходам` — необязателен
Проектные вопросы, адресованные **поимённо** конкретному проходу. Главный их
источник — журнал проскочивших дефектов: запись «проход не мог этого знать» чаще
всего лечится фактом в другом разделе, но иногда лечится не фактом, а
**вопросом**: «`ops`, спроси про поведение при откате бинаря поверх новой схемы»,
«`adversary`, проверь имена внутри архива». Такие вопросы живут здесь, а не в
charter'е: charter общий для всех проектов, а вопрос выведен из промаха в этом.
**Журнал — не единственный источник, а лучший.** У молодого проекта журнал пуст,
и слот тогда заполняется из того, что есть: незакрытые находки аудита, известное
расхождение кода с документацией, место, где решение принято «пока так». Правило
одно и не смягчается — **у каждого вопроса указан провенанс**, и по нему видно,
насколько он выстрадан: «журнал, запись такая-то» весит больше, чем «открытая
находка аудита».
Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Проход, увидев свой блок, задаёт
эти вопросы **дополнительно** к обязательным — и отвечает на них в выводе явно.
Читают: проходы, названные поимённо.
### `## Триггеры` — необязателен
Проектная конкретизация правила выбора профиля: какие пути и контракты означают
`deep`; что считается «поведением, видимым снаружи»; при каком изменении
запускается `reimpl`. Умолчания записаны в самом скилле и работают без этого
раздела — но общее правило говорит «изменение публичного контракта», а какой
контракт публичный, знает только проект.
Читают: скилл конвейера, пайплайн задачи.
### `## Недоступно проверке` — обязателен, и делится на два подраздела
Раздел целиком уезжает в границы покрытия финального отчёта — он существует ровно
затем, чтобы «критичных проблем не обнаружено» никогда не читалось как «проверено
всё». Но внутри лежат **два разных класса**, и смешивать их нельзя: при следующем
промахе один пересматривается, другой нет.
#### `### Не проверит ни один проход`
Принципиально недоступное: поведение внешних систем и их будущих версий, реальный
профиль нагрузки, соответствие сохранённого действительности, завязка внешних
потребителей на текущую форму, суждение «а нужна ли эта функциональность».
Этот список не пересматривается по факту промаха: дефект отсюда — не ошибка
конвейера, а его честная граница. Он меняется только когда меняется сам проект
(появился стенд, появился второй потребитель, появилась телеметрия).
#### `### Перестали проверять сознательно`
Решения о сужении: перестали звать проход, понизили профиль правилом, сузили
класс проверяемого, сняли правило линтера как шумное. Каждый пункт — **что
перестали, когда и почему**, со ссылкой на запись журнала ревью.
Этот список **пересматривается первым**, как только что-то проскочило: первый
вопрос по любому пропущенному дефекту — «не тот ли это класс, который мы перестали
проверять». Пункт, из-за которого дефект проскочил, либо возвращается, либо
получает строку «оставляем, цена поимки выше цены дефекта» с датой.
Оба подраздела обязательны; пустой называется пустым.
Читает: `triage`; каждый проход — свою часть.
## Правила ведения
- **Бриф не пересказывает документацию проекта.** Факт, записанный в `CLAUDE.md`
или в архитектуре, попадает сюда ссылкой и одной строкой сути. Два дома для
одного факта разъезжаются, и разошедшийся бриф хуже отсутствующего: он выглядит
актуальным. Исключение одно — раздел инвариантов, он цитируется.
- **Числа — с провенансом, и провенанс проверяется переходом по ссылке.** «Тела
доходили до 42 МБ (замер, ссылка)». Число без источника проход не имеет права
использовать как утверждение. Отдельный и более коварный случай — **число, чей
источник по ссылке не подтверждается**: в документе по ссылке другое число, или
его там нет вовсе. Такое число не выбрасывается и не переписывается по догадке:
оно остаётся с пометкой «расходится с источником: там <что нашли>», а проход
обязан читать его как условие, а не как замер. Молча подставить «правильное»
число хуже всего — расхождение перестанет быть видно, а причина его останется.
- **Пустой пункт называется пустым.** «Внешних зависимостей нет — смотри на диск
и на СУБД», «прецедентов не накоплено», «наблюдений на живых данных не ведём»,
«измеренных чисел нагрузки нет», «сознательно ничего не отключали». Отсутствие
строки читается проходом как «здесь не написали», и он тратит обязательный
вопрос впустую либо заполняет пробел выдумкой. Прямое «пусто» стоит одной
строки и экономит проход целиком.
- **Назначенное помечается назначенным.** Бриф отражает решения проекта, но
местами оказывается **первым** местом, где решение вообще записано: severity у
инварианта, адресат дорогой проверки, периметр, восстановленный из конфига.
Так можно — молчать хуже, — но пометка обязательна («выведена по обратимости»,
«назначен брифом, владельцем не подтверждён»). Проход имеет право знать, чьё
это суждение, а владелец — увидеть, что за него что-то решили.
- **Что вне модели — называется явно.** Это относится и к угрозам, и к нагрузке,
и к классам находок, которые проект сознательно перестал проверять (последние —
в свой подраздел `## Недоступно проверке`, а не вперемешку с принципиальным).
- **Бриф подчиняется промоуту.** Свойство, ставшее правилом линтера, из брифа
вычёркивается — как и из конвенций, и из charter'ов (см.
[promote.md](promote.md), шаг 3).
- **Когда обновлять:** сменился гейт; появился новый контур, зависимость или
источник входа; журнал ревью получил запись вида «проход не мог этого знать»;
воспроизвели дефект — он идёт в `## Прецеденты`. Планового пересмотра нет.
- **Заводится и обновляется шагом, а не руками** — скиллом
`av-dev-pipeline:project-brief`. Он же вызывается автоматически, когда конвейер
или пайплайн задачи не нашли брифа ни по одному пути.
- **Бриф ведёт проект**, а не плагин. Файл живёт в репозитории проекта; плагин
его читает и заводит по шаблону, но не хранит у себя и не подменяет.
@@ -0,0 +1,85 @@
# Откуда проход берёт проектную конкретику
Конвейер общий, находки — проектные. Проход, не знающий, что в этом проекте
нельзя нарушать, чем краснеет гейт и сколько данных реально идёт через узел,
выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать.
Отдельного файла-брифа **нет**. Проектная конкретика живёт в документах канона
`av-dev-pm`, и проход читает их напрямую: пути жёсткие, посредник не нужен, а
второй дом для тех же фактов разошёлся бы и выглядел актуальным.
Определение канона — в плагине `av-dev-pm`,
`skills/canon/references/canon.md`. Здесь только карта «что нужно проходу →
где это лежит».
## Карта
| Что нужно проходу | Где лежит |
| --- | --- |
| что система делает и **чего не делает**, граница домена | `docs/passport.md` |
| инварианты **с severity рядом с формулировкой** | `CLAUDE.md`, раздел инвариантов |
| команда гейта, чем краснеет безусловно, чего в нём нет, кто гоняет дорогое | `CLAUDE.md`, семантика гейта |
| что запускать запрещено, с путями; `testdata`; куда писать временное; имя основной ветки | `CLAUDE.md` |
| компоненты и capability, окружение, внешние зависимости поимённо, наблюдатель, характер потока, единые точки проекта | `docs/architecture.md` |
| чем физически лежит запись, что при чтении и записи, настройки с числовым значением | `docs/database.md` |
| периметр, недоверенный вход, из чего строятся пути и ключи, что вне модели | `docs/security.md` |
| измеренные числа **с провенансом**, поведение внешних систем на самом деле | `docs/research/` |
| конвенции прозой и **что уже механизировано** правилом | `docs/conventions/` |
| почему решено так, отвергнутые варианты | `docs/adr/` |
| типовые узлы, типовые ложноположительные, вопросы к проходам, недоступно проверке | `docs/review.md`, раздел настройки |
| прецеденты: воспроизведённые дефекты с оракулом | `docs/review.md`, журнал |
| нормативное поведение и дельты изменения | `openspec/specs/`, `openspec/changes/<id>/specs/` |
## Сшивать обязаны проходы
Раньше эти факты лежали рядом в одном файле, и соседство работало само. Теперь
они разложены по домам, и **проход обязан собрать их сам** — иначе снимет верное
число и честно понизит находку до гипотезы, потому что сравнить будет не с чем.
Два обязательных стыка:
- **замер + настройка.** «Пик 768 МиБ» — аномалия только рядом со строкой
«запись лежит сжатой и распаковывается целиком»; «блокировка удерживалась
5.019 с» — гарантированный отказ соседа только рядом с известным таймаутом
занятости. Числа в `docs/research/`, настройки в `docs/database.md`, и оба
читает `ops`, `adversary`, `reimpl`.
- **инвариант + обратимость.** severity берётся из `CLAUDE.md`; если её там
нет — она **выводится по обратимости последствия** и помечается «выведена по
обратимости», а не выдаётся за решение проекта.
## Деградация — поразрядная
Документа нет — деградирует то, что из него читалось, и **только оно**. Каждый
проход пишет свою строку в границы покрытия; триаж сводит их в одну.
| Нет документа | Что деградирует |
| --- | --- |
| `CLAUDE.md` без инвариантов | `critical` по основанию «нарушен инвариант проекта» не присваивается никем |
| `docs/security.md` | `adversary` не знает периметра — формулирует условиями, `critical` не ставит |
| `docs/research/` | числа неизвестны `ops`, `adversary`, `reimpl` — все трое формулируют условиями |
| `docs/database.md` | замер не с чем сравнить: находка не поднимается выше гипотезы |
| `docs/passport.md` | `architecture` теряет границу домена и вырождается в общее мнение |
| `docs/review.md` | `triage` отсеивает вслепую: типовых ложноположительных нет |
| `docs/architecture.md` | «не появился ли второй способ» не проверяется — единых точек не знает никто |
Строка в границах покрытия обязана называть **причину**: «`docs/security.md` в
проекте нет» читается иначе, чем «есть, но периметр не назван». Без причины
строка неотличима от «мы просто не стали» и перестаёт читаться на третьей задаче.
**Документов канона нет вовсе** — проект не приведён к канону. Это не повод
работать вслепую: скажи об этом строкой и предложи `av-dev-pm:canon`. Одна
операция на проект против деградации на каждой задаче.
## Правило чтения
- **Читай в источнике, не по памяти.** Документы правятся по ходу работы, в том
числе этой же задачей.
- **Число без провенанса — условие, а не утверждение.** Число, чей источник по
ссылке не подтвердился, читается как условие и **называется расходящимся**, а
не подменяется догадкой.
- **Пустое, названное пустым, — это факт.** «Внешних зависимостей нет — смотри
на диск и на СУБД» экономит обязательный вопрос. Отсутствие строки — не факт,
а пробел, и его надо назвать в границах покрытия.
- **Свойство, ставшее правилом линтера, из конвенций удалено** и лежит в
перечне механизированного в `docs/conventions/README.md`. Проверять его
проходом — тратить внимание на уже проверенное.
@@ -24,7 +24,7 @@
проход, чьи находки не доезжают никогда, — кандидат на `drop` (см. проход, чьи находки не доезжают никогда, — кандидат на `drop` (см.
[calibration.md](calibration.md)). [calibration.md](calibration.md)).
- Место записи — конвенции проекта, файл или нужный файл каталога (путь — в - Место записи — конвенции проекта, файл или нужный файл каталога (путь — в
разделе `## Карта` брифа). Если каталог `docs/conventions/`). Если
тема относится к поведению системы, а не к тому, как мы пишем код, — это не тема относится к поведению системы, а не к тому, как мы пишем код, — это не
конвенция, а требование: заводится дельта-спека обычным путём. конвенция, а требование: заводится дельта-спека обычным путём.
@@ -51,7 +51,7 @@
хук блокирует любой коммит, и правило снимут первым же раздражённым движением. хук блокирует любой коммит, и правило снимут первым же раздражённым движением.
Приводить код в соответствие — часть шага 2, отдельным коммитом. Приводить код в соответствие — часть шага 2, отдельным коммитом.
## Шаг 3. Удаление из конвенций, из брифа и из промптов ## Шаг 3. Удаление из конвенций и из промптов
**Шаг, который пропускают чаще всего, и единственный, ради которого затевались **Шаг, который пропускают чаще всего, и единственный, ради которого затевались
первые два.** первые два.**
@@ -61,13 +61,15 @@
- из файла конвенций убирается формулировка правила; остаётся, если нужно, одна - из файла конвенций убирается формулировка правила; остаётся, если нужно, одна
строка «проверяется линтером `<имя>`» — но только там, где без неё раздел строка «проверяется линтером `<имя>`» — но только там, где без неё раздел
теряет связность; теряет связность;
- **из брифа проекта** убирается соответствующий пункт, а в разделе `## Карта` - правило переезжает в **перечень механизированного в
правило переезжает в перечень «механизировано и потому проходом по конвенциям `docs/conventions/README.md`** — со ссылкой на место механизации: конфиг
не проверяется»; линтера, собственный анализатор, тест-сканер исходников. Непойманное место
означает, что проход будет добросовестно проверять уже проверенное;
- из контекста инструмента спек убирается дубль, если он там был. - из контекста инструмента спек убирается дубль, если он там был.
Charter'ы проходов при этом **не правятся**: они общие и живут в плагине, а Charter'ы проходов при этом **не правятся**: они общие и живут в плагине, а
предмет проверки приходит из брифа. Именно поэтому шаг 3 стал дешевле, чем был: предмет проверки приходит из документов проекта. Именно поэтому шаг 3 дешевле,
чем был:
вычеркнуть строку в одном файле проекта, а не в девяти промптах. вычеркнуть строку в одном файле проекта, а не в девяти промптах.
Практический критерий: **в прозаических конвенциях остаётся только то, что Практический критерий: **в прозаических конвенциях остаётся только то, что
@@ -1,42 +1,64 @@
# Журнал проскочивших дефектов # Журнал дефектов
Артефакт проекта, а не плагина: файл живёт в репозитории (путь — в разделе Артефакт проекта, а не плагина: файл живёт в репозитории **`docs/review.md`**,
`## Карта` брифа, по умолчанию `docs/review-journal.md`). Здесь описано, зачем он слот канона `av-dev-pm`. Здесь описано, зачем он и какой формы, потому что без
и какой формы, потому что без него конвейер не учится: находки закрываются, него конвейер не учится: находки закрываются, причины непоймания теряются, и один
причины непоймания теряются, и один и тот же класс проскакивает второй раз. и тот же класс проскакивает второй раз.
Тот же файл держит **настройку конвейера под проект** — типовые узлы, типовые
ложноположительные, вопросы к проходам, недоступно проверке. Это не соседство по
случаю: все четыре раздела — производные калибровки, а журнал им источник.
## Что туда попадает ## Что туда попадает
Дефект, который **прошёл ревью и всплыл позже**. Записывается **сразу**, а не **Воспроизведённый дефект — с пометкой `проскочил` или `пойман ревью`.**
ретроспективно: со временем теряется не сам факт, а причина непоймания — Записывается **сразу**, а не ретроспективно: со временем теряется не сам факт, а
единственное, ради чего журнал существует. причина непоймания — единственное, ради чего журнал существует.
Реализованные задачи, находки ревью и принятые решения сюда не пишутся: у них Пометка делит журнал на две выборки с разным назначением:
есть коммит, спека и задача. Здесь только промахи конвейера.
- **проскочил** — эвал-сет для калибровки конвейера. Реальный промах сильнее
синтетической пробы: синтетические смещены в сторону тех, которые уже умеешь
придумывать;
- **пойман ревью** — прецеденты с оракулом. Самая сильная опора, какая у прохода
бывает: проектная, воспроизводимая и однажды уже оказавшаяся правдой. Без
журнала они остаются только в отчётах триажа в архиве change, где их никто не
ищет.
Реализованные задачи и принятые решения сюда не пишутся: у них есть коммит, спека
и `docs/adr/`.
Отдельно сюда попадают **решения о составе прогонов**: перестали звать проход, Отдельно сюда попадают **решения о составе прогонов**: перестали звать проход,
понизили профиль правилом, сузили класс проверяемого. Не потому, что это промах, понизили профиль правилом, сузили класс проверяемого. Не потому, что это промах,
а потому, что здесь лежит цена: если что-то теперь проскочит, первый вопрос — а потому, что здесь лежит цена: если что-то теперь проскочит, первый вопрос —
«не тот ли это класс, который мы перестали проверять». «не тот ли это класс, который мы перестали проверять».
Каждое такое решение обязано получить **строку в брифе** — в подразделе Каждое такое решение обязано получить строку в подразделе **«Перестали проверять
`### Перестали проверять сознательно` раздела `## Недоступно проверке`. Журнал сознательно»** раздела «Недоступно проверке» того же файла. Журнал хранит «почему
хранит «почему тогда так решили», бриф — то, во что смотрит каждый прогон. тогда так решили», раздел настройки — то, во что смотрит каждый прогон. Решение,
Решение, оставшееся только в журнале, в границы покрытия не доедет. оставшееся только в журнале, в границы покрытия не доедет.
## Форма записи ## Форма записи
``` ```
## ГГГГ-ММ-ДД — <краткое последствие> ## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман]
- **Где:** путь:строка либо «конвейер, а не код» - **Где:** путь:строка либо «конвейер, а не код»
- **Симптом:** как обнаружилось, кем и когда - **Симптом:** как обнаружилось, кем и когда
- **Причина:** что на самом деле было не так - **Причина:** что на самом деле было не так
- **Почему не поймали:** какой проход обязан был найти и что ему помешало - **Чем воспроизведён:** тест, команда, замер — с числами
- **Что меняем:** правило прохода, шаг гейта, конвенция, пункт брифа — либо - **Почему не поймали:** только для проскочивших — какой проход обязан был найти
«ничего, цена поимки выше цены дефекта» и что ему помешало
- **Что меняем:** правило прохода, шаг гейта, конвенция, факт в документе
проекта — либо «ничего, цена поимки выше цены дефекта»
``` ```
Пункт «чем воспроизведён» отличает запись от байки: без него на неё нельзя
сослаться как на оракул. Регрессионный тест, написанный вместе с починкой,
годится наравне с независимым экспериментом — он исполняемый и падает на старом
коде. Слабее он ровно в одном: сформулирован уже зная ответ, и это отмечается
словом.
Последний пункт важнее остальных. Вывод «ничего не меняем» — законный исход: не Последний пункт важнее остальных. Вывод «ничего не меняем» — законный исход: не
всякий дефект стоит того, чтобы усложнять ради него ревью каждой задачи. всякий дефект стоит того, чтобы усложнять ради него ревью каждой задачи.
@@ -44,24 +66,27 @@
Три адреса, и выбор между ними — половина ценности журнала: Три адреса, и выбор между ними — половина ценности журнала:
- **в бриф проекта** — если проход не мог знать факта: объём, характер потока, - **в документ проекта** — если проход не мог знать факта. Адрес зависит от рода
что здесь необратимо, какой шаг гейта красит безусловно. Самый частый адрес и факта, и карта их всех — [project-facts.md](project-facts.md): объём и
самый дешёвый. Сюда же — **воспроизведённый случай** (раздел `## Прецеденты`: измеренное число → `docs/research/`; настройка хранилища → `docs/database.md`;
класс, симптом, чем воспроизведён, чем закончилось) и **вопрос конкретному что необратимо и какой шаг гейта красит безусловно → `CLAUDE.md`; периметр и
проходу**, если промах лечится не фактом, а заданным вопросом (раздел недоверенный вход → `docs/security.md`. **Вопрос конкретному проходу**, если
`## Вопросы к проходам`). Прежде чем править charter, проверь, не хватит ли промах лечится не фактом, а заданным вопросом, → раздел «Вопросы к проходам»
этих двух разделов: charter общий для всех проектов, бриф — про этот. того же `docs/review.md`. Самый частый адрес и самый дешёвый. Прежде чем
править charter, проверь, не хватит ли факта или вопроса: charter общий для
всех проектов, документ — про этот.
- **в конвенции или в правило линтера** — если свойство выражается - **в конвенции или в правило линтера** — если свойство выражается
детерминированно (процедура — [promote.md](promote.md)). детерминированно (процедура — [promote.md](promote.md)).
- **в charter прохода** — если сломан **метод**, а не знание. Правка charter'а - **в charter прохода** — если сломан **метод**, а не знание. Правка charter'а
меняет поведение во всех проектах, поэтому она требует калибровки меняет поведение во всех проектах, поэтому она требует калибровки
([calibration.md](calibration.md)) и обоснования, почему это не лечится ([calibration.md](calibration.md)) и обоснования, почему это не лечится фактом
брифом. в документе проекта.
## Что журнал даёт конвейеру ## Что журнал даёт конвейеру
- **пробы для калибровки** — реальный проскочивший дефект сильнее синтетического: - **пробы для калибровки** — выборка по пометке `проскочил`;
синтетические смещены в сторону тех, которые уже умеешь придумывать; - **готовые оракулы** — выборка по пометке `пойман ревью`: находка того же
класса подтверждается ссылкой на запись, а не рассуждением;
- **основание для правил конвейера** — требование называть запущенные проходы - **основание для правил конвейера** — требование называть запущенные проходы
поимённо, отказ от чисел, производных от размера корпуса, и правило поимённо, отказ от чисел, производных от размера корпуса, и правило
последовательного прогона выведены из конкретных записей, а не из общих последовательного прогона выведены из конкретных записей, а не из общих
+12 -10
View File
@@ -23,17 +23,19 @@ description: Проводит несколько задач разом — пл
проход `review-specs` финальной сверки. Проекта без OpenSpec это касается так проход `review-specs` финальной сверки. Проекта без OpenSpec это касается так
же, как одиночного пайплайна (см. его раздел «Предпосылки»). же, как одиночного пайплайна (см. его раздел «Предпосылки»).
- **Скиллы зовутся с пространством имён**: `av-dev-pipeline:task-pipeline`, - **Скиллы зовутся с пространством имён**: `av-dev-pipeline:task-pipeline`,
`av-dev-pipeline:review-pipeline`, `av-dev-pipeline:project-brief`. Короткое имя `av-dev-pipeline:review-pipeline`, `av-dev-pm:tasks`. Короткое имя
может разрешиться в устаревшую проектную копию, и это произойдёт молча — в может разрешиться в устаревшую проектную копию, и это произойдёт молча — в
charter'е сабагента пиши полное имя, он твоего контекста не видит. charter'е сабагента пиши полное имя, он твоего контекста не видит.
- **Проектные копии этих скиллов и агентов при установке плагина удаляются.** - **Проектные копии этих скиллов и агентов при установке плагина удаляются.**
Перед стартом прочитай `CLAUDE.md` проекта и бриф ревью (`docs/review-brief.md`): Перед стартом прочитай `CLAUDE.md` проекта: оттуда берутся **имя основной
из него берутся **основная ветка** (раздел `## Карта` — она подставляется в ветки** (оно подставляется в каждую команду git ниже), команда и семантика
каждую команду git ниже), команда гейта, инварианты и раскладка нумерованных гейта, инварианты и что запускать запрещено. Раскладка нумерованных артефактов —
артефактов. Брифа нет — заведи его Skill'ом `docs/database.md` и `docs/.pm.json` (ключ `migrations`).
**`av-dev-pipeline:project-brief`** один раз, до первой волны: иначе каждая
задача батча заплатит деградированным ревью, а имя основной ветки придётся **Документов канона нет — проект к нему не приведён.** Скажи это строкой и
предложи `av-dev-pm:canon` **до первой волны**: иначе каждая задача батча
заплатит поразрядной деградацией ревью, а имя основной ветки придётся
угадывать. угадывать.
## Границы ## Границы
@@ -92,14 +94,14 @@ fast-forward. Ветки после вливания удаляются.
- трогает конкурентность: транзакции, блокировки, фоновые циклы, общее - трогает конкурентность: транзакции, блокировки, фоновые циклы, общее
состояние; состояние;
- трогает размер тела, буфер, память, сжатие, ретеншен, темп потока; - трогает размер тела, буфер, память, сжатие, ретеншен, темп потока;
- её тема названа в разделах `## Прод и поток` или `## Прецеденты` брифа как - её тема названа в `docs/research/` или в журнале `docs/review.md` как место,
место, где уже мерили или уже ломалось. где уже мерили или уже ломалось.
Ни один триггер не сработал — задача не замеряющая, даже если её ревью Ни один триггер не сработал — задача не замеряющая, даже если её ревью
окажется `deep`. `deep` про глубину проверки, замеряющая — про соревнование за окажется `deep`. `deep` про глубину проверки, замеряющая — про соревнование за
железо; это разные вопросы, и совпадают они не всегда; железо; это разные вопросы, и совпадают они не всегда;
- **нумерованные артефакты — номера раздаёт оркестратор заранее.** Если проект - **нумерованные артефакты — номера раздаёт оркестратор заранее.** Если проект
нумерует миграции или подобные файлы (путь — из брифа), посмотри последний нумерует миграции (путь — `docs/.pm.json`, ключ `migrations`), посмотри последний
номер и **раздай номера всем задачам, которые, вероятно, их добавят**, до номер и **раздай номера всем задачам, которые, вероятно, их добавят**, до
запуска. Номер уходит в charter сабагента, и он берёт назначенный, а не запуска. Номер уходит в charter сабагента, и он берёт назначенный, а не
«следующий свободный». «следующий свободный».
+52 -38
View File
@@ -15,30 +15,28 @@ description: Автономно проводит одну задачу чере
## Предпосылки ## Предпосылки
- **OpenSpec и скиллы `opsx:*`**внешняя обвязка, на которой стоят шаги 2, 3, 6 - **OpenSpec и скиллы `opsx:*`жёсткая предпосылка, а не опция.** На них стоят
и 8, а также проход `review-specs` и профиль `design` (они завязаны на шаги 2, 3, 6 и 8, проход `review-specs` и профиль `design` (они завязаны на
`openspec/changes/<id>/specs/*/spec.md` и на `openspec validate --strict`). В `openspec/changes/<id>/specs/*/spec.md` и на `openspec validate --strict`).
проекте без OpenSpec эти шаги упадут на «нет такого скилла»: либо подключаем **Проект без OpenSpec этим пайплайном не ведётся** — подключай OpenSpec, а не
OpenSpec, либо цикл вырождается в «прочитать задачу → код → ревью кода → вырождай цикл: ветка деградации здесь не пишется, потому что непроверенная
коммит», и об отсутствии спекового контура говорится в докладе. ветка деградации хуже честного отказа.
- **Скиллы зовутся с пространством имён** — `av-dev-pipeline:review-pipeline`, - **Скиллы зовутся с пространством имён** — `av-dev-pipeline:review-pipeline`,
`av-dev-pipeline:project-brief`. Короткое имя может разрешиться в устаревшую `av-dev-pm:docs`, `av-dev-pm:tasks`. Короткое имя может разрешиться в
проектную копию, и это произойдёт молча. устаревшую проектную копию, и это произойдёт молча.
- **Проектные копии этих скиллов и агентов удаляются при установке плагина** - **Проектные копии этих скиллов и агентов удаляются при установке плагина**
(`.claude/skills/{task-pipeline,review-pipeline,task-batch}`, (`.claude/skills/{task-pipeline,review-pipeline,task-batch}`,
`.claude/agents/<проект>-review-*.md`). Две копии одного скилла расходятся, и `.claude/agents/<проект>-review-*.md`). Две копии одного скилла расходятся, и
побеждает та, что короче названа. побеждает та, что короче названа.
Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается, если ещё
(архитектура, конвенции), если ещё не в контексте. Проектные факты, нужные ревью не в контексте. Проектные факты, нужные ревью — инварианты, семантика гейта,
— инварианты, команда гейта, объёмы, модель угроз, прецеденты, — живут в брифе объёмы, модель угроз, прецеденты, — живут в **документах канона** `av-dev-pm`;
(`docs/review-brief.md`, контрактв references конвейера ревью). карта «что где»`references/project-facts.md` конвейера ревью.
**Брифа нет ни по одному пути — заведи его, а не работай в деградированном **Документов канона нет — проект к нему не приведён.** Скажи это строкой и
режиме.** Вызови Skill **`av-dev-pipeline:project-brief`**: он соберёт бриф из предложи скилл `av-dev-pm:canon`: одна операция на проект против поразрядной
`CLAUDE.md`, архитектуры, файла задач и конвенций, покажет человеку и вернёт деградации на каждой задаче. Работу при этом не останавливай.
путь, который дальше передаётся ревью. Это механика: спрашивать разрешения не
нужно. Один шаг один раз на проект — против деградации на каждой задаче.
## Границы: чем пайплайн не владеет ## Границы: чем пайплайн не владеет
@@ -109,7 +107,7 @@ description: Автономно проводит одну задачу чере
в объявленных границах. в объявленных границах.
**Что остатком не является — правило живёт не здесь.** Канонический текст с обеими **Что остатком не является — правило живёт не здесь.** Канонический текст с обеими
оговорками — в плагине `av-dev-tasks`, скилл `av-dev-tasks:session`, раздел оговорками — в плагине `av-dev-pm`, скилл `av-dev-pm:session`, раздел
`## Вопрос, блокер, необратимое`, подраздел «Отличать вопрос от застревания». `## Вопрос, блокер, необратимое`, подраздел «Отличать вопрос от застревания».
Правило принадлежит управлению задачами, потому что решает **сделана задача или Правило принадлежит управлению задачами, потому что решает **сделана задача или
вышла**, — это исход планирования, а не исполнения. **Ссылайся, не вышла**, — это исход планирования, а не исполнения. **Ссылайся, не
@@ -122,7 +120,7 @@ description: Автономно проводит одну задачу чере
Оба порога — стоп: первый поднимает решение до начала записи, второй даёт исход Оба порога — стоп: первый поднимает решение до начала записи, второй даёт исход
«не доведена». «не доведена».
Плагин `av-dev-tasks` не подключён — правило не отменяется, а становится Плагин `av-dev-pm` не подключён — правило не отменяется, а становится
осторожнее: прежде чем записать зависящее от нерешённого куда бы то ни было — осторожнее: прежде чем записать зависящее от нерешённого куда бы то ни было —
в хранилище, в журнал, в витрину или наружу, — спрашивай человека. в хранилище, в журнал, в витрину или наружу, — спрашивай человека.
@@ -183,7 +181,7 @@ description: Автономно проводит одну задачу чере
### 4. (Нетривиальная) Ревью предложения — профиль `design`, ДО кода ### 4. (Нетривиальная) Ревью предложения — профиль `design`, ДО кода
Первый чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`** с профилем Первый чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`** с профилем
`design`, ссылкой на change `<id>` и путём к брифу. Он запустит `review-specs` `design` и ссылкой на change `<id>`. Он запустит `review-specs`
(режим «дизайн ДО кода»), `review-rubric` (фаза 1: приёмочные критерии для (режим «дизайн ДО кода»), `review-rubric` (фаза 1: приёмочные критерии для
задуманного узла) и `review-architecture` по предложению. задуманного узла) и `review-architecture` по предложению.
@@ -202,14 +200,14 @@ description: Автономно проводит одну задачу чере
### 6. Написать код — `opsx:apply` ### 6. Написать код — `opsx:apply`
Вызови Skill `opsx:apply` для реализации `tasks.md`. Код — по конвенциям проекта Вызови Skill `opsx:apply` для реализации `tasks.md`. Код — по конвенциям проекта
(файл назван в разделе `## Карта` брифа). Меняешь схему — обнови её описание в (каталог `docs/conventions/`). Меняешь схему — обнови её описание в
документации тем же change, если проект этого требует: гейт обычно это проверяет. документации тем же change, если проект этого требует: гейт обычно это проверяет.
Прогони гейт и добейся зелёного — он же гейт следующего шага. Прогони гейт и добейся зелёного — он же гейт следующего шага.
**Поведенческая верификация.** Если задача меняет реальное поведение (новый **Поведенческая верификация.** Если задача меняет реальное поведение (новый
эндпоинт, разбор входа, схема, форма ответа) — зелёных юнит-тестов мало. Подними эндпоинт, разбор входа, схема, форма ответа) — зелёных юнит-тестов мало. Подними
изменение вживую командой из раздела `## Команды` брифа и прогони сценарий. изменение вживую командой из раздела команд `CLAUDE.md` и прогони сценарий.
Пропусти только для чисто внутренних правок без наблюдаемого рантайма. Пропусти только для чисто внутренних правок без наблюдаемого рантайма.
**Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца **Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца
@@ -218,10 +216,10 @@ description: Автономно проводит одну задачу чере
### 7. Ревью кода — Skill `av-dev-pipeline:review-pipeline` ### 7. Ревью кода — Skill `av-dev-pipeline:review-pipeline`
Второй чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`**, дав ссылку Второй чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`**, дав ссылку
на change `<id>`, базу диффа, путь к брифу, профиль **и режим запуска**. на change `<id>`, базу диффа, профиль **и режим запуска**.
**Правило выбора профиля живёт в скилле конвейера** (раздел «Профили»), проектные **Правило выбора профиля живёт в скилле конвейера** (раздел «Профили»), проектные
триггеры — в разделе `## Триггеры` брифа. Здесь оно не пересказывается: три триггеры — в `docs/review.md`, если записаны. Здесь оно не пересказывается: три
копии одного правила расходятся, и работать будет та, которую прочитали копии одного правила расходятся, и работать будет та, которую прочитали
последней. Помни ровно одно — **профиль выбирается по факту изменения, а не по последней. Помни ровно одно — **профиль выбирается по факту изменения, а не по
ощущению важности**, и посмотри таблицу перед вызовом. ощущению важности**, и посмотри таблицу перед вызовом.
@@ -271,22 +269,38 @@ description: Автономно проводит одну задачу чере
### 9. Синк документации ### 9. Синк документации
Ревью выполненного — до этого шага. Затем: Ревью выполненного — до этого шага. Затем **вызови Skill `av-dev-pm:docs`**: он
владеет содержимым документов канона и ведёт чек-лист синка. Плагина нет —
пройди чек-лист сам по списку ниже.
- суть переехавшего решения — в документацию проекта (архитектура, журнал **Правило одно и оно жёсткое: принуждённое отрицание.** Доклад обязан назвать
решений), если её там ещё нет; **каждый** документ канона — либо чем он обновлён, либо «не требуется, потому
- менялась схема — её описание обновлено тем же change; что…». Нетронутые группируются одной строкой с общей причиной. Список триггеров
- новое, узнанное о внешнем формате или о данных, — в тот файл проекта, который прозой уже проверен на живом проекте и дал 6 записей ADR на 43 изменения;
это накапливает; такой файл обычно ценнее кода; работает только обязательное отрицание.
- воспроизведённый дефект (свой или чужой) — в раздел `## Прецеденты` брифа:
класс, симптом, чем воспроизведён, чем закончилось. Это единственный артефакт,
который делает следующее ревью умнее.
**Задачу пайплайн не закрывает.** Записи учёта — индекс, статус, спринт — он не Документы и их триггеры: `openspec/specs/` (поведение — вливает `opsx:archive`),
трогает вовсе: закрытие происходит **после приёмки** и делается владельцем `database.md` (тронуты миграции), `architecture.md` (новый компонент, граница,
спринта, а пайплайн на этом шаге стоит до коммита и до всякой приёмки. Скриптов внешняя зависимость), `adr/` (дорогой откат, намеренный отказ, пересмотр
и скиллов учёта не зови — их у тебя и нет: пути между плагинами не разрешаются, и прежнего), `research/` (узнали новое о внешних данных), `security.md` (новый
моста здесь намеренно не проложено. Твоё дело — назвать исход в докладе. недоверенный вход, токен, путь наружу), `conventions/` (промоут, включая
**удаление** формулировки, ставшей правилом линтера), `review.md`
(воспроизведённый дефект — с пометкой «проскочил» или «пойман ревью»),
`passport.md`, `CLAUDE.md`.
### 9а. Закрыть задачу
**Вызови Skill `av-dev-pm:tasks`** и попроси закрыть задачу как реализованную —
он владеет форматом и двигает строку между индексами сам. Путь к его скрипту не
выясняй и индексы руками не правь: мост между плагинами — вызов скилла, а не
путь.
Плагина в проекте нет — вызов не разрешится. Тогда **ничего не выдумывай**:
скажи в докладе, что учёт задач остаётся за владельцем, и назови исход.
**Приёмщик и исполнитель здесь совпадают**, и закрытие не окончательно: человек
на сессии может вернуть задачу (`reopen` с причиной). Поэтому доклад по критериям
приёмки — не формальность, а единственное, по чему приёмка вообще возможна.
### 10. Коммит ### 10. Коммит
+22 -8
View File
@@ -17,7 +17,10 @@ description: Привести проект к канону документов
пересказывается: два описания одной раскладки разъедутся, и работать будет то, пересказывается: два описания одной раскладки разъедутся, и работать будет то,
которое прочитали последним. Прочитай его **до** первой правки. которое прочитали последним. Прочитай его **до** первой правки.
Журнал версий — [references/changelog.md](references/changelog.md). - [references/skeletons.md](references/skeletons.md) — **что именно класть** в
каждый незаполненный слот. Своей формой заглушку не выдумывай: `docs.py`
узнаёт только плейсхолдер `<!-- заполнить: … -->` из шаблонов.
- [references/changelog.md](references/changelog.md) — журнал версий канона.
## Три правила, из которых всё следует ## Три правила, из которых всё следует
@@ -53,9 +56,13 @@ python3 $ds version --dir <корень> # версия кано
нельзя**. `check`, отчитавшийся «канон соблюдён» на проекте, где из шести файлов нельзя**. `check`, отчитавшийся «канон соблюдён» на проекте, где из шести файлов
три лишние, хуже отсутствующего. три лишние, хуже отсутствующего.
Машина проверяет пути, лишние файлы, битые ссылки, версию, нетронутые Машина **дрейфом** считает: отсутствующий путь канона, файл вне канона, битую
плейсхолдеры, маркеры долга и две сверки с кодом. **Ты** судишь о том, чего она ссылку, отставшую версию, capability без упоминания в обзоре, миграцию без правки
не умеет: `database.md`. **Замечанием** — незаполненный плейсхолдер и слабое упоминание
capability: незаполненный канон это переходное состояние, а не отказ. Маркеры
долга просто считает числом.
**Ты** судишь о том, чего она не умеет:
- **смысловой дубль** — `docs/specs/recognition.md` описывает то же, что - **смысловой дубль** — `docs/specs/recognition.md` описывает то же, что
capability `recognition`. Файлы разные, содержание одно; capability `recognition`. Файлы разные, содержание одно;
@@ -113,8 +120,8 @@ capability), `openspec/config.yaml`.
Порядок важен — он минимизирует окно, в котором ссылки битые: Порядок важен — он минимизирует окно, в котором ссылки битые:
1. `docs/.pm.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть; 1. `docs/.pm.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть;
2. каталоги канона и скелет: незаполненное — **одной честной информативной 2. каталоги канона и скелет **по [references/skeletons.md](references/skeletons.md)**:
строкой**, а не «TBD» (см. canon.md, «Пустое называется пустым»); незаполненное — одной честной информативной строкой, а не «TBD»;
3. переносы содержимого; 3. переносы содержимого;
4. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он 4. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он
владеет форматом задач, включая переименование транслитных слагов в владеет форматом задач, включая переименование транслитных слагов в
@@ -122,8 +129,15 @@ capability), `openspec/config.yaml`.
5. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`, 5. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`,
`CLAUDE.md`, `README.md`; `CLAUDE.md`, `README.md`;
6. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**; 6. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**;
7. шаг `docs.py check` в гейт проекта; 7. **шаг `docs.py check` в гейт проекта.** Путь к скрипту — переменной с
8. `docs.py check` — до зелёного в механизируемой части. умолчанием на канонический путь маркетплейса, чтобы переустановка плагина не
меняла `Taskfile`; шаг обязан **краснеть внятно**, если скрипт не найден, а не
пропускаться. Передай ему базу диффа (`--base`) той же переменной, что и
остальным шагам гейта: без неё сверка миграций со схемой не гоняется вовсе.
Пример строки покажи человеку — гейт принадлежит проекту, и правит его он;
8. `docs.py check` — до **отсутствия дрейфа**. Замечания (незаполненные
плейсхолдеры, слабое упоминание capability) остаются: незаполненный канон это
объявленное переходное состояние из шага 5, а не отказ.
### 5. Объяви переходное состояние ### 5. Объяви переходное состояние
@@ -8,6 +8,13 @@
Плейсхолдер ставится только там, где ответ **обязан** быть и его не спросили. Плейсхолдер ставится только там, где ответ **обязан** быть и его не спросили.
Всё, чего в проекте пока просто нет, описывается словами, а не плейсхолдером. Всё, чего в проекте пока просто нет, описывается словами, а не плейсхолдером.
**Шаблоны — единственное место, где правило канона копируется намеренно.**
`adr/README.md` и `review.md` уезжают в репозиторий проекта и обязаны там что-то
говорить; определение при этом остаётся в [canon.md](canon.md). Отсюда
обязанность: **правка такого правила в каноне тянет запись в
[changelog.md](changelog.md)** с указанием, какой файл проекта поднимает
`upgrade`. Без этого копия в проекте останется на старой версии молча.
## `docs/passport.md` ## `docs/passport.md`
```markdown ```markdown
+71 -24
View File
@@ -76,19 +76,25 @@ RETIRED = {
DEBT_MARKER = re.compile(r"<!--\s*канон:\s*(.+?)\s*-->") DEBT_MARKER = re.compile(r"<!--\s*канон:\s*(.+?)\s*-->")
PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->") PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->")
MD_LINK = re.compile(r"\[[^\]]*\]\(([^)]+)\)") MD_LINK = re.compile(r"\[[^\]]*\]\(\s*<?([^)>\s]+)>?(?:\s+[\"'(][^)]*)?\)")
FENCE = re.compile(r"^\s*(```|~~~)") FENCE = re.compile(r"^\s*(```|~~~)")
INLINE_CODE = re.compile(r"`[^`\n]*`")
def strip_code(text: str) -> str: def strip_code(text: str) -> str:
"""Выкинуть блоки кода: пути в примерах и шаблонах — не ссылки, и краснеть """Выкинуть блоки кода и вставки в обратных кавычках.
на них значит краснеть на каждом образце документа."""
Путь в примере или в шаблоне — не ссылка, и краснеть на нём значит краснеть
на каждом образце документа. Инлайн-код тоже: `[docs/backlog](docs/tasks/…)`
в тексте про подписи ссылок — иллюстрация, а не ссылка."""
out, inside = [], False out, inside = [], False
for line in text.splitlines(): for line in text.splitlines():
if FENCE.match(line): if FENCE.match(line):
inside = not inside inside = not inside
continue continue
out.append("" if inside else line) out.append("" if inside else INLINE_CODE.sub("", line))
return "\n".join(out) return "\n".join(out)
@@ -226,7 +232,9 @@ def check_placeholders_and_debt(root: Path, rep: Report) -> None:
text = strip_code(path.read_text(encoding="utf-8", errors="replace")) text = strip_code(path.read_text(encoding="utf-8", errors="replace"))
rel = path.relative_to(root) rel = path.relative_to(root)
for what in PLACEHOLDER.findall(text): for what in PLACEHOLDER.findall(text):
rep.error(f"{rel}: плейсхолдер шаблона не заполнен{what}") # Замечание, а не дрейф: незаполненный канон — объявленное переходное
# состояние, и краснеть на нём значит требовать выдумать содержание.
rep.note(f"{rel}: плейсхолдер шаблона не заполнен — {what}")
for what in DEBT_MARKER.findall(text): for what in DEBT_MARKER.findall(text):
rep.debt(f"{rel}: {what}") rep.debt(f"{rel}: {what}")
@@ -238,28 +246,60 @@ def check_capabilities(root: Path, rep: Report) -> None:
rep.skip("openspec/specs/ нет — сверка capability с архитектурой неприменима") rep.skip("openspec/specs/ нет — сверка capability с архитектурой неприменима")
return return
if not arch.exists(): if not arch.exists():
rep.skip(
"docs/architecture.md нет — capability не сверены с обзором "
"(об отсутствии файла сказано отдельной строкой)"
)
return return
text = arch.read_text(encoding="utf-8", errors="replace") text = arch.read_text(encoding="utf-8", errors="replace")
missing = [d.name for d in sorted(specs.iterdir()) if d.is_dir() and d.name not in text] for d in sorted(specs.iterdir()):
for name in missing: if not d.is_dir():
rep.error( continue
f"capability {name} есть в openspec/specs/, но не упомянута в " name = d.name
f"docs/architecture.md — обзор отстал от нормативных спек" # Засчитываем только явное упоминание: ссылку на спеку или имя в обратных
) # кавычках. Голая подстрока совпадает с именем пакета или CLI-команды и
# даёт ложное «упомянуто» — то есть проверку, проходящую не по той причине.
explicit = f"openspec/specs/{name}" in text or f"`{name}`" in text
loose = re.search(rf"\b{re.escape(name)}\b", text) is not None
if explicit:
continue
if loose:
rep.note(
f"capability {name}: в docs/architecture.md есть слово «{name}», но "
f"нет ни ссылки на openspec/specs/{name}, ни имени в обратных "
f"кавычках — проверь, это про capability или про пакет"
)
else:
rep.error(
f"capability {name} есть в openspec/specs/, но не упомянута в "
f"docs/architecture.md — обзор отстал от нормативных спек"
)
def changed_files(root: Path, base: str, rep: Report) -> list[str] | None: def changed_files(root: Path, base: str, rep: Report) -> list[str] | None:
try: """Объединение закоммиченного, рабочего дерева и untracked.
out = subprocess.run(
["git", "-C", str(root), "diff", "--name-only", f"{base}...HEAD"], Гейт гоняют ДО коммита, поэтому `base...HEAD` не видит ровно ту правку, ради
capture_output=True, которой проверка и заводилась: миграция уже лежит в дереве, но ещё не в
text=True, истории. Пропущенная правка выглядела бы как зелёный шаг."""
check=True, cmds = [
) ["diff", "--name-only", base],
except (subprocess.CalledProcessError, FileNotFoundError) as exc: ["ls-files", "--others", "--exclude-standard"],
rep.skip(f"сверка миграций пропущена: git не отдал дифф ({exc})") ]
return None seen: list[str] = []
return [line for line in out.stdout.splitlines() if line] for cmd in cmds:
try:
out = subprocess.run(
["git", "-C", str(root), *cmd],
capture_output=True,
text=True,
check=True,
)
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
rep.skip(f"сверка миграций пропущена: git не отдал дифф ({exc})")
return None
seen.extend(line for line in out.stdout.splitlines() if line)
return sorted(set(seen))
def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> None: def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> None:
@@ -292,17 +332,24 @@ def check_tasks(root: Path, rep: Report) -> None:
if not script.exists(): if not script.exists():
rep.skip(f"tasks.py не найден по пути {script} — согласованность задач не проверена") rep.skip(f"tasks.py не найден по пути {script} — согласованность задач не проверена")
return return
# cwd=root обязателен: tasks.py отвергает --dir вне текущего каталога, и без
# этого его отказ окружения (код 3) схлопнулся бы в наш дрейф (код 1).
proc = subprocess.run( proc = subprocess.run(
[sys.executable, str(script), "check", "--dir", str(tasks)], [sys.executable, str(script), "check", "--dir", "docs/tasks"],
capture_output=True, capture_output=True,
text=True, text=True,
cwd=str(root),
) )
if proc.returncode == 0: if proc.returncode == 0:
return return
if proc.returncode == 1: if proc.returncode == 1:
rep.error("tasks.py check нашёл дрейф в docs/tasks/ — разбирать его командой tasks.py") rep.error("tasks.py check нашёл дрейф в docs/tasks/ — разбирать его командой tasks.py")
else: else:
rep.error(f"tasks.py check отказал с кодом {proc.returncode}: {proc.stderr.strip()}") # Чужой код выхода не выдаём за свой: 3 это окружение, а не дрейф.
rep.skip(
f"tasks.py check не отработал (код {proc.returncode}): "
f"{(proc.stderr or proc.stdout).strip().splitlines()[0] if (proc.stderr or proc.stdout).strip() else 'без сообщения'}"
)
# --- Отчёт ------------------------------------------------------------------ # --- Отчёт ------------------------------------------------------------------
+34 -50
View File
@@ -64,31 +64,24 @@ description: Вести содержимое документов канона
**ADR цитирует решение оттуда и ссылается на источник.** Не пересказывает и не **ADR цитирует решение оттуда и ссылается на источник.** Не пересказывает и не
сочиняет заново. сочиняет заново.
Заводится, когда верно одно из трёх: **Триггер заведения, форма имени и правило замены — в
[каноне](../canon/references/canon.md), раздел `adr/`.** Здесь они не
повторяются: копия правила расходится с оригиналом на первой же смене версии
канона, а расходится незаметно.
- **дорогой откат** — переделка стоит дороже переписывания одного файла; Твоя часть — **применить триггер к этой задаче и сказать вслух, сработал он или
- **намеренный отказ** от очевидного подхода — чтобы не переоткрывать «а почему нет**. Строка «adr/ — не требуется: решение рутинное» и есть то, ради чего
мы не сделали X»; чек-лист существует; её отсутствие неотличимо от «забыл посмотреть».
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
`заменено на ADR-…`, а у новой в контексте строка «Заменяет ADR-…».
Не заводится для рутины и для того, что видно из кода и `git log`. Порядок работы: открой архивный `design.md` change, найди в `Decisions` то, что
проходит триггер, процитируй решение и его причину, сошлись на источник, добавь
Порядок: имя `ADR-ГГГГ-ММ-ДД-slug.md`, дата — когда решение **принято**, слаг строку в индекс `docs/adr/README.md` сверху.
английский; тело по `docs/adr/template.md`; строка в индексе `docs/adr/README.md`
сверху. Активная запись статуса не имеет.
## Чистка `architecture.md` ## Чистка `architecture.md`
Обзор не держит поведение — его нормативный дом `openspec/specs/`. Раздел, где Обзор не держит поведение — его нормативный дом `openspec/specs/`. **Форма
поведение осталось, помечается маркером долга: маркера долга и правило «гейт от них не краснеет» — в
[каноне](../canon/references/canon.md), раздел `architecture.md`.**
```
<!-- канон: поведение → openspec/specs/<capability> -->
```
`docs.py` считает маркеры и печатает числом; **гейт от них не краснеет** — это
долг, а не отказ, иначе постепенный переезд стал бы невозможен.
Разбирается порциями: раздел вычищается той задачей, которая его касается. Разбирается порциями: раздел вычищается той задачей, которая его касается.
Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change, Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change,
@@ -97,45 +90,36 @@ description: Вести содержимое документов канона
## Запись в `research/` ## Запись в `research/`
Наблюдение о внешнем мире: что реально шлёт источник, чем документация формата Наблюдение о внешнем мире: что реально шлёт источник, чем документация формата
расходится с практикой. **Число — с провенансом**: команда или условия, которыми расходится с практикой. **Требование провенанса и правило про расходящееся
получено, чтобы его можно было перепроверить. число — в [каноне](../canon/references/canon.md), раздел `research/`.**
Число без источника проход обязан читать как условие. Число, чей источник по Твоя часть — заметить, что по ходу задачи узналось новое о внешних данных, и не
ссылке не подтвердился, **не переписывается по догадке** — остаётся с пометкой дать этому остаться в контексте. Признак: ты правил разбор, опираясь на то, чего
«расходится с источником: там <что нашли>». Молча подставить «правильное» число нет ни в одном документе.
хуже всего: расхождение перестанет быть видно, а причина останется.
## Запись в `review.md` ## Запись в `review.md`
Два раздела с разными сроками жизни, и путать их нельзя. Файл держит два раздела с разными сроками жизни — журнал дефектов и настройку
конвейера. **Что в каком и в какой форме — в
[каноне](../canon/references/canon.md), раздел `review.md`**; подробности формы
записи и выбор адреса, куда она ведёт, — в конвейере ревью,
`references/review-journal.md`.
**Журнал дефектов.** Запись на каждый воспроизведённый дефект с пометкой Твоя часть на синке: **дефект пишется сразу**, а не «потом, когда починим».
**проскочил / пойман ревью**. Пишется сразу, а не ретроспективно: со временем Со временем теряется не факт, а причина непоймания — единственное, ради чего
теряется не факт, а причина непоймания — единственное, ради чего журнал есть. журнал есть. И решение о сужении проверок (перестали звать проход, понизили
Форма: где, симптом, чем воспроизведён, почему не поймали (для проскочивших), профиль) обязано попасть в раздел настройки, а не остаться в отчёте ревью.
что меняем. Вывод «ничего не меняем, цена поимки выше цены дефекта» — законный
исход.
**Настройка конвейера.** Типовые узлы; типовые ложноположительные; вопросы к
проходам поимённо с провенансом; недоступно проверке. Последний раздел делится
на «не проверит ни один проход» (принципиальная граница, по факту промаха не
пересматривается) и «перестали проверять сознательно» — этот **пересматривается
первым**, как только что-то проскочило.
## Промоут в конвенции ## Промоут в конвенции
Находка → конвенция → правило линтера → **удаление из прозы**. Процедура Находка → конвенция → правило линтера → **удаление из прозы**. Процедура целиком
принадлежит конвейеру ревью и живёт в его `references/promote.md`; здесь только принадлежит конвейеру ревью и живёт в его `references/promote.md`; роль каталога
то, что касается документа: конвенций — в [каноне](../canon/references/canon.md).
- формулировка — **проверяемое свойство**, а не совет; Твоя часть — **третий шаг, который пропускают чаще всего**: правило заработало,
- в прозе остаётся только то, что принципиально не выражается правилом; а формулировка осталась в прозе, и проход продолжает проверять уже проверенное.
- как только правило работает, формулировка из `conventions/<тема>.md` На синке это отдельная строка: «conventions/ — правило X механизировано,
**удаляется**, а правило попадает в перечень механизированного в формулировка удалена» либо «не требуется».
`conventions/README.md` со ссылкой на место механизации.
Непойманное место механизации означает, что проход по конвенциям будет
добросовестно проверять уже проверенное.
## Чего этот скилл не делает ## Чего этот скилл не делает
+7 -3
View File
@@ -9,7 +9,9 @@ description: Завести новый проект — сессия вопро
которого дальше работают все остальные скиллы. которого дальше работают все остальные скиллы.
**Определение канона — [канон](../canon/references/canon.md).** Читается до **Определение канона — [канон](../canon/references/canon.md).** Читается до
первого вопроса: интервью идёт по слотам канона, а не по вкусу. первого вопроса: интервью идёт по слотам канона, а не по вкусу. Что класть в
каждый файл — [скелеты](../canon/references/skeletons.md); своей формы заглушки
не выдумывай, `docs.py` узнаёт только плейсхолдер оттуда.
## Что `init` физически не может произвести ## Что `init` физически не может произвести
@@ -69,10 +71,12 @@ description: Завести новый проект — сессия вопро
4. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и 4. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и
отдельным файлом не остаётся: два дома для одного замысла разойдутся на отдельным файлом не остаётся: два дома для одного замысла разойдутся на
первом же уточнении. первом же уточнении.
5. Заведи скелет остальных — каждый с честной строкой. 5. Заведи скелет остальных по [скелетам](../canon/references/skeletons.md) —
каждый с честной строкой.
6. Каталог задач и первые цели — **вызови скилл `av-dev-pm:tasks`**: он владеет 6. Каталог задач и первые цели — **вызови скилл `av-dev-pm:tasks`**: он владеет
форматом целей и задач. форматом целей и задач.
7. `docs.py check` из скилла `canon` — до зелёного в механизируемой части. 7. `docs.py check` из скилла `canon` — до отсутствия дрейфа. Замечания о
незаполненных плейсхолдерах остаются: их закрывает не `init`, а работа.
8. Покажи человеку, что получилось, и **отдельным списком** — что выведено из 8. Покажи человеку, что получилось, и **отдельным списком** — что выведено из
брифа, что предположено, что осталось неизвестным. Правят по этим строкам. брифа, что предположено, что осталось неизвестным. Правят по этим строкам.
+33 -5
View File
@@ -83,7 +83,8 @@ import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
CONFIG_NAME = ".tasks.json" CONFIG_NAME = ".tasks.json" # прежний дом настроек, читается для совместимости
PM_CONFIG_REL = "../.pm.json" # текущий дом: docs/.pm.json, ключ "tasks"
EXIT_OK = 0 EXIT_OK = 0
EXIT_DRIFT = 1 EXIT_DRIFT = 1
@@ -278,15 +279,42 @@ class Layout:
def load_config(root: Path) -> dict: def load_config(root: Path) -> dict:
"""Настройки каталога задач.
Дом один — `docs/.pm.json`, ключ `tasks`: один конфиг на весь канон, а не по
одному на каталог. Прежний `<tasks>/.tasks.json` читается, пока живы проекты,
которые ещё не переехали; когда есть оба, побеждает `.pm.json`, и об этом
говорится вслух, потому что молча выбранный из двух конфиг — это дрейф,
который потом никто не объяснит.
"""
pm = (root / PM_CONFIG_REL).resolve()
if pm.is_file():
data = _read_json(pm)
section = data.get("tasks", {})
if not isinstance(section, dict):
raise Env(f"{pm}: ключ «tasks» — ожидался объект с настройками")
if (root / CONFIG_NAME).is_file():
print(f"ЗАМЕЧАНИЕ настройки взяты из {pm}; {root / CONFIG_NAME}"
f" остался от прежней раскладки и не читается — удали его",
file=sys.stderr)
return _validate_config(section, pm)
path = root / CONFIG_NAME path = root / CONFIG_NAME
if not path.is_file(): if not path.is_file():
return {} return {}
return _validate_config(_read_json(path), path)
def _read_json(path: Path) -> dict:
try: try:
data = json.loads(path.read_text(encoding="utf-8")) data = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise Env(f"{path}: не разбирается как JSON — {e}") raise Env(f"{path}: не разбирается как JSON — {e}")
if not isinstance(data, dict): if not isinstance(data, dict):
raise Env(f"{path}: ожидался объект с настройками") raise Env(f"{path}: ожидался объект с настройками")
return data
def _validate_config(data: dict, path: Path) -> dict:
unknown = set(data) - set(DEFAULTS) unknown = set(data) - set(DEFAULTS)
if unknown: if unknown:
raise Env(f"{path}: неизвестные ключи: {', '.join(sorted(unknown))}" raise Env(f"{path}: неизвестные ключи: {', '.join(sorted(unknown))}"
@@ -351,10 +379,10 @@ def resolve_layout(explicit: str | None) -> Layout:
return Layout(rel if str(rel) != "." else candidate, load_config(candidate)) return Layout(rel if str(rel) != "." else candidate, load_config(candidate))
if (base / ".git").exists(): if (base / ".git").exists():
break # выше корня репозитория не ищем break # выше корня репозитория не ищем
raise Env("каталог задач не найден: ни --dir, ни .tasks.json вверх от" raise Env("каталог задач не найден: ни --dir, ни docs/tasks вверх от"
f" {here}, ни умолчания (docs/tasks, tasks, doc/tasks)." f" {here}. По канону путь всегда docs/tasks; чужую раскладку"
" Путь каталога называет CLAUDE.md проекта; новый проект —" " переводит скилл av-dev-pm:canon, новый проект —"
" tasks.py init --dir <путь>") " tasks.py init --dir docs/tasks")
# --- Чтение индексов --- # --- Чтение индексов ---