# Ревью: настройка и журнал Проектная часть конвейера ревью: чем jellybit отличается от абстрактного Go-сервиса и что здесь уже проскакивало. Устройство самого конвейера (профили, стадии, контракт находок) живёт в скилле, а не здесь. ## Как настроен конвейер ### Типовые узлы Рода узлов проекта и проверяемые свойства к каждому. Род, а не инвентарь пакетов: узел, которого ещё нет, но который проект заведёт, включён намеренно. **Клиент внешнего HTTP-сервиса** (`qbt`, `llm`, `metadata`, `jellyfin`, `tgbot`) - у каждого исходящего вызова свой таймаут из конфига, не дефолт транспорта; - `context` доходит до запроса и отменяет его, а не игнорируется; - ошибка зависимости отличима от ошибки нашей логики на приёме результата; - секреты (пароль, ключ, токен) не попадают ни в лог, ни в текст ошибки; - недоступность **опциональной** зависимости (метабаза, Jellyfin, Telegram) не двигает состояние загрузки и не краснеет ERROR-ом в фоновом цикле. **Тик воркера и переход состояния** - переход легален по декларативному графу, а не «просто присвоили `state`»; - работа идёт под per-download блокировкой; два транспорта не гонятся; - тик идемпотентен: повтор на том же состоянии не порождает второго эффекта; - отмена контекста на середине не оставляет полуприменённого состояния; - новое промежуточное состояние имеет выход **и** предохранитель по времени. **Репозиторий `store`** - запрос параметризован, время только через `store.Now()`, id через `ident`; - многошаговое изменение — в одной write-транзакции (`_txlock=immediate`); - инвариант, не выражаемый схемой («одна активная загрузка на infohash»), держится guarded-методом, а не проверкой в вызывающем коде; - миграция forward-only и сопровождается правкой [database.md](database.md). **Операция с файловой системой** (`layout`) - целевой путь проверяется **после** `filepath.Clean`, на принадлежность библиотеке; - существующая цель не перезаписывается ни при каком исходе; - под `paths.downloads` нет ни одной операции записи или удаления; - частичный сбой батча оставляет систему в состоянии, из которого повтор доводит начатое или откатывает целиком; - удаление снимает только свои ссылки своего батча и не снимает последнюю копию. **Парсер недоверенного входа** (`magnet`, `torrent`, парсер сообщения бота, разбор ответа LLM) - вход враждебный по умолчанию: длина, вложенность, мусорные байты, пустота; - разбор не паникует и не аллоцирует по числу из самого входа; - невалидный вход даёт доменную ошибку, а не тихий дефолт; - результат нормализуется на границе (lowercase hex, trim, `ident.Parse`). **htmx-хендлер** - один партиал обслуживает страницу и фрагмент, ветвление по `isHTMX`; - ошибка на htmx-пути — 200 плюс фрагмент, а не 4xx/5xx; - страница деградирует без JS; - поллинг самозавершается, когда наблюдать больше нечего. ### Типовые ложноположительные Находки, которые здесь выглядят убедительно и всегда неверны. - **«Веб-UI и REST без авторизации».** Принятое решение под сегодняшний периметр — [security.md](security.md). Дефектом станет только вместе с путём снаружи LAN. - **«Ошибка на htmx-пути возвращает 200».** Так и задумано — [conventions/web-ui.md](conventions/web-ui.md). - **«Решение auto/review должно опираться на `confidence` модели».** Наоборот: авто только при подтверждённом матче в базе — [ADR-2026-06-13-auto-link-requires-db-match](adr/ADR-2026-06-13-auto-link-requires-db-match.md). Самооценка LLM плохо откалибрована и поддаётся инъекции. - **«Копировать надёжнее, чем хардлинк» / «взять симлинк».** Хардлинк — осознанный выбор ради неприкосновенности источника и недублирования диска, [ADR-2026-06-13-hardlinks](adr/ADR-2026-06-13-hardlinks.md); copy — только фолбэк. - **«Целочисленный автоинкрементный ключ был бы проще».** ULID — требование capability `identity`; `AUTOINCREMENT` вдобавок краснит гейт. - **«Не хватает метрик, трейсинга, health-эндпоинтов по каждой зависимости».** «Минимум компонентов» — принцип проекта; глубокий healthcheck заведён задачей и ждёт своей очереди, а не является упущением. - **«Здесь нужен интерфейс, чтобы это можно было замокать».** Единственная реализация за интерфейсом — обычно лишний слой; см. «Честный предел» ниже. - **«Нет ретрая у вызова в фоновом цикле».** Тик повторится сам через `poll_interval`; ретрай внутри тика чаще вреден. - **«Оригинальное и локализованное названия дублируются — избыточность».** `original_title` заполняется всегда и при неуверенности дублирует `title` — это контракт capability `recognition`, а не недосмотр. ### Вопросы к проходам Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Журнал дефектов пока пуст, поэтому провенанс у всех пунктов — инвариант или ADR, а не пойманный случай; по мере накопления журнала список должен смещаться в сторону реальных промахов. - `adversary`: можно ли, управляя только именами файлов в раздаче и текстом контекста, добиться целевого пути вне `paths.movies`/`series` — включая путь через юникод, длину сверх лимита ФС и коллизию после нормализации? (инвариант «целевой путь строго под библиотекой», [security.md](security.md)) - `adversary`: есть ли последовательность команд, после которой снимается **последняя** копия данных — с учётом `superseded`-ссылок и гонки со сверкой? (инвариант «источник неприкосновенен») - `adversary`: что даёт крафт-магнет с чужим или подставным инфохэшем — присоединение к чужой активной загрузке, отравление владения? (открытая задача про идентичность инфохэшей) - `ops`: что делает эта ветка, когда qBittorrent недоступен несколько минут подряд — сколько ERROR-строк в секунду и меняется ли состояние задач? (задача про ERROR-шторм фоновых циклов) - `ops`: как это ведёт себя при сотне загрузок в базе и десятках тысяч `file_link` — есть ли запрос без индекса и полный проход по таблице? (задача про масштаб 100/1000, [database.md](database.md) → «Настройки») - `ops`: что остаётся на диске и в базе, если процесс убит посреди раскладки батча? (состояние `linking` и его восстановление) - `code`: логирующий чекпоинт один на операцию — или ошибка залогирована и возвращена вверх, где залогирована снова? ([conventions/logging.md](conventions/logging.md)) - `code`: новое поле конфига появилось в `config.example.toml` с описанием назначения, диапазона и единиц? ([conventions/config.md](conventions/config.md)) - `specs`: не завелось ли поведение, которого спека не заказывала — тихий дефолт, проглоченная ошибка, ретрай «на всякий случай», отброшенное поле? - `architecture`: не появился ли второй способ делать то, что уже делается — второе место, где генерится время или id, второй парсер источника, вторая логика целевых имён мимо `naming`? ([architecture.md](architecture.md) → «Единые точки проекта») ### Триггеры профиля Уточняет умолчания конвейера, не отменяет их. - **`deep`** — есть миграция в `internal/store/migrations/`; появляется новый пакет `internal/*`; меняется сигнатура публичной команды воркера; трогается раскладка файлов, построение целевых путей или удаление ссылок; трогается разбор недоверенного входа. - **`standard`** — меняется поведение, видимое снаружи: REST-эндпоинт, htmx-путь, набор или семантика состояний загрузки, формат сообщения бота, поле конфига. - **`quick`** — всё остальное: локальный багфикс, документация, тесты. - **Независимая реализация (`reimpl`)** запускается, когда узел одновременно новый и имеет внешний оракул в виде спеки: новый парсер, новый провайдер за существующим интерфейсом, новая стадия конвейера распознавания. - «Поведение, видимое снаружи» здесь включает **тексты и карточки Telegram** — для единственного пользователя это и есть интерфейс. ### Недоступно проверке **Не проверит ни один проход** — принципиальная граница, по факту промаха не пересматривается. - История инцидентов на umbar и то, что уже ломалось в проде. - Поведение таблицы SQLite под реальным объёмом и профилем нагрузки: реального профиля нет ни у кого, кроме сервера. - Завязка внешних потребителей (Jellyfin, закладки, чужие ссылки) на текущее поведение. - Качество распознавания как таковое: правильно ли LLM определил фильм — вопрос eval-харнеса и корпуса кейсов, а не ревью кода. - Суждение «этой функциональности не должно существовать». **Перестали проверять сознательно** — пересматривается первым, как только что-то проскочило. - **Идиоматичность Go — с 2026-08-04.** Проектный проход `idiom` (поимённая сверка с положениями Effective Go, Go Code Review Comments, стайлгайдов Uber и Google) удалён вместе с проектными копиями агентов при переезде на плагин `av-dev-pipeline`, который этот проход упразднил. Способные части переселены: эксперимент против поведения библиотеки и драйвера — в `ops`, «не изобретаем ли то, что уже есть в библиотеке» — в `architecture`. **Различение «идиоматично против распространено» теперь не спрашивает никто.** Класс обратимый: портит форму кода, не данные. Пересмотр — задача `quality-review-agents`. ## Журнал дефектов Запись на каждый воспроизведённый дефект, **сразу**, а не ретроспективно: со временем теряется не факт, а причина непоймания. Проскочившие — эвал-сет для калибровки конвейера, выборка по пометке. Форма: ## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман] - **Где:** путь:строка либо «конвейер, а не код» - **Симптом:** как обнаружилось, кем и когда - **Причина:** что на самом деле было не так - **Чем воспроизведён:** тест, команда, замер — с числами - **Почему не поймали:** только для проскочивших — какой проход обязан был найти и что ему помешало - **Что меняем:** правило прохода, шаг гейта, конвенция, факт в документе проекта — либо «ничего, цена поимки выше цены дефекта» ### Записи Пока пусто. Журнал заведён 2026-07-23 вместе с переработкой конвейера ([ADR-2026-07-23-review-pipeline-generative](adr/ADR-2026-07-23-review-pipeline-generative.md)); случаи до этой даты не восстанавливались — восстановленная постфактум причина непоймания недостоверна, а именно она и нужна.