- storage-without-pocketbase закрыта как реализованная: приёмка сошлась по всем
пяти критериям записи и по двенадцати приёмочным свойствам рубрики ревью
дизайна, работа лежит коммитом c9b7765
- урожай триажа ревью развёрнут в одиннадцать записей с тегом партии
review-2026-08-23 и расставлен по зависимости, а не в конец списка
- находка про признак живости воркера слита в stalled-pipeline-metric: у неё та
же причина — вставший конвейер неотличим от простоя
30 KiB
Беклог
Что можно взять. Одна задача = один файл items/<slug>.md
- строка здесь. Ведётся скиллом
av-dev:task-track.
Стадия проекта — стройка ([tasks] stage = "build").
Порядок строк — зависимость: это план стройки от базы к деталям,
и строка выше сделана раньше не потому, что важнее, а потому, что
иначе нельзя. Секция здесь одна: разложенный по полкам план
перестаёт быть планом. Список пишется вперёд целиком — это не
гниение беклога, а замысел. Пустой беклог значит, что стройка
окончена: дальше tasks.py stage support.
Чем основание отличается от детали в этом проекте. Сначала идёт то, на чём стоит остальное: проверки, которым можно верить, владелец записи, единый контракт API, покрытый тестами конвейер, — и только потом экраны и возможности поверх них. Этот порядок расставили 2026-08-12. Задача, взятая раньше своего основания, стоит дважды: сперва её пишут, потом переписывают под появившееся основание.
Одно место в очереди назначено не человеком, а типом: сырьё
(research без раздела «Вопрос») стоит в конце секции — его не берут.
Отсюда правило для новых записей. Заведённая по ходу работы —
интейком, урожаем ревью, разбором находок — задача встаёт в конец
очереди машинально, и это не её место, а отсутствие места.
Слой ей назначает человек на ближайшем груминге: ниже того, чего она
требует, и выше того, что требует её. Причина уезжает в запись
(move --after <слаг> --reason).
Разведки это касается вдвойне: её исход — новые задачи, и слой они наследуют не от разведки, а от того, что трогают. Разведка о конвейере может принести задачу основания, которой место в голове очереди.
Тип записи стоит первым полем меты и решает, что у неё может быть:
✨ feature 🐞 fix 🧹 chore 🔬 research
Секции «блокеры» здесь нет и не заводится: блокер — это состояние (работа не может продолжаться ни одной задачей), оно живёт до ответа человека, а его следы — вопросами в файлах задач.
Очередь
- 🧹 Поднимать сервис для локальной работы одной командой — Локальная проверка требует ручной чистки каталога данных и остановки процесса, а владельца панели заводят по одноразовой ссылке из журнала.
- 🧹 Вынести тело подкоманды resume из main-пакета и покрыть тестом — go test ./cmd/... отвечает no test files: подкоманда возврата записи в работу не проверена ничем, а тест на неё пришлось бы писать, повторяя её тело руками.
- ✨ Сделать экран загрузки записи и её состояния — Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит.
- ✨ Сделать экран списка своих записей и чтения текста — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем.
- ✨ Дать владельцу править запись, возвращать её в работу и видеть её путь — С записью нельзя сделать ничего: заголовок ставит одна языковая модель, остановленную возвращает в работу только владелец сервиса в панели, а журнал событий пишется и не читается никем, кроме него же
- 🔬 Адрес объекта в тексте отказа SpeechKit — Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись.
- 🧹 Разобрать мелочи http-транспорта — Маршруты зарегистрированы дважды, и переименование пути в cmd/transcriber проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
- 🐞 Судить маршрут по пути из запроса, а не по раскодированной копии — Адрес /%6detrics отдаёт метрики байт в байт: ServeMux сравнивает раскодированный путь, а правило прокси написано на литерал /metrics — тот же класс, что закрытый /%5f/.
- 🧹 Запретить обращаться к Bot API мимо клиента бота — Чистка отказа от адреса с токеном живёт в клиенте; свой http.Client в транспорте вернёт утечку молча — правило noctx такую подмену не ловит, а класс уже стоил одного дефекта.
- 🧹 Свести пять расхождений между документами канона — Сверка 2026-08-13 нашла шесть мест, где два документа отвечают на один вопрос по-разному; одно сведено при повышении раскладки, а три из пяти оставшихся стоят в architecture.md, и по ним читатель строит решения о выкладке и о периметре.
- 🧹 Свести норму схемы со служебной таблицей goose — Таблица учёта goose_db_version несёт второй вид времени и своё умолчание, а норма схемы объявлена абсолютной; сторож перечисляет наши таблицы поимённо и служебной не видит вовсе.
- 🧹 Переименовать требование webapp про путь в журнале — Заголовок требования называет половину нормы — путь чужого корня, — а само требование накрывает обе половины адресного пространства; заголовок это идентичность требования, и синк оставил бы вторую, устаревшую копию.
- ✨ Сопоставить пользователя Telegram с учётной записью — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
- ✨ Пускать скрипты в API по личным токенам — Скрипту недоступны ни браузерная сессия, ни вход у Authelia: домен целиком стоит за прокси, и автоматизировать загрузку нечем.
- 🧹 Покрыть тестами шаги конвейера и захват задачи — Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем.
- 🧹 Проверять ответ обработчика на отказ репозитория — Тесты обработчиков идут по настоящему sqlite без управляемых отказов: класс «база недоступна во время запроса» не проверен нигде.
- 🧹 Покрыть тестами разбор вывода ffprobe — Проверки приёма перестали звать настоящий ffprobe 2026-08-11, а своего теста у адаптера метаданных нет: разбор JSON и отличие «программы нет в PATH» от «обработка отказала» не проверяет ничто.
- 🧹 Задать таймауты обращениям к внешним сервисам — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата.
- 🐞 Различать отказ, который стоит повторить, и приговор записи — Отказ приведения останавливает запись с первой попытки, и предел в пять отказов не работает никогда: разовый сбой ffmpeg останавливает запись приговором, хотя повтор обработал бы её успешно.
- 🐞 Не закрывать базу, пока живёт брошенный воркер — По истечении жёсткого таймаута остановки процесс возвращается из run(), отложенный db.Close() обнуляет пулы, брошенный воркер разыменует nil и роняет процесс паникой; захват записи после этого стоит до восьми часов, и следа не остаётся.
- 🧹 Прервать шаг конвейера отменой контекста — Половина сделана 2026-08-13 — контекст доходит до внешних вызовов, а прерванный шаг оставляет задачу на повтор и не тратит попытку, — но осталось то, ради чего задача заводилась: хранилище контекста не принимает ни одним методом, и бюджет мягкой остановки не замерен.
- 🐞 Убирать записанный файл, когда приём отказал на середине — Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками.
- 🧹 Разобрать мелочи слоя хранилища — Три мелочи ниже потолка триажа: цикл воркера пишет потерю захвата уровнем ERROR и считает её отказом, тип ошибки заведён там, где конвенция просит sentinel, а FileName несёт два разных смысла.
- 🧹 Писать об одном отказе шага одну строку журнала — Временный отказ шага доезжает до владельца двумя строками ERROR: шаг пишет ошибку и возвращает её, воркер пишет её снова — в журнале отказов вдвое больше, чем было.
- 🧹 Разобрать мелочи хранилища на sqlite — Четыре мелочи ниже потолка триажа в одном пакете: три репозитория не переводят sql.ErrNoRows в доменную ошибку, два числа задают один и тот же предел страницы порознь, Append не заполняет время, которое сам же кладёт в базу, а у selectList — мёртвый довод prefix.
- 🧹 Закрепить версию рантайм-базы образа — Финальный слой Dockerfile собирается на alpine:latest, а task image идёт с --pull, поэтому два образа из одного коммита с разницей в неделю несут разный ffmpeg — регрессия конвертации после такой пересборки выглядит как задачи в failed при пустом диффе репозитория, и откат на прежний коммит её не чинит.
- 🧹 Отдавать файл записи через http.ServeContent — Отдача файла считает диапазоны байт своей рукой — около 110 строк семантики HTTP, которую стандартная библиотека делает сама, и на этих строках стоит проигрывание записи на экране.
- ✨ Проигрывать загруженную запись на экране записи — Послушать загруженное приложение не даёт, а самой копии для этого у задачи нет: указатель на файл перезаписывается на каждом шаге конвейера и у готовой задачи ведёт на объект в Object Storage.
- ✨ Сделать приложение устанавливаемым на телефон — Приложение, живущее вкладкой браузера, теряется среди прочих: ярлыка на экране у него нет.
- ✨ Узнавать уже загруженный файл по хеш-сумме — Один и тот же файл, отправленный дважды, распознаётся дважды и оплачивается дважды: приём не смотрит на содержимое вовсе.
- ✨ Принимать до десяти файлов одной загрузкой — Приём берёт один файл в запросе, а с телефона выбирают пачку сразу: десять записей значат десять заходов на экран загрузки.
- ✨ Показывать ход загрузки записи на экране — Гигабайтный файл уходит на сервер молча: до ответа сервера экран не отличает идущую загрузку от зависшей.
- 🔬 Загрузка большого файла частями — Гигабайтный файл едет одним запросом, и обрыв на девяноста процентах начинает его заново.
- 🔬 Сверка файла с записью при отдаче — Владение судится у записи, а файл открывается по её ссылке без сверки files.record_id и files.owner_id; схема этой связи не держит, и обе стороны будут править delete-record и long-audio-chunking.
- 🔬 Имя копии в каталоге записи без обращения к базе — Имена копий вида без базы не читаются: по каталогу на диске не сказать ни чья запись, ни какого она уровня, а раскладка объявлена необратимой, и цена решения растёт с каждой уложенной записью.
- ✨ Удалять запись со всеми уровнями текста по требованию владельца — Ни файлы, ни расшифровки не удаляются вовсе: убрать запись сегодня можно только руками в базе и в каталоге на сервере.
- ✨ Сделать экран настроек и хранить настройки по пользователю — Настроек у пользователя нет вовсе: уровни текста и канал уведомлений задаются общим конфигом сервиса.
- ✨ Считать заголовок, темы и пересказ внешней моделью — Расшифровка доходит стеной текста: ни заголовка, ни тем, ни пересказа сервис не считает, и клиента языковой модели в нём нет.
- ✨ Отдавать вычитанный текст рядом с сырым — Сырая расшифровка идёт без знаков препинания, с повторами и словами-паразитами: читать её подряд тяжело, а другого уровня текста нет.
- ✨ Показывать заголовок в списке, отбирать список по темам и считать токены — Заголовок, темы и пересказ считаются, но список по-прежнему показывает первые слова расшифровки и не отбирается ничем, а расход на модель не виден числом.
- ✨ Считать только те уровни текста, что включены у владельца записи — Дом настроек есть, а конвейер их не читает: выключенный уровень всё равно уходит платной модели, и настройка ничего не экономит.
- ✨ Отправлять готовый текст через apprise и ntfy — Пользователь веба узнаёт о готовности только опросом с открытого экрана.
- ✨ Слать готовый текст на почту из учётной записи — Адрес почты приходит вместе с входом через OIDC, но почтового отправителя в сервисе нет.
- 🔬 Потолки SpeechKit по длине записи и по формату — Потолок длины записи и перечень принимаемых форматов неизвестны, а работа над долгими записями без них не начинается.
- 🔬 Потолки приёма, конвертации и заливки по длине записи — Из пяти звеньев задача speechkit-limits замерила только модель распознавания: где отваливается шестичасовая запись до неё, неизвестно.
- ✨ Отклонять на приёме запись сверх потолка — Запись сверх потолка принимается молча и висит в конвейере до истечения часового захвата, а человек всё это время ждёт текста.
- ✨ Резать длинную запись на фрагменты и продолжать с места остановки — Шаг конвейера повторяется целиком: перезапуск на пятом часу шестичасовой записи начинает распознавание заново и оплачивает его второй раз.
- ✨ Отдавать текст в сотни килобайт файлом, а не сотней сообщений — Отправитель Telegram режет текст по 4000 знаков: расшифровка шестичасовой записи придёт сотней сообщений подряд.
- 🔬 Перечень форматов, которые конвейер принимает на самом деле — Команда ffmpeg проверена на голосовых Telegram, а что она берёт помимо них, не мерил никто: перечень выведен из документации, а не из прогона.
- ✨ Принимать видео и брать из него звуковую дорожку — Запись семейного архива приходит видеофайлом, а приём смотрит на аудио: человеку приходится доставать дорожку самому.
- 🔬 Стоит ли брать OpenTelemetry вместо голого Prometheus — Метрик одиннадцать штук на пять счётчиков, трассировки нет вовсе: путь одной записи по конвейеру собирается только чтением логов глазами.
- ✨ Собирать путь одной записи по конвейеру запросом — Звенья пути связаны только идентификатором задачи в строках журнала: чтобы понять, где запись провела минуты, владелец читает логи контейнера глазами.
- ✨ Считать вызовы, отказы и длительность по каждому внешнему сервису — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет ни одной метрики: отказ внешнего сервиса виден только строкой в журнале контейнера.
- ✨ Показывать метрикой задачу, застрявшую в состоянии — Вставший конвейер неотличим от простоя: возраст задачи в состоянии не считается, и очередь без движения выглядит как отсутствие работы.
- ✨ Оповещать владельца об отказе, не дожидаясь жалобы — Об отказе владелец узнаёт от пользователя: правил оповещения нет ни на одной метрике, а метрики читают глазами.
- 🔬 Уведомление SpeechKit о готовности вместо опроса — Шаг проверки дёргает операцию раз в 5 секунд всё время распознавания: часовая запись даёт порядка 720 обращений к платному сервису вместо одного ответа.
- ✨ Считать объём, минуты и расход по каждому пользователю — Ни объём, ни длительность, ни обращения к платным сервисам никуда не записываются: восстановить расход задним числом не из чего.
- ✨ Сделать страницу статистики для владельца — Собранный учёт читается только запросом к базе руками: ни страницы, ни признака владельца в приложении нет.
- 🐞 Не давать отправителю задавать, что уедет в журнал — Путь запроса уезжает в журнал целиком: аноним пишет туда до мегабайта своего текста одной строкой, а хвост имени файла приезжает внутри чужого текста ошибки вместо своего поля.
- 🐞 Писать в журнал код ответа отвергнутого запроса — У всякого запроса, отвергнутого роутером, в журнале стоит нулевой код: всплеск отказов доступа неотличим от обычного трафика.
- 🐞 Не затирать сохранённое вложение более бедным ответом — Пустой ответ распознавателя сохранённое больше не стирает, а более короткий — стирает: вложение заменяется, и пересчитать архив становится нечем.
- 🐞 Закрыть анониму адрес подтверждения почты — Адрес подтверждения почты хранилища открыт без сессии: зная адрес вошедшего, аноним вызывает у сервиса отправку письма — сегодня безвредно, потому что почтового отправителя нет.
- 🔬 Судьба рубежа расшифровки, у которого не осталось работы — Шаг завершения после убранной доставки только двигает колонку, а рубеж при этом подпадает под сторож застревания: готовая расшифровка платит отдельный захват и час сторожа.
- 🔬 Пределы длительности, приходящей из метаданных — Длительность приходит от ffprobe числом и не проверяется ничем: приведение к целому переполняется, а испорченная гистограмма чинится только перезапуском.
- 🧹 Проверять уязвимости в зависимостях приложения — Зависимости приложения пришли задачей spa-skeleton, а сканера у них нет: govulncheck смотрит только модули Go. Ни проверки, ни объявленного исключения — непроверенная поверхность, о которой никто не решал
- 🔬 Квота по общему размеру загруженного на пользователя — Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас.