diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index 89afec5..09bb680 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -53,3 +53,12 @@ - [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом. - [🧹 Переименовать образец конфига в config.example.toml](items/config-example-toml.md) — Конвенция называет config.dist.toml объявленным расхождением, но тут же пишет это имя как правило — документ противоречит сам себе, а образец расходится с конвенцией. - [🔬 Стоит ли брать OpenTelemetry вместо голого Prometheus](items/opentelemetry-fit.md) — Метрик одиннадцать штук на пять счётчиков, трассировки нет вовсе: путь одной записи по конвейеру собирается только чтением логов глазами. +- [🧹 Обновить Go до 1.26 в модуле и в образе](items/go-1-26-upgrade.md) — Модуль объявляет go 1.25.0, образ собирается на golang:1.25-alpine, а на машине разработки стоит 1.26.5: три места расходятся, и проверки идут не на той версии, что уезжает на сервер. +- [🧹 Ловить уязвимости в зависимостях шагом гейта](items/gate-dependency-vulnerabilities.md) — govulncheck находит две достижимые уязвимости в клиентах Yandex, а ни гейт, ни список «чего в гейте нет» о нём не знают: узнать о третьей будет неоткуда. +- [🧹 Сверять версию Go в образе с директивой go.mod](items/gate-go-version-sync.md) — Образ гейт не собирает, и расхождение версий прошло весь конвейер молча: task image упал бы уже на сервере. +- [🧹 Считать покрытие изменённых строк шагом гейта](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 несёт два разных смысла. +- [🔬 Уведомление SpeechKit о готовности вместо опроса](items/speechkit-callback-fit.md) — Шаг проверки дёргает операцию раз в 5 секунд всё время распознавания: часовая запись даёт порядка 720 обращений к платному сервису вместо одного ответа. +- [🔬 Адрес объекта в тексте отказа SpeechKit](items/speechkit-error-text-leak.md) — Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись. +- [🔬 Квота по общему размеру загруженного на пользователя](items/per-user-size-quota.md) — Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас. diff --git a/tasks/items/context-cancel-in-pipeline.md b/tasks/items/context-cancel-in-pipeline.md new file mode 100644 index 0000000..8752d9d --- /dev/null +++ b/tasks/items/context-cancel-in-pipeline.md @@ -0,0 +1,47 @@ +# 🧹 Прервать шаг конвейера отменой контекста + +- **Тип:** chore +- **Категория:** Очередь +- **Зачем:** Воркер читает ctx только между итерациями: остановка контейнера ждёт конца шага, а на занятом писателе один запрос к хранилищу держится до 9,5 секунды при мягком таймауте в 5. + +Долг назван в `docs/architecture.md` — раздел про capability `pipeline`, где +отмена контекста посреди шага перечислена среди неописанного. Ревью задачи +`pocketbase-storage` 2026-08-12 его уточнило, и уточнение меняет способ починки: +**передать `ctx` по всему коду недостаточно**. Хранилище не принимает контекста ни в +одном из методов записи — ни в сохранении записи, ни в транзакции, ни в чтении по +идентификатору; отменять там нечем без перехода части записи на сырые запросы, +у которых контекст есть. + +Число снято на прогоне: под внешней эксклюзивной блокировкой файла базы захват +задачи держится 9,53 секунды (таймаут занятости хранилища — 10 секунд) и +возвращает отличимый отказ, а не «работы нет». + +## Затрагивает + +- цикл воркера `internal/controller/worker`; +- сигнатуры шагов конвейера в `internal/service`; +- договор ядра с хранилищем `internal/contract` и его реализация в + `internal/adapter/repo/pocketbase`; +- таймауты мягкой и жёсткой остановки — конфиг, `[server] shutdown_timeout` и + `force_shutdown_timeout`; +- спека `pipeline`: сегодня раздел `Purpose` объявляет отмену контекста + неописанной. + +## Критерии приёмки + +- Шаг, идущий дольше мягкого таймаута, обрывается по сигналу остановки. Оракул — + тест с шагом, спящим дольше таймаута: остановка укладывается в бюджет, а не + доживает до жёсткой. +- Оборванный шаг оставляет задачу пригодной к повтору. Оракул — тот же тест: + после остановки задача не захвачена и выдаётся следующему. +- Отмена не путается с отказом: прерванный шаг не тратит попытку и не уходит в + «мертва». Оракул — тест на трёх прерываниях подряд, счётчик попыток не растёт + до предела. +- Спека `pipeline` описывает отмену, и раздел `Purpose` её больше не числит + неописанной. Оракул — `openspec validate --strict`. + +## Рамки + +Таймаут занятости хранилища этой задачей не трогаем: он свойство библиотеки, а не +наше. Мягкая остановка в 5 секунд может оказаться меньше неотменяемого минимума — +тогда число меняется, и это решение человека. diff --git a/tasks/items/data-ownership.md b/tasks/items/data-ownership.md index b5bf18e..cda086f 100644 --- a/tasks/items/data-ownership.md +++ b/tasks/items/data-ownership.md @@ -3,6 +3,7 @@ - **Тип:** goal - **Секция:** Направления - **Зачем:** Хранение бессрочное, а способа убрать запись нет ни одного: ошибочно загруженный файл и разговор, который человек не хочет держать у нас, остаются навсегда. +- **Теги:** decomposed Сервис объявлен архивом 2026-08-11, и с тем же решением у человека появляется обратное право: сказать «убери это» и убедиться, что убрано. Речь в записи diff --git a/tasks/items/gate-changed-lines-coverage.md b/tasks/items/gate-changed-lines-coverage.md new file mode 100644 index 0000000..38666fd --- /dev/null +++ b/tasks/items/gate-changed-lines-coverage.md @@ -0,0 +1,42 @@ +# 🧹 Считать покрытие изменённых строк шагом гейта + +- **Тип:** chore +- **Категория:** Очередь +- **Зачем:** Свойство «изменённое место покрыто хоть одним тестом» записано в docs/review.md, но не механизировано: за две задачи подряд непокрытые шаги ловили руками. +- **Теги:** question + +`CLAUDE.md`, раздел «Гейт», объявляет прямо: «покрытие изменённых строк не +считается ничем». Цена этого измерена дважды. В задаче +`http-handler-tests-never-green` тесты обработчика не были зелёными ни разу; в +`pocketbase-storage` два из трёх шагов конвейера переписали целиком и не +выполнили ни одним тестом — нашёл это проход ревью, а не машина. + +## Затрагивает + +- набор шагов `task gate` в `Taskfile.yml` и переменная `BASE` как база диффа; +- семантика гейта в `CLAUDE.md`, раздел «Гейт», строка про покрытие; +- `docs/review.md`, раздел настройки конвейера: чем проход `autotests` перестаёт + заниматься руками. + +## Критерии приёмки + +- Изменённая строка без покрытия роняет гейт. Оракул — прогон на дереве, где в + тронутый файл добавлена заведомо невыполняемая ветка: шаг краснеет с её + адресом. +- Изменение, не трогающее код, шаг не гоняет. Оракул — `task gate` на дереве с + правкой одной только документации: шаг сообщает о пропуске с причиной. +- Порог назван числом в `CLAUDE.md` и совпадает с тем, что проверяет шаг. Оракул + — `task gate`, шаг `docs.py check`. + +## Вопросы + +- **Какой порог и что считать изменённой строкой.** Варианты: сто процентов + изменённых строк (роняет на всякой ветке отказа, которую нечем изобразить), + порог ниже ста (число берётся из ниоткуда), только новые функции целиком. + Цена решения — частота ложных отказов гейта; пока решения нет, шаг заводить + нельзя. + +## Рамки + +Общее покрытие проекта не считаем и порога на него не ставим: он растёт от +тестов на тривиальное и не отвечает ни на один вопрос. diff --git a/tasks/items/gate-dependency-vulnerabilities.md b/tasks/items/gate-dependency-vulnerabilities.md new file mode 100644 index 0000000..b0f97a1 --- /dev/null +++ b/tasks/items/gate-dependency-vulnerabilities.md @@ -0,0 +1,35 @@ +# 🧹 Ловить уязвимости в зависимостях шагом гейта + +- **Тип:** chore +- **Категория:** Очередь +- **Зачем:** govulncheck находит две достижимые уязвимости в клиентах Yandex, а ни гейт, ни список «чего в гейте нет» о нём не знают: узнать о третьей будет неоткуда. + +Обе найдены ревью задачи `pocketbase-storage` 2026-08-12 и достижимы из кода +распознавателя: `GO-2026-6061` в `google.golang.org/grpc` (исправлено в v1.82.1) +и `GO-2026-5764` в `aws-sdk-go-v2` — `aws/protocol/eventstream` (в v1.7.8) и +`service/s3` (в v1.97.3). Ни одну из них эта задача не вносила: `git diff +origin/master -- go.mod` их версий не касался. + +## Затрагивает + +- версии `google.golang.org/grpc`, `aws-sdk-go-v2/service/s3` и + `aws-sdk-go-v2/aws/protocol/eventstream` в `go.mod`; +- набор шагов `task gate` в `Taskfile.yml`; +- семантика гейта в `CLAUDE.md` — раздел «Гейт», список «что красит безусловно» + либо «чего в гейте намеренно нет». + +## Критерии приёмки + +- Обе объявленные уязвимости из кода недостижимы. Оракул — `govulncheck ./...` + печатает «No vulnerabilities found» либо оставляет только недостижимые. +- Новая достижимая уязвимость роняет гейт. Оракул — прогон `task gate` на + дереве с намеренно откаченной версией `grpc`: шаг краснеет с именем уязвимости. +- Место шага названо в `CLAUDE.md`: либо в перечне красящих безусловно, либо в + «чего в гейте нет» с указанием, кто тогда обязан его гонять. Оракул — + `task gate`, шаг `docs.py check`. + +## Рамки + +Обновление версий трогает клиентов Yandex — прогон на реальных ключах запрещён, +подставляй `internal/adapter/recognizer/memory.go`. Решение, ронять ли гейт на +уязвимости без исправления, принимает человек. diff --git a/tasks/items/gate-go-version-sync.md b/tasks/items/gate-go-version-sync.md new file mode 100644 index 0000000..06d37f5 --- /dev/null +++ b/tasks/items/gate-go-version-sync.md @@ -0,0 +1,35 @@ +# 🧹 Сверять версию Go в образе с директивой go.mod + +- **Тип:** chore +- **Категория:** Очередь +- **Зачем:** Образ гейт не собирает, и расхождение версий прошло весь конвейер молча: task image упал бы уже на сервере. + +Случай записан журналом дефектов `docs/review.md` за 2026-08-12: `go mod tidy` +поднял директиву до 1.25, `Dockerfile` остался на `golang:1.24-alpine` с +`GOTOOLCHAIN=local`, и шесть проходов ревью вместе с восемью шагами гейта +показали зелёное. Нашлось случайным свойством прогона, а не устройством +проверок. + +Сборку образа в гейт не заводим — она дорога. Дешёвая замена: сравнение двух +строк. + +## Затрагивает + +- набор шагов `task gate` в `Taskfile.yml`; +- директива `go` в `go.mod` и сборочный слой `Dockerfile` как источники + сравнения; +- семантика гейта в `CLAUDE.md`, раздел «Гейт». + +## Критерии приёмки + +- Расхождение версий роняет гейт. Оракул — прогон `task gate` на дереве, где + версия в `Dockerfile` понижена на минор: шаг краснеет и называет оба числа. +- Совпадение гейт не роняет. Оракул — `task gate` на неизменённом дереве. +- Шаг не требует docker и работает без сети. Оракул — прогон с + `DOCKER_HOST=/dev/null`. + +## Рамки + +Сравнение строковое: `Dockerfile` называет `golang:.-alpine`, а +`go.mod` — `..`. Сверяются мажор и минор; патч сборочного +образа своей жизнью живёт законно. diff --git a/tasks/items/go-1-26-upgrade.md b/tasks/items/go-1-26-upgrade.md new file mode 100644 index 0000000..a3438a3 --- /dev/null +++ b/tasks/items/go-1-26-upgrade.md @@ -0,0 +1,30 @@ +# 🧹 Обновить Go до 1.26 в модуле и в образе + +- **Тип:** chore +- **Категория:** Очередь +- **Зачем:** Модуль объявляет go 1.25.0, образ собирается на golang:1.25-alpine, а на машине разработки стоит 1.26.5: три места расходятся, и проверки идут не на той версии, что уезжает на сервер. + +Заметка владельца от 2026-08-12. Версия 1.25 в модуле появилась не выбором, а +требованием PocketBase: `go mod tidy` поднял директиву сам, а образ подтянули +следом. То есть сегодняшнее число никем не назначено, и повторится это на +следующей же зависимости. + +## Затрагивает + +- директива `go` в `go.mod`; +- сборочный слой `Dockerfile`; +- строка стека в `CLAUDE.md` и в `README.md`. + +## Критерии приёмки + +- Модуль, образ и документы называют одну версию Go. Оракул — `grep` по + четырём местам: `go.mod`, `Dockerfile`, `CLAUDE.md`, `README.md`; все дают + одно число. +- Сборка на объявленной версии проходит. Оракул — `task image` и + `CGO_ENABLED=0 go build ./...` на чистом дереве. +- Гейт зелёный целиком. Оракул — `task gate`. + +## Рамки + +Версию выбирает человек: обновление до неподдерживаемой ещё зависимостями версии +ломает сборку молча. Выкладку не запускаем. diff --git a/tasks/items/intake-limits-measure.md b/tasks/items/intake-limits-measure.md index 4fe5229..96713ce 100644 --- a/tasks/items/intake-limits-measure.md +++ b/tasks/items/intake-limits-measure.md @@ -19,6 +19,13 @@ и заливка в Object Storage, — и сколько времени и места на диске занимает шестичасовая запись на каждом из них. +**Звено, добавленное ревью 2026-08-12:** приём пишет тело записи на диск +дважды — разбор формы записывает во временный каталог всё, что не поместилось в +память, выделенную под multipart, а следом рабочая копия переливает то же +содержимое в свой файл. Верхняя +оценка на один запрос — два объявленных потолка записи (`entity.MaxRecordSize`, +8 ГиБ). Числа нет: самая длинная проверенная запись — 9,6 МБ. + ## Куда ляжет ответ `docs/research/intake-limits.md` — числами и с командой замера по каждому звену. diff --git a/tasks/items/multi-user.md b/tasks/items/multi-user.md index 3726e48..64405b5 100644 --- a/tasks/items/multi-user.md +++ b/tasks/items/multi-user.md @@ -3,6 +3,7 @@ - **Тип:** goal - **Секция:** Запланировано - **Зачем:** У задачи нет владельца, а HTTP API открыт наружу без аутентификации: пригласить второго человека сейчас значит открыть ему чужие расшифровки. +- **Теги:** decomposed Приложение узнаёт, кто к нему пришёл, и показывает каждому только его записи. Учётные записи заводит и проверяет внешний провайдер — Authelia по OIDC; своей diff --git a/tasks/items/per-user-size-quota.md b/tasks/items/per-user-size-quota.md new file mode 100644 index 0000000..0d12cf2 --- /dev/null +++ b/tasks/items/per-user-size-quota.md @@ -0,0 +1,24 @@ +# 🔬 Квота по общему размеру загруженного на пользователя + +- **Тип:** research +- **Категория:** Очередь +- **Зачем:** Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас. + +Заметка звучит так: «Квота по размеру загруженных файлов на пользователя. В +конфиге устанавливать квоту по общему размеру, по дефолту не больше 5GB». + +**Взять в работу её нельзя, пока не решён спор с паспортом.** +`docs/passport.md`, раздел «Что целью не является», говорит прямо: «Цен, счетов +и отказов по исчерпании квоты не делаем: пользователя, потратившего слишком +много, останавливает разговор или отзыв доступа в Authelia». То же повторяет +`docs/security.md`, «Что вне модели»: квот нет и не будет. + +Владелец 2026-08-12 решил не двигать границу сейчас, а разобрать позже. Разбор +отвечает на три вещи: двигается ли граница паспорта; отказывает ли сервис +пользователю или только оповещает владельца; на чём считается объём, раз учёта +по пользователям сегодня нет вовсе (задача `usage-accounting`). + +## Рамки + +Пока раздел «Вопрос» пуст, запись числится сырьём и в работу не берётся. Правка +паспорта — решение человека: это граница домена, а не деталь. diff --git a/tasks/items/ready-notification.md b/tasks/items/ready-notification.md index b190e35..8ef220d 100644 --- a/tasks/items/ready-notification.md +++ b/tasks/items/ready-notification.md @@ -3,6 +3,7 @@ - **Тип:** goal - **Секция:** Запланировано - **Зачем:** Расшифровка занимает минуты, и всё это время человек либо смотрит на экран с опросом статуса, либо забывает вернуться. +- **Теги:** decomposed Задача уходит в работу — человек закрывает приложение и получает сообщение, когда текст готов. Пользователь Telegram это уже имеет: бот отвечает сам. diff --git a/tasks/items/records-list-screen.md b/tasks/items/records-list-screen.md index 3af3f1f..11acd25 100644 --- a/tasks/items/records-list-screen.md +++ b/tasks/items/records-list-screen.md @@ -30,6 +30,14 @@ экрана. Оракул — тест: подставной API отвечает `transcribe`, затем `done`; экран переходит к тексту сам. +**Учесть от ревью 2026-08-12:** имя файла в хранилище несёт хвост имени, данного +отправителем, — расширение берётся из него дословно, и запись +`разговор.тайное-слово` ложится на диск именем с этим хвостом. Сегодня наружу +оно не выходит: изъятие из инварианта приватности кончается журналом владельца. +Экран списка это меняет — имя попадает в ссылку на скачивание и в заголовок +ответа, то есть на страницу. Выхода за каталог хранения при этом нет, проверено +пятнадцатью враждебными именами. + ## Рамки Поиска по тексту, переименования и удаления записей не делаем. Правки текста не diff --git a/tasks/items/service-observability.md b/tasks/items/service-observability.md index e432af2..f80e0c5 100644 --- a/tasks/items/service-observability.md +++ b/tasks/items/service-observability.md @@ -3,6 +3,7 @@ - **Тип:** goal - **Секция:** Сопровождение - **Зачем:** Отказ замечает пользователь, а не владелец: оповещения нет, а путь записи по конвейеру собирается глазами по логам контейнера. +- **Теги:** decomposed Наблюдаем **мы**, а не пользователь сервиса, — поэтому цель стоит в сопровождении, а не среди возможностей приложения. Пользовательская половина diff --git a/tasks/items/speechkit-callback-fit.md b/tasks/items/speechkit-callback-fit.md new file mode 100644 index 0000000..218c31e --- /dev/null +++ b/tasks/items/speechkit-callback-fit.md @@ -0,0 +1,32 @@ +# 🔬 Уведомление SpeechKit о готовности вместо опроса + +- **Тип:** research +- **Категория:** Очередь +- **Зачем:** Шаг проверки дёргает операцию раз в 5 секунд всё время распознавания: часовая запись даёт порядка 720 обращений к платному сервису вместо одного ответа. + +Заметка владельца от 2026-08-12. Сегодня готовность узнаётся только опросом: +воркер проверки берёт задачу, спрашивает операцию и ставит задержку в 5 секунд — +и так до конца распознавания. Числа задержек стоят в `docs/database.md`, +«Настройки с числовым значением». + +## Вопрос + +Умеет ли отложенное распознавание SpeechKit сообщать о готовности само — +вызовом на наш адрес, очередью или иным способом, — и что это меняет для +сервиса: остаётся ли воркер проверки, чем закрывается пропущенное уведомление и +какой вход появляется снаружи. + +## Куда ляжет ответ + +- Наблюдение с командой и условиями — записка в `docs/research/`. +- Появляется вход снаружи — строка в `docs/security.md`, раздел «Недоверенный + вход», и в `docs/architecture.md`, «Внешние границы и форматы». +- Выбранный способ — решением в `docs/adr/`, потому что опрос он заменяет + целиком; реализация — отдельной задачей. + +## Рамки + +Прогон на реальных ключах Yandex запрещён — ответ ищется в документации сервиса +и, если понадобится замер, отдельным разрешением человека с названной ценой. +Уведомление требует адреса, доступного снаружи: сегодня он есть, но закрывать +его будет `oidc-login`, и это учитывается в ответе. diff --git a/tasks/items/speechkit-error-text-leak.md b/tasks/items/speechkit-error-text-leak.md new file mode 100644 index 0000000..8cb2877 --- /dev/null +++ b/tasks/items/speechkit-error-text-leak.md @@ -0,0 +1,33 @@ +# 🔬 Адрес объекта в тексте отказа SpeechKit + +- **Тип:** research +- **Категория:** Очередь +- **Зачем:** Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись. + +Гипотеза ревью задачи `pocketbase-storage` 2026-08-12, оставшаяся без оракула: +поднять настоящий отказ Yandex нечем, а прогон на реальных ключах запрещён. +Собственные цепочки отказов задача оборвала — чтение из хранилища и выгрузка в +Object Storage больше не называют ключ, — но текст, пришедший **чужими руками**, +идёт в журнал и в задачу как есть. + +## Вопрос + +Несёт ли текст отказа операции распознавания адрес объекта, который мы отдали +SpeechKit, и в каких случаях? Если несёт — чем его отсечь, не потеряв причину +отказа: разбором по образцу, перечнем известных кодов отказа или отказом писать +чужой текст вовсе. + +## Куда ляжет ответ + +- Наблюдение с командой и условиями — записка в `docs/research/`, рядом с + `pocketbase-defaults.md`. +- Если адрес приходит — норма в спеку `storage`, требование «Файл отдаётся + ссылкой», и строка в `docs/security.md`, раздел «Из чего строятся пути и + ключи»; сама правка — отдельной задачей. +- Если не приходит — строка в ту же записку, чтобы гипотезу не поднимали заново. + +## Рамки + +Реальные ключи Yandex и боевые данные не трогаем. Отказ придётся вызывать +дешёвым способом: битый объект, объект вне бакета, отозванный доступ — что из +этого доступно, решается по ходу. Цена прогона в деньгах называется до запуска. diff --git a/tasks/items/storage-layer-nits.md b/tasks/items/storage-layer-nits.md new file mode 100644 index 0000000..abd77a0 --- /dev/null +++ b/tasks/items/storage-layer-nits.md @@ -0,0 +1,44 @@ +# 🧹 Разобрать мелочи слоя хранилища + +- **Тип:** chore +- **Категория:** Очередь +- **Зачем:** Три мелочи ниже потолка триажа: цикл воркера пишет потерю захвата уровнем ERROR и считает её отказом, тип ошибки заведён там, где конвенция просит sentinel, а FileName несёт два разных смысла. + +Все три пришли ревью задачи `pocketbase-storage` 2026-08-12 и остались ниже +потолка отчёта — то есть реальны, но не блокировали. Собраны одной задачей, +потому что живут в одном пакете и правятся одним заходом. + +1. **Потеря захвата — не отказ шага.** Значение «задачу за время работы забрал + другой» доезжает до цикла воркера, который пишет его уровнем `ERROR` и + увеличивает счётчик отказов. `docs/conventions/logging.md` относит «задача + досталась повторно» к `WARN`, а метрика отказов после этого считает не отказы. +2. **Тип ошибки против sentinel.** `docs/conventions/errors.md` просит sentinel + там, где данных у ошибки не читает никто; у значения о потерянном захвате + вызывающий читает только сам факт. +3. **Одно поле в двух смыслах.** У местной копии `entity.File.FileName` — имя в + хранилище и последняя часть ссылки на скачивание, у внешней — ключ объекта. + Различает их только соседнее поле, а совпадают они сегодня дословно: на этом + совпадении держится верность строки `docs/architecture.md` про ключ объекта. + +## Затрагивает + +- `internal/contract/error.go` — значение о потерянном захвате; +- цикл воркера `internal/controller/worker` — уровень записи и счётчик отказов; +- `internal/entity/file.go` и его потребители в `internal/service`; +- строка про ключ объекта в `docs/architecture.md`, «Внешние границы и форматы». + +## Критерии приёмки + +- Потеря захвата не считается отказом и не пишется на уровне владельца сервиса. + Оракул — тест воркера: после прогона с потерянным захватом счётчик отказов не + вырос, а строка в журнале идёт уровнем `WARN`. +- Форма значения об отказе совпадает с конвенцией. Оракул — `golangci-lint run` + плюс чтение `docs/conventions/errors.md` рядом с кодом. +- Два смысла имени файла разведены либо названы в документе. Оракул — `grep` по + потребителям поля: каждый читает его в одном смысле, а `docs/architecture.md` + говорит про ключ объекта то же, что делает код. + +## Рамки + +Раскладку файлов на диске и имена коллекций не трогаем: формат хранения объявлен +необратимым, а шаг схемы, уехавший на сервер, не переписывается. diff --git a/tasks/items/text-insights.md b/tasks/items/text-insights.md index 55f136e..a088e9f 100644 --- a/tasks/items/text-insights.md +++ b/tasks/items/text-insights.md @@ -3,6 +3,7 @@ - **Тип:** goal - **Секция:** Направления - **Зачем:** Расшифровка часового разговора — это стена текста: найти в списке нужную запись и вспомнить, о чём она, сегодня нечем. +- **Теги:** decomposed Текст записи выдаётся уровнями: сырая расшифровка, вычитанный литературный текст, заголовок в одну строку, темы-теги и пересказ в два-три предложения. diff --git a/tasks/items/upload-reliability.md b/tasks/items/upload-reliability.md index 3315712..9a808d0 100644 --- a/tasks/items/upload-reliability.md +++ b/tasks/items/upload-reliability.md @@ -3,6 +3,7 @@ - **Тип:** goal - **Секция:** Запланировано - **Зачем:** Приём рассчитан на голосовое в пару мегабайт: обрыв на середине гигабайтного файла начинает загрузку заново, а один и тот же файл распознаётся повторно за наши деньги. +- **Теги:** decomposed Человек отдаёт диктофонную запись или видео из семейного архива с телефона, по сотовой сети, и загрузка либо доходит, либо честно говорит, что не дошла. diff --git a/tasks/items/usage-stats.md b/tasks/items/usage-stats.md index 1c6f1d6..859a8b5 100644 --- a/tasks/items/usage-stats.md +++ b/tasks/items/usage-stats.md @@ -3,6 +3,7 @@ - **Тип:** goal - **Секция:** Сопровождение - **Зачем:** Распознавание и языковая модель оплачиваются по факту, а счёт приходит одной суммой: кто её набрал, из сервиса не выясняется. +- **Теги:** decomposed Владелец открывает страницу и видит по каждому пользователю объём загруженного, длительность записей в минутах и расход на внешние сервисы. Приглашая человека, diff --git a/tasks/items/user-settings.md b/tasks/items/user-settings.md index e425f3d..b27217b 100644 --- a/tasks/items/user-settings.md +++ b/tasks/items/user-settings.md @@ -3,6 +3,7 @@ - **Тип:** goal - **Секция:** Запланировано - **Зачем:** Уровни текста считает платная модель, а уведомления приходят одним общим способом: отказаться от лишнего и выбрать свой канал пользователю нечем. +- **Теги:** decomposed У каждого своя мера: одному нужна только сырая расшифровка, другому — все пять уровней текста. Настройки принадлежат человеку, а не общему конфигу сервиса, и diff --git a/tasks/items/web-access.md b/tasks/items/web-access.md index e8f0af1..1fd03a3 100644 --- a/tasks/items/web-access.md +++ b/tasks/items/web-access.md @@ -3,6 +3,7 @@ - **Тип:** goal - **Секция:** Запланировано - **Зачем:** Сегодня записи принимает только бот и голый HTTP API без интерфейса: отдать сервис человеку, у которого нет Telegram, нечем. +- **Теги:** decomposed Приложение получает поверхность, на которой запись загружают и забирают текст, не открывая Telegram и не вызывая API руками. Ставится оно на телефон и