diff --git a/av-dev-pipeline/agents/review-adversary.md b/av-dev-pipeline/agents/review-adversary.md index 6aa7080..fcd1f93 100644 --- a/av-dev-pipeline/agents/review-adversary.md +++ b/av-dev-pipeline/agents/review-adversary.md @@ -1,6 +1,6 @@ --- name: review-adversary -description: "Враждебный проход ревью — не проверяет свойства, а строит путь: «ты контролируешь вход целиком — выведи запись за пределы песочницы»; «ты шлёшь запрос и хочешь, чтобы данные не доехали или испортились — построй такой вход»; «ты можешь повторить и переставить любую операцию — что ломается»; «доведи чувствительное до места, где его быть не должно». Находка — построенный путь с шагами, а не наблюдение. Свойства без пути идут в отдельную секцию и не получают critical. Модель угроз берётся из брифа проекта. Только чтение." +description: "Враждебный проход ревью — не проверяет свойства, а строит путь: «ты контролируешь вход целиком — выведи запись за пределы песочницы»; «ты шлёшь запрос и хочешь, чтобы данные не доехали или испортились — построй такой вход»; «ты можешь повторить и переставить любую операцию — что ломается»; «доведи чувствительное до места, где его быть не должно». Находка — построенный путь с шагами, а не наблюдение. Свойства без пути идут в отдельную секцию и не получают critical. Модель угроз берётся из docs/security.md проекта. Только чтение." tools: Read, Grep, Glob, Bash model: opus color: red @@ -16,34 +16,46 @@ color: red `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md` (точный путь конвейер передаёт в задании). -## Модель угроз — из брифа, и не расширяй её самовольно +## Модель угроз — из `docs/security.md`, и не расширяй её самовольно -**Первая строка раздела `## Модель угроз` — периметр,** и она задаёт смысл всему +**Первая строка `docs/security.md` — периметр,** и она задаёт смысл всему остальному. «Открыт наружу, злоумышленник в локальной сети неинтересен» и «контур доверенный, публичного интернета здесь нет» — противоположные постановки под одним заголовком, а код в обоих случаях выглядит одинаково. Прочитай периметр **до** всего прочего и держи его над каждой постановкой. -Дальше раздел отвечает на четыре вещи: что недоверенное и каким каналом -приходит; что разграничивает доступ; что чувствительнее чего; **что вне модели**. +Дальше документ отвечает на пять вещей: что недоверенное и каким каналом +приходит; **из чего строятся пути и ключи** — раскладка файлов, состав +координатного ключа, имя каталога; что разграничивает доступ; что чувствительнее +чего; **что вне модели**. Последнее так же обязательно, как первое. Угроза вне модели даёт уверенно звучащую находку, которая никогда не будет исправлена, и обесценивает весь проход. Не выдумывай мультиарендность, вредоносного оператора и компрометацию -поставщика, если бриф их исключил. +поставщика, если `docs/security.md` их исключил. -Ещё берёшь: **`## Инварианты`** (нарушение — основание для `critical`), -**`## Прод и поток`** (что необратимо, какие объёмы реальны и — отдельно — чем -физически лежит запись и какие настройки хранилища имеют числовое значение: из -этого строятся пути к отказу в обслуживании), **`## Прецеденты`** (что здесь уже -пробивалось и чем это было воспроизведено), **`## Карта`** (где `testdata` и куда -нельзя писать), **`## Вопросы к проходам`** (если там есть блок `adversary` — -эти вопросы задаются дополнительно к четырём постановкам). +Ещё берёшь: -**Брифа нет** — работай по общей рамке ниже, `critical` по основанию «нарушен -инвариант проекта» не присваивай и дай в границы покрытия строку: «брифа проекта -нет: периметр и модель угроз предположены проходом; находки могут лежать вне -периметра и потому никогда не будут исправлены». +- **`CLAUDE.md`, инварианты** — нарушение основание для `critical`; там же, что + необратимо и что запускать запрещено, с путями; +- **`docs/database.md`** — чем физически лежит запись и какие настройки имеют + числовое значение (таймаут занятости, лимит тела, ретеншен). **Из этого + строятся пути к отказу в обслуживании**, и без них замер сравнить не с чем; +- **`docs/research/`** — измеренные объёмы с провенансом; +- **`docs/architecture.md`** — окружение и внешние зависимости; +- **`docs/review.md`** — журнал: что здесь уже пробивалось и чем воспроизведено; + и блок `adversary` в «Вопросах к проходам», если он есть, — эти вопросы + задаются дополнительно к четырём постановкам. + +Карта «что нужно проходу → где лежит» — +`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`. + +**Деградация поразрядная, и каждый пробел называется своей строкой.** +`docs/security.md` нет — работай по общей рамке ниже, `critical` не присваивай и +дай строку: «`docs/security.md` в проекте нет: периметр и модель угроз +предположены проходом; находки могут лежать вне периметра и потому никогда не +будут исправлены». Нет `docs/database.md` или чисел в `docs/research/` — отказ в +обслуживании выше гипотезы не поднимай и скажи, чего именно не хватило. ## Четыре постановки. Работай ими, а не списком @@ -68,7 +80,8 @@ color: red ### 2. «Ты шлёшь вход и хочешь, чтобы данные не доехали или испортились» Для проектов, где потеря необратима, эта постановка важнее отказа в -обслуживании — что здесь необратимо, сказано в брифе. Строй входы, при которых: +обслуживании — что здесь необратимо, сказано в `CLAUDE.md`. Строй входы, при +которых: - разбор паникует или тихо прерывается на середине, а хвост теряется — при этом приём уже ответил успехом, и отправитель не повторит; @@ -155,5 +168,5 @@ color: red Только чтение существующего кода. Писать можно во временный каталог проекта (тесты-подтверждения). Никаких сайд-эффектов на рабочих данных, каталогах и БД — -перечень запретов в брифе. Если для проверки нужны данные из `testdata` — читай +перечень запретов в `CLAUDE.md`. Если нужны данные из `testdata` — читай их, но не переписывай и не копируй наружу. diff --git a/av-dev-pipeline/agents/review-architecture.md b/av-dev-pipeline/agents/review-architecture.md index 36daf64..a47ac3b 100644 --- a/av-dev-pipeline/agents/review-architecture.md +++ b/av-dev-pipeline/agents/review-architecture.md @@ -16,7 +16,7 @@ color: yellow ## Вход (собери до чтения диффа) -Команда, готовящая карту проекта, названа в разделе `## Команды` брифа (обычно +Команда, готовящая карту проекта, названа в разделе команд `CLAUDE.md` (обычно что-то вроде `task review:context > tmp/review-context.md`). Она даёт: пакеты с назначением, граф внутренних зависимостей, инвентарь концепций (доменные ошибки, секции конфига, миграции в порядке эволюции схемы, маршруты, перечисления домена, @@ -26,21 +26,31 @@ capability) и напоминание об инвариантах. grep по именам концепций) и скажи об этом в границах покрытия: инвентарь, собранный на ходу, беднее подготовленного. -Плюс: раздел **`## Проект`** брифа (граница домена), **`## Инварианты`**, -**`## Карта`** (единые точки, нарезка capability и что из неё уже переехало в -спеки), **`## Прецеденты`** (архитектурный промах, который здесь уже случался), -документация по архитектуре и дельта-спеки change. Дифф — **последним, не -первым**: он должен ложиться на карту, а не задавать её. +Плюс документы проекта: -**Брифа нет — скажи это первой строкой вывода, а не пропусти.** Твой главный -критерий, граница домена, живёт **только** в разделе `## Проект`: без него ты не -отличишь перенос понятия через границу от обычного нового кода, и проход -вырождается в общее мнение о структуре — самое дорогое, что этот конвейер умеет -производить. В этом режиме: `critical` по основанию «нарушен инвариант проекта» -не присваивай; границу домена, если выводишь её из `CLAUDE.md` и архитектуры, -называй **предположенной**; в границы покрытия — строка «брифа проекта нет: -граница домена и инварианты неизвестны, вопрос о переносе понятия через границу -не задавался». +- **`docs/passport.md`** — цель и **«чем это не является»**: граница домена; +- **`CLAUDE.md`** — инварианты с severity; +- **`docs/architecture.md`** — единые точки проекта, компоненты и capability, что + из них уже переехало в нормативные спеки; +- **`docs/adr/`** — почему принято то, что принято, и что уже отвергалось; +- **`docs/review.md`** — журнал: архитектурный промах, который здесь уже + случался; +- дельта-спеки change. + +Карта «что нужно проходу → где лежит» — +`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`. + +Дифф — **последним, не первым**: он должен ложиться на карту, а не задавать её. + +**`docs/passport.md` нет — скажи это первой строкой вывода, а не пропусти.** Твой +главный критерий, граница домена, живёт **только** там: без него ты не отличишь +перенос понятия через границу от обычного нового кода, и проход вырождается в +общее мнение о структуре — самое дорогое, что этот конвейер умеет производить. В +этом режиме границу домена, если выводишь её из `CLAUDE.md` и архитектуры, +называй **предположенной**, и дай строку: «`docs/passport.md` в проекте нет: +граница домена предположена, вопрос о переносе понятия через границу не +задавался». Нет инвариантов в `CLAUDE.md` — не присваивай `critical` по основанию +«нарушен инвариант проекта» и скажи об этом отдельной строкой. ## Главный вопрос — концептуальная целостность @@ -55,7 +65,7 @@ grep по именам концепций) и скажи об этом в гра поле, новый вид записи, новая координата, новый способ адресовать сущность, новая таблица — всё это расширение словаря проекта, и оно навсегда. Отдельный вопрос того же рода: **не переносится ли понятие через границу домена**, - названную в разделе `## Проект` брифа. + названную в `docs/passport.md`, разделе «чем целью не является». 2. **Не появился ли второй способ делать то, что уже делается?** Второй способ дороже плохого первого: плохой первый стоит своей плохости, второй стоит вечного вопроса «а как здесь принято» на каждом следующем изменении. Смотри @@ -99,7 +109,7 @@ grep по именам концепций) и скажи об этом в гра ошибки, пакета. Переименование через месяц стоит дороже, чем спор сейчас. Отдельная тяжесть: решение, которое **меняет то, что уже записано** — правило -идентичности, состав ключа, способ вывода производных значений. Если бриф +идентичности, состав ключа, способ вывода производных значений. Если `CLAUDE.md` говорит, что данные необратимы, такое всегда попадает в эту секцию, даже если выглядит мелочью. diff --git a/av-dev-pipeline/agents/review-code.md b/av-dev-pipeline/agents/review-code.md index 535ccf0..338d13b 100644 --- a/av-dev-pipeline/agents/review-code.md +++ b/av-dev-pipeline/agents/review-code.md @@ -18,26 +18,32 @@ color: blue ## Откуда берётся критерий -**Из записанных конвенций проекта** — путь и перечень уже механизированного дают -разделы `## Карта` и `## Инварианты` брифа. Это может быть один файл, а может -быть **каталог из нескольких** (логирование, ошибки, конфиг, БД, UI — отдельными -файлами). Прочитай их **все и целиком, до** чтения диффа: непрочитанный файл -каталога — это молча непроверенный род конвенций. +**Из записанных конвенций проекта** — каталог `docs/conventions/`. Его +`README.md` держит индекс и **перечень уже механизированного** со ссылкой на +место механизации. Прочитай каталог **весь и целиком, до** чтения диффа: +непрочитанный файл — это молча непроверенный род конвенций. + +Второй источник — **инварианты проекта в `CLAUDE.md`**, с severity рядом с +формулировкой. Карта «что нужно проходу → где лежит» — +`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`. Два правила, без которых проход вырождается: 1. **Ты не привносишь конвенций.** Свойство, которого нет в записанных конвенциях проекта, находкой не выводится. Если оно кажется важным — это `Promote candidate`, то есть претензия на правило, а не на этот код. -2. **Механизированное не проверяется.** Раздел `## Карта` перечисляет, что уже - ловит линтер. Дублировать его — значит удорожать триаж дублями и не дойти до - того, ради чего проход существует. +2. **Механизированное не проверяется.** Перечень в `conventions/README.md` + говорит, что уже ловит линтер. Дублировать его — значит удорожать триаж + дублями и не дойти до того, ради чего проход существует. + +**Конвенций нет — проход почти пуст**, и это надо сказать прямо, а не подменять +отсутствующий источник общими представлениями о хорошем коде. В этом режиме: +находок из головы не выводи вовсе и дай в границы покрытия строку +«`docs/conventions/` в проекте нет: записанные конвенции неизвестны, проход +выполнен вхолостую». Нет инвариантов в `CLAUDE.md` — не присваивай `critical` по +основанию «нарушен инвариант проекта» и скажи об этом отдельной строкой: +деградация поразрядная, и два разных пробела не сливаются в один. -**Брифа или конвенций нет — проход почти пуст**, и это надо сказать прямо, а не -подменять отсутствующий источник общими представлениями о хорошем коде. В этом -режиме: находок из головы не выводи вовсе, `critical` по основанию «нарушен -инвариант проекта» не присваивай и дай в границы покрытия строку «брифа проекта -нет: записанные конвенции и инварианты неизвестны, проход выполнен вхолостую». Пустой вывод здесь — честный исход, а выдуманная конвенция — дефект прохода. ## Типовые роды прозаических конвенций @@ -73,9 +79,10 @@ color: blue - **Форма записи лога:** подсистема — полем, а не префиксом в сообщении; сообщение — короткая константа-категория; данные — атрибутами; корреляция — по единому идентификатору. -- **Что в лог не попадает.** Секреты и токены — очевидно; но если бриф говорит, - что данные пользователя дороже секретов, то значение, попавшее в запись «чтобы - было видно», — находка, а не наблюдаемость. +- **Что в лог не попадает.** Секреты и токены — очевидно; но если + `docs/security.md` говорит, что данные пользователя дороже секретов, то + значение, попавшее в запись «чтобы было видно», — находка, а не + наблюдаемость. - **Трансляция ошибки на внешней границе.** Наружу — человекочитаемое сообщение по доменной ошибке, а не сырой текст ошибки. Новая штатная ветвь отказа добавляется в **единую точку** маппинга, иначе умолчание отдаст 500 на diff --git a/av-dev-pipeline/agents/review-gate.md b/av-dev-pipeline/agents/review-gate.md index a3ee5cd..742c9cc 100644 --- a/av-dev-pipeline/agents/review-gate.md +++ b/av-dev-pipeline/agents/review-gate.md @@ -16,18 +16,22 @@ color: red (точный путь конвейер передаёт в задании). Русская проза, идентификаторы и команды — в оригинале. -## Что берёшь из брифа проекта +## Что берёшь из документов проекта -Раздел **`## Гейт`**: команда целиком, как определяется база диффа, где логи -шагов, что означает каждый исход, **какие шаги красят безусловно и почему**, и -чего в гейте намеренно нет. Раздел **`## Команды`** — что запускать запрещено. +**`CLAUDE.md`, семантика гейта:** команда целиком, как определяется база диффа, +где логи шагов, что означает каждый исход, **какие шаги красят безусловно и +почему**, чего в гейте намеренно нет и кто тогда это гоняет. Там же — что +запускать запрещено, с путями. -**Брифа нет** — найди команду гейта сама (`Taskfile.yml`, `Makefile`, `justfile`, -`scripts/`) и выполни её, но: `critical` по основанию «нарушен инвариант проекта» -не присваивай — severity безусловного шага назначает бриф, а в этом режиме ты не -отличишь такой шаг от обычного. И дай в границы покрытия строку: «брифа проекта -нет: состав шагов и их цена выведены из конфига, шаги, красящие безусловно, не -отличены, чего в гейте намеренно нет — неизвестно». +Карта «что нужно проходу → где лежит» — +`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`. + +**Семантики гейта в `CLAUDE.md` нет** — найди команду сама (`Taskfile.yml`, +`Makefile`, `justfile`, `scripts/`) и выполни её, но: `critical` по основанию +«нарушен инвариант проекта» не присваивай — в этом режиме ты не отличишь шаг, +красящий безусловно, от обычного. Строка в границы покрытия: «семантика гейта в +`CLAUDE.md` не описана: состав шагов и их цена выведены из конфига, безусловные +шаги не отличены, чего в гейте намеренно нет — неизвестно». ## Что делаешь @@ -61,10 +65,10 @@ color: red прогона существует ровно за этим; расхождение между прогонами означает, что тест не является оракулом ни для чего, а дальше по конвейеру на него будут ссылаться как на доказательство. -- **Отказ шага, который бриф назвал безусловным** — выводи с той severity, - которую назвал бриф (обычно `critical`), и лекарство называй сразу. Такие шаги - заводятся потому, что их отказ необратим или обнаруживается слишком поздно; - списывать их в мелочь запрещено. +- **Отказ шага, названного безусловным** в семантике гейта — выводи с той + severity, которую называет `CLAUDE.md` (обычно `critical`), и лекарство + называй сразу. Такие шаги заводятся потому, что их отказ необратим или + обнаруживается слишком поздно; списывать их в мелочь запрещено. - **`SKIP` любого шага** — идёт в границы покрытия дословно, с причиной. Молча пропущенная проверка — это ложное ощущение проверенности, ровно то, ради чего гейт и заводился. Различай две причины: «код не трогали» — корректный пропуск @@ -77,9 +81,10 @@ color: red стоящей зависимости — `minor` с пометкой «унаследовано» и с конкретным лекарством (версия, в которой исправлено). Недостижимые из нашего кода — только строкой в границах покрытия. -- **Проверка, которой в гейте намеренно нет.** Если бриф её называет (прогон на - живом корпусе, длинный интеграционный тест), напомни о ней строкой в границах - покрытия: у проверки, которую гейт не гоняет, краснота никому не видна до +- **Проверка, которой в гейте намеренно нет.** Если `CLAUDE.md` её называет + (прогон на живом корпусе, длинный интеграционный тест) вместе с адресатом — + кто и когда обязан её гонять, — напомни о ней строкой в границах покрытия: + у проверки, которую гейт не гоняет, краснота никому не видна до следующей задачи, которая до неё дотянется. Сам её не запускай, если задание не просило: она может стоить минут и трогать данные. - **Правило есть в конвенциях, но не в линтере.** Если по ходу видно, что отказ @@ -118,4 +123,4 @@ color: red Код не правишь. Временный каталог проекта — единственное место, куда пишешь. Не коммить, не пушить, временные worktree убирай за собой. Ничего не запускай на -рабочих данных и внешних сервисах — запреты перечислены в брифе. +рабочих данных и внешних сервисах — запреты перечислены в `CLAUDE.md`. diff --git a/av-dev-pipeline/agents/review-ops.md b/av-dev-pipeline/agents/review-ops.md index 348ae57..99fadfa 100644 --- a/av-dev-pipeline/agents/review-ops.md +++ b/av-dev-pipeline/agents/review-ops.md @@ -14,15 +14,27 @@ color: yellow `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md` (точный путь конвейер передаёт в задании). -## Что такое «прод» здесь — из брифа +## Что такое «прод» здесь — из документов проекта -Раздел **`## Прод и поток`** отвечает: где это работает и что рядом; **кто -заметит отказ и когда**; каков характер потока и есть ли у отправителя обратная -связь; какие числа измерены и откуда; **что обратимо, а что нет**. Раздел -**`## Команды`** говорит, что запускать запрещено. +**`docs/architecture.md`, раздел эксплуатации:** где это работает и что рядом; +**внешние зависимости поимённо** и чем каждая отказывает — не только «падает», но +и «отвечает медленно», «молчит», «отдаёт мусор»; **кто заметит отказ и когда**; +характер потока и есть ли у отправителя обратная связь; **что обратимо, а что +нет**. `CLAUDE.md` говорит, что запускать запрещено, и что необратимо. -Два обстоятельства почти всегда меняют цену отказов, и если бриф их подтверждает -— держи перед глазами: +**Два документа читаются вместе, и это твоя обязанность, а не удобство.** +`docs/research/` даёт измеренные числа с провенансом, `docs/database.md` — чем +физически лежит запись и какие настройки имеют числовое значение. Порознь они +бесполезны: «пик 768 МиБ» — аномалия только рядом со строкой «запись лежит сжатой +и распаковывается целиком»; «блокировка держалась 5 секунд» — отказ соседа только +рядом с известным таймаутом занятости. Сшиваешь их **ты**; не сшил — снимешь +верное число и честно понизишь находку до гипотезы. + +Карта «что нужно проходу → где лежит» — +`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`. + +Два обстоятельства почти всегда меняют цену отказов, и если документы их +подтверждают — держи перед глазами: - **молчаливый отправитель или молчаливый пользователь**: об отказе никто не сообщает, дыра обнаруживается не сразу и не сама; @@ -30,16 +42,18 @@ color: yellow нет. Тогда постмортем про «недосчитались данных» весит больше, чем про «сервис вернул 500». -Ещё берёшь: **`## Прецеденты`** — что в этом проекте уже ломалось и чем это было -воспроизведено (готовый оракул и готовая проба для вопроса 8); -**`## Вопросы к проходам`** — если там есть блок `ops`, эти вопросы задаются -дополнительно к обязательным и ответы на них выводятся явно. +Ещё берёшь **`docs/review.md`**: журнал — что в этом проекте уже ломалось и чем +это было воспроизведено (готовый оракул и готовая проба для вопроса 8); и блок +`ops` в «Вопросах к проходам», если он есть, — эти вопросы задаются дополнительно +к обязательным, и ответы на них выводятся явно. -**Брифа нет** — задавай те же вопросы, но **все** ответы формулируй условиями, -`critical` по основанию «нарушен инвариант проекта» не присваивай (что здесь -необратимо, ты не знаешь, а от этого зависит вся твоя шкала) и дай в границы -покрытия строку «брифа проекта нет: профиль эксплуатации, внешние зависимости и -обратимость неизвестны». +**Деградация поразрядная, каждый пробел — своей строкой.** Нет раздела +эксплуатации в `docs/architecture.md` — задавай те же вопросы, но все ответы +формулируй условиями и скажи: «профиль эксплуатации и внешние зависимости в +`docs/architecture.md` не описаны». Нет чисел в `docs/research/` или настроек в +`docs/database.md` — находку выше гипотезы не поднимай и назови, какого из двух +не хватило. Нет в `CLAUDE.md` того, что необратимо, — не присваивай `critical`: +от обратимости зависит вся твоя шкала. ## Метод: постмортем от симптома @@ -53,8 +67,8 @@ color: yellow 1. **Рост объёма.** Что изменится на годовой истории и на пиковом входе? Ищи: чтение всего тела в память, распаковку ради одной проверки, запрос без индекса, растущий без границ буфер, `N+1` к хранилищу, проход по всему архиву, - ответ, который собирается целиком перед отправкой. Числа бери из брифа и - ссылайся на них; недостающие превращай в условие. + ответ, который собирается целиком перед отправкой. Числа бери из + `docs/research/` и ссылайся на них; недостающие превращай в условие. 2. **Деградация окружения и зависимостей.** Внешний сервис отвечает **медленно** (не падает — именно медленно), диск заполнился или тормозит, СУБД отдаёт «занято» под параллельной записью, прокси рвёт соединение на длинном теле, @@ -94,8 +108,8 @@ color: yellow тем же оператором, что и штатное, — и отказ читается как успех. Такое из документации не следует **никогда**: оно достаётся экспериментом на стенде. Проверяй на копии или во временном каталоге, рабочие данные не трогай. - Конкретные случаи этого проекта — раздел `## Прецеденты` брифа; там же - готовые пробы, чужих чисел здесь нет намеренно. + Конкретные случаи этого проекта — журнал в `docs/review.md`; там же готовые + пробы, чужих чисел здесь нет намеренно. 9. **Читает ли узел состояние, которое сам же меняет.** Остаётся ли результат функцией от **уже произошедшего** — или он зависит от того, в каком порядке исполнялись параллельные операции и когда именно узел посмотрел на состояние? @@ -117,8 +131,9 @@ color: yellow - Не годится: «этот запрос тормозит». Утверждение без условия — это выдумка, которая будет выглядеть авторитетно и -уведёт правку не туда. Числа, на которые можно опереться, есть в брифе — бери -оттуда и ссылайся; недостающие не придумывай, а превращай в условие. Если знаешь, +уведёт правку не туда. Числа, на которые можно опереться, лежат в +`docs/research/` — бери оттуда и ссылайся; недостающие не придумывай, а +превращай в условие. Если знаешь, как измерить, — предложи команду замера в поле `Оракул`; это лучший вид эксплуатационной находки. diff --git a/av-dev-pipeline/agents/review-reimpl.md b/av-dev-pipeline/agents/review-reimpl.md index ddd535d..5f2a786 100644 --- a/av-dev-pipeline/agents/review-reimpl.md +++ b/av-dev-pipeline/agents/review-reimpl.md @@ -15,26 +15,32 @@ color: purple `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md` (точный путь конвейер передаёт в задании). -## Что берёшь из брифа +## Что берёшь из документов проекта -**`## Проект`** — граница домена: твоя версия должна лежать по ту же сторону, что -и существующая, иначе весь дифф по решениям окажется спором о scope. -**`## Инварианты`** — то, что твоя реализация обязана соблюсти (дословность -хранения, «сохранили — значит приняли» и подобное). **`## Прод и поток`** — объёмы -и представление данных: решение, разумное на сотне записей, неразумно на -миллионе. **`## Карта`** — где конвенции и где файл наблюдений на живых данных. +- **`docs/passport.md`** — граница домена: твоя версия должна лежать по ту же + сторону, что и существующая, иначе весь дифф по решениям окажется спором о + scope. +- **`CLAUDE.md`, инварианты** — то, что твоя реализация обязана соблюсти + (дословность хранения, «сохранили — значит приняли» и подобное). +- **`docs/research/` и `docs/database.md` вместе** — измеренные объёмы и + представление данных. Решение, разумное на сотне записей, неразумно на + миллионе; и то и другое читается вместе, порознь они ничего не решают. +- **`docs/conventions/`** — твоя версия должна быть сравнимой по форме. -**Брифа нет** — пиши свою версию по спеке и конвенциям, но: `critical` по -основанию «нарушен инвариант проекта» не присваивай (инвариантов ты не знаешь, а -именно они чаще всего объясняют чужое решение), объёмы не предполагай и в границы -покрытия дай строку «брифа проекта нет: инварианты и профиль нагрузки прогону -неизвестны, расхождения по этим основаниям не оценивались». Без брифа риск -конкретно этого прохода максимален: твоя версия проще, потому что не знает, чего -проект боится. +Карта «что нужно проходу → где лежит» — +`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`. + +**Деградация поразрядная.** Нет инвариантов в `CLAUDE.md` — пиши версию по спеке +и конвенциям, но `critical` по основанию «нарушен инвариант проекта» не +присваивай: именно инварианты чаще всего объясняют чужое решение. Нет объёмов в +`docs/research/` — не предполагай их. Строка в границы покрытия называет, чего +именно не было. **Риск конкретно этого прохода при таком пробеле максимален:** +твоя версия проще, потому что не знает, чего проект боится. **Тебя запускают по триггеру, а не всегда.** Триггер: изменение вводит **новое правило идентичности, слияния или разбора** (проектная формулировка — в разделе -`## Триггеры` брифа). Вне его твой счёт — самый большой в конвейере (он +`docs/review.md`, если он там записан). Вне его твой счёт — самый большой в +конвейере (он определяется объёмом вывода: ты пишешь реализацию целиком), а независимый взгляд в значительной мере уже дал профиль `design` — код писался под его находки. Если тебя позвали, значит случай тот самый: работай в полную глубину и не экономь на @@ -44,8 +50,8 @@ color: purple Тебе дают: требования из дельта-спеки, сигнатуры соседей, с которыми узел договаривается, назначение узла. Описание внешнего мира (формат входа, поведение -источника) читай в документации проекта и в файле наблюдений на живых данных из -раздела `## Карта` брифа — это описание мира, а не реализации под ревью. +источника) читай в `docs/architecture.md` и в `docs/research/` — это описание +мира, а не реализации под ревью. Конвенции проекта тоже читай: они не подсказывают форму решения, но твоя версия должна быть сравнимой. diff --git a/av-dev-pipeline/agents/review-rubric.md b/av-dev-pipeline/agents/review-rubric.md index 2b4992c..7c8d95e 100644 --- a/av-dev-pipeline/agents/review-rubric.md +++ b/av-dev-pipeline/agents/review-rubric.md @@ -15,20 +15,24 @@ color: purple (точный путь конвейер передаёт в задании). Русская проза, идентификаторы — в оригинале. -## Что берёшь из брифа +## Что берёшь из документов проекта -- **`## Типовые узлы`** — роды узлов этого проекта и специфичные для них свойства. - Это материал для требования «минимум три пункта специфичны для типа узла». -- **`## Инварианты`** и **`## Проект`** — чтобы рубрика не противоречила тому, что - проект защищает и чем он себя ограничил. -- **`## Прецеденты`** — классы дефектов, уже случавшихся здесь: свойство, +- **`docs/review.md`, «Типовые узлы»** — рода узлов этого проекта и специфичные + для них свойства. Это материал для требования «минимум три пункта специфичны + для типа узла». +- **`CLAUDE.md`, инварианты** и **`docs/passport.md`** — чтобы рубрика не + противоречила тому, что проект защищает и чем он себя ограничил. +- **`docs/review.md`, журнал** — классы дефектов, уже случавшихся здесь: свойство, сформулированное по прецеденту, сильнее любого общего. -**Брифа или этих разделов нет** — порождай рубрику по общей практике, но -`critical` по основанию «нарушен инвариант проекта» (в фазе 2) не присваивай и -дай в границы покрытия строку: «брифа проекта нет: рода узлов, инварианты и -прецеденты неизвестны; требование «минимум три пункта специфичны для типа узла» -выполнено по общей практике, а не по этому проекту». +Карта «что нужно проходу → где лежит» — +`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`. + +**Этих документов нет** — порождай рубрику по общей практике, но `critical` по +основанию «нарушен инвариант проекта» (в фазе 2) не присваивай и дай строку: +«`docs/review.md` и инвариантов нет: рода узлов и прецеденты неизвестны; +требование «минимум три пункта специфичны для типа узла» выполнено по общей +практике, а не по этому проекту». ## Порядок фаз обязателен @@ -46,7 +50,7 @@ color: purple - отсортирована по важности, а не по порядку прихода в голову; - **минимум три пункта специфичны для типа узла**, а не общие слова. Ориентиры - по родам узлов (проектные — в брифе): + по родам узлов (проектные — в `docs/review.md`): - *парсер входного формата* — поведение на усечённом и враждебном входе, границы размера, отсутствие паники, детерминизм, судьба незнакомых полей; - *HTTP-обработчик приёма* — валидация формы конверта до записи, лимит тела и @@ -70,15 +74,15 @@ color: purple в середине слияния запись остаётся либо прежней, либо полной», а не «аккуратно работать с контекстом»; - пункты, специфичные для проекта, приветствуются, но не должны вытеснить общие: - если вся рубрика — пересказ инвариантов из брифа, проход выродился в + если вся рубрика — пересказ инвариантов из `CLAUDE.md`, проход выродился в applicative; - **отдельным пунктом — узел, читающий состояние, которое сам же меняет.** Спроси, остаётся ли результат функцией от того, что **уже произошло**, а не от того, в каком порядке исполнялись параллельные операции и когда именно узел посмотрел на состояние. Класс: запрос берёт «последнее выведенное значение» вообще вместо последнего предшествующего — и пересборка перестаёт - воспроизводить состояние. Случаи этого проекта — в разделе `## Прецеденты` - брифа. Тот же вопрос на **готовом коде** задаёт эксплуатационный проход + воспроизводить состояние. Случаи этого проекта — в журнале `docs/review.md`. + Тот же вопрос на **готовом коде** задаёт эксплуатационный проход (вопрос 9); здесь он задаётся дизайну. Выведи рубрику **до** любых находок. Она — часть результата, даже если код diff --git a/av-dev-pipeline/agents/review-specs.md b/av-dev-pipeline/agents/review-specs.md index 9579967..eb9194a 100644 --- a/av-dev-pipeline/agents/review-specs.md +++ b/av-dev-pipeline/agents/review-specs.md @@ -15,22 +15,26 @@ Development на OpenSpec). Оптика — требования, а не ст ключевые слова спек (`SHALL`, `GIVEN/WHEN/THEN`) — в оригинале. Читай реальные файлы перед выводом, ничего не выдумывай. -## Что берёшь из брифа проекта +## Что берёшь из документов проекта -- **`## Инварианты`** — по ним проверяется, отражены ли в спеке задетые свойства, - и по ним же присваивается severity. Цитируй пункт дословно, когда ссылаешься. -- **`## Карта`** — где актуальные спеки, где дельты, где архитектура и **где файл - наблюдений на живых данных**. Там же — **нарезка capability и миграционное - состояние спек**: по какому признаку проект режет capability и какие темы ещё - не переехали из документации в спеки. Без этого пункта непереехавшая тема - читается как пробел в спеке, и находка уходит в пустоту. -- **`## Проект`** — граница домена: требование, переносящее понятие через неё, — - находка в спеку, а не в код. +- **`CLAUDE.md`, инварианты** — по ним проверяется, отражены ли в спеке задетые + свойства, и по ним же присваивается severity. Цитируй пункт дословно, когда + ссылаешься. +- **`docs/architecture.md`** — компоненты и capability, и **что из них уже + переехало в нормативные спеки**. Без этого непереехавшая тема читается как + пробел в спеке, и находка уходит в пустоту. +- **`docs/research/`** — как внешний мир ведёт себя на самом деле. +- **`docs/passport.md`** — граница домена: требование, переносящее понятие через + неё, — находка в спеку, а не в код. -**Брифа нет** — сверяй только спеку с кодом, `critical` по основанию «нарушен -инвариант проекта» не присваивай и дай в границы покрытия строку: «брифа проекта -нет: инварианты, граница домена и состояние переноса capability в спеки -неизвестны; отражение инвариантов в спеке не проверялось». +Пути спек жёсткие: актуальные — `openspec/specs//spec.md`, дельты — +`openspec/changes//specs/`. Карта «что нужно проходу → где лежит» — +`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`. + +**Нет инвариантов в `CLAUDE.md`** — сверяй только спеку с кодом, `critical` по +основанию «нарушен инвариант проекта» не присваивай и дай строку: «инвариантов в +`CLAUDE.md` нет: отражение инвариантов в спеке не проверялось». Нет +`docs/passport.md` — граница домена неизвестна, и это отдельная строка. ## Источник требований @@ -40,19 +44,19 @@ Development на OpenSpec). Оптика — требования, а не ст находка. Дополнительно поднимаешь: `design.md` и `tasks.md` change, затронутые актуальные -спеки, инварианты из брифа. Если тема ещё не перенесена в спеки и живёт только в -документации проекта — источник истины там, и это фиксируется в границах -покрытия. Отдельно: файл наблюдений на живых данных (если он есть в карте) нормой -не является, но именно там записано, как внешний мир ведёт себя на самом деле; -требование, противоречащее наблюдению, — повод для находки в спеку. +спеки, инварианты из `CLAUDE.md`. Если тема ещё не перенесена в спеки и живёт +только в `docs/architecture.md` — источник истины там, и это фиксируется в +границах покрытия. Отдельно: `docs/research/` нормой не является, но именно там +записано, как внешний мир ведёт себя на самом деле; требование, противоречащее +наблюдению, — повод для находки в спеку. ## Режим 1 — дизайн/спеки ДО кода Проверяешь change как артефакт: полнота покрытия постановки; сценарии `GIVEN/WHEN/THEN` без дыр, противоречий и недостижимых веток; scope не раздут и не урезан молча; согласованность с текущими спеками и нарезкой capability; в -спеке отражены **задетые инварианты из брифа** — поимённо, а не «безопасность -учтена». +спеке отражены **задетые инварианты из `CLAUDE.md`** — поимённо, а не +«безопасность учтена». Прогоняй `openspec validate --strict ` сам — это оракул, а не догадка. @@ -111,7 +115,7 @@ Development на OpenSpec). Оптика — требования, а не ст ### 2.4 Право сомневаться в требовании Для верификатора спека обычно аксиома — здесь это ограничение **снято явно**. -Если требование выглядит неверным (противоречит инварианту из брифа, делает +Если требование выглядит неверным (противоречит инварианту из `CLAUDE.md`, делает невозможным штатный сценарий, теряет данные, которых потом не восстановить) — скажи об этом прямо, с последствием. Такая находка всегда `Действие: развилка`: менять спеку — решение человека. diff --git a/av-dev-pipeline/agents/review-triage.md b/av-dev-pipeline/agents/review-triage.md index 9612ea8..21da7ac 100644 --- a/av-dev-pipeline/agents/review-triage.md +++ b/av-dev-pipeline/agents/review-triage.md @@ -22,24 +22,31 @@ color: green ## Вход Сырые выводы всех запущенных проходов, `git diff <база>..HEAD`, **список -запущенных проходов**, профиль и режим прогона, путь к брифу проекта. Дельта-спеки -— по мере надобности. +запущенных проходов**, профиль и режим прогона. Дельта-спеки — по мере +надобности. -Из брифа тебе нужны: **`## Инварианты`** (что делает находку `critical` и что -делает её развилкой), **`## Прод и поток`** (что необратимо — от этого зависит -ранжирование), **`## Прецеденты`** (готовые оракулы: находка того же класса, что -уже воспроизводился здесь, подтверждается ссылкой на прецедент), -**`## Типовые ложноположительные`** (единственный проектный вход в шаг 4), -**`## Недоступно проверке`** — оба подраздела, они целиком уезжают в границы -покрытия и **не сливаются в один список**, — **`## Команды`** (что запускать -запрещено). +Из документов проекта тебе нужны: -**Брифа нет** — работай по общим правилам, но: ни одну находку не поднимай до -`critical` по основанию «нарушен инвариант проекта» (сослаться не на что), -ранжируй по обратимости, выведенной из кода, и назови это предположением. Первой -строкой сводки — «прогон шёл без брифа проекта (<причина>)», и это же идёт в -границы покрытия. Одинаковая строка «брифа нет» без причины перестаёт читаться -на третьей задаче — причину сохраняй. +- **`CLAUDE.md`, инварианты** — что делает находку `critical` и что делает её + развилкой; там же, **что необратимо** (от этого зависит ранжирование) и что + запускать запрещено; +- **`docs/review.md`, журнал** — готовые оракулы: находка того же класса, что уже + воспроизводился здесь, подтверждается ссылкой на запись; +- **`docs/review.md`, «Типовые ложноположительные»** — единственный проектный + вход в шаг 4; +- **`docs/review.md`, «Недоступно проверке»** — оба подраздела, они целиком + уезжают в границы покрытия и **не сливаются в один список**. + +Карта «что нужно проходу → где лежит» — +`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`. + +**Деградация поразрядная, и ты — тот, кто сводит её строки в одну.** Свою часть +тоже называй: нет инвариантов в `CLAUDE.md` — ни одну находку не поднимай до +`critical` по этому основанию (сослаться не на что), ранжируй по обратимости, +выведенной из кода, и назови это предположением. Нет `docs/review.md` — отсев +ложноположительных слепой, и это отдельная строка. **Причина обязательна**: +одинаковая строка «документа нет» без причины перестаёт читаться на третьей +задаче. ## Порядок. Не меняй его @@ -65,12 +72,12 @@ color: green рассуждение о ней ничего не доказывает; - выполнить команду и приложить вывод; - показать поимённое положение гайда, строку конвенции проекта или **дословный - пункт из раздела `## Инварианты` брифа**; -- сослаться на наблюдение в файле живых данных проекта — оно сильнее любого + пункт из раздела инвариантов `CLAUDE.md`**; +- сослаться на наблюдение в `docs/research/` — оно сильнее любого рассуждения о том, «как должно быть». Бюджет — по одной попытке на находку. Не превращай триаж в отдельное -расследование. Ничего не запускай на рабочих данных — запреты в брифе. +расследование. Ничего не запускай на рабочих данных — запреты в `CLAUDE.md`. ### 3. Понижение неподтверждённого @@ -92,7 +99,7 @@ severity: перестановка функций, «лучше вынести в отдельный файл», предложения обобщить работающий частный случай. -**Проектный вход сюда один — раздел `## Типовые ложноположительные` брифа.** +**Проектный вход сюда один — «Типовые ложноположительные» в `docs/review.md`.** Там перечислены находки, которые в этом проекте выглядят убедительно и всегда неверны: они выбрасываются со ссылкой на пункт и с пометкой почему, а не «смягчаются». Классический обитатель раздела — предложение «нормализовать» то, @@ -105,8 +112,8 @@ severity: Не по severity как таковой и не по числу нашедших проходов. **Порча и потеря данных с низкой вероятностью важнее гарантированного неудобства**, и перевес тем -сильнее, чем менее обратимы данные в этом проекте (раздел `## Прод и поток` -брифа). Падение сервиса, наоборот, обычно обратимо. +сильнее, чем менее обратимы данные в этом проекте (`CLAUDE.md`, что необратимо). +Падение сервиса, наоборот, обычно обратимо. Второй по весу класс — **молчание**: отказ, о котором владелец не узнает, дороже отказа, который виден сразу. @@ -128,7 +135,7 @@ severity: - **инлайн** — оркестратор чинит сам, не спрашивая и не логируя. Правка локальна, решение однозначно, объём right-size. - **развилка** — цена сопоставима с переработкой, либо меняется scope, либо - трогается инвариант из брифа, либо надо менять спеку. Формулируй готовым + трогается инвариант из `CLAUDE.md`, либо надо менять спеку. Формулируй готовым вопросом с 2–3 вариантами: оркестратор перенесёт его почти дословно. Сомневаешься — ставь `развилка`. Ошибка в сторону лишнего вопроса дешевле @@ -152,7 +159,7 @@ severity: - какие **не** запускались и почему (профиль, бюджет, недоступный инструмент, остановленный прогон); - что каждый запущенный проход **не мог проверить в принципе** — из его charter'а; -- **что осталось целиком на человеке** — раздел `## Недоступно проверке` брифа, +- **что осталось целиком на человеке** — «Недоступно проверке» из `docs/review.md`, **двумя отдельными списками**: «не проверит ни один проход» и «перестали проверять сознательно». Слитый список бесполезен: при следующем промахе первый вопрос — «не тот ли это класс, который мы перестали проверять», и ответить на @@ -160,8 +167,10 @@ severity: инцидентов, поведение под реальным потоком, поведение внешних систем в их версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта функциональность вообще»; -- если брифа не было — строку об этом **с причиной**: инварианты, модель угроз и - профиль нагрузки прогону были неизвестны, потому что <причина>. +- **каких документов проекта не хватило** — строкой на каждый, **с причиной**: + «`docs/security.md` в проекте нет», «есть, но периметр не назван». Строки + приходят из проходов; слить их в одну «документации не было» нельзя — + деградация поразрядная, и разные пробелы чинятся разным. Формулировка «критичных проблем не обнаружено» **запрещена** без этой секции: она потребляет ощущение проверенности, ничего не гарантируя, и это хуже, чем diff --git a/av-dev-pipeline/skills/project-brief/SKILL.md b/av-dev-pipeline/skills/project-brief/SKILL.md deleted file mode 100644 index 793c53d..0000000 --- a/av-dev-pipeline/skills/project-brief/SKILL.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -name: project-brief -description: Заводит или обновляет бриф ревью проекта (docs/review-brief.md) — файл, откуда конвейер ревью берёт инварианты, команду гейта, модель угроз, объёмы, прецеденты и карту проекта. Вызывать, когда брифа нет (это обнаруживают review-pipeline, task-pipeline и task-batch на старте), когда сменился гейт или появилась новая зависимость, и по прямой просьбе завести или обновить бриф. ---- - -# Заведение брифа проекта - -Бриф — **предмет** ревью: что здесь нельзя нарушать, чем краснеет гейт, сколько -данных реально проходит, что необратимо. Без него конвейер работает в -деградированном режиме: `critical` по основанию «нарушен инвариант проекта» -недоступен ни одному проходу, числа объёма не используются, архитектурный проход -теряет свой главный критерий (граница домена) и вырождается в общее мнение. - -Поэтому заведение брифа — **шаг, а не документ**. Этот скилл его выполняет. - -- Контракт разделов — [контракт брифа](../review-pipeline/references/project-brief.md). -- Форма и образцы заполнения — [шаблон](../review-pipeline/references/brief-template.md). - -## Когда вызывается - -- **Автоматически**, без спроса: `av-dev-pipeline:review-pipeline`, - `av-dev-pipeline:task-pipeline` и `av-dev-pipeline:task-batch` разрешают путь к - брифу на старте и, не найдя его ни по одному пути, зовут этот скилл. Это не - развилка и не повод остановиться — заведение брифа делается молча, как любая - другая механика. -- **По событию:** сменился гейт; появился новый контур, зависимость или источник - входа; в журнал ревью попала запись вида «проход не мог этого знать»; свойство - промоутнулось в правило линтера (тогда пункт из брифа **вычёркивается**). -- **По просьбе человека.** - -Планового пересмотра нет. - -## Шаг 1. Убедиться, что брифа действительно нет - -Порядок разрешения пути — тот же, что у конвейера: - -1. путь, названный в задании; -2. `docs/review-brief.md`; -3. `.claude/review-brief.md`. - -Файл есть, но неполон (нет обязательного раздела, раздел пуст, числа без -провенанса) — это **не** заведение с нуля: дозаполняй недостающее и не переписывай -то, что уже выверено. Разошедшийся бриф хуже отсутствующего, но переписанный -поверх выверенного — хуже разошедшегося. - -## Шаг 2. Собрать материал из проекта - -Бриф **выводится из проекта, а не сочиняется**. Источники по убыванию плотности: - -| Раздел брифа | Откуда берётся | -|---|---| -| `## Проект` | `CLAUDE.md` / `AGENTS.md`, паспорт или README — абзац «что это и чего оно не делает» | -| `## Инварианты` | раздел инвариантов `CLAUDE.md`, архитектура, журнал решений; **цитируются формулировкой** | -| `## Гейт` | `Taskfile.yml` / `Makefile` / `justfile` / CI — сама цель гейта, состав её шагов, коды и логи | -| `## Команды` | тот же файл задач: карта проекта, поднять вживую, тесты, дорогое вне гейта, запрещённое | -| `## Прод и поток` | документация по деплою и архитектуре, конфиг и его образец, схема БД, файл наблюдений на живых данных | -| `## Модель угроз` | конфиг (токены, права), раскладка файлов на диске, схема ключей, места приёма недоверенного входа | -| `## Карта` | дерево репозитория: спеки, конвенции, архитектура, журнал ревью, миграции, `testdata`, основная ветка | -| `## Типовые узлы` | дерево пакетов: какие рода узлов реально есть | -| `## Прецеденты` | журнал ревью, архивные отчёты триажа, `git log` по починкам | -| `## Недоступно проверке` | журнал ревью (что решили не проверять) плюс общий список из контракта | - -Прочитай `CLAUDE.md` и всё, на что он ссылается, **до** того, как писать первую -строку. Бриф, собранный из одного файла, повторяет его и потому бесполезен. - -## Шаг 3. Заполнить - -Идёшь по контракту раздел за разделом. Четыре правила ведения, из-за которых -брифы портятся чаще всего: - -1. **Не пересказывай документацию.** Факт, записанный в `CLAUDE.md` или в - архитектуре, попадает сюда ссылкой и одной строкой сути. Исключение — раздел - инвариантов: он цитируется дословно, потому что по нему присваивается severity. -2. **Числа — с провенансом.** «Тела доходили до 42 МБ (замер, - `docs/local-research.md`)». Число без источника проход обязан превратить в - условие, то есть оно бесполезно. -3. **Пустой пункт называется пустым.** «Внешних зависимостей нет — смотри на диск - и на СУБД» стоит целого прохода: без этой строки эксплуатационный проход - потратит обязательный вопрос впустую или выдумает зависимость. То же про - угрозы вне модели, про отсутствующие прецеденты, про отсутствие наблюдателя. -4. **Не выдумывай четыре вещи.** Измеренные числа; периметр модели угроз; то, что - в этом проекте необратимо; и **кто обязан гонять дорогую проверку вне гейта** - — всё это из кода не выводится. Не нашёл в документации — **спроси человека на - шаге 4**, а до ответа напиши пункт словом «неизвестно» с пометкой, что он ждёт - ответа. Придуманное число здесь дороже отсутствующего: проход сошлётся на него - как на замер. -5. **Что выведено, а не прочитано, — помечай.** Чаще всего это severity у - инвариантов: проекты редко пишут её рядом с формулировкой, и её приходится - выводить по обратимости последствия. Пометка «выведена по обратимости» стоит - трёх слов и сообщает проходу, чьё это суждение, — а он по ней ставит - `critical`. То же для периметра, восстановленного из конфига, и для чисел, чей - источник по ссылке не подтвердился. - -## Шаг 4. Показать человеку - -Бриф — единственный файл, который конвейер **читает как истину**, поэтому он -показывается, а не заводится молча: - -- покажи готовый файл (или дифф, если это обновление); -- отдельным коротким списком назови, **что выведено из проекта**, а что - **предположено или осталось неизвестным** — по этим строкам человек и правит; -- если на шаге 3 остались вопросы из класса «не выдумывай три вещи», задай их - здесь, разом и с вариантами. - -Ответа ждать не обязательно: работа продолжается по заведённому брифу, а -неизвестные пункты честно стоят словом «неизвестно» — проход прочитает его как -деградацию по этому пункту, а не как факт. - -**Бриф ведёт проект.** Файл кладётся в репозиторий проекта и коммитится вместе с -той работой, в ходе которой заведён. Плагин его больше не правит — он только -читает. - -## Шаг 5. Вернуться в вызвавший шаг - -Скажи вызвавшему скиллу путь к брифу — дальше конвейер передаёт его каждому -проходу готовым, и деградированный режим не включается. - -## Если завести нельзя - -Заведение отменяется ровно в трёх случаях: репозиторий доступен только на чтение; -человек прямо сказал брифа не заводить; проект настолько чужой, что вывести -инварианты неоткуда. Тогда — деградированный режим по контракту: строка в границы -покрытия и запрет на `critical` по основанию «нарушен инвариант проекта». - -Во всех остальных случаях бриф заводится. «Задача маленькая, брифа не надо» — -не основание: бриф заводится один раз на проект, а деградированный режим платит -на каждой задаче. diff --git a/av-dev-pipeline/skills/review-pipeline/SKILL.md b/av-dev-pipeline/skills/review-pipeline/SKILL.md index 0238c87..6828fb8 100644 --- a/av-dev-pipeline/skills/review-pipeline/SKILL.md +++ b/av-dev-pipeline/skills/review-pipeline/SKILL.md @@ -1,6 +1,6 @@ --- name: review-pipeline -description: Конвейер ревью изменения — детерминированный гейт, сверка с дельта-спеками в обе стороны, враждебные постановки и эксплуатационный постмортем, независимая реализация по триггеру, архитектура и обязательный триаж. Проходы гонятся последовательно; параллельно — только по явной просьбе и с явно названным набором. Проектная специфика приходит из файла-брифа. Вызывается из task-pipeline (чекпоинты ревью), из task-batch (финальная сверка) и отдельно — профилем design на предложении ДО кода. +description: Конвейер ревью изменения — детерминированный гейт, сверка с дельта-спеками в обе стороны, враждебные постановки и эксплуатационный постмортем, независимая реализация по триггеру, архитектура и обязательный триаж. Проходы гонятся последовательно; параллельно — только по явной просьбе и с явно названным набором. Проектная специфика приходит из документов канона av-dev-pm. Вызывается из task-pipeline (чекпоинты ревью), из task-batch (финальная сверка) и отдельно — профилем design на предложении ДО кода. --- # Конвейер ревью @@ -34,16 +34,17 @@ description: Конвейер ревью изменения — детермин Конвейер опирается на внешнюю обвязку и без неё работает не целиком. Проверь это один раз, при установке плагина в проект: -- **OpenSpec и скиллы `opsx:*`.** Профиль `design`, проход `review-specs` и +- **OpenSpec — жёсткая предпосылка, а не опция.** Профиль `design`, проход + `review-specs` и вызывающий пайплайн задачи завязаны на дельта-спеки (`openspec/changes//specs/*/spec.md`), на актуальные спеки (`openspec/specs/`) и на `openspec validate --strict`. В проекте без OpenSpec шаги, зовущие `opsx:explore` / `opsx:propose` / `opsx:apply` / `opsx:archive`, упадут на «нет такого скилла», а `review-specs` останется без источника - требований. Такой проект либо подключает OpenSpec, либо сознательно не зовёт - `review-specs` и профиль `design` — и тогда это идёт строкой «не запускался» в - границы покрытия, как любой другой пропуск. -- **Бриф проекта** — см. следующий раздел. Заводится скиллом, а не руками. + требований. **Проект без OpenSpec этим конвейером не проверяется** — подключай + OpenSpec, а не понижай прогон: ветка деградации здесь не пишется, потому что + непроверенная ветка деградации хуже честного отказа. +- **Документы канона** — см. следующий раздел. - **Проектные копии этих скиллов и агентов удаляются при установке.** Если в проекте уже лежат свои `.claude/skills/review-pipeline`, `.claude/skills/task-pipeline`, `.claude/skills/task-batch` или @@ -51,39 +52,35 @@ description: Конвейер ревью изменения — детермин в устаревшую проектную копию, молча и без признаков подмены. По той же причине **скиллы этого плагина зовутся с пространством имён**: `av-dev-pipeline:review-pipeline`, `av-dev-pipeline:task-pipeline`, - `av-dev-pipeline:task-batch`, `av-dev-pipeline:project-brief`. + `av-dev-pipeline:task-batch`. -## Что конвейер защищает — приходит из брифа +## Что конвейер защищает — приходит из документов проекта -Проходы общие, а нарушать нельзя проектное. Список инвариантов, команду гейта, -объёмы, прецеденты и модель угроз конвейер **не знает** — он читает их в брифе -проекта: [references/project-brief.md](references/project-brief.md) описывает -контракт, [references/brief-template.md](references/brief-template.md) — образец -заполнения. +Проходы общие, а нарушать нельзя проектное. Инварианты, команду гейта, объёмы, +прецеденты и модель угроз конвейер **не знает** — он читает их в документах +канона `av-dev-pm`, **напрямую и по жёстким путям**. Отдельного файла-брифа нет: +пути известны, посредник не нужен, а второй дом для тех же фактов разошёлся бы и +выглядел актуальным. -Разреши путь к брифу один раз, в начале прогона: путь из задания → -`docs/review-brief.md` → `.claude/review-brief.md`. Дальше передавай готовым. +Карта «что нужно проходу → где лежит» — +[references/project-facts.md](references/project-facts.md). Прочитай её до +раздачи заданий; там же таблица поразрядной деградации. -**Брифа нет по всем трём путям — заведи его, а не понижай прогон.** Вызови Skill -**`av-dev-pipeline:project-brief`**: он соберёт бриф из `CLAUDE.md`, архитектуры, -файла задач и конвенций, покажет человеку и вернёт путь. Это механика, а не -развилка: спрашивать разрешения не нужно, и остановка прогона тут не -предусмотрена. Заведение стоит одного шага один раз на проект — деградированный -режим платит на каждой задаче. +**Деградация поразрядная, а не всё-или-ничего.** Документа нет — деградирует то, +что из него читалось, и только оно: нет `docs/security.md` — слабеет +`adversary`; нет `docs/research/` — числа неизвестны трём проходам; нет +инвариантов в `CLAUDE.md` — `critical` по основанию «нарушен инвариант проекта» +не присваивается никем. Каждый проход пишет **свою** строку в границы покрытия, с +**причиной**; триаж сводит их и не сливает в одну. -**Деградированный режим — исход, а не умолчание.** Он включается ровно тогда, -когда бриф завести не удалось (репозиторий на чтение, человек прямо запретил, -инварианты вывести неоткуда): `critical` по основанию «нарушен инвариант -проекта» никем не присваивается, числа объёма не используются, и в границы -покрытия уезжает строка «брифа проекта нет, завести не удалось: <причина>». -Причина обязательна — без неё строка неотличима от «мы просто не стали». +**Документов канона нет вовсе** — проект не приведён к канону. Скажи это строкой +и предложи скилл `av-dev-pm:canon`: одна операция на проект против деградации на +каждой задаче. Прогон при этом не останавливается. ## Что получает каждый проход -Задание любому проходу состоит из шести вещей, и первая — главная: без брифа -проход теряет предмет проверки и уходит в деградированный режим. +Задание любому проходу состоит из пяти вещей: -- **бриф** — путь (разрешён или заведён на старте, см. выше); - **контракт находок** — путь к [references/finding-contract.md](references/finding-contract.md) (в установленном плагине — `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/`); @@ -146,7 +143,7 @@ description: Конвейер ревью изменения — детермин |---|---|---|---| | `quick` | багфикс, локальная правка, доки | 0, 1, 5 | 4 | | `standard` | новая функциональность в существующем пакете | 0, 1, 2, 5 | 6 | -| `deep` | новый пакет, изменение публичного контракта, миграция схемы, трогает инварианты брифа | 0, 1, 2, 3, 4, 5 | 7–8 | +| `deep` | новый пакет, изменение публичного контракта, миграция схемы, трогает инварианты проекта | 0, 1, 2, 3, 4, 5 | 7–8 | | `design` | **до кода**, на предложении | specs + rubric + architecture (см. ниже) | 3 | **Состав сверяется по этой таблице до коммита.** Реестр из трёх-восьми пунктов @@ -168,8 +165,8 @@ description: Конвейер ревью изменения — детермин - иначе → `quick`. Что именно в этом проекте считается публичным контрактом и какие пути означают -`deep` — раздел `## Триггеры` брифа. Он **уточняет** правило, а не отменяет его: -если триггеров в брифе нет, работает список выше. +`deep`, проект может уточнить в `docs/review.md`, разделе настройки конвейера. Это +**уточнение**, а не отмена: не записано — работает список выше. Профиль объявляется в отчёте. Понижение профиля — решение оркестратора, и оно попадает в границы покрытия строкой «профиль понижен до X, потому что …». @@ -207,8 +204,8 @@ description: Конвейер ревью изменения — детермин роста файлов журнала, длительность транзакции. Два меряющих прохода на одной машине соревнуются за диск, CPU и за саму СУБД и выдают числа, которые не воспроизведутся. Это не гипотеза: правило выведено из находок, целиком - державшихся на таких замерах, — у каждого проекта они свои и лежат в разделе - `## Прецеденты` его брифа. Число, снятое под конкурентную нагрузку от соседнего + державшихся на таких замерах, — у каждого проекта они свои и лежат в журнале + `docs/review.md`. Число, снятое под конкурентную нагрузку от соседнего прохода, — это находка с испорченным оракулом, а её опровержение стоит дороже всего выигрыша от параллельности. - **Машина одна.** Рядом идёт задача, поднят сервис, гоняется гейт или дорогая @@ -251,7 +248,7 @@ description: Конвейер ревью изменения — детермин ## Стадия 0 — Gate (обязательна во всех профилях) -Агент `review-gate`. Запускает команду гейта из раздела `## Гейт` брифа и +Агент `review-gate`. Запускает команду гейта из семантики гейта в `CLAUDE.md` и интерпретирует вывод. **Пока гейт красный — опиниативные проходы не запускаются.** Оркестратор чинит и @@ -267,7 +264,7 @@ description: Конвейер ревью изменения — детермин линтеры и детектор гонок. Пропуск при этом не молчит — он виден в сводке с причиной и уезжает в границы покрытия, как и любой другой `SKIP`. -Шаги, которые красят гейт безусловно, перечислены в брифе с причиной. Проходу +Шаги, которые красят гейт безусловно, перечислены в `CLAUDE.md` с причиной. Проходу запрещено списывать такой отказ в мелочь. ## Стадия 1 — Conformance (обязательна во всех профилях) @@ -279,11 +276,10 @@ description: Конвейер ревью изменения — детермин - `review-specs` — критерий взят из **дельта-спек предлагаемого изменения**, а не из proposal, сообщения коммита или описания задачи. Сверка двунаправленная; направление `code → spec` важнее. -- `review-code` — критерий взят из конвенций проекта: файла или каталога файлов, - путь — раздел `## Карта` брифа. Берётся только та их часть, которая **не - выражается правилом**: - механизируемое уже проверила стадия 0. Что именно механизировано, тот же раздел - брифа перечисляет — повторять это проходом вредно. +- `review-code` — критерий взят из конвенций проекта, каталог + `docs/conventions/`. Берётся только та их часть, которая **не выражается + правилом**: механизируемое уже проверила стадия 0. Что именно механизировано, + перечисляет `conventions/README.md` — повторять это проходом вредно. Recall обоих равен длине их источника — это и есть предел applicative-проходов, ради которого существует стадия 2. @@ -309,15 +305,17 @@ Recall обоих равен длине их источника — это и е **прогнать**, второй смотрит ось времени и эксплуатации, которую не смотрит никто другой. -Материал обоим даёт бриф: `## Модель угроз` — враждебному, `## Прод и поток` — -эксплуатационному. Без этих разделов стадия вырождается в общие места. +Материал берётся из документов: `docs/security.md` — враждебному, +`docs/architecture.md` плюс **`docs/research/` и `docs/database.md` вместе** — +эксплуатационному. Последние два сшивает сам проход: число без настройки не с чем +сравнить. Без этих документов стадия вырождается в общие места. ## Стадия 3 — Independent reimplementation (`deep`, по триггеру) - `review-reimpl` — пишет свою реализацию, не открывая существующую, затем диффит по решениям. **Запускается по триггеру, а не всегда:** изменение вводит новое правило идентичности, слияния или разбора (проектная формулировка - триггера — в разделе `## Триггеры` брифа). Это самый дорогой проход конвейера + триггера — в `docs/review.md`, если записана). Это самый дорогой проход конвейера (его счёт определяется объёмом вывода — он пишет реализацию целиком), а вне этого триггера независимый взгляд в значительной мере уже дал профиль `design`: код писался под его находки. Триггер выбран по факту: единственный раз, когда @@ -328,7 +326,7 @@ Recall обоих равен длине их источника — это и е Агент `review-architecture`. Получает **вход шире диффа**: дерево пакетов с назначением, граф внутренних зависимостей, инвентарь существующих концепций. -Команду, которая это готовит, даёт раздел `## Команды` брифа; нет команды — +Команду, которая это готовит, даёт раздел команд `CLAUDE.md`; нет команды — проход собирает карту сам и говорит об этом в границах покрытия. Главный вопрос — концептуальная целостность и **второй способ** делать то, что @@ -400,7 +398,7 @@ Recall обоих равен длине их источника — это и е у него свой формат, своя нарезка и свои правила дублей. Мелочь класса `nit` идёт в урожай одной пачкой, а не записью на находку. - `Promote candidates` — по процедуре [references/promote.md](references/promote.md): - находка → конвенция → правило линтера → **удаление из конвенций и из брифа**. + находка → конвенция → правило линтера → **удаление формулировки из конвенций**. Третий шаг обязателен. - Дефект, проскочивший ревью и всплывший позже, идёт в журнал проекта ([references/review-journal.md](references/review-journal.md)) — сразу, не @@ -421,8 +419,9 @@ Recall обоих равен длине их источника — это и е Согласие нескольких проходов — **не подтверждение**: это один источник, высказавшийся несколько раз. Совпадение повышает приоритет, но не `confidence`. -Что недоступно **этому** проекту принципиально — перечисляет раздел -`## Недоступно проверке` брифа, и он целиком уезжает в границы покрытия. +Что недоступно **этому** проекту принципиально — перечисляет «Недоступно +проверке» в `docs/review.md`, и оба его подраздела целиком уезжают в границы +покрытия. Независимо от проекта недоступно: - поведение внешних систем в их будущих версиях; @@ -442,9 +441,9 @@ Recall обоих равен длине их источника — это и е ## Ссылки -- Skill `av-dev-pipeline:project-brief` — заведение и обновление брифа. -- [references/project-brief.md](references/project-brief.md) — контракт брифа проекта. -- [references/brief-template.md](references/brief-template.md) — шаблон брифа. +- [references/project-facts.md](references/project-facts.md) — что нужно проходу + и где это лежит в документах проекта; таблица поразрядной деградации. +- Skill `av-dev-pm:canon` — приведение проекта к канону документов. - [references/finding-contract.md](references/finding-contract.md) — контракт находок. - [references/promote.md](references/promote.md) — промоут находка → конвенция → правило → удаление. - [references/calibration.md](references/calibration.md) — калибровка инъекцией, вердикты keep/retune/drop. diff --git a/av-dev-pipeline/skills/review-pipeline/references/brief-template.md b/av-dev-pipeline/skills/review-pipeline/references/brief-template.md deleted file mode 100644 index e94ceaf..0000000 --- a/av-dev-pipeline/skills/review-pipeline/references/brief-template.md +++ /dev/null @@ -1,289 +0,0 @@ -# Шаблон брифа проекта - -Образец заполнения. Контракт разделов — в -[project-brief.md](project-brief.md); заводит бриф по этому образцу скилл -`av-dev-pipeline:project-brief` — руками копировать не надо, но читать полезно. - -Курсивом даны пояснения — их из готового брифа убирают. Примеры взяты из двух -разных проектов (коллектор данных с непрерывным потоком и связующий сервис вокруг -внешних демонов), чтобы было видно, как один и тот же раздел выглядит при разной -природе проекта. - ---- - -## Проект - -*Абзац: что делает — и чего не делает.* - -> Коллектор выгрузок с телефона. Принимает доставки, хранит их и отдаёт другим -> сервисам. Это **хранилище, а не аналитика**: принять, дедуплицировать, -> сохранить, отдать. Не переименовывать поля источника, не интерпретировать -> значения; свёртка считается только в ответе на запрос. - -> Связующий сервис между качалкой и медиасервером: принимает задание, качает, -> распознаёт содержимое, раскладывает файлы ссылками. **Не медиатека и не -> плеер** — ничего не хранит сверх метаданных о раскладке. - -## Инварианты - -*Проверяемое свойство + последствие + severity по умолчанию. Цитируются -формулировкой. Severity проект обычно не пишет — тогда она выводится по -обратимости и помечается: «по умолчанию `critical` (выведена по обратимости)».* - -- **Точка сохраняется дословно.** Незнакомое поле не отбрасывается, число не - округляется при записи. Нарушение — необратимая потеря: сырой архив живёт - 14 дней, дальше истина только в свёртке. По умолчанию `critical`. -- **Источник неприкосновенен.** Только `mkdir`/`link(2)`/`unlink` собственных - ссылок; файлы под каталогом загрузок не трогаются никогда. Нарушение — - повреждение чужих данных, необратимое. По умолчанию `critical`. -- **Сохранили — значит приняли.** Код ответа отражает доставку, а не разбор: - непонятое содержимое — `200`, тело уже на диске. Нарушение стоит доставки, - которую отправитель не повторит. По умолчанию `critical`. -- **Секреты и данные пользователя не в логах.** Тело запроса — только на `DEBUG` - и с обрезкой. По умолчанию `critical`. -- **Агрегации при записи нет.** Нарушение искажает историю молча и - диагностируется только сверкой с внешним источником, то есть месяцами позже. - По умолчанию `major`, `critical` — если испорченное невосстановимо. - -## Гейт - -- **Команда:** `task gate BASE=<база>`; база по умолчанию — - `git merge-base HEAD master`, на `master` — `HEAD~1`. -- **Логи шагов:** `tmp/gate/<шаг>.log`. Сводка печатает `OK`/`FAIL`/`WARN`/`SKIP`; - краснит гейт только `FAIL`. -- **Шаги:** сборка, `vet`, линтеры, форматирование, тесты, повторный прогон на - флаки, `-race`, покрытие изменённых строк, накат миграций с нуля, поиск - секретов, `govulncheck`. -- **Красят безусловно** *(перечислить с причиной — это главная часть раздела)*: - - `no-user-data` — файл из каталога данных попал под контроль версий: убрать - обычным коммитом уже нельзя; - - `config-samples` — структура конфига изменилась, а образец нет: забытое поле - обнаруживается не тестом, а тем, что через полгода о нём никто не знает; - - `migrations` — миграции не накатываются с нуля: восстановление перестаёт - работать ровно тогда, когда оно нужно; - - `er-schema` — миграция тронута, а схема в документации не обновлена. -- **Чего в гейте намеренно нет:** прогон на живом корпусе (`task verify:archive`) - — минута работы и данные, которых нет ни на какой другой машине. У этой - проверки краснота не видна никому до следующей задачи, которая до неё - дотянется, — говори об этом в границах покрытия. - -## Команды - -- Карта проекта для архитектурного прохода: `task review:context > tmp/review-context.md` -- Поднять изменение вживую: `task restart`, логи — `task logs` -- Тесты и линт: `task test`, `task lint` -- **Дорогое вне гейта, с адресатом:** `task verify:archive` (минута, живые - данные). **Кто и когда обязан:** пайплайн задачи — после любого изменения - разбора входного формата или правила слияния, до архивации change; вручную — - человек перед выкладкой. Не прогонялась — строка в границы покрытия, а не - молчание. -- **Запускать запрещено:** ничего, что пишет в `./data`, в рабочую БД и в боевой - каталог архива. Замеры — только на копиях в `./tmp`. - -## Прод и поток - -*Первая строка — главный вопрос эксплуатации этого проекта.* - -> **Главный вопрос:** поток идёт непрерывно и молча, отправитель об отказе не -> узнает и не повторит — значит, дороже всего тихо потерянная доставка, а не -> упавший сервис. - -> *(В сервисе, который сам опрашивает чужих демонов, первая строка была бы -> противоположной: «главный вопрос — что происходит, когда внешний сервис -> отвечает медленно, а не когда он упал».)* - -- **Где:** один статический бинарь в контейнере на домашнем сервере, перед ним - обратный прокси с TLS, SQLite на диске. Ни оркестратора, ни реплик, ни дежурной - смены. -- **Внешние зависимости и как каждая отказывает:** прокси — рвёт соединение на - длинном теле; диск — заполняется и тормозит; СУБД — отдаёт «занято» под - параллельной записью; приложение-источник на телефоне — молча перестаёт слать. - *(В другом проекте здесь были бы качалка, медиасервер, LLM и база метаданных, и - каждая — со своим «отвечает медленно», а не только «упала».)* - *(Если зависимостей нет — так и пишут: «внешних зависимостей нет, смотри на - диск и на СУБД». Пустой пункт называется пустым.)* -- **Кто заметит отказ:** один пользователь-владелец, в лучшем случае вечером, а - скорее не заметит вовсе. -- **Характер потока:** телефон шлёт непрерывно и молча; обратной связи у - отправителя нет, об отказах он не сообщает, расписание плавает. Тихо - сломавшаяся доставка — главный эксплуатационный риск. -- **Представление данных и настройки хранилища:** запись — сжатый BLOB, читается - и пересобирается целиком на каждой операции (`internal/store`); журнал СУБД — - WAL; таймаут занятости — 5000 мс (`config.example.toml`); лимит тела приёма — - 64 МБ; ретеншен сырого архива — 14 дней. -- **Числа (с провенансом):** нижний слой — порядка 135 тыс. точек в сутки - (замер, `docs/local-research.md`); тела доходили до 42 МБ (там же); запись — - read-modify-write под конкурентными доставками (`docs/architecture.md`). -- **Обратимость:** падение сервиса обратимо — отправитель дошлёт широким - проходом. Потеря или порча точки необратима. Поэтому тихая порча весит больше, - чем «сервис вернул 500». - -## Модель угроз - -*Первая строка — периметр.* - -> **Периметр:** сервис открыт наружу через обратный прокси, недоверенным считается -> всё, что приходит по HTTP. Злоумышленник в локальной сети — вне периметра. - -> *(У сервиса в доверенном контуре первая строка противоположна: «контур -> доверенный, публичного интернета здесь нет — не выдумывай его; недоверенное -> здесь — то, что отдают внешние демоны и трекеры».)* - -> *(Контур ещё не развёрнут — тогда периметров два: «целевой — за прокси с TLS; -> сегодняшний — только локальная машина, токены пусты осознанно. **Находки -> строятся против целевого**, отсутствие TLS сегодня находкой не является».)* - -- **Недоверенное:** тело доставки целиком (имена метрик, единицы, формы точек, - метки времени, глубина вложенности, размер); заголовки доставки, часть которых - участвует в решениях; содержимое архива внешнего экспорта (имена файлов внутри - zip мы не формировали); параметры читающего API. -- **Из чего строятся пути и ключи:** файл сырого архива — - `raw/ГГГГ/ММ/ДД/.json.gz`, дата берётся из времени приёма, имя — из - генератора идентификаторов; ключ записи — `метрика + слой + начало + конец`, - источник в ключ не входит. -- **Разграничение:** статические токены в `Authorization: Bearer`, раздельные на - запись и на чтение; конфиг под `0600`. -- **Что дороже:** данные пользователя дороже токена. Путь, по которому значение - доезжает до лога выше `DEBUG`, до ответа с ошибкой или до `testdata` в git, — - полноценная находка, а не замечание по гигиене. -- **Вне модели:** злоумышленник в локальной сети; вредоносный оператор; - компрометация поставщика данных; мультиарендность. Находки этих классов не - выводятся — они никогда не будут исправлены. - -## Карта - -- **Основная ветка:** `master`. От неё берутся ветки задач, в неё вливается батч, - база диффа по умолчанию — `git merge-base HEAD master` (на самой ветке `HEAD~1`). -- Актуальные спеки: `openspec/specs//spec.md` -- Дельта-спеки изменения: `openspec/changes//specs/*/spec.md` -- **Нарезка capability и что из неё переехало в спеки:** режем по домену - (`ingest`, `storage`, `read-api`, `mcp`), а не по транспорту. В актуальные - спеки перенесены `ingest` и `storage`; `read-api` описан только в - `docs/architecture.md`, `mcp` — пока только в коде. Пробел в спеке по этим двум - темам — не находка, а известное состояние. -- Конвенции прозой: `docs/conventions.md` *(в другом проекте это каталог из - нескольких файлов — тогда перечисляют все: - `docs/conventions/{logging,errors,config,database,web-ui}.md`)*. Механизировано - и потому **не проверяется проходом по конвенциям**: форма логов, - `fmt.Print*`/`os.Getenv`/`time.Now` мимо единых точек, сравнение ошибок, - сторонние пакеты ошибок — всё это правила в `.golangci.yml`. -- Архитектура и решения: `docs/architecture.md` -- **Наблюдения на живых данных:** `docs/local-research.md` — что реально шлёт - источник и чем это расходится с его документацией. *(Не ведём — так и пишут: - «наблюдений на живых данных не ведём».)* -- Журнал проскочивших дефектов: `docs/review-journal.md` -- **Единые точки:** идентификаторы — `internal/ident`; время — `store.Now()`; - разбор дат входного формата — один парсер в `internal/parse`; маппинг доменной - ошибки в код ответа — одна точка в `internal/httpapi`; путь приёма — `ingest`, - общий для HTTP и CLI. Инвентарь целиком выгружает `task review:context`. -- **Нумерованные артефакты:** миграции — `internal/store/migrations/NNNN_*.sql`, - номер монотонный, следующий свободный смотреть там же. -- Задачи: `docs/backlog/` *(пайплайн только читает и сообщает исход)* -- Реальные пакеты для тестов разбора: `internal/parse/testdata` — там данные - пользователя с вычищенными токенами, наружу не копировать -- Временное: `./tmp` (не системный `/tmp`) -- **Не трогать:** `./data` — боевой архив и БД - -## Типовые узлы - -*Род узла + 3–5 специфичных проверяемых свойств.* - -- **Разбор входного формата** — поведение на усечённом и враждебном входе, - границы размера, отсутствие паники, детерминизм, судьба незнакомых полей. -- **HTTP-обработчик приёма** — валидация формы конверта до записи, лимит тела и - архивная бомба, что попадает в ответ, а что в лог, отсутствие доменной логики - в транспорте. -- **Обработчик читающего API** — предсказуемость размера ответа, поведение при - пустом диапазоне, коды ответа на невозможный запрос. -- **Репозиторий** — границы транзакции, конкурентная запись того же ключа, - откуда берутся время и id, что возвращается при отсутствии записи, - идемпотентность повторной записи. -- **Файловое хранилище с ретеншеном** — атомарность записи, поведение при - неполной записи и нехватке места, что удаляется и по какому критерию, можно ли - удалить лишнее. -- **CLI-команда пересборки** — идемпотентность повторного прогона, поведение при - отмене на середине, что остаётся после падения, отчёт для человека. -- **Клиент внешнего сервиса** — таймаут, протяжка `context`, поведение при - «медленно» против «упало», ретраи и их граница. - -## Прецеденты - -*Воспроизведённые случаи этого проекта: класс — симптом — чем воспроизведён — -чем закончилось. Прецедентов нет — так и пишут: «прецедентов не накоплено».* - -- **Вырожденный ответ библиотеки, неотличимый от штатного.** Симптом: пересборка - докладывала «журнал разобран целиком», а часть записей не доезжала. Причина: - контрольная точка журнала СУБД под занятой блокировкой возвращала `-1` вместо - пары чисел, и сравнение `-1 >= -1` читалось как успех — 1492 тика из 5502. - Воспроизведено экспериментом на стенде (`tmp/probe-checkpoint/`), из - документации драйвера не следовало. Закончилось: явная проверка вырожденного - значения + вопрос 8 в эксплуатационном проходе. -- **Канонизация внутри транзакции.** Симптом: соседняя доставка получала «база - занята». Причина: пересборка держала блокировку записи 5.019 с при таймауте - занятости 5000 мс — канонизация и хеширование шли внутри транзакции. - Воспроизведено замером на копии БД. Закончилось: вынос канонизации из - транзакции; числа — в раздел `## Прод и поток`. -- **Пик памяти на распаковке.** Симптом: контейнер убивался по памяти на крупных - доставках. Причина: сжатая запись распаковывалась целиком, пик 768 МиБ на теле - 40 МБ. Воспроизведено прогоном на реальном пакете из `testdata`. Закончилось: - потоковая обработка; факт «запись — сжатый BLOB» вынесен в бриф, потому что без - него замер не читается как аномалия. - -## Типовые ложноположительные - -*Находки, которые здесь выглядят убедительно и всегда неверны. Пусто — так и -пишут.* - -- «Значения из входа надо нормализовать перед записью» — инвариант требует - дословного хранения; нормализация тут порча, а не улучшение. -- «Приём должен отвечать ошибкой на непонятое содержимое» — инвариант «сохранили - — значит приняли»; отправитель доставку не повторит. -- «Порядок ключей в JSON стабилен, канонизация избыточна» — наблюдение на живых - данных говорит обратное. -- «Вынести в конфиг» про значения, заданные внешним форматом. - -## Вопросы к проходам - -*Производные от журнала: вопрос конкретному проходу плюс ссылка на запись, из -которой он взялся. Пусто — так и пишут.* - -- `ops`: что произойдёт при откате бинаря поверх уже накатившейся миграции — - стартует ли старая версия молча (журнал, запись 2026-05-12). -- `adversary`: имена файлов внутри архива внешнего экспорта мы не формировали — - проверь путь от имени в архиве до операции с файловой системой (журнал, запись - 2026-06-03). - -## Триггеры - -- `deep`: миграция в `internal/store/migrations/`, новый пакет `internal/*`, - изменение контракта читающего API, правило слияния или вывод слоя. -- «Видимое снаружи» (то есть `standard`): эндпоинт, форма ответа, код ответа - приёма, формат лога. -- `reimpl` запускается, когда изменение вводит **новое правило слияния, - идентичности или разбора**. - -## Недоступно проверке - -### Не проверит ни один проход - -*Принципиальные границы. По факту промаха не пересматриваются.* - -- Поведение внешнего приложения-источника на следующем его обновлении. -- Что реально лежит в системе-источнике: сверить можно только ручным экспортом, - а он делается раз в 2–3 месяца. -- Поведение таблицы под объёмом нескольких лет истории и реальный профиль - нагрузки. -- Завязка внешних потребителей на текущую форму ответа. -- Суждение «этой функциональности не должно существовать». - -### Перестали проверять сознательно - -*Что, когда, почему и где записано. Пересматривается первым, как только что-то -проскочило. Пусто — так и пишут: «сознательно ничего не отключали».* - -- **Поимённая сверка со стайлгайдами языка** — с 2026-05, вместе с упразднением - прохода про идиоматичность (журнал ревью, запись 2026-05-04). Класс обратимый: - портит форму кода, не данные. -- **Правило линтера про длину функции** — снято 2026-06-18: ложных срабатываний - больше трети (журнал, там же). Вернуть, если проскочит дефект «функция делает - три вещи». diff --git a/av-dev-pipeline/skills/review-pipeline/references/calibration.md b/av-dev-pipeline/skills/review-pipeline/references/calibration.md index 259dbc8..d6425f0 100644 --- a/av-dev-pipeline/skills/review-pipeline/references/calibration.md +++ b/av-dev-pipeline/skills/review-pipeline/references/calibration.md @@ -45,7 +45,7 @@ Отсюда два следствия: - **правка charter'а — правка для всех проектов.** Прежде чем сужать - формулировку под свою боль, проверь, не место ли ей в брифе: предмет проверки + формулировку под свою боль, проверь, не место ли ей в документах проекта: предмет проверки живёт там, метод — в charter'е; - **удаление прохода из плагина требует замера на двух проектах**, а не на одном: класс, не всплывший здесь, мог быть единственным работающим там. diff --git a/av-dev-pipeline/skills/review-pipeline/references/finding-contract.md b/av-dev-pipeline/skills/review-pipeline/references/finding-contract.md index 5e7d7e5..64af4e5 100644 --- a/av-dev-pipeline/skills/review-pipeline/references/finding-contract.md +++ b/av-dev-pipeline/skills/review-pipeline/references/finding-contract.md @@ -32,12 +32,12 @@ - **Находка без поля «Последствие» не выводится вовсе.** Пустое «Последствие: ухудшает читаемость» равносильно отсутствию поля. - **`nit` допустим только при нарушении записанной конвенции** — со ссылкой на - файл и раздел конвенций проекта (путь — из раздела `## Карта` брифа) либо на + файл и раздел конвенций проекта (`docs/conventions/`) либо на правило линтера. Если правило механизируемо, но не механизировано — это не находка ревью, это `Promote candidate` (см. [promote.md](promote.md)). -- **`critical` по основанию «нарушен инвариант проекта» требует брифа.** Ссылка - идёт на пункт раздела `## Инварианты` дословно. Без брифа такое основание - недоступно — см. [project-brief.md](project-brief.md), деградированный режим. +- **`critical` по основанию «нарушен инвариант проекта» требует инвариантов.** + Ссылка идёт на пункт раздела инвариантов `CLAUDE.md` дословно. Без них основание + недоступно — см. [project-facts.md](project-facts.md), поразрядная деградация. - **Расхождение — не дефект, пока не названо последствие.** Особенно для прохода независимой реализации: «я бы сделал иначе» без последствия не выводится. @@ -52,7 +52,7 @@ Шкала привязана к обратимости, а не к громкости: класс «необратимо и молча» всегда весит больше класса «шумно и лечится повтором». Что здесь необратимо, -говорит раздел `## Прод и поток` брифа. +говорит `CLAUDE.md` — что в этом проекте необратимо. ## Блок границ покрытия diff --git a/av-dev-pipeline/skills/review-pipeline/references/project-brief.md b/av-dev-pipeline/skills/review-pipeline/references/project-brief.md deleted file mode 100644 index b23cb8b..0000000 --- a/av-dev-pipeline/skills/review-pipeline/references/project-brief.md +++ /dev/null @@ -1,413 +0,0 @@ -# Бриф проекта — контракт - -Конвейер общий, а находки — проектные. Проход, не знающий, что в этом проекте -нельзя нарушать, чем краснеет гейт и сколько данных реально проходит через узел, -выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать. - -Поэтому проектная специфика живёт **в одном файле проекта**, а не в charter'ах -агентов. Charter описывает **метод** прохода (что он делает и почему именно так), -бриф — **предмет** (что здесь дорого, чем это меряется, где лежит). - -Шаблон для заполнения — [brief-template.md](brief-template.md). - -## Где лежит и как находится - -Порядок разрешения пути, одинаковый для скилла и для каждого агента: - -1. путь, названный в задании конвейера (`бриф: <путь>`) — конвейер обязан его - передавать каждому проходу; -2. `docs/review-brief.md`; -3. `.claude/review-brief.md`; -4. брифа нет ни по одному пути — **он заводится**, скиллом - `av-dev-pipeline:project-brief`, и прогон продолжается по заведённому. - -Разрешает путь конвейер, один раз, и дальше передаёт готовым. Агент, получивший -путь в задании, сам ничего не ищет. - -## Деградированный режим — исход, а не умолчание - -Он включается ровно тогда, когда бриф **завести не удалось**: репозиторий -доступен только на чтение, человек прямо запретил, инварианты вывести неоткуда. -Во всех остальных случаях брифа быть обязано. - -Каждый проход в этом режиме: - -- не присваивает `critical` по основанию «нарушен инвариант проекта» — инвариантов - он не знает; -- не оперирует числами объёма и потока — формулирует условиями; -- пишет в границы покрытия строку: «брифа проекта нет (<причина>): инварианты, - модель угроз и профиль нагрузки неизвестны; находки этих классов не искались». - -Причина обязательна: без неё строка неотличима от «мы просто не стали», и -одинаковая строка в каждом отчёте перестаёт читаться на третьей задаче. - -Триаж сводит эти строки в одну и выносит в финальный отчёт. Отсутствие брифа — -дыра покрытия, а не нейтральное умолчание. - -## Форма - -Markdown. Разделы — заголовки второго уровня с **точными именами** из списка -ниже: по ним агенты находят свой кусок. Порядок разделов свободен, лишние разделы -допустимы и игнорируются, отсутствующий раздел работает как деградированный режим -для тех проходов, которые его читают. - -## Разделы - -### `## Проект` — обязателен - -Абзац: что система делает — и, что важнее, **чего она не делает**. Граница домена -нужна архитектурному проходу как критерий: «хранилище, а не аналитика», «единое -ядро, тонкие транспорты», «связующий сервис, а не медиатека». Без неё перенос -понятия через границу выглядит просто новым кодом. - -Читают: `architecture`, `rubric`, `reimpl`, `specs`. - -### `## Инварианты` — обязателен - -Список того, что нарушать нельзя. Каждый пункт — три вещи: - -- формулировка **как проверяемое свойство**, а не как лозунг: «точка сохраняется - дословно: незнакомое поле не отбрасывается», а не «бережно относимся к данным»; -- **последствие нарушения** и его обратимость; -- **severity по умолчанию** — если это не `critical`, скажи прямо. - -Это единственный раздел, который **цитируется формулировкой**, а не пересказывается -ссылкой: по нему присваивается severity, и пересказ здесь стоит неверной оценки. - -**Оговорка про severity, потому что она единственная не цитируется.** Проекты -почти никогда не пишут severity рядом с инвариантом — её приходится выводить, и -правило вывода одно: **по обратимости последствия**. Необратимо и молча — -`critical`; лечится повтором, видно сразу — ниже. Выведенная severity помечается -словом «выведена по обратимости», а не выдаётся за решение проекта: проход ставит -по ней `critical`, и он вправе знать, чьё это суждение. Лучший исход — дописать -severity туда, откуда цитируется формулировка, и тогда пометка снимается. - -Читают: `specs` (режим 1 — отражены ли задетые инварианты в спеке), `code`, -`adversary`, `architecture`, `triage` (ранжирование и разметка «развилка»). - -### `## Гейт` — обязателен - -- **Команда** целиком, включая передачу базы диффа (`task gate BASE=<база>`), и - как база определяется по умолчанию. -- **Где логи** отдельных шагов. -- **Что означает каждый исход**: чем гейт краснеет, что предупреждает, что - пропускается по составу диффа. -- **Шаги, которые красят безусловно, и почему.** Это самая ценная часть раздела: - «данные под контролем версий», «структура конфига изменилась, а образец нет», - «миграции не накатываются с нуля» — проход обязан знать, что здесь не бывает - «ну это мелочь». -- **Чего в гейте намеренно нет** и почему — прогон на живом корпусе, длинный - интеграционный тест. У проверки, которую гейт не гоняет, краснота никому не - видна; это уезжает в границы покрытия. - -Читает: `gate`. - -### `## Команды` — обязателен - -Что проход имеет право выполнить и чем: - -- **карта проекта для архитектуры** — команда, отдающая пакеты, граф зависимостей - и инвентарь концепций (`task review:context`); -- **запуск изменения вживую** — чем поднять и как проверить поведение (нужно - пайплайну задачи на шаге поведенческой верификации); -- **тесты, линт, дополнительные проверки** — и какие из них дорогие; -- **дорогие проверки вне гейта — с адресатом.** Мало сказать «`verify:archive` - идёт минуту»: назови, **кто и когда обязан** её гонять — какой класс изменения - её требует, кто её запускает (проход, пайплайн, человек) и что делать, если она - не прогонялась. Без адресата дорогая проверка не гоняется никогда, а её - краснота не видна никому. **Адресат в проекте не записан нигде — тогда бриф его - назначает**, и назначение помечается: «адресат назначен брифом, владельцем не - подтверждён». Это тот же класс, что выведенная severity у инварианта: слот - честнее заполнить назначением с пометкой, чем оставить пустым; -- **что запускать запрещено**: рабочая БД, боевой каталог данных, внешние - сервисы. Формулируй запретом с путями, а не «будь осторожен». - -Читают: `architecture`, `gate`, `ops`, `triage`, пайплайн задачи. - -### `## Прод и поток` — обязателен - -Материал для эксплуатационного прохода, и он же — половина ранжирования триажа. - -**Первой строкой — главный вопрос эксплуатации этого проекта.** Один заголовок -покрывает противоположные постановки: «поток идёт непрерывно и молча, отправитель -об отказе не узнает» и «мы опрашиваем чужие сервисы, и главный вопрос — что -делать, когда сосед отвечает медленно». От того, какая из них здесь главная, -зависит порядок находок в отчёте, а вывести её проход не может — он видит -одинаковый код. - -Дальше: - -- где это работает: машина, окружение, что рядом, кто перезапускает; -- **внешние зависимости поимённо** и чем каждая отказывает: не только «падает», но - и «отвечает медленно», «молчит», «отдаёт мусор». Эксплуатационный проход - спрашивает про каждую отдельно, и список зависимостей он взять больше неоткуда. - **Зависимостей почти нет — так и напиши**: «внешних зависимостей нет, смотри на - диск и на СУБД». Пустой пункт, не названный пустым, проход тратит впустую или - заполняет выдумкой; -- **кто заметит отказ и когда** — есть ли вообще наблюдатель; -- **характер потока**: непрерывный и молчаливый, по запросу, по расписанию; есть - ли обратная связь у отправителя; -- **представление данных и настройки хранилища.** Чем физически лежит запись - (сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи - (распаковка целиком, read-modify-write), и **настройки, у которых есть - числовое значение**: таймаут занятости СУБД, режим журналирования, лимит тела, - размер пула, ретеншен. Это не украшение раздела: ровно эти два факта - превращают **замер** в находку. Замеренный пик памяти — аномалия только если - известно, что запись лежит сжатой и распаковывается целиком; замеренная - длительность удержания блокировки — гарантированный отказ соседа только если - известно, чему равен таймаут занятости. Без этих фактов проход снимет верное - число и честно понизит находку до гипотезы, потому что сравнить его будет не с - чем. Цена пропущенного пункта здесь не «не найдём», а **«найдём и не - починим»**. Числа с провенансом — в следующем пункте, воспроизведённые случаи — - в разделе `## Прецеденты`; -- **измеренные числа с провенансом**: объёмы, размеры тел, темп, размеры таблиц. - Число без источника проход обязан превратить в условие — так и напиши, откуда - оно. **Замер и настройка — разные пункты, и путать их нельзя:** настройка - (`busy_timeout`, лимит тела, размер пула) живёт пунктом выше и говорит, чему - равен порог; замер говорит, что происходит на самом деле. Проекту без - наблюдаемой нагрузки нечего писать во втором пункте — **так и напиши**: - «измеренных чисел нагрузки нет, всё, что ниже, — настройки». Тогда проход - формулирует условиями осознанно, а не потому, что не нашёл; -- **что обратимо, а что нет.** Падение, которое лечится повтором, и тихая потеря, - которую нечем восстановить, — разные классы, и порядок находок в отчёте зависит - от того, какой из них здесь главный. - -Читают: `ops`, `adversary`, `triage`, `reimpl`. - -### `## Модель угроз` — обязателен - -**Первой строкой — периметр.** «Сервис открыт наружу; злоумышленник в локальной -сети неинтересен» и «контур доверенный, публичного интернета здесь нет, не -выдумывай его» — это один и тот же заголовок при противоположной постановке, и -враждебный проход не может выбрать между ними сам. Периметр, объявленный первой -строкой, задаёт смысл всему остальному разделу. - -**Периметров может быть два — целевой и сегодняшний**, если контур ещё не -развёрнут: «целевой — открыт наружу за прокси с TLS; сегодняшний — только -локальная машина, токены пусты осознанно». Тогда назови оба и скажи прямо, -**против какого строятся находки**. Иначе враждебный проход либо завалит отчёт -находками «нет TLS» по сегодняшнему состоянию, либо не станет искать дефекты, -спящие до выкладки, — оба исхода стоят прохода целиком. - -Дальше: - -- **что недоверенное** и каким каналом приходит: тело запроса, файл, аргумент - команды, ответ внешней системы, содержимое архива; -- **из чего строятся пути и ключи** — раскладка файлов на диске, состав - координатного ключа записи, имя каталога. Враждебный проход выводит запись за - пределы песочницы именно отсюда, и без этого пункта он ищет вслепую; -- **что разграничивает доступ** — токены, контуры, права файлов; -- **что чувствительнее чего**: если данные дороже секретов, скажи это прямо; -- **что вне модели** — перечислить явно. Пустой пункт «вне модели» означает, что - враждебный проход выдумает угрозу сам, и находка никогда не будет исправлена. - -Читает: `adversary`. - -### `## Карта` — обязателен - -Где что лежит, путями: - -- **основная ветка** — её имя. Отсюда берутся ветки задач, в неё вливается батч, - от неё считается база диффа по умолчанию (`git merge-base HEAD <основная>`). - Батч подставляет это имя в каждую команду git; взять его больше неоткуда, а - угадывание между `master` и `main` ломает интеграцию целиком; -- актуальные спеки и дельта-спеки предлагаемого изменения; -- **нарезка capability и миграционное состояние спек** — по какому признаку - проект режет capability (по домену, по транспорту, по подсистеме), какие из них - уже перенесены в актуальные спеки, а какие ещё живут только в документации или - в коде. Проход по спекам иначе примет непереехавшую тему за пробел в спеке, а - архитектурный — за отсутствие понятия; -- конвенции прозой — **файл или каталог файлов**, путями; и **какая их часть уже - механизирована** правилом. Механизация бывает **в нескольких местах сразу**: - конфиг линтера, собственный анализатор и — чаще всего незамеченное — - **тест-сканер исходников** (правило про направление зависимостей, форму - миграций, логику в транспорте), который внешне неотличим от обычного теста. - Перечисли все места: непойманное место механизации означает, что проход по - конвенциям будет добросовестно проверять уже проверенное; -- **наблюдения на живых данных** — где записано, как внешний мир ведёт себя на - самом деле (что реально шлёт источник, чем документация формата расходится с - практикой, какие числа сняты с живого потока). Их спрашивают `specs`, `reimpl` - и `ops`, и все трое — «из раздела `## Карта`». Отдельного файла нет — **так и - напиши**, и перечисли суррогаты: спеки, где наблюдения рассыпаны, комментарии в - адаптерах, `testdata`. Отдельный файл — лучшая форма, потому что при нескольких - внешних источниках наблюдения иначе не сойдутся в одном месте; но честный - перечень суррогатов лучше молчания, от которого три прохода ищут - несуществующий путь; -- архитектура и решения; журнал проскочивших дефектов; -- **единые точки проекта** — где генерируются идентификаторы и время, где - единственный парсер входного формата, где маппинг доменной ошибки в код ответа, - где общий путь приёма. Это материал для вопроса «не появился ли второй способ»; - если команда карты проекта их выгружает, здесь хватит ссылки на неё; -- **нумерованные артефакты** — путь миграций и правило нумерации: батч раздаёт - номера заранее, чтобы параллельные задачи не столкнулись файлами; -- где ведутся задачи (пайплайн только читает и сообщает исход); -- `testdata` и что в них лежит; куда можно писать временное; -- **каталоги, которые не трогают вовсе**. - -Читают: все проходы. - -### `## Типовые узлы` — необязателен, но без него рубрика беднеет - -Роды узлов, из которых состоит проект (парсер входного формата, HTTP-обработчик, -репозиторий, воркер, клиент внешнего API, CLI-команда, файловое хранилище), и по -3–5 **специфичных для рода** проверяемых свойств к каждому. - -**Рода, а не инвентарь того, что сейчас лежит в пакетах.** Список пишется по -природе проекта: род, который проект уже задумал, но ещё не написал, включать -полезно (рубрика на него понадобится ровно на той задаче, где его заводят); а -род, случайно оказавшийся в коде в одном экземпляре, — нет. Иначе раздел -протухает на каждой задаче и требует пересмотра, которого никто не делает. - -Читает: `rubric`. Без раздела рубрика выродится в общие слова и повторит -конвенции — то есть станет applicative-проходом, ради отсутствия которого она и -существует. - -### `## Прецеденты` — обязателен, хотя бы строкой «пусто» - -**Воспроизведённые случаи этого проекта, с оракулом.** Не «здесь бывают гонки», а -«такой дефект здесь уже был, вот чем он воспроизведён»: что оказалось не так, -каким экспериментом или тестом это показано, какими числами, где это записано. - -Каждый пункт — четыре вещи: - -- **класс дефекта** — так, чтобы проход узнал его в другом месте; -- **как проявился** — симптом, который увидел человек; -- **чем воспроизведён** — команда, тест, стенд, замер. Без этого пункт - превращается в байку. **Регрессионный тест, написанный вместе с починкой, - годится** наравне с независимым экспериментом: он исполняемый и падает на - старом коде, а это всё, что требуется от оракула. Слабее он ровно в одном — - сформулирован уже зная ответ; это отмечается словом, а не служит поводом - выбросить пункт; -- **чем закончилось** — починка, правило линтера, пункт брифа, «ничего». - -Зачем раздел существует. Прецедент — самая сильная опора, какая у прохода вообще -бывает: он проектный, воспроизводимый и уже однажды оказался правдой. Пока слота -не было, прецеденты вмерзали в charter'ы проходов — то есть каждый проект читал -про чужую контрольную точку в чужой СУБД и искал её у себя. Charter описывает -**форму класса**, бриф — **случай**. - -Источники: журнал проскочивших дефектов, архивные отчёты триажа, `git log` по -починкам. Прецедентов нет — так и напиши: «прецедентов не накоплено», и это -честнее пустого раздела. - -Читают: все проходы — свой класс; `triage` — как готовый оракул. - -### `## Типовые ложноположительные` — необязателен, но без него отсев слепой - -Находки, которые в **этом** проекте выглядят убедительно и всегда неверны. Это -единственный проектный вход в шаг триажа «отсев вкусовщины»: общие критерии -(«не меняет поведения, не влияет на стоимость следующего изменения, не нарушает -записанного») ловят вкусовщину, но не ловят находку, которая нарушает общее -правило **осознанно**. - -Каждый пункт — формулировка находки, какой её выдаёт проход, плюс одна строка -«почему здесь это не дефект». Типичные обитатели: «дословное хранение надо -нормализовать» там, где дословность — инвариант; «повтор надо сделать -идемпотентным» там, где повтор невозможен по построению; «это надо вынести в -конфиг» там, где значение задано внешним протоколом. - -Читает: `triage`. - -### `## Вопросы к проходам` — необязателен - -Проектные вопросы, адресованные **поимённо** конкретному проходу. Главный их -источник — журнал проскочивших дефектов: запись «проход не мог этого знать» чаще -всего лечится фактом в другом разделе, но иногда лечится не фактом, а -**вопросом**: «`ops`, спроси про поведение при откате бинаря поверх новой схемы», -«`adversary`, проверь имена внутри архива». Такие вопросы живут здесь, а не в -charter'е: charter общий для всех проектов, а вопрос выведен из промаха в этом. - -**Журнал — не единственный источник, а лучший.** У молодого проекта журнал пуст, -и слот тогда заполняется из того, что есть: незакрытые находки аудита, известное -расхождение кода с документацией, место, где решение принято «пока так». Правило -одно и не смягчается — **у каждого вопроса указан провенанс**, и по нему видно, -насколько он выстрадан: «журнал, запись такая-то» весит больше, чем «открытая -находка аудита». - -Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Проход, увидев свой блок, задаёт -эти вопросы **дополнительно** к обязательным — и отвечает на них в выводе явно. - -Читают: проходы, названные поимённо. - -### `## Триггеры` — необязателен - -Проектная конкретизация правила выбора профиля: какие пути и контракты означают -`deep`; что считается «поведением, видимым снаружи»; при каком изменении -запускается `reimpl`. Умолчания записаны в самом скилле и работают без этого -раздела — но общее правило говорит «изменение публичного контракта», а какой -контракт публичный, знает только проект. - -Читают: скилл конвейера, пайплайн задачи. - -### `## Недоступно проверке` — обязателен, и делится на два подраздела - -Раздел целиком уезжает в границы покрытия финального отчёта — он существует ровно -затем, чтобы «критичных проблем не обнаружено» никогда не читалось как «проверено -всё». Но внутри лежат **два разных класса**, и смешивать их нельзя: при следующем -промахе один пересматривается, другой нет. - -#### `### Не проверит ни один проход` - -Принципиально недоступное: поведение внешних систем и их будущих версий, реальный -профиль нагрузки, соответствие сохранённого действительности, завязка внешних -потребителей на текущую форму, суждение «а нужна ли эта функциональность». - -Этот список не пересматривается по факту промаха: дефект отсюда — не ошибка -конвейера, а его честная граница. Он меняется только когда меняется сам проект -(появился стенд, появился второй потребитель, появилась телеметрия). - -#### `### Перестали проверять сознательно` - -Решения о сужении: перестали звать проход, понизили профиль правилом, сузили -класс проверяемого, сняли правило линтера как шумное. Каждый пункт — **что -перестали, когда и почему**, со ссылкой на запись журнала ревью. - -Этот список **пересматривается первым**, как только что-то проскочило: первый -вопрос по любому пропущенному дефекту — «не тот ли это класс, который мы перестали -проверять». Пункт, из-за которого дефект проскочил, либо возвращается, либо -получает строку «оставляем, цена поимки выше цены дефекта» с датой. - -Оба подраздела обязательны; пустой называется пустым. - -Читает: `triage`; каждый проход — свою часть. - -## Правила ведения - -- **Бриф не пересказывает документацию проекта.** Факт, записанный в `CLAUDE.md` - или в архитектуре, попадает сюда ссылкой и одной строкой сути. Два дома для - одного факта разъезжаются, и разошедшийся бриф хуже отсутствующего: он выглядит - актуальным. Исключение одно — раздел инвариантов, он цитируется. -- **Числа — с провенансом, и провенанс проверяется переходом по ссылке.** «Тела - доходили до 42 МБ (замер, ссылка)». Число без источника проход не имеет права - использовать как утверждение. Отдельный и более коварный случай — **число, чей - источник по ссылке не подтверждается**: в документе по ссылке другое число, или - его там нет вовсе. Такое число не выбрасывается и не переписывается по догадке: - оно остаётся с пометкой «расходится с источником: там <что нашли>», а проход - обязан читать его как условие, а не как замер. Молча подставить «правильное» - число хуже всего — расхождение перестанет быть видно, а причина его останется. -- **Пустой пункт называется пустым.** «Внешних зависимостей нет — смотри на диск - и на СУБД», «прецедентов не накоплено», «наблюдений на живых данных не ведём», - «измеренных чисел нагрузки нет», «сознательно ничего не отключали». Отсутствие - строки читается проходом как «здесь не написали», и он тратит обязательный - вопрос впустую либо заполняет пробел выдумкой. Прямое «пусто» стоит одной - строки и экономит проход целиком. -- **Назначенное помечается назначенным.** Бриф отражает решения проекта, но - местами оказывается **первым** местом, где решение вообще записано: severity у - инварианта, адресат дорогой проверки, периметр, восстановленный из конфига. - Так можно — молчать хуже, — но пометка обязательна («выведена по обратимости», - «назначен брифом, владельцем не подтверждён»). Проход имеет право знать, чьё - это суждение, а владелец — увидеть, что за него что-то решили. -- **Что вне модели — называется явно.** Это относится и к угрозам, и к нагрузке, - и к классам находок, которые проект сознательно перестал проверять (последние — - в свой подраздел `## Недоступно проверке`, а не вперемешку с принципиальным). -- **Бриф подчиняется промоуту.** Свойство, ставшее правилом линтера, из брифа - вычёркивается — как и из конвенций, и из charter'ов (см. - [promote.md](promote.md), шаг 3). -- **Когда обновлять:** сменился гейт; появился новый контур, зависимость или - источник входа; журнал ревью получил запись вида «проход не мог этого знать»; - воспроизвели дефект — он идёт в `## Прецеденты`. Планового пересмотра нет. -- **Заводится и обновляется шагом, а не руками** — скиллом - `av-dev-pipeline:project-brief`. Он же вызывается автоматически, когда конвейер - или пайплайн задачи не нашли брифа ни по одному пути. -- **Бриф ведёт проект**, а не плагин. Файл живёт в репозитории проекта; плагин - его читает и заводит по шаблону, но не хранит у себя и не подменяет. diff --git a/av-dev-pipeline/skills/review-pipeline/references/project-facts.md b/av-dev-pipeline/skills/review-pipeline/references/project-facts.md new file mode 100644 index 0000000..ed9aa3a --- /dev/null +++ b/av-dev-pipeline/skills/review-pipeline/references/project-facts.md @@ -0,0 +1,85 @@ +# Откуда проход берёт проектную конкретику + +Конвейер общий, находки — проектные. Проход, не знающий, что в этом проекте +нельзя нарушать, чем краснеет гейт и сколько данных реально идёт через узел, +выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать. + +Отдельного файла-брифа **нет**. Проектная конкретика живёт в документах канона +`av-dev-pm`, и проход читает их напрямую: пути жёсткие, посредник не нужен, а +второй дом для тех же фактов разошёлся бы и выглядел актуальным. + +Определение канона — в плагине `av-dev-pm`, +`skills/canon/references/canon.md`. Здесь только карта «что нужно проходу → +где это лежит». + +## Карта + +| Что нужно проходу | Где лежит | +| --- | --- | +| что система делает и **чего не делает**, граница домена | `docs/passport.md` | +| инварианты **с severity рядом с формулировкой** | `CLAUDE.md`, раздел инвариантов | +| команда гейта, чем краснеет безусловно, чего в нём нет, кто гоняет дорогое | `CLAUDE.md`, семантика гейта | +| что запускать запрещено, с путями; `testdata`; куда писать временное; имя основной ветки | `CLAUDE.md` | +| компоненты и capability, окружение, внешние зависимости поимённо, наблюдатель, характер потока, единые точки проекта | `docs/architecture.md` | +| чем физически лежит запись, что при чтении и записи, настройки с числовым значением | `docs/database.md` | +| периметр, недоверенный вход, из чего строятся пути и ключи, что вне модели | `docs/security.md` | +| измеренные числа **с провенансом**, поведение внешних систем на самом деле | `docs/research/` | +| конвенции прозой и **что уже механизировано** правилом | `docs/conventions/` | +| почему решено так, отвергнутые варианты | `docs/adr/` | +| типовые узлы, типовые ложноположительные, вопросы к проходам, недоступно проверке | `docs/review.md`, раздел настройки | +| прецеденты: воспроизведённые дефекты с оракулом | `docs/review.md`, журнал | +| нормативное поведение и дельты изменения | `openspec/specs/`, `openspec/changes//specs/` | + +## Сшивать обязаны проходы + +Раньше эти факты лежали рядом в одном файле, и соседство работало само. Теперь +они разложены по домам, и **проход обязан собрать их сам** — иначе снимет верное +число и честно понизит находку до гипотезы, потому что сравнить будет не с чем. + +Два обязательных стыка: + +- **замер + настройка.** «Пик 768 МиБ» — аномалия только рядом со строкой + «запись лежит сжатой и распаковывается целиком»; «блокировка удерживалась + 5.019 с» — гарантированный отказ соседа только рядом с известным таймаутом + занятости. Числа в `docs/research/`, настройки в `docs/database.md`, и оба + читает `ops`, `adversary`, `reimpl`. +- **инвариант + обратимость.** severity берётся из `CLAUDE.md`; если её там + нет — она **выводится по обратимости последствия** и помечается «выведена по + обратимости», а не выдаётся за решение проекта. + +## Деградация — поразрядная + +Документа нет — деградирует то, что из него читалось, и **только оно**. Каждый +проход пишет свою строку в границы покрытия; триаж сводит их в одну. + +| Нет документа | Что деградирует | +| --- | --- | +| `CLAUDE.md` без инвариантов | `critical` по основанию «нарушен инвариант проекта» не присваивается никем | +| `docs/security.md` | `adversary` не знает периметра — формулирует условиями, `critical` не ставит | +| `docs/research/` | числа неизвестны `ops`, `adversary`, `reimpl` — все трое формулируют условиями | +| `docs/database.md` | замер не с чем сравнить: находка не поднимается выше гипотезы | +| `docs/passport.md` | `architecture` теряет границу домена и вырождается в общее мнение | +| `docs/review.md` | `triage` отсеивает вслепую: типовых ложноположительных нет | +| `docs/architecture.md` | «не появился ли второй способ» не проверяется — единых точек не знает никто | + +Строка в границах покрытия обязана называть **причину**: «`docs/security.md` в +проекте нет» читается иначе, чем «есть, но периметр не назван». Без причины +строка неотличима от «мы просто не стали» и перестаёт читаться на третьей задаче. + +**Документов канона нет вовсе** — проект не приведён к канону. Это не повод +работать вслепую: скажи об этом строкой и предложи `av-dev-pm:canon`. Одна +операция на проект против деградации на каждой задаче. + +## Правило чтения + +- **Читай в источнике, не по памяти.** Документы правятся по ходу работы, в том + числе этой же задачей. +- **Число без провенанса — условие, а не утверждение.** Число, чей источник по + ссылке не подтвердился, читается как условие и **называется расходящимся**, а + не подменяется догадкой. +- **Пустое, названное пустым, — это факт.** «Внешних зависимостей нет — смотри + на диск и на СУБД» экономит обязательный вопрос. Отсутствие строки — не факт, + а пробел, и его надо назвать в границах покрытия. +- **Свойство, ставшее правилом линтера, из конвенций удалено** и лежит в + перечне механизированного в `docs/conventions/README.md`. Проверять его + проходом — тратить внимание на уже проверенное. diff --git a/av-dev-pipeline/skills/review-pipeline/references/promote.md b/av-dev-pipeline/skills/review-pipeline/references/promote.md index ef233c8..89e9287 100644 --- a/av-dev-pipeline/skills/review-pipeline/references/promote.md +++ b/av-dev-pipeline/skills/review-pipeline/references/promote.md @@ -24,7 +24,7 @@ проход, чьи находки не доезжают никогда, — кандидат на `drop` (см. [calibration.md](calibration.md)). - Место записи — конвенции проекта, файл или нужный файл каталога (путь — в - разделе `## Карта` брифа). Если + каталог `docs/conventions/`). Если тема относится к поведению системы, а не к тому, как мы пишем код, — это не конвенция, а требование: заводится дельта-спека обычным путём. @@ -51,7 +51,7 @@ хук блокирует любой коммит, и правило снимут первым же раздражённым движением. Приводить код в соответствие — часть шага 2, отдельным коммитом. -## Шаг 3. Удаление из конвенций, из брифа и из промптов +## Шаг 3. Удаление из конвенций и из промптов **Шаг, который пропускают чаще всего, и единственный, ради которого затевались первые два.** @@ -61,13 +61,15 @@ - из файла конвенций убирается формулировка правила; остаётся, если нужно, одна строка «проверяется линтером `<имя>`» — но только там, где без неё раздел теряет связность; -- **из брифа проекта** убирается соответствующий пункт, а в разделе `## Карта` - правило переезжает в перечень «механизировано и потому проходом по конвенциям - не проверяется»; +- правило переезжает в **перечень механизированного в + `docs/conventions/README.md`** — со ссылкой на место механизации: конфиг + линтера, собственный анализатор, тест-сканер исходников. Непойманное место + означает, что проход будет добросовестно проверять уже проверенное; - из контекста инструмента спек убирается дубль, если он там был. Charter'ы проходов при этом **не правятся**: они общие и живут в плагине, а -предмет проверки приходит из брифа. Именно поэтому шаг 3 стал дешевле, чем был: +предмет проверки приходит из документов проекта. Именно поэтому шаг 3 дешевле, +чем был: вычеркнуть строку в одном файле проекта, а не в девяти промптах. Практический критерий: **в прозаических конвенциях остаётся только то, что 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 96fcf0a..3c47fdc 100644 --- a/av-dev-pipeline/skills/review-pipeline/references/review-journal.md +++ b/av-dev-pipeline/skills/review-pipeline/references/review-journal.md @@ -1,42 +1,64 @@ -# Журнал проскочивших дефектов +# Журнал дефектов -Артефакт проекта, а не плагина: файл живёт в репозитории (путь — в разделе -`## Карта` брифа, по умолчанию `docs/review-journal.md`). Здесь описано, зачем он -и какой формы, потому что без него конвейер не учится: находки закрываются, -причины непоймания теряются, и один и тот же класс проскакивает второй раз. +Артефакт проекта, а не плагина: файл живёт в репозитории — **`docs/review.md`**, +слот канона `av-dev-pm`. Здесь описано, зачем он и какой формы, потому что без +него конвейер не учится: находки закрываются, причины непоймания теряются, и один +и тот же класс проскакивает второй раз. + +Тот же файл держит **настройку конвейера под проект** — типовые узлы, типовые +ложноположительные, вопросы к проходам, недоступно проверке. Это не соседство по +случаю: все четыре раздела — производные калибровки, а журнал им источник. ## Что туда попадает -Дефект, который **прошёл ревью и всплыл позже**. Записывается **сразу**, а не -ретроспективно: со временем теряется не сам факт, а причина непоймания — -единственное, ради чего журнал существует. +**Воспроизведённый дефект — с пометкой `проскочил` или `пойман ревью`.** +Записывается **сразу**, а не ретроспективно: со временем теряется не сам факт, а +причина непоймания — единственное, ради чего журнал существует. -Реализованные задачи, находки ревью и принятые решения сюда не пишутся: у них -есть коммит, спека и задача. Здесь только промахи конвейера. +Пометка делит журнал на две выборки с разным назначением: + +- **проскочил** — эвал-сет для калибровки конвейера. Реальный промах сильнее + синтетической пробы: синтетические смещены в сторону тех, которые уже умеешь + придумывать; +- **пойман ревью** — прецеденты с оракулом. Самая сильная опора, какая у прохода + бывает: проектная, воспроизводимая и однажды уже оказавшаяся правдой. Без + журнала они остаются только в отчётах триажа в архиве change, где их никто не + ищет. + +Реализованные задачи и принятые решения сюда не пишутся: у них есть коммит, спека +и `docs/adr/`. Отдельно сюда попадают **решения о составе прогонов**: перестали звать проход, понизили профиль правилом, сузили класс проверяемого. Не потому, что это промах, а потому, что здесь лежит цена: если что-то теперь проскочит, первый вопрос — «не тот ли это класс, который мы перестали проверять». -Каждое такое решение обязано получить **строку в брифе** — в подразделе -`### Перестали проверять сознательно` раздела `## Недоступно проверке`. Журнал -хранит «почему тогда так решили», бриф — то, во что смотрит каждый прогон. -Решение, оставшееся только в журнале, в границы покрытия не доедет. +Каждое такое решение обязано получить строку в подразделе **«Перестали проверять +сознательно»** раздела «Недоступно проверке» того же файла. Журнал хранит «почему +тогда так решили», раздел настройки — то, во что смотрит каждый прогон. Решение, +оставшееся только в журнале, в границы покрытия не доедет. ## Форма записи ``` -## ГГГГ-ММ-ДД — <краткое последствие> +## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман] - **Где:** путь:строка либо «конвейер, а не код» - **Симптом:** как обнаружилось, кем и когда - **Причина:** что на самом деле было не так -- **Почему не поймали:** какой проход обязан был найти и что ему помешало -- **Что меняем:** правило прохода, шаг гейта, конвенция, пункт брифа — либо - «ничего, цена поимки выше цены дефекта» +- **Чем воспроизведён:** тест, команда, замер — с числами +- **Почему не поймали:** только для проскочивших — какой проход обязан был найти + и что ему помешало +- **Что меняем:** правило прохода, шаг гейта, конвенция, факт в документе + проекта — либо «ничего, цена поимки выше цены дефекта» ``` +Пункт «чем воспроизведён» отличает запись от байки: без него на неё нельзя +сослаться как на оракул. Регрессионный тест, написанный вместе с починкой, +годится наравне с независимым экспериментом — он исполняемый и падает на старом +коде. Слабее он ровно в одном: сформулирован уже зная ответ, и это отмечается +словом. + Последний пункт важнее остальных. Вывод «ничего не меняем» — законный исход: не всякий дефект стоит того, чтобы усложнять ради него ревью каждой задачи. @@ -44,24 +66,27 @@ Три адреса, и выбор между ними — половина ценности журнала: -- **в бриф проекта** — если проход не мог знать факта: объём, характер потока, - что здесь необратимо, какой шаг гейта красит безусловно. Самый частый адрес и - самый дешёвый. Сюда же — **воспроизведённый случай** (раздел `## Прецеденты`: - класс, симптом, чем воспроизведён, чем закончилось) и **вопрос конкретному - проходу**, если промах лечится не фактом, а заданным вопросом (раздел - `## Вопросы к проходам`). Прежде чем править charter, проверь, не хватит ли - этих двух разделов: charter общий для всех проектов, бриф — про этот. +- **в документ проекта** — если проход не мог знать факта. Адрес зависит от рода + факта, и карта их всех — [project-facts.md](project-facts.md): объём и + измеренное число → `docs/research/`; настройка хранилища → `docs/database.md`; + что необратимо и какой шаг гейта красит безусловно → `CLAUDE.md`; периметр и + недоверенный вход → `docs/security.md`. **Вопрос конкретному проходу**, если + промах лечится не фактом, а заданным вопросом, → раздел «Вопросы к проходам» + того же `docs/review.md`. Самый частый адрес и самый дешёвый. Прежде чем + править charter, проверь, не хватит ли факта или вопроса: charter общий для + всех проектов, документ — про этот. - **в конвенции или в правило линтера** — если свойство выражается детерминированно (процедура — [promote.md](promote.md)). - **в charter прохода** — если сломан **метод**, а не знание. Правка charter'а меняет поведение во всех проектах, поэтому она требует калибровки - ([calibration.md](calibration.md)) и обоснования, почему это не лечится - брифом. + ([calibration.md](calibration.md)) и обоснования, почему это не лечится фактом + в документе проекта. ## Что журнал даёт конвейеру -- **пробы для калибровки** — реальный проскочивший дефект сильнее синтетического: - синтетические смещены в сторону тех, которые уже умеешь придумывать; +- **пробы для калибровки** — выборка по пометке `проскочил`; +- **готовые оракулы** — выборка по пометке `пойман ревью`: находка того же + класса подтверждается ссылкой на запись, а не рассуждением; - **основание для правил конвейера** — требование называть запущенные проходы поимённо, отказ от чисел, производных от размера корпуса, и правило последовательного прогона выведены из конкретных записей, а не из общих diff --git a/av-dev-pipeline/skills/task-batch/SKILL.md b/av-dev-pipeline/skills/task-batch/SKILL.md index 2014bea..8f64291 100644 --- a/av-dev-pipeline/skills/task-batch/SKILL.md +++ b/av-dev-pipeline/skills/task-batch/SKILL.md @@ -23,17 +23,19 @@ description: Проводит несколько задач разом — пл проход `review-specs` финальной сверки. Проекта без OpenSpec это касается так же, как одиночного пайплайна (см. его раздел «Предпосылки»). - **Скиллы зовутся с пространством имён**: `av-dev-pipeline:task-pipeline`, - `av-dev-pipeline:review-pipeline`, `av-dev-pipeline:project-brief`. Короткое имя + `av-dev-pipeline:review-pipeline`, `av-dev-pm:tasks`. Короткое имя может разрешиться в устаревшую проектную копию, и это произойдёт молча — в charter'е сабагента пиши полное имя, он твоего контекста не видит. - **Проектные копии этих скиллов и агентов при установке плагина удаляются.** -Перед стартом прочитай `CLAUDE.md` проекта и бриф ревью (`docs/review-brief.md`): -из него берутся **основная ветка** (раздел `## Карта` — она подставляется в -каждую команду git ниже), команда гейта, инварианты и раскладка нумерованных -артефактов. Брифа нет — заведи его Skill'ом -**`av-dev-pipeline:project-brief`** один раз, до первой волны: иначе каждая -задача батча заплатит деградированным ревью, а имя основной ветки придётся +Перед стартом прочитай `CLAUDE.md` проекта: оттуда берутся **имя основной +ветки** (оно подставляется в каждую команду git ниже), команда и семантика +гейта, инварианты и что запускать запрещено. Раскладка нумерованных артефактов — +`docs/database.md` и `docs/.pm.json` (ключ `migrations`). + +**Документов канона нет — проект к нему не приведён.** Скажи это строкой и +предложи `av-dev-pm:canon` **до первой волны**: иначе каждая задача батча +заплатит поразрядной деградацией ревью, а имя основной ветки придётся угадывать. ## Границы @@ -92,14 +94,14 @@ fast-forward. Ветки после вливания удаляются. - трогает конкурентность: транзакции, блокировки, фоновые циклы, общее состояние; - трогает размер тела, буфер, память, сжатие, ретеншен, темп потока; - - её тема названа в разделах `## Прод и поток` или `## Прецеденты` брифа как - место, где уже мерили или уже ломалось. + - её тема названа в `docs/research/` или в журнале `docs/review.md` как место, + где уже мерили или уже ломалось. Ни один триггер не сработал — задача не замеряющая, даже если её ревью окажется `deep`. `deep` про глубину проверки, замеряющая — про соревнование за железо; это разные вопросы, и совпадают они не всегда; - **нумерованные артефакты — номера раздаёт оркестратор заранее.** Если проект - нумерует миграции или подобные файлы (путь — из брифа), посмотри последний + нумерует миграции (путь — `docs/.pm.json`, ключ `migrations`), посмотри последний номер и **раздай номера всем задачам, которые, вероятно, их добавят**, до запуска. Номер уходит в charter сабагента, и он берёт назначенный, а не «следующий свободный». diff --git a/av-dev-pipeline/skills/task-pipeline/SKILL.md b/av-dev-pipeline/skills/task-pipeline/SKILL.md index e86e661..d03d3ad 100644 --- a/av-dev-pipeline/skills/task-pipeline/SKILL.md +++ b/av-dev-pipeline/skills/task-pipeline/SKILL.md @@ -15,30 +15,28 @@ description: Автономно проводит одну задачу чере ## Предпосылки -- **OpenSpec и скиллы `opsx:*`** — внешняя обвязка, на которой стоят шаги 2, 3, 6 - и 8, а также проход `review-specs` и профиль `design` (они завязаны на - `openspec/changes//specs/*/spec.md` и на `openspec validate --strict`). В - проекте без OpenSpec эти шаги упадут на «нет такого скилла»: либо подключаем - OpenSpec, либо цикл вырождается в «прочитать задачу → код → ревью кода → - коммит», и об отсутствии спекового контура говорится в докладе. +- **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`. Короткое имя может разрешиться в устаревшую - проектную копию, и это произойдёт молча. + `av-dev-pm:docs`, `av-dev-pm:tasks`. Короткое имя может разрешиться в + устаревшую проектную копию, и это произойдёт молча. - **Проектные копии этих скиллов и агентов удаляются при установке плагина** (`.claude/skills/{task-pipeline,review-pipeline,task-batch}`, `.claude/agents/<проект>-review-*.md`). Две копии одного скилла расходятся, и побеждает та, что короче названа. -Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается -(архитектура, конвенции), если ещё не в контексте. Проектные факты, нужные ревью -— инварианты, команда гейта, объёмы, модель угроз, прецеденты, — живут в брифе -(`docs/review-brief.md`, контракт — в references конвейера ревью). +Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается, если ещё +не в контексте. Проектные факты, нужные ревью — инварианты, семантика гейта, +объёмы, модель угроз, прецеденты, — живут в **документах канона** `av-dev-pm`; +карта «что где» — `references/project-facts.md` конвейера ревью. -**Брифа нет ни по одному пути — заведи его, а не работай в деградированном -режиме.** Вызови Skill **`av-dev-pipeline:project-brief`**: он соберёт бриф из -`CLAUDE.md`, архитектуры, файла задач и конвенций, покажет человеку и вернёт -путь, который дальше передаётся ревью. Это механика: спрашивать разрешения не -нужно. Один шаг один раз на проект — против деградации на каждой задаче. +**Документов канона нет — проект к нему не приведён.** Скажи это строкой и +предложи скилл `av-dev-pm:canon`: одна операция на проект против поразрядной +деградации на каждой задаче. Работу при этом не останавливай. ## Границы: чем пайплайн не владеет @@ -109,7 +107,7 @@ description: Автономно проводит одну задачу чере в объявленных границах. **Что остатком не является — правило живёт не здесь.** Канонический текст с обеими -оговорками — в плагине `av-dev-tasks`, скилл `av-dev-tasks:session`, раздел +оговорками — в плагине `av-dev-pm`, скилл `av-dev-pm:session`, раздел `## Вопрос, блокер, необратимое`, подраздел «Отличать вопрос от застревания». Правило принадлежит управлению задачами, потому что решает **сделана задача или вышла**, — это исход планирования, а не исполнения. **Ссылайся, не @@ -122,7 +120,7 @@ description: Автономно проводит одну задачу чере Оба порога — стоп: первый поднимает решение до начала записи, второй даёт исход «не доведена». -Плагин `av-dev-tasks` не подключён — правило не отменяется, а становится +Плагин `av-dev-pm` не подключён — правило не отменяется, а становится осторожнее: прежде чем записать зависящее от нерешённого куда бы то ни было — в хранилище, в журнал, в витрину или наружу, — спрашивай человека. @@ -183,7 +181,7 @@ description: Автономно проводит одну задачу чере ### 4. (Нетривиальная) Ревью предложения — профиль `design`, ДО кода Первый чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`** с профилем -`design`, ссылкой на change `` и путём к брифу. Он запустит `review-specs` +`design` и ссылкой на change ``. Он запустит `review-specs` (режим «дизайн ДО кода»), `review-rubric` (фаза 1: приёмочные критерии для задуманного узла) и `review-architecture` по предложению. @@ -202,14 +200,14 @@ description: Автономно проводит одну задачу чере ### 6. Написать код — `opsx:apply` Вызови Skill `opsx:apply` для реализации `tasks.md`. Код — по конвенциям проекта -(файл назван в разделе `## Карта` брифа). Меняешь схему — обнови её описание в +(каталог `docs/conventions/`). Меняешь схему — обнови её описание в документации тем же change, если проект этого требует: гейт обычно это проверяет. Прогони гейт и добейся зелёного — он же гейт следующего шага. **Поведенческая верификация.** Если задача меняет реальное поведение (новый эндпоинт, разбор входа, схема, форма ответа) — зелёных юнит-тестов мало. Подними -изменение вживую командой из раздела `## Команды` брифа и прогони сценарий. +изменение вживую командой из раздела команд `CLAUDE.md` и прогони сценарий. Пропусти только для чисто внутренних правок без наблюдаемого рантайма. **Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца @@ -218,10 +216,10 @@ description: Автономно проводит одну задачу чере ### 7. Ревью кода — Skill `av-dev-pipeline:review-pipeline` Второй чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`**, дав ссылку -на change ``, базу диффа, путь к брифу, профиль **и режим запуска**. +на change ``, базу диффа, профиль **и режим запуска**. **Правило выбора профиля живёт в скилле конвейера** (раздел «Профили»), проектные -триггеры — в разделе `## Триггеры` брифа. Здесь оно не пересказывается: три +триггеры — в `docs/review.md`, если записаны. Здесь оно не пересказывается: три копии одного правила расходятся, и работать будет та, которую прочитали последней. Помни ровно одно — **профиль выбирается по факту изменения, а не по ощущению важности**, и посмотри таблицу перед вызовом. @@ -271,22 +269,38 @@ description: Автономно проводит одну задачу чере ### 9. Синк документации -Ревью выполненного — до этого шага. Затем: +Ревью выполненного — до этого шага. Затем **вызови Skill `av-dev-pm:docs`**: он +владеет содержимым документов канона и ведёт чек-лист синка. Плагина нет — +пройди чек-лист сам по списку ниже. -- суть переехавшего решения — в документацию проекта (архитектура, журнал - решений), если её там ещё нет; -- менялась схема — её описание обновлено тем же change; -- новое, узнанное о внешнем формате или о данных, — в тот файл проекта, который - это накапливает; такой файл обычно ценнее кода; -- воспроизведённый дефект (свой или чужой) — в раздел `## Прецеденты` брифа: - класс, симптом, чем воспроизведён, чем закончилось. Это единственный артефакт, - который делает следующее ревью умнее. +**Правило одно и оно жёсткое: принуждённое отрицание.** Доклад обязан назвать +**каждый** документ канона — либо чем он обновлён, либо «не требуется, потому +что…». Нетронутые группируются одной строкой с общей причиной. Список триггеров +прозой уже проверен на живом проекте и дал 6 записей ADR на 43 изменения; +работает только обязательное отрицание. -**Задачу пайплайн не закрывает.** Записи учёта — индекс, статус, спринт — он не -трогает вовсе: закрытие происходит **после приёмки** и делается владельцем -спринта, а пайплайн на этом шаге стоит до коммита и до всякой приёмки. Скриптов -и скиллов учёта не зови — их у тебя и нет: пути между плагинами не разрешаются, и -моста здесь намеренно не проложено. Твоё дело — назвать исход в докладе. +Документы и их триггеры: `openspec/specs/` (поведение — вливает `opsx:archive`), +`database.md` (тронуты миграции), `architecture.md` (новый компонент, граница, +внешняя зависимость), `adr/` (дорогой откат, намеренный отказ, пересмотр +прежнего), `research/` (узнали новое о внешних данных), `security.md` (новый +недоверенный вход, токен, путь наружу), `conventions/` (промоут, включая +**удаление** формулировки, ставшей правилом линтера), `review.md` +(воспроизведённый дефект — с пометкой «проскочил» или «пойман ревью»), +`passport.md`, `CLAUDE.md`. + +### 9а. Закрыть задачу + +**Вызови Skill `av-dev-pm:tasks`** и попроси закрыть задачу как реализованную — +он владеет форматом и двигает строку между индексами сам. Путь к его скрипту не +выясняй и индексы руками не правь: мост между плагинами — вызов скилла, а не +путь. + +Плагина в проекте нет — вызов не разрешится. Тогда **ничего не выдумывай**: +скажи в докладе, что учёт задач остаётся за владельцем, и назови исход. + +**Приёмщик и исполнитель здесь совпадают**, и закрытие не окончательно: человек +на сессии может вернуть задачу (`reopen` с причиной). Поэтому доклад по критериям +приёмки — не формальность, а единственное, по чему приёмка вообще возможна. ### 10. Коммит diff --git a/av-dev-pm/skills/canon/SKILL.md b/av-dev-pm/skills/canon/SKILL.md index 27adb74..3631a37 100644 --- a/av-dev-pm/skills/canon/SKILL.md +++ b/av-dev-pm/skills/canon/SKILL.md @@ -17,7 +17,10 @@ description: Привести проект к канону документов пересказывается: два описания одной раскладки разъедутся, и работать будет то, которое прочитали последним. Прочитай его **до** первой правки. -Журнал версий — [references/changelog.md](references/changelog.md). +- [references/skeletons.md](references/skeletons.md) — **что именно класть** в + каждый незаполненный слот. Своей формой заглушку не выдумывай: `docs.py` + узнаёт только плейсхолдер `` из шаблонов. +- [references/changelog.md](references/changelog.md) — журнал версий канона. ## Три правила, из которых всё следует @@ -53,9 +56,13 @@ python3 $ds version --dir <корень> # версия кано нельзя**. `check`, отчитавшийся «канон соблюдён» на проекте, где из шести файлов три лишние, хуже отсутствующего. -Машина проверяет пути, лишние файлы, битые ссылки, версию, нетронутые -плейсхолдеры, маркеры долга и две сверки с кодом. **Ты** судишь о том, чего она -не умеет: +Машина **дрейфом** считает: отсутствующий путь канона, файл вне канона, битую +ссылку, отставшую версию, capability без упоминания в обзоре, миграцию без правки +`database.md`. **Замечанием** — незаполненный плейсхолдер и слабое упоминание +capability: незаполненный канон это переходное состояние, а не отказ. Маркеры +долга просто считает числом. + +**Ты** судишь о том, чего она не умеет: - **смысловой дубль** — `docs/specs/recognition.md` описывает то же, что capability `recognition`. Файлы разные, содержание одно; @@ -113,8 +120,8 @@ capability), `openspec/config.yaml`. Порядок важен — он минимизирует окно, в котором ссылки битые: 1. `docs/.pm.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть; -2. каталоги канона и скелет: незаполненное — **одной честной информативной - строкой**, а не «TBD» (см. canon.md, «Пустое называется пустым»); +2. каталоги канона и скелет **по [references/skeletons.md](references/skeletons.md)**: + незаполненное — одной честной информативной строкой, а не «TBD»; 3. переносы содержимого; 4. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он владеет форматом задач, включая переименование транслитных слагов в @@ -122,8 +129,15 @@ capability), `openspec/config.yaml`. 5. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`, `CLAUDE.md`, `README.md`; 6. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**; -7. шаг `docs.py check` в гейт проекта; -8. `docs.py check` — до зелёного в механизируемой части. +7. **шаг `docs.py check` в гейт проекта.** Путь к скрипту — переменной с + умолчанием на канонический путь маркетплейса, чтобы переустановка плагина не + меняла `Taskfile`; шаг обязан **краснеть внятно**, если скрипт не найден, а не + пропускаться. Передай ему базу диффа (`--base`) той же переменной, что и + остальным шагам гейта: без неё сверка миграций со схемой не гоняется вовсе. + Пример строки покажи человеку — гейт принадлежит проекту, и правит его он; +8. `docs.py check` — до **отсутствия дрейфа**. Замечания (незаполненные + плейсхолдеры, слабое упоминание capability) остаются: незаполненный канон это + объявленное переходное состояние из шага 5, а не отказ. ### 5. Объяви переходное состояние diff --git a/av-dev-pm/skills/canon/references/skeletons.md b/av-dev-pm/skills/canon/references/skeletons.md index b25b0bb..b483215 100644 --- a/av-dev-pm/skills/canon/references/skeletons.md +++ b/av-dev-pm/skills/canon/references/skeletons.md @@ -8,6 +8,13 @@ Плейсхолдер ставится только там, где ответ **обязан** быть и его не спросили. Всё, чего в проекте пока просто нет, описывается словами, а не плейсхолдером. +**Шаблоны — единственное место, где правило канона копируется намеренно.** +`adr/README.md` и `review.md` уезжают в репозиторий проекта и обязаны там что-то +говорить; определение при этом остаётся в [canon.md](canon.md). Отсюда +обязанность: **правка такого правила в каноне тянет запись в +[changelog.md](changelog.md)** с указанием, какой файл проекта поднимает +`upgrade`. Без этого копия в проекте останется на старой версии молча. + ## `docs/passport.md` ```markdown diff --git a/av-dev-pm/skills/canon/scripts/docs.py b/av-dev-pm/skills/canon/scripts/docs.py index acababc..dd67faa 100644 --- a/av-dev-pm/skills/canon/scripts/docs.py +++ b/av-dev-pm/skills/canon/scripts/docs.py @@ -76,19 +76,25 @@ RETIRED = { DEBT_MARKER = re.compile(r"") PLACEHOLDER = re.compile(r"") -MD_LINK = re.compile(r"\[[^\]]*\]\(([^)]+)\)") +MD_LINK = re.compile(r"\[[^\]]*\]\(\s*\s]+)>?(?:\s+[\"'(][^)]*)?\)") FENCE = re.compile(r"^\s*(```|~~~)") +INLINE_CODE = re.compile(r"`[^`\n]*`") + + def strip_code(text: str) -> str: - """Выкинуть блоки кода: пути в примерах и шаблонах — не ссылки, и краснеть - на них значит краснеть на каждом образце документа.""" + """Выкинуть блоки кода и вставки в обратных кавычках. + + Путь в примере или в шаблоне — не ссылка, и краснеть на нём значит краснеть + на каждом образце документа. Инлайн-код тоже: `[docs/backlog](docs/tasks/…)` + в тексте про подписи ссылок — иллюстрация, а не ссылка.""" out, inside = [], False for line in text.splitlines(): if FENCE.match(line): inside = not inside continue - out.append("" if inside else line) + out.append("" if inside else INLINE_CODE.sub("", line)) return "\n".join(out) @@ -226,7 +232,9 @@ def check_placeholders_and_debt(root: Path, rep: Report) -> None: text = strip_code(path.read_text(encoding="utf-8", errors="replace")) rel = path.relative_to(root) for what in PLACEHOLDER.findall(text): - rep.error(f"{rel}: плейсхолдер шаблона не заполнен — {what}") + # Замечание, а не дрейф: незаполненный канон — объявленное переходное + # состояние, и краснеть на нём значит требовать выдумать содержание. + rep.note(f"{rel}: плейсхолдер шаблона не заполнен — {what}") for what in DEBT_MARKER.findall(text): rep.debt(f"{rel}: {what}") @@ -238,28 +246,60 @@ def check_capabilities(root: Path, rep: Report) -> None: rep.skip("openspec/specs/ нет — сверка capability с архитектурой неприменима") return if not arch.exists(): + rep.skip( + "docs/architecture.md нет — capability не сверены с обзором " + "(об отсутствии файла сказано отдельной строкой)" + ) return text = arch.read_text(encoding="utf-8", errors="replace") - missing = [d.name for d in sorted(specs.iterdir()) if d.is_dir() and d.name not in text] - for name in missing: - rep.error( - f"capability {name} есть в openspec/specs/, но не упомянута в " - f"docs/architecture.md — обзор отстал от нормативных спек" - ) + for d in sorted(specs.iterdir()): + if not d.is_dir(): + continue + name = d.name + # Засчитываем только явное упоминание: ссылку на спеку или имя в обратных + # кавычках. Голая подстрока совпадает с именем пакета или CLI-команды и + # даёт ложное «упомянуто» — то есть проверку, проходящую не по той причине. + explicit = f"openspec/specs/{name}" in text or f"`{name}`" in text + loose = re.search(rf"\b{re.escape(name)}\b", text) is not None + if explicit: + continue + if loose: + rep.note( + f"capability {name}: в docs/architecture.md есть слово «{name}», но " + f"нет ни ссылки на openspec/specs/{name}, ни имени в обратных " + f"кавычках — проверь, это про capability или про пакет" + ) + else: + rep.error( + f"capability {name} есть в openspec/specs/, но не упомянута в " + f"docs/architecture.md — обзор отстал от нормативных спек" + ) def changed_files(root: Path, base: str, rep: Report) -> list[str] | None: - try: - out = subprocess.run( - ["git", "-C", str(root), "diff", "--name-only", f"{base}...HEAD"], - capture_output=True, - text=True, - check=True, - ) - except (subprocess.CalledProcessError, FileNotFoundError) as exc: - rep.skip(f"сверка миграций пропущена: git не отдал дифф ({exc})") - return None - return [line for line in out.stdout.splitlines() if line] + """Объединение закоммиченного, рабочего дерева и untracked. + + Гейт гоняют ДО коммита, поэтому `base...HEAD` не видит ровно ту правку, ради + которой проверка и заводилась: миграция уже лежит в дереве, но ещё не в + истории. Пропущенная правка выглядела бы как зелёный шаг.""" + cmds = [ + ["diff", "--name-only", base], + ["ls-files", "--others", "--exclude-standard"], + ] + seen: list[str] = [] + for cmd in cmds: + try: + out = subprocess.run( + ["git", "-C", str(root), *cmd], + capture_output=True, + text=True, + check=True, + ) + except (subprocess.CalledProcessError, FileNotFoundError) as exc: + rep.skip(f"сверка миграций пропущена: git не отдал дифф ({exc})") + return None + seen.extend(line for line in out.stdout.splitlines() if line) + return sorted(set(seen)) def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> None: @@ -292,17 +332,24 @@ def check_tasks(root: Path, rep: Report) -> None: if not script.exists(): rep.skip(f"tasks.py не найден по пути {script} — согласованность задач не проверена") return + # cwd=root обязателен: tasks.py отвергает --dir вне текущего каталога, и без + # этого его отказ окружения (код 3) схлопнулся бы в наш дрейф (код 1). proc = subprocess.run( - [sys.executable, str(script), "check", "--dir", str(tasks)], + [sys.executable, str(script), "check", "--dir", "docs/tasks"], capture_output=True, text=True, + cwd=str(root), ) if proc.returncode == 0: return if proc.returncode == 1: rep.error("tasks.py check нашёл дрейф в docs/tasks/ — разбирать его командой tasks.py") else: - rep.error(f"tasks.py check отказал с кодом {proc.returncode}: {proc.stderr.strip()}") + # Чужой код выхода не выдаём за свой: 3 это окружение, а не дрейф. + rep.skip( + f"tasks.py check не отработал (код {proc.returncode}): " + f"{(proc.stderr or proc.stdout).strip().splitlines()[0] if (proc.stderr or proc.stdout).strip() else 'без сообщения'}" + ) # --- Отчёт ------------------------------------------------------------------ diff --git a/av-dev-pm/skills/docs/SKILL.md b/av-dev-pm/skills/docs/SKILL.md index fa9cb19..621448b 100644 --- a/av-dev-pm/skills/docs/SKILL.md +++ b/av-dev-pm/skills/docs/SKILL.md @@ -64,31 +64,24 @@ description: Вести содержимое документов канона **ADR цитирует решение оттуда и ссылается на источник.** Не пересказывает и не сочиняет заново. -Заводится, когда верно одно из трёх: +**Триггер заведения, форма имени и правило замены — в +[каноне](../canon/references/canon.md), раздел `adr/`.** Здесь они не +повторяются: копия правила расходится с оригиналом на первой же смене версии +канона, а расходится незаметно. -- **дорогой откат** — переделка стоит дороже переписывания одного файла; -- **намеренный отказ** от очевидного подхода — чтобы не переоткрывать «а почему - мы не сделали X»; -- **пересмотр прежнего решения** — тогда у старой записи обязателен статус - `заменено на ADR-…`, а у новой в контексте строка «Заменяет ADR-…». +Твоя часть — **применить триггер к этой задаче и сказать вслух, сработал он или +нет**. Строка «adr/ — не требуется: решение рутинное» и есть то, ради чего +чек-лист существует; её отсутствие неотличимо от «забыл посмотреть». -Не заводится для рутины и для того, что видно из кода и `git log`. - -Порядок: имя `ADR-ГГГГ-ММ-ДД-slug.md`, дата — когда решение **принято**, слаг -английский; тело по `docs/adr/template.md`; строка в индексе `docs/adr/README.md` -сверху. Активная запись статуса не имеет. +Порядок работы: открой архивный `design.md` change, найди в `Decisions` то, что +проходит триггер, процитируй решение и его причину, сошлись на источник, добавь +строку в индекс `docs/adr/README.md` сверху. ## Чистка `architecture.md` -Обзор не держит поведение — его нормативный дом `openspec/specs/`. Раздел, где -поведение осталось, помечается маркером долга: - -``` - -``` - -`docs.py` считает маркеры и печатает числом; **гейт от них не краснеет** — это -долг, а не отказ, иначе постепенный переезд стал бы невозможен. +Обзор не держит поведение — его нормативный дом `openspec/specs/`. **Форма +маркера долга и правило «гейт от них не краснеет» — в +[каноне](../canon/references/canon.md), раздел `architecture.md`.** Разбирается порциями: раздел вычищается той задачей, которая его касается. Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change, @@ -97,45 +90,36 @@ description: Вести содержимое документов канона ## Запись в `research/` Наблюдение о внешнем мире: что реально шлёт источник, чем документация формата -расходится с практикой. **Число — с провенансом**: команда или условия, которыми -получено, чтобы его можно было перепроверить. +расходится с практикой. **Требование провенанса и правило про расходящееся +число — в [каноне](../canon/references/canon.md), раздел `research/`.** -Число без источника проход обязан читать как условие. Число, чей источник по -ссылке не подтвердился, **не переписывается по догадке** — остаётся с пометкой -«расходится с источником: там <что нашли>». Молча подставить «правильное» число -хуже всего: расхождение перестанет быть видно, а причина останется. +Твоя часть — заметить, что по ходу задачи узналось новое о внешних данных, и не +дать этому остаться в контексте. Признак: ты правил разбор, опираясь на то, чего +нет ни в одном документе. ## Запись в `review.md` -Два раздела с разными сроками жизни, и путать их нельзя. +Файл держит два раздела с разными сроками жизни — журнал дефектов и настройку +конвейера. **Что в каком и в какой форме — в +[каноне](../canon/references/canon.md), раздел `review.md`**; подробности формы +записи и выбор адреса, куда она ведёт, — в конвейере ревью, +`references/review-journal.md`. -**Журнал дефектов.** Запись на каждый воспроизведённый дефект с пометкой -**проскочил / пойман ревью**. Пишется сразу, а не ретроспективно: со временем -теряется не факт, а причина непоймания — единственное, ради чего журнал есть. -Форма: где, симптом, чем воспроизведён, почему не поймали (для проскочивших), -что меняем. Вывод «ничего не меняем, цена поимки выше цены дефекта» — законный -исход. - -**Настройка конвейера.** Типовые узлы; типовые ложноположительные; вопросы к -проходам поимённо с провенансом; недоступно проверке. Последний раздел делится -на «не проверит ни один проход» (принципиальная граница, по факту промаха не -пересматривается) и «перестали проверять сознательно» — этот **пересматривается -первым**, как только что-то проскочило. +Твоя часть на синке: **дефект пишется сразу**, а не «потом, когда починим». +Со временем теряется не факт, а причина непоймания — единственное, ради чего +журнал есть. И решение о сужении проверок (перестали звать проход, понизили +профиль) обязано попасть в раздел настройки, а не остаться в отчёте ревью. ## Промоут в конвенции -Находка → конвенция → правило линтера → **удаление из прозы**. Процедура -принадлежит конвейеру ревью и живёт в его `references/promote.md`; здесь только -то, что касается документа: +Находка → конвенция → правило линтера → **удаление из прозы**. Процедура целиком +принадлежит конвейеру ревью и живёт в его `references/promote.md`; роль каталога +конвенций — в [каноне](../canon/references/canon.md). -- формулировка — **проверяемое свойство**, а не совет; -- в прозе остаётся только то, что принципиально не выражается правилом; -- как только правило работает, формулировка из `conventions/<тема>.md` - **удаляется**, а правило попадает в перечень механизированного в - `conventions/README.md` со ссылкой на место механизации. - -Непойманное место механизации означает, что проход по конвенциям будет -добросовестно проверять уже проверенное. +Твоя часть — **третий шаг, который пропускают чаще всего**: правило заработало, +а формулировка осталась в прозе, и проход продолжает проверять уже проверенное. +На синке это отдельная строка: «conventions/ — правило X механизировано, +формулировка удалена» либо «не требуется». ## Чего этот скилл не делает diff --git a/av-dev-pm/skills/init/SKILL.md b/av-dev-pm/skills/init/SKILL.md index 3b0c6b6..6a4d2c9 100644 --- a/av-dev-pm/skills/init/SKILL.md +++ b/av-dev-pm/skills/init/SKILL.md @@ -9,7 +9,9 @@ description: Завести новый проект — сессия вопро которого дальше работают все остальные скиллы. **Определение канона — [канон](../canon/references/canon.md).** Читается до -первого вопроса: интервью идёт по слотам канона, а не по вкусу. +первого вопроса: интервью идёт по слотам канона, а не по вкусу. Что класть в +каждый файл — [скелеты](../canon/references/skeletons.md); своей формы заглушки +не выдумывай, `docs.py` узнаёт только плейсхолдер оттуда. ## Что `init` физически не может произвести @@ -69,10 +71,12 @@ description: Завести новый проект — сессия вопро 4. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и отдельным файлом не остаётся: два дома для одного замысла разойдутся на первом же уточнении. -5. Заведи скелет остальных — каждый с честной строкой. +5. Заведи скелет остальных по [скелетам](../canon/references/skeletons.md) — + каждый с честной строкой. 6. Каталог задач и первые цели — **вызови скилл `av-dev-pm:tasks`**: он владеет форматом целей и задач. -7. `docs.py check` из скилла `canon` — до зелёного в механизируемой части. +7. `docs.py check` из скилла `canon` — до отсутствия дрейфа. Замечания о + незаполненных плейсхолдерах остаются: их закрывает не `init`, а работа. 8. Покажи человеку, что получилось, и **отдельным списком** — что выведено из брифа, что предположено, что осталось неизвестным. Правят по этим строкам. diff --git a/av-dev-pm/skills/tasks/scripts/tasks.py b/av-dev-pm/skills/tasks/scripts/tasks.py index a003ddb..856a174 100755 --- a/av-dev-pm/skills/tasks/scripts/tasks.py +++ b/av-dev-pm/skills/tasks/scripts/tasks.py @@ -83,7 +83,8 @@ import subprocess import sys from pathlib import Path -CONFIG_NAME = ".tasks.json" +CONFIG_NAME = ".tasks.json" # прежний дом настроек, читается для совместимости +PM_CONFIG_REL = "../.pm.json" # текущий дом: docs/.pm.json, ключ "tasks" EXIT_OK = 0 EXIT_DRIFT = 1 @@ -278,15 +279,42 @@ class Layout: def load_config(root: Path) -> dict: + """Настройки каталога задач. + + Дом один — `docs/.pm.json`, ключ `tasks`: один конфиг на весь канон, а не по + одному на каталог. Прежний `/.tasks.json` читается, пока живы проекты, + которые ещё не переехали; когда есть оба, побеждает `.pm.json`, и об этом + говорится вслух, потому что молча выбранный из двух конфиг — это дрейф, + который потом никто не объяснит. + """ + pm = (root / PM_CONFIG_REL).resolve() + if pm.is_file(): + data = _read_json(pm) + section = data.get("tasks", {}) + if not isinstance(section, dict): + raise Env(f"{pm}: ключ «tasks» — ожидался объект с настройками") + if (root / CONFIG_NAME).is_file(): + print(f"ЗАМЕЧАНИЕ настройки взяты из {pm}; {root / CONFIG_NAME}" + f" остался от прежней раскладки и не читается — удали его", + file=sys.stderr) + return _validate_config(section, pm) path = root / CONFIG_NAME if not path.is_file(): return {} + return _validate_config(_read_json(path), path) + + +def _read_json(path: Path) -> dict: try: data = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as e: raise Env(f"{path}: не разбирается как JSON — {e}") if not isinstance(data, dict): raise Env(f"{path}: ожидался объект с настройками") + return data + + +def _validate_config(data: dict, path: Path) -> dict: unknown = set(data) - set(DEFAULTS) if unknown: raise Env(f"{path}: неизвестные ключи: {', '.join(sorted(unknown))}" @@ -351,10 +379,10 @@ def resolve_layout(explicit: str | None) -> Layout: return Layout(rel if str(rel) != "." else candidate, load_config(candidate)) if (base / ".git").exists(): break # выше корня репозитория не ищем - raise Env("каталог задач не найден: ни --dir, ни .tasks.json вверх от" - f" {here}, ни умолчания (docs/tasks, tasks, doc/tasks)." - " Путь каталога называет CLAUDE.md проекта; новый проект —" - " tasks.py init --dir <путь>") + raise Env("каталог задач не найден: ни --dir, ни docs/tasks вверх от" + f" {here}. По канону путь всегда docs/tasks; чужую раскладку" + " переводит скилл av-dev-pm:canon, новый проект —" + " tasks.py init --dir docs/tasks") # --- Чтение индексов ---