- в наборе шесть задач: локаль TVDB и confidence-гейт под цель, плюс баги и техдолг помимо неё - взятым дописаны разделы своего типа: «Затрагивает», критерии с оракулами, воспроизведение - решены развилки: оба расхождения код↔спека правятся спекой (и потому стали chore), опрос qBittorrent тормозится бэкоффом до минутного потолка
4.8 KiB
🧹 Зафиксировать в спеке разделение Cancel и Dismiss по состояниям
- Тип: chore
- Категория: Ядро продукта
- Зачем: спека обещает dismiss из любого состояния, код осознанно даёт Cancel для активных и Dismiss для терминальных — расходится буква, а не поведение
Найдено аудитом capability state-reconciliation (сверка код↔спека). Пред-существующее, вне scope пачки lifecycle-задач.
Суть
Спека openspec/specs/state-reconciliation/spec.md (требование «Ручное
закрытие»): команда dismiss доступна из любого состояния кроме deleted
во всех транспортах, и переход SHALL помечаться error_code = user_dismiss.
В веб-UI danger-zone «Закрыть» (dismiss) гейтится только для терминальных
состояний: Dismissable = IsTerminal() && !deleted && !cancelled
(internal/httpapi/download.go:130, шаблон
web/templates/partials/download_main.html:99-112). Для НЕ-терминальных
(stuck, deferred, downloading, review) закрытие в UI идёт кнопкой
«Отменить» → Cancel (internal/worker/worker.go:973), которая пишет пустой
error_code, а не user_dismiss.
Насколько больно
Функционально сценарии проходят: Cancel тоже даёт cancelled и не трогает
файлы/раздачу, семантика для пользователя идентична. Состояния без доступного
«закрытия» нет (кроме deleted/cancelled). Теряется только маркер
user_dismiss в error_code — расхождение с буквой спеки и небольшая потеря
наблюдаемости (в аналитике/логах не отличить «пользователь закрыл активную» от
«пользователь отменил»). Отсюда низкий приоритет.
Решение (2026-08-06): B — привести спеку к коду
Разделение осознанное: Cancel — стоп-кран для активных состояний, Dismiss —
закрытие терминальных. Требование «Ручное закрытие» уточняется: на
не-терминальных состояниях закрытие из интерфейса реализуется Cancel'ом, и
называется, какой error_code при этом ожидается.
Вариант A (звать Dismiss из веб-UI на не-терминальных ради единого маркера)
отклонён: он меняет рабочее поведение ради маркера в диагностике.
Кода задача не трогает: наблюдаемое поведение остаётся прежним, меняется заявленное.
Затрагивает
openspec/specs/state-reconciliation/spec.md— требование «Ручное закрытие»: доступностьdismissпо состояниям и ожидаемыйerror_code;- дельта-спека change'а — сценарий закрытия не-терминальной загрузки из веб-UI;
internal/httpapi/download.go,web/templates/partials/download_main.html— только чтение, правок не предполагается.
Критерии приёмки
- Требование спеки различает
CancelиDismissпо состояниям и называетerror_codeдля каждого пути закрытия (оракул:openspec validate --strict). - В спеке есть сценарий «пользователь закрывает не-терминальную загрузку из
веб-UI» с исходом
cancelledи названнымerror_code(оракул: тот же прогон). - Гейт
Dismissableв коде и danger-zone шаблона остаются как есть (оракул:git diff --statв отчёте ревью — файлов подinternal/иweb/нет).
Ссылки
internal/httpapi/download.go:130— гейтDismissableweb/templates/partials/download_main.html:99-112— danger-zoneinternal/worker/worker.go:973—Cancel;:1001—Dismissopenspec/specs/state-reconciliation/spec.md— требование «Ручное закрытие»