- скилл project-brief: бриф собирается из CLAUDE.md, архитектуры, Taskfile и конвенций и показывается человеку. Раньше единственная инструкция по его созданию лежала внутри шаблона, поэтому деградированный режим был не аварийным, а единственным: critical по основанию «нарушен инвариант» недостижим ни на одной задаче - rebase перенесён внутрь worktree задачи: прежняя форма падала на занятой ветке, и агент уводил весь батч в провалившиеся с ложной причиной - контракт брифа дополнен восемью слотами; проверен заполнением на обоих проектах, незаполнимых нет. Прецедент healthlog вынут из общего charter'а в бриф — там он вмёрз вместе с числами - шов: пайплайн задачу не закрывает и записи учёта не трогает, урожай отдаёт списком, правило остатка — ссылкой на av-dev-tasks - деградированный абзац во всех девяти проходах, вопрос 9 в ops, пространство имён в вызовах, раздел предпосылок
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, на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/ГГГГ/ММ/ДД/<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: ложных срабатываний больше трети (журнал, там же). Вернуть, если проскочит дефект «функция делает три вещи».