Files
transcriber/docs/review.md
T
av 4d1c2bf44c Канон документов, каталог задач и OpenSpec
docs/ по канону 12: паспорт с целью проекта, архитектура сегодняшнего
устройства, схема хранилища, модель угроз, конвенции кода, журнал ревью.
Конвенции перенесены из jellybit; места, где код им не следует, помечены
строкой «Расхождение» как объявленный долг.

tasks/ с роадмапом: две достигнутые цели, две запланированные (веб и
многопользовательский режим), два направления (все форматы, долгие
записи) и пять задач в беклоге.

openspec/config.yaml — маршрутизатор с адресами документов, спек пока нет.

CLAUDE.md переписан по форме канона: инварианты с severity, семантика
гейта, запреты с путями. Taskfile получил task gate.
2026-08-10 21:19:07 +03:00

15 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 — любой её кусок;
  • введение веб-UI: транспорт, шаблоны и статика одновременно;
  • изменение, трогающее оба входа сразу — Telegram и HTTP.

Незнакомое здесь (поднимает до large, ось формы решения):

  • вход через OIDC и разграничение доступа: как связаны пользователь Telegram и пользователь веба, до начала работы назвать нельзя;
  • работа с записями в несколько часов: потолки внешних сервисов не замерены, форма решения зависит от замера;
  • приём дорожки из видео и форматов, которых 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