Роадмап: web-access переименована под приложение, которое ставится на телефон; заведена цель ready-notification — уведомление о готовности без открытого приложения. Беклог: десять задач. Многопользовательская цепочка (oidc-login, record-ownership, telegram-account-link), веб (json-api-for-spa, spa-skeleton, upload-and-status-screen, records-list-screen, installable-pwa), уведомления через apprise и ntfy, разведка выбора фреймворка. Очередь: долги, хранилище, вход, приложение. Решение сменилось с htmx на SPA, поэтому conventions/web-ui.md снята целиком и оставлена честной строкой до итога разведки. Открытые вопросы архитектуры, границы паспорта и триггеры метки ревью приведены в соответствие.
16 KiB
Ревью: настройка и журнал
Как настроен конвейер
Конвейера ревью в проекте пока нет: плагин не подключён, ни одного прогона не было. Раздел заполнен наперёд по коду — он и служит настройкой первому прогону.
Типовые узлы
Рода узлов проекта и проверяемые свойства к каждому.
Шаг конвейера (FindAndRunConversionJob, FindAndRunTranscribeJob,
FindAndRunTranscribeCheckJob):
- отличает «задач нет» от отказа и не считает первое ошибкой;
- при отказе на середине оставляет задачу в состоянии, из которого повтор
корректен, либо переводит в
failedосознанно; - не теряет ссылку на файл:
job.FileIDпереставляется только после того, как запись о новом файле создана; - повтор шага на той же задаче не создаёт лишних файлов и записей;
- отвечает пользователю ровно один раз.
Транспорт (internal/controller/tg, internal/controller/http):
- проверяет право отправителя до всякой работы;
- не логирует ошибку, которую уже залогировал доменный слой;
- переводит доменную ошибку в свой ответ, а не отдаёт сырой текст;
- закрывает то, что открыл, на всех ветках выхода.
Клиент внешнего сервиса (adapter/recognizer/yandex, adapter/telegram):
- имеет таймаут и не виснет, когда внешний сервис не отвечает;
- не кладёт секрет в URL и не даёт ему утечь через ошибку транспорта;
- различает «сервис ответил отказом» и «сервис недоступен»;
- вырожденный ответ (пустой, усечённый, без ожидаемого поля) не превращает в успех молча.
Репозиторий SQLite (adapter/repo/sqlite):
- список колонок совпадает во всех четырёх запросах файла;
NULLв колонке разбирается в указатель, а не роняетScan;- захват задачи не выдаёт одну строку двум вызывающим;
- ошибка драйвера транслируется в доменную у источника.
Обёртка над внешним процессом (adapter/converter/ffmpeg,
adapter/metaviewer/ffmpeg):
- отсутствие программы в
PATHотличается от отказа обработки; - вход, пришедший от пользователя, не попадает в аргументы командной строки неразобранным;
- пустой или частично записанный выходной файл считается отказом;
- процесс не висит вечно.
Любой узел — сверх свойств своего рода:
- изменённое место покрыто хоть одним проходящим тестом. Тест, который никогда не был зелёным, обнуляет сигнал всего пакета: настоящий отказ в нём становится неотличим от привычного шума (журнал, запись 2026-08-10).
Типовые ложноположительные
- «Воркер глотает ошибку
NoopJobError». Не дефект: этот тип означает «задач в этом состоянии нет», иinternal/controller/worker/worker.goнамеренно не логирует его и не считает в метрику. Настоящий дефект рядом другой — проверка идёт приведением типа и сломается при первой же обёртке; он уже записан в conventions/errors.md. - «Захват задачи не в транзакции — гонка двух воркеров». По построению её нет: три воркера читают три разных состояния, и одну строку они не делят. Находка становится настоящей ровно тогда, когда появится второй экземпляр процесса или второй воркер на то же состояние.
- «Файлы и объекты не удаляются, диск растёт». Факт верный и записан в database.md; срок хранения не задан сознательно, задачи на него нет. Новой находкой это не считается, пока не измерен рост.
- «HTTP API открыт без аутентификации». Известно и записано первой строкой security.md. Находкой считается только новая поверхность, выставленная наружу, а не повторение этого факта.
Вопросы по темам
Форма: <тема>: <вопрос> (<провенанс>).
operations: пережил ли шаг конвейера отмену контекста на середине — воркеры получаютctx, но ни один шаг его внутрь не передаёт (чтениеworker.goиtranscribe.go, 2026-08-10).operations: появился ли таймаут у обращения к Telegram, S3 и SpeechKit — ни у одного из них таймаута нет (чтениеtg.go,s3.go,speechkit.go, 2026-08-10).operations: не удвоилась ли запись об одном сбое — шаг логирует ошибку и возвращает её воркеру, который логирует снова (чтениеtranscribe.go, 2026-08-10).security: не попал ли в лог текст расшифровки, имя файла пользователя или URL с токеном бота (запрет в security.md и conventions/logging.md).security: не строится ли путь на диске или ключ объекта из значения, пришедшего снаружи, — расширение файла сегодня берётся из имени отправителя (чтениеservice/transcribe.go, 2026-08-10).architecture: не появился ли второй путь приёма мимоcreateTranscribeJob— сегодня через него идут оба входа (architecture.md, «Единые точки проекта»).architecture: не поехало ли поведение вarchitecture.mdвместо спеки — спек ещё нет, и соблазн описать поведение в обзоре максимальный.conventions: новая колонка правится во всех четырёх местах репозитория (CLAUDE.md, «Инварианты»).autotests: покрыт ли изменённый шаг конвейера хоть одним тестом — сегодня тестов два файла, и оба мимо конвейера.
Триггеры метки
Проектная конкретизация правила выбора метки. Умолчание — medium.
Крупное здесь (поднимает до large, ось объёма):
- изменение, трогающее конвейер задач целиком: состояние, воркер, шаг сервиса и колонку разом;
- замена хранилища или переход на PocketBase — любой её кусок;
- каркас приложения: сборка фронтенда, раздача статики и шаг гейта разом;
- изменение, трогающее оба входа сразу — Telegram и HTTP.
Незнакомое здесь (поднимает до large, ось формы решения):
- вход через OIDC и разграничение доступа: как связаны пользователь Telegram и пользователь приложения, до начала работы назвать нельзя;
- всё, что делается на выбранном фреймворке впервые: форма решения нащупывается по ходу, пока конвенция веб-UI пуста;
- установка на телефон: service worker перехватывает запросы, и что он кэширует, до работы назвать нельзя;
- работа с записями в несколько часов: потолки внешних сервисов не замерены, форма решения зависит от замера;
- приём дорожки из видео и форматов, которых
ffmpegне берёт текущей командой; - всё, что требует записи в
research/прежде, чем начать.
Мелкое здесь (опускает до small):
- правка текста, который видит пользователь Telegram;
- новая метрика в
internal/metrics; - правка
config.dist.tomlи умолчанийdefaultConfig()без нового поля; - правка документов канона.
Помни отрицательный тест: миграция, формат файла на диске, публичный контракт
API и имя не откатываются обратной правкой после мерджа — какими бы маленькими
ни были, они не small.
Недоступно проверке
Не проверит ни один проход:
operations: поведение внешних сервисов под нагрузкой и на границах — SpeechKit и Object Storage поднять в тесте нечем;operations: реальный профиль нагрузки. Проект работает на единицах записей в день, и утверждения о росте остаются условиями, а не замерами;security: стойкостьffmpegк вредоносному входу — разбор чужого формата отдан внешней программе, и она вне нашей границы.
Перестали проверять сознательно:
Ничего не отключали — проверять пока и не начинали.
Журнал дефектов
Первая запись найдена прогоном гейта при заведении канона 2026-08-10, две
нижние восстановлены по истории git тогда же. Все три помечены проскочил:
ревью тогда не было, и поймать их было некому. У восстановленных нет поля «Чем
воспроизведён», и выдумывать его задним числом нельзя.
2026-08-10 — тесты http-обработчика ни разу не были зелёными [проскочил]
- Где:
internal/controller/http/transcribe_test.go - Симптом:
go test ./...падает четырьмя случаями; обнаружено первым же прогоном гейта при заведении канона - Причина: тест требует
testdata/sample.m4a, которого в репозитории нет и не могло быть —.gitignoreсодержит*.m4a. Остальные случаи записывают в файл строкуtest audio contentи ждут201, а обработчик зовёт настоящийffprobe, который такой вход отвергает - Чем воспроизведён:
go test ./internal/controller/http/— четыре отказа, из них один по отсутствию файла и три по коду500вместо201 - Почему не поймали: гейта не было вовсе, а
go testруками, судя по результату, не гоняли ни разу с коммита87d8b05 - Что меняем: заведена задача
http-handler-tests-never-green; в гейт добавлен шагgo test ./..., и красный тест теперь виден. Настоящий остаток шире: тест, который никогда не проходил, обнуляет сигнал всего пакета — в типовые узлы добавлено свойство «покрыт хоть одним проходящим тестом», а в вопросы темыautotests— вопрос про изменённый шаг конвейера
2025-10-23 — пустой ответ вместо текста расшифровки [проскочил]
- Где:
internal/service/transcribe.go, ветка завершения задачи - Симптом: пользователь Telegram получал пустое сообщение вместо текста
- Причина: SpeechKit возвращал операцию успешной, но с пустым текстом, и задача завершалась этим пустым значением
- Чем воспроизведён: восстановлено по коммиту
ec637c0, оракула нет - Почему не поймали: конвейера ревью не существовало
- Что меняем: уже сделано — пустой текст подменяется фразой «на записи нет текста». Настоящий остаток в другом: свойство «вырожденный ответ внешнего сервиса не превращается в успех молча» вынесено в типовой узел «клиент внешнего сервиса» выше
2025-08-17 — длинная расшифровка не доходила до пользователя [проскочил]
- Где:
internal/adapter/telegram/sender.go - Симптом: отправка текста длиннее предела сообщения Telegram завершалась ошибкой целиком, пользователь не получал ничего
- Причина: предел длины сообщения на стороне Telegram не учитывался
- Чем воспроизведён: восстановлено по коммиту
822e168, который тем же заходом завёлinternal/adapter/telegram/split_test.go - Почему не поймали: конвейера ревью не существовало
- Что меняем: уже сделано — деление по словам с пределом 4000 символов, число записано в database.md