# Ревью изменения `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` отказе отдал `
`), плюс `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` сделаны из разметки и текста конвенции.