# Отчёт ревью — change `pocketbase-storage` Метка `large`, режим «по графу». Составил проход `review-triage`; файл записал оркестратор — среда запрещает подагенту писать отчётные `.md`. Раздел «Исход по находкам» дописан оркестратором после отработки и помечен как таковой. ## Сводка - **Размер:** крупное. **Сложность:** незнакомое. **Метка:** `large` — максимум по обеим осям. Триггеры `docs/review.md`: «замена хранилища или переход на PocketBase — любой её кусок», «смена модели очереди», «изменение, трогающее оба входа сразу». - **Гейт:** зелёный, проверен триажем — `task gate`, exit 0, восемь шагов. - **Особенность прогона:** часть находок первой волны оркестратор починил до запуска `ops`. Каждую пометку «ПОЧИНЕНО» триаж проверял в коде. ### Сигнал о заниженной метке Не пришёл. `review-code` возражений не заявил; `review-basics` на этой метке не запускался. Это одно молчание и одно отсутствие, а не подтверждение двумя источниками. ### План разметки задачи с исходом по каждой теме | тема | дом | глубина | кто закрывает | исход | |---|---|---|---|---| | requirements | `openspec/specs/` + дельты | разбор | `specs` | **закрыта**, 6 находок | | autotests | `CLAUDE.md`, «Гейт», «Инварианты» | — | `autotests` | **закрыта**, 3 находки | | conventions | `docs/conventions/` | разбор | `code` | **закрыта**, 9 находок + 3 за срезом потолка | | architecture | `docs/architecture.md` + `passport.md` | доказательство | `architecture` | **закрыта**, 7 находок | | security | `docs/security.md` | доказательство | `adversary` | **закрыта**, 5 находок | | operations | `docs/architecture.md` «Эксплуатация» + `database.md` | доказательство | `ops` | **закрыта**, 3 находки + 3 замера | **Темы без отчёта нет.** `basics` не запускался по условию состава при метке `large` — темы он не уносил. Отдельно: результатов **ревью дизайна** на вход триажа не подавали. Сошлись ли ответы автора о трёх формах решения с построенным — на этом прогоне не проверено никем. ### Счёт находок 36 пунктов от шести проходов → 27 причин после дедупликации → 12 починено до `ops` (одна частично), 1 снята как неверная, 14 осталось плюс 1 новая, вскрытая проверкой починок. В отчёте: 3 блокирующих, 3 «исправить сейчас», 6 гипотез, 3 promote. ### Проверка починок на месте | находка | чем проверено | вердикт | |---|---|---| | `MaxSize: 0` = 5 МиБ (specs-1 = code-1 = adversary-2) | `entity.MaxRecordSize = 8 << 30` у поля файла и у тела приёма; тест судит границу `5<<20 + 1` и способен упасть | полна | | хук панели правил записи конвейера (specs-2 = code-2 = adversary-3 = architecture-1) | `OnRecordUpdateRequest`; два теста, включая обратный | полна в объявленном объёме; остаток — блокер 2 | | 413 на 32 МиБ и `ReadTimeout` 5 минут | `apis.BodyLimit(entity.MaxRecordSize)` на маршруте, `ReadTimeout = 0` | полна | | гонка `srv` | буферизованный канал, чтение `select`/`default` | полна | | `file` без `Required` | `Required: true`, шаг схемы на сервер не уезжал | полна | | спека утверждала про суффикс имени | прогон: имя в хранилище — ровно заданное, суффикса нет | полна | | рабочая копия без расширения | `Stage(ext string, …)`, приём передаёт расширение | полна | | инвариант «колонка в N местах» | возвращён в `CLAUDE.md` с перечнем мест | полна | | две записи ERROR на один отказ приёма | лог из обработчика снят | полна | | расхождения с `conventions/database.md` | обе строки на месте | полна | | **вечная ссылка на чужое аудио через журнал** | прогон: `Open` и `Localize` отдают текст с идентификатором записи, имени файла в нём нет | путь А закрыт; путь Б закрыт наполовину — см. «Стоит исправить», п. 2 | | пустой держатель в `Save` | комментарий исправлен, ветка оставлена | частично, и этого достаточно | **Снято как неверное:** замечание `architecture` про ключ объекта в Object Storage. Оно стояло на посылке о суффиксе имени, которой нет: имя в хранилище и есть `<расширение>`, значит утверждение `architecture.md` верно. ## Блокирует мердж ### 1. Образ не собирается: сборщик `golang:1.24`, а модуль требует Go 1.25 - Файл: `Dockerfile:2`, `go.mod:3` - Severity: major, Confidence: high - Оракул: сборка в контейнере `golang:1.24-alpine` → `go.mod requires go >= 1.25.0 (running go 1.24.13; GOTOOLCHAIN=local)`; на `golang:1.25-alpine` → успех. Директива не наша: `pocketbase@v0.39.10/go.mod` объявляет `go 1.25.0`. - Последствие: `task image` падает — выкладки change нет вовсе. Гейт этого не показывает: образ он не собирает, а `go build` идёт на хостовом Go. - Найдено: **никем** — вскрылось проверкой починок (триаж). - Действие: инлайн ### 2. Правка задачи в панели пропадает при ближайшем сохранении шага - Файл: `transcript_job_repo.go`, `job_mapping.go`, `panel.go` - Severity: major, Confidence: high - Оракул: временный тест — задача с `tg_chat_id = 111` захвачена шагом, правится через `PATCH /api/collections/transcribe_jobs/records/`, затем шаг сохраняет снимок. Итог: `expected 999999 / actual 111`. - Последствие: `Save` сторожил только `acquisition_id`, а запись клала **все** поля снимком с момента захвата. Окно — до восьми часов. Ни строки в журнале, ни отказа в панели. Панель ради правки и покупалась. - Найдено: `ops`. - Действие: развилка ### 3. Два из трёх шагов конвейера переписаны и не выполнены ни одним тестом - Файл: `internal/service/transcribe.go` — `transcribeJob`, `checkTranscribeJob`, `completeJob` - Severity: major, Confidence: high - Оракул: покрытие с `-coverpkg=./...` — 0.0% у всех трёх и у обоих `FindAndRun*`; `git diff --stat` по файлу — 268 вставок, 192 удаления. - Последствие: путь «отдать запись на распознавание → дождаться операции → ответить пользователю» прошёл замену хранилища без единого выполнения. - Найдено: `autotests`. - Действие: инлайн ## Стоит исправить сейчас ### 1. Документы обещают, что рабочую копию убирает хранилище; убирает вызывающий - Severity: minor, Confidence: high. Найдено: `architecture`. Действие: развилка. - `architecture.md`, `design.md` и дельта-спека `storage` требуют, чтобы уборку делало хранилище. В коде копию закрывает вызывающий тремя `defer`. Дефекта в коде нет — расхождение в записи, которая уедет в архив. ### 2. Отказ заливки в Object Storage больше не отличим от любого другого - Severity: minor, Confidence: high. Найдено: **никем** — вскрылось проверкой полноты починки. Действие: инлайн. - Починка приватности оборвала цепочку `%w` целиком: отозванные ключи, исчезнувший бакет и отказ сети дают одну строку. ### 3. Входящие HTTP-запросы исчезли из журнала процесса - Severity: minor, Confidence: high. Найдено: `code`. Действие: развилка. - `sloggin` ушёл вместе с gin; `activityLogger` PocketBase пишет в свою таблицу. Строка «*Расхождение:*» в `logging.md` указывает на удалённую библиотеку. ## Гипотезы без доказательства 1. Ответ SpeechKit об отказе операции может нести S3-URI и уехать в журнал и в `error_text`. Оракул требует настоящего отказа Yandex — запрещено `CLAUDE.md`. 2. Имя файла в хранилище принимает почти любой хвост имени отправителя. Выхода за каталог **нет** (15 враждебных имён). Станет находкой с появлением экрана списка записей. 3. `entity.File.FileName` несёт два смысла — имя в хранилище и ключ объекта. Сегодня они совпадают дословно. 4. Отмена контекста структурно невозможна: `RunInTransaction`/`FindRecordById` контекста не принимают. Замер: под внешней блокировкой захват держится 9.53 с при `busy_timeout=10000`. Уточнение уже объявленного долга. 5. Таймаутов у Telegram, S3 и SpeechKit по-прежнему нет. Задача заведена. 6. Приём пишет тело на диск дважды. Не замерено; нагрузка объявлена вне модели. Ниже потолка названы и не потеряны: `LostAcquisitionError` типом там, где конвенция просит sentinel; форма обёрток `"failed to <действие>: %w"`; уровень `ERROR` с инкрементом метрики на потерянном захвате против `WARN` конвенции; панель как третий вход в таблицу задач. ## Promote candidates 1. **`govulncheck ./...` шагом гейта.** Сегодня его нет ни в гейте, ни в списке «чего в гейте намеренно нет». Достижимых уязвимостей две, унаследованных: `GO-2026-6061` (grpc), `GO-2026-5764` (aws eventstream, s3). 2. **Сверка версии Go в `Dockerfile` с директивой `go` в `go.mod`.** Блокер 1 прошёл весь конвейер молча именно потому, что образ никто не собирает. 3. **Покрытие изменённых строк.** Блокер 3 — второй случай подряд, когда его ловят руками. ## Границы покрытия - Запускались: `specs`, `code`, `architecture`, `adversary`, `autotests` — по коду **до** починок первой волны; `ops` — по коду **после**. Находки пяти первых перепроверены триажем поимённо. - `basics` не запускался: при метке `large` темы разобраны именными проходами, своих тем проекта нет. - Независимой реализации для сравнения не строил никто — прохода нет в конвейере. - Поимённая сверка с руководствами по стилю Go не задавалась ни одним проходом. Для change, который впервые в проекте пишет на PocketBase, это самый дорогой пробел. - **Блоки `Coverage of this pass` до триажа дошли не от всех проходов.** О своих потолках сообщил только `code` (конвенционная половина, 4, за срезом три находки — перенесены). Остальные не сообщили; это находка о прогоне. - Решения проекта (`docs/adr/`) и записанные наблюдения (`docs/research/`) прогон не открывает — процессные документы. Расхождение change с записанным решением ловит не ревью, а сверка документации (`av-dev-docs:healthcheck`). - Не проверит ни один проход: поведение SpeechKit и Object Storage под нагрузкой и на границах; реальный профиль нагрузки; стойкость `ffmpeg` к вредоносному входу. - Перестали проверять сознательно: разбор вывода настоящего `ffprobe` — решение и цена в `ADR-2026-08-11-stub-adapters-in-tests.md`. - Каких документов не хватило: `docs/conventions/web-ui.md` описывает будущую SPA, а не панель — судить панель против конвенции было нечем; `docs/research/` по весу шестичасовой записи пуст, оценки читались как оценки. ## Исход по находкам *Раздел дописан оркестратором после отработки урожая; в отчёте триажа его нет.* **Починено:** - блокер 1 — `Dockerfile` переведён на `golang:1.25-alpine`, строки про Go 1.24 в `CLAUDE.md` и `README.md` исправлены; сборка в контейнере проверена; - блокер 2 — запись шага разделена: `applyOwnedByPipeline` кладёт только поля конвейера, `applyToRecord` целиком остаётся заведению. Заведён тест `TestSave_KeepsOwnerEditMadeWhileStepHeldTheJob`; норма дописана в дельта-спеку `pipeline` со сценарием; - блокер 3 — заведён управляемый двойник распознавателя и восемь проверок (`internal/service/recognition_test.go`). Покрытие: `transcribeJob` 0 → 67.6%, `checkTranscribeJob` 0 → 72.4%, `completeJob` 0 → 100%, оба `FindAndRun*` → 100%; - «стоит исправить» 2 — класс отказа SDK сохраняется через `smithy.APIError`, адрес объекта — нет; - «стоит исправить» 3 — журнал входящих запросов вернулся своим слоем в `main.go` с пятью полями конвенции, `/health` и `/metrics` на `DEBUG`; расхождение в `logging.md` снято. **Решено развилкой, вариант назван человеку на чекпоинте:** - «стоит исправить» 1 — норму привели к коду: хранилище даёт единственный способ убрать копию, зовёт его шаг. Замкнуть уборку на репозиторий мешает конвертация: ей нужны две копии разом. Цена названа — норма держится проверками, и проверки заведены на приём и на шаг конвертации. **Оставлено, передано урожаем:** все шесть гипотез и четыре находки ниже потолка; три кандидата в promote.