Files
dev-skills/av-dev-pipeline/skills/review-pipeline/references/project-brief.md
T
av 0eca206460 av-dev-pipeline: починены находки ревью, бриф заводится скиллом
- скилл project-brief: бриф собирается из CLAUDE.md, архитектуры, Taskfile
  и конвенций и показывается человеку. Раньше единственная инструкция по
  его созданию лежала внутри шаблона, поэтому деградированный режим был не
  аварийным, а единственным: critical по основанию «нарушен инвариант»
  недостижим ни на одной задаче
- rebase перенесён внутрь worktree задачи: прежняя форма падала на занятой
  ветке, и агент уводил весь батч в провалившиеся с ложной причиной
- контракт брифа дополнен восемью слотами; проверен заполнением на обоих
  проектах, незаполнимых нет. Прецедент healthlog вынут из общего charter'а
  в бриф — там он вмёрз вместе с числами
- шов: пайплайн задачу не закрывает и записи учёта не трогает, урожай
  отдаёт списком, правило остатка — ссылкой на av-dev-tasks
- деградированный абзац во всех девяти проходах, вопрос 9 в ops,
  пространство имён в вызовах, раздел предпосылок
2026-08-03 11:45:40 +03:00

414 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Бриф проекта — контракт
Конвейер общий, а находки — проектные. Проход, не знающий, что в этом проекте
нельзя нарушать, чем краснеет гейт и сколько данных реально проходит через узел,
выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать.
Поэтому проектная специфика живёт **в одном файле проекта**, а не в charter'ах
агентов. Charter описывает **метод** прохода (что он делает и почему именно так),
бриф — **предмет** (что здесь дорого, чем это меряется, где лежит).
Шаблон для заполнения — [brief-template.md](brief-template.md).
## Где лежит и как находится
Порядок разрешения пути, одинаковый для скилла и для каждого агента:
1. путь, названный в задании конвейера (`бриф: <путь>`) — конвейер обязан его
передавать каждому проходу;
2. `docs/review-brief.md`;
3. `.claude/review-brief.md`;
4. брифа нет ни по одному пути — **он заводится**, скиллом
`av-dev-pipeline:project-brief`, и прогон продолжается по заведённому.
Разрешает путь конвейер, один раз, и дальше передаёт готовым. Агент, получивший
путь в задании, сам ничего не ищет.
## Деградированный режим — исход, а не умолчание
Он включается ровно тогда, когда бриф **завести не удалось**: репозиторий
доступен только на чтение, человек прямо запретил, инварианты вывести неоткуда.
Во всех остальных случаях брифа быть обязано.
Каждый проход в этом режиме:
- не присваивает `critical` по основанию «нарушен инвариант проекта» — инвариантов
он не знает;
- не оперирует числами объёма и потока — формулирует условиями;
- пишет в границы покрытия строку: «брифа проекта нет (<причина>): инварианты,
модель угроз и профиль нагрузки неизвестны; находки этих классов не искались».
Причина обязательна: без неё строка неотличима от «мы просто не стали», и
одинаковая строка в каждом отчёте перестаёт читаться на третьей задаче.
Триаж сводит эти строки в одну и выносит в финальный отчёт. Отсутствие брифа —
дыра покрытия, а не нейтральное умолчание.
## Форма
Markdown. Разделы — заголовки второго уровня с **точными именами** из списка
ниже: по ним агенты находят свой кусок. Порядок разделов свободен, лишние разделы
допустимы и игнорируются, отсутствующий раздел работает как деградированный режим
для тех проходов, которые его читают.
## Разделы
### `## Проект` — обязателен
Абзац: что система делает — и, что важнее, **чего она не делает**. Граница домена
нужна архитектурному проходу как критерий: «хранилище, а не аналитика», «единое
ядро, тонкие транспорты», «связующий сервис, а не медиатека». Без неё перенос
понятия через границу выглядит просто новым кодом.
Читают: `architecture`, `rubric`, `reimpl`, `specs`.
### `## Инварианты` — обязателен
Список того, что нарушать нельзя. Каждый пункт — три вещи:
- формулировка **как проверяемое свойство**, а не как лозунг: «точка сохраняется
дословно: незнакомое поле не отбрасывается», а не «бережно относимся к данным»;
- **последствие нарушения** и его обратимость;
- **severity по умолчанию** — если это не `critical`, скажи прямо.
Это единственный раздел, который **цитируется формулировкой**, а не пересказывается
ссылкой: по нему присваивается severity, и пересказ здесь стоит неверной оценки.
**Оговорка про severity, потому что она единственная не цитируется.** Проекты
почти никогда не пишут severity рядом с инвариантом — её приходится выводить, и
правило вывода одно: **по обратимости последствия**. Необратимо и молча —
`critical`; лечится повтором, видно сразу — ниже. Выведенная severity помечается
словом «выведена по обратимости», а не выдаётся за решение проекта: проход ставит
по ней `critical`, и он вправе знать, чьё это суждение. Лучший исход — дописать
severity туда, откуда цитируется формулировка, и тогда пометка снимается.
Читают: `specs` (режим 1 — отражены ли задетые инварианты в спеке), `code`,
`adversary`, `architecture`, `triage` (ранжирование и разметка «развилка»).
### `## Гейт` — обязателен
- **Команда** целиком, включая передачу базы диффа (`task gate BASE=<база>`), и
как база определяется по умолчанию.
- **Где логи** отдельных шагов.
- **Что означает каждый исход**: чем гейт краснеет, что предупреждает, что
пропускается по составу диффа.
- **Шаги, которые красят безусловно, и почему.** Это самая ценная часть раздела:
«данные под контролем версий», «структура конфига изменилась, а образец нет»,
«миграции не накатываются с нуля» — проход обязан знать, что здесь не бывает
«ну это мелочь».
- **Чего в гейте намеренно нет** и почему — прогон на живом корпусе, длинный
интеграционный тест. У проверки, которую гейт не гоняет, краснота никому не
видна; это уезжает в границы покрытия.
Читает: `gate`.
### `## Команды` — обязателен
Что проход имеет право выполнить и чем:
- **карта проекта для архитектуры** — команда, отдающая пакеты, граф зависимостей
и инвентарь концепций (`task review:context`);
- **запуск изменения вживую** — чем поднять и как проверить поведение (нужно
пайплайну задачи на шаге поведенческой верификации);
- **тесты, линт, дополнительные проверки** — и какие из них дорогие;
- **дорогие проверки вне гейта — с адресатом.** Мало сказать «`verify:archive`
идёт минуту»: назови, **кто и когда обязан** её гонять — какой класс изменения
её требует, кто её запускает (проход, пайплайн, человек) и что делать, если она
не прогонялась. Без адресата дорогая проверка не гоняется никогда, а её
краснота не видна никому. **Адресат в проекте не записан нигде — тогда бриф его
назначает**, и назначение помечается: «адресат назначен брифом, владельцем не
подтверждён». Это тот же класс, что выведенная severity у инварианта: слот
честнее заполнить назначением с пометкой, чем оставить пустым;
- **что запускать запрещено**: рабочая БД, боевой каталог данных, внешние
сервисы. Формулируй запретом с путями, а не «будь осторожен».
Читают: `architecture`, `gate`, `ops`, `triage`, пайплайн задачи.
### `## Прод и поток` — обязателен
Материал для эксплуатационного прохода, и он же — половина ранжирования триажа.
**Первой строкой — главный вопрос эксплуатации этого проекта.** Один заголовок
покрывает противоположные постановки: «поток идёт непрерывно и молча, отправитель
об отказе не узнает» и «мы опрашиваем чужие сервисы, и главный вопрос — что
делать, когда сосед отвечает медленно». От того, какая из них здесь главная,
зависит порядок находок в отчёте, а вывести её проход не может — он видит
одинаковый код.
Дальше:
- где это работает: машина, окружение, что рядом, кто перезапускает;
- **внешние зависимости поимённо** и чем каждая отказывает: не только «падает», но
и «отвечает медленно», «молчит», «отдаёт мусор». Эксплуатационный проход
спрашивает про каждую отдельно, и список зависимостей он взять больше неоткуда.
**Зависимостей почти нет — так и напиши**: «внешних зависимостей нет, смотри на
диск и на СУБД». Пустой пункт, не названный пустым, проход тратит впустую или
заполняет выдумкой;
- **кто заметит отказ и когда** — есть ли вообще наблюдатель;
- **характер потока**: непрерывный и молчаливый, по запросу, по расписанию; есть
ли обратная связь у отправителя;
- **представление данных и настройки хранилища.** Чем физически лежит запись
(сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи
(распаковка целиком, read-modify-write), и **настройки, у которых есть
числовое значение**: таймаут занятости СУБД, режим журналирования, лимит тела,
размер пула, ретеншен. Это не украшение раздела: ровно эти два факта
превращают **замер** в находку. Замеренный пик памяти — аномалия только если
известно, что запись лежит сжатой и распаковывается целиком; замеренная
длительность удержания блокировки — гарантированный отказ соседа только если
известно, чему равен таймаут занятости. Без этих фактов проход снимет верное
число и честно понизит находку до гипотезы, потому что сравнить его будет не с
чем. Цена пропущенного пункта здесь не «не найдём», а **«найдём и не
починим»**. Числа с провенансом — в следующем пункте, воспроизведённые случаи —
в разделе `## Прецеденты`;
- **измеренные числа с провенансом**: объёмы, размеры тел, темп, размеры таблиц.
Число без источника проход обязан превратить в условие — так и напиши, откуда
оно. **Замер и настройка — разные пункты, и путать их нельзя:** настройка
(`busy_timeout`, лимит тела, размер пула) живёт пунктом выше и говорит, чему
равен порог; замер говорит, что происходит на самом деле. Проекту без
наблюдаемой нагрузки нечего писать во втором пункте — **так и напиши**:
«измеренных чисел нагрузки нет, всё, что ниже, — настройки». Тогда проход
формулирует условиями осознанно, а не потому, что не нашёл;
- **что обратимо, а что нет.** Падение, которое лечится повтором, и тихая потеря,
которую нечем восстановить, — разные классы, и порядок находок в отчёте зависит
от того, какой из них здесь главный.
Читают: `ops`, `adversary`, `triage`, `reimpl`.
### `## Модель угроз` — обязателен
**Первой строкой — периметр.** «Сервис открыт наружу; злоумышленник в локальной
сети неинтересен» и «контур доверенный, публичного интернета здесь нет, не
выдумывай его» — это один и тот же заголовок при противоположной постановке, и
враждебный проход не может выбрать между ними сам. Периметр, объявленный первой
строкой, задаёт смысл всему остальному разделу.
**Периметров может быть два — целевой и сегодняшний**, если контур ещё не
развёрнут: «целевой — открыт наружу за прокси с TLS; сегодняшний — только
локальная машина, токены пусты осознанно». Тогда назови оба и скажи прямо,
**против какого строятся находки**. Иначе враждебный проход либо завалит отчёт
находками «нет TLS» по сегодняшнему состоянию, либо не станет искать дефекты,
спящие до выкладки, — оба исхода стоят прохода целиком.
Дальше:
- **что недоверенное** и каким каналом приходит: тело запроса, файл, аргумент
команды, ответ внешней системы, содержимое архива;
- **из чего строятся пути и ключи** — раскладка файлов на диске, состав
координатного ключа записи, имя каталога. Враждебный проход выводит запись за
пределы песочницы именно отсюда, и без этого пункта он ищет вслепую;
- **что разграничивает доступ** — токены, контуры, права файлов;
- **что чувствительнее чего**: если данные дороже секретов, скажи это прямо;
- **что вне модели** — перечислить явно. Пустой пункт «вне модели» означает, что
враждебный проход выдумает угрозу сам, и находка никогда не будет исправлена.
Читает: `adversary`.
### `## Карта` — обязателен
Где что лежит, путями:
- **основная ветка** — её имя. Отсюда берутся ветки задач, в неё вливается батч,
от неё считается база диффа по умолчанию (`git merge-base HEAD <основная>`).
Батч подставляет это имя в каждую команду git; взять его больше неоткуда, а
угадывание между `master` и `main` ломает интеграцию целиком;
- актуальные спеки и дельта-спеки предлагаемого изменения;
- **нарезка capability и миграционное состояние спек** — по какому признаку
проект режет capability (по домену, по транспорту, по подсистеме), какие из них
уже перенесены в актуальные спеки, а какие ещё живут только в документации или
в коде. Проход по спекам иначе примет непереехавшую тему за пробел в спеке, а
архитектурный — за отсутствие понятия;
- конвенции прозой — **файл или каталог файлов**, путями; и **какая их часть уже
механизирована** правилом. Механизация бывает **в нескольких местах сразу**:
конфиг линтера, собственный анализатор и — чаще всего незамеченное —
**тест-сканер исходников** (правило про направление зависимостей, форму
миграций, логику в транспорте), который внешне неотличим от обычного теста.
Перечисли все места: непойманное место механизации означает, что проход по
конвенциям будет добросовестно проверять уже проверенное;
- **наблюдения на живых данных** — где записано, как внешний мир ведёт себя на
самом деле (что реально шлёт источник, чем документация формата расходится с
практикой, какие числа сняты с живого потока). Их спрашивают `specs`, `reimpl`
и `ops`, и все трое — «из раздела `## Карта`». Отдельного файла нет — **так и
напиши**, и перечисли суррогаты: спеки, где наблюдения рассыпаны, комментарии в
адаптерах, `testdata`. Отдельный файл — лучшая форма, потому что при нескольких
внешних источниках наблюдения иначе не сойдутся в одном месте; но честный
перечень суррогатов лучше молчания, от которого три прохода ищут
несуществующий путь;
- архитектура и решения; журнал проскочивших дефектов;
- **единые точки проекта** — где генерируются идентификаторы и время, где
единственный парсер входного формата, где маппинг доменной ошибки в код ответа,
где общий путь приёма. Это материал для вопроса «не появился ли второй способ»;
если команда карты проекта их выгружает, здесь хватит ссылки на неё;
- **нумерованные артефакты** — путь миграций и правило нумерации: батч раздаёт
номера заранее, чтобы параллельные задачи не столкнулись файлами;
- где ведутся задачи (пайплайн только читает и сообщает исход);
- `testdata` и что в них лежит; куда можно писать временное;
- **каталоги, которые не трогают вовсе**.
Читают: все проходы.
### `## Типовые узлы` — необязателен, но без него рубрика беднеет
Роды узлов, из которых состоит проект (парсер входного формата, HTTP-обработчик,
репозиторий, воркер, клиент внешнего API, CLI-команда, файловое хранилище), и по
35 **специфичных для рода** проверяемых свойств к каждому.
**Рода, а не инвентарь того, что сейчас лежит в пакетах.** Список пишется по
природе проекта: род, который проект уже задумал, но ещё не написал, включать
полезно (рубрика на него понадобится ровно на той задаче, где его заводят); а
род, случайно оказавшийся в коде в одном экземпляре, — нет. Иначе раздел
протухает на каждой задаче и требует пересмотра, которого никто не делает.
Читает: `rubric`. Без раздела рубрика выродится в общие слова и повторит
конвенции — то есть станет applicative-проходом, ради отсутствия которого она и
существует.
### `## Прецеденты` — обязателен, хотя бы строкой «пусто»
**Воспроизведённые случаи этого проекта, с оракулом.** Не «здесь бывают гонки», а
«такой дефект здесь уже был, вот чем он воспроизведён»: что оказалось не так,
каким экспериментом или тестом это показано, какими числами, где это записано.
Каждый пункт — четыре вещи:
- **класс дефекта** — так, чтобы проход узнал его в другом месте;
- **как проявился** — симптом, который увидел человек;
- **чем воспроизведён** — команда, тест, стенд, замер. Без этого пункт
превращается в байку. **Регрессионный тест, написанный вместе с починкой,
годится** наравне с независимым экспериментом: он исполняемый и падает на
старом коде, а это всё, что требуется от оракула. Слабее он ровно в одном —
сформулирован уже зная ответ; это отмечается словом, а не служит поводом
выбросить пункт;
- **чем закончилось** — починка, правило линтера, пункт брифа, «ничего».
Зачем раздел существует. Прецедент — самая сильная опора, какая у прохода вообще
бывает: он проектный, воспроизводимый и уже однажды оказался правдой. Пока слота
не было, прецеденты вмерзали в charter'ы проходов — то есть каждый проект читал
про чужую контрольную точку в чужой СУБД и искал её у себя. Charter описывает
**форму класса**, бриф — **случай**.
Источники: журнал проскочивших дефектов, архивные отчёты триажа, `git log` по
починкам. Прецедентов нет — так и напиши: «прецедентов не накоплено», и это
честнее пустого раздела.
Читают: все проходы — свой класс; `triage` — как готовый оракул.
### `## Типовые ложноположительные` — необязателен, но без него отсев слепой
Находки, которые в **этом** проекте выглядят убедительно и всегда неверны. Это
единственный проектный вход в шаг триажа «отсев вкусовщины»: общие критерии
(«не меняет поведения, не влияет на стоимость следующего изменения, не нарушает
записанного») ловят вкусовщину, но не ловят находку, которая нарушает общее
правило **осознанно**.
Каждый пункт — формулировка находки, какой её выдаёт проход, плюс одна строка
«почему здесь это не дефект». Типичные обитатели: «дословное хранение надо
нормализовать» там, где дословность — инвариант; «повтор надо сделать
идемпотентным» там, где повтор невозможен по построению; «это надо вынести в
конфиг» там, где значение задано внешним протоколом.
Читает: `triage`.
### `## Вопросы к проходам` — необязателен
Проектные вопросы, адресованные **поимённо** конкретному проходу. Главный их
источник — журнал проскочивших дефектов: запись «проход не мог этого знать» чаще
всего лечится фактом в другом разделе, но иногда лечится не фактом, а
**вопросом**: «`ops`, спроси про поведение при откате бинаря поверх новой схемы»,
«`adversary`, проверь имена внутри архива». Такие вопросы живут здесь, а не в
charter'е: charter общий для всех проектов, а вопрос выведен из промаха в этом.
**Журнал — не единственный источник, а лучший.** У молодого проекта журнал пуст,
и слот тогда заполняется из того, что есть: незакрытые находки аудита, известное
расхождение кода с документацией, место, где решение принято «пока так». Правило
одно и не смягчается — **у каждого вопроса указан провенанс**, и по нему видно,
насколько он выстрадан: «журнал, запись такая-то» весит больше, чем «открытая
находка аудита».
Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Проход, увидев свой блок, задаёт
эти вопросы **дополнительно** к обязательным — и отвечает на них в выводе явно.
Читают: проходы, названные поимённо.
### `## Триггеры` — необязателен
Проектная конкретизация правила выбора профиля: какие пути и контракты означают
`deep`; что считается «поведением, видимым снаружи»; при каком изменении
запускается `reimpl`. Умолчания записаны в самом скилле и работают без этого
раздела — но общее правило говорит «изменение публичного контракта», а какой
контракт публичный, знает только проект.
Читают: скилл конвейера, пайплайн задачи.
### `## Недоступно проверке` — обязателен, и делится на два подраздела
Раздел целиком уезжает в границы покрытия финального отчёта — он существует ровно
затем, чтобы «критичных проблем не обнаружено» никогда не читалось как «проверено
всё». Но внутри лежат **два разных класса**, и смешивать их нельзя: при следующем
промахе один пересматривается, другой нет.
#### `### Не проверит ни один проход`
Принципиально недоступное: поведение внешних систем и их будущих версий, реальный
профиль нагрузки, соответствие сохранённого действительности, завязка внешних
потребителей на текущую форму, суждение «а нужна ли эта функциональность».
Этот список не пересматривается по факту промаха: дефект отсюда — не ошибка
конвейера, а его честная граница. Он меняется только когда меняется сам проект
(появился стенд, появился второй потребитель, появилась телеметрия).
#### `### Перестали проверять сознательно`
Решения о сужении: перестали звать проход, понизили профиль правилом, сузили
класс проверяемого, сняли правило линтера как шумное. Каждый пункт — **что
перестали, когда и почему**, со ссылкой на запись журнала ревью.
Этот список **пересматривается первым**, как только что-то проскочило: первый
вопрос по любому пропущенному дефекту — «не тот ли это класс, который мы перестали
проверять». Пункт, из-за которого дефект проскочил, либо возвращается, либо
получает строку «оставляем, цена поимки выше цены дефекта» с датой.
Оба подраздела обязательны; пустой называется пустым.
Читает: `triage`; каждый проход — свою часть.
## Правила ведения
- **Бриф не пересказывает документацию проекта.** Факт, записанный в `CLAUDE.md`
или в архитектуре, попадает сюда ссылкой и одной строкой сути. Два дома для
одного факта разъезжаются, и разошедшийся бриф хуже отсутствующего: он выглядит
актуальным. Исключение одно — раздел инвариантов, он цитируется.
- **Числа — с провенансом, и провенанс проверяется переходом по ссылке.** «Тела
доходили до 42 МБ (замер, ссылка)». Число без источника проход не имеет права
использовать как утверждение. Отдельный и более коварный случай — **число, чей
источник по ссылке не подтверждается**: в документе по ссылке другое число, или
его там нет вовсе. Такое число не выбрасывается и не переписывается по догадке:
оно остаётся с пометкой «расходится с источником: там <что нашли>», а проход
обязан читать его как условие, а не как замер. Молча подставить «правильное»
число хуже всего — расхождение перестанет быть видно, а причина его останется.
- **Пустой пункт называется пустым.** «Внешних зависимостей нет — смотри на диск
и на СУБД», «прецедентов не накоплено», «наблюдений на живых данных не ведём»,
«измеренных чисел нагрузки нет», «сознательно ничего не отключали». Отсутствие
строки читается проходом как «здесь не написали», и он тратит обязательный
вопрос впустую либо заполняет пробел выдумкой. Прямое «пусто» стоит одной
строки и экономит проход целиком.
- **Назначенное помечается назначенным.** Бриф отражает решения проекта, но
местами оказывается **первым** местом, где решение вообще записано: severity у
инварианта, адресат дорогой проверки, периметр, восстановленный из конфига.
Так можно — молчать хуже, — но пометка обязательна («выведена по обратимости»,
«назначен брифом, владельцем не подтверждён»). Проход имеет право знать, чьё
это суждение, а владелец — увидеть, что за него что-то решили.
- **Что вне модели — называется явно.** Это относится и к угрозам, и к нагрузке,
и к классам находок, которые проект сознательно перестал проверять (последние —
в свой подраздел `## Недоступно проверке`, а не вперемешку с принципиальным).
- **Бриф подчиняется промоуту.** Свойство, ставшее правилом линтера, из брифа
вычёркивается — как и из конвенций, и из charter'ов (см.
[promote.md](promote.md), шаг 3).
- **Когда обновлять:** сменился гейт; появился новый контур, зависимость или
источник входа; журнал ревью получил запись вида «проход не мог этого знать»;
воспроизвели дефект — он идёт в `## Прецеденты`. Планового пересмотра нет.
- **Заводится и обновляется шагом, а не руками** — скиллом
`av-dev-pipeline:project-brief`. Он же вызывается автоматически, когда конвейер
или пайплайн задачи не нашли брифа ни по одному пути.
- **Бриф ведёт проект**, а не плагин. Файл живёт в репозитории проекта; плагин
его читает и заводит по шаблону, но не хранит у себя и не подменяет.