Files
jellybit/openspec/changes/archive/2026-08-10-card-live-refresh/review/report.md
T
av a5d873b62d web-ui: карточка и страница обновляются, пока задачу может двигать фон
- условие самообновления — доменный предикат store.State.IsObservable() вместо
  фазы catched; один поллер на поверхность, интервалы 5 с и 15 с
- отказ тика отвечает 200 и самозавершающимся фрагментом с корневым id цели
  вместо 404/500, который htmx не свопит
- заведён ADR-2026-08-10-observability-is-not-terminality, переписан раздел
  «Живой поллинг» в конвенции веб-UI
2026-08-10 14:02:38 +03:00

156 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ревью изменения `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` сделаны из
разметки и текста конвенции.