Files
dev-skills/av-dev-pipeline/skills/review-pipeline/references/brief-template.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

25 KiB

Шаблон брифа проекта

Образец заполнения. Контракт разделов — в 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, на masterHEAD~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/ГГГГ/ММ/ДД/<ulid>.json.gz, дата берётся из времени приёма, имя — из генератора идентификаторов; ключ записи — метрика + слой + начало + конец, источник в ключ не входит.
  • Разграничение: статические токены в Authorization: Bearer, раздельные на запись и на чтение; конфиг под 0600.
  • Что дороже: данные пользователя дороже токена. Путь, по которому значение доезжает до лога выше DEBUG, до ответа с ошибкой или до testdata в git, — полноценная находка, а не замечание по гигиене.
  • Вне модели: злоумышленник в локальной сети; вредоносный оператор; компрометация поставщика данных; мультиарендность. Находки этих классов не выводятся — они никогда не будут исправлены.

Карта

  • Основная ветка: master. От неё берутся ветки задач, в неё вливается батч, база диффа по умолчанию — git merge-base HEAD master (на самой ветке HEAD~1).
  • Актуальные спеки: openspec/specs/<capability>/spec.md
  • Дельта-спеки изменения: openspec/changes/<id>/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: ложных срабатываний больше трети (журнал, там же). Вернуть, если проскочит дефект «функция делает три вещи».