From b4b19db6e4dacd5f35d567e2a4e0c5b2dd9803d4 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 15 Aug 2026 07:46:16 +0300 Subject: [PATCH] =?UTF-8?q?tasks:=20=D0=B7=D0=B0=D0=B2=D0=B5=D0=B4=D1=91?= =?UTF-8?q?=D0=BD=20=D1=83=D1=80=D0=BE=D0=B6=D0=B0=D0=B9=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D1=8C=D1=8E=20remove-telegram-intake?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - шесть записей по кластерам причин: журнал под внешним значением, нулевой код ответа в журнале, затирание вложения бедным ответом, открытый анониму адрес подтверждения почты, рубеж расшифровки без работы, пределы длительности - находка про код 500 у отказа приёма дописана в json-api-for-spa: там живёт единая точка отображения доменной ошибки - telegram-account-link и bot-api-only-through-bot-client оставлены с оговоркой, что предмета у них нет до возвращения входа --- docs/review.md | 2 +- tasks/BACKLOG.md | 6 ++ .../items/bot-api-only-through-bot-client.md | 7 ++ tasks/items/close-verification-endpoint.md | 52 +++++++++++++++ tasks/items/duration-bounds-from-metadata.md | 49 ++++++++++++++ .../journal-fields-bounded-by-service.md | 66 +++++++++++++++++++ tasks/items/json-api-for-spa.md | 14 +++- ...ayload-not-overwritten-by-poorer-answer.md | 57 ++++++++++++++++ tasks/items/response-code-in-journal.md | 50 ++++++++++++++ tasks/items/telegram-account-link.md | 8 +++ tasks/items/transcribed-stage-without-work.md | 56 ++++++++++++++++ 11 files changed, 364 insertions(+), 3 deletions(-) create mode 100644 tasks/items/close-verification-endpoint.md create mode 100644 tasks/items/duration-bounds-from-metadata.md create mode 100644 tasks/items/journal-fields-bounded-by-service.md create mode 100644 tasks/items/payload-not-overwritten-by-poorer-answer.md create mode 100644 tasks/items/response-code-in-journal.md create mode 100644 tasks/items/transcribed-stage-without-work.md diff --git a/docs/review.md b/docs/review.md index 86a9820..e205436 100644 --- a/docs/review.md +++ b/docs/review.md @@ -306,7 +306,7 @@ API и имя не откатываются обратной правкой по - **Что меняем:** правило «пустое не кладётся поверх сохранённого» записано нормой в спеку `storage` и держится **хранилищем**, а не шагом: шагов, кладущих текст, больше одного, и правило у одного из них у остальных читалось бы как - снятое. Дефект пред-существующий, чинился решением владельца от 2026-08-14 в + снятое. Дефект существовал до той правки, чинился решением владельца от 2026-08-14 в задаче, которая его нашла ## 2026-08-15 — пустая расшифровка перестала быть заметной вместе с убранным входом [пойман ревью] diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index d9bdef5..8c0daec 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -95,4 +95,10 @@ - [🔬 Уведомление SpeechKit о готовности вместо опроса](items/speechkit-callback-fit.md) — Шаг проверки дёргает операцию раз в 5 секунд всё время распознавания: часовая запись даёт порядка 720 обращений к платному сервису вместо одного ответа. - [✨ Считать объём, минуты и расход по каждому пользователю](items/usage-accounting.md) — Ни объём, ни длительность, ни обращения к платным сервисам никуда не записываются: восстановить расход задним числом не из чего. - [✨ Сделать страницу статистики для владельца](items/admin-stats-screen.md) — Собранный учёт читается только запросом к базе руками: ни страницы, ни признака владельца в приложении нет. +- [🐞 Не давать отправителю задавать, что уедет в журнал](items/journal-fields-bounded-by-service.md) — Путь запроса уезжает в журнал целиком: аноним пишет туда до мегабайта своего текста одной строкой, а хвост имени файла приезжает внутри чужого текста ошибки вместо своего поля. +- [🐞 Писать в журнал код ответа отвергнутого запроса](items/response-code-in-journal.md) — У всякого запроса, отвергнутого роутером, в журнале стоит нулевой код: всплеск отказов доступа неотличим от обычного трафика. +- [🐞 Не затирать сохранённое вложение более бедным ответом](items/payload-not-overwritten-by-poorer-answer.md) — Пустой ответ распознавателя сохранённое больше не стирает, а более короткий — стирает: вложение заменяется, и пересчитать архив становится нечем. +- [🐞 Закрыть анониму адрес подтверждения почты](items/close-verification-endpoint.md) — Адрес подтверждения почты хранилища открыт без сессии: зная адрес вошедшего, аноним вызывает у сервиса отправку письма — сегодня безвредно, потому что почтового отправителя нет. +- [🔬 Судьба рубежа расшифровки, у которого не осталось работы](items/transcribed-stage-without-work.md) — Шаг завершения после убранной доставки только двигает колонку, а рубеж при этом подпадает под сторож застревания: готовая расшифровка платит отдельный захват и час сторожа. +- [🔬 Пределы длительности, приходящей из метаданных](items/duration-bounds-from-metadata.md) — Длительность приходит от ffprobe числом и не проверяется ничем: приведение к целому переполняется, а испорченная гистограмма чинится только перезапуском. - [🔬 Квота по общему размеру загруженного на пользователя](items/per-user-size-quota.md) — Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас. diff --git a/tasks/items/bot-api-only-through-bot-client.md b/tasks/items/bot-api-only-through-bot-client.md index 8bb7123..f5664d6 100644 --- a/tasks/items/bot-api-only-through-bot-client.md +++ b/tasks/items/bot-api-only-through-bot-client.md @@ -21,6 +21,13 @@ Ступень по лестнице механизации — четвёртая: свойство о структуре, а не о вызове, и выражается тест-сканером в `internal/archrules`. + +**Предмета у задачи сейчас нет.** Клиент бота удалён вместе со входом задачей +`remove-telegram-intake` 2026-08-15 — временно, решение записано +[ADR-2026-08-15-telegram-intake-removed-temporarily](../../docs/adr/ADR-2026-08-15-telegram-intake-removed-temporarily.md). +Запись остаётся гигиеной **возврата**: класс стоил проекту одного дефекта, и +запрет надо ставить вместе с новым клиентом, а не после него. + ## Затрагивает - `internal/archrules` — новое правило-сканер и его перечень предметов; diff --git a/tasks/items/close-verification-endpoint.md b/tasks/items/close-verification-endpoint.md new file mode 100644 index 0000000..73abcb2 --- /dev/null +++ b/tasks/items/close-verification-endpoint.md @@ -0,0 +1,52 @@ +# 🐞 Закрыть анониму адрес подтверждения почты + +- **Тип:** fix +- **Категория:** Очередь — Заведена урожаем ревью и встала в конец машинально: место в плане стройки назначает человек. +- **Зачем:** Адрес подтверждения почты хранилища открыт без сессии: зная адрес вошедшего, аноним вызывает у сервиса отправку письма — сегодня безвредно, потому что почтового отправителя нет. +- **Теги:** review-2026-08-15 + +Поверхность коллекции пользователей закрывается тем же способом, что и остальные +её адреса. + +Шаг схемы входа через провайдера закрыл заведение записи, вход паролем и +одноразовый код — но не запрос подтверждения почты. Проверено прогоном по +собранному роутеру: запрос без сессии отвечает `204`. + +Сервис своей проверки почты не делает вовсе: учётные записи заводит и проверяет +внешний провайдер, а адрес приезжает вместе с входом. То есть адрес не просто +открыт — он не нужен. + +Сегодня безвреден: почтовый сервер не настроен, письмо уходить некуда. Каналом +досаждения он становится вместе с задачей `email-notification`, которая почту как +раз заводит, — и закрыть его дешевле до неё, чем после. + +## Воспроизведение + +Собрать роутер сервиса и послать `POST /api/collections/users/request-verification` +без сессии, назвав адрес почты вошедшего. Ответ — `204`: сервис принял запрос от +неузнанного и завёл отправку письма. Ожидается отказ, как у прочих адресов +коллекции пользователей. + +Письма при этом не уходит, и потому вреда сегодня нет: почтовый сервер не +настроен. Проверка судит **код ответа**, а не доставку. + +## Затрагивает + +- шаг схемы, закрывающий адреса коллекции пользователей: заводится новый, прежний + не переписывается; +- `docs/security.md`, раздел о поверхности хранилища — перечень закрытых адресов. + +## Критерии приёмки + +- Запрос подтверждения почты без сессии отвергается. Оракул: проверка по + собранному роутеру — ответ не `204`, письма не заводится. +- Прочие способы открыть сессию остались закрытыми. Оракул: существующие + проверки поверхности хранилища зелёные, состав их не сокращён. +- Перечень закрытых адресов в модели угроз назван заново. Оракул: строка в + `docs/security.md`. + +## Рамки + +Новый шаг схемы — применённые не переписываются. Найдено прогоном ревью задачи +`remove-telegram-intake` 2026-08-15, враждебный проход, секция свойств без +построенного пути. diff --git a/tasks/items/duration-bounds-from-metadata.md b/tasks/items/duration-bounds-from-metadata.md new file mode 100644 index 0000000..ba13add --- /dev/null +++ b/tasks/items/duration-bounds-from-metadata.md @@ -0,0 +1,49 @@ +# 🔬 Пределы длительности, приходящей из метаданных + +- **Тип:** research +- **Категория:** Очередь — Заведена урожаем ревью и встала в конец машинально: место в плане стройки назначает человек. +- **Зачем:** Длительность приходит от ffprobe числом и не проверяется ничем: приведение к целому переполняется, а испорченная гистограмма чинится только перезапуском. +- **Теги:** review-2026-08-15 + +Длительность записи сервис берёт у источника метаданных и не проверяет ни на +знак, ни на потолок. Дальше сервис приводит её к целому, умножает на тысячу и +кладёт колонкой записи, а заодно отправляет в гистограмму длительностей. + +Число вне диапазона целого при приведении даёт наименьшее возможное значение — +отрицательное. Оно портит сумму семейства метрик необратимо: ряд не чинится +ничем, кроме перезапуска процесса, а страница метрик открыта без проверки +отправителя. + +Потолок записи у проекта объявлен — шесть часов, — и из него выведен предел +размера файла. Длительность такого предела не имеет вовсе. + +## Вопрос + +Может ли `ffprobe` вернуть длительность вне диапазона целого на входе, который +собрал отправитель, — и какой предел ставить на границе адаптера? + +Что для ответа нужно узнать: + +- какое поле и в какой форме `ffprobe` берёт за длительность у контейнеров, + которые сервис принимает: доверяет ли объявленному в контейнере числу или + считает по потоку; +- есть ли контейнер, где число объявлено явно и потому подделывается: Matroska + либо Ogg с подделанной меткой позиции — первые кандидаты; +- что делает приём, если длительность отрицательна либо больше объявленного + потолка: отвергать запись, обрезать значение или принимать, назвав в журнале. + +## Куда ляжет ответ + +Наблюдение о поведении `ffprobe` — в `docs/research/`, своей записью с командой и +условиями замера: разведки о `ffmpeg` у проекта пока нет ни одной, а открытые +вопросы про долгие записи и видео стоят в +[architecture.md](../../docs/architecture.md). Выбранный предел — задачей +следующим прогоном. + +## Рамки + +Прогон на реальных ключах Yandex запрещён и здесь не нужен: разведка меряет +`ffprobe`, а не распознавание. Найдено прогоном ревью задачи +`remove-telegram-intake` 2026-08-15, враждебный проход, секция свойств без +построенного пути — путь не достроен именно потому, что нужен специально +собранный контейнер. diff --git a/tasks/items/journal-fields-bounded-by-service.md b/tasks/items/journal-fields-bounded-by-service.md new file mode 100644 index 0000000..7d7a49c --- /dev/null +++ b/tasks/items/journal-fields-bounded-by-service.md @@ -0,0 +1,66 @@ +# 🐞 Не давать отправителю задавать, что уедет в журнал + +- **Тип:** fix +- **Категория:** Очередь — Заведена урожаем ревью и встала в конец машинально: место в плане стройки назначает человек. +- **Зачем:** Путь запроса уезжает в журнал целиком: аноним пишет туда до мегабайта своего текста одной строкой, а хвост имени файла приезжает внутри чужого текста ошибки вместо своего поля. +- **Теги:** review-2026-08-15 + +Длину и содержимое строки журнала задаёт тот, кто прислал запрос, а не сервис. + +Мест два, причина одна. Слой журнала пишет путь запроса полем `http.route` +целиком: приведение режет только последний сегмент и только у ссылок на файлы. +Отказ подготовки рабочей копии пересказывает отказ библиотеки целиком, а тот +несёт полное имя временного файла — вместе с расширением, взятым из имени +отправителя. + +Журнал в этом проекте — место, где наблюдаются инварианты: по нему видно +недоставку, остановку конвейера и молчаливый выход из шага. Вытесненный чужим +текстом, он перестаёт отвечать на вопрос, ради которого заведён. + +Класс проект уже назвал недопустимым в приведении отказа провайдера входа +([conventions/logging.md](../../docs/conventions/logging.md)): «без приведения +аноним пишет в журнал что угодно и сколько угодно». Здесь тот же канал шире и +дешевле — сессия для него не нужна. + +## Воспроизведение + +**Путь запроса.** Поднять сервис и послать запрос, чей путь длиной 900 000 байт: + +``` +python3 -c 'socket … GET /<900000×"A"> HTTP/1.1' +``` + +Ответ — `404`, журнал вырастает с 1048 до 901 194 байт одной строкой. Потолок +замерен: 1 048 000 байт ещё проходят, 1 100 000 отвергаются кодом `431`. +Внедрения строк при этом нет — библиотека журнала квотирует значение. + +**Хвост имени.** Принять запись, чьё имя несёт 280 байт после последней точки. +Отказ подготовки рабочей копии приезжает в журнал уровня `ERROR` строкой вида +`error="failed to create work file: open /tmp/transcriber-<число>.<280 байт>: +file name too long"`, и поля `file_ext` в ней нет вовсе. + +## Затрагивает + +- слой журнала запроса в `main.go` и приведение пути `journalRoute`; +- отказ подготовки рабочей копии в `internal/service`, шаг приёма записи; +- `docs/conventions/logging.md` — правило о длине поля журнала; +- умолчание `MaxHeaderBytes` у сервера: сегодня оно единственное неназванное + рядом с названным сроком чтения. + +## Критерии приёмки + +- Путь запроса в журнале не длиннее объявленного предела, и усечение видно в + самой строке. Оракул: тот же запрос с путём в 900 000 байт — прирост журнала + не превышает предела, а маршрут по первым байтам по-прежнему различим. +- Отказ подготовки рабочей копии не пересказывает отказ библиотеки: расширение + идёт своим полем. Оракул: проверка приёма с длинным хвостом имени — строки с + этим хвостом в журнале нет, поле `file_ext` есть. +- Правило записано конвенцией: длину поля журнала не задаёт вызывающий. Оракул: + строка в `docs/conventions/logging.md` и ссылка на неё из обеих правок. + +## Рамки + +Приведение пути не должно делать маршрут неразличимым: по журналу отличают +`/api/audio` от `/api/status/:id`. Найдено прогоном ревью задачи +`remove-telegram-intake` 2026-08-15, враждебный проход; дефект существовал до той +правки, и владелец решил вынести его задачей. diff --git a/tasks/items/json-api-for-spa.md b/tasks/items/json-api-for-spa.md index d261acb..9b5f24c 100644 --- a/tasks/items/json-api-for-spa.md +++ b/tasks/items/json-api-for-spa.md @@ -12,6 +12,14 @@ включая негодный файл. Экран, построенный на таком контракте, показывает «не найдено» при упавшей базе. +Отдельная ветка того же класса найдена ревью задачи `remove-telegram-intake` +2026-08-15, проход сверки требований: приём отвергает пустого владельца своей +ошибкой, а транспорт переводит **любую** ошибку службы в `500`. Спека приёма при +этом держит эту проверку ради понятного отказа — «отвечает отправителю понятным +отказом до того, как запись попадёт в память». Сегодня путь недостижим: слой +предъявления отвергает такого вызывающего раньше. Он становится достижимым вместе +с личными токенами и экранами приложения, то есть ровно здесь. + ## Затрагивает - `POST /api/audio` и `GET /api/status/:id` — коды ответа и форма ошибки; @@ -26,8 +34,10 @@ - Код ответа отвечает причине отказа, а не месту, где он случился: сбой базы при чтении даёт `500`, а не `404`, негодный файл — `400` с человекочитаемым - текстом, а не `500`. Оракул — два теста: репозиторий, возвращающий ошибку - драйвера, и файл, который отвергает разбор метаданных. + текстом, а не `500`, а отказ приёма по пустому владельцу — `403`, как и отказ + слоя предъявления. Оракул — три теста: репозиторий, возвращающий ошибку + драйвера; файл, который отвергает разбор метаданных; вызов приёма без учётной + записи. - Тело ошибки одной формы на всех эндпоинтах, не содержит сырого `err.Error()` и опечатки `transcibe`. Оракул — тест на четырёх ветвях отказа (форма совпадает, текста внутренней ошибки в теле нет) плюс пустой `grep -rn 'transcibe' diff --git a/tasks/items/payload-not-overwritten-by-poorer-answer.md b/tasks/items/payload-not-overwritten-by-poorer-answer.md new file mode 100644 index 0000000..67f66ca --- /dev/null +++ b/tasks/items/payload-not-overwritten-by-poorer-answer.md @@ -0,0 +1,57 @@ +# 🐞 Не затирать сохранённое вложение более бедным ответом + +- **Тип:** fix +- **Категория:** Очередь — Заведена урожаем ревью и встала в конец машинально: место в плане стройки назначает человек. +- **Зачем:** Пустой ответ распознавателя сохранённое больше не стирает, а более короткий — стирает: вложение заменяется, и пересчитать архив становится нечем. +- **Теги:** review-2026-08-15 + +Сохранённый ответ провайдера заменяется только тем, что не беднее прежнего. + +Задача `remove-telegram-intake` закрыла половину этого класса: текст и структура +реплик пустым больше не затираются, и норма записана требованием спеки хранилища +«Пустой результат не кладётся поверх сохранённого». Сырой ответ провайдера +защищён только от **пустого** — условием на непустую длину; ответ не пустой, но +короче прежнего, проходит это условие и заменяет сохранённое. + +Цена именно у вложения самая высокая. Оно единственное, из чего архив +пересчитывается без повторной оплаты: решение хранить его дословно записано +[ADR-2026-08-14-provider-payload-stored-verbatim](../../docs/adr/ADR-2026-08-14-provider-payload-stored-verbatim.md), +и обещание там прямое — «когда мы научимся размечать говорящих, архив +пересчитается из сохранённого без единого рубля». + +## Воспроизведение + +Повторный опрос одной операции наступает штатно: держатель захвата умер, +сохранение рубежа отказало, человек снял признак остановки в панели. + +Довести запись до сохранённого ответа, вернуть её на рубеж отправленной и +опросить снова, отдав со стороны провайдера ответ короче прежнего. Вложение +заменяется на короткое; прежнего не остаётся нигде. Оракул того же класса для +пустого ответа уже написан — `TestEmptySecondAnswerKeepsArchivedText`, — и этот +пишется по его образцу. + +## Затрагивает + +- коллекция `recognitions`, колонка `payload` — сохранённый ответ провайдера вложением; +- спека `storage`, требование «Пустой результат не кладётся поверх сохранённого» + — норма расширяется с пустого на «беднее прежнего»; +- определение «беднее»: длина в байтах либо число реплик разбора — выбирается + задачей. + +## Критерии приёмки + +- Более короткий ответ сохранённое вложение не заменяет. Оракул: проверка по + образцу `TestEmptySecondAnswerKeepsArchivedText` — второй опрос отдаёт ответ + короче, сохранённое вложение остаётся прежним, и проверка убеждается, что до + второго обращения дело дошло. +- Более полный ответ сохранённое заменяет. Оракул: та же проверка наоборот — + второй ответ длиннее, вложение обновилось. +- Норма записана требованием спеки, а не только кодом. Оракул: правленое + требование `storage` и ссылка на него из кода. + +## Рамки + +Найдено прогоном ревью задачи `remove-telegram-intake` 2026-08-15, враждебный +проход, секция свойств без построенного пути: пустой случай доказан падающим +тестом и закрыт, этот — выведен из того же кода и оракула не имеет. Тексту и +структуре реплик защита уже стоит, трогать их не надо. diff --git a/tasks/items/response-code-in-journal.md b/tasks/items/response-code-in-journal.md new file mode 100644 index 0000000..747f97a --- /dev/null +++ b/tasks/items/response-code-in-journal.md @@ -0,0 +1,50 @@ +# 🐞 Писать в журнал код ответа отвергнутого запроса + +- **Тип:** fix +- **Категория:** Очередь — Заведена урожаем ревью и встала в конец машинально: место в плане стройки назначает человек. +- **Зачем:** У всякого запроса, отвергнутого роутером, в журнале стоит нулевой код: всплеск отказов доступа неотличим от обычного трафика. +- **Теги:** review-2026-08-15 + +Код ответа в журнале называет исход запроса, а не ноль. + +Обработчик, завершившийся отказом роутера — `401` без сессии, `403` без учётной +записи, `404` на чужой записи, `500` на сбое, — пишет статус в ответ **после** +возврата из слоя журнала. Слой читает его раньше и получает ноль. Верное значение +попадает в журнал только у обработчиков, отвечающих самостоятельно. + +Разграничение записей по владельцу устроено так, что чужая запись отвечает тем +же, чем несуществующая. Единственное место, где виден перебор идентификаторов, — +журнал; а именно эти строки и приезжают с нулём. + +## Воспроизведение + +Поднять сервис и послать четыре запроса подряд: + +``` +/auth/login → 302 ... http.route=/auth/login http.status_code=302 +/api/audio → 404 ... http.route=/api/audio http.status_code=0 +/nope → 404 ... http.route=/nope http.status_code=0 +POST /api/audio без сессии → 401 ... http.status_code=0 +``` + +Первый обработчик отвечает сам и код пишет верно; три остальных отвергнуты +роутером и дают ноль. + +## Затрагивает + +- слой журнала запроса в `main.go`: место, где снимается код ответа; +- `docs/conventions/logging.md` — словарь полей запроса. + +## Критерии приёмки + +- Отвергнутый запрос пишет в журнал свой код. Оракул: те же четыре запроса — + `401`, `403`, `404` и `500` приезжают в поле кода, ноля в нём нет ни у одного. +- Обработчик, отвечающий самостоятельно, продолжает писать свой код. Оракул: + запрос входа по-прежнему даёт `302`. + +## Рамки + +Найдено прогоном ревью задачи `remove-telegram-intake` 2026-08-15, враждебный +проход, оракул — живой прогон. Дефект существовал до той правки: изменение этот +слой не трогало. Метрики кодов ответа у сервиса нет вовсе, и заводить её этой задачей не +надо — она про журнал. diff --git a/tasks/items/telegram-account-link.md b/tasks/items/telegram-account-link.md index a6885e6..555e904 100644 --- a/tasks/items/telegram-account-link.md +++ b/tasks/items/telegram-account-link.md @@ -10,6 +10,14 @@ Берётся после `record-ownership`: связывать не с чем, пока у записи нет владельца. + +**Предмета у задачи сейчас нет.** Вход Telegram убран задачей +`remove-telegram-intake` 2026-08-15 — временно, решение записано +[ADR-2026-08-15-telegram-intake-removed-temporarily](../../docs/adr/ADR-2026-08-15-telegram-intake-removed-temporarily.md). +Эта задача остаётся **условием возврата**: без связи чата с учётной записью вход +вернуть нельзя — записи снова пошли бы без владельца, а колонка владельца пустого +значения больше не принимает. + ## Затрагивает - связь учётной записи с чатом Telegram: таблица и её миграция; diff --git a/tasks/items/transcribed-stage-without-work.md b/tasks/items/transcribed-stage-without-work.md new file mode 100644 index 0000000..7139aad --- /dev/null +++ b/tasks/items/transcribed-stage-without-work.md @@ -0,0 +1,56 @@ +# 🔬 Судьба рубежа расшифровки, у которого не осталось работы + +- **Тип:** research +- **Категория:** Очередь — Заведена урожаем ревью и встала в конец машинально: место в плане стройки назначает человек. +- **Зачем:** Шаг завершения после убранной доставки только двигает колонку, а рубеж при этом подпадает под сторож застревания: готовая расшифровка платит отдельный захват и час сторожа. +- **Теги:** review-2026-08-15 + +Шаг завершения заводился ради ответа отправителю: он читал текст расшифровки и +отправлял его в чат, а перевод записи на конечный рубеж был хвостом этой работы. +Доставка убрана задачей `remove-telegram-intake`, и от шага осталось присваивание +колонки с сохранением. + +Цена этого остатка не нулевая. Между «расшифровка сохранена» и конечным рубежом +лежит полный круг воркера: захват с ростом числа отказов, чтение, сохранение, +строка журнала событий, счётчик. Рубеж при этом числится своей работой и потому +подпадает под предел простоя — час. Запись с готовым текстом, простоявшая этот час +без свободного воркера (очередь, ноль воркеров настройкой, окно выкладки), +останавливается признаком с причиной «застряла», и отправитель видит остановку у +записи, чья расшифровка лежит в хранилище. + +Вопрос не в том, дефект это или нет: сегодня и то и другое поведение законно. +Вопрос в том, чем рубеж остаётся дальше. + +## Вопрос + +Остаётся ли рубеж расшифровки отдельным шагом конвейера — и если да, какую работу +он держит? + +Две формы ответа видны сразу, и у каждой своя цена: + +- **шаг опроса двигает запись сразу на конечный рубеж**, а рубеж расшифровки + уходит к отжившим состояниям. Круг воркера и час сторожа исчезают; цена — + правка спеки конвейера (цепочка рубежей, требование «Рубеж записи называет + достигнутое») и значение, остающееся в перечне схемы; +- **рубеж остаётся объявленным местом** под задачу выводов из текста — заголовок, + пересказ и темы считаются шагом, который встанет ровно сюда. Тогда это надо + сказать словами в конвейере и в спеке, иначе следующий читатель прочтёт шаг как + забытый код. Цена — час сторожа над готовой записью, и его тоже надо назвать. + +Ответ зависит от того, чем станет `llm-insights-adapter`: отдельным шагом +конвейера или продолжением распознавания. Открытым вопросом это стоит в +[architecture.md](../../docs/architecture.md). + +## Куда ляжет ответ + +Решение — в `docs/adr/`, продвижением записки; сама записка не нужна, разведка +здесь выбирает форму, а не измеряет внешний мир. Изменение спеки конвейера, если +выбран первый ответ, заводит следующий прогон. + +## Рамки + +Перечень рубежей закрыт схемой, и удаление значения из него потребовало бы шага +схемы: разведка это только называет ценой, а решение принимает человек. Найдено +прогоном ревью задачи `remove-telegram-intake` 2026-08-15, архитектурный проход; +оракула у находки нет — случай выведен из кода и предела простоя, но не +воспроизведён.