ingest: закрыты мелочи приёма — вырожденное имя, контракт Result, корреляция add

- имя раздачи нормализуется на границе разбора: вырожденное `-`
  (metainfo.NoName) даёт пустое имя, пробельное схлопывается — сентинел больше
  не доходит ни до контекста распознавания, ни до source_ref, ни до подсказки
  вывода имени
- контракт «на любом пути ошибки приёма результат нулевой» объявлен в ingest и
  удерживается структурно; три транспорта перестали обещать идентификатор,
  которого нет, и коррелируют отказ по request_id
- scoped-логгер загрузки ставится до вызова внешнего сервиса в семи командах
  воркера — записи об отказе qBittorrent и метабаз получили download_id
  и infohash; граница разбора bencode записана в docs/research
This commit is contained in:
av
2026-08-06 18:20:12 +03:00
parent 52615e4e49
commit d081ef1d30
30 changed files with 2190 additions and 63 deletions
@@ -0,0 +1,137 @@
# Чекпоинт 1 — ревью предложения, профиль `design`
- **Change:** `ingest-nits`
- **База диффа:** `master` (`5c79fdfffe9e2ddc78b7ebf9f4cdc3aa83560b2b`); кода на
ветке на момент прогона не было.
- **Профиль:** `design`. **Режим прогона:** `по графу` (все три прохода одной
волной — граф профиля плоский, машину не держит никто).
- **Дата:** 2026-08-06.
## Запущенные проходы и исход
| Проход | Исход |
| --- | --- |
| `review-specs` | 6 находок (2 `major`, 4 `minor`) + 5 границ спеки |
| `review-rubric` (фаза 1) | рубрика из 12 свойств, 8 находок (2 `major`, 6 `minor`), 5 promote-кандидатов |
| `review-architecture` | 2 находки (обе `major`) + 3 пункта «дешевле переделать до мерджа» |
Триаж на этом чекпоинте не запускается — так устроен профиль: находок единицы,
каждая либо правит спеку, либо становится записанным вопросом. Сведение сделал
оркестратор пайплайна.
## Что отработано инлайн
Дедуплицировано по причине; в скобках — кто нашёл.
1. **Фактическое основание N1 было неверным** (`specs`, `major`, оракул —
прогон крафт-входов через `metainfo.Load`). Библиотека сентинел `-` не
синтезирует: `BestName()` на раздаче без имени возвращает пустую строку, а
`NoName` присваивается только при авторинге. Значит `-` доходит до нас лишь
от раздачи, объявившей его полем `name`. Следствия: критерий приёмки
постановки выполнялся **до** изменения; случай «раздача без `name`» не был
покрыт вовсе; неверный комментарий в `internal/torrent/torrent_test.go:224` и
есть источник исходной ошибки ревью 2026-07-08. → требование, дизайн и
предложение переписаны по наблюдению; два входа разведены сценариями и
тестами; комментарий теста правится.
2. **Требование `identity` заявляло больше, чем change делает** (`architecture`
и `specs`, оба `major`): обязанность формулировалась как «до первого вызова,
способного что-либо записать», и поимённо называла `cancel`/`dismiss`,
которые change сознательно не трогает. Нормативная спека стала бы ложной в
момент архивации. → обязанность сужена до **вызова внешнего сервиса**.
3. **`request_id` вменялся всем транспортам, а существует только на HTTP**
(`architecture` `major`, `specs` `minor`, `rubric` `major`). → требование
называет ключ поимённо по транспортам; для Telegram зафиксировано отсутствие
ключа как сегодняшнее состояние, вопрос записан в `design.md` → Open
Questions. Регрессии нет: `res.DownloadID` уже сегодня всегда пуст.
4. **Telegram числился среди `ext.*`-клиентов, которых у него нет** (`specs`,
`minor`). → убран; перечень клиентов не дублируется, а отдан конвенции.
5. **Третий вызов приёма (`handleUIAdd`, веб-форма) отсутствовал в задачах**
(`specs`, `minor`). → добавлен пункт 2.4; иначе требование было бы выполнено
на две трети.
6. **Заявленная однородность с фильтром magnet-заглушек ложна** (`specs`,
`minor`): у magnet заглушка `dn` фильтруется только в контексте, а в
подсказку имени уходит. → различие названо явно, распространение на magnet
объявлено отдельным изменением.
7. **Спека утверждала абсолют «ниже по потоку сентинел не встречается», а путь
файла его выпускает** (`rubric`, `minor`). → нормализуемые поля перечислены
поимённо, про пути файлов сказано, почему они не нормализуются.
8. **Имя с переводом строки подменяет строки построчного контекста** (`rubric`,
`major`). → нормализация расширена на разделители строк и краевые пробелы
тем же `oneLine`, что уже применён к комментарию. В требовании прямо сказано,
что защитой от инъекции в промпт это **не** является: пользовательский текст
многострочен по замыслу.
9. **Контракт «результат пуст» держался перечнем веток** (`rubric`, `minor`). →
удерживается структурно: именованный возврат + один `defer`; тест сравнивает
результат с нулевым значением целиком.
10. **У третьего потребителя `DisplayName` (подсказка namer) не было оракула**
(`rubric`, `minor`). → добавлен пункт 1.5, тест в `internal/worker`.
11. **Тест записи о `Fails.` проверял только наличие полей** (`rubric`,
`minor`). → добавлена негативная половина: `passkey` отправленного magnet в
записи не встречается. Инвариант «секреты не в логи» — `major`.
12. **Сценарий требовал `infohash` там, где тело требования смягчало до «когда
известен»** (`specs`, граница спеки). → в GIVEN добавлено «с известным
инфохэшем».
13. **Каталог `docs/research/` тихо расширялся с чужих данных на чужой код**
(`architecture`). → вводная README расширяется, записка получает условие
устаревания.
## Записанные вопросы
Оба — в `design.md` → Open Questions, оба с вариантами, ценой и рекомендацией.
1. Нужен ли Telegram-транспорту собственный корреляционный ключ у отказа приёма.
Рекомендация — поднять запись отказа разбора до `INFO` с `infohash` отдельной
задачей, а не заводить второй канал корреляции.
2. Приводить ли `Cancel`/`Dismiss` к форме `ctx = w.scoped(…)`. Рекомендация —
отдельной задачей-гигиеной, когда в них появится первый внешний вызов.
## Урожай (заведение задач — не работа пайплайна)
- **Заглушка `dn` вида `*-topic-<id>` уходит в подсказку вывода имени**, хотя из
контекста отфильтрована (`worker.sourceAddParts``magnet.Parse`
`DisplayName`, `internal/worker/worker.go:626`). Симметрично N1, но на
magnet-ветке. Правка требует изменения действующего требования «Синтез
контекста распознавания из полей magnet» (сценарий «Строки-факты не становятся
отображаемым именем»), поэтому в границы этой задачи не входит. Оракул: тест
на подсказку для голого magnet с заглушкой. Провенанс: `specs`, change
`ingest-nits`.
- **`infohash` в записи о внешнем вызове снимается со снимка загрузки, а не с
того, с чем вызов ушёл** (`rubric`, `low`). Сегодня значения совпадают;
расхождение возможно у загрузки с несколькими хешами (v1+v2). Класс совпадает
с журналом 2026-08-06 (признак снят с одной сущности, действие применено к
другой), но стоит здесь только разбора, не данных.
- **Пустой `source_ref` у torrent-загрузки** возможен: `.torrent` с именем `-` и
без имени присланного файла. Действующее требование велит `source_ref` быть
человекочитаемым референсом, но случая «нет ни того, ни другого» не описывает.
Дыра не новая, но изменение проходит ровно по ней. Провенанс: `specs`,
границы спеки.
- **Отказ разбора источника пишется на `DEBUG`** (`internal/ingest/ingest.go`),
то есть на боевом `INFO` не пишется вовсе — для Telegram-пути это означает, что
у отказа приёма нет ни ключа у пользователя, ни записи в журнале. Связано с
записанным вопросом 1.
- **Promote candidates** (`rubric`): (а) «scoped-логгер загрузки строится один
раз, на входе публичной команды воркера» → `docs/conventions/logging.md` плюс
механизация тестом-перебором; (б) «нормализация недоверенного имени на границе
— это вырожденные значения, краевые пробелы и разделители строк» →
`docs/review.md`, «Парсер недоверенного входа»; (в) «у каждого транспорта
назван свой корреляционный ключ публичного канала» →
`docs/conventions/errors.md`; (г) «тест клиента внешнего сервиса проверяет и
отсутствие секретосодержащих значений» → `docs/conventions/logging.md`.
## Границы покрытия
- **Гейт на этом чекпоинте не запускался** — кода на ветке не было; это
устройство профиля `design`, а не пропуск.
- **Триаж не запускался** — в профиле `design` его нет по построению; сведение
и дедупликацию находок сделал оркестратор пайплайна, то есть **тот же, кто
писал предложение**. Разведённости приёмщика и исполнителя на этом чекпоинте
нет.
- **Не запускались** проходы `code`, `adversary`, `ops`, `reimpl` — они не
входят в профиль `design` и работают после apply (чекпоинт 2).
- **Замер аллокаций bencode** на этом чекпоинте не воспроизводился ни одним
проходом: `specs` проверил только провенанс ссылок по исходникам библиотеки.
Сам замер — задача 4.1, его исход проверяет чекпоинт 2.
- **Ни один проход не открывал живой qBittorrent, LLM и метабазы** — запрещено
`CLAUDE.md` → «Запреты».
- **Ценность самой постановки** («нужно ли закрывать эти четыре нити») ревью не
оценивает ни в одном профиле.