From 8739b18a9f1ce80fdfe863aecf1a1242516e29e5 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Wed, 12 Aug 2026 20:48:53 +0300 Subject: [PATCH] =?UTF-8?q?tasks:=20=D0=BE=D1=87=D0=B5=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D1=8C=20=D1=80=D0=B0=D1=81=D1=81=D1=82=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BE=D1=82=20=D0=B1=D0=B0=D0=B7=D1=8B=20=D0=BA?= =?UTF-8?q?=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D1=8F=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - порядок беклога и роадмапа назначен слоями: проверки, которым можно верить → долги входа → владелец записи и контракт API → конвейер под тестами → приложение и возможности поверх; у каждого движения записана причина; - заведены восемь задач под пункты «Завершения», которых не закрывала ни одна запись, — цель any-audio-source была без задач вовсе; - у четырёх задач сняты критерии, требовавшие того, что делает задача ниже по очереди; исправлены ссылки на несуществующий repo/sqlite и на отменённую разведку об очереди. --- tasks/BACKLOG.md | 81 ++++++++++++------- tasks/ROADMAP.md | 6 +- tasks/items/admin-stats-screen.md | 2 +- tasks/items/any-audio-source.md | 3 +- tasks/items/api-tokens.md | 7 +- tasks/items/audio-format-coverage-measure.md | 32 ++++++++ tasks/items/chunked-upload-choice.md | 2 +- tasks/items/config-example-toml.md | 2 +- tasks/items/context-cancel-in-pipeline.md | 2 +- tasks/items/data-ownership.md | 2 +- tasks/items/dedup-by-content-hash.md | 6 +- tasks/items/delete-record.md | 2 +- tasks/items/email-notification.md | 2 +- tasks/items/external-call-timeouts.md | 10 +-- tasks/items/external-service-metrics.md | 41 ++++++++++ .../fix-migrations-path-in-docs-config.md | 2 +- tasks/items/gate-changed-lines-coverage.md | 27 ++++--- .../items/gate-dependency-vulnerabilities.md | 2 +- tasks/items/gate-extra-linters.md | 2 +- tasks/items/gate-step-exit-codes.md | 2 +- tasks/items/http-transport-nits.md | 2 +- tasks/items/insights-visible-in-list.md | 40 +++++++++ tasks/items/installable-pwa.md | 2 +- tasks/items/intake-limits-measure.md | 2 +- tasks/items/job-path-by-request.md | 41 ++++++++++ tasks/items/json-api-for-spa.md | 21 +++-- tasks/items/literary-text-level.md | 7 +- tasks/items/llm-insights-adapter.md | 10 +-- .../items/local-run-without-telegram-token.md | 2 +- tasks/items/login-surface-hypotheses.md | 2 +- .../login-url-from-collection-settings.md | 2 +- tasks/items/long-audio-chunking.md | 2 +- tasks/items/long-recordings.md | 2 +- tasks/items/long-text-delivery.md | 2 +- tasks/items/metaviewer-adapter-tests.md | 2 +- tasks/items/multi-file-upload.md | 2 +- tasks/items/multi-user.md | 2 +- tasks/items/ntfy-delivery.md | 2 +- tasks/items/opentelemetry-fit.md | 2 +- tasks/items/orphan-file-on-failed-intake.md | 2 +- tasks/items/owner-alerting.md | 38 +++++++++ tasks/items/per-user-size-quota.md | 2 +- tasks/items/pin-runtime-image-base.md | 2 +- tasks/items/pipeline-step-tests.md | 9 +-- tasks/items/play-recording-in-app.md | 2 +- .../items/provider-code-out-of-storage-log.md | 2 +- tasks/items/ready-notification.md | 2 +- tasks/items/record-ownership.md | 14 ++-- tasks/items/records-list-screen.md | 2 +- tasks/items/reject-oversized-recording.md | 2 +- .../items/response-assertions-judge-result.md | 2 +- tasks/items/review-config-from-go-upgrade.md | 2 +- .../rollback-does-not-undo-schema-step.md | 2 +- tasks/items/server-side-login-state.md | 2 +- tasks/items/service-observability.md | 2 +- tasks/items/settings-applied-in-pipeline.md | 41 ++++++++++ tasks/items/settings-screen.md | 5 +- tasks/items/spa-skeleton.md | 2 +- tasks/items/speechkit-callback-fit.md | 2 +- tasks/items/speechkit-error-text-leak.md | 2 +- tasks/items/speechkit-limits.md | 2 +- tasks/items/stalled-pipeline-metric.md | 37 +++++++++ tasks/items/storage-layer-nits.md | 2 +- tasks/items/telegram-account-link.md | 2 +- tasks/items/text-insights.md | 2 +- tasks/items/upload-and-status-screen.md | 9 ++- tasks/items/upload-progress.md | 2 +- tasks/items/upload-reliability.md | 2 +- tasks/items/usage-accounting.md | 2 +- tasks/items/usage-stats.md | 2 +- tasks/items/user-settings.md | 2 +- tasks/items/video-audio-track-intake.md | 42 ++++++++++ tasks/items/web-access.md | 2 +- 73 files changed, 490 insertions(+), 139 deletions(-) create mode 100644 tasks/items/audio-format-coverage-measure.md create mode 100644 tasks/items/external-service-metrics.md create mode 100644 tasks/items/insights-visible-in-list.md create mode 100644 tasks/items/job-path-by-request.md create mode 100644 tasks/items/owner-alerting.md create mode 100644 tasks/items/settings-applied-in-pipeline.md create mode 100644 tasks/items/stalled-pipeline-metric.md create mode 100644 tasks/items/video-audio-track-intake.md diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index ec938c0..cdadda2 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -11,6 +11,25 @@ Секция одна — полок домена у проекта нет, и делить очередь на две значило бы держать два порядка вместо одного. +**Чем очередь упорядочена на этом этапе — от базы к деталям.** +Сначала то, на чём стоит остальное: проверки, которым можно верить, +владелец записи, единый контракт API, покрытый тестами конвейер, — и +только потом экраны и возможности поверх них. Порядок расставлен на +груминге 2026-08-12 и держится, пока сервис не собран целиком: +задача, взятая раньше своего основания, стоит дважды — сперва её +пишут, потом переписывают под появившееся основание. + +Отсюда правило для **новых** записей. Заведённая по ходу работы — +интейком, урожаем ревью, разбором находок — задача встаёт в конец +очереди машинально, и это **не** её место, а отсутствие места. +Слой ей назначает человек на ближайшем груминге: ниже того, чего она +требует, и выше того, что требует её. Причина уезжает в запись +(`move --after <слаг> --reason`). + +Разведки это касается вдвойне: её исход — новые задачи, и слой они +наследуют не от разведки, а от того, что трогают. Разведка о конвейере +может принести задачу основания, которой место в голове очереди. + Тип записи стоит первым полем меты и решает, что у неё может быть: ✨ `feature` 🐞 `fix` 🧹 `chore` 🔬 `research` @@ -20,55 +39,63 @@ ## Очередь -- [🐞 Убирать записанный файл, когда приём отказал на середине](items/orphan-file-on-failed-intake.md) — Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками. -- [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата. +- [🐞 Починить путь миграций в настройке сверки документов](items/fix-migrations-path-in-docs-config.md) — Ключ migrations указывает на каталог migrations/, которого в репозитории нет: шаг гейта зелен при изменённой миграции и нетронутом database.md, а конвенции числят этот род механизированным. +- [🧹 Привести коды выхода шагов гейта к общему словарю](items/gate-step-exit-codes.md) — CLAUDE.md объявляет словарь кодов общим для четырёх проверочных шагов и описывает код 3 как «каталог или файл не найден», но все четыре обёртки в Taskfile.yml при отсутствующем скрипте отдают 1 («дрейф»), из-за чего читатель кода идёт искать разъехавшееся там, где просто неполно дерево. +- [🧹 Судить ответ в тестах по готовому ответу](items/response-assertions-judge-result.md) — Проверка, читающая изменяемую карту заголовков обработчика, зелена при неработающем коде: класс всплыл трижды, последний раз на уборке куки входа. +- [🧹 Ронять гейт на изменённой функции, которую не выполняет ни один тест](items/gate-changed-lines-coverage.md) — Свойство «изменённое место покрыто хоть одним тестом» записано в docs/review.md, но не механизировано: за две задачи подряд непокрытые шаги ловили руками. +- [🧹 Ловить уязвимости в зависимостях шагом гейта](items/gate-dependency-vulnerabilities.md) — govulncheck находит две достижимые уязвимости в клиентах Yandex, а ни гейт, ни список «чего в гейте нет» о нём не знают: узнать о третьей будет неоткуда. +- [🧹 Поднимать сервис локально без действующего токена бота](items/local-run-without-telegram-token.md) — Адаптер Telegram проверяет токен обращением к Telegram и роняет старт, а боевым токеном запускаться запрещено: проверить поведение живым прогоном не может ни одна задача. +- [🐞 Убрать код провайдера из журнала запросов хранилища](items/provider-code-out-of-storage-log.md) — Строка запроса с кодом входа целиком уезжает в таблицу _logs и лежит там пять суток, хотя спека access требует, чтобы код в журнал не попадал. +- [🐞 Вести учёт употреблённых состояний входа на сервере](items/server-side-login-state.md) — Одноразовость возврата держится на уборке куки, то есть на браузере: сервер не помнит, какие состояния уже потрачены. +- [🧹 Строить адрес входа из настроек коллекции, а не из конфига](items/login-url-from-collection-settings.md) — Первая половина входа собрана руками из конфига и на настройки провайдера не смотрит, вторая берётся из коллекции: обновление библиотеки изменит только вторую половину. +- [🔬 Четыре недоказанные гипотезы о поверхности входа](items/login-surface-hypotheses.md) — Ревью назвало четыре пути, которых не смогло ни подтвердить, ни опровергнуть: браузера и живого провайдера в прогоне не было. +- [🧹 Назвать в необратимом, что откат кода не откатывает шаг схемы](items/rollback-does-not-undo-schema-step.md) — Откат бинаря оставляет применённый шаг схемы в силе, и на этом строятся решения о выкладке: сегодня об этом не сказано нигде. +- [🔬 Адрес объекта в тексте отказа SpeechKit](items/speechkit-error-text-leak.md) — Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись. +- [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом. +- [🧹 Переименовать образец конфига в config.example.toml](items/config-example-toml.md) — Конвенция называет config.dist.toml объявленным расхождением, но тут же пишет это имя как правило — документ противоречит сам себе, а образец расходится с конвенцией. - [✨ Привязать запись к владельцу и отдавать только свои](items/record-ownership.md) — У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать. -- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны. - [✨ Свести приём и чтение записей к одному контракту для приложения](items/json-api-for-spa.md) — Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем. +- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны. - [✨ Пускать скрипты в API по личным токенам](items/api-tokens.md) — Вход через OIDC закрывает API целиком, а скрипту браузерная сессия недоступна: автоматизировать загрузку станет нечем. +- [🧹 Покрыть тестами шаги конвейера и захват задачи](items/pipeline-step-tests.md) — Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем. +- [🧹 Покрыть тестами разбор вывода ffprobe](items/metaviewer-adapter-tests.md) — Проверки приёма перестали звать настоящий ffprobe 2026-08-11, а своего теста у адаптера метаданных нет: разбор JSON и отличие «программы нет в PATH» от «обработка отказала» не проверяет ничто. +- [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата. +- [🧹 Прервать шаг конвейера отменой контекста](items/context-cancel-in-pipeline.md) — Воркер читает ctx только между итерациями: остановка контейнера ждёт конца шага, а на занятом писателе один запрос к хранилищу держится до 9,5 секунды при мягком таймауте в 5. +- [🐞 Убирать записанный файл, когда приём отказал на середине](items/orphan-file-on-failed-intake.md) — Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками. +- [🧹 Разобрать мелочи слоя хранилища](items/storage-layer-nits.md) — Три мелочи ниже потолка триажа: цикл воркера пишет потерю захвата уровнем ERROR и считает её отказом, тип ошибки заведён там, где конвенция просит sentinel, а FileName несёт два разных смысла. - [✨ Собрать каркас приложения и раздать его из бинарника](items/spa-skeleton.md) — Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует. - [✨ Сделать экран загрузки записи и её состояния](items/upload-and-status-screen.md) — Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит. - [✨ Сделать экран списка своих записей и чтения текста](items/records-list-screen.md) — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем. -- [✨ Удалять запись со всеми уровнями текста по требованию владельца](items/delete-record.md) — Ни файлы, ни расшифровки не удаляются вовсе: убрать запись сегодня можно только руками в базе и в каталоге на сервере. +- [✨ Проигрывать загруженную запись на экране записи](items/play-recording-in-app.md) — Послушать загруженное приложение не даёт, а самой копии для этого у задачи нет: указатель на файл перезаписывается на каждом шаге конвейера и у готовой задачи ведёт на объект в Object Storage. +- [✨ Сделать приложение устанавливаемым на телефон](items/installable-pwa.md) — Приложение, живущее вкладкой браузера, теряется среди прочих: ярлыка на экране у него нет. - [✨ Узнавать уже загруженный файл по хеш-сумме](items/dedup-by-content-hash.md) — Один и тот же файл, отправленный дважды, распознаётся дважды и оплачивается дважды: приём не смотрит на содержимое вовсе. - [✨ Принимать до десяти файлов одной загрузкой](items/multi-file-upload.md) — Приём берёт один файл в запросе, а с телефона выбирают пачку сразу: десять записей значат десять заходов на экран загрузки. - [✨ Показывать ход загрузки записи на экране](items/upload-progress.md) — Гигабайтный файл уходит на сервер молча: до ответа сервера экран не отличает идущую загрузку от зависшей. -- [✨ Сделать приложение устанавливаемым на телефон](items/installable-pwa.md) — Приложение, живущее вкладкой браузера, теряется среди прочих: ярлыка на экране у него нет. +- [🔬 Загрузка большого файла частями](items/chunked-upload-choice.md) — Гигабайтный файл едет одним запросом, и обрыв на девяноста процентах начинает его заново. +- [✨ Удалять запись со всеми уровнями текста по требованию владельца](items/delete-record.md) — Ни файлы, ни расшифровки не удаляются вовсе: убрать запись сегодня можно только руками в базе и в каталоге на сервере. - [✨ Сделать экран настроек и хранить настройки по пользователю](items/settings-screen.md) — Настроек у пользователя нет вовсе: уровни текста и канал уведомлений задаются общим конфигом сервиса. - [✨ Считать заголовок, темы и пересказ внешней моделью](items/llm-insights-adapter.md) — Расшифровка доходит стеной текста: ни заголовка, ни тем, ни пересказа сервис не считает, и клиента языковой модели в нём нет. - [✨ Отдавать вычитанный текст рядом с сырым](items/literary-text-level.md) — Сырая расшифровка идёт без знаков препинания, с повторами и словами-паразитами: читать её подряд тяжело, а другого уровня текста нет. +- [✨ Показывать заголовок в списке, отбирать список по темам и считать токены](items/insights-visible-in-list.md) — Заголовок, темы и пересказ считаются, но список по-прежнему показывает первые слова расшифровки и не отбирается ничем, а расход на модель не виден числом. +- [✨ Считать только те уровни текста, что включены у владельца записи](items/settings-applied-in-pipeline.md) — Дом настроек есть, а конвейер их не читает: выключенный уровень всё равно уходит платной модели, и настройка ничего не экономит. - [✨ Отправлять готовый текст через apprise и ntfy](items/ntfy-delivery.md) — Пользователь веба узнаёт о готовности только опросом с открытого экрана. - [✨ Слать готовый текст на почту из учётной записи](items/email-notification.md) — Адрес почты приходит вместе с входом через OIDC, но почтового отправителя в сервисе нет. -- [✨ Считать объём, минуты и расход по каждому пользователю](items/usage-accounting.md) — Ни объём, ни длительность, ни обращения к платным сервисам никуда не записываются: восстановить расход задним числом не из чего. -- [✨ Сделать страницу статистики для владельца](items/admin-stats-screen.md) — Собранный учёт читается только запросом к базе руками: ни страницы, ни признака владельца в приложении нет. - [🔬 Потолки SpeechKit по длине записи и по формату](items/speechkit-limits.md) — Потолок длины записи и перечень принимаемых форматов неизвестны, а цель про долгие записи без них не начинается. - [🔬 Потолки приёма, конвертации и заливки по длине записи](items/intake-limits-measure.md) — Из пяти звеньев задача speechkit-limits замерила только модель распознавания: где отваливается шестичасовая запись до неё, неизвестно. - [✨ Отклонять на приёме запись сверх потолка](items/reject-oversized-recording.md) — Запись сверх потолка принимается молча и висит в конвейере до истечения часового захвата, а человек всё это время ждёт текста. - [✨ Резать длинную запись на фрагменты и продолжать с места остановки](items/long-audio-chunking.md) — Шаг конвейера повторяется целиком: перезапуск на пятом часу шестичасовой записи начинает распознавание заново и оплачивает его второй раз. - [✨ Отдавать текст в сотни килобайт файлом, а не сотней сообщений](items/long-text-delivery.md) — Отправитель Telegram режет текст по 4000 знаков: расшифровка шестичасовой записи придёт сотней сообщений подряд. -- [🔬 Загрузка большого файла частями](items/chunked-upload-choice.md) — Гигабайтный файл едет одним запросом, и обрыв на девяноста процентах начинает его заново. -- [🧹 Покрыть тестами разбор вывода ffprobe](items/metaviewer-adapter-tests.md) — Проверки приёма перестали звать настоящий ffprobe 2026-08-11, а своего теста у адаптера метаданных нет: разбор JSON и отличие «программы нет в PATH» от «обработка отказала» не проверяет ничто. -- [🧹 Покрыть тестами шаги конвейера и захват задачи](items/pipeline-step-tests.md) — Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем. -- [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом. -- [🧹 Переименовать образец конфига в config.example.toml](items/config-example-toml.md) — Конвенция называет config.dist.toml объявленным расхождением, но тут же пишет это имя как правило — документ противоречит сам себе, а образец расходится с конвенцией. +- [🔬 Перечень форматов, которые конвейер принимает на самом деле](items/audio-format-coverage-measure.md) — Команда ffmpeg проверена на голосовых Telegram, а что она берёт помимо них, не мерил никто: перечень выведен из документации, а не из прогона. +- [✨ Принимать видео и брать из него звуковую дорожку](items/video-audio-track-intake.md) — Запись семейного архива приходит видеофайлом, а приём смотрит на аудио: человеку приходится доставать дорожку самому. - [🔬 Стоит ли брать OpenTelemetry вместо голого Prometheus](items/opentelemetry-fit.md) — Метрик одиннадцать штук на пять счётчиков, трассировки нет вовсе: путь одной записи по конвейеру собирается только чтением логов глазами. -- [🧹 Ловить уязвимости в зависимостях шагом гейта](items/gate-dependency-vulnerabilities.md) — govulncheck находит две достижимые уязвимости в клиентах Yandex, а ни гейт, ни список «чего в гейте нет» о нём не знают: узнать о третьей будет неоткуда. -- [🧹 Считать покрытие изменённых строк шагом гейта](items/gate-changed-lines-coverage.md) — Свойство «изменённое место покрыто хоть одним тестом» записано в docs/review.md, но не механизировано: за две задачи подряд непокрытые шаги ловили руками. -- [🧹 Прервать шаг конвейера отменой контекста](items/context-cancel-in-pipeline.md) — Воркер читает ctx только между итерациями: остановка контейнера ждёт конца шага, а на занятом писателе один запрос к хранилищу держится до 9,5 секунды при мягком таймауте в 5. -- [🧹 Разобрать мелочи слоя хранилища](items/storage-layer-nits.md) — Три мелочи ниже потолка триажа: цикл воркера пишет потерю захвата уровнем ERROR и считает её отказом, тип ошибки заведён там, где конвенция просит sentinel, а FileName несёт два разных смысла. +- [✨ Собирать путь одной записи по конвейеру запросом](items/job-path-by-request.md) — Звенья пути связаны только идентификатором задачи в строках журнала: чтобы понять, где запись провела минуты, владелец читает логи контейнера глазами. +- [✨ Считать вызовы, отказы и длительность по каждому внешнему сервису](items/external-service-metrics.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет ни одной метрики: отказ внешнего сервиса виден только строкой в журнале контейнера. +- [✨ Показывать метрикой задачу, застрявшую в состоянии](items/stalled-pipeline-metric.md) — Вставший конвейер неотличим от простоя: возраст задачи в состоянии не считается, и очередь без движения выглядит как отсутствие работы. +- [✨ Оповещать владельца об отказе, не дожидаясь жалобы](items/owner-alerting.md) — Об отказе владелец узнаёт от пользователя: правил оповещения нет ни на одной метрике, а метрики читают глазами. - [🔬 Уведомление SpeechKit о готовности вместо опроса](items/speechkit-callback-fit.md) — Шаг проверки дёргает операцию раз в 5 секунд всё время распознавания: часовая запись даёт порядка 720 обращений к платному сервису вместо одного ответа. -- [🔬 Адрес объекта в тексте отказа SpeechKit](items/speechkit-error-text-leak.md) — Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись. -- [✨ Проигрывать загруженную запись на экране записи](items/play-recording-in-app.md) — Послушать загруженное приложение не даёт, а самой копии для этого у задачи нет: указатель на файл перезаписывается на каждом шаге конвейера и у готовой задачи ведёт на объект в Object Storage. +- [✨ Считать объём, минуты и расход по каждому пользователю](items/usage-accounting.md) — Ни объём, ни длительность, ни обращения к платным сервисам никуда не записываются: восстановить расход задним числом не из чего. +- [✨ Сделать страницу статистики для владельца](items/admin-stats-screen.md) — Собранный учёт читается только запросом к базе руками: ни страницы, ни признака владельца в приложении нет. - [🧹 Завести недостающие проверки: shellcheck, hadolint и тест скрипта сверки версий](items/gate-extra-linters.md) — Скрипты оболочки и Dockerfile машина не проверяет: shellcheck и hadolint не заведены, lefthook смотрит только gitleaks, а единственный исполняемый скрипт проекта на shell — scripts/check-go-version.sh — разбирали на ревью глазами построчно, потому что машине его показать нечем. - [🧹 Закрепить версию рантайм-базы образа](items/pin-runtime-image-base.md) — Финальный слой Dockerfile собирается на alpine:latest, а task image идёт с --pull, поэтому два образа из одного коммита с разницей в неделю несут разный ffmpeg — регрессия конвертации после такой пересборки выглядит как задачи в failed при пустом диффе репозитория, и откат на прежний коммит её не чинит. -- [🧹 Привести коды выхода шагов гейта к общему словарю](items/gate-step-exit-codes.md) — CLAUDE.md объявляет словарь кодов общим для четырёх проверочных шагов и описывает код 3 как «каталог или файл не найден», но все четыре обёртки в Taskfile.yml при отсутствующем скрипте отдают 1 («дрейф»), из-за чего читатель кода идёт искать разъехавшееся там, где просто неполно дерево. - [🧹 Настроить конвейер ревью по итогам прогона go-1-26-upgrade](items/review-config-from-go-upgrade.md) — Прогон вскрыл две прорехи настройки: «Типовые узлы» знают только рантайм и не знают рода «проверочный шаг набора проверок», а «Триггеры метки» не видят оси «изменение трогает канон» — и именно она дала обе блокирующие находки. -- [🐞 Починить путь миграций в настройке сверки документов](items/fix-migrations-path-in-docs-config.md) — Ключ migrations указывает на каталог migrations/, которого в репозитории нет: шаг гейта зелен при изменённой миграции и нетронутом database.md, а конвенции числят этот род механизированным. -- [🐞 Убрать код провайдера из журнала запросов хранилища](items/provider-code-out-of-storage-log.md) — Строка запроса с кодом входа целиком уезжает в таблицу _logs и лежит там пять суток, хотя спека access требует, чтобы код в журнал не попадал. -- [🧹 Поднимать сервис локально без действующего токена бота](items/local-run-without-telegram-token.md) — Адаптер Telegram проверяет токен обращением к Telegram и роняет старт, а боевым токеном запускаться запрещено: проверить поведение живым прогоном не может ни одна задача. -- [🧹 Строить адрес входа из настроек коллекции, а не из конфига](items/login-url-from-collection-settings.md) — Первая половина входа собрана руками из конфига и на настройки провайдера не смотрит, вторая берётся из коллекции: обновление библиотеки изменит только вторую половину. -- [🧹 Судить ответ в тестах по готовому ответу](items/response-assertions-judge-result.md) — Проверка, читающая изменяемую карту заголовков обработчика, зелена при неработающем коде: класс всплыл трижды, последний раз на уборке куки входа. -- [🧹 Назвать в необратимом, что откат кода не откатывает шаг схемы](items/rollback-does-not-undo-schema-step.md) — Откат бинаря оставляет применённый шаг схемы в силе, и на этом строятся решения о выкладке: сегодня об этом не сказано нигде. -- [🐞 Вести учёт употреблённых состояний входа на сервере](items/server-side-login-state.md) — Одноразовость возврата держится на уборке куки, то есть на браузере: сервер не помнит, какие состояния уже потрачены. -- [🔬 Четыре недоказанные гипотезы о поверхности входа](items/login-surface-hypotheses.md) — Ревью назвало четыре пути, которых не смогло ни подтвердить, ни опровергнуть: браузера и живого провайдера в прогоне не было. - [🔬 Квота по общему размеру загруженного на пользователя](items/per-user-size-quota.md) — Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас. diff --git a/tasks/ROADMAP.md b/tasks/ROADMAP.md index d5d79c5..b231d97 100644 --- a/tasks/ROADMAP.md +++ b/tasks/ROADMAP.md @@ -25,15 +25,15 @@ - [🎯 Сервисом пользуются несколько человек, и записи одного не видны другому](items/multi-user.md) — У задачи нет владельца, а HTTP API открыт наружу без аутентификации: пригласить второго человека сейчас значит открыть ему чужие расшифровки. - [🎯 Записи загружаются и читаются в приложении, которое ставится на телефон](items/web-access.md) — Сегодня записи принимает только бот и голый HTTP API без интерфейса: отдать сервис человеку, у которого нет Telegram, нечем. - [🎯 Загрузка большого файла доходит до сервиса и не повторяется впустую](items/upload-reliability.md) — Приём рассчитан на голосовое в пару мегабайт: обрыв на середине гигабайтного файла начинает загрузку заново, а один и тот же файл распознаётся повторно за наши деньги. +- [🎯 Человек убирает свою запись из архива вместе со всеми текстами](items/data-ownership.md) — Хранение бессрочное, а способа убрать запись нет ни одного: ошибочно загруженный файл и разговор, который человек не хочет держать у нас, остаются навсегда. - [🎯 Пользователь настраивает, что сервис делает с его записями](items/user-settings.md) — Уровни текста считает платная модель, а уведомления приходят одним общим способом: отказаться от лишнего и выбрать свой канал пользователю нечем. +- [🎯 Приложение показывает, о чём запись, не читая её целиком](items/text-insights.md) — Расшифровка часового разговора — это стена текста: найти в списке нужную запись и вспомнить, о чём она, сегодня нечем. - [🎯 Пользователь узнаёт о готовности текста, не держа приложение открытым](items/ready-notification.md) — Расшифровка занимает минуты, и всё это время человек либо смотрит на экран с опросом статуса, либо забывает вернуться. ## Направления -- [🎯 Принимается запись любого формата, включая дорожку из видео](items/any-audio-source.md) — Конвертер вызывается одной командой ffmpeg, проверенной на голосовых Telegram; что он берёт помимо них, никто не мерил. - [🎯 Запись длиной до шести часов доходит до текста](items/long-recordings.md) — Потолок не замерен ни на одном звене: Telegram не отдаёт больше 20 МиБ, границы модели deferred-general неизвестны, а перезапуск на середине начинает распознавание заново. -- [🎯 Приложение показывает, о чём запись, не читая её целиком](items/text-insights.md) — Расшифровка часового разговора — это стена текста: найти в списке нужную запись и вспомнить, о чём она, сегодня нечем. -- [🎯 Человек убирает свою запись из архива вместе со всеми текстами](items/data-ownership.md) — Хранение бессрочное, а способа убрать запись нет ни одного: ошибочно загруженный файл и разговор, который человек не хочет держать у нас, остаются навсегда. +- [🎯 Принимается запись любого формата, включая дорожку из видео](items/any-audio-source.md) — Конвертер вызывается одной командой ffmpeg, проверенной на голосовых Telegram; что он берёт помимо них, никто не мерил. ## Сопровождение diff --git a/tasks/items/admin-stats-screen.md b/tasks/items/admin-stats-screen.md index 4c65851..5b1a00d 100644 --- a/tasks/items/admin-stats-screen.md +++ b/tasks/items/admin-stats-screen.md @@ -1,7 +1,7 @@ # ✨ Сделать страницу статистики для владельца - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Страница показывает собранный учёт: без учёта показывать нечего. - **Зачем:** Собранный учёт читается только запросом к базе руками: ни страницы, ни признака владельца в приложении нет. - **Теги:** goal:usage-stats diff --git a/tasks/items/any-audio-source.md b/tasks/items/any-audio-source.md index 67fb866..1dbedd9 100644 --- a/tasks/items/any-audio-source.md +++ b/tasks/items/any-audio-source.md @@ -1,8 +1,9 @@ # 🎯 Принимается запись любого формата, включая дорожку из видео - **Тип:** goal -- **Секция:** Направления +- **Секция:** Направления — Перечень форматов не замерен, и потолок длины у видео тот же, что у долгих записей: тянется следом за ними. - **Зачем:** Конвертер вызывается одной командой ffmpeg, проверенной на голосовых Telegram; что он берёт помимо них, никто не мерил. +- **Теги:** decomposed Человек отдаёт файл, не думая о том, что внутри: аудио любого распространённого контейнера или видео, из которого нужна только речь. Подготовка на стороне diff --git a/tasks/items/api-tokens.md b/tasks/items/api-tokens.md index 65ae5a9..9aaad92 100644 --- a/tasks/items/api-tokens.md +++ b/tasks/items/api-tokens.md @@ -1,7 +1,7 @@ # ✨ Пускать скрипты в API по личным токенам - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Второй способ представиться ставится на готовые владельца и контракт, иначе форма ошибки переписывается дважды. - **Зачем:** Вход через OIDC закрывает API целиком, а скрипту браузерная сессия недоступна: автоматизировать загрузку станет нечем. - **Теги:** goal:multi-user @@ -18,7 +18,6 @@ - таблица токенов: владелец, имя, отпечаток, время выпуска и последнего обращения, и её миграция; - эндпоинты выпуска, перечня и отзыва токена; -- экран настроек — место, где токен выпускают и отзывают; - `docs/security.md` — второй способ представиться и хранение отпечатка; - `README.md` — пример вызова API скриптом. @@ -38,4 +37,6 @@ Учётные записи по-прежнему заводит Authelia — свою регистрацию не делаем. Сроков жизни и областей действия у токена не заводим: он даёт права владельца -целиком. Берётся после `oidc-login`: до неё представляться некому. +целиком. Берётся после `oidc-login`: до неё представляться некому. Экрана выпуска здесь +нет — приложения ещё не существует, токен выпускается запросом к API; место +токена на экране настроек заводит `settings-screen`. diff --git a/tasks/items/audio-format-coverage-measure.md b/tasks/items/audio-format-coverage-measure.md new file mode 100644 index 0000000..b62434f --- /dev/null +++ b/tasks/items/audio-format-coverage-measure.md @@ -0,0 +1,32 @@ +# 🔬 Перечень форматов, которые конвейер принимает на самом деле + +- **Тип:** research +- **Категория:** Очередь — Форматы: сначала замер того, что конвейер берёт на самом деле. +- **Зачем:** Команда ffmpeg проверена на голосовых Telegram, а что она берёт помимо них, не мерил никто: перечень выведен из документации, а не из прогона. +- **Теги:** goal:any-audio-source + +Двигает пункты 1 и 4 «Завершения» цели: перечень принимаемых форматов замерен и +записан, а расхождение `ogg/vorbis` против заявленного SpeechKit `OGG_OPUS` +разобрано. + +## Вопрос + +Какие форматы доходят до текста целиком, какие ломаются на конвертации, какие — +на распознавании, и чем на самом деле кодирует конвертер: `vorbis` или `opus`. + +## Куда ляжет ответ + +- `docs/research/audio-formats.md` — таблица «формат на входе → исход», с + командой замера и версией ffmpeg, на которой он сделан; +- расхождение `ogg/vorbis` против `OGG_OPUS`: строка о том, устранено оно или + проверенно безвредно, и чем это подтверждено; +- форматы, которые принять нельзя, — задачей об отказе на приёме, с провенансом + этой разведки. + +## Рамки + +Замер идёт на своих файлах во временном каталоге и на подставном распознавателе +`internal/adapter/recognizer/memory.go`; прогон на реальных ключах Yandex +запрещён — там, где без настоящего SpeechKit не обойтись, ответ берётся из +задачи `speechkit-limits`, а не оплачивается заново. Видеофайлы здесь только +измеряются, приём их заводит `video-audio-track-intake`. diff --git a/tasks/items/chunked-upload-choice.md b/tasks/items/chunked-upload-choice.md index f4dcd40..e769ba1 100644 --- a/tasks/items/chunked-upload-choice.md +++ b/tasks/items/chunked-upload-choice.md @@ -1,7 +1,7 @@ # 🔬 Загрузка большого файла частями - **Тип:** research -- **Категория:** Очередь +- **Категория:** Очередь — Разведка про докачку идёт после того, как обычная загрузка доведена: иначе сравнивать не с чем. - **Зачем:** Гигабайтный файл едет одним запросом, и обрыв на девяноста процентах начинает его заново. Шестичасовая диктофонная запись и видео из семейного архива весят гигабайты, а diff --git a/tasks/items/config-example-toml.md b/tasks/items/config-example-toml.md index 1ee1203..33b3f0d 100644 --- a/tasks/items/config-example-toml.md +++ b/tasks/items/config-example-toml.md @@ -1,7 +1,7 @@ # 🧹 Переименовать образец конфига в config.example.toml - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Поднято наверх: шесть задач ниже правят конфиг и каждая допишет старое имя образца, удлиняя перечень мест переименования. - **Зачем:** Конвенция называет config.dist.toml объявленным расхождением, но тут же пишет это имя как правило — документ противоречит сам себе, а образец расходится с конвенцией. Конвенция конфигурации взята из проекта jellybit и **сама называет сегодняшнее diff --git a/tasks/items/context-cancel-in-pipeline.md b/tasks/items/context-cancel-in-pipeline.md index 8752d9d..de53c66 100644 --- a/tasks/items/context-cancel-in-pipeline.md +++ b/tasks/items/context-cancel-in-pipeline.md @@ -1,7 +1,7 @@ # 🧹 Прервать шаг конвейера отменой контекста - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Отмена контекста трогает те же шаги, что и таймауты: одна окрестность кода, два захода дороже одного. - **Зачем:** Воркер читает ctx только между итерациями: остановка контейнера ждёт конца шага, а на занятом писателе один запрос к хранилищу держится до 9,5 секунды при мягком таймауте в 5. Долг назван в `docs/architecture.md` — раздел про capability `pipeline`, где diff --git a/tasks/items/data-ownership.md b/tasks/items/data-ownership.md index cda086f..ac93fe5 100644 --- a/tasks/items/data-ownership.md +++ b/tasks/items/data-ownership.md @@ -1,7 +1,7 @@ # 🎯 Человек убирает свою запись из архива вместе со всеми текстами - **Тип:** goal -- **Секция:** Направления +- **Секция:** Запланировано — Очередь у цели появилась: удаление записи стоит 32-й строкой и трогает конвейер, файлы и колонку дедупликации, которые к тому месту готовы. - **Зачем:** Хранение бессрочное, а способа убрать запись нет ни одного: ошибочно загруженный файл и разговор, который человек не хочет держать у нас, остаются навсегда. - **Теги:** decomposed diff --git a/tasks/items/dedup-by-content-hash.md b/tasks/items/dedup-by-content-hash.md index 90a4938..d4857a4 100644 --- a/tasks/items/dedup-by-content-hash.md +++ b/tasks/items/dedup-by-content-hash.md @@ -1,7 +1,7 @@ # ✨ Узнавать уже загруженный файл по хеш-сумме - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Дедупликация ищет совпадение в пределах пользователя — то есть после владельца записи, и экономит деньги с первого дня приложения. - **Зачем:** Один и тот же файл, отправленный дважды, распознаётся дважды и оплачивается дважды: приём не смотрит на содержимое вовсе. - **Теги:** goal:upload-reliability @@ -38,4 +38,6 @@ Владелец записи приходит из `record-ownership` — до неё дедупликация опирается на того владельца, который уже есть. Хеш-сумма считается на сервере: подсчёт на стороне приложения относится к `chunked-upload-choice`. Колонка добавляется во -всех четырёх местах репозитория SQLite (инвариант `CLAUDE.md`). +всех четырёх местах пакета хранилища `internal/adapter/repo/pocketbase` — +`applyToRecord`, `recordToJob`, `acquireColumns`, `acquiredRow` с её `toJob`, — +плюс шаг схемы (инвариант `CLAUDE.md`). diff --git a/tasks/items/delete-record.md b/tasks/items/delete-record.md index 432ab78..bd9e6ed 100644 --- a/tasks/items/delete-record.md +++ b/tasks/items/delete-record.md @@ -1,7 +1,7 @@ # ✨ Удалять запись со всеми уровнями текста по требованию владельца - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Удаление трогает конвейер, файлы, объект хранилища и колонку дедупликации — всё это к этому месту уже готово. - **Зачем:** Ни файлы, ни расшифровки не удаляются вовсе: убрать запись сегодня можно только руками в базе и в каталоге на сервере. - **Теги:** goal:data-ownership diff --git a/tasks/items/email-notification.md b/tasks/items/email-notification.md index 0adbb97..b5ba52f 100644 --- a/tasks/items/email-notification.md +++ b/tasks/items/email-notification.md @@ -1,7 +1,7 @@ # ✨ Слать готовый текст на почту из учётной записи - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Второй канал на той же доставке. - **Зачем:** Адрес почты приходит вместе с входом через OIDC, но почтового отправителя в сервисе нет. - **Теги:** goal:ready-notification diff --git a/tasks/items/external-call-timeouts.md b/tasks/items/external-call-timeouts.md index c18c3a4..d763e76 100644 --- a/tasks/items/external-call-timeouts.md +++ b/tasks/items/external-call-timeouts.md @@ -1,7 +1,7 @@ # 🧹 Задать таймауты обращениям к внешним сервисам - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Молчащий внешний сервис держит шаг до истечения часового захвата — правится под свежими тестами конвейера. - **Зачем:** Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата. Все обращения наружу идут с `context.Background()` либо через `http.Get` без @@ -33,9 +33,6 @@ - У каждого обращения наружу есть таймаут, и его значение задаётся конфигом. Оракул — тест на подставном сервере, который не отвечает: вызов возвращается с ошибкой за назначенное время, а не висит. -- Отмена контекста при остановке приложения прерывает шаг конвейера. Оракул — - тест: отменённый контекст возвращает управление из шага, задача остаётся в - прежнем состоянии. - Прерванная по таймауту задача достаётся повторно и доходит до текста. Оракул — тест на повторный прогон шага после отказа по таймауту. - Числа таймаутов записаны в `docs/database.md`. Оракул — `task gate`. @@ -43,4 +40,7 @@ ## Рамки Повторов с нарастающей паузой не заводим — это отдельная работа; здесь только -таймаут и отмена. +таймаут. Прерывание шага отменённым контекстом сюда не входит: методы записи +хранилища контекста не принимают вовсе, и этим занимается следующая задача +`context-cancel-in-pipeline`. Контекст первым аргументом интерфейсов эта задача +всё же протаскивает — без него таймаут некуда передать. diff --git a/tasks/items/external-service-metrics.md b/tasks/items/external-service-metrics.md new file mode 100644 index 0000000..396c1e3 --- /dev/null +++ b/tasks/items/external-service-metrics.md @@ -0,0 +1,41 @@ +# ✨ Считать вызовы, отказы и длительность по каждому внешнему сервису + +- **Тип:** feature +- **Категория:** Очередь — Метрики внешних сервисов пишутся в выбранном словаре, а не переписываются потом. +- **Зачем:** Ни у Telegram, ни у Object Storage, ни у SpeechKit нет ни одной метрики: отказ внешнего сервиса виден только строкой в журнале контейнера. +- **Теги:** goal:service-observability + +Двигает пункты 2 и 5 «Завершения» цели: у каждого внешнего сервиса появляются +вызовы, отказы и длительность, а расход на платные сервисы виден числом. + +Внешних сервисов сегодня четыре — Telegram, Object Storage, SpeechKit и +`ffmpeg`/`ffprobe` как внешний процесс; пятым станет языковая модель. Метрика +заводится одной формой на все, чтобы шестой сервис не приносил шестого способа +считать. + +## Затрагивает + +- `internal/metrics` — форма метрики внешнего вызова: имя сервиса, операция, + исход, гистограмма длительности; +- адаптеры `internal/adapter/*` — место, где вызов оборачивается замером; +- `docs/architecture.md`, раздел эксплуатации — перечень метрик; +- `docs/conventions/` — правило: новый внешний вызов приходит со своей меткой, + а не со своей метрикой. + +## Критерии приёмки + +- У каждого из четырёх внешних сервисов есть счётчик вызовов, счётчик отказов и + гистограмма длительности. Оракул — тест: прогнать по одному вызову каждого + адаптера с подставным собеседником и снять `/metrics`. +- Отказ внешнего сервиса виден отдельно от успеха и не теряется в общем счётчике. + Оракул — тест: подставной собеседник отвечает отказом, метка исхода в метрике + отличается. +- Минуты распознавания и объём заливки видны числом. Оракул — тест: после шага + распознавания счётчик минут вырос на длительность записи, а после заливки в + Object Storage счётчик объёма — на размер файла. + +## Рамки + +Трассировку здесь не заводим — чем развивать наблюдаемость, решает разведка +`opentelemetry-fit`; эта задача остаётся в том словаре, который она выберет. +Учёт по пользователям — задача `usage-accounting`, здесь метрики сервисные. diff --git a/tasks/items/fix-migrations-path-in-docs-config.md b/tasks/items/fix-migrations-path-in-docs-config.md index 1580aac..63d1e51 100644 --- a/tasks/items/fix-migrations-path-in-docs-config.md +++ b/tasks/items/fix-migrations-path-in-docs-config.md @@ -1,7 +1,7 @@ # 🐞 Починить путь миграций в настройке сверки документов - **Тип:** fix -- **Категория:** Очередь +- **Категория:** Очередь — Шаг гейта зелен при изменённой миграции и нетронутом database.md: на этом шаге стоит приёмка всякой следующей задачи, трогающей схему. - **Зачем:** Ключ migrations указывает на каталог migrations/, которого в репозитории нет: шаг гейта зелен при изменённой миграции и нетронутом database.md, а конвенции числят этот род механизированным. Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — diff --git a/tasks/items/gate-changed-lines-coverage.md b/tasks/items/gate-changed-lines-coverage.md index 38666fd..583b4af 100644 --- a/tasks/items/gate-changed-lines-coverage.md +++ b/tasks/items/gate-changed-lines-coverage.md @@ -1,9 +1,8 @@ -# 🧹 Считать покрытие изменённых строк шагом гейта +# 🧹 Ронять гейт на изменённой функции, которую не выполняет ни один тест - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Непокрытую изменённую функцию дважды ловил проход ревью, а не машина; порог решён 2026-08-12, брать можно. - **Зачем:** Свойство «изменённое место покрыто хоть одним тестом» записано в docs/review.md, но не механизировано: за две задачи подряд непокрытые шаги ловили руками. -- **Теги:** question `CLAUDE.md`, раздел «Гейт», объявляет прямо: «покрытие изменённых строк не считается ничем». Цена этого измерена дважды. В задаче @@ -11,6 +10,13 @@ `pocketbase-storage` два из трёх шагов конвейера переписали целиком и не выполнили ни одним тестом — нашёл это проход ревью, а не машина. +**Единица счёта — функция, а не строка** (решение владельца 2026-08-12). Шаг +краснеет на новой или изменённой функции, которую не выполняет ни один тест; +доля покрытых строк внутри неё не считается и порогом не ограничивается. Довод: +процент изменённых строк роняет гейт на всякой ветке отказа, которую нечем +изобразить в тесте, а порог ниже ста пришлось бы брать из ниоткуда. Именно +непокрытая целиком функция — то, что дважды ловили руками. + ## Затрагивает - набор шагов `task gate` в `Taskfile.yml` и переменная `BASE` как база диффа; @@ -25,16 +31,11 @@ адресом. - Изменение, не трогающее код, шаг не гоняет. Оракул — `task gate` на дереве с правкой одной только документации: шаг сообщает о пропуске с причиной. -- Порог назван числом в `CLAUDE.md` и совпадает с тем, что проверяет шаг. Оракул - — `task gate`, шаг `docs.py check`. - -## Вопросы - -- **Какой порог и что считать изменённой строкой.** Варианты: сто процентов - изменённых строк (роняет на всякой ветке отказа, которую нечем изобразить), - порог ниже ста (число берётся из ниоткуда), только новые функции целиком. - Цена решения — частота ложных отказов гейта; пока решения нет, шаг заводить - нельзя. +- Единица счёта названа в `CLAUDE.md` и совпадает с тем, что проверяет шаг. + Оракул — `task gate`, шаг `docs.py check`. +- Функция, тронутая правкой на одну строку, шаг не роняет, если её вызывает хоть + один тест. Оракул — прогон на дереве с однострочной правкой внутри покрытой + функции: шаг зелёный. ## Рамки diff --git a/tasks/items/gate-dependency-vulnerabilities.md b/tasks/items/gate-dependency-vulnerabilities.md index b0f97a1..a1441fa 100644 --- a/tasks/items/gate-dependency-vulnerabilities.md +++ b/tasks/items/gate-dependency-vulnerabilities.md @@ -1,7 +1,7 @@ # 🧹 Ловить уязвимости в зависимостях шагом гейта - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — govulncheck находит две достижимые уязвимости, а гейт о нём не знает: о третьей узнать будет неоткуда. - **Зачем:** govulncheck находит две достижимые уязвимости в клиентах Yandex, а ни гейт, ни список «чего в гейте нет» о нём не знают: узнать о третьей будет неоткуда. Обе найдены ревью задачи `pocketbase-storage` 2026-08-12 и достижимы из кода diff --git a/tasks/items/gate-extra-linters.md b/tasks/items/gate-extra-linters.md index 937eb41..90ef1b6 100644 --- a/tasks/items/gate-extra-linters.md +++ b/tasks/items/gate-extra-linters.md @@ -1,7 +1,7 @@ # 🧹 Завести недостающие проверки: shellcheck, hadolint и тест скрипта сверки версий - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Инструмент: shellcheck и hadolint не ловят ничего из перечисленного выше и потому ждут. - **Зачем:** Скрипты оболочки и Dockerfile машина не проверяет: shellcheck и hadolint не заведены, lefthook смотрит только gitleaks, а единственный исполняемый скрипт проекта на shell — scripts/check-go-version.sh — разбирали на ревью глазами построчно, потому что машине его показать нечем. Собрана из двух источников: заметки владельца от 2026-08-12 про lefthook и diff --git a/tasks/items/gate-step-exit-codes.md b/tasks/items/gate-step-exit-codes.md index 62db7b5..2458c5a 100644 --- a/tasks/items/gate-step-exit-codes.md +++ b/tasks/items/gate-step-exit-codes.md @@ -1,7 +1,7 @@ # 🧹 Привести коды выхода шагов гейта к общему словарю - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Словарь кодов уже разъехался с обёртками: три новых шага гейта ниже напишутся по неверному образцу и попадут под переписывание. - **Зачем:** CLAUDE.md объявляет словарь кодов общим для четырёх проверочных шагов и описывает код 3 как «каталог или файл не найден», но все четыре обёртки в Taskfile.yml при отсутствующем скрипте отдают 1 («дрейф»), из-за чего читатель кода идёт искать разъехавшееся там, где просто неполно дерево. Расхождение старое и повторено всеми четырьмя обёртками одинаково — дефектом diff --git a/tasks/items/http-transport-nits.md b/tasks/items/http-transport-nits.md index 5c50b97..83a4aea 100644 --- a/tasks/items/http-transport-nits.md +++ b/tasks/items/http-transport-nits.md @@ -1,7 +1,7 @@ # 🧹 Разобрать мелочи http-транспорта - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Поднято до контракта API: маршруты объявлены дважды, и переименование пути проходит тесты зелёным — переписывать контроллер под слепыми проверками нельзя. - **Зачем:** Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом. - **Теги:** review-2026-08-11 diff --git a/tasks/items/insights-visible-in-list.md b/tasks/items/insights-visible-in-list.md new file mode 100644 index 0000000..b5ebda8 --- /dev/null +++ b/tasks/items/insights-visible-in-list.md @@ -0,0 +1,40 @@ +# ✨ Показывать заголовок в списке, отбирать список по темам и считать токены + +- **Тип:** feature +- **Категория:** Очередь — Три пункта «Завершения» цели не закрывала ни одна задача: показывать и отбирать можно, когда заголовки и темы уже считаются. +- **Зачем:** Заголовок, темы и пересказ считаются, но список по-прежнему показывает первые слова расшифровки и не отбирается ничем, а расход на модель не виден числом. +- **Теги:** goal:text-insights + +Двигает пункты 1, 4 и 6 «Завершения» цели — те три, где выводы из текста +становятся видны человеку и владельцу: заголовок в списке (1), отбор по темам +(4), стоимость числом (6). Сами уровни считает `llm-insights-adapter`, показать +их некому: экран списка написан раньше и знает только первые слова расшифровки. + +Берётся после `llm-insights-adapter`: пока заголовков и тем нет, показывать и +отбирать нечего. + +## Затрагивает + +- контракт списка записей: заголовок в элементе и отбор по теме параметром + запроса; +- экран списка — вместо первых слов расшифровки заголовок, и отбор по теме; +- индекс по темам в хранилище, если отбор без него читает всю выборку; +- `internal/metrics` — число обработанных записей и число токенов языковой + модели; +- `docs/architecture.md`, раздел эксплуатации — что означает счётчик токенов. + +## Критерии приёмки + +- В списке у записи с заголовком показан заголовок, а не первые слова + расшифровки; у записи без заголовка — прежнее поведение. Оракул — тест экрана + на подставном API с обеими записями. +- Список отбирается по теме и отдаёт только записи с ней. Оракул — тест API: + выборка по теме из трёх записей возвращает одну. +- Расход языковой модели виден числом: сколько записей обработано и сколько + потрачено токенов. Оракул — тест шага на подставной модели: оба счётчика + выросли на значения из ответа. + +## Рамки + +Поиска по тексту расшифровки не делаем — отбор идёт только по темам. Разбивку +расхода по пользователям считает `usage-accounting`, здесь метрика общая. diff --git a/tasks/items/installable-pwa.md b/tasks/items/installable-pwa.md index 0f3b835..2292c53 100644 --- a/tasks/items/installable-pwa.md +++ b/tasks/items/installable-pwa.md @@ -1,7 +1,7 @@ # ✨ Сделать приложение устанавливаемым на телефон - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Ставить на телефон есть смысл, когда есть что ставить. - **Зачем:** Приложение, живущее вкладкой браузера, теряется среди прочих: ярлыка на экране у него нет. - **Теги:** goal:web-access diff --git a/tasks/items/intake-limits-measure.md b/tasks/items/intake-limits-measure.md index 96713ce..529b2f5 100644 --- a/tasks/items/intake-limits-measure.md +++ b/tasks/items/intake-limits-measure.md @@ -1,7 +1,7 @@ # 🔬 Потолки приёма, конвертации и заливки по длине записи - **Тип:** research -- **Категория:** Очередь +- **Категория:** Очередь — Второй замер — остальные четыре звена. - **Зачем:** Из пяти звеньев задача speechkit-limits замерила только модель распознавания: где отваливается шестичасовая запись до неё, неизвестно. - **Теги:** goal:long-recordings diff --git a/tasks/items/job-path-by-request.md b/tasks/items/job-path-by-request.md new file mode 100644 index 0000000..295606b --- /dev/null +++ b/tasks/items/job-path-by-request.md @@ -0,0 +1,41 @@ +# ✨ Собирать путь одной записи по конвейеру запросом + +- **Тип:** feature +- **Категория:** Очередь — Пункт 3 «Завершения» цели не закрывала ни одна задача; применяет словарь, который выберет разведка строкой выше. +- **Зачем:** Звенья пути связаны только идентификатором задачи в строках журнала: чтобы понять, где запись провела минуты, владелец читает логи контейнера глазами. +- **Теги:** goal:service-observability + +Двигает пункт 3 «Завершения» цели: путь одной записи по конвейеру собирается +запросом, а не чтением логов глазами. + +Путь длиной в минуты идёт через четыре внешних сервиса и три воркера. Сегодня +его звенья связывает `job_id` в строках журнала, и собирает их человек. + +**Форма ответа зависит от исхода разведки** `opentelemetry-fit`: с OpenTelemetry +это трасса со спанами на шаг, без него — отметки времени переходов у самой +задачи и запрос по ним. Разведка выбирает словарь, эта задача его применяет. + +## Затрагивает + +- таблица задач: отметки времени перехода между состояниями и их шаг схемы — + либо экспорт спанов, если разведка выберет трассировку; +- шаги конвейера — место, где отмечается начало и конец звена; +- способ прочитать собранное: запрос к хранилищу либо внешний приёмник трасс; +- `docs/architecture.md`, раздел эксплуатации — как читается путь записи; +- `docs/security.md` — путь записи не несёт ни текста, ни имени файла. + +## Критерии приёмки + +- Путь записи от приёма до готового текста читается одним обращением, без чтения + журнала. Оракул — тест: прогнать задачу через все шаги на подставных адаптерах + и получить длительность каждого звена. +- Путь отказавшей записи показывает звено, на котором она встала. Оракул — тест + с отказом на конвертации: звено названо, дальнейших звеньев нет. +- Собранное не несёт содержимого записи. Оракул — тест: в ответе только + идентификаторы, состояния, длительности и расширение. + +## Рамки + +Берётся после `opentelemetry-fit`: пока словарь не выбран, работа рискует быть +переписанной целиком. Хранение путей дольше срока жизни задачи за задачей не +стоит. diff --git a/tasks/items/json-api-for-spa.md b/tasks/items/json-api-for-spa.md index 02409af..6131dac 100644 --- a/tasks/items/json-api-for-spa.md +++ b/tasks/items/json-api-for-spa.md @@ -1,7 +1,7 @@ # ✨ Свести приём и чтение записей к одному контракту для приложения - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Единая точка трансляции доменной ошибки — база и для экранов, и для токенов; список своих записей заводится после владельца, а не до. - **Зачем:** Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем. - **Теги:** goal:web-access @@ -25,10 +25,10 @@ ## Критерии приёмки -- Сбой базы при чтении задачи даёт `500`, а не `404`. Оракул — тест с - репозиторием, возвращающим ошибку драйвера. -- Негодный файл даёт `400` с человекочитаемым текстом, а не `500`. Оракул — - тест: файл, который отвергает разбор метаданных. +- Код ответа отвечает причине отказа, а не месту, где он случился: сбой базы при + чтении даёт `500`, а не `404`, негодный файл — `400` с человекочитаемым + текстом, а не `500`. Оракул — два теста: репозиторий, возвращающий ошибку + драйвера, и файл, который отвергает разбор метаданных. - Тело ошибки одной формы на всех эндпоинтах, не содержит сырого `err.Error()` и опечатки `transcibe`. Оракул — тест на четырёх ветвях отказа (форма совпадает, текста внутренней ошибки в теле нет) плюс пустой `grep -rn 'transcibe' @@ -37,12 +37,19 @@ тест на выборке больше страницы. - Отображение ошибки живёт в одной функции, и она названа в `docs/architecture.md`. Оракул — `task gate`, шаг `docs.py check`. +- Ответ приёма отдаёт список заведённых записей и место под признак повторного + файла, даже когда файл в запросе один. Оракул — тест приёма: тело ответа — + список из одного элемента, у элемента есть поле признака повтора. ## Рамки Аутентификацию и владельца не заводим — это `oidc-login` и `record-ownership`; задача про форму контракта. Публичный контракт после мерджа обратной правкой не -откатывается — поэтому сюда же приехала опечатка `transcibe` в тексте ошибки -приёма (нашёл проход `review-specs` на ревью change +откатывается, и по решению владельца от 2026-08-12 форма ответа приёма +согласуется здесь один раз — сразу списком и с местом под признак повтора, — +чтобы `dedup-by-content-hash`, `multi-file-upload` и `reject-oversized-recording` +её не переписывали, а экран загрузки не переделывался под вторую форму. Приём +по-прежнему берёт из запроса один файл: меняется форма ответа, не число файлов. +Сюда же приехала опечатка `transcibe` в тексте ошибки приёма (нашёл проход `review-specs` на ревью change `2026-08-11-fix-http-handler-tests`): в одиночку текст ошибки менять нельзя, а здесь контракт переписывается целиком. diff --git a/tasks/items/literary-text-level.md b/tasks/items/literary-text-level.md index f6eebb2..0cb62b3 100644 --- a/tasks/items/literary-text-level.md +++ b/tasks/items/literary-text-level.md @@ -1,7 +1,7 @@ # ✨ Отдавать вычитанный текст рядом с сырым - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Вычитанный текст считается тем же адаптером. - **Зачем:** Сырая расшифровка идёт без знаков препинания, с повторами и словами-паразитами: читать её подряд тяжело, а другого уровня текста нет. - **Теги:** goal:text-insights @@ -26,8 +26,6 @@ сырое совпадает с ответом распознавания. - Отказ модели на вычитке не роняет задачу и не портит сырой текст. Оракул — тест с моделью, возвращающей ошибку: задача в `done`, сырой текст на месте. -- Выключенный в настройках уровень не запрашивается и остаётся пустым. Оракул — - тест с выключенной вычиткой: обращений за ней нет. - Экран показывает оба уровня с переключением между ними. Оракул — тест экрана на подставном API. @@ -35,4 +33,5 @@ Ручной правки текста человеком не делаем — это граница паспорта: сервис отдаёт машинную вычитку, а редактором не становится. Берётся после -`llm-insights-adapter`: клиент модели заводит она. +`llm-insights-adapter`: клиент модели заводит она. Выключение уровня настройкой +пользователя сюда не входит — это `settings-applied-in-pipeline`. diff --git a/tasks/items/llm-insights-adapter.md b/tasks/items/llm-insights-adapter.md index fb2d3ea..796a279 100644 --- a/tasks/items/llm-insights-adapter.md +++ b/tasks/items/llm-insights-adapter.md @@ -1,7 +1,7 @@ # ✨ Считать заголовок, темы и пересказ внешней моделью - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Уровни текста: сюда приходит пятая внешняя зависимость, и конвейер к этому моменту покрыт тестами. - **Зачем:** Расшифровка доходит стеной текста: ни заголовка, ни тем, ни пересказа сервис не считает, и клиента языковой модели в нём нет. - **Теги:** goal:text-insights @@ -32,16 +32,14 @@ OpenAI-совместимым интерфейсом за шлюзом bifrost, - Отказ и молчание модели не роняют задачу: текст доходит до человека без выводов. Оракул — тест с моделью, возвращающей ошибку и таймаут: задача в `done`, текст на месте, в журнале одна запись уровня `WARN`. -- Уровень, выключенный в настройках пользователя, у модели не запрашивается, а - настройка берётся та, что действовала на приёме записи. Оракул — тест с - выключенными темами и тест с настройкой, изменённой после приёма: вызовов за - темами нет в обоих случаях. - Ключ и адрес шлюза не попадают ни в журнал, ни в ответ. Оракул — тест шага с перехваченным журналом. ## Рамки -Литературный текст сюда не входит — это `literary-text-level`. Своей модели не +Литературный текст сюда не входит — это `literary-text-level`. Выборочность по +настройкам пользователя тоже: здесь считаются все уровни, а выключать их учит +`settings-applied-in-pipeline`. Своей модели не держим и промптов в код не зашиваем сверх необходимого. Прогон на боевом ключе ради проверки запрещён: подставной адаптер. Обращение к модели платное, и объём расхода учитывает `usage-accounting`. diff --git a/tasks/items/local-run-without-telegram-token.md b/tasks/items/local-run-without-telegram-token.md index e1d6781..f5fa167 100644 --- a/tasks/items/local-run-without-telegram-token.md +++ b/tasks/items/local-run-without-telegram-token.md @@ -1,7 +1,7 @@ # 🧹 Поднимать сервис локально без действующего токена бота - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Поднято к долгам входа: живой прогон нужен именно им, а сегодня его нет ни у одной задачи. - **Зачем:** Адаптер Telegram проверяет токен обращением к Telegram и роняет старт, а боевым токеном запускаться запрещено: проверить поведение живым прогоном не может ни одна задача. Замечено при попытке проверить вход вживую в задаче `oidc-login` 2026-08-12; diff --git a/tasks/items/login-surface-hypotheses.md b/tasks/items/login-surface-hypotheses.md index 01bebbd..9b5a565 100644 --- a/tasks/items/login-surface-hypotheses.md +++ b/tasks/items/login-surface-hypotheses.md @@ -1,7 +1,7 @@ # 🔬 Четыре недоказанные гипотезы о поверхности входа - **Тип:** research -- **Категория:** Очередь +- **Категория:** Очередь — Разведка закрывает тему входа последней: остальные три задачи меняют то, что она проверяет. - **Зачем:** Ревью назвало четыре пути, которых не смогло ни подтвердить, ни опровергнуть: браузера и живого провайдера в прогоне не было. Провенанс — отчёт триажа ревью задачи `oidc-login` 2026-08-12, diff --git a/tasks/items/login-url-from-collection-settings.md b/tasks/items/login-url-from-collection-settings.md index 85cc9e2..07a2f5d 100644 --- a/tasks/items/login-url-from-collection-settings.md +++ b/tasks/items/login-url-from-collection-settings.md @@ -1,7 +1,7 @@ # 🧹 Строить адрес входа из настроек коллекции, а не из конфига - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Замыкает тройку правок обработчиков входа. - **Зачем:** Первая половина входа собрана руками из конфига и на настройки провайдера не смотрит, вторая берётся из коллекции: обновление библиотеки изменит только вторую половину. Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — diff --git a/tasks/items/long-audio-chunking.md b/tasks/items/long-audio-chunking.md index 8f1b11a..622eca7 100644 --- a/tasks/items/long-audio-chunking.md +++ b/tasks/items/long-audio-chunking.md @@ -1,7 +1,7 @@ # ✨ Резать длинную запись на фрагменты и продолжать с места остановки - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Резка на фрагменты — самая глубокая переделка конвейера, и она идёт по замеренным числам. - **Зачем:** Шаг конвейера повторяется целиком: перезапуск на пятом часу шестичасовой записи начинает распознавание заново и оплачивает его второй раз. - **Теги:** goal:long-recordings diff --git a/tasks/items/long-recordings.md b/tasks/items/long-recordings.md index 8427294..37977b5 100644 --- a/tasks/items/long-recordings.md +++ b/tasks/items/long-recordings.md @@ -1,7 +1,7 @@ # 🎯 Запись длиной до шести часов доходит до текста - **Тип:** goal -- **Секция:** Направления +- **Секция:** Направления — Цель начинается с двух замеров и кончается резкой на фрагменты — самой глубокой переделкой конвейера: очереди внутри нет, пока числа не получены. - **Зачем:** Потолок не замерен ни на одном звене: Telegram не отдаёт больше 20 МиБ, границы модели deferred-general неизвестны, а перезапуск на середине начинает распознавание заново. - **Теги:** decomposed diff --git a/tasks/items/long-text-delivery.md b/tasks/items/long-text-delivery.md index 824670d..77b380c 100644 --- a/tasks/items/long-text-delivery.md +++ b/tasks/items/long-text-delivery.md @@ -1,7 +1,7 @@ # ✨ Отдавать текст в сотни килобайт файлом, а не сотней сообщений - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Сотни килобайт текста появляются только после долгих записей. - **Зачем:** Отправитель Telegram режет текст по 4000 знаков: расшифровка шестичасовой записи придёт сотней сообщений подряд. - **Теги:** goal:long-recordings diff --git a/tasks/items/metaviewer-adapter-tests.md b/tasks/items/metaviewer-adapter-tests.md index c344e07..f564c0a 100644 --- a/tasks/items/metaviewer-adapter-tests.md +++ b/tasks/items/metaviewer-adapter-tests.md @@ -1,7 +1,7 @@ # 🧹 Покрыть тестами разбор вывода ffprobe - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Разбор вывода ffprobe не проверяет ничто, а приём видео его расширяет. - **Зачем:** Проверки приёма перестали звать настоящий ffprobe 2026-08-11, а своего теста у адаптера метаданных нет: разбор JSON и отличие «программы нет в PATH» от «обработка отказала» не проверяет ничто. - **Теги:** review-2026-08-11 diff --git a/tasks/items/multi-file-upload.md b/tasks/items/multi-file-upload.md index e687397..f8eeb64 100644 --- a/tasks/items/multi-file-upload.md +++ b/tasks/items/multi-file-upload.md @@ -1,7 +1,7 @@ # ✨ Принимать до десяти файлов одной загрузкой - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Пачка файлов заводится на готовом экране загрузки и готовой дедупликации. - **Зачем:** Приём берёт один файл в запросе, а с телефона выбирают пачку сразу: десять записей значат десять заходов на экран загрузки. - **Теги:** goal:upload-reliability diff --git a/tasks/items/multi-user.md b/tasks/items/multi-user.md index 64405b5..a7f6468 100644 --- a/tasks/items/multi-user.md +++ b/tasks/items/multi-user.md @@ -1,7 +1,7 @@ # 🎯 Сервисом пользуются несколько человек, и записи одного не видны другому - **Тип:** goal -- **Секция:** Запланировано +- **Секция:** Запланировано — Владелец записи — фундамент, на котором стоят список своих записей, дедупликация, удаление, учёт расхода и квота: пока его нет, остальные цели строятся на песке. - **Зачем:** У задачи нет владельца, а HTTP API открыт наружу без аутентификации: пригласить второго человека сейчас значит открыть ему чужие расшифровки. - **Теги:** decomposed diff --git a/tasks/items/ntfy-delivery.md b/tasks/items/ntfy-delivery.md index 271c37d..f8dab4a 100644 --- a/tasks/items/ntfy-delivery.md +++ b/tasks/items/ntfy-delivery.md @@ -1,7 +1,7 @@ # ✨ Отправлять готовый текст через apprise и ntfy - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Канал уведомлений выбирается в настройках, которые уже есть. - **Зачем:** Пользователь веба узнаёт о готовности только опросом с открытого экрана. - **Теги:** goal:ready-notification diff --git a/tasks/items/opentelemetry-fit.md b/tasks/items/opentelemetry-fit.md index 79b7a99..2a67bc2 100644 --- a/tasks/items/opentelemetry-fit.md +++ b/tasks/items/opentelemetry-fit.md @@ -1,7 +1,7 @@ # 🔬 Стоит ли брать OpenTelemetry вместо голого Prometheus - **Тип:** research -- **Категория:** Очередь +- **Категория:** Очередь — Сопровождение: словарь метрик выбирается до того, как метрик станет втрое больше. - **Зачем:** Метрик одиннадцать штук на пять счётчиков, трассировки нет вовсе: путь одной записи по конвейеру собирается только чтением логов глазами. - **Теги:** goal:service-observability diff --git a/tasks/items/orphan-file-on-failed-intake.md b/tasks/items/orphan-file-on-failed-intake.md index 8f8a6d1..1980186 100644 --- a/tasks/items/orphan-file-on-failed-intake.md +++ b/tasks/items/orphan-file-on-failed-intake.md @@ -1,7 +1,7 @@ # 🐞 Убирать записанный файл, когда приём отказал на середине - **Тип:** fix -- **Категория:** Очередь +- **Категория:** Очередь — Осиротевший файл — тот же отказ на середине, и чинится тем же местом приёма. - **Зачем:** Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками. - **Теги:** review-2026-08-11, goal:upload-reliability diff --git a/tasks/items/owner-alerting.md b/tasks/items/owner-alerting.md new file mode 100644 index 0000000..5542d54 --- /dev/null +++ b/tasks/items/owner-alerting.md @@ -0,0 +1,38 @@ +# ✨ Оповещать владельца об отказе, не дожидаясь жалобы + +- **Тип:** feature +- **Категория:** Очередь — Правило оповещения ставится на метрику, которой до этого нет. +- **Зачем:** Об отказе владелец узнаёт от пользователя: правил оповещения нет ни на одной метрике, а метрики читают глазами. +- **Теги:** goal:service-observability + +Двигает пункт 4 «Завершения» цели: владелец узнаёт об отказе сам. + +Берётся после `external-service-metrics` и `stalled-pipeline-metric`: правило +оповещения ставится на метрику, а метрик, на которых его ставить, сегодня нет. + +Доставку берём ту же, что и пользовательскую, — apprise с ntfy: второй канал +ради того же сообщения не заводим. + +## Затрагивает + +- правила оповещения: где они живут — в сервисе или в выкладке + `pet-project-server`, выбор входит в задачу; +- секция конфигурации канала оповещения владельца; +- `docs/architecture.md`, раздел эксплуатации — кто и о чём оповещает; +- `CLAUDE.md` — запрет на выкладку остаётся: правила выкладывает человек. + +## Критерии приёмки + +- Задача, ушедшая в `failed`, доходит до владельца сообщением. Оракул — тест с + подставным каналом: после перевода задачи в `failed` канал получил сообщение с + идентификатором задачи. +- Сообщение не несёт ни текста расшифровки, ни имени файла пользователя. Оракул + — тест: в отправленном теле только идентификаторы, длина и причина. +- Недоступность канала оповещения не роняет конвейер. Оракул — тест: канал + отвечает отказом, задача остаётся в прежнем состоянии, в журнале одна строка. + +## Рамки + +Выкладку правил запускает человек — `inv pl -- transcriber` из +`pet-project-server`. Пользовательские уведомления о готовности здесь не +трогаем: это цель `ready-notification`. diff --git a/tasks/items/per-user-size-quota.md b/tasks/items/per-user-size-quota.md index 0d12cf2..3829915 100644 --- a/tasks/items/per-user-size-quota.md +++ b/tasks/items/per-user-size-quota.md @@ -1,7 +1,7 @@ # 🔬 Квота по общему размеру загруженного на пользователя - **Тип:** research -- **Категория:** Очередь +- **Категория:** Очередь — Сырьё: спор с паспортом не решается, пока объём по пользователям не считается ничем. - **Зачем:** Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас. Заметка звучит так: «Квота по размеру загруженных файлов на пользователя. В diff --git a/tasks/items/pin-runtime-image-base.md b/tasks/items/pin-runtime-image-base.md index 3e070c8..5ac4d05 100644 --- a/tasks/items/pin-runtime-image-base.md +++ b/tasks/items/pin-runtime-image-base.md @@ -1,7 +1,7 @@ # 🧹 Закрепить версию рантайм-базы образа - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Закрепление базы образа окупается при частых пересборках, а они начнутся с приложением. - **Зачем:** Финальный слой Dockerfile собирается на alpine:latest, а task image идёт с --pull, поэтому два образа из одного коммита с разницей в неделю несут разный ffmpeg — регрессия конвертации после такой пересборки выглядит как задачи в failed при пустом диффе репозитория, и откат на прежний коммит её не чинит. Класс тот же, ради которого задача `go-1-26-upgrade` завела шаг сверки версий: diff --git a/tasks/items/pipeline-step-tests.md b/tasks/items/pipeline-step-tests.md index 0cde118..63b5095 100644 --- a/tasks/items/pipeline-step-tests.md +++ b/tasks/items/pipeline-step-tests.md @@ -1,7 +1,7 @@ # 🧹 Покрыть тестами шаги конвейера и захват задачи - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Шаги конвейера не покрыты ничем, а удаление, уровни текста и резка длинных записей переписывают именно их: дальше без тестов идти вслепую. - **Зачем:** Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем. - **Теги:** review-2026-08-11 @@ -38,8 +38,7 @@ Telegram. Шаги конвейера — `FindAndRunConversionJob`, `FindAndRun ## Рамки -Задачу лучше брать **после** разведки -[job-queue-choice](job-queue-choice.md): она может сменить модель очереди -целиком, и тесты, написанные под сегодняшний захват, придётся выбросить вместе с -ним. +Модель очереди решена 2026-08-11 — +[очередь коллекцией PocketBase](../../docs/adr/ADR-2026-08-11-queue-as-pocketbase-collection.md), +— и разведки о ней больше нет. Тесты пишутся под сегодняшний захват. diff --git a/tasks/items/play-recording-in-app.md b/tasks/items/play-recording-in-app.md index 933052f..ec93402 100644 --- a/tasks/items/play-recording-in-app.md +++ b/tasks/items/play-recording-in-app.md @@ -1,7 +1,7 @@ # ✨ Проигрывать загруженную запись на экране записи - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Проигрывание живёт на экране записи, которого до списка нет. - **Зачем:** Послушать загруженное приложение не даёт, а самой копии для этого у задачи нет: указатель на файл перезаписывается на каждом шаге конвейера и у готовой задачи ведёт на объект в Object Storage. - **Теги:** goal:web-access diff --git a/tasks/items/provider-code-out-of-storage-log.md b/tasks/items/provider-code-out-of-storage-log.md index 24689e6..e6ee2cc 100644 --- a/tasks/items/provider-code-out-of-storage-log.md +++ b/tasks/items/provider-code-out-of-storage-log.md @@ -1,7 +1,7 @@ # 🐞 Убрать код провайдера из журнала запросов хранилища - **Тип:** fix -- **Категория:** Очередь +- **Категория:** Очередь — Три задачи входа собраны подряд: все правят обработчики входа и одно требование спеки access — три захода в один файл дороже одного. - **Зачем:** Строка запроса с кодом входа целиком уезжает в таблицу _logs и лежит там пять суток, хотя спека access требует, чтобы код в журнал не попадал. Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — diff --git a/tasks/items/ready-notification.md b/tasks/items/ready-notification.md index 8ef220d..d635af6 100644 --- a/tasks/items/ready-notification.md +++ b/tasks/items/ready-notification.md @@ -1,7 +1,7 @@ # 🎯 Пользователь узнаёт о готовности текста, не держа приложение открытым - **Тип:** goal -- **Секция:** Запланировано +- **Секция:** Запланировано — Канал уведомлений выбирается в настройках, которые к этому месту уже есть. - **Зачем:** Расшифровка занимает минуты, и всё это время человек либо смотрит на экран с опросом статуса, либо забывает вернуться. - **Теги:** decomposed diff --git a/tasks/items/record-ownership.md b/tasks/items/record-ownership.md index 2274af3..f516bfe 100644 --- a/tasks/items/record-ownership.md +++ b/tasks/items/record-ownership.md @@ -1,7 +1,7 @@ # ✨ Привязать запись к владельцу и отдавать только свои - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Фундамент домена: без владельца знание UUID и есть право читать чужое, и на владельце стоят список, дедуп, удаление, учёт и квота. - **Зачем:** У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать. - **Теги:** goal:multi-user @@ -14,7 +14,8 @@ - таблица задач и таблица файлов — колонка владельца и её миграция; - `internal/contract`, интерфейсы репозиториев: чтение сужается владельцем; -- `internal/adapter/repo/sqlite` — все четыре запроса задач; +- `internal/adapter/repo/pocketbase` — все четыре места, где правятся колонки + очереди: `applyToRecord`, `recordToJob`, `acquireColumns` и `acquiredRow`; - `internal/service`, оба метода заведения задачи; - `GET /api/status/:id` — ответ на чужую запись; - `docs/database.md` — схема и правило выборки. @@ -24,9 +25,8 @@ - Запрос чужой записи по её идентификатору возвращает «не найдено», а не содержимое и не «доступ запрещён». Оракул — тест: две сессии, задача первой запрашивается второй, ответ 404 и пустое тело. -- Запись, заведённая из веба, принадлежит вошедшему; заведённая ботом — - пользователю, за которым закреплён чат. Оракул — тест на оба входа со сверкой - колонки владельца. +- Запись, заведённая из веба, принадлежит вошедшему. Оракул — тест приёма по + HTTP со сверкой колонки владельца. - Выборка воркера владельцем **не** сужается: конвейер обрабатывает записи всех. Оракул — тест: задачи двух владельцев проходят конвейер одним воркером. - База заводится с чистого листа, колонка владельца обязательна и без умолчания. @@ -35,4 +35,6 @@ ## Рамки Совместного доступа, ролей и передачи записи другому не делаем: владелец один и -неизменяем. Данные не переносим — чистый лист. +неизменяем. Данные не переносим — чистый лист. Владельца записи, пришедшей +ботом, здесь не назначаем: связи чата с учётной записью ещё нет, её заводит +`telegram-account-link` следующей задачей. diff --git a/tasks/items/records-list-screen.md b/tasks/items/records-list-screen.md index 11acd25..f322a5e 100644 --- a/tasks/items/records-list-screen.md +++ b/tasks/items/records-list-screen.md @@ -1,7 +1,7 @@ # ✨ Сделать экран списка своих записей и чтения текста - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Список и чтение текста берутся после экрана загрузки — так записано в самой задаче. - **Зачем:** Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем. - **Теги:** goal:web-access diff --git a/tasks/items/reject-oversized-recording.md b/tasks/items/reject-oversized-recording.md index 3187a8d..b6462b4 100644 --- a/tasks/items/reject-oversized-recording.md +++ b/tasks/items/reject-oversized-recording.md @@ -1,7 +1,7 @@ # ✨ Отклонять на приёме запись сверх потолка - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Отклонять сверх потолка можно, когда потолок замерен. - **Зачем:** Запись сверх потолка принимается молча и висит в конвейере до истечения часового захвата, а человек всё это время ждёт текста. - **Теги:** goal:long-recordings diff --git a/tasks/items/response-assertions-judge-result.md b/tasks/items/response-assertions-judge-result.md index 99372f4..843fb10 100644 --- a/tasks/items/response-assertions-judge-result.md +++ b/tasks/items/response-assertions-judge-result.md @@ -1,7 +1,7 @@ # 🧹 Судить ответ в тестах по готовому ответу - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Тест, читающий живую карту заголовков, зелен при неработающем коде: класс всплывал трижды и трижды стоил ложной приёмки. - **Зачем:** Проверка, читающая изменяемую карту заголовков обработчика, зелена при неработающем коде: класс всплыл трижды, последний раз на уборке куки входа. Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — diff --git a/tasks/items/review-config-from-go-upgrade.md b/tasks/items/review-config-from-go-upgrade.md index 683f40d..9fa48d7 100644 --- a/tasks/items/review-config-from-go-upgrade.md +++ b/tasks/items/review-config-from-go-upgrade.md @@ -1,7 +1,7 @@ # 🧹 Настроить конвейер ревью по итогам прогона go-1-26-upgrade - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Настройка конвейера ревью по итогам прогона — работа над процессом, не над продуктом. - **Зачем:** Прогон вскрыл две прорехи настройки: «Типовые узлы» знают только рантайм и не знают рода «проверочный шаг набора проверок», а «Триггеры метки» не видят оси «изменение трогает канон» — и именно она дала обе блокирующие находки. Обе прорехи одного рода — настройка конвейера, живущая в `docs/review.md`, — и diff --git a/tasks/items/rollback-does-not-undo-schema-step.md b/tasks/items/rollback-does-not-undo-schema-step.md index 568fb34..e47fe0d 100644 --- a/tasks/items/rollback-does-not-undo-schema-step.md +++ b/tasks/items/rollback-does-not-undo-schema-step.md @@ -1,7 +1,7 @@ # 🧹 Назвать в необратимом, что откат кода не откатывает шаг схемы - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Записать необратимость шага схемы надо до того, как схему тронут задачи фундамента. - **Зачем:** Откат бинаря оставляет применённый шаг схемы в силе, и на этом строятся решения о выкладке: сегодня об этом не сказано нигде. Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — diff --git a/tasks/items/server-side-login-state.md b/tasks/items/server-side-login-state.md index ca43424..900f442 100644 --- a/tasks/items/server-side-login-state.md +++ b/tasks/items/server-side-login-state.md @@ -1,7 +1,7 @@ # 🐞 Вести учёт употреблённых состояний входа на сервере - **Тип:** fix -- **Категория:** Очередь +- **Категория:** Очередь — Тот же файл и та же спека, что у строки выше. - **Зачем:** Одноразовость возврата держится на уборке куки, то есть на браузере: сервер не помнит, какие состояния уже потрачены. Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — diff --git a/tasks/items/service-observability.md b/tasks/items/service-observability.md index f80e0c5..712f459 100644 --- a/tasks/items/service-observability.md +++ b/tasks/items/service-observability.md @@ -1,7 +1,7 @@ # 🎯 Состояние сервиса видно без чтения логов - **Тип:** goal -- **Секция:** Сопровождение +- **Секция:** Сопровождение — Словарь метрик выбирается разведкой до того, как метрик станет втрое больше; наблюдаем мы, а не пользователь сервиса. - **Зачем:** Отказ замечает пользователь, а не владелец: оповещения нет, а путь записи по конвейеру собирается глазами по логам контейнера. - **Теги:** decomposed diff --git a/tasks/items/settings-applied-in-pipeline.md b/tasks/items/settings-applied-in-pipeline.md new file mode 100644 index 0000000..34ea10c --- /dev/null +++ b/tasks/items/settings-applied-in-pipeline.md @@ -0,0 +1,41 @@ +# ✨ Считать только те уровни текста, что включены у владельца записи + +- **Тип:** feature +- **Категория:** Очередь — Выключать нечего, пока уровней нет; настройка ставится сразу за ними, чтобы платная модель не считала лишнее. +- **Зачем:** Дом настроек есть, а конвейер их не читает: выключенный уровень всё равно уходит платной модели, и настройка ничего не экономит. +- **Теги:** goal:user-settings + +Двигает пункты 1 и 4 «Завершения» цели: выключенный уровень не запрашивается у +языковой модели вовсе, а запись обрабатывается по той настройке, которая +действовала на приёме. + +Берётся после `settings-screen` (настройки негде хранить) и после +`llm-insights-adapter` (нечего выключать). + +Настройка снимается **на приёме** и едет с задачей: правка настроек посреди +обработки не меняет того, что уже считается, и не оставляет запись наполовину +обработанной. + +## Затрагивает + +- колонка снимка настроек у задачи и её шаг схемы; +- заведение задачи обоими входами: бот и приём по HTTP; +- шаг конвейера, обращающийся к языковой модели, — выбор уровней перед вызовом; +- `internal/contract` — интерфейс расчёта выводов: состав запрошенных уровней; +- `docs/database.md` — колонка снимка и её значение по умолчанию. + +## Критерии приёмки + +- Выключенный уровень не запрашивается у модели ни одним обращением. Оракул — + тест с подставным адаптером: у пользователя выключены пересказ и темы, адаптер + получает запрос без них и не вызывается второй раз. +- Запись обрабатывается по настройке, действовавшей на приёме. Оракул — тест: + завести задачу, сменить настройки владельца, прогнать шаг — считается прежний + состав уровней. +- У пользователя без настроек берутся значения по умолчанию, а задача не падает. + Оракул — тест на приёме от пользователя, у которого строки настроек нет. + +## Рамки + +Экран настроек и таблицу заводит `settings-screen` — здесь только чтение +настройки конвейером. Уже обработанные записи задним числом не пересчитываются. diff --git a/tasks/items/settings-screen.md b/tasks/items/settings-screen.md index 98eec62..f4e57be 100644 --- a/tasks/items/settings-screen.md +++ b/tasks/items/settings-screen.md @@ -1,7 +1,7 @@ # ✨ Сделать экран настроек и хранить настройки по пользователю - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Дом настроек нужен раньше уровней текста и раньше выбора канала уведомлений. - **Зачем:** Настроек у пользователя нет вовсе: уровни текста и канал уведомлений задаются общим конфигом сервиса. - **Теги:** goal:user-settings @@ -17,7 +17,8 @@ - таблица настроек пользователя: переключатели уровней текста, канал и адрес уведомлений, и её миграция; - эндпоинты чтения и записи своих настроек; -- новый экран приложения; +- новый экран приложения, и на нём же место выпуска и отзыва личного токена — + сам токен заводит `api-tokens`, экрана у него нет; - значения по умолчанию для пользователя, у которого настроек ещё нет; - `docs/database.md` — представление данных и значения по умолчанию. diff --git a/tasks/items/spa-skeleton.md b/tasks/items/spa-skeleton.md index d08e240..ca37a66 100644 --- a/tasks/items/spa-skeleton.md +++ b/tasks/items/spa-skeleton.md @@ -1,7 +1,7 @@ # ✨ Собрать каркас приложения и раздать его из бинарника - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Каркас приложения: экранов нет и собирать их нечем, а на экране стоят настройки, удаление, уровни текста и статистика. - **Зачем:** Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует. - **Теги:** goal:web-access diff --git a/tasks/items/speechkit-callback-fit.md b/tasks/items/speechkit-callback-fit.md index 218c31e..c367033 100644 --- a/tasks/items/speechkit-callback-fit.md +++ b/tasks/items/speechkit-callback-fit.md @@ -1,7 +1,7 @@ # 🔬 Уведомление SpeechKit о готовности вместо опроса - **Тип:** research -- **Категория:** Очередь +- **Категория:** Очередь — Разведка про уведомление вместо опроса меняет тот же шаг проверки и видна метриками, заведёнными выше. - **Зачем:** Шаг проверки дёргает операцию раз в 5 секунд всё время распознавания: часовая запись даёт порядка 720 обращений к платному сервису вместо одного ответа. Заметка владельца от 2026-08-12. Сегодня готовность узнаётся только опросом: diff --git a/tasks/items/speechkit-error-text-leak.md b/tasks/items/speechkit-error-text-leak.md index 8cb2877..e6641da 100644 --- a/tasks/items/speechkit-error-text-leak.md +++ b/tasks/items/speechkit-error-text-leak.md @@ -1,7 +1,7 @@ # 🔬 Адрес объекта в тексте отказа SpeechKit - **Тип:** research -- **Категория:** Очередь +- **Категория:** Очередь — Тот же род утечки в журнал, но вне темы входа — идёт следом за ней. - **Зачем:** Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись. Гипотеза ревью задачи `pocketbase-storage` 2026-08-12, оставшаяся без оракула: diff --git a/tasks/items/speechkit-limits.md b/tasks/items/speechkit-limits.md index cad7ae6..aaa41ca 100644 --- a/tasks/items/speechkit-limits.md +++ b/tasks/items/speechkit-limits.md @@ -1,7 +1,7 @@ # 🔬 Потолки SpeechKit по длине записи и по формату - **Тип:** research -- **Категория:** Очередь +- **Категория:** Очередь — Долгие записи начинаются с замера: без потолков ни отказ на приёме, ни резка не проектируются. - **Зачем:** Потолок длины записи и перечень принимаемых форматов неизвестны, а цель про долгие записи без них не начинается. - **Теги:** goal:long-recordings diff --git a/tasks/items/stalled-pipeline-metric.md b/tasks/items/stalled-pipeline-metric.md new file mode 100644 index 0000000..c0b47ae --- /dev/null +++ b/tasks/items/stalled-pipeline-metric.md @@ -0,0 +1,37 @@ +# ✨ Показывать метрикой задачу, застрявшую в состоянии + +- **Тип:** feature +- **Категория:** Очередь — Признак вставшего конвейера — вторая половина той же работы. +- **Зачем:** Вставший конвейер неотличим от простоя: возраст задачи в состоянии не считается, и очередь без движения выглядит как отсутствие работы. +- **Теги:** goal:service-observability + +Двигает пункт 1 «Завершения» цели: по метрикам видно, что конвейер встал, и это +отличимо от «работы нет». + +Сегодня метрики считают события — принято, распознано, отказано, — а состояние +очереди не считает ничто. Пустая очередь и очередь, где десять задач висят третий +час, дают одинаковый нулевой прирост счётчиков. + +## Затрагивает + +- `internal/metrics` — метрика числа задач по состояниям и возраста самой старой + из них; +- таблица задач: колонка состояния, по которой считаются числа, и отметка + времени перехода, по которой считается возраст самой старой; +- `docs/architecture.md`, раздел эксплуатации — что означает каждое число; +- `docs/database.md` — частота съёма чисел. + +## Критерии приёмки + +- Число задач в каждом состоянии видно на `/metrics`. Оракул — тест: завести три + задачи в разных состояниях и снять метрики. +- Возраст самой старой задачи в состоянии виден числом и растёт, пока задача не + сменила состояние. Оракул — тест на задаче с заведомо старой отметкой времени. +- Съём чисел не мешает работе воркеров. Оракул — чтение кода: запрос идёт по + индексу состояния и не берёт захват. + +## Рамки + +Правило оповещения на этой метрике заводит задача `owner-alerting` — здесь +только число. Порог «сколько считается застреванием» здесь не решается: он +зависит от замеров цели `long-recordings`. diff --git a/tasks/items/storage-layer-nits.md b/tasks/items/storage-layer-nits.md index abd77a0..4bb20b3 100644 --- a/tasks/items/storage-layer-nits.md +++ b/tasks/items/storage-layer-nits.md @@ -1,7 +1,7 @@ # 🧹 Разобрать мелочи слоя хранилища - **Тип:** chore -- **Категория:** Очередь +- **Категория:** Очередь — Мелочи слоя хранилища разбираются, пока слой в руках. - **Зачем:** Три мелочи ниже потолка триажа: цикл воркера пишет потерю захвата уровнем ERROR и считает её отказом, тип ошибки заведён там, где конвенция просит sentinel, а FileName несёт два разных смысла. Все три пришли ревью задачи `pocketbase-storage` 2026-08-12 и остались ниже diff --git a/tasks/items/telegram-account-link.md b/tasks/items/telegram-account-link.md index 92d03de..4771322 100644 --- a/tasks/items/telegram-account-link.md +++ b/tasks/items/telegram-account-link.md @@ -1,7 +1,7 @@ # ✨ Сопоставить пользователя Telegram с учётной записью - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Связывать чат с учётной записью не с чем, пока у записи нет владельца. - **Зачем:** Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны. - **Теги:** goal:multi-user diff --git a/tasks/items/text-insights.md b/tasks/items/text-insights.md index a088e9f..f127b50 100644 --- a/tasks/items/text-insights.md +++ b/tasks/items/text-insights.md @@ -1,7 +1,7 @@ # 🎯 Приложение показывает, о чём запись, не читая её целиком - **Тип:** goal -- **Секция:** Направления +- **Секция:** Запланировано — Очередь у цели появилась: три задачи подряд после настроек, и прибавка видна даже в боте. - **Зачем:** Расшифровка часового разговора — это стена текста: найти в списке нужную запись и вспомнить, о чём она, сегодня нечем. - **Теги:** decomposed diff --git a/tasks/items/upload-and-status-screen.md b/tasks/items/upload-and-status-screen.md index 920ea99..cd9f2e9 100644 --- a/tasks/items/upload-and-status-screen.md +++ b/tasks/items/upload-and-status-screen.md @@ -1,7 +1,7 @@ # ✨ Сделать экран загрузки записи и её состояния - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Первое, ради чего приложение открывают; требует каркаса и контракта, оба выше. - **Зачем:** Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит. - **Теги:** goal:web-access @@ -28,9 +28,10 @@ тест: после ответа `done` новых запросов к API нет. - Отказ задачи показывается человекочитаемым текстом, а не кодом и не сырой ошибкой. Оракул — тест на ответе с состоянием `failed`. -- Файл больше предела отклоняется на экране до отправки, с названным числом. - Оракул — тест на файле сверх предела: запроса к API нет, на экране предел - числом. +- Файл больше предела отклоняется на экране до отправки, с названным числом, и + предел экран берёт у сервера, а не держит своей константой. Оракул — тест на + файле сверх предела: запроса на загрузку нет, на экране предел числом, и + смена предела на стороне сервера меняет это число. ## Рамки diff --git a/tasks/items/upload-progress.md b/tasks/items/upload-progress.md index e7522a5..e1d463f 100644 --- a/tasks/items/upload-progress.md +++ b/tasks/items/upload-progress.md @@ -1,7 +1,7 @@ # ✨ Показывать ход загрузки записи на экране - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Ход загрузки — доработка того же экрана. - **Зачем:** Гигабайтный файл уходит на сервер молча: до ответа сервера экран не отличает идущую загрузку от зависшей. - **Теги:** goal:upload-reliability diff --git a/tasks/items/upload-reliability.md b/tasks/items/upload-reliability.md index 9a808d0..2dfc579 100644 --- a/tasks/items/upload-reliability.md +++ b/tasks/items/upload-reliability.md @@ -1,7 +1,7 @@ # 🎯 Загрузка большого файла доходит до сервиса и не повторяется впустую - **Тип:** goal -- **Секция:** Запланировано +- **Секция:** Запланировано — Дедупликация и пачка файлов доводятся на готовом экране загрузки и экономят деньги с первого дня приложения. - **Зачем:** Приём рассчитан на голосовое в пару мегабайт: обрыв на середине гигабайтного файла начинает загрузку заново, а один и тот же файл распознаётся повторно за наши деньги. - **Теги:** decomposed diff --git a/tasks/items/usage-accounting.md b/tasks/items/usage-accounting.md index 83666e6..ba29cd3 100644 --- a/tasks/items/usage-accounting.md +++ b/tasks/items/usage-accounting.md @@ -1,7 +1,7 @@ # ✨ Считать объём, минуты и расход по каждому пользователю - **Тип:** feature -- **Категория:** Очередь +- **Категория:** Очередь — Учёт по пользователям требует владельца записи и метрик расхода — оба выше. - **Зачем:** Ни объём, ни длительность, ни обращения к платным сервисам никуда не записываются: восстановить расход задним числом не из чего. - **Теги:** goal:usage-stats diff --git a/tasks/items/usage-stats.md b/tasks/items/usage-stats.md index 859a8b5..514c616 100644 --- a/tasks/items/usage-stats.md +++ b/tasks/items/usage-stats.md @@ -1,7 +1,7 @@ # 🎯 Владелец видит, кто сколько загрузил и во что это обошлось - **Тип:** goal -- **Секция:** Сопровождение +- **Секция:** Сопровождение — Учёт по пользователям требует владельца записи и метрик расхода — оба появляются раньше. - **Зачем:** Распознавание и языковая модель оплачиваются по факту, а счёт приходит одной суммой: кто её набрал, из сервиса не выясняется. - **Теги:** decomposed diff --git a/tasks/items/user-settings.md b/tasks/items/user-settings.md index b27217b..58e8e29 100644 --- a/tasks/items/user-settings.md +++ b/tasks/items/user-settings.md @@ -1,7 +1,7 @@ # 🎯 Пользователь настраивает, что сервис делает с его записями - **Тип:** goal -- **Секция:** Запланировано +- **Секция:** Запланировано — Дом настроек нужен раньше уровней текста и раньше выбора канала уведомлений. - **Зачем:** Уровни текста считает платная модель, а уведомления приходят одним общим способом: отказаться от лишнего и выбрать свой канал пользователю нечем. - **Теги:** decomposed diff --git a/tasks/items/video-audio-track-intake.md b/tasks/items/video-audio-track-intake.md new file mode 100644 index 0000000..ab78fab --- /dev/null +++ b/tasks/items/video-audio-track-intake.md @@ -0,0 +1,42 @@ +# ✨ Принимать видео и брать из него звуковую дорожку + +- **Тип:** feature +- **Категория:** Очередь — Приём видео и отказ неизвестному формату ставятся на замеренный перечень. +- **Зачем:** Запись семейного архива приходит видеофайлом, а приём смотрит на аудио: человеку приходится доставать дорожку самому. +- **Теги:** goal:any-audio-source + +Двигает пункты 2 и 3 «Завершения» цели: видеофайл принимается и из него берётся +звуковая дорожка, а формат, который принять нельзя, отклоняется на приёме +понятным текстом, а не отказом на конвертации через минуту. + +Берётся после `audio-format-coverage-measure`: чем отклонять неизвестное, пока +неизвестно, что конвейер берёт. + +## Затрагивает + +- приём обоими входами: перечень допустимых типов содержимого и расширений; +- адаптер метаданных — распознавание дорожек в контейнере; +- команда конвертера: выбор звуковой дорожки из видео; +- метка формата в метрике — перечень известных форматов расширяется + ([ADR](../../docs/adr/ADR-2026-08-11-known-format-label.md)); +- `docs/passport.md` — граница «что принимаем» сдвигается на видео; +- `openspec/specs/intake/` — требование о принимаемых форматах. + +## Критерии приёмки + +- Видеофайл с одной звуковой дорожкой доходит до текста. Оракул — тест приёма и + конвертации на своём коротком видео из временного каталога. +- Видео без звуковой дорожки отклоняется на приёме понятным текстом и задачи не + заводит. Оракул — тест: файл без звука, ответ с причиной, в очереди пусто. +- Формат вне замеренного перечня отклоняется на приёме, а не висит в конвейере. + Оракул — тест на файле неизвестного типа: отказ приходит сразу. +- Расширение в журнале и метка в метрике остаются в перечне известных форматов. + Оракул — тест: на видео с новым контейнером в журнал уходит расширение из + перечня, а не сырое расширение файла, и метка метрики берётся из того же + перечня. + +## Рамки + +Видео с несколькими звуковыми дорожками берём первую, выбор дорожки человеком за +задачей не стоит. Потолок длины остаётся тем, что назначит цель +`long-recordings`. diff --git a/tasks/items/web-access.md b/tasks/items/web-access.md index fac09d8..4cfea4e 100644 --- a/tasks/items/web-access.md +++ b/tasks/items/web-access.md @@ -1,7 +1,7 @@ # 🎯 Записи загружаются и читаются в приложении, которое ставится на телефон - **Тип:** goal -- **Секция:** Запланировано +- **Секция:** Запланировано — Экраны приложения нужны раньше настроек, удаления, уровней текста и статистики: всем им негде показаться. - **Зачем:** Сегодня записи принимает только бот и голый HTTP API без интерфейса: отдать сервис человеку, у которого нет Telegram, нечем. - **Теги:** decomposed