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

38 KiB
Raw Blame History

Бриф проекта — контракт

Конвейер общий, а находки — проектные. Проход, не знающий, что в этом проекте нельзя нарушать, чем краснеет гейт и сколько данных реально проходит через узел, выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать.

Поэтому проектная специфика живёт в одном файле проекта, а не в charter'ах агентов. Charter описывает метод прохода (что он делает и почему именно так), бриф — предмет (что здесь дорого, чем это меряется, где лежит).

Шаблон для заполнения — 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, шаг 3).
  • Когда обновлять: сменился гейт; появился новый контур, зависимость или источник входа; журнал ревью получил запись вида «проход не мог этого знать»; воспроизвели дефект — он идёт в ## Прецеденты. Планового пересмотра нет.
  • Заводится и обновляется шагом, а не руками — скиллом av-dev-pipeline:project-brief. Он же вызывается автоматически, когда конвейер или пайплайн задачи не нашли брифа ни по одному пути.
  • Бриф ведёт проект, а не плагин. Файл живёт в репозитории проекта; плагин его читает и заводит по шаблону, но не хранит у себя и не подменяет.