# Ревью изменения `card-live-refresh` — сводный отчёт триажа

## Сводка

- **Размер / сложность / метка:** среднее / знакомое / `medium`.
- **Режим прогона:** по графу. База диффа `969926f`.
- **Гейт:** зелёный. `BASE=969926f task gate` прогнан проходом `autotests` независимо: 14 шагов
  `OK`, ни одного `SKIP`/`WARN`; `-race` реально исполнен, флаки-прогон побайтово совпал,
  diff-coverage 24/24, `gitleaks` по 187 коммитам чисто, `govulncheck` — 0 достижимых.
- **Находок на входе:** 15 (autotests 2, specs 3, code 7, basics 4) + 3 наблюдения «вне спеки»
  + 3 «дешевле переделать до мерджа». **На выходе:** 2 блокирующие + 4 «сейчас» + 4 гипотезы
  + 5 promote.

### План разметки с исходом по темам

| Тема | Дом | Глубина | Кто закрывает | Исход |
|---|---|---|---|---|
| requirements | `openspec/specs/{web-ui,live-status}` + дельты | разбор | `specs` | закрыта, 3 находки |
| autotests | CLAUDE.md «Гейт» | прогон | `autotests` | закрыта, гейт прогнан, 2 находки |
| conventions | `docs/conventions/{README,web-ui,logging}.md` | разбор | `code` | закрыта, 3 находки |
| architecture | `docs/architecture.md` + `passport.md` | разбор | `basics` | закрыта, 1 находка |
| security | `docs/security.md` | разбор | `basics` | закрыта, 0 находок, все 5 вопросов отвечены |
| operations | `docs/architecture.md` «Эксплуатация» + `docs/database.md` | разбор | `basics` | закрыта, 2 находки (обе понижены) |
| техника (без темы) | — | разбор | `code` | 4 находки, 3 дедуплицированы |

**Тем без отчёта нет.** Своих тем проекта план не называл.

### Сигнал о заниженной метке

`review-code` — сигнала нет, возражений против `medium` не подаёт. `review-basics` строки о
метке не прислал; «возражений нет» и «не проверял» по молчанию не различаются — сигнал по
этому проходу считается неполученным, а не отрицательным.

## Блокирует мердж

### 1. Тик страницы `/download/{id}` при отказе чтения оставляет её навсегда устаревшей и стучит в сервер до закрытия вкладки

- Файл: `internal/httpapi/download.go:84-102`; `web/templates/partials/download_main.html:2`
- Severity: major. Confidence: high.
- Оракул: временный падающий тест триажа (прогнан, файл удалён): `page tick (not found)
  status = 404, want 200`; `page tick (store failure) status = 500`; «ответ тика не несёт корня
  `#download-main`». Плюс дельта `web-ui`: требование написано для обеих поверхностей.
- Последствие: htmx не свопит 4xx/5xx и не снимает `hx-trigger` — страница показывает состояние,
  которого уже нет, человеку не сообщается ничего, вкладка стучит каждые 15 с бесконечно,
  добавляя строку ERROR за тик. До change это было незаметно: `SelfPoll` стоял только на
  короткоживущем `catched`, теперь наблюдаемых состояний 11.
- Найдено четырьмя проходами независимо (`specs`, `basics`, `code`, `autotests`).
- **Действие: развилка** (общая со следующей находкой).

### 2. Фрагмент отказа с чужим корневым `id` сносит `#review-main` и `#download-main` — экран мёртв до F5

- Файл: `internal/httpapi/live.go:175-182`; `web/templates/partials/frag_note.html:1`
- Severity: major. Confidence: high.
- Оракул: падающий тест триажа (тик `/review` при не-`ErrNotFound` отказе отдал
  `<article class="card" id="card-…">`), плюс `docs/conventions/web-ui.md:36-41` дословно:
  «корень `{{define}}` — это элемент с целевым `id` … если ответный фрагмент не несёт тот же
  корневой `id`, следующее действие/поллер не найдёт таргет».
- Последствие: `fragErr` зовут из шести мест с четырьмя разными целями свопа, а отдаёт он всегда
  карточку. Транзиентный `SQLITE_BUSY` на тике `recognizing` (самый частый тик проекта, 2 с)
  заменяет весь `#review-main` карточкой списка — экран ревью теряет якорь и все действия.
  **Регрессия против базы:** раньше 500 не свопился и экран оставался рабочим.
- **Действие: развилка** (тот же вопрос, отвечать один раз на обе).

## Стоит исправить сейчас

### 3. Тик страницы затирает то, что человек в этот момент читает

- Файл: `web/templates/partials/download_main.html:2,6,99-128`; `internal/httpapi/httpapi.go:515-526`
- Severity: minor. Confidence: high.
- Два проявления одной причины: (1) сообщение об отказе действия живёт ≤15 с и исчезает, а в
  `error_msg` штатный конфликт не пишется и в логе он `DEBUG` — причина не остаётся нигде;
  (2) раскрытая «Опасная зона» захлопывается каждый тик, пока человек читает текст про
  необратимое удаление раздачи с файлами. Пересечение `Dismissable` с `IsObservable` —
  `failed`, `orphaned`, `target_missing`.
