tasks: заведён урожай ревью remove-telegram-intake

- шесть записей по кластерам причин: журнал под внешним значением, нулевой
  код ответа в журнале, затирание вложения бедным ответом, открытый анониму
  адрес подтверждения почты, рубеж расшифровки без работы, пределы длительности
- находка про код 500 у отказа приёма дописана в json-api-for-spa: там живёт
  единая точка отображения доменной ошибки
- telegram-account-link и bot-api-only-through-bot-client оставлены с оговоркой,
  что предмета у них нет до возвращения входа
This commit is contained in:
av
2026-08-15 07:46:16 +03:00
parent 8f7c3a057a
commit b4b19db6e4
11 changed files with 364 additions and 3 deletions
+1 -1
View File
@@ -306,7 +306,7 @@ API и имя не откатываются обратной правкой по
- **Что меняем:** правило «пустое не кладётся поверх сохранённого» записано - **Что меняем:** правило «пустое не кладётся поверх сохранённого» записано
нормой в спеку `storage` и держится **хранилищем**, а не шагом: шагов, кладущих нормой в спеку `storage` и держится **хранилищем**, а не шагом: шагов, кладущих
текст, больше одного, и правило у одного из них у остальных читалось бы как текст, больше одного, и правило у одного из них у остальных читалось бы как
снятое. Дефект пред-существующий, чинился решением владельца от 2026-08-14 в снятое. Дефект существовал до той правки, чинился решением владельца от 2026-08-14 в
задаче, которая его нашла задаче, которая его нашла
## 2026-08-15 — пустая расшифровка перестала быть заметной вместе с убранным входом [пойман ревью] ## 2026-08-15 — пустая расшифровка перестала быть заметной вместе с убранным входом [пойман ревью]
+6
View File
@@ -95,4 +95,10 @@
- [🔬 Уведомление SpeechKit о готовности вместо опроса](items/speechkit-callback-fit.md) — Шаг проверки дёргает операцию раз в 5 секунд всё время распознавания: часовая запись даёт порядка 720 обращений к платному сервису вместо одного ответа. - [🔬 Уведомление SpeechKit о готовности вместо опроса](items/speechkit-callback-fit.md) — Шаг проверки дёргает операцию раз в 5 секунд всё время распознавания: часовая запись даёт порядка 720 обращений к платному сервису вместо одного ответа.
- [✨ Считать объём, минуты и расход по каждому пользователю](items/usage-accounting.md) — Ни объём, ни длительность, ни обращения к платным сервисам никуда не записываются: восстановить расход задним числом не из чего. - [✨ Считать объём, минуты и расход по каждому пользователю](items/usage-accounting.md) — Ни объём, ни длительность, ни обращения к платным сервисам никуда не записываются: восстановить расход задним числом не из чего.
- [✨ Сделать страницу статистики для владельца](items/admin-stats-screen.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 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас. - [🔬 Квота по общему размеру загруженного на пользователя](items/per-user-size-quota.md) — Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас.
@@ -21,6 +21,13 @@
Ступень по лестнице механизации — четвёртая: свойство о структуре, а не о Ступень по лестнице механизации — четвёртая: свойство о структуре, а не о
вызове, и выражается тест-сканером в `internal/archrules`. вызове, и выражается тест-сканером в `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` — новое правило-сканер и его перечень предметов; - `internal/archrules` — новое правило-сканер и его перечень предметов;
@@ -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, враждебный проход, секция свойств без
построенного пути.
@@ -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, враждебный проход, секция свойств без
построенного пути — путь не достроен именно потому, что нужен специально
собранный контейнер.
@@ -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, враждебный проход; дефект существовал до той
правки, и владелец решил вынести его задачей.
+12 -2
View File
@@ -12,6 +12,14 @@
включая негодный файл. Экран, построенный на таком контракте, показывает «не включая негодный файл. Экран, построенный на таком контракте, показывает «не
найдено» при упавшей базе. найдено» при упавшей базе.
Отдельная ветка того же класса найдена ревью задачи `remove-telegram-intake`
2026-08-15, проход сверки требований: приём отвергает пустого владельца своей
ошибкой, а транспорт переводит **любую** ошибку службы в `500`. Спека приёма при
этом держит эту проверку ради понятного отказа — «отвечает отправителю понятным
отказом до того, как запись попадёт в память». Сегодня путь недостижим: слой
предъявления отвергает такого вызывающего раньше. Он становится достижимым вместе
с личными токенами и экранами приложения, то есть ровно здесь.
## Затрагивает ## Затрагивает
- `POST /api/audio` и `GET /api/status/:id` — коды ответа и форма ошибки; - `POST /api/audio` и `GET /api/status/:id` — коды ответа и форма ошибки;
@@ -26,8 +34,10 @@
- Код ответа отвечает причине отказа, а не месту, где он случился: сбой базы при - Код ответа отвечает причине отказа, а не месту, где он случился: сбой базы при
чтении даёт `500`, а не `404`, негодный файл — `400` с человекочитаемым чтении даёт `500`, а не `404`, негодный файл — `400` с человекочитаемым
текстом, а не `500`. Оракул — два теста: репозиторий, возвращающий ошибку текстом, а не `500`, а отказ приёма по пустому владельцу — `403`, как и отказ
драйвера, и файл, который отвергает разбор метаданных. слоя предъявления. Оракул — три теста: репозиторий, возвращающий ошибку
драйвера; файл, который отвергает разбор метаданных; вызов приёма без учётной
записи.
- Тело ошибки одной формы на всех эндпоинтах, не содержит сырого `err.Error()` и - Тело ошибки одной формы на всех эндпоинтах, не содержит сырого `err.Error()` и
опечатки `transcibe`. Оракул — тест на четырёх ветвях отказа (форма совпадает, опечатки `transcibe`. Оракул — тест на четырёх ветвях отказа (форма совпадает,
текста внутренней ошибки в теле нет) плюс пустой `grep -rn 'transcibe' текста внутренней ошибки в теле нет) плюс пустой `grep -rn 'transcibe'
@@ -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, враждебный
проход, секция свойств без построенного пути: пустой случай доказан падающим
тестом и закрыт, этот — выведен из того же кода и оракула не имеет. Тексту и
структуре реплик защита уже стоит, трогать их не надо.
+50
View File
@@ -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, враждебный
проход, оракул — живой прогон. Дефект существовал до той правки: изменение этот
слой не трогало. Метрики кодов ответа у сервиса нет вовсе, и заводить её этой задачей не
надо — она про журнал.
+8
View File
@@ -10,6 +10,14 @@
Берётся после `record-ownership`: связывать не с чем, пока у записи нет Берётся после `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: таблица и её миграция; - связь учётной записи с чатом Telegram: таблица и её миграция;
@@ -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, архитектурный проход;
оракула у находки нет — случай выведен из кода и предела простоя, но не
воспроизведён.