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

- скилл project-brief: бриф собирается из CLAUDE.md, архитектуры, Taskfile
  и конвенций и показывается человеку. Раньше единственная инструкция по
  его созданию лежала внутри шаблона, поэтому деградированный режим был не
  аварийным, а единственным: critical по основанию «нарушен инвариант»
  недостижим ни на одной задаче
- rebase перенесён внутрь worktree задачи: прежняя форма падала на занятой
  ветке, и агент уводил весь батч в провалившиеся с ложной причиной
- контракт брифа дополнен восемью слотами; проверен заполнением на обоих
  проектах, незаполнимых нет. Прецедент healthlog вынут из общего charter'а
  в бриф — там он вмёрз вместе с числами
- шов: пайплайн задачу не закрывает и записи учёта не трогает, урожай
  отдаёт списком, правило остатка — ссылкой на av-dev-tasks
- деградированный абзац во всех девяти проходах, вопрос 9 в ops,
  пространство имён в вызовах, раздел предпосылок
This commit is contained in:
av
2026-08-03 11:45:40 +03:00
parent 20dca29add
commit 0eca206460
18 changed files with 1021 additions and 214 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "av-dev-pipeline", "name": "av-dev-pipeline",
"description": "Проведение задачи через полный цикл Spec Driven Development и конвейер ревью: детерминированный гейт, сверка со спеками, враждебные постановки, эксплуатационный постмортем, независимая реализация, архитектура и обязательный триаж. Плюс прогон нескольких задач разом. Проектная специфика — инварианты, команда гейта, доменные факты — приходит из файла-брифа проекта.", "description": "Проведение задачи через полный цикл Spec Driven Development и конвейер ревью: детерминированный гейт, сверка со спеками, враждебные постановки, эксплуатационный постмортем, независимая реализация, архитектура и обязательный триаж. Плюс прогон нескольких задач разом. Проектная специфика — инварианты, команда гейта, прецеденты, доменные факты — приходит из файла-брифа проекта, который заводится отдельным скиллом, а не пишется руками.",
"author": { "author": {
"name": "Anton Vakhrushev", "name": "Anton Vakhrushev",
"email": "anwinged@gmail.com" "email": "anwinged@gmail.com"
+18 -8
View File
@@ -18,9 +18,14 @@ color: red
## Модель угроз — из брифа, и не расширяй её самовольно ## Модель угроз — из брифа, и не расширяй её самовольно
Раздел **`## Модель угроз`** брифа отвечает на четыре вещи: что недоверенное и **Первая строка раздела `## Модель угроз` — периметр,** и она задаёт смысл всему
каким каналом приходит; что разграничивает доступ; что чувствительнее чего; **что остальному. «Открыт наружу, злоумышленник в локальной сети неинтересен» и «контур
вне модели**. доверенный, публичного интернета здесь нет» — противоположные постановки под
одним заголовком, а код в обоих случаях выглядит одинаково. Прочитай периметр
**до** всего прочего и держи его над каждой постановкой.
Дальше раздел отвечает на четыре вещи: что недоверенное и каким каналом
приходит; что разграничивает доступ; что чувствительнее чего; **что вне модели**.
Последнее так же обязательно, как первое. Угроза вне модели даёт уверенно Последнее так же обязательно, как первое. Угроза вне модели даёт уверенно
звучащую находку, которая никогда не будет исправлена, и обесценивает весь звучащую находку, которая никогда не будет исправлена, и обесценивает весь
@@ -28,12 +33,17 @@ color: red
поставщика, если бриф их исключил. поставщика, если бриф их исключил.
Ещё берёшь: **`## Инварианты`** (нарушение — основание для `critical`), Ещё берёшь: **`## Инварианты`** (нарушение — основание для `critical`),
**`## Прод и поток`** (что необратимо и какие объёмы реальны), **`## Карта`** **`## Прод и поток`** (что необратимо, какие объёмы реальны и — отдельно — чем
(где `testdata` и куда нельзя писать). физически лежит запись и какие настройки хранилища имеют числовое значение: из
этого строятся пути к отказу в обслуживании), **`## Прецеденты`** (что здесь уже
пробивалось и чем это было воспроизведено), **`## Карта`** (где `testdata` и куда
нельзя писать), **`## Вопросы к проходам`** (если там есть блок `adversary`
эти вопросы задаются дополнительно к четырём постановкам).
Брифа нет — работай по общей рамке ниже, `critical` по основанию «нарушен **Брифа нет** — работай по общей рамке ниже, `critical` по основанию «нарушен
инвариант» не присваивай и скажи в границах покрытия, что модель угроз ты инвариант проекта» не присваивай и дай в границы покрытия строку: «брифа проекта
предположила сама. нет: периметр и модель угроз предположены проходом; находки могут лежать вне
периметра и потому никогда не будут исправлены».
## Четыре постановки. Работай ими, а не списком ## Четыре постановки. Работай ими, а не списком
@@ -27,9 +27,21 @@ grep по именам концепций) и скажи об этом в гра
собранный на ходу, беднее подготовленного. собранный на ходу, беднее подготовленного.
Плюс: раздел **`## Проект`** брифа (граница домена), **`## Инварианты`**, Плюс: раздел **`## Проект`** брифа (граница домена), **`## Инварианты`**,
**`## Карта`** (единые точки, нарезка capability и что из неё уже переехало в
спеки), **`## Прецеденты`** (архитектурный промах, который здесь уже случался),
документация по архитектуре и дельта-спеки change. Дифф — **последним, не документация по архитектуре и дельта-спеки change. Дифф — **последним, не
первым**: он должен ложиться на карту, а не задавать её. первым**: он должен ложиться на карту, а не задавать её.
**Брифа нет — скажи это первой строкой вывода, а не пропусти.** Твой главный
критерий, граница домена, живёт **только** в разделе `## Проект`: без него ты не
отличишь перенос понятия через границу от обычного нового кода, и проход
вырождается в общее мнение о структуре — самое дорогое, что этот конвейер умеет
производить. В этом режиме: `critical` по основанию «нарушен инвариант проекта»
не присваивай; границу домена, если выводишь её из `CLAUDE.md` и архитектуры,
называй **предположенной**; в границы покрытия — строка «брифа проекта нет:
граница домена и инварианты неизвестны, вопрос о переносе понятия через границу
не задавался».
## Главный вопрос — концептуальная целостность ## Главный вопрос — концептуальная целостность
По порядку важности: По порядку важности:
+65 -12
View File
@@ -1,6 +1,6 @@
--- ---
name: review-code name: review-code
description: "Стадия 1 конвейера ревью (во всех профилях) — дешёвый applicative-проход по прозаическим конвенциям проекта, тем, которые НЕ выражаются правилом линтера: уровень лога по адресату, единственный логирующий чекпоинт на доменной границе, трансляция ошибки на внешней границе, что не попадает в логи, конфиг и его образцы, время и идентификаторы, тесты на реальных данных. Критерий берётся из файла конвенций проекта, а не из головы. Механизируемое проверяет гейт, архитектуру — review-architecture. Только чтение." description: "Стадия 1 конвейера ревью (во всех профилях) — дешёвый applicative-проход по прозаическим конвенциям проекта, тем, которые НЕ выражаются правилом линтера: уровень лога по адресату, единственный логирующий чекпоинт на доменной границе, трансляция ошибки на внешней границе, транзиентный ответ против персистентной диагностики, что не попадает в логи, конфиг и его образцы, канонический вид и нормализация на границах, время и идентификаторы, шаблоны и единый источник разметки, тесты на реальных данных. Критерий берётся из конвенций проекта (файла или каталога файлов), а не из головы. Механизируемое проверяет гейт, архитектуру — review-architecture. Только чтение."
tools: Read, Grep, Glob, Bash tools: Read, Grep, Glob, Bash
model: sonnet model: sonnet
color: blue color: blue
@@ -18,9 +18,11 @@ color: blue
## Откуда берётся критерий ## Откуда берётся критерий
**Из файла конвенций проекта** — путь и перечень уже механизированного дают **Из записанных конвенций проекта** — путь и перечень уже механизированного дают
разделы `## Карта` и `## Инварианты` брифа. Прочитай файл целиком **до** чтения разделы `## Карта` и `## Инварианты` брифа. Это может быть один файл, а может
диффа. быть **каталог из нескольких** (логирование, ошибки, конфиг, БД, UI — отдельными
файлами). Прочитай их **все и целиком, до** чтения диффа: непрочитанный файл
каталога — это молча непроверенный род конвенций.
Два правила, без которых проход вырождается: Два правила, без которых проход вырождается:
@@ -31,20 +33,39 @@ color: blue
ловит линтер. Дублировать его — значит удорожать триаж дублями и не дойти до ловит линтер. Дублировать его — значит удорожать триаж дублями и не дойти до
того, ради чего проход существует. того, ради чего проход существует.
Брифа или файла конвенций нет — проход **почти пуст**: скажи об этом прямо, не **Брифа или конвенций нет — проход почти пуст**, и это надо сказать прямо, а не
подменяй отсутствующий источник общими представлениями о хорошем коде и выведи подменять отсутствующий источник общими представлениями о хорошем коде. В этом
только то, что нарушает инварианты, если они даны. режиме: находок из головы не выводи вовсе, `critical` по основанию «нарушен
инвариант проекта» не присваивай и дай в границы покрытия строку «брифа проекта
нет: записанные конвенции и инварианты неизвестны, проход выполнен вхолостую».
Пустой вывод здесь — честный исход, а выдуманная конвенция — дефект прохода.
## Типовые роды прозаических конвенций ## Типовые роды прозаических конвенций
Ниже — не чек-лист требований, а **навигация**: на что смотреть в диффе, если у Ниже — не чек-лист требований, а **навигация**: на что смотреть в диффе, если у
проекта есть конвенция такого рода. Рода, которого у проекта нет, не существует проекта есть конвенция такого рода. Список работает в **обе стороны**, и вторая
и для тебя. важнее первой:
- **рода, которого у проекта нет, не существует и для тебя** — вычёркивай;
- **рода, который у проекта есть, а в списке нет, — работай по нему всё равно.**
Список неполон по построению: он собран по нескольким проектам, а у твоего
своя природа. Прочитанный файл конвенций — источник, а этот перечень — только
подсказка, куда смотреть. Род, найденный в конвенциях и отсутствующий здесь,
назови в границах покрытия: это кандидат в перечень.
Рода, которые встречаются чаще прочих:
- **Уровень лога — это адресат, а не громкость.** Отладочное — разработчику, - **Уровень лога — это адресат, а не громкость.** Отладочное — разработчику,
событийное — владельцу для аудита постфактум, «может стать проблемой» — событийное — владельцу для аудита постфактум, «может стать проблемой» —
предупреждением, «в разбор владельцу» — ошибкой. Невалидный ввод от отправителя предупреждением, «в разбор владельцу» — ошибкой. Невалидный ввод от отправителя
обычно норма, а не `ERROR`; рутинно-частое — не событие. обычно норма, а не `ERROR`; рутинно-частое — не событие. Отдельный вопрос того
же рода: **есть ли у этого места штатный повтор.** Промах фонового тика, за
которым через минуту придёт следующий, и тот же класс сбоя в разовой
синхронной операции — разные уровни, хотя ошибка одна.
- **Корреляция через `context`, а не через параметры.** Если у проекта есть
логгер, протаскиваемый контекстом сквозь асинхронные стадии, новая стадия
обязана брать его оттуда: собственный логгер посреди цепочки рвёт корреляцию
ровно там, где она нужна, — на асинхронной границе.
- **Логируем один раз, на доменной границе.** Промежуточные слои оборачивают и - **Логируем один раз, на доменной границе.** Промежуточные слои оборачивают и
возвращают; транспорт переводит ошибку в ответ и не логирует, иначе один сбой возвращают; транспорт переводит ошибку в ответ и не логирует, иначе один сбой
даёт три записи. Проверь, что новая ветвь отказа проходит через существующий даёт три записи. Проверь, что новая ветвь отказа проходит через существующий
@@ -62,6 +83,14 @@ color: blue
- **Код ответа отражает то, что проект считает событием**, а не удобство - **Код ответа отражает то, что проект считает событием**, а не удобство
реализации. Если инвариант говорит «сохранили — значит приняли», новая ветвь, реализации. Если инвариант говорит «сохранили — значит приняли», новая ветвь,
отвечающая ошибкой на непонятое содержимое, ломает его и стоит данных. отвечающая ошибкой на непонятое содержимое, ломает его и стоит данных.
- **Текст ошибки и «заикание» слоёв.** Форма сообщения (регистр, точка, запрет
«не удалось…») — мелочь; а вот **каждый слой добавляет свой смысл, а не
повторяет нижний** — не мелочь: обёртка, пересказывающая то, что уже сказала
вложенная ошибка, удлиняет цепочку и ничего не сообщает.
- **Граница паники.** Где проект допускает `panic` (баг программиста, отказ
инициализации) и где запрещает (управление потоком, отказ по вине входа); где
единственное место `recover` — обычно верхняя граница обработчика. Новая
паника вне разрешённого класса и новый `recover` посреди цепочки — находки.
- **Sentinel против типизированной ошибки.** Тип заводим, когда вызывающему нужны - **Sentinel против типизированной ошибки.** Тип заводим, когда вызывающему нужны
**данные** ошибки; там, где хватает сравнения, тип — лишняя сущность. **данные** ошибки; там, где хватает сравнения, тип — лишняя сущность.
Независимые ошибки собираются вместе. Глушение ошибки без лога — только с Независимые ошибки собираются вместе. Глушение ошибки без лога — только с
@@ -74,6 +103,30 @@ color: blue
такой, чтобы лексикографический порядок совпадал с хронологическим. такой, чтобы лексикографический порядок совпадал с хронологическим.
- **Схема и миграции.** Изменение структуры сопровождается обновлением её - **Схема и миграции.** Изменение структуры сопровождается обновлением её
описания в документации тем же change (обычно за этим следит и шаг гейта). описания в документации тем же change (обычно за этим следит и шаг гейта).
- **Транзиентный ответ против персистентной диагностики.** Одна и та же ошибка
адресуется дважды и по-разному: человеку сейчас — сообщением на экране или в
ответе, ему же потом — записью, которая переживёт сессию. Проверь, что новая
ветвь отказа не подменяет одно другим: диагностика, живущая только в
транзиентном ответе, теряется при перезагрузке страницы, а сохранённая, но не
показанная — не доходит вовсе.
- **Канонический вид значения и нормализация на границах.** Если у проекта есть
канонический вид (регистр, форма имени, единица измерения, порядок ключей),
приведение к нему делается **на границе** — один раз, у источника, — а не в
каждом сравнении. Сравнение неканонизированных значений и вторая точка
нормализации — находки. Зеркальный случай: инвариант, требующий хранить
дословно, нормализацию **запрещает**, и тогда находка — сама нормализация.
- **Естественные и составные ключи.** Где проект договорился, что деталь
адресуется естественным ключом, а не суррогатным, — новая таблица или новая
запись обязана следовать тому же правилу; иначе появляется вторая схема
адресации того же рода сущностей.
- **Вызовы внешних сервисов логируются все.** Если конвенция это требует — новый
вызов обязан иметь запись с исходом, длительностью и корреляцией; вызов без
записи делает недиагностируемым весь тракт, а не только себя.
- **Шаблоны и разметка: единый источник.** Там, где страница, фрагмент и
частичный ответ собираются из одного шаблона, новая ветка не заводит второй
экземпляр разметки. Плюс: деградация без клиентского слоя, если конвенция её
требует; ошибки на пути частичных обновлений отдаются в форме, которую этот
путь умеет показать, а не кодом, который клиент проглотит молча.
- **Тесты разбора — на реальных данных**, а не на придуманных, и с проверкой - **Тесты разбора — на реальных данных**, а не на придуманных, и с проверкой
идемпотентности повторного разбора. идемпотентности повторного разбора.
@@ -102,8 +155,8 @@ color: blue
## Формат вывода ## Формат вывода
Находки по контракту. Если конвенции нарушены не были — так и напиши, перечислив Находки по контракту. Если конвенции нарушены не были — так и напиши, перечислив
**проверенные разделы файла конвенций** (без этого «замечаний нет» ничего не **прочитанные файлы конвенций и проверенные разделы каждого** (без этого
значит). В конце — обязательный блок: «замечаний нет» ничего не значит). В конце — обязательный блок:
``` ```
## Coverage of this pass ## Coverage of this pass
+6 -4
View File
@@ -22,10 +22,12 @@ color: red
шагов, что означает каждый исход, **какие шаги красят безусловно и почему**, и шагов, что означает каждый исход, **какие шаги красят безусловно и почему**, и
чего в гейте намеренно нет. Раздел **`## Команды`** — что запускать запрещено. чего в гейте намеренно нет. Раздел **`## Команды`** — что запускать запрещено.
Брифа нет — найди команду гейта сама (`Taskfile.yml`, `Makefile`, `justfile`, **Брифа нет** — найди команду гейта сама (`Taskfile.yml`, `Makefile`, `justfile`,
`scripts/`), выполни её и **скажи в границах покрытия, что состав шагов и их `scripts/`) и выполни её, но: `critical` по основанию «нарушен инвариант проекта»
цену ты вывела из конфига, а не из брифа**: шаг, красящий безусловно, ты в этом не присваивай — severity безусловного шага назначает бриф, а в этом режиме ты не
режиме от обычного не отличишь. отличишь такой шаг от обычного. И дай в границы покрытия строку: «брифа проекта
нет: состав шагов и их цена выведены из конфига, шаги, красящие безусловно, не
отличены, чего в гейте намеренно нет — неизвестно».
## Что делаешь ## Что делаешь
+26 -7
View File
@@ -1,6 +1,6 @@
--- ---
name: review-ops name: review-ops
description: "Эксплуатационный проход ревью — пишет постмортем «это упало через неделю на проде» от симптома у владельца сервиса к строке кода. Обязательные вопросы: рост объёма, деградация окружения и внешних зависимостей, повторная и одновременная операция, частичный откат при двух версиях, миграция под живым потоком, отмена контекста на середине, наблюдаемость и тишина, поведение библиотеки и драйвера в вырожденном случае. Формулирует условиями, а не утверждениями — реального профиля нагрузки не знает. Только чтение." description: "Эксплуатационный проход ревью — пишет постмортем «это упало через неделю на проде» от симптома у владельца сервиса к строке кода. Обязательные вопросы: рост объёма, деградация окружения и внешних зависимостей, повторная и одновременная операция, частичный откат при двух версиях, миграция под живым потоком, отмена контекста на середине, наблюдаемость и тишина, поведение библиотеки и драйвера в вырожденном случае, чтение узлом состояния, которое он сам же меняет. Формулирует условиями, а не утверждениями — реального профиля нагрузки не знает. Только чтение."
tools: Read, Grep, Glob, Bash tools: Read, Grep, Glob, Bash
model: sonnet model: sonnet
color: yellow color: yellow
@@ -30,8 +30,16 @@ color: yellow
нет. Тогда постмортем про «недосчитались данных» весит больше, чем про «сервис нет. Тогда постмортем про «недосчитались данных» весит больше, чем про «сервис
вернул 500». вернул 500».
Брифа нет — задавай те же вопросы, но **все** ответы формулируй условиями и Ещё берёшь: **`## Прецеденты`** — что в этом проекте уже ломалось и чем это было
скажи в границах покрытия, что профиль эксплуатации неизвестен. воспроизведено (готовый оракул и готовая проба для вопроса 8);
**`## Вопросы к проходам`** — если там есть блок `ops`, эти вопросы задаются
дополнительно к обязательным и ответы на них выводятся явно.
**Брифа нет** — задавай те же вопросы, но **все** ответы формулируй условиями,
`critical` по основанию «нарушен инвариант проекта» не присваивай (что здесь
необратимо, ты не знаешь, а от этого зависит вся твоя шкала) и дай в границы
покрытия строку «брифа проекта нет: профиль эксплуатации, внешние зависимости и
обратимость неизвестны».
## Метод: постмортем от симптома ## Метод: постмортем от симптома
@@ -81,11 +89,22 @@ color: yellow
8. **Поведение библиотеки, драйвера и настроек — измеряется, а не вычитывается 8. **Поведение библиотеки, драйвера и настроек — измеряется, а не вычитывается
из документации.** Спрашивай: что возвращается в **вырожденном** случае — при из документации.** Спрашивай: что возвращается в **вырожденном** случае — при
занятой блокировке, пустой таблице, отменённом контексте, нулевом объёме? занятой блокировке, пустой таблице, отменённом контексте, нулевом объёме?
Отличим ли этот ответ от штатного? Прецедент, ради которого пункт существует: Отличим ли этот ответ от штатного? Класс, ради которого пункт существует:
контрольная точка журнала под занятой блокировкой возвращала `-1` вместо пары библиотека возвращает в вырожденном случае значение, которое код сравнивает
чисел, и сравнение `-1 >= -1` читалось как «журнал разобран целиком» — 1492 тем же оператором, что и штатное, — и отказ читается как успех. Такое из
тика из 5502, найдено экспериментом на стенде, из документации не следовало. документации не следует **никогда**: оно достаётся экспериментом на стенде.
Проверяй на копии или во временном каталоге, рабочие данные не трогай. Проверяй на копии или во временном каталоге, рабочие данные не трогай.
Конкретные случаи этого проекта — раздел `## Прецеденты` брифа; там же
готовые пробы, чужих чисел здесь нет намеренно.
9. **Читает ли узел состояние, которое сам же меняет.** Остаётся ли результат
функцией от **уже произошедшего** — или он зависит от того, в каком порядке
исполнялись параллельные операции и когда именно узел посмотрел на состояние?
Ищи: решение принимается по прочитанному значению, которое к моменту записи
уже другое; счётчик или курсор, который узел одновременно читает и двигает;
ветка, выбираемая по «сколько сейчас лежит в таблице»; повторный прогон,
дающий другой результат на тех же входных событиях. Это тот же вопрос, что
рубрика задаёт дизайну до кода, — но задать его **на коде** больше некому:
рубрика на код не смотрит.
## Правило формулировки ## Правило формулировки
+17
View File
@@ -15,6 +15,23 @@ color: purple
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md` `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
(точный путь конвейер передаёт в задании). (точный путь конвейер передаёт в задании).
## Что берёшь из брифа
**`## Проект`** — граница домена: твоя версия должна лежать по ту же сторону, что
и существующая, иначе весь дифф по решениям окажется спором о scope.
**`## Инварианты`** — то, что твоя реализация обязана соблюсти (дословность
хранения, «сохранили — значит приняли» и подобное). **`## Прод и поток`** — объёмы
и представление данных: решение, разумное на сотне записей, неразумно на
миллионе. **`## Карта`** — где конвенции и где файл наблюдений на живых данных.
**Брифа нет** — пиши свою версию по спеке и конвенциям, но: `critical` по
основанию «нарушен инвариант проекта» не присваивай (инвариантов ты не знаешь, а
именно они чаще всего объясняют чужое решение), объёмы не предполагай и в границы
покрытия дай строку «брифа проекта нет: инварианты и профиль нагрузки прогону
неизвестны, расхождения по этим основаниям не оценивались». Без брифа риск
конкретно этого прохода максимален: твоя версия проще, потому что не знает, чего
проект боится.
**Тебя запускают по триггеру, а не всегда.** Триггер: изменение вводит **новое **Тебя запускают по триггеру, а не всегда.** Триггер: изменение вводит **новое
правило идентичности, слияния или разбора** (проектная формулировка — в разделе правило идентичности, слияния или разбора** (проектная формулировка — в разделе
`## Триггеры` брифа). Вне его твой счёт — самый большой в конвейере (он `## Триггеры` брифа). Вне его твой счёт — самый большой в конвейере (он
+14 -6
View File
@@ -21,9 +21,14 @@ color: purple
Это материал для требования «минимум три пункта специфичны для типа узла». Это материал для требования «минимум три пункта специфичны для типа узла».
- **`## Инварианты`** и **`## Проект`** — чтобы рубрика не противоречила тому, что - **`## Инварианты`** и **`## Проект`** — чтобы рубрика не противоречила тому, что
проект защищает и чем он себя ограничил. проект защищает и чем он себя ограничил.
- **`## Прецеденты`** — классы дефектов, уже случавшихся здесь: свойство,
сформулированное по прецеденту, сильнее любого общего.
Разделов нет — порождай рубрику по общей практике и скажи в границах покрытия, **Брифа или этих разделов нет** — порождай рубрику по общей практике, но
что специфика узла в проекте не описана: часть пунктов неизбежно окажется общими. `critical` по основанию «нарушен инвариант проекта» (в фазе 2) не присваивай и
дай в границы покрытия строку: «брифа проекта нет: рода узлов, инварианты и
прецеденты неизвестны; требование «минимум три пункта специфичны для типа узла»
выполнено по общей практике, а не по этому проекту».
## Порядок фаз обязателен ## Порядок фаз обязателен
@@ -68,10 +73,13 @@ color: purple
если вся рубрика — пересказ инвариантов из брифа, проход выродился в если вся рубрика — пересказ инвариантов из брифа, проход выродился в
applicative; applicative;
- **отдельным пунктом — узел, читающий состояние, которое сам же меняет.** - **отдельным пунктом — узел, читающий состояние, которое сам же меняет.**
Спроси, остаётся ли результат функцией от того, что уже произошло, а не от Спроси, остаётся ли результат функцией от того, что **уже произошло**, а не от
того, что произойдёт: правило родилось из дефекта, где запрос брал последнее того, в каком порядке исполнялись параллельные операции и когда именно узел
выведенное значение **вообще**, а не последнее предшествующее, и пересборка посмотрел на состояние. Класс: запрос берёт «последнее выведенное значение»
переставала воспроизводить состояние. вообще вместо последнего предшествующего — и пересборка перестаёт
воспроизводить состояние. Случаи этого проекта — в разделе `## Прецеденты`
брифа. Тот же вопрос на **готовом коде** задаёт эксплуатационный проход
(вопрос 9); здесь он задаётся дизайну.
Выведи рубрику **до** любых находок. Она — часть результата, даже если код Выведи рубрику **до** любых находок. Она — часть результата, даже если код
окажется идеальным. окажется идеальным.
+9 -4
View File
@@ -19,13 +19,18 @@ Development на OpenSpec). Оптика — требования, а не ст
- **`## Инварианты`** — по ним проверяется, отражены ли в спеке задетые свойства, - **`## Инварианты`** — по ним проверяется, отражены ли в спеке задетые свойства,
и по ним же присваивается severity. Цитируй пункт дословно, когда ссылаешься. и по ним же присваивается severity. Цитируй пункт дословно, когда ссылаешься.
- **`## Карта`** — где актуальные спеки, где дельты, где архитектура и где лежат - **`## Карта`** — где актуальные спеки, где дельты, где архитектура и **где файл
наблюдения о реальном поведении внешних систем. наблюдений на живых данных**. Там же — **нарезка capability и миграционное
состояние спек**: по какому признаку проект режет capability и какие темы ещё
не переехали из документации в спеки. Без этого пункта непереехавшая тема
читается как пробел в спеке, и находка уходит в пустоту.
- **`## Проект`** — граница домена: требование, переносящее понятие через неё, — - **`## Проект`** — граница домена: требование, переносящее понятие через неё, —
находка в спеку, а не в код. находка в спеку, а не в код.
Брифа нет — сверяй только спеку с кодом, `critical` по основанию «нарушен **Брифа нет** — сверяй только спеку с кодом, `critical` по основанию «нарушен
инвариант» не присваивай и скажи об этом в границах покрытия. инвариант проекта» не присваивай и дай в границы покрытия строку: «брифа проекта
нет: инварианты, граница домена и состояние переноса capability в спеки
неизвестны; отражение инвариантов в спеке не проверялось».
## Источник требований ## Источник требований
+34 -13
View File
@@ -27,8 +27,19 @@ color: green
Из брифа тебе нужны: **`## Инварианты`** (что делает находку `critical` и что Из брифа тебе нужны: **`## Инварианты`** (что делает находку `critical` и что
делает её развилкой), **`## Прод и поток`** (что необратимо — от этого зависит делает её развилкой), **`## Прод и поток`** (что необратимо — от этого зависит
ранжирование), **`## Недоступно проверке`** (эта секция целиком уезжает в границы ранжирование), **`## Прецеденты`** (готовые оракулы: находка того же класса, что
покрытия), **`## Команды`** (что запускать запрещено). уже воспроизводился здесь, подтверждается ссылкой на прецедент),
**`## Типовые ложноположительные`** (единственный проектный вход в шаг 4),
**`## Недоступно проверке`** — оба подраздела, они целиком уезжают в границы
покрытия и **не сливаются в один список**, — **`## Команды`** (что запускать
запрещено).
**Брифа нет** — работай по общим правилам, но: ни одну находку не поднимай до
`critical` по основанию «нарушен инвариант проекта» (сослаться не на что),
ранжируй по обратимости, выведенной из кода, и назови это предположением. Первой
строкой сводки — «прогон шёл без брифа проекта (<причина>)», и это же идёт в
границы покрытия. Одинаковая строка «брифа нет» без причины перестаёт читаться
на третьей задаче — причину сохраняй.
## Порядок. Не меняй его ## Порядок. Не меняй его
@@ -79,9 +90,16 @@ severity:
Типовая вкусовщина в выводах generative-проходов: переименования без коллизии, Типовая вкусовщина в выводах generative-проходов: переименования без коллизии,
перестановка функций, «лучше вынести в отдельный файл», предложения обобщить перестановка функций, «лучше вынести в отдельный файл», предложения обобщить
работающий частный случай. Отдельный класс — предложение «нормализовать» то, что работающий частный случай.
инвариант проекта велит хранить дословно: это не просто вкусовщина, а нарушение
инварианта, и выбрасывать его надо с пометкой почему. **Проектный вход сюда один — раздел `## Типовые ложноположительные` брифа.**
Там перечислены находки, которые в этом проекте выглядят убедительно и всегда
неверны: они выбрасываются со ссылкой на пункт и с пометкой почему, а не
«смягчаются». Классический обитатель раздела — предложение «нормализовать» то,
что инвариант велит хранить дословно: это не просто вкусовщина, а находка,
предлагающая нарушить инвариант. Раздела нет или он пуст — скажи об этом строкой
в границах покрытия: отсев шёл по общим критериям, проектных ложноположительных
ты не знал.
### 5. Ранжирование по ущербу × вероятности ### 5. Ранжирование по ущербу × вероятности
@@ -121,8 +139,7 @@ severity:
Сводка отчёта называет **каждый проход профиля** и его исход: отработал (сколько Сводка отчёта называет **каждый проход профиля** и его исход: отработал (сколько
находок) / не запускался (почему). Сверь список запущенного с составом профиля находок) / не запускался (почему). Сверь список запущенного с составом профиля
сам, а не доверяй тому, что тебе подали: пропуск прохода **не отличим от прохода сам, а не доверяй тому, что тебе подали: пропуск прохода **не отличим от прохода
без находок**, и однажды это стоило семи находок и отдельной задачи на их без находок**, и назвать его больше некому.
дозакрытие.
Расхождение состава с профилем — это находка о прогоне, и она идёт в сводку Расхождение состава с профилем — это находка о прогоне, и она идёт в сводку
первой строкой, а не растворяется в границах покрытия. первой строкой, а не растворяется в границах покрытия.
@@ -135,12 +152,16 @@ severity:
- какие **не** запускались и почему (профиль, бюджет, недоступный инструмент, - какие **не** запускались и почему (профиль, бюджет, недоступный инструмент,
остановленный прогон); остановленный прогон);
- что каждый запущенный проход **не мог проверить в принципе** — из его charter'а; - что каждый запущенный проход **не мог проверить в принципе** — из его charter'а;
- **что осталось целиком на человеке** — раздел `## Недоступно проверке` брифа - **что осталось целиком на человеке** — раздел `## Недоступно проверке` брифа,
целиком, плюс: история инцидентов, поведение под реальным потоком, поведение **двумя отдельными списками**: «не проверит ни один проход» и «перестали
внешних систем в их версиях, завязка потребителей на текущее поведение и вопрос проверять сознательно». Слитый список бесполезен: при следующем промахе первый
«а нужна ли эта функциональность вообще»; вопрос — «не тот ли это класс, который мы перестали проверять», и ответить на
- если брифа не было — строку об этом: инварианты, модель угроз и профиль него можно только если второй список виден отдельно. Плюс общее: история
нагрузки прогону были неизвестны. инцидентов, поведение под реальным потоком, поведение внешних систем в их
версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта
функциональность вообще»;
- если брифа не было — строку об этом **с причиной**: инварианты, модель угроз и
профиль нагрузки прогону были неизвестны, потому что <причина>.
Формулировка «критичных проблем не обнаружено» **запрещена** без этой секции: она Формулировка «критичных проблем не обнаружено» **запрещена** без этой секции: она
потребляет ощущение проверенности, ничего не гарантируя, и это хуже, чем потребляет ощущение проверенности, ничего не гарантируя, и это хуже, чем
@@ -0,0 +1,127 @@
---
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` по основанию «нарушен инвариант проекта».
Во всех остальных случаях бриф заводится. «Задача маленькая, брифа не надо» —
не основание: бриф заводится один раз на проект, а деградированный режим платит
на каждой задаче.
+65 -27
View File
@@ -29,28 +29,61 @@ description: Конвейер ревью изменения — детермин
обнаружено» потребляет ощущение проверенности, ничего не гарантируя. Секция обнаружено» потребляет ощущение проверенности, ничего не гарантируя. Секция
границ покрытия обязательна и не сокращается — в том числе в докладе человеку. границ покрытия обязательна и не сокращается — в том числе в докладе человеку.
## Предпосылки
Конвейер опирается на внешнюю обвязку и без неё работает не целиком. Проверь это
один раз, при установке плагина в проект:
- **OpenSpec и скиллы `opsx:*`.** Профиль `design`, проход `review-specs` и
вызывающий пайплайн задачи завязаны на дельта-спеки
(`openspec/changes/<id>/specs/*/spec.md`), на актуальные спеки
(`openspec/specs/`) и на `openspec validate --strict`. В проекте без OpenSpec
шаги, зовущие `opsx:explore` / `opsx:propose` / `opsx:apply` / `opsx:archive`,
упадут на «нет такого скилла», а `review-specs` останется без источника
требований. Такой проект либо подключает OpenSpec, либо сознательно не зовёт
`review-specs` и профиль `design` — и тогда это идёт строкой «не запускался» в
границы покрытия, как любой другой пропуск.
- **Бриф проекта** — см. следующий раздел. Заводится скиллом, а не руками.
- **Проектные копии этих скиллов и агентов удаляются при установке.** Если в
проекте уже лежат свои `.claude/skills/review-pipeline`,
`.claude/skills/task-pipeline`, `.claude/skills/task-batch` или
`.claude/agents/<проект>-review-*.md` — снеси их. Иначе короткое имя разрешится
в устаревшую проектную копию, молча и без признаков подмены. По той же причине
**скиллы этого плагина зовутся с пространством имён**:
`av-dev-pipeline:review-pipeline`, `av-dev-pipeline:task-pipeline`,
`av-dev-pipeline:task-batch`, `av-dev-pipeline:project-brief`.
## Что конвейер защищает — приходит из брифа ## Что конвейер защищает — приходит из брифа
Проходы общие, а нарушать нельзя проектное. Список инвариантов, команду гейта, Проходы общие, а нарушать нельзя проектное. Список инвариантов, команду гейта,
объёмы и модель угроз конвейер **не знает** — он читает их в брифе проекта: объёмы, прецеденты и модель угроз конвейер **не знает** — он читает их в брифе
[references/project-brief.md](references/project-brief.md) описывает контракт, проекта: [references/project-brief.md](references/project-brief.md) описывает
[references/brief-template.md](references/brief-template.md) — образец контракт, [references/brief-template.md](references/brief-template.md) — образец
заполнения. заполнения.
Разреши путь к брифу один раз, в начале прогона: путь из задания → Разреши путь к брифу один раз, в начале прогона: путь из задания →
`docs/review-brief.md``.claude/review-brief.md`. Дальше передавай готовым. `docs/review-brief.md``.claude/review-brief.md`. Дальше передавай готовым.
**Брифа нет — прогон идёт в деградированном режиме**: `critical` по основанию **Брифа нет по всем трём путям — заведи его, а не понижай прогон.** Вызови Skill
«нарушен инвариант проекта» никем не присваивается, числа объёма не **`av-dev-pipeline:project-brief`**: он соберёт бриф из `CLAUDE.md`, архитектуры,
используются, и в границы покрытия уезжает строка «брифа проекта нет». Это дыра файла задач и конвенций, покажет человеку и вернёт путь. Это механика, а не
покрытия, а не нейтральное умолчание. развилка: спрашивать разрешения не нужно, и остановка прогона тут не
предусмотрена. Заведение стоит одного шага один раз на проект — деградированный
режим платит на каждой задаче.
**Деградированный режим — исход, а не умолчание.** Он включается ровно тогда,
когда бриф завести не удалось (репозиторий на чтение, человек прямо запретил,
инварианты вывести неоткуда): `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/`);
@@ -170,14 +203,14 @@ description: Конвейер ревью изменения — детермин
Почему умолчание именно такое: Почему умолчание именно такое:
- **Замеры.** Проходы `adversary` и `ops` доказывают находки числами: время - **Замеры.** Проходы `adversary` и `ops` доказывают находки числами: время
удержания блокировки, пик кучи, рост файлов журнала, длительность транзакции. удержания блокировки против её таймаута, пик кучи против размера тела, темп
Два меряющих прохода на одной машине соревнуются за диск, CPU и за саму СУБД и роста файлов журнала, длительность транзакции. Два меряющих прохода на одной
выдают числа, которые не воспроизведутся. Это не гипотеза: находки, ради машине соревнуются за диск, CPU и за саму СУБД и выдают числа, которые не
которых правило записано, опираются ровно на такие замеры (5.019 с удержания воспроизведутся. Это не гипотеза: правило выведено из находок, целиком
блокировки при таймауте 5000 мс, пик 768 МиБ на теле 40 МиБ, 7 МБ/с роста державшихся на таких замерах, — у каждого проекта они свои и лежат в разделе
журнала, 1492 тика из 5502). Число, снятое под конкурентную нагрузку от `## Прецеденты` его брифа. Число, снятое под конкурентную нагрузку от соседнего
соседнего прохода, — это находка с испорченным оракулом, а её опровержение прохода, — это находка с испорченным оракулом, а её опровержение стоит дороже
стоит дороже всего выигрыша от параллельности. всего выигрыша от параллельности.
- **Машина одна.** Рядом идёт задача, поднят сервис, гоняется гейт или дорогая - **Машина одна.** Рядом идёт задача, поднят сервис, гоняется гейт или дорогая
проверка проекта. проверка проекта.
- **Ранний выход** возможен только при последовательном прогоне (см. ниже). - **Ранний выход** возможен только при последовательном прогоне (см. ниже).
@@ -205,9 +238,9 @@ description: Конвейер ревью изменения — детермин
нулевой стадии** — а не «доезжает» остатком по старому коду; нулевой стадии** — а не «доезжает» остатком по старому коду;
- незапущенные проходы идут в границы покрытия строкой «не запускался: прогон - незапущенные проходы идут в границы покрытия строкой «не запускался: прогон
остановлен на <проход> из-за <находка>», поимённо; остановлен на <проход> из-за <находка>», поимённо;
- триаж запускается только на полном прогоне. Отчёт триажа по половине проходов - триаж запускается только на полном прогоне. Отчёт триажа по половине проходов
ровно тот случай, который уже стоил семи находок: он выглядит полным, потому выглядит полным, потому что агрегирует всё, что ему подали, — это тот же
что агрегирует всё, что ему подали. молчащий пропуск, что и в разделе «Профили».
Ранний выход по находке, которая чинится в пределах существующей формы Ранний выход по находке, которая чинится в пределах существующей формы
(`Действие: инлайн`), **не делается**: дешевле дособрать все находки и починить (`Действие: инлайн`), **не делается**: дешевле дособрать все находки и починить
@@ -246,8 +279,9 @@ description: Конвейер ревью изменения — детермин
- `review-specs` — критерий взят из **дельта-спек предлагаемого изменения**, а не - `review-specs` — критерий взят из **дельта-спек предлагаемого изменения**, а не
из proposal, сообщения коммита или описания задачи. Сверка двунаправленная; из proposal, сообщения коммита или описания задачи. Сверка двунаправленная;
направление `code → spec` важнее. направление `code → spec` важнее.
- `review-code` — критерий взят из файла конвенций проекта (раздел `## Карта` - `review-code` — критерий взят из конвенций проекта: файла или каталога файлов,
брифа), и только та его часть, которая **не выражается правилом**: путь — раздел `## Карта` брифа. Берётся только та их часть, которая **не
выражается правилом**:
механизируемое уже проверила стадия 0. Что именно механизировано, тот же раздел механизируемое уже проверила стадия 0. Что именно механизировано, тот же раздел
брифа перечисляет — повторять это проходом вредно. брифа перечисляет — повторять это проходом вредно.
@@ -322,7 +356,8 @@ Recall обоих равен длине их источника — это и е
## Профиль `design` — до кода ## Профиль `design` — до кода
Запускается на шаге ревью спек (шаг 4 скилла `task-pipeline`), когда change уже Запускается на шаге ревью спек (шаг 4 скилла `av-dev-pipeline:task-pipeline`),
когда change уже
имеет `proposal.md` и дельта-спеки, но кода ещё нет. Состав: имеет `proposal.md` и дельта-спеки, но кода ещё нет. Состав:
1. `review-specs` в режиме «дизайн ДО кода»; 1. `review-specs` в режиме «дизайн ДО кода»;
@@ -359,9 +394,11 @@ Recall обоих равен длине их источника — это и е
держит вопросы (это знает вызвавший пайплайн, а не конвейер). Оркестратор не держит вопросы (это знает вызвавший пайплайн, а не конвейер). Оркестратор не
останавливается: он урезает изменение до остатка и доводит его. останавливается: он урезает изменение до остатка и доводит его.
- Находка не для этого мерджа, но реальная (отложенный `major`, развилка, - Находка не для этого мерджа, но реальная (отложенный `major`, развилка,
решённая «потом») — не теряется: заводится задачей средствами проекта, с решённая «потом»), — не теряется, но **и не заводится здесь**. Конвейер отдаёт
оракулом и провенансом в теле. Мелочь класса `nit` — пачкой, а не записью на её **списком урожая** в отчёте: формулировка, оракул, провенанс (какой проход,
находку. какой change). Заведение задач принадлежит тому, кто ведёт задачи проекта, —
у него свой формат, своя нарезка и свои правила дублей. Мелочь класса `nit`
идёт в урожай одной пачкой, а не записью на находку.
- `Promote candidates` — по процедуре [references/promote.md](references/promote.md): - `Promote candidates` — по процедуре [references/promote.md](references/promote.md):
находка → конвенция → правило линтера → **удаление из конвенций и из брифа**. находка → конвенция → правило линтера → **удаление из конвенций и из брифа**.
Третий шаг обязателен. Третий шаг обязателен.
@@ -405,6 +442,7 @@ Recall обоих равен длине их источника — это и е
## Ссылки ## Ссылки
- Skill `av-dev-pipeline:project-brief` — заведение и обновление брифа.
- [references/project-brief.md](references/project-brief.md) — контракт брифа проекта. - [references/project-brief.md](references/project-brief.md) — контракт брифа проекта.
- [references/brief-template.md](references/brief-template.md) — шаблон брифа. - [references/brief-template.md](references/brief-template.md) — шаблон брифа.
- [references/finding-contract.md](references/finding-contract.md) — контракт находок. - [references/finding-contract.md](references/finding-contract.md) — контракт находок.
@@ -1,7 +1,8 @@
# Шаблон брифа проекта # Шаблон брифа проекта
Скопируй в `docs/review-brief.md` и заполни. Контракт разделов — в Образец заполнения. Контракт разделов — в
[project-brief.md](project-brief.md); здесь только образец заполнения. [project-brief.md](project-brief.md); заводит бриф по этому образцу скилл
`av-dev-pipeline:project-brief` — руками копировать не надо, но читать полезно.
Курсивом даны пояснения — их из готового брифа убирают. Примеры взяты из двух Курсивом даны пояснения — их из готового брифа убирают. Примеры взяты из двух
разных проектов (коллектор данных с непрерывным потоком и связующий сервис вокруг разных проектов (коллектор данных с непрерывным потоком и связующий сервис вокруг
@@ -26,7 +27,8 @@
## Инварианты ## Инварианты
*Проверяемое свойство + последствие + severity по умолчанию. Цитируются *Проверяемое свойство + последствие + severity по умолчанию. Цитируются
формулировкой.* формулировкой. Severity проект обычно не пишет — тогда она выводится по
обратимости и помечается: «по умолчанию `critical` (выведена по обратимости)».*
- **Точка сохраняется дословно.** Незнакомое поле не отбрасывается, число не - **Точка сохраняется дословно.** Незнакомое поле не отбрасывается, число не
округляется при записи. Нарушение — необратимая потеря: сырой архив живёт округляется при записи. Нарушение — необратимая потеря: сырой архив живёт
@@ -70,12 +72,26 @@
- Карта проекта для архитектурного прохода: `task review:context > tmp/review-context.md` - Карта проекта для архитектурного прохода: `task review:context > tmp/review-context.md`
- Поднять изменение вживую: `task restart`, логи — `task logs` - Поднять изменение вживую: `task restart`, логи — `task logs`
- Тесты и линт: `task test`, `task lint` - Тесты и линт: `task test`, `task lint`
- Дорогое, вручную: `task verify:archive` (минута, живые данные) - **Дорогое вне гейта, с адресатом:** `task verify:archive` (минута, живые
данные). **Кто и когда обязан:** пайплайн задачи — после любого изменения
разбора входного формата или правила слияния, до архивации change; вручную —
человек перед выкладкой. Не прогонялась — строка в границы покрытия, а не
молчание.
- **Запускать запрещено:** ничего, что пишет в `./data`, в рабочую БД и в боевой - **Запускать запрещено:** ничего, что пишет в `./data`, в рабочую БД и в боевой
каталог архива. Замеры — только на копиях в `./tmp`. каталог архива. Замеры — только на копиях в `./tmp`.
## Прод и поток ## Прод и поток
*Первая строка — главный вопрос эксплуатации этого проекта.*
> **Главный вопрос:** поток идёт непрерывно и молча, отправитель об отказе не
> узнает и не повторит — значит, дороже всего тихо потерянная доставка, а не
> упавший сервис.
> *(В сервисе, который сам опрашивает чужих демонов, первая строка была бы
> противоположной: «главный вопрос — что происходит, когда внешний сервис
> отвечает медленно, а не когда он упал».)*
- **Где:** один статический бинарь в контейнере на домашнем сервере, перед ним - **Где:** один статический бинарь в контейнере на домашнем сервере, перед ним
обратный прокси с TLS, SQLite на диске. Ни оркестратора, ни реплик, ни дежурной обратный прокси с TLS, SQLite на диске. Ни оркестратора, ни реплик, ни дежурной
смены. смены.
@@ -84,11 +100,17 @@
параллельной записью; приложение-источник на телефоне — молча перестаёт слать. параллельной записью; приложение-источник на телефоне — молча перестаёт слать.
*(В другом проекте здесь были бы качалка, медиасервер, LLM и база метаданных, и *(В другом проекте здесь были бы качалка, медиасервер, LLM и база метаданных, и
каждая — со своим «отвечает медленно», а не только «упала».)* каждая — со своим «отвечает медленно», а не только «упала».)*
*(Если зависимостей нет — так и пишут: «внешних зависимостей нет, смотри на
диск и на СУБД». Пустой пункт называется пустым.)*
- **Кто заметит отказ:** один пользователь-владелец, в лучшем случае вечером, а - **Кто заметит отказ:** один пользователь-владелец, в лучшем случае вечером, а
скорее не заметит вовсе. скорее не заметит вовсе.
- **Характер потока:** телефон шлёт непрерывно и молча; обратной связи у - **Характер потока:** телефон шлёт непрерывно и молча; обратной связи у
отправителя нет, об отказах он не сообщает, расписание плавает. Тихо отправителя нет, об отказах он не сообщает, расписание плавает. Тихо
сломавшаяся доставка — главный эксплуатационный риск. сломавшаяся доставка — главный эксплуатационный риск.
- **Представление данных и настройки хранилища:** запись — сжатый BLOB, читается
и пересобирается целиком на каждой операции (`internal/store`); журнал СУБД —
WAL; таймаут занятости — 5000 мс (`config.example.toml`); лимит тела приёма —
64 МБ; ретеншен сырого архива — 14 дней.
- **Числа (с провенансом):** нижний слой — порядка 135 тыс. точек в сутки - **Числа (с провенансом):** нижний слой — порядка 135 тыс. точек в сутки
(замер, `docs/local-research.md`); тела доходили до 42 МБ (там же); запись — (замер, `docs/local-research.md`); тела доходили до 42 МБ (там же); запись —
read-modify-write под конкурентными доставками (`docs/architecture.md`). read-modify-write под конкурентными доставками (`docs/architecture.md`).
@@ -98,6 +120,19 @@
## Модель угроз ## Модель угроз
*Первая строка — периметр.*
> **Периметр:** сервис открыт наружу через обратный прокси, недоверенным считается
> всё, что приходит по HTTP. Злоумышленник в локальной сети — вне периметра.
> *(У сервиса в доверенном контуре первая строка противоположна: «контур
> доверенный, публичного интернета здесь нет — не выдумывай его; недоверенное
> здесь — то, что отдают внешние демоны и трекеры».)*
> *(Контур ещё не развёрнут — тогда периметров два: «целевой — за прокси с TLS;
> сегодняшний — только локальная машина, токены пусты осознанно. **Находки
> строятся против целевого**, отсутствие TLS сегодня находкой не является».)*
- **Недоверенное:** тело доставки целиком (имена метрик, единицы, формы точек, - **Недоверенное:** тело доставки целиком (имена метрик, единицы, формы точек,
метки времени, глубина вложенности, размер); заголовки доставки, часть которых метки времени, глубина вложенности, размер); заголовки доставки, часть которых
участвует в решениях; содержимое архива внешнего экспорта (имена файлов внутри участвует в решениях; содержимое архива внешнего экспорта (имена файлов внутри
@@ -117,13 +152,25 @@
## Карта ## Карта
- **Основная ветка:** `master`. От неё берутся ветки задач, в неё вливается батч,
база диффа по умолчанию — `git merge-base HEAD master` (на самой ветке `HEAD~1`).
- Актуальные спеки: `openspec/specs/<capability>/spec.md` - Актуальные спеки: `openspec/specs/<capability>/spec.md`
- Дельта-спеки изменения: `openspec/changes/<id>/specs/*/spec.md` - Дельта-спеки изменения: `openspec/changes/<id>/specs/*/spec.md`
- Конвенции прозой: `docs/conventions.md`. Механизировано и потому **не - **Нарезка capability и что из неё переехало в спеки:** режем по домену
проверяется проходом по конвенциям**: форма логов, `fmt.Print*`/`os.Getenv`/ (`ingest`, `storage`, `read-api`, `mcp`), а не по транспорту. В актуальные
`time.Now` мимо единых точек, сравнение ошибок, сторонние пакеты ошибок — спеки перенесены `ingest` и `storage`; `read-api` описан только в
всё это правила в `.golangci.yml`. `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/architecture.md`
- **Наблюдения на живых данных:** `docs/local-research.md` — что реально шлёт
источник и чем это расходится с его документацией. *(Не ведём — так и пишут:
«наблюдений на живых данных не ведём».)*
- Журнал проскочивших дефектов: `docs/review-journal.md` - Журнал проскочивших дефектов: `docs/review-journal.md`
- **Единые точки:** идентификаторы — `internal/ident`; время — `store.Now()`; - **Единые точки:** идентификаторы — `internal/ident`; время — `store.Now()`;
разбор дат входного формата — один парсер в `internal/parse`; маппинг доменной разбор дат входного формата — один парсер в `internal/parse`; маппинг доменной
@@ -159,6 +206,53 @@
- **Клиент внешнего сервиса** — таймаут, протяжка `context`, поведение при - **Клиент внешнего сервиса** — таймаут, протяжка `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/*`, - `deep`: миграция в `internal/store/migrations/`, новый пакет `internal/*`,
@@ -170,6 +264,10 @@
## Недоступно проверке ## Недоступно проверке
### Не проверит ни один проход
*Принципиальные границы. По факту промаха не пересматриваются.*
- Поведение внешнего приложения-источника на следующем его обновлении. - Поведение внешнего приложения-источника на следующем его обновлении.
- Что реально лежит в системе-источнике: сверить можно только ручным экспортом, - Что реально лежит в системе-источнике: сверить можно только ручным экспортом,
а он делается раз в 2–3 месяца. а он делается раз в 2–3 месяца.
@@ -177,3 +275,15 @@
нагрузки. нагрузки.
- Завязка внешних потребителей на текущую форму ответа. - Завязка внешних потребителей на текущую форму ответа.
- Суждение «этой функциональности не должно существовать». - Суждение «этой функциональности не должно существовать».
### Перестали проверять сознательно
*Что, когда, почему и где записано. Пересматривается первым, как только что-то
проскочило. Пусто — так и пишут: «сознательно ничего не отключали».*
- **Поимённая сверка со стайлгайдами языка** — с 2026-05, вместе с упразднением
прохода про идиоматичность (журнал ревью, запись 2026-05-04). Класс обратимый:
портит форму кода, не данные.
- **Правило линтера про длину функции** — снято 2026-06-18: ложных срабатываний
больше трети (журнал, там же). Вернуть, если проскочит дефект «функция делает
три вещи».
@@ -18,21 +18,28 @@
передавать каждому проходу; передавать каждому проходу;
2. `docs/review-brief.md`; 2. `docs/review-brief.md`;
3. `.claude/review-brief.md`; 3. `.claude/review-brief.md`;
4. брифа нет **деградированный режим** (см. ниже). 4. брифа нет ни по одному пути — **он заводится**, скиллом
`av-dev-pipeline:project-brief`, и прогон продолжается по заведённому.
Разрешает путь конвейер, один раз, и дальше передаёт готовым. Агент, получивший Разрешает путь конвейер, один раз, и дальше передаёт готовым. Агент, получивший
путь в задании, сам ничего не ищет. путь в задании, сам ничего не ищет.
## Деградированный режим ## Деградированный режим — исход, а не умолчание
Брифа нет — проходы работают, но их recall падает предсказуемым образом, и это Он включается ровно тогда, когда бриф **завести не удалось**: репозиторий
**обязано быть названо**, а не сглажено. Каждый проход без брифа: доступен только на чтение, человек прямо запретил, инварианты вывести неоткуда.
Во всех остальных случаях брифа быть обязано.
Каждый проход в этом режиме:
- не присваивает `critical` по основанию «нарушен инвариант проекта» — инвариантов - не присваивает `critical` по основанию «нарушен инвариант проекта» — инвариантов
он не знает; он не знает;
- не оперирует числами объёма и потока — формулирует условиями; - не оперирует числами объёма и потока — формулирует условиями;
- пишет в границы покрытия строку: «брифа проекта нет: инварианты, модель угроз и - пишет в границы покрытия строку: «брифа проекта нет (<причина>): инварианты,
профиль нагрузки неизвестны; находки этих классов не искались». модель угроз и профиль нагрузки неизвестны; находки этих классов не искались».
Причина обязательна: без неё строка неотличима от «мы просто не стали», и
одинаковая строка в каждом отчёте перестаёт читаться на третьей задаче.
Триаж сводит эти строки в одну и выносит в финальный отчёт. Отсутствие брифа — Триаж сводит эти строки в одну и выносит в финальный отчёт. Отсутствие брифа —
дыра покрытия, а не нейтральное умолчание. дыра покрытия, а не нейтральное умолчание.
@@ -67,6 +74,14 @@ Markdown. Разделы — заголовки второго уровня с *
Это единственный раздел, который **цитируется формулировкой**, а не пересказывается Это единственный раздел, который **цитируется формулировкой**, а не пересказывается
ссылкой: по нему присваивается severity, и пересказ здесь стоит неверной оценки. ссылкой: по нему присваивается severity, и пересказ здесь стоит неверной оценки.
**Оговорка про severity, потому что она единственная не цитируется.** Проекты
почти никогда не пишут severity рядом с инвариантом — её приходится выводить, и
правило вывода одно: **по обратимости последствия**. Необратимо и молча —
`critical`; лечится повтором, видно сразу — ниже. Выведенная severity помечается
словом «выведена по обратимости», а не выдаётся за решение проекта: проход ставит
по ней `critical`, и он вправе знать, чьё это суждение. Лучший исход — дописать
severity туда, откуда цитируется формулировка, и тогда пометка снимается.
Читают: `specs` (режим 1 — отражены ли задетые инварианты в спеке), `code`, Читают: `specs` (режим 1 — отражены ли задетые инварианты в спеке), `code`,
`adversary`, `architecture`, `triage` (ранжирование и разметка «развилка»). `adversary`, `architecture`, `triage` (ранжирование и разметка «развилка»).
@@ -96,6 +111,14 @@ Markdown. Разделы — заголовки второго уровня с *
- **запуск изменения вживую** — чем поднять и как проверить поведение (нужно - **запуск изменения вживую** — чем поднять и как проверить поведение (нужно
пайплайну задачи на шаге поведенческой верификации); пайплайну задачи на шаге поведенческой верификации);
- **тесты, линт, дополнительные проверки** — и какие из них дорогие; - **тесты, линт, дополнительные проверки** — и какие из них дорогие;
- **дорогие проверки вне гейта — с адресатом.** Мало сказать «`verify:archive`
идёт минуту»: назови, **кто и когда обязан** её гонять — какой класс изменения
её требует, кто её запускает (проход, пайплайн, человек) и что делать, если она
не прогонялась. Без адресата дорогая проверка не гоняется никогда, а её
краснота не видна никому. **Адресат в проекте не записан нигде — тогда бриф его
назначает**, и назначение помечается: «адресат назначен брифом, владельцем не
подтверждён». Это тот же класс, что выведенная severity у инварианта: слот
честнее заполнить назначением с пометкой, чем оставить пустым;
- **что запускать запрещено**: рабочая БД, боевой каталог данных, внешние - **что запускать запрещено**: рабочая БД, боевой каталог данных, внешние
сервисы. Формулируй запретом с путями, а не «будь осторожен». сервисы. Формулируй запретом с путями, а не «будь осторожен».
@@ -103,18 +126,48 @@ Markdown. Разделы — заголовки второго уровня с *
### `## Прод и поток` — обязателен ### `## Прод и поток` — обязателен
Материал для эксплуатационного прохода, и он же — половина ранжирования триажа: Материал для эксплуатационного прохода, и он же — половина ранжирования триажа.
**Первой строкой — главный вопрос эксплуатации этого проекта.** Один заголовок
покрывает противоположные постановки: «поток идёт непрерывно и молча, отправитель
об отказе не узнает» и «мы опрашиваем чужие сервисы, и главный вопрос — что
делать, когда сосед отвечает медленно». От того, какая из них здесь главная,
зависит порядок находок в отчёте, а вывести её проход не может — он видит
одинаковый код.
Дальше:
- где это работает: машина, окружение, что рядом, кто перезапускает; - где это работает: машина, окружение, что рядом, кто перезапускает;
- **внешние зависимости поимённо** и чем каждая отказывает: не только «падает», но - **внешние зависимости поимённо** и чем каждая отказывает: не только «падает», но
и «отвечает медленно», «молчит», «отдаёт мусор». Эксплуатационный проход и «отвечает медленно», «молчит», «отдаёт мусор». Эксплуатационный проход
спрашивает про каждую отдельно, и список зависимостей он взять больше неоткуда; спрашивает про каждую отдельно, и список зависимостей он взять больше неоткуда.
**Зависимостей почти нет — так и напиши**: «внешних зависимостей нет, смотри на
диск и на СУБД». Пустой пункт, не названный пустым, проход тратит впустую или
заполняет выдумкой;
- **кто заметит отказ и когда** — есть ли вообще наблюдатель; - **кто заметит отказ и когда** — есть ли вообще наблюдатель;
- **характер потока**: непрерывный и молчаливый, по запросу, по расписанию; есть - **характер потока**: непрерывный и молчаливый, по запросу, по расписанию; есть
ли обратная связь у отправителя; ли обратная связь у отправителя;
- **представление данных и настройки хранилища.** Чем физически лежит запись
(сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи
(распаковка целиком, read-modify-write), и **настройки, у которых есть
числовое значение**: таймаут занятости СУБД, режим журналирования, лимит тела,
размер пула, ретеншен. Это не украшение раздела: ровно эти два факта
превращают **замер** в находку. Замеренный пик памяти — аномалия только если
известно, что запись лежит сжатой и распаковывается целиком; замеренная
длительность удержания блокировки — гарантированный отказ соседа только если
известно, чему равен таймаут занятости. Без этих фактов проход снимет верное
число и честно понизит находку до гипотезы, потому что сравнить его будет не с
чем. Цена пропущенного пункта здесь не «не найдём», а **«найдём и не
починим»**. Числа с провенансом — в следующем пункте, воспроизведённые случаи —
в разделе `## Прецеденты`;
- **измеренные числа с провенансом**: объёмы, размеры тел, темп, размеры таблиц. - **измеренные числа с провенансом**: объёмы, размеры тел, темп, размеры таблиц.
Число без источника проход обязан превратить в условие — так и напиши, откуда Число без источника проход обязан превратить в условие — так и напиши, откуда
оно; оно. **Замер и настройка — разные пункты, и путать их нельзя:** настройка
(`busy_timeout`, лимит тела, размер пула) живёт пунктом выше и говорит, чему
равен порог; замер говорит, что происходит на самом деле. Проекту без
наблюдаемой нагрузки нечего писать во втором пункте — **так и напиши**:
«измеренных чисел нагрузки нет, всё, что ниже, — настройки». Тогда проход
формулирует условиями осознанно, а не потому, что не нашёл;
- **что обратимо, а что нет.** Падение, которое лечится повтором, и тихая потеря, - **что обратимо, а что нет.** Падение, которое лечится повтором, и тихая потеря,
которую нечем восстановить, — разные классы, и порядок находок в отчёте зависит которую нечем восстановить, — разные классы, и порядок находок в отчёте зависит
от того, какой из них здесь главный. от того, какой из них здесь главный.
@@ -123,6 +176,21 @@ Markdown. Разделы — заголовки второго уровня с *
### `## Модель угроз` — обязателен ### `## Модель угроз` — обязателен
**Первой строкой — периметр.** «Сервис открыт наружу; злоумышленник в локальной
сети неинтересен» и «контур доверенный, публичного интернета здесь нет, не
выдумывай его» — это один и тот же заголовок при противоположной постановке, и
враждебный проход не может выбрать между ними сам. Периметр, объявленный первой
строкой, задаёт смысл всему остальному разделу.
**Периметров может быть два — целевой и сегодняшний**, если контур ещё не
развёрнут: «целевой — открыт наружу за прокси с TLS; сегодняшний — только
локальная машина, токены пусты осознанно». Тогда назови оба и скажи прямо,
**против какого строятся находки**. Иначе враждебный проход либо завалит отчёт
находками «нет TLS» по сегодняшнему состоянию, либо не станет искать дефекты,
спящие до выкладки, — оба исхода стоят прохода целиком.
Дальше:
- **что недоверенное** и каким каналом приходит: тело запроса, файл, аргумент - **что недоверенное** и каким каналом приходит: тело запроса, файл, аргумент
команды, ответ внешней системы, содержимое архива; команды, ответ внешней системы, содержимое архива;
- **из чего строятся пути и ключи** — раскладка файлов на диске, состав - **из чего строятся пути и ключи** — раскладка файлов на диске, состав
@@ -139,9 +207,32 @@ Markdown. Разделы — заголовки второго уровня с *
Где что лежит, путями: Где что лежит, путями:
- **основная ветка** — её имя. Отсюда берутся ветки задач, в неё вливается батч,
от неё считается база диффа по умолчанию (`git merge-base HEAD <основная>`).
Батч подставляет это имя в каждую команду git; взять его больше неоткуда, а
угадывание между `master` и `main` ломает интеграцию целиком;
- актуальные спеки и дельта-спеки предлагаемого изменения; - актуальные спеки и дельта-спеки предлагаемого изменения;
- конвенции прозой — и **какая их часть уже механизирована** правилом (её проход - **нарезка capability и миграционное состояние спек** — по какому признаку
по конвенциям не проверяет); проект режет capability (по домену, по транспорту, по подсистеме), какие из них
уже перенесены в актуальные спеки, а какие ещё живут только в документации или
в коде. Проход по спекам иначе примет непереехавшую тему за пробел в спеке, а
архитектурный — за отсутствие понятия;
- конвенции прозой — **файл или каталог файлов**, путями; и **какая их часть уже
механизирована** правилом. Механизация бывает **в нескольких местах сразу**:
конфиг линтера, собственный анализатор и — чаще всего незамеченное —
**тест-сканер исходников** (правило про направление зависимостей, форму
миграций, логику в транспорте), который внешне неотличим от обычного теста.
Перечисли все места: непойманное место механизации означает, что проход по
конвенциям будет добросовестно проверять уже проверенное;
- **наблюдения на живых данных** — где записано, как внешний мир ведёт себя на
самом деле (что реально шлёт источник, чем документация формата расходится с
практикой, какие числа сняты с живого потока). Их спрашивают `specs`, `reimpl`
и `ops`, и все трое — «из раздела `## Карта`». Отдельного файла нет — **так и
напиши**, и перечисли суррогаты: спеки, где наблюдения рассыпаны, комментарии в
адаптерах, `testdata`. Отдельный файл — лучшая форма, потому что при нескольких
внешних источниках наблюдения иначе не сойдутся в одном месте; но честный
перечень суррогатов лучше молчания, от которого три прохода ищут
несуществующий путь;
- архитектура и решения; журнал проскочивших дефектов; - архитектура и решения; журнал проскочивших дефектов;
- **единые точки проекта** — где генерируются идентификаторы и время, где - **единые точки проекта** — где генерируются идентификаторы и время, где
единственный парсер входного формата, где маппинг доменной ошибки в код ответа, единственный парсер входного формата, где маппинг доменной ошибки в код ответа,
@@ -161,10 +252,83 @@ Markdown. Разделы — заголовки второго уровня с *
репозиторий, воркер, клиент внешнего API, CLI-команда, файловое хранилище), и по репозиторий, воркер, клиент внешнего API, CLI-команда, файловое хранилище), и по
35 **специфичных для рода** проверяемых свойств к каждому. 35 **специфичных для рода** проверяемых свойств к каждому.
**Рода, а не инвентарь того, что сейчас лежит в пакетах.** Список пишется по
природе проекта: род, который проект уже задумал, но ещё не написал, включать
полезно (рубрика на него понадобится ровно на той задаче, где его заводят); а
род, случайно оказавшийся в коде в одном экземпляре, — нет. Иначе раздел
протухает на каждой задаче и требует пересмотра, которого никто не делает.
Читает: `rubric`. Без раздела рубрика выродится в общие слова и повторит Читает: `rubric`. Без раздела рубрика выродится в общие слова и повторит
конвенции — то есть станет applicative-проходом, ради отсутствия которого она и конвенции — то есть станет applicative-проходом, ради отсутствия которого она и
существует. существует.
### `## Прецеденты` — обязателен, хотя бы строкой «пусто»
**Воспроизведённые случаи этого проекта, с оракулом.** Не «здесь бывают гонки», а
«такой дефект здесь уже был, вот чем он воспроизведён»: что оказалось не так,
каким экспериментом или тестом это показано, какими числами, где это записано.
Каждый пункт — четыре вещи:
- **класс дефекта** — так, чтобы проход узнал его в другом месте;
- **как проявился** — симптом, который увидел человек;
- **чем воспроизведён** — команда, тест, стенд, замер. Без этого пункт
превращается в байку. **Регрессионный тест, написанный вместе с починкой,
годится** наравне с независимым экспериментом: он исполняемый и падает на
старом коде, а это всё, что требуется от оракула. Слабее он ровно в одном —
сформулирован уже зная ответ; это отмечается словом, а не служит поводом
выбросить пункт;
- **чем закончилось** — починка, правило линтера, пункт брифа, «ничего».
Зачем раздел существует. Прецедент — самая сильная опора, какая у прохода вообще
бывает: он проектный, воспроизводимый и уже однажды оказался правдой. Пока слота
не было, прецеденты вмерзали в charter'ы проходов — то есть каждый проект читал
про чужую контрольную точку в чужой СУБД и искал её у себя. Charter описывает
**форму класса**, бриф — **случай**.
Источники: журнал проскочивших дефектов, архивные отчёты триажа, `git log` по
починкам. Прецедентов нет — так и напиши: «прецедентов не накоплено», и это
честнее пустого раздела.
Читают: все проходы — свой класс; `triage` — как готовый оракул.
### `## Типовые ложноположительные` — необязателен, но без него отсев слепой
Находки, которые в **этом** проекте выглядят убедительно и всегда неверны. Это
единственный проектный вход в шаг триажа «отсев вкусовщины»: общие критерии
(«не меняет поведения, не влияет на стоимость следующего изменения, не нарушает
записанного») ловят вкусовщину, но не ловят находку, которая нарушает общее
правило **осознанно**.
Каждый пункт — формулировка находки, какой её выдаёт проход, плюс одна строка
«почему здесь это не дефект». Типичные обитатели: «дословное хранение надо
нормализовать» там, где дословность — инвариант; «повтор надо сделать
идемпотентным» там, где повтор невозможен по построению; «это надо вынести в
конфиг» там, где значение задано внешним протоколом.
Читает: `triage`.
### `## Вопросы к проходам` — необязателен
Проектные вопросы, адресованные **поимённо** конкретному проходу. Главный их
источник — журнал проскочивших дефектов: запись «проход не мог этого знать» чаще
всего лечится фактом в другом разделе, но иногда лечится не фактом, а
**вопросом**: «`ops`, спроси про поведение при откате бинаря поверх новой схемы»,
«`adversary`, проверь имена внутри архива». Такие вопросы живут здесь, а не в
charter'е: charter общий для всех проектов, а вопрос выведен из промаха в этом.
**Журнал — не единственный источник, а лучший.** У молодого проекта журнал пуст,
и слот тогда заполняется из того, что есть: незакрытые находки аудита, известное
расхождение кода с документацией, место, где решение принято «пока так». Правило
одно и не смягчается — **у каждого вопроса указан провенанс**, и по нему видно,
насколько он выстрадан: «журнал, запись такая-то» весит больше, чем «открытая
находка аудита».
Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Проход, увидев свой блок, задаёт
эти вопросы **дополнительно** к обязательным — и отвечает на них в выводе явно.
Читают: проходы, названные поимённо.
### `## Триггеры` — необязателен ### `## Триггеры` — необязателен
Проектная конкретизация правила выбора профиля: какие пути и контракты означают Проектная конкретизация правила выбора профиля: какие пути и контракты означают
@@ -175,16 +339,35 @@ Markdown. Разделы — заголовки второго уровня с *
Читают: скилл конвейера, пайплайн задачи. Читают: скилл конвейера, пайплайн задачи.
### `## Недоступно проверке` — обязателен ### `## Недоступно проверке` — обязателен, и делится на два подраздела
Что не проверит ни один проход и почему: поведение внешних систем и их будущих Раздел целиком уезжает в границы покрытия финального отчёта — он существует ровно
версий, реальный профиль нагрузки, соответствие сохранённого действительности, затем, чтобы «критичных проблем не обнаружено» никогда не читалось как «проверено
завязка внешних потребителей на текущую форму, суждение «а нужна ли эта всё». Но внутри лежат **два разных класса**, и смешивать их нельзя: при следующем
функциональность». промахе один пересматривается, другой нет.
Этот раздел целиком уезжает в границы покрытия финального отчёта. Он существует #### `### Не проверит ни один проход`
ровно затем, чтобы «критичных проблем не обнаружено» никогда не читалось как
«проверено всё». Принципиально недоступное: поведение внешних систем и их будущих версий, реальный
профиль нагрузки, соответствие сохранённого действительности, завязка внешних
потребителей на текущую форму, суждение «а нужна ли эта функциональность».
Этот список не пересматривается по факту промаха: дефект отсюда — не ошибка
конвейера, а его честная граница. Он меняется только когда меняется сам проект
(появился стенд, появился второй потребитель, появилась телеметрия).
#### `### Перестали проверять сознательно`
Решения о сужении: перестали звать проход, понизили профиль правилом, сузили
класс проверяемого, сняли правило линтера как шумное. Каждый пункт — **что
перестали, когда и почему**, со ссылкой на запись журнала ревью.
Этот список **пересматривается первым**, как только что-то проскочило: первый
вопрос по любому пропущенному дефекту — «не тот ли это класс, который мы перестали
проверять». Пункт, из-за которого дефект проскочил, либо возвращается, либо
получает строку «оставляем, цена поимки выше цены дефекта» с датой.
Оба подраздела обязательны; пустой называется пустым.
Читает: `triage`; каждый проход — свою часть. Читает: `triage`; каждый проход — свою часть.
@@ -194,15 +377,37 @@ Markdown. Разделы — заголовки второго уровня с *
или в архитектуре, попадает сюда ссылкой и одной строкой сути. Два дома для или в архитектуре, попадает сюда ссылкой и одной строкой сути. Два дома для
одного факта разъезжаются, и разошедшийся бриф хуже отсутствующего: он выглядит одного факта разъезжаются, и разошедшийся бриф хуже отсутствующего: он выглядит
актуальным. Исключение одно — раздел инвариантов, он цитируется. актуальным. Исключение одно — раздел инвариантов, он цитируется.
- **Числа — с провенансом.** «Тела доходили до 42 МБ (замер, ссылка)». Число без - **Числа — с провенансом, и провенанс проверяется переходом по ссылке.** «Тела
источника проход не имеет права использовать как утверждение. доходили до 42 МБ (замер, ссылка)». Число без источника проход не имеет права
использовать как утверждение. Отдельный и более коварный случай — **число, чей
источник по ссылке не подтверждается**: в документе по ссылке другое число, или
его там нет вовсе. Такое число не выбрасывается и не переписывается по догадке:
оно остаётся с пометкой «расходится с источником: там <что нашли>», а проход
обязан читать его как условие, а не как замер. Молча подставить «правильное»
число хуже всего — расхождение перестанет быть видно, а причина его останется.
- **Пустой пункт называется пустым.** «Внешних зависимостей нет — смотри на диск
и на СУБД», «прецедентов не накоплено», «наблюдений на живых данных не ведём»,
«измеренных чисел нагрузки нет», «сознательно ничего не отключали». Отсутствие
строки читается проходом как «здесь не написали», и он тратит обязательный
вопрос впустую либо заполняет пробел выдумкой. Прямое «пусто» стоит одной
строки и экономит проход целиком.
- **Назначенное помечается назначенным.** Бриф отражает решения проекта, но
местами оказывается **первым** местом, где решение вообще записано: severity у
инварианта, адресат дорогой проверки, периметр, восстановленный из конфига.
Так можно — молчать хуже, — но пометка обязательна («выведена по обратимости»,
«назначен брифом, владельцем не подтверждён»). Проход имеет право знать, чьё
это суждение, а владелец — увидеть, что за него что-то решили.
- **Что вне модели — называется явно.** Это относится и к угрозам, и к нагрузке, - **Что вне модели — называется явно.** Это относится и к угрозам, и к нагрузке,
и к классам находок, которые проект сознательно перестал проверять. и к классам находок, которые проект сознательно перестал проверять (последние —
в свой подраздел `## Недоступно проверке`, а не вперемешку с принципиальным).
- **Бриф подчиняется промоуту.** Свойство, ставшее правилом линтера, из брифа - **Бриф подчиняется промоуту.** Свойство, ставшее правилом линтера, из брифа
вычёркивается — как и из конвенций, и из charter'ов (см. вычёркивается — как и из конвенций, и из charter'ов (см.
[promote.md](promote.md), шаг 3). [promote.md](promote.md), шаг 3).
- **Когда обновлять:** сменился гейт; появился новый контур, зависимость или - **Когда обновлять:** сменился гейт; появился новый контур, зависимость или
источник входа; журнал ревью получил запись вида «проход не мог этого знать». источник входа; журнал ревью получил запись вида «проход не мог этого знать»;
Планового пересмотра нет. воспроизвели дефект — он идёт в `## Прецеденты`. Планового пересмотра нет.
- **Бриф ведёт проект**, а не плагин. Плагин его только читает и никогда не - **Заводится и обновляется шагом, а не руками** — скиллом
правит. `av-dev-pipeline:project-brief`. Он же вызывается автоматически, когда конвейер
или пайплайн задачи не нашли брифа ни по одному пути.
- **Бриф ведёт проект**, а не плагин. Файл живёт в репозитории проекта; плагин
его читает и заводит по шаблону, но не хранит у себя и не подменяет.
@@ -23,7 +23,8 @@
калибровки: проход, чьи находки регулярно доезжают до конвенции, оправдан; калибровки: проход, чьи находки регулярно доезжают до конвенции, оправдан;
проход, чьи находки не доезжают никогда, — кандидат на `drop` (см. проход, чьи находки не доезжают никогда, — кандидат на `drop` (см.
[calibration.md](calibration.md)). [calibration.md](calibration.md)).
- Место записи — файл конвенций проекта (путь — в разделе `## Карта` брифа). Если - Место записи — конвенции проекта, файл или нужный файл каталога (путь — в
разделе `## Карта` брифа). Если
тема относится к поведению системы, а не к тому, как мы пишем код, — это не тема относится к поведению системы, а не к тому, как мы пишем код, — это не
конвенция, а требование: заводится дельта-спека обычным путём. конвенция, а требование: заводится дельта-спека обычным путём.
@@ -19,6 +19,11 @@
а потому, что здесь лежит цена: если что-то теперь проскочит, первый вопрос — а потому, что здесь лежит цена: если что-то теперь проскочит, первый вопрос —
«не тот ли это класс, который мы перестали проверять». «не тот ли это класс, который мы перестали проверять».
Каждое такое решение обязано получить **строку в брифе** — в подразделе
`### Перестали проверять сознательно` раздела `## Недоступно проверке`. Журнал
хранит «почему тогда так решили», бриф — то, во что смотрит каждый прогон.
Решение, оставшееся только в журнале, в границы покрытия не доедет.
## Форма записи ## Форма записи
``` ```
@@ -41,7 +46,11 @@
- **в бриф проекта** — если проход не мог знать факта: объём, характер потока, - **в бриф проекта** — если проход не мог знать факта: объём, характер потока,
что здесь необратимо, какой шаг гейта красит безусловно. Самый частый адрес и что здесь необратимо, какой шаг гейта красит безусловно. Самый частый адрес и
самый дешёвый. самый дешёвый. Сюда же — **воспроизведённый случай** (раздел `## Прецеденты`:
класс, симптом, чем воспроизведён, чем закончилось) и **вопрос конкретному
проходу**, если промах лечится не фактом, а заданным вопросом (раздел
`## Вопросы к проходам`). Прежде чем править charter, проверь, не хватит ли
этих двух разделов: charter общий для всех проектов, бриф — про этот.
- **в конвенции или в правило линтера** — если свойство выражается - **в конвенции или в правило линтера** — если свойство выражается
детерминированно (процедура — [promote.md](promote.md)). детерминированно (процедура — [promote.md](promote.md)).
- **в charter прохода** — если сломан **метод**, а не знание. Правка charter'а - **в charter прохода** — если сломан **метод**, а не знание. Правка charter'а
+162 -52
View File
@@ -6,9 +6,10 @@ description: Проводит несколько задач разом — пл
# Батч задач # Батч задач
Оркестратор **набора** задач. Планирует порядок, раскидывает задачи по Оркестратор **набора** задач. Планирует порядок, раскидывает задачи по
изолированным worktree, каждую проводит через полный цикл `task-pipeline`, затем изолированным worktree, каждую проводит через полный цикл
сводит в основную ветку линейной историей и делает финальную сверку. Тонкая `av-dev-pipeline:task-pipeline`, затем сводит в основную ветку линейной историей
обёртка над `task-pipeline` — не переизобретай её шаги, вызывай как есть. и делает финальную сверку. Тонкая обёртка над пайплайном задачи — не
переизобретай её шаги, вызывай как есть.
Работай **максимально автономно**, по тому же принципу, что и одиночный пайплайн: Работай **максимально автономно**, по тому же принципу, что и одиночный пайплайн:
вопрос, который решать не тебе, записывается и не останавливает поток; спрашиваем вопрос, который решать не тебе, записывается и не останавливает поток; спрашиваем
@@ -16,8 +17,24 @@ description: Проводит несколько задач разом — пл
рабочих данных). Механику — планирование, worktree, rebase, интеграцию, чистку — рабочих данных). Механику — планирование, worktree, rebase, интеграцию, чистку —
делаем без спроса. делаем без спроса.
## Предпосылки
- **OpenSpec и скиллы `opsx:*`** — на них стоит цикл внутри каждого сабагента и
проход `review-specs` финальной сверки. Проекта без OpenSpec это касается так
же, как одиночного пайплайна (см. его раздел «Предпосылки»).
- **Скиллы зовутся с пространством имён**: `av-dev-pipeline:task-pipeline`,
`av-dev-pipeline:review-pipeline`, `av-dev-pipeline:project-brief`. Короткое имя
может разрешиться в устаревшую проектную копию, и это произойдёт молча — в
charter'е сабагента пиши полное имя, он твоего контекста не видит.
- **Проектные копии этих скиллов и агентов при установке плагина удаляются.**
Перед стартом прочитай `CLAUDE.md` проекта и бриф ревью (`docs/review-brief.md`): Перед стартом прочитай `CLAUDE.md` проекта и бриф ревью (`docs/review-brief.md`):
из него берутся команда гейта, инварианты и раскладка нумерованных артефактов. из него берутся **основная ветка** (раздел `## Карта` — она подставляется в
каждую команду git ниже), команда гейта, инварианты и раскладка нумерованных
артефактов. Брифа нет — заведи его Skill'ом
**`av-dev-pipeline:project-brief`** один раз, до первой волны: иначе каждая
задача батча заплатит деградированным ревью, а имя основной ветки придётся
угадывать.
## Границы ## Границы
@@ -27,6 +44,9 @@ description: Проводит несколько задач разом — пл
- **Батч не владеет спринтом и целями.** Он сообщает исход по каждой задаче в тех - **Батч не владеет спринтом и целями.** Он сообщает исход по каждой задаче в тех
же трёх словах, что и `task-pipeline`: сделана / не доведена / оказалась крупнее же трёх словах, что и `task-pipeline`: сделана / не доведена / оказалась крупнее
задачи. задачи.
- **Записей учёта батч не трогает и задач не закрывает** — как и одиночный
пайплайн. Закрытие — акт владельца спринта после приёмки. Урожай ревью батч
отдаёт списком, а задачи из него заводит тот, кто ведёт задачи проекта.
## Ключевое отличие от одиночного пайплайна ## Ключевое отличие от одиночного пайплайна
@@ -62,25 +82,53 @@ fast-forward. Ветки после вливания удаляются.
- **затронутые capability** — по её описанию и по каталогу актуальных спек - **затронутые capability** — по её описанию и по каталогу актуальных спек
(`openspec/specs/`); (`openspec/specs/`);
- **жёсткие зависимости**: задача B строится на результате A → A строго раньше B; - **жёсткие зависимости**: задача B строится на результате A → A строго раньше B;
- **замеряющая задача** — та, чьё ревью будет доказывать находки **числами**, и
потому она гонится в волне **одна** (обоснование — ниже, в шаге 4). Решается
здесь, на планировании, а не во время прогона: состав волны определяется
сейчас, а профиль ревью сабагент выберет только внутри задачи, и ключевать
волну на ещё не сделанный выбор нельзя. Триггеры — по фактам о задаче, каждый
сам по себе достаточен:
- трогает схему хранилища, миграцию, формат на диске или объём хранимого;
- трогает конкурентность: транзакции, блокировки, фоновые циклы, общее
состояние;
- трогает размер тела, буфер, память, сжатие, ретеншен, темп потока;
- её тема названа в разделах `## Прод и поток` или `## Прецеденты` брифа как
место, где уже мерили или уже ломалось.
Ни один триггер не сработал — задача не замеряющая, даже если её ревью
окажется `deep`. `deep` про глубину проверки, замеряющая — про соревнование за
железо; это разные вопросы, и совпадают они не всегда;
- **нумерованные артефакты — номера раздаёт оркестратор заранее.** Если проект - **нумерованные артефакты — номера раздаёт оркестратор заранее.** Если проект
нумерует миграции или подобные файлы (путь — из брифа), посмотри последний нумерует миграции или подобные файлы (путь — из брифа), посмотри последний
номер и **раздай номера тем задачам, которые, вероятно, их добавят**, до номер и **раздай номера всем задачам, которые, вероятно, их добавят**, до
запуска. Номер уходит в charter сабагента, и он берёт назначенный, а не запуска. Номер уходит в charter сабагента, и он берёт назначенный, а не
«следующий свободный». Так такие задачи можно гнать одновременно: файлы не «следующий свободный».
столкнутся, а описание схемы правят разные строки — конфликт мелкий и решается
на интеграции; **Это отдельная механика от правила волны, и она ему не служит** — их раньше
путали, и они тянули в разные стороны. Правило волны отвечает на вопрос «кто с
кем гонится одновременно», предраздача — на вопрос «какой номер берёт задача».
Раздача нужна там, где **две задачи одной под-пачки** добавляют нумерованный
артефакт: каждая считает «следующий свободный» по основной ветке, которая ещё
не видела соседку, и обе берут один номер. Миграции под это почти не попадают —
миграция и так триггер замеряющей задачи, а замеряющая идёт одна; но
нумерованные артефакты бывают не только миграциями. Поэтому номера раздаются
**всем** задачам с таким артефактом, независимо от того, в какой волне они
окажутся: раздача ничего не стоит, а её отсутствие ловится только конфликтом на
интеграции. Между волнами проблемы нет — ветка следующей волны берётся от
вершины, уже включающей предыдущие;
- **жёстко сериализуем** (не гоняем одновременно) настоящие пересечения: - **жёстко сериализуем** (не гоняем одновременно) настоящие пересечения:
- **одна capability на несколько задач** — две задачи, правящие одну спеку (тем - **одна capability на несколько задач** — две задачи, правящие одну спеку (тем
более одно и то же `### Requirement`), дают не текстовый, а **семантический** более одно и то же `### Requirement`), дают не текстовый, а **семантический**
конфликт при архивации; сериализуем по смыслу, а не только по файлам; конфликт при архивации; сериализуем по смыслу, а не только по файлам;
- пересечение по одним и тем же исходникам; - пересечение по одним и тем же исходникам;
- **мягкие конфликты** сериализовать не надо: индекс беклога (каждая задача - **мягкие конфликты** сериализовать не надо: файлы-перечни, где каждая задача
убирает свою строку) и спеки разных capability — разные строки и файлы, правит **свою** строку (индексы, оглавления, списки записей), и спеки разных
сливаются сами. capability — разные строки и файлы, сливаются сами.
Собери план: **волны** параллельно-безопасных задач плюс сериализованный хвост Собери план: **волны** параллельно-безопасных задач плюс сериализованный хвост
конфликтоопасных, с учётом зависимостей. Покажи план короткой репликой и иди конфликтоопасных, с учётом зависимостей; замеряющие задачи стоят в плане
дальше. отдельными волнами по одной. Покажи план короткой репликой — назвав, какие
задачи признаны замеряющими и по какому триггеру, — и иди дальше.
### 3. Свежая база ### 3. Свежая база
@@ -91,16 +139,17 @@ fast-forward. Ветки после вливания удаляются.
### 4. Прогнать волны ### 4. Прогнать волны
**Потолок параллелизма — 2–3 задачи одновременно.** Каждая задача тянет полный **Потолок параллелизма — 2–3 задачи одновременно.** Каждая задача тянет полный
`task-pipeline` с вложенным ревью и гейтом, поэтому больше трёх разом душат цикл пайплайна с вложенным ревью и гейтом, поэтому больше трёх разом душат
машину и провоцируют гонки. Волну шире трёх бей на под-пачки по ≤3. машину и провоцируют гонки. Волну шире трёх бей на под-пачки по ≤3 и **гони
под-пачки последовательно**: следующая стартует, когда предыдущая вернула отчёты.
Иначе потолок обходится тривиально — шесть задач, запущенных «двумя под-пачками»
в одном сообщении, это шесть задач разом.
**Волна из одной задачи — не вырожденный случай, а обязательный.** Задача, **Волна из одной задачи — не вырожденный случай, а обязательный.** Задача,
ревью которой будет доказывать находки **числами** (профиль `deep`, где работают признанная на шаге 2 **замеряющей**, гонится в волне одна: соседний прогон на той
`adversary` и `ops`: удержание блокировки, пик памяти, рост файлов, длительность же машине портит числа, а находка с испорченным оракулом хуже отсутствующей — она
операции), гонится в волне одна. Соседний прогон на той же машине портит эти выглядит доказанной. Если замеряющая задача всё же пошла в общей волне, её отчёт
числа, а находка с испорченным оракулом хуже отсутствующей — она выглядит обязан нести строку в границах покрытия: замеры сняты под соседней нагрузкой.
доказанной. Если задача всё же пошла в общей волне, её отчёт обязан нести строку
в границах покрытия: замеры сняты под соседней нагрузкой.
Для каждой задачи в под-пачке: Для каждой задачи в под-пачке:
@@ -111,19 +160,28 @@ fast-forward. Ветки после вливания удаляются.
`subagent_type: general-purpose`. Charter сабагента: `subagent_type: general-purpose`. Charter сабагента:
- работай **строго в своём worktree** `<path>`; в другие каталоги и в основную - работай **строго в своём worktree** `<path>`; в другие каталоги и в основную
ветку не лезь; ветку не лезь;
- прогони Skill **`task-pipeline`** ровно на этой задаче, полный цикл SDD с - прогони Skill **`av-dev-pipeline:task-pipeline`** ровно на этой задаче,
обоими чекпоинтами ревью; полный цикл SDD с обоими чекпоинтами ревью;
- если задаче назначен **номер артефакта** — используй строго его; - если задаче назначен **номер артефакта** — используй строго его;
- **профиль ревью выбирается по факту изменения.** Батч не повод понижать - **профиль ревью выбирается по факту изменения.** Батч не повод понижать
профиль: «нас много и мы спешим» — это ровно тот стимул, из-за которого профиль: «нас много и мы спешим» — это ровно тот стимул, из-за которого
проходы пропускают; проходы пропускают;
- **режим прогона проходов — последовательный.** Твой worktree не один на - **режим прогона проходов — последовательный.** Твой worktree не один на
машине; машине;
- **если вложенные сабагенты недоступны** (движок не даёт запускать агентов из
агента) — не пропускай ревью и не понижай профиль: проведи его **инлайн** по
тем же charter'ам `av-dev-pipeline`, сохранив обязательное — гейт до
опиниативных проходов, состав по профилю, триаж последним. И **скажи в
отчёте прямым текстом, что ревью шло инлайн**: инлайновый проход видит
контекст автора и потому декоррелирован слабее — это меняет доверие к
результату, а не только способ запуска;
- **вернуть отчёт**, в котором обязательно: исход задачи одним из трёх слов; - **вернуть отчёт**, в котором обязательно: исход задачи одним из трёх слов;
что сделано; какие вопросы записаны и куда; изменённые файлы; добавлялся ли **объявленный профиль ревью и режим прогона**; что сделано; какие вопросы
нумерованный артефакт и с каким номером; затронутые capability; состояние записаны и куда; изменённые файлы; добавлялся ли нумерованный артефакт и с
гейта; **перечень запущенных проходов ревью поимённо с исходом каждого** и каким номером; затронутые capability; состояние гейта; **перечень
границы покрытия. запущенных проходов ревью поимённо с исходом каждого**; **путь к
сохранённому отчёту триажа** (`openspec/changes/<id>/review/`); шло ли ревью
инлайн; границы покрытия.
Сабагент, упершийся в вопрос, **не останавливает батч**: он записывает вопрос, Сабагент, упершийся в вопрос, **не останавливает батч**: он записывает вопрос,
режет задачу до остатка и доводит остаток — либо, если остатка нет, возвращает режет задачу до остатка и доводит остаток — либо, если остатка нет, возвращает
@@ -132,40 +190,86 @@ fast-forward. Ветки после вливания удаляются.
### 5. Проверить полноту ревью — до интеграции ### 5. Проверить полноту ревью — до интеграции
**Ветка, чей отчёт не называет проходы поимённо, не вливается.** Пропуск прохода **Ветка, чей отчёт не называет профиль и проходы поимённо, не вливается.**
не отличим от прохода без находок, и на уровне батча это ещё опаснее: отчётов Пропуск прохода не отличим от прохода без находок, и на уровне батча это ещё
много, каждый выглядит полным, а сверять их некому, кроме тебя. опаснее: отчётов много, каждый выглядит полным, а сверять их некому, кроме тебя.
По каждой готовой ветке сверь перечень проходов с таблицей профилей скилла Сверка идёт в три шага, и порядок важен:
`review-pipeline` для объявленного профиля. Расхождение — не повод отменять
задачу: дозапусти недостающие проходы **на ветке**, в её worktree, через 1. **Возьми объявленный профиль** из отчёта задачи — он затем и заказан в
`review-pipeline`, и только потом интегрируй. Отчёт дозапуска приложи к отчёту обязательных полях шага 4. Профиля в отчёте нет — перечень проходов сверять
задачи. не с чем; это само по себе основание не вливать, пока сабагент не назовёт
профиль и не обоснует его по факту изменения.
2. **Сверяй с независимым артефактом, а не с прозой отчёта.** Перечень проходов
бери из **сохранённого отчёта триажа** (`openspec/changes/<id>/review/`) —
пайплайн обязан его туда положить. Проза сабагента написана тем же, кто мог
проход и пропустить: она подтверждает сама себя. Отчёта триажа на месте нет —
считай, что состав неизвестен, и дозапускай ревью целиком.
3. **Сверь состав** с таблицей профилей скилла
`av-dev-pipeline:review-pipeline` для объявленного профиля.
Расхождение — не повод отменять задачу: дозапусти недостающие проходы **на
ветке**, в её worktree, через `av-dev-pipeline:review-pipeline`, и только потом
интегрируй.
**Находки дозапуска — такие же находки, и зелёный гейт их не отменяет.** Правило
интеграции «вливаем только зелёные» смотрит на гейт, а дозапущенный `critical`
гейт не красит: он был бы пропущен молча, если это не сказать прямо. Поэтому:
- `critical` или `major` из дозапуска — **вливание этой ветки останавливается**.
Помеченное `инлайн` чинится в её worktree, после починки — гейт, затем
интеграция. Помеченное `развилка` — вопрос в запись, задача режется до остатка
ровно так же, как это сделал бы пайплайн внутри;
- остатка нет — ветка не вливается и уходит в доклад как провалившаяся, со своим
worktree;
- `minor` и `nit` из дозапуска — в урожай доклада, вливанию не мешают.
Отчёт дозапуска приложи к отчёту задачи и назови в докладе (шаг 9), почему он
понадобился: систематический пропуск одного и того же прохода — находка о самом
конвейере, а не о задаче.
### 6. Интегрировать — rebase + fast-forward, по одной ветке ### 6. Интегрировать — rebase + fast-forward, по одной ветке
Сводим ветки **строго последовательно** (линейная история), в порядке Сводим ветки **строго последовательно** (линейная история), в порядке
зависимостей. Вливаем **только зелёные**. зависимостей. Вливаем **только зелёные**.
**Ветка задачи занята её worktree, и это определяет форму команд.** Пока worktree
жив (а удаляется он последним, после зелёного гейта), ветка `task/<slug>`
checkout'нута в нём, и `git rebase <основная> task/<slug>` из главного worktree
**падает**: `fatal: 'task/<slug>' is already used by worktree at …`. Поэтому
rebase делается **внутри worktree задачи**, а ff-слияние — из главного.
Для каждой готовой ветки `task/<slug>`: Для каждой готовой ветки `task/<slug>`:
- `git rebase <основная> task/<slug>` — перенос на текущую вершину; - `git -C <path> rebase <основная>` — перенос ветки задачи на текущую вершину,
выполняется в её собственном worktree;
- резолв конфликтов (их почти нет — конфликтоопасное сериализовано, номера - резолв конфликтов (их почти нет — конфликтоопасное сериализовано, номера
розданы заранее). Неавтоматический конфликт — **не форсируй**: прерви розданы заранее). Неавтоматический конфликт — **не форсируй**: прерви
(`git rebase --abort`), оставь ветку и worktree как есть, вынеси это в доклад (`git -C <path> rebase --abort`), оставь ветку и worktree как есть, вынеси это
как нераспознанное пересечение; в доклад как нераспознанное пересечение;
- `git checkout <основная> && git merge --ff-only task/<slug>`; - **ненулевой код `rebase` относится к этой ветке и только к ней.** Прерванный
rebase в чужом worktree не трогает ни главное дерево, ни остальные ветки:
проверь `git -C <path> status` и `git status` — обе чистые. Уводить весь батч
в провалившиеся из-за одного ненулевого кода запрещено: это ложная причина,
из-за которой зелёные задачи не доедут до основной ветки. Провалилась одна —
провалилась одна;
- из главного worktree (он стоит на основной ветке — проверь
`git rev-parse --abbrev-ref HEAD`): `git merge --ff-only task/<slug>`. Ветку в
главном дереве **не переключай**`git checkout task/<slug>` тоже упрётся в
занятость;
- после каждой интеграции — **гейт на основной ветке**. Красное — **откати эту - после каждой интеграции — **гейт на основной ветке**. Красное — **откати эту
интеграцию** (`git reset --hard` на прошлую вершину), ветку с worktree сохрани, интеграцию** (`git reset --hard` на прошлую вершину), ветку с worktree сохрани,
задачу перечисли в докладе. Основная ветка **никогда** не остаётся задачу перечисли в докладе. Основная ветка **никогда** не остаётся
полузелёной; полузелёной;
- только после зелёного: `git worktree remove <path>` и - только после зелёного: `git worktree remove <path>` и
`git branch -d task/<slug>`. `git branch -d task/<slug>` — в этом порядке, иначе ветка снова занята.
**Политика частичного провала.** Упавшая задача (исход «не доведена», красные **Политика частичного провала.** Упавшая задача (исход «не доведена», красные
тесты в её worktree, конфликт при rebase) **не блокирует остальные**: интегрируем тесты в её worktree, конфликт при rebase, невлитая из-за находок дозапуска)
все зелёные, упавшую оставляем в её worktree и ветке нетронутой — ничего не **не блокирует остальные**: интегрируем все зелёные, упавшую оставляем в её
удаляем, — и перечисляем в докладе с причиной, отчётом и путём к worktree. worktree и ветке нетронутой — ничего не удаляем, — и перечисляем в докладе с
причиной, отчётом и путём к worktree. Причина называется **настоящая**: «конфликт
rebase в файле X», а не «нераспознанное пересечение» на всякий случай.
### 7. Финальный гейт ### 7. Финальный гейт
@@ -191,7 +295,7 @@ fast-forward. Ветки после вливания удаляются.
уже делается» — именно он возникает, когда две задачи независимо решали уже делается» — именно он возникает, когда две задачи независимо решали
похожее. похожее.
Замечания отрабатывай как в `task-pipeline`: `инлайн` чини сам, `развилка` Замечания отрабатывай как одиночный пайплайн: `инлайн` чини сам, `развилка`
вопросом в запись; после правок — снова гейт. вопросом в запись; после правок — снова гейт.
### 9. Прибраться и доложить ### 9. Прибраться и доложить
@@ -199,16 +303,22 @@ fast-forward. Ветки после вливания удаляются.
- Убери worktree и ветки **только успешно влитых** задач, в конце - Убери worktree и ветки **только успешно влитых** задач, в конце
`git worktree prune`. Worktree и ветки **провалившихся** не трогай — они нужны `git worktree prune`. Worktree и ветки **провалившихся** не трогай — они нужны
для ручного дожатия. для ручного дожатия.
- **Задачи батч не закрывает** — ни одну, ни свои, ни чужие записи учёта не
трогает. Он сообщает исход по каждой; закрытие происходит после приёмки и
делается владельцем спринта.
- Доложи кратко: - Доложи кратко:
- **исход по каждой задаче** одним из трёх слов, с хешем коммита; - **исход по каждой задаче** одним из трёх слов, с хешем коммита;
- план волн и порядок интеграции; - план волн и порядок интеграции, с пометкой, какие задачи шли по одной как
замеряющие;
- вопросы, записанные сабагентами, пачкой; - вопросы, записанные сабагентами, пачкой;
- что дозапускалось на шаге 5 и почему; - что дозапускалось на шаге 5 и почему; шло ли где-то ревью инлайн;
- итог финальной сверки и ссылки на архивные change; - итог финальной сверки и ссылки на архивные change;
- **отдельно — провалившиеся** задачи с причиной и путём к оставленному - **`Урожай`** — отложенные находки всех задач одним списком, с провенансом.
worktree; Задачи из него заводит тот, кто ведёт задачи проекта, а не батч;
- **отдельно — провалившиеся** задачи с настоящей причиной и путём к
оставленному worktree;
- **границы покрытия сводной строкой**, включая задачи, чьи замеры снимались в - **границы покрытия сводной строкой**, включая задачи, чьи замеры снимались в
общей волне. общей волне, и ветки, где ревью шло инлайн.
## Тонкости ## Тонкости
@@ -219,9 +329,9 @@ fast-forward. Ветки после вливания удаляются.
- Поведенческая верификация внутри сабагента поднимает изменение вживую: следи, - Поведенческая верификация внутри сабагента поднимает изменение вживую: следи,
чтобы соседние worktree не дрались за порты и рабочие каталоги. Если проект чтобы соседние worktree не дрались за порты и рабочие каталоги. Если проект
умеет поднимать только один экземпляр — такие задачи в одну волну не ставь. умеет поднимать только один экземпляр — такие задачи в одну волну не ставь.
- Ревью выполненного — **до** закрытия задачи; это забота `task-pipeline` внутри - Ревью выполненного — **до** интеграции; это забота
каждого сабагента, дублировать не надо. `av-dev-pipeline:task-pipeline` внутри каждого сабагента, дублировать не надо.
- `openspec validate --strict` тоже внутри `task-pipeline` — не пропускай его - `openspec validate --strict` тоже внутри пайплайна задачи — не пропускай его
своими правками на интеграции. своими правками на интеграции.
- Крупная переработка, предложенная ревью внутри задачи, — развилка: не вливай - Крупная переработка, предложенная ревью внутри задачи, — развилка: не вливай
молча, вынеси в доклад. молча, вынеси в доклад.
+104 -44
View File
@@ -10,18 +10,48 @@ description: Автономно проводит одну задачу чере
Это тонкая обёртка над каноническими скиллами `opsx:explore` / `opsx:propose` / Это тонкая обёртка над каноническими скиллами `opsx:explore` / `opsx:propose` /
`opsx:apply` / `opsx:archive` — вызывай их через Skill, не переизобретай их шаги. `opsx:apply` / `opsx:archive` — вызывай их через Skill, не переизобретай их шаги.
Ревью — скилл `review-pipeline`, он же держит правило выбора профиля. Ревью — скилл `av-dev-pipeline:review-pipeline`, он же держит правило выбора
профиля.
## Предпосылки
- **OpenSpec и скиллы `opsx:*`** — внешняя обвязка, на которой стоят шаги 2, 3, 6
и 8, а также проход `review-specs` и профиль `design` (они завязаны на
`openspec/changes/<id>/specs/*/spec.md` и на `openspec validate --strict`). В
проекте без OpenSpec эти шаги упадут на «нет такого скилла»: либо подключаем
OpenSpec, либо цикл вырождается в «прочитать задачу → код → ревью кода →
коммит», и об отсутствии спекового контура говорится в докладе.
- **Скиллы зовутся с пространством имён** — `av-dev-pipeline:review-pipeline`,
`av-dev-pipeline:project-brief`. Короткое имя может разрешиться в устаревшую
проектную копию, и это произойдёт молча.
- **Проектные копии этих скиллов и агентов удаляются при установке плагина**
(`.claude/skills/{task-pipeline,review-pipeline,task-batch}`,
`.claude/agents/<проект>-review-*.md`). Две копии одного скилла расходятся, и
побеждает та, что короче названа.
Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается
(архитектура, конвенции), если ещё не в контексте. Проектные факты, нужные ревью (архитектура, конвенции), если ещё не в контексте. Проектные факты, нужные ревью
— инварианты, команда гейта, объёмы, модель угроз, — живут в брифе — инварианты, команда гейта, объёмы, модель угроз, прецеденты, — живут в брифе
(`docs/review-brief.md`, контракт — в references конвейера ревью). (`docs/review-brief.md`, контракт — в references конвейера ревью).
**Брифа нет ни по одному пути — заведи его, а не работай в деградированном
режиме.** Вызови Skill **`av-dev-pipeline:project-brief`**: он соберёт бриф из
`CLAUDE.md`, архитектуры, файла задач и конвенций, покажет человеку и вернёт
путь, который дальше передаётся ревью. Это механика: спрашивать разрешения не
нужно. Один шаг один раз на проект — против деградации на каждой задаче.
## Границы: чем пайплайн не владеет ## Границы: чем пайплайн не владеет
- **Беклогом, спринтом, целями и приоритетами.** Задача приходит извне. Пайплайн - **Беклогом, спринтом, целями и приоритетами.** Задача приходит извне. Пайплайн
её не выбирает, не приоритизирует, не заводит и не переоценивает; если в её не выбирает, не приоритизирует, не заводит и не переоценивает; если в
проекте есть свой процесс управления задачами — он и решает, что брать. проекте есть свой процесс управления задачами — он и решает, что брать.
- **Записями учёта.** Пайплайн **не закрывает задачу**, не двигает её по
статусам, не правит индекс и не зовёт скриптов учёта. Он сообщает исход;
закрытие — акт владельца спринта **после приёмки**, и оно происходит снаружи.
Закрыть задачу самому — значит закрыть её до коммита и до всякой приёмки, то
есть заверить собственную работу.
- **Заведением задач из урожая ревью.** Отложенные находки отдаются **списком**
(см. шаг 7); превращать их в задачи — работа того, кто ведёт задачи проекта.
- **Определением ценности.** «Нужна ли эта функциональность» — не вопрос - **Определением ценности.** «Нужна ли эта функциональность» — не вопрос
пайплайна ни на одном шаге. пайплайна ни на одном шаге.
@@ -47,12 +77,17 @@ description: Автономно проводит одну задачу чере
поимённо, непущенные проходы названы в границах покрытия; поимённо, непущенные проходы названы в границах покрытия;
3. change заархивирован, дельты влиты в актуальные спеки; 3. change заархивирован, дельты влиты в актуальные спеки;
4. коммит сделан в текущую ветку; 4. коммит сделан в текущую ветку;
5. **критерии приёмки, если проект их дал**, проверены поимённо, у каждого назван 5. **критерии приёмки, если проект их дал, выписаны поимённо, и по каждому назван
оракул и исход. Критерии приходят снаружи; пайплайн их не сочиняет и не оракул и наблюдаемый исход** — «прогнал вот это, увидел вот то». Это **доклад,
занижает. Расхождение «критерии закрыты, а суть задачи не достигнута» — дефект а не сертификация: приёмка — не работа пайплайна.** Исполнитель, ставящий себе
критериев, и о нём сообщается, а не молча дорабатывается. галочку «принято», проверяет свою работу своим же взглядом — по границе это
может делать только декоррелированный приёмщик. Критерии приходят снаружи;
пайплайн их не сочиняет и не занижает. Расхождение «по каждому критерию исход
есть, а суть задачи не достигнута» — дефект критериев, и о нём сообщается, а
не молча дорабатывается.
Пункты 1–4 — своё. Пункт 5 — внешнее, и проверяется только если оно дано. Пункты 1–4 — своё. Пункт 5 — внешнее: пайплайн доводит его до наблюдаемого
исхода и передаёт дальше.
## Принцип автономности ## Принцип автономности
@@ -73,13 +108,23 @@ description: Автономно проводит одну задачу чере
3. Доведи остаток до конца и закоммить. Задача не «висит на вопросе», она сделана 3. Доведи остаток до конца и закоммить. Задача не «висит на вопросе», она сделана
в объявленных границах. в объявленных границах.
**Что остатком не является** — две оговорки, без которых правило вредит: **Что остатком не является — правило живёт не здесь.** Канонический текст с обеими
оговорками — в плагине `av-dev-tasks`, скилл `av-dev-tasks:session`, раздел
`## Вопрос, блокер, необратимое`, подраздел «Отличать вопрос от застревания».
Правило принадлежит управлению задачами, потому что решает **сделана задача или
вышла**, — это исход планирования, а не исполнения. **Ссылайся, не
пересказывай:** копия, заведённая здесь, уже однажды разошлась с оригиналом и
потеряла из перечня самое необратимое — запись **наружу**.
- **остаток, который записывает в хранилище или в журнал состояние, зависящее от Коротко, чтобы знать, когда идти читать: остаток проверяется двумя порогами —
нерешённого, — не остаток.** Решение поднимается до начала записи. Иначе **материализация нерешённого** (запись состояния, зависящего от неотвеченного
нерешённое материализуется в данные, а данные переживают решение; вопроса) и **пол по пользе** (из остатка пропала польза, названная в постановке).
- **остаток, из которого пропала польза, названная в постановке, — не остаток.** Оба порога — стоп: первый поднимает решение до начала записи, второй даёт исход
Это исход «не доведена», а не «сделана в границах». «не доведена».
Плагин `av-dev-tasks` не подключён — правило не отменяется, а становится
осторожнее: прежде чем записать зависящее от нерешённого куда бы то ни было —
в хранилище, в журнал, в витрину или наружу, — спрашивай человека.
Нет полезного остатка — задача заканчивается исходом «не доведена», вопрос Нет полезного остатка — задача заканчивается исходом «не доведена», вопрос
записан, ничего не коммитится наполовину. записан, ничего не коммитится наполовину.
@@ -137,10 +182,10 @@ description: Автономно проводит одну задачу чере
### 4. (Нетривиальная) Ревью предложения — профиль `design`, ДО кода ### 4. (Нетривиальная) Ревью предложения — профиль `design`, ДО кода
Первый чекпоинт. Вызови Skill **`review-pipeline`** с профилем `design`, ссылкой Первый чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`** с профилем
на change `<id>` и путём к брифу. Он запустит `review-specs` (режим «дизайн ДО `design`, ссылкой на change `<id>` и путём к брифу. Он запустит `review-specs`
кода»), `review-rubric` (фаза 1: приёмочные критерии для задуманного узла) и (режим «дизайн ДО кода»), `review-rubric` (фаза 1: приёмочные критерии для
`review-architecture` по предложению. задуманного узла) и `review-architecture` по предложению.
Смысл профиля: архитектурная находка на готовом коде стоит переписывания и Смысл профиля: архитектурная находка на готовом коде стоит переписывания и
потому игнорируется — та же находка здесь стоит абзаца обсуждения. Рубрику из потому игнорируется — та же находка здесь стоит абзаца обсуждения. Рубрику из
@@ -170,17 +215,16 @@ description: Автономно проводит одну задачу чере
**Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца **Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца
шага. шага.
### 7. Ревью кода — Skill `review-pipeline` ### 7. Ревью кода — Skill `av-dev-pipeline:review-pipeline`
Второй чекпоинт. Вызови Skill **`review-pipeline`**, дав ссылку на change `<id>`, Второй чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`**, дав ссылку
базу диффа, путь к брифу, профиль **и режим запуска**. Профиль выбирается по на change `<id>`, базу диффа, путь к брифу, профиль **и режим запуска**.
факту изменения, а не по ощущению важности; общее правило — в скилле, проектные
триггеры — в брифе:
- миграция схемы, новый пакет, публичный контракт, правило идентичности или **Правило выбора профиля живёт в скилле конвейера** (раздел «Профили»), проектные
слияния данных → `deep`; триггеры — в разделе `## Триггеры` брифа. Здесь оно не пересказывается: три
- иначе меняется поведение, видимое снаружи → `standard`; копии одного правила расходятся, и работать будет та, которую прочитали
- иначе (багфикс, локальная правка, доки) → `quick`. последней. Помни ровно одно — **профиль выбирается по факту изменения, а не по
ощущению важности**, и посмотри таблицу перед вызовом.
**Режим по умолчанию последовательный, и обосновывать его не надо.** Параллельно **Режим по умолчанию последовательный, и обосновывать его не надо.** Параллельно
гоняем только тогда, когда об этом попросили явно **и назвали набор** — какие гоняем только тогда, когда об этом попросили явно **и назвали набор** — какие
@@ -195,41 +239,54 @@ description: Автономно проводит одну задачу чере
покрытия. покрытия.
**Сверь состав прогона с таблицей профилей в скилле, прежде чем коммитить.** **Сверь состав прогона с таблицей профилей в скилле, прежде чем коммитить.**
Пропуск прохода не отличим от прохода без находок: гейт зелёный, спеки сошлись, Отчёт обязан называть запущенные проходы **поимённо и с исходом**; непущенный
отчёт выглядит полным. Единственный, кто мог бы заметить пропуск, — триаж, а он идёт строкой «не запускался» в границы покрытия. Реестр короткий (4–8 проходов) —
заполняется тем, что ему подали. Отчёт обязан называть запущенные проходы сверка стоит одного взгляда. Почему это правило существует, объясняет раздел
**поимённо и с исходом**; непущенный идёт строкой «не запускался» в границы «Профили» скилла конвейера; здесь — само требование.
покрытия. Реестр короткий (4–8 проходов) — сверка стоит одного взгляда, а
молчащий пропуск уже стоил семи находок и отдельной задачи на их дозакрытие.
Отработай так же, как шаг 5: помеченное `инлайн` чини сам и не логируй, Отработай так же, как шаг 5: помеченное `инлайн` чини сам и не логируй,
`развилка` — вопросом в запись (он уже сформулирован триажем, его остаётся `развилка` — вопросом в запись (он уже сформулирован триажем, его остаётся
перенести). После правок — снова гейт. перенести). После правок — снова гейт.
**Урожай — списком, не задачами.** Отложенные находки (реальный `major` не для
этого мерджа, развилка, решённая «потом», пачка `nit`) собери в секцию доклада
`Урожай`: формулировка, оракул, провенанс. Задачи из него **заводит не пайплайн**
— у того, кто ведёт задачи проекта, своя нарезка, свой формат и свои правила
дублей. Твоя обязанность — не потерять и передать.
**Границы покрытия из отчёта не выбрасывай** — они уезжают в финальный доклад **Границы покрытия из отчёта не выбрасывай** — они уезжают в финальный доклад
сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно», сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно»,
превращается в ложное ощущение проверенности. превращается в ложное ощущение проверенности.
Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`): по нему **Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`) — это
потом видно, что было найдено и что из этого осталось незаведённым. обязательно, а не «если удобно».** По нему потом видно, что было найдено и что из
этого осталось в урожае. И это единственный **независимый** артефакт о составе
прогона: под оркестратором `task-batch` именно по нему сверяют полноту ревью
ветки, а не по твоей прозе — она написана тем же, кто мог проход и пропустить.
### 8. Архивировать — `opsx:archive` ### 8. Архивировать — `opsx:archive`
Вызови Skill `opsx:archive`: change уезжает в архив, дельты вливаются в Вызови Skill `opsx:archive`: change уезжает в архив, дельты вливаются в
актуальные спеки. актуальные спеки.
### 9. Синк документации и закрытие задачи ### 9. Синк документации
Ревью выполненного — **до** закрытия. Затем: Ревью выполненного — до этого шага. Затем:
- суть переехавшего решения — в документацию проекта (архитектура, журнал - суть переехавшего решения — в документацию проекта (архитектура, журнал
решений), если её там ещё нет; решений), если её там ещё нет;
- менялась схема — её описание обновлено тем же change; - менялась схема — её описание обновлено тем же change;
- новое, узнанное о внешнем формате или о данных, — в тот файл проекта, который - новое, узнанное о внешнем формате или о данных, — в тот файл проекта, который
это накапливает; такой файл обычно ценнее кода; это накапливает; такой файл обычно ценнее кода;
- **задача закрывается процедурой проекта** — своей у пайплайна нет. Есть скилл - воспроизведённый дефект (свой или чужой) — в раздел `## Прецеденты` брифа:
или скрипт беклога — вызови его; нет — скажи в докладе, что задача сделана и класс, симптом, чем воспроизведён, чем закончилось. Это единственный артефакт,
закрытие остаётся за вызывающим. Не выдумывай формат чужого индекса. который делает следующее ревью умнее.
**Задачу пайплайн не закрывает.** Записи учёта — индекс, статус, спринт — он не
трогает вовсе: закрытие происходит **после приёмки** и делается владельцем
спринта, а пайплайн на этом шаге стоит до коммита и до всякой приёмки. Скриптов
и скиллов учёта не зови — их у тебя и нет: пути между плагинами не разрешаются, и
моста здесь намеренно не проложено. Твоё дело — назвать исход в докладе.
### 10. Коммит ### 10. Коммит
@@ -242,13 +299,17 @@ description: Автономно проводит одну задачу чере
сделано», тело списком 1–3 пункта, без трейлеров). Одна задача — один осмысленный сделано», тело списком 1–3 пункта, без трейлеров). Одна задача — один осмысленный
коммит. коммит.
Готово — доложи кратко: Готово — доложи кратко. Доклад и есть выход пайплайна: **задача остаётся
открытой**, её закрывает владелец спринта после приёмки.
- **исход** задачи одним из трёх слов и, если не «сделана», чем ограничен - **исход** задачи одним из трёх слов и, если не «сделана», чем ограничен
результат; результат;
- что сделано, какие вопросы записаны и куда; - что сделано, какие вопросы записаны и куда;
- ссылка на архивный change; - ссылка на архивный change и хеш коммита;
- исход по каждому критерию приёмки, если они были; - по каждому критерию приёмки, если они были: **оракул и наблюдаемый исход**
это доклад приёмщику, а не отметка «принято»;
- **`Урожай`** — отложенные находки списком (формулировка, оракул, провенанс).
Задачи из него заводит тот, кто ведёт задачи проекта;
- **одна строка границ покрытия**: какой профиль и режим гонялись, какие проходы - **одна строка границ покрытия**: какой профиль и режим гонялись, какие проходы
не запускались и что проверить было невозможно. Доклад без неё сообщает не запускались и что проверить было невозможно. Доклад без неё сообщает
«проверено», не сообщая, что именно. «проверено», не сообщая, что именно.
@@ -270,5 +331,4 @@ description: Автономно проводит одну задачу чере
- **Занизить профиль ревью или пропустить проход — самый дешёвый способ - **Занизить профиль ревью или пропустить проход — самый дешёвый способ
«ускориться», и он же самый дорогой по последствиям.** Защита одна: профиль «ускориться», и он же самый дорогой по последствиям.** Защита одна: профиль
выбирается по факту изменения, состав сверяется поимённо, а непущенное выбирается по факту изменения, состав сверяется поимённо, а непущенное
называется в отчёте. Пропуск, названный строкой, стоит строки; пропуск молчащий называется в отчёте строкой.
стоил семи находок.