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` → «Запреты».
- **Ценность самой постановки** («нужно ли закрывать эти четыре нити») ревью не
оценивает ни в одном профиле.
@@ -0,0 +1,350 @@
# Чекпоинт 2 — ревью изменения после apply, профиль `deep`
- **Change:** `ingest-nits`
- **База диффа:** `master` (`5c79fdfffe9e2ddc78b7ebf9f4cdc3aa83560b2b`);
изменения на ветке `task/ingest-nits` не закоммичены.
- **Профиль:** `deep`. **Режим прогона:** `по графу`.
- **Гейт:** ЗЕЛЁНЫЙ — 12/12 шагов `OK`, ни одного `SKIP`/`WARN`; `-race`
реально прогнан; diff-coverage 81 % (7 изменённых строк не покрыты — они и
стали находками F1/F2).
- **Дата:** 2026-08-06.
## Запущенные проходы и исход
Состав сверен с профилем `deep`: все семь стадий запускались, расхождений с
профилем нет.
| Проход | Исход |
| --- | --- |
| `review-gate` | отработал: 2 находки (обе `major`, пробелы покрытия изменённых строк) |
| `review-specs` | отработал: 5 находок + 3 границы спеки |
| `review-code` | отработал: 0 находок (нарушений записанных конвенций нет), 1 promote-кандидат |
| `review-adversary` | отработал: 4 находки + 1 гипотеза; фаззинг `torrent.Parse`+`Context()` 6.1 млн исполнений — паник нет; утечек экранирования нет |
| `review-ops` | отработал: 5 находок (3 с замерами); гипотезы «ERROR-шторм», «сирота при отмене ctx», «kill посреди раскладки» сняты проверкой |
| `review-reimpl` | отработал: 3 находки + перечень мест, где код лучше независимой реализации |
| `review-architecture` | отработал: 2 находки + 3 пункта «дешевле переделать до мерджа»; новых понятий change не вводит |
На входе — 21 находка + 1 гипотеза + 3 «дешевле до мерджа» + 1
promote-кандидат. После дедупликации по причине — 16 причин. В основном списке —
6 пунктов (потолок 7 соблюдён); остальное — в гипотезах, promote и урожае,
ничего не выброшено молча.
Дедупликация: S2 = A1 = R1 (один дефект порядка нормализации, найден тремя
проходами независимо — оракул есть, это повысило приоритет, не confidence);
S1 = R2 = AR1 (ложность требования `identity`; перечни команд расходились —
6 против 3, точный перечень установлен триажем по коду: 6); G1 + S5 (один
пробел — отказ приёма на HTTP-границе без оракула).
Отчёт чекпоинта 1 (`checkpoint-1-design.md`) учтён: находки, закрытые там
инлайн, повторно не поднимались; урожай и записанные вопросы не дублируются.
---
## Блокирует мердж
### B1. Имя, схлопывающееся в `-`, проходит нормализацию: `source_ref` и контекст получают сентинел, а фолбек на имя файла регрессировал против master
- Файл: `internal/torrent/torrent.go:94-99`
- Severity: major (сломано требование дельта-спеки этого же change —
«Вырожденное имя раздачи не считается именем», сценарий «Вырожденное имя даёт
source_ref из имени файла»)
- Confidence: high
- Оракул: падающий тест — прогнан триажем:
`tmp/_reimpl/probe/probe_test.go` даёт
`name " - " → DisplayName "-"`, `"-\n" → "-"`, `"\t-" → "-"`
(ожидалось `""`); `Context()` первой строкой несёт `-`. Второй независимый
падающий тест — у прохода `adversary`. Регрессия: на `master`
`internal/ingest` делал `TrimSpace` + сравнение с `-` (`git diff` по
`ingest.go:198-205`), и фолбек на имя присланного файла срабатывал; теперь
`source_ref` становится `-`.
- Причина: в `displayName` сравнение с `metainfo.NoName` стоит **до**
`oneLine`, а `oneLine` (`strings.Fields`) схлопывает `" - "`, `"-\n"`,
`"\t-"` ровно в `-`.
- Последствие: молчаливое — ошибки нет, просто карточка и `source_ref` несут
`-`, контекст распознавания получает строку-мусор вместо отсутствия строки
названия. Ровно тот класс входа, который change обещал закрыть.
- Предложение: в `displayName` сначала `oneLine`, потом сравнение с
`metainfo.NoName` (порядок независимой реализации `reimpl`); тест-входы
`" - "`, `"-\n"`, `"\t-"` в `internal/torrent/torrent_test.go`; в
дельта-спеке зафиксировать порядок (нормализовать → сравнить).
- Найдено проходами: `specs` (S2), `adversary` (A1), `reimpl` (R1)
- Действие: **инлайн**
### B2. При архивации нормативная спека `identity` станет ложной: шесть команд воркера зовут внешний сервис до scoped-логгера
- Файл: `internal/worker/review.go:410/417, 442/445, 465/468, 796, 849;
internal/worker/review.go:734→796`; `openspec/changes/ingest-nits/specs/identity/spec.md:12-17`;
`openspec/changes/ingest-nits/proposal.md:36-38`
- Severity: major
- Confidence: high
- Оракул: поимённое построение путей вызовов, проверено триажем по коду:
1. `Relink` — `ensureSourceReady` (`review.go:410`) → `torrentByInfohash` →
`qbt.Torrents` **до** `ctx = w.scoped(...)` (`:417`);
2. `Rerecognize` — `:442` до `:445`;
3. `Refine` — `:465` до `:468`;
4. `ChooseCandidate` → `chooseCandidateLocked` (`:796`) —
`recognizer.Director` → `metadata` (HTTP) с необогащённым `ctx`
(команда вообще не делает `ctx = w.scoped`, только точечные
`logctx.From(w.scoped(...))` на своих записях);
5. `AddManualSource` — тот же путь через `:738`;
6. `SetProviderID` — `Director` на `:849` с необогащённым `ctx`.
Клиенты metadata логируют через `logctx.FromOr(ctx, ...)`
(`internal/metadata/http.go:78`, `tvdb.go:101,128`) — записи уходят без
`download_id`/`infohash`. Требование дельты (`identity/spec.md:13-15`):
«операция … делающая вызов внешнего сервиса, SHALL положить scoped-логгер …
до такого вызова — включая команды, пришедшие с транспорта». `proposal.md:36-38`
утверждает, что `Retry` — «единственная команда воркера», не кладущая
scoped-логгер: ложь, уедет в архив. Остальные команды (`Apply`, `Undo`,
`Delete`, `Defer`, `RefreshDisplayName`) проверены — они требование соблюдают;
`Cancel`/`Dismiss` внешних вызовов не делают.
- Последствие: класс «молчание» — сбой внешнего сервиса с этих шести путей даёт
запись, которую фильтр по id загрузки не находит; спека, ставшая нормативной,
утверждает обратное, и следующая задача будет строиться на ложном тексте.
- Найдено проходами: `specs` (S1), `reimpl` (R2), `architecture` (AR1)
- Действие: **развилка**. Вопрос: как разрешить расхождение требования
`identity` с кодом?
- (а) сузить требование до фактически закрытого пути (`Retry` + фоновый
цикл), убрав «включая команды, пришедшие с транспорта» из нормы в цель.
Цена: спека честна, но шесть команд остаются без корреляции внешних
вызовов, вопрос вернётся на следующем ревью identity.
- (б) починить шесть команд в этом change: `Relink`/`Rerecognize`/`Refine` —
перенести `ctx = w.scoped(...)` выше `ensureSourceReady`;
`ChooseCandidate`/`AddManualSource`/`SetProviderID` — обогатить `ctx` на
входе (форма `Retry`/`Delete`). Цена: ~6 однотипных мелких правок + тесты
по образцу нового probe-хендлера `torrent_add_test.go`; scope растёт
умеренно, инвариантов не трогает.
- (в) = (б) сейчас + отдельная задача в беклог на обёртку
`w.withDownload(...)`, схлопывающую однородный пролог ~18 команд и делающую
пропуск невозможным по построению (рекомендация `architecture`).
Рекомендация проходов — (в). В любом варианте `proposal.md` («единственная
команда») требует правки — при (б)/(в) формулировкой «был не единственным»,
при (а) — снятием претензии.
## Стоит исправить сейчас
### F1. Сценарий «Отказ приёма на HTTP-границе» остался без оракула: ветка ошибки веб-формы не исполняется ни одним тестом
- Файл: `internal/httpapi/httpapi.go:437` (изменённая строка); дельта-спека
`ingest`, сценарий «Отказ приёма на HTTP-границе»
- Severity: major
- Confidence: high
- Оракул: `grep request_id internal/httpapi/*_test.go` — пусто; ни один тест не
делает `POST /ui/downloads` (форма добавления): проверено триажем — тесты
ходят только в `/ui/downloads/{id}/...`. REST-ошибки приёма покрыты
(`httpapi_test.go:138,154`), но `request_id` в ответе не проверяет и REST.
Асимметрия: Telegram-путь получил новый тест, веб-форма — нет.
- Последствие: THEN-обещание дельта-спеки (ответ несёт `request_id`, а не
идентификатор загрузки) не проверяет никто; регрессия на этой строке будет
молчаливой.
- Предложение: тест на `handleUIAdd` с `fakeIngestor.err` (редирект с ошибкой,
без download_id) + проверка `request_id` в теле/заголовке отказа на обоих
HTTP-транспортах.
- Найдено проходами: `gate` (G1), `specs` (S5)
- Действие: **инлайн**
### F2. Best-effort-ветки `Retry` (откат активации, сбросы базиса и счётчика) переписаны диффом и не исполняются тестами
- Файл: `internal/worker/worker.go:1085, 1093, 1103, 1111`
- Severity: major
- Confidence: high
- Оракул: diff-coverage гейта — 4 из 7 непокрытых изменённых строк; `fakeStore`
не умеет ронять `SetDownloadState`/`SetRetriedAt`/`SetSourceMissCount`.
Строки в диффе: `w.log.Error(...)` заменён на `logctx.From(ctx)...` (проверено
`git diff`).
- Последствие: пути отката «активация прошла, add не удался» держат инвариант
«задача не качается без раздачи в qBittorrent» — и не проверяются; молчаливая
регрессия отката оставила бы задачу в `downloading` без источника.
- Предложение: научить `fakeStore` инъекции ошибок этих трёх сеттеров; тесты:
откат возвращает прежнее состояние/код/сообщение, сбой best-effort-сеттеров
не проваливает состоявшийся retry (только WARN).
- Найдено проходом: `gate` (G2)
- Действие: **инлайн**
### F3. Записка research обещает несуществующий текст замера и переоценивает риск: мерился `TotalAlloc`, а физический RSS почти не растёт
- Файл: `docs/research/torrent-bencode-limits.md:22-26, 49-53`
- Severity: minor
- Confidence: high
- Оракул: (1) записка обещает «текст — в этом change,
`openspec/changes/archive/ingest-nits/`» — в каталоге change программы нет
(проверено листингом; в `tmp/` лежит только `bencode-alloc.out`), нарушено
правило воспроизводимости `docs/research/README.md`; (2) замер `ops`:
`make([]byte, 128MiB)` без касания страниц → +800 КБ RSS; деградационный путь
(`make` → проваленный `io.ReadFull` на 36-байтовом входе) страниц не касается;
concurrency=1000 → пик RSS 7 352 КБ. Вывод «N одновременных дают до
N × 128 MiB» относится к виртуальным аллокациям, не к памяти процесса.
- Последствие: будущий читатель примет решение (лимиты, семафор приёма) по
завышенной оценке риска; невоспроизводимость обесценивает записку как
наблюдение.
- Предложение: вложить текст программы в записку (или в change) и дописать
разграничение `TotalAlloc` vs RSS с числами замера `ops` — вывод «принято как
есть» усиливается, а не смягчается.
- Найдено проходами: `adversary` (A4), `ops` (O4)
- Действие: **инлайн**
### F4. Дельта-спека `ingest` в двух местах описывает не то поведение, которое реализовано и принято
- Файл: `openspec/changes/ingest-nits/specs/ingest/spec.md:28-30, 32-35`
- Severity: minor
- Confidence: high
- Оракул: (1) спека: нормализация схлопывает «любые разделители строк и краевые
пробелы»; код: `oneLine` = `strings.Join(strings.Fields(s), " ")`
(`torrent.go:234-236`) — любые пробельные последовательности, включая
внутренние табы/пробелы. Поведение кода корректно — неверен текст спеки.
(2) спека: пути файлов «не выводятся из имени»; код: `files()` при пустом
пути делает `p = meta.BestName()` (`torrent.go:114-116`) — сырое имя.
Решение не нормализовать пути остаётся верным (они только сигнал, целевые
пути строятся из ответа qBittorrent) — ложно только основание.
- Последствие: нормативный текст разойдётся с кодом в момент архивации; при B1
спека всё равно правится — дешевле сделать одним заходом.
- Предложение: (1) сформулировать правило как «любые пробельные
последовательности схлопываются в один пробел»; (2) заменить основание
исключения путей на честное (путь может совпасть с именем при пустом
`fi.Path`; не нормализуются, потому что служат только сигналом).
- Найдено проходом: `specs` (S3, S4)
- Действие: **инлайн**
## Гипотезы без доказательства
- **`naming.sanitize` не режет разделители пути, а выведенное имя уходит в
qBittorrent параметром `rename`** (`adversary`; заявлен `major`,
confidence medium — остаётся гипотезой). Неизвестно, применяет ли qBittorrent
`rename` к папке на диске — если да, крафт-имя могло бы дать запись по
произвольному относительному пути под `paths.downloads` (тень инварианта
«источник неприкосновенен»). Оракула нет и не будет в конвейере: ходить в
живой qBittorrent запрещено (`CLAUDE.md` → «Запреты»). Диффом не внесено
(`Retry` namer не зовёт — имя выводится при первом добавлении). Предложение:
задача в беклог — интеграционный тест за env-гейтом
(`*_integration_test.go`) на поведение `rename` c `/` и `..` в имени; до
ответа — дешёвый пояс: резать разделители пути в подсказке имени.
## Promote candidates
- **`docs/conventions/errors.md`: корреляционный ключ для транспорта без
понятия запроса.** Конвенция описывает трансляцию ошибок через `request_id`,
но Telegram запроса не имеет — правило «у каждого транспорта назван свой
корреляционный ключ публичного канала» просится в конвенцию (найдено
`code`; пересекается с Open Question 1 в `design.md` и promote-кандидатом
(в) чекпоинта 1 — при заведении объединить).
- **`docs/conventions/logging.md`: команда воркера не дублирует поля
scoped-логгера.** После `ctx = w.scoped(...)` отложенный
`logCmd(ctx, cmd, id, err)` пишет `download_id` и из логгера, и явным
аргументом — дублирующийся ключ в JSON-записи `command failed` (найдено
`adversary`, A3; путь `retry` — в диффе, та же пара давно у `Delete`).
Как находка это nit без записанной конвенции — потому promote: правило +
возможная механизация тестом-перебором команд (родственно promote (а)
чекпоинта 1).
## Урожай
Отложено: вне диффа, ниже порога либо требует отдельной задачи. Формат:
формулировка — оракул — провенанс.
1. **`recognizePending` при недоступном qBittorrent не обрывается по первому
сбою**: каждая `completed`-задача уезжает в `review` с диагностикой про
qBittorrent — тест с фейковым недоступным qbt и 2+ задачами — `ops` (O1),
вне диффа.
2. **`SupersedeForeignLinks` — `SCAN file_link` без индекса под `w.mu`**:
`EXPLAIN QUERY PLAN` + замер ~24 мс на 200k строк, растёт линейно, ретеншена
нет — `ops` (O2), вне диффа; задача: индекс или ретеншен.
3. **`Retry` держит глобальный `w.mu` через оба внешних вызова; таймаут
qbt-клиента (30 с дефолт) не настраивается конфигом**: замер — параллельный
`Cancel` другой загрузки ждал 282 мс при qbt 300 мс — `ops` (O3), диффом не
внесено (присвоение передвинуто внутри той же секции); связать с задачей на
`w.withDownload` из B2-(в) — per-download блокировка там же.
4. **`source_ref` не ограничен по длине**: `.torrent` 7 МиБ с именем 7 МиБ даёт
`source_ref` 7 340 032 байта при соседе `capContext` 16 КиБ; читается
`SELECT *` на каждом тике — замер `adversary` (A2); строка переписана диффом,
но поведение не новое; задача: кап по образцу `capContext`.
5. **Пустые `source_ref`+`display_name`: веб-UI рисует пустой заголовок
карточки, Telegram — `#id`**: чтение кода (`httpapi/download.go:71-81` vs
`tgbot/render.go:187-205`) — `ops` (O5), вне диффа; связан с записью урожая
чекпоинта 1 «пустой source_ref возможен» — закрывать одной задачей.
6. **Два рукодельных slog-хендлера родились в одном change**
(`captureHandler` в `internal/qbt/qbt_test.go`, `probeHandler` в
`internal/worker/torrent_add_test.go`), уже разошлись возможностями —
`architecture` (AR2); порог превращения в находку — третий потребитель.
7. **Нормализация комментария осталась в `Context()`, имени — в `Parse`**,
хотя спека подаёт правило как общее; сегодня `Info.Comment` вне пакета никто
не читает — `reimpl` (R3, confidence low); при следующей правке пакета —
`Comment: oneLine(mi.Comment)` в `Parse` либо оговорка в спеке.
8. **Доккомментарии** (по одной строке, «дешевле до мерджа» от `architecture`;
оркестратор может сделать заодно с B2/F4): `ingest.Ingest` цитирует
требование по русскому имени — при переименовании требования ссылка
оборвётся молча; `magnet.Info.DisplayName` не оговаривает, что оно НЕ
нормализуется, в отличие от `torrent.Info.DisplayName`.
9. **Приоритет `NameUtf8` над `Name` не описан спекой** — граница спеки —
`specs`; при случае — строкой в требование нормализации.
10. **Запись отказа приёма — `DEBUG`, на боевом `INFO` не пишется вовсе**, а
требование отправляет искать диагностику Telegram-отказа по этой записи —
`specs`; уже записано вопросом 1 в `design.md` → Open Questions с
рекомендацией поднять до `INFO` отдельной задачей — не дублировать.
Отсев вкусовщины: типовых generative-находок (переименования, перестановки,
«вынести в файл», обобщение частного случая) в выводах не оказалось; единственный
кандидат — A3 (дублирующийся ключ) — переведён в promote, т.к. записанной
конвенции под nit нет. Ни одна находка не попала под «Типовые ложноположительные»
`docs/review.md` — раздел просмотрен поимённо (никто не предлагал авторизацию,
интерфейсы под моки, ретраи в тике и т.п.).
## Границы покрытия
**Прогон.** Профиль `deep`, режим «по графу», чекпоинт 2. Запускались все семь
проходов профиля (перечень с исходами — в сводке); не запускался никакой — состав
полный. На чекпоинте 1 (профиль `design`) шли `review-specs`, `review-rubric`,
`review-architecture`; `rubric` на чекпоинте 2 не запускается по построению
профиля.
**Что каждый запущенный проход не мог проверить в принципе (из charter'ов):**
- `gate` — только механизируемое с объективным оракулом; осмысленность тестов
и спек не видит.
- `specs` — сверяет спеку с кодом; поведение, отсутствующее в обоих, не найдёт.
- `code` — только записанные конвенции; «хорошо ли это» вне них не судит.
- `adversary` — не ходил в живой qBittorrent (запрет `CLAUDE.md`) — гипотеза
про `rename` осталась гипотезой; фаззинг ограничен бюджетом 45 с; целевой
путь из имени раздачи не строится, поэтому главный вопрос раздела «Вопросы к
проходам» (выход за библиотеку через имена) на этом диффе не применим.
- `ops` — реального профиля нагрузки umbar нет ни у кого; замеры синтетические
(200k строк, concurrency=1000), боевые числа могут отличаться.
- `reimpl` — переписывал только затронутые нити; расхождение вне них не
обнаруживает.
- `architecture` — судит по карте и диффу, код не исполняет.
- Триаж — ничего нового не ищет по определению; пропуск любого прохода — его
пропуск; здесь пропусков состава нет.
**Осталось целиком на человеке.** Два списка из `docs/review.md` →
«Недоступно проверке», раздельно.
*Не проверит ни один проход (принципиальная граница):*
- история инцидентов на umbar и что уже ломалось в проде;
- поведение SQLite под реальным объёмом и профилем нагрузки;
- завязка внешних потребителей (Jellyfin, закладки, чужие ссылки) на текущее
поведение;
- качество распознавания как таковое (корпус решено не собирать,
`tasks/REJECTED.md`, 2026-08-06);
- суждение «этой функциональности не должно существовать».
*Перестали проверять сознательно (пересматривается первым при промахе):*
- идиоматичность Go — с 2026-08-04, проход `idiom` упразднён при переезде на
плагин `av-dev-pipeline`; различение «идиоматично против распространено» не
спрашивает никто; пересмотр — задача `quality-review-agents`.
Плюс общее для любого прогона: поведение под реальным потоком; поведение
внешних систем в их боевых версиях (в этом чекпоинте конкретно — реакция
qBittorrent на `rename` с разделителями пути и физическое поведение
`deleteFiles=true`); история инцидентов.
**Документы проекта.** Всех нужных хватило: `CLAUDE.md` с разделом инвариантов
(severity брались оттуда, а не выводились), `docs/review.md` с «Типовыми
ложноположительными» (отсев шёл по ним) и обоими подразделами «Недоступно
проверке», конвенции, `docs/research/`, дельта-спеки change. Одна оговорка:
журнал дефектов содержит единственную запись (2026-08-06, про уборку торрента) —
оракулов-прецедентов для классов находок этого чекпоинта в нём нет, подтверждение
«такое здесь уже воспроизводилось» было недоступно; все оракулы добывались
тестами и замерами прогона.
**Потолок.** В основной список не влезли и уехали в урожай десять пунктов —
все перечислены поимённо выше, молча не выброшено ничего.