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