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

14 KiB
Raw Blame History

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