- Тип сменён с chore: подъём без токена бота меняет наблюдаемое поведение и требует нормы, которой в openspec/specs нет. - В тело записан второй предмет работы — судьба задачи из Telegram, дошедшей до ответа при отсутствующем боте; он же стал четвёртым критерием приёмки.
24 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
Секции «блокеры» здесь нет и не заводится: блокер — это состояние (работа не может продолжаться ни одной задачей), оно живёт до ответа человека, а его следы — вопросами в файлах задач.
Очередь
- ✨ Поднимать сервис без токена бота, работая одним входом — Адаптер Telegram проверяет токен обращением к Telegram и роняет старт, а боевым токеном запускаться запрещено: проверить поведение живым прогоном не может ни одна задача.
- 🐞 Убрать код провайдера из журнала запросов хранилища — Строка запроса с кодом входа целиком уезжает в таблицу _logs и лежит там пять суток, хотя спека access требует, чтобы код в журнал не попадал.
- 🐞 Вести учёт употреблённых состояний входа на сервере — Одноразовость возврата держится на уборке куки, то есть на браузере: сервер не помнит, какие состояния уже потрачены.
- 🧹 Строить адрес входа из настроек коллекции, а не из конфига — Первая половина входа собрана руками из конфига и на настройки провайдера не смотрит, вторая берётся из коллекции: обновление библиотеки изменит только вторую половину.
- 🔬 Четыре недоказанные гипотезы о поверхности входа — Ревью назвало четыре пути, которых не смогло ни подтвердить, ни опровергнуть: браузера и живого провайдера в прогоне не было.
- 🐞 Починить срок сессии, который ставит откат шага входа — Константа defaultAuthTokenDuration в шаге 202608120001 названа умолчанием библиотеки, но 1209600 — это 14 суток, а умолчание PocketBase 432000, пять суток: откат объявляет возврат к умолчанию и ставит срок вдвое больше выбранных владельцем семи.
- 🔬 Адрес объекта в тексте отказа SpeechKit — Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись.
- 🧹 Разобрать мелочи http-транспорта — Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
- 🧹 Запретить обращаться к Bot API мимо клиента бота — Чистка отказа от адреса с токеном живёт в клиенте; свой http.Client в транспорте вернёт утечку молча — правило noctx такую подмену не ловит, а класс уже стоил одного дефекта.
- 🧹 Переименовать образец конфига в config.example.toml — Конвенция называет config.dist.toml объявленным расхождением, но тут же пишет это имя как правило — документ противоречит сам себе, а образец расходится с конвенцией.
- 🧹 Свести пять расхождений между документами канона — Сверка 2026-08-13 нашла шесть мест, где два документа отвечают на один вопрос по-разному; одно сведено при повышении раскладки, а три из пяти оставшихся стоят в architecture.md, и по ним читатель строит решения о выкладке и о периметре.
- ✨ Привязать запись к владельцу и отдавать только свои — У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать.
- ✨ Свести приём и чтение записей к одному контракту для приложения — Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем.
- ✨ Сопоставить пользователя Telegram с учётной записью — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
- ✨ Пускать скрипты в API по личным токенам — Вход через OIDC закрывает API целиком, а скрипту браузерная сессия недоступна: автоматизировать загрузку станет нечем.
- 🧹 Покрыть тестами шаги конвейера и захват задачи — Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем.
- 🧹 Покрыть тестами разбор вывода ffprobe — Проверки приёма перестали звать настоящий ffprobe 2026-08-11, а своего теста у адаптера метаданных нет: разбор JSON и отличие «программы нет в PATH» от «обработка отказала» не проверяет ничто.
- 🧹 Задать таймауты обращениям к внешним сервисам — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата.
- 🧹 Прервать шаг конвейера отменой контекста — Половина сделана 2026-08-13 — контекст доходит до внешних вызовов, а прерванный шаг оставляет задачу на повтор и не тратит попытку, — но осталось то, ради чего задача заводилась: хранилище контекста не принимает ни одним методом, и бюджет мягкой остановки не замерен.
- 🐞 Убирать записанный файл, когда приём отказал на середине — Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками.
- 🧹 Разобрать мелочи слоя хранилища — Три мелочи ниже потолка триажа: цикл воркера пишет потерю захвата уровнем ERROR и считает её отказом, тип ошибки заведён там, где конвенция просит sentinel, а FileName несёт два разных смысла.
- 🧹 Закрепить версию рантайм-базы образа — Финальный слой Dockerfile собирается на alpine:latest, а task image идёт с --pull, поэтому два образа из одного коммита с разницей в неделю несут разный ffmpeg — регрессия конвертации после такой пересборки выглядит как задачи в failed при пустом диффе репозитория, и откат на прежний коммит её не чинит.
- ✨ Собрать каркас приложения и раздать его из бинарника — Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует.
- ✨ Сделать экран загрузки записи и её состояния — Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит.
- ✨ Сделать экран списка своих записей и чтения текста — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем.
- ✨ Проигрывать загруженную запись на экране записи — Послушать загруженное приложение не даёт, а самой копии для этого у задачи нет: указатель на файл перезаписывается на каждом шаге конвейера и у готовой задачи ведёт на объект в Object Storage.
- ✨ Сделать приложение устанавливаемым на телефон — Приложение, живущее вкладкой браузера, теряется среди прочих: ярлыка на экране у него нет.
- ✨ Узнавать уже загруженный файл по хеш-сумме — Один и тот же файл, отправленный дважды, распознаётся дважды и оплачивается дважды: приём не смотрит на содержимое вовсе.
- ✨ Принимать до десяти файлов одной загрузкой — Приём берёт один файл в запросе, а с телефона выбирают пачку сразу: десять записей значат десять заходов на экран загрузки.
- ✨ Показывать ход загрузки записи на экране — Гигабайтный файл уходит на сервер молча: до ответа сервера экран не отличает идущую загрузку от зависшей.
- 🔬 Загрузка большого файла частями — Гигабайтный файл едет одним запросом, и обрыв на девяноста процентах начинает его заново.
- ✨ Удалять запись со всеми уровнями текста по требованию владельца — Ни файлы, ни расшифровки не удаляются вовсе: убрать запись сегодня можно только руками в базе и в каталоге на сервере.
- ✨ Сделать экран настроек и хранить настройки по пользователю — Настроек у пользователя нет вовсе: уровни текста и канал уведомлений задаются общим конфигом сервиса.
- ✨ Считать заголовок, темы и пересказ внешней моделью — Расшифровка доходит стеной текста: ни заголовка, ни тем, ни пересказа сервис не считает, и клиента языковой модели в нём нет.
- ✨ Отдавать вычитанный текст рядом с сырым — Сырая расшифровка идёт без знаков препинания, с повторами и словами-паразитами: читать её подряд тяжело, а другого уровня текста нет.
- ✨ Показывать заголовок в списке, отбирать список по темам и считать токены — Заголовок, темы и пересказ считаются, но список по-прежнему показывает первые слова расшифровки и не отбирается ничем, а расход на модель не виден числом.
- ✨ Считать только те уровни текста, что включены у владельца записи — Дом настроек есть, а конвейер их не читает: выключенный уровень всё равно уходит платной модели, и настройка ничего не экономит.
- ✨ Отправлять готовый текст через apprise и ntfy — Пользователь веба узнаёт о готовности только опросом с открытого экрана.
- ✨ Слать готовый текст на почту из учётной записи — Адрес почты приходит вместе с входом через OIDC, но почтового отправителя в сервисе нет.
- 🔬 Потолки SpeechKit по длине записи и по формату — Потолок длины записи и перечень принимаемых форматов неизвестны, а работа над долгими записями без них не начинается.
- 🔬 Потолки приёма, конвертации и заливки по длине записи — Из пяти звеньев задача speechkit-limits замерила только модель распознавания: где отваливается шестичасовая запись до неё, неизвестно.
- ✨ Отклонять на приёме запись сверх потолка — Запись сверх потолка принимается молча и висит в конвейере до истечения часового захвата, а человек всё это время ждёт текста.
- ✨ Резать длинную запись на фрагменты и продолжать с места остановки — Шаг конвейера повторяется целиком: перезапуск на пятом часу шестичасовой записи начинает распознавание заново и оплачивает его второй раз.
- ✨ Отдавать текст в сотни килобайт файлом, а не сотней сообщений — Отправитель Telegram режет текст по 4000 знаков: расшифровка шестичасовой записи придёт сотней сообщений подряд.
- 🔬 Перечень форматов, которые конвейер принимает на самом деле — Команда ffmpeg проверена на голосовых Telegram, а что она берёт помимо них, не мерил никто: перечень выведен из документации, а не из прогона.
- ✨ Принимать видео и брать из него звуковую дорожку — Запись семейного архива приходит видеофайлом, а приём смотрит на аудио: человеку приходится доставать дорожку самому.
- 🔬 Стоит ли брать OpenTelemetry вместо голого Prometheus — Метрик одиннадцать штук на пять счётчиков, трассировки нет вовсе: путь одной записи по конвейеру собирается только чтением логов глазами.
- ✨ Собирать путь одной записи по конвейеру запросом — Звенья пути связаны только идентификатором задачи в строках журнала: чтобы понять, где запись провела минуты, владелец читает логи контейнера глазами.
- ✨ Считать вызовы, отказы и длительность по каждому внешнему сервису — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет ни одной метрики: отказ внешнего сервиса виден только строкой в журнале контейнера.
- ✨ Показывать метрикой задачу, застрявшую в состоянии — Вставший конвейер неотличим от простоя: возраст задачи в состоянии не считается, и очередь без движения выглядит как отсутствие работы.
- ✨ Оповещать владельца об отказе, не дожидаясь жалобы — Об отказе владелец узнаёт от пользователя: правил оповещения нет ни на одной метрике, а метрики читают глазами.
- 🔬 Уведомление SpeechKit о готовности вместо опроса — Шаг проверки дёргает операцию раз в 5 секунд всё время распознавания: часовая запись даёт порядка 720 обращений к платному сервису вместо одного ответа.
- ✨ Считать объём, минуты и расход по каждому пользователю — Ни объём, ни длительность, ни обращения к платным сервисам никуда не записываются: восстановить расход задним числом не из чего.
- ✨ Сделать страницу статистики для владельца — Собранный учёт читается только запросом к базе руками: ни страницы, ни признака владельца в приложении нет.
- 🔬 Квота по общему размеру загруженного на пользователя — Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас.