- записи, метаданные и файлы съехались под один каталог данных; появилась панель владельца, а gin, goqu, goose и требование CGO ушли - захват задачи стал одним запросом с RETURNING; заведены число попыток, состояние dead и нарастающая пауза вместо признака is_error - имя файла в хранилище задаёт сервис и в журнал не идёт: вместе с идентификатором записи оно собирало бы ссылку на скачивание
18 KiB
Отчёт ревью — 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-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/<id>, затем шаг сохраняет снимок. Итог: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;activityLoggerPocketBase пишет в свою таблицу. Строка «Расхождение:» вlogging.mdуказывает на удалённую библиотеку.
Гипотезы без доказательства
- Ответ SpeechKit об отказе операции может нести S3-URI и уехать в журнал и в
error_text. Оракул требует настоящего отказа Yandex — запрещеноCLAUDE.md. - Имя файла в хранилище принимает почти любой хвост имени отправителя. Выхода за каталог нет (15 враждебных имён). Станет находкой с появлением экрана списка записей.
entity.File.FileNameнесёт два смысла — имя в хранилище и ключ объекта. Сегодня они совпадают дословно.- Отмена контекста структурно невозможна:
RunInTransaction/FindRecordByIdконтекста не принимают. Замер: под внешней блокировкой захват держится 9.53 с приbusy_timeout=10000. Уточнение уже объявленного долга. - Таймаутов у Telegram, S3 и SpeechKit по-прежнему нет. Задача заведена.
- Приём пишет тело на диск дважды. Не замерено; нагрузка объявлена вне модели.
Ниже потолка названы и не потеряны: LostAcquisitionError типом там, где
конвенция просит sentinel; форма обёрток "failed to <действие>: %w"; уровень
ERROR с инкрементом метрики на потерянном захвате против WARN конвенции;
панель как третий вход в таблицу задач.
Promote candidates
govulncheck ./...шагом гейта. Сегодня его нет ни в гейте, ни в списке «чего в гейте намеренно нет». Достижимых уязвимостей две, унаследованных:GO-2026-6061(grpc),GO-2026-5764(aws eventstream, s3).- Сверка версии Go в
Dockerfileс директивойgoвgo.mod. Блокер 1 прошёл весь конвейер молча именно потому, что образ никто не собирает. - Покрытие изменённых строк. Блокер 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). Покрытие:transcribeJob0 → 67.6%,checkTranscribeJob0 → 72.4%,completeJob0 → 100%, обаFindAndRun*→ 100%; - «стоит исправить» 2 — класс отказа SDK сохраняется через
smithy.APIError, адрес объекта — нет; - «стоит исправить» 3 — журнал входящих запросов вернулся своим слоем в
main.goс пятью полями конвенции,/healthи/metricsнаDEBUG; расхождение вlogging.mdснято.
Решено развилкой, вариант назван человеку на чекпоинте:
- «стоит исправить» 1 — норму привели к коду: хранилище даёт единственный способ убрать копию, зовёт его шаг. Замкнуть уборку на репозиторий мешает конвертация: ей нужны две копии разом. Цена названа — норма держится проверками, и проверки заведены на приём и на шаг конвертации.
Оставлено, передано урожаем: все шесть гипотез и четыре находки ниже потолка; три кандидата в promote.