# Шаблон брифа проекта Образец заполнения. Контракт разделов — в [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: ложных срабатываний больше трети (журнал, там же). Вернуть, если проскочит дефект «функция делает три вещи».