Files
transcriber/openspec/changes/archive/2026-08-12-pocketbase-storage/review/report.md
T
av 01cc31d45f хранилище, файлы записей и очередь переведены на встроенную PocketBase
- записи, метаданные и файлы съехались под один каталог данных; появилась
  панель владельца, а gin, goqu, goose и требование CGO ушли
- захват задачи стал одним запросом с RETURNING; заведены число попыток,
  состояние dead и нарастающая пауза вместо признака is_error
- имя файла в хранилище задаёт сервис и в журнал не идёт: вместе с
  идентификатором записи оно собирало бы ссылку на скачивание
2026-08-12 08:31:59 +03:00

18 KiB
Raw Blame History

Отчёт ревью — 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. Оно стояло на посылке о суффиксе имени, которой нет: имя в хранилище и есть <uuid><расширение>, значит утверждение architecture.md верно.

Блокирует мердж

1. Образ не собирается: сборщик golang:1.24, а модуль требует Go 1.25

  • Файл: Dockerfile:2, go.mod:3
  • Severity: major, Confidence: high
  • Оракул: сборка в контейнере golang:1.24-alpinego.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/<id>, затем шаг сохраняет снимок. Итог: expected 999999 / actual 111.
  • Последствие: Save сторожил только acquisition_id, а запись клала все поля снимком с момента захвата. Окно — до восьми часов. Ни строки в журнале, ни отказа в панели. Панель ради правки и покупалась.
  • Найдено: ops.
  • Действие: развилка

3. Два из трёх шагов конвейера переписаны и не выполнены ни одним тестом

  • Файл: internal/service/transcribe.gotranscribeJob, 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.