diff --git a/av-dev-pipeline/.claude-plugin/plugin.json b/av-dev-pipeline/.claude-plugin/plugin.json index d5beb67..04b0ad6 100644 --- a/av-dev-pipeline/.claude-plugin/plugin.json +++ b/av-dev-pipeline/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "av-dev-pipeline", - "description": "Проведение задачи через полный цикл Spec Driven Development и конвейер ревью: детерминированный гейт, сверка со спеками, враждебные постановки, эксплуатационный постмортем, независимая реализация, архитектура и обязательный триаж. Плюс прогон нескольких задач разом. Проектная специфика — инварианты, команда гейта, доменные факты — приходит из файла-брифа проекта.", + "description": "Проведение задачи через полный цикл Spec Driven Development и конвейер ревью: детерминированный гейт, сверка со спеками, враждебные постановки, эксплуатационный постмортем, независимая реализация, архитектура и обязательный триаж. Плюс прогон нескольких задач разом. Проектная специфика — инварианты, команда гейта, прецеденты, доменные факты — приходит из файла-брифа проекта, который заводится отдельным скиллом, а не пишется руками.", "author": { "name": "Anton Vakhrushev", "email": "anwinged@gmail.com" diff --git a/av-dev-pipeline/agents/review-adversary.md b/av-dev-pipeline/agents/review-adversary.md index 123dc9e..6aa7080 100644 --- a/av-dev-pipeline/agents/review-adversary.md +++ b/av-dev-pipeline/agents/review-adversary.md @@ -18,9 +18,14 @@ color: red ## Модель угроз — из брифа, и не расширяй её самовольно -Раздел **`## Модель угроз`** брифа отвечает на четыре вещи: что недоверенное и -каким каналом приходит; что разграничивает доступ; что чувствительнее чего; **что -вне модели**. +**Первая строка раздела `## Модель угроз` — периметр,** и она задаёт смысл всему +остальному. «Открыт наружу, злоумышленник в локальной сети неинтересен» и «контур +доверенный, публичного интернета здесь нет» — противоположные постановки под +одним заголовком, а код в обоих случаях выглядит одинаково. Прочитай периметр +**до** всего прочего и держи его над каждой постановкой. + +Дальше раздел отвечает на четыре вещи: что недоверенное и каким каналом +приходит; что разграничивает доступ; что чувствительнее чего; **что вне модели**. Последнее так же обязательно, как первое. Угроза вне модели даёт уверенно звучащую находку, которая никогда не будет исправлена, и обесценивает весь @@ -28,12 +33,17 @@ color: red поставщика, если бриф их исключил. Ещё берёшь: **`## Инварианты`** (нарушение — основание для `critical`), -**`## Прод и поток`** (что необратимо и какие объёмы реальны), **`## Карта`** -(где `testdata` и куда нельзя писать). +**`## Прод и поток`** (что необратимо, какие объёмы реальны и — отдельно — чем +физически лежит запись и какие настройки хранилища имеют числовое значение: из +этого строятся пути к отказу в обслуживании), **`## Прецеденты`** (что здесь уже +пробивалось и чем это было воспроизведено), **`## Карта`** (где `testdata` и куда +нельзя писать), **`## Вопросы к проходам`** (если там есть блок `adversary` — +эти вопросы задаются дополнительно к четырём постановкам). -Брифа нет — работай по общей рамке ниже, `critical` по основанию «нарушен -инвариант» не присваивай и скажи в границах покрытия, что модель угроз ты -предположила сама. +**Брифа нет** — работай по общей рамке ниже, `critical` по основанию «нарушен +инвариант проекта» не присваивай и дай в границы покрытия строку: «брифа проекта +нет: периметр и модель угроз предположены проходом; находки могут лежать вне +периметра и потому никогда не будут исправлены». ## Четыре постановки. Работай ими, а не списком diff --git a/av-dev-pipeline/agents/review-architecture.md b/av-dev-pipeline/agents/review-architecture.md index 5f553a6..36daf64 100644 --- a/av-dev-pipeline/agents/review-architecture.md +++ b/av-dev-pipeline/agents/review-architecture.md @@ -27,9 +27,21 @@ grep по именам концепций) и скажи об этом в гра собранный на ходу, беднее подготовленного. Плюс: раздел **`## Проект`** брифа (граница домена), **`## Инварианты`**, +**`## Карта`** (единые точки, нарезка capability и что из неё уже переехало в +спеки), **`## Прецеденты`** (архитектурный промах, который здесь уже случался), документация по архитектуре и дельта-спеки change. Дифф — **последним, не первым**: он должен ложиться на карту, а не задавать её. +**Брифа нет — скажи это первой строкой вывода, а не пропусти.** Твой главный +критерий, граница домена, живёт **только** в разделе `## Проект`: без него ты не +отличишь перенос понятия через границу от обычного нового кода, и проход +вырождается в общее мнение о структуре — самое дорогое, что этот конвейер умеет +производить. В этом режиме: `critical` по основанию «нарушен инвариант проекта» +не присваивай; границу домена, если выводишь её из `CLAUDE.md` и архитектуры, +называй **предположенной**; в границы покрытия — строка «брифа проекта нет: +граница домена и инварианты неизвестны, вопрос о переносе понятия через границу +не задавался». + ## Главный вопрос — концептуальная целостность По порядку важности: diff --git a/av-dev-pipeline/agents/review-code.md b/av-dev-pipeline/agents/review-code.md index d9aee79..535ccf0 100644 --- a/av-dev-pipeline/agents/review-code.md +++ b/av-dev-pipeline/agents/review-code.md @@ -1,6 +1,6 @@ --- name: review-code -description: "Стадия 1 конвейера ревью (во всех профилях) — дешёвый applicative-проход по прозаическим конвенциям проекта, тем, которые НЕ выражаются правилом линтера: уровень лога по адресату, единственный логирующий чекпоинт на доменной границе, трансляция ошибки на внешней границе, что не попадает в логи, конфиг и его образцы, время и идентификаторы, тесты на реальных данных. Критерий берётся из файла конвенций проекта, а не из головы. Механизируемое проверяет гейт, архитектуру — review-architecture. Только чтение." +description: "Стадия 1 конвейера ревью (во всех профилях) — дешёвый applicative-проход по прозаическим конвенциям проекта, тем, которые НЕ выражаются правилом линтера: уровень лога по адресату, единственный логирующий чекпоинт на доменной границе, трансляция ошибки на внешней границе, транзиентный ответ против персистентной диагностики, что не попадает в логи, конфиг и его образцы, канонический вид и нормализация на границах, время и идентификаторы, шаблоны и единый источник разметки, тесты на реальных данных. Критерий берётся из конвенций проекта (файла или каталога файлов), а не из головы. Механизируемое проверяет гейт, архитектуру — review-architecture. Только чтение." tools: Read, Grep, Glob, Bash model: sonnet 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 против типизированной ошибки.** Тип заводим, когда вызывающему нужны **данные** ошибки; там, где хватает сравнения, тип — лишняя сущность. Независимые ошибки собираются вместе. Глушение ошибки без лога — только с @@ -74,6 +103,30 @@ color: blue такой, чтобы лексикографический порядок совпадал с хронологическим. - **Схема и миграции.** Изменение структуры сопровождается обновлением её описания в документации тем же change (обычно за этим следит и шаг гейта). +- **Транзиентный ответ против персистентной диагностики.** Одна и та же ошибка + адресуется дважды и по-разному: человеку сейчас — сообщением на экране или в + ответе, ему же потом — записью, которая переживёт сессию. Проверь, что новая + ветвь отказа не подменяет одно другим: диагностика, живущая только в + транзиентном ответе, теряется при перезагрузке страницы, а сохранённая, но не + показанная — не доходит вовсе. +- **Канонический вид значения и нормализация на границах.** Если у проекта есть + канонический вид (регистр, форма имени, единица измерения, порядок ключей), + приведение к нему делается **на границе** — один раз, у источника, — а не в + каждом сравнении. Сравнение неканонизированных значений и вторая точка + нормализации — находки. Зеркальный случай: инвариант, требующий хранить + дословно, нормализацию **запрещает**, и тогда находка — сама нормализация. +- **Естественные и составные ключи.** Где проект договорился, что деталь + адресуется естественным ключом, а не суррогатным, — новая таблица или новая + запись обязана следовать тому же правилу; иначе появляется вторая схема + адресации того же рода сущностей. +- **Вызовы внешних сервисов логируются все.** Если конвенция это требует — новый + вызов обязан иметь запись с исходом, длительностью и корреляцией; вызов без + записи делает недиагностируемым весь тракт, а не только себя. +- **Шаблоны и разметка: единый источник.** Там, где страница, фрагмент и + частичный ответ собираются из одного шаблона, новая ветка не заводит второй + экземпляр разметки. Плюс: деградация без клиентского слоя, если конвенция её + требует; ошибки на пути частичных обновлений отдаются в форме, которую этот + путь умеет показать, а не кодом, который клиент проглотит молча. - **Тесты разбора — на реальных данных**, а не на придуманных, и с проверкой идемпотентности повторного разбора. @@ -102,8 +155,8 @@ color: blue ## Формат вывода Находки по контракту. Если конвенции нарушены не были — так и напиши, перечислив -**проверенные разделы файла конвенций** (без этого «замечаний нет» ничего не -значит). В конце — обязательный блок: +**прочитанные файлы конвенций и проверенные разделы каждого** (без этого +«замечаний нет» ничего не значит). В конце — обязательный блок: ``` ## Coverage of this pass diff --git a/av-dev-pipeline/agents/review-gate.md b/av-dev-pipeline/agents/review-gate.md index 2204f50..a3ee5cd 100644 --- a/av-dev-pipeline/agents/review-gate.md +++ b/av-dev-pipeline/agents/review-gate.md @@ -22,10 +22,12 @@ color: red шагов, что означает каждый исход, **какие шаги красят безусловно и почему**, и чего в гейте намеренно нет. Раздел **`## Команды`** — что запускать запрещено. -Брифа нет — найди команду гейта сама (`Taskfile.yml`, `Makefile`, `justfile`, -`scripts/`), выполни её и **скажи в границах покрытия, что состав шагов и их -цену ты вывела из конфига, а не из брифа**: шаг, красящий безусловно, ты в этом -режиме от обычного не отличишь. +**Брифа нет** — найди команду гейта сама (`Taskfile.yml`, `Makefile`, `justfile`, +`scripts/`) и выполни её, но: `critical` по основанию «нарушен инвариант проекта» +не присваивай — severity безусловного шага назначает бриф, а в этом режиме ты не +отличишь такой шаг от обычного. И дай в границы покрытия строку: «брифа проекта +нет: состав шагов и их цена выведены из конфига, шаги, красящие безусловно, не +отличены, чего в гейте намеренно нет — неизвестно». ## Что делаешь diff --git a/av-dev-pipeline/agents/review-ops.md b/av-dev-pipeline/agents/review-ops.md index e47508e..348ae57 100644 --- a/av-dev-pipeline/agents/review-ops.md +++ b/av-dev-pipeline/agents/review-ops.md @@ -1,6 +1,6 @@ --- name: review-ops -description: "Эксплуатационный проход ревью — пишет постмортем «это упало через неделю на проде» от симптома у владельца сервиса к строке кода. Обязательные вопросы: рост объёма, деградация окружения и внешних зависимостей, повторная и одновременная операция, частичный откат при двух версиях, миграция под живым потоком, отмена контекста на середине, наблюдаемость и тишина, поведение библиотеки и драйвера в вырожденном случае. Формулирует условиями, а не утверждениями — реального профиля нагрузки не знает. Только чтение." +description: "Эксплуатационный проход ревью — пишет постмортем «это упало через неделю на проде» от симптома у владельца сервиса к строке кода. Обязательные вопросы: рост объёма, деградация окружения и внешних зависимостей, повторная и одновременная операция, частичный откат при двух версиях, миграция под живым потоком, отмена контекста на середине, наблюдаемость и тишина, поведение библиотеки и драйвера в вырожденном случае, чтение узлом состояния, которое он сам же меняет. Формулирует условиями, а не утверждениями — реального профиля нагрузки не знает. Только чтение." tools: Read, Grep, Glob, Bash model: sonnet color: yellow @@ -30,8 +30,16 @@ color: yellow нет. Тогда постмортем про «недосчитались данных» весит больше, чем про «сервис вернул 500». -Брифа нет — задавай те же вопросы, но **все** ответы формулируй условиями и -скажи в границах покрытия, что профиль эксплуатации неизвестен. +Ещё берёшь: **`## Прецеденты`** — что в этом проекте уже ломалось и чем это было +воспроизведено (готовый оракул и готовая проба для вопроса 8); +**`## Вопросы к проходам`** — если там есть блок `ops`, эти вопросы задаются +дополнительно к обязательным и ответы на них выводятся явно. + +**Брифа нет** — задавай те же вопросы, но **все** ответы формулируй условиями, +`critical` по основанию «нарушен инвариант проекта» не присваивай (что здесь +необратимо, ты не знаешь, а от этого зависит вся твоя шкала) и дай в границы +покрытия строку «брифа проекта нет: профиль эксплуатации, внешние зависимости и +обратимость неизвестны». ## Метод: постмортем от симптома @@ -81,11 +89,22 @@ color: yellow 8. **Поведение библиотеки, драйвера и настроек — измеряется, а не вычитывается из документации.** Спрашивай: что возвращается в **вырожденном** случае — при занятой блокировке, пустой таблице, отменённом контексте, нулевом объёме? - Отличим ли этот ответ от штатного? Прецедент, ради которого пункт существует: - контрольная точка журнала под занятой блокировкой возвращала `-1` вместо пары - чисел, и сравнение `-1 >= -1` читалось как «журнал разобран целиком» — 1492 - тика из 5502, найдено экспериментом на стенде, из документации не следовало. + Отличим ли этот ответ от штатного? Класс, ради которого пункт существует: + библиотека возвращает в вырожденном случае значение, которое код сравнивает + тем же оператором, что и штатное, — и отказ читается как успех. Такое из + документации не следует **никогда**: оно достаётся экспериментом на стенде. Проверяй на копии или во временном каталоге, рабочие данные не трогай. + Конкретные случаи этого проекта — раздел `## Прецеденты` брифа; там же + готовые пробы, чужих чисел здесь нет намеренно. +9. **Читает ли узел состояние, которое сам же меняет.** Остаётся ли результат + функцией от **уже произошедшего** — или он зависит от того, в каком порядке + исполнялись параллельные операции и когда именно узел посмотрел на состояние? + Ищи: решение принимается по прочитанному значению, которое к моменту записи + уже другое; счётчик или курсор, который узел одновременно читает и двигает; + ветка, выбираемая по «сколько сейчас лежит в таблице»; повторный прогон, + дающий другой результат на тех же входных событиях. Это тот же вопрос, что + рубрика задаёт дизайну до кода, — но задать его **на коде** больше некому: + рубрика на код не смотрит. ## Правило формулировки diff --git a/av-dev-pipeline/agents/review-reimpl.md b/av-dev-pipeline/agents/review-reimpl.md index c22dd54..ddd535d 100644 --- a/av-dev-pipeline/agents/review-reimpl.md +++ b/av-dev-pipeline/agents/review-reimpl.md @@ -15,6 +15,23 @@ color: purple `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md` (точный путь конвейер передаёт в задании). +## Что берёшь из брифа + +**`## Проект`** — граница домена: твоя версия должна лежать по ту же сторону, что +и существующая, иначе весь дифф по решениям окажется спором о scope. +**`## Инварианты`** — то, что твоя реализация обязана соблюсти (дословность +хранения, «сохранили — значит приняли» и подобное). **`## Прод и поток`** — объёмы +и представление данных: решение, разумное на сотне записей, неразумно на +миллионе. **`## Карта`** — где конвенции и где файл наблюдений на живых данных. + +**Брифа нет** — пиши свою версию по спеке и конвенциям, но: `critical` по +основанию «нарушен инвариант проекта» не присваивай (инвариантов ты не знаешь, а +именно они чаще всего объясняют чужое решение), объёмы не предполагай и в границы +покрытия дай строку «брифа проекта нет: инварианты и профиль нагрузки прогону +неизвестны, расхождения по этим основаниям не оценивались». Без брифа риск +конкретно этого прохода максимален: твоя версия проще, потому что не знает, чего +проект боится. + **Тебя запускают по триггеру, а не всегда.** Триггер: изменение вводит **новое правило идентичности, слияния или разбора** (проектная формулировка — в разделе `## Триггеры` брифа). Вне его твой счёт — самый большой в конвейере (он diff --git a/av-dev-pipeline/agents/review-rubric.md b/av-dev-pipeline/agents/review-rubric.md index 1190a8a..2b4992c 100644 --- a/av-dev-pipeline/agents/review-rubric.md +++ b/av-dev-pipeline/agents/review-rubric.md @@ -21,9 +21,14 @@ color: purple Это материал для требования «минимум три пункта специфичны для типа узла». - **`## Инварианты`** и **`## Проект`** — чтобы рубрика не противоречила тому, что проект защищает и чем он себя ограничил. +- **`## Прецеденты`** — классы дефектов, уже случавшихся здесь: свойство, + сформулированное по прецеденту, сильнее любого общего. -Разделов нет — порождай рубрику по общей практике и скажи в границах покрытия, -что специфика узла в проекте не описана: часть пунктов неизбежно окажется общими. +**Брифа или этих разделов нет** — порождай рубрику по общей практике, но +`critical` по основанию «нарушен инвариант проекта» (в фазе 2) не присваивай и +дай в границы покрытия строку: «брифа проекта нет: рода узлов, инварианты и +прецеденты неизвестны; требование «минимум три пункта специфичны для типа узла» +выполнено по общей практике, а не по этому проекту». ## Порядок фаз обязателен @@ -68,10 +73,13 @@ color: purple если вся рубрика — пересказ инвариантов из брифа, проход выродился в applicative; - **отдельным пунктом — узел, читающий состояние, которое сам же меняет.** - Спроси, остаётся ли результат функцией от того, что уже произошло, а не от - того, что произойдёт: правило родилось из дефекта, где запрос брал последнее - выведенное значение **вообще**, а не последнее предшествующее, и пересборка - переставала воспроизводить состояние. + Спроси, остаётся ли результат функцией от того, что **уже произошло**, а не от + того, в каком порядке исполнялись параллельные операции и когда именно узел + посмотрел на состояние. Класс: запрос берёт «последнее выведенное значение» + вообще вместо последнего предшествующего — и пересборка перестаёт + воспроизводить состояние. Случаи этого проекта — в разделе `## Прецеденты` + брифа. Тот же вопрос на **готовом коде** задаёт эксплуатационный проход + (вопрос 9); здесь он задаётся дизайну. Выведи рубрику **до** любых находок. Она — часть результата, даже если код окажется идеальным. diff --git a/av-dev-pipeline/agents/review-specs.md b/av-dev-pipeline/agents/review-specs.md index 0d33507..9579967 100644 --- a/av-dev-pipeline/agents/review-specs.md +++ b/av-dev-pipeline/agents/review-specs.md @@ -19,13 +19,18 @@ Development на OpenSpec). Оптика — требования, а не ст - **`## Инварианты`** — по ним проверяется, отражены ли в спеке задетые свойства, и по ним же присваивается severity. Цитируй пункт дословно, когда ссылаешься. -- **`## Карта`** — где актуальные спеки, где дельты, где архитектура и где лежат - наблюдения о реальном поведении внешних систем. +- **`## Карта`** — где актуальные спеки, где дельты, где архитектура и **где файл + наблюдений на живых данных**. Там же — **нарезка capability и миграционное + состояние спек**: по какому признаку проект режет capability и какие темы ещё + не переехали из документации в спеки. Без этого пункта непереехавшая тема + читается как пробел в спеке, и находка уходит в пустоту. - **`## Проект`** — граница домена: требование, переносящее понятие через неё, — находка в спеку, а не в код. -Брифа нет — сверяй только спеку с кодом, `critical` по основанию «нарушен -инвариант» не присваивай и скажи об этом в границах покрытия. +**Брифа нет** — сверяй только спеку с кодом, `critical` по основанию «нарушен +инвариант проекта» не присваивай и дай в границы покрытия строку: «брифа проекта +нет: инварианты, граница домена и состояние переноса capability в спеки +неизвестны; отражение инвариантов в спеке не проверялось». ## Источник требований diff --git a/av-dev-pipeline/agents/review-triage.md b/av-dev-pipeline/agents/review-triage.md index 1a48b6a..9612ea8 100644 --- a/av-dev-pipeline/agents/review-triage.md +++ b/av-dev-pipeline/agents/review-triage.md @@ -27,8 +27,19 @@ color: green Из брифа тебе нужны: **`## Инварианты`** (что делает находку `critical` и что делает её развилкой), **`## Прод и поток`** (что необратимо — от этого зависит -ранжирование), **`## Недоступно проверке`** (эта секция целиком уезжает в границы -покрытия), **`## Команды`** (что запускать запрещено). +ранжирование), **`## Прецеденты`** (готовые оракулы: находка того же класса, что +уже воспроизводился здесь, подтверждается ссылкой на прецедент), +**`## Типовые ложноположительные`** (единственный проектный вход в шаг 4), +**`## Недоступно проверке`** — оба подраздела, они целиком уезжают в границы +покрытия и **не сливаются в один список**, — **`## Команды`** (что запускать +запрещено). + +**Брифа нет** — работай по общим правилам, но: ни одну находку не поднимай до +`critical` по основанию «нарушен инвариант проекта» (сослаться не на что), +ранжируй по обратимости, выведенной из кода, и назови это предположением. Первой +строкой сводки — «прогон шёл без брифа проекта (<причина>)», и это же идёт в +границы покрытия. Одинаковая строка «брифа нет» без причины перестаёт читаться +на третьей задаче — причину сохраняй. ## Порядок. Не меняй его @@ -79,9 +90,16 @@ severity: Типовая вкусовщина в выводах generative-проходов: переименования без коллизии, перестановка функций, «лучше вынести в отдельный файл», предложения обобщить -работающий частный случай. Отдельный класс — предложение «нормализовать» то, что -инвариант проекта велит хранить дословно: это не просто вкусовщина, а нарушение -инварианта, и выбрасывать его надо с пометкой почему. +работающий частный случай. + +**Проектный вход сюда один — раздел `## Типовые ложноположительные` брифа.** +Там перечислены находки, которые в этом проекте выглядят убедительно и всегда +неверны: они выбрасываются со ссылкой на пункт и с пометкой почему, а не +«смягчаются». Классический обитатель раздела — предложение «нормализовать» то, +что инвариант велит хранить дословно: это не просто вкусовщина, а находка, +предлагающая нарушить инвариант. Раздела нет или он пуст — скажи об этом строкой +в границах покрытия: отсев шёл по общим критериям, проектных ложноположительных +ты не знал. ### 5. Ранжирование по ущербу × вероятности @@ -121,8 +139,7 @@ severity: Сводка отчёта называет **каждый проход профиля** и его исход: отработал (сколько находок) / не запускался (почему). Сверь список запущенного с составом профиля сам, а не доверяй тому, что тебе подали: пропуск прохода **не отличим от прохода -без находок**, и однажды это стоило семи находок и отдельной задачи на их -дозакрытие. +без находок**, и назвать его больше некому. Расхождение состава с профилем — это находка о прогоне, и она идёт в сводку первой строкой, а не растворяется в границах покрытия. @@ -135,12 +152,16 @@ severity: - какие **не** запускались и почему (профиль, бюджет, недоступный инструмент, остановленный прогон); - что каждый запущенный проход **не мог проверить в принципе** — из его charter'а; -- **что осталось целиком на человеке** — раздел `## Недоступно проверке` брифа - целиком, плюс: история инцидентов, поведение под реальным потоком, поведение - внешних систем в их версиях, завязка потребителей на текущее поведение и вопрос - «а нужна ли эта функциональность вообще»; -- если брифа не было — строку об этом: инварианты, модель угроз и профиль - нагрузки прогону были неизвестны. +- **что осталось целиком на человеке** — раздел `## Недоступно проверке` брифа, + **двумя отдельными списками**: «не проверит ни один проход» и «перестали + проверять сознательно». Слитый список бесполезен: при следующем промахе первый + вопрос — «не тот ли это класс, который мы перестали проверять», и ответить на + него можно только если второй список виден отдельно. Плюс общее: история + инцидентов, поведение под реальным потоком, поведение внешних систем в их + версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта + функциональность вообще»; +- если брифа не было — строку об этом **с причиной**: инварианты, модель угроз и + профиль нагрузки прогону были неизвестны, потому что <причина>. Формулировка «критичных проблем не обнаружено» **запрещена** без этой секции: она потребляет ощущение проверенности, ничего не гарантируя, и это хуже, чем diff --git a/av-dev-pipeline/skills/project-brief/SKILL.md b/av-dev-pipeline/skills/project-brief/SKILL.md new file mode 100644 index 0000000..793c53d --- /dev/null +++ b/av-dev-pipeline/skills/project-brief/SKILL.md @@ -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` по основанию «нарушен инвариант проекта». + +Во всех остальных случаях бриф заводится. «Задача маленькая, брифа не надо» — +не основание: бриф заводится один раз на проект, а деградированный режим платит +на каждой задаче. diff --git a/av-dev-pipeline/skills/review-pipeline/SKILL.md b/av-dev-pipeline/skills/review-pipeline/SKILL.md index 697aaa9..0238c87 100644 --- a/av-dev-pipeline/skills/review-pipeline/SKILL.md +++ b/av-dev-pipeline/skills/review-pipeline/SKILL.md @@ -29,28 +29,61 @@ description: Конвейер ревью изменения — детермин обнаружено» потребляет ощущение проверенности, ничего не гарантируя. Секция границ покрытия обязательна и не сокращается — в том числе в докладе человеку. +## Предпосылки + +Конвейер опирается на внешнюю обвязку и без неё работает не целиком. Проверь это +один раз, при установке плагина в проект: + +- **OpenSpec и скиллы `opsx:*`.** Профиль `design`, проход `review-specs` и + вызывающий пайплайн задачи завязаны на дельта-спеки + (`openspec/changes//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/brief-template.md](references/brief-template.md) — образец +объёмы, прецеденты и модель угроз конвейер **не знает** — он читает их в брифе +проекта: [references/project-brief.md](references/project-brief.md) описывает +контракт, [references/brief-template.md](references/brief-template.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) (в установленном плагине — `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/`); @@ -170,14 +203,14 @@ description: Конвейер ревью изменения — детермин Почему умолчание именно такое: - **Замеры.** Проходы `adversary` и `ops` доказывают находки числами: время - удержания блокировки, пик кучи, рост файлов журнала, длительность транзакции. - Два меряющих прохода на одной машине соревнуются за диск, CPU и за саму СУБД и - выдают числа, которые не воспроизведутся. Это не гипотеза: находки, ради - которых правило записано, опираются ровно на такие замеры (5.019 с удержания - блокировки при таймауте 5000 мс, пик 768 МиБ на теле 40 МиБ, 7 МБ/с роста - журнала, 1492 тика из 5502). Число, снятое под конкурентную нагрузку от - соседнего прохода, — это находка с испорченным оракулом, а её опровержение - стоит дороже всего выигрыша от параллельности. + удержания блокировки против её таймаута, пик кучи против размера тела, темп + роста файлов журнала, длительность транзакции. Два меряющих прохода на одной + машине соревнуются за диск, CPU и за саму СУБД и выдают числа, которые не + воспроизведутся. Это не гипотеза: правило выведено из находок, целиком + державшихся на таких замерах, — у каждого проекта они свои и лежат в разделе + `## Прецеденты` его брифа. Число, снятое под конкурентную нагрузку от соседнего + прохода, — это находка с испорченным оракулом, а её опровержение стоит дороже + всего выигрыша от параллельности. - **Машина одна.** Рядом идёт задача, поднят сервис, гоняется гейт или дорогая проверка проекта. - **Ранний выход** возможен только при последовательном прогоне (см. ниже). @@ -205,9 +238,9 @@ description: Конвейер ревью изменения — детермин нулевой стадии** — а не «доезжает» остатком по старому коду; - незапущенные проходы идут в границы покрытия строкой «не запускался: прогон остановлен на <проход> из-за <находка>», поимённо; -- триаж запускается только на полном прогоне. Отчёт триажа по половине проходов — - ровно тот случай, который уже стоил семи находок: он выглядит полным, потому - что агрегирует всё, что ему подали. +- триаж запускается только на полном прогоне. Отчёт триажа по половине проходов + выглядит полным, потому что агрегирует всё, что ему подали, — это тот же + молчащий пропуск, что и в разделе «Профили». Ранний выход по находке, которая чинится в пределах существующей формы (`Действие: инлайн`), **не делается**: дешевле дособрать все находки и починить @@ -246,8 +279,9 @@ description: Конвейер ревью изменения — детермин - `review-specs` — критерий взят из **дельта-спек предлагаемого изменения**, а не из proposal, сообщения коммита или описания задачи. Сверка двунаправленная; направление `code → spec` важнее. -- `review-code` — критерий взят из файла конвенций проекта (раздел `## Карта` - брифа), и только та его часть, которая **не выражается правилом**: +- `review-code` — критерий взят из конвенций проекта: файла или каталога файлов, + путь — раздел `## Карта` брифа. Берётся только та их часть, которая **не + выражается правилом**: механизируемое уже проверила стадия 0. Что именно механизировано, тот же раздел брифа перечисляет — повторять это проходом вредно. @@ -322,7 +356,8 @@ Recall обоих равен длине их источника — это и е ## Профиль `design` — до кода -Запускается на шаге ревью спек (шаг 4 скилла `task-pipeline`), когда change уже +Запускается на шаге ревью спек (шаг 4 скилла `av-dev-pipeline:task-pipeline`), +когда change уже имеет `proposal.md` и дельта-спеки, но кода ещё нет. Состав: 1. `review-specs` в режиме «дизайн ДО кода»; @@ -359,9 +394,11 @@ Recall обоих равен длине их источника — это и е держит вопросы (это знает вызвавший пайплайн, а не конвейер). Оркестратор не останавливается: он урезает изменение до остатка и доводит его. - Находка не для этого мерджа, но реальная (отложенный `major`, развилка, - решённая «потом») — не теряется: заводится задачей средствами проекта, с - оракулом и провенансом в теле. Мелочь класса `nit` — пачкой, а не записью на - находку. + решённая «потом»), — не теряется, но **и не заводится здесь**. Конвейер отдаёт + её **списком урожая** в отчёте: формулировка, оракул, провенанс (какой проход, + какой change). Заведение задач принадлежит тому, кто ведёт задачи проекта, — + у него свой формат, своя нарезка и свои правила дублей. Мелочь класса `nit` + идёт в урожай одной пачкой, а не записью на находку. - `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/brief-template.md](references/brief-template.md) — шаблон брифа. - [references/finding-contract.md](references/finding-contract.md) — контракт находок. diff --git a/av-dev-pipeline/skills/review-pipeline/references/brief-template.md b/av-dev-pipeline/skills/review-pipeline/references/brief-template.md index bde2016..e94ceaf 100644 --- a/av-dev-pipeline/skills/review-pipeline/references/brief-template.md +++ b/av-dev-pipeline/skills/review-pipeline/references/brief-template.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 проект обычно не пишет — тогда она выводится по +обратимости и помечается: «по умолчанию `critical` (выведена по обратимости)».* - **Точка сохраняется дословно.** Незнакомое поле не отбрасывается, число не округляется при записи. Нарушение — необратимая потеря: сырой архив живёт @@ -70,12 +72,26 @@ - Карта проекта для архитектурного прохода: `task review:context > tmp/review-context.md` - Поднять изменение вживую: `task restart`, логи — `task logs` - Тесты и линт: `task test`, `task lint` -- Дорогое, вручную: `task verify:archive` (минута, живые данные) +- **Дорогое вне гейта, с адресатом:** `task verify:archive` (минута, живые + данные). **Кто и когда обязан:** пайплайн задачи — после любого изменения + разбора входного формата или правила слияния, до архивации change; вручную — + человек перед выкладкой. Не прогонялась — строка в границы покрытия, а не + молчание. - **Запускать запрещено:** ничего, что пишет в `./data`, в рабочую БД и в боевой каталог архива. Замеры — только на копиях в `./tmp`. ## Прод и поток +*Первая строка — главный вопрос эксплуатации этого проекта.* + +> **Главный вопрос:** поток идёт непрерывно и молча, отправитель об отказе не +> узнает и не повторит — значит, дороже всего тихо потерянная доставка, а не +> упавший сервис. + +> *(В сервисе, который сам опрашивает чужих демонов, первая строка была бы +> противоположной: «главный вопрос — что происходит, когда внешний сервис +> отвечает медленно, а не когда он упал».)* + - **Где:** один статический бинарь в контейнере на домашнем сервере, перед ним обратный прокси с TLS, SQLite на диске. Ни оркестратора, ни реплик, ни дежурной смены. @@ -84,11 +100,17 @@ параллельной записью; приложение-источник на телефоне — молча перестаёт слать. *(В другом проекте здесь были бы качалка, медиасервер, LLM и база метаданных, и каждая — со своим «отвечает медленно», а не только «упала».)* + *(Если зависимостей нет — так и пишут: «внешних зависимостей нет, смотри на + диск и на СУБД». Пустой пункт называется пустым.)* - **Кто заметит отказ:** один пользователь-владелец, в лучшем случае вечером, а скорее не заметит вовсе. - **Характер потока:** телефон шлёт непрерывно и молча; обратной связи у отправителя нет, об отказах он не сообщает, расписание плавает. Тихо сломавшаяся доставка — главный эксплуатационный риск. +- **Представление данных и настройки хранилища:** запись — сжатый BLOB, читается + и пересобирается целиком на каждой операции (`internal/store`); журнал СУБД — + WAL; таймаут занятости — 5000 мс (`config.example.toml`); лимит тела приёма — + 64 МБ; ретеншен сырого архива — 14 дней. - **Числа (с провенансом):** нижний слой — порядка 135 тыс. точек в сутки (замер, `docs/local-research.md`); тела доходили до 42 МБ (там же); запись — 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//spec.md` - Дельта-спеки изменения: `openspec/changes//specs/*/spec.md` -- Конвенции прозой: `docs/conventions.md`. Механизировано и потому **не - проверяется проходом по конвенциям**: форма логов, `fmt.Print*`/`os.Getenv`/ - `time.Now` мимо единых точек, сравнение ошибок, сторонние пакеты ошибок — - всё это правила в `.golangci.yml`. +- **Нарезка capability и что из неё переехало в спеки:** режем по домену + (`ingest`, `storage`, `read-api`, `mcp`), а не по транспорту. В актуальные + спеки перенесены `ingest` и `storage`; `read-api` описан только в + `docs/architecture.md`, `mcp` — пока только в коде. Пробел в спеке по этим двум + темам — не находка, а известное состояние. +- Конвенции прозой: `docs/conventions.md` *(в другом проекте это каталог из + нескольких файлов — тогда перечисляют все: + `docs/conventions/{logging,errors,config,database,web-ui}.md`)*. Механизировано + и потому **не проверяется проходом по конвенциям**: форма логов, + `fmt.Print*`/`os.Getenv`/`time.Now` мимо единых точек, сравнение ошибок, + сторонние пакеты ошибок — всё это правила в `.golangci.yml`. - Архитектура и решения: `docs/architecture.md` +- **Наблюдения на живых данных:** `docs/local-research.md` — что реально шлёт + источник и чем это расходится с его документацией. *(Не ведём — так и пишут: + «наблюдений на живых данных не ведём».)* - Журнал проскочивших дефектов: `docs/review-journal.md` - **Единые точки:** идентификаторы — `internal/ident`; время — `store.Now()`; разбор дат входного формата — один парсер в `internal/parse`; маппинг доменной @@ -159,6 +206,53 @@ - **Клиент внешнего сервиса** — таймаут, протяжка `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/*`, @@ -170,6 +264,10 @@ ## Недоступно проверке +### Не проверит ни один проход + +*Принципиальные границы. По факту промаха не пересматриваются.* + - Поведение внешнего приложения-источника на следующем его обновлении. - Что реально лежит в системе-источнике: сверить можно только ручным экспортом, а он делается раз в 2–3 месяца. @@ -177,3 +275,15 @@ нагрузки. - Завязка внешних потребителей на текущую форму ответа. - Суждение «этой функциональности не должно существовать». + +### Перестали проверять сознательно + +*Что, когда, почему и где записано. Пересматривается первым, как только что-то +проскочило. Пусто — так и пишут: «сознательно ничего не отключали».* + +- **Поимённая сверка со стайлгайдами языка** — с 2026-05, вместе с упразднением + прохода про идиоматичность (журнал ревью, запись 2026-05-04). Класс обратимый: + портит форму кода, не данные. +- **Правило линтера про длину функции** — снято 2026-06-18: ложных срабатываний + больше трети (журнал, там же). Вернуть, если проскочит дефект «функция делает + три вещи». diff --git a/av-dev-pipeline/skills/review-pipeline/references/project-brief.md b/av-dev-pipeline/skills/review-pipeline/references/project-brief.md index 52c0c6e..b23cb8b 100644 --- a/av-dev-pipeline/skills/review-pipeline/references/project-brief.md +++ b/av-dev-pipeline/skills/review-pipeline/references/project-brief.md @@ -18,21 +18,28 @@ передавать каждому проходу; 2. `docs/review-brief.md`; 3. `.claude/review-brief.md`; -4. брифа нет — **деградированный режим** (см. ниже). +4. брифа нет ни по одному пути — **он заводится**, скиллом + `av-dev-pipeline:project-brief`, и прогон продолжается по заведённому. Разрешает путь конвейер, один раз, и дальше передаёт готовым. Агент, получивший путь в задании, сам ничего не ищет. -## Деградированный режим +## Деградированный режим — исход, а не умолчание -Брифа нет — проходы работают, но их recall падает предсказуемым образом, и это -**обязано быть названо**, а не сглажено. Каждый проход без брифа: +Он включается ровно тогда, когда бриф **завести не удалось**: репозиторий +доступен только на чтение, человек прямо запретил, инварианты вывести неоткуда. +Во всех остальных случаях брифа быть обязано. + +Каждый проход в этом режиме: - не присваивает `critical` по основанию «нарушен инвариант проекта» — инвариантов он не знает; - не оперирует числами объёма и потока — формулирует условиями; -- пишет в границы покрытия строку: «брифа проекта нет: инварианты, модель угроз и - профиль нагрузки неизвестны; находки этих классов не искались». +- пишет в границы покрытия строку: «брифа проекта нет (<причина>): инварианты, + модель угроз и профиль нагрузки неизвестны; находки этих классов не искались». + +Причина обязательна: без неё строка неотличима от «мы просто не стали», и +одинаковая строка в каждом отчёте перестаёт читаться на третьей задаче. Триаж сводит эти строки в одну и выносит в финальный отчёт. Отсутствие брифа — дыра покрытия, а не нейтральное умолчание. @@ -67,6 +74,14 @@ Markdown. Разделы — заголовки второго уровня с * Это единственный раздел, который **цитируется формулировкой**, а не пересказывается ссылкой: по нему присваивается severity, и пересказ здесь стоит неверной оценки. +**Оговорка про severity, потому что она единственная не цитируется.** Проекты +почти никогда не пишут severity рядом с инвариантом — её приходится выводить, и +правило вывода одно: **по обратимости последствия**. Необратимо и молча — +`critical`; лечится повтором, видно сразу — ниже. Выведенная severity помечается +словом «выведена по обратимости», а не выдаётся за решение проекта: проход ставит +по ней `critical`, и он вправе знать, чьё это суждение. Лучший исход — дописать +severity туда, откуда цитируется формулировка, и тогда пометка снимается. + Читают: `specs` (режим 1 — отражены ли задетые инварианты в спеке), `code`, `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-команда, файловое хранилище), и по 3–5 **специфичных для рода** проверяемых свойств к каждому. +**Рода, а не инвентарь того, что сейчас лежит в пакетах.** Список пишется по +природе проекта: род, который проект уже задумал, но ещё не написал, включать +полезно (рубрика на него понадобится ровно на той задаче, где его заводят); а +род, случайно оказавшийся в коде в одном экземпляре, — нет. Иначе раздел +протухает на каждой задаче и требует пересмотра, которого никто не делает. + Читает: `rubric`. Без раздела рубрика выродится в общие слова и повторит конвенции — то есть станет applicative-проходом, ради отсутствия которого она и существует. +### `## Прецеденты` — обязателен, хотя бы строкой «пусто» + +**Воспроизведённые случаи этого проекта, с оракулом.** Не «здесь бывают гонки», а +«такой дефект здесь уже был, вот чем он воспроизведён»: что оказалось не так, +каким экспериментом или тестом это показано, какими числами, где это записано. + +Каждый пункт — четыре вещи: + +- **класс дефекта** — так, чтобы проход узнал его в другом месте; +- **как проявился** — симптом, который увидел человек; +- **чем воспроизведён** — команда, тест, стенд, замер. Без этого пункт + превращается в байку. **Регрессионный тест, написанный вместе с починкой, + годится** наравне с независимым экспериментом: он исполняемый и падает на + старом коде, а это всё, что требуется от оракула. Слабее он ровно в одном — + сформулирован уже зная ответ; это отмечается словом, а не служит поводом + выбросить пункт; +- **чем закончилось** — починка, правило линтера, пункт брифа, «ничего». + +Зачем раздел существует. Прецедент — самая сильная опора, какая у прохода вообще +бывает: он проектный, воспроизводимый и уже однажды оказался правдой. Пока слота +не было, прецеденты вмерзали в charter'ы проходов — то есть каждый проект читал +про чужую контрольную точку в чужой СУБД и искал её у себя. Charter описывает +**форму класса**, бриф — **случай**. + +Источники: журнал проскочивших дефектов, архивные отчёты триажа, `git log` по +починкам. Прецедентов нет — так и напиши: «прецедентов не накоплено», и это +честнее пустого раздела. + +Читают: все проходы — свой класс; `triage` — как готовый оракул. + +### `## Типовые ложноположительные` — необязателен, но без него отсев слепой + +Находки, которые в **этом** проекте выглядят убедительно и всегда неверны. Это +единственный проектный вход в шаг триажа «отсев вкусовщины»: общие критерии +(«не меняет поведения, не влияет на стоимость следующего изменения, не нарушает +записанного») ловят вкусовщину, но не ловят находку, которая нарушает общее +правило **осознанно**. + +Каждый пункт — формулировка находки, какой её выдаёт проход, плюс одна строка +«почему здесь это не дефект». Типичные обитатели: «дословное хранение надо +нормализовать» там, где дословность — инвариант; «повтор надо сделать +идемпотентным» там, где повтор невозможен по построению; «это надо вынести в +конфиг» там, где значение задано внешним протоколом. + +Читает: `triage`. + +### `## Вопросы к проходам` — необязателен + +Проектные вопросы, адресованные **поимённо** конкретному проходу. Главный их +источник — журнал проскочивших дефектов: запись «проход не мог этого знать» чаще +всего лечится фактом в другом разделе, но иногда лечится не фактом, а +**вопросом**: «`ops`, спроси про поведение при откате бинаря поверх новой схемы», +«`adversary`, проверь имена внутри архива». Такие вопросы живут здесь, а не в +charter'е: charter общий для всех проектов, а вопрос выведен из промаха в этом. + +**Журнал — не единственный источник, а лучший.** У молодого проекта журнал пуст, +и слот тогда заполняется из того, что есть: незакрытые находки аудита, известное +расхождение кода с документацией, место, где решение принято «пока так». Правило +одно и не смягчается — **у каждого вопроса указан провенанс**, и по нему видно, +насколько он выстрадан: «журнал, запись такая-то» весит больше, чем «открытая +находка аудита». + +Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Проход, увидев свой блок, задаёт +эти вопросы **дополнительно** к обязательным — и отвечает на них в выводе явно. + +Читают: проходы, названные поимённо. + ### `## Триггеры` — необязателен Проектная конкретизация правила выбора профиля: какие пути и контракты означают @@ -175,16 +339,35 @@ Markdown. Разделы — заголовки второго уровня с * Читают: скилл конвейера, пайплайн задачи. -### `## Недоступно проверке` — обязателен +### `## Недоступно проверке` — обязателен, и делится на два подраздела -Что не проверит ни один проход и почему: поведение внешних систем и их будущих -версий, реальный профиль нагрузки, соответствие сохранённого действительности, -завязка внешних потребителей на текущую форму, суждение «а нужна ли эта -функциональность». +Раздел целиком уезжает в границы покрытия финального отчёта — он существует ровно +затем, чтобы «критичных проблем не обнаружено» никогда не читалось как «проверено +всё». Но внутри лежат **два разных класса**, и смешивать их нельзя: при следующем +промахе один пересматривается, другой нет. -Этот раздел целиком уезжает в границы покрытия финального отчёта. Он существует -ровно затем, чтобы «критичных проблем не обнаружено» никогда не читалось как -«проверено всё». +#### `### Не проверит ни один проход` + +Принципиально недоступное: поведение внешних систем и их будущих версий, реальный +профиль нагрузки, соответствие сохранённого действительности, завязка внешних +потребителей на текущую форму, суждение «а нужна ли эта функциональность». + +Этот список не пересматривается по факту промаха: дефект отсюда — не ошибка +конвейера, а его честная граница. Он меняется только когда меняется сам проект +(появился стенд, появился второй потребитель, появилась телеметрия). + +#### `### Перестали проверять сознательно` + +Решения о сужении: перестали звать проход, понизили профиль правилом, сузили +класс проверяемого, сняли правило линтера как шумное. Каждый пункт — **что +перестали, когда и почему**, со ссылкой на запись журнала ревью. + +Этот список **пересматривается первым**, как только что-то проскочило: первый +вопрос по любому пропущенному дефекту — «не тот ли это класс, который мы перестали +проверять». Пункт, из-за которого дефект проскочил, либо возвращается, либо +получает строку «оставляем, цена поимки выше цены дефекта» с датой. + +Оба подраздела обязательны; пустой называется пустым. Читает: `triage`; каждый проход — свою часть. @@ -194,15 +377,37 @@ Markdown. Разделы — заголовки второго уровня с * или в архитектуре, попадает сюда ссылкой и одной строкой сути. Два дома для одного факта разъезжаются, и разошедшийся бриф хуже отсутствующего: он выглядит актуальным. Исключение одно — раздел инвариантов, он цитируется. -- **Числа — с провенансом.** «Тела доходили до 42 МБ (замер, ссылка)». Число без - источника проход не имеет права использовать как утверждение. +- **Числа — с провенансом, и провенанс проверяется переходом по ссылке.** «Тела + доходили до 42 МБ (замер, ссылка)». Число без источника проход не имеет права + использовать как утверждение. Отдельный и более коварный случай — **число, чей + источник по ссылке не подтверждается**: в документе по ссылке другое число, или + его там нет вовсе. Такое число не выбрасывается и не переписывается по догадке: + оно остаётся с пометкой «расходится с источником: там <что нашли>», а проход + обязан читать его как условие, а не как замер. Молча подставить «правильное» + число хуже всего — расхождение перестанет быть видно, а причина его останется. +- **Пустой пункт называется пустым.** «Внешних зависимостей нет — смотри на диск + и на СУБД», «прецедентов не накоплено», «наблюдений на живых данных не ведём», + «измеренных чисел нагрузки нет», «сознательно ничего не отключали». Отсутствие + строки читается проходом как «здесь не написали», и он тратит обязательный + вопрос впустую либо заполняет пробел выдумкой. Прямое «пусто» стоит одной + строки и экономит проход целиком. +- **Назначенное помечается назначенным.** Бриф отражает решения проекта, но + местами оказывается **первым** местом, где решение вообще записано: severity у + инварианта, адресат дорогой проверки, периметр, восстановленный из конфига. + Так можно — молчать хуже, — но пометка обязательна («выведена по обратимости», + «назначен брифом, владельцем не подтверждён»). Проход имеет право знать, чьё + это суждение, а владелец — увидеть, что за него что-то решили. - **Что вне модели — называется явно.** Это относится и к угрозам, и к нагрузке, - и к классам находок, которые проект сознательно перестал проверять. + и к классам находок, которые проект сознательно перестал проверять (последние — + в свой подраздел `## Недоступно проверке`, а не вперемешку с принципиальным). - **Бриф подчиняется промоуту.** Свойство, ставшее правилом линтера, из брифа вычёркивается — как и из конвенций, и из charter'ов (см. [promote.md](promote.md), шаг 3). - **Когда обновлять:** сменился гейт; появился новый контур, зависимость или - источник входа; журнал ревью получил запись вида «проход не мог этого знать». - Планового пересмотра нет. -- **Бриф ведёт проект**, а не плагин. Плагин его только читает и никогда не - правит. + источник входа; журнал ревью получил запись вида «проход не мог этого знать»; + воспроизвели дефект — он идёт в `## Прецеденты`. Планового пересмотра нет. +- **Заводится и обновляется шагом, а не руками** — скиллом + `av-dev-pipeline:project-brief`. Он же вызывается автоматически, когда конвейер + или пайплайн задачи не нашли брифа ни по одному пути. +- **Бриф ведёт проект**, а не плагин. Файл живёт в репозитории проекта; плагин + его читает и заводит по шаблону, но не хранит у себя и не подменяет. diff --git a/av-dev-pipeline/skills/review-pipeline/references/promote.md b/av-dev-pipeline/skills/review-pipeline/references/promote.md index 1b32069..ef233c8 100644 --- a/av-dev-pipeline/skills/review-pipeline/references/promote.md +++ b/av-dev-pipeline/skills/review-pipeline/references/promote.md @@ -23,7 +23,8 @@ калибровки: проход, чьи находки регулярно доезжают до конвенции, оправдан; проход, чьи находки не доезжают никогда, — кандидат на `drop` (см. [calibration.md](calibration.md)). -- Место записи — файл конвенций проекта (путь — в разделе `## Карта` брифа). Если +- Место записи — конвенции проекта, файл или нужный файл каталога (путь — в + разделе `## Карта` брифа). Если тема относится к поведению системы, а не к тому, как мы пишем код, — это не конвенция, а требование: заводится дельта-спека обычным путём. diff --git a/av-dev-pipeline/skills/review-pipeline/references/review-journal.md b/av-dev-pipeline/skills/review-pipeline/references/review-journal.md index 273db5b..96fcf0a 100644 --- a/av-dev-pipeline/skills/review-pipeline/references/review-journal.md +++ b/av-dev-pipeline/skills/review-pipeline/references/review-journal.md @@ -19,6 +19,11 @@ а потому, что здесь лежит цена: если что-то теперь проскочит, первый вопрос — «не тот ли это класс, который мы перестали проверять». +Каждое такое решение обязано получить **строку в брифе** — в подразделе +`### Перестали проверять сознательно` раздела `## Недоступно проверке`. Журнал +хранит «почему тогда так решили», бриф — то, во что смотрит каждый прогон. +Решение, оставшееся только в журнале, в границы покрытия не доедет. + ## Форма записи ``` @@ -41,7 +46,11 @@ - **в бриф проекта** — если проход не мог знать факта: объём, характер потока, что здесь необратимо, какой шаг гейта красит безусловно. Самый частый адрес и - самый дешёвый. + самый дешёвый. Сюда же — **воспроизведённый случай** (раздел `## Прецеденты`: + класс, симптом, чем воспроизведён, чем закончилось) и **вопрос конкретному + проходу**, если промах лечится не фактом, а заданным вопросом (раздел + `## Вопросы к проходам`). Прежде чем править charter, проверь, не хватит ли + этих двух разделов: charter общий для всех проектов, бриф — про этот. - **в конвенции или в правило линтера** — если свойство выражается детерминированно (процедура — [promote.md](promote.md)). - **в charter прохода** — если сломан **метод**, а не знание. Правка charter'а diff --git a/av-dev-pipeline/skills/task-batch/SKILL.md b/av-dev-pipeline/skills/task-batch/SKILL.md index fdfb956..2014bea 100644 --- a/av-dev-pipeline/skills/task-batch/SKILL.md +++ b/av-dev-pipeline/skills/task-batch/SKILL.md @@ -6,9 +6,10 @@ description: Проводит несколько задач разом — пл # Батч задач Оркестратор **набора** задач. Планирует порядок, раскидывает задачи по -изолированным worktree, каждую проводит через полный цикл `task-pipeline`, затем -сводит в основную ветку линейной историей и делает финальную сверку. Тонкая -обёртка над `task-pipeline` — не переизобретай её шаги, вызывай как есть. +изолированным worktree, каждую проводит через полный цикл +`av-dev-pipeline:task-pipeline`, затем сводит в основную ветку линейной историей +и делает финальную сверку. Тонкая обёртка над пайплайном задачи — не +переизобретай её шаги, вызывай как есть. Работай **максимально автономно**, по тому же принципу, что и одиночный пайплайн: вопрос, который решать не тебе, записывается и не останавливает поток; спрашиваем @@ -16,8 +17,24 @@ description: Проводит несколько задач разом — пл рабочих данных). Механику — планирование, 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`): -из него берутся команда гейта, инварианты и раскладка нумерованных артефактов. +из него берутся **основная ветка** (раздел `## Карта` — она подставляется в +каждую команду git ниже), команда гейта, инварианты и раскладка нумерованных +артефактов. Брифа нет — заведи его Skill'ом +**`av-dev-pipeline:project-brief`** один раз, до первой волны: иначе каждая +задача батча заплатит деградированным ревью, а имя основной ветки придётся +угадывать. ## Границы @@ -27,6 +44,9 @@ description: Проводит несколько задач разом — пл - **Батч не владеет спринтом и целями.** Он сообщает исход по каждой задаче в тех же трёх словах, что и `task-pipeline`: сделана / не доведена / оказалась крупнее задачи. +- **Записей учёта батч не трогает и задач не закрывает** — как и одиночный + пайплайн. Закрытие — акт владельца спринта после приёмки. Урожай ревью батч + отдаёт списком, а задачи из него заводит тот, кто ведёт задачи проекта. ## Ключевое отличие от одиночного пайплайна @@ -62,25 +82,53 @@ fast-forward. Ветки после вливания удаляются. - **затронутые capability** — по её описанию и по каталогу актуальных спек (`openspec/specs/`); - **жёсткие зависимости**: задача B строится на результате A → A строго раньше B; +- **замеряющая задача** — та, чьё ревью будет доказывать находки **числами**, и + потому она гонится в волне **одна** (обоснование — ниже, в шаге 4). Решается + здесь, на планировании, а не во время прогона: состав волны определяется + сейчас, а профиль ревью сабагент выберет только внутри задачи, и ключевать + волну на ещё не сделанный выбор нельзя. Триггеры — по фактам о задаче, каждый + сам по себе достаточен: + - трогает схему хранилища, миграцию, формат на диске или объём хранимого; + - трогает конкурентность: транзакции, блокировки, фоновые циклы, общее + состояние; + - трогает размер тела, буфер, память, сжатие, ретеншен, темп потока; + - её тема названа в разделах `## Прод и поток` или `## Прецеденты` брифа как + место, где уже мерили или уже ломалось. + + Ни один триггер не сработал — задача не замеряющая, даже если её ревью + окажется `deep`. `deep` про глубину проверки, замеряющая — про соревнование за + железо; это разные вопросы, и совпадают они не всегда; - **нумерованные артефакты — номера раздаёт оркестратор заранее.** Если проект нумерует миграции или подобные файлы (путь — из брифа), посмотри последний - номер и **раздай номера тем задачам, которые, вероятно, их добавят**, до + номер и **раздай номера всем задачам, которые, вероятно, их добавят**, до запуска. Номер уходит в charter сабагента, и он берёт назначенный, а не - «следующий свободный». Так такие задачи можно гнать одновременно: файлы не - столкнутся, а описание схемы правят разные строки — конфликт мелкий и решается - на интеграции; + «следующий свободный». + + **Это отдельная механика от правила волны, и она ему не служит** — их раньше + путали, и они тянули в разные стороны. Правило волны отвечает на вопрос «кто с + кем гонится одновременно», предраздача — на вопрос «какой номер берёт задача». + Раздача нужна там, где **две задачи одной под-пачки** добавляют нумерованный + артефакт: каждая считает «следующий свободный» по основной ветке, которая ещё + не видела соседку, и обе берут один номер. Миграции под это почти не попадают — + миграция и так триггер замеряющей задачи, а замеряющая идёт одна; но + нумерованные артефакты бывают не только миграциями. Поэтому номера раздаются + **всем** задачам с таким артефактом, независимо от того, в какой волне они + окажутся: раздача ничего не стоит, а её отсутствие ловится только конфликтом на + интеграции. Между волнами проблемы нет — ветка следующей волны берётся от + вершины, уже включающей предыдущие; - **жёстко сериализуем** (не гоняем одновременно) настоящие пересечения: - **одна capability на несколько задач** — две задачи, правящие одну спеку (тем более одно и то же `### Requirement`), дают не текстовый, а **семантический** конфликт при архивации; сериализуем по смыслу, а не только по файлам; - пересечение по одним и тем же исходникам; -- **мягкие конфликты** сериализовать не надо: индекс беклога (каждая задача - убирает свою строку) и спеки разных capability — разные строки и файлы, - сливаются сами. +- **мягкие конфликты** сериализовать не надо: файлы-перечни, где каждая задача + правит **свою** строку (индексы, оглавления, списки записей), и спеки разных + capability — разные строки и файлы, сливаются сами. Собери план: **волны** параллельно-безопасных задач плюс сериализованный хвост -конфликтоопасных, с учётом зависимостей. Покажи план короткой репликой и иди -дальше. +конфликтоопасных, с учётом зависимостей; замеряющие задачи стоят в плане +отдельными волнами по одной. Покажи план короткой репликой — назвав, какие +задачи признаны замеряющими и по какому триггеру, — и иди дальше. ### 3. Свежая база @@ -91,16 +139,17 @@ fast-forward. Ветки после вливания удаляются. ### 4. Прогнать волны **Потолок параллелизма — 2–3 задачи одновременно.** Каждая задача тянет полный -`task-pipeline` с вложенным ревью и гейтом, поэтому больше трёх разом душат -машину и провоцируют гонки. Волну шире трёх бей на под-пачки по ≤3. +цикл пайплайна с вложенным ревью и гейтом, поэтому больше трёх разом душат +машину и провоцируют гонки. Волну шире трёх бей на под-пачки по ≤3 и **гони +под-пачки последовательно**: следующая стартует, когда предыдущая вернула отчёты. +Иначе потолок обходится тривиально — шесть задач, запущенных «двумя под-пачками» +в одном сообщении, это шесть задач разом. **Волна из одной задачи — не вырожденный случай, а обязательный.** Задача, -ревью которой будет доказывать находки **числами** (профиль `deep`, где работают -`adversary` и `ops`: удержание блокировки, пик памяти, рост файлов, длительность -операции), гонится в волне одна. Соседний прогон на той же машине портит эти -числа, а находка с испорченным оракулом хуже отсутствующей — она выглядит -доказанной. Если задача всё же пошла в общей волне, её отчёт обязан нести строку -в границах покрытия: замеры сняты под соседней нагрузкой. +признанная на шаге 2 **замеряющей**, гонится в волне одна: соседний прогон на той +же машине портит числа, а находка с испорченным оракулом хуже отсутствующей — она +выглядит доказанной. Если замеряющая задача всё же пошла в общей волне, её отчёт +обязан нести строку в границах покрытия: замеры сняты под соседней нагрузкой. Для каждой задачи в под-пачке: @@ -111,19 +160,28 @@ fast-forward. Ветки после вливания удаляются. `subagent_type: general-purpose`. Charter сабагента: - работай **строго в своём worktree** ``; в другие каталоги и в основную ветку не лезь; - - прогони Skill **`task-pipeline`** ровно на этой задаче, полный цикл SDD с - обоими чекпоинтами ревью; + - прогони Skill **`av-dev-pipeline:task-pipeline`** ровно на этой задаче, + полный цикл SDD с обоими чекпоинтами ревью; - если задаче назначен **номер артефакта** — используй строго его; - **профиль ревью выбирается по факту изменения.** Батч не повод понижать профиль: «нас много и мы спешим» — это ровно тот стимул, из-за которого проходы пропускают; - **режим прогона проходов — последовательный.** Твой worktree не один на машине; + - **если вложенные сабагенты недоступны** (движок не даёт запускать агентов из + агента) — не пропускай ревью и не понижай профиль: проведи его **инлайн** по + тем же charter'ам `av-dev-pipeline`, сохранив обязательное — гейт до + опиниативных проходов, состав по профилю, триаж последним. И **скажи в + отчёте прямым текстом, что ревью шло инлайн**: инлайновый проход видит + контекст автора и потому декоррелирован слабее — это меняет доверие к + результату, а не только способ запуска; - **вернуть отчёт**, в котором обязательно: исход задачи одним из трёх слов; - что сделано; какие вопросы записаны и куда; изменённые файлы; добавлялся ли - нумерованный артефакт и с каким номером; затронутые capability; состояние - гейта; **перечень запущенных проходов ревью поимённо с исходом каждого** и - границы покрытия. + **объявленный профиль ревью и режим прогона**; что сделано; какие вопросы + записаны и куда; изменённые файлы; добавлялся ли нумерованный артефакт и с + каким номером; затронутые capability; состояние гейта; **перечень + запущенных проходов ревью поимённо с исходом каждого**; **путь к + сохранённому отчёту триажа** (`openspec/changes//review/`); шло ли ревью + инлайн; границы покрытия. Сабагент, упершийся в вопрос, **не останавливает батч**: он записывает вопрос, режет задачу до остатка и доводит остаток — либо, если остатка нет, возвращает @@ -132,40 +190,86 @@ fast-forward. Ветки после вливания удаляются. ### 5. Проверить полноту ревью — до интеграции -**Ветка, чей отчёт не называет проходы поимённо, не вливается.** Пропуск прохода -не отличим от прохода без находок, и на уровне батча это ещё опаснее: отчётов -много, каждый выглядит полным, а сверять их некому, кроме тебя. +**Ветка, чей отчёт не называет профиль и проходы поимённо, не вливается.** +Пропуск прохода не отличим от прохода без находок, и на уровне батча это ещё +опаснее: отчётов много, каждый выглядит полным, а сверять их некому, кроме тебя. -По каждой готовой ветке сверь перечень проходов с таблицей профилей скилла -`review-pipeline` для объявленного профиля. Расхождение — не повод отменять -задачу: дозапусти недостающие проходы **на ветке**, в её worktree, через -`review-pipeline`, и только потом интегрируй. Отчёт дозапуска приложи к отчёту -задачи. +Сверка идёт в три шага, и порядок важен: + +1. **Возьми объявленный профиль** из отчёта задачи — он затем и заказан в + обязательных полях шага 4. Профиля в отчёте нет — перечень проходов сверять + не с чем; это само по себе основание не вливать, пока сабагент не назовёт + профиль и не обоснует его по факту изменения. +2. **Сверяй с независимым артефактом, а не с прозой отчёта.** Перечень проходов + бери из **сохранённого отчёта триажа** (`openspec/changes//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, по одной ветке Сводим ветки **строго последовательно** (линейная история), в порядке зависимостей. Вливаем **только зелёные**. +**Ветка задачи занята её worktree, и это определяет форму команд.** Пока worktree +жив (а удаляется он последним, после зелёного гейта), ветка `task/` +checkout'нута в нём, и `git rebase <основная> task/` из главного worktree +**падает**: `fatal: 'task/' is already used by worktree at …`. Поэтому +rebase делается **внутри worktree задачи**, а ff-слияние — из главного. + Для каждой готовой ветки `task/`: -- `git rebase <основная> task/` — перенос на текущую вершину; +- `git -C rebase <основная>` — перенос ветки задачи на текущую вершину, + выполняется в её собственном worktree; - резолв конфликтов (их почти нет — конфликтоопасное сериализовано, номера розданы заранее). Неавтоматический конфликт — **не форсируй**: прерви - (`git rebase --abort`), оставь ветку и worktree как есть, вынеси это в доклад - как нераспознанное пересечение; -- `git checkout <основная> && git merge --ff-only task/`; + (`git -C rebase --abort`), оставь ветку и worktree как есть, вынеси это + в доклад как нераспознанное пересечение; +- **ненулевой код `rebase` относится к этой ветке и только к ней.** Прерванный + rebase в чужом worktree не трогает ни главное дерево, ни остальные ветки: + проверь `git -C status` и `git status` — обе чистые. Уводить весь батч + в провалившиеся из-за одного ненулевого кода запрещено: это ложная причина, + из-за которой зелёные задачи не доедут до основной ветки. Провалилась одна — + провалилась одна; +- из главного worktree (он стоит на основной ветке — проверь + `git rev-parse --abbrev-ref HEAD`): `git merge --ff-only task/`. Ветку в + главном дереве **не переключай** — `git checkout task/` тоже упрётся в + занятость; - после каждой интеграции — **гейт на основной ветке**. Красное — **откати эту интеграцию** (`git reset --hard` на прошлую вершину), ветку с worktree сохрани, задачу перечисли в докладе. Основная ветка **никогда** не остаётся полузелёной; - только после зелёного: `git worktree remove ` и - `git branch -d task/`. + `git branch -d task/` — в этом порядке, иначе ветка снова занята. **Политика частичного провала.** Упавшая задача (исход «не доведена», красные -тесты в её worktree, конфликт при rebase) **не блокирует остальные**: интегрируем -все зелёные, упавшую оставляем в её worktree и ветке нетронутой — ничего не -удаляем, — и перечисляем в докладе с причиной, отчётом и путём к worktree. +тесты в её worktree, конфликт при rebase, невлитая из-за находок дозапуска) +**не блокирует остальные**: интегрируем все зелёные, упавшую оставляем в её +worktree и ветке нетронутой — ничего не удаляем, — и перечисляем в докладе с +причиной, отчётом и путём к worktree. Причина называется **настоящая**: «конфликт +rebase в файле X», а не «нераспознанное пересечение» на всякий случай. ### 7. Финальный гейт @@ -191,7 +295,7 @@ fast-forward. Ветки после вливания удаляются. уже делается» — именно он возникает, когда две задачи независимо решали похожее. -Замечания отрабатывай как в `task-pipeline`: `инлайн` чини сам, `развилка` — +Замечания отрабатывай как одиночный пайплайн: `инлайн` чини сам, `развилка` — вопросом в запись; после правок — снова гейт. ### 9. Прибраться и доложить @@ -199,16 +303,22 @@ fast-forward. Ветки после вливания удаляются. - Убери worktree и ветки **только успешно влитых** задач, в конце `git worktree prune`. Worktree и ветки **провалившихся** не трогай — они нужны для ручного дожатия. +- **Задачи батч не закрывает** — ни одну, ни свои, ни чужие записи учёта не + трогает. Он сообщает исход по каждой; закрытие происходит после приёмки и + делается владельцем спринта. - Доложи кратко: - **исход по каждой задаче** одним из трёх слов, с хешем коммита; - - план волн и порядок интеграции; + - план волн и порядок интеграции, с пометкой, какие задачи шли по одной как + замеряющие; - вопросы, записанные сабагентами, пачкой; - - что дозапускалось на шаге 5 и почему; + - что дозапускалось на шаге 5 и почему; шло ли где-то ревью инлайн; - итог финальной сверки и ссылки на архивные change; - - **отдельно — провалившиеся** задачи с причиной и путём к оставленному - worktree; + - **`Урожай`** — отложенные находки всех задач одним списком, с провенансом. + Задачи из него заводит тот, кто ведёт задачи проекта, а не батч; + - **отдельно — провалившиеся** задачи с настоящей причиной и путём к + оставленному worktree; - **границы покрытия сводной строкой**, включая задачи, чьи замеры снимались в - общей волне. + общей волне, и ветки, где ревью шло инлайн. ## Тонкости @@ -219,9 +329,9 @@ fast-forward. Ветки после вливания удаляются. - Поведенческая верификация внутри сабагента поднимает изменение вживую: следи, чтобы соседние worktree не дрались за порты и рабочие каталоги. Если проект умеет поднимать только один экземпляр — такие задачи в одну волну не ставь. -- Ревью выполненного — **до** закрытия задачи; это забота `task-pipeline` внутри - каждого сабагента, дублировать не надо. -- `openspec validate --strict` тоже внутри `task-pipeline` — не пропускай его +- Ревью выполненного — **до** интеграции; это забота + `av-dev-pipeline:task-pipeline` внутри каждого сабагента, дублировать не надо. +- `openspec validate --strict` тоже внутри пайплайна задачи — не пропускай его своими правками на интеграции. - Крупная переработка, предложенная ревью внутри задачи, — развилка: не вливай молча, вынеси в доклад. diff --git a/av-dev-pipeline/skills/task-pipeline/SKILL.md b/av-dev-pipeline/skills/task-pipeline/SKILL.md index a8746eb..e86e661 100644 --- a/av-dev-pipeline/skills/task-pipeline/SKILL.md +++ b/av-dev-pipeline/skills/task-pipeline/SKILL.md @@ -10,18 +10,48 @@ description: Автономно проводит одну задачу чере Это тонкая обёртка над каноническими скиллами `opsx:explore` / `opsx:propose` / `opsx:apply` / `opsx:archive` — вызывай их через Skill, не переизобретай их шаги. -Ревью — скилл `review-pipeline`, он же держит правило выбора профиля. +Ревью — скилл `av-dev-pipeline:review-pipeline`, он же держит правило выбора +профиля. + +## Предпосылки + +- **OpenSpec и скиллы `opsx:*`** — внешняя обвязка, на которой стоят шаги 2, 3, 6 + и 8, а также проход `review-specs` и профиль `design` (они завязаны на + `openspec/changes//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` проекта и то, на что он ссылается (архитектура, конвенции), если ещё не в контексте. Проектные факты, нужные ревью -— инварианты, команда гейта, объёмы, модель угроз, — живут в брифе +— инварианты, команда гейта, объёмы, модель угроз, прецеденты, — живут в брифе (`docs/review-brief.md`, контракт — в references конвейера ревью). +**Брифа нет ни по одному пути — заведи его, а не работай в деградированном +режиме.** Вызови Skill **`av-dev-pipeline:project-brief`**: он соберёт бриф из +`CLAUDE.md`, архитектуры, файла задач и конвенций, покажет человеку и вернёт +путь, который дальше передаётся ревью. Это механика: спрашивать разрешения не +нужно. Один шаг один раз на проект — против деградации на каждой задаче. + ## Границы: чем пайплайн не владеет - **Беклогом, спринтом, целями и приоритетами.** Задача приходит извне. Пайплайн её не выбирает, не приоритизирует, не заводит и не переоценивает; если в проекте есть свой процесс управления задачами — он и решает, что брать. +- **Записями учёта.** Пайплайн **не закрывает задачу**, не двигает её по + статусам, не правит индекс и не зовёт скриптов учёта. Он сообщает исход; + закрытие — акт владельца спринта **после приёмки**, и оно происходит снаружи. + Закрыть задачу самому — значит закрыть её до коммита и до всякой приёмки, то + есть заверить собственную работу. +- **Заведением задач из урожая ревью.** Отложенные находки отдаются **списком** + (см. шаг 7); превращать их в задачи — работа того, кто ведёт задачи проекта. - **Определением ценности.** «Нужна ли эта функциональность» — не вопрос пайплайна ни на одном шаге. @@ -47,12 +77,17 @@ description: Автономно проводит одну задачу чере поимённо, непущенные проходы названы в границах покрытия; 3. change заархивирован, дельты влиты в актуальные спеки; 4. коммит сделан в текущую ветку; -5. **критерии приёмки, если проект их дал**, проверены поимённо, у каждого назван - оракул и исход. Критерии приходят снаружи; пайплайн их не сочиняет и не - занижает. Расхождение «критерии закрыты, а суть задачи не достигнута» — дефект - критериев, и о нём сообщается, а не молча дорабатывается. +5. **критерии приёмки, если проект их дал, выписаны поимённо, и по каждому назван + оракул и наблюдаемый исход** — «прогнал вот это, увидел вот то». Это **доклад, + а не сертификация: приёмка — не работа пайплайна.** Исполнитель, ставящий себе + галочку «принято», проверяет свою работу своим же взглядом — по границе это + может делать только декоррелированный приёмщик. Критерии приходят снаружи; + пайплайн их не сочиняет и не занижает. Расхождение «по каждому критерию исход + есть, а суть задачи не достигнута» — дефект критериев, и о нём сообщается, а + не молча дорабатывается. -Пункты 1–4 — своё. Пункт 5 — внешнее, и проверяется только если оно дано. +Пункты 1–4 — своё. Пункт 5 — внешнее: пайплайн доводит его до наблюдаемого +исхода и передаёт дальше. ## Принцип автономности @@ -73,13 +108,23 @@ description: Автономно проводит одну задачу чере 3. Доведи остаток до конца и закоммить. Задача не «висит на вопросе», она сделана в объявленных границах. -**Что остатком не является** — две оговорки, без которых правило вредит: +**Что остатком не является — правило живёт не здесь.** Канонический текст с обеими +оговорками — в плагине `av-dev-tasks`, скилл `av-dev-tasks:session`, раздел +`## Вопрос, блокер, необратимое`, подраздел «Отличать вопрос от застревания». +Правило принадлежит управлению задачами, потому что решает **сделана задача или +вышла**, — это исход планирования, а не исполнения. **Ссылайся, не +пересказывай:** копия, заведённая здесь, уже однажды разошлась с оригиналом и +потеряла из перечня самое необратимое — запись **наружу**. -- **остаток, который записывает в хранилище или в журнал состояние, зависящее от - нерешённого, — не остаток.** Решение поднимается до начала записи. Иначе - нерешённое материализуется в данные, а данные переживают решение; -- **остаток, из которого пропала польза, названная в постановке, — не остаток.** - Это исход «не доведена», а не «сделана в границах». +Коротко, чтобы знать, когда идти читать: остаток проверяется двумя порогами — +**материализация нерешённого** (запись состояния, зависящего от неотвеченного +вопроса) и **пол по пользе** (из остатка пропала польза, названная в постановке). +Оба порога — стоп: первый поднимает решение до начала записи, второй даёт исход +«не доведена». + +Плагин `av-dev-tasks` не подключён — правило не отменяется, а становится +осторожнее: прежде чем записать зависящее от нерешённого куда бы то ни было — +в хранилище, в журнал, в витрину или наружу, — спрашивай человека. Нет полезного остатка — задача заканчивается исходом «не доведена», вопрос записан, ничего не коммитится наполовину. @@ -137,10 +182,10 @@ description: Автономно проводит одну задачу чере ### 4. (Нетривиальная) Ревью предложения — профиль `design`, ДО кода -Первый чекпоинт. Вызови Skill **`review-pipeline`** с профилем `design`, ссылкой -на change `` и путём к брифу. Он запустит `review-specs` (режим «дизайн ДО -кода»), `review-rubric` (фаза 1: приёмочные критерии для задуманного узла) и -`review-architecture` по предложению. +Первый чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`** с профилем +`design`, ссылкой на change `` и путём к брифу. Он запустит `review-specs` +(режим «дизайн ДО кода»), `review-rubric` (фаза 1: приёмочные критерии для +задуманного узла) и `review-architecture` по предложению. Смысл профиля: архитектурная находка на готовом коде стоит переписывания и потому игнорируется — та же находка здесь стоит абзаца обсуждения. Рубрику из @@ -170,17 +215,16 @@ description: Автономно проводит одну задачу чере **Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца шага. -### 7. Ревью кода — Skill `review-pipeline` +### 7. Ревью кода — Skill `av-dev-pipeline:review-pipeline` -Второй чекпоинт. Вызови Skill **`review-pipeline`**, дав ссылку на change ``, -базу диффа, путь к брифу, профиль **и режим запуска**. Профиль выбирается по -факту изменения, а не по ощущению важности; общее правило — в скилле, проектные -триггеры — в брифе: +Второй чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`**, дав ссылку +на change ``, базу диффа, путь к брифу, профиль **и режим запуска**. -- миграция схемы, новый пакет, публичный контракт, правило идентичности или - слияния данных → `deep`; -- иначе меняется поведение, видимое снаружи → `standard`; -- иначе (багфикс, локальная правка, доки) → `quick`. +**Правило выбора профиля живёт в скилле конвейера** (раздел «Профили»), проектные +триггеры — в разделе `## Триггеры` брифа. Здесь оно не пересказывается: три +копии одного правила расходятся, и работать будет та, которую прочитали +последней. Помни ровно одно — **профиль выбирается по факту изменения, а не по +ощущению важности**, и посмотри таблицу перед вызовом. **Режим по умолчанию последовательный, и обосновывать его не надо.** Параллельно гоняем только тогда, когда об этом попросили явно **и назвали набор** — какие @@ -195,41 +239,54 @@ description: Автономно проводит одну задачу чере покрытия. **Сверь состав прогона с таблицей профилей в скилле, прежде чем коммитить.** -Пропуск прохода не отличим от прохода без находок: гейт зелёный, спеки сошлись, -отчёт выглядит полным. Единственный, кто мог бы заметить пропуск, — триаж, а он -заполняется тем, что ему подали. Отчёт обязан называть запущенные проходы -**поимённо и с исходом**; непущенный идёт строкой «не запускался» в границы -покрытия. Реестр короткий (4–8 проходов) — сверка стоит одного взгляда, а -молчащий пропуск уже стоил семи находок и отдельной задачи на их дозакрытие. +Отчёт обязан называть запущенные проходы **поимённо и с исходом**; непущенный +идёт строкой «не запускался» в границы покрытия. Реестр короткий (4–8 проходов) — +сверка стоит одного взгляда. Почему это правило существует, объясняет раздел +«Профили» скилла конвейера; здесь — само требование. Отработай так же, как шаг 5: помеченное `инлайн` чини сам и не логируй, `развилка` — вопросом в запись (он уже сформулирован триажем, его остаётся перенести). После правок — снова гейт. +**Урожай — списком, не задачами.** Отложенные находки (реальный `major` не для +этого мерджа, развилка, решённая «потом», пачка `nit`) собери в секцию доклада +`Урожай`: формулировка, оракул, провенанс. Задачи из него **заводит не пайплайн** +— у того, кто ведёт задачи проекта, своя нарезка, свой формат и свои правила +дублей. Твоя обязанность — не потерять и передать. + **Границы покрытия из отчёта не выбрасывай** — они уезжают в финальный доклад сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно», превращается в ложное ощущение проверенности. -Отчёт триажа сохрани вместе с change (`openspec/changes//review/`): по нему -потом видно, что было найдено и что из этого осталось незаведённым. +**Отчёт триажа сохрани вместе с change (`openspec/changes//review/`) — это +обязательно, а не «если удобно».** По нему потом видно, что было найдено и что из +этого осталось в урожае. И это единственный **независимый** артефакт о составе +прогона: под оркестратором `task-batch` именно по нему сверяют полноту ревью +ветки, а не по твоей прозе — она написана тем же, кто мог проход и пропустить. ### 8. Архивировать — `opsx:archive` Вызови Skill `opsx:archive`: change уезжает в архив, дельты вливаются в актуальные спеки. -### 9. Синк документации и закрытие задачи +### 9. Синк документации -Ревью выполненного — **до** закрытия. Затем: +Ревью выполненного — до этого шага. Затем: - суть переехавшего решения — в документацию проекта (архитектура, журнал решений), если её там ещё нет; - менялась схема — её описание обновлено тем же change; - новое, узнанное о внешнем формате или о данных, — в тот файл проекта, который это накапливает; такой файл обычно ценнее кода; -- **задача закрывается процедурой проекта** — своей у пайплайна нет. Есть скилл - или скрипт беклога — вызови его; нет — скажи в докладе, что задача сделана и - закрытие остаётся за вызывающим. Не выдумывай формат чужого индекса. +- воспроизведённый дефект (свой или чужой) — в раздел `## Прецеденты` брифа: + класс, симптом, чем воспроизведён, чем закончилось. Это единственный артефакт, + который делает следующее ревью умнее. + +**Задачу пайплайн не закрывает.** Записи учёта — индекс, статус, спринт — он не +трогает вовсе: закрытие происходит **после приёмки** и делается владельцем +спринта, а пайплайн на этом шаге стоит до коммита и до всякой приёмки. Скриптов +и скиллов учёта не зови — их у тебя и нет: пути между плагинами не разрешаются, и +моста здесь намеренно не проложено. Твоё дело — назвать исход в докладе. ### 10. Коммит @@ -242,13 +299,17 @@ description: Автономно проводит одну задачу чере сделано», тело списком 1–3 пункта, без трейлеров). Одна задача — один осмысленный коммит. -Готово — доложи кратко: +Готово — доложи кратко. Доклад и есть выход пайплайна: **задача остаётся +открытой**, её закрывает владелец спринта после приёмки. - **исход** задачи одним из трёх слов и, если не «сделана», чем ограничен результат; - что сделано, какие вопросы записаны и куда; -- ссылка на архивный change; -- исход по каждому критерию приёмки, если они были; +- ссылка на архивный change и хеш коммита; +- по каждому критерию приёмки, если они были: **оракул и наблюдаемый исход** — + это доклад приёмщику, а не отметка «принято»; +- **`Урожай`** — отложенные находки списком (формулировка, оракул, провенанс). + Задачи из него заводит тот, кто ведёт задачи проекта; - **одна строка границ покрытия**: какой профиль и режим гонялись, какие проходы не запускались и что проверить было невозможно. Доклад без неё сообщает «проверено», не сообщая, что именно. @@ -270,5 +331,4 @@ description: Автономно проводит одну задачу чере - **Занизить профиль ревью или пропустить проход — самый дешёвый способ «ускориться», и он же самый дорогой по последствиям.** Защита одна: профиль выбирается по факту изменения, состав сверяется поимённо, а непущенное - называется в отчёте. Пропуск, названный строкой, стоит строки; пропуск молчащий - стоил семи находок. + называется в отчёте строкой.