# 🧹 Зафиксировать в спеке разделение 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` — гейт `Dismissable` - `web/templates/partials/download_main.html:99-112` — danger-zone - `internal/worker/worker.go:973` — `Cancel`; `:1001` — `Dismiss` - `openspec/specs/state-reconciliation/spec.md` — требование «Ручное закрытие»