- **Действие: развилка.**

### 4. Отказ на повторяющемся тике пишется ERROR — шторм в журнале ровно тогда, когда хранилищу плохо

- Файл: `internal/httpapi/live.go:139-143`, `internal/httpapi/live.go:175-180`
- Severity: minor. Confidence: high.
- Оракул: `docs/conventions/logging.md:164-171` дословно: «Повторяющийся сбой фонового цикла
  (поллинг/сверка) — `WARN`, не `ERROR` … уровень задаёт не текст ошибки, а наличие штатного
  ретрая».
- **Действие: инлайн.**

### 5. Конвенция и комментарии описывают поллер, снятый этим же диффом

- Файл: `docs/conventions/web-ui.md:104-129`; `internal/httpapi/live.go:121-124`;
  `internal/httpapi/httpapi.go:314-318`
- Severity: minor. Confidence: high.
- Раздел «Живой поллинг» утверждает три неверных вещи: пример разметки с `hx-*` в `progress`;
  «эталон — `progress`/`seeding`»; «данные тика — из in-memory снимка, без БД/сети на каждый
  тик». Следующий автор возьмёт за образец снятое и заведёт второй поллер на поверхность.
- **Действие: инлайн.**

### 6. Тест `TestFragProgressStopsWhenNotDownloading` больше не может упасть

- Файл: `internal/httpapi/live_test.go:35-48`
- Severity: minor. Confidence: high.
- `hx-*` сняты с партиала безусловно, поэтому проверка ложна при любом состоянии: тест зелен
  независимо от логики, которую называет. diff-coverage меряет исполнение, а не проверку.
- **Действие: инлайн.**

## Гипотезы без доказательства

- Тик списка шлёт N HTTP-запросов и 2N запросов в SQLite там, где полный рендер обходится одним
  батчем (`live.go:133-146`). Понижено: замера нет; по-карточное обновление заказано дельтой,
  запрос идёт по индексу `idx_file_link_download`. Остаётся верным одно: арифметика Р4 в
  `design.md` занижена по числу обращений к БД.
- Отказ чтения размеров подменяет известный размер прочерком (`live.go:136-144`). Понижено:
  дельта про отказ вспомогательного чтения молчит — вопрос к тексту дельты, не дефект кода.
- Перечень «кого фон возвращает сам» разошёлся на три ручные копии
  (`store/download.go:67-84`, `worker/reconcile.go:22-26`). Понижено: расхождения и последствия
  сегодня нет, риск чисто будущий.
- `pollFast = "5s"` — второй дом настройки `[worker].poll_interval`. Понижено: связь держится на
  прозе, сегодняшнее значение верно.

**Отсеяно как вкусовщина:** «имя `IsObservable` и комментарий расходятся с поведением на
`review`/`deferred`» — предикат ровно такой, каким его определила дельта-спека поимённо.

**Проектных ложноположительных не сработало.**

## Promote candidates

- Правило «ответ-фрагмент несёт корневой `id` того узла, в который свопится» — записано в
  конвенции, но не механизировано; кандидат в табличный тест или `internal/archrules`.
- Задача на уборку маршрутов-гасителей `/fragments/downloads/{id}/progress` и `/seeding`.
- Правило «константа, дублирующая значение настройки конфига, считается из конфига».
- Тест-связка `worker.desyncStates` ↔ `store.selfHealingStates`.
- Правило «тест, который не может упасть, — дефект теста»; класс ловится мутационной проверкой,
  diff-coverage его не видит по устройству.

## Границы покрытия

- Запускались на метке `medium`, режим «по графу»: `autotests`, `specs`, `code`, `basics`. Все
  четыре вернули отчёт. Триаж — сток.
- **Не запускались** (нет на `medium`): враждебный проход с построенным путём атаки,
  эксплуатационный постмортем с замерами, независимая реализация. Их даёт только `large`.
- **Потолки:** `code/conventions` 3 из 4, срез не сработал; `basics` 4 из 4 — срез сработал, за
  ним осталось наблюдение про ERROR на каждом тике (выведено отдельной находкой) и три пункта
  «дешевле переделать до мерджа». `specs`, `autotests`, `code/техника` потолков не сообщили —
  это находка о самом прогоне.
- **На `small` и `medium` ничего не проверяется запуском сверх гейта:** построенный путь атаки,
  поведение библиотеки и драйвера в вырожденном случае, любые числа (время удержания блокировки,
  пик кучи, темп роста журнала). `basics` задаёт часть тех же вопросов чтением — его ответы
  слабее и выше гипотезы не поднимаются.
- **Решения проекта не сверялись:** `docs/adr/` — процессный документ, прогон его не открывает;
  расхождение с записанным решением ловит `av-dev-docs:healthcheck`.
- **Записанные наблюдения не использовались:** `docs/research/` — тоже процессный; всякое число
  в отчёте снято на этом прогоне.
- **Поимённая сверка с руководствами по стилю Go не задавалась ни одним проходом** — проход
  `idiom` упразднён (ADR-2026-08-04).
- **Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.**
- Ни один тик в этом прогоне не исполнялся браузером: выводы о свопе и `hx-trigger` сделаны из
  разметки и текста конвенции.
