- условие самообновления — доменный предикат store.State.IsObservable() вместо фазы catched; один поллер на поверхность, интервалы 5 с и 15 с - отказ тика отвечает 200 и самозавершающимся фрагментом с корневым id цели вместо 404/500, который htmx не свопит - заведён ADR-2026-08-10-observability-is-not-terminality, переписан раздел «Живой поллинг» в конвенции веб-UI
156 lines
14 KiB
Markdown
156 lines
14 KiB
Markdown
# Ревью изменения `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` сделаны из
|
||
разметки и текста конвенции.
|